near_api_types/
tokens.rs

1use near_token::NearToken;
2use serde::{Deserialize, Serialize};
3
4use crate::errors::DecimalNumberParsingError;
5
6/// Static instance of [FTBalance] for USDT token with correct decimals and symbol.
7pub const USDT_BALANCE: FTBalance = FTBalance::with_decimals_and_symbol(6, "USDT");
8/// Static instance of [FTBalance] for USDC token with correct decimals and symbol.
9pub const USDC_BALANCE: FTBalance = FTBalance::with_decimals_and_symbol(6, "USDC");
10/// Static instance of [FTBalance] for wNEAR token with correct decimals and symbol.
11pub const W_NEAR_BALANCE: FTBalance = FTBalance::with_decimals_and_symbol(24, "wNEAR");
12
13/// The cost of storage per byte in NEAR.
14pub const STORAGE_COST_PER_BYTE: NearToken = NearToken::from_yoctonear(10u128.pow(19));
15
16/// A helper type that represents the fungible token balance with a given precision.
17///
18/// The type is created to simplify the usage of fungible tokens in similar way as the [NearToken] type does.
19///
20/// The symbol is used only for display purposes.
21///
22/// The type has static instances for some of the most popular tokens with correct decimals and symbol.
23/// * [USDT_BALANCE] - USDT token with 6 decimals
24/// * [USDC_BALANCE] - USDC token with 6 decimals
25/// * [W_NEAR_BALANCE] - wNEAR token with 24 decimals
26///
27/// # Examples
28///
29/// ## Defining 2.5 USDT
30/// ```rust
31/// use near_api_types::tokens::FTBalance;
32///
33/// let usdt_balance = FTBalance::with_decimals(6).with_float_str("2.5").unwrap();
34///
35/// assert_eq!(usdt_balance.amount(), 2_500_000);
36/// ```
37///
38/// ## Defining 3 USDT using smaller precision
39/// ```rust
40/// use near_api_types::tokens::FTBalance;
41///
42/// let usdt = FTBalance::with_decimals(6);
43///
44/// let usdt_balance = usdt.with_amount(3 * 10u128.pow(6));
45///
46/// assert_eq!(usdt_balance, usdt.with_whole_amount(3));
47/// ```
48///
49/// ## Defining 3 wETH using 18 decimals
50/// ```rust
51/// use near_api_types::tokens::FTBalance;
52///
53/// let weth = FTBalance::with_decimals_and_symbol(18, "wETH");
54/// let weth_balance = weth.with_whole_amount(3);
55///
56/// assert_eq!(weth_balance, weth.with_amount(3 * 10u128.pow(18)));
57/// ```
58#[derive(Debug, Clone, PartialEq, Default, Eq, Serialize, Deserialize)]
59pub struct FTBalance {
60    amount: u128,
61    decimals: u8,
62    symbol: &'static str,
63}
64
65impl FTBalance {
66    /// Creates a new [FTBalance] with a given precision.
67    ///
68    /// The balance is initialized to 0.
69    pub const fn with_decimals(decimals: u8) -> Self {
70        Self {
71            amount: 0,
72            decimals,
73            symbol: "FT",
74        }
75    }
76
77    /// Creates a new [FTBalance] with a given precision and symbol.
78    ///
79    /// The balance is initialized to 0.
80    pub const fn with_decimals_and_symbol(decimals: u8, symbol: &'static str) -> Self {
81        Self {
82            amount: 0,
83            decimals,
84            symbol,
85        }
86    }
87
88    /// Stores the given amount without any transformations.
89    ///
90    /// The [NearToken] equivalent to this method is [NearToken::from_yoctonear].
91    ///
92    /// ## Example
93    /// ```rust
94    /// use near_api_types::tokens::FTBalance;
95    ///
96    /// let usdt_balance = FTBalance::with_decimals(6).with_amount(2_500_000);
97    /// assert_eq!(usdt_balance.amount(), 2_500_000);
98    /// assert_eq!(usdt_balance.to_whole(), 2);
99    /// ```
100    pub const fn with_amount(&self, amount: u128) -> Self {
101        Self {
102            amount,
103            decimals: self.decimals,
104            symbol: self.symbol,
105        }
106    }
107
108    /// Stores the number as an integer token value utilizing the given precision.
109    ///
110    /// The [NearToken] equivalent to this method is [NearToken::from_near].
111    ///
112    /// ## Example
113    /// ```rust
114    /// use near_api_types::tokens::FTBalance;
115    ///
116    /// let usdt_balance = FTBalance::with_decimals(6).with_whole_amount(3);
117    /// assert_eq!(usdt_balance.amount(), 3 * 10u128.pow(6));
118    /// assert_eq!(usdt_balance.to_whole(), 3);
119    /// ```
120    pub const fn with_whole_amount(&self, amount: u128) -> Self {
121        Self {
122            amount: amount * 10u128.pow(self.decimals as u32),
123            decimals: self.decimals,
124            symbol: self.symbol,
125        }
126    }
127
128    /// Parses float string and stores the value in defined precision.
129    ///
130    /// # Examples
131    ///
132    /// ## Defining 2.5 USDT
133    /// ```rust
134    /// use near_api_types::tokens::FTBalance;
135    ///
136    /// let usdt_balance = FTBalance::with_decimals(6).with_float_str("2.515").unwrap();
137    ///
138    /// assert_eq!(usdt_balance.amount(), 2_515_000);
139    /// ```
140    pub fn with_float_str(&self, float_str: &str) -> Result<Self, DecimalNumberParsingError> {
141        parse_decimal_number(float_str, 10u128.pow(self.decimals as u32))
142            .map(|amount| self.with_amount(amount))
143    }
144
145    /// Returns the amount without any transformations.
146    ///
147    /// The [NearToken] equivalent to this method is [NearToken::as_yoctonear].
148    pub const fn amount(&self) -> u128 {
149        self.amount
150    }
151
152    /// Returns the amount as a whole number in the integer precision.
153    ///
154    /// The method rounds down the fractional part, so 2.5 USDT will be 2.
155    ///
156    /// The [NearToken] equivalent to this method is [NearToken::as_near].
157    pub const fn to_whole(&self) -> u128 {
158        self.amount / 10u128.pow(self.decimals as u32)
159    }
160
161    /// Returns the number of decimals used by the token.
162    pub const fn decimals(&self) -> u8 {
163        self.decimals
164    }
165}
166
167impl PartialOrd for FTBalance {
168    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
169        if self.decimals != other.decimals || self.symbol != other.symbol {
170            return None;
171        }
172
173        Some(self.amount.cmp(&other.amount))
174    }
175}
176
177impl std::fmt::Display for FTBalance {
178    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
179        let whole_part = self.to_whole();
180        let fractional_part = self.amount % 10u128.pow(self.decimals as u32);
181
182        let fractional_part_str = format!(
183            "{:0width$}",
184            fractional_part,
185            width = self.decimals as usize
186        );
187        let fractional_part_str = fractional_part_str.trim_end_matches('0');
188
189        if fractional_part_str.is_empty() {
190            return write!(f, "{} {}", whole_part, self.symbol);
191        }
192
193        write!(f, "{}.{} {}", whole_part, fractional_part_str, self.symbol)
194    }
195}
196
197/// Account balance on the NEAR blockchain.
198///
199/// This balance doesn't include staked NEAR tokens or storage
200#[derive(Debug, Clone, PartialEq, Default, Eq, Serialize, Deserialize)]
201pub struct UserBalance {
202    /// The total amount of NEAR tokens in the account.
203    ///
204    /// Please note that this is the total amount of NEAR tokens in the account, not the amount available for use.
205    pub total: NearToken,
206    /// The amount of NEAR tokens locked in the account for storage usage.
207    ///
208    /// The storage lock equal to [Self::storage_usage] * [STORAGE_COST_PER_BYTE]
209    pub storage_locked: NearToken,
210    /// The storage usage by the account in bytes.
211    pub storage_usage: u64,
212    /// The amount of NEAR tokens staked on a protocol level.
213    /// Applicable for staking pools only in 99.99% of the cases.
214    ///
215    /// The PoS allows particular users to stake funds to become a validator, but the protocol itself
216    /// doesn't allow other users to delegate tokens to the validator.
217    /// This is why, the [NEP-27](https://github.com/near/core-contracts/tree/master/staking-pool) defines a Staking Pool smart contract
218    /// that allows other users to delegate tokens to the validator.
219    ///
220    /// Even though, the user can stake and become validator itself, it's highly unlikely and this field will be 0
221    /// for almost all the users, and not 0 for StakingPool contracts.
222    ///
223    /// Please note that this is not related to your delegations into the staking pools.
224    /// To get your delegation information in the staking pools, use [near-api::Delegation]
225    pub locked: NearToken,
226}
227
228/// Parsing decimal numbers from `&str` type in `u128`.
229/// Function also takes a value of metric prefix in u128 type.
230/// `parse_str` use the `u128` type, and have the same max and min values.
231///
232/// If the fractional part is longer than several zeros in the prefix, it will return the error `DecimalNumberParsingError::LongFractional`.
233///
234/// If the string slice has invalid chars, it will return the error `DecimalNumberParsingError::InvalidNumber`.
235///
236/// If the whole part of the number has a value more than the `u64` maximum value, it will return the error `DecimalNumberParsingError::LongWhole`.
237fn parse_decimal_number(s: &str, pref_const: u128) -> Result<u128, DecimalNumberParsingError> {
238    let (int, fraction) = if let Some((whole, fractional)) = s.trim().split_once('.') {
239        let int: u128 = whole
240            .parse()
241            .map_err(|_| DecimalNumberParsingError::InvalidNumber(s.to_owned()))?;
242        let mut fraction: u128 = fractional
243            .parse()
244            .map_err(|_| DecimalNumberParsingError::InvalidNumber(s.to_owned()))?;
245        let len = u32::try_from(fractional.len())
246            .map_err(|_| DecimalNumberParsingError::InvalidNumber(s.to_owned()))?;
247        fraction = fraction
248            .checked_mul(
249                pref_const
250                    .checked_div(10u128.checked_pow(len).ok_or_else(|| {
251                        DecimalNumberParsingError::LongFractional(fractional.to_owned())
252                    })?)
253                    .filter(|n| *n != 0u128)
254                    .ok_or_else(|| {
255                        DecimalNumberParsingError::LongFractional(fractional.to_owned())
256                    })?,
257            )
258            .ok_or_else(|| DecimalNumberParsingError::LongFractional(fractional.to_owned()))?;
259        (int, fraction)
260    } else {
261        let int: u128 = s
262            .parse()
263            .map_err(|_| DecimalNumberParsingError::InvalidNumber(s.to_owned()))?;
264        (int, 0)
265    };
266    let result = fraction
267        .checked_add(
268            int.checked_mul(pref_const)
269                .ok_or_else(|| DecimalNumberParsingError::LongWhole(int.to_string()))?,
270        )
271        .ok_or_else(|| DecimalNumberParsingError::LongWhole(int.to_string()))?;
272    Ok(result)
273}
274
275#[cfg(test)]
276mod tests {
277    use crate::{errors::DecimalNumberParsingError, tokens::parse_decimal_number};
278
279    use super::FTBalance;
280
281    #[test]
282    fn ft_balance_default() {
283        assert_eq!(
284            FTBalance::with_decimals(5).with_whole_amount(5).amount(),
285            500000
286        );
287        assert_eq!(FTBalance::with_decimals(5).with_amount(5).amount(), 5);
288
289        assert_eq!(
290            FTBalance::with_decimals(5).with_whole_amount(5).to_whole(),
291            5
292        );
293    }
294
295    #[test]
296    fn ft_balance_str() {
297        assert_eq!(
298            FTBalance::with_decimals(5)
299                .with_float_str("5")
300                .unwrap()
301                .amount(),
302            500000
303        );
304        assert_eq!(
305            FTBalance::with_decimals(5)
306                .with_float_str("5.00001")
307                .unwrap()
308                .amount(),
309            500001
310        );
311        assert_eq!(
312            FTBalance::with_decimals(5)
313                .with_float_str("5.55")
314                .unwrap()
315                .amount(),
316            555000
317        );
318    }
319
320    const TEST: [(u128, &str, u128); 6] = [
321        (129_380_000_001_u128, "129.380000001", 10u128.pow(9)),
322        (
323            12_938_000_000_100_000_000_u128,
324            "12938000000.1",
325            10u128.pow(9),
326        ),
327        (129_380_000_001_u128, "0.129380000001", 10u128.pow(12)),
328        (129_380_000_001_000_u128, "129.380000001000", 10u128.pow(12)),
329        (
330            9_488_129_380_000_001_u128,
331            "9488.129380000001",
332            10u128.pow(12),
333        ),
334        (129_380_000_001_u128, "00.129380000001", 10u128.pow(12)),
335    ];
336
337    #[test]
338    fn parse_test() {
339        for (expected_value, str_value, precision) in TEST {
340            let parsed_value = parse_decimal_number(str_value, precision).unwrap();
341            assert_eq!(parsed_value, expected_value)
342        }
343    }
344
345    #[test]
346    fn test_long_fraction() {
347        let data = "1.23456";
348        let prefix = 10000u128;
349        assert_eq!(
350            parse_decimal_number(data, prefix),
351            Err(DecimalNumberParsingError::LongFractional(23456.to_string()))
352        );
353    }
354
355    #[test]
356    fn invalid_number_whole() {
357        let num = "1h4.7859";
358        let prefix: u128 = 10000;
359        assert_eq!(
360            parse_decimal_number(num, prefix),
361            Err(DecimalNumberParsingError::InvalidNumber(
362                "1h4.7859".to_owned()
363            ))
364        );
365    }
366    #[test]
367    fn invalid_number_fraction() {
368        let num = "14.785h9";
369        let prefix: u128 = 10000;
370        assert_eq!(
371            parse_decimal_number(num, prefix),
372            Err(DecimalNumberParsingError::InvalidNumber(
373                "14.785h9".to_owned()
374            ))
375        );
376    }
377
378    #[test]
379    fn max_long_fraction() {
380        let max_data = 10u128.pow(17) + 1;
381        let data = "1.".to_string() + max_data.to_string().as_str();
382        let prefix = 10u128.pow(17);
383        assert_eq!(
384            parse_decimal_number(data.as_str(), prefix),
385            Err(DecimalNumberParsingError::LongFractional(
386                max_data.to_string()
387            ))
388        );
389    }
390
391    #[test]
392    fn parse_u128_error_test() {
393        let test_data = u128::MAX.to_string();
394        let gas = parse_decimal_number(&test_data, 10u128.pow(9));
395        assert_eq!(
396            gas,
397            Err(DecimalNumberParsingError::LongWhole(u128::MAX.to_string()))
398        );
399    }
400
401    #[test]
402    fn test() {
403        let data = "1.000000000000000000000000000000000000001";
404        let prefix = 100u128;
405        assert_eq!(
406            parse_decimal_number(data, prefix),
407            Err(DecimalNumberParsingError::LongFractional(
408                "000000000000000000000000000000000000001".to_string()
409            ))
410        );
411    }
412}