parent
c0d2dab28b
commit
8c4d58aa45
After Width: | Height: | Size: 786 B |
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,255 @@ |
|||||||
|
const { EventEmitter } = require('events') |
||||||
|
const ethUtil = require('ethereumjs-util') |
||||||
|
// const sigUtil = require('eth-sig-util')
|
||||||
|
//const { Lock } = require('semaphore-async-await')
|
||||||
|
|
||||||
|
const hdPathString = `m/44'/60'/0'/0` |
||||||
|
const keyringType = 'Trezor Hardware Keyring' |
||||||
|
|
||||||
|
const TrezorConnect = require('./trezor-connect.js') |
||||||
|
const HDKey = require('hdkey') |
||||||
|
const TREZOR_FIRMWARE_VERSION = '1.4.0' |
||||||
|
const log = require('loglevel') |
||||||
|
|
||||||
|
class TrezorKeyring extends EventEmitter { |
||||||
|
constructor (opts = {}) { |
||||||
|
super() |
||||||
|
this.type = keyringType |
||||||
|
//this.lock = new Lock()
|
||||||
|
this.accounts = [] |
||||||
|
this.hdk = new HDKey() |
||||||
|
this.deserialize(opts) |
||||||
|
this.page = 0 |
||||||
|
this.perPage = 5 |
||||||
|
} |
||||||
|
|
||||||
|
serialize () { |
||||||
|
return Promise.resolve({ hdPath: this.hdPath, accounts: this.accounts }) |
||||||
|
} |
||||||
|
|
||||||
|
deserialize (opts = {}) { |
||||||
|
this.hdPath = opts.hdPath || hdPathString |
||||||
|
this.accounts = opts.accounts || [] |
||||||
|
return Promise.resolve() |
||||||
|
} |
||||||
|
|
||||||
|
unlock () { |
||||||
|
if (this.hdk.publicKey) return Promise.resolve() |
||||||
|
|
||||||
|
return new Promise((resolve, reject) => { |
||||||
|
TrezorConnect.getXPubKey( |
||||||
|
this.hdPath, |
||||||
|
response => { |
||||||
|
log.debug('TREZOR CONNECT RESPONSE: ') |
||||||
|
log.debug(response) |
||||||
|
if (response.success) { |
||||||
|
this.hdk.publicKey = new Buffer(response.publicKey, 'hex') |
||||||
|
this.hdk.chainCode = new Buffer(response.chainCode, 'hex') |
||||||
|
resolve() |
||||||
|
} else { |
||||||
|
reject(response.error || 'Unknown error') |
||||||
|
} |
||||||
|
}, |
||||||
|
TREZOR_FIRMWARE_VERSION |
||||||
|
) |
||||||
|
}) |
||||||
|
} |
||||||
|
|
||||||
|
addAccounts (n = 1) { |
||||||
|
return new Promise((resolve, reject) => { |
||||||
|
return this.unlock() |
||||||
|
.then(_ => { |
||||||
|
const pathBase = 'm' |
||||||
|
const from = n |
||||||
|
const to = n + 1 |
||||||
|
|
||||||
|
this.accounts = [] |
||||||
|
|
||||||
|
for (let i = from; i < to; i++) { |
||||||
|
const dkey = this.hdk.derive(`${pathBase}/${i}`) |
||||||
|
const address = ethUtil |
||||||
|
.publicToAddress(dkey.publicKey, true) |
||||||
|
.toString('hex') |
||||||
|
this.accounts.push(ethUtil.toChecksumAddress(address)) |
||||||
|
this.page = 0 |
||||||
|
} |
||||||
|
resolve(this.accounts) |
||||||
|
}) |
||||||
|
.catch(e => { |
||||||
|
reject(e) |
||||||
|
}) |
||||||
|
}) |
||||||
|
} |
||||||
|
|
||||||
|
async getPage () { |
||||||
|
return new Promise((resolve, reject) => { |
||||||
|
return this.unlock() |
||||||
|
.then(_ => { |
||||||
|
const pathBase = 'm' |
||||||
|
const from = this.page === 0 ? 0 : (this.page - 1) * this.perPage |
||||||
|
const to = from + this.perPage |
||||||
|
|
||||||
|
const accounts = [] |
||||||
|
|
||||||
|
for (let i = from; i < to; i++) { |
||||||
|
const dkey = this.hdk.derive(`${pathBase}/${i}`) |
||||||
|
const address = ethUtil |
||||||
|
.publicToAddress(dkey.publicKey, true) |
||||||
|
.toString('hex') |
||||||
|
accounts.push({ |
||||||
|
address: ethUtil.toChecksumAddress(address), |
||||||
|
balance: 0, |
||||||
|
index: i, |
||||||
|
}) |
||||||
|
} |
||||||
|
resolve(accounts) |
||||||
|
}) |
||||||
|
.catch(e => { |
||||||
|
reject(e) |
||||||
|
}) |
||||||
|
}) |
||||||
|
} |
||||||
|
|
||||||
|
async getPrevAccountSet () { |
||||||
|
this.page-- |
||||||
|
return await this.getPage() |
||||||
|
} |
||||||
|
|
||||||
|
async getNextAccountSet () { |
||||||
|
this.page++ |
||||||
|
return await this.getPage() |
||||||
|
} |
||||||
|
|
||||||
|
getAccounts () { |
||||||
|
return Promise.resolve(this.accounts.slice()) |
||||||
|
} |
||||||
|
|
||||||
|
// tx is an instance of the ethereumjs-transaction class.
|
||||||
|
async signTransaction (address, tx) { |
||||||
|
throw new Error('Not supported on this device') |
||||||
|
/* |
||||||
|
await this.lock.acquire() |
||||||
|
try { |
||||||
|
|
||||||
|
// Look before we leap
|
||||||
|
await this._checkCorrectTrezorAttached() |
||||||
|
|
||||||
|
let accountId = await this._findAddressId(address) |
||||||
|
let eth = await this._getEth() |
||||||
|
tx.v = tx._chainId |
||||||
|
let TrezorSig = await eth.signTransaction( |
||||||
|
this._derivePath(accountId), |
||||||
|
tx.serialize().toString('hex') |
||||||
|
) |
||||||
|
tx.v = parseInt(TrezorSig.v, 16) |
||||||
|
tx.r = '0x' + TrezorSig.r |
||||||
|
tx.s = '0x' + TrezorSig.s |
||||||
|
|
||||||
|
// Since look before we leap check is racy, also check that signature is for account expected
|
||||||
|
let addressSignedWith = ethUtil.bufferToHex(tx.getSenderAddress()) |
||||||
|
if (addressSignedWith.toLowerCase() !== address.toLowerCase()) { |
||||||
|
throw new Error( |
||||||
|
`Signature is for ${addressSignedWith} but expected ${address} - is the correct Trezor device attached?` |
||||||
|
) |
||||||
|
} |
||||||
|
|
||||||
|
return tx |
||||||
|
|
||||||
|
} finally { |
||||||
|
await this.lock.release() |
||||||
|
}*/ |
||||||
|
} |
||||||
|
|
||||||
|
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) { |
||||||
|
throw new Error('Not supported on this device') |
||||||
|
/* |
||||||
|
await this.lock.acquire() |
||||||
|
try { |
||||||
|
// Look before we leap
|
||||||
|
await this._checkCorrectTrezorAttached() |
||||||
|
|
||||||
|
let accountId = await this._findAddressId(withAccount) |
||||||
|
let eth = await this._getEth() |
||||||
|
let msgHex = ethUtil.stripHexPrefix(message) |
||||||
|
let TrezorSig = await eth.signPersonalMessage( |
||||||
|
this._derivePath(accountId), |
||||||
|
msgHex |
||||||
|
) |
||||||
|
let signature = this._personalToRawSig(TrezorSig) |
||||||
|
|
||||||
|
// Since look before we leap check is racy, also check that signature is for account expected
|
||||||
|
let addressSignedWith = sigUtil.recoverPersonalSignature({ |
||||||
|
data: message, |
||||||
|
sig: signature, |
||||||
|
}) |
||||||
|
if (addressSignedWith.toLowerCase() !== withAccount.toLowerCase()) { |
||||||
|
throw new Error( |
||||||
|
`Signature is for ${addressSignedWith} but expected ${withAccount} - is the correct Trezor device attached?` |
||||||
|
) |
||||||
|
} |
||||||
|
|
||||||
|
return signature |
||||||
|
|
||||||
|
} finally { |
||||||
|
await this.lock.release() |
||||||
|
} */ |
||||||
|
} |
||||||
|
|
||||||
|
async signTypedData (withAccount, typedData) { |
||||||
|
throw new Error('Not supported on this device') |
||||||
|
} |
||||||
|
|
||||||
|
async exportAccount (address) { |
||||||
|
throw new Error('Not supported on this device') |
||||||
|
} |
||||||
|
|
||||||
|
async _findAddressId (addr) { |
||||||
|
const result = this.accounts.indexOf(addr) |
||||||
|
if (result === -1) throw new Error('Unknown address') |
||||||
|
else return result |
||||||
|
} |
||||||
|
|
||||||
|
async _addressFromId (i) { |
||||||
|
/* Must be called with lock acquired |
||||||
|
const eth = await this._getEth() |
||||||
|
return (await eth.getAddress(this._derivePath(i))).address*/ |
||||||
|
const result = this.accounts[i] |
||||||
|
if (!result) throw new Error('Unknown address') |
||||||
|
else return result |
||||||
|
} |
||||||
|
|
||||||
|
async _checkCorrectTrezorAttached () { |
||||||
|
return true |
||||||
|
/* Must be called with lock acquired |
||||||
|
if (this.accounts.length > 0) { |
||||||
|
const expectedFirstAccount = this.accounts[0] |
||||||
|
let actualFirstAccount = await this._addressFromId(0) |
||||||
|
if (expectedFirstAccount !== actualFirstAccount) { |
||||||
|
throw new Error( |
||||||
|
`Incorrect Trezor device attached - expected device containg account ${expectedFirstAccount}, but found ${actualFirstAccount}` |
||||||
|
) |
||||||
|
} |
||||||
|
}*/ |
||||||
|
} |
||||||
|
|
||||||
|
_derivePath (i) { |
||||||
|
return this.hdPath + '/' + i |
||||||
|
} |
||||||
|
|
||||||
|
_personalToRawSig (TrezorSig) { |
||||||
|
var v = TrezorSig['v'] - 27 |
||||||
|
v = v.toString(16) |
||||||
|
if (v.length < 2) { |
||||||
|
v = '0' + v |
||||||
|
} |
||||||
|
return '0x' + TrezorSig['r'] + TrezorSig['s'] + v |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
TrezorKeyring.type = keyringType |
||||||
|
module.exports = TrezorKeyring |
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,234 @@ |
|||||||
|
const { Component } = require('react') |
||||||
|
const PropTypes = require('prop-types') |
||||||
|
const h = require('react-hyperscript') |
||||||
|
const connect = require('react-redux').connect |
||||||
|
const actions = require('../../../actions') |
||||||
|
const genAccountLink = require('../../../../lib/account-link.js') |
||||||
|
const log = require('loglevel') |
||||||
|
const { DEFAULT_ROUTE } = require('../../../routes') |
||||||
|
|
||||||
|
class ConnectHardwareForm extends Component { |
||||||
|
constructor (props, context) { |
||||||
|
super(props) |
||||||
|
this.state = { |
||||||
|
error: null, |
||||||
|
response: null, |
||||||
|
btnText: 'Connect to Trezor', // Test
|
||||||
|
selectedAccount: '', |
||||||
|
accounts: [], |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
connectToTrezor () { |
||||||
|
if (this.state.accounts.length) { |
||||||
|
return null |
||||||
|
} |
||||||
|
this.setState({ btnText: 'Connecting...' }) |
||||||
|
this.getPage() |
||||||
|
} |
||||||
|
|
||||||
|
getPage (page = 1) { |
||||||
|
this.props |
||||||
|
.connectHardware('trezor', page) |
||||||
|
.then(accounts => { |
||||||
|
if (accounts.length) { |
||||||
|
this.setState({ accounts: accounts }) |
||||||
|
} |
||||||
|
}) |
||||||
|
.catch(e => { |
||||||
|
this.setState({ btnText: 'Connect to Trezor' }) |
||||||
|
}) |
||||||
|
} |
||||||
|
|
||||||
|
unlockAccount () { |
||||||
|
if (this.state.selectedAccount === '') { |
||||||
|
return Promise.reject({ error: 'You need to select an account!' }) |
||||||
|
} |
||||||
|
log.debug('should unlock account ', this.state.selectedAccount) |
||||||
|
return this.props.unlockTrezorAccount(this.state.selectedAccount) |
||||||
|
} |
||||||
|
|
||||||
|
handleRadioChange = e => { |
||||||
|
log.debug('Selected account with index ', e.target.value) |
||||||
|
|
||||||
|
this.setState({ |
||||||
|
selectedAccount: e.target.value, |
||||||
|
error: null, |
||||||
|
}) |
||||||
|
} |
||||||
|
|
||||||
|
renderAccounts () { |
||||||
|
if (!this.state.accounts.length) { |
||||||
|
return null |
||||||
|
} |
||||||
|
log.debug('ACCOUNTS : ', this.state.accounts) |
||||||
|
log.debug('SELECTED?', this.state.selectedAccount) |
||||||
|
|
||||||
|
return h('div.hw-account-list', [ |
||||||
|
h('div.hw-account-list__title_wrapper', [ |
||||||
|
h('div.hw-account-list__title', {}, ['Select an Address']), |
||||||
|
h('div.hw-account-list__device', {}, ['Trezor - ETH']), |
||||||
|
]), |
||||||
|
this.state.accounts.map((a, i) => { |
||||||
|
return h('div.hw-account-list__item', { key: a.address }, [ |
||||||
|
h('span.hw-account-list__item__index', a.index + 1), |
||||||
|
h('div.hw-account-list__item__radio', [ |
||||||
|
h('input', { |
||||||
|
type: 'radio', |
||||||
|
name: 'selectedAccount', |
||||||
|
id: `address-${i}`, |
||||||
|
value: a.index, |
||||||
|
onChange: this.handleRadioChange, |
||||||
|
}), |
||||||
|
h( |
||||||
|
'label.hw-account-list__item__label', |
||||||
|
{ |
||||||
|
htmlFor: `address-${i}`, |
||||||
|
}, |
||||||
|
`${a.address.slice(0, 4)}...${a.address.slice(-4)}` |
||||||
|
), |
||||||
|
]), |
||||||
|
h('span.hw-account-list__item__balance', `${a.balance} ETH`), |
||||||
|
h( |
||||||
|
'a.hw-account-list__item__link', |
||||||
|
{ |
||||||
|
href: genAccountLink(a.address, this.props.network), |
||||||
|
target: '_blank', |
||||||
|
title: this.context.t('etherscanView'), |
||||||
|
}, |
||||||
|
h('img', { src: 'images/popout.svg' }) |
||||||
|
), |
||||||
|
]) |
||||||
|
}), |
||||||
|
]) |
||||||
|
} |
||||||
|
|
||||||
|
renderPagination () { |
||||||
|
if (!this.state.accounts.length) { |
||||||
|
return null |
||||||
|
} |
||||||
|
return h('div.hw-list-pagination', [ |
||||||
|
h( |
||||||
|
'button.btn-primary.hw-list-pagination__button', |
||||||
|
{ |
||||||
|
onClick: () => this.getPage(-1), |
||||||
|
}, |
||||||
|
'< Prev' |
||||||
|
), |
||||||
|
|
||||||
|
h( |
||||||
|
'button.btn-primary.hw-list-pagination__button', |
||||||
|
{ |
||||||
|
onClick: () => this.getPage(), |
||||||
|
}, |
||||||
|
'Next >' |
||||||
|
), |
||||||
|
]) |
||||||
|
} |
||||||
|
|
||||||
|
renderButtons () { |
||||||
|
if (!this.state.accounts.length) { |
||||||
|
return null |
||||||
|
} |
||||||
|
const { history } = this.props |
||||||
|
|
||||||
|
return h('div.new-account-create-form__buttons', {}, [ |
||||||
|
h( |
||||||
|
'button.btn-default.btn--large.new-account-create-form__button', |
||||||
|
{ |
||||||
|
onClick: () => history.push(DEFAULT_ROUTE), |
||||||
|
}, |
||||||
|
[this.context.t('cancel')] |
||||||
|
), |
||||||
|
|
||||||
|
h( |
||||||
|
'button.btn-primary.btn--large.new-account-create-form__button', |
||||||
|
{ |
||||||
|
onClick: () => { |
||||||
|
this.unlockAccount(this.state.selectedAccount) |
||||||
|
.then(() => history.push(DEFAULT_ROUTE)) |
||||||
|
.catch(e => { |
||||||
|
this.setState({ error: e.error }) |
||||||
|
}) |
||||||
|
}, |
||||||
|
}, |
||||||
|
[this.context.t('unlock')] |
||||||
|
), |
||||||
|
]) |
||||||
|
} |
||||||
|
|
||||||
|
renderError () { |
||||||
|
return this.state.error |
||||||
|
? h('span.error', { style: { marginBottom: 40 } }, this.state.error) |
||||||
|
: null |
||||||
|
} |
||||||
|
|
||||||
|
renderConnectButton () { |
||||||
|
return !this.state.accounts.length |
||||||
|
? h( |
||||||
|
'button.btn-primary.btn--large', |
||||||
|
{ onClick: () => this.connectToTrezor(), style: { margin: 12 } }, |
||||||
|
this.state.btnText |
||||||
|
) |
||||||
|
: null |
||||||
|
} |
||||||
|
|
||||||
|
render () { |
||||||
|
return h('div.new-account-create-form', [ |
||||||
|
this.renderError(), |
||||||
|
this.renderConnectButton(), |
||||||
|
this.renderAccounts(), |
||||||
|
this.renderPagination(), |
||||||
|
this.renderButtons(), |
||||||
|
]) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
ConnectHardwareForm.propTypes = { |
||||||
|
hideModal: PropTypes.func, |
||||||
|
showImportPage: PropTypes.func, |
||||||
|
showConnectPage: PropTypes.func, |
||||||
|
connectHardware: PropTypes.func, |
||||||
|
unlockTrezorAccount: PropTypes.func, |
||||||
|
numberOfExistingAccounts: PropTypes.number, |
||||||
|
history: PropTypes.object, |
||||||
|
t: PropTypes.func, |
||||||
|
network: PropTypes.string, |
||||||
|
} |
||||||
|
|
||||||
|
const mapStateToProps = state => { |
||||||
|
const { |
||||||
|
metamask: { network, selectedAddress, identities = {} }, |
||||||
|
} = state |
||||||
|
const numberOfExistingAccounts = Object.keys(identities).length |
||||||
|
|
||||||
|
return { |
||||||
|
network, |
||||||
|
address: selectedAddress, |
||||||
|
numberOfExistingAccounts, |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
const mapDispatchToProps = dispatch => { |
||||||
|
return { |
||||||
|
toCoinbase: address => |
||||||
|
dispatch(actions.buyEth({ network: '1', address, amount: 0 })), |
||||||
|
hideModal: () => dispatch(actions.hideModal()), |
||||||
|
connectHardware: (deviceName, page) => { |
||||||
|
return dispatch(actions.connectHardware(deviceName, page)) |
||||||
|
}, |
||||||
|
unlockTrezorAccount: index => { |
||||||
|
return dispatch(actions.unlockTrezorAccount(index)) |
||||||
|
}, |
||||||
|
showImportPage: () => dispatch(actions.showImportPage()), |
||||||
|
showConnectPage: () => dispatch(actions.showConnectPage()), |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
ConnectHardwareForm.contextTypes = { |
||||||
|
t: PropTypes.func, |
||||||
|
} |
||||||
|
|
||||||
|
module.exports = connect(mapStateToProps, mapDispatchToProps)( |
||||||
|
ConnectHardwareForm |
||||||
|
) |
Loading…
Reference in new issue