From eee21b96d49d5f3b565c70b108c1490bcc611174 Mon Sep 17 00:00:00 2001 From: hotequil Date: Wed, 22 Jan 2025 16:06:37 -0300 Subject: [PATCH] CU-86a682zw2-NEON3 - Swaps Request - Verify if Ledger is unlocked before creating a Swap --- src/main/hardwareWallet.ts | 26 ++++++++++++++- src/renderer/src/hooks/useHardwareWallet.ts | 32 ++++++++++++++++++- src/renderer/src/locales/en/pages.json | 3 +- .../src/routes/pages/Swap/SwapPageContent.tsx | 21 ++++++++++-- src/shared/@types/api.ts | 2 ++ src/shared/@types/i18next-resources.d.ts | 1 + src/shared/@types/ipc.ts | 5 +++ tests/e2e/index.ts | 9 ++++-- 8 files changed, 91 insertions(+), 8 deletions(-) diff --git a/src/main/hardwareWallet.ts b/src/main/hardwareWallet.ts index 08bbae84..c98d5ea8 100644 --- a/src/main/hardwareWallet.ts +++ b/src/main/hardwareWallet.ts @@ -3,7 +3,11 @@ import { ledgerUSBVendorId } from '@ledgerhq/devices' import Transport from '@ledgerhq/hw-transport' import NodeHidTransport, { getDevices } from '@ledgerhq/hw-transport-node-hid-noevents' import { TBlockchainServiceKey } from '@shared/@types/blockchain' -import { TAddHardwareWalletAccountParams, THardwareWalletInfoWithTransport } from '@shared/@types/ipc' +import { + TAddHardwareWalletAccountParams, + THardwareWalletInfoWithTransport, + TIsConnectedAndUnlockedHardwareWalletParams, +} from '@shared/@types/ipc' import { mainApi } from '@shared/api/main' import { usb } from 'usb' @@ -80,9 +84,29 @@ const addNewHardwareAccount = async ({ blockchain, index }: TAddHardwareWalletAc return account } +const isConnectedAndUnlockedHardwareWallet = async ({ + account, + order, +}: TIsConnectedAndUnlockedHardwareWalletParams) => { + if (!account.isHardware) throw new Error("Accounts isn't a hardware wallet") + + const transporter = transporters.find(({ blockchain }) => blockchain === account.blockchain) + + if (!transporter) throw new Error("Transporter isn't found") + + const service = bsAggregator.blockchainServicesByName[transporter.blockchain] + + if (!hasLedger(service)) throw new Error("This blockchain doesn't support hardware wallet") + + const accountFromHardwareService = await service.ledgerService.getAccount(transporter.transport, order) + + return !!accountFromHardwareService +} + export function registerHardwareWalletHandler() { mainApi.listenAsync('connectHardwareWallet', connectHardwareWallet) mainApi.listenAsync('disconnectHardwareWallet', disconnectHardwareWallet) + mainApi.listenAsync('isConnectedAndUnlockedHardwareWallet', ({ args }) => isConnectedAndUnlockedHardwareWallet(args)) mainApi.listenAsync('addNewHardwareAccount', ({ args }) => addNewHardwareAccount(args)) Object.values(bsAggregator.blockchainServicesByName).forEach(service => { diff --git a/src/renderer/src/hooks/useHardwareWallet.ts b/src/renderer/src/hooks/useHardwareWallet.ts index 0d6a43f2..9a6b80a5 100644 --- a/src/renderer/src/hooks/useHardwareWallet.ts +++ b/src/renderer/src/hooks/useHardwareWallet.ts @@ -1,11 +1,14 @@ import { useCallback, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' +import { BlockchainService, BSWithLedger } from '@cityofzion/blockchain-service' import { AccountHelper } from '@renderer/helpers/AccountHelper' import { MnemonicHelper } from '@renderer/helpers/MnemonicHelper' import { ToastHelper } from '@renderer/helpers/ToastHelper' import { UtilsHelper } from '@renderer/helpers/UtilsHelper' +import { bsAggregator } from '@renderer/libs/blockchainService' +import { TBlockchainServiceKey } from '@shared/@types/blockchain' import { THardwareWalletInfo } from '@shared/@types/ipc' -import { IWalletState } from '@shared/@types/store' +import { IAccountState, IWalletState } from '@shared/@types/store' import { useCurrentLoginSessionSelector } from './useAuthSelector' import { useBlockchainActions } from './useBlockchainActions' @@ -67,6 +70,32 @@ export const useHardwareWalletActions = () => { const { createWallet, editAccount, importAccount, editWallet } = useBlockchainActions() const { currentLoginSessionRef } = useCurrentLoginSessionSelector() + const isConnectedAndUnlockedHardwareWallet = useCallback( + async ({ order, encryptedKey, blockchain }: IAccountState) => { + try { + const service = bsAggregator.blockchainServicesByName[blockchain] as BlockchainService & + BSWithLedger + + const key = await window.api.sendAsync('decryptBasedEncryptedSecret', { + value: encryptedKey!, + encryptedSecret: currentLoginSessionRef.current!.encryptedPassword, + }) + + const senderAccount = service.generateAccountFromPublicKey(key) + + senderAccount.isHardware = true + senderAccount.bip44Path = AccountHelper.getBip44Path(service, order) + + return await window.api.sendAsync('isConnectedAndUnlockedHardwareWallet', { account: senderAccount, order }) + } catch (error) { + console.error(error) + + return false + } + }, + [currentLoginSessionRef] + ) + const createHardwareWallet = useCallback( async (infos: THardwareWalletInfo[]) => { if (!currentLoginSessionRef.current) { @@ -180,5 +209,6 @@ export const useHardwareWalletActions = () => { return { createHardwareWallet, addNewHardwareAccount, + isConnectedAndUnlockedHardwareWallet, } } diff --git a/src/renderer/src/locales/en/pages.json b/src/renderer/src/locales/en/pages.json index 8fb1886c..eaa6294f 100644 --- a/src/renderer/src/locales/en/pages.json +++ b/src/renderer/src/locales/en/pages.json @@ -442,7 +442,8 @@ "amountMin": "Amount is too low. Minimum amount is {{amount}}", "amountMax": "Amount is too high. Maximum amount is {{amount}}", "insufficientFunds": "You don't have enough balance", - "insufficientFundsFee": "You don't have enough balance to pay the fee" + "insufficientFundsFee": "You don't have enough balance to pay the fee", + "hardwareWalletNotConnectedOrLocked": "Hardware wallet isn't connected or is locked, verify your device and try again" }, "hints": { "enterValidAddress": "Enter a valid address" diff --git a/src/renderer/src/routes/pages/Swap/SwapPageContent.tsx b/src/renderer/src/routes/pages/Swap/SwapPageContent.tsx index 42ca4c44..61c9a15d 100644 --- a/src/renderer/src/routes/pages/Swap/SwapPageContent.tsx +++ b/src/renderer/src/routes/pages/Swap/SwapPageContent.tsx @@ -34,6 +34,7 @@ import { useActions } from '@renderer/hooks/useActions' import { useCurrentLoginSessionSelector } from '@renderer/hooks/useAuthSelector' import { useBalance } from '@renderer/hooks/useBalances' import { useHasContactsByBlockchain } from '@renderer/hooks/useContactSelector' +import { useHardwareWalletActions } from '@renderer/hooks/useHardwareWallet' import { useModalNavigate } from '@renderer/hooks/useModalRouter' import { usePressOnce } from '@renderer/hooks/usePressOnce' import { useAppDispatch } from '@renderer/hooks/useRedux' @@ -69,6 +70,7 @@ export const SwapPageContent = ({ account }: TProps) => { const { networkByBlockchain } = useSelectedNetworkByBlockchainSelector() const { currentLoginSessionRef } = useCurrentLoginSessionSelector() const { accounts } = useAccountsSelector() + const { isConnectedAndUnlockedHardwareWallet } = useHardwareWalletActions() const dispatch = useAppDispatch() const pressOncePasteAddressToReceive = usePressOnce() @@ -298,6 +300,8 @@ export const SwapPageContent = ({ account }: TProps) => { } const handleSubmit = async () => { + const account = actionData.selectedAccountToUse.value + if ( !swapServiceRef.current || !service || @@ -307,17 +311,28 @@ export const SwapPageContent = ({ account }: TProps) => { !actionData.selectedTokenToReceive.value || !actionData.selectedAmountToUse.value || !actionData.selectedAmountToReceive.value || - !actionData.selectedAccountToUse.value || + !account || !actionData.selectedAddressToReceive.value || !actionData.selectedAddressToReceive.valid || !actionData.selectAmountToUseMinMax.value || isExtraIdToReceiveInvalid - ) { + ) return + + if (account.type === 'hardware' && hasLedger(service)) { + const isConnectedAndUnlocked = await isConnectedAndUnlockedHardwareWallet(account) + + if (!isConnectedAndUnlocked) { + const message = t('form.errors.hardwareWalletNotConnectedOrLocked') + + ToastHelper.error({ message, duration: 8000 }) + + throw new Error(message) + } } const swapRecord: TSwapRecord = { - account: actionData.selectedAccountToUse.value, + account, addressTo: actionData.selectedAddressToReceive.value, extraIdTo: actionData.selectedExtraIdToReceive.value, amountFrom: actionData.selectedAmountToUse.value, diff --git a/src/shared/@types/api.ts b/src/shared/@types/api.ts index 59560390..de352338 100644 --- a/src/shared/@types/api.ts +++ b/src/shared/@types/api.ts @@ -13,6 +13,7 @@ import { THardwareWalletInfo, TIpcMainAsyncListener, TIpcMainSyncListener, + TIsConnectedAndUnlockedHardwareWalletParams, } from './ipc' export type TMainApiListenersSync = { @@ -33,6 +34,7 @@ export type TMainApiListenersAsync = { setWindowButtonPosition: TIpcMainAsyncListener connectHardwareWallet: TIpcMainAsyncListener disconnectHardwareWallet: TIpcMainAsyncListener + isConnectedAndUnlockedHardwareWallet: TIpcMainAsyncListener addNewHardwareAccount: TIpcMainAsyncListener> checkForUpdates: TIpcMainAsyncListener quitAndInstall: TIpcMainAsyncListener diff --git a/src/shared/@types/i18next-resources.d.ts b/src/shared/@types/i18next-resources.d.ts index 73895bca..eb0af979 100644 --- a/src/shared/@types/i18next-resources.d.ts +++ b/src/shared/@types/i18next-resources.d.ts @@ -1287,6 +1287,7 @@ interface Resources { amountMax: 'Amount is too high. Maximum amount is {{amount}}' insufficientFunds: "You don't have enough balance" insufficientFundsFee: "You don't have enough balance to pay the fee" + hardwareWalletNotConnectedOrLocked: "Hardware wallet isn't connected or is locked, verify your device and try again" } hints: { enterValidAddress: 'Enter a valid address' diff --git a/src/shared/@types/ipc.ts b/src/shared/@types/ipc.ts index 64142cac..dd60fddc 100644 --- a/src/shared/@types/ipc.ts +++ b/src/shared/@types/ipc.ts @@ -38,6 +38,11 @@ export type TAddHardwareWalletAccountParams = { blockchain: TBlockchainServiceKey } +export type TIsConnectedAndUnlockedHardwareWalletParams = { + account: Account + order: number +} + export type THardwareWalletInfo = { accounts: Account[] blockchain: TBlockchainServiceKey diff --git a/tests/e2e/index.ts b/tests/e2e/index.ts index ea6abd74..d160879c 100644 --- a/tests/e2e/index.ts +++ b/tests/e2e/index.ts @@ -1,4 +1,4 @@ -import { Page } from '@playwright/test' +import { ElectronApplication, Page } from '@playwright/test' import { _electron as electron } from 'playwright-core' import { TCreateContact } from './types' @@ -6,8 +6,13 @@ import { TCreateContact } from './types' export const PASSWORD = '.7g/7i*Vcf%V3:9Ls3AAt3;i' export const ADDRESSES = ['NRwXs5yZRMuuXUo7AqvetHQ4GDHe3pV7Mb', 'NcuusM86eJ1u1FKxh2qUUpfsQ1kgjZqNrf'] +let electronApp: ElectronApplication + export const launch = async (shouldResetStorage = true) => { - const electronApp = await electron.launch({ args: ['.', '--no-sandbox'] }) + if (electronApp) await electronApp.close() + + electronApp = await electron.launch({ args: ['.', '--no-sandbox'] }) + const window = await electronApp.firstWindow() if (shouldResetStorage) {