[] = [
+ {
+ 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 Deployed
;
+ }
+ return (
+
+ );
+ },
+ },
+ ];
+
+ const table = useReactTable({
+ data: tableData,
+ 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 =
+ inputSalt || concatHex(["0x07", padHex("0x", { size: 31 })]).toString();
+
+ await switchChain(chain);
+
+ const steps: DeployModalStep[] = [
+ {
+ type: "deploy",
+ signatureCount: 1,
+ },
+ ];
+
+ deployStatusModal.setViewContractLink("");
+ deployStatusModal.open(steps);
+
+ let crosschainContractAddress: string | undefined;
+ if (initCode && isDirectDeploy) {
+ const tx = prepareTransaction({
+ client,
+ chain,
+ to: "0x4e59b44847b379578588920cA78FbF26c0B4956C",
+ data: initCode,
+ });
+
+ await sendAndConfirmTransaction({
+ transaction: tx,
+ account: activeAccount,
+ });
+
+ const code = await eth_getCode(
+ getRpcClient({
+ client,
+ chain,
+ }),
+ {
+ address: coreContract.address,
+ },
+ );
+
+ if (code && code.length > 2) {
+ crosschainContractAddress = coreContract.address;
+ }
+ } else {
+ crosschainContractAddress = await deployContractfromDeployMetadata({
+ account: activeAccount,
+ chain,
+ client,
+ deployMetadata: coreMetadata,
+ isCrosschain: true,
+ initializeData,
+ salt,
+ });
+
+ verifyContract({
+ address: crosschainContractAddress,
+ chain,
+ client,
+ });
+ if (modulesMetadata) {
+ for (const m of modulesMetadata) {
+ await getOrDeployInfraForPublishedContract({
+ chain,
+ client,
+ account: activeAccount,
+ contractId: m.name,
+ publisher: m.publisher,
+ });
+ }
+ }
+ }
+ deployStatusModal.nextStep();
+ deployStatusModal.setViewContractLink(
+ `/${chain.id}/${crosschainContractAddress}`,
+ );
+ } catch (e) {
+ onError(e);
+ console.error("failed to deploy contract", e);
+ deployStatusModal.close();
+ }
+ };
+
+ const handleAddRow = (chain: { chainId: number; name: string }) => {
+ const existingChain = tableData.find(
+ (row) => row.chainId === chain.chainId,
+ );
+ if (existingChain) {
+ return;
+ }
+
+ const newRow: CrossChain = {
+ id: chain.chainId,
+ network: chain.name,
+ chainId: chain.chainId,
+ status: "NOT_DEPLOYED",
+ };
+
+ setTableData((prevData) => [...prevData, newRow]);
+ };
+
+ return (
+
+
+ );
+}
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..3f2a0a1bf41
--- /dev/null
+++ b/apps/dashboard/src/app/(dashboard)/(chain)/[chain_id]/[contractAddress]/cross-chain/page.tsx
@@ -0,0 +1,270 @@
+import { fetchPublishedContractsFromDeploy } from "components/contract-components/fetchPublishedContractsFromDeploy";
+import { notFound } from "next/navigation";
+import {
+ eth_getTransactionByHash,
+ eth_getTransactionReceipt,
+ getContractEvents,
+ parseEventLogs,
+ prepareEvent,
+} from "thirdweb";
+import {
+ type ChainMetadata,
+ defineChain,
+ getChainMetadata,
+} from "thirdweb/chains";
+import {
+ type FetchDeployMetadataResult,
+ getContract,
+ getDeployedCloneFactoryContract,
+ resolveContractAbi,
+} from "thirdweb/contract";
+import { moduleInstalledEvent } from "thirdweb/modules";
+import { eth_getCode, getRpcClient } from "thirdweb/rpc";
+import type { TransactionReceipt } from "thirdweb/transaction";
+import { type AbiFunction, decodeFunctionData } from "thirdweb/utils";
+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 || []
+ );
+}
+
+async function fetchChainsFromApi() {
+ const res = await fetch("https://api.thirdweb.com/v1/chains");
+ const json = await res.json();
+
+ if (json.error) {
+ throw new Error(json.error.message);
+ }
+
+ return json.data as ChainMetadata[];
+}
+
+const allChains = await fetchChainsFromApi();
+
+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);
+
+ const ProxyDeployedEvent = prepareEvent({
+ signature:
+ "event ProxyDeployedV2(address indexed implementation, address indexed proxy, address indexed deployer, bytes32 inputSalt, bytes data, bytes extraData)",
+ });
+
+ const twCloneFactoryContract = await getDeployedCloneFactoryContract({
+ chain: contract.chain,
+ client: contract.client,
+ });
+
+ const originalCode = await eth_getCode(
+ getRpcClient({
+ client: contract.client,
+ chain: contract.chain,
+ }),
+ {
+ address: contract.address,
+ },
+ );
+
+ let initCode: `0x${string}` = "0x";
+ let creationTxReceipt: TransactionReceipt | undefined;
+ let isDirectDeploy = false;
+ try {
+ const res = await fetch(
+ `https://contract.thirdweb-dev.com/creation/${contract.chain.id}/${contract.address}`,
+ );
+ const creationData = await res.json();
+
+ if (creationData.status === "1" && creationData.result[0]?.txHash) {
+ const rpcClient = getRpcClient({
+ client: contract.client,
+ chain: contract.chain,
+ });
+ creationTxReceipt = await eth_getTransactionReceipt(rpcClient, {
+ hash: creationData.result[0]?.txHash,
+ });
+
+ const creationTx = await eth_getTransactionByHash(rpcClient, {
+ hash: creationData.result[0]?.txHash,
+ });
+
+ initCode = creationTx.input;
+ isDirectDeploy =
+ creationTx.to?.toLowerCase() ===
+ "0x4e59b44847b379578588920cA78FbF26c0B4956C".toLowerCase();
+ }
+ } catch (e) {
+ console.debug(e);
+ }
+
+ let initializeData: `0x${string}` | undefined;
+ let inputSalt: `0x${string}` | undefined;
+ let creationBlockNumber: bigint | undefined;
+
+ if (twCloneFactoryContract) {
+ const events = await getContractEvents({
+ contract: twCloneFactoryContract,
+ events: [ProxyDeployedEvent],
+ blockRange: 123456n,
+ });
+ const event = events.find(
+ (e) =>
+ e.args.proxy.toLowerCase() === params.contractAddress.toLowerCase(),
+ );
+
+ initializeData = event?.args.data;
+ inputSalt = event?.args.inputSalt;
+ creationBlockNumber = event?.blockNumber;
+ }
+
+ const chainsDeployedOn = (
+ await Promise.all(
+ allChains.map(async (c) => {
+ // eslint-disable-next-line no-restricted-syntax
+ const chain = defineChain(c.chainId);
+
+ try {
+ const chainMetadata = await getChainMetadata(chain);
+
+ const rpcRequest = getRpcClient({
+ client: contract.client,
+ chain,
+ });
+ const code = await eth_getCode(rpcRequest, {
+ address: params.contractAddress,
+ });
+
+ return {
+ id: chain.id,
+ network: chainMetadata.name,
+ chainId: chain.id,
+ status:
+ code === originalCode
+ ? ("DEPLOYED" as const)
+ : ("NOT_DEPLOYED" as const),
+ };
+ } catch {
+ return {
+ id: chain.id,
+ network: "",
+ chainId: chain.id,
+ status: "NOT_DEPLOYED" as const,
+ };
+ }
+ }),
+ )
+ ).filter((c) => c.status === "DEPLOYED");
+
+ const coreMetadata = (
+ await fetchPublishedContractsFromDeploy({
+ contract,
+ client: contract.client,
+ })
+ ).at(-1) as FetchDeployMetadataResult;
+
+ let modulesMetadata: FetchDeployMetadataResult[] | undefined;
+
+ if (isModularCore) {
+ let modules: string[] | undefined;
+ const moduleEvent = moduleInstalledEvent();
+ // extract module address in ModuleInstalled events from transaction receipt
+ if (creationTxReceipt) {
+ const decodedEvent = parseEventLogs({
+ events: [moduleEvent],
+ logs: creationTxReceipt.logs,
+ });
+
+ modules = decodedEvent.map((e) => e.args.installedModule);
+ }
+
+ // fetch events from contract
+ if (!modules && creationBlockNumber) {
+ const events = await getContractEvents({
+ contract: contract,
+ events: [moduleEvent],
+ blockRange: 123456n,
+ });
+
+ const filteredEvents = events.filter(
+ (e) => e.blockNumber === creationBlockNumber,
+ );
+
+ modules = filteredEvents.map((e) => e.args.installedModule);
+ }
+
+ // if receipt not available, try extracting module address from initialize data
+ if (!modules && initializeData) {
+ // biome-ignore lint/suspicious/noExplicitAny: FIXME
+ const decodedData: any = await decodeFunctionData({
+ contract,
+ data: initializeData,
+ });
+
+ const abi = await resolveContractAbi(contract).catch(() => undefined);
+
+ if (abi) {
+ const initializeFunction = abi.find(
+ (i: AbiFunction) => i.type === "function" && i.name === "initialize",
+ ) as unknown as AbiFunction;
+
+ const moduleIndex = initializeFunction.inputs.findIndex(
+ (i) => i.name === "_modules" || i.name === "modules",
+ );
+
+ modules = moduleIndex ? decodedData[moduleIndex] : undefined;
+ }
+ }
+
+ modulesMetadata = modules
+ ? ((await Promise.all(
+ modules.map(async (m) =>
+ (
+ await fetchPublishedContractsFromDeploy({
+ contract: getContract({
+ chain: contract.chain,
+ client: contract.client,
+ address: m,
+ }),
+ client: contract.client,
+ })
+ ).at(-1),
+ ),
+ )) as FetchDeployMetadataResult[])
+ : undefined;
+ }
+
+ if (!isDirectDeploy && !initializeData) {
+ return Multi chain deployments not available
;
+ }
+
+ return (
+
+ );
+}
diff --git a/apps/dashboard/src/app/(dashboard)/(chain)/[chain_id]/[contractAddress]/cross-chain/single-network-selector.tsx b/apps/dashboard/src/app/(dashboard)/(chain)/[chain_id]/[contractAddress]/cross-chain/single-network-selector.tsx
new file mode 100644
index 00000000000..6bfecefaaac
--- /dev/null
+++ b/apps/dashboard/src/app/(dashboard)/(chain)/[chain_id]/[contractAddress]/cross-chain/single-network-selector.tsx
@@ -0,0 +1,86 @@
+import { SelectWithSearch } from "@/components/blocks/select-with-search";
+import { Badge } from "@/components/ui/badge";
+import { ChainIcon } from "components/icons/ChainIcon";
+import { useAllChainsData } from "hooks/chains/allChains";
+import { useCallback, useMemo } from "react";
+
+type Option = { label: string; value: string };
+
+export function SingleNetworkSelector(props: {
+ onAddRow: (chain: { chainId: number; name: string }) => void;
+ className?: string;
+}) {
+ const { allChains, idToChain } = useAllChainsData();
+
+ const options = useMemo(() => {
+ return allChains.map((chain) => ({
+ label: chain.name,
+ value: String(chain.chainId),
+ }));
+ }, [allChains]);
+
+ const searchFn = useCallback(
+ (option: Option, searchValue: string) => {
+ const chain = idToChain.get(Number(option.value));
+ if (!chain) {
+ return false;
+ }
+
+ if (Number.isInteger(Number.parseInt(searchValue))) {
+ return String(chain.chainId).startsWith(searchValue);
+ }
+ return chain.name.toLowerCase().includes(searchValue.toLowerCase());
+ },
+ [idToChain],
+ );
+
+ const renderOption = useCallback(
+ (option: Option) => {
+ const chain = idToChain.get(Number(option.value));
+ if (!chain) {
+ return option.label;
+ }
+
+ return (
+
+
+
+ {chain.name}
+
+
+ Chain ID
+ {chain.chainId}
+
+
+ );
+ },
+ [idToChain],
+ );
+
+ const handleChange = (chainId: string) => {
+ const chain = idToChain.get(Number(chainId));
+ if (chain) {
+ props.onAddRow({ chainId: chain.chainId, name: chain.name });
+ }
+ };
+
+ 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 2377170962f..0c9d09acd9c 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 { isZkSyncChain } from "thirdweb/utils";
import { FormHelperText, FormLabel, Heading, Text } from "tw-components";
import { useCustomFactoryAbi, useFunctionParamsFromABI } from "../hooks";
@@ -187,6 +201,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(
@@ -459,13 +477,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,
@@ -473,11 +501,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;
},
});
@@ -800,7 +888,10 @@ export const CustomContractForm: React.FC = ({
{isModular && modules && modules.length > 0 && (
mod.name !== "SuperChainInterop",
+ )}
isTWPublisher={isTWPublisher}
/>
)}
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/scripts/generate/abis/thirdweb/IContractFactory.json b/packages/thirdweb/scripts/generate/abis/thirdweb/IContractFactory.json
index df338d68439..2460d2cdfa5 100644
--- a/packages/thirdweb/scripts/generate/abis/thirdweb/IContractFactory.json
+++ b/packages/thirdweb/scripts/generate/abis/thirdweb/IContractFactory.json
@@ -1,4 +1,6 @@
[
+ "function deployProxyByImplementation(address implementation, bytes data, bytes32 salt) returns (address)",
+ "function deployProxyByImplementationV2(address implementation, bytes data, bytes32 salt, bytes extraData) returns (address)",
"event ProxyDeployed(address indexed implementation, address proxy, address indexed deployer)",
- "function deployProxyByImplementation(address implementation, bytes data, bytes32 salt) returns (address)"
+ "event ProxyDeployedV2(address indexed implementation, address indexed proxy, address indexed deployer, bytes32 inputSalt, bytes data, bytes extraData)"
]
\ No newline at end of file
diff --git a/packages/thirdweb/src/contract/deployment/deploy-via-autofactory.ts b/packages/thirdweb/src/contract/deployment/deploy-via-autofactory.ts
index 3c4410fe8a9..a09a6bc1e74 100644
--- a/packages/thirdweb/src/contract/deployment/deploy-via-autofactory.ts
+++ b/packages/thirdweb/src/contract/deployment/deploy-via-autofactory.ts
@@ -1,6 +1,6 @@
import { parseEventLogs } from "../../event/actions/parse-logs.js";
-import { proxyDeployedEvent } from "../../extensions/thirdweb/__generated__/IContractFactory/events/ProxyDeployed.js";
-import { deployProxyByImplementation } from "../../extensions/thirdweb/__generated__/IContractFactory/write/deployProxyByImplementation.js";
+import { proxyDeployedV2Event } from "../../extensions/thirdweb/__generated__/IContractFactory/events/ProxyDeployedV2.js";
+import { deployProxyByImplementationV2 } from "../../extensions/thirdweb/__generated__/IContractFactory/write/deployProxyByImplementationV2.js";
import { eth_blockNumber } from "../../rpc/actions/eth_blockNumber.js";
import { getRpcClient } from "../../rpc/rpc.js";
import { encode } from "../../transaction/actions/encode.js";
@@ -23,11 +23,14 @@ import { zkDeployProxy } from "./zksync/zkDeployProxy.js";
export function prepareAutoFactoryDeployTransaction(
args: ClientAndChain & {
cloneFactoryContract: ThirdwebContract;
- initializeTransaction: PreparedTransaction;
+ initializeTransaction?: PreparedTransaction;
+ initializeData?: `0x${string}`;
+ implementationAddress?: string;
+ isCrosschain?: boolean;
salt?: string;
},
) {
- return deployProxyByImplementation({
+ return deployProxyByImplementationV2({
contract: args.cloneFactoryContract,
async asyncParams() {
const rpcRequest = getRpcClient({
@@ -35,10 +38,32 @@ export function prepareAutoFactoryDeployTransaction(
});
const blockNumber = await eth_blockNumber(rpcRequest);
const salt = args.salt
- ? keccakId(args.salt)
- : toHex(blockNumber, {
- size: 32,
- });
+ ? args.salt.startsWith("0x") && args.salt.length === 66
+ ? (args.salt as `0x${string}`)
+ : keccakId(args.salt)
+ : (`0x07${toHex(blockNumber, {
+ size: 31,
+ }).replace(/^0x/, "")}` as `0x${string}`);
+
+ if (args.isCrosschain) {
+ if (!args.initializeData || !args.implementationAddress) {
+ throw new Error(
+ "initializeData or implementationAddress can't be undefined",
+ );
+ }
+
+ return {
+ data: args.initializeData,
+ implementation: args.implementationAddress,
+ salt,
+ extraData: "0x",
+ } as const;
+ }
+
+ if (!args.initializeTransaction) {
+ throw new Error("initializeTransaction can't be undefined");
+ }
+
const implementation = await resolvePromisedValue(
args.initializeTransaction.to,
);
@@ -49,6 +74,7 @@ export function prepareAutoFactoryDeployTransaction(
data: await encode(args.initializeTransaction),
implementation,
salt,
+ extraData: "0x",
} as const;
},
});
@@ -60,7 +86,10 @@ export function prepareAutoFactoryDeployTransaction(
export async function deployViaAutoFactory(
options: ClientAndChainAndAccount & {
cloneFactoryContract: ThirdwebContract;
- initializeTransaction: PreparedTransaction;
+ initializeTransaction?: PreparedTransaction;
+ initializeData?: `0x${string}`;
+ implementationAddress?: string;
+ isCrosschain?: boolean;
salt?: string;
},
): Promise {
@@ -70,10 +99,16 @@ export async function deployViaAutoFactory(
account,
cloneFactoryContract,
initializeTransaction,
+ initializeData,
+ implementationAddress,
+ isCrosschain,
salt,
} = options;
if (await isZkSyncChain(chain)) {
+ if (!initializeTransaction) {
+ throw new Error("initializeTransaction can't be undefined");
+ }
return zkDeployProxy({
chain,
client,
@@ -89,14 +124,19 @@ export async function deployViaAutoFactory(
client,
cloneFactoryContract,
initializeTransaction,
+ initializeData,
+ implementationAddress,
+ isCrosschain,
salt,
});
const receipt = await sendAndConfirmTransaction({
transaction: tx,
account,
});
+
+ const proxyEvent = proxyDeployedV2Event();
const decodedEvent = parseEventLogs({
- events: [proxyDeployedEvent()],
+ events: [proxyEvent],
logs: receipt.logs,
});
if (decodedEvent.length === 0 || !decodedEvent[0]) {
diff --git a/packages/thirdweb/src/contract/deployment/utils/bootstrap.ts b/packages/thirdweb/src/contract/deployment/utils/bootstrap.ts
index a0199c5fe37..f796427d624 100644
--- a/packages/thirdweb/src/contract/deployment/utils/bootstrap.ts
+++ b/packages/thirdweb/src/contract/deployment/utils/bootstrap.ts
@@ -167,6 +167,7 @@ export async function deployCloneFactory(options: ClientAndChainAndAccount) {
...options,
contractId: "TWCloneFactory",
constructorParams: { _trustedForwarder: forwarder.address },
+ publisher: "0x6453a486d52e0EB6E79Ec4491038E2522a926936", // TODO: use default publisher
});
}
@@ -218,7 +219,10 @@ export async function getOrDeployInfraContract(
const contractMetadata = await fetchPublishedContractMetadata({
client: options.client,
contractId: options.contractId,
- publisher: options.publisher,
+ publisher:
+ options.contractId === "TWCloneFactory"
+ ? "0x6453a486d52e0EB6E79Ec4491038E2522a926936" // TODO: use default publisher
+ : options.publisher,
version: options.version,
});
return getOrDeployInfraContractFromMetadata({
diff --git a/packages/thirdweb/src/contract/deployment/utils/clone-factory.ts b/packages/thirdweb/src/contract/deployment/utils/clone-factory.ts
index 48410da5505..3ecd0557426 100644
--- a/packages/thirdweb/src/contract/deployment/utils/clone-factory.ts
+++ b/packages/thirdweb/src/contract/deployment/utils/clone-factory.ts
@@ -23,6 +23,7 @@ export async function getDeployedCloneFactoryContract(args: ClientAndChain) {
...args,
contractId: "TWCloneFactory",
constructorParams: { _trustedForwarder: forwarder.address },
+ publisher: "0x6453a486d52e0EB6E79Ec4491038E2522a926936", // TODO: use default publisher
});
if (!cloneFactory) {
return null;
diff --git a/packages/thirdweb/src/contract/deployment/utils/infra.ts b/packages/thirdweb/src/contract/deployment/utils/infra.ts
index b2f2a5c59e9..495bd844670 100644
--- a/packages/thirdweb/src/contract/deployment/utils/infra.ts
+++ b/packages/thirdweb/src/contract/deployment/utils/infra.ts
@@ -46,7 +46,10 @@ export async function getDeployedInfraContract(
const contractMetadata = await fetchPublishedContractMetadata({
client: options.client,
contractId: options.contractId,
- publisher: options.publisher,
+ publisher:
+ options.contractId === "TWCloneFactory"
+ ? "0x6453a486d52e0EB6E79Ec4491038E2522a926936" // TODO: use default publisher
+ : options.publisher,
version: options.version,
});
return getDeployedInfraContractFromMetadata({
diff --git a/packages/thirdweb/src/exports/contract.ts b/packages/thirdweb/src/exports/contract.ts
index 74e31db6c7f..cc0d614bb3d 100644
--- a/packages/thirdweb/src/exports/contract.ts
+++ b/packages/thirdweb/src/exports/contract.ts
@@ -43,3 +43,4 @@ export { prepareAutoFactoryDeployTransaction } from "../contract/deployment/depl
export { prepareMethod } from "../utils/abi/prepare-method.js";
export { getCompilerMetadata } from "../contract/actions/get-compiler-metadata.js";
+export { getDeployedCloneFactoryContract } from "../contract/deployment/utils/clone-factory.js";
diff --git a/packages/thirdweb/src/exports/modules.ts b/packages/thirdweb/src/exports/modules.ts
index 5e77f55f966..f101b748a63 100644
--- a/packages/thirdweb/src/exports/modules.ts
+++ b/packages/thirdweb/src/exports/modules.ts
@@ -178,3 +178,4 @@ export {
uninstallModuleByProxy,
type UninstallModuleByProxyOptions,
} from "../extensions/modules/common/uninstallModuleByProxy.js";
+export { moduleInstalledEvent } from "../extensions/modules/__generated__/IModularCore/events/ModuleInstalled.js";
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 dbb0157dcfe..03423b06671 100644
--- a/packages/thirdweb/src/extensions/prebuilts/deploy-published.ts
+++ b/packages/thirdweb/src/extensions/prebuilts/deploy-published.ts
@@ -125,7 +125,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 +147,9 @@ export async function deployContractfromDeployMetadata(
account,
chain,
initializeParams,
+ initializeData,
deployMetadata,
+ isCrosschain,
implementationConstructorParams,
modules,
salt,
@@ -217,16 +222,28 @@ export async function deployContractfromDeployMetadata(
version: deployMetadata.version,
});
+ if (isCrosschain) {
+ return deployViaAutoFactory({
+ client,
+ chain,
+ account,
+ cloneFactoryContract,
+ implementationAddress: implementationContract.address,
+ initializeData,
+ salt,
+ isCrosschain,
+ });
+ }
+
const initializeTransaction = await getInitializeTransaction({
client,
chain,
- deployMetadata: deployMetadata,
+ deployMetadata,
implementationContract,
initializeParams: processedInitializeParams,
account,
modules,
});
-
return deployViaAutoFactory({
client,
chain,
diff --git a/packages/thirdweb/src/extensions/thirdweb/__generated__/IContractFactory/events/ProxyDeployedV2.ts b/packages/thirdweb/src/extensions/thirdweb/__generated__/IContractFactory/events/ProxyDeployedV2.ts
new file mode 100644
index 00000000000..ecf298f13a1
--- /dev/null
+++ b/packages/thirdweb/src/extensions/thirdweb/__generated__/IContractFactory/events/ProxyDeployedV2.ts
@@ -0,0 +1,55 @@
+import { prepareEvent } from "../../../../../event/prepare-event.js";
+import type { AbiParameterToPrimitiveType } from "abitype";
+
+/**
+ * Represents the filters for the "ProxyDeployedV2" event.
+ */
+export type ProxyDeployedV2EventFilters = Partial<{
+ implementation: AbiParameterToPrimitiveType<{
+ type: "address";
+ name: "implementation";
+ indexed: true;
+ }>;
+ proxy: AbiParameterToPrimitiveType<{
+ type: "address";
+ name: "proxy";
+ indexed: true;
+ }>;
+ deployer: AbiParameterToPrimitiveType<{
+ type: "address";
+ name: "deployer";
+ indexed: true;
+ }>;
+}>;
+
+/**
+ * Creates an event object for the ProxyDeployedV2 event.
+ * @param filters - Optional filters to apply to the event.
+ * @returns The prepared event object.
+ * @extension THIRDWEB
+ * @example
+ * ```ts
+ * import { getContractEvents } from "thirdweb";
+ * import { proxyDeployedV2Event } from "thirdweb/extensions/thirdweb";
+ *
+ * const events = await getContractEvents({
+ * contract,
+ * events: [
+ * proxyDeployedV2Event({
+ * implementation: ...,
+ * proxy: ...,
+ * deployer: ...,
+ * })
+ * ],
+ * });
+ * ```
+ */
+export function proxyDeployedV2Event(
+ filters: ProxyDeployedV2EventFilters = {},
+) {
+ return prepareEvent({
+ signature:
+ "event ProxyDeployedV2(address indexed implementation, address indexed proxy, address indexed deployer, bytes32 inputSalt, bytes data, bytes extraData)",
+ filters,
+ });
+}
diff --git a/packages/thirdweb/src/extensions/thirdweb/__generated__/IContractFactory/write/deployProxyByImplementationV2.ts b/packages/thirdweb/src/extensions/thirdweb/__generated__/IContractFactory/write/deployProxyByImplementationV2.ts
new file mode 100644
index 00000000000..5455f8b90a0
--- /dev/null
+++ b/packages/thirdweb/src/extensions/thirdweb/__generated__/IContractFactory/write/deployProxyByImplementationV2.ts
@@ -0,0 +1,184 @@
+import type { AbiParameterToPrimitiveType } from "abitype";
+import type {
+ BaseTransactionOptions,
+ WithOverrides,
+} from "../../../../../transaction/types.js";
+import { prepareContractCall } from "../../../../../transaction/prepare-contract-call.js";
+import { encodeAbiParameters } from "../../../../../utils/abi/encodeAbiParameters.js";
+import { once } from "../../../../../utils/promise/once.js";
+import { detectMethod } from "../../../../../utils/bytecode/detectExtension.js";
+
+/**
+ * Represents the parameters for the "deployProxyByImplementationV2" function.
+ */
+export type DeployProxyByImplementationV2Params = WithOverrides<{
+ implementation: AbiParameterToPrimitiveType<{
+ type: "address";
+ name: "implementation";
+ }>;
+ data: AbiParameterToPrimitiveType<{ type: "bytes"; name: "data" }>;
+ salt: AbiParameterToPrimitiveType<{ type: "bytes32"; name: "salt" }>;
+ extraData: AbiParameterToPrimitiveType<{ type: "bytes"; name: "extraData" }>;
+}>;
+
+export const FN_SELECTOR = "0xd057c8b1" as const;
+const FN_INPUTS = [
+ {
+ type: "address",
+ name: "implementation",
+ },
+ {
+ type: "bytes",
+ name: "data",
+ },
+ {
+ type: "bytes32",
+ name: "salt",
+ },
+ {
+ type: "bytes",
+ name: "extraData",
+ },
+] as const;
+const FN_OUTPUTS = [
+ {
+ type: "address",
+ },
+] as const;
+
+/**
+ * Checks if the `deployProxyByImplementationV2` method is supported by the given contract.
+ * @param availableSelectors An array of 4byte function selectors of the contract. You can get this in various ways, such as using "whatsabi" or if you have the ABI of the contract available you can use it to generate the selectors.
+ * @returns A boolean indicating if the `deployProxyByImplementationV2` method is supported.
+ * @extension THIRDWEB
+ * @example
+ * ```ts
+ * import { isDeployProxyByImplementationV2Supported } from "thirdweb/extensions/thirdweb";
+ *
+ * const supported = isDeployProxyByImplementationV2Supported(["0x..."]);
+ * ```
+ */
+export function isDeployProxyByImplementationV2Supported(
+ availableSelectors: string[],
+) {
+ return detectMethod({
+ availableSelectors,
+ method: [FN_SELECTOR, FN_INPUTS, FN_OUTPUTS] as const,
+ });
+}
+
+/**
+ * Encodes the parameters for the "deployProxyByImplementationV2" function.
+ * @param options - The options for the deployProxyByImplementationV2 function.
+ * @returns The encoded ABI parameters.
+ * @extension THIRDWEB
+ * @example
+ * ```ts
+ * import { encodeDeployProxyByImplementationV2Params } from "thirdweb/extensions/thirdweb";
+ * const result = encodeDeployProxyByImplementationV2Params({
+ * implementation: ...,
+ * data: ...,
+ * salt: ...,
+ * extraData: ...,
+ * });
+ * ```
+ */
+export function encodeDeployProxyByImplementationV2Params(
+ options: DeployProxyByImplementationV2Params,
+) {
+ return encodeAbiParameters(FN_INPUTS, [
+ options.implementation,
+ options.data,
+ options.salt,
+ options.extraData,
+ ]);
+}
+
+/**
+ * Encodes the "deployProxyByImplementationV2" function into a Hex string with its parameters.
+ * @param options - The options for the deployProxyByImplementationV2 function.
+ * @returns The encoded hexadecimal string.
+ * @extension THIRDWEB
+ * @example
+ * ```ts
+ * import { encodeDeployProxyByImplementationV2 } from "thirdweb/extensions/thirdweb";
+ * const result = encodeDeployProxyByImplementationV2({
+ * implementation: ...,
+ * data: ...,
+ * salt: ...,
+ * extraData: ...,
+ * });
+ * ```
+ */
+export function encodeDeployProxyByImplementationV2(
+ options: DeployProxyByImplementationV2Params,
+) {
+ // we do a "manual" concat here to avoid the overhead of the "concatHex" function
+ // we can do this because we know the specific formats of the values
+ return (FN_SELECTOR +
+ encodeDeployProxyByImplementationV2Params(options).slice(
+ 2,
+ )) as `${typeof FN_SELECTOR}${string}`;
+}
+
+/**
+ * Prepares a transaction to call the "deployProxyByImplementationV2" function on the contract.
+ * @param options - The options for the "deployProxyByImplementationV2" function.
+ * @returns A prepared transaction object.
+ * @extension THIRDWEB
+ * @example
+ * ```ts
+ * import { sendTransaction } from "thirdweb";
+ * import { deployProxyByImplementationV2 } from "thirdweb/extensions/thirdweb";
+ *
+ * const transaction = deployProxyByImplementationV2({
+ * contract,
+ * implementation: ...,
+ * data: ...,
+ * salt: ...,
+ * extraData: ...,
+ * overrides: {
+ * ...
+ * }
+ * });
+ *
+ * // Send the transaction
+ * await sendTransaction({ transaction, account });
+ * ```
+ */
+export function deployProxyByImplementationV2(
+ options: BaseTransactionOptions<
+ | DeployProxyByImplementationV2Params
+ | {
+ asyncParams: () => Promise;
+ }
+ >,
+) {
+ const asyncOptions = once(async () => {
+ return "asyncParams" in options ? await options.asyncParams() : options;
+ });
+
+ return prepareContractCall({
+ contract: options.contract,
+ method: [FN_SELECTOR, FN_INPUTS, FN_OUTPUTS] as const,
+ params: async () => {
+ const resolvedOptions = await asyncOptions();
+ return [
+ resolvedOptions.implementation,
+ resolvedOptions.data,
+ resolvedOptions.salt,
+ resolvedOptions.extraData,
+ ] as const;
+ },
+ value: async () => (await asyncOptions()).overrides?.value,
+ accessList: async () => (await asyncOptions()).overrides?.accessList,
+ gas: async () => (await asyncOptions()).overrides?.gas,
+ gasPrice: async () => (await asyncOptions()).overrides?.gasPrice,
+ maxFeePerGas: async () => (await asyncOptions()).overrides?.maxFeePerGas,
+ maxPriorityFeePerGas: async () =>
+ (await asyncOptions()).overrides?.maxPriorityFeePerGas,
+ nonce: async () => (await asyncOptions()).overrides?.nonce,
+ extraGas: async () => (await asyncOptions()).overrides?.extraGas,
+ erc20Value: async () => (await asyncOptions()).overrides?.erc20Value,
+ });
+}
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);