Skip to content

Commit c89e980

Browse files
cdeckerclaude
andcommitted
plugin: Add opening_fee_msat to LspInvoiceResponse
Include the LSP's JIT channel opening fee in the lsp_invoice response so clients can display the negotiated fees when presenting an invoice. The fee is computed from the selected LSP's LSPS2 fee parameters using max(min_fee_msat, ceil(amount * proportional / 1_000_000)). When the node has sufficient incoming capacity and no JIT channel is needed, the fee is reported as 0. Also surfaces the fee through gl-sdk's ReceiveResponse for UniFFI bindings consumers. Co-Authored-By: Claude Opus 4.6 <[email protected]>
1 parent 4b13a81 commit c89e980

File tree

10 files changed

+79
-27
lines changed

10 files changed

+79
-27
lines changed

libs/gl-client-py/glclient/greenlight.proto

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -187,8 +187,8 @@ message NodeConfig {
187187

188188

189189
// The `GlConfig` is used to pass greenlight-specific startup parameters
190-
// to the node. The `gl-plugin` will look for a serialized config object in
191-
// the node's datastore to load these values from. Please refer to the
190+
// to the node. The `gl-plugin` will look for a serialized config object in
191+
// the node's datastore to load these values from. Please refer to the
192192
// individual fields to learn what they do.
193193
message GlConfig {
194194
string close_to_addr = 1;
@@ -240,7 +240,7 @@ message LspInvoiceRequest {
240240
// Optional: for discounts/API keys
241241
string token = 2; // len=0 => None
242242
// Pass-through of cln invoice rpc params
243-
uint64 amount_msat = 3; // 0 => Any
243+
uint64 amount_msat = 3; // 0 => Any
244244
string description = 4;
245245
string label = 5;
246246
}
@@ -250,6 +250,10 @@ message LspInvoiceResponse {
250250
uint32 expires_at = 3;
251251
bytes payment_hash = 4;
252252
bytes payment_secret = 5;
253+
// The fee charged by the LSP for opening a JIT channel, in
254+
// millisatoshi. This is 0 if the node already had sufficient
255+
// incoming capacity and no JIT channel was needed.
256+
uint64 opening_fee_msat = 6;
253257
}
254258

255259
// Request for streaming node events. Currently empty but defined as

libs/gl-client-py/glclient/greenlight_pb2.py

Lines changed: 13 additions & 13 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

libs/gl-client-py/glclient/greenlight_pb2.pyi

Lines changed: 10 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

libs/gl-client-py/tests/test_plugin.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -236,3 +236,8 @@ def test_lsps_plugin_calls(clients, bitcoind, node_factory, lsps_server):
236236
assert len(inv.route_hints.route_hints) == 1
237237
rh = inv.route_hints.route_hints[0]
238238
assert rh.pubkey == bytes.fromhex(lsp_id)
239+
240+
# The response should include the LSP opening fee computed from
241+
# the LSPS2 fee parameters (min_fee_msat=1000, proportional=1000).
242+
# For 31337 msat: max(1000, ceil(31337 * 1000 / 1_000_000)) = 1000
243+
assert res.opening_fee_msat == 1000

libs/gl-client/.resources/proto/glclient/greenlight.proto

Lines changed: 7 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

libs/gl-plugin/.resources/proto/glclient/greenlight.proto

Lines changed: 7 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

libs/gl-plugin/src/node/mod.rs

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -247,6 +247,7 @@ impl Node for PluginNodeServer {
247247
expires_at: res.expires_at as u32,
248248
payment_hash: <cln_rpc::primitives::Sha256 as Borrow<[u8]>>::borrow(&res.payment_hash).to_vec(),
249249
payment_secret: res.payment_secret.to_vec(),
250+
opening_fee_msat: 0,
250251
}));
251252
}
252253

@@ -279,8 +280,18 @@ impl Node for PluginNodeServer {
279280
let lsp = &lsps[0];
280281
log::info!("Selecting {:?} for invoice negotiation", lsp);
281282

283+
// Compute the expected opening fee from the LSP's fee parameters.
284+
let opening_fee_msat = lsp.params.first().map_or(0, |p| {
285+
let min_fee: u64 = p.min_fee_msat.parse().unwrap_or(0);
286+
let proportional_fee = req
287+
.amount_msat
288+
.saturating_mul(p.proportional)
289+
.div_ceil(1_000_000);
290+
std::cmp::max(min_fee, proportional_fee)
291+
});
292+
282293
// Use the new RPC method name for versions > v25.05gl1
283-
let res = if *version > *"v25.05gl1" {
294+
let mut res = if *version > *"v25.05gl1" {
284295
let mut invreq: crate::requests::LspInvoiceRequestV2 = req.into();
285296
invreq.lsp_id = lsp.node_id.to_owned();
286297
rpc.call_typed(&invreq)
@@ -294,6 +305,7 @@ impl Node for PluginNodeServer {
294305
.map_err(|e| Status::new(Code::Internal, e.to_string()))?
295306
};
296307

308+
res.opening_fee_msat = opening_fee_msat;
297309
Ok(Response::new(res.into()))
298310
}
299311

0 commit comments

Comments
 (0)