diff --git a/apps/dashboard/next-env.d.ts b/apps/dashboard/next-env.d.ts index 3cd7048ed94..725dd6f2451 100644 --- a/apps/dashboard/next-env.d.ts +++ b/apps/dashboard/next-env.d.ts @@ -3,4 +3,4 @@ /// // NOTE: This file should not be edited -// see https://nextjs.org/docs/app/api-reference/config/typescript for more information. +// see https://nextjs.org/docs/app/building-your-application/configuring/typescript for more information. 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..2281ed405a4 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: "Cross Chain", + href: `${layoutPrefix}/cross-chain`, + hide: !data.metadata.isModularCore, + exactMatch: true, + }, { label: "Code Snippets", href: `${layoutPrefix}/code`, diff --git a/apps/dashboard/src/app/(dashboard)/(chain)/[chain_id]/[contractAddress]/cross-chain/data-table.tsx b/apps/dashboard/src/app/(dashboard)/(chain)/[chain_id]/[contractAddress]/cross-chain/data-table.tsx new file mode 100644 index 00000000000..ceddff9897a --- /dev/null +++ b/apps/dashboard/src/app/(dashboard)/(chain)/[chain_id]/[contractAddress]/cross-chain/data-table.tsx @@ -0,0 +1,456 @@ +"use client"; + +import { Button } from "@/components/ui/button"; +import { + Form, + FormControl, + FormField, + FormItem, + FormMessage, +} from "@/components/ui/form"; +import { Input } from "@/components/ui/input"; +import { + Table, + TableBody, + TableCell, + TableContainer, + TableHead, + TableHeader, + TableRow, +} from "@/components/ui/table"; +import { ToolTipLabel } from "@/components/ui/tooltip"; +import { getThirdwebClient } from "@/constants/thirdweb.server"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { useMutation } from "@tanstack/react-query"; +import { + type ColumnDef, + flexRender, + getCoreRowModel, + useReactTable, +} from "@tanstack/react-table"; +import { verifyContract } from "app/(dashboard)/(chain)/[chain_id]/[contractAddress]/sources/ContractSourcesPage"; +import { + type DeployModalStep, + DeployStatusModal, + useDeployStatusModal, +} from "components/contract-components/contract-deploy-form/deploy-context-modal"; +import { + getModuleInstallParams, + showPrimarySaleFiedset, + showRoyaltyFieldset, + showSuperchainBridgeFieldset, +} from "components/contract-components/contract-deploy-form/modular-contract-default-modules-fieldset"; +import { useTxNotifications } from "hooks/useTxNotifications"; +import Link from "next/link"; +import { useForm } from "react-hook-form"; +import { + ZERO_ADDRESS, + defineChain, + eth_getTransactionCount, + getContract, + getRpcClient, + prepareContractCall, + readContract, + sendAndConfirmTransaction, + sendTransaction, + waitForReceipt, +} from "thirdweb"; +import type { + FetchDeployMetadataResult, + ThirdwebContract, +} from "thirdweb/contract"; +import { deployContractfromDeployMetadata } from "thirdweb/deploys"; +import { installPublishedModule } from "thirdweb/modules"; +import { useActiveAccount, useSwitchActiveWalletChain } from "thirdweb/react"; +import { + type AbiFunction, + concatHex, + encodeAbiParameters, + padHex, +} from "thirdweb/utils"; +import { z } from "zod"; + +type CrossChain = { + id: number; + network: string; + chainId: number; + status: "DEPLOYED" | "NOT_DEPLOYED"; +}; + +type ChainId = "84532" | "11155420" | "919" | "111557560" | "999999999"; + +const formSchema = z.object({ + amounts: z.object({ + "84532": z.string(), + "11155420": z.string(), + "919": z.string(), + "111557560": z.string(), + "999999999": z.string(), + }), +}); +type FormSchema = z.output; + +const positiveIntegerRegex = /^[0-9]\d*$/; +const superchainBridgeAddress = "0x4200000000000000000000000000000000000028"; + +export function DataTable({ + data, + coreMetadata, + coreContract, + modulesMetadata, + initializeData, +}: { + data: CrossChain[]; + coreMetadata: FetchDeployMetadataResult; + coreContract: ThirdwebContract; + modulesMetadata: FetchDeployMetadataResult[]; + initializeData?: `0x${string}`; +}) { + const activeAccount = useActiveAccount(); + const switchChain = useSwitchActiveWalletChain(); + const deployStatusModal = useDeployStatusModal(); + const { onError } = useTxNotifications( + "Successfully deployed contract", + "Failed to deploy contract", + ); + + const form = useForm({ + resolver: zodResolver(formSchema), + values: { + amounts: { + "84532": "", // Base + "11155420": "", // OP testnet + "919": "", // Mode Network + "111557560": "", // Cyber + "999999999": "", // Zora + }, + }, + }); + + const crossChainTransfer = async (chainId: ChainId) => { + if (!activeAccount) { + throw new Error("Account not connected"); + } + const amount = form.getValues().amounts[chainId]; + if (!positiveIntegerRegex.test(amount)) { + form.setError(`amounts.${chainId}`, { message: "Invalid Amount" }); + return; + } + + const superChainBridge = getContract({ + address: superchainBridgeAddress, + chain: coreContract.chain, + client: coreContract.client, + }); + + const sendErc20Tx = prepareContractCall({ + contract: superChainBridge, + method: + "function sendERC20(address _token, address _to, uint256 _amount, uint256 _chainId)", + params: [ + coreContract.address, + activeAccount.address, + BigInt(amount), + BigInt(chainId), + ], + }); + + await sendAndConfirmTransaction({ + account: activeAccount, + transaction: sendErc20Tx, + }); + }; + + const crossChainTransferNotifications = useTxNotifications( + "Successfully submitted cross chain transfer", + "Failed to submit cross chain transfer", + ); + + const crossChainTransferMutation = useMutation({ + mutationFn: crossChainTransfer, + onSuccess: crossChainTransferNotifications.onSuccess, + onError: crossChainTransferNotifications.onError, + }); + + const columns: ColumnDef[] = [ + { + accessorKey: "network", + header: "Network", + cell: ({ row }) => { + if (row.getValue("status") === "DEPLOYED") { + return ( + + {row.getValue("network")} + + ); + } + return row.getValue("network"); + }, + }, + { + accessorKey: "chainId", + header: "Chain ID", + }, + { + accessorKey: "status", + header: "Status", + cell: ({ row }) => { + if (row.getValue("status") === "DEPLOYED") { + return ( + ( + + + + + + + crossChainTransferMutation.mutate( + row.getValue("chainId"), + ) + } + className="rounded-lg rounded-l-none border border-l-0" + > + Transfer + + + + + + + )} + /> + ); + } + return ( + deployContract(row.getValue("chainId"))} + > + Deploy + + ); + }, + }, + ]; + + const table = useReactTable({ + data, + columns, + getCoreRowModel: getCoreRowModel(), + }); + + const deployContract = async (chainId: number) => { + try { + if (!activeAccount) { + throw new Error("No active account"); + } + + // eslint-disable-next-line no-restricted-syntax + const chain = defineChain(chainId); + const client = getThirdwebClient(); + const salt = concatHex(["0x0101", padHex("0x", { size: 30 })]).toString(); + + await switchChain(chain); + + const steps: DeployModalStep[] = [ + { + type: "deploy", + signatureCount: 1, + }, + ]; + + deployStatusModal.setViewContractLink(""); + deployStatusModal.open(steps); + + const isCrosschain = !!modulesMetadata?.find( + (m) => m.name === "SuperChainInterop", + ); + + const crosschainContractAddress = await deployContractfromDeployMetadata({ + account: activeAccount, + chain, + client, + deployMetadata: coreMetadata, + isCrosschain, + initializeData, + salt, + }); + + await verifyContract({ + address: crosschainContractAddress, + chain, + client, + }); + + if (isCrosschain) { + const owner = await readContract({ + contract: coreContract, + method: "function owner() view returns (address)", + params: [], + }); + + const moduleInitializeParams = modulesMetadata.reduce( + (acc, mod) => { + const params = getModuleInstallParams(mod); + const paramNames = params + .map((param) => param.name) + .filter((p) => p !== undefined); + const returnVal: Record = {}; + + // set connected wallet address as default "royaltyRecipient" + if (showRoyaltyFieldset(paramNames)) { + returnVal.royaltyRecipient = owner || ""; + returnVal.royaltyBps = "0"; + returnVal.transferValidator = ZERO_ADDRESS; + } + + // set connected wallet address as default "primarySaleRecipient" + else if (showPrimarySaleFiedset(paramNames)) { + returnVal.primarySaleRecipient = owner || ""; + } + + // set superchain bridge address + else if (showSuperchainBridgeFieldset(paramNames)) { + returnVal.superchainBridge = + "0x4200000000000000000000000000000000000028"; // OP Superchain Bridge + } + + acc[mod.name] = returnVal; + return acc; + }, + {} as Record>, + ); + + const moduleDeployData = modulesMetadata.map((m) => ({ + deployMetadata: m, + initializeParams: moduleInitializeParams[m.name], + })); + + const contract = getContract({ + address: crosschainContractAddress, + chain, + client, + }); + + const rpcRequest = getRpcClient({ + client, + chain, + }); + const currentNonce = await eth_getTransactionCount(rpcRequest, { + address: activeAccount.address, + }); + + for (const [i, m] of moduleDeployData.entries()) { + let moduleData: `0x${string}` | undefined; + + const moduleInstallParams = m.deployMetadata.abi.find( + (abiType) => + (abiType as AbiFunction).name === "encodeBytesOnInstall", + ) as AbiFunction | undefined; + + if (m.initializeParams && moduleInstallParams) { + moduleData = encodeAbiParameters( + ( + moduleInstallParams.inputs as { name: string; type: string }[] + ).map((p) => ({ + name: p.name, + type: p.type, + })), + Object.values(m.initializeParams), + ); + } + + const installTransaction = installPublishedModule({ + contract, + account: activeAccount, + moduleName: m.deployMetadata.name, + publisher: m.deployMetadata.publisher, + version: m.deployMetadata.version, + moduleData, + nonce: currentNonce + i, + }); + + const txResult = await sendTransaction({ + transaction: installTransaction, + account: activeAccount, + }); + + await waitForReceipt(txResult); + // can't handle parallel transactions, so wait a bit + await new Promise((resolve) => setTimeout(resolve, 1000)); + } + } + + deployStatusModal.nextStep(); + deployStatusModal.setViewContractLink( + `/${chain.id}/${crosschainContractAddress}`, + ); + } catch (e) { + onError(e); + console.error("failed to deploy contract", e); + deployStatusModal.close(); + } + }; + + return ( + + + + + + {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]/cross-chain/page.tsx b/apps/dashboard/src/app/(dashboard)/(chain)/[chain_id]/[contractAddress]/cross-chain/page.tsx new file mode 100644 index 00000000000..adfbac86b64 --- /dev/null +++ b/apps/dashboard/src/app/(dashboard)/(chain)/[chain_id]/[contractAddress]/cross-chain/page.tsx @@ -0,0 +1,139 @@ +import { fetchPublishedContractsFromDeploy } from "components/contract-components/fetchPublishedContractsFromDeploy"; +import { notFound, redirect } from "next/navigation"; +import { getContractEvents, prepareEvent } from "thirdweb"; +import { defineChain, getChainMetadata, localhost } from "thirdweb/chains"; +import { type FetchDeployMetadataResult, getContract } from "thirdweb/contract"; +import { getInstalledModules } from "thirdweb/modules"; +import { eth_getCode, getRpcClient } from "thirdweb/rpc"; +import { getContractPageParamsInfo } from "../_utils/getContractFromParams"; +import { getContractPageMetadata } from "../_utils/getContractPageMetadata"; +import { DataTable } from "./data-table"; + +export function getModuleInstallParams(mod: FetchDeployMetadataResult) { + return ( + mod.abi + .filter((a) => a.type === "function") + .find((f) => f.name === "encodeBytesOnInstall")?.inputs || [] + ); +} + +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; + + if (contract.chain.id === localhost.id) { + return asd; + } + + const { isModularCore } = await getContractPageMetadata(contract); + + if (!isModularCore) { + redirect(`/${params.chain_id}/${params.contractAddress}`); + } + + const originalCode = await eth_getCode( + getRpcClient({ + client: contract.client, + chain: contract.chain, + }), + { + address: contract.address, + }, + ); + + const topOPStackTestnetChainIds = [ + 84532, // Base + 11155420, // OP testnet + 919, // Mode Network + 111557560, // Cyber + 999999999, // Zora + ]; + + const chainsDeployedOn = await Promise.all( + topOPStackTestnetChainIds.map(async (chainId) => { + // eslint-disable-next-line no-restricted-syntax + const chain = defineChain(chainId); + const chainMetadata = await getChainMetadata(chain); + + const rpcRequest = getRpcClient({ + client: contract.client, + chain, + }); + const code = await eth_getCode(rpcRequest, { + address: params.contractAddress, + }); + + return { + id: chainId, + network: chainMetadata.name, + chainId: chain.id, + status: + code === originalCode + ? ("DEPLOYED" as const) + : ("NOT_DEPLOYED" as const), + }; + }), + ); + + const modules = await getInstalledModules({ contract }); + + const coreMetadata = ( + await fetchPublishedContractsFromDeploy({ + contract, + client: contract.client, + }) + ).at(-1) as FetchDeployMetadataResult; + const modulesMetadata = (await Promise.all( + modules.map(async (m) => + ( + await fetchPublishedContractsFromDeploy({ + contract: getContract({ + chain: contract.chain, + client: contract.client, + address: m.implementation, + }), + client: contract.client, + }) + ).at(-1), + ), + )) as FetchDeployMetadataResult[]; + + const ProxyDeployedEvent = prepareEvent({ + signature: + "event ProxyDeployed(address indexed implementation, address proxy, address indexed deployer, bytes data)", + }); + + const twCloneFactoryContract = getContract({ + address: "0xB83db4b940e4796aA1f53DBFC824B9B1865835D5", + chain: contract.chain, + client: contract.client, + }); + + const events = await getContractEvents({ + contract: twCloneFactoryContract, + events: [ProxyDeployedEvent], + blockRange: 123456n, + }); + const event = events.find((e) => e.args.proxy === params.contractAddress); + + return ( + + ); +} 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 591e38c6ccf..f700b35896d 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 @@ -25,15 +25,29 @@ import { CircleAlertIcon, ExternalLinkIcon, InfoIcon } from "lucide-react"; import Link from "next/link"; import { useCallback, useMemo } from "react"; import { FormProvider, type UseFormReturn, useForm } from "react-hook-form"; -import { ZERO_ADDRESS } from "thirdweb"; +import { + ZERO_ADDRESS, + eth_getTransactionCount, + getContract, + getRpcClient, + sendTransaction, + waitForReceipt, +} from "thirdweb"; import type { FetchDeployMetadataResult } from "thirdweb/contract"; import { deployContractfromDeployMetadata, deployMarketplaceContract, getRequiredTransactions, } from "thirdweb/deploys"; +import { installPublishedModule } from "thirdweb/modules"; import { useActiveAccount, useActiveWalletChain } from "thirdweb/react"; import { upload } from "thirdweb/storage"; +import { + type AbiFunction, + concatHex, + encodeAbiParameters, + padHex, +} from "thirdweb/utils"; import { FormHelperText, FormLabel, Heading, Text } from "tw-components"; import { useCustomFactoryAbi, useFunctionParamsFromABI } from "../hooks"; import { addContractToMultiChainRegistry } from "../utils"; @@ -186,6 +200,10 @@ export const CustomContractForm: React.FC = ({ !isFactoryDeployment && (metadata?.name.includes("AccountFactory") || false); + const isSuperchainInterop = !!modules?.find( + (m) => m.name === "SuperChainInterop", + ); + const parsedDeployParams = useMemo( () => ({ ...deployParams.reduce( @@ -454,13 +472,23 @@ export const CustomContractForm: React.FC = ({ _contractURI, }; - const salt = params.deployDeterministic - ? params.signerAsSalt - ? activeAccount.address.concat(params.saltForCreate2) - : params.saltForCreate2 - : undefined; - - return await deployContractfromDeployMetadata({ + const salt = isSuperchainInterop + ? concatHex(["0x0101", padHex("0x", { size: 30 })]).toString() + : params.deployDeterministic + ? params.signerAsSalt + ? activeAccount.address.concat(params.saltForCreate2) + : params.saltForCreate2 + : undefined; + + const moduleDeployData = modules?.map((m) => ({ + deployMetadata: m, + initializeParams: + m.name === "SuperChainInterop" + ? { superchainBridge: "0x4200000000000000000000000000000000000028" } + : params.moduleData[m.name], + })); + + const coreContractAddress = await deployContractfromDeployMetadata({ account: activeAccount, chain: walletChain, client: thirdwebClient, @@ -468,11 +496,71 @@ export const CustomContractForm: React.FC = ({ initializeParams, implementationConstructorParams, salt, - modules: modules?.map((m) => ({ - deployMetadata: m, - initializeParams: params.moduleData[m.name], - })), + isSuperchainInterop, + modules: isSuperchainInterop + ? // remove modules for superchain interop in order to deploy deterministically deploy just the core contract + [] + : moduleDeployData, + }); + const coreContract = getContract({ + client: thirdwebClient, + address: coreContractAddress, + chain: walletChain, }); + + if (isSuperchainInterop && moduleDeployData) { + const rpcRequest = getRpcClient({ + client: thirdwebClient, + chain: walletChain, + }); + const currentNonce = await eth_getTransactionCount(rpcRequest, { + address: activeAccount.address, + }); + + for (const [i, m] of moduleDeployData.entries()) { + let moduleData: `0x${string}` | undefined; + + const moduleInstallParams = m.deployMetadata.abi.find( + (abiType) => + (abiType as AbiFunction).name === "encodeBytesOnInstall", + ) as AbiFunction | undefined; + + if (m.initializeParams && moduleInstallParams) { + moduleData = encodeAbiParameters( + ( + moduleInstallParams.inputs as { name: string; type: string }[] + ).map((p) => ({ + name: p.name, + type: p.type, + })), + Object.values(m.initializeParams), + ); + } + + console.log("nonce used: ", currentNonce + i); + + const installTransaction = installPublishedModule({ + contract: coreContract, + account: activeAccount, + moduleName: m.deployMetadata.name, + publisher: m.deployMetadata.publisher, + version: m.deployMetadata.version, + moduleData, + nonce: currentNonce + i, + }); + + const txResult = await sendTransaction({ + transaction: installTransaction, + account: activeAccount, + }); + + await waitForReceipt(txResult); + // can't handle parallel transactions, so wait a bit + await new Promise((resolve) => setTimeout(resolve, 1000)); + } + } + + return coreContractAddress; }, }); @@ -795,7 +883,10 @@ export const CustomContractForm: React.FC = ({ {isModular && modules && modules.length > 0 && ( mod.name !== "SuperChainInterop", + )} isTWPublisher={isTWPublisher} /> )} diff --git a/apps/dashboard/src/components/contract-components/contract-deploy-form/modular-contract-default-modules-fieldset.tsx b/apps/dashboard/src/components/contract-components/contract-deploy-form/modular-contract-default-modules-fieldset.tsx index dab4862ac78..b0ae9a95dff 100644 --- a/apps/dashboard/src/components/contract-components/contract-deploy-form/modular-contract-default-modules-fieldset.tsx +++ b/apps/dashboard/src/components/contract-components/contract-deploy-form/modular-contract-default-modules-fieldset.tsx @@ -226,6 +226,10 @@ export function showPrimarySaleFiedset(paramNames: string[]) { return paramNames.length === 1 && paramNames.includes("primarySaleRecipient"); } +export function showSuperchainBridgeFieldset(paramNames: string[]) { + return paramNames.length === 1 && paramNames.includes("superchainBridge"); +} + function showSequentialTokenIdFieldset(paramNames: string[]) { return paramNames.length === 1 && paramNames.includes("startTokenId"); } diff --git a/apps/dashboard/src/data/explore.ts b/apps/dashboard/src/data/explore.ts index 731bd453120..e42206a031c 100644 --- a/apps/dashboard/src/data/explore.ts +++ b/apps/dashboard/src/data/explore.ts @@ -192,6 +192,29 @@ const MODULAR_CONTRACTS = { ], } satisfies ExploreCategory; +const CROSS_CHAIN = { + id: "cross-chain", + name: "cross-chain", + displayName: "Cross Chain", + description: + "Collection of contracts that are popular for building cross-chain applications.", + contracts: [ + // erc20 drop + [ + "deployer.thirdweb.eth/ERC20CoreInitializable", // TODO: replace this with the thirdweb published contract + [ + "0xf2d22310905EaD92C19c7ef0003C1AD38e129cb1/SuperChainInterop", // TODO: replace this with the OP published contract + "deployer.thirdweb.eth/ClaimableERC20", + ], + { + title: "OP Superchain Modular Token Drop", + description: + "ERC-20 Tokens crosschain compatible across OP Superchains", + }, + ], + ], +} satisfies ExploreCategory; + const AIRDROP = { id: "airdrop", name: "Airdrop", @@ -281,6 +304,7 @@ const SMART_WALLET = { const CATEGORIES: Record = { [POPULAR.id]: POPULAR, [MODULAR_CONTRACTS.id]: MODULAR_CONTRACTS, + [CROSS_CHAIN.id]: CROSS_CHAIN, [NFTS.id]: NFTS, [MARKETS.id]: MARKETS, [DROPS.id]: DROPS, diff --git a/packages/thirdweb/src/contract/deployment/deploy-via-autofactory.ts b/packages/thirdweb/src/contract/deployment/deploy-via-autofactory.ts index 3c4410fe8a9..ce2a35405e2 100644 --- a/packages/thirdweb/src/contract/deployment/deploy-via-autofactory.ts +++ b/packages/thirdweb/src/contract/deployment/deploy-via-autofactory.ts @@ -1,5 +1,8 @@ import { parseEventLogs } from "../../event/actions/parse-logs.js"; -import { proxyDeployedEvent } from "../../extensions/thirdweb/__generated__/IContractFactory/events/ProxyDeployed.js"; +import { + modifiedProxyDeployedEvent, + proxyDeployedEvent, +} from "../../extensions/thirdweb/__generated__/IContractFactory/events/ProxyDeployed.js"; import { deployProxyByImplementation } from "../../extensions/thirdweb/__generated__/IContractFactory/write/deployProxyByImplementation.js"; import { eth_blockNumber } from "../../rpc/actions/eth_blockNumber.js"; import { getRpcClient } from "../../rpc/rpc.js"; @@ -35,7 +38,9 @@ export function prepareAutoFactoryDeployTransaction( }); const blockNumber = await eth_blockNumber(rpcRequest); const salt = args.salt - ? keccakId(args.salt) + ? args.salt.startsWith("0x") && args.salt.length === 66 + ? (args.salt as `0x${string}`) + : keccakId(args.salt) : toHex(blockNumber, { size: 32, }); @@ -95,8 +100,69 @@ export async function deployViaAutoFactory( transaction: tx, account, }); + + // TODO: remove this once the modified version of TWCloneFactory has been published + const proxyEvent = salt?.startsWith("0x0101") + ? modifiedProxyDeployedEvent() + : proxyDeployedEvent(); + const decodedEvent = parseEventLogs({ + events: [proxyEvent], + logs: receipt.logs, + }); + if (decodedEvent.length === 0 || !decodedEvent[0]) { + throw new Error( + `No ProxyDeployed event found in transaction: ${receipt.transactionHash}`, + ); + } + return decodedEvent[0]?.args.proxy; +} + +/** + * @internal + */ +export async function deployViaAutoFactoryWithImplementationParams( + options: ClientAndChainAndAccount & { + cloneFactoryContract: ThirdwebContract; + initializeData?: `0x${string}`; + implementationAddress: string; + salt?: string; + }, +): Promise { + const { + client, + chain, + account, + cloneFactoryContract, + initializeData, + implementationAddress, + salt, + } = options; + + const rpcRequest = getRpcClient({ + client, + chain, + }); + const blockNumber = await eth_blockNumber(rpcRequest); + const parsedSalt = salt + ? salt.startsWith("0x") && salt.length === 66 + ? (salt as `0x${string}`) + : keccakId(salt) + : toHex(blockNumber, { + size: 32, + }); + + const tx = deployProxyByImplementation({ + contract: cloneFactoryContract, + data: initializeData || "0x", + implementation: implementationAddress, + salt: parsedSalt, + }); + const receipt = await sendAndConfirmTransaction({ + transaction: tx, + account, + }); const decodedEvent = parseEventLogs({ - events: [proxyDeployedEvent()], + events: [modifiedProxyDeployedEvent()], logs: receipt.logs, }); if (decodedEvent.length === 0 || !decodedEvent[0]) { diff --git a/packages/thirdweb/src/extensions/modules/common/installPublishedModule.ts b/packages/thirdweb/src/extensions/modules/common/installPublishedModule.ts index 0d43f338e81..ff022357c1d 100644 --- a/packages/thirdweb/src/extensions/modules/common/installPublishedModule.ts +++ b/packages/thirdweb/src/extensions/modules/common/installPublishedModule.ts @@ -14,6 +14,7 @@ export type InstallPublishedModuleOptions = { version?: string; constructorParams?: Record; moduleData?: `0x${string}`; + nonce?: number; }; /** @@ -43,10 +44,14 @@ export function installPublishedModule(options: InstallPublishedModuleOptions) { constructorParams, publisher, moduleData, + nonce, } = options; return installModule({ contract, + overrides: { + nonce, + }, asyncParams: async () => { const deployedModule = await getOrDeployInfraForPublishedContract({ chain: contract.chain, diff --git a/packages/thirdweb/src/extensions/prebuilts/deploy-published.ts b/packages/thirdweb/src/extensions/prebuilts/deploy-published.ts index ce0e0f662ff..721f1b42866 100644 --- a/packages/thirdweb/src/extensions/prebuilts/deploy-published.ts +++ b/packages/thirdweb/src/extensions/prebuilts/deploy-published.ts @@ -2,6 +2,7 @@ import type { AbiFunction } from "abitype"; import type { Chain } from "../../chains/types.js"; import type { ThirdwebClient } from "../../client/client.js"; import { type ThirdwebContract, getContract } from "../../contract/contract.js"; +import { deployViaAutoFactoryWithImplementationParams } from "../../contract/deployment/deploy-via-autofactory.js"; import { fetchPublishedContractMetadata } from "../../contract/deployment/publisher.js"; import { getOrDeployInfraContractFromMetadata } from "../../contract/deployment/utils/bootstrap.js"; import { sendAndConfirmTransaction } from "../../transaction/actions/send-and-confirm-transaction.js"; @@ -125,7 +126,10 @@ export type DeployContractfromDeployMetadataOptions = { account: Account; deployMetadata: FetchDeployMetadataResult; initializeParams?: Record; + initializeData?: `0x${string}`; implementationConstructorParams?: Record; + isSuperchainInterop?: boolean; + isCrosschain?: boolean; modules?: { deployMetadata: FetchDeployMetadataResult; initializeParams?: Record; @@ -144,7 +148,10 @@ export async function deployContractfromDeployMetadata( account, chain, initializeParams, + initializeData, deployMetadata, + isSuperchainInterop, // TODO: Remove this once the updated Clone Factory has been published + isCrosschain, implementationConstructorParams, modules, salt, @@ -215,24 +222,45 @@ export async function deployContractfromDeployMetadata( publisher: deployMetadata.publisher, }); - const initializeTransaction = await getInitializeTransaction({ + // TODO: remove this once the modified version of TWCloneFactory + // has been published under the thirdweb wallet + const modifiedCloneFactoryContract = getContract({ client, + address: "0xB83db4b940e4796aA1f53DBFC824B9B1865835D5", // only deployed on OP and zora testnets chain, - deployMetadata: deployMetadata, - implementationContract, - initializeParams: processedInitializeParams, - account, - modules, }); - return deployViaAutoFactory({ - client, - chain, - account, - cloneFactoryContract, - initializeTransaction, - salt, - }); + if (isCrosschain) { + return await deployViaAutoFactoryWithImplementationParams({ + client, + chain, + account, + cloneFactoryContract: modifiedCloneFactoryContract, + implementationAddress: implementationContract.address, + initializeData, + salt, + }); + } else { + const initializeTransaction = await getInitializeTransaction({ + client, + chain, + deployMetadata, + implementationContract, + initializeParams, + account, + modules, + }); + return deployViaAutoFactory({ + client, + chain, + account, + cloneFactoryContract: isSuperchainInterop // TODO: remove this once the updated clone factory is publsihed + ? modifiedCloneFactoryContract + : cloneFactoryContract, + initializeTransaction, + salt, + }); + } } case "customFactory": { if (!deployMetadata?.factoryDeploymentData?.customFactoryInput) { diff --git a/packages/thirdweb/src/extensions/thirdweb/__generated__/IContractFactory/events/ProxyDeployed.ts b/packages/thirdweb/src/extensions/thirdweb/__generated__/IContractFactory/events/ProxyDeployed.ts index bc0db42d912..e9c7d7546fa 100644 --- a/packages/thirdweb/src/extensions/thirdweb/__generated__/IContractFactory/events/ProxyDeployed.ts +++ b/packages/thirdweb/src/extensions/thirdweb/__generated__/IContractFactory/events/ProxyDeployed.ts @@ -45,3 +45,13 @@ export function proxyDeployedEvent(filters: ProxyDeployedEventFilters = {}) { filters, }); } + + +// TODO: remove this once the modified version of TWCloneFactory has been published +export function modifiedProxyDeployedEvent(filters: ProxyDeployedEventFilters = {}) { + return prepareEvent({ + signature: + "event ProxyDeployed(address indexed implementation, address proxy, address indexed deployer, bytes data)", + filters, + }); +} 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);