Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add contract encode command #118

Merged
merged 2 commits into from
Jan 30, 2024
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
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
Loading