Skip to content

Commit

Permalink
[wallet-ext] add feature to verify your Ledger connection for your ac…
Browse files Browse the repository at this point in the history
…counts (MystenLabs#10502)

## Description

This PR adds a "Verify Ledger Connection" button to the account actions
menu which was requested by the Ledger team as an important feature.
Once you do verify your device, we'll reset the status text back to the
original link.

PS 1: We opted to remove the loading state since the operation is almost
instantaneous
PS 2: The "g" being cut off on the button is a separate visual
regression that I can fix in a follow-up


https://user-images.githubusercontent.com/7453188/230483494-1705548e-fd49-4233-8fba-1da1995433f9.mov

<img width="341" alt="image"
src="https://user-images.githubusercontent.com/7453188/230484733-e192da90-9842-4243-9f5e-3f273e04a867.png">

## Test Plan 
- Manual testing (successful connection, error, etc.)
- Didn't test the "Failed" connection because I don't have a second
Ledger device, but I tested the UI looks good!

---
If your changes are not user-facing and not a breaking change, you can
skip the following section. Otherwise, please indicate what changed, and
then add to the Release Notes section as highlighted during the release
process.

### Type of Change (Check all that apply)

- [X] user-visible impact
- [ ] breaking change for a client SDKs
- [ ] breaking change for FNs (FN binary must upgrade)
- [ ] breaking change for validators or node operators (must upgrade
binaries)
- [ ] breaking change for on-chain data layout
- [ ] necessitate either a data wipe or data migration

### Release notes
N/A
  • Loading branch information
williamrobertson13 authored Apr 7, 2023
1 parent e31c144 commit b9592f1
Show file tree
Hide file tree
Showing 5 changed files with 149 additions and 44 deletions.
15 changes: 6 additions & 9 deletions apps/wallet/src/ui/app/components/menu/content/Account.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,16 +8,16 @@ import { cx } from 'class-variance-authority';

import { AccountBadge } from '../../AccountBadge';
import { AccountActions } from './AccountActions';
import { type AccountType } from '_src/background/keyring/Account';
import { type SerializedAccount } from '_src/background/keyring/Account';
import { useCopyToClipboard } from '_src/ui/app/hooks/useCopyToClipboard';
import { Heading } from '_src/ui/app/shared/heading';

export type AccountProps = {
address: string;
accountType: AccountType;
account: SerializedAccount;
};

export function Account({ address, accountType }: AccountProps) {
export function Account({ account }: AccountProps) {
const { address, type } = account;
const copyCallback = useCopyToClipboard(address, {
copySuccessMessage: 'Address copied',
});
Expand Down Expand Up @@ -46,7 +46,7 @@ export function Account({ address, accountType }: AccountProps) {
>
{formatAddress(address)}
</Heading>
<AccountBadge accountType={accountType} />
<AccountBadge accountType={type} />
</div>
<Copy16
onClick={copyCallback}
Expand All @@ -63,10 +63,7 @@ export function Account({ address, accountType }: AccountProps) {
leaveTo="transform opacity-0"
>
<Disclosure.Panel className="px-5 pb-4">
<AccountActions
accountAddress={address}
accountType={accountType}
/>
<AccountActions account={account} />
</Disclosure.Panel>
</Transition>
</div>
Expand Down
62 changes: 34 additions & 28 deletions apps/wallet/src/ui/app/components/menu/content/AccountActions.tsx
Original file line number Diff line number Diff line change
@@ -1,43 +1,49 @@
// Copyright (c) Mysten Labs, Inc.
// SPDX-License-Identifier: Apache-2.0

import { type SuiAddress } from '@mysten/sui.js';

import { useNextMenuUrl } from '../hooks';
import { AccountType } from '_src/background/keyring/Account';
import { VerifyLedgerConnectionStatus } from './VerifyLedgerConnectionStatus';
import {
AccountType,
type SerializedAccount,
} from '_src/background/keyring/Account';
import { Link } from '_src/ui/app/shared/Link';
import { Text } from '_src/ui/app/shared/text';

export type AccountActionsProps = {
accountAddress: SuiAddress;
accountType: AccountType;
account: SerializedAccount;
};

export function AccountActions({
accountAddress,
accountType,
}: AccountActionsProps) {
const exportAccountUrl = useNextMenuUrl(true, `/export/${accountAddress}`);
const canExportPrivateKey =
accountType === AccountType.DERIVED ||
accountType === AccountType.IMPORTED;
export function AccountActions({ account }: AccountActionsProps) {
const exportAccountUrl = useNextMenuUrl(true, `/export/${account.address}`);

let actionContent: JSX.Element | null = null;
switch (account.type) {
case AccountType.LEDGER:
actionContent = (
<VerifyLedgerConnectionStatus
accountAddress={account.address}
derivationPath={account.derivationPath}
/>
);
break;
case AccountType.IMPORTED:
case AccountType.DERIVED:
actionContent = (
<Link
text="Export Private Key"
to={exportAccountUrl}
color="heroDark"
weight="medium"
/>
);
break;
default:
throw new Error(`Encountered unknown account type`);
}

return (
<div className="flex flex-row flex-nowrap items-center flex-1">
{canExportPrivateKey ? (
<div>
<Link
text="Export Private Key"
to={exportAccountUrl}
color="heroDark"
weight="medium"
/>
</div>
) : (
<Text variant="bodySmall" weight="medium" color="steel">
No actions available
</Text>
)}
<div>{actionContent}</div>
</div>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -37,12 +37,8 @@ export function AccountsSettings() {
return (
<MenuLayout title="Accounts" back={backUrl}>
<div className="flex flex-col gap-3">
{accounts.map(({ address, type }) => (
<Account
key={address}
address={address}
accountType={type}
/>
{accounts.map((account) => (
<Account key={account.address} account={account} />
))}
{isMultiAccountsEnabled ? (
<>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
// Copyright (c) Mysten Labs, Inc.
// SPDX-License-Identifier: Apache-2.0

import { Check12, X12 } from '@mysten/icons';
import { Ed25519PublicKey, type SuiAddress } from '@mysten/sui.js';
import { useEffect, useRef, useState } from 'react';
import toast from 'react-hot-toast';

import { useSuiLedgerClient } from '../../ledger/SuiLedgerClientProvider';
import {
getLedgerConnectionErrorMessage,
getSuiApplicationErrorMessage,
} from '_src/ui/app/helpers/errorMessages';
import { Link } from '_src/ui/app/shared/Link';
import { Text } from '_src/ui/app/shared/text';

export type VerifyLedgerConnectionLinkProps = {
accountAddress: SuiAddress;
derivationPath: string;
};

enum VerificationStatus {
UNKNOWN = 'UNKNOWN',
VERIFIED = 'VERIFIED',
NOT_VERIFIED = 'NOT_VERIFIED',
}

const resetVerificationStatusTimeout = 5000;

export function VerifyLedgerConnectionStatus({
accountAddress,
derivationPath,
}: VerifyLedgerConnectionLinkProps) {
const { connectToLedger } = useSuiLedgerClient();
const [verificationStatus, setVerificationStatus] = useState(
VerificationStatus.UNKNOWN
);
const timeoutIdRef = useRef<number>();

useEffect(() => {
return () => clearTimeout(timeoutIdRef.current);
}, []);

switch (verificationStatus) {
case VerificationStatus.UNKNOWN:
return (
<Link
text="Verify Ledger connection"
onClick={async () => {
try {
const suiLedgerClient = await connectToLedger();
const publicKeyResult =
await suiLedgerClient.getPublicKey(
derivationPath
);
const publicKey = new Ed25519PublicKey(
publicKeyResult.publicKey
);
const suiAddress = publicKey.toSuiAddress();

setVerificationStatus(
accountAddress === suiAddress
? VerificationStatus.VERIFIED
: VerificationStatus.NOT_VERIFIED
);

timeoutIdRef.current = window.setTimeout(() => {
setVerificationStatus(
VerificationStatus.UNKNOWN
);
}, resetVerificationStatusTimeout);
} catch (error) {
const errorMessage =
getLedgerConnectionErrorMessage(error) ||
getSuiApplicationErrorMessage(error) ||
'Something went wrong';
toast.error(errorMessage);
}
}}
color="heroDark"
/>
);
case VerificationStatus.NOT_VERIFIED:
return (
<div className="flex items-center gap-1">
<X12 className="text-issue-dark" />
<Text variant="bodySmall" color="issue-dark">
Ledger is not connected
</Text>
</div>
);
case VerificationStatus.VERIFIED:
return (
<div className="flex items-center gap-1">
<Check12 className="text-success-dark" />
<Text variant="bodySmall" color="success-dark">
Ledger is connected
</Text>
</div>
);
default:
throw new Error(
`Encountered unknown verification status ${verificationStatus}`
);
}
}
2 changes: 1 addition & 1 deletion apps/wallet/src/ui/app/helpers/errorMessages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ export function getLedgerConnectionErrorMessage(error: unknown) {
} else if (error instanceof LedgerNoTransportMechanismError) {
return "Your browser unfortunately doesn't support USB or HID.";
} else if (error instanceof LedgerDeviceNotFoundError) {
return 'Connect your Ledger device and open the Sui app.';
return 'Connect your Ledger device and try again.';
} else if (error instanceof LockedDeviceError) {
return 'Your device is locked. Unlock it and try again.';
}
Expand Down

0 comments on commit b9592f1

Please sign in to comment.