diff --git a/.env.example b/.env.example index 53774dd..d24bc40 100644 --- a/.env.example +++ b/.env.example @@ -2,61 +2,7 @@ PRIVATE_KEY="PRIVATE_KEY" -# Set up zerodev project id for each network you want to deploy on -# Sign up for project IDs here: https://dashboard.zerodev.app/ +ZERODEV_API_KEY="" +ZERODEV_PROJECT_ID="" -ARBITRUM_PROJECT_ID="" -ARBITRUM_NOVA_PROJECT_ID="" -ARBITRUM_SEPOLIA_PROJECT_ID="" -ASTAR_ZKEVM_PROJECT_ID="" -ASTAR_ZKYOTO_PROJECT_ID="" -AVALANCHE_PROJECT_ID="" -AVALANCHE_FUJI_PROJECT_ID="" -BASE_PROJECT_ID="" -BASE_SEPOLIA_PROJECT_ID="" -BLAST_PROJECT_ID="" -BLAST_SEPOLIA_PROJECT_ID="" -BSC_PROJECT_ID="" -CELO_PROJECT_ID="" -CELO_ALFAJORES_PROJECT_ID="" -CYBER_PROJECT_ID="" -CYBER_TESTNET_PROJECT_ID="" -DEGEN_PROJECT_ID="" -MAINNET_PROJECT_ID="" -SEPOILA_PROJECT_ID="" -OPTIMISM_PROJECT_ID="" -OPTIMISM_SEPOLIA_PROJECT_ID="" -OPBNB_PROJECT_ID="" -POLYGON_PROJECT_ID="" -POLYGON_AMOY_PROJECT_ID="" -LINEA_PROJECT_ID="" -LINEA_TESTNET_PROJECT_ID="" - -# Set up etherscan API key for each network you want to verify on - -ARBITRUM_ETHERSCAN_API_KEY="" -ARBITRUM_NOVA_ETHERSCAN_API_KEY="" -ARBITRUM_SEPOLIA_ETHERSCAN_API_KEY="" -ASTAR_ZKEVM_ETHERSCAN_API_KEY="" -ASTAR_ZKYOTO_ETHERSCAN_API_KEY="" -AVALANCHE_ETHERSCAN_API_KEY="" -AVALANCHE_FUJI_ETHERSCAN_API_KEY="" -BASE_ETHERSCAN_API_KEY="" -BASE_SEPOLIA_ETHERSCAN_API_KEY="" -BLAST_ETHERSCAN_API_KEY="" -BLAST_SEPOLIA_ETHERSCAN_API_KEY="" -BSC_ETHERSCAN_API_KEY="" -CELO_ETHERSCAN_API_KEY="" -CELO_ALFAJORES_ETHERSCAN_API_KEY="" -CYBER_ETHERSCAN_API_KEY="" -CYBER_TESTNET_ETHERSCAN_API_KEY="" -DEGEN_ETHERSCAN_API_KEY="" -MAINNET_ETHERSCAN_API_KEY="" -SEPOILA_ETHERSCAN_API_KEY="" -OPTIMISM_ETHERSCAN_API_KEY="" -OPTIMISM_SEPOLIA_ETHERSCAN_API_KEY="" -OPBNB_ETHERSCAN_API_KEY="" -POLYGON_ETHERSCAN_API_KEY="" -POLYGON_AMOY_ETHERSCAN_API_KEY="" -LINEA_ETHERSCAN_API_KEY="" -LINEA_TESTNET_ETHERSCAN_API_KEY="" \ No newline at end of file +ETHERSCAN_API_KEY="" \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..f5ebf55 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,7 @@ +# @zerodev/orchestra + +## 0.2.0 + +### Minor Changes + +- update to use zerodev v3 api diff --git a/README.md b/README.md index bfef5e9..cb65d71 100644 --- a/README.md +++ b/README.md @@ -7,6 +7,7 @@ Orchestra is a CLI for deterministically deploying contracts to multiple chains, 1. `npm install -g @zerodev/orchestra` 2. Create a `.env` file based on `.env.example` - You can acquire the project IDs from [the ZeroDev dashboard](https://dashboard.zerodev.app/) + - Use [this link](https://dashboard.zerodev.app/account/api-key) to get the API key for the team 3. Test the installation by running `zerodev -h` ## Usage diff --git a/bun.lockb b/bun.lockb index 5c3614c..dd8ac52 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/package.json b/package.json index 996003c..be8b800 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@zerodev/orchestra", - "version": "0.1.3", + "version": "0.2.0", "description": "", "main": "dist/index.js", "type": "module", @@ -43,29 +43,30 @@ ], "license": "MIT", "dependencies": { - "@zerodev/ecdsa-validator": "^5.4.0", - "@zerodev/sdk": "^5.4.3", + "@zerodev/ecdsa-validator": "^5.4.9", + "@zerodev/sdk": "^5.4.36", "chalk": "4.1.2", - "cli-table3": "^0.6.3", + "cli-table3": "^0.6.5", "commander": "^11.1.0", - "dotenv": "^16.3.1", - "figlet": "^1.7.0", - "ora": "^8.0.1", - "tslib": "^2.6.2" + "dotenv": "^16.5.0", + "figlet": "^1.8.1", + "ora": "^8.2.0", + "tslib": "^2.8.1", + "viem": "^2.30.6" }, "devDependencies": { - "@biomejs/biome": "^1.4.1", + "@biomejs/biome": "^1.9.4", "@changesets/changelog-git": "^0.1.14", "@changesets/changelog-github": "^0.4.8", - "@changesets/cli": "^2.27.1", + "@changesets/cli": "^2.29.4", "@size-limit/esbuild-why": "^9.0.0", "@size-limit/preset-small-lib": "^9.0.0", - "@types/figlet": "^1.5.8", - "@types/node": "^18.19.4", + "@types/figlet": "^1.7.0", + "@types/node": "^18.19.110", "@types/ora": "^3.2.0", - "simple-git-hooks": "^2.9.0", + "simple-git-hooks": "^2.13.0", "ts-node": "^10.9.2", - "typescript": "^5.2.2" + "typescript": "^5.8.3" }, "simple-git-hooks": { "pre-commit": "bun run format && bun run lint" diff --git a/src/action/deployContracts.ts b/src/action/deployContracts.ts index cc7ea79..0545c1f 100644 --- a/src/action/deployContracts.ts +++ b/src/action/deployContracts.ts @@ -3,7 +3,7 @@ import ora from "ora" import type { Address, Hex } from "viem" import { http, createPublicClient, getAddress } from "viem" import { createKernelClient, getZeroDevBundlerRPC } from "../clients/index.js" -import { type Chain, DEPLOYER_CONTRACT_ADDRESS } from "../constant.js" +import { DEPLOYER_CONTRACT_ADDRESS, type ZerodevChain } from "../constant.js" import { ensureHex, writeErrorLogToFile } from "../utils/index.js" import { computeContractAddress } from "./computeAddress.js" import { DeploymentStatus, checkDeploymentOnChain } from "./findDeployment.js" @@ -21,15 +21,15 @@ type DeployResult = [string, string] export const deployToChain = async ( privateKey: Hex, - chain: Chain, + chain: ZerodevChain, bytecode: Hex, salt: Hex, expectedAddress: string | undefined, callGasLimit: bigint | undefined ): Promise => { const publicClient = createPublicClient({ - chain: chain.viemChainObject, - transport: http(getZeroDevBundlerRPC(chain.projectId)) + chain: chain, + transport: http() }) const kernelAccountClient = await createKernelClient(privateKey, chain) @@ -86,13 +86,20 @@ export const deployToChain = async ( ]) }) + await kernelAccountClient.waitForUserOperationReceipt({ + hash: opHash + }) + await kernelAccountClient.getUserOperationReceipt({ + hash: opHash + }) + return [getAddress(result.data as Address), opHash] } export const deployContracts = async ( privateKey: Hex, bytecode: Hex, - chains: Chain[], + chains: ZerodevChain[], salt: Hex, expectedAddress: string | undefined, callGasLimit: bigint | undefined @@ -100,6 +107,7 @@ export const deployContracts = async ( const spinner = ora( `Deploying contract on ${chains.map((chain) => chain.name).join(", ")}` ).start() + let anyError = false const deployments = chains.map(async (chain) => { return deployToChain( privateKey, @@ -130,6 +138,7 @@ export const deployContracts = async ( ) } else { writeErrorLogToFile(chain.name, error) + anyError = true spinner.fail( `Deployment for ${chalk.redBright( chain.name @@ -141,5 +150,10 @@ export const deployContracts = async ( await Promise.allSettled(deployments) spinner.stop() - console.log("✅ All deployments process successfully finished!") + if (anyError) { + console.log("❌ Some deployments failed!") + process.exit(1) + } else { + console.log("✅ All deployments process successfully finished!") + } } diff --git a/src/action/findDeployment.ts b/src/action/findDeployment.ts index bd746fa..963718b 100644 --- a/src/action/findDeployment.ts +++ b/src/action/findDeployment.ts @@ -1,7 +1,7 @@ import type { Address, Hex, PublicClient } from "viem" import { http, createPublicClient } from "viem" import { getZeroDevBundlerRPC } from "../clients/index.js" -import { type Chain, DEPLOYER_CONTRACT_ADDRESS } from "../constant.js" +import { DEPLOYER_CONTRACT_ADDRESS, type ZerodevChain } from "../constant.js" import { computeContractAddress } from "./computeAddress.js" export enum DeploymentStatus { Deployed = 0, @@ -29,12 +29,12 @@ export const checkDeploymentOnChain = async ( export const findDeployment = async ( bytecode: Hex, salt: Hex, - chains: Chain[] + chains: ZerodevChain[] ): Promise<{ address: Address - deployedChains: Chain[] - notDeployedChains: Chain[] - errorChains?: Chain[] + deployedChains: ZerodevChain[] + notDeployedChains: ZerodevChain[] + errorChains?: ZerodevChain[] }> => { const address = computeContractAddress( DEPLOYER_CONTRACT_ADDRESS, @@ -46,7 +46,8 @@ export const findDeployment = async ( chains.map((chain) => { return checkDeploymentOnChain( createPublicClient({ - transport: http(getZeroDevBundlerRPC(chain.projectId)) + chain: chain, + transport: http() }), address ).catch(() => DeploymentStatus.Error) diff --git a/src/action/verifyContracts.ts b/src/action/verifyContracts.ts index 7d3058b..0ea21b9 100644 --- a/src/action/verifyContracts.ts +++ b/src/action/verifyContracts.ts @@ -3,7 +3,7 @@ import util from "node:util" import chalk from "chalk" import ora from "ora" import type { Address } from "viem" -import type { Chain } from "../constant.js" +import type { ZerodevChain } from "../constant.js" const execPromise = util.promisify(exec) @@ -20,31 +20,16 @@ async function checkForgeAvailability() { async function verifyContract( contractName: string, contractAddress: Address, - chain: Chain + chain: ZerodevChain ): Promise { - if ( - !chain.etherscanApiKey && - chain.name !== "avalanche" && - chain.name !== "avalanche-fuji" && - chain.name !== "opbnb" && - chain.name !== "astar-zkatana" - ) { + if (!chain.explorerAPI) { throw new Error( - `Etherscan API key is not provided for ${chalk.yellowBright( + `Explorer API key is not provided for ${chalk.yellowBright( chain.name )}` ) } - - if (["opbnb", "astar-zkatana"].includes(chain.name)) { - throw new Error( - `Verification is not supported on ${chalk.yellowBright(chain.name)}` - ) - } - - const effectiveChainName = - chain.name === "linea-testnet" ? "linea-goerli" : chain.name - const command = `forge verify-contract -c ${effectiveChainName} ${contractAddress} ${contractName} -e ${chain.etherscanApiKey}` + const command = `forge verify-contract --chain ${chain.id} --verifier etherscan ${contractAddress} ${contractName} -e ${chain.explorerAPI} -a v2` try { const { stdout, stderr } = await execPromise(command) @@ -69,12 +54,11 @@ async function verifyContract( export const verifyContracts = async ( contractName: string, contractAddress: Address, - chains: Chain[] + chains: ZerodevChain[] ) => { await checkForgeAvailability() - const spinner = ora().start("Verifying contracts...") - + let anyError = false const verificationPromises = chains.map((chain) => verifyContract(contractName, contractAddress, chain) .then((message) => { @@ -85,7 +69,8 @@ export const verifyContracts = async ( } }) .catch((error) => { - ora() + anyError = true + return ora() .fail( `Verification failed on ${chain.name}: ${error.message}` ) @@ -93,10 +78,13 @@ export const verifyContracts = async ( .stop() }) ) - // Wait for all verifications to complete await Promise.all(verificationPromises) - spinner.stop() - console.log("✅ All verifications process successfully finished!") + if (anyError) { + console.log("❌ Some verifications failed!") + process.exit(1) + } else { + console.log("✅ All verifications process successfully finished!") + } } diff --git a/src/clients/createKernelClient.ts b/src/clients/createKernelClient.ts index 340c4ec..c723a2e 100644 --- a/src/clients/createKernelClient.ts +++ b/src/clients/createKernelClient.ts @@ -5,23 +5,26 @@ import { createKernelAccountClient, createZeroDevPaymasterClient } from "@zerodev/sdk" +import { getUserOperationGasPrice } from "@zerodev/sdk" import { KERNEL_V3_1, getEntryPoint } from "@zerodev/sdk/constants" import type { Hex } from "viem" + import { http, createPublicClient } from "viem" import { privateKeyToAccount } from "viem/accounts" -import type { Chain } from "../constant.js" +import type { ZerodevChain } from "../constant.js" import { getZeroDevBundlerRPC, getZeroDevPaymasterRPC } from "./index.js" export const createKernelClient = async ( privateKey: Hex, - chain: Chain + chain: ZerodevChain ): Promise => { - const rpcUrl = getZeroDevBundlerRPC(chain.projectId, "PIMLICO") - const paymasterRpcUrl = getZeroDevPaymasterRPC(chain.projectId, "PIMLICO") + const rpcUrl = getZeroDevBundlerRPC(chain.id, "PIMLICO") + const paymasterRpcUrl = getZeroDevPaymasterRPC(chain.id, "PIMLICO") const entryPoint = getEntryPoint("0.7") + const publicClient = createPublicClient({ - transport: http(rpcUrl), - chain: chain.viemChainObject + chain: chain, + transport: http(chain.rpcUrls.default.http[0]) }) const signer = privateKeyToAccount(privateKey) @@ -41,13 +44,14 @@ export const createKernelClient = async ( }) const zerodevPaymaster = createZeroDevPaymasterClient({ - chain: chain.viemChainObject, + chain: chain, transport: http(paymasterRpcUrl) }) // Construct a Kernel account client const kernelClient = createKernelAccountClient({ - account, - chain: chain.viemChainObject, + account: account, + client: publicClient, + chain: chain, bundlerTransport: http(rpcUrl), paymaster: { getPaymasterData(userOperation) { diff --git a/src/clients/index.ts b/src/clients/index.ts index 8746f8e..a9d8919 100644 --- a/src/clients/index.ts +++ b/src/clients/index.ts @@ -1,18 +1,18 @@ export const getZeroDevBundlerRPC = ( - projectId: string, + chainId: number, provider?: string ): string => { - let rpc = `https://rpc.zerodev.app/api/v2/bundler/${projectId}` + let rpc = `https://rpc.zerodev.app/api/v3/${process.env.ZERODEV_PROJECT_ID}/chain/${chainId}` if (provider) { rpc += `?provider=${provider}` } return rpc } export const getZeroDevPaymasterRPC = ( - projectId: string, + chainId: number, provider?: string ): string => { - let rpc = `https://rpc.zerodev.app/api/v2/paymaster/${projectId}` + let rpc = `https://rpc.zerodev.app/api/v3/${process.env.ZERODEV_PROJECT_ID}/chain/${chainId}` if (provider) { rpc += `?provider=${provider}` } diff --git a/src/command/index.ts b/src/command/index.ts index bfcbecb..2eb70a4 100644 --- a/src/command/index.ts +++ b/src/command/index.ts @@ -55,12 +55,10 @@ program.helpInformation = function () { program .command("chains") .description("Show the list of available chains") - .action(() => { - const chains = getSupportedChains().map((chain) => [ + .action(async () => { + const chains = (await getSupportedChains()).map((chain) => [ chain.name, - chain.type === "mainnet" - ? chalk.blue(chain.type) - : chalk.green(chain.type) + chain.testnet ? chalk.green("testnet") : chalk.blue("mainnet") ]) const table = new Table({ @@ -150,10 +148,11 @@ program const normalizedSalt = normalizeSalt(salt) validateInputs(file, bytecode, normalizedSalt, expectedAddress) - const chainObjects = processAndValidateChains(chains, { + const chainObjects = await processAndValidateChains({ testnetAll, mainnetAll, - allNetworks + allNetworks, + chainOption: chains }) let bytecodeToDeploy = bytecode @@ -216,10 +215,11 @@ program const normalizedSalt = normalizeSalt(salt) validateInputs(file, bytecode, normalizedSalt, undefined) - const chainObjects = processAndValidateChains(chains, { + const chainObjects = await processAndValidateChains({ testnetAll, mainnetAll, - allNetworks + allNetworks, + chainOption: chains }) let bytecodeToDeploy = bytecode diff --git a/src/constant.ts b/src/constant.ts index fbb546f..6ff8851 100644 --- a/src/constant.ts +++ b/src/constant.ts @@ -1,249 +1,141 @@ -import { - type Chain as ViemChain, - arbitrum, - arbitrumNova, - arbitrumSepolia, - astarZkEVM, - astarZkyoto, - avalanche, - avalancheFuji, - base, - baseSepolia, - blast, - blastSepolia, - bsc, - celo, - celoAlfajores, - cyber, - cyberTestnet, - degen, - linea, - lineaTestnet, - mainnet, - opBNB, - optimism, - optimismSepolia, - polygon, - polygonAmoy, - sepolia -} from "viem/chains" +import type { Chain } from "viem/chains" /** @dev deterministic-deployment-proxy contract address */ export const DEPLOYER_CONTRACT_ADDRESS = "0x4e59b44847b379578588920ca78fbf26c0b4956c" -export enum Network { - mainnet = "mainnet", - testnet = "testnet" -} +export type ZerodevChain = { + onlySelfFunded: boolean + rollupProvider: string | null + deprecated: boolean + explorerAPI: string | null +} & Chain -export interface UnvalidatedChain { +interface ZerodevChainResponse { + chainId: number name: string - projectId: string | null - etherscanApiKey?: string - viemChainObject: ViemChain - type: Network + nativeCurrencyName: string + nativeCurrencySymbol: string + nativeCurrencyDecimals: number + rpcUrl: string + explorerUrl: string + testnet: boolean + onlySelfFunded: boolean + rollupProvider: string | null + deprecated: boolean } -export interface Chain { +interface ZerodevProjectResponse { + id: string name: string - projectId: string - etherscanApiKey?: string - viemChainObject: ViemChain - type: Network + teamId: string + chains: { + chain_id: number + name: string + testnet: boolean + }[] } -export const getSupportedChains = (): UnvalidatedChain[] => [ - { - name: "arbitrum", - projectId: process.env.ARBITRUM_PROJECT_ID || null, - etherscanApiKey: process.env.ARBITRUM_ETHERSCAN_API_KEY || undefined, - viemChainObject: arbitrum, - type: Network.mainnet - }, - { - name: "arbitrum-nova", - projectId: process.env.ARBITRUM_NOVA_PROJECT_ID || null, - etherscanApiKey: - process.env.ARBITRUM_NOVA_ETHERSCAN_API_KEY || undefined, - viemChainObject: arbitrumNova, - type: Network.mainnet - }, - { - name: "arbitrum-sepolia", - projectId: process.env.ARBITRUM_SEPOLIA_PROJECT_ID || null, - etherscanApiKey: - process.env.ARBITRUM_SEPOLIA_ETHERSCAN_API_KEY || undefined, - viemChainObject: arbitrumSepolia, - type: Network.testnet - }, - { - name: "astar-zkEVM", - projectId: process.env.ASTAR_ZKEVM_PROJECT_ID || null, - etherscanApiKey: process.env.ASTAR_ZKEVM_ETHERSCAN_API_KEY || undefined, - viemChainObject: astarZkEVM, - type: Network.mainnet - }, - { - name: "astar-zkyoto", - projectId: process.env.ASTAR_ZKYOTO_PROJECT_ID || null, - etherscanApiKey: - process.env.ASTAR_ZKYOTO_ETHERSCAN_API_KEY || undefined, - viemChainObject: astarZkyoto, - type: Network.testnet - }, - { - name: "avalanche", - projectId: process.env.AVALANCHE_PROJECT_ID || null, - etherscanApiKey: process.env.AVALANCHE_ETHERSCAN_API_KEY || undefined, - viemChainObject: avalanche, - type: Network.mainnet - }, - { - name: "avalanche-fuji", - projectId: process.env.AVALANCHE_FUJI_PROJECT_ID || null, - etherscanApiKey: - process.env.AVALANCHE_FUJI_ETHERSCAN_API_KEY || undefined, - viemChainObject: avalancheFuji, - type: Network.testnet - }, - { - name: "base", - projectId: process.env.BASE_PROJECT_ID || null, - etherscanApiKey: process.env.BASE_ETHERSCAN_API_KEY || undefined, - viemChainObject: base, - type: Network.mainnet - }, - { - name: "base-sepolia", - projectId: process.env.BASE_SEPOLIA_PROJECT_ID || null, - etherscanApiKey: - process.env.BASE_SEPOLIA_ETHERSCAN_API_KEY || undefined, - viemChainObject: baseSepolia, - type: Network.testnet - }, - { - name: "blast", - projectId: process.env.BLAST_PROJECT_ID || null, - etherscanApiKey: process.env.BLAST_ETHERSCAN_API_KEY || undefined, - viemChainObject: blast, - type: Network.mainnet - }, - { - name: "blast-sepolia", - projectId: process.env.BLAST_SEPOLIA_PROJECT_ID || null, - etherscanApiKey: process.env.BLAST_ETHERSCAN_API_KEY || undefined, - viemChainObject: blastSepolia, - type: Network.testnet - }, - { - name: "bsc", - projectId: process.env.BSC_PROJECT_ID || null, - etherscanApiKey: process.env.BSC_ETHERSCAN_API_KEY || undefined, - viemChainObject: bsc, - type: Network.mainnet - }, - { - name: "celo", - projectId: process.env.CELO_PROJECT_ID || null, - etherscanApiKey: process.env.CELO_ETHERSCAN_API_KEY || undefined, - viemChainObject: celo, - type: Network.mainnet - }, - { - name: "celo-alfajores", - projectId: process.env.CELO_ALFAJORES_PROJECT_ID || null, - etherscanApiKey: - process.env.CELO_ALFAJORES_ETHERSCAN_API_KEY || undefined, - viemChainObject: celoAlfajores, - type: Network.testnet - }, - { - name: "cyber", - projectId: process.env.CYBER_PROJECT_ID || null, - etherscanApiKey: process.env.CYBER_ETHERSCAN_API_KEY || undefined, - viemChainObject: cyber, - type: Network.mainnet - }, - { - name: "cyber-testnet", - projectId: process.env.CYBER_TESTNET_PROJECT_ID || null, - etherscanApiKey: - process.env.CYBER_TESTNET_ETHERSCAN_API_KEY || undefined, - viemChainObject: cyberTestnet, - type: Network.testnet - }, - { - name: "degen", - projectId: process.env.DEGEN_PROJECT_ID || null, - etherscanApiKey: process.env.DEGEN_ETHERSCAN_API_KEY || undefined, - viemChainObject: degen, - type: Network.mainnet - }, - { - name: Network.mainnet, - projectId: process.env.MAINNET_PROJECT_ID || null, - etherscanApiKey: process.env.MAINNET_ETHERSCAN_API_KEY || undefined, - viemChainObject: mainnet, - type: Network.mainnet - }, - { - name: "sepolia", - projectId: process.env.SEPOILA_PROJECT_ID || null, - etherscanApiKey: process.env.SEPOILA_ETHERSCAN_API_KEY || undefined, - viemChainObject: sepolia, - type: Network.testnet - }, - { - name: "optimism", - projectId: process.env.OPTIMISM_PROJECT_ID || null, - etherscanApiKey: process.env.OPTIMISM_ETHERSCAN_API_KEY || undefined, - viemChainObject: optimism, - type: Network.mainnet - }, - { - name: "optimism-sepolia", - projectId: process.env.OPTIMISM_SEPOLIA_PROJECT_ID || null, - etherscanApiKey: - process.env.OPTIMISM_SEPOLIA_ETHERSCAN_API_KEY || undefined, - viemChainObject: optimismSepolia, - type: Network.testnet - }, - { - name: "opbnb", - projectId: process.env.OPBNB_PROJECT_ID || null, - etherscanApiKey: process.env.OPBNB_ETHERSCAN_API_KEY || undefined, - viemChainObject: opBNB, - type: Network.mainnet - }, - { - name: "polygon", - projectId: process.env.POLYGON_PROJECT_ID || null, - etherscanApiKey: process.env.POLYGON_ETHERSCAN_API_KEY || undefined, - viemChainObject: polygon, - type: Network.mainnet - }, - { - name: "polygon-amoy", - projectId: process.env.POLYGON_AMOY_PROJECT_ID || null, - etherscanApiKey: - process.env.POLYGON_AMOY_ETHERSCAN_API_KEY || undefined, - viemChainObject: polygonAmoy, - type: Network.testnet - }, - { - name: "linea", - projectId: process.env.LINEA_PROJECT_ID || null, - etherscanApiKey: process.env.LINEA_ETHERSCAN_API_KEY || undefined, - viemChainObject: linea, - type: Network.mainnet - }, - { - name: "linea-testnet", - projectId: process.env.LINEA_TESTNET_PROJECT_ID || null, - etherscanApiKey: - process.env.LINEA_TESTNET_ETHERSCAN_API_KEY || undefined, - viemChainObject: lineaTestnet, - type: Network.testnet +/* +curl --request GET \ + --url https://prod-api-us-east.onrender.com/v2/chains \ + --header `X-API-KEY: ${process.env.ZERODEV_API_KEY}` \ + --header 'accept: application/json' + { + "chainId": 1, + "name": "Ethereum", + "nativeCurrencyName": "Ether", + "nativeCurrencySymbol": "ETH", + "nativeCurrencyDecimals": 18, + "rpcUrl": "https://eth.llamarpc.com", + "explorerUrl": "https://etherscan.io", + "testnet": false, + "onlySelfFunded": false, + "rollupProvider": null, + "deprecated": false + }, +*/ + +export const getSupportedChains = async (): Promise => { + const response = await fetch( + "https://prod-api-us-east.onrender.com/v2/chains", + { + headers: { + "X-API-KEY": process.env.ZERODEV_API_KEY ?? "", + accept: "application/json" + } + } + ) + + if (!response.ok) { + throw new Error(`Failed to fetch chains: ${response.statusText}`) + } + + const data = (await response.json()) as ZerodevChainResponse[] + + const chains_all = data.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, + deprecated: chain.deprecated, + 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}`, + { + headers: { + "X-API-KEY": process.env.ZERODEV_API_KEY ?? "", + accept: "application/json" + } + } + ) + + if (!response_project.ok) { + throw new Error( + `Failed to fetch project chains: ${response_project.statusText}` + ) } -] + + const chains_project = + (await response_project.json()) as ZerodevProjectResponse + + const supportedChains = chains_project.chains + .map((chain) => { + const key = `${chain.name}-${chain.chain_id}` + const matchedChain = chains_all[key] + if (!matchedChain) { + return null + } + return matchedChain + }) + .filter((chain): chain is ZerodevChain => chain !== null) + + if (supportedChains.length === 0) { + throw new Error("No supported chains found for the project") + } + + return supportedChains +} diff --git a/src/utils/validate.ts b/src/utils/validate.ts index fe29f6d..b6f56cc 100644 --- a/src/utils/validate.ts +++ b/src/utils/validate.ts @@ -1,9 +1,5 @@ import type { Hex } from "viem" -import { - type Chain, - type UnvalidatedChain, - getSupportedChains -} from "../constant.js" +import { type ZerodevChain, getSupportedChains } from "../constant.js" import { readBytecodeFromFile } from "./file.js" const PRIVATE_KEY_REGEX = /^0x[0-9a-fA-F]{64}$/ @@ -73,16 +69,17 @@ interface CommandOptions { testnetAll?: boolean mainnetAll?: boolean allNetworks?: boolean + chainOption?: string } -export const processAndValidateChains = ( - chainOption: string | undefined, +export const processAndValidateChains = async ( options: CommandOptions -): Chain[] => { - const supportedChains = getSupportedChains() +): Promise => { + const supportedChains = await getSupportedChains() + // Check for mutually exclusive options const exclusiveOptions = [ - chainOption !== undefined, + options.chainOption !== undefined, options.testnetAll, options.mainnetAll, options.allNetworks @@ -103,41 +100,31 @@ export const processAndValidateChains = ( process.exit(1) } - let chains: string[] + let chains: ZerodevChain[] if (options.testnetAll) { - chains = supportedChains - .filter((chain) => chain.type === "testnet") - .map((chain) => chain.name) + chains = supportedChains.filter((chain) => chain.testnet) } else if (options.mainnetAll) { - chains = supportedChains - .filter((chain) => chain.type === "mainnet") - .map((chain) => chain.name) + chains = supportedChains.filter((chain) => !chain.testnet) } else if (options.allNetworks) { - chains = supportedChains.map((chain) => chain.name) + chains = supportedChains + } else if (options.chainOption) { + const chainNames = options.chainOption + ? options.chainOption.split(",") + : [] + + chains = chainNames + .map((chainName) => + supportedChains.find( + (chain) => + chain.name.toLowerCase() === chainName.toLowerCase() + ) + ) + .filter((chain) => chain !== undefined) } else { - chains = chainOption ? chainOption.split(",") : [] + console.error( + "Error: At least one of -c, -t, -m, -a options must be specified" + ) + process.exit(1) } - - 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 } diff --git a/test/deployContract.test.ts b/test/deployContract.test.ts index 6a202c1..b768783 100644 --- a/test/deployContract.test.ts +++ b/test/deployContract.test.ts @@ -3,6 +3,7 @@ import { deployToChain,deployContracts } from "../src/action/deployContracts"; import { processAndValidateChains } from "../src/utils"; import {generatePrivateKey,} from "viem/accounts"; import { concat } from 'viem' +import { getSupportedChains } from "../src/constant"; const PRIVATE_KEY = generatePrivateKey(); // this is copied from viem.sh docs, so don't use this in production and no it's not even ours @@ -10,11 +11,12 @@ const PRIVATE_KEY = generatePrivateKey(); // this is copied from viem.sh docs, s test("deployContract", async () => { await deployToChain( PRIVATE_KEY, - processAndValidateChains("sepolia", { + (await processAndValidateChains({ testnetAll : false, mainnetAll : false, - allNetworks : false - })[0], + allNetworks : false, + chainOption : "Sepolia,Base Sepolia" + }))[0], concat(['0x00', generatePrivateKey()]), generatePrivateKey(), undefined, @@ -26,13 +28,19 @@ test("deployContractTestnet", async () => { await deployContracts( PRIVATE_KEY, concat(['0x00', generatePrivateKey()]), - processAndValidateChains(undefined, { + await processAndValidateChains({ testnetAll : true, mainnetAll : false, - allNetworks : false + allNetworks : false, }), generatePrivateKey(), undefined, undefined ); -}, 30000); \ No newline at end of file +}, 30000); + +test("getSupportedChains", async () => { + const chains = await getSupportedChains(); + console.log(chains.map((chain) => chain.name + " " + chain.id)); + expect(chains).toBeDefined(); +}); \ No newline at end of file