diff --git a/e2e-tests/tests/e2e.rs b/e2e-tests/tests/e2e.rs index 0fb634a8..3a99f383 100644 --- a/e2e-tests/tests/e2e.rs +++ b/e2e-tests/tests/e2e.rs @@ -22,7 +22,8 @@ use ldk_node::lightning::offers::offer::Offer; use ldk_node::lightning_invoice::Bolt11Invoice; use ldk_server_client::client::EventStream; use ldk_server_client::ldk_server_grpc::api::{ - Bolt11ReceiveRequest, Bolt12ReceiveRequest, OnchainReceiveRequest, OpenChannelRequest, + Bolt11ReceiveRequest, Bolt12ReceiveRequest, ListPaymentsRequest, OnchainReceiveRequest, + OnchainSendRequest, OpenChannelRequest, }; use ldk_server_client::ldk_server_grpc::events::event_envelope::Event; use ldk_server_client::ldk_server_grpc::events::{ @@ -361,6 +362,54 @@ async fn test_cli_onchain_send() { assert!(!output["txid"].as_str().unwrap().is_empty()); } +#[tokio::test] +async fn test_list_payments_includes_onchain_send() { + let bitcoind = TestBitcoind::new(); + let server = LdkServerHandle::start(&bitcoind).await; + + let addr = server.client().onchain_receive(OnchainReceiveRequest {}).await.unwrap().address; + bitcoind.fund_address(&addr, 1.0); + mine_and_sync(&bitcoind, &[&server], 6).await; + wait_for_onchain_balance(server.client(), Duration::from_secs(30)).await; + + let dest_addr = + server.client().onchain_receive(OnchainReceiveRequest {}).await.unwrap().address; + let send_output = server + .client() + .onchain_send(OnchainSendRequest { + address: dest_addr, + amount_sats: Some(50_000), + send_all: None, + fee_rate_sat_per_vb: None, + }) + .await + .unwrap(); + + let mut found_payment = false; + for _ in 0..30 { + let output = + server.client().list_payments(ListPaymentsRequest { page_token: None }).await.unwrap(); + found_payment = output.payments.iter().any(|payment| { + matches!( + payment.kind.as_ref().and_then(|kind| kind.kind.as_ref()), + Some(ldk_server_grpc::types::payment_kind::Kind::Onchain( + onchain + )) if onchain.txid == send_output.txid + ) + }); + if found_payment { + break; + } + tokio::time::sleep(Duration::from_secs(1)).await; + } + assert!(found_payment); + + let output = + server.client().list_payments(ListPaymentsRequest { page_token: None }).await.unwrap(); + + assert_eq!(output.payments.len(), 2, "Expected two payments in list"); +} + #[tokio::test] async fn test_cli_connect_peer() { let bitcoind = TestBitcoind::new(); diff --git a/ldk-server/src/api/list_payments.rs b/ldk-server/src/api/list_payments.rs index 502d7458..828188ca 100644 --- a/ldk-server/src/api/list_payments.rs +++ b/ldk-server/src/api/list_payments.rs @@ -10,6 +10,7 @@ use std::sync::Arc; use bytes::Bytes; +use ldk_node::payment::{PaymentDetails, PaymentKind}; use ldk_server_grpc::api::{ListPaymentsRequest, ListPaymentsResponse}; use ldk_server_grpc::types::{PageToken, Payment}; use prost::Message; @@ -20,10 +21,16 @@ use crate::io::persist::{ PAYMENTS_PERSISTENCE_PRIMARY_NAMESPACE, PAYMENTS_PERSISTENCE_SECONDARY_NAMESPACE, }; use crate::service::Context; +use crate::util::proto_adapter::payment_to_proto; pub(crate) async fn handle_list_payments_request( context: Arc, request: ListPaymentsRequest, ) -> Result { + // TODO: Remove this backfill once LDK Node owns paginated payment listing. Today our + // paginated store is populated from Lightning events only, while on-chain payments live in + // LDK Node's payment store. + sync_onchain_payments_to_paginated_store(&context)?; + let page_token = request.page_token.map(|p| (p.token, p.index)); let list_response = context .paginated_kv_store @@ -64,3 +71,32 @@ pub(crate) async fn handle_list_payments_request( }; Ok(response) } + +// TODO: Delete this temporary bridge when on-chain and Lightning payments are served from the +// same paginated LDK Node source. +fn sync_onchain_payments_to_paginated_store(context: &Context) -> Result<(), LdkServerError> { + for payment_details in context.node.list_payments().into_iter().filter(is_onchain_payment) { + let payment = payment_to_proto(payment_details); + context + .paginated_kv_store + .write( + PAYMENTS_PERSISTENCE_PRIMARY_NAMESPACE, + PAYMENTS_PERSISTENCE_SECONDARY_NAMESPACE, + &payment.id, + payment.latest_update_timestamp as i64, + &payment.encode_to_vec(), + ) + .map_err(|e| { + LdkServerError::new( + InternalServerError, + format!("Failed to write on-chain payment data: {e}"), + ) + })?; + } + + Ok(()) +} + +fn is_onchain_payment(payment: &PaymentDetails) -> bool { + matches!(payment.kind, PaymentKind::Onchain { .. }) +}