diff --git a/contracts/interface.js b/contracts/interface.js index 0d6d3a7a9..366f93ca3 100644 --- a/contracts/interface.js +++ b/contracts/interface.js @@ -383,11 +383,13 @@ function Interface(abi) { var outputNames = getKeys(method.outputs, 'name', true); } + var signature = method.name + '(' + getKeys(method.inputs, 'type').join(',') + ')'; + var sighash = utils.keccak256(utils.toUtf8Bytes(signature)).substring(0, 10); var func = function() { - var signature = method.name + '(' + getKeys(method.inputs, 'type').join(',') + ')'; var result = { name: method.name, signature: signature, + sighash: sighash }; var params = Array.prototype.slice.call(arguments, 0); @@ -398,9 +400,7 @@ function Interface(abi) { throwError('too many parameters'); } - signature = utils.keccak256(utils.toUtf8Bytes(signature)).substring(0, 10); - - result.data = signature + Interface.encodeParams(inputTypes, params).substring(2); + result.data = sighash + Interface.encodeParams(inputTypes, params).substring(2); if (method.constant) { result.parse = function(data) { return Interface.decodeParams( @@ -417,6 +417,8 @@ function Interface(abi) { defineFrozen(func, 'inputs', getKeys(method.inputs, 'name')); defineFrozen(func, 'outputs', getKeys(method.outputs, 'name')); + utils.defineProperty(func, 'signature', signature); + utils.defineProperty(func, 'sighash', sighash); return func; })(); @@ -487,7 +489,9 @@ function Interface(abi) { }; return populateDescription(new EventDescription(), result); } + defineFrozen(func, 'inputs', getKeys(method.inputs, 'name')); + return func; })(); diff --git a/contracts/package.json b/contracts/package.json index a97b0fb80..df91a847d 100644 --- a/contracts/package.json +++ b/contracts/package.json @@ -1,6 +1,6 @@ { "name": "ethers-contracts", - "version": "2.1.2", + "version": "2.1.3", "description": "Contract and Interface (ABI) library for Ethereum.", "bugs": { "url": "http://github.com/ethers-io/ethers.js/issues", diff --git a/index.js b/index.js index db69954c5..70f4e4abc 100644 --- a/index.js +++ b/index.js @@ -14,6 +14,7 @@ module.exports = { Contract: contracts.Contract, Interface: contracts.Interface, + networks: providers.networks, providers: providers, utils: utils, diff --git a/providers/etherscan-provider.js b/providers/etherscan-provider.js index 712a39e13..99b0474bb 100644 --- a/providers/etherscan-provider.js +++ b/providers/etherscan-provider.js @@ -11,17 +11,48 @@ var utils = (function() { }; })(); +// @TODO: Add this to utils; lots of things need this now +function stripHexZeros(value) { + while (value.length > 3 && value.substring(0, 3) === '0x0') { + value = '0x' + value.substring(3); + } + return value; +} + function getTransactionString(transaction) { var result = []; for (var key in transaction) { if (transaction[key] == null) { continue; } - result.push(key + '=' + utils.hexlify(transaction[key])); + var value = utils.hexlify(transaction[key]); + if ({ gasLimit: true, gasPrice: true, nonce: true, value: true }[key]) { + value = stripHexZeros(value); + } + result.push(key + '=' + value); } return result.join('&'); } -function EtherscanProvider(testnet, apiKey) { - Provider.call(this, testnet); +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); } @@ -72,10 +103,11 @@ function checkLogTag(blockTag) { return parseInt(blockTag.substring(2), 16); } + utils.defineProperty(EtherscanProvider.prototype, 'perform', function(method, params) { if (!params) { params = {}; } - var url = this.testnet ? 'https://ropsten.etherscan.io': 'https://api.etherscan.io'; + var url = this.baseUrl; var apiKey = ''; if (this.apiKey) { apiKey += '&apikey=' + this.apiKey; } @@ -108,8 +140,8 @@ utils.defineProperty(EtherscanProvider.prototype, 'perform', function(method, pa case 'getStorageAt': url += '/api?module=proxy&action=eth_getStorageAt&address=' + params.address; - url += '&position=' + params.position; - url += '&tag=' + params.blockTag + apiKey; + url += '&position=' + stripHexZeros(params.position); + url += '&tag=' + stripHexZeros(params.blockTag) + apiKey; return Provider.fetchJSON(url, null, getJsonResult); case 'sendTransaction': @@ -120,7 +152,7 @@ utils.defineProperty(EtherscanProvider.prototype, 'perform', function(method, pa case 'getBlock': if (params.blockTag) { - url += '/api?module=proxy&action=eth_getBlockByNumber&tag=' + params.blockTag; + url += '/api?module=proxy&action=eth_getBlockByNumber&tag=' + stripHexZeros(params.blockTag); url += '&boolean=false'; url += apiKey; return Provider.fetchJSON(url, null, getJsonResult); diff --git a/providers/index.js b/providers/index.js index 403484b6f..fe4ae90f5 100644 --- a/providers/index.js +++ b/providers/index.js @@ -7,10 +7,10 @@ var FallbackProvider = require('./fallback-provider.js'); var InfuraProvider = require('./infura-provider.js'); var JsonRpcProvider = require('./json-rpc-provider.js'); -function getDefaultProvider(testnet) { +function getDefaultProvider(network) { return new FallbackProvider([ - new InfuraProvider(testnet), - new EtherscanProvider(testnet), + new InfuraProvider(network), + new EtherscanProvider(network), ]); } @@ -20,7 +20,9 @@ module.exports = { InfuraProvider: InfuraProvider, JsonRpcProvider: JsonRpcProvider, - isProvder: Provider.isProvider, + isProvider: Provider.isProvider, + + networks: Provider.networks, getDefaultProvider:getDefaultProvider, diff --git a/providers/infura-provider.js b/providers/infura-provider.js index 34cc685ab..c58e8d952 100644 --- a/providers/infura-provider.js +++ b/providers/infura-provider.js @@ -1,4 +1,7 @@ -var JsonRpcProvider = require('./json-rpc-provider.js'); +'use strict'; + +var Provider = require('./provider'); +var JsonRpcProvider = require('./json-rpc-provider'); var utils = (function() { return { @@ -6,13 +9,40 @@ var utils = (function() { } })(); -function InfuraProvider(testnet, apiAccessToken) { +function InfuraProvider(network, apiAccessToken) { if (!(this instanceof InfuraProvider)) { throw new Error('missing new'); } - var host = (testnet ? "ropsten": "mainnet") + '.infura.io'; + // Legacy constructor (testnet, chainId, apiAccessToken) + // @TODO: Remove this in the next major release + if (arguments.length === 3) { + apiAccessToken = arguments[2]; + network = Provider._legacyConstructor(network, 2, arguments[0], arguments[1]); + } else { + apiAccessToken = null; + network = Provider._legacyConstructor(network, arguments.length, arguments[0], arguments[1]); + } + + var host = null; + switch(network.name) { + case 'homestead': + host = 'mainnet.infura.io'; + break; + case 'ropsten': + host = 'ropsten.infura.io'; + break; + case 'rinkeby': + host = 'rinkeby.infura.io'; + break; + case 'kovan': + host = 'kovan.infura.io'; + break; + default: + throw new Error('unsupported network'); + } + var url = 'https://' + host + '/' + (apiAccessToken || ''); - JsonRpcProvider.call(this, url, testnet); + JsonRpcProvider.call(this, url, network); utils.defineProperty(this, 'apiAccessToken', apiAccessToken || null); } diff --git a/providers/json-rpc-provider.js b/providers/json-rpc-provider.js index 9635905ff..dcc356340 100644 --- a/providers/json-rpc-provider.js +++ b/providers/json-rpc-provider.js @@ -48,10 +48,12 @@ function getTransaction(transaction) { return result; } -function JsonRpcProvider(url, testnet, chainId) { +function JsonRpcProvider(url, network) { if (!(this instanceof JsonRpcProvider)) { throw new Error('missing new'); } - Provider.call(this, testnet, chainId); + network = Provider._legacyConstructor(network, arguments.length - 1, arguments[1], arguments[2]); + + Provider.call(this, network); if (!url) { url = 'http://localhost:8545'; } diff --git a/providers/networks.json b/providers/networks.json new file mode 100644 index 000000000..bb7d9049b --- /dev/null +++ b/providers/networks.json @@ -0,0 +1,42 @@ +{ + "unspecified": { + "chainId": 0, + "name": "unspecified" + }, + + "homestead": { + "chainId": 1, + "ensAddress": "0x314159265dd8dbb310642f98f50c066173c1259b", + "name": "homestead" + }, + "mainnet": { + "chainId": 1, + "ensAddress": "0x314159265dd8dbb310642f98f50c066173c1259b", + "name": "homestead" + }, + + "morden": { + "chainId": 2, + "name": "morden" + }, + + "ropsten": { + "chainId": 3, + "ensAddress": "0x112234455c3a32fd11230c42e7bccd4a84e02010", + "name": "ropsten" + }, + "testnet": { + "chainId": 3, + "ensAddress": "0x112234455c3a32fd11230c42e7bccd4a84e02010", + "name": "ropsten" + }, + + "rinkeby": { + "chainId": 4, + "name": "rinkeby" + }, + "kovan": { + "chainId": 42, + "name": "kovan" + } +} diff --git a/providers/provider.js b/providers/provider.js index 776513328..0de85bf93 100644 --- a/providers/provider.js +++ b/providers/provider.js @@ -4,6 +4,8 @@ var inherits = require('inherits'); var XMLHttpRequest = require('xmlhttprequest').XMLHttpRequest; +var networks = require('./networks.json'); + var utils = (function() { var convert = require('ethers-utils/convert'); return { @@ -127,8 +129,8 @@ var formatBlock = { number: checkNumber, timestamp: checkNumber, - nonce: utils.hexlify, - difficulty: checkNumber, + nonce: allowNull(utils.hexlify), + difficulty: allowNull(checkNumber), gasLimit: utils.bigNumberify, gasUsed: utils.bigNumberify, @@ -326,25 +328,24 @@ function checkLog(log) { return check(formatLog, log); } -var ensAddressTestnet = '0x112234455c3a32fd11230c42e7bccd4a84e02010'; -var ensAddressMainnet = '0x314159265dd8dbb310642f98f50c066173c1259b'; - -function Provider(testnet, chainId) { +function Provider(network) { if (!(this instanceof Provider)) { throw new Error('missing new'); } - testnet = !!testnet; + network = Provider._legacyConstructor(network, arguments.length, arguments[0], arguments[1]); - if (chainId == null) { - chainId = (testnet ? Provider.chainId.ropsten: Provider.chainId.homestead); + // Check the ensAddress (if any) + var ensAddress = null; + if (network.ensAddress) { + ensAddress = utils.getAddress(network.ensAddress); } - // Figure out which ENS to talk to - this.ensAddress = (testnet ? ensAddressTestnet: ensAddressMainnet); + // Setup our network properties + utils.defineProperty(this, 'chainId', network.chainId); + utils.defineProperty(this, 'ensAddress', ensAddress); + utils.defineProperty(this, 'name', network.name); - if (typeof(chainId) !== 'number') { throw new Error('invalid chainId'); } - - utils.defineProperty(this, 'testnet', testnet); - utils.defineProperty(this, 'chainId', chainId); + // @TODO: Remove in the next major release + utils.defineProperty(this, 'testnet', (network.name !== 'homestead')); var events = {}; utils.defineProperty(this, '_events', events); @@ -452,6 +453,35 @@ function(child) { } }); */ + +utils.defineProperty(Provider, '_legacyConstructor', function(network, length, arg0, arg1) { + + // Legacy parameters Provider(testnet:boolean, chainId:Number) + if (typeof(arg0) === 'boolean' || length === 2) { + var testnet = !!arg0; + var chainId = arg1; + + // true => testnet, false => mainnet + network = networks[testnet ? 'ropsten': 'homestead']; + + // Overriding chain ID + if (length === 2 && chainId != null) { + network = { + chainId: chainId, + ensAddress: network.ensAddress, + name: network.name + }; + } + + } else if (typeof(network) === 'string') { + network = networks[network]; + if (!network) { throw new Error('unknown network'); } + } + + if (typeof(network.chainId) !== 'number') { throw new Error('invalid chainId'); } + + return network; +}); utils.defineProperty(Provider, 'chainId', { homestead: 1, morden: 2, @@ -462,6 +492,8 @@ utils.defineProperty(Provider, 'chainId', { // return (object instanceof Provider); //}); +utils.defineProperty(Provider, 'networks', networks); + utils.defineProperty(Provider, 'fetchJSON', function(url, json, processFunc) { return new Promise(function(resolve, reject) { diff --git a/tests/run-providers.js b/tests/run-providers.js index 748d32a59..70cead282 100644 --- a/tests/run-providers.js +++ b/tests/run-providers.js @@ -10,7 +10,7 @@ var providers = require('../providers'); var contracts = require('../contracts'); -var TestContracts = require('./tests/test-contract.json'); +var TestContracts = require('./test-contract.json'); var TestContract = TestContracts.test; var TestContractDeploy = TestContracts.deploy; diff --git a/tests/test-providers.js b/tests/test-providers.js new file mode 100644 index 000000000..4b6b61dc5 --- /dev/null +++ b/tests/test-providers.js @@ -0,0 +1,155 @@ +'use strict'; + +var assert = require('assert'); + +var providers = require('../providers'); +var bigNumberify = require('../utils/bignumber').bigNumberify; + +var blockchainData = { + homestead: { + balance: { + address: '0xAC1639CF97a3A46D431e6d1216f576622894cBB5', + balance: bigNumberify('4918774100000000') + }, + block3: { + hash: '0x3d6122660cc824376f11ee842f83addc3525e2dd6756b9bcf0affa6aa88cf741', + parentHash: '0xb495a1d7e6663152ae92708da4843337b958146015a2802f4193a410044698c9', + number: 3, + timestamp: 1438270048, + nonce: '0x2e9344e0cbde83ce', + difficulty: 17154715646, + gasLimit: bigNumberify('0x1388'), + gasUsed: bigNumberify('0'), + miner: '0x5088D623ba0fcf0131E0897a91734A4D83596AA0', + extraData: '0x476574682f76312e302e302d66633739643332642f6c696e75782f676f312e34', + transactions: [] + }, + }, + kovan: { + balance: { + address: '0x09c967A0385eE3B3717779738cA0B9D116e0EcE7', + balance: bigNumberify('997787946734641021') + }, + block3: { + hash: '0xf0ec9bf41b99a6bd1f6cd29f91302f71a1a82d14634d2e207edea4b7962f3676', + parentHash: '0xf110ecd84454f116e2222378e7bca81ac3e59be0dac96d7ec56d5ef1c3bc1d64', + number: 3, + timestamp: 1488459452, + difficulty: 131072, + gasLimit: bigNumberify('0x5b48ec'), + gasUsed: bigNumberify('0'), + miner: '0x00A0A24b9f0E5EC7Aa4c7389b8302fd0123194dE', + extraData: '0xd5830105048650617269747986312e31352e31826c69', + transactions: [] + }, + }, + rinkeby: { + balance: { + address: '0xd09a624630a656a7dbb122cb05e41c12c7cd8c0e', + balance: bigNumberify('3000000000000000000') + }, + block3: { + hash: '0x9eb9db9c3ec72918c7db73ae44e520139e95319c421ed6f9fc11fa8dd0cddc56', + parentHash: '0x9b095b36c15eaf13044373aef8ee0bd3a382a5abb92e402afa44b8249c3a90e9', + number: 3, + timestamp: 1492010489, + nonce: '0x0000000000000000', + difficulty: 2, + gasLimit: bigNumberify('0x47e7c4'), + gasUsed: bigNumberify(0), + miner: '0x0000000000000000000000000000000000000000', + extraData: '0xd783010600846765746887676f312e372e33856c696e757800000000000000004e10f96536e45ceca7e34cc1bdda71db3f3bb029eb69afd28b57eb0202c0ec0859d383a99f63503c4df9ab6c1dc63bf6b9db77be952f47d86d2d7b208e77397301', + transactions: [] + }, + }, + ropsten: { + balance: { + address: '0x03a6F7a5ce5866d9A0CCC1D4C980b8d523f80480', + balance: bigNumberify('21991148575128552666') + }, + block3: { + hash: '0xaf2f2d55e6514389bcc388ccaf40c6ebf7b3814a199a214f1203fb674076e6df', + parentHash: '0x88e8bc1dd383672e96d77ee247e7524622ff3b15c337bd33ef602f15ba82d920', + number: 3, + timestamp: 1479642588, + nonce: '0x04668f72247a130c', + difficulty: 996427, + gasLimit: bigNumberify('0xff4033'), + gasUsed: bigNumberify('0'), + miner: '0xD1aEb42885A43b72B518182Ef893125814811048', + extraData: '0xd883010503846765746887676f312e372e318664617277696e', + transactions: [] + }, + }, +} + +function equals(name, actual, expected) { + if (expected.eq) { + assert.ok(expected.eq(actual), name + ' matches'); + + } else if (Array.isArray(expected)) { + assert.equal(actual.length, expected.length, name + ' array lengths match'); + for (var i = 0; i < expected.length; i++) { + equals(name + ' item ' + i, actual[i], expected[i]); + } + + } else { + assert.equal(actual, expected, name + ' matches'); + } +} + +function testProvider(providerName, networkName) { + describe(('Read-Only ' + providerName + ' (' + networkName + ')'), function() { + var provider = new providers[providerName](networkName); + + it('fetches block #3', function() { + this.timeout(20000); + var test = blockchainData[networkName].block3; + return provider.getBlock(3).then(function(block) { + for (var key in test) { + equals('Block ' + key, block[key], test[key]); + } + }); + }); + + it('fetches address balance', function() { + // @TODO: These tests could be fiddled with if someone sends ether to our address + // We should set up a contract on each network like: + // + // contract TestBalance { + // function resetBalance() { + // assert(_owner.send(this.balance - 0.0000314159 ether)); + // } + // } + this.timeout(20000); + var test = blockchainData[networkName].balance; + return provider.getBalance(test.address).then(function(balance) { + equals('Balance', test.balance, balance); + }); + }); + + // Obviously many more cases to add here + // - getTransactionCount + // - getCode + // - getStorageAt + // - getBlockNumber + // - getGasPrice + // - estimateGas + // - sendTransaction + // - getTransaction + // - getTransactionReceipt + // - call + // - getLogs + // + // Many of these are tested in run-providers, which uses nodeunit, but + // also creates a local private key which must then be funded to + // execute the tests. I am working on a better test contract to deploy + // to all the networks to help test these. + }); +} + +['homestead', 'ropsten', 'rinkeby', 'kovan'].forEach(function(networkName) { + ['InfuraProvider', 'EtherscanProvider'].forEach(function(providerName) { + testProvider(providerName, networkName); + }); +});