Skip to content

Commit

Permalink
refactor: reuse code for ledger signing in useLedgerSignTx
Browse files Browse the repository at this point in the history
  • Loading branch information
edgarkhanzadian committed Jan 31, 2024
1 parent ec1fdac commit 0dc2c76
Show file tree
Hide file tree
Showing 3 changed files with 189 additions and 157 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { Route, useLocation } from 'react-router-dom';

import * as btc from '@scure/btc-signer';
import { hexToBytes } from '@stacks/common';
import BitcoinApp from 'ledger-bitcoin';
import get from 'lodash.get';

import { BitcoinInputSigningConfig } from '@shared/crypto/bitcoin/signer-config';
Expand All @@ -14,18 +15,21 @@ import { useLocationStateWithCache } from '@app/common/hooks/use-location-state'
import { useScrollLock } from '@app/common/hooks/use-scroll-lock';
import { appEvents } from '@app/common/publish-subscribe';
import { delay } from '@app/common/utils';
import { ApproveSignLedgerBitcoinTx } from '@app/features/ledger/flows/bitcoin-tx-signing/steps/approve-bitcoin-sign-ledger-tx';
import { ledgerSignTxRoutes } from '@app/features/ledger/generic-flows/tx-signing/ledger-sign-tx-route-generator';
import { LedgerTxSigningContext } from '@app/features/ledger/generic-flows/tx-signing/ledger-sign-tx.context';
import { TxSigningFlow } from '@app/features/ledger/generic-flows/tx-signing/tx-signing-flow';
import { useLedgerSignTx } from '@app/features/ledger/generic-flows/tx-signing/use-ledger-sign-tx';
import { useLedgerAnalytics } from '@app/features/ledger/hooks/use-ledger-analytics.hook';
import { useLedgerNavigate } from '@app/features/ledger/hooks/use-ledger-navigate';
import {
connectLedgerBitcoinApp,
getBitcoinAppVersion,
isBitcoinAppOpen,
} from '@app/features/ledger/utils/bitcoin-ledger-utils';
import { useSignLedgerBitcoinTx } from '@app/store/accounts/blockchain/bitcoin/bitcoin.hooks';
import { useCurrentNetwork } from '@app/store/networks/networks.selectors';

import { ledgerSignTxRoutes } from '../../generic-flows/tx-signing/ledger-sign-tx-route-generator';
import { TxSigningFlow } from '../../generic-flows/tx-signing/tx-signing-flow';
import { useLedgerAnalytics } from '../../hooks/use-ledger-analytics.hook';
import { useLedgerNavigate } from '../../hooks/use-ledger-navigate';
import { connectLedgerBitcoinApp, getBitcoinAppVersion } from '../../utils/bitcoin-ledger-utils';
import { checkLockedDeviceError, useLedgerResponseState } from '../../utils/generic-ledger-utils';
import { ApproveSignLedgerBitcoinTx } from './steps/approve-bitcoin-sign-ledger-tx';

export const ledgerBitcoinTxSigningRoutes = ledgerSignTxRoutes({
component: <LedgerSignBitcoinTxContainer />,
customRoutes: (
Expand Down Expand Up @@ -56,67 +60,51 @@ function LedgerSignBitcoinTxContainer() {

useEffect(() => () => setUnsignedTransaction(null), []);

const [latestDeviceResponse, setLatestDeviceResponse] = useLedgerResponseState();

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

if (!inputsToSign) {
ledgerNavigate.cancelLedgerAction();
toast.error('No input signing config defined');
return null;
}

const signTransaction = async () => {
setAwaitingDeviceConnection(true);

try {
const bitcoinApp = await connectLedgerBitcoinApp(network.chain.bitcoin.bitcoinNetwork)();
const chain = 'bitcoin' as const;

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 { signTransaction, latestDeviceResponse, awaitingDeviceConnection } =
useLedgerSignTx<BitcoinApp>({
chain,
isAppOpen: isBitcoinAppOpen({ network: network.chain.bitcoin.bitcoinNetwork }),
getAppVersion: getBitcoinAppVersion,
connectApp: connectLedgerBitcoinApp(network.chain.bitcoin.bitcoinNetwork),
async signTransactionWithDevice(bitcoinApp) {
if (!inputsToSign) {
ledgerNavigate.cancelLedgerAction();
toast.error('No input signing config defined');
return;
}

ledgerNavigate.toDeviceBusyStep('Verifying public key on Ledger…');
ledgerNavigate.toDeviceBusyStep('Verifying public key on Ledger…');

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);

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,
});
} catch (e) {
logger.error('Unable to sign tx with ledger', e);
ledgerAnalytics.transactionSignedOnLedgerRejected();
ledgerNavigate.toOperationRejectedStep();
} finally {
void bitcoinApp.transport.close();
}
} catch (e) {
if (e instanceof Error && checkLockedDeviceError(e)) {
setLatestDeviceResponse({ deviceLocked: true } as any);
return;
}
}
};
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();
}
},
});

const ledgerContextValue: LedgerTxSigningContext = {
chain: 'bitcoin',
chain,
transaction: unsignedTransaction,
signTransaction,
latestDeviceResponse,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
import { useEffect, useMemo, useState } from 'react';
import { Route, useLocation, useNavigate } from 'react-router-dom';
import { Route, useLocation } from 'react-router-dom';

import { deserializeTransaction } from '@stacks/transactions';
import { LedgerError } from '@zondax/ledger-stacks';
import StacksApp, { LedgerError } from '@zondax/ledger-stacks';
import get from 'lodash.get';

import { logger } from '@shared/logger';
import { RouteUrls } from '@shared/route-urls';

import { useScrollLock } from '@app/common/hooks/use-scroll-lock';
Expand All @@ -16,20 +15,20 @@ import {
createWaitForUserToSeeWarningScreen,
} from '@app/features/ledger/generic-flows/tx-signing/ledger-sign-tx.context';
import {
connectLedgerStacksApp,
getStacksAppVersion,
isVersionOfLedgerStacksAppWithContractPrincipalBug,
prepareLedgerDeviceStacksAppConnection,
isStacksAppOpen,
signLedgerStacksTransaction,
signStacksTransactionWithSignature,
} from '@app/features/ledger/utils/stacks-ledger-utils';
import { useCurrentStacksAccount } from '@app/store/accounts/blockchain/stacks/stacks-account.hooks';

import { ledgerSignTxRoutes } from '../../generic-flows/tx-signing/ledger-sign-tx-route-generator';
import { TxSigningFlow } from '../../generic-flows/tx-signing/tx-signing-flow';
import { useLedgerSignTx } from '../../generic-flows/tx-signing/use-ledger-sign-tx';
import { useLedgerAnalytics } from '../../hooks/use-ledger-analytics.hook';
import { useLedgerNavigate } from '../../hooks/use-ledger-navigate';
import { useVerifyMatchingLedgerStacksPublicKey } from '../../hooks/use-verify-matching-stacks-public-key';
import { checkLockedDeviceError, useLedgerResponseState } from '../../utils/generic-ledger-utils';
import { ApproveSignLedgerStacksTx } from './steps/approve-sign-stacks-ledger-tx';

export const ledgerStacksTxSigningRoutes = ledgerSignTxRoutes({
Expand All @@ -41,7 +40,6 @@ export const ledgerStacksTxSigningRoutes = ledgerSignTxRoutes({

function LedgerSignStacksTxContainer() {
const location = useLocation();
const navigate = useNavigate();
const ledgerNavigate = useLedgerNavigate();
const ledgerAnalytics = useLedgerAnalytics();
useScrollLock(true);
Expand All @@ -51,7 +49,7 @@ function LedgerSignStacksTxContainer() {

const hasUserSkippedBuggyAppWarning = useMemo(() => createWaitForUserToSeeWarningScreen(), []);

const chain = 'stacks';
const chain = 'stacks' as const;

useEffect(() => {
const tx = get(location.state, 'tx');
Expand All @@ -60,102 +58,64 @@ function LedgerSignStacksTxContainer() {

useEffect(() => () => setUnsignedTx(null), []);

const [latestDeviceResponse, setLatestDeviceResponse] = useLedgerResponseState();

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

const signTransaction = async () => {
if (!account) return;
const { signTransaction, latestDeviceResponse, awaitingDeviceConnection } =
useLedgerSignTx<StacksApp>({
chain,
isAppOpen: isStacksAppOpen,
getAppVersion: getStacksAppVersion,
connectApp: connectLedgerStacksApp,
async signTransactionWithDevice(stacksApp) {
// TODO: need better handling
if (!account) return;

ledgerNavigate.toDeviceBusyStep('Verifying public key on Ledger…');
await verifyLedgerPublicKey(stacksApp);
ledgerNavigate.toConnectionSuccessStep('stacks');
await delay(1000);
if (!unsignedTx) throw new Error('No unsigned tx');

ledgerNavigate.toAwaitingDeviceOperation({ hasApprovedOperation: false });

const resp = await signLedgerStacksTransaction(stacksApp)(
Buffer.from(unsignedTx, 'hex'),
account.index
);

// Assuming here that public keys are wrong. Alternatively, we may want
// to proactively check the key before signing
if (resp.returnCode === LedgerError.DataIsInvalid) {
ledgerNavigate.toDevicePayloadInvalid();
return;
}

const stacksApp = await prepareLedgerDeviceStacksAppConnection({
setLoadingState: setAwaitingDeviceConnection,
onError(e) {
if (e instanceof Error && checkLockedDeviceError(e)) {
setLatestDeviceResponse({ deviceLocked: true } as any);
if (resp.returnCode === LedgerError.TransactionRejected) {
ledgerNavigate.toOperationRejectedStep();
ledgerAnalytics.transactionSignedOnLedgerRejected();
return;
}
ledgerNavigate.toErrorStep(chain);
},
});

try {
const versionInfo = await getStacksAppVersion(stacksApp);
ledgerAnalytics.trackDeviceVersionInfo(versionInfo);
setLatestDeviceResponse(versionInfo);
if (resp.returnCode !== LedgerError.NoErrors) {
throw new Error('Some other error');
}

if (versionInfo.deviceLocked) {
setAwaitingDeviceConnection(false);
return;
}
ledgerNavigate.toAwaitingDeviceOperation({ hasApprovedOperation: true });

if (versionInfo.returnCode !== LedgerError.NoErrors) {
logger.error('Return code from device has error', versionInfo);
return;
}
await delay(1000);

if (isVersionOfLedgerStacksAppWithContractPrincipalBug(versionInfo)) {
navigate(RouteUrls.LedgerOutdatedAppWarning);
const response = await hasUserSkippedBuggyAppWarning.wait();
const signedTx = signStacksTransactionWithSignature(unsignedTx, resp.signatureVRS);
ledgerAnalytics.transactionSignedOnLedgerSuccessfully();

if (response === 'cancelled-operation') {
ledgerNavigate.cancelLedgerAction();
try {
appEvents.publish('ledgerStacksTxSigned', {
unsignedTx,
signedTx,
});
} catch (e) {
ledgerNavigate.toBroadcastErrorStep(e instanceof Error ? e.message : 'Unknown error');
return;
}
}

ledgerNavigate.toDeviceBusyStep('Verifying public key on Ledger…');
await verifyLedgerPublicKey(stacksApp);

ledgerNavigate.toConnectionSuccessStep('stacks');
await delay(1000);
if (!unsignedTx) throw new Error('No unsigned tx');

ledgerNavigate.toAwaitingDeviceOperation({ hasApprovedOperation: false });

const resp = await signLedgerStacksTransaction(stacksApp)(
Buffer.from(unsignedTx, 'hex'),
account.index
);

// Assuming here that public keys are wrong. Alternatively, we may want
// to proactively check the key before signing
if (resp.returnCode === LedgerError.DataIsInvalid) {
ledgerNavigate.toDevicePayloadInvalid();
return;
}

if (resp.returnCode === LedgerError.TransactionRejected) {
ledgerNavigate.toOperationRejectedStep();
ledgerAnalytics.transactionSignedOnLedgerRejected();
return;
}

if (resp.returnCode !== LedgerError.NoErrors) {
throw new Error('Some other error');
}

ledgerNavigate.toAwaitingDeviceOperation({ hasApprovedOperation: true });

await delay(1000);

const signedTx = signStacksTransactionWithSignature(unsignedTx, resp.signatureVRS);
ledgerAnalytics.transactionSignedOnLedgerSuccessfully();

try {
appEvents.publish('ledgerStacksTxSigned', {
unsignedTx,
signedTx,
});
} catch (e) {
ledgerNavigate.toBroadcastErrorStep(e instanceof Error ? e.message : 'Unknown error');
return;
}
} catch (e) {
ledgerNavigate.toDeviceDisconnectStep();
} finally {
await stacksApp.transport.close();
}
};
},
});

function closeAction() {
appEvents.publish('ledgerStacksTxSigningCancelled', { unsignedTx: unsignedTx ?? '' });
Expand Down
Loading

0 comments on commit 0dc2c76

Please sign in to comment.