diff --git a/docs/api-guide.md b/docs/api-guide.md index 1b092c32..badd3fb0 100644 --- a/docs/api-guide.md +++ b/docs/api-guide.md @@ -212,6 +212,8 @@ See [Pagination](#pagination) below for how to page through results. | `PaymentClaimable` | A hodl invoice payment arrived and is waiting to be claimed or failed | | `PaymentForwarded` | A payment was routed through this node | | `ChannelStateChanged` | A channel changed state (pending, ready, open failed, closed) | +| `SpliceNegotiated` | A channel splice was negotiated and the funding transaction is pending confirmation | +| `SpliceNegotiationFailed` | A channel splice negotiation round failed | Events are broadcast to all connected subscribers. The server uses a bounded broadcast channel (capacity 1024). A slow subscriber that falls behind will miss events. diff --git a/e2e-tests/tests/e2e.rs b/e2e-tests/tests/e2e.rs index 61b2deb2..093a7625 100644 --- a/e2e-tests/tests/e2e.rs +++ b/e2e-tests/tests/e2e.rs @@ -1200,11 +1200,24 @@ async fn splice_in_via_cli(splice_amount: &str) { let server_b = LdkServerHandle::start(&bitcoind).await; let user_channel_id = setup_funded_channel(&bitcoind, &server_a, &server_b, 100_000).await; + let mut events_a = server_a.client().subscribe_events().await.unwrap(); + let output = run_cli( &server_a, &["splice-in", &user_channel_id, server_b.node_id(), splice_amount], ); assert!(output.is_object()); + + let event_a = + wait_for_event(&mut events_a, |e| matches!(e, Event::SpliceNegotiated(_))).await; + match &event_a.event { + Some(Event::SpliceNegotiated(splice_negotiated)) => { + assert_eq!(splice_negotiated.user_channel_id, user_channel_id); + assert_eq!(splice_negotiated.counterparty_node_id, server_b.node_id()); + assert!(!splice_negotiated.new_funding_txo.is_empty()); + }, + other => panic!("expected SpliceNegotiated event, got {other:?}"), + } } #[tokio::test] diff --git a/ldk-server-grpc/src/events.rs b/ldk-server-grpc/src/events.rs index 9fb50a8e..b4e54554 100644 --- a/ldk-server-grpc/src/events.rs +++ b/ldk-server-grpc/src/events.rs @@ -14,7 +14,7 @@ #[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] pub struct EventEnvelope { - #[prost(oneof = "event_envelope::Event", tags = "2, 3, 4, 6, 7, 8")] + #[prost(oneof = "event_envelope::Event", tags = "2, 3, 4, 6, 7, 8, 9, 10")] pub event: ::core::option::Option, } /// Nested message and enum types in `EventEnvelope`. @@ -36,6 +36,10 @@ pub mod event_envelope { PaymentClaimable(super::PaymentClaimable), #[prost(message, tag = "8")] ChannelStateChanged(super::ChannelStateChanged), + #[prost(message, tag = "9")] + SpliceNegotiated(super::SpliceNegotiated), + #[prost(message, tag = "10")] + SpliceNegotiationFailed(super::SpliceNegotiationFailed), } } #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] @@ -140,6 +144,38 @@ pub struct ChannelStateChanged { #[prost(enumeration = "ChannelClosureInitiator", tag = "7")] pub closure_initiator: i32, } +/// SpliceNegotiated indicates a channel splice has been negotiated and the funding +/// transaction is pending confirmation on-chain. +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))] +#[cfg_attr(feature = "serde", serde(default))] +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct SpliceNegotiated { + #[prost(string, tag = "1")] + pub channel_id: ::prost::alloc::string::String, + #[prost(string, tag = "2")] + pub user_channel_id: ::prost::alloc::string::String, + #[prost(string, tag = "3")] + pub counterparty_node_id: ::prost::alloc::string::String, + /// The outpoint of the channel's splice funding transaction. + #[prost(string, tag = "4")] + pub new_funding_txo: ::prost::alloc::string::String, +} +/// SpliceNegotiationFailed indicates a channel splice negotiation round has failed. +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))] +#[cfg_attr(feature = "serde", serde(default))] +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct SpliceNegotiationFailed { + #[prost(string, tag = "1")] + pub channel_id: ::prost::alloc::string::String, + #[prost(string, tag = "2")] + pub user_channel_id: ::prost::alloc::string::String, + #[prost(string, tag = "3")] + pub counterparty_node_id: ::prost::alloc::string::String, +} /// PaymentReceived indicates a payment has been received. #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))] diff --git a/ldk-server-grpc/src/proto/events.proto b/ldk-server-grpc/src/proto/events.proto index bc1c3cf0..1eec77b7 100644 --- a/ldk-server-grpc/src/proto/events.proto +++ b/ldk-server-grpc/src/proto/events.proto @@ -11,6 +11,8 @@ message EventEnvelope { PaymentForwarded payment_forwarded = 6; PaymentClaimable payment_claimable = 7; ChannelStateChanged channel_state_changed = 8; + SpliceNegotiated splice_negotiated = 9; + SpliceNegotiationFailed splice_negotiation_failed = 10; } } @@ -120,6 +122,23 @@ message ChannelStateChanged { ChannelClosureInitiator closure_initiator = 7; } +// SpliceNegotiated indicates a channel splice has been negotiated and the funding +// transaction is pending confirmation on-chain. +message SpliceNegotiated { + string channel_id = 1; + string user_channel_id = 2; + string counterparty_node_id = 3; + // The outpoint of the channel's splice funding transaction. + string new_funding_txo = 4; +} + +// SpliceNegotiationFailed indicates a channel splice negotiation round has failed. +message SpliceNegotiationFailed { + string channel_id = 1; + string user_channel_id = 2; + string counterparty_node_id = 3; +} + // PaymentReceived indicates a payment has been received. message PaymentReceived { // The payment details for the payment in event. diff --git a/ldk-server/src/main.rs b/ldk-server/src/main.rs index 126e8a5c..c284a61d 100644 --- a/ldk-server/src/main.rs +++ b/ldk-server/src/main.rs @@ -634,11 +634,57 @@ fn main() { } } }, - _ => { + Event::SpliceNegotiated { + channel_id, + user_channel_id, + counterparty_node_id, + new_funding_txo, + } => { + info!( + "SPLICE_NEGOTIATED: {} from counterparty {}", + channel_id, counterparty_node_id + ); + + send_channel_state_event( + event_envelope::Event::SpliceNegotiated(events::SpliceNegotiated { + channel_id: channel_id.0.to_lower_hex_string(), + user_channel_id: user_channel_id.0.to_string(), + counterparty_node_id: counterparty_node_id.to_string(), + new_funding_txo: new_funding_txo.to_string(), + }), + &event_sender, + ); + if let Err(e) = event_node.event_handled() { error!("Failed to mark event as handled: {e}"); } }, + Event::SpliceNegotiationFailed { + channel_id, + user_channel_id, + counterparty_node_id, + } => { + info!( + "SPLICE_NEGOTIATION_FAILED: {} from counterparty {}", + channel_id, counterparty_node_id + ); + + send_channel_state_event( + event_envelope::Event::SpliceNegotiationFailed( + events::SpliceNegotiationFailed { + channel_id: channel_id.0.to_lower_hex_string(), + user_channel_id: user_channel_id.0.to_string(), + counterparty_node_id: counterparty_node_id.to_string(), + }, + ), + &event_sender, + ); + + if let Err(e) = event_node.event_handled() { + error!("Failed to mark event as handled: {e}"); + } + }, + } }, res = grpc_listener.accept() => {