Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 46 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,52 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [2.0.0] - 2025-11-03

### Added

- **Trait-based architecture** enabling comprehensive testing through dependency injection
- New `traits` module with `BlockchainProvider`, `AttestationProvider`, and `Clock` traits
- New `providers` module with production implementations:
- `AlloyProvider` - Wraps Alloy RPC providers for blockchain operations
- `IrisAttestationProvider` - Production/sandbox Circle API client
- `TokioClock` - Real tokio::time-based clock implementation
- New `receipt_adapter` module with `ReceiptAdapter` trait for network-specific receipt handling
- `EthereumReceiptAdapter` implementation for Ethereum-compatible networks
- Comprehensive example in `examples/test_fakes.rs` demonstrating fake implementations for testing
- `AttestationResponse` now derives `Clone` to support test scenarios

### Changed

- **BREAKING**: `Cctp` struct now has 7 type parameters (up from 2) for full dependency injection:
- `SN` - Source network type
- `DN` - Destination network type
- `SP` - Source blockchain provider
- `DP` - Destination blockchain provider
- `A` - Attestation provider
- `C` - Clock implementation
- `RA` - Receipt adapter
- **BREAKING**: Builder API now requires explicit provider injection:
- Must wrap RPC providers with `AlloyProvider::new(provider)`
- Must provide `attestation_provider(IrisAttestationProvider::production())`
- Must provide `clock(TokioClock::new())`
- Must provide `receipt_adapter(EthereumReceiptAdapter)`
- **BREAKING**: Updated to Alloy 1.0+ API conventions
- All examples updated to use new trait-based API
- Library documentation updated with migration guide

### Benefits

- Full testability with ability to inject fake implementations for adversarial testing
- Time control in tests without actual waiting
- Network flexibility through receipt adapter abstraction
- Type-safe external dependencies
- Maintained backward compatibility for chain configurations and contract addresses

### Migration

See the [Migration Guide](README.md#migration-guide-from-version-1x-to-200) in README.md for detailed upgrade instructions.

## [0.4.0] - 2025-10-14

### Changed
Expand Down
3 changes: 2 additions & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 5 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "cctp-rs"
version = "0.5.1"
version = "2.0.0"
edition = "2021"
authors = ["Joseph Livesey <[email protected]>"]
categories = ["finance", "api-bindings"]
Expand Down Expand Up @@ -28,6 +28,7 @@ alloy-provider = { version = "1.0.41", default-features = false, features = [
alloy-rpc-types = "1.0.42"
alloy-sol-types = { version = "1.4.0", features = ["json"] }
alloy-transport = { version = "1.0.41", default-features = false }
async-trait = "0.1"
bon = "3.8.1"
reqwest = { version = "0.12.23", features = ["json"] }
serde = { version = "1.0.228", features = ["derive"] }
Expand All @@ -36,6 +37,9 @@ thiserror = "2.0.17"
tokio = { version = "1", default-features = false, features = ["time"] }
tracing = "0.1.41"

[features]
test-utils = []

[dev-dependencies]
rstest = "0.26"
tokio = { version = "1.0", features = [
Expand Down
115 changes: 101 additions & 14 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,15 @@

A production-ready Rust implementation of Circle's Cross-Chain Transfer Protocol (CCTP), enabling seamless USDC transfers across blockchain networks.

> **Note:** This is version 2.0.0 of cctp-rs (the crate), which introduces a trait-based architecture for comprehensive testing. This refers to the library version, not the CCTP protocol version (which is v1). See the [Migration Guide](#migration-guide-from-version-1x-to-200) below for upgrading from earlier versions.

## Features

- 🧪 **Trait-based architecture** - Full dependency injection for comprehensive testing
- 🚀 **Type-safe** contract interactions using Alloy
- 🔄 **Multi-chain support** for mainnet and testnet networks
- 📦 **Builder pattern** for intuitive API usage
- 🎯 **Test fakes included** - Built-in fake implementations for testing

## Supported Chains

Expand Down Expand Up @@ -40,42 +44,45 @@ Add to your `Cargo.toml`:

```toml
[dependencies]
cctp-rs = "0.3.0"
cctp-rs = "2.0"
```

### Basic Example

```rust
use cctp_rs::{Cctp, CctpError};
use cctp_rs::providers::{AlloyProvider, IrisAttestationProvider, TokioClock};
use alloy_chains::NamedChain;
use alloy_primitives::{Address, U256};
use alloy_provider::{Provider, ProviderBuilder};
use alloy_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()?);
.on_builtin("https://eth-mainnet.g.alchemy.com/v2/YOUR_API_KEY").await?;

let arb_provider = ProviderBuilder::new()
.on_http("https://arb-mainnet.g.alchemy.com/v2/YOUR_API_KEY".parse()?);
.on_builtin("https://arb-mainnet.g.alchemy.com/v2/YOUR_API_KEY").await?;

// Set up the CCTP bridge
// Set up the CCTP bridge with production providers
let bridge = Cctp::builder()
.source_chain(NamedChain::Mainnet)
.destination_chain(NamedChain::Arbitrum)
.source_provider(eth_provider)
.destination_provider(arb_provider)
.source_provider(AlloyProvider::new(eth_provider))
.destination_provider(AlloyProvider::new(arb_provider))
.attestation_provider(IrisAttestationProvider::production())
.clock(TokioClock::new())
.receipt_adapter(cctp_rs::EthereumReceiptAdapter)
.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(())
}
```
Expand Down Expand Up @@ -121,11 +128,14 @@ async fn bridge_usdc(bridge: &Cctp<impl Provider>) -> Result<(), CctpError> {

The library is organized into several key modules:

- **`bridge`** - Core CCTP bridge implementation
- **`chain`** - Chain-specific configurations and support
- **`bridge`** - Core CCTP bridge implementation with dependency injection
- **`chain`** - Chain-specific configurations and the `CctpV1` trait
- **`traits`** - Core trait abstractions (`BlockchainProvider`, `AttestationProvider`, `Clock`)
- **`providers`** - Production implementations (`AlloyProvider`, `IrisAttestationProvider`, `TokioClock`)
- **`receipt_adapter`** - Network-specific receipt handling (e.g., `EthereumReceiptAdapter`)
- **`attestation`** - Attestation response types from Circle's Iris API
- **`error`** - Comprehensive error types for proper error handling
- **`contracts`** - Type-safe bindings for TokenMessenger and MessageTransmitter
- **`message_transmitter`** / **`token_messenger`** - Type-safe contract bindings

## Error Handling

Expand Down Expand Up @@ -172,18 +182,95 @@ println!("Domain ID: {}", domain_id);
println!("Token Messenger: {}", token_messenger);
```

## Migration Guide: From Version 1.x to 2.0.0

Version 2.0.0 introduces breaking changes to enable comprehensive testing through dependency injection. All external I/O operations are now abstracted behind traits.

### What Changed

The `Cctp` struct now has 7 type parameters instead of 2, enabling you to inject custom implementations for:
- Blockchain providers (RPC calls)
- Attestation providers (Circle API calls)
- Clock (time operations)
- Receipt adapters (network-specific receipt handling)

### Migration Steps

**Version 1.x code:**
```rust
use cctp_rs::{Cctp, CctpError};
use alloy_provider::ProviderBuilder;

let eth_provider = ProviderBuilder::new()
.on_http("https://eth.llamarpc.com".parse()?);

let arb_provider = ProviderBuilder::new()
.on_http("https://arbitrum.llamarpc.com".parse()?);

let bridge = Cctp::builder()
.source_chain(NamedChain::Mainnet)
.destination_chain(NamedChain::Arbitrum)
.source_provider(eth_provider)
.destination_provider(arb_provider)
.recipient("0x...".parse()?)
.build();
```

**Version 2.0.0 code:**
```rust
use cctp_rs::{Cctp, CctpError, EthereumReceiptAdapter};
use cctp_rs::providers::{AlloyProvider, IrisAttestationProvider, TokioClock};
use alloy_provider::ProviderBuilder;

let eth_provider = ProviderBuilder::new()
.on_builtin("https://eth.llamarpc.com").await?;

let arb_provider = ProviderBuilder::new()
.on_builtin("https://arbitrum.llamarpc.com").await?;

let bridge = Cctp::builder()
.source_chain(NamedChain::Mainnet)
.destination_chain(NamedChain::Arbitrum)
.source_provider(AlloyProvider::new(eth_provider)) // Wrap with AlloyProvider
.destination_provider(AlloyProvider::new(arb_provider)) // Wrap with AlloyProvider
.attestation_provider(IrisAttestationProvider::production()) // Add attestation provider
.clock(TokioClock::new()) // Add clock
.receipt_adapter(EthereumReceiptAdapter) // Add receipt adapter
.recipient("0x...".parse()?)
.build();
```

### Key Changes

1. **Wrap RPC providers**: Use `AlloyProvider::new(provider)` to wrap Alloy providers
2. **Add attestation provider**: Use `IrisAttestationProvider::production()` for mainnet or `IrisAttestationProvider::sandbox()` for testnets
3. **Add clock**: Use `TokioClock::new()` for production
4. **Add receipt adapter**: Use `EthereumReceiptAdapter` for Ethereum-compatible networks
5. **Alloy API changes**: Use `.on_builtin()` instead of `.on_http()` (Alloy v1.0 change)

### Benefits of Version 2.0.0

- **Testability**: Inject fake implementations for comprehensive testing
- **Network flexibility**: Support for Optimism and other networks via custom receipt adapters
- **Time control**: Test timeout behavior without waiting
- **Type safety**: All external dependencies are explicit in the type signature

See [`examples/test_fakes.rs`](examples/test_fakes.rs) for examples of implementing test doubles.

## Examples

Check out the [`examples/`](examples/) directory for complete working examples:

- [`basic_bridge.rs`](examples/basic_bridge.rs) - Simple USDC bridge example
- [`attestation_monitoring.rs`](examples/attestation_monitoring.rs) - Monitor attestation status
- [`multi_chain.rs`](examples/multi_chain.rs) - Bridge across multiple chains
- [`test_fakes.rs`](examples/test_fakes.rs) - Comprehensive test fake implementations

Run examples with:

```bash
cargo run --example basic_bridge
cargo run --example test_fakes
```

## Contributing
Expand Down
25 changes: 19 additions & 6 deletions examples/attestation_monitoring.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,10 @@
//! Run with: `cargo run --example attestation_monitoring`

use alloy_chains::NamedChain;
use alloy_network::Ethereum;
use alloy_primitives::{FixedBytes, TxHash};
use alloy_provider::{Provider, ProviderBuilder};
use cctp_rs::{AttestationResponse, AttestationStatus, Cctp, CctpError};
use alloy_provider::ProviderBuilder;
use cctp_rs::providers::{AlloyProvider, IrisAttestationProvider, TokioClock};
use cctp_rs::{AttestationResponse, AttestationStatus, Cctp, CctpError, UniversalReceiptAdapter};
use std::time::Duration;
use tokio::time::sleep;

Expand All @@ -27,8 +27,11 @@ async fn main() -> Result<(), CctpError> {
let bridge = Cctp::builder()
.source_chain(NamedChain::Mainnet)
.destination_chain(NamedChain::Arbitrum)
.source_provider(eth_provider)
.destination_provider(arb_provider)
.source_provider(AlloyProvider::new(eth_provider))
.destination_provider(AlloyProvider::new(arb_provider))
.attestation_provider(IrisAttestationProvider::production())
.clock(TokioClock::new())
.receipt_adapter(UniversalReceiptAdapter)
.recipient(
"0x742d35Cc6634C0532925a3b844Bc9e7595f8fA0d"
.parse()
Expand Down Expand Up @@ -74,7 +77,17 @@ async fn main() -> Result<(), CctpError> {
}

/// Simulates monitoring attestation status changes
async fn simulate_attestation_monitoring(_bridge: &Cctp<impl Provider<Ethereum> + Clone>) {
async fn simulate_attestation_monitoring<SN, DN, SP, DP, A, C, RA>(
_bridge: &Cctp<SN, DN, SP, DP, A, C, RA>,
) where
SN: alloy_network::Network,
DN: alloy_network::Network,
SP: cctp_rs::traits::BlockchainProvider<SN>,
DP: cctp_rs::traits::BlockchainProvider<DN>,
A: cctp_rs::traits::AttestationProvider,
C: cctp_rs::traits::Clock,
RA: cctp_rs::ReceiptAdapter<SN>,
{
println!("\n📈 Simulating attestation status progression:");

let statuses = [
Expand Down
10 changes: 7 additions & 3 deletions examples/basic_bridge.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@
use alloy_chains::NamedChain;
use alloy_primitives::Address;
use alloy_provider::ProviderBuilder;
use cctp_rs::{Cctp, CctpError, CctpV1};
use cctp_rs::providers::{AlloyProvider, IrisAttestationProvider, TokioClock};
use cctp_rs::{Cctp, CctpError, CctpV1, UniversalReceiptAdapter};
use std::str::FromStr;

#[tokio::main]
Expand Down Expand Up @@ -37,8 +38,11 @@ async fn main() -> Result<(), CctpError> {
let bridge = Cctp::builder()
.source_chain(NamedChain::Mainnet)
.destination_chain(NamedChain::Arbitrum)
.source_provider(eth_provider)
.destination_provider(arb_provider)
.source_provider(AlloyProvider::new(eth_provider))
.destination_provider(AlloyProvider::new(arb_provider))
.attestation_provider(IrisAttestationProvider::production())
.clock(TokioClock::new())
.receipt_adapter(UniversalReceiptAdapter)
.recipient(recipient)
.build();

Expand Down
Loading