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=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<T> From<models::protocol::ProtocolComponent<T>> for ProtocolComponent
691where
692 T: Into<Address> + Clone,
693{
694 fn from(value: models::protocol::ProtocolComponent<T>) -> Self {
695 Self {
696 id: value.id,
697 protocol_system: value.protocol_system,
698 protocol_type_name: value.protocol_type_name,
699 chain: value.chain.into(),
700 tokens: value
701 .tokens
702 .into_iter()
703 .map(|t| t.into())
704 .collect(),
705 contract_ids: value.contract_addresses,
706 static_attributes: value.static_attributes,
707 change: value.change.into(),
708 creation_tx: value.creation_tx,
709 created_at: value.created_at,
710 }
711 }
712}
713
714#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, Default)]
715pub struct ComponentBalance {
716 #[serde(with = "hex_bytes")]
717 pub token: Bytes,
718 pub balance: Bytes,
719 pub balance_float: f64,
720 #[serde(with = "hex_bytes")]
721 pub modify_tx: Bytes,
722 pub component_id: String,
723}
724
725#[derive(Debug, PartialEq, Clone, Default, Serialize, Deserialize, ToSchema)]
726pub struct ProtocolStateDelta {
728 pub component_id: String,
729 #[schema(value_type=HashMap<String, String>)]
730 pub updated_attributes: HashMap<String, Bytes>,
731 pub deleted_attributes: HashSet<String>,
732}
733
734impl From<models::protocol::ProtocolComponentStateDelta> for ProtocolStateDelta {
735 fn from(value: models::protocol::ProtocolComponentStateDelta) -> Self {
736 Self {
737 component_id: value.component_id,
738 updated_attributes: value.updated_attributes,
739 deleted_attributes: value.deleted_attributes,
740 }
741 }
742}
743
744impl ProtocolStateDelta {
745 pub fn merge(&mut self, other: &Self) {
764 self.updated_attributes
766 .retain(|k, _| !other.deleted_attributes.contains(k));
767
768 self.deleted_attributes.retain(|attr| {
770 !other
771 .updated_attributes
772 .contains_key(attr)
773 });
774
775 self.updated_attributes.extend(
777 other
778 .updated_attributes
779 .iter()
780 .map(|(k, v)| (k.clone(), v.clone())),
781 );
782
783 self.deleted_attributes
785 .extend(other.deleted_attributes.iter().cloned());
786 }
787}
788
789#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, ToSchema, Eq, Hash, DeepSizeOf)]
791#[serde(deny_unknown_fields)]
792pub struct PaginationParams {
793 #[serde(default)]
795 pub page: i64,
796 #[serde(default)]
798 #[schema(default = 100)]
799 pub page_size: i64,
800}
801
802impl PaginationParams {
803 pub fn new(page: i64, page_size: i64) -> Self {
804 Self { page, page_size }
805 }
806}
807
808impl Default for PaginationParams {
809 fn default() -> Self {
810 PaginationParams { page: 0, page_size: 100 }
811 }
812}
813
814pub trait PaginationLimits {
819 const MAX_PAGE_SIZE_COMPRESSED: i64;
821
822 const MAX_PAGE_SIZE_UNCOMPRESSED: i64;
824
825 fn effective_max_page_size(compression: bool) -> i64 {
827 if compression {
828 Self::MAX_PAGE_SIZE_COMPRESSED
829 } else {
830 Self::MAX_PAGE_SIZE_UNCOMPRESSED
831 }
832 }
833
834 fn pagination(&self) -> &PaginationParams;
836}
837
838macro_rules! impl_pagination_limits {
845 ($type:ty, compressed = $comp:expr, uncompressed = $uncomp:expr) => {
846 impl $crate::dto::PaginationLimits for $type {
847 const MAX_PAGE_SIZE_COMPRESSED: i64 = $comp;
848 const MAX_PAGE_SIZE_UNCOMPRESSED: i64 = $uncomp;
849
850 fn pagination(&self) -> &$crate::dto::PaginationParams {
851 &self.pagination
852 }
853 }
854 };
855}
856
857#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, ToSchema, Eq, Hash, DeepSizeOf)]
858#[serde(deny_unknown_fields)]
859pub struct PaginationResponse {
860 pub page: i64,
861 pub page_size: i64,
862 pub total: i64,
864}
865
866impl PaginationResponse {
868 pub fn new(page: i64, page_size: i64, total: i64) -> Self {
869 Self { page, page_size, total }
870 }
871
872 pub fn total_pages(&self) -> i64 {
873 (self.total + self.page_size - 1) / self.page_size
875 }
876}
877
878#[derive(
879 Clone, Serialize, Debug, Default, Deserialize, PartialEq, ToSchema, Eq, Hash, DeepSizeOf,
880)]
881#[serde(deny_unknown_fields)]
882pub struct StateRequestBody {
883 #[serde(alias = "contractIds")]
885 #[schema(value_type=Option<Vec<String>>)]
886 pub contract_ids: Option<Vec<Bytes>>,
887 #[serde(alias = "protocolSystem", default)]
890 pub protocol_system: String,
891 #[serde(default = "VersionParam::default")]
892 pub version: VersionParam,
893 #[serde(default)]
894 pub chain: Chain,
895 #[serde(default)]
896 pub pagination: PaginationParams,
897}
898
899impl_pagination_limits!(StateRequestBody, compressed = 1200, uncompressed = 100);
901
902impl StateRequestBody {
903 pub fn new(
904 contract_ids: Option<Vec<Bytes>>,
905 protocol_system: String,
906 version: VersionParam,
907 chain: Chain,
908 pagination: PaginationParams,
909 ) -> Self {
910 Self { contract_ids, protocol_system, version, chain, pagination }
911 }
912
913 pub fn from_block(protocol_system: &str, block: BlockParam) -> Self {
914 Self {
915 contract_ids: None,
916 protocol_system: protocol_system.to_string(),
917 version: VersionParam { timestamp: None, block: Some(block.clone()) },
918 chain: block.chain.unwrap_or_default(),
919 pagination: PaginationParams::default(),
920 }
921 }
922
923 pub fn from_timestamp(protocol_system: &str, timestamp: NaiveDateTime, chain: Chain) -> Self {
924 Self {
925 contract_ids: None,
926 protocol_system: protocol_system.to_string(),
927 version: VersionParam { timestamp: Some(timestamp), block: None },
928 chain,
929 pagination: PaginationParams::default(),
930 }
931 }
932}
933
934#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, ToSchema, DeepSizeOf)]
936pub struct StateRequestResponse {
937 pub accounts: Vec<ResponseAccount>,
938 pub pagination: PaginationResponse,
939}
940
941impl StateRequestResponse {
942 pub fn new(accounts: Vec<ResponseAccount>, pagination: PaginationResponse) -> Self {
943 Self { accounts, pagination }
944 }
945}
946
947#[derive(PartialEq, Clone, Serialize, Deserialize, Default, ToSchema, DeepSizeOf)]
948#[serde(rename = "Account")]
949pub struct ResponseAccount {
953 pub chain: Chain,
954 #[schema(value_type=String, example="0xc9f2e6ea1637E499406986ac50ddC92401ce1f58")]
956 #[serde(with = "hex_bytes")]
957 pub address: Bytes,
958 #[schema(value_type=String, example="Protocol Vault")]
960 pub title: String,
961 #[schema(value_type=HashMap<String, String>, example=json!({"0x....": "0x...."}))]
963 #[serde(with = "hex_hashmap_key_value")]
964 pub slots: HashMap<Bytes, Bytes>,
965 #[schema(value_type=String, example="0x00")]
967 #[serde(with = "hex_bytes")]
968 pub native_balance: Bytes,
969 #[schema(value_type=HashMap<String, String>, example=json!({"0x....": "0x...."}))]
972 #[serde(with = "hex_hashmap_key_value")]
973 pub token_balances: HashMap<Bytes, Bytes>,
974 #[schema(value_type=String, example="0xBADBABE")]
976 #[serde(with = "hex_bytes")]
977 pub code: Bytes,
978 #[schema(value_type=String, example="0x123456789")]
980 #[serde(with = "hex_bytes")]
981 pub code_hash: Bytes,
982 #[schema(value_type=String, example="0x8f1133bfb054a23aedfe5d25b1d81b96195396d8b88bd5d4bcf865fc1ae2c3f4")]
984 #[serde(with = "hex_bytes")]
985 pub balance_modify_tx: Bytes,
986 #[schema(value_type=String, example="0x8f1133bfb054a23aedfe5d25b1d81b96195396d8b88bd5d4bcf865fc1ae2c3f4")]
988 #[serde(with = "hex_bytes")]
989 pub code_modify_tx: Bytes,
990 #[deprecated(note = "The `creation_tx` field is deprecated.")]
992 #[schema(value_type=Option<String>, example="0x8f1133bfb054a23aedfe5d25b1d81b96195396d8b88bd5d4bcf865fc1ae2c3f4")]
993 #[serde(with = "hex_bytes_option")]
994 pub creation_tx: Option<Bytes>,
995}
996
997impl ResponseAccount {
998 #[allow(clippy::too_many_arguments)]
999 pub fn new(
1000 chain: Chain,
1001 address: Bytes,
1002 title: String,
1003 slots: HashMap<Bytes, Bytes>,
1004 native_balance: Bytes,
1005 token_balances: HashMap<Bytes, Bytes>,
1006 code: Bytes,
1007 code_hash: Bytes,
1008 balance_modify_tx: Bytes,
1009 code_modify_tx: Bytes,
1010 creation_tx: Option<Bytes>,
1011 ) -> Self {
1012 Self {
1013 chain,
1014 address,
1015 title,
1016 slots,
1017 native_balance,
1018 token_balances,
1019 code,
1020 code_hash,
1021 balance_modify_tx,
1022 code_modify_tx,
1023 creation_tx,
1024 }
1025 }
1026}
1027
1028impl fmt::Debug for ResponseAccount {
1030 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1031 f.debug_struct("ResponseAccount")
1032 .field("chain", &self.chain)
1033 .field("address", &self.address)
1034 .field("title", &self.title)
1035 .field("slots", &self.slots)
1036 .field("native_balance", &self.native_balance)
1037 .field("token_balances", &self.token_balances)
1038 .field("code", &format!("[{} bytes]", self.code.len()))
1039 .field("code_hash", &self.code_hash)
1040 .field("balance_modify_tx", &self.balance_modify_tx)
1041 .field("code_modify_tx", &self.code_modify_tx)
1042 .field("creation_tx", &self.creation_tx)
1043 .finish()
1044 }
1045}
1046
1047#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, Default)]
1048pub struct AccountBalance {
1049 #[serde(with = "hex_bytes")]
1050 pub account: Bytes,
1051 #[serde(with = "hex_bytes")]
1052 pub token: Bytes,
1053 #[serde(with = "hex_bytes")]
1054 pub balance: Bytes,
1055 #[serde(with = "hex_bytes")]
1056 pub modify_tx: Bytes,
1057}
1058
1059#[derive(Debug, Serialize, Deserialize, PartialEq, Clone, ToSchema)]
1060#[serde(deny_unknown_fields)]
1061pub struct ContractId {
1062 #[serde(with = "hex_bytes")]
1063 #[schema(value_type=String)]
1064 pub address: Bytes,
1065 pub chain: Chain,
1066}
1067
1068impl ContractId {
1070 pub fn new(chain: Chain, address: Bytes) -> Self {
1071 Self { address, chain }
1072 }
1073
1074 pub fn address(&self) -> &Bytes {
1075 &self.address
1076 }
1077}
1078
1079impl fmt::Display for ContractId {
1080 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1081 write!(f, "{:?}: 0x{}", self.chain, hex::encode(&self.address))
1082 }
1083}
1084
1085#[derive(Debug, Serialize, Deserialize, PartialEq, Clone, ToSchema, Eq, Hash)]
1092#[serde(deny_unknown_fields)]
1093pub struct VersionParam {
1094 pub timestamp: Option<NaiveDateTime>,
1095 pub block: Option<BlockParam>,
1096}
1097
1098impl DeepSizeOf for VersionParam {
1099 fn deep_size_of_children(&self, ctx: &mut Context) -> usize {
1100 if let Some(block) = &self.block {
1101 return block.deep_size_of_children(ctx);
1102 }
1103
1104 0
1105 }
1106}
1107
1108impl VersionParam {
1109 pub fn new(timestamp: Option<NaiveDateTime>, block: Option<BlockParam>) -> Self {
1110 Self { timestamp, block }
1111 }
1112}
1113
1114impl Default for VersionParam {
1115 fn default() -> Self {
1116 VersionParam { timestamp: Some(Utc::now().naive_utc()), block: None }
1117 }
1118}
1119
1120#[deprecated(note = "Use StateRequestBody instead")]
1121#[derive(Serialize, Deserialize, Default, Debug, IntoParams)]
1122pub struct StateRequestParameters {
1123 #[param(default = 0)]
1125 pub tvl_gt: Option<u64>,
1126 #[param(default = 0)]
1128 pub inertia_min_gt: Option<u64>,
1129 #[serde(default = "default_include_balances_flag")]
1131 pub include_balances: bool,
1132 #[serde(default)]
1133 pub pagination: PaginationParams,
1134}
1135
1136impl StateRequestParameters {
1137 pub fn new(include_balances: bool) -> Self {
1138 Self {
1139 tvl_gt: None,
1140 inertia_min_gt: None,
1141 include_balances,
1142 pagination: PaginationParams::default(),
1143 }
1144 }
1145
1146 pub fn to_query_string(&self) -> String {
1147 let mut parts = vec![format!("include_balances={}", self.include_balances)];
1148
1149 if let Some(tvl_gt) = self.tvl_gt {
1150 parts.push(format!("tvl_gt={tvl_gt}"));
1151 }
1152
1153 if let Some(inertia) = self.inertia_min_gt {
1154 parts.push(format!("inertia_min_gt={inertia}"));
1155 }
1156
1157 let mut res = parts.join("&");
1158 if !res.is_empty() {
1159 res = format!("?{res}");
1160 }
1161 res
1162 }
1163}
1164
1165#[derive(
1166 Serialize, Deserialize, Debug, Default, PartialEq, ToSchema, Eq, Hash, Clone, DeepSizeOf,
1167)]
1168#[serde(deny_unknown_fields)]
1169pub struct TokensRequestBody {
1170 #[serde(alias = "tokenAddresses")]
1172 #[schema(value_type=Option<Vec<String>>)]
1173 pub token_addresses: Option<Vec<Bytes>>,
1174 #[serde(default)]
1182 pub min_quality: Option<i32>,
1183 #[serde(default)]
1185 pub traded_n_days_ago: Option<u64>,
1186 #[serde(default)]
1188 pub pagination: PaginationParams,
1189 #[serde(default)]
1191 pub chain: Chain,
1192}
1193
1194impl_pagination_limits!(TokensRequestBody, compressed = 12900, uncompressed = 3000);
1196
1197#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, ToSchema, Eq, Hash, DeepSizeOf)]
1199pub struct TokensRequestResponse {
1200 pub tokens: Vec<ResponseToken>,
1201 pub pagination: PaginationResponse,
1202}
1203
1204impl TokensRequestResponse {
1205 pub fn new(tokens: Vec<ResponseToken>, pagination_request: &PaginationResponse) -> Self {
1206 Self { tokens, pagination: pagination_request.clone() }
1207 }
1208}
1209
1210#[derive(
1211 PartialEq, Debug, Clone, Serialize, Deserialize, Default, ToSchema, Eq, Hash, DeepSizeOf,
1212)]
1213#[serde(rename = "Token")]
1214pub struct ResponseToken {
1216 pub chain: Chain,
1217 #[schema(value_type=String, example="0xc9f2e6ea1637E499406986ac50ddC92401ce1f58")]
1219 #[serde(with = "hex_bytes")]
1220 pub address: Bytes,
1221 #[schema(value_type=String, example="WETH")]
1223 pub symbol: String,
1224 pub decimals: u32,
1226 pub tax: u64,
1228 pub gas: Vec<Option<u64>>,
1230 pub quality: u32,
1238}
1239
1240impl From<models::token::Token> for ResponseToken {
1241 fn from(value: models::token::Token) -> Self {
1242 Self {
1243 chain: value.chain.into(),
1244 address: value.address,
1245 symbol: value.symbol,
1246 decimals: value.decimals,
1247 tax: value.tax,
1248 gas: value.gas,
1249 quality: value.quality,
1250 }
1251 }
1252}
1253
1254#[derive(Serialize, Deserialize, Debug, Default, ToSchema, Clone, DeepSizeOf)]
1255#[serde(deny_unknown_fields)]
1256pub struct ProtocolComponentsRequestBody {
1257 pub protocol_system: String,
1260 #[schema(value_type=Option<Vec<String>>)]
1262 #[serde(alias = "componentAddresses")]
1263 pub component_ids: Option<Vec<ComponentId>>,
1264 #[serde(default)]
1267 pub tvl_gt: Option<f64>,
1268 #[serde(default)]
1269 pub chain: Chain,
1270 #[serde(default)]
1272 pub pagination: PaginationParams,
1273}
1274
1275impl_pagination_limits!(ProtocolComponentsRequestBody, compressed = 2550, uncompressed = 500);
1277
1278impl PartialEq for ProtocolComponentsRequestBody {
1280 fn eq(&self, other: &Self) -> bool {
1281 let tvl_close_enough = match (self.tvl_gt, other.tvl_gt) {
1282 (Some(a), Some(b)) => (a - b).abs() < 1e-6,
1283 (None, None) => true,
1284 _ => false,
1285 };
1286
1287 self.protocol_system == other.protocol_system &&
1288 self.component_ids == other.component_ids &&
1289 tvl_close_enough &&
1290 self.chain == other.chain &&
1291 self.pagination == other.pagination
1292 }
1293}
1294
1295impl Eq for ProtocolComponentsRequestBody {}
1297
1298impl Hash for ProtocolComponentsRequestBody {
1299 fn hash<H: Hasher>(&self, state: &mut H) {
1300 self.protocol_system.hash(state);
1301 self.component_ids.hash(state);
1302
1303 if let Some(tvl) = self.tvl_gt {
1305 tvl.to_bits().hash(state);
1307 } else {
1308 state.write_u8(0);
1310 }
1311
1312 self.chain.hash(state);
1313 self.pagination.hash(state);
1314 }
1315}
1316
1317impl ProtocolComponentsRequestBody {
1318 pub fn system_filtered(system: &str, tvl_gt: Option<f64>, chain: Chain) -> Self {
1319 Self {
1320 protocol_system: system.to_string(),
1321 component_ids: None,
1322 tvl_gt,
1323 chain,
1324 pagination: Default::default(),
1325 }
1326 }
1327
1328 pub fn id_filtered(system: &str, ids: Vec<String>, chain: Chain) -> Self {
1329 Self {
1330 protocol_system: system.to_string(),
1331 component_ids: Some(ids),
1332 tvl_gt: None,
1333 chain,
1334 pagination: Default::default(),
1335 }
1336 }
1337}
1338
1339impl ProtocolComponentsRequestBody {
1340 pub fn new(
1341 protocol_system: String,
1342 component_ids: Option<Vec<String>>,
1343 tvl_gt: Option<f64>,
1344 chain: Chain,
1345 pagination: PaginationParams,
1346 ) -> Self {
1347 Self { protocol_system, component_ids, tvl_gt, chain, pagination }
1348 }
1349}
1350
1351#[deprecated(note = "Use ProtocolComponentsRequestBody instead")]
1352#[derive(Serialize, Deserialize, Default, Debug, IntoParams)]
1353pub struct ProtocolComponentRequestParameters {
1354 #[param(default = 0)]
1356 pub tvl_gt: Option<f64>,
1357}
1358
1359impl ProtocolComponentRequestParameters {
1360 pub fn tvl_filtered(min_tvl: f64) -> Self {
1361 Self { tvl_gt: Some(min_tvl) }
1362 }
1363}
1364
1365impl ProtocolComponentRequestParameters {
1366 pub fn to_query_string(&self) -> String {
1367 if let Some(tvl_gt) = self.tvl_gt {
1368 return format!("?tvl_gt={tvl_gt}");
1369 }
1370 String::new()
1371 }
1372}
1373
1374#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, ToSchema, DeepSizeOf)]
1376pub struct ProtocolComponentRequestResponse {
1377 pub protocol_components: Vec<ProtocolComponent>,
1378 pub pagination: PaginationResponse,
1379}
1380
1381impl ProtocolComponentRequestResponse {
1382 pub fn new(
1383 protocol_components: Vec<ProtocolComponent>,
1384 pagination: PaginationResponse,
1385 ) -> Self {
1386 Self { protocol_components, pagination }
1387 }
1388}
1389
1390#[derive(Debug, Serialize, Deserialize, PartialEq, Clone, ToSchema, Eq, Hash)]
1391#[serde(deny_unknown_fields)]
1392#[deprecated]
1393pub struct ProtocolId {
1394 pub id: String,
1395 pub chain: Chain,
1396}
1397
1398impl From<ProtocolId> for String {
1399 fn from(protocol_id: ProtocolId) -> Self {
1400 protocol_id.id
1401 }
1402}
1403
1404impl AsRef<str> for ProtocolId {
1405 fn as_ref(&self) -> &str {
1406 &self.id
1407 }
1408}
1409
1410#[derive(Debug, Clone, PartialEq, Default, Deserialize, Serialize, ToSchema, DeepSizeOf)]
1412pub struct ResponseProtocolState {
1413 pub component_id: String,
1415 #[schema(value_type=HashMap<String, String>)]
1418 #[serde(with = "hex_hashmap_value")]
1419 pub attributes: HashMap<String, Bytes>,
1420 #[schema(value_type=HashMap<String, String>)]
1422 #[serde(with = "hex_hashmap_key_value")]
1423 pub balances: HashMap<Bytes, Bytes>,
1424}
1425
1426impl From<models::protocol::ProtocolComponentState> for ResponseProtocolState {
1427 fn from(value: models::protocol::ProtocolComponentState) -> Self {
1428 Self {
1429 component_id: value.component_id,
1430 attributes: value.attributes,
1431 balances: value.balances,
1432 }
1433 }
1434}
1435
1436fn default_include_balances_flag() -> bool {
1437 true
1438}
1439
1440#[derive(Clone, Debug, Serialize, PartialEq, ToSchema, Default, Eq, Hash, DeepSizeOf)]
1442#[serde(deny_unknown_fields)]
1443pub struct ProtocolStateRequestBody {
1444 #[serde(alias = "protocolIds")]
1446 pub protocol_ids: Option<Vec<String>>,
1447 #[serde(alias = "protocolSystem")]
1450 pub protocol_system: String,
1451 #[serde(default)]
1452 pub chain: Chain,
1453 #[serde(default = "default_include_balances_flag")]
1455 pub include_balances: bool,
1456 #[serde(default = "VersionParam::default")]
1457 pub version: VersionParam,
1458 #[serde(default)]
1459 pub pagination: PaginationParams,
1460}
1461
1462impl_pagination_limits!(ProtocolStateRequestBody, compressed = 360, uncompressed = 100);
1464
1465impl ProtocolStateRequestBody {
1466 pub fn id_filtered<I, T>(ids: I) -> Self
1467 where
1468 I: IntoIterator<Item = T>,
1469 T: Into<String>,
1470 {
1471 Self {
1472 protocol_ids: Some(
1473 ids.into_iter()
1474 .map(Into::into)
1475 .collect(),
1476 ),
1477 ..Default::default()
1478 }
1479 }
1480}
1481
1482impl<'de> Deserialize<'de> for ProtocolStateRequestBody {
1486 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
1487 where
1488 D: Deserializer<'de>,
1489 {
1490 #[derive(Deserialize)]
1491 #[serde(untagged)]
1492 enum ProtocolIdOrString {
1493 Old(Vec<ProtocolId>),
1494 New(Vec<String>),
1495 }
1496
1497 struct ProtocolStateRequestBodyVisitor;
1498
1499 impl<'de> de::Visitor<'de> for ProtocolStateRequestBodyVisitor {
1500 type Value = ProtocolStateRequestBody;
1501
1502 fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
1503 formatter.write_str("struct ProtocolStateRequestBody")
1504 }
1505
1506 fn visit_map<V>(self, mut map: V) -> Result<ProtocolStateRequestBody, V::Error>
1507 where
1508 V: de::MapAccess<'de>,
1509 {
1510 let mut protocol_ids = None;
1511 let mut protocol_system = None;
1512 let mut version = None;
1513 let mut chain = None;
1514 let mut include_balances = None;
1515 let mut pagination = None;
1516
1517 while let Some(key) = map.next_key::<String>()? {
1518 match key.as_str() {
1519 "protocol_ids" | "protocolIds" => {
1520 let value: ProtocolIdOrString = map.next_value()?;
1521 protocol_ids = match value {
1522 ProtocolIdOrString::Old(ids) => {
1523 Some(ids.into_iter().map(|p| p.id).collect())
1524 }
1525 ProtocolIdOrString::New(ids_str) => Some(ids_str),
1526 };
1527 }
1528 "protocol_system" | "protocolSystem" => {
1529 protocol_system = Some(map.next_value()?);
1530 }
1531 "version" => {
1532 version = Some(map.next_value()?);
1533 }
1534 "chain" => {
1535 chain = Some(map.next_value()?);
1536 }
1537 "include_balances" => {
1538 include_balances = Some(map.next_value()?);
1539 }
1540 "pagination" => {
1541 pagination = Some(map.next_value()?);
1542 }
1543 _ => {
1544 return Err(de::Error::unknown_field(
1545 &key,
1546 &[
1547 "contract_ids",
1548 "protocol_system",
1549 "version",
1550 "chain",
1551 "include_balances",
1552 "pagination",
1553 ],
1554 ))
1555 }
1556 }
1557 }
1558
1559 Ok(ProtocolStateRequestBody {
1560 protocol_ids,
1561 protocol_system: protocol_system.unwrap_or_default(),
1562 version: version.unwrap_or_else(VersionParam::default),
1563 chain: chain.unwrap_or_else(Chain::default),
1564 include_balances: include_balances.unwrap_or(true),
1565 pagination: pagination.unwrap_or_else(PaginationParams::default),
1566 })
1567 }
1568 }
1569
1570 deserializer.deserialize_struct(
1571 "ProtocolStateRequestBody",
1572 &[
1573 "contract_ids",
1574 "protocol_system",
1575 "version",
1576 "chain",
1577 "include_balances",
1578 "pagination",
1579 ],
1580 ProtocolStateRequestBodyVisitor,
1581 )
1582 }
1583}
1584
1585#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, ToSchema, DeepSizeOf)]
1586pub struct ProtocolStateRequestResponse {
1587 pub states: Vec<ResponseProtocolState>,
1588 pub pagination: PaginationResponse,
1589}
1590
1591impl ProtocolStateRequestResponse {
1592 pub fn new(states: Vec<ResponseProtocolState>, pagination: PaginationResponse) -> Self {
1593 Self { states, pagination }
1594 }
1595}
1596
1597#[derive(Serialize, Clone, PartialEq, Hash, Eq)]
1598pub struct ProtocolComponentId {
1599 pub chain: Chain,
1600 pub system: String,
1601 pub id: String,
1602}
1603
1604#[derive(Debug, Serialize, ToSchema)]
1605#[serde(tag = "status", content = "message")]
1606#[schema(example = json!({"status": "NotReady", "message": "No db connection"}))]
1607pub enum Health {
1608 Ready,
1609 Starting(String),
1610 NotReady(String),
1611}
1612
1613#[derive(Serialize, Deserialize, Debug, Default, PartialEq, ToSchema, Eq, Hash, Clone)]
1614#[serde(deny_unknown_fields)]
1615pub struct ProtocolSystemsRequestBody {
1616 #[serde(default)]
1617 pub chain: Chain,
1618 #[serde(default)]
1619 pub pagination: PaginationParams,
1620}
1621
1622impl_pagination_limits!(ProtocolSystemsRequestBody, compressed = 100, uncompressed = 100);
1624
1625#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, ToSchema, Eq, Hash)]
1626pub struct ProtocolSystemsRequestResponse {
1627 pub protocol_systems: Vec<String>,
1629 #[serde(default)]
1632 pub dci_protocols: Vec<String>,
1633 pub pagination: PaginationResponse,
1634}
1635
1636impl ProtocolSystemsRequestResponse {
1637 pub fn new(
1638 protocol_systems: Vec<String>,
1639 dci_protocols: Vec<String>,
1640 pagination: PaginationResponse,
1641 ) -> Self {
1642 Self { protocol_systems, dci_protocols, pagination }
1643 }
1644}
1645
1646#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, Default)]
1647pub struct DCIUpdate {
1648 pub new_entrypoints: HashMap<ComponentId, HashSet<EntryPoint>>,
1650 pub new_entrypoint_params: HashMap<String, HashSet<(TracingParams, String)>>,
1653 pub trace_results: HashMap<String, TracingResult>,
1655}
1656
1657impl From<models::blockchain::DCIUpdate> for DCIUpdate {
1658 fn from(value: models::blockchain::DCIUpdate) -> Self {
1659 Self {
1660 new_entrypoints: value
1661 .new_entrypoints
1662 .into_iter()
1663 .map(|(k, v)| {
1664 (
1665 k,
1666 v.into_iter()
1667 .map(|v| v.into())
1668 .collect(),
1669 )
1670 })
1671 .collect(),
1672 new_entrypoint_params: value
1673 .new_entrypoint_params
1674 .into_iter()
1675 .map(|(k, v)| {
1676 (
1677 k,
1678 v.into_iter()
1679 .map(|(params, i)| (params.into(), i))
1680 .collect(),
1681 )
1682 })
1683 .collect(),
1684 trace_results: value
1685 .trace_results
1686 .into_iter()
1687 .map(|(k, v)| (k, v.into()))
1688 .collect(),
1689 }
1690 }
1691}
1692
1693#[derive(Serialize, Deserialize, Debug, Default, PartialEq, ToSchema, Eq, Hash, Clone)]
1694#[serde(deny_unknown_fields)]
1695pub struct ComponentTvlRequestBody {
1696 #[serde(default)]
1697 pub chain: Chain,
1698 #[serde(alias = "protocolSystem")]
1701 pub protocol_system: Option<String>,
1702 #[serde(default)]
1703 pub component_ids: Option<Vec<String>>,
1704 #[serde(default)]
1705 pub pagination: PaginationParams,
1706}
1707
1708impl_pagination_limits!(ComponentTvlRequestBody, compressed = 100, uncompressed = 100);
1710
1711impl ComponentTvlRequestBody {
1712 pub fn system_filtered(system: &str, chain: Chain) -> Self {
1713 Self {
1714 chain,
1715 protocol_system: Some(system.to_string()),
1716 component_ids: None,
1717 pagination: Default::default(),
1718 }
1719 }
1720
1721 pub fn id_filtered(ids: Vec<String>, chain: Chain) -> Self {
1722 Self {
1723 chain,
1724 protocol_system: None,
1725 component_ids: Some(ids),
1726 pagination: Default::default(),
1727 }
1728 }
1729}
1730#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, ToSchema)]
1732pub struct ComponentTvlRequestResponse {
1733 pub tvl: HashMap<String, f64>,
1734 pub pagination: PaginationResponse,
1735}
1736
1737impl ComponentTvlRequestResponse {
1738 pub fn new(tvl: HashMap<String, f64>, pagination: PaginationResponse) -> Self {
1739 Self { tvl, pagination }
1740 }
1741}
1742
1743#[derive(
1744 Serialize, Deserialize, Debug, Default, PartialEq, ToSchema, Eq, Hash, Clone, DeepSizeOf,
1745)]
1746pub struct TracedEntryPointRequestBody {
1747 #[serde(default)]
1748 pub chain: Chain,
1749 pub protocol_system: String,
1752 #[schema(value_type = Option<Vec<String>>)]
1754 pub component_ids: Option<Vec<ComponentId>>,
1755 #[serde(default)]
1757 pub pagination: PaginationParams,
1758}
1759
1760impl_pagination_limits!(TracedEntryPointRequestBody, compressed = 100, uncompressed = 100);
1762
1763#[derive(Debug, Serialize, Deserialize, PartialEq, Clone, ToSchema, Eq, Hash, DeepSizeOf)]
1764pub struct EntryPoint {
1765 #[schema(example = "0xEdf63cce4bA70cbE74064b7687882E71ebB0e988:getRate()")]
1766 pub external_id: String,
1768 #[schema(value_type=String, example="0x8f4E8439b970363648421C692dd897Fb9c0Bd1D9")]
1769 #[serde(with = "hex_bytes")]
1770 pub target: Bytes,
1772 #[schema(example = "getRate()")]
1773 pub signature: String,
1775}
1776
1777#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, ToSchema, Eq, Hash, DeepSizeOf)]
1778pub enum StorageOverride {
1779 #[schema(value_type=HashMap<String, String>)]
1783 Diff(BTreeMap<StoreKey, StoreVal>),
1784
1785 #[schema(value_type=HashMap<String, String>)]
1789 Replace(BTreeMap<StoreKey, StoreVal>),
1790}
1791
1792impl From<models::blockchain::StorageOverride> for StorageOverride {
1793 fn from(value: models::blockchain::StorageOverride) -> Self {
1794 match value {
1795 models::blockchain::StorageOverride::Diff(diff) => StorageOverride::Diff(diff),
1796 models::blockchain::StorageOverride::Replace(replace) => {
1797 StorageOverride::Replace(replace)
1798 }
1799 }
1800 }
1801}
1802
1803#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, ToSchema, Eq, Hash, DeepSizeOf)]
1808pub struct AccountOverrides {
1809 pub slots: Option<StorageOverride>,
1811 #[schema(value_type=Option<String>)]
1812 pub native_balance: Option<Balance>,
1814 #[schema(value_type=Option<String>)]
1815 pub code: Option<Code>,
1817}
1818
1819impl From<models::blockchain::AccountOverrides> for AccountOverrides {
1820 fn from(value: models::blockchain::AccountOverrides) -> Self {
1821 AccountOverrides {
1822 slots: value.slots.map(|s| s.into()),
1823 native_balance: value.native_balance,
1824 code: value.code,
1825 }
1826 }
1827}
1828
1829#[derive(Debug, Serialize, Deserialize, PartialEq, Clone, ToSchema, Eq, Hash, DeepSizeOf)]
1830pub struct RPCTracerParams {
1831 #[schema(value_type=Option<String>)]
1834 #[serde(with = "hex_bytes_option", default)]
1835 pub caller: Option<Bytes>,
1836 #[schema(value_type=String, example="0x679aefce")]
1838 #[serde(with = "hex_bytes")]
1839 pub calldata: Bytes,
1840 pub state_overrides: Option<BTreeMap<Address, AccountOverrides>>,
1842 #[schema(value_type=Option<Vec<String>>)]
1845 #[serde(default)]
1846 pub prune_addresses: Option<Vec<Address>>,
1847}
1848
1849impl From<models::blockchain::RPCTracerParams> for RPCTracerParams {
1850 fn from(value: models::blockchain::RPCTracerParams) -> Self {
1851 RPCTracerParams {
1852 caller: value.caller,
1853 calldata: value.calldata,
1854 state_overrides: value.state_overrides.map(|overrides| {
1855 overrides
1856 .into_iter()
1857 .map(|(address, account_overrides)| (address, account_overrides.into()))
1858 .collect()
1859 }),
1860 prune_addresses: value.prune_addresses,
1861 }
1862 }
1863}
1864
1865#[derive(Deserialize, Serialize, Debug, PartialEq, Eq, Clone, Hash, DeepSizeOf, ToSchema)]
1866#[serde(tag = "method", rename_all = "lowercase")]
1867pub enum TracingParams {
1868 RPCTracer(RPCTracerParams),
1870}
1871
1872impl From<models::blockchain::TracingParams> for TracingParams {
1873 fn from(value: models::blockchain::TracingParams) -> Self {
1874 match value {
1875 models::blockchain::TracingParams::RPCTracer(params) => {
1876 TracingParams::RPCTracer(params.into())
1877 }
1878 }
1879 }
1880}
1881
1882impl From<models::blockchain::EntryPoint> for EntryPoint {
1883 fn from(value: models::blockchain::EntryPoint) -> Self {
1884 Self { external_id: value.external_id, target: value.target, signature: value.signature }
1885 }
1886}
1887
1888#[derive(Serialize, Deserialize, Debug, PartialEq, ToSchema, Eq, Clone, DeepSizeOf)]
1889pub struct EntryPointWithTracingParams {
1890 pub entry_point: EntryPoint,
1892 pub params: TracingParams,
1894}
1895
1896impl From<models::blockchain::EntryPointWithTracingParams> for EntryPointWithTracingParams {
1897 fn from(value: models::blockchain::EntryPointWithTracingParams) -> Self {
1898 Self { entry_point: value.entry_point.into(), params: value.params.into() }
1899 }
1900}
1901
1902#[derive(
1903 Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash, Serialize, Deserialize, DeepSizeOf,
1904)]
1905pub struct AddressStorageLocation {
1906 pub key: StoreKey,
1907 pub offset: u8,
1908}
1909
1910impl AddressStorageLocation {
1911 pub fn new(key: StoreKey, offset: u8) -> Self {
1912 Self { key, offset }
1913 }
1914}
1915
1916impl From<models::blockchain::AddressStorageLocation> for AddressStorageLocation {
1917 fn from(value: models::blockchain::AddressStorageLocation) -> Self {
1918 Self { key: value.key, offset: value.offset }
1919 }
1920}
1921
1922fn deserialize_retriggers_from_value(
1923 value: &serde_json::Value,
1924) -> Result<HashSet<(StoreKey, AddressStorageLocation)>, String> {
1925 use serde::Deserialize;
1926 use serde_json::Value;
1927
1928 let mut result = HashSet::new();
1929
1930 if let Value::Array(items) = value {
1931 for item in items {
1932 if let Value::Array(pair) = item {
1933 if pair.len() == 2 {
1934 let key = StoreKey::deserialize(&pair[0])
1935 .map_err(|e| format!("Failed to deserialize key: {}", e))?;
1936
1937 let addr_storage = match &pair[1] {
1939 Value::String(_) => {
1940 let storage_key = StoreKey::deserialize(&pair[1]).map_err(|e| {
1942 format!("Failed to deserialize old format storage key: {}", e)
1943 })?;
1944 AddressStorageLocation::new(storage_key, 12)
1945 }
1946 Value::Object(_) => {
1947 AddressStorageLocation::deserialize(&pair[1]).map_err(|e| {
1949 format!("Failed to deserialize AddressStorageLocation: {}", e)
1950 })?
1951 }
1952 _ => return Err("Invalid retrigger format".to_string()),
1953 };
1954
1955 result.insert((key, addr_storage));
1956 }
1957 }
1958 }
1959 }
1960
1961 Ok(result)
1962}
1963
1964#[derive(Serialize, Debug, Default, PartialEq, ToSchema, Eq, Clone, DeepSizeOf)]
1965pub struct TracingResult {
1966 #[schema(value_type=HashSet<(String, String)>)]
1967 pub retriggers: HashSet<(StoreKey, AddressStorageLocation)>,
1968 #[schema(value_type=HashMap<String,HashSet<String>>)]
1969 pub accessed_slots: HashMap<Address, HashSet<StoreKey>>,
1970}
1971
1972impl<'de> Deserialize<'de> for TracingResult {
1975 fn deserialize<D>(deserializer: D) -> Result<TracingResult, D::Error>
1976 where
1977 D: Deserializer<'de>,
1978 {
1979 use serde::de::Error;
1980 use serde_json::Value;
1981
1982 let value = Value::deserialize(deserializer)?;
1983 let mut result = TracingResult::default();
1984
1985 if let Value::Object(map) = value {
1986 if let Some(retriggers_value) = map.get("retriggers") {
1988 result.retriggers =
1989 deserialize_retriggers_from_value(retriggers_value).map_err(|e| {
1990 D::Error::custom(format!("Failed to deserialize retriggers: {}", e))
1991 })?;
1992 }
1993
1994 if let Some(accessed_slots_value) = map.get("accessed_slots") {
1996 result.accessed_slots = serde_json::from_value(accessed_slots_value.clone())
1997 .map_err(|e| {
1998 D::Error::custom(format!("Failed to deserialize accessed_slots: {}", e))
1999 })?;
2000 }
2001 }
2002
2003 Ok(result)
2004 }
2005}
2006
2007impl From<models::blockchain::TracingResult> for TracingResult {
2008 fn from(value: models::blockchain::TracingResult) -> Self {
2009 TracingResult {
2010 retriggers: value
2011 .retriggers
2012 .into_iter()
2013 .map(|(k, v)| (k, v.into()))
2014 .collect(),
2015 accessed_slots: value.accessed_slots,
2016 }
2017 }
2018}
2019
2020#[derive(Serialize, PartialEq, ToSchema, Eq, Clone, Debug, Deserialize, DeepSizeOf)]
2021pub struct TracedEntryPointRequestResponse {
2022 #[schema(value_type = HashMap<String, Vec<(EntryPointWithTracingParams, TracingResult)>>)]
2025 pub traced_entry_points:
2026 HashMap<ComponentId, Vec<(EntryPointWithTracingParams, TracingResult)>>,
2027 pub pagination: PaginationResponse,
2028}
2029impl From<TracedEntryPointRequestResponse> for DCIUpdate {
2030 fn from(response: TracedEntryPointRequestResponse) -> Self {
2031 let mut new_entrypoints = HashMap::new();
2032 let mut new_entrypoint_params = HashMap::new();
2033 let mut trace_results = HashMap::new();
2034
2035 for (component, traces) in response.traced_entry_points {
2036 let mut entrypoints = HashSet::new();
2037
2038 for (entrypoint, trace) in traces {
2039 let entrypoint_id = entrypoint
2040 .entry_point
2041 .external_id
2042 .clone();
2043
2044 entrypoints.insert(entrypoint.entry_point.clone());
2046
2047 new_entrypoint_params
2049 .entry(entrypoint_id.clone())
2050 .or_insert_with(HashSet::new)
2051 .insert((entrypoint.params, component.clone()));
2052
2053 trace_results
2055 .entry(entrypoint_id)
2056 .and_modify(|existing_trace: &mut TracingResult| {
2057 existing_trace
2059 .retriggers
2060 .extend(trace.retriggers.clone());
2061 for (address, slots) in trace.accessed_slots.clone() {
2062 existing_trace
2063 .accessed_slots
2064 .entry(address)
2065 .or_default()
2066 .extend(slots);
2067 }
2068 })
2069 .or_insert(trace);
2070 }
2071
2072 if !entrypoints.is_empty() {
2073 new_entrypoints.insert(component, entrypoints);
2074 }
2075 }
2076
2077 DCIUpdate { new_entrypoints, new_entrypoint_params, trace_results }
2078 }
2079}
2080
2081#[derive(Serialize, Deserialize, Debug, Default, PartialEq, ToSchema, Eq, Clone)]
2082pub struct AddEntryPointRequestBody {
2083 #[serde(default)]
2084 pub chain: Chain,
2085 #[schema(value_type=String)]
2086 #[serde(default)]
2087 pub block_hash: Bytes,
2088 #[schema(value_type = Vec<(String, Vec<EntryPointWithTracingParams>)>)]
2090 pub entry_points_with_tracing_data: Vec<(ComponentId, Vec<EntryPointWithTracingParams>)>,
2091}
2092
2093#[derive(Serialize, PartialEq, ToSchema, Eq, Clone, Debug, Deserialize)]
2094pub struct AddEntryPointRequestResponse {
2095 #[schema(value_type = HashMap<String, Vec<(EntryPointWithTracingParams, TracingResult)>>)]
2098 pub traced_entry_points:
2099 HashMap<ComponentId, Vec<(EntryPointWithTracingParams, TracingResult)>>,
2100}
2101
2102#[cfg(test)]
2103mod test {
2104 use std::str::FromStr;
2105
2106 use maplit::hashmap;
2107 use rstest::rstest;
2108
2109 use super::*;
2110
2111 #[rstest]
2114 #[case::legacy_format(None, false)]
2115 #[case::explicit_true(Some(true), true)]
2116 #[case::explicit_false(Some(false), false)]
2117 fn test_subscribe_compression_backward_compatibility(
2118 #[case] compression: Option<bool>,
2119 #[case] expected: bool,
2120 ) {
2121 use serde_json::json;
2122
2123 let mut json_value = json!({
2124 "method": "subscribe",
2125 "extractor_id": {
2126 "chain": "ethereum",
2127 "name": "test"
2128 },
2129 "include_state": true
2130 });
2131
2132 if let Some(value) = compression {
2133 json_value["compression"] = json!(value);
2134 }
2135
2136 let command: Command =
2137 serde_json::from_value(json_value).expect("Failed to deserialize Subscribe command");
2138
2139 if let Command::Subscribe { compression, .. } = command {
2140 assert_eq!(compression, expected);
2141 } else {
2142 panic!("Expected Subscribe command");
2143 }
2144 }
2145
2146 #[rstest]
2149 #[case::legacy_format(None, false)]
2150 #[case::explicit_true(Some(true), true)]
2151 #[case::explicit_false(Some(false), false)]
2152 fn test_subscribe_partial_blocks_backward_compatibility(
2153 #[case] partial_blocks: Option<bool>,
2154 #[case] expected: bool,
2155 ) {
2156 use serde_json::json;
2157
2158 let mut json_value = json!({
2159 "method": "subscribe",
2160 "extractor_id": {
2161 "chain": "ethereum",
2162 "name": "test"
2163 },
2164 "include_state": true
2165 });
2166
2167 if let Some(value) = partial_blocks {
2168 json_value["partial_blocks"] = json!(value);
2169 }
2170
2171 let command: Command =
2172 serde_json::from_value(json_value).expect("Failed to deserialize Subscribe command");
2173
2174 if let Command::Subscribe { partial_blocks, .. } = command {
2175 assert_eq!(partial_blocks, expected);
2176 } else {
2177 panic!("Expected Subscribe command");
2178 }
2179 }
2180
2181 #[rstest]
2184 #[case::legacy_format(None, vec![])]
2185 #[case::with_dci(Some(vec!["vm:curve"]), vec!["vm:curve"])]
2186 #[case::empty_dci(Some(vec![]), vec![])]
2187 fn test_protocol_systems_dci_backward_compatibility(
2188 #[case] dci_protocols: Option<Vec<&str>>,
2189 #[case] expected: Vec<&str>,
2190 ) {
2191 use serde_json::json;
2192
2193 let mut json_value = json!({
2194 "protocol_systems": ["uniswap_v2", "vm:curve"],
2195 "pagination": { "page": 0, "page_size": 20, "total": 2 }
2196 });
2197
2198 if let Some(dci) = dci_protocols {
2199 json_value["dci_protocols"] = json!(dci);
2200 }
2201
2202 let resp: ProtocolSystemsRequestResponse =
2203 serde_json::from_value(json_value).expect("Failed to deserialize response");
2204
2205 assert_eq!(resp.dci_protocols, expected);
2206
2207 let serialized = serde_json::to_string(&resp).unwrap();
2209 let round_tripped: ProtocolSystemsRequestResponse =
2210 serde_json::from_str(&serialized).unwrap();
2211 assert_eq!(resp, round_tripped);
2212 }
2213
2214 #[test]
2215 fn test_tracing_result_backward_compatibility() {
2216 use serde_json::json;
2217
2218 let old_format_json = json!({
2220 "retriggers": [
2221 ["0x01", "0x02"],
2222 ["0x03", "0x04"]
2223 ],
2224 "accessed_slots": {
2225 "0x05": ["0x06", "0x07"]
2226 }
2227 });
2228
2229 let result: TracingResult = serde_json::from_value(old_format_json).unwrap();
2230
2231 assert_eq!(result.retriggers.len(), 2);
2233 let retriggers_vec: Vec<_> = result.retriggers.iter().collect();
2234 assert!(retriggers_vec.iter().any(|(k, v)| {
2235 k == &Bytes::from("0x01") && v.key == Bytes::from("0x02") && v.offset == 12
2236 }));
2237 assert!(retriggers_vec.iter().any(|(k, v)| {
2238 k == &Bytes::from("0x03") && v.key == Bytes::from("0x04") && v.offset == 12
2239 }));
2240
2241 let new_format_json = json!({
2243 "retriggers": [
2244 ["0x01", {"key": "0x02", "offset": 12}],
2245 ["0x03", {"key": "0x04", "offset": 5}]
2246 ],
2247 "accessed_slots": {
2248 "0x05": ["0x06", "0x07"]
2249 }
2250 });
2251
2252 let result2: TracingResult = serde_json::from_value(new_format_json).unwrap();
2253
2254 assert_eq!(result2.retriggers.len(), 2);
2256 let retriggers_vec2: Vec<_> = result2.retriggers.iter().collect();
2257 assert!(retriggers_vec2.iter().any(|(k, v)| {
2258 k == &Bytes::from("0x01") && v.key == Bytes::from("0x02") && v.offset == 12
2259 }));
2260 assert!(retriggers_vec2.iter().any(|(k, v)| {
2261 k == &Bytes::from("0x03") && v.key == Bytes::from("0x04") && v.offset == 5
2262 }));
2263 }
2264
2265 #[rstest]
2266 #[case::legacy_format(None, None)]
2267 #[case::full_block(Some(None), None)]
2268 #[case::partial_block(Some(Some(1)), Some(1))]
2269 fn test_block_changes_is_partial_backward_compatibility(
2270 #[case] has_partial_value: Option<Option<u32>>,
2271 #[case] expected: Option<u32>,
2272 ) {
2273 use serde_json::json;
2274
2275 let mut json_value = json!({
2276 "extractor": "test_extractor",
2277 "chain": "ethereum",
2278 "block": {
2279 "number": 100,
2280 "hash": "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef",
2281 "parent_hash": "0xabcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890",
2282 "chain": "ethereum",
2283 "ts": "2024-01-01T00:00:00"
2284 },
2285 "finalized_block_height": 99,
2286 "revert": false,
2287 "new_tokens": {},
2288 "account_updates": {},
2289 "state_updates": {},
2290 "new_protocol_components": {},
2291 "deleted_protocol_components": {},
2292 "component_balances": {},
2293 "account_balances": {},
2294 "component_tvl": {},
2295 "dci_update": {
2296 "new_entrypoints": {},
2297 "new_entrypoint_params": {},
2298 "trace_results": {}
2299 }
2300 });
2301
2302 if let Some(partial_value) = has_partial_value {
2304 json_value["partial_block_index"] = json!(partial_value);
2305 }
2306
2307 let block_changes: BlockChanges =
2308 serde_json::from_value(json_value).expect("Failed to deserialize BlockChanges");
2309
2310 assert_eq!(block_changes.partial_block_index, expected);
2311 }
2312
2313 #[test]
2314 fn test_protocol_components_equality() {
2315 let body1 = ProtocolComponentsRequestBody {
2316 protocol_system: "protocol1".to_string(),
2317 component_ids: Some(vec!["component1".to_string(), "component2".to_string()]),
2318 tvl_gt: Some(1000.0),
2319 chain: Chain::Ethereum,
2320 pagination: PaginationParams::default(),
2321 };
2322
2323 let body2 = ProtocolComponentsRequestBody {
2324 protocol_system: "protocol1".to_string(),
2325 component_ids: Some(vec!["component1".to_string(), "component2".to_string()]),
2326 tvl_gt: Some(1000.0 + 1e-7), chain: Chain::Ethereum,
2328 pagination: PaginationParams::default(),
2329 };
2330
2331 assert_eq!(body1, body2);
2333 }
2334
2335 #[test]
2336 fn test_protocol_components_inequality() {
2337 let body1 = ProtocolComponentsRequestBody {
2338 protocol_system: "protocol1".to_string(),
2339 component_ids: Some(vec!["component1".to_string(), "component2".to_string()]),
2340 tvl_gt: Some(1000.0),
2341 chain: Chain::Ethereum,
2342 pagination: PaginationParams::default(),
2343 };
2344
2345 let body2 = ProtocolComponentsRequestBody {
2346 protocol_system: "protocol1".to_string(),
2347 component_ids: Some(vec!["component1".to_string(), "component2".to_string()]),
2348 tvl_gt: Some(1000.0 + 1e-5), chain: Chain::Ethereum,
2350 pagination: PaginationParams::default(),
2351 };
2352
2353 assert_ne!(body1, body2);
2355 }
2356
2357 #[test]
2358 fn test_parse_state_request() {
2359 let json_str = r#"
2360 {
2361 "contractIds": [
2362 "0xb4eccE46b8D4e4abFd03C9B806276A6735C9c092"
2363 ],
2364 "protocol_system": "uniswap_v2",
2365 "version": {
2366 "timestamp": "2069-01-01T04:20:00",
2367 "block": {
2368 "hash": "0x24101f9cb26cd09425b52da10e8c2f56ede94089a8bbe0f31f1cda5f4daa52c4",
2369 "number": 213,
2370 "chain": "ethereum"
2371 }
2372 }
2373 }
2374 "#;
2375
2376 let result: StateRequestBody = serde_json::from_str(json_str).unwrap();
2377
2378 let contract0 = "b4eccE46b8D4e4abFd03C9B806276A6735C9c092"
2379 .parse()
2380 .unwrap();
2381 let block_hash = "24101f9cb26cd09425b52da10e8c2f56ede94089a8bbe0f31f1cda5f4daa52c4"
2382 .parse()
2383 .unwrap();
2384 let block_number = 213;
2385
2386 let expected_timestamp =
2387 NaiveDateTime::parse_from_str("2069-01-01T04:20:00", "%Y-%m-%dT%H:%M:%S").unwrap();
2388
2389 let expected = StateRequestBody {
2390 contract_ids: Some(vec![contract0]),
2391 protocol_system: "uniswap_v2".to_string(),
2392 version: VersionParam {
2393 timestamp: Some(expected_timestamp),
2394 block: Some(BlockParam {
2395 hash: Some(block_hash),
2396 chain: Some(Chain::Ethereum),
2397 number: Some(block_number),
2398 }),
2399 },
2400 chain: Chain::Ethereum,
2401 pagination: PaginationParams::default(),
2402 };
2403
2404 assert_eq!(result, expected);
2405 }
2406
2407 #[test]
2408 fn test_parse_state_request_dual_interface() {
2409 let json_common = r#"
2410 {
2411 "__CONTRACT_IDS__": [
2412 "0xb4eccE46b8D4e4abFd03C9B806276A6735C9c092"
2413 ],
2414 "version": {
2415 "timestamp": "2069-01-01T04:20:00",
2416 "block": {
2417 "hash": "0x24101f9cb26cd09425b52da10e8c2f56ede94089a8bbe0f31f1cda5f4daa52c4",
2418 "number": 213,
2419 "chain": "ethereum"
2420 }
2421 }
2422 }
2423 "#;
2424
2425 let json_str_snake = json_common.replace("\"__CONTRACT_IDS__\"", "\"contract_ids\"");
2426 let json_str_camel = json_common.replace("\"__CONTRACT_IDS__\"", "\"contractIds\"");
2427
2428 let snake: StateRequestBody = serde_json::from_str(&json_str_snake).unwrap();
2429 let camel: StateRequestBody = serde_json::from_str(&json_str_camel).unwrap();
2430
2431 assert_eq!(snake, camel);
2432 }
2433
2434 #[test]
2435 fn test_parse_state_request_unknown_field() {
2436 let body = r#"
2437 {
2438 "contract_ids_with_typo_error": [
2439 {
2440 "address": "0xb4eccE46b8D4e4abFd03C9B806276A6735C9c092",
2441 "chain": "ethereum"
2442 }
2443 ],
2444 "version": {
2445 "timestamp": "2069-01-01T04:20:00",
2446 "block": {
2447 "hash": "0x24101f9cb26cd09425b52da10e8c2f56ede94089a8bbe0f31f1cda5f4daa52c4",
2448 "parentHash": "0x8d75152454e60413efe758cc424bfd339897062d7e658f302765eb7b50971815",
2449 "number": 213,
2450 "chain": "ethereum"
2451 }
2452 }
2453 }
2454 "#;
2455
2456 let decoded = serde_json::from_str::<StateRequestBody>(body);
2457
2458 assert!(decoded.is_err(), "Expected an error due to unknown field");
2459
2460 if let Err(e) = decoded {
2461 assert!(
2462 e.to_string()
2463 .contains("unknown field `contract_ids_with_typo_error`"),
2464 "Error message does not contain expected unknown field information"
2465 );
2466 }
2467 }
2468
2469 #[test]
2470 fn test_parse_state_request_no_contract_specified() {
2471 let json_str = r#"
2472 {
2473 "protocol_system": "uniswap_v2",
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 let result: StateRequestBody = serde_json::from_str(json_str).unwrap();
2486
2487 let block_hash = "24101f9cb26cd09425b52da10e8c2f56ede94089a8bbe0f31f1cda5f4daa52c4".into();
2488 let block_number = 213;
2489 let expected_timestamp =
2490 NaiveDateTime::parse_from_str("2069-01-01T04:20:00", "%Y-%m-%dT%H:%M:%S").unwrap();
2491
2492 let expected = StateRequestBody {
2493 contract_ids: None,
2494 protocol_system: "uniswap_v2".to_string(),
2495 version: VersionParam {
2496 timestamp: Some(expected_timestamp),
2497 block: Some(BlockParam {
2498 hash: Some(block_hash),
2499 chain: Some(Chain::Ethereum),
2500 number: Some(block_number),
2501 }),
2502 },
2503 chain: Chain::Ethereum,
2504 pagination: PaginationParams { page: 0, page_size: 100 },
2505 };
2506
2507 assert_eq!(result, expected);
2508 }
2509
2510 #[rstest]
2511 #[case::deprecated_ids(
2512 r#"
2513 {
2514 "protocol_ids": [
2515 {
2516 "id": "0xb4eccE46b8D4e4abFd03C9B806276A6735C9c092",
2517 "chain": "ethereum"
2518 }
2519 ],
2520 "protocol_system": "uniswap_v2",
2521 "include_balances": false,
2522 "version": {
2523 "timestamp": "2069-01-01T04:20:00",
2524 "block": {
2525 "hash": "0x24101f9cb26cd09425b52da10e8c2f56ede94089a8bbe0f31f1cda5f4daa52c4",
2526 "number": 213,
2527 "chain": "ethereum"
2528 }
2529 }
2530 }
2531 "#
2532 )]
2533 #[case(
2534 r#"
2535 {
2536 "protocolIds": [
2537 "0xb4eccE46b8D4e4abFd03C9B806276A6735C9c092"
2538 ],
2539 "protocol_system": "uniswap_v2",
2540 "include_balances": false,
2541 "version": {
2542 "timestamp": "2069-01-01T04:20:00",
2543 "block": {
2544 "hash": "0x24101f9cb26cd09425b52da10e8c2f56ede94089a8bbe0f31f1cda5f4daa52c4",
2545 "number": 213,
2546 "chain": "ethereum"
2547 }
2548 }
2549 }
2550 "#
2551 )]
2552 fn test_parse_protocol_state_request(#[case] json_str: &str) {
2553 let result: ProtocolStateRequestBody = serde_json::from_str(json_str).unwrap();
2554
2555 let block_hash = "24101f9cb26cd09425b52da10e8c2f56ede94089a8bbe0f31f1cda5f4daa52c4"
2556 .parse()
2557 .unwrap();
2558 let block_number = 213;
2559
2560 let expected_timestamp =
2561 NaiveDateTime::parse_from_str("2069-01-01T04:20:00", "%Y-%m-%dT%H:%M:%S").unwrap();
2562
2563 let expected = ProtocolStateRequestBody {
2564 protocol_ids: Some(vec!["0xb4eccE46b8D4e4abFd03C9B806276A6735C9c092".to_string()]),
2565 protocol_system: "uniswap_v2".to_string(),
2566 version: VersionParam {
2567 timestamp: Some(expected_timestamp),
2568 block: Some(BlockParam {
2569 hash: Some(block_hash),
2570 chain: Some(Chain::Ethereum),
2571 number: Some(block_number),
2572 }),
2573 },
2574 chain: Chain::Ethereum,
2575 include_balances: false,
2576 pagination: PaginationParams::default(),
2577 };
2578
2579 assert_eq!(result, expected);
2580 }
2581
2582 #[rstest]
2583 #[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()])]
2584 #[case::with_strings(vec!["id1".to_string(), "id2".to_string()], vec!["id1".to_string(), "id2".to_string()])]
2585 fn test_id_filtered<T>(#[case] input_ids: Vec<T>, #[case] expected_ids: Vec<String>)
2586 where
2587 T: Into<String> + Clone,
2588 {
2589 let request_body = ProtocolStateRequestBody::id_filtered(input_ids);
2590
2591 assert_eq!(request_body.protocol_ids, Some(expected_ids));
2592 }
2593
2594 fn create_models_block_changes() -> crate::models::blockchain::BlockAggregatedChanges {
2595 let base_ts = 1694534400; crate::models::blockchain::BlockAggregatedChanges {
2598 extractor: "native_name".to_string(),
2599 block: models::blockchain::Block::new(
2600 3,
2601 models::Chain::Ethereum,
2602 Bytes::from_str("0x0000000000000000000000000000000000000000000000000000000000000003").unwrap(),
2603 Bytes::from_str("0x0000000000000000000000000000000000000000000000000000000000000002").unwrap(),
2604 chrono::DateTime::from_timestamp(base_ts + 3000, 0).unwrap().naive_utc(),
2605 ),
2606 db_committed_block_height: Some(1),
2607 finalized_block_height: 1,
2608 revert: true,
2609 state_deltas: HashMap::from([
2610 ("pc_1".to_string(), models::protocol::ProtocolComponentStateDelta {
2611 component_id: "pc_1".to_string(),
2612 updated_attributes: HashMap::from([
2613 ("attr_2".to_string(), Bytes::from("0x0000000000000002")),
2614 ("attr_1".to_string(), Bytes::from("0x00000000000003e8")),
2615 ]),
2616 deleted_attributes: HashSet::new(),
2617 }),
2618 ]),
2619 new_protocol_components: HashMap::from([
2620 ("pc_2".to_string(), crate::models::protocol::ProtocolComponent {
2621 id: "pc_2".to_string(),
2622 protocol_system: "native_protocol_system".to_string(),
2623 protocol_type_name: "pt_1".to_string(),
2624 chain: models::Chain::Ethereum,
2625 tokens: vec![
2626 Bytes::from_str("0xdac17f958d2ee523a2206206994597c13d831ec7").unwrap(),
2627 Bytes::from_str("0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48").unwrap(),
2628 ],
2629 contract_addresses: vec![],
2630 static_attributes: HashMap::new(),
2631 change: models::ChangeType::Creation,
2632 creation_tx: Bytes::from_str("0x000000000000000000000000000000000000000000000000000000000000c351").unwrap(),
2633 created_at: chrono::DateTime::from_timestamp(base_ts + 5000, 0).unwrap().naive_utc(),
2634 }),
2635 ]),
2636 deleted_protocol_components: HashMap::from([
2637 ("pc_3".to_string(), crate::models::protocol::ProtocolComponent {
2638 id: "pc_3".to_string(),
2639 protocol_system: "native_protocol_system".to_string(),
2640 protocol_type_name: "pt_2".to_string(),
2641 chain: models::Chain::Ethereum,
2642 tokens: vec![
2643 Bytes::from_str("0x6b175474e89094c44da98b954eedeac495271d0f").unwrap(),
2644 Bytes::from_str("0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2").unwrap(),
2645 ],
2646 contract_addresses: vec![],
2647 static_attributes: HashMap::new(),
2648 change: models::ChangeType::Deletion,
2649 creation_tx: Bytes::from_str("0x0000000000000000000000000000000000000000000000000000000000009c41").unwrap(),
2650 created_at: chrono::DateTime::from_timestamp(base_ts + 4000, 0).unwrap().naive_utc(),
2651 }),
2652 ]),
2653 component_balances: HashMap::from([
2654 ("pc_1".to_string(), HashMap::from([
2655 (Bytes::from_str("0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48").unwrap(), models::protocol::ComponentBalance {
2656 token: Bytes::from_str("0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48").unwrap(),
2657 balance: Bytes::from("0x00000001"),
2658 balance_float: 1.0,
2659 modify_tx: Bytes::from_str("0x0000000000000000000000000000000000000000000000000000000000000000").unwrap(),
2660 component_id: "pc_1".to_string(),
2661 }),
2662 (Bytes::from_str("0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2").unwrap(), models::protocol::ComponentBalance {
2663 token: Bytes::from_str("0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2").unwrap(),
2664 balance: Bytes::from("0x000003e8"),
2665 balance_float: 1000.0,
2666 modify_tx: Bytes::from_str("0x0000000000000000000000000000000000000000000000000000000000007531").unwrap(),
2667 component_id: "pc_1".to_string(),
2668 }),
2669 ])),
2670 ]),
2671 account_balances: HashMap::from([
2672 (Bytes::from_str("0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2").unwrap(), HashMap::from([
2673 (Bytes::from_str("0x7a250d5630b4cf539739df2c5dacb4c659f2488d").unwrap(), models::contract::AccountBalance {
2674 account: Bytes::from_str("0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2").unwrap(),
2675 token: Bytes::from_str("0x7a250d5630b4cf539739df2c5dacb4c659f2488d").unwrap(),
2676 balance: Bytes::from("0x000003e8"),
2677 modify_tx: Bytes::from_str("0x0000000000000000000000000000000000000000000000000000000000007531").unwrap(),
2678 }),
2679 ])),
2680 ]),
2681 ..Default::default()
2682 }
2683 }
2684
2685 #[test]
2686 fn test_serialize_deserialize_block_changes() {
2687 let block_entity_changes = create_models_block_changes();
2692
2693 let json_data = serde_json::to_string(&block_entity_changes).expect("Failed to serialize");
2695
2696 serde_json::from_str::<BlockChanges>(&json_data).expect("parsing failed");
2698 }
2699
2700 #[test]
2701 fn test_parse_block_changes() {
2702 let json_data = r#"
2703 {
2704 "extractor": "vm:ambient",
2705 "chain": "ethereum",
2706 "block": {
2707 "number": 123,
2708 "hash": "0x0000000000000000000000000000000000000000000000000000000000000000",
2709 "parent_hash": "0x0000000000000000000000000000000000000000000000000000000000000000",
2710 "chain": "ethereum",
2711 "ts": "2023-09-14T00:00:00"
2712 },
2713 "finalized_block_height": 0,
2714 "revert": false,
2715 "new_tokens": {},
2716 "account_updates": {
2717 "0x7a250d5630b4cf539739df2c5dacb4c659f2488d": {
2718 "address": "0x7a250d5630b4cf539739df2c5dacb4c659f2488d",
2719 "chain": "ethereum",
2720 "slots": {},
2721 "balance": "0x01f4",
2722 "code": "",
2723 "change": "Update"
2724 }
2725 },
2726 "state_updates": {
2727 "component_1": {
2728 "component_id": "component_1",
2729 "updated_attributes": {"attr1": "0x01"},
2730 "deleted_attributes": ["attr2"]
2731 }
2732 },
2733 "new_protocol_components":
2734 { "protocol_1": {
2735 "id": "protocol_1",
2736 "protocol_system": "system_1",
2737 "protocol_type_name": "type_1",
2738 "chain": "ethereum",
2739 "tokens": ["0x01", "0x02"],
2740 "contract_ids": ["0x01", "0x02"],
2741 "static_attributes": {"attr1": "0x01f4"},
2742 "change": "Update",
2743 "creation_tx": "0x01",
2744 "created_at": "2023-09-14T00:00:00"
2745 }
2746 },
2747 "deleted_protocol_components": {},
2748 "component_balances": {
2749 "protocol_1":
2750 {
2751 "0x01": {
2752 "token": "0x01",
2753 "balance": "0xb77831d23691653a01",
2754 "balance_float": 3.3844151001790677e21,
2755 "modify_tx": "0x01",
2756 "component_id": "protocol_1"
2757 }
2758 }
2759 },
2760 "account_balances": {
2761 "0x7a250d5630b4cf539739df2c5dacb4c659f2488d": {
2762 "0x7a250d5630b4cf539739df2c5dacb4c659f2488d": {
2763 "account": "0x7a250d5630b4cf539739df2c5dacb4c659f2488d",
2764 "token": "0x7a250d5630b4cf539739df2c5dacb4c659f2488d",
2765 "balance": "0x01f4",
2766 "modify_tx": "0x01"
2767 }
2768 }
2769 },
2770 "component_tvl": {
2771 "protocol_1": 1000.0
2772 },
2773 "dci_update": {
2774 "new_entrypoints": {
2775 "component_1": [
2776 {
2777 "external_id": "0x01:sig()",
2778 "target": "0x01",
2779 "signature": "sig()"
2780 }
2781 ]
2782 },
2783 "new_entrypoint_params": {
2784 "0x01:sig()": [
2785 [
2786 {
2787 "method": "rpctracer",
2788 "caller": "0x01",
2789 "calldata": "0x02"
2790 },
2791 "component_1"
2792 ]
2793 ]
2794 },
2795 "trace_results": {
2796 "0x01:sig()": {
2797 "retriggers": [
2798 ["0x01", {"key": "0x02", "offset": 12}]
2799 ],
2800 "accessed_slots": {
2801 "0x03": ["0x03", "0x04"]
2802 }
2803 }
2804 }
2805 }
2806 }
2807 "#;
2808
2809 serde_json::from_str::<BlockChanges>(json_data).expect("parsing failed");
2810 }
2811
2812 #[test]
2813 fn test_parse_websocket_message() {
2814 let json_data = r#"
2815 {
2816 "subscription_id": "5d23bfbe-89ad-4ea3-8672-dc9e973ac9dc",
2817 "deltas": {
2818 "type": "BlockChanges",
2819 "extractor": "uniswap_v2",
2820 "chain": "ethereum",
2821 "block": {
2822 "number": 19291517,
2823 "hash": "0xbc3ea4896c0be8da6229387a8571b72818aa258daf4fab46471003ad74c4ee83",
2824 "parent_hash": "0x89ca5b8d593574cf6c886f41ef8208bf6bdc1a90ef36046cb8c84bc880b9af8f",
2825 "chain": "ethereum",
2826 "ts": "2024-02-23T16:35:35"
2827 },
2828 "finalized_block_height": 0,
2829 "revert": false,
2830 "new_tokens": {},
2831 "account_updates": {
2832 "0x7a250d5630b4cf539739df2c5dacb4c659f2488d": {
2833 "address": "0x7a250d5630b4cf539739df2c5dacb4c659f2488d",
2834 "chain": "ethereum",
2835 "slots": {},
2836 "balance": "0x01f4",
2837 "code": "",
2838 "change": "Update"
2839 }
2840 },
2841 "state_updates": {
2842 "0xde6faedbcae38eec6d33ad61473a04a6dd7f6e28": {
2843 "component_id": "0xde6faedbcae38eec6d33ad61473a04a6dd7f6e28",
2844 "updated_attributes": {
2845 "reserve0": "0x87f7b5973a7f28a8b32404",
2846 "reserve1": "0x09e9564b11"
2847 },
2848 "deleted_attributes": []
2849 },
2850 "0x99c59000f5a76c54c4fd7d82720c045bdcf1450d": {
2851 "component_id": "0x99c59000f5a76c54c4fd7d82720c045bdcf1450d",
2852 "updated_attributes": {
2853 "reserve1": "0x44d9a8fd662c2f4d03",
2854 "reserve0": "0x500b1261f811d5bf423e"
2855 },
2856 "deleted_attributes": []
2857 }
2858 },
2859 "new_protocol_components": {},
2860 "deleted_protocol_components": {},
2861 "component_balances": {
2862 "0x99c59000f5a76c54c4fd7d82720c045bdcf1450d": {
2863 "0x9012744b7a564623b6c3e40b144fc196bdedf1a9": {
2864 "token": "0x9012744b7a564623b6c3e40b144fc196bdedf1a9",
2865 "balance": "0x500b1261f811d5bf423e",
2866 "balance_float": 3.779935574269033E23,
2867 "modify_tx": "0xe46c4db085fb6c6f3408a65524555797adb264e1d5cf3b66ad154598f85ac4bf",
2868 "component_id": "0x99c59000f5a76c54c4fd7d82720c045bdcf1450d"
2869 },
2870 "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2": {
2871 "token": "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2",
2872 "balance": "0x44d9a8fd662c2f4d03",
2873 "balance_float": 1.270062661329837E21,
2874 "modify_tx": "0xe46c4db085fb6c6f3408a65524555797adb264e1d5cf3b66ad154598f85ac4bf",
2875 "component_id": "0x99c59000f5a76c54c4fd7d82720c045bdcf1450d"
2876 }
2877 }
2878 },
2879 "account_balances": {
2880 "0x7a250d5630b4cf539739df2c5dacb4c659f2488d": {
2881 "0x7a250d5630b4cf539739df2c5dacb4c659f2488d": {
2882 "account": "0x7a250d5630b4cf539739df2c5dacb4c659f2488d",
2883 "token": "0x7a250d5630b4cf539739df2c5dacb4c659f2488d",
2884 "balance": "0x01f4",
2885 "modify_tx": "0x01"
2886 }
2887 }
2888 },
2889 "component_tvl": {},
2890 "dci_update": {
2891 "new_entrypoints": {
2892 "0xde6faedbcae38eec6d33ad61473a04a6dd7f6e28": [
2893 {
2894 "external_id": "0x01:sig()",
2895 "target": "0x01",
2896 "signature": "sig()"
2897 }
2898 ]
2899 },
2900 "new_entrypoint_params": {
2901 "0x01:sig()": [
2902 [
2903 {
2904 "method": "rpctracer",
2905 "caller": "0x01",
2906 "calldata": "0x02"
2907 },
2908 "0xde6faedbcae38eec6d33ad61473a04a6dd7f6e28"
2909 ]
2910 ]
2911 },
2912 "trace_results": {
2913 "0x01:sig()": {
2914 "retriggers": [
2915 ["0x01", {"key": "0x02", "offset": 12}]
2916 ],
2917 "accessed_slots": {
2918 "0x03": ["0x03", "0x04"]
2919 }
2920 }
2921 }
2922 }
2923 }
2924 }
2925 "#;
2926 serde_json::from_str::<WebSocketMessage>(json_data).expect("parsing failed");
2927 }
2928
2929 #[test]
2930 fn test_protocol_state_delta_merge_update_delete() {
2931 let mut delta1 = ProtocolStateDelta {
2933 component_id: "Component1".to_string(),
2934 updated_attributes: HashMap::from([(
2935 "Attribute1".to_string(),
2936 Bytes::from("0xbadbabe420"),
2937 )]),
2938 deleted_attributes: HashSet::new(),
2939 };
2940 let delta2 = ProtocolStateDelta {
2941 component_id: "Component1".to_string(),
2942 updated_attributes: HashMap::from([(
2943 "Attribute2".to_string(),
2944 Bytes::from("0x0badbabe"),
2945 )]),
2946 deleted_attributes: HashSet::from(["Attribute1".to_string()]),
2947 };
2948 let exp = ProtocolStateDelta {
2949 component_id: "Component1".to_string(),
2950 updated_attributes: HashMap::from([(
2951 "Attribute2".to_string(),
2952 Bytes::from("0x0badbabe"),
2953 )]),
2954 deleted_attributes: HashSet::from(["Attribute1".to_string()]),
2955 };
2956
2957 delta1.merge(&delta2);
2958
2959 assert_eq!(delta1, exp);
2960 }
2961
2962 #[test]
2963 fn test_protocol_state_delta_merge_delete_update() {
2964 let mut delta1 = ProtocolStateDelta {
2966 component_id: "Component1".to_string(),
2967 updated_attributes: HashMap::new(),
2968 deleted_attributes: HashSet::from(["Attribute1".to_string()]),
2969 };
2970 let delta2 = ProtocolStateDelta {
2971 component_id: "Component1".to_string(),
2972 updated_attributes: HashMap::from([(
2973 "Attribute1".to_string(),
2974 Bytes::from("0x0badbabe"),
2975 )]),
2976 deleted_attributes: HashSet::new(),
2977 };
2978 let exp = ProtocolStateDelta {
2979 component_id: "Component1".to_string(),
2980 updated_attributes: HashMap::from([(
2981 "Attribute1".to_string(),
2982 Bytes::from("0x0badbabe"),
2983 )]),
2984 deleted_attributes: HashSet::new(),
2985 };
2986
2987 delta1.merge(&delta2);
2988
2989 assert_eq!(delta1, exp);
2990 }
2991
2992 #[test]
2993 fn test_account_update_merge() {
2994 let mut account1 = AccountUpdate::new(
2996 Bytes::from(b"0x1234"),
2997 Chain::Ethereum,
2998 HashMap::from([(Bytes::from("0xaabb"), Bytes::from("0xccdd"))]),
2999 Some(Bytes::from("0x1000")),
3000 Some(Bytes::from("0xdeadbeaf")),
3001 ChangeType::Creation,
3002 );
3003
3004 let account2 = AccountUpdate::new(
3005 Bytes::from(b"0x1234"), Chain::Ethereum,
3007 HashMap::from([(Bytes::from("0xeeff"), Bytes::from("0x11223344"))]),
3008 Some(Bytes::from("0x2000")),
3009 Some(Bytes::from("0xcafebabe")),
3010 ChangeType::Update,
3011 );
3012
3013 account1.merge(&account2);
3015
3016 let expected = AccountUpdate::new(
3018 Bytes::from(b"0x1234"), Chain::Ethereum,
3020 HashMap::from([
3021 (Bytes::from("0xaabb"), Bytes::from("0xccdd")), (Bytes::from("0xeeff"), Bytes::from("0x11223344")), ]),
3024 Some(Bytes::from("0x2000")), Some(Bytes::from("0xcafebabe")), ChangeType::Creation, );
3028
3029 assert_eq!(account1, expected);
3031 }
3032
3033 #[test]
3034 fn test_block_account_changes_merge() {
3035 let old_account_updates: HashMap<Bytes, AccountUpdate> = [(
3037 Bytes::from("0x0011"),
3038 AccountUpdate {
3039 address: Bytes::from("0x00"),
3040 chain: Chain::Ethereum,
3041 slots: HashMap::from([(Bytes::from("0x0022"), Bytes::from("0x0033"))]),
3042 balance: Some(Bytes::from("0x01")),
3043 code: Some(Bytes::from("0x02")),
3044 change: ChangeType::Creation,
3045 },
3046 )]
3047 .into_iter()
3048 .collect();
3049 let new_account_updates: HashMap<Bytes, AccountUpdate> = [(
3050 Bytes::from("0x0011"),
3051 AccountUpdate {
3052 address: Bytes::from("0x00"),
3053 chain: Chain::Ethereum,
3054 slots: HashMap::from([(Bytes::from("0x0044"), Bytes::from("0x0055"))]),
3055 balance: Some(Bytes::from("0x03")),
3056 code: Some(Bytes::from("0x04")),
3057 change: ChangeType::Update,
3058 },
3059 )]
3060 .into_iter()
3061 .collect();
3062 let block_account_changes_initial = BlockChanges {
3064 extractor: "extractor1".to_string(),
3065 revert: false,
3066 account_updates: old_account_updates,
3067 ..Default::default()
3068 };
3069
3070 let block_account_changes_new = BlockChanges {
3071 extractor: "extractor2".to_string(),
3072 revert: true,
3073 account_updates: new_account_updates,
3074 ..Default::default()
3075 };
3076
3077 let res = block_account_changes_initial.merge(block_account_changes_new);
3079
3080 let expected_account_updates: HashMap<Bytes, AccountUpdate> = [(
3082 Bytes::from("0x0011"),
3083 AccountUpdate {
3084 address: Bytes::from("0x00"),
3085 chain: Chain::Ethereum,
3086 slots: HashMap::from([
3087 (Bytes::from("0x0044"), Bytes::from("0x0055")),
3088 (Bytes::from("0x0022"), Bytes::from("0x0033")),
3089 ]),
3090 balance: Some(Bytes::from("0x03")),
3091 code: Some(Bytes::from("0x04")),
3092 change: ChangeType::Creation,
3093 },
3094 )]
3095 .into_iter()
3096 .collect();
3097 let block_account_changes_expected = BlockChanges {
3098 extractor: "extractor1".to_string(),
3099 revert: true,
3100 account_updates: expected_account_updates,
3101 ..Default::default()
3102 };
3103 assert_eq!(res, block_account_changes_expected);
3104 }
3105
3106 #[test]
3107 fn test_block_entity_changes_merge() {
3108 let block_entity_changes_result1 = BlockChanges {
3110 extractor: String::from("extractor1"),
3111 revert: false,
3112 state_updates: hashmap! { "state1".to_string() => ProtocolStateDelta::default() },
3113 new_protocol_components: hashmap! { "component1".to_string() => ProtocolComponent::default() },
3114 deleted_protocol_components: HashMap::new(),
3115 component_balances: hashmap! {
3116 "component1".to_string() => TokenBalances(hashmap! {
3117 Bytes::from("0x01") => ComponentBalance {
3118 token: Bytes::from("0x01"),
3119 balance: Bytes::from("0x01"),
3120 balance_float: 1.0,
3121 modify_tx: Bytes::from("0x00"),
3122 component_id: "component1".to_string()
3123 },
3124 Bytes::from("0x02") => ComponentBalance {
3125 token: Bytes::from("0x02"),
3126 balance: Bytes::from("0x02"),
3127 balance_float: 2.0,
3128 modify_tx: Bytes::from("0x00"),
3129 component_id: "component1".to_string()
3130 },
3131 })
3132
3133 },
3134 component_tvl: hashmap! { "tvl1".to_string() => 1000.0 },
3135 ..Default::default()
3136 };
3137 let block_entity_changes_result2 = BlockChanges {
3138 extractor: String::from("extractor2"),
3139 revert: true,
3140 state_updates: hashmap! { "state2".to_string() => ProtocolStateDelta::default() },
3141 new_protocol_components: hashmap! { "component2".to_string() => ProtocolComponent::default() },
3142 deleted_protocol_components: hashmap! { "component3".to_string() => ProtocolComponent::default() },
3143 component_balances: hashmap! {
3144 "component1".to_string() => TokenBalances::default(),
3145 "component2".to_string() => TokenBalances::default()
3146 },
3147 component_tvl: hashmap! { "tvl2".to_string() => 2000.0 },
3148 ..Default::default()
3149 };
3150
3151 let res = block_entity_changes_result1.merge(block_entity_changes_result2);
3152
3153 let expected_block_entity_changes_result = BlockChanges {
3154 extractor: String::from("extractor1"),
3155 revert: true,
3156 state_updates: hashmap! {
3157 "state1".to_string() => ProtocolStateDelta::default(),
3158 "state2".to_string() => ProtocolStateDelta::default(),
3159 },
3160 new_protocol_components: hashmap! {
3161 "component1".to_string() => ProtocolComponent::default(),
3162 "component2".to_string() => ProtocolComponent::default(),
3163 },
3164 deleted_protocol_components: hashmap! {
3165 "component3".to_string() => ProtocolComponent::default(),
3166 },
3167 component_balances: hashmap! {
3168 "component1".to_string() => TokenBalances(hashmap! {
3169 Bytes::from("0x01") => ComponentBalance {
3170 token: Bytes::from("0x01"),
3171 balance: Bytes::from("0x01"),
3172 balance_float: 1.0,
3173 modify_tx: Bytes::from("0x00"),
3174 component_id: "component1".to_string()
3175 },
3176 Bytes::from("0x02") => ComponentBalance {
3177 token: Bytes::from("0x02"),
3178 balance: Bytes::from("0x02"),
3179 balance_float: 2.0,
3180 modify_tx: Bytes::from("0x00"),
3181 component_id: "component1".to_string()
3182 },
3183 }),
3184 "component2".to_string() => TokenBalances::default(),
3185 },
3186 component_tvl: hashmap! {
3187 "tvl1".to_string() => 1000.0,
3188 "tvl2".to_string() => 2000.0
3189 },
3190 ..Default::default()
3191 };
3192
3193 assert_eq!(res, expected_block_entity_changes_result);
3194 }
3195
3196 #[test]
3197 fn test_websocket_error_serialization() {
3198 let extractor_id = ExtractorIdentity::new(Chain::Ethereum, "test_extractor");
3199 let subscription_id = Uuid::new_v4();
3200
3201 let error = WebsocketError::ExtractorNotFound(extractor_id.clone());
3203 let json = serde_json::to_string(&error).unwrap();
3204 let deserialized: WebsocketError = serde_json::from_str(&json).unwrap();
3205 assert_eq!(error, deserialized);
3206
3207 let error = WebsocketError::SubscriptionNotFound(subscription_id);
3209 let json = serde_json::to_string(&error).unwrap();
3210 let deserialized: WebsocketError = serde_json::from_str(&json).unwrap();
3211 assert_eq!(error, deserialized);
3212
3213 let error = WebsocketError::ParseError("{asd".to_string(), "invalid json".to_string());
3215 let json = serde_json::to_string(&error).unwrap();
3216 let deserialized: WebsocketError = serde_json::from_str(&json).unwrap();
3217 assert_eq!(error, deserialized);
3218
3219 let error = WebsocketError::SubscribeError(extractor_id.clone());
3221 let json = serde_json::to_string(&error).unwrap();
3222 let deserialized: WebsocketError = serde_json::from_str(&json).unwrap();
3223 assert_eq!(error, deserialized);
3224
3225 let error =
3227 WebsocketError::CompressionError(subscription_id, "Compression failed".to_string());
3228 let json = serde_json::to_string(&error).unwrap();
3229 let deserialized: WebsocketError = serde_json::from_str(&json).unwrap();
3230 assert_eq!(error, deserialized);
3231 }
3232
3233 #[test]
3234 fn test_websocket_message_with_error_response() {
3235 let error =
3236 WebsocketError::ParseError("}asdfas".to_string(), "malformed request".to_string());
3237 let response = Response::Error(error.clone());
3238 let message = WebSocketMessage::Response(response);
3239
3240 let json = serde_json::to_string(&message).unwrap();
3241 let deserialized: WebSocketMessage = serde_json::from_str(&json).unwrap();
3242
3243 if let WebSocketMessage::Response(Response::Error(deserialized_error)) = deserialized {
3244 assert_eq!(error, deserialized_error);
3245 } else {
3246 panic!("Expected WebSocketMessage::Response(Response::Error)");
3247 }
3248 }
3249
3250 #[test]
3251 fn test_websocket_error_conversion_from_models() {
3252 use crate::models::error::WebsocketError as ModelsError;
3253
3254 let extractor_id =
3255 crate::models::ExtractorIdentity::new(crate::models::Chain::Ethereum, "test");
3256 let subscription_id = Uuid::new_v4();
3257
3258 let models_error = ModelsError::ExtractorNotFound(extractor_id.clone());
3260 let dto_error: WebsocketError = models_error.into();
3261 assert_eq!(dto_error, WebsocketError::ExtractorNotFound(extractor_id.clone().into()));
3262
3263 let models_error = ModelsError::SubscriptionNotFound(subscription_id);
3265 let dto_error: WebsocketError = models_error.into();
3266 assert_eq!(dto_error, WebsocketError::SubscriptionNotFound(subscription_id));
3267
3268 let json_result: Result<serde_json::Value, _> = serde_json::from_str("{invalid json");
3270 let json_error = json_result.unwrap_err();
3271 let models_error = ModelsError::ParseError("{invalid json".to_string(), json_error);
3272 let dto_error: WebsocketError = models_error.into();
3273 if let WebsocketError::ParseError(msg, error) = dto_error {
3274 assert!(!error.is_empty(), "Error message should not be empty, got: '{}'", msg);
3276 } else {
3277 panic!("Expected ParseError variant");
3278 }
3279
3280 let models_error = ModelsError::SubscribeError(extractor_id.clone());
3282 let dto_error: WebsocketError = models_error.into();
3283 assert_eq!(dto_error, WebsocketError::SubscribeError(extractor_id.into()));
3284
3285 let io_error = std::io::Error::other("Compression failed");
3287 let models_error = ModelsError::CompressionError(subscription_id, io_error);
3288 let dto_error: WebsocketError = models_error.into();
3289 if let WebsocketError::CompressionError(sub_id, msg) = &dto_error {
3290 assert_eq!(*sub_id, subscription_id);
3291 assert!(msg.contains("Compression failed"));
3292 } else {
3293 panic!("Expected CompressionError variant");
3294 }
3295 }
3296}
3297
3298#[cfg(test)]
3299mod memory_size_tests {
3300 use std::collections::HashMap;
3301
3302 use super::*;
3303
3304 #[test]
3305 fn test_state_request_response_memory_size_empty() {
3306 let response = StateRequestResponse {
3307 accounts: vec![],
3308 pagination: PaginationResponse::new(1, 10, 0),
3309 };
3310
3311 let size = response.deep_size_of();
3312
3313 assert!(size >= 48, "Empty response should have minimum size of 48 bytes, got {}", size);
3315 assert!(size < 200, "Empty response should not be too large, got {}", size);
3316 }
3317
3318 #[test]
3319 fn test_state_request_response_memory_size_scales_with_slots() {
3320 let create_response_with_slots = |slot_count: usize| {
3321 let mut slots = HashMap::new();
3322 for i in 0..slot_count {
3323 let key = vec![i as u8; 32]; let value = vec![(i + 100) as u8; 32]; slots.insert(key.into(), value.into());
3326 }
3327
3328 let account = ResponseAccount::new(
3329 Chain::Ethereum,
3330 vec![1; 20].into(),
3331 "Pool".to_string(),
3332 slots,
3333 vec![1; 32].into(),
3334 HashMap::new(),
3335 vec![].into(), vec![1; 32].into(),
3337 vec![1; 32].into(),
3338 vec![1; 32].into(),
3339 None,
3340 );
3341
3342 StateRequestResponse {
3343 accounts: vec![account],
3344 pagination: PaginationResponse::new(1, 10, 1),
3345 }
3346 };
3347
3348 let small_response = create_response_with_slots(10);
3349 let large_response = create_response_with_slots(100);
3350
3351 let small_size = small_response.deep_size_of();
3352 let large_size = large_response.deep_size_of();
3353
3354 assert!(
3356 large_size > small_size * 5,
3357 "Large response ({} bytes) should be much larger than small response ({} bytes)",
3358 large_size,
3359 small_size
3360 );
3361
3362 let size_diff = large_size - small_size;
3364 let expected_min_diff = 90 * 64; assert!(
3366 size_diff > expected_min_diff,
3367 "Size difference ({} bytes) should reflect the additional slot data",
3368 size_diff
3369 );
3370 }
3371}
3372
3373#[cfg(test)]
3374mod pagination_limits_tests {
3375 use super::*;
3376
3377 #[derive(Clone, Debug)]
3379 struct TestRequestBody {
3380 pagination: PaginationParams,
3381 }
3382
3383 impl_pagination_limits!(TestRequestBody, compressed = 500, uncompressed = 50);
3385
3386 #[test]
3387 fn test_effective_max_page_size() {
3388 let max_size = TestRequestBody::effective_max_page_size(true);
3390 assert_eq!(max_size, 500, "Should return compressed limit when compression is enabled");
3391
3392 let max_size = TestRequestBody::effective_max_page_size(false);
3394 assert_eq!(max_size, 50, "Should return uncompressed limit when compression is disabled");
3395 }
3396}