Skip to content

Commit ce4642b

Browse files
committed
xRefactor 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 a417999 commit ce4642b

File tree

3 files changed

+129
-62
lines changed

3 files changed

+129
-62
lines changed

bindings/ldk_node.udl

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -278,8 +278,8 @@ interface FeeRate {
278278
interface UnifiedPayment {
279279
[Throws=NodeError]
280280
string receive(u64 amount_sats, [ByRef]string message, u32 expiry_sec);
281-
[Throws=NodeError]
282-
UnifiedPaymentResult send([ByRef]string uri_str, RouteParametersConfig? route_parameters);
281+
[Throws=NodeError, Async]
282+
UnifiedPaymentResult send([ByRef]string uri_str, u64? amount_msat, RouteParametersConfig? route_parameters);
283283
};
284284

285285
interface LSPS1Liquidity {

src/payment/unified.rs

Lines changed: 109 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -19,13 +19,17 @@ use std::vec::IntoIter;
1919

2020
use bip21::de::ParamKind;
2121
use bip21::{DeserializationError, DeserializeParams, Param, SerializeParams};
22-
use bitcoin::address::{NetworkChecked, NetworkUnchecked};
22+
use bitcoin::address::NetworkChecked;
2323
use bitcoin::{Amount, Txid};
2424
use lightning::ln::channelmanager::PaymentId;
2525
use lightning::offers::offer::Offer;
2626
use lightning::routing::router::RouteParametersConfig;
2727
use lightning_invoice::{Bolt11Invoice, Bolt11InvoiceDescription, Description};
2828

29+
use bitcoin_payment_instructions::{
30+
amount::Amount as BPIAmount, PaymentInstructions, PaymentMethod,
31+
};
32+
2933
use crate::error::Error;
3034
use crate::ffi::maybe_wrap;
3135
use crate::logger::{log_error, LdkLogger, Logger};
@@ -138,63 +142,124 @@ impl UnifiedPayment {
138142
Ok(format_uri(uri))
139143
}
140144

141-
/// Sends a payment given a [BIP 21] URI.
145+
/// Sends a payment given a [BIP 21] URI or [BIP 353] HRN.
142146
///
143147
/// This method parses the provided URI string and attempts to send the payment. If the URI
144148
/// has an offer and or invoice, it will try to pay the offer first followed by the invoice.
145149
/// If they both fail, the on-chain payment will be paid.
146150
///
147-
/// Returns a `QrPaymentResult` indicating the outcome of the payment. If an error
151+
/// Returns a `UnifiedPaymentResult` indicating the outcome of the payment. If an error
148152
/// occurs, an `Error` is returned detailing the issue encountered.
149153
///
150154
/// If `route_parameters` are provided they will override the default as well as the
151155
/// node-wide parameters configured via [`Config::route_parameters`] on a per-field basis.
152156
///
153157
/// [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>,
158+
/// [BIP 353]: https://github.com/bitcoin/bips/blob/master/bip-0353.mediawiki
159+
pub async fn send(
160+
&self, uri_str: &str, amount_msat: Option<u64>,
161+
route_parameters: Option<RouteParametersConfig>,
156162
) -> 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)?;
164-
165-
if let Some(offer) = uri_network_checked.extras.bolt12_offer {
166-
let offer = maybe_wrap(offer);
167-
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),
171-
}
172-
}
173-
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),
180-
}
181-
}
182-
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);
163+
let instructions = PaymentInstructions::parse(
164+
uri_str,
165+
self.config.network,
166+
self.hrn_resolver.as_ref(),
167+
false,
168+
)
169+
.await
170+
.map_err(|e| {
171+
log_error!(self.logger, "Failed to parse payment instructions: {:?}", e);
172+
Error::UriParameterParsingFailed
173+
})?;
174+
175+
let resolved = match instructions {
176+
PaymentInstructions::ConfigurableAmount(instr) => {
177+
let amount = amount_msat.ok_or_else(|| {
178+
log_error!(self.logger, "No amount specified. Aborting the payment.");
179+
Error::InvalidAmount
180+
})?;
181+
182+
let amt = BPIAmount::from_milli_sats(amount).map_err(|e| {
183+
log_error!(self.logger, "Error while converting amount : {:?}", e);
184+
Error::InvalidAmount
185+
})?;
186+
187+
instr.set_amount(amt, self.hrn_resolver.as_ref()).await.map_err(|e| {
188+
log_error!(self.logger, "Failed to set amount: {:?}", e);
189+
Error::InvalidAmount
190+
})?
191+
},
192+
PaymentInstructions::FixedAmount(instr) => {
193+
if let Some(user_amount) = amount_msat {
194+
if instr.max_amount().map_or(false, |amt| user_amount < amt.milli_sats()) {
195+
log_error!(self.logger, "Amount specified is less than the amount in the parsed URI. Aborting the payment.");
196+
return Err(Error::InvalidAmount);
197+
}
198+
}
199+
instr
188200
},
189201
};
190202

191-
let txid = self.onchain_payment.send_to_address(
192-
&uri_network_checked.address,
193-
amount.to_sat(),
194-
None,
195-
)?;
203+
let mut sorted_payment_methods = resolved.methods().to_vec();
204+
sorted_payment_methods.sort_by_key(|method| match method {
205+
PaymentMethod::LightningBolt12(_) => 0,
206+
PaymentMethod::LightningBolt11(_) => 1,
207+
PaymentMethod::OnChain(_) => 2,
208+
});
209+
210+
for method in sorted_payment_methods {
211+
match method {
212+
PaymentMethod::LightningBolt12(offer) => {
213+
let offer = maybe_wrap(offer.clone());
214+
215+
let payment_result = if let Some(amount_msat) = amount_msat {
216+
self.bolt12_payment.send_using_amount(&offer, amount_msat, None, None, route_parameters)
217+
} else {
218+
self.bolt12_payment.send(&offer, None, None, route_parameters)
219+
}
220+
.map_err(|e| {
221+
log_error!(self.logger, "Failed to send BOLT12 offer: {:?}. This is part of a unified payment. Falling back to the BOLT11 invoice.", e);
222+
e
223+
});
224+
225+
if let Ok(payment_id) = payment_result {
226+
return Ok(UnifiedPaymentResult::Bolt12 { payment_id });
227+
}
228+
},
229+
PaymentMethod::LightningBolt11(invoice) => {
230+
let invoice = maybe_wrap(invoice.clone());
231+
let payment_result = self.bolt11_invoice.send(&invoice, route_parameters)
232+
.map_err(|e| {
233+
log_error!(self.logger, "Failed to send BOLT11 invoice: {:?}. This is part of a unified payment. Falling back to the on-chain transaction.", e);
234+
e
235+
});
236+
237+
if let Ok(payment_id) = payment_result {
238+
return Ok(UnifiedPaymentResult::Bolt11 { payment_id });
239+
}
240+
},
241+
PaymentMethod::OnChain(address) => {
242+
let amount = resolved.onchain_payment_amount().ok_or_else(|| {
243+
log_error!(self.logger, "No amount specified. Aborting the payment.");
244+
Error::InvalidAmount
245+
})?;
246+
247+
let amt_sats = amount.sats().map_err(|_| {
248+
log_error!(
249+
self.logger,
250+
"Amount in sats returned an error. Aborting the payment."
251+
);
252+
Error::InvalidAmount
253+
})?;
254+
255+
let txid = self.onchain_payment.send_to_address(&address, amt_sats, None)?;
256+
return Ok(UnifiedPaymentResult::Onchain { txid });
257+
},
258+
}
259+
}
196260

197-
Ok(UnifiedPaymentResult::Onchain { txid })
261+
log_error!(self.logger, "Payable methods not found in URI");
262+
Err(Error::PaymentSendingFailed)
198263
}
199264
}
200265

@@ -321,7 +386,8 @@ impl DeserializationError for Extras {
321386

322387
#[cfg(test)]
323388
mod tests {
324-
use super::{Amount, Bolt11Invoice, Extras, Offer};
389+
use super::*;
390+
use crate::payment::unified::Extras;
325391
use bitcoin::{address::NetworkUnchecked, Address, Network};
326392
use std::str::FromStr;
327393

tests/integration_tests_rust.rs

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

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

16261627
expect_payment_successful_event!(node_a, Some(offer_payment_id), None);
16271628

16281629
// Cut off the BOLT12 part to fallback to BOLT11.
16291630
let uri_str_without_offer = uri_str.split("&lno=").next().unwrap();
16301631
let invoice_payment_id: PaymentId =
1631-
match node_a.unified_payment().send(uri_str_without_offer, None) {
1632+
match node_a.unified_payment().send(uri_str_without_offer, None, None).await {
16321633
Ok(UnifiedPaymentResult::Bolt12 { payment_id: _ }) => {
16331634
panic!("Expected Bolt11 payment but got Bolt12");
16341635
},
@@ -1651,7 +1652,7 @@ async fn unified_qr_send_receive() {
16511652

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

0 commit comments

Comments
 (0)