Skip to content

Commit

Permalink
feat: improve error handling in ledger, surface incorrect app error
Browse files Browse the repository at this point in the history
  • Loading branch information
edgarkhanzadian committed Jan 10, 2024
1 parent f672bec commit 5bff514
Show file tree
Hide file tree
Showing 15 changed files with 271 additions and 166 deletions.
22 changes: 17 additions & 5 deletions src/app/features/ledger/components/ledger-inline-warnings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,17 @@ import { SupportedBlockchains } from '@shared/constants';
import { WarningLabel } from '@app/components/warning-label';
import { Capitalize } from '@app/ui/utils/capitalize';

import { LatestDeviceResponse } from '../utils/generic-ledger-utils';
import { isStacksLedgerAppClosed } from '../utils/stacks-ledger-utils';

interface RequiresChainProp {
chain: SupportedBlockchains;
}

interface CommonLedgerInlineWarningsProps extends RequiresChainProp {
latestDeviceResponse: any | null;
latestDeviceResponse: LatestDeviceResponse;
outdatedLedgerAppWarning?: boolean;
incorrectAppOpened: boolean;
}

function OutdatedLedgerAppWarning({ chain }: RequiresChainProp) {
Expand All @@ -37,6 +39,16 @@ function LedgerDeviceLockedWarning({ chain }: RequiresChainProp) {
);
}

function LedgerIncorrectAppWarning({ chain }: RequiresChainProp) {
return (
<WarningLabel textAlign="left">
Incorrect app is opened. Close it and open the {''}
<Capitalize>{chain}</Capitalize>
{''} app to continue.
</WarningLabel>
);
}

function LedgerAppClosedWarning({ chain }: RequiresChainProp) {
return (
<WarningLabel textAlign="left">
Expand All @@ -49,14 +61,14 @@ export function CommonLedgerDeviceInlineWarnings({
chain,
latestDeviceResponse,
outdatedLedgerAppWarning = false,
incorrectAppOpened,
}: CommonLedgerInlineWarningsProps) {
if (!latestDeviceResponse) return null;

if (outdatedLedgerAppWarning) {
return <OutdatedLedgerAppWarning chain={chain} />;
}
if (latestDeviceResponse.deviceLocked) return <LedgerDeviceLockedWarning chain={chain} />;
if (isStacksLedgerAppClosed(latestDeviceResponse))
if (latestDeviceResponse?.deviceLocked) return <LedgerDeviceLockedWarning chain={chain} />;
if (incorrectAppOpened) return <LedgerIncorrectAppWarning chain={chain} />;
if (latestDeviceResponse && isStacksLedgerAppClosed(latestDeviceResponse))
return <LedgerAppClosedWarning chain={chain} />;
return null;
}
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,11 @@ import { ledgerSignTxRoutes } from '../../generic-flows/tx-signing/ledger-sign-t
import { useLedgerAnalytics } from '../../hooks/use-ledger-analytics.hook';
import { useLedgerNavigate } from '../../hooks/use-ledger-navigate';
import { connectLedgerBitcoinApp, getBitcoinAppVersion } from '../../utils/bitcoin-ledger-utils';
import { useLedgerResponseState } from '../../utils/generic-ledger-utils';
import {
checkIncorrectAppOpenedError,
checkLockedDeviceError,
useLedgerResponseState,
} from '../../utils/generic-ledger-utils';
import { ApproveSignLedgerBitcoinTx } from './steps/approve-bitcoin-sign-ledger-tx';

export const ledgerBitcoinTxSigningRoutes = ledgerSignTxRoutes({
Expand Down Expand Up @@ -64,6 +68,7 @@ function LedgerSignBitcoinTxContainer() {
useEffect(() => () => setUnsignedTransaction(null), []);

const [latestDeviceResponse, setLatestDeviceResponse] = useLedgerResponseState();
const [incorrectAppOpened, setIncorrectAppOpened] = useState(false);

const [awaitingDeviceConnection, setAwaitingDeviceConnection] = useState(false);

Expand All @@ -75,42 +80,54 @@ function LedgerSignBitcoinTxContainer() {

const signTransaction = async () => {
setAwaitingDeviceConnection(true);
const bitcoinApp = await connectLedgerBitcoinApp(network.chain.bitcoin.bitcoinNetwork)();

try {
const versionInfo = await getBitcoinAppVersion(bitcoinApp);
ledgerAnalytics.trackDeviceVersionInfo(versionInfo);
setAwaitingDeviceConnection(false);
setLatestDeviceResponse(versionInfo as any);
} catch (e) {
setLatestDeviceResponse(e as any);
logger.error('Unable to get Ledger app version info', e);
}
const bitcoinApp = await connectLedgerBitcoinApp(network.chain.bitcoin.bitcoinNetwork)();

ledgerNavigate.toDeviceBusyStep('Verifying public key on Ledger…');
try {
const versionInfo = await getBitcoinAppVersion(bitcoinApp);
ledgerAnalytics.trackDeviceVersionInfo(versionInfo);
setAwaitingDeviceConnection(false);
setLatestDeviceResponse(versionInfo as any);
} catch (e) {
setLatestDeviceResponse(e as any);
logger.error('Unable to get Ledger app version info', e);
}

ledgerNavigate.toConnectionSuccessStep('bitcoin');
await delay(1200);
if (!unsignedTransaction) throw new Error('No unsigned tx');

ledgerNavigate.toAwaitingDeviceOperation({ hasApprovedOperation: false });

try {
const btcTx = await signLedger(bitcoinApp, unsignedTransaction.toPSBT(), inputsToSign);
ledgerNavigate.toDeviceBusyStep('Verifying public key on Ledger…');

if (!btcTx || !unsignedTransactionRaw) throw new Error('No tx returned');
ledgerNavigate.toAwaitingDeviceOperation({ hasApprovedOperation: true });
ledgerNavigate.toConnectionSuccessStep('bitcoin');
await delay(1200);
appEvents.publish('ledgerBitcoinTxSigned', {
signedPsbt: btcTx,
unsignedPsbt: unsignedTransactionRaw,
});
if (!unsignedTransaction) throw new Error('No unsigned tx');

ledgerNavigate.toAwaitingDeviceOperation({ hasApprovedOperation: false });

try {
const btcTx = await signLedger(bitcoinApp, unsignedTransaction.toPSBT(), inputsToSign);

if (!btcTx || !unsignedTransactionRaw) throw new Error('No tx returned');
ledgerNavigate.toAwaitingDeviceOperation({ hasApprovedOperation: true });
await delay(1200);
appEvents.publish('ledgerBitcoinTxSigned', {
signedPsbt: btcTx,
unsignedPsbt: unsignedTransactionRaw,
});
} catch (e) {
logger.error('Unable to sign tx with ledger', e);
ledgerAnalytics.transactionSignedOnLedgerRejected();
ledgerNavigate.toOperationRejectedStep();
} finally {
void bitcoinApp.transport.close();
}
} catch (e) {
logger.error('Unable to sign tx with ledger', e);
ledgerAnalytics.transactionSignedOnLedgerRejected();
ledgerNavigate.toOperationRejectedStep();
} finally {
void bitcoinApp.transport.close();
if (checkIncorrectAppOpenedError(e)) {
setIncorrectAppOpened(true);
return;
}
if (checkLockedDeviceError(e)) {
setLatestDeviceResponse({ deviceLocked: true } as any);
return;
}
}
};

Expand All @@ -120,6 +137,7 @@ function LedgerSignBitcoinTxContainer() {
signTransaction,
latestDeviceResponse,
awaitingDeviceConnection,
incorrectAppOpened,
};

return (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,11 @@ import {
} from '@app/store/accounts/blockchain/stacks/stacks-account.hooks';

import { useLedgerNavigate } from '../../hooks/use-ledger-navigate';
import { useLedgerResponseState } from '../../utils/generic-ledger-utils';
import {
checkIncorrectAppOpenedError,
checkLockedDeviceError,
useLedgerResponseState,
} from '../../utils/generic-ledger-utils';
import {
addSignatureToAuthResponseJwt,
getSha256HashOfJwtAuthPayload,
Expand Down Expand Up @@ -57,6 +61,7 @@ export function LedgerSignJwtContainer() {
}, [location.state]);

const [latestDeviceResponse, setLatestDeviceResponse] = useLedgerResponseState();
const [incorrectAppOpened, setIncorrectAppOpened] = useState(false);

const [awaitingDeviceConnection, setAwaitingDeviceConnection] = useState(false);

Expand Down Expand Up @@ -91,7 +96,15 @@ export function LedgerSignJwtContainer() {

const stacks = await prepareLedgerDeviceStacksAppConnection({
setLoadingState: setAwaitingDeviceConnection,
onError() {
onError(e) {
if (checkIncorrectAppOpenedError(e)) {
setIncorrectAppOpened(true);
return;
}
if (checkLockedDeviceError(e)) {
setLatestDeviceResponse({ deviceLocked: true } as any);
return;
}
ledgerNavigate.toErrorStep();
},
});
Expand Down Expand Up @@ -169,6 +182,7 @@ export function LedgerSignJwtContainer() {
jwtPayloadHash,
latestDeviceResponse,
awaitingDeviceConnection,
incorrectAppOpened,
};

return (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ export const ledgerJwtSigningContext = createContext<LedgerJwtSigningContext>({
awaitingDeviceConnection: false,
signJwtPayload: noop,
jwtPayloadHash: null,
incorrectAppOpened: false,
});

export const LedgerJwtSigningProvider = ledgerJwtSigningContext.Provider;
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { ledgerJwtSigningContext } from '@app/features/ledger/flows/jwt-signing/
import { ConnectLedger } from '@app/features/ledger/generic-steps';

export function ConnectLedgerSignJwt() {
const { signJwtPayload, latestDeviceResponse, awaitingDeviceConnection } =
const { signJwtPayload, latestDeviceResponse, awaitingDeviceConnection, incorrectAppOpened } =
useContext(ledgerJwtSigningContext);

return (
Expand All @@ -16,6 +16,7 @@ export function ConnectLedgerSignJwt() {
<CommonLedgerDeviceInlineWarnings
chain="stacks"
latestDeviceResponse={latestDeviceResponse}
incorrectAppOpened={incorrectAppOpened}
/>
}
onConnectLedger={signJwtPayload}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,37 +23,39 @@ function LedgerRequestBitcoinKeys() {
const ledgerNavigate = useLedgerNavigate();
const network = useCurrentNetwork();

const { requestKeys, latestDeviceResponse, awaitingDeviceConnection } = useRequestLedgerKeys({
chain: 'bitcoin',
connectApp: connectLedgerBitcoinApp(network.chain.bitcoin.bitcoinNetwork),
getAppVersion: getBitcoinAppVersion,
isAppOpen({ name }: { name: string }) {
return name === 'Bitcoin' || name === 'Bitcoin Test';
},
onSuccess() {
navigate('/', { replace: true });
},
async pullKeysFromDevice(app) {
const { keys } = await pullBitcoinKeysFromLedgerDevice(app)({
network: bitcoinNetworkModeToCoreNetworkMode(network.chain.bitcoin.bitcoinNetwork),
onRequestKey(index) {
if (index <= 4) {
ledgerNavigate.toDeviceBusyStep(
`Requesting Bitcoin Native Segwit address (${index + 1}…5)`
);
return;
}
ledgerNavigate.toDeviceBusyStep(`Requesting Bitcoin Taproot address (${index - 4}…5)`);
},
});
dispatch(bitcoinKeysSlice.actions.addKeys(keys));
},
});
const { requestKeys, latestDeviceResponse, awaitingDeviceConnection, incorrectAppOpened } =
useRequestLedgerKeys({
chain: 'bitcoin',
connectApp: connectLedgerBitcoinApp(network.chain.bitcoin.bitcoinNetwork),
getAppVersion: getBitcoinAppVersion,
isAppOpen({ name }: { name: string }) {
return name === 'Bitcoin' || name === 'Bitcoin Test';
},
onSuccess() {
navigate('/', { replace: true });
},
async pullKeysFromDevice(app) {
const { keys } = await pullBitcoinKeysFromLedgerDevice(app)({
network: bitcoinNetworkModeToCoreNetworkMode(network.chain.bitcoin.bitcoinNetwork),
onRequestKey(index) {
if (index <= 4) {
ledgerNavigate.toDeviceBusyStep(
`Requesting Bitcoin Native Segwit address (${index + 1}…5)`
);
return;
}
ledgerNavigate.toDeviceBusyStep(`Requesting Bitcoin Taproot address (${index - 4}…5)`);
},
});
dispatch(bitcoinKeysSlice.actions.addKeys(keys));
},
});

const ledgerContextValue: LedgerRequestKeysContext = {
chain: 'bitcoin',
pullPublicKeysFromDevice: requestKeys,
latestDeviceResponse,
incorrectAppOpened,
awaitingDeviceConnection,
outdatedAppVersionWarning: false,
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,47 +26,53 @@ function LedgerRequestStacksKeys() {

const dispatch = useDispatch();

const { requestKeys, latestDeviceResponse, awaitingDeviceConnection, outdatedAppVersionWarning } =
useRequestLedgerKeys({
chain: 'stacks',
connectApp: connectLedgerStacksApp,
getAppVersion: getStacksAppVersion,
isAppOpen(resp) {
return !isStacksLedgerAppClosed(resp);
},
onSuccess() {
navigate('/', { replace: true });
},
async pullKeysFromDevice(app) {
const resp = await pullStacksKeysFromLedgerDevice(app)({
onRequestKey(accountIndex) {
ledgerNavigate.toDeviceBusyStep(`Requesting STX addresses (${accountIndex + 1}…5)`);
},
});
if (resp.status === 'failure') {
toast.error(resp.errorMessage);
ledgerNavigate.toErrorStep(resp.errorMessage);
return;
}
ledgerNavigate.toDeviceBusyStep();
dispatch(
stacksKeysSlice.actions.addKeys(
resp.publicKeys.map(keys => ({
...keys,
id: keys.path.replace('m', defaultWalletKeyId),
targetId: latestDeviceResponse?.targetId || '',
}))
)
);
},
});
const {
requestKeys,
latestDeviceResponse,
awaitingDeviceConnection,
outdatedAppVersionWarning,
incorrectAppOpened,
} = useRequestLedgerKeys({
chain: 'stacks',
connectApp: connectLedgerStacksApp,
getAppVersion: getStacksAppVersion,
isAppOpen(resp) {
return !isStacksLedgerAppClosed(resp);
},
onSuccess() {
navigate('/', { replace: true });
},
async pullKeysFromDevice(app) {
const resp = await pullStacksKeysFromLedgerDevice(app)({
onRequestKey(accountIndex) {
ledgerNavigate.toDeviceBusyStep(`Requesting STX addresses (${accountIndex + 1}…5)`);
},
});
if (resp.status === 'failure') {
toast.error(resp.errorMessage);
ledgerNavigate.toErrorStep(resp.errorMessage);
return;
}
ledgerNavigate.toDeviceBusyStep();
dispatch(
stacksKeysSlice.actions.addKeys(
resp.publicKeys.map(keys => ({
...keys,
id: keys.path.replace('m', defaultWalletKeyId),
targetId: latestDeviceResponse?.targetId || '',
}))
)
);
},
});

const ledgerContextValue: LedgerRequestKeysContext = {
chain: 'stacks',
pullPublicKeysFromDevice: requestKeys,
latestDeviceResponse,
awaitingDeviceConnection,
outdatedAppVersionWarning,
incorrectAppOpened,
};

return (
Expand Down
Loading

0 comments on commit 5bff514

Please sign in to comment.