From 88635ef831ec495c43e91c9328ce414b55be2b31 Mon Sep 17 00:00:00 2001 From: Denis Fadeev Date: Tue, 17 Jun 2025 17:35:38 +0300 Subject: [PATCH 1/2] Commander.js CLI --- examples/call/commands/common.ts | 64 ++++++ examples/call/commands/connected/call.ts | 30 +++ examples/call/commands/connected/deposit.ts | 24 +++ .../call/commands/connected/depositAndCall.ts | 34 ++++ examples/call/commands/index.ts | 20 ++ examples/call/commands/universal/call.ts | 56 ++++++ examples/call/commands/universal/withdraw.ts | 51 +++++ examples/call/package.json | 3 +- examples/call/yarn.lock | 188 +++++++++++++++++- 9 files changed, 468 insertions(+), 2 deletions(-) create mode 100644 examples/call/commands/common.ts create mode 100644 examples/call/commands/connected/call.ts create mode 100644 examples/call/commands/connected/deposit.ts create mode 100644 examples/call/commands/connected/depositAndCall.ts create mode 100755 examples/call/commands/index.ts create mode 100644 examples/call/commands/universal/call.ts create mode 100644 examples/call/commands/universal/withdraw.ts diff --git a/examples/call/commands/common.ts b/examples/call/commands/common.ts new file mode 100644 index 00000000..8829e077 --- /dev/null +++ b/examples/call/commands/common.ts @@ -0,0 +1,64 @@ +import path from "path"; +import fs from "fs"; +import { ethers } from "ethers"; +import { Command } from "commander"; + +export const getAbi = (name: string) => { + const abiPath = path.resolve( + __dirname, + path.join("..", "artifacts", "contracts", `${name}.sol`, `${name}.json`) + ); + return JSON.parse(fs.readFileSync(abiPath, "utf8")); +}; + +export const createRevertOptions = (options: { + abortAddress: string; + callOnRevert: boolean; + onRevertGasLimit: string; + revertAddress: string; + revertMessage: string; +}) => { + return { + abortAddress: options.abortAddress, + callOnRevert: options.callOnRevert, + onRevertGasLimit: BigInt(options.onRevertGasLimit), + revertAddress: options.revertAddress, + revertMessage: ethers.hexlify(ethers.toUtf8Bytes(options.revertMessage)), + }; +}; + +export const createCommand = (name: string) => { + return new Command(name) + .requiredOption( + "-c, --contract
", + "The address of the deployed contract" + ) + .requiredOption( + "-r, --receiver
", + "The address of the receiver contract" + ) + .option("--call-on-revert", "Whether to call on revert", false) + .option( + "--revert-address
", + "Revert address", + "0x0000000000000000000000000000000000000000" + ) + .option( + "--abort-address
", + "Abort address", + "0x0000000000000000000000000000000000000000" + ) + .option("--revert-message ", "Revert message", "0x") + .option( + "--on-revert-gas-limit ", + "Gas limit for revert tx", + "500000" + ) + .option("-n, --name ", "Contract name", "Connected") + .option( + "--rpc ", + "RPC endpoint", + "https://zetachain-athens-evm.blockpi.network/v1/rpc/public" + ) + .option("--private-key ", "Private key to sign the transaction"); +}; diff --git a/examples/call/commands/connected/call.ts b/examples/call/commands/connected/call.ts new file mode 100644 index 00000000..01d9b827 --- /dev/null +++ b/examples/call/commands/connected/call.ts @@ -0,0 +1,30 @@ +import { ethers } from "ethers"; + +import { createCommand, createRevertOptions, getAbi } from "../common"; + +const main = async (options: any) => { + const provider = new ethers.JsonRpcProvider(options.rpc); + const signer = new ethers.Wallet(options.privateKey, provider); + + const message = ethers.AbiCoder.defaultAbiCoder().encode( + options.types, + options.values + ); + + const { abi } = getAbi(options.name); + const contract = new ethers.Contract(options.contract, abi, signer); + + const tx = await contract.call( + options.receiver, + message, + createRevertOptions(options) + ); + await tx.wait(); + + console.log(`✅ Transaction sent: ${tx.hash}`); +}; + +export const connectedCallCommand = createCommand("connected-call") + .requiredOption("-t, --types ", "Parameter types as JSON string") + .requiredOption("-v, --values ", "The values of the parameters") + .action(main); diff --git a/examples/call/commands/connected/deposit.ts b/examples/call/commands/connected/deposit.ts new file mode 100644 index 00000000..847b98e3 --- /dev/null +++ b/examples/call/commands/connected/deposit.ts @@ -0,0 +1,24 @@ +import { ethers } from "ethers"; + +import { createCommand, createRevertOptions, getAbi } from "../common"; + +const main = async (options: any) => { + const provider = new ethers.JsonRpcProvider(options.rpc); + const signer = new ethers.Wallet(options.privateKey, provider); + + const { abi } = getAbi(options.name); + const contract = new ethers.Contract(options.contract, abi, signer); + + const tx = await contract.deposit( + options.receiver, + createRevertOptions(options), + { value: ethers.parseEther(options.amount) } + ); + await tx.wait(); + + console.log(`✅ Transaction sent: ${tx.hash}`); +}; + +export const connectedDepositCommand = createCommand("connected-deposit") + .requiredOption("-a, --amount ", "Amount to deposit") + .action(main); diff --git a/examples/call/commands/connected/depositAndCall.ts b/examples/call/commands/connected/depositAndCall.ts new file mode 100644 index 00000000..630e9263 --- /dev/null +++ b/examples/call/commands/connected/depositAndCall.ts @@ -0,0 +1,34 @@ +import { ethers } from "ethers"; + +import { createCommand, createRevertOptions, getAbi } from "../common"; + +const main = async (options: any) => { + const provider = new ethers.JsonRpcProvider(options.rpc); + const signer = new ethers.Wallet(options.privateKey, provider); + + const message = ethers.AbiCoder.defaultAbiCoder().encode( + options.types, + options.values + ); + + const { abi } = getAbi(options.name); + const contract = new ethers.Contract(options.contract, abi, signer); + + const tx = await contract.depositAndCall( + options.receiver, + message, + createRevertOptions(options), + { value: ethers.parseEther(options.amount) } + ); + await tx.wait(); + + console.log(`✅ Transaction sent: ${tx.hash}`); +}; + +export const connectedDepositAndCallCommand = createCommand( + "connected-deposit-and-call" +) + .requiredOption("-t, --types ", "Parameter types as JSON string") + .requiredOption("-v, --values ", "The values of the parameters") + .requiredOption("-a, --amount ", "Amount to deposit") + .action(main); diff --git a/examples/call/commands/index.ts b/examples/call/commands/index.ts new file mode 100755 index 00000000..a19b14c2 --- /dev/null +++ b/examples/call/commands/index.ts @@ -0,0 +1,20 @@ +#!/usr/bin/env npx tsx +import { Command } from "commander"; + +import { connectedCallCommand } from "./connected/call"; +import { connectedDepositCommand } from "./connected/deposit"; +import { connectedDepositAndCallCommand } from "./connected/depositAndCall"; +import { universalCallCommand } from "./universal/call"; +import { universalWithdrawCommand } from "./universal/withdraw"; + +const program = new Command() + .helpCommand(false) + .addCommand(connectedCallCommand) + .addCommand(connectedDepositCommand) + .addCommand(connectedDepositAndCallCommand) + .addCommand(universalCallCommand) + .addCommand(universalWithdrawCommand); + +if (require.main === module) program.parse(); + +export default program; diff --git a/examples/call/commands/universal/call.ts b/examples/call/commands/universal/call.ts new file mode 100644 index 00000000..df0cf128 --- /dev/null +++ b/examples/call/commands/universal/call.ts @@ -0,0 +1,56 @@ +import { ethers } from "ethers"; + +import { createCommand, createRevertOptions, getAbi } from "../common"; +import ZRC20ABI from "@zetachain/protocol-contracts/abi/ZRC20.sol/ZRC20.json"; + +const main = async (options: any) => { + const provider = new ethers.JsonRpcProvider(options.rpc); + const signer = new ethers.Wallet(options.privateKey, provider); + + const message = ethers.AbiCoder.defaultAbiCoder().encode( + options.types, + options.values + ); + + const functionSignature = ethers.id(options.function).slice(0, 10); + + const payload = ethers.hexlify(ethers.concat([functionSignature, message])); + + const { abi } = getAbi(options.name); + const contract = new ethers.Contract(options.contract, abi, signer); + + const zrc20 = new ethers.Contract(options.zrc20, ZRC20ABI.abi, signer); + const [, gasFee] = await zrc20.withdrawGasFeeWithGasLimit( + options.callOptionsGasLimit + ); + const zrc20TransferTx = await zrc20.approve(options.contract, gasFee); + + await zrc20TransferTx.wait(); + + const tx = await contract.call( + options.receiver, + options.zrc20, + payload, + { + gasLimit: options.callOptionsGasLimit, + isArbitraryCall: options.callOptionsIsArbitraryCall, + }, + createRevertOptions(options), + { gasLimit: 2000000 } + ); + await tx.wait(); + + console.log(`✅ Transaction sent: ${tx.hash}`); +}; + +export const universalCallCommand = createCommand("universal-call") + .requiredOption("-t, --types ", "Parameter types as JSON string") + .requiredOption("-v, --values ", "The values of the parameters") + .option("--call-options-is-arbitrary-call", "Call any function", false) + .option("--call-options-gas-limit", "The gas limit for the call", "500000") + .option( + "--function ", + `Function to call (example: "hello(string)")` + ) + .option("--zrc20
", "The address of ZRC-20 to pay fees") + .action(main); diff --git a/examples/call/commands/universal/withdraw.ts b/examples/call/commands/universal/withdraw.ts new file mode 100644 index 00000000..b2cdf355 --- /dev/null +++ b/examples/call/commands/universal/withdraw.ts @@ -0,0 +1,51 @@ +import { ethers } from "ethers"; + +import { createCommand, createRevertOptions, getAbi } from "../common"; +import ZRC20ABI from "@zetachain/protocol-contracts/abi/ZRC20.sol/ZRC20.json"; + +const main = async (options: any) => { + const provider = new ethers.JsonRpcProvider(options.rpc); + const signer = new ethers.Wallet(options.privateKey, provider); + + const { abi } = getAbi(options.name); + const contract = new ethers.Contract(options.contract, abi, signer); + + const zrc20 = new ethers.Contract(options.zrc20, ZRC20ABI.abi, signer); + const [gasZRC20, gasFee] = await zrc20.withdrawGasFee(); + + const zrc20TransferTx = await zrc20.approve(options.contract, gasFee); + + await zrc20TransferTx.wait(); + + const decimals = await zrc20.decimals(); + if (gasZRC20 === options.zrc20) { + const targetTokenApprove = await zrc20.approve( + options.contract, + gasFee + ethers.parseUnits(options.amount, decimals) + ); + await targetTokenApprove.wait(); + } else { + const targetTokenApprove = await zrc20.approve( + options.contract, + ethers.parseUnits(options.amount, decimals) + ); + await targetTokenApprove.wait(); + const gasFeeApprove = await gasZRC20.approve(options.contract, gasFee); + await gasFeeApprove.wait(); + } + + const tx = await contract.withdraw( + options.receiver, + ethers.parseUnits(options.amount, decimals), + options.zrc20, + createRevertOptions(options) + ); + await tx.wait(); + + console.log(`✅ Transaction sent: ${tx.hash}`); +}; + +export const universalWithdrawCommand = createCommand("universal-withdraw") + .requiredOption("--amount ", "Amount to withdraw") + .option("--zrc20
", "The address of ZRC-20 to pay fees") + .action(main); diff --git a/examples/call/package.json b/examples/call/package.json index d9856b71..267c2309 100644 --- a/examples/call/package.json +++ b/examples/call/package.json @@ -42,12 +42,13 @@ "eslint-plugin-simple-import-sort": "^10.0.0", "eslint-plugin-sort-keys-fix": "^1.1.2", "eslint-plugin-typescript-sort-keys": "^2.3.0", - "ethers": "^5.4.7", + "ethers": "^6.14.4", "hardhat": "^2.17.2", "hardhat-gas-reporter": "^1.0.8", "prettier": "^2.8.8", "solidity-coverage": "^0.8.0", "ts-node": ">=8.0.0", + "tsx": "^4.20.3", "typechain": "^8.1.0", "typescript": ">=4.5.0" }, diff --git a/examples/call/yarn.lock b/examples/call/yarn.lock index c00074ec..3e6753b6 100644 --- a/examples/call/yarn.lock +++ b/examples/call/yarn.lock @@ -103,6 +103,131 @@ dependencies: "@jridgewell/trace-mapping" "0.3.9" +"@esbuild/aix-ppc64@0.25.5": + version "0.25.5" + resolved "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.5.tgz#4e0f91776c2b340e75558f60552195f6fad09f18" + integrity sha512-9o3TMmpmftaCMepOdA5k/yDw8SfInyzWWTjYTFCX3kPSDJMROQTb8jg+h9Cnwnmm1vOzvxN7gIfB5V2ewpjtGA== + +"@esbuild/android-arm64@0.25.5": + version "0.25.5" + resolved "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.5.tgz#bc766407f1718923f6b8079c8c61bf86ac3a6a4f" + integrity sha512-VGzGhj4lJO+TVGV1v8ntCZWJktV7SGCs3Pn1GRWI1SBFtRALoomm8k5E9Pmwg3HOAal2VDc2F9+PM/rEY6oIDg== + +"@esbuild/android-arm@0.25.5": + version "0.25.5" + resolved "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.5.tgz#4290d6d3407bae3883ad2cded1081a234473ce26" + integrity sha512-AdJKSPeEHgi7/ZhuIPtcQKr5RQdo6OO2IL87JkianiMYMPbCtot9fxPbrMiBADOWWm3T2si9stAiVsGbTQFkbA== + +"@esbuild/android-x64@0.25.5": + version "0.25.5" + resolved "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.5.tgz#40c11d9cbca4f2406548c8a9895d321bc3b35eff" + integrity sha512-D2GyJT1kjvO//drbRT3Hib9XPwQeWd9vZoBJn+bu/lVsOZ13cqNdDeqIF/xQ5/VmWvMduP6AmXvylO/PIc2isw== + +"@esbuild/darwin-arm64@0.25.5": + version "0.25.5" + resolved "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.5.tgz#49d8bf8b1df95f759ac81eb1d0736018006d7e34" + integrity sha512-GtaBgammVvdF7aPIgH2jxMDdivezgFu6iKpmT+48+F8Hhg5J/sfnDieg0aeG/jfSvkYQU2/pceFPDKlqZzwnfQ== + +"@esbuild/darwin-x64@0.25.5": + version "0.25.5" + resolved "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.5.tgz#e27a5d92a14886ef1d492fd50fc61a2d4d87e418" + integrity sha512-1iT4FVL0dJ76/q1wd7XDsXrSW+oLoquptvh4CLR4kITDtqi2e/xwXwdCVH8hVHU43wgJdsq7Gxuzcs6Iq/7bxQ== + +"@esbuild/freebsd-arm64@0.25.5": + version "0.25.5" + resolved "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.5.tgz#97cede59d638840ca104e605cdb9f1b118ba0b1c" + integrity sha512-nk4tGP3JThz4La38Uy/gzyXtpkPW8zSAmoUhK9xKKXdBCzKODMc2adkB2+8om9BDYugz+uGV7sLmpTYzvmz6Sw== + +"@esbuild/freebsd-x64@0.25.5": + version "0.25.5" + resolved "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.5.tgz#71c77812042a1a8190c3d581e140d15b876b9c6f" + integrity sha512-PrikaNjiXdR2laW6OIjlbeuCPrPaAl0IwPIaRv+SMV8CiM8i2LqVUHFC1+8eORgWyY7yhQY+2U2fA55mBzReaw== + +"@esbuild/linux-arm64@0.25.5": + version "0.25.5" + resolved "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.5.tgz#f7b7c8f97eff8ffd2e47f6c67eb5c9765f2181b8" + integrity sha512-Z9kfb1v6ZlGbWj8EJk9T6czVEjjq2ntSYLY2cw6pAZl4oKtfgQuS4HOq41M/BcoLPzrUbNd+R4BXFyH//nHxVg== + +"@esbuild/linux-arm@0.25.5": + version "0.25.5" + resolved "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.5.tgz#2a0be71b6cd8201fa559aea45598dffabc05d911" + integrity sha512-cPzojwW2okgh7ZlRpcBEtsX7WBuqbLrNXqLU89GxWbNt6uIg78ET82qifUy3W6OVww6ZWobWub5oqZOVtwolfw== + +"@esbuild/linux-ia32@0.25.5": + version "0.25.5" + resolved "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.5.tgz#763414463cd9ea6fa1f96555d2762f9f84c61783" + integrity sha512-sQ7l00M8bSv36GLV95BVAdhJ2QsIbCuCjh/uYrWiMQSUuV+LpXwIqhgJDcvMTj+VsQmqAHL2yYaasENvJ7CDKA== + +"@esbuild/linux-loong64@0.25.5": + version "0.25.5" + resolved "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.5.tgz#428cf2213ff786a502a52c96cf29d1fcf1eb8506" + integrity sha512-0ur7ae16hDUC4OL5iEnDb0tZHDxYmuQyhKhsPBV8f99f6Z9KQM02g33f93rNH5A30agMS46u2HP6qTdEt6Q1kg== + +"@esbuild/linux-mips64el@0.25.5": + version "0.25.5" + resolved "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.5.tgz#5cbcc7fd841b4cd53358afd33527cd394e325d96" + integrity sha512-kB/66P1OsHO5zLz0i6X0RxlQ+3cu0mkxS3TKFvkb5lin6uwZ/ttOkP3Z8lfR9mJOBk14ZwZ9182SIIWFGNmqmg== + +"@esbuild/linux-ppc64@0.25.5": + version "0.25.5" + resolved "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.5.tgz#0d954ab39ce4f5e50f00c4f8c4fd38f976c13ad9" + integrity sha512-UZCmJ7r9X2fe2D6jBmkLBMQetXPXIsZjQJCjgwpVDz+YMcS6oFR27alkgGv3Oqkv07bxdvw7fyB71/olceJhkQ== + +"@esbuild/linux-riscv64@0.25.5": + version "0.25.5" + resolved "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.5.tgz#0e7dd30730505abd8088321e8497e94b547bfb1e" + integrity sha512-kTxwu4mLyeOlsVIFPfQo+fQJAV9mh24xL+y+Bm6ej067sYANjyEw1dNHmvoqxJUCMnkBdKpvOn0Ahql6+4VyeA== + +"@esbuild/linux-s390x@0.25.5": + version "0.25.5" + resolved "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.5.tgz#5669af81327a398a336d7e40e320b5bbd6e6e72d" + integrity sha512-K2dSKTKfmdh78uJ3NcWFiqyRrimfdinS5ErLSn3vluHNeHVnBAFWC8a4X5N+7FgVE1EjXS1QDZbpqZBjfrqMTQ== + +"@esbuild/linux-x64@0.25.5": + version "0.25.5" + resolved "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.5.tgz#b2357dd153aa49038967ddc1ffd90c68a9d2a0d4" + integrity sha512-uhj8N2obKTE6pSZ+aMUbqq+1nXxNjZIIjCjGLfsWvVpy7gKCOL6rsY1MhRh9zLtUtAI7vpgLMK6DxjO8Qm9lJw== + +"@esbuild/netbsd-arm64@0.25.5": + version "0.25.5" + resolved "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.5.tgz#53b4dfb8fe1cee93777c9e366893bd3daa6ba63d" + integrity sha512-pwHtMP9viAy1oHPvgxtOv+OkduK5ugofNTVDilIzBLpoWAM16r7b/mxBvfpuQDpRQFMfuVr5aLcn4yveGvBZvw== + +"@esbuild/netbsd-x64@0.25.5": + version "0.25.5" + resolved "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.5.tgz#a0206f6314ce7dc8713b7732703d0f58de1d1e79" + integrity sha512-WOb5fKrvVTRMfWFNCroYWWklbnXH0Q5rZppjq0vQIdlsQKuw6mdSihwSo4RV/YdQ5UCKKvBy7/0ZZYLBZKIbwQ== + +"@esbuild/openbsd-arm64@0.25.5": + version "0.25.5" + resolved "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.5.tgz#2a796c87c44e8de78001d808c77d948a21ec22fd" + integrity sha512-7A208+uQKgTxHd0G0uqZO8UjK2R0DDb4fDmERtARjSHWxqMTye4Erz4zZafx7Di9Cv+lNHYuncAkiGFySoD+Mw== + +"@esbuild/openbsd-x64@0.25.5": + version "0.25.5" + resolved "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.5.tgz#28d0cd8909b7fa3953af998f2b2ed34f576728f0" + integrity sha512-G4hE405ErTWraiZ8UiSoesH8DaCsMm0Cay4fsFWOOUcz8b8rC6uCvnagr+gnioEjWn0wC+o1/TAHt+It+MpIMg== + +"@esbuild/sunos-x64@0.25.5": + version "0.25.5" + resolved "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.5.tgz#a28164f5b997e8247d407e36c90d3fd5ddbe0dc5" + integrity sha512-l+azKShMy7FxzY0Rj4RCt5VD/q8mG/e+mDivgspo+yL8zW7qEwctQ6YqKX34DTEleFAvCIUviCFX1SDZRSyMQA== + +"@esbuild/win32-arm64@0.25.5": + version "0.25.5" + resolved "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.5.tgz#6eadbead38e8bd12f633a5190e45eff80e24007e" + integrity sha512-O2S7SNZzdcFG7eFKgvwUEZ2VG9D/sn/eIiz8XRZ1Q/DO5a3s76Xv0mdBzVM5j5R639lXQmPmSo0iRpHqUUrsxw== + +"@esbuild/win32-ia32@0.25.5": + version "0.25.5" + resolved "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.5.tgz#bab6288005482f9ed2adb9ded7e88eba9a62cc0d" + integrity sha512-onOJ02pqs9h1iMJ1PQphR+VZv8qBMQ77Klcsqv9CNW2w6yLqoURLcgERAIurY6QE63bbLuqgP9ATqajFLK5AMQ== + +"@esbuild/win32-x64@0.25.5": + version "0.25.5" + resolved "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.5.tgz#7fc114af5f6563f19f73324b5d5ff36ece0803d1" + integrity sha512-TXv6YnJ8ZMVdX+SXWVBo/0p8LTcrUYngpWjvm91TMjjBQii7Oz11Lw5lbDV5Y0TzuhSJHwiH4hEtC1I42mMS0g== + "@eslint-community/eslint-utils@^4.2.0": version "4.4.1" resolved "https://registry.yarnpkg.com/@eslint-community/eslint-utils/-/eslint-utils-4.4.1.tgz#d1145bf2c20132d6400495d6df4bf59362fd9d56" @@ -4946,6 +5071,37 @@ es6-promisify@^5.0.0: dependencies: es6-promise "^4.0.3" +esbuild@~0.25.0: + version "0.25.5" + resolved "https://registry.npmjs.org/esbuild/-/esbuild-0.25.5.tgz#71075054993fdfae76c66586f9b9c1f8d7edd430" + integrity sha512-P8OtKZRv/5J5hhz0cUAdu/cLuPIKXpQl1R9pZtvmHWQvrAUVd0UNIPT4IB4W3rNOqVO0rlqHmCIbSwxh/c9yUQ== + optionalDependencies: + "@esbuild/aix-ppc64" "0.25.5" + "@esbuild/android-arm" "0.25.5" + "@esbuild/android-arm64" "0.25.5" + "@esbuild/android-x64" "0.25.5" + "@esbuild/darwin-arm64" "0.25.5" + "@esbuild/darwin-x64" "0.25.5" + "@esbuild/freebsd-arm64" "0.25.5" + "@esbuild/freebsd-x64" "0.25.5" + "@esbuild/linux-arm" "0.25.5" + "@esbuild/linux-arm64" "0.25.5" + "@esbuild/linux-ia32" "0.25.5" + "@esbuild/linux-loong64" "0.25.5" + "@esbuild/linux-mips64el" "0.25.5" + "@esbuild/linux-ppc64" "0.25.5" + "@esbuild/linux-riscv64" "0.25.5" + "@esbuild/linux-s390x" "0.25.5" + "@esbuild/linux-x64" "0.25.5" + "@esbuild/netbsd-arm64" "0.25.5" + "@esbuild/netbsd-x64" "0.25.5" + "@esbuild/openbsd-arm64" "0.25.5" + "@esbuild/openbsd-x64" "0.25.5" + "@esbuild/sunos-x64" "0.25.5" + "@esbuild/win32-arm64" "0.25.5" + "@esbuild/win32-ia32" "0.25.5" + "@esbuild/win32-x64" "0.25.5" + escalade@^3.1.1: version "3.2.0" resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.2.0.tgz#011a3f69856ba189dffa7dc8fcce99d2a87903e5" @@ -5415,6 +5571,19 @@ ethers@^5.4.7, ethers@^5.7.2: "@ethersproject/web" "5.8.0" "@ethersproject/wordlists" "5.8.0" +ethers@^6.14.4: + version "6.14.4" + resolved "https://registry.npmjs.org/ethers/-/ethers-6.14.4.tgz#0f6fbc562a8425c7c888da307fa71ef796be0c04" + integrity sha512-Jm/dzRs2Z9iBrT6e9TvGxyb5YVKAPLlpna7hjxH7KH/++DSh2T/JVmQUv7iHI5E55hDbp/gEVvstWYXVxXFzsA== + dependencies: + "@adraffy/ens-normalize" "1.10.1" + "@noble/curves" "1.2.0" + "@noble/hashes" "1.3.2" + "@types/node" "22.7.5" + aes-js "4.0.0-beta.5" + tslib "2.7.0" + ws "8.17.1" + ethjs-unit@0.1.6: version "0.1.6" resolved "https://registry.yarnpkg.com/ethjs-unit/-/ethjs-unit-0.1.6.tgz#c665921e476e87bce2a9d588a6fe0405b2c41699" @@ -5704,7 +5873,7 @@ fs.realpath@^1.0.0: resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" integrity sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw== -fsevents@~2.3.2: +fsevents@~2.3.2, fsevents@~2.3.3: version "2.3.3" resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.3.tgz#cac6407785d03675a2a5e1a5305c697b347d90d6" integrity sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw== @@ -5786,6 +5955,13 @@ get-tsconfig@^4.10.0: dependencies: resolve-pkg-maps "^1.0.0" +get-tsconfig@^4.7.5: + version "4.10.1" + resolved "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.10.1.tgz#d34c1c01f47d65a606c37aa7a177bc3e56ab4b2e" + integrity sha512-auHyJ4AgMz7vgS8Hp3N6HXSmlMdUyhSUrfBF16w153rxtLIEOE+HGqaBppczZvnHLqQJfiHotCYpNhl0lUROFQ== + dependencies: + resolve-pkg-maps "^1.0.0" + ghost-testrpc@^0.0.2: version "0.0.2" resolved "https://registry.yarnpkg.com/ghost-testrpc/-/ghost-testrpc-0.0.2.tgz#c4de9557b1d1ae7b2d20bbe474a91378ca90ce92" @@ -8554,6 +8730,16 @@ tsutils@^3.21.0: dependencies: tslib "^1.8.1" +tsx@^4.20.3: + version "4.20.3" + resolved "https://registry.npmjs.org/tsx/-/tsx-4.20.3.tgz#f913e4911d59ad177c1bcee19d1035ef8dd6e2fb" + integrity sha512-qjbnuR9Tr+FJOMBqJCW5ehvIo/buZq7vH7qD7JziU98h6l3qGy0a/yPFjwO+y0/T7GFpNgNAvEcPPVfyT8rrPQ== + dependencies: + esbuild "~0.25.0" + get-tsconfig "^4.7.5" + optionalDependencies: + fsevents "~2.3.3" + tweetnacl-util@^0.15.1: version "0.15.1" resolved "https://registry.yarnpkg.com/tweetnacl-util/-/tweetnacl-util-0.15.1.tgz#b80fcdb5c97bcc508be18c44a4be50f022eea00b" From c4574d91dd0599763d4e7799146bd1270d3fb463 Mon Sep 17 00:00:00 2001 From: Denis Fadeev Date: Tue, 17 Jun 2025 18:06:14 +0300 Subject: [PATCH 2/2] approveZRC20 --- examples/call/commands/common.ts | 43 ++++++++++++- examples/call/commands/connected/call.ts | 3 +- examples/call/commands/connected/deposit.ts | 2 +- .../call/commands/connected/depositAndCall.ts | 2 +- examples/call/commands/index.ts | 4 +- examples/call/commands/universal/call.ts | 2 +- examples/call/commands/universal/withdraw.ts | 40 ++++-------- .../commands/universal/withdrawAndCall.ts | 64 +++++++++++++++++++ 8 files changed, 128 insertions(+), 32 deletions(-) create mode 100644 examples/call/commands/universal/withdrawAndCall.ts diff --git a/examples/call/commands/common.ts b/examples/call/commands/common.ts index 8829e077..396c290a 100644 --- a/examples/call/commands/common.ts +++ b/examples/call/commands/common.ts @@ -2,6 +2,7 @@ import path from "path"; import fs from "fs"; import { ethers } from "ethers"; import { Command } from "commander"; +import ZRC20ABI from "@zetachain/protocol-contracts/abi/ZRC20.sol/ZRC20.json"; export const getAbi = (name: string) => { const abiPath = path.resolve( @@ -27,6 +28,45 @@ export const createRevertOptions = (options: { }; }; +export const approveZRC20 = async ( + zrc20Address: string, + contract: string, + amount: string, + signer: ethers.Wallet, + gasLimit?: number +) => { + const zrc20 = new ethers.Contract(zrc20Address, ZRC20ABI.abi, signer); + const [gasZRC20, gasFee] = gasLimit + ? await zrc20.withdrawGasFeeWithGasLimit(gasLimit) + : await zrc20.withdrawGasFee(); + + const zrc20TransferTx = await zrc20.approve(contract, gasFee); + await zrc20TransferTx.wait(); + + const decimals = await zrc20.decimals(); + if (gasZRC20 === zrc20.target) { + const targetTokenApprove = await zrc20.approve( + contract, + gasFee + ethers.parseUnits(amount, decimals) + ); + await targetTokenApprove.wait(); + } else { + const targetTokenApprove = await zrc20.approve( + contract, + ethers.parseUnits(amount, decimals) + ); + await targetTokenApprove.wait(); + const gasZRC20Contract = new ethers.Contract( + gasZRC20, + ZRC20ABI.abi, + signer + ); + const gasFeeApprove = await gasZRC20Contract.approve(contract, gasFee); + await gasFeeApprove.wait(); + } + return { decimals }; +}; + export const createCommand = (name: string) => { return new Command(name) .requiredOption( @@ -54,11 +94,12 @@ export const createCommand = (name: string) => { "Gas limit for revert tx", "500000" ) - .option("-n, --name ", "Contract name", "Connected") + .option("-n, --name ", "Contract name") .option( "--rpc ", "RPC endpoint", "https://zetachain-athens-evm.blockpi.network/v1/rpc/public" ) + .option("--gas-limit ", "Gas limit for the transaction", "1000000") .option("--private-key ", "Private key to sign the transaction"); }; diff --git a/examples/call/commands/connected/call.ts b/examples/call/commands/connected/call.ts index 01d9b827..e6ca4cc4 100644 --- a/examples/call/commands/connected/call.ts +++ b/examples/call/commands/connected/call.ts @@ -17,7 +17,8 @@ const main = async (options: any) => { const tx = await contract.call( options.receiver, message, - createRevertOptions(options) + createRevertOptions(options), + { gasLimit: options.gasLimit } ); await tx.wait(); diff --git a/examples/call/commands/connected/deposit.ts b/examples/call/commands/connected/deposit.ts index 847b98e3..f124446e 100644 --- a/examples/call/commands/connected/deposit.ts +++ b/examples/call/commands/connected/deposit.ts @@ -12,7 +12,7 @@ const main = async (options: any) => { const tx = await contract.deposit( options.receiver, createRevertOptions(options), - { value: ethers.parseEther(options.amount) } + { value: ethers.parseEther(options.amount), gasLimit: options.gasLimit } ); await tx.wait(); diff --git a/examples/call/commands/connected/depositAndCall.ts b/examples/call/commands/connected/depositAndCall.ts index 630e9263..e861eb09 100644 --- a/examples/call/commands/connected/depositAndCall.ts +++ b/examples/call/commands/connected/depositAndCall.ts @@ -18,7 +18,7 @@ const main = async (options: any) => { options.receiver, message, createRevertOptions(options), - { value: ethers.parseEther(options.amount) } + { value: ethers.parseEther(options.amount), gasLimit: options.gasLimit } ); await tx.wait(); diff --git a/examples/call/commands/index.ts b/examples/call/commands/index.ts index a19b14c2..0447ef09 100755 --- a/examples/call/commands/index.ts +++ b/examples/call/commands/index.ts @@ -6,6 +6,7 @@ import { connectedDepositCommand } from "./connected/deposit"; import { connectedDepositAndCallCommand } from "./connected/depositAndCall"; import { universalCallCommand } from "./universal/call"; import { universalWithdrawCommand } from "./universal/withdraw"; +import { universalWithdrawAndCallCommand } from "./universal/withdrawAndCall"; const program = new Command() .helpCommand(false) @@ -13,7 +14,8 @@ const program = new Command() .addCommand(connectedDepositCommand) .addCommand(connectedDepositAndCallCommand) .addCommand(universalCallCommand) - .addCommand(universalWithdrawCommand); + .addCommand(universalWithdrawCommand) + .addCommand(universalWithdrawAndCallCommand); if (require.main === module) program.parse(); diff --git a/examples/call/commands/universal/call.ts b/examples/call/commands/universal/call.ts index df0cf128..68f19df4 100644 --- a/examples/call/commands/universal/call.ts +++ b/examples/call/commands/universal/call.ts @@ -36,7 +36,7 @@ const main = async (options: any) => { isArbitraryCall: options.callOptionsIsArbitraryCall, }, createRevertOptions(options), - { gasLimit: 2000000 } + { gasLimit: options.gasLimit } ); await tx.wait(); diff --git a/examples/call/commands/universal/withdraw.ts b/examples/call/commands/universal/withdraw.ts index b2cdf355..e4acb223 100644 --- a/examples/call/commands/universal/withdraw.ts +++ b/examples/call/commands/universal/withdraw.ts @@ -1,7 +1,11 @@ import { ethers } from "ethers"; -import { createCommand, createRevertOptions, getAbi } from "../common"; -import ZRC20ABI from "@zetachain/protocol-contracts/abi/ZRC20.sol/ZRC20.json"; +import { + approveZRC20, + createCommand, + createRevertOptions, + getAbi, +} from "../common"; const main = async (options: any) => { const provider = new ethers.JsonRpcProvider(options.rpc); @@ -10,35 +14,19 @@ const main = async (options: any) => { const { abi } = getAbi(options.name); const contract = new ethers.Contract(options.contract, abi, signer); - const zrc20 = new ethers.Contract(options.zrc20, ZRC20ABI.abi, signer); - const [gasZRC20, gasFee] = await zrc20.withdrawGasFee(); - - const zrc20TransferTx = await zrc20.approve(options.contract, gasFee); - - await zrc20TransferTx.wait(); - - const decimals = await zrc20.decimals(); - if (gasZRC20 === options.zrc20) { - const targetTokenApprove = await zrc20.approve( - options.contract, - gasFee + ethers.parseUnits(options.amount, decimals) - ); - await targetTokenApprove.wait(); - } else { - const targetTokenApprove = await zrc20.approve( - options.contract, - ethers.parseUnits(options.amount, decimals) - ); - await targetTokenApprove.wait(); - const gasFeeApprove = await gasZRC20.approve(options.contract, gasFee); - await gasFeeApprove.wait(); - } + const { decimals } = await approveZRC20( + options.zrc20, + options.contract, + options.amount, + signer + ); const tx = await contract.withdraw( options.receiver, ethers.parseUnits(options.amount, decimals), options.zrc20, - createRevertOptions(options) + createRevertOptions(options), + { gasLimit: options.gasLimit } ); await tx.wait(); diff --git a/examples/call/commands/universal/withdrawAndCall.ts b/examples/call/commands/universal/withdrawAndCall.ts new file mode 100644 index 00000000..d3729562 --- /dev/null +++ b/examples/call/commands/universal/withdrawAndCall.ts @@ -0,0 +1,64 @@ +import { ethers } from "ethers"; + +import { + approveZRC20, + createCommand, + createRevertOptions, + getAbi, +} from "../common"; + +const main = async (options: any) => { + const provider = new ethers.JsonRpcProvider(options.rpc); + const signer = new ethers.Wallet(options.privateKey, provider); + + const message = ethers.AbiCoder.defaultAbiCoder().encode( + options.types, + options.values + ); + + const functionSignature = ethers.id(options.function).slice(0, 10); + + const payload = ethers.hexlify(ethers.concat([functionSignature, message])); + + const { abi } = getAbi(options.name); + const contract = new ethers.Contract(options.contract, abi, signer); + + const { decimals } = await approveZRC20( + options.zrc20, + options.contract, + options.amount, + signer, + options.callOptionsGasLimit + ); + + const tx = await contract.withdrawAndCall( + options.receiver, + ethers.parseUnits(options.amount, decimals), + options.zrc20, + payload, + { + gasLimit: options.callOptionsGasLimit, + isArbitraryCall: options.callOptionsIsArbitraryCall, + }, + createRevertOptions(options), + { gasLimit: options.gasLimit } + ); + await tx.wait(); + + console.log(`✅ Transaction sent: ${tx.hash}`); +}; + +export const universalWithdrawAndCallCommand = createCommand( + "universal-withdraw-and-call" +) + .requiredOption("-t, --types ", "Parameter types as JSON string") + .requiredOption("-v, --values ", "The values of the parameters") + .option("--call-options-is-arbitrary-call", "Call any function", false) + .option("--call-options-gas-limit", "The gas limit for the call", "500000") + .option( + "--function ", + `Function to call (example: "hello(string)")` + ) + .requiredOption("--amount ", "Amount to withdraw") + .option("--zrc20
", "The address of ZRC-20 to pay fees") + .action(main);