Skip to content

Commit 5d7dde4

Browse files
authored
Merge branch 'dev' into feat/interest-auctions
2 parents 5e34926 + 6ad7b46 commit 5d7dde4

26 files changed

Lines changed: 3088 additions & 259 deletions

apps/web-app/.env.example

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,3 +11,7 @@ NEXT_PUBLIC_SOROSWAP_API_KEY=
1111
# The faucet contract is the admin of all RWA tokens and handles set_authorized + mint.
1212
# Users call bulk_mint directly via Freighter (no server-side key needed).
1313
NEXT_PUBLIC_FAUCET_CONTRACT_ID=
14+
15+
# Lending admin address (Stellar public key). Used to guard /dashboard/admin.
16+
# Only this wallet can access the admin UI. Contract still enforces admin auth on-chain.
17+
NEXT_PUBLIC_LENDING_ADMIN_ADDRESS=
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import type { Metadata } from "next";
2+
import AdminPageClient from "@/features/admin/components/AdminPageClient";
3+
4+
export const metadata: Metadata = {
5+
title: "Admin | Neko Protocol",
6+
description:
7+
"Lending pool administration: pool state, treasury fees, collateral factors, interest rates.",
8+
};
9+
10+
export default function AdminPage() {
11+
return <AdminPageClient />;
12+
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import type { Metadata } from "next";
2+
import { BannerPage } from "@/components/ui/BannerPage";
3+
import { PageContainer } from "@/components/ui/PageContainer";
4+
import LiquidationsSection from "@/features/dashboard/components/ui/LiquidationsSection";
5+
6+
export const metadata: Metadata = {
7+
title: "Liquidations | Neko Protocol",
8+
description:
9+
"Create and participate in bad debt auctions. Cover uncovered debt and receive backstop tokens at a discount.",
10+
};
11+
12+
export default function LiquidationsPage() {
13+
return (
14+
<div className="w-full min-w-0 max-w-full min-h-screen overflow-x-hidden">
15+
<PageContainer maxWidth="7xl" className="space-y-8 sm:space-y-10">
16+
<BannerPage
17+
title="Liquidations"
18+
subtitle="Create and fill bad debt auctions. When borrowers have debt but no collateral, participate to cover debt and earn backstop tokens at a discount."
19+
badge="Bad Debt Auctions"
20+
imageSrc="/banners/oracle.svg"
21+
imageAlt="Liquidations illustration"
22+
/>
23+
<LiquidationsSection />
24+
</PageContainer>
25+
</div>
26+
);
27+
}

apps/web-app/src/components/navigation/MobileHeader.tsx

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
"use client";
22

3-
import React, { useState, useEffect, useRef } from "react";
3+
import React, { useState, useEffect, useRef, useMemo } from "react";
44
import Link from "next/link";
55
import Image from "next/image";
66
import { usePathname } from "next/navigation";
@@ -15,6 +15,7 @@ const NETWORK = (
1515
process.env.NEXT_PUBLIC_STELLAR_NETWORK ?? "TESTNET"
1616
).toUpperCase();
1717
const IS_TESTNET = NETWORK === "TESTNET";
18+
const ADMIN_ADDRESS = process.env.NEXT_PUBLIC_LENDING_ADMIN_ADDRESS ?? "";
1819

1920
export function MobileHeader() {
2021
const [isOpen, setIsOpen] = useState(false);
@@ -28,6 +29,17 @@ export function MobileHeader() {
2829
const isConnected = isStellarConnected;
2930
const activeAddress = stellarAddress ?? "";
3031

32+
const navItems = useMemo(() => {
33+
return NAV_ITEMS.filter((item) => {
34+
const adminOnly = "adminOnly" in item && item.adminOnly;
35+
// Admin link: ONLY show when admin is configured AND connected wallet is admin
36+
if (adminOnly) {
37+
return Boolean(ADMIN_ADDRESS && activeAddress === ADMIN_ADDRESS);
38+
}
39+
return true;
40+
});
41+
}, [activeAddress]);
42+
3143
const isActive = (href: string) =>
3244
href === "/dashboard"
3345
? pathname === "/dashboard"
@@ -172,7 +184,7 @@ export function MobileHeader() {
172184
)}
173185
>
174186
<div className="flex flex-col gap-2 px-4 py-6 sm:px-6">
175-
{NAV_ITEMS.map(({ label, href, icon }) => {
187+
{navItems.map(({ label, href, icon }) => {
176188
const Icon = icon;
177189
const active = isActive(href);
178190
return (

apps/web-app/src/components/navigation/Sidebar.tsx

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
"use client";
22

3-
import React from "react";
3+
import React, { useMemo } from "react";
44
import { usePathname } from "next/navigation";
55
import { useWalletType } from "@/hooks/useWalletType";
66
import { useStellarWallet } from "@/hooks/useStellarWallet";
@@ -12,6 +12,8 @@ import { SetupCard } from "./SetupCard";
1212

1313
export const SIDEBAR_WIDTH = "270px";
1414

15+
const ADMIN_ADDRESS = process.env.NEXT_PUBLIC_LENDING_ADMIN_ADDRESS ?? "";
16+
1517
export function Sidebar() {
1618
const pathname = usePathname();
1719
const { isStellarConnected, stellarAddress } = useWalletType();
@@ -20,6 +22,17 @@ export function Sidebar() {
2022
const isConnected = isStellarConnected;
2123
const activeAddress = stellarAddress ?? "";
2224

25+
const navItems = useMemo(() => {
26+
return NAV_ITEMS.filter((item) => {
27+
const adminOnly = "adminOnly" in item && item.adminOnly;
28+
// Admin link: ONLY show when admin is configured AND connected wallet is admin
29+
if (adminOnly) {
30+
return Boolean(ADMIN_ADDRESS && activeAddress === ADMIN_ADDRESS);
31+
}
32+
return true;
33+
});
34+
}, [activeAddress]);
35+
2336
const handleDisconnect = () => {
2437
void disconnectStellar();
2538
};
@@ -34,7 +47,7 @@ export function Sidebar() {
3447
<SidebarLogo />
3548

3649
<nav className="flex flex-1 flex-col gap-1 px-3">
37-
{NAV_ITEMS.map(({ label, href, icon }) => (
50+
{navItems.map(({ label, href, icon }) => (
3851
<NavItem
3952
key={href}
4053
label={label}

apps/web-app/src/components/navigation/sidebarConfig.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import {
55
TrendingUp,
66
BarChart2,
77
Settings,
8+
Shield,
89
} from "lucide-react";
910

1011
export const NAV_ITEMS = [
@@ -13,6 +14,7 @@ export const NAV_ITEMS = [
1314
{ label: "Borrow", href: "/borrowing", icon: Landmark },
1415
{ label: "Lend", href: "/lending", icon: TrendingUp },
1516
{ label: "Discover", href: "/discover", icon: BarChart2 },
17+
{ label: "Admin", href: "/dashboard/admin", icon: Shield, adminOnly: true },
1618
{ label: "Settings", href: "/settings", icon: Settings },
1719
] as const;
1820

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
"use client";
2+
3+
import React, { useEffect } from "react";
4+
import { useRouter } from "next/navigation";
5+
import { useWallet } from "@/hooks/useWallet";
6+
import { PageContainer } from "@/components/ui/PageContainer";
7+
import { BannerPage } from "@/components/ui/BannerPage";
8+
import { ConnectWalletModal } from "@/features/wallet/components/ConnectWalletModal";
9+
import PoolStateToggle from "./PoolStateToggle";
10+
import TreasuryFeesTable from "./TreasuryFeesTable";
11+
import CollateralFactorForm from "./CollateralFactorForm";
12+
import InterestRateParamsForm from "./InterestRateParamsForm";
13+
14+
const ADMIN_ADDRESS = process.env.NEXT_PUBLIC_LENDING_ADMIN_ADDRESS ?? "";
15+
16+
function ConnectWalletPrompt() {
17+
const [showModal, setShowModal] = React.useState(false);
18+
return (
19+
<div className="flex flex-col items-center justify-center min-h-[400px] gap-4">
20+
<p className="text-white/70 text-center">
21+
Connect your wallet to access the admin panel.
22+
</p>
23+
<button
24+
onClick={() => setShowModal(true)}
25+
className="px-6 py-2 rounded-xl bg-[#229EDF] hover:bg-[#1e8bc9] text-white font-medium transition-colors"
26+
>
27+
Connect Wallet
28+
</button>
29+
<ConnectWalletModal
30+
isOpen={showModal}
31+
onClose={() => setShowModal(false)}
32+
/>
33+
</div>
34+
);
35+
}
36+
37+
export default function AdminPageClient() {
38+
const { address } = useWallet();
39+
const router = useRouter();
40+
41+
// Page NOT exposed: redirect immediately if admin not configured or user is not admin
42+
useEffect(() => {
43+
if (!ADMIN_ADDRESS) {
44+
router.replace("/dashboard");
45+
return;
46+
}
47+
if (address && address !== ADMIN_ADDRESS) {
48+
router.replace("/dashboard");
49+
}
50+
}, [address, router]);
51+
52+
if (!ADMIN_ADDRESS) return null;
53+
if (address && address !== ADMIN_ADDRESS) return null;
54+
55+
// Only show Connect prompt when admin not yet connected (so they can connect)
56+
if (!address) {
57+
return (
58+
<div className="w-full min-w-0 max-w-full min-h-screen overflow-x-hidden">
59+
<PageContainer maxWidth="7xl" className="space-y-8 sm:space-y-10">
60+
<BannerPage
61+
title="Admin"
62+
subtitle="Lending pool administration"
63+
badge="Restricted"
64+
/>
65+
<ConnectWalletPrompt />
66+
</PageContainer>
67+
</div>
68+
);
69+
}
70+
71+
// Admin is connected: show full admin UI
72+
return (
73+
<div className="w-full min-w-0 max-w-full min-h-screen overflow-x-hidden">
74+
<PageContainer maxWidth="7xl" className="space-y-8 sm:space-y-10">
75+
<BannerPage
76+
title="Admin"
77+
subtitle="Pool state, treasury fees, collateral factors, interest rates"
78+
badge="Admin"
79+
/>
80+
<div className="space-y-10">
81+
<PoolStateToggle />
82+
<TreasuryFeesTable />
83+
<CollateralFactorForm />
84+
<InterestRateParamsForm />
85+
</div>
86+
</PageContainer>
87+
</div>
88+
);
89+
}

0 commit comments

Comments
 (0)