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 c6e8259d0..51bea159c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,14 @@ ## Current Master +## 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 diff --git a/app/manifest.json b/app/manifest.json index 9c6558d15..2f1ae9b25 100644 --- a/app/manifest.json +++ b/app/manifest.json @@ -1,7 +1,7 @@ { "name": "MetaMask", "short_name": "Metamask", - "version": "3.0.0", + "version": "3.1.0", "manifest_version": 2, "author": "https://metamask.io", "description": "Ethereum Browser Extension", diff --git a/app/scripts/keyring-controller.js b/app/scripts/keyring-controller.js index 79cfe6fbd..e609403cc 100644 --- a/app/scripts/keyring-controller.js +++ b/app/scripts/keyring-controller.js @@ -234,7 +234,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) @@ -397,6 +400,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 6b16137ae..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) { @@ -54,8 +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)) diff --git a/app/scripts/lib/config-manager.js b/app/scripts/lib/config-manager.js index 3a1f12ac0..e927c78ec 100644 --- a/app/scripts/lib/config-manager.js +++ b/app/scripts/lib/config-manager.js @@ -306,7 +306,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/metamask-controller.js b/app/scripts/metamask-controller.js index b94b98eac..629216e42 100644 --- a/app/scripts/metamask-controller.js +++ b/app/scripts/metamask-controller.js @@ -115,7 +115,12 @@ 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) }) + }, addNewAccount: nodeify(keyringController.addNewAccount).bind(keyringController), setSelectedAccount: nodeify(keyringController.setSelectedAccount).bind(keyringController), saveAccountLabel: nodeify(keyringController.saveAccountLabel).bind(keyringController), diff --git a/app/scripts/transaction-manager.js b/app/scripts/transaction-manager.js index cc9082394..6d0121afd 100644 --- a/app/scripts/transaction-manager.js +++ b/app/scripts/transaction-manager.js @@ -190,7 +190,7 @@ module.exports = class TransactionManager extends EventEmitter { let fromAddress = txParams.from let ethTx = this.txProviderUtils.buildEthTxFromParams(txParams, txMeta.gasMultiplier) this.signEthTx(ethTx, fromAddress).then(() => { - this.updateTxAsSigned(txMeta.id, ethTx) + this.setTxStatusSigned(txMeta.id) cb(null, ethUtil.bufferToHex(ethTx.serialize())) }).catch((err) => { cb(err) @@ -198,21 +198,20 @@ module.exports = class TransactionManager extends EventEmitter { } publishTransaction (txId, rawTx, cb) { - this.txProviderUtils.publishTransaction(rawTx, (err) => { + this.txProviderUtils.publishTransaction(rawTx, (err, txHash) => { if (err) return cb(err) + this.setTxHash(txId, txHash) this.setTxStatusSubmitted(txId) cb() }) } - // receives a signed tx object and updates the tx hash - updateTxAsSigned (txId, ethTx) { + // receives a txHash records the tx as signed + setTxHash (txId, txHash) { // Add the tx hash to the persisted meta-tx object - let txHash = ethUtil.bufferToHex(ethTx.hash()) let txMeta = this.getTx(txId) txMeta.hash = txHash this.updateTx(txMeta) - this.setTxStatusSigned(txMeta.id) } /* 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 52708fdab..2c0c30523 100644 --- a/package.json +++ b/package.json @@ -83,6 +83,7 @@ "react-hyperscript": "^2.2.2", "react-markdown": "^2.3.0", "react-redux": "^4.4.5", + "react-select": "^1.0.0-rc.2", "react-tooltip-component": "^0.3.0", "readable-stream": "^2.1.2", "redux": "^3.0.5", 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/keyrings/simple-test.js b/test/unit/keyrings/simple-test.js index 687318f99..77eeb834c 100644 --- a/test/unit/keyrings/simple-test.js +++ b/test/unit/keyrings/simple-test.js @@ -49,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() { diff --git a/ui/app/accounts/import/index.js b/ui/app/accounts/import/index.js new file mode 100644 index 000000000..18a6b985c --- /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 SeedImportView = require('./seed.js') +const PrivateKeyImportView = require('./private-key.js') + +const menuItems = [ + 'Private Key', +] + +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 'HD Key Tree': + return h(SeedImportView) + case 'Private Key': + return h(PrivateKeyImportView) + 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..22cf95cfd --- /dev/null +++ b/ui/app/accounts/import/json.js @@ -0,0 +1,27 @@ +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)(JsonImportSubview) + +function mapStateToProps (state) { + return {} +} + +inherits(JsonImportSubview, Component) +function JsonImportSubview () { + Component.call(this) +} + +JsonImportSubview.prototype.render = function () { + return ( + h('div', { + style: { + }, + }, [ + `Upload your json file here!`, + ]) + ) +} + diff --git a/ui/app/accounts/import/private-key.js b/ui/app/accounts/import/private-key.js new file mode 100644 index 000000000..6b988a76b --- /dev/null +++ b/ui/app/accounts/import/private-key.js @@ -0,0 +1,69 @@ +const inherits = require('util').inherits +const Component = require('react').Component +const h = require('react-hyperscript') +const connect = require('react-redux').connect +const type = 'Simple Key Pair' +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.addNewKeyring(type, [ 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..7934a329a 100644 --- a/ui/app/actions.js +++ b/ui/app/actions.js @@ -32,16 +32,20 @@ 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, addNewAccount, + NEW_ACCOUNT_SCREEN: 'NEW_ACCOUNT_SCREEN', + navigateToNewAccountScreen, showNewVaultSeed: showNewVaultSeed, showInfoPage: showInfoPage, // seed recovery actions @@ -249,7 +253,21 @@ 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 navigateToNewAccountScreen() { + return { + type: this.NEW_ACCOUNT_SCREEN, + } } function addNewAccount (ringNumber = 0) { @@ -376,6 +394,12 @@ function showInitializeMenu () { } } +function showImportPage () { + return { + type: actions.SHOW_IMPORT_PAGE, + } +} + function agreeToDisclaimer () { return (dispatch) => { dispatch(this.showLoadingIndication()) diff --git a/ui/app/app.js b/ui/app/app.js index 9efe95874..0e04c334c 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') @@ -304,6 +305,13 @@ App.prototype.renderDropdown = function () { icon: h('i.fa.fa-gear.fa-lg'), }), + 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 }), @@ -411,6 +419,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/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/conf-tx.js b/ui/app/conf-tx.js index 8e255a867..a6e03c3ed 100644 --- a/ui/app/conf-tx.js +++ b/ui/app/conf-tx.js @@ -41,12 +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 index = state.index !== undefined && unconfTxList[index] ? state.index : 0 var txData = unconfTxList[index] || {} - var txParams = txData.txParams + var txParams = txData.params || {} var isNotification = isPopupOrNotification() === 'notification' - if (!txParams) return null + if (unconfTxList.length === 0) return null return ( @@ -116,15 +117,19 @@ 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 address = txData.txParams.from || state.selectedAccount var account = state.accounts[address] 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 dc7344b3e..ae91272cc 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: { 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 () {