Skip to main content

defuse_core/engine/state/
cached.rs

1use crate::{
2    AccountId, AccountIdRef, DefuseError, Lock, Nonce, NoncePrefix, Nonces, Result, Salt,
3    amounts::Amounts,
4    fees::Pips,
5    intents::{
6        auth::AuthCall,
7        tokens::{
8            FtWithdraw, MtWithdraw, NativeWithdraw, NftWithdraw, NotifyOnTransfer, StorageDeposit,
9        },
10    },
11    public_key::PublicKey,
12    token_id::{TokenId, nep141::Nep141TokenId, nep171::Nep171TokenId, nep245::Nep245TokenId},
13    tokens::BATCH_MAX_TOKENS,
14};
15use defuse_bitmap::{U248, U256};
16use std::{
17    borrow::Cow,
18    collections::{HashMap, HashSet},
19};
20
21use super::{State, StateView};
22
23#[derive(Debug)]
24pub struct CachedState<W: StateView> {
25    view: W,
26    accounts: CachedAccounts,
27}
28
29impl<W> CachedState<W>
30where
31    W: StateView,
32{
33    #[inline]
34    pub fn new(view: W) -> Self {
35        Self {
36            view,
37            accounts: CachedAccounts::new(),
38        }
39    }
40}
41
42impl<W> StateView for CachedState<W>
43where
44    W: StateView,
45{
46    #[inline]
47    fn verifying_contract(&self) -> Cow<'_, AccountIdRef> {
48        self.view.verifying_contract()
49    }
50
51    #[inline]
52    fn wnear_id(&self) -> Cow<'_, AccountIdRef> {
53        self.view.wnear_id()
54    }
55
56    #[inline]
57    fn fee(&self) -> Pips {
58        self.view.fee()
59    }
60
61    #[inline]
62    fn fee_collector(&self) -> Cow<'_, AccountIdRef> {
63        self.view.fee_collector()
64    }
65
66    fn has_public_key(&self, account_id: &AccountIdRef, public_key: &PublicKey) -> bool {
67        if let Some(account) = self.accounts.get(account_id).map(Lock::as_inner_unchecked) {
68            if account.public_keys_added.contains(public_key) {
69                return true;
70            }
71            if account.public_keys_removed.contains(public_key) {
72                return false;
73            }
74        }
75        self.view.has_public_key(account_id, public_key)
76    }
77
78    fn iter_public_keys(&self, account_id: &AccountIdRef) -> impl Iterator<Item = PublicKey> + '_ {
79        let account = self.accounts.get(account_id).map(Lock::as_inner_unchecked);
80        self.view
81            .iter_public_keys(account_id)
82            .filter(move |pk| account.is_none_or(|a| !a.public_keys_removed.contains(pk)))
83            .chain(
84                account
85                    .map(|a| &a.public_keys_added)
86                    .into_iter()
87                    .flatten()
88                    .copied(),
89            )
90    }
91
92    fn is_nonce_used(&self, account_id: &AccountIdRef, nonce: Nonce) -> bool {
93        self.accounts
94            .get(account_id)
95            .map(Lock::as_inner_unchecked)
96            .is_some_and(|account| account.is_nonce_used(nonce))
97            || self.view.is_nonce_used(account_id, nonce)
98    }
99
100    fn balance_of(&self, account_id: &AccountIdRef, token_id: &TokenId) -> u128 {
101        self.accounts
102            .get(account_id)
103            .map(Lock::as_inner_unchecked)
104            .and_then(|account| account.token_amounts.get(token_id))
105            .copied()
106            .unwrap_or_else(|| self.view.balance_of(account_id, token_id))
107    }
108
109    fn is_account_locked(&self, account_id: &AccountIdRef) -> bool {
110        self.accounts
111            .get(account_id)
112            .map_or_else(|| self.view.is_account_locked(account_id), Lock::is_locked)
113    }
114
115    fn is_auth_by_predecessor_id_enabled(&self, account_id: &AccountIdRef) -> bool {
116        let was_enabled = self.view.is_auth_by_predecessor_id_enabled(account_id);
117        let toggled = self
118            .accounts
119            .get(account_id)
120            .map(Lock::as_inner_unchecked)
121            .is_some_and(|a| a.auth_by_predecessor_id_toggled);
122        was_enabled ^ toggled
123    }
124
125    fn is_valid_salt(&self, salt: Salt) -> bool {
126        self.view.is_valid_salt(salt)
127    }
128}
129
130impl<W> State for CachedState<W>
131where
132    W: StateView,
133{
134    fn add_public_key(&mut self, account_id: AccountId, public_key: PublicKey) -> Result<()> {
135        let had = self.view.has_public_key(&account_id, &public_key);
136        let account = self
137            .accounts
138            .get_or_create(account_id.clone(), |account_id| {
139                self.view.is_account_locked(account_id)
140            })
141            .get_mut()
142            .ok_or_else(|| DefuseError::AccountLocked(account_id.clone()))?;
143        let added = if had {
144            account.public_keys_removed.remove(&public_key)
145        } else {
146            account.public_keys_added.insert(public_key)
147        };
148        if !added {
149            return Err(DefuseError::PublicKeyExists(account_id, public_key));
150        }
151        Ok(())
152    }
153
154    fn remove_public_key(&mut self, account_id: AccountId, public_key: PublicKey) -> Result<()> {
155        let had = self.view.has_public_key(&account_id, &public_key);
156        let account = self
157            .accounts
158            .get_or_create(account_id.clone(), |account_id| {
159                self.view.is_account_locked(account_id)
160            })
161            .get_mut()
162            .ok_or_else(|| DefuseError::AccountLocked(account_id.clone()))?;
163        let removed = if had {
164            account.public_keys_removed.insert(public_key)
165        } else {
166            account.public_keys_added.remove(&public_key)
167        };
168        if !removed {
169            return Err(DefuseError::PublicKeyNotExist(account_id, public_key));
170        }
171        Ok(())
172    }
173
174    fn commit_nonce(&mut self, account_id: AccountId, nonce: Nonce) -> Result<()> {
175        if self.view.is_nonce_used(&account_id, nonce) {
176            return Err(DefuseError::NonceUsed);
177        }
178
179        self.accounts
180            .get_or_create(account_id.clone(), |account_id| {
181                self.view.is_account_locked(account_id)
182            })
183            .get_mut()
184            .ok_or(DefuseError::AccountLocked(account_id))?
185            .commit_nonce(nonce)
186    }
187
188    fn cleanup_nonce_by_prefix(
189        &mut self,
190        account_id: &AccountIdRef,
191        prefix: NoncePrefix,
192    ) -> Result<bool> {
193        let account = self
194            .accounts
195            .get_mut(account_id)
196            .ok_or_else(|| DefuseError::AccountNotFound(account_id.to_owned()))?
197            .as_inner_unchecked_mut();
198
199        Ok(account.cleanup_nonce_by_prefix(prefix))
200    }
201
202    fn internal_add_balance(
203        &mut self,
204        owner_id: AccountId,
205        token_amounts: impl IntoIterator<Item = (TokenId, u128)>,
206    ) -> Result<()> {
207        let account = self
208            .accounts
209            .get_or_create(owner_id.clone(), |owner_id| {
210                self.view.is_account_locked(owner_id)
211            })
212            .as_inner_unchecked_mut();
213        for (token_id, amount) in token_amounts {
214            if amount == 0 {
215                return Err(DefuseError::InvalidIntent);
216            }
217
218            let balance = account
219                .token_amounts
220                .entry(token_id)
221                // cache zero balances as well
222                .or_insert_with_key(|token_id| self.view.balance_of(&owner_id, token_id));
223
224            *balance = balance
225                .checked_add(amount)
226                .ok_or(DefuseError::BalanceOverflow)?;
227        }
228        Ok(())
229    }
230
231    fn internal_sub_balance(
232        &mut self,
233        owner_id: &AccountIdRef,
234        token_amounts: impl IntoIterator<Item = (TokenId, u128)>,
235    ) -> Result<()> {
236        let account = self
237            .accounts
238            .get_or_create(owner_id.to_owned(), |owner_id| {
239                self.view.is_account_locked(owner_id)
240            })
241            .get_mut()
242            .ok_or_else(|| DefuseError::AccountLocked(owner_id.to_owned()))?;
243        for (token_id, amount) in token_amounts {
244            if amount == 0 {
245                return Err(DefuseError::InvalidIntent);
246            }
247
248            let balance = account
249                .token_amounts
250                .entry(token_id)
251                // cache zero balances as well
252                .or_insert_with_key(|token_id| self.view.balance_of(owner_id, token_id));
253
254            *balance = balance
255                .checked_sub(amount)
256                .ok_or(DefuseError::BalanceOverflow)?;
257        }
258        Ok(())
259    }
260
261    fn ft_withdraw(&mut self, owner_id: &AccountIdRef, withdraw: FtWithdraw) -> Result<()> {
262        self.internal_sub_balance(
263            owner_id,
264            std::iter::once((
265                Nep141TokenId::new(withdraw.token.clone()).into(),
266                withdraw.amount,
267            ))
268            .chain(withdraw.storage_deposit.map(|amount| {
269                (
270                    Nep141TokenId::new(self.wnear_id().into_owned()).into(),
271                    amount.as_yoctonear(),
272                )
273            })),
274        )
275    }
276
277    fn nft_withdraw(&mut self, owner_id: &AccountIdRef, withdraw: NftWithdraw) -> Result<()> {
278        self.internal_sub_balance(
279            owner_id,
280            std::iter::once((
281                Nep171TokenId::new(withdraw.token.clone(), withdraw.token_id.clone()).into(),
282                1,
283            ))
284            .chain(withdraw.storage_deposit.map(|amount| {
285                (
286                    Nep141TokenId::new(self.wnear_id().into_owned()).into(),
287                    amount.as_yoctonear(),
288                )
289            })),
290        )
291    }
292
293    fn mt_withdraw(&mut self, owner_id: &AccountIdRef, withdraw: MtWithdraw) -> Result<()> {
294        if withdraw.token_ids.len() != withdraw.amounts.len() || withdraw.token_ids.is_empty() {
295            return Err(DefuseError::InvalidIntent);
296        }
297
298        if withdraw.token_ids.len() > BATCH_MAX_TOKENS {
299            return Err(DefuseError::TooManyTokens(withdraw.token_ids.len()));
300        }
301
302        self.internal_sub_balance(
303            owner_id,
304            withdraw
305                .token_ids
306                .iter()
307                .cloned()
308                .map(|token_id| Nep245TokenId::new(withdraw.token.clone(), token_id))
309                .map(Into::into)
310                .zip(withdraw.amounts.iter().copied())
311                .chain(
312                    withdraw
313                        .storage_deposit
314                        .map(|amount| (self.wnear_token_id(), amount.as_yoctonear())),
315                ),
316        )
317    }
318
319    fn native_withdraw(&mut self, owner_id: &AccountIdRef, withdraw: NativeWithdraw) -> Result<()> {
320        self.internal_sub_balance(
321            owner_id,
322            [(
323                Nep141TokenId::new(self.wnear_id().into_owned()).into(),
324                withdraw.amount.as_yoctonear(),
325            )],
326        )
327    }
328
329    // NOTE: Simulation that uses a cached state cannot create promises, as it is a view call
330    #[inline]
331    fn notify_on_transfer(
332        &self,
333        _sender_id: &AccountIdRef,
334        _receiver_id: AccountId,
335        tokens: Amounts,
336        _notification: NotifyOnTransfer,
337    ) -> Result<()> {
338        if tokens.len() > BATCH_MAX_TOKENS {
339            return Err(DefuseError::TooManyTokens(tokens.len()));
340        }
341
342        Ok(())
343    }
344
345    fn storage_deposit(
346        &mut self,
347        owner_id: &AccountIdRef,
348        storage_deposit: StorageDeposit,
349    ) -> Result<()> {
350        self.internal_sub_balance(
351            owner_id,
352            [(
353                Nep141TokenId::new(self.wnear_id().into_owned()).into(),
354                storage_deposit.amount.as_yoctonear(),
355            )],
356        )
357    }
358
359    fn set_auth_by_predecessor_id(&mut self, account_id: AccountId, enable: bool) -> Result<bool> {
360        let was_enabled = self.is_auth_by_predecessor_id_enabled(&account_id);
361        let toggle = was_enabled ^ enable;
362        if toggle {
363            self.accounts
364                .get_or_create(account_id.clone(), |owner_id| {
365                    self.view.is_account_locked(owner_id)
366                })
367                .get_mut()
368                .ok_or(DefuseError::AccountLocked(account_id))?
369                // toggle
370                .auth_by_predecessor_id_toggled ^= true;
371        }
372        Ok(toggle)
373    }
374
375    fn auth_call(&mut self, signer_id: &AccountIdRef, auth_call: AuthCall) -> Result<()> {
376        if !auth_call.attached_deposit.is_zero() {
377            self.internal_sub_balance(
378                signer_id,
379                [(
380                    Nep141TokenId::new(self.wnear_id().into_owned()).into(),
381                    auth_call.attached_deposit.as_yoctonear(),
382                )],
383            )?;
384        }
385
386        Ok(())
387    }
388
389    #[inline]
390    fn mint(&mut self, owner_id: AccountId, tokens: Amounts, _memo: Option<String>) -> Result<()> {
391        self.internal_add_balance(owner_id, tokens)
392    }
393
394    #[inline]
395    fn burn(
396        &mut self,
397        owner_id: &AccountIdRef,
398        tokens: Amounts,
399        _memo: Option<String>,
400    ) -> Result<()> {
401        self.internal_sub_balance(owner_id, tokens)
402    }
403}
404
405#[derive(Debug, Default)]
406pub struct CachedAccounts(HashMap<AccountId, Lock<CachedAccount>>);
407
408impl CachedAccounts {
409    #[must_use]
410    #[inline]
411    pub fn new() -> Self {
412        Self(HashMap::new())
413    }
414
415    #[inline]
416    pub fn get(&self, account_id: &AccountIdRef) -> Option<&Lock<CachedAccount>> {
417        self.0.get(account_id)
418    }
419
420    #[inline]
421    pub fn get_mut(&mut self, account_id: &AccountIdRef) -> Option<&mut Lock<CachedAccount>> {
422        self.0.get_mut(account_id)
423    }
424
425    #[inline]
426    pub fn get_or_create(
427        &mut self,
428        account_id: AccountId,
429        is_initially_locked: impl FnOnce(&AccountId) -> bool,
430    ) -> &mut Lock<CachedAccount> {
431        self.0.entry(account_id).or_insert_with_key(|account_id| {
432            Lock::new(is_initially_locked(account_id), CachedAccount::default())
433        })
434    }
435}
436
437#[derive(Debug, Clone, Default)]
438pub struct CachedAccount {
439    nonces: Nonces<HashMap<U248, U256>>,
440
441    auth_by_predecessor_id_toggled: bool,
442
443    public_keys_added: HashSet<PublicKey>,
444    public_keys_removed: HashSet<PublicKey>,
445
446    token_amounts: HashMap<TokenId, u128>,
447}
448
449impl CachedAccount {
450    #[inline]
451    pub fn is_nonce_used(&self, nonce: U256) -> bool {
452        self.nonces.is_used(nonce)
453    }
454
455    #[inline]
456    pub fn commit_nonce(&mut self, n: U256) -> Result<()> {
457        self.nonces.commit(n)
458    }
459
460    #[inline]
461    pub fn cleanup_nonce_by_prefix(&mut self, prefix: NoncePrefix) -> bool {
462        self.nonces.cleanup_by_prefix(prefix)
463    }
464}