Skip to content

Commit

Permalink
feat(nami): [lw-10405] connect hardware wallet
Browse files Browse the repository at this point in the history
  • Loading branch information
vetalcore committed Oct 2, 2024
1 parent de63c71 commit 48e3c54
Show file tree
Hide file tree
Showing 25 changed files with 444 additions and 286 deletions.
46 changes: 26 additions & 20 deletions apps/browser-extension-wallet/src/hooks/useWalletManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import {
AddWalletProps,
AnyBip32Wallet,
AnyWallet,
Bip32WalletAccount,
WalletId,
WalletManagerActivateProps,
WalletManagerApi,
Expand Down Expand Up @@ -87,9 +88,10 @@ type WalletManagerAddAccountProps = {
type ActivateWalletProps = Omit<WalletManagerActivateProps, 'chainId'>;

type CreateHardwareWalletRevampedParams = {
accountIndex: number;
accountIndexes: number[];
name: string;
connection: Wallet.HardwareWalletConnection;
getAccountName?: (index: number) => string;
};

type CreateHardwareWalletRevamped = (params: CreateHardwareWalletRevampedParams) => Promise<Wallet.CardanoWallet>;
Expand Down Expand Up @@ -266,33 +268,37 @@ export const useWalletManager = (): UseWalletManager => {
}, [currentChain]);

const createHardwareWalletRevamped = useCallback<CreateHardwareWalletRevamped>(
async ({ accountIndex, connection, name }) => {
let extendedAccountPublicKey;
try {
extendedAccountPublicKey = await Wallet.getHwExtendedAccountPublicKey(
connection.type,
async ({ accountIndexes, connection, name, getAccountName = defaultAccountName }) => {
const accounts: Bip32WalletAccount<Wallet.AccountMetadata>[] = [];
for (const accountIndex of accountIndexes) {
let extendedAccountPublicKey;
try {
extendedAccountPublicKey = await Wallet.getHwExtendedAccountPublicKey(
connection.type,
accountIndex,
connection.type === WalletType.Ledger ? connection.value : undefined
);
} catch (error: unknown) {
throw error;
}
accounts.push({
extendedAccountPublicKey,
accountIndex,
connection.type === WalletType.Ledger ? connection.value : undefined
);
} catch (error: unknown) {
throw error;
metadata: { name: getAccountName(accountIndex) }
});
}

const addWalletProps: AddWalletProps<Wallet.WalletMetadata, Wallet.AccountMetadata> = {
metadata: { name, lastActiveAccountIndex: accountIndex },
metadata: { name, lastActiveAccountIndex: accountIndexes[0] },
type: connection.type,
accounts: [
{
extendedAccountPublicKey,
accountIndex,
metadata: { name: defaultAccountName(accountIndex) }
}
]
accounts
};

const walletId = await walletRepository.addWallet(addWalletProps);
await walletManager.activate({
walletId,
chainId: getCurrentChainId(),
accountIndex
accountIndex: accountIndexes[0]
});

return {
Expand Down Expand Up @@ -323,7 +329,7 @@ export const useWalletManager = (): UseWalletManager => {
connectedDevice
}: CreateHardwareWallet): Promise<Wallet.CardanoWallet> =>
createHardwareWalletRevamped({
accountIndex,
accountIndexes: [accountIndex],
connection: {
type: connectedDevice,
value: typeof deviceConnection !== 'boolean' ? deviceConnection : undefined
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ export const backgroundServiceProperties: RemoteApiProperties<BackgroundService>
tokenPrices$: RemoteApiPropertyType.HotObservable
},
handleOpenBrowser: RemoteApiPropertyType.MethodReturningPromise,
handleOpenNamiBrowser: RemoteApiPropertyType.MethodReturningPromise,
handleOpenPopup: RemoteApiPropertyType.MethodReturningPromise,
handleChangeTheme: RemoteApiPropertyType.MethodReturningPromise,
handleChangeMode: RemoteApiPropertyType.MethodReturningPromise,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
Message,
MessageTypes,
OpenBrowserData,
OpenNamiBrowserData,
MigrationState,
TokenPrices,
CoinPrices,
Expand Down Expand Up @@ -87,11 +88,18 @@ const handleOpenBrowser = async (data: OpenBrowserData) => {
case BrowserViewSections.NAMI_MIGRATION:
path = walletRoutePaths.namiMigration.root;
break;
case BrowserViewSections.NAMI_HW_FLOW:
path = walletRoutePaths.namiMigration.hwFlow;
break;
}
const params = data.urlSearchParams ? `?${data.urlSearchParams}` : '';
await tabs.create({ url: `app.html#${path}${params}` }).catch((error) => console.error(error));
};

const handleOpenNamiBrowser = async (data: OpenNamiBrowserData) => {
await tabs.create({ url: `popup.html#${data.path}` }).catch((error) => console.error(error));
};

const handleOpenPopup = async () => {
if (typeof chrome.action.openPopup !== 'function') return;
await closeAllLaceWindows();
Expand Down Expand Up @@ -184,6 +192,7 @@ exposeApi<BackgroundService>(
{
api$: of({
handleOpenBrowser,
handleOpenNamiBrowser,
handleOpenPopup,
requestMessage$,
migrationState$,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,14 +43,19 @@ export enum BrowserViewSections {
FORGOT_PASSWORD = 'forgot_password',
NEW_WALLET = 'new_wallet',
ADD_SHARED_WALLET = 'add_shared_wallet',
NAMI_MIGRATION = 'nami_migration'
NAMI_MIGRATION = 'nami_migration',
NAMI_HW_FLOW = 'nami_hw_flow'
}

export interface OpenBrowserData {
section: BrowserViewSections;
urlSearchParams?: string;
}

export interface OpenNamiBrowserData {
path: string;
}

interface ChangeThemeMessage {
type: MessageTypes.CHANGE_THEME;
data: ChangeThemeData;
Expand All @@ -73,6 +78,7 @@ export type Message = ChangeThemeMessage | HTTPConnectionMessage | OpenBrowserMe
export type BackgroundService = {
handleOpenBrowser: (data: OpenBrowserData, urlSearchParams?: string) => Promise<void>;
handleOpenPopup: () => Promise<void>;
handleOpenNamiBrowser: (data: OpenNamiBrowserData) => Promise<void>;
requestMessage$: Subject<Message>;
migrationState$: BehaviorSubject<MigrationState | undefined>;
coinPrices: CoinPrices;
Expand Down
4 changes: 2 additions & 2 deletions apps/browser-extension-wallet/src/popup.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,8 @@ import { storage } from 'webextension-polyfill';
const App = (): React.ReactElement => {
const [mode, setMode] = useState<'lace' | 'nami'>();
storage.onChanged.addListener((changes) => {
const oldModeValue = changes.BACKGROUND_STORAGE.oldValue?.namiMigration;
const newModeValue = changes.BACKGROUND_STORAGE.newValue?.namiMigration;
const oldModeValue = changes.BACKGROUND_STORAGE?.oldValue?.namiMigration;
const newModeValue = changes.BACKGROUND_STORAGE?.newValue?.namiMigration;
if (oldModeValue?.mode !== newModeValue?.mode) {
setMode(newModeValue);
// Force back to original routing
Expand Down
3 changes: 2 additions & 1 deletion apps/browser-extension-wallet/src/routes/wallet-paths.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,8 @@ export const walletRoutePaths = {
activating: '/nami/migration/activating',
welcome: '/nami/migration/welcome',
customize: '/nami/migration/customize',
allDone: '/nami/migration/all-done'
allDone: '/nami/migration/all-done',
hwFlow: '/nami/nami-mode/hwTab'
}
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -170,7 +170,8 @@ export const HardwareWalletProvider = ({ children }: HardwareWalletProviderProps
try {
cardanoWallet = await createHardwareWalletRevamped({
connection,
...walletData
...walletData,
accountIndexes: [walletData.accountIndex]
});
} catch (error) {
console.error('ERROR creating hardware wallet', { error });
Expand Down
31 changes: 28 additions & 3 deletions apps/browser-extension-wallet/src/views/nami-mode/NamiView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import { BackgroundStorage } from '@lib/scripts/types';
import { isKeyHashAddress } from '@cardano-sdk/wallet';
import { useWalletState } from '@hooks/useWalletState';
import { certificateInspectorFactory } from '@src/features/dapp/components/confirm-transaction/utils';
import { useWrapWithTimeout } from '../browser-view/features/multi-wallet/hardware-wallet/useWrapWithTimeout';

const { AVAILABLE_CHAINS, DEFAULT_SUBMIT_API } = config();

Expand All @@ -40,11 +41,22 @@ export const NamiView = withDappContext((): React.ReactElement => {
const { priceResult } = useFetchCoinPrice();
const [namiMigration, setNamiMigration] = useState<BackgroundStorage['namiMigration']>();
const backgroundServices = useBackgroundServiceAPIContext();
const { createWallet, getMnemonic, deleteWallet, switchNetwork, enableCustomNode, addAccount, walletRepository } =
useWalletManager();
const {
createWallet,
getMnemonic,
deleteWallet,
switchNetwork,
enableCustomNode,
addAccount,
walletRepository,
connectHardwareWalletRevamped,
createHardwareWalletRevamped,
saveHardwareWallet
} = useWalletManager();
const {
walletUI,
inMemoryWallet,
walletType,
walletInfo,
currentChain,
environmentName,
Expand Down Expand Up @@ -128,6 +140,14 @@ export const NamiView = withDappContext((): React.ReactElement => {
[walletState]
);

const openHWFlow = useCallback(
(path: string) => {
backgroundServices.handleOpenNamiBrowser({ path });
},
[backgroundServices]
);
const connectHW = useWrapWithTimeout(connectHardwareWalletRevamped);

return (
<OutsideHandlesProvider
{...{
Expand Down Expand Up @@ -179,7 +199,12 @@ export const NamiView = withDappContext((): React.ReactElement => {
transactions: sortedHistoryTx,
eraSummaries: walletState?.eraSummaries,
getTxInputsValueAndAddress,
certificateInspectorFactory
certificateInspectorFactory,
openHWFlow,
walletType,
connectHW,
createHardwareWalletRevamped,
saveHardwareWallet
}}
>
<Nami />
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
#nami-mode {
width: 100%;
width: 100vw;
height: 100%;
font-family: sans-serif;
}
Expand Down
7 changes: 7 additions & 0 deletions packages/nami/.storybook/mocks/cardano-sdk.mock.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,13 @@ export const Cardano = {
NetworkMagics: {},
};

export const WalletType = {
InMemory: "InMemory",
Ledger: "Ledger",
Trezor: "Trezor",
Script: "Script"
}

export const Serialization = {
TransactionOutput: function () {},
Value: function () {
Expand Down
12 changes: 8 additions & 4 deletions packages/nami/src/adapters/account.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { act, renderHook } from '@testing-library/react-hooks';
import { BehaviorSubject, of } from 'rxjs';

import { useAccount } from './account';
import { getNextAccountIndex, useAccount } from './account';

import type {
AnyWallet,
Expand Down Expand Up @@ -68,7 +68,7 @@ const getAccountData = (
index: accIndex,
walletId: wallet.walletId,
name: acc?.metadata?.name || `${wallet.type} ${accIndex}`,

Check warning on line 70 in packages/nami/src/adapters/account.test.ts

View workflow job for this annotation

GitHub Actions / Prepare

Prefer using nullish coalescing operator (`??`) instead of a logical or (`||`), as it is a safer operator
hw: wallet.type === 'Ledger' || wallet.type === 'Trezor',
type: wallet.type,
...acc?.metadata?.namiMode,
};
};
Expand Down Expand Up @@ -263,7 +263,9 @@ describe('useAccount', () => {
}),
);

expect(result.current.nextIndex).toEqual(3);
expect(getNextAccountIndex(result.current.allAccounts, 'wallet1')).toEqual(
3,
);

act(() => {
wallets$.next([
Expand Down Expand Up @@ -296,7 +298,9 @@ describe('useAccount', () => {
]);
});

expect(result.current.nextIndex).toEqual(2);
expect(getNextAccountIndex(result.current.allAccounts, 'wallet1')).toEqual(
2,
);
});

it('should call removeWallet with correct arguments', async () => {
Expand Down
26 changes: 12 additions & 14 deletions packages/nami/src/adapters/account.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,16 +47,20 @@ export interface Account {
avatar?: string;
balance?: string;
recentSendToAddress?: string;
hw?: boolean;
type?: WalletType;
}

export interface UseAccount {
allAccounts: Account[];
activeAccount: Account;
nonActiveAccounts: Account[];
nextIndex: number;
addAccount: (
props: Readonly<{ index: number; name: string; passphrase: Uint8Array }>,
props: Readonly<{
index: number;
name: string;
passphrase: Uint8Array;
walletId: string;
}>,
) => Promise<void>;
activateAccount: (
props: Readonly<{
Expand Down Expand Up @@ -97,13 +101,11 @@ const getActiveAccountMetadata = ({
);
};

const getNextAccountIndex = (
export const getNextAccountIndex = (
accounts: readonly Account[],
activeAccount: Readonly<Account>,
walletId: string,
) => {
const walletAccounts = accounts.filter(
a => a.walletId === activeAccount.walletId,
);
const walletAccounts = accounts.filter(a => a.walletId === walletId);

for (const [index, account] of walletAccounts.entries()) {
if (account.index !== index) {
Expand All @@ -125,7 +127,7 @@ const getAcountsMapper =
index: accountIndex,
walletId: wallet.walletId,
name: metadata?.name || `${wallet.type} ${accountIndex}`,
hw: wallet.type === WalletType.Ledger || wallet.type === WalletType.Trezor,
type: wallet.type,
...metadata.namiMode,
});

Expand Down Expand Up @@ -186,10 +188,6 @@ export const useAccount = ({
return {
allAccounts: allAccountsSorted,
activeAccount,
nextIndex: useMemo(
() => getNextAccountIndex(allAccountsSorted, activeAccount),
[allAccountsSorted, activeAccount],
),
nonActiveAccounts: useMemo(
() =>
allAccountsSorted.filter(
Expand All @@ -199,7 +197,7 @@ export const useAccount = ({
[allAccountsSorted, accountIndex, walletId],
),
addAccount: useCallback(
async ({ index, name, passphrase }) => {
async ({ index, name, passphrase, walletId }) => {
const wallet = wallets?.find(elm => elm.walletId === walletId);
if (wallet === undefined || wallet.type === WalletType.Script) {
return;
Expand Down
Loading

0 comments on commit 48e3c54

Please sign in to comment.