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}