From 2fdbedfa1e1df23df8cb2bfcda7b81f06d977541 Mon Sep 17 00:00:00 2001 From: Johnny D Date: Mon, 30 Sep 2024 12:30:59 +1000 Subject: [PATCH 01/13] feat: v4 send reserved tokens (#4477) --- .../V4DistributeReservedTokensModal.tsx | 69 ++++++++++++------- 1 file changed, 46 insertions(+), 23 deletions(-) diff --git a/src/packages/v4/views/V4ProjectDashboard/V4ProjectTabs/V4TokensPanel/V4DistributeReservedTokensModal.tsx b/src/packages/v4/views/V4ProjectDashboard/V4ProjectTabs/V4TokensPanel/V4DistributeReservedTokensModal.tsx index 981da7cab9..382d1a3ac3 100644 --- a/src/packages/v4/views/V4ProjectDashboard/V4ProjectTabs/V4TokensPanel/V4DistributeReservedTokensModal.tsx +++ b/src/packages/v4/views/V4ProjectDashboard/V4ProjectTabs/V4TokensPanel/V4DistributeReservedTokensModal.tsx @@ -1,11 +1,15 @@ import { t, Trans } from '@lingui/macro' +import { waitForTransactionReceipt } from '@wagmi/core' import TransactionModal from 'components/modals/TransactionModal' +import { TxHistoryContext } from 'contexts/Transaction/TxHistoryContext' import useNameOfERC20 from 'hooks/ERC20/useNameOfERC20' -import { useJBContractContext, useReadJbTokensTokenOf } from 'juice-sdk-react' +import { useJBContractContext, useReadJbTokensTokenOf, useWriteJbControllerSendReservedTokensToSplitsOf } from 'juice-sdk-react' import SplitList from 'packages/v4/components/SplitList/SplitList' import useProjectOwnerOf from 'packages/v4/hooks/useV4ProjectOwnerOf' import { useV4ReservedSplits } from 'packages/v4/hooks/useV4ReservedSplits' -import { useState } from 'react' +import { wagmiConfig } from 'packages/v4/wagmiConfig' +import { useContext, useState } from 'react' +import { emitErrorNotification } from 'utils/notifications' import { tokenSymbolText } from 'utils/tokenSymbolText' import { useV4ReservedTokensSubPanel } from './hooks/useV4ReservedTokensSubPanel' @@ -18,7 +22,9 @@ export default function V4DistributeReservedTokensModal({ onCancel?: VoidFunction onConfirmed?: VoidFunction }) { - const { projectId } = useJBContractContext() + const { addTransaction } = useContext(TxHistoryContext) + + const { projectId, contracts } = useJBContractContext() const { splits: reservedTokensSplits } = useV4ReservedSplits() const { data: projectOwnerAddress } = useProjectOwnerOf() const { data: tokenAddress } = useReadJbTokensTokenOf() @@ -28,30 +34,47 @@ export default function V4DistributeReservedTokensModal({ const [transactionPending, setTransactionPending] = useState() // const distributeReservedTokensTx = useDistributeReservedTokens() + const { writeContractAsync: writeSendReservedTokens, data } = + useWriteJbControllerSendReservedTokensToSplitsOf() + const { pendingReservedTokens, pendingReservedTokensFormatted } = useV4ReservedTokensSubPanel() - async function distributeReservedTokens() { + async function sendReservedTokens() { + if ( + // !payoutLimitAmountCurrency || + // !distributionAmount || + !contracts.controller.data || + !projectId + ) + return + setLoading(true) - // const txSuccessful = await distributeReservedTokensTx( - // {}, - // { - // onDone: () => { - // setTransactionPending(true) - // }, - // onConfirmed: () => { - // setLoading(false) - // setTransactionPending(false) - // onConfirmed?.() - // }, - // }, - // ) - - // if (!txSuccessful) { - setLoading(false) - setTransactionPending(false) - // } + const args = [ + BigInt(projectId) + ] as const + + try { + const hash = await writeSendReservedTokens({ + address: contracts.controller.data, + args, + }) + setTransactionPending(true) + + addTransaction?.('Send reserved tokens', { hash }) + await waitForTransactionReceipt(wagmiConfig, { + hash, + }) + + setLoading(false) + setTransactionPending(false) + onConfirmed?.() + } catch (e) { + setLoading(false) + + emitErrorNotification((e as unknown as Error).message) + } } const tokenTextPlural = tokenSymbolText({ @@ -70,7 +93,7 @@ export default function V4DistributeReservedTokensModal({ Send reserved {tokenTextPlural}} open={open} - onOk={() => distributeReservedTokens()} + onOk={() => sendReservedTokens()} okText={t`Send ${tokenTextPlural}`} connectWalletText={t`Connect wallet to send reserved ${tokenTextPlural}`} confirmLoading={loading} From 999f6e68ca91386d8b462f52509fe1d63cbec99e Mon Sep 17 00:00:00 2001 From: Johnny D Date: Mon, 30 Sep 2024 17:19:05 +1000 Subject: [PATCH 02/13] feat: v4 claim tokens as erc20 (#4478) --- .../v4/hooks/useProjectHasErc20Token.ts | 5 +- .../v4/hooks/useV4IssueErc20TokenTx.ts | 14 +- .../V4DistributePayoutsModal.tsx | 2 +- .../V4TokensPanel/V4ClaimTokensModal.tsx | 199 ++++++++++++++++++ .../V4TokensPanel/V4TokensPanel.tsx | 14 +- .../V4TokensPanel/hooks/useV4TokensPanel.ts | 4 +- .../hooks/useV4YourBalanceMenuItems.tsx | 56 +++-- 7 files changed, 248 insertions(+), 46 deletions(-) create mode 100644 src/packages/v4/views/V4ProjectDashboard/V4ProjectTabs/V4TokensPanel/V4ClaimTokensModal.tsx diff --git a/src/packages/v4/hooks/useProjectHasErc20Token.ts b/src/packages/v4/hooks/useProjectHasErc20Token.ts index a38b254f4a..4a62028232 100644 --- a/src/packages/v4/hooks/useProjectHasErc20Token.ts +++ b/src/packages/v4/hooks/useProjectHasErc20Token.ts @@ -1,8 +1,9 @@ -import { useReadJbTokensTokenOf } from 'juice-sdk-react' +import { useJBTokenContext } from 'juice-sdk-react' import { isZeroAddress } from 'utils/address' export const useProjectHasErc20Token = () => { - const { data: tokenAddress } = useReadJbTokensTokenOf() + const { token } = useJBTokenContext() + const tokenAddress = token?.data?.address return Boolean(tokenAddress && !isZeroAddress(tokenAddress)) } diff --git a/src/packages/v4/hooks/useV4IssueErc20TokenTx.ts b/src/packages/v4/hooks/useV4IssueErc20TokenTx.ts index aff75e3842..0edd5fbb6d 100644 --- a/src/packages/v4/hooks/useV4IssueErc20TokenTx.ts +++ b/src/packages/v4/hooks/useV4IssueErc20TokenTx.ts @@ -1,9 +1,11 @@ import { useCallback, useContext } from 'react' +import { waitForTransactionReceipt } from '@wagmi/core' import { TxHistoryContext } from 'contexts/Transaction/TxHistoryContext' import { useJBContractContext, useWriteJbControllerDeployErc20For } from 'juice-sdk-react' import { Address, zeroAddress } from 'viem' import { BaseTxOpts } from '../models/transactions' +import { wagmiConfig } from '../wagmiConfig' export function useV4IssueErc20TokenTx() { const { addTransaction } = useContext(TxHistoryContext) @@ -47,12 +49,12 @@ export function useV4IssueErc20TokenTx() { onTransactionPendingCallback(hash) addTransaction?.('Launch ERC20 Token', { hash }) - // const transactionReceipt: WaitForTransactionReceiptReturnType = await waitForTransactionReceipt( - // wagmiConfig, - // { - // hash, - // }, - // ) + await waitForTransactionReceipt( + wagmiConfig, + { + hash, + }, + ) onTransactionConfirmedCallback() } catch (e) { diff --git a/src/packages/v4/views/V4ProjectDashboard/V4ProjectTabs/V4CyclesPayoutsPanel/V4DistributePayoutsModal.tsx b/src/packages/v4/views/V4ProjectDashboard/V4ProjectTabs/V4CyclesPayoutsPanel/V4DistributePayoutsModal.tsx index b559162cc0..dac12a89df 100644 --- a/src/packages/v4/views/V4ProjectDashboard/V4ProjectTabs/V4CyclesPayoutsPanel/V4DistributePayoutsModal.tsx +++ b/src/packages/v4/views/V4ProjectDashboard/V4ProjectTabs/V4CyclesPayoutsPanel/V4DistributePayoutsModal.tsx @@ -45,7 +45,7 @@ export default function V4DistributePayoutsModal({ const [loading, setLoading] = useState() const [distributionAmount, setDistributionAmount] = useState() - const { writeContractAsync: writeSendPayouts, data } = + const { writeContractAsync: writeSendPayouts } = useWriteJbMultiTerminalSendPayoutsOf() async function executeDistributePayoutsTx() { diff --git a/src/packages/v4/views/V4ProjectDashboard/V4ProjectTabs/V4TokensPanel/V4ClaimTokensModal.tsx b/src/packages/v4/views/V4ProjectDashboard/V4ProjectTabs/V4TokensPanel/V4ClaimTokensModal.tsx new file mode 100644 index 0000000000..00c03e335b --- /dev/null +++ b/src/packages/v4/views/V4ProjectDashboard/V4ProjectTabs/V4TokensPanel/V4ClaimTokensModal.tsx @@ -0,0 +1,199 @@ +import { WarningOutlined } from '@ant-design/icons' +import { t, Trans } from '@lingui/macro' +import { waitForTransactionReceipt } from '@wagmi/core' +import { Descriptions, Form } from 'antd' +import InputAccessoryButton from 'components/buttons/InputAccessoryButton' +import EthereumAddress from 'components/EthereumAddress' +import FormattedNumberInput from 'components/inputs/FormattedNumberInput' +import TransactionModal from 'components/modals/TransactionModal' +import { TxHistoryContext } from 'contexts/Transaction/TxHistoryContext' +import useSymbolOfERC20 from 'hooks/ERC20/useSymbolOfERC20' +import { useWallet } from 'hooks/Wallet' +import { NativeTokenValue, useJBContractContext, useReadJbTokensCreditBalanceOf, useReadJbTokensTokenOf, useWriteJbControllerClaimTokensFor } from 'juice-sdk-react' +import { useProjectHasErc20Token } from 'packages/v4/hooks/useProjectHasErc20Token' +import { wagmiConfig } from 'packages/v4/wagmiConfig' +import { useContext, useLayoutEffect, useState } from 'react' +import { fromWad, parseWad } from 'utils/format/formatNumber' +import { emitErrorNotification } from 'utils/notifications' +import { tokenSymbolText } from 'utils/tokenSymbolText' +import { zeroAddress } from 'viem' + +export function V4ClaimTokensModal({ + open, + onCancel, + onConfirmed, +}: { + open?: boolean + onCancel?: VoidFunction + onConfirmed?: VoidFunction +}) { + const { projectId, contracts } = useJBContractContext() + const { addTransaction } = useContext(TxHistoryContext) + + const { data: tokenAddress } = useReadJbTokensTokenOf() + const { data: tokenSymbol } = useSymbolOfERC20(tokenAddress) + + const [loading, setLoading] = useState() + const [transactionPending, setTransactionPending] = useState() + const [claimAmount, setClaimAmount] = useState() + + const { userAddress } = useWallet() + + const { writeContractAsync: writeClaimTokens } = + useWriteJbControllerClaimTokensFor() + + const hasIssuedTokens = useProjectHasErc20Token() + + const { data: unclaimedBalance } = useReadJbTokensCreditBalanceOf({ + args: [userAddress ?? zeroAddress, projectId], + }) + + useLayoutEffect(() => { + setClaimAmount(fromWad(unclaimedBalance)) + }, [unclaimedBalance]) + + async function executeClaimTokensTx() { + if ( + !contracts.controller.data || + !claimAmount || + !userAddress || + !projectId + ) + return + + setLoading(true) + + const args = [ + userAddress, + projectId, + parseWad(claimAmount).toBigInt(), + userAddress + ] as const + + try { + // SIMULATE TX: + // const encodedData = encodeFunctionData({ + // abi: jbControllerAbi, + // functionName: 'claimTokensFor', + // args, + // }) + // console.log('encodedData:', encodedData) + // console.log('contract:', contracts.controller.data) + + const hash = await writeClaimTokens({ + address: contracts.controller.data, + args, + }) + setTransactionPending(true) + + addTransaction?.('Claim tokens as ERC20', { hash }) + await waitForTransactionReceipt(wagmiConfig, { + hash, + }) + + setLoading(false) + setTransactionPending(false) + onConfirmed?.() + } catch (e) { + setLoading(false) + + emitErrorNotification((e as unknown as Error).message) + } + } + + const tokenTextLong = tokenSymbolText({ + tokenSymbol, + plural: true, + includeTokenWord: true, + }) + + const tokenTextShort = tokenSymbolText({ + tokenSymbol, + plural: true, + }) + + return ( + +
+ {!hasIssuedTokens && ( +
+ {' '} + + Tokens cannot be claimed because the project owner has not issued + an ERC-20 for this project. + +
+ )} + +
+

+ + Claiming {tokenTextLong} will convert your {tokenTextShort}{' '} + balance to ERC-20 tokens and mint them to your wallet. + +

+

+ + If you're not sure if you need to claim, you probably don't. + +

+

+ + You can redeem your {tokenTextLong} for ETH without claiming them. + You can transfer your unclaimed {tokenTextLong} to another address + from the Tools menu, which can be accessed from the wrench icon in + the upper right-hand corner of this project. + +

+
+ + + Your unclaimed {tokenTextLong}} + > + + + + {hasIssuedTokens && tokenSymbol && ( + {tokenSymbol} ERC-20 address} + > + + + )} + + +
+ + setClaimAmount(fromWad(unclaimedBalance))} + /> + } + onChange={val => setClaimAmount(val)} + /> + +
+
+
+ ) +} diff --git a/src/packages/v4/views/V4ProjectDashboard/V4ProjectTabs/V4TokensPanel/V4TokensPanel.tsx b/src/packages/v4/views/V4ProjectDashboard/V4ProjectTabs/V4TokensPanel/V4TokensPanel.tsx index a302700ae0..8a3944fd87 100644 --- a/src/packages/v4/views/V4ProjectDashboard/V4ProjectTabs/V4TokensPanel/V4TokensPanel.tsx +++ b/src/packages/v4/views/V4ProjectDashboard/V4ProjectTabs/V4TokensPanel/V4TokensPanel.tsx @@ -15,9 +15,11 @@ import { useJBContractContext } from 'juice-sdk-react' import { V4TokenHoldersModal } from 'packages/v4/components/modals/V4TokenHoldersModal/V4TokenHoldersModal' import { v4ProjectRoute } from 'packages/v4/utils/routes' import { useCallback, useState } from 'react' +import { reloadWindow } from 'utils/windowUtils' import { useChainId } from 'wagmi' import { useV4TokensPanel } from './hooks/useV4TokensPanel' import { useV4YourBalanceMenuItems } from './hooks/useV4YourBalanceMenuItems' +import { V4ClaimTokensModal } from './V4ClaimTokensModal' import { V4ReservedTokensSubPanel } from './V4ReservedTokensSubPanel' export const V4TokensPanel = () => { @@ -45,7 +47,7 @@ export const V4TokensPanel = () => { items, // redeemModalVisible, // setRedeemModalVisible, - // claimTokensModalVisible, + claimTokensModalVisible, setClaimTokensModalVisible, // mintModalVisible, // setMintModalVisible, @@ -68,7 +70,7 @@ export const V4TokensPanel = () => { title={t`Your balance`} description={ - {userTokenBalance.format()} tokens + {userTokenBalance.format(8)} tokens
{/* {projectHasErc20Token && ( + ) +} diff --git a/src/packages/v4/components/ProjectDashboard/components/V4ProjectToolsDrawer/PaymentAddressSection/LaunchProjectPayerButton.tsx b/src/packages/v4/components/ProjectDashboard/components/V4ProjectToolsDrawer/PaymentAddressSection/LaunchProjectPayerButton.tsx new file mode 100644 index 0000000000..5b8b43f584 --- /dev/null +++ b/src/packages/v4/components/ProjectDashboard/components/V4ProjectToolsDrawer/PaymentAddressSection/LaunchProjectPayerButton.tsx @@ -0,0 +1,23 @@ +import { Trans } from '@lingui/macro' +import { Button } from 'antd' +import { useState } from 'react' +import { LaunchProjectPayerModal } from './LaunchProjectPayerModal/LaunchProjectPayerModal' + +export function LaunchProjectPayerButton() { + const [modalVisible, setModalVisible] = useState(false) + + return ( + <> + + + setModalVisible(false)} + /> + + ) +} diff --git a/src/packages/v4/components/ProjectDashboard/components/V4ProjectToolsDrawer/PaymentAddressSection/LaunchProjectPayerModal/AdvancedOptionsCollapse.tsx b/src/packages/v4/components/ProjectDashboard/components/V4ProjectToolsDrawer/PaymentAddressSection/LaunchProjectPayerModal/AdvancedOptionsCollapse.tsx new file mode 100644 index 0000000000..232e6a9ae0 --- /dev/null +++ b/src/packages/v4/components/ProjectDashboard/components/V4ProjectToolsDrawer/PaymentAddressSection/LaunchProjectPayerModal/AdvancedOptionsCollapse.tsx @@ -0,0 +1,132 @@ +import { t, Trans } from '@lingui/macro' +import { Form, FormInstance, Input, Switch } from 'antd' +import { EthAddressInput } from 'components/inputs/EthAddressInput' +import { FormImageUploader } from 'components/inputs/FormImageUploader' +import { MinimalCollapse } from 'components/MinimalCollapse' +import TooltipLabel from 'components/TooltipLabel' +import { V2V3ProjectContext } from 'packages/v2v3/contexts/Project/V2V3ProjectContext' +import { useContext, useState } from 'react' +import { isZeroAddress } from 'utils/address' + +import type { AdvancedOptionsFields } from './LaunchProjectPayerModal' + +const defaultAdvancedOptions: AdvancedOptionsFields = { + memo: '', + memoImageUrl: undefined, + tokenMintingEnabled: true, + preferClaimed: false, + customBeneficiaryAddress: undefined, +} + +export default function AdvancedOptionsCollapse({ + form, +}: { + form: FormInstance +}) { + const { tokenAddress } = useContext(V2V3ProjectContext) + + // need state for this field to update dom + const [tokenMintingEnabled, setTokenMintingEnabled] = useState( + form.getFieldValue('tokenMintingEnabled') === false ? false : true, + ) + + const [customBeneficiaryEnabled, setCustomBeneficiaryEnabled] = + useState(Boolean(form.getFieldValue('customBeneficiaryAddress'))) + + return ( + Advanced (optional)}> +
+
+
+ + The onchain memo for each payment made through this address. + The project's payment feed will include the memo alongside the + payment. + + } + /> + + + + + + +
+
+ + + + +
+ {tokenMintingEnabled && + tokenAddress && + !isZeroAddress(tokenAddress) ? ( +
+ + When checked, payments made through this address will mint + the project's ERC-20 tokens. Payments will incur slightly + higher gas fees. When unchecked, the Juicebox protocol will + internally track the beneficiary's tokens, and they can + claim their ERC-20 tokens at any time. + + } + /> + + + +
+ ) : null} + + {tokenMintingEnabled ? ( +
+ + If enabled, project tokens will be minted to a custom + beneficiary address. By default, project tokens will be + minted to the wallet that pays this address. + + } + /> + { + setCustomBeneficiaryEnabled(checked) + if (!checked) { + form.setFieldsValue({ customBeneficiaryAddress: undefined }) + } + }} + checked={customBeneficiaryEnabled} + /> +
+ ) : null} + {tokenMintingEnabled && customBeneficiaryEnabled ? ( + + form.setFieldsValue({ customBeneficiaryAddress: value }) + } + /> + ) : null} + +
+
+ ) +} diff --git a/src/packages/v4/components/ProjectDashboard/components/V4ProjectToolsDrawer/PaymentAddressSection/LaunchProjectPayerModal/LaunchProjectPayerModal.tsx b/src/packages/v4/components/ProjectDashboard/components/V4ProjectToolsDrawer/PaymentAddressSection/LaunchProjectPayerModal/LaunchProjectPayerModal.tsx new file mode 100644 index 0000000000..6eca533420 --- /dev/null +++ b/src/packages/v4/components/ProjectDashboard/components/V4ProjectToolsDrawer/PaymentAddressSection/LaunchProjectPayerModal/LaunchProjectPayerModal.tsx @@ -0,0 +1,159 @@ +import { ToolOutlined } from '@ant-design/icons' +import { t, Trans } from '@lingui/macro' +import { Modal } from 'antd' +import { useForm } from 'antd/lib/form/Form' +import CopyTextButton from 'components/buttons/CopyTextButton' +import { Callout } from 'components/Callout/Callout' +import EtherscanLink from 'components/EtherscanLink' +import TransactionModal from 'components/modals/TransactionModal' +import { PROJECT_PAYER_ADDRESS_EXPLANATION } from 'components/strings' +import { providers } from 'ethers' +import { useState } from 'react' +import AdvancedOptionsCollapse from './AdvancedOptionsCollapse' + +const DEPLOY_EVENT_IDX = 0 + +/** + * Return the address of the project payer created from a `deployProjectPayer` transaction. + * @param txReceipt receipt of `deployProjectPayer` transaction + */ +const getProjectPayerAddressFromReceipt = ( + txReceipt: providers.TransactionReceipt, +): string => { + const newProjectPayerAddress = txReceipt?.logs[DEPLOY_EVENT_IDX]?.address + return newProjectPayerAddress +} + +export interface AdvancedOptionsFields { + memo: string + memoImageUrl: string | undefined + tokenMintingEnabled: boolean + customBeneficiaryAddress: string | undefined + preferClaimed: boolean +} + +export function LaunchProjectPayerModal({ + open, + onClose, + onConfirmed, +}: { + open: boolean + onClose: VoidFunction + onConfirmed?: VoidFunction +}) { + const [loadingProjectPayer, setLoadingProjectPayer] = useState() + const [transactionPending, setTransactionPending] = useState() + const [projectPayerAddress, setProjectPayerAddress] = useState() + + const [advancedOptionsForm] = useForm() + + const [confirmedModalVisible, setConfirmedModalVisible] = useState() + + // const deployProjectPayerTx = useDeployProjectPayerTx() + + // async function deployProjectPayer() { + // if (!deployProjectPayerTx) return + + // setLoadingProjectPayer(true) + + // const fields = advancedOptionsForm.getFieldsValue(true) + // const memo = [fields.memo ?? '', fields.memoImageUrl ?? ''].join(' ').trim() + + // const txSuccess = await deployProjectPayerTx( + // { + // customBeneficiaryAddress: fields.customBeneficiaryAddress, + // customMemo: memo.length > 0 ? memo : undefined, + // tokenMintingEnabled: fields.tokenMintingEnabled, + // preferClaimed: fields.preferClaimed, + // }, + // { + // onDone() { + // setTransactionPending(true) + // }, + // async onConfirmed(tx) { + // const txHash = tx?.hash + // if (!txHash) { + // return + // } + + // const txReceipt = await readProvider.getTransactionReceipt(txHash) + // const newProjectPayerAddress = + // getProjectPayerAddressFromReceipt(txReceipt) + // if (newProjectPayerAddress === undefined) { + // emitErrorNotification(t`Something went wrong.`) + // return + // } + // if (onConfirmed) onConfirmed() + // onClose() + // setProjectPayerAddress(newProjectPayerAddress) + // setLoadingProjectPayer(false) + // setTransactionPending(false) + // setConfirmedModalVisible(true) + // advancedOptionsForm.resetFields() + // }, + // }, + // ) + // if (!txSuccess) { + // setLoadingProjectPayer(false) + // setTransactionPending(false) + // } + // } + + return ( + <> + null}//deployProjectPayer} + onCancel={() => onClose()} + confirmLoading={loadingProjectPayer} + transactionPending={transactionPending} + width={600} + > +
+
{PROJECT_PAYER_ADDRESS_EXPLANATION}
+
+ + By default, the payer will receive any project tokens minted from + the payment. + +
+ + + + Contributors who pay this address from a custodial service + platform (like Coinbase){' '} + won't receive project tokens. + + + +
+
+ setConfirmedModalVisible(false)} + cancelButtonProps={{ hidden: true }} + okText={t`Done`} + centered + > +

+ Your new project payer address: +

+ {' '} + +

+ + Existing project payer addresses can be found in the Tools drawer ( + ) on the project page. + +

+
+ + ) +} diff --git a/src/packages/v4/components/ProjectDashboard/components/V4ProjectToolsDrawer/PaymentAddressSection/PaymentAddressSection.tsx b/src/packages/v4/components/ProjectDashboard/components/V4ProjectToolsDrawer/PaymentAddressSection/PaymentAddressSection.tsx new file mode 100644 index 0000000000..89e74c9358 --- /dev/null +++ b/src/packages/v4/components/ProjectDashboard/components/V4ProjectToolsDrawer/PaymentAddressSection/PaymentAddressSection.tsx @@ -0,0 +1,49 @@ +import { PROJECT_PAYER_ADDRESS_EXPLANATION } from 'components/strings' +import { LaunchProjectPayerButton } from './LaunchProjectPayerButton' + +export function PaymentAddressSection() { + // const { projectId } = useContext(ProjectMetadataContext) + + // const [projectPayersModalIsVisible, setProjectPayersModalIsVisible] = + // useState() + + // const { data, loading } = useEtherc20ProjectPayersQuery({ + // client, + // variables: { + // where: { + // projectId, + // }, + // }, + // }) + + // const projectPayers = data?.etherc20ProjectPayers + + return ( + <> +

{PROJECT_PAYER_ADDRESS_EXPLANATION}

+ +

+ {/* */} + {/* setProjectPayersModalIsVisible(false)} + projectPayers={projectPayers} + /> */} +

+ + + + ) +} diff --git a/src/packages/v4/components/ProjectDashboard/components/V4ProjectToolsDrawer/V4ProjectToolsDrawer.tsx b/src/packages/v4/components/ProjectDashboard/components/V4ProjectToolsDrawer/V4ProjectToolsDrawer.tsx new file mode 100644 index 0000000000..a6bbde9e60 --- /dev/null +++ b/src/packages/v4/components/ProjectDashboard/components/V4ProjectToolsDrawer/V4ProjectToolsDrawer.tsx @@ -0,0 +1,92 @@ +import { Trans } from '@lingui/macro' +import { Divider, Drawer } from 'antd' +import useMobile from 'hooks/useMobile' +import { AddToProjectBalanceForm } from './AddToProjectBalanceForm' + +export function V4ProjectToolsDrawer({ + open, + onClose, +}: { + open?: boolean + onClose?: VoidFunction +}) { + // const hasOFAC = projectMetadata?.projectRequiredOFACCheck + + const isMobile = useMobile() + + return ( + +

+ Tools +

+ +
+ {/* {hasOFAC ? null : ( */} + <> +
+ +
+ + + {/* @v4todo:
+

+ Project payer addresses +

+ + +
+ */} + + {/* )} */} + + {/* + groupedSplits={{ + splits: payoutSplits, + group: ETH_PAYOUT_SPLIT_GROUP, + }} + > + Export payouts CSV + + ) : undefined + } + exportReservedTokensButton={ + reservedTokensSplits ? ( + + groupedSplits={{ + splits: reservedTokensSplits, + group: RESERVED_TOKEN_SPLIT_GROUP, + }} + > + Export reserved token recipient CSV + + ) : undefined + } + /> */} + + {/* + +
+

+ Project contracts directory +

+ +

+ Browse the project's smart contract addresses.{' '} + + Go to contracts directory + + . +

+
*/} +
+
+ ) +} diff --git a/src/packages/v4/components/ProjectDashboard/components/V4ProjectToolsDrawer/index.tsx b/src/packages/v4/components/ProjectDashboard/components/V4ProjectToolsDrawer/index.tsx new file mode 100644 index 0000000000..321faa4723 --- /dev/null +++ b/src/packages/v4/components/ProjectDashboard/components/V4ProjectToolsDrawer/index.tsx @@ -0,0 +1,2 @@ +export { V4ProjectToolsDrawer } from './V4ProjectToolsDrawer'; + diff --git a/src/packages/v4/views/V4ProjectDashboard/V4ProjectHeader.tsx b/src/packages/v4/views/V4ProjectDashboard/V4ProjectHeader.tsx index db5bcef5f3..decdad49b9 100644 --- a/src/packages/v4/views/V4ProjectDashboard/V4ProjectHeader.tsx +++ b/src/packages/v4/views/V4ProjectDashboard/V4ProjectHeader.tsx @@ -11,6 +11,7 @@ import { SocialLinkButton } from 'components/Project/ProjectHeader/SocialLinkBut import { TruncatedText } from 'components/TruncatedText' import useMobile from 'hooks/useMobile' import Link from 'next/link' +import { ProjectHeaderPopupMenu } from 'packages/v4/components/ProjectDashboard/components/ProjectHeaderPopupMenu' import V4ProjectHandleLink from 'packages/v4/components/V4ProjectHandleLink' import { useV4WalletHasPermission } from 'packages/v4/hooks/useV4WalletHasPermission' import { V4OperatorPermission } from 'packages/v4/models/v4Permissions' @@ -57,8 +58,7 @@ export const V4ProjectHeader = ({ className }: { className?: string }) => {
{projectId ? ( isMobile ? ( - // - <> + ) : ( <>
@@ -73,7 +73,7 @@ export const V4ProjectHeader = ({ className }: { className?: string }) => { /> ))}
- {/* @v4todo: */} + {canQueueRuleSets && ( Date: Tue, 1 Oct 2024 11:57:10 +1000 Subject: [PATCH 05/13] fix: smol v4 project page bug (#4481) --- .../V4ProjectTabs/V4ActivityPanel/V4ActivityPanel.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/packages/v4/views/V4ProjectDashboard/V4ProjectTabs/V4ActivityPanel/V4ActivityPanel.tsx b/src/packages/v4/views/V4ProjectDashboard/V4ProjectTabs/V4ActivityPanel/V4ActivityPanel.tsx index cb8274affc..34829d4315 100644 --- a/src/packages/v4/views/V4ProjectDashboard/V4ProjectTabs/V4ActivityPanel/V4ActivityPanel.tsx +++ b/src/packages/v4/views/V4ProjectDashboard/V4ProjectTabs/V4ActivityPanel/V4ActivityPanel.tsx @@ -20,7 +20,7 @@ export function V4ActivityPanel() { } }) - const createdAt = data?.projects?.[0].createdAt + const createdAt = data?.projects?.[0]?.createdAt return (
From a78f919dc48f77d541ff2ad70c9af485e6ceb96f Mon Sep 17 00:00:00 2001 From: Johnny D Date: Wed, 2 Oct 2024 07:24:02 +1000 Subject: [PATCH 06/13] fix: v2v3 settings mint rate field bug (#4482) --- .../pages/EditCyclePage/TokensSection/MintRateField.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/packages/v2v3/components/V2V3Project/V2V3ProjectSettings/pages/EditCyclePage/TokensSection/MintRateField.tsx b/src/packages/v2v3/components/V2V3Project/V2V3ProjectSettings/pages/EditCyclePage/TokensSection/MintRateField.tsx index d9aaddb0e7..9b708bba3f 100644 --- a/src/packages/v2v3/components/V2V3Project/V2V3ProjectSettings/pages/EditCyclePage/TokensSection/MintRateField.tsx +++ b/src/packages/v2v3/components/V2V3Project/V2V3ProjectSettings/pages/EditCyclePage/TokensSection/MintRateField.tsx @@ -3,10 +3,9 @@ import { Form } from 'antd' import FormattedNumberInput from 'components/inputs/FormattedNumberInput' import { MAX_MINT_RATE } from 'packages/v2v3/utils/math' -// Note: "issuanceRate" = "mintRate" export function MintRateField() { return ( - + Date: Thu, 3 Oct 2024 17:45:18 +1000 Subject: [PATCH 07/13] fix: minor v4 project page bugs (#4484) --- .../hooks/useReservedTokensSubPanel.test.ts | 2 +- .../hooks/useReservedTokensSubPanel.ts | 4 ++-- .../useV4FormatConfigurationTokenSection.ts | 20 ++++++++++++------- .../hooks/useV4ReservedTokensSubPanel.ts | 4 ++-- 4 files changed, 18 insertions(+), 12 deletions(-) diff --git a/src/packages/v2v3/components/V2V3Project/ProjectDashboard/components/TokensPanel/hooks/useReservedTokensSubPanel.test.ts b/src/packages/v2v3/components/V2V3Project/ProjectDashboard/components/TokensPanel/hooks/useReservedTokensSubPanel.test.ts index eeebceb6e3..9f58b80e9b 100644 --- a/src/packages/v2v3/components/V2V3Project/ProjectDashboard/components/TokensPanel/hooks/useReservedTokensSubPanel.test.ts +++ b/src/packages/v2v3/components/V2V3Project/ProjectDashboard/components/TokensPanel/hooks/useReservedTokensSubPanel.test.ts @@ -57,7 +57,7 @@ describe('useReservedTokensSubPanel', () => { const { result } = renderHook(useReservedTokensSubPanel) expect(result.current.reservedList).toEqual([ { - projectId: 1, + projectId: 0, address: '0x0000000000000000000000000000000000000000', percent: '97%', }, diff --git a/src/packages/v2v3/components/V2V3Project/ProjectDashboard/components/TokensPanel/hooks/useReservedTokensSubPanel.ts b/src/packages/v2v3/components/V2V3Project/ProjectDashboard/components/TokensPanel/hooks/useReservedTokensSubPanel.ts index 866e083c40..30cb1ceab4 100644 --- a/src/packages/v2v3/components/V2V3Project/ProjectDashboard/components/TokensPanel/hooks/useReservedTokensSubPanel.ts +++ b/src/packages/v2v3/components/V2V3Project/ProjectDashboard/components/TokensPanel/hooks/useReservedTokensSubPanel.ts @@ -31,7 +31,7 @@ export const useReservedTokensSubPanel = () => { if (reservedTokensSplits?.length === 0) return [ { - projectId, + projectId: 0, address: projectOwnerAddress!, percent: `${formatSplitPercent(ONE_BILLION_BIG)}%`, }, @@ -72,7 +72,7 @@ export const useReservedTokensSubPanel = () => { // If it isn't, add a split at the beginning which brings the total percentage to 100%. else processedSplits.unshift({ - projectId, + projectId: 0, address: projectOwnerAddress!, percent: `${formatSplitPercent(remainingPercentage)}%`, }) diff --git a/src/packages/v4/views/V4ProjectDashboard/V4ProjectTabs/V4CyclesPayoutsPanel/hooks/useV4FormatConfigurationTokenSection.ts b/src/packages/v4/views/V4ProjectDashboard/V4ProjectTabs/V4CyclesPayoutsPanel/hooks/useV4FormatConfigurationTokenSection.ts index bca38922ca..ba6a1e5247 100644 --- a/src/packages/v4/views/V4ProjectDashboard/V4ProjectTabs/V4CyclesPayoutsPanel/hooks/useV4FormatConfigurationTokenSection.ts +++ b/src/packages/v4/views/V4ProjectDashboard/V4ProjectTabs/V4CyclesPayoutsPanel/hooks/useV4FormatConfigurationTokenSection.ts @@ -7,6 +7,7 @@ import { flagPairToDatum } from 'components/Project/ProjectTabs/utils/flagPairTo import { pairToDatum } from 'components/Project/ProjectTabs/utils/pairToDatum' import { JBRulesetData, JBRulesetMetadata } from 'juice-sdk-core' import { useMemo } from 'react' +import { formattedNum } from 'utils/format/formatNumber' import { tokenSymbolText } from 'utils/tokenSymbolText' export const useV4FormatConfigurationTokenSection = ({ @@ -32,6 +33,7 @@ export const useV4FormatConfigurationTokenSection = ({ const decayPercentFloat = ruleset?.decayPercent.toFloat() const currentTotalIssuanceRate = ruleset?.weight.toFloat() + const currentTotalIssuanceRateFormatted = formattedNum(currentTotalIssuanceRate) const queuedTotalIssuanceRate = upcomingRuleset ? upcomingRuleset?.weight.toFloat() @@ -39,10 +41,11 @@ export const useV4FormatConfigurationTokenSection = ({ typeof decayPercentFloat !== 'undefined' ? currentTotalIssuanceRate - currentTotalIssuanceRate * decayPercentFloat : undefined + const queuedTotalIssuanceRateFormatted = formattedNum(queuedTotalIssuanceRate) const totalIssuanceRateDatum: ConfigurationPanelDatum = useMemo(() => { - const current = currentTotalIssuanceRate !== undefined - ? `${currentTotalIssuanceRate} ${tokenSymbol}/ETH` + const current = currentTotalIssuanceRateFormatted !== undefined + ? `${currentTotalIssuanceRateFormatted} ${tokenSymbol}/ETH` : undefined if (upcomingRuleset === null || upcomingRulesetLoading) { @@ -50,16 +53,17 @@ export const useV4FormatConfigurationTokenSection = ({ } const queued = queuedTotalIssuanceRate !== undefined - ? `${queuedTotalIssuanceRate} ${tokenSymbol}/ETH` + ? `${queuedTotalIssuanceRateFormatted} ${tokenSymbol}/ETH` : undefined return pairToDatum(t`Total issuance rate`, current, queued) }, [ upcomingRuleset, - currentTotalIssuanceRate, tokenSymbol, queuedTotalIssuanceRate, upcomingRulesetLoading, + currentTotalIssuanceRateFormatted, + queuedTotalIssuanceRateFormatted ]) const reservedPercentFloat = rulesetMetadata?.reservedPercent.toFloat() @@ -73,9 +77,10 @@ export const useV4FormatConfigurationTokenSection = ({ ? currentTotalIssuanceRate - currentTotalIssuanceRate * reservedPercentFloat : undefined + const currentPayerIssuanceRateFormatted = formattedNum(currentPayerIssuanceRate) const current = currentPayerIssuanceRate !== undefined - ? `${currentPayerIssuanceRate} ${tokenSymbol}/ETH` + ? `${currentPayerIssuanceRateFormatted} ${tokenSymbol}/ETH` : undefined if ( @@ -88,11 +93,12 @@ export const useV4FormatConfigurationTokenSection = ({ const _reservedPercent = queuedReservedPercentFloat ?? reservedPercentFloat const queuedPayerIssuanceRate = - queuedTotalIssuanceRate && _reservedPercent + queuedTotalIssuanceRate && _reservedPercent !== undefined ? queuedTotalIssuanceRate - queuedTotalIssuanceRate * _reservedPercent : undefined + const queuedPayerIssuanceRateFormatted = formattedNum(queuedPayerIssuanceRate) const queued = queuedPayerIssuanceRate !== undefined - ? `${queuedPayerIssuanceRate} ${tokenSymbol}/ETH` + ? `${queuedPayerIssuanceRateFormatted} ${tokenSymbol}/ETH` : undefined return pairToDatum(t`Payer issuance rate`, current, queued) diff --git a/src/packages/v4/views/V4ProjectDashboard/V4ProjectTabs/V4TokensPanel/hooks/useV4ReservedTokensSubPanel.ts b/src/packages/v4/views/V4ProjectDashboard/V4ProjectTabs/V4TokensPanel/hooks/useV4ReservedTokensSubPanel.ts index dc02b87b25..63bfad69e2 100644 --- a/src/packages/v4/views/V4ProjectDashboard/V4ProjectTabs/V4TokensPanel/hooks/useV4ReservedTokensSubPanel.ts +++ b/src/packages/v4/views/V4ProjectDashboard/V4ProjectTabs/V4TokensPanel/hooks/useV4ReservedTokensSubPanel.ts @@ -28,7 +28,7 @@ export const useV4ReservedTokensSubPanel = () => { if (reservedTokensSplits?.length === 0) return [ { - projectId: Number(projectId), + projectId: 0, address: projectOwnerAddress!, percent: `${new SplitPortion( SPLITS_TOTAL_PERCENT, @@ -67,7 +67,7 @@ export const useV4ReservedTokensSubPanel = () => { // If it isn't, add a split at the beginning which brings the total percentage to 100%. else processedSplits.unshift({ - projectId: Number(projectId), + projectId: 0, address: projectOwnerAddress!, percent: `${new SplitPortion( remainingPercentage, From 6651f8b0268ae61807077349b77b105e36197111 Mon Sep 17 00:00:00 2001 From: wraeth-eth <104132113+wraeth-eth@users.noreply.github.com> Date: Tue, 15 Oct 2024 18:48:54 +1100 Subject: [PATCH 08/13] Wraeth/spacing fixes (#4485) --- .../components/TimelineViewSelector.tsx | 61 +++++++++++++++++-- .../inputs/FormattedNumberInput.tsx | 4 +- .../modals/V2V3DownloadActivityModal.tsx | 1 + 3 files changed, 58 insertions(+), 8 deletions(-) diff --git a/src/components/VolumeChart/components/TimelineViewSelector.tsx b/src/components/VolumeChart/components/TimelineViewSelector.tsx index 6a2760031e..886d7549ee 100644 --- a/src/components/VolumeChart/components/TimelineViewSelector.tsx +++ b/src/components/VolumeChart/components/TimelineViewSelector.tsx @@ -1,15 +1,65 @@ import { t } from '@lingui/macro' +import { JuiceListbox } from 'components/inputs/JuiceListbox' import React from 'react' -import { classNames } from 'utils/classNames' +import { twMerge } from 'tailwind-merge' import { ProjectTimelineView } from '../types' -export default function TimelineViewSelector({ +export default function TimelineViewSelector(props: { + timelineView: ProjectTimelineView + setTimelineView: React.Dispatch> +}) { + return ( + <> + + + + ) +} + +const MobileTimelineViewSelector = ({ timelineView, setTimelineView, }: { timelineView: ProjectTimelineView setTimelineView: React.Dispatch> -}) { +}) => { + const opts = (): { label: string; value: ProjectTimelineView }[] => [ + { + label: t`Volume`, + value: 'volume', + }, + { + label: t`In Juicebox`, + value: 'balance', + }, + { + label: t`Trending`, + value: 'trendingScore', + }, + ] + + const handleChange = (value: ProjectTimelineView) => { + setTimelineView(value) + } + + return ( + o.value === timelineView)} + onChange={v => handleChange(v.value)} + /> + ) +} + +const DesktopTimelineViewSelector = ({ + timelineView, + setTimelineView, +}: { + timelineView: ProjectTimelineView + setTimelineView: React.Dispatch> +}) => { const tab = (view: ProjectTimelineView) => { const selected = view === timelineView @@ -28,7 +78,7 @@ export default function TimelineViewSelector({ return (
) } - return ( -
+
{tab('volume')} {tab('balance')} {tab('trendingScore')} diff --git a/src/components/inputs/FormattedNumberInput.tsx b/src/components/inputs/FormattedNumberInput.tsx index a7f822ae45..47c86e6a5c 100644 --- a/src/components/inputs/FormattedNumberInput.tsx +++ b/src/components/inputs/FormattedNumberInput.tsx @@ -103,12 +103,12 @@ export default function FormattedNumberInput({ />
{accessory &&
{accessory}
}
- ); + ) } diff --git a/src/packages/v2v3/components/V2V3Project/modals/V2V3DownloadActivityModal.tsx b/src/packages/v2v3/components/V2V3Project/modals/V2V3DownloadActivityModal.tsx index ad288450ca..7912639d75 100644 --- a/src/packages/v2v3/components/V2V3Project/modals/V2V3DownloadActivityModal.tsx +++ b/src/packages/v2v3/components/V2V3Project/modals/V2V3DownloadActivityModal.tsx @@ -42,6 +42,7 @@ export default function V2V3DownloadActivityModal(props: ModalProps) { onChange={val => setBlockNumber(val ? parseInt(val) : undefined)} accessory={ setBlockNumber(latestBlockNumber)} disabled={blockNumber === latestBlockNumber} From 9f0fd73d54616e863735a96fc651dde87e248e7a Mon Sep 17 00:00:00 2001 From: wraeth-eth <104132113+wraeth-eth@users.noreply.github.com> Date: Wed, 16 Oct 2024 11:25:23 +1100 Subject: [PATCH 09/13] stop auto scrolling (#4486) --- src/components/Project/ProjectTabs/ProjectTab.tsx | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/src/components/Project/ProjectTabs/ProjectTab.tsx b/src/components/Project/ProjectTabs/ProjectTab.tsx index 25dbbbbbb2..1f4b84856e 100644 --- a/src/components/Project/ProjectTabs/ProjectTab.tsx +++ b/src/components/Project/ProjectTabs/ProjectTab.tsx @@ -1,5 +1,5 @@ import { Tab } from '@headlessui/react' -import { ReactNode, useRef } from 'react' +import React, { ReactNode, useRef } from 'react' import { twMerge } from 'tailwind-merge' interface ProjectTabProps { @@ -26,14 +26,6 @@ export const ProjectTab: React.FC = ({ onClick={onClick} > {({ selected }) => { - if (selected && tabRef.current) { - tabRef.current.scrollIntoView({ - behavior: 'smooth', - block: 'nearest', - inline: 'start', - }) - } - return (
Date: Sun, 27 Oct 2024 14:09:32 +1100 Subject: [PATCH 10/13] apply nft credit on checkout (#4487) --- src/locales/messages.pot | 3 + .../PayProjectModal/PayProjectModal.tsx | 83 ++++++++--- .../PayProjectModal/hooks/usePayAmounts.ts | 137 ++++++++++++++++++ .../usePayProjectModal/usePayProjectTx.ts | 36 +++-- 4 files changed, 229 insertions(+), 30 deletions(-) create mode 100644 src/packages/v2v3/components/V2V3Project/ProjectDashboard/components/PayRedeemCard/PayProjectModal/hooks/usePayAmounts.ts diff --git a/src/locales/messages.pot b/src/locales/messages.pot index a6d5c40ccf..77a55f2016 100644 --- a/src/locales/messages.pot +++ b/src/locales/messages.pot @@ -296,6 +296,9 @@ msgstr "" msgid "Project tokens" msgstr "" +msgid "NFT Credits" +msgstr "" + msgid "You must grant permission to migrate your V1 tokens." msgstr "" diff --git a/src/packages/v2v3/components/V2V3Project/ProjectDashboard/components/PayRedeemCard/PayProjectModal/PayProjectModal.tsx b/src/packages/v2v3/components/V2V3Project/ProjectDashboard/components/PayRedeemCard/PayProjectModal/PayProjectModal.tsx index 466aede953..24b0fbb66d 100644 --- a/src/packages/v2v3/components/V2V3Project/ProjectDashboard/components/PayRedeemCard/PayProjectModal/PayProjectModal.tsx +++ b/src/packages/v2v3/components/V2V3Project/ProjectDashboard/components/PayRedeemCard/PayProjectModal/PayProjectModal.tsx @@ -3,11 +3,15 @@ import EtherscanLink from 'components/EtherscanLink' import ExternalLink from 'components/ExternalLink' import { JuiceModal } from 'components/modals/JuiceModal' import { Formik } from 'formik' -import Image from "next/legacy/image" +import { useWallet } from 'hooks/Wallet' +import Image from 'next/legacy/image' +import { useNftCredits } from 'packages/v2v3/hooks/JB721Delegate/useNftCredits' +import React, { ReactNode } from 'react' import { twMerge } from 'tailwind-merge' import { helpPagePath } from 'utils/helpPagePath' import { MessageSection } from './components/MessageSection' import { ReceiveSection } from './components/ReceiveSection' +import { usePayAmounts } from './hooks/usePayAmounts' import { PayProjectModalFormValues, usePayProjectModal, @@ -16,8 +20,6 @@ import { export const PayProjectModal: React.FC = () => { const { open, - primaryAmount, - secondaryAmount, validationSchema, isTransactionPending, isTransactionConfirmed, @@ -27,6 +29,7 @@ export const PayProjectModal: React.FC = () => { setOpen, onPaySubmit, } = usePayProjectModal() + const { formattedTotalAmount } = usePayAmounts() return ( @@ -50,7 +53,7 @@ export const PayProjectModal: React.FC = () => { position="top" okLoading={props.isSubmitting || isTransactionPending} okButtonForm="PayProjectModalForm" - okText={t`Pay ${primaryAmount}`} + okText={t`Pay ${formattedTotalAmount.primaryAmount}`} cancelText={ isTransactionPending || isTransactionConfirmed ? t`Close` @@ -97,19 +100,7 @@ export const PayProjectModal: React.FC = () => { ) : ( <>
-
- - Total amount - -
- {primaryAmount}{' '} - {secondaryAmount && ( - - ({secondaryAmount}) - - )} -
-
+ @@ -172,3 +163,61 @@ export const PayProjectModal: React.FC = () => { ) } + +const AmountSection = () => { + const { userAddress } = useWallet() + const { data: nftCredits } = useNftCredits(userAddress) + const { formattedAmount, formattedNftCredits, formattedTotalAmount } = + usePayAmounts() + + const RowData = ({ + label, + primaryAmount, + secondaryAmount, + }: { + label: ReactNode + primaryAmount: ReactNode + secondaryAmount: ReactNode + }) => ( +
+ {label} +
+ {primaryAmount}{' '} + {secondaryAmount && ( + + ({secondaryAmount}) + + )} +
+
+ ) + + if (!nftCredits?.gt(0) || !formattedNftCredits) + return ( + + ) + + return ( +
+ + + +
+ ) +} diff --git a/src/packages/v2v3/components/V2V3Project/ProjectDashboard/components/PayRedeemCard/PayProjectModal/hooks/usePayAmounts.ts b/src/packages/v2v3/components/V2V3Project/ProjectDashboard/components/PayRedeemCard/PayProjectModal/hooks/usePayAmounts.ts new file mode 100644 index 0000000000..074455272f --- /dev/null +++ b/src/packages/v2v3/components/V2V3Project/ProjectDashboard/components/PayRedeemCard/PayProjectModal/hooks/usePayAmounts.ts @@ -0,0 +1,137 @@ +import { useCurrencyConverter } from 'hooks/useCurrencyConverter' +import { useWallet } from 'hooks/Wallet' +import { useNftCredits } from 'packages/v2v3/hooks/JB721Delegate/useNftCredits' +import { + V2V3_CURRENCY_ETH, + V2V3_CURRENCY_USD, +} from 'packages/v2v3/utils/currency' +import { formatCurrencyAmount } from 'packages/v2v3/utils/formatCurrencyAmount' +import React from 'react' +import { fromWad, parseWad } from 'utils/format/formatNumber' +import { useProjectSelector } from '../../../../redux/hooks' +import { usePayProjectModal } from './usePayProjectModal/usePayProjectModal' + +export const usePayAmounts = () => { + const converter = useCurrencyConverter() + const { payAmount } = useProjectSelector(state => state.projectCart) + const { primaryAmount, secondaryAmount } = usePayProjectModal() + const { userAddress } = useWallet() + const { data: nftCreditsData } = useNftCredits(userAddress) + + const payAmountRaw = React.useMemo(() => { + if (!payAmount) return + + switch (payAmount.currency) { + case V2V3_CURRENCY_ETH: + return { + eth: parseWad(payAmount.amount), + usd: converter.weiToUsd(parseWad(payAmount.amount))!, + } + case V2V3_CURRENCY_USD: + return { + eth: converter.usdToWei(payAmount.amount), + usd: parseWad(payAmount.amount), + } + } + }, [converter, payAmount]) + + const appliedNFTCreditsRaw = React.useMemo(() => { + if (!payAmountRaw || !nftCreditsData) return + + const nftCreditsApplied = payAmountRaw.eth.lt(nftCreditsData) + ? payAmountRaw.eth + : nftCreditsData + + const eth = nftCreditsApplied + const usd = parseWad(converter.weiToUsd(nftCreditsApplied))! + + return { + eth, + usd, + } + }, [converter, nftCreditsData, payAmountRaw]) + + const formattedNftCredits = React.useMemo(() => { + if (!appliedNFTCreditsRaw || !payAmount) return + + switch (payAmount.currency) { + case V2V3_CURRENCY_ETH: + return { + primaryAmount: formatCurrencyAmount({ + amount: fromWad(appliedNFTCreditsRaw.eth), + currency: V2V3_CURRENCY_ETH, + }), + secondaryAmount: formatCurrencyAmount({ + amount: fromWad(appliedNFTCreditsRaw.usd), + currency: V2V3_CURRENCY_USD, + }), + } + case V2V3_CURRENCY_USD: + return { + primaryAmount: formatCurrencyAmount({ + amount: fromWad(appliedNFTCreditsRaw.usd), + currency: V2V3_CURRENCY_USD, + }), + secondaryAmount: formatCurrencyAmount({ + amount: fromWad(appliedNFTCreditsRaw.eth), + currency: V2V3_CURRENCY_ETH, + }), + } + } + }, [appliedNFTCreditsRaw, payAmount]) + + const formattedTotalAmount = React.useMemo(() => { + if (!payAmountRaw || !payAmount) return + + if (!appliedNFTCreditsRaw) { + return { + primaryAmount: primaryAmount, + secondaryAmount: secondaryAmount, + } + } + + const totalEth = payAmountRaw.eth.sub(appliedNFTCreditsRaw.eth) + const totalUsd = converter.weiToUsd(parseWad(totalEth)) + + const formattedEth = formatCurrencyAmount({ + amount: fromWad(totalEth), + currency: V2V3_CURRENCY_ETH, + }) + const formattedUsd = formatCurrencyAmount({ + amount: fromWad(totalUsd), + currency: V2V3_CURRENCY_USD, + }) + + switch (payAmount?.currency) { + case V2V3_CURRENCY_ETH: + return { + primaryAmount: formattedEth, + secondaryAmount: formattedUsd, + } + case V2V3_CURRENCY_USD: + return { + primaryAmount: formattedUsd, + secondaryAmount: formattedEth, + } + } + }, [ + appliedNFTCreditsRaw, + converter, + payAmount, + payAmountRaw, + primaryAmount, + secondaryAmount, + ]) + + return { + formattedAmount: { primaryAmount, secondaryAmount }, + formattedNftCredits: { + primaryAmount: formattedNftCredits?.primaryAmount, + secondaryAmount: formattedNftCredits?.secondaryAmount, + }, + formattedTotalAmount: { + primaryAmount: formattedTotalAmount?.primaryAmount, + secondaryAmount: formattedTotalAmount?.secondaryAmount, + }, + } +} diff --git a/src/packages/v2v3/components/V2V3Project/ProjectDashboard/components/PayRedeemCard/PayProjectModal/hooks/usePayProjectModal/usePayProjectTx.ts b/src/packages/v2v3/components/V2V3Project/ProjectDashboard/components/PayRedeemCard/PayProjectModal/hooks/usePayProjectModal/usePayProjectTx.ts index f40a154bb2..b5d64a2cd7 100644 --- a/src/packages/v2v3/components/V2V3Project/ProjectDashboard/components/PayRedeemCard/PayProjectModal/hooks/usePayProjectModal/usePayProjectTx.ts +++ b/src/packages/v2v3/components/V2V3Project/ProjectDashboard/components/PayRedeemCard/PayProjectModal/hooks/usePayProjectModal/usePayProjectTx.ts @@ -5,6 +5,7 @@ import { useCurrencyConverter } from 'hooks/useCurrencyConverter' import { ProjectPayReceipt } from 'packages/v2v3/components/V2V3Project/ProjectDashboard/hooks/useProjectPageQueries' import { useProjectSelector } from 'packages/v2v3/components/V2V3Project/ProjectDashboard/redux/hooks' import { NftRewardsContext } from 'packages/v2v3/contexts/NftRewards/NftRewardsContext' +import { useNftCredits } from 'packages/v2v3/hooks/JB721Delegate/useNftCredits' import { usePayETHPaymentTerminalTx } from 'packages/v2v3/hooks/transactor/usePayETHPaymentTerminalTx' import { useProjectHasErc20 } from 'packages/v2v3/hooks/useProjectHasErc20' import { V2V3_CURRENCY_ETH } from 'packages/v2v3/utils/currency' @@ -36,6 +37,7 @@ export const usePayProjectTx = ({ const { payAmount, chosenNftRewards } = useProjectSelector( state => state.projectCart, ) + const { data: nftCredits } = useNftCredits(userAddress) const { nftRewards: { rewardTiers }, } = useContext(NftRewardsContext) @@ -64,12 +66,20 @@ export const usePayProjectTx = ({ const weiAmount = useMemo(() => { if (!payAmount) { return parseWad(0) - } else if (payAmount.currency === V2V3_CURRENCY_ETH) { - return parseWad(payAmount.amount) - } else { - return converter.usdToWei(payAmount.amount) } - }, [payAmount, converter]) + let weiAmount = + payAmount.currency === V2V3_CURRENCY_ETH + ? parseWad(payAmount.amount) + : converter.usdToWei(payAmount.amount) + if (nftCredits) { + if (nftCredits.gte(weiAmount)) { + weiAmount = parseWad(0) + } else { + weiAmount = weiAmount.sub(nftCredits) + } + } + return weiAmount + }, [payAmount, converter, nftCredits]) const prepareDelegateMetadata = usePrepareDelegatePayMetadata(weiAmount, { nftRewards: chosenNftRewards, @@ -136,17 +146,17 @@ export const usePayProjectTx = ({ } }, [ - projectHasErc20, - buildPayReceipt, chosenNftRewards, - onTransactionConfirmedCallback, - onTransactionErrorCallback, - onTransactionPendingCallback, - payProjectTx, - rewardTiers, - weiAmount, userAddress, + rewardTiers, + payProjectTx, prepareDelegateMetadata, + weiAmount, + projectHasErc20, + onTransactionConfirmedCallback, + buildPayReceipt, + onTransactionPendingCallback, + onTransactionErrorCallback, ], ) } From 8d0b74edb7c1cf951b0c4662c9822280ed468799 Mon Sep 17 00:00:00 2001 From: wraeth-eth <104132113+wraeth-eth@users.noreply.github.com> Date: Sun, 27 Oct 2024 14:19:55 +1100 Subject: [PATCH 11/13] Fix format number input element with div surrounding problematic object (#4488) --- .../inputs/FormattedNumberInput.tsx | 2 +- .../modals/V2V3DownloadActivityModal.tsx | 27 ++++++++++--------- 2 files changed, 15 insertions(+), 14 deletions(-) diff --git a/src/components/inputs/FormattedNumberInput.tsx b/src/components/inputs/FormattedNumberInput.tsx index 47c86e6a5c..944cea899e 100644 --- a/src/components/inputs/FormattedNumberInput.tsx +++ b/src/components/inputs/FormattedNumberInput.tsx @@ -103,7 +103,7 @@ export default function FormattedNumberInput({ />
diff --git a/src/packages/v2v3/components/V2V3Project/modals/V2V3DownloadActivityModal.tsx b/src/packages/v2v3/components/V2V3Project/modals/V2V3DownloadActivityModal.tsx index 7912639d75..d02385d9c0 100644 --- a/src/packages/v2v3/components/V2V3Project/modals/V2V3DownloadActivityModal.tsx +++ b/src/packages/v2v3/components/V2V3Project/modals/V2V3DownloadActivityModal.tsx @@ -37,19 +37,20 @@ export default function V2V3DownloadActivityModal(props: ModalProps) { - setBlockNumber(val ? parseInt(val) : undefined)} - accessory={ - setBlockNumber(latestBlockNumber)} - disabled={blockNumber === latestBlockNumber} - /> - } - className="mb-4" - /> +
+ setBlockNumber(val ? parseInt(val) : undefined)} + accessory={ + setBlockNumber(latestBlockNumber)} + disabled={blockNumber === latestBlockNumber} + /> + } + /> +
diff --git a/src/packages/v4/components/ProjectDashboard/V4PayRedeemCard/PayProjectModal/hooks/useProjectPaymentTokens.ts b/src/packages/v4/components/ProjectDashboard/V4PayRedeemCard/PayProjectModal/hooks/useProjectPaymentTokens.ts index ae6283fb0e..379752e218 100644 --- a/src/packages/v4/components/ProjectDashboard/V4PayRedeemCard/PayProjectModal/hooks/useProjectPaymentTokens.ts +++ b/src/packages/v4/components/ProjectDashboard/V4PayRedeemCard/PayProjectModal/hooks/useProjectPaymentTokens.ts @@ -1,10 +1,11 @@ -import { FixedInt } from 'fpnum' -import { getTokenAToBQuote, NATIVE_TOKEN_DECIMALS } from 'juice-sdk-core' +import { NATIVE_TOKEN_DECIMALS, getTokenAToBQuote } from 'juice-sdk-core' import { useJBRulesetContext, useJBTokenContext, useNativeTokenSymbol, } from 'juice-sdk-react' + +import { FixedInt } from 'fpnum' import { useProjectSelector } from 'packages/v4/components/ProjectDashboard/redux/hooks' import { V4_CURRENCY_USD } from 'packages/v4/utils/currency' import { tokenSymbolText } from 'utils/tokenSymbolText' @@ -48,7 +49,7 @@ export const useProjectPaymentTokens = () => { receivedTickets, receivedTokenSymbolText: receivedTokenSymbolText === 'tokens' - ? 'Project' - : receivedTokenSymbolText, + ? 'Project Token Credits' + : `${receivedTokenSymbolText} Token`, } } diff --git a/src/packages/v4/constants/ballotStrategies/index.ts b/src/packages/v4/constants/ballotStrategies/index.ts new file mode 100644 index 0000000000..b09263a0b2 --- /dev/null +++ b/src/packages/v4/constants/ballotStrategies/index.ts @@ -0,0 +1,83 @@ +import { plural, t } from '@lingui/macro' + +import { readNetwork } from 'constants/networks' +import { SECONDS_IN_DAY } from 'constants/numbers' +import { constants } from 'ethers' +import { NetworkName } from 'models/networkName' +import { ReconfigurationStrategy } from 'models/reconfigurationStrategy' + +type BallotOption = Record< + 'ONE_DAY' | 'THREE_DAY' | 'SEVEN_DAY', + Partial> +> + + +// v4TODO: Apply real v4 addresses +const BALLOT_ADDRESSES: BallotOption = { + ONE_DAY: { + mainnet: '0xDd9303491328F899796319C2b6bD614324b86314', + sepolia: '0x34E2992ea3C3E6CcfCf5bC668B68F285C1EDFE24', + }, + THREE_DAY: { + mainnet: '0x19D8C293D35EA4b2879A864A68D45a2025694929', + sepolia: '0xa2154aBD135be068540073cB4390139906d0FDc6', + }, + SEVEN_DAY: { + mainnet: '0x8E1AEc30063565e597705E71Ba14Dffc4C390Ef0', + sepolia: '0xb958fEa9089208d0Cc990EceCEcc42458F1B618e', + }, +} + +interface BallotStrategy { + id: ReconfigurationStrategy + name: string + description: string + address: string + durationSeconds: number +} + +const durationBallotStrategyDescription = (days: number) => + plural(days, { + one: 'Edits to an upcoming ruleset cycle must be submitted at least # day before that ruleset cycle starts.', + other: + 'Edits to an upcoming ruleset cycle must be submitted at least # days before that ruleset cycle starts.', + }) + +export function ballotStrategiesFn({ + network, +}: { + network?: NetworkName +} = {}): BallotStrategy[] { + const ballotStrategies: BallotStrategy[] = [ + { + id: 'none', + name: t`No deadline`, + description: t`Edits to upcoming ruleset cycles will take effect when the current cycle ends. A project with no deadline is vulnerable to last-second edits by its owner.`, + address: constants.AddressZero, + durationSeconds: 0, + }, + { + id: 'oneDay', + name: t`1-day deadline`, + description: durationBallotStrategyDescription(1), + address: BALLOT_ADDRESSES.ONE_DAY[network ?? readNetwork.name]!, + durationSeconds: SECONDS_IN_DAY, + }, + { + id: 'threeDay', + name: t`3-day deadline`, + description: durationBallotStrategyDescription(3), + address: BALLOT_ADDRESSES.THREE_DAY[network ?? readNetwork.name]!, + durationSeconds: SECONDS_IN_DAY * 3, + }, + { + id: 'sevenDay', + name: t`7-day deadline`, + description: durationBallotStrategyDescription(7), + address: BALLOT_ADDRESSES.SEVEN_DAY[network ?? readNetwork.name]!, + durationSeconds: SECONDS_IN_DAY * 7, + }, + ] + return ballotStrategies +} + From 41996839a4d0cccc2eb90b20aeb97902405c42a0 Mon Sep 17 00:00:00 2001 From: wraeth-eth <104132113+wraeth-eth@users.noreply.github.com> Date: Fri, 1 Nov 2024 16:51:34 +1100 Subject: [PATCH 13/13] Allow transfer of unclaimed tokens for projects with no erc20 (#4492) --- .../ProjectDashboard/hooks/useUnclaimedTokenBalance.ts | 9 +++++---- .../useBalanceMenuItemsUserFlags.ts | 7 +++++-- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/src/packages/v2v3/components/V2V3Project/ProjectDashboard/hooks/useUnclaimedTokenBalance.ts b/src/packages/v2v3/components/V2V3Project/ProjectDashboard/hooks/useUnclaimedTokenBalance.ts index edfec86d9d..9ae20bbdf5 100644 --- a/src/packages/v2v3/components/V2V3Project/ProjectDashboard/hooks/useUnclaimedTokenBalance.ts +++ b/src/packages/v2v3/components/V2V3Project/ProjectDashboard/hooks/useUnclaimedTokenBalance.ts @@ -12,12 +12,13 @@ export const useUnclaimedTokenBalance = () => { const { data: claimedBalance } = useERC20BalanceOf(tokenAddress, userAddress) const { data: totalBalance } = useTotalBalanceOf(userAddress, projectId) - if ( - typeof claimedBalance === 'undefined' || - typeof totalBalance === 'undefined' - ) { + if (typeof totalBalance === 'undefined') { return undefined } + if (typeof claimedBalance === 'undefined') { + return totalBalance + } + return totalBalance.sub(claimedBalance) } diff --git a/src/packages/v2v3/components/V2V3Project/ProjectDashboard/hooks/useYourBalanceMenuItems/useBalanceMenuItemsUserFlags.ts b/src/packages/v2v3/components/V2V3Project/ProjectDashboard/hooks/useYourBalanceMenuItems/useBalanceMenuItemsUserFlags.ts index 5d315a4305..48653d41db 100644 --- a/src/packages/v2v3/components/V2V3Project/ProjectDashboard/hooks/useYourBalanceMenuItems/useBalanceMenuItemsUserFlags.ts +++ b/src/packages/v2v3/components/V2V3Project/ProjectDashboard/hooks/useYourBalanceMenuItems/useBalanceMenuItemsUserFlags.ts @@ -47,8 +47,11 @@ export const useBalanceMenuItemsUserFlags = () => { ) const canTransferTokens = useMemo( - () => !!unclaimedTokenBalance?.gt(0) || isDev, - [unclaimedTokenBalance, isDev], + () => + (!!unclaimedTokenBalance?.gt(0) && + !fundingCycleMetadata?.global.pauseTransfers) || + isDev, + [unclaimedTokenBalance, fundingCycleMetadata?.global.pauseTransfers, isDev], ) return {