Skip to content

Commit 5490c27

Browse files
authored
LW-13072 feat: add trezor derivation type selection for hardware wallet onboarding (#1985)
* chore: remove NODE_OPTIONS flags from browser extension scripts - Remove NODE_OPTIONS='--openssl-legacy-provider' from scripts - The flag was a Node 17/OpenSSL-3 stopgap for old toolchains (e.g., very old webpack builds). Lace is on modern Node (see .nvmrc) and current build tooling, so it's not required. * feat: implement Trezor derivation type selector - Add TrezorDerivationTypeSelector component with UI for selecting derivation types - Support ICARUS, ICARUS_TREZOR, and LEDGER derivation types - Add derivationType parameter to createHardwareWalletRevamped function - Update getHwExtendedAccountPublicKey to pass derivationType to Trezor - Add DerivationType type export from @lace/cardano - Add translations for derivation type labels and descriptions - Show selector conditionally for Trezor devices or when connection is undefined - Pass derivationType through wallet creation flow to generate correct master key * fix: improve accuracy of public key export UI text Removes inaccurate reference to staking keys and clarifies read-only usage * chore: bump cardano-js-sdk packages - Update @cardano-sdk/web-extension to 0.39.32 - Sync related SDK deps via bump script * test: fix useWalletManager addAccount tests - Add missing RxJS observables to test mocks for activateWallet - Fix optional chaining for wallet.metadata in addAccount function - All 23 tests now pass including addAccount for InMemory, Trezor, and Ledger wallets * test: fix passphrase type and formatting in useWalletManager tests
1 parent caa32fc commit 5490c27

File tree

20 files changed

+469
-256
lines changed

20 files changed

+469
-256
lines changed

apps/browser-extension-wallet/package.json

Lines changed: 16 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -26,13 +26,13 @@
2626
],
2727
"scripts": {
2828
"build": "run-s cleanup:dist build:project",
29-
"build:app": "NODE_OPTIONS='--openssl-legacy-provider' run -T webpack --config webpack.app.${WEBPACK_ENV:-prod}.js --progress",
30-
"build:cs": "NODE_OPTIONS='--openssl-legacy-provider' run -T webpack --config webpack.cs.${WEBPACK_ENV:-prod}.js --progress",
29+
"build:app": "run -T webpack --config webpack.app.${WEBPACK_ENV:-prod}.js --progress",
30+
"build:cs": "run -T webpack --config webpack.cs.${WEBPACK_ENV:-prod}.js --progress",
3131
"build:dev": "WEBPACK_ENV=dev yarn build",
3232
"build:firefox": "BROWSER=firefox yarn build",
3333
"build:firefox:dev": "WEBPACK_ENV=dev yarn build:firefox",
3434
"build:project": "run-p build:sw build:app build:cs",
35-
"build:sw": "NODE_OPTIONS='--openssl-legacy-provider' run -T webpack --config webpack.sw.${WEBPACK_ENV:-prod}.js --progress",
35+
"build:sw": "run -T webpack --config webpack.sw.${WEBPACK_ENV:-prod}.js --progress",
3636
"cleanup": "run-p cleanup:*",
3737
"cleanup:dist": "rm -rf dist",
3838
"cleanup:firefox-artifacts": "rm -rf artifacts-firefox",
@@ -46,26 +46,26 @@
4646
"pack:firefox": "run-s cleanup:firefox-artifacts && web-ext build --source-dir=./dist/ --filename=lace-firefox-browser-extension.zip --artifacts-dir=artifacts-firefox && unzip -o artifacts-firefox/lace-firefox-browser-extension.zip -d artifacts-firefox && rm -rf artifacts-firefox/lace-firefox-browser-extension.zip",
4747
"prepare": "ts-patch install -s",
4848
"prettier": "run -T prettier --write .",
49-
"serve:app": "NODE_OPTIONS='--openssl-legacy-provider' run -T webpack serve --config webpack.app.dev.js --env RUN_DEV_SERVER=true",
49+
"serve:app": "run -T webpack serve --config webpack.app.dev.js --env RUN_DEV_SERVER=true",
5050
"test": "run -T jest --config test/jest.config.js",
5151
"test:e2e": "yarn exec echo \"No e2e tests on this app yet!\"",
5252
"type-check": "echo \"@lace/browser-extension-wallet: no type-check command specified\"",
5353
"watch": " run-s cleanup:dist watch:project",
54-
"watch:app": "NODE_OPTIONS='--openssl-legacy-provider' run -T webpack --config webpack.app.dev.js --progress --watch",
55-
"watch:sw": "NODE_OPTIONS='--openssl-legacy-provider' run -T webpack --config webpack.sw.dev.js --progress --watch",
54+
"watch:app": "run -T webpack --config webpack.app.dev.js --progress --watch",
55+
"watch:sw": "run -T webpack --config webpack.sw.dev.js --progress --watch",
5656
"watch:project": "run-p watch:sw watch:app"
5757
},
5858
"dependencies": {
5959
"@ant-design/icons": "^4.7.0",
60-
"@cardano-sdk/cardano-services-client": "0.26.27",
61-
"@cardano-sdk/core": "0.46.9",
62-
"@cardano-sdk/dapp-connector": "0.13.23",
63-
"@cardano-sdk/input-selection": "0.14.25",
64-
"@cardano-sdk/tx-construction": "0.28.7",
65-
"@cardano-sdk/util": "0.17.0",
66-
"@cardano-sdk/util-rxjs": "0.9.27",
67-
"@cardano-sdk/wallet": "0.53.20",
68-
"@cardano-sdk/web-extension": "0.39.30",
60+
"@cardano-sdk/cardano-services-client": "0.26.28",
61+
"@cardano-sdk/core": "0.46.10",
62+
"@cardano-sdk/dapp-connector": "0.13.24",
63+
"@cardano-sdk/input-selection": "0.14.26",
64+
"@cardano-sdk/tx-construction": "0.28.8",
65+
"@cardano-sdk/util": "0.17.1",
66+
"@cardano-sdk/util-rxjs": "0.9.28",
67+
"@cardano-sdk/wallet": "0.53.21",
68+
"@cardano-sdk/web-extension": "0.39.32",
6969
"@emurgo/cip14-js": "~3.0.1",
7070
"@input-output-hk/lace-ui-toolkit": "3.5.0",
7171
"@lace/bitcoin": "0.1.0",
@@ -120,7 +120,7 @@
120120
"zustand": "3.5.14"
121121
},
122122
"devDependencies": {
123-
"@cardano-sdk/hardware-ledger": "0.16.18",
123+
"@cardano-sdk/hardware-ledger": "0.16.19",
124124
"@emurgo/cardano-message-signing-asmjs": "1.0.1",
125125
"@openpgp/web-stream-tools": "0.0.11-patch-0",
126126
"@pdfme/common": "^4.0.2",

apps/browser-extension-wallet/src/components/MainMenu/DropdownMenuOverlay/components/WalletAccounts.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -156,7 +156,8 @@ export const WalletAccounts = ({ isPopup, onBack }: { isPopup: boolean; onBack:
156156
await addAccount({
157157
wallet,
158158
accountIndex,
159-
metadata: { name }
159+
metadata: { name },
160+
derivationType: wallet.metadata.trezorConfig?.derivationType
160161
});
161162
analytics.sendEventToPostHog(PostHogAction.MultiWalletEnableAccount, {
162163
// eslint-disable-next-line camelcase
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
@import '../../../../packages/common/src/ui/styles/theme.scss';
2+
@import '../../../../packages/common/src/ui/styles/abstracts/typography';
3+
4+
.container {
5+
margin-bottom: size_unit(2);
6+
}
7+
8+
.header {
9+
display: flex;
10+
align-items: center;
11+
gap: size_unit(2);
12+
margin-bottom: size_unit(1);
13+
}
14+
15+
.label {
16+
@include text-form-label;
17+
color: var(--text-color-primary);
18+
margin: 0;
19+
}
20+
21+
.content {
22+
width: 100%;
23+
}
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
import React from 'react';
2+
import { useTranslation } from 'react-i18next';
3+
import { Select, InfoComponent, Tooltip } from '@input-output-hk/lace-ui-toolkit';
4+
import { DerivationType } from '@lace/cardano';
5+
import styles from './TrezorDerivationTypeSelector.module.scss';
6+
7+
interface TrezorDerivationTypeSelectorProps {
8+
value?: DerivationType;
9+
onChange: (derivationType: DerivationType) => void;
10+
disabled?: boolean;
11+
}
12+
13+
export const TrezorDerivationTypeSelector: React.FC<TrezorDerivationTypeSelectorProps> = ({
14+
value,
15+
onChange,
16+
disabled = false
17+
}) => {
18+
const { t } = useTranslation();
19+
20+
const derivationTypes = [
21+
{
22+
value: 'ICARUS',
23+
label: t('core.derivationType.icarus'),
24+
description: t('core.derivationType.icarusDescription')
25+
},
26+
{
27+
value: 'ICARUS_TREZOR',
28+
label: t('core.derivationType.icarusTrezor'),
29+
description: t('core.derivationType.icarusTrezorDescription')
30+
},
31+
{
32+
value: 'LEDGER',
33+
label: t('core.derivationType.ledger'),
34+
description: t('core.derivationType.ledgerDescription')
35+
}
36+
];
37+
38+
// Create tooltip content with all derivation type descriptions
39+
const tooltipContent = (
40+
<div>
41+
{derivationTypes.map((type) => (
42+
<div key={type.value} style={{ marginBottom: '4px' }}>
43+
<strong>{type.label}:</strong> {type.description}
44+
</div>
45+
))}
46+
</div>
47+
);
48+
49+
return (
50+
<div className={styles.container}>
51+
<div className={styles.header}>
52+
<label className={styles.label}>{t('core.derivationType.selectDerivationType')}</label>
53+
<Tooltip label={tooltipContent} align="start" side="bottom">
54+
<InfoComponent data-testid="derivation-type-info" />
55+
</Tooltip>
56+
</div>
57+
<div className={styles.content}>
58+
<Select.Root
59+
variant="outline"
60+
value={value || 'ICARUS'}
61+
onChange={(newValue) => onChange(newValue as DerivationType)}
62+
disabled={disabled}
63+
showArrow
64+
placeholder={t('core.derivationType.selectDerivationType')}
65+
triggerTestId="derivation-type-selector"
66+
zIndex={1000}
67+
>
68+
{derivationTypes.map((type) => (
69+
<Select.Item key={type.value} value={type.value} title={type.label} />
70+
))}
71+
</Select.Root>
72+
</div>
73+
</div>
74+
);
75+
};

apps/browser-extension-wallet/src/hooks/__tests__/useWalletManager.test.tsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -853,7 +853,7 @@ describe('Testing useWalletManager hook', () => {
853853
'8403cf9d8267a7169381dd476f4fda48e1926fec8942ec51892e428e152fbed4835711cccb7efcae379627f477abb46c883f6b0c221f3aea40f9d931d2e8fdc69f85f16eb91ca380fc2e1edc2543e4dd71c1866208ea6c6960bca99f974e25776067e9a242b0e4066b96bd4d89ca99db5bd77bb65573b9cbeef85222ceed6d5a4dc516213ace986f03b183365505119b9a0abdc4375bfdf2363d7433'
854854
}
855855
},
856-
passphrase: Buffer.from('passphrase1'),
856+
passphrase: new Uint8Array(Buffer.from('passphrase1')),
857857
prepare: () => {
858858
mockEmip3decrypt.mockImplementationOnce(
859859
jest.requireActual('@lace/cardano').Wallet.KeyManagement.emip3decrypt
@@ -885,6 +885,10 @@ describe('Testing useWalletManager hook', () => {
885885
walletType: 'InMemory'
886886
});
887887
walletApiUi.walletRepository.updateWalletMetadata = jest.fn().mockResolvedValueOnce(void 0);
888+
// Mock the observables required by activateWallet
889+
walletApiUi.walletManager.activeWalletId$ = of({ walletId: 'otherId' } as WalletManagerActivateProps<any, any>);
890+
walletApiUi.bitcoinWalletManager.activeWalletId$ = of(null);
891+
(walletApiUi.walletManager as any).activate = jest.fn().mockResolvedValue(undefined);
888892
});
889893

890894
it.each(walletTypes)(

apps/browser-extension-wallet/src/hooks/useWalletManager.ts

Lines changed: 33 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
/* eslint-disable complexity */
44
/* eslint-disable sonarjs/cognitive-complexity */
55
import { useCallback, useMemo } from 'react';
6-
import { Wallet } from '@lace/cardano';
6+
import { Wallet, DerivationType } from '@lace/cardano';
77
import { EnvironmentTypes, useWalletStore } from '@stores';
88
import { useAppSettingsContext } from '@providers/AppSettings';
99
import { useBackgroundServiceAPIContext } from '@providers/BackgroundServiceAPI';
@@ -94,6 +94,7 @@ export type ExtendedAccountPublicKeyParams = {
9494
accountIndex: number;
9595
passphrase?: Uint8Array;
9696
purpose?: KeyManagement.KeyPurpose;
97+
derivationType?: DerivationType;
9798
};
9899

99100
export type CreateWalletParamsBase = {
@@ -156,6 +157,7 @@ type WalletManagerAddAccountProps = {
156157
metadata: Wallet.AccountMetadata;
157158
accountIndex: number;
158159
passphrase?: Uint8Array;
160+
derivationType?: DerivationType;
159161
};
160162

161163
type ActivateWalletProps = Omit<WalletManagerActivateProps, 'chainId'>;
@@ -164,6 +166,7 @@ type CreateHardwareWalletRevampedParams = {
164166
accountIndexes: number[];
165167
name: string;
166168
connection: Wallet.HardwareWalletConnection;
169+
derivationType?: DerivationType;
167170
};
168171

169172
type CreateHardwareWalletRevamped = (params: CreateHardwareWalletRevampedParams) => Promise<Wallet.CardanoWallet>;
@@ -251,7 +254,8 @@ const getExtendedAccountPublicKey = async ({
251254
wallet,
252255
accountIndex,
253256
passphrase,
254-
purpose = KeyManagement.KeyPurpose.STANDARD
257+
purpose = KeyManagement.KeyPurpose.STANDARD,
258+
derivationType
255259
}: ExtendedAccountPublicKeyParams) => {
256260
// eslint-disable-next-line sonarjs/no-small-switch
257261
switch (wallet.type) {
@@ -280,7 +284,8 @@ const getExtendedAccountPublicKey = async ({
280284
return await Wallet.getHwExtendedAccountPublicKey({
281285
walletType: wallet.type,
282286
accountIndex,
283-
purpose
287+
purpose,
288+
derivationType
284289
});
285290
}
286291
};
@@ -354,7 +359,7 @@ export const connectHardwareWallet = async (model: Wallet.HardwareWallets): Prom
354359
await Wallet.connectDevice(model);
355360

356361
const connectHardwareWalletRevamped = async (usbDevice: USBDevice): Promise<Wallet.HardwareWalletConnection> =>
357-
Wallet.connectDeviceRevamped(usbDevice);
362+
await Wallet.connectDeviceRevamped(usbDevice);
358363

359364
// eslint-disable-next-line max-statements
360365
export const useWalletManager = (): UseWalletManager => {
@@ -418,15 +423,16 @@ export const useWalletManager = (): UseWalletManager => {
418423
}, [backgroundService]);
419424

420425
const createHardwareWalletRevamped = useCallback<CreateHardwareWalletRevamped>(
421-
async ({ accountIndexes, connection, name }) => {
426+
async ({ accountIndexes, connection, name, derivationType = 'ICARUS' }) => {
422427
const accounts: Bip32WalletAccount<Wallet.AccountMetadata>[] = [];
423428
for (const accountIndex of accountIndexes) {
424429
let extendedAccountPublicKey;
425430
try {
426431
extendedAccountPublicKey = await Wallet.getHwExtendedAccountPublicKey({
427432
walletType: connection.type,
428433
accountIndex,
429-
ledgerConnection: connection.type === WalletType.Ledger ? connection.value : undefined
434+
ledgerConnection: connection.type === WalletType.Ledger ? connection.value : undefined,
435+
derivationType
430436
});
431437
} catch (error: unknown) {
432438
throw error;
@@ -439,7 +445,7 @@ export const useWalletManager = (): UseWalletManager => {
439445
}
440446

441447
const addWalletProps: AddWalletProps<Wallet.WalletMetadata, Wallet.AccountMetadata> = {
442-
metadata: { name, lastActiveAccountIndex: accountIndexes[0] },
448+
metadata: { name, lastActiveAccountIndex: accountIndexes[0], trezorConfig: { derivationType } },
443449
type: connection.type,
444450
accounts
445451
};
@@ -1269,11 +1275,14 @@ export const useWalletManager = (): UseWalletManager => {
12691275
if (wallet.type === WalletType.Script) throw new Error('Xpub keys not available for shared wallet');
12701276

12711277
const accountIndex = 0;
1278+
// Respect wallet derivation type (e.g. ICARUS_TREZOR) when deriving shared wallet keys
1279+
const derivationType = wallet.metadata?.trezorConfig?.derivationType;
12721280
const bip32AccountPublicKey = await getExtendedAccountPublicKey({
12731281
wallet,
12741282
accountIndex,
12751283
passphrase,
1276-
purpose: KeyManagement.KeyPurpose.MULTI_SIG
1284+
purpose: KeyManagement.KeyPurpose.MULTI_SIG,
1285+
derivationType
12771286
});
12781287
return Wallet.Cardano.Cip1854ExtendedAccountPublicKey.fromBip32PublicKeyHex(bip32AccountPublicKey);
12791288
},
@@ -1386,8 +1395,22 @@ export const useWalletManager = (): UseWalletManager => {
13861395
);
13871396

13881397
const addAccount = useCallback(
1389-
async ({ wallet, accountIndex, metadata, passphrase }: WalletManagerAddAccountProps): Promise<void> => {
1390-
const extendedAccountPublicKey = await getExtendedAccountPublicKey({ wallet, accountIndex, passphrase });
1398+
async ({
1399+
wallet,
1400+
accountIndex,
1401+
metadata,
1402+
passphrase,
1403+
derivationType
1404+
}: WalletManagerAddAccountProps): Promise<void> => {
1405+
// Get derivation type from wallet metadata if not provided
1406+
const walletDerivationType = derivationType || wallet.metadata?.trezorConfig?.derivationType;
1407+
1408+
const extendedAccountPublicKey = await getExtendedAccountPublicKey({
1409+
wallet,
1410+
accountIndex,
1411+
passphrase,
1412+
derivationType: walletDerivationType
1413+
});
13911414
await walletRepository.addAccount({
13921415
accountIndex,
13931416
extendedAccountPublicKey,

0 commit comments

Comments
 (0)