From 3aea974b3b2e76f70d9bc595d1d58a090dc03165 Mon Sep 17 00:00:00 2001 From: brusher_ru Date: Mon, 9 Sep 2024 22:12:03 -0300 Subject: [PATCH 1/4] refactor: use `useCopy` hook everywhere --- src/components/CopyButton.tsx | 19 ++++--------------- src/components/Receive.tsx | 18 +++--------------- .../createWallet/CreateMnemonicScreen.tsx | 16 +++------------- 3 files changed, 10 insertions(+), 43 deletions(-) diff --git a/src/components/CopyButton.tsx b/src/components/CopyButton.tsx index 7314dbc..e576422 100644 --- a/src/components/CopyButton.tsx +++ b/src/components/CopyButton.tsx @@ -1,8 +1,7 @@ -import { useState } from 'react'; - import { IconButton, Tooltip, useBreakpointValue } from '@chakra-ui/react'; import { IconCopy } from '@tabler/icons-react'; -import { useCopyToClipboard } from '@uidotdev/usehooks'; + +import useCopy from '../hooks/useCopy'; type CopyButtonProps = { value: string; @@ -13,26 +12,16 @@ function CopyButton({ value, withOutline = false, }: CopyButtonProps): JSX.Element { - const [isCopied, setIsCopied] = useState(false); - const [, copy] = useCopyToClipboard(); + const { isCopied, onCopy } = useCopy(); const iconSize = useBreakpointValue({ base: 14, md: 18 }, { ssr: false }); - let timeout: ReturnType; - return ( { - clearTimeout(timeout); - copy(value); - setIsCopied(true); - timeout = setTimeout(() => { - setIsCopied(false); - }, 5000); - }} + onClick={() => onCopy(value)} disabled={isCopied} icon={} ml={1} diff --git a/src/components/Receive.tsx b/src/components/Receive.tsx index 8d5e107..456707c 100644 --- a/src/components/Receive.tsx +++ b/src/components/Receive.tsx @@ -1,4 +1,3 @@ -import { useState } from 'react'; import QRCode from 'react-qr-code'; import { @@ -12,8 +11,8 @@ import { ModalOverlay, Text, } from '@chakra-ui/react'; -import { useCopyToClipboard } from '@uidotdev/usehooks'; +import useCopy from '../hooks/useCopy'; import { AccountWithAddress } from '../types/wallet'; import CopyButton from './CopyButton'; @@ -29,18 +28,7 @@ function ReceiveModal({ isOpen, onClose, }: ReceiveModalProps): JSX.Element { - const [isCopied, setIsCopied] = useState(false); - const [, copy] = useCopyToClipboard(); - - let timeout: ReturnType; - const onCopyClick = (data: string) => () => { - clearTimeout(timeout); - copy(data); - setIsCopied(true); - timeout = setTimeout(() => { - setIsCopied(false); - }, 5000); - }; + const { isCopied, onCopy } = useCopy(); return ( @@ -63,7 +51,7 @@ function ReceiveModal({ + + )} - + - diff --git a/src/hooks/useWalletSelectors.ts b/src/hooks/useWalletSelectors.ts index 3e627aa..99bfb0e 100644 --- a/src/hooks/useWalletSelectors.ts +++ b/src/hooks/useWalletSelectors.ts @@ -4,7 +4,13 @@ import { O } from '@mobily/ts-belt'; import { StdTemplateKeys } from '@spacemesh/sm-codec'; import useWallet from '../store/useWallet'; -import { Account, AccountWithAddress } from '../types/wallet'; +import { Account, AccountWithAddress, KeyPairType } from '../types/wallet'; +import { + isMultiSigAccount, + isSingleSigAccount, + isVaultAccount, + isVestingAccount, +} from '../utils/account'; import { AnySpawnArguments } from '../utils/templates'; import { computeAddress } from '../utils/wallet'; @@ -32,3 +38,30 @@ export const useCurrentAccount = ( const acc = accounts[selectedAccount]; return O.fromNullable(acc); }; + +export const useIsLedgerAccount = () => { + const { wallet } = useWallet(); + return (account: AccountWithAddress) => { + if (!wallet) return false; + // TODO + if (isSingleSigAccount(account)) { + const key = wallet.keychain.find( + (k) => k.publicKey === account.spawnArguments.PublicKey + ); + return key?.type === KeyPairType.Hardware; + } + if (isMultiSigAccount(account) || isVestingAccount(account)) { + const keys = wallet.keychain.filter((k) => + account.spawnArguments.PublicKeys.includes(k.publicKey) + ); + return keys.some((k) => k.type === KeyPairType.Hardware); + } + if (isVaultAccount(account)) { + // It has no key, so it cannot be ledger + return false; + } + + // Any other cases are unexpected and default to `false` + return false; + }; +}; diff --git a/src/store/useHardwareWallet.ts b/src/store/useHardwareWallet.ts index 9c6eccf..e69ba84 100644 --- a/src/store/useHardwareWallet.ts +++ b/src/store/useHardwareWallet.ts @@ -7,11 +7,21 @@ import Transport from '@ledgerhq/hw-transport'; import LedgerWebBLE from '@ledgerhq/hw-transport-web-ble'; import LedgerWebUSB from '@ledgerhq/hw-transport-webusb'; import { O } from '@mobily/ts-belt'; -import { ResponseError } from '@zondax/ledger-js'; -import { SpaceMeshApp } from '@zondax/ledger-spacemesh'; +import { LedgerError, ResponseError } from '@zondax/ledger-js'; +import { + Account, + AccountType, + ResponseAddress, + SpaceMeshApp, +} from '@zondax/ledger-spacemesh'; import { HexString } from '../types/common'; -import { KeyOrigin } from '../types/wallet'; +import { AccountWithAddress, KeyOrigin } from '../types/wallet'; +import { + isMultiSigAccount, + isSingleSigAccount, + isVestingAccount, +} from '../utils/account'; import Bip32KeyDerivation from '../utils/bip32'; import { getDisclosureDefaults } from '../utils/disclosure'; import { noop } from '../utils/func'; @@ -44,6 +54,14 @@ export enum LedgerTransports { WebUSB = 'WebUSB', } +export enum VerificationStatus { + Pending = 'Pending', + ApprovedCorrect = 'ApprovedCorrect', + ApprovedWrong = 'ApprovedWrong', + Rejected = 'Rejected', + NotConnected = 'NotConnected', +} + export type LedgerDevice = { type: KeyOrigin.Ledger; transportType: LedgerTransports; @@ -52,6 +70,11 @@ export type LedgerDevice = { actions: { getPubKey: (path: string) => Promise; signTx: (path: string, blob: Uint8Array) => Promise; + verify: ( + path: string, + account: AccountWithAddress, + index?: number + ) => Promise; }; }; @@ -89,6 +112,20 @@ const createLedgerTransport = ( } }; +const verifyAddress = + (account: AccountWithAddress) => (response: ResponseAddress) => + response.address === account.address + ? VerificationStatus.ApprovedCorrect + : VerificationStatus.ApprovedWrong; + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const handleVerificationError = (err: any) => { + if (err.returnCode === LedgerError.TransactionRejected) { + return VerificationStatus.Rejected; + } + throw err; +}; + const useHardwareWallet = (): UseHardwareWalletHook => { const [device, setDevice] = useState(null); const [connectionError, setConnectionError] = useState(null); @@ -133,6 +170,34 @@ const useHardwareWallet = (): UseHardwareWalletHook => { modalApproval.onClose(); return res; }, + verify: async (path, account, index = 0) => { + const result = (() => { + if (isSingleSigAccount(account)) { + return app.getAddressAndPubKey(path, true); + } + if (isMultiSigAccount(account) || isVestingAccount(account)) { + return app.getAddressMultisig( + path, + index, + new Account( + AccountType.Multisig, + account.spawnArguments.Required, + account.spawnArguments.PublicKeys.length, + account.spawnArguments.PublicKeys.map((pk, idx) => ({ + index: idx, + pubkey: Buffer.from(pk, 'hex'), + })) + ) + ); + } + throw new Error('Unsupported account type'); + })(); + + return result + .then(verifyAddress(account)) + .catch(handleVerificationError) + .catch(handleLedgerError); + }, }, }); modalConnect.onClose(); From adec66cd0b16bf5fbd267aca448df77d5a5ad96b Mon Sep 17 00:00:00 2001 From: brusher_ru Date: Fri, 13 Sep 2024 20:21:46 -0300 Subject: [PATCH 4/4] tweak: do not allow to close Receive modal if Ledger approval is pending --- src/components/Receive.tsx | 31 ++++++++++++++++++++----------- 1 file changed, 20 insertions(+), 11 deletions(-) diff --git a/src/components/Receive.tsx b/src/components/Receive.tsx index 451a3f9..5ff187c 100644 --- a/src/components/Receive.tsx +++ b/src/components/Receive.tsx @@ -78,10 +78,12 @@ function ReceiveModal({ }; const close = () => { - setVerificationError(null); - setVerificationStatus(null); + if (verificationStatus !== VerificationStatus.Pending) { + setVerificationError(null); + setVerificationStatus(null); - onClose(); + onClose(); + } }; const verifyAndShow = () => { @@ -95,6 +97,7 @@ function ReceiveModal({ setVerificationStatus(res); }) .catch((err) => { + setVerificationStatus(null); setVerificationError(err); }); }; @@ -103,7 +106,9 @@ function ReceiveModal({ - + {verificationStatus !== VerificationStatus.Pending && ( + + )} Receive funds @@ -122,7 +127,7 @@ function ReceiveModal({ }} value={account.address} /> - + {verificationStatus === VerificationStatus.ApprovedCorrect && ( The address is verified successfully! @@ -130,8 +135,9 @@ function ReceiveModal({ )} {verificationStatus === VerificationStatus.Pending && ( - Please review the address on the Ledger device and approve if it - matches the address above. + Please review the address on the Ledger device. +
+ Approve if it matches the address above.
)} {verificationStatus === VerificationStatus.NotConnected && ( @@ -162,12 +168,12 @@ function ReceiveModal({ )}
{isLedgerBasedAccount(account) && ( - +