rgbstd/stl/
specs.rs

1// RGB standard library for working with smart contracts on Bitcoin & Lightning
2//
3// SPDX-License-Identifier: Apache-2.0
4//
5// Written in 2019-2023 by
6//     Dr Maxim Orlovsky <orlovsky@lnp-bp.org>
7//
8// Copyright (C) 2019-2023 LNP/BP Standards Association. All rights reserved.
9//
10// Licensed under the Apache License, Version 2.0 (the "License");
11// you may not use this file except in compliance with the License.
12// You may obtain a copy of the License at
13//
14//     http://www.apache.org/licenses/LICENSE-2.0
15//
16// Unless required by applicable law or agreed to in writing, software
17// distributed under the License is distributed on an "AS IS" BASIS,
18// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
19// See the License for the specific language governing permissions and
20// limitations under the License.
21
22#![allow(unused_braces)] // caused by rustc unable to understand strict_dumb
23
24use std::fmt;
25use std::fmt::{Debug, Formatter};
26use std::iter::Sum;
27use std::str::FromStr;
28
29use amplify::ascii::AsciiString;
30use amplify::confinement::{Confined, NonEmptyString, NonEmptyVec, SmallOrdSet, SmallString, U8};
31use chrono::{DateTime, Local, NaiveDateTime, Utc};
32use strict_encoding::stl::{AlphaCapsNum, AsciiPrintable};
33use strict_encoding::{
34    InvalidIdent, StrictDeserialize, StrictDumb, StrictEncode, StrictSerialize, StrictType,
35    TypedWrite,
36};
37use strict_types::value::StrictNum;
38use strict_types::StrictVal;
39
40use super::{MediaType, ProofOfReserves, LIB_NAME_RGB_CONTRACT};
41
42#[derive(Clone, Eq, PartialEq, Hash, Debug, Default)]
43#[derive(StrictType, StrictEncode, StrictDecode)]
44#[strict_type(lib = LIB_NAME_RGB_CONTRACT)]
45#[cfg_attr(
46    feature = "serde",
47    derive(Serialize, Deserialize),
48    serde(crate = "serde_crate", rename_all = "camelCase")
49)]
50pub struct BurnMeta {
51    pub burn_proofs: SmallOrdSet<ProofOfReserves>,
52}
53impl StrictSerialize for BurnMeta {}
54impl StrictDeserialize for BurnMeta {}
55
56#[derive(Clone, Eq, PartialEq, Hash, Debug, Default)]
57#[derive(StrictType, StrictEncode, StrictDecode)]
58#[strict_type(lib = LIB_NAME_RGB_CONTRACT)]
59#[cfg_attr(
60    feature = "serde",
61    derive(Serialize, Deserialize),
62    serde(crate = "serde_crate", rename_all = "camelCase")
63)]
64pub struct IssueMeta {
65    pub reserves: SmallOrdSet<ProofOfReserves>,
66}
67impl StrictSerialize for IssueMeta {}
68impl StrictDeserialize for IssueMeta {}
69
70#[derive(
71    Wrapper, WrapperMut, Copy, Clone, Ord, PartialOrd, Eq, PartialEq, Hash, Debug, Default, From
72)]
73#[wrapper(Display, FromStr, Add, Sub, Mul, Div, Rem)]
74#[wrapper_mut(AddAssign, SubAssign, MulAssign, DivAssign, RemAssign)]
75#[derive(StrictType, StrictEncode, StrictDecode)]
76#[strict_type(lib = LIB_NAME_RGB_CONTRACT)]
77#[cfg_attr(
78    feature = "serde",
79    derive(Serialize, Deserialize),
80    serde(crate = "serde_crate", transparent)
81)]
82pub struct Amount(u64);
83
84impl StrictSerialize for Amount {}
85impl StrictDeserialize for Amount {}
86
87impl Amount {
88    pub fn zero() -> Self { Amount(0) }
89
90    pub fn one() -> Self { Amount(1) }
91
92    pub fn from_strict_val_unchecked(value: &StrictVal) -> Self {
93        value.unwrap_uint::<u64>().into()
94    }
95}
96
97impl Sum for Amount {
98    fn sum<I: Iterator<Item = Self>>(iter: I) -> Self {
99        iter.fold(Amount::zero(), |acc, i| acc + i)
100    }
101}
102
103#[derive(Copy, Clone, Ord, PartialOrd, Eq, PartialEq, Hash, Debug, Default)]
104#[repr(u8)]
105#[derive(StrictType, StrictEncode, StrictDecode)]
106#[strict_type(lib = LIB_NAME_RGB_CONTRACT, tags = repr, into_u8, try_from_u8)]
107#[cfg_attr(
108    feature = "serde",
109    derive(Serialize, Deserialize),
110    serde(crate = "serde_crate", rename_all = "camelCase")
111)]
112pub enum Precision {
113    Indivisible = 0,
114    Deci = 1,
115    Centi = 2,
116    Milli = 3,
117    DeciMilli = 4,
118    CentiMilli = 5,
119    Micro = 6,
120    DeciMicro = 7,
121    #[default]
122    CentiMicro = 8,
123    Nano = 9,
124    DeciNano = 10,
125    CentiNano = 11,
126    Pico = 12,
127    DeciPico = 13,
128    CentiPico = 14,
129    Femto = 15,
130    DeciFemto = 16,
131    CentiFemto = 17,
132    Atto = 18,
133}
134impl StrictSerialize for Precision {}
135impl StrictDeserialize for Precision {}
136
137impl Precision {
138    pub fn from_strict_val_unchecked(value: &StrictVal) -> Self { value.unwrap_enum() }
139}
140
141#[derive(Copy, Clone, Eq, PartialEq, Hash, Debug, Display)]
142#[display("{int}.{fract}")]
143pub struct CoinAmount {
144    pub int: u64,
145    pub fract: u64,
146    pub precision: Precision,
147}
148
149impl CoinAmount {
150    pub fn with(value: u64, precision: Precision) -> Self {
151        let pow = 10_u64.pow(precision as u32);
152        let int = value / pow;
153        let fract = value - int * pow;
154        CoinAmount {
155            int,
156            fract,
157            precision,
158        }
159    }
160}
161
162#[derive(Wrapper, Clone, Ord, PartialOrd, Eq, PartialEq, Hash, From)]
163#[wrapper(Deref, Display)]
164#[derive(StrictDumb, StrictType, StrictDecode)]
165#[strict_type(lib = LIB_NAME_RGB_CONTRACT, dumb = { Ticker::from("DUMB") })]
166#[cfg_attr(
167    feature = "serde",
168    derive(Serialize, Deserialize),
169    serde(crate = "serde_crate", transparent)
170)]
171pub struct Ticker(Confined<AsciiString, 1, 8>);
172impl StrictEncode for Ticker {
173    fn strict_encode<W: TypedWrite>(&self, writer: W) -> std::io::Result<W> {
174        let iter = self
175            .0
176            .as_bytes()
177            .iter()
178            .map(|c| AlphaCapsNum::try_from(*c).unwrap());
179        writer.write_newtype::<Self>(&NonEmptyVec::<AlphaCapsNum, 8>::try_from_iter(iter).unwrap())
180    }
181}
182impl StrictSerialize for Ticker {}
183impl StrictDeserialize for Ticker {}
184
185// TODO: Ensure all constructors filter invalid characters
186impl FromStr for Ticker {
187    type Err = InvalidIdent;
188
189    fn from_str(s: &str) -> Result<Self, Self::Err> {
190        let s = AsciiString::from_ascii(s.as_bytes())?;
191        Self::try_from(s)
192    }
193}
194
195impl From<&'static str> for Ticker {
196    fn from(s: &'static str) -> Self { Self::from_str(s).expect("invalid ticker name") }
197}
198
199impl TryFrom<String> for Ticker {
200    type Error = InvalidIdent;
201
202    fn try_from(s: String) -> Result<Self, Self::Error> {
203        let s = AsciiString::from_ascii(s.as_bytes())?;
204        Self::try_from(s)
205    }
206}
207
208impl TryFrom<AsciiString> for Ticker {
209    type Error = InvalidIdent;
210
211    fn try_from(ascii: AsciiString) -> Result<Self, InvalidIdent> {
212        if ascii.is_empty() {
213            return Err(InvalidIdent::Empty);
214        }
215        if let Some(ch) = ascii
216            .as_slice()
217            .iter()
218            .copied()
219            .find(|ch| AlphaCapsNum::try_from(ch.as_byte()).is_err())
220        {
221            return Err(InvalidIdent::InvalidChar(ascii, ch));
222        }
223        let s = Confined::try_from(ascii)?;
224        Ok(Self(s))
225    }
226}
227
228impl Debug for Ticker {
229    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
230        f.debug_tuple("Ticker").field(&self.as_str()).finish()
231    }
232}
233
234#[derive(Wrapper, Clone, Ord, PartialOrd, Eq, PartialEq, Hash, From)]
235#[wrapper(Deref, Display)]
236#[derive(StrictType, StrictDecode)]
237#[strict_type(lib = LIB_NAME_RGB_CONTRACT)]
238#[cfg_attr(
239    feature = "serde",
240    derive(Serialize, Deserialize),
241    serde(crate = "serde_crate", transparent)
242)]
243pub struct Name(Confined<AsciiString, 1, 40>);
244impl StrictEncode for Name {
245    fn strict_encode<W: TypedWrite>(&self, writer: W) -> std::io::Result<W> {
246        let iter = self
247            .0
248            .as_bytes()
249            .iter()
250            .map(|c| AsciiPrintable::try_from(*c).unwrap());
251        writer
252            .write_newtype::<Self>(&NonEmptyVec::<AsciiPrintable, 40>::try_from_iter(iter).unwrap())
253    }
254}
255impl StrictSerialize for Name {}
256impl StrictDeserialize for Name {}
257
258impl Name {
259    pub fn from_strict_val_unchecked(value: &StrictVal) -> Self {
260        Name::from_str(&value.unwrap_string()).unwrap()
261    }
262}
263
264impl StrictDumb for Name {
265    fn strict_dumb() -> Self { Name::from("Dumb contract name") }
266}
267
268// TODO: Ensure all constructors filter invalid characters
269impl FromStr for Name {
270    type Err = InvalidIdent;
271
272    fn from_str(s: &str) -> Result<Self, Self::Err> {
273        let s = AsciiString::from_ascii(s.as_bytes())?;
274        Self::try_from(s)
275    }
276}
277
278impl TryFrom<AsciiString> for Name {
279    type Error = InvalidIdent;
280
281    fn try_from(ascii: AsciiString) -> Result<Self, InvalidIdent> {
282        if ascii.is_empty() {
283            return Err(InvalidIdent::Empty);
284        }
285        if let Some(ch) = ascii
286            .as_slice()
287            .iter()
288            .copied()
289            .find(|ch| AsciiPrintable::try_from(ch.as_byte()).is_err())
290        {
291            return Err(InvalidIdent::InvalidChar(ascii, ch));
292        }
293        let s = Confined::try_from(ascii)?;
294        Ok(Self(s))
295    }
296}
297
298impl From<&'static str> for Name {
299    fn from(s: &'static str) -> Self { Self::from_str(s).expect("invalid ticker name") }
300}
301
302impl TryFrom<String> for Name {
303    type Error = InvalidIdent;
304
305    fn try_from(name: String) -> Result<Self, InvalidIdent> {
306        let name = AsciiString::from_ascii(name.as_bytes())?;
307        Self::try_from(name)
308    }
309}
310
311impl Debug for Name {
312    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
313        f.debug_tuple("ContractName").field(&self.as_str()).finish()
314    }
315}
316
317#[derive(Wrapper, Clone, Ord, PartialOrd, Eq, PartialEq, Hash, From)]
318#[wrapper(Deref, Display)]
319#[derive(StrictType, StrictEncode, StrictDecode)]
320#[strict_type(lib = LIB_NAME_RGB_CONTRACT)]
321#[cfg_attr(
322    feature = "serde",
323    derive(Serialize, Deserialize),
324    serde(crate = "serde_crate", transparent)
325)]
326pub struct Details(NonEmptyString<U8>);
327impl StrictSerialize for Details {}
328impl StrictDeserialize for Details {}
329
330impl Details {
331    pub fn from_strict_val_unchecked(value: &StrictVal) -> Self {
332        Details::from_str(&value.unwrap_string()).unwrap()
333    }
334}
335
336impl StrictDumb for Details {
337    fn strict_dumb() -> Self {
338        Self(Confined::try_from(s!("Dumb long description which is stupid and so on...")).unwrap())
339    }
340}
341
342impl FromStr for Details {
343    type Err = InvalidIdent;
344
345    fn from_str(s: &str) -> Result<Self, Self::Err> {
346        let s = Confined::try_from_iter(s.chars())?;
347        Ok(Self(s))
348    }
349}
350
351impl From<&'static str> for Details {
352    fn from(s: &'static str) -> Self { Self::from_str(s).expect("invalid ticker name") }
353}
354
355impl TryFrom<String> for Details {
356    type Error = InvalidIdent;
357
358    fn try_from(name: String) -> Result<Self, InvalidIdent> {
359        let s = Confined::try_from(name)?;
360        Ok(Self(s))
361    }
362}
363
364impl Debug for Details {
365    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
366        f.debug_tuple("ContractDetails")
367            .field(&self.as_str())
368            .finish()
369    }
370}
371
372#[derive(Clone, Eq, PartialEq, Debug)]
373#[derive(StrictDumb, StrictType, StrictEncode, StrictDecode)]
374#[strict_type(lib = LIB_NAME_RGB_CONTRACT)]
375#[cfg_attr(
376    feature = "serde",
377    derive(Serialize, Deserialize),
378    serde(crate = "serde_crate", rename_all = "camelCase")
379)]
380pub struct AssetNaming {
381    pub ticker: Ticker,
382    pub name: Name,
383    pub details: Option<Details>,
384}
385impl StrictSerialize for AssetNaming {}
386impl StrictDeserialize for AssetNaming {}
387
388impl AssetNaming {
389    pub fn new(ticker: &'static str, name: &'static str) -> AssetNaming {
390        AssetNaming {
391            ticker: Ticker::from(ticker),
392            name: Name::from(name),
393            details: None,
394        }
395    }
396
397    pub fn with(
398        ticker: &str,
399        name: &str,
400        details: Option<&str>,
401    ) -> Result<AssetNaming, InvalidIdent> {
402        Ok(AssetNaming {
403            ticker: Ticker::try_from(ticker.to_owned())?,
404            name: Name::try_from(name.to_owned())?,
405            details: details.map(Details::from_str).transpose()?,
406        })
407    }
408
409    pub fn from_strict_val_unchecked(value: &StrictVal) -> Self {
410        let ticker = value.unwrap_struct("ticker").unwrap_string();
411        let name = value.unwrap_struct("name").unwrap_string();
412        let details = value
413            .unwrap_struct("details")
414            .unwrap_option()
415            .map(StrictVal::unwrap_string);
416        AssetNaming {
417            ticker: Ticker::from_str(&ticker).expect("invalid asset ticker"),
418            name: Name::from_str(&name).expect("invalid asset name"),
419            details: details
420                .as_deref()
421                .map(Details::from_str)
422                .transpose()
423                .expect("invalid asset details"),
424        }
425    }
426}
427
428#[derive(Clone, Eq, PartialEq, Debug)]
429#[derive(StrictDumb, StrictType, StrictEncode, StrictDecode)]
430#[strict_type(lib = LIB_NAME_RGB_CONTRACT)]
431#[cfg_attr(
432    feature = "serde",
433    derive(Serialize, Deserialize),
434    serde(crate = "serde_crate", rename_all = "camelCase")
435)]
436pub struct DivisibleAssetSpec {
437    pub naming: AssetNaming,
438    pub precision: Precision,
439}
440impl StrictSerialize for DivisibleAssetSpec {}
441impl StrictDeserialize for DivisibleAssetSpec {}
442
443impl DivisibleAssetSpec {
444    pub fn new(
445        ticker: &'static str,
446        name: &'static str,
447        precision: Precision,
448    ) -> DivisibleAssetSpec {
449        DivisibleAssetSpec {
450            naming: AssetNaming::new(ticker, name),
451            precision,
452        }
453    }
454
455    pub fn with(
456        ticker: &str,
457        name: &str,
458        precision: Precision,
459        details: Option<&str>,
460    ) -> Result<DivisibleAssetSpec, InvalidIdent> {
461        Ok(DivisibleAssetSpec {
462            naming: AssetNaming::with(ticker, name, details)?,
463            precision,
464        })
465    }
466
467    pub fn from_strict_val_unchecked(value: &StrictVal) -> Self {
468        let naming = AssetNaming::from_strict_val_unchecked(value.unwrap_struct("naming"));
469        let precision = value.unwrap_struct("precision").unwrap_enum();
470        Self { naming, precision }
471    }
472
473    pub fn ticker(&self) -> &str { self.naming.ticker.as_str() }
474
475    pub fn name(&self) -> &str { self.naming.name.as_str() }
476
477    pub fn details(&self) -> Option<&str> { self.naming.details.as_ref().map(|d| d.as_str()) }
478}
479
480#[derive(Clone, Eq, PartialEq, Debug, Default)]
481#[derive(StrictType, StrictEncode, StrictDecode)]
482#[strict_type(lib = LIB_NAME_RGB_CONTRACT)]
483#[cfg_attr(
484    feature = "serde",
485    derive(Serialize, Deserialize),
486    serde(crate = "serde_crate", transparent)
487)]
488pub struct RicardianContract(SmallString);
489impl StrictSerialize for RicardianContract {}
490impl StrictDeserialize for RicardianContract {}
491
492impl FromStr for RicardianContract {
493    type Err = InvalidIdent;
494
495    fn from_str(s: &str) -> Result<Self, Self::Err> {
496        let s = Confined::try_from_iter(s.chars())?;
497        Ok(Self(s))
498    }
499}
500
501#[derive(Wrapper, WrapperMut, Copy, Clone, Ord, PartialOrd, Eq, PartialEq, Debug, From)]
502#[wrapper(Deref, Display, FromStr, MathOps)]
503#[wrapper_mut(DerefMut, MathAssign)]
504#[derive(StrictDumb, StrictType, StrictEncode, StrictDecode)]
505#[strict_type(lib = LIB_NAME_RGB_CONTRACT, dumb = Timestamp::start_of_epoch())]
506#[cfg_attr(
507    feature = "serde",
508    derive(Serialize, Deserialize),
509    serde(crate = "serde_crate", transparent)
510)]
511pub struct Timestamp(i64);
512impl StrictSerialize for Timestamp {}
513impl StrictDeserialize for Timestamp {}
514
515impl Timestamp {
516    pub fn start_of_epoch() -> Self { Timestamp(0) }
517
518    pub fn now() -> Self { Timestamp(Local::now().timestamp()) }
519
520    pub fn to_utc(self) -> Option<DateTime<Utc>> {
521        NaiveDateTime::from_timestamp_opt(self.0, 0)
522            .map(|naive| DateTime::<Utc>::from_naive_utc_and_offset(naive, Utc))
523    }
524
525    pub fn to_local(self) -> Option<DateTime<Local>> { self.to_utc().map(DateTime::<Local>::from) }
526
527    pub fn from_strict_val_unchecked(value: &StrictVal) -> Self {
528        // TODO: Move this logic to strict_types StrictVal::unwrap_int method
529        let StrictVal::Number(StrictNum::Int(val)) = value.skip_wrapper() else {
530            panic!("required integer number");
531        };
532        Self(*val as i64)
533    }
534}
535
536#[derive(Clone, Eq, PartialEq, Hash, Debug)]
537#[derive(StrictType, StrictDumb, StrictEncode, StrictDecode)]
538#[strict_type(lib = LIB_NAME_RGB_CONTRACT)]
539#[cfg_attr(
540    feature = "serde",
541    derive(Serialize, Deserialize),
542    serde(crate = "serde_crate", rename_all = "camelCase")
543)]
544pub struct Attachment {
545    #[strict_type(rename = "type")]
546    #[cfg_attr(feature = "serde", serde(rename = "type"))]
547    pub ty: MediaType,
548    pub digest: [u8; 32],
549}
550impl StrictSerialize for Attachment {}
551impl StrictDeserialize for Attachment {}
552
553impl Attachment {
554    pub fn from_strict_val_unchecked(value: &StrictVal) -> Self {
555        let ty = MediaType::from_strict_val_unchecked(value.unwrap_struct("type"));
556        let digest = value
557            .unwrap_struct("digest")
558            .unwrap_bytes()
559            .try_into()
560            .expect("invalid digest");
561        Self { ty, digest }
562    }
563}
564
565#[derive(Clone, Eq, PartialEq, Debug)]
566#[derive(StrictDumb, StrictType, StrictEncode, StrictDecode)]
567#[strict_type(lib = LIB_NAME_RGB_CONTRACT)]
568#[cfg_attr(
569    feature = "serde",
570    derive(Serialize, Deserialize),
571    serde(crate = "serde_crate", rename_all = "camelCase")
572)]
573pub struct ContractData {
574    pub terms: RicardianContract,
575    pub media: Option<Attachment>,
576}
577impl StrictSerialize for ContractData {}
578impl StrictDeserialize for ContractData {}
579
580impl ContractData {
581    pub fn from_strict_val_unchecked(value: &StrictVal) -> Self {
582        let terms = RicardianContract::from_str(&value.unwrap_struct("terms").unwrap_string())
583            .expect("invalid terms");
584        let media = value
585            .unwrap_struct("media")
586            .unwrap_option()
587            .map(Attachment::from_strict_val_unchecked);
588        Self { terms, media }
589    }
590}
591
592#[cfg(test)]
593mod test {
594    use super::*;
595
596    #[test]
597    fn coin_amount() {
598        #![allow(clippy::inconsistent_digit_grouping)]
599        let amount = CoinAmount::with(10_000_436_081_95, Precision::default());
600        assert_eq!(amount.int, 10_000);
601        assert_eq!(amount.fract, 436_081_95);
602        assert_eq!(format!("{amount}"), "10000.43608195");
603    }
604}