Skip to main content

tycho_common/models/
contract.rs

1use std::collections::HashMap;
2
3use deepsize::DeepSizeOf;
4use serde::{Deserialize, Serialize};
5use tracing::warn;
6
7use crate::{
8    dto, keccak256,
9    models::{
10        blockchain::Transaction, Address, Balance, Chain, ChangeType, Code, CodeHash, ContractId,
11        ContractStore, ContractStoreDeltas, MergeError, StoreKey, TxHash,
12    },
13    Bytes,
14};
15
16#[derive(Clone, Debug, PartialEq)]
17pub struct Account {
18    pub chain: Chain,
19    pub address: Address,
20    pub title: String,
21    pub slots: ContractStore,
22    pub native_balance: Balance,
23    pub token_balances: HashMap<Address, AccountBalance>,
24    pub code: Code,
25    pub code_hash: CodeHash,
26    pub balance_modify_tx: TxHash,
27    pub code_modify_tx: TxHash,
28    pub creation_tx: Option<TxHash>,
29}
30
31impl Account {
32    #[allow(clippy::too_many_arguments)]
33    pub fn new(
34        chain: Chain,
35        address: Address,
36        title: String,
37        slots: ContractStore,
38        native_balance: Balance,
39        token_balances: HashMap<Address, AccountBalance>,
40        code: Code,
41        code_hash: CodeHash,
42        balance_modify_tx: TxHash,
43        code_modify_tx: TxHash,
44        creation_tx: Option<TxHash>,
45    ) -> Self {
46        Self {
47            chain,
48            address,
49            title,
50            slots,
51            native_balance,
52            token_balances,
53            code,
54            code_hash,
55            balance_modify_tx,
56            code_modify_tx,
57            creation_tx,
58        }
59    }
60
61    pub fn set_balance(&mut self, new_balance: &Balance, modified_at: &Balance) {
62        self.native_balance = new_balance.clone();
63        self.balance_modify_tx = modified_at.clone();
64    }
65
66    pub fn apply_delta(&mut self, delta: &AccountDelta) -> Result<(), MergeError> {
67        let self_id = (self.chain, &self.address);
68        let other_id = (delta.chain, &delta.address);
69        if self_id != other_id {
70            return Err(MergeError::IdMismatch(
71                "AccountDeltas".to_string(),
72                format!("{self_id:?}"),
73                format!("{other_id:?}"),
74            ));
75        }
76        if let Some(balance) = delta.balance.as_ref() {
77            self.native_balance.clone_from(balance);
78        }
79        if let Some(code) = delta.code.as_ref() {
80            self.code.clone_from(code);
81        }
82        self.slots.extend(
83            delta
84                .slots
85                .clone()
86                .into_iter()
87                .map(|(k, v)| (k, v.unwrap_or_default())),
88        );
89        // TODO: Update modify_tx, code_modify_tx and code_hash.
90        Ok(())
91    }
92}
93
94#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, Default, DeepSizeOf)]
95pub struct AccountDelta {
96    pub chain: Chain,
97    pub address: Address,
98    pub slots: ContractStoreDeltas,
99    pub balance: Option<Balance>,
100    code: Option<Code>,
101    change: ChangeType,
102}
103
104impl AccountDelta {
105    pub fn deleted(chain: &Chain, address: &Address) -> Self {
106        Self {
107            chain: *chain,
108            address: address.clone(),
109            change: ChangeType::Deletion,
110            ..Default::default()
111        }
112    }
113
114    pub fn new(
115        chain: Chain,
116        address: Address,
117        slots: ContractStoreDeltas,
118        balance: Option<Balance>,
119        code: Option<Code>,
120        change: ChangeType,
121    ) -> Self {
122        if code.is_none() && matches!(change, ChangeType::Creation) {
123            warn!(?address, "Instantiated AccountDelta without code marked as creation!")
124        }
125        Self { chain, address, slots, balance, code, change }
126    }
127
128    pub fn contract_id(&self) -> ContractId {
129        ContractId::new(self.chain, self.address.clone())
130    }
131
132    pub fn into_account(self, tx: &Transaction) -> Account {
133        let empty_hash = keccak256(Vec::new());
134        Account::new(
135            self.chain,
136            self.address.clone(),
137            format!("{:#020x}", self.address),
138            self.slots
139                .into_iter()
140                .map(|(k, v)| (k, v.unwrap_or_default()))
141                .collect(),
142            self.balance.unwrap_or_default(),
143            // token balances are not set in the delta
144            HashMap::new(),
145            self.code.clone().unwrap_or_default(),
146            self.code
147                .as_ref()
148                .map(keccak256)
149                .unwrap_or(empty_hash)
150                .into(),
151            tx.hash.clone(),
152            tx.hash.clone(),
153            Some(tx.hash.clone()),
154        )
155    }
156
157    /// Convert the delta into an account. Note that data not present in the delta, such as
158    /// creation_tx etc, will be initialized to default values.
159    pub fn into_account_without_tx(self) -> Account {
160        let empty_hash = keccak256(Vec::new());
161        Account::new(
162            self.chain,
163            self.address.clone(),
164            format!("{:#020x}", self.address),
165            self.slots
166                .into_iter()
167                .map(|(k, v)| (k, v.unwrap_or_default()))
168                .collect(),
169            self.balance.unwrap_or_default(),
170            // token balances are not set in the delta
171            HashMap::new(),
172            self.code.clone().unwrap_or_default(),
173            self.code
174                .as_ref()
175                .map(keccak256)
176                .unwrap_or(empty_hash)
177                .into(),
178            Bytes::from("0x00"),
179            Bytes::from("0x00"),
180            None,
181        )
182    }
183
184    // Convert AccountUpdate into Account using references.
185    pub fn ref_into_account(&self, tx: &Transaction) -> Account {
186        let empty_hash = keccak256(Vec::new());
187        if self.change != ChangeType::Creation {
188            warn!("Creating an account from a partial change!")
189        }
190
191        Account::new(
192            self.chain,
193            self.address.clone(),
194            format!("{:#020x}", self.address),
195            self.slots
196                .clone()
197                .into_iter()
198                .map(|(k, v)| (k, v.unwrap_or_default()))
199                .collect(),
200            self.balance.clone().unwrap_or_default(),
201            // token balances are not set in the delta
202            HashMap::new(),
203            self.code.clone().unwrap_or_default(),
204            self.code
205                .as_ref()
206                .map(keccak256)
207                .unwrap_or(empty_hash)
208                .into(),
209            tx.hash.clone(),
210            tx.hash.clone(),
211            Some(tx.hash.clone()),
212        )
213    }
214
215    /// Merge this update (`self`) with another one (`other`)
216    ///
217    /// This function is utilized for aggregating multiple updates into a single
218    /// update. The attribute values of `other` are set on `self`.
219    /// Meanwhile, contract storage maps are merged, with keys from `other` taking precedence.
220    /// A creation in `other` makes the result a creation: the DCI can start to track an account
221    /// after earlier transactions of the block updated it.
222    ///
223    /// Be noted that, this function will mutate the state of the calling
224    /// struct. An error will occur if merging updates from different accounts.
225    ///
226    /// There are no further validation checks within this method, hence it
227    /// could be used as needed.
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        if other.change == ChangeType::Creation {
254            self.change = ChangeType::Creation;
255        }
256
257        if self.code.is_none() && matches!(self.change, ChangeType::Creation) {
258            warn!(address=?self.address, "AccountDelta without code marked as creation after merge!")
259        }
260
261        Ok(())
262    }
263
264    pub fn is_update(&self) -> bool {
265        self.change == ChangeType::Update
266    }
267
268    pub fn is_creation(&self) -> bool {
269        self.change == ChangeType::Creation
270    }
271
272    pub fn change_type(&self) -> ChangeType {
273        self.change
274    }
275
276    pub fn code(&self) -> &Option<Code> {
277        &self.code
278    }
279
280    pub fn set_code(&mut self, code: Bytes) {
281        self.code = Some(code)
282    }
283}
284
285impl From<Account> for AccountDelta {
286    fn from(value: Account) -> Self {
287        Self::new(
288            value.chain,
289            value.address,
290            value
291                .slots
292                .into_iter()
293                .map(|(k, v)| (k, Some(v)))
294                .collect(),
295            Some(value.native_balance),
296            Some(value.code),
297            ChangeType::Creation,
298        )
299    }
300}
301
302#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, DeepSizeOf)]
303pub struct AccountBalance {
304    pub account: Address,
305    pub token: Address,
306    pub balance: Balance,
307    pub modify_tx: TxHash,
308}
309
310impl AccountBalance {
311    pub fn new(account: Address, token: Address, balance: Balance, modify_tx: TxHash) -> Self {
312        Self { account, token, balance, modify_tx }
313    }
314}
315
316#[derive(Debug, PartialEq, Clone, DeepSizeOf)]
317pub struct ContractStorageChange {
318    pub value: Bytes,
319    pub previous: Bytes,
320}
321
322impl ContractStorageChange {
323    pub fn new(value: impl Into<Bytes>, previous: impl Into<Bytes>) -> Self {
324        Self { value: value.into(), previous: previous.into() }
325    }
326
327    pub fn initial(value: impl Into<Bytes>) -> Self {
328        Self { value: value.into(), previous: Bytes::default() }
329    }
330}
331
332#[derive(Debug, PartialEq, Default, Clone, DeepSizeOf)]
333pub struct ContractChanges {
334    pub account: Address,
335    pub slots: HashMap<StoreKey, ContractStorageChange>,
336    pub native_balance: Option<Balance>,
337}
338
339impl ContractChanges {
340    pub fn new(
341        account: Address,
342        slots: HashMap<StoreKey, ContractStorageChange>,
343        native_balance: Option<Balance>,
344    ) -> Self {
345        Self { account, slots, native_balance }
346    }
347}
348
349/// Multiple binary key-value stores grouped by account address.
350pub type AccountToContractChanges = HashMap<Address, ContractChanges>;
351
352impl From<dto::AccountBalance> for AccountBalance {
353    fn from(value: dto::AccountBalance) -> Self {
354        Self {
355            token: value.token,
356            balance: value.balance,
357            modify_tx: value.modify_tx,
358            account: value.account,
359        }
360    }
361}
362
363impl From<dto::AccountUpdate> for AccountDelta {
364    fn from(value: dto::AccountUpdate) -> Self {
365        // Client receives zero-value writes, not explicit deletions via WS,
366        // so all slot values are treated as Some(v).
367        AccountDelta::new(
368            value.chain.into(),
369            value.address,
370            value
371                .slots
372                .into_iter()
373                .map(|(k, v)| (k, Some(v)))
374                .collect(),
375            value.balance,
376            value.code,
377            value.change.into(),
378        )
379    }
380}
381
382#[allow(deprecated)] // creation_tx field on ResponseAccount is deprecated
383impl From<dto::ResponseAccount> for Account {
384    fn from(value: dto::ResponseAccount) -> Self {
385        // Snapshot responses don't carry per-balance modify_tx; use zero as placeholder.
386        let token_balances = value
387            .token_balances
388            .into_iter()
389            .map(|(token, balance)| {
390                let acct = value.address.clone();
391                (
392                    token.clone(),
393                    AccountBalance {
394                        token,
395                        balance,
396                        modify_tx: crate::Bytes::zero(32),
397                        account: acct,
398                    },
399                )
400            })
401            .collect();
402        Account::new(
403            value.chain.into(),
404            value.address,
405            value.title,
406            value.slots,
407            value.native_balance,
408            token_balances,
409            value.code,
410            value.code_hash,
411            value.balance_modify_tx,
412            value.code_modify_tx,
413            value.creation_tx,
414        )
415    }
416}
417
418#[cfg(test)]
419mod test {
420    use std::str::FromStr;
421
422    use super::*;
423
424    fn update_balance_delta() -> AccountDelta {
425        AccountDelta::new(
426            Chain::Ethereum,
427            Bytes::from_str("e688b84b23f322a994A53dbF8E15FA82CDB71127").unwrap(),
428            HashMap::new(),
429            Some(Bytes::from(420u64).lpad(32, 0)),
430            None,
431            ChangeType::Update,
432        )
433    }
434
435    fn update_slots_delta() -> AccountDelta {
436        AccountDelta::new(
437            Chain::Ethereum,
438            Bytes::from_str("e688b84b23f322a994A53dbF8E15FA82CDB71127").unwrap(),
439            slots([(0, 1), (1, 2)]),
440            None,
441            None,
442            ChangeType::Update,
443        )
444    }
445
446    // Utils function that return slots that match `AccountDelta` slots.
447    // TODO: this is temporary, we shoud make AccountDelta.slots use Bytes instead of Option<Bytes>
448    pub fn slots(data: impl IntoIterator<Item = (u64, u64)>) -> HashMap<Bytes, Option<Bytes>> {
449        data.into_iter()
450            .map(|(s, v)| (Bytes::from(s).lpad(32, 0), Some(Bytes::from(v).lpad(32, 0))))
451            .collect()
452    }
453
454    #[test]
455    fn test_merge_account_deltas() {
456        let mut update_left = update_balance_delta();
457        let update_right = update_slots_delta();
458        let mut exp = update_slots_delta();
459        exp.balance = Some(Bytes::from(420u64).lpad(32, 0));
460
461        update_left.merge(update_right).unwrap();
462
463        assert_eq!(update_left, exp);
464    }
465
466    fn creation_delta() -> AccountDelta {
467        AccountDelta::new(
468            Chain::Ethereum,
469            Bytes::from_str("e688b84b23f322a994A53dbF8E15FA82CDB71127").unwrap(),
470            slots([(1, 3), (2, 4)]),
471            Some(Bytes::from(7u64).lpad(32, 0)),
472            Some(Bytes::from("0x6000")),
473            ChangeType::Creation,
474        )
475    }
476
477    #[test]
478    fn test_merge_creation_into_update_is_creation() {
479        let mut delta = update_slots_delta();
480
481        delta.merge(creation_delta()).unwrap();
482
483        assert_eq!(delta.change_type(), ChangeType::Creation);
484        assert_eq!(delta.slots, slots([(0, 1), (1, 3), (2, 4)]));
485        assert_eq!(delta.code(), &Some(Bytes::from("0x6000")));
486    }
487
488    #[test]
489    fn test_merge_update_into_creation_stays_creation() {
490        let mut delta = creation_delta();
491
492        delta
493            .merge(update_slots_delta())
494            .unwrap();
495
496        assert_eq!(delta.change_type(), ChangeType::Creation);
497        assert_eq!(delta.slots, slots([(0, 1), (1, 2), (2, 4)]));
498    }
499
500    #[test]
501    fn test_merge_deletion_into_update_stays_update() {
502        let mut delta = update_slots_delta();
503        let address = delta.address.clone();
504
505        delta
506            .merge(AccountDelta::deleted(&Chain::Ethereum, &address))
507            .unwrap();
508
509        assert_eq!(delta.change_type(), ChangeType::Update);
510    }
511
512    #[test]
513    fn test_merge_account_delta_wrong_address() {
514        let mut update_left = update_balance_delta();
515        let mut update_right = update_slots_delta();
516        update_right.address = Bytes::zero(20);
517        let exp = Err(MergeError::IdMismatch(
518            "AccountDelta".to_string(),
519            format!("{:#020x}", update_left.address),
520            format!("{:#020x}", update_right.address),
521        ));
522
523        let res = update_left.merge(update_right);
524
525        assert_eq!(res, exp);
526    }
527
528    #[test]
529    fn test_account_from_delta_ref_into_account() {
530        let code = vec![0, 0, 0, 0];
531        let code_hash = Bytes::from(keccak256(&code));
532        let tx = Transaction::new(
533            Bytes::zero(32),
534            Bytes::zero(32),
535            Bytes::zero(20),
536            Some(Bytes::zero(20)),
537            10,
538        );
539
540        let delta = AccountDelta::new(
541            Chain::Ethereum,
542            Bytes::from_str("e688b84b23f322a994A53dbF8E15FA82CDB71127").unwrap(),
543            HashMap::new(),
544            Some(Bytes::from(10000u64).lpad(32, 0)),
545            Some(code.clone().into()),
546            ChangeType::Update,
547        );
548
549        let expected = Account::new(
550            Chain::Ethereum,
551            "0xe688b84b23f322a994A53dbF8E15FA82CDB71127"
552                .parse()
553                .unwrap(),
554            "0xe688b84b23f322a994a53dbf8e15fa82cdb71127".into(),
555            HashMap::new(),
556            Bytes::from(10000u64).lpad(32, 0),
557            HashMap::new(),
558            code.into(),
559            code_hash,
560            Bytes::zero(32),
561            Bytes::zero(32),
562            Some(Bytes::zero(32)),
563        );
564
565        assert_eq!(delta.ref_into_account(&tx), expected);
566    }
567}