aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--app/manifest.json3
-rw-r--r--app/scripts/eth-ledger-keyring-listener.js105
-rw-r--r--app/scripts/metamask-controller.js130
-rw-r--r--app/vendor/ledger/content-script.js18
-rw-r--r--package-lock.json23
-rw-r--r--package.json1
-rw-r--r--ui/app/actions.js8
-rw-r--r--ui/app/components/pages/create-account/connect-hardware/account-list.js9
-rw-r--r--ui/app/components/pages/create-account/connect-hardware/connect-screen.js14
-rw-r--r--ui/app/components/pages/create-account/connect-hardware/index.js39
10 files changed, 247 insertions, 103 deletions
diff --git a/app/manifest.json b/app/manifest.json
index 84cedd687..0bdf84ef1 100644
--- a/app/manifest.json
+++ b/app/manifest.json
@@ -48,7 +48,8 @@
"https://*/*"
],
"js": [
- "contentscript.js"
+ "contentscript.js",
+ "vendor/ledger/content-script.js"
],
"run_at": "document_start",
"all_frames": true
diff --git a/app/scripts/eth-ledger-keyring-listener.js b/app/scripts/eth-ledger-keyring-listener.js
new file mode 100644
index 000000000..a6db49772
--- /dev/null
+++ b/app/scripts/eth-ledger-keyring-listener.js
@@ -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
diff --git a/app/scripts/metamask-controller.js b/app/scripts/metamask-controller.js
index db323e3fe..593b722ff 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 LedgerKeyring = require('./eth-ledger-keyring-listener')
module.exports = class MetamaskController extends EventEmitter {
@@ -127,7 +128,7 @@ module.exports = class MetamaskController extends EventEmitter {
})
// key mgmt
- const additionalKeyrings = [TrezorKeyring]
+ const additionalKeyrings = [TrezorKeyring, LedgerKeyring]
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,6 +539,28 @@ module.exports = class MetamaskController extends EventEmitter {
// 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.
*
@@ -547,38 +568,26 @@ module.exports = class MetamaskController extends EventEmitter {
*/
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')
+ const oldAccounts = await this.keyringController.getAccounts()
+ const keyring = await this.getKeyringForDevice(deviceName)
+ 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
}
/**
@@ -587,20 +596,8 @@ 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')
- }
+ const keyring = await this.getKeyringForDevice(deviceName)
+ return keyring.isUnlocked()
}
/**
@@ -610,20 +607,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 +617,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 (deviceName, index) {
+ const keyring = await this.getKeyringForDevice(deviceName)
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/app/vendor/ledger/content-script.js b/app/vendor/ledger/content-script.js
new file mode 100644
index 000000000..425ff07a3
--- /dev/null
+++ b/app/vendor/ledger/content-script.js
@@ -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);
+ }
+}); \ No newline at end of file
diff --git a/package-lock.json b/package-lock.json
index 6bee47cd6..d48415cbc 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -317,6 +317,29 @@
"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": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/@material-ui/core/-/core-1.0.0.tgz",
diff --git a/package.json b/package.json
index 29295a65b..c8a37f564 100644
--- a/package.json
+++ b/package.json
@@ -74,6 +74,7 @@
]
},
"dependencies": {
+ "@ledgerhq/hw-app-eth": "^4.21.0",
"@material-ui/core": "1.0.0",
"@zxing/library": "^0.7.0",
"abi-decoder": "^1.0.9",
diff --git a/ui/app/actions.js b/ui/app/actions.js
index bd5d25327..04af9d7c8 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,
@@ -702,12 +702,12 @@ function connectHardware (deviceName, page) {
}
}
-function unlockTrezorAccount (index) {
- log.debug(`background.unlockTrezorAccount`, index)
+function unlockHardwareWalletAccount (index, deviceName) {
+ log.debug(`background.unlockHardwareWalletAccount`, index, deviceName)
return (dispatch, getState) => {
dispatch(actions.showLoadingIndication())
return new Promise((resolve, reject) => {
- background.unlockTrezorAccount(index, (err, accounts) => {
+ background.unlockHardwareWalletAccount(index, deviceName, (err, accounts) => {
if (err) {
log.error(err)
dispatch(actions.displayWarning(err.message))
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..730f2df34 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
@@ -61,7 +61,7 @@ class AccountList extends Component {
h(
'button.hw-list-pagination__button',
{
- onClick: () => this.props.getPage(-1),
+ onClick: () => this.props.getPage(-1, this.props.device),
},
`< ${this.context.t('prev')}`
),
@@ -69,7 +69,7 @@ class AccountList extends Component {
h(
'button.hw-list-pagination__button',
{
- onClick: () => this.props.getPage(1),
+ onClick: () => this.props.getPage(1, this.props.device),
},
`${this.context.t('next')} >`
),
@@ -95,7 +95,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 +106,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 +125,7 @@ class AccountList extends Component {
AccountList.propTypes = {
+ device: PropTypes.string.isRequired,
accounts: PropTypes.array.isRequired,
onAccountChange: PropTypes.func.isRequired,
onForgetDevice: PropTypes.func.isRequired,
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..af144d410 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
@@ -49,11 +49,19 @@ class ConnectScreen extends Component {
renderConnectToTrezorButton () {
return h(
'button.btn-primary.btn--large',
- { onClick: this.props.connectToTrezor.bind(this) },
+ { onClick: this.props.connectToHardwareWallet.bind(this, 'trezor') },
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) => {
if (this.referenceNode) this.referenceNode.scrollIntoView({behavior: 'smooth'})
}
@@ -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', {
@@ -120,6 +129,7 @@ class ConnectScreen extends Component {
this.renderHeader(),
this.renderTrezorAffiliateLink(),
this.renderConnectToTrezorButton(),
+ this.renderConnectToLedgerButton(),
this.renderLearnMore(),
this.renderTutorialSteps(),
this.renderFooter(),
@@ -136,7 +146,7 @@ class ConnectScreen extends Component {
}
ConnectScreen.propTypes = {
- connectToTrezor: PropTypes.func.isRequired,
+ connectToHardwareWallet: PropTypes.func.isRequired,
btnText: PropTypes.string.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..646ba8bec 100644
--- a/ui/app/components/pages/create-account/connect-hardware/index.js
+++ b/ui/app/components/pages/create-account/connect-hardware/index.js
@@ -18,6 +18,7 @@ class ConnectHardwareForm extends Component {
accounts: [],
browserSupported: true,
unlocked: false,
+ device: null
}
}
@@ -38,19 +39,22 @@ 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)
+ if (unlocked) {
+ this.setState({unlocked: true})
+ this.getPage(0, device)
+ }
+ })
}
- connectToTrezor = () => {
+ connectToHardwareWallet = (device) => {
+ debugger
if (this.state.accounts.length) {
return null
}
this.setState({ btnText: this.context.t('connecting')})
- this.getPage(0)
+ this.getPage(0, device)
}
onAccountChange = (account) => {
@@ -65,9 +69,9 @@ class ConnectHardwareForm extends Component {
}, 5000)
}
- getPage = (page) => {
+ getPage = (page, device) => {
this.props
- .connectHardware('trezor', page)
+ .connectHardware(device, page)
.then(accounts => {
if (accounts.length) {
@@ -77,7 +81,7 @@ class ConnectHardwareForm extends Component {
this.showTemporaryAlert()
}
- const newState = { unlocked: true }
+ const newState = { unlocked: true, device }
// Default to the first account
if (this.state.selectedAccount === null) {
accounts.forEach((a, i) => {
@@ -110,8 +114,8 @@ class ConnectHardwareForm extends Component {
})
}
- onForgetDevice = () => {
- this.props.forgetDevice('trezor')
+ onForgetDevice = (device) => {
+ this.props.forgetDevice(device)
.then(_ => {
this.setState({
error: null,
@@ -131,7 +135,7 @@ class ConnectHardwareForm extends Component {
this.setState({ error: this.context.t('accountSelectionRequired') })
}
- this.props.unlockTrezorAccount(this.state.selectedAccount)
+ this.props.unlockHardwareWalletAccount(this.state.selectedAccount, this.state.device)
.then(_ => {
this.props.history.push(DEFAULT_ROUTE)
}).catch(e => {
@@ -152,13 +156,14 @@ class ConnectHardwareForm extends Component {
renderContent () {
if (!this.state.accounts.length) {
return h(ConnectScreen, {
- connectToTrezor: this.connectToTrezor,
+ connectToHardwareWallet: this.connectToHardwareWallet,
btnText: this.state.btnText,
browserSupported: this.state.browserSupported,
})
}
return h(AccountList, {
+ device: this.state.device,
accounts: this.state.accounts,
selectedAccount: this.state.selectedAccount,
onAccountChange: this.onAccountChange,
@@ -188,7 +193,7 @@ ConnectHardwareForm.propTypes = {
forgetDevice: PropTypes.func,
showAlert: PropTypes.func,
hideAlert: PropTypes.func,
- unlockTrezorAccount: PropTypes.func,
+ unlockHardwareWalletAccount: PropTypes.func,
numberOfExistingAccounts: PropTypes.number,
history: PropTypes.object,
t: PropTypes.func,
@@ -222,8 +227,8 @@ const mapDispatchToProps = dispatch => {
forgetDevice: (deviceName) => {
return dispatch(actions.forgetDevice(deviceName))
},
- unlockTrezorAccount: index => {
- return dispatch(actions.unlockTrezorAccount(index))
+ unlockHardwareWalletAccount: (index, deviceName) => {
+ return dispatch(actions.unlockHardwareWalletAccount(index, deviceName))
},
showImportPage: () => dispatch(actions.showImportPage()),
showConnectPage: () => dispatch(actions.showConnectPage()),