Skip to content

Commit d66a156

Browse files
author
anon
committed
feat: channel events
1 parent 570ed52 commit d66a156

5 files changed

Lines changed: 749 additions & 30 deletions

File tree

docs/api-guide.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -188,7 +188,7 @@ See [Pagination](#pagination) below for how to page through results.
188188

189189
| RPC | Description |
190190
|-------------------|-------------------------------------------------------------|
191-
| `SubscribeEvents` | **Server-streaming.** Subscribe to real-time payment events |
191+
| `SubscribeEvents` | **Server-streaming.** Subscribe to real-time payment and channel events |
192192

193193
`SubscribeEvents` returns a stream of `EventEnvelope` messages. Each envelope contains one of:
194194

@@ -199,6 +199,7 @@ See [Pagination](#pagination) below for how to page through results.
199199
| `PaymentFailed` | An outbound payment failed |
200200
| `PaymentClaimable` | A hodl invoice payment arrived and is waiting to be claimed or failed |
201201
| `PaymentForwarded` | A payment was routed through this node |
202+
| `ChannelStateChanged` | A channel changed state (pending, ready, open failed, closed) |
202203

203204
Events are broadcast to all connected subscribers. The server uses a bounded broadcast channel
204205
(capacity 1024). A slow subscriber that falls behind will miss events.

ldk-server-client/README.md

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ The client handles HMAC-SHA256 authentication automatically. Pass the hex-encode
3434

3535
## Event Streaming
3636

37-
Subscribe to real-time payment events:
37+
Subscribe to real-time payment and channel events:
3838

3939
```rust,no_run
4040
# use ldk_server_client::client::LdkServerClient;
@@ -52,6 +52,39 @@ while let Some(result) = stream.next_message().await {
5252
# }
5353
```
5454

55+
Pattern-match channel state changes:
56+
57+
```rust,no_run
58+
# use ldk_server_client::client::LdkServerClient;
59+
# use ldk_server_client::ldk_server_grpc::events::{event_envelope, ChannelState};
60+
# #[tokio::main]
61+
# async fn main() {
62+
# let cert_pem = std::fs::read("/path/to/tls.crt").unwrap();
63+
# let client = LdkServerClient::new("localhost:3536".to_string(), "key".to_string(), &cert_pem).unwrap();
64+
let mut stream = client.subscribe_events().await.unwrap();
65+
while let Some(result) = stream.next_message().await {
66+
match result {
67+
Ok(event) => {
68+
if let Some(event_envelope::Event::ChannelStateChanged(channel_event)) = event.event {
69+
let state = ChannelState::from_i32(channel_event.state)
70+
.unwrap_or(ChannelState::Unspecified);
71+
println!(
72+
"channel {} -> {}",
73+
channel_event.channel_id,
74+
state.as_str_name()
75+
);
76+
77+
if let Some(reason) = channel_event.reason {
78+
println!("reason: {}", reason.message);
79+
}
80+
}
81+
}
82+
Err(e) => eprintln!("Error: {}", e),
83+
}
84+
}
85+
# }
86+
```
87+
5588
## Features
5689

5790
- **`serde`**: Enables `serde::Serialize` and `serde::Deserialize` on all proto types

ldk-server-grpc/src/events.rs

Lines changed: 291 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
#[allow(clippy::derive_partial_eq_without_eq)]
1414
#[derive(Clone, PartialEq, ::prost::Message)]
1515
pub struct EventEnvelope {
16-
#[prost(oneof = "event_envelope::Event", tags = "2, 3, 4, 6, 7")]
16+
#[prost(oneof = "event_envelope::Event", tags = "2, 3, 4, 6, 7, 8")]
1717
pub event: ::core::option::Option<event_envelope::Event>,
1818
}
1919
/// Nested message and enum types in `EventEnvelope`.
@@ -33,8 +33,105 @@ pub mod event_envelope {
3333
PaymentForwarded(super::PaymentForwarded),
3434
#[prost(message, tag = "7")]
3535
PaymentClaimable(super::PaymentClaimable),
36+
#[prost(message, tag = "8")]
37+
ChannelStateChanged(super::ChannelStateChanged),
3638
}
3739
}
40+
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
41+
#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
42+
#[allow(clippy::derive_partial_eq_without_eq)]
43+
#[derive(Clone, PartialEq, ::prost::Message)]
44+
pub struct CounterpartyForceClosedDetails {
45+
#[prost(string, tag = "1")]
46+
pub peer_msg: ::prost::alloc::string::String,
47+
}
48+
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
49+
#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
50+
#[allow(clippy::derive_partial_eq_without_eq)]
51+
#[derive(Clone, PartialEq, ::prost::Message)]
52+
pub struct HolderForceClosedDetails {
53+
#[prost(bool, optional, tag = "1")]
54+
pub broadcasted_latest_txn: ::core::option::Option<bool>,
55+
#[prost(string, tag = "2")]
56+
pub message: ::prost::alloc::string::String,
57+
}
58+
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
59+
#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
60+
#[allow(clippy::derive_partial_eq_without_eq)]
61+
#[derive(Clone, PartialEq, ::prost::Message)]
62+
pub struct ProcessingErrorDetails {
63+
#[prost(string, tag = "1")]
64+
pub err: ::prost::alloc::string::String,
65+
}
66+
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
67+
#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
68+
#[allow(clippy::derive_partial_eq_without_eq)]
69+
#[derive(Clone, PartialEq, ::prost::Message)]
70+
pub struct HtlcsTimedOutDetails {
71+
#[prost(string, optional, tag = "1")]
72+
pub payment_hash: ::core::option::Option<::prost::alloc::string::String>,
73+
}
74+
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
75+
#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
76+
#[allow(clippy::derive_partial_eq_without_eq)]
77+
#[derive(Clone, PartialEq, ::prost::Message)]
78+
pub struct PeerFeerateTooLowDetails {
79+
#[prost(uint32, tag = "1")]
80+
pub peer_feerate_sat_per_kw: u32,
81+
#[prost(uint32, tag = "2")]
82+
pub required_feerate_sat_per_kw: u32,
83+
}
84+
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
85+
#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
86+
#[allow(clippy::derive_partial_eq_without_eq)]
87+
#[derive(Clone, PartialEq, ::prost::Message)]
88+
pub struct ChannelStateChangeReason {
89+
#[prost(enumeration = "ChannelStateChangeReasonKind", tag = "1")]
90+
pub kind: i32,
91+
#[prost(string, tag = "2")]
92+
pub message: ::prost::alloc::string::String,
93+
#[prost(oneof = "channel_state_change_reason::Details", tags = "3, 4, 5, 6, 7")]
94+
pub details: ::core::option::Option<channel_state_change_reason::Details>,
95+
}
96+
/// Nested message and enum types in `ChannelStateChangeReason`.
97+
pub mod channel_state_change_reason {
98+
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
99+
#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
100+
#[allow(clippy::derive_partial_eq_without_eq)]
101+
#[derive(Clone, PartialEq, ::prost::Oneof)]
102+
pub enum Details {
103+
#[prost(message, tag = "3")]
104+
CounterpartyForceClosed(super::CounterpartyForceClosedDetails),
105+
#[prost(message, tag = "4")]
106+
HolderForceClosed(super::HolderForceClosedDetails),
107+
#[prost(message, tag = "5")]
108+
ProcessingError(super::ProcessingErrorDetails),
109+
#[prost(message, tag = "6")]
110+
HtlcsTimedOut(super::HtlcsTimedOutDetails),
111+
#[prost(message, tag = "7")]
112+
PeerFeerateTooLow(super::PeerFeerateTooLowDetails),
113+
}
114+
}
115+
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
116+
#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
117+
#[allow(clippy::derive_partial_eq_without_eq)]
118+
#[derive(Clone, PartialEq, ::prost::Message)]
119+
pub struct ChannelStateChanged {
120+
#[prost(string, tag = "1")]
121+
pub channel_id: ::prost::alloc::string::String,
122+
#[prost(string, tag = "2")]
123+
pub user_channel_id: ::prost::alloc::string::String,
124+
#[prost(string, optional, tag = "3")]
125+
pub counterparty_node_id: ::core::option::Option<::prost::alloc::string::String>,
126+
#[prost(enumeration = "ChannelState", tag = "4")]
127+
pub state: i32,
128+
#[prost(string, optional, tag = "5")]
129+
pub funding_txo: ::core::option::Option<::prost::alloc::string::String>,
130+
#[prost(message, optional, tag = "6")]
131+
pub reason: ::core::option::Option<ChannelStateChangeReason>,
132+
#[prost(enumeration = "ChannelClosureInitiator", tag = "7")]
133+
pub closure_initiator: i32,
134+
}
38135
/// PaymentReceived indicates a payment has been received.
39136
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
40137
#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
@@ -85,3 +182,196 @@ pub struct PaymentForwarded {
85182
#[prost(message, optional, tag = "1")]
86183
pub forwarded_payment: ::core::option::Option<super::types::ForwardedPayment>,
87184
}
185+
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
186+
#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
187+
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, ::prost::Enumeration)]
188+
#[repr(i32)]
189+
pub enum ChannelState {
190+
Unspecified = 0,
191+
Pending = 1,
192+
Ready = 2,
193+
OpenFailed = 3,
194+
Closed = 4,
195+
}
196+
impl ChannelState {
197+
/// String value of the enum field names used in the ProtoBuf definition.
198+
///
199+
/// The values are not transformed in any way and thus are considered stable
200+
/// (if the ProtoBuf definition does not change) and safe for programmatic use.
201+
pub fn as_str_name(&self) -> &'static str {
202+
match self {
203+
ChannelState::Unspecified => "CHANNEL_STATE_UNSPECIFIED",
204+
ChannelState::Pending => "CHANNEL_STATE_PENDING",
205+
ChannelState::Ready => "CHANNEL_STATE_READY",
206+
ChannelState::OpenFailed => "CHANNEL_STATE_OPEN_FAILED",
207+
ChannelState::Closed => "CHANNEL_STATE_CLOSED",
208+
}
209+
}
210+
/// Creates an enum from field names used in the ProtoBuf definition.
211+
pub fn from_str_name(value: &str) -> ::core::option::Option<Self> {
212+
match value {
213+
"CHANNEL_STATE_UNSPECIFIED" => Some(Self::Unspecified),
214+
"CHANNEL_STATE_PENDING" => Some(Self::Pending),
215+
"CHANNEL_STATE_READY" => Some(Self::Ready),
216+
"CHANNEL_STATE_OPEN_FAILED" => Some(Self::OpenFailed),
217+
"CHANNEL_STATE_CLOSED" => Some(Self::Closed),
218+
_ => None,
219+
}
220+
}
221+
}
222+
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
223+
#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
224+
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, ::prost::Enumeration)]
225+
#[repr(i32)]
226+
pub enum ChannelClosureInitiator {
227+
Unspecified = 0,
228+
Local = 1,
229+
Remote = 2,
230+
Unknown = 3,
231+
}
232+
impl ChannelClosureInitiator {
233+
/// String value of the enum field names used in the ProtoBuf definition.
234+
///
235+
/// The values are not transformed in any way and thus are considered stable
236+
/// (if the ProtoBuf definition does not change) and safe for programmatic use.
237+
pub fn as_str_name(&self) -> &'static str {
238+
match self {
239+
ChannelClosureInitiator::Unspecified => "CHANNEL_CLOSURE_INITIATOR_UNSPECIFIED",
240+
ChannelClosureInitiator::Local => "CHANNEL_CLOSURE_INITIATOR_LOCAL",
241+
ChannelClosureInitiator::Remote => "CHANNEL_CLOSURE_INITIATOR_REMOTE",
242+
ChannelClosureInitiator::Unknown => "CHANNEL_CLOSURE_INITIATOR_UNKNOWN",
243+
}
244+
}
245+
/// Creates an enum from field names used in the ProtoBuf definition.
246+
pub fn from_str_name(value: &str) -> ::core::option::Option<Self> {
247+
match value {
248+
"CHANNEL_CLOSURE_INITIATOR_UNSPECIFIED" => Some(Self::Unspecified),
249+
"CHANNEL_CLOSURE_INITIATOR_LOCAL" => Some(Self::Local),
250+
"CHANNEL_CLOSURE_INITIATOR_REMOTE" => Some(Self::Remote),
251+
"CHANNEL_CLOSURE_INITIATOR_UNKNOWN" => Some(Self::Unknown),
252+
_ => None,
253+
}
254+
}
255+
}
256+
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
257+
#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
258+
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, ::prost::Enumeration)]
259+
#[repr(i32)]
260+
pub enum ChannelStateChangeReasonKind {
261+
Unspecified = 0,
262+
CounterpartyForceClosed = 1,
263+
HolderForceClosed = 2,
264+
LegacyCooperativeClosure = 3,
265+
CounterpartyInitiatedCooperativeClosure = 4,
266+
LocallyInitiatedCooperativeClosure = 5,
267+
CommitmentTxConfirmed = 6,
268+
FundingTimedOut = 7,
269+
ProcessingError = 8,
270+
DisconnectedPeer = 9,
271+
OutdatedChannelManager = 10,
272+
CounterpartyCoopClosedUnfundedChannel = 11,
273+
LocallyCoopClosedUnfundedChannel = 12,
274+
FundingBatchClosure = 13,
275+
HtlcsTimedOut = 14,
276+
PeerFeerateTooLow = 15,
277+
}
278+
impl ChannelStateChangeReasonKind {
279+
/// String value of the enum field names used in the ProtoBuf definition.
280+
///
281+
/// The values are not transformed in any way and thus are considered stable
282+
/// (if the ProtoBuf definition does not change) and safe for programmatic use.
283+
pub fn as_str_name(&self) -> &'static str {
284+
match self {
285+
ChannelStateChangeReasonKind::Unspecified => {
286+
"CHANNEL_STATE_CHANGE_REASON_KIND_UNSPECIFIED"
287+
},
288+
ChannelStateChangeReasonKind::CounterpartyForceClosed => {
289+
"CHANNEL_STATE_CHANGE_REASON_KIND_COUNTERPARTY_FORCE_CLOSED"
290+
},
291+
ChannelStateChangeReasonKind::HolderForceClosed => {
292+
"CHANNEL_STATE_CHANGE_REASON_KIND_HOLDER_FORCE_CLOSED"
293+
},
294+
ChannelStateChangeReasonKind::LegacyCooperativeClosure => {
295+
"CHANNEL_STATE_CHANGE_REASON_KIND_LEGACY_COOPERATIVE_CLOSURE"
296+
},
297+
ChannelStateChangeReasonKind::CounterpartyInitiatedCooperativeClosure => {
298+
"CHANNEL_STATE_CHANGE_REASON_KIND_COUNTERPARTY_INITIATED_COOPERATIVE_CLOSURE"
299+
},
300+
ChannelStateChangeReasonKind::LocallyInitiatedCooperativeClosure => {
301+
"CHANNEL_STATE_CHANGE_REASON_KIND_LOCALLY_INITIATED_COOPERATIVE_CLOSURE"
302+
},
303+
ChannelStateChangeReasonKind::CommitmentTxConfirmed => {
304+
"CHANNEL_STATE_CHANGE_REASON_KIND_COMMITMENT_TX_CONFIRMED"
305+
},
306+
ChannelStateChangeReasonKind::FundingTimedOut => {
307+
"CHANNEL_STATE_CHANGE_REASON_KIND_FUNDING_TIMED_OUT"
308+
},
309+
ChannelStateChangeReasonKind::ProcessingError => {
310+
"CHANNEL_STATE_CHANGE_REASON_KIND_PROCESSING_ERROR"
311+
},
312+
ChannelStateChangeReasonKind::DisconnectedPeer => {
313+
"CHANNEL_STATE_CHANGE_REASON_KIND_DISCONNECTED_PEER"
314+
},
315+
ChannelStateChangeReasonKind::OutdatedChannelManager => {
316+
"CHANNEL_STATE_CHANGE_REASON_KIND_OUTDATED_CHANNEL_MANAGER"
317+
},
318+
ChannelStateChangeReasonKind::CounterpartyCoopClosedUnfundedChannel => {
319+
"CHANNEL_STATE_CHANGE_REASON_KIND_COUNTERPARTY_COOP_CLOSED_UNFUNDED_CHANNEL"
320+
},
321+
ChannelStateChangeReasonKind::LocallyCoopClosedUnfundedChannel => {
322+
"CHANNEL_STATE_CHANGE_REASON_KIND_LOCALLY_COOP_CLOSED_UNFUNDED_CHANNEL"
323+
},
324+
ChannelStateChangeReasonKind::FundingBatchClosure => {
325+
"CHANNEL_STATE_CHANGE_REASON_KIND_FUNDING_BATCH_CLOSURE"
326+
},
327+
ChannelStateChangeReasonKind::HtlcsTimedOut => {
328+
"CHANNEL_STATE_CHANGE_REASON_KIND_HTLCS_TIMED_OUT"
329+
},
330+
ChannelStateChangeReasonKind::PeerFeerateTooLow => {
331+
"CHANNEL_STATE_CHANGE_REASON_KIND_PEER_FEERATE_TOO_LOW"
332+
},
333+
}
334+
}
335+
/// Creates an enum from field names used in the ProtoBuf definition.
336+
pub fn from_str_name(value: &str) -> ::core::option::Option<Self> {
337+
match value {
338+
"CHANNEL_STATE_CHANGE_REASON_KIND_UNSPECIFIED" => Some(Self::Unspecified),
339+
"CHANNEL_STATE_CHANGE_REASON_KIND_COUNTERPARTY_FORCE_CLOSED" => {
340+
Some(Self::CounterpartyForceClosed)
341+
},
342+
"CHANNEL_STATE_CHANGE_REASON_KIND_HOLDER_FORCE_CLOSED" => Some(Self::HolderForceClosed),
343+
"CHANNEL_STATE_CHANGE_REASON_KIND_LEGACY_COOPERATIVE_CLOSURE" => {
344+
Some(Self::LegacyCooperativeClosure)
345+
},
346+
"CHANNEL_STATE_CHANGE_REASON_KIND_COUNTERPARTY_INITIATED_COOPERATIVE_CLOSURE" => {
347+
Some(Self::CounterpartyInitiatedCooperativeClosure)
348+
},
349+
"CHANNEL_STATE_CHANGE_REASON_KIND_LOCALLY_INITIATED_COOPERATIVE_CLOSURE" => {
350+
Some(Self::LocallyInitiatedCooperativeClosure)
351+
},
352+
"CHANNEL_STATE_CHANGE_REASON_KIND_COMMITMENT_TX_CONFIRMED" => {
353+
Some(Self::CommitmentTxConfirmed)
354+
},
355+
"CHANNEL_STATE_CHANGE_REASON_KIND_FUNDING_TIMED_OUT" => Some(Self::FundingTimedOut),
356+
"CHANNEL_STATE_CHANGE_REASON_KIND_PROCESSING_ERROR" => Some(Self::ProcessingError),
357+
"CHANNEL_STATE_CHANGE_REASON_KIND_DISCONNECTED_PEER" => Some(Self::DisconnectedPeer),
358+
"CHANNEL_STATE_CHANGE_REASON_KIND_OUTDATED_CHANNEL_MANAGER" => {
359+
Some(Self::OutdatedChannelManager)
360+
},
361+
"CHANNEL_STATE_CHANGE_REASON_KIND_COUNTERPARTY_COOP_CLOSED_UNFUNDED_CHANNEL" => {
362+
Some(Self::CounterpartyCoopClosedUnfundedChannel)
363+
},
364+
"CHANNEL_STATE_CHANGE_REASON_KIND_LOCALLY_COOP_CLOSED_UNFUNDED_CHANNEL" => {
365+
Some(Self::LocallyCoopClosedUnfundedChannel)
366+
},
367+
"CHANNEL_STATE_CHANGE_REASON_KIND_FUNDING_BATCH_CLOSURE" => {
368+
Some(Self::FundingBatchClosure)
369+
},
370+
"CHANNEL_STATE_CHANGE_REASON_KIND_HTLCS_TIMED_OUT" => Some(Self::HtlcsTimedOut),
371+
"CHANNEL_STATE_CHANGE_REASON_KIND_PEER_FEERATE_TOO_LOW" => {
372+
Some(Self::PeerFeerateTooLow)
373+
},
374+
_ => None,
375+
}
376+
}
377+
}

0 commit comments

Comments
 (0)