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