1#![allow(unused_braces)] use 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
185impl 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
268impl 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 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}