Skip to main content

defuse_core/engine/state/
deltas.rs

1use crate::{
2    DefuseError, Nonce, NoncePrefix, Result, Salt,
3    amounts::Amounts,
4    fees::Pips,
5    intents::{
6        auth::AuthCall,
7        token_diff::TokenDeltas,
8        tokens::{
9            FtWithdraw, MtWithdraw, NativeWithdraw, NftWithdraw, NotifyOnTransfer, StorageDeposit,
10        },
11    },
12    public_key::PublicKey,
13    token_id::TokenId,
14};
15use defuse_map_utils::cleanup::DefaultMap;
16use defuse_nep245::{MtEvent, MtTransferEvent};
17use near_sdk::{AccountId, AccountIdRef, json_types::U128};
18use serde::{Deserialize, Serialize};
19use serde_with::{DisplayFromStr, serde_as};
20use std::{
21    borrow::Cow,
22    cmp::Reverse,
23    collections::{BTreeMap, HashMap},
24    iter,
25};
26
27use super::{State, StateView};
28
29pub struct Deltas<S> {
30    state: S,
31    deltas: TransferMatcher,
32}
33
34impl<S> Deltas<S> {
35    #[inline]
36    pub fn new(state: S) -> Self {
37        Self {
38            state,
39            deltas: TransferMatcher::new(),
40        }
41    }
42
43    #[inline]
44    pub fn finalize(self) -> Result<Transfers, InvariantViolated> {
45        self.deltas.finalize()
46    }
47}
48
49impl<S> StateView for Deltas<S>
50where
51    S: StateView,
52{
53    #[inline]
54    fn verifying_contract(&self) -> Cow<'_, AccountIdRef> {
55        self.state.verifying_contract()
56    }
57
58    #[inline]
59    fn wnear_id(&self) -> Cow<'_, AccountIdRef> {
60        self.state.wnear_id()
61    }
62
63    #[inline]
64    fn fee(&self) -> Pips {
65        self.state.fee()
66    }
67
68    #[inline]
69    fn fee_collector(&self) -> Cow<'_, AccountIdRef> {
70        self.state.fee_collector()
71    }
72
73    #[inline]
74    fn has_public_key(&self, account_id: &AccountIdRef, public_key: &PublicKey) -> bool {
75        self.state.has_public_key(account_id, public_key)
76    }
77
78    #[inline]
79    fn iter_public_keys(&self, account_id: &AccountIdRef) -> impl Iterator<Item = PublicKey> + '_ {
80        self.state.iter_public_keys(account_id)
81    }
82
83    #[inline]
84    fn is_nonce_used(&self, account_id: &AccountIdRef, nonce: Nonce) -> bool {
85        self.state.is_nonce_used(account_id, nonce)
86    }
87
88    #[inline]
89    fn balance_of(&self, account_id: &AccountIdRef, token_id: &TokenId) -> u128 {
90        self.state.balance_of(account_id, token_id)
91    }
92
93    #[inline]
94    fn is_account_locked(&self, account_id: &AccountIdRef) -> bool {
95        self.state.is_account_locked(account_id)
96    }
97
98    #[inline]
99    fn is_auth_by_predecessor_id_enabled(&self, account_id: &AccountIdRef) -> bool {
100        self.state.is_auth_by_predecessor_id_enabled(account_id)
101    }
102
103    #[inline]
104    fn is_valid_salt(&self, salt: Salt) -> bool {
105        self.state.is_valid_salt(salt)
106    }
107}
108
109impl<S> State for Deltas<S>
110where
111    S: State,
112{
113    #[inline]
114    fn add_public_key(&mut self, account_id: AccountId, public_key: PublicKey) -> Result<()> {
115        self.state.add_public_key(account_id, public_key)
116    }
117
118    #[inline]
119    fn remove_public_key(&mut self, account_id: AccountId, public_key: PublicKey) -> Result<()> {
120        self.state.remove_public_key(account_id, public_key)
121    }
122
123    #[inline]
124    fn commit_nonce(&mut self, account_id: AccountId, nonce: Nonce) -> Result<()> {
125        self.state.commit_nonce(account_id, nonce)
126    }
127
128    #[inline]
129    fn cleanup_nonce_by_prefix(
130        &mut self,
131        account_id: &AccountIdRef,
132        prefix: NoncePrefix,
133    ) -> Result<bool> {
134        self.state.cleanup_nonce_by_prefix(account_id, prefix)
135    }
136
137    fn internal_add_balance(
138        &mut self,
139        owner_id: AccountId,
140        tokens: impl IntoIterator<Item = (TokenId, u128)>,
141    ) -> Result<()> {
142        for (token_id, amount) in tokens {
143            self.state
144                .internal_add_balance(owner_id.clone(), [(token_id.clone(), amount)])?;
145            if !self.deltas.deposit(owner_id.clone(), token_id, amount) {
146                return Err(DefuseError::BalanceOverflow);
147            }
148        }
149        Ok(())
150    }
151
152    fn internal_sub_balance(
153        &mut self,
154        owner_id: &AccountIdRef,
155        tokens: impl IntoIterator<Item = (TokenId, u128)>,
156    ) -> Result<()> {
157        for (token_id, amount) in tokens {
158            self.state
159                .internal_sub_balance(owner_id, [(token_id.clone(), amount)])?;
160            if !self.deltas.withdraw(owner_id.to_owned(), token_id, amount) {
161                return Err(DefuseError::BalanceOverflow);
162            }
163        }
164        Ok(())
165    }
166
167    #[inline]
168    fn ft_withdraw(&mut self, owner_id: &AccountIdRef, withdraw: FtWithdraw) -> Result<()> {
169        self.state.ft_withdraw(owner_id, withdraw)
170    }
171
172    #[inline]
173    fn nft_withdraw(&mut self, owner_id: &AccountIdRef, withdraw: NftWithdraw) -> Result<()> {
174        self.state.nft_withdraw(owner_id, withdraw)
175    }
176
177    #[inline]
178    fn mt_withdraw(&mut self, owner_id: &AccountIdRef, withdraw: MtWithdraw) -> Result<()> {
179        self.state.mt_withdraw(owner_id, withdraw)
180    }
181
182    #[inline]
183    fn native_withdraw(&mut self, owner_id: &AccountIdRef, withdraw: NativeWithdraw) -> Result<()> {
184        self.state.native_withdraw(owner_id, withdraw)
185    }
186
187    #[inline]
188    fn notify_on_transfer(
189        &self,
190        sender_id: &AccountIdRef,
191        receiver_id: AccountId,
192        tokens: Amounts,
193        notification: NotifyOnTransfer,
194    ) {
195        self.state
196            .notify_on_transfer(sender_id, receiver_id, tokens, notification);
197    }
198
199    #[inline]
200    fn storage_deposit(
201        &mut self,
202        owner_id: &AccountIdRef,
203        storage_deposit: StorageDeposit,
204    ) -> Result<()> {
205        self.state.storage_deposit(owner_id, storage_deposit)
206    }
207
208    #[inline]
209    fn set_auth_by_predecessor_id(&mut self, account_id: AccountId, enable: bool) -> Result<bool> {
210        self.state.set_auth_by_predecessor_id(account_id, enable)
211    }
212
213    #[inline]
214    fn auth_call(&mut self, signer_id: &AccountIdRef, auth_call: AuthCall) -> Result<()> {
215        self.state.auth_call(signer_id, auth_call)
216    }
217
218    #[inline]
219    fn mint(&mut self, owner_id: AccountId, tokens: Amounts, memo: Option<String>) -> Result<()> {
220        self.state.mint(owner_id, tokens, memo)
221    }
222
223    #[inline]
224    fn burn(
225        &mut self,
226        owner_id: &AccountIdRef,
227        tokens: Amounts,
228        memo: Option<String>,
229    ) -> Result<()> {
230        self.state.burn(owner_id, tokens, memo)
231    }
232}
233
234/// Accumulates internal deposits and withdrawals on different tokens
235/// to match transfers using `.finalize()`
236///
237/// Transfers in `TokenDiff` intents are represented as deltas without receivers.
238/// This struct accumulates tokens all transfers, and converts them from deltas, to
239/// a set of transfers from one account to another.
240/// Note that this doesn't touch account balances. The balances were already changed
241/// in an earlier stage while executing the intent.
242#[derive(Debug, Default)]
243pub struct TransferMatcher(HashMap<TokenId, TokenTransferMatcher>);
244
245impl TransferMatcher {
246    #[inline]
247    pub fn new() -> Self {
248        Self(HashMap::new())
249    }
250
251    #[inline]
252    pub fn deposit(&mut self, owner_id: AccountId, token_id: TokenId, amount: u128) -> bool {
253        self.0.entry_or_default(token_id).deposit(owner_id, amount)
254    }
255
256    #[inline]
257    pub fn withdraw(&mut self, owner_id: AccountId, token_id: TokenId, amount: u128) -> bool {
258        self.0.entry_or_default(token_id).withdraw(owner_id, amount)
259    }
260
261    #[inline]
262    pub fn add_delta(&mut self, owner_id: AccountId, token_id: TokenId, delta: i128) -> bool {
263        self.0.entry_or_default(token_id).add_delta(owner_id, delta)
264    }
265
266    // Finalizes all transfers, or returns unmatched deltas.
267    // If unmatched deltas overflow, then Err(None) is returned.
268    pub fn finalize(self) -> Result<Transfers, InvariantViolated> {
269        let mut transfers = Transfers::default();
270        let mut deltas = TokenDeltas::default();
271        for (token_id, transfer_matcher) in self.0 {
272            if let Err(unmatched) = transfer_matcher.finalize_into(&token_id, &mut transfers)
273                && (unmatched == 0 || deltas.apply_delta(token_id, unmatched).is_none())
274            {
275                return Err(InvariantViolated::Overflow);
276            }
277        }
278        if !deltas.is_empty() {
279            return Err(InvariantViolated::UnmatchedDeltas {
280                unmatched_deltas: deltas,
281            });
282        }
283        Ok(transfers)
284    }
285}
286
287type AccountAmounts = Amounts<HashMap<AccountId, u128>>;
288
289// Accumulates internal deposits and withdrawals on a single token
290#[derive(Debug, Default, PartialEq, Eq)]
291pub struct TokenTransferMatcher {
292    deposits: AccountAmounts,
293    withdrawals: AccountAmounts,
294}
295
296impl TokenTransferMatcher {
297    #[inline]
298    pub fn deposit(&mut self, owner_id: AccountId, amount: u128) -> bool {
299        Self::sub_add(&mut self.withdrawals, &mut self.deposits, owner_id, amount)
300    }
301
302    #[inline]
303    pub fn withdraw(&mut self, owner_id: AccountId, amount: u128) -> bool {
304        Self::sub_add(&mut self.deposits, &mut self.withdrawals, owner_id, amount)
305    }
306
307    #[inline]
308    pub fn add_delta(&mut self, owner_id: AccountId, delta: i128) -> bool {
309        let amount = delta.unsigned_abs();
310        if delta.is_negative() {
311            self.withdraw(owner_id, amount)
312        } else {
313            self.deposit(owner_id, amount)
314        }
315    }
316
317    fn sub_add(
318        sub: &mut AccountAmounts,
319        add: &mut AccountAmounts,
320        owner_id: AccountId,
321        mut amount: u128,
322    ) -> bool {
323        let s = sub.amount_for(&owner_id);
324        if s > 0 {
325            let a = s.min(amount);
326            sub.sub(owner_id.clone(), a)
327                .unwrap_or_else(|| unreachable!());
328            amount = amount.saturating_sub(a);
329            if amount == 0 {
330                return true;
331            }
332        }
333        add.add(owner_id, amount).is_some()
334    }
335
336    // Finalizes transfer of this token, or returns unmatched delta.
337    // If returned delta is zero, then overflow happened
338    pub fn finalize_into(self, token_id: &TokenId, transfers: &mut Transfers) -> Result<(), i128> {
339        // sort deposits and withdrawals in descending order
340        let [mut deposits, mut withdrawals] = [self.deposits, self.withdrawals].map(|amounts| {
341            let mut amounts: Vec<_> = amounts.into_iter().collect();
342            amounts.sort_unstable_by_key(|(_, amount)| Reverse(*amount));
343            amounts.into_iter()
344        });
345
346        // take first sender and receiver
347        let (mut deposit, mut withdraw) = (deposits.next(), withdrawals.next());
348
349        // as long as there is both: sender and receiver
350        while let Some(((sender, send), (receiver, receive))) =
351            withdraw.as_mut().zip(deposit.as_mut())
352        {
353            // get min amount and transfer
354            let transfer = (*send).min(*receive);
355            transfers
356                .transfer(sender.clone(), receiver.clone(), token_id.clone(), transfer)
357                // no error can happen since we add only one transfer for each
358                // combination of (sender, receiver, token_id)
359                .unwrap_or_else(|| unreachable!());
360
361            // subtract amount from sender and receiver
362            *send = send.saturating_sub(transfer);
363            *receive = receive.saturating_sub(transfer);
364
365            if *send == 0 {
366                // select next sender
367                withdraw = withdrawals.next();
368            }
369            if *receive == 0 {
370                // select next receiver
371                deposit = deposits.next();
372            }
373        }
374
375        // only sender(s) left
376        if let Some((_, send)) = withdraw {
377            return Err(withdrawals
378                .try_fold(send, |total, (_, s)| total.checked_add(s))
379                .and_then(|total| i128::try_from(total).ok())
380                .and_then(i128::checked_neg)
381                .unwrap_or_default());
382        }
383        // only receiver(s) left
384        if let Some((_, receive)) = deposit {
385            return Err(deposits
386                .try_fold(receive, |total, (_, r)| total.checked_add(r))
387                .and_then(|total| i128::try_from(total).ok())
388                .unwrap_or_default());
389        }
390
391        Ok(())
392    }
393}
394
395/// Raw transfers between accounts
396#[must_use]
397#[derive(Debug, Default, PartialEq, Eq)]
398pub struct Transfers(
399    /// `sender_id` -> `receiver_id` -> `token_id` -> `amount`
400    HashMap<AccountId, HashMap<AccountId, Amounts<HashMap<TokenId, u128>>>>,
401);
402
403impl Transfers {
404    #[must_use]
405    pub fn transfer(
406        &mut self,
407        sender_id: AccountId,
408        receiver_id: AccountId,
409        token_id: TokenId,
410        amount: u128,
411    ) -> Option<u128> {
412        let mut sender = self.0.entry_or_default(sender_id);
413        let mut receiver = sender.entry_or_default(receiver_id);
414        receiver.add(token_id, amount)
415    }
416
417    pub fn with_transfer(
418        mut self,
419        sender_id: AccountId,
420        receiver_id: AccountId,
421        token_id: TokenId,
422        amount: u128,
423    ) -> Option<Self> {
424        self.transfer(sender_id, receiver_id, token_id, amount)?;
425        Some(self)
426    }
427
428    pub fn as_mt_event(&self) -> Option<MtEvent<'_>> {
429        if self.0.is_empty() {
430            return None;
431        }
432        Some(MtEvent::MtTransfer(
433            self.0
434                .iter()
435                .flat_map(|(sender_id, transfers)| iter::repeat(sender_id).zip(transfers))
436                .map(|(sender_id, (receiver_id, transfers))| {
437                    let (token_ids, amounts) = transfers
438                        .iter()
439                        .map(|(token_id, amount)| (token_id.to_string(), U128(*amount)))
440                        .unzip();
441                    MtTransferEvent {
442                        authorized_id: None,
443                        old_owner_id: Cow::Borrowed(sender_id),
444                        new_owner_id: Cow::Borrowed(receiver_id),
445                        token_ids: Cow::Owned(token_ids),
446                        amounts: Cow::Owned(amounts),
447                        memo: None,
448                    }
449                })
450                .collect::<Vec<_>>()
451                .into(),
452        ))
453    }
454}
455
456#[serde_as]
457#[cfg_attr(feature = "abi", derive(::schemars::JsonSchema))]
458#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
459#[serde(tag = "error", rename_all = "snake_case")]
460pub enum InvariantViolated {
461    UnmatchedDeltas {
462        #[serde_as(as = "Amounts<BTreeMap<_, DisplayFromStr>>")]
463        unmatched_deltas: TokenDeltas,
464    },
465    Overflow,
466}
467
468impl InvariantViolated {
469    #[inline]
470    pub const fn as_unmatched_deltas(&self) -> Option<&TokenDeltas> {
471        match self {
472            Self::UnmatchedDeltas {
473                unmatched_deltas: deltas,
474            } => Some(deltas),
475            Self::Overflow => None,
476        }
477    }
478
479    #[inline]
480    pub fn into_unmatched_deltas(self) -> Option<TokenDeltas> {
481        match self {
482            Self::UnmatchedDeltas {
483                unmatched_deltas: deltas,
484            } => Some(deltas),
485            Self::Overflow => None,
486        }
487    }
488}
489
490#[cfg(test)]
491#[allow(clippy::many_single_char_names)]
492mod tests {
493    use crate::token_id::nep141::Nep141TokenId;
494
495    use super::*;
496
497    #[test]
498    fn test_transfers() {
499        let mut transfers = TransferMatcher::default();
500        let [a, b, c, d, e, f, g]: [AccountId; 7] =
501            ["a", "b", "c", "d", "e", "f", "g"].map(|s| format!("{s}.near").parse().unwrap());
502        let [ft1, ft2] = ["ft1", "ft2"].map(|a| {
503            TokenId::from(Nep141TokenId::new(
504                format!("{a}.near").parse::<AccountId>().unwrap(),
505            ))
506        });
507
508        let deltas: HashMap<AccountId, TokenDeltas> = [
509            (&a, [(&ft1, -5), (&ft2, 1)].as_slice()),
510            (&b, [(&ft1, 4), (&ft2, -1)].as_slice()),
511            (&c, [(&ft1, 3)].as_slice()),
512            (&d, [(&ft1, -10)].as_slice()),
513            (&e, [(&ft1, -1)].as_slice()),
514            (&f, [(&ft1, 10)].as_slice()),
515            (&g, [(&ft1, -1)].as_slice()),
516        ]
517        .into_iter()
518        .map(|(owner_id, deltas)| {
519            (
520                owner_id.clone(),
521                TokenDeltas::default()
522                    .with_apply_deltas(
523                        deltas
524                            .iter()
525                            .map(|(token_id, delta)| ((*token_id).clone(), *delta)),
526                    )
527                    .unwrap(),
528            )
529        })
530        .collect();
531
532        for (owner, (token_id, delta)) in deltas
533            .iter()
534            .flat_map(|(owner_id, deltas)| iter::repeat(owner_id).zip(deltas))
535        {
536            assert!(transfers.add_delta(owner.clone(), token_id.clone(), *delta));
537        }
538
539        let transfers = transfers.finalize().unwrap();
540        let mut new_deltas: HashMap<AccountId, TokenDeltas> = HashMap::new();
541
542        for (sender_id, transfers) in transfers.0 {
543            for (receiver_id, amounts) in transfers {
544                for (token_id, amount) in amounts {
545                    new_deltas
546                        .entry_or_default(sender_id.clone())
547                        .sub(token_id.clone(), amount)
548                        .unwrap();
549
550                    new_deltas
551                        .entry_or_default(receiver_id.clone())
552                        .add(token_id, amount)
553                        .unwrap();
554                }
555            }
556        }
557
558        assert_eq!(new_deltas, deltas);
559    }
560
561    #[test]
562    fn test_unmatched() {
563        let mut deltas = TransferMatcher::default();
564        let [a, b, _c, d, e, f, g]: [AccountId; 7] =
565            ["a", "b", "c", "d", "e", "f", "g"].map(|s| format!("{s}.near").parse().unwrap());
566        let [ft1, ft2] = ["ft1", "ft2"].map(|a| {
567            TokenId::from(Nep141TokenId::new(
568                format!("{a}.near").parse::<AccountId>().unwrap(),
569            ))
570        });
571
572        for (owner, token_id, delta) in [
573            (&a, &ft1, -5),
574            (&b, &ft1, 4),
575            (&d, &ft1, -10),
576            (&e, &ft1, -1),
577            (&f, &ft1, 10),
578            (&g, &ft1, -1),
579            (&a, &ft2, -1),
580        ] {
581            assert!(deltas.add_delta(owner.clone(), token_id.clone(), delta));
582        }
583
584        assert_eq!(
585            deltas.finalize().unwrap_err(),
586            InvariantViolated::UnmatchedDeltas {
587                unmatched_deltas: TokenDeltas::default()
588                    .with_apply_delta(ft1, -3)
589                    .unwrap()
590                    .with_apply_delta(ft2, -1)
591                    .unwrap()
592            }
593        );
594    }
595}