tx2uml
Version:
Ethereum transaction visualizer that generates UML sequence diagrams.
238 lines • 10.7 kB
JavaScript
;
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.parseReasonCode = void 0;
const axios_1 = __importDefault(require("axios"));
const ethers_1 = require("ethers");
const tx2umlTypes_1 = require("../types/tx2umlTypes");
const regEx_1 = require("../utils/regEx");
const utils_1 = require("ethers/lib/utils");
const EthereumNodeClient_1 = __importDefault(require("./EthereumNodeClient"));
require("axios-debug-log");
const debug = require("debug")("tx2uml");
class GethClient extends EthereumNodeClient_1.default {
constructor(url, network) {
super(url, network);
this.url = url;
this.jsonRpcId = 0;
}
async getTransactionTrace(txHash) {
if (!txHash?.match(regEx_1.transactionHash)) {
throw new TypeError(`Transaction hash "${txHash}" must be 32 bytes in hexadecimal format with a 0x prefix`);
}
try {
debug(`About to get transaction trace for ${txHash}`);
const response = await axios_1.default.post(this.url, {
id: this.jsonRpcId++,
jsonrpc: "2.0",
method: "debug_traceTransaction",
params: [txHash, { tracer: "callTracer" }],
});
if (response.data?.error?.message) {
throw new Error(response.data.error.message);
}
if (!response?.data?.result?.from) {
if (response?.data?.result?.structLogs) {
throw new Error(`Have you set the --nodeType option correctly? It looks like a debug_traceTransaction was run against a node that doesn't support tracing in their debugging API.`);
}
throw new Error(`no transaction trace messages in response. ${response?.data?.result}`);
}
// recursively add the traces
const traces = [];
addTraces(response.data.result, traces, 0, 0);
debug(`Got ${traces.length} traces actions for tx hash ${txHash} from ${this.url}`);
return traces;
}
catch (err) {
const error = new Error(`Failed to get transaction trace for tx hash ${txHash} from url ${this.url}.`, { cause: err });
throw error;
}
}
async getValueTransfers(txHash) {
if (!txHash?.match(regEx_1.transactionHash)) {
throw new TypeError(`Transaction hash "${txHash}" must be 32 bytes in hexadecimal format with a 0x prefix`);
}
try {
debug(`About to debug transaction ${txHash}`);
const response = await axios_1.default.post(this.url, {
id: this.jsonRpcId++,
jsonrpc: "2.0",
method: "debug_traceTransaction",
params: [
txHash,
{
// TODO need to handle LOG1 and LOG2 Deposit and Withdraw
tracer: '{data: [], fault: function(log) {}, step: function(log) { if (log.op.toString().match(/LOG/)) { const topic1 = log.stack.peek(2).toString(16); const tokenAddress = toHex(log.contract.getAddress()); if (topic1 === "ddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef") { this.data.push({ event: "Transfer", pc: log.getPC(), tokenAddress}) } else if (topic1 === "e1fffcc4923d04b559f4d29a8bfc6cda04eb5b0d3c460751c2402c5c5cc9109c" ) { this.data.push({ event: "Deposit", pc: log.getPC(), tokenAddress}) } else if (topic1 === "7fcf532c15f0a6db0bd6d0e038bea71d30d808c7d98cb3bf7268a95bf5081b65" ) { this.data.push({ event: "Withdraw", pc: log.getPC(), tokenAddress}) } } else if(log.op.toString() == "CALL" && log.stack.length() >= 3 && log.stack.peek(2) > 0) this.data.push({ from: toHex(log.contract.getAddress()), to: toHex(toAddress(log.stack.peek(1).toString(16))), value: log.stack.peek(2), pc: log.getPC() }) }, result: function() { return this.data; }}',
},
],
});
if (response.data?.error?.message) {
throw new Error(response.data.error.message);
}
if (!Array.isArray(response?.data?.result)) {
throw new Error(`No value transfers in response. ${response?.data}`);
}
debug(`Got ${response.data.result.length} value transfers for tx hash ${txHash}`);
// Format contract address with checksum formatting
const addressEncodedTransfers = response?.data?.result.map((t) => ({
...t,
from: t.from ? (0, utils_1.getAddress)(t.from) : undefined,
to: t.to ? (0, utils_1.getAddress)(t.to) : undefined,
tokenAddress: t.tokenAddress
? (0, utils_1.getAddress)(t.tokenAddress)
: undefined,
event: t.event,
type: tx2umlTypes_1.TransferType.Transfer,
}));
return addressEncodedTransfers;
}
catch (err) {
const error = new Error(`Failed to get value transfers for tx hash ${txHash} from url ${this.url}.`, { cause: err });
throw error;
}
}
async getTransactionError(tx) {
if (!tx?.hash.match(regEx_1.transactionHash)) {
throw TypeError(`There is no transaction hash on the receipt object`);
}
if (tx.status) {
return undefined;
}
if (tx.gasUsed === tx.gasLimit) {
throw Error("Transaction failed as it ran out of gas.");
}
let rawMessageData;
try {
const params = [
{
nonce: tx.nonce,
gasPrice: convertBigNumber2Hex(tx.gasPrice),
gas: convertBigNumber2Hex(tx.gasLimit),
value: convertBigNumber2Hex(tx.value),
from: tx.from,
to: tx.to,
data: tx.data,
},
// need to call for the block before
(0, utils_1.hexlify)(tx.blockNumber - 1).replace(/^0x0/, "0x"),
];
const response = await axios_1.default.post(this.url, {
id: this.jsonRpcId++,
jsonrpc: "2.0",
method: "eth_call",
params,
});
return response.data?.error?.message;
}
catch (e) {
if (e.message.startsWith("Node error: ")) {
// Trim "Node error: "
const errorObjectStr = e.message.slice(12);
// Parse the error object
const errorObject = JSON.parse(errorObjectStr);
if (!errorObject.data) {
throw Error("Failed to parse data field error object:" +
errorObjectStr);
}
if (errorObject.data.startsWith("Reverted 0x")) {
// Trim "Reverted 0x" from the data field
rawMessageData = errorObject.data.slice(11);
}
else if (errorObject.data.startsWith("0x")) {
// Trim "0x" from the data field
rawMessageData = errorObject.data.slice(2);
}
else {
throw Error("Failed to parse data field of error object:" +
errorObjectStr);
}
}
else {
throw Error("Failed to parse error message from Ethereum call: " +
e.message);
}
}
return (0, exports.parseReasonCode)(rawMessageData);
}
}
exports.default = GethClient;
// Adds calls from a Geth debug_traceTransaction API response to the traces
const addTraces = (callResponse, traces, id, depth, parentTrace) => {
const type = convertType(callResponse);
const delegatedFrom = parentTrace?.type === tx2umlTypes_1.MessageType.DelegateCall
? parentTrace.to
: callResponse.from;
const newTrace = {
id: id++,
type,
from: callResponse.from,
delegatedFrom,
to: callResponse.to,
value: callResponse.value
? convertBigNumber(callResponse.value)
: ethers_1.BigNumber.from(0),
// remove trailing 64 zeros
inputs: callResponse.input,
inputParams: [], // Will init later once we have the contract ABI
funcSelector: callResponse.input?.length >= 10
? callResponse.input.slice(0, 10)
: undefined,
outputs: callResponse.output,
outputParams: [], // Will init later once we have the contract ABI
gasLimit: convertBigNumber(callResponse.gas),
gasUsed: convertBigNumber(callResponse.gasUsed),
parentTrace,
childTraces: [],
depth,
error: callResponse.error,
};
if (parentTrace) {
parentTrace.childTraces.push(newTrace);
}
traces.push(newTrace);
if (callResponse.calls) {
callResponse.calls.forEach(childCall => {
// recursively add traces
id = addTraces(childCall, traces, id, depth + 1, newTrace);
});
}
return id;
};
const convertType = (trace) => {
let type = tx2umlTypes_1.MessageType.Call;
if (trace.type === "DELEGATECALL") {
return tx2umlTypes_1.MessageType.DelegateCall;
}
if (trace.type === "STATICCALL") {
return tx2umlTypes_1.MessageType.StaticCall;
}
if (trace.type === "CREATE" || trace.type === "CREATE2") {
return tx2umlTypes_1.MessageType.Create;
}
else if (trace.type === "SELFDESTRUCT") {
return tx2umlTypes_1.MessageType.Selfdestruct;
}
return type;
};
// convert an integer value to a decimal value. eg wei to Ethers which is to 18 decimal places
const convertBigNumber = (value) => {
if (!value)
return undefined;
return ethers_1.BigNumber.from(value);
};
const convertBigNumber2Hex = (value) => {
return value.toHexString().replace(/^0x0/, "0x");
};
const parseReasonCode = (messageData) => {
// Get the length of the revert reason
const strLen = parseInt(messageData.slice(8 + 64, 8 + 128), 16);
// Using the length and known offset, extract and convert the revert reason
const reasonCodeHex = messageData.slice(8 + 128, 8 + 128 + strLen * 2);
// Convert reason from hex to string
const reason = (0, utils_1.toUtf8String)("0x" + reasonCodeHex);
return reason;
};
exports.parseReasonCode = parseReasonCode;
//# sourceMappingURL=GethClient.js.map