@tatumio/transaction-simulator
Version:
Transaction Simulation Extension
207 lines • 10.2 kB
JavaScript
;
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