UNPKG

@tatumio/transaction-simulator

Version:

Transaction Simulation Extension

207 lines 10.2 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.TransactionSimulator = void 0; const tatum_1 = require("@tatumio/tatum"); const bignumber_js_1 = __importDefault(require("bignumber.js")); const ethers_1 = require("ethers"); const consts_1 = require("./consts"); const tracer_1 = require("./tracer"); const utils_1 = require("./utils"); class TransactionSimulator extends tatum_1.TatumSdkExtension { constructor(tatumSdkContainer) { super(tatumSdkContainer); this.supportedNetworks = [ tatum_1.Network.ARBITRUM_ONE, tatum_1.Network.AVALANCHE_C, tatum_1.Network.CELO, tatum_1.Network.CELO_ALFAJORES, tatum_1.Network.CHILIZ, tatum_1.Network.ETHEREUM, tatum_1.Network.ETHEREUM_SEPOLIA, tatum_1.Network.ETHEREUM_GOERLI, tatum_1.Network.ETHEREUM_HOLESKY, tatum_1.Network.ETHEREUM_CLASSIC, tatum_1.Network.POLYGON, tatum_1.Network.POLYGON_MUMBAI, tatum_1.Network.BINANCE_SMART_CHAIN, tatum_1.Network.BINANCE_SMART_CHAIN_TESTNET, tatum_1.Network.OPTIMISM, tatum_1.Network.OPTIMISM_TESTNET, ]; this.evmRpc = this.tatumSdkContainer.getRpc(); } init() { this.minifiedTracer = tracer_1.TRACER .replace(/\s+/g, ' ') .replace(/\s*{\s*/g, '{') .replace(/\s*}\s*/g, '}') .replace(/\s*\(\s*/g, '(') .replace(/\s*\)\s*/g, ')') .replace(/\s*:\s*/g, ':') .replace(/\s*,\s*/g, ','); return Promise.resolve(); } async simulateTransfer(payload) { const transferPayload = this.getTransferPayload(payload); await this.prepareFees(transferPayload); const trace = await this.getTraceCall(transferPayload); this.handleTraceError(trace); return this.mapTraceToSimulationResultNative(payload, transferPayload, trace); } async simulateTransferErc20(payload) { const tokenDetails = await this.getTokenDetails(payload); const data = this.generateErc20TransferData(payload.to, payload.value, tokenDetails); const tokenTransferPayload = this.getTokenTransferPayload(payload, data); await this.prepareFees(tokenTransferPayload); const trace = await this.getTraceCall(tokenTransferPayload); this.handleTraceError(trace); const matchedSlots = (0, utils_1.matchStorageSlotsToAddresses)([payload.to, payload.from], this.getStorageAddresses(trace, payload)); return this.mapTraceToSimulationResultErc20(payload, tokenTransferPayload, trace, tokenDetails, matchedSlots); } async getTokenDetails(payload) { const decimalsPromise = this.evmRpc.getTokenDecimals(payload.tokenContractAddress); const tokenNamePromise = this.evmRpc.getTokenName(payload.tokenContractAddress); const tokenSymbolPromise = this.evmRpc.getTokenSymbol(payload.tokenContractAddress); const [decimals, tokenName, tokenSymbol] = await Promise.all([ decimalsPromise, tokenNamePromise, tokenSymbolPromise, ]); if (!decimals.result) throw new Error(`Failed to retrieve decimals for contract: ${payload.to} - ${JSON.stringify(decimals.error)}`); if (!tokenName.result) throw new Error(`Failed to retrieve token name for contract: ${payload.to} - ${JSON.stringify(tokenName.error)}`); if (!tokenSymbol.result) throw new Error(`Failed to retrieve token symbol for contract: ${payload.to} - ${JSON.stringify(tokenSymbol.error)}`); return { decimals: decimals.result, tokenName: tokenName.result, tokenSymbol: tokenSymbol.result }; } async prepareFees(payload) { if (!payload.gas) { const fee = await this.evmRpc.estimateGas(payload); if (!fee.result) throw new Error(`Failed to estimate gas: ${JSON.stringify(fee.error)}`); payload.gas = `0x${fee.result?.toString(16)}`; } if (!payload.gasPrice) { const gasPrice = await this.evmRpc.gasPrice(); if (!gasPrice.result) throw new Error(`Failed to retrieve gas price: ${JSON.stringify(gasPrice.error)}`); payload.gasPrice = `0x${gasPrice.result.toString(16)}`; } payload.from = payload.from.toLowerCase(); payload.to = payload.to.toLowerCase(); } getStorageAddresses(trace, payload) { return [...Object.keys(trace.stateDiff[payload.tokenContractAddress.toLowerCase()].storage)]; } getTransferPayload(payload) { return { to: payload.to, from: payload.from, gas: payload.gas, gasPrice: payload.gasPrice, value: `0x${payload.value.toString(16)}`, }; } getTokenTransferPayload(payload, data) { return { to: payload.tokenContractAddress, from: payload.from, gas: payload.gas, gasPrice: payload.gasPrice, value: '0x0', data: data, }; } async getTraceCall(payload) { const jsonRpcResponse = await this.evmRpc.debugTraceCall(payload, 'latest', { tracer: this.minifiedTracer, tracerConfig: { onlyTopCall: false, timeout: '10s' }, }); if (!jsonRpcResponse.result) throw new Error(`Failed to trace call: ${JSON.stringify(jsonRpcResponse.error)}`); if (!Object.keys(jsonRpcResponse.result).length) throw new Error(`Failed to trace call - tracing returned empty result`); return jsonRpcResponse.result; } mapTraceToSimulationResultNative(payload, transferPayload, trace) { const balanceStateDiffFromAddress = trace.stateDiff[transferPayload.from].balance['*']; const balanceStateDiffToAddress = trace.stateDiff[transferPayload.to].balance['*']; return { transactionDetails: { from: payload.from, to: payload.to, value: payload.value, gasLimit: parseInt(transferPayload.gas, 16), gasPrice: parseInt(transferPayload.gasPrice, 16), }, status: 'success', balanceChanges: { [payload.from]: { from: new bignumber_js_1.default(balanceStateDiffFromAddress.from, 16), to: new bignumber_js_1.default(balanceStateDiffFromAddress.to, 16), }, [payload.to]: { from: new bignumber_js_1.default(balanceStateDiffToAddress.from, 16), to: new bignumber_js_1.default(balanceStateDiffToAddress.to, 16), }, }, }; } handleTraceError(trace) { const traceError = trace.trace[0].error; if (traceError) { throw new Error(`Transaction reverted with message: ${traceError}`); } } mapTraceToSimulationResultErc20(payload, tokenTransferPayload, trace, tokenDetails, matchedSlots) { const contractAddress = payload.tokenContractAddress.toLowerCase(); const fromAddress = payload.from.toLowerCase(); const storageStateDiffFromAddress = trace.stateDiff[contractAddress].storage[matchedSlots[payload.from]]['*']; const storageStateDiffToAddress = trace.stateDiff[contractAddress].storage[matchedSlots[payload.to]]['*']; const balanceStateDiffFromAddress = trace.stateDiff[fromAddress].balance['*']; return { transactionDetails: { from: payload.from, to: payload.to, tokenContractAddress: payload.tokenContractAddress, data: tokenTransferPayload.data, gasLimit: parseInt(tokenTransferPayload.gas, 16), gasPrice: parseInt(tokenTransferPayload.gasPrice, 16), }, status: 'success', balanceChanges: { [payload.from]: { from: new bignumber_js_1.default(balanceStateDiffFromAddress.from, 16), to: new bignumber_js_1.default(balanceStateDiffFromAddress.to, 16), }, }, tokenTransfers: { [payload.tokenContractAddress]: { name: tokenDetails.tokenName, symbol: tokenDetails.tokenSymbol, decimals: tokenDetails.decimals.toNumber(), [payload.from]: { from: new bignumber_js_1.default(storageStateDiffFromAddress.from, 16).dividedBy(new bignumber_js_1.default(10).pow(tokenDetails.decimals.toNumber())), to: new bignumber_js_1.default(storageStateDiffFromAddress.to, 16).dividedBy(new bignumber_js_1.default(10).pow(tokenDetails.decimals.toNumber())), }, [payload.to]: { from: new bignumber_js_1.default(storageStateDiffToAddress.from, 16).dividedBy(new bignumber_js_1.default(10).pow(tokenDetails.decimals.toNumber())), to: new bignumber_js_1.default(storageStateDiffToAddress.to, 16).dividedBy(new bignumber_js_1.default(10).pow(tokenDetails.decimals.toNumber())), }, }, }, }; } generateErc20TransferData(toAddress, amount, tokenDetails) { const amountWithDecimals = new bignumber_js_1.default(amount).multipliedBy(new bignumber_js_1.default(10).pow(tokenDetails.decimals.toNumber())); const encodedAddress = ethers_1.ethers.zeroPadValue((0, ethers_1.toBeHex)(toAddress), 32).slice(2); const encodedAmount = ethers_1.ethers.zeroPadValue((0, ethers_1.toBeHex)(amountWithDecimals.toString()), 32).slice(2); return `${consts_1.ERC20_TRANSFER_METHOD_SIGNATURE}${encodedAddress}${encodedAmount}`; } } exports.TransactionSimulator = TransactionSimulator; //# sourceMappingURL=extension.js.map