2019-05-15 01:25:46 +03:00
"use strict" ;
import { BlockTag , TransactionRequest , TransactionResponse } from "@ethersproject/abstract-provider" ;
2020-11-23 01:43:32 +03:00
import { hexlify , hexValue , isHexString } from "@ethersproject/bytes" ;
2020-05-04 00:32:16 +03:00
import { Network , Networkish } from "@ethersproject/networks" ;
2019-08-01 23:13:35 +03:00
import { deepCopy , defineReadOnly } from "@ethersproject/properties" ;
2021-04-14 22:04:24 +03:00
import { accessListify } from "@ethersproject/transactions" ;
2020-10-23 02:46:52 +03:00
import { ConnectionInfo , fetchJson } from "@ethersproject/web" ;
2019-05-15 01:25:46 +03:00
2020-07-14 09:26:45 +03:00
import { showThrottleMessage } from "./formatter" ;
2019-08-02 01:04:06 +03:00
import { Logger } from "@ethersproject/logger" ;
import { version } from "./_version" ;
const logger = new Logger ( version ) ;
2019-05-15 01:25:46 +03:00
import { BaseProvider } from "./base-provider" ;
// The transaction has already been sanitized by the calls in Provider
2020-10-23 02:46:52 +03:00
function getTransactionPostData ( transaction : TransactionRequest ) : Record < string , string > {
const result : Record < string , string > = { } ;
2019-05-15 01:25:46 +03:00
for ( let key in transaction ) {
if ( ( < any > transaction ) [ key ] == null ) { continue ; }
2021-03-26 23:16:56 +03:00
let value = ( < any > transaction ) [ key ] ;
2021-06-26 05:58:55 +03:00
if ( key === "type" && value === 0 ) { continue ; }
2020-10-23 02:46:52 +03:00
// Quantity-types require no leading zero, unless 0
2021-06-26 05:58:55 +03:00
if ( ( < any > { type : true , gasLimit : true , gasPrice : true , maxFeePerGs : true , maxPriorityFeePerGas : true , nonce : true , value : true } ) [ key ] ) {
2021-03-26 23:16:56 +03:00
value = hexValue ( hexlify ( value ) ) ;
} else if ( key === "accessList" ) {
2021-05-07 07:31:15 +03:00
value = "[" + accessListify ( value ) . map ( ( set ) = > {
2021-04-14 22:04:24 +03:00
return ` {address:" ${ set . address } ",storageKeys:[" ${ set . storageKeys . join ( '","' ) } "]} ` ;
} ) . join ( "," ) + "]" ;
2021-03-26 23:16:56 +03:00
} else {
value = hexlify ( value ) ;
2019-05-15 01:25:46 +03:00
}
2020-10-23 02:46:52 +03:00
result [ key ] = value ;
2019-05-15 01:25:46 +03:00
}
2020-10-23 02:46:52 +03:00
return result ;
2019-05-15 01:25:46 +03:00
}
function getResult ( result : { status? : number , message? : string , result? : any } ) : any {
// getLogs, getHistory have weird success responses
if ( result . status == 0 && ( result . message === "No records found" || result . message === "No transactions found" ) ) {
return result . result ;
}
if ( result . status != 1 || result . message != "OK" ) {
2019-11-01 17:51:08 +03:00
const error : any = new Error ( "invalid response" ) ;
2019-05-15 01:25:46 +03:00
error . result = JSON . stringify ( result ) ;
2020-07-14 09:26:45 +03:00
if ( ( result . result || "" ) . toLowerCase ( ) . indexOf ( "rate limit" ) >= 0 ) {
error . throttleRetry = true ;
}
2019-05-15 01:25:46 +03:00
throw error ;
}
return result . result ;
}
function getJsonResult ( result : { jsonrpc : string , result? : any , error ? : { code? : number , data? : any , message? : string } } ) : any {
2020-07-14 09:26:45 +03:00
// This response indicates we are being throttled
if ( result && ( < any > result ) . status == 0 && ( < any > result ) . message == "NOTOK" && ( result . result || "" ) . toLowerCase ( ) . indexOf ( "rate limit" ) >= 0 ) {
const error : any = new Error ( "throttled response" ) ;
error . result = JSON . stringify ( result ) ;
error . throttleRetry = true ;
throw error ;
}
2019-05-15 01:25:46 +03:00
if ( result . jsonrpc != "2.0" ) {
// @TODO: not any
2019-11-01 17:51:08 +03:00
const error : any = new Error ( "invalid response" ) ;
2019-05-15 01:25:46 +03:00
error . result = JSON . stringify ( result ) ;
throw error ;
}
if ( result . error ) {
// @TODO: not any
2019-11-01 17:51:08 +03:00
const error : any = new Error ( result . error . message || "unknown error" ) ;
2019-05-15 01:25:46 +03:00
if ( result . error . code ) { error . code = result . error . code ; }
if ( result . error . data ) { error . data = result . error . data ; }
throw error ;
}
return result . result ;
}
// The blockTag was normalized as a string by the Provider pre-perform operations
function checkLogTag ( blockTag : string ) : number | "latest" {
if ( blockTag === "pending" ) { throw new Error ( "pending not supported" ) ; }
if ( blockTag === "latest" ) { return blockTag ; }
return parseInt ( blockTag . substring ( 2 ) , 16 ) ;
}
2020-02-18 02:14:02 +03:00
const defaultApiKey = "9D13ZE7XSBTJ94N9BNJ2MA33VMAY2YPIRB" ;
2020-11-23 01:43:32 +03:00
function checkError ( method : string , error : any , transaction : any ) : any {
// Undo the "convenience" some nodes are attempting to prevent backwards
// incompatibility; maybe for v6 consider forwarding reverts as errors
if ( method === "call" && error . code === Logger . errors . SERVER_ERROR ) {
const e = error . error ;
2021-06-22 06:25:37 +03:00
// Etherscan keeps changing their string
if ( e && ( e . message . match ( /reverted/i ) || e . message . match ( /VM execution error/i ) ) ) {
// Etherscan prefixes the data like "Reverted 0x1234"
let data = e . data ;
if ( data ) { data = "0x" + data . replace ( /^.*0x/i , "" ) ; }
if ( isHexString ( data ) ) { return data ; }
logger . throwError ( "missing revert data in call exception" , Logger . errors . CALL_EXCEPTION , {
error , data : "0x"
} ) ;
2020-11-23 01:43:32 +03:00
}
}
2020-09-16 09:19:28 +03:00
// Get the message from any nested error structure
2020-09-11 08:10:15 +03:00
let message = error . message ;
2020-09-16 09:19:28 +03:00
if ( error . code === Logger . errors . SERVER_ERROR ) {
if ( error . error && typeof ( error . error . message ) === "string" ) {
message = error . error . message ;
} else if ( typeof ( error . body ) === "string" ) {
message = error . body ;
} else if ( typeof ( error . responseText ) === "string" ) {
message = error . responseText ;
}
}
message = ( message || "" ) . toLowerCase ( ) ;
// "Insufficient funds. The account you tried to send transaction from does not have enough funds. Required 21464000000000 and got: 0"
if ( message . match ( /insufficient funds/ ) ) {
logger . throwError ( "insufficient funds for intrinsic transaction cost" , Logger . errors . INSUFFICIENT_FUNDS , {
error , method , transaction
} ) ;
}
// "Transaction with the same hash was already imported."
2021-07-02 07:17:45 +03:00
if ( message . match ( /same hash was already imported|transaction nonce is too low|nonce too low/ ) ) {
2020-09-16 09:19:28 +03:00
logger . throwError ( "nonce has already been used" , Logger . errors . NONCE_EXPIRED , {
error , method , transaction
} ) ;
}
// "Transaction gas price is too low. There is another transaction with same nonce in the queue. Try increasing the gas price or incrementing the nonce."
if ( message . match ( /another transaction with same nonce/ ) ) {
logger . throwError ( "replacement fee too low" , Logger . errors . REPLACEMENT_UNDERPRICED , {
error , method , transaction
} ) ;
2020-09-11 08:10:15 +03:00
}
2021-07-03 07:45:03 +03:00
if ( message . match ( /execution failed due to an exception|execution reverted/ ) ) {
2020-09-11 08:10:15 +03:00
logger . throwError ( "cannot estimate gas; transaction may fail or may require manual gas limit" , Logger . errors . UNPREDICTABLE_GAS_LIMIT , {
2020-09-16 09:19:28 +03:00
error , method , transaction
2020-09-11 08:10:15 +03:00
} ) ;
}
throw error ;
}
2019-05-15 01:25:46 +03:00
export class EtherscanProvider extends BaseProvider {
readonly baseUrl : string ;
readonly apiKey : string ;
2020-07-14 09:26:45 +03:00
2019-05-15 01:25:46 +03:00
constructor ( network? : Networkish , apiKey? : string ) {
super ( network ) ;
2021-05-07 07:31:15 +03:00
defineReadOnly ( this , "baseUrl" , this . getBaseUrl ( ) ) ;
defineReadOnly ( this , "apiKey" , apiKey || defaultApiKey ) ;
}
2019-05-15 01:25:46 +03:00
2021-05-07 07:31:15 +03:00
getBaseUrl ( ) : string {
switch ( this . network ? this . network . name : "invalid" ) {
2019-05-15 01:25:46 +03:00
case "homestead" :
2021-05-07 07:31:15 +03:00
return "https:/\/api.etherscan.io" ;
2019-05-15 01:25:46 +03:00
case "ropsten" :
2021-05-07 07:31:15 +03:00
return "https:/\/api-ropsten.etherscan.io" ;
2019-05-15 01:25:46 +03:00
case "rinkeby" :
2021-05-07 07:31:15 +03:00
return "https:/\/api-rinkeby.etherscan.io" ;
2019-05-15 01:25:46 +03:00
case "kovan" :
2021-05-07 07:31:15 +03:00
return "https:/\/api-kovan.etherscan.io" ;
2019-05-15 01:25:46 +03:00
case "goerli" :
2021-05-07 07:31:15 +03:00
return "https:/\/api-goerli.etherscan.io" ;
2022-05-12 23:38:29 +03:00
case "optimism" :
return "https:/\/api-optimistic.etherscan.io" ;
2022-07-14 04:01:53 +03:00
case "optimism-kovan" :
return "https:/\/api-kovan-optimistic.etherscan.io" ;
2019-05-15 01:25:46 +03:00
default :
}
2022-05-12 23:38:29 +03:00
return logger . throwArgumentError ( "unsupported network" , "network" , this . network . name ) ;
2019-05-15 01:25:46 +03:00
}
2021-05-07 07:31:15 +03:00
getUrl ( module : string , params : Record < string , string > ) : string {
const query = Object . keys ( params ) . reduce ( ( accum , key ) = > {
const value = params [ key ] ;
if ( value != null ) {
accum += ` & ${ key } = ${ value } `
}
return accum
} , "" ) ;
const apiKey = ( ( this . apiKey ) ? ` &apikey= ${ this . apiKey } ` : "" ) ;
return ` ${ this . baseUrl } /api?module= ${ module } ${ query } ${ apiKey } ` ;
2020-05-04 00:32:16 +03:00
}
2019-05-15 01:25:46 +03:00
2021-05-07 07:31:15 +03:00
getPostUrl ( ) : string {
return ` ${ this . baseUrl } /api ` ;
}
2019-05-15 01:25:46 +03:00
2021-05-07 07:31:15 +03:00
getPostData ( module : string , params : Record < string , any > ) : Record < string , any > {
params . module = module ;
params . apikey = this . apiKey ;
return params ;
}
2019-05-15 01:25:46 +03:00
2021-05-07 07:31:15 +03:00
async fetch ( module : string , params : Record < string , any > , post? : boolean ) : Promise < any > {
const url = ( post ? this . getPostUrl ( ) : this . getUrl ( module , params ) ) ;
const payload = ( post ? this . getPostData ( module , params ) : null ) ;
const procFunc = ( module === "proxy" ) ? getJsonResult : getResult ;
2019-08-01 23:13:35 +03:00
2021-05-07 07:31:15 +03:00
this . emit ( "debug" , {
action : "request" ,
request : url ,
provider : this
} ) ;
2020-07-14 09:26:45 +03:00
2021-05-07 07:31:15 +03:00
const connection : ConnectionInfo = {
url : url ,
throttleSlotInterval : 1000 ,
throttleCallback : ( attempt : number , url : string ) = > {
if ( this . isCommunityResource ( ) ) {
showThrottleMessage ( ) ;
2020-07-14 09:26:45 +03:00
}
2021-05-07 07:31:15 +03:00
return Promise . resolve ( true ) ;
2020-10-23 02:46:52 +03:00
}
2021-05-07 07:31:15 +03:00
} ;
2020-10-23 02:46:52 +03:00
2021-05-07 07:31:15 +03:00
let payloadStr : string = null ;
if ( payload ) {
connection . headers = { "content-type" : "application/x-www-form-urlencoded; charset=UTF-8" } ;
payloadStr = Object . keys ( payload ) . map ( ( key ) = > {
return ` ${ key } = ${ payload [ key ] } `
} ) . join ( "&" ) ;
}
2019-11-01 17:51:08 +03:00
2021-05-07 07:31:15 +03:00
const result = await fetchJson ( connection , payloadStr , procFunc || getJsonResult ) ;
2019-11-01 17:51:08 +03:00
2021-05-07 07:31:15 +03:00
this . emit ( "debug" , {
action : "response" ,
request : url ,
response : deepCopy ( result ) ,
provider : this
} ) ;
return result ;
}
async detectNetwork ( ) : Promise < Network > {
return this . network ;
}
async perform ( method : string , params : any ) : Promise < any > {
2019-05-15 01:25:46 +03:00
switch ( method ) {
case "getBlockNumber" :
2021-05-07 07:31:15 +03:00
return this . fetch ( "proxy" , { action : "eth_blockNumber" } ) ;
2019-05-15 01:25:46 +03:00
case "getGasPrice" :
2021-05-07 07:31:15 +03:00
return this . fetch ( "proxy" , { action : "eth_gasPrice" } ) ;
2019-05-15 01:25:46 +03:00
case "getBalance" :
// Returns base-10 result
2021-05-07 07:31:15 +03:00
return this . fetch ( "account" , {
action : "balance" ,
address : params.address ,
tag : params.blockTag
} ) ;
2019-05-15 01:25:46 +03:00
case "getTransactionCount" :
2021-05-07 07:31:15 +03:00
return this . fetch ( "proxy" , {
action : "eth_getTransactionCount" ,
address : params.address ,
tag : params.blockTag
} ) ;
2019-05-15 01:25:46 +03:00
case "getCode" :
2021-05-07 07:31:15 +03:00
return this . fetch ( "proxy" , {
action : "eth_getCode" ,
address : params.address ,
tag : params.blockTag
} ) ;
2019-05-15 01:25:46 +03:00
case "getStorageAt" :
2021-05-07 07:31:15 +03:00
return this . fetch ( "proxy" , {
action : "eth_getStorageAt" ,
address : params.address ,
position : params.position ,
tag : params.blockTag
} ) ;
2019-05-15 01:25:46 +03:00
case "sendTransaction" :
2021-05-07 07:31:15 +03:00
return this . fetch ( "proxy" , {
2020-10-23 02:46:52 +03:00
action : "eth_sendRawTransaction" ,
2021-05-07 07:31:15 +03:00
hex : params.signedTransaction
} , true ) . catch ( ( error ) = > {
2020-09-16 09:19:28 +03:00
return checkError ( "sendTransaction" , error , params . signedTransaction ) ;
2019-05-15 01:25:46 +03:00
} ) ;
case "getBlock" :
if ( params . blockTag ) {
2021-05-07 07:31:15 +03:00
return this . fetch ( "proxy" , {
action : "eth_getBlockByNumber" ,
tag : params.blockTag ,
boolean : ( params . includeTransactions ? "true" : "false" )
} ) ;
2019-05-15 01:25:46 +03:00
}
2020-04-22 09:42:25 +03:00
throw new Error ( "getBlock by blockHash not implemented" ) ;
2019-05-15 01:25:46 +03:00
case "getTransaction" :
2021-05-07 07:31:15 +03:00
return this . fetch ( "proxy" , {
action : "eth_getTransactionByHash" ,
txhash : params.transactionHash
} ) ;
2019-05-15 01:25:46 +03:00
case "getTransactionReceipt" :
2021-05-07 07:31:15 +03:00
return this . fetch ( "proxy" , {
action : "eth_getTransactionReceipt" ,
txhash : params.transactionHash
} ) ;
2019-05-15 01:25:46 +03:00
case "call" : {
if ( params . blockTag !== "latest" ) {
throw new Error ( "EtherscanProvider does not support blockTag for call" ) ;
}
2020-10-23 02:46:52 +03:00
const postData = getTransactionPostData ( params . transaction ) ;
postData . module = "proxy" ;
postData . action = "eth_call" ;
2020-09-11 08:10:15 +03:00
try {
2021-05-07 07:31:15 +03:00
return await this . fetch ( "proxy" , postData , true ) ;
2020-09-11 08:10:15 +03:00
} catch ( error ) {
2020-09-16 09:19:28 +03:00
return checkError ( "call" , error , params . transaction ) ;
2020-09-11 08:10:15 +03:00
}
2019-05-15 01:25:46 +03:00
}
case "estimateGas" : {
2020-10-23 02:46:52 +03:00
const postData = getTransactionPostData ( params . transaction ) ;
postData . module = "proxy" ;
postData . action = "eth_estimateGas" ;
2020-09-11 08:10:15 +03:00
try {
2021-05-07 07:31:15 +03:00
return await this . fetch ( "proxy" , postData , true ) ;
2020-09-11 08:10:15 +03:00
} catch ( error ) {
2020-09-16 09:19:28 +03:00
return checkError ( "estimateGas" , error , params . transaction ) ;
2020-09-11 08:10:15 +03:00
}
2019-05-15 01:25:46 +03:00
}
2019-11-01 17:51:08 +03:00
case "getLogs" : {
2021-05-07 07:31:15 +03:00
const args : Record < string , any > = { action : "getLogs" }
2019-05-15 01:25:46 +03:00
2019-11-01 17:51:08 +03:00
if ( params . filter . fromBlock ) {
2021-05-07 07:31:15 +03:00
args . fromBlock = checkLogTag ( params . filter . fromBlock ) ;
2019-11-01 17:51:08 +03:00
}
2019-05-15 01:25:46 +03:00
2019-11-01 17:51:08 +03:00
if ( params . filter . toBlock ) {
2021-05-07 07:31:15 +03:00
args . toBlock = checkLogTag ( params . filter . toBlock ) ;
2019-11-01 17:51:08 +03:00
}
if ( params . filter . address ) {
2021-05-07 07:31:15 +03:00
args . address = params . filter . address ;
2019-11-01 17:51:08 +03:00
}
// @TODO: We can handle slightly more complicated logs using the logs API
if ( params . filter . topics && params . filter . topics . length > 0 ) {
if ( params . filter . topics . length > 1 ) {
logger . throwError ( "unsupported topic count" , Logger . errors . UNSUPPORTED_OPERATION , { topics : params.filter.topics } ) ;
2019-05-15 01:25:46 +03:00
}
2019-11-01 17:51:08 +03:00
if ( params . filter . topics . length === 1 ) {
const topic0 = params . filter . topics [ 0 ] ;
2019-05-15 01:25:46 +03:00
if ( typeof ( topic0 ) !== "string" || topic0 . length !== 66 ) {
2019-11-01 17:51:08 +03:00
logger . throwError ( "unsupported topic format" , Logger . errors . UNSUPPORTED_OPERATION , { topic0 : topic0 } ) ;
2019-05-15 01:25:46 +03:00
}
2021-05-07 07:31:15 +03:00
args . topic0 = topic0 ;
2019-05-15 01:25:46 +03:00
}
}
2021-05-07 07:31:15 +03:00
const logs : Array < any > = await this . fetch ( "logs" , args ) ;
2019-11-01 17:51:08 +03:00
// Cache txHash => blockHash
2020-10-23 04:03:51 +03:00
let blocks : { [ tag : string ] : string } = { } ;
2019-11-01 17:51:08 +03:00
// Add any missing blockHash to the logs
for ( let i = 0 ; i < logs . length ; i ++ ) {
const log = logs [ i ] ;
if ( log . blockHash != null ) { continue ; }
2020-10-23 04:03:51 +03:00
if ( blocks [ log . blockNumber ] == null ) {
const block = await this . getBlock ( log . blockNumber ) ;
if ( block ) {
blocks [ log . blockNumber ] = block . hash ;
2019-11-01 17:51:08 +03:00
}
}
2020-10-23 04:03:51 +03:00
log . blockHash = blocks [ log . blockNumber ] ;
2019-11-01 17:51:08 +03:00
}
return logs ;
}
2019-05-15 01:25:46 +03:00
case "getEtherPrice" :
2019-11-01 17:51:08 +03:00
if ( this . network . name !== "homestead" ) { return 0.0 ; }
2021-05-07 07:31:15 +03:00
return parseFloat ( ( await this . fetch ( "stats" , { action : "ethprice" } ) ) . ethusd ) ;
2019-05-15 01:25:46 +03:00
default :
break ;
}
return super . perform ( method , params ) ;
}
2021-05-07 07:31:15 +03:00
// Note: The `page` page parameter only allows pagination within the
2021-10-04 18:46:24 +03:00
// 10,000 window available without a page and offset parameter
2021-05-07 07:31:15 +03:00
// Error: Result window is too large, PageNo x Offset size must
// be less than or equal to 10000
async getHistory ( addressOrName : string | Promise < string > , startBlock? : BlockTag , endBlock? : BlockTag ) : Promise < Array < TransactionResponse > > {
const params = {
action : "txlist" ,
address : ( await this . resolveName ( addressOrName ) ) ,
startblock : ( ( startBlock == null ) ? 0 : startBlock ) ,
endblock : ( ( endBlock == null ) ? 99999999 : endBlock ) ,
sort : "asc"
} ;
2019-05-15 01:25:46 +03:00
2021-05-07 07:31:15 +03:00
const result = await this . fetch ( "account" , params ) ;
2019-05-15 01:25:46 +03:00
2021-05-07 07:31:15 +03:00
return result . map ( ( tx : any ) = > {
[ "contractAddress" , "to" ] . forEach ( function ( key ) {
if ( tx [ key ] == "" ) { delete tx [ key ] ; }
2019-08-01 23:13:35 +03:00
} ) ;
2021-05-07 07:31:15 +03:00
if ( tx . creates == null && tx . contractAddress != null ) {
tx . creates = tx . contractAddress ;
2020-07-14 09:26:45 +03:00
}
2021-05-07 07:31:15 +03:00
const item = this . formatter . transactionResponse ( tx ) ;
if ( tx . timeStamp ) { item . timestamp = parseInt ( tx . timeStamp ) ; }
return item ;
2019-05-15 01:25:46 +03:00
} ) ;
}
2020-10-08 00:28:18 +03:00
isCommunityResource ( ) : boolean {
return ( this . apiKey === defaultApiKey ) ;
}
2019-05-15 01:25:46 +03:00
}