From c8e789ef84aa7e17c2b576e549393392076ac62e Mon Sep 17 00:00:00 2001 From: Randall Naar Date: Mon, 14 Sep 2026 11:37:17 -0400 Subject: [PATCH] Added confirmation polling to transaction page. --- README.md | 8 + client/src/app.js | 18 +- client/src/components/status-badge.js | 4 +- client/src/views/tx.js | 19 +- test/app.test.js | 279 ++++++++++++++++++++++++++ 5 files changed, 318 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index 8ad9790f..b37def33 100644 --- a/README.md +++ b/README.md @@ -80,6 +80,14 @@ transactions also refresh on new blocks rather than on a timer. A newly observed tip resets pending block template polling and delays the next request by 15 seconds so the electrs cache can refresh. +While an unconfirmed transaction page is focused, mempool summary and fee +estimates refresh on the standard cadence to keep its ETA, mempool depth, and +fee analysis current. The transaction confirmation status does not use another +fixed-rate poll: each newly observed tip requests `/tx/:txid/status`. Once the +transaction is confirmed, those unconfirmed-transaction refreshes stop and the +containing block metadata is loaded for the block details shown below the +transaction. + The dashboard requests both `/mempool/recent` and `/mempool` because they serve different UI contracts. `/mempool/recent` supplies the recent transaction list; `/mempool` supplies aggregate backlog fields such as transaction count, virtual diff --git a/client/src/app.js b/client/src/app.js index a5ff5f08..647c5f9c 100644 --- a/client/src/app.js +++ b/client/src/app.js @@ -395,7 +395,16 @@ export default function main( ).startWith(null).scan((S, mod) => mod(S)) // Single TX - , tx$ = reply('tx').merge(goTx$.mapTo(null)).startWith(null) + , tx$ = O.merge( + reply('tx').map(tx => _ => tx), + reply('tx-status', true).map(r => tx => + tx && tx.txid == r.request.txid && !tx.status.confirmed && r.body.confirmed + ? { ...tx, status: r.body } + : tx), + page$.mapTo(_ => null) + ).startWith(_ => null) + .scan((tx, update) => update(tx), null) + .distinctUntilChanged() , txBlock$ = reply('tx-block').merge(goTx$.mapTo(null)).startWith(null) // Predecessor metadata for confirmed block interval calculations @@ -633,6 +642,13 @@ export default function main( // fetch single tx (including confirmation status) , goTx$.map(txid => ({ category: 'tx', method: 'GET', path: `/tx/${txid}` })) + // Refresh unconfirmed transactions on observed tip-height changes, + // reusing the existing tip poll. + , !pollingEnabled ? O.empty() : subsequentTipHeight$ + .withLatestFrom(view$, tx$, (_, view, tx) => ({ view, tx })) + .filter(({ view, tx }) => view == 'tx' && tx && tx.status && !tx.status.confirmed && hasFocus()) + .map(({ tx }) => ({ category: 'tx-status', method: 'GET', path: `/tx/${tx.txid}/status`, txid: tx.txid, bg: true })) + // fetch the block containing a confirmed tx , tx$.filter(tx => tx && tx.status && tx.status.confirmed && tx.status.block_hash) .map(tx => ({ category: 'tx-block', method: 'GET', path: `/block/${tx.status.block_hash}` })) diff --git a/client/src/components/status-badge.js b/client/src/components/status-badge.js index f9918a3d..dd37509d 100644 --- a/client/src/components/status-badge.js +++ b/client/src/components/status-badge.js @@ -11,8 +11,8 @@ export const StatusBadge = ( ); -export const StatusDot = () => ( -