Skip to content

Commit 82a3cb5

Browse files
authored
444 Add description hash in invoice (#16)
* add description hash in invoice * fix test: use dynamic peer ports and readiness check * skip pending check, proceed to claimable
1 parent c5bf702 commit 82a3cb5

26 files changed

Lines changed: 159 additions & 26 deletions

android-e2e/app/src/androidTest/java/org/rgblightningnode/ConcurrentBtcPaymentsTest.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -490,6 +490,7 @@ class ConcurrentBtcPaymentsTest {
490490
expirySec = 900u,
491491
assetId = null,
492492
assetAmount = null,
493+
descriptionHash = null,
493494
paymentHash = null,
494495
)
495496
).invoice
@@ -500,6 +501,7 @@ class ConcurrentBtcPaymentsTest {
500501
expirySec = 900u,
501502
assetId = null,
502503
assetAmount = null,
504+
descriptionHash = null,
503505
paymentHash = null,
504506
)
505507
).invoice

android-e2e/app/src/androidTest/java/org/rgblightningnode/PaymentTest.kt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -547,6 +547,7 @@ class PaymentTest {
547547
expirySec = 900u,
548548
assetId = assetId,
549549
assetAmount = 100u,
550+
descriptionHash = null,
550551
paymentHash = null,
551552
)
552553
).invoice
@@ -587,6 +588,7 @@ class PaymentTest {
587588
expirySec = 900u,
588589
assetId = assetId,
589590
assetAmount = 50u,
591+
descriptionHash = null,
590592
paymentHash = null,
591593
)
592594
).invoice
@@ -610,6 +612,7 @@ class PaymentTest {
610612
expirySec = 900u,
611613
assetId = assetId,
612614
assetAmount = 50u,
615+
descriptionHash = null,
613616
paymentHash = null,
614617
)
615618
).invoice
@@ -639,6 +642,7 @@ class PaymentTest {
639642
expirySec = 900u,
640643
assetId = assetId,
641644
assetAmount = 50u,
645+
descriptionHash = null,
642646
paymentHash = null,
643647
)
644648
).invoice

android-e2e/app/src/androidTest/java/org/rgblightningnode/RestartTest.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -486,6 +486,7 @@ class RestartTest {
486486
expirySec = 900u,
487487
assetId = assetId,
488488
assetAmount = 100u,
489+
descriptionHash = null,
489490
paymentHash = null,
490491
)
491492
).invoice

bindings/rgb_lightning_node.udl

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -489,6 +489,7 @@ dictionary LnInvoiceRequest {
489489
ContractId? asset_id;
490490
u64? asset_amount;
491491
PaymentHash? payment_hash;
492+
string? description_hash;
492493
};
493494

494495
dictionary LnInvoiceResponse {

openapi.yaml

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -710,7 +710,9 @@ paths:
710710
tags:
711711
- Invoices
712712
summary: Get a LN invoice
713-
description: Get a LN invoice to receive a payment. Provide `payment_hash` to create a HODL invoice.
713+
description: >-
714+
Get a LN invoice to receive a payment. Provide `payment_hash` to create a HODL invoice.
715+
Provide `description_hash` to include an out-of-band BOLT11 description hash.
714716
requestBody:
715717
content:
716718
application/json:
@@ -2313,6 +2315,10 @@ components:
23132315
type: string
23142316
description: Optional. When provided, the invoice is created as HODL.
23152317
example: 3febfae1e68b190c15461f4c2a3290f9af1dae63fd7d620d2bd61601869026cd
2318+
description_hash:
2319+
type: string
2320+
description: Optional. When provided, the invoice includes a BOLT11 description hash.
2321+
example: 5ca5d81b482b4015e7b14df7a27fe0a38c226273604ffd3b008b752571811938
23162322
LNInvoiceResponse:
23172323
type: object
23182324
required:

src/error.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,9 @@ pub enum APIError {
131131
#[error("Invalid channel ID")]
132132
InvalidChannelID,
133133

134+
#[error("Invalid description hash: {0}")]
135+
InvalidDescriptionHash(String),
136+
134137
#[error("Invalid details: {0}")]
135138
InvalidDetails(String),
136139

@@ -479,6 +482,7 @@ impl IntoResponse for APIError {
479482
| APIError::InvalidBackupPath
480483
| APIError::InvalidBiscuitToken
481484
| APIError::InvalidChannelID
485+
| APIError::InvalidDescriptionHash(_)
482486
| APIError::InvalidDetails(_)
483487
| APIError::InvalidEstimationBlocks
484488
| APIError::InvalidExpiration

src/routes.rs

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ use lightning::{
4040
util::config::{ChannelHandshakeConfig, ChannelHandshakeLimits, UserConfig},
4141
util::{errors::APIError as LDKAPIError, IS_SWAP_SCID},
4242
};
43-
use lightning_invoice::{Bolt11Invoice, PaymentSecret};
43+
use lightning_invoice::{Bolt11Invoice, Bolt11InvoiceDescription, Description, PaymentSecret};
4444
use regex::Regex;
4545
use rgb_lib::{
4646
bdk_wallet::keys::bip39::Mnemonic,
@@ -83,8 +83,9 @@ use crate::swap::{SwapData, SwapInfo, SwapString};
8383
use crate::utils::{
8484
check_already_initialized, check_channel_id, check_password_strength, check_password_validity,
8585
encrypt_and_save_mnemonic, get_max_local_rgb_amount, get_mnemonic_path, get_route, hex_str,
86-
hex_str_to_compressed_pubkey, hex_str_to_vec, validate_and_parse_payment_hash,
87-
validate_and_parse_payment_preimage, UnlockedAppState, UserOnionMessageContents,
86+
hex_str_to_compressed_pubkey, hex_str_to_vec, validate_and_parse_description_hash,
87+
validate_and_parse_payment_hash, validate_and_parse_payment_preimage, UnlockedAppState,
88+
UserOnionMessageContents,
8889
};
8990
use crate::{
9091
backup::{do_backup, restore_backup},
@@ -828,6 +829,7 @@ pub(crate) struct LNInvoiceRequest {
828829
pub(crate) asset_id: Option<String>,
829830
pub(crate) asset_amount: Option<u64>,
830831
pub(crate) payment_hash: Option<String>,
832+
pub(crate) description_hash: Option<String>,
831833
}
832834

833835
#[derive(Deserialize, Serialize)]
@@ -2950,9 +2952,16 @@ pub(crate) async fn ln_invoice(
29502952
}
29512953
None => None,
29522954
};
2955+
let description = match &payload.description_hash {
2956+
Some(description_hash) => Bolt11InvoiceDescription::Hash(
2957+
validate_and_parse_description_hash(description_hash)?,
2958+
),
2959+
None => Bolt11InvoiceDescription::Direct(Description::empty()),
2960+
};
29532961

29542962
let invoice_params = Bolt11InvoiceParameters {
29552963
amount_msats: payload.amt_msat,
2964+
description,
29562965
invoice_expiry_delta_secs: Some(payload.expiry_sec),
29572966
payment_hash: requested_payment_hash,
29582967
contract_id,

src/sdk/mod.rs

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,8 @@ use crate::utils::{
1111
check_already_initialized, check_channel_id, check_password_strength, check_password_validity,
1212
connect_peer_if_necessary, encrypt_and_save_mnemonic, get_current_timestamp,
1313
get_max_local_rgb_amount, get_mnemonic_path, get_route, hex_str, hex_str_to_vec,
14-
parse_peer_info, validate_and_parse_payment_hash, validate_and_parse_payment_preimage,
15-
AppState, UserOnionMessageContents,
14+
parse_peer_info, validate_and_parse_description_hash, validate_and_parse_payment_hash,
15+
validate_and_parse_payment_preimage, AppState, UserOnionMessageContents,
1616
};
1717
use amplify::{map, s};
1818
use bitcoin::hashes::sha256::Hash as Sha256;
@@ -49,7 +49,7 @@ use lightning::util::IS_SWAP_SCID;
4949
use lightning::{
5050
onion_message::messenger::Destination, onion_message::messenger::MessageSendInstructions,
5151
};
52-
use lightning_invoice::{Bolt11Invoice, PaymentSecret};
52+
use lightning_invoice::{Bolt11Invoice, Bolt11InvoiceDescription, Description, PaymentSecret};
5353
use regex::Regex;
5454
use rgb_lib::utils::recipient_id_from_script_buf;
5555
use rgb_lib::wallet::rust_only::check_indexer_url as rgb_lib_check_indexer_url;
@@ -3129,6 +3129,7 @@ pub(crate) async fn create_ln_invoice(
31293129
asset_id: Option<String>,
31303130
asset_amount: Option<u64>,
31313131
payment_hash: Option<String>,
3132+
description_hash: Option<String>,
31323133
) -> Result<LnInvoiceData, APIError> {
31333134
let guard = check_unlocked(&state).await?;
31343135
let unlocked_state = guard.as_ref().unwrap();
@@ -3160,9 +3161,16 @@ pub(crate) async fn create_ln_invoice(
31603161
}
31613162
None => None,
31623163
};
3164+
let description = match description_hash {
3165+
Some(description_hash) => {
3166+
Bolt11InvoiceDescription::Hash(validate_and_parse_description_hash(&description_hash)?)
3167+
}
3168+
None => Bolt11InvoiceDescription::Direct(Description::empty()),
3169+
};
31633170

31643171
let invoice_params = Bolt11InvoiceParameters {
31653172
amount_msats: amt_msat,
3173+
description,
31663174
invoice_expiry_delta_secs: Some(expiry_sec),
31673175
payment_hash: requested_payment_hash,
31683176
contract_id,

src/test/hodl_invoice.rs

Lines changed: 15 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -206,13 +206,15 @@ async fn run_expire_hodl_invoice_case(
206206

207207
async fn setup_two_nodes_with_asset_channel(
208208
test_dir_suffix: &str,
209-
port_offset: u16,
210209
) -> (SocketAddr, SocketAddr, String, String, String) {
211210
let test_dir_base = format!("{TEST_DIR_BASE}{test_dir_suffix}/");
212211
let test_dir_node1 = format!("{test_dir_base}node1");
213212
let test_dir_node2 = format!("{test_dir_base}node2");
214-
let node1_port = NODE1_PEER_PORT + port_offset;
215-
let node2_port = NODE2_PEER_PORT + port_offset;
213+
let node1_port = next_peer_port();
214+
let mut node2_port = next_peer_port();
215+
while node2_port == node1_port {
216+
node2_port = next_peer_port();
217+
}
216218
let (node1_addr, _) = start_node(&test_dir_node1, node1_port, false).await;
217219
let (node2_addr, _) = start_node(&test_dir_node2, node2_port, false).await;
218220

@@ -306,7 +308,7 @@ async fn autoclaim_and_expire_hodl_invoice_time_and_blocks() {
306308
initialize();
307309

308310
let (node1_addr, node2_addr, _test_dir_node1, test_dir_node2, _asset_id) =
309-
setup_two_nodes_with_asset_channel("autoclaim-expiry", 10).await;
311+
setup_two_nodes_with_asset_channel("autoclaim-expiry").await;
310312

311313
run_auto_claim_invoice_regression_case(node1_addr, node2_addr).await;
312314
run_expire_hodl_invoice_case(node1_addr, node2_addr, &test_dir_node2, ExpiryTrigger::Time)
@@ -318,6 +320,8 @@ async fn autoclaim_and_expire_hodl_invoice_time_and_blocks() {
318320
ExpiryTrigger::Blocks,
319321
)
320322
.await;
323+
324+
shutdown(&[node1_addr, node2_addr]).await;
321325
}
322326

323327
#[serial_test::serial]
@@ -328,7 +332,7 @@ async fn cancel_hodl_invoice_btc_rgb() {
328332

329333
let asset_payment_amount = 10;
330334
let (node1_addr, node2_addr, _test_dir_node1, test_dir_node2, asset_id) =
331-
setup_two_nodes_with_asset_channel("cancel-btc-rgb-rgb", 20).await;
335+
setup_two_nodes_with_asset_channel("cancel-btc-rgb-rgb").await;
332336
let initial_ln_rgb_balance_node1 = asset_balance_offchain_outbound(node1_addr, &asset_id).await;
333337
let initial_ln_rgb_balance_node2 = asset_balance_offchain_outbound(node2_addr, &asset_id).await;
334338

@@ -417,6 +421,8 @@ async fn cancel_hodl_invoice_btc_rgb() {
417421

418422
wait_for_ln_balance(node1_addr, &asset_id, initial_ln_rgb_balance_node1).await;
419423
wait_for_ln_balance(node2_addr, &asset_id, initial_ln_rgb_balance_node2).await;
424+
425+
shutdown(&[node1_addr, node2_addr]).await;
420426
}
421427

422428
#[serial_test::serial]
@@ -427,7 +433,7 @@ async fn claim_hodl_invoice_btc_rgb() {
427433

428434
let asset_payment_amount = 10;
429435
let (node1_addr, node2_addr, _test_dir_node1, test_dir_node2, asset_id) =
430-
setup_two_nodes_with_asset_channel("settle-btc-rgb", 30).await;
436+
setup_two_nodes_with_asset_channel("settle-btc-rgb").await;
431437

432438
let initial_ln_balance_node1 = asset_balance_offchain_outbound(node1_addr, &asset_id).await;
433439
let initial_ln_balance_node2 = asset_balance_offchain_outbound(node2_addr, &asset_id).await;
@@ -454,6 +460,7 @@ async fn claim_hodl_invoice_btc_rgb() {
454460
asset_id: None,
455461
asset_amount: None,
456462
payment_hash: Some(payment_hash.clone()),
463+
description_hash: None,
457464
};
458465
let duplicate_hash_res = reqwest::Client::new()
459466
.post(format!("http://{node2_addr}/lninvoice"))
@@ -470,10 +477,6 @@ async fn claim_hodl_invoice_btc_rgb() {
470477
.await;
471478

472479
let _ = send_payment_with_status(node1_addr, invoice.clone(), HTLCStatus::Pending).await;
473-
assert!(matches!(
474-
invoice_status(node2_addr, &invoice).await,
475-
InvoiceStatus::Pending
476-
));
477480
wait_for_claimable_state(&test_dir_node2, &payment_hash, true)
478481
.await
479482
.unwrap_or_else(|err| panic!("wait for claimable entry to appear: {err}"));
@@ -629,4 +632,6 @@ async fn claim_hodl_invoice_btc_rgb() {
629632
"InvoiceAlreadyClaimed",
630633
)
631634
.await;
635+
636+
shutdown(&[node1_addr, node2_addr]).await;
632637
}

src/test/invoice.rs

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
use super::*;
2+
use bitcoin::hashes::sha256::Hash as Sha256;
3+
use std::str::FromStr;
24

35
const TEST_DIR_BASE: &str = "tmp/invoice/";
46

@@ -22,6 +24,7 @@ async fn invoice() {
2224
asset_id: Some(asset_id.clone()),
2325
asset_amount: Some(1),
2426
payment_hash: None,
27+
description_hash: None,
2528
};
2629
let res = reqwest::Client::new()
2730
.post(format!("http://{node1_addr}/lninvoice"))
@@ -38,6 +41,7 @@ async fn invoice() {
3841
asset_id: Some(asset_id.clone()),
3942
asset_amount: Some(1),
4043
payment_hash: None,
44+
description_hash: None,
4145
};
4246
let res = reqwest::Client::new()
4347
.post(format!("http://{node1_addr}/lninvoice"))
@@ -54,6 +58,7 @@ async fn invoice() {
5458
asset_id: None,
5559
asset_amount: None,
5660
payment_hash: None,
61+
description_hash: None,
5762
};
5863
let res = reqwest::Client::new()
5964
.post(format!("http://{node1_addr}/lninvoice"))
@@ -66,6 +71,43 @@ async fn invoice() {
6671
assert!(res.is_ok());
6772
}
6873

74+
#[serial_test::serial]
75+
#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
76+
#[traced_test]
77+
async fn description_hash_invoice() {
78+
initialize();
79+
80+
let test_dir_node1 = format!("{TEST_DIR_BASE}description_hash/node1");
81+
let (node1_addr, _) = start_node(&test_dir_node1, NODE1_PEER_PORT, false).await;
82+
83+
fund_and_create_utxos(node1_addr, None).await;
84+
85+
let description_hash = lightning_invoice::Sha256(Sha256::hash(b"out-of-band description"));
86+
let payload = LNInvoiceRequest {
87+
amt_msat: None,
88+
expiry_sec: 900,
89+
asset_id: None,
90+
asset_amount: None,
91+
payment_hash: None,
92+
description_hash: Some(description_hash.0.to_string()),
93+
};
94+
let res = reqwest::Client::new()
95+
.post(format!("http://{node1_addr}/lninvoice"))
96+
.json(&payload)
97+
.send()
98+
.await
99+
.unwrap()
100+
.json::<LNInvoiceResponse>()
101+
.await
102+
.unwrap();
103+
104+
let invoice = Bolt11Invoice::from_str(&res.invoice).unwrap();
105+
assert!(matches!(
106+
invoice.description(),
107+
lightning_invoice::Bolt11InvoiceDescriptionRef::Hash(hash) if *hash == description_hash
108+
));
109+
}
110+
69111
#[serial_test::serial]
70112
#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
71113
#[traced_test]
@@ -103,6 +145,7 @@ async fn zero_amount_invoice() {
103145
asset_id: None,
104146
asset_amount: None,
105147
payment_hash: None,
148+
description_hash: None,
106149
};
107150
let res = reqwest::Client::new()
108151
.post(format!("http://{node2_addr}/lninvoice"))
@@ -184,6 +227,7 @@ async fn zero_amount_invoice() {
184227
asset_id: Some(asset_id.clone()),
185228
asset_amount: None,
186229
payment_hash: None,
230+
description_hash: None,
187231
};
188232
let invoice_without_amount = reqwest::Client::new()
189233
.post(format!("http://{node2_addr}/lninvoice"))
@@ -207,6 +251,7 @@ async fn zero_amount_invoice() {
207251
asset_id: Some(asset_id.clone()),
208252
asset_amount: Some(50),
209253
payment_hash: None,
254+
description_hash: None,
210255
};
211256
let invoice_with_amount = reqwest::Client::new()
212257
.post(format!("http://{node2_addr}/lninvoice"))

0 commit comments

Comments
 (0)