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 {
819 ($type:ty, compressed = $comp:expr, uncompressed = $uncomp:expr) => {
820 impl $crate::dto::PaginationLimits for $type {
821 const MAX_PAGE_SIZE_COMPRESSED: i64 = $comp;
822 const MAX_PAGE_SIZE_UNCOMPRESSED: i64 = $uncomp;
823
824 fn pagination(&self) -> &$crate::dto::PaginationParams {
825 &self.pagination
826 }
827 }
828 };
829}
830
831#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, ToSchema, Eq, Hash, DeepSizeOf)]
832#[serde(deny_unknown_fields)]
833pub struct PaginationResponse {
834 pub page: i64,
835 pub page_size: i64,
836 pub total: i64,
838}
839
840impl PaginationResponse {
842 pub fn new(page: i64, page_size: i64, total: i64) -> Self {
843 Self { page, page_size, total }
844 }
845
846 pub fn total_pages(&self) -> i64 {
847 (self.total + self.page_size - 1) / self.page_size
849 }
850}
851
852#[derive(
853 Clone, Serialize, Debug, Default, Deserialize, PartialEq, ToSchema, Eq, Hash, DeepSizeOf,
854)]
855#[serde(deny_unknown_fields)]
856pub struct StateRequestBody {
857 #[serde(alias = "contractIds")]
859 #[schema(value_type=Option<Vec<String>>)]
860 pub contract_ids: Option<Vec<Bytes>>,
861 #[serde(alias = "protocolSystem", default)]
864 pub protocol_system: String,
865 #[serde(default = "VersionParam::default")]
866 pub version: VersionParam,
867 #[serde(default)]
868 pub chain: Chain,
869 #[serde(default)]
870 pub pagination: PaginationParams,
871}
872
873impl StateRequestBody {
874 pub fn new(
875 contract_ids: Option<Vec<Bytes>>,
876 protocol_system: String,
877 version: VersionParam,
878 chain: Chain,
879 pagination: PaginationParams,
880 ) -> Self {
881 Self { contract_ids, protocol_system, version, chain, pagination }
882 }
883
884 pub fn from_block(protocol_system: &str, block: BlockParam) -> Self {
885 Self {
886 contract_ids: None,
887 protocol_system: protocol_system.to_string(),
888 version: VersionParam { timestamp: None, block: Some(block.clone()) },
889 chain: block.chain.unwrap_or_default(),
890 pagination: PaginationParams::default(),
891 }
892 }
893
894 pub fn from_timestamp(protocol_system: &str, timestamp: NaiveDateTime, chain: Chain) -> Self {
895 Self {
896 contract_ids: None,
897 protocol_system: protocol_system.to_string(),
898 version: VersionParam { timestamp: Some(timestamp), block: None },
899 chain,
900 pagination: PaginationParams::default(),
901 }
902 }
903}
904
905impl_pagination_limits!(StateRequestBody, compressed = 1200, uncompressed = 100);
906
907#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, ToSchema, DeepSizeOf)]
909pub struct StateRequestResponse {
910 pub accounts: Vec<ResponseAccount>,
911 pub pagination: PaginationResponse,
912}
913
914impl StateRequestResponse {
915 pub fn new(accounts: Vec<ResponseAccount>, pagination: PaginationResponse) -> Self {
916 Self { accounts, pagination }
917 }
918}
919
920#[derive(PartialEq, Clone, Serialize, Deserialize, Default, ToSchema, DeepSizeOf)]
921#[serde(rename = "Account")]
922pub struct ResponseAccount {
926 pub chain: Chain,
927 #[schema(value_type=String, example="0xc9f2e6ea1637E499406986ac50ddC92401ce1f58")]
929 #[serde(with = "hex_bytes")]
930 pub address: Bytes,
931 #[schema(value_type=String, example="Protocol Vault")]
933 pub title: String,
934 #[schema(value_type=HashMap<String, String>, example=json!({"0x....": "0x...."}))]
936 #[serde(with = "hex_hashmap_key_value")]
937 pub slots: HashMap<Bytes, Bytes>,
938 #[schema(value_type=String, example="0x00")]
940 #[serde(with = "hex_bytes")]
941 pub native_balance: Bytes,
942 #[schema(value_type=HashMap<String, String>, example=json!({"0x....": "0x...."}))]
945 #[serde(with = "hex_hashmap_key_value")]
946 pub token_balances: HashMap<Bytes, Bytes>,
947 #[schema(value_type=String, example="0xBADBABE")]
949 #[serde(with = "hex_bytes")]
950 pub code: Bytes,
951 #[schema(value_type=String, example="0x123456789")]
953 #[serde(with = "hex_bytes")]
954 pub code_hash: Bytes,
955 #[schema(value_type=String, example="0x8f1133bfb054a23aedfe5d25b1d81b96195396d8b88bd5d4bcf865fc1ae2c3f4")]
957 #[serde(with = "hex_bytes")]
958 pub balance_modify_tx: Bytes,
959 #[schema(value_type=String, example="0x8f1133bfb054a23aedfe5d25b1d81b96195396d8b88bd5d4bcf865fc1ae2c3f4")]
961 #[serde(with = "hex_bytes")]
962 pub code_modify_tx: Bytes,
963 #[deprecated(note = "The `creation_tx` field is deprecated.")]
965 #[schema(value_type=Option<String>, example="0x8f1133bfb054a23aedfe5d25b1d81b96195396d8b88bd5d4bcf865fc1ae2c3f4")]
966 #[serde(with = "hex_bytes_option")]
967 pub creation_tx: Option<Bytes>,
968}
969
970impl ResponseAccount {
971 #[allow(clippy::too_many_arguments)]
972 pub fn new(
973 chain: Chain,
974 address: Bytes,
975 title: String,
976 slots: HashMap<Bytes, Bytes>,
977 native_balance: Bytes,
978 token_balances: HashMap<Bytes, Bytes>,
979 code: Bytes,
980 code_hash: Bytes,
981 balance_modify_tx: Bytes,
982 code_modify_tx: Bytes,
983 creation_tx: Option<Bytes>,
984 ) -> Self {
985 Self {
986 chain,
987 address,
988 title,
989 slots,
990 native_balance,
991 token_balances,
992 code,
993 code_hash,
994 balance_modify_tx,
995 code_modify_tx,
996 creation_tx,
997 }
998 }
999}
1000
1001impl fmt::Debug for ResponseAccount {
1003 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1004 f.debug_struct("ResponseAccount")
1005 .field("chain", &self.chain)
1006 .field("address", &self.address)
1007 .field("title", &self.title)
1008 .field("slots", &self.slots)
1009 .field("native_balance", &self.native_balance)
1010 .field("token_balances", &self.token_balances)
1011 .field("code", &format!("[{} bytes]", self.code.len()))
1012 .field("code_hash", &self.code_hash)
1013 .field("balance_modify_tx", &self.balance_modify_tx)
1014 .field("code_modify_tx", &self.code_modify_tx)
1015 .field("creation_tx", &self.creation_tx)
1016 .finish()
1017 }
1018}
1019
1020#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, Default)]
1021pub struct AccountBalance {
1022 #[serde(with = "hex_bytes")]
1023 pub account: Bytes,
1024 #[serde(with = "hex_bytes")]
1025 pub token: Bytes,
1026 #[serde(with = "hex_bytes")]
1027 pub balance: Bytes,
1028 #[serde(with = "hex_bytes")]
1029 pub modify_tx: Bytes,
1030}
1031
1032#[derive(Debug, Serialize, Deserialize, PartialEq, Clone, ToSchema)]
1033#[serde(deny_unknown_fields)]
1034pub struct ContractId {
1035 #[serde(with = "hex_bytes")]
1036 #[schema(value_type=String)]
1037 pub address: Bytes,
1038 pub chain: Chain,
1039}
1040
1041impl ContractId {
1043 pub fn new(chain: Chain, address: Bytes) -> Self {
1044 Self { address, chain }
1045 }
1046
1047 pub fn address(&self) -> &Bytes {
1048 &self.address
1049 }
1050}
1051
1052impl fmt::Display for ContractId {
1053 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1054 write!(f, "{:?}: 0x{}", self.chain, hex::encode(&self.address))
1055 }
1056}
1057
1058#[derive(Debug, Serialize, Deserialize, PartialEq, Clone, ToSchema, Eq, Hash)]
1065#[serde(deny_unknown_fields)]
1066pub struct VersionParam {
1067 pub timestamp: Option<NaiveDateTime>,
1068 pub block: Option<BlockParam>,
1069}
1070
1071impl DeepSizeOf for VersionParam {
1072 fn deep_size_of_children(&self, ctx: &mut Context) -> usize {
1073 if let Some(block) = &self.block {
1074 return block.deep_size_of_children(ctx);
1075 }
1076
1077 0
1078 }
1079}
1080
1081impl VersionParam {
1082 pub fn new(timestamp: Option<NaiveDateTime>, block: Option<BlockParam>) -> Self {
1083 Self { timestamp, block }
1084 }
1085}
1086
1087impl Default for VersionParam {
1088 fn default() -> Self {
1089 VersionParam { timestamp: Some(Utc::now().naive_utc()), block: None }
1090 }
1091}
1092
1093#[deprecated(note = "Use StateRequestBody instead")]
1094#[derive(Serialize, Deserialize, Default, Debug, IntoParams)]
1095pub struct StateRequestParameters {
1096 #[param(default = 0)]
1098 pub tvl_gt: Option<u64>,
1099 #[param(default = 0)]
1101 pub inertia_min_gt: Option<u64>,
1102 #[serde(default = "default_include_balances_flag")]
1104 pub include_balances: bool,
1105 #[serde(default)]
1106 pub pagination: PaginationParams,
1107}
1108
1109impl StateRequestParameters {
1110 pub fn new(include_balances: bool) -> Self {
1111 Self {
1112 tvl_gt: None,
1113 inertia_min_gt: None,
1114 include_balances,
1115 pagination: PaginationParams::default(),
1116 }
1117 }
1118
1119 pub fn to_query_string(&self) -> String {
1120 let mut parts = vec![format!("include_balances={}", self.include_balances)];
1121
1122 if let Some(tvl_gt) = self.tvl_gt {
1123 parts.push(format!("tvl_gt={tvl_gt}"));
1124 }
1125
1126 if let Some(inertia) = self.inertia_min_gt {
1127 parts.push(format!("inertia_min_gt={inertia}"));
1128 }
1129
1130 let mut res = parts.join("&");
1131 if !res.is_empty() {
1132 res = format!("?{res}");
1133 }
1134 res
1135 }
1136}
1137
1138#[derive(
1139 Serialize, Deserialize, Debug, Default, PartialEq, ToSchema, Eq, Hash, Clone, DeepSizeOf,
1140)]
1141#[serde(deny_unknown_fields)]
1142pub struct TokensRequestBody {
1143 #[serde(alias = "tokenAddresses")]
1145 #[schema(value_type=Option<Vec<String>>)]
1146 pub token_addresses: Option<Vec<Bytes>>,
1147 #[serde(default)]
1155 pub min_quality: Option<i32>,
1156 #[serde(default)]
1158 pub traded_n_days_ago: Option<u64>,
1159 #[serde(default)]
1161 pub pagination: PaginationParams,
1162 #[serde(default)]
1164 pub chain: Chain,
1165}
1166
1167impl_pagination_limits!(TokensRequestBody, compressed = 12900, uncompressed = 3000);
1168
1169#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, ToSchema, Eq, Hash, DeepSizeOf)]
1171pub struct TokensRequestResponse {
1172 pub tokens: Vec<ResponseToken>,
1173 pub pagination: PaginationResponse,
1174}
1175
1176impl TokensRequestResponse {
1177 pub fn new(tokens: Vec<ResponseToken>, pagination_request: &PaginationResponse) -> Self {
1178 Self { tokens, pagination: pagination_request.clone() }
1179 }
1180}
1181
1182#[derive(
1183 PartialEq, Debug, Clone, Serialize, Deserialize, Default, ToSchema, Eq, Hash, DeepSizeOf,
1184)]
1185#[serde(rename = "Token")]
1186pub struct ResponseToken {
1188 pub chain: Chain,
1189 #[schema(value_type=String, example="0xc9f2e6ea1637E499406986ac50ddC92401ce1f58")]
1191 #[serde(with = "hex_bytes")]
1192 pub address: Bytes,
1193 #[schema(value_type=String, example="WETH")]
1195 pub symbol: String,
1196 pub decimals: u32,
1198 pub tax: u64,
1200 pub gas: Vec<Option<u64>>,
1202 pub quality: u32,
1210}
1211
1212impl From<models::token::Token> for ResponseToken {
1213 fn from(value: models::token::Token) -> Self {
1214 Self {
1215 chain: value.chain.into(),
1216 address: value.address,
1217 symbol: value.symbol,
1218 decimals: value.decimals,
1219 tax: value.tax,
1220 gas: value.gas,
1221 quality: value.quality,
1222 }
1223 }
1224}
1225
1226#[derive(Serialize, Deserialize, Debug, Default, ToSchema, Clone, DeepSizeOf)]
1227#[serde(deny_unknown_fields)]
1228pub struct ProtocolComponentsRequestBody {
1229 pub protocol_system: String,
1232 #[schema(value_type=Option<Vec<String>>)]
1234 #[serde(alias = "componentAddresses")]
1235 pub component_ids: Option<Vec<ComponentId>>,
1236 #[serde(default)]
1239 pub tvl_gt: Option<f64>,
1240 #[serde(default)]
1241 pub chain: Chain,
1242 #[serde(default)]
1244 pub pagination: PaginationParams,
1245}
1246
1247impl_pagination_limits!(ProtocolComponentsRequestBody, compressed = 2550, uncompressed = 500);
1248
1249impl PartialEq for ProtocolComponentsRequestBody {
1251 fn eq(&self, other: &Self) -> bool {
1252 let tvl_close_enough = match (self.tvl_gt, other.tvl_gt) {
1253 (Some(a), Some(b)) => (a - b).abs() < 1e-6,
1254 (None, None) => true,
1255 _ => false,
1256 };
1257
1258 self.protocol_system == other.protocol_system &&
1259 self.component_ids == other.component_ids &&
1260 tvl_close_enough &&
1261 self.chain == other.chain &&
1262 self.pagination == other.pagination
1263 }
1264}
1265
1266impl Eq for ProtocolComponentsRequestBody {}
1268
1269impl Hash for ProtocolComponentsRequestBody {
1270 fn hash<H: Hasher>(&self, state: &mut H) {
1271 self.protocol_system.hash(state);
1272 self.component_ids.hash(state);
1273
1274 if let Some(tvl) = self.tvl_gt {
1276 tvl.to_bits().hash(state);
1278 } else {
1279 state.write_u8(0);
1281 }
1282
1283 self.chain.hash(state);
1284 self.pagination.hash(state);
1285 }
1286}
1287
1288impl ProtocolComponentsRequestBody {
1289 pub fn system_filtered(system: &str, tvl_gt: Option<f64>, chain: Chain) -> Self {
1290 Self {
1291 protocol_system: system.to_string(),
1292 component_ids: None,
1293 tvl_gt,
1294 chain,
1295 pagination: Default::default(),
1296 }
1297 }
1298
1299 pub fn id_filtered(system: &str, ids: Vec<String>, chain: Chain) -> Self {
1300 Self {
1301 protocol_system: system.to_string(),
1302 component_ids: Some(ids),
1303 tvl_gt: None,
1304 chain,
1305 pagination: Default::default(),
1306 }
1307 }
1308}
1309
1310impl ProtocolComponentsRequestBody {
1311 pub fn new(
1312 protocol_system: String,
1313 component_ids: Option<Vec<String>>,
1314 tvl_gt: Option<f64>,
1315 chain: Chain,
1316 pagination: PaginationParams,
1317 ) -> Self {
1318 Self { protocol_system, component_ids, tvl_gt, chain, pagination }
1319 }
1320}
1321
1322#[deprecated(note = "Use ProtocolComponentsRequestBody instead")]
1323#[derive(Serialize, Deserialize, Default, Debug, IntoParams)]
1324pub struct ProtocolComponentRequestParameters {
1325 #[param(default = 0)]
1327 pub tvl_gt: Option<f64>,
1328}
1329
1330impl ProtocolComponentRequestParameters {
1331 pub fn tvl_filtered(min_tvl: f64) -> Self {
1332 Self { tvl_gt: Some(min_tvl) }
1333 }
1334}
1335
1336impl ProtocolComponentRequestParameters {
1337 pub fn to_query_string(&self) -> String {
1338 if let Some(tvl_gt) = self.tvl_gt {
1339 return format!("?tvl_gt={tvl_gt}");
1340 }
1341 String::new()
1342 }
1343}
1344
1345#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, ToSchema, DeepSizeOf)]
1347pub struct ProtocolComponentRequestResponse {
1348 pub protocol_components: Vec<ProtocolComponent>,
1349 pub pagination: PaginationResponse,
1350}
1351
1352impl ProtocolComponentRequestResponse {
1353 pub fn new(
1354 protocol_components: Vec<ProtocolComponent>,
1355 pagination: PaginationResponse,
1356 ) -> Self {
1357 Self { protocol_components, pagination }
1358 }
1359}
1360
1361#[derive(Debug, Serialize, Deserialize, PartialEq, Clone, ToSchema, Eq, Hash)]
1362#[serde(deny_unknown_fields)]
1363#[deprecated]
1364pub struct ProtocolId {
1365 pub id: String,
1366 pub chain: Chain,
1367}
1368
1369impl From<ProtocolId> for String {
1370 fn from(protocol_id: ProtocolId) -> Self {
1371 protocol_id.id
1372 }
1373}
1374
1375impl AsRef<str> for ProtocolId {
1376 fn as_ref(&self) -> &str {
1377 &self.id
1378 }
1379}
1380
1381#[derive(Debug, Clone, PartialEq, Default, Deserialize, Serialize, ToSchema, DeepSizeOf)]
1383pub struct ResponseProtocolState {
1384 pub component_id: String,
1386 #[schema(value_type=HashMap<String, String>)]
1389 #[serde(with = "hex_hashmap_value")]
1390 pub attributes: HashMap<String, Bytes>,
1391 #[schema(value_type=HashMap<String, String>)]
1393 #[serde(with = "hex_hashmap_key_value")]
1394 pub balances: HashMap<Bytes, Bytes>,
1395}
1396
1397impl From<models::protocol::ProtocolComponentState> for ResponseProtocolState {
1398 fn from(value: models::protocol::ProtocolComponentState) -> Self {
1399 Self {
1400 component_id: value.component_id,
1401 attributes: value.attributes,
1402 balances: value.balances,
1403 }
1404 }
1405}
1406
1407fn default_include_balances_flag() -> bool {
1408 true
1409}
1410
1411#[derive(Clone, Debug, Serialize, PartialEq, ToSchema, Default, Eq, Hash, DeepSizeOf)]
1413#[serde(deny_unknown_fields)]
1414pub struct ProtocolStateRequestBody {
1415 #[serde(alias = "protocolIds")]
1417 pub protocol_ids: Option<Vec<String>>,
1418 #[serde(alias = "protocolSystem")]
1421 pub protocol_system: String,
1422 #[serde(default)]
1423 pub chain: Chain,
1424 #[serde(default = "default_include_balances_flag")]
1426 pub include_balances: bool,
1427 #[serde(default = "VersionParam::default")]
1428 pub version: VersionParam,
1429 #[serde(default)]
1430 pub pagination: PaginationParams,
1431}
1432
1433impl_pagination_limits!(ProtocolStateRequestBody, compressed = 360, uncompressed = 100);
1434
1435impl ProtocolStateRequestBody {
1436 pub fn id_filtered<I, T>(ids: I) -> Self
1437 where
1438 I: IntoIterator<Item = T>,
1439 T: Into<String>,
1440 {
1441 Self {
1442 protocol_ids: Some(
1443 ids.into_iter()
1444 .map(Into::into)
1445 .collect(),
1446 ),
1447 ..Default::default()
1448 }
1449 }
1450}
1451
1452impl<'de> Deserialize<'de> for ProtocolStateRequestBody {
1456 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
1457 where
1458 D: Deserializer<'de>,
1459 {
1460 #[derive(Deserialize)]
1461 #[serde(untagged)]
1462 enum ProtocolIdOrString {
1463 Old(Vec<ProtocolId>),
1464 New(Vec<String>),
1465 }
1466
1467 struct ProtocolStateRequestBodyVisitor;
1468
1469 impl<'de> de::Visitor<'de> for ProtocolStateRequestBodyVisitor {
1470 type Value = ProtocolStateRequestBody;
1471
1472 fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
1473 formatter.write_str("struct ProtocolStateRequestBody")
1474 }
1475
1476 fn visit_map<V>(self, mut map: V) -> Result<ProtocolStateRequestBody, V::Error>
1477 where
1478 V: de::MapAccess<'de>,
1479 {
1480 let mut protocol_ids = None;
1481 let mut protocol_system = None;
1482 let mut version = None;
1483 let mut chain = None;
1484 let mut include_balances = None;
1485 let mut pagination = None;
1486
1487 while let Some(key) = map.next_key::<String>()? {
1488 match key.as_str() {
1489 "protocol_ids" | "protocolIds" => {
1490 let value: ProtocolIdOrString = map.next_value()?;
1491 protocol_ids = match value {
1492 ProtocolIdOrString::Old(ids) => {
1493 Some(ids.into_iter().map(|p| p.id).collect())
1494 }
1495 ProtocolIdOrString::New(ids_str) => Some(ids_str),
1496 };
1497 }
1498 "protocol_system" | "protocolSystem" => {
1499 protocol_system = Some(map.next_value()?);
1500 }
1501 "version" => {
1502 version = Some(map.next_value()?);
1503 }
1504 "chain" => {
1505 chain = Some(map.next_value()?);
1506 }
1507 "include_balances" => {
1508 include_balances = Some(map.next_value()?);
1509 }
1510 "pagination" => {
1511 pagination = Some(map.next_value()?);
1512 }
1513 _ => {
1514 return Err(de::Error::unknown_field(
1515 &key,
1516 &[
1517 "contract_ids",
1518 "protocol_system",
1519 "version",
1520 "chain",
1521 "include_balances",
1522 "pagination",
1523 ],
1524 ))
1525 }
1526 }
1527 }
1528
1529 Ok(ProtocolStateRequestBody {
1530 protocol_ids,
1531 protocol_system: protocol_system.unwrap_or_default(),
1532 version: version.unwrap_or_else(VersionParam::default),
1533 chain: chain.unwrap_or_else(Chain::default),
1534 include_balances: include_balances.unwrap_or(true),
1535 pagination: pagination.unwrap_or_else(PaginationParams::default),
1536 })
1537 }
1538 }
1539
1540 deserializer.deserialize_struct(
1541 "ProtocolStateRequestBody",
1542 &[
1543 "contract_ids",
1544 "protocol_system",
1545 "version",
1546 "chain",
1547 "include_balances",
1548 "pagination",
1549 ],
1550 ProtocolStateRequestBodyVisitor,
1551 )
1552 }
1553}
1554
1555#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, ToSchema, DeepSizeOf)]
1556pub struct ProtocolStateRequestResponse {
1557 pub states: Vec<ResponseProtocolState>,
1558 pub pagination: PaginationResponse,
1559}
1560
1561impl ProtocolStateRequestResponse {
1562 pub fn new(states: Vec<ResponseProtocolState>, pagination: PaginationResponse) -> Self {
1563 Self { states, pagination }
1564 }
1565}
1566
1567#[derive(Serialize, Clone, PartialEq, Hash, Eq)]
1568pub struct ProtocolComponentId {
1569 pub chain: Chain,
1570 pub system: String,
1571 pub id: String,
1572}
1573
1574#[derive(Debug, Serialize, ToSchema)]
1575#[serde(tag = "status", content = "message")]
1576#[schema(example = json!({"status": "NotReady", "message": "No db connection"}))]
1577pub enum Health {
1578 Ready,
1579 Starting(String),
1580 NotReady(String),
1581}
1582
1583#[derive(Serialize, Deserialize, Debug, Default, PartialEq, ToSchema, Eq, Hash, Clone)]
1584#[serde(deny_unknown_fields)]
1585pub struct ProtocolSystemsRequestBody {
1586 #[serde(default)]
1587 pub chain: Chain,
1588 #[serde(default)]
1589 pub pagination: PaginationParams,
1590}
1591
1592impl_pagination_limits!(ProtocolSystemsRequestBody, compressed = 100, uncompressed = 100);
1593
1594#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, ToSchema, Eq, Hash)]
1595pub struct ProtocolSystemsRequestResponse {
1596 pub protocol_systems: Vec<String>,
1598 pub pagination: PaginationResponse,
1599}
1600
1601impl ProtocolSystemsRequestResponse {
1602 pub fn new(protocol_systems: Vec<String>, pagination: PaginationResponse) -> Self {
1603 Self { protocol_systems, pagination }
1604 }
1605}
1606
1607#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, Default)]
1608pub struct DCIUpdate {
1609 pub new_entrypoints: HashMap<ComponentId, HashSet<EntryPoint>>,
1611 pub new_entrypoint_params: HashMap<String, HashSet<(TracingParams, Option<String>)>>,
1614 pub trace_results: HashMap<String, TracingResult>,
1616}
1617
1618impl From<models::blockchain::DCIUpdate> for DCIUpdate {
1619 fn from(value: models::blockchain::DCIUpdate) -> Self {
1620 Self {
1621 new_entrypoints: value
1622 .new_entrypoints
1623 .into_iter()
1624 .map(|(k, v)| {
1625 (
1626 k,
1627 v.into_iter()
1628 .map(|v| v.into())
1629 .collect(),
1630 )
1631 })
1632 .collect(),
1633 new_entrypoint_params: value
1634 .new_entrypoint_params
1635 .into_iter()
1636 .map(|(k, v)| {
1637 (
1638 k,
1639 v.into_iter()
1640 .map(|(params, i)| (params.into(), i))
1641 .collect(),
1642 )
1643 })
1644 .collect(),
1645 trace_results: value
1646 .trace_results
1647 .into_iter()
1648 .map(|(k, v)| (k, v.into()))
1649 .collect(),
1650 }
1651 }
1652}
1653
1654#[derive(Serialize, Deserialize, Debug, Default, PartialEq, ToSchema, Eq, Hash, Clone)]
1655#[serde(deny_unknown_fields)]
1656pub struct ComponentTvlRequestBody {
1657 #[serde(default)]
1658 pub chain: Chain,
1659 #[serde(alias = "protocolSystem")]
1662 pub protocol_system: Option<String>,
1663 #[serde(default)]
1664 pub component_ids: Option<Vec<String>>,
1665 #[serde(default)]
1666 pub pagination: PaginationParams,
1667}
1668
1669impl_pagination_limits!(ComponentTvlRequestBody, compressed = 100, uncompressed = 100);
1670
1671impl ComponentTvlRequestBody {
1672 pub fn system_filtered(system: &str, chain: Chain) -> Self {
1673 Self {
1674 chain,
1675 protocol_system: Some(system.to_string()),
1676 component_ids: None,
1677 pagination: Default::default(),
1678 }
1679 }
1680
1681 pub fn id_filtered(ids: Vec<String>, chain: Chain) -> Self {
1682 Self {
1683 chain,
1684 protocol_system: None,
1685 component_ids: Some(ids),
1686 pagination: Default::default(),
1687 }
1688 }
1689}
1690#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, ToSchema)]
1692pub struct ComponentTvlRequestResponse {
1693 pub tvl: HashMap<String, f64>,
1694 pub pagination: PaginationResponse,
1695}
1696
1697impl ComponentTvlRequestResponse {
1698 pub fn new(tvl: HashMap<String, f64>, pagination: PaginationResponse) -> Self {
1699 Self { tvl, pagination }
1700 }
1701}
1702
1703#[derive(
1704 Serialize, Deserialize, Debug, Default, PartialEq, ToSchema, Eq, Hash, Clone, DeepSizeOf,
1705)]
1706pub struct TracedEntryPointRequestBody {
1707 #[serde(default)]
1708 pub chain: Chain,
1709 pub protocol_system: String,
1712 #[schema(value_type = Option<Vec<String>>)]
1714 pub component_ids: Option<Vec<ComponentId>>,
1715 #[serde(default)]
1717 pub pagination: PaginationParams,
1718}
1719
1720impl_pagination_limits!(TracedEntryPointRequestBody, compressed = 100, uncompressed = 100);
1721
1722#[derive(Debug, Serialize, Deserialize, PartialEq, Clone, ToSchema, Eq, Hash, DeepSizeOf)]
1723pub struct EntryPoint {
1724 #[schema(example = "0xEdf63cce4bA70cbE74064b7687882E71ebB0e988:getRate()")]
1725 pub external_id: String,
1727 #[schema(value_type=String, example="0x8f4E8439b970363648421C692dd897Fb9c0Bd1D9")]
1728 #[serde(with = "hex_bytes")]
1729 pub target: Bytes,
1731 #[schema(example = "getRate()")]
1732 pub signature: String,
1734}
1735
1736#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, ToSchema, Eq, Hash, DeepSizeOf)]
1737pub enum StorageOverride {
1738 #[schema(value_type=HashMap<String, String>)]
1742 Diff(BTreeMap<StoreKey, StoreVal>),
1743
1744 #[schema(value_type=HashMap<String, String>)]
1748 Replace(BTreeMap<StoreKey, StoreVal>),
1749}
1750
1751impl From<models::blockchain::StorageOverride> for StorageOverride {
1752 fn from(value: models::blockchain::StorageOverride) -> Self {
1753 match value {
1754 models::blockchain::StorageOverride::Diff(diff) => StorageOverride::Diff(diff),
1755 models::blockchain::StorageOverride::Replace(replace) => {
1756 StorageOverride::Replace(replace)
1757 }
1758 }
1759 }
1760}
1761
1762#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, ToSchema, Eq, Hash, DeepSizeOf)]
1767pub struct AccountOverrides {
1768 pub slots: Option<StorageOverride>,
1770 #[schema(value_type=Option<String>)]
1771 pub native_balance: Option<Balance>,
1773 #[schema(value_type=Option<String>)]
1774 pub code: Option<Code>,
1776}
1777
1778impl From<models::blockchain::AccountOverrides> for AccountOverrides {
1779 fn from(value: models::blockchain::AccountOverrides) -> Self {
1780 AccountOverrides {
1781 slots: value.slots.map(|s| s.into()),
1782 native_balance: value.native_balance,
1783 code: value.code,
1784 }
1785 }
1786}
1787
1788#[derive(Debug, Serialize, Deserialize, PartialEq, Clone, ToSchema, Eq, Hash, DeepSizeOf)]
1789pub struct RPCTracerParams {
1790 #[schema(value_type=Option<String>)]
1793 #[serde(with = "hex_bytes_option", default)]
1794 pub caller: Option<Bytes>,
1795 #[schema(value_type=String, example="0x679aefce")]
1797 #[serde(with = "hex_bytes")]
1798 pub calldata: Bytes,
1799 pub state_overrides: Option<BTreeMap<Address, AccountOverrides>>,
1801 #[schema(value_type=Option<Vec<String>>)]
1804 #[serde(default)]
1805 pub prune_addresses: Option<Vec<Address>>,
1806}
1807
1808impl From<models::blockchain::RPCTracerParams> for RPCTracerParams {
1809 fn from(value: models::blockchain::RPCTracerParams) -> Self {
1810 RPCTracerParams {
1811 caller: value.caller,
1812 calldata: value.calldata,
1813 state_overrides: value.state_overrides.map(|overrides| {
1814 overrides
1815 .into_iter()
1816 .map(|(address, account_overrides)| (address, account_overrides.into()))
1817 .collect()
1818 }),
1819 prune_addresses: value.prune_addresses,
1820 }
1821 }
1822}
1823
1824#[derive(Deserialize, Serialize, Debug, PartialEq, Eq, Clone, Hash, DeepSizeOf, ToSchema)]
1825#[serde(tag = "method", rename_all = "lowercase")]
1826pub enum TracingParams {
1827 RPCTracer(RPCTracerParams),
1829}
1830
1831impl From<models::blockchain::TracingParams> for TracingParams {
1832 fn from(value: models::blockchain::TracingParams) -> Self {
1833 match value {
1834 models::blockchain::TracingParams::RPCTracer(params) => {
1835 TracingParams::RPCTracer(params.into())
1836 }
1837 }
1838 }
1839}
1840
1841impl From<models::blockchain::EntryPoint> for EntryPoint {
1842 fn from(value: models::blockchain::EntryPoint) -> Self {
1843 Self { external_id: value.external_id, target: value.target, signature: value.signature }
1844 }
1845}
1846
1847#[derive(Serialize, Deserialize, Debug, PartialEq, ToSchema, Eq, Clone, DeepSizeOf)]
1848pub struct EntryPointWithTracingParams {
1849 pub entry_point: EntryPoint,
1851 pub params: TracingParams,
1853}
1854
1855impl From<models::blockchain::EntryPointWithTracingParams> for EntryPointWithTracingParams {
1856 fn from(value: models::blockchain::EntryPointWithTracingParams) -> Self {
1857 Self { entry_point: value.entry_point.into(), params: value.params.into() }
1858 }
1859}
1860
1861#[derive(
1862 Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash, Serialize, Deserialize, DeepSizeOf,
1863)]
1864pub struct AddressStorageLocation {
1865 pub key: StoreKey,
1866 pub offset: u8,
1867}
1868
1869impl AddressStorageLocation {
1870 pub fn new(key: StoreKey, offset: u8) -> Self {
1871 Self { key, offset }
1872 }
1873}
1874
1875impl From<models::blockchain::AddressStorageLocation> for AddressStorageLocation {
1876 fn from(value: models::blockchain::AddressStorageLocation) -> Self {
1877 Self { key: value.key, offset: value.offset }
1878 }
1879}
1880
1881fn deserialize_retriggers_from_value(
1882 value: &serde_json::Value,
1883) -> Result<HashSet<(StoreKey, AddressStorageLocation)>, String> {
1884 use serde::Deserialize;
1885 use serde_json::Value;
1886
1887 let mut result = HashSet::new();
1888
1889 if let Value::Array(items) = value {
1890 for item in items {
1891 if let Value::Array(pair) = item {
1892 if pair.len() == 2 {
1893 let key = StoreKey::deserialize(&pair[0])
1894 .map_err(|e| format!("Failed to deserialize key: {}", e))?;
1895
1896 let addr_storage = match &pair[1] {
1898 Value::String(_) => {
1899 let storage_key = StoreKey::deserialize(&pair[1]).map_err(|e| {
1901 format!("Failed to deserialize old format storage key: {}", e)
1902 })?;
1903 AddressStorageLocation::new(storage_key, 12)
1904 }
1905 Value::Object(_) => {
1906 AddressStorageLocation::deserialize(&pair[1]).map_err(|e| {
1908 format!("Failed to deserialize AddressStorageLocation: {}", e)
1909 })?
1910 }
1911 _ => return Err("Invalid retrigger format".to_string()),
1912 };
1913
1914 result.insert((key, addr_storage));
1915 }
1916 }
1917 }
1918 }
1919
1920 Ok(result)
1921}
1922
1923#[derive(Serialize, Debug, Default, PartialEq, ToSchema, Eq, Clone, DeepSizeOf)]
1924pub struct TracingResult {
1925 #[schema(value_type=HashSet<(String, String)>)]
1926 pub retriggers: HashSet<(StoreKey, AddressStorageLocation)>,
1927 #[schema(value_type=HashMap<String,HashSet<String>>)]
1928 pub accessed_slots: HashMap<Address, HashSet<StoreKey>>,
1929}
1930
1931impl<'de> Deserialize<'de> for TracingResult {
1934 fn deserialize<D>(deserializer: D) -> Result<TracingResult, D::Error>
1935 where
1936 D: Deserializer<'de>,
1937 {
1938 use serde::de::Error;
1939 use serde_json::Value;
1940
1941 let value = Value::deserialize(deserializer)?;
1942 let mut result = TracingResult::default();
1943
1944 if let Value::Object(map) = value {
1945 if let Some(retriggers_value) = map.get("retriggers") {
1947 result.retriggers =
1948 deserialize_retriggers_from_value(retriggers_value).map_err(|e| {
1949 D::Error::custom(format!("Failed to deserialize retriggers: {}", e))
1950 })?;
1951 }
1952
1953 if let Some(accessed_slots_value) = map.get("accessed_slots") {
1955 result.accessed_slots = serde_json::from_value(accessed_slots_value.clone())
1956 .map_err(|e| {
1957 D::Error::custom(format!("Failed to deserialize accessed_slots: {}", e))
1958 })?;
1959 }
1960 }
1961
1962 Ok(result)
1963 }
1964}
1965
1966impl From<models::blockchain::TracingResult> for TracingResult {
1967 fn from(value: models::blockchain::TracingResult) -> Self {
1968 TracingResult {
1969 retriggers: value
1970 .retriggers
1971 .into_iter()
1972 .map(|(k, v)| (k, v.into()))
1973 .collect(),
1974 accessed_slots: value.accessed_slots,
1975 }
1976 }
1977}
1978
1979#[derive(Serialize, PartialEq, ToSchema, Eq, Clone, Debug, Deserialize, DeepSizeOf)]
1980pub struct TracedEntryPointRequestResponse {
1981 #[schema(value_type = HashMap<String, Vec<(EntryPointWithTracingParams, TracingResult)>>)]
1984 pub traced_entry_points:
1985 HashMap<ComponentId, Vec<(EntryPointWithTracingParams, TracingResult)>>,
1986 pub pagination: PaginationResponse,
1987}
1988impl From<TracedEntryPointRequestResponse> for DCIUpdate {
1989 fn from(response: TracedEntryPointRequestResponse) -> Self {
1990 let mut new_entrypoints = HashMap::new();
1991 let mut new_entrypoint_params = HashMap::new();
1992 let mut trace_results = HashMap::new();
1993
1994 for (component, traces) in response.traced_entry_points {
1995 let mut entrypoints = HashSet::new();
1996
1997 for (entrypoint, trace) in traces {
1998 let entrypoint_id = entrypoint
1999 .entry_point
2000 .external_id
2001 .clone();
2002
2003 entrypoints.insert(entrypoint.entry_point.clone());
2005
2006 new_entrypoint_params
2008 .entry(entrypoint_id.clone())
2009 .or_insert_with(HashSet::new)
2010 .insert((entrypoint.params, Some(component.clone())));
2011
2012 trace_results
2014 .entry(entrypoint_id)
2015 .and_modify(|existing_trace: &mut TracingResult| {
2016 existing_trace
2018 .retriggers
2019 .extend(trace.retriggers.clone());
2020 for (address, slots) in trace.accessed_slots.clone() {
2021 existing_trace
2022 .accessed_slots
2023 .entry(address)
2024 .or_default()
2025 .extend(slots);
2026 }
2027 })
2028 .or_insert(trace);
2029 }
2030
2031 if !entrypoints.is_empty() {
2032 new_entrypoints.insert(component, entrypoints);
2033 }
2034 }
2035
2036 DCIUpdate { new_entrypoints, new_entrypoint_params, trace_results }
2037 }
2038}
2039
2040#[derive(Serialize, Deserialize, Debug, Default, PartialEq, ToSchema, Eq, Clone)]
2041pub struct AddEntryPointRequestBody {
2042 #[serde(default)]
2043 pub chain: Chain,
2044 #[schema(value_type=String)]
2045 #[serde(default)]
2046 pub block_hash: Bytes,
2047 #[schema(value_type = Vec<(String, Vec<EntryPointWithTracingParams>)>)]
2049 pub entry_points_with_tracing_data: Vec<(ComponentId, Vec<EntryPointWithTracingParams>)>,
2050}
2051
2052#[derive(Serialize, PartialEq, ToSchema, Eq, Clone, Debug, Deserialize)]
2053pub struct AddEntryPointRequestResponse {
2054 #[schema(value_type = HashMap<String, Vec<(EntryPointWithTracingParams, TracingResult)>>)]
2057 pub traced_entry_points:
2058 HashMap<ComponentId, Vec<(EntryPointWithTracingParams, TracingResult)>>,
2059}
2060
2061#[cfg(test)]
2062mod test {
2063 use std::str::FromStr;
2064
2065 use maplit::hashmap;
2066 use rstest::rstest;
2067
2068 use super::*;
2069
2070 #[test]
2071 fn test_compression_backward_compatibility() {
2072 let json_without_compression = r#"{
2074 "method": "subscribe",
2075 "extractor_id": {
2076 "chain": "ethereum",
2077 "name": "test"
2078 },
2079 "include_state": true
2080 }"#;
2081
2082 let command: Command = serde_json::from_str(json_without_compression)
2083 .expect("Failed to deserialize Subscribe without compression field");
2084
2085 if let Command::Subscribe { compression, .. } = command {
2086 assert_eq!(
2087 compression, false,
2088 "compression should default to false when not specified"
2089 );
2090 } else {
2091 panic!("Expected Subscribe command");
2092 }
2093
2094 let json_with_compression = r#"{
2096 "method": "subscribe",
2097 "extractor_id": {
2098 "chain": "ethereum",
2099 "name": "test"
2100 },
2101 "include_state": true,
2102 "compression": true
2103 }"#;
2104
2105 let command_with_compression: Command = serde_json::from_str(json_with_compression)
2106 .expect("Failed to deserialize Subscribe with compression field");
2107
2108 if let Command::Subscribe { compression, .. } = command_with_compression {
2109 assert_eq!(compression, true, "compression should be true as specified in the JSON");
2110 } else {
2111 panic!("Expected Subscribe command");
2112 }
2113 }
2114
2115 #[test]
2116 fn test_tracing_result_backward_compatibility() {
2117 use serde_json::json;
2118
2119 let old_format_json = json!({
2121 "retriggers": [
2122 ["0x01", "0x02"],
2123 ["0x03", "0x04"]
2124 ],
2125 "accessed_slots": {
2126 "0x05": ["0x06", "0x07"]
2127 }
2128 });
2129
2130 let result: TracingResult = serde_json::from_value(old_format_json).unwrap();
2131
2132 assert_eq!(result.retriggers.len(), 2);
2134 let retriggers_vec: Vec<_> = result.retriggers.iter().collect();
2135 assert!(retriggers_vec.iter().any(|(k, v)| {
2136 k == &Bytes::from("0x01") && v.key == Bytes::from("0x02") && v.offset == 12
2137 }));
2138 assert!(retriggers_vec.iter().any(|(k, v)| {
2139 k == &Bytes::from("0x03") && v.key == Bytes::from("0x04") && v.offset == 12
2140 }));
2141
2142 let new_format_json = json!({
2144 "retriggers": [
2145 ["0x01", {"key": "0x02", "offset": 12}],
2146 ["0x03", {"key": "0x04", "offset": 5}]
2147 ],
2148 "accessed_slots": {
2149 "0x05": ["0x06", "0x07"]
2150 }
2151 });
2152
2153 let result2: TracingResult = serde_json::from_value(new_format_json).unwrap();
2154
2155 assert_eq!(result2.retriggers.len(), 2);
2157 let retriggers_vec2: Vec<_> = result2.retriggers.iter().collect();
2158 assert!(retriggers_vec2.iter().any(|(k, v)| {
2159 k == &Bytes::from("0x01") && v.key == Bytes::from("0x02") && v.offset == 12
2160 }));
2161 assert!(retriggers_vec2.iter().any(|(k, v)| {
2162 k == &Bytes::from("0x03") && v.key == Bytes::from("0x04") && v.offset == 5
2163 }));
2164 }
2165
2166 #[test]
2167 fn test_protocol_components_equality() {
2168 let body1 = ProtocolComponentsRequestBody {
2169 protocol_system: "protocol1".to_string(),
2170 component_ids: Some(vec!["component1".to_string(), "component2".to_string()]),
2171 tvl_gt: Some(1000.0),
2172 chain: Chain::Ethereum,
2173 pagination: PaginationParams::default(),
2174 };
2175
2176 let body2 = ProtocolComponentsRequestBody {
2177 protocol_system: "protocol1".to_string(),
2178 component_ids: Some(vec!["component1".to_string(), "component2".to_string()]),
2179 tvl_gt: Some(1000.0 + 1e-7), chain: Chain::Ethereum,
2181 pagination: PaginationParams::default(),
2182 };
2183
2184 assert_eq!(body1, body2);
2186 }
2187
2188 #[test]
2189 fn test_protocol_components_inequality() {
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-5), chain: Chain::Ethereum,
2203 pagination: PaginationParams::default(),
2204 };
2205
2206 assert_ne!(body1, body2);
2208 }
2209
2210 #[test]
2211 fn test_parse_state_request() {
2212 let json_str = r#"
2213 {
2214 "contractIds": [
2215 "0xb4eccE46b8D4e4abFd03C9B806276A6735C9c092"
2216 ],
2217 "protocol_system": "uniswap_v2",
2218 "version": {
2219 "timestamp": "2069-01-01T04:20:00",
2220 "block": {
2221 "hash": "0x24101f9cb26cd09425b52da10e8c2f56ede94089a8bbe0f31f1cda5f4daa52c4",
2222 "number": 213,
2223 "chain": "ethereum"
2224 }
2225 }
2226 }
2227 "#;
2228
2229 let result: StateRequestBody = serde_json::from_str(json_str).unwrap();
2230
2231 let contract0 = "b4eccE46b8D4e4abFd03C9B806276A6735C9c092"
2232 .parse()
2233 .unwrap();
2234 let block_hash = "24101f9cb26cd09425b52da10e8c2f56ede94089a8bbe0f31f1cda5f4daa52c4"
2235 .parse()
2236 .unwrap();
2237 let block_number = 213;
2238
2239 let expected_timestamp =
2240 NaiveDateTime::parse_from_str("2069-01-01T04:20:00", "%Y-%m-%dT%H:%M:%S").unwrap();
2241
2242 let expected = StateRequestBody {
2243 contract_ids: Some(vec![contract0]),
2244 protocol_system: "uniswap_v2".to_string(),
2245 version: VersionParam {
2246 timestamp: Some(expected_timestamp),
2247 block: Some(BlockParam {
2248 hash: Some(block_hash),
2249 chain: Some(Chain::Ethereum),
2250 number: Some(block_number),
2251 }),
2252 },
2253 chain: Chain::Ethereum,
2254 pagination: PaginationParams::default(),
2255 };
2256
2257 assert_eq!(result, expected);
2258 }
2259
2260 #[test]
2261 fn test_parse_state_request_dual_interface() {
2262 let json_common = r#"
2263 {
2264 "__CONTRACT_IDS__": [
2265 "0xb4eccE46b8D4e4abFd03C9B806276A6735C9c092"
2266 ],
2267 "version": {
2268 "timestamp": "2069-01-01T04:20:00",
2269 "block": {
2270 "hash": "0x24101f9cb26cd09425b52da10e8c2f56ede94089a8bbe0f31f1cda5f4daa52c4",
2271 "number": 213,
2272 "chain": "ethereum"
2273 }
2274 }
2275 }
2276 "#;
2277
2278 let json_str_snake = json_common.replace("\"__CONTRACT_IDS__\"", "\"contract_ids\"");
2279 let json_str_camel = json_common.replace("\"__CONTRACT_IDS__\"", "\"contractIds\"");
2280
2281 let snake: StateRequestBody = serde_json::from_str(&json_str_snake).unwrap();
2282 let camel: StateRequestBody = serde_json::from_str(&json_str_camel).unwrap();
2283
2284 assert_eq!(snake, camel);
2285 }
2286
2287 #[test]
2288 fn test_parse_state_request_unknown_field() {
2289 let body = r#"
2290 {
2291 "contract_ids_with_typo_error": [
2292 {
2293 "address": "0xb4eccE46b8D4e4abFd03C9B806276A6735C9c092",
2294 "chain": "ethereum"
2295 }
2296 ],
2297 "version": {
2298 "timestamp": "2069-01-01T04:20:00",
2299 "block": {
2300 "hash": "0x24101f9cb26cd09425b52da10e8c2f56ede94089a8bbe0f31f1cda5f4daa52c4",
2301 "parentHash": "0x8d75152454e60413efe758cc424bfd339897062d7e658f302765eb7b50971815",
2302 "number": 213,
2303 "chain": "ethereum"
2304 }
2305 }
2306 }
2307 "#;
2308
2309 let decoded = serde_json::from_str::<StateRequestBody>(body);
2310
2311 assert!(decoded.is_err(), "Expected an error due to unknown field");
2312
2313 if let Err(e) = decoded {
2314 assert!(
2315 e.to_string()
2316 .contains("unknown field `contract_ids_with_typo_error`"),
2317 "Error message does not contain expected unknown field information"
2318 );
2319 }
2320 }
2321
2322 #[test]
2323 fn test_parse_state_request_no_contract_specified() {
2324 let json_str = r#"
2325 {
2326 "protocol_system": "uniswap_v2",
2327 "version": {
2328 "timestamp": "2069-01-01T04:20:00",
2329 "block": {
2330 "hash": "0x24101f9cb26cd09425b52da10e8c2f56ede94089a8bbe0f31f1cda5f4daa52c4",
2331 "number": 213,
2332 "chain": "ethereum"
2333 }
2334 }
2335 }
2336 "#;
2337
2338 let result: StateRequestBody = serde_json::from_str(json_str).unwrap();
2339
2340 let block_hash = "24101f9cb26cd09425b52da10e8c2f56ede94089a8bbe0f31f1cda5f4daa52c4".into();
2341 let block_number = 213;
2342 let expected_timestamp =
2343 NaiveDateTime::parse_from_str("2069-01-01T04:20:00", "%Y-%m-%dT%H:%M:%S").unwrap();
2344
2345 let expected = StateRequestBody {
2346 contract_ids: None,
2347 protocol_system: "uniswap_v2".to_string(),
2348 version: VersionParam {
2349 timestamp: Some(expected_timestamp),
2350 block: Some(BlockParam {
2351 hash: Some(block_hash),
2352 chain: Some(Chain::Ethereum),
2353 number: Some(block_number),
2354 }),
2355 },
2356 chain: Chain::Ethereum,
2357 pagination: PaginationParams { page: 0, page_size: 100 },
2358 };
2359
2360 assert_eq!(result, expected);
2361 }
2362
2363 #[rstest]
2364 #[case::deprecated_ids(
2365 r#"
2366 {
2367 "protocol_ids": [
2368 {
2369 "id": "0xb4eccE46b8D4e4abFd03C9B806276A6735C9c092",
2370 "chain": "ethereum"
2371 }
2372 ],
2373 "protocol_system": "uniswap_v2",
2374 "include_balances": false,
2375 "version": {
2376 "timestamp": "2069-01-01T04:20:00",
2377 "block": {
2378 "hash": "0x24101f9cb26cd09425b52da10e8c2f56ede94089a8bbe0f31f1cda5f4daa52c4",
2379 "number": 213,
2380 "chain": "ethereum"
2381 }
2382 }
2383 }
2384 "#
2385 )]
2386 #[case(
2387 r#"
2388 {
2389 "protocolIds": [
2390 "0xb4eccE46b8D4e4abFd03C9B806276A6735C9c092"
2391 ],
2392 "protocol_system": "uniswap_v2",
2393 "include_balances": false,
2394 "version": {
2395 "timestamp": "2069-01-01T04:20:00",
2396 "block": {
2397 "hash": "0x24101f9cb26cd09425b52da10e8c2f56ede94089a8bbe0f31f1cda5f4daa52c4",
2398 "number": 213,
2399 "chain": "ethereum"
2400 }
2401 }
2402 }
2403 "#
2404 )]
2405 fn test_parse_protocol_state_request(#[case] json_str: &str) {
2406 let result: ProtocolStateRequestBody = serde_json::from_str(json_str).unwrap();
2407
2408 let block_hash = "24101f9cb26cd09425b52da10e8c2f56ede94089a8bbe0f31f1cda5f4daa52c4"
2409 .parse()
2410 .unwrap();
2411 let block_number = 213;
2412
2413 let expected_timestamp =
2414 NaiveDateTime::parse_from_str("2069-01-01T04:20:00", "%Y-%m-%dT%H:%M:%S").unwrap();
2415
2416 let expected = ProtocolStateRequestBody {
2417 protocol_ids: Some(vec!["0xb4eccE46b8D4e4abFd03C9B806276A6735C9c092".to_string()]),
2418 protocol_system: "uniswap_v2".to_string(),
2419 version: VersionParam {
2420 timestamp: Some(expected_timestamp),
2421 block: Some(BlockParam {
2422 hash: Some(block_hash),
2423 chain: Some(Chain::Ethereum),
2424 number: Some(block_number),
2425 }),
2426 },
2427 chain: Chain::Ethereum,
2428 include_balances: false,
2429 pagination: PaginationParams::default(),
2430 };
2431
2432 assert_eq!(result, expected);
2433 }
2434
2435 #[rstest]
2436 #[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()])]
2437 #[case::with_strings(vec!["id1".to_string(), "id2".to_string()], vec!["id1".to_string(), "id2".to_string()])]
2438 fn test_id_filtered<T>(#[case] input_ids: Vec<T>, #[case] expected_ids: Vec<String>)
2439 where
2440 T: Into<String> + Clone,
2441 {
2442 let request_body = ProtocolStateRequestBody::id_filtered(input_ids);
2443
2444 assert_eq!(request_body.protocol_ids, Some(expected_ids));
2445 }
2446
2447 fn create_models_block_changes() -> crate::models::blockchain::BlockAggregatedChanges {
2448 let base_ts = 1694534400; crate::models::blockchain::BlockAggregatedChanges {
2451 extractor: "native_name".to_string(),
2452 block: models::blockchain::Block::new(
2453 3,
2454 models::Chain::Ethereum,
2455 Bytes::from_str("0x0000000000000000000000000000000000000000000000000000000000000003").unwrap(),
2456 Bytes::from_str("0x0000000000000000000000000000000000000000000000000000000000000002").unwrap(),
2457 chrono::DateTime::from_timestamp(base_ts + 3000, 0).unwrap().naive_utc(),
2458 ),
2459 db_committed_block_height: Some(1),
2460 finalized_block_height: 1,
2461 revert: true,
2462 state_deltas: HashMap::from([
2463 ("pc_1".to_string(), models::protocol::ProtocolComponentStateDelta {
2464 component_id: "pc_1".to_string(),
2465 updated_attributes: HashMap::from([
2466 ("attr_2".to_string(), Bytes::from("0x0000000000000002")),
2467 ("attr_1".to_string(), Bytes::from("0x00000000000003e8")),
2468 ]),
2469 deleted_attributes: HashSet::new(),
2470 }),
2471 ]),
2472 new_protocol_components: HashMap::from([
2473 ("pc_2".to_string(), crate::models::protocol::ProtocolComponent {
2474 id: "pc_2".to_string(),
2475 protocol_system: "native_protocol_system".to_string(),
2476 protocol_type_name: "pt_1".to_string(),
2477 chain: models::Chain::Ethereum,
2478 tokens: vec![
2479 Bytes::from_str("0xdac17f958d2ee523a2206206994597c13d831ec7").unwrap(),
2480 Bytes::from_str("0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48").unwrap(),
2481 ],
2482 contract_addresses: vec![],
2483 static_attributes: HashMap::new(),
2484 change: models::ChangeType::Creation,
2485 creation_tx: Bytes::from_str("0x000000000000000000000000000000000000000000000000000000000000c351").unwrap(),
2486 created_at: chrono::DateTime::from_timestamp(base_ts + 5000, 0).unwrap().naive_utc(),
2487 }),
2488 ]),
2489 deleted_protocol_components: HashMap::from([
2490 ("pc_3".to_string(), crate::models::protocol::ProtocolComponent {
2491 id: "pc_3".to_string(),
2492 protocol_system: "native_protocol_system".to_string(),
2493 protocol_type_name: "pt_2".to_string(),
2494 chain: models::Chain::Ethereum,
2495 tokens: vec![
2496 Bytes::from_str("0x6b175474e89094c44da98b954eedeac495271d0f").unwrap(),
2497 Bytes::from_str("0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2").unwrap(),
2498 ],
2499 contract_addresses: vec![],
2500 static_attributes: HashMap::new(),
2501 change: models::ChangeType::Deletion,
2502 creation_tx: Bytes::from_str("0x0000000000000000000000000000000000000000000000000000000000009c41").unwrap(),
2503 created_at: chrono::DateTime::from_timestamp(base_ts + 4000, 0).unwrap().naive_utc(),
2504 }),
2505 ]),
2506 component_balances: HashMap::from([
2507 ("pc_1".to_string(), HashMap::from([
2508 (Bytes::from_str("0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48").unwrap(), models::protocol::ComponentBalance {
2509 token: Bytes::from_str("0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48").unwrap(),
2510 balance: Bytes::from("0x00000001"),
2511 balance_float: 1.0,
2512 modify_tx: Bytes::from_str("0x0000000000000000000000000000000000000000000000000000000000000000").unwrap(),
2513 component_id: "pc_1".to_string(),
2514 }),
2515 (Bytes::from_str("0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2").unwrap(), models::protocol::ComponentBalance {
2516 token: Bytes::from_str("0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2").unwrap(),
2517 balance: Bytes::from("0x000003e8"),
2518 balance_float: 1000.0,
2519 modify_tx: Bytes::from_str("0x0000000000000000000000000000000000000000000000000000000000007531").unwrap(),
2520 component_id: "pc_1".to_string(),
2521 }),
2522 ])),
2523 ]),
2524 account_balances: HashMap::from([
2525 (Bytes::from_str("0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2").unwrap(), HashMap::from([
2526 (Bytes::from_str("0x7a250d5630b4cf539739df2c5dacb4c659f2488d").unwrap(), models::contract::AccountBalance {
2527 account: Bytes::from_str("0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2").unwrap(),
2528 token: Bytes::from_str("0x7a250d5630b4cf539739df2c5dacb4c659f2488d").unwrap(),
2529 balance: Bytes::from("0x000003e8"),
2530 modify_tx: Bytes::from_str("0x0000000000000000000000000000000000000000000000000000000000007531").unwrap(),
2531 }),
2532 ])),
2533 ]),
2534 ..Default::default()
2535 }
2536 }
2537
2538 #[test]
2539 fn test_serialize_deserialize_block_changes() {
2540 let block_entity_changes = create_models_block_changes();
2545
2546 let json_data = serde_json::to_string(&block_entity_changes).expect("Failed to serialize");
2548
2549 serde_json::from_str::<BlockChanges>(&json_data).expect("parsing failed");
2551 }
2552
2553 #[test]
2554 fn test_parse_block_changes() {
2555 let json_data = r#"
2556 {
2557 "extractor": "vm:ambient",
2558 "chain": "ethereum",
2559 "block": {
2560 "number": 123,
2561 "hash": "0x0000000000000000000000000000000000000000000000000000000000000000",
2562 "parent_hash": "0x0000000000000000000000000000000000000000000000000000000000000000",
2563 "chain": "ethereum",
2564 "ts": "2023-09-14T00:00:00"
2565 },
2566 "finalized_block_height": 0,
2567 "revert": false,
2568 "new_tokens": {},
2569 "account_updates": {
2570 "0x7a250d5630b4cf539739df2c5dacb4c659f2488d": {
2571 "address": "0x7a250d5630b4cf539739df2c5dacb4c659f2488d",
2572 "chain": "ethereum",
2573 "slots": {},
2574 "balance": "0x01f4",
2575 "code": "",
2576 "change": "Update"
2577 }
2578 },
2579 "state_updates": {
2580 "component_1": {
2581 "component_id": "component_1",
2582 "updated_attributes": {"attr1": "0x01"},
2583 "deleted_attributes": ["attr2"]
2584 }
2585 },
2586 "new_protocol_components":
2587 { "protocol_1": {
2588 "id": "protocol_1",
2589 "protocol_system": "system_1",
2590 "protocol_type_name": "type_1",
2591 "chain": "ethereum",
2592 "tokens": ["0x01", "0x02"],
2593 "contract_ids": ["0x01", "0x02"],
2594 "static_attributes": {"attr1": "0x01f4"},
2595 "change": "Update",
2596 "creation_tx": "0x01",
2597 "created_at": "2023-09-14T00:00:00"
2598 }
2599 },
2600 "deleted_protocol_components": {},
2601 "component_balances": {
2602 "protocol_1":
2603 {
2604 "0x01": {
2605 "token": "0x01",
2606 "balance": "0xb77831d23691653a01",
2607 "balance_float": 3.3844151001790677e21,
2608 "modify_tx": "0x01",
2609 "component_id": "protocol_1"
2610 }
2611 }
2612 },
2613 "account_balances": {
2614 "0x7a250d5630b4cf539739df2c5dacb4c659f2488d": {
2615 "0x7a250d5630b4cf539739df2c5dacb4c659f2488d": {
2616 "account": "0x7a250d5630b4cf539739df2c5dacb4c659f2488d",
2617 "token": "0x7a250d5630b4cf539739df2c5dacb4c659f2488d",
2618 "balance": "0x01f4",
2619 "modify_tx": "0x01"
2620 }
2621 }
2622 },
2623 "component_tvl": {
2624 "protocol_1": 1000.0
2625 },
2626 "dci_update": {
2627 "new_entrypoints": {
2628 "component_1": [
2629 {
2630 "external_id": "0x01:sig()",
2631 "target": "0x01",
2632 "signature": "sig()"
2633 }
2634 ]
2635 },
2636 "new_entrypoint_params": {
2637 "0x01:sig()": [
2638 [
2639 {
2640 "method": "rpctracer",
2641 "caller": "0x01",
2642 "calldata": "0x02"
2643 },
2644 "component_1"
2645 ]
2646 ]
2647 },
2648 "trace_results": {
2649 "0x01:sig()": {
2650 "retriggers": [
2651 ["0x01", {"key": "0x02", "offset": 12}]
2652 ],
2653 "accessed_slots": {
2654 "0x03": ["0x03", "0x04"]
2655 }
2656 }
2657 }
2658 }
2659 }
2660 "#;
2661
2662 serde_json::from_str::<BlockChanges>(json_data).expect("parsing failed");
2663 }
2664
2665 #[test]
2666 fn test_parse_websocket_message() {
2667 let json_data = r#"
2668 {
2669 "subscription_id": "5d23bfbe-89ad-4ea3-8672-dc9e973ac9dc",
2670 "deltas": {
2671 "type": "BlockChanges",
2672 "extractor": "uniswap_v2",
2673 "chain": "ethereum",
2674 "block": {
2675 "number": 19291517,
2676 "hash": "0xbc3ea4896c0be8da6229387a8571b72818aa258daf4fab46471003ad74c4ee83",
2677 "parent_hash": "0x89ca5b8d593574cf6c886f41ef8208bf6bdc1a90ef36046cb8c84bc880b9af8f",
2678 "chain": "ethereum",
2679 "ts": "2024-02-23T16:35:35"
2680 },
2681 "finalized_block_height": 0,
2682 "revert": false,
2683 "new_tokens": {},
2684 "account_updates": {
2685 "0x7a250d5630b4cf539739df2c5dacb4c659f2488d": {
2686 "address": "0x7a250d5630b4cf539739df2c5dacb4c659f2488d",
2687 "chain": "ethereum",
2688 "slots": {},
2689 "balance": "0x01f4",
2690 "code": "",
2691 "change": "Update"
2692 }
2693 },
2694 "state_updates": {
2695 "0xde6faedbcae38eec6d33ad61473a04a6dd7f6e28": {
2696 "component_id": "0xde6faedbcae38eec6d33ad61473a04a6dd7f6e28",
2697 "updated_attributes": {
2698 "reserve0": "0x87f7b5973a7f28a8b32404",
2699 "reserve1": "0x09e9564b11"
2700 },
2701 "deleted_attributes": []
2702 },
2703 "0x99c59000f5a76c54c4fd7d82720c045bdcf1450d": {
2704 "component_id": "0x99c59000f5a76c54c4fd7d82720c045bdcf1450d",
2705 "updated_attributes": {
2706 "reserve1": "0x44d9a8fd662c2f4d03",
2707 "reserve0": "0x500b1261f811d5bf423e"
2708 },
2709 "deleted_attributes": []
2710 }
2711 },
2712 "new_protocol_components": {},
2713 "deleted_protocol_components": {},
2714 "component_balances": {
2715 "0x99c59000f5a76c54c4fd7d82720c045bdcf1450d": {
2716 "0x9012744b7a564623b6c3e40b144fc196bdedf1a9": {
2717 "token": "0x9012744b7a564623b6c3e40b144fc196bdedf1a9",
2718 "balance": "0x500b1261f811d5bf423e",
2719 "balance_float": 3.779935574269033E23,
2720 "modify_tx": "0xe46c4db085fb6c6f3408a65524555797adb264e1d5cf3b66ad154598f85ac4bf",
2721 "component_id": "0x99c59000f5a76c54c4fd7d82720c045bdcf1450d"
2722 },
2723 "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2": {
2724 "token": "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2",
2725 "balance": "0x44d9a8fd662c2f4d03",
2726 "balance_float": 1.270062661329837E21,
2727 "modify_tx": "0xe46c4db085fb6c6f3408a65524555797adb264e1d5cf3b66ad154598f85ac4bf",
2728 "component_id": "0x99c59000f5a76c54c4fd7d82720c045bdcf1450d"
2729 }
2730 }
2731 },
2732 "account_balances": {
2733 "0x7a250d5630b4cf539739df2c5dacb4c659f2488d": {
2734 "0x7a250d5630b4cf539739df2c5dacb4c659f2488d": {
2735 "account": "0x7a250d5630b4cf539739df2c5dacb4c659f2488d",
2736 "token": "0x7a250d5630b4cf539739df2c5dacb4c659f2488d",
2737 "balance": "0x01f4",
2738 "modify_tx": "0x01"
2739 }
2740 }
2741 },
2742 "component_tvl": {},
2743 "dci_update": {
2744 "new_entrypoints": {
2745 "0xde6faedbcae38eec6d33ad61473a04a6dd7f6e28": [
2746 {
2747 "external_id": "0x01:sig()",
2748 "target": "0x01",
2749 "signature": "sig()"
2750 }
2751 ]
2752 },
2753 "new_entrypoint_params": {
2754 "0x01:sig()": [
2755 [
2756 {
2757 "method": "rpctracer",
2758 "caller": "0x01",
2759 "calldata": "0x02"
2760 },
2761 "0xde6faedbcae38eec6d33ad61473a04a6dd7f6e28"
2762 ]
2763 ]
2764 },
2765 "trace_results": {
2766 "0x01:sig()": {
2767 "retriggers": [
2768 ["0x01", {"key": "0x02", "offset": 12}]
2769 ],
2770 "accessed_slots": {
2771 "0x03": ["0x03", "0x04"]
2772 }
2773 }
2774 }
2775 }
2776 }
2777 }
2778 "#;
2779 serde_json::from_str::<WebSocketMessage>(json_data).expect("parsing failed");
2780 }
2781
2782 #[test]
2783 fn test_protocol_state_delta_merge_update_delete() {
2784 let mut delta1 = ProtocolStateDelta {
2786 component_id: "Component1".to_string(),
2787 updated_attributes: HashMap::from([(
2788 "Attribute1".to_string(),
2789 Bytes::from("0xbadbabe420"),
2790 )]),
2791 deleted_attributes: HashSet::new(),
2792 };
2793 let delta2 = ProtocolStateDelta {
2794 component_id: "Component1".to_string(),
2795 updated_attributes: HashMap::from([(
2796 "Attribute2".to_string(),
2797 Bytes::from("0x0badbabe"),
2798 )]),
2799 deleted_attributes: HashSet::from(["Attribute1".to_string()]),
2800 };
2801 let exp = ProtocolStateDelta {
2802 component_id: "Component1".to_string(),
2803 updated_attributes: HashMap::from([(
2804 "Attribute2".to_string(),
2805 Bytes::from("0x0badbabe"),
2806 )]),
2807 deleted_attributes: HashSet::from(["Attribute1".to_string()]),
2808 };
2809
2810 delta1.merge(&delta2);
2811
2812 assert_eq!(delta1, exp);
2813 }
2814
2815 #[test]
2816 fn test_protocol_state_delta_merge_delete_update() {
2817 let mut delta1 = ProtocolStateDelta {
2819 component_id: "Component1".to_string(),
2820 updated_attributes: HashMap::new(),
2821 deleted_attributes: HashSet::from(["Attribute1".to_string()]),
2822 };
2823 let delta2 = ProtocolStateDelta {
2824 component_id: "Component1".to_string(),
2825 updated_attributes: HashMap::from([(
2826 "Attribute1".to_string(),
2827 Bytes::from("0x0badbabe"),
2828 )]),
2829 deleted_attributes: HashSet::new(),
2830 };
2831 let exp = ProtocolStateDelta {
2832 component_id: "Component1".to_string(),
2833 updated_attributes: HashMap::from([(
2834 "Attribute1".to_string(),
2835 Bytes::from("0x0badbabe"),
2836 )]),
2837 deleted_attributes: HashSet::new(),
2838 };
2839
2840 delta1.merge(&delta2);
2841
2842 assert_eq!(delta1, exp);
2843 }
2844
2845 #[test]
2846 fn test_account_update_merge() {
2847 let mut account1 = AccountUpdate::new(
2849 Bytes::from(b"0x1234"),
2850 Chain::Ethereum,
2851 HashMap::from([(Bytes::from("0xaabb"), Bytes::from("0xccdd"))]),
2852 Some(Bytes::from("0x1000")),
2853 Some(Bytes::from("0xdeadbeaf")),
2854 ChangeType::Creation,
2855 );
2856
2857 let account2 = AccountUpdate::new(
2858 Bytes::from(b"0x1234"), Chain::Ethereum,
2860 HashMap::from([(Bytes::from("0xeeff"), Bytes::from("0x11223344"))]),
2861 Some(Bytes::from("0x2000")),
2862 Some(Bytes::from("0xcafebabe")),
2863 ChangeType::Update,
2864 );
2865
2866 account1.merge(&account2);
2868
2869 let expected = AccountUpdate::new(
2871 Bytes::from(b"0x1234"), Chain::Ethereum,
2873 HashMap::from([
2874 (Bytes::from("0xaabb"), Bytes::from("0xccdd")), (Bytes::from("0xeeff"), Bytes::from("0x11223344")), ]),
2877 Some(Bytes::from("0x2000")), Some(Bytes::from("0xcafebabe")), ChangeType::Creation, );
2881
2882 assert_eq!(account1, expected);
2884 }
2885
2886 #[test]
2887 fn test_block_account_changes_merge() {
2888 let old_account_updates: HashMap<Bytes, AccountUpdate> = [(
2890 Bytes::from("0x0011"),
2891 AccountUpdate {
2892 address: Bytes::from("0x00"),
2893 chain: Chain::Ethereum,
2894 slots: HashMap::from([(Bytes::from("0x0022"), Bytes::from("0x0033"))]),
2895 balance: Some(Bytes::from("0x01")),
2896 code: Some(Bytes::from("0x02")),
2897 change: ChangeType::Creation,
2898 },
2899 )]
2900 .into_iter()
2901 .collect();
2902 let new_account_updates: HashMap<Bytes, AccountUpdate> = [(
2903 Bytes::from("0x0011"),
2904 AccountUpdate {
2905 address: Bytes::from("0x00"),
2906 chain: Chain::Ethereum,
2907 slots: HashMap::from([(Bytes::from("0x0044"), Bytes::from("0x0055"))]),
2908 balance: Some(Bytes::from("0x03")),
2909 code: Some(Bytes::from("0x04")),
2910 change: ChangeType::Update,
2911 },
2912 )]
2913 .into_iter()
2914 .collect();
2915 let block_account_changes_initial = BlockChanges {
2917 extractor: "extractor1".to_string(),
2918 revert: false,
2919 account_updates: old_account_updates,
2920 ..Default::default()
2921 };
2922
2923 let block_account_changes_new = BlockChanges {
2924 extractor: "extractor2".to_string(),
2925 revert: true,
2926 account_updates: new_account_updates,
2927 ..Default::default()
2928 };
2929
2930 let res = block_account_changes_initial.merge(block_account_changes_new);
2932
2933 let expected_account_updates: HashMap<Bytes, AccountUpdate> = [(
2935 Bytes::from("0x0011"),
2936 AccountUpdate {
2937 address: Bytes::from("0x00"),
2938 chain: Chain::Ethereum,
2939 slots: HashMap::from([
2940 (Bytes::from("0x0044"), Bytes::from("0x0055")),
2941 (Bytes::from("0x0022"), Bytes::from("0x0033")),
2942 ]),
2943 balance: Some(Bytes::from("0x03")),
2944 code: Some(Bytes::from("0x04")),
2945 change: ChangeType::Creation,
2946 },
2947 )]
2948 .into_iter()
2949 .collect();
2950 let block_account_changes_expected = BlockChanges {
2951 extractor: "extractor1".to_string(),
2952 revert: true,
2953 account_updates: expected_account_updates,
2954 ..Default::default()
2955 };
2956 assert_eq!(res, block_account_changes_expected);
2957 }
2958
2959 #[test]
2960 fn test_block_entity_changes_merge() {
2961 let block_entity_changes_result1 = BlockChanges {
2963 extractor: String::from("extractor1"),
2964 revert: false,
2965 state_updates: hashmap! { "state1".to_string() => ProtocolStateDelta::default() },
2966 new_protocol_components: hashmap! { "component1".to_string() => ProtocolComponent::default() },
2967 deleted_protocol_components: HashMap::new(),
2968 component_balances: hashmap! {
2969 "component1".to_string() => TokenBalances(hashmap! {
2970 Bytes::from("0x01") => ComponentBalance {
2971 token: Bytes::from("0x01"),
2972 balance: Bytes::from("0x01"),
2973 balance_float: 1.0,
2974 modify_tx: Bytes::from("0x00"),
2975 component_id: "component1".to_string()
2976 },
2977 Bytes::from("0x02") => ComponentBalance {
2978 token: Bytes::from("0x02"),
2979 balance: Bytes::from("0x02"),
2980 balance_float: 2.0,
2981 modify_tx: Bytes::from("0x00"),
2982 component_id: "component1".to_string()
2983 },
2984 })
2985
2986 },
2987 component_tvl: hashmap! { "tvl1".to_string() => 1000.0 },
2988 ..Default::default()
2989 };
2990 let block_entity_changes_result2 = BlockChanges {
2991 extractor: String::from("extractor2"),
2992 revert: true,
2993 state_updates: hashmap! { "state2".to_string() => ProtocolStateDelta::default() },
2994 new_protocol_components: hashmap! { "component2".to_string() => ProtocolComponent::default() },
2995 deleted_protocol_components: hashmap! { "component3".to_string() => ProtocolComponent::default() },
2996 component_balances: hashmap! {
2997 "component1".to_string() => TokenBalances::default(),
2998 "component2".to_string() => TokenBalances::default()
2999 },
3000 component_tvl: hashmap! { "tvl2".to_string() => 2000.0 },
3001 ..Default::default()
3002 };
3003
3004 let res = block_entity_changes_result1.merge(block_entity_changes_result2);
3005
3006 let expected_block_entity_changes_result = BlockChanges {
3007 extractor: String::from("extractor1"),
3008 revert: true,
3009 state_updates: hashmap! {
3010 "state1".to_string() => ProtocolStateDelta::default(),
3011 "state2".to_string() => ProtocolStateDelta::default(),
3012 },
3013 new_protocol_components: hashmap! {
3014 "component1".to_string() => ProtocolComponent::default(),
3015 "component2".to_string() => ProtocolComponent::default(),
3016 },
3017 deleted_protocol_components: hashmap! {
3018 "component3".to_string() => ProtocolComponent::default(),
3019 },
3020 component_balances: hashmap! {
3021 "component1".to_string() => TokenBalances(hashmap! {
3022 Bytes::from("0x01") => ComponentBalance {
3023 token: Bytes::from("0x01"),
3024 balance: Bytes::from("0x01"),
3025 balance_float: 1.0,
3026 modify_tx: Bytes::from("0x00"),
3027 component_id: "component1".to_string()
3028 },
3029 Bytes::from("0x02") => ComponentBalance {
3030 token: Bytes::from("0x02"),
3031 balance: Bytes::from("0x02"),
3032 balance_float: 2.0,
3033 modify_tx: Bytes::from("0x00"),
3034 component_id: "component1".to_string()
3035 },
3036 }),
3037 "component2".to_string() => TokenBalances::default(),
3038 },
3039 component_tvl: hashmap! {
3040 "tvl1".to_string() => 1000.0,
3041 "tvl2".to_string() => 2000.0
3042 },
3043 ..Default::default()
3044 };
3045
3046 assert_eq!(res, expected_block_entity_changes_result);
3047 }
3048
3049 #[test]
3050 fn test_websocket_error_serialization() {
3051 let extractor_id = ExtractorIdentity::new(Chain::Ethereum, "test_extractor");
3052 let subscription_id = Uuid::new_v4();
3053
3054 let error = WebsocketError::ExtractorNotFound(extractor_id.clone());
3056 let json = serde_json::to_string(&error).unwrap();
3057 let deserialized: WebsocketError = serde_json::from_str(&json).unwrap();
3058 assert_eq!(error, deserialized);
3059
3060 let error = WebsocketError::SubscriptionNotFound(subscription_id);
3062 let json = serde_json::to_string(&error).unwrap();
3063 let deserialized: WebsocketError = serde_json::from_str(&json).unwrap();
3064 assert_eq!(error, deserialized);
3065
3066 let error = WebsocketError::ParseError("{asd".to_string(), "invalid json".to_string());
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::SubscribeError(extractor_id.clone());
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 =
3080 WebsocketError::CompressionError(subscription_id, "Compression failed".to_string());
3081 let json = serde_json::to_string(&error).unwrap();
3082 let deserialized: WebsocketError = serde_json::from_str(&json).unwrap();
3083 assert_eq!(error, deserialized);
3084 }
3085
3086 #[test]
3087 fn test_websocket_message_with_error_response() {
3088 let error =
3089 WebsocketError::ParseError("}asdfas".to_string(), "malformed request".to_string());
3090 let response = Response::Error(error.clone());
3091 let message = WebSocketMessage::Response(response);
3092
3093 let json = serde_json::to_string(&message).unwrap();
3094 let deserialized: WebSocketMessage = serde_json::from_str(&json).unwrap();
3095
3096 if let WebSocketMessage::Response(Response::Error(deserialized_error)) = deserialized {
3097 assert_eq!(error, deserialized_error);
3098 } else {
3099 panic!("Expected WebSocketMessage::Response(Response::Error)");
3100 }
3101 }
3102
3103 #[test]
3104 fn test_websocket_error_conversion_from_models() {
3105 use crate::models::error::WebsocketError as ModelsError;
3106
3107 let extractor_id =
3108 crate::models::ExtractorIdentity::new(crate::models::Chain::Ethereum, "test");
3109 let subscription_id = Uuid::new_v4();
3110
3111 let models_error = ModelsError::ExtractorNotFound(extractor_id.clone());
3113 let dto_error: WebsocketError = models_error.into();
3114 assert_eq!(dto_error, WebsocketError::ExtractorNotFound(extractor_id.clone().into()));
3115
3116 let models_error = ModelsError::SubscriptionNotFound(subscription_id);
3118 let dto_error: WebsocketError = models_error.into();
3119 assert_eq!(dto_error, WebsocketError::SubscriptionNotFound(subscription_id));
3120
3121 let json_result: Result<serde_json::Value, _> = serde_json::from_str("{invalid json");
3123 let json_error = json_result.unwrap_err();
3124 let models_error = ModelsError::ParseError("{invalid json".to_string(), json_error);
3125 let dto_error: WebsocketError = models_error.into();
3126 if let WebsocketError::ParseError(msg, error) = dto_error {
3127 assert!(!error.is_empty(), "Error message should not be empty, got: '{}'", msg);
3129 } else {
3130 panic!("Expected ParseError variant");
3131 }
3132
3133 let models_error = ModelsError::SubscribeError(extractor_id.clone());
3135 let dto_error: WebsocketError = models_error.into();
3136 assert_eq!(dto_error, WebsocketError::SubscribeError(extractor_id.into()));
3137
3138 let io_error = std::io::Error::other("Compression failed");
3140 let models_error = ModelsError::CompressionError(subscription_id, io_error);
3141 let dto_error: WebsocketError = models_error.into();
3142 if let WebsocketError::CompressionError(sub_id, msg) = &dto_error {
3143 assert_eq!(*sub_id, subscription_id);
3144 assert!(msg.contains("Compression failed"));
3145 } else {
3146 panic!("Expected CompressionError variant");
3147 }
3148 }
3149}
3150
3151#[cfg(test)]
3152mod memory_size_tests {
3153 use std::collections::HashMap;
3154
3155 use super::*;
3156
3157 #[test]
3158 fn test_state_request_response_memory_size_empty() {
3159 let response = StateRequestResponse {
3160 accounts: vec![],
3161 pagination: PaginationResponse::new(1, 10, 0),
3162 };
3163
3164 let size = response.deep_size_of();
3165
3166 assert!(size >= 48, "Empty response should have minimum size of 48 bytes, got {}", size);
3168 assert!(size < 200, "Empty response should not be too large, got {}", size);
3169 }
3170
3171 #[test]
3172 fn test_state_request_response_memory_size_scales_with_slots() {
3173 let create_response_with_slots = |slot_count: usize| {
3174 let mut slots = HashMap::new();
3175 for i in 0..slot_count {
3176 let key = vec![i as u8; 32]; let value = vec![(i + 100) as u8; 32]; slots.insert(key.into(), value.into());
3179 }
3180
3181 let account = ResponseAccount::new(
3182 Chain::Ethereum,
3183 vec![1; 20].into(),
3184 "Pool".to_string(),
3185 slots,
3186 vec![1; 32].into(),
3187 HashMap::new(),
3188 vec![].into(), vec![1; 32].into(),
3190 vec![1; 32].into(),
3191 vec![1; 32].into(),
3192 None,
3193 );
3194
3195 StateRequestResponse {
3196 accounts: vec![account],
3197 pagination: PaginationResponse::new(1, 10, 1),
3198 }
3199 };
3200
3201 let small_response = create_response_with_slots(10);
3202 let large_response = create_response_with_slots(100);
3203
3204 let small_size = small_response.deep_size_of();
3205 let large_size = large_response.deep_size_of();
3206
3207 assert!(
3209 large_size > small_size * 5,
3210 "Large response ({} bytes) should be much larger than small response ({} bytes)",
3211 large_size,
3212 small_size
3213 );
3214
3215 let size_diff = large_size - small_size;
3217 let expected_min_diff = 90 * 64; assert!(
3219 size_diff > expected_min_diff,
3220 "Size difference ({} bytes) should reflect the additional slot data",
3221 size_diff
3222 );
3223 }
3224}
3225
3226#[cfg(test)]
3227mod pagination_limits_tests {
3228 use super::*;
3229
3230 #[derive(Clone, Debug)]
3232 struct TestRequestBody {
3233 pagination: PaginationParams,
3234 }
3235
3236 impl_pagination_limits!(TestRequestBody, compressed = 500, uncompressed = 50);
3238
3239 #[test]
3240 fn test_effective_max_page_size() {
3241 let max_size = TestRequestBody::effective_max_page_size(true);
3243 assert_eq!(max_size, 500, "Should return compressed limit when compression is enabled");
3244
3245 let max_size = TestRequestBody::effective_max_page_size(false);
3247 assert_eq!(max_size, 50, "Should return uncompressed limit when compression is disabled");
3248 }
3249}