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 #[schema(default = 10)]
874 pub page_size: i64,
875}
876
877impl PaginationParams {
878 pub fn new(page: i64, page_size: i64) -> Self {
879 Self { page, page_size }
880 }
881}
882
883impl Default for PaginationParams {
884 fn default() -> Self {
885 PaginationParams { page: 0, page_size: 20 }
886 }
887}
888
889#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, ToSchema, Eq, Hash)]
890#[serde(deny_unknown_fields)]
891pub struct PaginationResponse {
892 pub page: i64,
893 pub page_size: i64,
894 pub total: i64,
896}
897
898impl PaginationResponse {
900 pub fn new(page: i64, page_size: i64, total: i64) -> Self {
901 Self { page, page_size, total }
902 }
903
904 pub fn total_pages(&self) -> i64 {
905 (self.total + self.page_size - 1) / self.page_size
907 }
908}
909
910#[derive(PartialEq, Debug, Clone, Serialize, Deserialize, Default, ToSchema, Eq, Hash)]
911#[serde(rename = "Token")]
912pub struct ResponseToken {
914 pub chain: Chain,
915 #[schema(value_type=String, example="0xc9f2e6ea1637E499406986ac50ddC92401ce1f58")]
917 #[serde(with = "hex_bytes")]
918 pub address: Bytes,
919 #[schema(value_type=String, example="WETH")]
921 pub symbol: String,
922 pub decimals: u32,
924 pub tax: u64,
926 pub gas: Vec<Option<u64>>,
928 pub quality: u32,
936}
937
938impl From<models::token::CurrencyToken> for ResponseToken {
939 fn from(value: models::token::CurrencyToken) -> Self {
940 Self {
941 chain: value.chain.into(),
942 address: value.address,
943 symbol: value.symbol,
944 decimals: value.decimals,
945 tax: value.tax,
946 gas: value.gas,
947 quality: value.quality,
948 }
949 }
950}
951
952#[derive(Serialize, Deserialize, Debug, Default, ToSchema, Clone)]
953#[serde(deny_unknown_fields)]
954pub struct ProtocolComponentsRequestBody {
955 pub protocol_system: String,
958 #[serde(alias = "componentAddresses")]
960 pub component_ids: Option<Vec<String>>,
961 #[serde(default)]
964 pub tvl_gt: Option<f64>,
965 #[serde(default)]
966 pub chain: Chain,
967 #[serde(default)]
969 pub pagination: PaginationParams,
970}
971
972impl PartialEq for ProtocolComponentsRequestBody {
974 fn eq(&self, other: &Self) -> bool {
975 let tvl_close_enough = match (self.tvl_gt, other.tvl_gt) {
976 (Some(a), Some(b)) => (a - b).abs() < 1e-6,
977 (None, None) => true,
978 _ => false,
979 };
980
981 self.protocol_system == other.protocol_system &&
982 self.component_ids == other.component_ids &&
983 tvl_close_enough &&
984 self.chain == other.chain &&
985 self.pagination == other.pagination
986 }
987}
988
989impl Eq for ProtocolComponentsRequestBody {}
991
992impl Hash for ProtocolComponentsRequestBody {
993 fn hash<H: Hasher>(&self, state: &mut H) {
994 self.protocol_system.hash(state);
995 self.component_ids.hash(state);
996
997 if let Some(tvl) = self.tvl_gt {
999 tvl.to_bits().hash(state);
1001 } else {
1002 state.write_u8(0);
1004 }
1005
1006 self.chain.hash(state);
1007 self.pagination.hash(state);
1008 }
1009}
1010
1011impl ProtocolComponentsRequestBody {
1012 pub fn system_filtered(system: &str, tvl_gt: Option<f64>, chain: Chain) -> Self {
1013 Self {
1014 protocol_system: system.to_string(),
1015 component_ids: None,
1016 tvl_gt,
1017 chain,
1018 pagination: Default::default(),
1019 }
1020 }
1021
1022 pub fn id_filtered(system: &str, ids: Vec<String>, chain: Chain) -> Self {
1023 Self {
1024 protocol_system: system.to_string(),
1025 component_ids: Some(ids),
1026 tvl_gt: None,
1027 chain,
1028 pagination: Default::default(),
1029 }
1030 }
1031}
1032
1033impl ProtocolComponentsRequestBody {
1034 pub fn new(
1035 protocol_system: String,
1036 component_ids: Option<Vec<String>>,
1037 tvl_gt: Option<f64>,
1038 chain: Chain,
1039 pagination: PaginationParams,
1040 ) -> Self {
1041 Self { protocol_system, component_ids, tvl_gt, chain, pagination }
1042 }
1043}
1044
1045#[deprecated(note = "Use ProtocolComponentsRequestBody instead")]
1046#[derive(Serialize, Deserialize, Default, Debug, IntoParams)]
1047pub struct ProtocolComponentRequestParameters {
1048 #[param(default = 0)]
1050 pub tvl_gt: Option<f64>,
1051}
1052
1053impl ProtocolComponentRequestParameters {
1054 pub fn tvl_filtered(min_tvl: f64) -> Self {
1055 Self { tvl_gt: Some(min_tvl) }
1056 }
1057}
1058
1059impl ProtocolComponentRequestParameters {
1060 pub fn to_query_string(&self) -> String {
1061 if let Some(tvl_gt) = self.tvl_gt {
1062 return format!("?tvl_gt={tvl_gt}");
1063 }
1064 String::new()
1065 }
1066}
1067
1068#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, ToSchema)]
1070pub struct ProtocolComponentRequestResponse {
1071 pub protocol_components: Vec<ProtocolComponent>,
1072 pub pagination: PaginationResponse,
1073}
1074
1075impl ProtocolComponentRequestResponse {
1076 pub fn new(
1077 protocol_components: Vec<ProtocolComponent>,
1078 pagination: PaginationResponse,
1079 ) -> Self {
1080 Self { protocol_components, pagination }
1081 }
1082}
1083
1084#[derive(Debug, Serialize, Deserialize, PartialEq, Clone, ToSchema, Eq, Hash)]
1085#[serde(deny_unknown_fields)]
1086#[deprecated]
1087pub struct ProtocolId {
1088 pub id: String,
1089 pub chain: Chain,
1090}
1091
1092impl From<ProtocolId> for String {
1093 fn from(protocol_id: ProtocolId) -> Self {
1094 protocol_id.id
1095 }
1096}
1097
1098impl AsRef<str> for ProtocolId {
1099 fn as_ref(&self) -> &str {
1100 &self.id
1101 }
1102}
1103
1104#[derive(Debug, Clone, PartialEq, Default, Deserialize, Serialize, ToSchema)]
1106pub struct ResponseProtocolState {
1107 pub component_id: String,
1109 #[schema(value_type=HashMap<String, String>)]
1112 #[serde(with = "hex_hashmap_value")]
1113 pub attributes: HashMap<String, Bytes>,
1114 #[schema(value_type=HashMap<String, String>)]
1116 #[serde(with = "hex_hashmap_key_value")]
1117 pub balances: HashMap<Bytes, Bytes>,
1118}
1119
1120impl From<models::protocol::ProtocolComponentState> for ResponseProtocolState {
1121 fn from(value: models::protocol::ProtocolComponentState) -> Self {
1122 Self {
1123 component_id: value.component_id,
1124 attributes: value.attributes,
1125 balances: value.balances,
1126 }
1127 }
1128}
1129
1130fn default_include_balances_flag() -> bool {
1131 true
1132}
1133
1134#[derive(Clone, Debug, Serialize, PartialEq, ToSchema, Default, Eq, Hash)]
1136#[serde(deny_unknown_fields)]
1137pub struct ProtocolStateRequestBody {
1138 #[serde(alias = "protocolIds")]
1140 pub protocol_ids: Option<Vec<String>>,
1141 #[serde(alias = "protocolSystem")]
1144 pub protocol_system: String,
1145 #[serde(default)]
1146 pub chain: Chain,
1147 #[serde(default = "default_include_balances_flag")]
1149 pub include_balances: bool,
1150 #[serde(default = "VersionParam::default")]
1151 pub version: VersionParam,
1152 #[serde(default)]
1153 pub pagination: PaginationParams,
1154}
1155
1156impl ProtocolStateRequestBody {
1157 pub fn id_filtered<I, T>(ids: I) -> Self
1158 where
1159 I: IntoIterator<Item = T>,
1160 T: Into<String>,
1161 {
1162 Self {
1163 protocol_ids: Some(
1164 ids.into_iter()
1165 .map(Into::into)
1166 .collect(),
1167 ),
1168 ..Default::default()
1169 }
1170 }
1171}
1172
1173impl<'de> Deserialize<'de> for ProtocolStateRequestBody {
1177 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
1178 where
1179 D: Deserializer<'de>,
1180 {
1181 #[derive(Deserialize)]
1182 #[serde(untagged)]
1183 enum ProtocolIdOrString {
1184 Old(Vec<ProtocolId>),
1185 New(Vec<String>),
1186 }
1187
1188 struct ProtocolStateRequestBodyVisitor;
1189
1190 impl<'de> de::Visitor<'de> for ProtocolStateRequestBodyVisitor {
1191 type Value = ProtocolStateRequestBody;
1192
1193 fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
1194 formatter.write_str("struct ProtocolStateRequestBody")
1195 }
1196
1197 fn visit_map<V>(self, mut map: V) -> Result<ProtocolStateRequestBody, V::Error>
1198 where
1199 V: de::MapAccess<'de>,
1200 {
1201 let mut protocol_ids = None;
1202 let mut protocol_system = None;
1203 let mut version = None;
1204 let mut chain = None;
1205 let mut include_balances = None;
1206 let mut pagination = None;
1207
1208 while let Some(key) = map.next_key::<String>()? {
1209 match key.as_str() {
1210 "protocol_ids" | "protocolIds" => {
1211 let value: ProtocolIdOrString = map.next_value()?;
1212 protocol_ids = match value {
1213 ProtocolIdOrString::Old(ids) => {
1214 Some(ids.into_iter().map(|p| p.id).collect())
1215 }
1216 ProtocolIdOrString::New(ids_str) => Some(ids_str),
1217 };
1218 }
1219 "protocol_system" | "protocolSystem" => {
1220 protocol_system = Some(map.next_value()?);
1221 }
1222 "version" => {
1223 version = Some(map.next_value()?);
1224 }
1225 "chain" => {
1226 chain = Some(map.next_value()?);
1227 }
1228 "include_balances" => {
1229 include_balances = Some(map.next_value()?);
1230 }
1231 "pagination" => {
1232 pagination = Some(map.next_value()?);
1233 }
1234 _ => {
1235 return Err(de::Error::unknown_field(
1236 &key,
1237 &[
1238 "contract_ids",
1239 "protocol_system",
1240 "version",
1241 "chain",
1242 "include_balances",
1243 "pagination",
1244 ],
1245 ))
1246 }
1247 }
1248 }
1249
1250 Ok(ProtocolStateRequestBody {
1251 protocol_ids,
1252 protocol_system: protocol_system.unwrap_or_default(),
1253 version: version.unwrap_or_else(VersionParam::default),
1254 chain: chain.unwrap_or_else(Chain::default),
1255 include_balances: include_balances.unwrap_or(true),
1256 pagination: pagination.unwrap_or_else(PaginationParams::default),
1257 })
1258 }
1259 }
1260
1261 deserializer.deserialize_struct(
1262 "ProtocolStateRequestBody",
1263 &[
1264 "contract_ids",
1265 "protocol_system",
1266 "version",
1267 "chain",
1268 "include_balances",
1269 "pagination",
1270 ],
1271 ProtocolStateRequestBodyVisitor,
1272 )
1273 }
1274}
1275
1276#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, ToSchema)]
1277pub struct ProtocolStateRequestResponse {
1278 pub states: Vec<ResponseProtocolState>,
1279 pub pagination: PaginationResponse,
1280}
1281
1282impl ProtocolStateRequestResponse {
1283 pub fn new(states: Vec<ResponseProtocolState>, pagination: PaginationResponse) -> Self {
1284 Self { states, pagination }
1285 }
1286}
1287
1288#[derive(Clone, PartialEq, Hash, Eq)]
1289pub struct ProtocolComponentId {
1290 pub chain: Chain,
1291 pub system: String,
1292 pub id: String,
1293}
1294
1295#[derive(Debug, Serialize, ToSchema)]
1296#[serde(tag = "status", content = "message")]
1297#[schema(example = json!({"status": "NotReady", "message": "No db connection"}))]
1298pub enum Health {
1299 Ready,
1300 Starting(String),
1301 NotReady(String),
1302}
1303
1304#[derive(Serialize, Deserialize, Debug, Default, PartialEq, ToSchema, Eq, Hash, Clone)]
1305#[serde(deny_unknown_fields)]
1306pub struct ProtocolSystemsRequestBody {
1307 #[serde(default)]
1308 pub chain: Chain,
1309 #[serde(default)]
1310 pub pagination: PaginationParams,
1311}
1312
1313#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, ToSchema, Eq, Hash)]
1314pub struct ProtocolSystemsRequestResponse {
1315 pub protocol_systems: Vec<String>,
1317 pub pagination: PaginationResponse,
1318}
1319
1320impl ProtocolSystemsRequestResponse {
1321 pub fn new(protocol_systems: Vec<String>, pagination: PaginationResponse) -> Self {
1322 Self { protocol_systems, pagination }
1323 }
1324}
1325
1326#[cfg(test)]
1327mod test {
1328 use std::str::FromStr;
1329
1330 use maplit::hashmap;
1331 use rstest::rstest;
1332
1333 use super::*;
1334
1335 #[test]
1336 fn test_protocol_components_equality() {
1337 let body1 = ProtocolComponentsRequestBody {
1338 protocol_system: "protocol1".to_string(),
1339 component_ids: Some(vec!["component1".to_string(), "component2".to_string()]),
1340 tvl_gt: Some(1000.0),
1341 chain: Chain::Ethereum,
1342 pagination: PaginationParams::default(),
1343 };
1344
1345 let body2 = ProtocolComponentsRequestBody {
1346 protocol_system: "protocol1".to_string(),
1347 component_ids: Some(vec!["component1".to_string(), "component2".to_string()]),
1348 tvl_gt: Some(1000.0 + 1e-7), chain: Chain::Ethereum,
1350 pagination: PaginationParams::default(),
1351 };
1352
1353 assert_eq!(body1, body2);
1355 }
1356
1357 #[test]
1358 fn test_protocol_components_inequality() {
1359 let body1 = ProtocolComponentsRequestBody {
1360 protocol_system: "protocol1".to_string(),
1361 component_ids: Some(vec!["component1".to_string(), "component2".to_string()]),
1362 tvl_gt: Some(1000.0),
1363 chain: Chain::Ethereum,
1364 pagination: PaginationParams::default(),
1365 };
1366
1367 let body2 = ProtocolComponentsRequestBody {
1368 protocol_system: "protocol1".to_string(),
1369 component_ids: Some(vec!["component1".to_string(), "component2".to_string()]),
1370 tvl_gt: Some(1000.0 + 1e-5), chain: Chain::Ethereum,
1372 pagination: PaginationParams::default(),
1373 };
1374
1375 assert_ne!(body1, body2);
1377 }
1378
1379 #[test]
1380 fn test_parse_state_request() {
1381 let json_str = r#"
1382 {
1383 "contractIds": [
1384 "0xb4eccE46b8D4e4abFd03C9B806276A6735C9c092"
1385 ],
1386 "protocol_system": "uniswap_v2",
1387 "version": {
1388 "timestamp": "2069-01-01T04:20:00",
1389 "block": {
1390 "hash": "0x24101f9cb26cd09425b52da10e8c2f56ede94089a8bbe0f31f1cda5f4daa52c4",
1391 "number": 213,
1392 "chain": "ethereum"
1393 }
1394 }
1395 }
1396 "#;
1397
1398 let result: StateRequestBody = serde_json::from_str(json_str).unwrap();
1399
1400 let contract0 = "b4eccE46b8D4e4abFd03C9B806276A6735C9c092"
1401 .parse()
1402 .unwrap();
1403 let block_hash = "24101f9cb26cd09425b52da10e8c2f56ede94089a8bbe0f31f1cda5f4daa52c4"
1404 .parse()
1405 .unwrap();
1406 let block_number = 213;
1407
1408 let expected_timestamp =
1409 NaiveDateTime::parse_from_str("2069-01-01T04:20:00", "%Y-%m-%dT%H:%M:%S").unwrap();
1410
1411 let expected = StateRequestBody {
1412 contract_ids: Some(vec![contract0]),
1413 protocol_system: "uniswap_v2".to_string(),
1414 version: VersionParam {
1415 timestamp: Some(expected_timestamp),
1416 block: Some(BlockParam {
1417 hash: Some(block_hash),
1418 chain: Some(Chain::Ethereum),
1419 number: Some(block_number),
1420 }),
1421 },
1422 chain: Chain::Ethereum,
1423 pagination: PaginationParams::default(),
1424 };
1425
1426 assert_eq!(result, expected);
1427 }
1428
1429 #[test]
1430 fn test_parse_state_request_dual_interface() {
1431 let json_common = r#"
1432 {
1433 "__CONTRACT_IDS__": [
1434 "0xb4eccE46b8D4e4abFd03C9B806276A6735C9c092"
1435 ],
1436 "version": {
1437 "timestamp": "2069-01-01T04:20:00",
1438 "block": {
1439 "hash": "0x24101f9cb26cd09425b52da10e8c2f56ede94089a8bbe0f31f1cda5f4daa52c4",
1440 "number": 213,
1441 "chain": "ethereum"
1442 }
1443 }
1444 }
1445 "#;
1446
1447 let json_str_snake = json_common.replace("\"__CONTRACT_IDS__\"", "\"contract_ids\"");
1448 let json_str_camel = json_common.replace("\"__CONTRACT_IDS__\"", "\"contractIds\"");
1449
1450 let snake: StateRequestBody = serde_json::from_str(&json_str_snake).unwrap();
1451 let camel: StateRequestBody = serde_json::from_str(&json_str_camel).unwrap();
1452
1453 assert_eq!(snake, camel);
1454 }
1455
1456 #[test]
1457 fn test_parse_state_request_unknown_field() {
1458 let body = r#"
1459 {
1460 "contract_ids_with_typo_error": [
1461 {
1462 "address": "0xb4eccE46b8D4e4abFd03C9B806276A6735C9c092",
1463 "chain": "ethereum"
1464 }
1465 ],
1466 "version": {
1467 "timestamp": "2069-01-01T04:20:00",
1468 "block": {
1469 "hash": "0x24101f9cb26cd09425b52da10e8c2f56ede94089a8bbe0f31f1cda5f4daa52c4",
1470 "parentHash": "0x8d75152454e60413efe758cc424bfd339897062d7e658f302765eb7b50971815",
1471 "number": 213,
1472 "chain": "ethereum"
1473 }
1474 }
1475 }
1476 "#;
1477
1478 let decoded = serde_json::from_str::<StateRequestBody>(body);
1479
1480 assert!(decoded.is_err(), "Expected an error due to unknown field");
1481
1482 if let Err(e) = decoded {
1483 assert!(
1484 e.to_string()
1485 .contains("unknown field `contract_ids_with_typo_error`"),
1486 "Error message does not contain expected unknown field information"
1487 );
1488 }
1489 }
1490
1491 #[test]
1492 fn test_parse_state_request_no_contract_specified() {
1493 let json_str = r#"
1494 {
1495 "protocol_system": "uniswap_v2",
1496 "version": {
1497 "timestamp": "2069-01-01T04:20:00",
1498 "block": {
1499 "hash": "0x24101f9cb26cd09425b52da10e8c2f56ede94089a8bbe0f31f1cda5f4daa52c4",
1500 "number": 213,
1501 "chain": "ethereum"
1502 }
1503 }
1504 }
1505 "#;
1506
1507 let result: StateRequestBody = serde_json::from_str(json_str).unwrap();
1508
1509 let block_hash = "24101f9cb26cd09425b52da10e8c2f56ede94089a8bbe0f31f1cda5f4daa52c4".into();
1510 let block_number = 213;
1511 let expected_timestamp =
1512 NaiveDateTime::parse_from_str("2069-01-01T04:20:00", "%Y-%m-%dT%H:%M:%S").unwrap();
1513
1514 let expected = StateRequestBody {
1515 contract_ids: None,
1516 protocol_system: "uniswap_v2".to_string(),
1517 version: VersionParam {
1518 timestamp: Some(expected_timestamp),
1519 block: Some(BlockParam {
1520 hash: Some(block_hash),
1521 chain: Some(Chain::Ethereum),
1522 number: Some(block_number),
1523 }),
1524 },
1525 chain: Chain::Ethereum,
1526 pagination: PaginationParams { page: 0, page_size: 20 },
1527 };
1528
1529 assert_eq!(result, expected);
1530 }
1531
1532 #[rstest]
1533 #[case(
1534 r#"
1535 {
1536 "protocol_ids": [
1537 {
1538 "id": "0xb4eccE46b8D4e4abFd03C9B806276A6735C9c092",
1539 "chain": "ethereum"
1540 }
1541 ],
1542 "protocol_system": "uniswap_v2",
1543 "include_balances": false,
1544 "version": {
1545 "timestamp": "2069-01-01T04:20:00",
1546 "block": {
1547 "hash": "0x24101f9cb26cd09425b52da10e8c2f56ede94089a8bbe0f31f1cda5f4daa52c4",
1548 "number": 213,
1549 "chain": "ethereum"
1550 }
1551 }
1552 }
1553 "#
1554 )]
1555 #[case(
1556 r#"
1557 {
1558 "protocolIds": [
1559 "0xb4eccE46b8D4e4abFd03C9B806276A6735C9c092"
1560 ],
1561 "protocol_system": "uniswap_v2",
1562 "include_balances": false,
1563 "version": {
1564 "timestamp": "2069-01-01T04:20:00",
1565 "block": {
1566 "hash": "0x24101f9cb26cd09425b52da10e8c2f56ede94089a8bbe0f31f1cda5f4daa52c4",
1567 "number": 213,
1568 "chain": "ethereum"
1569 }
1570 }
1571 }
1572 "#
1573 )]
1574 fn test_parse_protocol_state_request(#[case] json_str: &str) {
1575 let result: ProtocolStateRequestBody = serde_json::from_str(json_str).unwrap();
1576
1577 let block_hash = "24101f9cb26cd09425b52da10e8c2f56ede94089a8bbe0f31f1cda5f4daa52c4"
1578 .parse()
1579 .unwrap();
1580 let block_number = 213;
1581
1582 let expected_timestamp =
1583 NaiveDateTime::parse_from_str("2069-01-01T04:20:00", "%Y-%m-%dT%H:%M:%S").unwrap();
1584
1585 let expected = ProtocolStateRequestBody {
1586 protocol_ids: Some(vec!["0xb4eccE46b8D4e4abFd03C9B806276A6735C9c092".to_string()]),
1587 protocol_system: "uniswap_v2".to_string(),
1588 version: VersionParam {
1589 timestamp: Some(expected_timestamp),
1590 block: Some(BlockParam {
1591 hash: Some(block_hash),
1592 chain: Some(Chain::Ethereum),
1593 number: Some(block_number),
1594 }),
1595 },
1596 chain: Chain::Ethereum,
1597 include_balances: false,
1598 pagination: PaginationParams::default(),
1599 };
1600
1601 assert_eq!(result, expected);
1602 }
1603
1604 #[rstest]
1605 #[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()])]
1606 #[case::with_strings(vec!["id1".to_string(), "id2".to_string()], vec!["id1".to_string(), "id2".to_string()])]
1607 fn test_id_filtered<T>(#[case] input_ids: Vec<T>, #[case] expected_ids: Vec<String>)
1608 where
1609 T: Into<String> + Clone,
1610 {
1611 let request_body = ProtocolStateRequestBody::id_filtered(input_ids);
1612 assert_eq!(request_body.protocol_ids, Some(expected_ids));
1613 }
1614
1615 fn create_models_block_changes() -> crate::models::blockchain::BlockAggregatedChanges {
1616 let base_ts = 1694534400; crate::models::blockchain::BlockAggregatedChanges {
1619 extractor: "native_name".to_string(),
1620 chain: models::Chain::Ethereum,
1621 block: models::blockchain::Block::new(
1622 3,
1623 models::Chain::Ethereum,
1624 Bytes::from_str("0x0000000000000000000000000000000000000000000000000000000000000003").unwrap(),
1625 Bytes::from_str("0x0000000000000000000000000000000000000000000000000000000000000002").unwrap(),
1626 NaiveDateTime::from_timestamp_opt(base_ts + 3000, 0).unwrap(),
1627 ),
1628 finalized_block_height: 1,
1629 revert: true,
1630 state_deltas: HashMap::from([
1631 ("pc_1".to_string(), models::protocol::ProtocolComponentStateDelta {
1632 component_id: "pc_1".to_string(),
1633 updated_attributes: HashMap::from([
1634 ("attr_2".to_string(), Bytes::from("0x0000000000000002")),
1635 ("attr_1".to_string(), Bytes::from("0x00000000000003e8")),
1636 ]),
1637 deleted_attributes: HashSet::new(),
1638 }),
1639 ]),
1640 new_tokens: HashMap::new(),
1641 new_protocol_components: HashMap::from([
1642 ("pc_2".to_string(), crate::models::protocol::ProtocolComponent {
1643 id: "pc_2".to_string(),
1644 protocol_system: "native_protocol_system".to_string(),
1645 protocol_type_name: "pt_1".to_string(),
1646 chain: models::Chain::Ethereum,
1647 tokens: vec![
1648 Bytes::from_str("0xdac17f958d2ee523a2206206994597c13d831ec7").unwrap(),
1649 Bytes::from_str("0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48").unwrap(),
1650 ],
1651 contract_addresses: vec![],
1652 static_attributes: HashMap::new(),
1653 change: models::ChangeType::Creation,
1654 creation_tx: Bytes::from_str("0x000000000000000000000000000000000000000000000000000000000000c351").unwrap(),
1655 created_at: NaiveDateTime::from_timestamp_opt(base_ts + 5000, 0).unwrap(),
1656 }),
1657 ]),
1658 deleted_protocol_components: HashMap::from([
1659 ("pc_3".to_string(), crate::models::protocol::ProtocolComponent {
1660 id: "pc_3".to_string(),
1661 protocol_system: "native_protocol_system".to_string(),
1662 protocol_type_name: "pt_2".to_string(),
1663 chain: models::Chain::Ethereum,
1664 tokens: vec![
1665 Bytes::from_str("0x6b175474e89094c44da98b954eedeac495271d0f").unwrap(),
1666 Bytes::from_str("0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2").unwrap(),
1667 ],
1668 contract_addresses: vec![],
1669 static_attributes: HashMap::new(),
1670 change: models::ChangeType::Deletion,
1671 creation_tx: Bytes::from_str("0x0000000000000000000000000000000000000000000000000000000000009c41").unwrap(),
1672 created_at: NaiveDateTime::from_timestamp_opt(base_ts + 4000, 0).unwrap(),
1673 }),
1674 ]),
1675 component_balances: HashMap::from([
1676 ("pc_1".to_string(), HashMap::from([
1677 (Bytes::from_str("0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48").unwrap(), models::protocol::ComponentBalance {
1678 token: Bytes::from_str("0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48").unwrap(),
1679 balance: Bytes::from("0x00000001"),
1680 balance_float: 1.0,
1681 modify_tx: Bytes::from_str("0x0000000000000000000000000000000000000000000000000000000000000000").unwrap(),
1682 component_id: "pc_1".to_string(),
1683 }),
1684 (Bytes::from_str("0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2").unwrap(), models::protocol::ComponentBalance {
1685 token: Bytes::from_str("0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2").unwrap(),
1686 balance: Bytes::from("0x000003e8"),
1687 balance_float: 1000.0,
1688 modify_tx: Bytes::from_str("0x0000000000000000000000000000000000000000000000000000000000007531").unwrap(),
1689 component_id: "pc_1".to_string(),
1690 }),
1691 ])),
1692 ]),
1693 account_balances: HashMap::from([
1694 (Bytes::from_str("0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2").unwrap(), HashMap::from([
1695 (Bytes::from_str("0x7a250d5630b4cf539739df2c5dacb4c659f2488d").unwrap(), models::contract::AccountBalance {
1696 account: Bytes::from_str("0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2").unwrap(),
1697 token: Bytes::from_str("0x7a250d5630b4cf539739df2c5dacb4c659f2488d").unwrap(),
1698 balance: Bytes::from("0x000003e8"),
1699 modify_tx: Bytes::from_str("0x0000000000000000000000000000000000000000000000000000000000007531").unwrap(),
1700 }),
1701 ])),
1702 ]),
1703 component_tvl: HashMap::new(),
1704 account_deltas: Default::default(),
1705 }
1706 }
1707
1708 #[test]
1709 fn test_serialize_deserialize_block_changes() {
1710 let block_entity_changes = create_models_block_changes();
1715
1716 let json_data = serde_json::to_string(&block_entity_changes).expect("Failed to serialize");
1718
1719 serde_json::from_str::<BlockChanges>(&json_data).expect("parsing failed");
1721 }
1722
1723 #[test]
1724 fn test_parse_block_changes() {
1725 let json_data = r#"
1726 {
1727 "extractor": "vm:ambient",
1728 "chain": "ethereum",
1729 "block": {
1730 "number": 123,
1731 "hash": "0x0000000000000000000000000000000000000000000000000000000000000000",
1732 "parent_hash": "0x0000000000000000000000000000000000000000000000000000000000000000",
1733 "chain": "ethereum",
1734 "ts": "2023-09-14T00:00:00"
1735 },
1736 "finalized_block_height": 0,
1737 "revert": false,
1738 "new_tokens": {},
1739 "account_updates": {
1740 "0x7a250d5630b4cf539739df2c5dacb4c659f2488d": {
1741 "address": "0x7a250d5630b4cf539739df2c5dacb4c659f2488d",
1742 "chain": "ethereum",
1743 "slots": {},
1744 "balance": "0x01f4",
1745 "code": "",
1746 "change": "Update"
1747 }
1748 },
1749 "state_updates": {
1750 "component_1": {
1751 "component_id": "component_1",
1752 "updated_attributes": {"attr1": "0x01"},
1753 "deleted_attributes": ["attr2"]
1754 }
1755 },
1756 "new_protocol_components":
1757 { "protocol_1": {
1758 "id": "protocol_1",
1759 "protocol_system": "system_1",
1760 "protocol_type_name": "type_1",
1761 "chain": "ethereum",
1762 "tokens": ["0x01", "0x02"],
1763 "contract_ids": ["0x01", "0x02"],
1764 "static_attributes": {"attr1": "0x01f4"},
1765 "change": "Update",
1766 "creation_tx": "0x01",
1767 "created_at": "2023-09-14T00:00:00"
1768 }
1769 },
1770 "deleted_protocol_components": {},
1771 "component_balances": {
1772 "protocol_1":
1773 {
1774 "0x01": {
1775 "token": "0x01",
1776 "balance": "0xb77831d23691653a01",
1777 "balance_float": 3.3844151001790677e21,
1778 "modify_tx": "0x01",
1779 "component_id": "protocol_1"
1780 }
1781 }
1782 },
1783 "account_balances": {
1784 "0x7a250d5630b4cf539739df2c5dacb4c659f2488d": {
1785 "0x7a250d5630b4cf539739df2c5dacb4c659f2488d": {
1786 "account": "0x7a250d5630b4cf539739df2c5dacb4c659f2488d",
1787 "token": "0x7a250d5630b4cf539739df2c5dacb4c659f2488d",
1788 "balance": "0x01f4",
1789 "modify_tx": "0x01"
1790 }
1791 }
1792 },
1793 "component_tvl": {
1794 "protocol_1": 1000.0
1795 }
1796 }
1797 "#;
1798
1799 serde_json::from_str::<BlockChanges>(json_data).expect("parsing failed");
1800 }
1801
1802 #[test]
1803 fn test_parse_websocket_message() {
1804 let json_data = r#"
1805 {
1806 "subscription_id": "5d23bfbe-89ad-4ea3-8672-dc9e973ac9dc",
1807 "deltas": {
1808 "type": "BlockChanges",
1809 "extractor": "uniswap_v2",
1810 "chain": "ethereum",
1811 "block": {
1812 "number": 19291517,
1813 "hash": "0xbc3ea4896c0be8da6229387a8571b72818aa258daf4fab46471003ad74c4ee83",
1814 "parent_hash": "0x89ca5b8d593574cf6c886f41ef8208bf6bdc1a90ef36046cb8c84bc880b9af8f",
1815 "chain": "ethereum",
1816 "ts": "2024-02-23T16:35:35"
1817 },
1818 "finalized_block_height": 0,
1819 "revert": false,
1820 "new_tokens": {},
1821 "account_updates": {
1822 "0x7a250d5630b4cf539739df2c5dacb4c659f2488d": {
1823 "address": "0x7a250d5630b4cf539739df2c5dacb4c659f2488d",
1824 "chain": "ethereum",
1825 "slots": {},
1826 "balance": "0x01f4",
1827 "code": "",
1828 "change": "Update"
1829 }
1830 },
1831 "state_updates": {
1832 "0xde6faedbcae38eec6d33ad61473a04a6dd7f6e28": {
1833 "component_id": "0xde6faedbcae38eec6d33ad61473a04a6dd7f6e28",
1834 "updated_attributes": {
1835 "reserve0": "0x87f7b5973a7f28a8b32404",
1836 "reserve1": "0x09e9564b11"
1837 },
1838 "deleted_attributes": [ ]
1839 },
1840 "0x99c59000f5a76c54c4fd7d82720c045bdcf1450d": {
1841 "component_id": "0x99c59000f5a76c54c4fd7d82720c045bdcf1450d",
1842 "updated_attributes": {
1843 "reserve1": "0x44d9a8fd662c2f4d03",
1844 "reserve0": "0x500b1261f811d5bf423e"
1845 },
1846 "deleted_attributes": [ ]
1847 }
1848 },
1849 "new_protocol_components": { },
1850 "deleted_protocol_components": { },
1851 "component_balances": {
1852 "0x99c59000f5a76c54c4fd7d82720c045bdcf1450d": {
1853 "0x9012744b7a564623b6c3e40b144fc196bdedf1a9": {
1854 "token": "0x9012744b7a564623b6c3e40b144fc196bdedf1a9",
1855 "balance": "0x500b1261f811d5bf423e",
1856 "balance_float": 3.779935574269033E23,
1857 "modify_tx": "0xe46c4db085fb6c6f3408a65524555797adb264e1d5cf3b66ad154598f85ac4bf",
1858 "component_id": "0x99c59000f5a76c54c4fd7d82720c045bdcf1450d"
1859 },
1860 "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2": {
1861 "token": "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2",
1862 "balance": "0x44d9a8fd662c2f4d03",
1863 "balance_float": 1.270062661329837E21,
1864 "modify_tx": "0xe46c4db085fb6c6f3408a65524555797adb264e1d5cf3b66ad154598f85ac4bf",
1865 "component_id": "0x99c59000f5a76c54c4fd7d82720c045bdcf1450d"
1866 }
1867 }
1868 },
1869 "account_balances": {
1870 "0x7a250d5630b4cf539739df2c5dacb4c659f2488d": {
1871 "0x7a250d5630b4cf539739df2c5dacb4c659f2488d": {
1872 "account": "0x7a250d5630b4cf539739df2c5dacb4c659f2488d",
1873 "token": "0x7a250d5630b4cf539739df2c5dacb4c659f2488d",
1874 "balance": "0x01f4",
1875 "modify_tx": "0x01"
1876 }
1877 }
1878 },
1879 "component_tvl": { }
1880 }
1881 }
1882 "#;
1883 serde_json::from_str::<WebSocketMessage>(json_data).expect("parsing failed");
1884 }
1885
1886 #[test]
1887 fn test_protocol_state_delta_merge_update_delete() {
1888 let mut delta1 = ProtocolStateDelta {
1890 component_id: "Component1".to_string(),
1891 updated_attributes: [("Attribute1".to_string(), Bytes::from("0xbadbabe420"))]
1892 .iter()
1893 .cloned()
1894 .collect(),
1895 deleted_attributes: HashSet::new(),
1896 };
1897 let delta2 = ProtocolStateDelta {
1898 component_id: "Component1".to_string(),
1899 updated_attributes: [("Attribute2".to_string(), Bytes::from("0x0badbabe"))]
1900 .iter()
1901 .cloned()
1902 .collect(),
1903 deleted_attributes: ["Attribute1".to_string()]
1904 .iter()
1905 .cloned()
1906 .collect(),
1907 };
1908 let exp = ProtocolStateDelta {
1909 component_id: "Component1".to_string(),
1910 updated_attributes: [("Attribute2".to_string(), Bytes::from("0x0badbabe"))]
1911 .iter()
1912 .cloned()
1913 .collect(),
1914 deleted_attributes: ["Attribute1".to_string()]
1915 .iter()
1916 .cloned()
1917 .collect(),
1918 };
1919
1920 delta1.merge(&delta2);
1921
1922 assert_eq!(delta1, exp);
1923 }
1924
1925 #[test]
1926 fn test_protocol_state_delta_merge_delete_update() {
1927 let mut delta1 = ProtocolStateDelta {
1929 component_id: "Component1".to_string(),
1930 updated_attributes: HashMap::new(),
1931 deleted_attributes: ["Attribute1".to_string()]
1932 .iter()
1933 .cloned()
1934 .collect(),
1935 };
1936 let delta2 = ProtocolStateDelta {
1937 component_id: "Component1".to_string(),
1938 updated_attributes: [("Attribute1".to_string(), Bytes::from("0x0badbabe"))]
1939 .iter()
1940 .cloned()
1941 .collect(),
1942 deleted_attributes: HashSet::new(),
1943 };
1944 let exp = ProtocolStateDelta {
1945 component_id: "Component1".to_string(),
1946 updated_attributes: [("Attribute1".to_string(), Bytes::from("0x0badbabe"))]
1947 .iter()
1948 .cloned()
1949 .collect(),
1950 deleted_attributes: HashSet::new(),
1951 };
1952
1953 delta1.merge(&delta2);
1954
1955 assert_eq!(delta1, exp);
1956 }
1957
1958 #[test]
1959 fn test_account_update_merge() {
1960 let mut account1 = AccountUpdate::new(
1962 Bytes::from(b"0x1234"),
1963 Chain::Ethereum,
1964 [(Bytes::from("0xaabb"), Bytes::from("0xccdd"))]
1965 .iter()
1966 .cloned()
1967 .collect(),
1968 Some(Bytes::from("0x1000")),
1969 Some(Bytes::from("0xdeadbeaf")),
1970 ChangeType::Creation,
1971 );
1972
1973 let account2 = AccountUpdate::new(
1974 Bytes::from(b"0x1234"), Chain::Ethereum,
1976 [(Bytes::from("0xeeff"), Bytes::from("0x11223344"))]
1977 .iter()
1978 .cloned()
1979 .collect(),
1980 Some(Bytes::from("0x2000")),
1981 Some(Bytes::from("0xcafebabe")),
1982 ChangeType::Update,
1983 );
1984
1985 account1.merge(&account2);
1987
1988 let expected = AccountUpdate::new(
1990 Bytes::from(b"0x1234"), Chain::Ethereum,
1992 [
1993 (Bytes::from("0xaabb"), Bytes::from("0xccdd")), (Bytes::from("0xeeff"), Bytes::from("0x11223344")), ]
1996 .iter()
1997 .cloned()
1998 .collect(),
1999 Some(Bytes::from("0x2000")), Some(Bytes::from("0xcafebabe")), ChangeType::Creation, );
2003
2004 assert_eq!(account1, expected);
2006 }
2007
2008 #[test]
2009 fn test_block_account_changes_merge() {
2010 let old_account_updates: HashMap<Bytes, AccountUpdate> = [(
2012 Bytes::from("0x0011"),
2013 AccountUpdate {
2014 address: Bytes::from("0x00"),
2015 chain: Chain::Ethereum,
2016 slots: [(Bytes::from("0x0022"), Bytes::from("0x0033"))]
2017 .into_iter()
2018 .collect(),
2019 balance: Some(Bytes::from("0x01")),
2020 code: Some(Bytes::from("0x02")),
2021 change: ChangeType::Creation,
2022 },
2023 )]
2024 .into_iter()
2025 .collect();
2026 let new_account_updates: HashMap<Bytes, AccountUpdate> = [(
2027 Bytes::from("0x0011"),
2028 AccountUpdate {
2029 address: Bytes::from("0x00"),
2030 chain: Chain::Ethereum,
2031 slots: [(Bytes::from("0x0044"), Bytes::from("0x0055"))]
2032 .into_iter()
2033 .collect(),
2034 balance: Some(Bytes::from("0x03")),
2035 code: Some(Bytes::from("0x04")),
2036 change: ChangeType::Update,
2037 },
2038 )]
2039 .into_iter()
2040 .collect();
2041 let block_account_changes_initial = BlockChanges::new(
2043 "extractor1",
2044 Chain::Ethereum,
2045 Block::default(),
2046 0,
2047 false,
2048 old_account_updates,
2049 HashMap::new(),
2050 HashMap::new(),
2051 HashMap::new(),
2052 HashMap::new(),
2053 HashMap::new(),
2054 );
2055
2056 let block_account_changes_new = BlockChanges::new(
2057 "extractor2",
2058 Chain::Ethereum,
2059 Block::default(),
2060 0,
2061 true,
2062 new_account_updates,
2063 HashMap::new(),
2064 HashMap::new(),
2065 HashMap::new(),
2066 HashMap::new(),
2067 HashMap::new(),
2068 );
2069
2070 let res = block_account_changes_initial.merge(block_account_changes_new);
2072
2073 let expected_account_updates: HashMap<Bytes, AccountUpdate> = [(
2075 Bytes::from("0x0011"),
2076 AccountUpdate {
2077 address: Bytes::from("0x00"),
2078 chain: Chain::Ethereum,
2079 slots: [
2080 (Bytes::from("0x0044"), Bytes::from("0x0055")),
2081 (Bytes::from("0x0022"), Bytes::from("0x0033")),
2082 ]
2083 .into_iter()
2084 .collect(),
2085 balance: Some(Bytes::from("0x03")),
2086 code: Some(Bytes::from("0x04")),
2087 change: ChangeType::Creation,
2088 },
2089 )]
2090 .into_iter()
2091 .collect();
2092 let block_account_changes_expected = BlockChanges::new(
2093 "extractor1",
2094 Chain::Ethereum,
2095 Block::default(),
2096 0,
2097 true,
2098 expected_account_updates,
2099 HashMap::new(),
2100 HashMap::new(),
2101 HashMap::new(),
2102 HashMap::new(),
2103 HashMap::new(),
2104 );
2105 assert_eq!(res, block_account_changes_expected);
2106 }
2107
2108 #[test]
2109 fn test_block_entity_changes_merge() {
2110 let block_entity_changes_result1 = BlockChanges {
2112 extractor: String::from("extractor1"),
2113 chain: Chain::Ethereum,
2114 block: Block::default(),
2115 revert: false,
2116 new_tokens: HashMap::new(),
2117 state_updates: hashmap! { "state1".to_string() => ProtocolStateDelta::default() },
2118 new_protocol_components: hashmap! { "component1".to_string() => ProtocolComponent::default() },
2119 deleted_protocol_components: HashMap::new(),
2120 component_balances: hashmap! {
2121 "component1".to_string() => TokenBalances(hashmap! {
2122 Bytes::from("0x01") => ComponentBalance {
2123 token: Bytes::from("0x01"),
2124 balance: Bytes::from("0x01"),
2125 balance_float: 1.0,
2126 modify_tx: Bytes::from("0x00"),
2127 component_id: "component1".to_string()
2128 },
2129 Bytes::from("0x02") => ComponentBalance {
2130 token: Bytes::from("0x02"),
2131 balance: Bytes::from("0x02"),
2132 balance_float: 2.0,
2133 modify_tx: Bytes::from("0x00"),
2134 component_id: "component1".to_string()
2135 },
2136 })
2137
2138 },
2139 component_tvl: hashmap! { "tvl1".to_string() => 1000.0 },
2140 ..Default::default()
2141 };
2142 let block_entity_changes_result2 = BlockChanges {
2143 extractor: String::from("extractor2"),
2144 chain: Chain::Ethereum,
2145 block: Block::default(),
2146 revert: true,
2147 new_tokens: HashMap::new(),
2148 state_updates: hashmap! { "state2".to_string() => ProtocolStateDelta::default() },
2149 new_protocol_components: hashmap! { "component2".to_string() => ProtocolComponent::default() },
2150 deleted_protocol_components: hashmap! { "component3".to_string() => ProtocolComponent::default() },
2151 component_balances: hashmap! {
2152 "component1".to_string() => TokenBalances::default(),
2153 "component2".to_string() => TokenBalances::default()
2154 },
2155 component_tvl: hashmap! { "tvl2".to_string() => 2000.0 },
2156 ..Default::default()
2157 };
2158
2159 let res = block_entity_changes_result1.merge(block_entity_changes_result2);
2160
2161 let expected_block_entity_changes_result = BlockChanges {
2162 extractor: String::from("extractor1"),
2163 chain: Chain::Ethereum,
2164 block: Block::default(),
2165 revert: true,
2166 new_tokens: HashMap::new(),
2167 state_updates: hashmap! {
2168 "state1".to_string() => ProtocolStateDelta::default(),
2169 "state2".to_string() => ProtocolStateDelta::default(),
2170 },
2171 new_protocol_components: hashmap! {
2172 "component1".to_string() => ProtocolComponent::default(),
2173 "component2".to_string() => ProtocolComponent::default(),
2174 },
2175 deleted_protocol_components: hashmap! {
2176 "component3".to_string() => ProtocolComponent::default(),
2177 },
2178 component_balances: hashmap! {
2179 "component1".to_string() => TokenBalances(hashmap! {
2180 Bytes::from("0x01") => ComponentBalance {
2181 token: Bytes::from("0x01"),
2182 balance: Bytes::from("0x01"),
2183 balance_float: 1.0,
2184 modify_tx: Bytes::from("0x00"),
2185 component_id: "component1".to_string()
2186 },
2187 Bytes::from("0x02") => ComponentBalance {
2188 token: Bytes::from("0x02"),
2189 balance: Bytes::from("0x02"),
2190 balance_float: 2.0,
2191 modify_tx: Bytes::from("0x00"),
2192 component_id: "component1".to_string()
2193 },
2194 }),
2195 "component2".to_string() => TokenBalances::default(),
2196 },
2197 component_tvl: hashmap! {
2198 "tvl1".to_string() => 1000.0,
2199 "tvl2".to_string() => 2000.0
2200 },
2201 ..Default::default()
2202 };
2203
2204 assert_eq!(res, expected_block_entity_changes_result);
2205 }
2206}