Skip to content

Commit e5b9665

Browse files
committed
Add BOLT11 JIT hold-invoice RPCs.
Wrap ldk-node receive_via_jit_channel_for_hash and receive_variable_amount_via_jit_channel_for_hash as two new RPCs.
1 parent c31191a commit e5b9665

18 files changed

Lines changed: 711 additions & 82 deletions

File tree

‎docs/api-guide.md‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,8 @@ a channel just-in-time when the invoice is paid.
125125
|--------------------------------------------|-----------------------------------------------------------|
126126
| `Bolt11ReceiveViaJitChannel` | Create a fixed-amount invoice with JIT channel opening |
127127
| `Bolt11ReceiveVariableAmountViaJitChannel` | Create a variable-amount invoice with JIT channel opening |
128+
| `Bolt11ReceiveViaJitChannelForHash` | Create a fixed-amount invoice with JIT channel opening for a given payment hash (manual claim required) |
129+
| `Bolt11ReceiveVariableAmountViaJitChannelForHash` | Create a variable-amount invoice with JIT channel opening for a given payment hash (manual claim required) |
128130

129131
### BOLT12 Offers and Refunds
130132

@@ -262,7 +264,7 @@ Hodl invoices allow you to inspect and conditionally accept incoming payments:
262264

263265
1. **Subscribe:** Call `SubscribeEvents` before you create or share the invoice. Events are not
264266
replayed.
265-
2. **Create the invoice:** Generate a new payment hash. Call `Bolt11ReceiveForHash` with this hash.
267+
2. **Create the invoice:** Generate a new payment hash. Call `Bolt11ReceiveForHash`, `Bolt11ReceiveViaJitChannelForHash` or `Bolt11ReceiveVariableAmountViaJitChannelForHash` with this hash.
266268
Never reuse a payment hash. Reuse is unsafe and can cause loss of funds.
267269
3. **Handle each payment:** Save the payment ID from each `PaymentClaimable` event. A payer can pay
268270
the same invoice more than once. Each payment has a separate event and payment ID.

‎docs/configuration.md‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -157,8 +157,8 @@ to register several LSPs. Each LSP's supported protocols are discovered on start
157157
[bLIP-50 / LSPS0](https://github.com/lightning/blips/blob/master/blip-0050.md). LDK Server
158158
currently supports LSPS2 only, so an LSP that does not advertise it is registered but unused.
159159

160-
When at least one LSPS2-capable LSP is configured, the `Bolt11ReceiveViaJitChannel` and
161-
`Bolt11ReceiveVariableAmountViaJitChannel` RPCs become available, and the cheapest fee offer
160+
When at least one LSPS2-capable LSP is configured, the `Bolt11ReceiveViaJitChannel`,
161+
`Bolt11ReceiveVariableAmountViaJitChannel`, `Bolt11ReceiveViaJitChannelForHash` and `Bolt11ReceiveVariableAmountViaJitChannelForHash` RPCs become available, and the cheapest fee offer
162162
across all LSPS2-capable LSPs is selected per invoice.
163163

164164
Requires each LSP's public key and address. Some LSPs also require an authentication token.

‎e2e-tests/src/lib.rs‎

Lines changed: 24 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -139,8 +139,12 @@ pub enum ChainSource {
139139
rpc_password: String,
140140
rest_address: Option<String>,
141141
},
142-
Electrum { server_url: String },
143-
Esplora { server_url: String },
142+
Electrum {
143+
server_url: String,
144+
},
145+
Esplora {
146+
server_url: String,
147+
},
144148
}
145149

146150
impl ChainSource {
@@ -181,6 +185,7 @@ pub struct TestConfigBuilder {
181185
metrics_auth: Option<(String, String)>,
182186
log: Option<(Option<String>, String)>,
183187
tls_hosts: Option<Vec<String>>,
188+
lsps_clients: Vec<(String, String, bool)>,
184189
}
185190

186191
impl TestConfigBuilder {
@@ -202,6 +207,7 @@ impl TestConfigBuilder {
202207
metrics_auth: None,
203208
log: None,
204209
tls_hosts: None,
210+
lsps_clients: Vec::new(),
205211
}
206212
}
207213

@@ -247,6 +253,12 @@ impl TestConfigBuilder {
247253
self
248254
}
249255

256+
/// Add a `[[liquidity.lsps_client]]` section.
257+
pub fn lsps_client(mut self, node_pubkey: &str, address: &str, trust_peer_0conf: bool) -> Self {
258+
self.lsps_clients.push((node_pubkey.to_string(), address.to_string(), trust_peer_0conf));
259+
self
260+
}
261+
250262
/// Build the config into a TOML string.
251263
pub fn build(&self) -> String {
252264
fn toml_string_array(values: &[String]) -> String {
@@ -321,6 +333,15 @@ poll_metrics_interval = 1{metrics_auth}
321333
config.push_str(&format!("\n[tls]\nhosts = {}\n", toml_string_array(hosts)));
322334
}
323335

336+
for (node_pubkey, address, trust_peer_0conf) in &self.lsps_clients {
337+
config.push_str(&format!(
338+
"\n[[liquidity.lsps_client]]\n\
339+
node_pubkey = \"{node_pubkey}\"\n\
340+
address = \"{address}\"\n\
341+
trust_peer_0conf = {trust_peer_0conf}\n"
342+
));
343+
}
344+
324345
config
325346
}
326347
}
@@ -783,9 +804,7 @@ pub async fn setup_funded_channel(
783804
.open_channel(OpenChannelRequest {
784805
node_pubkey: server_b.node_id().to_string(),
785806
address: format!("127.0.0.1:{}", server_b.p2p_port),
786-
amount: Some(open_channel_request::Amount::ChannelAmountSats(
787-
channel_amount_sats,
788-
)),
807+
amount: Some(open_channel_request::Amount::ChannelAmountSats(channel_amount_sats)),
789808
push_to_counterparty_msat: None,
790809
channel_config: None,
791810
announce_channel: true,

‎e2e-tests/tests/e2e.rs‎

Lines changed: 191 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ use std::time::Duration;
1414
use e2e_tests::{
1515
find_available_port, mine_and_sync, run_cli, run_cli_raw, run_cli_with_config,
1616
setup_funded_channel, wait_for_event, wait_for_onchain_balance, wait_for_usable_channel,
17-
LdkServerConfig, LdkServerHandle, TestBitcoind,
17+
LdkServerConfig, LdkServerHandle, TestBitcoind, TestConfigBuilder,
1818
};
1919
use hex_conservative::{DisplayHex, FromHex};
2020
use ldk_node::bitcoin::hashes::{sha256, Hash};
@@ -439,13 +439,7 @@ async fn open_channel_via_cli(channel_amount: &str) {
439439
let addr = format!("127.0.0.1:{}", server_b.p2p_port);
440440
let output = run_cli(
441441
&server_a,
442-
&[
443-
"open-channel",
444-
server_b.node_id(),
445-
&addr,
446-
channel_amount,
447-
"--announce-channel",
448-
],
442+
&["open-channel", server_b.node_id(), &addr, channel_amount, "--announce-channel"],
449443
);
450444
assert!(!output["user_channel_id"].as_str().unwrap().is_empty());
451445
}
@@ -482,9 +476,7 @@ async fn test_subscribe_events_channel_state_lifecycle_pending_ready_closed() {
482476
.open_channel(OpenChannelRequest {
483477
node_pubkey: server_b.node_id().to_string(),
484478
address: format!("127.0.0.1:{}", server_b.p2p_port),
485-
amount: Some(open_channel_request::Amount::ChannelAmountSats(
486-
100_000,
487-
)),
479+
amount: Some(open_channel_request::Amount::ChannelAmountSats(100_000)),
488480
push_to_counterparty_msat: None,
489481
channel_config: None,
490482
announce_channel: true,
@@ -512,7 +504,10 @@ async fn test_subscribe_events_channel_state_lifecycle_pending_ready_closed() {
512504
assert!(pending_a.reason.is_none());
513505
assert_eq!(pending_a.closure_initiator, ChannelClosureInitiator::Unspecified as i32);
514506
assert!(pending_a.former_temporary_channel_id.as_deref().is_some_and(|id| !id.is_empty()));
515-
assert_ne!(pending_a.former_temporary_channel_id.as_deref(), Some(pending_a.channel_id.as_str()));
507+
assert_ne!(
508+
pending_a.former_temporary_channel_id.as_deref(),
509+
Some(pending_a.channel_id.as_str())
510+
);
516511

517512
let pending_b = wait_for_event(&mut events_b, |e| {
518513
matches!(
@@ -650,9 +645,7 @@ async fn test_subscribe_events_channel_state_lifecycle_pending_ready_force_close
650645
.open_channel(OpenChannelRequest {
651646
node_pubkey: server_b.node_id().to_string(),
652647
address: format!("127.0.0.1:{}", server_b.p2p_port),
653-
amount: Some(open_channel_request::Amount::ChannelAmountSats(
654-
100_000,
655-
)),
648+
amount: Some(open_channel_request::Amount::ChannelAmountSats(100_000)),
656649
push_to_counterparty_msat: None,
657650
channel_config: None,
658651
announce_channel: true,
@@ -680,7 +673,10 @@ async fn test_subscribe_events_channel_state_lifecycle_pending_ready_force_close
680673
assert!(pending_a.reason.is_none());
681674
assert_eq!(pending_a.closure_initiator, ChannelClosureInitiator::Unspecified as i32);
682675
assert!(pending_a.former_temporary_channel_id.as_deref().is_some_and(|id| !id.is_empty()));
683-
assert_ne!(pending_a.former_temporary_channel_id.as_deref(), Some(pending_a.channel_id.as_str()));
676+
assert_ne!(
677+
pending_a.former_temporary_channel_id.as_deref(),
678+
Some(pending_a.channel_id.as_str())
679+
);
684680

685681
let pending_b = wait_for_event(&mut events_b, |e| {
686682
matches!(
@@ -1273,14 +1269,11 @@ async fn splice_in_via_cli(splice_amount: &str) {
12731269

12741270
let mut events_a = server_a.client().subscribe_events().await.unwrap();
12751271

1276-
let output = run_cli(
1277-
&server_a,
1278-
&["splice-in", &user_channel_id, server_b.node_id(), splice_amount],
1279-
);
1272+
let output =
1273+
run_cli(&server_a, &["splice-in", &user_channel_id, server_b.node_id(), splice_amount]);
12801274
assert!(output.is_object());
12811275

1282-
let event_a =
1283-
wait_for_event(&mut events_a, |e| matches!(e, Event::SpliceNegotiated(_))).await;
1276+
let event_a = wait_for_event(&mut events_a, |e| matches!(e, Event::SpliceNegotiated(_))).await;
12841277
match &event_a.event {
12851278
Some(Event::SpliceNegotiated(splice_negotiated)) => {
12861279
assert_eq!(splice_negotiated.user_channel_id, user_channel_id);
@@ -1663,10 +1656,183 @@ async fn test_hodl_invoice_fail() {
16631656
panic!("expected PaymentFailed");
16641657
};
16651658
assert!(!failed.payment.as_ref().unwrap().payment_id.is_empty());
1666-
assert_eq!(
1667-
failed.reason,
1668-
Some(PaymentFailureReason::RecipientRejected as i32)
1659+
assert_eq!(failed.reason, Some(PaymentFailureReason::RecipientRejected as i32));
1660+
}
1661+
1662+
#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
1663+
async fn test_jit_hodl_invoice_claim() {
1664+
let bitcoind = TestBitcoind::new();
1665+
1666+
// A: normal payer node
1667+
let server_a = LdkServerHandle::start(&bitcoind).await;
1668+
1669+
// Subscribe to events on A before any payments
1670+
let mut events_a = server_a.client().subscribe_events().await.unwrap();
1671+
1672+
// B: LSP node (all e2e servers include LSPS2 service config)
1673+
let server_b = LdkServerHandle::start(&bitcoind).await;
1674+
1675+
// Open channel A -> B (1M sats, larger for JIT forwarding)
1676+
setup_funded_channel(&bitcoind, &server_a, &server_b, 1_000_000).await;
1677+
1678+
// Fund B additionally so it can open JIT channel to C
1679+
let addr_b = server_b.client().onchain_receive(OnchainReceiveRequest {}).await.unwrap().address;
1680+
bitcoind.fund_address(&addr_b, 1.0);
1681+
mine_and_sync(&bitcoind, &[&server_a, &server_b], 6).await;
1682+
1683+
// C: JIT client
1684+
let lsp_pubkey = server_b.node_id().to_string();
1685+
let lsp_addr = format!("127.0.0.1:{}", server_b.p2p_port);
1686+
let server_c = LdkServerHandle::start_with_config(&bitcoind, |params| {
1687+
TestConfigBuilder::new(params).lsps_client(&lsp_pubkey, &lsp_addr, true).build()
1688+
})
1689+
.await;
1690+
let mut events_c = server_c.client().subscribe_events().await.unwrap();
1691+
1692+
let preimage_bytes_1 = [43u8; 32];
1693+
let preimage_hex_1 = preimage_bytes_1.to_lower_hex_string();
1694+
let payment_hash_1 = sha256::Hash::hash(&preimage_bytes_1);
1695+
let payment_hash_hex_1 = payment_hash_1.to_byte_array().to_lower_hex_string();
1696+
1697+
let preimage_bytes_2 = [44u8; 32];
1698+
let payment_hash_2 = sha256::Hash::hash(&preimage_bytes_2);
1699+
let payment_hash_hex_2 = payment_hash_2.to_byte_array().to_lower_hex_string();
1700+
1701+
// Create fixed amount hodl invoice on c
1702+
let invoice_resp_1 = run_cli(
1703+
&server_c,
1704+
&[
1705+
"bolt11-receive-via-jit-channel-for-hash",
1706+
&payment_hash_hex_1,
1707+
"100000000msat",
1708+
"-d",
1709+
"jit hodl test",
1710+
"-e",
1711+
"3600",
1712+
],
16691713
);
1714+
let invoice_1 = invoice_resp_1["invoice"].as_str().unwrap();
1715+
assert!(!invoice_1.is_empty());
1716+
1717+
// Create variable amount hodl invoice on c
1718+
let invoice_resp_2 = run_cli(
1719+
&server_c,
1720+
&[
1721+
"bolt11-receive-variable-amount-via-jit-channel-for-hash",
1722+
&payment_hash_hex_2,
1723+
"-d",
1724+
"jit variable hodl test",
1725+
"-e",
1726+
"3600",
1727+
],
1728+
);
1729+
let invoice_2 = invoice_resp_2["invoice"].as_str().unwrap();
1730+
assert!(!invoice_2.is_empty());
1731+
1732+
// Pay the hodl invoice from A
1733+
run_cli(&server_a, &["bolt11-send", invoice_1]);
1734+
1735+
// Wait for PaymentClaimable event on C (drain other events)
1736+
let claimable =
1737+
wait_for_event(&mut events_c, |e| matches!(e, Event::PaymentClaimable(_))).await;
1738+
let Some(Event::PaymentClaimable(claimable_event)) = &claimable.event else {
1739+
panic!("expected PaymentClaimable");
1740+
};
1741+
assert!(claimable_event.claim_deadline.is_some());
1742+
assert!(!claimable_event.payment_id.is_empty());
1743+
1744+
// Claim the payment on C
1745+
let claimable_amount = format!("{}msat", claimable_event.claimable_amount_msat);
1746+
let args: Vec<&str> = vec![
1747+
"bolt11-claim-for-id",
1748+
&claimable_event.payment_id,
1749+
&preimage_hex_1,
1750+
"-c",
1751+
&claimable_amount,
1752+
];
1753+
1754+
run_cli(&server_c, &args);
1755+
1756+
// Wait for PaymentSuccessful on A after claim (drain other events)
1757+
let successful =
1758+
wait_for_event(&mut events_a, |e| matches!(e, Event::PaymentSuccessful(_))).await;
1759+
let Some(Event::PaymentSuccessful(event)) = &successful.event else {
1760+
panic!("expected PaymentSuccessful");
1761+
};
1762+
assert!(!event.payment.as_ref().unwrap().payment_id.is_empty());
1763+
}
1764+
1765+
#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
1766+
async fn test_jit_hodl_invoice_fail() {
1767+
let bitcoind = TestBitcoind::new();
1768+
1769+
// A: normal payer node
1770+
let server_a = LdkServerHandle::start(&bitcoind).await;
1771+
1772+
// Subscribe to events on A before any payments
1773+
let mut events_a = server_a.client().subscribe_events().await.unwrap();
1774+
1775+
// B: LSP node (all e2e servers include LSPS2 service config)
1776+
let server_b = LdkServerHandle::start(&bitcoind).await;
1777+
1778+
// Open channel A -> B (1M sats, larger for JIT forwarding)
1779+
setup_funded_channel(&bitcoind, &server_a, &server_b, 1_000_000).await;
1780+
1781+
// Fund B additionally so it can open JIT channel to C
1782+
let addr_b = server_b.client().onchain_receive(OnchainReceiveRequest {}).await.unwrap().address;
1783+
bitcoind.fund_address(&addr_b, 1.0);
1784+
mine_and_sync(&bitcoind, &[&server_a, &server_b], 6).await;
1785+
1786+
// C: JIT client
1787+
let lsp_pubkey = server_b.node_id().to_string();
1788+
let lsp_addr = format!("127.0.0.1:{}", server_b.p2p_port);
1789+
let server_c = LdkServerHandle::start_with_config(&bitcoind, |params| {
1790+
TestConfigBuilder::new(params).lsps_client(&lsp_pubkey, &lsp_addr, true).build()
1791+
})
1792+
.await;
1793+
let mut events_c = server_c.client().subscribe_events().await.unwrap();
1794+
1795+
let preimage_bytes = [43u8; 32];
1796+
let payment_hash = sha256::Hash::hash(&preimage_bytes);
1797+
let payment_hash_hex = payment_hash.to_byte_array().to_lower_hex_string();
1798+
1799+
// Create fixed amount hodl invoice on c
1800+
let invoice_resp = run_cli(
1801+
&server_c,
1802+
&[
1803+
"bolt11-receive-via-jit-channel-for-hash",
1804+
&payment_hash_hex,
1805+
"100000000msat",
1806+
"-d",
1807+
"jit hodl test",
1808+
"-e",
1809+
"3600",
1810+
],
1811+
);
1812+
let invoice = invoice_resp["invoice"].as_str().unwrap();
1813+
assert!(!invoice.is_empty());
1814+
1815+
// Pay the hodl invoice from A
1816+
run_cli(&server_a, &["bolt11-send", invoice]);
1817+
1818+
// Wait for PaymentClaimable event on C (drain other events)
1819+
let claimable =
1820+
wait_for_event(&mut events_c, |e| matches!(e, Event::PaymentClaimable(_))).await;
1821+
let Some(Event::PaymentClaimable(claimable_event)) = &claimable.event else {
1822+
panic!("expected PaymentClaimable");
1823+
};
1824+
assert!(claimable_event.claim_deadline.is_some());
1825+
assert!(!claimable_event.payment_id.is_empty());
1826+
1827+
run_cli(&server_c, &["bolt11-fail-for-id", &claimable_event.payment_id]);
1828+
1829+
// Verify PaymentFailed on A and its failure reason.
1830+
let event_a = wait_for_event(&mut events_a, |e| matches!(e, Event::PaymentFailed(_))).await;
1831+
let Some(Event::PaymentFailed(failed)) = &event_a.event else {
1832+
panic!("expected PaymentFailed");
1833+
};
1834+
assert!(!failed.payment.as_ref().unwrap().payment_id.is_empty());
1835+
assert_eq!(failed.reason, Some(PaymentFailureReason::RecipientRejected as i32));
16701836
}
16711837

16721838
#[tokio::test]

0 commit comments

Comments
 (0)