tycho_common/models/
contract.rs

1use std::collections::{hash_map::Entry, HashMap};
2
3use serde::{Deserialize, Serialize};
4use tracing::warn;
5
6use crate::{
7    keccak256,
8    models::{
9        blockchain::Transaction,
10        protocol::{ComponentBalance, ProtocolComponent},
11        Address, Balance, Chain, ChangeType, Code, CodeHash, ComponentId, ContractId,
12        ContractStore, ContractStoreDeltas, MergeError, StoreKey, TxHash,
13    },
14    Bytes,
15};
16
17#[derive(Clone, Debug, PartialEq)]
18pub struct Account {
19    pub chain: Chain,
20    pub address: Address,
21    pub title: String,
22    pub slots: ContractStore,
23    pub native_balance: Balance,
24    pub token_balances: HashMap<Address, AccountBalance>,
25    pub code: Code,
26    pub code_hash: CodeHash,
27    pub balance_modify_tx: TxHash,
28    pub code_modify_tx: TxHash,
29    pub creation_tx: Option<TxHash>,
30}
31
32impl Account {
33    #[allow(clippy::too_many_arguments)]
34    pub fn new(
35        chain: Chain,
36        address: Address,
37        title: String,
38        slots: ContractStore,
39        native_balance: Balance,
40        token_balances: HashMap<Address, AccountBalance>,
41        code: Code,
42        code_hash: CodeHash,
43        balance_modify_tx: TxHash,
44        code_modify_tx: TxHash,
45        creation_tx: Option<TxHash>,
46    ) -> Self {
47        Self {
48            chain,
49            address,
50            title,
51            slots,
52            native_balance,
53            token_balances,
54            code,
55            code_hash,
56            balance_modify_tx,
57            code_modify_tx,
58            creation_tx,
59        }
60    }
61
62    pub fn set_balance(&mut self, new_balance: &Balance, modified_at: &Balance) {
63        self.native_balance = new_balance.clone();
64        self.balance_modify_tx = modified_at.clone();
65    }
66
67    pub fn apply_delta(&mut self, delta: &AccountDelta) -> Result<(), MergeError> {
68        let self_id = (self.chain, &self.address);
69        let other_id = (delta.chain, &delta.address);
70        if self_id != other_id {
71            return Err(MergeError::IdMismatch(
72                "AccountDeltas".to_string(),
73                format!("{self_id:?}"),
74                format!("{other_id:?}"),
75            ));
76        }
77        if let Some(balance) = delta.balance.as_ref() {
78            self.native_balance.clone_from(balance);
79        }
80        if let Some(code) = delta.code.as_ref() {
81            self.code.clone_from(code);
82        }
83        self.slots.extend(
84            delta
85                .slots
86                .clone()
87                .into_iter()
88                .map(|(k, v)| (k, v.unwrap_or_default())),
89        );
90        // TODO: Update modify_tx, code_modify_tx and code_hash.
91        Ok(())
92    }
93}
94
95#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, Default)]
96pub struct AccountDelta {
97    pub chain: Chain,
98    pub address: Address,
99    pub slots: ContractStoreDeltas,
100    pub balance: Option<Balance>,
101    code: Option<Code>,
102    change: ChangeType,
103}
104
105impl AccountDelta {
106    pub fn deleted(chain: &Chain, address: &Address) -> Self {
107        Self {
108            chain: *chain,
109            address: address.clone(),
110            change: ChangeType::Deletion,
111            ..Default::default()
112        }
113    }
114
115    pub fn new(
116        chain: Chain,
117        address: Address,
118        slots: ContractStoreDeltas,
119        balance: Option<Balance>,
120        code: Option<Code>,
121        change: ChangeType,
122    ) -> Self {
123        if code.is_none() && matches!(change, ChangeType::Creation) {
124            warn!(?address, "Instantiated AccountDelta without code marked as creation!")
125        }
126        Self { chain, address, slots, balance, code, change }
127    }
128
129    pub fn contract_id(&self) -> ContractId {
130        ContractId::new(self.chain, self.address.clone())
131    }
132
133    pub fn into_account(self, tx: &Transaction) -> Account {
134        let empty_hash = keccak256(Vec::new());
135        Account::new(
136            self.chain,
137            self.address.clone(),
138            format!("{:#020x}", self.address),
139            self.slots
140                .into_iter()
141                .map(|(k, v)| (k, v.unwrap_or_default()))
142                .collect(),
143            self.balance.unwrap_or_default(),
144            // token balances are not set in the delta
145            HashMap::new(),
146            self.code.clone().unwrap_or_default(),
147            self.code
148                .as_ref()
149                .map(keccak256)
150                .unwrap_or(empty_hash)
151                .into(),
152            tx.hash.clone(),
153            tx.hash.clone(),
154            Some(tx.hash.clone()),
155        )
156    }
157
158    /// Convert the delta into an account. Note that data not present in the delta, such as
159    /// creation_tx etc, will be initialized to default values.
160    pub fn into_account_without_tx(self) -> Account {
161        let empty_hash = keccak256(Vec::new());
162        Account::new(
163            self.chain,
164            self.address.clone(),
165            format!("{:#020x}", self.address),
166            self.slots
167                .into_iter()
168                .map(|(k, v)| (k, v.unwrap_or_default()))
169                .collect(),
170            self.balance.unwrap_or_default(),
171            // token balances are not set in the delta
172            HashMap::new(),
173            self.code.clone().unwrap_or_default(),
174            self.code
175                .as_ref()
176                .map(keccak256)
177                .unwrap_or(empty_hash)
178                .into(),
179            Bytes::from("0x00"),
180            Bytes::from("0x00"),
181            None,
182        )
183    }
184
185    // Convert AccountUpdate into Account using references.
186    pub fn ref_into_account(&self, tx: &Transaction) -> Account {
187        let empty_hash = keccak256(Vec::new());
188        if self.change != ChangeType::Creation {
189            warn!("Creating an account from a partial change!")
190        }
191
192        Account::new(
193            self.chain,
194            self.address.clone(),
195            format!("{:#020x}", self.address),
196            self.slots
197                .clone()
198                .into_iter()
199                .map(|(k, v)| (k, v.unwrap_or_default()))
200                .collect(),
201            self.balance.clone().unwrap_or_default(),
202            // token balances are not set in the delta
203            HashMap::new(),
204            self.code.clone().unwrap_or_default(),
205            self.code
206                .as_ref()
207                .map(keccak256)
208                .unwrap_or(empty_hash)
209                .into(),
210            tx.hash.clone(),
211            tx.hash.clone(),
212            Some(tx.hash.clone()),
213        )
214    }
215
216    /// Merge this update (`self`) with another one (`other`)
217    ///
218    /// This function is utilized for aggregating multiple updates into a single
219    /// update. The attribute values of `other` are set on `self`.
220    /// Meanwhile, contract storage maps are merged, with keys from `other` taking precedence.
221    ///
222    /// Be noted that, this function will mutate the state of the calling
223    /// struct. An error will occur if merging updates from different accounts.
224    ///
225    /// There are no further validation checks within this method, hence it
226    /// could be used as needed. However, you should give preference to
227    /// utilizing [AccountChangesWithTx] for merging, when possible.
228    ///
229    /// # Errors
230    ///
231    /// It returns an `CoreError::MergeError` error if `self.address` and
232    /// `other.address` are not identical.
233    ///
234    /// # Arguments
235    ///
236    /// * `other`: An instance of `AccountUpdate`. The attribute values and keys of `other` will
237    ///   overwrite those of `self`.
238    pub fn merge(&mut self, other: AccountDelta) -> Result<(), MergeError> {
239        if self.address != other.address {
240            return Err(MergeError::IdMismatch(
241                "AccountDelta".to_string(),
242                format!("{:#020x}", self.address),
243                format!("{:#020x}", other.address),
244            ));
245        }
246
247        self.slots.extend(other.slots);
248
249        if let Some(balance) = other.balance {
250            self.balance = Some(balance)
251        }
252        self.code = other.code.or(self.code.take());
253
254        if self.code.is_none() && matches!(self.change, ChangeType::Creation) {
255            warn!(address=?self.address, "AccountDelta without code marked as creation after merge!")
256        }
257
258        Ok(())
259    }
260
261    pub fn is_update(&self) -> bool {
262        self.change == ChangeType::Update
263    }
264
265    pub fn is_creation(&self) -> bool {
266        self.change == ChangeType::Creation
267    }
268
269    pub fn change_type(&self) -> ChangeType {
270        self.change
271    }
272
273    pub fn code(&self) -> &Option<Code> {
274        &self.code
275    }
276
277    pub fn set_code(&mut self, code: Bytes) {
278        self.code = Some(code)
279    }
280}
281
282impl From<Account> for AccountDelta {
283    fn from(value: Account) -> Self {
284        Self::new(
285            value.chain,
286            value.address,
287            value
288                .slots
289                .into_iter()
290                .map(|(k, v)| (k, Some(v)))
291                .collect(),
292            Some(value.native_balance),
293            Some(value.code),
294            ChangeType::Creation,
295        )
296    }
297}
298
299#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
300pub struct AccountBalance {
301    pub account: Address,
302    pub token: Address,
303    pub balance: Balance,
304    pub modify_tx: TxHash,
305}
306
307impl AccountBalance {
308    pub fn new(account: Address, token: Address, balance: Balance, modify_tx: TxHash) -> Self {
309        Self { account, token, balance, modify_tx }
310    }
311}
312
313/// Updates grouped by their respective transaction.
314#[derive(Debug, Clone, PartialEq)]
315pub struct AccountChangesWithTx {
316    // map of account changes in the transaction
317    pub account_deltas: HashMap<Address, AccountDelta>,
318    // map of new protocol components created in the transaction
319    pub protocol_components: HashMap<ComponentId, ProtocolComponent>,
320    // map of component balance updates given as component ids to their token-balance pairs
321    pub component_balances: HashMap<ComponentId, HashMap<Address, ComponentBalance>>,
322    // map of account balance updates given as account addresses to their token-balance pairs
323    pub account_balances: HashMap<Address, HashMap<Address, AccountBalance>>,
324    // transaction linked to the updates
325    pub tx: Transaction,
326}
327
328impl AccountChangesWithTx {
329    pub fn new(
330        account_deltas: HashMap<Address, AccountDelta>,
331        protocol_components: HashMap<ComponentId, ProtocolComponent>,
332        component_balances: HashMap<ComponentId, HashMap<Address, ComponentBalance>>,
333        account_balances: HashMap<Address, HashMap<Address, AccountBalance>>,
334        tx: Transaction,
335    ) -> Self {
336        Self { account_deltas, protocol_components, component_balances, account_balances, tx }
337    }
338
339    /// Merges this update with another one.
340    ///
341    /// The method combines two `AccountUpdateWithTx` instances under certain
342    /// conditions:
343    /// - The block from which both updates came should be the same. If the updates are from
344    ///   different blocks, the method will return an error.
345    /// - The transactions for each of the updates should be distinct. If they come from the same
346    ///   transaction, the method will return an error.
347    /// - The order of the transaction matters. The transaction from `other` must have occurred
348    ///   later than the self transaction. If the self transaction has a higher index than `other`,
349    ///   the method will return an error.
350    ///
351    /// The merged update keeps the transaction of `other`.
352    ///
353    /// # Errors
354    /// This method will return an error if any of the above conditions is violated.
355    pub fn merge(&mut self, other: &AccountChangesWithTx) -> Result<(), MergeError> {
356        if self.tx.block_hash != other.tx.block_hash {
357            return Err(MergeError::BlockMismatch(
358                "AccountChangesWithTx".to_string(),
359                self.tx.block_hash.clone(),
360                other.tx.block_hash.clone(),
361            ));
362        }
363        if self.tx.hash == other.tx.hash {
364            return Err(MergeError::SameTransaction(
365                "AccountChangesWithTx".to_string(),
366                self.tx.hash.clone(),
367            ));
368        }
369        if self.tx.index > other.tx.index {
370            return Err(MergeError::TransactionOrderError(
371                "AccountChangesWithTx".to_string(),
372                self.tx.index,
373                other.tx.index,
374            ));
375        }
376
377        self.tx = other.tx.clone();
378
379        for (address, update) in other.account_deltas.clone().into_iter() {
380            match self.account_deltas.entry(address) {
381                Entry::Occupied(mut e) => {
382                    e.get_mut().merge(update)?;
383                }
384                Entry::Vacant(e) => {
385                    e.insert(update);
386                }
387            }
388        }
389
390        // Add new protocol components
391        self.protocol_components
392            .extend(other.protocol_components.clone());
393
394        // Add new component balances and overwrite existing ones
395        for (component_id, balance_by_token_map) in other
396            .component_balances
397            .clone()
398            .into_iter()
399        {
400            // Check if the key exists in the first map
401            if let Some(existing_inner_map) = self
402                .component_balances
403                .get_mut(&component_id)
404            {
405                // Iterate through the inner map and update values
406                for (token, value) in balance_by_token_map {
407                    existing_inner_map.insert(token, value);
408                }
409            } else {
410                self.component_balances
411                    .insert(component_id, balance_by_token_map);
412            }
413        }
414
415        // Add new account balances and overwrite existing ones
416        for (account_addr, balance_by_token_map) in other
417            .account_balances
418            .clone()
419            .into_iter()
420        {
421            // Check if the key exists in the first map
422            if let Some(existing_inner_map) = self
423                .account_balances
424                .get_mut(&account_addr)
425            {
426                // Iterate through the inner map and update values
427                for (token, value) in balance_by_token_map {
428                    existing_inner_map.insert(token, value);
429                }
430            } else {
431                self.account_balances
432                    .insert(account_addr, balance_by_token_map);
433            }
434        }
435
436        Ok(())
437    }
438}
439
440impl From<&AccountChangesWithTx> for Vec<Account> {
441    /// Creates a full account from a change.
442    ///
443    /// This can be used to get an insertable an account if we know the update
444    /// is actually a creation.
445    ///
446    /// Assumes that all relevant changes are set on `self` if something is
447    /// missing, it will use the corresponding types default.
448    /// Will use the associated transaction as creation, balance and code modify
449    /// transaction.
450    fn from(value: &AccountChangesWithTx) -> Self {
451        value
452            .account_deltas
453            .clone()
454            .into_values()
455            .map(|update| {
456                let acc = Account::new(
457                    update.chain,
458                    update.address.clone(),
459                    format!("{:#020x}", update.address),
460                    update
461                        .slots
462                        .into_iter()
463                        .map(|(k, v)| (k, v.unwrap_or_default())) //TODO: is default ok here or should it be Bytes::zero(32)
464                        .collect(),
465                    update.balance.unwrap_or_default(),
466                    value
467                        .account_balances
468                        .get(&update.address)
469                        .cloned()
470                        .unwrap_or_default(),
471                    update.code.clone().unwrap_or_default(),
472                    update
473                        .code
474                        .as_ref()
475                        .map(keccak256)
476                        .unwrap_or_default()
477                        .into(),
478                    value.tx.hash.clone(),
479                    value.tx.hash.clone(),
480                    Some(value.tx.hash.clone()),
481                );
482                acc
483            })
484            .collect()
485    }
486}
487
488#[derive(Debug, PartialEq, Clone)]
489pub struct ContractStorageChange {
490    pub value: Bytes,
491    pub previous: Bytes,
492}
493
494impl ContractStorageChange {
495    pub fn new(value: impl Into<Bytes>, previous: impl Into<Bytes>) -> Self {
496        Self { value: value.into(), previous: previous.into() }
497    }
498
499    pub fn initial(value: impl Into<Bytes>) -> Self {
500        Self { value: value.into(), previous: Bytes::default() }
501    }
502}
503
504#[derive(Debug, PartialEq, Default, Clone)]
505pub struct ContractChanges {
506    pub account: Address,
507    pub slots: HashMap<StoreKey, ContractStorageChange>,
508    pub native_balance: Option<Balance>,
509}
510
511impl ContractChanges {
512    pub fn new(
513        account: Address,
514        slots: HashMap<StoreKey, ContractStorageChange>,
515        native_balance: Option<Balance>,
516    ) -> Self {
517        Self { account, slots, native_balance }
518    }
519}
520
521/// Multiple binary key-value stores grouped by account address.
522pub type AccountToContractChanges = HashMap<Address, ContractChanges>;
523
524#[cfg(test)]
525mod test {
526    use std::str::FromStr;
527
528    use chrono::DateTime;
529    use rstest::rstest;
530
531    use super::*;
532    use crate::models::blockchain::fixtures as block_fixtures;
533
534    const HASH_256_0: &str = "0x0000000000000000000000000000000000000000000000000000000000000000";
535    const HASH_256_1: &str = "0x0000000000000000000000000000000000000000000000000000000000000001";
536
537    fn update_balance_delta() -> AccountDelta {
538        AccountDelta::new(
539            Chain::Ethereum,
540            Bytes::from_str("e688b84b23f322a994A53dbF8E15FA82CDB71127").unwrap(),
541            HashMap::new(),
542            Some(Bytes::from(420u64).lpad(32, 0)),
543            None,
544            ChangeType::Update,
545        )
546    }
547
548    fn update_slots_delta() -> AccountDelta {
549        AccountDelta::new(
550            Chain::Ethereum,
551            Bytes::from_str("e688b84b23f322a994A53dbF8E15FA82CDB71127").unwrap(),
552            slots([(0, 1), (1, 2)]),
553            None,
554            None,
555            ChangeType::Update,
556        )
557    }
558
559    // Utils function that return slots that match `AccountDelta` slots.
560    // TODO: this is temporary, we shoud make AccountDelta.slots use Bytes instead of Option<Bytes>
561    pub fn slots(data: impl IntoIterator<Item = (u64, u64)>) -> HashMap<Bytes, Option<Bytes>> {
562        data.into_iter()
563            .map(|(s, v)| (Bytes::from(s).lpad(32, 0), Some(Bytes::from(v).lpad(32, 0))))
564            .collect()
565    }
566
567    #[test]
568    fn test_merge_account_deltas() {
569        let mut update_left = update_balance_delta();
570        let update_right = update_slots_delta();
571        let mut exp = update_slots_delta();
572        exp.balance = Some(Bytes::from(420u64).lpad(32, 0));
573
574        update_left.merge(update_right).unwrap();
575
576        assert_eq!(update_left, exp);
577    }
578
579    #[test]
580    fn test_merge_account_delta_wrong_address() {
581        let mut update_left = update_balance_delta();
582        let mut update_right = update_slots_delta();
583        update_right.address = Bytes::zero(20);
584        let exp = Err(MergeError::IdMismatch(
585            "AccountDelta".to_string(),
586            format!("{:#020x}", update_left.address),
587            format!("{:#020x}", update_right.address),
588        ));
589
590        let res = update_left.merge(update_right);
591
592        assert_eq!(res, exp);
593    }
594
595    fn tx_vm_update() -> AccountChangesWithTx {
596        let code = vec![0, 0, 0, 0];
597        let mut account_updates = HashMap::new();
598        account_updates.insert(
599            "0xe688b84b23f322a994A53dbF8E15FA82CDB71127"
600                .parse()
601                .unwrap(),
602            AccountDelta::new(
603                Chain::Ethereum,
604                Bytes::from_str("e688b84b23f322a994A53dbF8E15FA82CDB71127").unwrap(),
605                HashMap::new(),
606                Some(Bytes::from(10000u64).lpad(32, 0)),
607                Some(code.into()),
608                ChangeType::Update,
609            ),
610        );
611
612        AccountChangesWithTx::new(
613            account_updates,
614            HashMap::new(),
615            HashMap::new(),
616            HashMap::new(),
617            block_fixtures::transaction01(),
618        )
619    }
620
621    fn account() -> Account {
622        let code = vec![0, 0, 0, 0];
623        let code_hash = Bytes::from(keccak256(&code));
624        Account::new(
625            Chain::Ethereum,
626            "0xe688b84b23f322a994A53dbF8E15FA82CDB71127"
627                .parse()
628                .unwrap(),
629            "0xe688b84b23f322a994a53dbf8e15fa82cdb71127".into(),
630            HashMap::new(),
631            Bytes::from(10000u64).lpad(32, 0),
632            HashMap::new(),
633            code.into(),
634            code_hash,
635            Bytes::zero(32),
636            Bytes::zero(32),
637            Some(Bytes::zero(32)),
638        )
639    }
640
641    #[test]
642    fn test_account_from_update_w_tx() {
643        let update = tx_vm_update();
644        let exp = account();
645
646        assert_eq!(
647            update
648                .account_deltas
649                .values()
650                .next()
651                .unwrap()
652                .ref_into_account(&update.tx),
653            exp
654        );
655    }
656
657    #[rstest]
658    #[case::diff_block(
659    block_fixtures::create_transaction(HASH_256_1, HASH_256_1, 11),
660    Err(MergeError::BlockMismatch(
661        "AccountChangesWithTx".to_string(),
662        Bytes::zero(32),
663        HASH_256_1.into(),
664    ))
665    )]
666    #[case::same_tx(
667    block_fixtures::create_transaction(HASH_256_0, HASH_256_0, 11),
668    Err(MergeError::SameTransaction(
669        "AccountChangesWithTx".to_string(),
670        Bytes::zero(32),
671    ))
672    )]
673    #[case::lower_idx(
674    block_fixtures::create_transaction(HASH_256_1, HASH_256_0, 1),
675    Err(MergeError::TransactionOrderError(
676        "AccountChangesWithTx".to_string(),
677        10,
678        1,
679    ))
680    )]
681    fn test_merge_vm_updates_w_tx(#[case] tx: Transaction, #[case] exp: Result<(), MergeError>) {
682        let mut left = tx_vm_update();
683        let mut right = left.clone();
684        right.tx = tx;
685
686        let res = left.merge(&right);
687
688        assert_eq!(res, exp);
689    }
690
691    fn create_protocol_component(tx_hash: Bytes) -> ProtocolComponent {
692        ProtocolComponent {
693            id: "d417ff54652c09bd9f31f216b1a2e5d1e28c1dce1ba840c40d16f2b4d09b5902".to_owned(),
694            protocol_system: "ambient".to_string(),
695            protocol_type_name: String::from("WeightedPool"),
696            chain: Chain::Ethereum,
697            tokens: vec![
698                Bytes::from_str("0x6B175474E89094C44Da98b954EedeAC495271d0F").unwrap(),
699                Bytes::from_str("0x6B175474E89094C44Da98b954EedeAC495271d0F").unwrap(),
700            ],
701            contract_addresses: vec![
702                Bytes::from_str("0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2").unwrap(),
703                Bytes::from_str("0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2").unwrap(),
704            ],
705            static_attributes: HashMap::from([
706                ("key1".to_string(), Bytes::from(b"value1".to_vec())),
707                ("key2".to_string(), Bytes::from(b"value2".to_vec())),
708            ]),
709            change: ChangeType::Creation,
710            creation_tx: tx_hash,
711            created_at: DateTime::from_timestamp(1000, 0)
712                .unwrap()
713                .naive_utc(),
714        }
715    }
716
717    #[rstest]
718    fn test_merge_transaction_vm_updates() {
719        let tx_first_update = block_fixtures::transaction01();
720        let tx_second_update = block_fixtures::create_transaction(HASH_256_1, HASH_256_0, 15);
721        let protocol_component_first_tx = create_protocol_component(tx_first_update.hash.clone());
722        let protocol_component_second_tx = create_protocol_component(tx_second_update.hash.clone());
723        let account_address =
724            Bytes::from_str("0x0000000000000000000000000000000061626364").unwrap();
725        let token_address = Bytes::from_str("0x0000000000000000000000000000000066666666").unwrap();
726
727        let first_update = AccountChangesWithTx {
728            account_deltas: [(
729                account_address.clone(),
730                AccountDelta::new(
731                    Chain::Ethereum,
732                    account_address.clone(),
733                    slots([(2711790500, 2981278644), (3250766788, 3520254932)]),
734                    Some(Bytes::from(1903326068u64).lpad(32, 0)),
735                    Some(vec![129, 130, 131, 132].into()),
736                    ChangeType::Update,
737                ),
738            )]
739            .into_iter()
740            .collect(),
741            protocol_components: [(
742                protocol_component_first_tx.id.clone(),
743                protocol_component_first_tx.clone(),
744            )]
745            .into_iter()
746            .collect(),
747            component_balances: [(
748                protocol_component_first_tx.id.clone(),
749                [(
750                    token_address.clone(),
751                    ComponentBalance {
752                        token: token_address.clone(),
753                        balance: Bytes::from(0_i32.to_be_bytes()),
754                        modify_tx: Default::default(),
755                        component_id: protocol_component_first_tx.id.clone(),
756                        balance_float: 0.0,
757                    },
758                )]
759                .into_iter()
760                .collect(),
761            )]
762            .into_iter()
763            .collect(),
764            account_balances: [(
765                account_address.clone(),
766                [(
767                    token_address.clone(),
768                    AccountBalance {
769                        token: token_address.clone(),
770                        balance: Bytes::from(0_i32.to_be_bytes()),
771                        modify_tx: Default::default(),
772                        account: account_address.clone(),
773                    },
774                )]
775                .into_iter()
776                .collect(),
777            )]
778            .into_iter()
779            .collect(),
780            tx: tx_first_update,
781        };
782        let second_update = AccountChangesWithTx {
783            account_deltas: [(
784                account_address.clone(),
785                AccountDelta::new(
786                    Chain::Ethereum,
787                    account_address.clone(),
788                    slots([(2981278644, 3250766788), (2442302356, 2711790500)]),
789                    Some(Bytes::from(4059231220u64).lpad(32, 0)),
790                    Some(vec![1, 2, 3, 4].into()),
791                    ChangeType::Update,
792                ),
793            )]
794            .into_iter()
795            .collect(),
796            protocol_components: [(
797                protocol_component_second_tx.id.clone(),
798                protocol_component_second_tx.clone(),
799            )]
800            .into_iter()
801            .collect(),
802            component_balances: [(
803                protocol_component_second_tx.id.clone(),
804                [(
805                    token_address.clone(),
806                    ComponentBalance {
807                        token: token_address.clone(),
808                        balance: Bytes::from(500000_i32.to_be_bytes()),
809                        modify_tx: Default::default(),
810                        component_id: protocol_component_first_tx.id.clone(),
811                        balance_float: 500000.0,
812                    },
813                )]
814                .into_iter()
815                .collect(),
816            )]
817            .into_iter()
818            .collect(),
819            account_balances: [(
820                account_address.clone(),
821                [(
822                    token_address.clone(),
823                    AccountBalance {
824                        token: token_address,
825                        balance: Bytes::from(20000_i32.to_be_bytes()),
826                        modify_tx: Default::default(),
827                        account: account_address,
828                    },
829                )]
830                .into_iter()
831                .collect(),
832            )]
833            .into_iter()
834            .collect(),
835            tx: tx_second_update,
836        };
837
838        // merge
839        let mut to_merge_on = first_update.clone();
840        to_merge_on
841            .merge(&second_update)
842            .unwrap();
843
844        // assertions
845        let expected_protocol_components: HashMap<ComponentId, ProtocolComponent> = [
846            (protocol_component_first_tx.id.clone(), protocol_component_first_tx.clone()),
847            (protocol_component_second_tx.id.clone(), protocol_component_second_tx.clone()),
848        ]
849        .into_iter()
850        .collect();
851        assert_eq!(to_merge_on.component_balances, second_update.component_balances);
852        assert_eq!(to_merge_on.account_balances, second_update.account_balances);
853        assert_eq!(to_merge_on.protocol_components, expected_protocol_components);
854
855        let mut acc_update = second_update
856            .account_deltas
857            .clone()
858            .into_values()
859            .next()
860            .unwrap();
861
862        acc_update.slots = slots([
863            (2442302356, 2711790500),
864            (2711790500, 2981278644),
865            (3250766788, 3520254932),
866            (2981278644, 3250766788),
867        ]);
868
869        let acc_update = [(acc_update.address.clone(), acc_update)]
870            .iter()
871            .cloned()
872            .collect();
873
874        assert_eq!(to_merge_on.account_deltas, acc_update);
875    }
876}