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