zksync-cli
Version:
CLI tool that simplifies the process of developing applications and interacting with the ZKsync network
265 lines • 11.8 kB
JavaScript
import chalk from "chalk";
import { Option } from "commander";
import { ethers, AbiCoder } from "ethers";
import inquirer from "inquirer";
import ora from "ora";
import { utils } from "zksync-ethers";
import Program from "./command.js";
import { chainOption, l2RpcUrlOption } from "../../common/options.js";
import { promptChain } from "../../common/prompts.js";
import { ETH_TOKEN } from "../../utils/constants.js";
import { useDecimals, convertBigNumbersToStrings, formatSeparator, getTimeAgo } from "../../utils/formatters.js";
import { getL2Provider, optionNameToParam } from "../../utils/helpers.js";
import Logger from "../../utils/logger.js";
import { isTransactionHash } from "../../utils/validators.js";
import { abiOption } from "../contract/common/options.js";
import { getContractInformation, readAbiFromFile } from "../contract/utils/helpers.js";
const transactionHashOption = new Option("--tx, --transaction <transaction hash>", "Transaction hash");
const fullOption = new Option("--full", "Show all available data");
const rawOption = new Option("--raw", "Show raw JSON response");
export const handler = async (options) => {
const getTransactionFeeData = (receipt) => {
const transfers = [];
receipt.logs.forEach((log) => {
try {
const parsed = utils.IERC20.decodeEventLog("Transfer", log.data, log.topics);
transfers.push({
from: parsed.from,
to: parsed.to,
amount: parsed.value,
});
}
catch {
// ignore
}
});
const totalFee = receipt.gasUsed * receipt.gasPrice;
const refunded = transfers.reduce((acc, transfer) => {
if (transfer.from === utils.BOOTLOADER_FORMAL_ADDRESS) {
return acc + transfer.amount;
}
return acc;
}, 0n);
return {
refunded,
totalFee,
paidByPaymaster: !transfers.length ||
receipt.from !== transfers.find((transfer) => transfer.from === utils.BOOTLOADER_FORMAL_ADDRESS)?.to,
};
};
const getDecodedMethodSignature = async (hexSignature) => {
if (hexSignature === "0x") {
return;
}
return await fetch(`https://www.4byte.directory/api/v1/signatures/?format=json&hex_signature=${hexSignature}`, {
method: "GET",
headers: {
"Content-Type": "application/json",
},
})
.then((res) => res.json())
.then((data) => data?.results?.[0]?.text_signature)
.catch(() => undefined);
};
const getAddressAndMethodInfo = async (address, calldata, provider, chain) => {
let contractInfo = await getContractInformation(chain, provider, address, { fetchImplementation: true }).catch(() => undefined);
const hexSignature = calldata.slice(0, 10);
let decodedSignature;
let decodedArgs;
if (options.abi) {
if (!contractInfo) {
contractInfo = {
address,
bytecode: "0x",
abi: readAbiFromFile(options.abi),
};
}
else {
contractInfo.abi = readAbiFromFile(options.abi);
}
}
if (contractInfo?.abi || contractInfo?.implementation?.abi) {
const initialAddressInterface = new ethers.Interface(contractInfo?.abi || []);
const implementationInterface = new ethers.Interface(contractInfo?.implementation?.abi || []);
const matchedMethod = initialAddressInterface.getFunction(hexSignature) || implementationInterface.getFunction(hexSignature);
if (matchedMethod) {
decodedSignature = matchedMethod.format("full");
if (decodedSignature.startsWith("function")) {
decodedSignature = decodedSignature.slice("function".length + 1);
}
}
}
if (!decodedSignature) {
decodedSignature = await getDecodedMethodSignature(hexSignature);
}
if (decodedSignature) {
try {
const contractInterface = new ethers.Interface([`function ${decodedSignature}`]);
const fragment = contractInterface.getFunction(hexSignature);
if (!fragment) {
throw new Error(`Function ${hexSignature} not in ABI`);
}
const inputs = fragment.inputs;
const encodedArgs = calldata.slice(10);
const decoded = AbiCoder.defaultAbiCoder().decode(inputs, `0x${encodedArgs}`);
decodedArgs = inputs.map((input, index) => {
return {
name: input.name,
type: input.type,
value: decoded[index]?.toString(),
};
});
}
catch {
// ignore
}
}
return {
contractInfo,
hexSignature,
decodedSignature,
decodedArgs,
};
};
try {
const chain = await promptChain({
message: chainOption.description,
name: optionNameToParam(chainOption.long),
}, undefined, options);
const answers = await inquirer.prompt([
{
message: transactionHashOption.description,
name: optionNameToParam(transactionHashOption.long),
type: "input",
required: true,
validate: (input) => isTransactionHash(input),
},
], options);
options = {
...options,
...answers,
};
const l2Provider = getL2Provider(options.rpc ?? chain.rpcUrl);
const spinner = ora("Looking for transaction...").start();
try {
const [transactionData, transactionDetails, transactionReceipt] = await Promise.all([
l2Provider.getTransaction(options.transaction),
l2Provider.getTransactionDetails(options.transaction),
l2Provider.getTransactionReceipt(options.transaction),
]);
if (!transactionData) {
throw new Error("Transaction not found");
}
const { contractInfo, hexSignature: methodHexSignature, decodedSignature: methodDecodedSignature, decodedArgs: methodDecodedArgs, } = await getAddressAndMethodInfo(transactionData.to, transactionData.data, l2Provider, chain);
spinner.stop();
if (options.raw) {
Logger.info(JSON.stringify(convertBigNumbersToStrings(transactionReceipt || transactionDetails || transactionData), null, 2), {
noFormat: true,
});
return;
}
const { bigNumberToDecimal } = useDecimals(ETH_TOKEN.decimals);
Logger.info(formatSeparator("Main info").line, { noFormat: true });
let logString = "";
/* Main */
logString += `Transaction hash: ${transactionData.hash}`;
logString += "\nStatus: ";
if (transactionDetails?.status === "failed") {
logString += chalk.redBright("failed");
}
else if (transactionDetails?.status === "included" || transactionDetails?.status === "verified") {
logString += chalk.greenBright("completed");
}
else {
logString += transactionDetails?.status || chalk.gray("N/A");
}
logString += `\nFrom: ${transactionData.from}`;
logString += `\nTo: ${transactionData.to}`;
if (contractInfo?.implementation) {
logString += chalk.gray(" |");
logString += chalk.gray(` Implementation: ${contractInfo.implementation.address}`);
}
logString += `\nValue: ${bigNumberToDecimal(transactionData.value)} ETH`;
const initialFee = transactionData.gasLimit * transactionData.gasPrice;
const feeData = transactionReceipt ? getTransactionFeeData(transactionReceipt) : undefined;
logString += `\nFee: ${bigNumberToDecimal(feeData?.totalFee || initialFee)} ETH`;
if (feeData?.paidByPaymaster) {
logString += chalk.gray(" (paid by paymaster)");
}
if (feeData) {
logString += chalk.gray(" |");
logString += chalk.gray(` Initial: ${bigNumberToDecimal(initialFee)} ETH`);
logString += chalk.gray(` Refunded: ${bigNumberToDecimal(feeData.refunded)} ETH`);
}
logString += "\nMethod: ";
if (methodDecodedSignature) {
logString += `${methodDecodedSignature} `;
}
if (methodHexSignature !== "0x") {
logString += chalk.gray(methodHexSignature);
}
else {
logString += chalk.gray("N/A");
}
Logger.info(logString, { noFormat: true });
logString = "";
if (methodDecodedArgs) {
Logger.info(`\n${formatSeparator("Method arguments").line}`, { noFormat: true });
methodDecodedArgs.forEach((arg, index) => {
if (index !== 0) {
logString += "\n";
}
logString += `[${index + 1}] `;
if (arg.name) {
logString += `${arg.name} ${chalk.gray(`(${arg.type})`)}: `;
}
else {
logString += `${chalk.gray(arg.type)}: `;
}
logString += arg.value;
});
Logger.info(logString, { noFormat: true });
logString = "";
}
Logger.info(`\n${formatSeparator("Details").line}`, { noFormat: true });
logString += "Date: ";
let transactionDate;
if (transactionDetails?.receivedAt) {
transactionDate = new Date(transactionDetails.receivedAt);
}
if (transactionDate) {
logString += transactionDate.toLocaleString();
logString += chalk.gray(` (${getTimeAgo(transactionDate)})`);
}
else {
logString += chalk.gray("N/A");
}
logString += `\nBlock: #${transactionData.blockNumber}`;
logString += `\nNonce: ${transactionData.nonce}`;
if (options.full) {
logString += `\nTransaction type: ${transactionData.type}`;
logString += `\nEthereum commit hash: ${transactionDetails?.ethCommitTxHash || chalk.gray("in progress")}`;
logString += `\nEthereum prove hash: ${transactionDetails?.ethProveTxHash || chalk.gray("in progress")}`;
logString += `\nEthereum execute hash: ${transactionDetails?.ethExecuteTxHash || chalk.gray("in progress")}`;
}
Logger.info(logString, { noFormat: true });
}
finally {
spinner.stop();
}
}
catch (error) {
Logger.error("There was an error getting transaction info:");
Logger.error(error);
}
};
Program.command("info")
.description("Get transaction info")
.addOption(transactionHashOption)
.addOption(chainOption)
.addOption(l2RpcUrlOption)
.addOption(fullOption)
.addOption(rawOption)
.addOption(abiOption)
.action(handler);
//# sourceMappingURL=info.js.map