From 722f60286ada53dbf51f5f1de08fdef8bd54f30b Mon Sep 17 00:00:00 2001 From: Navin Developer Date: Mon, 25 May 2026 21:10:09 +0100 Subject: [PATCH 01/13] feat: add uniswap swap implementation --- apps/web/src/lib/swap/uniswap-swap.ts | 285 ++++++++++++++++++++++++++ 1 file changed, 285 insertions(+) create mode 100644 apps/web/src/lib/swap/uniswap-swap.ts diff --git a/apps/web/src/lib/swap/uniswap-swap.ts b/apps/web/src/lib/swap/uniswap-swap.ts new file mode 100644 index 0000000..e82b80a --- /dev/null +++ b/apps/web/src/lib/swap/uniswap-swap.ts @@ -0,0 +1,285 @@ +/** + * Uniswap V3 Swap engine for CELO ↔ USDC/USDT pairs on Celo mainnet. + * Uses SwapRouter02 (exactInputSingle) and QuoterV2 for on-chain quotes. + * + * This module is ONLY used for swaps involving CELO. + * Stablecoin-only swaps (USDC↔USDT) continue to use Mento via usdc-usdt-swap.ts. + */ + +import { + createPublicClient, + http, + encodeFunctionData, + formatUnits, + parseUnits, + type Address, +} from 'viem'; +import { celo } from 'viem/chains'; +import { + SWAP_TOKENS, + SUPPORTED_TOKENS, + PLATFORM_FEE_BPS, + UNISWAP_V3_CONTRACTS, + UNISWAP_POOL_FEE, + SWAP_CONFIG, +} from '../minipay/constants'; +import type { SwapQuote, SwapTransaction, SwapTokenSymbol } from './usdc-usdt-swap'; + +// ─── ABIs (minimal) ────────────────────────────────────────────────────────── + +const QUOTER_V2_ABI = [ + { + name: 'quoteExactInputSingle', + type: 'function', + stateMutability: 'nonpayable', + inputs: [ + { + components: [ + { name: 'tokenIn', type: 'address' }, + { name: 'tokenOut', type: 'address' }, + { name: 'amountIn', type: 'uint256' }, + { name: 'fee', type: 'uint24' }, + { name: 'sqrtPriceLimitX96', type: 'uint160' }, + ], + name: 'params', + type: 'tuple', + }, + ], + outputs: [ + { name: 'amountOut', type: 'uint256' }, + { name: 'sqrtPriceX96After', type: 'uint160' }, + { name: 'initializedTicksCrossed', type: 'uint32' }, + { name: 'gasEstimate', type: 'uint256' }, + ], + }, +] as const; + +const SWAP_ROUTER_ABI = [ + { + name: 'exactInputSingle', + type: 'function', + stateMutability: 'payable', + inputs: [ + { + components: [ + { name: 'tokenIn', type: 'address' }, + { name: 'tokenOut', type: 'address' }, + { name: 'fee', type: 'uint24' }, + { name: 'recipient', type: 'address' }, + { name: 'amountIn', type: 'uint256' }, + { name: 'amountOutMinimum', type: 'uint256' }, + { name: 'sqrtPriceLimitX96', type: 'uint160' }, + ], + name: 'params', + type: 'tuple', + }, + ], + outputs: [{ name: 'amountOut', type: 'uint256' }], + }, +] as const; + +const ERC20_ABI = [ + { + name: 'allowance', + type: 'function', + stateMutability: 'view', + inputs: [ + { name: 'owner', type: 'address' }, + { name: 'spender', type: 'address' }, + ], + outputs: [{ name: '', type: 'uint256' }], + }, + { + name: 'approve', + type: 'function', + stateMutability: 'nonpayable', + inputs: [ + { name: 'spender', type: 'address' }, + { name: 'amount', type: 'uint256' }, + ], + outputs: [{ name: '', type: 'bool' }], + }, +] as const; + +// ─── Helpers ────────────────────────────────────────────────────────────────── + +function getPublicClient() { + return createPublicClient({ + chain: celo, + transport: http(), + }); +} + +function getTokenAddress(symbol: string): Address { + const token = SUPPORTED_TOKENS.find((t) => t.symbol === symbol); + if (!token) { + const swapToken = SWAP_TOKENS.find((t) => t.symbol === symbol); + if (!swapToken) throw new Error(`Token ${symbol} not found`); + return swapToken.address as Address; + } + return token.address as Address; +} + +function getTokenDecimals(symbol: string): number { + const token = SUPPORTED_TOKENS.find((t) => t.symbol === symbol); + if (token) return token.decimals; + const swapToken = SWAP_TOKENS.find((t) => t.symbol === symbol); + if (swapToken) return swapToken.decimals; + throw new Error(`Token ${symbol} not found`); +} + +/** Apply platform fee and compute net amount */ +function applyFee( + grossAmountStr: string, + decimals: number, +): { gross: string; fee: string; net: string } { + const grossBig = BigInt( + Math.round(parseFloat(grossAmountStr) * 10 ** decimals), + ); + const feeBig = (grossBig * BigInt(PLATFORM_FEE_BPS)) / BigInt(10_000); + const netBig = grossBig - feeBig; + return { + gross: formatUnits(grossBig, decimals), + fee: formatUnits(feeBig, decimals), + net: formatUnits(netBig, decimals), + }; +} + +// ─── Get Uniswap V3 Quote ──────────────────────────────────────────────────── + +export async function getUniswapQuote( + fromToken: SwapTokenSymbol, + toToken: SwapTokenSymbol, + amountIn: string, + slippageBps: number = SWAP_CONFIG.DEFAULT_SLIPPAGE_BPS, +): Promise { + if (!amountIn || parseFloat(amountIn) <= 0) { + throw new Error('Amount must be greater than 0'); + } + + const client = getPublicClient(); + const fromAddr = getTokenAddress(fromToken); + const toAddr = getTokenAddress(toToken); + const fromDecimals = getTokenDecimals(fromToken); + const toDecimals = getTokenDecimals(toToken); + const amountInParsed = parseUnits(amountIn, fromDecimals); + + // Use QuoterV2 to get the expected output (read-only call via eth_call) + const result = await client.simulateContract({ + address: UNISWAP_V3_CONTRACTS.QUOTER_V2 as Address, + abi: QUOTER_V2_ABI, + functionName: 'quoteExactInputSingle', + args: [ + { + tokenIn: fromAddr, + tokenOut: toAddr, + amountIn: amountInParsed, + fee: UNISWAP_POOL_FEE, + sqrtPriceLimitX96: BigInt(0), + }, + ], + }); + + const amountOutRaw = result.result[0]; + const grossStr = formatUnits(amountOutRaw, toDecimals); + const { gross, fee, net } = applyFee(grossStr, toDecimals); + const rate = parseFloat(net) / parseFloat(amountIn); + + return { + fromToken, + toToken, + amountIn, + amountOutGross: gross, + amountOutNet: net, + platformFee: fee, + platformFeePercent: PLATFORM_FEE_BPS / 100, + rate, + route: 'uniswap-v3', + slippageBps, + isTradable: true, + timestamp: Date.now(), + }; +} + +// ─── Build Uniswap V3 Swap Transaction ─────────────────────────────────────── + +export async function buildUniswapSwapTransaction( + fromToken: SwapTokenSymbol, + toToken: SwapTokenSymbol, + amountIn: string, + userAddress: string, + slippageBps: number = SWAP_CONFIG.DEFAULT_SLIPPAGE_BPS, +): Promise { + const quote = await getUniswapQuote(fromToken, toToken, amountIn, slippageBps); + + const fromAddr = getTokenAddress(fromToken); + const toAddr = getTokenAddress(toToken); + const fromDecimals = getTokenDecimals(fromToken); + const amountInParsed = parseUnits(amountIn, fromDecimals); + + // Calculate minimum output with slippage tolerance + const toDecimals = getTokenDecimals(toToken); + const grossOut = parseUnits(quote.amountOutGross, toDecimals); + const amountOutMinimum = + grossOut - (grossOut * BigInt(slippageBps)) / BigInt(10_000); + + // Build exactInputSingle calldata + const swapData = encodeFunctionData({ + abi: SWAP_ROUTER_ABI, + functionName: 'exactInputSingle', + args: [ + { + tokenIn: fromAddr, + tokenOut: toAddr, + fee: UNISWAP_POOL_FEE, + recipient: userAddress as Address, + amountIn: amountInParsed, + amountOutMinimum, + sqrtPriceLimitX96: BigInt(0), + }, + ], + }); + + // Build approval tx if swapping FROM an ERC-20 (USDC/USDT → CELO) + let approval: { + to: Address; + data: `0x${string}`; + } | null = null; + + if (fromToken !== 'CELO') { + const client = getPublicClient(); + const currentAllowance = await client.readContract({ + address: fromAddr, + abi: ERC20_ABI, + functionName: 'allowance', + args: [userAddress as Address, UNISWAP_V3_CONTRACTS.SWAP_ROUTER as Address], + }); + + if (BigInt(currentAllowance.toString()) < amountInParsed) { + const approveData = encodeFunctionData({ + abi: ERC20_ABI, + functionName: 'approve', + args: [UNISWAP_V3_CONTRACTS.SWAP_ROUTER as Address, amountInParsed], + }); + approval = { + to: fromAddr, + data: approveData, + }; + } + } + + // For CELO → token swaps, we need to send native CELO value + const isFromCelo = fromToken === 'CELO'; + + return { + approval, + swap: { + params: { + to: UNISWAP_V3_CONTRACTS.SWAP_ROUTER as Address, + data: swapData, + ...(isFromCelo ? { value: amountInParsed } : {}), + }, + }, + quote, + }; +} From 8f8786efca8730740874bd50d5c0ce3b53b41bfc Mon Sep 17 00:00:00 2001 From: Navin Developer Date: Mon, 25 May 2026 21:10:14 +0100 Subject: [PATCH 02/13] refactor: update usdc-usdt swap implementation --- apps/web/src/lib/swap/usdc-usdt-swap.ts | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/apps/web/src/lib/swap/usdc-usdt-swap.ts b/apps/web/src/lib/swap/usdc-usdt-swap.ts index 3499cc5..dda14fa 100644 --- a/apps/web/src/lib/swap/usdc-usdt-swap.ts +++ b/apps/web/src/lib/swap/usdc-usdt-swap.ts @@ -6,6 +6,7 @@ import { Mento, ChainId, deadlineFromMinutes } from '@mento-protocol/mento-sdk'; import { parseUnits, formatUnits } from 'viem'; import { SWAP_TOKENS, SUPPORTED_TOKENS, PLATFORM_FEE_BPS, SWAP_CONFIG } from '../minipay/constants'; +import { getUniswapQuote, buildUniswapSwapTransaction } from './uniswap-swap'; // ─── Types ──────────────────────────────────────────────────────────────────── @@ -25,7 +26,7 @@ export interface SwapQuote { /** Exchange rate (net) */ rate: number; /** Is this a direct swap or routed via USDm? */ - route: 'direct' | 'via-usdm'; + route: 'direct' | 'via-usdm' | 'uniswap-v3'; /** Slippage tolerance in BPS used for this quote */ slippageBps: number; /** Is the pair currently tradable? */ @@ -39,6 +40,13 @@ export interface SwapTransaction { quote: SwapQuote; } +// ─── Routing ────────────────────────────────────────────────────────────────── + +/** Returns true when at least one side of the pair is CELO (requires Uniswap) */ +export function isCeloPair(from: SwapTokenSymbol, to: SwapTokenSymbol): boolean { + return from === 'CELO' || to === 'CELO'; +} + // ─── Helpers ────────────────────────────────────────────────────────────────── function getTokenAddress(symbol: string, chainId: number): string { @@ -105,6 +113,11 @@ export async function getSwapQuote( throw new Error('Amount must be greater than 0'); } + // Route CELO pairs through Uniswap V3 + if (isCeloPair(fromToken, toToken)) { + return getUniswapQuote(fromToken, toToken, amountIn, slippageBps); + } + const mento = await getMento(chainId); const fromAddr = getTokenAddress(fromToken, chainId); const toAddr = getTokenAddress(toToken, chainId); @@ -166,6 +179,11 @@ export async function buildSwapTransaction( slippageBps: number = SWAP_CONFIG.DEFAULT_SLIPPAGE_BPS, chainId = 42220 ): Promise { + // Route CELO pairs through Uniswap V3 + if (isCeloPair(fromToken, toToken)) { + return buildUniswapSwapTransaction(fromToken, toToken, amountIn, userAddress, slippageBps); + } + const quote = await getSwapQuote(fromToken, toToken, amountIn, slippageBps, chainId); if (!quote.isTradable) { From 10c6875d5a9732ca34790e01c29c6354b4689208 Mon Sep 17 00:00:00 2001 From: Navin Developer Date: Mon, 25 May 2026 21:10:20 +0100 Subject: [PATCH 03/13] refactor: update minipay constants --- apps/web/src/lib/minipay/constants.ts | 44 +++++++++++++++++++-------- 1 file changed, 31 insertions(+), 13 deletions(-) diff --git a/apps/web/src/lib/minipay/constants.ts b/apps/web/src/lib/minipay/constants.ts index dd5d5ad..c1ad514 100644 --- a/apps/web/src/lib/minipay/constants.ts +++ b/apps/web/src/lib/minipay/constants.ts @@ -37,8 +37,8 @@ export const SWAP_TOKENS = [ symbol: 'USDT', name: 'Tether USD', decimals: 6, - address: '0x48065fbbe25f71c9282ddf5e1cd6d6a887483d5e', // Celo Mainnet - addressSepolia: '0x617f3112bf5ad0E84e882D5142D04ae6C606cc89', // Celo Sepolia + address: '0x48065fbBE25f71C9282ddf5e1cD6D6A887483D5e', // Celo Mainnet + addressSepolia: '0x617f3112bF5ad0E84E882D5142D0aE6C606cc89', // Celo Sepolia color: '#26A17B', logo: '/tokens/usdt.png', issuer: 'Tether', @@ -47,8 +47,8 @@ export const SWAP_TOKENS = [ symbol: 'CELO', name: 'Celo', decimals: 18, - address: '0x471EcE3750Da237a93B122c29e4039db560e3F6f', // Celo Mainnet (wrapped CELO for Mento) - addressSepolia: '0xF194AfDf50Bae0a21eF85469D1521810657A1B53', // Celo Sepolia + address: '0x471EcE3750Da237f93B8E339c536989b8978a438', // Celo Mainnet + addressSepolia: '0xF194AFDF50bAE0a21EF85469d1521810657a1b53', // Celo Sepolia color: '#FCFF52', logo: '/tokens/celo.png', issuer: 'Celo', @@ -61,7 +61,7 @@ export const USDM_TOKEN = { name: 'Mento USD', decimals: 18, address: '0x765DE816845861e75A25fCA122bb6898B8B1282a', - addressSepolia: '0x10c892A6EC43a53E45D0B916B4b7D383B1b4f9f9', + addressSepolia: '0x10c892A6ec43a53E45d0B916b4b7D383B1b4F9f9', }; // Keep SUPPORTED_TOKENS for Mento SDK compatibility (includes USDm + CELO for rate API) @@ -71,12 +71,12 @@ export const SUPPORTED_TOKENS = [ symbol: 'CELO', name: 'Celo', decimals: 18, - address: '0x471EcE3750Da237a93B122c29e4039db560e3F6f', - addressSepolia: '0xF194AfDf50Bae0a21eF85469D1521810657A1B53', + address: '0x471EcE3750Da237f93B8E339c536989b8978a438', + addressSepolia: '0xF194AFDF50bAE0a21EF85469d1521810657a1b53', logo: '/tokens/celo.png', }, { symbol: 'USDC', name: 'USD Coin', decimals: 6, address: '0xcebA9300f2b948710d2653dD7B07f33A8B32118C', addressSepolia: '0x2A3684e9Dc20B857375EA04235F2F7edBe818FA7', logo: '/tokens/usdc.png' }, - { symbol: 'USDT', name: 'Tether USD', decimals: 6, address: '0x48065fbbe25f71c9282ddf5e1cd6d6a887483d5e', addressSepolia: '0x617f3112bf5ad0E84e882D5142D04ae6C606cc89', logo: '/tokens/usdt.png' }, + { symbol: 'USDT', name: 'Tether USD', decimals: 6, address: '0x48065fbBE25f71C9282ddf5e1cD6D6A887483D5e', addressSepolia: '0x617f3112bF5ad0E84E882D5142D0aE6C606cc89', logo: '/tokens/usdt.png' }, ]; // ─── Platform Fee ──────────────────────────────────────────────────────────── @@ -99,21 +99,27 @@ export const /** Set after first registration: NEXT_PUBLIC_AGENT_ID */ agentId: process.env.NEXT_PUBLIC_AGENT_ID || null, manifestUrl: '/api/agent/manifest', - chainId: 42220, + chainId: Number(process.env.NEXT_PUBLIC_CHAIN_ID || 42220), }; // ERC-8004 contract addresses on Celo Mainnet (from docs.celo.org) export const ERC8004_CONTRACTS = { - identityRegistry: '0x...', // fill after checking live chain deployments - reputationRegistry: '0x...', - validationRegistry: '0x...', + identityRegistry: + process.env.NEXT_PUBLIC_ERC8004_IDENTITY_REGISTRY || + '0x0000000000000000000000000000000000000000', + reputationRegistry: + process.env.NEXT_PUBLIC_ERC8004_REPUTATION_REGISTRY || + '0x0000000000000000000000000000000000000000', + validationRegistry: + process.env.NEXT_PUBLIC_ERC8004_VALIDATION_REGISTRY || + '0x0000000000000000000000000000000000000000', }; // ─── MiniPay / Fee Abstraction ──────────────────────────────────────────────── export const MINIPAY_CONFIG = { SUPPORTED_FEE_CURRENCY: '0x765DE816845861e75A25fCA122bb6898B8B1282a', // USDm mainnet - SUPPORTED_FEE_CURRENCY_SEPOLIA: '0x10c892A6EC43a53E45D0B916B4b7D383B1b4f9f9', + SUPPORTED_FEE_CURRENCY_SEPOLIA: '0x10c892A6ec43a53E45d0b916b4b7D383B1b4F9f9', USE_LEGACY_TRANSACTIONS: true, MOBILE_FIRST: true, }; @@ -129,6 +135,18 @@ export const SWAP_CONFIG = { QUOTE_DEBOUNCE_MS: 500, }; +// ─── Uniswap V3 (for CELO ↔ USDC/USDT swaps) ──────────────────────────────── + +export const UNISWAP_V3_CONTRACTS = { + /** SwapRouter02 on Celo Mainnet */ + SWAP_ROUTER: '0x5615CDAb10dc425a742d643d949a7F474C01abc4' as const, + /** Quoter V2 on Celo Mainnet */ + QUOTER_V2: '0x82825d0554fA07f7FC52Ab63c961F330fdEFa8E8' as const, +}; + +/** Standard Uniswap V3 pool fee tier for major pairs (0.3%) */ +export const UNISWAP_POOL_FEE = 3000; + export const TRANSACTION_STATUS = { PENDING: 'pending', PROCESSING: 'processing', From ed5ac914b43e1ff92f482a45e84944f3aab7d408 Mon Sep 17 00:00:00 2001 From: Navin Developer Date: Mon, 25 May 2026 21:10:26 +0100 Subject: [PATCH 04/13] refactor: update use-swap hook --- apps/web/src/lib/hooks/use-swap.ts | 24 +++++++++++++++++------- 1 file changed, 17 insertions(+), 7 deletions(-) diff --git a/apps/web/src/lib/hooks/use-swap.ts b/apps/web/src/lib/hooks/use-swap.ts index 1ee4ad6..88561fc 100644 --- a/apps/web/src/lib/hooks/use-swap.ts +++ b/apps/web/src/lib/hooks/use-swap.ts @@ -1,12 +1,12 @@ "use client"; import { useState, useCallback, useEffect, useRef } from "react"; -import { useAccount, useWalletClient, useChainId } from "wagmi"; +import { useAccount, useWalletClient, useChainId, usePublicClient } from "wagmi"; import { getSwapQuote, buildSwapTransaction, - getOppositeToken, formatTokenAmount, + isCeloPair, type SwapQuote, type SwapTokenSymbol, } from "@/lib/swap/usdc-usdt-swap"; @@ -25,6 +25,7 @@ export function useSwap(onRecommendation?: (rec: AgentRecommendation) => void) { const { address, isConnected } = useAccount(); const { data: walletClient } = useWalletClient(); const chainId = useChainId(); + const publicClient = usePublicClient({ chainId }); const { createTransaction } = useTransactions(); const [fromToken, setFromTokenState] = useState("USDC"); @@ -117,7 +118,14 @@ export function useSwap(onRecommendation?: (rec: AgentRecommendation) => void) { setQuote(q); setAiRec(rec); - setSlippageBps(rec.recommendedSlippageBps); + const shouldAutoApplySlippage = + !aiRec || slippageBps === aiRec.recommendedSlippageBps; + if ( + shouldAutoApplySlippage && + slippageBps !== rec.recommendedSlippageBps + ) { + setSlippageBps(rec.recommendedSlippageBps); + } onRecommendation?.(rec); } catch (err) { const categorized = categorizeError(err); @@ -132,7 +140,7 @@ export function useSwap(onRecommendation?: (rec: AgentRecommendation) => void) { return () => { if (debounceRef.current) clearTimeout(debounceRef.current); }; - }, [fromAmount, fromToken, toToken, slippageBps, chainId, onRecommendation, balance]); + }, [fromAmount, fromToken, toToken, slippageBps, chainId, onRecommendation, balance, aiRec]); const handleSwitch = useCallback(() => { const tempToken = fromToken; @@ -171,20 +179,22 @@ export function useSwap(onRecommendation?: (rec: AgentRecommendation) => void) { swap.params as Parameters[0], ); + const providerName = isCeloPair(fromToken, toToken) ? "Uniswap V3" : "Mento Protocol"; + createTransaction( TransactionType.SWAP, { type: TransactionType.SWAP, fromAmount, toAmount: quote.amountOutNet, - provider: "Mento Protocol", + provider: providerName, rate: quote.rate, fee: quote.platformFee, minAmount: "0", maxAmount: "0", estimatedTime: "Instant", metadata: { - providerName: "Mento Protocol", + providerName, fromAddress: fromToken, toAddress: toToken, feeCurrency: toToken, @@ -193,7 +203,7 @@ export function useSwap(onRecommendation?: (rec: AgentRecommendation) => void) { }, }, { - providerName: "Mento Protocol", + providerName, fromAddress: fromToken, toAddress: toToken, feeCurrency: toToken, From d495dd448d6b7ac248f9ec3d5228cfccebc2c780 Mon Sep 17 00:00:00 2001 From: Navin Developer Date: Mon, 25 May 2026 21:10:30 +0100 Subject: [PATCH 05/13] refactor: update agent intelligence --- apps/web/src/lib/agent/agent-intelligence.ts | 74 ++++++++++++++++++-- 1 file changed, 68 insertions(+), 6 deletions(-) diff --git a/apps/web/src/lib/agent/agent-intelligence.ts b/apps/web/src/lib/agent/agent-intelligence.ts index 4818436..7b55926 100644 --- a/apps/web/src/lib/agent/agent-intelligence.ts +++ b/apps/web/src/lib/agent/agent-intelligence.ts @@ -41,6 +41,8 @@ export interface AgentChatResponse { const SWAP_PAIRS: Array<[SwapTokenSymbol, SwapTokenSymbol]> = [ ['USDC', 'USDT'], ['USDT', 'USDC'], + ['CELO', 'USDC'], + ['CELO', 'USDT'], ]; export function detectIntent(message: string): ChatIntent { @@ -67,7 +69,28 @@ function parseTokens(message: string): { from: SwapTokenSymbol; to: SwapTokenSym const m = message.toUpperCase(); const hasUsdc = m.includes('USDC'); const hasUsdt = m.includes('USDT'); + const hasCelo = m.includes('CELO'); + + // CELO pairs + if (hasCelo && hasUsdc) { + const celoFirst = m.indexOf('CELO') < m.indexOf('USDC'); + return celoFirst + ? { from: 'CELO', to: 'USDC' } + : { from: 'USDC', to: 'CELO' }; + } + if (hasCelo && hasUsdt) { + const celoFirst = m.indexOf('CELO') < m.indexOf('USDT'); + return celoFirst + ? { from: 'CELO', to: 'USDT' } + : { from: 'USDT', to: 'CELO' }; + } + if (hasCelo) { + if (m.includes('TO USDC') || m.includes('FOR USDC')) return { from: 'CELO', to: 'USDC' }; + if (m.includes('TO USDT') || m.includes('FOR USDT')) return { from: 'CELO', to: 'USDT' }; + return { from: 'CELO', to: 'USDC' }; // default CELO target + } + // Stablecoin pairs (unchanged) if (hasUsdc && hasUsdt) { const usdcFirst = m.indexOf('USDC') < m.indexOf('USDT'); return usdcFirst @@ -85,6 +108,45 @@ export async function computeRecommendation( chainId = 42220, ): Promise { try { + // CELO swaps use Uniswap V3 — different volatility profile + if (_fromToken === 'CELO') { + // For CELO we can still probe a Uniswap quote via the routing layer + try { + const probeAmount = amount > 0 ? String(amount) : '1'; + const quote = await getSwapQuote('CELO', 'USDC', probeAmount, 50, chainId); + const rate = quote.rate; + + if (amount >= 500) { + return { + recommendedSlippageBps: 100, + marketCondition: 'volatile', + confidence: 75, + message: `Large CELO swap via Uniswap V3. Rate: 1 CELO \u2248 $${rate.toFixed(4)}. Higher slippage for price impact.`, + badge: 'AI: Safe Mode', + showBadge: true, + }; + } + return { + recommendedSlippageBps: 50, + marketCondition: 'normal', + confidence: 85, + message: `CELO swap via Uniswap V3. Rate: 1 CELO \u2248 $${rate.toFixed(4)}. Standard slippage recommended.`, + badge: 'AI Optimized', + showBadge: true, + }; + } catch { + return { + recommendedSlippageBps: 100, + marketCondition: 'volatile', + confidence: 65, + message: 'Could not probe CELO/USDC rate. Using higher slippage for safety.', + badge: 'AI: Caution', + showBadge: true, + }; + } + } + + // Stablecoin pairs — existing Mento logic (unchanged) const [usdcUsdt, usdtUsdc] = await Promise.all([ checkPairTradable('USDC', 'USDT', chainId), checkPairTradable('USDT', 'USDC', chainId), @@ -122,7 +184,7 @@ export async function computeRecommendation( recommendedSlippageBps: 100, marketCondition: 'normal', confidence: 82, - message: 'Large swap — live Mento liquidity supports 1% slippage for reliable fills.', + message: 'Large swap \u2014 live Mento liquidity supports 1% slippage for reliable fills.', badge: 'AI: Safe Mode', showBadge: true, }; @@ -133,7 +195,7 @@ export async function computeRecommendation( recommendedSlippageBps: 50, marketCondition: 'normal', confidence: 90, - message: 'Mid-size swap — Mento oracle rate is stable. Standard 0.5% slippage.', + message: 'Mid-size swap \u2014 Mento oracle rate is stable. Standard 0.5% slippage.', badge: 'AI: Standard', showBadge: true, }; @@ -143,7 +205,7 @@ export async function computeRecommendation( recommendedSlippageBps: 10, marketCondition: 'optimal', confidence: 96, - message: `Live rate: 1 USDC ≈ ${quote.rate.toFixed(6)} USDT. Minimal slippage is safe.`, + message: `Live rate: 1 USDC \u2248 ${quote.rate.toFixed(6)} USDT. Minimal slippage is safe.`, badge: 'AI Optimized', showBadge: true, }; @@ -189,10 +251,10 @@ export async function processAgentMessage( return { intent, message: - "I'm the Jahpay Swap Agent on Celo. I can fetch live Mento rates, quote swaps, recommend slippage, and guide you through USDC↔USDT swaps. Try: \"What's the rate for 500 USDC?\" or \"Swap 100 USDT to USDC\".", + "I'm the Jahpay Swap Agent on Celo. I can fetch live rates, quote swaps, recommend slippage, and guide you through USDC\u2194USDT and CELO\u2194USDC/USDT swaps. Try: \"What's the rate for 500 USDC?\" or \"Swap 10 CELO to USDC\".", suggestedActions: [ { label: 'Get rate', action: 'rate', payload: { amount: '100', fromToken: 'USDC', toToken: 'USDT' } }, - { label: 'Quote 1000 USDC', action: 'quote', payload: { amount: '1000', fromToken: 'USDC', toToken: 'USDT' } }, + { label: 'Quote 10 CELO', action: 'quote', payload: { amount: '10', fromToken: 'CELO', toToken: 'USDC' } }, ], }; @@ -207,7 +269,7 @@ export async function processAgentMessage( `**Live Mento quote** (${chainId === 11142220 ? 'Celo Sepolia' : 'Celo Mainnet'}):`, `• ${amount} ${tokens.from} → **${quote.amountOutNet}** ${tokens.to} (after ${PLATFORM_FEE_PERCENT}% platform fee)`, `• Rate: 1 ${tokens.from} = ${quote.rate.toFixed(6)} ${tokens.to}`, - `• Route: ${quote.route === 'direct' ? 'Direct' : 'USDC → USDm → USDT'}`, + `• Route: ${quote.route === 'uniswap-v3' ? 'Uniswap V3' : quote.route === 'direct' ? 'Direct (Mento)' : `${tokens.from} \u2192 USDm \u2192 ${tokens.to}`}`, `• Recommended slippage: ${rec.recommendedSlippageBps / 100}%`, quote.isTradable ? '• Pair is tradable ✓' From 4bae66fbcc1146a7087e99d3d8908a793ce2b5a4 Mon Sep 17 00:00:00 2001 From: Navin Developer Date: Mon, 25 May 2026 21:10:35 +0100 Subject: [PATCH 06/13] refactor: update swap UI components --- .../components/swap/swap-confirm-modal.tsx | 6 ++++- .../src/components/swap/swap-interface.tsx | 22 ++++++++++++++----- 2 files changed, 21 insertions(+), 7 deletions(-) diff --git a/apps/web/src/components/swap/swap-confirm-modal.tsx b/apps/web/src/components/swap/swap-confirm-modal.tsx index ad44adb..fd930b6 100644 --- a/apps/web/src/components/swap/swap-confirm-modal.tsx +++ b/apps/web/src/components/swap/swap-confirm-modal.tsx @@ -95,7 +95,11 @@ export function SwapConfirmModal({ { label: "Route", value: - quote.route === "direct" ? "Direct" : "USDC → USDm → USDT", + quote.route === "uniswap-v3" + ? "Uniswap V3" + : quote.route === "direct" + ? "Direct (Mento)" + : "via USDm (Mento)", }, { label: "Slippage", value: `${quote.slippageBps / 100}%` }, ].map(({ label, value }) => ( diff --git a/apps/web/src/components/swap/swap-interface.tsx b/apps/web/src/components/swap/swap-interface.tsx index e8aaa12..9edf6a4 100644 --- a/apps/web/src/components/swap/swap-interface.tsx +++ b/apps/web/src/components/swap/swap-interface.tsx @@ -20,6 +20,7 @@ import { } from "./transaction-history-sheet"; import type { AgentRecommendation } from "@/lib/agent/erc8004-agent"; import type { SwapTokenSymbol } from "@/lib/swap/usdc-usdt-swap"; +import { isCeloPair } from "@/lib/swap/usdc-usdt-swap"; type SwapStatus = "idle" | "loading" | "success" | "error"; @@ -48,6 +49,8 @@ export function SwapInterface() { toToken: SwapTokenSymbol; slippageBps?: number; } | null>(null); + const [currentFromToken, setCurrentFromToken] = useState('USDC'); + const [currentToToken, setCurrentToToken] = useState('USDT'); useEffect(() => { setMounted(true); @@ -80,6 +83,8 @@ export function SwapInterface() { slippageBps?: number; }) => { setChatSwapParams(payload); + setCurrentFromToken(payload.fromToken); + setCurrentToToken(payload.toToken); }, [], ); @@ -187,10 +192,15 @@ export function SwapInterface() { icon: , text: "Non-Custodial", }, - { - icon: , - text: "Mento Oracle", - }, + isCeloPair(currentFromToken, currentToToken) + ? { + icon: , + text: "Uniswap V3", + } + : { + icon: , + text: "Mento Oracle", + }, ].map(({ icon, text }) => (
From 403cc737e4310e1d82115ee0092d2ae8d9d7dd45 Mon Sep 17 00:00:00 2001 From: Navin Developer Date: Mon, 25 May 2026 21:10:41 +0100 Subject: [PATCH 07/13] refactor: update swap panel component --- .../src/components/main-app/panels/swap-panel.tsx | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/apps/web/src/components/main-app/panels/swap-panel.tsx b/apps/web/src/components/main-app/panels/swap-panel.tsx index bfbbf5b..3a99cea 100644 --- a/apps/web/src/components/main-app/panels/swap-panel.tsx +++ b/apps/web/src/components/main-app/panels/swap-panel.tsx @@ -14,6 +14,7 @@ import { getSwapTokenInfo, formatTokenAmount, isValidSwapPair, + isCeloPair, } from "@/lib/swap/usdc-usdt-swap"; import type { AgentRecommendation } from "@/lib/agent/erc8004-agent"; import { PLATFORM_FEE_PERCENT, SWAP_TOKENS } from "@/lib/minipay/constants"; @@ -266,7 +267,11 @@ function SwapPanelContent({ You Send - ≈ ${formatTokenAmount(swap.fromAmount || "0")} USD + {swap.fromToken === 'CELO' + ? (swap.quote + ? `≈ $${formatTokenAmount(String(parseFloat(swap.fromAmount || '0') * swap.quote.rate))} USD` + : '') + : `≈ $${formatTokenAmount(swap.fromAmount || '0')} USD`}
@@ -352,7 +357,12 @@ function SwapPanelContent({ {formatTokenAmount(swap.quote.platformFee)} {swap.toToken}
- {swap.quote.route === "via-usdm" && ( + {swap.quote.route === 'uniswap-v3' ? ( +
+ + Uniswap V3 on Celo +
+ ) : swap.quote.route === "via-usdm" && (
Routing via USDm for best price From 21e76b6c0a52869cf3f85db0bdbec95989d654f8 Mon Sep 17 00:00:00 2001 From: Navin Developer Date: Mon, 25 May 2026 21:10:47 +0100 Subject: [PATCH 08/13] refactor: update navbar component --- apps/web/src/components/layout/navbar.tsx | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/apps/web/src/components/layout/navbar.tsx b/apps/web/src/components/layout/navbar.tsx index 3d380a0..788ef28 100644 --- a/apps/web/src/components/layout/navbar.tsx +++ b/apps/web/src/components/layout/navbar.tsx @@ -3,7 +3,7 @@ import Link from "next/link"; import Image from "next/image"; import { usePathname } from "next/navigation"; -import { Menu, ExternalLink } from "lucide-react"; +import { Menu, ArrowUpRight } from "lucide-react"; import { useEffect, useState } from "react"; import { cn } from "@/lib/utils"; @@ -15,6 +15,7 @@ export function Navbar() { const pathname = usePathname(); const [prevScrollPos, setPrevScrollPos] = useState(0); const [visible, setVisible] = useState(true); + const isAppPage = pathname.startsWith("/app"); useEffect(() => { const handleScroll = () => { @@ -97,6 +98,17 @@ export function Navbar() { />