Overview
The TransferOrchestrator is the main component for executing CCTP transfers. It orchestrates the entire transfer workflow including balance checking, token approval, burning, attestation polling, and minting.
Type Definition
type TransferOrchestrator struct {
wallet *wallet.Wallet
irisClient *IrisClient
sourceClient *ethclient.Client
destClient *ethclient.Client
params *TransferParams
}
Constructor
NewTransferOrchestrator
Creates a new transfer orchestrator instance.
func NewTransferOrchestrator(
w *wallet.Wallet,
params *TransferParams,
irisBaseURL string,
) (*TransferOrchestrator, error)
Parameters
The wallet instance used to sign transactions. Must have sufficient USDC balance on the source chain.
The transfer parameters including source chain, destination chain, amount, recipient, and transfer type.
The base URL for Circle’s Iris attestation service API.
- Mainnet:
https://iris-api.circle.com
- Testnet:
https://iris-api-sandbox.circle.com
Returns
A new transfer orchestrator instance ready to execute transfers.
An error if the orchestrator creation fails (e.g., failed to connect to RPC endpoints).
Methods
Execute
Executes the complete transfer workflow from start to finish.
func (t *TransferOrchestrator) Execute(
ctx context.Context,
updates chan<- TransferUpdate,
) error
Parameters
The context for cancellation and timeout control. If the context is cancelled, the transfer will stop at the next safe checkpoint.
updates
chan<- TransferUpdate
required
A channel for receiving progress updates throughout the transfer process. The orchestrator will send TransferUpdate messages as it progresses through each step.The channel will be closed when the transfer completes (successfully or with error).
Returns
Returns nil if the transfer completes successfully, or an error describing what went wrong.
Transfer Steps
The Execute method performs the following steps in order:
- Check Balance (
StepCheckBalance) - Verifies sufficient USDC balance
- Check Allowance (
StepCheckAllowance) - Checks if TokenMessenger has approval
- Approve (
StepApproving, StepApprovingWait) - Approves USDC if needed
- Burn (
StepBurning, StepBurningWait) - Burns USDC on source chain
- Poll Attestation (
StepPollingAttestation) - Waits for Circle attestation
- Mint (
StepMinting, StepMintingWait) - Mints USDC on destination chain
- Complete (
StepComplete) - Transfer successful
If any step fails, the orchestrator will send an error update with StepError.
Close
Closes the RPC client connections.
func (t *TransferOrchestrator) Close()
Call this method when you’re done with the orchestrator to clean up resources.
Transfer Update Types
TransferUpdate
type TransferUpdate struct {
Step TransferStep
Message string
TxHash string
Error error
SourceChain *Chain
DestChain *Chain
}
The current step in the transfer process.
A human-readable message describing the current step.
The transaction hash for steps that involve on-chain transactions.
The error that occurred, if Step is StepError.
The source chain (optional, may be nil).
The destination chain (optional, may be nil).
TransferStep Constants
type TransferStep string
const (
StepCheckBalance TransferStep = "checking_balance"
StepCheckAllowance TransferStep = "checking_allowance"
StepApproving TransferStep = "approving"
StepApprovingWait TransferStep = "approving_wait"
StepBurning TransferStep = "burning"
StepBurningWait TransferStep = "burning_wait"
StepPollingAttestation TransferStep = "polling_attestation"
StepMinting TransferStep = "minting"
StepMintingWait TransferStep = "minting_wait"
StepComplete TransferStep = "complete"
StepError TransferStep = "error"
)
Usage Examples
Basic Transfer with Progress Updates
package main
import (
"context"
"fmt"
"log"
"math/big"
"github.com/circlefin/cctp-go"
"github.com/circlefin/cctp-go/internal/wallet"
"github.com/ethereum/go-ethereum/common"
)
func main() {
ctx := context.Background()
// Load wallet from private key
w, err := wallet.NewWallet("your-private-key")
if err != nil {
log.Fatal(err)
}
// Get chains
sourceChain, _ := cctp.GetChainByName("Ethereum", false)
destChain, _ := cctp.GetChainByName("Base", false)
// Create transfer parameters
params := &cctp.TransferParams{
SourceChain: sourceChain,
DestChain: destChain,
Amount: big.NewInt(100_000000), // 100 USDC
RecipientAddress: common.HexToAddress("0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb"),
Testnet: false,
TransferType: cctp.TransferTypeAuto,
}
// Create orchestrator
orchestrator, err := cctp.NewTransferOrchestrator(
w,
params,
"https://iris-api.circle.com",
)
if err != nil {
log.Fatal(err)
}
defer orchestrator.Close()
// Create channel for updates
updates := make(chan cctp.TransferUpdate)
// Start transfer in goroutine
go func() {
if err := orchestrator.Execute(ctx, updates); err != nil {
log.Printf("Transfer failed: %v", err)
}
}()
// Listen for updates
for update := range updates {
if update.Error != nil {
fmt.Printf("ERROR: %v\n", update.Error)
break
}
fmt.Printf("[%s] %s\n", update.Step, update.Message)
if update.TxHash != "" {
fmt.Printf(" Transaction: %s\n", update.TxHash)
}
if update.Step == cctp.StepComplete {
fmt.Println("Transfer completed successfully!")
break
}
}
}
Transfer with Timeout
package main
import (
"context"
"fmt"
"log"
"math/big"
"time"
"github.com/circlefin/cctp-go"
"github.com/circlefin/cctp-go/internal/wallet"
"github.com/ethereum/go-ethereum/common"
)
func main() {
// Create context with 30 minute timeout
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Minute)
defer cancel()
w, _ := wallet.NewWallet("your-private-key")
sourceChain, _ := cctp.GetChainByName("Ethereum", false)
destChain, _ := cctp.GetChainByName("Arbitrum", false)
params := &cctp.TransferParams{
SourceChain: sourceChain,
DestChain: destChain,
Amount: big.NewInt(500_000000), // 500 USDC
RecipientAddress: common.HexToAddress("0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb"),
Testnet: false,
TransferType: cctp.TransferTypeFast,
}
orchestrator, _ := cctp.NewTransferOrchestrator(
w,
params,
"https://iris-api.circle.com",
)
defer orchestrator.Close()
updates := make(chan cctp.TransferUpdate)
go func() {
if err := orchestrator.Execute(ctx, updates); err != nil {
if ctx.Err() == context.DeadlineExceeded {
log.Println("Transfer timed out")
} else {
log.Printf("Transfer failed: %v", err)
}
}
}()
for update := range updates {
if update.Error != nil {
fmt.Printf("ERROR: %v\n", update.Error)
break
}
fmt.Printf("[%s] %s\n", update.Step, update.Message)
}
}
Transfer with UI Progress Bar
package main
import (
"context"
"fmt"
"math/big"
"github.com/circlefin/cctp-go"
"github.com/circlefin/cctp-go/internal/wallet"
"github.com/ethereum/go-ethereum/common"
)
func main() {
ctx := context.Background()
w, _ := wallet.NewWallet("your-private-key")
sourceChain, _ := cctp.GetChainByName("Ethereum", false)
destChain, _ := cctp.GetChainByName("Base", false)
params := &cctp.TransferParams{
SourceChain: sourceChain,
DestChain: destChain,
Amount: big.NewInt(100_000000),
RecipientAddress: common.HexToAddress("0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb"),
Testnet: false,
TransferType: cctp.TransferTypeAuto,
}
orchestrator, _ := cctp.NewTransferOrchestrator(
w,
params,
"https://iris-api.circle.com",
)
defer orchestrator.Close()
updates := make(chan cctp.TransferUpdate)
go orchestrator.Execute(ctx, updates)
// Map steps to progress percentage
progressMap := map[cctp.TransferStep]int{
cctp.StepCheckBalance: 5,
cctp.StepCheckAllowance: 10,
cctp.StepApproving: 20,
cctp.StepApprovingWait: 25,
cctp.StepBurning: 40,
cctp.StepBurningWait: 50,
cctp.StepPollingAttestation: 70,
cctp.StepMinting: 85,
cctp.StepMintingWait: 95,
cctp.StepComplete: 100,
}
for update := range updates {
if update.Error != nil {
fmt.Printf("❌ Error: %v\n", update.Error)
break
}
progress := progressMap[update.Step]
fmt.Printf("[%3d%%] %s\n", progress, update.Message)
if update.Step == cctp.StepComplete {
fmt.Println("✅ Transfer completed successfully!")
break
}
}
}
Testnet Transfer
package main
import (
"context"
"fmt"
"math/big"
"github.com/circlefin/cctp-go"
"github.com/circlefin/cctp-go/internal/wallet"
"github.com/ethereum/go-ethereum/common"
)
func main() {
ctx := context.Background()
w, _ := wallet.NewWallet("your-testnet-private-key")
// Use testnet chains
sourceChain, _ := cctp.GetChainByName("Ethereum Sepolia", true)
destChain, _ := cctp.GetChainByName("Base Sepolia", true)
params := &cctp.TransferParams{
SourceChain: sourceChain,
DestChain: destChain,
Amount: big.NewInt(10_000000), // 10 USDC
RecipientAddress: common.HexToAddress("0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb"),
Testnet: true, // Important: set to true for testnet
TransferType: cctp.TransferTypeAuto,
}
// Use testnet Iris API
orchestrator, _ := cctp.NewTransferOrchestrator(
w,
params,
"https://iris-api-sandbox.circle.com", // Testnet API
)
defer orchestrator.Close()
updates := make(chan cctp.TransferUpdate)
go orchestrator.Execute(ctx, updates)
for update := range updates {
if update.Error != nil {
fmt.Printf("ERROR: %v\n", update.Error)
break
}
fmt.Printf("[%s] %s\n", update.Step, update.Message)
}
}
Error Handling
The orchestrator can return errors at various stages:
- Connection Errors: Failed to connect to RPC endpoints
- Balance Errors: Insufficient USDC balance
- Approval Errors: Failed to approve TokenMessenger
- Burn Errors: Failed to burn USDC on source chain
- Attestation Errors: Failed to get attestation from Circle
- Mint Errors: Failed to mint USDC on destination chain
All errors are sent through the updates channel with Step: StepError and the Error field populated.
The Execute method is designed to be safe. If the context is cancelled or an error occurs, the orchestrator will stop at the next safe checkpoint and send an error update.
Resource Management
Always call Close() when you’re done with the orchestrator to properly clean up RPC client connections:
orchestrator, _ := cctp.NewTransferOrchestrator(w, params, irisBaseURL)
defer orchestrator.Close() // Ensures cleanup
See Also