use alloy_chains::NamedChain;
use alloy_primitives::{Address, Bytes, U256};
use alloy_provider::ProviderBuilder;
use cctp_rs::{CctpV2, CctpV2Bridge, DomainId};
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
println!("🔍 CCTP v2 Integration Validation");
println!("==================================\n");
validate_chain_support()?;
validate_domain_ids()?;
validate_contract_addresses()?;
validate_bridge_configurations()?;
validate_api_endpoints()?;
validate_fast_transfer_support()?;
validate_error_handling()?;
validate_cross_chain_compatibility()?;
println!("\n✅ All validations passed!");
println!(" v2 implementation is correctly configured and ready for use.");
Ok(())
}
fn validate_chain_support() -> Result<(), Box<dyn std::error::Error>> {
println!("1️⃣ Validating Chain Support Matrix");
println!(" --------------------------------");
let v2_chains = vec![
NamedChain::Mainnet,
NamedChain::Arbitrum,
NamedChain::Base,
NamedChain::Optimism,
NamedChain::Avalanche,
NamedChain::Polygon,
NamedChain::Unichain,
NamedChain::Linea,
NamedChain::Sonic,
NamedChain::Sei,
NamedChain::Sepolia,
NamedChain::BaseSepolia,
NamedChain::ArbitrumSepolia,
NamedChain::OptimismSepolia,
NamedChain::AvalancheFuji,
NamedChain::PolygonAmoy,
];
let mut mainnet_count = 0;
let mut testnet_count = 0;
for chain in v2_chains.iter() {
assert!(
chain.supports_cctp_v2(),
"Chain {chain} should support v2 but doesn't"
);
if chain.is_testnet() {
testnet_count += 1;
} else {
mainnet_count += 1;
}
print!(" ✓ {:<20} ", format!("{chain}"));
let domain = chain.cctp_v2_domain_id()?;
print!("Domain: {:>2} ", domain.as_u32());
let tm = chain.token_messenger_v2_address()?;
let mt = chain.message_transmitter_v2_address()?;
println!(
"TM: {}... MT: {}...",
&tm.to_string()[..10],
&mt.to_string()[..10]
);
}
println!("\n Summary:");
println!(" • Mainnet chains: {mainnet_count}");
println!(" • Testnet chains: {testnet_count}");
println!(" • Total v2 chains: {}\n", mainnet_count + testnet_count);
Ok(())
}
fn validate_domain_ids() -> Result<(), Box<dyn std::error::Error>> {
println!("2️⃣ Validating Domain ID Mappings");
println!(" ------------------------------");
let known_mappings = vec![
(NamedChain::Mainnet, DomainId::Ethereum, 0),
(NamedChain::Avalanche, DomainId::Avalanche, 1),
(NamedChain::Optimism, DomainId::Optimism, 2),
(NamedChain::Arbitrum, DomainId::Arbitrum, 3),
(NamedChain::Base, DomainId::Base, 6),
(NamedChain::Polygon, DomainId::Polygon, 7),
(NamedChain::Unichain, DomainId::Unichain, 10),
(NamedChain::Linea, DomainId::Linea, 11),
(NamedChain::Sonic, DomainId::Sonic, 13),
(NamedChain::Sei, DomainId::Sei, 16),
(NamedChain::Sepolia, DomainId::Ethereum, 0),
(NamedChain::AvalancheFuji, DomainId::Avalanche, 1),
(NamedChain::OptimismSepolia, DomainId::Optimism, 2),
(NamedChain::ArbitrumSepolia, DomainId::Arbitrum, 3),
(NamedChain::BaseSepolia, DomainId::Base, 6),
(NamedChain::PolygonAmoy, DomainId::Polygon, 7),
];
for (chain, expected_domain, expected_id) in known_mappings {
let actual_domain = chain.cctp_v2_domain_id()?;
assert_eq!(
actual_domain, expected_domain,
"Domain mismatch for {chain}: expected {expected_domain}, got {actual_domain}"
);
assert_eq!(
actual_domain.as_u32(),
expected_id,
"Domain ID mismatch for {chain}: expected {expected_id}, got {}",
actual_domain.as_u32()
);
println!(
" ✓ {:<20} → {:>2} ({})",
format!("{chain}"),
expected_id,
expected_domain
);
}
println!();
Ok(())
}
fn validate_contract_addresses() -> Result<(), Box<dyn std::error::Error>> {
println!("3️⃣ Validating Contract Addresses");
println!(" ------------------------------");
let expected_mainnet_tm: Address = "0x28b5a0e9C621a5BadaA536219b3a228C8168cf5d".parse()?;
let expected_mainnet_mt: Address = "0x81D40F21F12A8F0E3252Bccb954D722d4c464B64".parse()?;
let expected_testnet_tm: Address = "0x8FE6B999Dc680CcFDD5Bf7EB0974218be2542DAA".parse()?;
let expected_testnet_mt: Address = "0xE737e5cEBEEBa77EFE34D4aa090756590b1CE275".parse()?;
println!(" Mainnet Address Consistency:");
let mainnet_chains = vec![
NamedChain::Mainnet,
NamedChain::Arbitrum,
NamedChain::Base,
NamedChain::Optimism,
NamedChain::Avalanche,
NamedChain::Polygon,
NamedChain::Unichain,
NamedChain::Linea,
NamedChain::Sonic,
NamedChain::Sei,
];
for chain in mainnet_chains {
let tm = chain.token_messenger_v2_address()?;
let mt = chain.message_transmitter_v2_address()?;
assert_eq!(
tm, expected_mainnet_tm,
"{chain} TokenMessenger doesn't match expected mainnet address"
);
assert_eq!(
mt, expected_mainnet_mt,
"{chain} MessageTransmitter doesn't match expected mainnet address"
);
println!(" ✓ {:<20} Uses unified v2 addresses", format!("{chain}"));
}
println!("\n Testnet Address Consistency:");
let testnet_chains = vec![
NamedChain::Sepolia,
NamedChain::BaseSepolia,
NamedChain::ArbitrumSepolia,
NamedChain::OptimismSepolia,
NamedChain::AvalancheFuji,
NamedChain::PolygonAmoy,
];
for chain in testnet_chains {
let tm = chain.token_messenger_v2_address()?;
let mt = chain.message_transmitter_v2_address()?;
assert_eq!(
tm, expected_testnet_tm,
"{chain} TokenMessenger doesn't match expected testnet address"
);
assert_eq!(
mt, expected_testnet_mt,
"{chain} MessageTransmitter doesn't match expected testnet address"
);
println!(
" ✓ {:<20} Uses unified testnet addresses",
format!("{chain}")
);
}
println!();
Ok(())
}
fn validate_bridge_configurations() -> Result<(), Box<dyn std::error::Error>> {
println!("4️⃣ Validating Bridge Configurations");
println!(" ---------------------------------");
let provider = ProviderBuilder::new().connect_http("http://localhost:8545".parse()?);
println!(" Standard Transfer:");
let standard = CctpV2Bridge::builder()
.source_chain(NamedChain::Mainnet)
.destination_chain(NamedChain::Linea)
.source_provider(provider.clone())
.destination_provider(provider.clone())
.recipient(Address::ZERO)
.build();
assert!(
!standard.is_fast_transfer(),
"Standard should not have fast_transfer"
);
assert!(
standard.hook_data().is_none(),
"Standard should not have hooks"
);
assert_eq!(standard.finality_threshold().as_u32(), 2000);
assert_eq!(standard.max_fee(), None);
println!(" ✓ Finality: 2000 (finalized)");
println!(" ✓ Fast transfer: disabled");
println!(" ✓ Max fee: None");
println!(" ✓ Hooks: None\n");
println!(" Fast Transfer:");
let fast = CctpV2Bridge::builder()
.source_chain(NamedChain::Mainnet)
.destination_chain(NamedChain::Linea)
.source_provider(provider.clone())
.destination_provider(provider.clone())
.recipient(Address::ZERO)
.fast_transfer(true)
.max_fee(U256::from(1000))
.build();
assert!(
fast.is_fast_transfer(),
"Fast should have fast_transfer enabled"
);
assert!(
fast.hook_data().is_none(),
"Fast (without hooks) should not have hooks"
);
assert_eq!(fast.finality_threshold().as_u32(), 1000);
assert_eq!(fast.max_fee(), Some(U256::from(1000)));
println!(" ✓ Finality: 1000 (confirmed)");
println!(" ✓ Fast transfer: enabled");
println!(" ✓ Max fee: 1000 (0.001 USDC)");
println!(" ✓ Hooks: None\n");
println!(" With Hooks:");
let hook_data = Bytes::from(vec![0xde, 0xad, 0xbe, 0xef]);
let hooks = CctpV2Bridge::builder()
.source_chain(NamedChain::Mainnet)
.destination_chain(NamedChain::Linea)
.source_provider(provider.clone())
.destination_provider(provider.clone())
.recipient(Address::ZERO)
.hook_data(hook_data.clone())
.build();
assert!(
!hooks.is_fast_transfer(),
"Hooks (standard) should not be fast"
);
assert_eq!(hooks.hook_data(), Some(&hook_data));
assert_eq!(hooks.finality_threshold().as_u32(), 2000);
println!(" ✓ Finality: 2000 (finalized)");
println!(" ✓ Fast transfer: disabled");
println!(" ✓ Hooks: Present (4 bytes)");
println!(" ✓ Hook data: 0xdeadbeef\n");
println!(" Fast + Hooks (priority test):");
let fast_hooks = CctpV2Bridge::builder()
.source_chain(NamedChain::Mainnet)
.destination_chain(NamedChain::Linea)
.source_provider(provider.clone())
.destination_provider(provider)
.recipient(Address::ZERO)
.fast_transfer(true)
.max_fee(U256::from(1000))
.hook_data(hook_data.clone())
.build();
assert!(fast_hooks.is_fast_transfer(), "Should have fast_transfer");
assert_eq!(
fast_hooks.hook_data(),
Some(&hook_data),
"Should have hooks"
);
assert_eq!(
fast_hooks.finality_threshold().as_u32(),
1000,
"Fast takes priority"
);
println!(" ✓ Finality: 1000 (fast finality with hooks)");
println!(" ✓ Fast transfer: enabled");
println!(" ✓ Hooks: Present");
println!(" ✓ Priority: Fast finality + hooks both active\n");
Ok(())
}
fn validate_api_endpoints() -> Result<(), Box<dyn std::error::Error>> {
println!("5️⃣ Validating API Endpoint Construction");
println!(" -------------------------------------");
let provider = ProviderBuilder::new().connect_http("http://localhost:8545".parse()?);
println!(" Mainnet API:");
let mainnet_bridge = CctpV2Bridge::builder()
.source_chain(NamedChain::Mainnet)
.destination_chain(NamedChain::Linea)
.source_provider(provider.clone())
.destination_provider(provider.clone())
.recipient(Address::ZERO)
.build();
let mainnet_api = mainnet_bridge.api_url();
assert!(
mainnet_api
.as_str()
.starts_with("https://iris-api.circle.com"),
"Mainnet should use production API"
);
println!(
" ✓ Base URL: {}",
mainnet_api.as_str().trim_end_matches('/')
);
let test_hash = [0xab; 32];
let url = mainnet_bridge.create_url(test_hash.into())?;
assert!(
url.as_str().contains("/v2/attestations/"),
"Should use v2 path"
);
assert!(
url.as_str().contains("0xabab"),
"Should include hash with 0x prefix"
);
assert!(url
.as_str()
.starts_with("https://iris-api.circle.com/v2/attestations/0x"));
println!(" ✓ Full URL format: {url}");
println!(" ✓ Path: /v2/attestations/");
println!(" ✓ Hash format: 0x-prefixed\n");
println!(" Testnet API:");
let testnet_bridge = CctpV2Bridge::builder()
.source_chain(NamedChain::Sepolia)
.destination_chain(NamedChain::BaseSepolia)
.source_provider(provider.clone())
.destination_provider(provider)
.recipient(Address::ZERO)
.build();
let testnet_api = testnet_bridge.api_url();
assert!(
testnet_api
.as_str()
.starts_with("https://iris-api-sandbox.circle.com"),
"Testnet should use sandbox API"
);
println!(
" ✓ Base URL: {}",
testnet_api.as_str().trim_end_matches('/')
);
let testnet_url = testnet_bridge.create_url(test_hash.into())?;
assert!(testnet_url.as_str().contains("iris-api-sandbox"));
println!(" ✓ Uses sandbox environment");
println!(" ✓ Full URL format: {testnet_url}\n");
Ok(())
}
fn validate_fast_transfer_support() -> Result<(), Box<dyn std::error::Error>> {
println!("6️⃣ Validating Fast Transfer Support");
println!(" ---------------------------------");
let test_chains = vec![
NamedChain::Mainnet,
NamedChain::Arbitrum,
NamedChain::Base,
NamedChain::Optimism,
NamedChain::Linea,
NamedChain::Sonic,
NamedChain::Avalanche,
NamedChain::Polygon,
NamedChain::Sei,
NamedChain::Unichain,
NamedChain::Sepolia,
NamedChain::BaseSepolia,
];
for chain in test_chains {
let supports_fast = chain.supports_fast_transfer()?;
assert!(supports_fast, "{chain} should support fast transfer");
let fee_bps = chain.fast_transfer_fee_bps()?;
print!(" ✓ {:<20} Fast: Yes ", format!("{chain}"));
match fee_bps {
Some(bps) => {
assert!(bps <= 14, "Fee should be 0-14 bps, got {bps}");
println!("Fee: {bps} bps");
}
None => println!("Fee: Free (0 bps)"),
}
}
println!();
Ok(())
}
fn validate_error_handling() -> Result<(), Box<dyn std::error::Error>> {
println!("7️⃣ Validating Error Handling");
println!(" --------------------------");
println!(" Testing unsupported chain error:");
let result = NamedChain::Moonbeam.cctp_v2_domain_id();
assert!(result.is_err(), "Moonbeam should not support v2");
println!(" ✓ Unsupported chain returns error");
let result = NamedChain::Moonbeam.token_messenger_v2_address();
assert!(result.is_err(), "Should error on unsupported chain");
println!(" ✓ Contract address query fails appropriately");
let result = NamedChain::Moonbeam.supports_fast_transfer();
assert!(result.is_err(), "Should error on unsupported chain");
println!(" ✓ Fast transfer query fails appropriately\n");
Ok(())
}
fn validate_cross_chain_compatibility() -> Result<(), Box<dyn std::error::Error>> {
println!("8️⃣ Validating Cross-Chain Compatibility");
println!(" -------------------------------------");
let test_pairs = vec![
(NamedChain::Mainnet, NamedChain::Linea, "Ethereum → Linea"),
(NamedChain::Arbitrum, NamedChain::Base, "Arbitrum → Base"),
(NamedChain::Base, NamedChain::Sonic, "Base → Sonic"),
(
NamedChain::Sepolia,
NamedChain::BaseSepolia,
"Sepolia → Base Sepolia",
),
];
let provider = ProviderBuilder::new().connect_http("http://localhost:8545".parse()?);
for (source, dest, description) in test_pairs {
println!(" Testing: {description}");
assert!(source.supports_cctp_v2());
assert!(dest.supports_cctp_v2());
let bridge = CctpV2Bridge::builder()
.source_chain(source)
.destination_chain(dest)
.source_provider(provider.clone())
.destination_provider(provider.clone())
.recipient(Address::ZERO)
.build();
let source_domain = bridge.source_chain().cctp_v2_domain_id()?;
let dest_domain = bridge.destination_domain_id()?;
println!(" ✓ Source domain: {source_domain}");
println!(" ✓ Dest domain: {dest_domain}");
let _tm = bridge.token_messenger_v2_contract()?;
let _mt = bridge.message_transmitter_v2_contract()?;
println!(" ✓ Contract addresses resolved");
let api_url = bridge.api_url();
if source.is_testnet() {
assert!(api_url.as_str().contains("sandbox"));
} else {
assert!(!api_url.as_str().contains("sandbox"));
}
println!(" ✓ API environment matches network\n");
}
Ok(())
}