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#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
39#[near(serializers = [json, borsh])]
40pub struct FungibleAsset<T: AssetClass> {
41 #[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 pub const GAS_FT_TRANSFER: Gas = Gas::from_tgas(6);
63
64 pub const GAS_MT_TRANSFER: Gas = Gas::from_tgas(7);
66
67 pub const GAS_FT_TRANSFER_CALL: Gas = Gas::from_tgas(100);
71
72 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 #[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), }
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 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 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}