1pub mod blockchain;
2pub mod chain_config;
3pub mod contract;
4pub mod error;
5pub mod protocol;
6pub mod token;
7
8use std::{collections::HashMap, fmt::Display, str::FromStr};
9
10pub use blockchain::{BlockChanges, TxWithContractChanges};
11use chain_config::{
12 chain_registry, ChainConfigError, ChainConfigRegistry, CustomChainConfig, CustomChainId,
13 TvlThresholdTier,
14};
15use deepsize::DeepSizeOf;
16use serde::{Deserialize, Serialize};
17use thiserror::Error;
18use token::Token;
19
20use crate::{dto, Bytes};
21
22pub type Address = Bytes;
25
26pub type BlockHash = Bytes;
29
30pub type TxHash = Bytes;
33
34pub type Code = Bytes;
36
37pub type CodeHash = Bytes;
39
40pub type Balance = Bytes;
42
43pub type StoreKey = Bytes;
45
46pub type AttrStoreKey = String;
48
49pub type StoreVal = Bytes;
51
52pub type ContractStore = HashMap<StoreKey, StoreVal>;
54pub type ContractStoreDeltas = HashMap<StoreKey, Option<StoreVal>>;
55pub type AccountToContractStoreDeltas = HashMap<Address, ContractStoreDeltas>;
56
57pub type ComponentId = String;
59
60pub type ProtocolSystem = String;
62
63pub type EntryPointId = String;
65
66#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
84#[serde(rename_all = "lowercase")]
85#[non_exhaustive]
86pub enum Chain {
87 #[default]
88 Ethereum,
89 Starknet,
90 ZkSync,
91 Arbitrum,
92 Base,
93 Bsc,
94 Unichain,
95 Polygon,
96 Custom(CustomChainId),
98}
99
100impl DeepSizeOf for Chain {
101 fn deep_size_of_children(&self, _context: &mut deepsize::Context) -> usize {
102 0
103 }
104}
105
106impl Chain {
107 pub fn builtin_from_str(s: &str) -> Option<Self> {
109 match s {
110 "ethereum" => Some(Chain::Ethereum),
111 "starknet" => Some(Chain::Starknet),
112 "zksync" => Some(Chain::ZkSync),
113 "arbitrum" => Some(Chain::Arbitrum),
114 "base" => Some(Chain::Base),
115 "bsc" => Some(Chain::Bsc),
116 "unichain" => Some(Chain::Unichain),
117 "polygon" => Some(Chain::Polygon),
118 _ => None,
119 }
120 }
121
122 pub fn custom(name: &str) -> Result<Self, ChainConfigError> {
126 CustomChainId::checked(name, chain_registry()).map(Chain::Custom)
127 }
128}
129
130impl FromStr for Chain {
131 type Err = ChainConfigError;
132
133 fn from_str(s: &str) -> Result<Self, Self::Err> {
137 if let Some(chain) = Self::builtin_from_str(s) {
138 return Ok(chain);
139 }
140 Self::custom(s)
141 }
142}
143
144impl Display for Chain {
145 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
146 match self {
147 Chain::Ethereum => f.write_str("ethereum"),
148 Chain::Starknet => f.write_str("starknet"),
149 Chain::ZkSync => f.write_str("zksync"),
150 Chain::Arbitrum => f.write_str("arbitrum"),
151 Chain::Base => f.write_str("base"),
152 Chain::Bsc => f.write_str("bsc"),
153 Chain::Unichain => f.write_str("unichain"),
154 Chain::Polygon => f.write_str("polygon"),
155 Chain::Custom(name) => f.write_str(name.as_str()),
156 }
157 }
158}
159
160impl From<dto::Chain> for Chain {
161 fn from(value: dto::Chain) -> Self {
162 match value {
163 dto::Chain::Ethereum => Chain::Ethereum,
164 dto::Chain::Starknet => Chain::Starknet,
165 dto::Chain::ZkSync => Chain::ZkSync,
166 dto::Chain::Arbitrum => Chain::Arbitrum,
167 dto::Chain::Base => Chain::Base,
168 dto::Chain::Bsc => Chain::Bsc,
169 dto::Chain::Unichain => Chain::Unichain,
170 dto::Chain::Polygon => Chain::Polygon,
171 dto::Chain::Custom(name) => Chain::custom(name.as_str()).unwrap_or_else(|e| {
172 panic!(
173 "received custom chain '{name}' with no registered config: {e}; install it via \
174 the chain config file (TYCHO_CHAINS_CONFIG, default ./chains.yaml) or \
175 init_chain_registry before decoding wire data"
176 )
177 }),
178 }
179 }
180}
181
182impl From<dto::ChangeType> for ChangeType {
183 fn from(value: dto::ChangeType) -> Self {
184 match value {
185 dto::ChangeType::Update => ChangeType::Update,
186 dto::ChangeType::Creation => ChangeType::Creation,
187 dto::ChangeType::Deletion => ChangeType::Deletion,
188 dto::ChangeType::Unspecified => ChangeType::Update,
189 }
190 }
191}
192
193fn native_eth(chain: Chain) -> Token {
194 Token::new(
195 &Bytes::from_str("0x0000000000000000000000000000000000000000").unwrap(),
196 "ETH",
197 18,
198 0,
199 &[Some(2300)],
200 chain,
201 100,
202 )
203}
204
205fn native_bsc(chain: Chain) -> Token {
206 Token::new(
207 &Bytes::from_str("0x0000000000000000000000000000000000000000").unwrap(),
208 "BNB",
209 18,
210 0,
211 &[Some(2300)],
212 chain,
213 100,
214 )
215}
216
217fn wrapped_native_eth(chain: Chain, address: &str) -> Token {
218 Token::new(&Bytes::from_str(address).unwrap(), "WETH", 18, 0, &[Some(2300)], chain, 100)
219}
220
221fn native_pol(chain: Chain) -> Token {
222 Token::new(
223 &Bytes::from_str("0x0000000000000000000000000000000000000000").unwrap(),
224 "POL",
225 18,
226 0,
227 &[Some(2300)],
228 chain,
229 100,
230 )
231}
232
233fn try_resolve_custom<'a>(
236 id: &CustomChainId,
237 registry: &'a ChainConfigRegistry,
238) -> Result<&'a CustomChainConfig, ChainConfigError> {
239 registry
240 .get(id.as_str())
241 .ok_or_else(|| ChainConfigError::UnknownChain(id.as_str().to_owned()))
242}
243
244fn expect_registered<T>(result: Result<T, ChainConfigError>) -> T {
248 result.unwrap_or_else(|e| {
249 panic!(
250 "internal invariant violation resolving custom chain config: {e}; Chain::Custom is \
251 validated against the set-once chain registry at construction"
252 )
253 })
254}
255
256fn native_custom(chain: Chain, cfg: &CustomChainConfig) -> Token {
257 let addr = Bytes::from(cfg.native.address.as_bytes().to_vec());
258 Token::new(
259 &addr,
260 cfg.native.symbol.as_str(),
261 cfg.native.decimals as u32,
262 0,
263 &[Some(2300)],
264 chain,
265 100,
266 )
267}
268
269fn wrapped_native_bsc(chain: Chain, address: &str) -> Token {
270 Token::new(&Bytes::from_str(address).unwrap(), "WBNB", 18, 0, &[Some(2300)], chain, 100)
271}
272
273fn wrapped_native_pol(chain: Chain, address: &str) -> Token {
274 Token::new(&Bytes::from_str(address).unwrap(), "WMATIC", 18, 0, &[Some(2300)], chain, 100)
275}
276
277fn wrapped_native_custom(chain: Chain, cfg: &CustomChainConfig) -> Token {
278 let addr = Bytes::from(
279 cfg.wrapped_native
280 .address
281 .as_bytes()
282 .to_vec(),
283 );
284 Token::new(
285 &addr,
286 cfg.wrapped_native.symbol.as_str(),
287 cfg.wrapped_native.decimals as u32,
288 0,
289 &[Some(2300)],
290 chain,
291 100,
292 )
293}
294
295impl Chain {
296 pub fn id(&self) -> u64 {
300 expect_registered(self.try_id())
301 }
302
303 pub fn try_id(&self) -> Result<u64, ChainConfigError> {
306 Ok(match self {
307 Chain::Ethereum => 1,
308 Chain::ZkSync => 324,
309 Chain::Arbitrum => 42161,
310 Chain::Starknet => 0,
311 Chain::Base => 8453,
312 Chain::Bsc => 56,
313 Chain::Unichain => 130,
314 Chain::Polygon => 137,
315 Chain::Custom(id) => try_resolve_custom(id, chain_registry())?.chain_id,
316 })
317 }
318
319 pub fn default_tvl_threshold(&self, tier: TvlThresholdTier) -> f64 {
329 expect_registered(self.try_default_tvl_threshold(tier))
330 }
331
332 pub fn try_default_tvl_threshold(
335 &self,
336 tier: TvlThresholdTier,
337 ) -> Result<f64, ChainConfigError> {
338 Ok(match (self, tier) {
339 (
342 Chain::Ethereum |
343 Chain::Starknet |
344 Chain::ZkSync |
345 Chain::Arbitrum |
346 Chain::Base |
347 Chain::Unichain,
348 TvlThresholdTier::Low,
349 ) => 10.0,
350 (
351 Chain::Ethereum |
352 Chain::Starknet |
353 Chain::ZkSync |
354 Chain::Arbitrum |
355 Chain::Base |
356 Chain::Unichain,
357 TvlThresholdTier::Medium,
358 ) => 100.0,
359
360 (Chain::Polygon, TvlThresholdTier::Low) => 200_000.0,
362 (Chain::Polygon, TvlThresholdTier::Medium) => 2_000_000.0,
363
364 (Chain::Bsc, TvlThresholdTier::Low) => 32.0,
366 (Chain::Bsc, TvlThresholdTier::Medium) => 320.0,
367
368 (Chain::Custom(id), TvlThresholdTier::Low) => {
369 try_resolve_custom(id, chain_registry())?
370 .default_tvl_thresholds
371 .low
372 }
373 (Chain::Custom(id), TvlThresholdTier::Medium) => {
374 try_resolve_custom(id, chain_registry())?
375 .default_tvl_thresholds
376 .medium
377 }
378 })
379 }
380
381 pub fn native_token(&self) -> Token {
384 expect_registered(self.try_native_token())
385 }
386
387 pub fn try_native_token(&self) -> Result<Token, ChainConfigError> {
390 Ok(match self {
391 Chain::Ethereum => native_eth(Chain::Ethereum),
392 Chain::Starknet => native_eth(Chain::Starknet),
395 Chain::ZkSync => native_eth(Chain::ZkSync),
396 Chain::Arbitrum => native_eth(Chain::Arbitrum),
397 Chain::Base => native_eth(Chain::Base),
398 Chain::Bsc => native_bsc(Chain::Bsc),
399 Chain::Unichain => native_eth(Chain::Unichain),
400 Chain::Polygon => native_pol(Chain::Polygon),
401 Chain::Custom(id) => native_custom(*self, try_resolve_custom(id, chain_registry())?),
402 })
403 }
404
405 pub fn wrapped_native_token(&self) -> Token {
408 expect_registered(self.try_wrapped_native_token())
409 }
410
411 pub fn try_wrapped_native_token(&self) -> Result<Token, ChainConfigError> {
414 Ok(match self {
415 Chain::Ethereum => {
416 wrapped_native_eth(Chain::Ethereum, "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2")
417 }
418 Chain::Starknet => {
420 wrapped_native_eth(Chain::Starknet, "0x0000000000000000000000000000000000000000")
421 }
422 Chain::ZkSync => {
423 wrapped_native_eth(Chain::ZkSync, "0x5AEa5775959fBC2557Cc8789bC1bf90A239D9a91")
424 }
425 Chain::Arbitrum => {
426 wrapped_native_eth(Chain::Arbitrum, "0x82aF49447D8a07e3bd95BD0d56f35241523fBab1")
427 }
428 Chain::Base => {
429 wrapped_native_eth(Chain::Base, "0x4200000000000000000000000000000000000006")
430 }
431 Chain::Bsc => {
432 wrapped_native_bsc(Chain::Bsc, "0xbb4CdB9CBd36B01bD1cBaEBF2De08d9173bc095c")
433 }
434 Chain::Unichain => {
435 wrapped_native_eth(Chain::Unichain, "0x4200000000000000000000000000000000000006")
436 }
437 Chain::Polygon => {
438 wrapped_native_pol(Chain::Polygon, "0x0d500B1d8E8eF31E21C99d1Db9A6444d3ADf1270")
439 }
440 Chain::Custom(id) => {
441 wrapped_native_custom(*self, try_resolve_custom(id, chain_registry())?)
442 }
443 })
444 }
445
446 pub fn block_time_secs(&self) -> u64 {
449 expect_registered(self.try_block_time_secs())
450 }
451
452 pub fn try_block_time_secs(&self) -> Result<u64, ChainConfigError> {
455 Ok(match self {
456 Chain::Ethereum => 12,
457 Chain::Starknet => 2,
458 Chain::ZkSync => 3,
459 Chain::Arbitrum => 1,
460 Chain::Base => 2,
461 Chain::Bsc => 1,
462 Chain::Unichain => 1,
463 Chain::Polygon => 2,
464 Chain::Custom(id) => try_resolve_custom(id, chain_registry())?.block_time_secs,
465 })
466 }
467}
468
469#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, Hash, Default)]
470pub struct ExtractorIdentity {
471 pub chain: Chain,
472 pub name: String,
473}
474
475impl ExtractorIdentity {
476 pub fn new(chain: Chain, name: &str) -> Self {
477 Self { chain, name: name.to_owned() }
478 }
479}
480
481impl std::fmt::Display for ExtractorIdentity {
482 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
483 write!(f, "{}:{}", self.chain, self.name)
484 }
485}
486
487impl From<ExtractorIdentity> for dto::ExtractorIdentity {
488 fn from(value: ExtractorIdentity) -> Self {
489 dto::ExtractorIdentity { chain: value.chain.into(), name: value.name }
490 }
491}
492
493impl From<dto::ExtractorIdentity> for ExtractorIdentity {
494 fn from(value: dto::ExtractorIdentity) -> Self {
495 Self { chain: value.chain.into(), name: value.name }
496 }
497}
498
499#[derive(Debug, PartialEq, Clone)]
500pub struct ExtractionState {
501 pub name: String,
502 pub chain: Chain,
503 pub attributes: serde_json::Value,
504 pub cursor: Vec<u8>,
505 pub block_hash: Bytes,
506}
507
508impl ExtractionState {
509 pub fn new(
510 name: String,
511 chain: Chain,
512 attributes: Option<serde_json::Value>,
513 cursor: &[u8],
514 block_hash: Bytes,
515 ) -> Self {
516 ExtractionState {
517 name,
518 chain,
519 attributes: attributes.unwrap_or_default(),
520 cursor: cursor.to_vec(),
521 block_hash,
522 }
523 }
524}
525
526#[derive(PartialEq, Debug, Clone, Default, Deserialize, Serialize)]
527pub enum ImplementationType {
528 #[default]
529 Vm,
530 Custom,
531}
532
533#[derive(PartialEq, Debug, Clone, Default, Deserialize, Serialize)]
534pub enum FinancialType {
535 #[default]
536 Swap,
537 Psm,
538 Debt,
539 Leverage,
540}
541
542#[derive(Debug, PartialEq, Clone, Default, Deserialize, Serialize)]
543pub struct ProtocolType {
544 pub name: String,
545 pub financial_type: FinancialType,
546 pub attribute_schema: Option<serde_json::Value>,
547 pub implementation: ImplementationType,
548}
549
550impl ProtocolType {
551 pub fn new(
552 name: String,
553 financial_type: FinancialType,
554 attribute_schema: Option<serde_json::Value>,
555 implementation: ImplementationType,
556 ) -> Self {
557 ProtocolType { name, financial_type, attribute_schema, implementation }
558 }
559}
560
561#[derive(Debug, PartialEq, Eq, Default, Copy, Clone, Deserialize, Serialize, DeepSizeOf)]
562pub enum ChangeType {
563 #[default]
564 Update,
565 Deletion,
566 Creation,
567}
568
569#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)]
570pub struct ContractId {
571 pub address: Address,
572 pub chain: Chain,
573}
574
575impl ContractId {
577 pub fn new(chain: Chain, address: Address) -> Self {
578 Self { address, chain }
579 }
580
581 pub fn address(&self) -> &Address {
582 &self.address
583 }
584}
585
586impl Display for ContractId {
587 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
588 write!(f, "{:?}: 0x{}", self.chain, hex::encode(&self.address))
589 }
590}
591
592#[derive(Debug, PartialEq, Clone, Default, Deserialize, Serialize)]
593pub struct PaginationParams {
594 pub page: i64,
595 pub page_size: i64,
596}
597
598impl PaginationParams {
599 pub fn new(page: i64, page_size: i64) -> Self {
600 Self { page, page_size }
601 }
602
603 pub fn offset(&self) -> i64 {
604 self.page * self.page_size
605 }
606}
607
608impl From<&dto::PaginationParams> for PaginationParams {
609 fn from(value: &dto::PaginationParams) -> Self {
610 PaginationParams { page: value.page, page_size: value.page_size }
611 }
612}
613
614#[derive(Error, Debug, PartialEq)]
615pub enum MergeError {
616 #[error("Can't merge {0} from differring idendities: Expected {1}, got {2}")]
617 IdMismatch(String, String, String),
618 #[error("Can't merge {0} from different blocks: 0x{1:x} != 0x{2:x}")]
619 BlockMismatch(String, Bytes, Bytes),
620 #[error("Can't merge {0} from the same transaction: 0x{1:x}")]
621 SameTransaction(String, Bytes),
622 #[error("Can't merge {0} with lower transaction index: {1} > {2}")]
623 TransactionOrderError(String, u64, u64),
624 #[error("Cannot merge: {0}")]
625 InvalidState(String),
626}
627
628#[cfg(test)]
632mod tests {
633 use arrayvec::ArrayString;
634
635 use super::{
636 chain_config::{
637 init_chain_registry, ChainAddress, ChainConfigError, ChainTokenConfig, TvlThresholds,
638 },
639 *,
640 };
641
642 fn test_config() -> CustomChainConfig {
643 CustomChainConfig::try_new(
644 "testchain",
645 9999,
646 5,
647 ChainTokenConfig::try_new("0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", "TST", 18)
648 .unwrap(),
649 ChainTokenConfig::try_new("0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", "WTST", 18)
650 .unwrap(),
651 TvlThresholds::new(50.0, 500.0),
652 )
653 .unwrap()
654 }
655
656 fn init_test_registry() {
657 init_chain_registry(ChainConfigRegistry::from_configs([test_config()]).unwrap())
658 .expect("chain registry already initialised; run tests under nextest");
659 }
660
661 #[test]
662 fn test_custom_chain_display() {
663 init_test_registry();
664 assert_eq!(
665 Chain::custom("testchain")
666 .unwrap()
667 .to_string(),
668 "testchain"
669 );
670 }
671
672 #[test]
673 fn test_from_str_custom_returns_err() {
674 assert!("custom".parse::<Chain>().is_err());
675 assert!("unknown".parse::<Chain>().is_err());
676 }
677
678 #[test]
679 fn test_custom_unregistered_returns_err() {
680 init_test_registry();
681 assert_eq!(Chain::custom("nope"), Err(ChainConfigError::UnknownChain("nope".to_owned())));
682 }
683
684 #[test]
685 fn test_from_dto_registered_custom_roundtrips() {
686 init_test_registry();
687 let dto_chain: dto::Chain = Chain::custom("testchain")
688 .unwrap()
689 .into();
690 let chain: Chain = dto_chain.into();
691 assert_eq!(chain.id(), 9999);
692 }
693
694 #[test]
695 #[should_panic(expected = "no registered config")]
696 fn test_from_dto_unregistered_custom_panics() {
697 let dto_chain = dto::Chain::Custom(ArrayString::from("nope").unwrap());
698 let _: Chain = dto_chain.into();
699 }
700
701 #[test]
702 fn test_try_accessors_ok_for_registered_custom() {
703 init_test_registry();
704 let chain = Chain::custom("testchain").unwrap();
705 assert_eq!(chain.try_id().unwrap(), 9999);
706 assert_eq!(chain.try_block_time_secs().unwrap(), 5);
707 assert_eq!(
708 chain
709 .try_default_tvl_threshold(TvlThresholdTier::Low)
710 .unwrap(),
711 50.0
712 );
713 assert_eq!(chain.try_native_token().unwrap().symbol, "TST");
714 assert_eq!(
715 chain
716 .try_wrapped_native_token()
717 .unwrap()
718 .symbol,
719 "WTST"
720 );
721 }
722
723 #[test]
724 fn test_try_accessors_err_for_unregistered_custom() {
725 let ghost: Chain = serde_json::from_str(r#"{"custom":"ghostchain"}"#).unwrap();
728 assert_eq!(ghost.try_id(), Err(ChainConfigError::UnknownChain("ghostchain".to_owned())));
729 assert!(ghost.try_native_token().is_err());
730 }
731
732 #[test]
733 fn test_chain_stays_small() {
734 assert!(
737 std::mem::size_of::<Chain>() <= 40,
738 "Chain is {} bytes",
739 std::mem::size_of::<Chain>()
740 );
741 }
742
743 #[test]
744 fn test_custom_chain_id() {
745 init_test_registry();
746 let chain = Chain::custom("testchain").unwrap();
747 assert_eq!(chain.id(), 9999);
748 }
749
750 #[test]
751 fn test_custom_chain_tvl_thresholds() {
752 init_test_registry();
753 let chain = Chain::custom("testchain").unwrap();
754 assert_eq!(chain.default_tvl_threshold(TvlThresholdTier::Low), 50.0);
755 assert_eq!(chain.default_tvl_threshold(TvlThresholdTier::Medium), 500.0);
756 }
757
758 #[test]
759 fn test_custom_chain_native_token() {
760 init_test_registry();
761 let chain = Chain::custom("testchain").unwrap();
762 let token = chain.native_token();
763 assert_eq!(token.symbol, "TST");
764 assert_eq!(token.decimals, 18);
765 assert_eq!(token.chain, chain);
766 assert_eq!(token.address, Bytes::from(vec![0xAA; 20]));
767 }
768
769 #[test]
770 fn test_custom_chain_wrapped_native_token() {
771 init_test_registry();
772 let chain = Chain::custom("testchain").unwrap();
773 let token = chain.wrapped_native_token();
774 assert_eq!(token.symbol, "WTST");
775 assert_eq!(token.chain, chain);
776 assert_eq!(token.address, Bytes::from(vec![0xBB; 20]));
777 }
778
779 #[test]
780 fn test_chain_address_new_rejects_oversized_input() {
781 assert_eq!(ChainAddress::new(&[0u8; 33]), Err(ChainConfigError::AddressTooLong(33)));
782 }
783
784 #[test]
785 fn test_chain_address_as_bytes_returns_active_slice() {
786 let addr = ChainAddress::new(&[0xAA; 20]).unwrap();
787 assert_eq!(addr.as_bytes(), &[0xAA; 20]);
788 assert_eq!(addr.as_bytes().len(), 20);
789 }
790}