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        self.internal_sub_balance(
297            owner_id,
298            withdraw
299                .token_ids
300                .iter()
301                .cloned()
302                .map(|token_id| Nep245TokenId::new(withdraw.token.clone(), token_id))
303                .map(Into::into)
304                .zip(withdraw.amounts.iter().map(|a| a.0))
305                .chain(
306                    withdraw
307                        .storage_deposit
308                        .map(|amount| (self.wnear_token_id(), amount.as_yoctonear())),
309                ),
310        )
311    }
312
313    fn native_withdraw(&mut self, owner_id: &AccountIdRef, withdraw: NativeWithdraw) -> Result<()> {
314        self.internal_sub_balance(
315            owner_id,
316            [(
317                Nep141TokenId::new(self.wnear_id().into_owned()).into(),
318                withdraw.amount.as_yoctonear(),
319            )],
320        )
321    }
322
323    // NOTE: Simulation that uses a cached state cannot create promises, as it is a view call
324    #[inline]
325    fn notify_on_transfer(
326        &self,
327        _sender_id: &AccountIdRef,
328        _receiver_id: AccountId,
329        _tokens: Amounts,
330        _notification: NotifyOnTransfer,
331    ) {
332    }
333
334    fn storage_deposit(
335        &mut self,
336        owner_id: &AccountIdRef,
337        storage_deposit: StorageDeposit,
338    ) -> Result<()> {
339        self.internal_sub_balance(
340            owner_id,
341            [(
342                Nep141TokenId::new(self.wnear_id().into_owned()).into(),
343                storage_deposit.amount.as_yoctonear(),
344            )],
345        )
346    }
347
348    fn set_auth_by_predecessor_id(&mut self, account_id: AccountId, enable: bool) -> Result<bool> {
349        let was_enabled = self.is_auth_by_predecessor_id_enabled(&account_id);
350        let toggle = was_enabled ^ enable;
351        if toggle {
352            self.accounts
353                .get_or_create(account_id.clone(), |owner_id| {
354                    self.view.is_account_locked(owner_id)
355                })
356                .get_mut()
357                .ok_or(DefuseError::AccountLocked(account_id))?
358                // toggle
359                .auth_by_predecessor_id_toggled ^= true;
360        }
361        Ok(was_enabled)
362    }
363
364    fn auth_call(&mut self, signer_id: &AccountIdRef, auth_call: AuthCall) -> Result<()> {
365        if !auth_call.attached_deposit.is_zero() {
366            self.internal_sub_balance(
367                signer_id,
368                [(
369                    Nep141TokenId::new(self.wnear_id().into_owned()).into(),
370                    auth_call.attached_deposit.as_yoctonear(),
371                )],
372            )?;
373        }
374
375        Ok(())
376    }
377}
378
379#[derive(Debug, Default)]
380pub struct CachedAccounts(HashMap<AccountId, Lock<CachedAccount>>);
381
382impl CachedAccounts {
383    #[must_use]
384    #[inline]
385    pub fn new() -> Self {
386        Self(HashMap::new())
387    }
388
389    #[inline]
390    pub fn get(&self, account_id: &AccountIdRef) -> Option<&Lock<CachedAccount>> {
391        self.0.get(account_id)
392    }
393
394    #[inline]
395    pub fn get_mut(&mut self, account_id: &AccountIdRef) -> Option<&mut Lock<CachedAccount>> {
396        self.0.get_mut(account_id)
397    }
398
399    #[inline]
400    pub fn get_or_create(
401        &mut self,
402        account_id: AccountId,
403        is_initially_locked: impl FnOnce(&AccountId) -> bool,
404    ) -> &mut Lock<CachedAccount> {
405        self.0.entry(account_id).or_insert_with_key(|account_id| {
406            Lock::new(is_initially_locked(account_id), CachedAccount::default())
407        })
408    }
409}
410
411#[derive(Debug, Clone, Default)]
412pub struct CachedAccount {
413    nonces: Nonces<HashMap<U248, U256>>,
414
415    auth_by_predecessor_id_toggled: bool,
416
417    public_keys_added: HashSet<PublicKey>,
418    public_keys_removed: HashSet<PublicKey>,
419
420    token_amounts: Amounts<HashMap<TokenId, u128>>,
421}
422
423impl CachedAccount {
424    #[inline]
425    pub fn is_nonce_used(&self, nonce: U256) -> bool {
426        self.nonces.is_used(nonce)
427    }
428
429    #[inline]
430    pub fn commit_nonce(&mut self, n: U256) -> Result<()> {
431        self.nonces.commit(n)
432    }
433
434    #[inline]
435    pub fn cleanup_nonce_by_prefix(&mut self, prefix: NoncePrefix) -> bool {
436        self.nonces.cleanup_by_prefix(prefix)
437    }
438}