Skip to content
Merged
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
105 changes: 105 additions & 0 deletions examples/call/commands/common.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
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(
__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 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(
"-c, --contract <address>",
"The address of the deployed contract"
)
.requiredOption(
"-r, --receiver <address>",
"The address of the receiver contract"
)
.option("--call-on-revert", "Whether to call on revert", false)
.option(
"--revert-address <address>",
"Revert address",
"0x0000000000000000000000000000000000000000"
)
.option(
"--abort-address <address>",
"Abort address",
"0x0000000000000000000000000000000000000000"
)
.option("--revert-message <string>", "Revert message", "0x")
.option(
"--on-revert-gas-limit <number>",
"Gas limit for revert tx",
"500000"
)
.option("-n, --name <contract>", "Contract name")
.option(
"--rpc <url>",
"RPC endpoint",
"https://zetachain-athens-evm.blockpi.network/v1/rpc/public"
)
.option("--gas-limit <number>", "Gas limit for the transaction", "1000000")
.option("--private-key <key>", "Private key to sign the transaction");
};
31 changes: 31 additions & 0 deletions examples/call/commands/connected/call.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { ethers } from "ethers";

import { createCommand, createRevertOptions, getAbi } from "../common";

const main = async (options: any) => {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's ensure we don't use any, these commands would benefit from zod argument validation, this function is an excellent example of why this is necessary because it uses types and values, and since we're not using any validation or refinements, they could differ in length. We could use a commander preAction hook if we think zod is overkill for this small project.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Examples is a different sort of code base where we can't have extra code laying around. I agree with you about additional validation and I think we can accomplish this by exporting required validation rules/functions from the toolkit and just importing them here.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I suppose we can add zod directly here.

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),
{ gasLimit: options.gasLimit }
);
await tx.wait();

console.log(`✅ Transaction sent: ${tx.hash}`);
};

export const connectedCallCommand = createCommand("connected-call")
.requiredOption("-t, --types <types...>", "Parameter types as JSON string")
.requiredOption("-v, --values <values...>", "The values of the parameters")
.action(main);
24 changes: 24 additions & 0 deletions examples/call/commands/connected/deposit.ts
Original file line number Diff line number Diff line change
@@ -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), gasLimit: options.gasLimit }
);
await tx.wait();

console.log(`✅ Transaction sent: ${tx.hash}`);
};

export const connectedDepositCommand = createCommand("connected-deposit")
.requiredOption("-a, --amount <number>", "Amount to deposit")
.action(main);
34 changes: 34 additions & 0 deletions examples/call/commands/connected/depositAndCall.ts
Original file line number Diff line number Diff line change
@@ -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), gasLimit: options.gasLimit }
);
await tx.wait();

console.log(`✅ Transaction sent: ${tx.hash}`);
};

export const connectedDepositAndCallCommand = createCommand(
"connected-deposit-and-call"
)
.requiredOption("-t, --types <types...>", "Parameter types as JSON string")
.requiredOption("-v, --values <values...>", "The values of the parameters")
.requiredOption("-a, --amount <number>", "Amount to deposit")
.action(main);
22 changes: 22 additions & 0 deletions examples/call/commands/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
#!/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";
import { universalWithdrawAndCallCommand } from "./universal/withdrawAndCall";

const program = new Command()
.helpCommand(false)
.addCommand(connectedCallCommand)
.addCommand(connectedDepositCommand)
.addCommand(connectedDepositAndCallCommand)
.addCommand(universalCallCommand)
.addCommand(universalWithdrawCommand)
.addCommand(universalWithdrawAndCallCommand);

if (require.main === module) program.parse();

export default program;
56 changes: 56 additions & 0 deletions examples/call/commands/universal/call.ts
Original file line number Diff line number Diff line change
@@ -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: options.gasLimit }
);
await tx.wait();

console.log(`✅ Transaction sent: ${tx.hash}`);
};

export const universalCallCommand = createCommand("universal-call")
.requiredOption("-t, --types <types...>", "Parameter types as JSON string")
.requiredOption("-v, --values <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>",
`Function to call (example: "hello(string)")`
)
.option("--zrc20 <address>", "The address of ZRC-20 to pay fees")
.action(main);
39 changes: 39 additions & 0 deletions examples/call/commands/universal/withdraw.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
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 { abi } = getAbi(options.name);
const contract = new ethers.Contract(options.contract, abi, signer);

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),
{ gasLimit: options.gasLimit }
);
await tx.wait();

console.log(`✅ Transaction sent: ${tx.hash}`);
};

export const universalWithdrawCommand = createCommand("universal-withdraw")
.requiredOption("--amount <number>", "Amount to withdraw")
.option("--zrc20 <address>", "The address of ZRC-20 to pay fees")
.action(main);
64 changes: 64 additions & 0 deletions examples/call/commands/universal/withdrawAndCall.ts
Original file line number Diff line number Diff line change
@@ -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 <types...>", "Parameter types as JSON string")
.requiredOption("-v, --values <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>",
`Function to call (example: "hello(string)")`
)
.requiredOption("--amount <number>", "Amount to withdraw")
.option("--zrc20 <address>", "The address of ZRC-20 to pay fees")
.action(main);
3 changes: 2 additions & 1 deletion examples/call/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
},
Expand Down
Loading
Loading