Skip to content
This repository has been archived by the owner on Apr 22, 2024. It is now read-only.

Etherscan upgrades #394

Closed
wants to merge 9 commits into from
172 changes: 116 additions & 56 deletions subproviders/etherscan.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,77 +25,99 @@
const xhr = process.browser ? require('xhr') : require('request')
const inherits = require('util').inherits
const Subprovider = require('./subprovider.js')
const MAINNET = 'mainnet'

module.exports = EtherscanProvider

inherits(EtherscanProvider, Subprovider)

function EtherscanProvider(opts) {
opts = opts || {}
this.network = opts.network || 'api'
this.network = opts.network || MAINNET
this.apiKey = opts.apiKey || ''
this.proto = (opts.https || false) ? 'https' : 'http'
this.requests = [];
this.times = isNaN(opts.times) ? 4 : opts.times;
this.interval = isNaN(opts.interval) ? 1000 : opts.interval;
this.retryFailed = typeof opts.retryFailed === 'boolean' ? opts.retryFailed : true; // not built yet

setInterval(this.handleRequests, this.interval, this);

this.intervalId = setInterval(this.handleRequests, this.interval, this);
unref(this.intervalId);
}

EtherscanProvider.prototype.handleRequests = function(self){
if(self.requests.length == 0) return;

//console.log('Handling the next ' + self.times + ' of ' + self.requests.length + ' requests');

for(var requestIndex = 0; requestIndex < self.times; requestIndex++) {
var requestItem = self.requests.shift()

if(typeof requestItem !== 'undefined')
handlePayload(requestItem.proto, requestItem.network, requestItem.payload, requestItem.next, requestItem.end)
}
self._handleRequests(self)
if(self.requests.length == 0) {
unref(self.intervalId);
}
}

EtherscanProvider.prototype._handleRequests = function(self){
if(self.requests.length == 0) return;

//console.log('Handling the next ' + self.times + ' of ' + self.requests.length + ' requests');

for(var requestIndex = 0; requestIndex < self.times; requestIndex++) {
var requestItem = self.requests.shift()

if(typeof requestItem !== 'undefined')
handlePayload(self.apiKey, requestItem.proto, requestItem.network, requestItem.payload, requestItem.next, requestItem.end)
}
}

EtherscanProvider.prototype.handleRequest = function(payload, next, end){
end = normalizeCallback(end)
var requestObject = {proto: this.proto, network: this.network, payload: payload, next: next, end: end},
self = this;
self = this;

if(this.retryFailed)
requestObject.end = function(err, result){
if(err === '403 - Forbidden: Access is denied.')
self.requests.push(requestObject);
else
end(err, result);
};
requestObject.end = function(err, result){
if(err === '403 - Forbidden: Access is denied.')
self.requests.push(requestObject);
else
end(err, result);
};

this.requests.push(requestObject);
ref(this.intervalId);
}

function handlePayload(proto, network, payload, next, end){
function handlePayload(apiKey, proto, network, payload, next, end){
const params0 = payload.params[0]
switch(payload.method) {
case 'eth_blockNumber':
etherscanXHR(true, proto, network, 'proxy', 'eth_blockNumber', {}, end)
etherscanXHR(apiKey, true, proto, network, 'proxy', 'eth_blockNumber', {}, end)
return

case 'eth_estimateGas':
etherscanXHR(apiKey, true, proto, network, 'proxy', 'eth_estimateGas', pickNonNull({
to: params0.to,
value: params0.value,
gasPrice: params0.gasPrice,
gas: params0.gas
}), end)
return

case 'eth_getBlockByNumber':
etherscanXHR(true, proto, network, 'proxy', 'eth_getBlockByNumber', {
etherscanXHR(apiKey, true, proto, network, 'proxy', 'eth_getBlockByNumber', {
tag: payload.params[0],
boolean: payload.params[1] }, end)
return

case 'eth_getBlockTransactionCountByNumber':
etherscanXHR(true, proto, network, 'proxy', 'eth_getBlockTransactionCountByNumber', {
etherscanXHR(apiKey, true, proto, network, 'proxy', 'eth_getBlockTransactionCountByNumber', {
tag: payload.params[0]
}, end)
return

case 'eth_getTransactionByHash':
etherscanXHR(true, proto, network, 'proxy', 'eth_getTransactionByHash', {
etherscanXHR(apiKey, true, proto, network, 'proxy', 'eth_getTransactionByHash', {
txhash: payload.params[0]
}, end)
return

case 'eth_getBalance':
etherscanXHR(true, proto, network, 'account', 'balance', {
etherscanXHR(apiKey, true, proto, network, 'account', 'balance', {
address: payload.params[0],
tag: payload.params[1] }, end)
return
Expand All @@ -113,22 +135,25 @@ function handlePayload(proto, network, payload, next, end){

const params = {}
for (let i = 0, l = Math.min(payload.params.length, props.length); i < l; i++) {
params[props[i]] = payload.params[i]
const value = payload.params[i]
if (value !== undefined) {
params[props[i]] = value
}
}

etherscanXHR(true, proto, network, 'account', 'txlist', params, end)
etherscanXHR(apiKey, true, proto, network, 'account', 'txlist', params, end)
})()

case 'eth_call':
etherscanXHR(true, proto, network, 'proxy', 'eth_call', payload.params[0], end)
etherscanXHR(apiKey, true, proto, network, 'proxy', 'eth_call', payload.params[0], end)
return

case 'eth_sendRawTransaction':
etherscanXHR(false, proto, network, 'proxy', 'eth_sendRawTransaction', { hex: payload.params[0] }, end)
etherscanXHR(apiKey, false, proto, network, 'proxy', 'eth_sendRawTransaction', { hex: payload.params[0] }, end)
return

case 'eth_getTransactionReceipt':
etherscanXHR(true, proto, network, 'proxy', 'eth_getTransactionReceipt', { txhash: payload.params[0] }, end)
etherscanXHR(apiKey, true, proto, network, 'proxy', 'eth_getTransactionReceipt', { txhash: payload.params[0] }, end)
return

// note !! this does not support topic filtering yet, it will return all block logs
Expand All @@ -138,13 +163,13 @@ function handlePayload(proto, network, payload, next, end){
txProcessed = 0,
logs = [];

etherscanXHR(true, proto, network, 'proxy', 'eth_getBlockByNumber', {
etherscanXHR(apiKey, true, proto, network, 'proxy', 'eth_getBlockByNumber', {
tag: payloadObject.toBlock,
boolean: payload.params[1] }, function(err, blockResult) {
if(err) return end(err);

for(var transaction in blockResult.transactions){
etherscanXHR(true, proto, network, 'proxy', 'eth_getTransactionReceipt', { txhash: transaction.hash }, function(err, receiptResult) {
etherscanXHR(apiKey, true, proto, network, 'proxy', 'eth_getTransactionReceipt', { txhash: transaction.hash }, function(err, receiptResult) {
if(!err) logs.concat(receiptResult.logs);
txProcessed += 1;
if(txProcessed === blockResult.transactions.length) end(null, logs)
Expand All @@ -154,21 +179,21 @@ function handlePayload(proto, network, payload, next, end){
})()

case 'eth_getTransactionCount':
etherscanXHR(true, proto, network, 'proxy', 'eth_getTransactionCount', {
etherscanXHR(apiKey, true, proto, network, 'proxy', 'eth_getTransactionCount', {
address: payload.params[0],
tag: payload.params[1]
}, end)
return

case 'eth_getCode':
etherscanXHR(true, proto, network, 'proxy', 'eth_getCode', {
etherscanXHR(apiKey, true, proto, network, 'proxy', 'eth_getCode', {
address: payload.params[0],
tag: payload.params[1]
}, end)
return

case 'eth_getStorageAt':
etherscanXHR(true, proto, network, 'proxy', 'eth_getStorageAt', {
etherscanXHR(apiKey, true, proto, network, 'proxy', 'eth_getStorageAt', {
address: payload.params[0],
position: payload.params[1],
tag: payload.params[2]
Expand All @@ -187,9 +212,10 @@ function toQueryString(params) {
}).join('&')
}

function etherscanXHR(useGetMethod, proto, network, module, action, params, end) {
var uri = proto + '://' + network + '.etherscan.io/api?' + toQueryString({ module: module, action: action }) + '&' + toQueryString(params)

function etherscanXHR(apiKey, useGetMethod, proto, network, module, action, params, end) {
const subdomain = network === MAINNET ? 'api' : `api-${network}`
const qs = toQueryString({ module: module, action: action, apikey: apiKey }) + '&' + toQueryString(params)
const uri = `${proto}://${subdomain}.etherscan.io/api?${qs}`
xhr({
uri: uri,
method: useGetMethod ? 'GET' : 'POST',
Expand All @@ -202,19 +228,20 @@ function etherscanXHR(useGetMethod, proto, network, module, action, params, end)
// console.log('[etherscan] response: ', err)

if (err) return end(err)

/*console.log('[etherscan request]'
+ ' method: ' + useGetMethod
+ ' proto: ' + proto
+ ' network: ' + network
+ ' module: ' + module
+ ' action: ' + action
+ ' params: ' + params
+ ' return body: ' + body);*/

if(body.indexOf('403 - Forbidden: Access is denied.') > -1)
return end('403 - Forbidden: Access is denied.')


if (res.statusCode > 300) {
return end(res.statusMessage || body)
}

/*console.log('[etherscan request]'
+ ' method: ' + useGetMethod
+ ' proto: ' + proto
+ ' network: ' + network
+ ' module: ' + module
+ ' action: ' + action
+ ' params: ' + params
+ ' return body: ' + body);*/

var data
try {
data = JSON.parse(body)
Expand All @@ -228,7 +255,7 @@ function etherscanXHR(useGetMethod, proto, network, module, action, params, end)
// NOTE: or use id === -1? (id=1 is 'success')
if ((module === 'proxy') && data.error) {
// Maybe send back the code too?
return end(data.error.message)
return end(data.error.message || data.error)
}

// NOTE: or data.status !== 1?
Expand All @@ -239,3 +266,36 @@ function etherscanXHR(useGetMethod, proto, network, module, action, params, end)
end(null, data.result)
})
}

function unref (timeout) {
if (timeout.unref) timeout.unref();
}

function ref (timeout) {
if (timeout.ref) timeout.ref();
}

function pickNonNull (obj) {
const defined = {}
for (let key in obj) {
if (obj[key] != null) {
defined[key] = obj[key]
}
}

return defined
}

function normalizeError (err) {
if (err instanceof Error) return err

return new Error("" + err)
}

function normalizeCallback (cb) {
return function (err, result) {
if (err) err = normalizeError(err)

cb(err, result)
}
}