'use strict'; var Provider = require('./provider.js'); var utils = (function() { var convert = require('../utils/convert.js'); return { defineProperty: require('../utils/properties.js').defineProperty, hexlify: convert.hexlify, hexStripZeros: convert.hexStripZeros, }; })(); function getTransactionString(transaction) { var result = []; for (var key in transaction) { if (transaction[key] == null) { continue; } var value = utils.hexlify(transaction[key]); if ({ gasLimit: true, gasPrice: true, nonce: true, value: true }[key]) { value = utils.hexStripZeros(value); } result.push(key + '=' + value); } return result.join('&'); } function EtherscanProvider(network, apiKey) { Provider.call(this, network); var baseUrl = null; switch(this.name) { case 'homestead': baseUrl = 'https://api.etherscan.io'; break; case 'ropsten': baseUrl = 'https://ropsten.etherscan.io'; break; case 'rinkeby': baseUrl = 'https://rinkeby.etherscan.io'; break; case 'kovan': baseUrl = 'https://kovan.etherscan.io'; break; default: throw new Error('unsupported network'); } utils.defineProperty(this, 'baseUrl', baseUrl); utils.defineProperty(this, 'apiKey', apiKey || null); } Provider.inherits(EtherscanProvider); utils.defineProperty(EtherscanProvider.prototype, '_call', function() { }); utils.defineProperty(EtherscanProvider.prototype, '_callProxy', function() { }); function getResult(result) { // getLogs has weird success responses if (result.status == 0 && result.message === 'No records found') { return result.result; } if (result.status != 1 || result.message != 'OK') { var error = new Error('invalid response'); error.result = JSON.stringify(result); throw error; } return result.result; } function getJsonResult(result) { if (result.jsonrpc != '2.0') { var error = new Error('invalid response'); error.result = JSON.stringify(result); throw error; } if (result.error) { var error = new Error(result.error.message || 'unknown error'); if (result.error.code) { error.code = result.error.code; } if (result.error.data) { error.data = result.error.data; } throw error; } return result.result; } function checkLogTag(blockTag) { if (blockTag === 'pending') { throw new Error('pending not supported'); } if (blockTag === 'latest') { return blockTag; } return parseInt(blockTag.substring(2), 16); } utils.defineProperty(EtherscanProvider.prototype, 'perform', function(method, params) { if (!params) { params = {}; } var url = this.baseUrl; var apiKey = ''; if (this.apiKey) { apiKey += '&apikey=' + this.apiKey; } switch (method) { case 'getBlockNumber': url += '/api?module=proxy&action=eth_blockNumber' + apiKey; return Provider.fetchJSON(url, null, getJsonResult); case 'getGasPrice': url += '/api?module=proxy&action=eth_gasPrice' + apiKey; return Provider.fetchJSON(url, null, getJsonResult); case 'getBalance': // Returns base-10 result url += '/api?module=account&action=balance&address=' + params.address; url += '&tag=' + params.blockTag + apiKey; return Provider.fetchJSON(url, null, getResult); case 'getTransactionCount': url += '/api?module=proxy&action=eth_getTransactionCount&address=' + params.address; url += '&tag=' + params.blockTag + apiKey; return Provider.fetchJSON(url, null, getJsonResult); case 'getCode': url += '/api?module=proxy&action=eth_getCode&address=' + params.address; url += '&tag=' + params.blockTag + apiKey; return Provider.fetchJSON(url, null, getJsonResult); case 'getStorageAt': url += '/api?module=proxy&action=eth_getStorageAt&address=' + params.address; url += '&position=' + params.position; url += '&tag=' + params.blockTag + apiKey; return Provider.fetchJSON(url, null, getJsonResult); case 'sendTransaction': url += '/api?module=proxy&action=eth_sendRawTransaction&hex=' + params.signedTransaction; url += apiKey; return Provider.fetchJSON(url, null, getJsonResult); case 'getBlock': if (params.blockTag) { url += '/api?module=proxy&action=eth_getBlockByNumber&tag=' + params.blockTag; url += '&boolean=false'; url += apiKey; return Provider.fetchJSON(url, null, getJsonResult); } throw new Error('getBlock by blockHash not implmeneted'); case 'getTransaction': url += '/api?module=proxy&action=eth_getTransactionByHash&txhash=' + params.transactionHash; url += apiKey; return Provider.fetchJSON(url, null, getJsonResult); case 'getTransactionReceipt': url += '/api?module=proxy&action=eth_getTransactionReceipt&txhash=' + params.transactionHash; url += apiKey; return Provider.fetchJSON(url, null, getJsonResult); case 'call': var transaction = getTransactionString(params.transaction); if (transaction) { transaction = '&' + transaction; } url += '/api?module=proxy&action=eth_call' + transaction; url += apiKey; return Provider.fetchJSON(url, null, getJsonResult); case 'estimateGas': var transaction = getTransactionString(params.transaction); if (transaction) { transaction = '&' + transaction; } url += '/api?module=proxy&action=eth_estimateGas&' + transaction; url += apiKey; return Provider.fetchJSON(url, null, getJsonResult); case 'getLogs': url += '/api?module=logs&action=getLogs'; try { if (params.filter.fromBlock) { url += '&fromBlock=' + checkLogTag(params.filter.fromBlock); } 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) { throw new Error('unsupported topic format'); } var topic0 = params.filter.topics[0]; if (typeof(topic0) !== 'string' || topic0.length !== 66) { throw new Error('unsupported topic0 format'); } url += '&topic0=' + topic0; } } catch (error) { return Promise.reject(error); } url += apiKey; var self = this; return Provider.fetchJSON(url, null, getResult).then(function(logs) { var txs = {}; var seq = Promise.resolve(); logs.forEach(function(log) { seq = seq.then(function() { if (log.blockHash != null) { return; } log.blockHash = txs[log.transactionHash]; if (log.blockHash == null) { return self.getTransaction(log.transactionHash).then(function(tx) { txs[log.transactionHash] = tx.blockHash; log.blockHash = tx.blockHash; }); } }); }) return seq.then(function() { return logs; }); }); case 'getEtherPrice': if (this.name !== 'homestead') { return Promise.resolve(0.0); } url += '/api?module=stats&action=ethprice'; url += apiKey; return Provider.fetchJSON(url, null, getResult).then(function(result) { return parseFloat(result.ethusd); }); default: break; } return Promise.reject(new Error('not implemented - ' + method)); }); utils.defineProperty(EtherscanProvider.prototype, 'getHistory', function(addressOrName, startBlock, endBlock) { var url = this.baseUrl; var apiKey = ''; if (this.apiKey) { apiKey += '&apikey=' + this.apiKey; } if (startBlock == null) { startBlock = 0; } if (endBlock == null) { endBlock = 99999999; } return this.resolveName(addressOrName).then(function(address) { url += '/api?module=account&action=txlist&address=' + address; url += '&startblock=' + startBlock; url += '&endblock=' + endBlock; url += '&sort=asc'; return Provider.fetchJSON(url, null, getResult).then(function(result) { var output = []; result.forEach(function(tx) { ['contractAddress', 'to'].forEach(function(key) { if (tx[key] == '') { delete tx[key]; } }); if (tx.creates == null && tx.contractAddress != null) { tx.creates = tx.contractAddress; } output.push(Provider._formatters.checkTransactionResponse(tx)); }); return output; }); }); }); module.exports = EtherscanProvider;;