From 0db4b352b6c86f8e9eded9640d072721680377c6 Mon Sep 17 00:00:00 2001 From: ricmoo Date: Sat, 25 Feb 2017 01:23:48 -0500 Subject: [PATCH] Added split up providers. --- providers/browser-xmlhttprequest.js | 8 + providers/etherscan-provider.js | 180 ++++++++ providers/fallback-provider.js | 62 +++ providers/index.js | 337 +------------- providers/infura-provider.js | 24 + providers/json-rpc-provider.js | 427 ++++++++++++++++++ providers/package.json | 29 ++ providers/provider.js | 668 ++++++++++++++++++++++++++++ 8 files changed, 1410 insertions(+), 325 deletions(-) create mode 100644 providers/browser-xmlhttprequest.js create mode 100644 providers/etherscan-provider.js create mode 100644 providers/fallback-provider.js create mode 100644 providers/infura-provider.js create mode 100644 providers/json-rpc-provider.js create mode 100644 providers/package.json create mode 100644 providers/provider.js diff --git a/providers/browser-xmlhttprequest.js b/providers/browser-xmlhttprequest.js new file mode 100644 index 000000000..6dd97e427 --- /dev/null +++ b/providers/browser-xmlhttprequest.js @@ -0,0 +1,8 @@ +'use strict'; + +try { + module.exports.XMLHttpRequest = XMLHttpRequest; +} catch(error) { + console.log('Warning: XMLHttpRequest is not defined'); + module.exports.XMLHttpRequest = null; +} diff --git a/providers/etherscan-provider.js b/providers/etherscan-provider.js new file mode 100644 index 000000000..cf3f946b3 --- /dev/null +++ b/providers/etherscan-provider.js @@ -0,0 +1,180 @@ +'use strict'; + +var inherits = require('inherits'); + +var Provider = require('./provider.js'); + +var utils = (function() { + var convert = require('ethers-utils/convert.js'); + return { + defineProperty: require('ethers-utils/properties.js').defineProperty, + + hexlify: convert.hexlify, + }; +})(); + +function getTransactionString(transaction) { + var result = []; + for (var key in transaction) { + if (transaction[key] == null) { continue; } + result.push(key + '=' + utils.hexlify(transaction[key])); + } + return result.join('&'); +} + +function EtherscanProvider(testnet, apiKey) { + Provider.call(this, testnet); + + utils.defineProperty(this, 'apiKey', apiKey || null); +} +inherits(EtherscanProvider, Provider); + +utils.defineProperty(EtherscanProvider.prototype, '_call', function() { +}); + +utils.defineProperty(EtherscanProvider.prototype, '_callProxy', function() { +}); + +function getResult(result) { + if (result.status != 1 || result.message != 'OK') { + throw new Error('invalid response'); + } + return result.result; +} + +function getJsonResult(result) { + if (result.jsonrpc != '2.0' || result.error) { + throw new Error('invalid response'); + } + 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.testnet ? 'https://testnet.etherscan.io': 'https://api.etherscan.io'; + + 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=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; + return Provider.fetchJSON(url, null, getResult); + + default: + break; + } + + return Promise.reject(new Error('not implemented - ' + method)); +}); + +module.exports = EtherscanProvider;; diff --git a/providers/fallback-provider.js b/providers/fallback-provider.js new file mode 100644 index 000000000..2868cddab --- /dev/null +++ b/providers/fallback-provider.js @@ -0,0 +1,62 @@ +'use strict'; + +var inherits = require('inherits'); + +var Provider = require('./provider.js'); + +var utils = (function() { + return { + defineProperty: require('ethers-utils/properties.js').defineProperty, + }; +})(); + + +function FallbackProvider(providers) { + if (providers.length === 0) { throw new Error('no providers'); } + + for (var i = 1; i < providers.length; i++) { + if (providers[0].chainId !== providers[i].chainId) { + throw new Error('incompatible providers - chainId mismatch'); + } + + if (providers[0].testnet !== providers[i].testnet) { + throw new Error('incompatible providers - testnet mismatch'); + } + } + + if (!(this instanceof FallbackProvider)) { throw new Error('missing new'); } + Provider.call(this, providers[0].testnet, providers[0].chainId); + + Object.defineProperty(this, 'providers', { + get: function() { + return providers.slice(0); + } + }); +} +inherits(FallbackProvider, Provider); + + +utils.defineProperty(FallbackProvider.prototype, 'perform', function(method, params) { + var providers = this.providers; + + return new Promise(function(resolve, reject) { + var firstError = null; + function next() { + if (!providers.length) { + reject(firstError); + return; + } + + var provider = providers.shift(); + provider.perform(method, params).then(function(result) { + resolve(result); + }, function (error) { + if (!firstError) { firstError = error; } + next(); + }); + } + next(); + }); +}); + +module.exports = FallbackProvider; diff --git a/providers/index.js b/providers/index.js index b95f7e5fa..513197928 100644 --- a/providers/index.js +++ b/providers/index.js @@ -1,332 +1,19 @@ 'use strict'; -var inherits = require('inherits'); -var XMLHttpRequest = require('xmlhttprequest').XMLHttpRequest; +var Provider = require('./provider.js'); -var utils = require('./utils.js'); +var EtherscanProvider = require('./etherscan-provider.js'); +var FallbackProvider = require('./fallback-provider.js'); +var InfuraProvider = require('./infura-provider.js'); +var JsonRpcProvider = require('./json-rpc-provider.js'); -// The required methods a provider must support -var methods = [ - 'getBalance', - 'getTransactionCount', - 'getGasPrice', - 'sendTransaction', - 'call', - 'estimateGas' -]; +module.exports = { + EtherscanProvider: EtherscanProvider, + FallbackProvider: FallbackProvider, + InfuraProvider: InfuraProvider, + JsonRpcProvider: JsonRpcProvider, + isProvder: Provider.isProvider, -// Manages JSON-RPC to an Ethereum node -function Web3Connector(provider) { - if (!(this instanceof Web3Connector)) { throw new Error('missing new'); } - - var nextMessageId = 1; - utils.defineProperty(this, 'sendMessage', function(method, params) { - return new Promise(function(resolve, reject) { - provider.sendAsync({ - id: (nextMessageId++), - jsonrpc: '2.0', - method: method, - params: params - }, function(error, result) { - if (error) { - reject(error); - } else { - if (result.error) { - var error = new Error(result.error.message); - error.code = result.error.code; - error.data = result.error.data; - reject(error); - } else { - resolve(result.result); - } - } - }); - }); - }); + _Provider: Provider, } - -// Mimics Web3 interface -function rpcSendAsync(url) { - return { - sendAsync: function(payload, callback) { - - var request = new XMLHttpRequest(); - request.open('POST', url, true); - request.setRequestHeader('Content-Type','application/json'); - request.onreadystatechange = function() { - if (request.readyState !== 4) { return; } - - if (typeof(callback) !== 'function') { return; } - - var result = request.responseText; - try { - callback(null, JSON.parse(result)); - } catch (error) { - var responseError = new Error('invalid response'); - responseError.orginialError = error; - responseError.data = result; - callback(responseError); - } - }; - - try { - request.send(JSON.stringify(payload)); - } catch (error) { - var connectionError = new Error('connection error'); - connectionError.error = error; - callback(connectionError); - } - } - } -} - -function SendAsyncProvider(sendAsync) { - if (!(this instanceof SendAsyncProvider)) { throw new Error('missing new'); } - utils.defineProperty(this, 'client', new Web3Connector(sendAsync)); -} - - -function validBlock(value) { - if (value == null) { return 'latest'; } - if (value === 'latest' || value === 'pending') { return value; } - - if (typeof(value) === 'number' && value == parseInt(value)) { - return parseInt(value); - } - - throw new Error('invalid blockNumber'); -} - -function postProcess(client, method, params, makeBN) { - return new Promise(function(resolve, reject) { - client.sendMessage(method, params).then(function (result) { - if (!utils.isHexString(result)) { - reject(new Error('invalid server response')); - } else { - result = result.substring(2); - if (makeBN) { - result = new utils.BN(result, 16); - } else { - result = parseInt(result, 16); - } - resolve(result); - } - }, function(error) { - reject(error); - }); - }); -} - -utils.defineProperty(SendAsyncProvider.prototype, 'getBalance', function(address, blockNumber) { - return postProcess(this.client, 'eth_getBalance', [ - utils.getAddress(address), - validBlock(blockNumber) - ], true); -}); - -utils.defineProperty(SendAsyncProvider.prototype, 'getTransactionCount', function(address, blockNumber) { - return postProcess(this.client, 'eth_getTransactionCount', [ - utils.getAddress(address), - validBlock(blockNumber) - ], false); -}); - -utils.defineProperty(SendAsyncProvider.prototype, 'getGasPrice', function() { - return postProcess(this.client, 'eth_gasPrice', [], true); -}); - -utils.defineProperty(SendAsyncProvider.prototype, 'sendTransaction', function(signedTransaction) { - if (!utils.isHexString(signedTransaction)) { throw new Error('invalid transaction'); } - return this.client.sendMessage('eth_sendRawTransaction', [signedTransaction]); -}); - -utils.defineProperty(SendAsyncProvider.prototype, 'call', function(transaction) { - // @TODO: check validTransaction? - // @TODO: allow passing in block tag? - return this.client.sendMessage('eth_call', [transaction, 'latest']); -}); - -utils.defineProperty(SendAsyncProvider.prototype, 'estimateGas', function(transaction) { - // @TODO: check validTransaction? - return postProcess(this.client, 'eth_estimateGas', [transaction], true); -}); - - -var providers = {}; - - -function HttpProvider(url) { - if (!(this instanceof HttpProvider)) { throw new Error('missing new'); } - SendAsyncProvider.call(this, rpcSendAsync(url)); -} -inherits(HttpProvider, SendAsyncProvider); -utils.defineProperty(providers, 'HttpProvider', HttpProvider); - - -function Web3Provider(provider) { - if (!(this instanceof Web3Provider)) { throw new Error('missing new'); } - if (provider.currentProvider) { provider = provider.currentProvider; } - if (!provider.sendAsync) { throw new Error('invalid provider'); } - SendAsyncProvider.call(this, provider); -} -inherits(Web3Provider, SendAsyncProvider); -utils.defineProperty(providers, 'Web3Provider', Web3Provider); - - -function base10ToBN(value) { - return new utils.BN(value); -} - -function hexToBN(value) { - return new utils.BN(ensureHex(value).substring(2), 16); -} - -function hexToNumber(value) { - if (!utils.isHexString(value)) { throw new Error('invalid hex string'); } - return parseInt(value.substring(2), 16); -} - -function ensureHex(value) { - if (!utils.isHexString(value)) { throw new Error('invalid hex string'); } - return value; -} - -function ensureTxid(value) { - if (!utils.isHexString(value, 32)) { throw new Error('invalid hex string'); } - return value; -} - -function getGasPrice(value) { - if (!value || !value.transactions || value.transactions.length === 0) { - throw new Error('invalid response'); - } - return hexToBN(value.transactions[0].gasPrice); -} - -function EtherscanProvider(options) { - if (!(this instanceof EtherscanProvider)) { throw new Error('missing new'); } - if (!options) { options = {}; } - - var testnet = options.testnet; - var apiKey = options.apiKey; - - utils.defineProperty(this, 'testnet', testnet); - utils.defineProperty(this, 'apiKey', apiKey); - - utils.defineProperty(this, '_send', function(query, check) { - var url = (testnet ? 'https://testnet.etherscan.io/api?': 'https://api.etherscan.io/api?'); - url += query; - if (apiKey) { url += 'apikey=' + apiKey; } - //console.log('URL', url); - - return new Promise(function(resolve, reject) { - var request = new XMLHttpRequest(); - request.open('GET', url, true); - request.onreadystatechange = function() { - if (request.readyState !== 4) { return; } - - var result = request.responseText; - //console.log(result); - try { - result = JSON.parse(result); - if (result.message) { - if (result.message === 'OK') { - resolve(check(result.result)); - } else { - reject(new Error('invalid response')); - } - } else { - if (result.error) { - console.log(result.error); - reject(new Error('invalid response')); - } else { - resolve(check(result.result)); - } - } - } catch (error) { - console.log(error); - reject(new Error('invalid response')); - } - } - - try { - request.send(); - } catch (error) { - var connectionError = new Error('connection error'); - connectionError.error = error; - reject(connectionError); - } - }); - - }); -} -utils.defineProperty(providers, 'EtherscanProvider', EtherscanProvider); - -utils.defineProperty(EtherscanProvider.prototype, 'getBalance', function(address, blockNumber) { - address = utils.getAddress(address); - blockNumber = validBlock(blockNumber); - var query = ('module=account&action=balance&address=' + address + '&tag=' + blockNumber); - return this._send(query, base10ToBN); -}); - -utils.defineProperty(EtherscanProvider.prototype, 'getTransactionCount', function(address, blockNumber) { - address = utils.getAddress(address); - blockNumber = validBlock(blockNumber); - var query = ('module=proxy&action=eth_getTransactionCount&address=' + address + '&tag=' + blockNumber); - return this._send(query, hexToNumber); -}); - -utils.defineProperty(EtherscanProvider.prototype, 'getGasPrice', function() { - var query = ('module=proxy&action=eth_gasPrice'); - return this._send(query, hexToBN); -}); - -utils.defineProperty(EtherscanProvider.prototype, 'sendTransaction', function(signedTransaction) { - if (!utils.isHexString(signedTransaction)) { throw new Error('invalid transaction'); } - var query = ('module=proxy&action=eth_sendRawTransaction&hex=' + signedTransaction); - return this._send(query, ensureTxid); -}); - -utils.defineProperty(EtherscanProvider.prototype, 'call', function(transaction) { - var address = utils.getAddress(transaction.to); - var data = transaction.data; - if (!utils.isHexString(data)) { throw new Error('invalid data'); } - var query = ('module=proxy&action=eth_call&to=' + address + '&data=' + data); - return this._send(query, ensureHex); -}); - -utils.defineProperty(EtherscanProvider.prototype, 'estimateGas', function(transaction) { - var address = utils.getAddress(transaction.to); - - var query = 'module=proxy&action=eth_estimateGas&to=' + address; - if (transaction.gasPrice) { - query += '&gasPrice=' + utils.hexlify(transaction.gasPrice); - } - if (transaction.gasLimit) { - query += '&gas=' + utils.hexlify(transaction.gasLimit); - } - if (transaction.from) { - query += '&from=' + utils.getAddress(transaction.from); - } - if (transaction.data) { - query += '&data=' + ensureHex(transaction.data); - } - if (transaction.value) { - query += '&value=' + utils.hexlify(transaction.value); - } - return this._send(query, hexToBN); -}); - - -utils.defineProperty(providers, 'isProvider', function(provider) { - if (!provider) { return false; } - for (var i = 0; i < methods; i++) { - if (typeof(provider[methods[i]]) !== 'function') { - return false; - } - } - return true; -}); - -module.exports = providers; diff --git a/providers/infura-provider.js b/providers/infura-provider.js new file mode 100644 index 000000000..aecbdcc68 --- /dev/null +++ b/providers/infura-provider.js @@ -0,0 +1,24 @@ +var inherits = require('inherits'); + +var JsonRpcProvider = require('./json-rpc-provider.js'); + +var utils = (function() { + return { + defineProperty: require('ethers-utils/properties.js').defineProperty + } +})(); + +function InfuraProvider(testnet, apiAccessToken) { + if (!(this instanceof InfuraProvider)) { throw new Error('missing new'); } + + var host = (testnet ? "ropsten": "mainnet") + '.infura.io'; + var url = 'https://' + host + '/' + (apiAccessToken || ''); + + JsonRpcProvider.call(this, url, testnet); + + utils.defineProperty(this, 'apiAccessToken', apiAccessToken || null); +} + +inherits(InfuraProvider, JsonRpcProvider); + +module.exports = InfuraProvider; diff --git a/providers/json-rpc-provider.js b/providers/json-rpc-provider.js new file mode 100644 index 000000000..3873c115e --- /dev/null +++ b/providers/json-rpc-provider.js @@ -0,0 +1,427 @@ +'use strict'; + +// See: https://github.com/ethereum/wiki/wiki/JSON-RPC + +var inherits = require('inherits'); + +var Provider = require('./provider.js'); + +var utils = (function() { + return { + defineProperty: require('ethers-utils/properties.js').defineProperty, + + hexlify: require('ethers-utils/convert.js').hexlify, + } +})(); + +function getResult(payload) { + if (payload.error) { + var error = new Error(payload.error.message); + error.code = payload.error.code; + error.data = payload.error.data; + throw error; + } + + return payload.result; +} + +function getTransaction(transaction) { + var result = []; + for (var key in transaction) { + result[key] = utils.hexlify(transaction[key]); + } + return result; +} + +function JsonRpcProvider(url, testnet, chainId) { + if (!(this instanceof JsonRpcProvider)) { throw new Error('missing new'); } + + Provider.call(this, testnet, chainId); + + utils.defineProperty(this, 'url', url); +} +inherits(JsonRpcProvider, Provider); + +utils.defineProperty(JsonRpcProvider.prototype, 'send', function(method, params) { + var request = { + method: method, + params: params, + id: 42, + jsonrpc: "2.0" + }; + return Provider.fetchJSON(this.url, JSON.stringify(request), getResult); +}); + +utils.defineProperty(JsonRpcProvider.prototype, 'perform', function(method, params) { + switch (method) { + case 'getBlockNumber': + return this.send('eth_blockNumber', []); + + case 'getGasPrice': + return this.send('eth_gasPrice', []); + + case 'getBalance': + return this.send('eth_getBalance', [params.address, params.blockTag]); + + case 'getTransactionCount': + return this.send('eth_getTransactionCount', [params.address, params.blockTag]); + + case 'getCode': + return this.send('eth_getCode', [params.address, params.blockTag]); + + case 'getStorageAt': + return this.send('eth_getStorageAt', [params.address, params.position, params.blockTag]); + + case 'sendTransaction': + return this.send('eth_sendRawTransaction', [params.signedTransaction]); + + case 'getBlock': + if (params.blockTag) { + return this.send('eth_getBlockByNumber', [params.blockTag, false]); + } else if (params.blockHash) { + return this.send('eth_getBlockByHash', [params.blockHash, false]); + } + return Promise.reject(new Error('invalid block tag or block hash')); + + case 'getTransaction': + return this.send('eth_getTransactionByHash', [params.transactionHash]); + + case 'getTransactionReceipt': + return this.send('eth_getTransactionReceipt', [params.transactionHash]); + + case 'call': + return this.send('eth_call', [getTransaction(params.transaction)]); + + case 'estimateGas': + return this.send('eth_estimateGas', [getTransaction(params.transaction)]); + + case 'getLogs': + console.log('FFF', params.filter); + return this.send('eth_getLogs', [params.filter]); + + default: + break; + } + + return Promise.reject(new Error('not implemented - ' + method)); +}); + +module.exports = JsonRpcProvider; + +/* + +// Manages JSON-RPC to an Ethereum node +function Web3Connector(provider) { + if (!(this instanceof Web3Connector)) { throw new Error('missing new'); } + + var nextMessageId = 1; + utils.defineProperty(this, 'sendMessage', function(method, params) { + return new Promise(function(resolve, reject) { + provider.sendAsync({ + id: (nextMessageId++), + jsonrpc: '2.0', + method: method, + params: params + }, function(error, result) { + if (error) { + reject(error); + } else { + if (result.error) { + var error = new Error(result.error.message); + error.code = result.error.code; + error.data = result.error.data; + reject(error); + } else { + resolve(result.result); + } + } + }); + }); + }); +} + +// Mimics Web3 interface +function rpcSendAsync(url) { + return { + sendAsync: function(payload, callback) { + + var request = new XMLHttpRequest(); + request.open('POST', url, true); + request.setRequestHeader('Content-Type','application/json'); + request.onreadystatechange = function() { + if (request.readyState !== 4) { return; } + + if (typeof(callback) !== 'function') { return; } + + var result = request.responseText; + try { + callback(null, JSON.parse(result)); + } catch (error) { + var responseError = new Error('invalid response'); + responseError.orginialError = error; + responseError.data = result; + callback(responseError); + } + }; + + try { + request.send(JSON.stringify(payload)); + } catch (error) { + var connectionError = new Error('connection error'); + connectionError.error = error; + callback(connectionError); + } + } + } +} + +function SendAsyncProvider(sendAsync) { + if (!(this instanceof SendAsyncProvider)) { throw new Error('missing new'); } + utils.defineProperty(this, 'client', new Web3Connector(sendAsync)); +} + + +function validBlock(value) { + if (value == null) { return 'latest'; } + if (value === 'latest' || value === 'pending') { return value; } + + if (typeof(value) === 'number' && value == parseInt(value)) { + return parseInt(value); + } + + throw new Error('invalid blockNumber'); +} + +function postProcess(client, method, params, makeBN) { + return new Promise(function(resolve, reject) { + client.sendMessage(method, params).then(function (result) { + if (!utils.isHexString(result)) { + reject(new Error('invalid server response')); + } else { + result = result.substring(2); + if (makeBN) { + result = new utils.BN(result, 16); + } else { + result = parseInt(result, 16); + } + resolve(result); + } + }, function(error) { + reject(error); + }); + }); +} + +utils.defineProperty(SendAsyncProvider.prototype, 'getBalance', function(address, blockNumber) { + return postProcess(this.client, 'eth_getBalance', [ + utils.getAddress(address), + validBlock(blockNumber) + ], true); +}); + +utils.defineProperty(SendAsyncProvider.prototype, 'getTransactionCount', function(address, blockNumber) { + return postProcess(this.client, 'eth_getTransactionCount', [ + utils.getAddress(address), + validBlock(blockNumber) + ], false); +}); + +utils.defineProperty(SendAsyncProvider.prototype, 'getGasPrice', function() { + return postProcess(this.client, 'eth_gasPrice', [], true); +}); + +utils.defineProperty(SendAsyncProvider.prototype, 'sendTransaction', function(signedTransaction) { + if (!utils.isHexString(signedTransaction)) { throw new Error('invalid transaction'); } + return this.client.sendMessage('eth_sendRawTransaction', [signedTransaction]); +}); + +utils.defineProperty(SendAsyncProvider.prototype, 'call', function(transaction) { + // @TODO: check validTransaction? + // @TODO: allow passing in block tag? + return this.client.sendMessage('eth_call', [transaction, 'latest']); +}); + +utils.defineProperty(SendAsyncProvider.prototype, 'estimateGas', function(transaction) { + // @TODO: check validTransaction? + return postProcess(this.client, 'eth_estimateGas', [transaction], true); +}); + + +var providers = {}; + + +function HttpProvider(url) { + if (!(this instanceof HttpProvider)) { throw new Error('missing new'); } + SendAsyncProvider.call(this, rpcSendAsync(url)); +} +inherits(HttpProvider, SendAsyncProvider); +utils.defineProperty(providers, 'HttpProvider', HttpProvider); + + +function Web3Provider(provider) { + if (!(this instanceof Web3Provider)) { throw new Error('missing new'); } + if (provider.currentProvider) { provider = provider.currentProvider; } + if (!provider.sendAsync) { throw new Error('invalid provider'); } + SendAsyncProvider.call(this, provider); +} +inherits(Web3Provider, SendAsyncProvider); +utils.defineProperty(providers, 'Web3Provider', Web3Provider); + + +function base10ToBN(value) { + return new utils.BN(value); +} + +function hexToBN(value) { + return new utils.BN(ensureHex(value).substring(2), 16); +} + +function hexToNumber(value) { + if (!utils.isHexString(value)) { throw new Error('invalid hex string'); } + return parseInt(value.substring(2), 16); +} + +function ensureHex(value) { + if (!utils.isHexString(value)) { throw new Error('invalid hex string'); } + return value; +} + +function ensureTxid(value) { + if (!utils.isHexString(value, 32)) { throw new Error('invalid hex string'); } + return value; +} + +function getGasPrice(value) { + if (!value || !value.transactions || value.transactions.length === 0) { + throw new Error('invalid response'); + } + return hexToBN(value.transactions[0].gasPrice); +} + +function EtherscanProvider(options) { + if (!(this instanceof EtherscanProvider)) { throw new Error('missing new'); } + if (!options) { options = {}; } + + var testnet = options.testnet; + var apiKey = options.apiKey; + + utils.defineProperty(this, 'testnet', testnet); + utils.defineProperty(this, 'apiKey', apiKey); + + utils.defineProperty(this, '_send', function(query, check) { + var url = (testnet ? 'https://testnet.etherscan.io/api?': 'https://api.etherscan.io/api?'); + url += query; + if (apiKey) { url += 'apikey=' + apiKey; } + //console.log('URL', url); + + return new Promise(function(resolve, reject) { + var request = new XMLHttpRequest(); + request.open('GET', url, true); + request.onreadystatechange = function() { + if (request.readyState !== 4) { return; } + + var result = request.responseText; + //console.log(result); + try { + result = JSON.parse(result); + if (result.message) { + if (result.message === 'OK') { + resolve(check(result.result)); + } else { + reject(new Error('invalid response')); + } + } else { + if (result.error) { + console.log(result.error); + reject(new Error('invalid response')); + } else { + resolve(check(result.result)); + } + } + } catch (error) { + console.log(error); + reject(new Error('invalid response')); + } + } + + try { + request.send(); + } catch (error) { + var connectionError = new Error('connection error'); + connectionError.error = error; + reject(connectionError); + } + }); + + }); +} +utils.defineProperty(providers, 'EtherscanProvider', EtherscanProvider); + +utils.defineProperty(EtherscanProvider.prototype, 'getBalance', function(address, blockNumber) { + address = utils.getAddress(address); + blockNumber = validBlock(blockNumber); + var query = ('module=account&action=balance&address=' + address + '&tag=' + blockNumber); + return this._send(query, base10ToBN); +}); + +utils.defineProperty(EtherscanProvider.prototype, 'getTransactionCount', function(address, blockNumber) { + address = utils.getAddress(address); + blockNumber = validBlock(blockNumber); + var query = ('module=proxy&action=eth_getTransactionCount&address=' + address + '&tag=' + blockNumber); + return this._send(query, hexToNumber); +}); + +utils.defineProperty(EtherscanProvider.prototype, 'getGasPrice', function() { + var query = ('module=proxy&action=eth_gasPrice'); + return this._send(query, hexToBN); +}); + +utils.defineProperty(EtherscanProvider.prototype, 'sendTransaction', function(signedTransaction) { + if (!utils.isHexString(signedTransaction)) { throw new Error('invalid transaction'); } + var query = ('module=proxy&action=eth_sendRawTransaction&hex=' + signedTransaction); + return this._send(query, ensureTxid); +}); + +utils.defineProperty(EtherscanProvider.prototype, 'call', function(transaction) { + var address = utils.getAddress(transaction.to); + var data = transaction.data; + if (!utils.isHexString(data)) { throw new Error('invalid data'); } + var query = ('module=proxy&action=eth_call&to=' + address + '&data=' + data); + return this._send(query, ensureHex); +}); + +utils.defineProperty(EtherscanProvider.prototype, 'estimateGas', function(transaction) { + var address = utils.getAddress(transaction.to); + + var query = 'module=proxy&action=eth_estimateGas&to=' + address; + if (transaction.gasPrice) { + query += '&gasPrice=' + utils.hexlify(transaction.gasPrice); + } + if (transaction.gasLimit) { + query += '&gas=' + utils.hexlify(transaction.gasLimit); + } + if (transaction.from) { + query += '&from=' + utils.getAddress(transaction.from); + } + if (transaction.data) { + query += '&data=' + ensureHex(transaction.data); + } + if (transaction.value) { + query += '&value=' + utils.hexlify(transaction.value); + } + return this._send(query, hexToBN); +}); + + +utils.defineProperty(providers, 'isProvider', function(provider) { + if (!provider) { return false; } + for (var i = 0; i < methods; i++) { + if (typeof(provider[methods[i]]) !== 'function') { + return false; + } + } + return true; +}); + +module.exports = providers; +*/ diff --git a/providers/package.json b/providers/package.json new file mode 100644 index 000000000..951cf78bd --- /dev/null +++ b/providers/package.json @@ -0,0 +1,29 @@ +{ + "name": "ethers-providers", + "version": "2.0.0", + "description": "Service provider for Ethereum wallet library.", + "main": "index.js", + "browser": { + "xmlhttprequest": "./browser-xmlhttprequest.js" + }, + "dependencies": { + "inherits": "2.0.1", + "xmlhttprequest": "1.8.0" + }, + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "keywords": [ + "ethereum", + "ethers", + "providers", + "etherscan", + "web3", + "json", + "rpc", + "json-rpc", + "jsonrpc" + ], + "author": "Richard Moore ", + "license": "MIT" +} diff --git a/providers/provider.js b/providers/provider.js new file mode 100644 index 000000000..94b1f4c01 --- /dev/null +++ b/providers/provider.js @@ -0,0 +1,668 @@ + +var XMLHttpRequest = require('xmlhttprequest').XMLHttpRequest; + +var utils = (function() { + var convert = require('ethers-utils/convert.js'); + return { + defineProperty: require('ethers-utils/properties.js').defineProperty, + + getAddress: require('ethers-utils/address.js').getAddress, + + bigNumberify: require('ethers-utils/bignumber.js').bigNumberify, + arrayify: convert.arrayify, + + hexlify: convert.hexlify, + isHexString: convert.isHexString, + } +})(); + +function check(format, object) { + var result = {}; + for (var key in format) { + try { + value = format[key](object[key]); + if (value !== undefined) { result[key] = value; } + } catch (error) { + console.log(error, key, object); + error.checkKey = key; + error.checkValue = object[key]; + throw error; + } + } + return result; +} + +function allowNull(check, nullValue) { + return (function(value) { + if (value == null) { return nullValue; } + return check(value); + }); +} + +function allowFalsish(check, replaceValue) { + return (function(value) { + if (!value) { return replaceValue; } + return check(value); + }); +} + +function arrayOf(check) { + return (function(array) { + if (!Array.isArray(array)) { throw new Error('not an array'); } + + var result = []; + + array.forEach(function(value) { + result.push(check(value)); + }); + + return result; + }); +} + +function checkHash(hash) { + if (!utils.isHexString(hash) || hash.length !== 66) { + throw new Error('invalid hash'); + } + return hash; +} + +function checkNumber(number) { + return utils.bigNumberify(number).toNumber(); +} + +function checkString(string) { + if (typeof(string) !== 'string') { throw new Error('invalid string'); } + return string; +} + +function checkBlockTag(blockTag) { + if (blockTag == null) { return 'latest'; } + + if (utils.isHexString(blockTag)) { return blockTag; } + + if (blockTag === 'earliest') { blockTag = 0; } + if (typeof(blockTag) === 'number') { + return utils.hexlify(blockTag); + } + + if (blockTag === 'latest' || blockTag === 'pending') { + return blockTag; + } + + throw new Error('invalid blockTag'); +} + +var formatBlock = { + hash: checkHash, + parentHash: checkHash, + number: checkNumber, + + timestamp: checkNumber, + nonce: utils.hexlify, + difficulty: checkNumber, + + gasLimit: utils.bigNumberify, + gasUsed: utils.bigNumberify, + + author: utils.getAddress, + extraData: utils.hexlify, + + //transactions: allowNull(arrayOf(checkTransaction)), + transactions: allowNull(arrayOf(checkHash)), + + //transactionRoot: checkHash, + //stateRoot: checkHash, + //sha3Uncles: checkHash, + + //logsBloom: utils.hexlify, +}; + +function checkBlock(block) { + return check(formatBlock, block); +} + + +var formatTransaction = { + hash: checkHash, + + blockHash: allowNull(checkHash, null), + blockNumber: allowNull(checkNumber, null), + transactionIndex: allowNull(checkNumber, null), + + from: utils.getAddress, + + gasPrice: utils.bigNumberify, + gasLimit: utils.bigNumberify, + to: allowNull(utils.getAddress, null), + value: utils.bigNumberify, + nonce: checkNumber, + data: utils.hexlify, + + r: checkHash, + s: checkHash, + v: checkNumber, + + creates: allowNull(utils.getAddress, null), +}; + +function checkTransaction(transaction) { + if (transaction.gas != null && transaction.gasLimit == null) { + transaction.gasLimit = transaction.gas; + } + if (transaction.input != null && transaction.data == null) { + transaction.data = transaction.input; + } + return check(formatTransaction, transaction); +} + +var formatTransactionReceiptLog = { + transactionLogIndex: checkNumber, + blockNumber: checkNumber, + transactionHash: checkHash, + address: utils.getAddress, + type: checkString, + topics: arrayOf(checkHash), + transactionIndex: checkNumber, + data: utils.hexlify, + logIndex: checkNumber, + blockHash: checkHash, +}; + +function checkTransactionReceiptLog(log) { + return check(formatTransactionReceiptLog, log); +} + +var formatTransactionReceipt = { + contractAddress: allowNull(utils.getAddress, null), + transactionIndex: checkNumber, + root: checkHash, + gasUsed: utils.bigNumberify, + logsBloom: utils.hexlify, + blockHash: checkHash, + transactionHash: checkHash, + logs: arrayOf(checkTransactionReceiptLog), + blockNumber: checkNumber, + cumulativeGasUsed: utils.bigNumberify, +}; + +function checkTransactionReceipt(transactionReceipt) { + return check(formatTransactionReceipt, transactionReceipt); +} + +function checkTopics(topics) { + if (Array.isArray(topics)) { + topics.forEach(function(topic) { + checkTopics(topic); + }); + + } else if (topics != null) { + checkHash(topics); + } + + return topics; +} + +var formatFilter = { + fromBlock: allowNull(checkBlockTag, undefined), + toBlock: allowNull(checkBlockTag, undefined), + address: allowNull(utils.getAddress, undefined), + topics: allowNull(checkTopics, undefined), +}; + +function checkFilter(filter) { + return check(formatFilter, filter); +} + +var formatLog = { + blockNumber: checkNumber, + transactionIndex: checkNumber, + + address: utils.getAddress, + data: allowFalsish(utils.hexlify, '0x'), + + topics: arrayOf(checkHash), + + transactionHash: checkHash, + logIndex: checkNumber, +} + +function checkLog(log) { + return check(formatLog, log); +} + +function Provider(testnet, chainId) { + if (!(this instanceof Provider)) { throw new Error('missing new'); } + + testnet = !!testnet; + + if (chainId == null) { + chainId = (testnet ? Provider.chainId.ropsten: Provider.chainId.homestead); + } + + if (typeof(chainId) !== 'number') { throw new Error('invalid chainId'); } + + utils.defineProperty(this, 'testnet', testnet); + utils.defineProperty(this, 'chainId', chainId); + + var events = {}; + utils.defineProperty(this, '_events', events); + + var self = this; + + var lastBlockNumber = null; + var poller = setInterval(function() { + self.getBlockNumber().then(function(blockNumber) { + + // If the block hasn't changed, meh. + if (blockNumber === lastBlockNumber) { return; } + + if (lastBlockNumber === null) { lastBlockNumber = blockNumber - 1; } + + // Notify all listener for each block that has passed + for (var i = lastBlockNumber + 1; i <= blockNumber; i++) { + self.emit('block', i); + } + lastBlockNumber = blockNumber; + + // Find all transaction hashes we are waiting on + for (var eventName in events) { + if (utils.isHexString(eventName) && eventName.length === 66) { + eventName = getString(eventName); + self.getTransaction(eventName).then(function(transaction) { + if (!transaction.blockNumber) { return; } + self.emit(eventName, transaction); + }); + } + } + }); + + self.doPoll(); + }, 4000); + + if (poller.unref) { poller.unref(); } +} + +utils.defineProperty(Provider, 'chainId', { + homestead: 1, + morden: 2, + ropsten: 3, +}); + +utils.defineProperty(Provider, 'isProvider', function(object) { + return (object instanceof Provider); +}); + +utils.defineProperty(Provider, 'fetchJSON', function(url, json, processFunc) { + + return new Promise(function(resolve, reject) { + var request = new XMLHttpRequest(); + + if (json) { + request.open('POST', url, true); + request.setRequestHeader('Content-Type','application/json'); + } else { + request.open('GET', url, true); + } + + request.onreadystatechange = function() { + if (request.readyState !== 4) { return; } + + if (request.status != 200) { + var error = new Error('invalid response'); + error.statusCode = request.statusCode; + reject(error); + return; + } + + var result = request.responseText; + + try { + result = JSON.parse(result); + } catch (error) { + var jsonError = new Error('invalid json response'); + jsonError.orginialError = error; + jsonError.responseText = request.responseText; + reject(jsonError); + return; + } + + if (processFunc) { + try { + result = processFunc(result); + } catch (error) { + error.responseText = request.responseText; + reject(error); + return; + } + } + + resolve(result); + }; + + request.onerror = function(error) { + reject(error); + } + + try { + if (json) { + request.send(json); + } else { + request.send(); + } + + } catch (error) { + var connectionError = new Error('connection error'); + connectionError.error = error; + reject(connectionError); + } + }); +}); + + +utils.defineProperty(Provider.prototype, 'waitForTransaction', function(transactionHash, timeout) { + return new Promise(function() { + var done = false; + var timer = null; + + function complete(transaction) { + if (done) { return; } + done = true; + + if (timer) { + clearTimeout(timer); + timer = null; + } + + resolve(transaction); + } + + function checkTransaction(transaction) { + } + + if (typeof(timeout) === 'number' && timeout > 0) { + timer = setTimeout(function() { + done = true; + timer = null; + reject(new Error('timeout')); + }, timeout); + } + + }); +}); + + +utils.defineProperty(Provider.prototype, 'getBlockNumber', function() { + try { + return this.perform('getBlockNumber').then(function(result) { + var value = parseInt(result); + if (value != result) { throw new Error('invalid response'); } + return value; + }); + } catch (error) { + return Promise.reject(error); + } +}); + +utils.defineProperty(Provider.prototype, 'getGasPrice', function() { + try { + return this.perform('getGasPrice').then(function(result) { + return utils.bigNumberify(result); + }); + } catch (error) { + return Promise.reject(error); + } +}); + + +utils.defineProperty(Provider.prototype, 'getBalance', function(address, blockTag) { + try { + var params = {address: utils.getAddress(address), blockTag: checkBlockTag(blockTag)}; + return this.perform('getBalance', params).then(function(result) { + return utils.bigNumberify(result); + }); + } catch (error) { + return Promise.reject(error); + } +}); + +utils.defineProperty(Provider.prototype, 'getTransactionCount', function(address, blockTag) { + try { + var params = {address: utils.getAddress(address), blockTag: checkBlockTag(blockTag)}; + return this.perform('getTransactionCount', params).then(function(result) { + var value = parseInt(result); + if (value != result) { throw new Error('invalid response'); } + return value; + }); + } catch (error) { + return Promise.reject(error); + } +}); + +utils.defineProperty(Provider.prototype, 'getCode', function(address, blockTag) { + try { + var params = {address: utils.getAddress(address), blockTag: checkBlockTag(blockTag)}; + return this.perform('getCode', params).then(function(result) { + return utils.hexlify(result); + }); + } catch (error) { + return Promise.reject(error); + } +}); + +utils.defineProperty(Provider.prototype, 'getStorageAt', function(address, position, blockTag) { + try { + var params = { + address: utils.getAddress(address), + position: utils.hexlify(position), + blockTag: checkBlockTag(blockTag) + }; + return this.perform('getStorageAt', params).then(function(result) { + return utils.hexlify(result); + }); + } catch (error) { + return Promise.reject(error); + } +}); + +utils.defineProperty(Provider.prototype, 'sendTransaction', function(signedTransaction) { + try { + var params = {signedTransaction: utils.hexlify(signedTransaction)}; + return this.perform('sendTransaction', params).then(function(result) { + result = utils.hexlify(result); + if (result.length !== 66) { throw new Error('invalid response'); } + return result; + }); + } catch (error) { + return Promise.reject(error); + } +}); + + +utils.defineProperty(Provider.prototype, 'call', function(transaction) { + try { + var params = {transaction: checkTransaction(transaction)}; + return this.perform('call', params).then(function(result) { + return utils.hexlify(result); + }); + } catch (error) { + return Promise.reject(error); + } +}); + +utils.defineProperty(Provider.prototype, 'estimateGas', function(transaction) { + try { + var params = {transaction: checkTransaction(transaction)}; + return this.perform('call', params).then(function(result) { + return utils.bigNumberify(result); + }); + } catch (error) { + return Promise.reject(error); + } +}); + + +utils.defineProperty(Provider.prototype, 'getBlock', function(blockHashOrBlockTag) { + try { + var blockHash = utils.hexlify(blockHashOrBlockTag); + if (blockHash.length === 66) { + return this.perform('getBlock', {blockHash: blockHash}).then(function(block) { + return checkBlock(block); + }); + } + } catch (error) { + console.log('DEBUG', error); + } + + try { + var blockTag = checkBlockTag(blockHashOrBlockTag); + return this.perform('getBlock', {blockTag: blockTag}).then(function(block) { + return checkBlock(block); + }); + } catch (error) { + console.log('DEBUG', error); + } + + return Promise.reject(new Error('invalid block hash or block tag')); +}); + +utils.defineProperty(Provider.prototype, 'getTransaction', function(transactionHash) { + try { + var params = {transactionHash: checkHash(transactionHash)}; + return this.perform('getTransaction', params).then(function(result) { + return checkTransaction(result); + }); + } catch (error) { + return Promise.reject(error); + } +}); + +utils.defineProperty(Provider.prototype, 'getTransactionReceipt', function(transactionHash) { + try { + var params = {transactionHash: checkHash(transactionHash)}; + return this.perform('getTransactionReceipt', params).then(function(result) { + return checkTransactionReceipt(result); + }); + } catch (error) { + return Promise.reject(error); + } +}); + +utils.defineProperty(Provider.prototype, 'getLogs', function(filter) { + try { + var params = {filter: checkFilter(filter)}; + return this.perform('getLogs', params).then(function(result) { + return arrayOf(checkLog)(result); + }); + } catch (error) { + return Promise.reject(error); + } +}); + +utils.defineProperty(Provider.prototype, 'getEtherPrice', function() { + try { + return this.perform('getEtherPrice', {}).then(function(result) { + // @TODO: Check valid float + return result; + }); + } catch (error) { + return Promise.reject(error); + } +}); + + +utils.defineProperty(Provider.prototype, 'doPoll', function() { + +}); + +utils.defineProperty(Provider.prototype, 'perform', function(method, params) { + return Promise.reject(new Error('not implemented - ' + method)); +}); + +function getString(object) { + if (Array.isArray(object)) { + var result = []; + for (var i = 0; i < object.length; i++) { + result.push(getString(object[i])); + } + return '[' + result.join(',') + ']'; + + } else if (typeof(object) === 'string') { + return object; + + } else if (object == null) { + return 'null'; + } + + throw new Error('invalid topic'); +} + +utils.defineProperty(Provider.prototype, 'on', function(eventName, listener) { + var key = getString(eventName); + if (!this._events[key]) { this._events[eventName] = []; } + this._events[key].push({eventName: eventName, listener: listener, type: 'on'}); +}); + +utils.defineProperty(Provider.prototype, 'once', function(eventName, listener) { + var key = getString(eventName); + if (!this._events[key]) { this._events[eventName] = []; } + this._events[key].push({listener: listener, type: 'once'}); +}); + +utils.defineProperty(Provider.prototype, 'emit', function(eventName) { + var key = getString(eventName); + + var args = Array.prototype.slice.call(arguments, 1); + var listeners = this._events[key]; + if (!listeners) { return; } + + for (var i = 0; i < listeners.length; i++) { + var listener = listeners[i]; + + if (listener.type === 'once') { + delete listeners[i]; + i--; + } + + try { + listener.listener.apply(this, args); + } catch (error) { + console.log('Event Listener Error: ' + error.message); + } + } +console.log('LLL', listeners); + if (listeners.length === 0) { delete this._events[key]; } +}); + +utils.defineProperty(Provider.prototype, 'listenerCount', function(eventName) { + var listeners = this._events[getString(eventName)]; + if (!listeners) { return 0; } + return listeners.length; +}); + +utils.defineProperty(Provider.prototype, 'listeners', function(eventName) { + var listeners = this._events[getString(eventName)]; + if (!listeners) { return 0; } + var result = []; + for (var i = 0; i < listeners.length; i++) { + result.push(lisrteners[i].listener); + } + return result; +}); + +utils.defineProperty(Provider.prototype, 'removeAllListeners', function(eventName) { + delete this._events[getString(eventName)]; +}); + +utils.defineProperty(Provider.prototype, 'removeListener', function(eventName, listener) { + var listeners = this._events[getString(eventName)]; + if (!listeners) { return 0; } + for (var i = 0; i < listeners.length; i++) { + if (listeners[i].listener === listener) { + delete listeners[i]; + return; + } + } +}); + +module.exports = Provider;