From 88c213b6d5c34f339f6f9fe6bedcc69b1a3f249f Mon Sep 17 00:00:00 2001 From: kumaryash90 Date: Mon, 24 Mar 2025 17:21:13 +0000 Subject: [PATCH] Handle default extensions (#6497) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ## PR-Codex overview This PR focuses on enhancing the deployment and transaction management of dynamic contracts within the `thirdweb` framework, adding support for extensions and improving the handling of contract metadata. ### Detailed summary - Improved return formatting in `bootstrap.ts`. - Updated `platformFeeBps` handling in `custom-contract.tsx`. - Added tests for dynamic contract transactions in `get-required-transactions.test.ts`. - Enhanced logic for deploying extensions in `deploy-published.ts`. - Introduced new functions for managing dynamic contract extensions in `get-required-transactions.ts`. - Updated `deploy-marketplace.ts` to include dynamic extensions in marketplace contracts. > ✨ Ask PR-Codex anything about this PR by commenting with `/codex {your question}` --- .../contract-deploy-form/custom-contract.tsx | 4 +- .../deployment/deploy-dynamic.test.ts | 52 ++++++ .../contract/deployment/utils/bootstrap.ts | 6 +- .../prebuilts/deploy-marketplace.ts | 159 +++++++++--------- .../extensions/prebuilts/deploy-published.ts | 24 +++ .../get-required-transactions.test.ts | 14 ++ .../prebuilts/get-required-transactions.ts | 138 +++++++++++++++ 7 files changed, 312 insertions(+), 85 deletions(-) create mode 100644 packages/thirdweb/src/contract/deployment/deploy-dynamic.test.ts 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 b826dd5a8d4..a0c5f22fa26 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 @@ -473,9 +473,7 @@ export const CustomContractForm: React.FC = ({ name: params.contractMetadata?.name || "", contractURI: _contractURI, defaultAdmin: params.deployParams._defaultAdmin as string, - platformFeeBps: hasInbuiltDefaultFeeConfig - ? DEFAULT_FEE_BPS_NEW - : DEFAULT_FEE_BPS, + platformFeeBps: DEFAULT_FEE_BPS_NEW, platformFeeRecipient: DEFAULT_FEE_RECIPIENT, trustedForwarders: params.deployParams._trustedForwarders ? JSON.parse(params.deployParams._trustedForwarders as string) diff --git a/packages/thirdweb/src/contract/deployment/deploy-dynamic.test.ts b/packages/thirdweb/src/contract/deployment/deploy-dynamic.test.ts new file mode 100644 index 00000000000..b72747a4b91 --- /dev/null +++ b/packages/thirdweb/src/contract/deployment/deploy-dynamic.test.ts @@ -0,0 +1,52 @@ +import { readContract } from "src/transaction/read-contract.js"; +import { resolveMethod } from "src/transaction/resolve-method.js"; +import { describe, expect, it } from "vitest"; +import { ANVIL_CHAIN } from "../../../test/src/chains.js"; +import { TEST_CLIENT } from "../../../test/src/test-clients.js"; +import { TEST_ACCOUNT_A } from "../../../test/src/test-wallets.js"; +import { deployPublishedContract } from "../../extensions/prebuilts/deploy-published.js"; +import { getContract } from "../contract.js"; +import { deployCloneFactory } from "./utils/bootstrap.js"; + +describe.runIf(process.env.TW_SECRET_KEY)("deploy dynamic", () => { + it.sequential("should deploy dynamic contract with extensions", async () => { + await deployCloneFactory({ + chain: ANVIL_CHAIN, + client: TEST_CLIENT, + account: TEST_ACCOUNT_A, + }); + + const deployed = await deployPublishedContract({ + chain: ANVIL_CHAIN, + client: TEST_CLIENT, + account: TEST_ACCOUNT_A, + contractId: "EvolvingNFT", + contractParams: { + name: "Evolving nft", + symbol: "ENFT", + defaultAdmin: TEST_ACCOUNT_A.address, + royaltyBps: 0n, + royaltyRecipient: TEST_ACCOUNT_A.address, + saleRecipient: TEST_ACCOUNT_A.address, + trustedForwarders: [], + contractURI: "", + }, + }); + + expect(deployed).toBeDefined(); + + const contract = getContract({ + client: TEST_CLIENT, + address: deployed, + chain: ANVIL_CHAIN, + }); + + const extensions = await readContract({ + contract, + method: resolveMethod("getAllExtensions"), + params: [], + }); + + expect(extensions.length).toEqual(3); + }); +}); diff --git a/packages/thirdweb/src/contract/deployment/utils/bootstrap.ts b/packages/thirdweb/src/contract/deployment/utils/bootstrap.ts index a0199c5fe37..5226ce9df49 100644 --- a/packages/thirdweb/src/contract/deployment/utils/bootstrap.ts +++ b/packages/thirdweb/src/contract/deployment/utils/bootstrap.ts @@ -142,7 +142,11 @@ export async function getOrDeployInfraForPublishedContract( version, }); } - return { cloneFactoryContract, implementationContract }; + + return { + cloneFactoryContract, + implementationContract, + }; } /** diff --git a/packages/thirdweb/src/extensions/prebuilts/deploy-marketplace.ts b/packages/thirdweb/src/extensions/prebuilts/deploy-marketplace.ts index 9717ed70fc3..f920556824a 100644 --- a/packages/thirdweb/src/extensions/prebuilts/deploy-marketplace.ts +++ b/packages/thirdweb/src/extensions/prebuilts/deploy-marketplace.ts @@ -1,10 +1,4 @@ -import type { - Abi, - AbiFunction, - AbiParametersToPrimitiveTypes, - Address, -} from "abitype"; -import { toFunctionSelector, toFunctionSignature } from "viem"; +import type { AbiParametersToPrimitiveTypes, Address } from "abitype"; import type { ThirdwebClient } from "../../client/client.js"; import { resolveContractAbi } from "../../contract/actions/resolve-abi.js"; import type { ThirdwebContract } from "../../contract/contract.js"; @@ -19,6 +13,19 @@ import { getRoyaltyEngineV1ByChainId } from "../../utils/royalty-engine.js"; import type { Prettify } from "../../utils/type-utils.js"; import type { ClientAndChainAndAccount } from "../../utils/types.js"; import { initialize as initMarketplace } from "./__generated__/Marketplace/write/initialize.js"; +import { generateExtensionFunctionsFromAbi } from "./get-required-transactions.js"; + +type Extension = { + metadata: { + name: string; + metadataURI: string; + implementation: `0x${string}`; + }; + functions: { + functionSelector: string; + functionSignature: string; + }[]; +}; export type MarketplaceContractParams = { name: string; @@ -74,41 +81,72 @@ export async function deployMarketplaceContract( account, contractId: "WETH9", }); - const direct = await getOrDeployInfraForPublishedContract({ - chain, - client, - account, - contractId: "DirectListingsLogic", - constructorParams: { _nativeTokenWrapper: WETH.address }, - }); - const english = await getOrDeployInfraForPublishedContract({ - chain, - client, - account, - contractId: "EnglishAuctionsLogic", - constructorParams: { _nativeTokenWrapper: WETH.address }, - }); + let extensions: Extension[] = []; - const offers = await getOrDeployInfraForPublishedContract({ - chain, - client, - account, - contractId: "OffersLogic", - }); + if (options.version !== "6.0.0") { + const direct = await getOrDeployInfraForPublishedContract({ + chain, + client, + account, + contractId: "DirectListingsLogic", + constructorParams: { _nativeTokenWrapper: WETH.address }, + }); - const [directFunctions, englishFunctions, offersFunctions] = - await Promise.all([ - resolveContractAbi(direct.implementationContract).then( - generateExtensionFunctionsFromAbi, - ), - resolveContractAbi(english.implementationContract).then( - generateExtensionFunctionsFromAbi, - ), - resolveContractAbi(offers.implementationContract).then( - generateExtensionFunctionsFromAbi, - ), - ]); + const english = await getOrDeployInfraForPublishedContract({ + chain, + client, + account, + contractId: "EnglishAuctionsLogic", + constructorParams: { _nativeTokenWrapper: WETH.address }, + }); + + const offers = await getOrDeployInfraForPublishedContract({ + chain, + client, + account, + contractId: "OffersLogic", + }); + + const [directFunctions, englishFunctions, offersFunctions] = + await Promise.all([ + resolveContractAbi(direct.implementationContract).then( + generateExtensionFunctionsFromAbi, + ), + resolveContractAbi(english.implementationContract).then( + generateExtensionFunctionsFromAbi, + ), + resolveContractAbi(offers.implementationContract).then( + generateExtensionFunctionsFromAbi, + ), + ]); + extensions = [ + { + metadata: { + name: "Direct Listings", + metadataURI: "", + implementation: direct.implementationContract.address, + }, + functions: directFunctions, + }, + { + metadata: { + name: "English Auctions", + metadataURI: "", + implementation: english.implementationContract.address, + }, + functions: englishFunctions, + }, + { + metadata: { + name: "Offers", + metadataURI: "", + implementation: offers.implementationContract.address, + }, + functions: offersFunctions, + }, + ]; + } const { cloneFactoryContract, implementationContract } = await getOrDeployInfraForPublishedContract({ @@ -118,32 +156,7 @@ export async function deployMarketplaceContract( contractId: "MarketplaceV3", constructorParams: { _marketplaceV3Params: { - extensions: [ - { - metadata: { - name: "Direct Listings", - metadataURI: "", - implementation: direct.implementationContract.address, - }, - functions: directFunctions, - }, - { - metadata: { - name: "English Auctions", - metadataURI: "", - implementation: english.implementationContract.address, - }, - functions: englishFunctions, - }, - { - metadata: { - name: "Offers", - metadataURI: "", - implementation: offers.implementationContract.address, - }, - functions: offersFunctions, - }, - ], + extensions, royaltyEngineAddress: getRoyaltyEngineV1ByChainId(chain.id), nativeTokenWrapper: WETH.address, } as MarketplaceConstructorParams[number], @@ -199,22 +212,6 @@ async function getInitializeTransaction(options: { }); } -// helperFns - -function generateExtensionFunctionsFromAbi(abi: Abi): Array<{ - functionSelector: string; - functionSignature: string; -}> { - const functions = abi.filter( - (item) => item.type === "function" && !item.name.startsWith("_"), - ) as AbiFunction[]; - - return functions.map((fn) => ({ - functionSelector: toFunctionSelector(fn), - functionSignature: toFunctionSignature(fn), - })); -} - // let's just ... put this down here type MarketplaceConstructorParams = AbiParametersToPrimitiveTypes< [ diff --git a/packages/thirdweb/src/extensions/prebuilts/deploy-published.ts b/packages/thirdweb/src/extensions/prebuilts/deploy-published.ts index 4c566380c56..752dd48d2b7 100644 --- a/packages/thirdweb/src/extensions/prebuilts/deploy-published.ts +++ b/packages/thirdweb/src/extensions/prebuilts/deploy-published.ts @@ -204,6 +204,29 @@ export async function deployContractfromDeployMetadata( import("../../contract/deployment/deploy-via-autofactory.js"), import("../../contract/deployment/utils/bootstrap.js"), ]); + + if ( + deployMetadata.routerType === "dynamic" && + deployMetadata.defaultExtensions + ) { + for (const e of deployMetadata.defaultExtensions) { + await getOrDeployInfraForPublishedContract({ + chain, + client, + account, + contractId: e.extensionName, + version: e.extensionVersion || "latest", + publisher: e.publisherAddress, + constructorParams: + await getAllDefaultConstructorParamsForImplementation({ + chain, + client, + contractId: e.extensionName, + }), + }); + } + } + const { cloneFactoryContract, implementationContract } = await getOrDeployInfraForPublishedContract({ chain, @@ -216,6 +239,7 @@ export async function deployContractfromDeployMetadata( chain, client, contractId: deployMetadata.name, + defaultExtensions: deployMetadata.defaultExtensions, })), publisher: deployMetadata.publisher, version: deployMetadata.version, diff --git a/packages/thirdweb/src/extensions/prebuilts/get-required-transactions.test.ts b/packages/thirdweb/src/extensions/prebuilts/get-required-transactions.test.ts index dac5601c530..faf27a8cfe6 100644 --- a/packages/thirdweb/src/extensions/prebuilts/get-required-transactions.test.ts +++ b/packages/thirdweb/src/extensions/prebuilts/get-required-transactions.test.ts @@ -64,6 +64,20 @@ describe.runIf(process.env.TW_SECRET_KEY)( expect(results.length).toBe(7); }); + it("should count transactions for a dynamic contract", async () => { + const deployMetadata = await fetchPublishedContractMetadata({ + client: TEST_CLIENT, + contractId: "EvolvingNFT", + }); + const results = await getRequiredTransactions({ + client: TEST_CLIENT, + chain: CLEAN_ANVIL_CHAIN, + deployMetadata, + }); + + expect(results.length).toBe(8); + }); + it("should return default constructor params for zksync chains", async () => { const params = await getAllDefaultConstructorParamsForImplementation({ chain: defineChain(300), diff --git a/packages/thirdweb/src/extensions/prebuilts/get-required-transactions.ts b/packages/thirdweb/src/extensions/prebuilts/get-required-transactions.ts index a7aa74b80dc..837dc206be2 100644 --- a/packages/thirdweb/src/extensions/prebuilts/get-required-transactions.ts +++ b/packages/thirdweb/src/extensions/prebuilts/get-required-transactions.ts @@ -1,5 +1,8 @@ +import type { Abi, AbiFunction } from "abitype"; +import { toFunctionSelector, toFunctionSignature } from "viem"; import type { Chain } from "../../chains/types.js"; import type { ThirdwebClient } from "../../client/client.js"; +import { resolveContractAbi } from "../../contract/actions/resolve-abi.js"; import { getDeployedCreate2Factory } from "../../contract/deployment/utils/create-2-factory.js"; import { getDeployedInfraContract } from "../../contract/deployment/utils/infra.js"; import { getDeployedInfraContractFromMetadata } from "../../contract/deployment/utils/infra.js"; @@ -19,6 +22,15 @@ type DeployTransactionType = | "extension" | "proxy"; +/** + * @internal + */ +type DynamicContractExtension = { + extensionName: string; + extensionVersion: string; + publisherAddress: string; +}; + /** * @internal */ @@ -131,6 +143,10 @@ async function getTransactionsForImplementation(options: { return getTransactionsForMaketplaceV3(options); } + if (deployMetadata.routerType === "dynamic") { + return getTransactionsForDynamicContract(options); + } + const constructorParams = implementationConstructorParams ?? (await getAllDefaultConstructorParamsForImplementation({ @@ -211,6 +227,54 @@ async function getTransactionsForMaketplaceV3(options: { return transactions; } +async function getTransactionsForDynamicContract(options: { + chain: Chain; + client: ThirdwebClient; + deployMetadata: FetchDeployMetadataResult; +}): Promise { + const { chain, client } = options; + const WETHAdress = await computePublishedContractAddress({ + chain, + client, + contractId: "WETH9", + }); + const wethTx = await getDeployedInfraContract({ + chain, + client, + contractId: "WETH9", + }).then((c) => + c ? null : ({ type: "infra", contractId: "WETH9" } as const), + ); + const extensions: (DeployTransactionResult | null)[] = options.deployMetadata + .defaultExtensions + ? await Promise.all( + options.deployMetadata.defaultExtensions.map((e) => { + return getDeployedInfraContract({ + chain, + client, + contractId: e.extensionName, + publisher: e.publisherAddress, + version: e.extensionVersion || "latest", + constructorParams: { _nativeTokenWrapper: WETHAdress }, + }).then((c) => + c + ? null + : ({ type: "extension", contractId: e.extensionName } as const), + ); + }), + ) + : []; + // hacky assumption: if we need to deploy any of the extensions, we also need to deploy the implementation + const transactions = [...extensions, wethTx].filter((e) => e !== null); + if (transactions.length) { + transactions.push({ + type: "implementation", + contractId: options.deployMetadata.name, + }); + } + return transactions; +} + /** * Gets the default constructor parameters required for contract implementation deployment * @param args - The arguments object @@ -226,6 +290,7 @@ export async function getAllDefaultConstructorParamsForImplementation(args: { chain: Chain; client: ThirdwebClient; contractId: string; + defaultExtensions?: DynamicContractExtension[]; }) { const { chain, client } = args; const isZkSync = await isZkSyncChain(chain); @@ -251,8 +316,81 @@ export async function getAllDefaultConstructorParamsForImplementation(args: { contractId: "WETH9", }), ]); + + const defaultExtensionInput = args.defaultExtensions + ? await generateExtensionInput({ + defaultExtensions: args.defaultExtensions, + chain, + client, + forwarder, + nativeTokenWrapper: weth, + }) + : []; + return { trustedForwarder: forwarder, nativeTokenWrapper: weth, + extensions: defaultExtensionInput, }; } + +async function generateExtensionInput(args: { + defaultExtensions: DynamicContractExtension[]; + chain: Chain; + client: ThirdwebClient; + forwarder: string; + nativeTokenWrapper: string; +}) { + const { defaultExtensions, chain, client, forwarder, nativeTokenWrapper } = + args; + + const deployedExtensions = await Promise.all( + defaultExtensions.map((e) => + getDeployedInfraContract({ + chain, + client, + contractId: e.extensionName, + publisher: e.publisherAddress, + version: e.extensionVersion || "latest", + constructorParams: { forwarder, nativeTokenWrapper }, + }).then((c) => ({ + name: e.extensionName, + metadataURI: "", + implementation: c, + })), + ), + ); + + const extensionInput = await Promise.all( + deployedExtensions.map(async (e) => { + if (!e.implementation) { + throw new Error("Extension not deployed"); + } + return resolveContractAbi(e.implementation) + .then(generateExtensionFunctionsFromAbi) + .then((c) => ({ + metadata: { + ...e, + implementation: e.implementation?.address, + }, + functions: c, + })); + }), + ); + + return extensionInput; +} + +export function generateExtensionFunctionsFromAbi(abi: Abi): Array<{ + functionSelector: string; + functionSignature: string; +}> { + const functions = abi.filter( + (item) => item.type === "function" && !item.name.startsWith("_"), + ) as AbiFunction[]; + + return functions.map((fn) => ({ + functionSelector: toFunctionSelector(fn), + functionSignature: toFunctionSignature(fn), + })); +}