From 2bdcdafcb16003927c4dbd0b41735fe7171ede86 Mon Sep 17 00:00:00 2001 From: leekt Date: Thu, 5 Jun 2025 23:38:25 +0900 Subject: [PATCH 1/5] update remove deprecated field in internal use --- src/constant.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/constant.ts b/src/constant.ts index 6ff8851..102f508 100644 --- a/src/constant.ts +++ b/src/constant.ts @@ -7,7 +7,6 @@ export const DEPLOYER_CONTRACT_ADDRESS = export type ZerodevChain = { onlySelfFunded: boolean rollupProvider: string | null - deprecated: boolean explorerAPI: string | null } & Chain @@ -73,7 +72,7 @@ export const getSupportedChains = async (): Promise => { const data = (await response.json()) as ZerodevChainResponse[] - const chains_all = data.reduce( + const chains_all = data.filter(chain => !chain.deprecated).reduce( (acc, chain) => { const key = `${chain.name}-${chain.chainId}` acc[key] = { @@ -89,7 +88,6 @@ export const getSupportedChains = async (): Promise => { }, onlySelfFunded: chain.onlySelfFunded, rollupProvider: chain.rollupProvider, - deprecated: chain.deprecated, testnet: chain.testnet, explorerAPI: process.env[ From b35359620aba27a4823243b159ecfdea1019f136 Mon Sep 17 00:00:00 2001 From: leekt Date: Thu, 5 Jun 2025 23:38:33 +0900 Subject: [PATCH 2/5] lint --- src/constant.ts | 58 +++++++++++++++++++++++++------------------------ 1 file changed, 30 insertions(+), 28 deletions(-) diff --git a/src/constant.ts b/src/constant.ts index 102f508..3a5cc3a 100644 --- a/src/constant.ts +++ b/src/constant.ts @@ -72,34 +72,36 @@ export const getSupportedChains = async (): Promise => { const data = (await response.json()) as ZerodevChainResponse[] - const chains_all = data.filter(chain => !chain.deprecated).reduce( - (acc, chain) => { - const key = `${chain.name}-${chain.chainId}` - acc[key] = { - id: chain.chainId, - name: chain.name, - nativeCurrency: { - name: chain.nativeCurrencyName, - symbol: chain.nativeCurrencySymbol, - decimals: chain.nativeCurrencyDecimals - }, - rpcUrls: { - default: { http: [chain.rpcUrl] } - }, - onlySelfFunded: chain.onlySelfFunded, - rollupProvider: chain.rollupProvider, - testnet: chain.testnet, - explorerAPI: - process.env[ - `${chain.name.toUpperCase()}_VERIFICATION_API_KEY` - ] ?? - process.env.ETHERSCAN_API_KEY ?? - "" // try get chain specific api key, if not found, use etherscan api key - } - return acc - }, - {} as Record - ) + const chains_all = data + .filter((chain) => !chain.deprecated) + .reduce( + (acc, chain) => { + const key = `${chain.name}-${chain.chainId}` + acc[key] = { + id: chain.chainId, + name: chain.name, + nativeCurrency: { + name: chain.nativeCurrencyName, + symbol: chain.nativeCurrencySymbol, + decimals: chain.nativeCurrencyDecimals + }, + rpcUrls: { + default: { http: [chain.rpcUrl] } + }, + onlySelfFunded: chain.onlySelfFunded, + rollupProvider: chain.rollupProvider, + testnet: chain.testnet, + explorerAPI: + process.env[ + `${chain.name.toUpperCase()}_VERIFICATION_API_KEY` + ] ?? + process.env.ETHERSCAN_API_KEY ?? + "" // try get chain specific api key, if not found, use etherscan api key + } + return acc + }, + {} as Record + ) const response_project = await fetch( `https://prod-api-us-east.onrender.com/v2/projects/${process.env.ZERODEV_PROJECT_ID}`, From 6cde435cd617a2db03544248f5e7cdca118bc566 Mon Sep 17 00:00:00 2001 From: leekt Date: Fri, 6 Jun 2025 02:14:44 +0900 Subject: [PATCH 3/5] lint --- src/command/index.ts | 248 ++++++++++++++++++++++++++++++++++++------- 1 file changed, 210 insertions(+), 38 deletions(-) diff --git a/src/command/index.ts b/src/command/index.ts index 2eb70a4..0685f5f 100644 --- a/src/command/index.ts +++ b/src/command/index.ts @@ -1,5 +1,8 @@ #!/usr/bin/env node import crypto from "node:crypto" +import { readFileSync } from "node:fs" +import { dirname, join } from "node:path" +import { fileURLToPath } from "node:url" import chalk from "chalk" import Table from "cli-table3" import { Command } from "commander" @@ -23,12 +26,125 @@ import { validatePrivateKey } from "../utils/index.js" -export const program = new Command() +const __filename = fileURLToPath(import.meta.url) +const __dirname = dirname(__filename) +const packageJson = JSON.parse( + readFileSync(join(__dirname, "../../package.json"), "utf8") +) +const version = packageJson.version + +// Define proper types for command options +type CommandOption = { + flags: string + description: string + defaultValue?: string | boolean | string[] | undefined +} + +// Extend Command class to add fluent API +declare module "commander" { + interface Command { + addOptions(options: CommandOption[]): Command + } +} + +Command.prototype.addOptions = function (options: CommandOption[]) { + for (const option of options) { + this.option(option.flags, option.description, option.defaultValue) + } + return this +} + +// Common options +const chainSelectionOptions: CommandOption[] = [ + { + flags: "-t, --testnet-all", + description: "select all testnets", + defaultValue: false + }, + { + flags: "-m, --mainnet-all", + description: "select all mainnets", + defaultValue: false + }, + { + flags: "-a, --all-networks", + description: "select all networks", + defaultValue: false + }, + { flags: "-c, --chains [CHAINS]", description: "list of chains to deploy" } +] + +const codeOptions: CommandOption[] = [ + { + flags: "-f, --file ", + description: + "file path of bytecode to deploy, a.k.a. init code, or a JSON file containing the bytecode of the contract (such as the output file by Forge), in which case it's assumed that the constructor takes no arguments." + }, + { flags: "-b, --bytecode ", description: "bytecode to deploy" }, + { + flags: "-s, --salt ", + description: + "salt to be used for CREATE2. This can be a full 32-byte hex string or a shorter numeric representation that will be converted to a 32-byte hex string." + } +] + +const deployOptions: CommandOption[] = [ + { + flags: "-e, --expected-address [ADDRESS]", + description: "expected address to confirm" + }, + { + flags: "-v, --verify-contract [CONTRACT_NAME]", + description: "verify the deployment on Etherscan" + }, + { + flags: "-g, --call-gas-limit ", + description: "gas limit for the call" + } +] + +const mirrorOption: CommandOption = { + flags: "-f, --from-chain ", + description: "source chain to mirror from" +} + +interface EtherscanContractCreationResponse { + status: string + message: string + result: Array<{ + contractAddress: string + contractCreator: string + txHash: string + blockNumber: string + timestamp: string + contractFactory: string + creationBytecode: string + }> +} -const fileOption = [ - "-f, --file ", - "file path of bytecode to deploy, a.k.a. init code, or a JSON file containing the bytecode of the contract (such as the output file by Forge), in which case it's assumed that the constructor takes no arguments." -] as [string, string] +interface EtherscanTransactionResponse { + jsonrpc: string + id: number + result: { + blockHash: string + blockNumber: string + from: string + gas: string + gasPrice: string + hash: string + input: string + nonce: string + to: string + transactionIndex: string + value: string + type: string + v: string + r: string + s: string + } +} + +export const program = new Command() program .name("zerodev") @@ -36,7 +152,7 @@ program "tool for deploying contracts to multichain with account abstraction" ) .usage(" [options]") - .version("0.1.3") + .version(version) program.helpInformation = function () { const asciiArt = chalk.blueBright( @@ -77,12 +193,7 @@ program program .command("compute-address") .description("Compute the address to be deployed") - .option(...fileOption) - .option("-b, --bytecode ", "bytecode to deploy") - .option( - "-s, --salt ", - "salt to be used for CREATE2. This can be a full 32-byte hex string or a shorter numeric representation that will be converted to a 32-byte hex string." - ) + .addOptions([...codeOptions]) .action(async (options) => { const { file, bytecode, salt } = options @@ -115,22 +226,7 @@ program .description( "Deploy contracts deterministically using CREATE2, in order of the chains specified" ) - .option(...fileOption) - .option("-b, --bytecode ", "bytecode to deploy") - .option( - "-s, --salt ", - "salt to be used for CREATE2. This can be a full 32-byte hex string or a shorter numeric representation that will be converted to a 32-byte hex string." - ) - .option("-t, --testnet-all", "select all testnets", false) - .option("-m, --mainnet-all", "select all mainnets", false) - .option("-a, --all-networks", "select all networks", false) - .option("-c, --chains [CHAINS]", "list of chains for deploying contracts") - .option("-e, --expected-address [ADDRESS]", "expected address to confirm") - .option( - "-v, --verify-contract [CONTRACT_NAME]", - "verify the deployment on Etherscan" - ) - .option("-g, --call-gas-limit ", "gas limit for the call") + .addOptions([...codeOptions, ...deployOptions, ...chainSelectionOptions]) .action(async (options) => { const { file, @@ -192,16 +288,7 @@ program .description( "check whether the contract has already been deployed on the specified networks" ) - .option(...fileOption) - .option("-b, --bytecode ", "deployed bytecode") - .option( - "-s, --salt ", - "salt to be used for CREATE2. This can be a full 32-byte hex string or a shorter numeric representation that will be converted to a 32-byte hex string." - ) - .option("-t, --testnet-all", "select all testnets", false) - .option("-m, --mainnet-all", "select all mainnets", false) - .option("-a, --all-networks", "select all networks", false) - .option("-c, --chains [CHAINS]", "list of chains for checking") + .addOptions([...codeOptions, ...chainSelectionOptions]) .action(async (options) => { const { file, @@ -245,6 +332,91 @@ program } }) +program + .command("mirror") + .description("Mirror a contract from one chain to other chains") + .addOptions([mirrorOption, ...chainSelectionOptions]) + .argument("", "contract address to mirror") + .action(async (contractAddress, options) => { + const { fromChain, testnetAll, mainnetAll, allNetworks, chains } = + options + + if (!fromChain) { + console.error("Error: Source chain must be specified") + process.exit(1) + } + + if (!contractAddress) { + console.error("Error: Contract address must be specified") + process.exit(1) + } + + const targetChains = await processAndValidateChains({ + testnetAll, + mainnetAll, + allNetworks, + chainOption: chains + }) + + // Get the source chain object + const sourceChain = (await getSupportedChains()).find( + (chain) => chain.name.toLowerCase() === fromChain.toLowerCase() + ) + + if (!sourceChain) { + console.error(`Error: Source chain ${fromChain} not found`) + process.exit(1) + } + + // Fetch contract data from Etherscan + const creationResponse = await fetch( + `https://api.etherscan.io/v2/api?chainid=${sourceChain.id}&module=contract&action=getcontractcreation&contractaddresses=${contractAddress}&apikey=${sourceChain.explorerAPI}` + ) + + const creationData = + (await creationResponse.json()) as EtherscanContractCreationResponse + if (creationData.status !== "1" || !creationData.result?.[0]) { + console.error("Error: Failed to fetch contract data from Etherscan") + process.exit(1) + } + + const contractData = creationData.result[0] + if (contractData.contractFactory !== DEPLOYER_CONTRACT_ADDRESS) { + console.error( + "Error: Contract was not deployed using the deterministic deployer" + ) + process.exit(1) + } + + // Since we can't get the salt from the internal transaction directly, + // we'll need to get it from the original transaction's input data + const originalTxResponse = await fetch( + `https://api.etherscan.io/v2/api?chainid=${sourceChain.id}&module=proxy&action=eth_getTransactionByHash&txhash=${contractData.txHash}&apikey=${sourceChain.explorerAPI}` + ) + + const originalTxData = + (await originalTxResponse.json()) as EtherscanTransactionResponse + if (!originalTxData.result?.input) { + console.error("Error: Could not get the original transaction data") + process.exit(1) + } + + // Extract salt from input data (first 32 bytes after the function selector) + const salt = `0x${originalTxData.result.input.slice(2, 66)}` // Skip 0x and 4 bytes of function selector + + // Deploy the contract to target chains + await deployContracts( + validatePrivateKey(PRIVATE_KEY), + ensureHex(contractData.creationBytecode), + targetChains, + ensureHex(salt), + contractAddress, + undefined + ) + + console.log("✅ Contracts mirrored successfully!") + }) + program .command("clear-log") .description("clear the log files") From 8a7d1e815a10f54679e7578cb9926dc89e7223ea Mon Sep 17 00:00:00 2001 From: leekt Date: Fri, 6 Jun 2025 02:26:20 +0900 Subject: [PATCH 4/5] separate mirror --- src/action/mirror.ts | 134 ++++++++++++++++++++ src/command/index.ts | 271 +++++++++++------------------------------ src/command/mirror.ts | 134 ++++++++++++++++++++ src/command/options.ts | 76 ++++++++++++ 4 files changed, 418 insertions(+), 197 deletions(-) create mode 100644 src/action/mirror.ts create mode 100644 src/command/mirror.ts create mode 100644 src/command/options.ts diff --git a/src/action/mirror.ts b/src/action/mirror.ts new file mode 100644 index 0000000..10c839b --- /dev/null +++ b/src/action/mirror.ts @@ -0,0 +1,134 @@ +import { DEPLOYER_CONTRACT_ADDRESS, getSupportedChains } from "../constant.js" +import { ensureHex } from "../utils/index.js" +import { validatePrivateKey } from "../utils/validate.js" +import { deployContracts } from "./deployContracts.js" + +interface EtherscanContractCreationResponse { + status: string + message: string + result: Array<{ + contractAddress: string + contractCreator: string + txHash: string + blockNumber: string + timestamp: string + contractFactory: string + creationBytecode: string + }> +} + +interface EtherscanTransactionResponse { + jsonrpc: string + id: number + result: { + blockHash: string + blockNumber: string + from: string + gas: string + gasPrice: string + hash: string + input: string + nonce: string + to: string + transactionIndex: string + value: string + type: string + v: string + r: string + s: string + } +} + +interface MirrorOptions { + fromChain: string + testnetAll?: boolean + mainnetAll?: boolean + allNetworks?: boolean + chains?: string +} + +export const mirrorContract = async ( + contractAddress: string, + options: MirrorOptions +) => { + const { fromChain, testnetAll, mainnetAll, allNetworks, chains } = options + + if (!fromChain) { + throw new Error("Error: Source chain must be specified") + } + + if (!contractAddress) { + throw new Error("Error: Contract address must be specified") + } + + const targetChains = await getSupportedChains() + + // Get the source chain object + const sourceChain = targetChains.find( + (chain) => chain.name.toLowerCase() === fromChain.toLowerCase() + ) + + if (!sourceChain) { + throw new Error(`Error: Source chain ${fromChain} not found`) + } + + // Fetch contract data from Etherscan + const creationResponse = await fetch( + `https://api.etherscan.io/v2/api?chainid=${sourceChain.id}&module=contract&action=getcontractcreation&contractaddresses=${contractAddress}&apikey=${sourceChain.explorerAPI}` + ) + + const creationData = + (await creationResponse.json()) as EtherscanContractCreationResponse + if (creationData.status !== "1" || !creationData.result?.[0]) { + throw new Error("Error: Failed to fetch contract data from Etherscan") + } + + const contractData = creationData.result[0] + if (contractData.contractFactory !== DEPLOYER_CONTRACT_ADDRESS) { + throw new Error( + "Error: Contract was not deployed using the deterministic deployer" + ) + } + + // Since we can't get the salt from the internal transaction directly, + // we'll need to get it from the original transaction's input data + const originalTxResponse = await fetch( + `https://api.etherscan.io/v2/api?chainid=${sourceChain.id}&module=proxy&action=eth_getTransactionByHash&txhash=${contractData.txHash}&apikey=${sourceChain.explorerAPI}` + ) + + const originalTxData = + (await originalTxResponse.json()) as EtherscanTransactionResponse + if (!originalTxData.result?.input) { + throw new Error("Error: Could not get the original transaction data") + } + + // Extract salt from input data (first 32 bytes after the function selector) + const saltHex = originalTxData.result.input.slice(2, 66) // Skip 0x and 4 bytes of function selector + const salt = ensureHex(saltHex) + + // Filter target chains based on options + let filteredChains = targetChains + if (testnetAll) { + filteredChains = targetChains.filter((chain) => chain.testnet) + } else if (mainnetAll) { + filteredChains = targetChains.filter((chain) => !chain.testnet) + } else if (chains) { + const chainNames = chains.split(",") + filteredChains = targetChains.filter((chain) => + chainNames.some( + (name: string) => + chain.name.toLowerCase() === name.toLowerCase() + ) + ) + } + + // Deploy the contract to target chains + await deployContracts( + validatePrivateKey(process.env.PRIVATE_KEY as `0x${string}`), + ensureHex(contractData.creationBytecode), + filteredChains, + salt, + contractAddress, + undefined + ) +} diff --git a/src/command/index.ts b/src/command/index.ts index 0685f5f..10ee663 100644 --- a/src/command/index.ts +++ b/src/command/index.ts @@ -14,6 +14,7 @@ import { getDeployerAddress, verifyContracts } from "../action/index.js" +import { mirrorContract } from "../action/mirror.js" import { PRIVATE_KEY } from "../config.js" import { DEPLOYER_CONTRACT_ADDRESS, getSupportedChains } from "../constant.js" import { @@ -25,6 +26,13 @@ import { validateInputs, validatePrivateKey } from "../utils/index.js" +import { mirrorCommand } from "./mirror.js" +import { + chainSelectionOptions, + codeOptions, + deployOptions, + mirrorOption +} from "./options.js" const __filename = fileURLToPath(import.meta.url) const __dirname = dirname(__filename) @@ -54,60 +62,6 @@ Command.prototype.addOptions = function (options: CommandOption[]) { return this } -// Common options -const chainSelectionOptions: CommandOption[] = [ - { - flags: "-t, --testnet-all", - description: "select all testnets", - defaultValue: false - }, - { - flags: "-m, --mainnet-all", - description: "select all mainnets", - defaultValue: false - }, - { - flags: "-a, --all-networks", - description: "select all networks", - defaultValue: false - }, - { flags: "-c, --chains [CHAINS]", description: "list of chains to deploy" } -] - -const codeOptions: CommandOption[] = [ - { - flags: "-f, --file ", - description: - "file path of bytecode to deploy, a.k.a. init code, or a JSON file containing the bytecode of the contract (such as the output file by Forge), in which case it's assumed that the constructor takes no arguments." - }, - { flags: "-b, --bytecode ", description: "bytecode to deploy" }, - { - flags: "-s, --salt ", - description: - "salt to be used for CREATE2. This can be a full 32-byte hex string or a shorter numeric representation that will be converted to a 32-byte hex string." - } -] - -const deployOptions: CommandOption[] = [ - { - flags: "-e, --expected-address [ADDRESS]", - description: "expected address to confirm" - }, - { - flags: "-v, --verify-contract [CONTRACT_NAME]", - description: "verify the deployment on Etherscan" - }, - { - flags: "-g, --call-gas-limit ", - description: "gas limit for the call" - } -] - -const mirrorOption: CommandOption = { - flags: "-f, --from-chain ", - description: "source chain to mirror from" -} - interface EtherscanContractCreationResponse { status: string message: string @@ -147,11 +101,8 @@ interface EtherscanTransactionResponse { export const program = new Command() program - .name("zerodev") - .description( - "tool for deploying contracts to multichain with account abstraction" - ) - .usage(" [options]") + .name("orchestra") + .description("CLI tool for deploying contracts to multiple chains") .version(version) program.helpInformation = function () { @@ -223,64 +174,51 @@ program program .command("deploy") - .description( - "Deploy contracts deterministically using CREATE2, in order of the chains specified" - ) - .addOptions([...codeOptions, ...deployOptions, ...chainSelectionOptions]) + .description("Deploy contracts to multiple chains") + .addOptions([...chainSelectionOptions, ...codeOptions, ...deployOptions]) .action(async (options) => { - const { - file, - bytecode, - salt, - testnetAll, - mainnetAll, - allNetworks, - chains, - expectedAddress, - verifyContract, - callGasLimit - } = options - - const normalizedSalt = normalizeSalt(salt) - - validateInputs(file, bytecode, normalizedSalt, expectedAddress) - const chainObjects = await processAndValidateChains({ - testnetAll, - mainnetAll, - allNetworks, - chainOption: chains - }) - - let bytecodeToDeploy = bytecode - if (file) { - bytecodeToDeploy = readBytecodeFromFile(file) - } - - await deployContracts( - validatePrivateKey(PRIVATE_KEY), - ensureHex(bytecodeToDeploy), - chainObjects, - ensureHex(normalizedSalt), - expectedAddress, - callGasLimit ? BigInt(callGasLimit) : undefined - ) + try { + validatePrivateKey(PRIVATE_KEY) + const chains = await getSupportedChains() + if (chains.length === 0) { + throw new Error("No chains selected") + } + + console.log( + "Selected chains:", + chains.map((chain) => chain.name).join(", ") + ) - console.log("✅ Contracts deployed successfully!") - - if (verifyContract) { - console.log("Verifying contracts on Etherscan...") - await verifyContracts( - verifyContract, - computeContractAddress( - DEPLOYER_CONTRACT_ADDRESS, - ensureHex(bytecodeToDeploy), - ensureHex(normalizedSalt) - ), - chainObjects + let bytecode: string + if (options.file) { + bytecode = readFileSync(options.file, "utf-8") + } else if (options.bytecode) { + bytecode = options.bytecode + } else { + throw new Error("Either --file or --bytecode must be provided") + } + + if (!bytecode.startsWith("0x")) { + bytecode = `0x${bytecode}` + } + + await deployContracts( + validatePrivateKey(PRIVATE_KEY), + bytecode as `0x${string}`, + chains, + options.salt, + options.expectedAddress, + options.callGasLimit ? BigInt(options.callGasLimit) : undefined + ) + console.log("✅ Contracts deployed successfully!") + } catch (error) { + console.error( + error instanceof Error + ? error.message + : "Unknown error occurred" ) + process.exit(1) } - - console.log("✅ Contracts verified successfully!") }) program @@ -332,90 +270,8 @@ program } }) -program - .command("mirror") - .description("Mirror a contract from one chain to other chains") - .addOptions([mirrorOption, ...chainSelectionOptions]) - .argument("", "contract address to mirror") - .action(async (contractAddress, options) => { - const { fromChain, testnetAll, mainnetAll, allNetworks, chains } = - options - - if (!fromChain) { - console.error("Error: Source chain must be specified") - process.exit(1) - } - - if (!contractAddress) { - console.error("Error: Contract address must be specified") - process.exit(1) - } - - const targetChains = await processAndValidateChains({ - testnetAll, - mainnetAll, - allNetworks, - chainOption: chains - }) - - // Get the source chain object - const sourceChain = (await getSupportedChains()).find( - (chain) => chain.name.toLowerCase() === fromChain.toLowerCase() - ) - - if (!sourceChain) { - console.error(`Error: Source chain ${fromChain} not found`) - process.exit(1) - } - - // Fetch contract data from Etherscan - const creationResponse = await fetch( - `https://api.etherscan.io/v2/api?chainid=${sourceChain.id}&module=contract&action=getcontractcreation&contractaddresses=${contractAddress}&apikey=${sourceChain.explorerAPI}` - ) - - const creationData = - (await creationResponse.json()) as EtherscanContractCreationResponse - if (creationData.status !== "1" || !creationData.result?.[0]) { - console.error("Error: Failed to fetch contract data from Etherscan") - process.exit(1) - } - - const contractData = creationData.result[0] - if (contractData.contractFactory !== DEPLOYER_CONTRACT_ADDRESS) { - console.error( - "Error: Contract was not deployed using the deterministic deployer" - ) - process.exit(1) - } - - // Since we can't get the salt from the internal transaction directly, - // we'll need to get it from the original transaction's input data - const originalTxResponse = await fetch( - `https://api.etherscan.io/v2/api?chainid=${sourceChain.id}&module=proxy&action=eth_getTransactionByHash&txhash=${contractData.txHash}&apikey=${sourceChain.explorerAPI}` - ) - - const originalTxData = - (await originalTxResponse.json()) as EtherscanTransactionResponse - if (!originalTxData.result?.input) { - console.error("Error: Could not get the original transaction data") - process.exit(1) - } - - // Extract salt from input data (first 32 bytes after the function selector) - const salt = `0x${originalTxData.result.input.slice(2, 66)}` // Skip 0x and 4 bytes of function selector - - // Deploy the contract to target chains - await deployContracts( - validatePrivateKey(PRIVATE_KEY), - ensureHex(contractData.creationBytecode), - targetChains, - ensureHex(salt), - contractAddress, - undefined - ) - - console.log("✅ Contracts mirrored successfully!") - }) +// Add mirror command +mirrorCommand(program) program .command("clear-log") @@ -441,3 +297,24 @@ program } console.log(`Generated salt: ${ensureHex(salt)}`) }) + +program + .command("mirror") + .description("Mirror a contract from one chain to other chains") + .addOptions([mirrorOption, ...chainSelectionOptions]) + .argument("", "contract address to mirror") + .action(async (contractAddress, options) => { + try { + await mirrorContract(contractAddress, options) + console.log("✅ Contracts mirrored successfully!") + } catch (error) { + console.error( + error instanceof Error + ? error.message + : "Unknown error occurred" + ) + process.exit(1) + } + }) + +program.parse() diff --git a/src/command/mirror.ts b/src/command/mirror.ts new file mode 100644 index 0000000..a45fdf5 --- /dev/null +++ b/src/command/mirror.ts @@ -0,0 +1,134 @@ +import type { Command } from "commander" +import { deployContracts } from "../action/index.js" +import { PRIVATE_KEY } from "../config.js" +import { DEPLOYER_CONTRACT_ADDRESS, getSupportedChains } from "../constant.js" +import { ensureHex, processAndValidateChains } from "../utils/index.js" +import { validatePrivateKey } from "../utils/validate.js" +import { chainSelectionOptions, mirrorOption } from "./options.js" + +interface EtherscanContractCreationResponse { + status: string + message: string + result: Array<{ + contractAddress: string + contractCreator: string + txHash: string + blockNumber: string + timestamp: string + contractFactory: string + creationBytecode: string + }> +} + +interface EtherscanTransactionResponse { + jsonrpc: string + id: number + result: { + blockHash: string + blockNumber: string + from: string + gas: string + gasPrice: string + hash: string + input: string + nonce: string + to: string + transactionIndex: string + value: string + type: string + v: string + r: string + s: string + } +} + +export const mirrorCommand = (program: Command) => { + program + .command("mirror") + .description("Mirror a contract from one chain to other chains") + .addOptions([mirrorOption, ...chainSelectionOptions]) + .argument("", "contract address to mirror") + .action(async (contractAddress, options) => { + const { fromChain, testnetAll, mainnetAll, allNetworks, chains } = + options + + if (!fromChain) { + console.error("Error: Source chain must be specified") + process.exit(1) + } + + if (!contractAddress) { + console.error("Error: Contract address must be specified") + process.exit(1) + } + + const targetChains = await processAndValidateChains({ + testnetAll, + mainnetAll, + allNetworks, + chainOption: chains + }) + + // Get the source chain object + const sourceChain = (await getSupportedChains()).find( + (chain) => chain.name.toLowerCase() === fromChain.toLowerCase() + ) + + if (!sourceChain) { + console.error(`Error: Source chain ${fromChain} not found`) + process.exit(1) + } + + // Fetch contract data from Etherscan + const creationResponse = await fetch( + `https://api.etherscan.io/v2/api?chainid=${sourceChain.id}&module=contract&action=getcontractcreation&contractaddresses=${contractAddress}&apikey=${sourceChain.explorerAPI}` + ) + + const creationData = + (await creationResponse.json()) as EtherscanContractCreationResponse + if (creationData.status !== "1" || !creationData.result?.[0]) { + console.error( + "Error: Failed to fetch contract data from Etherscan" + ) + process.exit(1) + } + + const contractData = creationData.result[0] + if (contractData.contractFactory !== DEPLOYER_CONTRACT_ADDRESS) { + console.error( + "Error: Contract was not deployed using the deterministic deployer" + ) + process.exit(1) + } + + // Since we can't get the salt from the internal transaction directly, + // we'll need to get it from the original transaction's input data + const originalTxResponse = await fetch( + `https://api.etherscan.io/v2/api?chainid=${sourceChain.id}&module=proxy&action=eth_getTransactionByHash&txhash=${contractData.txHash}&apikey=${sourceChain.explorerAPI}` + ) + + const originalTxData = + (await originalTxResponse.json()) as EtherscanTransactionResponse + if (!originalTxData.result?.input) { + console.error( + "Error: Could not get the original transaction data" + ) + process.exit(1) + } + + // Extract salt from input data (first 32 bytes after the function selector) + const salt = `0x${originalTxData.result.input.slice(2, 66)}` // Skip 0x and 4 bytes of function selector + + // Deploy the contract to target chains + await deployContracts( + validatePrivateKey(PRIVATE_KEY), + ensureHex(contractData.creationBytecode), + targetChains, + ensureHex(salt), + contractAddress, + undefined + ) + + console.log("✅ Contracts mirrored successfully!") + }) +} diff --git a/src/command/options.ts b/src/command/options.ts new file mode 100644 index 0000000..5393af8 --- /dev/null +++ b/src/command/options.ts @@ -0,0 +1,76 @@ +import { Command } from "commander" + +// Define proper types for command options +export type CommandOption = { + flags: string + description: string + defaultValue?: string | boolean | string[] | undefined +} + +// Extend Command class to add fluent API +declare module "commander" { + interface Command { + addOptions(options: CommandOption[]): Command + } +} + +Command.prototype.addOptions = function (options: CommandOption[]) { + for (const option of options) { + this.option(option.flags, option.description, option.defaultValue) + } + return this +} + +// Common options +export const chainSelectionOptions: CommandOption[] = [ + { + flags: "-t, --testnet-all", + description: "select all testnets", + defaultValue: false + }, + { + flags: "-m, --mainnet-all", + description: "select all mainnets", + defaultValue: false + }, + { + flags: "-a, --all-networks", + description: "select all networks", + defaultValue: false + }, + { flags: "-c, --chains [CHAINS]", description: "list of chains to deploy" } +] + +export const codeOptions: CommandOption[] = [ + { + flags: "-f, --file ", + description: + "file path of bytecode to deploy, a.k.a. init code, or a JSON file containing the bytecode of the contract (such as the output file by Forge), in which case it's assumed that the constructor takes no arguments." + }, + { flags: "-b, --bytecode ", description: "bytecode to deploy" }, + { + flags: "-s, --salt ", + description: + "salt to be used for CREATE2. This can be a full 32-byte hex string or a shorter numeric representation that will be converted to a 32-byte hex string." + } +] + +export const deployOptions: CommandOption[] = [ + { + flags: "-e, --expected-address [ADDRESS]", + description: "expected address to confirm" + }, + { + flags: "-v, --verify-contract [CONTRACT_NAME]", + description: "verify the deployment on Etherscan" + }, + { + flags: "-g, --call-gas-limit ", + description: "gas limit for the call" + } +] + +export const mirrorOption: CommandOption = { + flags: "-f, --from-chain ", + description: "source chain to mirror from" +} From 4d8ceb3382e69a10de676359c8500943c902fe74 Mon Sep 17 00:00:00 2001 From: leekt Date: Fri, 6 Jun 2025 02:30:15 +0900 Subject: [PATCH 5/5] mirror done, need to support feature of finding a chain that the contract has been deployed on a given address --- src/command/index.ts | 42 +------------ src/command/mirror.ts | 134 ------------------------------------------ 2 files changed, 1 insertion(+), 175 deletions(-) delete mode 100644 src/command/mirror.ts diff --git a/src/command/index.ts b/src/command/index.ts index 10ee663..fd72e3d 100644 --- a/src/command/index.ts +++ b/src/command/index.ts @@ -26,7 +26,6 @@ import { validateInputs, validatePrivateKey } from "../utils/index.js" -import { mirrorCommand } from "./mirror.js" import { chainSelectionOptions, codeOptions, @@ -62,46 +61,10 @@ Command.prototype.addOptions = function (options: CommandOption[]) { return this } -interface EtherscanContractCreationResponse { - status: string - message: string - result: Array<{ - contractAddress: string - contractCreator: string - txHash: string - blockNumber: string - timestamp: string - contractFactory: string - creationBytecode: string - }> -} - -interface EtherscanTransactionResponse { - jsonrpc: string - id: number - result: { - blockHash: string - blockNumber: string - from: string - gas: string - gasPrice: string - hash: string - input: string - nonce: string - to: string - transactionIndex: string - value: string - type: string - v: string - r: string - s: string - } -} - export const program = new Command() program - .name("orchestra") + .name("zerodev") .description("CLI tool for deploying contracts to multiple chains") .version(version) @@ -270,9 +233,6 @@ program } }) -// Add mirror command -mirrorCommand(program) - program .command("clear-log") .description("clear the log files") diff --git a/src/command/mirror.ts b/src/command/mirror.ts deleted file mode 100644 index a45fdf5..0000000 --- a/src/command/mirror.ts +++ /dev/null @@ -1,134 +0,0 @@ -import type { Command } from "commander" -import { deployContracts } from "../action/index.js" -import { PRIVATE_KEY } from "../config.js" -import { DEPLOYER_CONTRACT_ADDRESS, getSupportedChains } from "../constant.js" -import { ensureHex, processAndValidateChains } from "../utils/index.js" -import { validatePrivateKey } from "../utils/validate.js" -import { chainSelectionOptions, mirrorOption } from "./options.js" - -interface EtherscanContractCreationResponse { - status: string - message: string - result: Array<{ - contractAddress: string - contractCreator: string - txHash: string - blockNumber: string - timestamp: string - contractFactory: string - creationBytecode: string - }> -} - -interface EtherscanTransactionResponse { - jsonrpc: string - id: number - result: { - blockHash: string - blockNumber: string - from: string - gas: string - gasPrice: string - hash: string - input: string - nonce: string - to: string - transactionIndex: string - value: string - type: string - v: string - r: string - s: string - } -} - -export const mirrorCommand = (program: Command) => { - program - .command("mirror") - .description("Mirror a contract from one chain to other chains") - .addOptions([mirrorOption, ...chainSelectionOptions]) - .argument("", "contract address to mirror") - .action(async (contractAddress, options) => { - const { fromChain, testnetAll, mainnetAll, allNetworks, chains } = - options - - if (!fromChain) { - console.error("Error: Source chain must be specified") - process.exit(1) - } - - if (!contractAddress) { - console.error("Error: Contract address must be specified") - process.exit(1) - } - - const targetChains = await processAndValidateChains({ - testnetAll, - mainnetAll, - allNetworks, - chainOption: chains - }) - - // Get the source chain object - const sourceChain = (await getSupportedChains()).find( - (chain) => chain.name.toLowerCase() === fromChain.toLowerCase() - ) - - if (!sourceChain) { - console.error(`Error: Source chain ${fromChain} not found`) - process.exit(1) - } - - // Fetch contract data from Etherscan - const creationResponse = await fetch( - `https://api.etherscan.io/v2/api?chainid=${sourceChain.id}&module=contract&action=getcontractcreation&contractaddresses=${contractAddress}&apikey=${sourceChain.explorerAPI}` - ) - - const creationData = - (await creationResponse.json()) as EtherscanContractCreationResponse - if (creationData.status !== "1" || !creationData.result?.[0]) { - console.error( - "Error: Failed to fetch contract data from Etherscan" - ) - process.exit(1) - } - - const contractData = creationData.result[0] - if (contractData.contractFactory !== DEPLOYER_CONTRACT_ADDRESS) { - console.error( - "Error: Contract was not deployed using the deterministic deployer" - ) - process.exit(1) - } - - // Since we can't get the salt from the internal transaction directly, - // we'll need to get it from the original transaction's input data - const originalTxResponse = await fetch( - `https://api.etherscan.io/v2/api?chainid=${sourceChain.id}&module=proxy&action=eth_getTransactionByHash&txhash=${contractData.txHash}&apikey=${sourceChain.explorerAPI}` - ) - - const originalTxData = - (await originalTxResponse.json()) as EtherscanTransactionResponse - if (!originalTxData.result?.input) { - console.error( - "Error: Could not get the original transaction data" - ) - process.exit(1) - } - - // Extract salt from input data (first 32 bytes after the function selector) - const salt = `0x${originalTxData.result.input.slice(2, 66)}` // Skip 0x and 4 bytes of function selector - - // Deploy the contract to target chains - await deployContracts( - validatePrivateKey(PRIVATE_KEY), - ensureHex(contractData.creationBytecode), - targetChains, - ensureHex(salt), - contractAddress, - undefined - ) - - console.log("✅ Contracts mirrored successfully!") - }) -}