Skip to content

Commit c112eec

Browse files
committed
Refactor unified payment to support BIP 21 and HRNs
Restructure the payment initiation logic in unified.rs to handle both BIP 21 URIs and BIP 353 Human-Readable Names (HRNs) as input sources. This provides a single, unified entry point for initiating payments.
1 parent f9cbcb8 commit c112eec

File tree

3 files changed

+116
-58
lines changed

3 files changed

+116
-58
lines changed

bindings/ldk_node.udl

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -268,8 +268,8 @@ interface FeeRate {
268268
interface UnifiedPayment {
269269
[Throws=NodeError]
270270
string receive(u64 amount_sats, [ByRef]string message, u32 expiry_sec);
271-
[Throws=NodeError]
272-
UnifiedPaymentResult send([ByRef]string uri_str, RouteParametersConfig? route_parameters);
271+
[Throws=NodeError, Async]
272+
UnifiedPaymentResult send([ByRef]string uri_str, u64? amount_msat, RouteParametersConfig? route_parameters);
273273
};
274274

275275
interface LSPS1Liquidity {

src/payment/unified.rs

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

3131
use bip21::de::ParamKind;
3232
use bip21::{DeserializationError, DeserializeParams, Param, SerializeParams};
33-
use bitcoin::address::{NetworkChecked, NetworkUnchecked};
33+
use bitcoin::address::NetworkChecked;
3434
use bitcoin::{Amount, Txid};
35+
use bitcoin_payment_instructions::{
36+
amount::Amount as BPIAmount, PaymentInstructions, PaymentMethod,
37+
};
3538

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

@@ -138,63 +141,116 @@ impl UnifiedPayment {
138141
Ok(format_uri(uri))
139142
}
140143

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

165-
if let Some(offer) = uri_network_checked.extras.bolt12_offer {
166-
let offer = maybe_wrap(offer);
202+
if let Some(PaymentMethod::LightningBolt12(offer)) =
203+
resolved.methods().iter().find(|m| matches!(m, PaymentMethod::LightningBolt12(_)))
204+
{
205+
let offer = maybe_wrap(offer.clone());
206+
let payment_result = if let Some(amount_msat) = amount_msat {
207+
self.bolt12_payment.send_using_amount(&offer, amount_msat, None, None, route_parameters)
208+
} else {
209+
self.bolt12_payment.send(&offer, None, None, route_parameters)
210+
}
211+
.map_err(|e| {
212+
log_error!(self.logger, "Failed to send BOLT12 offer: {:?}. This is part of a unified payment. Falling back to the BOLT11 invoice.", e);
213+
e
214+
});
167215

168-
match self.bolt12_payment.send(&offer, None, None, route_parameters) {
169-
Ok(payment_id) => return Ok(UnifiedPaymentResult::Bolt12 { payment_id }),
170-
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),
216+
if let Ok(payment_id) = payment_result {
217+
return Ok(UnifiedPaymentResult::Bolt12 { payment_id });
171218
}
172219
}
173220

174-
if let Some(invoice) = uri_network_checked.extras.bolt11_invoice {
175-
let invoice = maybe_wrap(invoice);
176-
177-
match self.bolt11_invoice.send(&invoice, route_parameters) {
178-
Ok(payment_id) => return Ok(UnifiedPaymentResult::Bolt11 { payment_id }),
179-
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),
221+
if let Some(PaymentMethod::LightningBolt11(invoice)) =
222+
resolved.methods().iter().find(|m| matches!(m, PaymentMethod::LightningBolt11(_)))
223+
{
224+
let invoice = maybe_wrap(invoice.clone());
225+
let payment_result = self.bolt11_invoice.send(&invoice, route_parameters)
226+
.map_err(|e| {
227+
log_error!(self.logger, "Failed to send BOLT11 invoice: {:?}. This is part of a unified payment. Falling back to the on-chain transaction.", e);
228+
e
229+
});
230+
231+
if let Ok(payment_id) = payment_result {
232+
return Ok(UnifiedPaymentResult::Bolt11 { payment_id });
180233
}
181234
}
182235

183-
let amount = match uri_network_checked.amount {
184-
Some(amount) => amount,
185-
None => {
186-
log_error!(self.logger, "No amount specified in the URI. Aborting the payment.");
187-
return Err(Error::InvalidAmount);
188-
},
189-
};
190-
191-
let txid = self.onchain_payment.send_to_address(
192-
&uri_network_checked.address,
193-
amount.to_sat(),
194-
None,
195-
)?;
196-
197-
Ok(UnifiedPaymentResult::Onchain { txid })
236+
if let Some(PaymentMethod::OnChain(address)) =
237+
resolved.methods().iter().find(|m| matches!(m, PaymentMethod::OnChain(_)))
238+
{
239+
let amount = resolved.onchain_payment_amount().ok_or_else(|| {
240+
log_error!(self.logger, "No amount specified. Aborting the payment.");
241+
Error::InvalidAmount
242+
})?;
243+
244+
let amt_sats = amount.sats().map_err(|_| {
245+
log_error!(self.logger, "Amount in sats returned an error. Aborting the payment.");
246+
Error::InvalidAmount
247+
})?;
248+
249+
let txid = self.onchain_payment.send_to_address(&address, amt_sats, None)?;
250+
return Ok(UnifiedPaymentResult::Onchain { txid });
251+
}
252+
log_error!(self.logger, "Payable methods not found in URI");
253+
Err(Error::PaymentSendingFailed)
198254
}
199255
}
200256

@@ -321,7 +377,8 @@ impl DeserializationError for Extras {
321377

322378
#[cfg(test)]
323379
mod tests {
324-
use super::{Amount, Bolt11Invoice, Extras, Offer};
380+
use super::*;
381+
use crate::payment::unified::Extras;
325382
use bitcoin::{address::NetworkUnchecked, Address, Network};
326383
use std::str::FromStr;
327384

tests/integration_tests_rust.rs

Lines changed: 18 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1609,28 +1609,29 @@ async fn unified_qr_send_receive() {
16091609

16101610
let uni_payment = node_b.unified_payment().receive(expected_amount_sats, "asdf", expiry_sec);
16111611
let uri_str = uni_payment.clone().unwrap();
1612-
let offer_payment_id: PaymentId = match node_a.unified_payment().send(&uri_str, None) {
1613-
Ok(UnifiedPaymentResult::Bolt12 { payment_id }) => {
1614-
println!("\nBolt12 payment sent successfully with PaymentID: {:?}", payment_id);
1615-
payment_id
1616-
},
1617-
Ok(UnifiedPaymentResult::Bolt11 { payment_id: _ }) => {
1618-
panic!("Expected Bolt12 payment but got Bolt11");
1619-
},
1620-
Ok(UnifiedPaymentResult::Onchain { txid: _ }) => {
1621-
panic!("Expected Bolt12 payment but get On-chain transaction");
1622-
},
1623-
Err(e) => {
1624-
panic!("Expected Bolt12 payment but got error: {:?}", e);
1625-
},
1626-
};
1612+
let offer_payment_id: PaymentId =
1613+
match node_a.unified_payment().send(&uri_str, None, None).await {
1614+
Ok(UnifiedPaymentResult::Bolt12 { payment_id }) => {
1615+
println!("\nBolt12 payment sent successfully with PaymentID: {:?}", payment_id);
1616+
payment_id
1617+
},
1618+
Ok(UnifiedPaymentResult::Bolt11 { payment_id: _ }) => {
1619+
panic!("Expected Bolt12 payment but got Bolt11");
1620+
},
1621+
Ok(UnifiedPaymentResult::Onchain { txid: _ }) => {
1622+
panic!("Expected Bolt12 payment but get On-chain transaction");
1623+
},
1624+
Err(e) => {
1625+
panic!("Expected Bolt12 payment but got error: {:?}", e);
1626+
},
1627+
};
16271628

16281629
expect_payment_successful_event!(node_a, Some(offer_payment_id), None);
16291630

16301631
// Cut off the BOLT12 part to fallback to BOLT11.
16311632
let uri_str_without_offer = uri_str.split("&lno=").next().unwrap();
16321633
let invoice_payment_id: PaymentId =
1633-
match node_a.unified_payment().send(uri_str_without_offer, None) {
1634+
match node_a.unified_payment().send(uri_str_without_offer, None, None).await {
16341635
Ok(UnifiedPaymentResult::Bolt12 { payment_id: _ }) => {
16351636
panic!("Expected Bolt11 payment but got Bolt12");
16361637
},
@@ -1653,7 +1654,7 @@ async fn unified_qr_send_receive() {
16531654

16541655
// Cut off any lightning part to fallback to on-chain only.
16551656
let uri_str_without_lightning = onchain_uni_payment.split("&lightning=").next().unwrap();
1656-
let txid = match node_a.unified_payment().send(&uri_str_without_lightning, None) {
1657+
let txid = match node_a.unified_payment().send(&uri_str_without_lightning, None, None).await {
16571658
Ok(UnifiedPaymentResult::Bolt12 { payment_id: _ }) => {
16581659
panic!("Expected on-chain payment but got Bolt12")
16591660
},

0 commit comments

Comments
 (0)