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
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,3 +58,7 @@ YieldSafe is non-custodial. Your funds are always in the vault or the underlying
---

Built with ❤️ by [emdevelopa](https://github.com/emdevelopa)


i want to ask basaed on the screenshot, the APY 0.58% percentage in the screenshot is what is being used for the calculation right for yieldsave users also

260 changes: 167 additions & 93 deletions app/app/page.tsx

Large diffs are not rendered by default.

9 changes: 7 additions & 2 deletions app/page.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
"use client";
import Link from "next/link";
import { useRouter } from "next/navigation";
import { useAppKit, useAppKitAccount } from "@reown/appkit/react";
import {
ArrowRight,
Expand All @@ -16,6 +17,10 @@ import { AppNavbar } from "../components/ui/AppNavbar";
export default function HomePage() {
const { isConnected, address } = useAppKitAccount();
const { open } = useAppKit();
const router = useRouter();

// Removed automatic redirect to dashboard if the wallet is connected
// so users can still see the landing page

return (
<div className="min-h-screen selection:bg-primary selection:text-white bg-background text-foreground overflow-hidden">
Expand All @@ -41,7 +46,7 @@ export default function HomePage() {

<div className="flex flex-col sm:flex-row gap-4 justify-center lg:justify-start">
<button
onClick={() => { if(!isConnected) open(); else window.location.href="/app"; }}
onClick={() => { if(!isConnected) open(); else router.push("/app"); }}
className="btn-primary !px-8 !py-4 text-base group shadow-lg shadow-primary/25"
>
Start Saving
Expand All @@ -67,7 +72,7 @@ export default function HomePage() {
<div className="text-4xl lg:text-5xl font-extrabold text-foreground tracking-tight">$124,500.00</div>
</div>
<div className="bg-primary text-white text-sm font-bold px-3 py-1 rounded-md">
+4.2% APY
3.35% - 3.45% APY
</div>
</div>

Expand Down
9 changes: 5 additions & 4 deletions components/Dashboard/ActionTabs.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,16 @@
"use client";
import React from "react";
import { Tab } from "../ui/Sidebar";
import { DepositIcon, WithdrawIcon, EarnIcon } from "../ui/Icons";

type ActionTab = "deposit" | "withdraw" | "rewards";

interface ActionTabsProps {
activeTab: Tab;
setActiveTab: (tab: Tab) => void;
activeTab: ActionTab;
setActiveTab: (tab: ActionTab) => void;
}

export const ActionTabs = ({ activeTab, setActiveTab }: ActionTabsProps) => {
const tabs: { id: Tab; label: string; icon: React.ReactNode }[] = [
const tabs: { id: ActionTab; label: string; icon: React.ReactNode }[] = [
{ id: "deposit", label: "Deposit", icon: <DepositIcon /> },
{ id: "withdraw", label: "Withdraw", icon: <WithdrawIcon /> },
{ id: "rewards", label: "Rewards", icon: <EarnIcon /> },
Expand Down
11 changes: 10 additions & 1 deletion components/Dashboard/StatsGrid.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
"use client";
import React from "react";
import { formatUnits } from "ethers";
import { useAaveApr } from "../../hooks/useAaveApr";

interface StatsGridProps {
vaultBalance: bigint | null;
Expand All @@ -11,6 +12,8 @@ export const StatsGrid = ({
vaultBalance,
isLoadingVaultBalance,
}: StatsGridProps) => {
const { apr, isLoading: isLoadingApr } = useAaveApr();

const fmt = (val: bigint | null) =>
val
? parseFloat(formatUnits(val, 6)).toLocaleString(undefined, {
Expand All @@ -19,6 +22,8 @@ export const StatsGrid = ({
})
: "0.00";

const aprDisplay = isLoadingApr ? "…" : (apr ?? "—");

return (
<div className="space-y-4">
{[
Expand All @@ -27,7 +32,11 @@ export const StatsGrid = ({
label: "Total Vault TVL",
cls: "text-primary",
},
{ val: "4.8%", label: "Current Variable APR", cls: "text-teal" },
{
val: aprDisplay,
label: "Current Variable APR",
cls: "text-teal",
},
].map((stat, i) => (
<div
key={i}
Expand Down
85 changes: 42 additions & 43 deletions components/Withdraw/WithdrawForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,24 +20,35 @@ export const WithdrawForm = ({
isConnected,
}: WithdrawFormProps) => {
const [withdrawAmt, setWithdrawAmt] = useState("");
const [withdrawPreview, setWithdrawPreview] = useState<bigint | null>(null);
const [withdrawPreview, setWithdrawPreview] = useState<{
payout: bigint;
grossAssets: bigint;
fee: bigint;
} | null>(null);

const { previewByShares, isPreviewingWithdraw } = usePreviewWithdraw();
const { submitWithdraw, isWithdrawing } = useWithdraw();

useEffect(() => {
if (!withdrawAmt || !isConnected) {
// eslint-disable-next-line react-hooks/set-state-in-effect
if (withdrawPreview !== null) setWithdrawPreview(null);
return;
}
try {
const shares = parseUnits(withdrawAmt, USDC_DECIMALS);
previewByShares(shares).then(setWithdrawPreview);
} catch {
// Handle invalid input gracefully
}
}, [withdrawAmt, isConnected, previewByShares, withdrawPreview]);
const fetchPreview = async () => {
if (!withdrawAmt || !isConnected) {
setWithdrawPreview(null);
return;
}
try {
const shares = parseUnits(withdrawAmt, USDC_DECIMALS);
const res = await previewByShares(shares);
setWithdrawPreview(res);
} catch {
// Handle invalid input gracefully
}
};

const timer = setTimeout(() => {
fetchPreview();
}, 0);
return () => clearTimeout(timer);
}, [withdrawAmt, isConnected, previewByShares]);

const handleWithdraw = async () => {
if (!withdrawAmt) return;
Expand All @@ -49,34 +60,22 @@ export const WithdrawForm = ({
}
};

const fmt = (val: bigint | null) =>
val
? parseFloat(formatUnits(val, USDC_DECIMALS)).toLocaleString(undefined, {
minimumFractionDigits: 2,
maximumFractionDigits: 6,
})
: "0.00";

// Calculate yield accurately based on principal proportion
const getYieldInfo = () => {
if (!withdrawPreview || !withdrawAmt || !userShares || !userDeposits) return { yield: 0, fee: 0 };


const grossAssets = parseFloat(formatUnits(withdrawPreview, USDC_DECIMALS));

// Proportional principal calculation
// principalForShares = (sharesToWithdraw / totalShares) * totalDeposits
const totalShares = parseFloat(formatUnits(userShares, USDC_DECIMALS));
const totalDeposits = parseFloat(formatUnits(userDeposits, USDC_DECIMALS));

const principalForShares = (parseFloat(withdrawAmt) / totalShares) * totalDeposits;
const yieldAmount = Math.max(0, grossAssets - principalForShares);
const feeAmount = yieldAmount * 0.05;
// Compute payout, yield, and fee using bigint arithmetic for precision
const { payout, calcYield, calcFee } = React.useMemo(() => {
if (!withdrawPreview || !userShares || !userDeposits) {
return { payout: 0, calcYield: 0, calcFee: 0 };
}
// proportional principal = userDeposits * sharesRequested / userShares
const shares = withdrawAmt ? parseUnits(withdrawAmt, USDC_DECIMALS) : BigInt(0);
const principalPortion = userShares === BigInt(0) ? BigInt(0) : (userDeposits * shares) / userShares;
const yieldAmt = withdrawPreview.grossAssets > principalPortion ? withdrawPreview.grossAssets - principalPortion : BigInt(0);

return { yield: yieldAmount, fee: feeAmount };
};

const { yield: calcYield, fee: calcFee } = getYieldInfo();
return {
payout: Number(formatUnits(withdrawPreview.payout, USDC_DECIMALS)),
calcYield: Number(formatUnits(yieldAmt, USDC_DECIMALS)),
calcFee: Number(formatUnits(withdrawPreview.fee, USDC_DECIMALS)),
};
}, [withdrawPreview, userShares, userDeposits, withdrawAmt]);

const labelCls = "text-muted-foreground text-sm";
const valueCls = "font-mono font-bold text-sm text-foreground";
Expand Down Expand Up @@ -123,10 +122,10 @@ export const WithdrawForm = ({
<div className={infoRow}>
<span className={labelCls}>You will receive</span>
<span className={valueCls}>
{isPreviewingWithdraw
{withdrawPreview
? payout.toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 6 })
: isPreviewingWithdraw
? "…"
: withdrawPreview !== null
? fmt(withdrawPreview)
: "—"}{" "}
USDC
</span>
Expand Down
3 changes: 3 additions & 0 deletions components/ui/AppNavbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,9 @@ export const AppNavbar = ({

{isConnected && address ? (
<div className="flex items-center gap-4">
<Link href="/app" className="hidden sm:flex items-center gap-2 px-4 py-2 text-sm font-bold text-primary hover:bg-primary/10 rounded-lg transition-colors border border-primary/20">
Dashboard
</Link>
<div className="hidden sm:flex items-center gap-2 bg-muted px-4 py-2 font-mono text-sm font-semibold rounded-lg border border-border">
<div className="w-2 h-2 rounded-full bg-success shadow-sm" />
<span className="text-foreground">{shortAddr(address)}</span>
Expand Down
1 change: 1 addition & 0 deletions components/ui/Icons.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
"use client";
import React from "react";
import { Sun, Moon, ArrowUpRight, TrendingUp, Wallet, ArrowDownCircle, BarChart3, HelpCircle, LayoutDashboard, Landmark } from "lucide-react";

// logo component
export const Logo = () => (
<div className="relative flex items-center justify-center text-primary">
Expand Down
11 changes: 5 additions & 6 deletions components/ui/Sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,12 @@

import React from "react";
import Link from "next/link";
import { Logo, DashboardIcon, DepositIcon, EarnIcon, StatsIcon } from "./Icons";
import { Logo, DashboardIcon, DepositIcon, EarnIcon } from "./Icons";
import { ThemeToggle } from "./ThemeToggle";
import { useAppKit, useAppKitAccount } from "@reown/appkit/react";
import { LogOut, X } from "lucide-react";

export type Tab = "deposit" | "withdraw" | "rewards" | "stats";
export type Tab = "dashboard" | "portfolio" | "activity";

interface SidebarProps {
tab: Tab;
Expand Down Expand Up @@ -59,10 +59,9 @@ export const Sidebar = ({ tab, setTab, isOpen, onClose }: SidebarProps) => {
</div>

<nav className="flex-1 px-4 space-y-2 mt-4">
{navItem("deposit", "Dashboard", <DashboardIcon />)}
{navItem("withdraw", "Portfolio", <DepositIcon />)}
{navItem("rewards", "Activity", <EarnIcon />)}
{navItem("stats", "Settings", <StatsIcon />)}
{navItem("dashboard", "Dashboard", <DashboardIcon />)}
{navItem("portfolio", "Portfolio", <DepositIcon />)}
{navItem("activity", "Activity", <EarnIcon />)}
</nav>

<div className="p-6 border-t border-border space-y-4">
Expand Down
48 changes: 28 additions & 20 deletions hooks/specific/useRead.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,34 @@ export const useReadVault = () => {
}
}, [address, decodeError, vaultContract]);

const previewWithdraw = useCallback(
async (shares: bigint): Promise<{ payout: bigint; grossAssets: bigint; fee: bigint } | null> => {
if (!vaultContract) {
toast.error("Vault contract not found");
return null;
}
if (!address) {
toast.error("Wallet not connected");
return null;
}
try {
setIsLoading(true);
const result = await vaultContract.previewWithdrawFor(address, shares);
return {
payout: result[0],
grossAssets: result[1],
fee: result[2],
};
} catch (error) {
toast.error(await decodeError(error));
return null;
} finally {
setIsLoading(false);
}
},
[address, decodeError, vaultContract],
);

const getUserBalance = useCallback(async (): Promise<bigint | null> => {
if (!vaultContract) {
toast.error("Vault contract not found");
Expand Down Expand Up @@ -84,26 +112,6 @@ export const useReadVault = () => {
}
}, [address, decodeError, vaultContract]);

const previewWithdraw = useCallback(
async (shares: bigint): Promise<bigint | null> => {
if (!vaultContract) {
toast.error("Vault contract not found");
return null;
}

try {
setIsLoading(true);
return (await vaultContract.previewWithdraw(shares)) as bigint;
} catch (error) {
toast.error(await decodeError(error));
return null;
} finally {
setIsLoading(false);
}
},
[decodeError, vaultContract],
);

const previewDeposit = useCallback(
async (amount: bigint): Promise<bigint | null> => {
if (!vaultContract) {
Expand Down
Loading
Loading