Skip to main content

nym_validator_client/nyxd/
coin.rs

1// Copyright 2022 - Nym Technologies SA <contact@nymtech.net>
2// SPDX-License-Identifier: Apache-2.0
3
4use crate::nyxd::{Gas, GasPrice};
5pub use cosmrs::Coin as CosmosCoin;
6pub use cosmwasm_std::Coin as CosmWasmCoin;
7use cosmwasm_std::{Fraction, Uint128};
8use serde::{Deserialize, Serialize};
9use std::fmt;
10use std::ops::Div;
11use std::str::FromStr;
12use thiserror::Error;
13
14#[derive(Serialize, Deserialize, Clone, Copy, Default, Debug, PartialEq, Eq)]
15pub struct MismatchedDenoms;
16
17// the reason the coin is created here as opposed to different place in the codebase is that
18// eventually we want to either publish the cosmwasm client separately or commit it to
19// some other project, like cosmrs. Either way, in that case we can't really have
20// a dependency on an internal type
21#[derive(Serialize, Deserialize, Clone, Default, Debug, PartialEq, Eq)]
22pub struct Coin {
23    pub amount: u128,
24    pub denom: String,
25}
26
27impl Div<GasPrice> for Coin {
28    type Output = Gas;
29
30    fn div(self, rhs: GasPrice) -> Self::Output {
31        &self / rhs
32    }
33}
34
35impl Div<GasPrice> for &Coin {
36    type Output = Gas;
37
38    fn div(self, rhs: GasPrice) -> Self::Output {
39        if self.denom != rhs.denom {
40            panic!(
41                "attempted to use two different denoms for gas calculation ({} and {})",
42                self.denom, rhs.denom
43            );
44        }
45
46        // tsk, tsk. somebody tried to divide by zero here!
47        let Some(gas_price_inv) = rhs.amount.inv() else {
48            panic!("attempted to divide by zero!")
49        };
50
51        let implicit_gas_limit = Uint128::new(self.amount).mul_floor(gas_price_inv);
52        if implicit_gas_limit.u128() >= u64::MAX as u128 {
53            u64::MAX
54        } else {
55            implicit_gas_limit.u128() as u64
56        }
57    }
58}
59
60impl Coin {
61    pub fn new<S: Into<String>>(amount: u128, denom: S) -> Self {
62        Coin {
63            amount,
64            denom: denom.into(),
65        }
66    }
67
68    pub fn try_add(&self, other: &Self) -> Result<Self, MismatchedDenoms> {
69        if self.denom != other.denom {
70            Err(MismatchedDenoms)
71        } else {
72            Ok(Coin {
73                amount: self.amount + other.amount,
74                denom: self.denom.clone(),
75            })
76        }
77    }
78}
79
80impl fmt::Display for Coin {
81    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
82        write!(f, "{}{}", self.amount, self.denom)
83    }
84}
85
86impl From<Coin> for CosmosCoin {
87    fn from(coin: Coin) -> Self {
88        assert!(
89            coin.amount <= u64::MAX as u128,
90            "the coin amount is higher than the maximum supported by cosmrs"
91        );
92
93        CosmosCoin {
94            denom: coin
95                .denom
96                .parse()
97                .expect("the coin should have had a valid denom!"),
98            amount: (coin.amount as u64).into(),
99        }
100    }
101}
102
103impl From<CosmosCoin> for Coin {
104    fn from(coin: CosmosCoin) -> Self {
105        Coin {
106            amount: coin
107                .amount
108                .to_string()
109                .parse()
110                .expect("somehow failed to parse string representation of u64"),
111            denom: coin.denom.to_string(),
112        }
113    }
114}
115
116impl From<Coin> for CosmWasmCoin {
117    fn from(coin: Coin) -> Self {
118        CosmWasmCoin::new(coin.amount, coin.denom)
119    }
120}
121
122impl From<CosmWasmCoin> for Coin {
123    fn from(coin: CosmWasmCoin) -> Self {
124        Coin {
125            amount: coin.amount.u128(),
126            denom: coin.denom,
127        }
128    }
129}
130
131// unfortunately cosmwasm didn't re-export this correct so we just redefine its
132#[derive(Error, Debug, PartialEq, Eq)]
133pub enum CoinFromStrError {
134    #[error("Missing denominator")]
135    MissingDenom,
136    #[error("Missing amount or non-digit characters in amount")]
137    MissingAmount,
138    #[error("Invalid amount: {0}")]
139    InvalidAmount(#[from] std::num::ParseIntError),
140}
141
142impl FromStr for Coin {
143    type Err = CoinFromStrError;
144
145    fn from_str(s: &str) -> Result<Self, Self::Err> {
146        let pos = s
147            .find(|c: char| !c.is_ascii_digit())
148            .ok_or(CoinFromStrError::MissingDenom)?;
149        let (amount, denom) = s.split_at(pos);
150
151        if amount.is_empty() {
152            return Err(CoinFromStrError::MissingAmount);
153        }
154
155        Ok(Coin {
156            amount: amount.parse::<u128>()?,
157            denom: denom.to_string(),
158        })
159    }
160}
161
162pub trait CoinConverter {
163    type Target;
164
165    fn convert_coin(&self) -> Self::Target;
166}
167
168impl CoinConverter for CosmosCoin {
169    type Target = CosmWasmCoin;
170
171    fn convert_coin(&self) -> Self::Target {
172        CosmWasmCoin::new(self.amount, self.denom.to_string())
173    }
174}
175
176impl CoinConverter for CosmWasmCoin {
177    type Target = CosmosCoin;
178
179    fn convert_coin(&self) -> Self::Target {
180        assert!(
181            self.amount.u128() <= u64::MAX as u128,
182            "the coin amount is higher than the maximum supported by cosmrs"
183        );
184
185        CosmosCoin {
186            denom: self
187                .denom
188                .parse()
189                .expect("cosmwasm coin had an invalid amount assigned"),
190            amount: (self.amount.u128() as u64).into(),
191        }
192    }
193}
194
195#[cfg(test)]
196mod tests {
197    use super::*;
198
199    #[test]
200    #[should_panic]
201    fn division_by_zero_gas_price() {
202        let gas_price: GasPrice = "0unym".parse().unwrap();
203        let amount = Coin::new(123, "unym");
204        let _res = amount / gas_price;
205    }
206
207    #[test]
208    #[should_panic]
209    fn division_by_gas_price_of_different_denom() {
210        let gas_price: GasPrice = "0.025unyx".parse().unwrap();
211        let amount = Coin::new(123, "unym");
212        let _res = amount / gas_price;
213    }
214
215    #[test]
216    fn gas_price_division() {
217        let amount = Coin::new(3938, "unym");
218        let gas_price = "0.025unym".parse().unwrap();
219        let res = amount / gas_price;
220        assert_eq!(157520, res);
221
222        let amount = Coin::new(1234567890, "unym");
223        let gas_price = "0.025unym".parse().unwrap();
224        let res = amount / gas_price;
225        assert_eq!(49382715600, res);
226
227        let amount = Coin::new(1, "unym");
228        let gas_price = "0.025unym".parse().unwrap();
229        let res = amount / gas_price;
230        assert_eq!(40, res);
231
232        let amount = Coin::new(150_000_000, "unym");
233        let gas_price = "0.001234unym".parse().unwrap();
234        let res = amount / gas_price;
235        assert_eq!(121555915721, res);
236
237        let amount = Coin::new(150_000_000, "unym");
238        let gas_price = "1unym".parse().unwrap();
239        let res = amount / gas_price;
240        assert_eq!(150_000_000, res);
241
242        let amount = Coin::new(150_000_000, "unym");
243        let gas_price = "1234.56unym".parse().unwrap();
244        let res = amount / gas_price;
245        assert_eq!(121500, res);
246    }
247
248    #[test]
249    fn gas_price_division_identity() {
250        let amount = Coin::new(1234567890, "unym");
251        let gas_price: GasPrice = "0.025unym".parse().unwrap();
252        let res1 = (&amount) / gas_price.clone();
253        let res2 = &gas_price * res1;
254
255        assert_eq!(amount, Coin::from(res2));
256    }
257}