Build a Swap Interface
Last modified:
Overview
This tutorial walks through building a minimal React application that:
- Connects a wallet
- Gets a swap quote from QuoterV2
- Executes the swap via SwapRouter02
By the end, you'll have a working swap page that talks to CenturionDEX v3 on-chain.
Prerequisites
- Node.js 18+
- A wallet with testnet CTN (Sepolia)
- Basic familiarity with React and ethers.js
1. Project Setup
npx create-react-app centurion-swap --template typescript
cd centurion-swap
npm install ethers@6 @centurion-dex/v3-sdk @centurion-dex/sdk-core2. Constants
Create src/constants.ts:
import { Token, ChainId } from '@centurion-dex/sdk-core'
export const SWAP_ROUTER_ADDRESS = '0x68b3465833fb72A70ecDF485E0e4C7bD8665Fc45'
export const QUOTER_ADDRESS = '0x61fFE014bA17989E743c5F6cB21bF9697530B21e'
export const WCTN = new Token(
ChainId.MAINNET,
'0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2',
18,
'WCTN'
)
export const USDC = new Token(
ChainId.MAINNET,
'0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48',
6,
'USDC'
)
export const POOL_FEE = 3000 // 0.30%3. Getting a Quote
Create src/quote.ts:
import { ethers } from 'ethers'
import { QUOTER_ADDRESS, WCTN, USDC, POOL_FEE } from './constants'
const QUOTER_ABI = [
'function quoteExactInputSingle((address tokenIn, address tokenOut, uint256 amountIn, uint24 fee, uint160 sqrtPriceLimitX96)) external returns (uint256 amountOut, uint160 sqrtPriceX96After, uint32 initializedTicksCrossed, uint256 gasEstimate)',
]
export async function getQuote(
provider: ethers.Provider,
amountIn: bigint
): Promise<bigint> {
const quoter = new ethers.Contract(QUOTER_ADDRESS, QUOTER_ABI, provider)
const [amountOut] = await quoter.quoteExactInputSingle.staticCall({
tokenIn: WCTN.address,
tokenOut: USDC.address,
amountIn,
fee: POOL_FEE,
sqrtPriceLimitX96: 0,
})
return amountOut
}Key points:
- Use
staticCall— the quoter is designed to revert with the result, so we simulate rather than send a transaction sqrtPriceLimitX96: 0means no price limit
4. Executing the Swap
Create src/swap.ts:
import { ethers } from 'ethers'
import { SWAP_ROUTER_ADDRESS, WCTN, USDC, POOL_FEE } from './constants'
const ROUTER_ABI = [
'function exactInputSingle((address tokenIn, address tokenOut, uint24 fee, address recipient, uint256 amountIn, uint256 amountOutMinimum, uint160 sqrtPriceLimitX96)) external payable returns (uint256 amountOut)',
]
export async function executeSwap(
signer: ethers.Signer,
amountIn: bigint,
amountOutMinimum: bigint
): Promise<ethers.TransactionResponse> {
const router = new ethers.Contract(SWAP_ROUTER_ADDRESS, ROUTER_ABI, signer)
const recipient = await signer.getAddress()
const tx = await router.exactInputSingle(
{
tokenIn: WCTN.address,
tokenOut: USDC.address,
fee: POOL_FEE,
recipient,
amountIn,
amountOutMinimum,
sqrtPriceLimitX96: 0,
},
{ value: amountIn } // Send CTN which gets wrapped to WCTN by the router
)
return tx
}Key points:
amountOutMinimumis your slippage protection — set it to e.g., 99% of the quoted amount- Sending
valuewith the call wraps CTN → WCTN automatically - The router handles the WCTN approval internally when sending native CTN
5. Putting It Together
In your React component:
async function handleSwap() {
const provider = new ethers.BrowserProvider(window.ethereum)
const signer = await provider.getSigner()
const amountIn = ethers.parseEther('0.1') // 0.1 CTN
// Step 1: Get quote
const amountOut = await getQuote(provider, amountIn)
console.log(`Expected output: ${ethers.formatUnits(amountOut, 6)} USDC`)
// Step 2: Apply 1% slippage tolerance
const amountOutMinimum = amountOut * 99n / 100n
// Step 3: Execute
const tx = await executeSwap(signer, amountIn, amountOutMinimum)
const receipt = await tx.wait()
console.log(`Swap confirmed in block ${receipt.blockNumber}`)
}6. Error Handling
Common failure modes to handle:
try {
const tx = await executeSwap(signer, amountIn, amountOutMinimum)
await tx.wait()
} catch (error: any) {
if (error.code === 'ACTION_REJECTED') {
// User rejected the transaction in their wallet
} else if (error.message?.includes('Too little received')) {
// Slippage tolerance exceeded — price moved too much
// Suggest the user increase slippage or retry
} else if (error.message?.includes('Transaction too old')) {
// Deadline passed — transaction was pending too long
} else {
// Unexpected error — log and display to user
console.error('Swap failed:', error)
}
}Next Steps
- Add ERC-20 token approvals for non-CTN input tokens (use
token.approve(SWAP_ROUTER_ADDRESS, amount)) - Add multi-hop swaps with
exactInputand an encoded path - Display real-time quotes as the user types
- See the SDK swap guides for more advanced patterns