aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--app/_locales/en/messages.json21
-rw-r--r--app/scripts/metamask-controller.js138
-rw-r--r--package-lock.json72
-rw-r--r--package.json1
-rw-r--r--test/unit/app/controllers/metamask-controller-test.js36
-rw-r--r--ui/app/actions.js29
-rw-r--r--ui/app/components/account-menu/index.js1
-rw-r--r--ui/app/components/modals/account-details-modal.js16
-rw-r--r--ui/app/components/pages/create-account/connect-hardware/account-list.js67
-rw-r--r--ui/app/components/pages/create-account/connect-hardware/connect-screen.js25
-rw-r--r--ui/app/components/pages/create-account/connect-hardware/index.js91
-rw-r--r--ui/app/css/itcss/components/new-account.scss26
-rw-r--r--ui/app/reducers/app.js13
-rw-r--r--ui/app/selectors/confirm-transaction.js2
14 files changed, 374 insertions, 164 deletions
diff --git a/app/_locales/en/messages.json b/app/_locales/en/messages.json
index ddfcf6f12..1614ede81 100644
--- a/app/_locales/en/messages.json
+++ b/app/_locales/en/messages.json
@@ -119,8 +119,8 @@
"close": {
"message": "Close"
},
- "chromeRequiredForTrezor":{
- "message": "You need to use MetaMask on Google Chrome in order to connect to your TREZOR device."
+ "chromeRequiredForHardwareWallets":{
+ "message": "You need to use Metamask on Google Chrome in order to connect to your Hardware Wallet."
},
"confirm": {
"message": "Confirm"
@@ -146,6 +146,9 @@
"connecting": {
"message": "Connecting..."
},
+ "connectToLedger": {
+ "message": "Connect to Ledger"
+ },
"connectToTrezor": {
"message": "Connect to Trezor"
},
@@ -426,11 +429,11 @@
"hardwareWalletConnected": {
"message": "Hardware wallet connected"
},
- "hardwareSupport": {
- "message": "Hardware Support"
+ "hardwareWallets": {
+ "message": "Hardware Wallets"
},
- "hardwareSupportMsg": {
- "message": "You can now view your Hardware accounts in MetaMask! Scroll down and read how it works."
+ "hardwareWalletsMsg": {
+ "message": "You can now view your Hardware wallet accounts in MetaMask! Scroll down and read how it works."
},
"havingTroubleConnecting": {
"message": "Having trouble connecting?"
@@ -538,6 +541,9 @@
"learnMore": {
"message": "Learn more"
},
+ "ledgerAccountRestriction": {
+ "message": "You need to make use your last account before you can add a new one."
+ },
"lessThanMax": {
"message": "must be less than or equal to $1.",
"description": "helper for inputting hex as decimal input"
@@ -922,6 +928,9 @@
"selectAnAccountHelp": {
"message": "These are the accounts available in your hardware wallet. Select the one you’d like to use in MetaMask."
},
+ "selectPathHelp": {
+ "message": "If you don't see your existing Ledger address(es), please try selecting a different HD Path \"Legacy (MEW / MyCrypto)\""
+ },
"sendTokensAnywhere": {
"message": "Send Tokens to anyone with an Ethereum account"
},
diff --git a/app/scripts/metamask-controller.js b/app/scripts/metamask-controller.js
index db323e3fe..1e1aa035f 100644
--- a/app/scripts/metamask-controller.js
+++ b/app/scripts/metamask-controller.js
@@ -49,6 +49,7 @@ const seedPhraseVerifier = require('./lib/seed-phrase-verifier')
const cleanErrorStack = require('./lib/cleanErrorStack')
const log = require('loglevel')
const TrezorKeyring = require('eth-trezor-keyring')
+const LedgerBridgeKeyring = require('eth-ledger-bridge-keyring')
module.exports = class MetamaskController extends EventEmitter {
@@ -127,7 +128,7 @@ module.exports = class MetamaskController extends EventEmitter {
})
// key mgmt
- const additionalKeyrings = [TrezorKeyring]
+ const additionalKeyrings = [TrezorKeyring, LedgerBridgeKeyring]
this.keyringController = new KeyringController({
keyringTypes: additionalKeyrings,
initState: initState.KeyringController,
@@ -377,9 +378,7 @@ module.exports = class MetamaskController extends EventEmitter {
connectHardware: nodeify(this.connectHardware, this),
forgetDevice: nodeify(this.forgetDevice, this),
checkHardwareStatus: nodeify(this.checkHardwareStatus, this),
-
- // TREZOR
- unlockTrezorAccount: nodeify(this.unlockTrezorAccount, this),
+ unlockHardwareWalletAccount: nodeify(this.unlockHardwareWalletAccount, this),
// vault management
submitPassword: nodeify(this.submitPassword, this),
@@ -540,45 +539,57 @@ module.exports = class MetamaskController extends EventEmitter {
// Hardware
//
+ async getKeyringForDevice (deviceName, hdPath = null) {
+ let keyringName = null
+ switch (deviceName) {
+ case 'trezor':
+ keyringName = TrezorKeyring.type
+ break
+ case 'ledger':
+ keyringName = LedgerBridgeKeyring.type
+ break
+ default:
+ throw new Error('MetamaskController:getKeyringForDevice - Unknown device')
+ }
+ let keyring = await this.keyringController.getKeyringsByType(keyringName)[0]
+ if (!keyring) {
+ keyring = await this.keyringController.addNewKeyring(keyringName)
+ }
+ if (hdPath && keyring.setHdPath) {
+ keyring.setHdPath(hdPath)
+ }
+
+ keyring.network = this.networkController.getProviderConfig().type
+
+ return keyring
+
+ }
+
/**
* Fetch account list from a trezor device.
*
* @returns [] accounts
*/
- async connectHardware (deviceName, page) {
-
- switch (deviceName) {
- case 'trezor':
- const keyringController = this.keyringController
- const oldAccounts = await keyringController.getAccounts()
- let keyring = await keyringController.getKeyringsByType(
- 'Trezor Hardware'
- )[0]
- if (!keyring) {
- keyring = await this.keyringController.addNewKeyring('Trezor Hardware')
- }
- let accounts = []
-
- switch (page) {
- case -1:
- accounts = await keyring.getPreviousPage()
- break
- case 1:
- accounts = await keyring.getNextPage()
- break
- default:
- accounts = await keyring.getFirstPage()
- }
-
- // Merge with existing accounts
- // and make sure addresses are not repeated
- const accountsToTrack = [...new Set(oldAccounts.concat(accounts.map(a => a.address.toLowerCase())))]
- this.accountTracker.syncWithAddresses(accountsToTrack)
- return accounts
-
- default:
- throw new Error('MetamaskController:connectHardware - Unknown device')
+ async connectHardware (deviceName, page, hdPath) {
+ const keyring = await this.getKeyringForDevice(deviceName, hdPath)
+ let accounts = []
+ switch (page) {
+ case -1:
+ accounts = await keyring.getPreviousPage()
+ break
+ case 1:
+ accounts = await keyring.getNextPage()
+ break
+ default:
+ accounts = await keyring.getFirstPage()
}
+
+ // Merge with existing accounts
+ // and make sure addresses are not repeated
+ const oldAccounts = await this.keyringController.getAccounts()
+ const accountsToTrack = [...new Set(oldAccounts.concat(accounts.map(a => a.address.toLowerCase())))]
+ this.accountTracker.syncWithAddresses(accountsToTrack)
+ return accounts
}
/**
@@ -586,21 +597,9 @@ module.exports = class MetamaskController extends EventEmitter {
*
* @returns {Promise<boolean>}
*/
- async checkHardwareStatus (deviceName) {
-
- switch (deviceName) {
- case 'trezor':
- const keyringController = this.keyringController
- const keyring = await keyringController.getKeyringsByType(
- 'Trezor Hardware'
- )[0]
- if (!keyring) {
- return false
- }
- return keyring.isUnlocked()
- default:
- throw new Error('MetamaskController:checkHardwareStatus - Unknown device')
- }
+ async checkHardwareStatus (deviceName, hdPath) {
+ const keyring = await this.getKeyringForDevice(deviceName, hdPath)
+ return keyring.isUnlocked()
}
/**
@@ -610,20 +609,9 @@ module.exports = class MetamaskController extends EventEmitter {
*/
async forgetDevice (deviceName) {
- switch (deviceName) {
- case 'trezor':
- const keyringController = this.keyringController
- const keyring = await keyringController.getKeyringsByType(
- 'Trezor Hardware'
- )[0]
- if (!keyring) {
- throw new Error('MetamaskController:forgetDevice - Trezor Hardware keyring not found')
- }
- keyring.forgetDevice()
- return true
- default:
- throw new Error('MetamaskController:forgetDevice - Unknown device')
- }
+ const keyring = await this.getKeyringForDevice(deviceName)
+ keyring.forgetDevice()
+ return true
}
/**
@@ -631,23 +619,17 @@ module.exports = class MetamaskController extends EventEmitter {
*
* @returns {} keyState
*/
- async unlockTrezorAccount (index) {
- const keyringController = this.keyringController
- const keyring = await keyringController.getKeyringsByType(
- 'Trezor Hardware'
- )[0]
- if (!keyring) {
- throw new Error('MetamaskController - No Trezor Hardware Keyring found')
- }
+ async unlockHardwareWalletAccount (index, deviceName, hdPath) {
+ const keyring = await this.getKeyringForDevice(deviceName, hdPath)
keyring.setAccountToUnlock(index)
- const oldAccounts = await keyringController.getAccounts()
- const keyState = await keyringController.addNewAccount(keyring)
- const newAccounts = await keyringController.getAccounts()
+ const oldAccounts = await this.keyringController.getAccounts()
+ const keyState = await this.keyringController.addNewAccount(keyring)
+ const newAccounts = await this.keyringController.getAccounts()
this.preferencesController.setAddresses(newAccounts)
newAccounts.forEach(address => {
if (!oldAccounts.includes(address)) {
- this.preferencesController.setAccountLabel(address, `TREZOR #${parseInt(index, 10) + 1}`)
+ this.preferencesController.setAccountLabel(address, `${deviceName.toUpperCase()} ${parseInt(index, 10) + 1}`)
this.preferencesController.setSelectedAddress(address)
}
})
diff --git a/package-lock.json b/package-lock.json
index 6bee47cd6..4c45357fe 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -8435,6 +8435,62 @@
}
}
},
+ "eth-ledger-bridge-keyring": {
+ "version": "github:MetaMask/eth-ledger-bridge-keyring#d882deaae4c2ab0b83c3fac495f1972c47a1c8cd",
+ "from": "github:MetaMask/eth-ledger-bridge-keyring#master",
+ "requires": {
+ "eth-sig-util": "^1.4.2",
+ "ethereumjs-tx": "^1.3.4",
+ "ethereumjs-util": "^5.1.5",
+ "events": "^2.0.0",
+ "hdkey": "0.8.0"
+ },
+ "dependencies": {
+ "ethereum-common": {
+ "version": "0.0.18",
+ "resolved": "https://registry.npmjs.org/ethereum-common/-/ethereum-common-0.0.18.tgz",
+ "integrity": "sha1-L9w1dvIykDNYl26znaeDIT/5Uj8="
+ },
+ "ethereumjs-tx": {
+ "version": "1.3.7",
+ "resolved": "https://registry.npmjs.org/ethereumjs-tx/-/ethereumjs-tx-1.3.7.tgz",
+ "integrity": "sha512-wvLMxzt1RPhAQ9Yi3/HKZTn0FZYpnsmQdbKYfUUpi4j1SEIcbkd9tndVjcPrufY3V7j2IebOpC00Zp2P/Ay2kA==",
+ "requires": {
+ "ethereum-common": "^0.0.18",
+ "ethereumjs-util": "^5.0.0"
+ }
+ },
+ "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"
+ }
+ },
+ "events": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/events/-/events-2.1.0.tgz",
+ "integrity": "sha512-3Zmiobend8P9DjmKAty0Era4jV8oJ0yGYe2nJJAxgymF9+N8F2m0hhZiMoWtcfepExzNKZumFU3ksdQbInGWCg=="
+ },
+ "hdkey": {
+ "version": "0.8.0",
+ "resolved": "https://registry.npmjs.org/hdkey/-/hdkey-0.8.0.tgz",
+ "integrity": "sha512-oYsdlK22eobT68N5faWI3776f6tOLyqxLLYwxMx+TP0rkWzuCs0oiOm2VbLWcxdpHFP4LtiRR8udaIX8VkEaZQ==",
+ "requires": {
+ "coinstring": "^2.0.0",
+ "safe-buffer": "^5.1.1",
+ "secp256k1": "^3.0.1"
+ }
+ }
+ }
+ },
"eth-lib": {
"version": "0.1.27",
"resolved": "https://registry.npmjs.org/eth-lib/-/eth-lib-0.1.27.tgz",
@@ -8574,13 +8630,12 @@
"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#00ba8463a7f7a67fcad737ff9c2ebd95643427f7",
"ethereumjs-util": "^5.1.1"
},
"dependencies": {
"ethereumjs-abi": {
"version": "git+https://github.com/ethereumjs/ethereumjs-abi.git#00ba8463a7f7a67fcad737ff9c2ebd95643427f7",
- "from": "git+https://github.com/ethereumjs/ethereumjs-abi.git",
+ "from": "git+https://github.com/ethereumjs/ethereumjs-abi.git#00ba8463a7f7a67fcad737ff9c2ebd95643427f7",
"requires": {
"bn.js": "^4.10.0",
"ethereumjs-util": "^5.0.0"
@@ -30440,7 +30495,6 @@
"version": "3.1.5",
"resolved": "https://registry.npmjs.org/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz",
"integrity": "sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q==",
- "dev": true,
"requires": {
"is-typedarray": "^1.0.0"
}
@@ -31445,7 +31499,6 @@
"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": "*",
@@ -31454,7 +31507,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"
+ "from": "git+https://github.com/frozeman/bignumber.js-nolookahead.git#57692b3ecfc98bbdd6b3a516cb2353652ea49934"
}
}
},
@@ -31953,8 +32006,7 @@
"dev": true,
"requires": {
"underscore": "1.8.3",
- "web3-core-helpers": "1.0.0-beta.34",
- "websocket": "git://github.com/frozeman/WebSocket-Node.git#6c72925e3f8aaaea8dc8450f97627e85263999f2"
+ "web3-core-helpers": "1.0.0-beta.34"
},
"dependencies": {
"underscore": {
@@ -31965,8 +32017,7 @@
},
"websocket": {
"version": "git://github.com/frozeman/WebSocket-Node.git#6c72925e3f8aaaea8dc8450f97627e85263999f2",
- "from": "git://github.com/frozeman/WebSocket-Node.git#browserifyCompatible",
- "dev": true,
+ "from": "git://github.com/frozeman/WebSocket-Node.git#6c72925e3f8aaaea8dc8450f97627e85263999f2",
"requires": {
"debug": "^2.2.0",
"nan": "^2.3.3",
@@ -33316,8 +33367,7 @@
"yaeti": {
"version": "0.0.6",
"resolved": "https://registry.npmjs.org/yaeti/-/yaeti-0.0.6.tgz",
- "integrity": "sha1-8m9ITXJoTPQr7ft2lwqhYI+/lXc=",
- "dev": true
+ "integrity": "sha1-8m9ITXJoTPQr7ft2lwqhYI+/lXc="
},
"yallist": {
"version": "2.1.2",
diff --git a/package.json b/package.json
index 29295a65b..237868dcd 100644
--- a/package.json
+++ b/package.json
@@ -110,6 +110,7 @@
"eth-hd-keyring": "^1.2.2",
"eth-json-rpc-filters": "^1.2.6",
"eth-json-rpc-infura": "^3.0.0",
+ "eth-ledger-bridge-keyring": "github:MetaMask/eth-ledger-bridge-keyring#master",
"eth-method-registry": "^1.0.0",
"eth-phishing-detect": "^1.1.4",
"eth-query": "^2.1.2",
diff --git a/test/unit/app/controllers/metamask-controller-test.js b/test/unit/app/controllers/metamask-controller-test.js
index 9164fe246..9f25cf376 100644
--- a/test/unit/app/controllers/metamask-controller-test.js
+++ b/test/unit/app/controllers/metamask-controller-test.js
@@ -226,9 +226,9 @@ describe('MetaMaskController', function () {
it('should throw if it receives an unknown device name', async function () {
try {
- await metamaskController.connectHardware('Some random device name', 0)
+ await metamaskController.connectHardware('Some random device name', 0, `m/44/0'/0'`)
} catch (e) {
- assert.equal(e, 'Error: MetamaskController:connectHardware - Unknown device')
+ assert.equal(e, 'Error: MetamaskController:getKeyringForDevice - Unknown device')
}
})
@@ -242,14 +242,24 @@ describe('MetaMaskController', function () {
assert.equal(keyrings.length, 1)
})
+ it('should add the Ledger Hardware keyring', async function () {
+ sinon.spy(metamaskController.keyringController, 'addNewKeyring')
+ await metamaskController.connectHardware('ledger', 0).catch((e) => null)
+ const keyrings = await metamaskController.keyringController.getKeyringsByType(
+ 'Ledger Hardware'
+ )
+ assert.equal(metamaskController.keyringController.addNewKeyring.getCall(0).args, 'Ledger Hardware')
+ assert.equal(keyrings.length, 1)
+ })
+
})
describe('checkHardwareStatus', function () {
it('should throw if it receives an unknown device name', async function () {
try {
- await metamaskController.checkHardwareStatus('Some random device name')
+ await metamaskController.checkHardwareStatus('Some random device name', `m/44/0'/0'`)
} catch (e) {
- assert.equal(e, 'Error: MetamaskController:checkHardwareStatus - Unknown device')
+ assert.equal(e, 'Error: MetamaskController:getKeyringForDevice - Unknown device')
}
})
@@ -265,7 +275,7 @@ describe('MetaMaskController', function () {
try {
await metamaskController.forgetDevice('Some random device name')
} catch (e) {
- assert.equal(e, 'Error: MetamaskController:forgetDevice - Unknown device')
+ assert.equal(e, 'Error: MetamaskController:getKeyringForDevice - Unknown device')
}
})
@@ -282,7 +292,7 @@ describe('MetaMaskController', function () {
})
})
- describe('unlockTrezorAccount', function () {
+ describe('unlockHardwareWalletAccount', function () {
let accountToUnlock
let windowOpenStub
let addNewAccountStub
@@ -305,16 +315,20 @@ describe('MetaMaskController', function () {
sinon.spy(metamaskController.preferencesController, 'setAddresses')
sinon.spy(metamaskController.preferencesController, 'setSelectedAddress')
sinon.spy(metamaskController.preferencesController, 'setAccountLabel')
- await metamaskController.connectHardware('trezor', 0).catch((e) => null)
- await metamaskController.unlockTrezorAccount(accountToUnlock).catch((e) => null)
+ await metamaskController.connectHardware('trezor', 0, `m/44/0'/0'`).catch((e) => null)
+ await metamaskController.unlockHardwareWalletAccount(accountToUnlock, 'trezor', `m/44/0'/0'`)
})
afterEach(function () {
- metamaskController.keyringController.addNewAccount.restore()
window.open.restore()
+ metamaskController.keyringController.addNewAccount.restore()
+ metamaskController.keyringController.getAccounts.restore()
+ metamaskController.preferencesController.setAddresses.restore()
+ metamaskController.preferencesController.setSelectedAddress.restore()
+ metamaskController.preferencesController.setAccountLabel.restore()
})
- it('should set accountToUnlock in the keyring', async function () {
+ it('should set unlockedAccount in the keyring', async function () {
const keyrings = await metamaskController.keyringController.getKeyringsByType(
'Trezor Hardware'
)
@@ -322,7 +336,7 @@ describe('MetaMaskController', function () {
})
- it('should call keyringController.addNewAccount', async function () {
+ it('should call keyringController.addNewAccount', async function () {
assert(metamaskController.keyringController.addNewAccount.calledOnce)
})
diff --git a/ui/app/actions.js b/ui/app/actions.js
index bd5d25327..6bcc64e17 100644
--- a/ui/app/actions.js
+++ b/ui/app/actions.js
@@ -91,7 +91,7 @@ var actions = {
connectHardware,
checkHardwareStatus,
forgetDevice,
- unlockTrezorAccount,
+ unlockHardwareWalletAccount,
NEW_ACCOUNT_SCREEN: 'NEW_ACCOUNT_SCREEN',
navigateToNewAccountScreen,
resetAccount,
@@ -235,6 +235,8 @@ var actions = {
UPDATE_TOKENS: 'UPDATE_TOKENS',
setRpcTarget: setRpcTarget,
setProviderType: setProviderType,
+ SET_HARDWARE_WALLET_DEFAULT_HD_PATH: 'SET_HARDWARE_WALLET_DEFAULT_HD_PATH',
+ setHardwareWalletDefaultHdPath,
updateProviderType,
// loading overlay
SHOW_LOADING: 'SHOW_LOADING_INDICATION',
@@ -639,12 +641,12 @@ function addNewAccount () {
}
}
-function checkHardwareStatus (deviceName) {
- log.debug(`background.checkHardwareStatus`, deviceName)
+function checkHardwareStatus (deviceName, hdPath) {
+ log.debug(`background.checkHardwareStatus`, deviceName, hdPath)
return (dispatch, getState) => {
dispatch(actions.showLoadingIndication())
return new Promise((resolve, reject) => {
- background.checkHardwareStatus(deviceName, (err, unlocked) => {
+ background.checkHardwareStatus(deviceName, hdPath, (err, unlocked) => {
if (err) {
log.error(err)
dispatch(actions.displayWarning(err.message))
@@ -681,12 +683,12 @@ function forgetDevice (deviceName) {
}
}
-function connectHardware (deviceName, page) {
- log.debug(`background.connectHardware`, deviceName, page)
+function connectHardware (deviceName, page, hdPath) {
+ log.debug(`background.connectHardware`, deviceName, page, hdPath)
return (dispatch, getState) => {
dispatch(actions.showLoadingIndication())
return new Promise((resolve, reject) => {
- background.connectHardware(deviceName, page, (err, accounts) => {
+ background.connectHardware(deviceName, page, hdPath, (err, accounts) => {
if (err) {
log.error(err)
dispatch(actions.displayWarning(err.message))
@@ -702,12 +704,12 @@ function connectHardware (deviceName, page) {
}
}
-function unlockTrezorAccount (index) {
- log.debug(`background.unlockTrezorAccount`, index)
+function unlockHardwareWalletAccount (index, deviceName, hdPath) {
+ log.debug(`background.unlockHardwareWalletAccount`, index, deviceName, hdPath)
return (dispatch, getState) => {
dispatch(actions.showLoadingIndication())
return new Promise((resolve, reject) => {
- background.unlockTrezorAccount(index, (err, accounts) => {
+ background.unlockHardwareWalletAccount(index, deviceName, hdPath, (err, accounts) => {
if (err) {
log.error(err)
dispatch(actions.displayWarning(err.message))
@@ -1854,6 +1856,13 @@ function showLoadingIndication (message) {
}
}
+function setHardwareWalletDefaultHdPath ({ device, path }) {
+ return {
+ type: actions.SET_HARDWARE_WALLET_DEFAULT_HD_PATH,
+ value: {device, path},
+ }
+}
+
function hideLoadingIndication () {
return {
type: actions.HIDE_LOADING,
diff --git a/ui/app/components/account-menu/index.js b/ui/app/components/account-menu/index.js
index 9c063d31e..bcada41e3 100644
--- a/ui/app/components/account-menu/index.js
+++ b/ui/app/components/account-menu/index.js
@@ -229,6 +229,7 @@ AccountMenu.prototype.renderKeyringType = function (keyring) {
let label
switch (type) {
case 'Trezor Hardware':
+ case 'Ledger Hardware':
label = this.context.t('hardware')
break
case 'Simple Key Pair':
diff --git a/ui/app/components/modals/account-details-modal.js b/ui/app/components/modals/account-details-modal.js
index 5607cf051..cc90cf578 100644
--- a/ui/app/components/modals/account-details-modal.js
+++ b/ui/app/components/modals/account-details-modal.js
@@ -14,6 +14,7 @@ function mapStateToProps (state) {
return {
network: state.metamask.network,
selectedIdentity: getSelectedIdentity(state),
+ keyrings: state.metamask.keyrings,
}
}
@@ -50,9 +51,20 @@ AccountDetailsModal.prototype.render = function () {
network,
showExportPrivateKeyModal,
setAccountLabel,
+ keyrings,
} = this.props
const { name, address } = selectedIdentity
+ const keyring = keyrings.find((kr) => {
+ return kr.accounts.includes(address)
+ })
+
+ let exportPrivateKeyFeatureEnabled = true
+ // This feature is disabled for hardware wallets
+ if (keyring.type.search('Hardware') !== -1) {
+ exportPrivateKeyFeatureEnabled = false
+ }
+
return h(AccountModalContainer, {}, [
h(EditableLabel, {
className: 'account-modal__name',
@@ -73,9 +85,9 @@ AccountDetailsModal.prototype.render = function () {
}, this.context.t('etherscanView')),
// Holding on redesign for Export Private Key functionality
- h('button.btn-primary.account-modal__button', {
+ exportPrivateKeyFeatureEnabled ? h('button.btn-primary.account-modal__button', {
onClick: () => showExportPrivateKeyModal(),
- }, this.context.t('exportPrivateKey')),
+ }, this.context.t('exportPrivateKey')) : null,
])
}
diff --git a/ui/app/components/pages/create-account/connect-hardware/account-list.js b/ui/app/components/pages/create-account/connect-hardware/account-list.js
index c722d1f55..0acaded6b 100644
--- a/ui/app/components/pages/create-account/connect-hardware/account-list.js
+++ b/ui/app/components/pages/create-account/connect-hardware/account-list.js
@@ -2,16 +2,69 @@ const { Component } = require('react')
const PropTypes = require('prop-types')
const h = require('react-hyperscript')
const genAccountLink = require('../../../../../lib/account-link.js')
+const Select = require('react-select').default
class AccountList extends Component {
constructor (props, context) {
super(props)
}
+ getHdPaths () {
+ return [
+ {
+ label: `Ledger Live`,
+ value: `m/44'/60'/0'/0/0`,
+ },
+ {
+ label: `Legacy (MEW / MyCrypto)`,
+ value: `m/44'/60'/0'`,
+ },
+ ]
+ }
+
+ goToNextPage = () => {
+ // If we have < 5 accounts, it's restricted by BIP-44
+ if (this.props.accounts.length === 5) {
+ this.props.getPage(this.props.device, 1, this.props.selectedPath)
+ } else {
+ this.props.onAccountRestriction()
+ }
+ }
+
+ goToPreviousPage = () => {
+ this.props.getPage(this.props.device, -1, this.props.selectedPath)
+ }
+
+ renderHdPathSelector () {
+ const { onPathChange, selectedPath } = this.props
+
+ const options = this.getHdPaths()
+ return h('div', [
+ h('div.hw-connect__hdPath', [
+ h('h3.hw-connect__hdPath__title', {}, `HD Path`),
+ h(Select, {
+ className: 'hw-connect__hdPath__select',
+ name: 'hd-path-select',
+ clearable: false,
+ value: selectedPath,
+ options,
+ onChange: (opt) => {
+ onPathChange(opt.value)
+ },
+ }),
+ ]),
+ h('p.hw-connect__msg', {}, this.context.t('selectPathHelp')),
+ ])
+ }
renderHeader () {
+ const { device } = this.props
return (
h('div.hw-connect', [
- h('h3.hw-connect__title', {}, this.context.t('selectAnAccount')),
+
+ h('h3.hw-connect__title', {}, `${device.toUpperCase()} - ${this.context.t('selectAnAccount')}`),
+
+ device.toLowerCase() === 'ledger' ? this.renderHdPathSelector() : null,
+
h('p.hw-connect__msg', {}, this.context.t('selectAnAccountHelp')),
])
)
@@ -61,7 +114,7 @@ class AccountList extends Component {
h(
'button.hw-list-pagination__button',
{
- onClick: () => this.props.getPage(-1),
+ onClick: this.goToPreviousPage,
},
`< ${this.context.t('prev')}`
),
@@ -69,7 +122,7 @@ class AccountList extends Component {
h(
'button.hw-list-pagination__button',
{
- onClick: () => this.props.getPage(1),
+ onClick: this.goToNextPage,
},
`${this.context.t('next')} >`
),
@@ -95,7 +148,7 @@ class AccountList extends Component {
h(
`button.btn-primary.btn--large.new-account-connect-form__button.unlock ${disabled ? '.btn-primary--disabled' : ''}`,
{
- onClick: this.props.onUnlockAccount.bind(this),
+ onClick: this.props.onUnlockAccount.bind(this, this.props.device),
...buttonProps,
},
[this.context.t('unlock')]
@@ -106,7 +159,7 @@ class AccountList extends Component {
renderForgetDevice () {
return h('div.hw-forget-device-container', {}, [
h('a', {
- onClick: this.props.onForgetDevice.bind(this),
+ onClick: this.props.onForgetDevice.bind(this, this.props.device),
}, this.context.t('forgetDevice')),
])
}
@@ -125,6 +178,9 @@ class AccountList extends Component {
AccountList.propTypes = {
+ onPathChange: PropTypes.func.isRequired,
+ selectedPath: PropTypes.string.isRequired,
+ device: PropTypes.string.isRequired,
accounts: PropTypes.array.isRequired,
onAccountChange: PropTypes.func.isRequired,
onForgetDevice: PropTypes.func.isRequired,
@@ -134,6 +190,7 @@ AccountList.propTypes = {
history: PropTypes.object,
onUnlockAccount: PropTypes.func,
onCancel: PropTypes.func,
+ onAccountRestriction: PropTypes.func,
}
AccountList.contextTypes = {
diff --git a/ui/app/components/pages/create-account/connect-hardware/connect-screen.js b/ui/app/components/pages/create-account/connect-hardware/connect-screen.js
index cb2b86595..0a62f1c1e 100644
--- a/ui/app/components/pages/create-account/connect-hardware/connect-screen.js
+++ b/ui/app/components/pages/create-account/connect-hardware/connect-screen.js
@@ -12,7 +12,7 @@ class ConnectScreen extends Component {
h('div.new-account-connect-form.unsupported-browser', {}, [
h('div.hw-connect', [
h('h3.hw-connect__title', {}, this.context.t('browserNotSupported')),
- h('p.hw-connect__msg', {}, this.context.t('chromeRequiredForTrezor')),
+ h('p.hw-connect__msg', {}, this.context.t('chromeRequiredForHardwareWallets')),
]),
h(
'button.btn-primary.btn--large',
@@ -30,8 +30,8 @@ class ConnectScreen extends Component {
renderHeader () {
return (
h('div.hw-connect__header', {}, [
- h('h3.hw-connect__header__title', {}, this.context.t(`hardwareSupport`)),
- h('p.hw-connect__header__msg', {}, this.context.t(`hardwareSupportMsg`)),
+ h('h3.hw-connect__header__title', {}, this.context.t(`hardwareWallets`)),
+ h('p.hw-connect__header__msg', {}, this.context.t(`hardwareWalletsMsg`)),
])
)
}
@@ -49,8 +49,16 @@ class ConnectScreen extends Component {
renderConnectToTrezorButton () {
return h(
'button.btn-primary.btn--large',
- { onClick: this.props.connectToTrezor.bind(this) },
- this.props.btnText
+ { onClick: this.props.connectToHardwareWallet.bind(this, 'trezor') },
+ this.context.t('connectToTrezor')
+ )
+ }
+
+ renderConnectToLedgerButton () {
+ return h(
+ 'button.btn-primary.btn--large',
+ { onClick: this.props.connectToHardwareWallet.bind(this, 'ledger') },
+ this.context.t('connectToLedger')
)
}
@@ -103,6 +111,7 @@ class ConnectScreen extends Component {
h('div.hw-connect__footer', {}, [
h('h3.hw-connect__footer__title', {}, this.context.t(`readyToConnect`)),
this.renderConnectToTrezorButton(),
+ this.renderConnectToLedgerButton(),
h('p.hw-connect__footer__msg', {}, [
this.context.t(`havingTroubleConnecting`),
h('a.hw-connect__footer__link', {
@@ -118,8 +127,9 @@ class ConnectScreen extends Component {
return (
h('div.new-account-connect-form', {}, [
this.renderHeader(),
- this.renderTrezorAffiliateLink(),
+ this.renderConnectToLedgerButton(),
this.renderConnectToTrezorButton(),
+ this.renderTrezorAffiliateLink(),
this.renderLearnMore(),
this.renderTutorialSteps(),
this.renderFooter(),
@@ -136,8 +146,7 @@ class ConnectScreen extends Component {
}
ConnectScreen.propTypes = {
- connectToTrezor: PropTypes.func.isRequired,
- btnText: PropTypes.string.isRequired,
+ connectToHardwareWallet: PropTypes.func.isRequired,
browserSupported: PropTypes.bool.isRequired,
}
diff --git a/ui/app/components/pages/create-account/connect-hardware/index.js b/ui/app/components/pages/create-account/connect-hardware/index.js
index 3f66e7098..547df5223 100644
--- a/ui/app/components/pages/create-account/connect-hardware/index.js
+++ b/ui/app/components/pages/create-account/connect-hardware/index.js
@@ -7,17 +7,19 @@ const ConnectScreen = require('./connect-screen')
const AccountList = require('./account-list')
const { DEFAULT_ROUTE } = require('../../../../routes')
const { formatBalance } = require('../../../../util')
+const { getPlatform } = require('../../../../../../app/scripts/lib/util')
+const { PLATFORM_FIREFOX } = require('../../../../../../app/scripts/lib/enums')
class ConnectHardwareForm extends Component {
constructor (props, context) {
super(props)
this.state = {
error: null,
- btnText: context.t('connectToTrezor'),
selectedAccount: null,
accounts: [],
browserSupported: true,
unlocked: false,
+ device: null,
}
}
@@ -38,25 +40,44 @@ class ConnectHardwareForm extends Component {
}
async checkIfUnlocked () {
- const unlocked = await this.props.checkHardwareStatus('trezor')
- if (unlocked) {
- this.setState({unlocked: true})
- this.getPage(0)
- }
+ ['trezor', 'ledger'].forEach(async device => {
+ const unlocked = await this.props.checkHardwareStatus(device, this.props.defaultHdPaths[device])
+ if (unlocked) {
+ this.setState({unlocked: true})
+ this.getPage(device, 0, this.props.defaultHdPaths[device])
+ }
+ })
}
- connectToTrezor = () => {
+ connectToHardwareWallet = (device) => {
+ // None of the hardware wallets are supported
+ // At least for now
+ if (getPlatform() === PLATFORM_FIREFOX) {
+ this.setState({ browserSupported: false, error: null})
+ return null
+ }
+
if (this.state.accounts.length) {
return null
}
- this.setState({ btnText: this.context.t('connecting')})
- this.getPage(0)
+
+ // Default values
+ this.getPage(device, 0, this.props.defaultHdPaths[device])
+ }
+
+ onPathChange = (path) => {
+ this.props.setHardwareWalletDefaultHdPath({device: this.state.device, path})
+ this.getPage(this.state.device, 0, path)
}
onAccountChange = (account) => {
this.setState({selectedAccount: account.toString(), error: null})
}
+ onAccountRestriction = () => {
+ this.setState({error: this.context.t('ledgerAccountRestriction') })
+ }
+
showTemporaryAlert () {
this.props.showAlert(this.context.t('hardwareWalletConnected'))
// Autohide the alert after 5 seconds
@@ -65,9 +86,9 @@ class ConnectHardwareForm extends Component {
}, 5000)
}
- getPage = (page) => {
+ getPage = (device, page, hdPath) => {
this.props
- .connectHardware('trezor', page)
+ .connectHardware(device, page, hdPath)
.then(accounts => {
if (accounts.length) {
@@ -77,7 +98,7 @@ class ConnectHardwareForm extends Component {
this.showTemporaryAlert()
}
- const newState = { unlocked: true }
+ const newState = { unlocked: true, device, error: null }
// Default to the first account
if (this.state.selectedAccount === null) {
accounts.forEach((a, i) => {
@@ -104,18 +125,18 @@ class ConnectHardwareForm extends Component {
})
.catch(e => {
if (e === 'Window blocked') {
- this.setState({ browserSupported: false })
+ this.setState({ browserSupported: false, error: null})
+ } else if (e !== 'Window closed') {
+ this.setState({ error: e.toString() })
}
- this.setState({ btnText: this.context.t('connectToTrezor') })
})
}
- onForgetDevice = () => {
- this.props.forgetDevice('trezor')
+ onForgetDevice = (device) => {
+ this.props.forgetDevice(device)
.then(_ => {
this.setState({
error: null,
- btnText: this.context.t('connectToTrezor'),
selectedAccount: null,
accounts: [],
unlocked: false,
@@ -125,13 +146,13 @@ class ConnectHardwareForm extends Component {
})
}
- onUnlockAccount = () => {
+ onUnlockAccount = (device) => {
if (this.state.selectedAccount === null) {
this.setState({ error: this.context.t('accountSelectionRequired') })
}
- this.props.unlockTrezorAccount(this.state.selectedAccount)
+ this.props.unlockHardwareWalletAccount(this.state.selectedAccount, device)
.then(_ => {
this.props.history.push(DEFAULT_ROUTE)
}).catch(e => {
@@ -145,20 +166,22 @@ class ConnectHardwareForm extends Component {
renderError () {
return this.state.error
- ? h('span.error', { style: { marginBottom: 40 } }, this.state.error)
+ ? h('span.error', { style: { margin: '20px 20px 10px', display: 'block', textAlign: 'center' } }, this.state.error)
: null
}
renderContent () {
if (!this.state.accounts.length) {
return h(ConnectScreen, {
- connectToTrezor: this.connectToTrezor,
- btnText: this.state.btnText,
+ connectToHardwareWallet: this.connectToHardwareWallet,
browserSupported: this.state.browserSupported,
})
}
return h(AccountList, {
+ onPathChange: this.onPathChange,
+ selectedPath: this.props.defaultHdPaths[this.state.device],
+ device: this.state.device,
accounts: this.state.accounts,
selectedAccount: this.state.selectedAccount,
onAccountChange: this.onAccountChange,
@@ -168,6 +191,7 @@ class ConnectHardwareForm extends Component {
onUnlockAccount: this.onUnlockAccount,
onForgetDevice: this.onForgetDevice,
onCancel: this.onCancel,
+ onAccountRestriction: this.onAccountRestriction,
})
}
@@ -188,13 +212,15 @@ ConnectHardwareForm.propTypes = {
forgetDevice: PropTypes.func,
showAlert: PropTypes.func,
hideAlert: PropTypes.func,
- unlockTrezorAccount: PropTypes.func,
+ unlockHardwareWalletAccount: PropTypes.func,
+ setHardwareWalletDefaultHdPath: PropTypes.func,
numberOfExistingAccounts: PropTypes.number,
history: PropTypes.object,
t: PropTypes.func,
network: PropTypes.string,
accounts: PropTypes.object,
address: PropTypes.string,
+ defaultHdPaths: PropTypes.object,
}
const mapStateToProps = state => {
@@ -202,28 +228,35 @@ const mapStateToProps = state => {
metamask: { network, selectedAddress, identities = {}, accounts = [] },
} = state
const numberOfExistingAccounts = Object.keys(identities).length
+ const {
+ appState: { defaultHdPaths },
+ } = state
return {
network,
accounts,
address: selectedAddress,
numberOfExistingAccounts,
+ defaultHdPaths,
}
}
const mapDispatchToProps = dispatch => {
return {
- connectHardware: (deviceName, page) => {
- return dispatch(actions.connectHardware(deviceName, page))
+ setHardwareWalletDefaultHdPath: ({device, path}) => {
+ return dispatch(actions.setHardwareWalletDefaultHdPath({device, path}))
+ },
+ connectHardware: (deviceName, page, hdPath) => {
+ return dispatch(actions.connectHardware(deviceName, page, hdPath))
},
- checkHardwareStatus: (deviceName) => {
- return dispatch(actions.checkHardwareStatus(deviceName))
+ checkHardwareStatus: (deviceName, hdPath) => {
+ return dispatch(actions.checkHardwareStatus(deviceName, hdPath))
},
forgetDevice: (deviceName) => {
return dispatch(actions.forgetDevice(deviceName))
},
- unlockTrezorAccount: index => {
- return dispatch(actions.unlockTrezorAccount(index))
+ unlockHardwareWalletAccount: (index, deviceName, hdPath) => {
+ return dispatch(actions.unlockHardwareWalletAccount(index, deviceName, hdPath))
},
showImportPage: () => dispatch(actions.showImportPage()),
showConnectPage: () => dispatch(actions.showConnectPage()),
diff --git a/ui/app/css/itcss/components/new-account.scss b/ui/app/css/itcss/components/new-account.scss
index b12afb124..b9e6ac000 100644
--- a/ui/app/css/itcss/components/new-account.scss
+++ b/ui/app/css/itcss/components/new-account.scss
@@ -174,7 +174,26 @@
font-size: 14px;
color: #9b9b9b;
margin-top: 10px;
- margin-bottom: 0px;
+ margin-bottom: 20px;
+ }
+ }
+
+ &__hdPath {
+ display: flex;
+ flex-direction: row;
+ margin-top: 15px;
+ margin-bottom: 15px;
+ font-size: 14px;
+
+ &__title {
+ display: flex;
+ margin-top: 10px;
+ margin-right: 15px;
+ }
+
+ &__select {
+ display: flex;
+ flex: 1;
}
}
@@ -238,8 +257,8 @@
&__get-trezor {
width: 100%;
- padding-bottom: 20px;
- padding-top: 20px;
+ padding-bottom: 10px;
+ padding-top: 10px;
&__msg {
font-size: 14px;
@@ -412,6 +431,7 @@
min-height: 54px;
font-weight: 300;
font-size: 14px;
+ margin-bottom: 20px
}
&__button.unlock {
diff --git a/ui/app/reducers/app.js b/ui/app/reducers/app.js
index 98d467163..c246e7904 100644
--- a/ui/app/reducers/app.js
+++ b/ui/app/reducers/app.js
@@ -67,6 +67,10 @@ function reduceApp (state, action) {
isMouseUser: false,
gasIsLoading: false,
networkNonce: null,
+ defaultHdPaths: {
+ trezor: `m/44'/60'/0'/0`,
+ ledger: `m/44'/60'/0'/0/0`,
+ },
}, state.appState)
switch (action.type) {
@@ -525,6 +529,15 @@ function reduceApp (state, action) {
warning: '',
})
+ case actions.SET_HARDWARE_WALLET_DEFAULT_HD_PATH:
+ const { device, path } = action.value
+ const newDefaults = {...appState.defaultHdPaths}
+ newDefaults[device] = path
+
+ return extend(appState, {
+ defaultHdPaths: newDefaults,
+ })
+
case actions.SHOW_LOADING:
return extend(appState, {
isLoading: true,
diff --git a/ui/app/selectors/confirm-transaction.js b/ui/app/selectors/confirm-transaction.js
index aa1fc5404..6e760c429 100644
--- a/ui/app/selectors/confirm-transaction.js
+++ b/ui/app/selectors/confirm-transaction.js
@@ -159,7 +159,7 @@ export const approveTokenAmountAndToAddressSelector = createSelector(
if (tokenDecimals) {
tokenAmount = calcTokenAmount(value, tokenDecimals)
}
-
+
tokenAmount = roundExponential(tokenAmount)
}