diff --git a/public/html/index.html b/public/html/index.html
index 87b1295a388..b7acb05e531 100644
--- a/public/html/index.html
+++ b/public/html/index.html
@@ -9,13 +9,6 @@
-
= (
+ 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 = (
+ event: Key,
+ fn: (message: E[Key]) => void
+) => void;
+
+type MessageFn = (message: E[Key]) => void;
+
+interface PubSubType {
+ publish: PubTypeFn;
+ subscribe: SubTypeFn;
+ unsubscribe: SubTypeFn;
+}
+
+export function PublishSubscribe(): PubSubType {
+ const handlers: { [key: string]: MessageFn[] } = {};
+
+ 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();
+
+// function listenForNextEvent(name: T): Promise {
+// return new Promise(resolve => {
+// appEvents.subscribe(name, msg => {
+// appEvents.unsubscribe(name, resolve);
+// resolve(msg);
+// });
+// });
+// }
diff --git a/src/app/components/info-label.tsx b/src/app/components/info-label.tsx
index e757df1b164..ff0fec092d3 100644
--- a/src/app/components/info-label.tsx
+++ b/src/app/components/info-label.tsx
@@ -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) {
diff --git a/src/app/features/ledger/flows/stacks-tx-signing/ledger-sign-tx-container.tsx b/src/app/features/ledger/flows/stacks-tx-signing/ledger-sign-tx-container.tsx
index 5a276872568..ba06a4727d9 100644
--- a/src/app/features/ledger/flows/stacks-tx-signing/ledger-sign-tx-container.tsx
+++ b/src/app/features/ledger/flows/stacks-tx-signing/ledger-sign-tx-container.tsx
@@ -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 {
@@ -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';
@@ -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);
+ const [unsignedTx, setUnsignedTx] = useState(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();
@@ -87,6 +87,7 @@ export function LedgerSignStacksTxContainer() {
const response = await hasUserSkippedBuggyAppWarning.wait();
if (response === 'cancelled-operation') {
+ appEvents.publish('ledgerStacksTxCancelled');
ledgerNavigate.cancelLedgerAction();
return;
}
@@ -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
);
@@ -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;
@@ -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,
diff --git a/src/app/features/ledger/flows/stacks-tx-signing/stacks-tx-signing-event-listeners.ts b/src/app/features/ledger/flows/stacks-tx-signing/stacks-tx-signing-event-listeners.ts
new file mode 100644
index 00000000000..dcb27c3b1f6
--- /dev/null
+++ b/src/app/features/ledger/flows/stacks-tx-signing/stacks-tx-signing-event-listeners.ts
@@ -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 {
+ return new Promise(resolve => {
+ function handler(msg: GlobalAppEvents['ledgerStacksTxSigned']) {
+ if (msg.unsignedTx === unsignedTx) {
+ appEvents.unsubscribe('ledgerStacksTxSigned', handler);
+ resolve(msg.signedTx);
+ }
+ }
+ appEvents.subscribe('ledgerStacksTxSigned', handler);
+ });
+}
diff --git a/src/app/pages/home/home.tsx b/src/app/pages/home/home.tsx
index d910d52ea1d..6517de3204e 100644
--- a/src/app/pages/home/home.tsx
+++ b/src/app/pages/home/home.tsx
@@ -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';
@@ -26,7 +25,6 @@ export function Home() {
useRouteHeader(
<>
-
>
);
diff --git a/src/app/pages/rpc-sign-stacks-transaction/use-rpc-sign-stacks-transaction.ts b/src/app/pages/rpc-sign-stacks-transaction/use-rpc-sign-stacks-transaction.ts
index fc2cdf18a03..315fbe27478 100644
--- a/src/app/pages/rpc-sign-stacks-transaction/use-rpc-sign-stacks-transaction.ts
+++ b/src/app/pages/rpc-sign-stacks-transaction/use-rpc-sign-stacks-transaction.ts
@@ -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');
@@ -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;
@@ -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');
}
diff --git a/src/app/pages/send/send-crypto-asset-form/family/stacks/hooks/use-stacks-broadcast-transaction.tsx b/src/app/pages/send/send-crypto-asset-form/family/stacks/hooks/use-stacks-broadcast-transaction.tsx
index 6f735bfb0b4..cb2718c68b8 100644
--- a/src/app/pages/send/send-crypto-asset-form/family/stacks/hooks/use-stacks-broadcast-transaction.tsx
+++ b/src/app/pages/send/send-crypto-asset-form/family/stacks/hooks/use-stacks-broadcast-transaction.tsx
@@ -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';
@@ -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();
@@ -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);
}
diff --git a/src/app/pages/send/send-crypto-asset-form/form/stacks-sip10/sip10-token-send-form.tsx b/src/app/pages/send/send-crypto-asset-form/form/stacks-sip10/sip10-token-send-form.tsx
index 65fc550a811..1ce202aac31 100644
--- a/src/app/pages/send/send-crypto-asset-form/form/stacks-sip10/sip10-token-send-form.tsx
+++ b/src/app/pages/send/send-crypto-asset-form/form/stacks-sip10/sip10-token-send-form.tsx
@@ -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) {
diff --git a/src/app/pages/send/send-crypto-asset-form/form/stx/use-stx-send-form.tsx b/src/app/pages/send/send-crypto-asset-form/form/stx/use-stx-send-form.tsx
index 7b2bf317285..ae39961fc94 100644
--- a/src/app/pages/send/send-crypto-asset-form/form/stx/use-stx-send-form.tsx
+++ b/src/app/pages/send/send-crypto-asset-form/form/stx/use-stx-send-form.tsx
@@ -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';
@@ -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();
@@ -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);
},
};
}
diff --git a/src/app/store/transactions/fees.hooks.ts b/src/app/store/transactions/fees.hooks.ts
index 54282002578..23359844bca 100644
--- a/src/app/store/transactions/fees.hooks.ts
+++ b/src/app/store/transactions/fees.hooks.ts
@@ -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({
@@ -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);
diff --git a/src/app/store/transactions/transaction.hooks.ts b/src/app/store/transactions/transaction.hooks.ts
index ddf87100197..cdc2ab9ac38 100644
--- a/src/app/store/transactions/transaction.hooks.ts
+++ b/src/app/store/transactions/transaction.hooks.ts
@@ -2,6 +2,7 @@ import { useCallback, useMemo } from 'react';
import { useAsync } from 'react-async-hook';
import toast from 'react-hot-toast';
+import { bytesToHex } from '@noble/hashes/utils';
import { TransactionTypes } from '@stacks/connect';
import {
FungibleConditionCode,
@@ -27,6 +28,9 @@ import {
GenerateUnsignedTransactionOptions,
generateUnsignedTransaction,
} from '@app/common/transactions/stacks/generate-unsigned-txs';
+import { useWalletType } from '@app/common/use-wallet-type';
+import { listenForStacksTxLedgerSigning } from '@app/features/ledger/flows/stacks-tx-signing/stacks-tx-signing-event-listeners';
+import { useLedgerNavigate } from '@app/features/ledger/hooks/use-ledger-navigate';
import { useNextNonce } from '@app/query/stacks/nonce/account-nonces.hooks';
import {
useCurrentAccountStxAddressState,
@@ -91,26 +95,7 @@ export function useUnsignedPrepareTransactionDetails(values: StacksTransactionFo
return useMemo(() => unsignedStacksTransaction, [unsignedStacksTransaction]);
}
-export function useSignTransactionSoftwareWallet() {
- const account = useCurrentStacksAccount();
- return useCallback(
- (tx: StacksTransaction) => {
- if (account?.type !== 'software') {
- [toast.error, logger.error].forEach(fn =>
- fn('Cannot use this method to sign a non-software wallet transaction')
- );
- return;
- }
- const signer = new TransactionSigner(tx);
- if (!account) return null;
- signer.signOrigin(createStacksPrivateKey(account.stxPrivateKey));
- return tx;
- },
- [account]
- );
-}
-
-export function useTransactionBroadcast() {
+export function useStacksTransactionBroadcast() {
const submittedTransactionsActions = useSubmittedTransactionsActions();
const { tabId } = useDefaultRequestParams();
const requestToken = useTransactionRequest();
@@ -155,7 +140,7 @@ export function useSoftwareWalletTransactionRequestBroadcast() {
const { tabId } = useDefaultRequestParams();
const requestToken = useTransactionRequest();
const account = useCurrentStacksAccount();
- const txBroadcast = useTransactionBroadcast();
+ const txBroadcast = useStacksTransactionBroadcast();
return async (values: StacksTransactionFormValues) => {
if (!stacksTxBaseState) return;
@@ -227,3 +212,39 @@ function useUnsignedStacksTransaction(values: StacksTransactionFormValues) {
return tx.result;
}
+
+export function useSignTransactionSoftwareWallet() {
+ const account = useCurrentStacksAccount();
+ return useCallback(
+ (tx: StacksTransaction) => {
+ if (account?.type !== 'software') {
+ [toast.error, logger.error].forEach(fn =>
+ fn('Cannot use this method to sign a non-software wallet transaction')
+ );
+ return;
+ }
+ const signer = new TransactionSigner(tx);
+ if (!account) return null;
+ signer.signOrigin(createStacksPrivateKey(account.stxPrivateKey));
+ return tx;
+ },
+ [account]
+ );
+}
+
+export function useSignStacksTransaction() {
+ const { whenWallet } = useWalletType();
+ const ledgerNavigate = useLedgerNavigate();
+ const signSoftwareTx = useSignTransactionSoftwareWallet();
+
+ return (tx: StacksTransaction) =>
+ whenWallet({
+ async ledger(tx: StacksTransaction) {
+ ledgerNavigate.toConnectAndSignTransactionStep(tx);
+ return listenForStacksTxLedgerSigning(bytesToHex(tx.serialize()));
+ },
+ async software(tx: StacksTransaction) {
+ return signSoftwareTx(tx);
+ },
+ })(tx);
+}
diff --git a/src/shared/utils/type-utils.ts b/src/shared/utils/type-utils.ts
index d426dc3a336..d3e203f9fe5 100644
--- a/src/shared/utils/type-utils.ts
+++ b/src/shared/utils/type-utils.ts
@@ -5,3 +5,9 @@ type Primitive = null | undefined | string | number | boolean | symbol | bigint;
export type LiteralUnion =
| LiteralType
| (BaseType & Record);
+
+export type UnionToIntersection = (U extends any ? (k: U) => void : never) extends (
+ k: infer I
+) => void
+ ? I
+ : never;
diff --git a/theme/semantic-tokens.ts b/theme/semantic-tokens.ts
index 2636852533e..ff24e119668 100644
--- a/theme/semantic-tokens.ts
+++ b/theme/semantic-tokens.ts
@@ -70,10 +70,10 @@ export const semanticTokens = defineSemanticTokens({
value: { base: '{colors.lightModeBrown.1}', _dark: '{colors.darkModeBrown.1}' },
},
disabled: {
- value: { base: '{colors.blue.100}', _dark: '{colors.blue.100}' },
+ value: { base: '{colors.blue.100}', _dark: '{colors.blue.300}' },
},
warning: {
- value: { base: '{colors.yellow.100}', _dark: '{colors.yellow.100}' },
+ value: { base: '{colors.yellow.100}', _dark: '{colors.yellow.300}' },
},
'notification-text': {
value: { base: '{colors.lightModeBrown.12}', _dark: '{colors.darkModeBrown.12}' },