Skip to main content

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