| Crates.io | cctp-rs |
| lib.rs | cctp-rs |
| version | 2.1.1 |
| created_at | 2025-05-11 17:05:31.673459+00 |
| updated_at | 2026-01-19 19:09:23.331674+00 |
| description | Rust SDK for CCTP |
| homepage | https://crates.io/crates/cctp-rs |
| repository | https://github.com/semiotic-ai/cctp-rs |
| max_upload_size | |
| id | 1669546 |
| size | 651,855 |
A production-ready Rust implementation of Circle's Cross-Chain Transfer Protocol (CCTP), enabling seamless USDC transfers across blockchain networks.
Also supported for backwards compatibility
Add to your Cargo.toml:
[dependencies]
cctp-rs = "2"
use cctp_rs::{Cctp, CctpError};
use alloy_chains::NamedChain;
use alloy_primitives::{Address, U256};
use alloy_provider::{Provider, ProviderBuilder};
#[tokio::main]
async fn main() -> Result<(), CctpError> {
// Create providers for source and destination chains
let eth_provider = ProviderBuilder::new()
.on_http("https://eth-mainnet.g.alchemy.com/v2/YOUR_API_KEY".parse()?);
let arb_provider = ProviderBuilder::new()
.on_http("https://arb-mainnet.g.alchemy.com/v2/YOUR_API_KEY".parse()?);
// Set up the CCTP bridge
let bridge = Cctp::builder()
.source_chain(NamedChain::Mainnet)
.destination_chain(NamedChain::Arbitrum)
.source_provider(eth_provider)
.destination_provider(arb_provider)
.recipient("0xYourRecipientAddress".parse()?)
.build();
// Get contract addresses
let token_messenger = bridge.token_messenger_contract()?;
let destination_domain = bridge.destination_domain_id()?;
println!("Token Messenger: {}", token_messenger);
println!("Destination Domain: {}", destination_domain);
Ok(())
}
use cctp_rs::{Cctp, CctpError, PollingConfig};
use alloy_chains::NamedChain;
use alloy_primitives::{Address, U256};
use alloy_provider::Provider;
async fn bridge_usdc_v1<P: Provider + Clone>(bridge: &Cctp<P>) -> Result<(), CctpError> {
// Step 1: Burn USDC on source chain (get tx hash from your burn transaction)
let burn_tx_hash = "0x...".parse()?;
// Step 2: Get message and message hash from the burn transaction
let (message, message_hash) = bridge.get_message_sent_event(burn_tx_hash).await?;
// Step 3: Wait for attestation from Circle's API
let attestation = bridge.get_attestation(message_hash, PollingConfig::default()).await?;
println!("V1 Bridge successful!");
println!("Message: {} bytes", message.len());
println!("Attestation: {} bytes", attestation.len());
// Step 4: Mint on destination chain using message + attestation
// mint_on_destination(&message, &attestation).await?;
Ok(())
}
use cctp_rs::{CctpV2Bridge, CctpError, PollingConfig};
use alloy_chains::NamedChain;
use alloy_primitives::{Address, U256};
use alloy_provider::Provider;
async fn bridge_usdc_v2<P: Provider + Clone>(bridge: &CctpV2Bridge<P>) -> Result<(), CctpError> {
// Step 1: Burn USDC on source chain (get tx hash from your burn transaction)
let burn_tx_hash = "0x...".parse()?;
// Step 2: Get canonical message AND attestation from Circle's API
// Note: V2 returns both because the on-chain message has zeros in the nonce field
let (message, attestation) = bridge.get_attestation(
burn_tx_hash,
PollingConfig::fast_transfer(), // Optimized for v2 fast transfers
).await?;
println!("V2 Bridge successful!");
println!("Message: {} bytes", message.len());
println!("Attestation: {} bytes", attestation.len());
// Step 3: Mint on destination chain using message + attestation
// bridge.mint(message, attestation, recipient).await?;
Ok(())
}
The library is organized into several key modules:
bridge - Core CCTP bridge implementationchain - Chain-specific configurations and supportattestation - Attestation response types from Circle's Iris APIerror - Comprehensive error types for proper error handlingcontracts - Type-safe bindings for TokenMessenger and MessageTransmittercctp-rs provides detailed error types for different failure scenarios:
use cctp_rs::{CctpError, PollingConfig};
// V1 example
match bridge.get_attestation(message_hash, PollingConfig::default()).await {
Ok(attestation) => println!("Success: {} bytes", attestation.len()),
Err(CctpError::AttestationTimeout) => println!("Timeout waiting for attestation"),
Err(CctpError::UnsupportedChain(chain)) => println!("Chain {chain:?} not supported"),
Err(e) => println!("Other error: {}", e),
}
// V2 example (returns both message and attestation)
match v2_bridge.get_attestation(tx_hash, PollingConfig::fast_transfer()).await {
Ok((message, attestation)) => {
println!("Message: {} bytes", message.len());
println!("Attestation: {} bytes", attestation.len());
}
Err(CctpError::AttestationTimeout) => println!("Timeout waiting for attestation"),
Err(e) => println!("Error: {}", e),
}
use cctp_rs::PollingConfig;
// V1: Wait up to 10 minutes with 30-second intervals
let attestation = bridge.get_attestation(
message_hash,
PollingConfig::default()
.with_max_attempts(20)
.with_poll_interval_secs(30),
).await?;
// V2: Use preset for fast transfers (5 second intervals)
let (message, attestation) = v2_bridge.get_attestation(
tx_hash,
PollingConfig::fast_transfer(),
).await?;
// V2: Or customize for your needs
let (message, attestation) = v2_bridge.get_attestation(
tx_hash,
PollingConfig::default()
.with_max_attempts(60)
.with_poll_interval_secs(10),
).await?;
// Check total timeout
let config = PollingConfig::default();
println!("Max wait time: {} seconds", config.total_timeout_secs());
use cctp_rs::{CctpV1, CctpV2};
use alloy_chains::NamedChain;
// Get v1 chain-specific information
let chain = NamedChain::Arbitrum;
let confirmation_time = chain.confirmation_average_time_seconds()?; // Standard: 19 minutes
let domain_id = chain.cctp_domain_id()?;
let token_messenger = chain.token_messenger_address()?;
println!("Arbitrum V1 confirmation time: {} seconds", confirmation_time);
// Get v2 attestation times (choose based on transfer mode)
let fast_time = chain.fast_transfer_confirmation_time_seconds()?; // ~8 seconds
let standard_time = chain.standard_transfer_confirmation_time_seconds()?; // ~19 minutes
println!("V2 Fast Transfer: {} seconds", fast_time);
println!("V2 Standard Transfer: {} seconds", standard_time);
CCTP v2 is permissionless - anyone can relay a message once Circle's attestation is available. Third-party relayers (Synapse, LI.FI, etc.) actively monitor for burns and may complete transfers before your application does. This is a feature, not a bug!
If you don't need to self-relay, just wait for the transfer to complete:
use cctp_rs::{CctpV2Bridge, PollingConfig};
async fn wait_for_transfer<P: Provider + Clone>(bridge: &CctpV2Bridge<P>) -> Result<(), CctpError> {
let burn_tx = bridge.burn(amount, from, usdc).await?;
let (message, _attestation) = bridge.get_attestation(
burn_tx,
PollingConfig::fast_transfer(),
).await?;
// Wait for completion (by relayer or self)
bridge.wait_for_receive(&message, None, None).await?;
println!("Transfer complete!");
Ok(())
}
If you want to try minting yourself but handle relayer races:
use cctp_rs::{CctpV2Bridge, MintResult, PollingConfig};
async fn self_relay<P: Provider + Clone>(bridge: &CctpV2Bridge<P>) -> Result<(), CctpError> {
let burn_tx = bridge.burn(amount, from, usdc).await?;
let (message, attestation) = bridge.get_attestation(
burn_tx,
PollingConfig::fast_transfer(),
).await?;
match bridge.mint_if_needed(message, attestation, from).await? {
MintResult::Minted(tx) => println!("We minted: {tx}"),
MintResult::AlreadyRelayed => println!("Relayer completed it for us!"),
}
Ok(())
}
let is_complete = bridge.is_message_received(&message).await?;
if is_complete {
println!("Transfer already completed by relayer");
}
Check out the examples/ directory for complete working examples:
v2_integration_validation.rs - Comprehensive v2 validation (no network required)v2_standard_transfer.rs - Standard transfer with finalityv2_fast_transfer.rs - Fast transfer (<30s settlement)basic_bridge.rs - Simple USDC bridge exampleattestation_monitoring.rs - Monitor attestation statusmulti_chain.rs - Bridge across multiple chainsRun examples with:
# Recommended: Run v2 integration validation
cargo run --example v2_integration_validation
# Or run specific examples
cargo run --example v2_fast_transfer
cargo run --example basic_bridge
Contributions are welcome! Please feel free to submit a Pull Request.
git checkout -b feature/amazing-feature)git commit -m 'feat: add some amazing feature')git push origin feature/amazing-feature)Run the full test suite with:
cargo test --all-features
All 155 unit tests validate:
We provide comprehensive runnable examples that validate the complete v2 API without requiring network access:
# Validate all v2 configurations (no network required)
cargo run --example v2_integration_validation
# Educational examples showing complete flows
cargo run --example v2_standard_transfer
cargo run --example v2_fast_transfer
The v2_integration_validation example validates:
For pre-release validation on testnet:
Note: Integration tests requiring Circle's Iris API and live blockchains are not run in CI due to:
Instead, we validate via extensive unit tests and runnable examples. This approach ensures reliability while maintaining fast CI/CD pipelines.
This project is licensed under the Apache License 2.0 - see the LICENSE file for details.