Skip to main content

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, promise_result_checked_json_with_len};
10use defuse_nep245::{MtBurnEvent, MtEvent, MtMintEvent};
11use itertools::{Either, Itertools};
12use near_sdk::{AccountId, AccountIdRef, FunctionError, Gas, env, json_types::U128};
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(_) | TokenId::Imt(_) => {}
59            }
60
61            owner
62                .token_balances
63                .add(token_id, amount)
64                .ok_or(DefuseError::BalanceOverflow)?;
65        }
66
67        if !mint_event.amounts.is_empty() {
68            MtEvent::MtMint([mint_event].as_slice().into())
69                .check_refund()?
70                .emit();
71        }
72
73        Ok(())
74    }
75
76    pub(crate) fn withdraw(
77        &mut self,
78        owner_id: &AccountIdRef,
79        token_amounts: impl IntoIterator<Item = (TokenId, u128)>,
80        memo: Option<impl Into<String>>,
81        force: bool,
82    ) -> Result<()> {
83        let owner = self
84            .storage
85            .accounts
86            .get_mut(owner_id)
87            .ok_or_else(|| DefuseError::AccountNotFound(owner_id.to_owned()))?
88            .get_mut_maybe_forced(force)
89            .ok_or_else(|| DefuseError::AccountLocked(owner_id.to_owned()))?;
90
91        let mut burn_event = MtBurnEvent {
92            owner_id: Cow::Owned(owner_id.to_owned()),
93            authorized_id: None,
94            token_ids: Vec::new().into(),
95            amounts: Vec::new().into(),
96            memo: memo.map(Into::into).map(Into::into),
97        };
98
99        for (token_id, amount) in token_amounts {
100            if amount == 0 {
101                return Err(DefuseError::InvalidIntent);
102            }
103
104            burn_event.token_ids.to_mut().push(token_id.to_string());
105            burn_event.amounts.to_mut().push(U128(amount));
106
107            owner
108                .token_balances
109                .sub(token_id.clone(), amount)
110                .ok_or(DefuseError::BalanceOverflow)?;
111
112            self.storage
113                .state
114                .total_supplies
115                .sub(token_id, amount)
116                .ok_or(DefuseError::BalanceOverflow)?;
117        }
118
119        // Schedule to emit `mt_burn` events only in the end of tx
120        // to avoid confusion when `mt_burn` occurs before relevant
121        // `mt_transfer` arrives. This can happen due to postponed
122        // delta-matching during intents execution.
123        if !burn_event.amounts.is_empty() {
124            self.runtime.postponed_burns.mt_burn(burn_event);
125        }
126
127        Ok(())
128    }
129}
130
131impl Contract {
132    #[must_use]
133    pub(crate) fn mt_resolve_deposit_gas(token_count: usize) -> Gas {
134        const MT_RESOLVE_DEPOSIT_PER_TOKEN_GAS: Gas = Gas::from_tgas(2);
135        const MT_RESOLVE_DEPOSIT_BASE_GAS: Gas = Gas::from_tgas(4);
136
137        let token_count: u64 = token_count
138            .try_into()
139            .unwrap_or_else(|_| env::panic_str(&format!("token_count overflow: {token_count}")));
140
141        MT_RESOLVE_DEPOSIT_BASE_GAS
142            .checked_add(
143                MT_RESOLVE_DEPOSIT_PER_TOKEN_GAS
144                    .checked_mul(token_count)
145                    .ok_or(DefuseError::GasOverflow)
146                    .unwrap_or_else(|err| err.panic()),
147            )
148            .ok_or(DefuseError::GasOverflow)
149            .unwrap_or_else(|err| err.panic())
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 = promise_result_checked_json_with_len::<Vec<U128>>(0, tokens_count)
161            .ok()
162            .and_then(Result::ok)
163            .filter(|refunds| refunds.len() == tokens_count);
164
165        let mut burn_event = MtBurnEvent {
166            owner_id: Cow::Borrowed(receiver_id),
167            authorized_id: None,
168            token_ids: Vec::with_capacity(tokens_count).into(),
169            amounts: Vec::with_capacity(tokens_count).into(),
170            memo: Some(REFUND_MEMO.into()),
171        };
172
173        let Some(receiver) = self
174            .storage
175            .accounts
176            .get_mut(receiver_id)
177            .map(Lock::as_inner_unchecked_mut)
178        else {
179            tokens_iter.for_each(|(_, amount)| *amount = 0);
180            return;
181        };
182
183        for ((token_id, deposited), requested_refund) in
184            tokens_iter.zip_eq(requested_refunds.map_or_else(
185                || Either::Right(std::iter::repeat_n(None, tokens_count)),
186                |v| Either::Left(v.into_iter().map(|elem| Some(elem.0))),
187            ))
188        {
189            let requested_refund = requested_refund.unwrap_or(*deposited);
190            let balance_left = receiver.token_balances.amount_for(&token_id);
191            // NOTE: refunds are capped by deposited amounts and balance left on the receiver
192            let refund_amount = requested_refund.min(*deposited).min(balance_left);
193            *deposited = refund_amount;
194            if refund_amount == 0 {
195                continue;
196            }
197
198            burn_event.token_ids.to_mut().push(token_id.to_string());
199            burn_event.amounts.to_mut().push(U128(refund_amount));
200
201            receiver
202                .token_balances
203                .sub(token_id.clone(), refund_amount)
204                .ok_or(DefuseError::BalanceOverflow)
205                .unwrap_or_else(|err| err.panic());
206
207            self.storage
208                .state
209                .total_supplies
210                .sub(token_id, refund_amount)
211                .ok_or(DefuseError::BalanceOverflow)
212                .unwrap_or_else(|err| err.panic());
213        }
214
215        if !burn_event.amounts.is_empty() {
216            // NOTE: No need for `check_refund()` here since this IS the refund.
217            // The refund memo size was already accounted for in the original mint.
218            MtEvent::MtBurn([burn_event].as_slice().into()).emit();
219        }
220    }
221}