'use strict'; // See: https://github.com/ethereum/wiki/wiki/JSON-RPC var Provider = require('./provider.js'); var utils = (function() { var convert = require('../utils/convert'); return { defineProperty: require('../utils/properties').defineProperty, hexlify: convert.hexlify, isHexString: convert.isHexString, hexStripZeros: convert.hexStripZeros, toUtf8Bytes: require('../utils/utf8').toUtf8Bytes, getAddress: require('../utils/address').getAddress, } })(); var errors = require('../utils/errors'); function timer(timeout) { return new Promise(function(resolve) { setTimeout(function() { resolve(); }, timeout); }); } 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]); } // Some nodes (INFURA ropsten; INFURA mainnet is fine) don't like extra zeros. ['gasLimit', 'gasPrice', 'nonce', 'value'].forEach(function(key) { if (!result[key]) { return; } result[key] = utils.hexStripZeros(result[key]); }); // Transform "gasLimit" to "gas" if (result.gasLimit != null && result.gas == null) { result.gas = result.gasLimit; delete result.gasLimit; } return result; } function getLowerCase(value) { if (value) { return value.toLowerCase(); } return value; } function JsonRpcSigner(provider, address) { errors.checkNew(this, JsonRpcSigner); utils.defineProperty(this, 'provider', provider); // Statically attach to a given address if (address) { utils.defineProperty(this, 'address', address); utils.defineProperty(this, '_syncAddress', true); } else { Object.defineProperty(this, 'address', { enumerable: true, get: function() { errors.throwError('no sync sync address available; use getAddress', errors.UNSUPPORTED_OPERATION, { operation: 'address' }); } }); utils.defineProperty(this, '_syncAddress', false); } } utils.defineProperty(JsonRpcSigner.prototype, 'getAddress', function() { if (this._syncAddress) { return Promise.resolve(this.address); } return this.provider.send('eth_accounts', []).then(function(accounts) { if (accounts.length === 0) { errors.throwError('no accounts', errors.UNSUPPORTED_OPERATION, { operation: 'getAddress' }); } return utils.getAddress(accounts[0]); }); }); utils.defineProperty(JsonRpcSigner.prototype, 'getBalance', function(blockTag) { var provider = this.provider; return this.getAddress().then(function(address) { return provider.getBalance(address, blockTag); }); }); utils.defineProperty(JsonRpcSigner.prototype, 'getTransactionCount', function(blockTag) { var provider = this.provider; return this.getAddress().then(function(address) { return provider.getTransactionCount(address, blockTag); }); }); utils.defineProperty(JsonRpcSigner.prototype, 'sendTransaction', function(transaction) { var provider = this.provider; transaction = getTransaction(transaction); return this.getAddress().then(function(address) { transaction.from = address.toLowerCase(); return provider.send('eth_sendTransaction', [ transaction ]).then(function(hash) { return new Promise(function(resolve, reject) { function check() { provider.getTransaction(hash).then(function(transaction) { if (!transaction) { setTimeout(check, 1000); return; } transaction.wait = function() { return provider.waitForTransaction(hash); }; resolve(transaction); }); } check(); }); }); }); }); utils.defineProperty(JsonRpcSigner.prototype, 'signMessage', function(message) { var provider = this.provider; var data = ((typeof(message) === 'string') ? utils.toUtf8Bytes(message): message); return this.getAddress().then(function(address) { // https://github.com/ethereum/wiki/wiki/JSON-RPC#eth_sign return provider.send('eth_sign', [ address.toLowerCase(), utils.hexlify(data) ]); }); }); utils.defineProperty(JsonRpcSigner.prototype, 'unlock', function(password) { var provider = this.provider; return this.getAddress().then(function(address) { return provider.send('personal_unlockAccount', [ address.toLowerCase(), password, null ]); }); }); function JsonRpcProvider(url, network) { errors.checkNew(this, JsonRpcProvider); if (arguments.length == 1) { if (typeof(url) === 'string') { try { network = Provider.getNetwork(url); url = null; } catch (error) { } } else if (url && url.url == null) { network = url; url = null; } } Provider.call(this, network); if (!url) { url = 'http://localhost:8545'; } utils.defineProperty(this, 'url', url); } Provider.inherits(JsonRpcProvider); utils.defineProperty(JsonRpcProvider.prototype, 'getSigner', function(address) { return new JsonRpcSigner(this, address); }); utils.defineProperty(JsonRpcProvider.prototype, 'listAccounts', function() { return this.send('eth_accounts', []).then(function(accounts) { accounts.forEach(function(address, index) { accounts[index] = utils.getAddress(address); }); return accounts; }); }); 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', [getLowerCase(params.address), params.blockTag]); case 'getTransactionCount': return this.send('eth_getTransactionCount', [getLowerCase(params.address), params.blockTag]); case 'getCode': return this.send('eth_getCode', [getLowerCase(params.address), params.blockTag]); case 'getStorageAt': return this.send('eth_getStorageAt', [getLowerCase(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), 'latest']); case 'estimateGas': return this.send('eth_estimateGas', [getTransaction(params.transaction)]); case 'getLogs': if (params.filter && params.filter.address != null) { params.filter.address = getLowerCase(params.filter.address); } return this.send('eth_getLogs', [params.filter]); default: break; } return Promise.reject(new Error('not implemented - ' + method)); }); utils.defineProperty(JsonRpcProvider.prototype, '_startPending', function() { if (this._pendingFilter != null) { return; } var self = this; var pendingFilter = this.send('eth_newPendingTransactionFilter', []); this._pendingFilter = pendingFilter; pendingFilter.then(function(filterId) { function poll() { self.send('eth_getFilterChanges', [ filterId ]).then(function(hashes) { if (self._pendingFilter != pendingFilter) { return; } var seq = Promise.resolve(); hashes.forEach(function(hash) { self._emitted['t:' + hash.toLowerCase()] = 'pending'; seq = seq.then(function() { return self.getTransaction(hash).then(function(tx) { self.emit('pending', tx); }); }); }); return seq.then(function() { return timer(1000); }); }).then(function() { if (self._pendingFilter != pendingFilter) { self.send('eth_uninstallFilter', [ filterIf ]); return; } setTimeout(function() { poll(); }, 0); }); } poll(); return filterId; }); }); utils.defineProperty(JsonRpcProvider.prototype, '_stopPending', function() { this._pendingFilter = null; }); utils.defineProperty(JsonRpcProvider, '_hexlifyTransaction', function(transaction) { return getTransaction(transaction); }); module.exports = JsonRpcProvider;