Skip to content

Commit ceef099

Browse files
committed
Refactor unified.rs to support sending to BIP 21 URIs as well as BIP 353 HRNs
1 parent 80cd244 commit ceef099

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
@@ -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, RouteParametersConfig? route_parameters);
258+
[Throws=NodeError, Async]
259+
UnifiedPaymentResult send([ByRef]string uri_str, u64? amount_msat, RouteParametersConfig? route_parameters);
260260
};
261261

262262
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
@@ -1480,28 +1480,29 @@ async fn unified_qr_send_receive() {
14801480

14811481
let uni_payment = node_b.unified_payment().receive(expected_amount_sats, "asdf", expiry_sec);
14821482
let uri_str = uni_payment.clone().unwrap();
1483-
let offer_payment_id: PaymentId = match node_a.unified_payment().send(&uri_str, None) {
1484-
Ok(UnifiedPaymentResult::Bolt12 { payment_id }) => {
1485-
println!("\nBolt12 payment sent successfully with PaymentID: {:?}", payment_id);
1486-
payment_id
1487-
},
1488-
Ok(UnifiedPaymentResult::Bolt11 { payment_id: _ }) => {
1489-
panic!("Expected Bolt12 payment but got Bolt11");
1490-
},
1491-
Ok(UnifiedPaymentResult::Onchain { txid: _ }) => {
1492-
panic!("Expected Bolt12 payment but get On-chain transaction");
1493-
},
1494-
Err(e) => {
1495-
panic!("Expected Bolt12 payment but got error: {:?}", e);
1496-
},
1497-
};
1483+
let offer_payment_id: PaymentId =
1484+
match node_a.unified_payment().send(&uri_str, None, None).await {
1485+
Ok(UnifiedPaymentResult::Bolt12 { payment_id }) => {
1486+
println!("\nBolt12 payment sent successfully with PaymentID: {:?}", payment_id);
1487+
payment_id
1488+
},
1489+
Ok(UnifiedPaymentResult::Bolt11 { payment_id: _ }) => {
1490+
panic!("Expected Bolt12 payment but got Bolt11");
1491+
},
1492+
Ok(UnifiedPaymentResult::Onchain { txid: _ }) => {
1493+
panic!("Expected Bolt12 payment but get On-chain transaction");
1494+
},
1495+
Err(e) => {
1496+
panic!("Expected Bolt12 payment but got error: {:?}", e);
1497+
},
1498+
};
14981499

14991500
expect_payment_successful_event!(node_a, Some(offer_payment_id), None);
15001501

15011502
// Cut off the BOLT12 part to fallback to BOLT11.
15021503
let uri_str_without_offer = uri_str.split("&lno=").next().unwrap();
15031504
let invoice_payment_id: PaymentId =
1504-
match node_a.unified_payment().send(uri_str_without_offer, None) {
1505+
match node_a.unified_payment().send(uri_str_without_offer, None, None).await {
15051506
Ok(UnifiedPaymentResult::Bolt12 { payment_id: _ }) => {
15061507
panic!("Expected Bolt11 payment but got Bolt12");
15071508
},
@@ -1524,7 +1525,7 @@ async fn unified_qr_send_receive() {
15241525

15251526
// Cut off any lightning part to fallback to on-chain only.
15261527
let uri_str_without_lightning = onchain_uni_payment.split("&lightning=").next().unwrap();
1527-
let txid = match node_a.unified_payment().send(&uri_str_without_lightning, None) {
1528+
let txid = match node_a.unified_payment().send(&uri_str_without_lightning, None, None).await {
15281529
Ok(UnifiedPaymentResult::Bolt12 { payment_id: _ }) => {
15291530
panic!("Expected on-chain payment but got Bolt12")
15301531
},

0 commit comments

Comments
 (0)