Skip to content

Commit

Permalink
feat: omnichain pending reserved tokens hook and element (#4592)
Browse files Browse the repository at this point in the history
  • Loading branch information
johnnyd-eth authored Feb 2, 2025
1 parent 89a97a9 commit cc0c74e
Show file tree
Hide file tree
Showing 5 changed files with 164 additions and 44 deletions.
3 changes: 3 additions & 0 deletions src/locales/messages.pot
Original file line number Diff line number Diff line change
Expand Up @@ -2663,6 +2663,9 @@ msgstr ""
msgid "Recipients will recieve payouts in ETH"
msgstr ""

msgid "Send reserved {tokenTextPlural} on {0}"
msgstr ""

msgid "{0} must be numeric"
msgstr ""

Expand Down
69 changes: 69 additions & 0 deletions src/packages/v4/hooks/useSuckersPendingReservedTokens.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import {
SuckerPair,
readJbControllerPendingReservedTokenBalanceOf,
readJbDirectoryControllerOf
} from "juice-sdk-core";
import { useJBChainId, useJBContractContext, useSuckers } from "juice-sdk-react";

import { useConfig } from "wagmi";
import { useQuery } from "wagmi/query";
import { wagmiConfig } from "../wagmiConfig";

export function useSuckersPendingReservedTokens() {
const config = useConfig();

const chainId = useJBChainId();

const { projectId } = useJBContractContext();

const suckersQuery = useSuckers();
const pairs: SuckerPair[] = suckersQuery.data ?? [];

const pendingReservedTokensQuery = useQuery({
queryKey: [
"suckersPendingReservedTokens",
projectId?.toString() || "0",
chainId?.toString() || "0",
pairs?.map(p => p.peerChainId).join(",") || "noPairs",
],
queryFn: async () => {
if (!chainId) return null;
if (!pairs || pairs.length === 0) return [];

return await Promise.all(
pairs.map(async pair => {
const { peerChainId, projectId: peerProjectId } = pair;

const controllerAddress = await readJbDirectoryControllerOf(wagmiConfig, {
chainId,
args: [BigInt(projectId)],
})

const pendingReservedTokens = await readJbControllerPendingReservedTokenBalanceOf(config, {
chainId: Number(peerChainId),
address: controllerAddress,
args: [peerProjectId],
});

return {
chainId: peerChainId,
projectId: peerProjectId,
pendingReservedTokens,
};
})
);
},
});

return {
isLoading: pendingReservedTokensQuery.isLoading || suckersQuery.isLoading,
isError: pendingReservedTokensQuery.isError || suckersQuery.isError,
data: pendingReservedTokensQuery.data as
| {
chainId: number;
projectId: bigint;
pendingReservedTokens: bigint;
}[]
| undefined,
};
}
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import { Trans, t } from '@lingui/macro'
import { useJBContractContext, useJBTokenContext, useWriteJbControllerSendReservedTokensToSplitsOf } from 'juice-sdk-react'
import { useJBChainId, useJBContractContext, useJBTokenContext, useWriteJbControllerSendReservedTokensToSplitsOf } from 'juice-sdk-react'
import { useContext, useState } from 'react'

import { waitForTransactionReceipt } from '@wagmi/core'
import TransactionModal from 'components/modals/TransactionModal'
import { NETWORKS } from 'constants/networks'
import { TxHistoryContext } from 'contexts/Transaction/TxHistoryContext'
import SplitList from 'packages/v4/components/SplitList/SplitList'
import useV4ProjectOwnerOf from 'packages/v4/hooks/useV4ProjectOwnerOf'
Expand All @@ -24,6 +25,8 @@ export default function V4DistributeReservedTokensModal({
}) {
const { addTransaction } = useContext(TxHistoryContext)

const jbChainId = useJBChainId()

const { projectId, contracts } = useJBContractContext()
const { splits: reservedTokensSplits } = useV4ReservedSplits()
const { data: projectOwnerAddress } = useV4ProjectOwnerOf()
Expand Down Expand Up @@ -90,9 +93,11 @@ export default function V4DistributeReservedTokensModal({
plural: false,
})

if (!jbChainId) return null

return (
<TransactionModal
title={<Trans>Send reserved {tokenTextPlural}</Trans>}
title={<Trans>Send reserved {tokenTextPlural} on {NETWORKS[jbChainId].label}</Trans>}
open={open}
onOk={() => sendReservedTokens()}
okText={t`Send ${tokenTextPlural}`}
Expand All @@ -101,7 +106,7 @@ export default function V4DistributeReservedTokensModal({
transactionPending={transactionPending}
onCancel={onCancel}
width={640}
centered={true}
centered
>
<div className="flex flex-col gap-6">
<div className="flex justify-between">
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { Trans, t } from '@lingui/macro'

import { Skeleton } from 'antd'
import { TitleDescriptionDisplayCard } from 'components/Project/ProjectTabs/TitleDescriptionDisplayCard'
import { reservedTokensTooltip } from 'components/Project/ProjectTabs/TokensPanelTooltips'
import { ProjectChainSelect } from 'packages/v4/components/ProjectDashboard/ProjectChainSelect'
Expand All @@ -15,7 +16,7 @@ export const V4ReservedTokensSubPanel = ({
className?: string
}) => {

const { selectedChainId, setSelectedChainId, reservedList, pendingReservedTokensFormatted, reservedPercent } =
const { selectedChainId, setSelectedChainId, reservedList, aggregatedPendingReservedTokens, pendingReservedTokensElement, reservedPercent } =
useV4ReservedTokensSubPanel()

const reservedPercentTooltip = (
Expand Down Expand Up @@ -43,8 +44,8 @@ export const V4ReservedTokensSubPanel = ({
className="w-full min-w-min flex-[1_0_0]"
title={t`Reserved tokens`}
description={
pendingReservedTokensFormatted ? (
<>{pendingReservedTokensFormatted}</>
pendingReservedTokensElement ? (
<>{pendingReservedTokensElement}</>
) : (
<div className="h-7 w-24 animate-pulse rounded bg-grey-200 dark:bg-slate-200" />
)
Expand All @@ -54,21 +55,19 @@ export const V4ReservedTokensSubPanel = ({
<TitleDescriptionDisplayCard
className="w-full min-w-min flex-[1_0_0]"
title={t`Reserved rate`}
description={reservedPercent}
description={reservedPercent ?? <Skeleton paragraph={false} title={{ width: 100 }} active/>}
tooltip={reservedPercentTooltip}
/>
</div>
{reservedPercent &&
pendingReservedTokensFormatted &&
reservedPercent !== '0' ? (
{aggregatedPendingReservedTokens ? (
<TitleDescriptionDisplayCard
className="w-full"
title={t`Reserved tokens list`}
// kebabMenu={{
// items: kebabMenuItems,
// }}
>
{pendingReservedTokensFormatted ||
{aggregatedPendingReservedTokens ||
reservedPercent ||
(reservedList && reservedList.length > 1) ? (
<>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,16 +1,25 @@
import { JBChainId, SPLITS_TOTAL_PERCENT, SplitPortion, formatEther } from 'juice-sdk-core'
import {
JBChainId,
SPLITS_TOTAL_PERCENT,
SplitPortion,
formatEther
} from 'juice-sdk-core';
import {
useJBChainId,
useJBContractContext,
useReadJbControllerPendingReservedTokenBalanceOf
} from 'juice-sdk-react'
import { useEffect, useMemo, useState } from 'react'

import { useJBRulesetByChain } from 'packages/v4/hooks/useJBRulesetByChain'
import { useProjectIdOfChain } from 'packages/v4/hooks/useProjectIdOfChain'
import useV4ProjectOwnerOf from 'packages/v4/hooks/useV4ProjectOwnerOf'
import { useV4ReservedSplits } from 'packages/v4/hooks/useV4ReservedSplits'
import assert from 'utils/assert'
} from 'juice-sdk-react';
import { useEffect, useMemo, useState } from 'react';

import { Tooltip } from 'antd';
import { NETWORKS } from 'constants/networks';
import { ChainLogo } from 'packages/v4/components/ChainLogo';
import { useJBRulesetByChain } from 'packages/v4/hooks/useJBRulesetByChain';
import { useProjectIdOfChain } from 'packages/v4/hooks/useProjectIdOfChain';
import { useSuckersPendingReservedTokens } from 'packages/v4/hooks/useSuckersPendingReservedTokens';
import useV4ProjectOwnerOf from 'packages/v4/hooks/useV4ProjectOwnerOf';
import { useV4ReservedSplits } from 'packages/v4/hooks/useV4ReservedSplits';
import assert from 'utils/assert';

export const useV4ReservedTokensSubPanel = () => {
const { contracts } = useJBContractContext()
Expand All @@ -24,30 +33,72 @@ export const useV4ReservedTokensSubPanel = () => {
const { splits: reservedTokensSplits } = useV4ReservedSplits(selectedChainId)

const { rulesetMetadata } = useJBRulesetByChain(selectedChainId)
const reservedPercent = `${rulesetMetadata?.reservedPercent.formatPercentage()}%`
const reservedPercent = rulesetMetadata ? <>{rulesetMetadata?.reservedPercent.formatPercentage()}%</>: undefined

const { data: pendingReservedTokens } =
useReadJbControllerPendingReservedTokenBalanceOf({
address: contracts.controller.data ?? undefined,
args: [BigInt(projectId ?? 0)],
chainId: selectedChainId
chainId: selectedChainId,
})

const pendingReservedTokensFormatted = useMemo(() => {
if (pendingReservedTokens === undefined) return
return formatEther(pendingReservedTokens, { fractionDigits: 6 })
}, [pendingReservedTokens])

const { data: suckersPendingReservedTokens } = useSuckersPendingReservedTokens()

const aggregatedPendingReservedTokens =
suckersPendingReservedTokens?.reduce(
(acc, curr) => acc + curr.pendingReservedTokens,
0n,
) ?? 0n

const pendingReservedTokensElement = useMemo(() => {
return (
<Tooltip
title={
suckersPendingReservedTokens?.length &&
suckersPendingReservedTokens.length > 0 ? (
<div className="flex flex-col gap-2">
{suckersPendingReservedTokens.map(({ chainId, pendingReservedTokens }) => (
<div
className="flex items-center justify-between gap-4"
key={chainId}
>
<div className="flex items-center gap-2">
<ChainLogo chainId={chainId as JBChainId} />
<span>{NETWORKS[chainId].label}</span>
</div>
<span className="whitespace-nowrap font-medium">
{formatEther(pendingReservedTokens, { fractionDigits: 6 })}
</span>
</div>
))}
</div>
) : undefined
}
>
<span>
{formatEther(aggregatedPendingReservedTokens, { fractionDigits: 6 })}
</span>
</Tooltip>
)
}, [suckersPendingReservedTokens, aggregatedPendingReservedTokens])

const reservedList = useMemo(() => {
if (!projectOwnerAddress || !projectId || !reservedTokensSplits) return
// If there aren't explicitly defined splits, all reserved tokens go to this project.
if (reservedTokensSplits?.length === 0)
if (reservedTokensSplits.length === 0) {
return [
{
projectId: 0,
address: projectOwnerAddress!,
percent: `${new SplitPortion(
SPLITS_TOTAL_PERCENT,
).formatPercentage()}%`,
percent: `${new SplitPortion(SPLITS_TOTAL_PERCENT).formatPercentage()}%`,
},
]
}

// If the splits don't add up to 100%, remaining tokens go to this project.
let splitsPercentTotal = 0
const processedSplits = reservedTokensSplits
.sort((a, b) => Number(b.percent) - Number(a.percent))
Expand All @@ -64,36 +115,27 @@ export const useV4ReservedTokensSubPanel = () => {

const remainingPercentage = SPLITS_TOTAL_PERCENT - splitsPercentTotal

// Check if this project is already one of the splits.
if (!(remainingPercentage === 0)) {
if (remainingPercentage !== 0) {
const projectSplitIndex = processedSplits.findIndex(
v => v.projectId === Number(projectId),
)
if (projectSplitIndex != -1)
// If it is, increase its split percentage to bring the total to 100%.
if (projectSplitIndex !== -1) {
processedSplits[projectSplitIndex].percent = `${new SplitPortion(
remainingPercentage +
Number(reservedTokensSplits[projectSplitIndex].percent.value),
).formatPercentage()}%`
// If it isn't, add a split at the beginning which brings the total percentage to 100%.
else
} else {
processedSplits.unshift({
projectId: 0,
address: projectOwnerAddress!,
percent: `${new SplitPortion(
remainingPercentage,
).formatPercentage()}%`,
percent: `${new SplitPortion(remainingPercentage).formatPercentage()}%`,
})
}
}

return processedSplits
}, [reservedTokensSplits, projectOwnerAddress, projectId])

const pendingReservedTokensFormatted = useMemo(() => {
if (pendingReservedTokens === undefined) return
return formatEther(pendingReservedTokens, { fractionDigits: 6 })
}, [pendingReservedTokens])

useEffect(() => {
setSelectedChainId(projectPageChainId)
}, [projectPageChainId])
Expand All @@ -102,8 +144,10 @@ export const useV4ReservedTokensSubPanel = () => {
selectedChainId,
setSelectedChainId,
reservedList,
pendingReservedTokensFormatted: pendingReservedTokensFormatted,
pendingReservedTokens: pendingReservedTokens,
reservedPercent,
pendingReservedTokens,
pendingReservedTokensFormatted,
aggregatedPendingReservedTokens,
pendingReservedTokensElement,
}
}

0 comments on commit cc0c74e

Please sign in to comment.