Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions next.config.ts
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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);
2 changes: 2 additions & 0 deletions src/app/(main)/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
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 }) => {
return (
<div>
<EarlyAccessModal />
{children}
<AppVersion />
<ResponsiveNav />
</div>
);
Expand Down
23 changes: 17 additions & 6 deletions src/app/(main)/wallet/cash-in/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<TabType>("crypto");
const [selectedNetwork, setSelectedNetwork] = useState<NetworkType>("ethereum");

// Developer mode and XAUT position detection for conditional hiding
const { developerMode } = useDeveloperMode();
Expand All @@ -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;

Expand Down Expand Up @@ -103,7 +107,14 @@ export default function CashInPage() {
<p className="text-white/40 text-xs uppercase tracking-wider">
Network
</p>
<p className="text-white font-medium">{arbitrum.name}</p>
<select
value={selectedNetwork}
onChange={(e) => 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]"
>
<option value="ethereum" className="bg-zinc-900 text-white">Ethereum ERC20</option>
<option value="arbitrum" className="bg-zinc-900 text-white">Arbitrum One</option>
</select>
</div>
<div className="text-right">
<p className="text-white/40 text-xs uppercase tracking-wider">
Expand Down
113 changes: 93 additions & 20 deletions src/app/(main)/wallet/convert/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,49 +13,73 @@ 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<NetworkType, string[]> = {
arbitrum: ["WBTC", "ETH", "USDT"],
ethereum: ["XAUT", "USDT", "ETH"],
};

const NETWORK_CHAIN_IDS: Record<NetworkType, number> = {
arbitrum: 42161,
ethereum: 1,
};

// Token display names
const TOKEN_NAMES: Record<SwapToken, string> = {
const TOKEN_NAMES: Record<string, string> = {
WBTC: "Wrapped Bitcoin",
USDT: "Tether USD",
ETH: "Ethereum",
XAUT: "Tether Gold",
};

// Token decimals
const TOKEN_DECIMALS: Record<SwapToken, number> = {
const TOKEN_DECIMALS: Record<string, number> = {
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<NetworkType>("arbitrum");
const chainId = NETWORK_CHAIN_IDS[selectedNetwork];
const availableTokens = NETWORK_TOKENS[selectedNetwork];

const {
quote,
isLoading,
error,
getQuote,
executeSwap,
getDestAmount,
} = useGaslessSwap();
} = useGaslessSwap(chainId);

const [fromToken, setFromToken] = useState<SwapToken>("WBTC");
const [toToken, setToToken] = useState<SwapToken>("USDT");
const [fromToken, setFromToken] = useState<string>(availableTokens[0]);
const [toToken, setToToken] = useState<string>(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(() => {
Expand Down Expand Up @@ -142,6 +166,23 @@ export default function ConvertPage() {
<div className="w-full max-w-xl mx-auto px-4 md:px-6 py-6 flex flex-col gap-6 pt-[calc(80px+env(safe-area-inset-top)+0.5rem)]">
<PageHeader title="Convert" />

{/* Network Selector */}
<Card appearance="glassDark" padding="default">
<div>
<p className="text-white/40 text-xs uppercase tracking-wider">
Network
</p>
<select
value={selectedNetwork}
onChange={(e) => 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]"
>
<option value="arbitrum" className="bg-zinc-900 text-white">Arbitrum One</option>
<option value="ethereum" className="bg-zinc-900 text-white">Ethereum ERC20</option>
</select>
</div>
</Card>

{/* From Token */}
<div>
<p className="text-amber-500 text-xs font-medium uppercase tracking-wider mb-2">
Expand All @@ -152,7 +193,23 @@ export default function ConvertPage() {
<div className="flex items-center gap-3">
<TokenIcon symbol={fromToken} width={40} height={40} />
<div>
<p className="text-white font-semibold">{fromToken}</p>
<select
value={fromToken}
onChange={(e) => {
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 => (
<option key={t} value={t} className="bg-zinc-900 text-white">{t}</option>
))}
</select>
<p className="text-white/50 text-sm">{TOKEN_NAMES[fromToken]}</p>
</div>
</div>
Expand Down Expand Up @@ -200,7 +257,23 @@ export default function ConvertPage() {
<div className="flex items-center gap-3">
<TokenIcon symbol={toToken} width={40} height={40} />
<div>
<p className="text-white font-semibold">{toToken}</p>
<select
value={toToken}
onChange={(e) => {
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 => (
<option key={t} value={t} className="bg-zinc-900 text-white">{t}</option>
))}
</select>
<p className="text-white/50 text-sm">{TOKEN_NAMES[toToken]}</p>
</div>
</div>
Expand Down
36 changes: 23 additions & 13 deletions src/app/api/0x/route.ts
Original file line number Diff line number Diff line change
@@ -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<string, string> = {
WBTC: "0x2f2a2543b76a4166549f7aab2e75bef0aefc5b0f",
USDT: "0xFd086bC7CD5C481DCC9C85ebE478A1C0b69FCbb9",
};

const TOKEN_DECIMALS: Record<string, number> = {
WBTC: 8,
USDT: 6,
// Token addresses by chain
const TOKENS: Record<string, Record<number, string>> = {
WBTC: {
1: "0x2260fac5e5542a773aa44fbcfedf7c193bc2c599",
42161: "0x2f2a2543b76a4166549f7aab2e75bef0aefc5b0f",
},
USDT: {
1: "0xdAC17F958D2ee523a2206206994597C13D831ec7",
42161: "0xFd086bC7CD5C481DCC9C85ebE478A1C0b69FCbb9",
},
XAUT: {
1: "0x68749665FF8D2d112Fa859AA293F07A622782F38",
},
ETH: {
1: "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE",
42161: "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE",
},
};

// GET /api/0x - Get gasless quote
Expand All @@ -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;
Expand All @@ -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,
Expand Down
9 changes: 8 additions & 1 deletion src/app/api/swap/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, Record<number, string>> = {
WBTC: {
1: "0x2260fac5e5542a773aa44fbcfedf7c193bc2c599", // Ethereum
Expand All @@ -26,6 +26,13 @@ const TOKENS: Record<string, Record<number, string>> = {
1: "0xdAC17F958D2ee523a2206206994597C13D831ec7", // Ethereum
42161: "0xFd086bC7CD5C481DCC9C85ebE478A1C0b69FCbb9", // Arbitrum
},
XAUT: {
1: "0x68749665FF8D2d112Fa859AA293F07A622782F38", // Ethereum
},
ETH: {
1: "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE", // Native ETH
42161: "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE", // Native ETH
},
};

/**
Expand Down
12 changes: 12 additions & 0 deletions src/components/common/AppVersion.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<p className="text-center text-white/20 text-[10px] font-mono pb-20">
v{version}-{commitHash}
</p>
);
}
Loading
Loading