Skip to content

Commit f02a286

Browse files
committed
Refactor unified.rs to support sending to BIP 21 URIs as well as BIP 353 HRNs
1 parent 7c1a2db commit f02a286

File tree

2 files changed

+100
-41
lines changed

2 files changed

+100
-41
lines changed

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,54 +141,110 @@ 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 uri_network_checked =
156-
uri.clone().require_network(self.config.network).map_err(|_| Error::InvalidNetwork)?;
157-
158-
if let Some(offer) = uri_network_checked.extras.bolt12_offer {
159-
let offer = maybe_wrap(offer);
160-
match self.bolt12_payment.send(&offer, None, None) {
161-
Ok(payment_id) => return Ok(UnifiedPaymentResult::Bolt12 { payment_id }),
162-
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),
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+
true,
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(ref 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_sats(amount).map_err(|e| {
178+
log_error!(self.logger, "Error while converting amount : {:?}", e);
179+
Error::InvalidAmount
180+
})?;
181+
182+
instr.clone().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(ref instr) => {
188+
let instr = instr.clone();
189+
if let Some(user_amount) = amount_msat {
190+
if instr.ln_payment_amount().map_or(false, |amt| user_amount < amt.milli_sats())
191+
{
192+
log_error!(self.logger, "Amount specified is less than the amount in the parsed URI. Aborting the payment.");
193+
return Err(Error::InvalidAmount);
194+
}
195+
}
196+
instr
197+
},
198+
};
199+
200+
if let Some(PaymentMethod::LightningBolt12(offer)) =
201+
resolved.methods().iter().find(|m| matches!(m, PaymentMethod::LightningBolt12(_)))
202+
{
203+
let offer = maybe_wrap(offer.clone());
204+
let payment_result = if let Some(amount_msat) = amount_msat {
205+
self.bolt12_payment.send_using_amount(&offer, amount_msat, None, None)
206+
} else {
207+
self.bolt12_payment.send(&offer, None, None)
163208
}
164-
}
209+
.map_err(|e| {
210+
log_error!(self.logger, "Failed to send BOLT12 offer: {:?}. This is part of a unified payment. Falling back to the BOLT11 invoice.", e);
211+
e
212+
});
165213

166-
if let Some(invoice) = uri_network_checked.extras.bolt11_invoice {
167-
let invoice = maybe_wrap(invoice);
168-
match self.bolt11_invoice.send(&invoice, None) {
169-
Ok(payment_id) => return Ok(UnifiedPaymentResult::Bolt11 { payment_id }),
170-
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),
214+
if let Ok(payment_id) = payment_result {
215+
return Ok(UnifiedPaymentResult::Bolt12 { payment_id });
171216
}
172217
}
173218

174-
let amount = match uri_network_checked.amount {
175-
Some(amount) => amount,
176-
None => {
177-
log_error!(self.logger, "No amount specified in the URI. Aborting the payment.");
178-
return Err(Error::InvalidAmount);
179-
},
180-
};
181-
182-
let txid = self.onchain_payment.send_to_address(
183-
&uri_network_checked.address,
184-
amount.to_sat(),
185-
None,
186-
)?;
219+
if let Some(PaymentMethod::LightningBolt11(invoice)) =
220+
resolved.methods().iter().find(|m| matches!(m, PaymentMethod::LightningBolt11(_)))
221+
{
222+
let invoice = maybe_wrap(invoice.clone());
223+
let payment_result = self.bolt11_invoice.send(&invoice, None)
224+
.map_err(|e| {
225+
log_error!(self.logger, "Failed to send BOLT11 invoice: {:?}. This is part of a unified payment. Falling back to the on-chain transaction.", e);
226+
e
227+
});
228+
229+
if let Ok(payment_id) = payment_result {
230+
return Ok(UnifiedPaymentResult::Bolt11 { payment_id });
231+
}
232+
}
187233

188-
Ok(UnifiedPaymentResult::Onchain { txid })
234+
if let Some(PaymentMethod::OnChain(address)) =
235+
resolved.methods().iter().find(|m| matches!(m, PaymentMethod::OnChain(_)))
236+
{
237+
let amount = resolved.onchain_payment_amount().ok_or_else(|| {
238+
log_error!(self.logger, "No amount specified. Aborting the payment.");
239+
Error::InvalidAmount
240+
})?;
241+
242+
let txid =
243+
self.onchain_payment.send_to_address(&address, amount.sats().unwrap(), None)?;
244+
return Ok(UnifiedPaymentResult::Onchain { txid });
245+
}
246+
log_error!(self.logger, "Payable methods not found in URI");
247+
Err(Error::PaymentSendingFailed)
189248
}
190249
}
191250

@@ -314,7 +373,7 @@ impl DeserializationError for Extras {
314373
mod tests {
315374
use super::*;
316375
use crate::payment::unified::Extras;
317-
use bitcoin::{Address, Network};
376+
use bitcoin::{address::NetworkUnchecked, Address, Network};
318377
use std::str::FromStr;
319378

320379
#[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)