diff --git a/packages/thirdweb/src/contract/deployment/deploy-from-uri.ts b/packages/thirdweb/src/contract/deployment/deploy-from-uri.ts index a9d415c8e5f..c1de5393d42 100644 --- a/packages/thirdweb/src/contract/deployment/deploy-from-uri.ts +++ b/packages/thirdweb/src/contract/deployment/deploy-from-uri.ts @@ -41,8 +41,7 @@ export async function prepareDeployTransactionFromUri( const chainId = options.chain.id; const isNetworkEnabled = !!( extendedMetadata?.networksForDeployment?.networksEnabled?.includes( - // TODO: align with chainId being bigint - Number(chainId), + chainId, ) || extendedMetadata?.networksForDeployment?.allNetworks ); @@ -76,12 +75,10 @@ export async function prepareDeployTransactionFromUri( throw new Error(`Contract bytecode is invalid.\n\n${bytecode}`); } - const constructorAbi = compilerMetadata.abi.find( - (abi) => abi.type === "constructor", - ) as AbiConstructor; - if (!constructorAbi) { - throw new Error("No constructor found in the contract ABI"); - } + const constructorAbi = + (compilerMetadata.abi.find( + (abi) => abi.type === "constructor", + ) as AbiConstructor) || []; return prepareDirectDeployTransaction({ bytecode, diff --git a/packages/thirdweb/src/contract/deployment/deploy-via-autofactory.ts b/packages/thirdweb/src/contract/deployment/deploy-via-autofactory.ts new file mode 100644 index 00000000000..7ee703acb01 --- /dev/null +++ b/packages/thirdweb/src/contract/deployment/deploy-via-autofactory.ts @@ -0,0 +1,14 @@ +import type { SharedDeployOptions } from "./types.js"; +import type { FullPublishMetadata } from "./utils/deploy-metadata.js"; + +/** + * @internal + */ +export async function prepareDeployTransactionViaAutoFactory( + args: SharedDeployOptions & { + extendedMetadata: FullPublishMetadata; + }, +) { + // TODO + console.log(args); +} diff --git a/packages/thirdweb/src/contract/deployment/deploy-via-clone-factory.ts b/packages/thirdweb/src/contract/deployment/deploy-via-clone-factory.ts new file mode 100644 index 00000000000..7df5bc8c606 --- /dev/null +++ b/packages/thirdweb/src/contract/deployment/deploy-via-clone-factory.ts @@ -0,0 +1,86 @@ +import type { Abi } from "abitype"; +import type { SharedDeployOptions } from "./types.js"; +import { getContract } from "../contract.js"; +import { prepareContractCall } from "../../transaction/prepare-contract-call.js"; +import { encode } from "../../transaction/actions/encode.js"; +import { keccakId } from "../../utils/any-evm/keccak-id.js"; +import { getRpcClient } from "../../rpc/rpc.js"; +import { eth_blockNumber } from "../../rpc/actions/eth_blockNumber.js"; +import { toHex } from "../../utils/encoding/hex.js"; + +/** + * Prepares a deploy transaction via a proxy factory. + * @param args - The arguments for deploying the contract. + * @example + * ```ts + * import { prepareDeployTransactionViaCloneFactory } from "thirdweb/contract"; + * import { ethereum } from "thirdweb/chains"; + * + * const tx = await prepareDeployTransactionViaCloneFactory({ + * client, + * chain: ethereum, + * factoryAddress: "0x...", + * implementationAddress: "0x...", + * implementationAbi: abi, + * initializerFunction: "initialize", + * initializerArgs: [123, "hello"], + * }); + * ``` + * @returns A prepared deployment transaction ready to be sent. + */ +export async function prepareDeployTransactionViaCloneFactory( + args: SharedDeployOptions & { + factoryAddress: string; + implementationAddress: string; + implementationAbi: Abi; + initializerFunction: string; + initializerArgs: unknown[]; + saltForProxyDeploy?: string; + }, +) { + const { + client, + chain, + factoryAddress, + implementationAddress, + implementationAbi, + initializerFunction, + initializerArgs, + saltForProxyDeploy, + } = args; + const factory = getContract({ + client, + chain, + address: factoryAddress, + }); + return prepareContractCall({ + contract: factory, + method: + "function deployProxyByImplementation(address _implementation, bytes memory _data, bytes32 _salt) returns (address deployedProxy)", + params: async () => { + const implementation = getContract({ + client, + chain, + address: implementationAddress, + abi: implementationAbi, + }); + const initializerTransaction = prepareContractCall({ + contract: implementation, + method: initializerFunction, + params: initializerArgs, + }); + const rpcRequest = getRpcClient({ + client, + chain, + }); + const blockNumber = await eth_blockNumber(rpcRequest); + const salt = saltForProxyDeploy + ? keccakId(saltForProxyDeploy) + : toHex(blockNumber, { + size: 32, + }); + const encoded = await encode(initializerTransaction); + return [implementationAddress, encoded, salt] as const; + }, + }); +} diff --git a/packages/thirdweb/src/contract/deployment/utils/deploy-metadata.ts b/packages/thirdweb/src/contract/deployment/utils/deploy-metadata.ts index 3ee5cc2a739..a6579ba439b 100644 --- a/packages/thirdweb/src/contract/deployment/utils/deploy-metadata.ts +++ b/packages/thirdweb/src/contract/deployment/utils/deploy-metadata.ts @@ -25,31 +25,33 @@ export async function fetchDeployMetadata( ): Promise { const [compilerMetadata, extendedMetadata] = await Promise.all([ fetchPreDeployMetadata(options), - fetchExtendedReleaseMetadata(options).catch(() => undefined), + fetchPublishedMetadata(options).catch(() => undefined), ]); return { compilerMetadata, extendedMetadata }; } // helpers -async function fetchExtendedReleaseMetadata( +/** + * Fetches the published metadata. + * @param options - The options for fetching the published metadata. + * @internal + */ +async function fetchPublishedMetadata( options: FetchDeployMetadataOptions, ): Promise { - return JSON.parse( - await ( - await download({ - uri: options.uri, - client: options.client, - }) - ).text(), - ); + return download({ + uri: options.uri, + client: options.client, + }).then((r) => r.json()); } async function fetchPreDeployMetadata( options: FetchDeployMetadataOptions, ): Promise { - const rawMeta = JSON.parse( - await (await download({ uri: options.uri, client: options.client })).text(), - ) as RawPredeployMetadata; + const rawMeta: RawPredeployMetadata = await download({ + uri: options.uri, + client: options.client, + }).then((r) => r.json()); const [deployBytecode, parsedMeta] = await Promise.all([ download({ uri: rawMeta.bytecodeUri, client: options.client }).then( (res) => res.text() as Promise, @@ -68,7 +70,7 @@ const CONTRACT_METADATA_TIMEOUT_SEC = 2 * 1000; async function fetchContractMetadata( options: FetchDeployMetadataOptions, -): Promise { +): Promise { // short timeout to avoid hanging on unpinned contract metadata CIDs const metadata = await ( await download({ @@ -84,7 +86,7 @@ async function fetchContractMetadata( return formatCompilerMetadata(metadata); } -function formatCompilerMetadata(metadata: any): PublishedMetadata { +function formatCompilerMetadata(metadata: any): ParsedPredeployMetadata { const abi = metadata.output.abi; const compilationTarget = metadata.settings.compilationTarget; const targets = Object.keys(compilationTarget); @@ -120,7 +122,7 @@ type RawPredeployMetadata = { [key: string]: any; }; -type PublishedMetadata = { +type ParsedPredeployMetadata = { name: string; abi: Abi; metadata: Record; @@ -134,13 +136,13 @@ type PublishedMetadata = { isPartialAbi?: boolean; }; -type PreDeployMetadata = Prettify< - PublishedMetadata & { +export type PreDeployMetadata = Prettify< + ParsedPredeployMetadata & { bytecode: Hex; } >; -type FullPublishMetadata = { +export type FullPublishMetadata = { name: string; version: string; metadataUri: string; diff --git a/packages/thirdweb/src/contract/deployment/utils/fetch-published-contract.ts b/packages/thirdweb/src/contract/deployment/utils/fetch-published-contract.ts new file mode 100644 index 00000000000..5a5447143a7 --- /dev/null +++ b/packages/thirdweb/src/contract/deployment/utils/fetch-published-contract.ts @@ -0,0 +1,81 @@ +import { polygon } from "../../../chains/chain-definitions/polygon.js"; +import type { ThirdwebClient } from "../../../client/client.js"; +import { readContract } from "../../../transaction/read-contract.js"; +import { getContract } from "../../contract.js"; + +const ContractPublisherAddress = "0xf5b896Ddb5146D5dA77efF4efBb3Eae36E300808"; // Polygon only +export const THIRDWEB_DEPLOYER = "0xdd99b75f095d0c4d5112aCe938e4e6ed962fb024"; + +/** + * @internal + */ +export async function fetchPublishedContract(args: { + client: ThirdwebClient; + contractId: string; + publisherAddress?: string; +}) { + const { client, publisherAddress, contractId } = args; + const contractPublisher = getContract({ + client, + chain: polygon, + address: ContractPublisherAddress, + }); + // TODO support mutliple contract versions + return readContract({ + contract: contractPublisher, + method: GET_PUBLISHED_CONTRACT_ABI, + params: [publisherAddress || THIRDWEB_DEPLOYER, contractId], + }); +} + +const GET_PUBLISHED_CONTRACT_ABI = { + inputs: [ + { + internalType: "address", + name: "_publisher", + type: "address", + }, + { + internalType: "string", + name: "_contractId", + type: "string", + }, + ], + name: "getPublishedContract", + outputs: [ + { + components: [ + { + internalType: "string", + name: "contractId", + type: "string", + }, + { + internalType: "uint256", + name: "publishTimestamp", + type: "uint256", + }, + { + internalType: "string", + name: "publishMetadataUri", + type: "string", + }, + { + internalType: "bytes32", + name: "bytecodeHash", + type: "bytes32", + }, + { + internalType: "address", + name: "implementation", + type: "address", + }, + ], + internalType: "struct IContractPublisher.CustomContractInstance", + name: "published", + type: "tuple", + }, + ], + stateMutability: "view", + type: "function", +} as const; diff --git a/packages/thirdweb/src/contract/deployment/utils/predict-published-contract-address.ts b/packages/thirdweb/src/contract/deployment/utils/predict-published-contract-address.ts new file mode 100644 index 00000000000..d456c764ebb --- /dev/null +++ b/packages/thirdweb/src/contract/deployment/utils/predict-published-contract-address.ts @@ -0,0 +1,71 @@ +import type { AbiConstructor } from "abitype"; +import type { ThirdwebClient } from "../../../client/client.js"; +import { getInitBytecodeWithSalt } from "../../../utils/any-evm/get-init-bytecode-with-salt.js"; +import { fetchDeployMetadata } from "./deploy-metadata.js"; +import { fetchPublishedContract } from "./fetch-published-contract.js"; +import { encodeAbiParameters } from "viem"; +import { computeDeploymentAddress } from "../../../utils/any-evm/compute-deployment-address.js"; +import { getCreate2FactoryAddress } from "../../../utils/any-evm/create-2-factory.js"; +import type { Chain } from "../../../chains/types.js"; + +/** + * Predicts the implementation address of any published contract + * @param args - The arguments for predicting the address of a published contract. + * @param args.client - The Thirdweb client. + * @param args.chain - The chain to predict the address on. + * @param args.contractId - The ID of the contract to predict the address of. + * @param args.constructorParams - The parameters for the contract constructor. + * @example + * ```ts + * import { predictPublishedContractAddress } from "thirdweb/contract"; + * + * const address = await predictPublishedContractAddress({ + * client, + * chain, + * contractId, + * constructorParams, + * }); + * ``` + * @returns A promise that resolves to the predicted address of the contract. + */ +export async function predictPublishedContractAddress(args: { + client: ThirdwebClient; + chain: Chain; + contractId: string; + constructorParams: unknown[]; // TODO automate contract params from known inputs +}): Promise { + const { client, chain, contractId, constructorParams } = args; + const contractModel = await fetchPublishedContract({ + client, + contractId, + }); + const [{ compilerMetadata }, create2FactoryAddress] = await Promise.all([ + fetchDeployMetadata({ + client, + uri: contractModel.publishMetadataUri, + }), + getCreate2FactoryAddress({ + client, + chain, + }), + ]); + const bytecode = compilerMetadata.bytecode; + const constructorAbi = + (compilerMetadata.abi.find( + (abi) => abi.type === "constructor", + ) as AbiConstructor) || []; + const encodedArgs = encodeAbiParameters( + constructorAbi.inputs, + constructorParams, + ); + const initBytecodeWithsalt = getInitBytecodeWithSalt({ + bytecode, + encodedArgs, + }); + return computeDeploymentAddress({ + bytecode, + encodedArgs, + create2FactoryAddress, + salt: initBytecodeWithsalt, + }); +}