1#![allow(deprecated)]
8use std::{
9 collections::{HashMap, HashSet},
10 fmt,
11 hash::{Hash, Hasher},
12};
13
14use chrono::{NaiveDateTime, Utc};
15use serde::{de, Deserialize, Deserializer, Serialize};
16use strum_macros::{Display, EnumString};
17use utoipa::{IntoParams, ToSchema};
18use uuid::Uuid;
19
20use crate::{
21 models,
22 serde_primitives::{
23 hex_bytes, hex_bytes_option, hex_hashmap_key, hex_hashmap_key_value, hex_hashmap_value,
24 },
25 Bytes,
26};
27
28#[derive(
30 Debug,
31 Clone,
32 Copy,
33 PartialEq,
34 Eq,
35 Hash,
36 Serialize,
37 Deserialize,
38 EnumString,
39 Display,
40 Default,
41 ToSchema,
42)]
43#[serde(rename_all = "lowercase")]
44#[strum(serialize_all = "lowercase")]
45pub enum Chain {
46 #[default]
47 Ethereum,
48 Starknet,
49 ZkSync,
50 Arbitrum,
51 Base,
52 Unichain,
53}
54
55impl From<models::contract::Account> for ResponseAccount {
56 fn from(value: models::contract::Account) -> Self {
57 ResponseAccount::new(
58 value.chain.into(),
59 value.address,
60 value.title,
61 value.slots,
62 value.native_balance,
63 value
64 .token_balances
65 .into_iter()
66 .map(|(k, v)| (k, v.balance))
67 .collect(),
68 value.code,
69 value.code_hash,
70 value.balance_modify_tx,
71 value.code_modify_tx,
72 value.creation_tx,
73 )
74 }
75}
76
77impl From<models::Chain> for Chain {
78 fn from(value: models::Chain) -> Self {
79 match value {
80 models::Chain::Ethereum => Chain::Ethereum,
81 models::Chain::Starknet => Chain::Starknet,
82 models::Chain::ZkSync => Chain::ZkSync,
83 models::Chain::Arbitrum => Chain::Arbitrum,
84 models::Chain::Base => Chain::Base,
85 models::Chain::Unichain => Chain::Unichain,
86 }
87 }
88}
89
90#[derive(
91 Debug, PartialEq, Default, Copy, Clone, Deserialize, Serialize, ToSchema, EnumString, Display,
92)]
93pub enum ChangeType {
94 #[default]
95 Update,
96 Deletion,
97 Creation,
98 Unspecified,
99}
100
101impl From<models::ChangeType> for ChangeType {
102 fn from(value: models::ChangeType) -> Self {
103 match value {
104 models::ChangeType::Update => ChangeType::Update,
105 models::ChangeType::Creation => ChangeType::Creation,
106 models::ChangeType::Deletion => ChangeType::Deletion,
107 }
108 }
109}
110
111impl ChangeType {
112 pub fn merge(&self, other: &Self) -> Self {
113 if matches!(self, Self::Creation) {
114 Self::Creation
115 } else {
116 *other
117 }
118 }
119}
120
121#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, Hash, Default)]
122pub struct ExtractorIdentity {
123 pub chain: Chain,
124 pub name: String,
125}
126
127impl ExtractorIdentity {
128 pub fn new(chain: Chain, name: &str) -> Self {
129 Self { chain, name: name.to_owned() }
130 }
131}
132
133impl fmt::Display for ExtractorIdentity {
134 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
135 write!(f, "{}:{}", self.chain, self.name)
136 }
137}
138
139#[derive(Deserialize, Serialize, Debug, PartialEq, Eq)]
141#[serde(tag = "method", rename_all = "lowercase")]
142pub enum Command {
143 Subscribe { extractor_id: ExtractorIdentity, include_state: bool },
144 Unsubscribe { subscription_id: Uuid },
145}
146
147#[derive(Deserialize, Serialize, Debug, PartialEq, Eq)]
149#[serde(tag = "method", rename_all = "lowercase")]
150pub enum Response {
151 NewSubscription { extractor_id: ExtractorIdentity, subscription_id: Uuid },
152 SubscriptionEnded { subscription_id: Uuid },
153}
154
155#[allow(clippy::large_enum_variant)]
157#[derive(Serialize, Deserialize, Debug)]
158#[serde(untagged)]
159pub enum WebSocketMessage {
160 BlockChanges { subscription_id: Uuid, deltas: BlockChanges },
161 Response(Response),
162}
163
164#[derive(Debug, PartialEq, Clone, Deserialize, Serialize, Default, ToSchema)]
165pub struct Block {
166 pub number: u64,
167 #[serde(with = "hex_bytes")]
168 pub hash: Bytes,
169 #[serde(with = "hex_bytes")]
170 pub parent_hash: Bytes,
171 pub chain: Chain,
172 pub ts: NaiveDateTime,
173}
174
175#[derive(Debug, Serialize, Deserialize, PartialEq, Clone, ToSchema, Eq, Hash)]
176#[serde(deny_unknown_fields)]
177pub struct BlockParam {
178 #[schema(value_type=Option<String>)]
179 #[serde(with = "hex_bytes_option", default)]
180 pub hash: Option<Bytes>,
181 #[deprecated(
182 note = "The `chain` field is deprecated and will be removed in a future version."
183 )]
184 #[serde(default)]
185 pub chain: Option<Chain>,
186 #[serde(default)]
187 pub number: Option<i64>,
188}
189
190impl From<&Block> for BlockParam {
191 fn from(value: &Block) -> Self {
192 BlockParam { hash: Some(value.hash.clone()), chain: None, number: None }
194 }
195}
196
197#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, Default)]
198pub struct TokenBalances(#[serde(with = "hex_hashmap_key")] pub HashMap<Bytes, ComponentBalance>);
199
200impl From<HashMap<Bytes, ComponentBalance>> for TokenBalances {
201 fn from(value: HashMap<Bytes, ComponentBalance>) -> Self {
202 TokenBalances(value)
203 }
204}
205
206#[derive(Debug, PartialEq, Clone, Default, Deserialize, Serialize)]
207pub struct Transaction {
208 #[serde(with = "hex_bytes")]
209 pub hash: Bytes,
210 #[serde(with = "hex_bytes")]
211 pub block_hash: Bytes,
212 #[serde(with = "hex_bytes")]
213 pub from: Bytes,
214 #[serde(with = "hex_bytes_option")]
215 pub to: Option<Bytes>,
216 pub index: u64,
217}
218
219impl Transaction {
220 pub fn new(hash: Bytes, block_hash: Bytes, from: Bytes, to: Option<Bytes>, index: u64) -> Self {
221 Self { hash, block_hash, from, to, index }
222 }
223}
224
225#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, Default)]
227pub struct BlockChanges {
228 pub extractor: String,
229 pub chain: Chain,
230 pub block: Block,
231 pub finalized_block_height: u64,
232 pub revert: bool,
233 #[serde(with = "hex_hashmap_key", default)]
234 pub new_tokens: HashMap<Bytes, ResponseToken>,
235 #[serde(alias = "account_deltas", with = "hex_hashmap_key")]
236 pub account_updates: HashMap<Bytes, AccountUpdate>,
237 #[serde(alias = "state_deltas")]
238 pub state_updates: HashMap<String, ProtocolStateDelta>,
239 pub new_protocol_components: HashMap<String, ProtocolComponent>,
240 pub deleted_protocol_components: HashMap<String, ProtocolComponent>,
241 pub component_balances: HashMap<String, TokenBalances>,
242 pub account_balances: HashMap<Bytes, HashMap<Bytes, AccountBalance>>,
243 pub component_tvl: HashMap<String, f64>,
244}
245
246impl BlockChanges {
247 #[allow(clippy::too_many_arguments)]
248 pub fn new(
249 extractor: &str,
250 chain: Chain,
251 block: Block,
252 finalized_block_height: u64,
253 revert: bool,
254 account_updates: HashMap<Bytes, AccountUpdate>,
255 state_updates: HashMap<String, ProtocolStateDelta>,
256 new_protocol_components: HashMap<String, ProtocolComponent>,
257 deleted_protocol_components: HashMap<String, ProtocolComponent>,
258 component_balances: HashMap<String, HashMap<Bytes, ComponentBalance>>,
259 account_balances: HashMap<Bytes, HashMap<Bytes, AccountBalance>>,
260 ) -> Self {
261 BlockChanges {
262 extractor: extractor.to_owned(),
263 chain,
264 block,
265 finalized_block_height,
266 revert,
267 new_tokens: HashMap::new(),
268 account_updates,
269 state_updates,
270 new_protocol_components,
271 deleted_protocol_components,
272 component_balances: component_balances
273 .into_iter()
274 .map(|(k, v)| (k, v.into()))
275 .collect(),
276 account_balances,
277 component_tvl: HashMap::new(),
278 }
279 }
280
281 pub fn merge(mut self, other: Self) -> Self {
282 other
283 .account_updates
284 .into_iter()
285 .for_each(|(k, v)| {
286 self.account_updates
287 .entry(k)
288 .and_modify(|e| {
289 e.merge(&v);
290 })
291 .or_insert(v);
292 });
293
294 other
295 .state_updates
296 .into_iter()
297 .for_each(|(k, v)| {
298 self.state_updates
299 .entry(k)
300 .and_modify(|e| {
301 e.merge(&v);
302 })
303 .or_insert(v);
304 });
305
306 other
307 .component_balances
308 .into_iter()
309 .for_each(|(k, v)| {
310 self.component_balances
311 .entry(k)
312 .and_modify(|e| e.0.extend(v.0.clone()))
313 .or_insert_with(|| v);
314 });
315
316 other
317 .account_balances
318 .into_iter()
319 .for_each(|(k, v)| {
320 self.account_balances
321 .entry(k)
322 .and_modify(|e| e.extend(v.clone()))
323 .or_insert(v);
324 });
325
326 self.component_tvl
327 .extend(other.component_tvl);
328 self.new_protocol_components
329 .extend(other.new_protocol_components);
330 self.deleted_protocol_components
331 .extend(other.deleted_protocol_components);
332 self.revert = other.revert;
333 self.block = other.block;
334
335 self
336 }
337
338 pub fn get_block(&self) -> &Block {
339 &self.block
340 }
341
342 pub fn is_revert(&self) -> bool {
343 self.revert
344 }
345
346 pub fn filter_by_component<F: Fn(&str) -> bool>(&mut self, keep: F) {
347 self.state_updates
348 .retain(|k, _| keep(k));
349 self.component_balances
350 .retain(|k, _| keep(k));
351 self.component_tvl
352 .retain(|k, _| keep(k));
353 }
354
355 pub fn filter_by_contract<F: Fn(&Bytes) -> bool>(&mut self, keep: F) {
356 self.account_updates
357 .retain(|k, _| keep(k));
358 self.account_balances
359 .retain(|k, _| keep(k));
360 }
361
362 pub fn n_changes(&self) -> usize {
363 self.account_updates.len() + self.state_updates.len()
364 }
365}
366
367#[derive(PartialEq, Serialize, Deserialize, Clone, Debug, ToSchema)]
368pub struct AccountUpdate {
369 #[serde(with = "hex_bytes")]
370 #[schema(value_type=Vec<String>)]
371 pub address: Bytes,
372 pub chain: Chain,
373 #[serde(with = "hex_hashmap_key_value")]
374 #[schema(value_type=HashMap<String, String>)]
375 pub slots: HashMap<Bytes, Bytes>,
376 #[serde(with = "hex_bytes_option")]
377 #[schema(value_type=Option<String>)]
378 pub balance: Option<Bytes>,
379 #[serde(with = "hex_bytes_option")]
380 #[schema(value_type=Option<String>)]
381 pub code: Option<Bytes>,
382 pub change: ChangeType,
383}
384
385impl AccountUpdate {
386 pub fn new(
387 address: Bytes,
388 chain: Chain,
389 slots: HashMap<Bytes, Bytes>,
390 balance: Option<Bytes>,
391 code: Option<Bytes>,
392 change: ChangeType,
393 ) -> Self {
394 Self { address, chain, slots, balance, code, change }
395 }
396
397 pub fn merge(&mut self, other: &Self) {
398 self.slots.extend(
399 other
400 .slots
401 .iter()
402 .map(|(k, v)| (k.clone(), v.clone())),
403 );
404 self.balance.clone_from(&other.balance);
405 self.code.clone_from(&other.code);
406 self.change = self.change.merge(&other.change);
407 }
408}
409
410impl From<models::contract::AccountDelta> for AccountUpdate {
411 fn from(value: models::contract::AccountDelta) -> Self {
412 AccountUpdate::new(
413 value.address,
414 value.chain.into(),
415 value
416 .slots
417 .into_iter()
418 .map(|(k, v)| (k, v.unwrap_or_default()))
419 .collect(),
420 value.balance,
421 value.code,
422 value.change.into(),
423 )
424 }
425}
426
427#[derive(Debug, Clone, PartialEq, Default, Deserialize, Serialize, ToSchema)]
429pub struct ProtocolComponent {
430 pub id: String,
432 pub protocol_system: String,
434 pub protocol_type_name: String,
436 pub chain: Chain,
437 #[schema(value_type=Vec<String>)]
439 pub tokens: Vec<Bytes>,
440 #[serde(alias = "contract_addresses")]
443 #[schema(value_type=Vec<String>)]
444 pub contract_ids: Vec<Bytes>,
445 #[serde(with = "hex_hashmap_value")]
447 #[schema(value_type=HashMap<String, String>)]
448 pub static_attributes: HashMap<String, Bytes>,
449 #[serde(default)]
451 pub change: ChangeType,
452 #[serde(with = "hex_bytes")]
454 #[schema(value_type=String)]
455 pub creation_tx: Bytes,
456 pub created_at: NaiveDateTime,
458}
459
460impl From<models::protocol::ProtocolComponent> for ProtocolComponent {
461 fn from(value: models::protocol::ProtocolComponent) -> Self {
462 Self {
463 id: value.id,
464 protocol_system: value.protocol_system,
465 protocol_type_name: value.protocol_type_name,
466 chain: value.chain.into(),
467 tokens: value.tokens,
468 contract_ids: value.contract_addresses,
469 static_attributes: value.static_attributes,
470 change: value.change.into(),
471 creation_tx: value.creation_tx,
472 created_at: value.created_at,
473 }
474 }
475}
476
477#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, Default)]
478pub struct ComponentBalance {
479 #[serde(with = "hex_bytes")]
480 pub token: Bytes,
481 pub balance: Bytes,
482 pub balance_float: f64,
483 #[serde(with = "hex_bytes")]
484 pub modify_tx: Bytes,
485 pub component_id: String,
486}
487
488#[derive(Debug, PartialEq, Clone, Default, Serialize, Deserialize, ToSchema)]
489pub struct ProtocolStateDelta {
491 pub component_id: String,
492 #[schema(value_type=HashMap<String, String>)]
493 pub updated_attributes: HashMap<String, Bytes>,
494 pub deleted_attributes: HashSet<String>,
495}
496
497impl From<models::protocol::ProtocolComponentStateDelta> for ProtocolStateDelta {
498 fn from(value: models::protocol::ProtocolComponentStateDelta) -> Self {
499 Self {
500 component_id: value.component_id,
501 updated_attributes: value.updated_attributes,
502 deleted_attributes: value.deleted_attributes,
503 }
504 }
505}
506
507impl ProtocolStateDelta {
508 pub fn merge(&mut self, other: &Self) {
527 self.updated_attributes
529 .retain(|k, _| !other.deleted_attributes.contains(k));
530
531 self.deleted_attributes.retain(|attr| {
533 !other
534 .updated_attributes
535 .contains_key(attr)
536 });
537
538 self.updated_attributes.extend(
540 other
541 .updated_attributes
542 .iter()
543 .map(|(k, v)| (k.clone(), v.clone())),
544 );
545
546 self.deleted_attributes
548 .extend(other.deleted_attributes.iter().cloned());
549 }
550}
551
552#[derive(Clone, Serialize, Debug, Default, Deserialize, PartialEq, ToSchema, Eq, Hash)]
554#[serde(deny_unknown_fields)]
555pub struct StateRequestBody {
556 #[serde(alias = "contractIds")]
558 #[schema(value_type=Option<Vec<String>>)]
559 pub contract_ids: Option<Vec<Bytes>>,
560 #[serde(alias = "protocolSystem", default)]
563 pub protocol_system: String,
564 #[serde(default = "VersionParam::default")]
565 pub version: VersionParam,
566 #[serde(default)]
567 pub chain: Chain,
568 #[serde(default)]
569 pub pagination: PaginationParams,
570}
571
572impl StateRequestBody {
573 pub fn new(
574 contract_ids: Option<Vec<Bytes>>,
575 protocol_system: String,
576 version: VersionParam,
577 chain: Chain,
578 pagination: PaginationParams,
579 ) -> Self {
580 Self { contract_ids, protocol_system, version, chain, pagination }
581 }
582
583 pub fn from_block(protocol_system: &str, block: BlockParam) -> Self {
584 Self {
585 contract_ids: None,
586 protocol_system: protocol_system.to_string(),
587 version: VersionParam { timestamp: None, block: Some(block.clone()) },
588 chain: block.chain.unwrap_or_default(),
589 pagination: PaginationParams::default(),
590 }
591 }
592
593 pub fn from_timestamp(protocol_system: &str, timestamp: NaiveDateTime, chain: Chain) -> Self {
594 Self {
595 contract_ids: None,
596 protocol_system: protocol_system.to_string(),
597 version: VersionParam { timestamp: Some(timestamp), block: None },
598 chain,
599 pagination: PaginationParams::default(),
600 }
601 }
602}
603
604#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, ToSchema)]
606pub struct StateRequestResponse {
607 pub accounts: Vec<ResponseAccount>,
608 pub pagination: PaginationResponse,
609}
610
611impl StateRequestResponse {
612 pub fn new(accounts: Vec<ResponseAccount>, pagination: PaginationResponse) -> Self {
613 Self { accounts, pagination }
614 }
615}
616
617#[derive(PartialEq, Clone, Serialize, Deserialize, Default, ToSchema)]
618#[serde(rename = "Account")]
619pub struct ResponseAccount {
623 pub chain: Chain,
624 #[schema(value_type=String, example="0xc9f2e6ea1637E499406986ac50ddC92401ce1f58")]
626 #[serde(with = "hex_bytes")]
627 pub address: Bytes,
628 #[schema(value_type=String, example="Protocol Vault")]
630 pub title: String,
631 #[schema(value_type=HashMap<String, String>, example=json!({"0x....": "0x...."}))]
633 #[serde(with = "hex_hashmap_key_value")]
634 pub slots: HashMap<Bytes, Bytes>,
635 #[schema(value_type=String, example="0x00")]
637 #[serde(with = "hex_bytes")]
638 pub native_balance: Bytes,
639 #[schema(value_type=HashMap<String, String>, example=json!({"0x....": "0x...."}))]
642 #[serde(with = "hex_hashmap_key_value")]
643 pub token_balances: HashMap<Bytes, Bytes>,
644 #[schema(value_type=String, example="0xBADBABE")]
646 #[serde(with = "hex_bytes")]
647 pub code: Bytes,
648 #[schema(value_type=String, example="0x123456789")]
650 #[serde(with = "hex_bytes")]
651 pub code_hash: Bytes,
652 #[schema(value_type=String, example="0x8f1133bfb054a23aedfe5d25b1d81b96195396d8b88bd5d4bcf865fc1ae2c3f4")]
654 #[serde(with = "hex_bytes")]
655 pub balance_modify_tx: Bytes,
656 #[schema(value_type=String, example="0x8f1133bfb054a23aedfe5d25b1d81b96195396d8b88bd5d4bcf865fc1ae2c3f4")]
658 #[serde(with = "hex_bytes")]
659 pub code_modify_tx: Bytes,
660 #[schema(value_type=Option<String>, example="0x8f1133bfb054a23aedfe5d25b1d81b96195396d8b88bd5d4bcf865fc1ae2c3f4")]
662 #[serde(with = "hex_bytes_option")]
663 pub creation_tx: Option<Bytes>,
664}
665
666impl ResponseAccount {
667 #[allow(clippy::too_many_arguments)]
668 pub fn new(
669 chain: Chain,
670 address: Bytes,
671 title: String,
672 slots: HashMap<Bytes, Bytes>,
673 native_balance: Bytes,
674 token_balances: HashMap<Bytes, Bytes>,
675 code: Bytes,
676 code_hash: Bytes,
677 balance_modify_tx: Bytes,
678 code_modify_tx: Bytes,
679 creation_tx: Option<Bytes>,
680 ) -> Self {
681 Self {
682 chain,
683 address,
684 title,
685 slots,
686 native_balance,
687 token_balances,
688 code,
689 code_hash,
690 balance_modify_tx,
691 code_modify_tx,
692 creation_tx,
693 }
694 }
695}
696
697impl fmt::Debug for ResponseAccount {
699 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
700 f.debug_struct("ResponseAccount")
701 .field("chain", &self.chain)
702 .field("address", &self.address)
703 .field("title", &self.title)
704 .field("slots", &self.slots)
705 .field("native_balance", &self.native_balance)
706 .field("token_balances", &self.token_balances)
707 .field("code", &format!("[{} bytes]", self.code.len()))
708 .field("code_hash", &self.code_hash)
709 .field("balance_modify_tx", &self.balance_modify_tx)
710 .field("code_modify_tx", &self.code_modify_tx)
711 .field("creation_tx", &self.creation_tx)
712 .finish()
713 }
714}
715
716#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, Default)]
717pub struct AccountBalance {
718 #[serde(with = "hex_bytes")]
719 pub account: Bytes,
720 #[serde(with = "hex_bytes")]
721 pub token: Bytes,
722 #[serde(with = "hex_bytes")]
723 pub balance: Bytes,
724 #[serde(with = "hex_bytes")]
725 pub modify_tx: Bytes,
726}
727
728#[derive(Debug, Serialize, Deserialize, PartialEq, Clone, ToSchema)]
729#[serde(deny_unknown_fields)]
730pub struct ContractId {
731 #[serde(with = "hex_bytes")]
732 #[schema(value_type=String)]
733 pub address: Bytes,
734 pub chain: Chain,
735}
736
737impl ContractId {
739 pub fn new(chain: Chain, address: Bytes) -> Self {
740 Self { address, chain }
741 }
742
743 pub fn address(&self) -> &Bytes {
744 &self.address
745 }
746}
747
748impl fmt::Display for ContractId {
749 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
750 write!(f, "{:?}: 0x{}", self.chain, hex::encode(&self.address))
751 }
752}
753
754#[derive(Debug, Serialize, Deserialize, PartialEq, Clone, ToSchema, Eq, Hash)]
761#[serde(deny_unknown_fields)]
762pub struct VersionParam {
763 pub timestamp: Option<NaiveDateTime>,
764 pub block: Option<BlockParam>,
765}
766
767impl VersionParam {
768 pub fn new(timestamp: Option<NaiveDateTime>, block: Option<BlockParam>) -> Self {
769 Self { timestamp, block }
770 }
771}
772
773impl Default for VersionParam {
774 fn default() -> Self {
775 VersionParam { timestamp: Some(Utc::now().naive_utc()), block: None }
776 }
777}
778
779#[deprecated(note = "Use StateRequestBody instead")]
780#[derive(Serialize, Deserialize, Default, Debug, IntoParams)]
781pub struct StateRequestParameters {
782 #[param(default = 0)]
784 pub tvl_gt: Option<u64>,
785 #[param(default = 0)]
787 pub inertia_min_gt: Option<u64>,
788 #[serde(default = "default_include_balances_flag")]
790 pub include_balances: bool,
791 #[serde(default)]
792 pub pagination: PaginationParams,
793}
794
795impl StateRequestParameters {
796 pub fn new(include_balances: bool) -> Self {
797 Self {
798 tvl_gt: None,
799 inertia_min_gt: None,
800 include_balances,
801 pagination: PaginationParams::default(),
802 }
803 }
804
805 pub fn to_query_string(&self) -> String {
806 let mut parts = vec![format!("include_balances={}", self.include_balances)];
807
808 if let Some(tvl_gt) = self.tvl_gt {
809 parts.push(format!("tvl_gt={}", tvl_gt));
810 }
811
812 if let Some(inertia) = self.inertia_min_gt {
813 parts.push(format!("inertia_min_gt={}", inertia));
814 }
815
816 let mut res = parts.join("&");
817 if !res.is_empty() {
818 res = format!("?{res}");
819 }
820 res
821 }
822}
823
824#[derive(Serialize, Deserialize, Debug, Default, PartialEq, ToSchema, Eq, Hash, Clone)]
825#[serde(deny_unknown_fields)]
826pub struct TokensRequestBody {
827 #[serde(alias = "tokenAddresses")]
829 #[schema(value_type=Option<Vec<String>>)]
830 pub token_addresses: Option<Vec<Bytes>>,
831 #[serde(default)]
839 pub min_quality: Option<i32>,
840 #[serde(default)]
842 pub traded_n_days_ago: Option<u64>,
843 #[serde(default)]
845 pub pagination: PaginationParams,
846 #[serde(default)]
848 pub chain: Chain,
849}
850
851#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, ToSchema, Eq, Hash)]
853pub struct TokensRequestResponse {
854 pub tokens: Vec<ResponseToken>,
855 pub pagination: PaginationResponse,
856}
857
858impl TokensRequestResponse {
859 pub fn new(tokens: Vec<ResponseToken>, pagination_request: &PaginationResponse) -> Self {
860 Self { tokens, pagination: pagination_request.clone() }
861 }
862}
863
864#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, ToSchema, Eq, Hash)]
866#[serde(deny_unknown_fields)]
867pub struct PaginationParams {
868 #[serde(default)]
870 pub page: i64,
871 #[serde(default)]
873 pub page_size: i64,
874}
875
876impl PaginationParams {
877 pub fn new(page: i64, page_size: i64) -> Self {
878 Self { page, page_size }
879 }
880}
881
882impl Default for PaginationParams {
883 fn default() -> Self {
884 PaginationParams { page: 0, page_size: 20 }
885 }
886}
887
888#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, ToSchema, Eq, Hash)]
889#[serde(deny_unknown_fields)]
890pub struct PaginationResponse {
891 pub page: i64,
892 pub page_size: i64,
893 pub total: i64,
895}
896
897impl PaginationResponse {
899 pub fn new(page: i64, page_size: i64, total: i64) -> Self {
900 Self { page, page_size, total }
901 }
902
903 pub fn total_pages(&self) -> i64 {
904 (self.total + self.page_size - 1) / self.page_size
906 }
907}
908
909#[derive(PartialEq, Debug, Clone, Serialize, Deserialize, Default, ToSchema, Eq, Hash)]
910#[serde(rename = "Token")]
911pub struct ResponseToken {
913 pub chain: Chain,
914 #[schema(value_type=String, example="0xc9f2e6ea1637E499406986ac50ddC92401ce1f58")]
916 #[serde(with = "hex_bytes")]
917 pub address: Bytes,
918 #[schema(value_type=String, example="WETH")]
920 pub symbol: String,
921 pub decimals: u32,
923 pub tax: u64,
925 pub gas: Vec<Option<u64>>,
927 pub quality: u32,
935}
936
937impl From<models::token::CurrencyToken> for ResponseToken {
938 fn from(value: models::token::CurrencyToken) -> Self {
939 Self {
940 chain: value.chain.into(),
941 address: value.address,
942 symbol: value.symbol,
943 decimals: value.decimals,
944 tax: value.tax,
945 gas: value.gas,
946 quality: value.quality,
947 }
948 }
949}
950
951#[derive(Serialize, Deserialize, Debug, Default, ToSchema, Clone)]
952#[serde(deny_unknown_fields)]
953pub struct ProtocolComponentsRequestBody {
954 pub protocol_system: String,
957 #[serde(alias = "componentAddresses")]
959 pub component_ids: Option<Vec<String>>,
960 #[serde(default)]
963 pub tvl_gt: Option<f64>,
964 #[serde(default)]
965 pub chain: Chain,
966 #[serde(default)]
968 pub pagination: PaginationParams,
969}
970
971impl PartialEq for ProtocolComponentsRequestBody {
973 fn eq(&self, other: &Self) -> bool {
974 let tvl_close_enough = match (self.tvl_gt, other.tvl_gt) {
975 (Some(a), Some(b)) => (a - b).abs() < 1e-6,
976 (None, None) => true,
977 _ => false,
978 };
979
980 self.protocol_system == other.protocol_system &&
981 self.component_ids == other.component_ids &&
982 tvl_close_enough &&
983 self.chain == other.chain &&
984 self.pagination == other.pagination
985 }
986}
987
988impl Eq for ProtocolComponentsRequestBody {}
990
991impl Hash for ProtocolComponentsRequestBody {
992 fn hash<H: Hasher>(&self, state: &mut H) {
993 self.protocol_system.hash(state);
994 self.component_ids.hash(state);
995
996 if let Some(tvl) = self.tvl_gt {
998 tvl.to_bits().hash(state);
1000 } else {
1001 state.write_u8(0);
1003 }
1004
1005 self.chain.hash(state);
1006 self.pagination.hash(state);
1007 }
1008}
1009
1010impl ProtocolComponentsRequestBody {
1011 pub fn system_filtered(system: &str, tvl_gt: Option<f64>, chain: Chain) -> Self {
1012 Self {
1013 protocol_system: system.to_string(),
1014 component_ids: None,
1015 tvl_gt,
1016 chain,
1017 pagination: Default::default(),
1018 }
1019 }
1020
1021 pub fn id_filtered(system: &str, ids: Vec<String>, chain: Chain) -> Self {
1022 Self {
1023 protocol_system: system.to_string(),
1024 component_ids: Some(ids),
1025 tvl_gt: None,
1026 chain,
1027 pagination: Default::default(),
1028 }
1029 }
1030}
1031
1032impl ProtocolComponentsRequestBody {
1033 pub fn new(
1034 protocol_system: String,
1035 component_ids: Option<Vec<String>>,
1036 tvl_gt: Option<f64>,
1037 chain: Chain,
1038 pagination: PaginationParams,
1039 ) -> Self {
1040 Self { protocol_system, component_ids, tvl_gt, chain, pagination }
1041 }
1042}
1043
1044#[deprecated(note = "Use ProtocolComponentsRequestBody instead")]
1045#[derive(Serialize, Deserialize, Default, Debug, IntoParams)]
1046pub struct ProtocolComponentRequestParameters {
1047 #[param(default = 0)]
1049 pub tvl_gt: Option<f64>,
1050}
1051
1052impl ProtocolComponentRequestParameters {
1053 pub fn tvl_filtered(min_tvl: f64) -> Self {
1054 Self { tvl_gt: Some(min_tvl) }
1055 }
1056}
1057
1058impl ProtocolComponentRequestParameters {
1059 pub fn to_query_string(&self) -> String {
1060 if let Some(tvl_gt) = self.tvl_gt {
1061 return format!("?tvl_gt={}", tvl_gt);
1062 }
1063 String::new()
1064 }
1065}
1066
1067#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, ToSchema)]
1069pub struct ProtocolComponentRequestResponse {
1070 pub protocol_components: Vec<ProtocolComponent>,
1071 pub pagination: PaginationResponse,
1072}
1073
1074impl ProtocolComponentRequestResponse {
1075 pub fn new(
1076 protocol_components: Vec<ProtocolComponent>,
1077 pagination: PaginationResponse,
1078 ) -> Self {
1079 Self { protocol_components, pagination }
1080 }
1081}
1082
1083#[derive(Debug, Serialize, Deserialize, PartialEq, Clone, ToSchema, Eq, Hash)]
1084#[serde(deny_unknown_fields)]
1085#[deprecated]
1086pub struct ProtocolId {
1087 pub id: String,
1088 pub chain: Chain,
1089}
1090
1091impl From<ProtocolId> for String {
1092 fn from(protocol_id: ProtocolId) -> Self {
1093 protocol_id.id
1094 }
1095}
1096
1097impl AsRef<str> for ProtocolId {
1098 fn as_ref(&self) -> &str {
1099 &self.id
1100 }
1101}
1102
1103#[derive(Debug, Clone, PartialEq, Default, Deserialize, Serialize, ToSchema)]
1105pub struct ResponseProtocolState {
1106 pub component_id: String,
1108 #[schema(value_type=HashMap<String, String>)]
1111 #[serde(with = "hex_hashmap_value")]
1112 pub attributes: HashMap<String, Bytes>,
1113 #[schema(value_type=HashMap<String, String>)]
1115 #[serde(with = "hex_hashmap_key_value")]
1116 pub balances: HashMap<Bytes, Bytes>,
1117}
1118
1119impl From<models::protocol::ProtocolComponentState> for ResponseProtocolState {
1120 fn from(value: models::protocol::ProtocolComponentState) -> Self {
1121 Self {
1122 component_id: value.component_id,
1123 attributes: value.attributes,
1124 balances: value.balances,
1125 }
1126 }
1127}
1128
1129fn default_include_balances_flag() -> bool {
1130 true
1131}
1132
1133#[derive(Clone, Debug, Serialize, PartialEq, ToSchema, Default, Eq, Hash)]
1135#[serde(deny_unknown_fields)]
1136pub struct ProtocolStateRequestBody {
1137 #[serde(alias = "protocolIds")]
1139 pub protocol_ids: Option<Vec<String>>,
1140 #[serde(alias = "protocolSystem")]
1143 pub protocol_system: String,
1144 #[serde(default)]
1145 pub chain: Chain,
1146 #[serde(default = "default_include_balances_flag")]
1148 pub include_balances: bool,
1149 #[serde(default = "VersionParam::default")]
1150 pub version: VersionParam,
1151 #[serde(default)]
1152 pub pagination: PaginationParams,
1153}
1154
1155impl ProtocolStateRequestBody {
1156 pub fn id_filtered<I, T>(ids: I) -> Self
1157 where
1158 I: IntoIterator<Item = T>,
1159 T: Into<String>,
1160 {
1161 Self {
1162 protocol_ids: Some(
1163 ids.into_iter()
1164 .map(Into::into)
1165 .collect(),
1166 ),
1167 ..Default::default()
1168 }
1169 }
1170}
1171
1172impl<'de> Deserialize<'de> for ProtocolStateRequestBody {
1176 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
1177 where
1178 D: Deserializer<'de>,
1179 {
1180 #[derive(Deserialize)]
1181 #[serde(untagged)]
1182 enum ProtocolIdOrString {
1183 Old(Vec<ProtocolId>),
1184 New(Vec<String>),
1185 }
1186
1187 struct ProtocolStateRequestBodyVisitor;
1188
1189 impl<'de> de::Visitor<'de> for ProtocolStateRequestBodyVisitor {
1190 type Value = ProtocolStateRequestBody;
1191
1192 fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
1193 formatter.write_str("struct ProtocolStateRequestBody")
1194 }
1195
1196 fn visit_map<V>(self, mut map: V) -> Result<ProtocolStateRequestBody, V::Error>
1197 where
1198 V: de::MapAccess<'de>,
1199 {
1200 let mut protocol_ids = None;
1201 let mut protocol_system = None;
1202 let mut version = None;
1203 let mut chain = None;
1204 let mut include_balances = None;
1205 let mut pagination = None;
1206
1207 while let Some(key) = map.next_key::<String>()? {
1208 match key.as_str() {
1209 "protocol_ids" | "protocolIds" => {
1210 let value: ProtocolIdOrString = map.next_value()?;
1211 protocol_ids = match value {
1212 ProtocolIdOrString::Old(ids) => {
1213 Some(ids.into_iter().map(|p| p.id).collect())
1214 }
1215 ProtocolIdOrString::New(ids_str) => Some(ids_str),
1216 };
1217 }
1218 "protocol_system" | "protocolSystem" => {
1219 protocol_system = Some(map.next_value()?);
1220 }
1221 "version" => {
1222 version = Some(map.next_value()?);
1223 }
1224 "chain" => {
1225 chain = Some(map.next_value()?);
1226 }
1227 "include_balances" => {
1228 include_balances = Some(map.next_value()?);
1229 }
1230 "pagination" => {
1231 pagination = Some(map.next_value()?);
1232 }
1233 _ => {
1234 return Err(de::Error::unknown_field(
1235 &key,
1236 &[
1237 "contract_ids",
1238 "protocol_system",
1239 "version",
1240 "chain",
1241 "include_balances",
1242 "pagination",
1243 ],
1244 ))
1245 }
1246 }
1247 }
1248
1249 Ok(ProtocolStateRequestBody {
1250 protocol_ids,
1251 protocol_system: protocol_system.unwrap_or_default(),
1252 version: version.unwrap_or_else(VersionParam::default),
1253 chain: chain.unwrap_or_else(Chain::default),
1254 include_balances: include_balances.unwrap_or(true),
1255 pagination: pagination.unwrap_or_else(PaginationParams::default),
1256 })
1257 }
1258 }
1259
1260 deserializer.deserialize_struct(
1261 "ProtocolStateRequestBody",
1262 &[
1263 "contract_ids",
1264 "protocol_system",
1265 "version",
1266 "chain",
1267 "include_balances",
1268 "pagination",
1269 ],
1270 ProtocolStateRequestBodyVisitor,
1271 )
1272 }
1273}
1274
1275#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, ToSchema)]
1276pub struct ProtocolStateRequestResponse {
1277 pub states: Vec<ResponseProtocolState>,
1278 pub pagination: PaginationResponse,
1279}
1280
1281impl ProtocolStateRequestResponse {
1282 pub fn new(states: Vec<ResponseProtocolState>, pagination: PaginationResponse) -> Self {
1283 Self { states, pagination }
1284 }
1285}
1286
1287#[derive(Clone, PartialEq, Hash, Eq)]
1288pub struct ProtocolComponentId {
1289 pub chain: Chain,
1290 pub system: String,
1291 pub id: String,
1292}
1293
1294#[derive(Debug, Serialize, ToSchema)]
1295#[serde(tag = "status", content = "message")]
1296#[schema(example = json!({"status": "NotReady", "message": "No db connection"}))]
1297pub enum Health {
1298 Ready,
1299 Starting(String),
1300 NotReady(String),
1301}
1302
1303#[derive(Serialize, Deserialize, Debug, Default, PartialEq, ToSchema, Eq, Hash, Clone)]
1304#[serde(deny_unknown_fields)]
1305pub struct ProtocolSystemsRequestBody {
1306 #[serde(default)]
1307 pub chain: Chain,
1308 #[serde(default)]
1309 pub pagination: PaginationParams,
1310}
1311
1312#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, ToSchema, Eq, Hash)]
1313pub struct ProtocolSystemsRequestResponse {
1314 pub protocol_systems: Vec<String>,
1316 pub pagination: PaginationResponse,
1317}
1318
1319impl ProtocolSystemsRequestResponse {
1320 pub fn new(protocol_systems: Vec<String>, pagination: PaginationResponse) -> Self {
1321 Self { protocol_systems, pagination }
1322 }
1323}
1324
1325#[cfg(test)]
1326mod test {
1327 use std::str::FromStr;
1328
1329 use maplit::hashmap;
1330 use rstest::rstest;
1331
1332 use super::*;
1333
1334 #[test]
1335 fn test_protocol_components_equality() {
1336 let body1 = ProtocolComponentsRequestBody {
1337 protocol_system: "protocol1".to_string(),
1338 component_ids: Some(vec!["component1".to_string(), "component2".to_string()]),
1339 tvl_gt: Some(1000.0),
1340 chain: Chain::Ethereum,
1341 pagination: PaginationParams::default(),
1342 };
1343
1344 let body2 = ProtocolComponentsRequestBody {
1345 protocol_system: "protocol1".to_string(),
1346 component_ids: Some(vec!["component1".to_string(), "component2".to_string()]),
1347 tvl_gt: Some(1000.0 + 1e-7), chain: Chain::Ethereum,
1349 pagination: PaginationParams::default(),
1350 };
1351
1352 assert_eq!(body1, body2);
1354 }
1355
1356 #[test]
1357 fn test_protocol_components_inequality() {
1358 let body1 = ProtocolComponentsRequestBody {
1359 protocol_system: "protocol1".to_string(),
1360 component_ids: Some(vec!["component1".to_string(), "component2".to_string()]),
1361 tvl_gt: Some(1000.0),
1362 chain: Chain::Ethereum,
1363 pagination: PaginationParams::default(),
1364 };
1365
1366 let body2 = ProtocolComponentsRequestBody {
1367 protocol_system: "protocol1".to_string(),
1368 component_ids: Some(vec!["component1".to_string(), "component2".to_string()]),
1369 tvl_gt: Some(1000.0 + 1e-5), chain: Chain::Ethereum,
1371 pagination: PaginationParams::default(),
1372 };
1373
1374 assert_ne!(body1, body2);
1376 }
1377
1378 #[test]
1379 fn test_parse_state_request() {
1380 let json_str = r#"
1381 {
1382 "contractIds": [
1383 "0xb4eccE46b8D4e4abFd03C9B806276A6735C9c092"
1384 ],
1385 "protocol_system": "uniswap_v2",
1386 "version": {
1387 "timestamp": "2069-01-01T04:20:00",
1388 "block": {
1389 "hash": "0x24101f9cb26cd09425b52da10e8c2f56ede94089a8bbe0f31f1cda5f4daa52c4",
1390 "number": 213,
1391 "chain": "ethereum"
1392 }
1393 }
1394 }
1395 "#;
1396
1397 let result: StateRequestBody = serde_json::from_str(json_str).unwrap();
1398
1399 let contract0 = "b4eccE46b8D4e4abFd03C9B806276A6735C9c092"
1400 .parse()
1401 .unwrap();
1402 let block_hash = "24101f9cb26cd09425b52da10e8c2f56ede94089a8bbe0f31f1cda5f4daa52c4"
1403 .parse()
1404 .unwrap();
1405 let block_number = 213;
1406
1407 let expected_timestamp =
1408 NaiveDateTime::parse_from_str("2069-01-01T04:20:00", "%Y-%m-%dT%H:%M:%S").unwrap();
1409
1410 let expected = StateRequestBody {
1411 contract_ids: Some(vec![contract0]),
1412 protocol_system: "uniswap_v2".to_string(),
1413 version: VersionParam {
1414 timestamp: Some(expected_timestamp),
1415 block: Some(BlockParam {
1416 hash: Some(block_hash),
1417 chain: Some(Chain::Ethereum),
1418 number: Some(block_number),
1419 }),
1420 },
1421 chain: Chain::Ethereum,
1422 pagination: PaginationParams::default(),
1423 };
1424
1425 assert_eq!(result, expected);
1426 }
1427
1428 #[test]
1429 fn test_parse_state_request_dual_interface() {
1430 let json_common = r#"
1431 {
1432 "__CONTRACT_IDS__": [
1433 "0xb4eccE46b8D4e4abFd03C9B806276A6735C9c092"
1434 ],
1435 "version": {
1436 "timestamp": "2069-01-01T04:20:00",
1437 "block": {
1438 "hash": "0x24101f9cb26cd09425b52da10e8c2f56ede94089a8bbe0f31f1cda5f4daa52c4",
1439 "number": 213,
1440 "chain": "ethereum"
1441 }
1442 }
1443 }
1444 "#;
1445
1446 let json_str_snake = json_common.replace("\"__CONTRACT_IDS__\"", "\"contract_ids\"");
1447 let json_str_camel = json_common.replace("\"__CONTRACT_IDS__\"", "\"contractIds\"");
1448
1449 let snake: StateRequestBody = serde_json::from_str(&json_str_snake).unwrap();
1450 let camel: StateRequestBody = serde_json::from_str(&json_str_camel).unwrap();
1451
1452 assert_eq!(snake, camel);
1453 }
1454
1455 #[test]
1456 fn test_parse_state_request_unknown_field() {
1457 let body = r#"
1458 {
1459 "contract_ids_with_typo_error": [
1460 {
1461 "address": "0xb4eccE46b8D4e4abFd03C9B806276A6735C9c092",
1462 "chain": "ethereum"
1463 }
1464 ],
1465 "version": {
1466 "timestamp": "2069-01-01T04:20:00",
1467 "block": {
1468 "hash": "0x24101f9cb26cd09425b52da10e8c2f56ede94089a8bbe0f31f1cda5f4daa52c4",
1469 "parentHash": "0x8d75152454e60413efe758cc424bfd339897062d7e658f302765eb7b50971815",
1470 "number": 213,
1471 "chain": "ethereum"
1472 }
1473 }
1474 }
1475 "#;
1476
1477 let decoded = serde_json::from_str::<StateRequestBody>(body);
1478
1479 assert!(decoded.is_err(), "Expected an error due to unknown field");
1480
1481 if let Err(e) = decoded {
1482 assert!(
1483 e.to_string()
1484 .contains("unknown field `contract_ids_with_typo_error`"),
1485 "Error message does not contain expected unknown field information"
1486 );
1487 }
1488 }
1489
1490 #[test]
1491 fn test_parse_state_request_no_contract_specified() {
1492 let json_str = r#"
1493 {
1494 "protocol_system": "uniswap_v2",
1495 "version": {
1496 "timestamp": "2069-01-01T04:20:00",
1497 "block": {
1498 "hash": "0x24101f9cb26cd09425b52da10e8c2f56ede94089a8bbe0f31f1cda5f4daa52c4",
1499 "number": 213,
1500 "chain": "ethereum"
1501 }
1502 }
1503 }
1504 "#;
1505
1506 let result: StateRequestBody = serde_json::from_str(json_str).unwrap();
1507
1508 let block_hash = "24101f9cb26cd09425b52da10e8c2f56ede94089a8bbe0f31f1cda5f4daa52c4".into();
1509 let block_number = 213;
1510 let expected_timestamp =
1511 NaiveDateTime::parse_from_str("2069-01-01T04:20:00", "%Y-%m-%dT%H:%M:%S").unwrap();
1512
1513 let expected = StateRequestBody {
1514 contract_ids: None,
1515 protocol_system: "uniswap_v2".to_string(),
1516 version: VersionParam {
1517 timestamp: Some(expected_timestamp),
1518 block: Some(BlockParam {
1519 hash: Some(block_hash),
1520 chain: Some(Chain::Ethereum),
1521 number: Some(block_number),
1522 }),
1523 },
1524 chain: Chain::Ethereum,
1525 pagination: PaginationParams { page: 0, page_size: 20 },
1526 };
1527
1528 assert_eq!(result, expected);
1529 }
1530
1531 #[rstest]
1532 #[case(
1533 r#"
1534 {
1535 "protocol_ids": [
1536 {
1537 "id": "0xb4eccE46b8D4e4abFd03C9B806276A6735C9c092",
1538 "chain": "ethereum"
1539 }
1540 ],
1541 "protocol_system": "uniswap_v2",
1542 "include_balances": false,
1543 "version": {
1544 "timestamp": "2069-01-01T04:20:00",
1545 "block": {
1546 "hash": "0x24101f9cb26cd09425b52da10e8c2f56ede94089a8bbe0f31f1cda5f4daa52c4",
1547 "number": 213,
1548 "chain": "ethereum"
1549 }
1550 }
1551 }
1552 "#
1553 )]
1554 #[case(
1555 r#"
1556 {
1557 "protocolIds": [
1558 "0xb4eccE46b8D4e4abFd03C9B806276A6735C9c092"
1559 ],
1560 "protocol_system": "uniswap_v2",
1561 "include_balances": false,
1562 "version": {
1563 "timestamp": "2069-01-01T04:20:00",
1564 "block": {
1565 "hash": "0x24101f9cb26cd09425b52da10e8c2f56ede94089a8bbe0f31f1cda5f4daa52c4",
1566 "number": 213,
1567 "chain": "ethereum"
1568 }
1569 }
1570 }
1571 "#
1572 )]
1573 fn test_parse_protocol_state_request(#[case] json_str: &str) {
1574 let result: ProtocolStateRequestBody = serde_json::from_str(json_str).unwrap();
1575
1576 let block_hash = "24101f9cb26cd09425b52da10e8c2f56ede94089a8bbe0f31f1cda5f4daa52c4"
1577 .parse()
1578 .unwrap();
1579 let block_number = 213;
1580
1581 let expected_timestamp =
1582 NaiveDateTime::parse_from_str("2069-01-01T04:20:00", "%Y-%m-%dT%H:%M:%S").unwrap();
1583
1584 let expected = ProtocolStateRequestBody {
1585 protocol_ids: Some(vec!["0xb4eccE46b8D4e4abFd03C9B806276A6735C9c092".to_string()]),
1586 protocol_system: "uniswap_v2".to_string(),
1587 version: VersionParam {
1588 timestamp: Some(expected_timestamp),
1589 block: Some(BlockParam {
1590 hash: Some(block_hash),
1591 chain: Some(Chain::Ethereum),
1592 number: Some(block_number),
1593 }),
1594 },
1595 chain: Chain::Ethereum,
1596 include_balances: false,
1597 pagination: PaginationParams::default(),
1598 };
1599
1600 assert_eq!(result, expected);
1601 }
1602
1603 #[rstest]
1604 #[case::with_protocol_ids(vec![ProtocolId { id: "id1".to_string(), chain: Chain::Ethereum }, ProtocolId { id: "id2".to_string(), chain: Chain::Ethereum }], vec!["id1".to_string(), "id2".to_string()])]
1605 #[case::with_strings(vec!["id1".to_string(), "id2".to_string()], vec!["id1".to_string(), "id2".to_string()])]
1606 fn test_id_filtered<T>(#[case] input_ids: Vec<T>, #[case] expected_ids: Vec<String>)
1607 where
1608 T: Into<String> + Clone,
1609 {
1610 let request_body = ProtocolStateRequestBody::id_filtered(input_ids);
1611 assert_eq!(request_body.protocol_ids, Some(expected_ids));
1612 }
1613
1614 fn create_models_block_changes() -> crate::models::blockchain::BlockAggregatedChanges {
1615 let base_ts = 1694534400; crate::models::blockchain::BlockAggregatedChanges {
1618 extractor: "native_name".to_string(),
1619 chain: models::Chain::Ethereum,
1620 block: models::blockchain::Block::new(
1621 3,
1622 models::Chain::Ethereum,
1623 Bytes::from_str("0x0000000000000000000000000000000000000000000000000000000000000003").unwrap(),
1624 Bytes::from_str("0x0000000000000000000000000000000000000000000000000000000000000002").unwrap(),
1625 NaiveDateTime::from_timestamp_opt(base_ts + 3000, 0).unwrap(),
1626 ),
1627 finalized_block_height: 1,
1628 revert: true,
1629 state_deltas: HashMap::from([
1630 ("pc_1".to_string(), models::protocol::ProtocolComponentStateDelta {
1631 component_id: "pc_1".to_string(),
1632 updated_attributes: HashMap::from([
1633 ("attr_2".to_string(), Bytes::from("0x0000000000000002")),
1634 ("attr_1".to_string(), Bytes::from("0x00000000000003e8")),
1635 ]),
1636 deleted_attributes: HashSet::new(),
1637 }),
1638 ]),
1639 new_tokens: HashMap::new(),
1640 new_protocol_components: HashMap::from([
1641 ("pc_2".to_string(), crate::models::protocol::ProtocolComponent {
1642 id: "pc_2".to_string(),
1643 protocol_system: "native_protocol_system".to_string(),
1644 protocol_type_name: "pt_1".to_string(),
1645 chain: models::Chain::Ethereum,
1646 tokens: vec![
1647 Bytes::from_str("0xdac17f958d2ee523a2206206994597c13d831ec7").unwrap(),
1648 Bytes::from_str("0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48").unwrap(),
1649 ],
1650 contract_addresses: vec![],
1651 static_attributes: HashMap::new(),
1652 change: models::ChangeType::Creation,
1653 creation_tx: Bytes::from_str("0x000000000000000000000000000000000000000000000000000000000000c351").unwrap(),
1654 created_at: NaiveDateTime::from_timestamp_opt(base_ts + 5000, 0).unwrap(),
1655 }),
1656 ]),
1657 deleted_protocol_components: HashMap::from([
1658 ("pc_3".to_string(), crate::models::protocol::ProtocolComponent {
1659 id: "pc_3".to_string(),
1660 protocol_system: "native_protocol_system".to_string(),
1661 protocol_type_name: "pt_2".to_string(),
1662 chain: models::Chain::Ethereum,
1663 tokens: vec![
1664 Bytes::from_str("0x6b175474e89094c44da98b954eedeac495271d0f").unwrap(),
1665 Bytes::from_str("0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2").unwrap(),
1666 ],
1667 contract_addresses: vec![],
1668 static_attributes: HashMap::new(),
1669 change: models::ChangeType::Deletion,
1670 creation_tx: Bytes::from_str("0x0000000000000000000000000000000000000000000000000000000000009c41").unwrap(),
1671 created_at: NaiveDateTime::from_timestamp_opt(base_ts + 4000, 0).unwrap(),
1672 }),
1673 ]),
1674 component_balances: HashMap::from([
1675 ("pc_1".to_string(), HashMap::from([
1676 (Bytes::from_str("0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48").unwrap(), models::protocol::ComponentBalance {
1677 token: Bytes::from_str("0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48").unwrap(),
1678 balance: Bytes::from("0x00000001"),
1679 balance_float: 1.0,
1680 modify_tx: Bytes::from_str("0x0000000000000000000000000000000000000000000000000000000000000000").unwrap(),
1681 component_id: "pc_1".to_string(),
1682 }),
1683 (Bytes::from_str("0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2").unwrap(), models::protocol::ComponentBalance {
1684 token: Bytes::from_str("0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2").unwrap(),
1685 balance: Bytes::from("0x000003e8"),
1686 balance_float: 1000.0,
1687 modify_tx: Bytes::from_str("0x0000000000000000000000000000000000000000000000000000000000007531").unwrap(),
1688 component_id: "pc_1".to_string(),
1689 }),
1690 ])),
1691 ]),
1692 account_balances: HashMap::from([
1693 (Bytes::from_str("0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2").unwrap(), HashMap::from([
1694 (Bytes::from_str("0x7a250d5630b4cf539739df2c5dacb4c659f2488d").unwrap(), models::contract::AccountBalance {
1695 account: Bytes::from_str("0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2").unwrap(),
1696 token: Bytes::from_str("0x7a250d5630b4cf539739df2c5dacb4c659f2488d").unwrap(),
1697 balance: Bytes::from("0x000003e8"),
1698 modify_tx: Bytes::from_str("0x0000000000000000000000000000000000000000000000000000000000007531").unwrap(),
1699 }),
1700 ])),
1701 ]),
1702 component_tvl: HashMap::new(),
1703 account_deltas: Default::default(),
1704 }
1705 }
1706
1707 #[test]
1708 fn test_serialize_deserialize_block_changes() {
1709 let block_entity_changes = create_models_block_changes();
1714
1715 let json_data = serde_json::to_string(&block_entity_changes).expect("Failed to serialize");
1717
1718 serde_json::from_str::<BlockChanges>(&json_data).expect("parsing failed");
1720 }
1721
1722 #[test]
1723 fn test_parse_block_changes() {
1724 let json_data = r#"
1725 {
1726 "extractor": "vm:ambient",
1727 "chain": "ethereum",
1728 "block": {
1729 "number": 123,
1730 "hash": "0x0000000000000000000000000000000000000000000000000000000000000000",
1731 "parent_hash": "0x0000000000000000000000000000000000000000000000000000000000000000",
1732 "chain": "ethereum",
1733 "ts": "2023-09-14T00:00:00"
1734 },
1735 "finalized_block_height": 0,
1736 "revert": false,
1737 "new_tokens": {},
1738 "account_updates": {
1739 "0x7a250d5630b4cf539739df2c5dacb4c659f2488d": {
1740 "address": "0x7a250d5630b4cf539739df2c5dacb4c659f2488d",
1741 "chain": "ethereum",
1742 "slots": {},
1743 "balance": "0x01f4",
1744 "code": "",
1745 "change": "Update"
1746 }
1747 },
1748 "state_updates": {
1749 "component_1": {
1750 "component_id": "component_1",
1751 "updated_attributes": {"attr1": "0x01"},
1752 "deleted_attributes": ["attr2"]
1753 }
1754 },
1755 "new_protocol_components":
1756 { "protocol_1": {
1757 "id": "protocol_1",
1758 "protocol_system": "system_1",
1759 "protocol_type_name": "type_1",
1760 "chain": "ethereum",
1761 "tokens": ["0x01", "0x02"],
1762 "contract_ids": ["0x01", "0x02"],
1763 "static_attributes": {"attr1": "0x01f4"},
1764 "change": "Update",
1765 "creation_tx": "0x01",
1766 "created_at": "2023-09-14T00:00:00"
1767 }
1768 },
1769 "deleted_protocol_components": {},
1770 "component_balances": {
1771 "protocol_1":
1772 {
1773 "0x01": {
1774 "token": "0x01",
1775 "balance": "0xb77831d23691653a01",
1776 "balance_float": 3.3844151001790677e21,
1777 "modify_tx": "0x01",
1778 "component_id": "protocol_1"
1779 }
1780 }
1781 },
1782 "account_balances": {
1783 "0x7a250d5630b4cf539739df2c5dacb4c659f2488d": {
1784 "0x7a250d5630b4cf539739df2c5dacb4c659f2488d": {
1785 "account": "0x7a250d5630b4cf539739df2c5dacb4c659f2488d",
1786 "token": "0x7a250d5630b4cf539739df2c5dacb4c659f2488d",
1787 "balance": "0x01f4",
1788 "modify_tx": "0x01"
1789 }
1790 }
1791 },
1792 "component_tvl": {
1793 "protocol_1": 1000.0
1794 }
1795 }
1796 "#;
1797
1798 serde_json::from_str::<BlockChanges>(json_data).expect("parsing failed");
1799 }
1800
1801 #[test]
1802 fn test_parse_websocket_message() {
1803 let json_data = r#"
1804 {
1805 "subscription_id": "5d23bfbe-89ad-4ea3-8672-dc9e973ac9dc",
1806 "deltas": {
1807 "type": "BlockChanges",
1808 "extractor": "uniswap_v2",
1809 "chain": "ethereum",
1810 "block": {
1811 "number": 19291517,
1812 "hash": "0xbc3ea4896c0be8da6229387a8571b72818aa258daf4fab46471003ad74c4ee83",
1813 "parent_hash": "0x89ca5b8d593574cf6c886f41ef8208bf6bdc1a90ef36046cb8c84bc880b9af8f",
1814 "chain": "ethereum",
1815 "ts": "2024-02-23T16:35:35"
1816 },
1817 "finalized_block_height": 0,
1818 "revert": false,
1819 "new_tokens": {},
1820 "account_updates": {
1821 "0x7a250d5630b4cf539739df2c5dacb4c659f2488d": {
1822 "address": "0x7a250d5630b4cf539739df2c5dacb4c659f2488d",
1823 "chain": "ethereum",
1824 "slots": {},
1825 "balance": "0x01f4",
1826 "code": "",
1827 "change": "Update"
1828 }
1829 },
1830 "state_updates": {
1831 "0xde6faedbcae38eec6d33ad61473a04a6dd7f6e28": {
1832 "component_id": "0xde6faedbcae38eec6d33ad61473a04a6dd7f6e28",
1833 "updated_attributes": {
1834 "reserve0": "0x87f7b5973a7f28a8b32404",
1835 "reserve1": "0x09e9564b11"
1836 },
1837 "deleted_attributes": [ ]
1838 },
1839 "0x99c59000f5a76c54c4fd7d82720c045bdcf1450d": {
1840 "component_id": "0x99c59000f5a76c54c4fd7d82720c045bdcf1450d",
1841 "updated_attributes": {
1842 "reserve1": "0x44d9a8fd662c2f4d03",
1843 "reserve0": "0x500b1261f811d5bf423e"
1844 },
1845 "deleted_attributes": [ ]
1846 }
1847 },
1848 "new_protocol_components": { },
1849 "deleted_protocol_components": { },
1850 "component_balances": {
1851 "0x99c59000f5a76c54c4fd7d82720c045bdcf1450d": {
1852 "0x9012744b7a564623b6c3e40b144fc196bdedf1a9": {
1853 "token": "0x9012744b7a564623b6c3e40b144fc196bdedf1a9",
1854 "balance": "0x500b1261f811d5bf423e",
1855 "balance_float": 3.779935574269033E23,
1856 "modify_tx": "0xe46c4db085fb6c6f3408a65524555797adb264e1d5cf3b66ad154598f85ac4bf",
1857 "component_id": "0x99c59000f5a76c54c4fd7d82720c045bdcf1450d"
1858 },
1859 "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2": {
1860 "token": "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2",
1861 "balance": "0x44d9a8fd662c2f4d03",
1862 "balance_float": 1.270062661329837E21,
1863 "modify_tx": "0xe46c4db085fb6c6f3408a65524555797adb264e1d5cf3b66ad154598f85ac4bf",
1864 "component_id": "0x99c59000f5a76c54c4fd7d82720c045bdcf1450d"
1865 }
1866 }
1867 },
1868 "account_balances": {
1869 "0x7a250d5630b4cf539739df2c5dacb4c659f2488d": {
1870 "0x7a250d5630b4cf539739df2c5dacb4c659f2488d": {
1871 "account": "0x7a250d5630b4cf539739df2c5dacb4c659f2488d",
1872 "token": "0x7a250d5630b4cf539739df2c5dacb4c659f2488d",
1873 "balance": "0x01f4",
1874 "modify_tx": "0x01"
1875 }
1876 }
1877 },
1878 "component_tvl": { }
1879 }
1880 }
1881 "#;
1882 serde_json::from_str::<WebSocketMessage>(json_data).expect("parsing failed");
1883 }
1884
1885 #[test]
1886 fn test_protocol_state_delta_merge_update_delete() {
1887 let mut delta1 = ProtocolStateDelta {
1889 component_id: "Component1".to_string(),
1890 updated_attributes: [("Attribute1".to_string(), Bytes::from("0xbadbabe420"))]
1891 .iter()
1892 .cloned()
1893 .collect(),
1894 deleted_attributes: HashSet::new(),
1895 };
1896 let delta2 = ProtocolStateDelta {
1897 component_id: "Component1".to_string(),
1898 updated_attributes: [("Attribute2".to_string(), Bytes::from("0x0badbabe"))]
1899 .iter()
1900 .cloned()
1901 .collect(),
1902 deleted_attributes: ["Attribute1".to_string()]
1903 .iter()
1904 .cloned()
1905 .collect(),
1906 };
1907 let exp = ProtocolStateDelta {
1908 component_id: "Component1".to_string(),
1909 updated_attributes: [("Attribute2".to_string(), Bytes::from("0x0badbabe"))]
1910 .iter()
1911 .cloned()
1912 .collect(),
1913 deleted_attributes: ["Attribute1".to_string()]
1914 .iter()
1915 .cloned()
1916 .collect(),
1917 };
1918
1919 delta1.merge(&delta2);
1920
1921 assert_eq!(delta1, exp);
1922 }
1923
1924 #[test]
1925 fn test_protocol_state_delta_merge_delete_update() {
1926 let mut delta1 = ProtocolStateDelta {
1928 component_id: "Component1".to_string(),
1929 updated_attributes: HashMap::new(),
1930 deleted_attributes: ["Attribute1".to_string()]
1931 .iter()
1932 .cloned()
1933 .collect(),
1934 };
1935 let delta2 = ProtocolStateDelta {
1936 component_id: "Component1".to_string(),
1937 updated_attributes: [("Attribute1".to_string(), Bytes::from("0x0badbabe"))]
1938 .iter()
1939 .cloned()
1940 .collect(),
1941 deleted_attributes: HashSet::new(),
1942 };
1943 let exp = ProtocolStateDelta {
1944 component_id: "Component1".to_string(),
1945 updated_attributes: [("Attribute1".to_string(), Bytes::from("0x0badbabe"))]
1946 .iter()
1947 .cloned()
1948 .collect(),
1949 deleted_attributes: HashSet::new(),
1950 };
1951
1952 delta1.merge(&delta2);
1953
1954 assert_eq!(delta1, exp);
1955 }
1956
1957 #[test]
1958 fn test_account_update_merge() {
1959 let mut account1 = AccountUpdate::new(
1961 Bytes::from(b"0x1234"),
1962 Chain::Ethereum,
1963 [(Bytes::from("0xaabb"), Bytes::from("0xccdd"))]
1964 .iter()
1965 .cloned()
1966 .collect(),
1967 Some(Bytes::from("0x1000")),
1968 Some(Bytes::from("0xdeadbeaf")),
1969 ChangeType::Creation,
1970 );
1971
1972 let account2 = AccountUpdate::new(
1973 Bytes::from(b"0x1234"), Chain::Ethereum,
1975 [(Bytes::from("0xeeff"), Bytes::from("0x11223344"))]
1976 .iter()
1977 .cloned()
1978 .collect(),
1979 Some(Bytes::from("0x2000")),
1980 Some(Bytes::from("0xcafebabe")),
1981 ChangeType::Update,
1982 );
1983
1984 account1.merge(&account2);
1986
1987 let expected = AccountUpdate::new(
1989 Bytes::from(b"0x1234"), Chain::Ethereum,
1991 [
1992 (Bytes::from("0xaabb"), Bytes::from("0xccdd")), (Bytes::from("0xeeff"), Bytes::from("0x11223344")), ]
1995 .iter()
1996 .cloned()
1997 .collect(),
1998 Some(Bytes::from("0x2000")), Some(Bytes::from("0xcafebabe")), ChangeType::Creation, );
2002
2003 assert_eq!(account1, expected);
2005 }
2006
2007 #[test]
2008 fn test_block_account_changes_merge() {
2009 let old_account_updates: HashMap<Bytes, AccountUpdate> = [(
2011 Bytes::from("0x0011"),
2012 AccountUpdate {
2013 address: Bytes::from("0x00"),
2014 chain: Chain::Ethereum,
2015 slots: [(Bytes::from("0x0022"), Bytes::from("0x0033"))]
2016 .into_iter()
2017 .collect(),
2018 balance: Some(Bytes::from("0x01")),
2019 code: Some(Bytes::from("0x02")),
2020 change: ChangeType::Creation,
2021 },
2022 )]
2023 .into_iter()
2024 .collect();
2025 let new_account_updates: HashMap<Bytes, AccountUpdate> = [(
2026 Bytes::from("0x0011"),
2027 AccountUpdate {
2028 address: Bytes::from("0x00"),
2029 chain: Chain::Ethereum,
2030 slots: [(Bytes::from("0x0044"), Bytes::from("0x0055"))]
2031 .into_iter()
2032 .collect(),
2033 balance: Some(Bytes::from("0x03")),
2034 code: Some(Bytes::from("0x04")),
2035 change: ChangeType::Update,
2036 },
2037 )]
2038 .into_iter()
2039 .collect();
2040 let block_account_changes_initial = BlockChanges::new(
2042 "extractor1",
2043 Chain::Ethereum,
2044 Block::default(),
2045 0,
2046 false,
2047 old_account_updates,
2048 HashMap::new(),
2049 HashMap::new(),
2050 HashMap::new(),
2051 HashMap::new(),
2052 HashMap::new(),
2053 );
2054
2055 let block_account_changes_new = BlockChanges::new(
2056 "extractor2",
2057 Chain::Ethereum,
2058 Block::default(),
2059 0,
2060 true,
2061 new_account_updates,
2062 HashMap::new(),
2063 HashMap::new(),
2064 HashMap::new(),
2065 HashMap::new(),
2066 HashMap::new(),
2067 );
2068
2069 let res = block_account_changes_initial.merge(block_account_changes_new);
2071
2072 let expected_account_updates: HashMap<Bytes, AccountUpdate> = [(
2074 Bytes::from("0x0011"),
2075 AccountUpdate {
2076 address: Bytes::from("0x00"),
2077 chain: Chain::Ethereum,
2078 slots: [
2079 (Bytes::from("0x0044"), Bytes::from("0x0055")),
2080 (Bytes::from("0x0022"), Bytes::from("0x0033")),
2081 ]
2082 .into_iter()
2083 .collect(),
2084 balance: Some(Bytes::from("0x03")),
2085 code: Some(Bytes::from("0x04")),
2086 change: ChangeType::Creation,
2087 },
2088 )]
2089 .into_iter()
2090 .collect();
2091 let block_account_changes_expected = BlockChanges::new(
2092 "extractor1",
2093 Chain::Ethereum,
2094 Block::default(),
2095 0,
2096 true,
2097 expected_account_updates,
2098 HashMap::new(),
2099 HashMap::new(),
2100 HashMap::new(),
2101 HashMap::new(),
2102 HashMap::new(),
2103 );
2104 assert_eq!(res, block_account_changes_expected);
2105 }
2106
2107 #[test]
2108 fn test_block_entity_changes_merge() {
2109 let block_entity_changes_result1 = BlockChanges {
2111 extractor: String::from("extractor1"),
2112 chain: Chain::Ethereum,
2113 block: Block::default(),
2114 revert: false,
2115 new_tokens: HashMap::new(),
2116 state_updates: hashmap! { "state1".to_string() => ProtocolStateDelta::default() },
2117 new_protocol_components: hashmap! { "component1".to_string() => ProtocolComponent::default() },
2118 deleted_protocol_components: HashMap::new(),
2119 component_balances: hashmap! {
2120 "component1".to_string() => TokenBalances(hashmap! {
2121 Bytes::from("0x01") => ComponentBalance {
2122 token: Bytes::from("0x01"),
2123 balance: Bytes::from("0x01"),
2124 balance_float: 1.0,
2125 modify_tx: Bytes::from("0x00"),
2126 component_id: "component1".to_string()
2127 },
2128 Bytes::from("0x02") => ComponentBalance {
2129 token: Bytes::from("0x02"),
2130 balance: Bytes::from("0x02"),
2131 balance_float: 2.0,
2132 modify_tx: Bytes::from("0x00"),
2133 component_id: "component1".to_string()
2134 },
2135 })
2136
2137 },
2138 component_tvl: hashmap! { "tvl1".to_string() => 1000.0 },
2139 ..Default::default()
2140 };
2141 let block_entity_changes_result2 = BlockChanges {
2142 extractor: String::from("extractor2"),
2143 chain: Chain::Ethereum,
2144 block: Block::default(),
2145 revert: true,
2146 new_tokens: HashMap::new(),
2147 state_updates: hashmap! { "state2".to_string() => ProtocolStateDelta::default() },
2148 new_protocol_components: hashmap! { "component2".to_string() => ProtocolComponent::default() },
2149 deleted_protocol_components: hashmap! { "component3".to_string() => ProtocolComponent::default() },
2150 component_balances: hashmap! {
2151 "component1".to_string() => TokenBalances::default(),
2152 "component2".to_string() => TokenBalances::default()
2153 },
2154 component_tvl: hashmap! { "tvl2".to_string() => 2000.0 },
2155 ..Default::default()
2156 };
2157
2158 let res = block_entity_changes_result1.merge(block_entity_changes_result2);
2159
2160 let expected_block_entity_changes_result = BlockChanges {
2161 extractor: String::from("extractor1"),
2162 chain: Chain::Ethereum,
2163 block: Block::default(),
2164 revert: true,
2165 new_tokens: HashMap::new(),
2166 state_updates: hashmap! {
2167 "state1".to_string() => ProtocolStateDelta::default(),
2168 "state2".to_string() => ProtocolStateDelta::default(),
2169 },
2170 new_protocol_components: hashmap! {
2171 "component1".to_string() => ProtocolComponent::default(),
2172 "component2".to_string() => ProtocolComponent::default(),
2173 },
2174 deleted_protocol_components: hashmap! {
2175 "component3".to_string() => ProtocolComponent::default(),
2176 },
2177 component_balances: hashmap! {
2178 "component1".to_string() => TokenBalances(hashmap! {
2179 Bytes::from("0x01") => ComponentBalance {
2180 token: Bytes::from("0x01"),
2181 balance: Bytes::from("0x01"),
2182 balance_float: 1.0,
2183 modify_tx: Bytes::from("0x00"),
2184 component_id: "component1".to_string()
2185 },
2186 Bytes::from("0x02") => ComponentBalance {
2187 token: Bytes::from("0x02"),
2188 balance: Bytes::from("0x02"),
2189 balance_float: 2.0,
2190 modify_tx: Bytes::from("0x00"),
2191 component_id: "component1".to_string()
2192 },
2193 }),
2194 "component2".to_string() => TokenBalances::default(),
2195 },
2196 component_tvl: hashmap! {
2197 "tvl1".to_string() => 1000.0,
2198 "tvl2".to_string() => 2000.0
2199 },
2200 ..Default::default()
2201 };
2202
2203 assert_eq!(res, expected_block_entity_changes_result);
2204 }
2205}