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