diff --git a/.github/dependabot.yml b/.github/dependabot.yml index c11fc03..b756b0a 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -5,7 +5,8 @@ updates: schedule: interval: "weekly" ignore: + - dependency-name: "bincode*" - dependency-name: "solana*" - dependency-name: "spl-*" - - dependency-name: "*" + - dependency-name: ".*" update-types: ["version-update:semver-major"] diff --git a/src/bridge/evm.rs b/src/bridge/evm.rs index c26d919..63af7c9 100644 --- a/src/bridge/evm.rs +++ b/src/bridge/evm.rs @@ -1,6 +1,7 @@ use { super::Cctp, crate::{ + Attestation, CctpChain, ERC20, EvmBridgeResult, @@ -40,20 +41,17 @@ impl< } #[instrument(skip(self,max_fee,destination_caller,min_finality_threshold), level = Level::INFO)] - pub async fn bridge( + pub async fn burn( &self, amount: alloy_primitives::U256, destination_caller: Option, max_fee: Option, min_finality_threshold: Option, - // attestation_poll_interval: Option, - ) -> Result { + ) -> Result<(TxHash, Option)> { info!("burning {amount}"); let source_provider = self.source_provider(); - let destination_provider = self.destination_provider(); let recipient: EvmAddress = self.recipient().try_into()?; let token_messenger: EvmAddress = self.token_messenger_contract()?.try_into()?; - let message_transmitter: EvmAddress = self.message_transmitter_contract()?.try_into()?; let destination_domain = self.destination_domain_id()?; let usdc_address = self.source_chain().usdc_token_address()?.try_into()?; let erc20 = ERC20::new(usdc_address, source_provider); @@ -102,6 +100,14 @@ impl< ))) .watch() .await?; + + Ok((burn_hash, approval_hash)) + } + + #[instrument(skip(self), level = Level::INFO)] + pub async fn recv(&self, burn_hash: TxHash) -> Result<(Attestation, TxHash)> { + let destination_provider = self.destination_provider(); + let message_transmitter: EvmAddress = self.message_transmitter_contract()?.try_into()?; let attestation = self .get_attestation_with_retry(format!("0x{}", encode(burn_hash)), None, Some(10)) .await?; @@ -114,14 +120,32 @@ impl< attestation.attestation.clone().into(), ); - info!("receiving {amount} on chain {}", self.destination_chain(),); - let recv_hash = recv_message_tx - .send() - .await? - .with_required_confirmations(2) - .with_timeout(Some(Duration::from_secs(90))) - .watch() + info!("receiving on chain {}", self.destination_chain(),); + Ok(( + attestation, + recv_message_tx + .send() + .await? + .with_required_confirmations(2) + .with_timeout(Some(Duration::from_secs(90))) + .watch() + .await?, + )) + } + + #[instrument(skip(self,max_fee,destination_caller,min_finality_threshold), level = Level::INFO)] + pub async fn bridge( + &self, + amount: alloy_primitives::U256, + destination_caller: Option, + max_fee: Option, + min_finality_threshold: Option, + // attestation_poll_interval: Option, + ) -> Result { + let (burn_hash, approval_hash) = self + .burn(amount, destination_caller, max_fee, min_finality_threshold) .await?; + let (attestation, recv_hash) = self.recv(burn_hash).await?; Ok(EvmBridgeResult { approval: approval_hash, diff --git a/src/chain.rs b/src/chain.rs index 441ed90..458cd6d 100644 --- a/src/chain.rs +++ b/src/chain.rs @@ -280,177 +280,214 @@ impl CctpChain for NamedChain { } } -// #[cfg(test)] -// mod tests { -// use super::*; -// use alloy_chains::NamedChain; -// use rstest::rstest; - -// #[rstest] -// #[case(NamedChain::Mainnet, true)] -// #[case(NamedChain::Arbitrum, true)] -// #[case(NamedChain::Base, true)] -// #[case(NamedChain::Optimism, true)] -// #[case(NamedChain::Unichain, true)] -// #[case(NamedChain::Avalanche, true)] -// #[case(NamedChain::Polygon, true)] -// #[case(NamedChain::Sepolia, true)] -// #[case(NamedChain::ArbitrumSepolia, true)] -// #[case(NamedChain::AvalancheFuji, true)] -// #[case(NamedChain::BaseSepolia, true)] -// #[case(NamedChain::OptimismSepolia, true)] -// #[case(NamedChain::PolygonAmoy, true)] -// #[case(NamedChain::BinanceSmartChain, false)] -// #[case(NamedChain::Fantom, false)] -// fn test_is_supported(#[case] chain: NamedChain, #[case] expected: bool) { -// assert_eq!(chain.is_supported(), expected); -// } - -// #[rstest] -// #[case(NamedChain::Mainnet, 19 * 60)] -// #[case(NamedChain::Arbitrum, 19 * 60)] -// #[case(NamedChain::Base, 19 * 60)] -// #[case(NamedChain::Optimism, 19 * 60)] -// #[case(NamedChain::Unichain, 19 * 60)] -// #[case(NamedChain::Avalanche, 20)] -// #[case(NamedChain::Polygon, 8 * 60)] -// #[case(NamedChain::Sepolia, 60)] -// #[case(NamedChain::ArbitrumSepolia, 20)] -// #[case(NamedChain::AvalancheFuji, 20)] -// #[case(NamedChain::BaseSepolia, 20)] -// #[case(NamedChain::OptimismSepolia, 20)] -// #[case(NamedChain::PolygonAmoy, 20)] -// fn test_confirmation_average_time_seconds_supported_chains( -// #[case] chain: NamedChain, -// #[case] expected: u64, -// ) { -// assert_eq!(chain.confirmation_average_time_seconds().unwrap(), -// expected); } - -// #[test] -// fn test_confirmation_average_time_seconds_unsupported_chain() { -// let result = -// NamedChain::BinanceSmartChain.confirmation_average_time_seconds(); -// assert!(result.is_err()); -// assert!(matches!( -// result.unwrap_err(), -// CctpError::ChainNotSupported { .. } -// )); -// } - -// #[rstest] -// #[case(NamedChain::Arbitrum, ARBITRUM_DOMAIN_ID)] -// #[case(NamedChain::ArbitrumSepolia, ARBITRUM_DOMAIN_ID)] -// #[case(NamedChain::Avalanche, AVALANCHE_DOMAIN_ID)] -// #[case(NamedChain::Base, BASE_DOMAIN_ID)] -// #[case(NamedChain::BaseSepolia, BASE_DOMAIN_ID)] -// #[case(NamedChain::Mainnet, ETHEREUM_DOMAIN_ID)] -// #[case(NamedChain::Sepolia, ETHEREUM_DOMAIN_ID)] -// #[case(NamedChain::Optimism, OPTIMISM_DOMAIN_ID)] -// #[case(NamedChain::Polygon, POLYGON_DOMAIN_ID)] -// #[case(NamedChain::Unichain, UNICHAIN_DOMAIN_ID)] -// fn test_cctp_domain_id_supported_chains(#[case] chain: NamedChain, -// #[case] expected: u32) { assert_eq!(chain.cctp_domain_id().unwrap(), -// expected); } - -// #[test] -// fn test_cctp_domain_id_unsupported_chain() { -// let result = NamedChain::BinanceSmartChain.cctp_domain_id(); -// assert!(result.is_err()); -// assert!(matches!( -// result.unwrap_err(), -// CctpError::ChainNotSupported { .. } -// )); -// } - -// #[rstest] -// #[case(NamedChain::Arbitrum, ARBITRUM_TOKEN_MESSENGER_ADDRESS)] -// #[case(NamedChain::ArbitrumSepolia, -// ARBITRUM_SEPOLIA_TOKEN_MESSENGER_ADDRESS)] #[case(NamedChain::Avalanche, -// AVALANCHE_TOKEN_MESSENGER_ADDRESS)] #[case(NamedChain::Base, -// BASE_TOKEN_MESSENGER_ADDRESS)] #[case(NamedChain::BaseSepolia, -// BASE_SEPOLIA_TOKEN_MESSENGER_ADDRESS)] #[case(NamedChain::Sepolia, -// ETHEREUM_SEPOLIA_TOKEN_MESSENGER_ADDRESS)] #[case(NamedChain::Mainnet, -// ETHEREUM_TOKEN_MESSENGER_ADDRESS)] #[case(NamedChain::Optimism, -// OPTIMISM_TOKEN_MESSENGER_ADDRESS)] #[case(NamedChain::Polygon, -// POLYGON_CCTP_TOKEN_MESSENGER)] #[case(NamedChain::Unichain, -// UNICHAIN_CCTP_TOKEN_MESSENGER)] -// fn test_token_messenger_address_supported_chains( -// #[case] chain: NamedChain, -// #[case] expected_str: &str, -// ) { -// let result = chain.token_messenger_address().unwrap(); -// let expected: Address = expected_str.parse().unwrap(); -// assert_eq!(result, expected); -// } - -// #[test] -// fn test_token_messenger_address_unsupported_chain() { -// let result = NamedChain::BinanceSmartChain.token_messenger_address(); -// assert!(result.is_err()); -// assert!(matches!( -// result.unwrap_err(), -// CctpError::ChainNotSupported { .. } -// )); -// } - -// #[rstest] -// #[case(NamedChain::Arbitrum, ARBITRUM_MESSAGE_TRANSMITTER_ADDRESS)] -// #[case(NamedChain::Avalanche, AVALANCHE_MESSAGE_TRANSMITTER_ADDRESS)] -// #[case(NamedChain::Base, BASE_MESSAGE_TRANSMITTER_ADDRESS)] -// #[case(NamedChain::Mainnet, ETHEREUM_MESSAGE_TRANSMITTER_ADDRESS)] -// #[case(NamedChain::Optimism, OPTIMISM_MESSAGE_TRANSMITTER_ADDRESS)] -// #[case(NamedChain::Polygon, POLYGON_CCTP_MESSAGE_TRANSMITTER)] -// #[case( -// NamedChain::ArbitrumSepolia, -// ARBITRUM_SEPOLIA_MESSAGE_TRANSMITTER_ADDRESS -// )] -// #[case(NamedChain::BaseSepolia, -// BASE_SEPOLIA_MESSAGE_TRANSMITTER_ADDRESS)] #[case(NamedChain::Sepolia, -// ETHEREUM_SEPOLIA_MESSAGE_TRANSMITTER_ADDRESS)] -// #[case(NamedChain::Unichain, UNICHAIN_CCTP_MESSAGE_TRANSMITTER)] -// fn test_message_transmitter_address_supported_chains( -// #[case] chain: NamedChain, -// #[case] expected_str: &str, -// ) { -// let result = chain.message_transmitter_address().unwrap(); -// let expected: Address = expected_str.parse().unwrap(); -// assert_eq!(result, expected); -// } - -// #[test] -// fn test_message_transmitter_address_unsupported_chain() { -// let result = -// NamedChain::BinanceSmartChain.message_transmitter_address(); assert! -// (result.is_err()); assert!(matches!( -// result.unwrap_err(), -// CctpError::ChainNotSupported { .. } -// )); -// } - -// #[test] -// fn test_address_parsing_validation() { -// // All addresses should be valid Ethereum addresses -// for chain in [ -// NamedChain::Mainnet, -// NamedChain::Arbitrum, -// NamedChain::Base, -// NamedChain::Optimism, -// NamedChain::Unichain, -// NamedChain::Avalanche, -// NamedChain::Polygon, -// NamedChain::Sepolia, -// NamedChain::ArbitrumSepolia, -// NamedChain::BaseSepolia, -// ] { -// assert!( -// chain.token_messenger_address().is_ok(), -// "Token messenger address should be valid for {chain:?}" -// ); -// assert!( -// chain.message_transmitter_address().is_ok(), -// "Message transmitter address should be valid for {chain:?}" -// ); -// } -// } -// } +#[cfg(test)] +mod tests { + use { + super::*, + crate::{SOLANA_DEVNET, SOLANA_MAINNET}, + alloy_chains::NamedChain, + rstest::rstest, + }; + + #[rstest] + #[case(NamedChain::Mainnet, true)] + #[case(NamedChain::Arbitrum, true)] + #[case(NamedChain::Base, true)] + #[case(NamedChain::Optimism, true)] + #[case(NamedChain::Unichain, true)] + #[case(NamedChain::Avalanche, true)] + #[case(NamedChain::Polygon, true)] + #[case(NamedChain::Sepolia, true)] + #[case(NamedChain::ArbitrumSepolia, true)] + #[case(NamedChain::AvalancheFuji, true)] + #[case(NamedChain::BaseSepolia, true)] + #[case(NamedChain::OptimismSepolia, true)] + #[case(NamedChain::PolygonAmoy, true)] + #[case(NamedChain::BinanceSmartChain, false)] + #[case(NamedChain::Fantom, false)] + fn test_is_supported(#[case] chain: NamedChain, #[case] expected: bool) { + assert_eq!(chain.is_supported(), expected); + } + + #[rstest] + #[case(NamedChain::Mainnet, 19 * 60)] + #[case(NamedChain::Arbitrum, 19 * 60)] + #[case(NamedChain::Base, 19 * 60)] + #[case(NamedChain::Optimism, 19 * 60)] + #[case(NamedChain::Unichain, 19 * 60)] + #[case(NamedChain::Avalanche, 20)] + #[case(NamedChain::Polygon, 8 * 60)] + #[case(NamedChain::Sepolia, 60)] + #[case(NamedChain::ArbitrumSepolia, 20)] + #[case(NamedChain::AvalancheFuji, 20)] + #[case(NamedChain::BaseSepolia, 20)] + #[case(NamedChain::OptimismSepolia, 20)] + #[case(NamedChain::PolygonAmoy, 20)] + fn test_confirmation_average_time_seconds_supported_chains( + #[case] chain: NamedChain, + #[case] expected: u64, + ) { + assert_eq!(chain.confirmation_average_time_seconds().unwrap(), expected); + } + + #[test] + fn test_confirmation_average_time_seconds_unsupported_chain() { + let result = NamedChain::BinanceSmartChain.confirmation_average_time_seconds(); + assert!(result.is_err()); + assert!(matches!( + result.unwrap_err(), + Error::ChainNotSupported { .. } + )); + } + + #[rstest] + #[case(NamedChain::Arbitrum, ARBITRUM_DOMAIN_ID)] + #[case(NamedChain::ArbitrumSepolia, ARBITRUM_DOMAIN_ID)] + #[case(NamedChain::Avalanche, AVALANCHE_DOMAIN_ID)] + #[case(NamedChain::Base, BASE_DOMAIN_ID)] + #[case(NamedChain::BaseSepolia, BASE_DOMAIN_ID)] + #[case(NamedChain::Mainnet, ETHEREUM_DOMAIN_ID)] + #[case(NamedChain::Sepolia, ETHEREUM_DOMAIN_ID)] + #[case(NamedChain::Optimism, OPTIMISM_DOMAIN_ID)] + #[case(NamedChain::Polygon, POLYGON_DOMAIN_ID)] + #[case(NamedChain::Unichain, UNICHAIN_DOMAIN_ID)] + fn test_cctp_domain_id_supported_chains(#[case] chain: NamedChain, #[case] expected: u32) { + assert_eq!(chain.cctp_domain_id().unwrap(), expected); + } + + #[test] + fn test_cctp_domain_id_unsupported_chain() { + let result = NamedChain::BinanceSmartChain.cctp_domain_id(); + assert!(result.is_err()); + assert!(matches!( + result.unwrap_err(), + Error::ChainNotSupported { .. } + )); + } + + #[rstest] + #[case(NamedChain::Arbitrum, ARBITRUM_TOKEN_MESSENGER_ADDRESS)] + #[case(NamedChain::ArbitrumSepolia, ARBITRUM_SEPOLIA_TOKEN_MESSENGER_ADDRESS)] + #[case(NamedChain::Avalanche, AVALANCHE_TOKEN_MESSENGER_ADDRESS)] + #[case(NamedChain::Base, BASE_TOKEN_MESSENGER_ADDRESS)] + #[case(NamedChain::BaseSepolia, BASE_SEPOLIA_TOKEN_MESSENGER_ADDRESS)] + #[case(NamedChain::Sepolia, ETHEREUM_SEPOLIA_TOKEN_MESSENGER_ADDRESS)] + #[case(NamedChain::Mainnet, ETHEREUM_TOKEN_MESSENGER_ADDRESS)] + #[case(NamedChain::Optimism, OPTIMISM_TOKEN_MESSENGER_ADDRESS)] + #[case(NamedChain::Polygon, POLYGON_CCTP_TOKEN_MESSENGER)] + #[case(NamedChain::Unichain, UNICHAIN_CCTP_TOKEN_MESSENGER)] + fn test_token_messenger_address_supported_chains( + #[case] chain: NamedChain, + #[case] expected_addr: alloy_primitives::Address, + ) -> anyhow::Result<()> { + let result: alloy_primitives::Address = chain.token_messenger_address()?.try_into()?; + assert_eq!(result, expected_addr); + Ok(()) + } + + #[rstest] + #[case(SOLANA_DEVNET, SOLANA_DEVNET_USDC_TOKEN)] + #[case(SOLANA_MAINNET, SOLANA_MAINNET_USDC_TOKEN)] + fn test_sol_usdc_address( + #[case] chain: Chain, + #[case] expected_addr: solana_pubkey::Pubkey, + ) -> anyhow::Result<()> { + let result: solana_pubkey::Pubkey = chain.usdc_token_address()?.try_into()?; + assert_eq!(result, expected_addr); + Ok(()) + } + + #[rstest] + #[case(SOLANA_DEVNET, nitrogen_circle_token_messenger_minter_v2_encoder::ID)] + #[case(SOLANA_MAINNET, nitrogen_circle_token_messenger_minter_v2_encoder::ID)] + fn test_sol_token_messenger_address_supported_chains( + #[case] chain: Chain, + #[case] expected_addr: solana_pubkey::Pubkey, + ) -> anyhow::Result<()> { + let result: solana_pubkey::Pubkey = chain.token_messenger_address()?.try_into()?; + assert_eq!(result, expected_addr); + Ok(()) + } + + #[rstest] + #[case(SOLANA_DEVNET, nitrogen_circle_message_transmitter_v2_encoder::ID)] + #[case(SOLANA_MAINNET, nitrogen_circle_message_transmitter_v2_encoder::ID)] + fn test_sol_messenger_address_supported_chains( + #[case] chain: Chain, + #[case] expected_addr: solana_pubkey::Pubkey, + ) -> anyhow::Result<()> { + let result: solana_pubkey::Pubkey = chain.message_transmitter_address()?.try_into()?; + assert_eq!(result, expected_addr); + Ok(()) + } + + #[test] + fn test_token_messenger_address_unsupported_chain() { + let result = NamedChain::BinanceSmartChain.token_messenger_address(); + assert!(result.is_err()); + assert!(matches!( + result.unwrap_err(), + Error::ChainNotSupported { .. } + )); + } + + #[rstest] + #[case(NamedChain::Arbitrum, ARBITRUM_MESSAGE_TRANSMITTER_ADDRESS)] + #[case(NamedChain::Avalanche, AVALANCHE_MESSAGE_TRANSMITTER_ADDRESS)] + #[case(NamedChain::Base, BASE_MESSAGE_TRANSMITTER_ADDRESS)] + #[case(NamedChain::Mainnet, ETHEREUM_MESSAGE_TRANSMITTER_ADDRESS)] + #[case(NamedChain::Optimism, OPTIMISM_MESSAGE_TRANSMITTER_ADDRESS)] + #[case(NamedChain::Polygon, POLYGON_CCTP_MESSAGE_TRANSMITTER)] + #[case( + NamedChain::ArbitrumSepolia, + ARBITRUM_SEPOLIA_MESSAGE_TRANSMITTER_ADDRESS + )] + #[case(NamedChain::BaseSepolia, BASE_SEPOLIA_MESSAGE_TRANSMITTER_ADDRESS)] + #[case(NamedChain::Sepolia, ETHEREUM_SEPOLIA_MESSAGE_TRANSMITTER_ADDRESS)] + #[case(NamedChain::Unichain, UNICHAIN_CCTP_MESSAGE_TRANSMITTER)] + fn test_message_transmitter_address_supported_chains( + #[case] chain: NamedChain, + #[case] expected_addr: alloy_primitives::Address, + ) -> anyhow::Result<()> { + let result: alloy_primitives::Address = + chain.message_transmitter_address().unwrap().try_into()?; + assert_eq!(result, expected_addr); + Ok(()) + } + + #[test] + fn test_message_transmitter_address_unsupported_chain() { + let result = NamedChain::BinanceSmartChain.message_transmitter_address(); + assert!(result.is_err()); + assert!(matches!( + result.unwrap_err(), + Error::ChainNotSupported { .. } + )); + } + + #[test] + fn test_address_parsing_validation() { + // All addresses should be valid Ethereum addresses + for chain in [ + NamedChain::Mainnet, + NamedChain::Arbitrum, + NamedChain::Base, + NamedChain::Optimism, + NamedChain::Unichain, + NamedChain::Avalanche, + NamedChain::Polygon, + NamedChain::Sepolia, + NamedChain::ArbitrumSepolia, + NamedChain::BaseSepolia, + ] { + assert!( + chain.token_messenger_address().is_ok(), + "Token messenger address should be valid for {chain:?}" + ); + assert!( + chain.message_transmitter_address().is_ok(), + "Message transmitter address should be valid for {chain:?}" + ); + } + } +} diff --git a/tests/integration.rs b/tests/integration.rs index c185a6a..18b6892 100644 --- a/tests/integration.rs +++ b/tests/integration.rs @@ -44,11 +44,13 @@ pub fn evm_setup(base_sepolia: bool) -> anyhow::Result anyhow::Result<(Keypair, RpcClient)> { let kp_file = env::var("KEYPAIR_FILE").ok(); let owner = if let Some(kp) = kp_file { @@ -82,13 +84,11 @@ async fn test_reclaim() -> Result<()> { } #[tokio::test] -async fn test_evm() -> Result<()> { +async fn test_evm_bridge() -> Result<()> { setup(); let sepolia_provider = evm_setup(false)?; let base_provider = evm_setup(true)?; let recipient = base_provider.default_signer_address(); - info!("evm address {recipient}"); - let bridge = Cctp::new( sepolia_provider, base_provider, @@ -101,6 +101,31 @@ async fn test_evm() -> Result<()> { Ok(()) } +#[tokio::test] +async fn test_evm_burn_recv_split() -> Result<()> { + setup(); + let sepolia_provider = evm_setup(false)?; + let base_provider = evm_setup(true)?; + let recipient = base_provider.default_signer_address(); + + let bridge = Cctp::new( + sepolia_provider, + base_provider, + NamedChain::Sepolia, + NamedChain::BaseSepolia, + recipient, + ); + let (burn_hash, approval_hash) = bridge.burn(U256::from(10), None, None, None).await?; + info!( + "burn {burn_hash} approval {}", + approval_hash.unwrap_or_default() + ); + + let (attest, recv_hash) = bridge.recv(burn_hash).await?; + info!("attest {attest} recv {recv_hash}"); + Ok(()) +} + #[tokio::test] async fn test_evm_sol() -> Result<()> { setup();