From 98ae00224406fbc22e24bdf648357d478b83e339 Mon Sep 17 00:00:00 2001 From: junderw Date: Thu, 26 Mar 2026 20:39:36 +0900 Subject: [PATCH] fix: continue address tx pagination across mempool boundary The address tx history endpoints could stop pagination at the mempool/chain boundary when after_txid pointed to the last mempool transaction for an address or address group. The confirmed-history query was reusing after_txid whenever the mempool query returned no rows, even if that cursor only existed in mempool. This change makes the confirmed-history query reuse after_txid only when the cursor was actually found in chain history; mempool cursors now correctly fall through to the newest confirmed transactions. Co-authored-by: Saravanan Mani <228955468+saravanan7mani7@users.noreply.github.com> --- src/rest.rs | 64 +++++++++++++++++++++++++++++++++++++++++------------ 1 file changed, 50 insertions(+), 14 deletions(-) diff --git a/src/rest.rs b/src/rest.rs index 48bdb908..27006a6c 100644 --- a/src/rest.rs +++ b/src/rest.rs @@ -583,6 +583,19 @@ fn find_txid( } } +#[inline] +fn confirmed_after_txid<'a>( + after_txid_location: &TxidLocation, + after_txid: Option<&'a Txid>, +) -> Option<&'a Txid> { + match after_txid_location { + // A mempool cursor never exists in chain history, so always + // start from the newest confirmed tx when crossing the boundary. + TxidLocation::Mempool | TxidLocation::None => None, + TxidLocation::Chain(_) => after_txid, + } +} + /// Prepare transactions to be serialized in a JSON response /// /// Any transactions with missing prevouts will be filtered out of the response, rather than returned with incorrect data. @@ -960,13 +973,8 @@ fn handle_request( }; if txs.len() < max_txs { - let after_txid_ref = if !txs.is_empty() { - // If there are any txs, we know mempool found the - // after_txid IF it exists... so always return None. - None - } else { - after_txid.as_ref() - }; + let after_txid_ref = + confirmed_after_txid(&after_txid_location, after_txid.as_ref()); let mut confirmed_txs = query .chain() .history( @@ -1067,13 +1075,8 @@ fn handle_request( }; if txs.len() < max_txs { - let after_txid_ref = if !txs.is_empty() { - // If there are any txs, we know mempool found the - // after_txid IF it exists... so always return None. - None - } else { - after_txid.as_ref() - }; + let after_txid_ref = + confirmed_after_txid(&after_txid_location, after_txid.as_ref()); let mut confirmed_txs = query .chain() .history_group( @@ -2148,6 +2151,8 @@ impl From for HttpError { #[cfg(test)] mod tests { + use super::{confirmed_after_txid, TxidLocation}; + use crate::chain::Txid; use crate::rest::HttpError; use serde_json::Value; use std::collections::HashMap; @@ -2214,6 +2219,37 @@ mod tests { assert!(err.is_err()); } + #[test] + fn test_confirmed_after_txid_uses_chain_cursor_only() { + let txid: Txid = "0000000000000000000000000000000000000000000000000000000000000001" + .parse() + .unwrap(); + + assert_eq!( + confirmed_after_txid(&TxidLocation::Mempool, Some(&txid)), + None + ); + assert_eq!(confirmed_after_txid(&TxidLocation::None, Some(&txid)), None); + assert_eq!( + confirmed_after_txid(&TxidLocation::Chain(123), Some(&txid)), + Some(&txid) + ); + } + + #[test] + fn test_confirmed_after_txid_allows_mempool_chain_boundary_progress() { + let txid: Txid = "0000000000000000000000000000000000000000000000000000000000000002" + .parse() + .unwrap(); + + // If a mempool cursor returns no newer mempool txs, confirmed history + // must start from the newest confirmed tx instead of seeking this txid. + assert_eq!( + confirmed_after_txid(&TxidLocation::Mempool, Some(&txid)), + None + ); + } + #[test] fn test_difficulty_new() { use super::difficulty_new;