tycho_common/models/
contract.rs

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