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