import { Addressable, BalanceChangeInfoStorage, ErrorStore, MsgError, TraceType, ViewTrace, ViewTraceTree, ViewTraceTreeWithTotalFee, } from "../types"; import _ from "lodash"; import { Address, Contract, DecodedEventWithTransaction } from "everscale-inpage-provider"; import { AbiEventName } from "everscale-inpage-provider/dist/models"; import { extractAddress, extractStringAddress, fetchAccounts, isT } from "../utils"; import { ContractWithName } from "../../../types"; import { applyTotalFees, calculateTotalFees, getBalanceChangingInfo, getBalanceDiff, getErrorsInfo, printer, PrinterConfig, } from "./utils"; import { Tokens } from "./tokens"; import { pipe } from "rxjs"; type NameAndType = { type: TraceType; name: T; contract?: Addressable }; type EventNames = DecodedEventWithTransaction>["event"]; type EventsNamesInner> = EventNames ? f : never>; //@ts-ignore type MethodParams, N extends keyof C["methods"]> = Parameters[0]; type EventParams = Extract< DecodedEventWithTransaction>, { event: N } >["data"]; export class ViewTracingTree { readonly viewTraceTree: ViewTraceTreeWithTotalFee; readonly tokens: Tokens; balanceChangeInfo: BalanceChangeInfoStorage; msgErrorsStore: ErrorStore; constructor( viewTraceTree: ViewTraceTree, private readonly contractGetter: (codeHash: string, address: Address) => ContractWithName | undefined, private readonly endpoint: string, ) { this.viewTraceTree = applyTotalFees(_.cloneDeep(viewTraceTree)); this.balanceChangeInfo = pipe(getBalanceChangingInfo, getBalanceDiff)(this.viewTraceTree); this.msgErrorsStore = getErrorsInfo(this.viewTraceTree); this.tokens = new Tokens(this.viewTraceTree); } getErrorsByContract = | Address | string>(contract: T): Array => { return this.msgErrorsStore[extractStringAddress(contract)]; }; getAllErrors = () => Object.entries(this.msgErrorsStore).flatMap(([key, errors]) => errors.map(error => ({ contract: key, ...error }))); getBalanceDiff = | Address | string>( contracts: T[] | T, ): T extends T[] ? Record : string => { if (Array.isArray(contracts)) { return contracts.reduce((acc, contract) => { const address = extractStringAddress(contract); return { ...acc, [address as string]: this.balanceChangeInfo[address.toString()]?.balanceDiff.toString() }; }, {} as Record) as T extends T[] ? Record : string; } const address = extractStringAddress(contracts); return this.balanceChangeInfo[address].balanceDiff.toString() as T extends T[] ? Record : string; }; findCallsForContract = , N extends keyof C["methods"] & string>({ contract, name, }: { contract: C } & { name: N }) => { return this.findForContract({ contract, name }) .map(el => el?.params) .filter(isT); }; findEventsForContract = < C extends Contract, Abi extends C extends Contract ? f : never, N extends EventNames, >({ contract, name, }: { contract: C } & { name: N }) => this.findForContract({ name, contract }) .map(el => el?.params) .filter(isT); findForContract = < C extends Contract, N extends (keyof C["methods"] & string) | EventsNamesInner, Abi extends C extends Contract ? f : never, E extends N extends keyof C["methods"] & string ? MethodParams : EventParams, >({ contract, name, }: { contract: C } & { name: N }) => { if (name in contract.methodsAbi) { return this.findByType>({ name, type: TraceType.FUNCTION_CALL, contract }); } return this.findByType({ name, type: TraceType.EVENT, contract }); }; findByType = (params: NameAndType): Array["decodedMsg"]> => this._findByType(params, this.viewTraceTree).map(el => el.decodedMsg); findByTypeWithFullData = (params: NameAndType) => this._findByType(params, this.viewTraceTree); private _findByType = ( { type, name, contract }: NameAndType, tree: ViewTraceTree, ): Array> => { const matchedMethods: Array> = []; for (const trace of tree.outTraces) { if ( type === trace.type && name === trace.decodedMsg?.method && (contract ? extractAddress(contract).equals(extractAddress(trace.contract.contract)) : true) ) { matchedMethods.push(trace as any); } if (trace.outTraces.length > 0) { matchedMethods.push(...this._findByType({ name, type, contract }, trace)); } } return matchedMethods; }; totalGasUsed = () => calculateTotalFees(this.viewTraceTree).toNumber(); beautyPrint = async (printerConfig?: PrinterConfig): Promise => { const result = _(extractAllAddresses(this.viewTraceTree)).uniq().value(); const contracts = (await fetchAccounts(result, this.endpoint)).map(({ code_hash: codeHash, id }) => this.contractGetter(codeHash, new Address(id)), ); console.log( printer( this.viewTraceTree, { contracts, }, printerConfig, ) + "\n" + this._beautyPrint(this.viewTraceTree, 0, contracts, printerConfig), ); }; private _beautyPrint = ( viewTrace: ViewTraceTreeWithTotalFee, offset: number, contracts: Array, printerConfig?: PrinterConfig, ): string => { let traces = ""; for (const viewTraceInt of viewTrace.outTraces) { traces = traces + `${Array(offset).fill(" ").join("")} ${printer( viewTraceInt, { contracts, }, printerConfig, )}\n${this._beautyPrint(viewTraceInt, offset + 1, contracts, printerConfig)}`; } return traces; }; } const extractAllAddresses = (viewTrace: ViewTraceTree): Array
=> { const addresses: Array
= extractAddressFromObject(viewTrace.decodedMsg || {}); return viewTrace.outTraces.reduce((acc, next) => { return [...acc, ...extractAllAddresses(next)]; }, addresses); }; const extractAddressFromObject = (obj: Record): Array
=> { return Object.values(obj).reduce((acc, value) => { if (value instanceof Address) { return [...acc, value.toString()]; } if (value instanceof Object) { return [...acc, ...extractAddressFromObject(value)]; } return acc; }, [] as Array
); };