UNPKG

txtracer-core-test-dev

Version:

Core TxTracer library for collecting transaction information

525 lines 22.3 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.shardAccountToBase64 = exports.calculateSentTotal = exports.findFinalActions = exports.computeFinalData = exports.prepareEmulator = exports.emulatePreviousTransactions = exports.collectUsedLibraries = exports.getLibraryByHash = exports.computeMinLt = exports.getBlockAccount = exports.getBlockConfig = exports.findAllTransactionsBetween = exports.findFullBlockForSeqno = exports.findShardBlockForTx = exports.findRawTxByHash = exports.findBaseTxByHash = void 0; exports.createShardAccountFromAPI = createShardAccountFromAPI; exports.normalizeStateFromAPI = normalizeStateFromAPI; const core_1 = require("@ton/core"); const axios_1 = __importDefault(require("axios")); const ton_1 = require("@ton/ton"); const sandbox_1 = require("@ton/sandbox"); const utils_1 = require("./utils"); // We don't usually want to store keys this way, but without keys it's almost // impossible to use API calls :( const TONCENTER_API_KEY = process.env["TONCENTER_API_KEY"] ?? "49efa980ccdcd018fd09d387e63537afd9db4dbb8509d69e7bc2303ca2b2c860"; const DTON_API_KEY = process.env["DTON_API_KEY"] ?? "fpYxhGTWfIe3ZEf2s6vvgAGmps_qnNmD"; const BASE_TIMEOUT = 20000; /** * Returns base transaction information by its hash. * @param testnet if true finds in testnet otherwise in mainnet * @param txHash transaction hash to find */ const findBaseTxByHash = async (testnet, txHash) => { const res = await axios_1.default.get(`https://${testnet ? "testnet." : ""}toncenter.com/api/v3/transactions`, { params: { hash: txHash, limit: 1 }, headers: { "X-API-Key": TONCENTER_API_KEY, }, }); const transactionInfo = res.data; const rawTx = transactionInfo.transactions.at(0); if (rawTx === undefined) { return undefined; } const lt = BigInt(rawTx.lt); const hash = Buffer.from(rawTx.hash, "base64"); const address = core_1.Address.parseRaw(rawTx.account); return { lt, hash, address }; }; exports.findBaseTxByHash = findBaseTxByHash; /** * Returns full information for transaction by base information obtained from `findBaseTxByHash` * @param testnet if true finds in testnet otherwise in mainnet * @param info information for search */ const findRawTxByHash = async (testnet, info) => { const { lt, hash, address } = info; const clientV4 = createTonClient4(testnet); return clientV4.getAccountTransactions(address, lt, hash); }; exports.findRawTxByHash = findRawTxByHash; /** * Return the shard‑block header that contains a given * {@link RawTransaction}. * * @param testnet Mainnet/testnet flag. * @param tx Raw transaction object. * @returns The matching shard‑block or `undefined` * if Toncenter cannot find it. */ const findShardBlockForTx = async (testnet, tx) => { const shard = tx.block; // normalize potentially negative shart to positive one const shardInt = BigInt(shard.shard); const shardUint = shardInt < 0 ? shardInt + BigInt("0x10000000000000000") : shardInt; const res = await axios_1.default.get(`https://${testnet ? "testnet." : ""}toncenter.com/api/v3/blocks`, { params: { workchain: shard.workchain, shard: "0x" + shardUint.toString(16), seqno: shard.seqno, }, }); return res.data.blocks[0]; }; exports.findShardBlockForTx = findShardBlockForTx; /** * Return a master‑block (full representation, including `shards[]`) * by its `seqno` via TON API v4. * * @param testnet Mainnet/testnet flag. * @param seqno Master‑block sequence number. * @returns The complete {@link BlockInfo}. */ const findFullBlockForSeqno = async (testnet, seqno) => { return createTonClient4(testnet).getBlock(seqno); }; exports.findFullBlockForSeqno = findFullBlockForSeqno; /** * Retrieve all transactions of a given account whose logical‑time * lies in the interval `(minLt, baseTx.lt]`, inclusive of `baseTx`. * * Used to reconstruct in‑block history before emulation. * * @param testnet Mainnet/testnet flag. * @param baseTx The “upper bound” transaction. * @param minLt Lower logical‑time boundary * @returns Transactions ordered **newest → oldest**. */ const findAllTransactionsBetween = async (testnet, baseTx, minLt) => { const clientV2 = new ton_1.TonClient({ endpoint: `https://${testnet ? "testnet." : ""}toncenter.com/api/v2/jsonRPC`, timeout: BASE_TIMEOUT, }); return clientV2.getTransactions(baseTx.address, { inclusive: true, lt: baseTx.lt.toString(), to_lt: (minLt - 1n).toString(), hash: baseTx.hash.toString("base64"), archival: true, limit: 1000, }); }; exports.findAllTransactionsBetween = findAllTransactionsBetween; /** * Load the global configuration cell valid for the master‑block that * encloses the target transaction. Required by the TVM executor to * calculate gas, random‑seed and limits exactly as onchain. * * @param testnet Mainnet/testnet flag. * @param block Full master‑block object (with `shards[]` array). * @returns Config cell as a string. */ const getBlockConfig = async (testnet, block) => { const clientV4 = createTonClient4(testnet); const blockSeqno = block.shards[0].seqno; const res = await clientV4.getConfig(blockSeqno); return res.config.cell; }; exports.getBlockConfig = getBlockConfig; /** * Return an account snapshot *before* the current master‑block. * The snapshot is converted to {@link ShardAccount} so it can be * directly fed into `runTransaction`. * * @param testnet Mainnet/testnet flag. * @param address Account address. * @param block Master‑block N (the one that contains the tx). * @returns ShardAccount representing state on master‑block N‑1. */ const getBlockAccount = async (testnet, address, block) => { const blockSeqno = block.shards[0].seqno; const clientV4 = createTonClient4(testnet); try { const res = await clientV4.getAccount(blockSeqno - 1, address); return createShardAccountFromAPI(res.account, address); } catch (error) { // @ton/ton testnet integration broken right now, fallback console.error("Cannot get account from API", error); const res = await getBlockAccountFallback(testnet, blockSeqno - 1, address); return createShardAccountFromAPI(res.data.account, address); } }; exports.getBlockAccount = getBlockAccount; async function getBlockAccountFallback(testnet, seqno, address) { const endpoint = `https://${testnet ? "sandbox" : "mainnet"}-v4.tonhubapi.com`; const path = `${endpoint}/block/${seqno}/${address.toString({ urlSafe: true })}`; return axios_1.default.get(path); } /** * Scan every shard‑summary inside a master‑block and return the * smallest `lt` for the specified account. This value marks the * earliest transaction of the account inside that master‑block. * * @param tx Target (latest) transaction object. * @param address Account address. * @param block Master‑block that contains `tx`. * @returns Minimum logical‑time as `bigint`. */ const computeMinLt = (tx, address, block) => { let minLt = tx.lt; const addrStr = address.toString(); for (const shard of block.shards) { for (const txInBlock of shard.transactions) { if (txInBlock.account === addrStr && BigInt(txInBlock.lt) < minLt) { minLt = BigInt(txInBlock.lt); } } } return minLt; }; exports.computeMinLt = computeMinLt; /** * Load a library cell (T‑lib) from dton.io GraphQL by its * 256‑bit hash. * * @param testnet Mainnet/testnet flag. * @param hash Hex string of the library hash. * @returns Decoded {@link Cell} containing actual code. * @throws Error if the library is missing on the server. */ const getLibraryByHash = async (testnet, hash) => { await (0, utils_1.wait)(1000); // needed if we load several libs in a row const dtonEndpoint = `https://${testnet ? "testnet." : ""}dton.io/${DTON_API_KEY}/graphql`; const graphqlQuery = { query: `query fetchAuthor { get_lib(lib_hash: "${hash}") }`, variables: {}, }; try { const res = await axios_1.default.post(dtonEndpoint, graphqlQuery, { headers: { "Content-Type": "application/json", }, }); return core_1.Cell.fromBase64(res.data.data.get_lib); } catch (error) { console.error("Error fetching library from dton:", error); if (error instanceof Error) { throw new Error("Get library on dton's graphql: " + error.message); } throw error; } }; exports.getLibraryByHash = getLibraryByHash; /** * Inspect the contract’s current code and (optionally) the init * code of the pending message, detect all **exotic library cells** * (tag 2) and build a dict mapping hash → real library code. * * @param testnet Mainnet/testnet flag. * @param account Current {@link ShardAccount} snapshot. * @param tx Transaction whose `inMessage` may include `Init`. * @returns Serialized dict cell or `undefined` * when no libraries are referenced and actual code cell if * original code is just an exotic library cell */ const collectUsedLibraries = async (testnet, account, tx) => { const libs = core_1.Dictionary.empty(core_1.Dictionary.Keys.BigUint(256), core_1.Dictionary.Values.Cell()); const addMaybeExoticLibrary = async (code) => { const EXOTIC_LIBRARY_TAG = 2; if (code === undefined) return undefined; if (code.bits.length !== 256 + 8) return undefined; // not an exotic library cell const cs = code.beginParse(true); // allow exotics const tag = cs.loadUint(8); if (tag !== EXOTIC_LIBRARY_TAG) return undefined; // not a library cell const libHash = cs.loadBuffer(32); const libHashHex = libHash.toString("hex").toUpperCase(); const actualCode = await (0, exports.getLibraryByHash)(testnet, libHashHex); libs.set(BigInt(`0x${libHashHex}`), actualCode); return actualCode; }; // if current contract code is exotic cell, we want to return actual code to the user let loadedCellCode = undefined; // 1. scan the *current* contract code for exotic‑library links const state = account.account?.storage.state; if (state?.type === "active") { // The contract is already deployed and “active” so its `code` // cell may itself be a 264‑bit exotic library reference (tag 2). // If that’s the case, download the real library code and // register it in the `libs` dictionary. loadedCellCode = await addMaybeExoticLibrary(state.state.code ?? undefined); } // 2. scan the *incoming StateInit* (if present) const init = tx.inMessage?.init; if (init) { // This transaction might *deploy* a brand‑new contract or // *upgrade* the existing one. Its `StateInit.code` could also // be an exotic library cell. We must preload such libraries as // well, otherwise the sandbox would fail to resolve a library // during emulation. loadedCellCode ?? (loadedCellCode = await addMaybeExoticLibrary(init.code ?? undefined)); } // no libs found, return undefined, for emulator this means no libraries if (libs.size === 0) return [undefined, loadedCellCode]; // emulator expects libraries as a Cell with immediate dictionary return [(0, core_1.beginCell)().storeDictDirect(libs).endCell(), loadedCellCode]; }; exports.collectUsedLibraries = collectUsedLibraries; /** * Convert an account record received from Toncenter / Tonhub API * (`AccountFromAPI`) into the low‑level `ShardAccount` structure * expected by core TON libraries and the sandbox executor. * * @param apiAccount Raw JSON account object from REST API. * @param address Parsed {@link Address} of the account * (API does not always include it). * @returns A fully‑typed {@link ShardAccount} ready for * serialization with `storeShardAccount`. */ function createShardAccountFromAPI(apiAccount, address) { const toBigint = (num) => (num === undefined ? 0n : BigInt(num)); return { account: { addr: address, storage: { lastTransLt: BigInt(apiAccount.last?.lt ?? 0), balance: { coins: BigInt(apiAccount.balance.coins) }, state: normalizeStateFromAPI(apiAccount.state), }, storageStats: { used: { cells: toBigint(apiAccount.storageStat?.used.cells), bits: toBigint(apiAccount.storageStat?.used.bits), }, lastPaid: apiAccount.storageStat?.lastPaid ?? 0, duePayment: typeof apiAccount.storageStat?.duePayment === "string" ? BigInt(apiAccount.storageStat.duePayment) : undefined, storageExtra: null }, }, lastTransactionLt: BigInt(apiAccount.last?.lt ?? 0), lastTransactionHash: apiAccount.last?.hash === undefined ? 0n : (0, utils_1.base64ToBigint)(apiAccount.last.hash), }; } /** * Transform the `state` sub‑object of an API response into the canonical * `AccountState` union used by `@ton/core`. * * @param givenState State payload exactly as returned by Toncenter API. * @returns Normalised `AccountState` object suitable for TVM. */ function normalizeStateFromAPI(givenState) { if (givenState.type === "uninit") { return { type: "uninit" }; } if (givenState.type === "frozen") { return { type: "frozen", stateHash: (0, utils_1.base64ToBigint)(givenState.stateHash), }; } return { type: "active", state: { code: givenState.code === null ? undefined : core_1.Cell.fromBase64(givenState.code), data: givenState.data === null ? undefined : core_1.Cell.fromBase64(givenState.data), }, }; } /** * Sequentially emulate the list of earlier transactions to roll * the shard‑account forward until the moment right before the * target transaction. Returns the updated balance and the new * base64‑encoded shard‑account string. * * @param prevBalance Balance at the snapshot start. * @param prevTxsInBlock Transactions to replay (oldest → newest). * @param emulate Helper that runs a single transaction. * @param shardAccountBase64 Starting shard‑account (base64). * @returns `{ prevBalance, shardAccountBase64 }` * after applying all txs. */ const emulatePreviousTransactions = async (prevBalance, prevTxsInBlock, emulate, shardAccountBase64) => { if (prevTxsInBlock.length === 0) { return { prevBalance, shardAccountBase64 }; } for (const tx of prevTxsInBlock) { const res = await emulate(tx, shardAccountBase64); if (!res.result.success) { throw new Error(`Transaction failed for lt: ${tx.lt}, logs: ${res.logs}, debugLogs: ${res.debugLogs}`); } // since we change state at each transaction we need to save new state as current one shardAccountBase64 = res.result.shardAccount; const shardAccount = (0, core_1.loadShardAccount)(core_1.Cell.fromBase64(shardAccountBase64).asSlice()); const newBalance = shardAccount.account?.storage.balance.coins; prevBalance = newBalance ?? 0n; } return { prevBalance, shardAccountBase64 }; }; exports.emulatePreviousTransactions = emulatePreviousTransactions; /** * Spin up TON Sandbox, configure verbosity, wrap the executor * into a convenience helper `emulate` and return both the helper * and the sandbox version metadata. * * @param blockConfig Global config cell. * @param libs Dict of referenced libraries or `undefined`. * @param randSeed Random seed from master‑block header. * @returns `{ emulatorVersion, emulate }` */ const prepareEmulator = async (blockConfig, libs, randSeed) => { const blockchain = await sandbox_1.Blockchain.create(); blockchain.verbosity.print = false; // don't print logs to stdout blockchain.verbosity.vmLogs = "vm_logs_verbose"; // most verbose logs including full Cells const executor = blockchain.executor; const emulatorVersion = executor instanceof sandbox_1.Executor ? executor.getVersion() : { commitHash: "", commitDate: "", }; async function emulate(tx, shardAccountBase64) { const inMsg = tx.inMessage; if (!inMsg) throw new Error("No in_message was found in transaction"); return executor.runTransaction({ config: blockConfig, libs: libs ?? null, verbosity: "full_location_stack_verbose", shardAccount: shardAccountBase64, message: (0, core_1.beginCell)().store((0, core_1.storeMessage)(inMsg)).endCell(), now: tx.now, lt: tx.lt, randomSeed: randSeed, ignoreChksig: false, debugEnabled: true, }); } return { emulatorVersion, emulate }; }; exports.prepareEmulator = prepareEmulator; /** * Convert the raw `EmulationResultSuccess` plus the prior balance * into a structured set of money movements, compute‑phase stats and * convenience fields for higher‑level reporting. * * @param res Successful result from TVM executor. * @param balanceBefore Balance **before** the emulated tx. * @returns Breakdown containing sender/dest, amounts, * gas usage and the parsed `emulatedTx`. */ const computeFinalData = (res, balanceBefore) => { const shardAccount = (0, core_1.loadShardAccount)(core_1.Cell.fromBase64(res.shardAccount).asSlice()); const endBalance = shardAccount.account?.storage.balance.coins ?? 0n; const emulatedTx = (0, core_1.loadTransaction)(core_1.Cell.fromBase64(res.transaction).asSlice()); if (!emulatedTx.inMessage) { throw new Error("No in_message was found in result tx"); } const src = emulatedTx.inMessage.info.src ?? undefined; const dest = emulatedTx.inMessage.info.dest; if (src !== undefined && !core_1.Address.isAddress(src)) { throw new Error(`Invalid src address: ${src.toString()}`); } if (!core_1.Address.isAddress(dest)) { throw new Error(`Invalid dest address: ${dest?.toString()}`); } const amount = emulatedTx.inMessage.info.type === "internal" ? emulatedTx.inMessage.info.value.coins : undefined; const sentTotal = (0, exports.calculateSentTotal)(emulatedTx); const totalFees = emulatedTx.totalFees.coins; if (emulatedTx.description.type !== "generic") { throw new Error("TxTracer doesn't support non-generic transaction. Given type: " + emulatedTx.description.type); } const computePhase = emulatedTx.description.computePhase; const computeInfo = computePhase.type === "skipped" ? "skipped" : { success: computePhase.success, exitCode: computePhase.exitCode === 0 ? (emulatedTx.description.actionPhase?.resultCode ?? 0) : computePhase.exitCode, vmSteps: computePhase.vmSteps, gasUsed: computePhase.gasUsed, gasFees: computePhase.gasFees, }; const money = { balanceBefore, sentTotal, totalFees, balanceAfter: endBalance, }; return { sender: src, contract: dest, money, emulatedTx, amount, computeInfo, }; }; exports.computeFinalData = computeFinalData; /** * Extract the final `c5` register (action list) from emulation results, * decode it into an array of `OutAction`s and * return both the list and the original `c5` cell. * * @param res Successful emulation result. * @returns `{ finalActions, c5 }` */ const findFinalActions = (res) => { const actions = res.actions; if (actions === null) { return { finalActions: [], c5: undefined }; } const c5 = core_1.Cell.fromBase64(actions); const finalActions = (0, core_1.loadOutList)(c5.asSlice()); return { finalActions, c5 }; }; exports.findFinalActions = findFinalActions; /** * Sum the value (`coins`) of every *internal* outgoing message * produced by a transaction. External messages are ignored since its * value is always 0. * * @param tx Parsed {@link Transaction}. * @returns Total toncoins sent out by the contract in this tx. */ const calculateSentTotal = (tx) => { let total = 0n; for (const msg of tx.outMessages.values()) { if (msg.info.type === "internal") { total += msg.info.value.coins; } } return total; }; exports.calculateSentTotal = calculateSentTotal; /** * Helper to serialize a {@link ShardAccount} object into base64 * exactly as expected by `executor.runTransaction`. * * @param shardAccountBeforeTx Account snapshot to serialize. * @returns Base64 string of the BOC‑encoded cell. */ const shardAccountToBase64 = (shardAccountBeforeTx) => (0, core_1.beginCell)().store((0, core_1.storeShardAccount)(shardAccountBeforeTx)).endCell().toBoc().toString("base64"); exports.shardAccountToBase64 = shardAccountToBase64; const createTonClient4 = (testnet) => new ton_1.TonClient4({ endpoint: `https://${testnet ? "sandbox" : "mainnet"}-v4.tonhubapi.com`, timeout: BASE_TIMEOUT, requestInterceptor: config => { config.headers["Content-Type"] = "application/json"; return config; }, }); //# sourceMappingURL=methods.js.map