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