From fd019aea4efd41d409ec8c239c9d899ca7e2af2e Mon Sep 17 00:00:00 2001
From: Jack Hamer <47187316+JackHamer09@users.noreply.github.com>
Date: Tue, 30 Jan 2024 13:58:41 +0200
Subject: [PATCH] feat: add contract encode command (#118)
---
README.md | 1 +
docs/contract-interaction.md | 42 +++++++-
src/commands/contract/encode.ts | 131 +++++++++++++++++++++++++
src/commands/contract/index.ts | 1 +
src/commands/contract/read.ts | 10 +-
src/commands/contract/utils/helpers.ts | 45 +++++++--
src/commands/contract/write.ts | 10 +-
7 files changed, 227 insertions(+), 13 deletions(-)
create mode 100644 src/commands/contract/encode.ts
diff --git a/README.md b/README.md
index 58d1fb2d..0f477a89 100644
--- a/README.md
+++ b/README.md
@@ -42,6 +42,7 @@ Run `npx zksync-cli dev` to see the full list of commands.
### Contract interaction commands
- `npx zksync-cli contract read`: run read-only contract methods
- `npx zksync-cli contract write`: send transactions to the contract
+- `npx zksync-cli contract encode`: get calldata from the contract method
See full documentation and advanced examples [here](./docs/contract-interaction.md).
diff --git a/docs/contract-interaction.md b/docs/contract-interaction.md
index 12157e15..d9d7dff5 100644
--- a/docs/contract-interaction.md
+++ b/docs/contract-interaction.md
@@ -1,5 +1,5 @@
# Contract interaction
-The zksync-cli tool, now enhanced with `contract read` and `contract write` commands, offers efficient ways for developers to interact with smart contracts on zkSync. These commands automate tasks such as method verification, ABI handling, output decoding, and proxy contract processing.
+The zksync-cli tool, now enhanced with `contract read`, `contract write` and `contract encode` commands, offers efficient ways for developers to interact with smart contracts on zkSync. These commands automate tasks such as method verification, ABI handling, output decoding, and proxy contract processing.
### Table of contents
- [Contract Read - Running read-only methods](#contract-read)
@@ -7,6 +7,7 @@ The zksync-cli tool, now enhanced with `contract read` and `contract write` comm
- [Examples](#examples)
- [Basic read example](#basic-read-example)
- [Basic write example](#basic-write-example)
+ - [Basic encode example](#basic-encode-example)
- [Using local ABI file](#using-local-abi-file)
- [Running read on behalf of another address](#running-read-on-behalf-of-another-address)
- [Write operation with value transfer](#write-operation-with-value-transfer)
@@ -186,6 +187,45 @@ npx zksync-cli contract write \
+#### Basic encode example
+```bash
+npx zksync-cli contract encode
+```
+This command allows you to encode contract method signature and arguments into raw calldata (e.g. `0x1234...`).
+
+You will need to select a **method (function) to encode**.
+- Enter method signature manually, for example `transfer(address,uint256)`.
+ ```bash
+ ? Enter method to call: transfer(address,uint256)
+ ```
+- Alternatively, you can specify the ABI file using the `--abi` option. [See example](#using-local-abi-file)
+ ```bash
+ ? Contract method to call
+ ────────── Provided contract ──────────
+ ❯ approve(address spender, uint256 amount) returns (bool)
+ transfer(address to, uint256 amount) returns (bool)
+ ───────────────────────────────────────
+ Type method manually
+ ```
+
+After that, you will be prompted to enter **arguments** for the method, one by one.
+```bash
+? Provide method arguments:
+? [1/2] to (address): 0xa1cf087DB965Ab02Fb3CFaCe1f5c63935815f044
+? [2/2] amount (uint256): 1
+```
+
+When finished you will see the encoded data.
+```bash
+✔ Encoded data: 0xa41368620000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000c48656c6c6f20776f726c64210000000000000000000000000000000000000000
+```
+
+
+
+---
+
+
+
### Using local ABI file
You can specify a local ABI file using the `--abi` option. It should be a JSON file with either ABI data (array) or contract artifact which you get after compiling your contracts.
```bash
diff --git a/src/commands/contract/encode.ts b/src/commands/contract/encode.ts
new file mode 100644
index 00000000..b689db0a
--- /dev/null
+++ b/src/commands/contract/encode.ts
@@ -0,0 +1,131 @@
+import chalk from "chalk";
+import { ethers } from "ethers";
+import inquirer from "inquirer";
+
+import Program from "./command.js";
+import { abiOption, argumentsOption, methodOption } from "./common/options.js";
+import { encodeData, encodeParam, getFragmentFromSignature, getInputsFromSignature } from "./utils/formatters.js";
+import { readAbiFromFile, askAbiMethod, formatMethodString } from "./utils/helpers.js";
+import { logFullCommandFromOptions, optionNameToParam } from "../../utils/helpers.js";
+import Logger from "../../utils/logger.js";
+
+import type { ABI } from "./utils/helpers.js";
+import type { Command } from "commander";
+import type { DistinctQuestion } from "inquirer";
+
+type EncodeOptions = {
+ method?: string;
+ arguments?: string[];
+ abi?: string;
+};
+
+// ----------------
+// prompts
+// ----------------
+
+const askMethod = async (contractAbi: ABI | undefined, options: EncodeOptions) => {
+ if (options.method) {
+ return;
+ }
+
+ const methodByAbi = await askAbiMethod({ abi: contractAbi });
+ if (methodByAbi !== "manual") {
+ const fullMethodName = methodByAbi.format(ethers.utils.FormatTypes.full);
+ options.method = formatMethodString(fullMethodName);
+ return;
+ }
+
+ const answers: Pick = await inquirer.prompt(
+ [
+ {
+ message: "Enter method to encode",
+ name: optionNameToParam(methodOption.long!),
+ type: "input",
+ validate: (input: string) => {
+ try {
+ getFragmentFromSignature(input); // throws if invalid
+ return true;
+ } catch {
+ return `Invalid method signature. Example: ${chalk.blueBright("balanceOf(address)")}`;
+ }
+ },
+ },
+ ],
+ options
+ );
+
+ options.method = answers.method;
+};
+
+const askArguments = async (method: string, options: EncodeOptions) => {
+ if (options.arguments) {
+ return;
+ }
+ const inputs = getInputsFromSignature(method);
+ if (!inputs.length) {
+ options.arguments = [];
+ return;
+ }
+ Logger.info(chalk.green("?") + chalk.bold(" Provide method arguments:"));
+ const prompts: DistinctQuestion[] = [];
+
+ inputs.forEach((input, index) => {
+ let name = chalk.gray(`[${index + 1}/${inputs.length}]`);
+ if (input.name) {
+ name += ` ${input.name}`;
+ name += chalk.gray(` (${input.type})`);
+ } else {
+ name += ` ${input.type}`;
+ }
+
+ prompts.push({
+ message: name,
+ name: index.toString(),
+ type: "input",
+ validate: (value: string) => {
+ try {
+ encodeParam(input, value); // throws if invalid
+ return true;
+ } catch (error) {
+ return `${chalk.redBright(
+ "Failed to encode provided argument: " + (error instanceof Error ? error.message : error)
+ )}`;
+ }
+ },
+ });
+ });
+
+ const answers = await inquirer.prompt(prompts);
+ options.arguments = Object.values(answers);
+};
+
+// ----------------
+// request handler
+// ----------------
+
+export const handler = async (options: EncodeOptions, context: Command) => {
+ try {
+ let abi: ABI | undefined;
+ if (options.abi) {
+ abi = readAbiFromFile(options.abi);
+ Logger.info(chalk.gray("Using provided ABI file"));
+ }
+ await askMethod(abi, options);
+ await askArguments(options.method!, options);
+
+ const data = encodeData(options.method!, options.arguments!);
+ Logger.info("");
+ Logger.info(chalk.greenBright("✔ Encoded data: ") + data);
+ logFullCommandFromOptions(options, context, { emptyLine: true });
+ } catch (error) {
+ Logger.error("There was an error while performing encoding");
+ Logger.error(error);
+ }
+};
+
+Program.command("encode")
+ .addOption(methodOption)
+ .addOption(argumentsOption)
+ .addOption(abiOption)
+ .description("Get calldata (e.g. 0x1234) from contract method signature and arguments")
+ .action(handler);
diff --git a/src/commands/contract/index.ts b/src/commands/contract/index.ts
index 451a6e1b..deed9050 100644
--- a/src/commands/contract/index.ts
+++ b/src/commands/contract/index.ts
@@ -1,4 +1,5 @@
import "./read.js";
import "./write.js";
+import "./encode.js";
import "./command.js"; // registers all the commands above
diff --git a/src/commands/contract/read.ts b/src/commands/contract/read.ts
index 44c1da00..2ad0764c 100644
--- a/src/commands/contract/read.ts
+++ b/src/commands/contract/read.ts
@@ -21,7 +21,13 @@ import {
getInputValues,
getInputsFromSignature,
} from "./utils/formatters.js";
-import { checkIfMethodExists, getContractInfoWithLoader, readAbiFromFile, askAbiMethod } from "./utils/helpers.js";
+import {
+ checkIfMethodExists,
+ getContractInfoWithLoader,
+ readAbiFromFile,
+ askAbiMethod,
+ formatMethodString,
+} from "./utils/helpers.js";
import { chainOption, l2RpcUrlOption } from "../../common/options.js";
import { l2Chains } from "../../data/chains.js";
import { getL2Provider, logFullCommandFromOptions, optionNameToParam } from "../../utils/helpers.js";
@@ -63,7 +69,7 @@ const askMethod = async (contractInfo: ContractInfo, options: CallOptions) => {
const methodByAbi = await askAbiMethod(contractInfo, "read");
if (methodByAbi !== "manual") {
const fullMethodName = methodByAbi.format(ethers.utils.FormatTypes.full);
- options.method = fullMethodName.substring("function ".length).replace(/\).+$/, ")"); // remove "function " prefix and return type
+ options.method = formatMethodString(fullMethodName);
if (methodByAbi.outputs) {
options.outputTypes = methodByAbi.outputs.map((output) => output.type);
}
diff --git a/src/commands/contract/utils/helpers.ts b/src/commands/contract/utils/helpers.ts
index d39403c4..eb9a32a6 100644
--- a/src/commands/contract/utils/helpers.ts
+++ b/src/commands/contract/utils/helpers.ts
@@ -21,15 +21,22 @@ export type ContractInfo = {
implementation?: ContractInfo;
};
-export const getMethodsFromAbi = (abi: ABI, type: "read" | "write"): ethers.utils.FunctionFragment[] => {
- if (type === "read") {
+export const formatMethodString = (method: string): string => {
+ // remove "function " prefix and return type
+ // e.g. "greet() view returns (string)" -> "greet()"
+ return method.substring("function ".length).replace(/\).+$/, ")");
+};
+
+export const getMethodsFromAbi = (abi: ABI, type: "read" | "write" | "any"): ethers.utils.FunctionFragment[] => {
+ const getReadMethods = () => {
const readMethods = abi.filter(
(fragment) =>
fragment.type === "function" && (fragment.stateMutability === "view" || fragment.stateMutability === "pure")
);
const contractInterface = new ethers.utils.Interface(readMethods);
return contractInterface.fragments as ethers.utils.FunctionFragment[];
- } else {
+ };
+ const getWriteMethods = () => {
const writeMethods = abi.filter(
(fragment) =>
fragment.type === "function" &&
@@ -37,7 +44,13 @@ export const getMethodsFromAbi = (abi: ABI, type: "read" | "write"): ethers.util
);
const contractInterface = new ethers.utils.Interface(writeMethods);
return contractInterface.fragments as ethers.utils.FunctionFragment[];
+ };
+ if (type === "read") {
+ return getReadMethods();
+ } else if (type === "write") {
+ return getWriteMethods();
}
+ return [...getReadMethods(), ...getWriteMethods()];
};
export const checkIfMethodExists = (contractInfo: ContractInfo, method: string) => {
@@ -121,8 +134,11 @@ export const getContractInfoWithLoader = async (
};
export const askAbiMethod = async (
- contractInfo: ContractInfo,
- type: "read" | "write"
+ contractInfo: {
+ abi?: ContractInfo["abi"];
+ implementation?: ContractInfo["implementation"];
+ },
+ type: "read" | "write" | "any" = "any"
): Promise => {
if (!contractInfo.abi && !contractInfo.implementation?.abi) {
return "manual";
@@ -148,7 +164,7 @@ export const askAbiMethod = async (
};
const formatFragment = (fragment: ethers.utils.FunctionFragment): DistinctChoice => {
let name = fragment.format(ethers.utils.FormatTypes.full);
- if (type === "write" && name.includes(" returns ")) {
+ if ((type === "write" || type === "any") && name.includes(" returns ")) {
name = name.substring(0, name.indexOf(" returns ")); // remove return type for write methods
}
return {
@@ -161,6 +177,7 @@ export const askAbiMethod = async (
const separators = {
noReadMethods: { type: "separator", line: chalk.white("No read methods found") } as DistinctChoice,
noWriteMethods: { type: "separator", line: chalk.white("No write methods found") } as DistinctChoice,
+ noMethods: { type: "separator", line: chalk.white("No methods found") } as DistinctChoice,
contractNotVerified: { type: "separator", line: chalk.white("Contract is not verified") } as DistinctChoice,
};
choices.push(formatSeparator("Provided contract"));
@@ -169,7 +186,13 @@ export const askAbiMethod = async (
if (methods.length) {
choices.push(...methods.map(formatFragment));
} else {
- choices.push(type === "read" ? separators.noReadMethods : separators.noWriteMethods);
+ if (type === "read") {
+ choices.push(separators.noReadMethods);
+ } else if (type === "write") {
+ choices.push(separators.noWriteMethods);
+ } else {
+ choices.push(separators.noMethods);
+ }
}
} else {
choices.push(separators.contractNotVerified);
@@ -181,7 +204,13 @@ export const askAbiMethod = async (
if (implementationMethods.length) {
choices.push(...implementationMethods.map(formatFragment));
} else {
- choices.push(type === "read" ? separators.noReadMethods : separators.noWriteMethods);
+ if (type === "read") {
+ choices.push(separators.noReadMethods);
+ } else if (type === "write") {
+ choices.push(separators.noWriteMethods);
+ } else {
+ choices.push(separators.noMethods);
+ }
}
} else {
choices.push(separators.contractNotVerified);
diff --git a/src/commands/contract/write.ts b/src/commands/contract/write.ts
index 6fa85361..6b42b5fa 100644
--- a/src/commands/contract/write.ts
+++ b/src/commands/contract/write.ts
@@ -14,7 +14,13 @@ import {
showTransactionInfoOption,
} from "./common/options.js";
import { encodeData, encodeParam, getFragmentFromSignature, getInputsFromSignature } from "./utils/formatters.js";
-import { checkIfMethodExists, getContractInfoWithLoader, readAbiFromFile, askAbiMethod } from "./utils/helpers.js";
+import {
+ checkIfMethodExists,
+ getContractInfoWithLoader,
+ readAbiFromFile,
+ askAbiMethod,
+ formatMethodString,
+} from "./utils/helpers.js";
import { chainOption, l2RpcUrlOption, privateKeyOption } from "../../common/options.js";
import { l2Chains } from "../../data/chains.js";
import { getL2Provider, getL2Wallet, logFullCommandFromOptions, optionNameToParam } from "../../utils/helpers.js";
@@ -52,7 +58,7 @@ const askMethod = async (contractInfo: ContractInfo, options: WriteOptions) => {
const methodByAbi = await askAbiMethod(contractInfo, "write");
if (methodByAbi !== "manual") {
const fullMethodName = methodByAbi.format(ethers.utils.FormatTypes.full);
- options.method = fullMethodName.substring("function ".length).replace(/\).+$/, ")"); // remove "function " prefix and return type
+ options.method = formatMethodString(fullMethodName);
return;
}