nym_types/
currency.rs

1use crate::error::TypesError;
2use cosmwasm_std::Fraction;
3use cosmwasm_std::{Decimal, Uint128};
4use nym_config::defaults::{DenomDetails, DenomDetailsOwned, NymNetworkDetails};
5use nym_validator_client::nyxd::Coin;
6use schemars::JsonSchema;
7use serde::{Deserialize, Serialize};
8use std::cmp::Ordering;
9use std::collections::HashMap;
10use std::fmt::{Display, Formatter};
11use strum_macros::{Display, EnumString, VariantNames};
12
13#[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))]
14#[cfg_attr(
15    feature = "generate-ts",
16    ts(
17        export,
18        export_to = "ts-packages/types/src/types/rust/CurrencyDenom.ts"
19    )
20)]
21#[cfg_attr(feature = "generate-ts", ts(rename_all = "lowercase"))]
22#[derive(
23    Display,
24    Default,
25    Serialize,
26    Deserialize,
27    Clone,
28    Debug,
29    EnumString,
30    VariantNames,
31    PartialEq,
32    Eq,
33    JsonSchema,
34)]
35#[serde(rename_all = "lowercase")]
36#[strum(serialize_all = "lowercase")]
37pub enum CurrencyDenom {
38    #[strum(ascii_case_insensitive)]
39    #[default]
40    Unknown,
41    #[strum(ascii_case_insensitive)]
42    Nym,
43    #[strum(ascii_case_insensitive)]
44    Nymt,
45    #[strum(ascii_case_insensitive)]
46    Nyx,
47    #[strum(ascii_case_insensitive)]
48    Nyxt,
49}
50
51pub type Denom = String;
52
53#[derive(Debug, Default)]
54pub struct RegisteredCoins(HashMap<Denom, CoinMetadata>);
55
56impl RegisteredCoins {
57    pub fn default_denoms(network: &NymNetworkDetails) -> Self {
58        let mut network_coins = HashMap::new();
59        network_coins.insert(
60            network.chain_details.mix_denom.base.clone(),
61            network.chain_details.mix_denom.clone().into(),
62        );
63        network_coins.insert(
64            network.chain_details.stake_denom.base.clone(),
65            network.chain_details.stake_denom.clone().into(),
66        );
67        RegisteredCoins(network_coins)
68    }
69
70    pub fn insert(&mut self, denom: Denom, metadata: CoinMetadata) -> Option<CoinMetadata> {
71        self.0.insert(denom, metadata)
72    }
73
74    pub fn remove(&mut self, denom: &Denom) -> Option<CoinMetadata> {
75        self.0.remove(denom)
76    }
77
78    pub fn attempt_create_display_coin_from_base_dec_amount(
79        &self,
80        denom: &Denom,
81        base_amount: Decimal,
82    ) -> Result<DecCoin, TypesError> {
83        for registered_coin in self.0.values() {
84            if let Some(exponent) = registered_coin.get_exponent(denom) {
85                // if this fails it means we haven't registered our display denom which honestly should never be the case
86                // unless somebody is rocking their own custom network
87                let display_exponent = registered_coin
88                    .get_exponent(&registered_coin.display)
89                    .ok_or_else(|| TypesError::UnknownCoinDenom(denom.clone()))?;
90
91                return match exponent.cmp(&display_exponent) {
92                    Ordering::Greater => {
93                        // we need to scale up, unlikely to ever be needed but included for completion sake,
94                        // for example if we decided to created knym with exponent 9 and wanted to convert to nym with exponent 6
95                        Ok(DecCoin {
96                            denom: denom.into(),
97                            amount: try_scale_up_decimal(base_amount, exponent - display_exponent)?,
98                        })
99                    }
100                    // we're already in the display denom
101                    Ordering::Equal => Ok(DecCoin {
102                        denom: denom.into(),
103                        amount: base_amount,
104                    }),
105                    Ordering::Less => {
106                        // we need to scale down, the most common case, for example we're in base unym with exponent 0 and want to convert to nym with exponent 6
107                        Ok(DecCoin {
108                            denom: denom.into(),
109                            amount: try_scale_down_decimal(
110                                base_amount,
111                                display_exponent - exponent,
112                            )?,
113                        })
114                    }
115                };
116            }
117        }
118
119        Err(TypesError::UnknownCoinDenom(denom.clone()))
120    }
121
122    pub fn attempt_convert_to_base_coin(&self, coin: DecCoin) -> Result<Coin, TypesError> {
123        // check if this is already in the base denom
124        if self.0.contains_key(&coin.denom) {
125            // if we're converting a base DecCoin it CANNOT fail, unless somebody is providing
126            // bullshit data on purpose : )
127            return coin.try_into();
128        } else {
129            // TODO: this kinda suggests we may need a better data structure
130            for registered_coin in self.0.values() {
131                if let Some(exponent) = registered_coin.get_exponent(&coin.denom) {
132                    let amount = try_convert_decimal_to_u128(coin.try_scale_up_value(exponent)?)?;
133                    return Ok(Coin::new(amount, &registered_coin.base));
134                }
135            }
136        }
137        Err(TypesError::UnknownCoinDenom(coin.denom))
138    }
139
140    pub fn attempt_convert_to_display_dec_coin(&self, coin: Coin) -> Result<DecCoin, TypesError> {
141        for registered_coin in self.0.values() {
142            if let Some(exponent) = registered_coin.get_exponent(&coin.denom) {
143                // if this fails it means we haven't registered our display denom which honestly should never be the case
144                // unless somebody is rocking their own custom network
145                let display_exponent = registered_coin
146                    .get_exponent(&registered_coin.display)
147                    .ok_or_else(|| TypesError::UnknownCoinDenom(coin.denom.clone()))?;
148
149                return match exponent.cmp(&display_exponent) {
150                    Ordering::Greater => {
151                        // we need to scale up, unlikely to ever be needed but included for completion sake,
152                        // for example if we decided to created knym with exponent 9 and wanted to convert to nym with exponent 6
153                        Ok(DecCoin::new_scaled_up(
154                            coin.amount,
155                            &registered_coin.display,
156                            exponent - display_exponent,
157                        )?)
158                    }
159                    // we're already in the display denom
160                    Ordering::Equal => Ok(coin.into()),
161                    Ordering::Less => {
162                        // we need to scale down, the most common case, for example we're in base unym with exponent 0 and want to convert to nym with exponent 6
163                        Ok(DecCoin::new_scaled_down(
164                            coin.amount,
165                            &registered_coin.display,
166                            display_exponent - exponent,
167                        )?)
168                    }
169                };
170            }
171        }
172
173        Err(TypesError::UnknownCoinDenom(coin.denom))
174    }
175}
176
177// TODO: should this live here?
178// attempts to replicate cosmos-sdk's coin metadata
179// https://docs.cosmos.network/master/architecture/adr-024-coin-metadata.html
180// this way we could more easily handle multiple coin types simultaneously (like nym/nyx/nymt/nyx + local currencies)
181#[derive(Debug)]
182pub struct DenomUnit {
183    pub denom: Denom,
184    pub exponent: u32,
185    // pub aliases: Vec<String>,
186}
187
188impl DenomUnit {
189    pub fn new(denom: Denom, exponent: u32) -> Self {
190        DenomUnit { denom, exponent }
191    }
192}
193
194#[derive(Debug)]
195pub struct CoinMetadata {
196    pub denom_units: Vec<DenomUnit>,
197    pub base: Denom,
198    pub display: Denom,
199}
200
201impl CoinMetadata {
202    pub fn new(denom_units: Vec<DenomUnit>, base: Denom, display: Denom) -> Self {
203        CoinMetadata {
204            denom_units,
205            base,
206            display,
207        }
208    }
209
210    pub fn get_exponent(&self, denom: &str) -> Option<u32> {
211        self.denom_units
212            .iter()
213            .find(|denom_unit| denom_unit.denom == denom)
214            .map(|denom_unit| denom_unit.exponent)
215    }
216}
217
218impl From<DenomDetails> for CoinMetadata {
219    fn from(denom_details: DenomDetails) -> Self {
220        CoinMetadata::new(
221            vec![
222                DenomUnit::new(denom_details.base.into(), 0),
223                DenomUnit::new(denom_details.display.into(), denom_details.display_exponent),
224            ],
225            denom_details.base.into(),
226            denom_details.display.into(),
227        )
228    }
229}
230
231impl From<DenomDetailsOwned> for CoinMetadata {
232    fn from(denom_details: DenomDetailsOwned) -> Self {
233        CoinMetadata::new(
234            vec![
235                DenomUnit::new(denom_details.base.clone(), 0),
236                DenomUnit::new(
237                    denom_details.display.clone(),
238                    denom_details.display_exponent,
239                ),
240            ],
241            denom_details.base,
242            denom_details.display,
243        )
244    }
245}
246
247// tries to semi-replicate cosmos-sdk's DecCoin for being able to handle tokens with decimal amounts
248// https://github.com/cosmos/cosmos-sdk/blob/v0.45.4/types/dec_coin.go
249#[derive(Serialize, Deserialize, Clone, Debug, Eq, PartialEq, JsonSchema)]
250#[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))]
251#[cfg_attr(
252    feature = "generate-ts",
253    ts(export, export_to = "ts-packages/types/src/types/rust/DecCoin.ts")
254)]
255pub struct DecCoin {
256    #[cfg_attr(feature = "generate-ts", ts(as = "CurrencyDenom"))]
257    pub denom: Denom,
258    // Decimal is already serialized to string and using string in its schema, so lets also go straight to string for ts_rs
259    // todo: is `Decimal` the correct type to use? Do we want to depend on cosmwasm_std here?
260    #[cfg_attr(feature = "generate-ts", ts(type = "string"))]
261    pub amount: Decimal,
262}
263
264impl DecCoin {
265    pub fn new_base<S: Into<String>>(amount: impl Into<Uint128>, denom: S) -> Self {
266        DecCoin {
267            denom: denom.into(),
268            amount: Decimal::from_atomics(amount, 0).unwrap(),
269        }
270    }
271
272    pub fn zero<S: Into<String>>(denom: S) -> Self {
273        DecCoin {
274            denom: denom.into(),
275            amount: Decimal::zero(),
276        }
277    }
278
279    pub fn new_scaled_up<S: Into<String>>(
280        base_amount: impl Into<Uint128>,
281        denom: S,
282        exponent: u32,
283    ) -> Result<Self, TypesError> {
284        let base_amount = Decimal::from_atomics(base_amount, 0).unwrap();
285        Ok(DecCoin {
286            denom: denom.into(),
287            amount: try_scale_up_decimal(base_amount, exponent)?,
288        })
289    }
290
291    pub fn new_scaled_down<S: Into<String>>(
292        base_amount: impl Into<Uint128>,
293        denom: S,
294        exponent: u32,
295    ) -> Result<Self, TypesError> {
296        let base_amount = Decimal::from_atomics(base_amount, 0).unwrap();
297        Ok(DecCoin {
298            denom: denom.into(),
299            amount: try_scale_down_decimal(base_amount, exponent)?,
300        })
301    }
302
303    pub fn try_scale_down_value(&self, exponent: u32) -> Result<Decimal, TypesError> {
304        try_scale_down_decimal(self.amount, exponent)
305    }
306
307    pub fn try_scale_up_value(&self, exponent: u32) -> Result<Decimal, TypesError> {
308        try_scale_up_decimal(self.amount, exponent)
309    }
310}
311
312// TODO: should thoese live here?
313pub fn try_scale_down_decimal(dec: Decimal, exponent: u32) -> Result<Decimal, TypesError> {
314    let rhs = 10u128
315        .checked_pow(exponent)
316        .ok_or(TypesError::UnsupportedExponent(exponent))?;
317    let denominator = dec
318        .denominator()
319        .checked_mul(rhs.into())
320        .map_err(|_| TypesError::UnsupportedExponent(exponent))?;
321
322    Ok(Decimal::from_ratio(dec.numerator(), denominator))
323}
324
325pub fn try_scale_up_decimal(dec: Decimal, exponent: u32) -> Result<Decimal, TypesError> {
326    let rhs = 10u128
327        .checked_pow(exponent)
328        .ok_or(TypesError::UnsupportedExponent(exponent))?;
329    let denominator = dec
330        .denominator()
331        .checked_div(rhs.into())
332        .map_err(|_| TypesError::UnsupportedExponent(exponent))?;
333
334    Ok(Decimal::from_ratio(dec.numerator(), denominator))
335}
336
337pub fn try_convert_decimal_to_u128(dec: Decimal) -> Result<u128, TypesError> {
338    let whole = dec.numerator() / dec.denominator();
339
340    // unwrap is fine as we're not dividing by zero here
341    let fractional = (dec.numerator()).checked_rem(dec.denominator()).unwrap();
342
343    // we cannot convert as we'd lose our decimal places
344    // (for example if somebody attempted to represent our gas price (WHICH YOU SHOULDN'T DO) as DecCoin)
345    if fractional != Uint128::zero() {
346        return Err(TypesError::LossyCoinConversion);
347    }
348    Ok(whole.u128())
349}
350
351impl Display for DecCoin {
352    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
353        write!(f, "{} {}", self.amount, self.denom)
354    }
355}
356
357impl From<Coin> for DecCoin {
358    fn from(coin: Coin) -> Self {
359        DecCoin::new_base(coin.amount, coin.denom)
360    }
361}
362
363// this conversion assumes same denomination
364impl TryFrom<DecCoin> for Coin {
365    type Error = TypesError;
366
367    fn try_from(value: DecCoin) -> Result<Self, Self::Error> {
368        Ok(Coin {
369            amount: try_convert_decimal_to_u128(value.try_scale_down_value(0)?)?,
370            denom: value.denom,
371        })
372    }
373}
374
375#[cfg(test)]
376mod test {
377    use super::*;
378
379    #[test]
380    fn dec_value_scale_down() {
381        let dec = DecCoin {
382            denom: "foo".to_string(),
383            amount: "1234007000".parse().unwrap(),
384        };
385
386        assert_eq!(
387            "1234007000".parse::<Decimal>().unwrap(),
388            dec.try_scale_down_value(0).unwrap()
389        );
390        assert_eq!(
391            "123400700".parse::<Decimal>().unwrap(),
392            dec.try_scale_down_value(1).unwrap()
393        );
394        assert_eq!(
395            "12340070".parse::<Decimal>().unwrap(),
396            dec.try_scale_down_value(2).unwrap()
397        );
398        assert_eq!(
399            "123400.7".parse::<Decimal>().unwrap(),
400            dec.try_scale_down_value(4).unwrap()
401        );
402
403        let dec = DecCoin {
404            denom: "foo".to_string(),
405            amount: "10000000000".parse().unwrap(),
406        };
407
408        assert_eq!(
409            "100".parse::<Decimal>().unwrap(),
410            dec.try_scale_down_value(8).unwrap()
411        );
412        assert_eq!(
413            "1".parse::<Decimal>().unwrap(),
414            dec.try_scale_down_value(10).unwrap()
415        );
416        assert_eq!(
417            "0.01".parse::<Decimal>().unwrap(),
418            dec.try_scale_down_value(12).unwrap()
419        );
420    }
421
422    #[test]
423    fn dec_value_scale_up() {
424        let dec = DecCoin {
425            denom: "foo".to_string(),
426            amount: "1234.56".parse().unwrap(),
427        };
428
429        assert_eq!(
430            "1234.56".parse::<Decimal>().unwrap(),
431            dec.try_scale_up_value(0).unwrap()
432        );
433        assert_eq!(
434            "12345.6".parse::<Decimal>().unwrap(),
435            dec.try_scale_up_value(1).unwrap()
436        );
437        assert_eq!(
438            "123456".parse::<Decimal>().unwrap(),
439            dec.try_scale_up_value(2).unwrap()
440        );
441        assert_eq!(
442            "1234560".parse::<Decimal>().unwrap(),
443            dec.try_scale_up_value(3).unwrap()
444        );
445        assert_eq!(
446            "12345600".parse::<Decimal>().unwrap(),
447            dec.try_scale_up_value(4).unwrap()
448        );
449
450        let dec = DecCoin {
451            denom: "foo".to_string(),
452            amount: "0.00000123".parse().unwrap(),
453        };
454
455        assert_eq!(
456            "0.0000123".parse::<Decimal>().unwrap(),
457            dec.try_scale_up_value(1).unwrap()
458        );
459        assert_eq!(
460            "0.000123".parse::<Decimal>().unwrap(),
461            dec.try_scale_up_value(2).unwrap()
462        );
463        assert_eq!(
464            "123".parse::<Decimal>().unwrap(),
465            dec.try_scale_up_value(8).unwrap()
466        );
467        assert_eq!(
468            "1230".parse::<Decimal>().unwrap(),
469            dec.try_scale_up_value(9).unwrap()
470        );
471        assert_eq!(
472            "12300".parse::<Decimal>().unwrap(),
473            dec.try_scale_up_value(10).unwrap()
474        );
475    }
476
477    #[test]
478    fn coin_to_dec_coin() {
479        let coin = Coin::new(123, "foo");
480        let dec = DecCoin::from(coin.clone());
481        assert_eq!(coin.denom, dec.denom);
482        assert_eq!(dec.amount, Decimal::from_atomics(coin.amount, 0).unwrap());
483    }
484
485    #[test]
486    fn dec_coin_to_coin() {
487        let dec = DecCoin {
488            denom: "foo".to_string(),
489            amount: "123".parse().unwrap(),
490        };
491        let coin = Coin::try_from(dec.clone()).unwrap();
492        assert_eq!(dec.denom, coin.denom);
493        assert_eq!(coin.amount, 123u128);
494    }
495
496    #[test]
497    fn converting_to_display() {
498        let reg = RegisteredCoins::default_denoms(&NymNetworkDetails::new_mainnet());
499        let values = vec![
500            (1u128, "0.000001"),
501            (10u128, "0.00001"),
502            (100u128, "0.0001"),
503            (1000u128, "0.001"),
504            (10000u128, "0.01"),
505            (100000u128, "0.1"),
506            (1000000u128, "1"),
507            (1234567u128, "1.234567"),
508            (123456700u128, "123.4567"),
509        ];
510
511        for (raw, expected) in values {
512            let coin = Coin::new(
513                raw,
514                NymNetworkDetails::new_mainnet()
515                    .chain_details
516                    .mix_denom
517                    .base
518                    .clone(),
519            );
520            let display = reg.attempt_convert_to_display_dec_coin(coin).unwrap();
521            assert_eq!(
522                NymNetworkDetails::new_mainnet()
523                    .chain_details
524                    .mix_denom
525                    .display,
526                display.denom
527            );
528            assert_eq!(expected, display.amount.to_string());
529        }
530    }
531
532    #[test]
533    fn converting_to_base() {
534        let reg = RegisteredCoins::default_denoms(&NymNetworkDetails::new_mainnet());
535        let values = vec![
536            (1u128, "0.000001"),
537            (10u128, "0.00001"),
538            (100u128, "0.0001"),
539            (1000u128, "0.001"),
540            (10000u128, "0.01"),
541            (100000u128, "0.1"),
542            (1000000u128, "1"),
543            (1234567u128, "1.234567"),
544            (123456700u128, "123.4567"),
545        ];
546
547        for (expected, raw_display) in values {
548            let coin = DecCoin {
549                denom: NymNetworkDetails::new_mainnet()
550                    .chain_details
551                    .mix_denom
552                    .display
553                    .clone(),
554                amount: raw_display.parse().unwrap(),
555            };
556            let base = reg.attempt_convert_to_base_coin(coin).unwrap();
557            assert_eq!(
558                NymNetworkDetails::new_mainnet()
559                    .chain_details
560                    .mix_denom
561                    .base,
562                base.denom
563            );
564            assert_eq!(expected, base.amount);
565        }
566    }
567}