1#![allow(deprecated)]
8use std::{
9 collections::{BTreeMap, HashMap, HashSet},
10 fmt,
11 hash::{Hash, Hasher},
12};
13
14use chrono::{NaiveDateTime, Utc};
15use deepsize::{Context, DeepSizeOf};
16use serde::{de, Deserialize, Deserializer, Serialize};
17use strum_macros::{Display, EnumString};
18use thiserror::Error;
19use utoipa::{IntoParams, ToSchema};
20use uuid::Uuid;
21
22use crate::{
23 models::{
24 self, blockchain::BlockAggregatedChanges, Address, Balance, Code, ComponentId, StoreKey,
25 StoreVal,
26 },
27 serde_primitives::{
28 hex_bytes, hex_bytes_option, hex_hashmap_key, hex_hashmap_key_value, hex_hashmap_value,
29 },
30 Bytes,
31};
32
33#[derive(
35 Debug,
36 Clone,
37 Copy,
38 PartialEq,
39 Eq,
40 Hash,
41 Serialize,
42 Deserialize,
43 EnumString,
44 Display,
45 Default,
46 ToSchema,
47 DeepSizeOf,
48)]
49#[serde(rename_all = "lowercase")]
50#[strum(serialize_all = "lowercase")]
51pub enum Chain {
52 #[default]
53 Ethereum,
54 Starknet,
55 ZkSync,
56 Arbitrum,
57 Base,
58 Bsc,
59 Unichain,
60}
61
62impl From<models::contract::Account> for ResponseAccount {
63 fn from(value: models::contract::Account) -> Self {
64 ResponseAccount::new(
65 value.chain.into(),
66 value.address,
67 value.title,
68 value.slots,
69 value.native_balance,
70 value
71 .token_balances
72 .into_iter()
73 .map(|(k, v)| (k, v.balance))
74 .collect(),
75 value.code,
76 value.code_hash,
77 value.balance_modify_tx,
78 value.code_modify_tx,
79 value.creation_tx,
80 )
81 }
82}
83
84impl From<models::Chain> for Chain {
85 fn from(value: models::Chain) -> Self {
86 match value {
87 models::Chain::Ethereum => Chain::Ethereum,
88 models::Chain::Starknet => Chain::Starknet,
89 models::Chain::ZkSync => Chain::ZkSync,
90 models::Chain::Arbitrum => Chain::Arbitrum,
91 models::Chain::Base => Chain::Base,
92 models::Chain::Bsc => Chain::Bsc,
93 models::Chain::Unichain => Chain::Unichain,
94 }
95 }
96}
97
98#[derive(
99 Debug,
100 PartialEq,
101 Default,
102 Copy,
103 Clone,
104 Deserialize,
105 Serialize,
106 ToSchema,
107 EnumString,
108 Display,
109 DeepSizeOf,
110)]
111pub enum ChangeType {
112 #[default]
113 Update,
114 Deletion,
115 Creation,
116 Unspecified,
117}
118
119impl From<models::ChangeType> for ChangeType {
120 fn from(value: models::ChangeType) -> Self {
121 match value {
122 models::ChangeType::Update => ChangeType::Update,
123 models::ChangeType::Creation => ChangeType::Creation,
124 models::ChangeType::Deletion => ChangeType::Deletion,
125 }
126 }
127}
128
129impl ChangeType {
130 pub fn merge(&self, other: &Self) -> Self {
131 if matches!(self, Self::Creation) {
132 Self::Creation
133 } else {
134 *other
135 }
136 }
137}
138
139#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, Hash, Default)]
140pub struct ExtractorIdentity {
141 pub chain: Chain,
142 pub name: String,
143}
144
145impl ExtractorIdentity {
146 pub fn new(chain: Chain, name: &str) -> Self {
147 Self { chain, name: name.to_owned() }
148 }
149}
150
151impl fmt::Display for ExtractorIdentity {
152 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
153 write!(f, "{}:{}", self.chain, self.name)
154 }
155}
156
157#[derive(Deserialize, Serialize, Debug, PartialEq, Eq)]
159#[serde(tag = "method", rename_all = "lowercase")]
160pub enum Command {
161 Subscribe {
162 extractor_id: ExtractorIdentity,
163 include_state: bool,
164 #[serde(default)]
167 compression: bool,
168 #[serde(default)]
171 partial_blocks: bool,
172 },
173 Unsubscribe {
174 subscription_id: Uuid,
175 },
176}
177
178#[derive(Error, Debug, Serialize, Deserialize, Eq, PartialEq, Clone)]
186pub enum WebsocketError {
187 #[error("Extractor not found: {0}")]
188 ExtractorNotFound(ExtractorIdentity),
189
190 #[error("Subscription not found: {0}")]
191 SubscriptionNotFound(Uuid),
192
193 #[error("Failed to parse JSON: {1}, msg: {0}")]
194 ParseError(String, String),
195
196 #[error("Failed to subscribe to extractor: {0}")]
197 SubscribeError(ExtractorIdentity),
198
199 #[error("Failed to compress message for subscription: {0}, error: {1}")]
200 CompressionError(Uuid, String),
201}
202
203impl From<crate::models::error::WebsocketError> for WebsocketError {
204 fn from(value: crate::models::error::WebsocketError) -> Self {
205 match value {
206 crate::models::error::WebsocketError::ExtractorNotFound(eid) => {
207 Self::ExtractorNotFound(eid.into())
208 }
209 crate::models::error::WebsocketError::SubscriptionNotFound(sid) => {
210 Self::SubscriptionNotFound(sid)
211 }
212 crate::models::error::WebsocketError::ParseError(raw, error) => {
213 Self::ParseError(error.to_string(), raw)
214 }
215 crate::models::error::WebsocketError::SubscribeError(eid) => {
216 Self::SubscribeError(eid.into())
217 }
218 crate::models::error::WebsocketError::CompressionError(sid, error) => {
219 Self::CompressionError(sid, error.to_string())
220 }
221 }
222 }
223}
224
225#[derive(Deserialize, Serialize, Debug, PartialEq, Eq, Clone)]
227#[serde(tag = "method", rename_all = "lowercase")]
228pub enum Response {
229 NewSubscription { extractor_id: ExtractorIdentity, subscription_id: Uuid },
230 SubscriptionEnded { subscription_id: Uuid },
231 Error(WebsocketError),
232}
233
234#[allow(clippy::large_enum_variant)]
236#[derive(Serialize, Deserialize, Debug, Display, Clone)]
237#[serde(untagged)]
238pub enum WebSocketMessage {
239 BlockChanges { subscription_id: Uuid, deltas: BlockChanges },
240 Response(Response),
241}
242
243#[derive(Debug, PartialEq, Clone, Deserialize, Serialize, Default, ToSchema)]
244pub struct Block {
245 pub number: u64,
246 #[serde(with = "hex_bytes")]
247 pub hash: Bytes,
248 #[serde(with = "hex_bytes")]
249 pub parent_hash: Bytes,
250 pub chain: Chain,
251 pub ts: NaiveDateTime,
252}
253
254#[derive(Debug, Serialize, Deserialize, PartialEq, Clone, ToSchema, Eq, Hash, DeepSizeOf)]
255#[serde(deny_unknown_fields)]
256pub struct BlockParam {
257 #[schema(value_type=Option<String>)]
258 #[serde(with = "hex_bytes_option", default)]
259 pub hash: Option<Bytes>,
260 #[deprecated(
261 note = "The `chain` field is deprecated and will be removed in a future version."
262 )]
263 #[serde(default)]
264 pub chain: Option<Chain>,
265 #[serde(default)]
266 pub number: Option<i64>,
267}
268
269impl From<&Block> for BlockParam {
270 fn from(value: &Block) -> Self {
271 BlockParam { hash: Some(value.hash.clone()), chain: None, number: None }
273 }
274}
275
276#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, Default)]
277pub struct TokenBalances(#[serde(with = "hex_hashmap_key")] pub HashMap<Bytes, ComponentBalance>);
278
279impl From<HashMap<Bytes, ComponentBalance>> for TokenBalances {
280 fn from(value: HashMap<Bytes, ComponentBalance>) -> Self {
281 TokenBalances(value)
282 }
283}
284
285#[derive(Debug, PartialEq, Clone, Default, Deserialize, Serialize)]
286pub struct Transaction {
287 #[serde(with = "hex_bytes")]
288 pub hash: Bytes,
289 #[serde(with = "hex_bytes")]
290 pub block_hash: Bytes,
291 #[serde(with = "hex_bytes")]
292 pub from: Bytes,
293 #[serde(with = "hex_bytes_option")]
294 pub to: Option<Bytes>,
295 pub index: u64,
296}
297
298impl Transaction {
299 pub fn new(hash: Bytes, block_hash: Bytes, from: Bytes, to: Option<Bytes>, index: u64) -> Self {
300 Self { hash, block_hash, from, to, index }
301 }
302}
303
304#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, Default)]
306pub struct BlockChanges {
307 pub extractor: String,
308 pub chain: Chain,
309 pub block: Block,
310 pub finalized_block_height: u64,
311 pub revert: bool,
312 #[serde(with = "hex_hashmap_key", default)]
313 pub new_tokens: HashMap<Bytes, ResponseToken>,
314 #[serde(alias = "account_deltas", with = "hex_hashmap_key")]
315 pub account_updates: HashMap<Bytes, AccountUpdate>,
316 #[serde(alias = "state_deltas")]
317 pub state_updates: HashMap<String, ProtocolStateDelta>,
318 pub new_protocol_components: HashMap<String, ProtocolComponent>,
319 pub deleted_protocol_components: HashMap<String, ProtocolComponent>,
320 pub component_balances: HashMap<String, TokenBalances>,
321 pub account_balances: HashMap<Bytes, HashMap<Bytes, AccountBalance>>,
322 pub component_tvl: HashMap<String, f64>,
323 pub dci_update: DCIUpdate,
324 #[serde(default, skip_serializing_if = "Option::is_none")]
326 pub partial_block_index: Option<u32>,
327}
328
329impl BlockChanges {
330 #[allow(clippy::too_many_arguments)]
331 pub fn new(
332 extractor: &str,
333 chain: Chain,
334 block: Block,
335 finalized_block_height: u64,
336 revert: bool,
337 account_updates: HashMap<Bytes, AccountUpdate>,
338 state_updates: HashMap<String, ProtocolStateDelta>,
339 new_protocol_components: HashMap<String, ProtocolComponent>,
340 deleted_protocol_components: HashMap<String, ProtocolComponent>,
341 component_balances: HashMap<String, HashMap<Bytes, ComponentBalance>>,
342 account_balances: HashMap<Bytes, HashMap<Bytes, AccountBalance>>,
343 dci_update: DCIUpdate,
344 ) -> Self {
345 BlockChanges {
346 extractor: extractor.to_owned(),
347 chain,
348 block,
349 finalized_block_height,
350 revert,
351 new_tokens: HashMap::new(),
352 account_updates,
353 state_updates,
354 new_protocol_components,
355 deleted_protocol_components,
356 component_balances: component_balances
357 .into_iter()
358 .map(|(k, v)| (k, v.into()))
359 .collect(),
360 account_balances,
361 component_tvl: HashMap::new(),
362 dci_update,
363 partial_block_index: None,
364 }
365 }
366
367 pub fn merge(mut self, other: Self) -> Self {
368 other
369 .account_updates
370 .into_iter()
371 .for_each(|(k, v)| {
372 self.account_updates
373 .entry(k)
374 .and_modify(|e| {
375 e.merge(&v);
376 })
377 .or_insert(v);
378 });
379
380 other
381 .state_updates
382 .into_iter()
383 .for_each(|(k, v)| {
384 self.state_updates
385 .entry(k)
386 .and_modify(|e| {
387 e.merge(&v);
388 })
389 .or_insert(v);
390 });
391
392 other
393 .component_balances
394 .into_iter()
395 .for_each(|(k, v)| {
396 self.component_balances
397 .entry(k)
398 .and_modify(|e| e.0.extend(v.0.clone()))
399 .or_insert_with(|| v);
400 });
401
402 other
403 .account_balances
404 .into_iter()
405 .for_each(|(k, v)| {
406 self.account_balances
407 .entry(k)
408 .and_modify(|e| e.extend(v.clone()))
409 .or_insert(v);
410 });
411
412 self.component_tvl
413 .extend(other.component_tvl);
414 self.new_protocol_components
415 .extend(other.new_protocol_components);
416 self.deleted_protocol_components
417 .extend(other.deleted_protocol_components);
418 self.revert = other.revert;
419 self.block = other.block;
420
421 self
422 }
423
424 pub fn get_block(&self) -> &Block {
425 &self.block
426 }
427
428 pub fn is_revert(&self) -> bool {
429 self.revert
430 }
431
432 pub fn filter_by_component<F: Fn(&str) -> bool>(&mut self, keep: F) {
433 self.state_updates
434 .retain(|k, _| keep(k));
435 self.component_balances
436 .retain(|k, _| keep(k));
437 self.component_tvl
438 .retain(|k, _| keep(k));
439 }
440
441 pub fn filter_by_contract<F: Fn(&Bytes) -> bool>(&mut self, keep: F) {
442 self.account_updates
443 .retain(|k, _| keep(k));
444 self.account_balances
445 .retain(|k, _| keep(k));
446 }
447
448 pub fn n_changes(&self) -> usize {
449 self.account_updates.len() + self.state_updates.len()
450 }
451
452 pub fn drop_state(&self) -> Self {
453 Self {
454 extractor: self.extractor.clone(),
455 chain: self.chain,
456 block: self.block.clone(),
457 finalized_block_height: self.finalized_block_height,
458 revert: self.revert,
459 new_tokens: self.new_tokens.clone(),
460 account_updates: HashMap::new(),
461 state_updates: HashMap::new(),
462 new_protocol_components: self.new_protocol_components.clone(),
463 deleted_protocol_components: self.deleted_protocol_components.clone(),
464 component_balances: self.component_balances.clone(),
465 account_balances: self.account_balances.clone(),
466 component_tvl: self.component_tvl.clone(),
467 dci_update: self.dci_update.clone(),
468 partial_block_index: self.partial_block_index,
469 }
470 }
471
472 pub fn is_partial_block(&self) -> bool {
473 self.partial_block_index.is_some()
474 }
475}
476
477impl From<models::blockchain::Block> for Block {
478 fn from(value: models::blockchain::Block) -> Self {
479 Self {
480 number: value.number,
481 hash: value.hash,
482 parent_hash: value.parent_hash,
483 chain: value.chain.into(),
484 ts: value.ts,
485 }
486 }
487}
488
489impl From<models::protocol::ComponentBalance> for ComponentBalance {
490 fn from(value: models::protocol::ComponentBalance) -> Self {
491 Self {
492 token: value.token,
493 balance: value.balance,
494 balance_float: value.balance_float,
495 modify_tx: value.modify_tx,
496 component_id: value.component_id,
497 }
498 }
499}
500
501impl From<models::contract::AccountBalance> for AccountBalance {
502 fn from(value: models::contract::AccountBalance) -> Self {
503 Self {
504 account: value.account,
505 token: value.token,
506 balance: value.balance,
507 modify_tx: value.modify_tx,
508 }
509 }
510}
511
512impl From<BlockAggregatedChanges> for BlockChanges {
513 fn from(value: BlockAggregatedChanges) -> Self {
514 Self {
515 extractor: value.extractor,
516 chain: value.chain.into(),
517 block: value.block.into(),
518 finalized_block_height: value.finalized_block_height,
519 revert: value.revert,
520 account_updates: value
521 .account_deltas
522 .into_iter()
523 .map(|(k, v)| (k, v.into()))
524 .collect(),
525 state_updates: value
526 .state_deltas
527 .into_iter()
528 .map(|(k, v)| (k, v.into()))
529 .collect(),
530 new_protocol_components: value
531 .new_protocol_components
532 .into_iter()
533 .map(|(k, v)| (k, v.into()))
534 .collect(),
535 deleted_protocol_components: value
536 .deleted_protocol_components
537 .into_iter()
538 .map(|(k, v)| (k, v.into()))
539 .collect(),
540 component_balances: value
541 .component_balances
542 .into_iter()
543 .map(|(component_id, v)| {
544 let balances: HashMap<Bytes, ComponentBalance> = v
545 .into_iter()
546 .map(|(k, v)| (k, ComponentBalance::from(v)))
547 .collect();
548 (component_id, balances.into())
549 })
550 .collect(),
551 account_balances: value
552 .account_balances
553 .into_iter()
554 .map(|(k, v)| {
555 (
556 k,
557 v.into_iter()
558 .map(|(k, v)| (k, v.into()))
559 .collect(),
560 )
561 })
562 .collect(),
563 dci_update: value.dci_update.into(),
564 new_tokens: value
565 .new_tokens
566 .into_iter()
567 .map(|(k, v)| (k, v.into()))
568 .collect(),
569 component_tvl: value.component_tvl,
570 partial_block_index: value.partial_block_index,
571 }
572 }
573}
574
575#[derive(PartialEq, Serialize, Deserialize, Clone, Debug, ToSchema)]
576pub struct AccountUpdate {
577 #[serde(with = "hex_bytes")]
578 #[schema(value_type=Vec<String>)]
579 pub address: Bytes,
580 pub chain: Chain,
581 #[serde(with = "hex_hashmap_key_value")]
582 #[schema(value_type=HashMap<String, String>)]
583 pub slots: HashMap<Bytes, Bytes>,
584 #[serde(with = "hex_bytes_option")]
585 #[schema(value_type=Option<String>)]
586 pub balance: Option<Bytes>,
587 #[serde(with = "hex_bytes_option")]
588 #[schema(value_type=Option<String>)]
589 pub code: Option<Bytes>,
590 pub change: ChangeType,
591}
592
593impl AccountUpdate {
594 pub fn new(
595 address: Bytes,
596 chain: Chain,
597 slots: HashMap<Bytes, Bytes>,
598 balance: Option<Bytes>,
599 code: Option<Bytes>,
600 change: ChangeType,
601 ) -> Self {
602 Self { address, chain, slots, balance, code, change }
603 }
604
605 pub fn merge(&mut self, other: &Self) {
606 self.slots.extend(
607 other
608 .slots
609 .iter()
610 .map(|(k, v)| (k.clone(), v.clone())),
611 );
612 self.balance.clone_from(&other.balance);
613 self.code.clone_from(&other.code);
614 self.change = self.change.merge(&other.change);
615 }
616}
617
618impl From<models::contract::AccountDelta> for AccountUpdate {
619 fn from(value: models::contract::AccountDelta) -> Self {
620 let code = value.code().clone();
621 let change_type = value.change_type().into();
622 AccountUpdate::new(
623 value.address,
624 value.chain.into(),
625 value
626 .slots
627 .into_iter()
628 .map(|(k, v)| (k, v.unwrap_or_default()))
629 .collect(),
630 value.balance,
631 code,
632 change_type,
633 )
634 }
635}
636
637#[derive(Debug, Clone, PartialEq, Default, Deserialize, Serialize, ToSchema)]
639pub struct ProtocolComponent {
640 pub id: String,
642 pub protocol_system: String,
644 pub protocol_type_name: String,
646 pub chain: Chain,
647 #[schema(value_type=Vec<String>)]
649 pub tokens: Vec<Bytes>,
650 #[serde(alias = "contract_addresses")]
653 #[schema(value_type=Vec<String>)]
654 pub contract_ids: Vec<Bytes>,
655 #[serde(with = "hex_hashmap_value")]
657 #[schema(value_type=HashMap<String, String>)]
658 pub static_attributes: HashMap<String, Bytes>,
659 #[serde(default)]
661 pub change: ChangeType,
662 #[serde(with = "hex_bytes")]
664 #[schema(value_type=String)]
665 pub creation_tx: Bytes,
666 pub created_at: NaiveDateTime,
668}
669
670impl DeepSizeOf for ProtocolComponent {
672 fn deep_size_of_children(&self, ctx: &mut Context) -> usize {
673 self.id.deep_size_of_children(ctx) +
674 self.protocol_system
675 .deep_size_of_children(ctx) +
676 self.protocol_type_name
677 .deep_size_of_children(ctx) +
678 self.chain.deep_size_of_children(ctx) +
679 self.tokens.deep_size_of_children(ctx) +
680 self.contract_ids
681 .deep_size_of_children(ctx) +
682 self.static_attributes
683 .deep_size_of_children(ctx) +
684 self.change.deep_size_of_children(ctx) +
685 self.creation_tx
686 .deep_size_of_children(ctx)
687 }
688}
689
690impl From<models::protocol::ProtocolComponent> for ProtocolComponent {
691 fn from(value: models::protocol::ProtocolComponent) -> Self {
692 Self {
693 id: value.id,
694 protocol_system: value.protocol_system,
695 protocol_type_name: value.protocol_type_name,
696 chain: value.chain.into(),
697 tokens: value.tokens,
698 contract_ids: value.contract_addresses,
699 static_attributes: value.static_attributes,
700 change: value.change.into(),
701 creation_tx: value.creation_tx,
702 created_at: value.created_at,
703 }
704 }
705}
706
707#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, Default)]
708pub struct ComponentBalance {
709 #[serde(with = "hex_bytes")]
710 pub token: Bytes,
711 pub balance: Bytes,
712 pub balance_float: f64,
713 #[serde(with = "hex_bytes")]
714 pub modify_tx: Bytes,
715 pub component_id: String,
716}
717
718#[derive(Debug, PartialEq, Clone, Default, Serialize, Deserialize, ToSchema)]
719pub struct ProtocolStateDelta {
721 pub component_id: String,
722 #[schema(value_type=HashMap<String, String>)]
723 pub updated_attributes: HashMap<String, Bytes>,
724 pub deleted_attributes: HashSet<String>,
725}
726
727impl From<models::protocol::ProtocolComponentStateDelta> for ProtocolStateDelta {
728 fn from(value: models::protocol::ProtocolComponentStateDelta) -> Self {
729 Self {
730 component_id: value.component_id,
731 updated_attributes: value.updated_attributes,
732 deleted_attributes: value.deleted_attributes,
733 }
734 }
735}
736
737impl ProtocolStateDelta {
738 pub fn merge(&mut self, other: &Self) {
757 self.updated_attributes
759 .retain(|k, _| !other.deleted_attributes.contains(k));
760
761 self.deleted_attributes.retain(|attr| {
763 !other
764 .updated_attributes
765 .contains_key(attr)
766 });
767
768 self.updated_attributes.extend(
770 other
771 .updated_attributes
772 .iter()
773 .map(|(k, v)| (k.clone(), v.clone())),
774 );
775
776 self.deleted_attributes
778 .extend(other.deleted_attributes.iter().cloned());
779 }
780}
781
782#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, ToSchema, Eq, Hash, DeepSizeOf)]
784#[serde(deny_unknown_fields)]
785pub struct PaginationParams {
786 #[serde(default)]
788 pub page: i64,
789 #[serde(default)]
791 #[schema(default = 100)]
792 pub page_size: i64,
793}
794
795impl PaginationParams {
796 pub fn new(page: i64, page_size: i64) -> Self {
797 Self { page, page_size }
798 }
799}
800
801impl Default for PaginationParams {
802 fn default() -> Self {
803 PaginationParams { page: 0, page_size: 100 }
804 }
805}
806
807pub trait PaginationLimits {
812 const MAX_PAGE_SIZE_COMPRESSED: i64;
814
815 const MAX_PAGE_SIZE_UNCOMPRESSED: i64;
817
818 fn effective_max_page_size(compression: bool) -> i64 {
820 if compression {
821 Self::MAX_PAGE_SIZE_COMPRESSED
822 } else {
823 Self::MAX_PAGE_SIZE_UNCOMPRESSED
824 }
825 }
826
827 fn pagination(&self) -> &PaginationParams;
829}
830
831macro_rules! impl_pagination_limits {
838 ($type:ty, compressed = $comp:expr, uncompressed = $uncomp:expr) => {
839 impl $crate::dto::PaginationLimits for $type {
840 const MAX_PAGE_SIZE_COMPRESSED: i64 = $comp;
841 const MAX_PAGE_SIZE_UNCOMPRESSED: i64 = $uncomp;
842
843 fn pagination(&self) -> &$crate::dto::PaginationParams {
844 &self.pagination
845 }
846 }
847 };
848}
849
850#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, ToSchema, Eq, Hash, DeepSizeOf)]
851#[serde(deny_unknown_fields)]
852pub struct PaginationResponse {
853 pub page: i64,
854 pub page_size: i64,
855 pub total: i64,
857}
858
859impl PaginationResponse {
861 pub fn new(page: i64, page_size: i64, total: i64) -> Self {
862 Self { page, page_size, total }
863 }
864
865 pub fn total_pages(&self) -> i64 {
866 (self.total + self.page_size - 1) / self.page_size
868 }
869}
870
871#[derive(
872 Clone, Serialize, Debug, Default, Deserialize, PartialEq, ToSchema, Eq, Hash, DeepSizeOf,
873)]
874#[serde(deny_unknown_fields)]
875pub struct StateRequestBody {
876 #[serde(alias = "contractIds")]
878 #[schema(value_type=Option<Vec<String>>)]
879 pub contract_ids: Option<Vec<Bytes>>,
880 #[serde(alias = "protocolSystem", default)]
883 pub protocol_system: String,
884 #[serde(default = "VersionParam::default")]
885 pub version: VersionParam,
886 #[serde(default)]
887 pub chain: Chain,
888 #[serde(default)]
889 pub pagination: PaginationParams,
890}
891
892impl_pagination_limits!(StateRequestBody, compressed = 1200, uncompressed = 100);
894
895impl StateRequestBody {
896 pub fn new(
897 contract_ids: Option<Vec<Bytes>>,
898 protocol_system: String,
899 version: VersionParam,
900 chain: Chain,
901 pagination: PaginationParams,
902 ) -> Self {
903 Self { contract_ids, protocol_system, version, chain, pagination }
904 }
905
906 pub fn from_block(protocol_system: &str, block: BlockParam) -> Self {
907 Self {
908 contract_ids: None,
909 protocol_system: protocol_system.to_string(),
910 version: VersionParam { timestamp: None, block: Some(block.clone()) },
911 chain: block.chain.unwrap_or_default(),
912 pagination: PaginationParams::default(),
913 }
914 }
915
916 pub fn from_timestamp(protocol_system: &str, timestamp: NaiveDateTime, chain: Chain) -> Self {
917 Self {
918 contract_ids: None,
919 protocol_system: protocol_system.to_string(),
920 version: VersionParam { timestamp: Some(timestamp), block: None },
921 chain,
922 pagination: PaginationParams::default(),
923 }
924 }
925}
926
927#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, ToSchema, DeepSizeOf)]
929pub struct StateRequestResponse {
930 pub accounts: Vec<ResponseAccount>,
931 pub pagination: PaginationResponse,
932}
933
934impl StateRequestResponse {
935 pub fn new(accounts: Vec<ResponseAccount>, pagination: PaginationResponse) -> Self {
936 Self { accounts, pagination }
937 }
938}
939
940#[derive(PartialEq, Clone, Serialize, Deserialize, Default, ToSchema, DeepSizeOf)]
941#[serde(rename = "Account")]
942pub struct ResponseAccount {
946 pub chain: Chain,
947 #[schema(value_type=String, example="0xc9f2e6ea1637E499406986ac50ddC92401ce1f58")]
949 #[serde(with = "hex_bytes")]
950 pub address: Bytes,
951 #[schema(value_type=String, example="Protocol Vault")]
953 pub title: String,
954 #[schema(value_type=HashMap<String, String>, example=json!({"0x....": "0x...."}))]
956 #[serde(with = "hex_hashmap_key_value")]
957 pub slots: HashMap<Bytes, Bytes>,
958 #[schema(value_type=String, example="0x00")]
960 #[serde(with = "hex_bytes")]
961 pub native_balance: Bytes,
962 #[schema(value_type=HashMap<String, String>, example=json!({"0x....": "0x...."}))]
965 #[serde(with = "hex_hashmap_key_value")]
966 pub token_balances: HashMap<Bytes, Bytes>,
967 #[schema(value_type=String, example="0xBADBABE")]
969 #[serde(with = "hex_bytes")]
970 pub code: Bytes,
971 #[schema(value_type=String, example="0x123456789")]
973 #[serde(with = "hex_bytes")]
974 pub code_hash: Bytes,
975 #[schema(value_type=String, example="0x8f1133bfb054a23aedfe5d25b1d81b96195396d8b88bd5d4bcf865fc1ae2c3f4")]
977 #[serde(with = "hex_bytes")]
978 pub balance_modify_tx: Bytes,
979 #[schema(value_type=String, example="0x8f1133bfb054a23aedfe5d25b1d81b96195396d8b88bd5d4bcf865fc1ae2c3f4")]
981 #[serde(with = "hex_bytes")]
982 pub code_modify_tx: Bytes,
983 #[deprecated(note = "The `creation_tx` field is deprecated.")]
985 #[schema(value_type=Option<String>, example="0x8f1133bfb054a23aedfe5d25b1d81b96195396d8b88bd5d4bcf865fc1ae2c3f4")]
986 #[serde(with = "hex_bytes_option")]
987 pub creation_tx: Option<Bytes>,
988}
989
990impl ResponseAccount {
991 #[allow(clippy::too_many_arguments)]
992 pub fn new(
993 chain: Chain,
994 address: Bytes,
995 title: String,
996 slots: HashMap<Bytes, Bytes>,
997 native_balance: Bytes,
998 token_balances: HashMap<Bytes, Bytes>,
999 code: Bytes,
1000 code_hash: Bytes,
1001 balance_modify_tx: Bytes,
1002 code_modify_tx: Bytes,
1003 creation_tx: Option<Bytes>,
1004 ) -> Self {
1005 Self {
1006 chain,
1007 address,
1008 title,
1009 slots,
1010 native_balance,
1011 token_balances,
1012 code,
1013 code_hash,
1014 balance_modify_tx,
1015 code_modify_tx,
1016 creation_tx,
1017 }
1018 }
1019}
1020
1021impl fmt::Debug for ResponseAccount {
1023 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1024 f.debug_struct("ResponseAccount")
1025 .field("chain", &self.chain)
1026 .field("address", &self.address)
1027 .field("title", &self.title)
1028 .field("slots", &self.slots)
1029 .field("native_balance", &self.native_balance)
1030 .field("token_balances", &self.token_balances)
1031 .field("code", &format!("[{} bytes]", self.code.len()))
1032 .field("code_hash", &self.code_hash)
1033 .field("balance_modify_tx", &self.balance_modify_tx)
1034 .field("code_modify_tx", &self.code_modify_tx)
1035 .field("creation_tx", &self.creation_tx)
1036 .finish()
1037 }
1038}
1039
1040#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, Default)]
1041pub struct AccountBalance {
1042 #[serde(with = "hex_bytes")]
1043 pub account: Bytes,
1044 #[serde(with = "hex_bytes")]
1045 pub token: Bytes,
1046 #[serde(with = "hex_bytes")]
1047 pub balance: Bytes,
1048 #[serde(with = "hex_bytes")]
1049 pub modify_tx: Bytes,
1050}
1051
1052#[derive(Debug, Serialize, Deserialize, PartialEq, Clone, ToSchema)]
1053#[serde(deny_unknown_fields)]
1054pub struct ContractId {
1055 #[serde(with = "hex_bytes")]
1056 #[schema(value_type=String)]
1057 pub address: Bytes,
1058 pub chain: Chain,
1059}
1060
1061impl ContractId {
1063 pub fn new(chain: Chain, address: Bytes) -> Self {
1064 Self { address, chain }
1065 }
1066
1067 pub fn address(&self) -> &Bytes {
1068 &self.address
1069 }
1070}
1071
1072impl fmt::Display for ContractId {
1073 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1074 write!(f, "{:?}: 0x{}", self.chain, hex::encode(&self.address))
1075 }
1076}
1077
1078#[derive(Debug, Serialize, Deserialize, PartialEq, Clone, ToSchema, Eq, Hash)]
1085#[serde(deny_unknown_fields)]
1086pub struct VersionParam {
1087 pub timestamp: Option<NaiveDateTime>,
1088 pub block: Option<BlockParam>,
1089}
1090
1091impl DeepSizeOf for VersionParam {
1092 fn deep_size_of_children(&self, ctx: &mut Context) -> usize {
1093 if let Some(block) = &self.block {
1094 return block.deep_size_of_children(ctx);
1095 }
1096
1097 0
1098 }
1099}
1100
1101impl VersionParam {
1102 pub fn new(timestamp: Option<NaiveDateTime>, block: Option<BlockParam>) -> Self {
1103 Self { timestamp, block }
1104 }
1105}
1106
1107impl Default for VersionParam {
1108 fn default() -> Self {
1109 VersionParam { timestamp: Some(Utc::now().naive_utc()), block: None }
1110 }
1111}
1112
1113#[deprecated(note = "Use StateRequestBody instead")]
1114#[derive(Serialize, Deserialize, Default, Debug, IntoParams)]
1115pub struct StateRequestParameters {
1116 #[param(default = 0)]
1118 pub tvl_gt: Option<u64>,
1119 #[param(default = 0)]
1121 pub inertia_min_gt: Option<u64>,
1122 #[serde(default = "default_include_balances_flag")]
1124 pub include_balances: bool,
1125 #[serde(default)]
1126 pub pagination: PaginationParams,
1127}
1128
1129impl StateRequestParameters {
1130 pub fn new(include_balances: bool) -> Self {
1131 Self {
1132 tvl_gt: None,
1133 inertia_min_gt: None,
1134 include_balances,
1135 pagination: PaginationParams::default(),
1136 }
1137 }
1138
1139 pub fn to_query_string(&self) -> String {
1140 let mut parts = vec![format!("include_balances={}", self.include_balances)];
1141
1142 if let Some(tvl_gt) = self.tvl_gt {
1143 parts.push(format!("tvl_gt={tvl_gt}"));
1144 }
1145
1146 if let Some(inertia) = self.inertia_min_gt {
1147 parts.push(format!("inertia_min_gt={inertia}"));
1148 }
1149
1150 let mut res = parts.join("&");
1151 if !res.is_empty() {
1152 res = format!("?{res}");
1153 }
1154 res
1155 }
1156}
1157
1158#[derive(
1159 Serialize, Deserialize, Debug, Default, PartialEq, ToSchema, Eq, Hash, Clone, DeepSizeOf,
1160)]
1161#[serde(deny_unknown_fields)]
1162pub struct TokensRequestBody {
1163 #[serde(alias = "tokenAddresses")]
1165 #[schema(value_type=Option<Vec<String>>)]
1166 pub token_addresses: Option<Vec<Bytes>>,
1167 #[serde(default)]
1175 pub min_quality: Option<i32>,
1176 #[serde(default)]
1178 pub traded_n_days_ago: Option<u64>,
1179 #[serde(default)]
1181 pub pagination: PaginationParams,
1182 #[serde(default)]
1184 pub chain: Chain,
1185}
1186
1187impl_pagination_limits!(TokensRequestBody, compressed = 12900, uncompressed = 3000);
1189
1190#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, ToSchema, Eq, Hash, DeepSizeOf)]
1192pub struct TokensRequestResponse {
1193 pub tokens: Vec<ResponseToken>,
1194 pub pagination: PaginationResponse,
1195}
1196
1197impl TokensRequestResponse {
1198 pub fn new(tokens: Vec<ResponseToken>, pagination_request: &PaginationResponse) -> Self {
1199 Self { tokens, pagination: pagination_request.clone() }
1200 }
1201}
1202
1203#[derive(
1204 PartialEq, Debug, Clone, Serialize, Deserialize, Default, ToSchema, Eq, Hash, DeepSizeOf,
1205)]
1206#[serde(rename = "Token")]
1207pub struct ResponseToken {
1209 pub chain: Chain,
1210 #[schema(value_type=String, example="0xc9f2e6ea1637E499406986ac50ddC92401ce1f58")]
1212 #[serde(with = "hex_bytes")]
1213 pub address: Bytes,
1214 #[schema(value_type=String, example="WETH")]
1216 pub symbol: String,
1217 pub decimals: u32,
1219 pub tax: u64,
1221 pub gas: Vec<Option<u64>>,
1223 pub quality: u32,
1231}
1232
1233impl From<models::token::Token> for ResponseToken {
1234 fn from(value: models::token::Token) -> Self {
1235 Self {
1236 chain: value.chain.into(),
1237 address: value.address,
1238 symbol: value.symbol,
1239 decimals: value.decimals,
1240 tax: value.tax,
1241 gas: value.gas,
1242 quality: value.quality,
1243 }
1244 }
1245}
1246
1247#[derive(Serialize, Deserialize, Debug, Default, ToSchema, Clone, DeepSizeOf)]
1248#[serde(deny_unknown_fields)]
1249pub struct ProtocolComponentsRequestBody {
1250 pub protocol_system: String,
1253 #[schema(value_type=Option<Vec<String>>)]
1255 #[serde(alias = "componentAddresses")]
1256 pub component_ids: Option<Vec<ComponentId>>,
1257 #[serde(default)]
1260 pub tvl_gt: Option<f64>,
1261 #[serde(default)]
1262 pub chain: Chain,
1263 #[serde(default)]
1265 pub pagination: PaginationParams,
1266}
1267
1268impl_pagination_limits!(ProtocolComponentsRequestBody, compressed = 2550, uncompressed = 500);
1270
1271impl PartialEq for ProtocolComponentsRequestBody {
1273 fn eq(&self, other: &Self) -> bool {
1274 let tvl_close_enough = match (self.tvl_gt, other.tvl_gt) {
1275 (Some(a), Some(b)) => (a - b).abs() < 1e-6,
1276 (None, None) => true,
1277 _ => false,
1278 };
1279
1280 self.protocol_system == other.protocol_system &&
1281 self.component_ids == other.component_ids &&
1282 tvl_close_enough &&
1283 self.chain == other.chain &&
1284 self.pagination == other.pagination
1285 }
1286}
1287
1288impl Eq for ProtocolComponentsRequestBody {}
1290
1291impl Hash for ProtocolComponentsRequestBody {
1292 fn hash<H: Hasher>(&self, state: &mut H) {
1293 self.protocol_system.hash(state);
1294 self.component_ids.hash(state);
1295
1296 if let Some(tvl) = self.tvl_gt {
1298 tvl.to_bits().hash(state);
1300 } else {
1301 state.write_u8(0);
1303 }
1304
1305 self.chain.hash(state);
1306 self.pagination.hash(state);
1307 }
1308}
1309
1310impl ProtocolComponentsRequestBody {
1311 pub fn system_filtered(system: &str, tvl_gt: Option<f64>, chain: Chain) -> Self {
1312 Self {
1313 protocol_system: system.to_string(),
1314 component_ids: None,
1315 tvl_gt,
1316 chain,
1317 pagination: Default::default(),
1318 }
1319 }
1320
1321 pub fn id_filtered(system: &str, ids: Vec<String>, chain: Chain) -> Self {
1322 Self {
1323 protocol_system: system.to_string(),
1324 component_ids: Some(ids),
1325 tvl_gt: None,
1326 chain,
1327 pagination: Default::default(),
1328 }
1329 }
1330}
1331
1332impl ProtocolComponentsRequestBody {
1333 pub fn new(
1334 protocol_system: String,
1335 component_ids: Option<Vec<String>>,
1336 tvl_gt: Option<f64>,
1337 chain: Chain,
1338 pagination: PaginationParams,
1339 ) -> Self {
1340 Self { protocol_system, component_ids, tvl_gt, chain, pagination }
1341 }
1342}
1343
1344#[deprecated(note = "Use ProtocolComponentsRequestBody instead")]
1345#[derive(Serialize, Deserialize, Default, Debug, IntoParams)]
1346pub struct ProtocolComponentRequestParameters {
1347 #[param(default = 0)]
1349 pub tvl_gt: Option<f64>,
1350}
1351
1352impl ProtocolComponentRequestParameters {
1353 pub fn tvl_filtered(min_tvl: f64) -> Self {
1354 Self { tvl_gt: Some(min_tvl) }
1355 }
1356}
1357
1358impl ProtocolComponentRequestParameters {
1359 pub fn to_query_string(&self) -> String {
1360 if let Some(tvl_gt) = self.tvl_gt {
1361 return format!("?tvl_gt={tvl_gt}");
1362 }
1363 String::new()
1364 }
1365}
1366
1367#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, ToSchema, DeepSizeOf)]
1369pub struct ProtocolComponentRequestResponse {
1370 pub protocol_components: Vec<ProtocolComponent>,
1371 pub pagination: PaginationResponse,
1372}
1373
1374impl ProtocolComponentRequestResponse {
1375 pub fn new(
1376 protocol_components: Vec<ProtocolComponent>,
1377 pagination: PaginationResponse,
1378 ) -> Self {
1379 Self { protocol_components, pagination }
1380 }
1381}
1382
1383#[derive(Debug, Serialize, Deserialize, PartialEq, Clone, ToSchema, Eq, Hash)]
1384#[serde(deny_unknown_fields)]
1385#[deprecated]
1386pub struct ProtocolId {
1387 pub id: String,
1388 pub chain: Chain,
1389}
1390
1391impl From<ProtocolId> for String {
1392 fn from(protocol_id: ProtocolId) -> Self {
1393 protocol_id.id
1394 }
1395}
1396
1397impl AsRef<str> for ProtocolId {
1398 fn as_ref(&self) -> &str {
1399 &self.id
1400 }
1401}
1402
1403#[derive(Debug, Clone, PartialEq, Default, Deserialize, Serialize, ToSchema, DeepSizeOf)]
1405pub struct ResponseProtocolState {
1406 pub component_id: String,
1408 #[schema(value_type=HashMap<String, String>)]
1411 #[serde(with = "hex_hashmap_value")]
1412 pub attributes: HashMap<String, Bytes>,
1413 #[schema(value_type=HashMap<String, String>)]
1415 #[serde(with = "hex_hashmap_key_value")]
1416 pub balances: HashMap<Bytes, Bytes>,
1417}
1418
1419impl From<models::protocol::ProtocolComponentState> for ResponseProtocolState {
1420 fn from(value: models::protocol::ProtocolComponentState) -> Self {
1421 Self {
1422 component_id: value.component_id,
1423 attributes: value.attributes,
1424 balances: value.balances,
1425 }
1426 }
1427}
1428
1429fn default_include_balances_flag() -> bool {
1430 true
1431}
1432
1433#[derive(Clone, Debug, Serialize, PartialEq, ToSchema, Default, Eq, Hash, DeepSizeOf)]
1435#[serde(deny_unknown_fields)]
1436pub struct ProtocolStateRequestBody {
1437 #[serde(alias = "protocolIds")]
1439 pub protocol_ids: Option<Vec<String>>,
1440 #[serde(alias = "protocolSystem")]
1443 pub protocol_system: String,
1444 #[serde(default)]
1445 pub chain: Chain,
1446 #[serde(default = "default_include_balances_flag")]
1448 pub include_balances: bool,
1449 #[serde(default = "VersionParam::default")]
1450 pub version: VersionParam,
1451 #[serde(default)]
1452 pub pagination: PaginationParams,
1453}
1454
1455impl_pagination_limits!(ProtocolStateRequestBody, compressed = 360, uncompressed = 100);
1457
1458impl ProtocolStateRequestBody {
1459 pub fn id_filtered<I, T>(ids: I) -> Self
1460 where
1461 I: IntoIterator<Item = T>,
1462 T: Into<String>,
1463 {
1464 Self {
1465 protocol_ids: Some(
1466 ids.into_iter()
1467 .map(Into::into)
1468 .collect(),
1469 ),
1470 ..Default::default()
1471 }
1472 }
1473}
1474
1475impl<'de> Deserialize<'de> for ProtocolStateRequestBody {
1479 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
1480 where
1481 D: Deserializer<'de>,
1482 {
1483 #[derive(Deserialize)]
1484 #[serde(untagged)]
1485 enum ProtocolIdOrString {
1486 Old(Vec<ProtocolId>),
1487 New(Vec<String>),
1488 }
1489
1490 struct ProtocolStateRequestBodyVisitor;
1491
1492 impl<'de> de::Visitor<'de> for ProtocolStateRequestBodyVisitor {
1493 type Value = ProtocolStateRequestBody;
1494
1495 fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
1496 formatter.write_str("struct ProtocolStateRequestBody")
1497 }
1498
1499 fn visit_map<V>(self, mut map: V) -> Result<ProtocolStateRequestBody, V::Error>
1500 where
1501 V: de::MapAccess<'de>,
1502 {
1503 let mut protocol_ids = None;
1504 let mut protocol_system = None;
1505 let mut version = None;
1506 let mut chain = None;
1507 let mut include_balances = None;
1508 let mut pagination = None;
1509
1510 while let Some(key) = map.next_key::<String>()? {
1511 match key.as_str() {
1512 "protocol_ids" | "protocolIds" => {
1513 let value: ProtocolIdOrString = map.next_value()?;
1514 protocol_ids = match value {
1515 ProtocolIdOrString::Old(ids) => {
1516 Some(ids.into_iter().map(|p| p.id).collect())
1517 }
1518 ProtocolIdOrString::New(ids_str) => Some(ids_str),
1519 };
1520 }
1521 "protocol_system" | "protocolSystem" => {
1522 protocol_system = Some(map.next_value()?);
1523 }
1524 "version" => {
1525 version = Some(map.next_value()?);
1526 }
1527 "chain" => {
1528 chain = Some(map.next_value()?);
1529 }
1530 "include_balances" => {
1531 include_balances = Some(map.next_value()?);
1532 }
1533 "pagination" => {
1534 pagination = Some(map.next_value()?);
1535 }
1536 _ => {
1537 return Err(de::Error::unknown_field(
1538 &key,
1539 &[
1540 "contract_ids",
1541 "protocol_system",
1542 "version",
1543 "chain",
1544 "include_balances",
1545 "pagination",
1546 ],
1547 ))
1548 }
1549 }
1550 }
1551
1552 Ok(ProtocolStateRequestBody {
1553 protocol_ids,
1554 protocol_system: protocol_system.unwrap_or_default(),
1555 version: version.unwrap_or_else(VersionParam::default),
1556 chain: chain.unwrap_or_else(Chain::default),
1557 include_balances: include_balances.unwrap_or(true),
1558 pagination: pagination.unwrap_or_else(PaginationParams::default),
1559 })
1560 }
1561 }
1562
1563 deserializer.deserialize_struct(
1564 "ProtocolStateRequestBody",
1565 &[
1566 "contract_ids",
1567 "protocol_system",
1568 "version",
1569 "chain",
1570 "include_balances",
1571 "pagination",
1572 ],
1573 ProtocolStateRequestBodyVisitor,
1574 )
1575 }
1576}
1577
1578#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, ToSchema, DeepSizeOf)]
1579pub struct ProtocolStateRequestResponse {
1580 pub states: Vec<ResponseProtocolState>,
1581 pub pagination: PaginationResponse,
1582}
1583
1584impl ProtocolStateRequestResponse {
1585 pub fn new(states: Vec<ResponseProtocolState>, pagination: PaginationResponse) -> Self {
1586 Self { states, pagination }
1587 }
1588}
1589
1590#[derive(Serialize, Clone, PartialEq, Hash, Eq)]
1591pub struct ProtocolComponentId {
1592 pub chain: Chain,
1593 pub system: String,
1594 pub id: String,
1595}
1596
1597#[derive(Debug, Serialize, ToSchema)]
1598#[serde(tag = "status", content = "message")]
1599#[schema(example = json!({"status": "NotReady", "message": "No db connection"}))]
1600pub enum Health {
1601 Ready,
1602 Starting(String),
1603 NotReady(String),
1604}
1605
1606#[derive(Serialize, Deserialize, Debug, Default, PartialEq, ToSchema, Eq, Hash, Clone)]
1607#[serde(deny_unknown_fields)]
1608pub struct ProtocolSystemsRequestBody {
1609 #[serde(default)]
1610 pub chain: Chain,
1611 #[serde(default)]
1612 pub pagination: PaginationParams,
1613}
1614
1615impl_pagination_limits!(ProtocolSystemsRequestBody, compressed = 100, uncompressed = 100);
1617
1618#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, ToSchema, Eq, Hash)]
1619pub struct ProtocolSystemsRequestResponse {
1620 pub protocol_systems: Vec<String>,
1622 pub pagination: PaginationResponse,
1623}
1624
1625impl ProtocolSystemsRequestResponse {
1626 pub fn new(protocol_systems: Vec<String>, pagination: PaginationResponse) -> Self {
1627 Self { protocol_systems, pagination }
1628 }
1629}
1630
1631#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, Default)]
1632pub struct DCIUpdate {
1633 pub new_entrypoints: HashMap<ComponentId, HashSet<EntryPoint>>,
1635 pub new_entrypoint_params: HashMap<String, HashSet<(TracingParams, String)>>,
1638 pub trace_results: HashMap<String, TracingResult>,
1640}
1641
1642impl From<models::blockchain::DCIUpdate> for DCIUpdate {
1643 fn from(value: models::blockchain::DCIUpdate) -> Self {
1644 Self {
1645 new_entrypoints: value
1646 .new_entrypoints
1647 .into_iter()
1648 .map(|(k, v)| {
1649 (
1650 k,
1651 v.into_iter()
1652 .map(|v| v.into())
1653 .collect(),
1654 )
1655 })
1656 .collect(),
1657 new_entrypoint_params: value
1658 .new_entrypoint_params
1659 .into_iter()
1660 .map(|(k, v)| {
1661 (
1662 k,
1663 v.into_iter()
1664 .map(|(params, i)| (params.into(), i))
1665 .collect(),
1666 )
1667 })
1668 .collect(),
1669 trace_results: value
1670 .trace_results
1671 .into_iter()
1672 .map(|(k, v)| (k, v.into()))
1673 .collect(),
1674 }
1675 }
1676}
1677
1678#[derive(Serialize, Deserialize, Debug, Default, PartialEq, ToSchema, Eq, Hash, Clone)]
1679#[serde(deny_unknown_fields)]
1680pub struct ComponentTvlRequestBody {
1681 #[serde(default)]
1682 pub chain: Chain,
1683 #[serde(alias = "protocolSystem")]
1686 pub protocol_system: Option<String>,
1687 #[serde(default)]
1688 pub component_ids: Option<Vec<String>>,
1689 #[serde(default)]
1690 pub pagination: PaginationParams,
1691}
1692
1693impl_pagination_limits!(ComponentTvlRequestBody, compressed = 100, uncompressed = 100);
1695
1696impl ComponentTvlRequestBody {
1697 pub fn system_filtered(system: &str, chain: Chain) -> Self {
1698 Self {
1699 chain,
1700 protocol_system: Some(system.to_string()),
1701 component_ids: None,
1702 pagination: Default::default(),
1703 }
1704 }
1705
1706 pub fn id_filtered(ids: Vec<String>, chain: Chain) -> Self {
1707 Self {
1708 chain,
1709 protocol_system: None,
1710 component_ids: Some(ids),
1711 pagination: Default::default(),
1712 }
1713 }
1714}
1715#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, ToSchema)]
1717pub struct ComponentTvlRequestResponse {
1718 pub tvl: HashMap<String, f64>,
1719 pub pagination: PaginationResponse,
1720}
1721
1722impl ComponentTvlRequestResponse {
1723 pub fn new(tvl: HashMap<String, f64>, pagination: PaginationResponse) -> Self {
1724 Self { tvl, pagination }
1725 }
1726}
1727
1728#[derive(
1729 Serialize, Deserialize, Debug, Default, PartialEq, ToSchema, Eq, Hash, Clone, DeepSizeOf,
1730)]
1731pub struct TracedEntryPointRequestBody {
1732 #[serde(default)]
1733 pub chain: Chain,
1734 pub protocol_system: String,
1737 #[schema(value_type = Option<Vec<String>>)]
1739 pub component_ids: Option<Vec<ComponentId>>,
1740 #[serde(default)]
1742 pub pagination: PaginationParams,
1743}
1744
1745impl_pagination_limits!(TracedEntryPointRequestBody, compressed = 100, uncompressed = 100);
1747
1748#[derive(Debug, Serialize, Deserialize, PartialEq, Clone, ToSchema, Eq, Hash, DeepSizeOf)]
1749pub struct EntryPoint {
1750 #[schema(example = "0xEdf63cce4bA70cbE74064b7687882E71ebB0e988:getRate()")]
1751 pub external_id: String,
1753 #[schema(value_type=String, example="0x8f4E8439b970363648421C692dd897Fb9c0Bd1D9")]
1754 #[serde(with = "hex_bytes")]
1755 pub target: Bytes,
1757 #[schema(example = "getRate()")]
1758 pub signature: String,
1760}
1761
1762#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, ToSchema, Eq, Hash, DeepSizeOf)]
1763pub enum StorageOverride {
1764 #[schema(value_type=HashMap<String, String>)]
1768 Diff(BTreeMap<StoreKey, StoreVal>),
1769
1770 #[schema(value_type=HashMap<String, String>)]
1774 Replace(BTreeMap<StoreKey, StoreVal>),
1775}
1776
1777impl From<models::blockchain::StorageOverride> for StorageOverride {
1778 fn from(value: models::blockchain::StorageOverride) -> Self {
1779 match value {
1780 models::blockchain::StorageOverride::Diff(diff) => StorageOverride::Diff(diff),
1781 models::blockchain::StorageOverride::Replace(replace) => {
1782 StorageOverride::Replace(replace)
1783 }
1784 }
1785 }
1786}
1787
1788#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, ToSchema, Eq, Hash, DeepSizeOf)]
1793pub struct AccountOverrides {
1794 pub slots: Option<StorageOverride>,
1796 #[schema(value_type=Option<String>)]
1797 pub native_balance: Option<Balance>,
1799 #[schema(value_type=Option<String>)]
1800 pub code: Option<Code>,
1802}
1803
1804impl From<models::blockchain::AccountOverrides> for AccountOverrides {
1805 fn from(value: models::blockchain::AccountOverrides) -> Self {
1806 AccountOverrides {
1807 slots: value.slots.map(|s| s.into()),
1808 native_balance: value.native_balance,
1809 code: value.code,
1810 }
1811 }
1812}
1813
1814#[derive(Debug, Serialize, Deserialize, PartialEq, Clone, ToSchema, Eq, Hash, DeepSizeOf)]
1815pub struct RPCTracerParams {
1816 #[schema(value_type=Option<String>)]
1819 #[serde(with = "hex_bytes_option", default)]
1820 pub caller: Option<Bytes>,
1821 #[schema(value_type=String, example="0x679aefce")]
1823 #[serde(with = "hex_bytes")]
1824 pub calldata: Bytes,
1825 pub state_overrides: Option<BTreeMap<Address, AccountOverrides>>,
1827 #[schema(value_type=Option<Vec<String>>)]
1830 #[serde(default)]
1831 pub prune_addresses: Option<Vec<Address>>,
1832}
1833
1834impl From<models::blockchain::RPCTracerParams> for RPCTracerParams {
1835 fn from(value: models::blockchain::RPCTracerParams) -> Self {
1836 RPCTracerParams {
1837 caller: value.caller,
1838 calldata: value.calldata,
1839 state_overrides: value.state_overrides.map(|overrides| {
1840 overrides
1841 .into_iter()
1842 .map(|(address, account_overrides)| (address, account_overrides.into()))
1843 .collect()
1844 }),
1845 prune_addresses: value.prune_addresses,
1846 }
1847 }
1848}
1849
1850#[derive(Deserialize, Serialize, Debug, PartialEq, Eq, Clone, Hash, DeepSizeOf, ToSchema)]
1851#[serde(tag = "method", rename_all = "lowercase")]
1852pub enum TracingParams {
1853 RPCTracer(RPCTracerParams),
1855}
1856
1857impl From<models::blockchain::TracingParams> for TracingParams {
1858 fn from(value: models::blockchain::TracingParams) -> Self {
1859 match value {
1860 models::blockchain::TracingParams::RPCTracer(params) => {
1861 TracingParams::RPCTracer(params.into())
1862 }
1863 }
1864 }
1865}
1866
1867impl From<models::blockchain::EntryPoint> for EntryPoint {
1868 fn from(value: models::blockchain::EntryPoint) -> Self {
1869 Self { external_id: value.external_id, target: value.target, signature: value.signature }
1870 }
1871}
1872
1873#[derive(Serialize, Deserialize, Debug, PartialEq, ToSchema, Eq, Clone, DeepSizeOf)]
1874pub struct EntryPointWithTracingParams {
1875 pub entry_point: EntryPoint,
1877 pub params: TracingParams,
1879}
1880
1881impl From<models::blockchain::EntryPointWithTracingParams> for EntryPointWithTracingParams {
1882 fn from(value: models::blockchain::EntryPointWithTracingParams) -> Self {
1883 Self { entry_point: value.entry_point.into(), params: value.params.into() }
1884 }
1885}
1886
1887#[derive(
1888 Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash, Serialize, Deserialize, DeepSizeOf,
1889)]
1890pub struct AddressStorageLocation {
1891 pub key: StoreKey,
1892 pub offset: u8,
1893}
1894
1895impl AddressStorageLocation {
1896 pub fn new(key: StoreKey, offset: u8) -> Self {
1897 Self { key, offset }
1898 }
1899}
1900
1901impl From<models::blockchain::AddressStorageLocation> for AddressStorageLocation {
1902 fn from(value: models::blockchain::AddressStorageLocation) -> Self {
1903 Self { key: value.key, offset: value.offset }
1904 }
1905}
1906
1907fn deserialize_retriggers_from_value(
1908 value: &serde_json::Value,
1909) -> Result<HashSet<(StoreKey, AddressStorageLocation)>, String> {
1910 use serde::Deserialize;
1911 use serde_json::Value;
1912
1913 let mut result = HashSet::new();
1914
1915 if let Value::Array(items) = value {
1916 for item in items {
1917 if let Value::Array(pair) = item {
1918 if pair.len() == 2 {
1919 let key = StoreKey::deserialize(&pair[0])
1920 .map_err(|e| format!("Failed to deserialize key: {}", e))?;
1921
1922 let addr_storage = match &pair[1] {
1924 Value::String(_) => {
1925 let storage_key = StoreKey::deserialize(&pair[1]).map_err(|e| {
1927 format!("Failed to deserialize old format storage key: {}", e)
1928 })?;
1929 AddressStorageLocation::new(storage_key, 12)
1930 }
1931 Value::Object(_) => {
1932 AddressStorageLocation::deserialize(&pair[1]).map_err(|e| {
1934 format!("Failed to deserialize AddressStorageLocation: {}", e)
1935 })?
1936 }
1937 _ => return Err("Invalid retrigger format".to_string()),
1938 };
1939
1940 result.insert((key, addr_storage));
1941 }
1942 }
1943 }
1944 }
1945
1946 Ok(result)
1947}
1948
1949#[derive(Serialize, Debug, Default, PartialEq, ToSchema, Eq, Clone, DeepSizeOf)]
1950pub struct TracingResult {
1951 #[schema(value_type=HashSet<(String, String)>)]
1952 pub retriggers: HashSet<(StoreKey, AddressStorageLocation)>,
1953 #[schema(value_type=HashMap<String,HashSet<String>>)]
1954 pub accessed_slots: HashMap<Address, HashSet<StoreKey>>,
1955}
1956
1957impl<'de> Deserialize<'de> for TracingResult {
1960 fn deserialize<D>(deserializer: D) -> Result<TracingResult, D::Error>
1961 where
1962 D: Deserializer<'de>,
1963 {
1964 use serde::de::Error;
1965 use serde_json::Value;
1966
1967 let value = Value::deserialize(deserializer)?;
1968 let mut result = TracingResult::default();
1969
1970 if let Value::Object(map) = value {
1971 if let Some(retriggers_value) = map.get("retriggers") {
1973 result.retriggers =
1974 deserialize_retriggers_from_value(retriggers_value).map_err(|e| {
1975 D::Error::custom(format!("Failed to deserialize retriggers: {}", e))
1976 })?;
1977 }
1978
1979 if let Some(accessed_slots_value) = map.get("accessed_slots") {
1981 result.accessed_slots = serde_json::from_value(accessed_slots_value.clone())
1982 .map_err(|e| {
1983 D::Error::custom(format!("Failed to deserialize accessed_slots: {}", e))
1984 })?;
1985 }
1986 }
1987
1988 Ok(result)
1989 }
1990}
1991
1992impl From<models::blockchain::TracingResult> for TracingResult {
1993 fn from(value: models::blockchain::TracingResult) -> Self {
1994 TracingResult {
1995 retriggers: value
1996 .retriggers
1997 .into_iter()
1998 .map(|(k, v)| (k, v.into()))
1999 .collect(),
2000 accessed_slots: value.accessed_slots,
2001 }
2002 }
2003}
2004
2005#[derive(Serialize, PartialEq, ToSchema, Eq, Clone, Debug, Deserialize, DeepSizeOf)]
2006pub struct TracedEntryPointRequestResponse {
2007 #[schema(value_type = HashMap<String, Vec<(EntryPointWithTracingParams, TracingResult)>>)]
2010 pub traced_entry_points:
2011 HashMap<ComponentId, Vec<(EntryPointWithTracingParams, TracingResult)>>,
2012 pub pagination: PaginationResponse,
2013}
2014impl From<TracedEntryPointRequestResponse> for DCIUpdate {
2015 fn from(response: TracedEntryPointRequestResponse) -> Self {
2016 let mut new_entrypoints = HashMap::new();
2017 let mut new_entrypoint_params = HashMap::new();
2018 let mut trace_results = HashMap::new();
2019
2020 for (component, traces) in response.traced_entry_points {
2021 let mut entrypoints = HashSet::new();
2022
2023 for (entrypoint, trace) in traces {
2024 let entrypoint_id = entrypoint
2025 .entry_point
2026 .external_id
2027 .clone();
2028
2029 entrypoints.insert(entrypoint.entry_point.clone());
2031
2032 new_entrypoint_params
2034 .entry(entrypoint_id.clone())
2035 .or_insert_with(HashSet::new)
2036 .insert((entrypoint.params, component.clone()));
2037
2038 trace_results
2040 .entry(entrypoint_id)
2041 .and_modify(|existing_trace: &mut TracingResult| {
2042 existing_trace
2044 .retriggers
2045 .extend(trace.retriggers.clone());
2046 for (address, slots) in trace.accessed_slots.clone() {
2047 existing_trace
2048 .accessed_slots
2049 .entry(address)
2050 .or_default()
2051 .extend(slots);
2052 }
2053 })
2054 .or_insert(trace);
2055 }
2056
2057 if !entrypoints.is_empty() {
2058 new_entrypoints.insert(component, entrypoints);
2059 }
2060 }
2061
2062 DCIUpdate { new_entrypoints, new_entrypoint_params, trace_results }
2063 }
2064}
2065
2066#[derive(Serialize, Deserialize, Debug, Default, PartialEq, ToSchema, Eq, Clone)]
2067pub struct AddEntryPointRequestBody {
2068 #[serde(default)]
2069 pub chain: Chain,
2070 #[schema(value_type=String)]
2071 #[serde(default)]
2072 pub block_hash: Bytes,
2073 #[schema(value_type = Vec<(String, Vec<EntryPointWithTracingParams>)>)]
2075 pub entry_points_with_tracing_data: Vec<(ComponentId, Vec<EntryPointWithTracingParams>)>,
2076}
2077
2078#[derive(Serialize, PartialEq, ToSchema, Eq, Clone, Debug, Deserialize)]
2079pub struct AddEntryPointRequestResponse {
2080 #[schema(value_type = HashMap<String, Vec<(EntryPointWithTracingParams, TracingResult)>>)]
2083 pub traced_entry_points:
2084 HashMap<ComponentId, Vec<(EntryPointWithTracingParams, TracingResult)>>,
2085}
2086
2087#[cfg(test)]
2088mod test {
2089 use std::str::FromStr;
2090
2091 use maplit::hashmap;
2092 use rstest::rstest;
2093
2094 use super::*;
2095
2096 #[rstest]
2099 #[case::legacy_format(None, false)]
2100 #[case::explicit_true(Some(true), true)]
2101 #[case::explicit_false(Some(false), false)]
2102 fn test_subscribe_compression_backward_compatibility(
2103 #[case] compression: Option<bool>,
2104 #[case] expected: bool,
2105 ) {
2106 use serde_json::json;
2107
2108 let mut json_value = json!({
2109 "method": "subscribe",
2110 "extractor_id": {
2111 "chain": "ethereum",
2112 "name": "test"
2113 },
2114 "include_state": true
2115 });
2116
2117 if let Some(value) = compression {
2118 json_value["compression"] = json!(value);
2119 }
2120
2121 let command: Command =
2122 serde_json::from_value(json_value).expect("Failed to deserialize Subscribe command");
2123
2124 if let Command::Subscribe { compression, .. } = command {
2125 assert_eq!(compression, expected);
2126 } else {
2127 panic!("Expected Subscribe command");
2128 }
2129 }
2130
2131 #[rstest]
2134 #[case::legacy_format(None, false)]
2135 #[case::explicit_true(Some(true), true)]
2136 #[case::explicit_false(Some(false), false)]
2137 fn test_subscribe_partial_blocks_backward_compatibility(
2138 #[case] partial_blocks: Option<bool>,
2139 #[case] expected: bool,
2140 ) {
2141 use serde_json::json;
2142
2143 let mut json_value = json!({
2144 "method": "subscribe",
2145 "extractor_id": {
2146 "chain": "ethereum",
2147 "name": "test"
2148 },
2149 "include_state": true
2150 });
2151
2152 if let Some(value) = partial_blocks {
2153 json_value["partial_blocks"] = json!(value);
2154 }
2155
2156 let command: Command =
2157 serde_json::from_value(json_value).expect("Failed to deserialize Subscribe command");
2158
2159 if let Command::Subscribe { partial_blocks, .. } = command {
2160 assert_eq!(partial_blocks, expected);
2161 } else {
2162 panic!("Expected Subscribe command");
2163 }
2164 }
2165
2166 #[test]
2167 fn test_tracing_result_backward_compatibility() {
2168 use serde_json::json;
2169
2170 let old_format_json = json!({
2172 "retriggers": [
2173 ["0x01", "0x02"],
2174 ["0x03", "0x04"]
2175 ],
2176 "accessed_slots": {
2177 "0x05": ["0x06", "0x07"]
2178 }
2179 });
2180
2181 let result: TracingResult = serde_json::from_value(old_format_json).unwrap();
2182
2183 assert_eq!(result.retriggers.len(), 2);
2185 let retriggers_vec: Vec<_> = result.retriggers.iter().collect();
2186 assert!(retriggers_vec.iter().any(|(k, v)| {
2187 k == &Bytes::from("0x01") && v.key == Bytes::from("0x02") && v.offset == 12
2188 }));
2189 assert!(retriggers_vec.iter().any(|(k, v)| {
2190 k == &Bytes::from("0x03") && v.key == Bytes::from("0x04") && v.offset == 12
2191 }));
2192
2193 let new_format_json = json!({
2195 "retriggers": [
2196 ["0x01", {"key": "0x02", "offset": 12}],
2197 ["0x03", {"key": "0x04", "offset": 5}]
2198 ],
2199 "accessed_slots": {
2200 "0x05": ["0x06", "0x07"]
2201 }
2202 });
2203
2204 let result2: TracingResult = serde_json::from_value(new_format_json).unwrap();
2205
2206 assert_eq!(result2.retriggers.len(), 2);
2208 let retriggers_vec2: Vec<_> = result2.retriggers.iter().collect();
2209 assert!(retriggers_vec2.iter().any(|(k, v)| {
2210 k == &Bytes::from("0x01") && v.key == Bytes::from("0x02") && v.offset == 12
2211 }));
2212 assert!(retriggers_vec2.iter().any(|(k, v)| {
2213 k == &Bytes::from("0x03") && v.key == Bytes::from("0x04") && v.offset == 5
2214 }));
2215 }
2216
2217 #[rstest]
2218 #[case::legacy_format(None, None)]
2219 #[case::full_block(Some(None), None)]
2220 #[case::partial_block(Some(Some(1)), Some(1))]
2221 fn test_block_changes_is_partial_backward_compatibility(
2222 #[case] has_partial_value: Option<Option<u32>>,
2223 #[case] expected: Option<u32>,
2224 ) {
2225 use serde_json::json;
2226
2227 let mut json_value = json!({
2228 "extractor": "test_extractor",
2229 "chain": "ethereum",
2230 "block": {
2231 "number": 100,
2232 "hash": "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef",
2233 "parent_hash": "0xabcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890",
2234 "chain": "ethereum",
2235 "ts": "2024-01-01T00:00:00"
2236 },
2237 "finalized_block_height": 99,
2238 "revert": false,
2239 "new_tokens": {},
2240 "account_updates": {},
2241 "state_updates": {},
2242 "new_protocol_components": {},
2243 "deleted_protocol_components": {},
2244 "component_balances": {},
2245 "account_balances": {},
2246 "component_tvl": {},
2247 "dci_update": {
2248 "new_entrypoints": {},
2249 "new_entrypoint_params": {},
2250 "trace_results": {}
2251 }
2252 });
2253
2254 if let Some(partial_value) = has_partial_value {
2256 json_value["partial_block_index"] = json!(partial_value);
2257 }
2258
2259 let block_changes: BlockChanges =
2260 serde_json::from_value(json_value).expect("Failed to deserialize BlockChanges");
2261
2262 assert_eq!(block_changes.partial_block_index, expected);
2263 }
2264
2265 #[test]
2266 fn test_protocol_components_equality() {
2267 let body1 = ProtocolComponentsRequestBody {
2268 protocol_system: "protocol1".to_string(),
2269 component_ids: Some(vec!["component1".to_string(), "component2".to_string()]),
2270 tvl_gt: Some(1000.0),
2271 chain: Chain::Ethereum,
2272 pagination: PaginationParams::default(),
2273 };
2274
2275 let body2 = ProtocolComponentsRequestBody {
2276 protocol_system: "protocol1".to_string(),
2277 component_ids: Some(vec!["component1".to_string(), "component2".to_string()]),
2278 tvl_gt: Some(1000.0 + 1e-7), chain: Chain::Ethereum,
2280 pagination: PaginationParams::default(),
2281 };
2282
2283 assert_eq!(body1, body2);
2285 }
2286
2287 #[test]
2288 fn test_protocol_components_inequality() {
2289 let body1 = ProtocolComponentsRequestBody {
2290 protocol_system: "protocol1".to_string(),
2291 component_ids: Some(vec!["component1".to_string(), "component2".to_string()]),
2292 tvl_gt: Some(1000.0),
2293 chain: Chain::Ethereum,
2294 pagination: PaginationParams::default(),
2295 };
2296
2297 let body2 = ProtocolComponentsRequestBody {
2298 protocol_system: "protocol1".to_string(),
2299 component_ids: Some(vec!["component1".to_string(), "component2".to_string()]),
2300 tvl_gt: Some(1000.0 + 1e-5), chain: Chain::Ethereum,
2302 pagination: PaginationParams::default(),
2303 };
2304
2305 assert_ne!(body1, body2);
2307 }
2308
2309 #[test]
2310 fn test_parse_state_request() {
2311 let json_str = r#"
2312 {
2313 "contractIds": [
2314 "0xb4eccE46b8D4e4abFd03C9B806276A6735C9c092"
2315 ],
2316 "protocol_system": "uniswap_v2",
2317 "version": {
2318 "timestamp": "2069-01-01T04:20:00",
2319 "block": {
2320 "hash": "0x24101f9cb26cd09425b52da10e8c2f56ede94089a8bbe0f31f1cda5f4daa52c4",
2321 "number": 213,
2322 "chain": "ethereum"
2323 }
2324 }
2325 }
2326 "#;
2327
2328 let result: StateRequestBody = serde_json::from_str(json_str).unwrap();
2329
2330 let contract0 = "b4eccE46b8D4e4abFd03C9B806276A6735C9c092"
2331 .parse()
2332 .unwrap();
2333 let block_hash = "24101f9cb26cd09425b52da10e8c2f56ede94089a8bbe0f31f1cda5f4daa52c4"
2334 .parse()
2335 .unwrap();
2336 let block_number = 213;
2337
2338 let expected_timestamp =
2339 NaiveDateTime::parse_from_str("2069-01-01T04:20:00", "%Y-%m-%dT%H:%M:%S").unwrap();
2340
2341 let expected = StateRequestBody {
2342 contract_ids: Some(vec![contract0]),
2343 protocol_system: "uniswap_v2".to_string(),
2344 version: VersionParam {
2345 timestamp: Some(expected_timestamp),
2346 block: Some(BlockParam {
2347 hash: Some(block_hash),
2348 chain: Some(Chain::Ethereum),
2349 number: Some(block_number),
2350 }),
2351 },
2352 chain: Chain::Ethereum,
2353 pagination: PaginationParams::default(),
2354 };
2355
2356 assert_eq!(result, expected);
2357 }
2358
2359 #[test]
2360 fn test_parse_state_request_dual_interface() {
2361 let json_common = r#"
2362 {
2363 "__CONTRACT_IDS__": [
2364 "0xb4eccE46b8D4e4abFd03C9B806276A6735C9c092"
2365 ],
2366 "version": {
2367 "timestamp": "2069-01-01T04:20:00",
2368 "block": {
2369 "hash": "0x24101f9cb26cd09425b52da10e8c2f56ede94089a8bbe0f31f1cda5f4daa52c4",
2370 "number": 213,
2371 "chain": "ethereum"
2372 }
2373 }
2374 }
2375 "#;
2376
2377 let json_str_snake = json_common.replace("\"__CONTRACT_IDS__\"", "\"contract_ids\"");
2378 let json_str_camel = json_common.replace("\"__CONTRACT_IDS__\"", "\"contractIds\"");
2379
2380 let snake: StateRequestBody = serde_json::from_str(&json_str_snake).unwrap();
2381 let camel: StateRequestBody = serde_json::from_str(&json_str_camel).unwrap();
2382
2383 assert_eq!(snake, camel);
2384 }
2385
2386 #[test]
2387 fn test_parse_state_request_unknown_field() {
2388 let body = r#"
2389 {
2390 "contract_ids_with_typo_error": [
2391 {
2392 "address": "0xb4eccE46b8D4e4abFd03C9B806276A6735C9c092",
2393 "chain": "ethereum"
2394 }
2395 ],
2396 "version": {
2397 "timestamp": "2069-01-01T04:20:00",
2398 "block": {
2399 "hash": "0x24101f9cb26cd09425b52da10e8c2f56ede94089a8bbe0f31f1cda5f4daa52c4",
2400 "parentHash": "0x8d75152454e60413efe758cc424bfd339897062d7e658f302765eb7b50971815",
2401 "number": 213,
2402 "chain": "ethereum"
2403 }
2404 }
2405 }
2406 "#;
2407
2408 let decoded = serde_json::from_str::<StateRequestBody>(body);
2409
2410 assert!(decoded.is_err(), "Expected an error due to unknown field");
2411
2412 if let Err(e) = decoded {
2413 assert!(
2414 e.to_string()
2415 .contains("unknown field `contract_ids_with_typo_error`"),
2416 "Error message does not contain expected unknown field information"
2417 );
2418 }
2419 }
2420
2421 #[test]
2422 fn test_parse_state_request_no_contract_specified() {
2423 let json_str = r#"
2424 {
2425 "protocol_system": "uniswap_v2",
2426 "version": {
2427 "timestamp": "2069-01-01T04:20:00",
2428 "block": {
2429 "hash": "0x24101f9cb26cd09425b52da10e8c2f56ede94089a8bbe0f31f1cda5f4daa52c4",
2430 "number": 213,
2431 "chain": "ethereum"
2432 }
2433 }
2434 }
2435 "#;
2436
2437 let result: StateRequestBody = serde_json::from_str(json_str).unwrap();
2438
2439 let block_hash = "24101f9cb26cd09425b52da10e8c2f56ede94089a8bbe0f31f1cda5f4daa52c4".into();
2440 let block_number = 213;
2441 let expected_timestamp =
2442 NaiveDateTime::parse_from_str("2069-01-01T04:20:00", "%Y-%m-%dT%H:%M:%S").unwrap();
2443
2444 let expected = StateRequestBody {
2445 contract_ids: None,
2446 protocol_system: "uniswap_v2".to_string(),
2447 version: VersionParam {
2448 timestamp: Some(expected_timestamp),
2449 block: Some(BlockParam {
2450 hash: Some(block_hash),
2451 chain: Some(Chain::Ethereum),
2452 number: Some(block_number),
2453 }),
2454 },
2455 chain: Chain::Ethereum,
2456 pagination: PaginationParams { page: 0, page_size: 100 },
2457 };
2458
2459 assert_eq!(result, expected);
2460 }
2461
2462 #[rstest]
2463 #[case::deprecated_ids(
2464 r#"
2465 {
2466 "protocol_ids": [
2467 {
2468 "id": "0xb4eccE46b8D4e4abFd03C9B806276A6735C9c092",
2469 "chain": "ethereum"
2470 }
2471 ],
2472 "protocol_system": "uniswap_v2",
2473 "include_balances": false,
2474 "version": {
2475 "timestamp": "2069-01-01T04:20:00",
2476 "block": {
2477 "hash": "0x24101f9cb26cd09425b52da10e8c2f56ede94089a8bbe0f31f1cda5f4daa52c4",
2478 "number": 213,
2479 "chain": "ethereum"
2480 }
2481 }
2482 }
2483 "#
2484 )]
2485 #[case(
2486 r#"
2487 {
2488 "protocolIds": [
2489 "0xb4eccE46b8D4e4abFd03C9B806276A6735C9c092"
2490 ],
2491 "protocol_system": "uniswap_v2",
2492 "include_balances": false,
2493 "version": {
2494 "timestamp": "2069-01-01T04:20:00",
2495 "block": {
2496 "hash": "0x24101f9cb26cd09425b52da10e8c2f56ede94089a8bbe0f31f1cda5f4daa52c4",
2497 "number": 213,
2498 "chain": "ethereum"
2499 }
2500 }
2501 }
2502 "#
2503 )]
2504 fn test_parse_protocol_state_request(#[case] json_str: &str) {
2505 let result: ProtocolStateRequestBody = serde_json::from_str(json_str).unwrap();
2506
2507 let block_hash = "24101f9cb26cd09425b52da10e8c2f56ede94089a8bbe0f31f1cda5f4daa52c4"
2508 .parse()
2509 .unwrap();
2510 let block_number = 213;
2511
2512 let expected_timestamp =
2513 NaiveDateTime::parse_from_str("2069-01-01T04:20:00", "%Y-%m-%dT%H:%M:%S").unwrap();
2514
2515 let expected = ProtocolStateRequestBody {
2516 protocol_ids: Some(vec!["0xb4eccE46b8D4e4abFd03C9B806276A6735C9c092".to_string()]),
2517 protocol_system: "uniswap_v2".to_string(),
2518 version: VersionParam {
2519 timestamp: Some(expected_timestamp),
2520 block: Some(BlockParam {
2521 hash: Some(block_hash),
2522 chain: Some(Chain::Ethereum),
2523 number: Some(block_number),
2524 }),
2525 },
2526 chain: Chain::Ethereum,
2527 include_balances: false,
2528 pagination: PaginationParams::default(),
2529 };
2530
2531 assert_eq!(result, expected);
2532 }
2533
2534 #[rstest]
2535 #[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()])]
2536 #[case::with_strings(vec!["id1".to_string(), "id2".to_string()], vec!["id1".to_string(), "id2".to_string()])]
2537 fn test_id_filtered<T>(#[case] input_ids: Vec<T>, #[case] expected_ids: Vec<String>)
2538 where
2539 T: Into<String> + Clone,
2540 {
2541 let request_body = ProtocolStateRequestBody::id_filtered(input_ids);
2542
2543 assert_eq!(request_body.protocol_ids, Some(expected_ids));
2544 }
2545
2546 fn create_models_block_changes() -> crate::models::blockchain::BlockAggregatedChanges {
2547 let base_ts = 1694534400; crate::models::blockchain::BlockAggregatedChanges {
2550 extractor: "native_name".to_string(),
2551 block: models::blockchain::Block::new(
2552 3,
2553 models::Chain::Ethereum,
2554 Bytes::from_str("0x0000000000000000000000000000000000000000000000000000000000000003").unwrap(),
2555 Bytes::from_str("0x0000000000000000000000000000000000000000000000000000000000000002").unwrap(),
2556 chrono::DateTime::from_timestamp(base_ts + 3000, 0).unwrap().naive_utc(),
2557 ),
2558 db_committed_block_height: Some(1),
2559 finalized_block_height: 1,
2560 revert: true,
2561 state_deltas: HashMap::from([
2562 ("pc_1".to_string(), models::protocol::ProtocolComponentStateDelta {
2563 component_id: "pc_1".to_string(),
2564 updated_attributes: HashMap::from([
2565 ("attr_2".to_string(), Bytes::from("0x0000000000000002")),
2566 ("attr_1".to_string(), Bytes::from("0x00000000000003e8")),
2567 ]),
2568 deleted_attributes: HashSet::new(),
2569 }),
2570 ]),
2571 new_protocol_components: HashMap::from([
2572 ("pc_2".to_string(), crate::models::protocol::ProtocolComponent {
2573 id: "pc_2".to_string(),
2574 protocol_system: "native_protocol_system".to_string(),
2575 protocol_type_name: "pt_1".to_string(),
2576 chain: models::Chain::Ethereum,
2577 tokens: vec![
2578 Bytes::from_str("0xdac17f958d2ee523a2206206994597c13d831ec7").unwrap(),
2579 Bytes::from_str("0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48").unwrap(),
2580 ],
2581 contract_addresses: vec![],
2582 static_attributes: HashMap::new(),
2583 change: models::ChangeType::Creation,
2584 creation_tx: Bytes::from_str("0x000000000000000000000000000000000000000000000000000000000000c351").unwrap(),
2585 created_at: chrono::DateTime::from_timestamp(base_ts + 5000, 0).unwrap().naive_utc(),
2586 }),
2587 ]),
2588 deleted_protocol_components: HashMap::from([
2589 ("pc_3".to_string(), crate::models::protocol::ProtocolComponent {
2590 id: "pc_3".to_string(),
2591 protocol_system: "native_protocol_system".to_string(),
2592 protocol_type_name: "pt_2".to_string(),
2593 chain: models::Chain::Ethereum,
2594 tokens: vec![
2595 Bytes::from_str("0x6b175474e89094c44da98b954eedeac495271d0f").unwrap(),
2596 Bytes::from_str("0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2").unwrap(),
2597 ],
2598 contract_addresses: vec![],
2599 static_attributes: HashMap::new(),
2600 change: models::ChangeType::Deletion,
2601 creation_tx: Bytes::from_str("0x0000000000000000000000000000000000000000000000000000000000009c41").unwrap(),
2602 created_at: chrono::DateTime::from_timestamp(base_ts + 4000, 0).unwrap().naive_utc(),
2603 }),
2604 ]),
2605 component_balances: HashMap::from([
2606 ("pc_1".to_string(), HashMap::from([
2607 (Bytes::from_str("0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48").unwrap(), models::protocol::ComponentBalance {
2608 token: Bytes::from_str("0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48").unwrap(),
2609 balance: Bytes::from("0x00000001"),
2610 balance_float: 1.0,
2611 modify_tx: Bytes::from_str("0x0000000000000000000000000000000000000000000000000000000000000000").unwrap(),
2612 component_id: "pc_1".to_string(),
2613 }),
2614 (Bytes::from_str("0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2").unwrap(), models::protocol::ComponentBalance {
2615 token: Bytes::from_str("0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2").unwrap(),
2616 balance: Bytes::from("0x000003e8"),
2617 balance_float: 1000.0,
2618 modify_tx: Bytes::from_str("0x0000000000000000000000000000000000000000000000000000000000007531").unwrap(),
2619 component_id: "pc_1".to_string(),
2620 }),
2621 ])),
2622 ]),
2623 account_balances: HashMap::from([
2624 (Bytes::from_str("0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2").unwrap(), HashMap::from([
2625 (Bytes::from_str("0x7a250d5630b4cf539739df2c5dacb4c659f2488d").unwrap(), models::contract::AccountBalance {
2626 account: Bytes::from_str("0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2").unwrap(),
2627 token: Bytes::from_str("0x7a250d5630b4cf539739df2c5dacb4c659f2488d").unwrap(),
2628 balance: Bytes::from("0x000003e8"),
2629 modify_tx: Bytes::from_str("0x0000000000000000000000000000000000000000000000000000000000007531").unwrap(),
2630 }),
2631 ])),
2632 ]),
2633 ..Default::default()
2634 }
2635 }
2636
2637 #[test]
2638 fn test_serialize_deserialize_block_changes() {
2639 let block_entity_changes = create_models_block_changes();
2644
2645 let json_data = serde_json::to_string(&block_entity_changes).expect("Failed to serialize");
2647
2648 serde_json::from_str::<BlockChanges>(&json_data).expect("parsing failed");
2650 }
2651
2652 #[test]
2653 fn test_parse_block_changes() {
2654 let json_data = r#"
2655 {
2656 "extractor": "vm:ambient",
2657 "chain": "ethereum",
2658 "block": {
2659 "number": 123,
2660 "hash": "0x0000000000000000000000000000000000000000000000000000000000000000",
2661 "parent_hash": "0x0000000000000000000000000000000000000000000000000000000000000000",
2662 "chain": "ethereum",
2663 "ts": "2023-09-14T00:00:00"
2664 },
2665 "finalized_block_height": 0,
2666 "revert": false,
2667 "new_tokens": {},
2668 "account_updates": {
2669 "0x7a250d5630b4cf539739df2c5dacb4c659f2488d": {
2670 "address": "0x7a250d5630b4cf539739df2c5dacb4c659f2488d",
2671 "chain": "ethereum",
2672 "slots": {},
2673 "balance": "0x01f4",
2674 "code": "",
2675 "change": "Update"
2676 }
2677 },
2678 "state_updates": {
2679 "component_1": {
2680 "component_id": "component_1",
2681 "updated_attributes": {"attr1": "0x01"},
2682 "deleted_attributes": ["attr2"]
2683 }
2684 },
2685 "new_protocol_components":
2686 { "protocol_1": {
2687 "id": "protocol_1",
2688 "protocol_system": "system_1",
2689 "protocol_type_name": "type_1",
2690 "chain": "ethereum",
2691 "tokens": ["0x01", "0x02"],
2692 "contract_ids": ["0x01", "0x02"],
2693 "static_attributes": {"attr1": "0x01f4"},
2694 "change": "Update",
2695 "creation_tx": "0x01",
2696 "created_at": "2023-09-14T00:00:00"
2697 }
2698 },
2699 "deleted_protocol_components": {},
2700 "component_balances": {
2701 "protocol_1":
2702 {
2703 "0x01": {
2704 "token": "0x01",
2705 "balance": "0xb77831d23691653a01",
2706 "balance_float": 3.3844151001790677e21,
2707 "modify_tx": "0x01",
2708 "component_id": "protocol_1"
2709 }
2710 }
2711 },
2712 "account_balances": {
2713 "0x7a250d5630b4cf539739df2c5dacb4c659f2488d": {
2714 "0x7a250d5630b4cf539739df2c5dacb4c659f2488d": {
2715 "account": "0x7a250d5630b4cf539739df2c5dacb4c659f2488d",
2716 "token": "0x7a250d5630b4cf539739df2c5dacb4c659f2488d",
2717 "balance": "0x01f4",
2718 "modify_tx": "0x01"
2719 }
2720 }
2721 },
2722 "component_tvl": {
2723 "protocol_1": 1000.0
2724 },
2725 "dci_update": {
2726 "new_entrypoints": {
2727 "component_1": [
2728 {
2729 "external_id": "0x01:sig()",
2730 "target": "0x01",
2731 "signature": "sig()"
2732 }
2733 ]
2734 },
2735 "new_entrypoint_params": {
2736 "0x01:sig()": [
2737 [
2738 {
2739 "method": "rpctracer",
2740 "caller": "0x01",
2741 "calldata": "0x02"
2742 },
2743 "component_1"
2744 ]
2745 ]
2746 },
2747 "trace_results": {
2748 "0x01:sig()": {
2749 "retriggers": [
2750 ["0x01", {"key": "0x02", "offset": 12}]
2751 ],
2752 "accessed_slots": {
2753 "0x03": ["0x03", "0x04"]
2754 }
2755 }
2756 }
2757 }
2758 }
2759 "#;
2760
2761 serde_json::from_str::<BlockChanges>(json_data).expect("parsing failed");
2762 }
2763
2764 #[test]
2765 fn test_parse_websocket_message() {
2766 let json_data = r#"
2767 {
2768 "subscription_id": "5d23bfbe-89ad-4ea3-8672-dc9e973ac9dc",
2769 "deltas": {
2770 "type": "BlockChanges",
2771 "extractor": "uniswap_v2",
2772 "chain": "ethereum",
2773 "block": {
2774 "number": 19291517,
2775 "hash": "0xbc3ea4896c0be8da6229387a8571b72818aa258daf4fab46471003ad74c4ee83",
2776 "parent_hash": "0x89ca5b8d593574cf6c886f41ef8208bf6bdc1a90ef36046cb8c84bc880b9af8f",
2777 "chain": "ethereum",
2778 "ts": "2024-02-23T16:35:35"
2779 },
2780 "finalized_block_height": 0,
2781 "revert": false,
2782 "new_tokens": {},
2783 "account_updates": {
2784 "0x7a250d5630b4cf539739df2c5dacb4c659f2488d": {
2785 "address": "0x7a250d5630b4cf539739df2c5dacb4c659f2488d",
2786 "chain": "ethereum",
2787 "slots": {},
2788 "balance": "0x01f4",
2789 "code": "",
2790 "change": "Update"
2791 }
2792 },
2793 "state_updates": {
2794 "0xde6faedbcae38eec6d33ad61473a04a6dd7f6e28": {
2795 "component_id": "0xde6faedbcae38eec6d33ad61473a04a6dd7f6e28",
2796 "updated_attributes": {
2797 "reserve0": "0x87f7b5973a7f28a8b32404",
2798 "reserve1": "0x09e9564b11"
2799 },
2800 "deleted_attributes": []
2801 },
2802 "0x99c59000f5a76c54c4fd7d82720c045bdcf1450d": {
2803 "component_id": "0x99c59000f5a76c54c4fd7d82720c045bdcf1450d",
2804 "updated_attributes": {
2805 "reserve1": "0x44d9a8fd662c2f4d03",
2806 "reserve0": "0x500b1261f811d5bf423e"
2807 },
2808 "deleted_attributes": []
2809 }
2810 },
2811 "new_protocol_components": {},
2812 "deleted_protocol_components": {},
2813 "component_balances": {
2814 "0x99c59000f5a76c54c4fd7d82720c045bdcf1450d": {
2815 "0x9012744b7a564623b6c3e40b144fc196bdedf1a9": {
2816 "token": "0x9012744b7a564623b6c3e40b144fc196bdedf1a9",
2817 "balance": "0x500b1261f811d5bf423e",
2818 "balance_float": 3.779935574269033E23,
2819 "modify_tx": "0xe46c4db085fb6c6f3408a65524555797adb264e1d5cf3b66ad154598f85ac4bf",
2820 "component_id": "0x99c59000f5a76c54c4fd7d82720c045bdcf1450d"
2821 },
2822 "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2": {
2823 "token": "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2",
2824 "balance": "0x44d9a8fd662c2f4d03",
2825 "balance_float": 1.270062661329837E21,
2826 "modify_tx": "0xe46c4db085fb6c6f3408a65524555797adb264e1d5cf3b66ad154598f85ac4bf",
2827 "component_id": "0x99c59000f5a76c54c4fd7d82720c045bdcf1450d"
2828 }
2829 }
2830 },
2831 "account_balances": {
2832 "0x7a250d5630b4cf539739df2c5dacb4c659f2488d": {
2833 "0x7a250d5630b4cf539739df2c5dacb4c659f2488d": {
2834 "account": "0x7a250d5630b4cf539739df2c5dacb4c659f2488d",
2835 "token": "0x7a250d5630b4cf539739df2c5dacb4c659f2488d",
2836 "balance": "0x01f4",
2837 "modify_tx": "0x01"
2838 }
2839 }
2840 },
2841 "component_tvl": {},
2842 "dci_update": {
2843 "new_entrypoints": {
2844 "0xde6faedbcae38eec6d33ad61473a04a6dd7f6e28": [
2845 {
2846 "external_id": "0x01:sig()",
2847 "target": "0x01",
2848 "signature": "sig()"
2849 }
2850 ]
2851 },
2852 "new_entrypoint_params": {
2853 "0x01:sig()": [
2854 [
2855 {
2856 "method": "rpctracer",
2857 "caller": "0x01",
2858 "calldata": "0x02"
2859 },
2860 "0xde6faedbcae38eec6d33ad61473a04a6dd7f6e28"
2861 ]
2862 ]
2863 },
2864 "trace_results": {
2865 "0x01:sig()": {
2866 "retriggers": [
2867 ["0x01", {"key": "0x02", "offset": 12}]
2868 ],
2869 "accessed_slots": {
2870 "0x03": ["0x03", "0x04"]
2871 }
2872 }
2873 }
2874 }
2875 }
2876 }
2877 "#;
2878 serde_json::from_str::<WebSocketMessage>(json_data).expect("parsing failed");
2879 }
2880
2881 #[test]
2882 fn test_protocol_state_delta_merge_update_delete() {
2883 let mut delta1 = ProtocolStateDelta {
2885 component_id: "Component1".to_string(),
2886 updated_attributes: HashMap::from([(
2887 "Attribute1".to_string(),
2888 Bytes::from("0xbadbabe420"),
2889 )]),
2890 deleted_attributes: HashSet::new(),
2891 };
2892 let delta2 = ProtocolStateDelta {
2893 component_id: "Component1".to_string(),
2894 updated_attributes: HashMap::from([(
2895 "Attribute2".to_string(),
2896 Bytes::from("0x0badbabe"),
2897 )]),
2898 deleted_attributes: HashSet::from(["Attribute1".to_string()]),
2899 };
2900 let exp = ProtocolStateDelta {
2901 component_id: "Component1".to_string(),
2902 updated_attributes: HashMap::from([(
2903 "Attribute2".to_string(),
2904 Bytes::from("0x0badbabe"),
2905 )]),
2906 deleted_attributes: HashSet::from(["Attribute1".to_string()]),
2907 };
2908
2909 delta1.merge(&delta2);
2910
2911 assert_eq!(delta1, exp);
2912 }
2913
2914 #[test]
2915 fn test_protocol_state_delta_merge_delete_update() {
2916 let mut delta1 = ProtocolStateDelta {
2918 component_id: "Component1".to_string(),
2919 updated_attributes: HashMap::new(),
2920 deleted_attributes: HashSet::from(["Attribute1".to_string()]),
2921 };
2922 let delta2 = ProtocolStateDelta {
2923 component_id: "Component1".to_string(),
2924 updated_attributes: HashMap::from([(
2925 "Attribute1".to_string(),
2926 Bytes::from("0x0badbabe"),
2927 )]),
2928 deleted_attributes: HashSet::new(),
2929 };
2930 let exp = ProtocolStateDelta {
2931 component_id: "Component1".to_string(),
2932 updated_attributes: HashMap::from([(
2933 "Attribute1".to_string(),
2934 Bytes::from("0x0badbabe"),
2935 )]),
2936 deleted_attributes: HashSet::new(),
2937 };
2938
2939 delta1.merge(&delta2);
2940
2941 assert_eq!(delta1, exp);
2942 }
2943
2944 #[test]
2945 fn test_account_update_merge() {
2946 let mut account1 = AccountUpdate::new(
2948 Bytes::from(b"0x1234"),
2949 Chain::Ethereum,
2950 HashMap::from([(Bytes::from("0xaabb"), Bytes::from("0xccdd"))]),
2951 Some(Bytes::from("0x1000")),
2952 Some(Bytes::from("0xdeadbeaf")),
2953 ChangeType::Creation,
2954 );
2955
2956 let account2 = AccountUpdate::new(
2957 Bytes::from(b"0x1234"), Chain::Ethereum,
2959 HashMap::from([(Bytes::from("0xeeff"), Bytes::from("0x11223344"))]),
2960 Some(Bytes::from("0x2000")),
2961 Some(Bytes::from("0xcafebabe")),
2962 ChangeType::Update,
2963 );
2964
2965 account1.merge(&account2);
2967
2968 let expected = AccountUpdate::new(
2970 Bytes::from(b"0x1234"), Chain::Ethereum,
2972 HashMap::from([
2973 (Bytes::from("0xaabb"), Bytes::from("0xccdd")), (Bytes::from("0xeeff"), Bytes::from("0x11223344")), ]),
2976 Some(Bytes::from("0x2000")), Some(Bytes::from("0xcafebabe")), ChangeType::Creation, );
2980
2981 assert_eq!(account1, expected);
2983 }
2984
2985 #[test]
2986 fn test_block_account_changes_merge() {
2987 let old_account_updates: HashMap<Bytes, AccountUpdate> = [(
2989 Bytes::from("0x0011"),
2990 AccountUpdate {
2991 address: Bytes::from("0x00"),
2992 chain: Chain::Ethereum,
2993 slots: HashMap::from([(Bytes::from("0x0022"), Bytes::from("0x0033"))]),
2994 balance: Some(Bytes::from("0x01")),
2995 code: Some(Bytes::from("0x02")),
2996 change: ChangeType::Creation,
2997 },
2998 )]
2999 .into_iter()
3000 .collect();
3001 let new_account_updates: HashMap<Bytes, AccountUpdate> = [(
3002 Bytes::from("0x0011"),
3003 AccountUpdate {
3004 address: Bytes::from("0x00"),
3005 chain: Chain::Ethereum,
3006 slots: HashMap::from([(Bytes::from("0x0044"), Bytes::from("0x0055"))]),
3007 balance: Some(Bytes::from("0x03")),
3008 code: Some(Bytes::from("0x04")),
3009 change: ChangeType::Update,
3010 },
3011 )]
3012 .into_iter()
3013 .collect();
3014 let block_account_changes_initial = BlockChanges {
3016 extractor: "extractor1".to_string(),
3017 revert: false,
3018 account_updates: old_account_updates,
3019 ..Default::default()
3020 };
3021
3022 let block_account_changes_new = BlockChanges {
3023 extractor: "extractor2".to_string(),
3024 revert: true,
3025 account_updates: new_account_updates,
3026 ..Default::default()
3027 };
3028
3029 let res = block_account_changes_initial.merge(block_account_changes_new);
3031
3032 let expected_account_updates: HashMap<Bytes, AccountUpdate> = [(
3034 Bytes::from("0x0011"),
3035 AccountUpdate {
3036 address: Bytes::from("0x00"),
3037 chain: Chain::Ethereum,
3038 slots: HashMap::from([
3039 (Bytes::from("0x0044"), Bytes::from("0x0055")),
3040 (Bytes::from("0x0022"), Bytes::from("0x0033")),
3041 ]),
3042 balance: Some(Bytes::from("0x03")),
3043 code: Some(Bytes::from("0x04")),
3044 change: ChangeType::Creation,
3045 },
3046 )]
3047 .into_iter()
3048 .collect();
3049 let block_account_changes_expected = BlockChanges {
3050 extractor: "extractor1".to_string(),
3051 revert: true,
3052 account_updates: expected_account_updates,
3053 ..Default::default()
3054 };
3055 assert_eq!(res, block_account_changes_expected);
3056 }
3057
3058 #[test]
3059 fn test_block_entity_changes_merge() {
3060 let block_entity_changes_result1 = BlockChanges {
3062 extractor: String::from("extractor1"),
3063 revert: false,
3064 state_updates: hashmap! { "state1".to_string() => ProtocolStateDelta::default() },
3065 new_protocol_components: hashmap! { "component1".to_string() => ProtocolComponent::default() },
3066 deleted_protocol_components: HashMap::new(),
3067 component_balances: hashmap! {
3068 "component1".to_string() => TokenBalances(hashmap! {
3069 Bytes::from("0x01") => ComponentBalance {
3070 token: Bytes::from("0x01"),
3071 balance: Bytes::from("0x01"),
3072 balance_float: 1.0,
3073 modify_tx: Bytes::from("0x00"),
3074 component_id: "component1".to_string()
3075 },
3076 Bytes::from("0x02") => ComponentBalance {
3077 token: Bytes::from("0x02"),
3078 balance: Bytes::from("0x02"),
3079 balance_float: 2.0,
3080 modify_tx: Bytes::from("0x00"),
3081 component_id: "component1".to_string()
3082 },
3083 })
3084
3085 },
3086 component_tvl: hashmap! { "tvl1".to_string() => 1000.0 },
3087 ..Default::default()
3088 };
3089 let block_entity_changes_result2 = BlockChanges {
3090 extractor: String::from("extractor2"),
3091 revert: true,
3092 state_updates: hashmap! { "state2".to_string() => ProtocolStateDelta::default() },
3093 new_protocol_components: hashmap! { "component2".to_string() => ProtocolComponent::default() },
3094 deleted_protocol_components: hashmap! { "component3".to_string() => ProtocolComponent::default() },
3095 component_balances: hashmap! {
3096 "component1".to_string() => TokenBalances::default(),
3097 "component2".to_string() => TokenBalances::default()
3098 },
3099 component_tvl: hashmap! { "tvl2".to_string() => 2000.0 },
3100 ..Default::default()
3101 };
3102
3103 let res = block_entity_changes_result1.merge(block_entity_changes_result2);
3104
3105 let expected_block_entity_changes_result = BlockChanges {
3106 extractor: String::from("extractor1"),
3107 revert: true,
3108 state_updates: hashmap! {
3109 "state1".to_string() => ProtocolStateDelta::default(),
3110 "state2".to_string() => ProtocolStateDelta::default(),
3111 },
3112 new_protocol_components: hashmap! {
3113 "component1".to_string() => ProtocolComponent::default(),
3114 "component2".to_string() => ProtocolComponent::default(),
3115 },
3116 deleted_protocol_components: hashmap! {
3117 "component3".to_string() => ProtocolComponent::default(),
3118 },
3119 component_balances: hashmap! {
3120 "component1".to_string() => TokenBalances(hashmap! {
3121 Bytes::from("0x01") => ComponentBalance {
3122 token: Bytes::from("0x01"),
3123 balance: Bytes::from("0x01"),
3124 balance_float: 1.0,
3125 modify_tx: Bytes::from("0x00"),
3126 component_id: "component1".to_string()
3127 },
3128 Bytes::from("0x02") => ComponentBalance {
3129 token: Bytes::from("0x02"),
3130 balance: Bytes::from("0x02"),
3131 balance_float: 2.0,
3132 modify_tx: Bytes::from("0x00"),
3133 component_id: "component1".to_string()
3134 },
3135 }),
3136 "component2".to_string() => TokenBalances::default(),
3137 },
3138 component_tvl: hashmap! {
3139 "tvl1".to_string() => 1000.0,
3140 "tvl2".to_string() => 2000.0
3141 },
3142 ..Default::default()
3143 };
3144
3145 assert_eq!(res, expected_block_entity_changes_result);
3146 }
3147
3148 #[test]
3149 fn test_websocket_error_serialization() {
3150 let extractor_id = ExtractorIdentity::new(Chain::Ethereum, "test_extractor");
3151 let subscription_id = Uuid::new_v4();
3152
3153 let error = WebsocketError::ExtractorNotFound(extractor_id.clone());
3155 let json = serde_json::to_string(&error).unwrap();
3156 let deserialized: WebsocketError = serde_json::from_str(&json).unwrap();
3157 assert_eq!(error, deserialized);
3158
3159 let error = WebsocketError::SubscriptionNotFound(subscription_id);
3161 let json = serde_json::to_string(&error).unwrap();
3162 let deserialized: WebsocketError = serde_json::from_str(&json).unwrap();
3163 assert_eq!(error, deserialized);
3164
3165 let error = WebsocketError::ParseError("{asd".to_string(), "invalid json".to_string());
3167 let json = serde_json::to_string(&error).unwrap();
3168 let deserialized: WebsocketError = serde_json::from_str(&json).unwrap();
3169 assert_eq!(error, deserialized);
3170
3171 let error = WebsocketError::SubscribeError(extractor_id.clone());
3173 let json = serde_json::to_string(&error).unwrap();
3174 let deserialized: WebsocketError = serde_json::from_str(&json).unwrap();
3175 assert_eq!(error, deserialized);
3176
3177 let error =
3179 WebsocketError::CompressionError(subscription_id, "Compression failed".to_string());
3180 let json = serde_json::to_string(&error).unwrap();
3181 let deserialized: WebsocketError = serde_json::from_str(&json).unwrap();
3182 assert_eq!(error, deserialized);
3183 }
3184
3185 #[test]
3186 fn test_websocket_message_with_error_response() {
3187 let error =
3188 WebsocketError::ParseError("}asdfas".to_string(), "malformed request".to_string());
3189 let response = Response::Error(error.clone());
3190 let message = WebSocketMessage::Response(response);
3191
3192 let json = serde_json::to_string(&message).unwrap();
3193 let deserialized: WebSocketMessage = serde_json::from_str(&json).unwrap();
3194
3195 if let WebSocketMessage::Response(Response::Error(deserialized_error)) = deserialized {
3196 assert_eq!(error, deserialized_error);
3197 } else {
3198 panic!("Expected WebSocketMessage::Response(Response::Error)");
3199 }
3200 }
3201
3202 #[test]
3203 fn test_websocket_error_conversion_from_models() {
3204 use crate::models::error::WebsocketError as ModelsError;
3205
3206 let extractor_id =
3207 crate::models::ExtractorIdentity::new(crate::models::Chain::Ethereum, "test");
3208 let subscription_id = Uuid::new_v4();
3209
3210 let models_error = ModelsError::ExtractorNotFound(extractor_id.clone());
3212 let dto_error: WebsocketError = models_error.into();
3213 assert_eq!(dto_error, WebsocketError::ExtractorNotFound(extractor_id.clone().into()));
3214
3215 let models_error = ModelsError::SubscriptionNotFound(subscription_id);
3217 let dto_error: WebsocketError = models_error.into();
3218 assert_eq!(dto_error, WebsocketError::SubscriptionNotFound(subscription_id));
3219
3220 let json_result: Result<serde_json::Value, _> = serde_json::from_str("{invalid json");
3222 let json_error = json_result.unwrap_err();
3223 let models_error = ModelsError::ParseError("{invalid json".to_string(), json_error);
3224 let dto_error: WebsocketError = models_error.into();
3225 if let WebsocketError::ParseError(msg, error) = dto_error {
3226 assert!(!error.is_empty(), "Error message should not be empty, got: '{}'", msg);
3228 } else {
3229 panic!("Expected ParseError variant");
3230 }
3231
3232 let models_error = ModelsError::SubscribeError(extractor_id.clone());
3234 let dto_error: WebsocketError = models_error.into();
3235 assert_eq!(dto_error, WebsocketError::SubscribeError(extractor_id.into()));
3236
3237 let io_error = std::io::Error::other("Compression failed");
3239 let models_error = ModelsError::CompressionError(subscription_id, io_error);
3240 let dto_error: WebsocketError = models_error.into();
3241 if let WebsocketError::CompressionError(sub_id, msg) = &dto_error {
3242 assert_eq!(*sub_id, subscription_id);
3243 assert!(msg.contains("Compression failed"));
3244 } else {
3245 panic!("Expected CompressionError variant");
3246 }
3247 }
3248}
3249
3250#[cfg(test)]
3251mod memory_size_tests {
3252 use std::collections::HashMap;
3253
3254 use super::*;
3255
3256 #[test]
3257 fn test_state_request_response_memory_size_empty() {
3258 let response = StateRequestResponse {
3259 accounts: vec![],
3260 pagination: PaginationResponse::new(1, 10, 0),
3261 };
3262
3263 let size = response.deep_size_of();
3264
3265 assert!(size >= 48, "Empty response should have minimum size of 48 bytes, got {}", size);
3267 assert!(size < 200, "Empty response should not be too large, got {}", size);
3268 }
3269
3270 #[test]
3271 fn test_state_request_response_memory_size_scales_with_slots() {
3272 let create_response_with_slots = |slot_count: usize| {
3273 let mut slots = HashMap::new();
3274 for i in 0..slot_count {
3275 let key = vec![i as u8; 32]; let value = vec![(i + 100) as u8; 32]; slots.insert(key.into(), value.into());
3278 }
3279
3280 let account = ResponseAccount::new(
3281 Chain::Ethereum,
3282 vec![1; 20].into(),
3283 "Pool".to_string(),
3284 slots,
3285 vec![1; 32].into(),
3286 HashMap::new(),
3287 vec![].into(), vec![1; 32].into(),
3289 vec![1; 32].into(),
3290 vec![1; 32].into(),
3291 None,
3292 );
3293
3294 StateRequestResponse {
3295 accounts: vec![account],
3296 pagination: PaginationResponse::new(1, 10, 1),
3297 }
3298 };
3299
3300 let small_response = create_response_with_slots(10);
3301 let large_response = create_response_with_slots(100);
3302
3303 let small_size = small_response.deep_size_of();
3304 let large_size = large_response.deep_size_of();
3305
3306 assert!(
3308 large_size > small_size * 5,
3309 "Large response ({} bytes) should be much larger than small response ({} bytes)",
3310 large_size,
3311 small_size
3312 );
3313
3314 let size_diff = large_size - small_size;
3316 let expected_min_diff = 90 * 64; assert!(
3318 size_diff > expected_min_diff,
3319 "Size difference ({} bytes) should reflect the additional slot data",
3320 size_diff
3321 );
3322 }
3323}
3324
3325#[cfg(test)]
3326mod pagination_limits_tests {
3327 use super::*;
3328
3329 #[derive(Clone, Debug)]
3331 struct TestRequestBody {
3332 pagination: PaginationParams,
3333 }
3334
3335 impl_pagination_limits!(TestRequestBody, compressed = 500, uncompressed = 50);
3337
3338 #[test]
3339 fn test_effective_max_page_size() {
3340 let max_size = TestRequestBody::effective_max_page_size(true);
3342 assert_eq!(max_size, 500, "Should return compressed limit when compression is enabled");
3343
3344 let max_size = TestRequestBody::effective_max_page_size(false);
3346 assert_eq!(max_size, 50, "Should return uncompressed limit when compression is disabled");
3347 }
3348}