invoice/
base.rs

1// LNP/BP universal invoice library implementing LNPBP-38 standard
2// Written in 2021 by
3//     Dr. Maxim Orlovsky <orlovsky@pandoracore.com>
4//
5// To the extent possible under law, the author(s) have dedicated all
6// copyright and related and neighboring rights to this software to
7// the public domain worldwide. This software is distributed without
8// any warranty.
9//
10// You should have received a copy of the MIT License
11// along with this software.
12// If not, see <https://opensource.org/licenses/MIT>.
13
14use 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/// Error when an RGB-only operation is attempted on a non-RGB invoice.
43#[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/// NB: Invoice fields are non-public since each time we update them we must
50/// clear signature
51#[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 byte, always 0 for the initial version
64    version: u8,
65
66    /// Amount in the specified asset - a price per single item, if `quantity`
67    /// options is set
68    #[cfg_attr(feature = "serde", serde(with = "As::<DisplayFromStr>"))]
69    amount: AmountExt,
70
71    /// Main beneficiary. Separating the first beneficiary into a standalone
72    /// field allows to ensure that there is always at least one beneficiary
73    /// at compile time
74    beneficiary: Beneficiary,
75
76    /// List of beneficiary ordered in most desirable-first order, which follow
77    /// `beneficiary` value
78    #[network_encoding(tlv = 0x01)]
79    alt_beneficiaries: Vec<Beneficiary>,
80
81    /// AssetId can also be used to define blockchain. If it's empty it implies
82    /// bitcoin mainnet
83    #[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>, // Must be mapped to i64
96
97    /// Interval between recurrent payments
98    #[network_encoding(tlv = 0x04)]
99    recurrent: Recurrent,
100
101    #[network_encoding(tlv = 0x06)]
102    quantity: Option<Quantity>,
103
104    /// If the price of the asset provided by fiat provider URL goes below this
105    /// limit the merchant will not accept the payment and it will become
106    /// expired
107    #[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    /// List of nodes which are able to accept RGB consignment
127    #[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        // TODO: Change signature encoding algorithm to a merkle-tree based
436        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/// An endpoint to a consignment exchange medium.
487#[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    /// Storm protocol
508    #[display("storm:{0}")]
509    Storm(NodeAddr),
510
511    /// RGB HTTP JSON-RPC protocol
512    #[display("rgbhttpjsonrpc:{0}")]
513    RgbHttpJsonRpc(String), // Url,
514}
515
516#[derive(
517    Copy, Clone, Ord, PartialOrd, Eq, PartialEq, Debug, Display, Error,
518)]
519#[display(doc_comments)]
520/// Incorrect consignment endpoint format
521pub 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// TODO: Derive `Eq` & `Hash` once Psbt will support them
603#[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    /// Addresses are useful when you do not like to leak public key
616    /// information
617    #[from]
618    Address(
619        #[cfg_attr(feature = "serde", serde(with = "As::<DisplayFromStr>"))]
620        Address,
621    ),
622
623    /// Used by protocols that work with existing UTXOs and can assign some
624    /// client-validated data to them (like in RGB). We always hide the real
625    /// UTXO behind the hashed version (using some salt)
626    #[from]
627    BlindUtxo(ConcealedSeal),
628
629    /// Miniscript-based descriptors allowing custom derivation & key
630    /// generation
631    // TODO: Use Tracking account here
632    #[from]
633    Descriptor(
634        #[cfg_attr(feature = "serde", serde(with = "As::<DisplayFromStr>"))]
635        Descriptor<DescriptorPublicKey>,
636    ),
637
638    /// Full transaction template in PSBT format
639    #[from]
640    // TODO: Fix display once PSBT implement `Display`
641    #[display("PSBT!")]
642    Psbt(Psbt),
643
644    /// Lightning node receiving the payment. Not the same as lightning invoice
645    /// since many of the invoice data now will be part of [`Invoice`] here.
646    #[from]
647    Bolt(LnAddress),
648
649    // TODO: Add Bifrost invoices
650    /// Fallback option for all future variants
651    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)]
661/// Incorrect beneficiary format
662pub struct BeneficiaryParseError;
663
664// TODO: Since we can't present full beneficiary data in a string form (because
665//       of the lightning part) we have to remove this implementation once
666//       serde_with will be working
667impl 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, /* When PTLC will be available the same field will
709                         * be re-used + the use of
710                         * PTCL will be indicated with
711                         * a feature flag */
712    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/// Path hints for a lightning network payment, equal to the value of the `r`
719/// key of the lightning BOLT-11 invoice
720/// <https://github.com/lightningnetwork/lightning-rfc/blob/master/11-payment-encoding.md#tagged-fields>
721#[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    /// Payments for any amount is accepted: useful for charity/donations, etc
766    #[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)]
798/// Incorrect beneficiary format
799pub 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, // Url
841}
842
843#[derive(Clone, Ord, PartialOrd, Eq, PartialEq, Hash, Debug, From)]
844// TODO: Move to amplify library
845pub 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    /// Wrong string length to parse ISO4217 data
861    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, // Url,
923}
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, // We will default to zero
945    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}