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