Skip to content

Commit 2c809b1

Browse files
committed
Refactor unified.rs to support sending to BIP 21 URIs as well as BIP 353 HRNs
1 parent 6d20a9e commit 2c809b1

File tree

3 files changed

+116
-55
lines changed

3 files changed

+116
-55
lines changed

bindings/ldk_node.udl

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -255,8 +255,8 @@ interface FeeRate {
255255
interface UnifiedPayment {
256256
[Throws=NodeError]
257257
string receive(u64 amount_sats, [ByRef]string message, u32 expiry_sec);
258-
[Throws=NodeError]
259-
UnifiedPaymentResult send([ByRef]string uri_str);
258+
[Throws=NodeError, Async]
259+
UnifiedPaymentResult send([ByRef]string uri_str, u64? amount_msat);
260260
};
261261

262262
interface LSPS1Liquidity {

src/payment/unified.rs

Lines changed: 96 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,11 @@ use lightning_invoice::{Bolt11Invoice, Bolt11InvoiceDescription, Description};
2929

3030
use bip21::de::ParamKind;
3131
use bip21::{DeserializationError, DeserializeParams, Param, SerializeParams};
32-
use bitcoin::address::{NetworkChecked, NetworkUnchecked};
32+
use bitcoin::address::NetworkChecked;
3333
use bitcoin::{Amount, Txid};
34+
use bitcoin_payment_instructions::{
35+
amount::Amount as BPIAmount, PaymentInstructions, PaymentMethod,
36+
};
3437

3538
type Uri<'a> = bip21::Uri<'a, NetworkChecked, Extras>;
3639

@@ -137,56 +140,112 @@ impl UnifiedPayment {
137140
Ok(format_uri(uri))
138141
}
139142

140-
/// Sends a payment given a [BIP 21] URI.
143+
/// Sends a payment given a [BIP 21] URI or [BIP 353] HRN.
141144
///
142145
/// This method parses the provided URI string and attempts to send the payment. If the URI
143146
/// has an offer and or invoice, it will try to pay the offer first followed by the invoice.
144147
/// If they both fail, the on-chain payment will be paid.
145148
///
146-
/// Returns a `QrPaymentResult` indicating the outcome of the payment. If an error
149+
/// Returns a `UnifiedPaymentResult` indicating the outcome of the payment. If an error
147150
/// occurs, an `Error` is returned detailing the issue encountered.
148151
///
149152
/// [BIP 21]: https://github.com/bitcoin/bips/blob/master/bip-0021.mediawiki
150-
pub fn send(&self, uri_str: &str) -> Result<UnifiedPaymentResult, Error> {
151-
let uri: bip21::Uri<NetworkUnchecked, Extras> =
152-
uri_str.parse().map_err(|_| Error::InvalidUri)?;
153-
154-
let _resolver = &self.hrn_resolver;
153+
/// [BIP 353]: https://github.com/bitcoin/bips/blob/master/bip-0353.mediawiki
154+
pub async fn send(
155+
&self, uri_str: &str, amount_msat: Option<u64>,
156+
) -> Result<UnifiedPaymentResult, Error> {
157+
let instructions = PaymentInstructions::parse(
158+
uri_str,
159+
self.config.network,
160+
self.hrn_resolver.as_ref(),
161+
false,
162+
)
163+
.await
164+
.map_err(|e| {
165+
log_error!(self.logger, "Failed to parse payment instructions: {:?}", e);
166+
Error::UriParameterParsingFailed
167+
})?;
168+
169+
let resolved = match instructions {
170+
PaymentInstructions::ConfigurableAmount(instr) => {
171+
let amount = amount_msat.ok_or_else(|| {
172+
log_error!(self.logger, "No amount specified. Aborting the payment.");
173+
Error::InvalidAmount
174+
})?;
175+
176+
let amt = BPIAmount::from_milli_sats(amount).map_err(|e| {
177+
log_error!(self.logger, "Error while converting amount : {:?}", e);
178+
Error::InvalidAmount
179+
})?;
180+
181+
instr.set_amount(amt, self.hrn_resolver.as_ref()).await.map_err(|e| {
182+
log_error!(self.logger, "Failed to set amount: {:?}", e);
183+
Error::InvalidAmount
184+
})?
185+
},
186+
PaymentInstructions::FixedAmount(instr) => {
187+
if let Some(user_amount) = amount_msat {
188+
if instr.max_amount().map_or(false, |amt| user_amount < amt.milli_sats()) {
189+
log_error!(self.logger, "Amount specified is less than the amount in the parsed URI. Aborting the payment.");
190+
return Err(Error::InvalidAmount);
191+
}
192+
}
193+
instr
194+
},
195+
};
155196

156-
let uri_network_checked =
157-
uri.clone().require_network(self.config.network).map_err(|_| Error::InvalidNetwork)?;
197+
if let Some(PaymentMethod::LightningBolt12(offer)) =
198+
resolved.methods().iter().find(|m| matches!(m, PaymentMethod::LightningBolt12(_)))
199+
{
200+
let offer = maybe_wrap(offer.clone());
201+
let payment_result = if let Some(amount_msat) = amount_msat {
202+
self.bolt12_payment.send_using_amount(&offer, amount_msat, None, None)
203+
} else {
204+
self.bolt12_payment.send(&offer, None, None)
205+
}
206+
.map_err(|e| {
207+
log_error!(self.logger, "Failed to send BOLT12 offer: {:?}. This is part of a unified payment. Falling back to the BOLT11 invoice.", e);
208+
e
209+
});
158210

159-
if let Some(offer) = uri_network_checked.extras.bolt12_offer {
160-
let offer = maybe_wrap(offer);
161-
match self.bolt12_payment.send(&offer, None, None) {
162-
Ok(payment_id) => return Ok(UnifiedPaymentResult::Bolt12 { payment_id }),
163-
Err(e) => log_error!(self.logger, "Failed to send BOLT12 offer: {:?}. This is part of a unified QR code payment. Falling back to the BOLT11 invoice.", e),
211+
if let Ok(payment_id) = payment_result {
212+
return Ok(UnifiedPaymentResult::Bolt12 { payment_id });
164213
}
165214
}
166215

167-
if let Some(invoice) = uri_network_checked.extras.bolt11_invoice {
168-
let invoice = maybe_wrap(invoice);
169-
match self.bolt11_invoice.send(&invoice, None) {
170-
Ok(payment_id) => return Ok(UnifiedPaymentResult::Bolt11 { payment_id }),
171-
Err(e) => log_error!(self.logger, "Failed to send BOLT11 invoice: {:?}. This is part of a unified QR code payment. Falling back to the on-chain transaction.", e),
216+
if let Some(PaymentMethod::LightningBolt11(invoice)) =
217+
resolved.methods().iter().find(|m| matches!(m, PaymentMethod::LightningBolt11(_)))
218+
{
219+
let invoice = maybe_wrap(invoice.clone());
220+
let payment_result = self.bolt11_invoice.send(&invoice, None)
221+
.map_err(|e| {
222+
log_error!(self.logger, "Failed to send BOLT11 invoice: {:?}. This is part of a unified payment. Falling back to the on-chain transaction.", e);
223+
e
224+
});
225+
226+
if let Ok(payment_id) = payment_result {
227+
return Ok(UnifiedPaymentResult::Bolt11 { payment_id });
172228
}
173229
}
174230

175-
let amount = match uri_network_checked.amount {
176-
Some(amount) => amount,
177-
None => {
178-
log_error!(self.logger, "No amount specified in the URI. Aborting the payment.");
179-
return Err(Error::InvalidAmount);
180-
},
181-
};
182-
183-
let txid = self.onchain_payment.send_to_address(
184-
&uri_network_checked.address,
185-
amount.to_sat(),
186-
None,
187-
)?;
188-
189-
Ok(UnifiedPaymentResult::Onchain { txid })
231+
if let Some(PaymentMethod::OnChain(address)) =
232+
resolved.methods().iter().find(|m| matches!(m, PaymentMethod::OnChain(_)))
233+
{
234+
let amount = resolved.onchain_payment_amount().ok_or_else(|| {
235+
log_error!(self.logger, "No amount specified. Aborting the payment.");
236+
Error::InvalidAmount
237+
})?;
238+
239+
let amt_sats = amount.sats().map_err(|_| {
240+
log_error!(self.logger, "Amount in sats returned an error. Aborting the payment.");
241+
Error::InvalidAmount
242+
})?;
243+
244+
let txid = self.onchain_payment.send_to_address(&address, amt_sats, None)?;
245+
return Ok(UnifiedPaymentResult::Onchain { txid });
246+
}
247+
log_error!(self.logger, "Payable methods not found in URI");
248+
Err(Error::PaymentSendingFailed)
190249
}
191250
}
192251

@@ -313,7 +372,8 @@ impl DeserializationError for Extras {
313372

314373
#[cfg(test)]
315374
mod tests {
316-
use super::{Amount, Bolt11Invoice, Extras, Offer};
375+
use super::*;
376+
use crate::payment::unified::Extras;
317377
use bitcoin::{address::NetworkUnchecked, Address, Network};
318378
use std::str::FromStr;
319379

tests/integration_tests_rust.rs

Lines changed: 18 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1473,7 +1473,7 @@ async fn unified_qr_send_receive() {
14731473

14741474
let uni_payment = node_b.unified_payment().receive(expected_amount_sats, "asdf", expiry_sec);
14751475
let uri_str = uni_payment.clone().unwrap();
1476-
let offer_payment_id: PaymentId = match node_a.unified_payment().send(&uri_str) {
1476+
let offer_payment_id: PaymentId = match node_a.unified_payment().send(&uri_str, None).await {
14771477
Ok(UnifiedPaymentResult::Bolt12 { payment_id }) => {
14781478
println!("\nBolt12 payment sent successfully with PaymentID: {:?}", payment_id);
14791479
payment_id
@@ -1493,21 +1493,22 @@ async fn unified_qr_send_receive() {
14931493

14941494
// Cut off the BOLT12 part to fallback to BOLT11.
14951495
let uri_str_without_offer = uri_str.split("&lno=").next().unwrap();
1496-
let invoice_payment_id: PaymentId = match node_a.unified_payment().send(uri_str_without_offer) {
1497-
Ok(UnifiedPaymentResult::Bolt12 { payment_id: _ }) => {
1498-
panic!("Expected Bolt11 payment but got Bolt12");
1499-
},
1500-
Ok(UnifiedPaymentResult::Bolt11 { payment_id }) => {
1501-
println!("\nBolt11 payment sent successfully with PaymentID: {:?}", payment_id);
1502-
payment_id
1503-
},
1504-
Ok(UnifiedPaymentResult::Onchain { txid: _ }) => {
1505-
panic!("Expected Bolt11 payment but got on-chain transaction");
1506-
},
1507-
Err(e) => {
1508-
panic!("Expected Bolt11 payment but got error: {:?}", e);
1509-
},
1510-
};
1496+
let invoice_payment_id: PaymentId =
1497+
match node_a.unified_payment().send(uri_str_without_offer, None).await {
1498+
Ok(UnifiedPaymentResult::Bolt12 { payment_id: _ }) => {
1499+
panic!("Expected Bolt11 payment but got Bolt12");
1500+
},
1501+
Ok(UnifiedPaymentResult::Bolt11 { payment_id }) => {
1502+
println!("\nBolt11 payment sent successfully with PaymentID: {:?}", payment_id);
1503+
payment_id
1504+
},
1505+
Ok(UnifiedPaymentResult::Onchain { txid: _ }) => {
1506+
panic!("Expected Bolt11 payment but got on-chain transaction");
1507+
},
1508+
Err(e) => {
1509+
panic!("Expected Bolt11 payment but got error: {:?}", e);
1510+
},
1511+
};
15111512
expect_payment_successful_event!(node_a, Some(invoice_payment_id), None);
15121513

15131514
let expect_onchain_amount_sats = 800_000;
@@ -1516,7 +1517,7 @@ async fn unified_qr_send_receive() {
15161517

15171518
// Cut off any lightning part to fallback to on-chain only.
15181519
let uri_str_without_lightning = onchain_uni_payment.split("&lightning=").next().unwrap();
1519-
let txid = match node_a.unified_payment().send(&uri_str_without_lightning) {
1520+
let txid = match node_a.unified_payment().send(&uri_str_without_lightning, None).await {
15201521
Ok(UnifiedPaymentResult::Bolt12 { payment_id: _ }) => {
15211522
panic!("Expected on-chain payment but got Bolt12")
15221523
},

0 commit comments

Comments
 (0)