Skip to content

Commit

Permalink
app: Add DerivationPath dropdown
Browse files Browse the repository at this point in the history
  • Loading branch information
matevz committed Mar 24, 2022
1 parent d1f957a commit 0271de2
Show file tree
Hide file tree
Showing 5 changed files with 64 additions and 26 deletions.
23 changes: 14 additions & 9 deletions src/app/lib/ledger.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Ledger, LedgerSigner } from './ledger'
import { DerivationPathLegacy, DerivationPathAdr8, Ledger, LedgerSigner } from './ledger'
import OasisApp from '@oasisprotocol/ledger'
import { WalletError, WalletErrors } from 'types/errors'
import { Wallet, WalletType } from 'app/state/wallet/types'
Expand All @@ -18,18 +18,23 @@ describe('Ledger Library', () => {
describe('Ledger', () => {
it('enumerateAccounts should pass when Oasis App is open', async () => {
mockAppIsOpen('Oasis')
const accounts = Ledger.enumerateAccounts({} as any, 0)
const accounts = Ledger.enumerateAccounts({} as any, DerivationPathLegacy, 0)
await expect(accounts).resolves.toEqual([])
})
it('enumerateAccounts should pass when Oasis App is open', async () => {
mockAppIsOpen('Oasis')
const accounts = Ledger.enumerateAccounts({} as any, DerivationPathAdr8, 0)
await expect(accounts).resolves.toEqual([])
})

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

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

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

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

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

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

const signer = new LedgerSigner({
type: WalletType.Ledger,
path: [44, 474, 0, 0, 0],
path: Ledger.mustGetPath(DerivationPathLegacy, 0),
publicKey: 'aabbcc',
} as Wallet)

Expand Down
26 changes: 15 additions & 11 deletions src/app/lib/ledger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ import { WalletError, WalletErrors } from 'types/errors'
import { hex2uint } from './helpers'
import type Transport from '@ledgerhq/hw-transport'

export const DerivationPathAdr8 = 'adr8';
export const DerivationPathLegacy = 'legacy';

interface Response {
return_code: number
error_message: string
Expand Down Expand Up @@ -34,27 +37,28 @@ const successOrThrow = (response: Response, message: string) => {
}

export class Ledger {
public static async enumerateAccounts(transport: Transport, pathType: string, count = 5) {
const accounts: LedgerAccount[] = []

var pathBase;
public static mustGetPath(pathType: string, i: number) {
switch (pathType) {
case "adr8":
pathBase = [44, 474];
break;
case "legacy":
pathBase = [44, 474, 0, 0];
break;
case DerivationPathAdr8:
return [44, 474, i];
case DerivationPathLegacy:
return [44, 474, 0, 0, i];
}

throw new TypeError("invalid pathType: "+pathType);
}

public static async enumerateAccounts(transport: Transport, pathType: string, count = 5) {
const accounts: LedgerAccount[] = []

try {
const app = new OasisApp(transport)
const appInfo = successOrThrow(await app.appInfo(), 'ledger app info')
if (appInfo.appName !== 'Oasis') {
throw new WalletError(WalletErrors.LedgerOasisAppIsNotOpen, 'Oasis App is not open')
}
for (let i = 0; i < count; i++) {
const path = pathBase.concat(i)
const path = Ledger.mustGetPath(pathType, i);
const publicKeyResponse = successOrThrow(await app.publicKey(path), 'ledger public key')
accounts.push({ path, publicKey: new Uint8Array(publicKeyResponse.pk as Buffer) })
}
Expand Down
11 changes: 10 additions & 1 deletion src/app/pages/OpenWalletPage/Features/FromLedger/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,13 @@ import { ErrorFormatter } from 'app/components/ErrorFormatter'
import { LedgerStepFormatter } from 'app/components/LedgerStepFormatter'
import { ResponsiveLayer } from 'app/components/ResponsiveLayer'
import { Account } from 'app/components/Toolbar/Features/AccountSelector'
import { DerivationPathAdr8, DerivationPathLegacy } from 'app/lib/ledger'
import { ledgerActions, useLedgerSlice } from 'app/state/ledger'
import { selectLedger, selectSelectedLedgerAccounts } from 'app/state/ledger/selectors'
import { LedgerAccount, LedgerStep } from 'app/state/ledger/types'
import { useWalletSlice } from 'app/state/wallet'
import { WalletType } from 'app/state/wallet/types'
import { Box, Button, Heading, Spinner, Text } from 'grommet'
import { Box, Button, Heading, Select, Spinner, Text } from 'grommet'
import * as React from 'react'
import { useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'
Expand Down Expand Up @@ -118,13 +119,21 @@ export function FromLedgerModal(props: FromLedgerModalProps) {

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

return (
<ResponsiveLayer position="center" modal>
<Box width="750px" pad="medium" background="background-front">
<Heading size="1" margin={{ bottom: 'medium', top: 'none' }}>
{t('openWallet.ledger.selectWallets', 'Select the wallets to open')}
</Heading>
<Select
id='DerivationPathSelect'
options={[ DerivationPathAdr8, DerivationPathLegacy ]}
value={DerivationPathAdr8}
onChange={({ value: nextValue }) => setValue(nextValue)}
/>
{ledger.step && ledger.step !== LedgerStep.Done && (
<Box direction="row" gap="medium" alignContent="center">
<Spinner size="medium" />
Expand Down
25 changes: 22 additions & 3 deletions src/app/state/ledger/saga.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { expectSaga } from 'redux-saga-test-plan'
import { ledgerActions } from '.'
import { ledgerSaga, sign } from './saga'
import * as matchers from 'redux-saga-test-plan/matchers'
import { Ledger, LedgerSigner } from 'app/lib/ledger'
import {DerivationPathAdr8, DerivationPathLegacy, Ledger, LedgerSigner} from 'app/lib/ledger'
import { getBalance } from '../wallet/saga'
import { addressToPublicKey } from 'app/lib/helpers'
import { LedgerStep } from './types'
Expand All @@ -12,10 +12,29 @@ import { OasisTransaction } from 'app/lib/transaction'

describe('Ledger Sagas', () => {
describe('enumerateAccounts', () => {
it('should list accounts', async () => {
it('should list ADR8 accounts', async () => {
const validAccount = {
publicKey: await addressToPublicKey('oasis1qz0k5q8vjqvu4s4nwxyj406ylnflkc4vrcjghuwk'),
path: [44, 474, 0, 0, 0],
path: Ledger.mustGetPath(DerivationPathAdr8, 0),
}

return expectSaga(ledgerSaga)
.provide([
[matchers.call.fn(TransportWebUSB.isSupported), true],
[matchers.call.fn(TransportWebUSB.create), { close: () => {} }],
[matchers.call.fn(Ledger.enumerateAccounts), [validAccount]],
[matchers.call.fn(getBalance), {}],
])
.dispatch(ledgerActions.enumerateAccounts())
.put(ledgerActions.setStep(LedgerStep.Done))
.put.actionType(ledgerActions.accountsListed.type)
.run(50)
})

it('should list legacy accounts', async () => {
const validAccount = {
publicKey: await addressToPublicKey('oasis1qz0k5q8vjqvu4s4nwxyj406ylnflkc4vrcjghuwk'),
path: Ledger.mustGetPath(DerivationPathLegacy, 0),
}

return expectSaga(ledgerSaga)
Expand Down
5 changes: 3 additions & 2 deletions src/app/state/ledger/saga.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
import TransportWebUSB from '@ledgerhq/hw-transport-webusb'
import * as oasis from '@oasisprotocol/client'
import { publicKeyToAddress, uint2hex } from 'app/lib/helpers'
import { Ledger, LedgerSigner } from 'app/lib/ledger'
import {DerivationPathAdr8, Ledger, LedgerSigner} from 'app/lib/ledger'
import { OasisTransaction } from 'app/lib/transaction'
import { all, call, put, select, takeEvery } from 'typed-redux-saga'
import { ErrorPayload, WalletError, WalletErrors } from 'types/errors'
Expand Down Expand Up @@ -41,7 +41,8 @@ function* enumerateAccounts() {
transport = yield* getUSBTransport()

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

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

0 comments on commit 0271de2

Please sign in to comment.