defuse_core/engine/state/
cached.rs

1use crate::{
2    DefuseError, 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    token_id::{TokenId, nep141::Nep141TokenId, nep171::Nep171TokenId, nep245::Nep245TokenId},
12};
13use defuse_bitmap::{U248, U256};
14use defuse_crypto::PublicKey;
15use defuse_near_utils::Lock;
16use near_sdk::{AccountId, AccountIdRef};
17use std::{
18    borrow::Cow,
19    collections::{HashMap, HashSet},
20};
21
22use super::{State, StateView};
23
24#[derive(Debug)]
25pub struct CachedState<W: StateView> {
26    view: W,
27    accounts: CachedAccounts,
28}
29
30impl<W> CachedState<W>
31where
32    W: StateView,
33{
34    #[inline]
35    pub fn new(view: W) -> Self {
36        Self {
37            view,
38            accounts: CachedAccounts::new(),
39        }
40    }
41}
42
43impl<W> StateView for CachedState<W>
44where
45    W: StateView,
46{
47    #[inline]
48    fn verifying_contract(&self) -> Cow<'_, AccountIdRef> {
49        self.view.verifying_contract()
50    }
51
52    #[inline]
53    fn wnear_id(&self) -> Cow<'_, AccountIdRef> {
54        self.view.wnear_id()
55    }
56
57    #[inline]
58    fn fee(&self) -> Pips {
59        self.view.fee()
60    }
61
62    #[inline]
63    fn fee_collector(&self) -> Cow<'_, AccountIdRef> {
64        self.view.fee_collector()
65    }
66
67    fn has_public_key(&self, account_id: &AccountIdRef, public_key: &PublicKey) -> bool {
68        if let Some(account) = self.accounts.get(account_id).map(Lock::as_inner_unchecked) {
69            if account.public_keys_added.contains(public_key) {
70                return true;
71            }
72            if account.public_keys_removed.contains(public_key) {
73                return false;
74            }
75        }
76        self.view.has_public_key(account_id, public_key)
77    }
78
79    fn iter_public_keys(&self, account_id: &AccountIdRef) -> impl Iterator<Item = PublicKey> + '_ {
80        let account = self.accounts.get(account_id).map(Lock::as_inner_unchecked);
81        self.view
82            .iter_public_keys(account_id)
83            .filter(move |pk| account.is_none_or(|a| !a.public_keys_removed.contains(pk)))
84            .chain(
85                account
86                    .map(|a| &a.public_keys_added)
87                    .into_iter()
88                    .flatten()
89                    .copied(),
90            )
91    }
92
93    fn is_nonce_used(&self, account_id: &AccountIdRef, nonce: Nonce) -> bool {
94        self.accounts
95            .get(account_id)
96            .map(Lock::as_inner_unchecked)
97            .is_some_and(|account| account.is_nonce_used(nonce))
98            || self.view.is_nonce_used(account_id, nonce)
99    }
100
101    fn balance_of(&self, account_id: &AccountIdRef, token_id: &TokenId) -> u128 {
102        self.accounts
103            .get(account_id)
104            .map(Lock::as_inner_unchecked)
105            .and_then(|account| account.token_amounts.get(token_id).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 account.token_amounts.get(&token_id).is_none() {
215                account
216                    .token_amounts
217                    .add(token_id.clone(), self.view.balance_of(&owner_id, &token_id))
218                    .ok_or(DefuseError::BalanceOverflow)?;
219            }
220            account
221                .token_amounts
222                .add(token_id, amount)
223                .ok_or(DefuseError::BalanceOverflow)?;
224        }
225        Ok(())
226    }
227
228    fn internal_sub_balance(
229        &mut self,
230        owner_id: &AccountIdRef,
231        token_amounts: impl IntoIterator<Item = (TokenId, u128)>,
232    ) -> Result<()> {
233        let account = self
234            .accounts
235            .get_or_create(owner_id.to_owned(), |owner_id| {
236                self.view.is_account_locked(owner_id)
237            })
238            .get_mut()
239            .ok_or_else(|| DefuseError::AccountLocked(owner_id.to_owned()))?;
240        for (token_id, amount) in token_amounts {
241            if amount == 0 {
242                return Err(DefuseError::InvalidIntent);
243            }
244
245            if account.token_amounts.get(&token_id).is_none() {
246                account
247                    .token_amounts
248                    .add(token_id.clone(), self.view.balance_of(owner_id, &token_id))
249                    .ok_or(DefuseError::BalanceOverflow)?;
250            }
251            account
252                .token_amounts
253                .sub(token_id, amount)
254                .ok_or(DefuseError::BalanceOverflow)?;
255        }
256        Ok(())
257    }
258
259    fn ft_withdraw(&mut self, owner_id: &AccountIdRef, withdraw: FtWithdraw) -> Result<()> {
260        self.internal_sub_balance(
261            owner_id,
262            std::iter::once((
263                Nep141TokenId::new(withdraw.token.clone()).into(),
264                withdraw.amount.0,
265            ))
266            .chain(withdraw.storage_deposit.map(|amount| {
267                (
268                    Nep141TokenId::new(self.wnear_id().into_owned()).into(),
269                    amount.as_yoctonear(),
270                )
271            })),
272        )
273    }
274
275    fn nft_withdraw(&mut self, owner_id: &AccountIdRef, withdraw: NftWithdraw) -> Result<()> {
276        self.internal_sub_balance(
277            owner_id,
278            std::iter::once((
279                Nep171TokenId::new(withdraw.token.clone(), withdraw.token_id.clone())?.into(),
280                1,
281            ))
282            .chain(withdraw.storage_deposit.map(|amount| {
283                (
284                    Nep141TokenId::new(self.wnear_id().into_owned()).into(),
285                    amount.as_yoctonear(),
286                )
287            })),
288        )
289    }
290
291    fn mt_withdraw(&mut self, owner_id: &AccountIdRef, withdraw: MtWithdraw) -> Result<()> {
292        if withdraw.token_ids.len() != withdraw.amounts.len() || withdraw.token_ids.is_empty() {
293            return Err(DefuseError::InvalidIntent);
294        }
295
296        let token_ids = std::iter::repeat(withdraw.token.clone())
297            .zip(withdraw.token_ids.iter().cloned())
298            .map(|(token, token_id)| Nep245TokenId::new(token, token_id))
299            .collect::<Result<Vec<_>, _>>()?;
300
301        self.internal_sub_balance(
302            owner_id,
303            token_ids
304                .into_iter()
305                .map(Into::into)
306                .zip(withdraw.amounts.iter().map(|a| a.0))
307                .chain(
308                    withdraw
309                        .storage_deposit
310                        .map(|amount| (self.wnear_token_id(), amount.as_yoctonear())),
311                ),
312        )
313    }
314
315    fn native_withdraw(&mut self, owner_id: &AccountIdRef, withdraw: NativeWithdraw) -> Result<()> {
316        self.internal_sub_balance(
317            owner_id,
318            [(
319                Nep141TokenId::new(self.wnear_id().into_owned()).into(),
320                withdraw.amount.as_yoctonear(),
321            )],
322        )
323    }
324
325    // NOTE: Simulation that uses a cached state cannot create promises, as it is a view call
326    #[inline]
327    fn notify_on_transfer(
328        &self,
329        _sender_id: &AccountIdRef,
330        _receiver_id: AccountId,
331        _tokens: Amounts,
332        _notification: NotifyOnTransfer,
333    ) {
334    }
335
336    fn storage_deposit(
337        &mut self,
338        owner_id: &AccountIdRef,
339        storage_deposit: StorageDeposit,
340    ) -> Result<()> {
341        self.internal_sub_balance(
342            owner_id,
343            [(
344                Nep141TokenId::new(self.wnear_id().into_owned()).into(),
345                storage_deposit.amount.as_yoctonear(),
346            )],
347        )
348    }
349
350    fn set_auth_by_predecessor_id(&mut self, account_id: AccountId, enable: bool) -> Result<bool> {
351        let was_enabled = self.is_auth_by_predecessor_id_enabled(&account_id);
352        let toggle = was_enabled ^ enable;
353        if toggle {
354            self.accounts
355                .get_or_create(account_id.clone(), |owner_id| {
356                    self.view.is_account_locked(owner_id)
357                })
358                .get_mut()
359                .ok_or(DefuseError::AccountLocked(account_id))?
360                // toggle
361                .auth_by_predecessor_id_toggled ^= true;
362        }
363        Ok(was_enabled)
364    }
365
366    fn auth_call(&mut self, signer_id: &AccountIdRef, auth_call: AuthCall) -> Result<()> {
367        if !auth_call.attached_deposit.is_zero() {
368            self.internal_sub_balance(
369                signer_id,
370                [(
371                    Nep141TokenId::new(self.wnear_id().into_owned()).into(),
372                    auth_call.attached_deposit.as_yoctonear(),
373                )],
374            )?;
375        }
376
377        Ok(())
378    }
379}
380
381#[derive(Debug, Default)]
382pub struct CachedAccounts(HashMap<AccountId, Lock<CachedAccount>>);
383
384impl CachedAccounts {
385    #[must_use]
386    #[inline]
387    pub fn new() -> Self {
388        Self(HashMap::new())
389    }
390
391    #[inline]
392    pub fn get(&self, account_id: &AccountIdRef) -> Option<&Lock<CachedAccount>> {
393        self.0.get(account_id)
394    }
395
396    #[inline]
397    pub fn get_mut(&mut self, account_id: &AccountIdRef) -> Option<&mut Lock<CachedAccount>> {
398        self.0.get_mut(account_id)
399    }
400
401    #[inline]
402    pub fn get_or_create(
403        &mut self,
404        account_id: AccountId,
405        is_initially_locked: impl FnOnce(&AccountId) -> bool,
406    ) -> &mut Lock<CachedAccount> {
407        self.0.entry(account_id).or_insert_with_key(|account_id| {
408            Lock::new(is_initially_locked(account_id), CachedAccount::default())
409        })
410    }
411}
412
413#[derive(Debug, Clone, Default)]
414pub struct CachedAccount {
415    nonces: Nonces<HashMap<U248, U256>>,
416
417    auth_by_predecessor_id_toggled: bool,
418
419    public_keys_added: HashSet<PublicKey>,
420    public_keys_removed: HashSet<PublicKey>,
421
422    token_amounts: Amounts<HashMap<TokenId, u128>>,
423}
424
425impl CachedAccount {
426    #[inline]
427    pub fn is_nonce_used(&self, nonce: U256) -> bool {
428        self.nonces.is_used(nonce)
429    }
430
431    #[inline]
432    pub fn commit_nonce(&mut self, n: U256) -> Result<()> {
433        self.nonces.commit(n)
434    }
435
436    #[inline]
437    pub fn cleanup_nonce_by_prefix(&mut self, prefix: NoncePrefix) -> bool {
438        self.nonces.cleanup_by_prefix(prefix)
439    }
440}