# 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 });
}
}
```