Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
134 changes: 134 additions & 0 deletions src/action/mirror.ts
Original file line number Diff line number Diff line change
@@ -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
)
}
193 changes: 101 additions & 92 deletions src/command/index.ts
Original file line number Diff line number Diff line change
@@ -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"
Expand All @@ -11,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 {
Expand All @@ -22,21 +26,47 @@ import {
validateInputs,
validatePrivateKey
} from "../utils/index.js"
import {
chainSelectionOptions,
codeOptions,
deployOptions,
mirrorOption
} from "./options.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
}

const fileOption = [
"-f, --file <path-to-bytecode>",
"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]
// 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
}

export const program = new Command()

program
.name("zerodev")
.description(
"tool for deploying contracts to multichain with account abstraction"
)
.usage("<command> [options]")
.version("0.1.3")
.description("CLI tool for deploying contracts to multiple chains")
.version(version)

program.helpInformation = function () {
const asciiArt = chalk.blueBright(
Expand Down Expand Up @@ -77,12 +107,7 @@ program
program
.command("compute-address")
.description("Compute the address to be deployed")
.option(...fileOption)
.option("-b, --bytecode <bytecode>", "bytecode to deploy")
.option(
"-s, --salt <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

Expand Down Expand Up @@ -112,96 +137,59 @@ program

program
.command("deploy")
.description(
"Deploy contracts deterministically using CREATE2, in order of the chains specified"
)
.option(...fileOption)
.option("-b, --bytecode <bytecode>", "bytecode to deploy")
.option(
"-s, --salt <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 <call-gas-limit>", "gas limit for the call")
.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)
try {
validatePrivateKey(PRIVATE_KEY)
const chains = await getSupportedChains()
if (chains.length === 0) {
throw new Error("No chains selected")
}

validateInputs(file, bytecode, normalizedSalt, expectedAddress)
const chainObjects = await processAndValidateChains({
testnetAll,
mainnetAll,
allNetworks,
chainOption: chains
})
console.log(
"Selected chains:",
chains.map((chain) => chain.name).join(", ")
)

let bytecodeToDeploy = bytecode
if (file) {
bytecodeToDeploy = readBytecodeFromFile(file)
}
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")
}

await deployContracts(
validatePrivateKey(PRIVATE_KEY),
ensureHex(bytecodeToDeploy),
chainObjects,
ensureHex(normalizedSalt),
expectedAddress,
callGasLimit ? BigInt(callGasLimit) : undefined
)

console.log("✅ Contracts deployed successfully!")
if (!bytecode.startsWith("0x")) {
bytecode = `0x${bytecode}`
}

if (verifyContract) {
console.log("Verifying contracts on Etherscan...")
await verifyContracts(
verifyContract,
computeContractAddress(
DEPLOYER_CONTRACT_ADDRESS,
ensureHex(bytecodeToDeploy),
ensureHex(normalizedSalt)
),
chainObjects
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
.command("check-deployment")
.description(
"check whether the contract has already been deployed on the specified networks"
)
.option(...fileOption)
.option("-b, --bytecode <bytecode>", "deployed bytecode")
.option(
"-s, --salt <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,
Expand Down Expand Up @@ -269,3 +257,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>", "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()
Loading
Loading