aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--CHANGELOG.md5
-rw-r--r--app/manifest.json2
-rw-r--r--app/scripts/controllers/currency.js (renamed from app/scripts/lib/controllers/currency.js)0
-rw-r--r--app/scripts/controllers/preferences.js63
-rw-r--r--app/scripts/controllers/shapeshift.js (renamed from app/scripts/lib/controllers/shapeshift.js)0
-rw-r--r--app/scripts/keyring-controller.js23
-rw-r--r--app/scripts/lib/controllers/preferences.js33
-rw-r--r--app/scripts/lib/personal-message-manager.js22
-rw-r--r--app/scripts/lib/tx-utils.js39
-rw-r--r--app/scripts/metamask-controller.js32
-rw-r--r--app/scripts/popup-core.js6
-rw-r--r--app/scripts/transaction-manager.js2
-rw-r--r--development/states/first-time.json1
-rw-r--r--development/states/send.json89
-rw-r--r--package.json6
-rw-r--r--test/unit/actions/config_test.js7
-rw-r--r--test/unit/components/binary-renderer-test.js25
-rw-r--r--test/unit/currency-controller-test.js2
-rw-r--r--test/unit/notice-controller-test.js2
-rw-r--r--test/unit/personal-message-manager-test.js23
-rw-r--r--test/unit/tx-utils-test.js58
-rw-r--r--ui/app/actions.js28
-rw-r--r--ui/app/app.js35
-rw-r--r--ui/app/components/binary-renderer.js43
-rw-r--r--ui/app/components/ens-input.js37
-rw-r--r--ui/app/components/hex-as-decimal-input.js14
-rw-r--r--ui/app/components/pending-personal-msg-details.js14
-rw-r--r--ui/app/components/pending-tx-details.js29
-rw-r--r--ui/app/components/pending-tx.js34
-rw-r--r--ui/app/config.js1
-rw-r--r--ui/app/css/lib.css6
-rw-r--r--ui/app/reducers/metamask.js6
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: {