@circle-fin/swap-kit
v1.0.1
Published
SDK for seamless token swaps
Downloads
231
Keywords
Readme
SwapKit
A strongly-typed TypeScript SDK for single-chain token swaps with comprehensive validation and type safety.
Table of Contents
Overview
SwapKit provides a foundation for building tree-shakeable swap operations with:
- 🔒 Type Safety: Full TypeScript support with strict type checking
- ✅ Runtime Validation: Comprehensive Zod schemas for parameter validation
- 📝 Rich Documentation: Complete JSDoc with runnable examples
- 🎯 Developer Experience: IntelliSense support and clear error messages
- 🔧 Flexible Configuration: Customizable fee policies and swap strategies
Installation
npm install @circle-fin/swap-kit
# or
yarn add @circle-fin/swap-kitQuick Start
SwapKit offers two usage patterns - choose the one that fits your project best:
- Chain selection is constrained to swap-supported networks. Supported chains include
mainnet networks with CCTP v2 and at least one supported token (USDC, USDT, EURC,
or other registered tokens). Current examples: Ethereum, Base, Polygon, Arbitrum,
Optimism, Avalanche, Monad, Solana, and others. Use
getSupportedChains()to get the complete, up-to-date list.
Class-Based Usage
import { SwapKit } from '@circle-fin/swap-kit'
import { createViemAdapterFromPrivateKey } from '@circle-fin/adapter-viem-v2'
// Create a SwapKit instance
const kit = new SwapKit()
// Create an adapter
const adapter = createViemAdapterFromPrivateKey({
privateKey: process.env.PRIVATE_KEY,
})
// Define swap parameters
const params = {
from: { adapter, chain: 'Ethereum' },
tokenIn: 'USDC',
tokenOut: 'USDT',
amountIn: '100.50',
config: {
slippageBps: 300, // 3% slippage tolerance
allowanceStrategy: 'permit',
},
}
// Get an estimate
const estimate = await kit.estimate(params)
console.log(
`Estimated output: ${estimate.estimatedOutput?.amount} ${estimate.estimatedOutput?.token}`,
)
// Execute the swap (implementation pending)
// const result = await kit.swap(params)Functional Usage (Tree-Shakeable)
import { createSwapKitContext, estimate, swap } from '@circle-fin/swap-kit'
import { createViemAdapterFromPrivateKey } from '@circle-fin/adapter-viem-v2'
// Create a swap context
const context = createSwapKitContext()
// Create an adapter
const adapter = createViemAdapterFromPrivateKey({
privateKey: process.env.PRIVATE_KEY,
})
// Define swap parameters
const params = {
from: { adapter, chain: 'Ethereum' },
tokenIn: 'USDC',
tokenOut: 'USDT',
amountIn: '100.50',
config: {
slippageBps: 300,
allowanceStrategy: 'permit',
},
}
// Get an estimate
const quoteResult = await estimate(context, params)
console.log(
`Estimated output: ${quoteResult.estimatedOutput?.amount} ${quoteResult.estimatedOutput?.token}`,
)
// Execute the swap (implementation pending)
// const result = await swap(context, params)Both patterns provide identical functionality - the class-based approach offers a familiar class-based interface, while the functional approach allows for better tree-shaking and smaller bundle sizes.
Configuration
Custom Fees
SwapKit supports two approaches for charging custom developer fees.
1. Percentage-Based Fees
For straightforward percentage-based fees:
await kit.swap({
from: { adapter, chain: Ethereum },
tokenIn: 'USDC',
tokenOut: 'DAI',
amountIn: '100',
config: {
customFee: {
percentageBps: 1000, // 10% fee (100 = 1%)
recipientAddress: '0x742d35Cc6634C0532925a3b844Bc454e4438f44e',
},
},
})2. Callback-Based Fees
For complex fee logic:
Class-Based:
const kit = new SwapKit({
customFeePolicy: {
computeFee: async (ctx) => {
// Full context with adapter, chain, tokens, amounts
if (ctx.type === 'output') {
// Output fee scenario
// Access to minAmount and estimatedAmount
const user = await getUser(ctx.from.address)
if (user.isVIP) {
return (parseFloat(ctx.minAmount) * 0.05).toString()
}
return (parseFloat(ctx.estimatedAmount) * 0.1).toString()
}
// Input fee scenario
return (parseFloat(ctx.amountIn) * 0.05).toString()
},
resolveFeeRecipientAddress: (chain, ctx) => {
return chain.type === 'solana'
? 'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v'
: '0x1234567890123456789012345678901234567890'
},
},
})
// Or set/update it later
kit.setCustomFeePolicy({
computeFee: (ctx) => {
if (ctx.from.chain.name === 'Ethereum') {
return (parseFloat(ctx.amountIn) * 0.01).toString()
}
return (parseFloat(ctx.amountIn) * 0.005).toString()
},
resolveFeeRecipientAddress: (chain) => '0x...',
})
// Remove the fee policy when needed
kit.removeCustomFeePolicy()Functional:
import {
createSwapKitContext,
setCustomFeePolicy,
removeCustomFeePolicy,
} from '@circle-fin/swap-kit'
const context = createSwapKitContext({
customFeePolicy: {
computeFee: async (ctx) => {
if (ctx.type === 'output') {
return (parseFloat(ctx.estimatedAmount) * 0.1).toString()
}
return (parseFloat(ctx.amountIn) * 0.05).toString()
},
resolveFeeRecipientAddress: (chain, ctx) => {
return chain.type === 'solana' ? 'Sol...' : '0x...'
},
},
})
// Or set/update later
setCustomFeePolicy(context, {
computeFee: () => '0.05',
resolveFeeRecipientAddress: (chain) => '0x...',
})
// Remove when needed
removeCustomFeePolicy(context)Custom Providers (typed)
import { createSwapKitContext } from '@circle-fin/swap-kit'
class ExperimentalSwapProvider {
readonly name = 'ExperimentalSwapProvider'
// implements SwappingProvider methods (supportsRoute, estimate, swap)
}
const extraProviders = [new ExperimentalSwapProvider()] as const
const context = createSwapKitContext({ providers: extraProviders })
// TypeScript knows the exact provider types:
type RegisteredProviders = typeof context.providers
// ^? readonly [...DefaultProviders, ExperimentalSwapProvider]The context merges default providers with any custom providers you supply while preserving their literal
types. This keeps context.providers strongly typed and ready for future provider integrations.
Swap Configuration Options
const params = {
from: { adapter, chain: Ethereum },
tokenIn: 'USDC',
tokenOut: 'USDT',
amountIn: '100',
config: {
// Allowance strategy (default: 'permit')
allowanceStrategy: 'permit', // or 'approve'
// Maximum slippage in basis points (default: 300 = 3%)
slippageBps: 300,
// Minimum output amount (stop-limit)
stopLimit: '95000000', // 95 USDT in smallest units
// Custom fee for this specific swap (percentage approach)
customFee: {
percentageBps: 100, // 1% fee
recipientAddress: '0x742d35Cc6634C0532925a3b844Bc454e4438f44e',
},
},
}Note: Use transaction-level
percentageBpsfor simple fees, or kit-levelsetCustomFeePolicy()for complex logic. They are mutually exclusive.
Supported Tokens
SwapKit supports 16 tokens for fee collection. At least one token (input OR output) must be from this list.
Stablecoins (6 decimals):
USDC- USD CoinEURC- Euro CoinUSDT- Tether USDPYUSD- PayPal USD
Stablecoins (18 decimals):
DAI- MakerDAO stablecoinUSDE- Ethena USD
Wrapped Tokens:
WBTC- Wrapped Bitcoin (8 decimals)WETH- Wrapped Ethereum (18 decimals)WSOL- Wrapped Solana (9 decimals)WAVAX- Wrapped Avalanche (18 decimals)WPOL- Wrapped Polygon (18 decimals)
Native Token:
NATIVE- Resolves to chain's native gas token (ETH on Ethereum, SOL on Solana, etc.)
Important: At least one token (input OR output) must be from the supported list. Non-supported → Non-supported swaps are not allowed.
Examples:
// Swap between 6-decimal stablecoins
await kit.swap({
from: { adapter, chain: 'Ethereum' },
tokenIn: 'EURC',
tokenOut: 'USDC',
amountIn: '100.50',
})
// Swap DAI (18 decimals) to USDC
await kit.swap({
from: { adapter, chain: 'Ethereum' },
tokenIn: 'DAI',
tokenOut: 'USDC',
amountIn: '500.0', // Automatically handles 18-decimal precision
})
// Swap native gas token to stablecoin (generic)
await kit.swap({
from: { adapter, chain: 'Ethereum' },
tokenIn: 'NATIVE', // ETH on Ethereum, POL on Polygon, SOL on Solana, etc.
tokenOut: 'USDC',
amountIn: '1.5',
})
// Swap wrapped ETH to stablecoin on Polygon
await kit.swap({
from: { adapter, chain: 'Polygon' },
tokenIn: 'WETH', // Wrapped ETH on Polygon
tokenOut: 'USDC',
amountIn: '0.5',
})Note: Use NATIVE for the chain's native gas token. Chain-specific symbols like ETH, POL are not supported as swap token aliases — use NATIVE or contract addresses instead.
Type Definitions
SwapParams
interface SwapParams {
from: AdapterContext // Source adapter and chain
tokenIn: SupportedToken // Input token
tokenOut: SupportedToken // Output token
amountIn: string // Decimal string (e.g., '10.5')
config?: SwapConfig // Optional configuration
}
// SupportedToken can be:
// - Stablecoin aliases (6 decimals): 'USDC' | 'EURC' | 'USDT' | 'PYUSD'
// - Stablecoin aliases (18 decimals): 'DAI' | 'USDE'
// - Wrapped tokens: 'WBTC' (8 dec) | 'WETH' | 'WSOL' (9 dec) | 'WAVAX' | 'WPOL' (18 dec)
// - Native token: 'NATIVE' (chain's native gas token)
type SupportedToken =
| 'USDC'
| 'EURC'
| 'USDT'
| 'USDE'
| 'DAI'
| 'PYUSD'
| 'WBTC'
| 'WETH'
| 'WSOL'
| 'WAVAX'
| 'WPOL'
| 'NATIVE'SwapEstimate
The estimate result includes input context fields for correlating estimates with inputs:
interface SwapEstimate {
// Input context (populated from your params)
tokenIn: SupportedToken // e.g., 'NATIVE'
tokenOut: SupportedToken // e.g., 'USDC'
amountIn: string // e.g., '0.00001'
chain: string // e.g., 'Ethereum'
fromAddress: string // e.g., '0x2971...5EE9f'
toAddress: string // e.g., '0x2971...5EE9f'
// Estimate details
transaction: object // Raw transaction to sign and submit
stopLimit: string // Minimum output in base units
estimatedOutput?: string // Human-readable expected output
fees?: ServiceSwapFee[] // Fee breakdown
}SwapResult
The swap result uses a simplified chain field:
interface SwapResult {
tokenIn: SupportedToken
tokenOut: SupportedToken
chain: string // e.g., 'Ethereum'
amountIn: string // Human-readable amount
fromAddress: string
toAddress: string
txHash: string
explorerUrl?: string
fees?: ServiceSwapFee[]
config?: SwapResultConfig
}Resolving Chain Name to ChainDefinition
Both SwapEstimate and SwapResult return a simplified chain string (e.g., 'Ethereum')
If you need the full chain definition, use the getChainByEnum utility:
import { getChainByEnum } from '@circle-fin/swap-kit'
// After getting a swap result or estimate
const result = await kit.swap(params)
// Resolve the chain name back to full ChainDefinition
const chainDef = getChainByEnum(result.chain)
console.log(chainDef.chainId) // 1 (for Ethereum)
console.log(chainDef.rpcEndpoints) // ['https://...']CustomFeePolicy
interface CustomFeePolicy {
computeFee: (params: SwapParams) => Promise<string> | string
resolveFeeRecipientAddress: (
chain: ChainDefinition,
params: SwapParams,
) => Promise<string> | string
}Validation
SwapKit provides comprehensive runtime validation:
import { assertSwapParams, swapParamsSchema } from '@circle-fin/swap-kit'
try {
assertSwapParams(params, swapParamsSchema)
// Parameters are valid
} catch (error) {
console.error('Validation failed:', error.message)
}Usage Patterns: Class vs Functional
SwapKit supports both class-based and functional programming styles. Choose based on your preferences:
Use Class-Based (SwapKit) When:
- ✅ You prefer traditional class-based patterns
- ✅ Your codebase already uses class-based libraries
- ✅ You want a familiar API similar to other SDKs
- ✅ You don't need aggressive tree-shaking optimizations
Use Functional (createSwapKitContext + operations) When:
- ✅ You prefer functional programming patterns
- ✅ You need maximum tree-shaking for smaller bundles
- ✅ You want to import only specific operations
- ✅ You're building a performance-critical application
Both approaches:
- ✅ Provide identical functionality
- ✅ Use the same underlying operations
- ✅ Offer full TypeScript support
- ✅ Include comprehensive validation
Example: Choosing an approach
// Class-based: Great for general use
import { SwapKit } from '@circle-fin/swap-kit'
const kit = new SwapKit()
await kit.swap(params)
// Functional: Great for tree-shaking
import { createSwapKitContext, swap } from '@circle-fin/swap-kit'
const context = createSwapKitContext()
await swap(context, params)License
Apache-2.0
Support
For issues and questions, please visit the GitHub repository.
