defuse/contract/tokens/nep245/
core.rs

1use crate::contract::{Contract, ContractExt};
2use defuse_core::{
3    DefuseError, Result, engine::StateView, intents::tokens::NotifyOnTransfer, token_id::TokenId,
4};
5use defuse_near_utils::{UnwrapOrPanic, UnwrapOrPanicError};
6use defuse_nep245::{MtEvent, MtTransferEvent, MultiTokenCore, receiver::ext_mt_receiver};
7use near_plugins::{Pausable, pause};
8use near_sdk::{
9    AccountId, AccountIdRef, Gas, NearToken, Promise, PromiseOrValue, assert_one_yocto, env,
10    json_types::U128, near, require,
11};
12use std::borrow::Cow;
13
14#[near]
15impl MultiTokenCore for Contract {
16    #[payable]
17    fn mt_transfer(
18        &mut self,
19        receiver_id: AccountId,
20        token_id: defuse_nep245::TokenId,
21        amount: U128,
22        approval: Option<(AccountId, u64)>,
23        memo: Option<String>,
24    ) {
25        self.mt_batch_transfer(
26            receiver_id,
27            [token_id].into(),
28            [amount].into(),
29            approval.map(|a| vec![Some(a)]),
30            memo,
31        );
32    }
33
34    #[pause(name = "mt_transfer")]
35    #[payable]
36    fn mt_batch_transfer(
37        &mut self,
38        receiver_id: AccountId,
39        token_ids: Vec<defuse_nep245::TokenId>,
40        amounts: Vec<U128>,
41        approvals: Option<Vec<Option<(AccountId, u64)>>>,
42        memo: Option<String>,
43    ) {
44        assert_one_yocto();
45        require!(approvals.is_none(), "approvals are not supported");
46
47        self.internal_mt_batch_transfer(
48            &self.ensure_auth_predecessor_id(),
49            &receiver_id,
50            &token_ids,
51            &amounts,
52            memo.as_deref(),
53            false,
54        )
55        .unwrap_or_panic()
56    }
57
58    #[pause(name = "mt_transfer")]
59    #[payable]
60    fn mt_transfer_call(
61        &mut self,
62        receiver_id: AccountId,
63        token_id: defuse_nep245::TokenId,
64        amount: U128,
65        approval: Option<(AccountId, u64)>,
66        memo: Option<String>,
67        msg: String,
68    ) -> PromiseOrValue<Vec<U128>> {
69        self.mt_batch_transfer_call(
70            receiver_id,
71            [token_id].into(),
72            [amount].into(),
73            approval.map(|a| vec![Some(a)]),
74            memo,
75            msg,
76        )
77    }
78
79    #[pause(name = "mt_transfer")]
80    #[payable]
81    fn mt_batch_transfer_call(
82        &mut self,
83        receiver_id: AccountId,
84        token_ids: Vec<defuse_nep245::TokenId>,
85        amounts: Vec<U128>,
86        approvals: Option<Vec<Option<(AccountId, u64)>>>,
87        memo: Option<String>,
88        msg: String,
89    ) -> PromiseOrValue<Vec<U128>> {
90        assert_one_yocto();
91        require!(approvals.is_none(), "approvals are not supported");
92
93        self.internal_mt_batch_transfer_call(
94            self.ensure_auth_predecessor_id(),
95            receiver_id,
96            token_ids,
97            amounts,
98            memo.as_deref(),
99            msg,
100            false,
101        )
102        .unwrap_or_panic()
103    }
104
105    fn mt_token(
106        &self,
107        token_ids: Vec<defuse_nep245::TokenId>,
108    ) -> Vec<Option<defuse_nep245::Token>> {
109        token_ids
110            .into_iter()
111            .map(|token_id| {
112                self.total_supplies
113                    .contains_key(&token_id.parse().ok()?)
114                    .then_some(defuse_nep245::Token {
115                        token_id,
116                        owner_id: None,
117                    })
118            })
119            .collect()
120    }
121
122    fn mt_balance_of(&self, account_id: AccountId, token_id: defuse_nep245::TokenId) -> U128 {
123        U128(self.internal_mt_balance_of(&account_id, &token_id))
124    }
125
126    fn mt_batch_balance_of(
127        &self,
128        account_id: AccountId,
129        token_ids: Vec<defuse_nep245::TokenId>,
130    ) -> Vec<U128> {
131        token_ids
132            .into_iter()
133            .map(|token_id| self.internal_mt_balance_of(&account_id, &token_id))
134            .map(U128)
135            .collect()
136    }
137
138    fn mt_supply(&self, token_id: defuse_nep245::TokenId) -> Option<U128> {
139        Some(U128(
140            self.total_supplies.amount_for(&token_id.parse().ok()?),
141        ))
142    }
143
144    fn mt_batch_supply(&self, token_ids: Vec<defuse_nep245::TokenId>) -> Vec<Option<U128>> {
145        token_ids
146            .into_iter()
147            .map(|token_id| self.mt_supply(token_id))
148            .collect()
149    }
150}
151
152impl Contract {
153    pub(crate) fn internal_mt_balance_of(
154        &self,
155        account_id: &AccountIdRef,
156        token_id: &defuse_nep245::TokenId,
157    ) -> u128 {
158        let Ok(token_id) = token_id.parse() else {
159            return 0;
160        };
161        self.balance_of(account_id, &token_id)
162    }
163
164    pub(crate) fn internal_mt_batch_transfer(
165        &mut self,
166        sender_id: &AccountIdRef,
167        receiver_id: &AccountIdRef,
168        token_ids: &[defuse_nep245::TokenId],
169        amounts: &[U128],
170        memo: Option<&str>,
171        force: bool,
172    ) -> Result<()> {
173        if sender_id == receiver_id || token_ids.len() != amounts.len() || amounts.is_empty() {
174            return Err(DefuseError::InvalidIntent);
175        }
176
177        for (token_id, amount) in token_ids.iter().zip(amounts.iter().map(|a| a.0)) {
178            if amount == 0 {
179                return Err(DefuseError::InvalidIntent);
180            }
181            let token_id: TokenId = token_id.parse()?;
182
183            self.accounts
184                .get_mut(sender_id)
185                .ok_or_else(|| DefuseError::AccountNotFound(sender_id.to_owned()))?
186                .get_mut_maybe_forced(force)
187                .ok_or_else(|| DefuseError::AccountLocked(sender_id.to_owned()))?
188                .token_balances
189                .sub(token_id.clone(), amount)
190                .ok_or(DefuseError::BalanceOverflow)?;
191            self.accounts
192                .get_or_create(receiver_id.to_owned())
193                // locked accounts are allowed to receive incoming transfers
194                .as_inner_unchecked_mut()
195                .token_balances
196                .add(token_id, amount)
197                .ok_or(DefuseError::BalanceOverflow)?;
198        }
199
200        MtEvent::MtTransfer(
201            [MtTransferEvent {
202                authorized_id: None,
203                old_owner_id: sender_id.into(),
204                new_owner_id: Cow::Borrowed(receiver_id),
205                token_ids: token_ids.into(),
206                amounts: amounts.into(),
207                memo: memo.map(Into::into),
208            }]
209            .as_slice()
210            .into(),
211        )
212        .check_refund()?
213        .emit();
214
215        Ok(())
216    }
217
218    #[allow(clippy::too_many_arguments)]
219    pub(crate) fn internal_mt_batch_transfer_call(
220        &mut self,
221        sender_id: AccountId,
222        receiver_id: AccountId,
223        token_ids: Vec<defuse_nep245::TokenId>,
224        amounts: Vec<U128>,
225        memo: Option<&str>,
226        msg: String,
227        force: bool,
228    ) -> Result<PromiseOrValue<Vec<U128>>> {
229        self.internal_mt_batch_transfer(
230            &sender_id,
231            &receiver_id,
232            &token_ids,
233            &amounts,
234            memo,
235            force,
236        )?;
237
238        Ok(Self::notify_and_resolve_transfer(
239            sender_id,
240            receiver_id,
241            token_ids,
242            amounts,
243            NotifyOnTransfer::new(msg),
244        ))
245    }
246
247    pub(crate) fn notify_and_resolve_transfer(
248        sender_id: AccountId,
249        receiver_id: AccountId,
250        token_ids: Vec<defuse_nep245::TokenId>,
251        amounts: Vec<U128>,
252        notify: NotifyOnTransfer,
253    ) -> PromiseOrValue<Vec<U128>> {
254        let previous_owner_ids = vec![sender_id.clone(); token_ids.len()];
255
256        Self::notify_on_transfer(
257            sender_id,
258            previous_owner_ids.clone(),
259            receiver_id.clone(),
260            token_ids.clone(),
261            amounts.clone(),
262            notify,
263        )
264        .then(
265            Self::ext(env::current_account_id())
266                .with_static_gas(Self::mt_resolve_gas(token_ids.len()))
267                // do not distribute remaining gas here (so that all that's left goes to `mt_on_transfer`)
268                .with_unused_gas_weight(0)
269                .mt_resolve_transfer(previous_owner_ids, receiver_id, token_ids, amounts, None),
270        )
271        .into()
272    }
273
274    pub(crate) fn notify_on_transfer(
275        sender_id: AccountId,
276        previous_owner_ids: Vec<AccountId>,
277        receiver_id: AccountId,
278        token_ids: Vec<defuse_nep245::TokenId>,
279        amounts: Vec<U128>,
280        notify: NotifyOnTransfer,
281    ) -> Promise {
282        let mut p = Promise::new(receiver_id);
283
284        if let Some(state_init) = notify.state_init {
285            // No need to require `receiver_id == state_init.derive_account_id()` here,
286            // since Near runtime does this validation for us and current receipt will
287            // fail in case of mismatch anyway:
288            // https://github.com/near/nearcore/blob/523c659ac47ea31205fec830a1427a71352c605a/runtime/runtime/src/verifier.rs#L637-L644
289
290            p = p.state_init(
291                state_init,
292                // we can't spend native NEAR from sender's account during the deposits
293                NearToken::ZERO,
294            );
295        }
296
297        ext_mt_receiver::ext_on(p)
298            .with_static_gas(notify.min_gas.unwrap_or_default())
299            // distribute remaining gas here
300            .with_unused_gas_weight(1)
301            .mt_on_transfer(
302                sender_id,
303                previous_owner_ids,
304                token_ids,
305                amounts,
306                notify.msg,
307            )
308    }
309
310    #[must_use]
311    fn mt_resolve_gas(token_count: usize) -> Gas {
312        // These represent a linear model total_gas_cost = per_token*n + base,
313        // where `n` is the number of tokens.
314        const MT_RESOLVE_TRANSFER_PER_TOKEN_GAS: Gas = Gas::from_tgas(2);
315        const MT_RESOLVE_TRANSFER_BASE_GAS: Gas = Gas::from_tgas(8);
316        let token_count: u64 = token_count.try_into().unwrap_or_panic_display();
317
318        MT_RESOLVE_TRANSFER_BASE_GAS
319            .checked_add(
320                MT_RESOLVE_TRANSFER_PER_TOKEN_GAS
321                    .checked_mul(token_count)
322                    .unwrap_or_panic(),
323            )
324            .unwrap_or_panic()
325    }
326}