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