import { gatherSourcesAndCanonizeImports , ImportsFsEngine , resolvers } from '@resolver-engine/imports-fs' ; import fs from 'fs' ; import path from 'path' ; import chalk from 'chalk' ; import axios from 'axios' ; import querystring from 'querystring' ; const isDirectory = ( directoryPath ) => fs . existsSync ( path . resolve ( directoryPath ) ) && fs . statSync ( path . resolve ( directoryPath ) ) . isDirectory ( ) ; function findInputs ( sourcePath ) { const stack = [ sourcePath ] ; const inputFiles = [ ] ; while ( stack . length > 0 ) { const dir = stack . pop ( ) ! ; const files = fs . readdirSync ( dir ) ; for ( const file of files ) { const filePath = path . join ( dir , file ) ; if ( isDirectory ( filePath ) ) { stack . push ( filePath ) ; } else if ( file . endsWith ( '.sol' ) ) { inputFiles . push ( filePath ) ; } } } return inputFiles ; } function fromEntries < T extends string | number | symbol , U > ( args ) { return args . reduce ( ( result , [ key , value ] ) => ( { ... result , [ key ] : value , } ) , { } as Record < T , U > ) ; } function canonizeImports ( source , globalUrls ) { let canonizedSource = source ; for ( const globalUrl of globalUrls ) { canonizedSource = canonizedSource . replace ( new RegExp ( globalUrl , 'gim' ) , path . basename ( globalUrl ) ) ; } return canonizedSource ; } export async function createJsonInputs ( sourcePath ) { const resolver = ImportsFsEngine ( ) . addResolver ( resolvers . BacktrackFsResolver ( ) ) ; const allContracts = findInputs ( sourcePath ) ; return fromEntries ( await Promise . all ( allContracts . map ( async ( contract ) => { const sources = await gatherSourcesAndCanonizeImports ( [ contract ] , '.' , resolver ) ; const globalUrls = sources . map ( ( { url } ) => url ) ; return [ path . parse ( contract ) . name , { sources : fromEntries ( sources . map ( ( { source , url } ) => [ path . basename ( url ) , { content : canonizeImports ( source , globalUrls ) } ] ) ) , language : 'Solidity' , } , ] as [ string , { sources : { [ key ] : { content : string ; } ; } ; language : string ; } ] ; } ) ) ) ; } type Awaited < T > = T extends Promise < infer U > ? U : never ; export type JsonInputs = Awaited < ReturnType < typeof createJsonInputs > > ; const etherscanUrl = ( network ? ) => { if ( ! network || network === 'mainnet' ) { return 'https://api.etherscan.io/api' ; } return `https://api-${ network }.etherscan.io/api` ; } ; function getEtherscanContractAddress ( address , network ? ) { if ( ! network || network === 'mainnet' ) { return `https://etherscan.io/address/${ address }` ; } return `https://${ network }.etherscan.io/address/${ address }` ; } async function getCompilerOptions ( waffleConfigPath ) { const config = JSON . parse ( fs . readFileSync ( waffleConfigPath ) . toString ( ) ) ; const compilerVersion = config . compilerVersion ; const isOptimized = config . compilerOptions ?. optimizer ?. enabled ?? true ; const optimizerRuns = ! isOptimized ? 0 : config . compilerOptions ?. optimizer ?. runs ?? 200 ; return { compilerVersion , isOptimized , optimizerRuns , } ; } async function isContractVerified ( etherscanApiKey , address , network ? ) { const response = ( await axios . get ( `${ etherscanUrl ( network ) }?${ querystring . stringify ( { module : 'contract' , action : 'getabi' , apikey : etherscanApiKey , address , } ) }` ) ) . data ; return response ?. status === '1' ; } async function getVerificationRequestBody ( etherscanApiKey , waffleConfigPath , jsonInput , address , contractName , constructorArgs ? ) { const waffleConfig = await getCompilerOptions ( waffleConfigPath ) ; const inputWithOptions = { ... jsonInput , settings : { optimizer : { enabled : waffleConfig . isOptimized , runs : waffleConfig . optimizerRuns , } , } , } ; return querystring . stringify ( { apikey : etherscanApiKey , module : 'contract' , action : 'verifysourcecode' , contractaddress : address . toLowerCase ( ) , sourceCode : JSON . stringify ( inputWithOptions ) , codeformat : 'solidity-standard-json-input' , contractname : `${ contractName }.sol:${ contractName }` , compilerversion : waffleConfig . compilerVersion , constructorArguements : constructorArgs ?. slice ( 2 ) ?? '' , licenseType : '1' , } ) ; } async function getSingleFileVerificationRequestBody ( etherscanApiKey , waffleConfigPath , source , address , contractName , constructorArgs ? ) { const waffleConfig = await getCompilerOptions ( waffleConfigPath ) ; return querystring . stringify ( { apikey : etherscanApiKey , module : 'contract' , action : 'verifysourcecode' , contractaddress : address . toLowerCase ( ) , sourceCode : source , codeformat : 'solidity-single-file' , contractname : contractName , compilerversion : waffleConfig . compilerVersion , constructorArguements : constructorArgs ?. slice ( 2 ) ?? '' , licenseType : '3' , optimizationUsed : waffleConfig . isOptimized ? 1 : 0 , runs : waffleConfig . optimizerRuns , } ) ; } const sleep = ( time ) => new Promise ( ( resolve ) => setTimeout ( resolve , time ) ) ; async function waitForContract ( ) { process . stdout . write ( chalk . blue ( 'Waiting for Etherscan to acknowledge the contract...' ) ) ; await sleep ( 30000 ) ; } function clearLine ( ) { process . stdout . clearLine ( 0 ) ; process . stdout . cursorTo ( 0 ) ; } async function sendRequest ( body , contractName , network ? ) { clearLine ( ) ; process . stdout . write ( chalk . blue ( 'Sending request to Etherscan...' ) ) ; const res = ( await axios . post ( etherscanUrl ( network ) , body , { headers : { 'Content-Type' : 'application/x-www-form-urlencoded; charset=UTF-8' , } , } ) ) . data ; if ( res . status === '0' ) { console . log ( chalk . bold ( chalk . yellow ( `\nVerification of ${ contractName } failed: ${ res . result }` ) ) ) ; return ; } return res . result ; } async function waitForResult ( etherscanApiKey , guid , network ? ) { clearLine ( ) ; process . stdout . write ( chalk . blue ( 'Patiently waiting for verification in queue...' ) ) ; while ( true ) { await sleep ( 5000 ) ; const res = ( await axios . get ( `${ etherscanUrl ( network ) }?${ querystring . stringify ( { apikey : etherscanApiKey , guid , module : 'contract' , action : 'checkverifystatus' , } ) }` ) ) . data ; if ( res . result . startsWith ( 'Fail' ) ) { clearLine ( ) ; console . log ( chalk . bold ( chalk . yellow ( `Verification of failed: ${ res . result }` ) ) ) ; return false ; } if ( res . result . startsWith ( 'Pass' ) ) { clearLine ( ) ; return true ; } } } export async function verifySingleFile ( etherscanApiKey , flattenScript , waffleConfigPath , contractName , address , constructorArgs ? , network ? ) { const flatContract = await flattenScript ( `${ contractName }.sol` ) ; if ( await isContractVerified ( etherscanApiKey , address , network ) ) { console . log ( chalk . bold ( chalk . green ( `Contract ${ contractName } is already verified under ${ address }. Skipping\n` ) ) ) ; return ; } console . log ( chalk . green ( `Verifying ${ contractName } on Etherscan` ) ) ; try { const body = await getSingleFileVerificationRequestBody ( etherscanApiKey , waffleConfigPath , flatContract , address , contractName , constructorArgs ) ; await waitForContract ( ) ; const guid = await sendRequest ( body , contractName , network ) ; if ( ! guid ) { return ; } if ( await waitForResult ( etherscanApiKey , guid ) ) { console . log ( chalk . bold ( chalk . green ( `Contract verified at ${ getEtherscanContractAddress ( address , network ) }\n` ) ) ) ; } } catch ( err ) { console . log ( chalk . bold ( chalk . yellow ( `Error during verification: ${ err . message ?? err }. Skipping\n` ) ) ) ; } } export async function verify ( etherscanApiKey , jsonInputs , waffleConfigPath , contractName , address , constructorArgs ? , network ? ) { const jsonInput = jsonInputs [ contractName ] ; if ( ! jsonInput ) { console . log ( chalk . bold ( chalk . yellow ( `No sources found for ${ contractName }. Skipping\n` ) ) ) ; return ; } if ( await isContractVerified ( etherscanApiKey , address , network ) ) { console . log ( chalk . bold ( chalk . green ( `Contract ${ contractName } is already verified under ${ address }. Skipping\n` ) ) ) ; return ; } console . log ( chalk . green ( `Verifying ${ contractName } on Etherscan` ) ) ; try { const body = await getVerificationRequestBody ( etherscanApiKey , waffleConfigPath , jsonInput , address , contractName , constructorArgs ) ; await waitForContract ( ) ; const guid = await sendRequest ( body , contractName , network ) ; if ( ! guid ) { return ; } if ( await waitForResult ( etherscanApiKey , guid ) ) { console . log ( chalk . bold ( chalk . green ( `Contract verified at ${ getEtherscanContractAddress ( address , network ) }\n` ) ) ) ; } } catch ( err ) { console . log ( chalk . bold ( chalk . yellow ( `Error during verification: ${ err . message ?? err }. Skipping\n` ) ) ) ; } }