# Swap Kit

<div align="center">

[![npm version](https://badge.fury.io/js/@circle-fin%2Fswap-kit.svg)](https://badge.fury.io/js/@circle-fin%2Fswap-kit)
[![TypeScript](https://img.shields.io/badge/TypeScript-strict-blue.svg)](https://www.typescriptlang.org/)
[![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://opensource.org/licenses/Apache-2.0)
[![Discord](https://img.shields.io/discord/473781666251538452?label=Discord&logo=discord)](https://discord.com/invite/buildoncircle)

**A Circle SDK for single-chain token swaps with minimal code, backed by strong type safety and comprehensive validation.**

_Making swapping as simple as a single function call_

</div>

## Table of Contents

- [Swap Kit](#swap-kit)
  - [Overview](#overview)
  - [Installation](#installation)
  - [Quick Start](#quick-start)
  - [Configuration](#configuration)
    - [Custom Fees](#custom-fees)
    - [Custom Providers (typed)](#custom-providers-typed)
    - [Swap Configuration Options](#swap-configuration-options)
    - [Supported Tokens](#supported-tokens)
    - [Using Native Token](#using-native-token)
  - [Type Definitions](#type-definitions)
    - [SwapParams](#swapparams)
    - [CustomFeePolicy](#customfeepolicy)
  - [Validation](#validation)
  - [License](#license)
  - [Support](#support)

## 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

## Developer Documentation

- [Swap Kit Documentation](https://docs.arc.network/app-kit/swap)

## Installation

```bash
npm install @circle-fin/swap-kit
# or
yarn add @circle-fin/swap-kit
```

## Quick 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

```typescript
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: {
    kitKey: process.env.KIT_KEY,
    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)

```typescript
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: {
    kitKey: process.env.KIT_KEY,
    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:

```typescript
await kit.swap({
  from: { adapter, chain: Ethereum },
  tokenIn: 'USDC',
  tokenOut: 'DAI',
  amountIn: '100',
  config: {
    kitKey: process.env.KIT_KEY,
    customFee: {
      percentageBps: 1000, // 10% fee (100 = 1%)
      recipientAddress: '0x742d35Cc6634C0532925a3b844Bc454e4438f44e',
    },
  },
})
```

### 2. Callback-Based Fees

For complex fee logic:

**Class-Based:**

```typescript
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:**

```typescript
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)

```typescript
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

```typescript
const params = {
  from: { adapter, chain: Ethereum },
  tokenIn: 'USDC',
  tokenOut: 'USDT',
  amountIn: '100',
  config: {
    // Kit key for authentication (required)
    kitKey: process.env.KIT_KEY,

    // 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 `percentageBps` for simple fees, or kit-level `setCustomFeePolicy()` 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 Coin
- `EURC` - Euro Coin
- `USDT` - Tether USD
- `PYUSD` - PayPal USD

**Stablecoins (18 decimals):**

- `DAI` - MakerDAO stablecoin
- `USDE` - 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:**

```typescript
// Swap between 6-decimal stablecoins
await kit.swap({
  from: { adapter, chain: 'Ethereum' },
  tokenIn: 'EURC',
  tokenOut: 'USDC',
  amountIn: '100.50',
  config: { kitKey: process.env.KIT_KEY },
})

// 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
  config: { kitKey: process.env.KIT_KEY },
})

// 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',
  config: { kitKey: process.env.KIT_KEY },
})

// 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',
  config: { kitKey: process.env.KIT_KEY },
})
```

**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

```typescript
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:

```typescript
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:

```typescript
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:

```typescript
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

```typescript
interface CustomFeePolicy {
  computeFee: (params: SwapParams) => Promise<string> | string
  resolveFeeRecipientAddress: (
    chain: ChainDefinition,
    params: SwapParams,
  ) => Promise<string> | string
}
```

## Validation

SwapKit provides comprehensive runtime validation:

```typescript
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**

```typescript
// 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](https://github.com/circlefin/stablecoin-kits).
