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