Skip to main content

bark/
payment_request.rs

1//! Payment string parsing and BIP 321 URI construction for bark wallets.
2//!
3//! This module provides two main capabilities:
4//!
5//! - **Parsing**: [`Wallet::parse_payment_details`] accepts any payment string
6//!   the wallet understands (BIP 321 URIs, BOLT11 invoices, BOLT12 offers,
7//!   lightning addresses, output scripts, bitcoin addresses, ark addresses)
8//!   and returns structured [`PaymentRequest`] with per-method validation
9//!   errors.
10//!
11//! - **Construction**: [`Wallet::bip321_uri`] returns a [`BarkBip321UriBuilder`]
12//!   for creating BIP 321 URIs backed by the wallet's Ark and Lightning
13//!   capabilities.
14
15pub use crate::movement::PaymentMethod;
16
17use std::str::FromStr;
18
19use anyhow::Context;
20use bitcoin::{Amount, Network};
21use bitcoin::constants::ChainHash;
22use lnurllib::lightning_address::LightningAddress;
23
24use ark::lightning::{Bolt11Invoice, Invoice, Offer, OfferAmountExt};
25use bip321::{Bip321Error, Bip321Uri, ExtensionHandler, FieldWithAttributes};
26use bitcoin_ext::AmountExt;
27use log::debug;
28
29use crate::{FeeEstimate, Wallet};
30use crate::arkoor::ArkoorAddressError;
31use crate::onchain::GetAddress;
32
33#[derive(Default, Clone, PartialEq, Eq, Debug)]
34pub struct BarkExtension {
35	ark: Vec<FieldWithAttributes<ark::Address>>,
36}
37
38impl ExtensionHandler for BarkExtension {
39	fn handle_param(
40		&mut self,
41		key: &str,
42		value: &str,
43		required: bool,
44	) -> Result<bool, Bip321Error> {
45		if key == "ark" {
46			let address = ark::Address::from_str(value)
47				.map_err(|e| Bip321Error::ExtensionError(e.to_string()))?;
48			self.ark.push(FieldWithAttributes::new(address, required));
49			Ok(true)
50		} else {
51			Ok(false)
52		}
53	}
54
55	fn is_empty(&self) -> bool {
56		self.ark.is_empty()
57	}
58
59	fn serialize_params(&self) -> Vec<(String, String)> {
60		self.ark.iter()
61			.map(|a| ("ark".to_string(), a.inner().to_string()))
62			.collect()
63	}
64}
65
66type BarkBip321Uri = Bip321Uri<BarkExtension>;
67
68/// A non-fatal issue detected while validating a single payment option.
69///
70/// These are collected per-option in [`AvailablePaymentMethod::errors`] so
71/// callers can present all options to the user and let them choose, rather
72/// than failing on the first problem.
73#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
74pub enum PaymentMethodParsingError {
75	/// The payment target uses a different bitcoin network than the wallet.
76	#[error("network mismatch")]
77	NetworkMismatch,
78	/// The Ark address is invalid.
79	#[error("invalid ark address: {0}")]
80	InvalidArkAddress(#[from] ArkoorAddressError),
81	/// An amount is required but was not provided and cannot be inferred.
82	#[error("amount required")]
83	MissingAmount,
84	/// The provided amount does not satisfy the payment target's requirements.
85	#[error("amount mismatch: expected {expected}, got {got}")]
86	AmountMismatch { expected: Amount, got: Amount },
87	/// The payment target's amount is invalid.
88	#[error("invalid amount")]
89	InvalidAmount,
90	/// The payment option is not supported.
91	#[error("unsupported payment option")]
92	Unsupported,
93}
94
95/// A single payment option with its validation issues.
96///
97/// A option with a non-empty [`errors`](Self::errors) list may still be
98/// presented to the user, but should be flagged as problematic.
99#[derive(Debug, Clone, PartialEq, Eq)]
100pub struct AvailablePaymentMethod {
101	pub method: PaymentMethod,
102	pub errors: Vec<PaymentMethodParsingError>,
103}
104
105/// The result of parsing a payment string.
106///
107/// Contains optional BIP 321 metadata (`amount`, `label`, `message`) and
108/// one or more [`AvailablePaymentMethod`] the caller can present to the user.
109/// When parsed from a bare string (not a BIP 321 URI), `label` and `message`
110/// are `None` and `methods` contains a single entry.
111#[derive(Debug, Clone, PartialEq, Eq)]
112pub struct PaymentRequest {
113	pub amount: Option<Amount>,
114	pub label: Option<String>,
115	pub message: Option<String>,
116	pub options: Vec<AvailablePaymentMethod>,
117}
118
119impl From<AvailablePaymentMethod> for PaymentRequest {
120	fn from(option: AvailablePaymentMethod) -> Self {
121		Self {
122			amount: None,
123			label: None,
124			message: None,
125			options: vec![option],
126		}
127	}
128}
129
130/// Builder for constructing a [`Bip321Uri`] backed by a bark [`Wallet`].
131///
132/// Each setter records the intent; the actual address/invoice generation
133/// happens in [`build`](Self::build).
134///
135/// # Example
136///
137/// ```no_run
138/// # use bitcoin::Amount;
139/// # use bark::Wallet;
140/// # async fn example(wallet: &mut Wallet) -> anyhow::Result<()> {
141/// // Default URI has all options that don't require amount
142/// let uri = wallet.bip321_uri().build().await?;
143///
144/// // bitcoin:?ark=tark1pwh9vsmezqqpharv69q4z8m6x364d5m5prnmcalcalq9pdmzw0y7mpveck4pcfhezqypczkrrj3lkx5ue4qrf4jc7ztpt9htdttmh2judhqnu7aue8p0y9mq47jn9z
145/// println!("{}", uri.to_string());
146///
147/// // Add an amount to enable BOLT-11 invoice; can disable options as well
148/// let uri = wallet.bip321_uri()
149/// 	.amount(Amount::from_sat(100_000))
150/// 	.ark(false)
151/// 	.build().await?;
152///
153/// // bitcoin:?amount=100000&lightning=lnbc20m1pvjluezsp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygshp58yjmdan79s6qqdhdzgynm4zwqd5d7xmw5fk98klysy043l2ahrqspp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqfp4qrp33g0q5c5txsp9arysrx4k6zdkfs4nce4xj0gdcccefvpysxf3q9qrsgq9vlvyj8cqvq6ggvpwd53jncp9nwc47xlrsnenq2zp70fq83qlgesn4u3uyf4tesfkkwwfg3qs54qe426hp3tz7z6sweqdjg05axsrjqp9yrrwc
154/// println!("{}", uri.to_string());
155///
156/// # Ok(())
157/// # }
158/// ```
159pub struct BarkBip321UriBuilder<'a> {
160	wallet: &'a mut Wallet,
161	onchain_wallet: Option<&'a mut dyn GetAddress>,
162
163	amount: Option<Amount>,
164	label: Option<String>,
165	message: Option<String>,
166
167	ark: bool,
168	onchain: bool,
169	bolt11: bool,
170}
171
172impl<'a> BarkBip321UriBuilder<'a> {
173	pub fn new(wallet: &'a mut Wallet) -> Self {
174		Self {
175			wallet,
176			onchain_wallet: None,
177
178			amount: None,
179			label: None,
180			message: None,
181
182			ark: true,
183			onchain: true,
184			bolt11: true,
185		}
186	}
187
188	pub fn label(mut self, label: String) -> Self {
189		self.label = Some(label);
190		self
191	}
192
193	pub fn message(mut self, message: String) -> Self {
194		self.message = Some(message);
195		self
196	}
197
198	pub fn amount(mut self, amount: Amount) -> Self {
199		self.amount = Some(amount);
200		self
201	}
202
203	pub fn amount_sat(self, amount_sat: u64) -> Self {
204		self.amount(Amount::from_sat(amount_sat))
205	}
206
207	/// Disable all payment methods
208	///
209	/// You can then enable them one by one.
210	pub fn disable_all(self) -> Self {
211		self.onchain(false).ark(false).lightning_bolt11(false)
212	}
213
214	/// Include an onchain address destination in the URI
215	///
216	/// This will only work if the builder has an onchain wallet.
217	pub fn onchain(mut self, enabled: bool) -> Self {
218		self.onchain = enabled;
219		self
220	}
221
222	/// Set the onchain wallet to fetch onchain address from
223	///
224	/// Setting this will also set the flag to include an onchain address.
225	pub fn onchain_wallet(mut self, onchain: &'a mut dyn GetAddress) -> Self {
226		self.onchain_wallet = Some(onchain);
227		self.onchain = true;
228		self
229	}
230
231	/// Include an Ark address destination in the URI.
232	///
233	/// They are enabled by default.
234	pub fn ark(mut self, enabled: bool) -> Self {
235		self.ark = enabled;
236		self
237	}
238
239	/// Include a BOLT11 Lightning invoice destination in the URI.
240	///
241	/// Requires [`amount`](Self::amount) to have been called first,
242	/// because the builder needs an amount to generate the invoice.
243	///
244	/// This is enabled by default when an amount is given.
245	pub fn lightning_bolt11(mut self, enabled: bool) -> Self {
246		self.bolt11 = enabled;
247		self
248	}
249
250	/// Consume the builder, generate addresses/invoices, and return the URI.
251	pub async fn build(self) -> anyhow::Result<BarkBip321Uri> {
252		let mut uri = BarkBip321Uri::new();
253
254		if let Some(amount) = self.amount {
255			if amount == Amount::ZERO {
256				bail!("amount cannot be zero")
257			}
258			uri.set_amount(amount).context("failed to set amount")?;
259		}
260		if let Some(label) = self.label {
261			uri.set_label(label);
262		}
263		if let Some(message) = self.message {
264			uri.set_message(message);
265		}
266
267		if self.onchain {
268			if let Some(onchain) = self.onchain_wallet {
269				let address = onchain.address().await
270					.context("failed to get onchain address")?;
271				// As per BIP 321, onchain addresses are only supported on mainnet.
272				if self.wallet.network().await? == Network::Bitcoin {
273					uri.set_address(address.into_unchecked())
274						.context("failed to set address")?;
275				} else {
276					uri.push_tb(address.into_unchecked(), false)?;
277				}
278			}
279		}
280
281		if self.ark {
282			let address = self.wallet.new_address().await
283				.context("failed to generate new ark address")?;
284
285			uri.extensions_mut().ark.push(FieldWithAttributes::new(address, false));
286		}
287
288		if self.bolt11 {
289			if let Some(amount) = self.amount {
290				let invoice = self.wallet.bolt11_invoice(amount, None).await
291					.context("failed to generate lightning invoice")?;
292
293				uri.push_lightning(invoice, false);
294			} else {
295				debug!("amount is required to enable lightning invoice payment method");
296			}
297		}
298
299		let res = uri.validate();
300		debug_assert!(res.is_ok());
301
302		Ok(uri)
303	}
304}
305
306impl Wallet {
307	fn details_for_bolt11(
308		bolt11: &Bolt11Invoice,
309		network: Network,
310		uri_amount: Option<Amount>,
311	) -> AvailablePaymentMethod {
312		let mut errors = vec![];
313
314		if bolt11.network() != network {
315			errors.push(PaymentMethodParsingError::NetworkMismatch);
316		}
317
318		let bolt11_amount = bolt11.amount_milli_satoshis().map(|a| Amount::from_msat_ceil(a));
319		match (bolt11_amount, uri_amount) {
320			(Some(bolt11_amount), Some(amount)) => {
321				if bolt11_amount != amount {
322					errors.push(PaymentMethodParsingError::AmountMismatch {
323						expected: bolt11_amount,
324						got: amount,
325					});
326				}
327			},
328			_ => {},
329		}
330
331		AvailablePaymentMethod {
332			method: PaymentMethod::Invoice(Invoice::Bolt11(bolt11.clone())),
333			errors,
334		}
335	}
336
337	fn details_for_offer(
338		offer: &Offer,
339		network: Network,
340		uri_amount: Option<Amount>,
341	) -> AvailablePaymentMethod {
342		let mut errors = vec![];
343
344		// Check network
345		let network_chain = ChainHash::using_genesis_block_const(network);
346		if offer.chains().iter().all(|c| *c != network_chain) {
347			errors.push(PaymentMethodParsingError::NetworkMismatch);
348		}
349
350		let offer_amount = offer.amount().map(|a| a.to_bitcoin_amount().unwrap());
351		match (offer_amount, uri_amount) {
352			(Some(offer_amount), Some(amount)) => {
353				if offer_amount != amount {
354					errors.push(PaymentMethodParsingError::AmountMismatch { expected: offer_amount, got: amount });
355				}
356			},
357			_ => {},
358		}
359
360		AvailablePaymentMethod {
361			method: PaymentMethod::Offer(offer.clone()),
362			errors,
363		}
364	}
365
366	fn details_for_lightning_address(addr: &LightningAddress) -> AvailablePaymentMethod {
367		// We cannot validate network without fetching the invoice
368		AvailablePaymentMethod {
369			method: PaymentMethod::LightningAddress(addr.clone()),
370			errors: vec![],
371		}
372	}
373
374	fn details_for_bitcoin_address(
375		address: &bitcoin::Address<bitcoin::address::NetworkUnchecked>,
376		network: Network,
377	) -> AvailablePaymentMethod {
378		let mut errors = vec![];
379
380		if !address.is_valid_for_network(network) {
381			errors.push(PaymentMethodParsingError::NetworkMismatch);
382		}
383
384		AvailablePaymentMethod {
385			method: PaymentMethod::Bitcoin(address.clone()),
386			errors,
387		}
388	}
389
390	fn details_for_output_script(script: &bitcoin::ScriptBuf) -> AvailablePaymentMethod {
391
392		AvailablePaymentMethod {
393			method: PaymentMethod::OutputScript(script.clone()),
394			// We don't support sending to output scripts yet
395			errors: vec![PaymentMethodParsingError::Unsupported],
396		}
397	}
398
399	async fn details_for_ark_address(
400		&self,
401		ark_address: &ark::Address,
402	) -> AvailablePaymentMethod {
403		let mut errors = vec![];
404
405		match self.validate_arkoor_address(ark_address).await.err() {
406			None => {},
407			Some(e) => {
408				errors.push(PaymentMethodParsingError::InvalidArkAddress(e));
409			},
410		}
411
412		AvailablePaymentMethod {
413			method: PaymentMethod::Ark(ark_address.clone()),
414			errors,
415		}
416	}
417
418	async fn parse_bip321_uri(
419		&self,
420		network: Network,
421		uri: &BarkBip321Uri,
422	) -> anyhow::Result<PaymentRequest> {
423		let amount = uri.amount().map(|a| *a);
424		let label = uri.label().map(|l| l.clone());
425		let message = uri.message().map(|m| m.clone());
426
427		let mut options = Vec::new();
428
429		for extension in uri.bc() {
430			let details = Self::details_for_bitcoin_address(
431				&extension.inner().as_unchecked(), network
432			);
433			options.push(details);
434		}
435
436		for extension in uri.tb() {
437			let details = Self::details_for_bitcoin_address(
438				&extension.inner().as_unchecked(), network
439			);
440			options.push(details);
441		}
442
443		for extension in uri.lightning() {
444			let details = Self::details_for_bolt11(extension.inner(), network, amount);
445			options.push(details);
446		}
447
448		for extension in uri.lno() {
449			let details = Self::details_for_offer(extension.inner(), network, amount);
450			options.push(details);
451		}
452
453		for extension in uri.sp() {
454			if extension.required() {
455				bail!("Silent payment is required in URI but unsupported on Bark");
456			}
457		}
458
459		for extension in uri.pay() {
460			if extension.required() {
461				bail!("Private payment is required in URI but unsupported on Bark");
462			}
463		}
464
465		for extension in &uri.extensions().ark {
466			let details = self.details_for_ark_address(&extension.inner()).await;
467			options.push(details);
468		}
469
470		if let Some(address) = uri.address() {
471			let details = Self::details_for_bitcoin_address(
472				address.as_unchecked(), network
473			);
474			options.push(details);
475		}
476
477		return Ok(PaymentRequest { amount, label, message, options })
478	}
479
480	/// Try each supported payment format in priority order and return the
481	/// first successful parse as a [`PaymentRequest`].
482	///
483	/// Formats are attempted in this order:
484	/// 1. BIP 321 `bitcoin:` URI (may yield multiple options from destinations)
485	/// 2. Bare BOLT11 invoice
486	/// 3. Bare BOLT12 offer
487	/// 4. Lightning address (`user@domain`)
488	/// 5. Ark address
489	/// 6. Bare bitcoin address
490	/// 7. Hex-encoded output script
491	///
492	/// Returns `None` when `payment_str` does not match any known format.
493	async fn inner_parse_payment_request(
494		&self,
495		network: Network,
496		payment_str: &str,
497	) -> anyhow::Result<PaymentRequest> {
498		// BIP 321 URI
499		if let Ok(uri) = BarkBip321Uri::from_str(payment_str) {
500			return self.parse_bip321_uri(network, &uri).await;
501		}
502
503		// Bare BOLT11 invoice
504		if let Ok(bolt11) = Bolt11Invoice::from_str(payment_str) {
505			let details = Self::details_for_bolt11(&bolt11, network, None);
506
507			return Ok(PaymentRequest {
508				label: None,
509				amount: bolt11.amount_milli_satoshis().map(|a| Amount::from_msat_ceil(a)),
510				message: Some(bolt11.description().to_string()),
511				options: vec![details],
512			});
513		}
514
515		// Bare BOLT12 offer
516		if let Ok(offer) = Offer::from_str(payment_str) {
517			let details = Self::details_for_offer(&offer, network, None);
518
519			return Ok(PaymentRequest {
520				label: None,
521				amount: offer.amount().map(|a| a.to_bitcoin_amount().unwrap()),
522				message: offer.description().map(|d| d.to_string()),
523				options: vec![details],
524			});
525		}
526
527		// Lightning address
528		if let Ok(addr) = LightningAddress::from_str(payment_str) {
529			return Ok(Self::details_for_lightning_address(&addr).into());
530		}
531
532		// Ark address
533		if let Ok(ark_address) = ark::Address::from_str(payment_str) {
534			return Ok(self.details_for_ark_address(&ark_address).await.into());
535		}
536
537		// Bare bitcoin address
538		if let Ok(address) = bitcoin::Address::from_str(payment_str) {
539			return Ok(Self::details_for_bitcoin_address(&address, network).into());
540		}
541
542		// Hex-encoded output script
543		if let Ok(script) = bitcoin::ScriptBuf::from_hex(payment_str) {
544			return Ok(Self::details_for_output_script(&script).into());
545		}
546
547		bail!("No valid payment option found")
548	}
549
550	/// Parse a payment request into structured payment options.
551	///
552	/// Accepts any format supported by the wallet: BIP 321 URIs, BOLT11
553	/// invoices, BOLT12 offers, lightning addresses, hex output scripts,
554	/// bare bitcoin addresses, and ark addresses.
555	///
556	/// Formats are attempted in this order:
557	/// 1. BIP 321 `bitcoin:` URI (may yield multiple options from destinations)
558	/// 2. Bare BOLT11 invoice
559	/// 3. Bare BOLT12 offer
560	/// 4. Lightning address (`user@domain`)
561	/// 5. Ark address
562	/// 6. Bare bitcoin address
563	/// 7. Hex-encoded output script
564	///
565	/// Returns a [`PaymentRequest`] with one or more [`AvailablePaymentMethod`]
566	/// the caller can present to the user. Returns an error if no valid payment
567	/// option is found.
568	pub async fn parse_payment_request(&self, payment_str: &str)
569		-> anyhow::Result<PaymentRequest>
570	{
571		let network = self.network().await?;
572		let req = self.inner_parse_payment_request(
573			network, payment_str
574		).await.context("Invalid payment request")?;
575		debug_assert!(req.options.len() > 0, "Parser should bail if no valid payment option is found");
576
577		Ok(req)
578	}
579
580	/// Estimate fees for a single payment option.
581	///
582	/// Returns a [`FeeEstimate`] for the given [`AvailablePaymentMethod`] and amount.
583	pub async fn estimate_payment_fee(&self, option: &AvailablePaymentMethod, amount: Amount)
584		-> anyhow::Result<FeeEstimate>
585	{
586		match &option.method {
587			PaymentMethod::Invoice(_) => self.estimate_lightning_send_fee(amount).await,
588			PaymentMethod::Offer(_) => self.estimate_lightning_send_fee(amount).await,
589			PaymentMethod::LightningAddress(_) => self.estimate_lightning_send_fee(amount).await,
590			PaymentMethod::Bitcoin(address) => {
591				let addr = address.assume_checked_ref();
592				self.estimate_send_onchain(addr, amount).await
593			},
594			PaymentMethod::Ark(_) => self.estimate_arkoor_payment_fee(amount).await,
595			PaymentMethod::OutputScript(_) => bail!("Sending to output scripts is not supported yet"),
596			PaymentMethod::Custom(_) => bail!("Cannot estimate fees for custom payment method"),
597		}
598	}
599
600	/// Estimate fees for all payment options in a [`PaymentRequest`].
601	///
602	/// Returns a list of tuples containing the [`AvailablePaymentMethod`] and its [`FeeEstimate`].
603	/// The list is sorted by the gross amount of the fee estimate in ascending order.
604	pub async fn estimate_payment_fees(&self, request: PaymentRequest, amount: Option<Amount>)
605		-> anyhow::Result<Vec<(AvailablePaymentMethod, FeeEstimate)>>
606	{
607		let amount = match (amount, request.amount) {
608			(Some(amount), _) => amount,
609			(None, Some(amount)) => amount,
610			(None, None) => bail!("Amount is required to estimate fees"),
611		};
612
613		let mut options_with_fees = Vec::new();
614		for option in request.options {
615			let fee = self.estimate_payment_fee(&option, amount).await?;
616			options_with_fees.push((option, fee));
617		}
618
619		options_with_fees.sort_by_key(|(_, fee)| fee.gross_amount);
620
621		Ok(options_with_fees)
622	}
623
624	/// Create a builder for constructing a BIP 321 payment URI.
625	///
626	/// # Example
627	///
628	/// ```no_run
629	/// # use bitcoin::Amount;
630	/// # use bark::Wallet;
631	/// # async fn example(wallet: &mut Wallet) -> anyhow::Result<()> {
632	/// let mut builder = wallet.bip321_uri();
633	/// let uri = builder
634	///		.amount(Amount::from_sat(100_000))
635	/// 	.build().await?;
636	///
637	/// // bitcoin:?amount=100000&ark=tark1pwh9vsmezqqpharv69q4z8m6x364d5m5prnmcalcalq9pdmzw0y7mpveck4pcfhezqypczkrrj3lkx5ue4qrf4jc7ztpt9htdttmh2judhqnu7aue8p0y9mq47jn9z&lightning=lnbc20m1pvjluezsp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygshp58yjmdan79s6qqdhdzgynm4zwqd5d7xmw5fk98klysy043l2ahrqspp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqfp4qrp33g0q5c5txsp9arysrx4k6zdkfs4nce4xj0gdcccefvpysxf3q9qrsgq9vlvyj8cqvq6ggvpwd53jncp9nwc47xlrsnenq2zp70fq83qlgesn4u3uyf4tesfkkwwfg3qs54qe426hp3tz7z6sweqdjg05axsrjqp9yrrwc
638	/// println!("{}", uri.to_string());
639	///
640	/// # Ok(())
641	/// # }
642	/// ```
643	pub fn bip321_uri<'a>(&'a mut self) -> BarkBip321UriBuilder<'a> {
644		BarkBip321UriBuilder::new(self)
645	}
646}