All notable changes to this project will be documented in this file.
The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.
- New
TransferModeenum that names the four valid CCTP v2 burn configurations (Standard,Fast { max_fee },StandardWithHook { hook_data },FastWithHook { max_fee, hook_data }). Selected via a singletransfer_modebuilder field onCctpV2Bridge. CctpV2Bridge::transfer_mode()accessor.ParsedV2MessageSummarynow carries the canonical 32-byte header words assender_bytes,recipient_bytes, anddestination_caller_bytes. These are always populated and are the authoritative source of truth for non-EVM domains such asDomainId::SolanaandDomainId::StarknetTestnet. Tracks issue #219.- New
CctpError::ReceiveTimeoutvariant, distinct fromCctpError::AttestationTimeout. Returned byCctpV2Bridge::wait_for_receivewhen polling for destination-chain receipt confirmation exhausts its attempt budget — at that point attestation has already succeeded, so the failure is observing the message as received in time, not waiting on attestation. Both variants continue to satisfyCctpError::is_timeout()and thereforeis_transient(). Tracks issue #217. - New
cctp_rs.wait_for_receiveOpenTelemetry span emitted aroundCctpV2Bridge::wait_for_receive's polling loop, plus a publicspans::wait_for_receiveconstructor. On polling exhaustion the span recordserror.type = "ReceiveTimeout", making Tempo/Jaeger queries like{ span.error.type = "ReceiveTimeout" }count receive-side timeouts separately from attestation-side ones. The existingwait_for_receive_timeoutlog event also gains anerror_type = "ReceiveTimeout"field for log-stream parity withattestation_timeout. Tracks issue #240.
CctpV2Bridge::wait_for_receivenow returnsCctpError::ReceiveTimeoutinstead ofCctpError::AttestationTimeoutwhen the receive-confirmation polling loop exhausts its attempts.CctpErroris#[non_exhaustive], so this is not a compile-time break, but downstream code that pattern-matches onCctpError::AttestationTimeoutto detect await_for_receivetimeout must update its match to cover the new variant (or rely onis_timeout()/is_transient(), which already include both). Tracks issue #217.- Breaking:
ParsedV2MessageSummary::senderandrecipientare nowOption<Address>instead ofAddress, anddestination_callerisNonefor non-EVM destinations as well as for permissionless messages. The EVMAddressprojections are populated only when the correspondingDomainId::is_evm()is true; consumers that need the raw header word for non-EVM domains should read the new*_bytesfields. JSON output omits the EVM fields entirely for non-EVM domains (viaskip_serializing_if), so it no longer looks precise and EVM-shaped when it is not. Tracks issue #219. - Breaking:
MessageHeader::sender_addressandMessageHeader::recipient_addressnow returnOption<Address>(wasAddress), andMessageHeader::destination_caller_addressreturnsNonefor non-EVM destinations in addition to the existing permissionless case. The rawbytes32header fields remain authoritative for non-EVM domains. Tracks issue #219. - Breaking:
CctpV2Bridge::builder()no longer accepts the independentfast_transfer(bool),hook_data(Bytes), andmax_fee(U256)fields. Usetransfer_mode(TransferMode::…)instead. Theis_fast_transfer(),hook_data(),max_fee(), andfinality_threshold()accessors are preserved and now derive from the configured mode. Note thatmax_fee()returnsSome(U256::ZERO)(notNone) forTransferMode::Fast { max_fee: U256::ZERO }andTransferMode::FastWithHook { max_fee: U256::ZERO, .. }— the prior shape always returnedNonewhen the optional.max_fee(...)builder method was unset. - Breaking:
TokenMessengerV2Contract::deposit_for_burn_with_hooks_transactionnow takesmax_fee: U256andmin_finality_threshold: u32arguments. The previous signature hardcoded standard finality with zero fee, which excluded the hook + fast-finality burn that Circle'sdepositForBurnWithHooksupports natively. - Breaking:
TokenMessengerV2Contract::deposit_for_burn_transactionanddeposit_for_burn_fast_transactionnow take an additionalmin_finality_threshold: u32argument. The previous signatures hardcoded2000and1000internally, so the bridge could only guarantee the wireminFinalityThresholdmatchedtransfer_mode().finality_threshold()by routing eachTransferModeto the right helper. Parameterizing both letsCctpV2::burnderive every wire value from a singleself.transfer_mode.finality_threshold().as_u32()call and extends the issue #218 structural defense to all fourTransferModevariants. Tracks issue #224. - README and AGENTS.md now distinguish the bridge SDK's supported
chains (the set where
NamedChain::supports_cctp_v2()returnstrue) from the broader set of CCTP v2 domain IDs that the protocol parser can decode. The previous "26+ networks" claim conflated protocol parsing reach with bridge SDK reach; the bridge SDK currently routes 10 v2-capable chain families with testnets, while the parser recognizes all 21 announced domains including non-EVM ones. Tracks issue #216. - Breaking:
CctpV2::fast_transfer_fee_bpsnow returnsResult<FastTransferFee>instead ofResult<Option<u32>>, whereFastTransferFeeis a new public enum withKnown(u32)andUnknownvariants. Previous versions returnedOk(Some(0))for every v2-capable chain because per-chain fees had not yet been sourced — making unknown external protocol data look like a confirmed zero fee to downstream consumers. Until each chain's fee is sourced against an authoritative Circle reference, every chain now reportsFastTransferFee::Unknown. Callers handling user funds should fetch the live fee rather than treatingUnknownas zero. The enum is#[non_exhaustive]so future variants (e.g. a sourced-with-provenance form) can be added without another major break, and the trait method carries#[must_use]to surface accidental drops. Tracks issue #215.
- Breaking:
BurnMessageV2::decode,BurnMessageV2::parse,ParsedV2Message::decode, andParsedV2Message::parsenow reject messages whoseburn_token,mint_recipient, ormessage_senderbytes32words are not zero-padded in the leading 12 bytes. Previously such non-canonical inputs decoded successfully with the stray bytes dropped, sodecode(raw).encode() != rawandmessage_hash(decode(raw)) != keccak256(raw). The strict parser restores the round-trip and hash invariants for every accepted input.BurnMessageV2::parseandParsedV2Message::parsesurface a per-field error naming the offending word (burn_token,mint_recipient, ormessage_sender);decodereturnsNone. Downstream consumers that previously fed unvalidated bytes to these parsers must now handle the rejection path. Tracks issue #214. - Fast transfer + hook configurations now actually send a
fast-finality burn on-chain. Previous versions silently fell back
to the standard-finality hook path while still reporting
FinalityThreshold::Fastfromfinality_threshold(), so the reported mode and the transmittedminFinalityThreshold(andmaxFee) diverged. The accessor result and the burn now agree by construction. Tracks issue #218.
- New
api_url_override: Option<Url>builder field onCctpandCctpV2. When set,api_url()returns the override unchanged; when unset, the existing mainnet/testnet selection overIRIS_API/IRIS_API_SANDBOXis preserved. Lets callers point the attestation polling loop at a custom Iris-compatible endpoint (e.g. a self-hosted mirror or a local mock) without forking the crate. Non-breaking — existing builders compile unchanged.
- Per-attempt and per-response inner spans
(
cctp_rs.get_attestation,cctp_rs.process_attestation_response) now carry theerror.type,error.message,error.context, andotel.status_codefields thatrecord_error_with_contextwrites forHttpRequestFailed,AttestationFailed,AttestationDataMissing, andMessageDataMissing. Previous versions omitted these field declarations at span creation, sotracing::Span::recordsilently dropped the writes — TraceQL queries like{ span.error.type = "AttestationDataMissing" }returned zero hits even while the SDK was emitting the errors in production. Operators relying on inner-span error attributes for alerting will now see matches.
- OpenTelemetry traces no longer attribute events from unrelated async
tasks to CCTP attestation and message-extraction spans. Previous
versions held a
Span::enter()guard live across.awaitpoints insideCctp::get_attestation,Cctp::get_message_sent_event, and their v2 counterparts; in async Rust that leaves the span "current" on the executor thread after the future yields, so events emitted by other tasks landed under attestation spans and made production traces misleading. The affected futures now attach their span viatracing::Instrument, which correctly enters on poll and exits on yield. The retry-loop demo inexamples/complete_bridge_trace.rswas updated to the same shape.
cctp_rs::spansmodule documentation no longer demonstrates the unsafelet _guard = span.enter();pattern for async work. The module-level example usestracing::Instrument, and therecord_error/record_error_with_contextdoctests note that the entered-guard pattern shown is for synchronous scopes only.
- Updated alloy dependencies —
alloy-primitivesandalloy-sol-typesfrom 1.5 to 1.6, and the workspace 2.0.x crates (alloy-contract,alloy-json-rpc,alloy-network,alloy-provider,alloy-rpc-types,alloy-transport, dev-onlyalloy-signer-local) from 2.0.4 to 2.0.5. No source changes required; semver-compatible for callers on alloy 1.x / 2.0.x.
-
New
AttestationFailureKindenum (#[non_exhaustive]) with three variants —ApiReportedFailed,AttestationMissing,MessageMissing— exposed at the crate root. Lets callersmatchon a specific attestation-poll failure mode instead of substring-matching on a free-formreasonstring. -
New
CctpError::TransactionNotFound { tx_hash }variant signals that the source-chain RPC returned no receipt for the given hash (likely not yet mined or not indexed by the queried node). The hash is carried on the variant so callers can log or retry without parsing it out of a message string. -
New
CctpError::MessageSentEventMissing { tx_hash }variant signals that the receipt was found but did not contain the CCTPMessageSentlog — typically because the call hit the wrong contract or reverted before reaching the burn step. Also carries thetx_hash.
-
Breaking:
CctpError::AttestationFailedis now a tuple variant carryingAttestationFailureKind(AttestationFailed(AttestationFailureKind)) instead of the struct variantAttestationFailed { reason: String }. The five internal construction sites inbridge/cctp.rsandbridge/v2.rscollapse onto three named cases. TheDisplayoutput changes from the prior free-textreasonto a fixed prose phrase per kind (Attestation failed: Iris API reported failed status,... attestation field missing in complete response, or... message field missing in complete response); telemetry that scraped the prior strings should switch to matching on the typed kind. -
Breaking:
CctpError::InvalidUrlis now a tuple variant carryingurl::ParseError(InvalidUrl(url::ParseError)) instead of the struct variantInvalidUrl { reason: String }. Callers that destructured{ reason }must switch to tuple-pattern matching. The new shape exposes the structured parse error (kind, position) for diagnostics; theDisplayoutput retains theInvalid URL: <details>shape, though the per-call-site prefix (Failed to construct attestation URL:/Failed to construct v2 messages URL:) is no longer included.
-
Breaking:
CctpError::Provider(String)has been removed. Callers that constructedProvider(...)directly should switch toRpc(...)(which acceptsRpcError<TransportErrorKind>viaFrom) for transport errors, orContract(...)for typed contract-call errors. -
Breaking:
CctpError::TransactionFailed { reason: String }has been removed. Its three internal construction patterns now flow through typed variants: RPC-fetch failures propagate throughCctpError::Rpcvia the existing#[from]impl; the "receipt missing" case routes to the newTransactionNotFound { tx_hash }; the "MessageSent log absent" case routes to the newMessageSentEventMissing { tx_hash }. Callers that destructuredTransactionFailed { reason }for substring matching should switch to matching the typed variants directly.
- New
CctpError::Contract(alloy_contract::Error)variant preserves alloy's structured contract-call error so callers can usealloy_contract::Error::as_revert_dataandas_decoded_interface_errorfor revert decoding without re-parsing Display strings. Bridge call sites that previously stringified contract errors viaCctpError::ContractCall(format!(...))now propagate the typed variant through?, andis_already_relayedinspects the underlyingTransportErrorpayload directly.
- Breaking:
CctpErroris now#[non_exhaustive]. Downstream matches must include a wildcard arm. In return, future variant additions toCctpErrorwill be non-breaking —DomainIdalready follows this pattern. - Breaking:
CctpError::ContractCall(String)has been removed. All internal call sites now use the typedContract(alloy_contract::Error)variant; callers that constructedContractCallfor their own contract-call paths should switch toContract(which accepts analloy_contract::Errordirectly viaFrom) or toProvider(String)for non-alloy contract code.
- Breaking:
batch_token_checks, deprecated in 3.3.0, has been removed. Usebatch_token_stateinstead — it returns the same values inside aTokenStatewith named fields and predicate helpers (can_transfer,needs_approval,has_sufficient_balance).
CctpError::is_already_relayednow detects already-relayed reverts on contract calls consistently. The previous implementation only matched the legacyContractCall(String)variant, so a revert surfaced through alloy's typed error path was missed and the bridge helpers reported a hard failure instead of recognizing the message as already delivered.
- Dropped a dead type-erasing fallback in
is_already_relayed: onlyalloy_contract::Error::TransportErrorcarries chain-level revert data, so the other variants now answerfalsedirectly rather than being stringified through pattern matching. - Refactored bridge and example error handling to use
?with span- based error context instead of repeatedmap_err+format!bridges. - Examples now read USDC balances via the public
Erc20ContractAPI and parallelize the four pre-flight balance reads withtokio::join!. ETH balance reads were aligned with the same span-based?idiom. - Added regression tests covering
is_already_relayedagainst both the typedContractNotDeployedvariant (no revert payload) and an RPCErrorPayloadcarrying a revert message. - Documentation: clarified why BNB Smart Chain is excluded from CCTP
v2 (USYC-only on domain 17) and removed stale TODOs; spelled out
the threshold semantics of
TokenState::has_sufficient_balance.
batch_token_checksis deprecated in favor ofbatch_token_state. The new helper returns a typedTokenStatewithcan_transfer,needs_approval, andhas_sufficient_balancepredicates instead of a positional(allowance, balance)tuple, eliminating the field-order hazard between the two return shapes.batch_token_checkscontinues to work and now delegates tobatch_token_state; callers will see a#[deprecated]warning steering them to the new API.
- Module-level rustdoc and the
AGENTS.mdhelper table now lead withbatch_token_stateso new code reaches for the predicate API first. batch_token_stateholds the join logic and error mapping thatbatch_token_checkspreviously duplicated; both now share a single implementation.
InvalidDomainIdandInvalidFinalityThresholdare now re-exported from the crate root. Consumers ofDomainId::try_from(u32)andFinalityThreshold::try_from(u32)can name the error types directly to match on them, propagate them, or wrap them in their own error enums without piercing private module paths. Closes #175.
- Accessor methods on
FinalityThresholdandDomainIdnow carry#[must_use], so dropping a return value triggers a lint at the call site instead of silently discarding the result. InvalidDomainIdandInvalidFinalityThresholdnow derivethiserror::Errorto matchCctpError's style. TheDisplaymessage format andstd::error::Errorimpl are unchanged — purely an internal cleanup.- The
Default for FinalityThresholddoc comment no longer restates the impl signature; the rationale ("Standard transfers have no fees and are the safest option") is now the lead. Closes #176.
- Crate metadata now names the actual problem space —
descriptionmentions USDC, Circle, CCTP v1/v2, and bridging;keywordsare the specific terms agents and users search for (cctp,usdc,circle,bridge,alloy) rather than genericdefi/web3filler. Improves crates.io discoverability. - Module-level rustdoc on
lib.rsnow opens with aChoosing an APItable that points readers toCctpV2Bridge(recommended),Cctp(legacy v1-only chains),mint_if_needed/wait_for_receive, and the v2 message inspection types. The V2 Quick Start now appears before the V1 Quick Start. AGENTS.mdis the canonical agent-facing guide;CLAUDE.mdpoints to it to prevent drift. Adds a v1-vs-v2 decision matrix, a footguns section (zeroed v2 nonces, permissionless relayer races, bytes32 recipient padding, mainnet/testnet Iris hosts), and a public-API map keyed to source.
batch_token_checksno longer emits its owndebug!/info!records. The innerErc20Contract::allowanceandbalance_ofcalls already log every read, so each batch was producing three duplicateinfo!entries per call. Log volume for callers using the batch helper is reduced accordingly; structured fields and event names from the inner ERC20 layer are unchanged.
record_error_with_contextnow actually populateserror.contexton the top-level bridge operation spans. The field was previously written to spans that did not declare it, making the call a silent no-op.
- Observability schema (breaking for dashboards/alerts):
- Renamed the span field
error.sourcetoerror.contextoncctp_rs.get_message_sent_event,cctp_rs.get_attestation_with_retry,cctp_rs.get_v2_attestation_with_retry, andcctp_rs.deposit_for_burn. - Renamed the v2 attestation polling span from
cctp_rs.get_attestation_with_retrytocctp_rs.get_v2_attestation_with_retry, and dropped theversion = "v2"attribute (the span name now disambiguates). record_errornow setserror.typetostd::any::type_name::<E>()rather than the first colon-delimited fragment of the Display string.
- Renamed the span field
- Span helper API:
spans::send_transactionnow takestx_hash: TxHashinstead oftx_hash: &str, matching the typing used by the other span helpers.
- Switched
message_hashspan attributes toFixedBytes<32>'sDisplayimpl (output now includes the0xprefix) and removed#[inline]from the trivial span constructors.
- Documented why v1 and v2 attestation-retry spans require separate
constructors (
tracing::info_span!field identifiers must be literals). - Fixed a typo in the module-level rustdoc example (
poll intervali).
- Breaking: Upgraded the alloy workspace crates (
alloy-contract,alloy-json-rpc,alloy-network,alloy-provider,alloy-rpc-types,alloy-transport, and the dev-onlyalloy-signer-local) from 1.7 to 2.0. The cctp-rs source does not use any of the APIs broken by alloy 2.0 (GasFillerunit struct, splitTransactionBuildertrait, blob-tx builder reshuffle, strictAnyNetworksigning, exhaustiveChainConfigmatches), so migration is transparent for callers who only depend on cctp-rs. Callers who also depend on alloy directly must upgrade their alloy dependency to 2.0 in the same step. - Scoped
dotenvyto dev-dependencies so it is no longer resolved transitively by downstream consumers.
- Unused
alloy-dyn-abiroot dependency was dropped — no public API impact, but shrinks the dependency surface resolved by downstream.
- Pulled in
rustls-webpki0.103.12 via a lockfile refresh to resolve RUSTSEC-2026-0098 (name constraints for URI names incorrectly accepted) and RUSTSEC-2026-0099 (name constraints accepted for certificates asserting a wildcard name).
- Kept the advisory baseline clean: only the pre-existing transitive
derivative(RUSTSEC-2024-0388) andpaste(RUSTSEC-2024-0436) unmaintained-crate warnings remain, both tracked consistently indeny.tomland.cargo/audit.toml.
- Extracted cargo-deny policy from the security workflow heredoc into a
repo-root
deny.toml, so contributors can reproduce CI's license and advisory checks locally. - Modernized GitHub Actions workflows to use
Swatinem/rust-cachefor Rust-aware caching andtaiki-e/install-actionto installcargo-audit,cargo-deny, andcargo-semver-checksfrom prebuilt binaries instead of compiling from source on every run. - Removed the dependabot ignore rule for alloy semver-major updates so future breaking alloy releases surface as reviewable PRs.
- Applied pedantic clippy cleanups (named format-string interpolation,
u64::fromin place ofas u64for widening conversions, type and event names in rustdoc backticks).
- Agent/tooling-friendly CCTP v2 message inspection API:
ParsedV2Messagefor structured decoding of canonical Circle v2 messagesParsedV2MessageSummaryfor JSON-friendly summaries suitable for tool outputs- Header helpers for nonce inspection, permissionless relay detection, address extraction, and finality interpretation
- Burn message body encode/decode helpers for round-tripping canonical transfer payloads
- New
ParseMessageErrortype for v2 message parsing failures without changing the existing bridge/runtime error surface
- Enabled
serdesupport on agent-facing public types including:PollingConfigMintResultAttestationResponse,V2AttestationResponse,V2Message,AttestationStatusDomainIdandFinalityThresholdMessageHeaderandBurnMessageV2
- Expanded README and crate docs with agent-tooling guidance for turning canonical Circle v2 messages into structured JSON
- Avoided redundant message re-encoding in
ParsedV2Message::summary()when computing both hash and length
- Preserved semver compatibility for
2.xby keepingCctpErrorunchanged and isolating parser failures inParseMessageError - Documented forward-compatibility expectations for
DomainIdserde values and clarified that the current address helpers use EVMbytes32address conventions
- Fixed RUSTSEC-2026-0044, RUSTSEC-2026-0048 (AWS-LC X.509/CRL vulnerabilities) by updating aws-lc-sys to 0.39.0
- Fixed RUSTSEC-2026-0049 (CRL distribution point matching) by updating rustls-webpki to 0.103.10
- Bumped minimum tokio version to 1.50
- Updated cargo dependencies to latest stable versions
- Fixed RUSTSEC-2026-0037 security vulnerability in quinn-proto (upgraded to 0.11.14)
- Updated alloy dependencies from 1.4 to 1.6
- Updated alloy dependencies to 1.4
-
Typed error detection methods on
CctpErrorfor robust error handling:is_already_relayed(): Detect when a CCTP message was already relayed by a third partyis_timeout(): Check for timeout errors (attestation or network)is_rate_limited(): Detect HTTP 429 rate limiting responsesis_transient(): Identify retriable errors (timeouts, rate limits, network issues)
-
Batch token check helpers for parallel RPC calls:
batch_token_checks(): Fetch allowance and balance concurrently usingtokio::join!batch_token_state(): Returns structuredTokenStatewith helper methodsTokenStatestruct withcan_transfer(),needs_approval(),has_sufficient_balance()
-
Provider utilities for gas estimation and configuration:
estimate_gas_with_buffer(): Gas estimation with configurable safety buffer (default 20%)calculate_gas_price_with_buffer(): EIP-1559 gas price calculation with tip bufferProviderConfigstruct with presets:fast_transfer(),high_reliability(),rate_limited()
-
Production retry documentation with patterns for throttle usage and custom retry logic
- Refactored
mint_if_needed()to use typedis_already_relayed()instead of string matching
- BREAKING: Replaced
Option<u32>, Option<u64>parameters inget_attestation()withPollingConfigstruct- Before:
bridge.get_attestation(hash, Some(20), Some(30)).await? - After:
bridge.get_attestation(hash, PollingConfig::default().with_max_attempts(20).with_poll_interval_secs(30)).await?
- Before:
- New
PollingConfigstruct for self-documenting attestation polling configurationPollingConfig::default()- Standard v1 transfers (30 attempts, 60s intervals)PollingConfig::fast_transfer()- Optimized for v2 fast transfers (30 attempts, 5s intervals)- Builder methods:
with_max_attempts(),with_poll_interval_secs() - Helper method:
total_timeout_secs()to calculate maximum wait time
// V1 - Before (1.x)
let attestation = bridge.get_attestation(message_hash, None, None).await?;
let attestation = bridge.get_attestation(message_hash, Some(20), Some(30)).await?;
// V1 - After (2.0)
use cctp_rs::PollingConfig;
let attestation = bridge.get_attestation(message_hash, PollingConfig::default()).await?;
let attestation = bridge.get_attestation(
message_hash,
PollingConfig::default()
.with_max_attempts(20)
.with_poll_interval_secs(30),
).await?;
// V2 - Before (1.x)
let (message, attestation) = bridge.get_attestation(tx_hash, None, None).await?;
// V2 - After (2.0)
let (message, attestation) = bridge.get_attestation(
tx_hash,
PollingConfig::fast_transfer(), // or PollingConfig::default() for standard
).await?;- Removed unused chain confirmation configuration from bridge config module
- Removed
DEFAULT_CONFIRMATION_TIMEOUTconstant - Removed
CHAIN_CONFIRMATION_CONFIGconstant - Removed
get_chain_confirmation_config()function - These were dead code (marked with
#[allow(dead_code)]) and never called - The SDK correctly delegates finality validation to Circle's attestation service
- Removed
First stable release! This release marks production-readiness with comprehensive CCTP v1 and v2 support, 155 passing tests, and zero clippy warnings.
- Full CCTP v1/v2 Protocol Support: Type-safe interfaces for both protocol versions
- Relayer-Aware API: Graceful handling of permissionless relay model with
MintResultenum - Production Observability: Comprehensive OpenTelemetry instrumentation
- Robust Error Handling: Consolidated error types with HTTP client timeouts
- HTTP client timeout (30 seconds) to prevent indefinite hangs on network issues
- BREAKING: Consolidated duplicate error variants - removed
ChainNotSupported { chain: String }, keeping onlyUnsupportedChain(NamedChain)for type safety and zero allocation
- Error handling now uses consistent, type-safe error variants throughout
-
Relayer-Aware API for CCTP v2: New methods to gracefully handle the permissionless relay model
MintResultenum: Distinguishes betweenMinted(TxHash)(we relayed) andAlreadyRelayed(third party completed)is_message_received(): Query on-chain nonce status to check if transfer is completewait_for_receive(): Poll until transfer completes (by relayer or self), with configurable attempts and intervalmint_if_needed(): Conservative mint strategy that checks status first, avoiding wasted gas on races
-
AlreadyRelayed Error Variant: New
CctpError::AlreadyRelayedfor explicit race condition handling
- Added "Relayer-Aware Patterns (V2)" section to README with three usage patterns:
- Option A: Wait for completion (recommended for most use cases)
- Option B: Self-relay with graceful race handling
- Option C: Manual status checking
- BREAKING: Replaced
confirmation_average_time_seconds()inCctpV2trait with two explicit methods:fast_transfer_confirmation_time_seconds()- Returns attestation times with Fast Transfer enabled (5-20 seconds)standard_transfer_confirmation_time_seconds()- Returns attestation times for Standard Transfer (5 seconds to 8 hours)
- Fixed v2
CctpV2trait returning block production times instead of Circle attestation times- Previously returned 1-12 seconds (block times)
- Now returns actual attestation confirmation times based on Circle's documentation
- Fast Transfer: Ethereum 20s, most chains 8s, high-perf chains 5s
- Standard Transfer: Ethereum/L2s 19min, Avalanche 20s, Polygon 8min, Linea 8hrs
// Before (0.14.0)
let time = chain.confirmation_average_time_seconds()?; // Returned block times (wrong!)
// After (0.15.0)
// Choose based on your transfer mode:
let time = chain.fast_transfer_confirmation_time_seconds()?; // Fast Transfer
let time = chain.standard_transfer_confirmation_time_seconds()?; // Standard (default)-
ERC20 Contract Wrapper: New
Erc20Contractfor token approval and allowance operationsapprove()for granting token spending permission before CCTP burnsallowance()for checking current approval amounts- Essential for the complete CCTP transfer workflow
- Exposed publicly for direct use
-
Public Chain Addresses Module: Exposed
chain::addressesmodule for accessing contract addresses directly -
V2-specific span function: Added
get_v2_attestation_with_retryfor proper v2 observability withTxHashinstead of message hash
-
BREAKING: Simplified attestation API with type-safe methods per version
- V1
Cctp:get_attestation(message_hash, ...)- takes message hash directly - V2
CctpV2:get_attestation(tx_hash, ...)- takes transaction hash directly - Removed
AttestationQueryenum in favor of compile-time type safety - Method signatures now clearly indicate what parameters are needed
- V1
-
BREAKING: V2
get_attestation()now returns(Vec<u8>, AttestationBytes)instead of justAttestationBytes- Always returns both the canonical message and attestation from Circle's API
- Eliminates foot-gun where users might use wrong message for minting
- The
MessageSentevent log contains zeros in the nonce field; Circle fills this in before signing
-
BREAKING: Removed
get_attestation_with_message()from V2 - useget_attestation()instead
- BREAKING: Removed
BridgeParamsfrom public API (was unused)
- Fixed v2 API path: renamed constant to
MESSAGES_PATH_V2reflecting the actual endpoint format - V2 now correctly uses
/v2/messages/{domain}?transactionHash={tx}format - Fixed critical v2 attestation bug: MessageSent event contains template message with zero nonce; now always uses canonical message from Circle's API
// V1 - Before (0.13.0)
bridge.get_attestation(AttestationQuery::by_message_hash(hash), max_attempts, poll_interval).await?;
// V1 - After (0.14.0)
let attestation = bridge.get_attestation(hash, None, None).await?;
// V2 - Before (0.13.0)
let attestation = bridge.get_attestation(tx_hash, None, None).await?;
let (message, _hash) = bridge.get_message_sent_event(tx_hash).await?; // BUG: wrong message!
// V2 - After (0.14.0)
let (message, attestation) = bridge.get_attestation(tx_hash, None, None).await?;
// Now use `message` (canonical from Circle API) for minting - this is the correct message!- v1 MessageTransmitterContract: Exposed
MessageTransmitterContractin the public APIreceive_message_transaction()for receiving cross-chain messages with attestationis_nonce_used()for checking anti-replay protection- Matches v2 API symmetry for consistent developer experience
- Removed unnecessary
#[allow(dead_code)]attributes from public contract wrappers- Applies to
TokenMessengerContract,MessageTransmitterContract(v1) - Applies to
TokenMessengerV2Contract,MessageTransmitterV2Contract(v2)
- Applies to
-
CCTP v2 Support: Complete implementation of Circle's CCTP v2 protocol
- Support for 26+ chains (10 mainnet, 6 testnet v2 chains + v1 chains)
- New v2-only chains: Linea, Sonic, Sei
CctpV2BridgeandCctpV2traits with unified contract addresses- Comprehensive test suite with 149 unit tests (100% pass rate)
-
Fast Transfers: Sub-30 second settlement with optimized finality thresholds
- Standard transfers: 2000 confirmations (finalized)
- Fast transfers: 1000 confirmations (confirmed)
- Optional fee support for fast finality
- Smart contract method selection based on configuration
-
Programmable Hooks: Advanced integration capabilities
depositForBurnWithHook()support for custom destination logic- Hook data validation and configuration
- Priority handling when combined with fast transfers
-
Enhanced Testing Infrastructure
- 149 comprehensive unit tests covering all business logic
v2_integration_validationexample for CI/CD validation (no network required)- Comprehensive examples:
v2_standard_transfer,v2_fast_transfer - Testing guidelines documentation for maintaining test quality
-
Contract Wrappers: Direct access to type-safe contract interfaces
- Exported
TokenMessengerContract(v1) for direct contract interaction - Exported
TokenMessengerV2ContractandMessageTransmitterV2Contract(v2) - Enables advanced use cases beyond the bridge abstraction
- All wrappers include OpenTelemetry instrumentation built-in
- Exported
-
BREAKING: New trait system for v1/v2 polymorphism
CctpBridgetrait enables version-independent codeCctpV1andCctpV2traits for version-specific configuration- Clean separation between v1 (legacy) and v2 (current) implementations
-
v2-specific modules added:
bridge/v2.rs,chain/v2.rs,contracts/v2/
- Added comprehensive testing strategy section to README
- Documented integration test limitations and pragmatic approach
- Added v2-specific usage examples
- Created testing-guidelines.md for maintaining test quality
- Documented fast transfer behavior and fee structures
- BREAKING: Replaced primitive domain ID types with strongly-typed
DomainIdenum- Domain IDs are now type-safe with compile-time validation
- Added
DomainId::from_u32(),DomainId::as_u32(),DomainId::name(), andDisplaytrait support - Improved API ergonomics with meaningful type names instead of raw integers
- Reorganized crate into conceptual modules for better code organization
- Split code into logical modules:
bridge,chain,contracts,protocol, andspans - Improved maintainability and discoverability of functionality
- Split code into logical modules:
- Updated all Cargo dependencies to latest versions
- Enhanced OpenTelemetry instrumentation with comprehensive error tracking
- Added structured error recording with full context preservation
- Improved observability for debugging production issues
- Better span hierarchy for cross-chain transfer tracing
- CRITICAL: Fixed deserialization failure when Circle's Iris API returns
"attestation": "PENDING"as a string instead ofnull- Added custom deserializer that gracefully handles the "PENDING" string by treating it as
None - This fixes production crashes on Arbitrum and other chains where attestation polling would fail with:
JSON error: invalid value: string "PENDING", expected a valid hex string - Enhanced error logging to include raw response body, message hash, and attempt number for better debugging
- Added comprehensive test coverage for all attestation response formats (valid hex, "PENDING" string, null, empty string, missing field)
- No breaking changes to public API
- Added custom deserializer that gracefully handles the "PENDING" string by treating it as
- Documented Circle API quirk where attestation field may be "PENDING" instead of null
- Added inline comments explaining the custom deserializer workaround
- BREAKING: Minimized public API surface for improved semver stability
- Reduced from 30+ exports to 6 core types:
AttestationBytes,AttestationResponse,AttestationStatus,BridgeParams,Cctp,CctpV1,CctpError,Result - Removed public exports of domain ID constants, contract address constants, and internal configuration
- Internal modules (
domain_id,message_transmitter,token_messenger) remain accessible via trait methods
- Reduced from 30+ exports to 6 core types:
- BREAKING: Made
BridgeParamsfields private (public getter methods remain available) - BREAKING: Made
TokenMessengerContract.instancefield private - Refactored
bridge.rs(589 lines) into clean submodules (bridge/cctp.rs,bridge/params.rs,bridge/config.rs)
- Exposed
spansmodule publicly for advanced OpenTelemetry instrumentation - Added comprehensive documentation to
spansmodule with usage examples
- Fixed all documentation warnings by referencing types instead of private modules
- Updated Cargo dependencies to latest versions
- Converted snapshot tests to inline snapshots for better maintainability
- Replaced Anvil provider with HTTP provider in tests for improved stability
- Fixed attestation URL construction
- Updated Cargo dependencies to latest versions
- Fixed typo in Iris API URL creation
- Implemented OpenTelemetry logging with structured spans for observability
- Updated Cargo dependencies to latest versions
- Refactored attestation to use
Bytestype for improved type safety
- BREAKING: Refactored to use
Urltype instead ofStringfor API endpoints - Improved type safety for URL handling
- Updated Cargo dependencies
- Initial support for improved error handling
- Enhanced API ergonomics
- BREAKING: Replaced all
const &straddress constants with typedconst Addressusingaddress!()macro - BREAKING: Removed
InvalidAddresserror variant fromCctpErrorenum (addresses now validated at compile time) - Improved structured logging with static messages and event fields for better observability
- Eliminated runtime address parsing overhead with compile-time validation
- Removed potential runtime failures from address string parsing
- Comprehensive CI/CD pipeline with GitHub Actions
- Security audit workflows with cargo-audit and cargo-deny
- Automated dependency updates with Dependabot
- Code coverage reporting with codecov
- Documentation generation and deployment
- Issue and PR templates for better contribution workflow
- Contribution guidelines and security policy
- Updated examples to use modern Alloy provider API
- Improved error handling with detailed error types
- Enhanced documentation with better examples
- All clippy warnings resolved with strict linting
- Deprecated method usage in examples
- Format string improvements for better performance
- Support for Unichain network
- Comprehensive test suite with 69 tests
- Custom error types replacing anyhow
- Builder pattern for CCTP bridge configuration
- Attestation polling with configurable retry logic
- Multi-chain examples and documentation
- Improved type safety with custom error handling
- Better API design with builder patterns
- Enhanced documentation with usage examples
- Removed all unsafe unwrap() and panic! calls
- Fixed chain support validation logic
- Improved error propagation throughout the library
- Eliminated potential panics from unsafe operations
- Added input validation for addresses and parameters
- Implemented proper error handling for network operations
- Repository field in Cargo.toml manifest
- Initial CCTP bridge implementation
- Support for major EVM chains (Ethereum, Arbitrum, Base, Optimism, Avalanche, Polygon)
- Circle Iris API integration for attestations
- Contract bindings for TokenMessenger and MessageTransmitter
- Basic examples and documentation
- Updated to Alloy 1.0 framework
- Improved chain configuration system
- Initial project structure
- Basic CCTP domain ID support
- Chain configuration foundations