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