diff options
Get basic ENS working
& merge master into ENS branch.
32 files changed, 519 insertions, 168 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md index d07154bdf..c0ba9b0b0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,8 +2,13 @@ ## Current Master +## 3.4.0 2017-3-8 + +- Add two most recently used custom RPCs to network dropdown menu. - Add personal_sign method support. +- Add personal_ecRecover method support. - Add ability to customize gas and gasPrice on the transaction approval screen. +- Increase default gas buffer to 1.5x estimated gas value. ## 3.3.0 2017-2-20 diff --git a/app/manifest.json b/app/manifest.json index f8a08363b..910a5701e 100644 --- a/app/manifest.json +++ b/app/manifest.json @@ -1,7 +1,7 @@ { "name": "MetaMask", "short_name": "Metamask", - "version": "3.3.0", + "version": "3.4.0", "manifest_version": 2, "author": "https://metamask.io", "description": "Ethereum Browser Extension", diff --git a/app/scripts/lib/controllers/currency.js b/app/scripts/controllers/currency.js index c4904f8ac..c4904f8ac 100644 --- a/app/scripts/lib/controllers/currency.js +++ b/app/scripts/controllers/currency.js diff --git a/app/scripts/controllers/preferences.js b/app/scripts/controllers/preferences.js new file mode 100644 index 000000000..18fccf11b --- /dev/null +++ b/app/scripts/controllers/preferences.js @@ -0,0 +1,63 @@ +const ObservableStore = require('obs-store') +const normalizeAddress = require('eth-sig-util').normalize +const extend = require('xtend') + +class PreferencesController { + + constructor (opts = {}) { + const initState = extend({ frequentRpcList: [] }, opts.initState) + this.store = new ObservableStore(initState) + } + + // + // PUBLIC METHODS + // + + setSelectedAddress (_address) { + return new Promise((resolve, reject) => { + const address = normalizeAddress(_address) + this.store.updateState({ selectedAddress: address }) + resolve() + }) + } + + getSelectedAddress (_address) { + return this.store.getState().selectedAddress + } + + updateFrequentRpcList (_url) { + return this.addToFrequentRpcList(_url) + .then((rpcList) => { + this.store.updateState({ frequentRpcList: rpcList }) + return Promise.resolve() + }) + } + + addToFrequentRpcList (_url) { + let rpcList = this.getFrequentRpcList() + let index = rpcList.findIndex((element) => { return element === _url }) + if (index !== -1) { + rpcList.splice(index, 1) + } + if (_url !== 'http://localhost:8545') { + rpcList.push(_url) + } + if (rpcList.length > 2) { + rpcList.shift() + } + return Promise.resolve(rpcList) + } + + getFrequentRpcList () { + return this.store.getState().frequentRpcList + } + + // + // PRIVATE METHODS + // + + + +} + +module.exports = PreferencesController diff --git a/app/scripts/lib/controllers/shapeshift.js b/app/scripts/controllers/shapeshift.js index 3d955c01f..3d955c01f 100644 --- a/app/scripts/lib/controllers/shapeshift.js +++ b/app/scripts/controllers/shapeshift.js diff --git a/app/scripts/keyring-controller.js b/app/scripts/keyring-controller.js index e1b1c4335..72f613641 100644 --- a/app/scripts/keyring-controller.js +++ b/app/scripts/keyring-controller.js @@ -164,8 +164,11 @@ class KeyringController extends EventEmitter { return keyring.getAccounts() }) .then((accounts) => { + return this.checkForDuplicate(type, accounts) + }) + .then((checkedAccounts) => { this.keyrings.push(keyring) - return this.setupAccounts(accounts) + return this.setupAccounts(checkedAccounts) }) .then(() => this.persistAllKeyrings()) .then(() => this.fullUpdate()) @@ -175,6 +178,24 @@ class KeyringController extends EventEmitter { }) } + // For now just checks for simple key pairs + // but in the future + // should possibly add HD and other types + // + checkForDuplicate (type, newAccount) { + return this.getAccounts() + .then((accounts) => { + switch (type) { + case 'Simple Key Pair': + let isNotIncluded = !accounts.find((key) => key === newAccount[0] || key === ethUtil.stripHexPrefix(newAccount[0])) + return (isNotIncluded) ? Promise.resolve(newAccount) : Promise.reject(new Error('The account you\'re are trying to import is a duplicate')) + default: + return Promise.resolve(newAccount) + } + }) + } + + // Add New Account // @number keyRingNum // diff --git a/app/scripts/lib/controllers/preferences.js b/app/scripts/lib/controllers/preferences.js deleted file mode 100644 index c5e93a5b9..000000000 --- a/app/scripts/lib/controllers/preferences.js +++ /dev/null @@ -1,33 +0,0 @@ -const ObservableStore = require('obs-store') -const normalizeAddress = require('eth-sig-util').normalize - -class PreferencesController { - - constructor (opts = {}) { - const initState = opts.initState || {} - this.store = new ObservableStore(initState) - } - - // - // PUBLIC METHODS - // - - setSelectedAddress(_address) { - return new Promise((resolve, reject) => { - const address = normalizeAddress(_address) - this.store.updateState({ selectedAddress: address }) - resolve() - }) - } - - getSelectedAddress(_address) { - return this.store.getState().selectedAddress - } - - // - // PRIVATE METHODS - // - -} - -module.exports = PreferencesController diff --git a/app/scripts/lib/personal-message-manager.js b/app/scripts/lib/personal-message-manager.js index 3b8510767..bbc978446 100644 --- a/app/scripts/lib/personal-message-manager.js +++ b/app/scripts/lib/personal-message-manager.js @@ -2,6 +2,7 @@ const EventEmitter = require('events') const ObservableStore = require('obs-store') const ethUtil = require('ethereumjs-util') const createId = require('./random-id') +const hexRe = /^[0-9A-Fa-f]+$/g module.exports = class PersonalMessageManager extends EventEmitter{ @@ -24,7 +25,8 @@ module.exports = class PersonalMessageManager extends EventEmitter{ } addUnapprovedMessage (msgParams) { - msgParams.data = normalizeMsgData(msgParams.data) + log.debug(`PersonalMessageManager addUnapprovedMessage: ${JSON.stringify(msgParams)}`) + msgParams.data = this.normalizeMsgData(msgParams.data) // create txData obj with parameters and meta data var time = (new Date()).getTime() var msgId = createId() @@ -106,14 +108,18 @@ module.exports = class PersonalMessageManager extends EventEmitter{ this.emit('updateBadge') } -} + normalizeMsgData(data) { + try { + const stripped = ethUtil.stripHexPrefix(data) + if (stripped.match(hexRe)) { + return ethUtil.addHexPrefix(stripped) + } + } catch (e) { + log.debug(`Message was not hex encoded, interpreting as utf8.`) + } -function normalizeMsgData(data) { - if (data.slice(0, 2) === '0x') { - // data is already hex - return data - } else { - // data is unicode, convert to hex return ethUtil.bufferToHex(new Buffer(data, 'utf8')) } + } + diff --git a/app/scripts/lib/tx-utils.js b/app/scripts/lib/tx-utils.js index 19a2d430e..c6814c05f 100644 --- a/app/scripts/lib/tx-utils.js +++ b/app/scripts/lib/tx-utils.js @@ -53,26 +53,23 @@ module.exports = class txProviderUtils { } // if gasLimit not originally specified, // try adding an additional gas buffer to our estimation for safety - const estimatedGasBn = new BN(ethUtil.stripHexPrefix(txData.estimatedGas), 16) - const blockGasLimitBn = new BN(ethUtil.stripHexPrefix(blockGasLimitHex), 16) - const estimationWithBuffer = new BN(this.addGasBuffer(estimatedGasBn), 16) - // added gas buffer is too high - if (estimationWithBuffer.gt(blockGasLimitBn)) { - txParams.gas = txData.estimatedGas - // added gas buffer is safe - } else { - const gasWithBufferHex = ethUtil.intToHex(estimationWithBuffer) - txParams.gas = gasWithBufferHex - } + const recommendedGasHex = this.addGasBuffer(txData.estimatedGas, blockGasLimitHex) + txParams.gas = recommendedGasHex cb() return } - addGasBuffer (gas) { - const gasBuffer = new BN('100000', 10) - const bnGas = new BN(ethUtil.stripHexPrefix(gas), 16) - const correct = bnGas.add(gasBuffer) - return ethUtil.addHexPrefix(correct.toString(16)) + addGasBuffer (initialGasLimitHex, blockGasLimitHex) { + const initialGasLimitBn = hexToBn(initialGasLimitHex) + const blockGasLimitBn = hexToBn(blockGasLimitHex) + const bufferedGasLimitBn = initialGasLimitBn.muln(1.5) + + // if initialGasLimit is above blockGasLimit, dont modify it + if (initialGasLimitBn.gt(blockGasLimitBn)) return bnToHex(initialGasLimitBn) + // if bufferedGasLimit is below blockGasLimit, use bufferedGasLimit + if (bufferedGasLimitBn.lt(blockGasLimitBn)) return bnToHex(bufferedGasLimitBn) + // otherwise use blockGasLimit + return bnToHex(blockGasLimitBn) } fillInTxParams (txParams, cb) { @@ -94,7 +91,7 @@ module.exports = class txProviderUtils { // builds ethTx from txParams object buildEthTxFromParams (txParams) { // apply gas multiplyer - let gasPrice = new BN(ethUtil.stripHexPrefix(txParams.gasPrice), 16) + let gasPrice = hexToBn(txParams.gasPrice) // multiply and divide by 100 so as to add percision to integer mul txParams.gasPrice = ethUtil.intToHex(gasPrice.toNumber()) // normalize values @@ -130,3 +127,11 @@ module.exports = class txProviderUtils { function isUndef(value) { return value === undefined } + +function bnToHex(inputBn) { + return ethUtil.addHexPrefix(inputBn.toString(16)) +} + +function hexToBn(inputHex) { + return new BN(ethUtil.stripHexPrefix(inputHex), 16) +}
\ No newline at end of file diff --git a/app/scripts/metamask-controller.js b/app/scripts/metamask-controller.js index bd01a260d..536891dc6 100644 --- a/app/scripts/metamask-controller.js +++ b/app/scripts/metamask-controller.js @@ -11,10 +11,10 @@ const streamIntoProvider = require('web3-stream-provider/handler') const MetaMaskProvider = require('web3-provider-engine/zero.js') const setupMultiplex = require('./lib/stream-utils.js').setupMultiplex const KeyringController = require('./keyring-controller') -const PreferencesController = require('./lib/controllers/preferences') -const CurrencyController = require('./lib/controllers/currency') +const PreferencesController = require('./controllers/preferences') +const CurrencyController = require('./controllers/currency') const NoticeController = require('./notice-controller') -const ShapeShiftController = require('./lib/controllers/shapeshift') +const ShapeShiftController = require('./controllers/shapeshift') const MessageManager = require('./lib/message-manager') const PersonalMessageManager = require('./lib/personal-message-manager') const TxManager = require('./transaction-manager') @@ -244,7 +244,6 @@ module.exports = class MetamaskController extends EventEmitter { return { // etc getState: (cb) => cb(null, this.getState()), - setRpcTarget: this.setRpcTarget.bind(this), setProviderType: this.setProviderType.bind(this), useEtherscanProvider: this.useEtherscanProvider.bind(this), setCurrentCurrency: this.setCurrentCurrency.bind(this), @@ -265,6 +264,8 @@ module.exports = class MetamaskController extends EventEmitter { // PreferencesController setSelectedAddress: nodeify(preferencesController.setSelectedAddress).bind(preferencesController), + setDefaultRpc: nodeify(this.setDefaultRpc).bind(this), + setCustomRpc: nodeify(this.setCustomRpc).bind(this), // KeyringController setLocked: nodeify(keyringController.setLocked).bind(keyringController), @@ -414,14 +415,14 @@ module.exports = class MetamaskController extends EventEmitter { self.sendUpdate() self.opts.showUnapprovedTx(txMeta) // listen for tx completion (success, fail) - self.txManager.once(`${txMeta.id}:finished`, (status) => { - switch (status) { + self.txManager.once(`${txMeta.id}:finished`, (completedTx) => { + switch (completedTx.status) { case 'submitted': - return cb(null, txMeta.hash) + return cb(null, completedTx.hash) case 'rejected': return cb(new Error('MetaMask Tx Signature: User denied transaction signature.')) default: - return cb(new Error(`MetaMask Tx Signature: Unknown problem: ${JSON.stringify(txMeta.txParams)}`)) + return cb(new Error(`MetaMask Tx Signature: Unknown problem: ${JSON.stringify(completedTx.txParams)}`)) } }) }) @@ -661,10 +662,21 @@ module.exports = class MetamaskController extends EventEmitter { if (this.isNetworkLoading()) this.lookupNetwork() } - setRpcTarget (rpcTarget) { - this.configManager.setRpcTarget(rpcTarget) + setDefaultRpc () { + this.configManager.setRpcTarget('http://localhost:8545') extension.runtime.reload() this.lookupNetwork() + return Promise.resolve('http://localhost:8545') + } + + setCustomRpc (rpcTarget, rpcList) { + this.configManager.setRpcTarget(rpcTarget) + return this.preferencesController.updateFrequentRpcList(rpcTarget) + .then(() => { + extension.runtime.reload() + this.lookupNetwork() + return Promise.resolve(rpcTarget) + }) } setProviderType (type) { diff --git a/app/scripts/popup-core.js b/app/scripts/popup-core.js index 0c97a5d19..b1e521a7a 100644 --- a/app/scripts/popup-core.js +++ b/app/scripts/popup-core.js @@ -49,12 +49,14 @@ function setupControllerConnection (connectionStream, cb) { } function setupApp (err, accountManager) { + var container = document.getElementById('app-content') if (err) { - alert(err.stack) + container.innerHTML = '<div class="critical-error">The MetaMask app failed to load: please open and close MetaMask again to restart.</div>' + container.style.height = '80px' + log.error(err.stack) throw err } - var container = document.getElementById('app-content') MetaMaskUi({ container: container, diff --git a/app/scripts/transaction-manager.js b/app/scripts/transaction-manager.js index 07c90af7e..c6cfdf11d 100644 --- a/app/scripts/transaction-manager.js +++ b/app/scripts/transaction-manager.js @@ -353,7 +353,7 @@ module.exports = class TransactionManager extends EventEmitter { txMeta.status = status this.emit(`${txMeta.id}:${status}`, txId) if (status === 'submitted' || status === 'rejected') { - this.emit(`${txMeta.id}:finished`, status) + this.emit(`${txMeta.id}:finished`, txMeta) } this.updateTx(txMeta) this.emit('updateBadge') diff --git a/development/states/first-time.json b/development/states/first-time.json index 108af9117..3554ee911 100644 --- a/development/states/first-time.json +++ b/development/states/first-time.json @@ -4,6 +4,7 @@ "isUnlocked": false, "rpcTarget": "https://rawtestrpc.metamask.io/", "identities": {}, + "frequentRpcList": [], "unapprovedTxs": {}, "currentFiat": "USD", "conversionRate": 12.7527416, diff --git a/development/states/send.json b/development/states/send.json index c054af55b..3f52185af 100644 --- a/development/states/send.json +++ b/development/states/send.json @@ -2,73 +2,108 @@ "metamask": { "isInitialized": true, "isUnlocked": true, - "currentDomain": "example.com", "rpcTarget": "https://rawtestrpc.metamask.io/", "identities": { "0xfdea65c8e26263f6d9a1b5de9555d2931a33b825": { - "name": "Wallet 1", "address": "0xfdea65c8e26263f6d9a1b5de9555d2931a33b825", - "mayBeFauceting": false + "name": "Account 1" }, "0xc5b8dbac4c1d3f152cdeb400e2313f309c410acb": { - "name": "Wallet 2", "address": "0xc5b8dbac4c1d3f152cdeb400e2313f309c410acb", - "mayBeFauceting": false + "name": "Account 2" }, "0x2f8d4a878cfa04a6e60d46362f5644deab66572d": { - "name": "Wallet 3", "address": "0x2f8d4a878cfa04a6e60d46362f5644deab66572d", - "mayBeFauceting": false + "name": "Account 3" + }, + "0xd85a4b6a394794842887b8284293d69163007bbb": { + "address": "0xd85a4b6a394794842887b8284293d69163007bbb", + "name": "Account 4" } }, - "unconfTxs": {}, + "unapprovedTxs": {}, "currentFiat": "USD", - "conversionRate": 11.21283484, - "conversionDate": 1472158984, + "conversionRate": 16.88200327, + "conversionDate": 1489013762, + "noActiveNotices": true, + "frequentRpcList": [], + "network": "3", "accounts": { "0xfdea65c8e26263f6d9a1b5de9555d2931a33b825": { "code": "0x", - "balance": "0x34693f54a1e25900", - "nonce": "0x100013", + "balance": "0x47c9d71831c76efe", + "nonce": "0x1b", "address": "0xfdea65c8e26263f6d9a1b5de9555d2931a33b825" }, "0xc5b8dbac4c1d3f152cdeb400e2313f309c410acb": { "code": "0x", - "nonce": "0x100000", - "balance": "0x18af912cee770000", + "balance": "0x37452b1315889f80", + "nonce": "0xa", "address": "0xc5b8dbac4c1d3f152cdeb400e2313f309c410acb" }, "0x2f8d4a878cfa04a6e60d46362f5644deab66572d": { "code": "0x", - "nonce": "0x100000", - "balance": "0x2386f26fc10000", + "balance": "0x0", + "nonce": "0x0", "address": "0x2f8d4a878cfa04a6e60d46362f5644deab66572d" + }, + "0xd85a4b6a394794842887b8284293d69163007bbb": { + "code": "0x", + "balance": "0x0", + "nonce": "0x0", + "address": "0xd85a4b6a394794842887b8284293d69163007bbb" } }, - "transactions": [], - "network": "2", - "seedWords": null, - "unconfMsgs": {}, - "messages": [], - "shapeShiftTxList": [], + "transactions": {}, + "selectedAddressTxList": [], + "unapprovedMsgs": {}, + "unapprovedMsgCount": 0, + "unapprovedPersonalMsgs": {}, + "unapprovedPersonalMsgCount": 0, + "keyringTypes": [ + "Simple Key Pair", + "HD Key Tree" + ], + "keyrings": [ + { + "type": "HD Key Tree", + "accounts": [ + "fdea65c8e26263f6d9a1b5de9555d2931a33b825", + "c5b8dbac4c1d3f152cdeb400e2313f309c410acb", + "2f8d4a878cfa04a6e60d46362f5644deab66572d" + ] + }, + { + "type": "Simple Key Pair", + "accounts": [ + "0xd85a4b6a394794842887b8284293d69163007bbb" + ] + } + ], + "selectedAddress": "0xd85a4b6a394794842887b8284293d69163007bbb", + "currentCurrency": "USD", "provider": { "type": "testnet" }, - "selectedAddress": "0xfdea65c8e26263f6d9a1b5de9555d2931a33b825" + "shapeShiftTxList": [], + "lostAccounts": [] }, "appState": { "menuOpen": false, "currentView": { - "name": "sendTransaction" + "name": "sendTransaction", + "context": "0xd85a4b6a394794842887b8284293d69163007bbb" }, "accountDetail": { - "subview": "transactions" + "subview": "transactions", + "accountExport": "none", + "privateKey": "" }, - "currentDomain": "127.0.0.1:9966", "transForward": true, "isLoading": false, "warning": null, - "detailView": {} + "scrollToBottom": false, + "forgottenPassword": null }, "identities": {} } diff --git a/package.json b/package.json index 5a511cc96..a03f8a38d 100644 --- a/package.json +++ b/package.json @@ -56,11 +56,11 @@ "eth-lightwallet": "^2.3.3", "eth-query": "^1.0.3", "eth-sig-util": "^1.1.1", - "eth-simple-keyring": "^1.1.0", - "ethereum-ens": "^0.5.0", + "eth-simple-keyring": "^1.1.1", "ethereumjs-tx": "^1.0.0", "ethereumjs-util": "ethereumjs/ethereumjs-util#ac5d0908536b447083ea422b435da27f26615de9", "ethereumjs-wallet": "^0.6.0", + "ethjs-ens": "^1.0.1", "express": "^4.14.0", "extension-link-enabler": "^1.0.0", "extensionizer": "^1.0.0", @@ -110,7 +110,7 @@ "valid-url": "^1.0.9", "vreme": "^3.0.2", "web3": "0.18.2", - "web3-provider-engine": "^9.1.0", + "web3-provider-engine": "^10.0.1", "web3-stream-provider": "^2.0.6", "xtend": "^4.0.1" }, diff --git a/test/unit/actions/config_test.js b/test/unit/actions/config_test.js index f851e4102..14198fa8a 100644 --- a/test/unit/actions/config_test.js +++ b/test/unit/actions/config_test.js @@ -11,6 +11,7 @@ describe ('config view actions', function() { var initialState = { metamask: { rpcTarget: 'foo', + frequentRpcList: [] }, appState: { currentView: { @@ -32,13 +33,13 @@ describe ('config view actions', function() { it('sets the state.metamask.rpcTarget property of the state to the action.value', function() { const action = { type: actions.SET_RPC_TARGET, - value: 'bar', + value: 'foo', } var result = reducers(initialState, action) assert.equal(result.metamask.provider.type, 'rpc') - assert.equal(result.metamask.provider.rpcTarget, action.value) + assert.equal(result.metamask.provider.rpcTarget, 'foo') }) }) -}) +}) diff --git a/test/unit/components/binary-renderer-test.js b/test/unit/components/binary-renderer-test.js new file mode 100644 index 000000000..3264faddc --- /dev/null +++ b/test/unit/components/binary-renderer-test.js @@ -0,0 +1,25 @@ +var assert = require('assert') +var BinaryRenderer = require('../../../ui/app/components/binary-renderer') + +describe('BinaryRenderer', function() { + + let binaryRenderer + const message = 'Hello, world!' + const buffer = new Buffer(message, 'utf8') + const hex = buffer.toString('hex') + + beforeEach(function() { + binaryRenderer = new BinaryRenderer() + }) + + it('recovers message', function() { + const result = binaryRenderer.hexToText(hex) + assert.equal(result, message) + }) + + + it('recovers message with hex prefix', function() { + const result = binaryRenderer.hexToText('0x' + hex) + assert.equal(result, message) + }) +}) diff --git a/test/unit/currency-controller-test.js b/test/unit/currency-controller-test.js index c57b522c7..dd7fa91e0 100644 --- a/test/unit/currency-controller-test.js +++ b/test/unit/currency-controller-test.js @@ -5,7 +5,7 @@ const assert = require('assert') const extend = require('xtend') const rp = require('request-promise') const nock = require('nock') -const CurrencyController = require('../../app/scripts/lib/controllers/currency') +const CurrencyController = require('../../app/scripts/controllers/currency') describe('config-manager', function() { var currencyController diff --git a/test/unit/notice-controller-test.js b/test/unit/notice-controller-test.js index cf00daeba..73fdb2f2e 100644 --- a/test/unit/notice-controller-test.js +++ b/test/unit/notice-controller-test.js @@ -4,7 +4,7 @@ const rp = require('request-promise') const nock = require('nock') const configManagerGen = require('../lib/mock-config-manager') const NoticeController = require('../../app/scripts/notice-controller') -const STORAGE_KEY = 'metamask-persistance-key' +const STORAGE_KEY = 'metamask-persistence-key' describe('notice-controller', function() { var noticeController diff --git a/test/unit/personal-message-manager-test.js b/test/unit/personal-message-manager-test.js index 657d5e675..f2c01392c 100644 --- a/test/unit/personal-message-manager-test.js +++ b/test/unit/personal-message-manager-test.js @@ -4,7 +4,7 @@ const EventEmitter = require('events') const PersonalMessageManager = require('../../app/scripts/lib/personal-message-manager') -describe('Transaction Manager', function() { +describe('Personal Message Manager', function() { let messageManager beforeEach(function() { @@ -86,4 +86,25 @@ describe('Transaction Manager', function() { assert.equal(messageManager.getMsg('2').status, 'approved') }) }) + + describe('#normalizeMsgData', function() { + it('converts text to a utf8 hex string', function() { + var input = 'hello' + var output = messageManager.normalizeMsgData(input) + assert.equal(output, '0x68656c6c6f', 'predictably hex encoded') + }) + + it('tolerates a hex prefix', function() { + var input = '0x12' + var output = messageManager.normalizeMsgData(input) + assert.equal(output, '0x12', 'un modified') + }) + + it('tolerates normal hex', function() { + var input = '12' + var output = messageManager.normalizeMsgData(input) + assert.equal(output, '0x12', 'adds prefix') + }) + }) + }) diff --git a/test/unit/tx-utils-test.js b/test/unit/tx-utils-test.js new file mode 100644 index 000000000..e57b25e83 --- /dev/null +++ b/test/unit/tx-utils-test.js @@ -0,0 +1,58 @@ +const assert = require('assert') +const ethUtil = require('ethereumjs-util') +const BN = ethUtil.BN + +const TxUtils = require('../../app/scripts/lib/tx-utils') + + +describe('txUtils', function() { + let txUtils + + before(function() { + txUtils = new TxUtils() + }) + + describe('addGasBuffer', function() { + it('multiplies by 1.5, when within block gas limit', function() { + // naive estimatedGas: 0x123fad (~1.2 mil) + const inputHex = '0x123fad' + // dummy gas limit: 0x3d4c52 (4 mil) + const blockGasLimitHex = '0x3d4c52' + const output = txUtils.addGasBuffer(inputHex, blockGasLimitHex) + const inputBn = hexToBn(inputHex) + const outputBn = hexToBn(output) + const expectedBn = inputBn.muln(1.5) + assert(outputBn.eq(expectedBn), 'returns 1.5 the input value') + }) + + it('uses original estimatedGas, when above block gas limit', function() { + // naive estimatedGas: 0x123fad (~1.2 mil) + const inputHex = '0x123fad' + // dummy gas limit: 0x0f4240 (1 mil) + const blockGasLimitHex = '0x0f4240' + const output = txUtils.addGasBuffer(inputHex, blockGasLimitHex) + const inputBn = hexToBn(inputHex) + const outputBn = hexToBn(output) + const expectedBn = hexToBn(inputHex) + assert(outputBn.eq(expectedBn), 'returns the original estimatedGas value') + }) + + it('buffers up to block gas limit', function() { + // naive estimatedGas: 0x123fad (~1.2 mil) + const inputHex = '0x1e8480' + // dummy gas limit: 0x1e8480 (2 mil) + const blockGasLimitHex = '0x1e8480' + const output = txUtils.addGasBuffer(inputHex, blockGasLimitHex) + const inputBn = hexToBn(inputHex) + const outputBn = hexToBn(output) + const expectedBn = hexToBn(blockGasLimitHex) + assert(outputBn.eq(expectedBn), 'returns the block gas limit value') + }) + }) +}) + +// util + +function hexToBn(inputHex) { + return new BN(ethUtil.stripHexPrefix(inputHex), 16) +}
\ No newline at end of file diff --git a/ui/app/actions.js b/ui/app/actions.js index 7f972fb37..d4fd7553b 100644 --- a/ui/app/actions.js +++ b/ui/app/actions.js @@ -112,11 +112,13 @@ var actions = { // config screen SHOW_CONFIG_PAGE: 'SHOW_CONFIG_PAGE', SET_RPC_TARGET: 'SET_RPC_TARGET', + SET_DEFAULT_RPC_TARGET: 'SET_DEFAULT_RPC_TARGET', SET_PROVIDER_TYPE: 'SET_PROVIDER_TYPE', USE_ETHERSCAN_PROVIDER: 'USE_ETHERSCAN_PROVIDER', useEtherscanProvider: useEtherscanProvider, showConfigPage: showConfigPage, setRpcTarget: setRpcTarget, + setDefaultRpcTarget: setDefaultRpcTarget, setProviderType: setProviderType, // loading overlay SHOW_LOADING: 'SHOW_LOADING_INDICATION', @@ -669,12 +671,28 @@ function markAccountsFound() { // config // +// default rpc target refers to localhost:8545 in this instance. +function setDefaultRpcTarget (rpcList) { + log.debug(`background.setDefaultRpcTarget`) + return (dispatch) => { + background.setDefaultRpc((err, result) => { + if (err) { + log.error(err) + return dispatch(self.displayWarning('Had a problem changing networks.')) + } + }) + } +} + function setRpcTarget (newRpc) { log.debug(`background.setRpcTarget`) - background.setRpcTarget(newRpc) - return { - type: actions.SET_RPC_TARGET, - value: newRpc, + return (dispatch) => { + background.setCustomRpc(newRpc, (err, result) => { + if (err) { + log.error(err) + return dispatch(self.displayWarning('Had a problem changing networks!')) + } + }) } } @@ -750,7 +768,7 @@ function exportAccount (address) { dispatch(self.hideLoadingIndication()) if (err) { - console.error(err) + log.error(err) return dispatch(self.displayWarning('Had a problem exporting the account.')) } diff --git a/ui/app/app.js b/ui/app/app.js index 63fab5db8..2bc92b54c 100644 --- a/ui/app/app.js +++ b/ui/app/app.js @@ -58,6 +58,7 @@ function mapStateToProps (state) { forgottenPassword: state.appState.forgottenPassword, lastUnreadNotice: state.metamask.lastUnreadNotice, lostAccounts: state.metamask.lostAccounts, + frequentRpcList: state.metamask.frequentRpcList || [], } } @@ -211,6 +212,7 @@ App.prototype.renderAppBar = function () { App.prototype.renderNetworkDropdown = function () { const props = this.props + const rpcList = props.frequentRpcList const state = this.state || {} const isOpen = state.isNetworkMenuOpen @@ -256,12 +258,13 @@ App.prototype.renderNetworkDropdown = function () { h(DropMenuItem, { label: 'Localhost 8545', closeMenu: () => this.setState({ isNetworkMenuOpen: false }), - action: () => props.dispatch(actions.setRpcTarget('http://localhost:8545')), + action: () => props.dispatch(actions.setDefaultRpcTarget(rpcList)), icon: h('i.fa.fa-question-circle.fa-lg'), activeNetworkRender: props.provider.rpcTarget, }), this.renderCustomOption(props.provider), + this.renderCommonRpc(rpcList, props.provider), props.isUnlocked && h(DropMenuItem, { label: 'Custom RPC', @@ -496,6 +499,12 @@ App.prototype.renderCustomOption = function (provider) { const { rpcTarget, type } = provider if (type !== 'rpc') return null + // Concatenate long URLs + let label = rpcTarget + if (rpcTarget.length > 31) { + label = label.substr(0, 34) + '...' + } + switch (rpcTarget) { case 'http://localhost:8545': @@ -503,10 +512,32 @@ App.prototype.renderCustomOption = function (provider) { default: return h(DropMenuItem, { - label: `${rpcTarget}`, + label, + key: rpcTarget, closeMenu: () => this.setState({ isNetworkMenuOpen: false }), icon: h('i.fa.fa-question-circle.fa-lg'), activeNetworkRender: 'custom', }) } } + +App.prototype.renderCommonRpc = function (rpcList, provider) { + const { rpcTarget } = provider + const props = this.props + + return rpcList.map((rpc) => { + if ((rpc === 'http://localhost:8545') || (rpc === rpcTarget)) { + return null + } else { + return h(DropMenuItem, { + label: rpc, + key: rpc, + closeMenu: () => this.setState({ isNetworkMenuOpen: false }), + action: () => props.dispatch(actions.setRpcTarget(rpc)), + icon: h('i.fa.fa-question-circle.fa-lg'), + activeNetworkRender: rpc, + }) + } + }) + +} diff --git a/ui/app/components/binary-renderer.js b/ui/app/components/binary-renderer.js new file mode 100644 index 000000000..a9d49b128 --- /dev/null +++ b/ui/app/components/binary-renderer.js @@ -0,0 +1,43 @@ +const Component = require('react').Component +const h = require('react-hyperscript') +const inherits = require('util').inherits +const ethUtil = require('ethereumjs-util') + +module.exports = BinaryRenderer + +inherits(BinaryRenderer, Component) +function BinaryRenderer () { + Component.call(this) +} + +BinaryRenderer.prototype.render = function () { + const props = this.props + const { value } = props + const text = this.hexToText(value) + + return ( + h('textarea.font-small', { + readOnly: true, + style: { + width: '315px', + maxHeight: '210px', + resize: 'none', + border: 'none', + background: 'white', + padding: '3px', + }, + defaultValue: text, + }) + ) +} + +BinaryRenderer.prototype.hexToText = function (hex) { + try { + const stripped = ethUtil.stripHexPrefix(hex) + const buff = Buffer.from(stripped, 'hex') + return buff.toString('utf8') + } catch (e) { + return hex + } +} + diff --git a/ui/app/components/ens-input.js b/ui/app/components/ens-input.js index f5edab9fd..f018cc632 100644 --- a/ui/app/components/ens-input.js +++ b/ui/app/components/ens-input.js @@ -3,7 +3,7 @@ const h = require('react-hyperscript') const inherits = require('util').inherits const extend = require('xtend') const debounce = require('debounce') -const ENS = require('ethereum-ens') +const ENS = require('ethjs-ens') const ensRE = /.+\.eth$/ const networkResolvers = { @@ -30,6 +30,8 @@ EnsInput.prototype.render = function () { console.dir(recipient) return this.setState({ loadingEns: false, + ensResolution: null, + ensFailure: null, }) } @@ -53,12 +55,16 @@ EnsInput.prototype.componentDidMount = function () { let resolverAddress = networkResolvers[network] if (resolverAddress) { - this.ens = new ENS(web3, resolverAddress) + const provider = web3.currentProvider + this.ens = new ENS({ provider, network }) this.checkName = debounce(this.lookupEnsName.bind(this), 200) } } EnsInput.prototype.lookupEnsName = function () { + const recipient = document.querySelector('input[name="address"]').value + const { ensResolution } = this.state + if (!this.ens) { return this.setState({ loadingEns: false, @@ -67,17 +73,23 @@ EnsInput.prototype.lookupEnsName = function () { }) } - const recipient = document.querySelector('input[name="address"]').value log.info(`ENS attempting to resolve name: ${recipient}`) - this.ens.resolver(recipient).addr() + this.ens.lookup(recipient.trim()) .then((address) => { - this.setState({ - loadingEns: false, - ensResolution: address, - hoverText: address, - }) + console.log('ens called back with ' + address) + + if (address !== ensResolution) { + this.setState({ + loadingEns: false, + ensResolution: address, + hoverText: address, + }) + } }) .catch((reason) => { + console.log('ens threw error: ' + reason.message) + console.trace(reason) + debugger return this.setState({ loadingEns: false, ensFailure: true, @@ -86,10 +98,12 @@ EnsInput.prototype.lookupEnsName = function () { }) } -EnsInput.prototype.componentDidUpdate = function () { +EnsInput.prototype.componentDidUpdate = function (prevProps, prevState) { const state = this.state || {} const { ensResolution } = state - if (ensResolution && this.props.onChange) { + if (ensResolution && this.props.onChange && + ensResolution !== prevState.ensResolution) { + console.log('Firing on change to parent') this.props.onChange(ensResolution) } } @@ -115,6 +129,7 @@ EnsInput.prototype.ensIconContents = function (recipient) { style: { width: '30px', height: '30px', + transform: 'translateY(-6px)', }, }) } diff --git a/ui/app/components/hex-as-decimal-input.js b/ui/app/components/hex-as-decimal-input.js index 523c1264b..c89ed0416 100644 --- a/ui/app/components/hex-as-decimal-input.js +++ b/ui/app/components/hex-as-decimal-input.js @@ -39,15 +39,17 @@ HexAsDecimalInput.prototype.render = function () { }, }, [ h('input.ether-balance.ether-balance-amount', { + type: 'number', style: extend({ display: 'block', textAlign: 'right', backgroundColor: 'transparent', border: '1px solid #bdbdbd', + }, style), value: decimalValue, onChange: (event) => { - const hexString = hexify(event.target.value) + const hexString = (event.target.value === '') ? '' : hexify(event.target.value) onChange(hexString) }, }), @@ -70,7 +72,11 @@ function hexify (decimalString) { } function decimalize (input, toEth) { - const strippedInput = ethUtil.stripHexPrefix(input) - const inputBN = new BN(strippedInput, 'hex') - return inputBN.toString(10) + if (input === '') { + return '' + } else { + const strippedInput = ethUtil.stripHexPrefix(input) + const inputBN = new BN(strippedInput, 'hex') + return inputBN.toString(10) + } } diff --git a/ui/app/components/pending-personal-msg-details.js b/ui/app/components/pending-personal-msg-details.js index ffd11ca0b..fa2c6416c 100644 --- a/ui/app/components/pending-personal-msg-details.js +++ b/ui/app/components/pending-personal-msg-details.js @@ -3,6 +3,7 @@ const h = require('react-hyperscript') const inherits = require('util').inherits const AccountPanel = require('./account-panel') +const BinaryRenderer = require('./binary-renderer') module.exports = PendingMsgDetails @@ -41,18 +42,7 @@ PendingMsgDetails.prototype.render = function () { // message data h('div', [ h('label.font-small', { style: { display: 'block' } }, 'MESSAGE'), - h('textarea.font-small', { - readOnly: true, - style: { - width: '315px', - maxHeight: '210px', - resize: 'none', - border: 'none', - background: 'white', - padding: '3px', - }, - defaultValue: data, - }), + h(BinaryRenderer, { value: data }), ]), ]) diff --git a/ui/app/components/pending-tx-details.js b/ui/app/components/pending-tx-details.js index b1ab9576b..e92ce575f 100644 --- a/ui/app/components/pending-tx-details.js +++ b/ui/app/components/pending-tx-details.js @@ -32,10 +32,8 @@ PTXP.render = function () { var account = props.accounts[address] var balance = account ? account.balance : '0x0' - const gas = state.gas || txParams.gas - const gasPrice = state.gasPrice || txData.gasPrice - const gasDefault = txParams.gas - const gasPriceDefault = txData.gasPrice + const gas = (state.gas === undefined) ? txParams.gas : state.gas + const gasPrice = (state.gasPrice === undefined) ? txData.gasPrice : state.gasPrice var txFee = state.txFee || txData.txFee || '' var maxCost = state.maxCost || txData.maxCost || '' @@ -131,11 +129,7 @@ PTXP.render = function () { }, onChange: (newHex) => { log.info(`Gas limit changed to ${newHex}`) - if (newHex === '0x0') { - this.setState({gas: gasDefault}) - } else { - this.setState({ gas: newHex }) - } + this.setState({ gas: newHex }) }, }), ]), @@ -155,11 +149,7 @@ PTXP.render = function () { }, onChange: (newHex) => { log.info(`Gas price changed to: ${newHex}`) - if (newHex === '0x0') { - this.setState({gasPrice: gasPriceDefault}) - } else { - this.setState({ gasPrice: newHex }) - } + this.setState({ gasPrice: newHex }) }, }), ]), @@ -316,7 +306,6 @@ PTXP.gatherParams = function () { const state = this.state || {} const txData = state.txData || props.txData const txParams = txData.txParams - const gas = state.gas || txParams.gas const gasPrice = state.gasPrice || txParams.gasPrice const resultTx = extend(txParams, { @@ -330,6 +319,16 @@ PTXP.gatherParams = function () { return resultTxMeta } +PTXP.verifyGasParams = function () { + // We call this in case the gas has not been modified at all + if (!this.state) { return true } + return this._notZeroOrEmptyString(this.state.gas) && this._notZeroOrEmptyString(this.state.gasPrice) +} + +PTXP._notZeroOrEmptyString = function (obj) { + return obj !== '' && obj !== '0x0' +} + function forwardCarrat () { return ( diff --git a/ui/app/components/pending-tx.js b/ui/app/components/pending-tx.js index d39cbc0f8..2ab6f25a9 100644 --- a/ui/app/components/pending-tx.js +++ b/ui/app/components/pending-tx.js @@ -1,10 +1,18 @@ const Component = require('react').Component +const connect = require('react-redux').connect const h = require('react-hyperscript') const inherits = require('util').inherits const PendingTxDetails = require('./pending-tx-details') const extend = require('xtend') +const actions = require('../actions') -module.exports = PendingTx +module.exports = connect(mapStateToProps)(PendingTx) + +function mapStateToProps (state) { + return { + + } +} inherits(PendingTx, Component) function PendingTx () { @@ -60,25 +68,31 @@ PendingTx.prototype.render = function () { }, [ props.insufficientBalance ? - h('button.btn-green', { + h('button', { onClick: props.buyEth, }, 'Buy Ether') : null, - h('button.confirm', { + h('button', { + onClick: () => { + this.refs.details.resetGasFields() + }, + }, 'Reset'), + + h('button.confirm.btn-green', { disabled: props.insufficientBalance, - onClick: props.sendTransaction, + onClick: (txData, event) => { + if (this.refs.details.verifyGasParams()) { + props.sendTransaction(txData, event) + } else { + this.props.dispatch(actions.displayWarning('Invalid Gas Parameters')) + } + }, }, 'Accept'), h('button.cancel.btn-red', { onClick: props.cancelTransaction, }, 'Reject'), - - h('button', { - onClick: () => { - this.refs.details.resetGasFields() - }, - }, 'Reset'), ]), ]) ) diff --git a/ui/app/config.js b/ui/app/config.js index 65b1ed712..00a4cba88 100644 --- a/ui/app/config.js +++ b/ui/app/config.js @@ -5,6 +5,7 @@ const connect = require('react-redux').connect const actions = require('./actions') const currencies = require('./conversion.json').rows const validUrl = require('valid-url') + module.exports = connect(mapStateToProps)(ConfigScreen) function mapStateToProps (state) { diff --git a/ui/app/css/lib.css b/ui/app/css/lib.css index a8df1d115..99c6f1b9d 100644 --- a/ui/app/css/lib.css +++ b/ui/app/css/lib.css @@ -256,3 +256,9 @@ hr.horizontal-line { text-overflow: ellipsis; white-space: nowrap; } + +.critical-error { + text-align: center; + margin-top: 20px; + color: red; +} diff --git a/ui/app/reducers/metamask.js b/ui/app/reducers/metamask.js index 3875cf6d1..a3c07d977 100644 --- a/ui/app/reducers/metamask.js +++ b/ui/app/reducers/metamask.js @@ -18,6 +18,7 @@ function reduceMetamask (state, action) { conversionDate: 'N/A', noActiveNotices: true, lastUnreadNotice: undefined, + frequentRpcList: [], }, state.metamask) switch (action.type) { @@ -53,6 +54,11 @@ function reduceMetamask (state, action) { isUnlocked: false, }) + case actions.SET_RPC_LIST: + return extend(metamaskState, { + frequentRpcList: action.value, + }) + case actions.SET_RPC_TARGET: return extend(metamaskState, { provider: { |