diff --git a/subproviders/etherscan.js b/subproviders/etherscan.js index c6537887..33ee994b 100644 --- a/subproviders/etherscan.js +++ b/subproviders/etherscan.js @@ -25,6 +25,7 @@ const xhr = process.browser ? require('xhr') : require('request') const inherits = require('util').inherits const Subprovider = require('./subprovider.js') +const MAINNET = 'mainnet' module.exports = EtherscanProvider @@ -32,70 +33,91 @@ 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 @@ -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 @@ -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) @@ -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] @@ -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', @@ -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) @@ -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? @@ -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) + } +}