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.

204 lines (183 loc) 6.71 kB
import { CONSOLE_ADDRESS } from "../constants"; import { AllowedCodes, DecodedMsg, MessageTree, TraceContext, TraceType } from "../types"; import { Address } from "everscale-inpage-provider"; import { ContractWithArtifacts } from "../../../types"; import { contractInformation, decoder, isErrorExistsInAllowedArr } from "./utils"; import { TracingInternal } from "../tracingInternal"; import * as nt from "nekoton-wasm"; export class Trace<Abi = any> { outTraces: Array<Trace> = []; error: null | { phase: "compute" | "action"; code: number | null; ignored?: boolean; reason: nt.TrComputeSkippedReason | undefined; } = null; transactionTrace: nt.EngineTraceInfo[] | undefined = undefined; type: TraceType | null = null; contract!: ContractWithArtifacts; decodedMsg: DecodedMsg | undefined = undefined; hasErrorInTree = false; constructor( private readonly tracing: TracingInternal, readonly msg: MessageTree, private readonly srcTrace: Trace | null, private readonly context: TraceContext, ) {} async buildTree( allowedCodes: AllowedCodes = { compute: [], action: [], contracts: { any: { compute: [], action: [] } } }, ) { this.setMsgType(); // eslint-disable-next-line @typescript-eslint/no-non-null-assertion const { codeHash, address } = contractInformation({ msg: this.msg, type: this.type!, ctx: this.context }); const contract = this.tracing.getSavedContract(address) || this.tracing.factory.getContractByCodeHashOrDefault(codeHash || "", new Address(address)); this.checkForErrors(allowedCodes); if (this.error && !this.error.ignored && this.msg.dstTransaction) { this.transactionTrace = this.tracing.network.getTxTrace(this.msg.dstTransaction.hash); } await this.decode(contract); for (const msg of this.msg.outMessages) { const trace = new Trace(this.tracing, msg, this, this.context); await trace.buildTree(allowedCodes); if (trace.hasErrorInTree) { this.hasErrorInTree = true; } this.outTraces.push(trace); } } // allowed_codes - {compute: [100, 50, 12], action: [11, 12]} checkForErrors( allowedCodes: AllowedCodes = { compute: [], action: [], contracts: { any: { compute: [], action: [] } } }, ) { const tx = this.msg.dstTransaction; if (this.msg.dst === CONSOLE_ADDRESS) { return; } let skipComputeCheck = false; if (tx && tx.compute.status === "vm" && tx.compute.success) { skipComputeCheck = true; } let skipActionCheck = false; if (tx && tx.action && tx.action.success) { skipActionCheck = true; } // error occured during compute phase if (!skipComputeCheck && tx) { if (tx.compute.status === "skipped") { this.error = { phase: "compute", code: null, reason: tx.compute.reason }; } else { this.error = { phase: "compute", code: tx.compute.exitCode, reason: undefined }; } // we didn't expect this error, save error if ( isErrorExistsInAllowedArr(allowedCodes.compute, this.error.code) || isErrorExistsInAllowedArr(allowedCodes.contracts?.[this.msg.dst as string]?.compute, this.error.code) ) { this.error.ignored = true; } } else if (!skipActionCheck && tx && tx.action && tx.action.resultCode !== 0) { this.error = { phase: "action", code: tx.action.resultCode, reason: undefined }; // we didn't expect this error, save error if ( isErrorExistsInAllowedArr(allowedCodes.action, tx.action.resultCode) || isErrorExistsInAllowedArr(allowedCodes.contracts?.[this.msg.dst as string]?.action, tx.action.resultCode) ) { this.error.ignored = true; } } if (this.error && !this.error.ignored) { this.hasErrorInTree = true; } } async decodeMsg(contract: ContractWithArtifacts | null = null): Promise< | { decoded: DecodedMsg; finalType: TraceType | null; } | undefined > { if (contract === null) { contract = this.contract; } if (this.msg.dst === CONSOLE_ADDRESS) { return; } if (this.type === TraceType.TRANSFER || this.type === TraceType.BOUNCE) { return; } if (this.type === TraceType.FUNCTION_CALL && this.srcTrace) { // this is responsible callback with answerId = 0, we cant decode it, however contract doesnt need it too // TODO: check // @ts-ignore if (this.srcTrace.decodedMsg && this.srcTrace.decodedMsg.value?.answerId === "0") { return; } } // function call, but we dont have contract here => we cant decode msg if (this.type === TraceType.FUNCTION_CALL && !contract) { return; } // 60 error on compute phase - wrong function id. We cant decode this msg with contract abi if (this.error && this.error.phase === "compute" && this.error.code === 60) { return; } if (!contract) { return; } return await decoder({ msgBody: this.msg.body as string, msgType: this.msg.msgType, contract, initialType: this.type, }); } async decode(contract: ContractWithArtifacts<Abi> | undefined) { // eslint-disable-next-line @typescript-eslint/no-non-null-assertion this.contract = contract!; const decoded = await this.decodeMsg(contract); if (decoded) { this.type = decoded.finalType; this.decodedMsg = decoded.decoded; } } setMsgType() { switch (this.msg.msgType) { // internal - deploy or function call or bound or transfer case "IntMsg": // code hash is presented, deploy if (this.msg.init?.codeHash !== undefined) { this.type = TraceType.DEPLOY; // bounced msg } else if (this.msg.bounced) { this.type = TraceType.BOUNCE; // empty body, just transfer } else if (this.msg.body === undefined) { this.type = TraceType.TRANSFER; } else { this.type = TraceType.FUNCTION_CALL; } return; // extIn - deploy or function call case "ExtIn": if (this.msg.init?.codeHash !== undefined) { this.type = TraceType.DEPLOY; } else { this.type = TraceType.FUNCTION_CALL; } return; // extOut - event or return case "ExtOut": // if this msg was produced by extIn msg, this can be return or event if (this.srcTrace !== null && this.srcTrace.msg.msgType === "ExtIn") { this.type = TraceType.EVENT_OR_FUNCTION_RETURN; } else { this.type = TraceType.EVENT; } return; default: return; } } }