mod common;
use std::future::IntoFuture;
use alloy_network::EthereumWallet;
use alloy_primitives::U256;
use alloy_provider::{Provider, ProviderBuilder};
use alloy_signer_local::PrivateKeySigner;
use cctp_rs::{CctpError, CctpV2, CctpV2Bridge, Erc20Contract, MintResult, PollingConfig};
use common::funded_v2::{
format_eth_balance, format_usdc_balance, redacted_rpc_url, supported_testnet_routes,
FundedV2Route, DEFAULT_TRANSFER_AMOUNT, MIN_NATIVE_BALANCE_WEI, MIN_USDC_BALANCE,
};
use dotenvy::dotenv;
use tracing::{info_span, Instrument};
#[tokio::main]
async fn main() -> Result<(), CctpError> {
dotenv().ok();
tracing_subscriber::fmt()
.with_max_level(tracing::Level::INFO)
.init();
let route = FundedV2Route::from_env()?;
let source_chain = route.source_chain();
let destination_chain = route.destination_chain();
println!("๐งช CCTP v2 Standard Transfer: {source_chain} โ {destination_chain}");
println!("====================================================\n");
let private_key_str =
std::env::var("TESTNET_PRIVATE_KEY").expect("TESTNET_PRIVATE_KEY must be set in .env file");
let signer: PrivateKeySigner = private_key_str
.parse()
.expect("Invalid TESTNET_PRIVATE_KEY format");
let wallet_address = signer.address();
println!("๐ Configuration:");
println!(" Wallet: {wallet_address}");
println!(" Source: {source_chain}");
println!(" Destination: {destination_chain}");
println!(
" Supported testnet aliases: {}",
supported_testnet_routes()
);
println!(" Source RPC: {}", redacted_rpc_url(&route.source_rpc_url));
println!(
" Destination RPC: {}\n",
redacted_rpc_url(&route.destination_rpc_url)
);
let wallet = EthereumWallet::from(signer);
println!("1๏ธโฃ Creating blockchain providers...");
let source_provider = ProviderBuilder::new()
.wallet(wallet.clone())
.connect_http(route.source_rpc_url.clone());
let destination_provider = ProviderBuilder::new()
.wallet(wallet)
.connect_http(route.destination_rpc_url.clone());
println!(" โ
Providers created (with wallet signer)\n");
println!("2๏ธโฃ Checking balances...");
let source_usdc_contract = Erc20Contract::new(route.source_usdc_address, &source_provider);
let destination_usdc_contract =
Erc20Contract::new(route.destination_usdc_address, &destination_provider);
let (
source_eth_balance,
source_usdc_balance,
destination_eth_balance,
destination_usdc_balance,
) = tokio::try_join!(
async {
source_provider
.get_balance(wallet_address)
.into_future()
.instrument(info_span!("get_eth_balance", chain = %source_chain))
.await
.map_err(CctpError::from)
},
async {
source_usdc_contract
.balance_of(wallet_address)
.instrument(info_span!("get_usdc_balance", chain = %source_chain))
.await
.map_err(CctpError::from)
},
async {
destination_provider
.get_balance(wallet_address)
.into_future()
.instrument(info_span!("get_eth_balance", chain = %destination_chain))
.await
.map_err(CctpError::from)
},
async {
destination_usdc_contract
.balance_of(wallet_address)
.instrument(info_span!("get_usdc_balance", chain = %destination_chain))
.await
.map_err(CctpError::from)
},
)?;
println!(" {source_chain}:");
println!(
" ETH Balance: {} ETH",
format_eth_balance(source_eth_balance)
);
println!(
" USDC Balance: {} USDC",
format_usdc_balance(source_usdc_balance)
);
println!(" {destination_chain}:");
println!(
" ETH Balance: {} ETH",
format_eth_balance(destination_eth_balance)
);
println!(
" USDC Balance: {} USDC",
format_usdc_balance(destination_usdc_balance)
);
println!(" โ
Balances retrieved\n");
println!("๐ Balance Summary:");
println!("โโโโโโโโโโโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโโโโโ");
println!("โ Chain โ ETH Balance โ USDC Balance โ");
println!("โโโโโโโโโโโโโโโโโโโโโโโผโโโโโโโโโโโโโโโโโโโผโโโโโโโโโโโโโโโโโโโค");
println!(
"โ {:<19} โ {:>16} โ {:>16} โ",
source_chain.to_string(),
format_eth_balance(source_eth_balance),
format_usdc_balance(source_usdc_balance)
);
println!(
"โ {:<19} โ {:>16} โ {:>16} โ",
destination_chain.to_string(),
format_eth_balance(destination_eth_balance),
format_usdc_balance(destination_usdc_balance)
);
println!("โโโโโโโโโโโโโโโโโโโโโโโดโโโโโโโโโโโโโโโโโโโดโโโโโโโโโโโโโโโโโโโ\n");
let min_eth = U256::from(MIN_NATIVE_BALANCE_WEI); let min_usdc = U256::from(MIN_USDC_BALANCE);
let mut issues: Vec<String> = Vec::new();
if source_eth_balance < min_eth {
issues.push(format!(
"โ Insufficient native gas token on {source_chain}: {} (need >= 0.001)\n \
โ Fund source gas before running with EXECUTE_TRANSFER=true",
format_eth_balance(source_eth_balance)
));
}
if destination_eth_balance < min_eth {
issues.push(format!(
"โ Insufficient native gas token on {destination_chain}: {} (need >= 0.001)\n \
โ Fund destination gas in case this run needs to self-relay",
format_eth_balance(destination_eth_balance)
));
}
if source_usdc_balance < min_usdc {
issues.push(format!(
"โ Insufficient USDC on {source_chain}: {} (need >= 1 USDC)\n \
โ Get testnet USDC: https://faucet.circle.com/",
format_usdc_balance(source_usdc_balance)
));
}
if !issues.is_empty() {
println!("โ ๏ธ Cannot proceed - insufficient balances:\n");
for issue in &issues {
println!(" {issue}\n");
}
println!("Please fund your wallet and try again.");
return Ok(());
}
println!("โ
All balance requirements met!\n");
println!("๐ Dry run complete. To execute the actual transfer:");
println!(" Set the environment variable: EXECUTE_TRANSFER=true");
println!(" Then run: cargo run --example testnet_validation\n");
if std::env::var("EXECUTE_TRANSFER").unwrap_or_default() != "true" {
return Ok(());
}
println!("๐ EXECUTE_TRANSFER=true detected, proceeding with transfer...\n");
println!("4๏ธโฃ Setting up CCTP v2 bridge...");
let bridge = CctpV2Bridge::builder()
.source_chain(source_chain)
.destination_chain(destination_chain)
.source_provider(source_provider)
.destination_provider(destination_provider)
.recipient(wallet_address)
.build();
println!(" โ
Bridge created\n");
println!("5๏ธโฃ Bridge Configuration:");
println!(" Transfer Type: Standard");
println!(" Finality Threshold: {}", bridge.finality_threshold());
println!(" Fast Transfer: {}", bridge.is_fast_transfer());
println!(" Expected Settlement: 10-15 minutes\n");
println!("6๏ธโฃ Domain ID Validation:");
let source_domain = bridge.source_chain().cctp_v2_domain_id()?;
let dest_domain = bridge.destination_domain_id()?;
println!(" Source Domain ({source_chain}): {source_domain}");
println!(" Destination Domain ({destination_chain}): {dest_domain}");
println!(" โ
Domain IDs correct\n");
println!("7๏ธโฃ Contract Addresses:");
let token_messenger = bridge.token_messenger_v2_contract()?;
let message_transmitter = bridge.message_transmitter_v2_contract()?;
println!(" TokenMessenger: {token_messenger}");
println!(" MessageTransmitter: {message_transmitter}");
println!(" โ
Addresses correct\n");
println!("8๏ธโฃ API Endpoint:");
let api_url = bridge.api_url();
println!(" {}", api_url.as_str());
assert!(
api_url.as_str().contains("sandbox"),
"Should use sandbox API for testnet"
);
println!(" โ
Using sandbox API\n");
let amount = U256::from(DEFAULT_TRANSFER_AMOUNT);
println!("9๏ธโฃ Transfer Details:");
println!(" Token: USDC ({source_chain})");
println!(" Source Token Address: {}", route.source_usdc_address);
println!(
" Destination Token Address: {}",
route.destination_usdc_address
);
println!(" Amount: 1.0 USDC");
println!(" From: {wallet_address}");
println!(" To: {wallet_address} (same address on {destination_chain})\n");
println!("\n๐ Starting Transfer...\n");
println!("๐ Approval Phase:");
println!(" Checking TokenMessenger allowance...");
let token_messenger = bridge.token_messenger_v2_contract()?;
let current_allowance = bridge
.get_allowance(route.source_usdc_address, wallet_address)
.await?;
println!(
" Current allowance: {} USDC",
format_usdc_balance(current_allowance)
);
println!(" TokenMessenger: {token_messenger}");
if current_allowance < amount {
println!(" โ ๏ธ Insufficient allowance, sending approval transaction...");
let approval_tx = bridge
.approve(route.source_usdc_address, wallet_address, amount)
.await?;
println!(" โ
Approval TX: {approval_tx}");
println!(" Explorer: {}", route.source_tx_url(approval_tx));
println!(" Waiting for approval confirmation...");
tokio::time::sleep(tokio::time::Duration::from_secs(5)).await;
} else {
println!(" โ
Sufficient allowance already granted");
}
println!("\n1๏ธโฃ1๏ธโฃ Burn Phase:");
println!(" Burning 1 USDC on {source_chain}...");
let burn_tx = bridge
.burn(amount, wallet_address, route.source_usdc_address)
.await?;
println!(" โ
Burn TX: {burn_tx}");
println!(" Explorer: {}", route.source_tx_url(burn_tx));
println!("\n1๏ธโฃ2๏ธโฃ Attestation Phase:");
println!(" Polling Circle API for attestation and message...");
println!(" This typically takes the standard finality window for {source_chain}.");
println!(" Progress will be shown every 60 seconds.\n");
let (message, attestation) = bridge
.get_attestation(burn_tx, PollingConfig::default())
.await?;
println!("\n โ
Attestation and message received!");
println!(" Message length: {} bytes", message.len());
println!(" Attestation length: {} bytes", attestation.len());
println!("\n1๏ธโฃ3๏ธโฃ Mint Phase:");
println!(" Minting 1 USDC on {destination_chain}...");
let mint_result = bridge
.mint_if_needed(message, attestation, wallet_address)
.await?;
let mint_summary = match mint_result {
MintResult::Minted(mint_tx) => {
println!(" โ
Mint TX: {mint_tx}");
println!(" Explorer: {}", route.destination_tx_url(mint_tx));
mint_tx.to_string()
}
MintResult::AlreadyRelayed => {
println!(" โ
Already relayed by a third party");
"already relayed".to_string()
}
};
println!("\n๐ Transfer Complete!");
println!(
" Your 1 USDC has been successfully bridged from {source_chain} to {destination_chain}."
);
println!("\n Summary:");
println!(" - Burn TX: {burn_tx}");
println!(" - Mint result: {mint_summary}");
println!("\nโ
CCTP v2 Testnet Validation: PASSED");
Ok(())
}