diff --git a/app/_locales/en/messages.json b/app/_locales/en/messages.json index 5f42ac437f86..2c021a92de6f 100644 --- a/app/_locales/en/messages.json +++ b/app/_locales/en/messages.json @@ -11,6 +11,16 @@ "exposeDescription": { "message": "Expose accounts to the current website. Useful for legacy dapps." }, + "external": { + "message": "External" + }, + "externalAcc": { + "message": "External Account", + "description": "select this for entering external account without password" + }, + "externalAccountMsg": { + "message": "External accounts will not be associated with your originally created MetaMask account seedphrase. When you want to confirm a transaction with external account, Metamask will provide transaction details and expects a valid signature from external signer app." + }, "confirmExpose": { "message": "Are you sure you want to expose your accounts to the current website?" }, @@ -663,6 +673,15 @@ "invalidSeedPhrase": { "message": "Invalid seed phrase" }, + "invalidSignature": { + "message": "Invalid signature" + }, + "invalidSignatureLength": { + "message": "Length should be 130 chars." + }, + "invalidSignatureChar": { + "message": "Invalid character in signature." + }, "jsonFail": { "message": "Something went wrong. Please make sure your JSON file is properly formatted." }, @@ -916,6 +935,10 @@ "message": "Paste your private key string here:", "description": "For importing an account from a private key" }, + "pasteExternalAccount": { + "message": "Paste your external account address here:", + "description": "For importing an external account from account number" + }, "pasteSeed": { "message": "Paste your seed phrase here!" }, @@ -974,6 +997,9 @@ "recipientAddress": { "message": "Recipient Address" }, + "signature": { + "message": "Signature" + }, "refundAddress": { "message": "Your Refund Address" }, diff --git a/app/scripts/metamask-controller.js b/app/scripts/metamask-controller.js index c7e9cfcc774c..ec6b8dff3cd6 100644 --- a/app/scripts/metamask-controller.js +++ b/app/scripts/metamask-controller.js @@ -155,7 +155,10 @@ module.exports = class MetamaskController extends EventEmitter { }) // key mgmt - const additionalKeyrings = [TrezorKeyring, LedgerBridgeKeyring] + const additionalKeyrings = [ + TrezorKeyring, + LedgerBridgeKeyring, + ] this.keyringController = new KeyringController({ keyringTypes: additionalKeyrings, initState: initState.KeyringController, @@ -401,6 +404,7 @@ module.exports = class MetamaskController extends EventEmitter { resetAccount: nodeify(this.resetAccount, this), removeAccount: nodeify(this.removeAccount, this), importAccountWithStrategy: nodeify(this.importAccountWithStrategy, this), + addExtAccount: nodeify(this.addExtAccount, this), // hardware wallets connectHardware: nodeify(this.connectHardware, this), @@ -438,6 +442,7 @@ module.exports = class MetamaskController extends EventEmitter { createNewVaultAndRestore: nodeify(this.createNewVaultAndRestore, this), addNewKeyring: nodeify(keyringController.addNewKeyring, keyringController), exportAccount: nodeify(keyringController.exportAccount, keyringController), + updateExternalSign: nodeify(keyringController.updateExternalSign, keyringController), // txController cancelTransaction: nodeify(txController.cancelTransaction, txController), @@ -877,6 +882,23 @@ module.exports = class MetamaskController extends EventEmitter { await this.preferencesController.setSelectedAddress(accounts[0]) } + /** + * Creates and external account and adds it to a newly created + * ExternalAccountKeyring. + * + * @param {string} address - A valid Ethereum address string. + */ + async addExtAccount (address) { + if (!ethUtil.isValidAddress(address)) throw new Error('Address is invalid.') + const keyring = await this.keyringController.addNewKeyring('External Account', [ address ]) + const accounts = await keyring.getAccounts() + // update accounts in preferences controller + const allAccounts = await this.keyringController.getAccounts() + this.preferencesController.setAddresses(allAccounts) + // set new account as selected + await this.preferencesController.setSelectedAddress(accounts[0]) + } + // --------------------------------------------------------------------------- // Identity Management (signature operations) diff --git a/package-lock.json b/package-lock.json index 357b1acf3a78..2c7f55e3d012 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4761,7 +4761,7 @@ }, "base-x": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/base-x/-/base-x-1.1.0.tgz", + "resolved": "http://registry.npmjs.org/base-x/-/base-x-1.1.0.tgz", "integrity": "sha1-QtPXF0dPnqAiB/bRqh9CaRPut6w=" }, "base62": { @@ -5408,7 +5408,6 @@ "version": "5.2.1", "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.2.1.tgz", "integrity": "sha512-c+Ko0loDaFfuPWiL02ls9Xd3GO3cPVmUobQ6t3rXNUk304u6hGq+8N/kFi+QEIKhzK3uwolVhLzszmfLmMLnqg==", - "dev": true, "requires": { "base64-js": "^1.0.2", "ieee754": "^1.1.4" @@ -7698,7 +7697,7 @@ "dependencies": { "pify": { "version": "2.3.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "resolved": "http://registry.npmjs.org/pify/-/pify-2.3.0.tgz", "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=", "dev": true } @@ -9691,7 +9690,7 @@ }, "eth-json-rpc-middleware": { "version": "1.6.0", - "resolved": "https://registry.npmjs.org/eth-json-rpc-middleware/-/eth-json-rpc-middleware-1.6.0.tgz", + "resolved": "http://registry.npmjs.org/eth-json-rpc-middleware/-/eth-json-rpc-middleware-1.6.0.tgz", "integrity": "sha512-tDVCTlrUvdqHKqivYMjtFZsdD7TtpNLBCfKAcOpaVs7orBMS/A8HWro6dIzNtTZIR05FAbJ3bioFOnZpuCew9Q==", "requires": { "async": "^2.5.0", @@ -9775,6 +9774,33 @@ } } }, + "eth-external-account-keyring": { + "version": "github:r001/eth-external-account-keyring#807da82ce472594bbb5c33125a85d89c729ebc7a", + "from": "github:r001/eth-external-account-keyring#v1.0.0", + "requires": { + "eth-sig-util": "^2.0.2", + "ethereumjs-abi": "^0.6.5", + "ethereumjs-util": "^5.1.1", + "events": "^1.1.1", + "xtend": "^4.0.1" + }, + "dependencies": { + "ethereumjs-util": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ethereumjs-util/-/ethereumjs-util-5.2.0.tgz", + "integrity": "sha512-CJAKdI0wgMbQFLlLRtZKGcy/L6pzVRgelIZqRqNbuVFM3K9VEnyfbcvz0ncWMRNCe4kaHWjwRYQcYMucmwsnWA==", + "requires": { + "bn.js": "^4.11.0", + "create-hash": "^1.1.2", + "ethjs-util": "^0.1.3", + "keccak": "^1.0.2", + "rlp": "^2.0.0", + "safe-buffer": "^5.1.1", + "secp256k1": "^3.0.1" + } + } + } + }, "eth-hd-keyring": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/eth-hd-keyring/-/eth-hd-keyring-1.2.2.tgz", @@ -9793,12 +9819,13 @@ "resolved": "https://registry.npmjs.org/eth-sig-util/-/eth-sig-util-1.4.2.tgz", "integrity": "sha1-jZWCAsftuq6Dlwf7pvCf8ydgYhA=", "requires": { + "ethereumjs-abi": "git+https://github.com/ethereumjs/ethereumjs-abi.git#2863c40e0982acfc0b7163f0285d4c56427c7799", "ethereumjs-util": "^5.1.1" }, "dependencies": { "ethereumjs-abi": { "version": "git+https://github.com/ethereumjs/ethereumjs-abi.git#2863c40e0982acfc0b7163f0285d4c56427c7799", - "from": "git+https://github.com/ethereumjs/ethereumjs-abi.git#2863c40e0982acfc0b7163f0285d4c56427c7799", + "from": "git+https://github.com/ethereumjs/ethereumjs-abi.git", "requires": { "bn.js": "^4.10.0", "ethereumjs-util": "^5.0.0" @@ -9882,12 +9909,13 @@ "resolved": "https://registry.npmjs.org/eth-sig-util/-/eth-sig-util-1.4.2.tgz", "integrity": "sha1-jZWCAsftuq6Dlwf7pvCf8ydgYhA=", "requires": { + "ethereumjs-abi": "git+https://github.com/ethereumjs/ethereumjs-abi.git#2863c40e0982acfc0b7163f0285d4c56427c7799", "ethereumjs-util": "^5.1.1" }, "dependencies": { "ethereumjs-abi": { "version": "git+https://github.com/ethereumjs/ethereumjs-abi.git#2863c40e0982acfc0b7163f0285d4c56427c7799", - "from": "git+https://github.com/ethereumjs/ethereumjs-abi.git#2863c40e0982acfc0b7163f0285d4c56427c7799", + "from": "git+https://github.com/ethereumjs/ethereumjs-abi.git", "requires": { "bn.js": "^4.10.0", "ethereumjs-util": "^5.0.0" @@ -10059,12 +10087,14 @@ "integrity": "sha1-jZWCAsftuq6Dlwf7pvCf8ydgYhA=", "dev": true, "requires": { + "ethereumjs-abi": "git+https://github.com/ethereumjs/ethereumjs-abi.git#2863c40e0982acfc0b7163f0285d4c56427c7799", "ethereumjs-util": "^5.1.1" }, "dependencies": { "ethereumjs-abi": { "version": "git+https://github.com/ethereumjs/ethereumjs-abi.git#2863c40e0982acfc0b7163f0285d4c56427c7799", - "from": "git+https://github.com/ethereumjs/ethereumjs-abi.git#2863c40e0982acfc0b7163f0285d4c56427c7799", + "from": "git+https://github.com/ethereumjs/ethereumjs-abi.git", + "dev": true, "requires": { "bn.js": "^4.10.0", "ethereumjs-util": "^5.0.0" @@ -10136,16 +10166,15 @@ } }, "eth-keyring-controller": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/eth-keyring-controller/-/eth-keyring-controller-3.3.1.tgz", - "integrity": "sha512-Knz3alRHjgJ+/4LUBxXLApXeuoMsehaLOvVHpalSTkU//YPXBjvaITlc6653n+g1vvJ4sD2VbMNfFmYYyFHEtw==", + "version": "github:r001/KeyringController#9a2bdcd030b0fed5fa28b9eac3949c35698cf00f", + "from": "github:r001/KeyringController#9a2bdcd030b0fed5fa28b9eac3949c35698cf00f", "requires": { "bip39": "^2.4.0", "bluebird": "^3.5.0", "browser-passworder": "^2.0.3", - "eth-hd-keyring": "^1.2.2", + "eth-hd-keyring": "^2.0.0", "eth-sig-util": "^1.4.0", - "eth-simple-keyring": "^1.3.0", + "eth-simple-keyring": "^2.0.0", "ethereumjs-util": "^5.1.2", "loglevel": "^1.5.0", "obs-store": "^2.4.1", @@ -10161,6 +10190,35 @@ "object-assign": "^4.0.0" } }, + "eth-hd-keyring": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/eth-hd-keyring/-/eth-hd-keyring-2.0.0.tgz", + "integrity": "sha512-lTeANNPNj/j08sWU7LUQZTsx9NUJaUsiOdVxeP0UI5kke7L+Sd7zJWBmCShudEVG8PkqKLE1KJo08o430sl6rw==", + "requires": { + "bip39": "^2.2.0", + "eth-sig-util": "^2.0.1", + "ethereumjs-abi": "^0.6.5", + "ethereumjs-util": "^5.1.1", + "ethereumjs-wallet": "^0.6.0", + "events": "^1.1.1", + "xtend": "^4.0.1" + }, + "dependencies": { + "eth-sig-util": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/eth-sig-util/-/eth-sig-util-2.1.0.tgz", + "integrity": "sha512-JRKmq1zytYoOuAj8llYiGlRGSlWrQ0jGGh9+YPhELfmMP1PD/dkwq2kzMoB8pRF6sEgZojQfSasswto3xsKFvw==", + "requires": { + "buffer": "^5.2.1", + "elliptic": "^6.4.0", + "ethereumjs-abi": "0.6.5", + "ethereumjs-util": "^5.1.1", + "tweetnacl": "^1.0.0", + "tweetnacl-util": "^0.15.0" + } + } + } + }, "eth-sig-util": { "version": "1.4.2", "resolved": "https://registry.npmjs.org/eth-sig-util/-/eth-sig-util-1.4.2.tgz", @@ -10172,7 +10230,7 @@ "dependencies": { "ethereumjs-abi": { "version": "git+https://github.com/ethereumjs/ethereumjs-abi.git#2863c40e0982acfc0b7163f0285d4c56427c7799", - "from": "git+https://github.com/ethereumjs/ethereumjs-abi.git#2863c40e0982acfc0b7163f0285d4c56427c7799", + "from": "git+https://github.com/ethereumjs/ethereumjs-abi.git", "requires": { "bn.js": "^4.10.0", "ethereumjs-util": "^5.0.0" @@ -10180,14 +10238,6 @@ } } }, - "ethereumjs-abi": { - "version": "git+https://github.com/ethereumjs/ethereumjs-abi.git#2863c40e0982acfc0b7163f0285d4c56427c7799", - "from": "git+https://github.com/ethereumjs/ethereumjs-abi.git", - "requires": { - "bn.js": "^4.10.0", - "ethereumjs-util": "^5.0.0" - } - }, "ethereumjs-util": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/ethereumjs-util/-/ethereumjs-util-5.2.0.tgz", @@ -10213,6 +10263,11 @@ "through2": "^2.0.3", "xtend": "^4.0.1" } + }, + "tweetnacl": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-1.0.0.tgz", + "integrity": "sha1-cT2LgY2kIGh0C/aDhtBHnmb8ins=" } } }, @@ -10445,44 +10500,18 @@ } }, "eth-simple-keyring": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/eth-simple-keyring/-/eth-simple-keyring-1.3.1.tgz", - "integrity": "sha512-oOASghMej6WO+bjFF+/8bT2DU7D9QKgD81BbS+/qd26z25ueATcgwPNP2LrkoWUbe39OVVM4P5A4fTEEZpGAHg==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/eth-simple-keyring/-/eth-simple-keyring-2.0.0.tgz", + "integrity": "sha512-4dMbkIy2k1qotDTjWINvXG+7tBmofp0YUhlXgcG0+I3w684V46+MAHEkBtD2Y09iEeIB07RDXrezKP9WxOpynA==", "requires": { - "eth-sig-util": "^1.4.2", + "eth-sig-util": "^2.0.1", + "ethereumjs-abi": "^0.6.5", "ethereumjs-util": "^5.1.1", "ethereumjs-wallet": "^0.6.0", "events": "^1.1.1", "xtend": "^4.0.1" }, "dependencies": { - "eth-sig-util": { - "version": "1.4.2", - "resolved": "https://registry.npmjs.org/eth-sig-util/-/eth-sig-util-1.4.2.tgz", - "integrity": "sha1-jZWCAsftuq6Dlwf7pvCf8ydgYhA=", - "requires": { - "ethereumjs-abi": "git+https://github.com/ethereumjs/ethereumjs-abi.git#2863c40e0982acfc0b7163f0285d4c56427c7799", - "ethereumjs-util": "^5.1.1" - }, - "dependencies": { - "ethereumjs-abi": { - "version": "git+https://github.com/ethereumjs/ethereumjs-abi.git#2863c40e0982acfc0b7163f0285d4c56427c7799", - "from": "git+https://github.com/ethereumjs/ethereumjs-abi.git#2863c40e0982acfc0b7163f0285d4c56427c7799", - "requires": { - "bn.js": "^4.10.0", - "ethereumjs-util": "^5.0.0" - } - } - } - }, - "ethereumjs-abi": { - "version": "git+https://github.com/ethereumjs/ethereumjs-abi.git#2863c40e0982acfc0b7163f0285d4c56427c7799", - "from": "git+https://github.com/ethereumjs/ethereumjs-abi.git", - "requires": { - "bn.js": "^4.10.0", - "ethereumjs-util": "^5.0.0" - } - }, "ethereumjs-util": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/ethereumjs-util/-/ethereumjs-util-5.2.0.tgz", @@ -10669,12 +10698,13 @@ "resolved": "https://registry.npmjs.org/eth-sig-util/-/eth-sig-util-1.4.2.tgz", "integrity": "sha1-jZWCAsftuq6Dlwf7pvCf8ydgYhA=", "requires": { + "ethereumjs-abi": "git+https://github.com/ethereumjs/ethereumjs-abi.git#2863c40e0982acfc0b7163f0285d4c56427c7799", "ethereumjs-util": "^5.1.1" }, "dependencies": { "ethereumjs-abi": { "version": "git+https://github.com/ethereumjs/ethereumjs-abi.git#2863c40e0982acfc0b7163f0285d4c56427c7799", - "from": "git+https://github.com/ethereumjs/ethereumjs-abi.git#2863c40e0982acfc0b7163f0285d4c56427c7799", + "from": "git+https://github.com/ethereumjs/ethereumjs-abi.git", "requires": { "bn.js": "^4.10.0", "ethereumjs-util": "^5.0.0" @@ -11095,9 +11125,8 @@ } }, "ethereumjs-wallet": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/ethereumjs-wallet/-/ethereumjs-wallet-0.6.0.tgz", - "integrity": "sha1-gnY7Fpfuenlr5xVdqd+0my+Yz9s=", + "version": "github:r001/ethereumjs-wallet#4e35d91bd8b563f06c926b045f60371077e15d82", + "from": "github:r001/ethereumjs-wallet#4e35d91bd8b563f06c926b045f60371077e15d82", "requires": { "aes-js": "^0.2.3", "bs58check": "^1.0.8", @@ -11110,7 +11139,7 @@ "dependencies": { "ethereumjs-util": { "version": "4.5.0", - "resolved": "https://registry.npmjs.org/ethereumjs-util/-/ethereumjs-util-4.5.0.tgz", + "resolved": "http://registry.npmjs.org/ethereumjs-util/-/ethereumjs-util-4.5.0.tgz", "integrity": "sha1-PpQosxfuvaPXJg2FT93alUsfG8Y=", "requires": { "bn.js": "^4.8.0", @@ -11505,7 +11534,7 @@ }, "event-stream": { "version": "3.3.4", - "resolved": "https://registry.npmjs.org/event-stream/-/event-stream-3.3.4.tgz", + "resolved": "http://registry.npmjs.org/event-stream/-/event-stream-3.3.4.tgz", "integrity": "sha1-SrTJoPWlTbkzi0w02Gv86PSzVXE=", "dev": true, "requires": { @@ -13820,7 +13849,7 @@ }, "babelify": { "version": "7.3.0", - "resolved": "http://registry.npmjs.org/babelify/-/babelify-7.3.0.tgz", + "resolved": "https://registry.npmjs.org/babelify/-/babelify-7.3.0.tgz", "integrity": "sha1-qlau3nBn/XvVSWZu4W3ChQh+iOU=", "dev": true, "requires": { @@ -14915,7 +14944,7 @@ "dependencies": { "pify": { "version": "2.3.0", - "resolved": "http://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=", "dev": true } @@ -15215,7 +15244,7 @@ }, "ethereumjs-block": { "version": "1.2.2", - "resolved": "http://registry.npmjs.org/ethereumjs-block/-/ethereumjs-block-1.2.2.tgz", + "resolved": "https://registry.npmjs.org/ethereumjs-block/-/ethereumjs-block-1.2.2.tgz", "integrity": "sha1-LsdTSlkCG47JuDww5JaQxuuu3aE=", "dev": true, "requires": { @@ -15228,13 +15257,13 @@ "dependencies": { "async": { "version": "1.5.2", - "resolved": "http://registry.npmjs.org/async/-/async-1.5.2.tgz", + "resolved": "https://registry.npmjs.org/async/-/async-1.5.2.tgz", "integrity": "sha1-7GphrlZIDAw8skHJVhjiCJL5Zyo=", "dev": true }, "ethereumjs-util": { "version": "4.5.0", - "resolved": "http://registry.npmjs.org/ethereumjs-util/-/ethereumjs-util-4.5.0.tgz", + "resolved": "https://registry.npmjs.org/ethereumjs-util/-/ethereumjs-util-4.5.0.tgz", "integrity": "sha1-PpQosxfuvaPXJg2FT93alUsfG8Y=", "dev": true, "requires": { @@ -15307,7 +15336,7 @@ }, "ethereumjs-block": { "version": "1.7.1", - "resolved": "http://registry.npmjs.org/ethereumjs-block/-/ethereumjs-block-1.7.1.tgz", + "resolved": "https://registry.npmjs.org/ethereumjs-block/-/ethereumjs-block-1.7.1.tgz", "integrity": "sha512-B+sSdtqm78fmKkBq78/QLKJbu/4Ts4P2KFISdgcuZUPDm9x+N7qgBPIIFUGbaakQh8bzuquiRVbdmvPKqbILRg==", "dev": true, "requires": { @@ -16970,7 +16999,8 @@ "ms": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", + "dev": true }, "mz": { "version": "2.7.0", @@ -17004,7 +17034,7 @@ }, "node-fetch": { "version": "2.1.2", - "resolved": "http://registry.npmjs.org/node-fetch/-/node-fetch-2.1.2.tgz", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.1.2.tgz", "integrity": "sha1-q4hOjn5X44qUR1POxwb3iNF2i7U=", "dev": true }, @@ -17680,7 +17710,8 @@ "safe-buffer": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true }, "safer-buffer": { "version": "2.1.2", @@ -17911,7 +17942,7 @@ "dependencies": { "fs-extra": { "version": "0.30.0", - "resolved": "http://registry.npmjs.org/fs-extra/-/fs-extra-0.30.0.tgz", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-0.30.0.tgz", "integrity": "sha1-8jP/zAjU2n1DLapEl3aYnbHfk/A=", "dev": true, "requires": { @@ -18914,9 +18945,18 @@ "websocket": "git://github.com/frozeman/WebSocket-Node.git#6c72925e3f8aaaea8dc8450f97627e85263999f2" }, "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + }, "websocket": { "version": "git://github.com/frozeman/WebSocket-Node.git#6c72925e3f8aaaea8dc8450f97627e85263999f2", - "from": "git://github.com/frozeman/WebSocket-Node.git#6c72925e3f8aaaea8dc8450f97627e85263999f2", + "from": "git://github.com/frozeman/WebSocket-Node.git#browserifyCompatible", "dev": true, "requires": { "nan": "^2.3.3", @@ -18993,7 +19033,7 @@ }, "whatwg-fetch": { "version": "2.0.4", - "resolved": "http://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-2.0.4.tgz", + "resolved": "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-2.0.4.tgz", "integrity": "sha512-dcQ1GWpOD/eEQ97k66aiEVpNnapVj90/+R+SXTPYGHpYBBypfKJEQjLrvMZ7YXbKm21gXd4NcuxUTjiv1YtLng==", "dev": true }, @@ -19123,7 +19163,7 @@ }, "yargs-parser": { "version": "2.4.1", - "resolved": "http://registry.npmjs.org/yargs-parser/-/yargs-parser-2.4.1.tgz", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-2.4.1.tgz", "integrity": "sha1-hVaN488VD/SfpRgl8DqMiA3cxcQ=", "dev": true, "requires": { @@ -19366,7 +19406,7 @@ }, "pify": { "version": "2.3.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "resolved": "http://registry.npmjs.org/pify/-/pify-2.3.0.tgz", "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=", "dev": true } @@ -27384,7 +27424,7 @@ }, "caching-transform": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/caching-transform/-/caching-transform-1.0.1.tgz", + "resolved": false, "integrity": "sha1-bb2y8g+Nj7znnz6U6dF0Lc31wKE=", "dev": true, "requires": { @@ -27395,7 +27435,7 @@ "dependencies": { "md5-hex": { "version": "1.3.0", - "resolved": "https://registry.npmjs.org/md5-hex/-/md5-hex-1.3.0.tgz", + "resolved": false, "integrity": "sha1-0sSv6YPENwZiF5uMrRRSGRNQRsQ=", "dev": true, "requires": { @@ -27463,7 +27503,7 @@ }, "convert-source-map": { "version": "1.5.1", - "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.5.1.tgz", + "resolved": false, "integrity": "sha1-uCeAl7m8IpNl3lxiz1/K7YtVmeU=", "dev": true }, @@ -27509,7 +27549,7 @@ "dependencies": { "strip-bom": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "resolved": false, "integrity": "sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM=", "dev": true } @@ -27517,7 +27557,7 @@ }, "error-ex": { "version": "1.3.1", - "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.1.tgz", + "resolved": false, "integrity": "sha1-+FWobOYa3E6GIcPNoh56dhLDqNw=", "dev": true, "requires": { @@ -27554,7 +27594,7 @@ }, "find-cache-dir": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-1.0.0.tgz", + "resolved": false, "integrity": "sha1-kojj6ePMN0hxfTnq3hfPcfww7m8=", "dev": true, "requires": { @@ -27565,7 +27605,7 @@ }, "find-up": { "version": "2.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-2.1.0.tgz", + "resolved": false, "integrity": "sha1-RdG35QbHF93UgndaK3eSCjwMV6c=", "dev": true, "requires": { @@ -27590,7 +27630,7 @@ }, "get-caller-file": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-1.0.2.tgz", + "resolved": false, "integrity": "sha1-9wLmMSfn4jHBYKgMFVSstw1QR+U=", "dev": true }, @@ -27602,7 +27642,7 @@ }, "glob": { "version": "7.1.2", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz", + "resolved": false, "integrity": "sha512-MJTUg1kjuLeQCJ+ccE4Vpa6kKVXkPYJ2mOCQyUuKLcLQsdrMCpBPUi8qVE6+YuaJkozeA9NusTAw3hLr8Xe5EQ==", "dev": true, "requires": { @@ -27651,7 +27691,7 @@ }, "hosted-git-info": { "version": "2.6.0", - "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.6.0.tgz", + "resolved": false, "integrity": "sha512-lIbgIIQA3lz5XaB6vxakj6sDHADJiZadYEJB+FgA+C4nubM1NwcuvUr9EJPmnH1skZqpqUzWborWo8EIUi0Sdw==", "dev": true }, @@ -27724,13 +27764,13 @@ }, "istanbul-lib-coverage": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.0.tgz", + "resolved": false, "integrity": "sha512-yMSw5xLIbdaxiVXHk3amfNM2WeBxLrwH/BCyZ9HvA/fylwziAIJOG2rKqWyLqEJqwKT725vxxqidv+SyynnGAA==", "dev": true }, "istanbul-lib-hook": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/istanbul-lib-hook/-/istanbul-lib-hook-2.0.0.tgz", + "resolved": false, "integrity": "sha512-qm3dt628HKpCVtIjbdZLuQyXn0+LO8qz+YHQDfkeXuSk5D+p299SEV5DrnUUnPi2SXvdMmWapMYWiuE75o2rUQ==", "dev": true, "requires": { @@ -27739,7 +27779,7 @@ }, "istanbul-lib-report": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-2.0.0.tgz", + "resolved": false, "integrity": "sha512-RiELmy9oIRYUv36ITOAhVum9PUvuj6bjyXVEKEHNiD1me6qXtxfx7vSEJWnjOGk2QmYw/GRFjLXWJv3qHpLceQ==", "dev": true, "requires": { @@ -27750,7 +27790,7 @@ }, "istanbul-lib-source-maps": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-2.0.0.tgz", + "resolved": false, "integrity": "sha512-jenUeC0gMSSMGkvqD9xuNfs3nD7XWeXLhqaIkqHsNZ3DJBWPdlKEydE7Ya5aTgdWjrEQhrCYTv+J606cGC2vuQ==", "dev": true, "requires": { @@ -27771,7 +27811,7 @@ }, "istanbul-reports": { "version": "1.5.0", - "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-1.5.0.tgz", + "resolved": false, "integrity": "sha512-HeZG0WHretI9FXBni5wZ9DOgNziqDCEwetxnme5k1Vv5e81uTqcsy3fMH99gXGDGKr1ea87TyGseDMa2h4HEUA==", "dev": true, "requires": { @@ -27811,7 +27851,7 @@ }, "locate-path": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-2.0.0.tgz", + "resolved": false, "integrity": "sha1-K1aLJl7slExtnA3pw9u7ygNUzY4=", "dev": true, "requires": { @@ -27821,7 +27861,7 @@ "dependencies": { "path-exists": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", + "resolved": false, "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=", "dev": true } @@ -27854,7 +27894,7 @@ "dependencies": { "pify": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", + "resolved": false, "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=", "dev": true } @@ -27918,7 +27958,7 @@ }, "minimist": { "version": "0.0.8", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", + "resolved": false, "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=", "dev": true }, @@ -28008,7 +28048,7 @@ }, "p-limit": { "version": "1.2.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-1.2.0.tgz", + "resolved": false, "integrity": "sha512-Y/OtIaXtUPr4/YpMv1pCL5L5ed0rumAaAeBSj12F+bSlMdys7i8oQF/GUJmfpTS/QoaRrS/k6pma29haJpsMng==", "dev": true, "requires": { @@ -28017,7 +28057,7 @@ }, "p-locate": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-2.0.0.tgz", + "resolved": false, "integrity": "sha1-IKAQOyIqcMj9OcwuWAaA893l7EM=", "dev": true, "requires": { @@ -28026,7 +28066,7 @@ }, "p-try": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/p-try/-/p-try-1.0.0.tgz", + "resolved": false, "integrity": "sha1-y8ec26+P1CKOE/Yh8rGiN8GyB7M=", "dev": true }, @@ -28044,7 +28084,7 @@ }, "pkg-dir": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-2.0.0.tgz", + "resolved": false, "integrity": "sha1-9tXREJ4Z1j7fQo4L1X4Sd3YVM0s=", "dev": true, "requires": { @@ -28135,7 +28175,7 @@ }, "slide": { "version": "1.1.6", - "resolved": "https://registry.npmjs.org/slide/-/slide-1.1.6.tgz", + "resolved": false, "integrity": "sha1-VusCfWW00tzmyy4tMsTUr8nh1wc=", "dev": true }, @@ -28228,7 +28268,7 @@ }, "test-exclude": { "version": "4.2.2", - "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-4.2.2.tgz", + "resolved": false, "integrity": "sha512-2kTGf+3tykCfrWVREgyTR0bmVO0afE6i7zVXi/m+bZZ8ujV89Aulxdcdv32yH+unVFg3Y5o6GA8IzsHnGQuFgQ==", "dev": true, "requires": { @@ -28240,7 +28280,7 @@ "dependencies": { "load-json-file": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-4.0.0.tgz", + "resolved": false, "integrity": "sha1-L19Fq5HjMhYjT9U62rZo607AmTs=", "dev": true, "requires": { @@ -28252,7 +28292,7 @@ }, "parse-json": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-4.0.0.tgz", + "resolved": false, "integrity": "sha1-vjX1Qlvh9/bHRxhPmKeIy5lHfuA=", "dev": true, "requires": { @@ -28262,7 +28302,7 @@ }, "path-type": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/path-type/-/path-type-3.0.0.tgz", + "resolved": false, "integrity": "sha512-T2ZUsdZFHgA3u4e5PfPbjd7HDDpxPnQb5jN0SrDsjNSuVXHJqtwTnWqG0B1jZrgmJ/7lj1EmVIByWt1gxGkWvg==", "dev": true, "requires": { @@ -28271,13 +28311,13 @@ }, "pify": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", + "resolved": false, "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=", "dev": true }, "read-pkg": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-3.0.0.tgz", + "resolved": false, "integrity": "sha1-nLxoaXj+5l0WwA4rGcI3/Pbjg4k=", "dev": true, "requires": { @@ -28288,7 +28328,7 @@ }, "read-pkg-up": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-3.0.0.tgz", + "resolved": false, "integrity": "sha1-PtSWaF26D4/hGNBpHcUfSh/5bwc=", "dev": true, "requires": { @@ -28298,7 +28338,7 @@ }, "strip-bom": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "resolved": false, "integrity": "sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM=", "dev": true } @@ -28431,7 +28471,7 @@ }, "write-file-atomic": { "version": "1.3.4", - "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-1.3.4.tgz", + "resolved": false, "integrity": "sha1-+Aek8LHZ6ROuekgRLmzDrxmRtF8=", "dev": true, "requires": { @@ -28474,7 +28514,7 @@ "dependencies": { "camelcase": { "version": "4.1.0", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-4.1.0.tgz", + "resolved": false, "integrity": "sha1-1UVjW+HjPFQmScaRc+Xeas+uNN0=", "dev": true }, @@ -28491,7 +28531,7 @@ }, "yargs-parser": { "version": "9.0.2", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-9.0.2.tgz", + "resolved": false, "integrity": "sha1-nM9qQ0YP5O1Aqbto9I1DuKaMwHc=", "dev": true, "requires": { @@ -29644,7 +29684,7 @@ }, "through2": { "version": "0.2.3", - "resolved": "http://registry.npmjs.org/through2/-/through2-0.2.3.tgz", + "resolved": "https://registry.npmjs.org/through2/-/through2-0.2.3.tgz", "integrity": "sha1-6zKE2k6jEbbMis42U3SKUqvyWj8=", "dev": true, "requires": { @@ -31631,11 +31671,25 @@ "integrity": "sha512-8YOJEHtxpySA3fFDyCRxA+UUV+fA+rTWnuWvylOK/NCjhY+b4ocCtmu8TtsWb+mYeU+GCHf/S66KZF/AsteKHg==", "dev": true }, + "qr.js": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/qr.js/-/qr.js-0.0.0.tgz", + "integrity": "sha1-ys6GOG9ZoNuAUPqQ2baw6IoeNk8=" + }, "qrcode-npm": { "version": "0.0.3", "resolved": "https://registry.npmjs.org/qrcode-npm/-/qrcode-npm-0.0.3.tgz", "integrity": "sha1-d+5vvvqcDyn6CdTRUggHxqYEK5o=" }, + "qrcode.react": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/qrcode.react/-/qrcode.react-0.7.2.tgz", + "integrity": "sha512-s1x+E3bsp0ojI8cHQ+czr+aG3huLZegH+tqAuRsXh6oXvzNfC+9L2PeFRBBu8eRBiejMRrRzSH7iwi5LDyWfRg==", + "requires": { + "prop-types": "^15.5.8", + "qr.js": "0.0.0" + } + }, "qs": { "version": "6.5.1", "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.1.tgz", @@ -37297,6 +37351,11 @@ "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=", "optional": true }, + "tweetnacl-util": { + "version": "0.15.0", + "resolved": "https://registry.npmjs.org/tweetnacl-util/-/tweetnacl-util-0.15.0.tgz", + "integrity": "sha1-RXbBzuXi1j0gf+5S8boCgZSAvHU=" + }, "type-check": { "version": "0.3.2", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz", @@ -37974,7 +38033,7 @@ }, "uuid": { "version": "2.0.3", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-2.0.3.tgz", + "resolved": "http://registry.npmjs.org/uuid/-/uuid-2.0.3.tgz", "integrity": "sha1-Z+LoY3lyFVMN/zGOW/nc6/1Hsho=" }, "v8flags": { @@ -38327,6 +38386,7 @@ "resolved": "https://registry.npmjs.org/web3/-/web3-0.20.3.tgz", "integrity": "sha1-yqRDc9yIFayHZ73ba6cwc5ZMqos=", "requires": { + "bignumber.js": "git+https://github.com/frozeman/bignumber.js-nolookahead.git#57692b3ecfc98bbdd6b3a516cb2353652ea49934", "crypto-js": "^3.1.4", "utf8": "^2.1.1", "xhr2": "*", @@ -38335,7 +38395,7 @@ "dependencies": { "bignumber.js": { "version": "git+https://github.com/frozeman/bignumber.js-nolookahead.git#57692b3ecfc98bbdd6b3a516cb2353652ea49934", - "from": "git+https://github.com/frozeman/bignumber.js-nolookahead.git#57692b3ecfc98bbdd6b3a516cb2353652ea49934" + "from": "git+https://github.com/frozeman/bignumber.js-nolookahead.git" } } }, diff --git a/package.json b/package.json index f8da2dbe34b6..74d1c03f0908 100644 --- a/package.json +++ b/package.json @@ -118,8 +118,9 @@ "eth-hd-keyring": "^1.2.2", "eth-json-rpc-filters": "^3.0.1", "eth-json-rpc-infura": "^3.0.0", - "eth-keyring-controller": "^3.3.1", "eth-ledger-bridge-keyring": "^0.1.1", + "eth-keyring-controller": "github:r001/KeyringController#9a2bdcd030b0fed5fa28b9eac3949c35698cf00f", + "eth-external-account-keyring": "github:r001/eth-external-account-keyring#v1.0.0", "eth-method-registry": "^1.0.0", "eth-phishing-detect": "^1.1.4", "eth-query": "^2.1.2", @@ -129,7 +130,7 @@ "ethereumjs-abi": "^0.6.4", "ethereumjs-tx": "^1.3.0", "ethereumjs-util": "github:ethereumjs/ethereumjs-util#ac5d0908536b447083ea422b435da27f26615de9", - "ethereumjs-wallet": "^0.6.0", + "ethereumjs-wallet": "github:r001/ethereumjs-wallet#4e35d91bd8b563f06c926b045f60371077e15d82", "etherscan-link": "^1.0.2", "ethjs": "^0.4.0", "ethjs-contract": "^0.2.3", @@ -180,6 +181,7 @@ "pump": "^3.0.0", "pumpify": "^1.3.4", "qrcode-npm": "0.0.3", + "qrcode.react": "0.7.2", "ramda": "^0.24.1", "react": "^15.6.2", "react-addons-css-transition-group": "^15.6.0", diff --git a/ui/app/actions.js b/ui/app/actions.js index 5a4389d67fc2..c653542cc6e7 100644 --- a/ui/app/actions.js +++ b/ui/app/actions.js @@ -87,6 +87,7 @@ var actions = { createNewVaultInProgress: createNewVaultInProgress, addNewKeyring, importNewAccount, + addExternalAccount, addNewAccount, connectHardware, checkHardwareStatus, @@ -178,6 +179,7 @@ var actions = { VIEW_PENDING_TX: 'VIEW_PENDING_TX', updateTransactionParams, UPDATE_TRANSACTION_PARAMS: 'UPDATE_TRANSACTION_PARAMS', + updateSignature: updateSignature, // send screen UPDATE_GAS_LIMIT: 'UPDATE_GAS_LIMIT', UPDATE_GAS_PRICE: 'UPDATE_GAS_PRICE', @@ -330,6 +332,7 @@ var actions = { approveProviderRequest, rejectProviderRequest, clearApprovedOrigins, + updateExternalSign, } module.exports = actions @@ -611,6 +614,32 @@ function addNewKeyring (type, opts) { } } +function addExternalAccount (address) { + return async (dispatch) => { + let newState + dispatch(actions.showLoadingIndication('This may take a while, please be patient.')) + try { + log.debug(`background.addExtAccount`) + await pify(background.addExtAccount).call(background, address) + log.debug(`background.getState`) + newState = await pify(background.getState).call(background) + } catch (err) { + dispatch(actions.hideLoadingIndication()) + dispatch(actions.displayWarning(err.message)) + throw err + } + dispatch(actions.hideLoadingIndication()) + dispatch(actions.updateMetamaskState(newState)) + if (newState.selectedAddress) { + dispatch({ + type: actions.SHOW_ACCOUNT_DETAIL, + value: newState.selectedAddress, + }) + } + return newState + } +} + function importNewAccount (strategy, args) { return async (dispatch) => { let newState @@ -753,7 +782,7 @@ function showInfoPage () { } } -function showQrScanner (ROUTE) { +function showQrScanner (ROUTE, props = {}) { return (dispatch, getState) => { return WebcamUtils.checkStatus() .then(status => { @@ -761,8 +790,10 @@ function showQrScanner (ROUTE) { // We need to switch to fullscreen mode to ask for permission global.platform.openExtensionInBrowser(`${ROUTE}`, `scan=true`) } else { + props.route = ROUTE dispatch(actions.showModal({ name: 'QR_SCANNER', + ...props, })) } }).catch(e => { @@ -770,6 +801,7 @@ function showQrScanner (ROUTE) { name: 'QR_SCANNER', error: true, errorType: e.type, + ...props, })) }) } @@ -1174,6 +1206,13 @@ function updateTransactionParams (id, txParams) { } } +function updateSignature(newSignature) { + return { + type: actions.UPDATE_SIGNATURE, + value: newSignature, + } +} + function txError (err) { return { type: actions.TRANSACTION_ERROR, @@ -2518,3 +2557,10 @@ function clearApprovedOrigins () { background.clearApprovedOrigins() } } + +function updateExternalSign (payload) { +return (dispatch) => { + background.updateExternalSign(payload) + } +} + diff --git a/ui/app/components/account-menu/index.js b/ui/app/components/account-menu/index.js index e88389096a37..2bd8bbf8ec76 100644 --- a/ui/app/components/account-menu/index.js +++ b/ui/app/components/account-menu/index.js @@ -239,6 +239,9 @@ AccountMenu.prototype.renderKeyringType = function (keyring) { case 'Simple Key Pair': label = this.context.t('imported') break + case 'External Account': + label = this.context.t('external') + break default: label = '' } diff --git a/ui/app/components/modals/external-sign-modal/external-sign-modal-container.js b/ui/app/components/modals/external-sign-modal/external-sign-modal-container.js new file mode 100644 index 000000000000..897f33e1c4bc --- /dev/null +++ b/ui/app/components/modals/external-sign-modal/external-sign-modal-container.js @@ -0,0 +1,49 @@ +const Component = require('react').Component +const PropTypes = require('prop-types') +const h = require('react-hyperscript') +const inherits = require('util').inherits +const connect = require('react-redux').connect +const actions = require('../../../actions') +const { getSelectedIdentity } = require('../../../selectors') + +function mapStateToProps (state, ownProps) { + return { + selectedIdentity: ownProps.selectedIdentity || getSelectedIdentity(state), + } +} + +function mapDispatchToProps (dispatch) { + return {} +} + +inherits(ExternalSignModalContainer, Component) +function ExternalSignModalContainer () { + Component.call(this) +} + +ExternalSignModalContainer.contextTypes = { + t: PropTypes.func, +} + +export default connect(mapStateToProps, mapDispatchToProps)(ExternalSignModalContainer) + + +ExternalSignModalContainer.prototype.render = function () { + let { children } = this.props + + if (children.constructor !== Array) { + children = [children] + } + + return h('div', { style: { borderRadius: '4px' }}, [ + h('div.account-modal-container', [ + + h('div.account-modal-close', { + onClick: this.props.hideModal, + }), + + ...children, + + ]), + ]) +} diff --git a/ui/app/components/modals/external-sign-modal/external-sign-modal.js b/ui/app/components/modals/external-sign-modal/external-sign-modal.js new file mode 100644 index 000000000000..f0ff09806d2c --- /dev/null +++ b/ui/app/components/modals/external-sign-modal/external-sign-modal.js @@ -0,0 +1,347 @@ +import React, { PureComponent } from 'react' +import ExternalSignModalContainer from './external-sign-modal-container' +import QrView from '../../qr-code' +import SendRowWrapper from '../../send/send-content/send-row-wrapper' +import TxInput from './tx-input.js' +import PageContainerFooter from '../../page-container/page-container-footer' +const PropTypes = require('prop-types') +const h = require('react-hyperscript') +const connect = require('react-redux').connect +const ethUtil = require('ethereumjs-util') +const sigUtil = require('eth-sig-util') +const Transaction = require('ethereumjs-tx') +const extend = require('xtend') + +import { + updateSignature, +} from '../../../ducks/confirm-transaction.duck' +import { + showQrScanner, + qrCodeDetected, + hideModal, + updateExternalSign, + hideLoadingIndication, +} from '../../../actions' +import { CONFIRM_TRANSACTION_ROUTE } from '../../../routes' + +const mapStateToProps = (state, ownProps) => { + const { confirmTransaction } = state + const { + txData, + signature, + } = confirmTransaction + const qrCodeData = state.appState.qrCodeData + const props = state.appState.modal.modalState.props + const signable = props.signable + const qrTxData = reduce(signable) + const extToSign = state.metamask.extToSign + const extCancel = state.metamask.extCancel + const extSigned = state.metamask.extSigned + const showError = !!signature && signatureInError(signable, signature) + var errorType + var errors = { + invalidSignatureLength: 'invalidSignatureLength', + invalidSignature: 'invalidSignature', + invalidSignatureChar: 'invalidSignatureChar', + } + if (signature.replace(/[^a-f0-9A-Fx]/g, '') !== signature) { + errorType = 'invalidSignatureChar' + } else if (ethUtil.stripHexPrefix(signature).length !== 130) { + errorType = 'invalidSignatureLength' + } else { + errorType = showError ? 'invalidSignature' : null + } + const qrScanner = true // display qr scanner icon on input field + return { + errors, + errorType, + extCancel, + extSigned, + extToSign, + qrCodeData, + qrScanner, + qrTxData, + showError, + signable, + signature, + txData, + } +} + +const mapDispatchToProps = (dispatch) => { + return { + onChange: (newSignature) => { + dispatch(updateSignature(newSignature)) + }, + scanSignatureQrCode: (props) => { + dispatch(showQrScanner(CONFIRM_TRANSACTION_ROUTE, props)) + }, + qrCodeDetected: (data) => dispatch(qrCodeDetected(data)), + onCancel: (extCancelCopy) => { + dispatch(updateExternalSign({extCancel: extCancelCopy})) + dispatch(hideModal()) + }, + onSubmit: (extSignedCopy) => { + dispatch(updateExternalSign({extSigned: extSignedCopy})) + dispatch(hideModal()) + }, + updateExternalSign: (update) => { + dispatch(updateExternalSign(update)) + }, + hideLodingIndication: () => { + dispatch(hideLoadingIndication()) + }, + } +} + +class ExternalSignModal extends PureComponent { + static propTypes = { + errors: PropTypes.object, + errorType: PropTypes.string, + extCancel: PropTypes.array, + extSigned: PropTypes.array, + extToSign: PropTypes.array, + hideLodingIndication: PropTypes.func, + onCancel: PropTypes.func, + onChange: PropTypes.func, + onSubmit: PropTypes.func, + qrCodeData: PropTypes.object, + qrCodeDetected: PropTypes.func, + qrScanner: PropTypes.bool, + qrTxData: PropTypes.string, + scanSignatureQrCode: PropTypes.func, + showError: PropTypes.bool, + signable: PropTypes.object, + signature: PropTypes.string, + updateExternalSign: PropTypes.func, + } + + static contextTypes = { + t: PropTypes.func, + } + constructor (props) { + super(props) + this.state = { cancelOrSubmitPressed: false } + this.onCancel = this.onCancel.bind(this) + this.onSubmit = this.onSubmit.bind(this) + this.container = null + + this.setContainerRef = element => { + this.container = element + } + } + render () { + const { + errors, + errorType, + onChange, + qrTxData, + qrScanner, + scanSignatureQrCode, + showError, + signable, + signature, + } = this.props + + return h(ExternalSignModalContainer, {hideModal: this.onCancel }, [ + h(QrView, { + Qr: { + data: qrTxData, + width: Math.min(480, window.innerWidth - 70), + isDataAddress: false, + }, + }), + h(SendRowWrapper, { + label: this.context.t('signature'), + errors: errors, + errorType: errorType, + showError: showError, + }, [ + h(TxInput, { + signature: signature, + onChange: onChange, + inError: showError, + qrScanner: qrScanner, + scanSignatureQrCode: scanSignatureQrCode, + scannerProps: {showNext: 'EXTERNAL_SIGN', nextProps: {signable: signable} }, + }), + ]), + h(PageContainerFooter, { + onCancel: this.onCancel, + onSubmit: this.onSubmit, + submitText: this.context.t('confirm'), + disabled: showError || !signature, + }), + ]) + } + + onCancel () { + this.setState({cancelOrSubmitPressed: true}, () => { + const {extCancel, signable} = this.props + this.extToSignUpdate() + var extCancelCopy = extCancel.slice() + extCancelCopy.push(signable) + this.props.onCancel(extCancelCopy) + }) + } + + onSubmit () { + this.setState({cancelOrSubmitPressed: true}, () => { + const {extSigned, signable, signature} = this.props + var signableCopy = extend({}, signable) + signableCopy.signature = signature + var extSignedCopy = extSigned.slice() + extSignedCopy.push(signableCopy) + this.extToSignUpdate() + this.props.onSubmit(extSignedCopy) + }) + } + + extToSignUpdate () { + const {extToSign, signable, updateExternalSign} = this.props + var signables = extToSign.filter(sgn => sgn.id !== signable.id) + updateExternalSign({extToSign: signables}) + } + + componentDidMount () { + const { + onChange, + qrCodeData, + qrCodeDetected, + } = this.props + if (qrCodeData && qrCodeData.type === 'signature') { + onChange(qrCodeData.values.signature) + qrCodeDetected(null) + } + this.props.hideLodingIndication() + } + + componentWillUnmount () { + if (!this.state.cancelOrSubmitPressed) { + this.onCancel() + } + } +} + +export default connect(mapStateToProps, mapDispatchToProps)(ExternalSignModal) + +// converts hex encoded signature to r, s, v signature +function signatureHexToRSV (signature) { + signature = ethUtil.stripHexPrefix(signature) + const r = ethUtil.toBuffer('0x' + signature.substr(0, 64)) + const s = ethUtil.toBuffer('0x' + signature.substr(64, 64)) + const v = ethUtil.toBuffer('0x' + signature.substr(128, 2)) + return {r: r, s: s, v: v} +} + +function signatureInError (signable, signature) { + try { + const {r, s, v} = signatureHexToRSV(signature) + switch (signable.type) { + case 'sign_transaction': + const tx = new Transaction(signable.payload) + tx.r = r + tx.s = s + tx.v = v + return !tx.verifySignature() + case 'sign_message': + ethUtil.ecrecover(ethUtil.sha3(ethUtil.toBuffer(signable.payload)), ethUtil.bufferToInt(v), r, s) + return false + case 'sign_personal_message': + ethUtil.ecrecover(ethUtil.hashPersonalMessage(ethUtil.toBuffer(signable.payload)), ethUtil.bufferToInt(v), r, s) + return false + case 'sign_typed_data': + ethUtil.ecrecover(sigUtil.sign(signable.payload), ethUtil.bufferToInt(v), r, s) + return false + } + } catch (e) { + return true + } +} + +function reduce (signable) { + const fromAddress = ethUtil.stripHexPrefix(signable.from) + const partFrom = fromAddress.substr(0, 4) + + fromAddress.substr(18, 2) + fromAddress.slice(-4) + switch (signable.type) { + case 'sign_message': + return 'eths:/?m=' + partFrom + signable.payloadd + case 'sign_personal_message': + return 'eths:/?p=' + partFrom + signable.payload + case 'sign_typed_data': + return 'eths:/?y=' + partFrom + signable.payload + case 'sign_transaction': + const tx = signable.payload + var chainId = tx.chainId + var chainCode + switch (chainId) { + case 1: + chainCode = 0 + break + case 61: + chainCode = 1 + break + case 2: + chainCode = 3 // Morden testnet + break + case 3: + chainCode = 4 + break + case 4: + chainCode = 5 + break + case 42: + chainCode = 6 + break + case 77: + chainCode = 7 + break + case 99: + chainCode = 9 + break + case 7762959: + chainCode = 10 + break + default: + chainCode = 8 + break + } + var signingType = 0 + var signChainVersion = ethUtil.stripHexPrefix(ethUtil.intToHex(signingType * 4 + chainCode * 16)) + var flatStr = signChainVersion + partFrom + flatStr += ethUtil.stripHexPrefix(tx.to) + flatStr += chainCode === 8 ? chainId.toString() + '|' : '' + flatStr += ethUtil.stripHexPrefix(tx.nonce) + '|' + flatStr += ethUtil.stripHexPrefix(tx.gasPrice) + '|' + flatStr += ethUtil.stripHexPrefix(tx.gasLimit) + '|' + flatStr += ethUtil.stripHexPrefix(tx.value) + '|' + flatStr += ethUtil.stripHexPrefix(tx.data) + flatStr = flatStr.toLowerCase() + + // escape all '9' in flat string + const numStr = flatStr + .replace(/9/g, '99') + .replace(/a/g, '90') + .replace(/b/g, '91') + .replace(/c/g, '92') + .replace(/d/g, '93') + .replace(/e/g, '94') + .replace(/f/g, '95') + .replace(/\|/g, '96') + + // zero compressing + const zeroCompStr = numStr.replace(/0{48}/g, '975').replace(/0{24}/g, '974').replace(/0{12}/g, '973').replace(/0{6}/g, '972').replace(/0{5}/g, '971').replace(/0{4}/g, '970') + var errorChk = ethUtil.stripHexPrefix(ethUtil.bufferToHex(ethUtil.sha3(ethUtil.toBuffer(zeroCompStr))).slice(-2)) + + // error check + errorChk = errorChk + .replace(/9/g, '99') + .replace(/a/g, '90') + .replace(/b/g, '91') + .replace(/c/g, '92') + .replace(/d/g, '93') + .replace(/e/g, '94') + .replace(/f/g, '95') + return 'eths:/?t=' + errorChk + zeroCompStr + } +} diff --git a/ui/app/components/modals/external-sign-modal/index.js b/ui/app/components/modals/external-sign-modal/index.js new file mode 100644 index 000000000000..348f929b0a60 --- /dev/null +++ b/ui/app/components/modals/external-sign-modal/index.js @@ -0,0 +1 @@ +export { default } from './external-sign-modal' diff --git a/ui/app/components/modals/external-sign-modal/tx-input.js b/ui/app/components/modals/external-sign-modal/tx-input.js new file mode 100644 index 000000000000..cfa892d916cb --- /dev/null +++ b/ui/app/components/modals/external-sign-modal/tx-input.js @@ -0,0 +1,49 @@ +const Component = require('react').Component +const PropTypes = require('prop-types') +const h = require('react-hyperscript') +const inherits = require('util').inherits +const connect = require('react-redux').connect +const Tooltip = require('../../tooltip') + +TxInput.contextTypes = { + t: PropTypes.func, +} + +module.exports = connect()(TxInput) + + +inherits(TxInput, Component) +function TxInput () { + Component.call(this) +} + +TxInput.prototype.render = function () { + const { + signature, + onChange, + inError, + qrScanner, + scanSignatureQrCode, + scannerProps, + } = this.props + + return h('div.send-v2__to-autocomplete', {}, [ + + h(`input.send-v2__to-autocomplete__input${qrScanner ? '.with-qr' : ''}`, { + placeholder: this.context.t('signature'), + className: inError ? `send-v2__error-border` : '', + value: signature, + onChange: event => onChange(event.target.value), + style: { + borderColor: inError ? 'red' : null, + }, + }), + qrScanner && h(Tooltip, { + title: this.context.t('scanQrCode'), + position: 'bottom', + }, h(`i.fa.fa-qrcode.fa-lg.send-v2__to-autocomplete__qr-code`, { + style: { color: '#33333' }, + onClick: () => scanSignatureQrCode(scannerProps), + })), + ]) +} diff --git a/ui/app/components/modals/modal.js b/ui/app/components/modals/modal.js index 0a603db4e230..d91c0f7aa807 100644 --- a/ui/app/components/modals/modal.js +++ b/ui/app/components/modals/modal.js @@ -8,6 +8,7 @@ const { resetCustomData: resetCustomGasData } = require('../../ducks/gas.duck') const isMobileView = require('../../../lib/is-mobile-view') const { getEnvironmentType } = require('../../../../app/scripts/lib/util') const { ENVIRONMENT_TYPE_POPUP } = require('../../../../app/scripts/lib/enums') +const extend = require('xtend') // Modal Components const BuyOptions = require('./buy-options-modal') @@ -29,6 +30,7 @@ import WelcomeBeta from './welcome-beta' import RejectTransactions from './reject-transactions' import ClearApprovedOrigins from './clear-approved-origins' import ConfirmCustomizeGasModal from '../gas-customization/gas-modal-page-container' +import ExternalSignModal from './external-sign-modal' const modalContainerBaseStyle = { transform: 'translate3d(-50%, 0, 0px)', @@ -172,6 +174,30 @@ const MODALS = { ...accountModalStyle, }, + EXTERNAL_SIGN: { + contents: [ + h(ExternalSignModal), + ], + mobileModalStyle: { + width: '100vw', + height: '100vh', + top: '0', + transform: 'none', + left: '0', + right: '0', + margin: '0 auto', + }, + laptopModalStyle: { + width: '600px', + height: '640px', + top: '20px', + transform: 'none', + left: '0', + right: '0', + margin: '0 auto', + }, + }, + EXPORT_PRIVATE_KEY: { contents: [ h(ExportPrivateKeyModal, {}, []), @@ -391,7 +417,9 @@ const BACKDROPSTYLE = { function mapStateToProps (state) { return { active: state.appState.modal.open, + currentView: state.appState.currentView.name, modalState: state.appState.modal.modalState, + extToSign: state.metamask.extToSign, } } @@ -406,7 +434,9 @@ function mapDispatchToProps (dispatch) { hideWarning: () => { dispatch(actions.hideWarning()) }, - + showModal: (payload) => { + dispatch(actions.showModal(payload)) + }, } } @@ -448,6 +478,18 @@ Modal.prototype.render = function () { } Modal.prototype.componentWillReceiveProps = function (nextProps) { + if ( + nextProps.extToSign.length > this.props.extToSign.length && + this.props.currentView === 'confTx' && + !(this.props.modalState.name === 'EXTERNAL_SIGN' && + this.props.modalState.open) + ) { + this.props.showModal({ + name: 'EXTERNAL_SIGN', + signable: extend({}, nextProps.extToSign[0]), + updateMetamask: true, + }) + } if (nextProps.active) { this.show() } else if (this.props.active) { diff --git a/ui/app/components/modals/qr-scanner/qr-scanner.component.js b/ui/app/components/modals/qr-scanner/qr-scanner.component.js index cb8d07d83831..64dcb6614701 100644 --- a/ui/app/components/modals/qr-scanner/qr-scanner.component.js +++ b/ui/app/components/modals/qr-scanner/qr-scanner.component.js @@ -9,10 +9,14 @@ import PageContainerFooter from '../../page-container/page-container-footer/page export default class QrScanner extends Component { static propTypes = { hideModal: PropTypes.func.isRequired, + showModal: PropTypes.func.isRequired, qrCodeDetected: PropTypes.func, scanQrCode: PropTypes.func, error: PropTypes.bool, errorType: PropTypes.string, + showNext: PropTypes.string, + nextProps: PropTypes.object, + route: PropTypes.string, } static contextTypes = { @@ -25,6 +29,7 @@ export default class QrScanner extends Component { this.state = { ready: false, msg: context.t('accessingYourCamera'), + showNextModal: true, } this.codeReader = null this.permissionChecker = null @@ -66,6 +71,7 @@ export default class QrScanner extends Component { if (this.codeReader) { this.codeReader.reset() } + this.showNextOrHide() } initCamera () { @@ -112,6 +118,11 @@ export default class QrScanner extends Component { type = 'address' values = {'address': content.split('ethereum:')[1] } + } else if (content.split('eths:').length > 1) { + // signature from external signer + type = 'signature' + values = {'signature': content.split('signature:')[1]} + // Regular ethereum addresses - fox ex. 0x.....1111 } else if (content.substring(0, 2).toLowerCase() === '0x') { @@ -122,22 +133,36 @@ export default class QrScanner extends Component { return {type, values} } + showNextOrHide = () => { + if (this.state.showNextModal && this.props.showNext) { + setTimeout(_ => { + this.props.showModal({ + name: this.props.showNext, + ...this.props.nextProps, + }) + }, 300) + } else { + this.props.hideModal() + } + } stopAndClose = () => { if (this.codeReader) { this.codeReader.reset() } this.setState({ ready: false }) - this.props.hideModal() + this.showNextOrHide() } tryAgain = () => { - // close the modal - this.stopAndClose() - // wait for the animation and try again - setTimeout(_ => { - this.props.scanQrCode() - }, 1000) + this.setState({showNextModal: false}, () => { + // close the modal + this.stopAndClose() + // wait for the animation and try again + setTimeout(_ => { + this.props.scanQrCode(this.props.route, {showNext: this.props.showNext, nextProps: this.props.nextProps}) + }, 1000) + }) } renderVideo () { diff --git a/ui/app/components/modals/qr-scanner/qr-scanner.container.js b/ui/app/components/modals/qr-scanner/qr-scanner.container.js index d0a35e03b70e..aaae59cf8d80 100644 --- a/ui/app/components/modals/qr-scanner/qr-scanner.container.js +++ b/ui/app/components/modals/qr-scanner/qr-scanner.container.js @@ -1,23 +1,28 @@ import { connect } from 'react-redux' import QrScanner from './qr-scanner.component' -const { hideModal, qrCodeDetected, showQrScanner } = require('../../../actions') +const { hideModal, showModal, qrCodeDetected, showQrScanner } = require('../../../actions') import { SEND_ROUTE, } from '../../../routes' const mapStateToProps = state => { + const props = state.appState.modal.modalState.props return { - error: state.appState.modal.modalState.props.error, - errorType: state.appState.modal.modalState.props.errorType, + error: props.error, + errorType: props.errorType, + showNext: props.showNext, + nextProps: props.nextProps, + route: props.route, } } const mapDispatchToProps = dispatch => { return { + showModal: (props) => dispatch(showModal(props)), hideModal: () => dispatch(hideModal()), qrCodeDetected: (data) => dispatch(qrCodeDetected(data)), - scanQrCode: () => dispatch(showQrScanner(SEND_ROUTE)), + scanQrCode: (route, props) => dispatch(showQrScanner(route || SEND_ROUTE, props)), } } diff --git a/ui/app/components/pages/create-account/external-account/external-account.js b/ui/app/components/pages/create-account/external-account/external-account.js new file mode 100644 index 000000000000..4d13a9594687 --- /dev/null +++ b/ui/app/components/pages/create-account/external-account/external-account.js @@ -0,0 +1,112 @@ +const inherits = require('util').inherits +const Component = require('react').Component +const h = require('react-hyperscript') +const { withRouter } = require('react-router-dom') +const { compose } = require('recompose') +const PropTypes = require('prop-types') +const connect = require('react-redux').connect +const actions = require('../../../../actions') +const { DEFAULT_ROUTE } = require('../../../../routes') +import Button from '../../../button' + +ExternalAccountImportView.contextTypes = { + t: PropTypes.func, +} + +module.exports = compose( + withRouter, + connect(mapStateToProps, mapDispatchToProps) +)(ExternalAccountImportView) + + +function mapStateToProps (state) { + return { + error: state.appState.warning, + firstAddress: Object.keys(state.metamask.accounts)[0], + } +} + +function mapDispatchToProps (dispatch) { + return { + addExternalAccount: (address) => { + return dispatch(actions.addExternalAccount(address)) + }, + displayWarning: (message) => dispatch(actions.displayWarning(message || null)), + setSelectedAddress: (address) => dispatch(actions.setSelectedAddress(address)), + } +} + +inherits(ExternalAccountImportView, Component) +function ExternalAccountImportView () { + this.createKeyringOnEnter = this.createKeyringOnEnter.bind(this) + Component.call(this) +} + +ExternalAccountImportView.prototype.render = function () { + const { error, displayWarning } = this.props + + return ( + h('div.new-account-import-form__private-key', [ + + h('span.new-account-create-form__instruction', this.context.t('pasteExternalAccount')), + + h('div.new-account-import-form__private-key-password-container', [ + + h('input.new-account-import-form__input-password', { + type: 'text', + id: 'external-account-box', + onKeyPress: e => this.createKeyringOnEnter(e), + }), + + ]), + + h('div.new-account-import-form__buttons', {}, [ + + h(Button, { + type: 'default', + large: true, + className: 'new-account-create-form__button', + onClick: () => { + displayWarning(null) + this.props.history.push(DEFAULT_ROUTE) + }, + }, [this.context.t('cancel')]), + + h(Button, { + type: 'primary', + large: true, + className: 'new-account-create-form__button', + onClick: () => this.createNewKeychain(), + }, [this.context.t('create')]), + + ]), + + error ? h('span.error', error) : null, + ]) + ) +} + +ExternalAccountImportView.prototype.createKeyringOnEnter = function (event) { + if (event.key === 'Enter') { + event.preventDefault() + this.createNewKeychain() + } +} + +ExternalAccountImportView.prototype.createNewKeychain = function () { + const input = document.getElementById('external-account-box') + const address = input.value + const { addExternalAccount, history, displayWarning, setSelectedAddress, firstAddress } = this.props + + addExternalAccount(address) + .then(({ selectedAddress }) => { + if (selectedAddress) { + history.push(DEFAULT_ROUTE) + displayWarning(null) + } else { + displayWarning('Error adding external account.') + setSelectedAddress(firstAddress) + } + }) + .catch(err => err && displayWarning(err.message || err)) +} diff --git a/ui/app/components/pages/create-account/external-account/index.js b/ui/app/components/pages/create-account/external-account/index.js new file mode 100644 index 000000000000..73075e979c6f --- /dev/null +++ b/ui/app/components/pages/create-account/external-account/index.js @@ -0,0 +1,39 @@ +const inherits = require('util').inherits +const Component = require('react').Component +//const React = require('react').React +const h = require('react-hyperscript') +const PropTypes = require('prop-types') +const connect = require('react-redux').connect + +// Subviews +const ExternalAccountImportView = require('./external-account.js') + + +ExternalAccountForm.contextTypes = { + t: PropTypes.func, +} + +module.exports = connect()(ExternalAccountForm) + + +inherits(ExternalAccountForm, Component) +function ExternalAccountForm () { + Component.call(this) +} + +ExternalAccountForm.prototype.render = function () { + + return ( + h('div.new-account-import-form', [ + + h('.new-account-import-disclaimer', [ + h('span', this.context.t('externalAccountMsg')), + ]), + + h('div.new-account-import-form__select-section', [ + + h(ExternalAccountImportView), + ]), + ]) + ) +} diff --git a/ui/app/components/pages/create-account/external-account/seed.js b/ui/app/components/pages/create-account/external-account/seed.js new file mode 100644 index 000000000000..d98909baa1f3 --- /dev/null +++ b/ui/app/components/pages/create-account/external-account/seed.js @@ -0,0 +1,35 @@ +const inherits = require('util').inherits +const Component = require('react').Component +const h = require('react-hyperscript') +const PropTypes = require('prop-types') +const connect = require('react-redux').connect + +SeedImportSubview.contextTypes = { + t: PropTypes.func, +} + +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: { + }, + }, [ + this.context.t('pasteSeed'), + h('textarea'), + h('br'), + h('button', this.context.t('submit')), + ]) + ) +} diff --git a/ui/app/components/pages/create-account/index.js b/ui/app/components/pages/create-account/index.js index d3de1ea0118e..41eb06878f1a 100644 --- a/ui/app/components/pages/create-account/index.js +++ b/ui/app/components/pages/create-account/index.js @@ -9,10 +9,12 @@ const classnames = require('classnames') const NewAccountCreateForm = require('./new-account') const NewAccountImportForm = require('./import-account') const ConnectHardwareForm = require('./connect-hardware') +const ExternalAccountForm = require('./external-account') const { NEW_ACCOUNT_ROUTE, IMPORT_ACCOUNT_ROUTE, CONNECT_HARDWARE_ROUTE, + EXTERNAL_ACCOUNT_ROUTE, } = require('../../../routes') class CreateAccountPage extends Component { @@ -54,6 +56,19 @@ class CreateAccountPage extends Component { }, this.context.t('connect') ), + h( + 'div.new-account__tabs__tab', + { + className: classnames('new-account__tabs__tab', { + 'new-account__tabs__selected': matchPath(location.pathname, { + path: EXTERNAL_ACCOUNT_ROUTE, + exact: true, + }), + }), + onClick: () => history.push(EXTERNAL_ACCOUNT_ROUTE), + }, + this.context.t('external') + ), ]) } @@ -80,6 +95,11 @@ class CreateAccountPage extends Component { path: CONNECT_HARDWARE_ROUTE, component: ConnectHardwareForm, }), + h(Route, { + exact: true, + path: EXTERNAL_ACCOUNT_ROUTE, + component: ExternalAccountForm, + }), ]), ]), ]) diff --git a/ui/app/components/qr-code.js b/ui/app/components/qr-code.js index d3242ddf5f85..3feb06e4bcd9 100644 --- a/ui/app/components/qr-code.js +++ b/ui/app/components/qr-code.js @@ -6,7 +6,7 @@ const connect = require('react-redux').connect const { isHexPrefixed } = require('ethereumjs-util') const ReadOnlyInput = require('./readonly-input') const { checksumAddress } = require('../util') - +import QRCode from 'qrcode.react' module.exports = connect(mapStateToProps)(QrCodeView) function mapStateToProps (state) { @@ -25,12 +25,15 @@ function QrCodeView () { QrCodeView.prototype.render = function () { const props = this.props - const { message, data } = props.Qr - const address = `${isHexPrefixed(data) ? 'ethereum:' : ''}${checksumAddress(data)}` - const qrImage = qrCode(4, 'M') - qrImage.addData(address) - qrImage.make() - + const { message, data, isDataAddress, width} = props.Qr + const isAddress = isDataAddress !== false + var qrImage + if (isAddress) { + const address = `${isHexPrefixed(data) ? 'ethereum:' : ''}${checksumAddress(data)}` + qrImage = qrCode(4, 'M') + qrImage.addData(address) + qrImage.make() + } return h('.div.flex-column.flex-center', [ Array.isArray(message) ? h('.message-container', this.renderMultiMessage()) @@ -42,16 +45,22 @@ QrCodeView.prototype.render = function () { }, this.props.warning) : null, - h('.div.qr-wrapper', { + isAddress ? h('.div.qr-wrapper', { style: {}, dangerouslySetInnerHTML: { __html: qrImage.createTableTag(4), }, - }), + }) : h('.div.qr-wrapper', { style: {} }, [ + h(QRCode, { + value: data, + size: width || 480, + }), + ]), + h(ReadOnlyInput, { wrapperClass: 'ellip-address-wrapper', inputClass: 'qr-ellip-address', - value: checksumAddress(data), + value: isDataAddress ? checksumAddress(data) : data, }), ]) } diff --git a/ui/app/components/send/send-content/send-row-wrapper/send-row-error-message/send-row-error-message.component.js b/ui/app/components/send/send-content/send-row-wrapper/send-row-error-message/send-row-error-message.component.js index 61bc7bab77ed..7fb4ca598118 100644 --- a/ui/app/components/send/send-content/send-row-wrapper/send-row-error-message/send-row-error-message.component.js +++ b/ui/app/components/send/send-content/send-row-wrapper/send-row-error-message/send-row-error-message.component.js @@ -6,6 +6,7 @@ export default class SendRowErrorMessage extends Component { static propTypes = { errors: PropTypes.object, errorType: PropTypes.string, + customErrors: PropTypes.object, }; static contextTypes = { @@ -13,9 +14,9 @@ export default class SendRowErrorMessage extends Component { }; render () { - const { errors, errorType } = this.props + const { errors, errorType, customErrors } = this.props - const errorMessage = errors[errorType] + const errorMessage = errors[errorType] || customErrors[errorType] return ( errorMessage diff --git a/ui/app/components/send/send-content/send-row-wrapper/send-row-wrapper.component.js b/ui/app/components/send/send-content/send-row-wrapper/send-row-wrapper.component.js index 04f4f8a15d99..0b496e69b3a3 100644 --- a/ui/app/components/send/send-content/send-row-wrapper/send-row-wrapper.component.js +++ b/ui/app/components/send/send-content/send-row-wrapper/send-row-wrapper.component.js @@ -6,6 +6,7 @@ export default class SendRowWrapper extends Component { static propTypes = { children: PropTypes.node, + errors: PropTypes.object, errorType: PropTypes.string, label: PropTypes.string, showError: PropTypes.bool, @@ -21,6 +22,7 @@ export default class SendRowWrapper extends Component { errorType = '', label, showError = false, + errors, } = this.props const formField = Array.isArray(children) ? children[1] || children[0] : children @@ -29,9 +31,9 @@ export default class SendRowWrapper extends Component { return (
- {label} - {showError && } - {customLabelContent} + {label} + {showError && } + {customLabelContent}
{formField} diff --git a/ui/app/components/wallet-view.js b/ui/app/components/wallet-view.js index 8ad6637ae7ab..a67cab2a2bce 100644 --- a/ui/app/components/wallet-view.js +++ b/ui/app/components/wallet-view.js @@ -142,15 +142,23 @@ WalletView.prototype.render = function () { let type if (keyring) { type = keyring.type - if (type !== 'HD Key Tree') { - if (type.toLowerCase().search('hardware') !== -1) { + switch (type) { + case 'Trezor Hardware': + case 'Ledger Hardware': label = this.context.t('hardware') - } else { + break + case 'Simple Key Pair': label = this.context.t('imported') - } + break + case 'External Account': + label = this.context.t('external') + break + default: + label = '' } } + return h('div.wallet-view.flex-column', { style: {}, className: responsiveDisplayClassname, diff --git a/ui/app/ducks/confirm-transaction.duck.js b/ui/app/ducks/confirm-transaction.duck.js index e228d2d3959d..ceec4f5d14d0 100644 --- a/ui/app/ducks/confirm-transaction.duck.js +++ b/ui/app/ducks/confirm-transaction.duck.js @@ -43,6 +43,7 @@ const UPDATE_NONCE = createActionType('UPDATE_NONCE') const UPDATE_TO_SMART_CONTRACT = createActionType('UPDATE_TO_SMART_CONTRACT') const FETCH_DATA_START = createActionType('FETCH_DATA_START') const FETCH_DATA_END = createActionType('FETCH_DATA_END') +const UPDATE_SIGNATURE = createActionType('UPDATE_SIGNATURE') // Initial state const initState = { @@ -65,6 +66,7 @@ const initState = { nonce: '', toSmartContract: false, fetchingData: false, + signature: '', } // Reducer @@ -162,6 +164,11 @@ export default function reducer ({ confirmTransaction: confirmState = initState } case CLEAR_CONFIRM_TRANSACTION: return initState + case UPDATE_SIGNATURE: + return { + ...confirmState, + signature: action.payload, + } default: return confirmState } @@ -414,3 +421,11 @@ export function clearConfirmTransaction () { type: CLEAR_CONFIRM_TRANSACTION, } } + +export function updateSignature (newSignature) { + return { + type: UPDATE_SIGNATURE, + payload: newSignature, + } +} + diff --git a/ui/app/routes.js b/ui/app/routes.js index 76afed5db392..608d94fa7d18 100644 --- a/ui/app/routes.js +++ b/ui/app/routes.js @@ -11,6 +11,7 @@ const CONFIRM_ADD_SUGGESTED_TOKEN_ROUTE = '/confirm-add-suggested-token' const NEW_ACCOUNT_ROUTE = '/new-account' const IMPORT_ACCOUNT_ROUTE = '/new-account/import' const CONNECT_HARDWARE_ROUTE = '/new-account/connect' +const EXTERNAL_ACCOUNT_ROUTE = '/new-account/external' const SEND_ROUTE = '/send' const NOTICE_ROUTE = '/notice' const WELCOME_ROUTE = '/welcome' @@ -46,6 +47,7 @@ module.exports = { NEW_ACCOUNT_ROUTE, IMPORT_ACCOUNT_ROUTE, CONNECT_HARDWARE_ROUTE, + EXTERNAL_ACCOUNT_ROUTE, SEND_ROUTE, NOTICE_ROUTE, WELCOME_ROUTE,