1use amplify::Slice32;
15use chrono::NaiveDateTime;
16#[cfg(feature = "serde")]
17use serde_with::{As, DisplayFromStr};
18use std::cmp::Ordering;
19use std::fmt::{self, Display, Formatter, Write};
20use std::io;
21use std::str::FromStr;
22
23#[cfg(feature = "rgb")]
24use amplify::Wrapper;
25#[cfg(feature = "rgb")]
26use bitcoin::hashes::sha256t;
27use bitcoin::hashes::{sha256d, Hash};
28use bitcoin::secp256k1::{self, schnorr};
29use bitcoin::Address;
30use bitcoin_scripts::hlc::HashLock;
31use bp::seals::txout::blind::ConcealedSeal;
32use commit_verify::merkle::MerkleNode;
33use internet2::addr::{NodeAddr, NodeId};
34use internet2::tlv;
35use lnp::p2p::bolt::{InitFeatures, ShortChannelId};
36use lnpbp::bech32::{self, Blob, FromBech32Str, ToBech32String};
37use lnpbp::chain::{AssetId, Chain};
38use miniscript::{descriptor::DescriptorPublicKey, Descriptor};
39use strict_encoding::{StrictDecode, StrictEncode};
40use wallet::psbt::Psbt;
41
42#[derive(
44 Copy, Clone, Ord, PartialOrd, Eq, PartialEq, Hash, Debug, Display, Error,
45)]
46#[display("the operation is supported only for RGB invoices")]
47pub struct NotRgbInvoice;
48
49#[cfg_attr(
52 feature = "serde",
53 serde_as,
54 derive(Serialize, Deserialize),
55 serde(crate = "serde_crate", rename_all = "camelCase")
56)]
57#[derive(
58 Getters, Clone, Eq, PartialEq, Debug, Display, NetworkEncode, NetworkDecode,
59)]
60#[network_encoding(use_tlv)]
61#[display(Invoice::to_bech32_string)]
62pub struct Invoice {
63 version: u8,
65
66 #[cfg_attr(feature = "serde", serde(with = "As::<DisplayFromStr>"))]
69 amount: AmountExt,
70
71 beneficiary: Beneficiary,
75
76 #[network_encoding(tlv = 0x01)]
79 alt_beneficiaries: Vec<Beneficiary>,
80
81 #[network_encoding(tlv = 0x02)]
84 #[cfg_attr(
85 feature = "serde",
86 serde(with = "As::<Option<DisplayFromStr>>")
87 )]
88 asset: Option<AssetId>,
89
90 #[network_encoding(tlv = 0x03)]
91 #[cfg_attr(
92 feature = "serde",
93 serde(with = "As::<Option<DisplayFromStr>>")
94 )]
95 expiry: Option<NaiveDateTime>, #[network_encoding(tlv = 0x04)]
99 recurrent: Recurrent,
100
101 #[network_encoding(tlv = 0x06)]
102 quantity: Option<Quantity>,
103
104 #[network_encoding(tlv = 0x08)]
108 currency_requirement: Option<CurrencyData>,
109
110 #[network_encoding(tlv = 0x05)]
111 merchant: Option<String>,
112
113 #[network_encoding(tlv = 0x07)]
114 purpose: Option<String>,
115
116 #[network_encoding(tlv = 0x09)]
117 details: Option<Details>,
118
119 #[network_encoding(tlv = 0x00)]
120 #[cfg_attr(
121 feature = "serde",
122 serde(with = "As::<Option<(DisplayFromStr, DisplayFromStr)>>")
123 )]
124 signature: Option<(secp256k1::PublicKey, schnorr::Signature)>,
125
126 #[network_encoding(tlv = 0x0a)]
128 consignment_endpoints: Vec<ConsignmentEndpoint>,
129
130 #[network_encoding(unknown_tlvs)]
131 #[cfg_attr(feature = "serde", serde(skip))]
132 unknown: tlv::Stream,
133}
134
135impl bech32::Strategy for Invoice {
136 const HRP: &'static str = "i";
137
138 type Strategy = bech32::strategies::CompressedStrictEncoding;
139}
140
141impl FromStr for Invoice {
142 type Err = bech32::Error;
143
144 fn from_str(s: &str) -> Result<Self, Self::Err> {
145 Invoice::from_bech32_str(s)
146 }
147}
148
149impl Ord for Invoice {
150 fn cmp(&self, other: &Self) -> Ordering {
151 self.to_string().cmp(&other.to_string())
152 }
153}
154
155impl PartialOrd for Invoice {
156 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
157 Some(self.cmp(other))
158 }
159}
160
161impl Invoice {
162 pub fn new(
163 beneficiary: Beneficiary,
164 amount: Option<u64>,
165 asset: Option<AssetId>,
166 ) -> Invoice {
167 Invoice {
168 version: 0,
169 amount: amount
170 .map(|value| AmountExt::Normal(value))
171 .unwrap_or(AmountExt::Any),
172 beneficiary,
173 alt_beneficiaries: vec![],
174 asset,
175 recurrent: Default::default(),
176 expiry: None,
177 quantity: None,
178 currency_requirement: None,
179 merchant: None,
180 purpose: None,
181 details: None,
182 signature: None,
183 consignment_endpoints: empty!(),
184 unknown: Default::default(),
185 }
186 }
187
188 pub fn with_descriptor(
189 descr: Descriptor<DescriptorPublicKey>,
190 amount: Option<u64>,
191 chain: &Chain,
192 ) -> Invoice {
193 Invoice::new(
194 Beneficiary::Descriptor(descr),
195 amount,
196 if chain == &Chain::Mainnet {
197 None
198 } else {
199 Some(chain.native_asset())
200 },
201 )
202 }
203
204 pub fn with_address(address: Address, amount: Option<u64>) -> Invoice {
205 let asset = if address.network != bitcoin::Network::Bitcoin {
206 Some(AssetId::native(&address.network.into()))
207 } else {
208 None
209 };
210 Invoice::new(Beneficiary::Address(address), amount, asset)
211 }
212
213 #[cfg(feature = "rgb")]
214 pub fn is_rgb(&self) -> bool {
215 self.rgb_asset().is_none()
216 }
217
218 #[cfg(feature = "rgb")]
219 pub fn rgb_asset(&self) -> Option<rgb::ContractId> {
220 self.asset.and_then(|asset_id| {
221 if *&[
222 Chain::Mainnet,
223 Chain::Signet,
224 Chain::LiquidV1,
225 Chain::Testnet3,
226 ]
227 .iter()
228 .map(Chain::native_asset)
229 .all(|id| id != asset_id)
230 {
231 Some(rgb::ContractId::from_inner(sha256t::Hash::from_inner(
232 asset_id.into_inner(),
233 )))
234 } else {
235 None
236 }
237 })
238 }
239
240 pub fn classify_asset(&self, chain: Option<Chain>) -> AssetClass {
241 match (self.asset, chain) {
242 (None, Some(Chain::Mainnet)) => AssetClass::Native,
243 (None, _) => AssetClass::InvalidNativeChain,
244 (Some(asset_id), Some(chain))
245 if asset_id == chain.native_asset() =>
246 {
247 AssetClass::Native
248 }
249 (Some(asset_id), _)
250 if *&[
251 Chain::Mainnet,
252 Chain::Signet,
253 Chain::LiquidV1,
254 Chain::Testnet3,
255 ]
256 .iter()
257 .map(Chain::native_asset)
258 .find(|id| id == &asset_id)
259 .is_some() =>
260 {
261 AssetClass::InvalidNativeChain
262 }
263 #[cfg(feature = "rgb")]
264 (Some(asset_id), _) => {
265 AssetClass::Rgb(rgb::ContractId::from_inner(
266 sha256t::Hash::from_inner(asset_id.into_inner()),
267 ))
268 }
269 #[cfg(not(feature = "rgb"))]
270 (Some(asset_id), _) => AssetClass::Other(asset_id),
271 }
272 }
273
274 pub fn beneficiaries(&self) -> BeneficiariesIter {
275 BeneficiariesIter {
276 invoice: self,
277 index: 0,
278 }
279 }
280
281 pub fn set_amount(&mut self, amount: AmountExt) -> bool {
282 if self.amount == amount {
283 return false;
284 }
285 self.amount = amount;
286 self.signature = None;
287 return true;
288 }
289
290 pub fn set_recurrent(&mut self, recurrent: Recurrent) -> bool {
291 if self.recurrent == recurrent {
292 return false;
293 }
294 self.recurrent = recurrent;
295 self.signature = None;
296 return true;
297 }
298
299 pub fn set_expiry(&mut self, expiry: NaiveDateTime) -> bool {
300 if self.expiry == Some(expiry) {
301 return false;
302 }
303 self.expiry = Some(expiry);
304 self.signature = None;
305 return true;
306 }
307
308 pub fn set_no_expiry(&mut self) -> bool {
309 if self.expiry == None {
310 return false;
311 }
312 self.expiry = None;
313 self.signature = None;
314 return true;
315 }
316
317 pub fn set_quantity(&mut self, quantity: Quantity) -> bool {
318 if self.quantity == Some(quantity) {
319 return false;
320 }
321 self.quantity = Some(quantity);
322 self.signature = None;
323 return true;
324 }
325
326 pub fn remove_quantity(&mut self) -> bool {
327 if self.quantity == None {
328 return false;
329 }
330 self.quantity = None;
331 self.signature = None;
332 return true;
333 }
334
335 pub fn set_currency_requirement(
336 &mut self,
337 currency_data: CurrencyData,
338 ) -> bool {
339 let currency_data = Some(currency_data);
340 if self.currency_requirement == currency_data {
341 return false;
342 }
343 self.currency_requirement = currency_data;
344 self.signature = None;
345 return true;
346 }
347
348 pub fn remove_currency_requirement(&mut self) -> bool {
349 if self.currency_requirement == None {
350 return false;
351 }
352 self.currency_requirement = None;
353 self.signature = None;
354 return true;
355 }
356
357 pub fn set_merchant(&mut self, merchant: String) -> bool {
358 let merchant = if merchant.is_empty() {
359 None
360 } else {
361 Some(merchant)
362 };
363 if self.merchant == merchant {
364 return false;
365 }
366 self.merchant = merchant;
367 self.signature = None;
368 return true;
369 }
370
371 pub fn remove_merchant(&mut self) -> bool {
372 if self.merchant == None {
373 return false;
374 }
375 self.merchant = None;
376 self.signature = None;
377 return true;
378 }
379
380 pub fn set_purpose(&mut self, purpose: String) -> bool {
381 let purpose = if purpose.is_empty() {
382 None
383 } else {
384 Some(purpose)
385 };
386 if self.purpose == purpose {
387 return false;
388 }
389 self.purpose = purpose;
390 self.signature = None;
391 return true;
392 }
393
394 pub fn remove_purpose(&mut self) -> bool {
395 if self.purpose == None {
396 return false;
397 }
398 self.purpose = None;
399 self.signature = None;
400 return true;
401 }
402
403 pub fn set_details(&mut self, details: Details) -> bool {
404 let details = Some(details);
405 if self.details == details {
406 return false;
407 }
408 self.details = details;
409 self.signature = None;
410 return true;
411 }
412
413 pub fn remove_details(&mut self) -> bool {
414 if self.details == None {
415 return false;
416 }
417 self.details = None;
418 self.signature = None;
419 return true;
420 }
421
422 #[cfg(feature = "rgb")]
423 pub fn add_consignment_endpoint(
424 &mut self,
425 node: ConsignmentEndpoint,
426 ) -> bool {
427 if self.consignment_endpoints.contains(&node) {
428 return false;
429 }
430 self.consignment_endpoints.push(node);
431 true
432 }
433
434 pub fn signature_hash(&self) -> MerkleNode {
435 MerkleNode::hash(
437 &self.strict_serialize().expect(
438 "invoice data are inconsistent for strict serialization",
439 ),
440 )
441 }
442
443 pub fn set_signature(
444 &mut self,
445 pubkey: secp256k1::PublicKey,
446 signature: schnorr::Signature,
447 ) {
448 self.signature = Some((pubkey, signature))
449 }
450
451 pub fn remove_signature(&mut self) {
452 self.signature = None
453 }
454}
455
456#[derive(Clone, Ord, PartialOrd, Eq, PartialEq, Hash, Debug)]
457#[non_exhaustive]
458pub enum AssetClass {
459 Native,
460 #[cfg(feature = "rgb")]
461 Rgb(rgb::ContractId),
462 #[cfg(not(feature = "rgb"))]
463 Other(AssetId),
464 InvalidNativeChain,
465}
466
467#[derive(Clone, PartialEq, Eq, Debug)]
468pub struct BeneficiariesIter<'a> {
469 invoice: &'a Invoice,
470 index: usize,
471}
472
473impl<'a> Iterator for BeneficiariesIter<'a> {
474 type Item = &'a Beneficiary;
475
476 fn next(&mut self) -> Option<Self::Item> {
477 self.index += 1;
478 if self.index == 1 {
479 Some(&self.invoice.beneficiary)
480 } else {
481 self.invoice.alt_beneficiaries.get(self.index - 2)
482 }
483 }
484}
485
486#[derive(
488 Clone,
489 Ord,
490 PartialOrd,
491 Eq,
492 PartialEq,
493 Hash,
494 Debug,
495 Display,
496 StrictEncode,
497 StrictDecode,
498)]
499#[cfg_attr(
500 feature = "serde",
501 derive(Serialize, Deserialize),
502 serde(crate = "serde_crate")
503)]
504#[display(inner)]
505#[non_exhaustive]
506pub enum ConsignmentEndpoint {
507 #[display("storm:{0}")]
509 Storm(NodeAddr),
510
511 #[display("rgbhttpjsonrpc:{0}")]
513 RgbHttpJsonRpc(String), }
515
516#[derive(
517 Copy, Clone, Ord, PartialOrd, Eq, PartialEq, Debug, Display, Error,
518)]
519#[display(doc_comments)]
520pub struct ConsignmentEndpointParseError;
522
523impl FromStr for ConsignmentEndpoint {
524 type Err = ConsignmentEndpointParseError;
525
526 fn from_str(s: &str) -> Result<Self, Self::Err> {
527 match s.split_once(":") {
528 Some((protocol, endpoint)) => match protocol {
529 "storm" => Ok(ConsignmentEndpoint::Storm(
530 NodeAddr::from_str(endpoint)
531 .or(Err(ConsignmentEndpointParseError))?,
532 )),
533 "rgbhttpjsonrpc" => Ok(ConsignmentEndpoint::RgbHttpJsonRpc(
534 endpoint.to_string(),
535 )),
536 _ => Err(ConsignmentEndpointParseError),
537 },
538 _ => Err(ConsignmentEndpointParseError),
539 }
540 }
541}
542
543#[derive(
544 Clone,
545 Copy,
546 PartialEq,
547 Eq,
548 PartialOrd,
549 Ord,
550 Hash,
551 Debug,
552 Display,
553 From,
554 StrictEncode,
555 StrictDecode,
556)]
557#[cfg_attr(
558 feature = "serde",
559 derive(Serialize, Deserialize),
560 serde(crate = "serde_crate", rename_all = "camelCase")
561)]
562#[non_exhaustive]
563pub enum Recurrent {
564 #[display("non-recurrent")]
565 NonRecurrent,
566
567 #[display("each {0} seconds")]
568 Seconds(u64),
569
570 #[display("each {0} months")]
571 Months(u8),
572
573 #[display("each {0} years")]
574 Years(u8),
575}
576
577impl Default for Recurrent {
578 fn default() -> Self {
579 Recurrent::NonRecurrent
580 }
581}
582
583impl Recurrent {
584 #[inline]
585 pub fn iter(&self) -> Recurrent {
586 *self
587 }
588}
589
590impl Iterator for Recurrent {
591 type Item = Recurrent;
592
593 #[inline]
594 fn next(&mut self) -> Option<Self::Item> {
595 match self {
596 Recurrent::NonRecurrent => None,
597 _ => Some(*self),
598 }
599 }
600}
601
602#[cfg_attr(
604 feature = "serde",
605 serde_as,
606 derive(Serialize, Deserialize),
607 serde(crate = "serde_crate", rename = "lowercase", untagged)
608)]
609#[derive(
610 Clone, Eq, PartialEq, Debug, Display, From, StrictEncode, StrictDecode,
611)]
612#[display(inner)]
613#[non_exhaustive]
614pub enum Beneficiary {
615 #[from]
618 Address(
619 #[cfg_attr(feature = "serde", serde(with = "As::<DisplayFromStr>"))]
620 Address,
621 ),
622
623 #[from]
627 BlindUtxo(ConcealedSeal),
628
629 #[from]
633 Descriptor(
634 #[cfg_attr(feature = "serde", serde(with = "As::<DisplayFromStr>"))]
635 Descriptor<DescriptorPublicKey>,
636 ),
637
638 #[from]
640 #[display("PSBT!")]
642 Psbt(Psbt),
643
644 #[from]
647 Bolt(LnAddress),
648
649 Unknown(
652 #[cfg_attr(feature = "serde", serde(with = "As::<DisplayFromStr>"))]
653 Blob,
654 ),
655}
656
657#[derive(
658 Copy, Clone, Ord, PartialOrd, Eq, PartialEq, Debug, Display, Error,
659)]
660#[display(doc_comments)]
661pub struct BeneficiaryParseError;
663
664impl FromStr for Beneficiary {
668 type Err = BeneficiaryParseError;
669
670 fn from_str(s: &str) -> Result<Self, Self::Err> {
671 if let Ok(address) = Address::from_str(s) {
672 Ok(Beneficiary::Address(address))
673 } else if let Ok(outpoint) = ConcealedSeal::from_str(s) {
674 Ok(Beneficiary::BlindUtxo(outpoint))
675 } else if let Ok(descriptor) =
676 Descriptor::<DescriptorPublicKey>::from_str(s)
677 {
678 Ok(Beneficiary::Descriptor(descriptor))
679 } else {
680 Err(BeneficiaryParseError)
681 }
682 }
683}
684
685#[cfg_attr(
686 feature = "serde",
687 serde_as,
688 derive(Serialize, Deserialize),
689 serde(crate = "serde_crate")
690)]
691#[derive(
692 Clone,
693 Ord,
694 PartialOrd,
695 Eq,
696 PartialEq,
697 Hash,
698 Debug,
699 Display,
700 StrictEncode,
701 StrictDecode,
702)]
703#[display("{node_id}")]
704pub struct LnAddress {
705 pub node_id: NodeId,
706 pub features: InitFeatures,
707 #[cfg_attr(feature = "serde", serde(with = "As::<DisplayFromStr>"))]
708 pub lock: HashLock, pub secret: Option<Slice32>,
713 pub network: Chain,
714 pub min_final_cltv_expiry: Option<u16>,
715 pub path_hints: Vec<LnPathHint>,
716}
717
718#[cfg_attr(
722 feature = "serde",
723 serde_as,
724 derive(Serialize, Deserialize),
725 serde(crate = "serde_crate")
726)]
727#[derive(
728 Copy,
729 Clone,
730 Ord,
731 PartialOrd,
732 Eq,
733 PartialEq,
734 Hash,
735 Debug,
736 Display,
737 StrictEncode,
738 StrictDecode,
739)]
740#[display("{short_channel_id}@{node_id}")]
741pub struct LnPathHint {
742 pub node_id: NodeId,
743 #[cfg_attr(feature = "serde", serde(with = "As::<DisplayFromStr>"))]
744 pub short_channel_id: ShortChannelId,
745 pub fee_base_msat: u32,
746 pub fee_proportional_millionths: u32,
747 pub cltv_expiry_delta: u16,
748}
749
750#[derive(
751 Copy,
752 Clone,
753 Ord,
754 PartialOrd,
755 Eq,
756 PartialEq,
757 Hash,
758 Debug,
759 Display,
760 From,
761 StrictEncode,
762 StrictDecode,
763)]
764pub enum AmountExt {
765 #[display("any")]
767 Any,
768
769 #[from]
770 #[display(inner)]
771 Normal(u64),
772
773 #[display("{0}.{1}")]
774 Milli(u64, u16),
775}
776
777impl Default for AmountExt {
778 fn default() -> Self {
779 AmountExt::Any
780 }
781}
782
783impl AmountExt {
784 pub fn atomic_value(&self) -> Option<u64> {
785 match self {
786 AmountExt::Any => None,
787 AmountExt::Normal(val) => Some(*val),
788 AmountExt::Milli(_, _) => None,
789 }
790 }
791}
792
793#[derive(
794 Copy, Clone, Ord, PartialOrd, Eq, PartialEq, Debug, Display, Error, From,
795)]
796#[display(doc_comments)]
797#[from(std::num::ParseIntError)]
798pub struct AmountParseError;
800
801impl FromStr for AmountExt {
802 type Err = AmountParseError;
803
804 fn from_str(s: &str) -> Result<Self, Self::Err> {
805 if s.trim().to_lowercase() == "any" {
806 return Ok(AmountExt::Any);
807 }
808 let mut split = s.split(".");
809 Ok(match (split.next(), split.next()) {
810 (Some(amt), None) => AmountExt::Normal(amt.parse()?),
811 (Some(int), Some(frac)) => {
812 AmountExt::Milli(int.parse()?, frac.parse()?)
813 }
814 _ => Err(AmountParseError)?,
815 })
816 }
817}
818
819#[derive(
820 Clone,
821 Ord,
822 PartialOrd,
823 Eq,
824 PartialEq,
825 Hash,
826 Debug,
827 Display,
828 StrictEncode,
829 StrictDecode,
830)]
831#[cfg_attr(
832 feature = "serde",
833 derive(Serialize, Deserialize),
834 serde(crate = "serde_crate")
835)]
836#[display("{source}#commitment")]
837pub struct Details {
838 #[cfg_attr(feature = "serde", serde(with = "As::<DisplayFromStr>"))]
839 pub commitment: sha256d::Hash,
840 pub source: String, }
842
843#[derive(Clone, Ord, PartialOrd, Eq, PartialEq, Hash, Debug, From)]
844pub struct Iso4217([u8; 3]);
846
847impl Display for Iso4217 {
848 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
849 f.write_char(self.0[0].into())?;
850 f.write_char(self.0[1].into())?;
851 f.write_char(self.0[2].into())
852 }
853}
854
855#[derive(
856 Copy, Clone, Ord, PartialOrd, Eq, PartialEq, Hash, Debug, Display, Error,
857)]
858#[display(doc_comments)]
859pub enum Iso4217Error {
860 WrongLen,
862}
863
864impl FromStr for Iso4217 {
865 type Err = Iso4217Error;
866
867 fn from_str(s: &str) -> Result<Self, Self::Err> {
868 if s.bytes().len() != 3 {
869 return Err(Iso4217Error::WrongLen);
870 }
871
872 let mut inner = [0u8; 3];
873 inner.copy_from_slice(&s.bytes().collect::<Vec<u8>>()[0..3]);
874 Ok(Iso4217(inner))
875 }
876}
877
878impl StrictEncode for Iso4217 {
879 fn strict_encode<E: io::Write>(
880 &self,
881 mut e: E,
882 ) -> Result<usize, strict_encoding::Error> {
883 e.write(&self.0)?;
884 Ok(3)
885 }
886}
887
888impl StrictDecode for Iso4217 {
889 fn strict_decode<D: io::Read>(
890 mut d: D,
891 ) -> Result<Self, strict_encoding::Error> {
892 let mut me = Self([0u8; 3]);
893 d.read_exact(&mut me.0)?;
894 Ok(me)
895 }
896}
897
898#[cfg_attr(
899 feature = "serde",
900 serde_as,
901 derive(Serialize, Deserialize),
902 serde(crate = "serde_crate")
903)]
904#[derive(
905 Clone,
906 Ord,
907 PartialOrd,
908 Eq,
909 PartialEq,
910 Hash,
911 Debug,
912 Display,
913 StrictEncode,
914 StrictDecode,
915)]
916#[display("{coins}.{fractions} {iso4217}")]
917pub struct CurrencyData {
918 #[cfg_attr(feature = "serde", serde(with = "As::<DisplayFromStr>"))]
919 pub iso4217: Iso4217,
920 pub coins: u32,
921 pub fractions: u8,
922 pub price_provider: String, }
924
925#[derive(
926 Copy,
927 Clone,
928 Ord,
929 PartialOrd,
930 Eq,
931 PartialEq,
932 Hash,
933 Debug,
934 From,
935 StrictEncode,
936 StrictDecode,
937)]
938#[cfg_attr(
939 feature = "serde",
940 derive(Serialize, Deserialize),
941 serde(crate = "serde_crate")
942)]
943pub struct Quantity {
944 pub min: u32, pub max: Option<u32>,
946 #[from]
947 pub default: u32,
948}
949
950impl Default for Quantity {
951 fn default() -> Self {
952 Self {
953 min: 0,
954 max: None,
955 default: 1,
956 }
957 }
958}
959
960impl Display for Quantity {
961 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
962 write!(f, "{} items", self.default)?;
963 match (self.min, self.max) {
964 (0, Some(max)) => write!(f, " (or any amount up to {})", max),
965 (0, None) => Ok(()),
966 (_, Some(max)) => write!(f, " (or from {} to {})", self.min, max),
967 (_, None) => write!(f, " (or any amount above {})", self.min),
968 }
969 }
970}