1pub 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#[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 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#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
121pub enum PaymentMethodParsingError {
122 #[error("network mismatch")]
124 NetworkMismatch,
125 #[error("invalid ark address: {0}")]
127 InvalidArkAddress(#[from] ArkoorAddressError),
128 #[error("amount mismatch: expected {expected}, got {got}")]
130 AmountMismatch { expected: Amount, got: Amount },
131 #[error("invalid amount")]
133 InvalidAmount,
134 #[error("unsupported payment option")]
136 Unsupported,
137}
138
139#[derive(Debug, Clone, PartialEq, Eq)]
144pub struct AvailablePaymentMethod {
145 pub method: PaymentMethod,
146 pub errors: Vec<PaymentMethodParsingError>,
147}
148
149#[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 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#[derive(Debug, Clone)]
197pub enum PaymentInitOutput {
198 Onchain(Txid),
199 Lightning(Invoice),
200 Ark,
201}
202
203pub struct BarkBip321UriBuilder<'a> {
233 wallet: &'a mut Wallet,
234 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 pub fn disable_all(self) -> Self {
285 self.onchain(false).ark(false).lightning_bolt11(false)
286 }
287
288 pub fn onchain(mut self, enabled: bool) -> Self {
292 self.onchain = enabled;
293 self
294 }
295
296 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 pub fn ark(mut self, enabled: bool) -> Self {
309 self.ark = enabled;
310 self
311 }
312
313 pub fn lightning_bolt11(mut self, enabled: bool) -> Self {
320 self.bolt11 = enabled;
321 self
322 }
323
324 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 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 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 AvailablePaymentMethod {
443 method: PaymentMethod::LightningAddress(addr.clone()),
444 errors: vec![],
445 }
446 }
447
448 fn details_for_lnurl(lnurl: &LnUrl) -> Option<AvailablePaymentMethod> {
449 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 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 async fn inner_parse_payment_request(
591 &self,
592 network: Network,
593 payment_str: &str,
594 ) -> anyhow::Result<PaymentRequest> {
595 if let Ok(uri) = BarkBip321Uri::from_str(payment_str) {
597 return self.parse_bip321_uri(network, &uri).await;
598 }
599
600 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 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 if let Ok(addr) = LightningAddress::from_str(payment_str) {
626 return Ok(Self::details_for_lightning_address(&addr).into());
627 }
628
629 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 if let Ok(addr) = ArkAddressType::from_str(payment_str) {
639 return Ok(self.details_for_ark_address(&addr).await.into());
640 }
641
642 if let Ok(address) = bitcoin::Address::from_str(payment_str) {
644 return Ok(Self::details_for_bitcoin_address(&address, network).into());
645 }
646
647 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 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 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 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 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 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 #[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 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 #[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}