Skip to content

Commit 125235c

Browse files
author
Bingle Kruger
committed
feat(ntc-web): Wallet linking
1 parent feeb7e0 commit 125235c

File tree

8 files changed

+19051
-4460
lines changed

8 files changed

+19051
-4460
lines changed

ntc-web/app/api/wallet/route.ts

+127
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
/**
2+
* Nautilus Trusted Compute
3+
* Copyright (C) 2025 Nautilus
4+
*
5+
* This program is free software: you can redistribute it and/or modify
6+
* it under the terms of the GNU Affero General Public License as published
7+
* by the Free Software Foundation, either version 3 of the License, or
8+
* (at your option) any later version.
9+
*
10+
* This program is distributed in the hope that it will be useful,
11+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
12+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13+
* GNU Affero General Public License for more details.
14+
*
15+
* You should have received a copy of the GNU Affero General Public License
16+
* along with this program. If not, see <https://www.gnu.org/licenses/>.
17+
*/
18+
19+
// app/api/wallet/route.ts
20+
import { NextRequest, NextResponse } from "next/server";
21+
import { currentUser } from "@clerk/nextjs/server";
22+
import { prisma } from "@/lib/prisma";
23+
24+
export async function GET(request: NextRequest) {
25+
try {
26+
const user = await currentUser();
27+
28+
if (!user) {
29+
console.warn("🚨 Unauthorized: No user found in Clerk.");
30+
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
31+
}
32+
33+
console.log(`✅ Fetching wallet for user: ${user.id}`);
34+
35+
const userData = await prisma.user.findUnique({
36+
where: { clerkId: user.id },
37+
select: { walletAddress: true }
38+
});
39+
40+
return NextResponse.json({
41+
walletAddress: userData?.walletAddress ?? null
42+
});
43+
44+
} catch (error) {
45+
console.error("❌ Error fetching wallet:", error);
46+
return NextResponse.json(
47+
{ error: "Internal server error" },
48+
{ status: 500 }
49+
);
50+
} finally {
51+
await prisma.$disconnect();
52+
}
53+
}
54+
55+
export async function POST(request: NextRequest) {
56+
try {
57+
const user = await currentUser();
58+
59+
if (!user) {
60+
console.warn("🚨 Unauthorized: No user found in Clerk.");
61+
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
62+
}
63+
64+
const { walletAddress } = await request.json();
65+
66+
if (!walletAddress) {
67+
console.warn("⚠️ Bad request: Missing wallet address");
68+
return NextResponse.json(
69+
{ error: "Wallet address is required" },
70+
{ status: 400 }
71+
);
72+
}
73+
74+
console.log(`✅ Linking wallet for user: ${user.id}`);
75+
76+
const updatedUser = await prisma.user.update({
77+
where: { clerkId: user.id },
78+
data: { walletAddress },
79+
select: { walletAddress: true }
80+
});
81+
82+
return NextResponse.json({
83+
walletAddress: updatedUser.walletAddress
84+
});
85+
86+
} catch (error) {
87+
console.error("❌ Error linking wallet:", error);
88+
return NextResponse.json(
89+
{ error: "Failed to link wallet" },
90+
{ status: 500 }
91+
);
92+
} finally {
93+
await prisma.$disconnect();
94+
}
95+
}
96+
97+
export async function DELETE(request: NextRequest) {
98+
try {
99+
const user = await currentUser();
100+
101+
if (!user) {
102+
console.warn("🚨 Unauthorized: No user found in Clerk.");
103+
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
104+
}
105+
106+
console.log(`✅ Unlinking wallet for user: ${user.id}`);
107+
108+
const updatedUser = await prisma.user.update({
109+
where: { clerkId: user.id },
110+
data: { walletAddress: null },
111+
select: { walletAddress: true }
112+
});
113+
114+
return NextResponse.json({
115+
walletAddress: updatedUser.walletAddress
116+
});
117+
118+
} catch (error) {
119+
console.error("❌ Error unlinking wallet:", error);
120+
return NextResponse.json(
121+
{ error: "Failed to unlink wallet" },
122+
{ status: 500 }
123+
);
124+
} finally {
125+
await prisma.$disconnect();
126+
}
127+
}

ntc-web/app/layout.tsx

+58-24
Original file line numberDiff line numberDiff line change
@@ -18,38 +18,72 @@
1818

1919
"use client";
2020

21-
import { Inter } from 'next/font/google';
22-
import './globals.css';
23-
import { ClerkProvider, SignedIn, SignedOut, RedirectToSignIn } from '@clerk/nextjs';
24-
import LayoutClient from './LayoutClient';
25-
import { usePathname } from 'next/navigation';
21+
import { useMemo } from "react";
22+
import { ClerkProvider } from "@clerk/nextjs";
23+
import { Inter } from "next/font/google";
24+
import "./globals.css";
2625

27-
const inter = Inter({ subsets: ['latin'] });
26+
// Wallet adapter imports:
27+
import { WalletAdapterNetwork } from "@solana/wallet-adapter-base";
28+
import { clusterApiUrl } from "@solana/web3.js";
29+
import { ConnectionProvider, WalletProvider } from "@solana/wallet-adapter-react";
30+
import { WalletModalProvider } from "@solana/wallet-adapter-react-ui";
31+
import { PhantomWalletAdapter, SolflareWalletAdapter } from "@solana/wallet-adapter-wallets";
2832

29-
export default function RootLayout({ children }: { children: React.ReactNode }) {
30-
const pathname = usePathname() ?? ""; // Ensure pathname is always a string
31-
const isAuthPage = pathname.startsWith('/sign-in') || pathname.startsWith('/sign-up');
33+
// Default styles
34+
import "@solana/wallet-adapter-react-ui/styles.css";
35+
36+
import LayoutClient from "./LayoutClient";
37+
import { SignedIn, SignedOut, RedirectToSignIn } from "@clerk/nextjs";
38+
import { usePathname } from "next/navigation";
39+
40+
const inter = Inter({ subsets: ["latin"] });
41+
42+
export default function RootLayout({
43+
children,
44+
}: {
45+
children: React.ReactNode;
46+
}) {
47+
const network = WalletAdapterNetwork.Devnet;
48+
const endpoint = useMemo(() => clusterApiUrl(network), [network]);
49+
const wallets = useMemo(
50+
() => [
51+
new PhantomWalletAdapter(),
52+
new SolflareWalletAdapter({ network })
53+
],
54+
[network]
55+
);
56+
57+
const pathname = usePathname() ?? "";
58+
const isAuthPage =
59+
pathname.startsWith("/sign-in") || pathname.startsWith("/sign-up");
3260

3361
return (
3462
<ClerkProvider>
3563
<html lang="en" suppressHydrationWarning>
3664
<body className={inter.className} suppressHydrationWarning>
37-
<div id="app-root">
38-
{isAuthPage ? (
39-
children
40-
) : (
41-
<>
42-
<SignedIn>
43-
<LayoutClient>{children}</LayoutClient>
44-
</SignedIn>
45-
<SignedOut>
46-
<RedirectToSignIn />
47-
</SignedOut>
48-
</>
49-
)}
50-
</div>
65+
<ConnectionProvider endpoint={endpoint}>
66+
<WalletProvider wallets={wallets} autoConnect>
67+
<WalletModalProvider>
68+
<div id="app-root">
69+
{isAuthPage ? (
70+
children
71+
) : (
72+
<>
73+
<SignedIn>
74+
<LayoutClient>{children}</LayoutClient>
75+
</SignedIn>
76+
<SignedOut>
77+
<RedirectToSignIn />
78+
</SignedOut>
79+
</>
80+
)}
81+
</div>
82+
</WalletModalProvider>
83+
</WalletProvider>
84+
</ConnectionProvider>
5185
</body>
5286
</html>
5387
</ClerkProvider>
5488
);
55-
}
89+
}

ntc-web/components/TopBar.tsx

+25-19
Original file line numberDiff line numberDiff line change
@@ -16,18 +16,20 @@
1616
* along with this program. If not, see <https://www.gnu.org/licenses/>.
1717
*/
1818

19-
'use client'
19+
// components/TopBar.tsx
20+
"use client";
2021

21-
import Image from 'next/image'
22-
import { PanelLeftClose, PanelLeftOpen } from 'lucide-react'
23-
import {
24-
SignInButton,
25-
SignedIn,
26-
SignedOut,
22+
import Image from "next/image";
23+
import { PanelLeftClose, PanelLeftOpen } from "lucide-react";
24+
import {
25+
SignInButton,
26+
SignedIn,
27+
SignedOut,
2728
UserButton,
2829
ClerkLoading,
29-
ClerkLoaded
30-
} from '@clerk/nextjs'
30+
ClerkLoaded,
31+
} from "@clerk/nextjs";
32+
import WalletConnector from "@/components/WalletConnector";
3133

3234
interface TopBarProps {
3335
isNavOpen: boolean;
@@ -37,11 +39,11 @@ interface TopBarProps {
3739
export default function TopBar({ isNavOpen, toggleNav }: TopBarProps) {
3840
return (
3941
<header className="h-16 bg-white shadow-sm flex">
40-
<div className={`${isNavOpen ? 'w-64' : 'w-16'} transition-all duration-300`}>
42+
<div className={`${isNavOpen ? "w-64" : "w-16"} transition-all duration-300`}>
4143
<div className="h-full p-2 flex items-center">
4244
<div className="w-12 h-10 flex items-center justify-center">
4345
<div className="w-8 h-8 relative">
44-
<Image
46+
<Image
4547
src="/logo.png"
4648
alt="Nautilus Logo"
4749
fill
@@ -50,21 +52,23 @@ export default function TopBar({ isNavOpen, toggleNav }: TopBarProps) {
5052
/>
5153
</div>
5254
</div>
53-
<div className={`flex-1 overflow-hidden transition-all duration-300 ${
54-
isNavOpen ? 'w-40 opacity-100' : 'w-0 opacity-0'
55-
}`}>
55+
<div
56+
className={`flex-1 overflow-hidden transition-all duration-300 ${
57+
isNavOpen ? "w-40 opacity-100" : "w-0 opacity-0"
58+
}`}
59+
>
5660
<span className="text-2xl font-bold text-gray-800 whitespace-nowrap">
5761
NAUTILUS
5862
</span>
5963
</div>
6064
</div>
6165
</div>
6266

63-
<button
67+
<button
6468
onClick={toggleNav}
6569
className="px-3 mx-4 hover:bg-gray-100 transition-colors border rounded-lg flex items-center h-8 self-center"
6670
type="button"
67-
aria-label={isNavOpen ? 'Close sidebar' : 'Open sidebar'}
71+
aria-label={isNavOpen ? "Close sidebar" : "Open sidebar"}
6872
>
6973
{isNavOpen ? (
7074
<PanelLeftClose className="h-5 w-5 text-gray-600" />
@@ -73,7 +77,9 @@ export default function TopBar({ isNavOpen, toggleNav }: TopBarProps) {
7377
)}
7478
</button>
7579

76-
<div className="flex-1 flex items-center justify-end pr-4">
80+
<div className="flex-1 flex items-center justify-end pr-4 space-x-4">
81+
{/* Insert the wallet connector component */}
82+
<WalletConnector />
7783
<ClerkLoading>
7884
<div className="w-24 h-8 bg-gray-100 animate-pulse rounded-lg"></div>
7985
</ClerkLoading>
@@ -86,10 +92,10 @@ export default function TopBar({ isNavOpen, toggleNav }: TopBarProps) {
8692
</SignInButton>
8793
</SignedOut>
8894
<SignedIn>
89-
<UserButton afterSignOutUrl="/"/>
95+
<UserButton afterSignOutUrl="/" />
9096
</SignedIn>
9197
</ClerkLoaded>
9298
</div>
9399
</header>
94-
)
100+
);
95101
}

0 commit comments

Comments
 (0)