templar_common/
asset.rs

1use std::{
2    fmt::{Debug, Display},
3    marker::PhantomData,
4};
5
6use near_contract_standards::fungible_token::core::ext_ft_core;
7#[cfg(all(not(target_arch = "wasm32"), feature = "rpc"))]
8use near_primitives::action::FunctionCallAction;
9use near_sdk::{
10    env,
11    json_types::U128,
12    near,
13    serde_json::{self, json},
14    AccountId, AccountIdRef, Gas, NearToken, Promise,
15};
16use templar_primitives::number::Decimal;
17
18use crate::panic_with_message;
19
20/// Assets may be configuread as one of the supported asset types.
21///
22/// The following asset contract standards are supported:
23///
24/// - [NEP-141 Fungible Token (FT)](https://nomicon.io/Standards/Tokens/FungibleToken/Core)
25/// - [NEP-245 Multi-Token (MT)](https://nomicon.io/Standards/Tokens/MultiToken/Core)
26///
27/// ---
28///
29/// Assets can be constructed using associated functions:
30///
31/// ```
32/// let my_ft = FungibleAsset::<BorrowAsset>::nep141("contract_id".parse().unwrap());
33/// let my_mt = FungibleAsset::<CollateralAsset>::nep245(
34///     "contract_id".parse().unwrap(),
35///     "token_id".to_string(),
36/// );
37/// ```
38#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
39#[near(serializers = [json, borsh])]
40pub struct FungibleAsset<T: AssetClass> {
41    // Necessary because there is no clean way to use PhantomData<T> in an enum.
42    // https://internals.rust-lang.org/t/type-parameter-not-used-on-enums/13342
43    #[serde(skip)]
44    #[borsh(skip)]
45    discriminant: PhantomData<T>,
46    #[serde(flatten)]
47    kind: FungibleAssetKind,
48}
49
50#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
51#[near(serializers = [json, borsh])]
52enum FungibleAssetKind {
53    Nep141(AccountId),
54    Nep245 {
55        contract_id: AccountId,
56        token_id: String,
57    },
58}
59
60impl<T: AssetClass> FungibleAsset<T> {
61    /// Gas for simple transfers (`ft_transfer`)
62    pub const GAS_FT_TRANSFER: Gas = Gas::from_tgas(6);
63
64    /// Gas for simple NEP-245 transfers (`mt_transfer`)
65    pub const GAS_MT_TRANSFER: Gas = Gas::from_tgas(7);
66
67    /// Gas for `transfer_call` operations (includes callback to receiver)
68    /// NEP-141 `ft_transfer_call`: Transfer + receiver callback execution
69    /// Needs extra gas for the receiver contract logic (e.g., market liquidation)
70    pub const GAS_FT_TRANSFER_CALL: Gas = Gas::from_tgas(100);
71
72    /// Gas for NEP-245 `mt_transfer_call` operations
73    /// NEAR Intents `mt_transfer_call`: Transfer + receiver callback + collateral transfer back
74    pub const GAS_MT_TRANSFER_CALL: Gas = Gas::from_tgas(150);
75
76    #[allow(clippy::missing_panics_doc, clippy::unwrap_used)]
77    pub fn transfer(&self, receiver_id: AccountId, amount: FungibleAssetAmount<T>) -> Promise {
78        match self.kind {
79            FungibleAssetKind::Nep141(ref contract_id) => ext_ft_core::ext(contract_id.clone())
80                .with_static_gas(Self::GAS_FT_TRANSFER)
81                .with_attached_deposit(NearToken::from_yoctonear(1))
82                .ft_transfer(receiver_id, u128::from(amount).into(), None),
83            FungibleAssetKind::Nep245 {
84                ref contract_id,
85                ref token_id,
86            } => Promise::new(contract_id.clone()).function_call(
87                "mt_transfer".to_string(),
88                serde_json::to_vec(&json!({
89                   "receiver_id": receiver_id,
90                   "token_id": token_id,
91                   "amount": amount,
92                }))
93                .unwrap(),
94                NearToken::from_yoctonear(1),
95                Self::GAS_MT_TRANSFER,
96            ),
97        }
98    }
99
100    #[cfg(all(not(target_arch = "wasm32"), feature = "rpc"))]
101    pub fn transfer_call_method_name(&self) -> &str {
102        match self.kind {
103            FungibleAssetKind::Nep141(_) => "ft_transfer_call",
104            FungibleAssetKind::Nep245 { .. } => "mt_transfer_call",
105        }
106    }
107
108    #[allow(clippy::missing_panics_doc, clippy::unwrap_used)]
109    pub fn transfer_call(
110        &self,
111        receiver_id: &AccountId,
112        amount: FungibleAssetAmount<T>,
113        msg: Option<&str>,
114    ) -> Promise {
115        let msg = msg.unwrap_or_default().to_string();
116        match self.kind {
117            FungibleAssetKind::Nep141(ref contract_id) => ext_ft_core::ext(contract_id.clone())
118                .with_static_gas(Self::GAS_FT_TRANSFER)
119                .with_attached_deposit(NearToken::from_yoctonear(1))
120                .ft_transfer_call(receiver_id.clone(), u128::from(amount).into(), None, msg),
121            FungibleAssetKind::Nep245 {
122                ref contract_id,
123                ref token_id,
124            } => Promise::new(contract_id.clone()).function_call(
125                "mt_transfer_call".to_string(),
126                serde_json::to_vec(&json!({
127                   "receiver_id": receiver_id,
128                   "token_id": token_id,
129                   "amount": amount,
130                   "msg": msg,
131                }))
132                .unwrap(),
133                NearToken::from_yoctonear(1),
134                Self::GAS_MT_TRANSFER,
135            ),
136        }
137    }
138
139    /// Creates a simple `ft_transfer` action (no callback).
140    #[cfg(all(not(target_arch = "wasm32"), feature = "rpc"))]
141    pub fn transfer_action(
142        &self,
143        receiver_id: &AccountId,
144        amount: FungibleAssetAmount<T>,
145    ) -> FunctionCallAction {
146        let (method_name, args, gas) = match self.kind {
147            FungibleAssetKind::Nep141(_) => (
148                "ft_transfer",
149                json!({
150                    "receiver_id": receiver_id,
151                    "amount": u128::from(amount).to_string(),
152                }),
153                Self::GAS_FT_TRANSFER,
154            ),
155            FungibleAssetKind::Nep245 { ref token_id, .. } => (
156                "mt_transfer",
157                json!({
158                    "receiver_id": receiver_id,
159                    "token_id": token_id,
160                    "amount": u128::from(amount).to_string(),
161                }),
162                Self::GAS_MT_TRANSFER,
163            ),
164        };
165
166        FunctionCallAction {
167            method_name: method_name.to_string(),
168            #[allow(clippy::unwrap_used)]
169            args: serde_json::to_vec(&args).unwrap(),
170            gas: near_primitives::gas::Gas::from_gas(gas.as_gas()),
171            deposit: NearToken::from_yoctonear(1), // 1 yoctoNEAR for security
172        }
173    }
174
175    #[cfg(all(not(target_arch = "wasm32"), feature = "rpc"))]
176    pub fn transfer_call_action(
177        &self,
178        receiver_id: &AccountId,
179        amount: FungibleAssetAmount<T>,
180        msg: &str,
181    ) -> FunctionCallAction {
182        let (args, gas) = match self.kind {
183            FungibleAssetKind::Nep141(_) => (
184                json!({
185                    "receiver_id": receiver_id,
186                    "amount": u128::from(amount).to_string(),
187                    "msg": msg,
188                }),
189                Self::GAS_FT_TRANSFER_CALL,
190            ),
191            FungibleAssetKind::Nep245 { ref token_id, .. } => (
192                json!({
193                    "receiver_id": receiver_id,
194                    "token_id": token_id,
195                    "amount": u128::from(amount).to_string(),
196                    "msg": msg,
197                }),
198                Self::GAS_MT_TRANSFER_CALL,
199            ),
200        };
201
202        FunctionCallAction {
203            method_name: self.transfer_call_method_name().to_string(),
204            #[allow(
205                clippy::unwrap_used,
206                reason = "All of the types have infallible serialization"
207            )]
208            args: serde_json::to_vec(&args).unwrap(),
209            gas: near_primitives::gas::Gas::from_gas(gas.as_gas()),
210            deposit: NearToken::from_yoctonear(1),
211        }
212    }
213
214    #[cfg(all(not(target_arch = "wasm32"), feature = "rpc"))]
215    pub fn balance_of_action(&self, account_id: &AccountId) -> FunctionCallAction {
216        let (method_name, args) = match self.kind {
217            FungibleAssetKind::Nep141(_) => (
218                "ft_balance_of",
219                json!({
220                    "account_id": account_id,
221                }),
222            ),
223            FungibleAssetKind::Nep245 { ref token_id, .. } => (
224                "mt_balance_of",
225                json!({
226                    "account_id": account_id,
227                    "token_id": token_id,
228                }),
229            ),
230        };
231
232        FunctionCallAction {
233            method_name: method_name.to_string(),
234            #[allow(
235                clippy::unwrap_used,
236                reason = "All of the types have infallible serialization"
237            )]
238            args: serde_json::to_vec(&args).unwrap(),
239            gas: near_primitives::gas::Gas::from_teragas(3),
240            deposit: NearToken::ZERO,
241        }
242    }
243
244    pub fn nep141(contract_id: AccountId) -> Self {
245        Self {
246            discriminant: PhantomData,
247            kind: FungibleAssetKind::Nep141(contract_id),
248        }
249    }
250
251    pub fn nep245(contract_id: AccountId, token_id: String) -> Self {
252        Self {
253            discriminant: PhantomData,
254            kind: FungibleAssetKind::Nep245 {
255                contract_id,
256                token_id,
257            },
258        }
259    }
260
261    pub fn is_nep141(&self, account_id: &AccountId) -> bool {
262        matches!(self.kind, FungibleAssetKind::Nep141(ref contract_id) if contract_id == account_id)
263    }
264
265    pub fn into_nep141(self) -> Option<AccountId> {
266        match self.kind {
267            FungibleAssetKind::Nep141(contract_id) => Some(contract_id),
268            FungibleAssetKind::Nep245 { .. } => None,
269        }
270    }
271
272    pub fn is_nep245(&self, account_id: &AccountId, token_id: &str) -> bool {
273        let t = token_id;
274        matches!(self.kind, FungibleAssetKind::Nep245 { ref contract_id, ref token_id } if contract_id == account_id && token_id == t)
275    }
276
277    pub fn into_nep245(self) -> Option<(AccountId, String)> {
278        match self.kind {
279            FungibleAssetKind::Nep245 {
280                contract_id,
281                token_id,
282            } => Some((contract_id, token_id)),
283            FungibleAssetKind::Nep141(_) => None,
284        }
285    }
286
287    /// The NEP-245 token id, or `None` for an NEP-141 fungible token. Combined
288    /// with [`Self::contract_id`], this identifies the token without consuming
289    /// the asset or exposing its representation.
290    pub fn nep245_token_id(&self) -> Option<&str> {
291        match self.kind {
292            FungibleAssetKind::Nep245 { ref token_id, .. } => Some(token_id),
293            FungibleAssetKind::Nep141(_) => None,
294        }
295    }
296
297    #[allow(clippy::missing_panics_doc, clippy::unwrap_used)]
298    pub fn current_account_balance(&self) -> Promise {
299        let current_account_id = env::current_account_id();
300        match self.kind {
301            FungibleAssetKind::Nep141(ref account_id) => {
302                ext_ft_core::ext(account_id.clone()).ft_balance_of(current_account_id.clone())
303            }
304            FungibleAssetKind::Nep245 {
305                ref contract_id,
306                ref token_id,
307            } => Promise::new(contract_id.clone()).function_call(
308                "mt_balance_of".to_string(),
309                serde_json::to_vec(&json!({
310                    "account_id": current_account_id,
311                    "token_id": token_id,
312                }))
313                .unwrap(),
314                NearToken::from_millinear(0),
315                Gas::from_tgas(4),
316            ),
317        }
318    }
319
320    pub fn coerce<U: AssetClass>(self) -> FungibleAsset<U> {
321        FungibleAsset {
322            discriminant: PhantomData,
323            kind: self.kind,
324        }
325    }
326
327    pub fn contract_id(&self) -> &AccountIdRef {
328        match self.kind {
329            FungibleAssetKind::Nep141(ref account_id) => account_id,
330            FungibleAssetKind::Nep245 {
331                ref contract_id, ..
332            } => contract_id,
333        }
334    }
335}
336
337impl<T: AssetClass> Display for FungibleAsset<T> {
338    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
339        match self.kind {
340            FungibleAssetKind::Nep141(ref contract_id) => {
341                write!(f, "nep141:{contract_id}")
342            }
343            FungibleAssetKind::Nep245 {
344                ref contract_id,
345                ref token_id,
346            } => write!(f, "nep245:{contract_id}:{token_id}"),
347        }
348    }
349}
350
351impl<T: AssetClass> std::str::FromStr for FungibleAsset<T> {
352    type Err = FungibleAssetParseError;
353
354    fn from_str(s: &str) -> Result<Self, Self::Err> {
355        // Use splitn to limit splits - important for NEP-245 where token_id can contain colons
356        // e.g., "nep245:intents.near:nep141:btc.omft.near" should split into 3 parts max
357        let parts: Vec<&str> = s.splitn(3, ':').collect();
358
359        match parts.as_slice() {
360            ["nep141", contract_id] => {
361                let account_id = contract_id
362                    .parse::<AccountId>()
363                    .map_err(|e| FungibleAssetParseError::InvalidAccountId(e.to_string()))?;
364                Ok(FungibleAsset::nep141(account_id))
365            }
366            ["nep245", contract_id, token_id] => {
367                let account_id = contract_id
368                    .parse::<AccountId>()
369                    .map_err(|e| FungibleAssetParseError::InvalidAccountId(e.to_string()))?;
370
371                if token_id.is_empty() {
372                    return Err(FungibleAssetParseError::EmptyTokenId);
373                }
374
375                Ok(FungibleAsset::nep245(account_id, (*token_id).to_string()))
376            }
377            _ => Err(FungibleAssetParseError::InvalidFormat),
378        }
379    }
380}
381
382#[derive(Debug, thiserror::Error)]
383pub enum FungibleAssetParseError {
384    #[error(
385        "Invalid format. Expected 'nep141:<contract_id>' or 'nep245:<contract_id>:<token_id>'"
386    )]
387    InvalidFormat,
388    #[error("Invalid account ID: {0}")]
389    InvalidAccountId(String),
390    #[error("Token ID cannot be empty for NEP-245 assets")]
391    EmptyTokenId,
392}
393
394mod sealed {
395    pub trait Sealed {}
396}
397pub trait AssetClass: sealed::Sealed + Copy + Clone + Send + Sync + std::fmt::Debug {}
398
399#[derive(Default, Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
400#[near(serializers = [borsh, json])]
401pub struct CollateralAsset;
402impl sealed::Sealed for CollateralAsset {}
403impl AssetClass for CollateralAsset {}
404
405#[derive(Default, Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
406#[near(serializers = [borsh, json])]
407pub struct BorrowAsset;
408impl sealed::Sealed for BorrowAsset {}
409impl AssetClass for BorrowAsset {}
410
411#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
412#[near(serializers = [borsh, json])]
413#[serde(from = "U128", into = "U128")]
414#[cfg_attr(not(target_arch = "wasm32"), schemars(transparent))]
415pub struct FungibleAssetAmount<T: AssetClass> {
416    #[cfg_attr(not(target_arch = "wasm32"), schemars(with = "U128"))]
417    amount: U128,
418    #[borsh(skip)]
419    discriminant: PhantomData<T>,
420}
421
422impl<T: AssetClass> Debug for FungibleAssetAmount<T> {
423    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
424        write!(f, "{}<{}>", self.amount.0, std::any::type_name::<T>())
425    }
426}
427
428impl<T: AssetClass> Default for FungibleAssetAmount<T> {
429    fn default() -> Self {
430        Self::zero()
431    }
432}
433
434impl<T: AssetClass> From<U128> for FungibleAssetAmount<T> {
435    fn from(amount: U128) -> Self {
436        Self {
437            amount,
438            discriminant: PhantomData,
439        }
440    }
441}
442
443impl<T: AssetClass> From<FungibleAssetAmount<T>> for U128 {
444    fn from(value: FungibleAssetAmount<T>) -> Self {
445        value.amount
446    }
447}
448
449impl<T: AssetClass> From<u128> for FungibleAssetAmount<T> {
450    fn from(value: u128) -> Self {
451        Self::new(value)
452    }
453}
454
455impl<T: AssetClass> FungibleAssetAmount<T> {
456    pub fn new(amount: u128) -> Self {
457        Self {
458            amount: U128(amount),
459            discriminant: PhantomData,
460        }
461    }
462
463    pub const fn zero() -> Self {
464        Self {
465            amount: U128(0),
466            discriminant: PhantomData,
467        }
468    }
469
470    pub fn is_zero(&self) -> bool {
471        self.amount.0 == 0
472    }
473
474    #[must_use]
475    pub fn unwrap_add(self, other: impl Into<Self>, message: &str) -> Self {
476        Self {
477            amount: self
478                .amount
479                .0
480                .checked_add(other.into().amount.0)
481                .unwrap_or_else(|| panic_with_message(&format!("Arithmetic overflow: {message}")))
482                .into(),
483            ..self
484        }
485    }
486
487    #[must_use]
488    pub fn saturating_add(self, other: impl Into<Self>) -> Self {
489        Self {
490            amount: self.amount.0.saturating_add(other.into().amount.0).into(),
491            ..self
492        }
493    }
494
495    #[must_use]
496    pub fn checked_add(self, other: impl Into<Self>) -> Option<Self> {
497        Some(Self {
498            amount: self.amount.0.checked_add(other.into().amount.0)?.into(),
499            ..self
500        })
501    }
502
503    #[must_use]
504    pub fn unwrap_sub(self, other: impl Into<Self>, message: &str) -> Self {
505        Self {
506            amount: self
507                .amount
508                .0
509                .checked_sub(other.into().amount.0)
510                .unwrap_or_else(|| panic_with_message(&format!("Arithmetic underflow: {message}")))
511                .into(),
512            ..self
513        }
514    }
515
516    #[must_use]
517    pub fn saturating_sub(self, other: impl Into<Self>) -> Self {
518        Self {
519            amount: self.amount.0.saturating_sub(other.into().amount.0).into(),
520            ..self
521        }
522    }
523
524    #[must_use]
525    pub fn checked_sub(self, other: impl Into<Self>) -> Option<Self> {
526        Some(Self {
527            amount: self.amount.0.checked_sub(other.into().amount.0)?.into(),
528            ..self
529        })
530    }
531}
532
533impl<T: AssetClass> From<FungibleAssetAmount<T>> for Decimal {
534    fn from(value: FungibleAssetAmount<T>) -> Self {
535        value.amount.0.into()
536    }
537}
538
539impl<T: AssetClass> From<FungibleAssetAmount<T>> for u128 {
540    fn from(value: FungibleAssetAmount<T>) -> Self {
541        value.amount.0
542    }
543}
544
545impl<T: AssetClass> std::fmt::Display for FungibleAssetAmount<T> {
546    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
547        write!(f, "{}", self.amount.0)
548    }
549}
550
551impl<T: AssetClass, R: Into<Self>> std::ops::Add<R> for FungibleAssetAmount<T> {
552    type Output = Self;
553
554    fn add(self, rhs: R) -> Self::Output {
555        Self {
556            amount: U128(self.amount.0 + rhs.into().amount.0),
557            ..self
558        }
559    }
560}
561
562impl<T: AssetClass, R: Into<Self>> std::ops::AddAssign<R> for FungibleAssetAmount<T> {
563    fn add_assign(&mut self, rhs: R) {
564        self.amount.0 += rhs.into().amount.0;
565    }
566}
567
568impl<T: AssetClass, R: Into<Self>> std::ops::Sub<R> for FungibleAssetAmount<T> {
569    type Output = Self;
570
571    fn sub(self, rhs: R) -> Self::Output {
572        Self {
573            amount: U128(self.amount.0 - rhs.into().amount.0),
574            ..self
575        }
576    }
577}
578
579impl<T: AssetClass, R: Into<Self>> std::ops::SubAssign<R> for FungibleAssetAmount<T> {
580    fn sub_assign(&mut self, rhs: R) {
581        self.amount.0 -= rhs.into().amount.0;
582    }
583}
584
585pub type BorrowAssetAmount = FungibleAssetAmount<BorrowAsset>;
586pub type CollateralAssetAmount = FungibleAssetAmount<CollateralAsset>;
587
588#[cfg(test)]
589mod tests {
590    use super::*;
591    use near_sdk::serde_json;
592
593    #[test]
594    fn serialization() {
595        let amount = BorrowAssetAmount::new(100);
596        let serialized = serde_json::to_string(&amount).unwrap();
597        assert_eq!(serialized, "\"100\"");
598        let deserialized: BorrowAssetAmount = serde_json::from_str(&serialized).unwrap();
599        assert_eq!(deserialized, amount);
600    }
601    #[test]
602    fn amount_schema_matches_serde_wire_format() {
603        let schema = serde_json::to_value(schemars::schema_for!(BorrowAssetAmount))
604            .expect("amount schema serializes");
605        let validator = jsonschema::draft7::new(&schema).expect("amount schema is Draft 7");
606        let amount = BorrowAssetAmount::new(100);
607        let serialized = serde_json::to_value(amount).expect("amount serializes");
608
609        assert_eq!(serialized, serde_json::json!("100"));
610        validator
611            .validate(&serialized)
612            .expect("serialized amount is valid under its schema");
613        assert_eq!(
614            serde_json::from_value::<BorrowAssetAmount>(serialized).expect("amount deserializes"),
615            amount
616        );
617    }
618
619    #[test]
620    fn checked_add() {
621        let v = BorrowAssetAmount::new(0).checked_add(BorrowAssetAmount::new(0));
622        assert_eq!(v, Some(BorrowAssetAmount::new(0)));
623        let v = BorrowAssetAmount::new(0).checked_add(BorrowAssetAmount::new(100));
624        assert_eq!(v, Some(BorrowAssetAmount::new(100)));
625        let v = BorrowAssetAmount::new(100).checked_add(BorrowAssetAmount::new(0));
626        assert_eq!(v, Some(BorrowAssetAmount::new(100)));
627        let v = BorrowAssetAmount::new(100).checked_add(BorrowAssetAmount::new(100));
628        assert_eq!(v, Some(BorrowAssetAmount::new(200)));
629        let v = BorrowAssetAmount::new(1).checked_add(BorrowAssetAmount::new(u128::MAX));
630        assert_eq!(v, None);
631    }
632
633    #[test]
634    fn checked_sub() {
635        let v = BorrowAssetAmount::new(0).checked_sub(BorrowAssetAmount::new(0));
636        assert_eq!(v, Some(BorrowAssetAmount::new(0)));
637        let v = BorrowAssetAmount::new(0).checked_sub(BorrowAssetAmount::new(100));
638        assert_eq!(v, None);
639        let v = BorrowAssetAmount::new(100).checked_sub(BorrowAssetAmount::new(0));
640        assert_eq!(v, Some(BorrowAssetAmount::new(100)));
641        let v = BorrowAssetAmount::new(100).checked_sub(BorrowAssetAmount::new(100));
642        assert_eq!(v, Some(BorrowAssetAmount::new(0)));
643        let v = BorrowAssetAmount::new(1).checked_sub(BorrowAssetAmount::new(u128::MAX - 33));
644        assert_eq!(v, None);
645    }
646
647    #[test]
648    fn saturating_add() {
649        let v = BorrowAssetAmount::new(0).saturating_add(BorrowAssetAmount::new(0));
650        assert_eq!(v, BorrowAssetAmount::new(0));
651        let v = BorrowAssetAmount::new(0).saturating_add(BorrowAssetAmount::new(100));
652        assert_eq!(v, BorrowAssetAmount::new(100));
653        let v = BorrowAssetAmount::new(100).saturating_add(BorrowAssetAmount::new(0));
654        assert_eq!(v, BorrowAssetAmount::new(100));
655        let v = BorrowAssetAmount::new(100).saturating_add(BorrowAssetAmount::new(100));
656        assert_eq!(v, BorrowAssetAmount::new(200));
657        let v = BorrowAssetAmount::new(100).saturating_add(BorrowAssetAmount::new(u128::MAX - 33));
658        assert_eq!(v, BorrowAssetAmount::new(u128::MAX));
659    }
660
661    #[test]
662    fn saturating_sub() {
663        let v = BorrowAssetAmount::new(0).saturating_sub(BorrowAssetAmount::new(0));
664        assert_eq!(v, BorrowAssetAmount::new(0));
665        let v = BorrowAssetAmount::new(0).saturating_sub(BorrowAssetAmount::new(100));
666        assert_eq!(v, BorrowAssetAmount::new(0));
667        let v = BorrowAssetAmount::new(100).saturating_sub(BorrowAssetAmount::new(0));
668        assert_eq!(v, BorrowAssetAmount::new(100));
669        let v = BorrowAssetAmount::new(100).saturating_sub(BorrowAssetAmount::new(100));
670        assert_eq!(v, BorrowAssetAmount::new(0));
671        let v = BorrowAssetAmount::new(100).saturating_sub(BorrowAssetAmount::new(u128::MAX - 33));
672        assert_eq!(v, BorrowAssetAmount::new(0));
673    }
674
675    #[test]
676    #[should_panic = "overflow"]
677    fn overflow_unwrap_add() {
678        let _ =
679            BorrowAssetAmount::new(100).unwrap_add(BorrowAssetAmount::new(u128::MAX), "overflow");
680    }
681
682    #[test]
683    #[should_panic = "overflow"]
684    fn overflow_unwrap_sub() {
685        let _ =
686            BorrowAssetAmount::new(100).unwrap_sub(BorrowAssetAmount::new(u128::MAX), "overflow");
687    }
688
689    #[test]
690    #[should_panic = "attempt to add with overflow"]
691    fn overflow_add() {
692        let _ = BorrowAssetAmount::new(u128::MAX) + BorrowAssetAmount::new(1);
693    }
694
695    #[test]
696    #[should_panic = "attempt to add with overflow"]
697    fn overflow_add_assign() {
698        let mut v = BorrowAssetAmount::new(u128::MAX);
699        v += BorrowAssetAmount::new(1);
700    }
701
702    #[test]
703    #[should_panic = "attempt to subtract with overflow"]
704    fn overflow_sub() {
705        let _ = BorrowAssetAmount::new(0) - BorrowAssetAmount::new(1);
706    }
707
708    #[test]
709    #[should_panic = "attempt to subtract with overflow"]
710    fn overflow_sub_assign() {
711        let mut v = BorrowAssetAmount::new(1);
712        v -= BorrowAssetAmount::new(u128::MAX);
713    }
714}
715
716#[derive(Clone, Debug)]
717#[near(serializers = [json])]
718pub enum ReturnStyle {
719    Nep141FtTransferCall,
720    Nep245MtTransferCall,
721}
722
723impl ReturnStyle {
724    pub fn serialize(&self, amount: FungibleAssetAmount<impl AssetClass>) -> serde_json::Value {
725        match self {
726            Self::Nep141FtTransferCall => serde_json::json!(amount),
727            Self::Nep245MtTransferCall => serde_json::json!([amount]),
728        }
729    }
730}