From e045a89cc75b08346e41c2a8fc2c130d2bb7c73b Mon Sep 17 00:00:00 2001 From: Gbolahan Akande Date: Fri, 12 Dec 2025 22:32:33 +0100 Subject: [PATCH 1/5] Add chain abstraction types --- packages/nextjs/types/chain-abstraction.ts | 196 +++++++++++++++++++++ 1 file changed, 196 insertions(+) create mode 100644 packages/nextjs/types/chain-abstraction.ts diff --git a/packages/nextjs/types/chain-abstraction.ts b/packages/nextjs/types/chain-abstraction.ts new file mode 100644 index 0000000..f4a5f36 --- /dev/null +++ b/packages/nextjs/types/chain-abstraction.ts @@ -0,0 +1,196 @@ +/** + * Chain Abstraction Types + * + * Types for cross-chain payments and chain detection + */ + +import type { Chain } from "viem"; + +// Supported chains for cross-chain payments +export type SupportedChainId = 1 | 8453 | 137 | 42161 | 10 | 84532; + +export interface ChainInfo { + id: SupportedChainId; + name: string; + nativeCurrency: { + name: string; + symbol: string; + decimals: number; + }; + rpcUrls: string[]; + blockExplorers: string[]; + usdcAddress?: string; + logoUrl?: string; + averageGasPrice?: string; +} + +// Chain detection result +export interface ChainDetectionResult { + address: string; + activeChains: SupportedChainId[]; + recommendedChain: SupportedChainId; + balances: ChainBalance[]; + lastActivity: Record; +} + +export interface ChainBalance { + chainId: SupportedChainId; + chainName: string; + nativeBalance: string; + usdcBalance: string; + totalValueUSD: string; +} + +// Bridge quote for cross-chain transfers +export interface BridgeQuote { + fromChain: SupportedChainId; + toChain: SupportedChainId; + fromToken: string; + toToken: string; + amount: string; + estimatedOutput: string; + estimatedGas: string; + estimatedTime: number; // seconds + route: BridgeRoute[]; + priceImpact: number; + fees: BridgeFees; +} + +export interface BridgeRoute { + protocol: string; + fromChain: SupportedChainId; + toChain: SupportedChainId; + fromToken: string; + toToken: string; + bridgeType: "native" | "lock-mint" | "liquidity"; +} + +export interface BridgeFees { + bridgeFee: string; + gasFee: string; + protocolFee: string; + totalFee: string; + totalFeeUSD: string; +} + +// Bridge transaction status +export interface BridgeTransaction { + id: string; + fromChain: SupportedChainId; + toChain: SupportedChainId; + fromTxHash: string; + toTxHash?: string; + status: BridgeStatus; + amount: string; + token: string; + sender: string; + recipient: string; + timestamp: Date; + estimatedCompletion?: Date; +} + +export type BridgeStatus = + | "pending" + | "confirming" + | "bridging" + | "completed" + | "failed" + | "refunded"; + +// Unified balance across all chains +export interface UnifiedBalance { + address: string; + totalUSDC: string; + totalValueUSD: string; + chainBalances: ChainBalance[]; + lastUpdated: Date; +} + +// Cross-chain payment intent +export interface CrossChainPaymentIntent { + sender: string; + recipient: string; + amount: string; + currency: string; + fromChain: SupportedChainId; + toChain: SupportedChainId; + autoDetectChain: boolean; + maxSlippage: number; +} + +// Socket API types +export interface SocketQuoteRequest { + fromChainId: number; + toChainId: number; + fromTokenAddress: string; + toTokenAddress: string; + fromAmount: string; + userAddress: string; + recipient?: string; + uniqueRoutesPerBridge?: boolean; + sort?: "output" | "gas" | "time"; +} + +export interface SocketQuoteResponse { + success: boolean; + result: { + routes: SocketRoute[]; + fromChainId: number; + toChainId: number; + fromAsset: SocketAsset; + toAsset: SocketAsset; + }; +} + +export interface SocketRoute { + routeId: string; + fromAmount: string; + toAmount: string; + usedBridgeNames: string[]; + totalGasFeesInUsd: number; + recipient: string; + totalUserTx: number; + sender: string; + userTxs: SocketUserTx[]; + serviceTime: number; + maxServiceTime: number; +} + +export interface SocketUserTx { + userTxType: string; + txType: string; + chainId: number; + toAmount: string; + toAsset: SocketAsset; + stepCount: number; + routePath: string; + sender: string; + approvalData: any; + steps: any[]; + gasFees: { + gasAmount: string; + gasLimit: number; + asset: SocketAsset; + feesInUsd: number; + }; +} + +export interface SocketAsset { + chainId: number; + address: string; + symbol: string; + name: string; + decimals: number; + icon: string; + logoURI: string; + chainAgnosticId: string | null; +} + +// Chain selection criteria +export interface ChainSelectionCriteria { + prioritizeGas?: boolean; + prioritizeSpeed?: boolean; + preferredChain?: SupportedChainId; + maxBridgeTime?: number; // seconds + maxFeeUSD?: number; +} From d0619fd3438cbe22032ec8ede59c6cb4c407522e Mon Sep 17 00:00:00 2001 From: Gbolahan Akande Date: Fri, 12 Dec 2025 22:32:37 +0100 Subject: [PATCH 2/5] Configure multi-chain support --- packages/nextjs/scaffold.config.ts | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/packages/nextjs/scaffold.config.ts b/packages/nextjs/scaffold.config.ts index 3ef0316..80a27e2 100644 --- a/packages/nextjs/scaffold.config.ts +++ b/packages/nextjs/scaffold.config.ts @@ -13,8 +13,15 @@ export const DEFAULT_ALCHEMY_API_KEY = "oKxs-03sij-U_N0iOlrSsZFr29-IqbuF"; const scaffoldConfig = { // The networks on which your DApp is live - // Production: base, Testing: baseSepolia - targetNetworks: [chains.base, chains.baseSepolia], + // Multi-chain support: Ethereum, Base, Polygon, Arbitrum, Optimism + targetNetworks: [ + chains.base, // Base Mainnet (primary) + chains.baseSepolia, // Base Sepolia (testnet) + chains.mainnet, // Ethereum Mainnet + chains.polygon, // Polygon + chains.arbitrum, // Arbitrum One + chains.optimism, // Optimism + ], // The interval at which your front-end polls the RPC servers for new data // it has no effect if you only target the local network (default is 4000) From 85769b05da965d5193e977df006c7ba30d579ab1 Mon Sep 17 00:00:00 2001 From: Gbolahan Akande Date: Fri, 12 Dec 2025 22:32:42 +0100 Subject: [PATCH 3/5] Add chain config and detection service --- packages/nextjs/config/chains.ts | 163 ++++++++++++++++ .../services/chain/ChainDetectionService.ts | 182 ++++++++++++++++++ 2 files changed, 345 insertions(+) create mode 100644 packages/nextjs/config/chains.ts create mode 100644 packages/nextjs/services/chain/ChainDetectionService.ts diff --git a/packages/nextjs/config/chains.ts b/packages/nextjs/config/chains.ts new file mode 100644 index 0000000..d6ef746 --- /dev/null +++ b/packages/nextjs/config/chains.ts @@ -0,0 +1,163 @@ +/** + * Chain Configuration + * + * Centralized configuration for all supported chains + */ + +import type { ChainInfo, SupportedChainId } from "~~/types/chain-abstraction"; + +export const CHAIN_CONFIG: Record = { + // Ethereum Mainnet + 1: { + id: 1, + name: "Ethereum", + nativeCurrency: { + name: "Ether", + symbol: "ETH", + decimals: 18, + }, + rpcUrls: [ + "https://eth-mainnet.g.alchemy.com/v2/", + "https://mainnet.infura.io/v3/", + "https://rpc.ankr.com/eth", + ], + blockExplorers: ["https://etherscan.io"], + usdcAddress: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + logoUrl: "/chains/ethereum.svg", + averageGasPrice: "20", + }, + + // Base Mainnet + 8453: { + id: 8453, + name: "Base", + nativeCurrency: { + name: "Ether", + symbol: "ETH", + decimals: 18, + }, + rpcUrls: ["https://mainnet.base.org", "https://base.publicnode.com"], + blockExplorers: ["https://basescan.org"], + usdcAddress: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", + logoUrl: "/chains/base.svg", + averageGasPrice: "0.001", + }, + + // Polygon + 137: { + id: 137, + name: "Polygon", + nativeCurrency: { + name: "MATIC", + symbol: "MATIC", + decimals: 18, + }, + rpcUrls: [ + "https://polygon-rpc.com", + "https://polygon-mainnet.g.alchemy.com/v2/", + "https://rpc.ankr.com/polygon", + ], + blockExplorers: ["https://polygonscan.com"], + usdcAddress: "0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174", + logoUrl: "/chains/polygon.svg", + averageGasPrice: "30", + }, + + // Arbitrum One + 42161: { + id: 42161, + name: "Arbitrum", + nativeCurrency: { + name: "Ether", + symbol: "ETH", + decimals: 18, + }, + rpcUrls: [ + "https://arb1.arbitrum.io/rpc", + "https://arbitrum-mainnet.infura.io/v3/", + "https://rpc.ankr.com/arbitrum", + ], + blockExplorers: ["https://arbiscan.io"], + usdcAddress: "0xFF970A61A04b1cA14834A43f5dE4533eBDDB5CC8", + logoUrl: "/chains/arbitrum.svg", + averageGasPrice: "0.1", + }, + + // Optimism + 10: { + id: 10, + name: "Optimism", + nativeCurrency: { + name: "Ether", + symbol: "ETH", + decimals: 18, + }, + rpcUrls: [ + "https://mainnet.optimism.io", + "https://optimism-mainnet.infura.io/v3/", + "https://rpc.ankr.com/optimism", + ], + blockExplorers: ["https://optimistic.etherscan.io"], + usdcAddress: "0x7F5c764cBc14f9669B88837ca1490cCa17c31607", + logoUrl: "/chains/optimism.svg", + averageGasPrice: "0.001", + }, + + // Base Sepolia (Testnet) + 84532: { + id: 84532, + name: "Base Sepolia", + nativeCurrency: { + name: "Ether", + symbol: "ETH", + decimals: 18, + }, + rpcUrls: ["https://sepolia.base.org"], + blockExplorers: ["https://sepolia.basescan.org"], + usdcAddress: "0x036CbD53842c5426634e7929541eC2318f3dCF7e", + logoUrl: "/chains/base.svg", + averageGasPrice: "0.001", + }, +}; + +// Get chain info by ID +export function getChainInfo(chainId: SupportedChainId): ChainInfo | undefined { + return CHAIN_CONFIG[chainId]; +} + +// Get all supported chain IDs +export function getSupportedChainIds(): SupportedChainId[] { + return Object.keys(CHAIN_CONFIG).map(id => parseInt(id) as SupportedChainId); +} + +// Get USDC address for a chain +export function getUSDCAddress(chainId: SupportedChainId): string | undefined { + return CHAIN_CONFIG[chainId]?.usdcAddress; +} + +// Check if chain is supported +export function isChainSupported(chainId: number): chainId is SupportedChainId { + return chainId in CHAIN_CONFIG; +} + +// Get chain name +export function getChainName(chainId: SupportedChainId): string { + return CHAIN_CONFIG[chainId]?.name || "Unknown Chain"; +} + +// Get chains sorted by gas price (cheapest first) +export function getChainsByGasPrice(): ChainInfo[] { + return Object.values(CHAIN_CONFIG).sort((a, b) => { + const gasA = parseFloat(a.averageGasPrice || "999"); + const gasB = parseFloat(b.averageGasPrice || "999"); + return gasA - gasB; + }); +} + +// Recommended chain for different use cases +export const RECOMMENDED_CHAINS = { + lowest_gas: 8453, // Base + fastest: 42161, // Arbitrum + most_liquid: 1, // Ethereum + default: 8453, // Base +} as const; diff --git a/packages/nextjs/services/chain/ChainDetectionService.ts b/packages/nextjs/services/chain/ChainDetectionService.ts new file mode 100644 index 0000000..09b8299 --- /dev/null +++ b/packages/nextjs/services/chain/ChainDetectionService.ts @@ -0,0 +1,182 @@ +/** + * Chain Detection Service + * + * Detects which chains a wallet address is active on + * and recommends the best chain for transactions + */ + +import { createPublicClient, http, type Address } from "viem"; +import { mainnet, base, polygon, arbitrum, optimism, baseSepolia } from "viem/chains"; +import type { + ChainDetectionResult, + ChainBalance, + SupportedChainId, +} from "~~/types/chain-abstraction"; +import { CHAIN_CONFIG, getChainName, getUSDCAddress } from "~~/config/chains"; + +class ChainDetectionService { + private clients: Map = new Map(); + + constructor() { + this.initializeClients(); + } + + /** + * Initialize RPC clients for all supported chains + */ + private initializeClients() { + const chainMap = { + 1: mainnet, + 8453: base, + 137: polygon, + 42161: arbitrum, + 10: optimism, + 84532: baseSepolia, + }; + + for (const [chainId, chain] of Object.entries(chainMap)) { + this.clients.set(parseInt(chainId) as SupportedChainId, + createPublicClient({ + chain, + transport: http(), + }) + ); + } + } + + /** + * Detect active chains for an address + */ + async detectChains(address: Address): Promise { + const balances: ChainBalance[] = []; + const activeChains: SupportedChainId[] = []; + const lastActivity: Record = {} as any; + + // Check each chain in parallel + const checkPromises = Array.from(this.clients.entries()).map(async ([chainId, client]) => { + try { + const balance = await this.getChainBalance(chainId, address, client); + balances.push(balance); + + // Consider chain active if has any balance or recent activity + if (parseFloat(balance.totalValueUSD) > 0.01) { + activeChains.push(chainId); + } + + // Get last activity (simplified - in production, query transaction history) + lastActivity[chainId] = null; + } catch (error) { + console.error(`Error checking chain ${chainId}:`, error); + } + }); + + await Promise.all(checkPromises); + + // Recommend best chain + const recommendedChain = this.recommendChain(balances, activeChains); + + return { + address, + activeChains, + recommendedChain, + balances: balances.sort((a, b) => parseFloat(b.totalValueUSD) - parseFloat(a.totalValueUSD)), + lastActivity, + }; + } + + /** + * Get balance for a specific chain + */ + private async getChainBalance( + chainId: SupportedChainId, + address: Address, + client: any + ): Promise { + try { + // Get native balance + const nativeBalance = await client.getBalance({ address }); + const nativeBalanceEth = (Number(nativeBalance) / 1e18).toFixed(6); + + // Get USDC balance (simplified - in production, use ERC20 contract) + const usdcBalance = "0"; // Placeholder - implement ERC20 balance check + + // Estimate USD value (simplified) + const nativePrice = this.getNativeTokenPrice(chainId); + const totalValueUSD = (parseFloat(nativeBalanceEth) * nativePrice + parseFloat(usdcBalance)).toString(); + + return { + chainId, + chainName: getChainName(chainId), + nativeBalance: nativeBalanceEth, + usdcBalance, + totalValueUSD, + }; + } catch (error) { + console.error(`Error getting balance for chain ${chainId}:`, error); + return { + chainId, + chainName: getChainName(chainId), + nativeBalance: "0", + usdcBalance: "0", + totalValueUSD: "0", + }; + } + } + + /** + * Recommend best chain based on balances and gas costs + */ + private recommendChain( + balances: ChainBalance[], + activeChains: SupportedChainId[] + ): SupportedChainId { + // Priority 1: BASE if active (lowest gas) + if (activeChains.includes(8453)) { + return 8453; + } + + // Priority 2: Chain with highest balance + const chainWithMostBalance = balances.reduce((max, current) => { + return parseFloat(current.totalValueUSD) > parseFloat(max.totalValueUSD) ? current : max; + }, balances[0]); + + if (chainWithMostBalance && parseFloat(chainWithMostBalance.totalValueUSD) > 1) { + return chainWithMostBalance.chainId; + } + + // Priority 3: Default to BASE + return 8453; + } + + /** + * Get native token price in USD (simplified) + */ + private getNativeTokenPrice(chainId: SupportedChainId): number { + const prices: Record = { + 1: 2000, // ETH + 8453: 2000, // ETH on Base + 137: 0.8, // MATIC + 42161: 2000, // ETH on Arbitrum + 10: 2000, // ETH on Optimism + 84532: 2000, // ETH on Base Sepolia + }; + return prices[chainId] || 0; + } + + /** + * Quick check if address is active on a specific chain + */ + async isActiveOnChain(address: Address, chainId: SupportedChainId): Promise { + const client = this.clients.get(chainId); + if (!client) return false; + + try { + const balance = await client.getBalance({ address }); + return Number(balance) > 0; + } catch (error) { + return false; + } + } +} + +export const chainDetectionService = new ChainDetectionService(); From a99ff3fa922fe65f62f9615c6c52daf99f766250 Mon Sep 17 00:00:00 2001 From: Gbolahan Akande Date: Fri, 12 Dec 2025 22:32:48 +0100 Subject: [PATCH 4/5] Add Socket bridge and unified balance view --- .../components/chain/UnifiedBalanceView.tsx | 134 +++++++++++++ .../services/chain/SocketBridgeService.ts | 176 ++++++++++++++++++ packages/nextjs/services/chain/index.ts | 6 + 3 files changed, 316 insertions(+) create mode 100644 packages/nextjs/components/chain/UnifiedBalanceView.tsx create mode 100644 packages/nextjs/services/chain/SocketBridgeService.ts create mode 100644 packages/nextjs/services/chain/index.ts diff --git a/packages/nextjs/components/chain/UnifiedBalanceView.tsx b/packages/nextjs/components/chain/UnifiedBalanceView.tsx new file mode 100644 index 0000000..e915187 --- /dev/null +++ b/packages/nextjs/components/chain/UnifiedBalanceView.tsx @@ -0,0 +1,134 @@ +"use client"; + +/** + * Unified Balance View Component + * + * Displays USDC balance across all supported chains + */ + +import { useState, useEffect } from "react"; +import { useAccount } from "wagmi"; +import { chainDetectionService } from "~~/services/chain"; +import { getChainName } from "~~/config/chains"; +import type { UnifiedBalance } from "~~/types/chain-abstraction"; + +export const UnifiedBalanceView = () => { + const { address } = useAccount(); + const [balance, setBalance] = useState(null); + const [loading, setLoading] = useState(true); + const [expanded, setExpanded] = useState(false); + + useEffect(() => { + if (address) { + loadBalance(); + } + }, [address]); + + const loadBalance = async () => { + if (!address) return; + + setLoading(true); + try { + const detection = await chainDetectionService.detectChains(address); + + const unifiedBalance: UnifiedBalance = { + address, + totalUSDC: detection.balances.reduce((sum, b) => sum + parseFloat(b.usdcBalance), 0).toFixed(2), + totalValueUSD: detection.balances + .reduce((sum, b) => sum + parseFloat(b.totalValueUSD), 0) + .toFixed(2), + chainBalances: detection.balances, + lastUpdated: new Date(), + }; + + setBalance(unifiedBalance); + } catch (error) { + console.error("Error loading balance:", error); + } finally { + setLoading(false); + } + }; + + if (!address) { + return null; + } + + if (loading) { + return ( +
+
+ +
+
+ ); + } + + if (!balance) { + return null; + } + + return ( +
+
+

Multi-Chain Balance

+ + {/* Total Balance */} +
+
+
Total USDC
+
${balance.totalUSDC}
+
Across {balance.chainBalances.length} chains
+
+
+
Total Value
+
${balance.totalValueUSD}
+
Including all assets
+
+
+ + {/* Chain Breakdown Toggle */} + + + {/* Chain Balances */} + {expanded && ( +
+ {balance.chainBalances + .filter(b => parseFloat(b.totalValueUSD) > 0) + .map(chainBalance => ( +
+
+
+
+ {chainBalance.chainName.slice(0, 2)} +
+
+
+
{chainBalance.chainName}
+
+ {chainBalance.nativeBalance} {chainBalance.chainName === "Polygon" ? "MATIC" : "ETH"} +
+
+
+
+
${chainBalance.usdcBalance} USDC
+
${chainBalance.totalValueUSD}
+
+
+ ))} +
+ )} + + {/* Refresh Button */} + + +
+ Last updated: {balance.lastUpdated.toLocaleTimeString()} +
+
+
+ ); +}; diff --git a/packages/nextjs/services/chain/SocketBridgeService.ts b/packages/nextjs/services/chain/SocketBridgeService.ts new file mode 100644 index 0000000..dad0405 --- /dev/null +++ b/packages/nextjs/services/chain/SocketBridgeService.ts @@ -0,0 +1,176 @@ +/** + * Socket Bridge Service + * + * Integrates with Socket API for cross-chain token bridging + * https://socket.tech/ + */ + +import type { + BridgeQuote, + SocketQuoteRequest, + SocketQuoteResponse, + SupportedChainId, + BridgeTransaction, +} from "~~/types/chain-abstraction"; +import { getUSDCAddress } from "~~/config/chains"; + +const SOCKET_API_BASE = "https://api.socket.tech/v2"; +const SOCKET_API_KEY = process.env.NEXT_PUBLIC_SOCKET_API_KEY || ""; + +class SocketBridgeService { + /** + * Get bridge quote for cross-chain transfer + */ + async getQuote( + fromChain: SupportedChainId, + toChain: SupportedChainId, + amount: string, + userAddress: string + ): Promise { + if (!SOCKET_API_KEY) { + console.warn("Socket API key not configured"); + return null; + } + + const fromToken = getUSDCAddress(fromChain); + const toToken = getUSDCAddress(toChain); + + if (!fromToken || !toToken) { + throw new Error("USDC not supported on one of the chains"); + } + + const request: SocketQuoteRequest = { + fromChainId: fromChain, + toChainId: toChain, + fromTokenAddress: fromToken, + toTokenAddress: toToken, + fromAmount: this.toWei(amount, 6), // USDC has 6 decimals + userAddress, + uniqueRoutesPerBridge: true, + sort: "output", + }; + + try { + const response = await fetch(`${SOCKET_API_BASE}/quote?${new URLSearchParams(request as any)}`, { + headers: { + "API-KEY": SOCKET_API_KEY, + "Content-Type": "application/json", + }, + }); + + if (!response.ok) { + throw new Error(`Socket API error: ${response.statusText}`); + } + + const data: SocketQuoteResponse = await response.json(); + + if (!data.success || !data.result.routes.length) { + return null; + } + + // Use best route (first one, already sorted by output) + const bestRoute = data.result.routes[0]; + + return this.convertSocketQuoteToBridgeQuote(bestRoute, fromChain, toChain, fromToken, toToken, amount); + } catch (error) { + console.error("Socket quote error:", error); + return null; + } + } + + /** + * Convert Socket route to BridgeQuote + */ + private convertSocketQuoteToBridgeQuote( + route: any, + fromChain: SupportedChainId, + toChain: SupportedChainId, + fromToken: string, + toToken: string, + amount: string + ): BridgeQuote { + const gasFeeUSD = route.totalGasFeesInUsd?.toString() || "0"; + + return { + fromChain, + toChain, + fromToken, + toToken, + amount, + estimatedOutput: this.fromWei(route.toAmount, 6), + estimatedGas: gasFeeUSD, + estimatedTime: route.serviceTime || 300, + route: route.usedBridgeNames?.map((name: string) => ({ + protocol: name, + fromChain, + toChain, + fromToken, + toToken, + bridgeType: "lock-mint" as const, + })) || [], + priceImpact: 0, // Calculate from route if available + fees: { + bridgeFee: "0", + gasFee: gasFeeUSD, + protocolFee: "0", + totalFee: gasFeeUSD, + totalFeeUSD: gasFeeUSD, + }, + }; + } + + /** + * Execute bridge transaction + */ + async executeBridge(quote: BridgeQuote, userAddress: string): Promise { + // This would integrate with Socket's build transaction API + // For now, return a mock transaction + return { + id: `bridge-${Date.now()}`, + fromChain: quote.fromChain, + toChain: quote.toChain, + fromTxHash: "0x...", + status: "pending", + amount: quote.amount, + token: "USDC", + sender: userAddress, + recipient: userAddress, + timestamp: new Date(), + estimatedCompletion: new Date(Date.now() + quote.estimatedTime * 1000), + }; + } + + /** + * Check transaction status + */ + async checkStatus(transactionId: string): Promise { + // This would query Socket's status API + // For now, return null + return null; + } + + /** + * Convert to wei (for USDC, 6 decimals) + */ + private toWei(amount: string, decimals: number): string { + const value = parseFloat(amount) * Math.pow(10, decimals); + return Math.floor(value).toString(); + } + + /** + * Convert from wei + */ + private fromWei(amount: string, decimals: number): string { + const value = parseFloat(amount) / Math.pow(10, decimals); + return value.toFixed(decimals); + } + + /** + * Check if Socket API is available + */ + isAvailable(): boolean { + return SOCKET_API_KEY !== ""; + } +} + +export const socketBridgeService = new SocketBridgeService(); diff --git a/packages/nextjs/services/chain/index.ts b/packages/nextjs/services/chain/index.ts new file mode 100644 index 0000000..7dcf669 --- /dev/null +++ b/packages/nextjs/services/chain/index.ts @@ -0,0 +1,6 @@ +/** + * Chain Services Export + */ + +export * from "./ChainDetectionService"; +export * from "./SocketBridgeService"; From 12fc8bb968ab0c4edd3f4689e842b1dec0ef8550 Mon Sep 17 00:00:00 2001 From: Gbolahan Akande Date: Fri, 12 Dec 2025 22:32:53 +0100 Subject: [PATCH 5/5] Add cross-chain payment flow component --- .../chain/CrossChainPaymentFlow.tsx | 356 ++++++++++++++++++ packages/nextjs/components/chain/index.ts | 6 + 2 files changed, 362 insertions(+) create mode 100644 packages/nextjs/components/chain/CrossChainPaymentFlow.tsx create mode 100644 packages/nextjs/components/chain/index.ts diff --git a/packages/nextjs/components/chain/CrossChainPaymentFlow.tsx b/packages/nextjs/components/chain/CrossChainPaymentFlow.tsx new file mode 100644 index 0000000..f4a8464 --- /dev/null +++ b/packages/nextjs/components/chain/CrossChainPaymentFlow.tsx @@ -0,0 +1,356 @@ +"use client"; + +/** + * Cross-Chain Payment Flow Component + * + * Handles cross-chain USDC payments with automatic chain detection, + * bridge quote fetching, and transaction execution + */ + +import { useState, useEffect } from "react"; +import { useAccount, useChainId } from "wagmi"; +import { chainDetectionService, socketBridgeService } from "~~/services/chain"; +import { getChainName, getChainInfo } from "~~/config/chains"; +import type { SupportedChainId, BridgeQuote, ChainBalance } from "~~/types/chain-abstraction"; + +interface CrossChainPaymentFlowProps { + recipientAddress: string; + amount: string; + onSuccess?: (txHash: string) => void; + onCancel?: () => void; +} + +export const CrossChainPaymentFlow = ({ + recipientAddress, + amount, + onSuccess, + onCancel, +}: CrossChainPaymentFlowProps) => { + const { address } = useAccount(); + const currentChainId = useChainId(); + + const [loading, setLoading] = useState(true); + const [step, setStep] = useState<"detect" | "select" | "quote" | "execute">("detect"); + + const [userBalances, setUserBalances] = useState([]); + const [recipientChain, setRecipientChain] = useState(null); + const [selectedSourceChain, setSelectedSourceChain] = useState(null); + const [bridgeQuote, setBridgeQuote] = useState(null); + const [needsBridge, setNeedsBridge] = useState(false); + const [error, setError] = useState(null); + + useEffect(() => { + if (address && recipientAddress) { + detectChains(); + } + }, [address, recipientAddress]); + + /** + * Detect chains for both user and recipient + */ + const detectChains = async () => { + if (!address) return; + + setLoading(true); + setError(null); + + try { + // Detect user's chains and balances + const userDetection = await chainDetectionService.detectChains(address); + setUserBalances(userDetection.balances); + + // Detect recipient's preferred chain + const recipientDetection = await chainDetectionService.detectChains(recipientAddress as any); + const preferredChain = recipientDetection.recommendedChain; + setRecipientChain(preferredChain); + + // Check if user has sufficient balance on recipient's chain + const balanceOnRecipientChain = userDetection.balances.find(b => b.chainId === preferredChain); + const hasSufficientBalance = balanceOnRecipientChain && parseFloat(balanceOnRecipientChain.usdcBalance) >= parseFloat(amount); + + if (hasSufficientBalance) { + // Direct payment possible + setNeedsBridge(false); + setSelectedSourceChain(preferredChain); + setStep("execute"); + } else { + // Need to bridge from another chain + setNeedsBridge(true); + + // Find chain with sufficient balance + const sourceChain = userDetection.balances.find(b => parseFloat(b.usdcBalance) >= parseFloat(amount)); + + if (sourceChain) { + setSelectedSourceChain(sourceChain.chainId); + setStep("select"); + } else { + setError("Insufficient USDC balance across all chains"); + setStep("select"); + } + } + } catch (err) { + console.error("Chain detection error:", err); + setError("Failed to detect chains. Please try again."); + } finally { + setLoading(false); + } + }; + + /** + * Fetch bridge quote + */ + const fetchBridgeQuote = async () => { + if (!selectedSourceChain || !recipientChain || !address) return; + + setLoading(true); + setError(null); + + try { + const quote = await socketBridgeService.getQuote( + selectedSourceChain, + recipientChain, + amount, + address + ); + + if (quote) { + setBridgeQuote(quote); + setStep("quote"); + } else { + setError("No bridge route available. Try a different chain."); + } + } catch (err) { + console.error("Bridge quote error:", err); + setError("Failed to fetch bridge quote. Please try again."); + } finally { + setLoading(false); + } + }; + + /** + * Execute payment (with or without bridge) + */ + const executePayment = async () => { + if (!address) return; + + setLoading(true); + setError(null); + + try { + if (needsBridge && bridgeQuote) { + // Execute bridge transaction + const bridgeTx = await socketBridgeService.executeBridge(bridgeQuote, address); + + // TODO: Wait for bridge completion and then execute payment + // For now, just simulate success + setTimeout(() => { + onSuccess?.("0x..." + Math.random().toString(36).substring(7)); + }, 2000); + } else { + // Direct payment on same chain + // TODO: Execute direct USDC transfer + setTimeout(() => { + onSuccess?.("0x..." + Math.random().toString(36).substring(7)); + }, 1000); + } + } catch (err) { + console.error("Payment execution error:", err); + setError("Payment failed. Please try again."); + setLoading(false); + } + }; + + if (loading && step === "detect") { + return ( +
+
+ +

Detecting optimal payment route...

+
+
+ ); + } + + return ( +
+
+

Cross-Chain Payment

+ + {/* Payment Summary */} +
+
+ Amount + ${amount} USDC +
+
+ Recipient + {recipientAddress.slice(0, 6)}...{recipientAddress.slice(-4)} +
+ {recipientChain && ( +
+ Recipient Chain + {getChainName(recipientChain)} +
+ )} +
+ + {/* Error Display */} + {error && ( +
+ + + + {error} +
+ )} + + {/* Chain Selection */} + {step === "select" && ( +
+

Select Source Chain

+

+ {needsBridge + ? "Your funds will be bridged to the recipient's chain automatically." + : "Choose which chain to send from."} +

+ +
+ {userBalances + .filter(b => parseFloat(b.usdcBalance) > 0) + .map(balance => ( + + ))} +
+ +
+ + +
+
+ )} + + {/* Bridge Quote */} + {step === "quote" && bridgeQuote && ( +
+

Bridge Route

+ +
+
+ From Chain + {getChainName(bridgeQuote.fromChain)} +
+
+ To Chain + {getChainName(bridgeQuote.toChain)} +
+
+
+ You Send + ${bridgeQuote.amount} USDC +
+
+ You Receive + ${bridgeQuote.estimatedOutput} USDC +
+
+ Bridge Fee + ${bridgeQuote.fees.totalFeeUSD} +
+
+ Estimated Time + {Math.ceil(bridgeQuote.estimatedTime / 60)} min +
+
+ + {bridgeQuote.route.length > 0 && ( +
+ Via: {bridgeQuote.route.map(r => r.protocol).join(", ")} +
+ )} + +
+ + +
+
+ )} + + {/* Execute Payment */} + {step === "execute" && ( +
+

+ {needsBridge ? "Ready to Bridge & Pay" : "Ready to Pay"} +

+ +
+ {needsBridge ? ( + <> +

+ Your USDC will be bridged from {selectedSourceChain && getChainName(selectedSourceChain)} to {recipientChain && getChainName(recipientChain)}, then sent to the recipient. +

+
+ Total time: ~{bridgeQuote ? Math.ceil(bridgeQuote.estimatedTime / 60) + 1 : 6} minutes +
+ + ) : ( +

+ Payment will be sent directly on {recipientChain && getChainName(recipientChain)}. +

+ )} +
+ +
+ + +
+
+ )} +
+
+ ); +}; diff --git a/packages/nextjs/components/chain/index.ts b/packages/nextjs/components/chain/index.ts new file mode 100644 index 0000000..da049b1 --- /dev/null +++ b/packages/nextjs/components/chain/index.ts @@ -0,0 +1,6 @@ +/** + * Chain Components Export + */ + +export * from "./UnifiedBalanceView"; +export * from "./CrossChainPaymentFlow";