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