diff --git a/next.config.ts b/next.config.ts index 4777bb4..74a9518 100644 --- a/next.config.ts +++ b/next.config.ts @@ -1,5 +1,14 @@ import type { NextConfig } from "next"; import { withSerwist } from "@serwist/turbopack"; +import { execSync } from "child_process"; + +const commitHash = (() => { + try { + return execSync("git rev-parse --short HEAD").toString().trim(); + } catch { + return "unknown"; + } +})(); const nextConfig: NextConfig = { // Keep packages that need native Node.js modules as server externals @@ -9,6 +18,10 @@ const nextConfig: NextConfig = { '@cardano-sdk/crypto', 'libsodium-wrappers-sumo', ], + env: { + NEXT_PUBLIC_APP_VERSION: process.env.npm_package_version || "0.0.0", + NEXT_PUBLIC_COMMIT_HASH: commitHash, + }, }; export default withSerwist(nextConfig); diff --git a/src/app/(main)/layout.tsx b/src/app/(main)/layout.tsx index b0479b3..7e61580 100644 --- a/src/app/(main)/layout.tsx +++ b/src/app/(main)/layout.tsx @@ -1,5 +1,6 @@ import ResponsiveNav from "@/components/ui/ResponsiveNav"; import EarlyAccessModal from "@/components/ui/EarlyAccessModal"; +import AppVersion from "@/components/common/AppVersion"; import React from "react"; const MainLayout = ({ children }: { children: React.ReactNode }) => { @@ -7,6 +8,7 @@ const MainLayout = ({ children }: { children: React.ReactNode }) => {
{children} +
); diff --git a/src/app/(main)/wallet/cash-in/page.tsx b/src/app/(main)/wallet/cash-in/page.tsx index d7acb3a..a28b29f 100644 --- a/src/app/(main)/wallet/cash-in/page.tsx +++ b/src/app/(main)/wallet/cash-in/page.tsx @@ -6,15 +6,17 @@ import Card from "@/components/ui/card"; import { useWeb3 } from "@/providers/Web3Provider"; import { QRCodeSVG } from "qrcode.react"; import React, { useState } from "react"; -import { arbitrum } from "viem/chains"; + import { useDeveloperMode } from "@/providers/developerMode"; import { useHasXautPosition } from "@/hooks/useHasXautPosition"; type TabType = "crypto" | "otc"; +type NetworkType = "ethereum" | "arbitrum"; export default function CashInPage() { const { walletClient, activeWalletAddress } = useWeb3(); const [activeTab, setActiveTab] = useState("crypto"); + const [selectedNetwork, setSelectedNetwork] = useState("ethereum"); // Developer mode and XAUT position detection for conditional hiding const { developerMode } = useDeveloperMode(); @@ -23,10 +25,12 @@ export default function CashInPage() { // Hide XAUT if developerMode=true AND hasXautPosition=false const shouldShowXaut = !developerMode || hasXautPosition; - // Conditional warning text based on shouldShowXaut - const warningText = shouldShowXaut - ? "Send only WBTC, ETH, XAUT and USDT to this address. Sending any other asset may result in permanent loss." - : "Send only WBTC, ETH and USDT to this address. Sending any other asset may result in permanent loss."; + // Conditional warning text based on network and shouldShowXaut + const warningText = selectedNetwork === "ethereum" + ? "Send only XAUT, USDT and ETH to this address. Sending any other asset may result in permanent loss." + : shouldShowXaut + ? "Send only WBTC, ETH, XAUT and USDT to this address. Sending any other asset may result in permanent loss." + : "Send only WBTC, ETH and USDT to this address. Sending any other asset may result in permanent loss."; const address = walletClient?.account?.address || activeWalletAddress; @@ -103,7 +107,14 @@ export default function CashInPage() {

Network

-

{arbitrum.name}

+

diff --git a/src/app/(main)/wallet/convert/page.tsx b/src/app/(main)/wallet/convert/page.tsx index 549cacf..0fab065 100644 --- a/src/app/(main)/wallet/convert/page.tsx +++ b/src/app/(main)/wallet/convert/page.tsx @@ -13,25 +13,43 @@ import { useState, useEffect, useCallback, useMemo } from "react"; import { parseUnits } from "viem"; import { toast } from "react-toastify"; -// Available tokens for swapping -const SWAP_TOKENS = ["WBTC", "USDT"] as const; -type SwapToken = (typeof SWAP_TOKENS)[number]; +type NetworkType = "ethereum" | "arbitrum"; + +// Available tokens per network +const NETWORK_TOKENS: Record = { + arbitrum: ["WBTC", "ETH", "USDT"], + ethereum: ["XAUT", "USDT", "ETH"], +}; + +const NETWORK_CHAIN_IDS: Record = { + arbitrum: 42161, + ethereum: 1, +}; // Token display names -const TOKEN_NAMES: Record = { +const TOKEN_NAMES: Record = { WBTC: "Wrapped Bitcoin", USDT: "Tether USD", + ETH: "Ethereum", + XAUT: "Tether Gold", }; // Token decimals -const TOKEN_DECIMALS: Record = { +const TOKEN_DECIMALS: Record = { WBTC: 8, USDT: 6, + ETH: 18, + XAUT: 6, }; export default function ConvertPage() { const router = useRouter(); - const { assets, refetchBalances } = useWalletBalanceContext(); + const { assets, refetchBalances, chainBalances } = useWalletBalanceContext(); + + const [selectedNetwork, setSelectedNetwork] = useState("arbitrum"); + const chainId = NETWORK_CHAIN_IDS[selectedNetwork]; + const availableTokens = NETWORK_TOKENS[selectedNetwork]; + const { quote, isLoading, @@ -39,23 +57,29 @@ export default function ConvertPage() { getQuote, executeSwap, getDestAmount, - } = useGaslessSwap(); + } = useGaslessSwap(chainId); - const [fromToken, setFromToken] = useState("WBTC"); - const [toToken, setToToken] = useState("USDT"); + const [fromToken, setFromToken] = useState(availableTokens[0]); + const [toToken, setToToken] = useState(availableTokens[1]); const [amount, setAmount] = useState(""); const [isSwapping, setIsSwapping] = useState(false); - // Get token balances - const fromBalance = useMemo(() => { - const asset = assets.find((a) => a.symbol === fromToken); - return asset?.amount || 0; - }, [assets, fromToken]); + // Reset tokens when network changes + useEffect(() => { + const tokens = NETWORK_TOKENS[selectedNetwork]; + setFromToken(tokens[0]); + setToToken(tokens[1]); + setAmount(""); + }, [selectedNetwork]); + + // Get chain-specific token balances + const getChainBalance = useCallback((token: string): number => { + const balances = selectedNetwork === "arbitrum" ? chainBalances.arbitrum : chainBalances.ethereum; + return balances[token] || 0; + }, [chainBalances, selectedNetwork]); - const toBalance = useMemo(() => { - const asset = assets.find((a) => a.symbol === toToken); - return asset?.amount || 0; - }, [assets, toToken]); + const fromBalance = useMemo(() => getChainBalance(fromToken), [getChainBalance, fromToken]); + const toBalance = useMemo(() => getChainBalance(toToken), [getChainBalance, toToken]); // Calculate output amount from quote const outputAmount = useMemo(() => { @@ -142,6 +166,23 @@ export default function ConvertPage() {

+ {/* Network Selector */} + +
+

+ Network +

+ +
+
+ {/* From Token */}

@@ -152,7 +193,23 @@ export default function ConvertPage() {

-

{fromToken}

+

{TOKEN_NAMES[fromToken]}

@@ -200,7 +257,23 @@ export default function ConvertPage() {
-

{toToken}

+

{TOKEN_NAMES[toToken]}

diff --git a/src/app/api/0x/route.ts b/src/app/api/0x/route.ts index d88ff27..8f5f793 100644 --- a/src/app/api/0x/route.ts +++ b/src/app/api/0x/route.ts @@ -1,17 +1,24 @@ import { NextRequest, NextResponse } from "next/server"; const OX_API_URL = "https://api.0x.org"; -const ARBITRUM_CHAIN_ID = 42161; -// Token addresses on Arbitrum -const TOKENS: Record = { - WBTC: "0x2f2a2543b76a4166549f7aab2e75bef0aefc5b0f", - USDT: "0xFd086bC7CD5C481DCC9C85ebE478A1C0b69FCbb9", -}; - -const TOKEN_DECIMALS: Record = { - WBTC: 8, - USDT: 6, +// Token addresses by chain +const TOKENS: Record> = { + WBTC: { + 1: "0x2260fac5e5542a773aa44fbcfedf7c193bc2c599", + 42161: "0x2f2a2543b76a4166549f7aab2e75bef0aefc5b0f", + }, + USDT: { + 1: "0xdAC17F958D2ee523a2206206994597C13D831ec7", + 42161: "0xFd086bC7CD5C481DCC9C85ebE478A1C0b69FCbb9", + }, + XAUT: { + 1: "0x68749665FF8D2d112Fa859AA293F07A622782F38", + }, + ETH: { + 1: "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE", + 42161: "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE", + }, }; // GET /api/0x - Get gasless quote @@ -21,6 +28,7 @@ export async function GET(request: NextRequest) { const buyToken = searchParams.get("buyToken"); const sellAmount = searchParams.get("sellAmount"); const taker = searchParams.get("taker"); + const chainIdStr = searchParams.get("chainId") || "42161"; // Get API key from env const apiKey = process.env.OX_API_KEY; @@ -39,12 +47,14 @@ export async function GET(request: NextRequest) { ); } + const chainId = parseInt(chainIdStr); + try { - const sellTokenAddress = TOKENS[sellToken] || sellToken; - const buyTokenAddress = TOKENS[buyToken] || buyToken; + const sellTokenAddress = TOKENS[sellToken]?.[chainId] || sellToken; + const buyTokenAddress = TOKENS[buyToken]?.[chainId] || buyToken; const params = new URLSearchParams({ - chainId: ARBITRUM_CHAIN_ID.toString(), + chainId: chainId.toString(), sellToken: sellTokenAddress, buyToken: buyTokenAddress, sellAmount, diff --git a/src/app/api/swap/route.ts b/src/app/api/swap/route.ts index 378bceb..fef118f 100644 --- a/src/app/api/swap/route.ts +++ b/src/app/api/swap/route.ts @@ -16,7 +16,7 @@ import { } from "@/lib/swap"; import type { SwapProviderName, SupportedChainId } from "@/lib/swap"; -// Token addresses for convenience (Arbitrum) +// Token addresses by chain const TOKENS: Record> = { WBTC: { 1: "0x2260fac5e5542a773aa44fbcfedf7c193bc2c599", // Ethereum @@ -26,6 +26,13 @@ const TOKENS: Record> = { 1: "0xdAC17F958D2ee523a2206206994597C13D831ec7", // Ethereum 42161: "0xFd086bC7CD5C481DCC9C85ebE478A1C0b69FCbb9", // Arbitrum }, + XAUT: { + 1: "0x68749665FF8D2d112Fa859AA293F07A622782F38", // Ethereum + }, + ETH: { + 1: "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE", // Native ETH + 42161: "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE", // Native ETH + }, }; /** diff --git a/src/components/common/AppVersion.tsx b/src/components/common/AppVersion.tsx new file mode 100644 index 0000000..a90c868 --- /dev/null +++ b/src/components/common/AppVersion.tsx @@ -0,0 +1,12 @@ +"use client"; + +const version = process.env.NEXT_PUBLIC_APP_VERSION || "0.0.0"; +const commitHash = process.env.NEXT_PUBLIC_COMMIT_HASH || "unknown"; + +export default function AppVersion() { + return ( +

+ v{version}-{commitHash} +

+ ); +} diff --git a/src/hooks/useGaslessSwap.ts b/src/hooks/useGaslessSwap.ts index 678cf52..30d4931 100644 --- a/src/hooks/useGaslessSwap.ts +++ b/src/hooks/useGaslessSwap.ts @@ -124,8 +124,8 @@ function parseApiError(errorString: string): string { return errorString; } -export function useGaslessSwap() { - const { walletClient, activeWalletAddress, ensureCorrectNetwork } = useWeb3(); +export function useGaslessSwap(chainId: number = 42161) { + const { walletClient, activeWalletAddress, switchToNetwork } = useWeb3(); const [isLoading, setIsLoading] = useState(false); const [quote, setQuote] = useState(null); const [error, setError] = useState(null); @@ -151,6 +151,7 @@ export function useGaslessSwap() { buyToken, sellAmount, taker: activeWalletAddress, + chainId: chainId.toString(), }); console.log("[Swap] Fetching quote:", params.toString()); @@ -181,7 +182,7 @@ export function useGaslessSwap() { setIsLoading(false); } }, - [activeWalletAddress] + [activeWalletAddress, chainId] ); // Split signature for 0x API format @@ -217,9 +218,9 @@ export function useGaslessSwap() { try { // Ensure wallet is on the correct network before signing // This fixes the "chainId should be same as current chainId" error - // when external wallets are connected to a different network (e.g., Ethereum) - console.log("[0x] Ensuring correct network..."); - await ensureCorrectNetwork(); + // when external wallets are connected to a different network + console.log("[0x] Ensuring correct network for chain", chainId, "..."); + await switchToNetwork(chainId); console.log("[0x] Network check passed"); // CRITICAL: Fetch a fresh quote right before signing @@ -230,6 +231,7 @@ export function useGaslessSwap() { buyToken: quote.buyToken, sellAmount: quote.sellAmount, taker: activeWalletAddress, + chainId: chainId.toString(), }); const freshQuoteResponse = await fetch(`/api/0x?${params}`); @@ -250,7 +252,7 @@ export function useGaslessSwap() { } const submitPayload: Record = { - chainId: 42161, + chainId, }; // Sign approval if needed (using fresh quote data) @@ -334,7 +336,7 @@ export function useGaslessSwap() { } finally { setIsLoading(false); } - }, [quote, walletClient, activeWalletAddress, ensureCorrectNetwork]); + }, [quote, walletClient, activeWalletAddress, switchToNetwork, chainId]); // Get destination amount from quote const getDestAmount = useCallback( diff --git a/src/hooks/useWalletBalance.ts b/src/hooks/useWalletBalance.ts index ae81d9f..ebeb9af 100644 --- a/src/hooks/useWalletBalance.ts +++ b/src/hooks/useWalletBalance.ts @@ -133,7 +133,7 @@ export function useWalletBalance(tokenPrices: Record = // Use Ethereum mainnet ETH balance (needed for Fluid/XAUT gas on Ethereum) const ethBalance = ethereumData?.ethBalance ?? 0; - // Track chain-specific balances for multi-chain tokens + // Track chain-specific balances for multi-chain tokens (including native ETH) const chainBalances = useMemo((): ChainBalances => { const arbitrum: Record = {}; const ethereum: Record = {}; @@ -144,6 +144,9 @@ export function useWalletBalance(tokenPrices: Record = arbitrum[symbol] = data.balance; }); } + if (arbitrumData?.ethBalance !== undefined) { + arbitrum.ETH = arbitrumData.ethBalance; + } // Ethereum balances if (ethereumData?.tokenBalances) { @@ -151,9 +154,12 @@ export function useWalletBalance(tokenPrices: Record = ethereum[symbol] = data.balance; }); } + if (ethereumData?.ethBalance !== undefined) { + ethereum.ETH = ethereumData.ethBalance; + } return { arbitrum, ethereum }; - }, [arbitrumData?.tokenBalances, ethereumData?.tokenBalances]); + }, [arbitrumData?.tokenBalances, arbitrumData?.ethBalance, ethereumData?.tokenBalances, ethereumData?.ethBalance]); // Merge token balances from both chains (for tokens that exist on both, sum them) const tokenBalances = useMemo(() => { diff --git a/test/hooks/useGaslessSwap.test.ts b/test/hooks/useGaslessSwap.test.ts index e87d5de..d4fbfae 100644 --- a/test/hooks/useGaslessSwap.test.ts +++ b/test/hooks/useGaslessSwap.test.ts @@ -212,12 +212,8 @@ describe("useGaslessSwap - Network Switching Tests", () => { }); }); - describe("Current implementation verification (expected to fail)", () => { - // These tests read the actual implementation and verify it contains the bug - // They will pass once the implementation is fixed - - it("executeSwap should call ensureCorrectNetwork (implementation check)", async () => { - // Read the actual implementation + describe("Network switching implementation verification", () => { + it("executeSwap should call switchToNetwork with chainId (implementation check)", async () => { const fs = await import("fs"); const path = await import("path"); @@ -232,35 +228,40 @@ describe("useGaslessSwap - Network Switching Tests", () => { if (executeSwapMatch) { const executeSwapCode = executeSwapMatch[0]; - // Check if ensureCorrectNetwork is called - const hasNetworkCheck = executeSwapCode.includes("ensureCorrectNetwork"); - - // This test documents the EXPECTED behavior - // Currently this will FAIL because the bug exists - expect(hasNetworkCheck).toBe(true); + // Check if switchToNetwork is called with chainId + const hasNetworkSwitch = executeSwapCode.includes("switchToNetwork(chainId)"); + expect(hasNetworkSwitch).toBe(true); } }); - it("should import ensureCorrectNetwork from useWeb3", async () => { + it("should import switchToNetwork from useWeb3", async () => { const fs = await import("fs"); const path = await import("path"); const hookPath = path.resolve(process.cwd(), "src/hooks/useGaslessSwap.ts"); const hookCode = fs.readFileSync(hookPath, "utf-8"); - // Check if ensureCorrectNetwork is destructured from useWeb3 + // Check if switchToNetwork is destructured from useWeb3 const useWeb3Destructure = hookCode.match(/const \{[^}]*\} = useWeb3\(\)/); expect(useWeb3Destructure).not.toBeNull(); if (useWeb3Destructure) { const destructured = useWeb3Destructure[0]; - const hasEnsureCorrectNetwork = destructured.includes("ensureCorrectNetwork"); - - // This test documents the EXPECTED behavior - expect(hasEnsureCorrectNetwork).toBe(true); + expect(destructured).toContain("switchToNetwork"); } }); + + it("should accept chainId parameter", async () => { + const fs = await import("fs"); + const path = await import("path"); + + const hookPath = path.resolve(process.cwd(), "src/hooks/useGaslessSwap.ts"); + const hookCode = fs.readFileSync(hookPath, "utf-8"); + + // The hook should accept a chainId parameter + expect(hookCode).toContain("export function useGaslessSwap(chainId:"); + }); }); describe("Fresh Quote Fetch Before Signing (Quote Expiry Fix)", () => { diff --git a/test/hooks/useWalletBalanceEthDisplay.test.ts b/test/hooks/useWalletBalanceEthDisplay.test.ts index f9a9517..f34b44f 100644 --- a/test/hooks/useWalletBalanceEthDisplay.test.ts +++ b/test/hooks/useWalletBalanceEthDisplay.test.ts @@ -55,18 +55,21 @@ describe("useWalletBalance - ETH Display for All Users", () => { }); describe("Ethereum mainnet ETH balance", () => { - it("should use ethereumData.ethBalance instead of arbitrumData.ethBalance", () => { - // The ETH balance should come from Ethereum mainnet since that's where + it("should use ethereumData.ethBalance for the top-level ethBalance variable", () => { + // The top-level ETH balance should come from Ethereum mainnet since that's where // gas is needed for Fluid/XAUT operations + // Note: arbitrumData?.ethBalance may appear in chainBalances for per-chain tracking, + // but the main ethBalance variable must use ethereumData - // Should NOT have: arbitrumData?.ethBalance - // Should HAVE: ethereumData?.ethBalance - - const usesArbitrumEth = hookCode.includes("arbitrumData?.ethBalance"); const usesEthereumEth = hookCode.includes("ethereumData?.ethBalance"); - - expect(usesArbitrumEth).toBe(false); expect(usesEthereumEth).toBe(true); + + // Inside useWalletBalance function, the ethBalance assignment should reference ethereumData + const hookFnStart = hookCode.indexOf("export function useWalletBalance"); + const hookBody = hookCode.slice(hookFnStart); + const ethBalanceLine = hookBody.match(/const ethBalance = .+/); + expect(ethBalanceLine).not.toBeNull(); + expect(ethBalanceLine![0]).toContain("ethereumData"); }); });