diff --git a/package.json b/package.json index bdf65527a0..2d3f96b8cf 100644 --- a/package.json +++ b/package.json @@ -71,7 +71,7 @@ "dependencies": { "@ledgerhq/hw-transport-webusb": "6.20.0", "@oasisprotocol/client": "0.1.0-alpha8", - "@oasisprotocol/ledger": "0.2.1", + "@oasisprotocol/ledger": "0.2.2", "@reduxjs/toolkit": "1.7.1", "babel-jest": "27.5.1", "babel-plugin-istanbul": "6.1.1", diff --git a/src/app/components/Toolbar/Features/AccountSelector/index.tsx b/src/app/components/Toolbar/Features/AccountSelector/index.tsx index 0339236750..5ea60d2d47 100644 --- a/src/app/components/Toolbar/Features/AccountSelector/index.tsx +++ b/src/app/components/Toolbar/Features/AccountSelector/index.tsx @@ -75,7 +75,7 @@ export const Account = memo((props: AccountProps) => { - {walletTypes[props.type]} {props.details && ({props.details})} + {walletTypes[props.type]} {props.details &&  ({props.details})} diff --git a/src/app/lib/ledger.test.ts b/src/app/lib/ledger.test.ts index 97fb2c45bf..e42fadd329 100644 --- a/src/app/lib/ledger.test.ts +++ b/src/app/lib/ledger.test.ts @@ -1,4 +1,4 @@ -import { Ledger, LedgerSigner } from './ledger' +import { DerivationPathTypeLegacy, DerivationPathTypeAdr8, Ledger, LedgerSigner } from './ledger' import OasisApp from '@oasisprotocol/ledger' import { WalletError, WalletErrors } from 'types/errors' import { Wallet, WalletType } from 'app/state/wallet/types' @@ -18,29 +18,46 @@ 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, DerivationPathTypeLegacy, 0) + await expect(accounts).resolves.toEqual([]) + }) + it('enumerateAccounts should pass when Oasis App is open', async () => { + mockAppIsOpen('Oasis') + const accounts = Ledger.enumerateAccounts({} as any, DerivationPathTypeAdr8, 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, DerivationPathTypeLegacy, 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, DerivationPathTypeLegacy, 0) await expect(accountsEth).rejects.toThrowError(WalletError) await expect(accountsEth).rejects.toHaveProperty('type', WalletErrors.LedgerOasisAppIsNotOpen) }) - it('Should enumerate and return the accounts', async () => { + it('Should enumerate and return adr8 accounts', async () => { + mockAppIsOpen('Oasis') + const pubKey: jest.Mock = OasisApp.prototype.publicKey + 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, DerivationPathTypeAdr8, 2) + expect(accounts).toHaveLength(2) + expect(accounts).toContainEqual({ path: [44, 474, 0], publicKey: new Uint8Array([1, 2, 3]) }) + expect(accounts).toContainEqual({ path: [44, 474, 1], publicKey: new Uint8Array([4, 5, 6]) }) + }) + + it('Should enumerate and return legacy accounts', async () => { mockAppIsOpen('Oasis') const pubKey: jest.Mock = OasisApp.prototype.publicKey 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, DerivationPathTypeLegacy, 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]) }) @@ -51,7 +68,7 @@ describe('Ledger Library', () => { const pubKey: jest.Mock = OasisApp.prototype.publicKey pubKey.mockResolvedValueOnce({ return_code: 0x6804 }) - const accounts = Ledger.enumerateAccounts({} as any) + const accounts = Ledger.enumerateAccounts({} as any, DerivationPathTypeLegacy) await expect(accounts).rejects.toThrowError(WalletError) await expect(accounts).rejects.toHaveProperty('type', WalletErrors.LedgerCannotOpenOasisApp) }) @@ -61,7 +78,7 @@ describe('Ledger Library', () => { const pubKey: jest.Mock = OasisApp.prototype.publicKey pubKey.mockResolvedValueOnce({ return_code: 0x6400 }) - const accounts = Ledger.enumerateAccounts({} as any) + const accounts = Ledger.enumerateAccounts({} as any, DerivationPathTypeLegacy) await expect(accounts).rejects.toThrowError(WalletError) await expect(accounts).rejects.toHaveProperty('type', WalletErrors.LedgerAppVersionNotSupported) }) @@ -71,7 +88,7 @@ describe('Ledger Library', () => { const pubKey: jest.Mock = 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, DerivationPathTypeLegacy) await expect(accounts).rejects.toThrowError(WalletError) await expect(accounts).rejects.toThrow(/unknown dummy error/) await expect(accounts).rejects.toHaveProperty('type', WalletErrors.LedgerUnknownError) @@ -96,7 +113,7 @@ describe('Ledger Library', () => { it('Should fail without USB transport', () => { const signer = new LedgerSigner({ type: WalletType.Ledger, - path: [44, 474, 0, 0, 0], + path: Ledger.mustGetPath(DerivationPathTypeAdr8, 0), publicKey: '00', } as Wallet) @@ -111,7 +128,7 @@ describe('Ledger Library', () => { const signer = new LedgerSigner({ type: WalletType.Ledger, - path: [44, 474, 0, 0, 0], + path: Ledger.mustGetPath(DerivationPathTypeAdr8, 0), publicKey: 'aabbcc', } as Wallet) @@ -124,7 +141,7 @@ describe('Ledger Library', () => { const signer = new LedgerSigner({ type: WalletType.Ledger, - path: [44, 474, 0, 0, 0], + path: Ledger.mustGetPath(DerivationPathTypeAdr8, 0), publicKey: '00', } as Wallet) @@ -140,7 +157,7 @@ describe('Ledger Library', () => { const signer = new LedgerSigner({ type: WalletType.Ledger, - path: [44, 474, 0, 0, 0], + path: Ledger.mustGetPath(DerivationPathTypeAdr8, 0), publicKey: '00', } as Wallet) diff --git a/src/app/lib/ledger.ts b/src/app/lib/ledger.ts index c5f944ec67..eb5309e739 100644 --- a/src/app/lib/ledger.ts +++ b/src/app/lib/ledger.ts @@ -5,6 +5,9 @@ import { WalletError, WalletErrors } from 'types/errors' import { hex2uint } from './helpers' import type Transport from '@ledgerhq/hw-transport' +export const DerivationPathTypeAdr8 = 'adr8' +export const DerivationPathTypeLegacy = 'legacy' + interface Response { return_code: number error_message: string @@ -34,7 +37,18 @@ const successOrThrow = (response: Response, message: string) => { } export class Ledger { - public static async enumerateAccounts(transport: Transport, count = 5) { + public static mustGetPath(pathType: string, i: number) { + switch (pathType) { + case DerivationPathTypeAdr8: + return [44, 474, i] + case DerivationPathTypeLegacy: + 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 { @@ -44,7 +58,7 @@ export class Ledger { throw new WalletError(WalletErrors.LedgerOasisAppIsNotOpen, 'Oasis App is not open') } for (let i = 0; i < count; i++) { - const path = [44, 474, 0, 0, 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) }) } diff --git a/src/app/pages/OpenWalletPage/Features/FromLedger/__tests__/__snapshots__/index.test.tsx.snap b/src/app/pages/OpenWalletPage/Features/FromLedger/__tests__/__snapshots__/index.test.tsx.snap index 19751f1a92..470bd034f0 100644 --- a/src/app/pages/OpenWalletPage/Features/FromLedger/__tests__/__snapshots__/index.test.tsx.snap +++ b/src/app/pages/OpenWalletPage/Features/FromLedger/__tests__/__snapshots__/index.test.tsx.snap @@ -3,7 +3,41 @@ exports[` should match snapshot 1`] = ` Object { "asFragment": [Function], - "baseElement": .c5 { + "baseElement": .c18 { + display: inline-block; + -webkit-flex: 0 0 auto; + -ms-flex: 0 0 auto; + flex: 0 0 auto; + width: 24px; + height: 24px; + fill: #0092f6; + stroke: #0092f6; +} + +.c18 g { + fill: inherit; + stroke: inherit; +} + +.c18 *:not([stroke])[fill="none"] { + stroke-width: 0; +} + +.c18 *[stroke*="#"], +.c18 *[STROKE*="#"] { + stroke: inherit; + fill: none; +} + +.c18 *[fill-rule], +.c18 *[FILL-RULE], +.c18 *[fill*="#"], +.c18 *[FILL*="#"] { + fill: inherit; + stroke: none; +} + +.c5 { display: -webkit-box; display: -webkit-flex; display: -ms-flexbox; @@ -22,6 +56,119 @@ Object { } .c7 { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + box-sizing: border-box; + max-width: 100%; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + min-width: 0; + min-height: 0; + -webkit-flex-direction: row; + -ms-flex-direction: row; + flex-direction: row; + height: 100%; + -webkit-box-pack: end; + -webkit-justify-content: flex-end; + -ms-flex-pack: end; + justify-content: flex-end; +} + +.c8 { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + box-sizing: border-box; + max-width: 100%; + min-width: 0; + min-height: 0; + -webkit-flex-direction: column; + -ms-flex-direction: column; + flex-direction: column; + padding-right: 5px; +} + +.c9 { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + box-sizing: border-box; + max-width: 100%; + min-width: 0; + min-height: 0; + -webkit-flex-direction: column; + -ms-flex-direction: column; + flex-direction: column; + width: 440px; +} + +.c12 { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + box-sizing: border-box; + max-width: 100%; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + min-width: 0; + min-height: 0; + -webkit-flex-direction: row; + -ms-flex-direction: row; + flex-direction: row; + -webkit-box-pack: justify; + -webkit-justify-content: space-between; + -ms-flex-pack: justify; + justify-content: space-between; +} + +.c13 { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + box-sizing: border-box; + min-width: 0; + min-height: 0; + -webkit-flex-direction: row; + -ms-flex-direction: row; + flex-direction: row; + -webkit-flex: 1 1; + -ms-flex: 1 1; + flex: 1 1; + -webkit-flex-basis: auto; + -ms-flex-preferred-size: auto; + flex-basis: auto; +} + +.c17 { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + box-sizing: border-box; + max-width: 100%; + margin-left: 12px; + margin-right: 12px; + min-width: 0; + min-height: 0; + -webkit-flex-direction: column; + -ms-flex-direction: column; + flex-direction: column; + -webkit-flex: 0 0 auto; + -ms-flex: 0 0 auto; + flex: 0 0 auto; +} + +.c21 { display: -webkit-box; display: -webkit-flex; display: -ms-flexbox; @@ -39,7 +186,7 @@ Object { padding-top: 48px; } -.c9 { +.c23 { -webkit-flex: 0 0 auto; -ms-flex: 0 0 auto; flex: 0 0 auto; @@ -49,7 +196,75 @@ Object { width: 12px; } -.c8 { +.c19 { + -webkit-align-self: flex-end; + -ms-flex-item-align: end; + align-self: flex-end; + font-size: 14px; + line-height: 20px; +} + +.c10 { + display: inline-block; + box-sizing: border-box; + cursor: pointer; + font: inherit; + -webkit-text-decoration: none; + text-decoration: none; + margin: 0; + background: transparent; + overflow: visible; + text-transform: none; + color: inherit; + outline: none; + border: none; + padding: 0; + text-align: inherit; + opacity: 0.3; + cursor: default; +} + +.c10:focus { + outline: none; + box-shadow: 0 0 2px 2px #6FFFB0; +} + +.c10:focus > circle, +.c10:focus > ellipse, +.c10:focus > line, +.c10:focus > path, +.c10:focus > polygon, +.c10:focus > polyline, +.c10:focus > rect { + outline: none; + box-shadow: 0 0 2px 2px #6FFFB0; +} + +.c10:focus::-moz-focus-inner { + border: 0; +} + +.c10:focus:not(:focus-visible) { + outline: none; + box-shadow: none; +} + +.c10:focus:not(:focus-visible) > circle, +.c10:focus:not(:focus-visible) > ellipse, +.c10:focus:not(:focus-visible) > line, +.c10:focus:not(:focus-visible) > path, +.c10:focus:not(:focus-visible) > polygon, +.c10:focus:not(:focus-visible) > polyline, +.c10:focus:not(:focus-visible) > rect { + outline: none; + box-shadow: none; +} + +.c10:focus:not(:focus-visible)::-moz-focus-inner { + border: 0; +} + +.c22 { display: inline-block; box-sizing: border-box; cursor: pointer; @@ -77,47 +292,47 @@ Object { font-weight: bold; } -.c8:focus { +.c22:focus { outline: none; box-shadow: 0 0 2px 2px #6FFFB0; } -.c8:focus > circle, -.c8:focus > ellipse, -.c8:focus > line, -.c8:focus > path, -.c8:focus > polygon, -.c8:focus > polyline, -.c8:focus > rect { +.c22:focus > circle, +.c22:focus > ellipse, +.c22:focus > line, +.c22:focus > path, +.c22:focus > polygon, +.c22:focus > polyline, +.c22:focus > rect { outline: none; box-shadow: 0 0 2px 2px #6FFFB0; } -.c8:focus::-moz-focus-inner { +.c22:focus::-moz-focus-inner { border: 0; } -.c8:focus:not(:focus-visible) { +.c22:focus:not(:focus-visible) { outline: none; box-shadow: none; } -.c8:focus:not(:focus-visible) > circle, -.c8:focus:not(:focus-visible) > ellipse, -.c8:focus:not(:focus-visible) > line, -.c8:focus:not(:focus-visible) > path, -.c8:focus:not(:focus-visible) > polygon, -.c8:focus:not(:focus-visible) > polyline, -.c8:focus:not(:focus-visible) > rect { +.c22:focus:not(:focus-visible) > circle, +.c22:focus:not(:focus-visible) > ellipse, +.c22:focus:not(:focus-visible) > line, +.c22:focus:not(:focus-visible) > path, +.c22:focus:not(:focus-visible) > polygon, +.c22:focus:not(:focus-visible) > polyline, +.c22:focus:not(:focus-visible) > rect { outline: none; box-shadow: none; } -.c8:focus:not(:focus-visible)::-moz-focus-inner { +.c22:focus:not(:focus-visible)::-moz-focus-inner { border: 0; } -.c10 { +.c24 { display: inline-block; box-sizing: border-box; cursor: pointer; @@ -151,43 +366,43 @@ Object { font-weight: bold; } -.c10:focus { +.c24:focus { outline: none; box-shadow: 0 0 2px 2px #6FFFB0; } -.c10:focus > circle, -.c10:focus > ellipse, -.c10:focus > line, -.c10:focus > path, -.c10:focus > polygon, -.c10:focus > polyline, -.c10:focus > rect { +.c24:focus > circle, +.c24:focus > ellipse, +.c24:focus > line, +.c24:focus > path, +.c24:focus > polygon, +.c24:focus > polyline, +.c24:focus > rect { outline: none; box-shadow: 0 0 2px 2px #6FFFB0; } -.c10:focus::-moz-focus-inner { +.c24:focus::-moz-focus-inner { border: 0; } -.c10:focus:not(:focus-visible) { +.c24:focus:not(:focus-visible) { outline: none; box-shadow: none; } -.c10:focus:not(:focus-visible) > circle, -.c10:focus:not(:focus-visible) > ellipse, -.c10:focus:not(:focus-visible) > line, -.c10:focus:not(:focus-visible) > path, -.c10:focus:not(:focus-visible) > polygon, -.c10:focus:not(:focus-visible) > polyline, -.c10:focus:not(:focus-visible) > rect { +.c24:focus:not(:focus-visible) > circle, +.c24:focus:not(:focus-visible) > ellipse, +.c24:focus:not(:focus-visible) > line, +.c24:focus:not(:focus-visible) > path, +.c24:focus:not(:focus-visible) > polygon, +.c24:focus:not(:focus-visible) > polyline, +.c24:focus:not(:focus-visible) > rect { outline: none; box-shadow: none; } -.c10:focus:not(:focus-visible)::-moz-focus-inner { +.c24:focus:not(:focus-visible)::-moz-focus-inner { border: 0; } @@ -200,6 +415,71 @@ Object { font-weight: 600; } +.c20 { + box-sizing: border-box; + font-size: inherit; + line-height: inherit; + color: #0092f6; + font-weight: 600; + -webkit-text-decoration: none; + text-decoration: none; + cursor: pointer; +} + +.c20:hover { + -webkit-text-decoration: underline; + text-decoration: underline; +} + +.c15 { + box-sizing: border-box; + font-size: inherit; + font-family: inherit; + border: none; + -webkit-appearance: none; + background: transparent; + color: inherit; + width: 100%; + padding: 11px; + font-weight: 600; + margin: 0; + border: 1px solid rgba(0,0,0,0.33); + border-radius: 4px; + outline: none; + border: none; +} + +.c15::-webkit-input-placeholder { + color: #AAAAAA; +} + +.c15::-moz-placeholder { + color: #AAAAAA; +} + +.c15:-ms-input-placeholder { + color: #AAAAAA; +} + +.c15::-webkit-search-decoration { + -webkit-appearance: none; +} + +.c15::-moz-focus-inner { + border: none; + outline: none; +} + +.c15:-moz-placeholder, +.c15::-moz-placeholder { + opacity: 1; +} + +.c14 { + position: relative; + width: 100%; +} + .c0 { font-family: Rubik; font-size: 18px; @@ -282,6 +562,15 @@ Object { position: absolute; } +.c16 { + cursor: default; +} + +.c11 { + border: 1px solid rgba(0,0,0,0.33); + border-radius: 4px; +} + @media only screen and (max-width:768px) { .c5 { padding: 12px; @@ -289,13 +578,26 @@ Object { } @media only screen and (max-width:768px) { - .c7 { + .c8 { + padding-right: 5px; + } +} + +@media only screen and (max-width:768px) { + .c17 { + margin-left: 6px; + margin-right: 6px; + } +} + +@media only screen and (max-width:768px) { + .c21 { padding-top: 24px; } } @media only screen and (max-width:768px) { - .c9 { + .c23 { width: 6px; } } @@ -381,8 +683,81 @@ Object {
-
+
+ +
+ + + + Learn more + + +
+