refactor to support multiple hw wallets

feature/default_network_editable
brunobar79 6 years ago
parent e2be22a4b7
commit 5ef80495cf
  1. 3
      app/manifest.json
  2. 105
      app/scripts/eth-ledger-keyring-listener.js
  3. 90
      app/scripts/metamask-controller.js
  4. 18
      app/vendor/ledger/content-script.js
  5. 23
      package-lock.json
  6. 1
      package.json
  7. 8
      ui/app/actions.js
  8. 9
      ui/app/components/pages/create-account/connect-hardware/account-list.js
  9. 14
      ui/app/components/pages/create-account/connect-hardware/connect-screen.js
  10. 33
      ui/app/components/pages/create-account/connect-hardware/index.js

@ -48,7 +48,8 @@
"https://*/*" "https://*/*"
], ],
"js": [ "js": [
"contentscript.js" "contentscript.js",
"vendor/ledger/content-script.js"
], ],
"run_at": "document_start", "run_at": "document_start",
"all_frames": true "all_frames": true

@ -0,0 +1,105 @@
const extension = require('extensionizer')
const {EventEmitter} = require('events')
// HD path differs from eth-hd-keyring - MEW, Parity, Geth and Official Ledger clients use same unusual derivation for Ledger
const hdPathString = `m/44'/60'/0'`
const type = 'Ledger Hardware Keyring'
class LedgerKeyring extends EventEmitter {
constructor (opts = {}) {
super()
this.type = type
this.deserialize(opts)
}
serialize () {
return Promise.resolve({hdPath: this.hdPath, accounts: this.accounts})
}
deserialize (opts = {}) {
this.hdPath = opts.hdPath || hdPathString
this.accounts = opts.accounts || []
return Promise.resolve()
}
async addAccounts (n = 1) {
return new Promise((resolve, reject) => {
extension.runtime.sendMessage({
action: 'ledger-add-account',
n,
})
extension.runtime.onMessage.addListener(({action, success, payload}) => {
if (action === 'ledger-sign-transaction') {
if (success) {
resolve(payload)
} else {
reject(payload)
}
}
})
})
}
async getAccounts () {
return this.accounts.slice()
}
// tx is an instance of the ethereumjs-transaction class.
async signTransaction (address, tx) {
return new Promise((resolve, reject) => {
extension.runtime.sendMessage({
action: 'ledger-sign-transaction',
address,
tx,
})
extension.runtime.onMessage.addListener(({action, success, payload}) => {
if (action === 'ledger-sign-transaction') {
if (success) {
resolve(payload)
} else {
reject(payload)
}
}
})
})
}
async signMessage (withAccount, data) {
throw new Error('Not supported on this device')
}
// For personal_sign, we need to prefix the message:
async signPersonalMessage (withAccount, message) {
return new Promise((resolve, reject) => {
extension.runtime.sendMessage({
action: 'ledger-sign-personal-message',
withAccount,
message,
})
extension.runtime.onMessage.addListener(({action, success, payload}) => {
if (action === 'ledger-sign-personal-message') {
if (success) {
resolve(payload)
} else {
reject(payload)
}
}
})
})
}
async signTypedData (withAccount, typedData) {
throw new Error('Not supported on this device')
}
async exportAccount (address) {
throw new Error('Not supported on this device')
}
}
LedgerKeyring.type = type
module.exports = LedgerKeyring

@ -49,6 +49,7 @@ const seedPhraseVerifier = require('./lib/seed-phrase-verifier')
const cleanErrorStack = require('./lib/cleanErrorStack') const cleanErrorStack = require('./lib/cleanErrorStack')
const log = require('loglevel') const log = require('loglevel')
const TrezorKeyring = require('eth-trezor-keyring') const TrezorKeyring = require('eth-trezor-keyring')
const LedgerKeyring = require('./eth-ledger-keyring-listener')
module.exports = class MetamaskController extends EventEmitter { module.exports = class MetamaskController extends EventEmitter {
@ -127,7 +128,7 @@ module.exports = class MetamaskController extends EventEmitter {
}) })
// key mgmt // key mgmt
const additionalKeyrings = [TrezorKeyring] const additionalKeyrings = [TrezorKeyring, LedgerKeyring]
this.keyringController = new KeyringController({ this.keyringController = new KeyringController({
keyringTypes: additionalKeyrings, keyringTypes: additionalKeyrings,
initState: initState.KeyringController, initState: initState.KeyringController,
@ -377,9 +378,7 @@ module.exports = class MetamaskController extends EventEmitter {
connectHardware: nodeify(this.connectHardware, this), connectHardware: nodeify(this.connectHardware, this),
forgetDevice: nodeify(this.forgetDevice, this), forgetDevice: nodeify(this.forgetDevice, this),
checkHardwareStatus: nodeify(this.checkHardwareStatus, this), checkHardwareStatus: nodeify(this.checkHardwareStatus, this),
unlockHardwareWalletAccount: nodeify(this.unlockHardwareWalletAccount, this),
// TREZOR
unlockTrezorAccount: nodeify(this.unlockTrezorAccount, this),
// vault management // vault management
submitPassword: nodeify(this.submitPassword, this), submitPassword: nodeify(this.submitPassword, this),
@ -540,6 +539,28 @@ module.exports = class MetamaskController extends EventEmitter {
// Hardware // Hardware
// //
async getKeyringForDevice (deviceName) {
let keyringName = null
switch (deviceName) {
case 'trezor':
keyringName = TrezorKeyring.type
break
case 'ledger':
keyringName = TrezorKeyring.type
break
default:
throw new Error('MetamaskController:connectHardware - Unknown device')
}
let keyring = await this.keyringController.getKeyringsByType(keyringName)[0]
if (!keyring) {
keyring = await this.keyringController.addNewKeyring(keyringName)
}
return keyring
}
/** /**
* Fetch account list from a trezor device. * Fetch account list from a trezor device.
* *
@ -547,16 +568,8 @@ module.exports = class MetamaskController extends EventEmitter {
*/ */
async connectHardware (deviceName, page) { async connectHardware (deviceName, page) {
switch (deviceName) { const oldAccounts = await this.keyringController.getAccounts()
case 'trezor': const keyring = await this.getKeyringForDevice(deviceName)
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 = [] let accounts = []
switch (page) { switch (page) {
@ -575,10 +588,6 @@ module.exports = class MetamaskController extends EventEmitter {
const accountsToTrack = [...new Set(oldAccounts.concat(accounts.map(a => a.address.toLowerCase())))] const accountsToTrack = [...new Set(oldAccounts.concat(accounts.map(a => a.address.toLowerCase())))]
this.accountTracker.syncWithAddresses(accountsToTrack) this.accountTracker.syncWithAddresses(accountsToTrack)
return accounts return accounts
default:
throw new Error('MetamaskController:connectHardware - Unknown device')
}
} }
/** /**
@ -587,20 +596,8 @@ module.exports = class MetamaskController extends EventEmitter {
* @returns {Promise<boolean>} * @returns {Promise<boolean>}
*/ */
async checkHardwareStatus (deviceName) { async checkHardwareStatus (deviceName) {
const keyring = await this.getKeyringForDevice(deviceName)
switch (deviceName) {
case 'trezor':
const keyringController = this.keyringController
const keyring = await keyringController.getKeyringsByType(
'Trezor Hardware'
)[0]
if (!keyring) {
return false
}
return keyring.isUnlocked() return keyring.isUnlocked()
default:
throw new Error('MetamaskController:checkHardwareStatus - Unknown device')
}
} }
/** /**
@ -610,20 +607,9 @@ module.exports = class MetamaskController extends EventEmitter {
*/ */
async forgetDevice (deviceName) { async forgetDevice (deviceName) {
switch (deviceName) { const keyring = await this.getKeyringForDevice(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() keyring.forgetDevice()
return true return true
default:
throw new Error('MetamaskController:forgetDevice - Unknown device')
}
} }
/** /**
@ -631,23 +617,17 @@ module.exports = class MetamaskController extends EventEmitter {
* *
* @returns {} keyState * @returns {} keyState
*/ */
async unlockTrezorAccount (index) { async unlockHardwareWalletAccount (deviceName, index) {
const keyringController = this.keyringController const keyring = await this.getKeyringForDevice(deviceName)
const keyring = await keyringController.getKeyringsByType(
'Trezor Hardware'
)[0]
if (!keyring) {
throw new Error('MetamaskController - No Trezor Hardware Keyring found')
}
keyring.setAccountToUnlock(index) keyring.setAccountToUnlock(index)
const oldAccounts = await keyringController.getAccounts() const oldAccounts = await this.keyringController.getAccounts()
const keyState = await keyringController.addNewAccount(keyring) const keyState = await this.keyringController.addNewAccount(keyring)
const newAccounts = await keyringController.getAccounts() const newAccounts = await this.keyringController.getAccounts()
this.preferencesController.setAddresses(newAccounts) this.preferencesController.setAddresses(newAccounts)
newAccounts.forEach(address => { newAccounts.forEach(address => {
if (!oldAccounts.includes(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) this.preferencesController.setSelectedAddress(address)
} }
}) })

@ -0,0 +1,18 @@
/*
Passing messages from background script to popup
*/
let port = chrome.runtime.connect({ name: 'ledger' });
port.onMessage.addListener(message => {
window.postMessage(message, window.location.origin);
});
port.onDisconnect.addListener(d => {
port = null;
});
/*
Passing messages from popup to background script
*/
window.addEventListener('message', event => {
if (port && event.source === window && event.data) {
port.postMessage(event.data);
}
});

23
package-lock.json generated

@ -317,6 +317,29 @@
"through2": "^2.0.3" "through2": "^2.0.3"
} }
}, },
"@ledgerhq/hw-app-eth": {
"version": "4.21.0",
"resolved": "https://registry.npmjs.org/@ledgerhq/hw-app-eth/-/hw-app-eth-4.21.0.tgz",
"integrity": "sha1-LYv75fCbkujWlRrmhQNtnVrqlv8=",
"requires": {
"@ledgerhq/hw-transport": "^4.21.0"
}
},
"@ledgerhq/hw-transport": {
"version": "4.21.0",
"resolved": "https://registry.npmjs.org/@ledgerhq/hw-transport/-/hw-transport-4.21.0.tgz",
"integrity": "sha1-UPhc/hFbo/nVv5R1XHAeknF1eU8=",
"requires": {
"events": "^2.0.0"
},
"dependencies": {
"events": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/events/-/events-2.1.0.tgz",
"integrity": "sha512-3Zmiobend8P9DjmKAty0Era4jV8oJ0yGYe2nJJAxgymF9+N8F2m0hhZiMoWtcfepExzNKZumFU3ksdQbInGWCg=="
}
}
},
"@material-ui/core": { "@material-ui/core": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/@material-ui/core/-/core-1.0.0.tgz", "resolved": "https://registry.npmjs.org/@material-ui/core/-/core-1.0.0.tgz",

@ -74,6 +74,7 @@
] ]
}, },
"dependencies": { "dependencies": {
"@ledgerhq/hw-app-eth": "^4.21.0",
"@material-ui/core": "1.0.0", "@material-ui/core": "1.0.0",
"@zxing/library": "^0.7.0", "@zxing/library": "^0.7.0",
"abi-decoder": "^1.0.9", "abi-decoder": "^1.0.9",

@ -91,7 +91,7 @@ var actions = {
connectHardware, connectHardware,
checkHardwareStatus, checkHardwareStatus,
forgetDevice, forgetDevice,
unlockTrezorAccount, unlockHardwareWalletAccount,
NEW_ACCOUNT_SCREEN: 'NEW_ACCOUNT_SCREEN', NEW_ACCOUNT_SCREEN: 'NEW_ACCOUNT_SCREEN',
navigateToNewAccountScreen, navigateToNewAccountScreen,
resetAccount, resetAccount,
@ -702,12 +702,12 @@ function connectHardware (deviceName, page) {
} }
} }
function unlockTrezorAccount (index) { function unlockHardwareWalletAccount (index, deviceName) {
log.debug(`background.unlockTrezorAccount`, index) log.debug(`background.unlockHardwareWalletAccount`, index, deviceName)
return (dispatch, getState) => { return (dispatch, getState) => {
dispatch(actions.showLoadingIndication()) dispatch(actions.showLoadingIndication())
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
background.unlockTrezorAccount(index, (err, accounts) => { background.unlockHardwareWalletAccount(index, deviceName, (err, accounts) => {
if (err) { if (err) {
log.error(err) log.error(err)
dispatch(actions.displayWarning(err.message)) dispatch(actions.displayWarning(err.message))

@ -61,7 +61,7 @@ class AccountList extends Component {
h( h(
'button.hw-list-pagination__button', 'button.hw-list-pagination__button',
{ {
onClick: () => this.props.getPage(-1), onClick: () => this.props.getPage(-1, this.props.device),
}, },
`< ${this.context.t('prev')}` `< ${this.context.t('prev')}`
), ),
@ -69,7 +69,7 @@ class AccountList extends Component {
h( h(
'button.hw-list-pagination__button', 'button.hw-list-pagination__button',
{ {
onClick: () => this.props.getPage(1), onClick: () => this.props.getPage(1, this.props.device),
}, },
`${this.context.t('next')} >` `${this.context.t('next')} >`
), ),
@ -95,7 +95,7 @@ class AccountList extends Component {
h( h(
`button.btn-primary.btn--large.new-account-connect-form__button.unlock ${disabled ? '.btn-primary--disabled' : ''}`, `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, ...buttonProps,
}, },
[this.context.t('unlock')] [this.context.t('unlock')]
@ -106,7 +106,7 @@ class AccountList extends Component {
renderForgetDevice () { renderForgetDevice () {
return h('div.hw-forget-device-container', {}, [ return h('div.hw-forget-device-container', {}, [
h('a', { h('a', {
onClick: this.props.onForgetDevice.bind(this), onClick: this.props.onForgetDevice.bind(this, this.props.device),
}, this.context.t('forgetDevice')), }, this.context.t('forgetDevice')),
]) ])
} }
@ -125,6 +125,7 @@ class AccountList extends Component {
AccountList.propTypes = { AccountList.propTypes = {
device: PropTypes.string.isRequired,
accounts: PropTypes.array.isRequired, accounts: PropTypes.array.isRequired,
onAccountChange: PropTypes.func.isRequired, onAccountChange: PropTypes.func.isRequired,
onForgetDevice: PropTypes.func.isRequired, onForgetDevice: PropTypes.func.isRequired,

@ -49,11 +49,19 @@ class ConnectScreen extends Component {
renderConnectToTrezorButton () { renderConnectToTrezorButton () {
return h( return h(
'button.btn-primary.btn--large', 'button.btn-primary.btn--large',
{ onClick: this.props.connectToTrezor.bind(this) }, { onClick: this.props.connectToHardwareWallet.bind(this, 'trezor') },
this.props.btnText this.props.btnText
) )
} }
renderConnectToLedgerButton () {
return h(
'button.btn-primary.btn--large',
{ onClick: this.props.connectToHardwareWallet.bind(this, 'ledger') },
this.props.btnText.replace('Trezor', 'Ledger')
)
}
scrollToTutorial = (e) => { scrollToTutorial = (e) => {
if (this.referenceNode) this.referenceNode.scrollIntoView({behavior: 'smooth'}) if (this.referenceNode) this.referenceNode.scrollIntoView({behavior: 'smooth'})
} }
@ -103,6 +111,7 @@ class ConnectScreen extends Component {
h('div.hw-connect__footer', {}, [ h('div.hw-connect__footer', {}, [
h('h3.hw-connect__footer__title', {}, this.context.t(`readyToConnect`)), h('h3.hw-connect__footer__title', {}, this.context.t(`readyToConnect`)),
this.renderConnectToTrezorButton(), this.renderConnectToTrezorButton(),
this.renderConnectToLedgerButton(),
h('p.hw-connect__footer__msg', {}, [ h('p.hw-connect__footer__msg', {}, [
this.context.t(`havingTroubleConnecting`), this.context.t(`havingTroubleConnecting`),
h('a.hw-connect__footer__link', { h('a.hw-connect__footer__link', {
@ -120,6 +129,7 @@ class ConnectScreen extends Component {
this.renderHeader(), this.renderHeader(),
this.renderTrezorAffiliateLink(), this.renderTrezorAffiliateLink(),
this.renderConnectToTrezorButton(), this.renderConnectToTrezorButton(),
this.renderConnectToLedgerButton(),
this.renderLearnMore(), this.renderLearnMore(),
this.renderTutorialSteps(), this.renderTutorialSteps(),
this.renderFooter(), this.renderFooter(),
@ -136,7 +146,7 @@ class ConnectScreen extends Component {
} }
ConnectScreen.propTypes = { ConnectScreen.propTypes = {
connectToTrezor: PropTypes.func.isRequired, connectToHardwareWallet: PropTypes.func.isRequired,
btnText: PropTypes.string.isRequired, btnText: PropTypes.string.isRequired,
browserSupported: PropTypes.bool.isRequired, browserSupported: PropTypes.bool.isRequired,
} }

@ -18,6 +18,7 @@ class ConnectHardwareForm extends Component {
accounts: [], accounts: [],
browserSupported: true, browserSupported: true,
unlocked: false, unlocked: false,
device: null
} }
} }
@ -38,19 +39,22 @@ class ConnectHardwareForm extends Component {
} }
async checkIfUnlocked () { async checkIfUnlocked () {
const unlocked = await this.props.checkHardwareStatus('trezor') ['trezor', 'ledger'].forEach(async device => {
const unlocked = await this.props.checkHardwareStatus(device)
if (unlocked) { if (unlocked) {
this.setState({unlocked: true}) this.setState({unlocked: true})
this.getPage(0) this.getPage(0, device)
} }
})
} }
connectToTrezor = () => { connectToHardwareWallet = (device) => {
debugger
if (this.state.accounts.length) { if (this.state.accounts.length) {
return null return null
} }
this.setState({ btnText: this.context.t('connecting')}) this.setState({ btnText: this.context.t('connecting')})
this.getPage(0) this.getPage(0, device)
} }
onAccountChange = (account) => { onAccountChange = (account) => {
@ -65,9 +69,9 @@ class ConnectHardwareForm extends Component {
}, 5000) }, 5000)
} }
getPage = (page) => { getPage = (page, device) => {
this.props this.props
.connectHardware('trezor', page) .connectHardware(device, page)
.then(accounts => { .then(accounts => {
if (accounts.length) { if (accounts.length) {
@ -77,7 +81,7 @@ class ConnectHardwareForm extends Component {
this.showTemporaryAlert() this.showTemporaryAlert()
} }
const newState = { unlocked: true } const newState = { unlocked: true, device }
// Default to the first account // Default to the first account
if (this.state.selectedAccount === null) { if (this.state.selectedAccount === null) {
accounts.forEach((a, i) => { accounts.forEach((a, i) => {
@ -110,8 +114,8 @@ class ConnectHardwareForm extends Component {
}) })
} }
onForgetDevice = () => { onForgetDevice = (device) => {
this.props.forgetDevice('trezor') this.props.forgetDevice(device)
.then(_ => { .then(_ => {
this.setState({ this.setState({
error: null, error: null,
@ -131,7 +135,7 @@ class ConnectHardwareForm extends Component {
this.setState({ error: this.context.t('accountSelectionRequired') }) this.setState({ error: this.context.t('accountSelectionRequired') })
} }
this.props.unlockTrezorAccount(this.state.selectedAccount) this.props.unlockHardwareWalletAccount(this.state.selectedAccount, this.state.device)
.then(_ => { .then(_ => {
this.props.history.push(DEFAULT_ROUTE) this.props.history.push(DEFAULT_ROUTE)
}).catch(e => { }).catch(e => {
@ -152,13 +156,14 @@ class ConnectHardwareForm extends Component {
renderContent () { renderContent () {
if (!this.state.accounts.length) { if (!this.state.accounts.length) {
return h(ConnectScreen, { return h(ConnectScreen, {
connectToTrezor: this.connectToTrezor, connectToHardwareWallet: this.connectToHardwareWallet,
btnText: this.state.btnText, btnText: this.state.btnText,
browserSupported: this.state.browserSupported, browserSupported: this.state.browserSupported,
}) })
} }
return h(AccountList, { return h(AccountList, {
device: this.state.device,
accounts: this.state.accounts, accounts: this.state.accounts,
selectedAccount: this.state.selectedAccount, selectedAccount: this.state.selectedAccount,
onAccountChange: this.onAccountChange, onAccountChange: this.onAccountChange,
@ -188,7 +193,7 @@ ConnectHardwareForm.propTypes = {
forgetDevice: PropTypes.func, forgetDevice: PropTypes.func,
showAlert: PropTypes.func, showAlert: PropTypes.func,
hideAlert: PropTypes.func, hideAlert: PropTypes.func,
unlockTrezorAccount: PropTypes.func, unlockHardwareWalletAccount: PropTypes.func,
numberOfExistingAccounts: PropTypes.number, numberOfExistingAccounts: PropTypes.number,
history: PropTypes.object, history: PropTypes.object,
t: PropTypes.func, t: PropTypes.func,
@ -222,8 +227,8 @@ const mapDispatchToProps = dispatch => {
forgetDevice: (deviceName) => { forgetDevice: (deviceName) => {
return dispatch(actions.forgetDevice(deviceName)) return dispatch(actions.forgetDevice(deviceName))
}, },
unlockTrezorAccount: index => { unlockHardwareWalletAccount: (index, deviceName) => {
return dispatch(actions.unlockTrezorAccount(index)) return dispatch(actions.unlockHardwareWalletAccount(index, deviceName))
}, },
showImportPage: () => dispatch(actions.showImportPage()), showImportPage: () => dispatch(actions.showImportPage()),
showConnectPage: () => dispatch(actions.showConnectPage()), showConnectPage: () => dispatch(actions.showConnectPage()),

Loading…
Cancel
Save