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