diff --git a/CHANGELOG.md b/CHANGELOG.md index 0f070d9a9..b77ceb864 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,14 @@ ## Current Master +- Add better error messages for when a transaction fails on approval - Allow sending to ENS names in send form on Ropsten. +- Added an address book functionality that remembers the last 15 unique addresses sent to. +- Can now change network to custom RPC URL from lock screen. +- Removed support for old, lightwallet based vault. Users who have not opened app in over a month will need to recover with their seed phrase. This will allow Firefox support sooner. +- Fixed bug where spinner wouldn't disappear on incorrect password submission on seed word reveal. +- Polish the private key UI. +- Add Kovan as an option on our network list. ## 3.4.0 2017-3-8 diff --git a/app/scripts/config.js b/app/scripts/config.js index b4541a04a..ec421744d 100644 --- a/app/scripts/config.js +++ b/app/scripts/config.js @@ -1,5 +1,6 @@ const MAINET_RPC_URL = 'https://mainnet.infura.io/metamask' const TESTNET_RPC_URL = 'https://ropsten.infura.io/metamask' +const KOVAN_RPC_URL = 'https://kovan.infura.io/metamask' const DEFAULT_RPC_URL = TESTNET_RPC_URL global.METAMASK_DEBUG = 'GULP_METAMASK_DEBUG' @@ -10,5 +11,6 @@ module.exports = { mainnet: MAINET_RPC_URL, testnet: TESTNET_RPC_URL, morden: TESTNET_RPC_URL, + kovan: KOVAN_RPC_URL, }, } diff --git a/app/scripts/contentscript.js b/app/scripts/contentscript.js index ab64dc9fa..020208ceb 100644 --- a/app/scripts/contentscript.js +++ b/app/scripts/contentscript.js @@ -69,14 +69,10 @@ function shouldInjectWeb3 () { } function isAllowedSuffix (testCase) { - var prohibitedTypes = ['xml', 'pdf'] - var currentUrl = window.location.href - var currentRegex - for (let i = 0; i < prohibitedTypes.length; i++) { - currentRegex = new RegExp(`\.${prohibitedTypes[i]}$`) - if (currentRegex.test(currentUrl)) { - return false - } + const doctype = window.document.doctype + if (doctype) { + return doctype.name === 'html' + } else { + return false } - return true } diff --git a/app/scripts/controllers/address-book.js b/app/scripts/controllers/address-book.js new file mode 100644 index 000000000..c66eb2bd4 --- /dev/null +++ b/app/scripts/controllers/address-book.js @@ -0,0 +1,79 @@ +const ObservableStore = require('obs-store') +const extend = require('xtend') + +class AddressBookController { + + + // Controller in charge of managing the address book functionality from the + // recipients field on the send screen. Manages a history of all saved + // addresses and all currently owned addresses. + constructor (opts = {}, keyringController) { + const initState = extend({ + addressBook: [], + }, opts.initState) + this.store = new ObservableStore(initState) + this.keyringController = keyringController + } + + // + // PUBLIC METHODS + // + + // Sets a new address book in store by accepting a new address and nickname. + setAddressBook (address, name) { + return this._addToAddressBook(address, name) + .then((addressBook) => { + this.store.updateState({ + addressBook, + }) + return Promise.resolve() + }) + } + + // + // PRIVATE METHODS + // + + + // Performs the logic to add the address and name into the address book. The + // pushed object is an object of two fields. Current behavior does not set an + // upper limit to the number of addresses. + _addToAddressBook (address, name) { + let addressBook = this._getAddressBook() + let identities = this._getIdentities() + + let addressBookIndex = addressBook.findIndex((element) => { return element.address.toLowerCase() === address.toLowerCase() || element.name === name }) + let identitiesIndex = Object.keys(identities).findIndex((element) => { return element.toLowerCase() === address.toLowerCase() }) + // trigger this condition if we own this address--no need to overwrite. + if (identitiesIndex !== -1) { + return Promise.resolve(addressBook) + // trigger this condition if we've seen this address before--may need to update nickname. + } else if (addressBookIndex !== -1) { + addressBook.splice(addressBookIndex, 1) + } else if (addressBook.length > 15) { + addressBook.shift() + } + + + addressBook.push({ + address: address, + name, + }) + return Promise.resolve(addressBook) + } + + // Internal method to get the address book. Current persistence behavior + // should not require that this method be called from the UI directly. + _getAddressBook () { + return this.store.getState().addressBook + } + + // Retrieves identities from the keyring controller in order to avoid + // duplication + _getIdentities () { + return this.keyringController.memStore.getState().identities + } + +} + +module.exports = AddressBookController diff --git a/app/scripts/controllers/preferences.js b/app/scripts/controllers/preferences.js index 18fccf11b..c7f675a41 100644 --- a/app/scripts/controllers/preferences.js +++ b/app/scripts/controllers/preferences.js @@ -5,7 +5,9 @@ const extend = require('xtend') class PreferencesController { constructor (opts = {}) { - const initState = extend({ frequentRpcList: [] }, opts.initState) + const initState = extend({ + frequentRpcList: [], + }, opts.initState) this.store = new ObservableStore(initState) } diff --git a/app/scripts/lib/config-manager.js b/app/scripts/lib/config-manager.js index 6868637e5..e31cb45ed 100644 --- a/app/scripts/lib/config-manager.js +++ b/app/scripts/lib/config-manager.js @@ -5,6 +5,7 @@ const normalize = require('eth-sig-util').normalize const TESTNET_RPC = MetamaskConfig.network.testnet const MAINNET_RPC = MetamaskConfig.network.mainnet const MORDEN_RPC = MetamaskConfig.network.morden +const KOVAN_RPC = MetamaskConfig.network.kovan /* The config-manager is a convenience object * wrapping a pojo-migrator. @@ -150,6 +151,9 @@ ConfigManager.prototype.getCurrentRpcAddress = function () { case 'morden': return MORDEN_RPC + case 'kovan': + return KOVAN_RPC + default: return provider && provider.rpcTarget ? provider.rpcTarget : TESTNET_RPC } diff --git a/app/scripts/lib/eth-store.js b/app/scripts/lib/eth-store.js index 8812a507b..243253df2 100644 --- a/app/scripts/lib/eth-store.js +++ b/app/scripts/lib/eth-store.js @@ -19,6 +19,8 @@ class EthereumStore extends ObservableStore { super({ accounts: {}, transactions: {}, + currentBlockNumber: '0', + currentBlockHash: '', }) this._provider = opts.provider this._query = new EthQuery(this._provider) @@ -69,6 +71,8 @@ class EthereumStore extends ObservableStore { _updateForBlock (block) { const blockNumber = '0x' + block.number.toString('hex') this._currentBlockNumber = blockNumber + this.updateState({ currentBlockNumber: parseInt(blockNumber) }) + this.updateState({ currentBlockHash: `0x${block.hash.toString('hex')}`}) async.parallel([ this._updateAccounts.bind(this), this._updateTransactions.bind(this, blockNumber), @@ -129,4 +133,4 @@ class EthereumStore extends ObservableStore { } -module.exports = EthereumStore \ No newline at end of file +module.exports = EthereumStore diff --git a/app/scripts/lib/id-management.js b/app/scripts/lib/id-management.js deleted file mode 100644 index 90b3fdb13..000000000 --- a/app/scripts/lib/id-management.js +++ /dev/null @@ -1,90 +0,0 @@ -/* ID Management - * - * This module exists to hold the decrypted credentials for the current session. - * It therefore exposes sign methods, because it is able to perform these - * with noa dditional authentication, because its very instantiation - * means the vault is unlocked. - */ - -const ethUtil = require('ethereumjs-util') -const Transaction = require('ethereumjs-tx') - -module.exports = IdManagement - -function IdManagement (opts) { - if (!opts) opts = {} - - this.keyStore = opts.keyStore - this.derivedKey = opts.derivedKey - this.configManager = opts.configManager - this.hdPathString = "m/44'/60'/0'/0" - - this.getAddresses = function () { - return this.keyStore.getAddresses(this.hdPathString).map(function (address) { return '0x' + address }) - } - - this.signTx = function (txParams) { - - // normalize values - txParams.gasPrice = ethUtil.intToHex(txParams.gasPrice) - txParams.to = ethUtil.addHexPrefix(txParams.to) - txParams.from = ethUtil.addHexPrefix(txParams.from.toLowerCase()) - txParams.value = ethUtil.addHexPrefix(txParams.value) - txParams.data = ethUtil.addHexPrefix(txParams.data) - txParams.gasLimit = ethUtil.addHexPrefix(txParams.gasLimit || txParams.gas) - txParams.nonce = ethUtil.addHexPrefix(txParams.nonce) - var tx = new Transaction(txParams) - - // sign tx - var privKeyHex = this.exportPrivateKey(txParams.from) - var privKey = ethUtil.toBuffer(privKeyHex) - tx.sign(privKey) - - // Add the tx hash to the persisted meta-tx object - var txHash = ethUtil.bufferToHex(tx.hash()) - var metaTx = this.configManager.getTx(txParams.metamaskId) - metaTx.hash = txHash - this.configManager.updateTx(metaTx) - - // return raw serialized tx - var rawTx = ethUtil.bufferToHex(tx.serialize()) - return rawTx - } - - this.signMsg = function (address, message) { - // sign message - var privKeyHex = this.exportPrivateKey(address.toLowerCase()) - var privKey = ethUtil.toBuffer(privKeyHex) - var msgSig = ethUtil.ecsign(new Buffer(message.replace('0x', ''), 'hex'), privKey) - var rawMsgSig = ethUtil.bufferToHex(concatSig(msgSig.v, msgSig.r, msgSig.s)) - return rawMsgSig - } - - this.getSeed = function () { - return this.keyStore.getSeed(this.derivedKey) - } - - this.exportPrivateKey = function (address) { - var privKeyHex = ethUtil.addHexPrefix(this.keyStore.exportPrivateKey(address, this.derivedKey, this.hdPathString)) - return privKeyHex - } -} - -function padWithZeroes (number, length) { - var myString = '' + number - while (myString.length < length) { - myString = '0' + myString - } - return myString -} - -function concatSig (v, r, s) { - const rSig = ethUtil.fromSigned(r) - const sSig = ethUtil.fromSigned(s) - const vSig = ethUtil.bufferToInt(v) - const rStr = padWithZeroes(ethUtil.toUnsigned(rSig).toString('hex'), 64) - const sStr = padWithZeroes(ethUtil.toUnsigned(sSig).toString('hex'), 64) - const vStr = ethUtil.stripHexPrefix(ethUtil.intToHex(vSig)) - return ethUtil.addHexPrefix(rStr.concat(sStr, vStr)).toString('hex') -} - diff --git a/app/scripts/lib/idStore-migrator.js b/app/scripts/lib/idStore-migrator.js deleted file mode 100644 index 62d21eee7..000000000 --- a/app/scripts/lib/idStore-migrator.js +++ /dev/null @@ -1,80 +0,0 @@ -const IdentityStore = require('./idStore') -const HdKeyring = require('eth-hd-keyring') -const sigUtil = require('eth-sig-util') -const normalize = sigUtil.normalize -const denodeify = require('denodeify') - -module.exports = class IdentityStoreMigrator { - - constructor ({ configManager }) { - this.configManager = configManager - const hasOldVault = this.hasOldVault() - if (!hasOldVault) { - this.idStore = new IdentityStore({ configManager }) - } - } - - migratedVaultForPassword (password) { - const hasOldVault = this.hasOldVault() - const configManager = this.configManager - - if (!this.idStore) { - this.idStore = new IdentityStore({ configManager }) - } - - if (!hasOldVault) { - return Promise.resolve(null) - } - - const idStore = this.idStore - const submitPassword = denodeify(idStore.submitPassword.bind(idStore)) - - return submitPassword(password) - .then(() => { - const serialized = this.serializeVault() - return this.checkForLostAccounts(serialized) - }) - } - - serializeVault () { - const mnemonic = this.idStore._idmgmt.getSeed() - const numberOfAccounts = this.idStore._getAddresses().length - - return { - type: 'HD Key Tree', - data: { mnemonic, numberOfAccounts }, - } - } - - checkForLostAccounts (serialized) { - const hd = new HdKeyring() - return hd.deserialize(serialized.data) - .then((hexAccounts) => { - const newAccounts = hexAccounts.map(normalize) - const oldAccounts = this.idStore._getAddresses().map(normalize) - const lostAccounts = oldAccounts.reduce((result, account) => { - if (newAccounts.includes(account)) { - return result - } else { - result.push(account) - return result - } - }, []) - - return { - serialized, - lostAccounts: lostAccounts.map((address) => { - return { - address, - privateKey: this.idStore.exportAccount(address), - } - }), - } - }) - } - - hasOldVault () { - const wallet = this.configManager.getWallet() - return wallet - } -} diff --git a/app/scripts/lib/idStore.js b/app/scripts/lib/idStore.js deleted file mode 100644 index 01474035e..000000000 --- a/app/scripts/lib/idStore.js +++ /dev/null @@ -1,343 +0,0 @@ -const EventEmitter = require('events').EventEmitter -const inherits = require('util').inherits -const ethUtil = require('ethereumjs-util') -const KeyStore = require('eth-lightwallet').keystore -const clone = require('clone') -const extend = require('xtend') -const autoFaucet = require('./auto-faucet') -const DEFAULT_RPC = 'https://testrpc.metamask.io/' -const IdManagement = require('./id-management') - - -module.exports = IdentityStore - -inherits(IdentityStore, EventEmitter) -function IdentityStore (opts = {}) { - EventEmitter.call(this) - - // we just use the ethStore to auto-add accounts - this._ethStore = opts.ethStore - this.configManager = opts.configManager - // lightwallet key store - this._keyStore = null - // lightwallet wrapper - this._idmgmt = null - - this.hdPathString = "m/44'/60'/0'/0" - - this._currentState = { - selectedAddress: null, - identities: {}, - } - // not part of serilized metamask state - only kept in memory -} - -// -// public -// - -IdentityStore.prototype.createNewVault = function (password, cb) { - delete this._keyStore - var serializedKeystore = this.configManager.getWallet() - - if (serializedKeystore) { - this.configManager.setData({}) - } - - this.purgeCache() - this._createVault(password, null, (err) => { - if (err) return cb(err) - - this._autoFaucet() - - this.configManager.setShowSeedWords(true) - var seedWords = this._idmgmt.getSeed() - - this._loadIdentities() - - cb(null, seedWords) - }) -} - -IdentityStore.prototype.recoverSeed = function (cb) { - this.configManager.setShowSeedWords(true) - if (!this._idmgmt) return cb(new Error('Unauthenticated. Please sign in.')) - var seedWords = this._idmgmt.getSeed() - cb(null, seedWords) -} - -IdentityStore.prototype.recoverFromSeed = function (password, seed, cb) { - this.purgeCache() - - this._createVault(password, seed, (err) => { - if (err) return cb(err) - - this._loadIdentities() - cb(null, this.getState()) - }) -} - -IdentityStore.prototype.setStore = function (store) { - this._ethStore = store -} - -IdentityStore.prototype.clearSeedWordCache = function (cb) { - const configManager = this.configManager - configManager.setShowSeedWords(false) - cb(null, configManager.getSelectedAccount()) -} - -IdentityStore.prototype.getState = function () { - const configManager = this.configManager - var seedWords = this.getSeedIfUnlocked() - return clone(extend(this._currentState, { - isInitialized: !!configManager.getWallet() && !seedWords, - isUnlocked: this._isUnlocked(), - seedWords: seedWords, - selectedAddress: configManager.getSelectedAccount(), - })) -} - -IdentityStore.prototype.getSeedIfUnlocked = function () { - const configManager = this.configManager - var showSeed = configManager.getShouldShowSeedWords() - var idmgmt = this._idmgmt - var shouldShow = showSeed && !!idmgmt - var seedWords = shouldShow ? idmgmt.getSeed() : null - return seedWords -} - -IdentityStore.prototype.getSelectedAddress = function () { - const configManager = this.configManager - return configManager.getSelectedAccount() -} - -IdentityStore.prototype.setSelectedAddressSync = function (address) { - const configManager = this.configManager - if (!address) { - var addresses = this._getAddresses() - address = addresses[0] - } - - configManager.setSelectedAccount(address) - return address -} - -IdentityStore.prototype.setSelectedAddress = function (address, cb) { - const resultAddress = this.setSelectedAddressSync(address) - if (cb) return cb(null, resultAddress) -} - -IdentityStore.prototype.revealAccount = function (cb) { - const derivedKey = this._idmgmt.derivedKey - const keyStore = this._keyStore - const configManager = this.configManager - - keyStore.setDefaultHdDerivationPath(this.hdPathString) - keyStore.generateNewAddress(derivedKey, 1) - const addresses = keyStore.getAddresses() - const address = addresses[ addresses.length - 1 ] - - this._ethStore.addAccount(ethUtil.addHexPrefix(address)) - - configManager.setWallet(keyStore.serialize()) - - this._loadIdentities() - this._didUpdate() - cb(null) -} - -IdentityStore.prototype.getNetwork = function (err) { - if (err) { - this._currentState.network = 'loading' - this._didUpdate() - } - - this.web3.version.getNetwork((err, network) => { - if (err) { - this._currentState.network = 'loading' - return this._didUpdate() - } - if (global.METAMASK_DEBUG) { - console.log('web3.getNetwork returned ' + network) - } - this._currentState.network = network - this._didUpdate() - }) -} - -IdentityStore.prototype.setLocked = function (cb) { - delete this._keyStore - delete this._idmgmt - cb() -} - -IdentityStore.prototype.submitPassword = function (password, cb) { - const configManager = this.configManager - this.tryPassword(password, (err) => { - if (err) return cb(err) - // load identities before returning... - this._loadIdentities() - cb(null, configManager.getSelectedAccount()) - }) -} - -IdentityStore.prototype.exportAccount = function (address, cb) { - var privateKey = this._idmgmt.exportPrivateKey(address) - if (cb) cb(null, privateKey) - return privateKey -} - -// private -// - -IdentityStore.prototype._didUpdate = function () { - this.emit('update', this.getState()) -} - -IdentityStore.prototype._isUnlocked = function () { - var result = Boolean(this._keyStore) && Boolean(this._idmgmt) - return result -} - -// load identities from keyStoreet -IdentityStore.prototype._loadIdentities = function () { - const configManager = this.configManager - if (!this._isUnlocked()) throw new Error('not unlocked') - - var addresses = this._getAddresses() - addresses.forEach((address, i) => { - // // add to ethStore - if (this._ethStore) { - this._ethStore.addAccount(ethUtil.addHexPrefix(address)) - } - // add to identities - const defaultLabel = 'Account ' + (i + 1) - const nickname = configManager.nicknameForWallet(address) - var identity = { - name: nickname || defaultLabel, - address: address, - mayBeFauceting: this._mayBeFauceting(i), - } - this._currentState.identities[address] = identity - }) - this._didUpdate() -} - -IdentityStore.prototype.saveAccountLabel = function (account, label, cb) { - const configManager = this.configManager - configManager.setNicknameForWallet(account, label) - this._loadIdentities() - cb(null, label) -} - -// mayBeFauceting -// If on testnet, index 0 may be fauceting. -// The UI will have to check the balance to know. -// If there is no balance and it mayBeFauceting, -// then it is in fact fauceting. -IdentityStore.prototype._mayBeFauceting = function (i) { - const configManager = this.configManager - var config = configManager.getProvider() - if (i === 0 && - config.type === 'rpc' && - config.rpcTarget === DEFAULT_RPC) { - return true - } - return false -} - -// -// keyStore managment - unlocking + deserialization -// - -IdentityStore.prototype.tryPassword = function (password, cb) { - var serializedKeystore = this.configManager.getWallet() - var keyStore = KeyStore.deserialize(serializedKeystore) - - keyStore.keyFromPassword(password, (err, pwDerivedKey) => { - if (err) return cb(err) - - const isCorrect = keyStore.isDerivedKeyCorrect(pwDerivedKey) - if (!isCorrect) return cb(new Error('Lightwallet - password incorrect')) - - this._keyStore = keyStore - this._createIdMgmt(pwDerivedKey) - cb() - }) -} - -IdentityStore.prototype._createVault = function (password, seedPhrase, cb) { - const opts = { - password, - hdPathString: this.hdPathString, - } - - if (seedPhrase) { - opts.seedPhrase = seedPhrase - } - - KeyStore.createVault(opts, (err, keyStore) => { - if (err) return cb(err) - - this._keyStore = keyStore - - keyStore.keyFromPassword(password, (err, derivedKey) => { - if (err) return cb(err) - - this.purgeCache() - - keyStore.addHdDerivationPath(this.hdPathString, derivedKey, {curve: 'secp256k1', purpose: 'sign'}) - - this._createFirstWallet(derivedKey) - this._createIdMgmt(derivedKey) - this.setSelectedAddressSync() - - cb() - }) - }) -} - -IdentityStore.prototype._createIdMgmt = function (derivedKey) { - this._idmgmt = new IdManagement({ - keyStore: this._keyStore, - derivedKey: derivedKey, - configManager: this.configManager, - }) -} - -IdentityStore.prototype.purgeCache = function () { - this._currentState.identities = {} - let accounts - try { - accounts = Object.keys(this._ethStore._currentState.accounts) - } catch (e) { - accounts = [] - } - accounts.forEach((address) => { - this._ethStore.removeAccount(address) - }) -} - -IdentityStore.prototype._createFirstWallet = function (derivedKey) { - const keyStore = this._keyStore - keyStore.setDefaultHdDerivationPath(this.hdPathString) - keyStore.generateNewAddress(derivedKey, 1) - this.configManager.setWallet(keyStore.serialize()) - var addresses = keyStore.getAddresses() - this._ethStore.addAccount(ethUtil.addHexPrefix(addresses[0])) -} - -// get addresses and normalize address hexString -IdentityStore.prototype._getAddresses = function () { - return this._keyStore.getAddresses(this.hdPathString).map((address) => { - return ethUtil.addHexPrefix(address) - }) -} - -IdentityStore.prototype._autoFaucet = function () { - var addresses = this._getAddresses() - autoFaucet(addresses[0]) -} - -// util diff --git a/app/scripts/metamask-controller.js b/app/scripts/metamask-controller.js index a26c0e45d..15bf9f436 100644 --- a/app/scripts/metamask-controller.js +++ b/app/scripts/metamask-controller.js @@ -15,6 +15,7 @@ const PreferencesController = require('./controllers/preferences') const CurrencyController = require('./controllers/currency') const NoticeController = require('./notice-controller') const ShapeShiftController = require('./controllers/shapeshift') +const AddressBookController = require('./controllers/address-book') const MessageManager = require('./lib/message-manager') const PersonalMessageManager = require('./lib/personal-message-manager') const TxManager = require('./transaction-manager') @@ -22,7 +23,6 @@ const ConfigManager = require('./lib/config-manager') const extension = require('./lib/extension') const autoFaucet = require('./lib/auto-faucet') const nodeify = require('./lib/nodeify') -const IdStoreMigrator = require('./lib/idStore-migrator') const accountImporter = require('./account-import-strategies') const version = require('../manifest.json').version @@ -80,6 +80,11 @@ module.exports = class MetamaskController extends EventEmitter { autoFaucet(address) }) + // address book controller + this.addressBookController = new AddressBookController({ + initState: initState.AddressBookController, + }, this.keyringController) + // tx mgmt this.txManager = new TxManager({ initState: initState.TransactionManager, @@ -109,11 +114,6 @@ module.exports = class MetamaskController extends EventEmitter { this.personalMessageManager = new PersonalMessageManager() this.publicConfigStore = this.initPublicConfigStore() - // TEMPORARY UNTIL FULL DEPRECATION: - this.idStoreMigrator = new IdStoreMigrator({ - configManager: this.configManager, - }) - // manual disk state subscriptions this.txManager.store.subscribe((state) => { this.store.updateState({ TransactionManager: state }) @@ -124,6 +124,9 @@ module.exports = class MetamaskController extends EventEmitter { this.preferencesController.store.subscribe((state) => { this.store.updateState({ PreferencesController: state }) }) + this.addressBookController.store.subscribe((state) => { + this.store.updateState({ AddressBookController: state }) + }) this.currencyController.store.subscribe((state) => { this.store.updateState({ CurrencyController: state }) }) @@ -142,6 +145,7 @@ module.exports = class MetamaskController extends EventEmitter { this.personalMessageManager.memStore.subscribe(this.sendUpdate.bind(this)) this.keyringController.memStore.subscribe(this.sendUpdate.bind(this)) this.preferencesController.store.subscribe(this.sendUpdate.bind(this)) + this.addressBookController.store.subscribe(this.sendUpdate.bind(this)) this.currencyController.store.subscribe(this.sendUpdate.bind(this)) this.noticeController.memStore.subscribe(this.sendUpdate.bind(this)) this.shapeshiftController.store.subscribe(this.sendUpdate.bind(this)) @@ -218,6 +222,7 @@ module.exports = class MetamaskController extends EventEmitter { this.personalMessageManager.memStore.getState(), this.keyringController.memStore.getState(), this.preferencesController.store.getState(), + this.addressBookController.store.getState(), this.currencyController.store.getState(), this.noticeController.memStore.getState(), // config manager @@ -239,6 +244,7 @@ module.exports = class MetamaskController extends EventEmitter { const preferencesController = this.preferencesController const txManager = this.txManager const noticeController = this.noticeController + const addressBookController = this.addressBookController return { // etc @@ -266,6 +272,9 @@ module.exports = class MetamaskController extends EventEmitter { setDefaultRpc: nodeify(this.setDefaultRpc).bind(this), setCustomRpc: nodeify(this.setCustomRpc).bind(this), + // AddressController + setAddressBook: nodeify(addressBookController.setAddressBook).bind(addressBookController), + // KeyringController setLocked: nodeify(keyringController.setLocked).bind(keyringController), createNewVaultAndKeychain: nodeify(keyringController.createNewVaultAndKeychain).bind(keyringController), @@ -350,8 +359,7 @@ module.exports = class MetamaskController extends EventEmitter { // submitPassword (password, cb) { - this.migrateOldVaultIfAny(password) - .then(this.keyringController.submitPassword.bind(this.keyringController, password)) + return this.keyringController.submitPassword(password) .then((newState) => { cb(null, newState) }) .catch((reason) => { cb(reason) }) } @@ -546,35 +554,6 @@ module.exports = class MetamaskController extends EventEmitter { cb(null, this.getState()) } - // Migrate Old Vault If Any - // @string password - // - // returns Promise() - // - // Temporary step used when logging in. - // Checks if old style (pre-3.0.0) Metamask Vault exists. - // If so, persists that vault in the new vault format - // with the provided password, so the other unlock steps - // may be completed without interruption. - migrateOldVaultIfAny (password) { - - if (!this.checkIfShouldMigrate()) { - return Promise.resolve(password) - } - - const keyringController = this.keyringController - - return this.idStoreMigrator.migratedVaultForPassword(password) - .then(this.restoreOldVaultAccounts.bind(this)) - .then(this.restoreOldLostAccounts.bind(this)) - .then(keyringController.persistAllKeyrings.bind(keyringController, password)) - .then(() => password) - } - - checkIfShouldMigrate() { - return !!this.configManager.getWallet() && !this.configManager.getVault() - } - restoreOldVaultAccounts(migratorOutput) { const { serialized } = migratorOutput return this.keyringController.restoreKeyring(serialized) @@ -620,7 +599,7 @@ module.exports = class MetamaskController extends EventEmitter { this.currencyController.updateConversionRate() const data = { conversionRate: this.currencyController.getConversionRate(), - currentFiat: this.currencyController.getCurrentCurrency(), + currentCurrency: this.currencyController.getCurrentCurrency(), conversionDate: this.currencyController.getConversionDate(), } cb(null, data) diff --git a/app/scripts/migrations/010.js b/app/scripts/migrations/010.js index 48a841bc1..c0cc56ae4 100644 --- a/app/scripts/migrations/010.js +++ b/app/scripts/migrations/010.js @@ -2,7 +2,7 @@ const version = 10 /* -This migration breaks out the CurrencyController substate +This migration breaks out the ShapeShiftController substate */ diff --git a/app/scripts/migrations/011.js b/app/scripts/migrations/011.js index bf283ef98..0d5d6d307 100644 --- a/app/scripts/migrations/011.js +++ b/app/scripts/migrations/011.js @@ -2,7 +2,7 @@ const version = 11 /* -This migration breaks out the CurrencyController substate +This migration removes the discaimer state from our app, which was integrated into our notices. */ diff --git a/app/scripts/transaction-manager.js b/app/scripts/transaction-manager.js index c6cfdf11d..31c1c8431 100644 --- a/app/scripts/transaction-manager.js +++ b/app/scripts/transaction-manager.js @@ -172,7 +172,10 @@ module.exports = class TransactionManager extends EventEmitter { ], (err) => { self.nonceLock.leave() if (err) { - this.setTxStatusFailed(txId) + this.setTxStatusFailed(txId, { + errCode: err.errCode || err, + message: err.message || 'Transaction failed during approval', + }) return cb(err) } cb() @@ -291,7 +294,10 @@ module.exports = class TransactionManager extends EventEmitter { this._setTxStatus(txId, 'confirmed') } - setTxStatusFailed (txId) { + setTxStatusFailed (txId, reason) { + let txMeta = this.getTx(txId) + txMeta.err = reason + this.updateTx(txMeta) this._setTxStatus(txId, 'failed') } @@ -312,12 +318,11 @@ module.exports = class TransactionManager extends EventEmitter { var txHash = txMeta.hash var txId = txMeta.id if (!txHash) { - txMeta.err = { + let errReason = { errCode: 'No hash was provided', message: 'We had an error while submitting this transaction, please try again.', } - this.updateTx(txMeta) - return this.setTxStatusFailed(txId) + return this.setTxStatusFailed(txId, errReason) } this.txProviderUtils.query.getTransactionByHash(txHash, (err, txParams) => { if (err || !txParams) { diff --git a/development/states/account-detail-with-shapeshift-tx.json b/development/states/account-detail-with-shapeshift-tx.json index c14062350..97d5e9f06 100644 --- a/development/states/account-detail-with-shapeshift-tx.json +++ b/development/states/account-detail-with-shapeshift-tx.json @@ -1,6 +1,6 @@ { "metamask": { - "currentFiat": "USD", + "currentCurrency": "USD", "conversionRate": 11.06608791, "conversionDate": 1470421024, "isInitialized": true, diff --git a/development/states/account-detail-with-transaction-history.json b/development/states/account-detail-with-transaction-history.json index 8cb495656..a6bcc2658 100644 --- a/development/states/account-detail-with-transaction-history.json +++ b/development/states/account-detail-with-transaction-history.json @@ -1,6 +1,6 @@ { "metamask": { - "currentFiat": "USD", + "currentCurrency": "USD", "conversionRate": 11.06608791, "conversionDate": 1470421024, "isInitialized": true, diff --git a/development/states/account-detail.json b/development/states/account-detail.json index 644e3674d..6d11c1deb 100644 --- a/development/states/account-detail.json +++ b/development/states/account-detail.json @@ -1,6 +1,6 @@ { "metamask": { - "currentFiat": "USD", + "currentCurrency": "USD", "conversionRate": 11.06608791, "conversionDate": 1470421024, "isInitialized": true, diff --git a/development/states/account-list-with-imported.json b/development/states/account-list-with-imported.json index b450b4fb8..41d586db6 100644 --- a/development/states/account-list-with-imported.json +++ b/development/states/account-list-with-imported.json @@ -14,7 +14,7 @@ } }, "unconfTxs": {}, - "currentFiat": "USD", + "currentCurrency": "USD", "conversionRate": 10.19458075, "conversionDate": 1484696373, "noActiveNotices": true, diff --git a/development/states/accounts-loose.json b/development/states/accounts-loose.json index 542e207d4..df51f0d7e 100644 --- a/development/states/accounts-loose.json +++ b/development/states/accounts-loose.json @@ -30,7 +30,7 @@ } }, "unconfTxs": {}, - "currentFiat": "USD", + "currentCurrency": "USD", "conversionRate": 0, "conversionDate": "N/A", "noActiveNotices": true, diff --git a/development/states/accounts.json b/development/states/accounts.json index 4eef1145f..c8ff40ed9 100644 --- a/development/states/accounts.json +++ b/development/states/accounts.json @@ -41,7 +41,7 @@ } }, "unconfTxs": {}, - "currentFiat": "USD", + "currentCurrency": "USD", "conversionRate": 11.84461814, "conversionDate": 1476226414, "accounts": { diff --git a/development/states/compilation-bug.json b/development/states/compilation-bug.json index b33e59c5c..588d069d4 100644 --- a/development/states/compilation-bug.json +++ b/development/states/compilation-bug.json @@ -41,7 +41,7 @@ "simulationFails": true } }, - "currentFiat": "USD", + "currentCurrency": "USD", "conversionRate": 7.69158136, "conversionDate": 1482279663, "noActiveNotices": true, diff --git a/development/states/conf-tx.json b/development/states/conf-tx.json index b44d23ad8..0f1a13751 100644 --- a/development/states/conf-tx.json +++ b/development/states/conf-tx.json @@ -48,7 +48,7 @@ "gasPrice": "4a817c800" } }, - "currentFiat": "USD", + "currentCurrency": "USD", "conversionRate": 12.7200827, "conversionDate": 1487363041, "noActiveNotices": true, diff --git a/development/states/config.json b/development/states/config.json index de1df1831..551c0e3fa 100644 --- a/development/states/config.json +++ b/development/states/config.json @@ -1,6 +1,6 @@ { "metamask": { - "currentFiat": "USD", + "currentCurrency": "USD", "conversionRate": 11.06608791, "conversionDate": 1470421024, "isInitialized": true, diff --git a/development/states/first-time.json b/development/states/first-time.json index 3554ee911..683a61fdf 100644 --- a/development/states/first-time.json +++ b/development/states/first-time.json @@ -6,7 +6,7 @@ "identities": {}, "frequentRpcList": [], "unapprovedTxs": {}, - "currentFiat": "USD", + "currentCurrency": "USD", "conversionRate": 12.7527416, "conversionDate": 1487624341, "noActiveNotices": false, diff --git a/development/states/import-private-key-warning.json b/development/states/import-private-key-warning.json index a2d33ed7f..80ebc650d 100644 --- a/development/states/import-private-key-warning.json +++ b/development/states/import-private-key-warning.json @@ -10,7 +10,7 @@ } }, "unconfTxs": {}, - "currentFiat": "USD", + "currentCurrency": "USD", "conversionRate": 10.1219126, "conversionDate": 1484695442, "noActiveNotices": true, diff --git a/development/states/import-private-key.json b/development/states/import-private-key.json index 73fbd68db..bd455c6d5 100644 --- a/development/states/import-private-key.json +++ b/development/states/import-private-key.json @@ -10,7 +10,7 @@ } }, "unconfTxs": {}, - "currentFiat": "USD", + "currentCurrency": "USD", "conversionRate": 10.10788584, "conversionDate": 1484694362, "noActiveNotices": true, diff --git a/development/states/locked.json b/development/states/locked.json index 8e6ed7860..866394e86 100644 --- a/development/states/locked.json +++ b/development/states/locked.json @@ -6,7 +6,7 @@ "rpcTarget": "https://rawtestrpc.metamask.io/", "identities": {}, "unconfTxs": {}, - "currentFiat": "USD", + "currentCurrency": "USD", "conversionRate": 11.4379398, "conversionDate": 1473358355, "accounts": {}, diff --git a/development/states/lost-accounts.json b/development/states/lost-accounts.json index dcba7a764..4f50092b0 100644 --- a/development/states/lost-accounts.json +++ b/development/states/lost-accounts.json @@ -1,6 +1,6 @@ { "metamask": { - "currentFiat": "USD", + "currentCurrency": "USD", "lostAccounts": [ "0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc", "0xec1adf982415d2ef5ec55899b9bfb8bc0f29251b" diff --git a/development/states/new-account.json b/development/states/new-account.json index a056e14e7..dc2341b0e 100644 --- a/development/states/new-account.json +++ b/development/states/new-account.json @@ -14,7 +14,7 @@ } }, "unconfTxs": {}, - "currentFiat": "USD", + "currentCurrency": "USD", "conversionRate": 10.92067835, "conversionDate": 1478282884, "network": null, diff --git a/development/states/notice.json b/development/states/notice.json index 1cdcefcfb..efeffab08 100644 --- a/development/states/notice.json +++ b/development/states/notice.json @@ -9,7 +9,7 @@ } }, "unconfTxs": {}, - "currentFiat": "USD", + "currentCurrency": "USD", "conversionRate": 8.3533002, "conversionDate": 1481671082, "noActiveNotices": false, diff --git a/development/states/pending-signature.json b/development/states/pending-signature.json index 0d49e0229..7deaee8f7 100644 --- a/development/states/pending-signature.json +++ b/development/states/pending-signature.json @@ -22,7 +22,7 @@ } }, "unconfTxs": {}, - "currentFiat": "USD", + "currentCurrency": "USD", "conversionRate": 11.02269525, "conversionDate": 1472076963, "accounts": { diff --git a/development/states/pending-tx-insufficient.json b/development/states/pending-tx-insufficient.json index 10ecc826d..18be84089 100644 --- a/development/states/pending-tx-insufficient.json +++ b/development/states/pending-tx-insufficient.json @@ -31,7 +31,7 @@ "maxCost": "de234b52e4a0800" } }, - "currentFiat": "USD", + "currentCurrency": "USD", "conversionRate": 12.59854817, "conversionDate": 1487662141, "noActiveNotices": true, diff --git a/development/states/personal-sign.json b/development/states/personal-sign.json index 2fc71f448..8ded6205c 100644 --- a/development/states/personal-sign.json +++ b/development/states/personal-sign.json @@ -18,7 +18,7 @@ } }, "unapprovedTxs": {}, - "currentFiat": "USD", + "currentCurrency": "USD", "conversionRate": 13.2126613, "conversionDate": 1487888522, "noActiveNotices": true, diff --git a/development/states/private-key-export-success.json b/development/states/private-key-export-success.json new file mode 100644 index 000000000..2ff3c4d17 --- /dev/null +++ b/development/states/private-key-export-success.json @@ -0,0 +1,70 @@ +{ + "metamask": { + "isInitialized": true, + "isUnlocked": true, + "rpcTarget": "https://rawtestrpc.metamask.io/", + "identities": { + "0x07284e146926a4facd0ea60598dc4f001ad620f1": { + "address": "0x07284e146926a4facd0ea60598dc4f001ad620f1", + "name": "Account 1" + } + }, + "unapprovedTxs": {}, + "noActiveNotices": true, + "frequentRpcList": [], + "addressBook": [], + "network": "3", + "accounts": { + "0x07284e146926a4facd0ea60598dc4f001ad620f1": { + "code": "0x", + "nonce": "0x0", + "balance": "0x0", + "address": "0x07284e146926a4facd0ea60598dc4f001ad620f1" + } + }, + "transactions": {}, + "selectedAddressTxList": [], + "unapprovedMsgs": {}, + "unapprovedMsgCount": 0, + "unapprovedPersonalMsgs": {}, + "unapprovedPersonalMsgCount": 0, + "keyringTypes": [ + "Simple Key Pair", + "HD Key Tree" + ], + "keyrings": [ + { + "type": "HD Key Tree", + "accounts": [ + "07284e146926a4facd0ea60598dc4f001ad620f1" + ] + } + ], + "selectedAddress": "0x07284e146926a4facd0ea60598dc4f001ad620f1", + "currentCurrency": "USD", + "conversionRate": 43.35903476, + "conversionDate": 1490105102, + "provider": { + "type": "testnet" + }, + "shapeShiftTxList": [], + "lostAccounts": [], + "seedWords": null + }, + "appState": { + "menuOpen": false, + "currentView": { + "name": "accountDetail", + "context": "0x07284e146926a4facd0ea60598dc4f001ad620f1" + }, + "accountDetail": { + "subview": "export", + "accountExport": "completed", + "privateKey": "549c9638ad06432568969accacad4a02f8548cc358085938071745138ec134b7" + }, + "transForward": true, + "isLoading": false, + "warning": null + }, + "identities": {} +} diff --git a/development/states/private-key-export.json b/development/states/private-key-export.json new file mode 100644 index 000000000..db7a53e22 --- /dev/null +++ b/development/states/private-key-export.json @@ -0,0 +1,69 @@ +{ + "metamask": { + "isInitialized": true, + "isUnlocked": true, + "rpcTarget": "https://rawtestrpc.metamask.io/", + "identities": { + "0x07284e146926a4facd0ea60598dc4f001ad620f1": { + "address": "0x07284e146926a4facd0ea60598dc4f001ad620f1", + "name": "Account 1" + } + }, + "unapprovedTxs": {}, + "noActiveNotices": true, + "frequentRpcList": [], + "addressBook": [], + "network": "3", + "accounts": { + "0x07284e146926a4facd0ea60598dc4f001ad620f1": { + "code": "0x", + "nonce": "0x0", + "balance": "0x0", + "address": "0x07284e146926a4facd0ea60598dc4f001ad620f1" + } + }, + "transactions": {}, + "selectedAddressTxList": [], + "unapprovedMsgs": {}, + "unapprovedMsgCount": 0, + "unapprovedPersonalMsgs": {}, + "unapprovedPersonalMsgCount": 0, + "keyringTypes": [ + "Simple Key Pair", + "HD Key Tree" + ], + "keyrings": [ + { + "type": "HD Key Tree", + "accounts": [ + "07284e146926a4facd0ea60598dc4f001ad620f1" + ] + } + ], + "selectedAddress": "0x07284e146926a4facd0ea60598dc4f001ad620f1", + "currentCurrency": "USD", + "conversionRate": 43.35903476, + "conversionDate": 1490105102, + "provider": { + "type": "testnet" + }, + "shapeShiftTxList": [], + "lostAccounts": [], + "seedWords": null + }, + "appState": { + "menuOpen": false, + "currentView": { + "name": "accountDetail", + "context": "0x07284e146926a4facd0ea60598dc4f001ad620f1" + }, + "accountDetail": { + "subview": "export", + "accountExport": "requested" + }, + "transForward": true, + "isLoading": false, + "warning": null + }, + "identities": {} +} diff --git a/development/states/private-network.json b/development/states/private-network.json index 155aa6584..8cdcb4eeb 100644 --- a/development/states/private-network.json +++ b/development/states/private-network.json @@ -16,7 +16,7 @@ } }, "unconfTxs": {}, - "currentFiat": "USD", + "currentCurrency": "USD", "conversionRate": 9.52855776, "conversionDate": 1479756513, "accounts": { diff --git a/development/states/restore-vault.json b/development/states/restore-vault.json index ad136c78e..0d6c2610d 100644 --- a/development/states/restore-vault.json +++ b/development/states/restore-vault.json @@ -6,7 +6,7 @@ "rpcTarget": "https://rawtestrpc.metamask.io/", "identities": {}, "unconfTxs": {}, - "currentFiat": "USD", + "currentCurrency": "USD", "conversionRate": 0, "conversionDate": "N/A", "accounts": {}, diff --git a/development/states/send.json b/development/states/send.json index 3f52185af..73ac62f65 100644 --- a/development/states/send.json +++ b/development/states/send.json @@ -22,7 +22,7 @@ } }, "unapprovedTxs": {}, - "currentFiat": "USD", + "currentCurrency": "USD", "conversionRate": 16.88200327, "conversionDate": 1489013762, "noActiveNotices": true, diff --git a/development/states/shapeshift.json b/development/states/shapeshift.json index bc10143d0..bfd4b7c16 100644 --- a/development/states/shapeshift.json +++ b/development/states/shapeshift.json @@ -22,7 +22,7 @@ } }, "unconfTxs": {}, - "currentFiat": "USD", + "currentCurrency": "USD", "conversionRate": 11.21274318, "conversionDate": 1472159644, "accounts": { diff --git a/development/states/terms-and-conditions.json b/development/states/terms-and-conditions.json index b995e446f..f5ebe8254 100644 --- a/development/states/terms-and-conditions.json +++ b/development/states/terms-and-conditions.json @@ -5,7 +5,7 @@ "rpcTarget": "https://rawtestrpc.metamask.io/", "identities": {}, "unconfTxs": {}, - "currentFiat": "USD", + "currentCurrency": "USD", "conversionRate": 8.18703468, "conversionDate": 1481755832, "network": "3", diff --git a/package.json b/package.json index 1d19d5e75..c08d92339 100644 --- a/package.json +++ b/package.json @@ -53,7 +53,6 @@ "ensnare": "^1.0.0", "eth-bin-to-ops": "^1.0.1", "eth-hd-keyring": "^1.1.1", - "eth-lightwallet": "^2.3.3", "eth-query": "^1.0.3", "eth-sig-util": "^1.1.1", "eth-simple-keyring": "^1.1.1", @@ -99,7 +98,7 @@ "react-tooltip-component": "^0.3.0", "readable-stream": "^2.1.2", "redux": "^3.0.5", - "redux-logger": "^2.3.1", + "redux-logger": "^2.10.2", "redux-thunk": "^1.0.2", "request-promise": "^4.1.1", "sandwich-expando": "^1.0.5", diff --git a/test/integration/lib/idStore-migrator-test.js b/test/integration/lib/idStore-migrator-test.js deleted file mode 100644 index 290216ae8..000000000 --- a/test/integration/lib/idStore-migrator-test.js +++ /dev/null @@ -1,92 +0,0 @@ -const ObservableStore = require('obs-store') -const ConfigManager = require('../../../app/scripts/lib/config-manager') -const IdStoreMigrator = require('../../../app/scripts/lib/idStore-migrator') -const SimpleKeyring = require('eth-simple-keyring') -const normalize = require('eth-sig-util').normalize - -const oldStyleVault = require('../mocks/oldVault.json').data -const badStyleVault = require('../mocks/badVault.json').data - -const PASSWORD = '12345678' -const FIRST_ADDRESS = '0x4dd5d356c5A016A220bCD69e82e5AF680a430d00'.toLowerCase() -const BAD_STYLE_FIRST_ADDRESS = '0xac39b311dceb2a4b2f5d8461c1cdaf756f4f7ae9' -const SEED = 'fringe damage bounce extend tunnel afraid alert sound all soldier all dinner' - -QUnit.module('Old Style Vaults', { - beforeEach: function () { - let managers = managersFromInitState(oldStyleVault) - - this.configManager = managers.configManager - this.migrator = managers.migrator - } -}) - -QUnit.test('migrator:isInitialized', function (assert) { - assert.ok(this.migrator) -}) - -QUnit.test('migrator:migratedVaultForPassword', function (assert) { - var done = assert.async() - - this.migrator.migratedVaultForPassword(PASSWORD) - .then((result) => { - assert.ok(result, 'migratedVaultForPassword returned result') - const { serialized, lostAccounts } = result - assert.equal(serialized.data.mnemonic, SEED, 'seed phrase recovered') - assert.equal(lostAccounts.length, 0, 'no lost accounts') - done() - }) -}) - -QUnit.module('Old Style Vaults with bad HD seed', { - beforeEach: function () { - let managers = managersFromInitState(badStyleVault) - - this.configManager = managers.configManager - this.migrator = managers.migrator - } -}) - -QUnit.test('migrator:migratedVaultForPassword', function (assert) { - var done = assert.async() - - this.migrator.migratedVaultForPassword(PASSWORD) - .then((result) => { - assert.ok(result, 'migratedVaultForPassword returned result') - const { serialized, lostAccounts } = result - - assert.equal(lostAccounts.length, 1, 'one lost account') - assert.equal(lostAccounts[0].address, '0xe15D894BeCB0354c501AE69429B05143679F39e0'.toLowerCase()) - assert.ok(lostAccounts[0].privateKey, 'private key exported') - - var lostAccount = lostAccounts[0] - var privateKey = lostAccount.privateKey - - var simple = new SimpleKeyring() - simple.deserialize([privateKey]) - .then(() => { - return simple.getAccounts() - }) - .then((accounts) => { - assert.equal(normalize(accounts[0]), lostAccount.address, 'recovered address.') - done() - }) - .catch((reason) => { - assert.ifError(reason) - done(reason) - }) - }) -}) - -function managersFromInitState(initState){ - - let configManager = new ConfigManager({ - store: new ObservableStore(initState), - }) - - let migrator = new IdStoreMigrator({ - configManager: configManager, - }) - - return { configManager, migrator } -} diff --git a/test/unit/address-book-controller.js b/test/unit/address-book-controller.js new file mode 100644 index 000000000..f345b0328 --- /dev/null +++ b/test/unit/address-book-controller.js @@ -0,0 +1,56 @@ +const assert = require('assert') +const extend = require('xtend') +const AddressBookController = require('../../app/scripts/controllers/address-book') + +const mockKeyringController = { + memStore: { + getState: function () { + return { + identities: { + '0x0aaa' : { + address: '0x0aaa', + name: 'owned', + } + } + } + } + } +} + + +describe('address-book-controller', function() { + var addressBookController + + beforeEach(function() { + addressBookController = new AddressBookController({}, mockKeyringController) + }) + + describe('addres book management', function () { + describe('#_getAddressBook', function () { + it('should be empty by default.', function () { + assert.equal(addressBookController._getAddressBook().length, 0) + }) + }) + describe('#setAddressBook', function () { + it('should properly set a new address.', function () { + addressBookController.setAddressBook('0x01234', 'test') + var addressBook = addressBookController._getAddressBook() + assert.equal(addressBook.length, 1, 'incorrect address book length.') + assert.equal(addressBook[0].address, '0x01234', 'incorrect addresss') + assert.equal(addressBook[0].name, 'test', 'incorrect nickname') + }) + + it('should reject duplicates.', function () { + addressBookController.setAddressBook('0x01234', 'test') + addressBookController.setAddressBook('0x01234', 'test') + var addressBook = addressBookController._getAddressBook() + assert.equal(addressBook.length, 1, 'incorrect address book length.') + }) + it('should not add any identities that are under user control', function () { + addressBookController.setAddressBook('0x0aaa', ' ') + var addressBook = addressBookController._getAddressBook() + assert.equal(addressBook.length, 0, 'incorrect address book length.') + }) + }) + }) +}) diff --git a/test/unit/currency-controller-test.js b/test/unit/currency-controller-test.js index dd7fa91e0..079f8b488 100644 --- a/test/unit/currency-controller-test.js +++ b/test/unit/currency-controller-test.js @@ -7,7 +7,7 @@ const rp = require('request-promise') const nock = require('nock') const CurrencyController = require('../../app/scripts/controllers/currency') -describe('config-manager', function() { +describe('currency-controller', function() { var currencyController beforeEach(function() { diff --git a/test/unit/id-management-test.js b/test/unit/id-management-test.js deleted file mode 100644 index cbc6403bc..000000000 --- a/test/unit/id-management-test.js +++ /dev/null @@ -1,35 +0,0 @@ -var assert = require('assert') -var IdManagement = require('../../app/scripts/lib/id-management') -var sinon = require('sinon') - -describe('IdManagement', function() { - - beforeEach(function() { - // sinon allows stubbing methods that are easily verified - this.sinon = sinon.sandbox.create() - window.localStorage = {} // Hacking localStorage support into JSDom - }) - - afterEach(function() { - // sinon requires cleanup otherwise it will overwrite context - this.sinon.restore() - }) - - describe('#signMsg', function () { - it('passes the dennis test', function() { - const address = '0x9858e7d8b79fc3e6d989636721584498926da38a' - const message = '0x879a053d4800c6354e76c7985a865d2922c82fb5b3f4577b2fe08b998954f2e0' - const privateKey = '0x7dd98753d7b4394095de7d176c58128e2ed6ee600abe97c9f6d9fd65015d9b18' - const expectedResult = '0x28fcb6768e5110144a55b2e6ce9d1ea5a58103033632d272d2b5cf506906f7941a00b539383fd872109633d8c71c404e13dba87bc84166ee31b0e36061a69e161c' - - const idManagement = new IdManagement() - const exportKeyStub = sinon.stub(idManagement, 'exportPrivateKey', (addr) => { - assert.equal(addr, address) - return privateKey - }) - - const result = idManagement.signMsg(address, message) - assert.equal(result, expectedResult) - }) - }) -}) diff --git a/test/unit/idStore-migration-test.js b/test/unit/idStore-migration-test.js deleted file mode 100644 index 81a99ef63..000000000 --- a/test/unit/idStore-migration-test.js +++ /dev/null @@ -1,83 +0,0 @@ -const async = require('async') -const assert = require('assert') -const ObservableStore = require('obs-store') -const ethUtil = require('ethereumjs-util') -const BN = ethUtil.BN -const ConfigManager = require('../../app/scripts/lib/config-manager') -const firstTimeState = require('../../app/scripts/first-time-state') -const delegateCallCode = require('../lib/example-code.json').delegateCallCode -const clone = require('clone') - -// The old way: -const IdentityStore = require('../../app/scripts/lib/idStore') -const STORAGE_KEY = 'metamask-config' - -// The new ways: -var KeyringController = require('../../app/scripts/keyring-controller') -const mockEncryptor = require('../lib/mock-encryptor') -const MockSimpleKeychain = require('../lib/mock-simple-keychain') -const sinon = require('sinon') - -const mockVault = { - seed: 'picnic injury awful upper eagle junk alert toss flower renew silly vague', - account: '0x5d8de92c205279c10e5669f797b853ccef4f739a', -} - -const badVault = { - seed: 'radar blur cabbage chef fix engine embark joy scheme fiction master release', -} - -describe('IdentityStore to KeyringController migration', function() { - - // The stars of the show: - let idStore, keyringController, seedWords, configManager - - let password = 'password123' - let entropy = 'entripppppyy duuude' - let accounts = [] - let newAccounts = [] - let originalKeystore - - // This is a lot of setup, I know! - // We have to create an old style vault, populate it, - // and THEN create a new one, before we can run tests on it. - beforeEach(function(done) { - this.sinon = sinon.sandbox.create() - let store = new ObservableStore(clone(firstTimeState)) - configManager = new ConfigManager({ store }) - - idStore = new IdentityStore({ - configManager: configManager, - ethStore: { - addAccount(acct) { accounts.push(ethUtil.addHexPrefix(acct)) }, - del(acct) { delete accounts[acct] }, - }, - }) - - idStore._createVault(password, mockVault.seed, (err) => { - assert.ifError(err, 'createNewVault threw error') - originalKeystore = idStore._idmgmt.keyStore - - idStore.setLocked((err) => { - assert.ifError(err, 'createNewVault threw error') - keyringController = new KeyringController({ - configManager, - ethStore: { - addAccount(acct) { newAccounts.push(ethUtil.addHexPrefix(acct)) }, - del(acct) { delete newAccounts[acct] }, - }, - txManager: { - getTxList: () => [], - getUnapprovedTxList: () => [] - }, - }) - - // Stub out the browser crypto for a mock encryptor. - // Browser crypto is tested in the integration test suite. - keyringController.encryptor = mockEncryptor - done() - }) - }) - }) - -}) diff --git a/test/unit/idStore-test.js b/test/unit/idStore-test.js deleted file mode 100644 index 000c58a82..000000000 --- a/test/unit/idStore-test.js +++ /dev/null @@ -1,142 +0,0 @@ -const async = require('async') -const assert = require('assert') -const ethUtil = require('ethereumjs-util') -const BN = ethUtil.BN -const configManagerGen = require('../lib/mock-config-manager') -const delegateCallCode = require('../lib/example-code.json').delegateCallCode -const IdentityStore = require('../../app/scripts/lib/idStore') - -describe('IdentityStore', function() { - - describe('#createNewVault', function () { - let idStore - let password = 'password123' - let seedWords - let accounts = [] - let originalKeystore - - before(function(done) { - window.localStorage = {} // Hacking localStorage support into JSDom - - idStore = new IdentityStore({ - configManager: configManagerGen(), - ethStore: { - addAccount(acct) { accounts.push(ethUtil.addHexPrefix(acct)) }, - }, - }) - - idStore.createNewVault(password, (err, seeds) => { - assert.ifError(err, 'createNewVault threw error') - seedWords = seeds - originalKeystore = idStore._idmgmt.keyStore - done() - }) - }) - - describe('#recoverFromSeed', function() { - let newAccounts = [] - - before(function() { - window.localStorage = {} // Hacking localStorage support into JSDom - - idStore = new IdentityStore({ - configManager: configManagerGen(), - ethStore: { - addAccount(acct) { newAccounts.push(ethUtil.addHexPrefix(acct)) }, - }, - }) - }) - - it('should return the expected keystore', function (done) { - - idStore.recoverFromSeed(password, seedWords, (err) => { - assert.ifError(err) - - let newKeystore = idStore._idmgmt.keyStore - assert.equal(newAccounts[0], accounts[0]) - done() - }) - }) - }) - }) - - describe('#recoverFromSeed BIP44 compliance', function() { - const salt = 'lightwalletSalt' - - let password = 'secret!' - let accounts = {} - let idStore - - var assertions = [ - { - seed: 'picnic injury awful upper eagle junk alert toss flower renew silly vague', - account: '0x5d8de92c205279c10e5669f797b853ccef4f739a', - }, - { - seed: 'radar blur cabbage chef fix engine embark joy scheme fiction master release', - account: '0xe15d894becb0354c501ae69429b05143679f39e0', - }, - { - seed: 'phone coyote caught pattern found table wedding list tumble broccoli chief swing', - account: '0xb0e868f24bc7fec2bce2efc2b1c344d7569cd9d2', - }, - { - seed: 'recycle tag bird palace blue village anxiety census cook soldier example music', - account: '0xab34a45920afe4af212b96ec51232aaa6a33f663', - }, - { - seed: 'half glimpse tape cute harvest sweet bike voyage actual floor poet lazy', - account: '0x28e9044597b625ac4beda7250011670223de43b2', - }, - { - seed: 'flavor tiger carpet motor angry hungry document inquiry large critic usage liar', - account: '0xb571be96558940c4e9292e1999461aa7499fb6cd', - }, - ] - - before(function() { - window.localStorage = {} // Hacking localStorage support into JSDom - - idStore = new IdentityStore({ - configManager: configManagerGen(), - ethStore: { - addAccount(acct) { accounts[acct] = acct}, - del(acct) { delete accounts[acct] }, - }, - }) - }) - - it('should enforce seed compliance with TestRPC', function (done) { - this.timeout(10000) - const tests = assertions.map((assertion) => { - return function (cb) { - - idStore.recoverFromSeed(password, assertion.seed, (err) => { - assert.ifError(err) - - var expected = assertion.account.toLowerCase() - var received = accounts[expected].toLowerCase() - assert.equal(received, expected) - - idStore.tryPassword(password, function (err) { - - assert.ok(idStore._isUnlocked(), 'should unlock the id store') - - idStore.submitPassword(password, function(err, account) { - assert.ifError(err) - assert.equal(account, expected) - assert.equal(Object.keys(idStore._getAddresses()).length, 1, 'only one account on restore') - cb() - }) - }) - }) - } - }) - - async.series(tests, function(err, results) { - assert.ifError(err) - done() - }) - }) - }) -}) diff --git a/ui/app/accounts/import/json.js b/ui/app/accounts/import/json.js index 1c2b331d4..5ed31ab0a 100644 --- a/ui/app/accounts/import/json.js +++ b/ui/app/accounts/import/json.js @@ -60,7 +60,7 @@ JsonImportSubview.prototype.render = function () { }, }, 'Import'), - error ? h('span.warning', error) : null, + error ? h('span.error', error) : null, ]) ) } @@ -95,4 +95,3 @@ JsonImportSubview.prototype.createNewKeychain = function () { 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 index b139a0374..68ccee58e 100644 --- a/ui/app/accounts/import/private-key.js +++ b/ui/app/accounts/import/private-key.js @@ -48,7 +48,7 @@ PrivateKeyImportView.prototype.render = function () { }, }, 'Import'), - error ? h('span.warning', error) : null, + error ? h('span.error', error) : null, ]) ) } @@ -65,4 +65,3 @@ PrivateKeyImportView.prototype.createNewKeychain = function () { const privateKey = input.value this.props.dispatch(actions.importNewAccount('Private Key', [ privateKey ])) } - diff --git a/ui/app/actions.js b/ui/app/actions.js index d4fd7553b..d02b7dcaa 100644 --- a/ui/app/actions.js +++ b/ui/app/actions.js @@ -71,10 +71,12 @@ var actions = { SHOW_CONF_TX_PAGE: 'SHOW_CONF_TX_PAGE', SHOW_CONF_MSG_PAGE: 'SHOW_CONF_MSG_PAGE', SET_CURRENT_FIAT: 'SET_CURRENT_FIAT', - setCurrentFiat: setCurrentFiat, + setCurrentCurrency: setCurrentCurrency, // account detail screen SHOW_SEND_PAGE: 'SHOW_SEND_PAGE', showSendPage: showSendPage, + ADD_TO_ADDRESS_BOOK: 'ADD_TO_ADDRESS_BOOK', + addToAddressBook: addToAddressBook, REQUEST_ACCOUNT_EXPORT: 'REQUEST_ACCOUNT_EXPORT', requestExportAccount: requestExportAccount, EXPORT_ACCOUNT: 'EXPORT_ACCOUNT', @@ -267,11 +269,12 @@ function requestRevealSeed (password) { dispatch(actions.showLoadingIndication()) log.debug(`background.submitPassword`) background.submitPassword(password, (err) => { - if (err) return dispatch(actions.displayWarning(err.message)) + if (err) { + return dispatch(actions.displayWarning(err.message)) + } log.debug(`background.placeSeedWords`) background.placeSeedWords((err) => { if (err) return dispatch(actions.displayWarning(err.message)) - dispatch(actions.hideLoadingIndication()) }) }) } @@ -294,10 +297,10 @@ function importNewAccount (strategy, args) { dispatch(actions.showLoadingIndication('This may take a while, be patient.')) log.debug(`background.importAccountWithStrategy`) background.importAccountWithStrategy(strategy, args, (err) => { - dispatch(actions.hideLoadingIndication()) if (err) return dispatch(actions.displayWarning(err.message)) log.debug(`background.getState`) background.getState((err, newState) => { + dispatch(actions.hideLoadingIndication()) if (err) { return dispatch(actions.displayWarning(err.message)) } @@ -328,10 +331,10 @@ function showInfoPage () { } } -function setCurrentFiat (currencyCode) { +function setCurrentCurrency (currencyCode) { return (dispatch) => { dispatch(this.showLoadingIndication()) - log.debug(`background.setCurrentFiat`) + log.debug(`background.setCurrentCurrency`) background.setCurrentCurrency(currencyCode, (err, data) => { dispatch(this.hideLoadingIndication()) if (err) { @@ -341,7 +344,7 @@ function setCurrentFiat (currencyCode) { dispatch({ type: this.SET_CURRENT_FIAT, value: { - currentFiat: data.currentFiat, + currentCurrency: data.currentCurrency, conversionRate: data.conversionRate, conversionDate: data.conversionDate, }, @@ -696,6 +699,19 @@ function setRpcTarget (newRpc) { } } +// Calls the addressBookController to add a new address. +function addToAddressBook (recipient, nickname) { + log.debug(`background.addToAddressBook`) + return (dispatch) => { + background.setAddressBook(recipient, nickname, (err, result) => { + if (err) { + log.error(err) + return dispatch(self.displayWarning('Address book failed to update')) + } + }) + } +} + function setProviderType (type) { log.debug(`background.setProviderType`) background.setProviderType(type) @@ -757,22 +773,30 @@ function requestExportAccount () { } } -function exportAccount (address) { +function exportAccount (password, address) { var self = this return function (dispatch) { dispatch(self.showLoadingIndication()) - log.debug(`background.exportAccount`) - background.exportAccount(address, function (err, result) { - dispatch(self.hideLoadingIndication()) - + log.debug(`background.submitPassword`) + background.submitPassword(password, function (err) { if (err) { - log.error(err) - return dispatch(self.displayWarning('Had a problem exporting the account.')) + log.error('Error in submiting password.') + dispatch(self.hideLoadingIndication()) + return dispatch(self.displayWarning('Incorrect Password.')) } + log.debug(`background.exportAccount`) + background.exportAccount(address, function (err, result) { + dispatch(self.hideLoadingIndication()) - dispatch(self.showPrivateKey(result)) + if (err) { + log.error(err) + return dispatch(self.displayWarning('Had a problem exporting the account.')) + } + + dispatch(self.showPrivateKey(result)) + }) }) } } diff --git a/ui/app/app.js b/ui/app/app.js index 2bc92b54c..5a7596aca 100644 --- a/ui/app/app.js +++ b/ui/app/app.js @@ -255,6 +255,15 @@ App.prototype.renderNetworkDropdown = function () { provider: props.provider, }), + h(DropMenuItem, { + label: 'Kovan Test Network', + closeMenu: () => this.setState({ isNetworkMenuOpen: false}), + action: () => props.dispatch(actions.setProviderType('kovan')), + icon: h('.menu-icon.hollow-diamond'), + activeNetworkRender: props.network, + provider: props.provider, + }), + h(DropMenuItem, { label: 'Localhost 8545', closeMenu: () => this.setState({ isNetworkMenuOpen: false }), @@ -266,7 +275,7 @@ App.prototype.renderNetworkDropdown = function () { this.renderCustomOption(props.provider), this.renderCommonRpc(rpcList, props.provider), - props.isUnlocked && h(DropMenuItem, { + h(DropMenuItem, { label: 'Custom RPC', closeMenu: () => this.setState({ isNetworkMenuOpen: false }), action: () => this.props.dispatch(actions.showConfigPage()), @@ -400,6 +409,10 @@ App.prototype.renderPrimary = function () { log.debug('rendering restore vault screen') return h(HDRestoreVaultScreen, {key: 'HDRestoreVaultScreen'}) + case 'config': + log.debug('rendering config screen from unlock screen.') + return h(ConfigScreen, {key: 'config'}) + default: log.debug('rendering locked screen') return h(UnlockScreen, {key: 'locked'}) diff --git a/ui/app/components/account-export.js b/ui/app/components/account-export.js index 6d8b099a5..888196c5d 100644 --- a/ui/app/components/account-export.js +++ b/ui/app/components/account-export.js @@ -4,14 +4,21 @@ const inherits = require('util').inherits const copyToClipboard = require('copy-to-clipboard') const actions = require('../actions') const ethUtil = require('ethereumjs-util') +const connect = require('react-redux').connect -module.exports = ExportAccountView +module.exports = connect(mapStateToProps)(ExportAccountView) inherits(ExportAccountView, Component) function ExportAccountView () { Component.call(this) } +function mapStateToProps (state) { + return { + warning: state.appState.warning, + } +} + ExportAccountView.prototype.render = function () { console.log('EXPORT VIEW') console.dir(this.props) @@ -28,35 +35,58 @@ ExportAccountView.prototype.render = function () { if (notExporting) return h('div') if (exportRequested) { - var warning = `Exporting your private key is very dangerous, - and you should only do it if you know what you're doing.` - var confirmation = `If you're absolutely sure, type "I understand" below and - submit.` + var warning = `Export private keys at your own risk.` return ( - h('div', { - key: 'exporting', style: { - margin: '0 20px', + display: 'inline-block', + textAlign: 'center', }, - }, [ - h('p.error', warning), - h('p', confirmation), - h('input#exportAccount.sizing-input', { - onKeyPress: this.onExportKeyPress.bind(this), - style: { - position: 'relative', - top: '1.5px', + }, + [ + h('div', { + key: 'exporting', + style: { + margin: '0 20px', + }, + }, [ + h('p.error', warning), + h('input#exportAccount.sizing-input', { + type: 'password', + placeholder: 'confirm password', + onKeyPress: this.onExportKeyPress.bind(this), + style: { + position: 'relative', + top: '1.5px', + marginBottom: '7px', + }, + }), + ]), + h('div', { + key: 'buttons', + style: { + margin: '0 20px', + }, }, - }), - h('button', { - onClick: () => this.onExportKeyPress({ key: 'Enter', preventDefault: () => {} }), - }, 'Submit'), - h('button', { - onClick: () => this.props.dispatch(actions.backToAccountDetail(this.props.address)), - }, 'Cancel'), - ]) - + [ + h('button', { + onClick: () => this.onExportKeyPress({ key: 'Enter', preventDefault: () => {} }), + style: { + marginRight: '10px', + }, + }, 'Submit'), + h('button', { + onClick: () => this.props.dispatch(actions.backToAccountDetail(this.props.address)), + }, 'Cancel'), + ]), + (this.props.warning) && ( + h('span.error', { + style: { + margin: '20px', + }, + }, this.props.warning.split('-')) + ), + ]) ) } @@ -89,15 +119,6 @@ ExportAccountView.prototype.onExportKeyPress = function (event) { if (event.key !== 'Enter') return event.preventDefault() - var input = document.getElementById('exportAccount') - if (input.value === 'I understand') { - this.props.dispatch(actions.exportAccount(this.props.address)) - } else { - input.value = '' - input.placeholder = 'Please retype "I understand" exactly.' - } -} - -ExportAccountView.prototype.exportAccount = function (address) { - this.props.dispatch(actions.exportAccount(address)) + var input = document.getElementById('exportAccount').value + this.props.dispatch(actions.exportAccount(input, this.props.address)) } diff --git a/ui/app/components/binary-renderer.js b/ui/app/components/binary-renderer.js index a9d49b128..0b6a1f5c2 100644 --- a/ui/app/components/binary-renderer.js +++ b/ui/app/components/binary-renderer.js @@ -2,6 +2,7 @@ const Component = require('react').Component const h = require('react-hyperscript') const inherits = require('util').inherits const ethUtil = require('ethereumjs-util') +const extend = require('xtend') module.exports = BinaryRenderer @@ -12,20 +13,22 @@ function BinaryRenderer () { BinaryRenderer.prototype.render = function () { const props = this.props - const { value } = props + const { value, style } = props const text = this.hexToText(value) + const defaultStyle = extend({ + width: '315px', + maxHeight: '210px', + resize: 'none', + border: 'none', + background: 'white', + padding: '3px', + }, style) + return ( h('textarea.font-small', { readOnly: true, - style: { - width: '315px', - maxHeight: '210px', - resize: 'none', - border: 'none', - background: 'white', - padding: '3px', - }, + style: defaultStyle, defaultValue: text, }) ) diff --git a/ui/app/components/drop-menu-item.js b/ui/app/components/drop-menu-item.js index 9f002234e..3eb6ec876 100644 --- a/ui/app/components/drop-menu-item.js +++ b/ui/app/components/drop-menu-item.js @@ -42,7 +42,10 @@ DropMenuItem.prototype.activeNetworkRender = function () { if (providerType === 'mainnet') return h('.check', '✓') break case 'Ropsten Test Network': - if (provider.type === 'testnet') return h('.check', '✓') + if (providerType === 'testnet') return h('.check', '✓') + break + case 'Kovan Test Network': + if (providerType === 'kovan') return h('.check', '✓') break case 'Localhost 8545': if (activeNetwork === 'http://localhost:8545') return h('.check', '✓') diff --git a/ui/app/components/ens-input.js b/ui/app/components/ens-input.js index ffc4eab4a..facf29d97 100644 --- a/ui/app/components/ens-input.js +++ b/ui/app/components/ens-input.js @@ -21,6 +21,7 @@ function EnsInput () { EnsInput.prototype.render = function () { const props = this.props const opts = extend(props, { + list: 'addresses', onChange: () => { const network = this.props.network let resolverAddress = networkResolvers[network] @@ -46,6 +47,25 @@ EnsInput.prototype.render = function () { style: { width: '100%' }, }, [ h('input.large-input', opts), + // The address book functionality. + h('datalist#addresses', + [ + // Corresponds to the addresses owned. + Object.keys(props.identities).map((key) => { + let identity = props.identities[key] + return h('option', { + value: identity.address, + label: identity.name, + }) + }), + // Corresponds to previously sent-to addresses. + props.addressBook.map((identity) => { + return h('option', { + value: identity.address, + label: identity.name, + }) + }), + ]), this.ensIcon(), ]) } @@ -80,11 +100,13 @@ EnsInput.prototype.lookupEnsName = function () { this.setState({ loadingEns: false, ensResolution: address, + nickname: recipient.trim(), hoverText: address + '\nClick to Copy', }) } }) .catch((reason) => { + log.error(reason) return this.setState({ loadingEns: false, ensFailure: true, @@ -95,10 +117,13 @@ EnsInput.prototype.lookupEnsName = function () { EnsInput.prototype.componentDidUpdate = function (prevProps, prevState) { const state = this.state || {} - const { ensResolution } = state + const ensResolution = state.ensResolution + // If an address is sent without a nickname, meaning not from ENS or from + // the user's own accounts, a default of a one-space string is used. + const nickname = state.nickname || ' ' if (ensResolution && this.props.onChange && ensResolution !== prevState.ensResolution) { - this.props.onChange(ensResolution) + this.props.onChange(ensResolution, nickname) } } diff --git a/ui/app/components/fiat-value.js b/ui/app/components/fiat-value.js index 13ee48245..298809b30 100644 --- a/ui/app/components/fiat-value.js +++ b/ui/app/components/fiat-value.js @@ -9,7 +9,7 @@ module.exports = connect(mapStateToProps)(FiatValue) function mapStateToProps (state) { return { conversionRate: state.metamask.conversionRate, - currentFiat: state.metamask.currentFiat, + currentCurrency: state.metamask.currentCurrency, } } @@ -34,7 +34,7 @@ FiatValue.prototype.render = function () { fiatTooltipNumber = 'Unknown' } - var fiatSuffix = props.currentFiat + var fiatSuffix = props.currentCurrency return fiatDisplay(fiatDisplayNumber, fiatSuffix) } diff --git a/ui/app/components/network.js b/ui/app/components/network.js index 77805fd57..d9045167f 100644 --- a/ui/app/components/network.js +++ b/ui/app/components/network.js @@ -40,6 +40,9 @@ Network.prototype.render = function () { } else if (parseInt(networkNumber) === 3) { hoverText = 'Ropsten Test Network' iconName = 'ropsten-test-network' + } else if (providerName === 'kovan') { + hoverText = 'Kovan Test Network' + iconName = 'kovan-test-network' } else { hoverText = 'Unknown Private Network' iconName = 'unknown-private-network' @@ -70,6 +73,15 @@ Network.prototype.render = function () { }}, 'Ropsten Test Net'), ]) + case 'kovan-test-network': + return h('.network-indicator', [ + h('.menu-icon.hollow-diamond'), + h('.network-name', { + style: { + color: '#690496', + }}, + 'Kovan Test Net'), + ]) default: return h('.network-indicator', [ h('i.fa.fa-question-circle.fa-lg', { diff --git a/ui/app/components/pending-personal-msg-details.js b/ui/app/components/pending-personal-msg-details.js index fa2c6416c..1050513f2 100644 --- a/ui/app/components/pending-personal-msg-details.js +++ b/ui/app/components/pending-personal-msg-details.js @@ -40,9 +40,18 @@ PendingMsgDetails.prototype.render = function () { }), // message data - h('div', [ + h('div', { + style: { + height: '260px', + }, + }, [ h('label.font-small', { style: { display: 'block' } }, 'MESSAGE'), - h(BinaryRenderer, { value: data }), + h(BinaryRenderer, { + value: data, + style: { + height: '215px', + }, + }), ]), ]) diff --git a/ui/app/components/transaction-list-item.js b/ui/app/components/transaction-list-item.js index 44d2dc587..ee32be7d3 100644 --- a/ui/app/components/transaction-list-item.js +++ b/ui/app/components/transaction-list-item.js @@ -28,7 +28,7 @@ TransactionListItem.prototype.render = function () { let isLinkable = false const numericNet = parseInt(network) - isLinkable = numericNet === 1 || numericNet === 3 + isLinkable = numericNet === 1 || numericNet === 3 || numericNet === 42 var isMsg = ('msgParams' in transaction) var isTx = ('txParams' in transaction) diff --git a/ui/app/config.js b/ui/app/config.js index 00a4cba88..444365de2 100644 --- a/ui/app/config.js +++ b/ui/app/config.js @@ -125,19 +125,19 @@ function rpcValidation (newRpc, state) { } function currentConversionInformation (metamaskState, state) { - var currentFiat = metamaskState.currentFiat + var currentCurrency = metamaskState.currentCurrency var conversionDate = metamaskState.conversionDate return h('div', [ h('span', {style: { fontWeight: 'bold', paddingRight: '10px'}}, 'Current Conversion'), h('span', {style: { fontWeight: 'bold', paddingRight: '10px', fontSize: '13px'}}, `Updated ${Date(conversionDate)}`), - h('select#currentFiat', { + h('select#currentCurrency', { onChange (event) { event.preventDefault() - var element = document.getElementById('currentFiat') - var newFiat = element.value - state.dispatch(actions.setCurrentFiat(newFiat)) + var element = document.getElementById('currentCurrency') + var newCurrency = element.value + state.dispatch(actions.setCurrentCurrency(newCurrency)) }, - defaultValue: currentFiat, + defaultValue: currentCurrency, }, currencies.map((currency) => { return h('option', {key: currency.code, value: currency.code}, `${currency.code} - ${currency.name}`) }) @@ -161,6 +161,11 @@ function currentProviderDisplay (metamaskState) { value = 'Ropsten Test Network' break + case 'kovan': + title = 'Current Network' + value = 'Kovan Test Network' + break + default: title = 'Current RPC' value = metamaskState.provider.rpcTarget diff --git a/ui/app/css/index.css b/ui/app/css/index.css index 8c6ff29d3..3ec0ac5c5 100644 --- a/ui/app/css/index.css +++ b/ui/app/css/index.css @@ -266,8 +266,9 @@ app sections } .sizing-input{ - font-size: 1em; + font-size: 14px; height: 30px; + padding-left: 5px; } .editable-label{ display: flex; diff --git a/ui/app/css/lib.css b/ui/app/css/lib.css index 99c6f1b9d..670dc9fd0 100644 --- a/ui/app/css/lib.css +++ b/ui/app/css/lib.css @@ -188,7 +188,7 @@ hr.horizontal-line { .hollow-diamond { transform: rotate(45deg); - border: 1px solid #038789; + border: 3px solid #690496; } .pending-dot { diff --git a/ui/app/reducers/app.js b/ui/app/reducers/app.js index 7ea1e1d7c..b9e3f7b16 100644 --- a/ui/app/reducers/app.js +++ b/ui/app/reducers/app.js @@ -426,6 +426,7 @@ function reduceApp (state, action) { case actions.DISPLAY_WARNING: return extend(appState, { warning: action.value, + isLoading: false, }) case actions.HIDE_WARNING: diff --git a/ui/app/reducers/metamask.js b/ui/app/reducers/metamask.js index a3c07d977..2b5151466 100644 --- a/ui/app/reducers/metamask.js +++ b/ui/app/reducers/metamask.js @@ -13,12 +13,10 @@ function reduceMetamask (state, action) { rpcTarget: 'https://rawtestrpc.metamask.io/', identities: {}, unapprovedTxs: {}, - currentFiat: 'USD', - conversionRate: 0, - conversionDate: 'N/A', noActiveNotices: true, lastUnreadNotice: undefined, frequentRpcList: [], + addressBook: [], }, state.metamask) switch (action.type) { @@ -126,7 +124,7 @@ function reduceMetamask (state, action) { case actions.SET_CURRENT_FIAT: return extend(metamaskState, { - currentFiat: action.value.currentFiat, + currentCurrency: action.value.currentCurrency, conversionRate: action.value.conversionRate, conversionDate: action.value.conversionDate, }) diff --git a/ui/app/send.js b/ui/app/send.js index a281a5fcf..eb32d5e06 100644 --- a/ui/app/send.js +++ b/ui/app/send.js @@ -20,6 +20,7 @@ function mapStateToProps (state) { identities: state.metamask.identities, warning: state.appState.warning, network: state.metamask.network, + addressBook: state.metamask.addressBook, } result.error = result.warning && result.warning.split('.')[0] @@ -44,6 +45,8 @@ SendTransactionScreen.prototype.render = function () { var account = state.account var identity = state.identity var network = state.network + var identities = state.identities + var addressBook = state.addressBook return ( @@ -153,6 +156,8 @@ SendTransactionScreen.prototype.render = function () { placeholder: 'Recipient Address', onChange: this.recipientDidChange.bind(this), network, + identities, + addressBook, }), ]), @@ -222,13 +227,17 @@ SendTransactionScreen.prototype.back = function () { this.props.dispatch(actions.backToAccountDetail(address)) } -SendTransactionScreen.prototype.recipientDidChange = function (recipient) { - this.setState({ recipient }) +SendTransactionScreen.prototype.recipientDidChange = function (recipient, nickname) { + this.setState({ + recipient: recipient, + nickname: nickname, + }) } SendTransactionScreen.prototype.onSubmit = function () { const state = this.state || {} const recipient = state.recipient || document.querySelector('input[name="address"]').value + const nickname = state.nickname || ' ' const input = document.querySelector('input[name="amount"]').value const value = util.normalizeEthStringToWei(input) const txData = document.querySelector('input[name="txData"]').value @@ -257,6 +266,8 @@ SendTransactionScreen.prototype.onSubmit = function () { this.props.dispatch(actions.hideWarning()) + this.props.dispatch(actions.addToAddressBook(recipient, nickname)) + var txParams = { from: this.props.address, value: '0x' + value.toString(16), diff --git a/ui/lib/account-link.js b/ui/lib/account-link.js index 77db0851d..948f32da1 100644 --- a/ui/lib/account-link.js +++ b/ui/lib/account-link.js @@ -1,7 +1,6 @@ module.exports = function (address, network) { const net = parseInt(network) let link - switch (net) { case 1: // main net link = `http://etherscan.io/address/${address}` @@ -12,6 +11,9 @@ module.exports = function (address, network) { case 3: // ropsten test net link = `http://testnet.etherscan.io/address/${address}` break + case 42: // kovan test net + link = `http://kovan.etherscan.io/address/${address}` + break default: link = '' break diff --git a/ui/lib/explorer-link.js b/ui/lib/explorer-link.js index dc6be2984..7ae19cca0 100644 --- a/ui/lib/explorer-link.js +++ b/ui/lib/explorer-link.js @@ -5,9 +5,12 @@ module.exports = function (hash, network) { case 1: // main net prefix = '' break - case 3: // morden test net + case 3: // ropsten test net prefix = 'testnet.' break + case 42: // kovan test net + prefix = 'kovan.' + break default: prefix = '' }