mirror of
https://github.com/kremalicious/metamask-extension.git
synced 2024-12-22 17:33:23 +01:00
initial trezor support
This commit is contained in:
parent
c0d2dab28b
commit
8c4d58aa45
@ -119,6 +119,12 @@
|
|||||||
"confirmTransaction": {
|
"confirmTransaction": {
|
||||||
"message": "Confirm Transaction"
|
"message": "Confirm Transaction"
|
||||||
},
|
},
|
||||||
|
"connectHardware": {
|
||||||
|
"message": "Connect Hardware"
|
||||||
|
},
|
||||||
|
"connect": {
|
||||||
|
"message": "Connect"
|
||||||
|
},
|
||||||
"continue": {
|
"continue": {
|
||||||
"message": "Continue"
|
"message": "Continue"
|
||||||
},
|
},
|
||||||
@ -930,6 +936,9 @@
|
|||||||
"unknownNetworkId": {
|
"unknownNetworkId": {
|
||||||
"message": "Unknown network ID"
|
"message": "Unknown network ID"
|
||||||
},
|
},
|
||||||
|
"unlock": {
|
||||||
|
"message": "Unlock"
|
||||||
|
},
|
||||||
"unlockMessage": {
|
"unlockMessage": {
|
||||||
"message": "The decentralized web awaits"
|
"message": "The decentralized web awaits"
|
||||||
},
|
},
|
||||||
|
11
app/images/connect-icon.svg
Normal file
11
app/images/connect-icon.svg
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
<svg width="288" height="288" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
|
||||||
|
<g>
|
||||||
|
<title>background</title>
|
||||||
|
<rect fill="none" id="canvas_background" height="402" width="582" y="-1" x="-1"/>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
<title>Layer 1</title>
|
||||||
|
<path fill="#ffffff" id="svg_1" d="m122,25l15,-21c4,-5 10,-5 14,0l16,22c4,5 2,10 -5,10l-12,0l0,118c0,3 3,3 5,1l25,-25c4,-4 6,-10 6,-16l0,-24c-7,0 -12,-5 -12,-12l0,-12c0,-6 5,-12 12,-12l12,0c7,0 12,5 12,12l0,12c0,7 -5,12 -12,12l0,24c0,10 -3,18 -10,25l-31,31c-4,4 -7,6 -7,16l0,49c12,3 21,13 21,26c0,15 -12,27 -27,27s-27,-12 -27,-27c0,-13 9,-23 21,-26l0,-13c0,-10 -3,-13 -7,-17l-31,-31c-6,-6 -10,-14 -10,-24l0,-25c-7,-2 -12,-9 -12,-17c0,-10 8,-18 18,-18s18,8 18,18c0,8 -5,15 -12,17l0,25c0,7 3,12 7,16l25,25c2,2 4,2 4,-1l0,-154l-12,0c-7,0 -9,-5 -4,-11z"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 786 B |
1138
app/scripts/lib/trezor-connect.js
Normal file
1138
app/scripts/lib/trezor-connect.js
Normal file
File diff suppressed because it is too large
Load Diff
255
app/scripts/lib/trezorKeyring.js
Normal file
255
app/scripts/lib/trezorKeyring.js
Normal file
@ -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
|
@ -48,6 +48,7 @@ const seedPhraseVerifier = require('./lib/seed-phrase-verifier')
|
|||||||
const cleanErrorStack = require('./lib/cleanErrorStack')
|
const cleanErrorStack = require('./lib/cleanErrorStack')
|
||||||
const DiagnosticsReporter = require('./lib/diagnostics-reporter')
|
const DiagnosticsReporter = require('./lib/diagnostics-reporter')
|
||||||
const log = require('loglevel')
|
const log = require('loglevel')
|
||||||
|
const TrezorKeyring = require("./lib/trezorKeyring");
|
||||||
|
|
||||||
module.exports = class MetamaskController extends EventEmitter {
|
module.exports = class MetamaskController extends EventEmitter {
|
||||||
|
|
||||||
@ -132,7 +133,9 @@ module.exports = class MetamaskController extends EventEmitter {
|
|||||||
})
|
})
|
||||||
|
|
||||||
// key mgmt
|
// key mgmt
|
||||||
|
const additionalKeyrings = [TrezorKeyring]
|
||||||
this.keyringController = new KeyringController({
|
this.keyringController = new KeyringController({
|
||||||
|
keyringTypes: additionalKeyrings,
|
||||||
initState: initState.KeyringController,
|
initState: initState.KeyringController,
|
||||||
getNetwork: this.networkController.getNetworkState.bind(this.networkController),
|
getNetwork: this.networkController.getNetworkState.bind(this.networkController),
|
||||||
encryptor: opts.encryptor || undefined,
|
encryptor: opts.encryptor || undefined,
|
||||||
@ -363,6 +366,10 @@ module.exports = class MetamaskController extends EventEmitter {
|
|||||||
resetAccount: nodeify(this.resetAccount, this),
|
resetAccount: nodeify(this.resetAccount, this),
|
||||||
importAccountWithStrategy: nodeify(this.importAccountWithStrategy, this),
|
importAccountWithStrategy: nodeify(this.importAccountWithStrategy, this),
|
||||||
|
|
||||||
|
// trezor
|
||||||
|
connectHardware: nodeify(this.connectHardware, this),
|
||||||
|
unlockTrezorAccount: nodeify(this.unlockTrezorAccount, this),
|
||||||
|
|
||||||
// vault management
|
// vault management
|
||||||
submitPassword: nodeify(this.submitPassword, this),
|
submitPassword: nodeify(this.submitPassword, this),
|
||||||
|
|
||||||
@ -523,6 +530,60 @@ module.exports = class MetamaskController extends EventEmitter {
|
|||||||
this.preferencesController.setSelectedAddress(address)
|
this.preferencesController.setSelectedAddress(address)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Hardware
|
||||||
|
//
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch account list from a trezor device.
|
||||||
|
*
|
||||||
|
* @returns [] accounts
|
||||||
|
*/
|
||||||
|
async connectHardware (deviceName, page) {
|
||||||
|
const keyringController = this.keyringController
|
||||||
|
const keyring = await keyringController.getKeyringsByType(
|
||||||
|
'Trezor Hardware Keyring'
|
||||||
|
)[0]
|
||||||
|
if (!keyring) {
|
||||||
|
throw new Error('MetamaskController - No Trezor Hardware Keyring found')
|
||||||
|
}
|
||||||
|
|
||||||
|
const accounts = page === -1 ? await keyring.getPrevAccountSet() : await keyring.getNextAccountSet()
|
||||||
|
|
||||||
|
return accounts
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Imports an account from a trezor device.
|
||||||
|
*
|
||||||
|
* @returns {} keyState
|
||||||
|
*/
|
||||||
|
async unlockTrezorAccount (index) {
|
||||||
|
const keyringController = this.keyringController
|
||||||
|
const keyring = await keyringController.getKeyringsByType(
|
||||||
|
'Trezor Hardware Keyring'
|
||||||
|
)[0]
|
||||||
|
if (!keyring) {
|
||||||
|
throw new Error('MetamaskController - No Trezor Hardware Keyring found')
|
||||||
|
}
|
||||||
|
|
||||||
|
const oldAccounts = await keyringController.getAccounts()
|
||||||
|
const keyState = await keyringController.addNewAccount(keyring)
|
||||||
|
const newAccounts = await keyringController.getAccounts()
|
||||||
|
|
||||||
|
this.preferencesController.setAddresses(newAccounts)
|
||||||
|
newAccounts.forEach(address => {
|
||||||
|
if (!oldAccounts.includes(address)) {
|
||||||
|
this.preferencesController.setSelectedAddress(address)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const { identities } = this.preferencesController.store.getState()
|
||||||
|
return { ...keyState, identities }
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
//
|
//
|
||||||
// Account Management
|
// Account Management
|
||||||
//
|
//
|
||||||
|
12949
package-lock.json
generated
12949
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -124,6 +124,7 @@
|
|||||||
"gulp-eslint": "^4.0.0",
|
"gulp-eslint": "^4.0.0",
|
||||||
"gulp-sass": "^4.0.0",
|
"gulp-sass": "^4.0.0",
|
||||||
"hat": "0.0.3",
|
"hat": "0.0.3",
|
||||||
|
"hdkey": "0.8.0",
|
||||||
"human-standard-token-abi": "^1.0.2",
|
"human-standard-token-abi": "^1.0.2",
|
||||||
"idb-global": "^2.1.0",
|
"idb-global": "^2.1.0",
|
||||||
"identicon.js": "^2.3.1",
|
"identicon.js": "^2.3.1",
|
||||||
@ -229,7 +230,7 @@
|
|||||||
"eslint-plugin-mocha": "^5.0.0",
|
"eslint-plugin-mocha": "^5.0.0",
|
||||||
"eslint-plugin-react": "^7.4.0",
|
"eslint-plugin-react": "^7.4.0",
|
||||||
"eth-json-rpc-middleware": "^1.6.0",
|
"eth-json-rpc-middleware": "^1.6.0",
|
||||||
"eth-keyring-controller": "^3.1.4",
|
"eth-keyring-controller": "github:brunobar79/KeyringController#e447cb7cc93d1b7bf082710a6d5e21150588571e",
|
||||||
"file-loader": "^1.1.11",
|
"file-loader": "^1.1.11",
|
||||||
"fs-promise": "^2.0.3",
|
"fs-promise": "^2.0.3",
|
||||||
"ganache-cli": "^6.1.0",
|
"ganache-cli": "^6.1.0",
|
||||||
|
@ -79,6 +79,8 @@ var actions = {
|
|||||||
addNewKeyring,
|
addNewKeyring,
|
||||||
importNewAccount,
|
importNewAccount,
|
||||||
addNewAccount,
|
addNewAccount,
|
||||||
|
connectHardware,
|
||||||
|
unlockTrezorAccount,
|
||||||
NEW_ACCOUNT_SCREEN: 'NEW_ACCOUNT_SCREEN',
|
NEW_ACCOUNT_SCREEN: 'NEW_ACCOUNT_SCREEN',
|
||||||
navigateToNewAccountScreen,
|
navigateToNewAccountScreen,
|
||||||
resetAccount,
|
resetAccount,
|
||||||
@ -596,6 +598,46 @@ function addNewAccount () {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function connectHardware (deviceName, page) {
|
||||||
|
log.debug(`background.connectHardware`, deviceName, page)
|
||||||
|
return (dispatch, getState) => {
|
||||||
|
dispatch(actions.showLoadingIndication())
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
background.connectHardware(deviceName, page, (err, accounts) => {
|
||||||
|
if (err) {
|
||||||
|
log.error(err)
|
||||||
|
dispatch(actions.displayWarning(err.message))
|
||||||
|
return reject(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
dispatch(actions.hideLoadingIndication())
|
||||||
|
|
||||||
|
forceUpdateMetamaskState(dispatch)
|
||||||
|
return resolve(accounts)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function unlockTrezorAccount (index) {
|
||||||
|
log.debug(`background.unlockTrezorAccount`, index)
|
||||||
|
return (dispatch, getState) => {
|
||||||
|
dispatch(actions.showLoadingIndication())
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
background.unlockTrezorAccount(index, (err, accounts) => {
|
||||||
|
if (err) {
|
||||||
|
log.error(err)
|
||||||
|
dispatch(actions.displayWarning(err.message))
|
||||||
|
return reject(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
dispatch(actions.hideLoadingIndication())
|
||||||
|
return resolve()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function showInfoPage () {
|
function showInfoPage () {
|
||||||
return {
|
return {
|
||||||
type: actions.SHOW_INFO_PAGE,
|
type: actions.SHOW_INFO_PAGE,
|
||||||
|
@ -14,6 +14,7 @@ const {
|
|||||||
INFO_ROUTE,
|
INFO_ROUTE,
|
||||||
NEW_ACCOUNT_ROUTE,
|
NEW_ACCOUNT_ROUTE,
|
||||||
IMPORT_ACCOUNT_ROUTE,
|
IMPORT_ACCOUNT_ROUTE,
|
||||||
|
CONNECT_HARDWARE_ROUTE,
|
||||||
DEFAULT_ROUTE,
|
DEFAULT_ROUTE,
|
||||||
} = require('../../routes')
|
} = require('../../routes')
|
||||||
|
|
||||||
@ -106,6 +107,14 @@ AccountMenu.prototype.render = function () {
|
|||||||
icon: h('img.account-menu__item-icon', { src: 'images/import-account.svg' }),
|
icon: h('img.account-menu__item-icon', { src: 'images/import-account.svg' }),
|
||||||
text: this.context.t('importAccount'),
|
text: this.context.t('importAccount'),
|
||||||
}),
|
}),
|
||||||
|
h(Item, {
|
||||||
|
onClick: () => {
|
||||||
|
toggleAccountMenu()
|
||||||
|
history.push(CONNECT_HARDWARE_ROUTE)
|
||||||
|
},
|
||||||
|
icon: h('img.account-menu__item-icon', { src: 'images/connect-icon.svg' }),
|
||||||
|
text: this.context.t('connectHardware'),
|
||||||
|
}),
|
||||||
h(Divider),
|
h(Divider),
|
||||||
h(Item, {
|
h(Item, {
|
||||||
onClick: () => {
|
onClick: () => {
|
||||||
|
234
ui/app/components/pages/create-account/connect-hardware.js
Normal file
234
ui/app/components/pages/create-account/connect-hardware.js
Normal file
@ -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
|
||||||
|
)
|
@ -8,7 +8,12 @@ const { getCurrentViewContext } = require('../../../selectors')
|
|||||||
const classnames = require('classnames')
|
const classnames = require('classnames')
|
||||||
const NewAccountCreateForm = require('./new-account')
|
const NewAccountCreateForm = require('./new-account')
|
||||||
const NewAccountImportForm = require('./import-account')
|
const NewAccountImportForm = require('./import-account')
|
||||||
const { NEW_ACCOUNT_ROUTE, IMPORT_ACCOUNT_ROUTE } = require('../../../routes')
|
const ConnectHardwareForm = require('./connect-hardware')
|
||||||
|
const {
|
||||||
|
NEW_ACCOUNT_ROUTE,
|
||||||
|
IMPORT_ACCOUNT_ROUTE,
|
||||||
|
CONNECT_HARDWARE_ROUTE,
|
||||||
|
} = require('../../../routes')
|
||||||
|
|
||||||
class CreateAccountPage extends Component {
|
class CreateAccountPage extends Component {
|
||||||
renderTabs () {
|
renderTabs () {
|
||||||
@ -36,6 +41,19 @@ class CreateAccountPage extends Component {
|
|||||||
}, [
|
}, [
|
||||||
this.context.t('import'),
|
this.context.t('import'),
|
||||||
]),
|
]),
|
||||||
|
h(
|
||||||
|
'div.new-account__tabs__tab',
|
||||||
|
{
|
||||||
|
className: classnames('new-account__tabs__tab', {
|
||||||
|
'new-account__tabs__selected': matchPath(location.pathname, {
|
||||||
|
path: CONNECT_HARDWARE_ROUTE,
|
||||||
|
exact: true,
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
onClick: () => history.push(CONNECT_HARDWARE_ROUTE),
|
||||||
|
},
|
||||||
|
this.context.t('connect')
|
||||||
|
),
|
||||||
])
|
])
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -57,6 +75,11 @@ class CreateAccountPage extends Component {
|
|||||||
path: IMPORT_ACCOUNT_ROUTE,
|
path: IMPORT_ACCOUNT_ROUTE,
|
||||||
component: NewAccountImportForm,
|
component: NewAccountImportForm,
|
||||||
}),
|
}),
|
||||||
|
h(Route, {
|
||||||
|
exact: true,
|
||||||
|
path: CONNECT_HARDWARE_ROUTE,
|
||||||
|
component: ConnectHardwareForm,
|
||||||
|
}),
|
||||||
]),
|
]),
|
||||||
]),
|
]),
|
||||||
])
|
])
|
||||||
|
@ -62,6 +62,7 @@ class NewAccountCreateForm extends Component {
|
|||||||
NewAccountCreateForm.propTypes = {
|
NewAccountCreateForm.propTypes = {
|
||||||
hideModal: PropTypes.func,
|
hideModal: PropTypes.func,
|
||||||
showImportPage: PropTypes.func,
|
showImportPage: PropTypes.func,
|
||||||
|
showConnectPage: PropTypes.func,
|
||||||
createAccount: PropTypes.func,
|
createAccount: PropTypes.func,
|
||||||
numberOfExistingAccounts: PropTypes.number,
|
numberOfExistingAccounts: PropTypes.number,
|
||||||
history: PropTypes.object,
|
history: PropTypes.object,
|
||||||
@ -92,6 +93,7 @@ const mapDispatchToProps = dispatch => {
|
|||||||
})
|
})
|
||||||
},
|
},
|
||||||
showImportPage: () => dispatch(actions.showImportPage()),
|
showImportPage: () => dispatch(actions.showImportPage()),
|
||||||
|
showConnectPage: () => dispatch(actions.showConnectPage()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -28,7 +28,6 @@
|
|||||||
|
|
||||||
&__tab {
|
&__tab {
|
||||||
height: 54px;
|
height: 54px;
|
||||||
width: 75px;
|
|
||||||
padding: 15px 10px;
|
padding: 15px 10px;
|
||||||
color: $dusty-gray;
|
color: $dusty-gray;
|
||||||
font-family: Roboto;
|
font-family: Roboto;
|
||||||
@ -38,10 +37,6 @@
|
|||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
&__tab:first-of-type {
|
|
||||||
margin-right: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__tab:hover {
|
&__tab:hover {
|
||||||
color: $black;
|
color: $black;
|
||||||
border-bottom: none;
|
border-bottom: none;
|
||||||
@ -158,6 +153,106 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.hw-account-list {
|
||||||
|
display: flex;
|
||||||
|
flex: 1;
|
||||||
|
flex-flow: column;
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
&__title_wrapper {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__title {
|
||||||
|
margin-bottom: 23px;
|
||||||
|
align-self: flex-start;
|
||||||
|
color: $scorpion;
|
||||||
|
font-family: Roboto;
|
||||||
|
font-size: 16px;
|
||||||
|
line-height: 21px;
|
||||||
|
font-weight: bold;
|
||||||
|
display: flex;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__device {
|
||||||
|
margin-bottom: 23px;
|
||||||
|
align-self: flex-end;
|
||||||
|
color: $scorpion;
|
||||||
|
font-family: Roboto;
|
||||||
|
font-size: 16px;
|
||||||
|
line-height: 21px;
|
||||||
|
font-weight: normal;
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__item {
|
||||||
|
font-size: 15px;
|
||||||
|
flex-direction: row;
|
||||||
|
display: flex;
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__item:nth-of-type(even) {
|
||||||
|
background-color: #fbfbfb;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__item:nth-of-type(odd) {
|
||||||
|
background: rgba(0, 0, 0, 0.03);
|
||||||
|
}
|
||||||
|
|
||||||
|
&__item:hover {
|
||||||
|
background-color: rgba(0, 0, 0, 0.06);
|
||||||
|
}
|
||||||
|
|
||||||
|
&__item__index {
|
||||||
|
display: flex;
|
||||||
|
margin-right: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__item__radio {
|
||||||
|
display: flex;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__item__label {
|
||||||
|
margin-left: 10px;
|
||||||
|
display: flex;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__item__balance {
|
||||||
|
display: flex;
|
||||||
|
flex: 1;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__item__link {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__item__link img {
|
||||||
|
width: 15px;
|
||||||
|
height: 15px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.hw-list-pagination {
|
||||||
|
display: flex;
|
||||||
|
align-self: flex-end;
|
||||||
|
margin-top: 10px;
|
||||||
|
|
||||||
|
&__button {
|
||||||
|
height: 25px;
|
||||||
|
flex: initial;
|
||||||
|
min-width: 90px;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
.new-account-create-form {
|
.new-account-create-form {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-flow: column;
|
flex-flow: column;
|
||||||
|
@ -9,6 +9,7 @@ const ADD_TOKEN_ROUTE = '/add-token'
|
|||||||
const CONFIRM_ADD_TOKEN_ROUTE = '/confirm-add-token'
|
const CONFIRM_ADD_TOKEN_ROUTE = '/confirm-add-token'
|
||||||
const NEW_ACCOUNT_ROUTE = '/new-account'
|
const NEW_ACCOUNT_ROUTE = '/new-account'
|
||||||
const IMPORT_ACCOUNT_ROUTE = '/new-account/import'
|
const IMPORT_ACCOUNT_ROUTE = '/new-account/import'
|
||||||
|
const CONNECT_HARDWARE_ROUTE = '/new-account/connect'
|
||||||
const SEND_ROUTE = '/send'
|
const SEND_ROUTE = '/send'
|
||||||
const CONFIRM_TRANSACTION_ROUTE = '/confirm-transaction'
|
const CONFIRM_TRANSACTION_ROUTE = '/confirm-transaction'
|
||||||
const SIGNATURE_REQUEST_ROUTE = '/confirm-transaction/signature-request'
|
const SIGNATURE_REQUEST_ROUTE = '/confirm-transaction/signature-request'
|
||||||
@ -35,6 +36,7 @@ module.exports = {
|
|||||||
CONFIRM_ADD_TOKEN_ROUTE,
|
CONFIRM_ADD_TOKEN_ROUTE,
|
||||||
NEW_ACCOUNT_ROUTE,
|
NEW_ACCOUNT_ROUTE,
|
||||||
IMPORT_ACCOUNT_ROUTE,
|
IMPORT_ACCOUNT_ROUTE,
|
||||||
|
CONNECT_HARDWARE_ROUTE,
|
||||||
SEND_ROUTE,
|
SEND_ROUTE,
|
||||||
CONFIRM_TRANSACTION_ROUTE,
|
CONFIRM_TRANSACTION_ROUTE,
|
||||||
NOTICE_ROUTE,
|
NOTICE_ROUTE,
|
||||||
|
Loading…
Reference in New Issue
Block a user