Skip to content

Commit

Permalink
feat: add contract encode command (#118)
Browse files Browse the repository at this point in the history
  • Loading branch information
JackHamer09 authored Jan 30, 2024
1 parent 242d0a2 commit fd019ae
Show file tree
Hide file tree
Showing 7 changed files with 227 additions and 13 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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).

Expand Down
42 changes: 41 additions & 1 deletion docs/contract-interaction.md
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
# 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)
- [Contract Write - Executing write operations](#contract-write)
- [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)
Expand Down Expand Up @@ -186,6 +187,45 @@ npx zksync-cli contract write \
<br />
#### 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
```
<br />
---
<br />
### 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
Expand Down
131 changes: 131 additions & 0 deletions src/commands/contract/encode.ts
Original file line number Diff line number Diff line change
@@ -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<EncodeOptions, "method"> = 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);
1 change: 1 addition & 0 deletions src/commands/contract/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import "./read.js";
import "./write.js";
import "./encode.js";

import "./command.js"; // registers all the commands above
10 changes: 8 additions & 2 deletions src/commands/contract/read.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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);
}
Expand Down
45 changes: 37 additions & 8 deletions src/commands/contract/utils/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,23 +21,36 @@ 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" &&
(fragment.stateMutability === "nonpayable" || fragment.stateMutability === "payable")
);
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) => {
Expand Down Expand Up @@ -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<ethers.utils.FunctionFragment | "manual"> => {
if (!contractInfo.abi && !contractInfo.implementation?.abi) {
return "manual";
Expand All @@ -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 {
Expand All @@ -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"));
Expand All @@ -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);
Expand All @@ -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);
Expand Down
10 changes: 8 additions & 2 deletions src/commands/contract/write.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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;
}

Expand Down

0 comments on commit fd019ae

Please sign in to comment.