defuse/contract/tokens/
mod.rs

1#[cfg(feature = "imt")]
2mod imt;
3mod nep141;
4mod nep171;
5mod nep245;
6
7use super::Contract;
8use defuse_core::{DefuseError, Result, token_id::TokenId};
9use defuse_near_utils::{Lock, REFUND_MEMO, UnwrapOrPanic, UnwrapOrPanicError};
10use defuse_nep245::{MtBurnEvent, MtEvent, MtMintEvent};
11use itertools::{Either, Itertools};
12use near_sdk::{AccountId, AccountIdRef, Gas, env, json_types::U128, serde_json};
13use std::borrow::Cow;
14
15pub const STORAGE_DEPOSIT_GAS: Gas = Gas::from_tgas(10);
16
17impl Contract {
18    pub(crate) fn deposit(
19        &mut self,
20        owner_id: AccountId,
21        tokens: impl IntoIterator<Item = (TokenId, u128)>,
22        memo: Option<&str>,
23    ) -> Result<()> {
24        let owner = self
25            .storage
26            .accounts
27            .get_or_create(owner_id.clone())
28            // deposits are allowed for locked accounts
29            .as_inner_unchecked_mut();
30
31        let mut mint_event = MtMintEvent {
32            owner_id: owner_id.into(),
33            token_ids: Vec::new().into(),
34            amounts: Vec::new().into(),
35            memo: memo.map(Into::into),
36        };
37
38        for (token_id, amount) in tokens {
39            if amount == 0 {
40                return Err(DefuseError::InvalidIntent);
41            }
42
43            mint_event.token_ids.to_mut().push(token_id.to_string());
44            mint_event.amounts.to_mut().push(U128(amount));
45
46            let total_supply = self
47                .storage
48                .state
49                .total_supplies
50                .add(token_id.clone(), amount)
51                .ok_or(DefuseError::BalanceOverflow)?;
52            match token_id {
53                TokenId::Nep171(ref tid) => {
54                    if total_supply > 1 {
55                        return Err(DefuseError::NftAlreadyDeposited(tid.clone()));
56                    }
57                }
58                TokenId::Nep141(_) | TokenId::Nep245(_) => {}
59                #[cfg(feature = "imt")]
60                TokenId::Imt(_) => {}
61            }
62
63            owner
64                .token_balances
65                .add(token_id, amount)
66                .ok_or(DefuseError::BalanceOverflow)?;
67        }
68
69        if !mint_event.amounts.is_empty() {
70            MtEvent::MtMint([mint_event].as_slice().into())
71                .check_refund()?
72                .emit();
73        }
74
75        Ok(())
76    }
77
78    pub(crate) fn withdraw(
79        &mut self,
80        owner_id: &AccountIdRef,
81        token_amounts: impl IntoIterator<Item = (TokenId, u128)>,
82        memo: Option<impl Into<String>>,
83        force: bool,
84    ) -> Result<()> {
85        let owner = self
86            .storage
87            .accounts
88            .get_mut(owner_id)
89            .ok_or_else(|| DefuseError::AccountNotFound(owner_id.to_owned()))?
90            .get_mut_maybe_forced(force)
91            .ok_or_else(|| DefuseError::AccountLocked(owner_id.to_owned()))?;
92
93        let mut burn_event = MtBurnEvent {
94            owner_id: Cow::Owned(owner_id.to_owned()),
95            authorized_id: None,
96            token_ids: Vec::new().into(),
97            amounts: Vec::new().into(),
98            memo: memo.map(Into::into).map(Into::into),
99        };
100
101        for (token_id, amount) in token_amounts {
102            if amount == 0 {
103                return Err(DefuseError::InvalidIntent);
104            }
105
106            burn_event.token_ids.to_mut().push(token_id.to_string());
107            burn_event.amounts.to_mut().push(U128(amount));
108
109            owner
110                .token_balances
111                .sub(token_id.clone(), amount)
112                .ok_or(DefuseError::BalanceOverflow)?;
113
114            self.storage
115                .state
116                .total_supplies
117                .sub(token_id, amount)
118                .ok_or(DefuseError::BalanceOverflow)?;
119        }
120
121        // Schedule to emit `mt_burn` events only in the end of tx
122        // to avoid confusion when `mt_burn` occurs before relevant
123        // `mt_transfer` arrives. This can happen due to postponed
124        // delta-matching during intents execution.
125        if !burn_event.amounts.is_empty() {
126            self.runtime.postponed_burns.mt_burn(burn_event);
127        }
128
129        Ok(())
130    }
131}
132
133impl Contract {
134    #[must_use]
135    pub(crate) fn mt_resolve_deposit_gas(token_count: usize) -> Gas {
136        const MT_RESOLVE_DEPOSIT_PER_TOKEN_GAS: Gas = Gas::from_tgas(2);
137        const MT_RESOLVE_DEPOSIT_BASE_GAS: Gas = Gas::from_tgas(4);
138
139        let token_count: u64 = token_count
140            .try_into()
141            .unwrap_or_else(|_| env::panic_str(&format!("token_count overflow: {token_count}")));
142
143        MT_RESOLVE_DEPOSIT_BASE_GAS
144            .checked_add(
145                MT_RESOLVE_DEPOSIT_PER_TOKEN_GAS
146                    .checked_mul(token_count)
147                    .unwrap_or_else(|| env::panic_str("gas calculation overflow")),
148            )
149            .unwrap_or_else(|| env::panic_str("gas calculation overflow"))
150    }
151
152    pub fn resolve_deposit_internal<'a, I>(&mut self, receiver_id: &AccountIdRef, tokens: I)
153    where
154        I: IntoIterator<Item = (TokenId, &'a mut u128)>,
155        I::IntoIter: ExactSizeIterator,
156    {
157        let tokens_iter = tokens.into_iter();
158        let tokens_count = tokens_iter.len();
159
160        let requested_refunds =
161            env::promise_result_checked(0, Self::mt_on_transfer_max_result_len(tokens_count))
162                .ok()
163                .and_then(|value| serde_json::from_slice::<Vec<U128>>(&value).ok())
164                .filter(|refunds| refunds.len() == tokens_count);
165
166        let mut burn_event = MtBurnEvent {
167            owner_id: Cow::Borrowed(receiver_id),
168            authorized_id: None,
169            token_ids: Vec::with_capacity(tokens_count).into(),
170            amounts: Vec::with_capacity(tokens_count).into(),
171            memo: Some(REFUND_MEMO.into()),
172        };
173
174        let Some(receiver) = self
175            .storage
176            .accounts
177            .get_mut(receiver_id)
178            .map(Lock::as_inner_unchecked_mut)
179        else {
180            tokens_iter.for_each(|(_, amount)| *amount = 0);
181            return;
182        };
183
184        for ((token_id, deposited), requested_refund) in
185            tokens_iter.zip_eq(requested_refunds.map_or_else(
186                || Either::Right(std::iter::repeat_n(None, tokens_count)),
187                |v| Either::Left(v.into_iter().map(|elem| Some(elem.0))),
188            ))
189        {
190            //NOTE: refunds are capped by deposited amounts
191            let requested_refund = requested_refund.unwrap_or(*deposited);
192            let balance_left = receiver.token_balances.amount_for(&token_id);
193            let refund_amount = balance_left.min(requested_refund);
194            *deposited = refund_amount;
195            if refund_amount == 0 {
196                continue;
197            }
198
199            burn_event.token_ids.to_mut().push(token_id.to_string());
200            burn_event.amounts.to_mut().push(U128(refund_amount));
201
202            receiver
203                .token_balances
204                .sub(token_id.clone(), refund_amount)
205                .ok_or(DefuseError::BalanceOverflow)
206                .unwrap_or_panic();
207
208            self.storage
209                .state
210                .total_supplies
211                .sub(token_id, refund_amount)
212                .ok_or(DefuseError::BalanceOverflow)
213                .unwrap_or_panic();
214        }
215
216        if !burn_event.amounts.is_empty() {
217            MtEvent::MtBurn([burn_event].as_slice().into())
218                .check_refund()
219                .unwrap_or_panic_display()
220                .emit();
221        }
222    }
223
224    const fn mt_on_transfer_max_result_len(amounts_count: usize) -> usize {
225        // we allow at most one newline char and up to 8 spaces/tabs if the JSON was prettified
226        const MAX_LEN_PER_AMOUNT: usize =
227            "        \"+340282366920938463463374607431768211455\",\n".len(); // u128::MAX
228
229        amounts_count
230            .saturating_mul(MAX_LEN_PER_AMOUNT)
231            .saturating_add("[\n]".len())
232    }
233}