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