Skip to content

Commit

Permalink
refactor(ledger): stacks signing, closes #4420
Browse files Browse the repository at this point in the history
  • Loading branch information
kyranjamie committed Oct 25, 2023
1 parent 7afc103 commit cad4b0d
Show file tree
Hide file tree
Showing 14 changed files with 161 additions and 64 deletions.
7 changes: 0 additions & 7 deletions public/html/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,6 @@
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link href="/assets/base.css" rel="stylesheet" />
<link
rel="preload"
href="/assets/fonts/diatype/diatype-light.woff2"
as="font"
crossorigin
type="font/woff2"
/>
<link
rel="preload"
href="/assets/fonts/diatype/diatype-medium.woff2"
Expand Down
64 changes: 64 additions & 0 deletions src/app/common/publish-subscribe.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import type { StacksTransaction } from '@stacks/transactions';

type PubTypeFn<E> = <Key extends string & keyof E>(
event: Key,
// Ensures if we have an event with no payload, the second arg can be empty,
// rather than `undefined`
...message: null extends E[Key] ? [data?: never] : [data: E[Key]]
) => void;

type SubTypeFn<E> = <Key extends string & keyof E>(
event: Key,
fn: (message: E[Key]) => void
) => void;

type MessageFn<E> = <Key extends string & keyof E>(message: E[Key]) => void;

interface PubSubType<E> {
publish: PubTypeFn<E>;
subscribe: SubTypeFn<E>;
unsubscribe: SubTypeFn<E>;
}

export function PublishSubscribe<E>(): PubSubType<E> {
const handlers: { [key: string]: MessageFn<any>[] } = {};

return {
publish(event, msg?) {
handlers[event].forEach(h => h(msg));
},

subscribe(event, callback) {
const list = handlers[event] ?? [];
list.push(callback);
handlers[event] = list;
},

unsubscribe(event, callback) {
let list = handlers[event] ?? [];
list = list.filter(h => h !== callback);
handlers[event] = list;
},
};
}

// Global app events. Only add events if your feature isn't capable of
//communicating internally.
export interface GlobalAppEvents {
ledgerStacksTxSigned: {
unsignedTx: string;
signedTx: StacksTransaction;
};
ledgerStacksTxCancelled: null;
}

export const appEvents = PublishSubscribe<GlobalAppEvents>();

// function listenForNextEvent<T extends keyof GlobalAppEvents>(name: T): Promise<GlobalAppEvents[T]> {
// return new Promise(resolve => {
// appEvents.subscribe(name, msg => {
// appEvents.unsubscribe(name, resolve);
// resolve(msg);
// });
// });
// }
2 changes: 1 addition & 1 deletion src/app/components/info-label.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { ReactNode } from 'react';
import { Flex, FlexProps, Stack, styled } from 'leather-styles/jsx';

interface InfoLabelProps extends FlexProps {
children: ReactNode | undefined;
children: ReactNode;
title: string;
}
export function InfoLabel({ children, title, ...rest }: InfoLabelProps) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { logger } from '@shared/logger';
import { RouteUrls } from '@shared/route-urls';

import { useScrollLock } from '@app/common/hooks/use-scroll-lock';
import { appEvents } from '@app/common/publish-subscribe';
import { delay } from '@app/common/utils';
import { BaseDrawer } from '@app/components/drawer/base-drawer';
import {
Expand All @@ -25,7 +26,6 @@ import {
useActionCancellableByUser,
} from '@app/features/ledger/utils/stacks-ledger-utils';
import { useCurrentStacksAccount } from '@app/store/accounts/blockchain/stacks/stacks-account.hooks';
import { useTransactionBroadcast } from '@app/store/transactions/transaction.hooks';

import { useLedgerAnalytics } from '../../hooks/use-ledger-analytics.hook';
import { useLedgerNavigate } from '../../hooks/use-ledger-navigate';
Expand All @@ -39,19 +39,19 @@ export function LedgerSignStacksTxContainer() {
const ledgerAnalytics = useLedgerAnalytics();
useScrollLock(true);
const account = useCurrentStacksAccount();
const hwWalletTxBroadcast = useTransactionBroadcast();
// const hwWalletTxBroadcast = useTransactionBroadcast();
const canUserCancelAction = useActionCancellableByUser();
const verifyLedgerPublicKey = useVerifyMatchingLedgerStacksPublicKey();
const [unsignedTransaction, setUnsignedTransaction] = useState<null | string>(null);
const [unsignedTx, setUnsignedTx] = useState<null | string>(null);

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

useEffect(() => {
const tx = get(location.state, 'tx');
if (tx) setUnsignedTransaction(tx);
if (tx) setUnsignedTx(tx);
}, [location.state]);

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

const [latestDeviceResponse, setLatestDeviceResponse] = useLedgerResponseState();

Expand Down Expand Up @@ -87,6 +87,7 @@ export function LedgerSignStacksTxContainer() {
const response = await hasUserSkippedBuggyAppWarning.wait();

if (response === 'cancelled-operation') {
appEvents.publish('ledgerStacksTxCancelled');
ledgerNavigate.cancelLedgerAction();
return;
}
Expand All @@ -97,12 +98,12 @@ export function LedgerSignStacksTxContainer() {

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

ledgerNavigate.toAwaitingDeviceOperation({ hasApprovedOperation: false });

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

Expand All @@ -127,12 +128,14 @@ export function LedgerSignStacksTxContainer() {

await delay(1000);

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

try {
await hwWalletTxBroadcast({ signedTx });
navigate(RouteUrls.Home);
appEvents.publish('ledgerStacksTxSigned', {
unsignedTx,
signedTx,
});
} catch (e) {
ledgerNavigate.toBroadcastErrorStep(e instanceof Error ? e.message : 'Unknown error');
return;
Expand All @@ -147,7 +150,7 @@ export function LedgerSignStacksTxContainer() {
const allowUserToGoBack = get(location.state, 'goBack');

const ledgerContextValue: LedgerTxSigningContext = {
transaction: unsignedTransaction ? deserializeTransaction(unsignedTransaction) : null,
transaction: unsignedTx ? deserializeTransaction(unsignedTx) : null,
signTransaction,
latestDeviceResponse,
awaitingDeviceConnection,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import type { StacksTransaction } from '@stacks/transactions';

import { GlobalAppEvents, appEvents } from '@app/common/publish-subscribe';

export async function listenForStacksTxLedgerSigning(
unsignedTx: string
): Promise<StacksTransaction> {
return new Promise(resolve => {
function handler(msg: GlobalAppEvents['ledgerStacksTxSigned']) {
if (msg.unsignedTx === unsignedTx) {
appEvents.unsubscribe('ledgerStacksTxSigned', handler);
resolve(msg.signedTx);
}
}
appEvents.subscribe('ledgerStacksTxSigned', handler);
});
}
2 changes: 0 additions & 2 deletions src/app/pages/home/home.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ import { useRouteHeader } from '@app/common/hooks/use-route-header';
import { Header } from '@app/components/header';
import { ActivityList } from '@app/features/activity-list/activity-list';
import { AssetsList } from '@app/features/asset-list/asset-list';
import { InAppMessages } from '@app/features/hiro-messages/in-app-messages';
import { homePageModalRoutes } from '@app/routes/app-routes';
import { ModalBackgroundWrapper } from '@app/routes/components/modal-background-wrapper';

Expand All @@ -26,7 +25,6 @@ export function Home() {

useRouteHeader(
<>
<InAppMessages />
<Header />
</>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import { closeWindow } from '@shared/utils';

import { useDefaultRequestParams } from '@app/common/hooks/use-default-request-search-params';
import { useRejectIfLedgerWallet } from '@app/common/rpc-helpers';
import { useSignTransactionSoftwareWallet } from '@app/store/transactions/transaction.hooks';
import { useSignStacksTransaction } from '@app/store/transactions/transaction.hooks';

function useRpcSignStacksTransactionParams() {
useRejectIfLedgerWallet('stx_signTransaction');
Expand Down Expand Up @@ -38,7 +38,7 @@ function useRpcSignStacksTransactionParams() {
export function useRpcSignStacksTransaction() {
const { origin, requestId, tabId, stacksTransaction, isMultisig } =
useRpcSignStacksTransactionParams();
const signSoftwareWalletTx = useSignTransactionSoftwareWallet();
const signStacksTx = useSignStacksTransaction();
const wasSignedByOtherOwners =
isMultisig &&
(stacksTransaction.auth.spendingCondition as MultiSigSpendingCondition).fields?.length > 0;
Expand All @@ -49,11 +49,11 @@ export function useRpcSignStacksTransaction() {
disableNonceSelection: wasSignedByOtherOwners,
stacksTransaction,
isMultisig,
onSignStacksTransaction(fee: number, nonce: number) {
async onSignStacksTransaction(fee: number, nonce: number) {
stacksTransaction.setFee(fee);
stacksTransaction.setNonce(nonce);

const signedTransaction = signSoftwareWalletTx(stacksTransaction);
const signedTransaction = await signStacksTx(stacksTransaction);
if (!signedTransaction) {
throw new Error('Error signing stacks transaction');
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import { isString } from '@shared/utils';

import { LoadingKeys } from '@app/common/hooks/use-loading';
import { useSubmitTransactionCallback } from '@app/common/hooks/use-submit-stx-transaction';
import { useSignTransactionSoftwareWallet } from '@app/store/transactions/transaction.hooks';
import { useSignStacksTransaction } from '@app/store/transactions/transaction.hooks';

import { useStacksTransactionSummary } from './use-stacks-transaction-summary';

Expand All @@ -20,7 +20,7 @@ export function useStacksBroadcastTransaction(
token: CryptoCurrencies,
decimals?: number
) {
const signSoftwareWalletTx = useSignTransactionSoftwareWallet();
const signSoftwareWalletTx = useSignStacksTransaction();
const [isBroadcasting, setIsBroadcasting] = useState(false);
const { formSentSummaryTxState } = useStacksTransactionSummary(token);
const navigate = useNavigate();
Expand Down Expand Up @@ -69,7 +69,7 @@ export function useStacksBroadcastTransaction(

async function broadcastTransaction(unsignedTx: StacksTransaction) {
if (!unsignedTx) return;
const signedTx = signSoftwareWalletTx(unsignedTx);
const signedTx = await signSoftwareWalletTx(unsignedTx);
if (!signedTx) return;
await broadcastTransactionAction(signedTx);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ export function Sip10TokenSendForm() {
interface Sip10TokenSendFormLoaderProps {
children: (data: { symbol: string; contractId: string }) => React.JSX.Element;
}

function Sip10TokenSendFormLoader({ children }: Sip10TokenSendFormLoaderProps) {
const { symbol, contractId } = useParams();
if (!symbol || !contractId) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,11 @@ import { StacksSendFormValues } from '@shared/models/form.model';

import { useStxBalance } from '@app/common/hooks/balance/stx/use-stx-balance';
import { convertAmountToBaseUnit } from '@app/common/money/calculate-money';
import { useWalletType } from '@app/common/use-wallet-type';
import {
stxAmountValidator,
stxAvailableBalanceValidator,
} from '@app/common/validation/forms/amount-validators';
import { stxFeeValidator } from '@app/common/validation/forms/fee-validators';
import { useLedgerNavigate } from '@app/features/ledger/hooks/use-ledger-navigate';
import { useUpdatePersistedSendFormValues } from '@app/features/popup-send-form-restoration/use-update-persisted-send-form-values';
import { useCalculateStacksTxFees } from '@app/query/stacks/fees/fees.hooks';
import { useStacksValidateFeeByNonce } from '@app/query/stacks/mempool/mempool.hooks';
Expand All @@ -34,8 +32,6 @@ export function useStxSendForm() {
const generateTx = useGenerateStxTokenTransferUnsignedTx();
const { onFormStateChange } = useUpdatePersistedSendFormValues();

const { whenWallet } = useWalletType();
const ledgerNavigate = useLedgerNavigate();
const sendFormNavigate = useSendFormNavigate();
const { changeFeeByNonce } = useStacksValidateFeeByNonce();

Expand Down Expand Up @@ -88,10 +84,7 @@ export function useStxSendForm() {
const tx = await generateTx(values);
if (!tx) return logger.error('Attempted to generate unsigned tx, but tx is undefined');

whenWallet({
software: () => sendFormNavigate.toConfirmAndSignStxTransaction(tx, showFeeChangeWarning),
ledger: () => ledgerNavigate.toConnectAndSignTransactionStep(tx),
})();
sendFormNavigate.toConfirmAndSignStxTransaction(tx, showFeeChangeWarning);
},
};
}
11 changes: 7 additions & 4 deletions src/app/store/transactions/fees.hooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,11 @@ import { LoadingKeys } from '@app/common/hooks/use-loading';
import { useSubmitTransactionCallback } from '@app/common/hooks/use-submit-stx-transaction';
import { useRawTxIdState } from '@app/store/transactions/raw.hooks';

import { useSignTransactionSoftwareWallet } from './transaction.hooks';
import { useSignStacksTransaction } from './transaction.hooks';

export const useReplaceByFeeSoftwareWalletSubmitCallBack = () => {
const [, setTxId] = useRawTxIdState();
const signTx = useSignTransactionSoftwareWallet();
const signTx = useSignStacksTransaction();
const navigate = useNavigate();

const submitTransaction = useSubmitTransactionCallback({
Expand All @@ -24,8 +24,11 @@ export const useReplaceByFeeSoftwareWalletSubmitCallBack = () => {
return useCallback(
async (rawTx: StacksTransaction) => {
if (!rawTx) return;
const signedTx = signTx(rawTx);
if (!signedTx) return;
const signedTx = await signTx(rawTx);
if (!signedTx) {
logger.warn('Error signing transaction when replacing by fee');
return;
}
await submitTransaction({
onSuccess() {
setTxId(null);
Expand Down
Loading

0 comments on commit cad4b0d

Please sign in to comment.