aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--.eslintrc5
-rw-r--r--CHANGELOG.md22
-rw-r--r--app/manifest.json2
-rw-r--r--app/scripts/account-import-strategies/index.js45
-rw-r--r--app/scripts/background.js2
-rw-r--r--app/scripts/keyring-controller.js21
-rw-r--r--app/scripts/keyrings/hd.js2
-rw-r--r--app/scripts/keyrings/simple.js28
-rw-r--r--app/scripts/lib/config-manager.js2
-rw-r--r--app/scripts/lib/eth-store.js4
-rw-r--r--app/scripts/lib/tx-utils.js58
-rw-r--r--app/scripts/metamask-controller.js74
-rw-r--r--app/scripts/transaction-manager.js227
-rw-r--r--development/states/account-list-with-imported.json84
-rw-r--r--development/states/compilation-bug.json124
-rw-r--r--development/states/import-private-key-warning.json92
-rw-r--r--development/states/import-private-key.json64
-rw-r--r--development/states/new-account.json66
-rw-r--r--notices/notice_0.md12
-rw-r--r--package.json5
-rw-r--r--test/integration/lib/first-time.js3
-rw-r--r--test/unit/explorer-link-test.js2
-rw-r--r--test/unit/keyrings/simple-test.js27
-rw-r--r--test/unit/metamask-controller-test.js18
-rw-r--r--test/unit/notice-controller-test.js5
-rw-r--r--test/unit/tx-manager-test.js69
-rw-r--r--ui/app/account-detail.js17
-rw-r--r--ui/app/accounts/import/index.js91
-rw-r--r--ui/app/accounts/import/json.js98
-rw-r--r--ui/app/accounts/import/private-key.js68
-rw-r--r--ui/app/accounts/import/seed.js30
-rw-r--r--ui/app/accounts/index.js10
-rw-r--r--ui/app/actions.js45
-rw-r--r--ui/app/app.js16
-rw-r--r--ui/app/components/buy-button-subview.js82
-rw-r--r--ui/app/components/coinbase-form.js8
-rw-r--r--ui/app/components/loading.js8
-rw-r--r--ui/app/components/pending-tx-details.js12
-rw-r--r--ui/app/components/tab-bar.js35
-rw-r--r--ui/app/components/transaction-list-item-icon.js39
-rw-r--r--ui/app/components/transaction-list-item.js21
-rw-r--r--ui/app/components/transaction-list.js9
-rw-r--r--ui/app/conf-tx.js27
-rw-r--r--ui/app/css/lib.css8
-rw-r--r--ui/app/info.js2
-rw-r--r--ui/app/reducers/app.js20
-rw-r--r--ui/app/unlock.js2
-rw-r--r--ui/css.js1
-rw-r--r--ui/lib/explorer-link.js2
49 files changed, 1406 insertions, 308 deletions
diff --git a/.eslintrc b/.eslintrc
index 72b3d3e6d..84f65bea4 100644
--- a/.eslintrc
+++ b/.eslintrc
@@ -1,5 +1,6 @@
{
"parserOptions": {
+ "sourceType": "module",
"ecmaVersion": 6,
"ecmaFeatures": {
"experimentalObjectRestSpread": true,
@@ -44,7 +45,7 @@
"eol-last": 1,
"eqeqeq": [2, "allow-null"],
"generator-star-spacing": [2, { "before": true, "after": true }],
- "handle-callback-err": [2, "^(err|error)$" ],
+ "handle-callback-err": [1, "^(err|error)$" ],
"indent": [2, 2, { "SwitchCase": 1 }],
"jsx-quotes": [2, "prefer-single"],
"key-spacing": [2, { "beforeColon": false, "afterColon": true }],
@@ -145,6 +146,6 @@
"wrap-iife": [2, "any"],
"yield-star-spacing": [2, "both"],
"yoda": [2, "never"],
- "prefer-const": 1
+ "prefer-const": 1,
}
}
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 9f1ec6823..fa825e860 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,26 @@
## Current Master
+- Add ability to import accounts in JSON file format (used by Mist, Geth, MyEtherWallet, and more!)
+## 3.1.1 2017-1-20
+
+- Fix HD wallet seed export
+
+## 3.1.0 2017-1-18
+
+- Add ability to import accounts by private key.
+- Fixed bug that returned the wrong transaction hashes on private networks that had not implemented EIP 155 replay protection (like TestRPC).
+
+## 3.0.1 2017-1-17
+
+- Fixed bug that prevented eth.sign from working.
+- Fix the displaying of transactions that have been submitted to the network in Transaction History
+
+## 3.0.0 2017-1-16
+
+- Fix seed word account generation (https://medium.com/metamask/metamask-3-migration-guide-914b79533cdd#.t4i1qmmsz).
+- Fix Bug where you see a empty transaction flash by on the confirm transaction view.
+- Create visible difference in transaction history between a approved but not yet included in a block transaction and a transaction who has been confirmed.
- Fix memory leak in RPC Cache
- Override RPC commands eth_syncing and web3_clientVersion
- Remove certain non-essential permissions from certain builds.
@@ -14,6 +34,8 @@
## 2.14.1 2016-12-20
+- Update Coinbase info. and increase the buy amount to $15
+- Fixed ropsten transaction links
- Temporarily disable extension reload detection causing infinite reload bug.
- Implemented basic checking for valid RPC URIs.
diff --git a/app/manifest.json b/app/manifest.json
index 95dcfc31a..c34b17e72 100644
--- a/app/manifest.json
+++ b/app/manifest.json
@@ -1,7 +1,7 @@
{
"name": "MetaMask",
"short_name": "Metamask",
- "version": "2.14.1",
+ "version": "3.1.1",
"manifest_version": 2,
"author": "https://metamask.io",
"description": "Ethereum Browser Extension",
diff --git a/app/scripts/account-import-strategies/index.js b/app/scripts/account-import-strategies/index.js
new file mode 100644
index 000000000..d5124eb7f
--- /dev/null
+++ b/app/scripts/account-import-strategies/index.js
@@ -0,0 +1,45 @@
+const Wallet = require('ethereumjs-wallet')
+const importers = require('ethereumjs-wallet/thirdparty')
+const ethUtil = require('ethereumjs-util')
+
+const accountImporter = {
+
+ importAccount(strategy, args) {
+ try {
+ const importer = this.strategies[strategy]
+ const privateKeyHex = importer.apply(null, args)
+ return Promise.resolve(privateKeyHex)
+ } catch (e) {
+ return Promise.reject(e)
+ }
+ },
+
+ strategies: {
+ 'Private Key': (privateKey) => {
+ const stripped = ethUtil.stripHexPrefix(privateKey)
+ return stripped
+ },
+ 'JSON File': (input, password) => {
+ let wallet
+ try {
+ wallet = importers.fromEtherWallet(input, password)
+ } catch (e) {
+ console.log('Attempt to import as EtherWallet format failed, trying V3...')
+ }
+
+ if (!wallet) {
+ wallet = Wallet.fromV3(input, password, true)
+ }
+
+ return walletToPrivateKey(wallet)
+ },
+ },
+
+}
+
+function walletToPrivateKey (wallet) {
+ const privateKeyBuffer = wallet.getPrivateKey()
+ return ethUtil.bufferToHex(privateKeyBuffer)
+}
+
+module.exports = accountImporter
diff --git a/app/scripts/background.js b/app/scripts/background.js
index 1f269da7b..f95e194dd 100644
--- a/app/scripts/background.js
+++ b/app/scripts/background.js
@@ -16,6 +16,7 @@ const firstTimeState = require('./first-time-state')
const STORAGE_KEY = 'metamask-config'
const METAMASK_DEBUG = 'GULP_METAMASK_DEBUG'
+
let popupIsOpen = false
// state persistence
@@ -135,6 +136,7 @@ function setupController (initState) {
// User Interface setup
//
+ updateBadge()
controller.txManager.on('updateBadge', updateBadge)
// plugin badge text
diff --git a/app/scripts/keyring-controller.js b/app/scripts/keyring-controller.js
index d4c0d863e..86c93f5a3 100644
--- a/app/scripts/keyring-controller.js
+++ b/app/scripts/keyring-controller.js
@@ -95,7 +95,6 @@ module.exports = class KeyringController extends EventEmitter {
isInitialized: (!!wallet || !!vault),
isUnlocked: Boolean(this.password),
isDisclaimerConfirmed: this.configManager.getConfirmedDisclaimer(),
- transactions: this.configManager.getTxList(),
unconfMsgs: messageManager.unconfirmedMsgs(),
messages: messageManager.getMsgList(),
selectedAccount: address,
@@ -173,7 +172,9 @@ module.exports = class KeyringController extends EventEmitter {
// Used when creating a first vault, to allow confirmation.
// Also used when revealing the seed words in the confirmation view.
placeSeedWords () {
- const firstKeyring = this.keyrings[0]
+ const hdKeyrings = this.keyrings.filter((keyring) => keyring.type === 'HD Key Tree')
+ const firstKeyring = hdKeyrings[0]
+ if (!firstKeyring) throw new Error('KeyringController - No HD Key Tree found')
return firstKeyring.serialize()
.then((serialized) => {
const seedWords = serialized.mnemonic
@@ -235,7 +236,10 @@ module.exports = class KeyringController extends EventEmitter {
addNewKeyring (type, opts) {
const Keyring = this.getKeyringClassForType(type)
const keyring = new Keyring(opts)
- return keyring.getAccounts()
+ return keyring.deserialize(opts)
+ .then(() => {
+ return keyring.getAccounts()
+ })
.then((accounts) => {
this.keyrings.push(keyring)
return this.setupAccounts(accounts)
@@ -317,13 +321,11 @@ module.exports = class KeyringController extends EventEmitter {
// This method signs tx and returns a promise for
// TX Manager to update the state after signing
- signTransaction (ethTx, selectedAddress, txId) {
- const address = normalize(selectedAddress)
- return this.getKeyringForAccount(address)
+ signTransaction (ethTx, _fromAddress) {
+ const fromAddress = normalize(_fromAddress)
+ return this.getKeyringForAccount(fromAddress)
.then((keyring) => {
- return keyring.signTransaction(address, ethTx)
- }).then((tx) => {
- return {tx, txId}
+ return keyring.signTransaction(fromAddress, ethTx)
})
}
// Add Unconfirmed Message
@@ -400,6 +402,7 @@ module.exports = class KeyringController extends EventEmitter {
}).then((rawSig) => {
cb(null, rawSig)
approvalCb(null, true)
+ messageManager.confirmMsg(msgId)
return rawSig
})
} catch (e) {
diff --git a/app/scripts/keyrings/hd.js b/app/scripts/keyrings/hd.js
index 80b713b58..1b9796e07 100644
--- a/app/scripts/keyrings/hd.js
+++ b/app/scripts/keyrings/hd.js
@@ -76,7 +76,7 @@ class HdKeyring extends EventEmitter {
// For eth_sign, we need to sign transactions:
signMessage (withAccount, data) {
const wallet = this._getWalletForAccount(withAccount)
- const message = ethUtil.removeHexPrefix(data)
+ const message = ethUtil.stripHexPrefix(data)
var privKey = wallet.getPrivateKey()
var msgSig = ethUtil.ecsign(new Buffer(message, 'hex'), privKey)
var rawMsgSig = ethUtil.bufferToHex(sigUtil.concatSig(msgSig.v, msgSig.r, msgSig.s))
diff --git a/app/scripts/keyrings/simple.js b/app/scripts/keyrings/simple.js
index 9717f1c45..46687fcaf 100644
--- a/app/scripts/keyrings/simple.js
+++ b/app/scripts/keyrings/simple.js
@@ -20,13 +20,19 @@ class SimpleKeyring extends EventEmitter {
}
deserialize (privateKeys = []) {
- this.wallets = privateKeys.map((privateKey) => {
- const stripped = ethUtil.stripHexPrefix(privateKey)
- const buffer = new Buffer(stripped, 'hex')
- const wallet = Wallet.fromPrivateKey(buffer)
- return wallet
+ return new Promise((resolve, reject) => {
+ try {
+ this.wallets = privateKeys.map((privateKey) => {
+ const stripped = ethUtil.stripHexPrefix(privateKey)
+ const buffer = new Buffer(stripped, 'hex')
+ const wallet = Wallet.fromPrivateKey(buffer)
+ return wallet
+ })
+ } catch (e) {
+ reject(e)
+ }
+ resolve()
})
- return Promise.resolve()
}
addAccounts (n = 1) {
@@ -35,12 +41,12 @@ class SimpleKeyring extends EventEmitter {
newWallets.push(Wallet.generate())
}
this.wallets = this.wallets.concat(newWallets)
- const hexWallets = newWallets.map(w => w.getAddress().toString('hex'))
+ const hexWallets = newWallets.map(w => ethUtil.bufferToHex(w.getAddress()))
return Promise.resolve(hexWallets)
}
getAccounts () {
- return Promise.resolve(this.wallets.map(w => w.getAddress().toString('hex')))
+ return Promise.resolve(this.wallets.map(w => ethUtil.bufferToHex(w.getAddress())))
}
// tx is an instance of the ethereumjs-transaction class.
@@ -54,7 +60,7 @@ class SimpleKeyring extends EventEmitter {
// For eth_sign, we need to sign transactions:
signMessage (withAccount, data) {
const wallet = this._getWalletForAccount(withAccount)
- const message = ethUtil.removeHexPrefix(data)
+ const message = ethUtil.stripHexPrefix(data)
var privKey = wallet.getPrivateKey()
var msgSig = ethUtil.ecsign(new Buffer(message, 'hex'), privKey)
var rawMsgSig = ethUtil.bufferToHex(sigUtil.concatSig(msgSig.v, msgSig.r, msgSig.s))
@@ -70,7 +76,9 @@ class SimpleKeyring extends EventEmitter {
/* PRIVATE METHODS */
_getWalletForAccount (account) {
- return this.wallets.find(w => w.getAddress().toString('hex') === account)
+ let wallet = this.wallets.find(w => ethUtil.bufferToHex(w.getAddress()) === account)
+ if (!wallet) throw new Error('Simple Keyring - Unable to find matching address.')
+ return wallet
}
}
diff --git a/app/scripts/lib/config-manager.js b/app/scripts/lib/config-manager.js
index 6d7305377..daba8bc7b 100644
--- a/app/scripts/lib/config-manager.js
+++ b/app/scripts/lib/config-manager.js
@@ -281,7 +281,7 @@ ConfigManager.prototype.updateConversionRate = function () {
this.setConversionPrice(parsedResponse.ticker.price)
this.setConversionDate(parsedResponse.timestamp)
}).catch((err) => {
- console.error('Error in conversion.', err)
+ console.warn('MetaMask - Failed to query currency conversion.')
this.setConversionPrice(0)
this.setConversionDate('N/A')
})
diff --git a/app/scripts/lib/eth-store.js b/app/scripts/lib/eth-store.js
index a42b2417f..7e2caf884 100644
--- a/app/scripts/lib/eth-store.js
+++ b/app/scripts/lib/eth-store.js
@@ -43,7 +43,9 @@ EthereumStore.prototype.addAccount = function (address) {
self._currentState.accounts[address] = {}
self._didUpdate()
if (!self.currentBlockNumber) return
- self._updateAccount(address, noop)
+ self._updateAccount(address, () => {
+ self._didUpdate()
+ })
}
EthereumStore.prototype.removeAccount = function (address) {
diff --git a/app/scripts/lib/tx-utils.js b/app/scripts/lib/tx-utils.js
index d1fb98f42..5116cb93b 100644
--- a/app/scripts/lib/tx-utils.js
+++ b/app/scripts/lib/tx-utils.js
@@ -1,6 +1,8 @@
const async = require('async')
const EthQuery = require('eth-query')
const ethUtil = require('ethereumjs-util')
+const Transaction = require('ethereumjs-tx')
+const normalize = require('./sig-util').normalize
const BN = ethUtil.BN
/*
@@ -14,6 +16,7 @@ module.exports = class txProviderUtils {
this.provider = provider
this.query = new EthQuery(provider)
}
+
analyzeGasUsage (txData, cb) {
var self = this
this.query.getBlockByNumber('latest', true, (err, block) => {
@@ -71,4 +74,59 @@ module.exports = class txProviderUtils {
const correct = bnGas.add(gasBuffer)
return ethUtil.addHexPrefix(correct.toString(16))
}
+
+ fillInTxParams (txParams, cb) {
+ let fromAddress = txParams.from
+ let reqs = {}
+
+ if (isUndef(txParams.gas)) reqs.gas = (cb) => this.query.estimateGas(txParams, cb)
+ if (isUndef(txParams.gasPrice)) reqs.gasPrice = (cb) => this.query.gasPrice(cb)
+ if (isUndef(txParams.nonce)) reqs.nonce = (cb) => this.query.getTransactionCount(fromAddress, 'pending', cb)
+
+ async.parallel(reqs, function(err, result) {
+ if (err) return cb(err)
+ // write results to txParams obj
+ Object.assign(txParams, result)
+ cb()
+ })
+ }
+
+ // builds ethTx from txParams object
+ buildEthTxFromParams (txParams, gasMultiplier = 1) {
+ // apply gas multiplyer
+ let gasPrice = new BN(ethUtil.stripHexPrefix(txParams.gasPrice), 16)
+ // multiply and divide by 100 so as to add percision to integer mul
+ gasPrice = gasPrice.mul(new BN(gasMultiplier * 100, 10)).div(new BN(100, 10))
+ txParams.gasPrice = ethUtil.intToHex(gasPrice.toNumber())
+ // normalize values
+ txParams.to = normalize(txParams.to)
+ txParams.from = normalize(txParams.from)
+ txParams.value = normalize(txParams.value)
+ txParams.data = normalize(txParams.data)
+ txParams.gasLimit = normalize(txParams.gasLimit || txParams.gas)
+ txParams.nonce = normalize(txParams.nonce)
+ // build ethTx
+ const ethTx = new Transaction(txParams)
+ return ethTx
+ }
+
+ publishTransaction (rawTx, cb) {
+ this.query.sendRawTransaction(rawTx, cb)
+ }
+
+ validateTxParams (txParams, cb) {
+ if (('value' in txParams) && txParams.value.indexOf('-') === 0) {
+ cb(new Error(`Invalid transaction value of ${txParams.value} not a positive number.`))
+ } else {
+ cb()
+ }
+ }
+
+
+}
+
+// util
+
+function isUndef(value) {
+ return value === undefined
}
diff --git a/app/scripts/metamask-controller.js b/app/scripts/metamask-controller.js
index e15844a56..2847873bd 100644
--- a/app/scripts/metamask-controller.js
+++ b/app/scripts/metamask-controller.js
@@ -15,6 +15,8 @@ const IdStoreMigrator = require('./lib/idStore-migrator')
const ObservableStore = require('./lib/observable/')
const HostStore = require('./lib/observable/host')
const synchronizeStore = require('./lib/observable/util/sync')
+const accountImporter = require('./account-import-strategies')
+
const version = require('../manifest.json').version
module.exports = class MetamaskController extends EventEmitter {
@@ -57,6 +59,7 @@ module.exports = class MetamaskController extends EventEmitter {
getSelectedAccount: this.configManager.getSelectedAccount.bind(this.configManager),
getGasMultiplier: this.configManager.getGasMultiplier.bind(this.configManager),
getNetwork: this.getStateNetwork.bind(this),
+ signTransaction: this.keyringController.signTransaction.bind(this.keyringController),
provider: this.provider,
blockTracker: this.provider,
})
@@ -77,6 +80,7 @@ module.exports = class MetamaskController extends EventEmitter {
this.ethStore.on('update', this.sendUpdate.bind(this))
this.keyringController.on('update', this.sendUpdate.bind(this))
+ this.txManager.on('update', this.sendUpdate.bind(this))
}
getState () {
@@ -125,7 +129,22 @@ module.exports = class MetamaskController extends EventEmitter {
.then((newState) => { cb(null, newState) })
.catch((reason) => { cb(reason) })
},
- addNewKeyring: nodeify(keyringController.addNewKeyring).bind(keyringController),
+ addNewKeyring: (type, opts, cb) => {
+ keyringController.addNewKeyring(type, opts)
+ .then(() => keyringController.fullUpdate())
+ .then((newState) => { cb(null, newState) })
+ .catch((reason) => { cb(reason) })
+ },
+ importAccountWithStrategy: (strategy, args, cb) => {
+ accountImporter.importAccount(strategy, args)
+ .then((privateKey) => {
+ return keyringController.addNewKeyring('Simple Key Pair', [ privateKey ])
+ })
+ .then(keyring => keyring.getAccounts())
+ .then((accounts) => keyringController.setSelectedAccount(accounts[0]))
+ .then(() => { cb(null, keyringController.fullUpdate()) })
+ .catch((reason) => { cb(reason) })
+ },
addNewAccount: nodeify(keyringController.addNewAccount).bind(keyringController),
setSelectedAccount: nodeify(keyringController.setSelectedAccount).bind(keyringController),
saveAccountLabel: nodeify(keyringController.saveAccountLabel).bind(keyringController),
@@ -200,26 +219,7 @@ module.exports = class MetamaskController extends EventEmitter {
cb(null, result)
},
// tx signing
- approveTransaction: this.newUnsignedTransaction.bind(this),
- signTransaction: (txParams, cb) => {
- this.txManager.formatTxForSigining(txParams)
- .then(({ethTx, address, txId}) => {
- return this.keyringController.signTransaction(ethTx, address, txId)
- })
- .then(({tx, txId}) => {
- return this.txManager.resolveSignedTransaction({tx, txId})
- })
- .then((rawTx) => {
- cb(null, rawTx)
- this.sendUpdate()
- this.txManager.emit(`${txParams.metamaskId}:signingComplete`)
- })
- .catch((err) => {
- console.error(err)
- cb(err)
- })
- },
-
+ processTransaction: (txParams, cb) => this.newUnapprovedTransaction(txParams, cb),
// msg signing
approveMessage: this.newUnsignedMessage.bind(this),
signMessage: (...args) => {
@@ -259,24 +259,26 @@ module.exports = class MetamaskController extends EventEmitter {
return publicConfigStore
}
- newUnsignedTransaction (txParams, onTxDoneCb) {
- const txManager = this.txManager
- const err = this.enforceTxValidations(txParams)
- if (err) return onTxDoneCb(err)
- txManager.addUnapprovedTransaction(txParams, onTxDoneCb, (err, txData) => {
- if (err) return onTxDoneCb(err)
- this.sendUpdate()
- this.opts.showUnapprovedTx(txParams, txData, onTxDoneCb)
+ newUnapprovedTransaction (txParams, cb) {
+ const self = this
+ self.txManager.addUnapprovedTransaction(txParams, (err, txMeta) => {
+ if (err) return cb(err)
+ self.sendUpdate()
+ self.opts.showUnapprovedTx(txMeta)
+ // listen for tx completion (success, fail)
+ self.txManager.once(`${txMeta.id}:finished`, (status) => {
+ switch (status) {
+ case 'submitted':
+ return cb(null, txMeta.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)}`))
+ }
+ })
})
}
- enforceTxValidations (txParams) {
- if (('value' in txParams) && txParams.value.indexOf('-') === 0) {
- const msg = `Invalid transaction value of ${txParams.value} not a positive number.`
- return new Error(msg)
- }
- }
-
newUnsignedMessage (msgParams, cb) {
var state = this.keyringController.getState()
if (!state.isUnlocked) {
diff --git a/app/scripts/transaction-manager.js b/app/scripts/transaction-manager.js
index 6becfa6d1..6d0121afd 100644
--- a/app/scripts/transaction-manager.js
+++ b/app/scripts/transaction-manager.js
@@ -1,11 +1,11 @@
const EventEmitter = require('events')
+const async = require('async')
const extend = require('xtend')
+const Semaphore = require('semaphore')
const ethUtil = require('ethereumjs-util')
-const Transaction = require('ethereumjs-tx')
-const BN = ethUtil.BN
+const BN = require('ethereumjs-util').BN
const TxProviderUtil = require('./lib/tx-utils')
const createId = require('./lib/random-id')
-const normalize = require('./lib/sig-util').normalize
module.exports = class TransactionManager extends EventEmitter {
constructor (opts) {
@@ -20,6 +20,8 @@ module.exports = class TransactionManager extends EventEmitter {
this.blockTracker.on('block', this.checkForTxInBlock.bind(this))
this.getGasMultiplier = opts.getGasMultiplier
this.getNetwork = opts.getNetwork
+ this.signEthTx = opts.signTransaction
+ this.nonceLock = Semaphore(1)
}
getState () {
@@ -33,11 +35,12 @@ module.exports = class TransactionManager extends EventEmitter {
// Returns the tx list
getTxList () {
- return this.txList
+ let network = this.getNetwork()
+ return this.txList.filter(txMeta => txMeta.metamaskNetworkId === network)
}
// Adds a tx to the txlist
- addTx (txMeta, onTxDoneCb = warn) {
+ addTx (txMeta) {
var txList = this.getTxList()
var txHistoryLimit = this.txHistoryLimit
@@ -53,16 +56,11 @@ module.exports = class TransactionManager extends EventEmitter {
txList.push(txMeta)
this._saveTxList(txList)
- // keep the onTxDoneCb around in a listener
- // for after approval/denial (requires user interaction)
- // This onTxDoneCb fires completion to the Dapp's write operation.
this.once(`${txMeta.id}:signed`, function (txId) {
this.removeAllListeners(`${txMeta.id}:rejected`)
- onTxDoneCb(null, true)
})
this.once(`${txMeta.id}:rejected`, function (txId) {
this.removeAllListeners(`${txMeta.id}:signed`)
- onTxDoneCb(null, false)
})
this.emit('updateBadge')
@@ -83,6 +81,7 @@ module.exports = class TransactionManager extends EventEmitter {
var index = txList.findIndex(txData => txData.id === txId)
txList[index] = txMeta
this._saveTxList(txList)
+ this.emit('update')
}
get unapprovedTxCount () {
@@ -93,28 +92,51 @@ module.exports = class TransactionManager extends EventEmitter {
return this.getTxsByMetaData('status', 'signed').length
}
- addUnapprovedTransaction (txParams, onTxDoneCb, cb) {
- // create txData obj with parameters and meta data
- var time = (new Date()).getTime()
- var txId = createId()
- txParams.metamaskId = txId
- txParams.metamaskNetworkId = this.getNetwork()
- var txData = {
- id: txId,
- txParams: txParams,
- time: time,
- status: 'unapproved',
- gasMultiplier: this.getGasMultiplier() || 1,
- metamaskNetworkId: this.getNetwork(),
- }
- this.txProviderUtils.analyzeGasUsage(txData, this.txDidComplete.bind(this, txData, onTxDoneCb, cb))
- // calculate metadata for tx
+ addUnapprovedTransaction (txParams, done) {
+ let txMeta
+ async.waterfall([
+ // validate
+ (cb) => this.txProviderUtils.validateTxParams(txParams, cb),
+ // prepare txMeta
+ (cb) => {
+ // create txMeta obj with parameters and meta data
+ let time = (new Date()).getTime()
+ let txId = createId()
+ txParams.metamaskId = txId
+ txParams.metamaskNetworkId = this.getNetwork()
+ txMeta = {
+ id: txId,
+ time: time,
+ status: 'unapproved',
+ gasMultiplier: this.getGasMultiplier() || 1,
+ metamaskNetworkId: this.getNetwork(),
+ txParams: txParams,
+ }
+ // calculate metadata for tx
+ this.txProviderUtils.analyzeGasUsage(txMeta, cb)
+ },
+ // save txMeta
+ (cb) => {
+ this.addTx(txMeta)
+ this.setMaxTxCostAndFee(txMeta)
+ cb(null, txMeta)
+ },
+ ], done)
}
- txDidComplete (txMeta, onTxDoneCb, cb, err) {
- if (err) return cb(err)
- this.addTx(txMeta, onTxDoneCb)
- cb(null, txMeta)
+ setMaxTxCostAndFee (txMeta) {
+ var txParams = txMeta.txParams
+ var gasMultiplier = txMeta.gasMultiplier
+ var gasCost = new BN(ethUtil.stripHexPrefix(txParams.gas || txMeta.estimatedGas), 16)
+ var gasPrice = new BN(ethUtil.stripHexPrefix(txParams.gasPrice || '0x4a817c800'), 16)
+ gasPrice = gasPrice.mul(new BN(gasMultiplier * 100), 10).div(new BN(100, 10))
+ var txFee = gasCost.mul(gasPrice)
+ var txValue = new BN(ethUtil.stripHexPrefix(txParams.value || '0x0'), 16)
+ var maxCost = txValue.add(txFee)
+ txMeta.txFee = txFee
+ txMeta.txValue = txValue
+ txMeta.maxCost = maxCost
+ this.updateTx(txMeta)
}
getUnapprovedTxList () {
@@ -127,8 +149,25 @@ module.exports = class TransactionManager extends EventEmitter {
}
approveTransaction (txId, cb = warn) {
- this.setTxStatusSigned(txId)
- this.once(`${txId}:signingComplete`, cb)
+ const self = this
+ // approve
+ self.setTxStatusApproved(txId)
+ // only allow one tx at a time for atomic nonce usage
+ self.nonceLock.take(() => {
+ // begin signature process
+ async.waterfall([
+ (cb) => self.fillInTxParams(txId, cb),
+ (cb) => self.signTransaction(txId, cb),
+ (rawTx, cb) => self.publishTransaction(txId, rawTx, cb),
+ ], (err) => {
+ self.nonceLock.leave()
+ if (err) {
+ this.setTxStatusFailed(txId)
+ return cb(err)
+ }
+ cb()
+ })
+ })
}
cancelTransaction (txId, cb = warn) {
@@ -136,38 +175,43 @@ module.exports = class TransactionManager extends EventEmitter {
cb()
}
- // formats txParams so the keyringController can sign it
- formatTxForSigining (txParams) {
- var address = txParams.from
- var metaTx = this.getTx(txParams.metamaskId)
- var gasMultiplier = metaTx.gasMultiplier
- var gasPrice = new BN(ethUtil.stripHexPrefix(txParams.gasPrice), 16)
- gasPrice = gasPrice.mul(new BN(gasMultiplier * 100, 10)).div(new BN(100, 10))
- txParams.gasPrice = ethUtil.intToHex(gasPrice.toNumber())
-
- // normalize values
- txParams.to = normalize(txParams.to)
- txParams.from = normalize(txParams.from)
- txParams.value = normalize(txParams.value)
- txParams.data = normalize(txParams.data)
- txParams.gasLimit = normalize(txParams.gasLimit || txParams.gas)
- txParams.nonce = normalize(txParams.nonce)
- const ethTx = new Transaction(txParams)
- var txId = txParams.metamaskId
- return Promise.resolve({ethTx, address, txId})
+ fillInTxParams (txId, cb) {
+ let txMeta = this.getTx(txId)
+ this.txProviderUtils.fillInTxParams(txMeta.txParams, (err) => {
+ if (err) return cb(err)
+ this.updateTx(txMeta)
+ cb()
+ })
}
- // receives a signed tx object and updates the tx hash
- // and pass it to the cb to be sent off
- resolveSignedTransaction ({tx, txId, cb = warn}) {
- // Add the tx hash to the persisted meta-tx object
- var txHash = ethUtil.bufferToHex(tx.hash())
- var metaTx = this.getTx(txId)
- metaTx.hash = txHash
- this.updateTx(metaTx)
- var rawTx = ethUtil.bufferToHex(tx.serialize())
- return Promise.resolve(rawTx)
+ signTransaction (txId, cb) {
+ let txMeta = this.getTx(txId)
+ let txParams = txMeta.txParams
+ let fromAddress = txParams.from
+ let ethTx = this.txProviderUtils.buildEthTxFromParams(txParams, txMeta.gasMultiplier)
+ this.signEthTx(ethTx, fromAddress).then(() => {
+ this.setTxStatusSigned(txMeta.id)
+ cb(null, ethUtil.bufferToHex(ethTx.serialize()))
+ }).catch((err) => {
+ cb(err)
+ })
+ }
+
+ publishTransaction (txId, rawTx, cb) {
+ this.txProviderUtils.publishTransaction(rawTx, (err, txHash) => {
+ if (err) return cb(err)
+ this.setTxHash(txId, txHash)
+ this.setTxStatusSubmitted(txId)
+ cb()
+ })
+ }
+ // receives a txHash records the tx as signed
+ setTxHash (txId, txHash) {
+ // Add the tx hash to the persisted meta-tx object
+ let txMeta = this.getTx(txId)
+ txMeta.hash = txHash
+ this.updateTx(txMeta)
}
/*
@@ -212,23 +256,35 @@ module.exports = class TransactionManager extends EventEmitter {
return txMeta.status
}
+ // should update the status of the tx to 'rejected'.
+ setTxStatusRejected (txId) {
+ this._setTxStatus(txId, 'rejected')
+ }
+
+ // should update the status of the tx to 'approved'.
+ setTxStatusApproved (txId) {
+ this._setTxStatus(txId, 'approved')
+ }
// should update the status of the tx to 'signed'.
setTxStatusSigned (txId) {
this._setTxStatus(txId, 'signed')
- this.emit('updateBadge')
}
- // should update the status of the tx to 'rejected'.
- setTxStatusRejected (txId) {
- this._setTxStatus(txId, 'rejected')
- this.emit('updateBadge')
+ // should update the status of the tx to 'submitted'.
+ setTxStatusSubmitted (txId) {
+ this._setTxStatus(txId, 'submitted')
}
+ // should update the status of the tx to 'confirmed'.
setTxStatusConfirmed (txId) {
this._setTxStatus(txId, 'confirmed')
}
+ setTxStatusFailed (txId) {
+ this._setTxStatus(txId, 'failed')
+ }
+
// merges txParams obj onto txData.txParams
// use extend to ensure that all fields are filled
updateTxParams (txId, txParams) {
@@ -240,19 +296,31 @@ module.exports = class TransactionManager extends EventEmitter {
// checks if a signed tx is in a block and
// if included sets the tx status as 'confirmed'
checkForTxInBlock () {
- var signedTxList = this.getFilteredTxList({status: 'signed', err: undefined})
+ var signedTxList = this.getFilteredTxList({status: 'submitted'})
if (!signedTxList.length) return
- signedTxList.forEach((tx) => {
- var txHash = tx.hash
- var txId = tx.id
- if (!txHash) return
- this.txProviderUtils.query.getTransactionByHash(txHash, (err, txMeta) => {
- if (err || !txMeta) {
- tx.err = err || 'Tx could possibly have not been submitted'
- this.updateTx(tx)
- return txMeta ? console.error(err) : console.debug(`txMeta is ${txMeta} for:`, tx)
+ signedTxList.forEach((txMeta) => {
+ var txHash = txMeta.hash
+ var txId = txMeta.id
+ if (!txHash) {
+ txMeta.err = {
+ errCode: 'No hash was provided',
+ message: 'We had an error while submitting this transaction, please try again.',
}
- if (txMeta.blockNumber) {
+ this.updateTx(txMeta)
+ return this.setTxStatusFailed(txId)
+ }
+ this.txProviderUtils.query.getTransactionByHash(txHash, (err, txParams) => {
+ if (err || !txParams) {
+ if (!txParams) return
+ txMeta.err = {
+ isWarning: true,
+ errorCode: err,
+ message: 'There was a problem loading this transaction.',
+ }
+ this.updateTx(txMeta)
+ return console.error(err)
+ }
+ if (txParams.blockNumber) {
this.setTxStatusConfirmed(txId)
}
})
@@ -266,6 +334,7 @@ module.exports = class TransactionManager extends EventEmitter {
// should set the status in txData
// - `'unapproved'` the user has not responded
// - `'rejected'` the user has responded no!
+ // - `'approved'` the user has approved the tx
// - `'signed'` the tx is signed
// - `'submitted'` the tx is sent to a server
// - `'confirmed'` the tx has been included in a block.
@@ -273,7 +342,11 @@ module.exports = class TransactionManager extends EventEmitter {
var txMeta = this.getTx(txId)
txMeta.status = status
this.emit(`${txMeta.id}:${status}`, txId)
+ if (status === 'submitted' || status === 'rejected') {
+ this.emit(`${txMeta.id}:finished`, status)
+ }
this.updateTx(txMeta)
+ this.emit('updateBadge')
}
// Saves the new/updated txList.
diff --git a/development/states/account-list-with-imported.json b/development/states/account-list-with-imported.json
new file mode 100644
index 000000000..e32327743
--- /dev/null
+++ b/development/states/account-list-with-imported.json
@@ -0,0 +1,84 @@
+{
+ "metamask": {
+ "isInitialized": true,
+ "isUnlocked": true,
+ "rpcTarget": "https://rawtestrpc.metamask.io/",
+ "identities": {
+ "0x58bda1f9d87dc7d2bcc6f7c2513efc9d03fca683": {
+ "address": "0x58bda1f9d87dc7d2bcc6f7c2513efc9d03fca683",
+ "name": "Account 1"
+ },
+ "0x9858e7d8b79fc3e6d989636721584498926da38a": {
+ "address": "0x9858e7d8b79fc3e6d989636721584498926da38a",
+ "name": "Imported Account"
+ }
+ },
+ "unconfTxs": {},
+ "currentFiat": "USD",
+ "conversionRate": 10.19458075,
+ "conversionDate": 1484696373,
+ "noActiveNotices": true,
+ "network": "3",
+ "accounts": {
+ "0x58bda1f9d87dc7d2bcc6f7c2513efc9d03fca683": {
+ "code": "0x",
+ "balance": "0x0",
+ "nonce": "0x0",
+ "address": "0x58bda1f9d87dc7d2bcc6f7c2513efc9d03fca683"
+ },
+ "0x9858e7d8b79fc3e6d989636721584498926da38a": {
+ "code": "0x",
+ "balance": "0x0",
+ "nonce": "0x0",
+ "address": "0x9858e7d8b79fc3e6d989636721584498926da38a"
+ }
+ },
+ "transactions": [],
+ "provider": {
+ "type": "testnet"
+ },
+ "selectedAccount": "0x9858e7d8b79fc3e6d989636721584498926da38a",
+ "selectedAccountTxList": [],
+ "isDisclaimerConfirmed": true,
+ "unconfMsgs": {},
+ "messages": [],
+ "shapeShiftTxList": [],
+ "keyringTypes": [
+ "Simple Key Pair",
+ "HD Key Tree"
+ ],
+ "keyrings": [
+ {
+ "type": "HD Key Tree",
+ "accounts": [
+ "58bda1f9d87dc7d2bcc6f7c2513efc9d03fca683"
+ ]
+ },
+ {
+ "type": "Simple Key Pair",
+ "accounts": [
+ "0x9858e7d8b79fc3e6d989636721584498926da38a"
+ ]
+ }
+ ],
+ "lostAccounts": [],
+ "seedWords": null
+ },
+ "appState": {
+ "menuOpen": false,
+ "currentView": {
+ "name": "accounts"
+ },
+ "accountDetail": {
+ "subview": "transactions",
+ "accountExport": "none",
+ "privateKey": ""
+ },
+ "transForward": true,
+ "isLoading": false,
+ "warning": null,
+ "scrollToBottom": false,
+ "forgottenPassword": false
+ },
+ "identities": {}
+} \ No newline at end of file
diff --git a/development/states/compilation-bug.json b/development/states/compilation-bug.json
new file mode 100644
index 000000000..a9dfc4d4e
--- /dev/null
+++ b/development/states/compilation-bug.json
@@ -0,0 +1,124 @@
+{
+ "metamask": {
+ "isInitialized": true,
+ "isUnlocked": true,
+ "rpcTarget": "https://rawtestrpc.metamask.io/",
+ "identities": {
+ "0xac39b311dceb2a4b2f5d8461c1cdaf756f4f7ae9": {
+ "address": "0xac39b311dceb2a4b2f5d8461c1cdaf756f4f7ae9",
+ "name": "Account 1"
+ },
+ "0xd7c0cd9e7d2701c710d64fc492c7086679bdf7b4": {
+ "address": "0xd7c0cd9e7d2701c710d64fc492c7086679bdf7b4",
+ "name": "Account 2"
+ },
+ "0x1acfb961c5a8268eac8e09d6241a26cbeff42241": {
+ "address": "0x1acfb961c5a8268eac8e09d6241a26cbeff42241",
+ "name": "Account 3"
+ },
+ "0xabc2bca51709b8615147352c62420f547a63a00c": {
+ "address": "0xabc2bca51709b8615147352c62420f547a63a00c",
+ "name": "Account 4"
+ }
+ },
+ "unconfTxs": {
+ "7992944905869041": {
+ "id": 7992944905869041,
+ "txParams": {
+ "from": "0xac39b311dceb2a4b2f5d8461c1cdaf756f4f7ae9",
+ "value": "0x0",
+ "data": "0x606060405234610000575b60da806100186000396000f30060606040526000357c0100000000000000000000000000000000000000000000000000000000900463ffffffff1680630dbe671f14603c575b6000565b3460005760466088565b604051808273ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200191505060405180910390f35b600060009054906101000a900473ffffffffffffffffffffffffffffffffffffffff16815600a165627a7a72305820a99dfa6091771f518dd1ae8d1ee347bae3304dffd98fd24b1b99a8380bc60a750029",
+ "gas": "0x1af75",
+ "metamaskId": 7992944905869041,
+ "metamaskNetworkId": "3"
+ },
+ "time": 1482279685589,
+ "status": "unconfirmed",
+ "gasMultiplier": 1,
+ "metamaskNetworkId": "3",
+ "gasLimitSpecified": true,
+ "estimatedGas": "0x1af75",
+ "simulationFails": true
+ }
+ },
+ "currentFiat": "USD",
+ "conversionRate": 7.69158136,
+ "conversionDate": 1482279663,
+ "noActiveNotices": true,
+ "network": "3",
+ "accounts": {
+ "0xac39b311dceb2a4b2f5d8461c1cdaf756f4f7ae9": {
+ "code": "0x",
+ "nonce": "0x3",
+ "balance": "0x11f646fe14c9c000",
+ "address": "0xac39b311dceb2a4b2f5d8461c1cdaf756f4f7ae9"
+ },
+ "0xd7c0cd9e7d2701c710d64fc492c7086679bdf7b4": {
+ "code": "0x",
+ "nonce": "0x0",
+ "balance": "0x0",
+ "address": "0xd7c0cd9e7d2701c710d64fc492c7086679bdf7b4"
+ },
+ "0x1acfb961c5a8268eac8e09d6241a26cbeff42241": {
+ "code": "0x",
+ "balance": "0x0",
+ "nonce": "0x0",
+ "address": "0x1acfb961c5a8268eac8e09d6241a26cbeff42241"
+ },
+ "0xabc2bca51709b8615147352c62420f547a63a00c": {
+ "code": "0x",
+ "balance": "0x0",
+ "nonce": "0x0",
+ "address": "0xabc2bca51709b8615147352c62420f547a63a00c"
+ }
+ },
+ "transactions": [
+ {
+ "id": 7992944905869041,
+ "txParams": {
+ "from": "0xac39b311dceb2a4b2f5d8461c1cdaf756f4f7ae9",
+ "value": "0x0",
+ "data": "0x606060405234610000575b60da806100186000396000f30060606040526000357c0100000000000000000000000000000000000000000000000000000000900463ffffffff1680630dbe671f14603c575b6000565b3460005760466088565b604051808273ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200191505060405180910390f35b600060009054906101000a900473ffffffffffffffffffffffffffffffffffffffff16815600a165627a7a72305820a99dfa6091771f518dd1ae8d1ee347bae3304dffd98fd24b1b99a8380bc60a750029",
+ "gas": "0x1af75",
+ "metamaskId": 7992944905869041,
+ "metamaskNetworkId": "3"
+ },
+ "time": 1482279685589,
+ "status": "unconfirmed",
+ "gasMultiplier": 1,
+ "metamaskNetworkId": "3",
+ "gasLimitSpecified": true,
+ "estimatedGas": "0x1af75",
+ "simulationFails": true
+ }
+ ],
+ "provider": {
+ "type": "testnet"
+ },
+ "selectedAccount": "0xac39b311dceb2a4b2f5d8461c1cdaf756f4f7ae9",
+ "seedWords": false,
+ "isDisclaimerConfirmed": true,
+ "unconfMsgs": {},
+ "messages": [],
+ "shapeShiftTxList": [],
+ "keyringTypes": [
+ "Simple Key Pair",
+ "HD Key Tree"
+ ],
+ "lostAccounts": []
+ },
+ "appState": {
+ "menuOpen": false,
+ "currentView": {
+ "name": "confTx",
+ "context": 0
+ },
+ "accountDetail": {
+ "subview": "transactions"
+ },
+ "transForward": true,
+ "isLoading": false,
+ "warning": null
+ },
+ "identities": {}
+} \ No newline at end of file
diff --git a/development/states/import-private-key-warning.json b/development/states/import-private-key-warning.json
new file mode 100644
index 000000000..f4ac99b05
--- /dev/null
+++ b/development/states/import-private-key-warning.json
@@ -0,0 +1,92 @@
+{
+ "metamask": {
+ "isInitialized": true,
+ "isUnlocked": true,
+ "rpcTarget": "https://rawtestrpc.metamask.io/",
+ "identities": {
+ "0x01208723ba84e15da2e71656544a2963b0c06d40": {
+ "address": "0x01208723ba84e15da2e71656544a2963b0c06d40",
+ "name": "Account 1"
+ }
+ },
+ "unconfTxs": {},
+ "currentFiat": "USD",
+ "conversionRate": 10.1219126,
+ "conversionDate": 1484695442,
+ "noActiveNotices": true,
+ "network": "3",
+ "accounts": {
+ "0x01208723ba84e15da2e71656544a2963b0c06d40": {
+ "nonce": "0x0",
+ "balance": "0x0",
+ "code": "0x",
+ "address": "0x01208723ba84e15da2e71656544a2963b0c06d40"
+ }
+ },
+ "transactions": [],
+ "provider": {
+ "type": "testnet"
+ },
+ "selectedAccount": "0x01208723ba84e15da2e71656544a2963b0c06d40",
+ "selectedAccountTxList": [],
+ "seedWords": false,
+ "isDisclaimerConfirmed": true,
+ "unconfMsgs": {},
+ "messages": [],
+ "shapeShiftTxList": [],
+ "keyringTypes": [
+ "Simple Key Pair",
+ "HD Key Tree"
+ ],
+ "keyrings": [
+ {
+ "type": "Simple Key Pair",
+ "accounts": []
+ },
+ {
+ "type": "Simple Key Pair",
+ "accounts": []
+ },
+ {
+ "type": "Simple Key Pair",
+ "accounts": []
+ },
+ {
+ "type": "Simple Key Pair",
+ "accounts": []
+ },
+ {
+ "type": "Simple Key Pair",
+ "accounts": []
+ },
+ {
+ "type": "Simple Key Pair",
+ "accounts": []
+ },
+ {
+ "type": "Simple Key Pair",
+ "accounts": []
+ },
+ {
+ "type": "HD Key Tree",
+ "accounts": [
+ "01208723ba84e15da2e71656544a2963b0c06d40"
+ ]
+ }
+ ],
+ "lostAccounts": []
+ },
+ "appState": {
+ "menuOpen": false,
+ "currentView": {
+ "name": "import-menu"
+ },
+ "accountDetail": {
+ "subview": "transactions"
+ },
+ "transForward": true,
+ "isLoading": false,
+ "warning": "Invalid hex string"
+ },
+ "identities": {}
+} \ No newline at end of file
diff --git a/development/states/import-private-key.json b/development/states/import-private-key.json
new file mode 100644
index 000000000..c70f02a36
--- /dev/null
+++ b/development/states/import-private-key.json
@@ -0,0 +1,64 @@
+{
+ "metamask": {
+ "isInitialized": true,
+ "isUnlocked": true,
+ "rpcTarget": "https://rawtestrpc.metamask.io/",
+ "identities": {
+ "0x01208723ba84e15da2e71656544a2963b0c06d40": {
+ "address": "0x01208723ba84e15da2e71656544a2963b0c06d40",
+ "name": "Account 1"
+ }
+ },
+ "unconfTxs": {},
+ "currentFiat": "USD",
+ "conversionRate": 10.10788584,
+ "conversionDate": 1484694362,
+ "noActiveNotices": true,
+ "network": "3",
+ "accounts": {
+ "0x01208723ba84e15da2e71656544a2963b0c06d40": {
+ "balance": "0x0",
+ "code": "0x",
+ "nonce": "0x0",
+ "address": "0x01208723ba84e15da2e71656544a2963b0c06d40"
+ }
+ },
+ "transactions": [],
+ "provider": {
+ "type": "testnet"
+ },
+ "selectedAccount": "0x01208723ba84e15da2e71656544a2963b0c06d40",
+ "selectedAccountTxList": [],
+ "seedWords": null,
+ "isDisclaimerConfirmed": true,
+ "unconfMsgs": {},
+ "messages": [],
+ "shapeShiftTxList": [],
+ "keyringTypes": [
+ "Simple Key Pair",
+ "HD Key Tree"
+ ],
+ "keyrings": [
+ {
+ "type": "HD Key Tree",
+ "accounts": [
+ "01208723ba84e15da2e71656544a2963b0c06d40"
+ ]
+ }
+ ],
+ "lostAccounts": []
+ },
+ "appState": {
+ "menuOpen": false,
+ "currentView": {
+ "name": "import-menu"
+ },
+ "accountDetail": {
+ "subview": "transactions"
+ },
+ "transForward": true,
+ "isLoading": false,
+ "warning": null
+ },
+ "identities": {}
+} \ No newline at end of file
diff --git a/development/states/new-account.json b/development/states/new-account.json
new file mode 100644
index 000000000..8c9be3654
--- /dev/null
+++ b/development/states/new-account.json
@@ -0,0 +1,66 @@
+{
+ "metamask": {
+ "isInitialized": true,
+ "isUnlocked": true,
+ "rpcTarget": "https://rawtestrpc.metamask.io/",
+ "identities": {
+ "0xa6ef573d60594731178b7f85d80da13cc2af52dd": {
+ "address": "0xa6ef573d60594731178b7f85d80da13cc2af52dd",
+ "name": "Dan! 1"
+ },
+ "0xf9f52e84ad2c9122caa87478d27041ddaa215666": {
+ "address": "0xf9f52e84ad2c9122caa87478d27041ddaa215666",
+ "name": "Account 2"
+ }
+ },
+ "unconfTxs": {},
+ "currentFiat": "USD",
+ "conversionRate": 10.92067835,
+ "conversionDate": 1478282884,
+ "network": null,
+ "accounts": {
+ "0xa6ef573d60594731178b7f85d80da13cc2af52dd": {
+ "balance": "0x00",
+ "nonce": "0x100000",
+ "code": "0x",
+ "address": "0xa6ef573d60594731178b7f85d80da13cc2af52dd"
+ },
+ "0xf9f52e84ad2c9122caa87478d27041ddaa215666": {
+ "balance": "0x00",
+ "nonce": "0x100000",
+ "code": "0x",
+ "address": "0xf9f52e84ad2c9122caa87478d27041ddaa215666"
+ }
+ },
+ "transactions": [],
+ "provider": {
+ "type": "testnet"
+ },
+ "selectedAccount": "0xa6ef573d60594731178b7f85d80da13cc2af52dd",
+ "isConfirmed": true,
+ "unconfMsgs": {},
+ "messages": [],
+ "selectedAddress": "0xa6ef573d60594731178b7f85d80da13cc2af52dd",
+ "shapeShiftTxList": [],
+ "keyringTypes": [
+ "Simple Key Pair",
+ "HD Key Tree"
+ ]
+ },
+ "appState": {
+ "menuOpen": false,
+ "currentView": {
+ "name": "new-account"
+ },
+ "accountDetail": {
+ "subview": "transactions"
+ },
+ "transForward": true,
+ "isLoading": false,
+ "warning": null,
+ "forgottenPassword": null,
+ "detailView": {},
+ "scrollToBottom": false
+ },
+ "identities": {}
+} \ No newline at end of file
diff --git a/notices/notice_0.md b/notices/notice_0.md
deleted file mode 100644
index 1b2d5d018..000000000
--- a/notices/notice_0.md
+++ /dev/null
@@ -1,12 +0,0 @@
-Due to [recent events](https://blog.ethereum.org/2016/11/20/from-morden-to-ropsten/), MetaMask is now deprecating support for the Morden Test Network.
-
-Users will still be able to access Morden through a locally hosted node, but we will no longer be providing hosted access to this network through [Infura](http://infura.io/).
-
-Please use the new Ropsten Network as your new default test network.
-
-You can fund your Ropsten account using the buy button on your account page.
-
-Best wishes!
-
-The MetaMask Team
-
diff --git a/package.json b/package.json
index 954f5a10e..25a2abc86 100644
--- a/package.json
+++ b/package.json
@@ -84,19 +84,22 @@
"react-hyperscript": "^2.2.2",
"react-markdown": "^2.3.0",
"react-redux": "^4.4.5",
+ "react-select": "^1.0.0-rc.2",
+ "react-simple-file-input": "^1.0.0",
"react-tooltip-component": "^0.3.0",
"readable-stream": "^2.1.2",
"redux": "^3.0.5",
"redux-logger": "^2.3.1",
"redux-thunk": "^1.0.2",
"sandwich-expando": "^1.0.5",
+ "semaphore": "^1.0.5",
"textarea-caret": "^3.0.1",
"three.js": "^0.73.2",
"through2": "^2.0.1",
"valid-url": "^1.0.9",
"vreme": "^3.0.2",
"web3": "0.17.0-beta",
- "web3-provider-engine": "^8.2.0",
+ "web3-provider-engine": "^8.4.0",
"web3-stream-provider": "^2.0.6",
"xtend": "^4.0.1"
},
diff --git a/test/integration/lib/first-time.js b/test/integration/lib/first-time.js
index 1811ccbd4..777fcbb7e 100644
--- a/test/integration/lib/first-time.js
+++ b/test/integration/lib/first-time.js
@@ -66,7 +66,8 @@ QUnit.test('agree to terms', function (assert) {
}).then(function() {
var sandwich = app.find('.menu-droppo')[0]
- var lock = sandwich.children[2]
+ var children = sandwich.children
+ var lock = children[children.length - 2]
assert.ok(lock, 'Lock menu item found')
lock.click()
diff --git a/test/unit/explorer-link-test.js b/test/unit/explorer-link-test.js
index 961b400fd..8aa58bff9 100644
--- a/test/unit/explorer-link-test.js
+++ b/test/unit/explorer-link-test.js
@@ -4,7 +4,7 @@ var linkGen = require('../../ui/lib/explorer-link')
describe('explorer-link', function() {
it('adds testnet prefix to morden test network', function() {
- var result = linkGen('hash', '2')
+ var result = linkGen('hash', '3')
assert.notEqual(result.indexOf('testnet'), -1, 'testnet injected')
})
diff --git a/test/unit/keyrings/simple-test.js b/test/unit/keyrings/simple-test.js
index 979abdb69..77eeb834c 100644
--- a/test/unit/keyrings/simple-test.js
+++ b/test/unit/keyrings/simple-test.js
@@ -1,5 +1,6 @@
const assert = require('assert')
const extend = require('xtend')
+const ethUtil = require('ethereumjs-util')
const SimpleKeyring = require('../../../app/scripts/keyrings/simple')
const TYPE_STR = 'Simple Key Pair'
@@ -48,6 +49,24 @@ describe('simple-keyring', function() {
})
})
+ describe('#signMessage', function() {
+ const address = '0x9858e7d8b79fc3e6d989636721584498926da38a'
+ const message = '0x879a053d4800c6354e76c7985a865d2922c82fb5b3f4577b2fe08b998954f2e0'
+ const privateKey = '0x7dd98753d7b4394095de7d176c58128e2ed6ee600abe97c9f6d9fd65015d9b18'
+ const expectedResult = '0x28fcb6768e5110144a55b2e6ce9d1ea5a58103033632d272d2b5cf506906f7941a00b539383fd872109633d8c71c404e13dba87bc84166ee31b0e36061a69e161c'
+
+ it('passes the dennis test', function(done) {
+ keyring.deserialize([ privateKey ])
+ .then(() => {
+ return keyring.signMessage(address, message)
+ })
+ .then((result) => {
+ assert.equal(result, expectedResult)
+ done()
+ })
+ })
+ })
+
describe('#addAccounts', function() {
describe('with no arguments', function() {
it('creates a single wallet', function() {
@@ -72,14 +91,10 @@ describe('simple-keyring', function() {
it('calls getAddress on each wallet', function(done) {
// Push a mock wallet
- const desiredOutput = 'foo'
+ const desiredOutput = '0x18a3462427bcc9133bb46e88bcbe39cd7ef0e761'
keyring.wallets.push({
getAddress() {
- return {
- toString() {
- return desiredOutput
- }
- }
+ return ethUtil.toBuffer(desiredOutput)
}
})
diff --git a/test/unit/metamask-controller-test.js b/test/unit/metamask-controller-test.js
index e648ebd1d..24d9ddd67 100644
--- a/test/unit/metamask-controller-test.js
+++ b/test/unit/metamask-controller-test.js
@@ -27,24 +27,6 @@ describe('MetaMaskController', function() {
this.sinon.restore()
})
- describe('#enforceTxValidations', function () {
- it('returns null for positive values', function() {
- var sample = {
- value: '0x01'
- }
- var res = controller.enforceTxValidations(sample)
- assert.equal(res, null, 'no error')
- })
-
-
- it('returns error for negative values', function() {
- var sample = {
- value: '-0x01'
- }
- var res = controller.enforceTxValidations(sample)
- assert.ok(res, 'error')
- })
- })
})
diff --git a/test/unit/notice-controller-test.js b/test/unit/notice-controller-test.js
index 4aa4c8e7b..cf00daeba 100644
--- a/test/unit/notice-controller-test.js
+++ b/test/unit/notice-controller-test.js
@@ -5,13 +5,14 @@ const nock = require('nock')
const configManagerGen = require('../lib/mock-config-manager')
const NoticeController = require('../../app/scripts/notice-controller')
const STORAGE_KEY = 'metamask-persistance-key'
-// Hacking localStorage support into JSDom
-window.localStorage = {}
describe('notice-controller', function() {
var noticeController
beforeEach(function() {
+ // simple localStorage polyfill
+ window.localStorage = {}
+ if (window.localStorage.clear) window.localStorage.clear()
let configManager = configManagerGen()
noticeController = new NoticeController({
configManager: configManager,
diff --git a/test/unit/tx-manager-test.js b/test/unit/tx-manager-test.js
index be16facad..a66003f85 100644
--- a/test/unit/tx-manager-test.js
+++ b/test/unit/tx-manager-test.js
@@ -15,6 +15,28 @@ describe('Transaction Manager', function() {
provider: "testnet",
txHistoryLimit: 10,
blockTracker: new EventEmitter(),
+ getNetwork: function(){ return 'unit test' }
+ })
+ })
+
+ describe('#validateTxParams', function () {
+ it('returns null for positive values', function() {
+ var sample = {
+ value: '0x01'
+ }
+ var res = txManager.txProviderUtils.validateTxParams(sample, (err) => {
+ assert.equal(err, null, 'no error')
+ })
+ })
+
+
+ it('returns error for negative values', function() {
+ var sample = {
+ value: '-0x01'
+ }
+ var res = txManager.txProviderUtils.validateTxParams(sample, (err) => {
+ assert.ok(err, 'error')
+ })
})
})
@@ -31,7 +53,7 @@ describe('Transaction Manager', function() {
describe('#_saveTxList', function() {
it('saves the submitted data to the tx list', function() {
- var target = [{ foo: 'bar' }]
+ var target = [{ foo: 'bar', metamaskNetworkId: 'unit test' }]
txManager._saveTxList(target)
var result = txManager.getTxList()
assert.equal(result[0].foo, 'bar')
@@ -40,7 +62,7 @@ describe('Transaction Manager', function() {
describe('#addTx', function() {
it('adds a tx returned in getTxList', function() {
- var tx = { id: 1, status: 'confirmed',}
+ var tx = { id: 1, status: 'confirmed', metamaskNetworkId: 'unit test' }
txManager.addTx(tx, onTxDoneCb)
var result = txManager.getTxList()
assert.ok(Array.isArray(result))
@@ -51,7 +73,7 @@ describe('Transaction Manager', function() {
it('cuts off early txs beyond a limit', function() {
const limit = txManager.txHistoryLimit
for (let i = 0; i < limit + 1; i++) {
- let tx = { id: i, time: new Date(), status: 'confirmed'}
+ let tx = { id: i, time: new Date(), status: 'confirmed', metamaskNetworkId: 'unit test' }
txManager.addTx(tx, onTxDoneCb)
}
var result = txManager.getTxList()
@@ -59,10 +81,10 @@ describe('Transaction Manager', function() {
assert.equal(result[0].id, 1, 'early txs truncted')
})
- it('cuts off early txs beyond a limit weather or not it is confirmed or rejected', function() {
+ it('cuts off early txs beyond a limit whether or not it is confirmed or rejected', function() {
const limit = txManager.txHistoryLimit
for (let i = 0; i < limit + 1; i++) {
- let tx = { id: i, time: new Date(), status: 'rejected'}
+ let tx = { id: i, time: new Date(), status: 'rejected', metamaskNetworkId: 'unit test' }
txManager.addTx(tx, onTxDoneCb)
}
var result = txManager.getTxList()
@@ -71,11 +93,11 @@ describe('Transaction Manager', function() {
})
it('cuts off early txs beyond a limit but does not cut unapproved txs', function() {
- var unconfirmedTx = { id: 0, time: new Date(), status: 'unapproved'}
+ var unconfirmedTx = { id: 0, time: new Date(), status: 'unapproved', metamaskNetworkId: 'unit test' }
txManager.addTx(unconfirmedTx, onTxDoneCb)
const limit = txManager.txHistoryLimit
for (let i = 1; i < limit + 1; i++) {
- let tx = { id: i, time: new Date(), status: 'confirmed'}
+ let tx = { id: i, time: new Date(), status: 'confirmed', metamaskNetworkId: 'unit test' }
txManager.addTx(tx, onTxDoneCb)
}
var result = txManager.getTxList()
@@ -88,7 +110,7 @@ describe('Transaction Manager', function() {
describe('#setTxStatusSigned', function() {
it('sets the tx status to signed', function() {
- var tx = { id: 1, status: 'unapproved' }
+ var tx = { id: 1, status: 'unapproved', metamaskNetworkId: 'unit test' }
txManager.addTx(tx, onTxDoneCb)
txManager.setTxStatusSigned(1)
var result = txManager.getTxList()
@@ -99,20 +121,21 @@ describe('Transaction Manager', function() {
it('should emit a signed event to signal the exciton of callback', (done) => {
this.timeout(10000)
- var tx = { id: 1, status: 'unapproved' }
- let onTxDoneCb = function (err, txId) {
+ var tx = { id: 1, status: 'unapproved', metamaskNetworkId: 'unit test' }
+ let onTxDoneCb = function () {
assert(true, 'event listener has been triggered and onTxDoneCb executed')
done()
}
- txManager.addTx(tx, onTxDoneCb)
+ txManager.addTx(tx)
+ txManager.on('1:signed', onTxDoneCb)
txManager.setTxStatusSigned(1)
})
})
describe('#setTxStatusRejected', function() {
it('sets the tx status to rejected', function() {
- var tx = { id: 1, status: 'unapproved' }
- txManager.addTx(tx, onTxDoneCb)
+ var tx = { id: 1, status: 'unapproved', metamaskNetworkId: 'unit test' }
+ txManager.addTx(tx)
txManager.setTxStatusRejected(1)
var result = txManager.getTxList()
assert.ok(Array.isArray(result))
@@ -122,12 +145,13 @@ describe('Transaction Manager', function() {
it('should emit a rejected event to signal the exciton of callback', (done) => {
this.timeout(10000)
- var tx = { id: 1, status: 'unapproved' }
+ var tx = { id: 1, status: 'unapproved', metamaskNetworkId: 'unit test' }
+ txManager.addTx(tx)
let onTxDoneCb = function (err, txId) {
assert(true, 'event listener has been triggered and onTxDoneCb executed')
done()
}
- txManager.addTx(tx, onTxDoneCb)
+ txManager.on('1:rejected', onTxDoneCb)
txManager.setTxStatusRejected(1)
})
@@ -135,9 +159,9 @@ describe('Transaction Manager', function() {
describe('#updateTx', function() {
it('replaces the tx with the same id', function() {
- txManager.addTx({ id: '1', status: 'unapproved' }, onTxDoneCb)
- txManager.addTx({ id: '2', status: 'confirmed' }, onTxDoneCb)
- txManager.updateTx({ id: '1', status: 'blah', hash: 'foo' })
+ txManager.addTx({ id: '1', status: 'unapproved', metamaskNetworkId: 'unit test' }, onTxDoneCb)
+ txManager.addTx({ id: '2', status: 'confirmed', metamaskNetworkId: 'unit test' }, onTxDoneCb)
+ txManager.updateTx({ id: '1', status: 'blah', hash: 'foo', metamaskNetworkId: 'unit test' })
var result = txManager.getTx('1')
assert.equal(result.hash, 'foo')
})
@@ -145,8 +169,8 @@ describe('Transaction Manager', function() {
describe('#getUnapprovedTxList', function() {
it('returns unapproved txs in a hash', function() {
- txManager.addTx({ id: '1', status: 'unapproved' }, onTxDoneCb)
- txManager.addTx({ id: '2', status: 'confirmed' }, onTxDoneCb)
+ txManager.addTx({ id: '1', status: 'unapproved', metamaskNetworkId: 'unit test' }, onTxDoneCb)
+ txManager.addTx({ id: '2', status: 'confirmed', metamaskNetworkId: 'unit test' }, onTxDoneCb)
let result = txManager.getUnapprovedTxList()
assert.equal(typeof result, 'object')
assert.equal(result['1'].status, 'unapproved')
@@ -156,8 +180,8 @@ describe('Transaction Manager', function() {
describe('#getTx', function() {
it('returns a tx with the requested id', function() {
- txManager.addTx({ id: '1', status: 'unapproved' }, onTxDoneCb)
- txManager.addTx({ id: '2', status: 'confirmed' }, onTxDoneCb)
+ txManager.addTx({ id: '1', status: 'unapproved', metamaskNetworkId: 'unit test' }, onTxDoneCb)
+ txManager.addTx({ id: '2', status: 'confirmed', metamaskNetworkId: 'unit test' }, onTxDoneCb)
assert.equal(txManager.getTx('1').status, 'unapproved')
assert.equal(txManager.getTx('2').status, 'confirmed')
})
@@ -171,6 +195,7 @@ describe('Transaction Manager', function() {
let everyOther = i % 2
txManager.addTx({ id: i,
status: everyOther ? 'unapproved' : 'confirmed',
+ metamaskNetworkId: 'unit test',
txParams: {
from: everyOther ? 'foop' : 'zoop',
to: everyOther ? 'zoop' : 'foop',
diff --git a/ui/app/account-detail.js b/ui/app/account-detail.js
index c41ba61fd..7a0c599ba 100644
--- a/ui/app/account-detail.js
+++ b/ui/app/account-detail.js
@@ -26,11 +26,10 @@ function mapStateToProps (state) {
accounts: state.metamask.accounts,
address: state.metamask.selectedAccount,
accountDetail: state.appState.accountDetail,
- transactions: state.metamask.transactions,
network: state.metamask.network,
- unconfTxs: valuesFor(state.metamask.unconfTxs),
unconfMsgs: valuesFor(state.metamask.unconfMsgs),
shapeShiftTxList: state.metamask.shapeShiftTxList,
+ transactions: state.metamask.selectedAccountTxList || [],
}
}
@@ -248,20 +247,10 @@ AccountDetailScreen.prototype.subview = function () {
}
AccountDetailScreen.prototype.transactionList = function () {
- const { transactions, unconfTxs, unconfMsgs, address, network, shapeShiftTxList } = this.props
-
- var txsToRender = transactions.concat(unconfTxs)
- // only transactions that are from the current address
- .filter(tx => tx.txParams.from === address)
- // only transactions that are on the current network
- .filter(tx => tx.txParams.metamaskNetworkId === network)
- // sort by recency
- .sort((a, b) => b.time - a.time)
-
+ const {transactions, unconfMsgs, address, network, shapeShiftTxList } = this.props
return h(TransactionList, {
- txsToRender,
+ transactions: transactions.sort((a, b) => b.time - a.time),
network,
- unconfTxs,
unconfMsgs,
address,
shapeShiftTxList,
diff --git a/ui/app/accounts/import/index.js b/ui/app/accounts/import/index.js
new file mode 100644
index 000000000..96350852a
--- /dev/null
+++ b/ui/app/accounts/import/index.js
@@ -0,0 +1,91 @@
+const inherits = require('util').inherits
+const Component = require('react').Component
+const h = require('react-hyperscript')
+const connect = require('react-redux').connect
+import Select from 'react-select'
+
+// Subviews
+const JsonImportView = require('./json.js')
+const PrivateKeyImportView = require('./private-key.js')
+
+const menuItems = [
+ 'Private Key',
+ 'JSON File',
+]
+
+module.exports = connect(mapStateToProps)(AccountImportSubview)
+
+function mapStateToProps (state) {
+ return {
+ menuItems,
+ }
+}
+
+inherits(AccountImportSubview, Component)
+function AccountImportSubview () {
+ Component.call(this)
+}
+
+AccountImportSubview.prototype.render = function () {
+ const props = this.props
+ const state = this.state || {}
+ const { menuItems } = props
+ const { type } = state
+
+ return (
+ h('div', {
+ style: {
+ },
+ }, [
+ h('div', {
+ style: {
+ padding: '10px',
+ color: 'rgb(174, 174, 174)',
+ },
+ }, [
+
+ h('h3', { style: { padding: '3px' } }, 'SELECT TYPE'),
+
+ h('style', `
+ .has-value.Select--single > .Select-control .Select-value .Select-value-label, .Select-value-label {
+ color: rgb(174,174,174);
+ }
+ `),
+
+ h(Select, {
+ name: 'import-type-select',
+ clearable: false,
+ value: type || menuItems[0],
+ options: menuItems.map((type) => {
+ return {
+ value: type,
+ label: type,
+ }
+ }),
+ onChange: (opt) => {
+ this.setState({ type: opt.value })
+ },
+ }),
+ ]),
+
+ this.renderImportView(),
+ ])
+ )
+}
+
+AccountImportSubview.prototype.renderImportView = function() {
+ const props = this.props
+ const state = this.state || {}
+ const { type } = state
+ const { menuItems } = props
+ const current = type || menuItems[0]
+
+ switch (current) {
+ case 'Private Key':
+ return h(PrivateKeyImportView)
+ case 'JSON File':
+ return h(JsonImportView)
+ default:
+ return h(JsonImportView)
+ }
+}
diff --git a/ui/app/accounts/import/json.js b/ui/app/accounts/import/json.js
new file mode 100644
index 000000000..1c2b331d4
--- /dev/null
+++ b/ui/app/accounts/import/json.js
@@ -0,0 +1,98 @@
+const inherits = require('util').inherits
+const Component = require('react').Component
+const h = require('react-hyperscript')
+const connect = require('react-redux').connect
+const actions = require('../../actions')
+const FileInput = require('react-simple-file-input').default
+
+module.exports = connect(mapStateToProps)(JsonImportSubview)
+
+function mapStateToProps (state) {
+ return {
+ error: state.appState.warning,
+ }
+}
+
+inherits(JsonImportSubview, Component)
+function JsonImportSubview () {
+ Component.call(this)
+}
+
+JsonImportSubview.prototype.render = function () {
+ const { error } = this.props
+
+ return (
+ h('div', {
+ style: {
+ display: 'flex',
+ flexDirection: 'column',
+ alignItems: 'center',
+ padding: '5px 15px 0px 15px',
+ },
+ }, [
+
+ h('p', 'Used by a variety of different clients'),
+
+ h(FileInput, {
+ readAs: 'text',
+ onLoad: this.onLoad.bind(this),
+ style: {
+ margin: '20px 0px 12px 20px',
+ fontSize: '15px',
+ },
+ }),
+
+ h('input.large-input.letter-spacey', {
+ type: 'password',
+ placeholder: 'Enter password',
+ id: 'json-password-box',
+ onKeyPress: this.createKeyringOnEnter.bind(this),
+ style: {
+ width: 260,
+ marginTop: 12,
+ },
+ }),
+
+ h('button.primary', {
+ onClick: this.createNewKeychain.bind(this),
+ style: {
+ margin: 12,
+ },
+ }, 'Import'),
+
+ error ? h('span.warning', error) : null,
+ ])
+ )
+}
+
+JsonImportSubview.prototype.onLoad = function (event, file) {
+ this.setState({file: file, fileContents: event.target.result})
+}
+
+JsonImportSubview.prototype.createKeyringOnEnter = function (event) {
+ if (event.key === 'Enter') {
+ event.preventDefault()
+ this.createNewKeychain()
+ }
+}
+
+JsonImportSubview.prototype.createNewKeychain = function () {
+ const state = this.state
+ const { fileContents } = state
+
+ if (!fileContents) {
+ const message = 'You must select a file to import.'
+ return this.props.dispatch(actions.displayWarning(message))
+ }
+
+ const passwordInput = document.getElementById('json-password-box')
+ const password = passwordInput.value
+
+ if (!password) {
+ const message = 'You must enter a password for the selected file.'
+ return this.props.dispatch(actions.displayWarning(message))
+ }
+
+ this.props.dispatch(actions.importNewAccount('JSON File', [ fileContents, password ]))
+}
+
diff --git a/ui/app/accounts/import/private-key.js b/ui/app/accounts/import/private-key.js
new file mode 100644
index 000000000..b139a0374
--- /dev/null
+++ b/ui/app/accounts/import/private-key.js
@@ -0,0 +1,68 @@
+const inherits = require('util').inherits
+const Component = require('react').Component
+const h = require('react-hyperscript')
+const connect = require('react-redux').connect
+const actions = require('../../actions')
+
+module.exports = connect(mapStateToProps)(PrivateKeyImportView)
+
+function mapStateToProps (state) {
+ return {
+ error: state.appState.warning,
+ }
+}
+
+inherits(PrivateKeyImportView, Component)
+function PrivateKeyImportView () {
+ Component.call(this)
+}
+
+PrivateKeyImportView.prototype.render = function () {
+ const { error } = this.props
+
+ return (
+ h('div', {
+ style: {
+ display: 'flex',
+ flexDirection: 'column',
+ alignItems: 'center',
+ padding: '5px 15px 0px 15px',
+ },
+ }, [
+ h('span', 'Paste your private key string here'),
+
+ h('input.large-input.letter-spacey', {
+ type: 'password',
+ id: 'private-key-box',
+ onKeyPress: this.createKeyringOnEnter.bind(this),
+ style: {
+ width: 260,
+ marginTop: 12,
+ },
+ }),
+
+ h('button.primary', {
+ onClick: this.createNewKeychain.bind(this),
+ style: {
+ margin: 12,
+ },
+ }, 'Import'),
+
+ error ? h('span.warning', error) : null,
+ ])
+ )
+}
+
+PrivateKeyImportView.prototype.createKeyringOnEnter = function (event) {
+ if (event.key === 'Enter') {
+ event.preventDefault()
+ this.createNewKeychain()
+ }
+}
+
+PrivateKeyImportView.prototype.createNewKeychain = function () {
+ const input = document.getElementById('private-key-box')
+ const privateKey = input.value
+ this.props.dispatch(actions.importNewAccount('Private Key', [ privateKey ]))
+}
+
diff --git a/ui/app/accounts/import/seed.js b/ui/app/accounts/import/seed.js
new file mode 100644
index 000000000..b4a7c0afa
--- /dev/null
+++ b/ui/app/accounts/import/seed.js
@@ -0,0 +1,30 @@
+const inherits = require('util').inherits
+const Component = require('react').Component
+const h = require('react-hyperscript')
+const connect = require('react-redux').connect
+
+module.exports = connect(mapStateToProps)(SeedImportSubview)
+
+function mapStateToProps (state) {
+ return {}
+}
+
+inherits(SeedImportSubview, Component)
+function SeedImportSubview () {
+ Component.call(this)
+}
+
+SeedImportSubview.prototype.render = function () {
+ return (
+ h('div', {
+ style: {
+ },
+ }, [
+ `Paste your seed phrase here!`,
+ h('textarea'),
+ h('br'),
+ h('button', 'Submit'),
+ ])
+ )
+}
+
diff --git a/ui/app/accounts/index.js b/ui/app/accounts/index.js
index edb15eafe..e6f376735 100644
--- a/ui/app/accounts/index.js
+++ b/ui/app/accounts/index.js
@@ -73,7 +73,8 @@ AccountsScreen.prototype.render = function () {
const simpleAddress = identity.address.substring(2).toLowerCase()
const keyring = keyrings.find((kr) => {
- return kr.accounts.includes(simpleAddress)
+ return kr.accounts.includes(simpleAddress) ||
+ kr.accounts.includes(identity.address)
})
return h(AccountListItem, {
@@ -154,6 +155,13 @@ AccountsScreen.prototype.addNewAccount = function () {
this.props.dispatch(actions.addNewAccount(0))
}
+/* An optional view proposed in this design:
+ * https://consensys.quip.com/zZVrAysM5znY
+AccountsScreen.prototype.addNewAccount = function () {
+ this.props.dispatch(actions.navigateToNewAccountScreen())
+}
+*/
+
AccountsScreen.prototype.goHome = function () {
this.props.dispatch(actions.goHome())
}
diff --git a/ui/app/actions.js b/ui/app/actions.js
index 5a3968f82..bf3617310 100644
--- a/ui/app/actions.js
+++ b/ui/app/actions.js
@@ -32,16 +32,21 @@ var actions = {
SHOW_INIT_MENU: 'SHOW_INIT_MENU',
SHOW_NEW_VAULT_SEED: 'SHOW_NEW_VAULT_SEED',
SHOW_INFO_PAGE: 'SHOW_INFO_PAGE',
+ SHOW_IMPORT_PAGE: 'SHOW_IMPORT_PAGE',
unlockMetamask: unlockMetamask,
unlockFailed: unlockFailed,
showCreateVault: showCreateVault,
showRestoreVault: showRestoreVault,
showInitializeMenu: showInitializeMenu,
+ showImportPage,
createNewVaultAndKeychain: createNewVaultAndKeychain,
createNewVaultAndRestore: createNewVaultAndRestore,
createNewVaultInProgress: createNewVaultInProgress,
addNewKeyring,
+ importNewAccount,
addNewAccount,
+ NEW_ACCOUNT_SCREEN: 'NEW_ACCOUNT_SCREEN',
+ navigateToNewAccountScreen,
showNewVaultSeed: showNewVaultSeed,
showInfoPage: showInfoPage,
// seed recovery actions
@@ -249,7 +254,36 @@ function requestRevealSeed (password) {
}
function addNewKeyring (type, opts) {
- return callBackgroundThenUpdate(background.addNewKeyring, type, opts)
+ return (dispatch) => {
+ dispatch(actions.showLoadingIndication())
+ background.addNewKeyring(type, opts, (err, newState) => {
+ dispatch(actions.hideLoadingIndication())
+ if (err) return dispatch(actions.displayWarning(err.message))
+ dispatch(actions.updateMetamaskState(newState))
+ dispatch(actions.showAccountsPage())
+ })
+ }
+}
+
+function importNewAccount (strategy, args) {
+ return (dispatch) => {
+ dispatch(actions.showLoadingIndication('This may take a while, be patient.'))
+ background.importAccountWithStrategy(strategy, args, (err, newState) => {
+ dispatch(actions.hideLoadingIndication())
+ if (err) return dispatch(actions.displayWarning(err.message))
+ dispatch(actions.updateMetamaskState(newState))
+ dispatch({
+ type: actions.SHOW_ACCOUNT_DETAIL,
+ value: newState.selectedAccount,
+ })
+ })
+ }
+}
+
+function navigateToNewAccountScreen() {
+ return {
+ type: this.NEW_ACCOUNT_SCREEN,
+ }
}
function addNewAccount (ringNumber = 0) {
@@ -376,6 +410,12 @@ function showInitializeMenu () {
}
}
+function showImportPage () {
+ return {
+ type: actions.SHOW_IMPORT_PAGE,
+ }
+}
+
function agreeToDisclaimer () {
return (dispatch) => {
dispatch(this.showLoadingIndication())
@@ -590,9 +630,10 @@ function useEtherscanProvider () {
}
}
-function showLoadingIndication () {
+function showLoadingIndication (message) {
return {
type: actions.SHOW_LOADING,
+ value: message,
}
}
diff --git a/ui/app/app.js b/ui/app/app.js
index 9efe95874..d8dedd397 100644
--- a/ui/app/app.js
+++ b/ui/app/app.js
@@ -20,6 +20,7 @@ const NoticeScreen = require('./components/notice')
const generateLostAccountsNotice = require('../lib/lost-accounts-notice')
// other views
const ConfigScreen = require('./config')
+const Import = require('./accounts/import')
const InfoScreen = require('./info')
const LoadingIndicator = require('./components/loading')
const SandwichExpando = require('sandwich-expando')
@@ -42,6 +43,7 @@ function mapStateToProps (state) {
return {
// state from plugin
isLoading: state.appState.isLoading,
+ loadingMessage: state.appState.loadingMessage,
isDisclaimerConfirmed: state.metamask.isDisclaimerConfirmed,
noActiveNotices: state.metamask.noActiveNotices,
isInitialized: state.metamask.isInitialized,
@@ -63,7 +65,7 @@ function mapStateToProps (state) {
App.prototype.render = function () {
var props = this.props
- const { isLoading, transForward } = props
+ const { isLoading, loadingMessage, transForward } = props
return (
@@ -75,7 +77,7 @@ App.prototype.render = function () {
},
}, [
- h(LoadingIndicator, { isLoading }),
+ h(LoadingIndicator, { isLoading, loadingMessage }),
// app bar
this.renderAppBar(),
@@ -305,6 +307,13 @@ App.prototype.renderDropdown = function () {
}),
h(DropMenuItem, {
+ label: 'Import Account',
+ closeMenu: () => this.setState({ isMainMenuOpen: !isOpen }),
+ action: () => this.props.dispatch(actions.showImportPage()),
+ icon: h('i.fa.fa-arrow-circle-o-up.fa-lg'),
+ }),
+
+ h(DropMenuItem, {
label: 'Lock',
closeMenu: () => this.setState({ isMainMenuOpen: !isOpen }),
action: () => this.props.dispatch(actions.lockMetamask()),
@@ -411,6 +420,9 @@ App.prototype.renderPrimary = function () {
case 'config':
return h(ConfigScreen, {key: 'config'})
+ case 'import-menu':
+ return h(Import, {key: 'import-menu'})
+
case 'reveal-seed-conf':
return h(RevealSeedConfirmation, {key: 'reveal-seed-conf'})
diff --git a/ui/app/components/buy-button-subview.js b/ui/app/components/buy-button-subview.js
index 35eda647e..afda5bf59 100644
--- a/ui/app/components/buy-button-subview.js
+++ b/ui/app/components/buy-button-subview.js
@@ -7,6 +7,7 @@ const CoinbaseForm = require('./coinbase-form')
const ShapeshiftForm = require('./shapeshift-form')
const extension = require('../../../app/scripts/lib/extension')
const Loading = require('./loading')
+const TabBar = require('./tab-bar')
module.exports = connect(mapStateToProps)(BuyButtonSubview)
@@ -29,7 +30,6 @@ function BuyButtonSubview () {
BuyButtonSubview.prototype.render = function () {
const props = this.props
- const currentForm = props.buyView.formView
const isLoading = props.isSubLoading
return (
@@ -53,43 +53,53 @@ BuyButtonSubview.prototype.render = function () {
h(Loading, { isLoading }),
- h('h3.flex-row.text-transform-uppercase', {
- style: {
- background: '#EBEBEB',
- color: '#AEAEAE',
- paddingTop: '4px',
- justifyContent: 'space-around',
+ h(TabBar, {
+ tabs: [
+ {
+ content: [
+ 'Coinbase',
+ h('a', {
+ onClick: (event) => this.navigateTo('https://github.com/MetaMask/faq/blob/master/COINBASE.md'),
+ }, [
+ h('i.fa.fa-question-circle', {
+ style: {
+ margin: '0px 5px',
+ },
+ }),
+ ]),
+ ],
+ key: 'coinbase',
+ },
+ {
+ content: [
+ 'Shapeshift',
+ h('a', {
+ href: 'https://github.com/MetaMask/faq/blob/master/COINBASE.md',
+ onClick: (event) => this.navigateTo('https://info.shapeshift.io/about'),
+ }, [
+ h('i.fa.fa-question-circle', {
+ style: {
+ margin: '0px 5px',
+ },
+ }),
+ ]),
+ ],
+ key: 'shapeshift',
+ },
+ ],
+ defaultTab: 'coinbase',
+ tabSelected: (key) => {
+ switch (key) {
+ case 'coinbase':
+ props.dispatch(actions.coinBaseSubview())
+ break
+ case 'shapeshift':
+ props.dispatch(actions.shapeShiftSubview(props.provider.type))
+ break
+ }
},
- }, [
- h(currentForm.coinbase ? '.activeForm' : '.inactiveForm.pointer', {
- onClick: () => props.dispatch(actions.coinBaseSubview()),
- }, 'Coinbase'),
- h('a', {
- onClick: (event) => this.navigateTo('https://github.com/MetaMask/faq/blob/master/COINBASE.md'),
- }, [
- h('i.fa.fa-question-circle', {
- style: {
- position: 'relative',
- right: '33px',
- },
- }),
- ]),
- h(currentForm.shapeshift ? '.activeForm' : '.inactiveForm.pointer', {
- onClick: () => props.dispatch(actions.shapeShiftSubview(props.provider.type)),
- }, 'Shapeshift'),
+ }),
- h('a', {
- href: 'https://github.com/MetaMask/faq/blob/master/COINBASE.md',
- onClick: (event) => this.navigateTo('https://info.shapeshift.io/about'),
- }, [
- h('i.fa.fa-question-circle', {
- style: {
- position: 'relative',
- right: '28px',
- },
- }),
- ]),
- ]),
this.formVersionSubview(),
])
)
diff --git a/ui/app/components/coinbase-form.js b/ui/app/components/coinbase-form.js
index 693eb2ea8..430a3eead 100644
--- a/ui/app/components/coinbase-form.js
+++ b/ui/app/components/coinbase-form.js
@@ -72,7 +72,7 @@ CoinbaseForm.prototype.render = function () {
lineHeight: '13px',
},
},
- `there is a USD$ 5 a day max and a USD$ 50
+ `there is a USD$ 15 a day max and a USD$ 50
dollar limit per the life time of an account without a
coinbase account. A fee of 3.75% will be aplied to debit/credit cards.`),
@@ -136,14 +136,14 @@ CoinbaseForm.prototype.renderLoading = function () {
function isValidAmountforCoinBase (amount) {
amount = parseFloat(amount)
if (amount) {
- if (amount <= 5 && amount > 0) {
+ if (amount <= 15 && amount > 0) {
return {
valid: true,
}
- } else if (amount > 5) {
+ } else if (amount > 15) {
return {
valid: false,
- message: 'The amount can not be greater then $5',
+ message: 'The amount can not be greater then $15',
}
} else {
return {
diff --git a/ui/app/components/loading.js b/ui/app/components/loading.js
index ae735894f..88dc535df 100644
--- a/ui/app/components/loading.js
+++ b/ui/app/components/loading.js
@@ -12,7 +12,7 @@ function LoadingIndicator () {
}
LoadingIndicator.prototype.render = function () {
- var isLoading = this.props.isLoading
+ const { isLoading, loadingMessage } = this.props
return (
h(ReactCSSTransitionGroup, {
@@ -37,8 +37,14 @@ LoadingIndicator.prototype.render = function () {
h('img', {
src: 'images/loading.svg',
}),
+
+ showMessageIfAny(loadingMessage),
]) : null,
])
)
}
+function showMessageIfAny (loadingMessage) {
+ if (!loadingMessage) return null
+ return h('span', loadingMessage)
+}
diff --git a/ui/app/components/pending-tx-details.js b/ui/app/components/pending-tx-details.js
index 89472b221..286931f6f 100644
--- a/ui/app/components/pending-tx-details.js
+++ b/ui/app/components/pending-tx-details.js
@@ -7,8 +7,6 @@ const EthBalance = require('./eth-balance')
const util = require('../util')
const addressSummary = util.addressSummary
const nameForAddress = require('../../lib/contract-namer')
-const ethUtil = require('ethereumjs-util')
-const BN = ethUtil.BN
module.exports = PendingTxDetails
@@ -29,15 +27,9 @@ PTXP.render = function () {
var account = props.accounts[address]
var balance = account ? account.balance : '0x0'
- var gasMultiplier = txData.gasMultiplier
- var gasCost = new BN(ethUtil.stripHexPrefix(txParams.gas || txData.estimatedGas), 16)
- var gasPrice = new BN(ethUtil.stripHexPrefix(txParams.gasPrice || '0x4a817c800'), 16)
- gasPrice = gasPrice.mul(new BN(gasMultiplier * 100), 10).div(new BN(100, 10))
- var txFee = gasCost.mul(gasPrice)
- var txValue = new BN(ethUtil.stripHexPrefix(txParams.value || '0x0'), 16)
- var maxCost = txValue.add(txFee)
+ var txFee = txData.txFee || ''
+ var maxCost = txData.maxCost || ''
var dataLength = txParams.data ? (txParams.data.length - 2) / 2 : 0
-
var imageify = props.imageifyIdenticons === undefined ? true : props.imageifyIdenticons
return (
diff --git a/ui/app/components/tab-bar.js b/ui/app/components/tab-bar.js
new file mode 100644
index 000000000..65078e0a4
--- /dev/null
+++ b/ui/app/components/tab-bar.js
@@ -0,0 +1,35 @@
+const Component = require('react').Component
+const h = require('react-hyperscript')
+const inherits = require('util').inherits
+
+module.exports = TabBar
+
+inherits(TabBar, Component)
+function TabBar () {
+ Component.call(this)
+}
+
+TabBar.prototype.render = function () {
+ const props = this.props
+ const state = this.state || {}
+ const { tabs = [], defaultTab, tabSelected } = props
+ const { subview = defaultTab } = state
+
+ return (
+ h('.flex-row.space-around.text-transform-uppercase', {
+ style: {
+ background: '#EBEBEB',
+ color: '#AEAEAE',
+ paddingTop: '4px',
+ },
+ }, tabs.map((tab) => {
+ const { key, content } = tab
+ return h(subview === key ? '.activeForm' : '.inactiveForm.pointer', {
+ onClick: () => {
+ this.setState({ subview: key })
+ tabSelected(key)
+ },
+ }, content)
+ }))
+ )
+}
diff --git a/ui/app/components/transaction-list-item-icon.js b/ui/app/components/transaction-list-item-icon.js
index 8b118b1d4..353401099 100644
--- a/ui/app/components/transaction-list-item-icon.js
+++ b/ui/app/components/transaction-list-item-icon.js
@@ -13,13 +13,40 @@ function TransactionIcon () {
TransactionIcon.prototype.render = function () {
const { transaction, txParams, isMsg } = this.props
+ switch (transaction.status) {
+ case 'unapproved':
+ return h('.unapproved-tx', {
+ style: {
+ width: '24px',
+ height: '24px',
+ background: '#4dffff',
+ border: 'solid',
+ borderColor: '#AEAEAE',
+ borderWidth: '0.5px',
+ borderRadius: '13px',
+ },
+ })
- if (transaction.status === 'rejected') {
- return h('i.fa.fa-exclamation-triangle.fa-lg.warning', {
- style: {
- width: '24px',
- },
- })
+ case 'rejected':
+ return h('i.fa.fa-exclamation-triangle.fa-lg.warning', {
+ style: {
+ width: '24px',
+ },
+ })
+
+ case 'failed':
+ return h('i.fa.fa-exclamation-triangle.fa-lg.error', {
+ style: {
+ width: '24px',
+ },
+ })
+
+ case 'submitted':
+ return h('i.fa.fa-ellipsis-h', {
+ style: {
+ fontSize: '27px',
+ },
+ })
}
if (isMsg) {
diff --git a/ui/app/components/transaction-list-item.js b/ui/app/components/transaction-list-item.js
index bb685abda..95e850264 100644
--- a/ui/app/components/transaction-list-item.js
+++ b/ui/app/components/transaction-list-item.js
@@ -8,6 +8,7 @@ const explorerLink = require('../../lib/explorer-link')
const CopyButton = require('./copyButton')
const vreme = new (require('vreme'))
const extension = require('../../../app/scripts/lib/extension')
+const Tooltip = require('./tooltip')
const TransactionIcon = require('./transaction-list-item-icon')
const ShiftListItem = require('./shift-list-item')
@@ -27,7 +28,7 @@ TransactionListItem.prototype.render = function () {
let isLinkable = false
const numericNet = parseInt(network)
- isLinkable = numericNet === 1 || numericNet === 2
+ isLinkable = numericNet === 1 || numericNet === 3
var isMsg = ('msgParams' in transaction)
var isTx = ('txParams' in transaction)
@@ -41,7 +42,6 @@ TransactionListItem.prototype.render = function () {
}
const isClickable = ('hash' in transaction && isLinkable) || isPending
-
return (
h(`.transaction-list-item.flex-row.flex-space-between${isClickable ? '.pointer' : ''}`, {
onClick: (event) => {
@@ -59,11 +59,7 @@ TransactionListItem.prototype.render = function () {
}, [
h('.identicon-wrapper.flex-column.flex-center.select-none', [
- transaction.status === 'unapproved' ? h('i.fa.fa-ellipsis-h', {
- style: {
- fontSize: '27px',
- },
- }) : h('.pop-hover', {
+ h('.pop-hover', {
onClick: (event) => {
event.stopPropagation()
if (!isTx || isPending) return
@@ -139,7 +135,14 @@ function failIfFailed (transaction) {
if (transaction.status === 'rejected') {
return h('span.error', ' (Rejected)')
}
- if (transaction.status === 'failed') {
- return h('span.error', ' (Failed)')
+ if (transaction.err) {
+
+ return h(Tooltip, {
+ title: transaction.err.message,
+ position: 'bottom',
+ }, [
+ h('span.error', ' (Failed)'),
+ ])
}
+
}
diff --git a/ui/app/components/transaction-list.js b/ui/app/components/transaction-list.js
index 7e1bedb05..b055ca9d5 100644
--- a/ui/app/components/transaction-list.js
+++ b/ui/app/components/transaction-list.js
@@ -13,12 +13,13 @@ function TransactionList () {
}
TransactionList.prototype.render = function () {
- const { txsToRender, network, unconfMsgs } = this.props
+ const { transactions, network, unconfMsgs } = this.props
+
var shapeShiftTxList
if (network === '1') {
shapeShiftTxList = this.props.shapeShiftTxList
}
- const transactions = !shapeShiftTxList ? txsToRender.concat(unconfMsgs) : txsToRender.concat(unconfMsgs, shapeShiftTxList)
+ const txsToRender = !shapeShiftTxList ? transactions.concat(unconfMsgs) : transactions.concat(unconfMsgs, shapeShiftTxList)
.sort((a, b) => b.time - a.time)
return (
@@ -55,8 +56,8 @@ TransactionList.prototype.render = function () {
},
}, [
- transactions.length
- ? transactions.map((transaction, i) => {
+ txsToRender.length
+ ? txsToRender.map((transaction, i) => {
let key
switch (transaction.key) {
case 'shapeshift':
diff --git a/ui/app/conf-tx.js b/ui/app/conf-tx.js
index 5a645022a..a6e03c3ed 100644
--- a/ui/app/conf-tx.js
+++ b/ui/app/conf-tx.js
@@ -41,11 +41,13 @@ ConfirmTxScreen.prototype.render = function () {
var provider = state.provider
var unconfTxs = state.unconfTxs
var unconfMsgs = state.unconfMsgs
+
var unconfTxList = txHelper(unconfTxs, unconfMsgs, network)
- var index = state.index !== undefined ? state.index : 0
- var txData = unconfTxList[index] || unconfTxList[0] || {}
- var txParams = txData.txParams || {}
+ var index = state.index !== undefined && unconfTxList[index] ? state.index : 0
+ var txData = unconfTxList[index] || {}
+ var txParams = txData.params || {}
var isNotification = isPopupOrNotification() === 'notification'
+ if (unconfTxList.length === 0) return null
return (
@@ -115,27 +117,24 @@ ConfirmTxScreen.prototype.render = function () {
}
function currentTxView (opts) {
- if ('txParams' in opts.txData) {
+ const { txData } = opts
+ const { txParams, msgParams } = txData
+
+ if (txParams) {
// This is a pending transaction
return h(PendingTx, opts)
- } else if ('msgParams' in opts.txData) {
+ } else if (msgParams) {
// This is a pending message to sign
return h(PendingMsg, opts)
}
}
ConfirmTxScreen.prototype.checkBalanceAgainstTx = function (txData) {
+ if (!txData.txParams) return false
var state = this.props
-
- var txParams = txData.txParams || {}
- var address = txParams.from || state.selectedAccount
+ var address = txData.txParams.from || state.selectedAccount
var account = state.accounts[address]
var balance = account ? account.balance : '0x0'
-
- var gasCost = new BN(ethUtil.stripHexPrefix(txParams.gas || txData.estimatedGas), 16)
- var gasPrice = new BN(ethUtil.stripHexPrefix(txParams.gasPrice || '0x4a817c800'), 16)
- var txFee = gasCost.mul(gasPrice)
- var txValue = new BN(ethUtil.stripHexPrefix(txParams.value || '0x0'), 16)
- var maxCost = txValue.add(txFee)
+ var maxCost = new BN(txData.maxCost)
var balanceBn = new BN(ethUtil.stripHexPrefix(balance), 16)
return maxCost.gt(balanceBn)
diff --git a/ui/app/css/lib.css b/ui/app/css/lib.css
index abbf8667e..a8df1d115 100644
--- a/ui/app/css/lib.css
+++ b/ui/app/css/lib.css
@@ -23,6 +23,14 @@
flex-direction: column;
}
+.space-between {
+ justify-content: space-between;
+}
+
+.space-around {
+ justify-content: space-around;
+}
+
.flex-column-bottom {
display: flex;
flex-direction: column-reverse;
diff --git a/ui/app/info.js b/ui/app/info.js
index cc753b2ea..e79580be4 100644
--- a/ui/app/info.js
+++ b/ui/app/info.js
@@ -110,7 +110,7 @@ InfoScreen.prototype.render = function () {
onClick (event) { this.navigateTo(event.target.href) },
}, [
h('img.icon-size', {
- src: manifest.icons[128],
+ src: manifest.icons['128'],
style: {
filter: 'grayscale(100%)', /* IE6-9 */
WebkitFilter: 'grayscale(100%)', /* Microsoft Edge and Firefox 35+ */
diff --git a/ui/app/reducers/app.js b/ui/app/reducers/app.js
index 65a3dba49..6a2c93f78 100644
--- a/ui/app/reducers/app.js
+++ b/ui/app/reducers/app.js
@@ -99,6 +99,14 @@ function reduceApp (state, action) {
transForward: action.value,
})
+ case actions.SHOW_IMPORT_PAGE:
+ return extend(appState, {
+ currentView: {
+ name: 'import-menu',
+ },
+ transForward: true,
+ })
+
case actions.SHOW_INFO_PAGE:
return extend(appState, {
currentView: {
@@ -128,6 +136,15 @@ function reduceApp (state, action) {
isLoading: false,
})
+ case actions.NEW_ACCOUNT_SCREEN:
+ return extend(appState, {
+ currentView: {
+ name: 'new-account',
+ context: appState.currentView.context,
+ },
+ transForward: true,
+ })
+
case actions.SHOW_SEND_PAGE:
return extend(appState, {
currentView: {
@@ -369,6 +386,7 @@ function reduceApp (state, action) {
case actions.SHOW_LOADING:
return extend(appState, {
isLoading: true,
+ loadingMessage: action.value,
})
case actions.HIDE_LOADING:
@@ -446,7 +464,7 @@ function reduceApp (state, action) {
},
buyView: {
subview: 'buyForm',
- amount: '5.00',
+ amount: '15.00',
buyAddress: action.value,
formView: {
coinbase: true,
diff --git a/ui/app/unlock.js b/ui/app/unlock.js
index 19f5eaec2..1aee3c5d0 100644
--- a/ui/app/unlock.js
+++ b/ui/app/unlock.js
@@ -26,7 +26,7 @@ UnlockScreen.prototype.render = function () {
const state = this.props
const warning = state.warning
return (
- h('.flex-column.hey-im-here', [
+ h('.flex-column', [
h('.unlock-screen.flex-column.flex-center.flex-grow', [
h(Mascot, {
diff --git a/ui/css.js b/ui/css.js
index 01f317acd..043363cd7 100644
--- a/ui/css.js
+++ b/ui/css.js
@@ -10,6 +10,7 @@ var cssFiles = {
'index.css': fs.readFileSync(path.join(__dirname, '/app/css/index.css'), 'utf8'),
'transitions.css': fs.readFileSync(path.join(__dirname, '/app/css/transitions.css'), 'utf8'),
'react-tooltip-component.css': fs.readFileSync(path.join(__dirname, '..', 'node_modules', 'react-tooltip-component', 'dist', 'react-tooltip-component.css'), 'utf8'),
+ 'react-css': fs.readFileSync(path.join(__dirname, '..', 'node_modules', 'react-select', 'dist', 'react-select.css'), 'utf8'),
}
function bundleCss () {
diff --git a/ui/lib/explorer-link.js b/ui/lib/explorer-link.js
index 2993d1cf1..dc6be2984 100644
--- a/ui/lib/explorer-link.js
+++ b/ui/lib/explorer-link.js
@@ -5,7 +5,7 @@ module.exports = function (hash, network) {
case 1: // main net
prefix = ''
break
- case 2: // morden test net
+ case 3: // morden test net
prefix = 'testnet.'
break
default: