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