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