Skip to content

Commit

Permalink
feat: add verify address feature
Browse files Browse the repository at this point in the history
  • Loading branch information
brusherru committed Sep 11, 2024
1 parent 2bd2d7d commit 3eb2192
Show file tree
Hide file tree
Showing 4 changed files with 241 additions and 19 deletions.
12 changes: 3 additions & 9 deletions src/components/KeyManager.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -150,12 +150,7 @@ function KeyManager({ isOpen, onClose }: KeyManagerProps): JSX.Element {
};

return (
<Drawer
size="lg"
placement="right"
isOpen={isOpen}
onClose={closeHandler}
>
<Drawer size="lg" placement="right" isOpen={isOpen} onClose={closeHandler}>
<DrawerOverlay />
<DrawerContent>
<DrawerCloseButton zIndex={2} />
Expand Down Expand Up @@ -341,9 +336,8 @@ function KeyManager({ isOpen, onClose }: KeyManagerProps): JSX.Element {
/* eslint-disable max-len */
keys.length > 1 &&
`${keys.length} / ${
(
acc.spawnArguments as MultiSigSpawnArguments
).Required
(acc.spawnArguments as MultiSigSpawnArguments)
.Required
} keys`
/* eslint-enable max-len */
}
Expand Down
142 changes: 136 additions & 6 deletions src/components/Receive.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { useState } from 'react';
import QRCode from 'react-qr-code';

import {
Box,
Button,
Modal,
ModalBody,
Expand All @@ -13,7 +15,17 @@ import {
} from '@chakra-ui/react';

import useCopy from '../hooks/useCopy';
import { useIsLedgerAccount } from '../hooks/useWalletSelectors';
import useHardwareWallet, {
VerificationStatus,
} from '../store/useHardwareWallet';
import useWallet from '../store/useWallet';
import { AccountWithAddress } from '../types/wallet';
import {
isMultiSigAccount,
isSingleSigAccount,
isVestingAccount,
} from '../utils/account';

import CopyButton from './CopyButton';

Expand All @@ -29,9 +41,66 @@ function ReceiveModal({
onClose,
}: ReceiveModalProps): JSX.Element {
const { isCopied, onCopy } = useCopy();
const isLedgerBasedAccount = useIsLedgerAccount();
const { wallet } = useWallet();
const { checkDeviceConnection, connectedDevice } = useHardwareWallet();

const [verificationError, setVerificationError] = useState<Error | null>(
null
);
const [verificationStatus, setVerificationStatus] =
useState<VerificationStatus | null>(null);

const verify = async (): Promise<VerificationStatus> => {
if (!wallet || !(await checkDeviceConnection()) || !connectedDevice) {
return VerificationStatus.NotConnected;
}
if (isSingleSigAccount(account)) {
const key = wallet.keychain.find(
(k) => k.publicKey === account.spawnArguments.PublicKey
);
if (!key || !key.path) {
throw new Error('Key not found');
}
return connectedDevice.actions.verify(key.path, account);
}
if (isMultiSigAccount(account) || isVestingAccount(account)) {
const keyIndex = wallet.keychain.findIndex((k) =>
account.spawnArguments.PublicKeys.includes(k.publicKey)
);
const key = wallet.keychain[keyIndex];
if (!key || !key.path) {
throw new Error('Key not found');
}
return connectedDevice.actions.verify(key.path, account, keyIndex);
}
throw new Error('Unknown account type');
};

const close = () => {
setVerificationError(null);
setVerificationStatus(null);

onClose();
};

const verifyAndShow = () => {
// Show "verify on device" message
setVerificationStatus(VerificationStatus.Pending);
// Reset errors
setVerificationError(null);
// Verify the address
verify()
.then((res) => {
setVerificationStatus(res);
})
.catch((err) => {
setVerificationError(err);
});
};

return (
<Modal isOpen={isOpen} onClose={onClose} isCentered size="xl">
<Modal isOpen={isOpen} onClose={close} isCentered size="xl">
<ModalOverlay />
<ModalContent>
<ModalCloseButton />
Expand All @@ -44,21 +113,82 @@ function ReceiveModal({
<QRCode
bgColor="var(--chakra-colors-brand-lightGray)"
fgColor="var(--chakra-colors-blackAlpha-500)"
style={{ height: 'auto', maxWidth: '100%', width: '100%' }}
style={{
height: 'auto',
maxWidth: '300px',
width: '100%',
marginLeft: 'auto',
marginRight: 'auto',
}}
value={account.address}
/>
<Box mt={2} fontSize="sm" textAlign="center">
{verificationStatus === VerificationStatus.ApprovedCorrect && (
<Text color="brand.green">
The address is verified successfully!
</Text>
)}
{verificationStatus === VerificationStatus.Pending && (
<Text color="yellow">
Please review the address on the Ledger device and approve if it
matches the address above.
</Text>
)}
{verificationStatus === VerificationStatus.NotConnected && (
<Text color="yellow">
The ledger device is not connected.
<br />
Please connect the device and then click &quot;Verify&quot;
button once again.
</Text>
)}
{verificationStatus === VerificationStatus.ApprovedWrong && (
<Text color="brand.red">
The address is incorrect, probably you are using another Ledger
device.
</Text>
)}
{verificationStatus === VerificationStatus.Rejected && (
<Text color="red">
The address was rejected on the Ledger device.
</Text>
)}
{verificationError && (
<Text color="brand.red">
Cannot verify the address:
<br />
{verificationError.message}
</Text>
)}
</Box>
{isLedgerBasedAccount(account) && (
<Box textAlign="center" mt={4}>
<Button
variant="whiteModal"
onClick={verifyAndShow}
disabled={verificationStatus === VerificationStatus.Pending}
w="80%"
maxW={340}
>
Verify
</Button>
</Box>
)}
</ModalBody>
<ModalFooter>
<ModalFooter gap={2}>
<Button
isDisabled={isCopied}
onClick={() => onCopy(account.address)}
mr={2}
w="50%"
w={{ base: '100%', md: '50%' }}
variant="whiteModal"
>
{isCopied ? 'Copied' : 'Copy'}
</Button>
<Button variant="whiteModal" onClick={onClose} ml={2} w="50%">
<Button
variant="whiteModal"
onClick={close}
w={{ base: '100%', md: '50%' }}
>
OK
</Button>
</ModalFooter>
Expand Down
35 changes: 34 additions & 1 deletion src/hooks/useWalletSelectors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -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;
};
};
71 changes: 68 additions & 3 deletions src/store/useHardwareWallet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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;
Expand All @@ -52,6 +70,11 @@ export type LedgerDevice = {
actions: {
getPubKey: (path: string) => Promise<HexString>;
signTx: (path: string, blob: Uint8Array) => Promise<Uint8Array>;
verify: (
path: string,
account: AccountWithAddress,
index?: number
) => Promise<VerificationStatus>;
};
};

Expand Down Expand Up @@ -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<LedgerDevice | null>(null);
const [connectionError, setConnectionError] = useState<string | null>(null);
Expand Down Expand Up @@ -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();
Expand Down

0 comments on commit 3eb2192

Please sign in to comment.