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
+
+
setSelectedNetwork(e.target.value as NetworkType)}
+ className="bg-transparent text-white font-medium mt-1 outline-none cursor-pointer appearance-none pr-6 bg-[url('data:image/svg+xml;charset=utf-8,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%2212%22%20height%3D%2212%22%20viewBox%3D%220%200%2012%2012%22%3E%3Cpath%20fill%3D%22%23ffffff%22%20d%3D%22M2%204l4%204%204-4%22%2F%3E%3C%2Fsvg%3E')] bg-no-repeat bg-[right_center]"
+ >
+ Arbitrum One
+ Ethereum ERC20
+
+
+
+
{/* From Token */}
@@ -152,7 +193,23 @@ export default function ConvertPage() {
-
{fromToken}
+
{
+ const newFrom = e.target.value;
+ setFromToken(newFrom);
+ if (newFrom === toToken) {
+ const other = availableTokens.find(t => t !== newFrom);
+ if (other) setToToken(other);
+ }
+ setAmount("");
+ }}
+ className="bg-transparent text-white font-semibold outline-none cursor-pointer appearance-none pr-5 bg-[url('data:image/svg+xml;charset=utf-8,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%2210%22%20height%3D%2210%22%20viewBox%3D%220%200%2012%2012%22%3E%3Cpath%20fill%3D%22%23ffffff%22%20d%3D%22M2%204l4%204%204-4%22%2F%3E%3C%2Fsvg%3E')] bg-no-repeat bg-[right_center]"
+ >
+ {availableTokens.map(t => (
+ {t}
+ ))}
+
{TOKEN_NAMES[fromToken]}
@@ -200,7 +257,23 @@ export default function ConvertPage() {
-
{toToken}
+
{
+ const newTo = e.target.value;
+ setToToken(newTo);
+ if (newTo === fromToken) {
+ const other = availableTokens.find(t => t !== newTo);
+ if (other) setFromToken(other);
+ }
+ setAmount("");
+ }}
+ className="bg-transparent text-white font-semibold outline-none cursor-pointer appearance-none pr-5 bg-[url('data:image/svg+xml;charset=utf-8,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%2210%22%20height%3D%2210%22%20viewBox%3D%220%200%2012%2012%22%3E%3Cpath%20fill%3D%22%23ffffff%22%20d%3D%22M2%204l4%204%204-4%22%2F%3E%3C%2Fsvg%3E')] bg-no-repeat bg-[right_center]"
+ >
+ {availableTokens.map(t => (
+ {t}
+ ))}
+
{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");
});
});