# FX Swap Smart Contract Integration Guide The FxSwap system is a StableSwap AMM (Automated Market Maker) designed for foreign exchange trading pairs with integrated oracle-based pricing. The system consists of a factory pattern for pool creation, individual pool contracts for liquidity management, and routing capabilities for multi-hop swaps. ## Smart Contract Components ### FxSwapManager Factory contract for creating and managing FX swap pools ## Pool Creation & Initial Liquidity ### FxSwapManager.create() The factory contract deploys new trading pools with initial liquidity in a single transaction. **Function Signature:** ```solidity function create( address[2] memory _tokens, address[2] memory _oracles, uint256[2] memory amounts, uint256 minShares, uint256 _A, uint256 _baseFee, uint256 _feeMultiplier, bytes32 _salt ) external returns (address poolAddress, uint256 shares) ``` **Parameters:** - `_tokens`: Array of two ERC20 token addresses (must be sorted by address) - `_oracles`: Array of two oracle addresses providing price feeds for each token - `amounts`: Array of initial token amounts to deposit [token0Amount, token1Amount] - `minShares`: Minimum LP tokens expected from initial deposit (slippage protection) - `_A`: Amplification parameter for StableSwap curve (higher = more stable) - `_baseFee`: Base trading fee in 10 decimals (e.g., 1e8 = 1%) - `_feeMultiplier`: Dynamic fee multiplier in 10 decimals - `_salt`: Bytes32 salt for deterministic pool address generation **Returns:** - `poolAddress`: Address of the newly created pool contract - `shares`: Number of LP tokens minted to the caller **System Integration:** 1. Validates token pair doesn't already exist 2. Deploys pool contract using CREATE2 for deterministic addresses 3. Transfers initial tokens from caller to pool 4. Mints LP tokens representing proportional ownership 5. Emits pool creation event for indexing ### Adding Liquidity to Existing Pools `FxSwap` address is initialized from create2 from the create() factory return using deterministic clones as ```_fxSwap = Clones.cloneDeterministic(implementation, _salt);``` ### FxSwap.addLiquidity() Adds liquidity to an existing pool in exchange for LP tokens. **Function Signature:** ```solidity function addLiquidity( uint256[2] memory amounts, uint256 minShares, address receiver ) external nonReentrant returns (uint256 shares) ``` **Parameters:** - `amounts`: Array of token amounts to deposit [token0Amount, token1Amount] - `minShares`: Minimum LP tokens expected (slippage protection) - `receiver`: Address to receive LP tokens **Returns:** - `shares`: Number of LP tokens minted **Fee Calculation:** - **Dynamic Fee Structure**: Fees are calculated based on pool imbalance - **Imbalanced Deposits**: Adding liquidity at ratios different from current pool ratio incurs higher fees - **Admin Fee**: 50% of trading fees go to protocol treasury - **Fee Formula**: `dynamicFee = avgIn.calculateDynamicFee(avgOut, baseFee, feeMultiplier)` **System Integration:** 1. Validates deposit amounts and approvals 2. Calculates virtual balances using oracle rates 3. Computes invariant (D) before and after deposit 4. Applies dynamic fees for imbalanced deposits 5. Mints LP tokens proportional to pool ownership increase 6. Updates internal balances and emits events ### FxSwap.getMinShares() Calculates minimum shares for a given liquidity deposit without execution. **Function Signature:** ```solidity function getMinShares(uint256[2] memory amounts) external view returns (uint256) ``` **Parameters:** - `amounts`: Array of token amounts [token0Amount, token1Amount] **Returns:** - Estimated LP tokens that would be mint ## Token Swapping ### FxSwap.exchange() Executes token swaps within individual pools using the StableSwap curve. **Function Signature:** ```solidity function exchange( address tokenIn, address tokenOut, uint256 amountIn ) external nonReentrant returns (uint256 amountOut) ``` **Parameters:** - `tokenIn`: Address of input token to swap from - `tokenOut`: Address of output token to swap to - `amountIn`: Amount of input tokens to swap **Returns:** - `amountOut`: Amount of output tokens received **System Integration:** 1. Validates token addresses are part of the pool 2. Calculates virtual balances using oracle rates 3. Applies dynamic fee based on pool imbalance 4. Executes swap using StableSwap curve mathematics 5. Updates pool balances and collects fees 6. Transfers output tokens to caller ### FxSwap.getAmountOut() Calculates expected output amount for a given input without executing the swap. **Function Signature:** ```solidity function getAmountOut( address tokenIn, address tokenOut, uint256 amountIn ) external view returns (uint256 amountOut) ``` **Parameters:** - `tokenIn`: Address of input token - `tokenOut`: Address of output token - `amountIn`: Amount of input tokens **Returns:** - `amountOut`: Expected amount of output tokens **Usage Pattern:** ```solidity // Get quote before executing swap uint256 expectedOut = FxSwap(poolAddress).getAmountOut(tokenA, tokenB, amountIn); uint256 minAmountOut = expectedOut ; FxSwap(poolAddress).exchange(tokenA, tokenB, amountIn); ``` ### FxSwap.getVirtualPrice() Returns the current virtual price of the pool's LP tokens. **Function Signature:** ```solidity function getVirtualPrice() external view returns (uint256 price) ``` **Returns:** - `price`: Virtual price of LP tokens in underlying asset terms **Usage:** - Monitor pool health and performance - Calculate LP token value for portfolio tracking - Assess arbitrage opportunities ### FxSwap.getDynamicFee() Calculates the current dynamic fee rate based on pool imbalance. **Function Signature:** ```solidity function getDynamicFee( address tokenIn, address tokenOut, uint256 amountIn ) external view returns (uint256 fee) ``` **Parameters:** - `tokenIn`: Address of input token - `tokenOut`: Address of output token - `amountIn`: Amount of input tokens **Returns:** - `fee`: Dynamic fee rate in basis points **Fee Calculation:** - **Base Component**: Fixed `baseFee` parameter - **Dynamic Component**: Adjusts based on pool imbalance using `feeMultiplier` ## Multi-Hop Routing ### FxRouter.swap() Executes single or multi-hop swaps across multiple pools. **Function Signature:** ```solidity function swap( address tokenIn, address tokenOut, uint256 amountIn, uint256 minAmountOut, Path[] memory _path ) external nonReentrant ``` **Parameters:** - `direction: 0` - Swap from token0 to token1 - `direction: 1` - Swap from token1 to token0 - `tokenIn`: Address of initial input token router.swap(chNGN, USDC, amountIn, minAmountOut, path); - Router uses `forceApprove()` to handle token approvals efficiently - Batch operations reduce transaction costs - Router-level slippage protection via `minAmountOut` - Dynamic fees adjust based on each pool's imbalance ``` **Multi-hop swap (chNGN → USDC → chTRY):** ```solidity FxRouter.Path[] memory path = new FxRouter.Path[](2); path[0] = FxRouter.Path({ pool: chNGN_USDC_pool, direction: 0 // chNGN → USDC }); path[1] = FxRouter.Path({ pool: chTRY_USDC_pool, direction: 1 // USDC → chTRY }); roter.swap(chNGN, chTRY, amountIn, minAmountOut, path); ``` ## Core Integration Patterns Code Snippets ### Contract ABIs First, create the ABI files. Here's an example dummy ERC20 ABI for token interactions: ```json // src/abis/ERC20.json [ { "inputs": [], "name": "symbol", "outputs": [{ "internalType": "string", "name": "", "type": "string" }], "stateMutability": "view", "type": "function" } ] ``` ### Network Configuration ```typescript // src/constants/addresses.ts export const NETWORKS = { mainnet: { chainId: 1, name: 'Avax Mainnet', fxSwapManager: '0x...', fxRouter: '0x...', subgraphUrl: 'https://api.thegraph.com/subgraphs/name/your-subgraph', usdc: '0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2' }, avax: { chainId: 111, name: 'Avax-Fuji Testnet', fxSwapManager: '0x...', fxRouter: '0x...', subgraphUrl: 'https://api.thegraph.com/subgraphs/name/your-subgraph-sepolia', cNGN: '0xfFf9976782d46CC05630D1f6eBAb18b2324d6B14' } } as const; // src/constants/tokens.ts export const TOKENS = { CNGN: { address: '0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2', symbol: 'cNGN', decimals: 18, name: 'c-Naira' }, USDC: { address: '0xA0b86a33E6441d6b3A9D73cEB3d3c3C8F4E6e1c9', symbol: 'USDC', decimals: 6, name: 'USDC Coin' }, } as const; ``` ## Step-by-Step Integration Guide - Step 1: Setting Up Wallet Connection - Step 2: Building Custom React Hooks ``` // src/hooks/usePoolCreation.ts import { useState, useCallback } from 'react'; import { useAccount, usePublicClient, useWalletClient } from 'wagmi'; import { getContract, parseEther, formatEther, keccak256, encodePacked } from 'viem'; import { NETWORKS } from '../constants/addresses'; import FxSwapManagerABI from '../abis/FxSwapManager.json'; import ERC20ABI from '../abis/ERC20.json'; interface CreatePoolParams { baseToken: string; // Token 1 address quoteToken: string; // Token 2 address baseOracle: string; // Oracle for token 1 quoteOracle: string; // Oracle for token 2 baseAmount: number; // Amount of token 1 to deposit amplificationParameter: number; // Pool amplification (e.g., 100) baseFee: number; // Base fee (e.g., 3e8 for 0.3%) feeMultiplier: number; // Fee multiplier (e.g., 5e10 for 5x) } export function usePoolCreation() { const { address } = useAccount(); const publicClient = usePublicClient(); const { data: walletClient } = useWalletClient(); const [loading, setLoading] = useState(false); const [error, setError] = useState<string | null>(null); const createPool = useCallback(async (params: CreatePoolParams) => { if (!walletClient || !address) { throw new Error('Wallet not connected'); } try { setLoading(true); setError(null); const { baseToken, quoteToken, baseOracle, quoteOracle, baseAmount, amplificationParameter, baseFee, feeMultiplier } = params; // Get oracle prices console.log('Getting oracle prices...'); const [basePrice, quotePrice] = await getOraclePrices(baseOracle, quoteOracle); // Calculate required amount of second token console.log('Calculating paired amount...'); const pairedAmount = await calculatePairedAmount( baseAmount, basePrice, quotePrice ); // Calculate minimum shares needed console.log('Calculating minimum shares...'); const minShares = await calculateMinShares( [baseToken, quoteToken], [baseAmount, pairedAmount], amplificationParameter, [basePrice, quotePrice] ); // Get pool address (deterministic) const poolAddress = await getPoolAddress(baseToken, quoteToken); // Approve tokens console.log('Approving tokens...'); await approveTokens(baseToken, quoteToken, baseAmount, pairedAmount, poolAddress); // Create the pool console.log('Creating pool...'); const hash = await createPoolTransaction( baseToken, quoteToken, baseOracle, quoteOracle, baseAmount, pairedAmount, minShares, amplificationParameter, baseFee, feeMultiplier ); // Wait for confirmation const receipt = await publicClient?.waitForTransactionReceipt({ hash }); return { poolAddress, transactionHash: hash, shares: minShares, success: true }; } catch (err) { const errorMessage = err instanceof Error ? err.message : 'Pool creation failed'; setError(errorMessage); throw new Error(errorMessage); } finally { setLoading(false); } }, [walletClient, address, publicClient]); // Helper function: Get oracle prices const getOraclePrices = async (baseOracle: string, quoteOracle: string) => { const baseOracleContract = getContract({ address: baseOracle as `0x${string}`, abi: [ { inputs: [], name: "latestData", outputs: [{ internalType: "int192", name: "rate", type: "int192" }], stateMutability: "view", type: "function" } ], client: publicClient }); const quoteOracleContract = getContract({ address: quoteOracle as `0x${string}`, abi: [ { inputs: [], name: "latestData", outputs: [{ internalType: "int192", name: "rate", type: "int192" }], stateMutability: "view", type: "function" } ], client: publicClient }); const [baseData, quoteData] = await Promise.all([ baseOracleContract.read.latestData(), quoteOracleContract.read.latestData() ]); return [baseData.rate, quoteData.rate]; }; // Helper function: Calculate paired amount const calculatePairedAmount = async (baseAmount: number, basePrice: bigint, quotePrice: bigint) => { const managerContract = getContract({ address: NETWORKS.mainnet.fxSwapManager as `0x${string}`, abi: FxSwapManagerABI, client: publicClient }); const pairedAmount = await managerContract.read.getPairedAmount([ parseEther(baseAmount.toString()), [basePrice, quotePrice] ]); return parseFloat(formatEther(pairedAmount as bigint)); }; // Helper function: Calculate minimum shares const calculateMinShares = async ( tokens: [string, string], amounts: [number, number], amplificationParameter: number, oracleRates: [bigint, bigint] ) => { const managerContract = getContract({ address: NETWORKS.mainnet.fxSwapManager as `0x${string}`, abi: FxSwapManagerABI, client: publicClient }); const minShares = await managerContract.read.getCreateMinShares([ tokens, [parseEther(amounts[0].toString()), pairedAmount.toString()], amplificationParameter, oracleRates ]); return minShares as bigint; }; // Helper function: Get pool address const getPoolAddress = async (baseToken: string, quoteToken: string) => { const managerContract = getContract({ address: NETWORKS.mainnet.fxSwapManager as `0x${string}`, abi: FxSwapManagerABI, client: publicClient }); const salt = keccak256( encodePacked(['address', 'address'], [baseToken as `0x${string}`, quoteToken as `0x${string}`]) ); const address = await managerContract.read.getDeterministicAddress([salt]); return address as string; }; // Helper function: Approve tokens const approveTokens = async ( baseToken: string, quoteToken: string, baseAmount: number, quoteAmount: number, poolAddress: string ) => { const baseTokenContract = getContract({ address: baseToken as `0x${string}`, abi: ERC20ABI, client: walletClient }); const quoteTokenContract = getContract({ address: quoteToken as `0x${string}`, abi: ERC20ABI, client: walletClient }); // Approve base token await baseTokenContract.write.approve([ poolAddress as `0x${string}`, parseEther(baseAmount.toString()) ]); // Approve quote token await quoteTokenContract.write.approve([ poolAddress as `0x${string}`, parseEther(quoteAmount.toString()) ]); }; // Helper function: Create pool transaction const createPoolTransaction = async ( baseToken: string, quoteToken: string, baseOracle: string, quoteOracle: string, baseAmount: number, quoteAmount: number, minShares: bigint, amplificationParameter: number, baseFee: number, feeMultiplier: number ) => { const managerContract = getContract({ address: NETWORKS.mainnet.fxSwapManager as `0x${string}`, abi: FxSwapManagerABI, client: walletClient }); const salt = keccak256( encodePacked(['address', 'address'], [baseToken as `0x${string}`, quoteToken as `0x${string}`]) ); const hash = await managerContract.write.create([ [baseToken, quoteToken], // Token addresses [baseOracle, quoteOracle], // Oracle addresses [parseEther(baseAmount.toString()), parseEther(pairedAmount.toString())], // Token amounts minShares, // Minimum shares amplificationParameter, // Amplification parameter baseFee, // Base fee feeMultiplier, // Fee multiplier salt // Salt for deterministic address ]); return hash; }; return { createPool, loading, error }; } ``` ----------------------------------------------- Now let's create the core hooks that will handle our swap functionality: ## Building Custom React Hooks ### Creating the useFxSwap Hook We create two functions `getPoolImmutables` and `getPoolState` which do the actual data fetching and return the data corresponding to their respective interface. Since we will only read from that contract and make no transactions it’s enough to use a provider rather than a signer (a provider with a wallet connected to it). Next up we create two interfaces `Immutables` and `State` modelling the on-chain pool data that we need. ``` interface PoolImmutables { token0: string; token1: string; fee: number; direction: number; } interface PoolState { virtualPrice: bigint; dynamicFee: bigint; balances: [bigint, bigint]; totalSupply: bigint; } ``` ```typescript // src/hooks/useFxSwap.ts import { useState, useCallback } from 'react'; import { useAccount, usePublicClient, useWalletClient } from 'wagmi'; import { getContract, parseEther, formatEther } from 'viem'; import { NETWORKS } from '../constants/addresses'; import FxSwapABI from '../abis/FxSwap.json'; import FxRouterABI from '../abis/FxRouter.json'; import FxSwapManagerABI from '../abis/FxSwapManager.json'; interface PoolImmutables { token0: string; token1: string; fee: number; direction: number; } interface PoolState { virtualPrice: bigint; dynamicFee: bigint; balances: [bigint, bigint]; totalSupply: bigint; } interface SwapPath { pool: string; direction: number; } export function useFxSwap() { const { address } = useAccount(); const publicClient = usePublicClient(); const { data: walletClient } = useWalletClient(); const [loading, setLoading] = useState(false); const [error, setError] = useState<string | null>(null); const network = NETWORKS.mainnet; // You can make this dynamic based on current chain // Get pool immutables (static data that doesn't change) const getPoolImmutables = useCallback(async (poolAddress: string): Promise<PoolImmutables> => { const poolContract = getContract({ address: poolAddress as `0x${string}`, abi: FxSwapABI, publicClient }); const [tokens] = await poolContract.read.getTokens(); const [token0, token1] = tokens as [string, string]; return { token0, token1, fee: 0, // FX Swap uses dynamic fees direction: 0 // Will be determined by token order }; }, [publicClient]); // Get pool state (dynamic data that changes) const getPoolState = useCallback(async (poolAddress: string): Promise<PoolState> => { const poolContract = getContract({ address: poolAddress as `0x${string}`, abi: FxSwapABI, publicClient }); const [virtualPrice, dynamicFee, balances, totalSupply] = await Promise.all([ poolContract.read.getVirtualPrice(), poolContract.read.getDynamicFee(), poolContract.read.getVirtualBalances(), poolContract.read.totalSupply() ]); return { virtualPrice: virtualPrice as bigint, dynamicFee: dynamicFee as bigint, balances: balances as [bigint, bigint], totalSupply: totalSupply as bigint }; }, [publicClient]); // Get quote for a swap const getQuote = useCallback(async ( tokenIn: string, tokenOut: string, amountIn: number ): Promise<number> => { try { setError(null); setLoading(true); const routerContract = getContract({ address: network.fxRouter as `0x${string}`, abi: FxRouterABI, publicClient }); // Find the optimal path const path = await findOptimalPath(tokenIn, tokenOut); // Get amount out const amountOut = await routerContract.read.getAmountOut([ parseEther(amountIn.toString()), path ]); return parseFloat(formatEther(amountOut as bigint)); } catch (err) { setError(err instanceof Error ? err.message : 'Failed to get quote'); return 0; } finally { setLoading(false); } }, [network.fxRouter, publicClient]); const findOptimalPath = useCallback(async ( tokenIn: string, tokenOut: string ): Promise<RouterPath[]> => { try { const directPath = await findDirectPath(tokenIn, tokenOut); return directPath; } catch (directError) { // If direct path fails, try multi-hop via USDC console.log('Direct path failed, attempting multi-hop via USDC'); try { const multiHopPath = await findMultiHopPath(tokenIn, tokenOut); return multiHopPath; } catch (multiHopError) { throw new Error(`No available path: Direct failed (${directError.message}), Multi-hop failed (${multiHopError.message})`); } } }, [findDirectPath, findMultiHopPath]); const findMultiHopPath = useCallback(async ( tokenIn: string, tokenOut: string, intermediateToken: string = network.usdc ): Promise<RouterPath[]> => { if (tokenIn === intermediateToken || tokenOut === intermediateToken) { throw new Error('Cannot route through same token'); } try { // First hop: tokenIn -> intermediate (e.g., NGN -> USDC) const path1 = await findDirectPath(tokenIn, intermediateToken); // Second hop: intermediate -> tokenOut (e.g., USDC -> TRY) const path2 = await findDirectPath(intermediateToken, tokenOut); return [...path1, ...path2]; } catch (err) { throw new Error(`Multi-hop routing failed: ${err.message}`); } }, [network.usdc, findDirectPath]); const findDirectPath = useCallback(async ( tokenA: string, tokenB: string ): Promise<RouterPath[]> => { const managerContract = getContract({ address: network.fxSwapManager as `0x${string}`, abi: FxSwapManagerABI, publicClient }); const poolInfo = await managerContract.read.getPool([tokenA, tokenB]); const [poolAddress] = poolInfo; // Check if pool exists if (!poolAddress || poolAddress === '0x0000000000000000000000000000000000000000') { throw new Error(`No pool found for ${tokenA} -> ${tokenB}`); } const poolContract = getContract({ address: poolAddress as `0x${string}`, abi: FxSwapABI, publicClient }); const [tokens] = await poolContract.read.getTokens(); const [token0, token1] = tokens as [string, string]; const direction = token0.toLowerCase() === tokenA.toLowerCase() ? 0 : 1; return [{ pool: poolAddress, direction: direction }]; }, [network.fxSwapManager, publicClient]); // Execute swap const swap = useCallback(async ( tokenIn: string, tokenOut: string, amountIn: number, minAmountOut: number, slippageTolerance: number = 0.5 ) => { if (!walletClient || !address) { throw new Error('Wallet not connected'); } try { setError(null); setLoading(true); const routerContract = getContract({ address: network.fxRouter as `0x${string}`, abi: FxRouterABI, walletClient }); // Calculate minimum amount out with slippage const minAmountOutWithSlippage = minAmountOut * (1 - slippageTolerance / 100); // Find optimal path const path = await findOptimalPath(tokenIn, tokenOut); // Execute swap const hash = await routerContract.write.swap([ tokenIn, tokenOut, parseEther(amountIn.toString()), parseEther(minAmountOutWithSlippage.toString()), path ], { gas: 500000n // You may need to adjust this }); return { hash }; } catch (err) { setError(err instanceof Error ? err.message : 'Swap failed'); throw err; } finally { setLoading(false); } }, [walletClient, address, network.fxRouter, findOptimalPath]); return { getQuote, swap, getPoolImmutables, getPoolState, loading, error }; } ``` This `swap()` above accepts the amount of USDC to be swapped and the minimum expected cNGN output as arguments. The function first validates that the router contract has been properly initialized, then sets up the token addresses for USDC (input) and cNGN (output). The core swap execution happens through the `routerContract.write.swap()` call, which takes an array of parameters including the input token, output token, input amount, minimum output amount (with slippage protection), and the swap path. A custom gas limit of 500,000 is set because the gas estimation might not be accurate for this type of transaction. The function returns the transaction hash, which can be used to track the transaction status on the blockchain. ## Fetching Pool Data and Spot Prices ```typescript // src/hooks/usePoolData.ts import { useState, useEffect, useCallback } from 'react'; import { usePublicClient } from 'wagmi'; import { getContract } from 'viem'; import { NETWORKS } from '../constants/addresses'; import FxSwapABI from '../abis/FxSwap.json'; interface PoolData { virtualPrice: number; spotPrice: number; dynamicFee: number; balanceRatio: number; tvl: number; apr: number; } export function usePoolData(poolAddress: string) { const [poolData, setPoolData] = useState<PoolData | null>(null); const [loading, setLoading] = useState(false); const publicClient = usePublicClient(); const fetchPoolData = useCallback(async () => { if (!poolAddress || !publicClient) return; try { setLoading(true); const poolContract = getContract({ address: poolAddress as `0x${string}`, abi: FxSwapABI, publicClient }); const [virtualPrice, spotPrice, dynamicFee, balanceRatio] = await Promise.all([ poolContract.read.getVirtualPrice(), poolContract.read.getSpotPrice(), poolContract.read.getDynamicFee(), poolContract.read.getBalanceRatio() ]); // Convert to human-readable format const data: PoolData = { virtualPrice: Number(virtualPrice) / 1e18, spotPrice: Number(spotPrice) / 1e18, dynamicFee: Number(dynamicFee) / 1e4, // Assuming fee is in basis points balanceRatio: Number(balanceRatio) / 1e18, tvl: 0, // Would need to calculate based on balances and prices apr: 0 // Would need historical data }; setPoolData(data); } catch (error) { console.error('Error fetching pool data:', error); } finally { setLoading(false); } }, [poolAddress, publicClient]); useEffect(() => { fetchPoolData(); // Set up interval to refresh data const interval = setInterval(fetchPoolData, 30000); // Refresh every 30 seconds return () => clearInterval(interval); }, [fetchPoolData]); return { poolData, loading, refresh: fetchPoolData }; } ``` ## Token Approval and Balance Management ```typescript // src/hooks/useTokenApproval.ts import { useState, useCallback } from 'react'; import { useAccount, usePublicClient, useWalletClient } from 'wagmi'; import { getContract, parseEther, maxUint256 } from 'viem'; import ERC20ABI from '../abis/ERC20.json'; export function useTokenApproval() { const { address } = useAccount(); const publicClient = usePublicClient(); const { data: walletClient } = useWalletClient(); const [loading, setLoading] = useState(false); const checkAllowance = useCallback(async ( tokenAddress: string, spenderAddress: string, amount: bigint ): Promise<boolean> => { if (!address || !publicClient) return false; try { const tokenContract = getContract({ address: tokenAddress as `0x${string}`, abi: ERC20ABI, publicClient }); const allowance = await tokenContract.read.allowance([ address, spenderAddress ]); return (allowance as bigint) >= amount; } catch (error) { console.error('Error checking allowance:', error); return false; } }, [address, publicClient]); const approveToken = useCallback(async ( tokenAddress: string, spenderAddress: string, amount?: bigint ) => { if (!walletClient || !address) { throw new Error('Wallet not connected'); } try { setLoading(true); const tokenContract = getContract({ address: tokenAddress as `0x${string}`, abi: ERC20ABI, walletClient }); const hash = await tokenContract.write.approve([ spenderAddress, amount || maxUint256 ]); return { hash }; } catch (error) { console.error('Error approving token:', error); throw error; } finally { setLoading(false); } }, [walletClient, address]); const ensureApproval = useCallback(async ( tokenAddress: string, spenderAddress: string, amount: bigint ) => { const hasAllowance = await checkAllowance(tokenAddress, spenderAddress, amount); if (!hasAllowance) { const result = await approveToken(tokenAddress, spenderAddress, amount); return result; } return null; }, [checkAllowance, approveToken]); return { checkAllowance, approveToken, ensureApproval, loading }; } ``` ### Balance Management Hook The watch property makes sure that our balance is always up-to-date by checking if the balance changed after each new block. To directly see how much USDC you are able to swap and to get some sort of feedback on the result of the swap it would be nice to display the current balance. This template can be achieved with wagmi or alternate framework. ```typescript // src/hooks/useBalances.ts import { useBalance } from 'wagmi'; import { useAccount } from 'wagmi'; export function useTokenBalances(tokenAddresses: string[]) { const { address } = useAccount(); const ethBalance = useBalance({ address, watch: true, }); const tokenBalances = tokenAddresses.map(tokenAddress => useBalance({ address, token: tokenAddress as `0x${string}`, watch: true, }) ); return { ethBalance, tokenBalances, isLoading: ethBalance.isLoading || tokenBalances.some(b => b.isLoading), error: ethBalance.error || tokenBalances.find(b => b.error)?.error }; } ``` ## Executing Swaps ### Complete Swap Hook integration template ```typescript // src/hooks/useSwapExecution.ts import { useState, useCallback } from 'react'; import { useAccount, usePublicClient, useWalletClient } from 'wagmi'; import { parseEther, formatEther } from 'viem'; import { useFxSwap } from './useFxSwap'; import { useTokenApproval } from './useTokenApproval'; import { NETWORKS } from '../constants/addresses'; interface SwapParams { tokenIn: string; tokenOut: string; amountIn: number; slippageTolerance: number; } interface SwapResult { hash: string; expectedOutput: number; actualOutput?: number; gasUsed?: bigint; } export function useSwapExecution() { const { address } = useAccount(); const publicClient = usePublicClient(); const { getQuote, swap: executeSwap } = useFxSwap(); const { ensureApproval } = useTokenApproval(); const [loading, setLoading] = useState(false); const [error, setError] = useState<string | null>(null); const executeCompleteSwap = useCallback(async ( params: SwapParams ): Promise<SwapResult> => { if (!address) { throw new Error('Wallet not connected'); } try { setLoading(true); setError(null); const { tokenIn, tokenOut, amountIn, slippageTolerance } = params; const network = NETWORKS.mainnet; // Step 1: Get quote console.log('Getting quote...'); const expectedOutput = await getQuote(tokenIn, tokenOut, amountIn); if (expectedOutput === 0) { throw new Error('Unable to get quote for this swap'); } // Step 2: Ensure token approval console.log('Checking token approval...'); const amountInWei = parseEther(amountIn.toString()); await ensureApproval(tokenIn, network.fxRouter, amountInWei); // Step 3: Calculate minimum amount out with slippage const minAmountOut = expectedOutput * (1 - slippageTolerance / 100); // Step 4: Execute swap console.log('Executing swap...'); const swapResult = await executeSwap( tokenIn, tokenOut, amountIn, minAmountOut, slippageTolerance ); // Step 5: Wait for transaction and get receipt if (publicClient) { const receipt = await publicClient.waitForTransactionReceipt({ hash: swapResult.hash as `0x${string}` }); return { hash: swapResult.hash, expectedOutput, gasUsed: receipt.gasUsed }; } return { hash: swapResult.hash, expectedOutput }; } catch (err) { const errorMessage = err instanceof Error ? err.message : 'Swap execution failed'; setError(errorMessage); throw new Error(errorMessage); } finally { setLoading(false); } }, [address, getQuote, ensureApproval, executeSwap, publicClient]); return { executeCompleteSwap, loading, error }; } ``` ## Building the Swap Interface ```typescript // src/components/SwapCard.tsx import React, { useState, useCallback, useEffect } from 'react'; import { useAccount } from 'wagmi'; import { ConnectButton } from '@rainbow-me/rainbowkit'; import { useFxSwap } from '../hooks/useFxSwap'; import { useSwapExecution } from '../hooks/useSwapExecution'; import { useTokenBalances } from '../hooks/useBalances'; import { TOKENS } from '../constants/tokens'; import { formatNumber } from '../utils/formatting'; const SwapCard: React.FC = () => { const { address } = useAccount(); const [tokenIn, setTokenIn] = useState(TOKENS.WETH.address); const [tokenOut, setTokenOut] = useState(TOKENS.USDC.address); const [amountIn, setAmountIn] = useState(''); const [amountOut, setAmountOut] = useState(''); const [slippageTolerance, setSlippageTolerance] = useState(0.5); const [quote, setQuote] = useState<number>(0); const { getQuote, loading: quoteLoading, error: quoteError } = useFxSwap(); const { executeCompleteSwap, loading: swapLoading, error: swapError } = useSwapExecution(); const { ethBalance, tokenBalances } = useTokenBalances([tokenIn, tokenOut]); // Get quote when amount changes const handleAmountChange = useCallback(async (value: string) => { setAmountIn(value); if (!value || parseFloat(value) <= 0) { setQuote(0); setAmountOut(''); return; } try { const quoteResult = await getQuote(tokenIn, tokenOut, parseFloat(value)); setQuote(quoteResult); setAmountOut(quoteResult.toString()); } catch (error) { console.error('Error getting quote:', error); setQuote(0); setAmountOut(''); } }, [getQuote, tokenIn, tokenOut]); // Execute swap const handleSwap = useCallback(async () => { if (!amountIn || !quote) return; try { const result = await executeCompleteSwap({ tokenIn, tokenOut, amountIn: parseFloat(amountIn), slippageTolerance }); console.log('Swap completed:', result); // Reset form setAmountIn(''); setAmountOut(''); setQuote(0); } catch (error) { console.error('Swap failed:', error); } }, [executeCompleteSwap, tokenIn, tokenOut, amountIn, quote, slippageTolerance]); // Swap tokens const handleSwapTokens = useCallback(() => { setTokenIn(tokenOut); setTokenOut(tokenIn); setAmountIn(''); setAmountOut(''); setQuote(0); }, [tokenIn, tokenOut]); const getTokenSymbol = (address: string) => { const token = Object.values(TOKENS).find(t => t.address === address); return token?.symbol || 'Unknown'; }; const getTokenBalance = (address: string) => { if (address === TOKENS.WETH.address) { return ethBalance.data?.formatted; } const tokenBalance = tokenBalances.find(b => b.data?.symbol === getTokenSymbol(address)); return tokenBalance?.data?.formatted; }; return ( {/* Swap Button */} <div className="flex justify-center"> <button onClick={handleSwapTokens} className="p-2 border rounded-full hover:bg-gray-50" disabled={swapLoading} > ↓↑ </button> ) } ``` ## Subgraph Integration ### 1. Subgraph Schema The subgraph should index key events and provide efficient querying: ```graphql type Pool @entity { id: ID! token0: Token! token1: Token! reserve0: BigInt! reserve1: BigInt! totalSupply: BigInt! virtualPrice: BigInt! dynamicFee: BigInt! volume24h: BigInt! createdAt: BigInt! updatedAt: BigInt! } type Token @entity { id: ID! symbol: String! name: String! decimals: Int! totalSupply: BigInt! } type Swap @entity { id: ID! pool: Pool! tokenIn: Token! tokenOut: Token! amountIn: BigInt! amountOut: BigInt! sender: Bytes! timestamp: BigInt! blockNumber: BigInt! } ``` ### 2. Subgraph Client ```javascript import { request, gql } from 'graphql-request'; class SubgraphClient { constructor(endpoint) { this.endpoint = endpoint; } async getPools() { const query = gql` query GetPools { pools(orderBy: volume24h, orderDirection: desc) { id token0 { id symbol decimals } token1 { id symbol decimals } reserve0 reserve1 virtualPrice dynamicFee volume24h } } `; return await request(this.endpoint, query); } async getPoolByTokens(token0, token1) { const query = gql` query GetPool($token0: String!, $token1: String!) { pools( where: { or: [ { token0: $token0, token1: $token1 }, { token0: $token1, token1: $token0 } ] } ) { id token0 { id symbol decimals } token1 { id symbol decimals } reserve0 reserve1 virtualPrice dynamicFee } } `; const result = await request(this.endpoint, query, { token0, token1 }); return result.pools[0]; } async getSwapHistory(poolId, limit = 50) { const query = gql` query GetSwapHistory($poolId: String!, $limit: Int!) { swaps( where: { pool: $poolId } orderBy: timestamp orderDirection: desc first: $limit ) { id tokenIn { symbol } tokenOut { symbol } amountIn amountOut sender timestamp } } `; return await request(this.endpoint, query, { poolId, limit }); } } ```