From 1fa2d8d984faa7d364094042b45781c30c95c960 Mon Sep 17 00:00:00 2001 From: Stanley Date: Thu, 12 Dec 2024 15:55:15 -0500 Subject: [PATCH 1/9] implemented split configuration modal for both claimable and mintable --- .../modules/components/Claimable.tsx | 424 ++++++++------- .../modules/components/Mintable.tsx | 82 ++- .../modules/components/mintable.stories.tsx | 11 + .../split-fees/ConfigureSplitFees.tsx | 498 ++++++++++++++++++ 4 files changed, 821 insertions(+), 194 deletions(-) create mode 100644 apps/dashboard/src/app/(dashboard)/(chain)/[chain_id]/[contractAddress]/split-fees/ConfigureSplitFees.tsx diff --git a/apps/dashboard/src/app/(dashboard)/(chain)/[chain_id]/[contractAddress]/modules/components/Claimable.tsx b/apps/dashboard/src/app/(dashboard)/(chain)/[chain_id]/[contractAddress]/modules/components/Claimable.tsx index 643569d6ad8..a2885493403 100644 --- a/apps/dashboard/src/app/(dashboard)/(chain)/[chain_id]/[contractAddress]/modules/components/Claimable.tsx +++ b/apps/dashboard/src/app/(dashboard)/(chain)/[chain_id]/[contractAddress]/modules/components/Claimable.tsx @@ -24,7 +24,7 @@ import { Separator } from "@/components/ui/separator"; import { Skeleton } from "@/components/ui/skeleton"; import { ToolTipLabel } from "@/components/ui/tooltip"; import { zodResolver } from "@hookform/resolvers/zod"; -import { useMutation } from "@tanstack/react-query"; +import { useMutation, useQuery } from "@tanstack/react-query"; import { TransactionButton } from "components/buttons/TransactionButton"; import { addDays, fromUnixTime } from "date-fns"; import { useAllChainsData } from "hooks/chains/allChains"; @@ -40,11 +40,13 @@ import { useFieldArray, useForm } from "react-hook-form"; import { NATIVE_TOKEN_ADDRESS, type PreparedTransaction, + type ThirdwebClient, ZERO_ADDRESS, getContract, sendAndConfirmTransaction, toTokens, } from "thirdweb"; +import { getBytecode } from "thirdweb/contract"; import { decimals } from "thirdweb/extensions/erc20"; import { ClaimableERC20, @@ -52,7 +54,9 @@ import { ClaimableERC1155, } from "thirdweb/modules"; import { useActiveAccount, useReadContract } from "thirdweb/react"; +import { isContractDeployed } from "thirdweb/utils"; import { z } from "zod"; +import ConfigureSplit from "../../split-fees/ConfigureSplitFees"; import { addressSchema } from "../zod-schemas"; import { CurrencySelector } from "./CurrencySelector"; import { ModuleCardUI, type ModuleCardUIProps } from "./module-card"; @@ -71,6 +75,9 @@ export type ClaimConditionValue = { const positiveIntegerRegex = /^[0-9]\d*$/; +const splitWalletBytecode = + "0x3d3d3d3d363d3d37363d7341dc1be6e4c7698f46268251b88b1f789aa9df265af43d3d93803e602a57fd5bf3"; + function ClaimableModule(props: ModuleInstanceProps) { const { contract, ownerAccount } = props; const account = useActiveAccount(); @@ -108,6 +115,28 @@ function ClaimableModule(props: ModuleInstanceProps) { }, ); + const splitRecipientContract = getContract({ + address: primarySaleRecipientQuery.data || "", + chain: contract.chain, + client: contract.client, + }); + + const isSplitRecipientQuery = useQuery({ + queryKey: ["isSplitRecipient", primarySaleRecipientQuery.data], + queryFn: async () => { + if (!primarySaleRecipientQuery.data) return false; + + const contractDeployed = await isContractDeployed(splitRecipientContract); + if (!contractDeployed) return false; + + const bytecode = await getBytecode(splitRecipientContract); + if (bytecode !== splitWalletBytecode) return false; + + return true; + }, + enabled: !!primarySaleRecipientQuery.data, + }); + const noClaimConditionSet = claimConditionQuery.data?.availableSupply === 0n && claimConditionQuery.data?.allowlistMerkleRoot === @@ -254,9 +283,13 @@ function ClaimableModule(props: ModuleInstanceProps) { >; isValidTokenId: boolean; noClaimConditionSet: boolean; + client: ThirdwebClient; primarySaleRecipientSection: { setPrimarySaleRecipient: ( values: PrimarySaleRecipientFormValues, @@ -305,6 +340,7 @@ export function ClaimableModuleUI( data: | { primarySaleRecipient: string; + isSplitRecipient: boolean; } | undefined; }; @@ -410,6 +446,10 @@ export function ClaimableModuleUI( props.primarySaleRecipientSection.setPrimarySaleRecipient } contractChainId={props.contractChainId} + isSplitRecipient={ + props.primarySaleRecipientSection.data?.isSplitRecipient + } + client={props.client} /> ) : ( @@ -470,8 +510,6 @@ function ClaimConditionSection(props: { const { idToChain } = useAllChainsData(); const chain = idToChain.get(props.chainId); const { tokenId, claimCondition } = props; - const [addClaimConditionButtonClicked, setAddClaimConditionButtonClicked] = - useState(false); const form = useForm({ resolver: zodResolver(claimConditionFormSchema), @@ -544,193 +582,181 @@ function ClaimConditionSection(props: { return (
- {props.noClaimConditionSet && !addClaimConditionButtonClicked && ( - <> - - - No Claim Condition Set - - You have not set a claim condition for this token. You can set a - claim condition by clicking the "Set Claim Condition" button. - - - - - + {props.noClaimConditionSet && ( + + + No Claim Condition Set + + You have not set a claim condition for this token. You can set a + claim condition by clicking the "Set Claim Condition" button. + + )} - {(!props.noClaimConditionSet || addClaimConditionButtonClicked) && ( -
- -
-
- ( - - Price Per Token - - - - - - )} - /> + + +
+
+ ( + + Price Per Token + + + + + + )} + /> - ( - - Currency - - - )} - /> -
+ ( + + Currency + + + )} + /> +
-
- ( - - Max Available Supply - - - - - - )} - /> +
+ ( + + Max Available Supply + + + + + + )} + /> - ( - - Maximum number of mints per wallet - - - - - - )} + ( + + Maximum number of mints per wallet + + + + + + )} + /> +
+ + +
+ form.setValue("startTime", from)} + setTo={(to: Date) => form.setValue("endTime", to)} />
- - -
- form.setValue("startTime", from)} - setTo={(to: Date) => form.setValue("endTime", to)} - /> -
-
- - - -
- Allowlist -
- {allowListFields.fields.map((fieldItem, index) => ( -
- ( - - - - - - - )} - /> - - - -
- ))} - - {allowListFields.fields.length === 0 && ( - - - - No allowlist configured - - - )} -
- -
- -
- -
+ + + + +
+ Allowlist +
+ {allowListFields.fields.map((fieldItem, index) => ( +
+ ( + + + + + + + )} + /> + + + +
+ ))} + + {allowListFields.fields.length === 0 && ( + + + + No allowlist configured + + + )}
-
- + +
+
- {" "} - - )} + +
+ + Update + +
+
+ {" "} +
); } @@ -747,7 +773,9 @@ function PrimarySaleRecipientSection(props: { primarySaleRecipient: string | undefined; update: (values: PrimarySaleRecipientFormValues) => Promise; isOwnerAccount: boolean; + isSplitRecipient?: boolean; contractChainId: number; + client: ThirdwebClient; }) { const form = useForm({ resolver: zodResolver(primarySaleRecipientFormSchema), @@ -771,6 +799,11 @@ function PrimarySaleRecipientSection(props: { updateMutation.mutateAsync(form.getValues()); }; + const postSplitConfigure = async (splitWallet: string) => { + form.setValue("primarySaleRecipient", splitWallet); + await onSubmit(); + }; + return (
@@ -781,11 +814,32 @@ function PrimarySaleRecipientSection(props: { Sale Recipient - +
+ + {props.isOwnerAccount && ( + + + + )} +
diff --git a/apps/dashboard/src/app/(dashboard)/(chain)/[chain_id]/[contractAddress]/modules/components/Mintable.tsx b/apps/dashboard/src/app/(dashboard)/(chain)/[chain_id]/[contractAddress]/modules/components/Mintable.tsx index 32a639e2e1b..8b2418acfdc 100644 --- a/apps/dashboard/src/app/(dashboard)/(chain)/[chain_id]/[contractAddress]/modules/components/Mintable.tsx +++ b/apps/dashboard/src/app/(dashboard)/(chain)/[chain_id]/[contractAddress]/modules/components/Mintable.tsx @@ -6,6 +6,7 @@ import { AccordionTrigger, } from "@/components/ui/accordion"; import { Alert, AlertTitle } from "@/components/ui/alert"; +import { Button } from "@/components/ui/button"; import { Checkbox, CheckboxWithLabel } from "@/components/ui/checkbox"; import { Form, @@ -21,13 +22,19 @@ import { Separator } from "@/components/ui/separator"; import { Skeleton } from "@/components/ui/skeleton"; import { Textarea } from "@/components/ui/textarea"; import { zodResolver } from "@hookform/resolvers/zod"; -import { useMutation } from "@tanstack/react-query"; +import { useMutation, useQuery } from "@tanstack/react-query"; import { TransactionButton } from "components/buttons/TransactionButton"; import { useTxNotifications } from "hooks/useTxNotifications"; import { CircleAlertIcon } from "lucide-react"; import { useCallback } from "react"; import { useForm } from "react-hook-form"; -import { type PreparedTransaction, sendAndConfirmTransaction } from "thirdweb"; +import { + type PreparedTransaction, + type ThirdwebClient, + getContract, + sendAndConfirmTransaction, +} from "thirdweb"; +import { getBytecode } from "thirdweb/contract"; import { MintableERC20, MintableERC721, @@ -35,9 +42,11 @@ import { } from "thirdweb/modules"; import { grantRoles, hasAllRoles } from "thirdweb/modules"; import { useReadContract } from "thirdweb/react"; +import { isContractDeployed } from "thirdweb/utils"; import type { NFTMetadataInputLimited } from "types/modified-types"; import { parseAttributes } from "utils/parseAttributes"; import { z } from "zod"; +import ConfigureSplit from "../../split-fees/ConfigureSplitFees"; import { addressSchema } from "../zod-schemas"; import { ModuleCardUI, type ModuleCardUIProps } from "./module-card"; import type { ModuleInstanceProps } from "./module-instance"; @@ -69,6 +78,9 @@ const isValidNft = (values: MintFormValues) => const MINTER_ROLE = 1n; +const splitWalletBytecode = + "0x6080604052600436106100b15760003560e01c8063f04e283e11610069578063f61510891161004e578063f615108914610163578063f7448a3114610197578063fee81cf4146101b757600080fd5b8063f04e283e1461013d578063f2fde38b1461015057600080fd5b806354d1f13d1161009a57806354d1f13d146100d3578063715018a6146100db5780638da5cb5b146100e357600080fd5b806325692962146100b65780634329db46146100c0575b600080fd5b6100be6101f8565b005b6100be6100ce366004610629565b610248565b6100be6103a9565b6100be6103e5565b3480156100ef57600080fd5b507fffffffffffffffffffffffffffffffffffffffffffffffffffffffff74873927545b60405173ffffffffffffffffffffffffffffffffffffffff90911681526020015b60405180910390f35b6100be61014b36600461066b565b6103f9565b6100be61015e36600461066b565b610439565b34801561016f57600080fd5b506101137f000000000000000000000000b0293be0b3d5d5946cfa074b45d507319659c95f81565b3480156101a357600080fd5b506100be6101b236600461068d565b610460565b3480156101c357600080fd5b506101ea6101d236600461066b565b63389a75e1600c908152600091909152602090205490565b604051908152602001610134565b60006202a30067ffffffffffffffff164201905063389a75e1600c5233600052806020600c2055337fdbf36a107da19e49527a7176a1babf963b4b0ff8cde35ee35d6cd8f1f9ac7e1d600080a250565b3373ffffffffffffffffffffffffffffffffffffffff7f000000000000000000000000b0293be0b3d5d5946cfa074b45d507319659c95f16146102b7576040517f6fd1f78c00000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b60007f000000000000000000000000b0293be0b3d5d5946cfa074b45d507319659c95f73ffffffffffffffffffffffffffffffffffffffff168260405160006040518083038185875af1925050503d8060008114610331576040519150601f19603f3d011682016040523d82523d6000602084013e610336565b606091505b50509050806103a5576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152601460248201527f4661696c656420746f2073656e64204574686572000000000000000000000000604482015260640160405180910390fd5b5050565b63389a75e1600c523360005260006020600c2055337ffa7b8eab7da67f412cc9575ed43464468f9bfbae89d1675917346ca6d8fe3c92600080a2565b6103ed61058d565b6103f760006105c3565b565b61040161058d565b63389a75e1600c52806000526020600c20805442111561042957636f5e88186000526004601cfd5b60009055610436816105c3565b50565b61044161058d565b8060601b61045757637448fbae6000526004601cfd5b610436816105c3565b3373ffffffffffffffffffffffffffffffffffffffff7f000000000000000000000000b0293be0b3d5d5946cfa074b45d507319659c95f16146104cf576040517f6fd1f78c00000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b6040517fa9059cbb00000000000000000000000000000000000000000000000000000000815273ffffffffffffffffffffffffffffffffffffffff7f000000000000000000000000b0293be0b3d5d5946cfa074b45d507319659c95f811660048301526024820183905283169063a9059cbb906044016020604051808303816000875af1158015610564573d6000803e3d6000fd5b505050506040513d601f19601f8201168201806040525081019061058891906106b7565b505050565b7fffffffffffffffffffffffffffffffffffffffffffffffffffffffff748739275433146103f7576382b429006000526004601cfd5b7fffffffffffffffffffffffffffffffffffffffffffffffffffffffff74873927805473ffffffffffffffffffffffffffffffffffffffff9092169182907f8be0079c531659141344cd1fd0a4f28419497f9722a3daafe3b4186f6b6457e0600080a355565b60006020828403121561063b57600080fd5b5035919050565b803573ffffffffffffffffffffffffffffffffffffffff8116811461066657600080fd5b919050565b60006020828403121561067d57600080fd5b61068682610642565b9392505050565b600080604083850312156106a057600080fd5b6106a983610642565b946020939093013593505050565b6000602082840312156106c957600080fd5b8151801515811461068657600080fdfea264697066735822122007cf0f59b98eb9ce1e88de58df5114d9b37a2fb3de097a3520bda4a6ac89592664736f6c634300081a0033"; + function MintableModule(props: ModuleInstanceProps) { const { contract, ownerAccount } = props; @@ -86,6 +98,28 @@ function MintableModule(props: ModuleInstanceProps) { roles: MINTER_ROLE, }); + const splitRecipientContract = getContract({ + address: primarySaleRecipientQuery.data || "", + chain: contract.chain, + client: contract.client, + }); + + const isSplitRecipientQuery = useQuery({ + queryKey: ["isSplitRecipient", primarySaleRecipientQuery.data], + queryFn: async () => { + if (!primarySaleRecipientQuery.data) return false; + + const contractDeployed = await isContractDeployed(splitRecipientContract); + if (!contractDeployed) return false; + + const bytecode = await getBytecode(splitRecipientContract); + if (bytecode !== splitWalletBytecode) return false; + + return true; + }, + enabled: !!primarySaleRecipientQuery.data, + }); + const isBatchMetadataInstalled = !!props.allModuleContractInfo.find( (module) => module.name.includes("BatchMetadata"), ); @@ -172,21 +206,26 @@ function MintableModule(props: ModuleInstanceProps) { return ( ); } export function MintableModuleUI( props: Omit & { - primarySaleRecipient: string | undefined; + primarySaleRecipient?: string | undefined; + isSplitRecipient?: boolean; isPending: boolean; isOwnerAccount: boolean; updatePrimaryRecipient: (values: UpdateFormValues) => Promise; @@ -194,6 +233,7 @@ export function MintableModuleUI( name: string; isBatchMetadataInstalled: boolean; contractChainId: number; + client: ThirdwebClient; }, ) { return ( @@ -240,8 +280,10 @@ export function MintableModuleUI( @@ -258,9 +300,11 @@ const primarySaleRecipientFormSchema = z.object({ function PrimarySalesSection(props: { primarySaleRecipient: string | undefined; + isSplitRecipient?: boolean; update: (values: UpdateFormValues) => Promise; isOwnerAccount: boolean; contractChainId: number; + client: ThirdwebClient; }) { const form = useForm({ resolver: zodResolver(primarySaleRecipientFormSchema), @@ -284,6 +328,11 @@ function PrimarySalesSection(props: { updateMutation.mutateAsync(form.getValues()); }; + const postSplitConfigure = async (splitWallet: string) => { + form.setValue("primarySaleRecipient", splitWallet); + await onSubmit(); + }; + return ( @@ -297,11 +346,26 @@ function PrimarySalesSection(props: { sales of the assets. - +
+ + + + +
diff --git a/apps/dashboard/src/app/(dashboard)/(chain)/[chain_id]/[contractAddress]/modules/components/mintable.stories.tsx b/apps/dashboard/src/app/(dashboard)/(chain)/[chain_id]/[contractAddress]/modules/components/mintable.stories.tsx index 35ae421e33c..4e638ef8cd3 100644 --- a/apps/dashboard/src/app/(dashboard)/(chain)/[chain_id]/[contractAddress]/modules/components/mintable.stories.tsx +++ b/apps/dashboard/src/app/(dashboard)/(chain)/[chain_id]/[contractAddress]/modules/components/mintable.stories.tsx @@ -52,6 +52,7 @@ function Component() { const [name, setName] = useState("MintableERC721"); const [isBatchMetadataInstalled, setIsBatchMetadataInstalled] = useState(false); + const [isSplitRecipient, setIsSplitRecipient] = useState(false); async function updatePrimaryRecipientStub(values: UpdateFormValues) { console.log("submitting", values); await new Promise((resolve) => setTimeout(resolve, 1000)); @@ -101,6 +102,13 @@ function Component() { label="isBatchMetadataInstalled" /> + + + + + + )} + /> + + ( + + + + + + + )} + /> +
+ + + + +
+ ))} + + {form.formState.errors.totalAllocation && ( +

+ {form.formState.errors.totalAllocation.message} +

+ )} + +
+ +
+ + + + + Advanced + + + ( + + Controller + + + + + + )} + /> + + + + + + + {props.isNewSplit ? "Create Split" : "Update Split"} + + + + + ); +} + +export default ConfigureSplit; From b2c6587fa90beae11e6562a1cb046a40be4c3ca7 Mon Sep 17 00:00:00 2001 From: Stanley Date: Thu, 12 Dec 2024 20:23:54 -0500 Subject: [PATCH 2/9] fixed typecheck and lint issues --- .../modules/components/Claimable.tsx | 6 ------ .../[contractAddress]/modules/components/Mintable.tsx | 6 ------ .../modules/components/claimable.stories.tsx | 1 + .../split-fees/ConfigureSplitFees.tsx | 11 +---------- 4 files changed, 2 insertions(+), 22 deletions(-) diff --git a/apps/dashboard/src/app/(dashboard)/(chain)/[chain_id]/[contractAddress]/modules/components/Claimable.tsx b/apps/dashboard/src/app/(dashboard)/(chain)/[chain_id]/[contractAddress]/modules/components/Claimable.tsx index a2885493403..511181f8a22 100644 --- a/apps/dashboard/src/app/(dashboard)/(chain)/[chain_id]/[contractAddress]/modules/components/Claimable.tsx +++ b/apps/dashboard/src/app/(dashboard)/(chain)/[chain_id]/[contractAddress]/modules/components/Claimable.tsx @@ -40,7 +40,6 @@ import { useFieldArray, useForm } from "react-hook-form"; import { NATIVE_TOKEN_ADDRESS, type PreparedTransaction, - type ThirdwebClient, ZERO_ADDRESS, getContract, sendAndConfirmTransaction, @@ -316,7 +315,6 @@ function ClaimableModule(props: ModuleInstanceProps) { setTokenId={setTokenId} isValidTokenId={isValidTokenId} noClaimConditionSet={noClaimConditionSet} - client={contract.client} mintSection={{ mint, }} @@ -332,7 +330,6 @@ export function ClaimableModuleUI( setTokenId: Dispatch>; isValidTokenId: boolean; noClaimConditionSet: boolean; - client: ThirdwebClient; primarySaleRecipientSection: { setPrimarySaleRecipient: ( values: PrimarySaleRecipientFormValues, @@ -449,7 +446,6 @@ export function ClaimableModuleUI( isSplitRecipient={ props.primarySaleRecipientSection.data?.isSplitRecipient } - client={props.client} /> ) : ( @@ -775,7 +771,6 @@ function PrimarySaleRecipientSection(props: { isOwnerAccount: boolean; isSplitRecipient?: boolean; contractChainId: number; - client: ThirdwebClient; }) { const form = useForm({ resolver: zodResolver(primarySaleRecipientFormSchema), @@ -826,7 +821,6 @@ function PrimarySaleRecipientSection(props: { {props.isOwnerAccount && ( ); } @@ -233,7 +231,6 @@ export function MintableModuleUI( name: string; isBatchMetadataInstalled: boolean; contractChainId: number; - client: ThirdwebClient; }, ) { return ( @@ -283,7 +280,6 @@ export function MintableModuleUI( isSplitRecipient={props.isSplitRecipient} update={props.updatePrimaryRecipient} contractChainId={props.contractChainId} - client={props.client} /> @@ -304,7 +300,6 @@ function PrimarySalesSection(props: { update: (values: UpdateFormValues) => Promise; isOwnerAccount: boolean; contractChainId: number; - client: ThirdwebClient; }) { const form = useForm({ resolver: zodResolver(primarySaleRecipientFormSchema), @@ -355,7 +350,6 @@ function PrimarySalesSection(props: { /> void; @@ -77,7 +68,7 @@ function ConfigureSplit(props: { const splitFeesCore = getContract({ address: splitFeesCoreAddress, client, - chain, + chain: chain!, }); const split = useReadContract({ From c06c4eea66d62c173ae3a1166e04a8c8ecbec362 Mon Sep 17 00:00:00 2001 From: Stanley Date: Fri, 13 Dec 2024 13:20:58 -0500 Subject: [PATCH 3/9] added in splits fees tab --- .../_utils/getContractPageSidebarLinks.ts | 6 + .../modules/components/Claimable.tsx | 7 + .../split-fees/ConfigureSplitFees.tsx | 9 +- .../split-fees/SplitFeesCard.tsx | 177 ++++++++++++++++++ .../[contractAddress]/split-fees/page.tsx | 88 +++++++++ 5 files changed, 283 insertions(+), 4 deletions(-) create mode 100644 apps/dashboard/src/app/(dashboard)/(chain)/[chain_id]/[contractAddress]/split-fees/SplitFeesCard.tsx create mode 100644 apps/dashboard/src/app/(dashboard)/(chain)/[chain_id]/[contractAddress]/split-fees/page.tsx diff --git a/apps/dashboard/src/app/(dashboard)/(chain)/[chain_id]/[contractAddress]/_utils/getContractPageSidebarLinks.ts b/apps/dashboard/src/app/(dashboard)/(chain)/[chain_id]/[contractAddress]/_utils/getContractPageSidebarLinks.ts index 474b31d404b..b147255e8f0 100644 --- a/apps/dashboard/src/app/(dashboard)/(chain)/[chain_id]/[contractAddress]/_utils/getContractPageSidebarLinks.ts +++ b/apps/dashboard/src/app/(dashboard)/(chain)/[chain_id]/[contractAddress]/_utils/getContractPageSidebarLinks.ts @@ -24,6 +24,12 @@ export function getContractPageSidebarLinks(data: { hide: !data.metadata.isModularCore, exactMatch: true, }, + { + label: "Split Fees", + href: `${layoutPrefix}/split-fees`, + hide: !data.metadata.isModularCore, + exactMatch: true, + }, { label: "Code Snippets", href: `${layoutPrefix}/code`, diff --git a/apps/dashboard/src/app/(dashboard)/(chain)/[chain_id]/[contractAddress]/modules/components/Claimable.tsx b/apps/dashboard/src/app/(dashboard)/(chain)/[chain_id]/[contractAddress]/modules/components/Claimable.tsx index 511181f8a22..618d8db7723 100644 --- a/apps/dashboard/src/app/(dashboard)/(chain)/[chain_id]/[contractAddress]/modules/components/Claimable.tsx +++ b/apps/dashboard/src/app/(dashboard)/(chain)/[chain_id]/[contractAddress]/modules/components/Claimable.tsx @@ -287,6 +287,7 @@ function ClaimableModule(props: ModuleInstanceProps) { ? { primarySaleRecipient: primarySaleRecipientQuery.data, isSplitRecipient: isSplitRecipientQuery.data || false, + referenceContract: props.contract.address, } : undefined, setPrimarySaleRecipient, @@ -338,6 +339,7 @@ export function ClaimableModuleUI( | { primarySaleRecipient: string; isSplitRecipient: boolean; + referenceContract: string; } | undefined; }; @@ -446,6 +448,9 @@ export function ClaimableModuleUI( isSplitRecipient={ props.primarySaleRecipientSection.data?.isSplitRecipient } + referenceContract={ + props.primarySaleRecipientSection.data?.referenceContract + } /> ) : ( @@ -771,6 +776,7 @@ function PrimarySaleRecipientSection(props: { isOwnerAccount: boolean; isSplitRecipient?: boolean; contractChainId: number; + referenceContract: string; }) { const form = useForm({ resolver: zodResolver(primarySaleRecipientFormSchema), @@ -822,6 +828,7 @@ function PrimarySaleRecipientSection(props: { void; }) { const activeAccount = useActiveAccount(); @@ -139,8 +140,8 @@ function ConfigureSplit(props: { const transaction = prepareContractCall({ contract: splitFeesCore, method: - "function createSplit(address[] memory _recipients, uint256[] memory _allocations, address _controller)", - params: [recipients, allocations, controller], + "function createSplit(address[] memory _recipients, uint256[] memory _allocations, address _controller, address _referenceContract)", + params: [recipients, allocations, controller, props.referenceContract], }); const receipt = await sendAndConfirmTransaction({ @@ -152,7 +153,7 @@ function ConfigureSplit(props: { events: [ prepareEvent({ signature: - "event SplitCreated(address indexed splitWallet, address[] recipients, uint256[] allocations, address controller)", + "event SplitCreated(address indexed splitWallet, address[] recipients, uint256[] allocations, address controller, address referenceContract)", }), ], logs: receipt.logs, @@ -244,7 +245,7 @@ function ConfigureSplit(props: { } // TODO: place this somwhere appropriate -const splitFeesCoreAddress = "0xB0293Be0b3d5D5946cfA074B45d507319659C95F"; +const splitFeesCoreAddress = "0x7d6Ba9e63eFb30c42b25db50dBD3C2F0a4578Ba2"; const formSchema = z .object({ diff --git a/apps/dashboard/src/app/(dashboard)/(chain)/[chain_id]/[contractAddress]/split-fees/SplitFeesCard.tsx b/apps/dashboard/src/app/(dashboard)/(chain)/[chain_id]/[contractAddress]/split-fees/SplitFeesCard.tsx new file mode 100644 index 00000000000..47d95dfaaac --- /dev/null +++ b/apps/dashboard/src/app/(dashboard)/(chain)/[chain_id]/[contractAddress]/split-fees/SplitFeesCard.tsx @@ -0,0 +1,177 @@ +"use client"; + +import { WalletAddress } from "@/components/blocks/wallet-address"; +import { CopyAddressButton } from "@/components/ui/CopyAddressButton"; +import { Button } from "@/components/ui/button"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogHeader, + DialogTitle, + DialogTrigger, +} from "@/components/ui/dialog"; +import { + Table, + TableBody, + TableCell, + TableContainer, + TableHead, + TableHeader, + TableRow, +} from "@/components/ui/table"; +import { + type ColumnDef, + flexRender, + getCoreRowModel, + useReactTable, +} from "@tanstack/react-table"; +import { InfoIcon } from "lucide-react"; + +export function SplitFeesCard(props: { + splitWallet: string; + recipients: string[]; + allocations: bigint[]; + controller: string; + referenceContract: string; +}) { + const isController = false; + + const columns: ColumnDef<{ allocation: number; recipient: string }>[] = [ + { + accessorKey: "recipient", + header: "Recipient", + }, + { + accessorKey: "allocation", + header: "Percentage", + }, + ]; + + console.log("allocations: ", props.allocations); + console.log("recipients: ", props.recipients); + + const totalAllocation = props.allocations.reduce( + (acc, curr) => acc + curr, + 0n, + ); + const data = props.recipients.map((recipient, i) => ({ + recipient: recipient, + allocation: + (Number(props.allocations[i] || 0n) / Number(totalAllocation)) * 100, + })); + + const table = useReactTable({ + data, + columns, + getCoreRowModel: getCoreRowModel(), + }); + + return ( +
+ {/* Header */} +
+ {/* Title */} +
+

+ Split Fees + {/* Info Dialog */} + + + + + + + Split Fees Contract + + This contract holds the funds that are split between the + recipients. + + + {/* Avoid adding focus on other elements to prevent tooltips from opening on modal open */} + + +
+ +
+
+

+ Split Fees +

+ +
+ +
+

+ Controller +

+ +
+
+ + +
+

+
+ +
+ + + + + {table.getHeaderGroups().map((headerGroup) => ( + + {headerGroup.headers.map((header) => { + return ( + + {header.isPlaceholder + ? null + : flexRender( + header.column.columnDef.header, + header.getContext(), + )} + + ); + })} + + ))} + + + {table.getRowModel().rows.map((row) => ( + + {row.getVisibleCells().map((cell) => ( + + {flexRender( + cell.column.columnDef.cell, + cell.getContext(), + )} + + ))} + + ))} + +
+
+
+ +
+ +
+
+ ); +} diff --git a/apps/dashboard/src/app/(dashboard)/(chain)/[chain_id]/[contractAddress]/split-fees/page.tsx b/apps/dashboard/src/app/(dashboard)/(chain)/[chain_id]/[contractAddress]/split-fees/page.tsx new file mode 100644 index 00000000000..1a40f73ec9c --- /dev/null +++ b/apps/dashboard/src/app/(dashboard)/(chain)/[chain_id]/[contractAddress]/split-fees/page.tsx @@ -0,0 +1,88 @@ +import { Button } from "@/components/ui/button"; +import { notFound, redirect } from "next/navigation"; +import { getContractEvents, prepareEvent } from "thirdweb"; +import { type FetchDeployMetadataResult, getContract } from "thirdweb/contract"; +import { getContractPageParamsInfo } from "../_utils/getContractFromParams"; +import { getContractPageMetadata } from "../_utils/getContractPageMetadata"; +import { SplitFeesCard } from "./SplitFeesCard"; + +export function getModuleInstallParams(mod: FetchDeployMetadataResult) { + return ( + mod.abi + .filter((a) => a.type === "function") + .find((f) => f.name === "encodeBytesOnInstall")?.inputs || [] + ); +} + +// TODO: place this somwhere appropriate +const splitFeesCoreAddress = "0x7d6Ba9e63eFb30c42b25db50dBD3C2F0a4578Ba2"; + +export default async function Page(props: { + params: Promise<{ + contractAddress: string; + chain_id: string; + }>; +}) { + const params = await props.params; + const info = await getContractPageParamsInfo(params); + + if (!info) { + notFound(); + } + + const { contract } = info; + + const { isModularCore } = await getContractPageMetadata(contract); + + if (!isModularCore) { + redirect(`/${params.chain_id}/${params.contractAddress}`); + } + const splitFeesCore = getContract({ + address: splitFeesCoreAddress, + client: contract.client, + chain: contract.chain, + }); + + const events = await getContractEvents({ + contract: splitFeesCore, + events: [ + prepareEvent({ + signature: + "event SplitCreated(address indexed splitWallet, address[] recipients, uint256[] allocations, address controller, address referenceContract)", + filters: { + referenceContract: contract.address, + }, + }), + ], + blockRange: 123456n, + }); + + const splits = events.map((e) => { + const args = e.args as { + splitWallet: string; + recipients: string[]; + allocations: bigint[]; + controller: string; + referenceContract: string; + }; + return { + splitWallet: args.splitWallet, + recipients: args.recipients, + allocations: args.allocations, + controller: args.controller, + referenceContract: args.referenceContract, + }; + }); + console.log("splits: ", splits); + + return ( +
+ {splits.map((split) => ( + + ))} + +
+ ); +} From ff14b5933df911f22cba855da14df915f7b271f7 Mon Sep 17 00:00:00 2001 From: Stanley Date: Fri, 13 Dec 2024 14:02:11 -0500 Subject: [PATCH 4/9] WIP: commit for now --- .../split-fees/ClaimFeesCard.tsx | 216 ++++++++++++++++++ 1 file changed, 216 insertions(+) create mode 100644 apps/dashboard/src/app/(dashboard)/(chain)/[chain_id]/[contractAddress]/split-fees/ClaimFeesCard.tsx diff --git a/apps/dashboard/src/app/(dashboard)/(chain)/[chain_id]/[contractAddress]/split-fees/ClaimFeesCard.tsx b/apps/dashboard/src/app/(dashboard)/(chain)/[chain_id]/[contractAddress]/split-fees/ClaimFeesCard.tsx new file mode 100644 index 00000000000..04410cfa126 --- /dev/null +++ b/apps/dashboard/src/app/(dashboard)/(chain)/[chain_id]/[contractAddress]/split-fees/ClaimFeesCard.tsx @@ -0,0 +1,216 @@ +"use client"; + +import { WalletAddress } from "@/components/blocks/wallet-address"; +import { CopyAddressButton } from "@/components/ui/CopyAddressButton"; +import { Button } from "@/components/ui/button"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogHeader, + DialogTitle, + DialogTrigger, +} from "@/components/ui/dialog"; +import { FormField, FormItem, FormLabel } from "@/components/ui/form"; +import { + Table, + TableBody, + TableCell, + TableContainer, + TableHead, + TableHeader, + TableRow, +} from "@/components/ui/table"; +import { + type ColumnDef, + flexRender, + getCoreRowModel, + useReactTable, +} from "@tanstack/react-table"; +import { InfoIcon } from "lucide-react"; +import { useForm } from "react-hook-form"; +import { NATIVE_TOKEN_ADDRESS } from "thirdweb"; +import { useActiveWalletChain } from "thirdweb/react"; +import { CurrencySelector } from "../modules/components/CurrencySelector"; + +export function ClaimFeesCard(props: { + splitWallet: string; + recipients: string[]; + allocations: bigint[]; + controller: string; + referenceContract: string; +}) { + const isController = false; + const chain = useActiveWalletChain(); + const form = useForm<{ currencyAddress: string }>({ + values: { + currencyAddress: NATIVE_TOKEN_ADDRESS, + }, + }); + + const _currencyAddress = form.watch("currencyAddress"); + + const columns: ColumnDef<{ + recipient: string; + claimable: bigint; + claim: string; + }>[] = [ + { + accessorKey: "recipient", + header: "Recipient", + }, + { + accessorKey: "claimable", + header: "Claimable Amount", + }, + { + accessorKey: "claim", + header: "Claim", + cell: ({ row }) => { + return ( + + ); + }, + }, + ]; + + console.log("allocations: ", props.allocations); + console.log("recipients: ", props.recipients); + + const totalAllocation = props.allocations.reduce( + (acc, curr) => acc + curr, + 0n, + ); + const data = props.recipients.map((recipient, i) => ({ + recipient: recipient, + allocation: + (Number(props.allocations[i] || 0n) / Number(totalAllocation)) * 100, + })); + + const table = useReactTable({ + data, + columns, + getCoreRowModel: getCoreRowModel(), + }); + + return ( +
+ {/* Header */} +
+ {/* Title */} +
+

+ Split Fees + {/* Info Dialog */} + + + + + + + Split Fees Contract + + This contract holds the funds that are split between the + recipients. + + + {/* Avoid adding focus on other elements to prevent tooltips from opening on modal open */} + + +
+ +
+
+

+ Split Fees +

+ +
+ +
+

+ Controller +

+ +
+
+ + +
+

+
+ +
+ + ( + + Currency + + + )} + /> + + + + + {table.getHeaderGroups().map((headerGroup) => ( + + {headerGroup.headers.map((header) => { + return ( + + {header.isPlaceholder + ? null + : flexRender( + header.column.columnDef.header, + header.getContext(), + )} + + ); + })} + + ))} + + + {table.getRowModel().rows.map((row) => ( + + {row.getVisibleCells().map((cell) => ( + + {flexRender( + cell.column.columnDef.cell, + cell.getContext(), + )} + + ))} + + ))} + +
+
+
+ +
+ +
+
+ ); +} From 4cbc755584b9c438691e4fb8d6db166c38f373e4 Mon Sep 17 00:00:00 2001 From: Stanley Date: Sat, 14 Dec 2024 17:45:23 -0500 Subject: [PATCH 5/9] created claim page --- .../split-fees/ClaimFeesCard.tsx | 210 +++++++++++++----- .../[contractAddress]/split-fees/page.tsx | 11 +- 2 files changed, 161 insertions(+), 60 deletions(-) diff --git a/apps/dashboard/src/app/(dashboard)/(chain)/[chain_id]/[contractAddress]/split-fees/ClaimFeesCard.tsx b/apps/dashboard/src/app/(dashboard)/(chain)/[chain_id]/[contractAddress]/split-fees/ClaimFeesCard.tsx index 04410cfa126..f63c6c83712 100644 --- a/apps/dashboard/src/app/(dashboard)/(chain)/[chain_id]/[contractAddress]/split-fees/ClaimFeesCard.tsx +++ b/apps/dashboard/src/app/(dashboard)/(chain)/[chain_id]/[contractAddress]/split-fees/ClaimFeesCard.tsx @@ -11,7 +11,7 @@ import { DialogTitle, DialogTrigger, } from "@/components/ui/dialog"; -import { FormField, FormItem, FormLabel } from "@/components/ui/form"; +import { Form, FormField, FormItem, FormLabel } from "@/components/ui/form"; import { Table, TableBody, @@ -21,16 +21,26 @@ import { TableHeader, TableRow, } from "@/components/ui/table"; +import { useQuery } from "@tanstack/react-query"; import { type ColumnDef, flexRender, getCoreRowModel, useReactTable, } from "@tanstack/react-table"; +import { useAllChainsData } from "hooks/chains/allChains"; import { InfoIcon } from "lucide-react"; +import { useMemo } from "react"; import { useForm } from "react-hook-form"; -import { NATIVE_TOKEN_ADDRESS } from "thirdweb"; -import { useActiveWalletChain } from "thirdweb/react"; +import { + NATIVE_TOKEN_ADDRESS, + type ThirdwebContract, + eth_getBalance, + getContract, + getRpcClient, + readContract, +} from "thirdweb"; +import { getBalance, getCurrencyMetadata } from "thirdweb/extensions/erc20"; import { CurrencySelector } from "../modules/components/CurrencySelector"; export function ClaimFeesCard(props: { @@ -38,56 +48,140 @@ export function ClaimFeesCard(props: { recipients: string[]; allocations: bigint[]; controller: string; - referenceContract: string; + splitFeesCore: ThirdwebContract; }) { - const isController = false; - const chain = useActiveWalletChain(); + const _isController = false; + const { idToChain } = useAllChainsData(); + const chain = idToChain.get(props.splitFeesCore.chain.id); const form = useForm<{ currencyAddress: string }>({ values: { currencyAddress: NATIVE_TOKEN_ADDRESS, }, }); - const _currencyAddress = form.watch("currencyAddress"); - - const columns: ColumnDef<{ - recipient: string; - claimable: bigint; - claim: string; - }>[] = [ - { - accessorKey: "recipient", - header: "Recipient", - }, - { - accessorKey: "claimable", - header: "Claimable Amount", + const currencyAddress = form.watch("currencyAddress"); + const currencyMetadata = useQuery({ + queryKey: ["currencyMetadata", currencyAddress], + queryFn: async () => { + const erc20Contract = getContract({ + address: currencyAddress, + client: props.splitFeesCore.client, + chain: props.splitFeesCore.chain, + }); + return getCurrencyMetadata({ + contract: erc20Contract, + }); }, - { - accessorKey: "claim", - header: "Claim", - cell: ({ row }) => { - return ( - - ); - }, + enabled: currencyAddress !== NATIVE_TOKEN_ADDRESS, + }); + + const claimAmounts = useQuery({ + queryKey: ["claimAmounts", currencyAddress], + queryFn: async () => { + const erc6909Balances = await Promise.all( + props.recipients.map(async (recipient) => + readContract({ + contract: props.splitFeesCore, + method: + "function balanceOf(address owner, uint256 id) returns (uint256 amount)", + params: [recipient, BigInt(currencyAddress)], + }), + ), + ); + console.log("erc6909Balances: ", erc6909Balances); + + let splitWalletBalance: bigint; + if (currencyAddress === NATIVE_TOKEN_ADDRESS) { + const rpcRequest = getRpcClient({ + client: props.splitFeesCore.client, + chain: props.splitFeesCore.chain, + }); + splitWalletBalance = await eth_getBalance(rpcRequest, { + address: props.splitWallet, + }); + } else { + const { value } = await getBalance({ + contract: props.splitFeesCore, + address: currencyAddress, + }); + splitWalletBalance = value; + } + console.log("splitWalletBalance: ", splitWalletBalance); + + const totalAllocation = props.allocations.reduce( + (acc, curr) => acc + curr, + 0n, + ); + console.log("totalAllocation: ", totalAllocation); + const claimAmounts = erc6909Balances.map( + (balance, i) => + ((props.allocations[i] || 0n) * splitWalletBalance) / + totalAllocation + + balance, + ); + console.log("claimAmounts: ", claimAmounts); + + return claimAmounts; }, - ]; + }); - console.log("allocations: ", props.allocations); - console.log("recipients: ", props.recipients); + const columns = useMemo< + ColumnDef<{ recipient: string; claimable: bigint; claim: string }>[] + >( + () => [ + { + accessorKey: "recipient", + header: "Recipient", + }, + { + accessorKey: "claimable", + header: "Claimable Amount", + cell: ({ row }) => { + if ( + currencyAddress !== NATIVE_TOKEN_ADDRESS && + !currencyMetadata.data + ) + return null; + if (currencyAddress === NATIVE_TOKEN_ADDRESS) { + return ( +

{(row.getValue("claimable") as bigint) / 10n ** 18n} ETH

+ ); + } - const totalAllocation = props.allocations.reduce( - (acc, curr) => acc + curr, - 0n, + return ( +

+ {(row.getValue("claimable") as bigint) / + 10n ** BigInt(currencyMetadata.data?.decimals || 0n)}{" "} + {currencyMetadata.data?.symbol} +

+ ); + }, + }, + { + accessorKey: "claim", + header: "Claim", + cell: ({ row }) => { + return ( + + ); + }, + }, + ], + [], ); - const data = props.recipients.map((recipient, i) => ({ - recipient: recipient, - allocation: - (Number(props.allocations[i] || 0n) / Number(totalAllocation)) * 100, - })); + + const data = useMemo(() => { + return props.recipients.map((recipient, i) => ({ + recipient, + claimable: claimAmounts?.data?.[i] ?? 0n, + claim: "", // dummy value for react-table + })); + }, [props.recipients, claimAmounts.data]); const table = useReactTable({ data, @@ -152,18 +246,24 @@ export function ClaimFeesCard(props: {
-
+
- ( - - Currency - - - )} - /> +
+ + ( + + Currency + + + )} + /> + + + +
@@ -205,12 +305,6 @@ export function ClaimFeesCard(props: {
- -
- -
); } diff --git a/apps/dashboard/src/app/(dashboard)/(chain)/[chain_id]/[contractAddress]/split-fees/page.tsx b/apps/dashboard/src/app/(dashboard)/(chain)/[chain_id]/[contractAddress]/split-fees/page.tsx index 1a40f73ec9c..84eeb26f73e 100644 --- a/apps/dashboard/src/app/(dashboard)/(chain)/[chain_id]/[contractAddress]/split-fees/page.tsx +++ b/apps/dashboard/src/app/(dashboard)/(chain)/[chain_id]/[contractAddress]/split-fees/page.tsx @@ -4,7 +4,7 @@ import { getContractEvents, prepareEvent } from "thirdweb"; import { type FetchDeployMetadataResult, getContract } from "thirdweb/contract"; import { getContractPageParamsInfo } from "../_utils/getContractFromParams"; import { getContractPageMetadata } from "../_utils/getContractPageMetadata"; -import { SplitFeesCard } from "./SplitFeesCard"; +import { ClaimFeesCard } from "./ClaimFeesCard"; export function getModuleInstallParams(mod: FetchDeployMetadataResult) { return ( @@ -78,8 +78,15 @@ export default async function Page(props: { return (
{splits.map((split) => ( - + ))} + {/*splits.map((split) => ( + + ))*/} From 52b50c7a9693603df6e04f85a46297ab345e5b1b Mon Sep 17 00:00:00 2001 From: Stanley Date: Sun, 15 Dec 2024 13:43:53 -0700 Subject: [PATCH 6/9] implemented claim function --- .../split-fees/ClaimFeesCard.tsx | 43 ++++++++++++++++++- 1 file changed, 41 insertions(+), 2 deletions(-) diff --git a/apps/dashboard/src/app/(dashboard)/(chain)/[chain_id]/[contractAddress]/split-fees/ClaimFeesCard.tsx b/apps/dashboard/src/app/(dashboard)/(chain)/[chain_id]/[contractAddress]/split-fees/ClaimFeesCard.tsx index f63c6c83712..b44866d7515 100644 --- a/apps/dashboard/src/app/(dashboard)/(chain)/[chain_id]/[contractAddress]/split-fees/ClaimFeesCard.tsx +++ b/apps/dashboard/src/app/(dashboard)/(chain)/[chain_id]/[contractAddress]/split-fees/ClaimFeesCard.tsx @@ -38,9 +38,12 @@ import { eth_getBalance, getContract, getRpcClient, + prepareContractCall, readContract, + sendAndConfirmTransaction, } from "thirdweb"; import { getBalance, getCurrencyMetadata } from "thirdweb/extensions/erc20"; +import { useActiveAccount } from "thirdweb/react"; import { CurrencySelector } from "../modules/components/CurrencySelector"; export function ClaimFeesCard(props: { @@ -50,9 +53,9 @@ export function ClaimFeesCard(props: { controller: string; splitFeesCore: ThirdwebContract; }) { - const _isController = false; const { idToChain } = useAllChainsData(); const chain = idToChain.get(props.splitFeesCore.chain.id); + const account = useActiveAccount(); const form = useForm<{ currencyAddress: string }>({ values: { currencyAddress: NATIVE_TOKEN_ADDRESS, @@ -125,6 +128,42 @@ export function ClaimFeesCard(props: { }, }); + const claim = async (recipient: string) => { + if (!account) { + throw new Error("Account does not exist"); + } + const splitWallet = getContract({ + address: props.splitWallet, + client: props.splitFeesCore.client, + chain: props.splitFeesCore.chain, + }); + const { value: splitWalletBalance } = await getBalance({ + contract: splitWallet, + address: currencyAddress, + }); + if (splitWalletBalance > 0n) { + const distributeTx = prepareContractCall({ + contract: props.splitFeesCore, + method: "function distribute(address _splitWallet, address _token)", + params: [props.splitWallet, currencyAddress], + }); + await sendAndConfirmTransaction({ + account, + transaction: distributeTx, + }); + } + + const withdrawTx = prepareContractCall({ + contract: props.splitFeesCore, + method: "function withdraw(address account, address _token)", + params: [recipient, currencyAddress], + }); + await sendAndConfirmTransaction({ + account, + transaction: withdrawTx, + }); + }; + const columns = useMemo< ColumnDef<{ recipient: string; claimable: bigint; claim: string }>[] >( @@ -163,7 +202,7 @@ export function ClaimFeesCard(props: { cell: ({ row }) => { return ( + +
+ {tab === "splitFeesCard" && + props.splits.map((split) => ( + + ))} + + {tab === "claimFeesCard" && ( + <> + {props.splits.map((split) => ( + + ))} + + + + + + )} +
+ ); +} + +export default SplitFees; diff --git a/apps/dashboard/src/app/(dashboard)/(chain)/[chain_id]/[contractAddress]/split-fees/SplitFeesCard.tsx b/apps/dashboard/src/app/(dashboard)/(chain)/[chain_id]/[contractAddress]/split-fees/SplitFeesCard.tsx index 47d95dfaaac..177ed880920 100644 --- a/apps/dashboard/src/app/(dashboard)/(chain)/[chain_id]/[contractAddress]/split-fees/SplitFeesCard.tsx +++ b/apps/dashboard/src/app/(dashboard)/(chain)/[chain_id]/[contractAddress]/split-fees/SplitFeesCard.tsx @@ -27,6 +27,8 @@ import { useReactTable, } from "@tanstack/react-table"; import { InfoIcon } from "lucide-react"; +import { useMemo } from "react"; +import { useActiveAccount } from "thirdweb/react"; export function SplitFeesCard(props: { splitWallet: string; @@ -35,7 +37,8 @@ export function SplitFeesCard(props: { controller: string; referenceContract: string; }) { - const isController = false; + const account = useActiveAccount(); + const isController = props.controller === account?.address; const columns: ColumnDef<{ allocation: number; recipient: string }>[] = [ { @@ -48,18 +51,19 @@ export function SplitFeesCard(props: { }, ]; - console.log("allocations: ", props.allocations); - console.log("recipients: ", props.recipients); - const totalAllocation = props.allocations.reduce( (acc, curr) => acc + curr, 0n, ); - const data = props.recipients.map((recipient, i) => ({ - recipient: recipient, - allocation: - (Number(props.allocations[i] || 0n) / Number(totalAllocation)) * 100, - })); + const data = useMemo( + () => + props.recipients.map((recipient, i) => ({ + recipient: recipient, + allocation: + (Number(props.allocations[i] || 0n) / Number(totalAllocation)) * 100, + })), + [props.recipients, props.allocations, totalAllocation], + ); const table = useReactTable({ data, diff --git a/apps/dashboard/src/app/(dashboard)/(chain)/[chain_id]/[contractAddress]/split-fees/page.tsx b/apps/dashboard/src/app/(dashboard)/(chain)/[chain_id]/[contractAddress]/split-fees/page.tsx index 84eeb26f73e..c09698625a5 100644 --- a/apps/dashboard/src/app/(dashboard)/(chain)/[chain_id]/[contractAddress]/split-fees/page.tsx +++ b/apps/dashboard/src/app/(dashboard)/(chain)/[chain_id]/[contractAddress]/split-fees/page.tsx @@ -1,10 +1,9 @@ -import { Button } from "@/components/ui/button"; import { notFound, redirect } from "next/navigation"; import { getContractEvents, prepareEvent } from "thirdweb"; import { type FetchDeployMetadataResult, getContract } from "thirdweb/contract"; import { getContractPageParamsInfo } from "../_utils/getContractFromParams"; import { getContractPageMetadata } from "../_utils/getContractPageMetadata"; -import { ClaimFeesCard } from "./ClaimFeesCard"; +import SplitFees from "./SplitFees"; export function getModuleInstallParams(mod: FetchDeployMetadataResult) { return ( @@ -76,20 +75,10 @@ export default async function Page(props: { console.log("splits: ", splits); return ( -
- {splits.map((split) => ( - - ))} - {/*splits.map((split) => ( - - ))*/} - -
+ ); } diff --git a/packages/thirdweb/src/react/core/hooks/contract/useReadContract.ts b/packages/thirdweb/src/react/core/hooks/contract/useReadContract.ts index 91fab7227db..97e7dab5bcd 100644 --- a/packages/thirdweb/src/react/core/hooks/contract/useReadContract.ts +++ b/packages/thirdweb/src/react/core/hooks/contract/useReadContract.ts @@ -114,6 +114,7 @@ export function useReadContract< queryOptions?: PickedQueryOptions; }, ) { + console.error("extensionOrOptions: ", extensionOrOptions); // extension case if (typeof extensionOrOptions === "function") { if (!options) { @@ -123,6 +124,8 @@ export function useReadContract< } const { queryOptions, contract, ...params } = options; + console.error("contract: ", contract); + const query = defineQuery({ queryKey: [ "readContract", diff --git a/packages/thirdweb/src/utils/ens/namehash.ts b/packages/thirdweb/src/utils/ens/namehash.ts index 87eb6fae7e4..fa450a83f30 100644 --- a/packages/thirdweb/src/utils/ens/namehash.ts +++ b/packages/thirdweb/src/utils/ens/namehash.ts @@ -20,7 +20,10 @@ export function namehash(name: string) { const hashed = hashFromEncodedLabel ? toBytes(hashFromEncodedLabel) : keccak256(stringToBytes(item), "bytes"); - result = keccak256(concat([result, hashed]), "bytes"); + result = keccak256( + concat([result, hashed]), + "bytes", + ) as Uint8Array; } return bytesToHex(result); From 7b9289fe754f46d419acdfcd3ef0cd564ffe476f Mon Sep 17 00:00:00 2001 From: Stanley Date: Wed, 18 Dec 2024 10:14:44 +0900 Subject: [PATCH 8/9] implemented update and create splits in split fees tab --- .../modules/components/Claimable.tsx | 1 + .../split-fees/ClaimFeesCard.tsx | 63 ++++++++++---- .../split-fees/ConfigureSplitFees.tsx | 30 ++++--- .../split-fees/SplitFees.tsx | 10 ++- .../split-fees/SplitFeesCard.tsx | 22 +++-- .../[contractAddress]/split-fees/page.tsx | 82 +++++++++++++++---- .../core/hooks/contract/useReadContract.ts | 3 - 7 files changed, 156 insertions(+), 55 deletions(-) diff --git a/apps/dashboard/src/app/(dashboard)/(chain)/[chain_id]/[contractAddress]/modules/components/Claimable.tsx b/apps/dashboard/src/app/(dashboard)/(chain)/[chain_id]/[contractAddress]/modules/components/Claimable.tsx index a2d425bb14b..d7c820882ae 100644 --- a/apps/dashboard/src/app/(dashboard)/(chain)/[chain_id]/[contractAddress]/modules/components/Claimable.tsx +++ b/apps/dashboard/src/app/(dashboard)/(chain)/[chain_id]/[contractAddress]/modules/components/Claimable.tsx @@ -77,6 +77,7 @@ const positiveIntegerRegex = /^[0-9]\d*$/; const splitWalletBytecode = "0x3d3d3d3d363d3d37363d7341dc1be6e4c7698f46268251b88b1f789aa9df265af43d3d93803e602a57fd5bf3"; +("0x3d3d3d3d363d3d37363d73207d879bae9cbf900d5e1eec04019613cedc25455af43d3d93803e602a57fd5bf3"); function ClaimableModule(props: ModuleInstanceProps) { const { contract, ownerAccount } = props; diff --git a/apps/dashboard/src/app/(dashboard)/(chain)/[chain_id]/[contractAddress]/split-fees/ClaimFeesCard.tsx b/apps/dashboard/src/app/(dashboard)/(chain)/[chain_id]/[contractAddress]/split-fees/ClaimFeesCard.tsx index b44866d7515..9618342dc5b 100644 --- a/apps/dashboard/src/app/(dashboard)/(chain)/[chain_id]/[contractAddress]/split-fees/ClaimFeesCard.tsx +++ b/apps/dashboard/src/app/(dashboard)/(chain)/[chain_id]/[contractAddress]/split-fees/ClaimFeesCard.tsx @@ -2,6 +2,7 @@ import { WalletAddress } from "@/components/blocks/wallet-address"; import { CopyAddressButton } from "@/components/ui/CopyAddressButton"; +import { Spinner } from "@/components/ui/Spinner/Spinner"; import { Button } from "@/components/ui/button"; import { Dialog, @@ -21,7 +22,7 @@ import { TableHeader, TableRow, } from "@/components/ui/table"; -import { useQuery } from "@tanstack/react-query"; +import { useMutation, useQuery } from "@tanstack/react-query"; import { type ColumnDef, flexRender, @@ -29,6 +30,7 @@ import { useReactTable, } from "@tanstack/react-table"; import { useAllChainsData } from "hooks/chains/allChains"; +import { useTxNotifications } from "hooks/useTxNotifications"; import { InfoIcon } from "lucide-react"; import { useMemo } from "react"; import { useForm } from "react-hook-form"; @@ -41,6 +43,8 @@ import { prepareContractCall, readContract, sendAndConfirmTransaction, + toEther, + toUnits, } from "thirdweb"; import { getBalance, getCurrencyMetadata } from "thirdweb/extensions/erc20"; import { useActiveAccount } from "thirdweb/react"; @@ -48,8 +52,8 @@ import { CurrencySelector } from "../modules/components/CurrencySelector"; export function ClaimFeesCard(props: { splitWallet: string; - recipients: string[]; - allocations: bigint[]; + recipients: readonly string[]; + allocations: readonly bigint[]; controller: string; splitFeesCore: ThirdwebContract; }) { @@ -127,6 +131,7 @@ export function ClaimFeesCard(props: { return claimAmounts; }, }); + console.log("claimAmounts: ", claimAmounts.data); const claim = async (recipient: string) => { if (!account) { @@ -137,10 +142,25 @@ export function ClaimFeesCard(props: { client: props.splitFeesCore.client, chain: props.splitFeesCore.chain, }); - const { value: splitWalletBalance } = await getBalance({ - contract: splitWallet, - address: currencyAddress, - }); + console.log("splitWallet: ", splitWallet); + let splitWalletBalance: bigint; + if (currencyAddress === NATIVE_TOKEN_ADDRESS) { + const rpcRequest = getRpcClient({ + client: props.splitFeesCore.client, + chain: props.splitFeesCore.chain, + }); + splitWalletBalance = await eth_getBalance(rpcRequest, { + address: props.splitWallet, + }); + } else { + const { value } = await getBalance({ + contract: props.splitFeesCore, + address: currencyAddress, + }); + splitWalletBalance = value; + } + console.log("split balance in claim: ", splitWalletBalance); + if (splitWalletBalance > 0n) { const distributeTx = prepareContractCall({ contract: props.splitFeesCore, @@ -164,6 +184,16 @@ export function ClaimFeesCard(props: { }); }; + const claimNotifications = useTxNotifications( + "Claim successful", + "Claim failed", + ); + const claimMutation = useMutation({ + mutationFn: claim, + onSuccess: claimNotifications.onSuccess, + onError: claimNotifications.onError, + }); + const columns = useMemo< ColumnDef<{ recipient: string; claimable: bigint; claim: string }>[] >( @@ -182,15 +212,15 @@ export function ClaimFeesCard(props: { ) return null; if (currencyAddress === NATIVE_TOKEN_ADDRESS) { - return ( -

{(row.getValue("claimable") as bigint) / 10n ** 18n} ETH

- ); + return

{toEther(row.getValue("claimable") as bigint)} ETH

; } return (

- {(row.getValue("claimable") as bigint) / - 10n ** BigInt(currencyMetadata.data?.decimals || 0n)}{" "} + {toUnits( + row.getValue("claimable"), + currencyMetadata.data?.decimals || 18, + )}{" "} {currencyMetadata.data?.symbol}

); @@ -202,16 +232,19 @@ export function ClaimFeesCard(props: { cell: ({ row }) => { return ( ); }, }, ], - [], + [currencyAddress, currencyMetadata.data], ); const data = useMemo(() => { diff --git a/apps/dashboard/src/app/(dashboard)/(chain)/[chain_id]/[contractAddress]/split-fees/ConfigureSplitFees.tsx b/apps/dashboard/src/app/(dashboard)/(chain)/[chain_id]/[contractAddress]/split-fees/ConfigureSplitFees.tsx index 2a88ec9af93..05f98b17bdc 100644 --- a/apps/dashboard/src/app/(dashboard)/(chain)/[chain_id]/[contractAddress]/split-fees/ConfigureSplitFees.tsx +++ b/apps/dashboard/src/app/(dashboard)/(chain)/[chain_id]/[contractAddress]/split-fees/ConfigureSplitFees.tsx @@ -1,11 +1,10 @@ -"use client"; - import { Accordion, AccordionContent, AccordionItem, AccordionTrigger, } from "@/components/ui/accordion"; +import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"; import { Button } from "@/components/ui/button"; import { Dialog, @@ -26,7 +25,6 @@ import { } from "@/components/ui/form"; import { Input } from "@/components/ui/input"; import { ToolTipLabel } from "@/components/ui/tooltip"; -import { Alert, AlertDescription, AlertTitle } from "@chakra-ui/react"; import { zodResolver } from "@hookform/resolvers/zod"; import { useMutation } from "@tanstack/react-query"; import { TransactionButton } from "components/buttons/TransactionButton"; @@ -52,19 +50,21 @@ type Recipient = { function ConfigureSplit(props: { children: React.ReactNode; - isNewSplit: boolean; + isNewSplit?: boolean; splitWallet?: string; referenceContract: ThirdwebContract; - postSplitConfigure?: (splitWallet: string) => void; + postSplitConfigure?: (splitWallet: string) => Promise | void; }) { const activeAccount = useActiveAccount(); const [open, setOpen] = useState(false); + console.log("reference contract: ", props.referenceContract); const splitFeesCore = getContract({ address: splitFeesCoreAddress, client: props.referenceContract.client, chain: props.referenceContract.chain, }); + console.log("gets here"); const split = useReadContract({ contract: splitFeesCore, @@ -147,6 +147,7 @@ function ConfigureSplit(props: { transaction, account: activeAccount, }); + console.log("receipt for create split: ", receipt); const decodedEvent = parseEventLogs({ events: [ @@ -164,9 +165,10 @@ function ConfigureSplit(props: { } if (props.postSplitConfigure) { - props.postSplitConfigure( - (decodedEvent[0]?.args as { splitWallet: string }).splitWallet, - ); + const { splitWallet } = decodedEvent[0]?.args as { splitWallet: string }; + console.log("split wallet: ", splitWallet); + await props.postSplitConfigure(splitWallet); + console.log("post split configured"); } split.refetch(); @@ -212,6 +214,11 @@ function ConfigureSplit(props: { }); console.log("transaction sent"); + if (props.postSplitConfigure) { + await props.postSplitConfigure(""); + console.log("post split configured"); + } + split.refetch(); setOpen(false); }; @@ -247,7 +254,7 @@ function ConfigureSplit(props: { } // TODO: place this somwhere appropriate -const splitFeesCoreAddress = "0x7d6Ba9e63eFb30c42b25db50dBD3C2F0a4578Ba2"; +const splitFeesCoreAddress = "0x640a2bb44A4c3644B416aCA8e60C67B11E41C8DF"; const formSchema = z .object({ @@ -350,7 +357,10 @@ function ConfigureSplitUI(props: { const recipients = values.recipients.map((r) => r.address); console.log("recipients in submit: ", recipients); console.log("is new split: ", props.isNewSplit); - (props.isNewSplit ? createSplitMutation : updateSplitMutation).mutateAsync({ + await (props.isNewSplit + ? createSplitMutation + : updateSplitMutation + ).mutateAsync({ recipients, allocations, controller: values.controller, diff --git a/apps/dashboard/src/app/(dashboard)/(chain)/[chain_id]/[contractAddress]/split-fees/SplitFees.tsx b/apps/dashboard/src/app/(dashboard)/(chain)/[chain_id]/[contractAddress]/split-fees/SplitFees.tsx index 8346baae649..7aef114cc8b 100644 --- a/apps/dashboard/src/app/(dashboard)/(chain)/[chain_id]/[contractAddress]/split-fees/SplitFees.tsx +++ b/apps/dashboard/src/app/(dashboard)/(chain)/[chain_id]/[contractAddress]/split-fees/SplitFees.tsx @@ -9,8 +9,8 @@ import { SplitFeesCard } from "./SplitFeesCard"; type Split = { splitWallet: string; - recipients: string[]; - allocations: bigint[]; + recipients: readonly string[]; + allocations: readonly bigint[]; controller: string; referenceContract: string; }; @@ -46,7 +46,11 @@ function SplitFees(props: { {tab === "claimFeesCard" && ( <> {props.splits.map((split) => ( - + ))} diff --git a/apps/dashboard/src/app/(dashboard)/(chain)/[chain_id]/[contractAddress]/split-fees/SplitFeesCard.tsx b/apps/dashboard/src/app/(dashboard)/(chain)/[chain_id]/[contractAddress]/split-fees/SplitFeesCard.tsx index 177ed880920..c22d1b5d989 100644 --- a/apps/dashboard/src/app/(dashboard)/(chain)/[chain_id]/[contractAddress]/split-fees/SplitFeesCard.tsx +++ b/apps/dashboard/src/app/(dashboard)/(chain)/[chain_id]/[contractAddress]/split-fees/SplitFeesCard.tsx @@ -27,18 +27,22 @@ import { useReactTable, } from "@tanstack/react-table"; import { InfoIcon } from "lucide-react"; +import { useRouter } from "next/navigation"; import { useMemo } from "react"; +import type { ThirdwebContract } from "thirdweb"; import { useActiveAccount } from "thirdweb/react"; +import ConfigureSplit from "./ConfigureSplitFees"; export function SplitFeesCard(props: { splitWallet: string; - recipients: string[]; - allocations: bigint[]; + recipients: readonly string[]; + allocations: readonly bigint[]; controller: string; - referenceContract: string; + referenceContract: ThirdwebContract; }) { const account = useActiveAccount(); const isController = props.controller === account?.address; + const router = useRouter(); const columns: ColumnDef<{ allocation: number; recipient: string }>[] = [ { @@ -172,9 +176,15 @@ export function SplitFeesCard(props: {
- + router.refresh()} + > + +
); diff --git a/apps/dashboard/src/app/(dashboard)/(chain)/[chain_id]/[contractAddress]/split-fees/page.tsx b/apps/dashboard/src/app/(dashboard)/(chain)/[chain_id]/[contractAddress]/split-fees/page.tsx index c09698625a5..2b2baf327f5 100644 --- a/apps/dashboard/src/app/(dashboard)/(chain)/[chain_id]/[contractAddress]/split-fees/page.tsx +++ b/apps/dashboard/src/app/(dashboard)/(chain)/[chain_id]/[contractAddress]/split-fees/page.tsx @@ -1,5 +1,5 @@ import { notFound, redirect } from "next/navigation"; -import { getContractEvents, prepareEvent } from "thirdweb"; +import { getContractEvents, prepareEvent, readContract } from "thirdweb"; import { type FetchDeployMetadataResult, getContract } from "thirdweb/contract"; import { getContractPageParamsInfo } from "../_utils/getContractFromParams"; import { getContractPageMetadata } from "../_utils/getContractPageMetadata"; @@ -14,7 +14,7 @@ export function getModuleInstallParams(mod: FetchDeployMetadataResult) { } // TODO: place this somwhere appropriate -const splitFeesCoreAddress = "0x7d6Ba9e63eFb30c42b25db50dBD3C2F0a4578Ba2"; +const splitFeesCoreAddress = "0x640a2bb44A4c3644B416aCA8e60C67B11E41C8DF"; export default async function Page(props: { params: Promise<{ @@ -56,22 +56,68 @@ export default async function Page(props: { blockRange: 123456n, }); - const splits = events.map((e) => { - const args = e.args as { - splitWallet: string; - recipients: string[]; - allocations: bigint[]; - controller: string; - referenceContract: string; - }; - return { - splitWallet: args.splitWallet, - recipients: args.recipients, - allocations: args.allocations, - controller: args.controller, - referenceContract: args.referenceContract, - }; - }); + const splits = await Promise.all( + events + .filter( + (e) => + (e.args as { referenceContract: string }).referenceContract === + contract.address, + ) + .map(async (e) => { + const split = await readContract({ + contract: splitFeesCore, + method: { + type: "function", + name: "getSplit", + inputs: [ + { + name: "_splitWallet", + type: "address", + internalType: "address", + }, + ], + outputs: [ + { + name: "", + type: "tuple", + internalType: "struct Split", + components: [ + { + name: "controller", + type: "address", + internalType: "address", + }, + { + name: "recipients", + type: "address[]", + internalType: "address[]", + }, + { + name: "allocations", + type: "uint256[]", + internalType: "uint256[]", + }, + { + name: "totalAllocation", + type: "uint256", + internalType: "uint256", + }, + ], + }, + ], + stateMutability: "view", + }, + params: [(e.args as { splitWallet: string }).splitWallet], + }); + return { + splitWallet: (e.args as { splitWallet: string }).splitWallet, + recipients: split.recipients, + allocations: split.allocations, + controller: split.controller, + referenceContract: contract.address, + }; + }), + ); console.log("splits: ", splits); return ( diff --git a/packages/thirdweb/src/react/core/hooks/contract/useReadContract.ts b/packages/thirdweb/src/react/core/hooks/contract/useReadContract.ts index 97e7dab5bcd..91fab7227db 100644 --- a/packages/thirdweb/src/react/core/hooks/contract/useReadContract.ts +++ b/packages/thirdweb/src/react/core/hooks/contract/useReadContract.ts @@ -114,7 +114,6 @@ export function useReadContract< queryOptions?: PickedQueryOptions; }, ) { - console.error("extensionOrOptions: ", extensionOrOptions); // extension case if (typeof extensionOrOptions === "function") { if (!options) { @@ -124,8 +123,6 @@ export function useReadContract< } const { queryOptions, contract, ...params } = options; - console.error("contract: ", contract); - const query = defineQuery({ queryKey: [ "readContract", From a178b3d1de5f2fad29e400ee94ecfbde3860a3dd Mon Sep 17 00:00:00 2001 From: Stanley Date: Wed, 18 Dec 2024 10:51:33 +0900 Subject: [PATCH 9/9] done end to end loop draft --- .../split-fees/SplitFees.tsx | 26 +++++++++++++------ 1 file changed, 18 insertions(+), 8 deletions(-) diff --git a/apps/dashboard/src/app/(dashboard)/(chain)/[chain_id]/[contractAddress]/split-fees/SplitFees.tsx b/apps/dashboard/src/app/(dashboard)/(chain)/[chain_id]/[contractAddress]/split-fees/SplitFees.tsx index 7aef114cc8b..fa26c385f5e 100644 --- a/apps/dashboard/src/app/(dashboard)/(chain)/[chain_id]/[contractAddress]/split-fees/SplitFees.tsx +++ b/apps/dashboard/src/app/(dashboard)/(chain)/[chain_id]/[contractAddress]/split-fees/SplitFees.tsx @@ -1,6 +1,7 @@ "use client"; import { Button } from "@/components/ui/button"; +import { TabButtons } from "@/components/ui/tabs"; import { useState } from "react"; import type { ThirdwebContract } from "thirdweb"; import { ClaimFeesCard } from "./ClaimFeesCard"; @@ -24,16 +25,25 @@ function SplitFees(props: { "splitFeesCard", ); + const tabs = [ + { + name: "Split Fees", + onClick: () => setTab("splitFeesCard"), + isActive: tab === "splitFeesCard", + isEnabled: true, + }, + { + name: "Claim Fees", + onClick: () => setTab("claimFeesCard"), + isActive: tab === "claimFeesCard", + isEnabled: true, + }, + ]; + return (
-
- - -
+ + {tab === "splitFeesCard" && props.splits.map((split) => (