diff --git a/apps/dashboard/src/components/contract-components/contract-deploy-form/custom-contract.tsx b/apps/dashboard/src/components/contract-components/contract-deploy-form/custom-contract.tsx index 1f8237dd256..28176570641 100644 --- a/apps/dashboard/src/components/contract-components/contract-deploy-form/custom-contract.tsx +++ b/apps/dashboard/src/components/contract-components/contract-deploy-form/custom-contract.tsx @@ -37,7 +37,12 @@ import { import { useActiveAccount, useActiveWalletChain } from "thirdweb/react"; import { upload } from "thirdweb/storage"; import { FormHelperText, FormLabel, Heading, Text } from "tw-components"; -import { useCustomFactoryAbi, useFunctionParamsFromABI } from "../hooks"; +import { + useCustomFactoryAbi, + useFunctionParamsFromABI, + useMintfeeManager, + useMultisig, +} from "../hooks"; import { addContractToMultiChainRegistry } from "../utils"; import { Fieldset } from "./common"; import { ContractMetadataFieldset } from "./contract-metadata-fieldset"; @@ -149,6 +154,9 @@ export const CustomContractForm: React.FC = ({ const isTWPublisher = checkTwPublisher(metadata?.publisher); + const multisig = useMultisig(walletChain?.id); + const mintfeeManager = useMintfeeManager(walletChain?.id); + const initializerParams = useFunctionParamsFromABI( metadata?.deployType === "customFactory" && customFactoryAbi?.data ? customFactoryAbi.data @@ -193,6 +201,8 @@ export const CustomContractForm: React.FC = ({ { connectedWallet: activeAccount?.address, chainId: walletChain?.id, + multisig: multisig.data?.multisig, + mintFeeManager: mintfeeManager.data?.mintfeeManager, }, ); @@ -212,7 +222,14 @@ export const CustomContractForm: React.FC = ({ {} as Record, ), }), - [deployParams, metadata?.constructorParams, activeAccount, walletChain?.id], + [ + deployParams, + metadata?.constructorParams, + activeAccount, + walletChain?.id, + mintfeeManager, + multisig, + ], ); const transformedQueryData = useMemo( diff --git a/apps/dashboard/src/components/contract-components/contract-publish-form/contract-params-fieldset.tsx b/apps/dashboard/src/components/contract-components/contract-publish-form/contract-params-fieldset.tsx index b9cdbef3c58..b66c5b1af96 100644 --- a/apps/dashboard/src/components/contract-components/contract-publish-form/contract-params-fieldset.tsx +++ b/apps/dashboard/src/components/contract-components/contract-publish-form/contract-params-fieldset.tsx @@ -5,8 +5,8 @@ import { Input, InputGroup, InputRightElement, + Select, Textarea, - Tooltip, useBreakpointValue, } from "@chakra-ui/react"; import type { AbiParameter } from "abitype"; @@ -15,8 +15,6 @@ import { camelToTitle } from "contract-ui/components/solidity-inputs/helpers"; import { getTemplateValuesForType } from "lib/deployment/template-values"; import { useFormContext } from "react-hook-form"; import { - Button, - Card, Checkbox, FormErrorMessage, FormHelperText, @@ -133,44 +131,27 @@ export const ContractParamsFieldset: React.FC = ({ /> {paramTemplateValues.length > 0 && ( - - - - {paramTemplateValues[0]?.helperText} Click to - apply. - - - } + + )} diff --git a/apps/dashboard/src/components/contract-components/hooks.ts b/apps/dashboard/src/components/contract-components/hooks.ts index 9d3721856ee..04e3ce948f6 100644 --- a/apps/dashboard/src/components/contract-components/hooks.ts +++ b/apps/dashboard/src/components/contract-components/hooks.ts @@ -6,8 +6,12 @@ import type { Abi } from "abitype"; import { isEnsName, resolveEns } from "lib/ens"; import { useV5DashboardChain } from "lib/v5-adapter"; import { useMemo } from "react"; -import { type ThirdwebContract, getContract } from "thirdweb"; +import { type ThirdwebContract, ZERO_ADDRESS, getContract } from "thirdweb"; import { resolveContractAbi } from "thirdweb/contract"; +import { + getPredictedMintFeeManagerAddress, + getPredictedMultisigAddress, +} from "thirdweb/deploys"; import { isAddress } from "thirdweb/utils"; import { type PublishedContractWithVersion, @@ -150,6 +154,68 @@ export function usePublishedContractsQuery(address?: string) { }); } +function multisigQuery(chainId: number | undefined) { + let chainIdFinal = chainId; + if (!chainIdFinal) { + chainIdFinal = 1; + } + + return queryOptions({ + queryKey: ["multisig", chainIdFinal], + queryFn: async () => { + const chain = useV5DashboardChain(chainIdFinal); + const client = useThirdwebClient(); + + const multisig = await getPredictedMultisigAddress({ client, chain }); + + return { + multisig, + }; + }, + enabled: !!chainId, + // 24h + gcTime: 60 * 60 * 24 * 1000, + // 1h + staleTime: 60 * 60 * 1000, + // default to zero address + placeholderData: { multisig: ZERO_ADDRESS }, + retry: false, + }); +} + +function mintfeeManagerQuery(chainId: number | undefined) { + let chainIdFinal = chainId; + + if (!chainIdFinal) { + chainIdFinal = 1; + } + + return queryOptions({ + queryKey: ["mintfee-manager", chainIdFinal], + queryFn: async () => { + const chain = useV5DashboardChain(chainIdFinal); + const client = useThirdwebClient(); + + const mintfeeManager = await getPredictedMintFeeManagerAddress({ + client, + chain, + }); + + return { + mintfeeManager, + }; + }, + enabled: !!chainId, + // 24h + gcTime: 60 * 60 * 24 * 1000, + // 1h + staleTime: 60 * 60 * 1000, + // default to zero address + placeholderData: { mintfeeManager: ZERO_ADDRESS }, + retry: false, + }); +} + function ensQuery(addressOrEnsName?: string) { // if the address is `thirdweb.eth` we actually want `deployer.thirdweb.eth` here... if (addressOrEnsName === "thirdweb.eth") { @@ -208,6 +274,14 @@ export function useEns(addressOrEnsName?: string) { return useQuery(ensQuery(addressOrEnsName)); } +export function useMultisig(chainId: number | undefined) { + return useQuery(multisigQuery(chainId)); +} + +export function useMintfeeManager(chainId: number | undefined) { + return useQuery(mintfeeManagerQuery(chainId)); +} + export function useContractEvents(abi: Abi) { return abi.filter((a) => a.type === "event"); } diff --git a/apps/dashboard/src/lib/deployment/template-values.ts b/apps/dashboard/src/lib/deployment/template-values.ts index ed36f7b114c..d7ef5e4dafc 100644 --- a/apps/dashboard/src/lib/deployment/template-values.ts +++ b/apps/dashboard/src/lib/deployment/template-values.ts @@ -3,6 +3,8 @@ import type { SolidityType } from "lib/solidity-types"; interface ReplacementProps { connectedWallet?: string; chainId?: number; + mintFeeManager?: string; + multisig?: string; } interface TemplateValue { @@ -25,6 +27,26 @@ const ADDRESS_TEMPLATE_VALUES: TemplateValue[] = [ ); }, }, + { + value: "{{tw_mintfee_manager}}", + helperText: "Replaced with the address of the thirdweb mint fee manager.", + replacerFunction: (searchValue, replacers) => { + return searchValue.replaceAll( + "{{tw_mintfee_manager}}", + replacers.mintFeeManager || "", + ); + }, + }, + { + value: "{{tw_multisig}}", + helperText: "Replaced with the address of the thirdweb multisig.", + replacerFunction: (searchValue, replacers) => { + return searchValue.replaceAll( + "{{tw_multisig}}", + replacers.multisig || "", + ); + }, + }, ]; const ADDRESS_ARRAY_TEMPLATE_VALUES: TemplateValue[] = [ diff --git a/packages/thirdweb/src/contract/deployment/utils/bootstrap.ts b/packages/thirdweb/src/contract/deployment/utils/bootstrap.ts index 3f8e48a8b7d..2946b704114 100644 --- a/packages/thirdweb/src/contract/deployment/utils/bootstrap.ts +++ b/packages/thirdweb/src/contract/deployment/utils/bootstrap.ts @@ -1,3 +1,4 @@ +import { ZERO_ADDRESS } from "../../../constants/addresses.js"; import { sendAndConfirmTransaction } from "../../../transaction/actions/send-and-confirm-transaction.js"; import { type FetchDeployMetadataResult, @@ -20,6 +21,12 @@ import { getDeployedInfraContractFromMetadata, prepareInfraContractDeployTransactionFromMetadata, } from "./infra.js"; +import { getDeployedMintFeeManagerContract, getDeployedMultisigContract } from "./mintfee-manager.js"; + +export interface ReplacementValues { + mintFeeManager?: string; + multisig?: string; +} /** * @internal @@ -34,6 +41,7 @@ export async function getOrDeployInfraForPublishedContract( ): Promise<{ cloneFactoryContract: ThirdwebContract; implementationContract: ThirdwebContract; + mintfeeManagerContract?: ThirdwebContract; }> { const { chain, @@ -83,11 +91,19 @@ export async function getOrDeployInfraForPublishedContract( }; } - let [cloneFactoryContract, implementationContract] = await Promise.all([ + let [cloneFactoryContract, mintfeeManagerContract, multisig, implementationContract] = await Promise.all([ getDeployedCloneFactoryContract({ chain, client, }), + getDeployedMultisigContract({ + chain, + client, + }), + getDeployedMintFeeManagerContract({ + chain, + client, + }), getDeployedInfraContract({ chain, client, @@ -98,13 +114,23 @@ export async function getOrDeployInfraForPublishedContract( }), ]); - if (!implementationContract || !cloneFactoryContract) { + if (!implementationContract || !cloneFactoryContract || !multisig || !mintfeeManagerContract) { // deploy the infra and implementation contracts if not found cloneFactoryContract = await deployCloneFactory({ client, chain, account, }); + multisig = await deployMultisig({ + client, + chain, + account, + }); + mintfeeManagerContract = await deployMintFeeManager({ + client, + chain, + account, + }); implementationContract = await deployImplementation({ client, chain, @@ -113,9 +139,13 @@ export async function getOrDeployInfraForPublishedContract( constructorParams, publisher, version, + replacementValues: { + mintFeeManager: mintfeeManagerContract.address, + multisig: multisig.address + } }); } - return { cloneFactoryContract, implementationContract }; + return { cloneFactoryContract, mintfeeManagerContract, implementationContract }; } /** @@ -143,6 +173,53 @@ export async function deployCloneFactory(options: ClientAndChainAndAccount) { }); } +/** + * @internal + * @returns the deployed mint fee manager contract + */ +export async function deployMintFeeManager(options: ClientAndChainAndAccount) { + // create2 factory + const create2Factory = await getDeployedCreate2Factory(options); + if (!create2Factory) { + await deployCreate2Factory(options); + } + + // Multisig + const multisig = await deployMultisig(options); + + // clone factory + return getOrDeployInfraContract({ + ...options, + contractId: "MintFeeManagerCore", + constructorParams: { _owner: multisig, _modules: [], _moduleInstallData: [] }, + }); +} + +/** + * @internal + * @returns the deployed multisig contract + */ +export async function deployMultisig(options: ClientAndChainAndAccount) { + // create2 factory + const create2Factory = await getDeployedCreate2Factory(options); + if (!create2Factory) { + await deployCreate2Factory(options); + } + + return getOrDeployInfraContract({ + ...options, + contractId: "MultiSig", + constructorParams: { _signers: [TW_SIGNER_1, TW_SIGNER_2, TW_SIGNER_3], _requiredApprovals: MULTISIG_REQUIRED_APPROVALS }, + publisher: "0x6453a486d52e0EB6E79Ec4491038E2522a926936", // TODO: remove before merging + }); +} + +export const TW_SIGNER_1 = ZERO_ADDRESS; +export const TW_SIGNER_2 = ZERO_ADDRESS; +export const TW_SIGNER_3 = ZERO_ADDRESS; + +export const MULTISIG_REQUIRED_APPROVALS = 2; + /** * @internal * @returns the deployed infra contract @@ -153,6 +230,7 @@ export async function deployImplementation( constructorParams?: Record; publisher?: string; version?: string; + replacementValues?: ReplacementValues }, ) { return getOrDeployInfraContract({ @@ -161,6 +239,7 @@ export async function deployImplementation( constructorParams: options.constructorParams, publisher: options.publisher, version: options.version, + replacementValues: options.replacementValues }); } @@ -174,6 +253,7 @@ export async function getOrDeployInfraContract( constructorParams?: Record; publisher?: string; version?: string; + replacementValues?: ReplacementValues }, ) { const contractMetadata = await fetchPublishedContractMetadata({ @@ -195,6 +275,7 @@ export async function getOrDeployInfraContractFromMetadata( options: ClientAndChainAndAccount & { contractMetadata: FetchDeployMetadataResult; constructorParams?: Record; + replacementValues?: ReplacementValues }, ) { const infraContract = await getDeployedInfraContractFromMetadata(options); diff --git a/packages/thirdweb/src/contract/deployment/utils/infra.ts b/packages/thirdweb/src/contract/deployment/utils/infra.ts index 91aafa811ce..bd240ef9411 100644 --- a/packages/thirdweb/src/contract/deployment/utils/infra.ts +++ b/packages/thirdweb/src/contract/deployment/utils/infra.ts @@ -9,6 +9,7 @@ import type { Prettify } from "../../../utils/type-utils.js"; import type { ClientAndChain } from "../../../utils/types.js"; import { type ThirdwebContract, getContract } from "../../contract.js"; import { fetchPublishedContractMetadata } from "../publisher.js"; +import type { ReplacementValues } from "./bootstrap.js"; import { computeCreate2FactoryAddress } from "./create-2-factory.js"; export type InfraContractId = @@ -16,6 +17,8 @@ export type InfraContractId = | "Forwarder" | "ForwarderEOAOnly" | "TWCloneFactory" + | "MintFeeManagerCore" + | "MultiSig" | (string & {}); type GetDeployedInfraParams = Prettify< @@ -27,6 +30,26 @@ type GetDeployedInfraParams = Prettify< } >; +/** + * @internal + */ +export async function getPredictedInfraContractAddress( + options: GetDeployedInfraParams, +): Promise { + const contractMetadata = await fetchPublishedContractMetadata({ + client: options.client, + contractId: options.contractId, + publisher: options.publisher, + version: options.version, + }); + return await computeContractAddress({ + client: options.client, + chain: options.chain, + contractMetadata, + constructorParams: options.constructorParams, + }) +} + /** * @internal */ @@ -77,8 +100,26 @@ export function prepareInfraContractDeployTransactionFromMetadata(options: { contractMetadata: FetchDeployMetadataResult; constructorParams?: Record; salt?: string; + replacementValues?: ReplacementValues }) { const { client, chain } = options; + let params: Record; + if(options.contractMetadata.constructorParams) { + if(!options.constructorParams) { + Object.keys(options.contractMetadata.constructorParams).forEach(async (key, index) => { + const param = options.contractMetadata.constructorParams![key]; + + if (param?.defaultValue === "{{tw-mintfee-mmanager}}") { + params[index] = options.replacementValues?.mintFeeManager || ""; + } else if (param?.defaultValue === "{{tw-multisig}}") { + params[index] = options.replacementValues?.multisig || ""; + } else { + params[index] = ""; + } + }); + } + } + return prepareTransaction({ client, chain, @@ -89,7 +130,10 @@ export function prepareInfraContractDeployTransactionFromMetadata(options: { }), data: async () => { const infraContractInfo = - await computeDeploymentInfoFromMetadata(options); + await computeDeploymentInfoFromMetadata({ + ...options, + constructorParams: params + }); return infraContractInfo.initBytecodeWithsalt; }, }); diff --git a/packages/thirdweb/src/contract/deployment/utils/mintfee-manager.ts b/packages/thirdweb/src/contract/deployment/utils/mintfee-manager.ts new file mode 100644 index 00000000000..9ab9d1ce76a --- /dev/null +++ b/packages/thirdweb/src/contract/deployment/utils/mintfee-manager.ts @@ -0,0 +1,86 @@ +import type { ClientAndChain } from "../../../utils/types.js"; +import { TW_SIGNER_1, TW_SIGNER_2, TW_SIGNER_3, MULTISIG_REQUIRED_APPROVALS } from "./bootstrap.js"; +import { getDeployedInfraContract, getPredictedInfraContractAddress } from "./infra.js"; + +/** + * @internal + */ +export async function getDeployedMintFeeManagerContract(args: ClientAndChain) { + // check if Multisig is deployed + const multisig = await getDeployedInfraContract({ + ...args, + contractId: "MultiSig", + constructorParams: { _signers: [TW_SIGNER_1, TW_SIGNER_2, TW_SIGNER_3], _requiredApprovals: MULTISIG_REQUIRED_APPROVALS }, + publisher: "0x6453a486d52e0EB6E79Ec4491038E2522a926936", // TODO: remove before merging + }); + if (!multisig) { + return null; + } + + // check if MintFeeManager is deployed + const mintfeeManager = await getDeployedInfraContract({ + ...args, + contractId: "MintFeeManagerCore", + constructorParams: { _owner: multisig, _modules: [], _moduleInstallData: [] }, + publisher: "0x6453a486d52e0EB6E79Ec4491038E2522a926936", // TODO: remove before merging + }); + if (!mintfeeManager) { + return null; + } + return mintfeeManager; +} + +/** + * @internal + */ +export async function getDeployedMultisigContract(args: ClientAndChain) { + const multisig = await getDeployedInfraContract({ + ...args, + contractId: "MultiSig", + constructorParams: { _signers: [TW_SIGNER_1, TW_SIGNER_2, TW_SIGNER_3], _requiredApprovals: MULTISIG_REQUIRED_APPROVALS }, + publisher: "0x6453a486d52e0EB6E79Ec4491038E2522a926936", // TODO: remove before merging + }); + if (!multisig) { + return null; + } + + return multisig; +} + +/** + * @internal + */ +export async function getPredictedMintFeeManagerAddress(args: ClientAndChain) { + // compute multisig address + const multisig = await getPredictedInfraContractAddress({ + ...args, + contractId: "MultiSig", + constructorParams: { _signers: [TW_SIGNER_1, TW_SIGNER_2, TW_SIGNER_3], _requiredApprovals: MULTISIG_REQUIRED_APPROVALS }, + publisher: "0x6453a486d52e0EB6E79Ec4491038E2522a926936", // TODO: remove before merging + }); + + // compute mintfee manager address + const mintfeeManager = await getPredictedInfraContractAddress({ + ...args, + contractId: "MintFeeManagerCore", + constructorParams: { _owner: multisig, _modules: [], _moduleInstallData: [] }, + publisher: "0x6453a486d52e0EB6E79Ec4491038E2522a926936", // TODO: remove before merging + }); + + return mintfeeManager; +} + +/** + * @internal + */ +export async function getPredictedMultisigAddress(args: ClientAndChain) { + // compute multisig address + const multisig = await getPredictedInfraContractAddress({ + ...args, + contractId: "MultiSig", + constructorParams: { _signers: [TW_SIGNER_1, TW_SIGNER_2, TW_SIGNER_3], _requiredApprovals: MULTISIG_REQUIRED_APPROVALS }, + publisher: "0x6453a486d52e0EB6E79Ec4491038E2522a926936", // TODO: remove before merging + }); + + return multisig; +} diff --git a/packages/thirdweb/src/exports/deploys.ts b/packages/thirdweb/src/exports/deploys.ts index 96c8e402445..dc3f8292f10 100644 --- a/packages/thirdweb/src/exports/deploys.ts +++ b/packages/thirdweb/src/exports/deploys.ts @@ -54,3 +54,7 @@ export { type DeployPackContractOptions, deployPackContract, } from "../extensions/prebuilts/deploy-pack.js"; +export { + getPredictedMintFeeManagerAddress, + getPredictedMultisigAddress +} from "../contract/deployment/utils/mintfee-manager.js"; diff --git a/packages/thirdweb/src/extensions/prebuilts/deploy-published.ts b/packages/thirdweb/src/extensions/prebuilts/deploy-published.ts index d6df91acef1..36dd7b27d7c 100644 --- a/packages/thirdweb/src/extensions/prebuilts/deploy-published.ts +++ b/packages/thirdweb/src/extensions/prebuilts/deploy-published.ts @@ -3,7 +3,7 @@ import type { Chain } from "../../chains/types.js"; import type { ThirdwebClient } from "../../client/client.js"; import { type ThirdwebContract, getContract } from "../../contract/contract.js"; import { fetchPublishedContractMetadata } from "../../contract/deployment/publisher.js"; -import { getOrDeployInfraContractFromMetadata } from "../../contract/deployment/utils/bootstrap.js"; +import { deployMintFeeManager, getOrDeployInfraContractFromMetadata } from "../../contract/deployment/utils/bootstrap.js"; import { sendAndConfirmTransaction } from "../../transaction/actions/send-and-confirm-transaction.js"; import { simulateTransaction } from "../../transaction/actions/simulate.js"; import { prepareContractCall } from "../../transaction/prepare-contract-call.js"; @@ -319,9 +319,15 @@ async function getInitializeTransaction(options: { (i) => i.name === "moduleInstallData" || i.name === "_moduleInstallData", ); if (hasModules) { + await deployMintFeeManager({client, chain, account}); + const moduleAddresses: Hex[] = []; const moduleInstallData: Hex[] = []; for (const module of modules) { + // deploy mint fee manager and multisig if not already deployed + + module.deployMetadata.abi + // deploy the module if not already deployed const contract = await getOrDeployInfraContractFromMetadata({ client,