Skip to content

Commit 1d36b19

Browse files
committed
Merge branch 'bolt12-4'
2 parents cfed1ff + f341401 commit 1d36b19

4 files changed

Lines changed: 177 additions & 77 deletions

File tree

lwk_boltz/src/lib.rs

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -880,6 +880,24 @@ pub fn parse_bolt12_invoice(bolt12_invoice: &str) -> Result<Bolt12Invoice, Error
880880
Ok(lightning::offers::invoice::Bolt12Invoice::try_from(data)?)
881881
}
882882

883+
pub fn display_bolt12_invoice(bolt12_invoice: &Bolt12Invoice) -> String {
884+
use bech32::ToBase32;
885+
use lightning::util::ser::Writeable;
886+
887+
// Serialize the invoice to bytes using the Writeable trait
888+
let mut bytes = Vec::new();
889+
bolt12_invoice
890+
.write(&mut bytes)
891+
.expect("writing to vec should not fail");
892+
893+
// Convert bytes to base32
894+
let data = bytes.to_base32();
895+
896+
// Encode with bech32 using the invoice HRP without checksum (matching decode_without_checksum)
897+
bech32::encode_without_checksum(BECH32_BOLT12_INVOICE_HRP, data)
898+
.expect("encoding valid invoice should succeed")
899+
}
900+
883901
#[cfg(test)]
884902
mod tests {
885903
use std::str::FromStr;
@@ -947,4 +965,19 @@ mod tests {
947965
let data: Vec<SwapRestoreResponse> = serde_json::from_str(data).unwrap();
948966
assert_eq!(data.len(), 32);
949967
}
968+
969+
#[test]
970+
fn test_bolt12_invoice_roundtrip() {
971+
// Test data - a real BOLT12 invoice string from submarine.rs tests
972+
let invoice_str = "lni1qqgwwn892vxqk9fsgul2fgzxyj5wk93pqtqft5rf2w8ed0c5chus7mqg2x7lx49qajrq8x3yhuu2w0msttwzc5srqxr2q4qqtqss80rn9yedw8hsef9w2lwa83zsfxglnhaen4kl272wrv4uccukswxm5zvq9sy46p548rukhu2vt7g0dsy9r00n2jswepsrngjt7w988ac94hpvqws6qvd2q863an980srs7dpnt6qpqzlxrdkds6l8zz33enxmr42ujqgzfyq6zkdznkzf5m4u7ran24078mtlcdnaltufm4znls5gkq9lyhvqqvhwq0uy4rzc77s7d8gfx4hxemjql7gfcd7l97c3m76vtqnqmkg3eafm2msn4jj864haz42dc6r8r47gt64zrsqqqqqqqqqqqqqqzgqqqqqqqqqqqqqayjedltzjqqqqqq9yq35mrksp4qst37he8z5zvgq948434andxfzlfru53mfvvaycmed6ynt67qyg3xa2qvqcdg9wqvpqqq9syypvp9wsd9fcl94lznzljrmvppgmmu655rkgvqu6yjln3felwpddct8sgrt30e0uynvhy5ydaktehuwctyzkd05wgw4zqn0ayx4d9yndcfhd4ygpjceygz9629n4qm0zn7xa5k8e8xaphu280n4v2y3dzc2etywv";
973+
974+
// Parse the invoice string
975+
let parsed = crate::parse_bolt12_invoice(invoice_str).unwrap();
976+
977+
// Convert back to string
978+
let displayed = crate::display_bolt12_invoice(&parsed);
979+
980+
// Verify roundtrip - should get back the same string
981+
assert_eq!(invoice_str, displayed);
982+
}
950983
}

lwk_boltz/src/prepare_pay_data.rs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ use bip39::Mnemonic;
44
use boltz_client::boltz::CreateSubmarineResponse;
55
use boltz_client::{Bolt11Invoice, Keypair};
66
use lightning::bitcoin::XKeyIdentifier;
7+
use lightning::offers::invoice::Bolt12Invoice;
78
use serde::{Deserialize, Serialize};
89

910
use crate::error::Error;
@@ -22,6 +23,8 @@ pub struct PreparePayData {
2223
pub fee: Option<u64>,
2324
pub boltz_fee: Option<u64>,
2425
pub bolt11_invoice: Option<Bolt11Invoice>,
26+
pub bolt12_invoice: Option<Bolt12Invoice>,
27+
2528
pub create_swap_response: CreateSubmarineResponse,
2629
pub our_keys: Keypair,
2730
pub refund_address: String,
@@ -38,6 +41,7 @@ pub struct PreparePayDataSerializable {
3841
pub fee: Option<u64>,
3942
pub boltz_fee: Option<u64>,
4043
pub bolt11_invoice: Option<String>,
44+
pub bolt12_invoice: Option<String>,
4145
pub create_swap_response: CreateSubmarineResponse,
4246
pub key_index: u32,
4347
pub refund_address: String,
@@ -55,6 +59,9 @@ impl From<PreparePayData> for PreparePayDataSerializable {
5559
refund_txid: data.refund_txid,
5660
fee: data.fee,
5761
bolt11_invoice: data.bolt11_invoice.map(|i| i.to_string()),
62+
bolt12_invoice: data
63+
.bolt12_invoice
64+
.map(|i| crate::display_bolt12_invoice(&i)),
5865
create_swap_response: data.create_swap_response,
5966
key_index: data.key_index,
6067
refund_address: data.refund_address,
@@ -81,13 +88,19 @@ pub fn to_prepare_pay_data(
8188
.as_ref()
8289
.map(|i| Bolt11Invoice::from_str(i))
8390
.transpose()?;
91+
let bolt12_invoice = data
92+
.bolt12_invoice
93+
.as_ref()
94+
.map(|i| crate::parse_bolt12_invoice(i))
95+
.transpose()?;
8496
Ok(PreparePayData {
8597
last_state: data.last_state,
8698
swap_type: data.swap_type,
8799
lockup_txid: data.lockup_txid,
88100
refund_txid: data.refund_txid,
89101
fee: data.fee,
90102
bolt11_invoice,
103+
bolt12_invoice,
91104
create_swap_response: data.create_swap_response,
92105
our_keys,
93106
refund_address: data.refund_address,

lwk_boltz/src/submarine.rs

Lines changed: 57 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -21,12 +21,12 @@ use lwk_wollet::elements;
2121
use crate::error::Error;
2222
use crate::prepare_pay_data::{to_prepare_pay_data, PreparePayData, PreparePayDataSerializable};
2323
use crate::swap_state::SwapStateTrait;
24-
use crate::DynStore;
2524
use crate::SwapPersistence;
2625
use crate::{
2726
broadcast_tx_with_retry, mnemonic_identifier, next_status, BoltzSession, LightningPayment,
2827
SwapState, SwapType,
2928
};
29+
use crate::{display_bolt12_invoice, DynStore};
3030

3131
pub struct PreparePayResponse {
3232
pub data: PreparePayData,
@@ -75,14 +75,27 @@ impl BoltzSession {
7575
) -> Result<PreparePayResponse, Error> {
7676
let chain = self.chain();
7777

78-
let (bolt11_invoice_str, bolt11_invoice) = match lightning_payment {
79-
LightningPayment::Bolt11(invoice) => (invoice.to_string(), invoice),
78+
let (invoice_str, bolt11_invoice, bolt12_invoice) = match lightning_payment {
79+
LightningPayment::Bolt11(invoice) => {
80+
(invoice.to_string(), Some(invoice.as_ref().clone()), None)
81+
}
8082
LightningPayment::Bolt12 {
81-
offer: _,
82-
invoice_amount: _,
83+
offer,
84+
invoice_amount,
8385
} => {
84-
// TODO check if the amount is in the offer or in the amount, if there is in both or in neither error (add an error variant)
85-
return Err(Error::Bolt12Unsupported);
86+
match invoice_amount {
87+
Some(invoice_amount) => {
88+
log::info!("Preparing to pay {invoice_amount}");
89+
let bolt12_invoice =
90+
self.fetch_bolt12_invoice(offer, *invoice_amount).await?;
91+
(
92+
display_bolt12_invoice(&bolt12_invoice),
93+
None,
94+
Some(bolt12_invoice),
95+
)
96+
}
97+
None => return Err(Error::Generic("Amount is required".to_string())), // TODO use appropriate variant
98+
}
8699
}
87100
LightningPayment::LnUrl(_) => {
88101
return Err(Error::LnUrlUnsupported);
@@ -96,26 +109,29 @@ impl BoltzSession {
96109
compressed: true,
97110
};
98111

99-
if let Some((address, amount)) =
100-
check_for_mrh(&self.api, &bolt11_invoice_str, chain).await?
101-
{
102-
let asset_id = self.network().policy_asset().to_string();
103-
let mrh_uri = format!(
104-
"liquidnetwork:{address}?amount={}&assetid={}",
105-
amount.to_string_in(Denomination::Bitcoin),
106-
asset_id
107-
);
108-
return Err(Error::MagicRoutingHint {
109-
address: address.to_string(),
110-
amount: amount.to_sat(),
111-
uri: mrh_uri,
112-
});
112+
if bolt11_invoice.is_some() {
113+
// mrh works only with bolt11
114+
115+
if let Some((address, amount)) = check_for_mrh(&self.api, &invoice_str, chain).await? {
116+
let asset_id = self.network().policy_asset().to_string();
117+
let mrh_uri = format!(
118+
"liquidnetwork:{address}?amount={}&assetid={}",
119+
amount.to_string_in(Denomination::Bitcoin),
120+
asset_id
121+
);
122+
return Err(Error::MagicRoutingHint {
123+
address: address.to_string(),
124+
amount: amount.to_sat(),
125+
uri: mrh_uri,
126+
});
127+
}
113128
}
129+
log::error!("no mrh");
114130

115131
let create_swap_req = CreateSubmarineRequest {
116132
from: chain.to_string(),
117133
to: "BTC".to_string(),
118-
invoice: bolt11_invoice_str.clone(),
134+
invoice: invoice_str.clone(),
119135
refund_public_key,
120136
pair_hash: None,
121137
referral_id: self.referral_id.clone(),
@@ -127,16 +143,22 @@ impl BoltzSession {
127143
"accept zero conf: {}",
128144
create_swap_response.accept_zero_conf
129145
);
130-
let bolt11_amount = bolt11_invoice
131-
.amount_milli_satoshis()
132-
.ok_or(Error::InvoiceWithoutAmount(bolt11_invoice_str.clone()))?
133-
/ 1000;
146+
let bolt11_amount = match (bolt11_invoice.as_ref(), bolt12_invoice.as_ref()) {
147+
(Some(bolt11_invoice), None) => {
148+
bolt11_invoice
149+
.amount_milli_satoshis()
150+
.ok_or(Error::InvoiceWithoutAmount(invoice_str.clone()))?
151+
/ 1000
152+
}
153+
(None, Some(bolt12_invoice)) => bolt12_invoice.amount_msats() / 1000,
154+
_ => unreachable!(),
155+
};
134156
let fee = create_swap_response
135157
.expected_amount
136158
.checked_sub(bolt11_amount)
137159
.ok_or(Error::ExpectedAmountLowerThanInvoice(
138160
create_swap_response.expected_amount,
139-
bolt11_invoice_str.clone(),
161+
invoice_str.clone(),
140162
))?;
141163

142164
let boltz_fee = self
@@ -149,8 +171,11 @@ impl BoltzSession {
149171

150172
log::info!("Got Swap Response from Boltz server {create_swap_response:?}");
151173

152-
create_swap_response.validate(&bolt11_invoice_str, &refund_public_key, chain)?;
153-
log::info!("VALIDATED RESPONSE!");
174+
if bolt11_invoice.is_some() {
175+
// TODO: boltz-rust dep doesn't support bolt12 yet
176+
create_swap_response.validate(&invoice_str, &refund_public_key, chain)?;
177+
log::info!("VALIDATED RESPONSE!");
178+
}
154179

155180
let swap_script =
156181
SwapScript::submarine_from_swap_resp(chain, &create_swap_response, refund_public_key)?;
@@ -188,7 +213,8 @@ impl BoltzSession {
188213
refund_txid: None,
189214
fee: Some(fee),
190215
boltz_fee,
191-
bolt11_invoice: Some((**bolt11_invoice).clone()),
216+
bolt11_invoice,
217+
bolt12_invoice,
192218
our_keys,
193219
refund_address: refund_address.to_string(),
194220
create_swap_response: create_swap_response.clone(),
@@ -361,6 +387,7 @@ pub(crate) fn convert_swap_restore_response_to_prepare_pay_data(
361387
refund_txid: None,
362388
fee: None, // Fee information not available in restore response
363389
bolt11_invoice: None, // Invoice information not available in restore response
390+
bolt12_invoice: None, // Invoice information not available in restore response
364391
our_keys,
365392
refund_address: refund_address.to_string(),
366393
create_swap_response,

lwk_boltz/tests/submarine.rs

Lines changed: 74 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,80 @@ mod tests {
5454

5555
#[tokio::test]
5656
#[ignore = "requires regtest environment"]
57-
async fn test_bolt12_offer_to_bolt11_and_pay_with_session() {
57+
async fn test_bolt12_pay_with_session() {
58+
let _ = env_logger::try_init();
59+
60+
// Start concurrent block mining task
61+
let mining_handle = utils::start_block_mining();
62+
63+
// Ask CLN for a BOLT12 offer
64+
let offer_str = utils::cln_offer_any().expect("cln_offer_any should succeed");
65+
66+
let mut payment: LightningPayment = offer_str.parse().unwrap();
67+
assert!(payment.bolt12().is_some());
68+
assert!(payment.bolt12_invoice_amount().unwrap().is_none());
69+
70+
// create a BoltzSession
71+
let client = Arc::new(
72+
ElectrumClient::new(
73+
DEFAULT_REGTEST_NODE,
74+
false,
75+
false,
76+
ElementsNetwork::default_regtest(),
77+
)
78+
.unwrap(),
79+
);
80+
let session = BoltzSession::builder(
81+
ElementsNetwork::default_regtest(),
82+
AnyClient::Electrum(client.clone()),
83+
)
84+
.build()
85+
.await
86+
.unwrap();
87+
88+
// Try to pay the bolt12
89+
let refund_address = utils::generate_address(Chain::Liquid(LiquidChain::LiquidRegtest))
90+
.await
91+
.unwrap();
92+
let refund_address = elements::Address::from_str(&refund_address).unwrap();
93+
let prepare_pay_err = session
94+
.prepare_pay(&payment, &refund_address, None)
95+
.await
96+
.unwrap_err()
97+
.to_string();
98+
assert!(prepare_pay_err.contains("Amount is required"));
99+
100+
let millisat_amount = 10_000_000;
101+
payment.set_bolt12_invoice_amount(millisat_amount).unwrap();
102+
assert_eq!(
103+
payment.bolt12_invoice_amount().unwrap().unwrap(),
104+
millisat_amount
105+
);
106+
107+
let prepare_pay = session
108+
.prepare_pay(&payment, &refund_address, None)
109+
.await
110+
.unwrap();
111+
112+
// Send funds to the swap address
113+
utils::send_to_address(
114+
Chain::Liquid(LiquidChain::LiquidRegtest),
115+
&prepare_pay.data.create_swap_response.address,
116+
prepare_pay.data.create_swap_response.expected_amount,
117+
)
118+
.await
119+
.unwrap();
120+
121+
// Complete the payment
122+
prepare_pay.complete_pay().await.unwrap();
123+
124+
// Stop the mining task
125+
mining_handle.abort();
126+
}
127+
128+
#[tokio::test]
129+
#[ignore = "requires regtest environment"]
130+
async fn test_bolt12_offer_to_invoice_and_pay_with_session() {
58131
let _ = env_logger::try_init();
59132

60133
// Ask CLN for a BOLT12 offer
@@ -90,52 +163,6 @@ mod tests {
90163
assert!(!verify_invoice_from_offer(&another_bolt12_invoice, &offer));
91164
}
92165

93-
#[tokio::test]
94-
#[ignore = "requires regtest environment"]
95-
async fn test_bolt12_offer_returns_unsupported_error() {
96-
let _ = env_logger::try_init();
97-
98-
// Get a BOLT12 offer from CLN
99-
let offer_str = utils::cln_offer_any().expect("cln_offer_any should succeed");
100-
let lightning_payment = LightningPayment::from_str(&offer_str).unwrap();
101-
102-
// Set up BoltzSession
103-
let refund_address = utils::generate_address(Chain::Liquid(LiquidChain::LiquidRegtest))
104-
.await
105-
.unwrap();
106-
let refund_address = elements::Address::from_str(&refund_address).unwrap();
107-
let client = Arc::new(
108-
ElectrumClient::new(
109-
DEFAULT_REGTEST_NODE,
110-
false,
111-
false,
112-
ElementsNetwork::default_regtest(),
113-
)
114-
.unwrap(),
115-
);
116-
117-
let session = BoltzSession::builder(
118-
ElementsNetwork::default_regtest(),
119-
AnyClient::Electrum(client.clone()),
120-
)
121-
.create_swap_timeout(TIMEOUT)
122-
.build()
123-
.await
124-
.unwrap();
125-
126-
// Try to pay the bolt12 offer and expect Bolt12Unsupported error
127-
let result = session
128-
.prepare_pay(&lightning_payment, &refund_address, None)
129-
.await;
130-
131-
match result {
132-
Err(lwk_boltz::Error::Bolt12Unsupported) => {
133-
// Expected error
134-
}
135-
_ => panic!("Expected Bolt12Unsupported error, got: {:?}", result),
136-
}
137-
}
138-
139166
#[tokio::test]
140167
#[ignore = "requires regtest environment"]
141168
async fn test_session_submarine_base() {

0 commit comments

Comments
 (0)