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