diff --git a/crates/contract/src/snapshots/mpc_contract__foreign_chains_metadata__tests__foreign_chains_metadata_borsh_schema__should_not_change.snap b/crates/contract/src/snapshots/mpc_contract__foreign_chains_metadata__tests__foreign_chains_metadata_borsh_schema__should_not_change.snap index 48fa313ae..91d672ffd 100644 --- a/crates/contract/src/snapshots/mpc_contract__foreign_chains_metadata__tests__foreign_chains_metadata_borsh_schema__should_not_change.snap +++ b/crates/contract/src/snapshots/mpc_contract__foreign_chains_metadata__tests__foreign_chains_metadata_borsh_schema__should_not_change.snap @@ -95,6 +95,16 @@ BorshSchemaContainer { "Sui", "ForeignChain__Sui", ), + ( + 13, + "Avalanche", + "ForeignChain__Avalanche", + ), + ( + 14, + "Adi", + "ForeignChain__Adi", + ), ], }, "ForeignChainRpcWhitelist": Struct { @@ -114,12 +124,18 @@ BorshSchemaContainer { "ForeignChain__Abstract": Struct { fields: Empty, }, + "ForeignChain__Adi": Struct { + fields: Empty, + }, "ForeignChain__Aptos": Struct { fields: Empty, }, "ForeignChain__Arbitrum": Struct { fields: Empty, }, + "ForeignChain__Avalanche": Struct { + fields: Empty, + }, "ForeignChain__Base": Struct { fields: Empty, }, diff --git a/crates/contract/tests/sandbox/common.rs b/crates/contract/tests/sandbox/common.rs index 582d76386..71fafb476 100644 --- a/crates/contract/tests/sandbox/common.rs +++ b/crates/contract/tests/sandbox/common.rs @@ -897,6 +897,22 @@ pub fn polygon_evm_request() -> ForeignChainRpcRequest { }) } +pub fn avalanche_evm_request() -> ForeignChainRpcRequest { + ForeignChainRpcRequest::Avalanche(EvmRpcRequest { + tx_id: EvmTxId([0xbb; 32]), + extractors: vec![EvmExtractor::BlockHash], + finality: EvmFinality::Finalized, + }) +} + +pub fn adi_evm_request() -> ForeignChainRpcRequest { + ForeignChainRpcRequest::Adi(EvmRpcRequest { + tx_id: EvmTxId([0xbb; 32]), + extractors: vec![EvmExtractor::BlockHash], + finality: EvmFinality::Finalized, + }) +} + pub fn ton_request() -> ForeignChainRpcRequest { ForeignChainRpcRequest::Ton(TonRpcRequest { tx_id: TonTxId([0xbb; 32]), diff --git a/crates/contract/tests/sandbox/foreign_chain_request.rs b/crates/contract/tests/sandbox/foreign_chain_request.rs index cf73ac816..164d49ec3 100644 --- a/crates/contract/tests/sandbox/foreign_chain_request.rs +++ b/crates/contract/tests/sandbox/foreign_chain_request.rs @@ -1,8 +1,9 @@ #![allow(non_snake_case)] use crate::sandbox::common::{ - SandboxTestSetup, abstract_evm_request, aptos_extracted_values, aptos_request, - arbitrum_evm_request, await_pending_foreign_tx_request_observed_on_contract, base_evm_request, + SandboxTestSetup, abstract_evm_request, adi_evm_request, aptos_extracted_values, aptos_request, + arbitrum_evm_request, avalanche_evm_request, + await_pending_foreign_tx_request_observed_on_contract, base_evm_request, bitcoin_extracted_values, bitcoin_request, bnb_evm_request, bogus_ton_log_extracted_value, ethereum_evm_request, evm_block_hash_extracted_values, hyper_evm_request, polygon_evm_request, register_foreign_chain_configuration, sign_foreign_tx_response, starknet_extracted_values, @@ -32,6 +33,8 @@ const SIGNATURE_TIMEOUT_BLOCKS: u64 = 200; #[case::ton(ton_request(), bogus_ton_log_extracted_value())] #[case::aptos(aptos_request(), aptos_extracted_values())] #[case::sui(sui_request(), sui_extracted_values())] +#[case::avalanche(avalanche_evm_request(), evm_block_hash_extracted_values())] +#[case::adi(adi_evm_request(), evm_block_hash_extracted_values())] #[tokio::test] async fn verify_foreign_transaction__should_succeed( #[case] rpc_request: ForeignChainRpcRequest, @@ -321,6 +324,8 @@ async fn verify_foreign_transaction__should_succeed_when_response_matches_expect #[case::ton(ton_request())] #[case::aptos(aptos_request())] #[case::sui(sui_request())] +#[case::avalanche(avalanche_evm_request())] +#[case::adi(adi_evm_request())] #[tokio::test] async fn verify_foreign_transaction__should_reject_without_policy( #[case] rpc_request: ForeignChainRpcRequest, @@ -365,6 +370,8 @@ async fn verify_foreign_transaction__should_reject_without_policy( #[case::ton(ton_request())] #[case::aptos(aptos_request())] #[case::sui(sui_request())] +#[case::avalanche(avalanche_evm_request())] +#[case::adi(adi_evm_request())] #[tokio::test] async fn verify_foreign_transaction__should_timeout_without_response( #[case] rpc_request: ForeignChainRpcRequest, diff --git a/crates/contract/tests/snapshots/abi__abi_has_not_changed.snap b/crates/contract/tests/snapshots/abi__abi_has_not_changed.snap index 2fca2a8fa..3dc51495d 100644 --- a/crates/contract/tests/snapshots/abi__abi_has_not_changed.snap +++ b/crates/contract/tests/snapshots/abi__abi_has_not_changed.snap @@ -256,6 +256,16 @@ expression: abi 12, "Sui", "ForeignChain__Sui" + ], + [ + 13, + "Avalanche", + "ForeignChain__Avalanche" + ], + [ + 14, + "Adi", + "ForeignChain__Adi" ] ] } @@ -263,12 +273,18 @@ expression: abi "ForeignChain__Abstract": { "Struct": null }, + "ForeignChain__Adi": { + "Struct": null + }, "ForeignChain__Aptos": { "Struct": null }, "ForeignChain__Arbitrum": { "Struct": null }, + "ForeignChain__Avalanche": { + "Struct": null + }, "ForeignChain__Base": { "Struct": null }, @@ -3089,6 +3105,16 @@ expression: abi 12, "Sui", "ForeignChain__Sui" + ], + [ + 13, + "Avalanche", + "ForeignChain__Avalanche" + ], + [ + 14, + "Adi", + "ForeignChain__Adi" ] ] } @@ -3096,12 +3122,18 @@ expression: abi "ForeignChain__Abstract": { "Struct": null }, + "ForeignChain__Adi": { + "Struct": null + }, "ForeignChain__Aptos": { "Struct": null }, "ForeignChain__Arbitrum": { "Struct": null }, + "ForeignChain__Avalanche": { + "Struct": null + }, "ForeignChain__Base": { "Struct": null }, @@ -4067,7 +4099,9 @@ expression: abi "HyperEvm", "Ton", "Aptos", - "Sui" + "Sui", + "Avalanche", + "Adi" ] }, "ForeignChainConfiguration": { @@ -4234,6 +4268,30 @@ expression: abi } }, "additionalProperties": false + }, + { + "type": "object", + "required": [ + "Avalanche" + ], + "properties": { + "Avalanche": { + "$ref": "#/definitions/EvmRpcRequest" + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "Adi" + ], + "properties": { + "Adi": { + "$ref": "#/definitions/EvmRpcRequest" + } + }, + "additionalProperties": false } ] }, diff --git a/crates/e2e-tests/tests/foreign_chain_tx_validation.rs b/crates/e2e-tests/tests/foreign_chain_tx_validation.rs index 31a82d681..2f6318a6d 100644 --- a/crates/e2e-tests/tests/foreign_chain_tx_validation.rs +++ b/crates/e2e-tests/tests/foreign_chain_tx_validation.rs @@ -54,6 +54,8 @@ struct MockServerUrls { base: String, arbitrum: String, hyper_evm: String, + avalanche: String, + adi: String, polygon: Vec, } @@ -114,6 +116,8 @@ fn build_foreign_chains_config(urls: &MockServerUrls) -> ForeignChainsConfig { )), arbitrum: Some(mock_chain(&urls.arbitrum, Default::default())), hyper_evm: Some(mock_chain(&urls.hyper_evm, Default::default())), + avalanche: Some(mock_chain(&urls.avalanche, Default::default())), + adi: Some(mock_chain(&urls.adi, Default::default())), polygon: Some(mock_chain_with_providers( common::build_providers_from_urls(&urls.polygon, "polygon"), )), @@ -129,6 +133,8 @@ async fn setup_foreign_tx_cluster() -> anyhow::Result { let base_server = MockServer::start(); let arbitrum_server = MockServer::start(); let hyper_evm_server = MockServer::start(); + let avalanche_server = MockServer::start(); + let adi_server = MockServer::start(); let bitcoin_mock_id = setup_bitcoin_mock( &bitcoin_server, @@ -154,6 +160,8 @@ async fn setup_foreign_tx_cluster() -> anyhow::Result { setup_starknet_mock(&starknet_server, MockAuthExpectation::None); setup_evm_mock(&arbitrum_server, MockAuthExpectation::None); setup_evm_mock(&hyper_evm_server, MockAuthExpectation::None); + setup_evm_mock(&avalanche_server, MockAuthExpectation::None); + setup_evm_mock(&adi_server, MockAuthExpectation::None); // Polygon is configured with three RPC providers so the test can assert // that `FanOut` queries every one of them. @@ -175,6 +183,8 @@ async fn setup_foreign_tx_cluster() -> anyhow::Result { base: base_server.url("/"), arbitrum: arbitrum_server.url("/"), hyper_evm: hyper_evm_server.url("/"), + avalanche: avalanche_server.url("/"), + adi: adi_server.url("/"), polygon: polygon_mocks.iter().map(|m| m.server.url("/")).collect(), }; @@ -186,6 +196,8 @@ async fn setup_foreign_tx_cluster() -> anyhow::Result { starknet_server, arbitrum_server, hyper_evm_server, + avalanche_server, + adi_server, ]; let fc_config = build_foreign_chains_config(&urls); @@ -198,6 +210,8 @@ async fn setup_foreign_tx_cluster() -> anyhow::Result { ForeignChain::Base, ForeignChain::Arbitrum, ForeignChain::HyperEvm, + ForeignChain::Avalanche, + ForeignChain::Adi, ForeignChain::Polygon, ] .into_iter() @@ -487,6 +501,44 @@ async fn verify_hyper_evm(env: &ForeignTxTestEnv) -> anyhow::Result<()> { verify_foreign_tx_response(&outcome) } +async fn verify_avalanche(env: &ForeignTxTestEnv) -> anyhow::Result<()> { + let request = VerifyForeignTransactionRequestArgs { + request: ForeignChainRpcRequest::Avalanche(EvmRpcRequest { + tx_id: EvmTxId([0xbb; 32]), + extractors: vec![EvmExtractor::BlockHash, EvmExtractor::Log { log_index: 0 }], + finality: EvmFinality::Finalized, + }), + domain_id: env.foreign_tx_domain_id, + payload_version: ForeignTxPayloadVersion::V1, + expected_payload_hash: None, + }; + let outcome = env + .cluster + .send_verify_foreign_transaction(&request) + .await + .context("verify_foreign_transaction (Avalanche) failed")?; + verify_foreign_tx_response(&outcome) +} + +async fn verify_adi(env: &ForeignTxTestEnv) -> anyhow::Result<()> { + let request = VerifyForeignTransactionRequestArgs { + request: ForeignChainRpcRequest::Adi(EvmRpcRequest { + tx_id: EvmTxId([0xbb; 32]), + extractors: vec![EvmExtractor::BlockHash, EvmExtractor::Log { log_index: 0 }], + finality: EvmFinality::Finalized, + }), + domain_id: env.foreign_tx_domain_id, + payload_version: ForeignTxPayloadVersion::V1, + expected_payload_hash: None, + }; + let outcome = env + .cluster + .send_verify_foreign_transaction(&request) + .await + .context("verify_foreign_transaction (ADI) failed")?; + verify_foreign_tx_response(&outcome) +} + /// A successful verification implies the credentialed mock answered, /// so this is a backstop against the mock setup being loosened to answer unauthenticated requests. fn assert_authenticated_provider_was_queried(mock: &MockServerExt, provider: &str) { @@ -541,7 +593,7 @@ async fn verify_polygon(env: &ForeignTxTestEnv) -> anyhow::Result<()> { #[expect(non_snake_case)] async fn verify_foreign_transaction__should_sign_all_supported_chains() { // Given — 2-node cluster with Bitcoin, Abstract, BNB, Base, Starknet, - // Arbitrum, HyperEVM, and Polygon configured + // Arbitrum, HyperEVM, Avalanche, ADI, and Polygon configured let env = setup_foreign_tx_cluster() .await .expect("setup_foreign_tx_cluster failed"); @@ -570,6 +622,10 @@ async fn verify_foreign_transaction__should_sign_all_supported_chains() { verify_hyper_evm(&env) .await .expect("hyper_evm verification failed"); + verify_avalanche(&env) + .await + .expect("avalanche verification failed"); + verify_adi(&env).await.expect("adi verification failed"); verify_polygon(&env) .await .expect("polygon verification failed"); diff --git a/crates/foreign-chain-health-check/src/golden.rs b/crates/foreign-chain-health-check/src/golden.rs index f87696449..95f127bfa 100644 --- a/crates/foreign-chain-health-check/src/golden.rs +++ b/crates/foreign-chain-health-check/src/golden.rs @@ -37,6 +37,8 @@ pub struct GoldenSet { pub arbitrum: Option, pub polygon: Option, pub hyper_evm: Option, + pub avalanche: Option, + pub adi: Option, pub abstract_chain: Option, pub bitcoin: Option, pub starknet: Option, @@ -72,6 +74,14 @@ const MAINNET: GoldenSet = GoldenSet { tx: "4d94e2c9c33c533f125bd28a788e80ee24c108356e8fa8a7878f642cf94dcf4a", block_hash: "657b2ee81add87e3f654840425baca06a06d5876f6d2d873197e70f00f6762e6", }), + avalanche: Some(BlockHashVector { + tx: "51f5b652c9917189b64a5abb5e1814d3bd0a58dbe433f3f7a58e9b0d20f40bb5", + block_hash: "ce5c4ceb1b1c14ba8a0d58f23545106934278390446466810991245a9cff2a43", + }), + adi: Some(BlockHashVector { + tx: "df89849ce8e1b4cf390560395198a10f1bd0498822c6880346a8ce28869ec8e5", + block_hash: "e49e64cb14a417a1356929043dc87559c57afb540492bc62c8c0d8270902f5c2", + }), abstract_chain: Some(BlockHashVector { tx: "4572b72d765f07712cf571993fd805888ede9cd05107f65338defee02f7ea755", block_hash: "3bb255d468a552a75fc3f4916623b207ceb2d3074dfa14442ac03f0f73423708", @@ -100,6 +110,8 @@ const TESTNET: GoldenSet = GoldenSet { arbitrum: None, polygon: None, hyper_evm: None, + avalanche: None, + adi: None, abstract_chain: Some(BlockHashVector { tx: "497fc5f5b5d81d6bc15cccc6d4d8be8ef6ad19376233b944a60dc435593f7234", block_hash: "4c93dd4a8f347e6480b0a44f8c2b7eecdfb31d711e8d542fd60112ea5d98fb02", @@ -196,6 +208,8 @@ mod tests { set.arbitrum, set.polygon, set.hyper_evm, + set.avalanche, + set.adi, set.abstract_chain, set.bitcoin, ] diff --git a/crates/foreign-chain-health-check/src/lib.rs b/crates/foreign-chain-health-check/src/lib.rs index a0abecbe3..39a0f30b9 100644 --- a/crates/foreign-chain-health-check/src/lib.rs +++ b/crates/foreign-chain-health-check/src/lib.rs @@ -15,7 +15,9 @@ use std::future::Future; use std::time::Duration; use foreign_chain_inspector::abstract_chain::inspector::Abstract; +use foreign_chain_inspector::adi::inspector::Adi; use foreign_chain_inspector::arbitrum::inspector::Arbitrum; +use foreign_chain_inspector::avalanche::inspector::Avalanche; use foreign_chain_inspector::base::inspector::Base; use foreign_chain_inspector::bnb::inspector::Bnb; use foreign_chain_inspector::evm::inspector::EvmChain; @@ -73,6 +75,16 @@ pub async fn check_all_providers( } else { mark_not_configured("hyper_evm", &mut out); } + if let Some(cfg) = &fc.avalanche { + run_evm::("avalanche", cfg, golden.avalanche, network, &mut out).await; + } else { + mark_not_configured("avalanche", &mut out); + } + if let Some(cfg) = &fc.adi { + run_evm::("adi", cfg, golden.adi, network, &mut out).await; + } else { + mark_not_configured("adi", &mut out); + } if let Some(cfg) = &fc.abstract_chain { run_evm::("abstract", cfg, golden.abstract_chain, network, &mut out).await; } else { @@ -413,6 +425,8 @@ mod tests { "arbitrum", "polygon", "hyper_evm", + "avalanche", + "adi", "abstract", "bitcoin", "starknet", diff --git a/crates/foreign-chain-health-check/src/probe.rs b/crates/foreign-chain-health-check/src/probe.rs index 6987c00a3..bada5de3a 100644 --- a/crates/foreign-chain-health-check/src/probe.rs +++ b/crates/foreign-chain-health-check/src/probe.rs @@ -5,7 +5,9 @@ use std::collections::BTreeMap; use std::time::Duration; use foreign_chain_inspector::abstract_chain::inspector::Abstract; +use foreign_chain_inspector::adi::inspector::Adi; use foreign_chain_inspector::arbitrum::inspector::Arbitrum; +use foreign_chain_inspector::avalanche::inspector::Avalanche; use foreign_chain_inspector::base::inspector::Base; use foreign_chain_inspector::bitcoin::inspector::BitcoinInspector; use foreign_chain_inspector::bnb::inspector::Bnb; @@ -103,7 +105,9 @@ pub async fn probe_all_providers(config: &ForeignChainsConfig) -> ProbeReport { .await } ForeignChain::Abstract => probe_evm::(chain, chain_config).await, + ForeignChain::Adi => probe_evm::(chain, chain_config).await, ForeignChain::Arbitrum => probe_evm::(chain, chain_config).await, + ForeignChain::Avalanche => probe_evm::(chain, chain_config).await, ForeignChain::Base => probe_evm::(chain, chain_config).await, ForeignChain::Bnb => probe_evm::(chain, chain_config).await, ForeignChain::HyperEvm => probe_evm::(chain, chain_config).await, @@ -268,15 +272,23 @@ mod tests { } /// Every EVM chain the probe covers, with its mainnet chain id. - const EVM_MAINNETS: [EvmMainnet; 6] = [ + const EVM_MAINNETS: [EvmMainnet; 8] = [ EvmMainnet { chain: ForeignChain::Abstract, chain_id: 2741, }, + EvmMainnet { + chain: ForeignChain::Adi, + chain_id: 36900, + }, EvmMainnet { chain: ForeignChain::Arbitrum, chain_id: 42161, }, + EvmMainnet { + chain: ForeignChain::Avalanche, + chain_id: 43114, + }, EvmMainnet { chain: ForeignChain::Base, chain_id: 8453, @@ -362,7 +374,9 @@ mod tests { ) { let slot = match chain { ForeignChain::Abstract => &mut chains.abstract_chain, + ForeignChain::Adi => &mut chains.adi, ForeignChain::Arbitrum => &mut chains.arbitrum, + ForeignChain::Avalanche => &mut chains.avalanche, ForeignChain::Base => &mut chains.base, ForeignChain::Bnb => &mut chains.bnb, ForeignChain::HyperEvm => &mut chains.hyper_evm, diff --git a/crates/foreign-chain-inspector/src/adi.rs b/crates/foreign-chain-inspector/src/adi.rs new file mode 100644 index 000000000..ed7e4beff --- /dev/null +++ b/crates/foreign-chain-inspector/src/adi.rs @@ -0,0 +1,6 @@ +pub use ethereum_types; + +pub mod inspector; + +mpc_primitives::define_hash!(AdiBlockHash, 32); +mpc_primitives::define_hash!(AdiTransactionHash, 32); diff --git a/crates/foreign-chain-inspector/src/adi/inspector.rs b/crates/foreign-chain-inspector/src/adi/inspector.rs new file mode 100644 index 000000000..381df3a1d --- /dev/null +++ b/crates/foreign-chain-inspector/src/adi/inspector.rs @@ -0,0 +1,16 @@ +use crate::{ + adi::{AdiBlockHash, AdiTransactionHash}, + evm::inspector::EvmChain, +}; + +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct Adi; + +impl EvmChain for Adi { + type BlockHash = AdiBlockHash; + type TransactionHash = AdiTransactionHash; +} + +pub type AdiInspector = crate::evm::inspector::EvmInspector; +pub type AdiExtractedValue = crate::evm::inspector::EvmExtractedValue; +pub type AdiExtractor = crate::evm::inspector::EvmExtractor; diff --git a/crates/foreign-chain-inspector/src/avalanche.rs b/crates/foreign-chain-inspector/src/avalanche.rs new file mode 100644 index 000000000..694b5c8c8 --- /dev/null +++ b/crates/foreign-chain-inspector/src/avalanche.rs @@ -0,0 +1,6 @@ +pub use ethereum_types; + +pub mod inspector; + +mpc_primitives::define_hash!(AvalancheBlockHash, 32); +mpc_primitives::define_hash!(AvalancheTransactionHash, 32); diff --git a/crates/foreign-chain-inspector/src/avalanche/inspector.rs b/crates/foreign-chain-inspector/src/avalanche/inspector.rs new file mode 100644 index 000000000..10fb10d43 --- /dev/null +++ b/crates/foreign-chain-inspector/src/avalanche/inspector.rs @@ -0,0 +1,16 @@ +use crate::{ + avalanche::{AvalancheBlockHash, AvalancheTransactionHash}, + evm::inspector::EvmChain, +}; + +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct Avalanche; + +impl EvmChain for Avalanche { + type BlockHash = AvalancheBlockHash; + type TransactionHash = AvalancheTransactionHash; +} + +pub type AvalancheInspector = crate::evm::inspector::EvmInspector; +pub type AvalancheExtractedValue = crate::evm::inspector::EvmExtractedValue; +pub type AvalancheExtractor = crate::evm::inspector::EvmExtractor; diff --git a/crates/foreign-chain-inspector/src/lib.rs b/crates/foreign-chain-inspector/src/lib.rs index 56cb04121..95db979c0 100644 --- a/crates/foreign-chain-inspector/src/lib.rs +++ b/crates/foreign-chain-inspector/src/lib.rs @@ -16,8 +16,10 @@ use thiserror::Error; pub use jsonrpsee::http_client; pub mod abstract_chain; +pub mod adi; pub mod aptos; pub mod arbitrum; +pub mod avalanche; pub mod base; pub mod bitcoin; pub mod bnb; diff --git a/crates/foreign-chain-inspector/tests/adi_rpc_manual.rs b/crates/foreign-chain-inspector/tests/adi_rpc_manual.rs new file mode 100644 index 000000000..734cbab81 --- /dev/null +++ b/crates/foreign-chain-inspector/tests/adi_rpc_manual.rs @@ -0,0 +1,84 @@ +use assert_matches::assert_matches; +use foreign_chain_inspector::{ + EthereumFinality, ForeignChainInspector, NetworkFingerprintInspector, RpcAuthentication, + adi::{ + AdiBlockHash, AdiTransactionHash, + inspector::{AdiExtractedValue, AdiExtractor, AdiInspector}, + }, +}; + +const ADI_RPC_URL: &str = "https://rpc.adifoundation.ai"; + +#[tokio::test] +#[ignore = "manual test to sanity check against live ADI Chain RPC provider"] +async fn inspector_extracts_block_hash_against_live_rpc_provider() { + // given + let threshold = EthereumFinality::Finalized; + + // Example transaction on ADI Chain (block 0x186a0) with 17 logs; + // https://explorer.adifoundation.ai/tx/0xdf89849ce8e1b4cf390560395198a10f1bd0498822c6880346a8ce28869ec8e5 + let transaction_id: AdiTransactionHash = + "df89849ce8e1b4cf390560395198a10f1bd0498822c6880346a8ce28869ec8e5" + .parse() + .unwrap(); + let expected_block_hash: AdiBlockHash = + "e49e64cb14a417a1356929043dc87559c57afb540492bc62c8c0d8270902f5c2" + .parse() + .unwrap(); + + let http_client = foreign_chain_inspector::build_http_client( + ADI_RPC_URL.to_string(), + RpcAuthentication::KeyInUrl, + ) + .unwrap(); + let inspector = AdiInspector::new(http_client); + + // when + let extracted_values = inspector + .extract( + transaction_id, + threshold, + vec![ + AdiExtractor::BlockHash, + AdiExtractor::Log { log_index: 0 }, + AdiExtractor::Log { log_index: 1 }, + AdiExtractor::Log { log_index: 2 }, + ], + ) + .await + .expect("extract should succeed"); + + // then + assert_eq!(extracted_values.len(), 4); + assert_eq!( + extracted_values[0], + AdiExtractedValue::BlockHash(expected_block_hash) + ); + assert_matches!(extracted_values[1], AdiExtractedValue::Log(_)); + assert_matches!(extracted_values[2], AdiExtractedValue::Log(_)); + assert_matches!(extracted_values[3], AdiExtractedValue::Log(_)); +} + +/// ADI Chain mainnet's chain id, as shipped in `expected_network_fingerprint`. +const EXPECTED_NETWORK_FINGERPRINT: &str = "36900"; + +#[tokio::test] +#[ignore = "manual test to sanity check against live ADI Chain RPC provider"] +async fn network_fingerprint_matches_the_shipped_config_value_against_live_rpc_provider() { + // given + let http_client = foreign_chain_inspector::build_http_client( + ADI_RPC_URL.to_string(), + RpcAuthentication::KeyInUrl, + ) + .unwrap(); + let inspector = AdiInspector::new(http_client); + + // when + let fingerprint = inspector + .network_fingerprint() + .await + .expect("network_fingerprint should succeed"); + + // then + assert_eq!(fingerprint.to_string(), EXPECTED_NETWORK_FINGERPRINT); +} diff --git a/crates/foreign-chain-inspector/tests/avalanche_rpc_manual.rs b/crates/foreign-chain-inspector/tests/avalanche_rpc_manual.rs new file mode 100644 index 000000000..80f8ac769 --- /dev/null +++ b/crates/foreign-chain-inspector/tests/avalanche_rpc_manual.rs @@ -0,0 +1,80 @@ +use assert_matches::assert_matches; +use foreign_chain_inspector::{ + EthereumFinality, ForeignChainInspector, NetworkFingerprintInspector, RpcAuthentication, + avalanche::{ + AvalancheBlockHash, AvalancheTransactionHash, + inspector::{AvalancheExtractedValue, AvalancheExtractor, AvalancheInspector}, + }, +}; + +const AVALANCHE_RPC_URL: &str = "https://api.avax.network/ext/bc/C/rpc"; + +#[tokio::test] +#[ignore = "manual test to sanity check against live Avalanche C-Chain RPC provider"] +async fn inspector_extracts_block_hash_against_live_rpc_provider() { + // given + let threshold = EthereumFinality::Finalized; + + // Example transaction on Avalanche C-Chain (block 0x989680) with 1 log; + // https://snowtrace.io/tx/0x51f5b652c9917189b64a5abb5e1814d3bd0a58dbe433f3f7a58e9b0d20f40bb5 + let transaction_id: AvalancheTransactionHash = + "51f5b652c9917189b64a5abb5e1814d3bd0a58dbe433f3f7a58e9b0d20f40bb5" + .parse() + .unwrap(); + let expected_block_hash: AvalancheBlockHash = + "ce5c4ceb1b1c14ba8a0d58f23545106934278390446466810991245a9cff2a43" + .parse() + .unwrap(); + + let http_client = foreign_chain_inspector::build_http_client( + AVALANCHE_RPC_URL.to_string(), + RpcAuthentication::KeyInUrl, + ) + .unwrap(); + let inspector = AvalancheInspector::new(http_client); + + // when + let extracted_values = inspector + .extract( + transaction_id, + threshold, + vec![ + AvalancheExtractor::BlockHash, + AvalancheExtractor::Log { log_index: 0 }, + ], + ) + .await + .expect("extract should succeed"); + + // then + assert_eq!(extracted_values.len(), 2); + assert_eq!( + extracted_values[0], + AvalancheExtractedValue::BlockHash(expected_block_hash) + ); + assert_matches!(extracted_values[1], AvalancheExtractedValue::Log(_)); +} + +/// Avalanche C-Chain mainnet's chain id, as shipped in `expected_network_fingerprint`. +const EXPECTED_NETWORK_FINGERPRINT: &str = "43114"; + +#[tokio::test] +#[ignore = "manual test to sanity check against live Avalanche C-Chain RPC provider"] +async fn network_fingerprint_matches_the_shipped_config_value_against_live_rpc_provider() { + // given + let http_client = foreign_chain_inspector::build_http_client( + AVALANCHE_RPC_URL.to_string(), + RpcAuthentication::KeyInUrl, + ) + .unwrap(); + let inspector = AvalancheInspector::new(http_client); + + // when + let fingerprint = inspector + .network_fingerprint() + .await + .expect("network_fingerprint should succeed"); + + // then + assert_eq!(fingerprint.to_string(), EXPECTED_NETWORK_FINGERPRINT); +} diff --git a/crates/foreign-chain-inspector/tests/evm_inspector.rs b/crates/foreign-chain-inspector/tests/evm_inspector.rs index b4f71b228..5d8f5afa9 100644 --- a/crates/foreign-chain-inspector/tests/evm_inspector.rs +++ b/crates/foreign-chain-inspector/tests/evm_inspector.rs @@ -720,6 +720,11 @@ evm_inspector_tests!( foreign_chain_inspector::polygon::inspector::Polygon, polygon ); +evm_inspector_tests!( + foreign_chain_inspector::avalanche::inspector::Avalanche, + avalanche +); +evm_inspector_tests!(foreign_chain_inspector::adi::inspector::Adi, adi); // Base mainnet, standing in for every EVM chain: the fingerprint call has no chain-specific parts. const CHAIN_ID_8453: &str = "0x2105"; diff --git a/crates/near-mpc-contract-interface/src/types/foreign_chain.rs b/crates/near-mpc-contract-interface/src/types/foreign_chain.rs index aeaba3f3d..665e81c66 100644 --- a/crates/near-mpc-contract-interface/src/types/foreign_chain.rs +++ b/crates/near-mpc-contract-interface/src/types/foreign_chain.rs @@ -186,6 +186,8 @@ pub enum ForeignChainRpcRequest { Ton(TonRpcRequest), Aptos(AptosRpcRequest), Sui(SuiRpcRequest), + Avalanche(EvmRpcRequest), + Adi(EvmRpcRequest), } impl ForeignChainRpcRequest { @@ -204,6 +206,8 @@ impl ForeignChainRpcRequest { Self::Ton(_) => ForeignChain::Ton, Self::Aptos(_) => ForeignChain::Aptos, Self::Sui(_) => ForeignChain::Sui, + Self::Avalanche(_) => ForeignChain::Avalanche, + Self::Adi(_) => ForeignChain::Adi, } } } @@ -1276,6 +1280,8 @@ pub enum ForeignChain { Ton, Aptos, Sui, + Avalanche, + Adi, } #[derive( @@ -2015,6 +2021,22 @@ mod tests { }), ForeignChain::Sui, )] + #[case::avalanche( + ForeignChainRpcRequest::Avalanche(EvmRpcRequest { + tx_id: EvmTxId([12; 32]), + extractors: vec![], + finality: EvmFinality::Finalized, + }), + ForeignChain::Avalanche, + )] + #[case::adi( + ForeignChainRpcRequest::Adi(EvmRpcRequest { + tx_id: EvmTxId([12; 32]), + extractors: vec![], + finality: EvmFinality::Finalized, + }), + ForeignChain::Adi, + )] fn foreign_chain_rpc_request_chain__should_return_correct_chain( #[case] request: ForeignChainRpcRequest, #[case] expected_chain: ForeignChain, diff --git a/crates/near-mpc-sdk/src/foreign_chain.rs b/crates/near-mpc-sdk/src/foreign_chain.rs index b77ae9628..920869067 100644 --- a/crates/near-mpc-sdk/src/foreign_chain.rs +++ b/crates/near-mpc-sdk/src/foreign_chain.rs @@ -3,7 +3,9 @@ use borsh::{BorshDeserialize, BorshSerialize}; pub use near_mpc_contract_interface::method_names::VERIFY_FOREIGN_TRANSACTION as VERIFY_FOREIGN_TRANSACTION_METHOD_NAME; pub mod abstract_chain; +pub mod adi; pub mod arbitrum; +pub mod avalanche; pub mod base; pub mod bitcoin; pub mod bnb; diff --git a/crates/near-mpc-sdk/src/foreign_chain/adi.rs b/crates/near-mpc-sdk/src/foreign_chain/adi.rs new file mode 100644 index 000000000..493871a83 --- /dev/null +++ b/crates/near-mpc-sdk/src/foreign_chain/adi.rs @@ -0,0 +1,63 @@ +use crate::{ + foreign_chain::{ + ForeignChainRequestBuilder, + evm::{EvmChainVariant, EvmRequest}, + }, + sign::NotSet, +}; + +pub use crate::foreign_chain::evm::EvmBlockHash as AdiBlockHash; +pub use crate::foreign_chain::evm::{ + EvmExtractedValue, EvmExtractor, EvmFinality, EvmLog, EvmRpcRequest, EvmTxId, + ForeignChainRpcRequest, +}; + +#[derive(Debug, Clone)] +pub struct Adi; + +impl EvmChainVariant for Adi { + fn wrap(request: EvmRpcRequest) -> ForeignChainRpcRequest { + ForeignChainRpcRequest::Adi(request) + } +} + +pub type AdiRequest = EvmRequest; + +impl ForeignChainRequestBuilder, NotSet> { + pub fn new_adi() -> Self { + Self { + request: EvmRequest { + tx_id: NotSet, + finality: NotSet, + expected_block_hash: None, + expected_logs: vec![], + _chain: std::marker::PhantomData, + }, + domain_id: NotSet, + } + } +} + +#[cfg(test)] +mod test { + use assert_matches::assert_matches; + use near_mpc_contract_interface::types::DomainId; + + use crate::foreign_chain::ForeignChainRequestBuilder; + + use super::*; + + #[test] + fn build_wraps_into_adi_variant() { + // given / when + let (_verifier, request_args) = ForeignChainRequestBuilder::new_adi() + .with_tx_id(EvmTxId::from([1; 32])) + .with_finality(EvmFinality::Finalized) + .with_domain_id(DomainId::from(1)) + .build() + .unwrap(); + + // then + assert_matches!(request_args.request, ForeignChainRpcRequest::Adi(_)); + } +} diff --git a/crates/near-mpc-sdk/src/foreign_chain/avalanche.rs b/crates/near-mpc-sdk/src/foreign_chain/avalanche.rs new file mode 100644 index 000000000..33920c2fa --- /dev/null +++ b/crates/near-mpc-sdk/src/foreign_chain/avalanche.rs @@ -0,0 +1,63 @@ +use crate::{ + foreign_chain::{ + ForeignChainRequestBuilder, + evm::{EvmChainVariant, EvmRequest}, + }, + sign::NotSet, +}; + +pub use crate::foreign_chain::evm::EvmBlockHash as AvalancheBlockHash; +pub use crate::foreign_chain::evm::{ + EvmExtractedValue, EvmExtractor, EvmFinality, EvmLog, EvmRpcRequest, EvmTxId, + ForeignChainRpcRequest, +}; + +#[derive(Debug, Clone)] +pub struct Avalanche; + +impl EvmChainVariant for Avalanche { + fn wrap(request: EvmRpcRequest) -> ForeignChainRpcRequest { + ForeignChainRpcRequest::Avalanche(request) + } +} + +pub type AvalancheRequest = EvmRequest; + +impl ForeignChainRequestBuilder, NotSet> { + pub fn new_avalanche() -> Self { + Self { + request: EvmRequest { + tx_id: NotSet, + finality: NotSet, + expected_block_hash: None, + expected_logs: vec![], + _chain: std::marker::PhantomData, + }, + domain_id: NotSet, + } + } +} + +#[cfg(test)] +mod test { + use assert_matches::assert_matches; + use near_mpc_contract_interface::types::DomainId; + + use crate::foreign_chain::ForeignChainRequestBuilder; + + use super::*; + + #[test] + fn build_wraps_into_avalanche_variant() { + // given / when + let (_verifier, request_args) = ForeignChainRequestBuilder::new_avalanche() + .with_tx_id(EvmTxId::from([1; 32])) + .with_finality(EvmFinality::Finalized) + .with_domain_id(DomainId::from(1)) + .build() + .unwrap(); + + // then + assert_matches!(request_args.request, ForeignChainRpcRequest::Avalanche(_)); + } +} diff --git a/crates/near-mpc-sdk/tests/adi.rs b/crates/near-mpc-sdk/tests/adi.rs new file mode 100644 index 000000000..759d8a2f1 --- /dev/null +++ b/crates/near-mpc-sdk/tests/adi.rs @@ -0,0 +1,27 @@ +use assert_matches::assert_matches; +use near_mpc_sdk::foreign_chain::{ + DomainId, ForeignChainRequestBuilder, + adi::{EvmFinality, EvmTxId, ForeignChainRpcRequest}, +}; + +#[test] +fn no_extractor_added() { + // given + let domain_id = DomainId::from(2); + let tx_id = EvmTxId::from([123; 32]); + + // when + let (_verifier, built_sign_request_args) = ForeignChainRequestBuilder::new_adi() + .with_tx_id(tx_id) + .with_finality(EvmFinality::Finalized) + .with_domain_id(domain_id) + .build() + .unwrap(); + + // then + let no_extractors = vec![]; + + assert_matches!(built_sign_request_args.request, ForeignChainRpcRequest::Adi(adi_rpc_request) => { + assert_eq!(adi_rpc_request.extractors, no_extractors); + }); +} diff --git a/crates/near-mpc-sdk/tests/avalanche.rs b/crates/near-mpc-sdk/tests/avalanche.rs new file mode 100644 index 000000000..234adcb2e --- /dev/null +++ b/crates/near-mpc-sdk/tests/avalanche.rs @@ -0,0 +1,27 @@ +use assert_matches::assert_matches; +use near_mpc_sdk::foreign_chain::{ + DomainId, ForeignChainRequestBuilder, + avalanche::{EvmFinality, EvmTxId, ForeignChainRpcRequest}, +}; + +#[test] +fn no_extractor_added() { + // given + let domain_id = DomainId::from(2); + let tx_id = EvmTxId::from([123; 32]); + + // when + let (_verifier, built_sign_request_args) = ForeignChainRequestBuilder::new_avalanche() + .with_tx_id(tx_id) + .with_finality(EvmFinality::Finalized) + .with_domain_id(domain_id) + .build() + .unwrap(); + + // then + let no_extractors = vec![]; + + assert_matches!(built_sign_request_args.request, ForeignChainRpcRequest::Avalanche(avalanche_rpc_request) => { + assert_eq!(avalanche_rpc_request.extractors, no_extractors); + }); +} diff --git a/crates/node-config/src/foreign_chains.rs b/crates/node-config/src/foreign_chains.rs index ef3a361e6..a253df39b 100644 --- a/crates/node-config/src/foreign_chains.rs +++ b/crates/node-config/src/foreign_chains.rs @@ -40,6 +40,10 @@ pub struct ForeignChainsConfig { pub aptos: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub sui: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub avalanche: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub adi: Option, } #[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] @@ -175,6 +179,8 @@ impl ForeignChainsConfig { (self.polygon.as_ref(), dtos::ForeignChain::Polygon), (self.aptos.as_ref(), dtos::ForeignChain::Aptos), (self.sui.as_ref(), dtos::ForeignChain::Sui), + (self.avalanche.as_ref(), dtos::ForeignChain::Avalanche), + (self.adi.as_ref(), dtos::ForeignChain::Adi), ] .into_iter() .filter_map(|(config, dto_identifier)| config.map(|config| (config, dto_identifier))) diff --git a/crates/node/src/providers/verify_foreign_tx.rs b/crates/node/src/providers/verify_foreign_tx.rs index 9615254ef..5adb810e0 100644 --- a/crates/node/src/providers/verify_foreign_tx.rs +++ b/crates/node/src/providers/verify_foreign_tx.rs @@ -7,8 +7,10 @@ use crate::storage::VerifyForeignTransactionRequestStorage; use crate::types::VerifyForeignTxId; use borsh::{BorshDeserialize, BorshSerialize}; use foreign_chain_inspector::abstract_chain::inspector::AbstractInspector; +use foreign_chain_inspector::adi::inspector::AdiInspector; use foreign_chain_inspector::aptos::inspector::AptosInspector; use foreign_chain_inspector::arbitrum::inspector::ArbitrumInspector; +use foreign_chain_inspector::avalanche::inspector::AvalancheInspector; use foreign_chain_inspector::base::inspector::BaseInspector; use foreign_chain_inspector::bitcoin::inspector::BitcoinInspector; use foreign_chain_inspector::bnb::inspector::BnbInspector; @@ -40,6 +42,8 @@ pub(crate) struct ForeignChainInspectors { pub arbitrum: Option>>, pub hyper_evm: Option>>, pub polygon: Option>>, + pub avalanche: Option>>, + pub adi: Option>>, pub aptos: Option>>, pub sui: Option>>, } @@ -140,6 +144,11 @@ impl ForeignChainInspectors { config.polygon.as_ref(), with_http_client(PolygonInspector::new), )?, + avalanche: build_fanout( + config.avalanche.as_ref(), + with_http_client(AvalancheInspector::new), + )?, + adi: build_fanout(config.adi.as_ref(), with_http_client(AdiInspector::new))?, aptos: build_fanout(config.aptos.as_ref(), new_aptos_inspector)?, sui: build_fanout(config.sui.as_ref(), new_sui_inspector)?, }) diff --git a/crates/node/src/providers/verify_foreign_tx/sign.rs b/crates/node/src/providers/verify_foreign_tx/sign.rs index cae149c68..f68eb839e 100644 --- a/crates/node/src/providers/verify_foreign_tx/sign.rs +++ b/crates/node/src/providers/verify_foreign_tx/sign.rs @@ -1,7 +1,9 @@ use anyhow::{Context, bail}; use foreign_chain_inspector::abstract_chain::inspector::AbstractExtractor; +use foreign_chain_inspector::adi::inspector::AdiExtractor; use foreign_chain_inspector::aptos::inspector::{AptosExtractor, AptosFinality}; use foreign_chain_inspector::arbitrum::inspector::ArbitrumExtractor; +use foreign_chain_inspector::avalanche::inspector::AvalancheExtractor; use foreign_chain_inspector::base::inspector::BaseExtractor; use foreign_chain_inspector::bitcoin::inspector::BitcoinExtractor; use foreign_chain_inspector::bnb::inspector::BnbExtractor; @@ -291,6 +293,50 @@ where .context("timed out during execution of foreign chain request")??; values.into_iter().map(Into::into).collect() } + dtos::ForeignChainRpcRequest::Avalanche(request) => { + let inspector = self + .inspectors + .avalanche + .as_ref() + .context("no inspector configured for Avalanche")?; + + let transaction_id = request.tx_id.0.into(); + let finality: EthereumFinality = request.finality.clone().try_into()?; + let extractors: Vec = request + .extractors + .iter() + .cloned() + .map(TryInto::try_into) + .collect::>()?; + let values = inspector + .extract(transaction_id, finality, extractors) + .timeout(FOREIGN_CHAIN_INSPECTION_TIMEOUT) + .await + .context("timed out during execution of foreign chain request")??; + values.into_iter().map(Into::into).collect() + } + dtos::ForeignChainRpcRequest::Adi(request) => { + let inspector = self + .inspectors + .adi + .as_ref() + .context("no inspector configured for ADI")?; + + let transaction_id = request.tx_id.0.into(); + let finality: EthereumFinality = request.finality.clone().try_into()?; + let extractors: Vec = request + .extractors + .iter() + .cloned() + .map(TryInto::try_into) + .collect::>()?; + let values = inspector + .extract(transaction_id, finality, extractors) + .timeout(FOREIGN_CHAIN_INSPECTION_TIMEOUT) + .await + .context("timed out during execution of foreign chain request")??; + values.into_iter().map(Into::into).collect() + } dtos::ForeignChainRpcRequest::Starknet(request) => { let inspector = self .inspectors diff --git a/crates/node/src/tests/foreign_chain_configuration.rs b/crates/node/src/tests/foreign_chain_configuration.rs index 957615b70..5442d8363 100644 --- a/crates/node/src/tests/foreign_chain_configuration.rs +++ b/crates/node/src/tests/foreign_chain_configuration.rs @@ -60,6 +60,8 @@ async fn foreign_chain_configuration_auto_registered_to_contract_on_startup__sho polygon: None, aptos: None, sui: None, + avalanche: None, + adi: None, }; for config in &mut setup.configs { config.config.foreign_chains = foreign_chains.clone(); diff --git a/crates/node/src/web.rs b/crates/node/src/web.rs index 345019c7e..63d046817 100644 --- a/crates/node/src/web.rs +++ b/crates/node/src/web.rs @@ -154,6 +154,10 @@ struct ForeignChainsProviderCounts { aptos: usize, #[serde(skip_serializing_if = "is_zero")] sui: usize, + #[serde(skip_serializing_if = "is_zero")] + avalanche: usize, + #[serde(skip_serializing_if = "is_zero")] + adi: usize, } impl From for ForeignChainsProviderCounts { @@ -171,6 +175,8 @@ impl From for ForeignChainsProviderCounts { polygon: config.polygon.map_or(0, |c| c.providers.len()), aptos: config.aptos.map_or(0, |c| c.providers.len()), sui: config.sui.map_or(0, |c| c.providers.len()), + avalanche: config.avalanche.map_or(0, |c| c.providers.len()), + adi: config.adi.map_or(0, |c| c.providers.len()), } } } @@ -410,6 +416,8 @@ mod tests { const POLYGON_RPC_URL: &str = "https://polygon-bor-rpc.publicnode.com"; const APTOS_RPC_URL: &str = "https://aptos-mainnet.nodereal.io/v1/"; const SUI_RPC_URL: &str = "https://fullnode.mainnet.sui.io/"; + const AVALANCHE_RPC_URL: &str = "https://api.avax.network/ext/bc/C/rpc"; + const ADI_RPC_URL: &str = "https://rpc.adifoundation.ai"; const SOLANA_BEARER_TOKEN: &str = "sk-SUPER-SECRET-KEY"; const BITCOIN_PATH_TOKEN: &str = "ankr-secret-token"; @@ -532,6 +540,12 @@ mod tests { )), aptos: Some(test_chain(PROVIDER_PUBLIC, APTOS_RPC_URL, AuthConfig::None)), sui: Some(test_chain(PROVIDER_PUBLIC, SUI_RPC_URL, AuthConfig::None)), + avalanche: Some(test_chain( + PROVIDER_PUBLIC, + AVALANCHE_RPC_URL, + AuthConfig::None, + )), + adi: Some(test_chain(PROVIDER_PUBLIC, ADI_RPC_URL, AuthConfig::None)), }, cores: Some(4), separate_asset_generation_runtime: true, @@ -571,6 +585,8 @@ mod tests { "polygon", "aptos", "sui", + "avalanche", + "adi", ] { assert_eq!( counts.get(chain).and_then(|v| v.as_u64()), @@ -598,6 +614,8 @@ mod tests { POLYGON_RPC_URL, APTOS_RPC_URL, SUI_RPC_URL, + AVALANCHE_RPC_URL, + ADI_RPC_URL, SOLANA_BEARER_TOKEN, BITCOIN_PATH_TOKEN, STARKNET_QUERY_TOKEN, diff --git a/docs/foreign-chain-transactions.md b/docs/foreign-chain-transactions.md index 9e4224d8a..bf1e921b8 100644 --- a/docs/foreign-chain-transactions.md +++ b/docs/foreign-chain-transactions.md @@ -553,7 +553,7 @@ Not every chain has a fingerprint probe. The table lists the ones that do, with | chain | probe | |---|---| | starknet | `starknet_chainId` | -| base, bnb, arbitrum, polygon, hyper_evm, abstract | `eth_chainId` | +| base, bnb, arbitrum, polygon, hyper_evm, avalanche, adi, abstract | `eth_chainId` | | bitcoin | `getblockhash` at height 0 | The reported and the configured value are normalized before they are compared, because the same fingerprint has several legal spellings. Starknet's is the chain id felt in lowercase `0x` hex without leading zeros, which providers and operators alike are free to pad and upper-case. The EVM chain id is compared in decimal, the form it is published and configured in, while `eth_chainId` answers a `0x` hex quantity. Bitcoin's genesis hash is compared in lowercase hex, with the leading zeros kept, since they are digits of the hash. @@ -693,6 +693,8 @@ checkpoint digest — hence the neutral name. | arbitrum | EIP-155 chain id, decimal | `"42161"` | `"421614"` (Sepolia) | | polygon | EIP-155 chain id, decimal | `"137"` | `"80002"` (Amoy) | | hyper_evm | EIP-155 chain id, decimal | `"999"` | `"998"` | +| avalanche | EIP-155 chain id, decimal | `"43114"` | `"43113"` (Fuji) | +| adi | EIP-155 chain id, decimal | `"36900"` | `"99999"` (AB testnet) | | abstract | EIP-155 chain id, decimal | `"2741"` | `"11124"` | | starknet | chain id felt, lowercase `0x` hex | `"0x534e5f4d41494e"` | `"0x534e5f5345504f4c4941"` (Sepolia) | | bitcoin | genesis block hash, lowercase hex | `"000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f"` | `"000000000933ea01ad0ee984209779baaec3ced90fa3f408719526f8d77f4943"` (testnet3) |