2019-05-14 18:25:46 -04:00
"use strict" ;
import { BlockTag , TransactionRequest , TransactionResponse } from "@ethersproject/abstract-provider" ;
import { hexlify , hexValue } from "@ethersproject/bytes" ;
2020-05-03 17:32:16 -04:00
import { Network , Networkish } from "@ethersproject/networks" ;
2019-08-01 16:13:35 -04:00
import { deepCopy , defineReadOnly } from "@ethersproject/properties" ;
2019-05-14 18:25:46 -04:00
import { fetchJson } from "@ethersproject/web" ;
2020-07-14 02:26:45 -04:00
import { showThrottleMessage } from "./formatter" ;
2019-08-01 18:04:06 -04:00
import { Logger } from "@ethersproject/logger" ;
import { version } from "./_version" ;
const logger = new Logger ( version ) ;
2019-05-14 18:25:46 -04:00
import { BaseProvider } from "./base-provider" ;
// The transaction has already been sanitized by the calls in Provider
function getTransactionString ( transaction : TransactionRequest ) : string {
2019-11-01 23:51:08 +09:00
const result = [ ] ;
2019-05-14 18:25:46 -04:00
for ( let key in transaction ) {
if ( ( < any > transaction ) [ key ] == null ) { continue ; }
let value = hexlify ( ( < any > transaction ) [ key ] ) ;
if ( ( < any > { gasLimit : true , gasPrice : true , nonce : true , value : true } ) [ key ] ) {
value = hexValue ( value ) ;
}
result . push ( key + "=" + value ) ;
}
return result . join ( "&" ) ;
}
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 23:51:08 +09:00
const error : any = new Error ( "invalid response" ) ;
2019-05-14 18:25:46 -04:00
error . result = JSON . stringify ( result ) ;
2020-07-14 02:26:45 -04:00
if ( ( result . result || "" ) . toLowerCase ( ) . indexOf ( "rate limit" ) >= 0 ) {
error . throttleRetry = true ;
}
2019-05-14 18:25:46 -04:00
throw error ;
}
return result . result ;
}
function getJsonResult ( result : { jsonrpc : string , result? : any , error ? : { code? : number , data? : any , message? : string } } ) : any {
2020-07-14 02:26:45 -04: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-14 18:25:46 -04:00
if ( result . jsonrpc != "2.0" ) {
// @TODO: not any
2019-11-01 23:51:08 +09:00
const error : any = new Error ( "invalid response" ) ;
2019-05-14 18:25:46 -04:00
error . result = JSON . stringify ( result ) ;
throw error ;
}
if ( result . error ) {
// @TODO: not any
2019-11-01 23:51:08 +09:00
const error : any = new Error ( result . error . message || "unknown error" ) ;
2019-05-14 18:25:46 -04: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-17 18:14:02 -05:00
const defaultApiKey = "9D13ZE7XSBTJ94N9BNJ2MA33VMAY2YPIRB" ;
2019-05-14 18:25:46 -04:00
export class EtherscanProvider extends BaseProvider {
readonly baseUrl : string ;
readonly apiKey : string ;
2020-07-14 02:26:45 -04:00
2019-05-14 18:25:46 -04:00
constructor ( network? : Networkish , apiKey? : string ) {
2019-08-01 18:04:06 -04:00
logger . checkNew ( new . target , EtherscanProvider ) ;
2019-05-14 18:25:46 -04:00
super ( network ) ;
let name = "invalid" ;
if ( this . network ) { name = this . network . name ; }
let baseUrl = null ;
switch ( name ) {
case "homestead" :
baseUrl = "https://api.etherscan.io" ;
break ;
case "ropsten" :
baseUrl = "https://api-ropsten.etherscan.io" ;
break ;
case "rinkeby" :
baseUrl = "https://api-rinkeby.etherscan.io" ;
break ;
case "kovan" :
baseUrl = "https://api-kovan.etherscan.io" ;
break ;
case "goerli" :
baseUrl = "https://api-goerli.etherscan.io" ;
break ;
default :
throw new Error ( "unsupported network" ) ;
}
defineReadOnly ( this , "baseUrl" , baseUrl ) ;
2020-02-17 18:14:02 -05:00
defineReadOnly ( this , "apiKey" , apiKey || defaultApiKey ) ;
2019-05-14 18:25:46 -04:00
}
2020-05-03 17:32:16 -04:00
async detectNetwork ( ) : Promise < Network > {
return this . network ;
}
2019-05-14 18:25:46 -04:00
2019-11-01 23:51:08 +09:00
async perform ( method : string , params : any ) : Promise < any > {
2019-05-14 18:25:46 -04:00
let url = this . baseUrl ;
let apiKey = "" ;
if ( this . apiKey ) { apiKey += "&apikey=" + this . apiKey ; }
2019-11-01 23:51:08 +09:00
const get = async ( url : string , procFunc ? : ( value : any ) = > any ) : Promise < any > = > {
2019-08-01 16:13:35 -04:00
this . emit ( "debug" , {
action : "request" ,
request : url ,
provider : this
} ) ;
2020-07-14 02:26:45 -04:00
const connection = {
url : url ,
2020-07-16 05:29:33 -04:00
throttleSlotInterval : 1000 ,
2020-07-14 02:26:45 -04:00
throttleCallback : ( attempt : number , url : string ) = > {
if ( this . apiKey === defaultApiKey ) {
showThrottleMessage ( ) ;
}
return Promise . resolve ( true ) ;
}
} ;
const result = await fetchJson ( connection , null , procFunc || getJsonResult ) ;
2019-11-01 23:51:08 +09:00
this . emit ( "debug" , {
action : "response" ,
request : url ,
response : deepCopy ( result ) ,
provider : this
2019-05-14 18:25:46 -04:00
} ) ;
2019-11-01 23:51:08 +09:00
return result ;
2019-05-14 18:25:46 -04:00
} ;
switch ( method ) {
case "getBlockNumber" :
url += "/api?module=proxy&action=eth_blockNumber" + apiKey ;
return get ( url ) ;
case "getGasPrice" :
url += "/api?module=proxy&action=eth_gasPrice" + apiKey ;
return get ( url ) ;
case "getBalance" :
// Returns base-10 result
url += "/api?module=account&action=balance&address=" + params . address ;
url += "&tag=" + params . blockTag + apiKey ;
return get ( url , getResult ) ;
case "getTransactionCount" :
url += "/api?module=proxy&action=eth_getTransactionCount&address=" + params . address ;
url += "&tag=" + params . blockTag + apiKey ;
return get ( url ) ;
case "getCode" :
url += "/api?module=proxy&action=eth_getCode&address=" + params . address ;
url += "&tag=" + params . blockTag + apiKey ;
2020-07-14 02:26:45 -04:00
return get ( url ) ;
2019-05-14 18:25:46 -04:00
case "getStorageAt" :
url += "/api?module=proxy&action=eth_getStorageAt&address=" + params . address ;
url += "&position=" + params . position ;
url += "&tag=" + params . blockTag + apiKey ;
2020-07-14 02:26:45 -04:00
return get ( url ) ;
2019-05-14 18:25:46 -04:00
case "sendTransaction" :
url += "/api?module=proxy&action=eth_sendRawTransaction&hex=" + params . signedTransaction ;
url += apiKey ;
return get ( url ) . catch ( ( error ) = > {
if ( error . responseText ) {
// "Insufficient funds. The account you tried to send transaction from does not have enough funds. Required 21464000000000 and got: 0"
if ( error . responseText . toLowerCase ( ) . indexOf ( "insufficient funds" ) >= 0 ) {
2019-08-01 18:04:06 -04:00
logger . throwError ( "insufficient funds" , Logger . errors . INSUFFICIENT_FUNDS , { } ) ;
2019-05-14 18:25:46 -04:00
}
// "Transaction with the same hash was already imported."
if ( error . responseText . indexOf ( "same hash was already imported" ) >= 0 ) {
2019-08-01 18:04:06 -04:00
logger . throwError ( "nonce has already been used" , Logger . errors . NONCE_EXPIRED , { } ) ;
2019-05-14 18:25:46 -04:00
}
// "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 ( error . responseText . indexOf ( "another transaction with same nonce" ) >= 0 ) {
2019-08-01 18:04:06 -04:00
logger . throwError ( "replacement fee too low" , Logger . errors . REPLACEMENT_UNDERPRICED , { } ) ;
2019-05-14 18:25:46 -04:00
}
}
throw error ;
} ) ;
case "getBlock" :
if ( params . blockTag ) {
url += "/api?module=proxy&action=eth_getBlockByNumber&tag=" + params . blockTag ;
if ( params . includeTransactions ) {
url += "&boolean=true" ;
} else {
url += "&boolean=false" ;
}
url += apiKey ;
return get ( url ) ;
}
2020-04-22 02:42:25 -04:00
throw new Error ( "getBlock by blockHash not implemented" ) ;
2019-05-14 18:25:46 -04:00
case "getTransaction" :
url += "/api?module=proxy&action=eth_getTransactionByHash&txhash=" + params . transactionHash ;
url += apiKey ;
return get ( url ) ;
case "getTransactionReceipt" :
url += "/api?module=proxy&action=eth_getTransactionReceipt&txhash=" + params . transactionHash ;
url += apiKey ;
return get ( url ) ;
case "call" : {
let transaction = getTransactionString ( params . transaction ) ;
if ( transaction ) { transaction = "&" + transaction ; }
url += "/api?module=proxy&action=eth_call" + transaction ;
//url += "&tag=" + params.blockTag + apiKey;
if ( params . blockTag !== "latest" ) {
throw new Error ( "EtherscanProvider does not support blockTag for call" ) ;
}
url += apiKey ;
return get ( url ) ;
}
case "estimateGas" : {
let transaction = getTransactionString ( params . transaction ) ;
if ( transaction ) { transaction = "&" + transaction ; }
url += "/api?module=proxy&action=eth_estimateGas&" + transaction ;
url += apiKey ;
return get ( url ) ;
}
2019-11-01 23:51:08 +09:00
case "getLogs" : {
2019-05-14 18:25:46 -04:00
url += "/api?module=logs&action=getLogs" ;
2019-11-01 23:51:08 +09:00
if ( params . filter . fromBlock ) {
url += "&fromBlock=" + checkLogTag ( params . filter . fromBlock ) ;
}
2019-05-14 18:25:46 -04:00
2019-11-01 23:51:08 +09:00
if ( params . filter . toBlock ) {
url += "&toBlock=" + checkLogTag ( params . filter . toBlock ) ;
}
if ( params . filter . address ) {
url += "&address=" + params . filter . address ;
}
// @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-14 18:25:46 -04:00
}
2019-11-01 23:51:08 +09:00
if ( params . filter . topics . length === 1 ) {
const topic0 = params . filter . topics [ 0 ] ;
2019-05-14 18:25:46 -04:00
if ( typeof ( topic0 ) !== "string" || topic0 . length !== 66 ) {
2019-11-01 23:51:08 +09:00
logger . throwError ( "unsupported topic format" , Logger . errors . UNSUPPORTED_OPERATION , { topic0 : topic0 } ) ;
2019-05-14 18:25:46 -04:00
}
url += "&topic0=" + topic0 ;
}
}
url += apiKey ;
2019-11-01 23:51:08 +09:00
const logs : Array < any > = await get ( url , getResult ) ;
// Cache txHash => blockHash
let txs : { [ hash : string ] : string } = { } ;
// Add any missing blockHash to the logs
for ( let i = 0 ; i < logs . length ; i ++ ) {
const log = logs [ i ] ;
if ( log . blockHash != null ) { continue ; }
if ( txs [ log . transactionHash ] == null ) {
const tx = await this . getTransaction ( log . transactionHash ) ;
if ( tx ) {
txs [ log . transactionHash ] = tx . blockHash
}
}
log . blockHash = txs [ log . transactionHash ] ;
}
return logs ;
}
2019-05-14 18:25:46 -04:00
case "getEtherPrice" :
2019-11-01 23:51:08 +09:00
if ( this . network . name !== "homestead" ) { return 0.0 ; }
2019-05-14 18:25:46 -04:00
url += "/api?module=stats&action=ethprice" ;
url += apiKey ;
2020-03-31 23:17:10 -04:00
return parseFloat ( ( await get ( url , getResult ) ) . ethusd ) ;
2019-05-14 18:25:46 -04:00
default :
break ;
}
return super . perform ( method , params ) ;
}
// @TODO: Allow startBlock and endBlock to be Promises
getHistory ( addressOrName : string | Promise < string > , startBlock? : BlockTag , endBlock? : BlockTag ) : Promise < Array < TransactionResponse > > {
let url = this . baseUrl ;
let apiKey = "" ;
if ( this . apiKey ) { apiKey += "&apikey=" + this . apiKey ; }
if ( startBlock == null ) { startBlock = 0 ; }
if ( endBlock == null ) { endBlock = 99999999 ; }
return this . resolveName ( addressOrName ) . then ( ( address ) = > {
url += "/api?module=account&action=txlist&address=" + address ;
url += "&startblock=" + startBlock ;
url += "&endblock=" + endBlock ;
url += "&sort=asc" + apiKey ;
2019-08-01 16:13:35 -04:00
this . emit ( "debug" , {
action : "request" ,
request : url ,
provider : this
} ) ;
2020-07-14 02:26:45 -04:00
const connection = {
url : url ,
2020-07-16 05:29:33 -04:00
throttleSlotInterval : 1000 ,
2020-07-14 02:26:45 -04:00
throttleCallback : ( attempt : number , url : string ) = > {
if ( this . apiKey === defaultApiKey ) {
showThrottleMessage ( ) ;
}
return Promise . resolve ( true ) ;
}
}
return fetchJson ( connection , null , getResult ) . then ( ( result : Array < any > ) = > {
2019-05-14 18:25:46 -04:00
this . emit ( "debug" , {
2019-08-01 16:13:35 -04:00
action : "response" ,
2019-05-14 18:25:46 -04:00
request : url ,
2019-08-01 16:13:35 -04:00
response : deepCopy ( result ) ,
2019-05-14 18:25:46 -04:00
provider : this
} ) ;
2019-08-01 16:13:35 -04:00
2019-05-14 18:25:46 -04:00
let output : Array < TransactionResponse > = [ ] ;
result . forEach ( ( tx ) = > {
[ "contractAddress" , "to" ] . forEach ( function ( key ) {
if ( tx [ key ] == "" ) { delete tx [ key ] ; }
} ) ;
if ( tx . creates == null && tx . contractAddress != null ) {
tx . creates = tx . contractAddress ;
}
let item = this . formatter . transactionResponse ( tx ) ;
if ( tx . timeStamp ) { item . timestamp = parseInt ( tx . timeStamp ) ; }
output . push ( item ) ;
} ) ;
return output ;
} ) ;
} ) ;
}
}