on master help sign in
Merge pull request #129 from urbit/ledger-hdpaths

Help with hardware wallet derivation path selection

commita0fc18f46f9c02ca780f9efde27fd8ec16e2e5c9 authorFang <Fang-@users.noreply.github.com> dateMon 6 May 2019, 20:03 (7 years ago) parent666d0c6430a7b6
5 files changed+151−34
src/bridge/components/InnerLabelDropdown.js+2−2 src/bridge/lib/ledger.js+4−2 src/bridge/lib/trezor.js+2−2 src/bridge/views/Ledger.js+86−14 src/bridge/views/Trezor.js+57−14
src/bridge/components/InnerLabelDropdown.js+2−2
@@ -62,14 +62,14 @@ class InnerLabelDropdown extends React.Component {
6262 const fullWidthClass = props.fullWidth ? 'full-width' : ''
6363
6464 return (
65− <div className={`rel table ${fullWidthClass}`}>
65+ <div className={`rel table ${fullWidthClass} ${props.className}`}>
6666 <div
6767 className={`fix full o-0 ${menuVisibility}`}
6868 onClick={this.close}
6969 />
7070
7171 <button
72− className={`h-10 mp-0 bg-white outline-blue sans ${props.className} ${fullWidthClass}`}
72+ className={`h-10 mp-0 bg-white outline-blue sans ${fullWidthClass}`}
7373 onClick={this.toggle}
7474 disabled={props.disabled}>
7575 <div className={'flex'}>
src/bridge/lib/ledger.js+4−2
@@ -1,7 +1,8 @@
11import Transport from '@ledgerhq/hw-transport-u2f'
22import Eth from '@ledgerhq/hw-app-eth'
33
4−const LEDGER_BASE_PATH = "44'/60'/0'/0/0"
4+const LEDGER_LEGACY_PATH = "m/44'/60'/0'/x"
5+const LEDGER_LIVE_PATH = "m/44'/60'/x'/0/0"
56
67const chopHdPrefix = str =>
78 str.slice(0, 2) === "m/"
@@ -24,6 +25,7 @@ const ledgerSignTransaction = async (txn, hdpath) => {
2425}
2526
2627export {
27− LEDGER_BASE_PATH,
28+ LEDGER_LIVE_PATH,
29+ LEDGER_LEGACY_PATH,
2830 ledgerSignTransaction
2931}
src/bridge/lib/trezor.js+2−2
@@ -1,6 +1,6 @@
11import TrezorConnect from 'trezor-connect'
22
3−const TREZOR_BASE_PATH = "m/44'/60'/0'/0/0"
3+const TREZOR_PATH = "m/44'/60'/0'/0/x"
44
55const formatChainId = val =>
66 typeof val === 'number'
@@ -33,7 +33,7 @@ const trezorSignTransaction = async (txn, hdpath) => {
3333}
3434
3535export {
36− TREZOR_BASE_PATH,
36+ TREZOR_PATH,
3737 trezorSignTransaction
3838}
3939
src/bridge/views/Ledger.js+86−14
@@ -3,12 +3,12 @@ import React from 'react'
33import Maybe from 'folktale/maybe'
44import { Button } from '../components/Base'
55import { Row, Col, H1, P, H2 } from '../components/Base'
6−import { Input, InnerLabel } from '../components/Base'
6+import { Input, InnerLabel, InnerLabelDropdown } from '../components/Base'
77import Transport from '@ledgerhq/hw-transport-u2f'
88import Eth from '@ledgerhq/hw-app-eth'
99import * as secp256k1 from 'secp256k1'
1010
11−import { LEDGER_BASE_PATH } from '../lib/ledger'
11+import { LEDGER_LIVE_PATH, LEDGER_LEGACY_PATH } from '../lib/ledger'
1212import { ROUTE_NAMES } from '../lib/router'
1313
1414const chopHdPrefix = str =>
@@ -26,11 +26,32 @@ class Ledger extends React.Component {
2626 super(props)
2727
2828 this.state = {
29− hdpath: LEDGER_BASE_PATH
29+ basePath: LEDGER_LIVE_PATH,
30+ hdpath: LEDGER_LIVE_PATH.replace(/x/g, 0),
31+ account: 0
3032 }
3133
3234 this.pollDevice = this.pollDevice.bind(this)
3335 this.handleHdPathInput = this.handleHdPathInput.bind(this)
36+ this.handlePathSelection = this.handlePathSelection.bind(this);
37+ this.handleAccountSelection = this.handleAccountSelection.bind(this);
38+ this.updateHdPath = this.updateHdPath.bind(this);
39+ }
40+
41+ handlePathSelection(basePath) {
42+ this.updateHdPath(basePath, this.state.account);
43+ }
44+
45+ handleAccountSelection(account) {
46+ this.updateHdPath(this.state.basePath, account);
47+ }
48+
49+ updateHdPath(basePath, account) {
50+ let hdpath = this.state.hdpath;
51+ if (basePath !== 'custom') {
52+ hdpath = basePath.replace(/x/g, account);
53+ }
54+ this.setState({ basePath, account, hdpath });
3455 }
3556
3657 handleHdPathInput(hdpath) {
@@ -60,7 +81,66 @@ class Ledger extends React.Component {
6081
6182 render() {
6283 const { pushRoute, popRoute, wallet } = this.props
63− const { hdpath } = this.state
84+ const { basePath, hdpath, account } = this.state;
85+ const { handlePathSelection, handleAccountSelection } = this;
86+
87+ const pathOptions = [
88+ { title: 'Ledger Live',
89+ value: LEDGER_LIVE_PATH
90+ },
91+ { title: 'Ledger Legacy',
92+ value: LEDGER_LEGACY_PATH
93+ },
94+ {
95+ title: 'Custom path',
96+ value: 'custom'
97+ }
98+ ];
99+ let basePathTitle = pathOptions.find(o => (o.value === basePath)).title;
100+
101+ let accountOptions = [];
102+ for (let i = 0; i < 20; i++) {
103+ accountOptions.push({
104+ title: 'Account #' + (i+1),
105+ value: i
106+ });
107+ }
108+ let accountTitle = accountOptions.find(o => (o.value === account)).title;
109+
110+ const basePathSelection = (
111+ <InnerLabelDropdown
112+ className='mt-8'
113+ options={pathOptions}
114+ handleUpdate={handlePathSelection}
115+ title={'Derivation path'}
116+ currentSelectionTitle={basePathTitle}
117+ fullWidth={true}
118+ >
119+ </InnerLabelDropdown>
120+ );
121+
122+ const truePathSelection = (basePath === 'custom')
123+ ? ( <Input
124+ className='pt-8 mt-4 text-mono'
125+ prop-size='md'
126+ prop-format='innerLabel'
127+ name='hdpath'
128+ value={ addHdPrefix(hdpath) }
129+ autocomplete='off'
130+ onChange={ this.handleHdPathInput }>
131+ <InnerLabel>{'HD Path'}</InnerLabel>
132+ </Input> )
133+ : ( <InnerLabelDropdown
134+ className='mt-4'
135+ prop-size='md'
136+ prop-format='innerLabel'
137+ options={accountOptions}
138+ handleUpdate={handleAccountSelection}
139+ title={'Account'}
140+ currentSelectionTitle={accountTitle}
141+ fullWidth={true}
142+ >
143+ </InnerLabelDropdown> );
64144
65145 return (
66146
@@ -99,16 +179,8 @@ class Ledger extends React.Component {
99179 <li className={'mt-4'}>{'Run '}<code>{'python bridge-https.py'}</code></li>
100180 </ol>
101181
102− <Input
103− className='pt-8 mt-8 text-mono'
104− prop-size='md'
105− prop-format='innerLabel'
106− name='hdpath'
107− value={ addHdPrefix(hdpath) }
108− autocomplete='off'
109− onChange={ this.handleHdPathInput }>
110− <InnerLabel>{'HD Path'}</InnerLabel>
111− </Input>
182+ { basePathSelection }
183+ { truePathSelection }
112184
113185 <Button
114186 prop-size={'wide lg'}
src/bridge/views/Trezor.js+57−14
@@ -3,11 +3,11 @@ import React from 'react'
33import Maybe from 'folktale/maybe'
44import { Button } from '../components/Base'
55import { Row, Col, H1, P } from '../components/Base'
6−import { InnerLabel, Input } from '../components/Base'
6+import { InnerLabel, Input, InnerLabelDropdown } from '../components/Base'
77import TrezorConnect from 'trezor-connect'
88import * as secp256k1 from 'secp256k1'
99
10−import { TREZOR_BASE_PATH } from '../lib/trezor'
10+import { TREZOR_PATH } from '../lib/trezor'
1111import { ROUTE_NAMES } from '../lib/router'
1212
1313class Trezor extends React.Component {
@@ -16,13 +16,23 @@ class Trezor extends React.Component {
1616 super(props)
1717
1818 this.state = {
19− hdpath: TREZOR_BASE_PATH
19+ hdpath: TREZOR_PATH.replace(/x/g, 0),
20+ account: 0
2021 }
2122
23+ this.handleAccountSelection = this.handleAccountSelection.bind(this)
2224 this.handleHdPathInput = this.handleHdPathInput.bind(this)
2325 this.pollDevice = this.pollDevice.bind(this)
2426 }
2527
28+ handleAccountSelection(account) {
29+ let hdpath = this.state.hdpath;
30+ if (account !== 'custom') {
31+ hdpath = TREZOR_PATH.replace(/x/g, account);
32+ }
33+ this.setState({ account, hdpath });
34+ }
35+
2636 handleHdPathInput(hdpath) {
2737 this.setState({ hdpath })
2838 }
@@ -53,7 +63,48 @@ class Trezor extends React.Component {
5363
5464 render() {
5565 const { pushRoute, popRoute, wallet } = this.props
56− const { hdpath } = this.state
66+ const { hdpath, account } = this.state
67+ const { handleAccountSelection } = this;
68+
69+ let accountOptions = [{
70+ title: 'Custom path',
71+ value: 'custom'
72+ }];
73+ for (let i = 0; i < 20; i++) {
74+ accountOptions.push({
75+ title: 'Account #' + (i+1),
76+ value: i
77+ });
78+ }
79+ let accountTitle = accountOptions.find(o => (o.value === account)).title;
80+
81+ const accountSelection = (
82+ <InnerLabelDropdown
83+ className='mt-8'
84+ prop-size='md'
85+ prop-format='innerLabel'
86+ options={accountOptions}
87+ handleUpdate={handleAccountSelection}
88+ title={'Account'}
89+ currentSelectionTitle={accountTitle}
90+ fullWidth={true}
91+ >
92+ </InnerLabelDropdown>
93+ );
94+
95+ const pathSelection = (account !== 'custom') ? null :
96+ (
97+ <Input
98+ className='mt-8 pt-8 text-mono'
99+ prop-size='md'
100+ prop-format='innerLabel'
101+ name='hdpath'
102+ value={ hdpath }
103+ autocomplete='off'
104+ onChange={ this.handleHdPathInput }>
105+ <InnerLabel>{'HD Path'}</InnerLabel>
106+ </Input>
107+ );
57108
58109 return (
59110 <Row>
@@ -66,16 +117,8 @@ class Trezor extends React.Component {
66117 }
67118 </P>
68119
69− <Input
70− className='pt-8 text-mono'
71− prop-size='md'
72− prop-format='innerLabel'
73− name='hdpath'
74− value={ hdpath }
75− autocomplete='off'
76− onChange={ this.handleHdPathInput }>
77− <InnerLabel>{'HD Path'}</InnerLabel>
78− </Input>
120+ { accountSelection }
121+ { pathSelection }
79122
80123 <Button
81124 className={'mt-8'}