import axios from "axios" ; import { BigNumber } from "ethers" ; import { VError } from "verror" ; import { MessageType , Trace , TransactionDetails } from "../transaction" ; import { transactionHash } from "../utils/regEx" ; import { hexlify , toUtf8String } from "ethers/lib/utils" ; import EthereumNodeClient from "./EthereumNodeClient" ; require ( "axios-debug-log" ) ; const debug = require ( "debug" ) ( "tx2uml" ) ; export type CallResponse = { type : "CALL" | "CALLCODE" | "CREATE" | "CREATE2" | "DELEGATECALL" | "SELFDESTRUCT" | "STATICCALL" ; from ? : string ; to ? : string ; input ? : string ; output ? : string ; value ? : string ; gas ? : string ; gasUsed ? : string ; time ? : string ; error ? : string ; calls ? : CallResponse [ ] ; } ; export default class GethClient extends EthereumNodeClient { private jsonRpcId = 0 ; constructor ( public readonly url = "http://localhost:8545" , public readonly chain = "mainnet" ) { super ( url , chain ) ; } async getTransactionTrace ( txHash ) { if ( ! txHash ?. match ( 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 . 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 }` ) ; } 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 ) { throw new VError ( err , `Failed to get transaction trace for tx hash ${ txHash } from url ${ this . url }.` ) ; } } async getTransactionError ( tx ) { if ( ! tx ?. hash . match ( 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 , } , hexlify ( tx . blockNumber - 1 ) . replace ( /^0x0/ , "0x" ) , ] ; const response = await axios . 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: " ) ) { const errorObjectStr = e . message . slice ( 12 ) ; const errorObject = JSON . parse ( errorObjectStr ) ; if ( ! errorObject . data ) { throw Error ( "Failed to parse data field error object:" + errorObjectStr ) ; } if ( errorObject . data . startsWith ( "Reverted 0x" ) ) { rawMessageData = errorObject . data . slice ( 11 ) ; } else if ( errorObject . data . startsWith ( "0x" ) ) { 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 parseReasonCode ( rawMessageData ) ; } } const addTraces = ( callResponse , traces , id , depth , parentTrace ? ) : number => { const type = convertType ( callResponse ) ; const delegatedFrom = parentTrace ?. type === MessageType . DelegateCall ? parentTrace . to : callResponse . from ; const newTrace = { id : id ++ , type , from : callResponse . from , delegatedFrom , to : callResponse . to , value : callResponse . value ? convertBigNumber ( callResponse . value ) : BigNumber . from ( 0 ) , inputs : callResponse . input , inputParams : [ ] , funcSelector : callResponse . input ?. length >= 10 ? callResponse . input . slice ( 0 , 10 ) : undefined , outputs : callResponse . output , outputParams : [ ] , 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 => { id = addTraces ( childCall , traces , id , depth + 1 , newTrace ) ; } ) ; } return id ; } ; const convertType = ( trace ) : MessageType => { let type = MessageType . Call ; if ( trace . type === "DELEGATECALL" ) { return MessageType . DelegateCall ; } if ( trace . type === "STATICCALL" ) { return MessageType . StaticCall ; } if ( trace . type === "CREATE" || trace . type === "CREATE2" ) { return MessageType . Create ; } else if ( trace . type === "SELFDESTRUCT" ) { return MessageType . Selfdestruct ; } return type ; } ; const convertBigNumber = ( value ) : BigNumber | undefined => { if ( ! value ) return undefined ; return BigNumber . from ( value ) ; } ; const convertBigNumber2Hex = ( value ) => { return value . toHexString ( ) . replace ( /^0x0/ , "0x" ) ; } ; export const parseReasonCode = ( messageData ) : string => { const strLen = parseInt ( messageData . slice ( 8 + 64 , 8 + 128 ) , 16 ) ; const reasonCodeHex = messageData . slice ( 8 + 128 , 8 + 128 + strLen * 2 ) ; const reason = toUtf8String ( "0x" + reasonCodeHex ) ; return reason ; } ;