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 Plasma,
97 Robinhood,
98 Arc,
99 Custom(CustomChainId),
101}
102
103#[derive(Debug, Clone, PartialEq, Eq)]
105pub enum NativeAsset {
106 Wrapper { native: Token, wrapper: Token },
108 SharedBalance { native: Token, routable: Token },
110 NativeOnly { native: Token },
112}
113
114impl NativeAsset {
115 pub fn native_token(&self) -> &Token {
116 match self {
117 NativeAsset::Wrapper { native, wrapper: _ } |
118 NativeAsset::SharedBalance { native, routable: _ } |
119 NativeAsset::NativeOnly { native } => native,
120 }
121 }
122
123 pub fn routable_token(&self) -> Option<&Token> {
124 match self {
125 NativeAsset::Wrapper { native: _, wrapper } => Some(wrapper),
126 NativeAsset::SharedBalance { native: _, routable } => Some(routable),
127 NativeAsset::NativeOnly { native: _ } => None,
128 }
129 }
130
131 pub fn wrapper(&self) -> Option<&Token> {
133 match self {
134 NativeAsset::Wrapper { native: _, wrapper } => Some(wrapper),
135 NativeAsset::SharedBalance { native: _, routable: _ } |
136 NativeAsset::NativeOnly { native: _ } => None,
137 }
138 }
139
140 pub fn representations(&self) -> impl Iterator<Item = &Token> {
142 std::iter::once(self.native_token()).chain(self.routable_token())
143 }
144}
145
146impl DeepSizeOf for Chain {
147 fn deep_size_of_children(&self, _context: &mut deepsize::Context) -> usize {
148 0
149 }
150}
151
152impl Chain {
153 pub fn builtin_from_str(s: &str) -> Option<Self> {
155 match s {
156 "ethereum" => Some(Chain::Ethereum),
157 "starknet" => Some(Chain::Starknet),
158 "zksync" => Some(Chain::ZkSync),
159 "arbitrum" => Some(Chain::Arbitrum),
160 "base" => Some(Chain::Base),
161 "bsc" => Some(Chain::Bsc),
162 "unichain" => Some(Chain::Unichain),
163 "polygon" => Some(Chain::Polygon),
164 "plasma" => Some(Chain::Plasma),
165 "robinhood" => Some(Chain::Robinhood),
166 "arc" => Some(Chain::Arc),
167 _ => None,
168 }
169 }
170
171 pub fn custom(name: &str) -> Result<Self, ChainConfigError> {
175 CustomChainId::checked(name, chain_registry()).map(Chain::Custom)
176 }
177}
178
179impl FromStr for Chain {
180 type Err = ChainConfigError;
181
182 fn from_str(s: &str) -> Result<Self, Self::Err> {
186 if let Some(chain) = Self::builtin_from_str(s) {
187 return Ok(chain);
188 }
189 Self::custom(s)
190 }
191}
192
193impl Display for Chain {
194 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
195 match self {
196 Chain::Ethereum => f.write_str("ethereum"),
197 Chain::Starknet => f.write_str("starknet"),
198 Chain::ZkSync => f.write_str("zksync"),
199 Chain::Arbitrum => f.write_str("arbitrum"),
200 Chain::Base => f.write_str("base"),
201 Chain::Bsc => f.write_str("bsc"),
202 Chain::Unichain => f.write_str("unichain"),
203 Chain::Polygon => f.write_str("polygon"),
204 Chain::Plasma => f.write_str("plasma"),
205 Chain::Robinhood => f.write_str("robinhood"),
206 Chain::Arc => f.write_str("arc"),
207 Chain::Custom(name) => f.write_str(name.as_str()),
208 }
209 }
210}
211
212impl From<dto::Chain> for Chain {
213 fn from(value: dto::Chain) -> Self {
214 match value {
215 dto::Chain::Ethereum => Chain::Ethereum,
216 dto::Chain::Starknet => Chain::Starknet,
217 dto::Chain::ZkSync => Chain::ZkSync,
218 dto::Chain::Arbitrum => Chain::Arbitrum,
219 dto::Chain::Base => Chain::Base,
220 dto::Chain::Bsc => Chain::Bsc,
221 dto::Chain::Unichain => Chain::Unichain,
222 dto::Chain::Polygon => Chain::Polygon,
223 dto::Chain::Plasma => Chain::Plasma,
224 dto::Chain::Robinhood => Chain::Robinhood,
225 dto::Chain::Arc => Chain::Arc,
226 dto::Chain::Custom(name) => Chain::custom(name.as_str()).unwrap_or_else(|e| {
227 panic!(
228 "received custom chain '{name}' with no registered config: {e}; install it via \
229 the chain config file (TYCHO_CHAINS_CONFIG, default ./chains.yaml) or \
230 init_chain_registry before decoding wire data"
231 )
232 }),
233 }
234 }
235}
236
237impl From<dto::ChangeType> for ChangeType {
238 fn from(value: dto::ChangeType) -> Self {
239 match value {
240 dto::ChangeType::Update => ChangeType::Update,
241 dto::ChangeType::Creation => ChangeType::Creation,
242 dto::ChangeType::Deletion => ChangeType::Deletion,
243 dto::ChangeType::Unspecified => ChangeType::Update,
244 }
245 }
246}
247
248fn native_eth(chain: Chain) -> Token {
249 Token::new(
250 &Bytes::from_str("0x0000000000000000000000000000000000000000").unwrap(),
251 "ETH",
252 18,
253 0,
254 &[Some(2300)],
255 chain,
256 100,
257 )
258}
259
260fn native_bsc(chain: Chain) -> Token {
261 Token::new(
262 &Bytes::from_str("0x0000000000000000000000000000000000000000").unwrap(),
263 "BNB",
264 18,
265 0,
266 &[Some(2300)],
267 chain,
268 100,
269 )
270}
271
272fn wrapped_native_eth(chain: Chain, address: &str) -> Token {
273 Token::new(&Bytes::from_str(address).unwrap(), "WETH", 18, 0, &[Some(2300)], chain, 100)
274}
275
276fn native_pol(chain: Chain) -> Token {
277 Token::new(
278 &Bytes::from_str("0x0000000000000000000000000000000000000000").unwrap(),
279 "POL",
280 18,
281 0,
282 &[Some(2300)],
283 chain,
284 100,
285 )
286}
287
288fn native_xpl(chain: Chain) -> Token {
289 Token::new(
290 &Bytes::from_str("0x0000000000000000000000000000000000000000").unwrap(),
291 "XPL",
292 18,
293 0,
294 &[Some(2300)],
295 chain,
296 100,
297 )
298}
299
300fn native_arc_usdc(chain: Chain) -> Token {
301 Token::new(&Bytes::from([0u8; 20]), "USDC", 18, 0, &[Some(2300)], chain, 100)
302}
303
304fn try_resolve_custom<'a>(
307 id: &CustomChainId,
308 registry: &'a ChainConfigRegistry,
309) -> Result<&'a CustomChainConfig, ChainConfigError> {
310 registry
311 .get(id.as_str())
312 .ok_or_else(|| ChainConfigError::UnknownChain(id.as_str().to_owned()))
313}
314
315fn expect_registered<T>(result: Result<T, ChainConfigError>) -> T {
319 result.unwrap_or_else(|e| {
320 panic!(
321 "internal invariant violation resolving custom chain config: {e}; Chain::Custom is \
322 validated against the set-once chain registry at construction"
323 )
324 })
325}
326
327fn native_custom(chain: Chain, cfg: &CustomChainConfig) -> Token {
328 let addr = Bytes::from(cfg.native.address.as_bytes().to_vec());
329 Token::new(
330 &addr,
331 cfg.native.symbol.as_str(),
332 cfg.native.decimals as u32,
333 0,
334 &[Some(2300)],
335 chain,
336 100,
337 )
338}
339
340fn wrapped_native_bsc(chain: Chain, address: &str) -> Token {
341 Token::new(&Bytes::from_str(address).unwrap(), "WBNB", 18, 0, &[Some(2300)], chain, 100)
342}
343
344fn wrapped_native_pol(chain: Chain, address: &str) -> Token {
345 Token::new(&Bytes::from_str(address).unwrap(), "WMATIC", 18, 0, &[Some(2300)], chain, 100)
346}
347
348fn wrapped_native_xpl(chain: Chain, address: &str) -> Token {
349 Token::new(&Bytes::from_str(address).unwrap(), "WXPL", 18, 0, &[Some(2300)], chain, 100)
350}
351
352fn routable_arc_usdc(chain: Chain) -> Token {
353 Token::new(
354 &Bytes::from_str("0x3600000000000000000000000000000000000000").unwrap(),
355 "USDC",
356 6,
357 0,
358 &[Some(2300)],
359 chain,
360 100,
361 )
362}
363
364fn wrapped_native_custom(chain: Chain, cfg: &CustomChainConfig) -> Token {
365 let addr = Bytes::from(
366 cfg.wrapped_native
367 .address
368 .as_bytes()
369 .to_vec(),
370 );
371 Token::new(
372 &addr,
373 cfg.wrapped_native.symbol.as_str(),
374 cfg.wrapped_native.decimals as u32,
375 0,
376 &[Some(2300)],
377 chain,
378 100,
379 )
380}
381
382impl Chain {
383 pub fn id(&self) -> u64 {
387 expect_registered(self.try_id())
388 }
389
390 pub fn try_id(&self) -> Result<u64, ChainConfigError> {
393 Ok(match self {
394 Chain::Ethereum => 1,
395 Chain::ZkSync => 324,
396 Chain::Arbitrum => 42161,
397 Chain::Starknet => 0,
398 Chain::Base => 8453,
399 Chain::Bsc => 56,
400 Chain::Unichain => 130,
401 Chain::Polygon => 137,
402 Chain::Plasma => 9745,
403 Chain::Robinhood => 4663,
404 Chain::Arc => 5042,
405 Chain::Custom(id) => try_resolve_custom(id, chain_registry())?.chain_id,
406 })
407 }
408
409 pub fn default_tvl_threshold(&self, tier: TvlThresholdTier) -> f64 {
419 expect_registered(self.try_default_tvl_threshold(tier))
420 }
421
422 pub fn try_default_tvl_threshold(
425 &self,
426 tier: TvlThresholdTier,
427 ) -> Result<f64, ChainConfigError> {
428 Ok(match (self, tier) {
429 (
432 Chain::Ethereum |
433 Chain::Starknet |
434 Chain::ZkSync |
435 Chain::Arbitrum |
436 Chain::Base |
437 Chain::Unichain |
438 Chain::Robinhood,
439 TvlThresholdTier::Low,
440 ) => 10.0,
441 (
442 Chain::Ethereum |
443 Chain::Starknet |
444 Chain::ZkSync |
445 Chain::Arbitrum |
446 Chain::Base |
447 Chain::Unichain |
448 Chain::Robinhood,
449 TvlThresholdTier::Medium,
450 ) => 100.0,
451
452 (Chain::Polygon, TvlThresholdTier::Low) => 200_000.0,
454 (Chain::Polygon, TvlThresholdTier::Medium) => 2_000_000.0,
455
456 (Chain::Plasma, TvlThresholdTier::Low) => 200_000.0,
458 (Chain::Plasma, TvlThresholdTier::Medium) => 2_000_000.0,
459
460 (Chain::Bsc, TvlThresholdTier::Low) => 32.0,
462 (Chain::Bsc, TvlThresholdTier::Medium) => 320.0,
463
464 (Chain::Arc, TvlThresholdTier::Low) => 20_000.0,
466 (Chain::Arc, TvlThresholdTier::Medium) => 200_000.0,
467
468 (Chain::Custom(id), TvlThresholdTier::Low) => {
469 try_resolve_custom(id, chain_registry())?
470 .default_tvl_thresholds
471 .low
472 }
473 (Chain::Custom(id), TvlThresholdTier::Medium) => {
474 try_resolve_custom(id, chain_registry())?
475 .default_tvl_thresholds
476 .medium
477 }
478 })
479 }
480
481 pub fn native_token(&self) -> Token {
484 expect_registered(self.try_native_token())
485 }
486
487 pub fn try_native_token(&self) -> Result<Token, ChainConfigError> {
490 Ok(match self {
491 Chain::Ethereum => native_eth(Chain::Ethereum),
492 Chain::Starknet => native_eth(Chain::Starknet),
495 Chain::ZkSync => native_eth(Chain::ZkSync),
496 Chain::Arbitrum => native_eth(Chain::Arbitrum),
497 Chain::Base => native_eth(Chain::Base),
498 Chain::Bsc => native_bsc(Chain::Bsc),
499 Chain::Unichain => native_eth(Chain::Unichain),
500 Chain::Polygon => native_pol(Chain::Polygon),
501 Chain::Plasma => native_xpl(Chain::Plasma),
502 Chain::Robinhood => native_eth(Chain::Robinhood),
503 Chain::Arc => native_arc_usdc(Chain::Arc),
504 Chain::Custom(id) => native_custom(*self, try_resolve_custom(id, chain_registry())?),
505 })
506 }
507
508 pub fn wrapped_native_token(&self) -> Option<Token> {
511 expect_registered(self.try_wrapped_native_token())
512 }
513
514 pub fn try_wrapped_native_token(&self) -> Result<Option<Token>, ChainConfigError> {
517 Ok(self
518 .try_native_asset()?
519 .wrapper()
520 .cloned())
521 }
522
523 pub fn native_asset(&self) -> NativeAsset {
527 expect_registered(self.try_native_asset())
528 }
529
530 pub fn try_native_asset(&self) -> Result<NativeAsset, ChainConfigError> {
533 Ok(match self {
534 Chain::Ethereum => NativeAsset::Wrapper {
535 native: native_eth(Chain::Ethereum),
536 wrapper: wrapped_native_eth(
537 Chain::Ethereum,
538 "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2",
539 ),
540 },
541 Chain::Starknet => NativeAsset::NativeOnly { native: native_eth(Chain::Starknet) },
544 Chain::ZkSync => NativeAsset::Wrapper {
545 native: native_eth(Chain::ZkSync),
546 wrapper: wrapped_native_eth(
547 Chain::ZkSync,
548 "0x5AEa5775959fBC2557Cc8789bC1bf90A239D9a91",
549 ),
550 },
551 Chain::Arbitrum => NativeAsset::Wrapper {
552 native: native_eth(Chain::Arbitrum),
553 wrapper: wrapped_native_eth(
554 Chain::Arbitrum,
555 "0x82aF49447D8a07e3bd95BD0d56f35241523fBab1",
556 ),
557 },
558 Chain::Base => NativeAsset::Wrapper {
559 native: native_eth(Chain::Base),
560 wrapper: wrapped_native_eth(
561 Chain::Base,
562 "0x4200000000000000000000000000000000000006",
563 ),
564 },
565 Chain::Bsc => NativeAsset::Wrapper {
566 native: native_bsc(Chain::Bsc),
567 wrapper: wrapped_native_bsc(
568 Chain::Bsc,
569 "0xbb4CdB9CBd36B01bD1cBaEBF2De08d9173bc095c",
570 ),
571 },
572 Chain::Unichain => NativeAsset::Wrapper {
573 native: native_eth(Chain::Unichain),
574 wrapper: wrapped_native_eth(
575 Chain::Unichain,
576 "0x4200000000000000000000000000000000000006",
577 ),
578 },
579 Chain::Polygon => NativeAsset::Wrapper {
580 native: native_pol(Chain::Polygon),
581 wrapper: wrapped_native_pol(
582 Chain::Polygon,
583 "0x0d500B1d8E8eF31E21C99d1Db9A6444d3ADf1270",
584 ),
585 },
586 Chain::Plasma => NativeAsset::Wrapper {
587 native: native_xpl(Chain::Plasma),
588 wrapper: wrapped_native_xpl(
589 Chain::Plasma,
590 "0x6100E367285b01F48D07953803A2d8dCA5D19873",
591 ),
592 },
593 Chain::Robinhood => NativeAsset::Wrapper {
594 native: native_eth(Chain::Robinhood),
595 wrapper: wrapped_native_eth(
597 Chain::Robinhood,
598 "0x0Bd7D308f8E1639FAb988df18A8011f41EAcAD73",
599 ),
600 },
601 Chain::Arc => NativeAsset::SharedBalance {
602 native: native_arc_usdc(Chain::Arc),
605 routable: routable_arc_usdc(Chain::Arc),
606 },
607 Chain::Custom(id) => {
608 let config = try_resolve_custom(id, chain_registry())?;
609 let native = native_custom(*self, config);
610 let wrapper = wrapped_native_custom(*self, config);
611 if native.address == wrapper.address {
612 NativeAsset::NativeOnly { native }
613 } else {
614 NativeAsset::Wrapper { native, wrapper }
615 }
616 }
617 })
618 }
619
620 pub fn block_time_secs(&self) -> u64 {
623 expect_registered(self.try_block_time_secs())
624 }
625
626 pub fn try_block_time_secs(&self) -> Result<u64, ChainConfigError> {
629 Ok(match self {
630 Chain::Ethereum => 12,
631 Chain::Starknet => 2,
632 Chain::ZkSync => 3,
633 Chain::Arbitrum => 1,
634 Chain::Base => 2,
635 Chain::Bsc => 1,
636 Chain::Unichain => 1,
637 Chain::Polygon => 2,
638 Chain::Plasma => 1,
639 Chain::Robinhood => 1,
640 Chain::Arc => 1,
642 Chain::Custom(id) => try_resolve_custom(id, chain_registry())?.block_time_secs,
643 })
644 }
645}
646
647#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, Hash, Default)]
648pub struct ExtractorIdentity {
649 pub chain: Chain,
650 pub name: String,
651}
652
653impl ExtractorIdentity {
654 pub fn new(chain: Chain, name: &str) -> Self {
655 Self { chain, name: name.to_owned() }
656 }
657}
658
659impl std::fmt::Display for ExtractorIdentity {
660 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
661 write!(f, "{}:{}", self.chain, self.name)
662 }
663}
664
665impl From<ExtractorIdentity> for dto::ExtractorIdentity {
666 fn from(value: ExtractorIdentity) -> Self {
667 dto::ExtractorIdentity { chain: value.chain.into(), name: value.name }
668 }
669}
670
671impl From<dto::ExtractorIdentity> for ExtractorIdentity {
672 fn from(value: dto::ExtractorIdentity) -> Self {
673 Self { chain: value.chain.into(), name: value.name }
674 }
675}
676
677#[derive(Debug, PartialEq, Clone)]
678pub struct ExtractionState {
679 pub name: String,
680 pub chain: Chain,
681 pub attributes: serde_json::Value,
682 pub cursor: Vec<u8>,
683 pub block_hash: Bytes,
684}
685
686impl ExtractionState {
687 pub fn new(
688 name: String,
689 chain: Chain,
690 attributes: Option<serde_json::Value>,
691 cursor: &[u8],
692 block_hash: Bytes,
693 ) -> Self {
694 ExtractionState {
695 name,
696 chain,
697 attributes: attributes.unwrap_or_default(),
698 cursor: cursor.to_vec(),
699 block_hash,
700 }
701 }
702}
703
704#[derive(PartialEq, Debug, Clone, Default, Deserialize, Serialize)]
705pub enum ImplementationType {
706 #[default]
707 Vm,
708 Custom,
709}
710
711#[derive(PartialEq, Debug, Clone, Default, Deserialize, Serialize)]
712pub enum FinancialType {
713 #[default]
714 Swap,
715 Psm,
716 Debt,
717 Leverage,
718}
719
720#[derive(Debug, PartialEq, Clone, Default, Deserialize, Serialize)]
721pub struct ProtocolType {
722 pub name: String,
723 pub financial_type: FinancialType,
724 pub attribute_schema: Option<serde_json::Value>,
725 pub implementation: ImplementationType,
726}
727
728impl ProtocolType {
729 pub fn new(
730 name: String,
731 financial_type: FinancialType,
732 attribute_schema: Option<serde_json::Value>,
733 implementation: ImplementationType,
734 ) -> Self {
735 ProtocolType { name, financial_type, attribute_schema, implementation }
736 }
737}
738
739#[derive(Debug, PartialEq, Eq, Default, Copy, Clone, Deserialize, Serialize, DeepSizeOf)]
740pub enum ChangeType {
741 #[default]
742 Update,
743 Deletion,
744 Creation,
745}
746
747#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)]
748pub struct ContractId {
749 pub address: Address,
750 pub chain: Chain,
751}
752
753impl ContractId {
755 pub fn new(chain: Chain, address: Address) -> Self {
756 Self { address, chain }
757 }
758
759 pub fn address(&self) -> &Address {
760 &self.address
761 }
762}
763
764impl Display for ContractId {
765 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
766 write!(f, "{:?}: 0x{}", self.chain, hex::encode(&self.address))
767 }
768}
769
770#[derive(Debug, PartialEq, Clone, Default, Deserialize, Serialize)]
771pub struct PaginationParams {
772 pub page: i64,
773 pub page_size: i64,
774}
775
776impl PaginationParams {
777 pub fn new(page: i64, page_size: i64) -> Self {
778 Self { page, page_size }
779 }
780
781 pub fn offset(&self) -> i64 {
782 self.page * self.page_size
783 }
784}
785
786impl From<&dto::PaginationParams> for PaginationParams {
787 fn from(value: &dto::PaginationParams) -> Self {
788 PaginationParams { page: value.page, page_size: value.page_size }
789 }
790}
791
792#[derive(Error, Debug, PartialEq)]
793pub enum MergeError {
794 #[error("Can't merge {0} from differring idendities: Expected {1}, got {2}")]
795 IdMismatch(String, String, String),
796 #[error("Can't merge {0} from different blocks: 0x{1:x} != 0x{2:x}")]
797 BlockMismatch(String, Bytes, Bytes),
798 #[error("Can't merge {0} from the same transaction: 0x{1:x}")]
799 SameTransaction(String, Bytes),
800 #[error("Can't merge {0} with lower transaction index: {1} > {2}")]
801 TransactionOrderError(String, u64, u64),
802 #[error("Cannot merge: {0}")]
803 InvalidState(String),
804}
805
806#[cfg(test)]
810mod tests {
811 use arrayvec::ArrayString;
812
813 use super::{
814 chain_config::{
815 init_chain_registry, ChainAddress, ChainConfigError, ChainTokenConfig, TvlThresholds,
816 },
817 *,
818 };
819
820 fn test_config() -> CustomChainConfig {
821 CustomChainConfig::try_new(
822 "testchain",
823 9999,
824 5,
825 ChainTokenConfig::try_new("0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", "TST", 18)
826 .unwrap(),
827 ChainTokenConfig::try_new("0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", "WTST", 18)
828 .unwrap(),
829 TvlThresholds::new(50.0, 500.0),
830 )
831 .unwrap()
832 }
833
834 fn init_test_registry() {
835 init_chain_registry(ChainConfigRegistry::from_configs([test_config()]).unwrap())
836 .expect("chain registry already initialised; run tests under nextest");
837 }
838
839 #[test]
840 fn test_custom_chain_display() {
841 init_test_registry();
842 assert_eq!(
843 Chain::custom("testchain")
844 .unwrap()
845 .to_string(),
846 "testchain"
847 );
848 }
849
850 #[test]
851 fn test_from_str_custom_returns_err() {
852 assert!("custom".parse::<Chain>().is_err());
853 assert!("unknown".parse::<Chain>().is_err());
854 }
855
856 #[test]
857 fn test_custom_unregistered_returns_err() {
858 init_test_registry();
859 assert_eq!(Chain::custom("nope"), Err(ChainConfigError::UnknownChain("nope".to_owned())));
860 }
861
862 #[test]
863 fn test_from_dto_registered_custom_roundtrips() {
864 init_test_registry();
865 let dto_chain: dto::Chain = Chain::custom("testchain")
866 .unwrap()
867 .into();
868 let chain: Chain = dto_chain.into();
869 assert_eq!(chain.id(), 9999);
870 }
871
872 #[test]
873 #[should_panic(expected = "no registered config")]
874 fn test_from_dto_unregistered_custom_panics() {
875 let dto_chain = dto::Chain::Custom(ArrayString::from("nope").unwrap());
876 let _: Chain = dto_chain.into();
877 }
878
879 #[test]
880 fn test_try_accessors_ok_for_registered_custom() {
881 init_test_registry();
882 let chain = Chain::custom("testchain").unwrap();
883 assert_eq!(chain.try_id().unwrap(), 9999);
884 assert_eq!(chain.try_block_time_secs().unwrap(), 5);
885 assert_eq!(
886 chain
887 .try_default_tvl_threshold(TvlThresholdTier::Low)
888 .unwrap(),
889 50.0
890 );
891 assert_eq!(chain.try_native_token().unwrap().symbol, "TST");
892 assert_eq!(
893 chain
894 .try_wrapped_native_token()
895 .unwrap()
896 .expect("testchain should have a wrapper")
897 .symbol,
898 "WTST"
899 );
900 }
901
902 #[test]
903 fn test_try_accessors_err_for_unregistered_custom() {
904 let ghost: Chain = serde_json::from_str(r#"{"custom":"ghostchain"}"#).unwrap();
907 assert_eq!(ghost.try_id(), Err(ChainConfigError::UnknownChain("ghostchain".to_owned())));
908 assert!(ghost.try_native_token().is_err());
909 }
910
911 #[test]
912 fn test_chain_stays_small() {
913 assert!(
916 std::mem::size_of::<Chain>() <= 40,
917 "Chain is {} bytes",
918 std::mem::size_of::<Chain>()
919 );
920 }
921
922 #[test]
923 fn test_custom_chain_id() {
924 init_test_registry();
925 let chain = Chain::custom("testchain").unwrap();
926 assert_eq!(chain.id(), 9999);
927 }
928
929 #[test]
930 fn test_custom_chain_tvl_thresholds() {
931 init_test_registry();
932 let chain = Chain::custom("testchain").unwrap();
933 assert_eq!(chain.default_tvl_threshold(TvlThresholdTier::Low), 50.0);
934 assert_eq!(chain.default_tvl_threshold(TvlThresholdTier::Medium), 500.0);
935 }
936
937 #[test]
938 fn test_custom_chain_native_token() {
939 init_test_registry();
940 let chain = Chain::custom("testchain").unwrap();
941 let token = chain.native_token();
942 assert_eq!(token.symbol, "TST");
943 assert_eq!(token.decimals, 18);
944 assert_eq!(token.chain, chain);
945 assert_eq!(token.address, Bytes::from(vec![0xAA; 20]));
946 }
947
948 #[test]
949 fn test_custom_chain_wrapped_native_token() {
950 init_test_registry();
951 let chain = Chain::custom("testchain").unwrap();
952 let token = chain
953 .wrapped_native_token()
954 .expect("testchain should have a wrapper");
955 assert_eq!(token.symbol, "WTST");
956 assert_eq!(token.chain, chain);
957 assert_eq!(token.address, Bytes::from(vec![0xBB; 20]));
958 }
959
960 #[test]
961 fn test_custom_chain_with_same_native_and_wrapped_address_is_native_only() {
962 let address = "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa";
963 let config = CustomChainConfig::try_new(
964 "nativeonly",
965 9998,
966 5,
967 ChainTokenConfig::try_new(address, "TST", 18).unwrap(),
968 ChainTokenConfig::try_new(address, "WTST", 18).unwrap(),
969 TvlThresholds::new(50.0, 500.0),
970 )
971 .unwrap();
972 init_chain_registry(ChainConfigRegistry::from_configs([config]).unwrap())
973 .expect("chain registry already initialised; run tests under nextest");
974
975 let chain = Chain::custom("nativeonly").unwrap();
976 let asset = chain.native_asset();
977
978 assert!(asset.routable_token().is_none());
979 assert!(chain.wrapped_native_token().is_none());
980 assert_eq!(asset.representations().count(), 1);
981 }
982
983 #[test]
984 fn test_chain_address_new_rejects_oversized_input() {
985 assert_eq!(ChainAddress::new(&[0u8; 33]), Err(ChainConfigError::AddressTooLong(33)));
986 }
987
988 #[test]
989 fn test_robinhood_chain_id() {
990 assert_eq!(Chain::Robinhood.id(), 4663);
991 }
992
993 #[test]
994 fn test_robinhood_chain_display() {
995 assert_eq!(Chain::Robinhood.to_string(), "robinhood");
996 }
997
998 #[test]
999 fn test_robinhood_chain_from_str() {
1000 assert_eq!("robinhood".parse::<Chain>().unwrap(), Chain::Robinhood);
1001 }
1002
1003 #[test]
1004 fn test_robinhood_native_token() {
1005 let token = Chain::Robinhood.native_token();
1006 assert_eq!(token.symbol, "ETH");
1007 assert_eq!(token.chain, Chain::Robinhood);
1008 assert_eq!(
1009 token.address,
1010 Bytes::from_str("0x0000000000000000000000000000000000000000").unwrap()
1011 );
1012 }
1013
1014 #[test]
1015 fn test_robinhood_wrapped_native_token() {
1016 let token = Chain::Robinhood
1017 .wrapped_native_token()
1018 .expect("Robinhood should have a wrapper");
1019 assert_eq!(token.symbol, "WETH");
1020 assert_eq!(token.chain, Chain::Robinhood);
1021 assert_eq!(
1022 token.address,
1023 Bytes::from_str("0x0Bd7D308f8E1639FAb988df18A8011f41EAcAD73").unwrap()
1024 );
1025 }
1026
1027 #[test]
1028 fn test_robinhood_default_tvl_threshold() {
1029 assert_eq!(Chain::Robinhood.default_tvl_threshold(TvlThresholdTier::Low), 10.0);
1030 assert_eq!(Chain::Robinhood.default_tvl_threshold(TvlThresholdTier::Medium), 100.0);
1031 }
1032
1033 #[test]
1034 fn test_robinhood_block_time_secs() {
1035 assert_eq!(Chain::Robinhood.block_time_secs(), 1);
1036 }
1037
1038 #[test]
1039 fn test_arc_chain_identity_and_dto_round_trip() {
1040 assert_eq!(Chain::Arc.id(), 5042);
1041 assert_eq!(Chain::Arc.to_string(), "arc");
1042 assert_eq!("arc".parse::<Chain>().unwrap(), Chain::Arc);
1043
1044 let dto_chain: dto::Chain = Chain::Arc.into();
1045 assert_eq!(dto_chain, dto::Chain::Arc);
1046 assert_eq!(Chain::from(dto_chain), Chain::Arc);
1047 assert_eq!(serde_json::to_string(&Chain::Arc).unwrap(), r#""arc""#);
1048 assert_eq!(serde_json::from_str::<Chain>(r#""arc""#).unwrap(), Chain::Arc);
1049
1050 let native = Chain::Arc.native_token();
1051 assert_eq!(native.symbol, "USDC");
1052 assert_eq!(native.decimals, 18);
1053 assert_eq!(native.address, Bytes::from([0u8; 20]));
1054
1055 assert!(Chain::Arc
1056 .wrapped_native_token()
1057 .is_none());
1058 let native_asset = Chain::Arc.native_asset();
1059 let routable = native_asset
1060 .routable_token()
1061 .expect("Arc should expose its routable USDC representation");
1062 assert_eq!(routable.symbol, "USDC");
1063 assert_eq!(routable.decimals, 6);
1064 assert_eq!(
1065 routable.address,
1066 Bytes::from_str("0x3600000000000000000000000000000000000000").unwrap()
1067 );
1068
1069 assert_eq!(Chain::Arc.block_time_secs(), 1);
1070 assert_eq!(Chain::Arc.default_tvl_threshold(TvlThresholdTier::Low), 20_000.0);
1071 assert_eq!(Chain::Arc.default_tvl_threshold(TvlThresholdTier::Medium), 200_000.0);
1072 }
1073
1074 #[test]
1075 fn test_arc_native_asset_uses_shared_balance_without_wrapper() {
1076 let asset = Chain::Arc.native_asset();
1077
1078 assert_eq!(asset.native_token(), &Chain::Arc.native_token());
1079 assert!(matches!(asset, NativeAsset::SharedBalance { .. }));
1080 assert!(asset.wrapper().is_none());
1081
1082 let representations: Vec<_> = asset
1083 .representations()
1084 .map(|token| (token.address.clone(), token.decimals))
1085 .collect();
1086 assert_eq!(
1087 representations,
1088 vec![
1089 (Bytes::from([0u8; 20]), 18),
1090 (Bytes::from_str("0x3600000000000000000000000000000000000000").unwrap(), 6,),
1091 ]
1092 );
1093 }
1094
1095 #[test]
1096 fn test_existing_native_asset_relationships_remain_compatible() {
1097 let ethereum = Chain::Ethereum.native_asset();
1098 assert!(matches!(ethereum, NativeAsset::Wrapper { .. }));
1099 assert_eq!(ethereum.wrapper(), ethereum.routable_token());
1100 assert_eq!(
1101 ethereum.routable_token(),
1102 Chain::Ethereum
1103 .wrapped_native_token()
1104 .as_ref()
1105 );
1106
1107 let starknet = Chain::Starknet.native_asset();
1108 assert!(matches!(starknet, NativeAsset::NativeOnly { .. }));
1109 assert!(starknet.wrapper().is_none());
1110 assert!(starknet.routable_token().is_none());
1111 assert!(Chain::Starknet
1112 .wrapped_native_token()
1113 .is_none());
1114 assert_eq!(starknet.representations().count(), 1);
1115 }
1116
1117 #[test]
1118 fn test_chain_address_as_bytes_returns_active_slice() {
1119 let addr = ChainAddress::new(&[0xAA; 20]).unwrap();
1120 assert_eq!(addr.as_bytes(), &[0xAA; 20]);
1121 assert_eq!(addr.as_bytes().len(), 20);
1122 }
1123}