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