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
11pub use crate::movement::PaymentMethod;
12
13use std::str::FromStr;
14
15use anyhow::Context;
16use bitcoin::{Amount, Network};
17use bitcoin::constants::ChainHash;
18use lnurllib::lightning_address::LightningAddress;
19
20use ark::lightning::{Bolt11Invoice, Invoice, Offer, OfferAmountExt};
21use bip321::{Bip321Error, Bip321Uri, ExtensionHandler, FieldWithAttributes};
22use bitcoin_ext::AmountExt;
23
24use crate::{FeeEstimate, Wallet};
25use crate::arkoor::ArkoorAddressError;
26
27#[derive(Default, Clone, PartialEq, Eq, Debug)]
28struct BarkExtension {
29	ark: Vec<FieldWithAttributes<ark::Address>>,
30}
31
32impl ExtensionHandler for BarkExtension {
33	fn handle_param(
34		&mut self,
35		key: &str,
36		value: &str,
37		required: bool,
38	) -> Result<bool, Bip321Error> {
39		if key == "ark" {
40			let address = ark::Address::from_str(value)
41				.map_err(|e| Bip321Error::ExtensionError(e.to_string()))?;
42			self.ark.push(FieldWithAttributes::new(address, required));
43			Ok(true)
44		} else {
45			Ok(false)
46		}
47	}
48
49	fn is_empty(&self) -> bool {
50		self.ark.is_empty()
51	}
52
53	fn serialize_params(&self) -> Vec<(String, String)> {
54		self.ark.iter()
55			.map(|a| ("ark".to_string(), a.inner().to_string()))
56			.collect()
57	}
58}
59
60type BarkBip321Uri = Bip321Uri<BarkExtension>;
61
62/// A non-fatal issue detected while validating a single payment option.
63///
64/// These are collected per-option in [`AvailablePaymentMethod::errors`] so
65/// callers can present all options to the user and let them choose, rather
66/// than failing on the first problem.
67#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
68pub enum PaymentMethodParsingError {
69	/// The payment target uses a different bitcoin network than the wallet.
70	#[error("network mismatch")]
71	NetworkMismatch,
72	/// The Ark address is invalid.
73	#[error("invalid ark address: {0}")]
74	InvalidArkAddress(#[from] ArkoorAddressError),
75	/// An amount is required but was not provided and cannot be inferred.
76	#[error("amount required")]
77	MissingAmount,
78	/// The provided amount does not satisfy the payment target's requirements.
79	#[error("amount mismatch: expected {expected}, got {got}")]
80	AmountMismatch { expected: Amount, got: Amount },
81	/// The payment target's amount is invalid.
82	#[error("invalid amount")]
83	InvalidAmount,
84	/// The payment option is not supported.
85	#[error("unsupported payment option")]
86	Unsupported,
87}
88
89/// A single payment option with its validation issues.
90///
91/// A option with a non-empty [`errors`](Self::errors) list may still be
92/// presented to the user, but should be flagged as problematic.
93#[derive(Debug, Clone, PartialEq, Eq)]
94pub struct AvailablePaymentMethod {
95	pub method: PaymentMethod,
96	pub errors: Vec<PaymentMethodParsingError>,
97}
98
99/// The result of parsing a payment string.
100///
101/// Contains optional BIP 321 metadata (`amount`, `label`, `message`) and
102/// one or more [`AvailablePaymentMethod`] the caller can present to the user.
103/// When parsed from a bare string (not a BIP 321 URI), `label` and `message`
104/// are `None` and `methods` contains a single entry.
105#[derive(Debug, Clone, PartialEq, Eq)]
106pub struct PaymentRequest {
107	pub amount: Option<Amount>,
108	pub label: Option<String>,
109	pub message: Option<String>,
110	pub options: Vec<AvailablePaymentMethod>,
111}
112
113impl From<AvailablePaymentMethod> for PaymentRequest {
114	fn from(option: AvailablePaymentMethod) -> Self {
115		Self {
116			amount: None,
117			label: None,
118			message: None,
119			options: vec![option],
120		}
121	}
122}
123
124impl Wallet {
125	fn details_for_bolt11(
126		bolt11: &Bolt11Invoice,
127		network: Network,
128		uri_amount: Option<Amount>,
129	) -> AvailablePaymentMethod {
130		let mut errors = vec![];
131
132		if bolt11.network() != network {
133			errors.push(PaymentMethodParsingError::NetworkMismatch);
134		}
135
136		let bolt11_amount = bolt11.amount_milli_satoshis().map(|a| Amount::from_msat_ceil(a));
137		match (bolt11_amount, uri_amount) {
138			(Some(bolt11_amount), Some(amount)) => {
139				if bolt11_amount != amount {
140					errors.push(PaymentMethodParsingError::AmountMismatch {
141						expected: bolt11_amount,
142						got: amount,
143					});
144				}
145			},
146			_ => {},
147		}
148
149		AvailablePaymentMethod {
150			method: PaymentMethod::Invoice(Invoice::Bolt11(bolt11.clone())),
151			errors,
152		}
153	}
154
155	fn details_for_offer(
156		offer: &Offer,
157		network: Network,
158		uri_amount: Option<Amount>,
159	) -> AvailablePaymentMethod {
160		let mut errors = vec![];
161
162		// Check network
163		let network_chain = ChainHash::using_genesis_block_const(network);
164		if offer.chains().iter().all(|c| *c != network_chain) {
165			errors.push(PaymentMethodParsingError::NetworkMismatch);
166		}
167
168		let offer_amount = offer.amount().map(|a| a.to_bitcoin_amount().unwrap());
169		match (offer_amount, uri_amount) {
170			(Some(offer_amount), Some(amount)) => {
171				if offer_amount != amount {
172					errors.push(PaymentMethodParsingError::AmountMismatch { expected: offer_amount, got: amount });
173				}
174			},
175			_ => {},
176		}
177
178		AvailablePaymentMethod {
179			method: PaymentMethod::Offer(offer.clone()),
180			errors,
181		}
182	}
183
184	fn details_for_lightning_address(addr: &LightningAddress) -> AvailablePaymentMethod {
185		// We cannot validate network without fetching the invoice
186		AvailablePaymentMethod {
187			method: PaymentMethod::LightningAddress(addr.clone()),
188			errors: vec![],
189		}
190	}
191
192	fn details_for_bitcoin_address(
193		address: &bitcoin::Address<bitcoin::address::NetworkUnchecked>,
194		network: Network,
195	) -> AvailablePaymentMethod {
196		let mut errors = vec![];
197
198		if !address.is_valid_for_network(network) {
199			errors.push(PaymentMethodParsingError::NetworkMismatch);
200		}
201
202		AvailablePaymentMethod {
203			method: PaymentMethod::Bitcoin(address.clone()),
204			errors,
205		}
206	}
207
208	fn details_for_output_script(script: &bitcoin::ScriptBuf) -> AvailablePaymentMethod {
209
210		AvailablePaymentMethod {
211			method: PaymentMethod::OutputScript(script.clone()),
212			// We don't support sending to output scripts yet
213			errors: vec![PaymentMethodParsingError::Unsupported],
214		}
215	}
216
217	async fn details_for_ark_address(
218		&self,
219		ark_address: &ark::Address,
220	) -> AvailablePaymentMethod {
221		let mut errors = vec![];
222
223		match self.validate_arkoor_address(ark_address).await.err() {
224			None => {},
225			Some(e) => {
226				errors.push(PaymentMethodParsingError::InvalidArkAddress(e));
227			},
228		}
229
230		AvailablePaymentMethod {
231			method: PaymentMethod::Ark(ark_address.clone()),
232			errors,
233		}
234	}
235
236	async fn parse_bip321_uri(
237		&self,
238		network: Network,
239		uri: &BarkBip321Uri,
240	) -> anyhow::Result<PaymentRequest> {
241		let amount = uri.amount().map(|a| *a);
242		let label = uri.label().map(|l| l.clone());
243		let message = uri.message().map(|m| m.clone());
244
245		let mut options = Vec::new();
246
247		for extension in uri.bc() {
248			let details = Self::details_for_bitcoin_address(
249				&extension.inner().as_unchecked(), network
250			);
251			options.push(details);
252		}
253
254		for extension in uri.tb() {
255			let details = Self::details_for_bitcoin_address(
256				&extension.inner().as_unchecked(), network
257			);
258			options.push(details);
259		}
260
261		for extension in uri.lightning() {
262			let details = Self::details_for_bolt11(extension.inner(), network, amount);
263			options.push(details);
264		}
265
266		for extension in uri.lno() {
267			let details = Self::details_for_offer(extension.inner(), network, amount);
268			options.push(details);
269		}
270
271		for extension in uri.sp() {
272			if extension.required() {
273				bail!("Silent payment is required in URI but unsupported on Bark");
274			}
275		}
276
277		for extension in uri.pay() {
278			if extension.required() {
279				bail!("Private payment is required in URI but unsupported on Bark");
280			}
281		}
282
283		for extension in &uri.extensions().ark {
284			let details = self.details_for_ark_address(&extension.inner()).await;
285			options.push(details);
286		}
287
288		if let Some(address) = uri.address() {
289			let details = Self::details_for_bitcoin_address(
290				address.as_unchecked(), network
291			);
292			options.push(details);
293		}
294
295		return Ok(PaymentRequest { amount, label, message, options })
296	}
297
298	/// Try each supported payment format in priority order and return the
299	/// first successful parse as a [`PaymentRequest`].
300	///
301	/// Formats are attempted in this order:
302	/// 1. BIP 321 `bitcoin:` URI (may yield multiple options from destinations)
303	/// 2. Bare BOLT11 invoice
304	/// 3. Bare BOLT12 offer
305	/// 4. Lightning address (`user@domain`)
306	/// 5. Ark address
307	/// 6. Bare bitcoin address
308	/// 7. Hex-encoded output script
309	///
310	/// Returns `None` when `payment_str` does not match any known format.
311	async fn inner_parse_payment_request(
312		&self,
313		network: Network,
314		payment_str: &str,
315	) -> anyhow::Result<PaymentRequest> {
316		// BIP 321 URI
317		if let Ok(uri) = BarkBip321Uri::from_str(payment_str) {
318			return self.parse_bip321_uri(network, &uri).await;
319		}
320
321		// Bare BOLT11 invoice
322		if let Ok(bolt11) = Bolt11Invoice::from_str(payment_str) {
323			let details = Self::details_for_bolt11(&bolt11, network, None);
324
325			return Ok(PaymentRequest {
326				label: None,
327				amount: bolt11.amount_milli_satoshis().map(|a| Amount::from_msat_ceil(a)),
328				message: Some(bolt11.description().to_string()),
329				options: vec![details],
330			});
331		}
332
333		// Bare BOLT12 offer
334		if let Ok(offer) = Offer::from_str(payment_str) {
335			let details = Self::details_for_offer(&offer, network, None);
336
337			return Ok(PaymentRequest {
338				label: None,
339				amount: offer.amount().map(|a| a.to_bitcoin_amount().unwrap()),
340				message: offer.description().map(|d| d.to_string()),
341				options: vec![details],
342			});
343		}
344
345		// Lightning address
346		if let Ok(addr) = LightningAddress::from_str(payment_str) {
347			return Ok(Self::details_for_lightning_address(&addr).into());
348		}
349
350		// Ark address
351		if let Ok(ark_address) = ark::Address::from_str(payment_str) {
352			return Ok(self.details_for_ark_address(&ark_address).await.into());
353		}
354
355		// Bare bitcoin address
356		if let Ok(address) = bitcoin::Address::from_str(payment_str) {
357			return Ok(Self::details_for_bitcoin_address(&address, network).into());
358		}
359
360		// Hex-encoded output script
361		if let Ok(script) = bitcoin::ScriptBuf::from_hex(payment_str) {
362			return Ok(Self::details_for_output_script(&script).into());
363		}
364
365		bail!("No valid payment option found")
366	}
367
368	/// Parse a payment request into structured payment options.
369	///
370	/// Accepts any format supported by the wallet: BIP 321 URIs, BOLT11
371	/// invoices, BOLT12 offers, lightning addresses, hex output scripts,
372	/// bare bitcoin addresses, and ark addresses.
373	///
374	/// Formats are attempted in this order:
375	/// 1. BIP 321 `bitcoin:` URI (may yield multiple options from destinations)
376	/// 2. Bare BOLT11 invoice
377	/// 3. Bare BOLT12 offer
378	/// 4. Lightning address (`user@domain`)
379	/// 5. Ark address
380	/// 6. Bare bitcoin address
381	/// 7. Hex-encoded output script
382	///
383	/// Returns a [`PaymentRequest`] with one or more [`AvailablePaymentMethod`]
384	/// the caller can present to the user. Returns an error if no valid payment
385	/// option is found.
386	pub async fn parse_payment_request(&self, payment_str: &str)
387		-> anyhow::Result<PaymentRequest>
388	{
389		let network = self.network().await?;
390		let req = self.inner_parse_payment_request(
391			network, payment_str
392		).await.context("Invalid payment request")?;
393		debug_assert!(req.options.len() > 0, "Parser should bail if no valid payment option is found");
394
395		Ok(req)
396	}
397
398	/// Estimate fees for a single payment option.
399	///
400	/// Returns a [`FeeEstimate`] for the given [`AvailablePaymentMethod`] and amount.
401	pub async fn estimate_payment_fee(&self, option: &AvailablePaymentMethod, amount: Amount)
402		-> anyhow::Result<FeeEstimate>
403	{
404		match &option.method {
405			PaymentMethod::Invoice(_) => self.estimate_lightning_send_fee(amount).await,
406			PaymentMethod::Offer(_) => self.estimate_lightning_send_fee(amount).await,
407			PaymentMethod::LightningAddress(_) => self.estimate_lightning_send_fee(amount).await,
408			PaymentMethod::Bitcoin(address) => {
409				let addr = address.assume_checked_ref();
410				self.estimate_send_onchain(addr, amount).await
411			},
412			PaymentMethod::Ark(_) => self.estimate_arkoor_payment_fee(amount).await,
413			PaymentMethod::OutputScript(_) => bail!("Sending to output scripts is not supported yet"),
414			PaymentMethod::Custom(_) => bail!("Cannot estimate fees for custom payment method"),
415		}
416	}
417
418	/// Estimate fees for all payment options in a [`PaymentRequest`].
419	///
420	/// Returns a list of tuples containing the [`AvailablePaymentMethod`] and its [`FeeEstimate`].
421	/// The list is sorted by the gross amount of the fee estimate in ascending order.
422	pub async fn estimate_payment_fees(&self, request: PaymentRequest, amount: Option<Amount>)
423		-> anyhow::Result<Vec<(AvailablePaymentMethod, FeeEstimate)>>
424	{
425		let amount = match (amount, request.amount) {
426			(Some(amount), _) => amount,
427			(None, Some(amount)) => amount,
428			(None, None) => bail!("Amount is required to estimate fees"),
429		};
430
431		let mut options_with_fees = Vec::new();
432		for option in request.options {
433			let fee = self.estimate_payment_fee(&option, amount).await?;
434			options_with_fees.push((option, fee));
435		}
436
437		options_with_fees.sort_by_key(|(_, fee)| fee.gross_amount);
438
439		Ok(options_with_fees)
440	}
441}