import { keccak_256 } from 'js-sha3' ; import { groupBy , omit } from 'lodash' ; import { Dictionary } from 'ts-essentials' ; import { debug } from '../utils/debug' ; import { MalformedAbiError } from '../utils/errors' ; import { normalizeName } from './normalizeName' ; import { EvmOutputType , EvmType , parseEvmType , StructType } from './parseEvmType' ; export interface AbiParameter { name : string ; type : EvmType ; } export interface AbiOutputParameter { name : string ; type : EvmOutputType ; } export type Named < T > = { name : string ; values : T ; } ; export type StateMutability = 'pure' | 'view' | 'nonpayable' | 'payable' ; export interface FunctionDocumentation { author ? : string ; details ? : string ; notice ? : string ; params ? : { [ paramName ] : string ; } ; return ? : string ; } export interface FunctionDeclaration { name : string ; stateMutability : StateMutability ; inputs : AbiParameter [ ] ; outputs : AbiOutputParameter [ ] ; documentation ? : FunctionDocumentation | undefined ; } export interface FunctionWithoutOutputDeclaration extends FunctionDeclaration { outputs : [ ] ; } export interface FunctionWithoutInputDeclaration extends FunctionDeclaration { inputs : [ ] ; } export interface Contract { name : string ; rawName : string ; fallback ? : FunctionWithoutInputDeclaration | undefined ; constructor : FunctionWithoutOutputDeclaration [ ] ; functions : Dictionary < FunctionDeclaration [ ] > ; events : Dictionary < EventDeclaration [ ] > ; structs : Dictionary < StructType [ ] > ; documentation ? : { author ? : string ; details ? : string ; notice ? : string ; } | undefined ; } export interface RawAbiParameter { name : string ; type : string ; internalType ? : string ; components ? : RawAbiParameter [ ] ; } export interface RawAbiDefinition { name : string ; constant : boolean ; payable : boolean ; stateMutability ? : StateMutability ; inputs : RawAbiParameter [ ] ; outputs : RawAbiParameter [ ] ; type : string ; } export interface EventDeclaration { name : string ; isAnonymous : boolean ; inputs : EventArgDeclaration [ ] ; } export interface EventArgDeclaration { isIndexed : boolean ; name ? : string | undefined ; type : EvmType ; } export interface RawEventAbiDefinition { type : 'event' ; anonymous ? : boolean ; name : string ; inputs : RawEventArgAbiDefinition [ ] ; } export interface RawEventArgAbiDefinition { indexed : boolean ; name : string ; type : string ; } export interface BytecodeLinkReference { reference : string ; name ? : string ; } export interface BytecodeWithLinkReferences { bytecode : string ; linkReferences ? : BytecodeLinkReference [ ] ; } export interface DocumentationResult { author ? : string ; details ? : string ; notice ? : string ; title ? : string ; methods ? : { [ methodName ] : FunctionDocumentation ; } ; } export function parse ( abi , rawName , documentation ? ) { const constructors = [ ] ; let fallback ; const functions = [ ] ; const events = [ ] ; const structs = [ ] ; function registerStruct ( newStruct ) { const newStructName = newStruct . structName ?. toString ( ) ; if ( ! structs . find ( ( s ) => s . structName ?. toString ( ) === newStructName ) ) { structs . push ( newStruct ) ; } } abi . forEach ( ( abiPiece ) => { if ( abiPiece . type === 'fallback' ) { if ( fallback ) { throw new Error ( `Fallback function can't be defined more than once! ${ JSON . stringify ( abiPiece ) } Previously defined: ${ JSON . stringify ( fallback ) }` ) ; } fallback = parseFallback ( abiPiece , registerStruct ) ; return ; } if ( abiPiece . type === 'constructor' ) { constructors . push ( parseConstructor ( abiPiece , registerStruct ) ) ; return ; } if ( abiPiece . type === 'function' ) { functions . push ( parseFunctionDeclaration ( abiPiece , registerStruct , documentation ) ) ; return ; } if ( abiPiece . type === 'event' ) { const eventAbi = ( abiPiece as any ) as RawEventAbiDefinition ; events . push ( parseEvent ( eventAbi , registerStruct ) ) ; return ; } debug ( `Unrecognized abi element: ${ abiPiece . type }` ) ; } ) ; return { name : normalizeName ( rawName ) , rawName , fallback , constructor : constructors , functions : groupBy ( functions , ( f ) => f . name ) , events : groupBy ( events , ( e ) => e . name ) , structs : groupBy ( structs , ( e ) => e . structName && e . structName . toString ( ) ) , documentation : documentation ? omit ( documentation , [ 'methods' ] ) : undefined , } ; } function parseOutputs ( registerStruct , outputs ? ) { if ( ! outputs || outputs . length === 0 ) { return [ { name : '' , type : { type : 'void' } } ] ; } else { return outputs . map ( parseRawAbiParameter . bind ( null , registerStruct ) ) ; } } export function parseEvent ( abiPiece , registerStruct ) { debug ( `Parsing event "${ abiPiece . name }"` ) ; return { name : abiPiece . name , isAnonymous : abiPiece . anonymous ?? false , inputs : abiPiece . inputs . map ( parseRawEventArg . bind ( null , registerStruct ) ) , } ; } function parseRawEventArg ( registerStruct , eventArg ) { return { name : parseEmptyAsUndefined ( eventArg . name ) , isIndexed : eventArg . indexed , type : parseRawAbiParameterType ( eventArg , registerStruct ) , } ; } function parseEmptyAsUndefined ( smt ) { if ( smt === '' ) { return undefined ; } return smt ; } function findStateMutability ( abiPiece ) { if ( abiPiece . stateMutability ) { return abiPiece . stateMutability ; } if ( abiPiece . constant ) { return 'view' ; } return abiPiece . payable ? 'payable' : 'nonpayable' ; } export function getFunctionDocumentation ( abiPiece , documentation ? ) { const docKey = `${ abiPiece . name }(${ abiPiece . inputs . map ( ( { type } ) => type ) . join ( ',' ) })` ; return documentation && documentation . methods && documentation . methods [ docKey ] ; } function parseConstructor ( abiPiece , registerStruct ) { debug ( `Parsing constructor declaration` ) ; return { name : 'constructor' , inputs : abiPiece . inputs . map ( parseRawAbiParameter . bind ( null , registerStruct ) ) , outputs : [ ] , stateMutability : findStateMutability ( abiPiece ) , } ; } function parseFallback ( abiPiece , registerStruct ) { debug ( `Parsing fallback declaration` ) ; return { name : 'fallback' , inputs : [ ] , outputs : parseOutputs ( registerStruct , abiPiece . outputs ) , stateMutability : findStateMutability ( abiPiece ) , } ; } function parseFunctionDeclaration ( abiPiece , registerStruct , documentation ? ) { debug ( `Parsing function declaration "${ abiPiece . name }"` ) ; return { name : abiPiece . name , inputs : abiPiece . inputs . map ( parseRawAbiParameter . bind ( null , registerStruct ) ) , outputs : parseOutputs ( registerStruct , abiPiece . outputs ) , stateMutability : findStateMutability ( abiPiece ) , documentation : getFunctionDocumentation ( abiPiece , documentation ) , } ; } function parseRawAbiParameter ( registerStruct , rawAbiParameter ) { return { name : rawAbiParameter . name , type : parseRawAbiParameterType ( rawAbiParameter , registerStruct ) , } ; } const isStructType = ( evmType ) : evmType is StructType => evmType . type === 'array' || evmType . type === 'tuple' ; function parseRawAbiParameterType ( rawAbiParameter , registerStruct ) { const components = rawAbiParameter . components && rawAbiParameter . components . map ( ( component ) => ( { name : component . name , type : parseRawAbiParameterType ( component , registerStruct ) , } ) ) ; const parsed = parseEvmType ( rawAbiParameter . type , components , rawAbiParameter . internalType ) ; if ( isStructType ( parsed ) ) { if ( 'size' in parsed && parsed . size > 1 && isStructType ( parsed . itemType ) && parsed . structName ) { registerStruct ( { ... parsed . itemType , structName : parsed . structName . merge ( { identifier : parsed . structName . identifier . replace ( new RegExp ( `\\[${ parsed . size }\\]$` ) , '' ) , } ) , } ) ; } else { registerStruct ( parsed ) ; } } return parsed ; } export function extractAbi ( rawJson ) { let json ; try { json = JSON . parse ( rawJson ) ; } catch { throw new MalformedAbiError ( 'Not a json' ) ; } if ( ! json ) { throw new MalformedAbiError ( 'Not a json' ) ; } if ( Array . isArray ( json ) ) { return json ; } if ( Array . isArray ( json . abi ) ) { return json . abi ; } else if ( json . compilerOutput && Array . isArray ( json . compilerOutput . abi ) ) { return json . compilerOutput . abi ; } throw new MalformedAbiError ( 'Not a valid ABI' ) ; } export function extractBytecode ( rawContents ) { const bytecodeRegex = /^(0x)?(([0-9a-fA-F][0-9a-fA-F])|(__[a-zA-Z0-9/\\:_$.-]{36}__))+$/ ; if ( rawContents . match ( bytecodeRegex ) ) return extractLinkReferences ( rawContents ) ; let json ; try { json = JSON . parse ( rawContents ) ; } catch { return undefined ; } if ( ! json ) return undefined ; if ( json . evm ?. bytecode ?. object ?. match ( bytecodeRegex ) ) { return extractLinkReferences ( json . evm . bytecode . object , json . evm . bytecode . linkReferences ) ; } if ( json . compilerOutput ?. evm ?. bytecode ?. object ?. match ( bytecodeRegex ) ) { return extractLinkReferences ( json . compilerOutput . evm . bytecode . object , json . compilerOutput . evm . bytecode . linkReferences ) ; } if ( json . bytecode ?. match ( bytecodeRegex ) ) { return extractLinkReferences ( json . bytecode , json . linkReferences ) ; } return undefined ; } export function extractDocumentation ( rawContents ) { let json ; try { json = JSON . parse ( rawContents ) ; } catch { return undefined ; } if ( ! json || ( ! json . devdoc && ! json . userdoc ) ) return undefined ; const result = json . devdoc || { } ; if ( json . userdoc ) { result . notice = json . userdoc . notice ; if ( ! json . userdoc . methods ) return result ; result . methods = result . methods || { } ; Object . entries < { notice : string ; } > ( json . userdoc . methods ) . forEach ( ( [ key , { notice } ] ) => { if ( result . methods ) result . methods [ key ] = { ... result . methods [ key ] , notice } ; } ) ; } return result ; } function extractLinkReferences ( _bytecode , linkReferencesObj ? ) { const bytecode = ensure0xPrefix ( _bytecode ) ; const allLinkReferencesRegex = /__[a-zA-Z0-9/\\:_$.-]{36}__/g ; const allReferences = bytecode . match ( allLinkReferencesRegex ) ; if ( ! allReferences ) return { bytecode } ; const uniqueReferences = Array . from ( new Set ( allReferences ) ) ; const refToNameMap = linkReferencesObj ? extractLinkReferenceContractNames ( linkReferencesObj ) : { } ; const linkReferences = uniqueReferences . map ( ( reference ) => refToNameMap [ reference ] ? { reference , name : refToNameMap [ reference ] } : { reference } ) ; return { bytecode , linkReferences } ; } function extractLinkReferenceContractNames ( linkReferences ) { const nameMap = { } ; Object . keys ( linkReferences ) . forEach ( ( contractFile ) => Object . keys ( linkReferences [ contractFile ] ) . forEach ( ( contractName ) => { const contractPath = `${ contractFile }:${ contractName }` ; const contractRef = `__$${ keccak_256 ( contractPath ) . slice ( 0 , 34 ) }$__` ; nameMap [ contractRef ] = contractPath ; } ) ) ; return nameMap ; } export function ensure0xPrefix ( hexString ) { if ( hexString . startsWith ( '0x' ) ) return hexString ; return '0x' + hexString ; } export function isConstant ( fn ) { return ( ( fn . stateMutability === 'pure' || fn . stateMutability === 'view' ) && fn . inputs . length === 0 && fn . outputs . length === 1 ) ; } export function isConstantFn ( fn ) { return ( fn . stateMutability === 'pure' || fn . stateMutability === 'view' ) && ! isConstant ( fn ) ; }