From d7d5505b81f7ec4d96a6b9be4cc338f17da8fa79 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Szymon=20Mas=C5=82owski?= Date: Mon, 15 Apr 2024 11:25:47 +0200 Subject: [PATCH] feat(extension,cardano,core): LW-9808 revamp hardware wallet onboarding (#1020) * feat(extension,cardano,core): revamp hardware wallet connection in onboarding * feat(extension,cardano,staking): bump hardware-ledger, web-extension and wallet SDK packages * feat(extension): changes after self review * feat(extension): improve error cases * feat(extension): improve error handling in creation and reorganize the code * feat(cardano): allow only supported ledger devices * test(extension): fix unit tests of useWalletManager hook * refactor(extension): use enums insted of string unions * feat(extension): update hw walet creation generic error message * feat(extension,cardano,core): add unit tests, restrict allowed HW devices * refactor(extension): remove redundant eslint disable comment * fix(extension): fix typo --------- Co-authored-by: Emir Hodzic --- apps/browser-extension-wallet/package.json | 4 +- .../hooks/__tests__/useWalletManager.test.tsx | 8 +- .../src/hooks/useWalletManager.ts | 96 ++++---- .../src/lib/translations/en.json | 34 ++- .../features/multi-wallet/MultiWallet.tsx | 8 +- .../hardware-wallet/HardwareWallet.test.tsx | 6 +- .../multi-wallet/hardware-wallet/context.tsx | 2 +- .../hardware-wallet/steps/Connect.tsx | 3 +- .../hardware-wallet/steps/ErrorHandling.tsx | 22 +- .../multi-wallet/hardware-wallet/types.ts | 2 +- ...isMultidelegationSupportedByDevice.test.ts | 71 ++++++ .../browser-view/features/staking/helpers.ts | 50 ---- .../browser-view/features/staking/index.ts | 2 +- .../isMultidelegationSupportedByDevice.ts | 30 +++ .../wallet-setup/components/ErrorDialog.tsx | 59 ----- .../components/HardwareWalletFlow.tsx | 225 ------------------ .../HardwareWalletFlow/HardwareWalletFlow.tsx | 172 +++++++++++++ .../HardwareWalletFlow/StepConnect.tsx | 138 +++++++++++ .../HardwareWalletFlow/StepCreate.tsx | 95 ++++++++ .../components/HardwareWalletFlow/index.ts | 2 + .../makeErrorDialog.module.scss} | 4 +- .../HardwareWalletFlow/makeErrorDialog.tsx | 43 ++++ .../wallet-setup/components/WalletSetup.tsx | 6 +- .../components/WalletSetupMainPage.tsx | 22 +- .../features/wallet-setup/helpers.ts | 56 ----- packages/cardano/package.json | 8 +- .../cardano/src/wallet/lib/hardware-wallet.ts | 150 +++++++++++- packages/cardano/src/wallet/types.ts | 20 +- .../WalletSetup/WalletSetupCreationStep.tsx | 28 --- .../src/ui/components/WalletSetup/index.ts | 1 - ...onnectHardwareWalletStepRevamp.module.scss | 20 ++ ...etSetupConnectHardwareWalletStepRevamp.tsx | 119 +++------ .../WalletSetupHWCreationStep.module.scss | 11 + .../WalletSetupHWCreationStep.tsx | 25 ++ .../WalletSetupNamePasswordStepRevamp.tsx | 2 +- .../WalletSetupSelectAccountsStepRevamp.tsx | 2 - .../WalletSetupStepLayoutRevamp.tsx | 23 +- .../ui/components/WalletSetupRevamp/index.ts | 5 +- packages/staking/package.json | 6 +- .../StakePoolConfirmationFooter.tsx | 23 +- yarn.lock | 107 ++++++--- 41 files changed, 1023 insertions(+), 687 deletions(-) create mode 100644 apps/browser-extension-wallet/src/views/browser-view/features/staking/__tests__/isMultidelegationSupportedByDevice.test.ts delete mode 100644 apps/browser-extension-wallet/src/views/browser-view/features/staking/helpers.ts create mode 100644 apps/browser-extension-wallet/src/views/browser-view/features/staking/isMultidelegationSupportedByDevice.ts delete mode 100644 apps/browser-extension-wallet/src/views/browser-view/features/wallet-setup/components/ErrorDialog.tsx delete mode 100644 apps/browser-extension-wallet/src/views/browser-view/features/wallet-setup/components/HardwareWalletFlow.tsx create mode 100644 apps/browser-extension-wallet/src/views/browser-view/features/wallet-setup/components/HardwareWalletFlow/HardwareWalletFlow.tsx create mode 100644 apps/browser-extension-wallet/src/views/browser-view/features/wallet-setup/components/HardwareWalletFlow/StepConnect.tsx create mode 100644 apps/browser-extension-wallet/src/views/browser-view/features/wallet-setup/components/HardwareWalletFlow/StepCreate.tsx create mode 100644 apps/browser-extension-wallet/src/views/browser-view/features/wallet-setup/components/HardwareWalletFlow/index.ts rename apps/browser-extension-wallet/src/views/browser-view/features/wallet-setup/components/{ErrorDialog.module.scss => HardwareWalletFlow/makeErrorDialog.module.scss} (84%) create mode 100644 apps/browser-extension-wallet/src/views/browser-view/features/wallet-setup/components/HardwareWalletFlow/makeErrorDialog.tsx delete mode 100644 apps/browser-extension-wallet/src/views/browser-view/features/wallet-setup/helpers.ts delete mode 100644 packages/core/src/ui/components/WalletSetup/WalletSetupCreationStep.tsx create mode 100644 packages/core/src/ui/components/WalletSetupRevamp/WalletSetupConnectHardwareWalletStepRevamp.module.scss create mode 100644 packages/core/src/ui/components/WalletSetupRevamp/WalletSetupHWCreationStep.module.scss create mode 100644 packages/core/src/ui/components/WalletSetupRevamp/WalletSetupHWCreationStep.tsx diff --git a/apps/browser-extension-wallet/package.json b/apps/browser-extension-wallet/package.json index cdc8667ac..4935b1ce5 100644 --- a/apps/browser-extension-wallet/package.json +++ b/apps/browser-extension-wallet/package.json @@ -45,8 +45,8 @@ "@cardano-sdk/input-selection": "0.12.26", "@cardano-sdk/tx-construction": "0.18.2", "@cardano-sdk/util": "0.15.0", - "@cardano-sdk/wallet": "0.35.2", - "@cardano-sdk/web-extension": "0.26.1", + "@cardano-sdk/wallet": "0.36.0", + "@cardano-sdk/web-extension": "0.26.2", "@emurgo/cip14-js": "~3.0.1", "@koralabs/handles-public-api-interfaces": "^1.6.6", "@lace/cardano": "0.1.0", diff --git a/apps/browser-extension-wallet/src/hooks/__tests__/useWalletManager.test.tsx b/apps/browser-extension-wallet/src/hooks/__tests__/useWalletManager.test.tsx index 62d6c0510..102decfc5 100644 --- a/apps/browser-extension-wallet/src/hooks/__tests__/useWalletManager.test.tsx +++ b/apps/browser-extension-wallet/src/hooks/__tests__/useWalletManager.test.tsx @@ -13,6 +13,7 @@ import SpyInstance = jest.SpyInstance; const mockEmip3decrypt = jest.fn(); const mockEmip3encrypt = jest.fn(); const mockConnectDevice = jest.fn(); +const mockGetHwExtendedAccountPublicKey = jest.fn(); const mockRestoreWalletFromKeyAgent = jest.fn(); const mockSwitchKeyAgents = jest.fn(); const mockLedgerCheckDeviceConnection = jest.fn(); @@ -80,6 +81,7 @@ jest.mock('@lace/cardano', () => { restoreWalletFromKeyAgent: mockRestoreWalletFromKeyAgent, switchKeyAgents: mockSwitchKeyAgents, connectDevice: mockConnectDevice, + getHwExtendedAccountPublicKey: mockGetHwExtendedAccountPublicKey, KeyManagement: { ...actual.Wallet.KeyManagement, emip3decrypt: mockEmip3decrypt, @@ -382,7 +384,7 @@ describe('Testing useWalletManager hook', () => { describe('createHardwareWallet', () => { test('should use cardano manager to create wallet', async () => { const walletId = 'walletId'; - mockLedgerGetXpub.mockResolvedValue('pubkey'); + mockGetHwExtendedAccountPublicKey.mockResolvedValue('pubkey'); (walletApiUi.walletRepository as any).addWallet = jest.fn().mockResolvedValue(walletId); (walletApiUi.walletRepository as any).addAccount = jest.fn().mockResolvedValue(undefined); (walletApiUi.walletManager as any).activate = jest.fn().mockResolvedValue(undefined); @@ -696,11 +698,11 @@ describe('Testing useWalletManager hook', () => { }, { type: WalletType.Trezor, - prepare: () => mockTrezorGetXpub.mockResolvedValueOnce(extendedAccountPublicKey) + prepare: () => mockGetHwExtendedAccountPublicKey.mockResolvedValueOnce(extendedAccountPublicKey) }, { type: WalletType.Ledger, - prepare: () => mockLedgerGetXpub.mockResolvedValueOnce(extendedAccountPublicKey) + prepare: () => mockGetHwExtendedAccountPublicKey.mockResolvedValueOnce(extendedAccountPublicKey) } ]; diff --git a/apps/browser-extension-wallet/src/hooks/useWalletManager.ts b/apps/browser-extension-wallet/src/hooks/useWalletManager.ts index 7d230bdff..27b3c8d87 100644 --- a/apps/browser-extension-wallet/src/hooks/useWalletManager.ts +++ b/apps/browser-extension-wallet/src/hooks/useWalletManager.ts @@ -44,12 +44,6 @@ export interface CreateWallet { chainId?: Wallet.Cardano.ChainId; } -export interface SetWallet { - walletInstance: Wallet.CardanoWallet; - chainName?: Wallet.ChainName; - mnemonicVerificationFrequency?: string; -} - export interface CreateHardwareWallet { accountIndex?: number; name: string; @@ -66,6 +60,14 @@ type WalletManagerAddAccountProps = { type ActivateWalletProps = Omit; +type CreateHardwareWalletRevampedParams = { + accountIndex: number; + name: string; + connection: Wallet.HardwareWalletConnection; +}; + +type CreateHardwareWalletRevamped = (params: CreateHardwareWalletRevampedParams) => Promise; + export interface UseWalletManager { walletManager: WalletManagerApi; walletRepository: WalletRepositoryApi; @@ -78,7 +80,9 @@ export interface UseWalletManager { createWallet: (args: CreateWallet) => Promise; activateWallet: (args: Omit) => Promise; createHardwareWallet: (args: CreateHardwareWallet) => Promise; + createHardwareWalletRevamped: CreateHardwareWalletRevamped; connectHardwareWallet: (model: Wallet.HardwareWallets) => Promise; + connectHardwareWalletRevamped: typeof connectHardwareWalletRevamped; saveHardwareWallet: (wallet: Wallet.CardanoWallet, chainName?: Wallet.ChainName) => Promise; /** * @returns active wallet id after deleting the wallet; undefined if deleted the last wallet @@ -95,27 +99,6 @@ const clearBytes = (bytes: Uint8Array) => { } }; -const getHwExtendedAccountPublicKey = async ( - walletType: Wallet.HardwareWallets, - accountIndex: number, - deviceConnection?: Wallet.DeviceConnection -) => { - switch (walletType) { - case WalletType.Ledger: - await Wallet.Ledger.LedgerKeyAgent.checkDeviceConnection(Wallet.KeyManagement.CommunicationType.Web); - return Wallet.Ledger.LedgerKeyAgent.getXpub({ - communicationType: Wallet.KeyManagement.CommunicationType.Web, - deviceConnection: typeof deviceConnection !== 'boolean' ? deviceConnection : undefined, - accountIndex - }); - case WalletType.Trezor: - return Wallet.Trezor.TrezorKeyAgent.getXpub({ - communicationType: Wallet.KeyManagement.CommunicationType.Web, - accountIndex - }); - } -}; - const getExtendedAccountPublicKey = async ( wallet: AnyBip32Wallet, accountIndex: number, @@ -143,7 +126,7 @@ const getExtendedAccountPublicKey = async ( } case WalletType.Ledger: case WalletType.Trezor: - return getHwExtendedAccountPublicKey(wallet.type, accountIndex); + return Wallet.getHwExtendedAccountPublicKey(wallet.type, accountIndex); } }; @@ -210,6 +193,9 @@ const encryptMnemonic = async (mnemonic: string[], passphrase: Uint8Array) => { export const connectHardwareWallet = async (model: Wallet.HardwareWallets): Promise => await Wallet.connectDevice(model); +const connectHardwareWalletRevamped = async (usbDevice: USBDevice): Promise => + Wallet.connectDeviceRevamped(usbDevice); + export const useWalletManager = (): UseWalletManager => { const { walletLock, @@ -241,25 +227,21 @@ export const useWalletManager = (): UseWalletManager => { return (storedChain?.chainName && chainIdFromName(storedChain.chainName)) || DEFAULT_CHAIN_ID; }, [currentChain]); - /** - * Creates a Ledger or Trezor hardware wallet - * and saves it in browser storage with the data to lock/unlock it - */ - const createHardwareWallet = useCallback( - async ({ - accountIndex = 0, - deviceConnection, - name, - connectedDevice - }: CreateHardwareWallet): Promise => { - const extendedAccountPublicKey = await getHwExtendedAccountPublicKey( - connectedDevice, - accountIndex, - deviceConnection - ); + const createHardwareWalletRevamped = useCallback( + async ({ accountIndex, connection, name }) => { + let extendedAccountPublicKey; + try { + extendedAccountPublicKey = await Wallet.getHwExtendedAccountPublicKey( + connection.type, + accountIndex, + connection.type === WalletType.Ledger ? connection.value : undefined + ); + } catch (error: unknown) { + throw error; + } const addWalletProps: AddWalletProps = { metadata: { name, lastActiveAccountIndex: accountIndex }, - type: connectedDevice, + type: connection.type, accounts: [ { extendedAccountPublicKey, @@ -291,6 +273,28 @@ export const useWalletManager = (): UseWalletManager => { [getCurrentChainId] ); + /** + * Creates a Ledger or Trezor hardware wallet + * and saves it in browser storage with the data to lock/unlock it + */ + const createHardwareWallet = useCallback( + async ({ + accountIndex = 0, + deviceConnection, + name, + connectedDevice + }: CreateHardwareWallet): Promise => + createHardwareWalletRevamped({ + accountIndex, + connection: { + type: connectedDevice, + value: typeof deviceConnection !== 'boolean' ? deviceConnection : undefined + }, + name + }), + [createHardwareWalletRevamped] + ); + const tryMigrateToWalletRepository = useCallback(async (): Promise< AnyWallet[] | undefined > => { @@ -742,7 +746,9 @@ export const useWalletManager = (): UseWalletManager => { loadWallet, createWallet, createHardwareWallet, + createHardwareWalletRevamped, connectHardwareWallet, + connectHardwareWalletRevamped, saveHardwareWallet, deleteWallet, switchNetwork, diff --git a/apps/browser-extension-wallet/src/lib/translations/en.json b/apps/browser-extension-wallet/src/lib/translations/en.json index 06bcf0320..5a6150caf 100644 --- a/apps/browser-extension-wallet/src/lib/translations/en.json +++ b/apps/browser-extension-wallet/src/lib/translations/en.json @@ -494,9 +494,6 @@ "browserView.walletSetup.mnemonicResetModal.content": "In order to keep you safe, we'll show you a new set of 24 words.", "browserView.walletSetup.mnemonicResetModal.cancel": "Cancel", "browserView.walletSetup.mnemonicResetModal.confirm": "OK", - "browserView.walletSetup.confirmExperimentalHwDapp.header": "Limited support for Dapp connection", - "browserView.walletSetup.confirmExperimentalHwDapp.content": "This current version does not support signing transactions through the Dapp connection feature with hardware wallets. Please stay tuned for upcoming releases and new features through @lace on Twitter.", - "browserView.walletSetup.confirmExperimentalHwDapp.confirm": "OK", "browserView.crypto.emptyDashboard.welcome": "Welcome", "browserView.crypto.emptyDashboard.addSomeFundsYoStartYourJourney": "Add some funds to start your journey", "browserView.crypto.emptyDashboard.useThisAddressOrScanTheQRCodeToTransferFunds": "Use this address or scan the QR code to transfer funds", @@ -647,13 +644,11 @@ "browserView.staking.details.noFundsModal.buttons.confirm": "Add funds", "browserView.staking.details.errors.utxoFullyDepleted": "UTxO has been fully depleted", "browserView.staking.details.errors.utxoBalanceInsufficient": "Balance Insufficient", - "browserView.onboarding.commonError.title": "Oops! Something went wrong", - "browserView.onboarding.commonError.description": "Please check your hardware device.", - "browserView.onboarding.commonError.ok": "OK", - "browserView.onboarding.notDetectedError.title": "Failed to detect device", - "browserView.onboarding.notDetectedError.description": "Please make sure your device is unlocked and the Cardano app is open.", - "browserView.onboarding.notDetectedError.agree": "Agree", - "browserView.onboarding.notDetectedError.trezorDescription": "Please make sure your device is unlocked.", + "browserView.onboarding.errorDialog.title": "Opps! Something went wrong", + "browserView.onboarding.errorDialog.cta": "OK", + "browserView.onboarding.errorDialog.messageDeviceDisconnected": "Please check your hardware device connection and for Ledger, if Cardano App is open", + "browserView.onboarding.errorDialog.messagePublicKeyExportRejected": "Public key export unsuccessful. User declined action on hardware wallet device.", + "browserView.onboarding.errorDialog.messageGeneric": "Try connecting you device again", "browserView.onboarding.startOver.title": "Are you sure you want to start again?", "browserView.onboarding.startOver.description": "Connection to this device will be cancelled and you will need to re-connect.", "browserView.onboarding.startOver.cancel": "Cancel", @@ -1263,6 +1258,18 @@ "core.walletSetupConnectHardwareWalletStep.supportedDevices": "Lace is supporting Ledger Nano X, Nano S and Nano S Plus", "core.walletSetupConnectHardwareWalletStep.connectDevice": "Just connect your device to your computer, unlock and open the Cardano app to hit continue.", "core.walletSetupConnectHardwareWalletStep.connectDeviceFull": "Just connect your device to your computer and unlock it to continue. If you're using a Ledger device, make sure you open the Cardano App.", + "core.walletSetupConnectHardwareWalletStepRevamp.title": "Connect your device", + "core.walletSetupConnectHardwareWalletStepRevamp.subTitleLedgerOnly": "Lace supports Ledger Nano X, Nano S, Nano S Plus. Unlock your device and open the Cardano app.", + "core.walletSetupConnectHardwareWalletStepRevamp.subTitle": "Lace supports Ledger Nano X, Nano S, Nano S Plus, Trezor Model T. Unlock your device and for Ledger, open the Cardano app.", + "core.walletSetupConnectHardwareWalletStepRevamp.errorMessage.userGestureRequired": "It appears the page reload interrupted the search for connected hardware wallet devices.", + "core.walletSetupConnectHardwareWalletStepRevamp.errorMessage.devicePickerRejected": "No hardware wallet device was chosen.", + "core.walletSetupConnectHardwareWalletStepRevamp.errorMessage.deviceLocked": "Your hardware wallet device seems to be locked. Please unlock it to proceed.", + "core.walletSetupConnectHardwareWalletStepRevamp.errorMessage.deviceBusy": "Your hardware wallet device seems to be busy with some other App. Please ensure it is free to connect.", + "core.walletSetupConnectHardwareWalletStepRevamp.errorMessage.cardanoAppNotOpen": "Cardano App is not open on your hardware wallet device. Please open it to proceed.", + "core.walletSetupConnectHardwareWalletStepRevamp.errorMessage.generic": "Something went wrong. Please try again.", + "core.walletSetupConnectHardwareWalletStepRevamp.errorCta": "Try again", + "core.walletSetupCreateStep.title": "Creating your wallet", + "core.walletSetupCreateStep.description": "Confirm exporting your public key on your hardware wallet device to create your Lace wallet.", "core.walletSetupRestoreStep.title": "Restoring your wallet", "core.walletSetupMnemonicStep.writePassphrase": "Write down your secret passphrase", "core.walletSetupMnemonicStep.enterPassphrase": "Enter your secret passphrase", @@ -1392,6 +1399,13 @@ "multiWallet.confirmationDialog.description": "You'll have to start over.", "multiWallet.confirmationDialog.cancel": "Go back", "multiWallet.confirmationDialog.confirm": "Proceed", + "multiWallet.errorDialog.commonError.title": "Oops! Something went wrong", + "multiWallet.errorDialog.commonError.description": "Please check your hardware device.", + "multiWallet.errorDialog.commonError.ok": "OK", + "multiWallet.errorDialog.notDetectedError.title": "Failed to detect device", + "multiWallet.errorDialog.notDetectedError.description": "Please make sure your device is unlocked and the Cardano app is open.", + "multiWallet.errorDialog.notDetectedError.agree": "Agree", + "multiWallet.errorDialog.notDetectedError.trezorDescription": "Please make sure your device is unlocked.", "multiWallet.activated.wallet": "Wallet \"{{walletName}}\" activated", "multiWallet.activated.account": "Account \"{{accountName}}\" activated", "multiWallet.popupHwAccountEnable": "Hardware wallets require the <0>expanded view to enable accounts", diff --git a/apps/browser-extension-wallet/src/views/browser-view/features/multi-wallet/MultiWallet.tsx b/apps/browser-extension-wallet/src/views/browser-view/features/multi-wallet/MultiWallet.tsx index 1674ddf71..10865d408 100644 --- a/apps/browser-extension-wallet/src/views/browser-view/features/multi-wallet/MultiWallet.tsx +++ b/apps/browser-extension-wallet/src/views/browser-view/features/multi-wallet/MultiWallet.tsx @@ -42,7 +42,7 @@ export const SetupHardwareWallet = ({ shouldShowDialog$ }: ConfirmationDialog): const { t } = useTranslation(); const { connectHardwareWallet, createHardwareWallet, walletRepository } = useWalletManager(); const analytics = useAnalyticsContext(); - const disconnectHardwareWallet$ = useMemo(() => new Subject(), []); + const disconnectHardwareWallet$ = useMemo(() => new Subject(), []); const hardwareWalletProviders = useMemo( (): Providers => ({ @@ -83,14 +83,14 @@ export const SetupHardwareWallet = ({ shouldShowDialog$ }: ConfirmationDialog): ); useEffect(() => { - const onHardwareWalletDisconnect = (event: HIDConnectionEvent) => { + const onHardwareWalletDisconnect = (event: USBConnectionEvent) => { disconnectHardwareWallet$.next(event); }; - navigator.hid.addEventListener('disconnect', onHardwareWalletDisconnect); + navigator.usb.addEventListener('disconnect', onHardwareWalletDisconnect); return () => { - navigator.hid.removeEventListener('disconnect', onHardwareWalletDisconnect); + navigator.usb.removeEventListener('disconnect', onHardwareWalletDisconnect); disconnectHardwareWallet$.complete(); }; }, [disconnectHardwareWallet$]); diff --git a/apps/browser-extension-wallet/src/views/browser-view/features/multi-wallet/hardware-wallet/HardwareWallet.test.tsx b/apps/browser-extension-wallet/src/views/browser-view/features/multi-wallet/hardware-wallet/HardwareWallet.test.tsx index 175f0eb64..2f36a529f 100644 --- a/apps/browser-extension-wallet/src/views/browser-view/features/multi-wallet/hardware-wallet/HardwareWallet.test.tsx +++ b/apps/browser-extension-wallet/src/views/browser-view/features/multi-wallet/hardware-wallet/HardwareWallet.test.tsx @@ -59,7 +59,7 @@ describe('Multi Wallet Setup/Hardware Wallet', () => { let providers = {} as { connectHardwareWallet: jest.Mock; createWallet: jest.Mock; - disconnectHardwareWallet$: Subject; + disconnectHardwareWallet$: Subject; shouldShowDialog$: Subject; }; @@ -67,7 +67,7 @@ describe('Multi Wallet Setup/Hardware Wallet', () => { providers = { connectHardwareWallet: jest.fn(), createWallet: jest.fn(), - disconnectHardwareWallet$: new Subject(), + disconnectHardwareWallet$: new Subject(), shouldShowDialog$: new Subject() }; }); @@ -101,7 +101,7 @@ describe('Multi Wallet Setup/Hardware Wallet', () => { await selectAccountStep(); act(() => { - providers.disconnectHardwareWallet$.next({ device: { opened: true } } as HIDConnectionEvent); + providers.disconnectHardwareWallet$.next({ device: { opened: true } } as USBConnectionEvent); }); await waitFor(() => expect(screen.queryByText('Oops! Something went wrong')).toBeInTheDocument()); diff --git a/apps/browser-extension-wallet/src/views/browser-view/features/multi-wallet/hardware-wallet/context.tsx b/apps/browser-extension-wallet/src/views/browser-view/features/multi-wallet/hardware-wallet/context.tsx index f8a9eb89e..767db218a 100644 --- a/apps/browser-extension-wallet/src/views/browser-view/features/multi-wallet/hardware-wallet/context.tsx +++ b/apps/browser-extension-wallet/src/views/browser-view/features/multi-wallet/hardware-wallet/context.tsx @@ -15,7 +15,7 @@ interface State { setAccount: (account: number) => void; resetConnection: () => void; createWallet: () => Promise; - disconnectHardwareWallet$: Observable; + disconnectHardwareWallet$: Observable; } // eslint-disable-next-line unicorn/no-null diff --git a/apps/browser-extension-wallet/src/views/browser-view/features/multi-wallet/hardware-wallet/steps/Connect.tsx b/apps/browser-extension-wallet/src/views/browser-view/features/multi-wallet/hardware-wallet/steps/Connect.tsx index 542f35c05..fdf64c8ab 100644 --- a/apps/browser-extension-wallet/src/views/browser-view/features/multi-wallet/hardware-wallet/steps/Connect.tsx +++ b/apps/browser-extension-wallet/src/views/browser-view/features/multi-wallet/hardware-wallet/steps/Connect.tsx @@ -2,7 +2,6 @@ import { WalletSetupConnectHardwareWalletStep } from '@lace/core'; import React, { useState } from 'react'; import { useTranslation } from 'react-i18next'; import { useHistory } from 'react-router'; -import { isTrezorHWSupported } from '../../../wallet-setup/helpers'; import { Wallet } from '@lace/cardano'; import { useHardwareWallet } from '../context'; import { walletRoutePaths } from '@routes'; @@ -11,6 +10,8 @@ import { WalletType } from '@cardano-sdk/web-extension'; import { useAnalyticsContext } from '@providers'; import { PostHogAction } from '@lace/common'; +export const isTrezorHWSupported = (): boolean => process.env.USE_TREZOR_HW === 'true'; + interface State { error?: 'notDetectedLedger' | 'notDetectedTrezor'; } diff --git a/apps/browser-extension-wallet/src/views/browser-view/features/multi-wallet/hardware-wallet/steps/ErrorHandling.tsx b/apps/browser-extension-wallet/src/views/browser-view/features/multi-wallet/hardware-wallet/steps/ErrorHandling.tsx index 957dfe276..de4b88589 100644 --- a/apps/browser-extension-wallet/src/views/browser-view/features/multi-wallet/hardware-wallet/steps/ErrorHandling.tsx +++ b/apps/browser-extension-wallet/src/views/browser-view/features/multi-wallet/hardware-wallet/steps/ErrorHandling.tsx @@ -1,11 +1,29 @@ import React, { useEffect, useState } from 'react'; import { useHistory } from 'react-router'; import { useHardwareWallet } from '../context'; -import { ErrorDialog } from '../../../wallet-setup/components/ErrorDialog'; +import { makeErrorDialog } from '../../../wallet-setup/components/HardwareWalletFlow'; import { walletRoutePaths } from '@routes'; type Errors = 'notDetectedLedger' | 'notDetectedTrezor' | 'common'; +const ErrorDialog = makeErrorDialog({ + common: { + title: 'multiWallet.errorDialog.commonError.title', + description: 'multiWallet.errorDialog.commonError.description', + confirm: 'multiWallet.errorDialog.commonError.ok' + }, + notDetectedLedger: { + title: 'multiWallet.errorDialog.notDetectedError.title', + description: 'multiWallet.errorDialog.notDetectedError.description', + confirm: 'multiWallet.errorDialog.notDetectedError.agree' + }, + notDetectedTrezor: { + title: 'multiWallet.errorDialog.notDetectedError.title', + description: 'multiWallet.errorDialog.notDetectedError.trezorDescription', + confirm: 'multiWallet.errorDialog.notDetectedError.agree' + } +}); + interface State { error?: Errors; } @@ -21,7 +39,7 @@ export const ErrorHandling = ({ error, onRetry }: Props): JSX.Element => { const [state, setState] = useState({}); useEffect(() => { - const subscription = disconnectHardwareWallet$.subscribe((event: HIDConnectionEvent) => { + const subscription = disconnectHardwareWallet$.subscribe((event: USBConnectionEvent) => { if (event.device.opened) { setState({ error: 'common' }); } diff --git a/apps/browser-extension-wallet/src/views/browser-view/features/multi-wallet/hardware-wallet/types.ts b/apps/browser-extension-wallet/src/views/browser-view/features/multi-wallet/hardware-wallet/types.ts index c8105b223..9c9a130a6 100644 --- a/apps/browser-extension-wallet/src/views/browser-view/features/multi-wallet/hardware-wallet/types.ts +++ b/apps/browser-extension-wallet/src/views/browser-view/features/multi-wallet/hardware-wallet/types.ts @@ -11,6 +11,6 @@ export interface Data { export interface Providers { createWallet: (params: Data) => Promise; connectHardwareWallet: (model: Wallet.HardwareWallets) => Promise; - disconnectHardwareWallet$: Observable; + disconnectHardwareWallet$: Observable; shouldShowDialog$: Subject; } diff --git a/apps/browser-extension-wallet/src/views/browser-view/features/staking/__tests__/isMultidelegationSupportedByDevice.test.ts b/apps/browser-extension-wallet/src/views/browser-view/features/staking/__tests__/isMultidelegationSupportedByDevice.test.ts new file mode 100644 index 000000000..f64d9d889 --- /dev/null +++ b/apps/browser-extension-wallet/src/views/browser-view/features/staking/__tests__/isMultidelegationSupportedByDevice.test.ts @@ -0,0 +1,71 @@ +/* eslint-disable import/imports-first */ +const initConnectionAndGetSoftwareVersionMock = jest.fn(); + +import { + LedgerMultidelegationMinAppVersion, + isMultidelegationSupportedByDevice +} from '../isMultidelegationSupportedByDevice'; +import { WalletType } from '@cardano-sdk/web-extension'; + +jest.mock('@lace/cardano', () => ({ + Wallet: { + initConnectionAndGetSoftwareVersion: initConnectionAndGetSoftwareVersionMock + } +})); + +describe('isMultidelegationSupportedByDevice', () => { + it('calls Wallet.isMultidelegationSupportedByDevice to get a version', async () => { + initConnectionAndGetSoftwareVersionMock.mockResolvedValue({ + major: 0, + minor: 0, + patch: 0 + }); + await isMultidelegationSupportedByDevice(WalletType.Ledger); + expect(initConnectionAndGetSoftwareVersionMock).toHaveBeenCalledWith(WalletType.Ledger); + }); + + it('returns false if the version is lower than expected', async () => { + initConnectionAndGetSoftwareVersionMock.mockResolvedValue({ + major: 0, + minor: 0, + patch: 0 + }); + expect(await isMultidelegationSupportedByDevice(WalletType.Trezor)).toEqual(false); + }); + + it('returns true if the version is greater on the major level', async () => { + initConnectionAndGetSoftwareVersionMock.mockResolvedValue({ + major: LedgerMultidelegationMinAppVersion.MAJOR + 1, + minor: 0, + patch: 0 + }); + expect(await isMultidelegationSupportedByDevice(WalletType.Ledger)).toEqual(true); + }); + + it('returns true if the version is greater on the minor level', async () => { + initConnectionAndGetSoftwareVersionMock.mockResolvedValue({ + major: LedgerMultidelegationMinAppVersion.MAJOR, + minor: LedgerMultidelegationMinAppVersion.MINOR + 1, + patch: 0 + }); + expect(await isMultidelegationSupportedByDevice(WalletType.Ledger)).toEqual(true); + }); + + it('returns true if the version is greater on the patch level', async () => { + initConnectionAndGetSoftwareVersionMock.mockResolvedValue({ + major: LedgerMultidelegationMinAppVersion.MAJOR, + minor: LedgerMultidelegationMinAppVersion.MINOR, + patch: LedgerMultidelegationMinAppVersion.PATCH + 1 + }); + expect(await isMultidelegationSupportedByDevice(WalletType.Ledger)).toEqual(true); + }); + + it('returns true if the version is equal to the minimal required', async () => { + initConnectionAndGetSoftwareVersionMock.mockResolvedValue({ + major: LedgerMultidelegationMinAppVersion.MAJOR, + minor: LedgerMultidelegationMinAppVersion.MINOR, + patch: LedgerMultidelegationMinAppVersion.PATCH + }); + expect(await isMultidelegationSupportedByDevice(WalletType.Trezor)).toEqual(true); + }); +}); diff --git a/apps/browser-extension-wallet/src/views/browser-view/features/staking/helpers.ts b/apps/browser-extension-wallet/src/views/browser-view/features/staking/helpers.ts deleted file mode 100644 index 77452c9f9..000000000 --- a/apps/browser-extension-wallet/src/views/browser-view/features/staking/helpers.ts +++ /dev/null @@ -1,50 +0,0 @@ -/* eslint-disable no-magic-numbers */ -import { Wallet } from '@lace/cardano'; -import * as HardwareLedger from '@cardano-sdk/hardware-ledger'; -import * as HardwareTrezor from '@cardano-sdk/hardware-trezor'; -import { WalletType } from '@cardano-sdk/web-extension'; - -export enum LedgerMultidelegationMinAppVersion { - MAJOR = 6, - MINOR = 1, - PATCH = 2 -} - -export enum TrezorMultidelegationFirmwareMinVersion { - MAJOR = 2, - MINOR = 6, - PATCH = 4 -} - -export const isMultidelegationSupportedByDevice = async ( - walletType: Exclude -): Promise => { - switch (walletType) { - case WalletType.Ledger: { - const ledgerInfo = await HardwareLedger.LedgerKeyAgent.getAppVersion(Wallet.KeyManagement.CommunicationType.Web); - return ( - ledgerInfo.version.major >= LedgerMultidelegationMinAppVersion.MAJOR && - ledgerInfo.version.minor >= LedgerMultidelegationMinAppVersion.MINOR && - ledgerInfo.version.patch >= LedgerMultidelegationMinAppVersion.PATCH - ); - } - case WalletType.Trezor: { - // To allow checks once the app is refreshed. It won't affect the user flow - // TODO: Smarter Trezor initialization logic after onboarding revamp LW-9808 - await HardwareTrezor.TrezorKeyAgent.initializeTrezorTransport({ - manifest: Wallet.manifest, - communicationType: Wallet.KeyManagement.CommunicationType.Web - }); - const trezorInfo = await HardwareTrezor.TrezorKeyAgent.checkDeviceConnection( - Wallet.KeyManagement.CommunicationType.Web - ); - return ( - trezorInfo.major_version >= TrezorMultidelegationFirmwareMinVersion.MAJOR && - trezorInfo.minor_version >= TrezorMultidelegationFirmwareMinVersion.MINOR && - trezorInfo.patch_version >= TrezorMultidelegationFirmwareMinVersion.PATCH - ); - } - default: - return true; - } -}; diff --git a/apps/browser-extension-wallet/src/views/browser-view/features/staking/index.ts b/apps/browser-extension-wallet/src/views/browser-view/features/staking/index.ts index 2ac30533f..5d897b296 100644 --- a/apps/browser-extension-wallet/src/views/browser-view/features/staking/index.ts +++ b/apps/browser-extension-wallet/src/views/browser-view/features/staking/index.ts @@ -1,2 +1,2 @@ export * from './components/StakingContainer'; -export * from './helpers'; +export * from './isMultidelegationSupportedByDevice'; diff --git a/apps/browser-extension-wallet/src/views/browser-view/features/staking/isMultidelegationSupportedByDevice.ts b/apps/browser-extension-wallet/src/views/browser-view/features/staking/isMultidelegationSupportedByDevice.ts new file mode 100644 index 000000000..c31f2fda0 --- /dev/null +++ b/apps/browser-extension-wallet/src/views/browser-view/features/staking/isMultidelegationSupportedByDevice.ts @@ -0,0 +1,30 @@ +/* eslint-disable no-magic-numbers */ +import { Wallet } from '@lace/cardano'; +import { WalletType } from '@cardano-sdk/web-extension'; + +export enum LedgerMultidelegationMinAppVersion { + MAJOR = 6, + MINOR = 1, + PATCH = 2 +} + +export enum TrezorMultidelegationFirmwareMinVersion { + MAJOR = 2, + MINOR = 6, + PATCH = 4 +} + +export const isMultidelegationSupportedByDevice = async (walletType: Wallet.HardwareWallets): Promise => { + const version = await Wallet.initConnectionAndGetSoftwareVersion(walletType); + const expectedVersion = + walletType === WalletType.Ledger ? LedgerMultidelegationMinAppVersion : TrezorMultidelegationFirmwareMinVersion; + + const higherOnMajor = version.major > expectedVersion.MAJOR; + const higherOnMinor = version.major === expectedVersion.MAJOR && version.minor > expectedVersion.MINOR; + const higherOrEqualOnPatch = + version.major === expectedVersion.MAJOR && + version.minor === expectedVersion.MINOR && + version.patch >= expectedVersion.PATCH; + + return higherOnMajor || higherOnMinor || higherOrEqualOnPatch; +}; diff --git a/apps/browser-extension-wallet/src/views/browser-view/features/wallet-setup/components/ErrorDialog.tsx b/apps/browser-extension-wallet/src/views/browser-view/features/wallet-setup/components/ErrorDialog.tsx deleted file mode 100644 index ea823c23a..000000000 --- a/apps/browser-extension-wallet/src/views/browser-view/features/wallet-setup/components/ErrorDialog.tsx +++ /dev/null @@ -1,59 +0,0 @@ -import React from 'react'; -import { Modal, Typography } from 'antd'; -import { Button } from '@lace/common'; -import styles from './ErrorDialog.module.scss'; -import { useTranslation } from 'react-i18next'; - -const { Title, Text } = Typography; - -export type HWErrorCode = 'common' | 'notDetectedLedger' | 'notDetectedTrezor'; - -interface ErrorDialogProps { - visible: boolean; - onRetry: () => void; - errorCode: HWErrorCode; -} - -export const ErrorDialog = ({ visible, onRetry, errorCode = 'common' }: ErrorDialogProps): React.ReactElement => { - const { t } = useTranslation(); - - const ERROR_MESSAGES = { - common: { - title: t('browserView.onboarding.commonError.title'), - description: t('browserView.onboarding.commonError.description'), - confirm: t('browserView.onboarding.commonError.ok') - }, - notDetectedLedger: { - title: t('browserView.onboarding.notDetectedError.title'), - description: t('browserView.onboarding.notDetectedError.description'), - confirm: t('browserView.onboarding.notDetectedError.agree') - }, - notDetectedTrezor: { - title: t('browserView.onboarding.notDetectedError.title'), - description: t('browserView.onboarding.notDetectedError.trezorDescription'), - confirm: t('browserView.onboarding.notDetectedError.agree') - } - }; - - const error = ERROR_MESSAGES[errorCode]; - - return ( - - - {error.title} - - {error.description} - - - ); -}; diff --git a/apps/browser-extension-wallet/src/views/browser-view/features/wallet-setup/components/HardwareWalletFlow.tsx b/apps/browser-extension-wallet/src/views/browser-view/features/wallet-setup/components/HardwareWalletFlow.tsx deleted file mode 100644 index 8d0edd615..000000000 --- a/apps/browser-extension-wallet/src/views/browser-view/features/wallet-setup/components/HardwareWalletFlow.tsx +++ /dev/null @@ -1,225 +0,0 @@ -// / -/* eslint-disable max-statements */ -/* eslint-disable react/no-multi-comp */ -import { useWalletManager, useTimeSpentOnPage, useLocalStorage } from '@hooks'; -import { WalletSetupSelectAccountsStepRevamp, WalletSetupConnectHardwareWalletStepRevamp } from '@lace/core'; -import React, { useState, useCallback, useEffect } from 'react'; -import { Switch, Route, useHistory, useLocation, Redirect } from 'react-router-dom'; -import { Wallet } from '@lace/cardano'; -import { WalletSetupLayout } from '@src/views/browser-view/components/Layout'; -import { ErrorDialog, HWErrorCode } from './ErrorDialog'; -import { StartOverDialog } from '@views/browser/features/wallet-setup/components/StartOverDialog'; -import { useTranslation } from 'react-i18next'; -import { EnhancedAnalyticsOptInStatus, postHogOnboardingActions } from '@providers/AnalyticsProvider/analyticsTracker'; -import { config } from '@src/config'; -import { walletRoutePaths } from '@routes/wallet-paths'; -import { getHWPersonProperties, isTrezorHWSupported } from '../helpers'; -import { useAnalyticsContext } from '@providers'; -import { ENHANCED_ANALYTICS_OPT_IN_STATUS_LS_KEY } from '@providers/AnalyticsProvider/config'; -import { SendOnboardingAnalyticsEvent } from '../types'; -import { WalletType } from '@cardano-sdk/web-extension'; - -const { CHAIN } = config(); -const { AVAILABLE_WALLETS } = Wallet; -export interface HardwareWalletFlowProps { - onCancel: () => void; - onAppReload: () => void; - sendAnalytics: SendOnboardingAnalyticsEvent; -} - -type HardwareWalletStep = 'connect' | 'setup'; - -const TOTAL_ACCOUNTS = 50; - -const route = (path: string) => `${walletRoutePaths.setup.hardware}/${path}`; - -export const HardwareWalletFlow = ({ - onCancel, - onAppReload, - sendAnalytics -}: HardwareWalletFlowProps): React.ReactElement => { - const history = useHistory(); - const location = useLocation(); - const { t } = useTranslation(); - const [isErrorDialogVisible, setIsErrorDialogVisible] = useState(false); - const [hardwareWalletErrorCode, setHardwareWalletErrorCode] = useState('common'); - const [isStartOverDialogVisible, setIsStartOverDialogVisible] = useState(false); - const showStartOverDialog = () => setIsStartOverDialogVisible(true); - const [walletCreated, setWalletCreated] = useState(); - const [deviceConnection, setDeviceConnection] = useState(); - const [connectedDevice, setConnectedDevice] = useState(); - const [accountIndex, setAccountIndex] = useState(0); - const [isSubmitting, setIsSubmitting] = useState(false); - const { createHardwareWallet, connectHardwareWallet, saveHardwareWallet } = useWalletManager(); - const { updateEnteredAtTime } = useTimeSpentOnPage(); - const analytics = useAnalyticsContext(); - - useEffect(() => { - updateEnteredAtTime(); - }, [location.pathname, updateEnteredAtTime]); - - const showHardwareWalletError = (errorCode: HWErrorCode) => { - setHardwareWalletErrorCode(errorCode); - setIsErrorDialogVisible(true); - }; - - const walletSetupConnectHardwareWalletStepTranslations = { - title: t('core.walletSetupConnectHardwareWalletStep.title'), - subTitle: t(`core.walletSetupConnectHardwareWalletStep.${isTrezorHWSupported() ? 'subTitleFull' : 'subTitle'}`), - supportedDevices: t( - `core.walletSetupConnectHardwareWalletStep.${isTrezorHWSupported() ? 'supportedDevicesFull' : 'supportedDevices'}` - ), - connectDevice: t( - `core.walletSetupConnectHardwareWalletStep.${isTrezorHWSupported() ? 'connectDeviceFull' : 'connectDevice'}` - ) - }; - - const navigateTo = useCallback( - (nexthPath: string) => { - history.replace(route(nexthPath)); - }, - [history] - ); - - const [enhancedAnalyticsStatus] = useLocalStorage( - ENHANCED_ANALYTICS_OPT_IN_STATUS_LS_KEY, - EnhancedAnalyticsOptInStatus.OptedOut - ); - - const handleCreateWallet = async (name: string) => { - try { - const cardanoWallet = await createHardwareWallet({ - accountIndex, - deviceConnection, - name, - connectedDevice - }); - setWalletCreated(cardanoWallet); - } catch (error) { - console.error('ERROR creating hardware wallet', { error }); - showHardwareWalletError('common'); - } - }; - - const handleConnect = async (model: Wallet.HardwareWallets) => { - try { - const connection = await connectHardwareWallet(model); - setDeviceConnection(connection); - setConnectedDevice(model); - } catch (error) { - console.error('ERROR connecting hardware wallet', error); - if (error.innerError?.innerError?.message === 'The device is already open.') { - setDeviceConnection(deviceConnection); - } else { - showHardwareWalletError(model === WalletType.Trezor ? 'notDetectedTrezor' : 'notDetectedLedger'); - } - } - }; - - const handleFinishCreation = () => saveHardwareWallet(walletCreated, CHAIN); - - const handleGoToMyWalletClick = async () => { - try { - const posthogProperties = await getHWPersonProperties(connectedDevice, deviceConnection); - await sendAnalytics(postHogOnboardingActions.hw.DONE_GO_TO_WALLET, { - ...posthogProperties, - // eslint-disable-next-line camelcase - $set: { wallet_accounts_quantity: '1' } - }); - } catch { - console.error('We were not able to send the analytics event'); - } finally { - await handleFinishCreation(); - if (enhancedAnalyticsStatus === EnhancedAnalyticsOptInStatus.OptedIn) { - await analytics.sendAliasEvent(); - } - } - }; - - const onHardwareWalletDisconnect = useCallback((event: HIDConnectionEvent) => { - if (event.device.opened) showHardwareWalletError('common'); - }, []); - - useEffect(() => { - navigator.hid.addEventListener('disconnect', onHardwareWalletDisconnect); - return () => { - navigator.hid.removeEventListener('disconnect', onHardwareWalletDisconnect); - }; - }, [onHardwareWalletDisconnect]); - - const handleEnterWallet = async (account: number, name: string) => { - sendAnalytics(postHogOnboardingActions.hw.SETUP_HW_WALLET_NEXT_CLICK); - setAccountIndex(account); - setIsSubmitting(true); - await handleCreateWallet(name); - setIsSubmitting(false); - await handleGoToMyWalletClick(); - }; - - const hardwareWalletStepRenderFunctions: Record JSX.Element> = { - connect: () => ( - { - analytics.sendEventToPostHog(postHogOnboardingActions.hw.CONNECT_HW_NEXT_CLICK); - navigateTo('setup'); - }} - isNextEnable={!!deviceConnection} - translations={walletSetupConnectHardwareWalletStepTranslations} - isHardwareWallet - /> - ), - setup: () => ( - - ) - }; - - const goBackToConnect = () => { - /* eslint-disable unicorn/no-useless-undefined */ - setDeviceConnection(undefined); - setConnectedDevice(undefined); - setAccountIndex(0); - setWalletCreated(undefined); - history.replace(route('connect')); - }; - - const onRetry = () => { - setIsErrorDialogVisible(false); - goBackToConnect(); - // TODO: Remove this workaround with full app reload when SDK allows to connect Hardware Wallet for the 2nd time. - onAppReload(); - }; - - const handleStartOver = () => { - setIsStartOverDialogVisible(false); - goBackToConnect(); - // TODO: Remove this workaround with full app reload when SDK allows to connect Hardware Wallet for the 2nd time. - onAppReload(); - }; - - return ( - <> - - setIsStartOverDialogVisible(false)} - /> - - - {hardwareWalletStepRenderFunctions.connect()} - {hardwareWalletStepRenderFunctions.setup()} - - - - - ); -}; diff --git a/apps/browser-extension-wallet/src/views/browser-view/features/wallet-setup/components/HardwareWalletFlow/HardwareWalletFlow.tsx b/apps/browser-extension-wallet/src/views/browser-view/features/wallet-setup/components/HardwareWalletFlow/HardwareWalletFlow.tsx new file mode 100644 index 000000000..5c8c00e50 --- /dev/null +++ b/apps/browser-extension-wallet/src/views/browser-view/features/wallet-setup/components/HardwareWalletFlow/HardwareWalletFlow.tsx @@ -0,0 +1,172 @@ +/* eslint-disable unicorn/no-useless-undefined */ +import { useTimeSpentOnPage } from '@hooks'; +import { WalletSetupSelectAccountsStepRevamp } from '@lace/core'; +import React, { useCallback, useEffect, useState } from 'react'; +import { Redirect, Route, Switch, useHistory } from 'react-router-dom'; +import { Wallet } from '@lace/cardano'; +import { WalletSetupLayout } from '@src/views/browser-view/components/Layout'; +import { makeErrorDialog } from './makeErrorDialog'; +import { StartOverDialog } from '@views/browser/features/wallet-setup/components/StartOverDialog'; +import { walletRoutePaths } from '@routes/wallet-paths'; +import { StepConnect } from './StepConnect'; +import { WalletType } from '@cardano-sdk/web-extension'; +import { StepCreate, WalletData } from './StepCreate'; + +export interface HardwareWalletFlowProps { + onCancel: () => void; +} + +const TOTAL_ACCOUNTS = 50; +const route = (path: FlowStep) => `${walletRoutePaths.setup.hardware}/${path}`; + +enum FlowStep { + Connect = 'Connect', + Setup = 'Setup', + Create = 'Create' +} + +enum ErrorDialogCode { + DeviceDisconnected = 'DeviceDisconnected', + PublicKeyExportRejected = 'PublicKeyExportRejected', + Generic = 'Generic' +} + +const commonErrorDialogTranslationKeys = { + title: 'browserView.onboarding.errorDialog.title' as const, + confirm: 'browserView.onboarding.errorDialog.cta' as const +}; +const ErrorDialog = makeErrorDialog({ + [ErrorDialogCode.DeviceDisconnected]: { + ...commonErrorDialogTranslationKeys, + description: 'browserView.onboarding.errorDialog.messageDeviceDisconnected' + }, + [ErrorDialogCode.PublicKeyExportRejected]: { + ...commonErrorDialogTranslationKeys, + description: 'browserView.onboarding.errorDialog.messagePublicKeyExportRejected' + }, + [ErrorDialogCode.Generic]: { + ...commonErrorDialogTranslationKeys, + description: 'browserView.onboarding.errorDialog.messageGeneric' + } +}); + +export const HardwareWalletFlow = ({ onCancel }: HardwareWalletFlowProps): React.ReactElement => { + const history = useHistory(); + const [connectedUsbDevice, setConnectedUsbDevice] = useState(); + const [errorDialogCode, setErrorDialogCode] = useState(); + const [isStartOverDialogVisible, setIsStartOverDialogVisible] = useState(false); + const [connection, setConnection] = useState(); + const [walletData, setWalletData] = useState(); + const { updateEnteredAtTime } = useTimeSpentOnPage(); + + useEffect(() => { + updateEnteredAtTime(); + }, [history.location.pathname, updateEnteredAtTime]); + + useEffect(() => { + const onHardwareWalletDisconnect = (event: USBConnectionEvent) => { + if (event.device !== connectedUsbDevice || !connection) return; + setErrorDialogCode(ErrorDialogCode.DeviceDisconnected); + }; + + navigator.usb.addEventListener('disconnect', onHardwareWalletDisconnect); + return () => { + navigator.usb.removeEventListener('disconnect', onHardwareWalletDisconnect); + }; + }, [connectedUsbDevice, connection]); + + const navigateTo = useCallback( + (nexthPath: FlowStep) => { + history.replace(route(nexthPath)); + }, + [history] + ); + + const onConnected = useCallback( + (result?: Wallet.HardwareWalletConnection) => { + if (result) { + setConnection(result); + } + navigateTo(FlowStep.Setup); + }, + [navigateTo] + ); + + const closeConnection = () => { + if (connection.type === WalletType.Ledger) { + void connection.value.transport.close(); + } + }; + + const onAccountAndNameSubmit = async (accountIndex: number, name: string) => { + setWalletData({ + accountIndex, + name + }); + navigateTo(FlowStep.Create); + }; + + const cleanupConnectionState = () => { + setConnection(undefined); + navigateTo(FlowStep.Connect); + closeConnection(); + }; + + const onRetry = () => { + setErrorDialogCode(undefined); + cleanupConnectionState(); + }; + + const handleStartOver = () => { + setIsStartOverDialogVisible(false); + cleanupConnectionState(); + }; + + const onWalletCreateError = (error: Error) => { + let errorCode: ErrorDialogCode = ErrorDialogCode.Generic; + + const ledgerPkRejection = + error.message.includes('Failed to export extended account public key') && + error.message.includes('Action rejected by user'); + const trezorPkRejection = error.message.includes('Trezor transport failed'); + if (ledgerPkRejection || trezorPkRejection) { + errorCode = ErrorDialogCode.PublicKeyExportRejected; + } + + setErrorDialogCode(errorCode); + closeConnection(); + }; + + return ( + <> + {!!errorDialogCode && } + setIsStartOverDialogVisible(false)} + /> + + + + + + {!!connection && ( + <> + + setIsStartOverDialogVisible(true)} + onSubmit={onAccountAndNameSubmit} + /> + + + + + + )} + + + + + ); +}; diff --git a/apps/browser-extension-wallet/src/views/browser-view/features/wallet-setup/components/HardwareWalletFlow/StepConnect.tsx b/apps/browser-extension-wallet/src/views/browser-view/features/wallet-setup/components/HardwareWalletFlow/StepConnect.tsx new file mode 100644 index 000000000..e77290b23 --- /dev/null +++ b/apps/browser-extension-wallet/src/views/browser-view/features/wallet-setup/components/HardwareWalletFlow/StepConnect.tsx @@ -0,0 +1,138 @@ +/* eslint-disable unicorn/no-null */ +import { UseWalletManager, useWalletManager } from '@hooks'; +import { Wallet } from '@lace/cardano'; +import { WalletSetupConnectHardwareWalletStepRevamp } from '@lace/core'; +import { TranslationKey } from '@lib/translations/types'; +import { TFunction } from 'i18next'; +import React, { useCallback, useEffect, useState, VFC } from 'react'; +import { useTranslation } from 'react-i18next'; + +export const isTrezorHWSupported = (): boolean => process.env.USE_TREZOR_HW === 'true'; + +const requestHardwareWalletConnection = (): Promise => + navigator.usb.requestDevice({ + filters: isTrezorHWSupported() ? Wallet.supportedHwUsbDescriptors : Wallet.ledgerDescriptors + }); + +const threeSecondsTimeout = 3000; +const timeoutErrorMessage = 'Timeout. Connecting too long.'; + +const isTimeoutError = (error: Error): boolean => error.message === timeoutErrorMessage; + +const useConnectHardwareWalletWithTimeout = (connect: UseWalletManager['connectHardwareWalletRevamped']) => + useCallback( + async (usbDevice: USBDevice) => { + const result = await Promise.race([ + connect(usbDevice), + new Promise<'timeout'>((resolve) => setTimeout(() => resolve('timeout'), threeSecondsTimeout)) + ]); + + if (result === 'timeout') { + throw new Error(timeoutErrorMessage); + } + + return result; + }, + [connect] + ); + +type ConnectionError = + | 'userGestureRequired' + | 'devicePickerRejected' + | 'deviceLocked' + | 'deviceBusy' + | 'cardanoAppNotOpen' + | 'generic'; + +const connectionSubtitleErrorTranslationsMap: Record = { + cardanoAppNotOpen: 'core.walletSetupConnectHardwareWalletStepRevamp.errorMessage.cardanoAppNotOpen', + deviceLocked: 'core.walletSetupConnectHardwareWalletStepRevamp.errorMessage.deviceLocked', + deviceBusy: 'core.walletSetupConnectHardwareWalletStepRevamp.errorMessage.deviceBusy', + devicePickerRejected: 'core.walletSetupConnectHardwareWalletStepRevamp.errorMessage.devicePickerRejected', + userGestureRequired: 'core.walletSetupConnectHardwareWalletStepRevamp.errorMessage.userGestureRequired', + generic: 'core.walletSetupConnectHardwareWalletStepRevamp.errorMessage.generic' +}; + +const makeTranslations = ({ connectionError, t }: { connectionError: ConnectionError; t: TFunction }) => ({ + title: t('core.walletSetupConnectHardwareWalletStepRevamp.title'), + subTitle: isTrezorHWSupported() + ? t('core.walletSetupConnectHardwareWalletStepRevamp.subTitle') + : t('core.walletSetupConnectHardwareWalletStepRevamp.subTitleLedgerOnly'), + errorMessage: connectionError ? t(connectionSubtitleErrorTranslationsMap[connectionError]) : '', + errorCta: t('core.walletSetupConnectHardwareWalletStepRevamp.errorCta') +}); + +const parseConnectionError = (error: Error): ConnectionError | null => { + if (error instanceof DOMException) { + if (error.message.includes('user gesture')) return 'userGestureRequired'; + if (error.message.includes('No device selected')) return 'devicePickerRejected'; + } + if (isTimeoutError(error)) return 'deviceBusy'; + if (error.message.includes('Cannot communicate with Ledger Cardano App')) { + if (error.message.includes('General error 0x5515')) return 'deviceLocked'; + if (error.message.includes('General error 0x6e01')) return 'cardanoAppNotOpen'; + } + return 'generic'; +}; + +enum DiscoveryState { + Idle = 'Idle', + Requested = 'Requested', + Running = 'Running' +} + +type StepConnectProps = { + onBack: () => void; + onConnected: (result?: Wallet.HardwareWalletConnection) => void; + onUsbDeviceChange: (usbDevice: USBDevice) => void; +}; + +export const StepConnect: VFC = ({ onBack, onConnected, onUsbDeviceChange }) => { + const { t } = useTranslation(); + const [discoveryState, setDiscoveryState] = useState(DiscoveryState.Requested); + const [connectionError, setConnectionError] = useState(null); + const { connectHardwareWalletRevamped } = useWalletManager(); + + const translations = makeTranslations({ connectionError, t }); + const connect = useConnectHardwareWalletWithTimeout(connectHardwareWalletRevamped); + + const onRetry = useCallback(() => { + setDiscoveryState(DiscoveryState.Requested); + setConnectionError(null); + }, []); + + useEffect(() => { + (async () => { + if (discoveryState !== DiscoveryState.Requested) return; + + setDiscoveryState(DiscoveryState.Running); + let connectionResult: Wallet.HardwareWalletConnection; + try { + const usbDevice = await requestHardwareWalletConnection(); + onUsbDeviceChange(usbDevice); + connectionResult = await connect(usbDevice); + onConnected(connectionResult); + setDiscoveryState(DiscoveryState.Idle); + } catch (error) { + setDiscoveryState(DiscoveryState.Idle); + console.error('ERROR connecting hardware wallet', error); + + if (error.innerError?.innerError?.message === 'The device is already open.') { + onConnected(); + return; + } + + setConnectionError(parseConnectionError(error)); + } + })(); + }, [connect, discoveryState, onUsbDeviceChange, onConnected]); + + return ( + + ); +}; diff --git a/apps/browser-extension-wallet/src/views/browser-view/features/wallet-setup/components/HardwareWalletFlow/StepCreate.tsx b/apps/browser-extension-wallet/src/views/browser-view/features/wallet-setup/components/HardwareWalletFlow/StepCreate.tsx new file mode 100644 index 000000000..ca209fed9 --- /dev/null +++ b/apps/browser-extension-wallet/src/views/browser-view/features/wallet-setup/components/HardwareWalletFlow/StepCreate.tsx @@ -0,0 +1,95 @@ +import { Wallet } from '@lace/cardano'; +import { WalletSetupHWCreationStep } from '@lace/core'; +import { EnhancedAnalyticsOptInStatus, postHogOnboardingActions } from '@providers/AnalyticsProvider/analyticsTracker'; +import { TFunction } from 'i18next'; +import React, { VFC, useMemo, useEffect, useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { useLocalStorage, useWalletManager } from '@hooks'; +import { config } from '@src/config'; +import { useAnalyticsContext } from '@providers'; +import { ENHANCED_ANALYTICS_OPT_IN_STATUS_LS_KEY } from '@providers/AnalyticsProvider/config'; + +const { CHAIN } = config(); + +const makeWalletSetupCreateStepTranslations = (t: TFunction) => ({ + title: t('core.walletSetupCreateStep.title'), + description: t('core.walletSetupCreateStep.description') +}); + +enum CreationState { + Idle = 'Idle', + Working = 'Working' +} + +export type WalletData = { + accountIndex: number; + name: string; +}; + +type StepCreateProps = { + connection: Wallet.HardwareWalletConnection; + onError: (error: Error) => void; + walletData: WalletData; +}; + +export const StepCreate: VFC = ({ connection, onError, walletData }) => { + const { t } = useTranslation(); + const [status, setStatus] = useState(CreationState.Idle); + const { createHardwareWalletRevamped, saveHardwareWallet } = useWalletManager(); + const analytics = useAnalyticsContext(); + const [enhancedAnalyticsStatus] = useLocalStorage( + ENHANCED_ANALYTICS_OPT_IN_STATUS_LS_KEY, + EnhancedAnalyticsOptInStatus.OptedOut + ); + + const walletSetupCreateStepTranslations = useMemo(() => makeWalletSetupCreateStepTranslations(t), [t]); + + useEffect(() => { + (async () => { + if (status !== CreationState.Idle) return; + setStatus(CreationState.Working); + + void analytics.sendEventToPostHog(postHogOnboardingActions.hw.SETUP_HW_WALLET_NEXT_CLICK); + + let cardanoWallet: Wallet.CardanoWallet; + try { + cardanoWallet = await createHardwareWalletRevamped({ + connection, + ...walletData + }); + } catch (error) { + console.error('ERROR creating hardware wallet', { error }); + onError(error); + throw error; + } + + const deviceSpec = await Wallet.getDeviceSpec(connection); + await analytics.sendEventToPostHog(postHogOnboardingActions.hw.DONE_GO_TO_WALLET, { + /* eslint-disable camelcase */ + $set_once: { + initial_hardware_wallet_model: deviceSpec.model, + initial_firmware_version: deviceSpec?.firmwareVersion, + initial_cardano_app_version: deviceSpec?.cardanoAppVersion + }, + $set: { wallet_accounts_quantity: '1' } + /* eslint-enable camelcase */ + }); + + await saveHardwareWallet(cardanoWallet, CHAIN); + if (enhancedAnalyticsStatus === EnhancedAnalyticsOptInStatus.OptedIn) { + await analytics.sendAliasEvent(); + } + })(); + }, [ + analytics, + connection, + createHardwareWalletRevamped, + enhancedAnalyticsStatus, + onError, + saveHardwareWallet, + status, + walletData + ]); + + return ; +}; diff --git a/apps/browser-extension-wallet/src/views/browser-view/features/wallet-setup/components/HardwareWalletFlow/index.ts b/apps/browser-extension-wallet/src/views/browser-view/features/wallet-setup/components/HardwareWalletFlow/index.ts new file mode 100644 index 000000000..c7bf754c0 --- /dev/null +++ b/apps/browser-extension-wallet/src/views/browser-view/features/wallet-setup/components/HardwareWalletFlow/index.ts @@ -0,0 +1,2 @@ +export { HardwareWalletFlow } from './HardwareWalletFlow'; +export { makeErrorDialog } from './makeErrorDialog'; diff --git a/apps/browser-extension-wallet/src/views/browser-view/features/wallet-setup/components/ErrorDialog.module.scss b/apps/browser-extension-wallet/src/views/browser-view/features/wallet-setup/components/HardwareWalletFlow/makeErrorDialog.module.scss similarity index 84% rename from apps/browser-extension-wallet/src/views/browser-view/features/wallet-setup/components/ErrorDialog.module.scss rename to apps/browser-extension-wallet/src/views/browser-view/features/wallet-setup/components/HardwareWalletFlow/makeErrorDialog.module.scss index 5e7985546..79059e6cb 100644 --- a/apps/browser-extension-wallet/src/views/browser-view/features/wallet-setup/components/ErrorDialog.module.scss +++ b/apps/browser-extension-wallet/src/views/browser-view/features/wallet-setup/components/HardwareWalletFlow/makeErrorDialog.module.scss @@ -1,5 +1,5 @@ -@import '../../../../../../../../packages/common/src/ui/styles/theme.scss'; -@import '../../../../../../../../packages/common/src/ui/styles/abstracts/typography'; +@import '../../../../../../../../../packages/common/src/ui/styles/theme'; +@import '../../../../../../../../../packages/common/src/ui/styles/abstracts/typography'; .errorDialog { :global(.ant-modal-content) { diff --git a/apps/browser-extension-wallet/src/views/browser-view/features/wallet-setup/components/HardwareWalletFlow/makeErrorDialog.tsx b/apps/browser-extension-wallet/src/views/browser-view/features/wallet-setup/components/HardwareWalletFlow/makeErrorDialog.tsx new file mode 100644 index 000000000..b22f2ca2a --- /dev/null +++ b/apps/browser-extension-wallet/src/views/browser-view/features/wallet-setup/components/HardwareWalletFlow/makeErrorDialog.tsx @@ -0,0 +1,43 @@ +import { TranslationKey } from '@lib/translations/types'; +import React from 'react'; +import { Modal, Typography } from 'antd'; +import { Button } from '@lace/common'; +import styles from './makeErrorDialog.module.scss'; +import { useTranslation } from 'react-i18next'; + +const { Title, Text } = Typography; + +type TranslationKeys = Record<'title' | 'description' | 'confirm', TranslationKey>; + +interface ErrorDialogProps { + visible: boolean; + onRetry: () => void; + errorCode?: ErrorCode; +} + +export const makeErrorDialog = + (translationsMap: Record) => + ({ visible, onRetry, errorCode }: ErrorDialogProps): React.ReactElement => { + const { t } = useTranslation(); + const errorTranslationKeys = translationsMap[errorCode]; + + return ( + + + {t(errorTranslationKeys.title)} + + {t(errorTranslationKeys.description)} + + + ); + }; diff --git a/apps/browser-extension-wallet/src/views/browser-view/features/wallet-setup/components/WalletSetup.tsx b/apps/browser-extension-wallet/src/views/browser-view/features/wallet-setup/components/WalletSetup.tsx index 9e7b7635b..4c6bff0ef 100644 --- a/apps/browser-extension-wallet/src/views/browser-view/features/wallet-setup/components/WalletSetup.tsx +++ b/apps/browser-extension-wallet/src/views/browser-view/features/wallet-setup/components/WalletSetup.tsx @@ -107,11 +107,7 @@ export const WalletSetup = ({ initialStep = WalletSetupSteps.Mnemonic }: WalletS /> - location.reload()} - sendAnalytics={sendAnalyticsHandler} - /> + )} diff --git a/apps/browser-extension-wallet/src/views/browser-view/features/wallet-setup/components/WalletSetupMainPage.tsx b/apps/browser-extension-wallet/src/views/browser-view/features/wallet-setup/components/WalletSetupMainPage.tsx index 52ac42238..085fb99d5 100644 --- a/apps/browser-extension-wallet/src/views/browser-view/features/wallet-setup/components/WalletSetupMainPage.tsx +++ b/apps/browser-extension-wallet/src/views/browser-view/features/wallet-setup/components/WalletSetupMainPage.tsx @@ -22,9 +22,8 @@ import { useHistory } from 'react-router-dom'; export const WalletSetupMainPage = (): ReactElement => { const history = useHistory(); - const [isDappConnectorWarningOpen, setIsDappConnectorWarningOpen] = useState(false); const [isAnalyticsModalOpen, setIsAnalyticsModalOpen] = useState(false); - const { t: translate, Trans } = useTranslate(); + const { t: translate } = useTranslate(); const analytics = useAnalyticsContext(); const [enhancedAnalyticsStatus, { updateLocalStorage: setDoesUserAllowAnalytics }] = useLocalStorage( @@ -53,7 +52,7 @@ export const WalletSetupMainPage = (): ReactElement => { }; const handleStartHardwareOnboarding = () => { - setIsDappConnectorWarningOpen(true); + history.push(walletRoutePaths.setup.hardware); analytics.sendEventToPostHog(postHogOnboardingActions.hw?.SETUP_OPTION_CLICK); }; @@ -100,23 +99,6 @@ export const WalletSetupMainPage = (): ReactElement => { onRestoreWalletRequest={handleRestoreWallet} translations={walletSetupOptionsStepTranslations} /> - -

- -

- - } - visible={isDappConnectorWarningOpen} - confirmLabel={translate('browserView.walletSetup.confirmExperimentalHwDapp.confirm')} - onCancel={() => setIsDappConnectorWarningOpen(false)} - onConfirm={() => { - setIsDappConnectorWarningOpen(false); - history.push(walletRoutePaths.setup.hardware); - }} - /> process.env.USE_TREZOR_HW === 'true'; -export const isHardwareWalletAvailable = (wallet: Wallet.HardwareWallets): boolean => - wallet !== WalletType.Trezor || isTrezorHWSupported(); -type HardwareWalletPersonProperties = { - model: string; - firmwareVersion?: string; - cardanoAppVersion?: string; -}; - -export const getTrezorSpecifications = async (): Promise => { - const { model, major_version, minor_version, patch_version } = - await HardwareTrezor.TrezorKeyAgent.checkDeviceConnection(Wallet.KeyManagement.CommunicationType.Web); - return { - model: `${WalletType.Trezor} model ${model}`, - firmwareVersion: `${major_version}.${minor_version}.${patch_version}` - }; -}; - -export const getLedgerSpecifications = async ( - deviceConnection: HardwareLedger.LedgerKeyAgent['deviceConnection'] -): Promise => { - const cardanoApp = await deviceConnection.getVersion(); - return { - model: deviceConnection.transport.deviceModel.id, - cardanoAppVersion: `${cardanoApp.version.major}.${cardanoApp.version.minor}.${cardanoApp.version.patch}` - }; -}; - -export const getHWPersonProperties = async ( - connectedDevice: Wallet.HardwareWallets, - deviceConnection: Wallet.DeviceConnection -): Promise => { - // TODO: Remove these hardcoded specs once we have a logic that will prevent additional interaction with 3rd party Trezor Connect popup - const trezorSpecificationsHC: HardwareWalletPersonProperties = { - // We are only accepting Model T for now - model: 'Trezor model T' - }; - const HWSpecifications = - connectedDevice === WalletType.Trezor - ? trezorSpecificationsHC - : await getLedgerSpecifications(deviceConnection as HardwareLedger.LedgerKeyAgent['deviceConnection']); - return { - $set_once: { - initial_hardware_wallet_model: HWSpecifications.model, - initial_firmware_version: HWSpecifications?.firmwareVersion, - initial_cardano_app_version: HWSpecifications?.cardanoAppVersion - } - }; -}; diff --git a/packages/cardano/package.json b/packages/cardano/package.json index 484414c1b..eb10574bb 100644 --- a/packages/cardano/package.json +++ b/packages/cardano/package.json @@ -41,14 +41,16 @@ "@cardano-sdk/cardano-services-client": "0.18.0", "@cardano-sdk/core": "0.30.0", "@cardano-sdk/crypto": "0.1.22", - "@cardano-sdk/hardware-ledger": "0.8.19", + "@cardano-sdk/hardware-ledger": "0.9.0", "@cardano-sdk/hardware-trezor": "0.4.19", "@cardano-sdk/key-management": "0.20.1", "@cardano-sdk/util": "0.15.0", - "@cardano-sdk/wallet": "0.35.2", - "@cardano-sdk/web-extension": "0.26.1", + "@cardano-sdk/wallet": "0.36.0", + "@cardano-sdk/web-extension": "0.26.2", "@lace/common": "0.1.0", + "@ledgerhq/devices": "^8.2.1", "@stablelib/chacha20poly1305": "1.0.1", + "@trezor/transport": "^1.1.18", "bignumber.js": "9.0.1", "buffer": "6.0.3", "classnames": "2.3.1", diff --git a/packages/cardano/src/wallet/lib/hardware-wallet.ts b/packages/cardano/src/wallet/lib/hardware-wallet.ts index ded662d2c..883d35fab 100644 --- a/packages/cardano/src/wallet/lib/hardware-wallet.ts +++ b/packages/cardano/src/wallet/lib/hardware-wallet.ts @@ -1,10 +1,12 @@ /* eslint-disable unicorn/no-null */ +import { Bip32PublicKeyHex } from '@cardano-sdk/crypto'; import * as KeyManagement from '@cardano-sdk/key-management'; -import { DeviceConnection, HardwareWallets } from '../types'; +import { HardwareWalletConnection, DeviceConnection, HardwareWallets, LedgerConnection } from '../types'; import * as HardwareLedger from '@cardano-sdk/hardware-ledger'; import * as HardwareTrezor from '@cardano-sdk/hardware-trezor'; import { WalletType } from '@cardano-sdk/web-extension'; -// Using nodejs CML version to satisfy the tests requirements, but this gets replaced by webpack to the browser version in the build +import { ledgerUSBVendorId } from '@ledgerhq/devices'; +import { TREZOR_USB_DESCRIPTORS } from '@trezor/transport'; const isTrezorHWSupported = (): boolean => process.env.USE_TREZOR_HW === 'true'; @@ -20,16 +22,150 @@ export const manifest: KeyManagement.TrezorConfig['manifest'] = { email: process.env.EMAIL_ADDRESS }; +const initializeTrezor = () => + HardwareTrezor.TrezorKeyAgent.initializeTrezorTransport({ + manifest, + communicationType: DEFAULT_COMMUNICATION_TYPE + }); + const connectDevices: Record Promise> = { [WalletType.Ledger]: async () => await HardwareLedger.LedgerKeyAgent.checkDeviceConnection(DEFAULT_COMMUNICATION_TYPE), ...(AVAILABLE_WALLETS.includes(WalletType.Trezor) && { - [WalletType.Trezor]: async () => - await HardwareTrezor.TrezorKeyAgent.initializeTrezorTransport({ - manifest, - communicationType: DEFAULT_COMMUNICATION_TYPE - }) + [WalletType.Trezor]: async () => await initializeTrezor() }) }; export const connectDevice = async (model: HardwareWallets): Promise => await connectDevices[model](); + +type Descriptor = Partial; +type DescriptorEntries = [keyof T, T[keyof T]][]; +const isDeviceDescribedBy = (device: USBDevice, descriptors: Descriptor[]) => + descriptors.some((descriptor) => + (Object.entries(descriptor) as DescriptorEntries).every(([key, value]) => device[key] === value) + ); + +const ledgerNanoSWithNoAppOpenProductId = 4113; +const ledgerNanoSWithCardanoAppOpenProductId = 4117; +const ledgerNanoSPlusWithNoAppOpenProductId = 20_497; +const ledgerNanoSPlusWithCardanoAppOpenProductId = 20_501; +const ledgerNanoXWithNoAppOpenProductId = 16_401; +const ledgerNanoXWithCardanoAppOpenProductId = 16_405; +export const ledgerDescriptors = [ + ledgerNanoSWithNoAppOpenProductId, + ledgerNanoSWithCardanoAppOpenProductId, + ledgerNanoSPlusWithNoAppOpenProductId, + ledgerNanoSPlusWithCardanoAppOpenProductId, + ledgerNanoXWithNoAppOpenProductId, + ledgerNanoXWithCardanoAppOpenProductId +].map((productId) => ({ + vendorId: ledgerUSBVendorId, + productId +})); + +// eslint-disable-next-line unicorn/number-literal-case +const trezorModelTProductId = 0x53_c1; +const trezorDescriptors = TREZOR_USB_DESCRIPTORS.filter(({ productId }) => productId === trezorModelTProductId); +export const supportedHwUsbDescriptors = [...ledgerDescriptors, ...trezorDescriptors]; + +export const connectDeviceRevamped = async (usbDevice: USBDevice): Promise => { + if (isDeviceDescribedBy(usbDevice, ledgerDescriptors)) { + return { + type: WalletType.Ledger, + value: await HardwareLedger.LedgerKeyAgent.establishDeviceConnection(DEFAULT_COMMUNICATION_TYPE, usbDevice) + }; + } + if (isTrezorHWSupported() && isDeviceDescribedBy(usbDevice, trezorDescriptors)) { + await initializeTrezor(); + return { + type: WalletType.Trezor + }; + } + + throw new Error('Could not recognize the device'); +}; + +const invalidDeviceError = new Error('Invalid device type'); + +export const getHwExtendedAccountPublicKey = async ( + walletType: HardwareWallets, + accountIndex: number, + ledgerConnection?: LedgerConnection +): Promise => { + if (walletType === WalletType.Ledger) { + return HardwareLedger.LedgerKeyAgent.getXpub({ + communicationType: DEFAULT_COMMUNICATION_TYPE, + deviceConnection: ledgerConnection, + accountIndex + }); + } + if (isTrezorHWSupported() && walletType === WalletType.Trezor) { + return HardwareTrezor.TrezorKeyAgent.getXpub({ + communicationType: DEFAULT_COMMUNICATION_TYPE, + accountIndex + }); + } + throw invalidDeviceError; +}; + +type DeviceSpec = { + model: string; + firmwareVersion?: string; + cardanoAppVersion?: string; +}; + +const makeVersion = (major: number, minor: number, patch: number) => `${major}.${minor}.${patch}`; + +export const getDeviceSpec = async (connection: HardwareWalletConnection): Promise => { + if (connection.type === WalletType.Ledger) { + const { version } = await connection.value.getVersion(); + return { + model: connection.value.transport.deviceModel.id, + cardanoAppVersion: makeVersion(version.major, version.minor, version.patch) + }; + } + if (isTrezorHWSupported() && connection.type === WalletType.Trezor) { + // TODO: Remove these hardcoded specs once we have a logic that will prevent additional interaction with 3rd party Trezor Connect popup + const hardcodeTrezorSpec = true; + if (hardcodeTrezorSpec) { + return { + model: 'Trezor model T' + }; + } + + const features = await HardwareTrezor.TrezorKeyAgent.checkDeviceConnection(DEFAULT_COMMUNICATION_TYPE); + return { + model: `${WalletType.Trezor} model ${features.model}`, + firmwareVersion: makeVersion(features.major_version, features.minor_version, features.patch_version) + }; + } + + throw invalidDeviceError; +}; + +type SoftwareVersion = { + major: number; + minor: number; + patch: number; +}; + +export const initConnectionAndGetSoftwareVersion = async (type: HardwareWallets): Promise => { + if (type === WalletType.Ledger) { + const connection = await HardwareLedger.LedgerKeyAgent.establishDeviceConnection(DEFAULT_COMMUNICATION_TYPE); + const { version } = await connection.getVersion(); + return version; + } + if (isTrezorHWSupported() && type === WalletType.Trezor) { + // To allow checks once the app is refreshed. It won't affect the user flow + // TODO: Smarter Trezor initialization logic after onboarding revamp LW-9808 + await initializeTrezor(); + const features = await HardwareTrezor.TrezorKeyAgent.checkDeviceConnection(DEFAULT_COMMUNICATION_TYPE); + return { + major: features.major_version, + minor: features.minor_version, + patch: features.patch_version + }; + } + + throw invalidDeviceError; +}; diff --git a/packages/cardano/src/wallet/types.ts b/packages/cardano/src/wallet/types.ts index 21e2dd835..2942522c5 100644 --- a/packages/cardano/src/wallet/types.ts +++ b/packages/cardano/src/wallet/types.ts @@ -2,10 +2,21 @@ import { Cardano, Paginated } from '@cardano-sdk/core'; import type { LedgerKeyAgent } from '@cardano-sdk/hardware-ledger'; import { WalletType } from '@cardano-sdk/web-extension'; -export type DeviceConnection = LedgerKeyAgent['deviceConnection'] | boolean; +export type LedgerConnection = LedgerKeyAgent['deviceConnection']; + +export type DeviceConnection = LedgerConnection | boolean; export type HardwareWallets = WalletType.Trezor | WalletType.Ledger; +export type HardwareWalletConnection = + | { + type: WalletType.Trezor; + } + | { + type: WalletType.Ledger; + value: LedgerConnection; + }; + export type StakePoolSearchResults = Paginated; export type DappInfo = { @@ -69,10 +80,3 @@ export enum WalletManagerProviderTypes { } export type ChainName = keyof typeof Cardano.ChainIds; - -export interface CreateHardwareWalletArgs { - deviceConnection: DeviceConnection; - name: string; - accountIndex: number; - activeChainId: Cardano.ChainId; -} diff --git a/packages/core/src/ui/components/WalletSetup/WalletSetupCreationStep.tsx b/packages/core/src/ui/components/WalletSetup/WalletSetupCreationStep.tsx deleted file mode 100644 index 922fca549..000000000 --- a/packages/core/src/ui/components/WalletSetup/WalletSetupCreationStep.tsx +++ /dev/null @@ -1,28 +0,0 @@ -/* eslint-disable no-magic-numbers */ -import React from 'react'; -import cn from 'classnames'; -import { WalletSetupStepLayout, WalletTimelineSteps } from './WalletSetupStepLayout'; -import { TranslationsFor } from '@ui/utils/types'; -import styles from './WalletSetupFinalStep.module.scss'; -import { Loader } from '@lace/common'; - -type TranslationKeys = 'title' | 'description'; -export interface WalletSetupCreationStepProps { - translations: TranslationsFor; - isHardwareWallet?: boolean; -} -export const WalletSetupCreationStep = ({ - translations, - isHardwareWallet = false -}: WalletSetupCreationStepProps): React.ReactElement => ( - -
- -
-
-); diff --git a/packages/core/src/ui/components/WalletSetup/index.ts b/packages/core/src/ui/components/WalletSetup/index.ts index a67bcfde6..0977ff451 100644 --- a/packages/core/src/ui/components/WalletSetup/index.ts +++ b/packages/core/src/ui/components/WalletSetup/index.ts @@ -3,7 +3,6 @@ export * from './WalletSetupStepLayout'; export * from './WalletSetupRegisterStep'; export * from './WalletSetupMnemonicIntroStep'; export * from './WalletSetupMnemonicStep'; -export * from './WalletSetupCreationStep'; export * from './WalletSetupModeStep'; export * from './WalletSetupFinalStep'; export * from './WalletSetupMnemonicVerificationStep'; diff --git a/packages/core/src/ui/components/WalletSetupRevamp/WalletSetupConnectHardwareWalletStepRevamp.module.scss b/packages/core/src/ui/components/WalletSetupRevamp/WalletSetupConnectHardwareWalletStepRevamp.module.scss new file mode 100644 index 000000000..120e3e063 --- /dev/null +++ b/packages/core/src/ui/components/WalletSetupRevamp/WalletSetupConnectHardwareWalletStepRevamp.module.scss @@ -0,0 +1,20 @@ +@import '../../styles/theme.scss'; +@import '../../../../../common/src/ui/styles/abstracts/typography'; + +.wrapper { + align-items: center; + display: flex; + flex-direction: column; + gap: size_unit(1); + height: 100%; + justify-content: center; +} + +.loader { + transform: translate(50%); +} + +.errorImage { + flex-grow: 1; + width: size_unit(16); +} diff --git a/packages/core/src/ui/components/WalletSetupRevamp/WalletSetupConnectHardwareWalletStepRevamp.tsx b/packages/core/src/ui/components/WalletSetupRevamp/WalletSetupConnectHardwareWalletStepRevamp.tsx index d24207145..1e645e54f 100644 --- a/packages/core/src/ui/components/WalletSetupRevamp/WalletSetupConnectHardwareWalletStepRevamp.tsx +++ b/packages/core/src/ui/components/WalletSetupRevamp/WalletSetupConnectHardwareWalletStepRevamp.tsx @@ -1,94 +1,45 @@ -import React, { useState } from 'react'; -import { WalletTimelineSteps } from '../WalletSetup/WalletSetupStepLayout'; -import styles from '../WalletSetup/WalletSetupConnectHardwareWalletStep.module.scss'; -import Icon from '@ant-design/icons'; -import { ReactComponent as LedgerLogo } from '../../assets/icons/ledger-wallet.component.svg'; -import { ReactComponent as TrezorLogo } from '../../assets/icons/trezor-wallet.component.svg'; -import { Typography } from 'antd'; +import { Banner, Loader } from '@lace/common'; import { TranslationsFor } from '@ui/utils/types'; -import classnames from 'classnames'; +import React from 'react'; +import ExclamationCircleIcon from '../../assets/icons/exclamation-circle.svg'; +import { WalletTimelineSteps } from '../WalletSetup'; +import styles from './WalletSetupConnectHardwareWalletStepRevamp.module.scss'; import { WalletSetupStepLayoutRevamp } from './WalletSetupStepLayoutRevamp'; -const { Text } = Typography; - -const logoMap = { - Ledger: LedgerLogo, - Trezor: TrezorLogo -}; - -export interface WalletSetupConnectHardwareWalletStepRevampProps { - wallets: string[]; +export interface WalletSetupConnectHardwareWalletStepProps { onBack: () => void; - onNext: () => void; - onConnect: (model: string) => Promise; - isNextEnable: boolean; - translations: TranslationsFor<'title' | 'subTitle' | 'supportedDevices' | 'connectDevice'>; - isHardwareWallet?: boolean; + translations: TranslationsFor<'title' | 'subTitle' | 'errorMessage' | 'errorCta'>; + state: 'loading' | 'error'; + onRetry?: () => void; } export const WalletSetupConnectHardwareWalletStepRevamp = ({ - wallets, onBack, - onNext, - onConnect, - isNextEnable, translations, - isHardwareWallet = false -}: WalletSetupConnectHardwareWalletStepRevampProps): React.ReactElement => { - const [walletModel, setWalletModel] = useState(); - const [isConnecting, setIsConnecting] = useState(false); - const isButtonActive = (model: string) => model === walletModel; - - const handleConnect = async (model: string) => { - setIsConnecting(true); - setWalletModel(model); - try { - await onConnect(model); - } finally { - setIsConnecting(false); - } - }; - - return ( - - - {translations.subTitle} - -
-
- {wallets.map((wallet: string) => ( - - ))} + state, + onRetry +}: WalletSetupConnectHardwareWalletStepProps): React.ReactElement => ( + +
+ {state === 'loading' && ( +
+
- - {translations.supportedDevices} - -
- - {translations.connectDevice} - -
- ); -}; + )} + {state === 'error' && ( + <> + hardware wallet connection error image + + + )} +
+ +); diff --git a/packages/core/src/ui/components/WalletSetupRevamp/WalletSetupHWCreationStep.module.scss b/packages/core/src/ui/components/WalletSetupRevamp/WalletSetupHWCreationStep.module.scss new file mode 100644 index 000000000..5376ed944 --- /dev/null +++ b/packages/core/src/ui/components/WalletSetupRevamp/WalletSetupHWCreationStep.module.scss @@ -0,0 +1,11 @@ +.walletCreationStep { + align-items: center; + display: flex; + flex-direction: column; + height: 100%; + justify-content: center; +} + +.loader { + transform: translate(50%); +} diff --git a/packages/core/src/ui/components/WalletSetupRevamp/WalletSetupHWCreationStep.tsx b/packages/core/src/ui/components/WalletSetupRevamp/WalletSetupHWCreationStep.tsx new file mode 100644 index 000000000..f8f387e54 --- /dev/null +++ b/packages/core/src/ui/components/WalletSetupRevamp/WalletSetupHWCreationStep.tsx @@ -0,0 +1,25 @@ +import { Loader } from '@lace/common'; +import { TranslationsFor } from '@ui/utils/types'; +import React from 'react'; +import { WalletTimelineSteps } from '../WalletSetup'; +import styles from './WalletSetupHWCreationStep.module.scss'; +import { WalletSetupStepLayoutRevamp } from './WalletSetupStepLayoutRevamp'; + +type WalletSetupCreationStepProps = { + translations: TranslationsFor<'title' | 'description'>; +}; + +export const WalletSetupHWCreationStep = ({ translations }: WalletSetupCreationStepProps): React.ReactElement => ( + +
+
+ +
+
+
+); diff --git a/packages/core/src/ui/components/WalletSetupRevamp/WalletSetupNamePasswordStepRevamp.tsx b/packages/core/src/ui/components/WalletSetupRevamp/WalletSetupNamePasswordStepRevamp.tsx index 4bc0e486b..3e6c142bd 100644 --- a/packages/core/src/ui/components/WalletSetupRevamp/WalletSetupNamePasswordStepRevamp.tsx +++ b/packages/core/src/ui/components/WalletSetupRevamp/WalletSetupNamePasswordStepRevamp.tsx @@ -11,7 +11,7 @@ import { } from '../WalletSetup/WalletSetupNamePasswordStep/utils'; import { WalletNameInput } from '../WalletSetup/WalletSetupNamePasswordStep/WalletNameInput'; import { WalletPasswordConfirmationInput } from '../WalletSetup/WalletSetupNamePasswordStep/WalletPasswordConfirmationInput'; -import { WalletSetupStepLayoutRevamp } from '../WalletSetupRevamp/WalletSetupStepLayoutRevamp'; +import { WalletSetupStepLayoutRevamp } from './WalletSetupStepLayoutRevamp'; import { TranslationsFor } from '@ui/utils/types'; import { useTranslate } from '@ui/hooks'; import { passwordComplexity } from '@ui/utils/password-complexity'; diff --git a/packages/core/src/ui/components/WalletSetupRevamp/WalletSetupSelectAccountsStepRevamp.tsx b/packages/core/src/ui/components/WalletSetupRevamp/WalletSetupSelectAccountsStepRevamp.tsx index 69a31ace8..157ac9d2a 100644 --- a/packages/core/src/ui/components/WalletSetupRevamp/WalletSetupSelectAccountsStepRevamp.tsx +++ b/packages/core/src/ui/components/WalletSetupRevamp/WalletSetupSelectAccountsStepRevamp.tsx @@ -15,8 +15,6 @@ export interface WalletSetupSelectAccountsStepRevampProps { accounts: number; onBack: () => void; onSubmit: (accountIndex: number, name: string) => void; - isHardwareWallet?: boolean; - wallet?: string; isNextLoading?: boolean; } diff --git a/packages/core/src/ui/components/WalletSetupRevamp/WalletSetupStepLayoutRevamp.tsx b/packages/core/src/ui/components/WalletSetupRevamp/WalletSetupStepLayoutRevamp.tsx index 333732cc2..0d295f4e5 100644 --- a/packages/core/src/ui/components/WalletSetupRevamp/WalletSetupStepLayoutRevamp.tsx +++ b/packages/core/src/ui/components/WalletSetupRevamp/WalletSetupStepLayoutRevamp.tsx @@ -3,10 +3,10 @@ import styles from './WalletSetupStepLayoutRevamp.module.scss'; import cn from 'classnames'; import { Button, Timeline } from '@lace/common'; import { Tooltip } from 'antd'; -import { urls } from '../../utils/constants'; +import { urls } from '@ui/utils/constants'; import { useTranslate } from '@ui/hooks'; import i18n from '@ui/lib/i18n'; -import { WalletSetupFlow, useWalletSetupFlow, WalletTimelineSteps } from '../WalletSetup'; +import { WalletTimelineSteps } from '../WalletSetup'; export interface WalletSetupStepLayoutRevampProps { title: React.ReactNode; @@ -27,16 +27,7 @@ export interface WalletSetupStepLayoutRevampProps { isHardwareWallet?: boolean; } -const removeLegalAndAnalyticsStep = ( - steps: { - key: WalletTimelineSteps; - name: string; - }[] -) => { - steps.shift(); -}; - -const getTimelineSteps = (currentStep: WalletTimelineSteps, isHardwareWallet: boolean, flow: WalletSetupFlow) => { +const getTimelineSteps = (currentStep: WalletTimelineSteps, isHardwareWallet: boolean) => { const inMemoryWalletSteps = [ { key: WalletTimelineSteps.RECOVERY_PHRASE, name: i18n.t('core.walletSetupStep.recoveryPhrase') }, { key: WalletTimelineSteps.WALLET_SETUP, name: i18n.t('core.walletSetupStep.walletSetup') }, @@ -51,11 +42,6 @@ const getTimelineSteps = (currentStep: WalletTimelineSteps, isHardwareWallet: bo const walletSteps = isHardwareWallet ? hardwareWalletSteps : inMemoryWalletSteps; - if (flow === WalletSetupFlow.ADD_WALLET) { - // remove legal and analytics step - removeLegalAndAnalyticsStep(walletSteps); - } - if (typeof currentStep !== 'undefined') { const currentStepIndex = walletSteps.findIndex((step) => step.key === currentStep); return walletSteps.map((step, index) => ({ ...step, active: index <= currentStepIndex })); @@ -83,7 +69,6 @@ export const WalletSetupStepLayoutRevamp = ({ }: WalletSetupStepLayoutRevampProps): React.ReactElement => { const { t } = useTranslate(); const nextButtonContainerRef = useRef(null); - const flow = useWalletSetupFlow(); const defaultLabel = { next: t('core.walletSetupStep.next'), @@ -91,7 +76,7 @@ export const WalletSetupStepLayoutRevamp = ({ skip: t('core.walletSetupStep.skip') }; - const timelineSteps = getTimelineSteps(currentTimelineStep, isHardwareWallet, flow); + const timelineSteps = getTimelineSteps(currentTimelineStep, isHardwareWallet); return (
diff --git a/packages/core/src/ui/components/WalletSetupRevamp/index.ts b/packages/core/src/ui/components/WalletSetupRevamp/index.ts index 3892e3136..4587a7601 100644 --- a/packages/core/src/ui/components/WalletSetupRevamp/index.ts +++ b/packages/core/src/ui/components/WalletSetupRevamp/index.ts @@ -1,8 +1,9 @@ -export * from './WalletSetupSelectAccountsStepRevamp'; -export * from './WalletSetupOptionsStepRevamp'; +export { WalletSetupSelectAccountsStepRevamp } from './WalletSetupSelectAccountsStepRevamp'; +export { WalletSetupOptionsStepRevamp } from './WalletSetupOptionsStepRevamp'; export { MnemonicVideoPopupContent } from './MnemonicVideoPopupContent'; export { WalletSetupMnemonicStepRevamp } from './WalletSetupMnemonicStepRevamp'; export { WalletSetupMnemonicVerificationStepRevamp } from './WalletSetupMnemonicStepRevamp'; export { WalletSetupStepLayoutRevamp } from './WalletSetupStepLayoutRevamp'; export { WalletSetupNamePasswordStepRevamp } from './WalletSetupNamePasswordStepRevamp'; export { WalletSetupConnectHardwareWalletStepRevamp } from './WalletSetupConnectHardwareWalletStepRevamp'; +export { WalletSetupHWCreationStep } from './WalletSetupHWCreationStep'; diff --git a/packages/staking/package.json b/packages/staking/package.json index 334a12ce9..becb11234 100644 --- a/packages/staking/package.json +++ b/packages/staking/package.json @@ -75,6 +75,8 @@ "@cardano-sdk/input-selection": "0.12.26", "@cardano-sdk/tx-construction": "0.18.2", "@cardano-sdk/util": "0.15.0", + "@cardano-sdk/wallet": "0.36.0", + "@cardano-sdk/web-extension": "0.26.2", "@storybook/addon-actions": "^7.6.7", "@storybook/addon-essentials": "^7.6.7", "@storybook/addon-interactions": "^7.6.7", @@ -123,8 +125,8 @@ "@cardano-sdk/input-selection": "0.12.26", "@cardano-sdk/tx-construction": "0.18.2", "@cardano-sdk/util": "0.15.0", - "@cardano-sdk/wallet": "0.35.2", - "@cardano-sdk/web-extension": "0.26.1", + "@cardano-sdk/wallet": "0.36.0", + "@cardano-sdk/web-extension": "0.26.2", "@lace/cardano": "^0.1.0", "@lace/common": "^0.1.0", "@lace/core": "0.1.0", diff --git a/packages/staking/src/features/Drawer/confirmation/StakePoolConfirmationFooter.tsx b/packages/staking/src/features/Drawer/confirmation/StakePoolConfirmationFooter.tsx index 62ae3df8b..90ac80e91 100644 --- a/packages/staking/src/features/Drawer/confirmation/StakePoolConfirmationFooter.tsx +++ b/packages/staking/src/features/Drawer/confirmation/StakePoolConfirmationFooter.tsx @@ -37,14 +37,14 @@ export const StakePoolConfirmationFooter = ({ popupView }: StakePoolConfirmation const [openPoolsManagementConfirmationModal, setOpenPoolsManagementConfirmationModal] = useState(null); - const isInMemory = walletType === WalletType.InMemory; + const isHardwareWallet = walletType === WalletType.Trezor || walletType === WalletType.Ledger; // TODO unify const signAndSubmitTransaction = useCallback(async () => { if (!delegationTxBuilder) throw new Error('Unable to submit transaction. The delegationTxBuilder not available'); const isMultidelegation = draftPortfolio && draftPortfolio.length > 1; - if (!isInMemory && isMultidelegation) { + if (isHardwareWallet && isMultidelegation) { const isSupported = await isMultidelegationSupportedByDevice(walletType); if (!isSupported) { throw new Error('MULTIDELEGATION_NOT_SUPPORTED'); @@ -52,11 +52,18 @@ export const StakePoolConfirmationFooter = ({ popupView }: StakePoolConfirmation } const signedTx = await delegationTxBuilder.build().sign(); await inMemoryWallet.submitTx(signedTx); - }, [delegationTxBuilder, inMemoryWallet, isInMemory, isMultidelegationSupportedByDevice, walletType, draftPortfolio]); + }, [ + delegationTxBuilder, + inMemoryWallet, + isHardwareWallet, + isMultidelegationSupportedByDevice, + walletType, + draftPortfolio, + ]); const handleSubmission = useCallback(async () => { setOpenPoolsManagementConfirmationModal(null); - if (isInMemory) { + if (!isHardwareWallet) { portfolioMutators.executeCommand({ type: 'DrawerContinue' }); return; } @@ -76,8 +83,8 @@ export const StakePoolConfirmationFooter = ({ popupView }: StakePoolConfirmation setIsConfirmingTx(false); } }, [ - currentPortfolio, - isInMemory, + currentPortfolio.length, + isHardwareWallet, portfolioMutators, setIsRestaking, signAndSubmitTransaction, @@ -105,14 +112,14 @@ export const StakePoolConfirmationFooter = ({ popupView }: StakePoolConfirmation }, [analytics, currentPortfolio, draftPortfolio, handleSubmission]); const confirmLabel = useMemo(() => { - if (!isInMemory) { + if (!isHardwareWallet) { const staleLabels = popupView ? t('drawer.confirmation.button.continueInAdvancedView') : t('drawer.confirmation.button.confirmWithDevice', { hardwareWallet: walletType }); return isConfirmingTx ? t('drawer.confirmation.button.signing') : staleLabels; } return t('drawer.confirmation.button.confirm'); - }, [isConfirmingTx, isInMemory, walletType, popupView, t]); + }, [isConfirmingTx, isHardwareWallet, walletType, popupView, t]); return ( <> diff --git a/yarn.lock b/yarn.lock index 6dedb7380..6a4e1308c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7756,9 +7756,9 @@ __metadata: languageName: node linkType: hard -"@cardano-sdk/hardware-ledger@npm:0.8.19, @cardano-sdk/hardware-ledger@npm:~0.8.19": - version: 0.8.19 - resolution: "@cardano-sdk/hardware-ledger@npm:0.8.19" +"@cardano-sdk/hardware-ledger@npm:0.9.0, @cardano-sdk/hardware-ledger@npm:~0.9.0": + version: 0.9.0 + resolution: "@cardano-sdk/hardware-ledger@npm:0.9.0" dependencies: "@cardano-foundation/ledgerjs-hw-app-cardano": ^6.0.0 "@cardano-sdk/core": ~0.30.0 @@ -7768,10 +7768,11 @@ __metadata: "@cardano-sdk/util": ~0.15.0 "@ledgerhq/hw-transport": ^6.28.1 "@ledgerhq/hw-transport-node-hid-noevents": ^6.27.12 - "@ledgerhq/hw-transport-webhid": ^6.27.12 + "@ledgerhq/hw-transport-webusb": ^6.27.12 + node-hid: ^2.1.2 ts-custom-error: ^3.2.0 ts-log: ^2.2.4 - checksum: ac2cb838d10de6cde097a5c1536fbc0f783fe2594bfa9ff00b00a0bf1d98c0dfb919a2439929e6b3969bf17fdd245c919fb3de26834027f29613d10886284cce + checksum: 46811188b5e055de130fc661c8235dfc40881427b570ff4ed301620808e413c7693e4f052e94b2c49947262310b3a44e65e44caa96055c9b930e5eb1f63cc2ff languageName: node linkType: hard @@ -7895,14 +7896,14 @@ __metadata: languageName: node linkType: hard -"@cardano-sdk/wallet@npm:0.35.2, @cardano-sdk/wallet@npm:~0.35.2": - version: 0.35.2 - resolution: "@cardano-sdk/wallet@npm:0.35.2" +"@cardano-sdk/wallet@npm:0.36.0, @cardano-sdk/wallet@npm:~0.36.0": + version: 0.36.0 + resolution: "@cardano-sdk/wallet@npm:0.36.0" dependencies: "@cardano-sdk/core": ~0.30.0 "@cardano-sdk/crypto": ~0.1.22 "@cardano-sdk/dapp-connector": ~0.12.14 - "@cardano-sdk/hardware-ledger": ~0.8.19 + "@cardano-sdk/hardware-ledger": ~0.9.0 "@cardano-sdk/hardware-trezor": ~0.4.19 "@cardano-sdk/input-selection": ~0.12.26 "@cardano-sdk/key-management": ~0.20.1 @@ -7918,24 +7919,24 @@ __metadata: rxjs: ^7.4.0 ts-custom-error: ^3.2.0 ts-log: ^2.2.3 - checksum: af592b22f6290c6640b71234a28781c251f6c4b7ca2f8fb6cb608bb4e9581fb386ba75b1698bb83a81f638202ab55a300f7a395e2138eb96cc96ba9490c2c69d + checksum: 9fadb07be8ab4b603383e41b6712f2b09ed32075d60480bcf9bbfe798d2d858a769fceb308febea842b83d3473a225984f665834b190e9bee050386917a1a23c languageName: node linkType: hard -"@cardano-sdk/web-extension@npm:0.26.1": - version: 0.26.1 - resolution: "@cardano-sdk/web-extension@npm:0.26.1" +"@cardano-sdk/web-extension@npm:0.26.2": + version: 0.26.2 + resolution: "@cardano-sdk/web-extension@npm:0.26.2" dependencies: "@cardano-sdk/core": ~0.30.0 "@cardano-sdk/crypto": ~0.1.22 "@cardano-sdk/dapp-connector": ~0.12.14 - "@cardano-sdk/hardware-ledger": ~0.8.19 + "@cardano-sdk/hardware-ledger": ~0.9.0 "@cardano-sdk/hardware-trezor": ~0.4.19 "@cardano-sdk/key-management": ~0.20.1 "@cardano-sdk/tx-construction": ~0.18.2 "@cardano-sdk/util": ~0.15.0 "@cardano-sdk/util-rxjs": ~0.7.9 - "@cardano-sdk/wallet": ~0.35.2 + "@cardano-sdk/wallet": ~0.36.0 backoff-rxjs: ^7.0.0 lodash: ^4.17.21 rxjs: ^7.4.0 @@ -7943,7 +7944,7 @@ __metadata: ts-log: ^2.2.3 uuid: ^8.3.2 webextension-polyfill: ^0.8.0 - checksum: 3362f8fedb1af6ad2b167763edc640eb879c87d25ac27ec3f12be6506c61951d02fea4ad3de9f9817528333d7e6cb11298309b81f450cd58eac35d7a302f5476 + checksum: 08bb56073d783f730b254e7c6a8e51448e29d9ce436d0156da6f32c4420868ad1890126b2e24085721d150ceea693fa7d3e6640e24f02f97790263e6b718b3f9 languageName: node linkType: hard @@ -10583,8 +10584,8 @@ __metadata: "@cardano-sdk/input-selection": 0.12.26 "@cardano-sdk/tx-construction": 0.18.2 "@cardano-sdk/util": 0.15.0 - "@cardano-sdk/wallet": 0.35.2 - "@cardano-sdk/web-extension": 0.26.1 + "@cardano-sdk/wallet": 0.36.0 + "@cardano-sdk/web-extension": 0.26.2 "@emurgo/cardano-message-signing-asmjs": 1.0.1 "@emurgo/cip14-js": ~3.0.1 "@koralabs/handles-public-api-interfaces": ^1.6.6 @@ -10650,16 +10651,18 @@ __metadata: "@cardano-sdk/cardano-services-client": 0.18.0 "@cardano-sdk/core": 0.30.0 "@cardano-sdk/crypto": 0.1.22 - "@cardano-sdk/hardware-ledger": 0.8.19 + "@cardano-sdk/hardware-ledger": 0.9.0 "@cardano-sdk/hardware-trezor": 0.4.19 "@cardano-sdk/key-management": 0.20.1 "@cardano-sdk/util": 0.15.0 "@cardano-sdk/util-dev": 0.19.18 - "@cardano-sdk/wallet": 0.35.2 - "@cardano-sdk/web-extension": 0.26.1 + "@cardano-sdk/wallet": 0.36.0 + "@cardano-sdk/web-extension": 0.26.2 "@emurgo/cardano-message-signing-browser": 1.0.1 "@lace/common": 0.1.0 + "@ledgerhq/devices": ^8.2.1 "@stablelib/chacha20poly1305": 1.0.1 + "@trezor/transport": ^1.1.18 bignumber.js: 9.0.1 buffer: 6.0.3 classnames: 2.3.1 @@ -10821,6 +10824,8 @@ __metadata: "@cardano-sdk/input-selection": 0.12.26 "@cardano-sdk/tx-construction": 0.18.2 "@cardano-sdk/util": 0.15.0 + "@cardano-sdk/wallet": 0.36.0 + "@cardano-sdk/web-extension": 0.26.2 "@lace/cardano": ^0.1.0 "@lace/common": ^0.1.0 "@lace/core": 0.1.0 @@ -10887,8 +10892,8 @@ __metadata: "@cardano-sdk/input-selection": 0.12.26 "@cardano-sdk/tx-construction": 0.18.2 "@cardano-sdk/util": 0.15.0 - "@cardano-sdk/wallet": 0.35.2 - "@cardano-sdk/web-extension": 0.26.1 + "@cardano-sdk/wallet": 0.36.0 + "@cardano-sdk/web-extension": 0.26.2 "@lace/cardano": ^0.1.0 "@lace/common": ^0.1.0 "@lace/core": 0.1.0 @@ -10989,6 +10994,18 @@ __metadata: languageName: node linkType: hard +"@ledgerhq/devices@npm:^8.2.1": + version: 8.2.1 + resolution: "@ledgerhq/devices@npm:8.2.1" + dependencies: + "@ledgerhq/errors": ^6.16.2 + "@ledgerhq/logs": ^6.12.0 + rxjs: ^7.8.1 + semver: ^7.3.5 + checksum: 5c7fa3004a4ebd30b0dcb8563642db308478bbec115102e5404dd0affcc99f880d094137e88c1f2cc064f78d65a5e946d5ebd8db89141977e32860885ea23ebe + languageName: node + linkType: hard + "@ledgerhq/errors@npm:^6.12.3": version: 6.12.3 resolution: "@ledgerhq/errors@npm:6.12.3" @@ -10996,6 +11013,13 @@ __metadata: languageName: node linkType: hard +"@ledgerhq/errors@npm:^6.16.2": + version: 6.16.2 + resolution: "@ledgerhq/errors@npm:6.16.2" + checksum: 2dd796c78b8428339c8906cfe2325e62c211f484576835198a9bf4efc8fed38b4ca5d342bfb08aef6c623720753ea3e5ce77e50367f2808ad5610e3ff54cec70 + languageName: node + linkType: hard + "@ledgerhq/hw-transport-node-hid-noevents@npm:^6.27.12": version: 6.27.12 resolution: "@ledgerhq/hw-transport-node-hid-noevents@npm:6.27.12" @@ -11009,15 +11033,15 @@ __metadata: languageName: node linkType: hard -"@ledgerhq/hw-transport-webhid@npm:^6.27.12": - version: 6.27.12 - resolution: "@ledgerhq/hw-transport-webhid@npm:6.27.12" +"@ledgerhq/hw-transport-webusb@npm:^6.27.12": + version: 6.28.4 + resolution: "@ledgerhq/hw-transport-webusb@npm:6.28.4" dependencies: - "@ledgerhq/devices": ^8.0.0 - "@ledgerhq/errors": ^6.12.3 - "@ledgerhq/hw-transport": ^6.28.1 - "@ledgerhq/logs": ^6.10.1 - checksum: 5f5253417ba6f5eb4d979e031a24c8bef9a642bb6e317c49a6abec33e316061c13aac6de8a7b353a907a11bbb7dd14ef16716be4017f4a008a6cc9136a366cdd + "@ledgerhq/devices": ^8.2.1 + "@ledgerhq/errors": ^6.16.2 + "@ledgerhq/hw-transport": ^6.30.4 + "@ledgerhq/logs": ^6.12.0 + checksum: 41e3c71b11c9cc8363e42c11874d00f3b4673a3ea6dde738d8f483ea08a4bfe7c529e5db92e17c6e3fe9d585f22eafd90d8c803320fe2e79de09fb31e998240b languageName: node linkType: hard @@ -11032,6 +11056,18 @@ __metadata: languageName: node linkType: hard +"@ledgerhq/hw-transport@npm:^6.30.4": + version: 6.30.4 + resolution: "@ledgerhq/hw-transport@npm:6.30.4" + dependencies: + "@ledgerhq/devices": ^8.2.1 + "@ledgerhq/errors": ^6.16.2 + "@ledgerhq/logs": ^6.12.0 + events: ^3.3.0 + checksum: f4878e0b1ea093c69d2905c94bc9567c1c6694af9cff034634dd9639bd318666f131f2394f67c95d3cfd96b64693e14e4735883136545a19d8df902e3b59bf5e + languageName: node + linkType: hard + "@ledgerhq/logs@npm:^6.10.1": version: 6.10.1 resolution: "@ledgerhq/logs@npm:6.10.1" @@ -11039,6 +11075,13 @@ __metadata: languageName: node linkType: hard +"@ledgerhq/logs@npm:^6.12.0": + version: 6.12.0 + resolution: "@ledgerhq/logs@npm:6.12.0" + checksum: 53fb9ceaf26b2a9fd6e7639b19119f4fef2f814d465fdd910e69c9486dce78137a1790e24f019a03bfabc87e19b2e6683f4da93a7fd203a61117a709fdf6484c + languageName: node + linkType: hard + "@leichtgewicht/ip-codec@npm:^2.0.1": version: 2.0.4 resolution: "@leichtgewicht/ip-codec@npm:2.0.4" @@ -18800,7 +18843,7 @@ __metadata: languageName: node linkType: hard -"@trezor/transport@npm:1.1.18": +"@trezor/transport@npm:1.1.18, @trezor/transport@npm:^1.1.18": version: 1.1.18 resolution: "@trezor/transport@npm:1.1.18" dependencies: