import { BigNumber , Contract as EthersContract } from "ethers" ; import { defaultAbiCoder , TransactionDescription } from "@ethersproject/abi" ; import { Log } from "@ethersproject/abstract-provider" ; import { FunctionFragment , LogDescription } from "ethers/lib/utils" ; import pLimit from "p-limit" ; import VError from "verror" ; import { transactionHash } from "./utils/regEx" ; import EtherscanClient from "./clients/EtherscanClient" ; import EthereumNodeClient from "./clients/EthereumNodeClient" ; const debug = require ( "debug" ) ( "tx2uml" ) ; export enum MessageType { Unknown , Call , Create , Selfdestruct , DelegateCall , StaticCall } export type Param = { name : string ; type : string ; value ? : string ; components ? : Param [ ] ; } ; export type Trace = { id : number ; type : MessageType ; from : string ; delegatedFrom : string ; to : string ; value : BigNumber ; funcSelector ? : string ; funcName ? : string ; inputs ? : string ; inputParams ? : Param [ ] ; parsedConstructorParams ? : boolean ; outputs ? : string ; outputParams ? : Param [ ] ; proxy ? : boolean ; gasLimit : BigNumber ; gasUsed : BigNumber ; parentTrace ? : Trace ; childTraces : Trace [ ] ; depth : number ; error ? : string ; } ; export type Event = { name : string ; params : Param [ ] ; } ; export type Contract = { address : string ; contractName ? : string ; appName ? : string ; balance ? : number ; tokenName ? : string ; symbol ? : string ; decimals ? : number ; proxyImplementation ? : string ; ethersContract ? : EthersContract ; delegatedToContracts ? : Contract [ ] ; constructorInputs ? : string ; events ? : Event [ ] ; minDepth ? : number ; } ; export type TokenDetails = { address : string ; name ? : string ; symbol ? : string ; decimals ? : number ; } ; export type Contracts = { [ address ] : Contract ; } ; export type Token = { address : string ; name : string ; symbol : string ; decimals ? : number ; totalSupply ? : BigNumber ; } ; export type Transfer = { from : string ; to : string ; value : BigNumber ; ether : boolean ; tokenAddress ? : string ; tokenSymbol ? : string ; tokenName ? : string ; decimals ? : number ; } ; export interface TransactionDetails { hash : string ; from : string ; to : string ; data : string ; nonce : number ; index : number ; value : BigNumber ; gasPrice : BigNumber ; gasLimit : BigNumber ; gasUsed : BigNumber ; timestamp : Date ; status : boolean ; blockNumber : number ; logs : Array < Log > ; error ? : string ; } type ParamTypeInternal = { name : string ; type : string ; baseType : string ; components ? : ParamTypeInternal [ ] ; } ; export type Networks = "mainnet" | "ropsten" | "rinkeby" | "kovan" ; export class TransactionManager { constructor ( public readonly ethereumNodeClient , public readonly etherscanClient , public apiConcurrencyLimit = 2 ) { } async getTransactions ( txHashes ) { const transactions = [ ] ; for ( const txHash of txHashes ) { if ( ! txHash ?. match ( transactionHash ) ) { console . error ( `Array of transaction hashes must be in hexadecimal format with a 0x prefix` ) ; process . exit ( 1 ) ; } transactions . push ( await this . getTransaction ( txHash ) ) ; } return transactions ; } async getTransaction ( txHash ) { return this . ethereumNodeClient . getTransactionDetails ( txHash ) ; } async getTraces ( transactions ) { const transactionsTraces = [ ] ; for ( const transaction of transactions ) { transactionsTraces . push ( await this . ethereumNodeClient . getTransactionTrace ( transaction . hash ) ) ; } return transactionsTraces ; } async getContracts ( transactionsTraces ) { const flatTraces = transactionsTraces . flat ( ) ; const participantAddresses = [ ] ; const delegatesToContracts = { } ; for ( const trace of Object . values ( flatTraces ) ) { participantAddresses . push ( trace . from ) ; participantAddresses . push ( trace . to ) ; if ( trace . type === MessageType . DelegateCall ) { if ( ! delegatesToContracts [ trace . from ] ) { delegatesToContracts [ trace . from ] = [ trace . to ] ; } else if ( ! delegatesToContracts [ trace . from ] . includes ( trace . to ) ) { delegatesToContracts [ trace . from ] . push ( trace . to ) ; } } } const uniqueAddresses = Array . from ( new Set ( participantAddresses ) ) ; debug ( `${ uniqueAddresses . length } contracts in the transactions` ) ; const contracts = await this . getContractsFromAddresses ( uniqueAddresses ) ; for ( const [ address , toAddresses ] of Object . entries ( delegatesToContracts ) ) { contracts [ address ] . delegatedToContracts = toAddresses . map ( toAddress => ( { ... contracts [ toAddress ] , address , } ) ) ; } return await this . setTokenAttributes ( contracts ) ; } async getContractsFromAddresses ( addresses ) { const contracts = { } ; const limit = pLimit ( this . apiConcurrencyLimit ) ; const getContractPromises = addresses . map ( address => { return limit ( ( ) => this . etherscanClient . getContract ( address ) ) ; } ) ; const results = await Promise . all ( getContractPromises ) ; results . forEach ( result => { contracts [ result . address ] = result ; } ) ; return contracts ; } async setTokenAttributes ( contracts ) { const contractAddresses = Object . keys ( contracts ) ; const tokensDetails = await this . ethereumNodeClient . getTokenDetails ( contractAddresses ) ; tokensDetails . forEach ( tokenDetails => { contracts [ tokenDetails . address ] . tokenName = tokenDetails . name ; contracts [ tokenDetails . address ] . symbol = tokenDetails . symbol ; } ) ; return contracts ; } static parseTraceParams ( traces , contracts ) { const functionSelector2Contract = mapFunctionSelectors2Contracts ( contracts ) ; for ( const trace of traces . flat ( ) ) { if ( trace . inputs ?. length >= 10 ) { if ( trace . type === MessageType . Create ) { trace . funcName = "constructor" ; addConstructorParamsToTrace ( trace , contracts ) ; return ; } const selectedContracts = functionSelector2Contract [ trace . funcSelector ] ; if ( selectedContracts ?. length > 0 ) { let contract = selectedContracts . find ( contract => contract . address === trace . to ) ; if ( ! contract ) { contract = selectedContracts [ 0 ] ; trace . proxy = true ; } try { const txDescription = contract . interface . parseTransaction ( { data : trace . inputs , } ) ; trace . funcName = txDescription . name ; addInputParamsToTrace ( trace , txDescription ) ; addOutputParamsToTrace ( trace , txDescription ) ; } catch ( err ) { if ( ! err . message . match ( "no matching function" ) ) { const error = new VError ( err , `Failed to parse selector ${ trace . funcSelector } in trace with id ${ trace . id } from ${ trace . from } to ${ trace . to }` ) ; console . warn ( error ) ; } } } } } } static parseTransactionLogs ( logs , contracts ) { for ( const log of logs ) { const contract = contracts [ log . address . toLowerCase ( ) ] ; if ( contract ?. ethersContract ) { try { const event = contract . ethersContract . interface . parseLog ( log ) ; contract . events . push ( parseEvent ( contract , event ) ) ; } catch ( err ) { debug ( `Failed to parse log with topic ${ log ?. topics [ 0 ] } on contract ${ log . address }` ) ; } } contract ?. delegatedToContracts ?. forEach ( delegatedToContract => { try { const event = delegatedToContract . ethersContract . interface . parseLog ( log ) ; contract . events . push ( parseEvent ( contract , event ) ) ; } catch ( err ) { debug ( `Failed to parse log with topic ${ log ?. topics [ 0 ] } on contract ${ log . address }` ) ; } } ) ; } } static parseTraceDepths ( traces , contracts ) { const flatTraces = traces . flat ( ) ; contracts [ flatTraces [ 0 ] . from ] . minDepth = 0 ; for ( const trace of flatTraces ) { if ( contracts [ trace . to ] . minDepth == undefined || trace . depth < contracts [ trace . to ] . minDepth ) { contracts [ trace . to ] . minDepth = trace . depth ; } } } static filterTransactionTraces ( transactionTraces , contracts , options ) { const filteredTransactionTraces = transactionTraces . map ( t => [ ] ) ; let usedAddresses = new Set < string > ( ) ; transactionTraces . forEach ( ( tx , i ) => { const filteredExcludedTraces = filterExcludedContracts ( tx [ 0 ] , options . excludedContracts ) ; const filteredTraces = options . noDelegates ? filterOutDelegatedTraces ( filteredExcludedTraces ) : [ filteredExcludedTraces ] ; filteredTransactionTraces [ i ] = arrayifyTraces ( filteredTraces [ 0 ] ) ; usedAddresses . add ( filteredTransactionTraces [ i ] [ 0 ] . from ) ; filteredTransactionTraces [ i ] . forEach ( t => usedAddresses . add ( t . to ) ) ; } ) ; const usedContracts = { } ; Array . from ( usedAddresses ) . forEach ( address => ( usedContracts [ address ] = contracts [ address ] ) ) ; return [ filteredTransactionTraces , usedContracts ] ; } } const filterOutDelegatedTraces = ( trace , lastValidParentTrace ? ) : Trace [ ] => { const removeTrace = trace . type === MessageType . DelegateCall ; const parentTrace = removeTrace ? lastValidParentTrace : { ... trace . parentTrace , type : MessageType . Call } ; let filteredChildren = [ ] ; trace . childTraces . forEach ( child => { filteredChildren = filteredChildren . concat ( filterOutDelegatedTraces ( child , parentTrace ) ) ; } ) ; if ( removeTrace ) { return filteredChildren ; } return [ { ... trace , proxy : false , childTraces : filteredChildren , parentTrace : lastValidParentTrace , depth : ( lastValidParentTrace ?. depth || 0 ) + 1 , delegatedFrom : trace . from , type : removeTrace ? MessageType . Call : trace . type , } , ] ; } ; const filterExcludedContracts = ( trace , excludedContracts = [ ] ) : Trace => { let filteredChildren = [ ] ; trace . childTraces . forEach ( child => { if ( excludedContracts . includes ( child . to ) ) return ; filteredChildren = filteredChildren . concat ( filterExcludedContracts ( child , excludedContracts ) ) ; } ) ; return { ... trace , childTraces : filteredChildren , } ; } ; const arrayifyTraces = ( trace ) : Trace [ ] => { let traces = [ trace ] ; trace . childTraces . forEach ( child => { const arrayifiedChildren = arrayifyTraces ( child ) ; traces = traces . concat ( arrayifiedChildren ) ; } ) ; return traces ; } ; const mapFunctionSelectors2Contracts = ( contracts ) : { [ functionSelector ] : EthersContract [ ] ; } => { const functionSelector2Contract = { } ; Object . values ( contracts ) . forEach ( contract => { if ( contract . ethersContract ) { Object . values ( contract . ethersContract . interface . fragments ) . filter ( fragment => fragment . type === "function" ) . forEach ( ( fragment ) => { const sighash = contract . ethersContract . interface . getSighash ( fragment ) ; if ( ! functionSelector2Contract [ sighash ] ) { functionSelector2Contract [ sighash ] = [ ] ; } functionSelector2Contract [ sighash ] . push ( contract . ethersContract ) ; } ) ; } } ) ; return functionSelector2Contract ; } ; const addInputParamsToTrace = ( trace , txDescription ) => { txDescription . args . forEach ( ( arg , i ) => { const functionFragment = txDescription . functionFragment . inputs [ i ] ; const components = addValuesToComponents ( functionFragment , arg ) ; trace . inputParams . push ( { name : functionFragment . name , type : functionFragment . type , value : arg , components , } ) ; } ) ; } ; const addOutputParamsToTrace = ( trace , txDescription ) : void => { if ( ! trace . outputs || trace . outputs === "0x" || trace . error ) return ; const functionFragments = txDescription . functionFragment . outputs ; const outputParams = defaultAbiCoder . decode ( functionFragments , trace . outputs ) ; outputParams . forEach ( ( param , i ) => { const components = addValuesToComponents ( functionFragments [ i ] , param ) ; trace . outputParams . push ( { name : functionFragments [ i ] . name , type : functionFragments [ i ] . type , value : param , components , } ) ; } ) ; debug ( `Decoded ${ trace . outputParams . length } output params for ${ trace . funcName } with selector ${ trace . funcSelector }` ) ; } ; const addConstructorParamsToTrace = ( trace , contracts ) => { const constructor = contracts [ trace . to ] ?. ethersContract ?. interface ?. deploy ; if ( ! constructor ?. inputs ) { return ; } trace . parsedConstructorParams = true ; if ( ! contracts [ trace . to ] ?. constructorInputs ?. length ) { return ; } const constructorParams = defaultAbiCoder . decode ( constructor . inputs , "0x" + contracts [ trace . to ] ?. constructorInputs ) ; constructorParams . forEach ( ( param , i ) => { const components = addValuesToComponents ( constructor . inputs [ i ] , param ) ; trace . inputParams . push ( { name : constructor . inputs [ i ] . name , type : constructor . inputs [ i ] . type , value : param , components , } ) ; } ) ; debug ( `Decoded ${ trace . inputParams . length } constructor params.` ) ; } ; const parseEvent = ( contract , log ) : Event => { const params = [ ] ; log . eventFragment . inputs . forEach ( ( param , i ) => { const components = addValuesToComponents ( log . eventFragment . inputs [ i ] , param ) ; params . push ( { name : log . eventFragment . inputs [ i ] . name , type : log . eventFragment . inputs [ i ] . type , value : log . args [ i ] , components , } ) ; } ) ; return { name : log . name , params , } ; } ; const addValuesToComponents = ( paramType , args ) : Param [ ] => { if ( paramType . baseType !== "array" ) { if ( ! paramType ?. components ) { return undefined ; } return paramType . components . map ( ( component , j ) => { return { name : component . name , type : component . type , value : args [ j ] , components : addValuesToComponents ( component , args [ j ] ) , } ; } ) ; } else { return args . map ( ( row , r ) => { const childType = paramType . type . slice ( 0 , - 2 ) ; const childBaseType = childType . slice ( - 2 ) === "[]" ? "array" : childType ; const components = addValuesToComponents ( { ... paramType , type : childType , baseType : childBaseType } , row ) ; return { name : r . toString ( ) , type : childType , value : row , components , } ; } ) ; } } ;