Skip to content

fix(coordinator): track pending transactions without change outputs u…#471

Open
RIYAKUMARI001 wants to merge 4 commits intocaravan-bitcoin:mainfrom
RIYAKUMARI001:fix-pending-tx-tracking
Open

fix(coordinator): track pending transactions without change outputs u…#471
RIYAKUMARI001 wants to merge 4 commits intocaravan-bitcoin:mainfrom
RIYAKUMARI001:fix-pending-tx-tracking

Conversation

@RIYAKUMARI001
Copy link
Copy Markdown
Contributor

@RIYAKUMARI001 RIYAKUMARI001 commented Feb 8, 2026

What kind of change does this PR introduce?

Bugfix

Issue Number:

Fixes #407

Snapshots:

Screenshot 2026-02-08 222413 Screenshot 2026-02-08 222358 Screenshot 2026-02-08 222342 Screenshot 2026-02-08 224604 Screenshot 2026-02-08 222328

Summary
This PR resolves a critical bug where transactions without change outputs (e.g., "MAX" sends, consolidations, or 1:1 transactions) would disappear from the Caravan "Pending" transactions view immediately after broadcast.

The Fix:

Previously, the pending transaction logic relied primarily on detecting new "unconfirmed" outputs in the wallet's change/deposit nodes. For transactions that spent the entire balance to a single external or internal address without a change output, this detection would fail.
This PR refactors the transaction hooks to use historical address scanning for pending transactions.
Updated
selectProcessedTransactions
to support a explicit "pending" filter.
Implemented usePublicClientPendingTransactions and usePrivateClientPendingTransactions that fetch the mempool history of all relevant wallet addresses.
This ensures that any transaction involving the wallet's addresses is tracked in the Pending tab, regardless of whether a change output exists.
Motivation: Users performing consolidation or spending their full balance were left with no visual feedback in Caravan that their transaction was successfully broadcast and pending. This fix provides a reliable and consistent UI state.

Does this PR introduce a breaking change?

No.

Checklist
I have tested my changes thoroughly.
I have added or updated tests to cover my changes (if applicable).
I have verified that test coverage meets or exceeds 95% (if applicable).
I have run the test suite locally, and although some legacy environment issues exist, my changes do not introduce new regressions.
I have written tests for all new changes/features
I have followed the project's coding style and conventions (formatted with Prettier).
I have created a changeset to document my changes (npm run changeset)
Other information Successfully verified on Bitcoin Testnet using a 2-of-2 multisig wallet integrated with Sparrow. A "MAX" send (consolidation) was performed, and the transaction was correctly tracked in Caravan's "Pending" tab until it reached its first confirmation.

Have you read the contributing guide?
Yes

For information on creating and using changesets, please refer to our documentation on changesets.

@changeset-bot
Copy link
Copy Markdown

changeset-bot Bot commented Feb 8, 2026

🦋 Changeset detected

Latest commit: 84b9b74

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
caravan-coordinator Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@vercel
Copy link
Copy Markdown

vercel Bot commented Feb 8, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
caravan-coordinator Ready Ready Preview, Comment Apr 3, 2026 11:34am

Request Review

@Legend101Zz
Copy link
Copy Markdown
Contributor

Screenshot 2026-03-04 at 20 10 53

@RIYAKUMARI001 Sorry for the delay for getting back on this PR , the initial testing looks really good I was able to on a private node send a 1 to 1 tx and I was able to see that in my pending tx tab :)

[...transactionKeys.all, txid, "withHex"] as const,
coins: (txid: string) => [...transactionKeys.all, txid, "coins"] as const,
confirmedHistory: () => [...transactionKeys.all, "confirmed"] as const,
confirmedHistory: () =>
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

don' change this

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Restored it!

Copy link
Copy Markdown
Contributor

@Legend101Zz Legend101Zz left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi @RIYAKUMARI001, thanks for the PR! I had a few comments while going through the changes.

Instead of introducing new hooks in transactions.ts (which would duplicate the public/private client logic we already have in txHistory.ts), I think we could extend the existing hooks — usePublicClientTransactions and usePrivateClientTransactions — by adding a filter parameter. We could default this to "confirmed" to preserve the current behavior.

With that in place, usePendingTransactions could simply call these hooks with "pending", which avoids creating a separate hook tree. Since selectProcessedTransactions already supports filtering, the integration should be fairly straightforward.

One thing that might be worth discussing with @bucko13 , though: if we reuse the same hooks for both confirmed history and pending transactions, they may end up having different polling requirements. Pending transactions likely need a faster refetchInterval than confirmed ones. So we might want to consider whether that should be configurable via options passed to the hooks, or handled in another way.

Not a blocker, but probably good to align on the approach before we move forward.

all: ["transactions"] as const,
tx: (txid: string) => [...transactionKeys.all, txid] as const,
pending: () => [...transactionKeys.all, "pending"] as const,
pendingHistory: () => [...transactionKeys.all, "pending", "history"] as const,
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

so you can remove this too

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done. Removed the unnecessary pendingHistory


// Hook for fetching pending transaction IDs and their details
// Service function for fetching pending transaction fees
const fetchPendingTransactionFee = async (
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes these can be removed as you did


// Basic hook for raw pending transactions (no processing)
export const useRawPendingTransactions = () => {
export const usePublicClientPendingTransactions = () => {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

no need to add new hook

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Understood. Removed the redundant hook and switched to using the existing ones with a filter.

error,
refetch: () => {
transactionQueries.forEach((query) => query.refetch());
export const usePrivateClientPendingTransactions = () => {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same no need to add new hook

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed this as well. Now utilizing the consolidated base hooks

const walletAddresses = useSelector(getWalletAddresses);
const rawPendingQuery = useRawPendingTransactions();
const clientType = useSelector((state: WalletState) => state.client.type);
const privateQuery = usePrivateClientPendingTransactions();
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

see instead in txHistory.ts we already have these hooks so add a filter parameter to the existing hooks in txHistory.ts:

like eg :

export const usePublicClientTransactions = (
  filter: "confirmed" | "pending" | "all" = "confirmed"
) => {

and then here in transcation.ts file

export const usePendingTransactions = () => {
  const clientType = useSelector((state: WalletState) => state.client.type);
  const privateQuery = usePrivateClientTransactions("pending");
  const publicQuery = usePublicClientTransactions("pending");
  const query = clientType === "private" ? privateQuery : publicQuery;

  return {
    transactions: query.data || [],
    isLoading: query.isLoading,
    error: query.error,
    refetch: query.refetch,
  };
};

and delete the rest unnecessary and duplicate code from transactions.ts , let's follow the DRY ( do not repeat ) principle here :)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done! I've moved the logic to txHistory.ts and refactored it to use the existing client hooks with a filter parameter as suggested. The duplicate code has been removed

transactions: WalletTransactionDetails[],
walletAddresses: string[],
filter: "all" | "confirmed" | "unconfirmed" = "all",
filter: "all" | "confirmed" | "unconfirmed" | "pending" = "all",
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why add a new status , we can add use "unconfirmed" itself , also in above too

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've reverted to using the existing 'unconfirmed '

import { useGetClient } from "hooks/client";
import { bitcoinsToSatoshis } from "@caravan/bitcoin";

const MAX_TRANSACTIONS_TO_FETCH = 500;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can be removed now

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done!

@Legend101Zz
Copy link
Copy Markdown
Contributor

Hi @RIYAKUMARI001 gentle ping to check if you had the chance to work on the reviews :)

@RIYAKUMARI001
Copy link
Copy Markdown
Contributor Author

Hi @RIYAKUMARI001 gentle ping to check if you had the chance to work on the reviews :)

Hi! Yes, I saw the requested changes. I’m planning to start working on them now. My exams were going on, and tomorrow is my last exam. After that, I’ll push the changes as soon as possible.

@RIYAKUMARI001
Copy link
Copy Markdown
Contributor Author

RIYAKUMARI001 commented Apr 3, 2026

Screenshot 2026-04-02 163923 Screenshot 2026-04-02 160859 Hi @Legend101Zz , thank you for your patience. I’ve gone through all your comments and added a changeset. Please take a look when you can. I'm ready for another review. Thanks.

@RIYAKUMARI001
Copy link
Copy Markdown
Contributor Author

Hi @RIYAKUMARI001, thanks for the PR! I had a few comments while going through the changes.

Instead of introducing new hooks in transactions.ts (which would duplicate the public/private client logic we already have in txHistory.ts), I think we could extend the existing hooks — usePublicClientTransactions and usePrivateClientTransactions — by adding a filter parameter. We could default this to "confirmed" to preserve the current behavior.

With that in place, usePendingTransactions could simply call these hooks with "pending", which avoids creating a separate hook tree. Since selectProcessedTransactions already supports filtering, the integration should be fairly straightforward.

One thing that might be worth discussing with @bucko13 , though: if we reuse the same hooks for both confirmed history and pending transactions, they may end up having different polling requirements. Pending transactions likely need a faster refetchInterval than confirmed ones. So we might want to consider whether that should be configurable via options passed to the hooks, or handled in another way.

Not a blocker, but probably good to align on the approach before we move forward.

Thank you for the detailed feedback. I changed the hooks in txHistory.ts to use a filter parameter and followed the DRY principle as you suggested. I also made the refetchInterval faster, now set at 30 seconds, for unconfirmed transactions to keep the UI updated.

Copy link
Copy Markdown
Contributor

@Legend101Zz Legend101Zz left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the changes they look solid to me now :)

Image

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Broken PendingTx Logic: getPendingTransactionIds fails to track transactions without change outputs (1:1, 2:2, etc.)

2 participants