Skip to content

Commit

Permalink
CU-86a682zw2-NEON3 - Swaps Request - Verify if Ledger is unlocked bef…
Browse files Browse the repository at this point in the history
…ore creating a Swap
  • Loading branch information
hotequil committed Jan 23, 2025
1 parent 7f4655b commit eee21b9
Show file tree
Hide file tree
Showing 8 changed files with 91 additions and 8 deletions.
26 changes: 25 additions & 1 deletion src/main/hardwareWallet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'

Expand Down Expand Up @@ -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 => {
Expand Down
32 changes: 31 additions & 1 deletion src/renderer/src/hooks/useHardwareWallet.ts
Original file line number Diff line number Diff line change
@@ -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'
Expand Down Expand Up @@ -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<TBlockchainServiceKey> &
BSWithLedger<TBlockchainServiceKey>

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) {
Expand Down Expand Up @@ -180,5 +209,6 @@ export const useHardwareWalletActions = () => {
return {
createHardwareWallet,
addNewHardwareAccount,
isConnectedAndUnlockedHardwareWallet,
}
}
3 changes: 2 additions & 1 deletion src/renderer/src/locales/en/pages.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
21 changes: 18 additions & 3 deletions src/renderer/src/routes/pages/Swap/SwapPageContent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -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()

Expand Down Expand Up @@ -298,6 +300,8 @@ export const SwapPageContent = ({ account }: TProps) => {
}

const handleSubmit = async () => {
const account = actionData.selectedAccountToUse.value

if (
!swapServiceRef.current ||
!service ||
Expand All @@ -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,
Expand Down
2 changes: 2 additions & 0 deletions src/shared/@types/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
THardwareWalletInfo,
TIpcMainAsyncListener,
TIpcMainSyncListener,
TIsConnectedAndUnlockedHardwareWalletParams,
} from './ipc'

export type TMainApiListenersSync = {
Expand All @@ -33,6 +34,7 @@ export type TMainApiListenersAsync = {
setWindowButtonPosition: TIpcMainAsyncListener<Electron.Point, void>
connectHardwareWallet: TIpcMainAsyncListener<undefined, THardwareWalletInfo[]>
disconnectHardwareWallet: TIpcMainAsyncListener<undefined, void>
isConnectedAndUnlockedHardwareWallet: TIpcMainAsyncListener<TIsConnectedAndUnlockedHardwareWalletParams, boolean>
addNewHardwareAccount: TIpcMainAsyncListener<TAddHardwareWalletAccountParams, Account<TBlockchainServiceKey>>
checkForUpdates: TIpcMainAsyncListener<undefined, boolean>
quitAndInstall: TIpcMainAsyncListener<undefined, void>
Expand Down
1 change: 1 addition & 0 deletions src/shared/@types/i18next-resources.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down
5 changes: 5 additions & 0 deletions src/shared/@types/ipc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,11 @@ export type TAddHardwareWalletAccountParams = {
blockchain: TBlockchainServiceKey
}

export type TIsConnectedAndUnlockedHardwareWalletParams = {
account: Account<TBlockchainServiceKey>
order: number
}

export type THardwareWalletInfo = {
accounts: Account<TBlockchainServiceKey>[]
blockchain: TBlockchainServiceKey
Expand Down
9 changes: 7 additions & 2 deletions tests/e2e/index.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,18 @@
import { Page } from '@playwright/test'
import { ElectronApplication, Page } from '@playwright/test'
import { _electron as electron } from 'playwright-core'

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) {
Expand Down

0 comments on commit eee21b9

Please sign in to comment.