diff --git a/src/locales/messages.pot b/src/locales/messages.pot index 08f3ebe39c..a6d5c40ccf 100644 --- a/src/locales/messages.pot +++ b/src/locales/messages.pot @@ -92,6 +92,9 @@ msgstr "" msgid "Claim {tokensLabel} as ERC-20" msgstr "" +msgid "Save project" +msgstr "" + msgid "Total issuance" msgstr "" @@ -2807,6 +2810,9 @@ msgstr "" msgid "We've disabled payments because the project has opted to reserve 100% of new tokens. You would receive no tokens from your payment." msgstr "" +msgid "Get notifications" +msgstr "" + msgid "Unarchiving your project has the following effects:" msgstr "" diff --git a/src/packages/v1/components/V1Project/V1ProjectToolsDrawer/V1ProjectToolsDrawer.tsx b/src/packages/v1/components/V1Project/V1ProjectToolsDrawer/V1ProjectToolsDrawer.tsx index dc342a7ff0..884f91356f 100644 --- a/src/packages/v1/components/V1Project/V1ProjectToolsDrawer/V1ProjectToolsDrawer.tsx +++ b/src/packages/v1/components/V1Project/V1ProjectToolsDrawer/V1ProjectToolsDrawer.tsx @@ -1,13 +1,13 @@ import { Trans } from '@lingui/macro' import { Divider, Drawer, Space, Tabs } from 'antd' -import { AddToProjectBalanceForm } from 'components/Project/ProjectToolsDrawer/AddToProjectBalanceForm' -import { ExportSection } from 'components/Project/ProjectToolsDrawer/ExportSection' -import { TransferOwnershipForm } from 'components/Project/ProjectToolsDrawer/TransferOwnershipForm' import { useIsUserAddress } from 'hooks/useIsUserAddress' import { V1ProjectContext } from 'packages/v1/contexts/Project/V1ProjectContext' import { useAddToBalanceTx } from 'packages/v1/hooks/transactor/useAddToBalanceTx' import { useSafeTransferFromTx } from 'packages/v1/hooks/transactor/useSafeTransferFromTx' import { useSetProjectUriTx } from 'packages/v1/hooks/transactor/useSetProjectUriTx' +import { AddToProjectBalanceForm } from 'packages/v2v3/components/V2V3Project/V2V3ProjectToolsDrawer/AddToProjectBalanceForm' +import { ExportSection } from 'packages/v2v3/components/V2V3Project/V2V3ProjectToolsDrawer/ExportSection' +import { TransferOwnershipForm } from 'packages/v2v3/components/V2V3Project/V2V3ProjectToolsDrawer/TransferOwnershipForm' import { useContext } from 'react' import ArchiveV1Project from './ArchiveV1Project' import { ExportPayoutModsButton } from './ExportPayoutModsButton' diff --git a/src/packages/v2v3/components/V2V3Project/ProjectDashboard/components/ProjectHeaderPopupMenu.tsx b/src/packages/v2v3/components/V2V3Project/ProjectDashboard/components/ProjectHeaderPopupMenu.tsx new file mode 100644 index 0000000000..1e4e1a6d5c --- /dev/null +++ b/src/packages/v2v3/components/V2V3Project/ProjectDashboard/components/ProjectHeaderPopupMenu.tsx @@ -0,0 +1,120 @@ +import { WrenchScrewdriverIcon } from '@heroicons/react/24/outline' +import { Trans } from '@lingui/macro' +import { SocialLinkButton } from 'components/Project/ProjectHeader/SocialLinkButton' +import { useSocialLinks } from 'components/Project/ProjectHeader/hooks/useSocialLinks' +import { BookmarkButtonIcon } from 'components/buttons/BookmarkButton/BookmarkButtonIcon' +import { useBookmarkButton } from 'components/buttons/BookmarkButton/hooks/useBookmarkButton' +import { SubscribeButtonIcon } from 'components/buttons/SubscribeButton/SubscribeButtonIcon' +import { useSubscribeButton } from 'components/buttons/SubscribeButton/hooks/useSubscribeButton' +import { PopupMenu } from 'components/ui/PopupMenu' +import { PV_V2 } from 'constants/pv' +import useMobile from 'hooks/useMobile' +import { useMemo, useState } from 'react' +import { twJoin } from 'tailwind-merge' +import { V2V3ProjectToolsDrawer } from '../../V2V3ProjectToolsDrawer' + +type SocialLink = 'twitter' | 'discord' | 'telegram' | 'website' + +export function ProjectHeaderPopupMenu({ + className, + projectId, +}: { + className?: string + projectId: number +}) { + const socialLinks = useSocialLinks() + const isMobile = useMobile() + const [toolsIsOpen, setToolsIsOpen] = useState() + + const { isBookmarked, onBookmarkButtonClicked } = useBookmarkButton({ + projectId, + pv: PV_V2, + }) + const { isSubscribed, onSubscribeButtonClicked } = useSubscribeButton({ + projectId, + }) + + const socialItems = useMemo( + () => Object.entries(socialLinks).filter(([, href]) => !!href), + [socialLinks], + ) as [string, string][] + + return ( + <> + ({ + id: type, + label: ( + + ), + href, + })) + : []), + { + id: 'subscribe', + label: ( + <> + + + + Get notifications + + + ), + onClick: onSubscribeButtonClicked, + }, + { + id: 'bookmark', + label: ( + <> + + + Save project + + + ), + onClick(ev) { + ev.preventDefault() + ev.stopPropagation() + + onBookmarkButtonClicked() + }, + }, + { + id: 'tools', + label: ( + <> + + + + Tools + + + ), + onClick: ev => { + ev.preventDefault() + ev.stopPropagation() + + setToolsIsOpen(true) + }, + }, + ]} + /> + + setToolsIsOpen(false)} + /> + + ) +} diff --git a/src/packages/v2v3/components/V2V3Project/ProjectDashboard/components/V2V3ProjectHeader.tsx b/src/packages/v2v3/components/V2V3Project/ProjectDashboard/components/V2V3ProjectHeader.tsx index dca6c7b878..ddcff0e240 100644 --- a/src/packages/v2v3/components/V2V3Project/ProjectDashboard/components/V2V3ProjectHeader.tsx +++ b/src/packages/v2v3/components/V2V3Project/ProjectDashboard/components/V2V3ProjectHeader.tsx @@ -6,7 +6,6 @@ import { DomainBadge } from 'components/DomainBadge' import EthereumAddress from 'components/EthereumAddress' import { GnosisSafeBadge } from 'components/Project/ProjectHeader/GnosisSafeBadge' import { ProjectHeaderLogo } from 'components/Project/ProjectHeader/ProjectHeaderLogo' -import { ProjectHeaderPopupMenu } from 'components/Project/ProjectHeader/ProjectHeaderPopupMenu' import { SocialLinkButton } from 'components/Project/ProjectHeader/SocialLinkButton' import { Subtitle } from 'components/Project/ProjectHeader/Subtitle' import { useSocialLinks } from 'components/Project/ProjectHeader/hooks/useSocialLinks' @@ -21,6 +20,7 @@ import { V2V3OperatorPermission } from 'packages/v2v3/models/v2v3Permissions' import { settingsPagePath, v2v3ProjectRoute } from 'packages/v2v3/utils/routes' import { twMerge } from 'tailwind-merge' import { SocialLink } from '../hooks/useAboutPanel' +import { ProjectHeaderPopupMenu } from './ProjectHeaderPopupMenu' export const V2V3ProjectHeader = ({ className }: { className?: string }) => { const socialLinks = useSocialLinks() 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 ( - + () - const { isBookmarked, onBookmarkButtonClicked } = useBookmarkButton({ - projectId, - pv: PV_V2, - }) - const { isSubscribed, onSubscribeButtonClicked } = useSubscribeButton({ - projectId, - }) + // const { isBookmarked, onBookmarkButtonClicked } = useBookmarkButton({ + // projectId, + // pv: PV_V2, + // }) + // const { isSubscribed, onSubscribeButtonClicked } = useSubscribeButton({ + // projectId, + // }) const socialItems = useMemo( () => Object.entries(socialLinks).filter(([, href]) => !!href), @@ -109,7 +106,7 @@ export function ProjectHeaderPopupMenu({ ]} /> - setToolsIsOpen(false)} /> diff --git a/src/packages/v4/components/ProjectDashboard/components/V4ProjectToolsDrawer/AddToProjectBalanceForm.tsx b/src/packages/v4/components/ProjectDashboard/components/V4ProjectToolsDrawer/AddToProjectBalanceForm.tsx new file mode 100644 index 0000000000..71fd4a421e --- /dev/null +++ b/src/packages/v4/components/ProjectDashboard/components/V4ProjectToolsDrawer/AddToProjectBalanceForm.tsx @@ -0,0 +1,100 @@ +import { Trans } from '@lingui/macro' +import { waitForTransactionReceipt } from '@wagmi/core' +import { Form } from 'antd' +import InputAccessoryButton from 'components/buttons/InputAccessoryButton' +import TransactorButton from 'components/buttons/TransactorButton' +import FormattedNumberInput from 'components/inputs/FormattedNumberInput' +import { TxHistoryContext } from 'contexts/Transaction/TxHistoryContext' +import { useWallet } from 'hooks/Wallet' +import { NATIVE_TOKEN } from 'juice-sdk-core' +import { useJBContractContext, useWriteJbMultiTerminalAddToBalanceOf } from 'juice-sdk-react' +import { wagmiConfig } from 'packages/v4/wagmiConfig' +import { useContext, useState } from 'react' +import { parseWad } from 'utils/format/formatNumber' +import { emitErrorNotification } from 'utils/notifications' +import { reloadWindow } from 'utils/windowUtils' + +export function AddToProjectBalanceForm() { + const { contracts, projectId } = useJBContractContext() + const { addTransaction } = useContext(TxHistoryContext) + + const [loadingAddToBalance, setLoadingAddToBalance] = useState() + + const { userAddress } = useWallet() + + const [addToBalanceForm] = Form.useForm<{ amount: string }>() + + const { writeContractAsync: writeAddToBalance } = + useWriteJbMultiTerminalAddToBalanceOf() + + + async function addToBalance() { + const amount = parseWad(addToBalanceForm.getFieldValue('amount')).toBigInt() + if ( + !amount || + !contracts.primaryNativeTerminal.data || + !projectId + ) + return + + setLoadingAddToBalance(true) + + const args = [ + projectId, + NATIVE_TOKEN, + amount, + false, // shouldReturnHeldFees + '', // memo + '0x', // metadata + ] as const + + try { + const hash = await writeAddToBalance({ + address: contracts.primaryNativeTerminal.data, + args, + }) + + addTransaction?.('Send payouts', { hash }) + await waitForTransactionReceipt(wagmiConfig, { + hash, + }) + + reloadWindow() + + setLoadingAddToBalance(false) + } catch (e) { + setLoadingAddToBalance(false) + + emitErrorNotification((e as unknown as Error).message) + } + } + + return ( +
+

+ Transfer ETH to this project +

+

+ + Transfer ETH from your wallet to this project without minting tokens. + +

+ + Transfer amount}> + } + /> + + addToBalance()} + loading={loadingAddToBalance} + size="small" + type="primary" + text={Transfer ETH to project} + disabled={!userAddress} + connectWalletText={Connect wallet to transfer ETH} + /> + + ) +} diff --git a/src/packages/v4/components/ProjectDashboard/components/V4ProjectToolsDrawer/ExportSplitsButton.tsx b/src/packages/v4/components/ProjectDashboard/components/V4ProjectToolsDrawer/ExportSplitsButton.tsx new file mode 100644 index 0000000000..6f67f1fca1 --- /dev/null +++ b/src/packages/v4/components/ProjectDashboard/components/V4ProjectToolsDrawer/ExportSplitsButton.tsx @@ -0,0 +1,104 @@ +import { DownloadOutlined } from '@ant-design/icons' +import { t } from '@lingui/macro' +import { Button } from 'antd' +import { ETH_PAYOUT_SPLIT_GROUP } from 'constants/splits' +import { ProjectMetadataContext } from 'contexts/ProjectMetadataContext' +import { BigNumber } from 'ethers' +import { V2V3ProjectContext } from 'packages/v2v3/contexts/Project/V2V3ProjectContext' +import { GroupedSplits, Split, SplitGroup } from 'packages/v2v3/models/splits' +import { formatSplitPercent } from 'packages/v2v3/utils/math' +import { getProjectOwnerRemainderSplit } from 'packages/v2v3/utils/v2v3Splits' +import { PropsWithChildren, useContext, useState } from 'react' +import { downloadCsvFile } from 'utils/csv' +import { emitErrorNotification } from 'utils/notifications' + +const CSV_HEADER = [ + 'beneficiary', + 'percent', + 'preferClaimed', + 'lockedUntil', + 'projectId', + 'allocator', +] + +const splitToCsvRow = (split: Split) => { + return [ + split.beneficiary, + `${parseFloat(formatSplitPercent(BigNumber.from(split.percent))) / 100}`, + `${split.preferClaimed}`, + `${split.lockedUntil}`, + split.projectId, + split.allocator, + ] +} + +const prepareSplitsCsv = ( + splits: Split[], + projectOwnerAddress: string, +): (string | undefined)[][] => { + const csvContent = splits.map(splitToCsvRow) + + const rows = [CSV_HEADER, ...csvContent] + + const projectOwnerSplit = getProjectOwnerRemainderSplit( + projectOwnerAddress, + splits, + ) + if (projectOwnerSplit.percent > 0) { + rows.push(splitToCsvRow(projectOwnerSplit)) + } + + return rows +} + +export function ExportSplitsButton({ + children, + groupedSplits, +}: PropsWithChildren<{ groupedSplits: GroupedSplits }>) { + const { handle, fundingCycle, projectOwnerAddress } = + useContext(V2V3ProjectContext) + const { projectId } = useContext(ProjectMetadataContext) + const [loading, setLoading] = useState(false) + + const onExportSplitsButtonClick = () => { + if (!groupedSplits || !fundingCycle || !projectOwnerAddress) { + emitErrorNotification( + t`CSV data wasn't ready for export. Wait a few seconds and try again.`, + ) + return + } + + setLoading(true) + + try { + const csvContent = prepareSplitsCsv( + groupedSplits.splits, + projectOwnerAddress, + ) + const projectIdentifier = handle ? `@${handle}` : `project-${projectId}` + const splitType = + groupedSplits.group === ETH_PAYOUT_SPLIT_GROUP + ? 'payouts' + : 'reserved-tokens' + const filename = `${projectIdentifier}_${splitType}_fc-${fundingCycle.number}` + + downloadCsvFile(filename, csvContent) + } catch (e) { + console.error(e) + emitErrorNotification(t`CSV download failed.`) + } finally { + setLoading(false) + } + } + + return ( + + ) +} 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/components/modals/V4TokenHoldersModal/V4TokenHoldersModal.tsx b/src/packages/v4/components/modals/V4TokenHoldersModal/V4TokenHoldersModal.tsx index 4b0e57d700..c0bd31d635 100644 --- a/src/packages/v4/components/modals/V4TokenHoldersModal/V4TokenHoldersModal.tsx +++ b/src/packages/v4/components/modals/V4TokenHoldersModal/V4TokenHoldersModal.tsx @@ -1,8 +1,7 @@ import { t, Trans } from '@lingui/macro' import { Modal } from 'antd' import EthereumAddress from 'components/EthereumAddress' -import useNameOfERC20 from 'hooks/ERC20/useNameOfERC20' -import { useJBContractContext, useReadJbTokensTokenOf } from 'juice-sdk-react' +import { useJBContractContext, useJBTokenContext } from 'juice-sdk-react' import { OrderDirection, Participant_OrderBy, ParticipantsDocument } from 'packages/v4/graphql/client/graphql' import { useSubgraphQuery } from 'packages/v4/graphql/useSubgraphQuery' import { isZeroAddress } from 'utils/address' @@ -19,8 +18,10 @@ export const V4TokenHoldersModal = ({ onClose: VoidFunction }) => { const { projectId } = useJBContractContext() - const { data: tokenAddress } = useReadJbTokensTokenOf() - const { data: tokenSymbol } = useNameOfERC20(tokenAddress) + + const { token } = useJBTokenContext() + const tokenAddress = token?.data?.address + const tokenSymbol = token?.data?.symbol const { data: totalTokenSupply } = useV4TotalTokenSupply() 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/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 && ( diff --git a/src/packages/v4/views/V4ProjectDashboard/V4ProjectTabs/V4CyclesPayoutsPanel/V4DistributePayoutsModal.tsx b/src/packages/v4/views/V4ProjectDashboard/V4ProjectTabs/V4CyclesPayoutsPanel/V4DistributePayoutsModal.tsx index b559162cc0..ed88416781 100644 --- a/src/packages/v4/views/V4ProjectDashboard/V4ProjectTabs/V4CyclesPayoutsPanel/V4DistributePayoutsModal.tsx +++ b/src/packages/v4/views/V4ProjectDashboard/V4ProjectTabs/V4CyclesPayoutsPanel/V4DistributePayoutsModal.tsx @@ -6,7 +6,6 @@ import { Callout } from 'components/Callout/Callout' import FormattedNumberInput from 'components/inputs/FormattedNumberInput' import TransactionModal from 'components/modals/TransactionModal' import { FEES_EXPLANATION } from 'components/strings' -import { useProjectMetadataContext } from 'contexts/ProjectMetadataContext' import { TxHistoryContext } from 'contexts/Transaction/TxHistoryContext' import { NATIVE_TOKEN, NATIVE_TOKEN_DECIMALS } from 'juice-sdk-core' import { @@ -35,8 +34,7 @@ export default function V4DistributePayoutsModal({ const { data: payoutSplits } = useV4CurrentPayoutSplits() const { data: payoutLimit } = usePayoutLimit() const { distributableAmount: distributable } = useV4DistributableAmount() - const { projectId } = useProjectMetadataContext() - const { contracts } = useJBContractContext() + const { contracts, projectId } = useJBContractContext() const { addTransaction } = useContext(TxHistoryContext) const payoutLimitAmountCurrency = payoutLimit?.currency ?? V4_CURRENCY_ETH @@ -45,7 +43,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..4198629406 --- /dev/null +++ b/src/packages/v4/views/V4ProjectDashboard/V4ProjectTabs/V4TokensPanel/V4ClaimTokensModal.tsx @@ -0,0 +1,200 @@ +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 { useWallet } from 'hooks/Wallet' +import { Ether } from 'juice-sdk-core' +import { useJBContractContext, useJBTokenContext, useReadJbTokensCreditBalanceOf, 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 { token } = useJBTokenContext() + const tokenAddress = token?.data?.address + const tokenSymbol = token?.data?.symbol + + 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}} + > + {new Ether(unclaimedBalance ?? 0n).format()}{' '}{tokenTextShort} + + + {hasIssuedTokens && tokenSymbol && ( + {tokenSymbol} ERC-20 address} + > + + + )} + + +
+ + setClaimAmount(fromWad(unclaimedBalance))} + /> + } + onChange={val => setClaimAmount(val)} + /> + +
+
+
+ ) +} diff --git a/src/packages/v4/views/V4ProjectDashboard/V4ProjectTabs/V4TokensPanel/V4DistributeReservedTokensModal.tsx b/src/packages/v4/views/V4ProjectDashboard/V4ProjectTabs/V4TokensPanel/V4DistributeReservedTokensModal.tsx index 981da7cab9..fd8c6a0f2c 100644 --- a/src/packages/v4/views/V4ProjectDashboard/V4ProjectTabs/V4TokensPanel/V4DistributeReservedTokensModal.tsx +++ b/src/packages/v4/views/V4ProjectDashboard/V4ProjectTabs/V4TokensPanel/V4DistributeReservedTokensModal.tsx @@ -1,11 +1,14 @@ import { t, Trans } from '@lingui/macro' +import { waitForTransactionReceipt } from '@wagmi/core' import TransactionModal from 'components/modals/TransactionModal' -import useNameOfERC20 from 'hooks/ERC20/useNameOfERC20' -import { useJBContractContext, useReadJbTokensTokenOf } from 'juice-sdk-react' +import { TxHistoryContext } from 'contexts/Transaction/TxHistoryContext' +import { useJBContractContext, useJBTokenContext, 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,40 +21,60 @@ 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() - const { data: tokenSymbol } = useNameOfERC20(tokenAddress) + + const { token } = useJBTokenContext() + const tokenSymbol = token?.data?.symbol const [loading, setLoading] = useState() 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} diff --git a/src/packages/v4/views/V4ProjectDashboard/V4ProjectTabs/V4TokensPanel/V4MintModal.tsx b/src/packages/v4/views/V4ProjectDashboard/V4ProjectTabs/V4TokensPanel/V4MintModal.tsx new file mode 100644 index 0000000000..389b9f4b15 --- /dev/null +++ b/src/packages/v4/views/V4ProjectDashboard/V4ProjectTabs/V4TokensPanel/V4MintModal.tsx @@ -0,0 +1,161 @@ +import { t } from '@lingui/macro' +import { waitForTransactionReceipt } from '@wagmi/core' +import { Form, Input } from 'antd' +import { useForm } from 'antd/lib/form/Form' +import { EthAddressInput } from 'components/inputs/EthAddressInput' +import FormattedNumberInput from 'components/inputs/FormattedNumberInput' +import TransactionModal from 'components/modals/TransactionModal' +import { TxHistoryContext } from 'contexts/Transaction/TxHistoryContext' +import { utils } from 'ethers' +import useNameOfERC20 from 'hooks/ERC20/useNameOfERC20' +import { useJBContractContext, useReadJbTokensTokenOf, useWriteJbControllerMintTokensOf } from 'juice-sdk-react' +import { wagmiConfig } from 'packages/v4/wagmiConfig' +import { useContext, useState } from 'react' +import { parseWad } from 'utils/format/formatNumber' +import { emitErrorNotification } from 'utils/notifications' +import { tokenSymbolText } from 'utils/tokenSymbolText' +import { Address } from 'viem' + +type MintForm = { + beneficary: string + memo: string + amount: string +} + +export function V4MintModal({ + open, + onCancel, + onConfirmed, +}: { + open?: boolean + onCancel?: VoidFunction + onConfirmed?: VoidFunction +}) { + const { writeContractAsync: writeMintTokens } = + useWriteJbControllerMintTokensOf() + const [form] = useForm() + + const [loading, setLoading] = useState() + const [transactionPending, setTransactionPending] = useState() + + const { projectId, contracts } = useJBContractContext() + const { addTransaction } = useContext(TxHistoryContext) + + const { data: tokenAddress } = useReadJbTokensTokenOf() + const { data: tokenSymbol } = useNameOfERC20(tokenAddress) + + async function executeMintTx() { + const formValues = form.getFieldsValue(true) as MintForm + const amount = parseWad(formValues.amount ?? '0').toBigInt() + const memo = formValues.memo + const beneficiary = formValues.beneficary as Address + + if ( + !contracts.controller.data || + !beneficiary || + !amount || + !projectId + ) + return + + setLoading(true) + + const args = [ + projectId, + amount, + beneficiary, + memo, + false, //useReservedPercent + ] as const + + try { + const hash = await writeMintTokens({ + address: contracts.controller.data, + args, + }) + setTransactionPending(true) + + addTransaction?.('Mint tokens', { hash }) + await waitForTransactionReceipt(wagmiConfig, { + hash, + }) + + setLoading(false) + setTransactionPending(false) + onConfirmed?.() + } catch (e) { + setLoading(false) + + emitErrorNotification((e as unknown as Error).message) + } + } + + const tokensTokenLower = tokenSymbolText({ + tokenSymbol, + capitalize: false, + plural: true, + }) + + const tokensTokenUpper = tokenSymbolText({ + tokenSymbol, + capitalize: true, + plural: false, + }) + + return ( + +

Mint new tokens to a specified address.

+ +
+ { + if (!value || !utils.isAddress(value)) + return Promise.reject('Not a valid ETH address') + else return Promise.resolve() + }, + }, + ]} + > + + + { + if (!value || value === '0') { + return Promise.reject('Invalid value') + } + return Promise.resolve() + }, + }, + ]} + required + > + + + + + +
+
+ ) +} diff --git a/src/packages/v4/views/V4ProjectDashboard/V4ProjectTabs/V4TokensPanel/V4TokensPanel.tsx b/src/packages/v4/views/V4ProjectDashboard/V4ProjectTabs/V4TokensPanel/V4TokensPanel.tsx index a302700ae0..de2f7b7234 100644 --- a/src/packages/v4/views/V4ProjectDashboard/V4ProjectTabs/V4TokensPanel/V4TokensPanel.tsx +++ b/src/packages/v4/views/V4ProjectDashboard/V4ProjectTabs/V4TokensPanel/V4TokensPanel.tsx @@ -15,9 +15,13 @@ 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 { useV4BalanceMenuItemsUserFlags } from './hooks/useV4BalanceMenuItemsUserFlags' import { useV4TokensPanel } from './hooks/useV4TokensPanel' import { useV4YourBalanceMenuItems } from './hooks/useV4YourBalanceMenuItems' +import { V4ClaimTokensModal } from './V4ClaimTokensModal' +import { V4MintModal } from './V4MintModal' import { V4ReservedTokensSubPanel } from './V4ReservedTokensSubPanel' export const V4TokensPanel = () => { @@ -31,6 +35,8 @@ export const V4TokensPanel = () => { totalSupply, } = useV4TokensPanel() + const { canMintTokens } = useV4BalanceMenuItemsUserFlags() + const [tokenHolderModalOpen, setTokenHolderModalOpen] = useState(false) const openTokenHolderModal = useCallback( () => setTokenHolderModalOpen(true), @@ -45,10 +51,10 @@ export const V4TokensPanel = () => { items, // redeemModalVisible, // setRedeemModalVisible, - // claimTokensModalVisible, + claimTokensModalVisible, setClaimTokensModalVisible, - // mintModalVisible, - // setMintModalVisible, + mintModalVisible, + setMintModalVisible, // transferUnclaimedTokensModalVisible, // setTransferUnclaimedTokensModalVisible, } = useV4YourBalanceMenuItems() @@ -68,7 +74,7 @@ export const V4TokensPanel = () => { title={t`Your balance`} description={ - {userTokenBalance.format()} tokens + {userTokenBalance.format(8)} tokens
{/* {projectHasErc20Token && (