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