Skip to content

Commit 0271de2

Browse files
committed
app: Add DerivationPath dropdown
1 parent d1f957a commit 0271de2

5 files changed

Lines changed: 64 additions & 26 deletions

File tree

src/app/lib/ledger.test.ts

Lines changed: 14 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { Ledger, LedgerSigner } from './ledger'
1+
import { DerivationPathLegacy, DerivationPathAdr8, Ledger, LedgerSigner } from './ledger'
22
import OasisApp from '@oasisprotocol/ledger'
33
import { WalletError, WalletErrors } from 'types/errors'
44
import { Wallet, WalletType } from 'app/state/wallet/types'
@@ -18,18 +18,23 @@ describe('Ledger Library', () => {
1818
describe('Ledger', () => {
1919
it('enumerateAccounts should pass when Oasis App is open', async () => {
2020
mockAppIsOpen('Oasis')
21-
const accounts = Ledger.enumerateAccounts({} as any, 0)
21+
const accounts = Ledger.enumerateAccounts({} as any, DerivationPathLegacy, 0)
22+
await expect(accounts).resolves.toEqual([])
23+
})
24+
it('enumerateAccounts should pass when Oasis App is open', async () => {
25+
mockAppIsOpen('Oasis')
26+
const accounts = Ledger.enumerateAccounts({} as any, DerivationPathAdr8, 0)
2227
await expect(accounts).resolves.toEqual([])
2328
})
2429

2530
it('Should catch "Oasis App is not open"', async () => {
2631
mockAppIsOpen('BOLOS')
27-
const accountsMainMenu = Ledger.enumerateAccounts({} as any, 0)
32+
const accountsMainMenu = Ledger.enumerateAccounts({} as any, DerivationPathLegacy, 0)
2833
await expect(accountsMainMenu).rejects.toThrowError(WalletError)
2934
await expect(accountsMainMenu).rejects.toHaveProperty('type', WalletErrors.LedgerOasisAppIsNotOpen)
3035

3136
mockAppIsOpen('Ethereum')
32-
const accountsEth = Ledger.enumerateAccounts({} as any, 0)
37+
const accountsEth = Ledger.enumerateAccounts({} as any, DerivationPathLegacy, 0)
3338
await expect(accountsEth).rejects.toThrowError(WalletError)
3439
await expect(accountsEth).rejects.toHaveProperty('type', WalletErrors.LedgerOasisAppIsNotOpen)
3540
})
@@ -40,7 +45,7 @@ describe('Ledger Library', () => {
4045
pubKey.mockResolvedValueOnce({ return_code: 0x9000, pk: Buffer.from(new Uint8Array([1, 2, 3])) })
4146
pubKey.mockResolvedValueOnce({ return_code: 0x9000, pk: Buffer.from(new Uint8Array([4, 5, 6])) })
4247

43-
const accounts = await Ledger.enumerateAccounts({} as any, 2)
48+
const accounts = await Ledger.enumerateAccounts({} as any, DerivationPathLegacy, 2)
4449
expect(accounts).toHaveLength(2)
4550
expect(accounts).toContainEqual({ path: [44, 474, 0, 0, 0], publicKey: new Uint8Array([1, 2, 3]) })
4651
expect(accounts).toContainEqual({ path: [44, 474, 0, 0, 1], publicKey: new Uint8Array([4, 5, 6]) })
@@ -51,7 +56,7 @@ describe('Ledger Library', () => {
5156
const pubKey: jest.Mock<any> = OasisApp.prototype.publicKey
5257
pubKey.mockResolvedValueOnce({ return_code: 0x6804 })
5358

54-
const accounts = Ledger.enumerateAccounts({} as any)
59+
const accounts = Ledger.enumerateAccounts({} as any, DerivationPathLegacy)
5560
await expect(accounts).rejects.toThrowError(WalletError)
5661
await expect(accounts).rejects.toHaveProperty('type', WalletErrors.LedgerCannotOpenOasisApp)
5762
})
@@ -61,7 +66,7 @@ describe('Ledger Library', () => {
6166
const pubKey: jest.Mock<any> = OasisApp.prototype.publicKey
6267
pubKey.mockResolvedValueOnce({ return_code: 0x6400 })
6368

64-
const accounts = Ledger.enumerateAccounts({} as any)
69+
const accounts = Ledger.enumerateAccounts({} as any, DerivationPathLegacy)
6570
await expect(accounts).rejects.toThrowError(WalletError)
6671
await expect(accounts).rejects.toHaveProperty('type', WalletErrors.LedgerAppVersionNotSupported)
6772
})
@@ -71,7 +76,7 @@ describe('Ledger Library', () => {
7176
const pubKey: jest.Mock<any> = OasisApp.prototype.publicKey
7277
pubKey.mockResolvedValueOnce({ return_code: -1, error_message: 'unknown dummy error' })
7378

74-
const accounts = Ledger.enumerateAccounts({} as any)
79+
const accounts = Ledger.enumerateAccounts({} as any, DerivationPathLegacy)
7580
await expect(accounts).rejects.toThrowError(WalletError)
7681
await expect(accounts).rejects.toThrow(/unknown dummy error/)
7782
await expect(accounts).rejects.toHaveProperty('type', WalletErrors.LedgerUnknownError)
@@ -111,7 +116,7 @@ describe('Ledger Library', () => {
111116

112117
const signer = new LedgerSigner({
113118
type: WalletType.Ledger,
114-
path: [44, 474, 0, 0, 0],
119+
path: Ledger.mustGetPath(DerivationPathLegacy, 0),
115120
publicKey: 'aabbcc',
116121
} as Wallet)
117122

src/app/lib/ledger.ts

Lines changed: 15 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,9 @@ import { WalletError, WalletErrors } from 'types/errors'
55
import { hex2uint } from './helpers'
66
import type Transport from '@ledgerhq/hw-transport'
77

8+
export const DerivationPathAdr8 = 'adr8';
9+
export const DerivationPathLegacy = 'legacy';
10+
811
interface Response {
912
return_code: number
1013
error_message: string
@@ -34,27 +37,28 @@ const successOrThrow = (response: Response, message: string) => {
3437
}
3538

3639
export class Ledger {
37-
public static async enumerateAccounts(transport: Transport, pathType: string, count = 5) {
38-
const accounts: LedgerAccount[] = []
39-
40-
var pathBase;
40+
public static mustGetPath(pathType: string, i: number) {
4141
switch (pathType) {
42-
case "adr8":
43-
pathBase = [44, 474];
44-
break;
45-
case "legacy":
46-
pathBase = [44, 474, 0, 0];
47-
break;
42+
case DerivationPathAdr8:
43+
return [44, 474, i];
44+
case DerivationPathLegacy:
45+
return [44, 474, 0, 0, i];
4846
}
4947

48+
throw new TypeError("invalid pathType: "+pathType);
49+
}
50+
51+
public static async enumerateAccounts(transport: Transport, pathType: string, count = 5) {
52+
const accounts: LedgerAccount[] = []
53+
5054
try {
5155
const app = new OasisApp(transport)
5256
const appInfo = successOrThrow(await app.appInfo(), 'ledger app info')
5357
if (appInfo.appName !== 'Oasis') {
5458
throw new WalletError(WalletErrors.LedgerOasisAppIsNotOpen, 'Oasis App is not open')
5559
}
5660
for (let i = 0; i < count; i++) {
57-
const path = pathBase.concat(i)
61+
const path = Ledger.mustGetPath(pathType, i);
5862
const publicKeyResponse = successOrThrow(await app.publicKey(path), 'ledger public key')
5963
accounts.push({ path, publicKey: new Uint8Array(publicKeyResponse.pk as Buffer) })
6064
}

src/app/pages/OpenWalletPage/Features/FromLedger/index.tsx

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,13 @@ import { ErrorFormatter } from 'app/components/ErrorFormatter'
77
import { LedgerStepFormatter } from 'app/components/LedgerStepFormatter'
88
import { ResponsiveLayer } from 'app/components/ResponsiveLayer'
99
import { Account } from 'app/components/Toolbar/Features/AccountSelector'
10+
import { DerivationPathAdr8, DerivationPathLegacy } from 'app/lib/ledger'
1011
import { ledgerActions, useLedgerSlice } from 'app/state/ledger'
1112
import { selectLedger, selectSelectedLedgerAccounts } from 'app/state/ledger/selectors'
1213
import { LedgerAccount, LedgerStep } from 'app/state/ledger/types'
1314
import { useWalletSlice } from 'app/state/wallet'
1415
import { WalletType } from 'app/state/wallet/types'
15-
import { Box, Button, Heading, Spinner, Text } from 'grommet'
16+
import { Box, Button, Heading, Select, Spinner, Text } from 'grommet'
1617
import * as React from 'react'
1718
import { useEffect, useState } from 'react'
1819
import { useTranslation } from 'react-i18next'
@@ -118,13 +119,21 @@ export function FromLedgerModal(props: FromLedgerModalProps) {
118119

119120
const cancelDisabled = ledger.step === LedgerStep.Done || error ? false : true
120121
const confirmDisabled = ledger.step !== LedgerStep.Done || selectedAccounts.length === 0
122+
// TODO
123+
const [value, setValue] = useState('');
121124

122125
return (
123126
<ResponsiveLayer position="center" modal>
124127
<Box width="750px" pad="medium" background="background-front">
125128
<Heading size="1" margin={{ bottom: 'medium', top: 'none' }}>
126129
{t('openWallet.ledger.selectWallets', 'Select the wallets to open')}
127130
</Heading>
131+
<Select
132+
id='DerivationPathSelect'
133+
options={[ DerivationPathAdr8, DerivationPathLegacy ]}
134+
value={DerivationPathAdr8}
135+
onChange={({ value: nextValue }) => setValue(nextValue)}
136+
/>
128137
{ledger.step && ledger.step !== LedgerStep.Done && (
129138
<Box direction="row" gap="medium" alignContent="center">
130139
<Spinner size="medium" />

src/app/state/ledger/saga.test.ts

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { expectSaga } from 'redux-saga-test-plan'
33
import { ledgerActions } from '.'
44
import { ledgerSaga, sign } from './saga'
55
import * as matchers from 'redux-saga-test-plan/matchers'
6-
import { Ledger, LedgerSigner } from 'app/lib/ledger'
6+
import {DerivationPathAdr8, DerivationPathLegacy, Ledger, LedgerSigner} from 'app/lib/ledger'
77
import { getBalance } from '../wallet/saga'
88
import { addressToPublicKey } from 'app/lib/helpers'
99
import { LedgerStep } from './types'
@@ -12,10 +12,29 @@ import { OasisTransaction } from 'app/lib/transaction'
1212

1313
describe('Ledger Sagas', () => {
1414
describe('enumerateAccounts', () => {
15-
it('should list accounts', async () => {
15+
it('should list ADR8 accounts', async () => {
1616
const validAccount = {
1717
publicKey: await addressToPublicKey('oasis1qz0k5q8vjqvu4s4nwxyj406ylnflkc4vrcjghuwk'),
18-
path: [44, 474, 0, 0, 0],
18+
path: Ledger.mustGetPath(DerivationPathAdr8, 0),
19+
}
20+
21+
return expectSaga(ledgerSaga)
22+
.provide([
23+
[matchers.call.fn(TransportWebUSB.isSupported), true],
24+
[matchers.call.fn(TransportWebUSB.create), { close: () => {} }],
25+
[matchers.call.fn(Ledger.enumerateAccounts), [validAccount]],
26+
[matchers.call.fn(getBalance), {}],
27+
])
28+
.dispatch(ledgerActions.enumerateAccounts())
29+
.put(ledgerActions.setStep(LedgerStep.Done))
30+
.put.actionType(ledgerActions.accountsListed.type)
31+
.run(50)
32+
})
33+
34+
it('should list legacy accounts', async () => {
35+
const validAccount = {
36+
publicKey: await addressToPublicKey('oasis1qz0k5q8vjqvu4s4nwxyj406ylnflkc4vrcjghuwk'),
37+
path: Ledger.mustGetPath(DerivationPathLegacy, 0),
1938
}
2039

2140
return expectSaga(ledgerSaga)

src/app/state/ledger/saga.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
import TransportWebUSB from '@ledgerhq/hw-transport-webusb'
33
import * as oasis from '@oasisprotocol/client'
44
import { publicKeyToAddress, uint2hex } from 'app/lib/helpers'
5-
import { Ledger, LedgerSigner } from 'app/lib/ledger'
5+
import {DerivationPathAdr8, Ledger, LedgerSigner} from 'app/lib/ledger'
66
import { OasisTransaction } from 'app/lib/transaction'
77
import { all, call, put, select, takeEvery } from 'typed-redux-saga'
88
import { ErrorPayload, WalletError, WalletErrors } from 'types/errors'
@@ -41,7 +41,8 @@ function* enumerateAccounts() {
4141
transport = yield* getUSBTransport()
4242

4343
yield* setStep(LedgerStep.LoadingAccounts)
44-
const accounts = yield* call(Ledger.enumerateAccounts, PathSelect.value, transport)
44+
// TODO: Pick the selected derivation path.
45+
const accounts = yield* call(Ledger.enumerateAccounts, transport, DerivationPathAdr8)
4546

4647
const balances = yield* all(accounts.map(a => call(getBalance, a.publicKey)))
4748
const addresses = yield* all(accounts.map(a => call(publicKeyToAddress, a.publicKey)))

0 commit comments

Comments
 (0)