Skip to content

Commit 73c7bd8

Browse files
committed
Add MdkClient::splice_in wrapper
Wrap Node::splice_in with local validation (channel exists, channel is usable, amount > 0) and a typed SpliceError ADT. Cache the parsed LSP PublicKey on MdkClient so the wrapper and the upcoming splice manager do not repeat the parse on every call. The error mapping is the meaningful part. ldk-node's NodeError exposes a single ChannelSplicingFailed variant for nearly every splice failure (peer rejected, channel not yet ready, coin selection failed under fee pressure) and InsufficientFunds for the broad spendable-balance check. The splice manager needs to react differently to "no funds, retry next tick" vs "something is genuinely wrong", so the wrapper exposes the two cases as SpliceError::InsufficientFunds and SpliceError::Rejected. The mapping lives in a free helper (map_splice_error) rather than a From impl. NodeError::InsufficientFunds is also produced by open_channel and on-chain wallet paths, so a blanket From<NodeError> for SpliceError would be wrong outside splice context. A free function keeps the conversion's narrow scope obvious by name and location, and avoids adding a discoverable trait impl that future code might mistakenly use elsewhere. ldk-node does not expose a way to distinguish the fee-too-high coin-selection path from other ChannelSplicingFailed cases (the detail only appears in a log line). The splice manager will treat any Rejected as a retry candidate anyway, so we accept the lossy mapping rather than parsing log output. Anything other than InsufficientFunds collapses into Rejected. ChannelNotUsable is a third, locally-detected variant. It could have been folded into Rejected and left for ldk-node to surface, but catching it before the network round-trip is cheaper and gives the splice manager a clear signal that the channel was filtered out by our own pre-check, not by the peer. The daemon HTTP error mapper folds all SpliceError cases into Internal for now. A future POST /splicein endpoint may want finer status codes; the wrapper is only consumed internally by the splice manager today, so that refinement is deferred.
1 parent 49507a3 commit 73c7bd8

3 files changed

Lines changed: 98 additions & 2 deletions

File tree

src/daemon/api/error.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ impl From<MdkError> for AppError {
2121
MdkError::Node(msg) => AppError::Internal(msg),
2222
MdkError::Platform { message, .. } => AppError::Internal(message),
2323
MdkError::Network(msg) => AppError::Internal(msg),
24+
MdkError::Splice(e) => AppError::Internal(e.to_string()),
2425
}
2526
}
2627
}

src/mdk/client.rs

Lines changed: 66 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,20 @@
1+
use std::str::FromStr;
12
use std::sync::Arc;
23

34
use chrono::{DateTime, SecondsFormat};
45
use ldk_node::bitcoin::hashes::sha256;
56
use ldk_node::bitcoin::hashes::Hash as _;
7+
use ldk_node::bitcoin::secp256k1::PublicKey;
68
use ldk_node::lightning::ln::channelmanager::PaymentId;
79
use ldk_node::lightning_invoice::{Bolt11Invoice, Bolt11InvoiceDescription, Description, Sha256};
8-
use ldk_node::{Event, Node};
10+
use ldk_node::{Event, Node, NodeError, UserChannelId};
911
use log::{error, info, warn};
1012
use reqwest::{Client, Proxy};
1113
use tokio::runtime::Handle;
1214
use tokio::sync::broadcast;
1315
use tokio_util::sync::CancellationToken;
1416

15-
use crate::mdk::error::MdkError;
17+
use crate::mdk::error::{MdkError, SpliceError};
1618
use crate::mdk::mdk_api::client::MdkApiClient;
1719
use crate::mdk::mdk_api::types::{
1820
CheckoutCustomer, CreateCheckoutRequest, PaymentEntry, PaymentReceivedRequest,
@@ -32,6 +34,7 @@ pub type EventHandler = Arc<dyn Fn(MdkEvent) + Send + Sync>;
3234
pub struct MdkClient {
3335
node: Arc<Node>,
3436
api: Arc<MdkApiClient>,
37+
lsp_pubkey: PublicKey,
3538
event_tx: broadcast::Sender<MdkEvent>,
3639
event_handler: Option<EventHandler>,
3740
shutdown: CancellationToken,
@@ -69,6 +72,8 @@ impl MdkClient {
6972

7073
let api_base_url = config.infra.mdk_api_base_url.clone();
7174
let socks_proxy = config.socks_proxy.clone();
75+
let lsp_pubkey = PublicKey::from_str(&config.infra.lsp_node_id)
76+
.map_err(|e| MdkError::InvalidInput(format!("bad lsp_node_id: {e}")))?;
7277

7378
let node = build_node(config, handle.clone())?;
7479
let http_client = build_http_client(socks_proxy.as_deref())?;
@@ -82,6 +87,7 @@ impl MdkClient {
8287
Ok(Self {
8388
node,
8489
api,
90+
lsp_pubkey,
8591
event_tx,
8692
event_handler,
8793
shutdown: CancellationToken::new(),
@@ -115,6 +121,50 @@ impl MdkClient {
115121
Arc::clone(&self.node)
116122
}
117123

124+
pub fn lsp_pubkey(&self) -> PublicKey {
125+
self.lsp_pubkey
126+
}
127+
128+
/// Splice `amount_sats` of confirmed on-chain funds into the
129+
/// existing channel identified by `user_channel_id`, with the
130+
/// LSP as counterparty.
131+
///
132+
/// Validates locally that the channel exists and is usable
133+
/// before delegating to ldk-node. ldk-node's splice errors are
134+
/// mapped to typed `SpliceError` variants so callers (notably
135+
/// the splice manager) can pattern-match on the failure mode
136+
/// without inspecting strings.
137+
pub fn splice_in(
138+
&self,
139+
user_channel_id: UserChannelId,
140+
amount_sats: u64,
141+
) -> Result<(), MdkError> {
142+
if amount_sats == 0 {
143+
return Err(MdkError::InvalidInput(
144+
"splice amount must be greater than zero".into(),
145+
));
146+
}
147+
148+
let channels = self.node.list_channels();
149+
let channel = channels
150+
.iter()
151+
.find(|c| c.user_channel_id == user_channel_id)
152+
.ok_or_else(|| {
153+
MdkError::NotFound(format!(
154+
"channel with user_channel_id {}",
155+
user_channel_id.0
156+
))
157+
})?;
158+
159+
if !channel.is_usable {
160+
return Err(MdkError::Splice(SpliceError::ChannelNotUsable));
161+
}
162+
163+
self.node
164+
.splice_in(&user_channel_id, self.lsp_pubkey, amount_sats)
165+
.map_err(map_splice_error)
166+
}
167+
118168
pub fn subscribe(&self) -> broadcast::Receiver<MdkEvent> {
119169
self.event_tx.subscribe()
120170
}
@@ -403,3 +453,17 @@ fn format_payment_id(id: &Option<PaymentId>) -> String {
403453
None => "unknown".into(),
404454
}
405455
}
456+
457+
/// Map an ldk-node error returned from a splice call into a typed
458+
/// `MdkError::Splice`. Kept as a free helper (rather than a
459+
/// `From<NodeError>` impl) because `NodeError::InsufficientFunds`
460+
/// is also produced by `open_channel` and on-chain wallet paths,
461+
/// where mapping it to a splice variant would be wrong. Anything
462+
/// other than `InsufficientFunds` collapses to `Rejected` — the
463+
/// splice manager treats the catch-all bucket uniformly.
464+
fn map_splice_error(e: NodeError) -> MdkError {
465+
match e {
466+
NodeError::InsufficientFunds => MdkError::Splice(SpliceError::InsufficientFunds),
467+
_ => MdkError::Splice(SpliceError::Rejected),
468+
}
469+
}

src/mdk/error.rs

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,36 @@ pub enum MdkError {
1313
},
1414
Network(String),
1515
NotFound(String),
16+
Splice(SpliceError),
17+
}
18+
19+
/// Typed splice failure modes. Modeled as an ADT so the splice
20+
/// manager can decide what to do (skip vs. emit failure event)
21+
/// without inspecting log strings.
22+
#[derive(Debug, Clone, PartialEq, Eq)]
23+
pub enum SpliceError {
24+
/// The target channel exists but is not currently usable
25+
/// (mid-splice, peer disconnected, mid-monitor-update). The
26+
/// splice manager should skip this tick and try again.
27+
ChannelNotUsable,
28+
/// The on-chain wallet does not have enough confirmed funds
29+
/// for the requested splice amount. Retried next tick.
30+
InsufficientFunds,
31+
/// ldk-node refused the splice (coin selection failed under
32+
/// fee pressure, channel not yet ready, peer rejected, etc.).
33+
/// ldk-node currently collapses these into one error variant;
34+
/// we do too.
35+
Rejected,
36+
}
37+
38+
impl fmt::Display for SpliceError {
39+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
40+
match self {
41+
SpliceError::ChannelNotUsable => write!(f, "channel not usable"),
42+
SpliceError::InsufficientFunds => write!(f, "insufficient confirmed on-chain funds"),
43+
SpliceError::Rejected => write!(f, "splice rejected by ldk-node"),
44+
}
45+
}
1646
}
1747

1848
impl fmt::Display for MdkError {
@@ -27,6 +57,7 @@ impl fmt::Display for MdkError {
2757
} => write!(f, "platform API error ({status}): [{code}] {message}"),
2858
MdkError::Network(msg) => write!(f, "network error: {msg}"),
2959
MdkError::NotFound(msg) => write!(f, "not found: {msg}"),
60+
MdkError::Splice(e) => write!(f, "splice error: {e}"),
3061
}
3162
}
3263
}

0 commit comments

Comments
 (0)