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.

228 lines (205 loc) 8.94 kB
import { Address, Contract, ProviderRpcClient } from "everscale-inpage-provider"; import { consoleAbi, ConsoleAbi } from "../../console.abi"; import { CONSOLE_ADDRESS } from "./constants"; import { extractAccountsFromMsgTree, extractStringAddress, getDefaultAllowedCodes, throwErrorInConsole } from "./utils"; import { Trace } from "./trace/trace"; import { AccountData, Addressable, AllowedCodes, MessageTree, OptionalContracts, RevertedBranch, TraceParams, TransactionWithAccountAndBoc, TruncatedTransaction, } from "./types"; import { Factory } from "../factory"; import _, { difference } from "lodash"; import { logger } from "../logger"; import { ViewTracingTree } from "./viewTraceTree/viewTracingTree"; import { extractTransactionFromParams } from "../../utils"; import { TracingTransport } from "./transport"; import { decodeRawTransaction, JsRawMessage } from "nekoton-wasm"; import { LockliftNetwork } from "@broxus/locklift-network"; import { ContractWithArtifacts } from "../../types"; export class TracingInternal { private labelsMap = new Map<string, string>(); private savedContracts = new Map<string, ContractWithArtifacts>(); private readonly consoleContract: Contract<ConsoleAbi>; private _allowedCodes: Required<AllowedCodes> = { ...getDefaultAllowedCodes(), contracts: {}, }; public setContractLabels = (contracts: Array<{ address: Addressable; label: string }>) => { contracts.forEach(({ address, label }) => this.labelsMap.set(extractStringAddress(address), label)); }; constructor( private readonly ever: ProviderRpcClient, readonly factory: Factory<any>, private readonly tracingTransport: TracingTransport, readonly network: LockliftNetwork, ) { this.consoleContract = new ever.Contract(consoleAbi, new Address(CONSOLE_ADDRESS)); } saveContract = (address: Addressable, contract: ContractWithArtifacts) => { const stringAddress = extractStringAddress(address); this.savedContracts.set(stringAddress, contract); }; getSavedContract = (address: Addressable): ContractWithArtifacts | undefined => { const stringAddress = extractStringAddress(address); return this.savedContracts.get(stringAddress); }; get allowedCodes(): AllowedCodes { return this._allowedCodes; } setAllowedCodes(allowedCodes: OptionalContracts) { if (allowedCodes.action) { this._allowedCodes.action.push(...allowedCodes.action); } if (allowedCodes.compute) { this._allowedCodes.compute.push(...allowedCodes.compute); } } setAllowedCodesForAddress(address: string | Address, allowedCodes: OptionalContracts) { const stringAddress = address.toString(); if (!this._allowedCodes.contracts?.[stringAddress]) { this._allowedCodes.contracts[stringAddress] = getDefaultAllowedCodes(); } if (allowedCodes.compute) { (this._allowedCodes.contracts[stringAddress].compute || []).push(...allowedCodes.compute); } if (allowedCodes.action) { (this._allowedCodes.contracts[stringAddress].action || []).push(...allowedCodes.action); } } removeAllowedCodesForAddress(address: string | Address, codesToRemove: OptionalContracts) { const stringAddress = address.toString(); if (codesToRemove.compute) { this._allowedCodes.contracts[stringAddress].compute = difference( this._allowedCodes.contracts[stringAddress]?.compute || [], codesToRemove.compute, ); } if (codesToRemove.action) { this._allowedCodes.contracts[stringAddress].action = difference( this._allowedCodes.contracts[stringAddress]?.action || [], codesToRemove.action, ); } } removeAllowedCodes(codesToRemove: OptionalContracts) { if (codesToRemove.compute) { this._allowedCodes.compute = difference(this._allowedCodes.compute || [], codesToRemove.compute); } if (codesToRemove.action) { this._allowedCodes.action = difference(this._allowedCodes.action || [], codesToRemove.action); } } private popKey = (obj: any, key: string): any => { const value = obj[key]; delete obj[key]; return value; }; // input transactions are unordered private buildMsgTree = async (transactions: TransactionWithAccountAndBoc[]): Promise<MessageTree> => { type _ShortMessageTree = JsRawMessage & { dstTransaction: TruncatedTransaction | undefined; outMessages: Array<JsRawMessage>; }; // restructure transaction inside out for more convenient access in later processing const hashToMsg: { [msg_hash: string]: _ShortMessageTree } = {}; const msgs = transactions.map((tx): _ShortMessageTree => { const extendedTx = decodeRawTransaction(tx.boc); const inMsg: JsRawMessage = this.popKey(extendedTx, "inMessage"); const description = this.popKey(extendedTx, "description"); const outMsgs: JsRawMessage[] = this.popKey(extendedTx, "outMessages"); const msg: _ShortMessageTree = { ...inMsg, dstTransaction: { ...extendedTx, ...description }, outMessages: outMsgs, }; hashToMsg[msg.hash] = msg; // special move for extOut messages (events), because they don't have dstTransaction msg.outMessages.map(outMsg => { if (outMsg.msgType === "ExtOut") { // console.log(outMsg); hashToMsg[outMsg.hash] = { ...outMsg, dstTransaction: undefined, outMessages: [] }; } }); return msg; }); // recursively build message tree const buildTree = async (msgHash: string): Promise<MessageTree> => { const msg = hashToMsg[msgHash]; if (msg.dst === CONSOLE_ADDRESS) { await this.printConsoleMsg(msg as MessageTree); } const outMessages = await Promise.all(msg.outMessages.map(async outMsg => buildTree(outMsg.hash))); return { ...msg, outMessages }; }; return await buildTree(msgs[0].hash); }; // allowed_codes example - {compute: [100, 50, 12], action: [11, 12], "ton_addr": {compute: [60], action: [2]}} async trace<T>({ finalizedTx, allowedCodes, raise = true }: TraceParams<T>): Promise<ViewTracingTree | undefined> { // @ts-ignore const externalTx = extractTransactionFromParams(finalizedTx.extTransaction) as TransactionWithAccountAndBoc; const msgTree = await this.buildMsgTree([externalTx, ...finalizedTx.transactions]); const accounts = extractAccountsFromMsgTree(msgTree); const accountDataList = await this.tracingTransport.getAccountsData(accounts); const accountDataMap = accountDataList.reduce( (acc, accountData) => ({ ...acc, [accountData.id]: accountData }), {}, ); const allowedCodesExtended = _.mergeWith(_.cloneDeep(this._allowedCodes), allowedCodes, (objValue, srcValue) => Array.isArray(objValue) ? objValue.concat(srcValue) : undefined, ); const traceTree = await this.buildTracingTree(msgTree, allowedCodesExtended, accountDataMap); const reverted = this.findRevertedBranch(_.cloneDeep(traceTree)); if (reverted && raise) { throwErrorInConsole(reverted); } return new ViewTracingTree(traceTree, this.factory.getContractByCodeHash, accountDataList); } private async printConsoleMsg(msg: MessageTree) { const decoded = await this.ever.rawApi.decodeEvent({ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion body: msg.body!, abi: JSON.stringify(consoleAbi), event: "Log", }); logger.printInfo(decoded && "_log" in decoded.data && decoded.data._log); } private async buildTracingTree( msgTree: MessageTree, allowedCodes: AllowedCodes = { compute: [], action: [], contracts: { any: { compute: [], action: [] } } }, accountData: { [key: string]: AccountData }, ): Promise<Trace> { const trace = new Trace(this, msgTree, null, { accounts: accountData }); await trace.buildTree(allowedCodes); return trace; } // apply depth-first search on trace tree, return first found reverted branch private findRevertedBranch(traceTree: Trace): Array<RevertedBranch> | undefined { if (!traceTree.hasErrorInTree) { return; } return this.depthSearch(traceTree, 1, 0); } private depthSearch(traceTree: Trace, totalActions: number, actionIdx: number): Array<RevertedBranch> | undefined { if (traceTree.error && !traceTree.error.ignored) { // clean unnecessary structure traceTree.outTraces = []; return [{ totalActions, actionIdx: actionIdx, traceLog: traceTree }]; } for (const [index, trace] of traceTree.outTraces.entries()) { const actionsNum = traceTree.outTraces.length; const corruptedBranch = this.depthSearch(trace, actionsNum, index); if (corruptedBranch) { // clean unnecessary structure traceTree.outTraces = []; return [{ totalActions, actionIdx, traceLog: traceTree }].concat(corruptedBranch); } } } }