UNPKG

locklift

Version:

Node JS framework for working with Ever contracts. Inspired by Truffle and Hardhat. Helps you to build, test, run and maintain your smart contracts.

253 lines (225 loc) 10.1 kB
import { Addressable, AllowedCodes, MessageTree, RevertedBranch, TraceType, TruncatedTransaction } from "./types"; import { logger } from "../logger"; import { Address } from "everscale-inpage-provider"; import BigNumber from "bignumber.js"; import { Trace } from "./trace/trace"; import path from "path"; import * as process from "process"; import chalk from "chalk"; import { EngineTraceInfo } from "nekoton-wasm"; import { ActionCodeHints, ComputeCodesHints, CONSOLE_ADDRESS } from "./constants"; const fs = require("fs"); export const extractAccountsFromMsgTree = (msgTree: MessageTree): Address[] => { const extractAccounts = (msgTree: MessageTree): Address[] => { const accounts: Address[] = msgTree.dst && msgTree.dst !== CONSOLE_ADDRESS ? [new Address(msgTree.dst)] : []; for (const outMsg of msgTree.outMessages) { accounts.push(...extractAccounts(outMsg)); } return accounts; }; return [...new Set(extractAccounts(msgTree))]; }; export const convert = (number: number, decimals = 9, precision = 4): string => { return (number / 10 ** decimals).toPrecision(precision); }; export const convertForLogger = (amount: number) => new BigNumber(convert(amount, 9, 8) || 0); export const hexToValue = (amount: number) => new BigNumber(convert(amount, 9, 9) || 0); type ErrorPosition = { filename: string; line: number; }; const normalizeFilePath = (errorPosition: ErrorPosition) => { let errFilePath = errorPosition.filename; // contracts paths look like: "../contracts/ContractName.tsol" if (errFilePath.startsWith("../contracts/")) { errFilePath = path.resolve(process.cwd(), errFilePath.split("../")[1]); } // .code paths look like: "ContractName.code" if (errFilePath.endsWith(".code")) { errFilePath = path.resolve(process.cwd(), "build", errFilePath); } return errFilePath; }; const printErrorPositionSnippet = (trace: Trace, filename: string, errLine: number, offset: number) => { const errFile = fs.readFileSync(filename, "utf8"); const lines = errFile.split("\n"); const lastLineLen = `${errLine + offset}`.length; const { name, method } = getContractNameAndMethod(trace); logger.printTracingLog( "".padStart(lastLineLen - 1, " "), chalk.blueBright.bold("-->"), chalk.bold(`${name}.${method} (${path.basename(filename)}:${errLine})`), ); logger.printTracingLog("".padStart(lastLineLen, " "), chalk.blueBright.bold("|")); const linesToPrint: string[][] = []; lines.map((line: string, i: number) => { if (i < errLine - offset - 1 || i >= errLine + offset) return; const lineNum = `${i + 1}`.padEnd(lastLineLen, " "); if (i === errLine - 1) { linesToPrint.push([chalk.redBright.bold(`${lineNum} |`), chalk.redBright(line)]); } else { linesToPrint.push([chalk.blueBright.bold(`${lineNum} |`), line]); } }); const firstNotEmpty = linesToPrint.findIndex(line => line[1].trim() !== ""); const lastNotEmpty = linesToPrint.length - linesToPrint.reverse().findIndex(line => line[1].trim() !== ""); linesToPrint .reverse() .slice(firstNotEmpty, lastNotEmpty) .map(line => { logger.printTracingLog(...line); }); logger.printTracingLog("".padStart(lastLineLen, " "), chalk.blueBright.bold("|")); }; export const throwTrace = (trace: Trace) => { // const _trace = trace.transactionTrace!.map((trace) => JSON.stringify(trace)).join('\n'); // fs.writeFileSync('log.json', _trace); logger.printTracingLog(chalk.redBright("-----------------------------------------------------------------")); // SKIPPED COMPUTE PHASE if (trace.error?.phase === "compute" && trace.error?.reason) { let errorDescription: string = trace.error?.reason; if (errorDescription === "NoState") { errorDescription = "NoState. Looks like you tried to call method of contract that doesn't exist"; } const errorMsg = `!!! Compute phase was skipped with reason: ${errorDescription} !!!`; logger.printError(errorMsg); throw new Error(errorMsg); } let errorDescription = ""; if (trace.error?.phase === "action") { errorDescription = ActionCodeHints[Number(trace.error.code)]; } if (trace.error?.phase === "compute") { errorDescription = ComputeCodesHints[Number(trace.error.code)]; } // short common error description const mainErrorMsg = `!!! Reverted with ${trace.error?.code} error code on ${trace.error?.phase} phase !!!`; logger.printError(mainErrorMsg); logger.printError(errorDescription); logger.printTracingLog(chalk.redBright("-----------------------------------------------------------------")); // no trace -> we cant detect line with error if (trace.transactionTrace === undefined) throw new Error(mainErrorMsg); const vmTraces = trace.transactionTrace; // no debug-map -> we cant detect line with error if (trace.contract.map === undefined) throw new Error(mainErrorMsg); const contract = trace.contract; const tx = trace.msg.dstTransaction as TruncatedTransaction; let errPosition: ErrorPosition | undefined; // COMPUTE PHASE ERROR if (tx.compute.status === "vm" && !tx.compute.success) { // last vm step is the error position const lastStep = vmTraces.pop() as EngineTraceInfo; errPosition = contract.map[lastStep.cmdCodeCellHash][lastStep.cmdCodeOffset]; if (errPosition === undefined) throw new Error(mainErrorMsg); } // ACTION PHASE ERROR if (tx.action?.success === false) { // catch all vm steps, where actions are produced const actionsSent = vmTraces.filter( t => t.cmdStr === "SENDRAWMSG" || t.cmdStr === "RAWRESERVE" || t.cmdStr === "SETCODE", ); let failedAction = tx.action.resultArg; // too many actions, point to 256th action if (Number(tx.action.resultCode) === 33) failedAction = 255; const failedActionStep = actionsSent[failedAction]; errPosition = contract.map[failedActionStep.cmdCodeCellHash][failedActionStep.cmdCodeOffset]; if (errPosition === undefined) throw new Error(mainErrorMsg); } const errFilePath = normalizeFilePath(errPosition as ErrorPosition); const errLineNum = (errPosition as ErrorPosition).line; const filename = path.basename(errFilePath); if (filename.endsWith(".tsol") || filename.endsWith(".sol")) { printErrorPositionSnippet(trace, errFilePath, errLineNum, 2); } throw new Error(mainErrorMsg); }; const getContractNameAndMethod = (trace: Trace) => { let name = "undefinedContract"; if (trace.contract) { name = trace.contract.name; } let method = "undefinedMethod"; if (trace.decodedMsg?.method) { method = trace.decodedMsg.method; } else if (trace.type === TraceType.BOUNCE) { method = "onBounce"; } return { name, method }; }; export const throwErrorInConsole = <Abi>(revertedBranch: Array<RevertedBranch<Abi>>) => { for (const { totalActions, actionIdx, traceLog } of revertedBranch) { const bounce = traceLog.msg.bounce; const { name, method } = getContractNameAndMethod(traceLog); let paramsStr = "()"; if (traceLog.decodedMsg) { if (Object.values(traceLog.decodedMsg.params || {}).length === 0) { paramsStr = "()"; } else { paramsStr = "(\n"; for (const [key, value] of Object.entries(traceLog.decodedMsg.params || {})) { paramsStr += ` ${key}: ${JSON.stringify(value, null, 4)}\n`; } paramsStr += ")"; } } logger.printTracingLog("\t\t⬇\n\t\t⬇"); logger.printTracingLog(`\t#${actionIdx + 1} action out of ${totalActions}`); // green tags logger.printTracingLog(`Addr: \x1b[32m${traceLog.msg.dst}\x1b[0m`); logger.printTracingLog(`MsgId: \x1b[32m${traceLog.msg.hash}\x1b[0m`); logger.printTracingLog("-----------------------------------------------------------------"); if (traceLog.type === TraceType.BOUNCE) { logger.printTracingLog("-> Bounced msg"); } if (traceLog.error && traceLog.error.ignored) { logger.printTracingLog(`-> Ignored ${traceLog.error.code} code on ${traceLog.error.phase} phase`); } if (!traceLog.contract) { logger.printTracingLog("-> Contract not deployed/Not recognized because build artifacts not provided"); } // bold tag logger.printTracingLog( `\x1b[1m${name}.${method}\x1b[22m{value: ${convert(Number(traceLog.msg.value))}, bounce: ${bounce}}${paramsStr}`, ); if (traceLog.msg.dstTransaction) { const tx = traceLog.msg.dstTransaction; if (tx.storage) { logger.printTracingLog(`Storage fees: ${convert(Number(tx.storage.storageFeesCollected))}`); } if (tx.compute) { const gasFees = tx.compute.status === "vm" ? tx.compute.gasFees : 0; logger.printTracingLog(`Compute fees: ${convert(Number(gasFees))}`); } if (tx.action) { logger.printTracingLog(`Action fees: ${convert(Number(tx.action.totalActionFees))}`); } logger.printTracingLog(chalk.bold("Total fees:"), `${convert(Number(tx.totalFees))}`); if (tx.compute.status === "vm") { const gasLimit = Number(tx.compute.gasLimit) === 0 ? 1000000 : Number(tx.compute.gasLimit); const percentage = ((Number(tx.compute.gasUsed) / gasLimit) * 100).toPrecision(2); logger.printTracingLog( chalk.bold("Gas used:"), `${Number(tx.compute.gasUsed).toLocaleString()}/${gasLimit.toLocaleString()} (${percentage}%)`, ); } } if (traceLog.error && !traceLog.error.ignored) { throwTrace(traceLog); } } }; export const isT = <T>(p: T): p is NonNullable<T> => !!p; export const getDefaultAllowedCodes = (): Omit<Required<AllowedCodes>, "contracts"> => ({ compute: [], action: [], }); export const isExistsInArr = <T>(srcArr: Array<T>, isExist: T): boolean => { return srcArr.some(item => item === isExist); }; export const extractStringAddress = (contract: Addressable) => typeof contract === "string" ? contract : contract instanceof Address ? contract.toString() : contract.address.toString(); export const extractAddress = (contract: Addressable): Address => new Address(extractStringAddress(contract));