Skip to content

Commit f1d1fa1

Browse files
committed
Refactor unified.rs to support sending to BIP 21 URIs as well as BIP 353 HRNs
1 parent 55522b9 commit f1d1fa1

File tree

3 files changed

+102
-43
lines changed

3 files changed

+102
-43
lines changed

bindings/ldk_node.udl

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -246,8 +246,8 @@ interface FeeRate {
246246
interface UnifiedPayment {
247247
[Throws=NodeError]
248248
string receive(u64 amount_sats, [ByRef]string message, u32 expiry_sec);
249-
[Throws=NodeError]
250-
UnifiedPaymentResult send([ByRef]string uri_str);
249+
[Throws=NodeError, Async]
250+
UnifiedPaymentResult send([ByRef]string uri_str, u64? amount_msat);
251251
};
252252

253253
interface LSPS1Liquidity {

src/payment/unified.rs

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

2828
use bip21::de::ParamKind;
2929
use bip21::{DeserializationError, DeserializeParams, Param, SerializeParams};
30-
use bitcoin::address::{NetworkChecked, NetworkUnchecked};
30+
use bitcoin::address::NetworkChecked;
3131
use bitcoin::{Amount, Txid};
32+
use bitcoin_payment_instructions::{
33+
amount::Amount as BPIAmount, PaymentInstructions, PaymentMethod,
34+
};
3235

3336
use std::sync::Arc;
3437
use std::vec::IntoIter;
@@ -138,56 +141,112 @@ 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
/// [BIP 21]: https://github.com/bitcoin/bips/blob/master/bip-0021.mediawiki
151-
pub fn send(&self, uri_str: &str) -> Result<UnifiedPaymentResult, Error> {
152-
let uri: bip21::Uri<NetworkUnchecked, Extras> =
153-
uri_str.parse().map_err(|_| Error::InvalidUri)?;
154-
155-
let _resolver = &self.hrn_resolver;
154+
/// [BIP 353]: https://github.com/bitcoin/bips/blob/master/bip-0353.mediawiki
155+
pub async fn send(
156+
&self, uri_str: &str, amount_msat: Option<u64>,
157+
) -> Result<UnifiedPaymentResult, Error> {
158+
let instructions = PaymentInstructions::parse(
159+
uri_str,
160+
self.config.network,
161+
self.hrn_resolver.as_ref(),
162+
false,
163+
)
164+
.await
165+
.map_err(|e| {
166+
log_error!(self.logger, "Failed to parse payment instructions: {:?}", e);
167+
Error::UriParameterParsingFailed
168+
})?;
169+
170+
let resolved = match instructions {
171+
PaymentInstructions::ConfigurableAmount(instr) => {
172+
let amount = amount_msat.ok_or_else(|| {
173+
log_error!(self.logger, "No amount specified. Aborting the payment.");
174+
Error::InvalidAmount
175+
})?;
176+
177+
let amt = BPIAmount::from_milli_sats(amount).map_err(|e| {
178+
log_error!(self.logger, "Error while converting amount : {:?}", e);
179+
Error::InvalidAmount
180+
})?;
181+
182+
instr.set_amount(amt, self.hrn_resolver.as_ref()).await.map_err(|e| {
183+
log_error!(self.logger, "Failed to set amount: {:?}", e);
184+
Error::InvalidAmount
185+
})?
186+
},
187+
PaymentInstructions::FixedAmount(instr) => {
188+
if let Some(user_amount) = amount_msat {
189+
if instr.max_amount().map_or(false, |amt| user_amount < amt.milli_sats()) {
190+
log_error!(self.logger, "Amount specified is less than the amount in the parsed URI. Aborting the payment.");
191+
return Err(Error::InvalidAmount);
192+
}
193+
}
194+
instr
195+
},
196+
};
156197

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

160-
if let Some(offer) = uri_network_checked.extras.bolt12_offer {
161-
let offer = maybe_wrap(offer);
162-
match self.bolt12_payment.send(&offer, None, None) {
163-
Ok(payment_id) => return Ok(UnifiedPaymentResult::Bolt12 { payment_id }),
164-
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),
212+
if let Ok(payment_id) = payment_result {
213+
return Ok(UnifiedPaymentResult::Bolt12 { payment_id });
165214
}
166215
}
167216

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

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

@@ -316,7 +375,7 @@ impl DeserializationError for Extras {
316375
mod tests {
317376
use super::*;
318377
use crate::payment::unified::Extras;
319-
use bitcoin::{Address, Network};
378+
use bitcoin::{address::NetworkUnchecked, Address, Network};
320379
use std::str::FromStr;
321380

322381
#[test]

tests/integration_tests_rust.rs

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1274,8 +1274,8 @@ fn generate_bip21_uri() {
12741274
assert!(uni_payment.contains("lno="));
12751275
}
12761276

1277-
#[test]
1278-
fn unified_qr_send_receive() {
1277+
#[tokio::test(flavor = "multi_thread")]
1278+
async fn unified_qr_send_receive() {
12791279
let (bitcoind, electrsd) = setup_bitcoind_and_electrsd();
12801280
let chain_source = TestChainSource::Esplora(&electrsd);
12811281

@@ -1314,7 +1314,7 @@ fn unified_qr_send_receive() {
13141314

13151315
let uni_payment = node_b.unified_payment().receive(expected_amount_sats, "asdf", expiry_sec);
13161316
let uri_str = uni_payment.clone().unwrap();
1317-
let offer_payment_id: PaymentId = match node_a.unified_payment().send(&uri_str) {
1317+
let offer_payment_id: PaymentId = match node_a.unified_payment().send(&uri_str, None).await {
13181318
Ok(UnifiedPaymentResult::Bolt12 { payment_id }) => {
13191319
println!("\nBolt12 payment sent successfully with PaymentID: {:?}", payment_id);
13201320
payment_id
@@ -1335,7 +1335,7 @@ fn unified_qr_send_receive() {
13351335
// Cut off the BOLT12 part to fallback to BOLT11.
13361336
let uri_str_without_offer = uri_str.split("&lno=").next().unwrap();
13371337
let invoice_payment_id: PaymentId =
1338-
match node_a.unified_payment().send(uri_str_without_offer) {
1338+
match node_a.unified_payment().send(uri_str_without_offer, None).await {
13391339
Ok(UnifiedPaymentResult::Bolt12 { payment_id: _ }) => {
13401340
panic!("Expected Bolt11 payment but got Bolt12");
13411341
},
@@ -1358,7 +1358,7 @@ fn unified_qr_send_receive() {
13581358

13591359
// Cut off any lightning part to fallback to on-chain only.
13601360
let uri_str_without_lightning = onchain_uni_payment.split("&lightning=").next().unwrap();
1361-
let txid = match node_a.unified_payment().send(&uri_str_without_lightning) {
1361+
let txid = match node_a.unified_payment().send(&uri_str_without_lightning, None).await {
13621362
Ok(UnifiedPaymentResult::Bolt12 { payment_id: _ }) => {
13631363
panic!("Expected on-chain payment but got Bolt12")
13641364
},

0 commit comments

Comments
 (0)