Skip to content

Commit 14ea435

Browse files
committed
Include on-chain payments in list API
ListPayments only read from ldk-server's paginated payment store, which is populated from Lightning payment events. On-chain payments are stored by LDK Node directly, so they never appeared in paginated list responses. Backfill on-chain payments from LDK Node before reading the existing paginated store. This keeps list payments complete until LDK Node owns paginated listing for both Lightning and on-chain payments. Add an e2e test covering an on-chain send appearing in list payments.
1 parent 3f98b85 commit 14ea435

2 files changed

Lines changed: 86 additions & 1 deletion

File tree

e2e-tests/tests/e2e.rs

Lines changed: 50 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,8 @@ use ldk_node::lightning::offers::offer::Offer;
2222
use ldk_node::lightning_invoice::Bolt11Invoice;
2323
use ldk_server_client::client::EventStream;
2424
use ldk_server_client::ldk_server_grpc::api::{
25-
Bolt11ReceiveRequest, Bolt12ReceiveRequest, OnchainReceiveRequest, OpenChannelRequest,
25+
Bolt11ReceiveRequest, Bolt12ReceiveRequest, ListPaymentsRequest, OnchainReceiveRequest,
26+
OnchainSendRequest, OpenChannelRequest,
2627
};
2728
use ldk_server_client::ldk_server_grpc::events::event_envelope::Event;
2829
use ldk_server_client::ldk_server_grpc::events::{
@@ -361,6 +362,54 @@ async fn test_cli_onchain_send() {
361362
assert!(!output["txid"].as_str().unwrap().is_empty());
362363
}
363364

365+
#[tokio::test]
366+
async fn test_list_payments_includes_onchain_send() {
367+
let bitcoind = TestBitcoind::new();
368+
let server = LdkServerHandle::start(&bitcoind).await;
369+
370+
let addr = server.client().onchain_receive(OnchainReceiveRequest {}).await.unwrap().address;
371+
bitcoind.fund_address(&addr, 1.0);
372+
mine_and_sync(&bitcoind, &[&server], 6).await;
373+
wait_for_onchain_balance(server.client(), Duration::from_secs(30)).await;
374+
375+
let dest_addr =
376+
server.client().onchain_receive(OnchainReceiveRequest {}).await.unwrap().address;
377+
let send_output = server
378+
.client()
379+
.onchain_send(OnchainSendRequest {
380+
address: dest_addr,
381+
amount_sats: Some(50_000),
382+
send_all: None,
383+
fee_rate_sat_per_vb: None,
384+
})
385+
.await
386+
.unwrap();
387+
388+
let mut found_payment = false;
389+
for _ in 0..30 {
390+
let output =
391+
server.client().list_payments(ListPaymentsRequest { page_token: None }).await.unwrap();
392+
found_payment = output.payments.iter().any(|payment| {
393+
matches!(
394+
payment.kind.as_ref().and_then(|kind| kind.kind.as_ref()),
395+
Some(ldk_server_grpc::types::payment_kind::Kind::Onchain(
396+
onchain
397+
)) if onchain.txid == send_output.txid
398+
)
399+
});
400+
if found_payment {
401+
break;
402+
}
403+
tokio::time::sleep(Duration::from_secs(1)).await;
404+
}
405+
assert!(found_payment);
406+
407+
let output =
408+
server.client().list_payments(ListPaymentsRequest { page_token: None }).await.unwrap();
409+
410+
assert_eq!(output.payments.len(), 2, "Expected two payments in list");
411+
}
412+
364413
#[tokio::test]
365414
async fn test_cli_connect_peer() {
366415
let bitcoind = TestBitcoind::new();

ldk-server/src/api/list_payments.rs

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
use std::sync::Arc;
1111

1212
use bytes::Bytes;
13+
use ldk_node::payment::{PaymentDetails, PaymentKind};
1314
use ldk_server_grpc::api::{ListPaymentsRequest, ListPaymentsResponse};
1415
use ldk_server_grpc::types::{PageToken, Payment};
1516
use prost::Message;
@@ -20,10 +21,16 @@ use crate::io::persist::{
2021
PAYMENTS_PERSISTENCE_PRIMARY_NAMESPACE, PAYMENTS_PERSISTENCE_SECONDARY_NAMESPACE,
2122
};
2223
use crate::service::Context;
24+
use crate::util::proto_adapter::payment_to_proto;
2325

2426
pub(crate) async fn handle_list_payments_request(
2527
context: Arc<Context>, request: ListPaymentsRequest,
2628
) -> Result<ListPaymentsResponse, LdkServerError> {
29+
// TODO: Remove this backfill once LDK Node owns paginated payment listing. Today our
30+
// paginated store is populated from Lightning events only, while on-chain payments live in
31+
// LDK Node's payment store.
32+
sync_onchain_payments_to_paginated_store(&context)?;
33+
2734
let page_token = request.page_token.map(|p| (p.token, p.index));
2835
let list_response = context
2936
.paginated_kv_store
@@ -64,3 +71,32 @@ pub(crate) async fn handle_list_payments_request(
6471
};
6572
Ok(response)
6673
}
74+
75+
// TODO: Delete this temporary bridge when on-chain and Lightning payments are served from the
76+
// same paginated LDK Node source.
77+
fn sync_onchain_payments_to_paginated_store(context: &Context) -> Result<(), LdkServerError> {
78+
for payment_details in context.node.list_payments_with_filter(is_onchain_payment).into_iter() {
79+
let payment = payment_to_proto(payment_details);
80+
context
81+
.paginated_kv_store
82+
.write(
83+
PAYMENTS_PERSISTENCE_PRIMARY_NAMESPACE,
84+
PAYMENTS_PERSISTENCE_SECONDARY_NAMESPACE,
85+
&payment.id,
86+
payment.latest_update_timestamp as i64,
87+
&payment.encode_to_vec(),
88+
)
89+
.map_err(|e| {
90+
LdkServerError::new(
91+
InternalServerError,
92+
format!("Failed to write on-chain payment data: {e}"),
93+
)
94+
})?;
95+
}
96+
97+
Ok(())
98+
}
99+
100+
fn is_onchain_payment(payment: &&PaymentDetails) -> bool {
101+
matches!(payment.kind, PaymentKind::Onchain { .. })
102+
}

0 commit comments

Comments
 (0)