import { Readable } from "stream" ; import { formatEther , formatUnits } from "ethers/lib/utils" ; import { BigNumber } from "ethers" ; import { Contracts , MessageType , Param , Trace , TransactionDetails , } from "./transaction" ; import { formatNumber , participantId , shortAddress , shortBytes , } from "./utils/formatters" ; const debug = require ( "debug" ) ( "tx2uml" ) ; export interface PumlGenerationOptions { noGas ? : boolean ; noParams ? : boolean ; noEther ? : boolean ; noTxDetails ? : boolean ; noLogDetails ? : boolean ; noDelegates ? : boolean ; network ? : string ; depth ? : number ; } const DelegateLifelineColor = "#809ECB" ; const DelegateMessageColor = "#3471CD" ; const FailureFillColor = "#FFAAAA" ; export const streamTxPlantUml = ( transactions , traces , contracts , options = { } ) : Readable => { const pumlStream = new Readable ( { read ( ) { } , } ) ; if ( transactions . length > 1 ) { streamMultiTxsPuml ( pumlStream , transactions , traces , contracts , options ) ; } else { streamSingleTxPuml ( pumlStream , transactions [ 0 ] , traces [ 0 ] , contracts , options ) ; } return pumlStream ; } ; export const streamMultiTxsPuml = ( pumlStream , transactions , traces , contracts , options = { } ) => { pumlStream . push ( `@startuml\n` ) ; writeParticipants ( pumlStream , contracts , options ) ; let i = 0 ; for ( const transaction of transactions ) { pumlStream . push ( `\ngroup ${ transaction . hash }` ) ; writeTransactionDetails ( pumlStream , transaction , options ) ; writeMessages ( pumlStream , traces [ i ++ ] , options ) ; writeEvents ( pumlStream , contracts , options ) ; pumlStream . push ( "end" ) ; } pumlStream . push ( "\n@endumls" ) ; pumlStream . push ( null ) ; return pumlStream ; } ; export const streamSingleTxPuml = ( pumlStream , transaction , traces , contracts , options = { } ) : Readable => { pumlStream . push ( `@startuml\ntitle ${ transaction . hash }\n` ) ; pumlStream . push ( genCaption ( transaction , options ) ) ; writeParticipants ( pumlStream , contracts , options ) ; writeTransactionDetails ( pumlStream , transaction , options ) ; writeMessages ( pumlStream , traces , options ) ; writeEvents ( pumlStream , contracts , options ) ; pumlStream . push ( "\n@endumls" ) ; pumlStream . push ( null ) ; return pumlStream ; } ; export const writeParticipants = ( plantUmlStream , contracts , options = { } ) => { plantUmlStream . push ( "\n" ) ; for ( const [ address , contract ] of Object . entries ( contracts ) ) { if ( options . depth > 0 && contract . minDepth > options . depth ) continue ; let name = "" ; if ( contract . tokenName ) { if ( contract . symbol ) { name = `<<${ contract . tokenName } (${ contract . symbol })>>` ; } else { name = `<<${ contract . tokenName }>>` ; } } if ( contract . contractName ) { name += `<<${ contract . contractName }>>` ; } debug ( `Write lifeline ${ shortAddress ( address ) } with stereotype ${ name }` ) ; plantUmlStream . push ( `participant "${ shortAddress ( address ) }" as ${ participantId ( address ) } ${ name }\n` ) ; } } ; const writeTransactionDetails = ( plantUmlStream , transaction , options = { } ) : void => { if ( options . noTxDetails ) { return ; } plantUmlStream . push ( `\nnote over ${ participantId ( transaction . from ) }` ) ; if ( transaction . error ) { plantUmlStream . push ( ` ${ FailureFillColor }\nError: ${ transaction . error } \n` ) ; } else { plantUmlStream . push ( "\n" ) ; } plantUmlStream . push ( `Nonce: ${ transaction . nonce . toLocaleString ( ) }\n` ) ; plantUmlStream . push ( `Gas Price: ${ formatUnits ( transaction . gasPrice , "gwei" ) } Gwei\n` ) ; plantUmlStream . push ( `Gas Limit: ${ formatNumber ( transaction . gasLimit . toString ( ) ) }\n` ) ; plantUmlStream . push ( `Gas Used: ${ formatNumber ( transaction . gasUsed . toString ( ) ) }\n` ) ; const txFeeInWei = transaction . gasUsed . mul ( transaction . gasPrice ) ; const txFeeInEther = formatEther ( txFeeInWei ) ; const tFeeInEtherFormatted = Number ( txFeeInEther ) . toLocaleString ( ) ; plantUmlStream . push ( `Tx Fee: ${ tFeeInEtherFormatted } ETH\n` ) ; plantUmlStream . push ( "end note\n" ) ; } ; export const writeMessages = ( plantUmlStream , traces , options = { } ) => { if ( ! traces ?. length ) { return ; } let contractCallStack = [ ] ; let previousTrace ; plantUmlStream . push ( "\n" ) ; for ( const trace of traces ) { if ( trace . depth > options . depth ) continue ; debug ( `Write message ${ trace . id } from ${ shortAddress ( trace . from ) } to ${ shortAddress ( trace . to ) }` ) ; if ( trace . delegatedFrom !== previousTrace ?. to ) { for ( const callStack of [ ... contractCallStack ] ) { if ( trace . delegatedFrom === callStack . to ) { break ; } plantUmlStream . push ( genEndLifeline ( callStack , options ) ) ; contractCallStack . shift ( ) ; } } if ( trace . type === MessageType . Selfdestruct ) { plantUmlStream . push ( `${ participantId ( trace . from ) } ${ genArrow ( trace ) } ${ participantId ( trace . from ) }: Self-Destruct\n` ) ; } else { plantUmlStream . push ( `${ participantId ( trace . from ) } ${ genArrow ( trace ) } ${ participantId ( trace . to ) }: ${ genFunctionText ( trace , options . noParams ) }${ genGasUsage ( trace . gasUsed , options . noGas ) }${ genEtherValue ( trace , options . noEther ) }\n` ) ; if ( trace . type === MessageType . DelegateCall ) { plantUmlStream . push ( `activate ${ participantId ( trace . to ) } ${ DelegateLifelineColor }\n` ) ; } else { plantUmlStream . push ( `activate ${ participantId ( trace . to ) }\n` ) ; } } if ( trace . type !== MessageType . Selfdestruct ) { contractCallStack . unshift ( trace ) ; previousTrace = trace ; } } contractCallStack . forEach ( callStack => { plantUmlStream . push ( genEndLifeline ( callStack , options ) ) ; } ) ; } ; const genEndLifeline = ( trace , options = { } ) : string => { let plantUml = "" ; if ( ! trace . error ) { if ( options . noParams ) { plantUml += `return\n` ; } else { plantUml += `return${ genParams ( trace . outputParams ) }\n` ; } if ( ! options . noGas && trace . childTraces . length > 0 ) { const gasUsedLessChildCalls = calculateGasUsedLessChildTraces ( trace ) ; if ( gasUsedLessChildCalls ?. gt ( 0 ) ) { plantUml += `note right of ${ participantId ( trace . to ) }: ${ genGasUsage ( gasUsedLessChildCalls ) }\n` ; } } } else { plantUml += `destroy ${ participantId ( trace . to ) }\nreturn\n` ; plantUml += `note right of ${ participantId ( trace . to ) } ${ FailureFillColor }: ${ trace . error }\n` ; } return plantUml ; } ; const calculateGasUsedLessChildTraces = ( trace ) : BigNumber | undefined => { let gasUsedLessChildTraces = BigNumber . from ( 0 ) ; for ( const childTrace of trace . childTraces ) { if ( ! childTrace . gasUsed ) { return undefined ; } gasUsedLessChildTraces = gasUsedLessChildTraces . add ( childTrace . gasUsed ) ; } return trace . gasUsed . sub ( gasUsedLessChildTraces ) ; } ; const genArrow = ( trace ) : string => { const arrowColor = trace . parentTrace ?. type === MessageType . DelegateCall ? `[${ DelegateMessageColor }]` : "" ; const line = trace . proxy ? "--" : "-" ; if ( trace . type === MessageType . DelegateCall ) { return `${ line }${ arrowColor }>>` ; } if ( trace . type === MessageType . Create ) { return `${ line }${ arrowColor }>o` ; } if ( trace . type === MessageType . Selfdestruct ) { return `${ line }${ arrowColor }\\` ; } return `${ line }${ arrowColor }>` ; } ; const genFunctionText = ( trace , noParams = false ) : string => { if ( ! trace ) { return "" ; } if ( trace . type === MessageType . Create ) { if ( noParams ) { return "constructor" ; } if ( trace . parsedConstructorParams ) { return `${ trace . funcName }(${ genParams ( trace . inputParams ) })` ; } return "constructor(?)" ; } if ( ! trace . funcSelector ) { return noParams ? "fallback" : "fallback()" ; } if ( ! trace . funcName ) { return `${ trace . funcSelector }` ; } return noParams ? trace . funcName : `${ trace . funcName }(${ genParams ( trace . inputParams ) })` ; } ; const oneIndent = "  " ; export const genParams = ( params , plantUml = "" , indent = "" ) : string => { if ( ! params ) { return "" ; } for ( const param of params ) { plantUml += "\\n" + indent ; if ( param . name ) { plantUml += `${ param . name }: ` ; } if ( param . type === "address" ) { plantUml += `${ shortAddress ( param . value ) },` ; } else if ( param . components ) { if ( Array . isArray ( param . components ) ) { plantUml += `[` ; plantUml = `${ genParams ( param . components , plantUml , indent + oneIndent ) }` ; plantUml += `],` ; } else { debug ( `Unsupported components type ${ JSON . stringify ( param . components ) }` ) ; } } else if ( Array . isArray ( param . value ) ) { plantUml += `[` ; param . value . forEach ( ( value , i ) => { plantUml = `${ genParams ( [ { name : i . toString ( ) , value , type : param . type . slice ( 0 , - 2 ) , } , ] , plantUml , indent + oneIndent ) }` ; } ) ; plantUml += `],` ; } else if ( param . type . slice ( 0 , 5 ) === "bytes" ) { plantUml += `${ shortBytes ( param . value ) },` ; } else if ( param . type . match ( "int" ) ) { plantUml += `${ formatNumber ( param . value ) },` ; } else { plantUml += `${ param . value },` ; } } return plantUml . slice ( 0 , - 1 ) ; } ; const genGasUsage = ( gasUsed ? , noGasUsage = false ) : string => { if ( noGasUsage || ! gasUsed ) { return "" ; } const gasValueWithCommas = formatNumber ( gasUsed . toString ( ) ) ; return `\\n${ gasValueWithCommas } gas` ; } ; const genEtherValue = ( trace , noEtherValue = false ) : string => { if ( noEtherValue || trace . value . eq ( 0 ) ) { return "" ; } const ether = formatEther ( trace . value ) ; const etherFormatted = Number ( ether ) . toLocaleString ( ) ; return `\\n${ etherFormatted } ETH` ; } ; const genCaption = ( details , options ) : string => { return `caption ${ options . network || "" } ${ details . timestamp . toUTCString ( ) }` ; } ; export const writeEvents = ( plantUmlStream , contracts , options = { } ) => { if ( options . noLogDetails ) { return ; } for ( const contract of Object . values ( contracts ) ) { if ( contract . ethersContract && contract . events . length && ( options . depth === undefined || contract . minDepth <= options . depth ) ) { plantUmlStream . push ( `\nnote over ${ participantId ( contract . address ) } #aqua` ) ; for ( const event of contract . events ) { plantUmlStream . push ( `\n${ event . name }:` ) ; plantUmlStream . push ( `${ genParams ( event . params ) . replace ( /\\n/g , "\n" ) }` ) ; } plantUmlStream . push ( "\nend note" ) ; } } } ;