Skip to content
Open
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
3 changes: 1 addition & 2 deletions next.config.mjs
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
/** @type {import('next').NextConfig} */
const nextConfig = {
output: "export",
distDir: "out",

trailingSlash: true,
images: {
unoptimized: true,
Expand Down
9 changes: 9 additions & 0 deletions src/app/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -907,3 +907,12 @@ html {
.dark .glass-card-smooth:hover {
box-shadow: 0 12px 40px 0 rgba(0, 0, 0, 0.5);
}

@keyframes pulse {
0%, 100% {
opacity: 1;
}
50% {
opacity: 0.5;
}
}
16 changes: 10 additions & 6 deletions src/app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ import Navbar from "@/components/layout/Navbar";
import Footer from "@/components/layout/Footer";
import { ThemeProvider } from "@/components/themeProvider";

import { headers } from "next/headers";
import { cookieToInitialState } from "wagmi";
import { config } from "@/utils/wagmiConfig";

const geistSans = Geist({
variable: "--font-geist-sans",
Expand All @@ -28,11 +31,14 @@ export const metadata: Metadata = {
},
};

export default function RootLayout({
export default async function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
const cookieHeader = (await headers()).get('cookie');
const initialState = cookieHeader ? cookieToInitialState(config, cookieHeader) : undefined;

return (
<html lang="en" suppressHydrationWarning>
<head>
Expand All @@ -41,7 +47,7 @@ export default function RootLayout({
__html: `
(function() {
try {
var theme = localStorage.getItem('theme') || 'system';
var theme = localStorage.getItem('fate-protocol-theme') || 'system';
var systemTheme = window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
var finalTheme = theme === 'system' ? systemTheme : theme;

Expand All @@ -57,17 +63,15 @@ export default function RootLayout({
/>
</head>
<body className={`${geistSans.variable} ${geistMono.variable} antialiased`}>
<ThemeProvider>
<ClientProviders>
<ClientProviders initialState={initialState}>
<Navbar />

<main className="pt-16">
{children}
</main>

<Footer />
</ClientProviders>
</ThemeProvider>
</ClientProviders>
</body>
</html>
);
Expand Down
201 changes: 193 additions & 8 deletions src/app/trade/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@ import {
isDeployedAddress,
} from "@/utils/addresses";

import { motion, AnimatePresence } from "framer-motion";
import { Loader2, ArrowDownUp, CheckCircle2, AlertCircle, Info, Wallet } from "lucide-react";

import DJED_ABI from "@/utils/abi/Djed.json";
import COIN_ABI from "@/utils/abi/Coin.json";
import UnsupportedNetwork from "@/components/UnsupportedNetwork";
Expand Down Expand Up @@ -118,6 +121,24 @@ function TradePage() {
enabled: isDeployedAddress(contractAddress),
});

const { data: baseCoinBalance } = useReadContract({
address: baseCoinAddress as `0x${string}`,
abi: COIN_ABI,
functionName: "balanceOf",
args: address ? [address] : undefined,
chainId,
enabled: isDeployedAddress(baseCoinAddress) && Boolean(address),
});
Comment thread
coderabbitai[bot] marked this conversation as resolved.

const { data: stableCoinBalance } = useReadContract({
address: stableCoin,
abi: COIN_ABI,
functionName: "balanceOf",
args: address ? [address] : undefined,
chainId,
enabled: isDeployedAddress(stableCoin) && Boolean(address),
});

const { data: baseCoinAllowance } = useReadContract({
address: baseCoinAddress as `0x${string}`,
abi: COIN_ABI,
Expand Down Expand Up @@ -280,16 +301,180 @@ function TradePage() {
return <div>Connect Wallet</div>;
}

/* ================= UI ================= */
/* ================= UI / FEE / STATE CALCS ================= */

return (
<div>
<button onClick={handleTrade} disabled={!parsedAmount || isPending}>
Execute Trade
</button>
const isBuy = tradeType === "buy-stable";

// Balances
const dBaseBalance = baseCoinBalance ? Number(formatUnits(baseCoinBalance as bigint, 18)).toFixed(2) : "0.00";
const dStableBalance = stableCoinBalance ? Number(formatUnits(stableCoinBalance as bigint, 18)).toFixed(2) : "0.00";

// Complex fee calculation (Mocked 2% platform fee for demonstration)
const numericAmount = parseFloat(amount || "0");
const feePercentage = 0.02;
const feeAmount = numericAmount * feePercentage;
const finalAmount = numericAmount > 0 ? numericAmount - feeAmount : 0;
Comment on lines +312 to +316

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Mocked fee calculation displayed as actual transaction cost.

The 2% fee is hardcoded and labeled "Platform Fee" in the UI, but the actual on-chain fee logic may differ. This could mislead users about the real cost of their transaction. Either fetch the actual fee from the contract or clearly label this as an estimate/placeholder.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/app/trade/page.tsx` around lines 312 - 316, The UI currently uses a
hardcoded 2% calculation (numericAmount, feePercentage, feeAmount, finalAmount)
and presents it as "Platform Fee"; update the implementation to avoid misleading
users by either (A) replacing the hardcoded feePercentage with a real lookup
from your on‑chain or backend fee estimator (e.g., call a new or existing
getEstimatedFee(amount) or fetchContractFee() and compute feeAmount from that
result) or (B) if the real fee cannot yet be retrieved, change the label and
messaging to explicitly mark the value as an estimate/placeholder (e.g., add
“Estimated Platform Fee” and an info tooltip) and keep the fallback numeric
computation only for display. Ensure the code paths that compute feeAmount and
finalAmount reference the fetched fee value or an isEstimate flag so consumers
of numericAmount, feeAmount, and finalAmount are unambiguous.


// Deriving transaction stages
const needsApproval = isBuy
? (baseCoinAllowance ?? 0n) < parsedAmount
: (stableCoinAllowance ?? 0n) < parsedAmount;

const isInsufficientBalance = isBuy
? (baseCoinBalance !== undefined && parsedAmount > (baseCoinBalance as bigint))
: (stableCoinBalance !== undefined && parsedAmount > (stableCoinBalance as bigint));

{error && <div>{error.message}</div>}
{isConfirmed && <div>Trade Confirmed</div>}
const isApproving = isPending && isApprovalConfirmed === false && approvalHash !== undefined;
const isExecuting = isPending && !isApproving;

const getButtonState = () => {
if (isPending) {
if (needsApproval) return { text: "Processing Approval...", icon: <Loader2 className="w-5 h-5 animate-spin" />, disabled: true };
return { text: "Processing Trade...", icon: <Loader2 className="w-5 h-5 animate-spin" />, disabled: true };
}
if (isConfirmed && !error && numericAmount > 0) return { text: "Confirmed! ✅", icon: null, disabled: false };

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Button shows "Confirmed!" even when a previous transaction error exists.

The condition isConfirmed && !error && numericAmount > 0 checks the current error state, but isConfirmed persists from the last successful transaction. If a user completes a trade, then enters a new amount and gets an error, the button could still show "Confirmed!" because isConfirmed wasn't reset.

Consider resetting transaction state when the user changes the amount or trade type.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/app/trade/page.tsx` at line 335, Button shows "Confirmed!" because
isConfirmed persists across input changes; update the component to reset
transaction state when the user modifies the trade inputs. Specifically,
wherever you store the states (isConfirmed, error, numericAmount/trade type) add
a handler or useEffect that listens to changes in numericAmount or tradeType and
calls the state setters to clear previous transaction state (e.g., call
setIsConfirmed(false) and setError(null) when numericAmount or tradeType
changes). Ensure this reset logic runs on both direct input change handlers and
any trade-type selector changes so the condition in the render (isConfirmed &&
!error && numericAmount > 0) reflects the current inputs.


if (numericAmount === 0) return { text: "Enter an amount", icon: null, disabled: true };
if (isInsufficientBalance) return { text: "Insufficient Balance", icon: null, disabled: true };
if (needsApproval) return { text: `Step 1: Approve ${isBuy ? 'BaseCoin' : 'StableCoin'}`, icon: null, disabled: false };

return { text: `Step 2: ${isBuy ? 'Buy StableCoin' : 'Sell StableCoin'}`, icon: null, disabled: false };
};

const btnState = getButtonState();

return (
<div className="w-full max-w-lg mx-auto mt-12 p-1">
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
className="bg-white/50 dark:bg-slate-900/50 backdrop-blur-xl rounded-[2rem] p-6 shadow-2xl border border-white/20 dark:border-slate-800/50"
>
<div className="flex items-center justify-between mb-8">
<h2 className="text-2xl font-bold bg-gradient-to-r from-orange-600 to-orange-400 bg-clip-text text-transparent">
Swap StableCoins
</h2>
<div className="flex bg-slate-200/50 dark:bg-slate-800/50 rounded-full p-1 border border-slate-300/30 dark:border-slate-700/50">
<button
onClick={() => { setTradeType("buy-stable"); setAmount(""); }}
className={`px-4 py-1.5 rounded-full text-sm font-semibold transition-all ${isBuy ? 'bg-white dark:bg-slate-700 shadow-md text-orange-600 dark:text-orange-400' : 'text-slate-600 dark:text-slate-400'}`}
>
Buy
</button>
<button
onClick={() => { setTradeType("sell-stable"); setAmount(""); }}
className={`px-4 py-1.5 rounded-full text-sm font-semibold transition-all ${!isBuy ? 'bg-white dark:bg-slate-700 shadow-md text-orange-600 dark:text-orange-400' : 'text-slate-600 dark:text-slate-400'}`}
>
Sell
</button>
</div>
</div>

{/* Input Box */}
<div className="bg-slate-100/50 dark:bg-slate-800/50 rounded-2xl p-4 mb-2 border border-slate-200/50 dark:border-slate-700/50 transition-all focus-within:ring-2 focus-within:ring-orange-500/50">
<div className="flex justify-between items-center mb-2">
<label className="text-sm font-medium text-slate-500 dark:text-slate-400">
You {isBuy ? 'Pay' : 'Convert'}
</label>
<div className="flex items-center gap-1.5 text-xs font-semibold text-slate-500 bg-white/50 dark:bg-slate-900/50 px-2.5 py-1 rounded-lg">
<Wallet className="w-3.5 h-3.5 text-orange-500" />
{isBuy ? `${dBaseBalance} BaseCoin` : `${dStableBalance} DJED`}
</div>
</div>

<div className="flex items-center gap-3">
<input
type="number"
value={amount}
onChange={(e) => setAmount(e.target.value)}
placeholder="0.00"
className="w-full bg-transparent text-4xl font-black text-slate-800 dark:text-white outline-none placeholder:text-slate-300 dark:placeholder:text-slate-700"
/>
<div className="shrink-0 bg-white dark:bg-slate-800 shadow-sm border border-slate-200 dark:border-slate-700 rounded-xl px-4 py-2 flex items-center gap-2 font-bold text-base md:text-lg">
{isBuy ? '🪙 Base' : '🌟 DJED'}
</div>
</div>
</div>

{/* Swap Arrow */}
<div className="flex justify-center -my-3 relative z-10 pointer-events-none">
<div className="bg-white dark:bg-slate-800 border-4 border-slate-50 dark:border-slate-900 p-2 rounded-full shadow-lg">
<ArrowDownUp className="w-5 h-5 text-orange-500" />
</div>
</div>

{/* Output Box */}
<div className="bg-white/60 dark:bg-slate-800/30 rounded-2xl p-4 mt-2 border border-slate-200/50 dark:border-slate-700/50 relative overflow-hidden">
<div className="flex justify-between items-center mb-2">
<label className="text-sm font-medium text-slate-500 dark:text-slate-400">
You Receive (Estimated)
</label>
<div className="flex items-center gap-1.5 text-xs font-semibold text-slate-500 bg-white/50 dark:bg-slate-900/50 px-2.5 py-1 rounded-lg">
<Wallet className="w-3.5 h-3.5 text-orange-500" />
{!isBuy ? `${dBaseBalance} BaseCoin` : `${dStableBalance} DJED`}
</div>
</div>

<div className="flex items-center gap-3">
<input
type="text"
readOnly
value={finalAmount > 0 ? finalAmount.toFixed(4) : ""}
placeholder="0.00"
className="w-full bg-transparent text-3xl font-bold text-slate-600 dark:text-slate-200 outline-none placeholder:text-slate-300 dark:placeholder:text-slate-700"
/>
<div className="shrink-0 bg-slate-100 dark:bg-slate-900 shadow-inner rounded-xl px-4 py-2 flex items-center gap-2 font-bold text-base md:text-lg text-slate-500">
{!isBuy ? '🪙 Base' : '🌟 DJED'}
</div>
</div>
</div>

{/* Complex Fee Details */}
<AnimatePresence>
{numericAmount > 0 && (
<motion.div
initial={{ height: 0, opacity: 0 }}
animate={{ height: "auto", opacity: 1 }}
exit={{ height: 0, opacity: 0 }}
className="overflow-hidden mt-4"
>
<div className="bg-slate-50 dark:bg-slate-900/50 rounded-xl p-4 space-y-2 border border-slate-200/50 dark:border-slate-800">
<div className="flex justify-between items-center text-sm">
<span className="text-slate-500 flex items-center gap-1"><Info className="w-4 h-4"/> Input Amount</span>
<span className="font-semibold">{numericAmount.toFixed(2)} {isBuy ? 'BaseCoin' : 'DJED'}</span>
</div>
<div className="flex justify-between items-center text-sm">
<span className="text-slate-500 flex items-center gap-1">Platform Fee (2%)</span>
<span className="font-semibold text-red-500">- {feeAmount.toFixed(4)} {isBuy ? 'BaseCoin' : 'DJED'}</span>
</div>
<div className="h-px w-full bg-slate-200 dark:bg-slate-700 my-2" />
<div className="flex justify-between items-center font-bold text-orange-600 dark:text-orange-400">
<span>Final Output</span>
<span>{finalAmount.toFixed(4)} {!isBuy ? 'BaseCoin' : 'DJED'}</span>
</div>
</div>
</motion.div>
)}
</AnimatePresence>

{/* Transaction Status Area */}
{error && (
<div className="mt-4 bg-red-50 dark:bg-red-900/20 text-red-600 dark:text-red-400 p-4 rounded-xl flex items-start gap-3 border border-red-200 dark:border-red-800/30">
<AlertCircle className="w-5 h-5 shrink-0 mt-0.5" />
<div className="text-sm font-medium pr-2 break-all overflow-hidden overflow-ellipsis line-clamp-2">
Transaction Failed: {error?.message?.split('\n')[0] || "Unknown error"}
</div>
</div>
)}

<button
onClick={handleTrade}
disabled={!parsedAmount || btnState.disabled}
className="w-full mt-6 bg-gradient-to-r from-orange-600 to-orange-500 hover:from-orange-500 hover:to-orange-400 text-white font-bold py-4 px-6 rounded-xl shadow-xl shadow-orange-500/30 transition-all active:scale-[0.98] disabled:opacity-50 disabled:active:scale-100 disabled:shadow-none flex items-center justify-center gap-2"
>
{btnState.text} {btnState.icon}
</button>
</motion.div>
</div>
);
}
Expand Down
12 changes: 10 additions & 2 deletions src/components/layout/Layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,15 @@ import { ThemeProvider } from "@/components/themeProvider";
import { WalletProvider } from "@/context/walletProvider";
import { Toaster } from "sonner";

export function ClientProviders({ children }: { children: React.ReactNode }) {
import { State } from "wagmi";

export function ClientProviders({
children,
initialState
}: {
children: React.ReactNode;
initialState?: State;
}) {
return (
<ThemeProvider
attribute="class"
Expand All @@ -12,7 +20,7 @@ export function ClientProviders({ children }: { children: React.ReactNode }) {
disableTransitionOnChange
storageKey="fate-protocol-theme"
>
<WalletProvider>
<WalletProvider initialState={initialState}>
{children}
<Toaster position="top-right" richColors />
</WalletProvider>
Expand Down
Loading