diff --git a/src/command/index.ts b/src/command/index.ts index fcec63d..a0e7d79 100644 --- a/src/command/index.ts +++ b/src/command/index.ts @@ -6,256 +6,257 @@ import Table from "cli-table3" import { Command } from "commander" import figlet from "figlet" import { - computeContractAddress, - deployContracts, - findDeployment, - getDeployerAddress, - verifyContracts, + computeContractAddress, + deployContracts, + findDeployment, + getDeployerAddress, + verifyContracts } from "../action/index.js" import { PRIVATE_KEY } from "../config.js" import { DEPLOYER_CONTRACT_ADDRESS, getSupportedChains } from "../constant.js" import { - clearFiles, - ensureHex, - normalizeSalt, - processAndValidateChains, - readBytecodeFromFile, - validateInputs, - validatePrivateKey, + clearFiles, + ensureHex, + normalizeSalt, + processAndValidateChains, + readBytecodeFromFile, + validateInputs, + validatePrivateKey } from "../utils/index.js" export const program = new Command() 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.", + "-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] program - .name("zerodev") - .description( - "tool for deploying contracts to multichain with account abstraction" - ) - .usage(" [options]") - .version("1.0.0") + .name("zerodev") + .description( + "tool for deploying contracts to multichain with account abstraction" + ) + .usage(" [options]") + .version("1.0.0") program.helpInformation = function () { - const asciiArt = chalk.blueBright( - figlet.textSync("ZeroDev Orchestra", { - horizontalLayout: "default", - verticalLayout: "default", - width: 100, - whitespaceBreak: true, - }) - ) + const asciiArt = chalk.blueBright( + figlet.textSync("ZeroDev Orchestra", { + horizontalLayout: "default", + verticalLayout: "default", + width: 100, + whitespaceBreak: true + }) + ) - const originalHelpInformation = Command.prototype.helpInformation.call(this) - return `\n\n${asciiArt}\n\n\n${originalHelpInformation}` + const originalHelpInformation = Command.prototype.helpInformation.call(this) + return `\n\n${asciiArt}\n\n\n${originalHelpInformation}` } program - .command("chains") - .description("Show the list of available chains") - .action(() => { - const chains = getSupportedChains().map((chain) => [ - chain.name, - chain.type === "mainnet" - ? chalk.blue(chain.type) - : chalk.green(chain.type), - ]) + .command("chains") + .description("Show the list of available chains") + .action(() => { + const chains = getSupportedChains().map((chain) => [ + chain.name, + chain.type === "mainnet" + ? chalk.blue(chain.type) + : chalk.green(chain.type) + ]) - const table = new Table({ - head: ["Name", "Type"], - chars: { mid: "", "left-mid": "", "mid-mid": "", "right-mid": "" }, - }) + const table = new Table({ + head: ["Name", "Type"], + chars: { mid: "", "left-mid": "", "mid-mid": "", "right-mid": "" } + }) - for (const chain of chains) { - table.push(chain) - } + for (const chain of chains) { + table.push(chain) + } - console.log("[Available chains]") - console.log(table.toString()) - }) + console.log("[Available chains]") + console.log(table.toString()) + }) 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." - ) - .action(async (options) => { - const { file, bytecode, salt } = options + .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." + ) + .action(async (options) => { + const { file, bytecode, salt } = options - const normalizedSalt = normalizeSalt(salt) - validateInputs(file, bytecode, normalizedSalt, undefined) + const normalizedSalt = normalizeSalt(salt) + validateInputs(file, bytecode, normalizedSalt, undefined) - let bytecodeToDeploy = bytecode - if (file) { - bytecodeToDeploy = readBytecodeFromFile(file) - } + let bytecodeToDeploy = bytecode + if (file) { + bytecodeToDeploy = readBytecodeFromFile(file) + } - const address = computeContractAddress( - DEPLOYER_CONTRACT_ADDRESS, - ensureHex(bytecodeToDeploy), - ensureHex(normalizedSalt) - ) - console.log(`computed address: ${address}`) - }) + const address = computeContractAddress( + DEPLOYER_CONTRACT_ADDRESS, + ensureHex(bytecodeToDeploy), + ensureHex(normalizedSalt) + ) + console.log(`computed address: ${address}`) + }) program - .command("get-deployer-address") - .description("Get the deployer's address") - .action(async () => { - const address = getDeployerAddress(validatePrivateKey(PRIVATE_KEY), 0n) - console.log(`deployer address: ${address}`) - }) + .command("get-deployer-address") + .description("Get the deployer's address") + .action(async () => { + const address = getDeployerAddress(validatePrivateKey(PRIVATE_KEY), 0n) + console.log(`deployer address: ${address}`) + }) program - .command("deploy") - .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", true) - .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, defaults to all testnets" - ) - .option("-e, --expected-address [ADDRESS]", "expected address to confirm") - .option( - "-v, --verify-contract [CONTRACT_NAME]", - "verify the deployment on Etherscan" - ) - .action(async (options) => { - const { - file, - bytecode, - salt, - testnetAll, - mainnetAll, - allNetworks, - chains, - expectedAddress, - verifyContract, - } = options + .command("deploy") + .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", true) + .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, defaults to all testnets" + ) + .option("-e, --expected-address [ADDRESS]", "expected address to confirm") + .option( + "-v, --verify-contract [CONTRACT_NAME]", + "verify the deployment on Etherscan" + ) + .action(async (options) => { + const { + file, + bytecode, + salt, + testnetAll, + mainnetAll, + allNetworks, + chains, + expectedAddress, + verifyContract + } = options - const normalizedSalt = normalizeSalt(salt) + const normalizedSalt = normalizeSalt(salt) - validateInputs(file, bytecode, normalizedSalt, expectedAddress) - const chainObjects = processAndValidateChains(chains, { - testnetAll, - mainnetAll, - allNetworks, - }) + validateInputs(file, bytecode, normalizedSalt, expectedAddress) + const chainObjects = processAndValidateChains(chains, { + testnetAll, + mainnetAll, + allNetworks + }) - let bytecodeToDeploy = bytecode - if (file) { - bytecodeToDeploy = readBytecodeFromFile(file) - } + let bytecodeToDeploy = bytecode + if (file) { + bytecodeToDeploy = readBytecodeFromFile(file) + } - await deployContracts( - validatePrivateKey(PRIVATE_KEY), - ensureHex(bytecodeToDeploy), - chainObjects, - ensureHex(normalizedSalt), - expectedAddress - ) + await deployContracts( + validatePrivateKey(PRIVATE_KEY), + ensureHex(bytecodeToDeploy), + chainObjects, + ensureHex(normalizedSalt), + expectedAddress + ) - if (verifyContract) { - await verifyContracts( - verifyContract, - computeContractAddress( - DEPLOYER_CONTRACT_ADDRESS, - ensureHex(bytecodeToDeploy), - ensureHex(normalizedSalt) - ), - chainObjects - ) - } - }) + if (verifyContract) { + await verifyContracts( + verifyContract, + computeContractAddress( + DEPLOYER_CONTRACT_ADDRESS, + ensureHex(bytecodeToDeploy), + ensureHex(normalizedSalt) + ), + chainObjects + ) + } + }) program - .command("check-deployment") - .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( - "-c, --chains [CHAINS]", - "list of chains to check, with all selected by default", - "all" - ) + .command("check-deployment") + .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( + "-c, --chains [CHAINS]", + "list of chains to check, with all selected by default", + "all" + ) - .option("-t, --testnet-all", "check all testnets", false) - .option("-m, --mainnet-all", "check all mainnets", false) - .action(async (options) => { - const { file, bytecode, salt, chains, testnetAll, mainnetAll } = options + .option("-t, --testnet-all", "check all testnets", false) + .option("-m, --mainnet-all", "check all mainnets", false) + .action(async (options) => { + const { file, bytecode, salt, chains, testnetAll, mainnetAll } = options - const normalizedSalt = normalizeSalt(salt) - validateInputs(file, bytecode, normalizedSalt, undefined) - const chainObjects = processAndValidateChains(chains, { - testnetAll, - mainnetAll, - }) + const normalizedSalt = normalizeSalt(salt) + validateInputs(file, bytecode, normalizedSalt, undefined) + const chainObjects = processAndValidateChains(chains, { + testnetAll, + mainnetAll + }) - let bytecodeToDeploy = bytecode - if (file) { - bytecodeToDeploy = readBytecodeFromFile(file) - } + let bytecodeToDeploy = bytecode + if (file) { + bytecodeToDeploy = readBytecodeFromFile(file) + } - const { address, deployedChains, notDeployedChains } = await findDeployment( - ensureHex(bytecodeToDeploy), - ensureHex(normalizedSalt), - chainObjects - ) + const { address, deployedChains, notDeployedChains } = + await findDeployment( + ensureHex(bytecodeToDeploy), + ensureHex(normalizedSalt), + chainObjects + ) - console.log(`contract address: ${address}`) - console.log("deployed on:") - for (const chain of deployedChains) { - console.log(`- ${chain.name}`) - } - console.log("not deployed on:") - for (const chain of notDeployedChains) { - console.log(`- ${chain.name}`) - } - }) + console.log(`contract address: ${address}`) + console.log("deployed on:") + for (const chain of deployedChains) { + console.log(`- ${chain.name}`) + } + console.log("not deployed on:") + for (const chain of notDeployedChains) { + console.log(`- ${chain.name}`) + } + }) program - .command("clear-log") - .description("clear the log files") - .action(() => { - clearFiles("./log") - console.log("✅ Log files are cleared!") - }) + .command("clear-log") + .description("clear the log files") + .action(() => { + clearFiles("./log") + console.log("✅ Log files are cleared!") + }) program - .command("generate-salt") - .description( - "generate a random 32 bytes salt, or convert the numeric input to salt" - ) - .option("-i, --input ", "input to convert to salt") - .action((options) => { - let salt: string - if (options.input) { - const inputNum = BigInt(options.input) - salt = inputNum.toString(16).padStart(64, "0") // pad the input with zeros to make it 32 bytes - } else { - salt = crypto.randomBytes(32).toString("hex") - } - console.log(`Generated salt: ${ensureHex(salt)}`) - }) + .command("generate-salt") + .description( + "generate a random 32 bytes salt, or convert the numeric input to salt" + ) + .option("-i, --input ", "input to convert to salt") + .action((options) => { + let salt: string + if (options.input) { + const inputNum = BigInt(options.input) + salt = inputNum.toString(16).padStart(64, "0") // pad the input with zeros to make it 32 bytes + } else { + salt = crypto.randomBytes(32).toString("hex") + } + console.log(`Generated salt: ${ensureHex(salt)}`) + }) diff --git a/src/utils/validate.ts b/src/utils/validate.ts index 71e4d7f..3ec1941 100644 --- a/src/utils/validate.ts +++ b/src/utils/validate.ts @@ -8,130 +8,132 @@ const SALT_REGEX = /^0x[0-9a-fA-F]{64}$/ const ADDRESS_REGEX = /^0x[0-9a-fA-F]{40}$/ export const validatePrivateKey = (privateKey: Hex | null): Hex => { - if (!privateKey) { - console.error("Error: This command requires a private key") - process.exit(1) - } - if (!PRIVATE_KEY_REGEX.test(privateKey)) { - console.error("Error: Private key must be a 32 bytes hex string") - process.exit(1) - } - return privateKey + if (!privateKey) { + console.error("Error: This command requires a private key") + process.exit(1) + } + if (!PRIVATE_KEY_REGEX.test(privateKey)) { + console.error("Error: Private key must be a 32 bytes hex string") + process.exit(1) + } + return privateKey } export const validateInputs = ( - filePath: string | undefined, - bytecode: string | undefined, - salt: string, - expectedAddress: string | undefined + filePath: string | undefined, + bytecode: string | undefined, + salt: string, + expectedAddress: string | undefined ) => { - if (!filePath && !bytecode) { - console.error("Error: Either filePath or bytecode must be specified") - process.exit(1) - } - - if (filePath && bytecode) { - console.error("Error: Only one of filePath and bytecode can be specified") - process.exit(1) - } - - const bytecodeToValidate = filePath - ? readBytecodeFromFile(filePath) - : bytecode - - if (!bytecodeToValidate) { - console.error("Error: Bytecode must be specified") - process.exit(1) - } - - if ( - !BYTECODE_REGEX.test(bytecodeToValidate) || - bytecodeToValidate.length % 2 !== 0 - ) { - console.error("Error: Bytecode must be a hexadecimal string") - process.exit(1) - } - - if (!SALT_REGEX.test(salt)) { - console.error("Error: Salt must be a 32 bytes hex string") - process.exit(1) - } - - if (expectedAddress && !ADDRESS_REGEX.test(expectedAddress)) { - console.error("Error: Expected address must be a 20 bytes hex string") - process.exit(1) - } + if (!filePath && !bytecode) { + console.error("Error: Either filePath or bytecode must be specified") + process.exit(1) + } + + if (filePath && bytecode) { + console.error( + "Error: Only one of filePath and bytecode can be specified" + ) + process.exit(1) + } + + const bytecodeToValidate = filePath + ? readBytecodeFromFile(filePath) + : bytecode + + if (!bytecodeToValidate) { + console.error("Error: Bytecode must be specified") + process.exit(1) + } + + if ( + !BYTECODE_REGEX.test(bytecodeToValidate) || + bytecodeToValidate.length % 2 !== 0 + ) { + console.error("Error: Bytecode must be a hexadecimal string") + process.exit(1) + } + + if (!SALT_REGEX.test(salt)) { + console.error("Error: Salt must be a 32 bytes hex string") + process.exit(1) + } + + if (expectedAddress && !ADDRESS_REGEX.test(expectedAddress)) { + console.error("Error: Expected address must be a 20 bytes hex string") + process.exit(1) + } } interface CommandOptions { - testnetAll?: boolean - mainnetAll?: boolean - allNetworks?: boolean + testnetAll?: boolean + mainnetAll?: boolean + allNetworks?: boolean } export const processAndValidateChains = ( - chainOption: string | undefined, - options: CommandOptions + chainOption: string | undefined, + options: CommandOptions ): Chain[] => { - const supportedChains = getSupportedChains() - if ( - chainOption !== undefined && - (options.testnetAll || options.mainnetAll || options.allNetworks) - ) { - console.error("Error: Cannot use -c option with -t, -m, -a options") - process.exit(1) - } - - if (options.testnetAll && options.mainnetAll) { - console.error("Error: Cannot use -t and -m options together") - process.exit(1) - } - - if (options.testnetAll && options.allNetworks) { - console.error("Error: Cannot use -t and -a options together") - process.exit(1) - } - - if (options.mainnetAll && options.allNetworks) { - console.error("Error: Cannot use -m and -a options together") - process.exit(1) - } - - let chains: string[] - if (options.testnetAll) { - chains = supportedChains - .filter((chain) => chain.type === "testnet") - .map((chain) => chain.name) - } else if (options.mainnetAll) { - chains = supportedChains - .filter((chain) => chain.type === "mainnet") - .map((chain) => chain.name) - } else if (options.allNetworks) { - chains = supportedChains.map((chain) => chain.name) - } else { - chains = chainOption ? chainOption.split(",") : [] - } - - const chainObjects: UnvalidatedChain[] = chains.map((chainName: string) => { - const chain = supportedChains.find((c) => c.name === chainName) - if (!chain) { - console.error(`Error: Chain ${chainName} is not supported`) - process.exit(1) + const supportedChains = getSupportedChains() + if ( + chainOption !== undefined && + (options.testnetAll || options.mainnetAll || options.allNetworks) + ) { + console.error("Error: Cannot use -c option with -t, -m, -a options") + process.exit(1) + } + + if (options.testnetAll && options.mainnetAll) { + console.error("Error: Cannot use -t and -m options together") + process.exit(1) + } + + if (options.testnetAll && options.allNetworks) { + console.error("Error: Cannot use -t and -a options together") + process.exit(1) } - return chain - }) - return validateChains(chainObjects) + if (options.mainnetAll && options.allNetworks) { + console.error("Error: Cannot use -m and -a options together") + process.exit(1) + } + + let chains: string[] + if (options.testnetAll) { + chains = supportedChains + .filter((chain) => chain.type === "testnet") + .map((chain) => chain.name) + } else if (options.mainnetAll) { + chains = supportedChains + .filter((chain) => chain.type === "mainnet") + .map((chain) => chain.name) + } else if (options.allNetworks) { + chains = supportedChains.map((chain) => chain.name) + } else { + chains = chainOption ? chainOption.split(",") : [] + } + + const chainObjects: UnvalidatedChain[] = chains.map((chainName: string) => { + const chain = supportedChains.find((c) => c.name === chainName) + if (!chain) { + console.error(`Error: Chain ${chainName} is not supported`) + process.exit(1) + } + return chain + }) + + return validateChains(chainObjects) } const validateChains = (chains: UnvalidatedChain[]): Chain[] => { - return chains.map((chain) => { - if (!chain.projectId) { - console.error( - `Error: PROJECT_ID for chain ${chain.name} is not specified` - ) - process.exit(1) - } - return chain as Chain - }) + return chains.map((chain) => { + if (!chain.projectId) { + console.error( + `Error: PROJECT_ID for chain ${chain.name} is not specified` + ) + process.exit(1) + } + return chain as Chain + }) }