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