tycho_common/models/
contract.rs

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