oasis_runtime_sdk/types/
token.rs

1//! Token types.
2use std::{convert::TryFrom, fmt};
3
4/// Name/type of the token.
5#[derive(Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash, cbor::Encode)]
6#[cbor(transparent)]
7pub struct Denomination(Vec<u8>);
8
9impl Denomination {
10    /// Maximum length of a denomination.
11    pub const MAX_LENGTH: usize = 32;
12    /// Denomination in native token.
13    pub const NATIVE: Denomination = Denomination(Vec::new());
14
15    /// Whether the denomination represents the native token.
16    pub fn is_native(&self) -> bool {
17        self.0.is_empty()
18    }
19
20    /// Raw representation of a denomination.
21    pub fn into_vec(self) -> Vec<u8> {
22        self.0
23    }
24}
25
26impl AsRef<[u8]> for Denomination {
27    fn as_ref(&self) -> &[u8] {
28        &self.0
29    }
30}
31
32impl fmt::Display for Denomination {
33    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
34        if self.is_native() {
35            write!(f, "<native>")?;
36        } else {
37            write!(f, "{}", String::from_utf8_lossy(&self.0))?;
38        }
39        Ok(())
40    }
41}
42
43impl std::str::FromStr for Denomination {
44    type Err = Error;
45
46    fn from_str(v: &str) -> Result<Self, Self::Err> {
47        Self::try_from(v.as_bytes())
48    }
49}
50
51impl TryFrom<&[u8]> for Denomination {
52    type Error = Error;
53
54    fn try_from(bytes: &[u8]) -> Result<Self, Self::Error> {
55        if bytes.len() > Self::MAX_LENGTH {
56            return Err(Error::NameTooLong {
57                length: bytes.len(),
58            });
59        }
60        Ok(Self(bytes.to_vec()))
61    }
62}
63
64impl cbor::Decode for Denomination {
65    fn try_default() -> Result<Self, cbor::DecodeError> {
66        Ok(Default::default())
67    }
68
69    fn try_from_cbor_value(value: cbor::Value) -> Result<Self, cbor::DecodeError> {
70        match value {
71            cbor::Value::ByteString(data) => {
72                Self::try_from(data.as_ref()).map_err(|_| cbor::DecodeError::UnexpectedType)
73            }
74            _ => Err(cbor::DecodeError::UnexpectedType),
75        }
76    }
77}
78
79#[derive(Debug, thiserror::Error)]
80pub enum Error {
81    #[error(
82        "denomination name too long. received length {length} exceeded maximum of {}",
83        Denomination::MAX_LENGTH
84    )]
85    NameTooLong { length: usize },
86}
87
88/// Token amount of given denomination in base units.
89#[derive(Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord, cbor::Encode, cbor::Decode)]
90pub struct BaseUnits(pub u128, pub Denomination);
91
92impl BaseUnits {
93    /// Creates a new token amount of the given denomination.
94    pub const fn new(amount: u128, denomination: Denomination) -> Self {
95        BaseUnits(amount, denomination)
96    }
97
98    /// Creates a new token amount with native denomination.
99    pub const fn native(amount: u128) -> Self {
100        BaseUnits(amount, Denomination::NATIVE)
101    }
102
103    /// Token amount in base units.
104    pub fn amount(&self) -> u128 {
105        self.0
106    }
107
108    /// Mutable token amount in base units.
109    pub fn amount_mut(&mut self) -> &mut u128 {
110        &mut self.0
111    }
112
113    /// Denomination of the token amount.
114    pub fn denomination(&self) -> &Denomination {
115        &self.1
116    }
117}
118
119impl fmt::Display for BaseUnits {
120    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
121        write!(f, "{} {}", self.0, self.1)?;
122        Ok(())
123    }
124}
125
126#[cfg(test)]
127mod test {
128    use super::*;
129
130    #[test]
131    fn test_basic() {
132        let cases = vec![
133            // Native denomination.
134            (0, Denomination::NATIVE, "824040"),
135            (1, Denomination::NATIVE, "82410140"),
136            (1000, Denomination::NATIVE, "824203e840"),
137            // Custom denomination.
138            (0, "test".parse().unwrap(), "82404474657374"),
139            (1, "test".parse().unwrap(), "8241014474657374"),
140            (1000, "test".parse().unwrap(), "824203e84474657374"),
141        ];
142
143        for tc in cases {
144            let token = BaseUnits::new(tc.0, tc.1);
145            let enc = cbor::to_vec(token.clone());
146            assert_eq!(hex::encode(&enc), tc.2, "serialization should match");
147
148            let dec: BaseUnits = cbor::from_slice(&enc).expect("deserialization should succeed");
149            assert_eq!(dec, token, "serialization should round-trip");
150        }
151    }
152
153    #[test]
154    fn test_decoding_denomination() {
155        macro_rules! assert_rountrip_ok {
156            ($bytes:expr) => {
157                let enc = cbor::to_vec($bytes.to_vec());
158                let dec: Denomination = cbor::from_slice(&enc).unwrap();
159                assert_eq!(dec, Denomination::try_from($bytes).unwrap());
160                assert_eq!(dec.0, $bytes);
161            };
162        }
163
164        let bytes_fixture = vec![42u8; Denomination::MAX_LENGTH + 1];
165
166        assert_rountrip_ok!(&bytes_fixture[0..0]);
167        assert_rountrip_ok!(&bytes_fixture[0..1]);
168        assert_rountrip_ok!(&bytes_fixture[0..Denomination::MAX_LENGTH]);
169
170        // Too long denomination:
171        let dec_result: Result<Denomination, _> = cbor::from_slice(&cbor::to_vec(bytes_fixture));
172        assert!(dec_result.is_err());
173    }
174}