@circle-fin/adapter-circle-wallets
v1.7.0
Published
Circle Wallets blockchain adapter
Readme
Circle Wallets Adapter
🔐 Enterprise-grade hybrid adapter for EVM and Solana powered by Circle Wallets
Seamlessly manage cross-chain operations with developer-controlled wallets
⚠️ SERVER ENTRIES ONLY - The main, /utils, and /ucw/server
entries require secrets and must never be bundled into a browser.
Use /ucw/client for the browser-safe UCW surface.
Table of Contents
- Circle Wallets Adapter
Overview
The Circle Wallets Adapter is a hybrid adapter that provides unified access to both EVM and Solana blockchains through Circle's developer-controlled wallet infrastructure. Unlike ecosystem-specific adapters (Viem, Ethers, Solana), this adapter seamlessly operates across both ecosystems using a single instance.
Built for enterprise and backend applications, this adapter integrates Circle Wallets and Circle Contracts to provide secure, programmatic wallet management.
Why Circle Wallets Adapter?
- 🌉 True Cross-Ecosystem Support - Single adapter instance works across EVM chains (Ethereum, Base, Polygon, etc.) AND Solana
- 🏢 Enterprise-Grade - Built on Circle's institutional wallet infrastructure with developer-controlled addressing
- 🔐 Secure by Design - API key and entity secret authentication keeps credentials server-side only
- 🔒 Type-Safe - Built with TypeScript strict mode for complete type safety
- 🎯 Simplified Management - One adapter for all chains eliminates the need to manage separate EVM and Solana adapters
- 🔄 Complete Transaction Lifecycle - Full prepare/estimate/execute workflow across both ecosystems
- 🚀 Production Ready - Leverage Circle's reliable infrastructure and built-in wallet management
When and How Should I Use The Circle Wallets Adapter?
I'm a developer building a backend service
If you're building a server-side application that needs to perform blockchain operations across EVM and Solana using Circle Wallets, this adapter provides a unified interface for both ecosystems.
Perfect for:
- Backend services performing automated cross-chain transfers
- Enterprise applications managing wallets programmatically
- Server-side scripts requiring multi-ecosystem support
- Applications using Circle's custody infrastructure
Example:
import { createCircleWalletsAdapter } from '@circle-fin/adapter-circle-wallets'
// Server-side only - credentials from secure environment variables
const adapter = createCircleWalletsAdapter({
apiKey: process.env.CIRCLE_API_KEY!,
entitySecret: process.env.CIRCLE_ENTITY_SECRET!,
})
// Same adapter works for both EVM and Solana operations!I'm a developer making a Kit Provider
If you're building a provider (e.g., a custom BridgingProvider) that needs to work across both EVM and Solana, the Circle Wallets Adapter provides a unified abstraction. You don't need separate adapters for each ecosystem.
Benefits:
- Single adapter instance for cross-ecosystem operations
- Consistent API across EVM and Solana chains
- Built-in wallet management through Circle's infrastructure
- Developer-controlled addressing for explicit wallet specification
⚠️ Server-Side Only
The main entry, /utils, and /ucw/server can only be used in server-side
(Node.js) environments. Browser builds resolve these entries to a guard that
directs the integrator to /ucw/client.
Security Reasons:
- ❌ API Keys - Your Circle API key must never be exposed to client-side code
- ❌ Entity Secrets - Entity secrets are sensitive credentials that must remain server-side only
- ❌ Wallet Control - Developer-controlled wallets should only be accessible from trusted backend services
Technical Reasons:
- ❌ Node.js Dependencies - Requires Node.js built-in modules (
crypto,buffer, etc.) not available in browsers - ❌ SDK Requirements - Circle's Developer Controlled Wallets SDK requires a Node.js runtime environment
✅ For UCW Browser Applications:
Import only the transport-agnostic transaction waiter and signing-rejection helpers from the client entry:
import { createWaitForTransaction } from '@circle-fin/adapter-circle-wallets/ucw/client'
const waitForTransaction = createWaitForTransaction({
getStatus: ({ challengeId, signal }) =>
fetch(`/api/transactions/${challengeId}`, { signal }).then((response) =>
response.json(),
),
})
const result = await waitForTransaction({ challengeId })The browser helper calls the endpoint supplied by your application. It never calls Circle directly and does not prescribe how the server authenticates the request.
UCW Typed-Data Signing
The server-side UCW adapter can sign EIP-712 typed data through a Circle
challenge. Unlike a transaction — whose result the adapter reads back from
Circle — a signature is delivered only to the client that approves the
challenge, in the W3S browser SDK's challenge result. Hand it back through
resolveTypedDataSignature:
const adapter = await createCircleUserWalletAdapter({
apiKey,
userToken,
wallet,
// Your existing approval channel: forward the challenge to the browser.
onChallenge: ({ challengeId }) => notifyApprovalSurface(challengeId),
// The other half: return the signature the browser received for it.
resolveTypedDataSignature: ({ challengeId }) =>
awaitSignatureFromApprovalSurface(challengeId),
})
const signature = await adapter.signTypedData(typedData, { chain })In the browser, the signature is on the challenge result the W3S SDK already gives you for PIN approval:
sdk.execute(challengeId, (error, result) => {
if (error) return reportRejected(challengeId, error)
reportSignature(challengeId, result?.data?.signature)
})Without resolveTypedDataSignature the adapter reports
supportsSignTypedData() === false and rejects typed-data requests, so a
caller can pick an on-chain alternative rather than discover the gap after the
owner has approved.
Keep the signTypedData invocation and its resolveTypedDataSignature promise
alive until the approving client returns the signature. Circle's completed
challenge has no server-readable signature, so if the process exits after
approval, the result cannot be recovered and the caller must start a new signing
operation. Run this flow in a durable server or worker process; a stateless
request handler that can end before browser approval is not a supported
deployment shape.
Authenticate the client-to-server return channel and bind each challenge id to the user and in-flight signing request before accepting its signature. The adapter validates the returned signature's format, but your application owns authorization for that relay.
For an SCA wallet, Circle returns the wallet owner's 65-byte ECDSA signature,
which the wallet validates through ERC-1271 isValidSignature. Plain
ecrecover recovers the owner's key, not the SCA address, so use an
ERC-1271-aware verifier — USDC's signature-checker path, used by ERC-3009
authorizations, accepts it. Verifiers that only ecrecover, including standard
EIP-2612 permit and Gateway burn-intent flows, cannot validate it. An EOA wallet
produces the standard signature that ecrecover validates against its address.
For other browser wallet flows, use a browser-compatible adapter:
@circle-fin/adapter-viem-v2- For EVM chains with browser wallets@circle-fin/adapter-solana- For Solana with browser wallets@circle-fin/adapter-ethers-v6- For EVM chains with Ethers.js
Installation
npm install @circle-fin/adapter-circle-wallets
# or
yarn add @circle-fin/adapter-circle-walletsPrerequisites:
- Node.js environment (v18+ recommended)
- Circle API Key and Entity Secret from Circle Developer Console
Initial Setup - Generate and Register Entity Secret (If Not Already Done)
If you haven't already set up an entity secret with Circle, you'll need to complete a one-time setup process. The adapter provides convenience utilities (re-exported from Circle's SDK) to simplify this:
import {
generateEntitySecret,
registerEntitySecretCiphertext,
} from '@circle-fin/adapter-circle-wallets/utils'
// Step 1: Generate a new entity secret
const entitySecret = generateEntitySecret()
console.log('Generated entity secret:', entitySecret)
// Step 2: Register the entity secret with Circle
const response = await registerEntitySecretCiphertext({
apiKey: 'TEST_API_KEY:abc123:def456', // Your Circle API key
entitySecret: entitySecret,
})
console.log('Recovery file:', response.data?.recoveryFile)
// The recovery file is also automatically downloaded to your filesystemImportant Security Notes:
- ⚠️ Entity Secret - Store this securely in environment variables. Never commit it to version control.
- ⚠️ Recovery File - Back up the generated
recovery_file_<timestamp>.datsecurely. This is your only way to restore access if the entity secret is lost. - ⚠️ One-Time Setup - This only needs to be done once per Circle account/environment (test/production). If you already have an entity secret registered, skip this step.
After completing this setup (or if you already have credentials), store your entity secret in environment variables:
# .env file (add to .gitignore!)
CIRCLE_API_KEY=TEST_API_KEY:abc123:def456
CIRCLE_ENTITY_SECRET=your-generated-64-char-secretNote: These utilities are re-exported from
@circle-fin/developer-controlled-walletsfor convenience. For more details, see the Circle Developer Controlled Wallets documentation.
Quick Start
🚀 Basic Setup
Create an adapter instance with your Circle credentials:
import { createCircleWalletsAdapter } from '@circle-fin/adapter-circle-wallets'
// Initialize adapter with Circle credentials (server-side only!)
const adapter = createCircleWalletsAdapter({
apiKey: process.env.CIRCLE_API_KEY!, // Format: TEST_API_KEY:abc:def or Base64
entitySecret: process.env.CIRCLE_ENTITY_SECRET!, // Format: 64 lowercase alphanumeric chars
})
// Works across both EVM and Solana!
// No need to create separate adapters for each ecosystemAPI Key Format:
- Environment-prefixed:
TEST_API_KEY:abc123:def456orLIVE_API_KEY:xyz:uvw - Base64 encoded: Standard Base64 string
Entity Secret Format:
- 64 lowercase alphanumeric characters (e.g.,
abc123...× 64 chars)
Usage Examples
Cross-Chain Bridging with Bridge Kit
Use the Circle Wallets Adapter with Bridge Kit for seamless cross-chain USDC transfers across both EVM and Solana:
import { createCircleWalletsAdapter } from '@circle-fin/adapter-circle-wallets'
import { BridgeKit } from '@circle-fin/bridge-kit'
// Single adapter instance for both ecosystems
const adapter = createCircleWalletsAdapter({
apiKey: process.env.CIRCLE_API_KEY!,
entitySecret: process.env.CIRCLE_ENTITY_SECRET!,
})
const kit = new BridgeKit()
// Bridge from Ethereum (EVM) to Solana - same adapter for both!
const result = await kit.bridge({
from: {
adapter,
chain: 'Ethereum',
address: '0x1234...', // EVM address (developer-controlled)
},
to: {
adapter, // Same adapter instance!
chain: 'Solana',
address: 'ABC123...', // Solana address (developer-controlled)
},
amount: '100.00',
token: 'USDC',
})
console.log('Bridge transaction:', result.transactionHash)Key Benefits:
- ✅ One adapter for cross-ecosystem bridging (EVM ↔ Solana)
- ✅ Developer-controlled addressing for explicit wallet specification
- ✅ Unified API regardless of source/destination chain type
Direct SDK Access
Access the underlying Circle Wallets SDK for advanced operations:
import { createCircleWalletsAdapter } from '@circle-fin/adapter-circle-wallets'
const adapter = createCircleWalletsAdapter({
apiKey: process.env.CIRCLE_API_KEY!,
entitySecret: process.env.CIRCLE_ENTITY_SECRET!,
})
// Access Circle's Developer Controlled Wallets and Smart Contract Platform clients
const sdk = await adapter.getSdk()
// Use Developer Controlled Wallets client
const wallets = await sdk.devc.listWallets()
console.log('Available wallets:', wallets)
// Use Smart Contract Platform client
const contracts = await sdk.scp.listContracts()
console.log('Deployed contracts:', contracts)API Reference
Factory Function
createCircleWalletsAdapter(options)
Creates a Circle Wallets adapter instance for server-side use.
Parameters:
apiKey- Circle API key (environment-prefixed or Base64 format)entitySecret- Circle entity secret (64 lowercase alphanumeric characters)baseUrl?- Optional custom Circle API base URL (default: Circle's production endpoint)
Returns: CircleWalletsAdapter instance
Throws: Validation error if API key or entity secret format is invalid
const adapter = createCircleWalletsAdapter({
apiKey: process.env.CIRCLE_API_KEY!,
entitySecret: process.env.CIRCLE_ENTITY_SECRET!,
baseUrl: 'https://api.circle.com/v1', // Optional
})Adapter Methods
getSdk()
Returns the underlying Circle Wallets SDK clients for advanced operations.
Returns: Promise<CircleWalletsSDK>
const sdk = await adapter.getSdk()
// Access Developer Controlled Wallets client
sdk.devc.listWallets()
// Access Smart Contract Platform client
sdk.scp.listContracts()Inherited Methods
The Circle Wallets Adapter extends HybridAdapter and supports all standard adapter methods:
prepare(params, ctx)- Prepare contract transactionssignTypedData(typedData, ctx)- Sign EIP-712 typed data (EVM) or messages (Solana)getAddress(ctx)- Get wallet address for specified chainvalidateChainSupport(chain)- Validate chain compatibility
All methods support the developer-controlled addressing pattern - you must specify the address in the operation context.
User-Controlled Wallets (UCW)
The /ucw/server entry (also re-exported from the main entry) builds adapters
whose signer is the user's own Circle wallet. Every state change becomes a
Circle challenge the wallet's owner approves on your surface, so the adapter
holds no signing authority. The factory accepts Circle smart contract accounts
(SCAs), so batched steps (e.g. a bridge's approve + burn) execute atomically in
one approval. The wallet helpers can still find or provision EOA wallets for
applications that manage them outside this adapter.
createCircleUserWalletAdapter(options)
Creates a user-controlled adapter. By default it also finds or creates the user's wallets before resolving.
Parameters: the Circle UCW connection (apiKey + userToken, a client,
or a proxy baseUrl), an optional onChallenge callback for approval
notifications, an optional resolveTypedDataSignature callback that enables
typed-data signing, and one of five wallet forms:
| Form | Wallet resolution | Returns |
| --- | --- | --- |
| chain | Finds or creates the wallet on that chain | single-chain ViemAdapter |
| chains | Finds or creates one wallet per chain, provisioning missing ones in one challenge | MultiChainCircleUserWalletAdapter |
| wallet | Uses the given SCA CircleUserWallet and derives its chain from wallet.blockchain | single-chain ViemAdapter |
| walletId + walletAddress + chain + accountType: 'SCA' | Uses an explicitly chain-bound SCA identity | single-chain ViemAdapter |
| wallets | Routes each chain's operations to that chain's wallet | MultiChainCircleUserWalletAdapter |
Optional across all forms: rpcUrls (per-chain overrides, keyed by EVM chain
id), onProgress, pollIntervalMs, and timeoutMs. The chain and chains
forms also accept signal while finding or creating wallets. Explicit wallet
objects and wallet sets derive their supported chains from each wallet's
blockchain; contradictory chains overrides are rejected.
Returns: Promise<ViemAdapter>, or
Promise<MultiChainCircleUserWalletAdapter> for the chains / wallets
forms. The multi-chain adapter serves both sides of a bridge from one
instance.
Throws: KitError when the options are invalid, a chain is unsupported by
Circle UCW, the user has not completed wallet setup, or approval times out.
import { createCircleUserWalletAdapter } from '@circle-fin/adapter-circle-wallets/ucw/server'
import { BridgeKit } from '@circle-fin/bridge-kit'
const adapter = await createCircleUserWalletAdapter({
apiKey: process.env.CIRCLE_API_KEY!,
userToken, // the end user's Circle session token
chains: ['Arc_Testnet', 'Base_Sepolia'],
onChallenge: ({ challengeId }) => sendToBrowser(challengeId),
})
// One adapter, both sides of the bridge — no address in the context, because
// a user-controlled adapter resolves its own operating address.
const kit = new BridgeKit()
const result = await kit.bridge({
from: { adapter, chain: 'Arc_Testnet' },
to: { adapter, chain: 'Base_Sepolia' },
amount: '1.5',
})onChallenge hands each challenge id to your application, which forwards it to
the owner's browser to approve with Circle's W3S browser SDK. The browser-safe
/ucw/client entry's createWaitForTransaction then waits for the result
through your own status endpoint — it never calls Circle directly.
Use onProgress to publish server-held lifecycle state. The strategy invokes
handlers serially in observation order, and every update carries a monotonic
sequence as a secondary guard against stale writes across transports or
retries.
Wallet Helpers
Manage the user's wallets directly — all three take the connection plus an options object, and name chains the way the kits do:
findCircleUserWallet(connection, { chain, accountType? })- the user's wallet on a chain, orundefined. Never creates one.ensureCircleUserWallet(connection, { chain, accountType?, onChallenge?, pollIntervalMs?, timeoutMs?, signal? })- finds the wallet, creating it through a challenge when missing.ensureCircleUserWallets(connection, { chains, ... })- the same for several chains, provisioning every missing one in a single challenge and returning the wallets in the requested order.
Throws: KitError when the chain is unrecognized or has no Circle
equivalent, or (for the ensure helpers) when the user has not established a
signing method yet — check for
CIRCLE_USER_WALLET_PIN_NOT_SET_ERROR_CODE via circleErrorCode(error) and
run your setup flow before retrying.
import {
ensureCircleUserWallet,
findCircleUserWallet,
} from '@circle-fin/adapter-circle-wallets/ucw/server'
const existing = await findCircleUserWallet(auth, { chain: 'Arc_Testnet' })
const wallet = await ensureCircleUserWallet(auth, {
chain: 'Arc_Testnet',
onChallenge: ({ challengeId }) => sendToBrowser(challengeId),
})
// Pass the whole wallet to skip re-resolving it.
const adapter = await createCircleUserWalletAdapter({ ...auth, wallet })Supported Chains
The Circle Wallets Adapter supports a curated subset of CCTP v2-enabled chains across both EVM and Solana ecosystems:
Mainnet (9 chains):
- Arbitrum
- Avalanche
- Base
- Ethereum
- Monad
- OP Mainnet
- Polygon PoS
- Solana
- Unichain
Testnet (10 chains):
- Arc Testnet
- Arbitrum Sepolia
- Avalanche Fuji
- Base Sepolia
- Ethereum Sepolia
- Monad Testnet
- OP Sepolia
- Polygon PoS Amoy
- Solana Devnet
- Unichain Sepolia
Note: Circle Wallets supports a subset of all CCTP v2 chains. For full CCTP v2 chain support with external wallets, use
@circle-fin/adapter-viem-v2(47 EVM chains) or@circle-fin/adapter-solana-kitfor Solana.
License
This project is licensed under the Apache 2.0 License. Contact support for details.
Ready to integrate?
Join Discord • Visit our Help-Desk • Circle Developer Docs
Built with ❤️ by Circle
