You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
When a vault expires without its Pre-PegIn Bitcoin transaction ever reaching the network, the depositor's BTC was never locked — there is genuinely nothing to recover. #1701 fixed the action: opening the refund modal for such a vault now shows "nothing to refund" instead of walking the user through a doomed signing flow.
But the vault still occupies a permanent slot in the dashboard's Expired Deposits section, with a "Refund" button, indefinitely. There is no retention limit or cleanup.
Why it persists
services/vault/src/services/vault/fetchVaults.ts queries vaults(where: { depositor: $depositor }) — no status filter, no time/recency cutoff, no pagination. Every vault the depositor ever registered is returned on every load.
services/vault/src/hooks/usePendingDeposits.ts builds expiredActivities from every activity with contractStatus === EXPIRED && !!unsignedPrePeginTx. For rows that reach the dashboard, unsignedPrePeginTx is effectively always present — the fetch path validates it as required and drops malformed/null rows — so expiredActivities includes every expired vault.
services/vault/src/components/simple/ExpiredDepositSection.tsx renders every expired activity; the list is visually scroll-capped (max-h-[400px]) but has no count limit and no age-based filtering.
There is no client-side cleanup of indexer-sourced expired vaults — shouldRemoveFromLocalStorage only prunes localStorage pending entries, not confirmed/expired indexer rows. Once a vault is EXPIRED, the dashboard never ages it out.
Net effect: a depositor who registers a vault on Ethereum, never broadcasts the Pre-PegIn, and lets it lapse accumulates a permanent dead entry in "Expired Deposits" — accurate but useless, since nothing was ever locked on Bitcoin. On the list itself it is indistinguishable from a genuinely-refundable expired vault; the "nothing to refund" distinction only surfaces inside the modal.
The "is the Pre-PegIn actually on Bitcoin" signal is only resolved when the refund modal opens (a per-vault mempool probe). Hiding such a vault from the list up front would need that result available at list-render time — i.e. persisting it, or a per-row probe (the polling anti-pattern the codebase deliberately avoids).
Affects single-vault and batched deposits equally.
Scope
Decide how a never-broadcast expired vault should age out of the dashboard — for example a way to dismiss/acknowledge it, an age-based cutoff for expired entries, or accepting it as a permanent ledger entry. Where the on-chain "is it on Bitcoin" signal is available (modal-open only) constrains the cleaner options and is the main thing to design around.
Problem
When a vault expires without its Pre-PegIn Bitcoin transaction ever reaching the network, the depositor's BTC was never locked — there is genuinely nothing to recover. #1701 fixed the action: opening the refund modal for such a vault now shows "nothing to refund" instead of walking the user through a doomed signing flow.
But the vault still occupies a permanent slot in the dashboard's Expired Deposits section, with a "Refund" button, indefinitely. There is no retention limit or cleanup.
Why it persists
services/vault/src/services/vault/fetchVaults.tsqueriesvaults(where: { depositor: $depositor })— no status filter, no time/recency cutoff, no pagination. Every vault the depositor ever registered is returned on every load.services/vault/src/hooks/usePendingDeposits.tsbuildsexpiredActivitiesfrom every activity withcontractStatus === EXPIRED && !!unsignedPrePeginTx. For rows that reach the dashboard,unsignedPrePeginTxis effectively always present — the fetch path validates it as required and drops malformed/null rows — soexpiredActivitiesincludes every expired vault.services/vault/src/components/simple/ExpiredDepositSection.tsxrenders every expired activity; the list is visually scroll-capped (max-h-[400px]) but has no count limit and no age-based filtering.shouldRemoveFromLocalStorageonly prunes localStorage pending entries, not confirmed/expired indexer rows. Once a vault is EXPIRED, the dashboard never ages it out.Net effect: a depositor who registers a vault on Ethereum, never broadcasts the Pre-PegIn, and lets it lapse accumulates a permanent dead entry in "Expired Deposits" — accurate but useless, since nothing was ever locked on Bitcoin. On the list itself it is indistinguishable from a genuinely-refundable expired vault; the "nothing to refund" distinction only surfaces inside the modal.
Notes
Scope
Decide how a never-broadcast expired vault should age out of the dashboard — for example a way to dismiss/acknowledge it, an age-based cutoff for expired entries, or accepting it as a permanent ledger entry. Where the on-chain "is it on Bitcoin" signal is available (modal-open only) constrains the cleaner options and is the main thing to design around.