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")]
414pub struct FungibleAssetAmount<T: AssetClass> {
415    amount: U128,
416    #[borsh(skip)]
417    discriminant: PhantomData<T>,
418}
419
420impl<T: AssetClass> Debug for FungibleAssetAmount<T> {
421    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
422        write!(f, "{}<{}>", self.amount.0, std::any::type_name::<T>())
423    }
424}
425
426impl<T: AssetClass> Default for FungibleAssetAmount<T> {
427    fn default() -> Self {
428        Self::zero()
429    }
430}
431
432impl<T: AssetClass> From<U128> for FungibleAssetAmount<T> {
433    fn from(amount: U128) -> Self {
434        Self {
435            amount,
436            discriminant: PhantomData,
437        }
438    }
439}
440
441impl<T: AssetClass> From<FungibleAssetAmount<T>> for U128 {
442    fn from(value: FungibleAssetAmount<T>) -> Self {
443        value.amount
444    }
445}
446
447impl<T: AssetClass> From<u128> for FungibleAssetAmount<T> {
448    fn from(value: u128) -> Self {
449        Self::new(value)
450    }
451}
452
453impl<T: AssetClass> FungibleAssetAmount<T> {
454    pub fn new(amount: u128) -> Self {
455        Self {
456            amount: U128(amount),
457            discriminant: PhantomData,
458        }
459    }
460
461    pub const fn zero() -> Self {
462        Self {
463            amount: U128(0),
464            discriminant: PhantomData,
465        }
466    }
467
468    pub fn is_zero(&self) -> bool {
469        self.amount.0 == 0
470    }
471
472    #[must_use]
473    pub fn unwrap_add(self, other: impl Into<Self>, message: &str) -> Self {
474        Self {
475            amount: self
476                .amount
477                .0
478                .checked_add(other.into().amount.0)
479                .unwrap_or_else(|| panic_with_message(&format!("Arithmetic overflow: {message}")))
480                .into(),
481            ..self
482        }
483    }
484
485    #[must_use]
486    pub fn saturating_add(self, other: impl Into<Self>) -> Self {
487        Self {
488            amount: self.amount.0.saturating_add(other.into().amount.0).into(),
489            ..self
490        }
491    }
492
493    #[must_use]
494    pub fn checked_add(self, other: impl Into<Self>) -> Option<Self> {
495        Some(Self {
496            amount: self.amount.0.checked_add(other.into().amount.0)?.into(),
497            ..self
498        })
499    }
500
501    #[must_use]
502    pub fn unwrap_sub(self, other: impl Into<Self>, message: &str) -> Self {
503        Self {
504            amount: self
505                .amount
506                .0
507                .checked_sub(other.into().amount.0)
508                .unwrap_or_else(|| panic_with_message(&format!("Arithmetic underflow: {message}")))
509                .into(),
510            ..self
511        }
512    }
513
514    #[must_use]
515    pub fn saturating_sub(self, other: impl Into<Self>) -> Self {
516        Self {
517            amount: self.amount.0.saturating_sub(other.into().amount.0).into(),
518            ..self
519        }
520    }
521
522    #[must_use]
523    pub fn checked_sub(self, other: impl Into<Self>) -> Option<Self> {
524        Some(Self {
525            amount: self.amount.0.checked_sub(other.into().amount.0)?.into(),
526            ..self
527        })
528    }
529}
530
531impl<T: AssetClass> From<FungibleAssetAmount<T>> for Decimal {
532    fn from(value: FungibleAssetAmount<T>) -> Self {
533        value.amount.0.into()
534    }
535}
536
537impl<T: AssetClass> From<FungibleAssetAmount<T>> for u128 {
538    fn from(value: FungibleAssetAmount<T>) -> Self {
539        value.amount.0
540    }
541}
542
543impl<T: AssetClass> std::fmt::Display for FungibleAssetAmount<T> {
544    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
545        write!(f, "{}", self.amount.0)
546    }
547}
548
549impl<T: AssetClass, R: Into<Self>> std::ops::Add<R> for FungibleAssetAmount<T> {
550    type Output = Self;
551
552    fn add(self, rhs: R) -> Self::Output {
553        Self {
554            amount: U128(self.amount.0 + rhs.into().amount.0),
555            ..self
556        }
557    }
558}
559
560impl<T: AssetClass, R: Into<Self>> std::ops::AddAssign<R> for FungibleAssetAmount<T> {
561    fn add_assign(&mut self, rhs: R) {
562        self.amount.0 += rhs.into().amount.0;
563    }
564}
565
566impl<T: AssetClass, R: Into<Self>> std::ops::Sub<R> for FungibleAssetAmount<T> {
567    type Output = Self;
568
569    fn sub(self, rhs: R) -> Self::Output {
570        Self {
571            amount: U128(self.amount.0 - rhs.into().amount.0),
572            ..self
573        }
574    }
575}
576
577impl<T: AssetClass, R: Into<Self>> std::ops::SubAssign<R> for FungibleAssetAmount<T> {
578    fn sub_assign(&mut self, rhs: R) {
579        self.amount.0 -= rhs.into().amount.0;
580    }
581}
582
583pub type BorrowAssetAmount = FungibleAssetAmount<BorrowAsset>;
584pub type CollateralAssetAmount = FungibleAssetAmount<CollateralAsset>;
585
586#[cfg(test)]
587mod tests {
588    use super::*;
589    use near_sdk::serde_json;
590
591    #[test]
592    fn serialization() {
593        let amount = BorrowAssetAmount::new(100);
594        let serialized = serde_json::to_string(&amount).unwrap();
595        assert_eq!(serialized, "\"100\"");
596        let deserialized: BorrowAssetAmount = serde_json::from_str(&serialized).unwrap();
597        assert_eq!(deserialized, amount);
598    }
599
600    #[test]
601    fn checked_add() {
602        let v = BorrowAssetAmount::new(0).checked_add(BorrowAssetAmount::new(0));
603        assert_eq!(v, Some(BorrowAssetAmount::new(0)));
604        let v = BorrowAssetAmount::new(0).checked_add(BorrowAssetAmount::new(100));
605        assert_eq!(v, Some(BorrowAssetAmount::new(100)));
606        let v = BorrowAssetAmount::new(100).checked_add(BorrowAssetAmount::new(0));
607        assert_eq!(v, Some(BorrowAssetAmount::new(100)));
608        let v = BorrowAssetAmount::new(100).checked_add(BorrowAssetAmount::new(100));
609        assert_eq!(v, Some(BorrowAssetAmount::new(200)));
610        let v = BorrowAssetAmount::new(1).checked_add(BorrowAssetAmount::new(u128::MAX));
611        assert_eq!(v, None);
612    }
613
614    #[test]
615    fn checked_sub() {
616        let v = BorrowAssetAmount::new(0).checked_sub(BorrowAssetAmount::new(0));
617        assert_eq!(v, Some(BorrowAssetAmount::new(0)));
618        let v = BorrowAssetAmount::new(0).checked_sub(BorrowAssetAmount::new(100));
619        assert_eq!(v, None);
620        let v = BorrowAssetAmount::new(100).checked_sub(BorrowAssetAmount::new(0));
621        assert_eq!(v, Some(BorrowAssetAmount::new(100)));
622        let v = BorrowAssetAmount::new(100).checked_sub(BorrowAssetAmount::new(100));
623        assert_eq!(v, Some(BorrowAssetAmount::new(0)));
624        let v = BorrowAssetAmount::new(1).checked_sub(BorrowAssetAmount::new(u128::MAX - 33));
625        assert_eq!(v, None);
626    }
627
628    #[test]
629    fn saturating_add() {
630        let v = BorrowAssetAmount::new(0).saturating_add(BorrowAssetAmount::new(0));
631        assert_eq!(v, BorrowAssetAmount::new(0));
632        let v = BorrowAssetAmount::new(0).saturating_add(BorrowAssetAmount::new(100));
633        assert_eq!(v, BorrowAssetAmount::new(100));
634        let v = BorrowAssetAmount::new(100).saturating_add(BorrowAssetAmount::new(0));
635        assert_eq!(v, BorrowAssetAmount::new(100));
636        let v = BorrowAssetAmount::new(100).saturating_add(BorrowAssetAmount::new(100));
637        assert_eq!(v, BorrowAssetAmount::new(200));
638        let v = BorrowAssetAmount::new(100).saturating_add(BorrowAssetAmount::new(u128::MAX - 33));
639        assert_eq!(v, BorrowAssetAmount::new(u128::MAX));
640    }
641
642    #[test]
643    fn saturating_sub() {
644        let v = BorrowAssetAmount::new(0).saturating_sub(BorrowAssetAmount::new(0));
645        assert_eq!(v, BorrowAssetAmount::new(0));
646        let v = BorrowAssetAmount::new(0).saturating_sub(BorrowAssetAmount::new(100));
647        assert_eq!(v, BorrowAssetAmount::new(0));
648        let v = BorrowAssetAmount::new(100).saturating_sub(BorrowAssetAmount::new(0));
649        assert_eq!(v, BorrowAssetAmount::new(100));
650        let v = BorrowAssetAmount::new(100).saturating_sub(BorrowAssetAmount::new(100));
651        assert_eq!(v, BorrowAssetAmount::new(0));
652        let v = BorrowAssetAmount::new(100).saturating_sub(BorrowAssetAmount::new(u128::MAX - 33));
653        assert_eq!(v, BorrowAssetAmount::new(0));
654    }
655
656    #[test]
657    #[should_panic = "overflow"]
658    fn overflow_unwrap_add() {
659        let _ =
660            BorrowAssetAmount::new(100).unwrap_add(BorrowAssetAmount::new(u128::MAX), "overflow");
661    }
662
663    #[test]
664    #[should_panic = "overflow"]
665    fn overflow_unwrap_sub() {
666        let _ =
667            BorrowAssetAmount::new(100).unwrap_sub(BorrowAssetAmount::new(u128::MAX), "overflow");
668    }
669
670    #[test]
671    #[should_panic = "attempt to add with overflow"]
672    fn overflow_add() {
673        let _ = BorrowAssetAmount::new(u128::MAX) + BorrowAssetAmount::new(1);
674    }
675
676    #[test]
677    #[should_panic = "attempt to add with overflow"]
678    fn overflow_add_assign() {
679        let mut v = BorrowAssetAmount::new(u128::MAX);
680        v += BorrowAssetAmount::new(1);
681    }
682
683    #[test]
684    #[should_panic = "attempt to subtract with overflow"]
685    fn overflow_sub() {
686        let _ = BorrowAssetAmount::new(0) - BorrowAssetAmount::new(1);
687    }
688
689    #[test]
690    #[should_panic = "attempt to subtract with overflow"]
691    fn overflow_sub_assign() {
692        let mut v = BorrowAssetAmount::new(1);
693        v -= BorrowAssetAmount::new(u128::MAX);
694    }
695}
696
697#[derive(Clone, Debug)]
698#[near(serializers = [json])]
699pub enum ReturnStyle {
700    Nep141FtTransferCall,
701    Nep245MtTransferCall,
702}
703
704impl ReturnStyle {
705    pub fn serialize(&self, amount: FungibleAssetAmount<impl AssetClass>) -> serde_json::Value {
706        match self {
707            Self::Nep141FtTransferCall => serde_json::json!(amount),
708            Self::Nep245MtTransferCall => serde_json::json!([amount]),
709        }
710    }
711}