From 99604b94219d3a83394cc949f68d487f403472ad Mon Sep 17 00:00:00 2001 From: wraeth-eth <104132113+wraeth-eth@users.noreply.github.com> Date: Wed, 4 Dec 2024 14:14:23 +1100 Subject: [PATCH] v4 token redemption (#4554) --- .../V4PayRedeemCard/RedeemConfiguration.tsx | 2 +- .../V4UserTotalTokensBalanceProvider.tsx | 34 ++ .../v4/hooks/useETHReceivedFromTokens.ts | 31 +- src/packages/v4/utils/math.ts | 4 +- .../V4TokensPanel/V4BurnOrRedeemModal.tsx | 409 ++++++++++++++++++ .../V4TokensPanel/V4RedeemTokensButton.tsx | 71 +++ .../V4TokenRedemptionCallout.tsx | 46 ++ .../V4TokensPanel/V4TokensPanel.tsx | 17 +- .../V4TokensPanel/hooks/useV4TokensPanel.ts | 17 +- .../hooks/useV4YourBalanceMenuItems.tsx | 31 +- .../v4/[chainName]/p/[projectId]/index.tsx | 13 +- 11 files changed, 623 insertions(+), 52 deletions(-) create mode 100644 src/packages/v4/contexts/V4UserTotalTokensBalanceProvider.tsx create mode 100644 src/packages/v4/views/V4ProjectDashboard/V4ProjectTabs/V4TokensPanel/V4BurnOrRedeemModal.tsx create mode 100644 src/packages/v4/views/V4ProjectDashboard/V4ProjectTabs/V4TokensPanel/V4RedeemTokensButton.tsx create mode 100644 src/packages/v4/views/V4ProjectDashboard/V4ProjectTabs/V4TokensPanel/V4TokenRedemptionCallout.tsx diff --git a/src/packages/v4/components/ProjectDashboard/V4PayRedeemCard/RedeemConfiguration.tsx b/src/packages/v4/components/ProjectDashboard/V4PayRedeemCard/RedeemConfiguration.tsx index 17048aa8ee..65f9880744 100644 --- a/src/packages/v4/components/ProjectDashboard/V4PayRedeemCard/RedeemConfiguration.tsx +++ b/src/packages/v4/components/ProjectDashboard/V4PayRedeemCard/RedeemConfiguration.tsx @@ -73,7 +73,7 @@ export const RedeemConfiguration: React.FC = ({ const balance = userTokenBalance ?? 0 return amount > balance }, [redeemAmount, userTokenBalance]) - + const tokenTicker = tokenSymbol || 'TOKENS' // 0.5% slippage for USD-denominated tokens diff --git a/src/packages/v4/contexts/V4UserTotalTokensBalanceProvider.tsx b/src/packages/v4/contexts/V4UserTotalTokensBalanceProvider.tsx new file mode 100644 index 0000000000..b92ec5bcaa --- /dev/null +++ b/src/packages/v4/contexts/V4UserTotalTokensBalanceProvider.tsx @@ -0,0 +1,34 @@ +import { useWallet } from 'hooks/Wallet' +import { + useJBContractContext, + useReadJbTokensTotalBalanceOf, +} from 'juice-sdk-react' +import React, { PropsWithChildren } from 'react' +import { zeroAddress } from 'viem' + +const V4UserTotalTokensBalanceContext = React.createContext<{ + data: bigint | undefined + isLoading: boolean +}>({ + data: undefined, + isLoading: false, +}) + +export const V4UserTotalTokensBalanceProvider: React.FC = ({ + children, +}) => { + const { userAddress } = useWallet() + const { projectId } = useJBContractContext() + const value = useReadJbTokensTotalBalanceOf({ + args: [userAddress ?? zeroAddress, projectId], + }) + + return ( + + {children} + + ) +} + +export const useV4UserTotalTokensBalance = () => + React.useContext(V4UserTotalTokensBalanceContext) diff --git a/src/packages/v4/hooks/useETHReceivedFromTokens.ts b/src/packages/v4/hooks/useETHReceivedFromTokens.ts index 0847ee6b22..d68802b0c5 100644 --- a/src/packages/v4/hooks/useETHReceivedFromTokens.ts +++ b/src/packages/v4/hooks/useETHReceivedFromTokens.ts @@ -25,19 +25,28 @@ export function useETHReceivedFromTokens( const redemptionRate = rulesetMetadata.data?.redemptionRate?.value if ( - !redemptionRate || - !totalSupply || - !tokensReserved || - !tokenAmountWei || - !nativeTokenSurplus + redemptionRate === undefined || + totalSupply === undefined || + tokensReserved === undefined || + tokenAmountWei === undefined || + nativeTokenSurplus === undefined ) { return } - return getTokenRedemptionQuoteEth(tokenAmountWei, { - redemptionRate: Number(redemptionRate), - totalSupply, - tokensReserved, - overflowWei: nativeTokenSurplus, - }) + try { + return getTokenRedemptionQuoteEth(tokenAmountWei, { + redemptionRate: Number(redemptionRate), + totalSupply, + tokensReserved, + overflowWei: nativeTokenSurplus, + }) + } catch (e) { + // Division by zero can cause a RangeError + if (e instanceof RangeError) { + return + } else { + throw e + } + } } diff --git a/src/packages/v4/utils/math.ts b/src/packages/v4/utils/math.ts index a5d55b13ce..422f772fa4 100644 --- a/src/packages/v4/utils/math.ts +++ b/src/packages/v4/utils/math.ts @@ -1,6 +1,8 @@ import { feeForAmount } from 'utils/math' -export const MAX_PAYOUT_LIMIT = BigInt('26959946667150639794667015087019630673637144422540572481103610249215') // uint 224, probably a better way lol +export const MAX_PAYOUT_LIMIT = BigInt( + '26959946667150639794667015087019630673637144422540572481103610249215', +) // uint 224, probably a better way lol export const amountSubFee = ( amountWad: bigint | undefined, diff --git a/src/packages/v4/views/V4ProjectDashboard/V4ProjectTabs/V4TokensPanel/V4BurnOrRedeemModal.tsx b/src/packages/v4/views/V4ProjectDashboard/V4ProjectTabs/V4TokensPanel/V4BurnOrRedeemModal.tsx new file mode 100644 index 0000000000..a4a6c36814 --- /dev/null +++ b/src/packages/v4/views/V4ProjectDashboard/V4ProjectTabs/V4TokensPanel/V4BurnOrRedeemModal.tsx @@ -0,0 +1,409 @@ +import { t, Trans } from '@lingui/macro' +import { waitForTransactionReceipt } from '@wagmi/core' +import { Checkbox, Descriptions, Form } from 'antd' +import { useForm } from 'antd/lib/form/Form' +import InputAccessoryButton from 'components/buttons/InputAccessoryButton' +import { Callout } from 'components/Callout/Callout' +import ETHAmount from 'components/currency/ETHAmount' +import FormattedNumberInput from 'components/inputs/FormattedNumberInput' +import TransactionModal from 'components/modals/TransactionModal' +import { TokenAmount } from 'components/TokenAmount' +import { TxHistoryContext } from 'contexts/Transaction/TxHistoryContext' +import { BigNumber } from 'ethers' +import { useWallet } from 'hooks/Wallet' +import { formatEther, NATIVE_TOKEN } from 'juice-sdk-core' +import { + useJBContractContext, + useJBRulesetContext, + useJBTokenContext, + useNativeTokenSurplus, + useWriteJbControllerBurnTokensOf, + useWriteJbMultiTerminalRedeemTokensOf, +} from 'juice-sdk-react' +import { useV4UserTotalTokensBalance } from 'packages/v4/contexts/V4UserTotalTokensBalanceProvider' +import { useETHReceivedFromTokens } from 'packages/v4/hooks/useETHReceivedFromTokens' +import { usePayoutLimit } from 'packages/v4/hooks/usePayoutLimit' +import { useV4TotalTokenSupply } from 'packages/v4/hooks/useV4TotalTokenSupply' +import { V4_CURRENCY_USD } from 'packages/v4/utils/currency' +import { MAX_PAYOUT_LIMIT } from 'packages/v4/utils/math' +import { wagmiConfig } from 'packages/v4/wagmiConfig' +import React, { useState } from 'react' +import { emitErrorNotification } from 'utils/notifications' +import { tokenSymbolText } from 'utils/tokenSymbolText' +import { parseEther } from 'viem' + +// This doubles as the 'Redeem' and 'Burn' modal depending on if project has overflow +export function V4BurnOrRedeemModal({ + open, + onCancel, + onConfirmed, +}: { + open?: boolean + onCancel?: VoidFunction + onConfirmed?: VoidFunction +}) { + const { contracts, projectId } = useJBContractContext() + const { addTransaction } = React.useContext(TxHistoryContext) + const { token } = useJBTokenContext() + const { userAddress } = useWallet() + const payoutLimitResult = usePayoutLimit() + const payoutLimit = payoutLimitResult.data.amount ?? 0n + const surplusResult = useNativeTokenSurplus() + const totalTokenSupplyResult = useV4TotalTokenSupply() + const rulesetMetadata = useJBRulesetContext().rulesetMetadata.data + const userTokenBalance = useV4UserTotalTokensBalance().data ?? 0n + + const { writeContractAsync: writeRedeem } = + useWriteJbMultiTerminalRedeemTokensOf() + const { writeContractAsync: writeBurnTokens } = + useWriteJbControllerBurnTokensOf() + + const [form] = useForm<{ + redeemAmount: string | undefined + legalCheckbox: boolean + }>() + const checkedLegal = Form.useWatch('legalCheckbox', form) + const redeemAmount = Form.useWatch('redeemAmount', form) + + const redeemAmountBigInt = React.useMemo( + () => parseEther(redeemAmount ?? '0'), + [redeemAmount], + ) + const [loading, setLoading] = useState() + const [memo] = useState('') + const [transactionPending, setTransactionPending] = useState() + + const maxClaimable = useETHReceivedFromTokens(userTokenBalance) ?? 0n + const rewardAmount = useETHReceivedFromTokens(redeemAmountBigInt) ?? 0n + + const surplus = React.useMemo( + () => surplusResult.data ?? 0n, + [surplusResult.data], + ) + const redemptionEnabled = React.useMemo( + () => payoutLimit !== MAX_PAYOUT_LIMIT, + [payoutLimit], + ) + const hasSurplus = React.useMemo(() => surplus > 0n, [surplus]) + const canRedeem = React.useMemo( + () => hasSurplus && redemptionEnabled, + [hasSurplus, redemptionEnabled], + ) + + const totalSupplyExceeded = React.useMemo(() => { + if (!redeemAmount) return + return parseEther(redeemAmount) > (totalTokenSupplyResult.data ?? 0n) + }, [redeemAmount, totalTokenSupplyResult.data]) + + const personalBalanceExceeded = React.useMemo(() => { + if (!redeemAmount) return + return parseEther(redeemAmount) > userTokenBalance + }, [redeemAmount, userTokenBalance]) + + const inUsd = React.useMemo( + () => payoutLimitResult.data.currency === V4_CURRENCY_USD, + [payoutLimitResult.data.currency], + ) + + const totalTokenSupply = totalTokenSupplyResult.data ?? 0n + // 0.5% slippage for USD-denominated projects + const minReturnedTokens = + payoutLimitResult.data.currency === V4_CURRENCY_USD + ? ((rewardAmount ?? 0n) * 1000n) / 1005n + : rewardAmount ?? 0n + + const modalTitle = React.useMemo(() => { + const tokensTextLong = tokenSymbolText({ + tokenSymbol: token.data?.symbol, + capitalize: false, + plural: true, + includeTokenWord: true, + }) + if (canRedeem) { + return t`Redeem ${tokensTextLong} for ETH` + } + return t`Burn ${tokensTextLong}` + }, [canRedeem, token.data?.symbol]) + + const validateRedeemAmount = () => { + if (redeemAmountBigInt === 0n) { + return Promise.reject(t`Required`) + } else if (redeemAmountBigInt > userTokenBalance) { + return Promise.reject(t`Insufficient token balance`) + } else if (redeemAmountBigInt > totalTokenSupply) { + // Error message already showing for this case + return Promise.reject() + } + return Promise.resolve() + } + + const executeRedeemTransaction = async () => { + await form.validateFields() + if (!minReturnedTokens) return + if (!contracts.primaryNativeTerminal.data || !projectId) { + emitErrorNotification('Failed to prepare transaction') + return + } + if (!userAddress) { + emitErrorNotification('No wallet connected') + return + } + + setLoading(true) + + try { + const hash = await writeRedeem({ + address: contracts.primaryNativeTerminal.data, + args: [ + userAddress, + BigInt(projectId), + NATIVE_TOKEN, + redeemAmountBigInt, + 0n, + userAddress, + '0x0', + ] as const, + }) + addTransaction?.('Redeem', { hash }) + + setTransactionPending(true) + form.resetFields() + + await waitForTransactionReceipt(wagmiConfig, { + hash, + }) + setTransactionPending(false) + setLoading(false) + onConfirmed?.() + } catch (e) { + setTransactionPending(false) + setLoading(false) + emitErrorNotification((e as unknown as Error).message) + } + } + + const executeBurnTransaction = async () => { + await form.validateFields() + if (!contracts.controller.data || !projectId) { + emitErrorNotification('Failed to prepare transaction') + return + } + if (!userAddress) { + emitErrorNotification('No wallet connected') + return + } + + setLoading(true) + + try { + const hash = await writeBurnTokens({ + address: contracts.controller.data, + args: [userAddress, projectId, redeemAmountBigInt, memo] as const, + }) + addTransaction?.('Burn', { hash }) + setTransactionPending(true) + form.resetFields() + + await waitForTransactionReceipt(wagmiConfig, { + hash, + }) + setTransactionPending(false) + setLoading(false) + onConfirmed?.() + } catch (e) { + setTransactionPending(false) + setLoading(false) + emitErrorNotification((e as unknown as Error).message) + } + } + + return ( + { + canRedeem ? executeRedeemTransaction() : executeBurnTransaction() + }} + onCancel={() => { + form.resetFields() + onCancel?.() + }} + okText={modalTitle} + okButtonProps={{ + disabled: + !redeemAmount || parseFloat(redeemAmount) === 0 || !checkedLegal, + }} + width={540} + centered + > +
+
+ {canRedeem ? ( +
+ + Tokens are burned when they are redeemed. + +
+ + Redeem your tokens to reclaim some ETH that isn't needed for + payouts. This cycle's redemption rate{' '} + determines the amount of ETH you'll receive. + +
+
+ ) : ( + + {!hasSurplus && ( + + + This project has no ETH available for redemptions + + . You won't receive any ETH for burning your tokens. + + )} + {!redemptionEnabled && ( + + This project has a redemptions turned off. + You won't receive any ETH for burning your tokens. + + )} + + )} +
+ + + Redemption rate}> + {rulesetMetadata?.redemptionRate.formatPercentage()} + + + Your{' '} + {tokenSymbolText({ + tokenSymbol: token.data?.symbol, + })}{' '} + balance + + } + > + + + Total redemption value}> + + + + +
+
+ Tokens to redeem + ) : ( + Tokens to burn + ) + } + rules={[{ validator: validateRedeemAmount }]} + > + { + form.setFieldsValue({ + redeemAmount: formatEther(userTokenBalance), + }) + }} + /> + } + disabled={userTokenBalance === 0n} + // onChange={val => form.se(val)} + /> + + + value + ? Promise.resolve() + : Promise.reject( + new Error(t`You must confirm your compliance.`), + ), + }, + ]} + > + + + + I confirm that the use and redemption of crypto tokens is + legal in my jurisdiction, and that I am fully responsible + for compliance with all relevant laws and regulations. + + + + + + {/* Will comment memo form due to [note] missing in subgraph - see discussion in https://discord.com/channels/939317843059679252/1035458515709464586/1053400936971771974 */} + {/* + + */} +
+ + {canRedeem && !totalSupplyExceeded && minReturnedTokens > 0n ? ( +
+ <> + {/* If USD denominated, can only define the lower limit (not exact amount), hence 'at least' */} + {/* Using 4 full sentences for translation purposes */} + {!personalBalanceExceeded ? ( + <> + {inUsd ? ( + + You will receive at least{' '} + + + ) : ( + + You will receive{' '} + + + )} + + ) : ( + <> + {inUsd ? ( + + You would receive at least{' '} + + + ) : ( + + You would receive{' '} + + + )} + + )} + +
+ ) : null} +
+
+
+ ) +} diff --git a/src/packages/v4/views/V4ProjectDashboard/V4ProjectTabs/V4TokensPanel/V4RedeemTokensButton.tsx b/src/packages/v4/views/V4ProjectDashboard/V4ProjectTabs/V4TokensPanel/V4RedeemTokensButton.tsx new file mode 100644 index 0000000000..4cc06070ef --- /dev/null +++ b/src/packages/v4/views/V4ProjectDashboard/V4ProjectTabs/V4TokensPanel/V4RedeemTokensButton.tsx @@ -0,0 +1,71 @@ +import { Trans, t } from '@lingui/macro' +import { Button, Tooltip } from 'antd' +import { useNativeTokenSurplus } from 'juice-sdk-react' +import { useV4UserTotalTokensBalance } from 'packages/v4/contexts/V4UserTotalTokensBalanceProvider' +import { usePayoutLimit } from 'packages/v4/hooks/usePayoutLimit' +import { MAX_PAYOUT_LIMIT } from 'packages/v4/utils/math' +import React, { useCallback, useMemo, useState } from 'react' +import { V4BurnOrRedeemModal } from './V4BurnOrRedeemModal' + +export const V4RedeemTokensButton = ({ + className, + containerClassName, +}: { + className?: string + containerClassName?: string +}) => { + const payoutLimitResult = usePayoutLimit() + const payoutLimit = payoutLimitResult.data.amount ?? 0n + const surplusResult = useNativeTokenSurplus() + const surplus = surplusResult.data ?? 0n + const userTokenBalance = useV4UserTotalTokensBalance().data ?? 0n + + const loading = React.useMemo( + () => payoutLimitResult.isLoading && surplusResult.isLoading, + [payoutLimitResult.isLoading, surplusResult.isLoading], + ) + + const [open, setOpen] = useState(false) + const openModal = useCallback(() => setOpen(true), []) + const closeModal = useCallback(() => setOpen(false), []) + + const hasSurplus = React.useMemo(() => surplus > 0n, [surplus]) + + const redeemTokensDisabled = useMemo(() => { + if (!userTokenBalance || !hasSurplus || payoutLimit === MAX_PAYOUT_LIMIT) + return true + return userTokenBalance === 0n + }, [hasSurplus, payoutLimit, userTokenBalance]) + + const redeemDisabledTooltip = useMemo(() => { + if (!userTokenBalance || userTokenBalance === 0n) + return t`No tokens to redeem.` + if (!hasSurplus) + return t`This project has no ETH, or is using all of its ETH for payouts.` + if (payoutLimit === 0n) return t`This project has redemptions turned off.` + return undefined + }, [hasSurplus, payoutLimit, userTokenBalance]) + + return ( + + + + + ) +} diff --git a/src/packages/v4/views/V4ProjectDashboard/V4ProjectTabs/V4TokensPanel/V4TokenRedemptionCallout.tsx b/src/packages/v4/views/V4ProjectDashboard/V4ProjectTabs/V4TokensPanel/V4TokenRedemptionCallout.tsx new file mode 100644 index 0000000000..28a35fd7ea --- /dev/null +++ b/src/packages/v4/views/V4ProjectDashboard/V4ProjectTabs/V4TokensPanel/V4TokenRedemptionCallout.tsx @@ -0,0 +1,46 @@ +import { InformationCircleIcon } from '@heroicons/react/24/outline' +import { t } from '@lingui/macro' +import { useJBRulesetContext } from 'juice-sdk-react' +import { usePayoutLimit } from 'packages/v4/hooks/usePayoutLimit' +import { MAX_PAYOUT_LIMIT } from 'packages/v4/utils/math' +import React, { useMemo } from 'react' +import { twMerge } from 'tailwind-merge' + +export const V4TokenRedemptionCallout = () => { + const rulesetMetadata = useJBRulesetContext().rulesetMetadata.data + const payoutLimit = usePayoutLimit() + + const redemptionEnabled = React.useMemo(() => { + if (!rulesetMetadata || payoutLimit.isLoading) return + // TODO: Update redemptionRate to be cashOut + return ( + rulesetMetadata.redemptionRate.value > 0 && + payoutLimit.data.amount !== MAX_PAYOUT_LIMIT + ) + }, [payoutLimit.data.amount, payoutLimit.isLoading, rulesetMetadata]) + + const loading = useMemo( + () => redemptionEnabled === undefined, + [redemptionEnabled], + ) + + const text = t`This cycle has token redemptions ${ + redemptionEnabled ? 'enabled' : 'disabled' + }` + + if (loading) return null + + return ( +
+ + {text} +
+ ) +} diff --git a/src/packages/v4/views/V4ProjectDashboard/V4ProjectTabs/V4TokensPanel/V4TokensPanel.tsx b/src/packages/v4/views/V4ProjectDashboard/V4ProjectTabs/V4TokensPanel/V4TokensPanel.tsx index 65281c7549..852df78c49 100644 --- a/src/packages/v4/views/V4ProjectDashboard/V4ProjectTabs/V4TokensPanel/V4TokensPanel.tsx +++ b/src/packages/v4/views/V4ProjectDashboard/V4ProjectTabs/V4TokensPanel/V4TokensPanel.tsx @@ -21,9 +21,12 @@ import { useChainId } from 'wagmi' import { useV4BalanceMenuItemsUserFlags } from './hooks/useV4BalanceMenuItemsUserFlags' import { useV4TokensPanel } from './hooks/useV4TokensPanel' import { useV4YourBalanceMenuItems } from './hooks/useV4YourBalanceMenuItems' +import { V4BurnOrRedeemModal } from './V4BurnOrRedeemModal' import { V4ClaimTokensModal } from './V4ClaimTokensModal' import { V4MintModal } from './V4MintModal' +import { V4RedeemTokensButton } from './V4RedeemTokensButton' import { V4ReservedTokensSubPanel } from './V4ReservedTokensSubPanel' +import { V4TokenRedemptionCallout } from './V4TokenRedemptionCallout' export const V4TokensPanel = () => { const { @@ -51,8 +54,8 @@ export const V4TokensPanel = () => { const { items, - // redeemModalVisible, - // setRedeemModalVisible, + redeemModalVisible, + setRedeemModalVisible, claimTokensModalVisible, setClaimTokensModalVisible, mintModalVisible, @@ -68,7 +71,7 @@ export const V4TokensPanel = () => {

Tokens

- {/* */} +
{!userTokenBalanceLoading && userTokenBalance !== undefined && ( @@ -90,10 +93,10 @@ export const V4TokensPanel = () => { Claim ERC-20 token )} - {/* */} + />
} @@ -153,11 +156,11 @@ export const V4TokensPanel = () => { open={tokenHolderModalOpen} onClose={closeTokenHolderModal} /> - {/* setRedeemModalVisible(false)} onConfirmed={reloadWindow} - /> */} + /> setClaimTokensModalVisible(false)} diff --git a/src/packages/v4/views/V4ProjectDashboard/V4ProjectTabs/V4TokensPanel/hooks/useV4TokensPanel.ts b/src/packages/v4/views/V4ProjectDashboard/V4ProjectTabs/V4TokensPanel/hooks/useV4TokensPanel.ts index aaeb54f165..ebc3beae96 100644 --- a/src/packages/v4/views/V4ProjectDashboard/V4ProjectTabs/V4TokensPanel/hooks/useV4TokensPanel.ts +++ b/src/packages/v4/views/V4ProjectDashboard/V4ProjectTabs/V4TokensPanel/hooks/useV4TokensPanel.ts @@ -1,20 +1,17 @@ -import { useWallet } from 'hooks/Wallet' import { JBProjectToken } from 'juice-sdk-core' -import { useJBContractContext, useJBTokenContext, useReadJbTokensTotalBalanceOf } from 'juice-sdk-react' +import { useJBTokenContext } from 'juice-sdk-react' +import { useV4UserTotalTokensBalance } from 'packages/v4/contexts/V4UserTotalTokensBalanceProvider' import { useProjectHasErc20Token } from 'packages/v4/hooks/useProjectHasErc20Token' import { useV4TotalTokenSupply } from 'packages/v4/hooks/useV4TotalTokenSupply' import { useV4WalletHasPermission } from 'packages/v4/hooks/useV4WalletHasPermission' import { V4OperatorPermission } from 'packages/v4/models/v4Permissions' import { useMemo } from 'react' import { tokenSymbolText } from 'utils/tokenSymbolText' -import { zeroAddress } from 'viem' export const useV4TokensPanel = () => { - const { projectId } = useJBContractContext() - const { userAddress } = useWallet() const { token } = useJBTokenContext() const tokenAddress = token?.data?.address - + const { data: _totalTokenSupply } = useV4TotalTokenSupply() const projectToken = tokenSymbolText({ @@ -29,12 +26,8 @@ export const useV4TokensPanel = () => { const projectHasErc20Token = useProjectHasErc20Token() const { data: _userTokenBalance, isLoading: userTokenBalanceLoading } = - useReadJbTokensTotalBalanceOf({ - args: [ - userAddress ?? zeroAddress, - projectId - ] - }) + useV4UserTotalTokensBalance() + const userTokenBalance = useMemo(() => { if (_userTokenBalance === undefined) return return new JBProjectToken(_userTokenBalance ?? 0n) diff --git a/src/packages/v4/views/V4ProjectDashboard/V4ProjectTabs/V4TokensPanel/hooks/useV4YourBalanceMenuItems.tsx b/src/packages/v4/views/V4ProjectDashboard/V4ProjectTabs/V4TokensPanel/hooks/useV4YourBalanceMenuItems.tsx index f122e8b256..e5e5486336 100644 --- a/src/packages/v4/views/V4ProjectDashboard/V4ProjectTabs/V4TokensPanel/hooks/useV4YourBalanceMenuItems.tsx +++ b/src/packages/v4/views/V4ProjectDashboard/V4ProjectTabs/V4TokensPanel/hooks/useV4YourBalanceMenuItems.tsx @@ -1,6 +1,7 @@ import { + FireIcon, PlusCircleIcon, - ReceiptRefundIcon + ReceiptRefundIcon, } from '@heroicons/react/24/outline' import { t } from '@lingui/macro' import { PopupMenuItem } from 'components/ui/PopupMenu' @@ -8,7 +9,7 @@ import { ReactNode, useMemo, useState } from 'react' import { useV4BalanceMenuItemsUserFlags } from './useV4BalanceMenuItemsUserFlags' export const useV4YourBalanceMenuItems = () => { - const { canClaimErcTokens, canMintTokens } = + const { canBurnTokens, canClaimErcTokens, canMintTokens } = useV4BalanceMenuItemsUserFlags() const [redeemModalVisible, setRedeemModalVisible] = useState(false) @@ -21,18 +22,18 @@ export const useV4YourBalanceMenuItems = () => { const items = useMemo(() => { const tokenMenuItems: PopupMenuItem[] = [] - // if (canBurnTokens) { - // tokenMenuItems.push({ - // id: 'burn', - // label: ( - // } - // /> - // ), - // onClick: () => setRedeemModalVisible(true), - // }) - // } + if (canBurnTokens) { + tokenMenuItems.push({ + id: 'burn', + label: ( + } + /> + ), + onClick: () => setRedeemModalVisible(true), + }) + } if (canClaimErcTokens) { tokenMenuItems.push({ id: 'claim', @@ -70,7 +71,7 @@ export const useV4YourBalanceMenuItems = () => { // }) // } return tokenMenuItems - }, [canClaimErcTokens, canMintTokens]) + }, [canBurnTokens, canClaimErcTokens, canMintTokens]) return { items, diff --git a/src/pages/v4/[chainName]/p/[projectId]/index.tsx b/src/pages/v4/[chainName]/p/[projectId]/index.tsx index 07a3dd836b..bff1a78653 100644 --- a/src/pages/v4/[chainName]/p/[projectId]/index.tsx +++ b/src/pages/v4/[chainName]/p/[projectId]/index.tsx @@ -8,6 +8,7 @@ import store from 'packages/v4/components/ProjectDashboard/redux/store' import { V4NftRewardsProvider } from 'packages/v4/contexts/V4NftRewards/V4NftRewardsProvider' import V4ProjectMetadataProvider from 'packages/v4/contexts/V4ProjectMetadataProvider' import { V4UserNftCreditsProvider } from 'packages/v4/contexts/V4UserNftCreditsProvider' +import { V4UserTotalTokensBalanceProvider } from 'packages/v4/contexts/V4UserTotalTokensBalanceProvider' import { useCurrentRouteChainId } from 'packages/v4/hooks/useCurrentRouteChainId' import { V4ProjectDashboard } from 'packages/v4/views/V4ProjectDashboard/V4ProjectDashboard' import { wagmiConfig } from 'packages/v4/wagmiConfig' @@ -74,11 +75,13 @@ const Providers: React.FC< - - - {children} - - + + + + {children} + + +