import fs from 'fs' ; import { ensureDirSync } from 'fs-extra' ; import ts , { ClassElement , PropertySignature , SyntaxKind , TypeNode } from 'typescript' ; import { AbiInput , AbiOutput , ContractAbiDefinition , ContractEntryDefinition } from 'web3x/contract' ; import { ContractBuildData , loadDataFromConfig } from './sources' ; import { Config } from './sources/config' ; const printer = ts . createPrinter ( { newLine : ts . NewLineKind . LineFeed , } ) ; function makeImports ( name , web3xPath ) { return [ ts . createImportDeclaration ( undefined , undefined , ts . createImportClause ( ts . createIdentifier ( 'BN' ) , undefined ) , ts . createLiteral ( 'bn.js' ) ) , ts . createImportDeclaration ( undefined , undefined , ts . createImportClause ( undefined , ts . createNamedImports ( [ ts . createImportSpecifier ( undefined , ts . createIdentifier ( 'Address' ) ) ] ) ) , ts . createLiteral ( `${ web3xPath }/address` ) ) , ts . createImportDeclaration ( undefined , undefined , ts . createImportClause ( undefined , ts . createNamedImports ( [ ts . createImportSpecifier ( undefined , ts . createIdentifier ( 'EventLog' ) ) , ts . createImportSpecifier ( undefined , ts . createIdentifier ( 'TransactionReceipt' ) ) , ] ) ) , ts . createLiteral ( `${ web3xPath }/formatters` ) ) , ts . createImportDeclaration ( undefined , undefined , ts . createImportClause ( undefined , ts . createNamedImports ( [ ts . createImportSpecifier ( undefined , ts . createIdentifier ( 'Contract' ) ) , ts . createImportSpecifier ( undefined , ts . createIdentifier ( 'ContractOptions' ) ) , ts . createImportSpecifier ( undefined , ts . createIdentifier ( 'TxCall' ) ) , ts . createImportSpecifier ( undefined , ts . createIdentifier ( 'TxSend' ) ) , ts . createImportSpecifier ( undefined , ts . createIdentifier ( 'EventSubscriptionFactory' ) ) , ] ) ) , ts . createLiteral ( `${ web3xPath }/contract` ) ) , ts . createImportDeclaration ( undefined , undefined , ts . createImportClause ( undefined , ts . createNamedImports ( [ ts . createImportSpecifier ( undefined , ts . createIdentifier ( 'Eth' ) ) ] ) ) , ts . createLiteral ( `${ web3xPath }/eth` ) ) , ts . createImportDeclaration ( undefined , undefined , ts . createImportClause ( ts . createIdentifier ( 'abi' ) , undefined ) , ts . createLiteral ( `./${ name }Abi` ) ) , ] ; } function makeEventType ( definition ) { const props = ts . createTypeLiteralNode ( definition . inputs ! . map ( input => ts . createPropertySignature ( undefined , input . name , undefined , getTsTypeFromSolidityType ( input , true ) , undefined ) ) ) ; return ts . createTypeAliasDeclaration ( undefined , [ ts . createToken ( ts . SyntaxKind . ExportKeyword ) ] , `${ definition . name }Event` , undefined , props ) ; } function makeEventTypes ( abi ) { return abi . filter ( def => def . type === 'event' ) . map ( makeEventType ) ; } function makeEventLogInterface ( definition ) { const eventName = `${ definition . name ! }Event` ; return ts . createInterfaceDeclaration ( undefined , [ ts . createToken ( ts . SyntaxKind . ExportKeyword ) ] , `${ eventName }Log` , undefined , [ ts . createHeritageClause ( ts . SyntaxKind . ExtendsKeyword , [ ts . createExpressionWithTypeArguments ( [ ts . createTypeReferenceNode ( eventName , undefined ) , ts . createLiteralTypeNode ( ts . createLiteral ( definition . name ! ) ) , ] , ts . createRegularExpressionLiteral ( 'EventLog' ) ) , ] ) , ] , [ ] ) ; } function makeEventLogInterfaces ( abi ) { return abi . filter ( def => def . type === 'event' ) . map ( makeEventLogInterface ) ; } function makeEventsInterface ( name , abi ) { const events = abi . filter ( def => def . type === 'event' ) . map ( event => event . name ! ) ; return ts . createInterfaceDeclaration ( undefined , undefined , `${ name }Events` , undefined , undefined , events . map ( eventName => ts . createPropertySignature ( undefined , eventName , undefined , ts . createTypeReferenceNode ( `EventSubscriptionFactory` , [ ts . createTypeReferenceNode ( `${ eventName }EventLog` , undefined ) , ] ) , undefined ) ) ) ; } function makeEventLogsInterface ( name , abi ) { const events = abi . filter ( def => def . type === 'event' ) . map ( event => event . name ! ) ; return ts . createInterfaceDeclaration ( undefined , undefined , `${ name }EventLogs` , undefined , undefined , events . map ( eventName => ts . createPropertySignature ( undefined , eventName , undefined , ts . createTypeReferenceNode ( `${ eventName }EventLog` , undefined ) , undefined ) ) ) ; } function makeTxEventLogsInterface ( name , abi ) { const events = abi . filter ( def => def . type === 'event' ) . map ( event => event . name ! ) ; return ts . createInterfaceDeclaration ( undefined , undefined , `${ name }TxEventLogs` , undefined , undefined , events . map ( eventName => ts . createPropertySignature ( undefined , eventName , undefined , ts . createArrayTypeNode ( ts . createTypeReferenceNode ( `${ eventName }EventLog` , undefined ) ) , undefined ) ) ) ; } function makeTransactionReceiptInterface ( name ) { return ts . createInterfaceDeclaration ( undefined , [ ts . createToken ( ts . SyntaxKind . ExportKeyword ) ] , `${ name }TransactionReceipt` , undefined , [ ts . createHeritageClause ( ts . SyntaxKind . ExtendsKeyword , [ ts . createExpressionWithTypeArguments ( [ ts . createTypeReferenceNode ( `${ name }TxEventLogs` , undefined ) ] , ts . createRegularExpressionLiteral ( 'TransactionReceipt' ) ) , ] ) , ] , [ ] ) ; } function getBaseType ( type , returnValue ) { if ( type . match ( /u?int\d*/ ) || type . match ( /u?fixed[0-9x]*/ ) ) { return ! returnValue ? ts . createUnionTypeNode ( [ ts . createKeywordTypeNode ( ts . SyntaxKind . NumberKeyword ) , ts . createKeywordTypeNode ( ts . SyntaxKind . StringKeyword ) , ts . createTypeReferenceNode ( ts . createIdentifier ( 'BN' ) , undefined ) , ] ) : ts . createKeywordTypeNode ( ts . SyntaxKind . StringKeyword ) ; } if ( type === 'bool' ) { return ts . createKeywordTypeNode ( ts . SyntaxKind . BooleanKeyword ) ; } if ( type === 'address' ) { return ts . createTypeReferenceNode ( 'Address' , undefined ) ; } return ts . createKeywordTypeNode ( ts . SyntaxKind . StringKeyword ) ; } function getTupleType ( components , returnValue ) { return ts . createTypeLiteralNode ( components ! . map ( prop => ts . createPropertySignature ( undefined , prop . name , undefined , getTsTypeFromSolidityType ( prop , returnValue ) , undefined ) ) ) ; } function getTsTypeFromSolidityType ( input , returnValue , type ? ) { type = type || input . type ; const arrayMatched = type . match ( /(.+)\[\d*\]$/ ) ; if ( arrayMatched ) { const tsType = getTsTypeFromSolidityType ( input , returnValue , arrayMatched [ 1 ] ) ; return ts . createArrayTypeNode ( tsType ) ; } else { const isTuple = type === 'tuple' ; return isTuple ? getTupleType ( input . components , returnValue ) : getBaseType ( type , returnValue ) ; } } function makeParameter ( input , index ) { return ts . createParameter ( undefined , undefined , undefined , input . name || `a${ index }` , undefined , getTsTypeFromSolidityType ( input , false ) ) ; } function generateReturnTypes ( outputs ) { if ( outputs . length === 0 ) { return [ ] ; } else if ( outputs . length === 1 ) { return [ getTsTypeFromSolidityType ( outputs [ 0 ] , true ) ] ; } else { const propSigs = [ ] ; for ( let index = 0 ; index < outputs . length ; index ++ ) { const output = outputs [ index ] ; const type = getTsTypeFromSolidityType ( output as AbiInput , true ) ; if ( output . name ) { const nameSig = ts . createPropertySignature ( undefined , ts . createStringLiteral ( output . name ) , undefined , type , undefined ) ; propSigs . push ( nameSig ) ; } const indexSig = ts . createPropertySignature ( undefined , ts . createNumericLiteral ( index . toString ( ) ) , undefined , type , undefined ) ; propSigs . push ( indexSig ) ; } return [ ts . createTypeLiteralNode ( propSigs ) ] ; } } function getOutputType ( name , definition ) { if ( ! definition . stateMutability ) { if ( definition . constant && definition . constant === true ) { return ts . createTypeReferenceNode ( 'TxCall' , generateReturnTypes ( definition . outputs || [ ] ) ) ; } else { return ts . createTypeReferenceNode ( 'TxSend' , [ ts . createTypeReferenceNode ( `${ name }TransactionReceipt` , undefined ) ] ) ; } } if ( definition . stateMutability === 'view' || definition . stateMutability === 'pure' ) { if ( definition . outputs && definition . outputs . length ) { return ts . createTypeReferenceNode ( 'TxCall' , generateReturnTypes ( definition . outputs ) ) ; } else { return ts . createTypeReferenceNode ( 'TxCall' , [ ts . createKeywordTypeNode ( SyntaxKind . VoidKeyword ) ] ) ; } } else { return ts . createTypeReferenceNode ( 'TxSend' , [ ts . createTypeReferenceNode ( `${ name }TransactionReceipt` , undefined ) ] ) ; } } function makeMethodSignature ( name , definition ) { return ts . createMethodSignature ( undefined , definition . inputs ! . map ( makeParameter ) , getOutputType ( name , definition ) , definition . name ! , undefined ) ; } function makeMethodsInterface ( name , abi ) { const methods = abi . filter ( def => def . type === 'function' ) . map ( def => makeMethodSignature ( name , def ) ) ; return ts . createInterfaceDeclaration ( undefined , undefined , `${ name }Methods` , undefined , undefined , methods ) ; } function makeContract ( name , initData , abi ) { const members = [ ] ; const ctor = ts . createConstructor ( undefined , undefined , [ ts . createParameter ( undefined , undefined , undefined , 'eth' , undefined , ts . createTypeReferenceNode ( 'Eth' , undefined ) ) , ts . createParameter ( undefined , undefined , undefined , 'address' , ts . createToken ( ts . SyntaxKind . QuestionToken ) , ts . createTypeReferenceNode ( 'Address' , undefined ) ) , ts . createParameter ( undefined , undefined , undefined , 'options' , ts . createToken ( ts . SyntaxKind . QuestionToken ) , ts . createTypeReferenceNode ( 'ContractOptions' , undefined ) ) , ] , ts . createBlock ( [ ts . createStatement ( ts . createCall ( ts . createSuper ( ) , undefined , [ ts . createIdentifier ( 'eth' ) , ts . createIdentifier ( 'abi' ) , ts . createIdentifier ( 'address' ) , ts . createIdentifier ( 'options' ) , ] ) ) , ] , true ) ) ; members . push ( ctor ) ; if ( initData ) { const ctorDef = abi . find ( def => def . type === 'constructor' ) ! ; const inputs = ctorDef && ctorDef . inputs ? ctorDef . inputs : [ ] ; const deployMethod = ts . createMethod ( undefined , undefined , undefined , 'deploy' , undefined , undefined , inputs . map ( makeParameter ) , ts . createTypeReferenceNode ( 'TxSend' , [ ts . createTypeReferenceNode ( `${ name }TransactionReceipt` , undefined ) ] ) , ts . createBlock ( [ ts . createReturn ( ts . createAsExpression ( ts . createCall ( ts . createPropertyAccess ( ts . createSuper ( ) , 'deployBytecode' ) , undefined , [ ts . createStringLiteral ( initData ) , ... inputs . map ( input => ts . createIdentifier ( input . name ) ) , ] ) , ts . createKeywordTypeNode ( ts . SyntaxKind . AnyKeyword ) ) ) , ] , true ) ) ; members . push ( deployMethod ) ; } return ts . createClassDeclaration ( undefined , [ ts . createToken ( ts . SyntaxKind . ExportKeyword ) ] , name , undefined , [ ts . createHeritageClause ( ts . SyntaxKind . ExtendsKeyword , [ ts . createExpressionWithTypeArguments ( [ ts . createTypeReferenceNode ( `${ name }Definition` , undefined ) ] , ts . createRegularExpressionLiteral ( 'Contract' ) ) , ] ) , ] , members ) ; } function makeDefinitionInterface ( name ) { const props = [ ts . createPropertySignature ( undefined , 'methods' , undefined , ts . createTypeReferenceNode ( `${ name }Methods` , undefined ) , undefined ) , ts . createPropertySignature ( undefined , 'events' , undefined , ts . createTypeReferenceNode ( `${ name }Events` , undefined ) , undefined ) , ts . createPropertySignature ( undefined , 'eventLogs' , undefined , ts . createTypeReferenceNode ( `${ name }EventLogs` , undefined ) , undefined ) , ] ; return ts . createInterfaceDeclaration ( undefined , [ ts . createToken ( ts . SyntaxKind . ExportKeyword ) ] , `${ name }Definition` , undefined , undefined , props ) ; } function makeAbiExport ( name ) { return ts . createVariableStatement ( [ ts . createModifier ( ts . SyntaxKind . ExportKeyword ) ] , [ ts . createVariableDeclaration ( `${ name }Abi` , undefined , ts . createIdentifier ( 'abi' ) ) ] ) ; } function makeFile ( name , abi , initData , web3xPath ) { const imports = makeImports ( name , web3xPath ) ; const eventTypes = makeEventTypes ( abi ) ; const eventLogTypes = makeEventLogInterfaces ( abi ) ; const eventsInterface = makeEventsInterface ( name , abi ) ; const eventLogsInterface = makeEventLogsInterface ( name , abi ) ; const eventTxLogsInterface = makeTxEventLogsInterface ( name , abi ) ; const txReceiptInterface = makeTransactionReceiptInterface ( name ) ; const methodsInterface = makeMethodsInterface ( name , abi ) ; const definitionInterface = makeDefinitionInterface ( name ) ; const contract = makeContract ( name , initData , abi ) ; const abiExport = makeAbiExport ( name ) ; return ts . createNodeArray ( [ ... imports , ... eventTypes , ... eventLogTypes , eventsInterface , eventLogsInterface , eventTxLogsInterface , txReceiptInterface , methodsInterface , definitionInterface , contract , abiExport , ] ) ; } async function makeAndWriteAbi ( outputPath , name , abi , web3xPath ) { const abiOutputFile = `${ outputPath }/${ name }Abi.ts` ; const output = `import { ContractAbi} from '${ web3xPath }/contract';\nexport default new ContractAbi(${ JSON . stringify ( abi , undefined , 2 ) });` ; fs . writeFileSync ( abiOutputFile , output ) ; } export async function makeAndWriteFiles ( outputPath , name , { abi , initData } , web3xPath ) { const interfaceOutputFile = `${ outputPath }/${ name }.ts` ; const resultFile = ts . createSourceFile ( '' , '' , ts . ScriptTarget . Latest , false , ts . ScriptKind . TS ) ; resultFile . statements = makeFile ( name , abi , initData , web3xPath ) ; fs . writeFileSync ( interfaceOutputFile , printer . printFile ( resultFile ) ) ; makeAndWriteAbi ( outputPath , name , abi , web3xPath ) ; } export function getWeb3xPath ( ) { const pkg = JSON . parse ( fs . readFileSync ( 'package.json' ) . toString ( ) ) ; return { ... pkg . dependencies , ... pkg . devDependencies } [ 'web3x-es' ] ? 'web3x-es' : 'web3x' ; } async function main ( ) { const configFile = process . argv [ 2 ] || 'contracts.json' ; const config = JSON . parse ( fs . readFileSync ( configFile ) . toString ( ) ) as Config ; const { outputPath = './contracts' , web3xPath = getWeb3xPath ( ) } = config ; ensureDirSync ( outputPath ) ; await Promise . all ( Object . entries ( config . contracts ) . map ( async ( entry ) => { const buildData = await loadDataFromConfig ( entry [ 1 ] ) ; makeAndWriteFiles ( outputPath , entry [ 0 ] , buildData , web3xPath ) ; } ) ) ; } if ( require . main === module ) { main ( ) . catch ( console . error ) ; }