Skip to content

Commit 0135e57

Browse files
committed
Add paginated payment listing API
Replace the full payment listing API with paginated listing so callers can migrate away from fetching every payment at once. Deprecate the filtering helper because it still implies scanning the full in-memory store.
1 parent f2e44fd commit 0135e57

8 files changed

Lines changed: 259 additions & 74 deletions

File tree

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@
1616
- `EsploraSyncConfig` and `ElectrumSyncConfig` now support `force_wallet_full_scan`. When set,
1717
the on-chain wallet keeps using BDK `full_scan` instead of incremental sync until a full scan
1818
succeeds, allowing restored wallets to rediscover funds sent to previously-unknown addresses.
19+
- `Node::list_payments` now retrieves payments page-by-page, ordered from most recently created to
20+
least recently created, instead of returning all payments at once.
1921

2022
## Bug Fixes and Improvements
2123
- Building a fresh node against a Bitcoin Core RPC or REST chain source that fails to return the

bindings/ldk_node.udl

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -136,7 +136,8 @@ interface Node {
136136
[Throws=NodeError]
137137
void remove_payment([ByRef]PaymentId payment_id);
138138
BalanceDetails list_balances();
139-
sequence<PaymentDetails> list_payments();
139+
[Throws=NodeError]
140+
PaymentDetailsPage list_payments(PageToken? page_token);
140141
sequence<PeerDetails> list_peers();
141142
sequence<ChannelDetails> list_channels();
142143
NetworkGraph network_graph();
@@ -261,6 +262,11 @@ enum PaymentFailureReason {
261262

262263
typedef dictionary PaymentDetails;
263264

265+
typedef dictionary PaymentDetailsPage;
266+
267+
[Remote]
268+
interface PageToken {};
269+
264270
[Remote]
265271
dictionary RouteParametersConfig {
266272
u64? max_total_routing_fee_msat;

src/data_store.rs

Lines changed: 80 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ use std::collections::HashMap;
99
use std::ops::Deref;
1010
use std::sync::{Arc, Mutex};
1111

12-
use lightning::util::persist::KVStore;
12+
use lightning::util::persist::{KVStore, PageToken, PaginatedKVStore};
1313
use lightning::util::ser::{Readable, Writeable};
1414

1515
use crate::logger::{log_error, LdkLogger};
@@ -179,6 +179,47 @@ where
179179
self.objects.lock().expect("lock").values().filter(f).cloned().collect::<Vec<SO>>()
180180
}
181181

182+
/// Returns a page of objects, ordered from most recently created to least recently created,
183+
/// together with a token that can be passed to a subsequent call to retrieve the next page.
184+
///
185+
/// The underlying store is only queried for the page's key order, which the in-memory map
186+
/// doesn't track; the objects themselves are served from memory.
187+
pub(crate) async fn list_page(
188+
&self, page_token: Option<PageToken>,
189+
) -> Result<(Vec<SO>, Option<PageToken>), Error> {
190+
let response = PaginatedKVStore::list_paginated(
191+
&*self.kv_store,
192+
&self.primary_namespace,
193+
&self.secondary_namespace,
194+
page_token,
195+
)
196+
.await
197+
.map_err(|e| {
198+
log_error!(
199+
self.logger,
200+
"Listing object data under {}/{} failed due to: {}",
201+
&self.primary_namespace,
202+
&self.secondary_namespace,
203+
e
204+
);
205+
Error::PersistenceFailed
206+
})?;
207+
208+
let locked_objects = self.objects.lock().expect("lock");
209+
let objects_by_store_key: HashMap<String, &SO> =
210+
locked_objects.values().map(|obj| (obj.id().encode_to_hex_str(), obj)).collect();
211+
212+
// Objects removed between listing the page's keys and this lookup are skipped rather
213+
// than failing the page.
214+
let objects = response
215+
.keys
216+
.iter()
217+
.filter_map(|key| objects_by_store_key.get(key).map(|obj| (*obj).clone()))
218+
.collect();
219+
220+
Ok((objects, response.next_page_token))
221+
}
222+
182223
async fn persist(&self, object: &SO) -> Result<(), Error> {
183224
let (store_key, data) = Self::encode_object(object);
184225
self.persist_encoded(store_key, data).await
@@ -338,6 +379,44 @@ mod tests {
338379
)
339380
}
340381

382+
#[tokio::test]
383+
async fn list_page_paginates_in_reverse_creation_order() {
384+
let store: Arc<DynStore> = Arc::new(DynStoreWrapper(InMemoryStore::new()));
385+
let logger = Arc::new(TestLogger::new());
386+
let data_store: DataStore<TestObject, Arc<TestLogger>> = DataStore::new(
387+
Vec::new(),
388+
"datastore_test_primary".to_string(),
389+
"datastore_test_secondary".to_string(),
390+
Arc::clone(&store),
391+
logger,
392+
);
393+
394+
// Insert more objects than fit in a single page to exercise the pagination loop.
395+
let num_objects = 120u32;
396+
for i in 0..num_objects {
397+
let id = TestObjectId { id: i.to_be_bytes() };
398+
data_store.insert(TestObject { id, data: [7u8; 3] }).await.unwrap();
399+
}
400+
401+
let mut listed = Vec::with_capacity(num_objects as usize);
402+
let mut page_token = None;
403+
loop {
404+
let (page, next_page_token) = data_store.list_page(page_token).await.unwrap();
405+
assert!(!page.is_empty());
406+
listed.extend(page);
407+
page_token = next_page_token;
408+
if page_token.is_none() {
409+
break;
410+
}
411+
}
412+
413+
let expected: Vec<TestObject> = (0..num_objects)
414+
.rev()
415+
.map(|i| TestObject { id: TestObjectId { id: i.to_be_bytes() }, data: [7u8; 3] })
416+
.collect();
417+
assert_eq!(listed, expected);
418+
}
419+
341420
#[tokio::test]
342421
async fn data_is_persisted() {
343422
let store: Arc<DynStore> = Arc::new(DynStoreWrapper(InMemoryStore::new()));

src/lib.rs

Lines changed: 47 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,7 @@ use lightning::ln::peer_handler::CustomMessageHandler;
156156
use lightning::routing::gossip::NodeAlias;
157157
use lightning::sign::EntropySource;
158158
use lightning::util::persist::KVStore;
159+
pub use lightning::util::persist::PageToken;
159160
use lightning::util::wallet_utils::{Input, Wallet as LdkWallet};
160161
use lightning_background_processor::process_events_async;
161162
pub use lightning_invoice;
@@ -2124,15 +2125,44 @@ impl Node {
21242125
/// # let node = builder.build(node_entropy.into()).unwrap();
21252126
/// node.list_payments_with_filter(|p| p.direction == PaymentDirection::Outbound);
21262127
/// ```
2128+
#[deprecated(
2129+
note = "Use the paginated list_payments API and filter the returned pages instead."
2130+
)]
21272131
pub fn list_payments_with_filter<F: FnMut(&&PaymentDetails) -> bool>(
21282132
&self, f: F,
21292133
) -> Vec<PaymentDetails> {
21302134
self.payment_store.list_filter(f)
21312135
}
21322136

2133-
/// Retrieves all payments.
2134-
pub fn list_payments(&self) -> Vec<PaymentDetails> {
2135-
self.payment_store.list_filter(|_| true)
2137+
/// Retrieves a page of payments from the underlying paginated store, ordered from most
2138+
/// recently created to least recently created.
2139+
///
2140+
/// Pass `None` to start listing from the most recently created payment. If the returned
2141+
/// [`PaymentDetailsPage::next_page_token`] is `Some`, pass it to a subsequent call to
2142+
/// retrieve the next page.
2143+
#[cfg(not(feature = "uniffi"))]
2144+
pub fn list_payments(
2145+
&self, page_token: Option<PageToken>,
2146+
) -> Result<PaymentDetailsPage, Error> {
2147+
let (payments, next_page_token) =
2148+
self.runtime.block_on(self.payment_store.list_page(page_token))?;
2149+
Ok(PaymentDetailsPage { payments, next_page_token })
2150+
}
2151+
2152+
/// Retrieves a page of payments from the underlying paginated store, ordered from most
2153+
/// recently created to least recently created.
2154+
///
2155+
/// Pass `None` to start listing from the most recently created payment. If the returned
2156+
/// [`PaymentDetailsPage::next_page_token`] is `Some`, pass it to a subsequent call to
2157+
/// retrieve the next page.
2158+
#[cfg(feature = "uniffi")]
2159+
pub fn list_payments(
2160+
&self, page_token: Option<Arc<PageToken>>,
2161+
) -> Result<PaymentDetailsPage, Error> {
2162+
let page_token = page_token.map(|t| (*t).clone());
2163+
let (payments, next_page_token) =
2164+
self.runtime.block_on(self.payment_store.list_page(page_token))?;
2165+
Ok(PaymentDetailsPage { payments, next_page_token: next_page_token.map(Arc::new) })
21362166
}
21372167

21382168
/// Retrieves a list of known peers.
@@ -2250,6 +2280,20 @@ impl Drop for Node {
22502280
}
22512281
}
22522282

2283+
/// A page of payments returned from a paginated listing.
2284+
#[derive(Clone, Debug, PartialEq, Eq)]
2285+
#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
2286+
pub struct PaymentDetailsPage {
2287+
/// Payments in this page, ordered from most recently created to least recently created.
2288+
pub payments: Vec<PaymentDetails>,
2289+
/// Token to pass to the next call to continue listing, if another page exists.
2290+
#[cfg(not(feature = "uniffi"))]
2291+
pub next_page_token: Option<PageToken>,
2292+
/// Token to pass to the next call to continue listing, if another page exists.
2293+
#[cfg(feature = "uniffi")]
2294+
pub next_page_token: Option<Arc<PageToken>>,
2295+
}
2296+
22532297
/// The best known block as identified by its hash and height.
22542298
#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq)]
22552299
#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]

0 commit comments

Comments
 (0)