diff --git a/.env.example b/.env.example index b1086fc..abab8e6 100644 --- a/.env.example +++ b/.env.example @@ -1,31 +1,31 @@ -# OpenAI Configuration +# Get your OpenAI API Key here: https://platform.openai.com/account/api-keys OPENAI_API_KEY=your_openai_api_key_here -NEXT_PUBLIC_OPENAI_API_KEY=your_openai_api_key_here -# Supabase Configuration NEXT_PUBLIC_SUPABASE_URL=your_supabase_url_here NEXT_PUBLIC_SUPABASE_ANON_KEY=your_supabase_anon_key_here -SUPABASE_SERVICE_ROLE_KEY=your_service_role_key_here -# Vercel Blob Storage BLOB_READ_WRITE_TOKEN=your_blob_token_here -# Coinbase Developer Platform +# See https://www.coinbase.com/developer-platform/products/base-node NEXT_PUBLIC_CDP_API_KEY=your_cdp_api_key_here -NEXT_PUBLIC_CDP_API_KEY_NAME=your_cdp_key_name_here -NEXT_PUBLIC_CDP_API_KEY_PRIVATE_KEY=your_cdp_private_key_here + +NEXT_PUBLIC_OPENAI_API_KEY=your_openai_api_key_here # Environment NEXT_PUBLIC_ENVIRONMENT=localhost NEXT_PUBLIC_SITE_URL=http://localhost:3000 -# WalletConnect +# See https://cloud.walletconnect.com/ NEXT_PUBLIC_WALLET_CONNECT_PROJECT_ID=your_wallet_connect_project_id_here +# CDP Configuration +NEXT_PUBLIC_CDP_API_KEY_NAME=your_cdp_api_key_name_here +NEXT_PUBLIC_CDP_API_KEY_PRIVATE_KEY=your_cdp_private_key_here + # Vercel KV (Redis) Configuration KV_URL=your_kv_url_here KV_REST_API_URL=your_kv_rest_api_url_here -KV_REST_API_TOKEN=your_kv_api_token_here +KV_REST_API_TOKEN=your_kv_token_here KV_REST_API_READ_ONLY_TOKEN=your_kv_readonly_token_here # Base Network Configuration @@ -47,7 +47,20 @@ DEBANK_API_KEY=your_debank_api_key_here NEXT_PUBLIC_ALCHEMY_API_KEY=your_alchemy_api_key_here BING_API_KEY=your_bing_api_key_here DUCKDUCKGO_API_KEY=your_duckduckgo_api_key_here +SERP_API_KEY=your_serp_api_key_here + +# Supabase Service Role +SUPABASE_SERVICE_ROLE_KEY=your_supabase_service_role_key_here # Feature Flags NEXT_PUBLIC_FEATURE_ATTACHMENTS=true -NEXT_PUBLIC_FEATURE_WEB_SEARCH=true \ No newline at end of file +NEXT_PUBLIC_FEATURE_WEB_SEARCH=true + +NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=your_clerk_publishable_key_here +CLERK_SECRET_KEY=your_clerk_secret_key_here +CLERK_JWT_TEMPLATE=supabase + +//ad the others + +JWKS_URL=https://cute-stallion-6.clerk.accounts.dev/.well-known/jwks.json +JWT_PUBLIC_KEY=your_jwt_public_key_here \ No newline at end of file diff --git a/api/websocket.ts b/api/websocket.ts new file mode 100644 index 0000000..67d067e --- /dev/null +++ b/api/websocket.ts @@ -0,0 +1,11 @@ +export const createWebSocket = (url: string) => { + const ws = new WebSocket(url); + + const send = (data: any) => { + if (ws.readyState === WebSocket.OPEN) { + ws.send(JSON.stringify(data)); + } + }; + + return { ws, send }; +}; diff --git a/app/(auth)/auth/callback/route.ts b/app/(auth)/auth/callback/route.ts new file mode 100644 index 0000000..c748338 --- /dev/null +++ b/app/(auth)/auth/callback/route.ts @@ -0,0 +1,15 @@ +import { createRouteHandlerClient } from "@supabase/auth-helpers-nextjs"; +import { cookies } from "next/headers"; +import { NextResponse } from "next/server"; + +export async function GET(request: Request) { + const requestUrl = new URL(request.url); + const code = requestUrl.searchParams.get("code"); + + if (code) { + const supabase = createRouteHandlerClient({ cookies }); + await supabase.auth.exchangeCodeForSession(code); + } + + return NextResponse.redirect(new URL("/", requestUrl.origin)); +} \ No newline at end of file diff --git a/app/(auth)/layout.tsx b/app/(auth)/layout.tsx index 1fdaeeb..a5e1e22 100644 --- a/app/(auth)/layout.tsx +++ b/app/(auth)/layout.tsx @@ -1,34 +1,35 @@ -import { Metadata } from "next"; +"use client"; -export const metadata: Metadata = { - title: "Chainable", - description: "Secure blockchain integration with AI", - icons: { - icon: [ - { url: "/icons/chainable.svg", type: "image/svg+xml", sizes: "any" }, - { url: "/icons/chainable-32.png", sizes: "32x32", type: "image/png" }, - { url: "/icons/chainable-16.png", sizes: "16x16", type: "image/png" }, - ], - apple: [{ url: "/icons/chainable-180.png", sizes: "180x180" }], - shortcut: "/icons/favicon.ico", - }, -}; +import { ClerkProvider } from "@clerk/nextjs"; +import { dark } from "@clerk/themes"; +import { useTheme } from "next-themes"; export default function AuthLayout({ children, }: { children: React.ReactNode; }) { + const { theme } = useTheme(); + return ( - - - - - - - + +
{children} - - +
+
); } diff --git a/app/(auth)/login/page.tsx b/app/(auth)/login/page.tsx index 8722d7d..d551d1f 100644 --- a/app/(auth)/login/page.tsx +++ b/app/(auth)/login/page.tsx @@ -1,100 +1,14 @@ "use client"; -import Link from "next/link"; -import { useRouter } from "next/navigation"; -import { useState } from "react"; -import { toast } from "sonner"; -import { Loader2 } from "lucide-react"; - -import { Button } from "@/components/ui/button"; -import { Input } from "@/components/ui/input"; -import { Label } from "@/components/ui/label"; -import { signIn } from "@/db/auth"; +import { SignIn } from "@clerk/nextjs"; export default function LoginPage() { - const [isLoading, setIsLoading] = useState(false); - const [isTransitioning, setIsTransitioning] = useState(false); - const router = useRouter(); - - async function handleSubmit(event: React.FormEvent) { - event.preventDefault(); - setIsLoading(true); - - try { - const formData = new FormData(event.currentTarget); - const email = formData.get("email") as string; - const password = formData.get("password") as string; - - await signIn(email, password); - setIsTransitioning(true); - router.push("/"); - router.refresh(); - } catch (error: any) { - toast.error(error.message); - setIsLoading(false); - } - } - - if (isTransitioning) { - return ( -
-
- -

Redirecting...

-
-
- ); - } - - return ( -
-
-
-

Login

-

- Enter your email below to login to your account -

-
-
-
- - -
-
- - -
- -
-
- Don't have an account?{" "} - - Register - -
-
-
- ); + return ( + + ); } diff --git a/app/(auth)/metadata.ts b/app/(auth)/metadata.ts new file mode 100644 index 0000000..15d5837 --- /dev/null +++ b/app/(auth)/metadata.ts @@ -0,0 +1,15 @@ +import { Metadata } from "next"; + +export const metadata: Metadata = { + title: "Chainable", + description: "Secure blockchain integration with AI", + icons: { + icon: [ + { url: "/icons/chainable.svg", type: "image/svg+xml", sizes: "any" }, + { url: "/icons/chainable-32.png", sizes: "32x32", type: "image/png" }, + { url: "/icons/chainable-16.png", sizes: "16x16", type: "image/png" }, + ], + apple: [{ url: "/icons/chainable-180.png", sizes: "180x180" }], + shortcut: "/icons/favicon.ico", + }, +}; \ No newline at end of file diff --git a/app/(auth)/register/page.tsx b/app/(auth)/register/page.tsx index d730d7f..cc27aa0 100644 --- a/app/(auth)/register/page.tsx +++ b/app/(auth)/register/page.tsx @@ -1,73 +1,14 @@ "use client"; -import Link from "next/link"; -import { useRouter } from "next/navigation"; -import { useState } from "react"; -import { toast } from "sonner"; - -import { Button } from "@/components/ui/button"; -import { Input } from "@/components/ui/input"; -import { Label } from "@/components/ui/label"; -import { signUp } from "@/db/auth"; +import { SignUp } from "@clerk/nextjs"; export default function RegisterPage() { - const [isLoading, setIsLoading] = useState(false); - const router = useRouter(); - - async function handleSubmit(event: React.FormEvent) { - event.preventDefault(); - setIsLoading(true); - - try { - const formData = new FormData(event.currentTarget); - const email = formData.get("email") as string; - const password = formData.get("password") as string; - - await signUp(email, password); - toast.success("Check your email to confirm your account"); - router.push("/login"); - } catch (error: any) { - toast.error(error.message); - } finally { - setIsLoading(false); - } - } - - return ( -
-
-
-

Register

-

- Enter your information to create an account -

-
-
-
- - -
-
- - -
- -
-
- Already have an account?{" "} - - Login - -
-
-
- ); + return ( + + ); } diff --git a/app/(auth)/sign-in/page.tsx b/app/(auth)/sign-in/page.tsx new file mode 100644 index 0000000..0519ecb --- /dev/null +++ b/app/(auth)/sign-in/page.tsx @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/app/(chat)/api/chat/route.ts b/app/(chat)/api/chat/route.ts index d3f31aa..e3cd056 100644 --- a/app/(chat)/api/chat/route.ts +++ b/app/(chat)/api/chat/route.ts @@ -34,9 +34,10 @@ import { getServerWalletState } from "@/hooks/useServerWalletState"; import { kv } from "@vercel/kv"; import { useAccount, useBalance, useChainId } from "wagmi"; +import { auth } from "@clerk/nextjs"; +import { createMiddlewareClient } from '@supabase/auth-helpers-nextjs'; import { generateTitleFromUserMessage } from "../../actions"; - export const maxDuration = 60; interface WeatherParams { @@ -87,21 +88,23 @@ const allTools: AllowedTools[] = [ ...weatherTools, "getWalletBalance" as AllowedTools, "checkWalletState" as AllowedTools, - ...(FEATURES.WEB_SEARCH ? ["webSearch" as AllowedTools] : []), + "webSearch" as AllowedTools, ]; async function getUser() { + const { userId } = auth(); + if (!userId) { + throw new Error("Unauthorized"); + } + const supabase = await createClient(); - const { - data: { user }, - error, - } = await supabase.auth.getUser(); + const { data: user, error } = await supabase.auth.getUser(); if (error || !user) { throw new Error("Unauthorized"); } - return user; + return { clerkId: userId, ...user }; } // Add helper function to format message content for database storage @@ -545,54 +548,49 @@ const tools = { } }, }, - ...(FEATURES.WEB_SEARCH - ? { - webSearch: { - description: "Search the web using DuckDuckGo", - parameters: z.object({ - query: z.string().describe("The search query"), - searchType: z - .enum(["duckduckgo", "opensearch"]) - .describe("The search engine to use"), - }), - execute: async ({ + webSearch: { + description: "Search the web using DuckDuckGo", + parameters: z.object({ + query: z.string().describe("The search query"), + searchType: z + .enum(["duckduckgo", "opensearch"]) + .describe("The search engine to use"), + }), + execute: async ({ + query, + searchType, + }: { + query: string; + searchType: "duckduckgo" | "opensearch"; + }) => { + try { + let results; + if (searchType === "duckduckgo") { + results = await searchDuckDuckGo(query); + } else { + results = await searchOpenSearch(query); + } + + return { + type: "tool-result", + result: { + searchEngine: searchType, query, - searchType, - }: { - query: string; - searchType: "duckduckgo" | "opensearch"; - }) => { - try { - let results; - if (searchType === "duckduckgo") { - results = await searchDuckDuckGo(query); - } else { - results = await searchOpenSearch(query); - } - - return { - type: "tool-result", - result: { - searchEngine: searchType, - query, - results, - timestamp: new Date().toISOString(), - }, - }; - } catch (error) { - return { - type: "tool-result", - result: { - error: "Search failed", - details: - error instanceof Error ? error.message : "Unknown error", - }, - }; - } + results, + timestamp: new Date().toISOString(), }, - }, + }; + } catch (error) { + return { + type: "tool-result", + result: { + error: "Search failed", + details: error instanceof Error ? error.message : "Unknown error", + }, + }; } - : {}), + }, + }, }; export async function POST(request: Request) { @@ -749,11 +747,12 @@ export async function POST(request: Request) { }), }, }, - onFinish: async ({ responseMessages }) => { + onFinish: async (event) => { + const { responseMessages } = event; if (user && user.id) { try { const responseMessagesWithoutIncompleteToolCalls = - sanitizeResponseMessages(responseMessages); + sanitizeResponseMessages(responseMessages as (CoreAssistantMessage | CoreToolMessage)[]); await saveMessages({ chatId: id, diff --git a/app/(chat)/api/files/upload/route.ts b/app/(chat)/api/files/upload/route.ts index 54a1ce5..63a27b0 100644 --- a/app/(chat)/api/files/upload/route.ts +++ b/app/(chat)/api/files/upload/route.ts @@ -3,6 +3,7 @@ import { NextResponse } from "next/server"; import { createClient } from "@/lib/supabase/server"; import { generateUUID } from "@/lib/utils"; +import { getServerDatabase } from '@/lib/db/server'; export async function POST(req: Request): Promise { try { diff --git a/app/(chat)/api/vote/route.ts b/app/(chat)/api/vote/route.ts index ec0efa1..59b6c12 100644 --- a/app/(chat)/api/vote/route.ts +++ b/app/(chat)/api/vote/route.ts @@ -1,11 +1,13 @@ import { getSession } from "@/db/cached-queries"; + import { voteMessage } from "@/db/mutations"; -import { createClient } from "@/lib/supabase/server"; +import { createServerClient } from "@/lib/supabase/server"; + export async function POST(request: Request) { try { const { chatId, messageId, type } = await request.json(); - const supabase = await createClient(); + const supabase = await createServerClient(); const { data: { user }, } = await supabase.auth.getUser(); @@ -32,7 +34,7 @@ export async function GET(request: Request) { } try { - const supabase = await createClient(); + const supabase =await createServerClient(); const { data: votes } = await supabase .from("votes") .select() diff --git a/app/(chat)/layout.tsx b/app/(chat)/layout.tsx index ba0a5a3..664e6fa 100644 --- a/app/(chat)/layout.tsx +++ b/app/(chat)/layout.tsx @@ -1,23 +1,58 @@ -import { cookies } from "next/headers"; +import { currentUser } from "@clerk/nextjs/server"; +import { createServerComponentClient } from '@supabase/auth-helpers-nextjs'; +import { cookies } from 'next/headers'; +import { redirect } from 'next/navigation'; +import { Sidebar } from "@/components/ui/sidebar"; +import { Database } from '@/lib/supabase/types'; +import { cache } from 'react'; -import { AppSidebar } from "@/components/custom/app-sidebar"; -import { SidebarInset, SidebarProvider } from "@/components/ui/sidebar"; -import { getSession } from "@/db/cached-queries"; +// Create cached Supabase client to avoid type issues +const createClient = cache(() => { + const cookieStore = cookies(); + return createServerComponentClient({ + cookies: () => cookieStore, + }); +}); -export default async function Layout({ +export default async function ChatLayout({ children, }: { children: React.ReactNode; }) { - const cookieStore = await cookies(); - const isCollapsed = cookieStore.get("sidebar:state")?.value !== "true"; + try { + // Get Clerk user + const user = await currentUser(); + if (!user) { + redirect('/login'); + } - const user = await getSession(); + // Get Supabase client + const supabase = createClient(); - return ( - - - {children} - - ); + // Get user settings with fallback + const { data: settings } = await supabase + .from('user_settings') + .select('sidebar_collapsed') + .single(); + + return ( +
+ +
+ {children} +
+
+ ); + } catch (error) { + console.error('Layout error:', error); + // Fallback layout without settings + return ( +
+ +
+ {children} +
+
+ ); + } } diff --git a/app/(chat)/loading.tsx b/app/(chat)/loading.tsx new file mode 100644 index 0000000..efbfb82 --- /dev/null +++ b/app/(chat)/loading.tsx @@ -0,0 +1,5 @@ +import { SkeletonLayout } from "@/components/ui/skeleton-layout"; + +export default function Loading() { + return ; +} \ No newline at end of file diff --git a/app/api/debank/route.ts b/app/api/debank/route.ts new file mode 100644 index 0000000..148cf3b --- /dev/null +++ b/app/api/debank/route.ts @@ -0,0 +1,40 @@ +import { NextRequest, NextResponse } from 'next/server' + +const DEBANK_API_KEY = process.env.DEBANK_API_KEY +const DEBANK_API_URL = 'https://pro-openapi.debank.com/v1' + +export async function GET(req: NextRequest) { + try { + const { searchParams } = new URL(req.url) + const address = searchParams.get('address') + const type = searchParams.get('type') || 'balances' + + if (!address) { + return NextResponse.json({ error: 'Address is required' }, { status: 400 }) + } + + const endpoint = type === 'tokens' ? '/token_list' : '/user/total_balance' + const response = await fetch(`${DEBANK_API_URL}${endpoint}?address=${address}`, { + headers: { + 'AccessKey': DEBANK_API_KEY!, + 'accept': 'application/json', + }, + }) + + if (!response.ok) { + throw new Error('Failed to fetch from Debank API') + } + + const data = await response.json() + return NextResponse.json({ + [type]: data, + timestamp: Date.now() + }) + } catch (error) { + console.error('Debank API error:', error) + return NextResponse.json( + { error: 'Failed to fetch data from Debank' }, + { status: 500 } + ) + } +} \ No newline at end of file diff --git a/app/api/search/route.ts b/app/api/search/route.ts new file mode 100644 index 0000000..860c2b2 --- /dev/null +++ b/app/api/search/route.ts @@ -0,0 +1,151 @@ +import { NextResponse } from "next/server"; +import { rateLimit } from "@/lib/rate-limit"; + +// Initialize rate limiter +const limiter = rateLimit({ + interval: 60 * 1000, // 60 seconds + uniqueTokenPerInterval: 500, +}); + +// Define search providers +const searchProviders = { + async duckduckgo(query: string) { + const response = await fetch( + `https://api.duckduckgo.com/?q=${encodeURIComponent(query)}&format=json&no_html=1&skip_disambig=1`, + { + headers: { + 'Accept': 'application/json', + 'User-Agent': 'Chainable Search Bot/1.0', + }, + next: { revalidate: 3600 } + } + ); + + if (!response.ok) throw new Error('DuckDuckGo search failed'); + + const data = await response.json(); + return { + results: [ + ...(data.Abstract ? [{ + Text: data.Abstract, + FirstURL: data.AbstractURL, + Source: data.AbstractSource, + isAbstract: true, + }] : []), + ...(data.RelatedTopics?.map((topic: any) => ({ + Text: topic.Text, + FirstURL: topic.FirstURL, + Icon: topic.Icon?.URL, + })) || []) + ].filter((result: any) => result.Text && result.Text.trim()), + source: 'DuckDuckGo' + }; + }, + + async serp(query: string) { + const SERP_API_KEY = process.env.SERP_API_KEY; + if (!SERP_API_KEY) throw new Error('SERP API key not configured'); + + const response = await fetch( + `https://serpapi.com/search.json?q=${encodeURIComponent(query)}&api_key=${SERP_API_KEY}`, + { next: { revalidate: 3600 } } + ); + + if (!response.ok) throw new Error('SERP search failed'); + + const data = await response.json(); + return { + results: data.organic_results?.map((result: any) => ({ + Text: result.snippet, + FirstURL: result.link, + Title: result.title, + })) || [], + source: 'SERP' + }; + } +} as const; + +export async function GET(request: Request) { + try { + await limiter.check(request, 10); + + const url = new URL(request.url); + const query = url.searchParams.get("query"); + + if (!query) { + return NextResponse.json({ error: "Query is required" }, { status: 400 }); + } + + // Try each search provider in sequence until one works + let searchError = null; + let results = null; + let source = ''; + + for (const [providerName, provider] of Object.entries(searchProviders)) { + try { + const searchResult = await provider(query); + if (searchResult.results && searchResult.results.length > 0) { + results = searchResult.results; + source = searchResult.source; + break; + } + } catch (error) { + console.error(`${providerName} search error:`, error); + searchError = error instanceof Error ? error : new Error('Unknown error'); + } + } + + // If no results found from any provider + if (!results || results.length === 0) { + // Provide a fallback response with blockchain-specific information + if (query.toLowerCase().includes('blockchain') || query.toLowerCase().includes('crypto')) { + return NextResponse.json({ + results: [{ + Text: "No recent results found. Here are some reliable blockchain resources:\n" + + "- CoinDesk (https://www.coindesk.com)\n" + + "- Cointelegraph (https://cointelegraph.com)\n" + + "- The Block (https://www.theblock.co)\n" + + "- Base Network News (https://base.mirror.xyz)", + FirstURL: "https://base.org", + isAbstract: true + }], + query, + source: 'Fallback', + timestamp: new Date().toISOString(), + }); + } + + return NextResponse.json({ + results: [], + query, + error: "No results found", + message: searchError?.message || "No matching results found", + timestamp: new Date().toISOString(), + }); + } + + return NextResponse.json({ + results: results.slice(0, 5), + query, + source, + timestamp: new Date().toISOString(), + }, { + headers: { + 'Cache-Control': 'public, s-maxage=3600, stale-while-revalidate=86400', + }, + }); + + } catch (error) { + console.error("Search error:", error); + const url = new URL(request.url); + return NextResponse.json( + { + error: "Failed to perform search", + message: error instanceof Error ? error.message : "Unknown error", + query: url.searchParams.get("query"), + timestamp: new Date().toISOString(), + }, + { status: 500 } + ); + } +} \ No newline at end of file diff --git a/app/api/wallet/balance/route.ts b/app/api/wallet/balance/route.ts new file mode 100644 index 0000000..7816ed7 --- /dev/null +++ b/app/api/wallet/balance/route.ts @@ -0,0 +1,57 @@ +import { NextResponse } from "next/server"; +import { createPublicClient, http, formatEther } from "viem"; +import { baseSepolia, base } from "viem/chains"; + +// Initialize public clients for different networks +const clients = { + "base-sepolia": createPublicClient({ + chain: baseSepolia, + transport: http(), + }), + "base": createPublicClient({ + chain: base, + transport: http(), + }), +}; + +export async function GET(request: Request) { + try { + const { searchParams } = new URL(request.url); + const address = searchParams.get("address"); + const network = searchParams.get("network") || "base-sepolia"; + + if (!address) { + return NextResponse.json( + { error: "Wallet address is required" }, + { status: 400 } + ); + } + + const client = clients[network as keyof typeof clients]; + if (!client) { + return NextResponse.json( + { error: "Invalid network specified" }, + { status: 400 } + ); + } + + // Get ETH balance + const balance = await client.getBalance({ address: address as `0x${string}` }); + + // Format the balance + const formattedBalance = formatEther(balance); + + return NextResponse.json({ + address, + network, + balance: formattedBalance, + timestamp: new Date().toISOString(), + }); + } catch (error) { + console.error("Balance fetch error:", error); + return NextResponse.json( + { error: "Failed to fetch wallet balance" }, + { status: 500 } + ); + } +} \ No newline at end of file diff --git a/app/api/wallet/debank/route.ts b/app/api/wallet/debank/route.ts new file mode 100644 index 0000000..7f41d9b --- /dev/null +++ b/app/api/wallet/debank/route.ts @@ -0,0 +1,74 @@ +import { NextResponse } from "next/server"; + +const DEBANK_API_KEY = process.env.DEBANK_API_KEY; +const DEBANK_BASE_URL = "https://pro-openapi.debank.com/v1"; + +// Helper function to fetch from DeBankAPI +async function fetchFromDeBank(endpoint: string, address: string) { + try { + const response = await fetch(`${DEBANK_BASE_URL}${endpoint}${address}`, { + headers: { + 'Accept': 'application/json', + 'AccessKey': DEBANK_API_KEY || '', + }, + }); + + if (!response.ok) { + throw new Error(`DeBankAPI error: ${response.statusText}`); + } + + return await response.json(); + } catch (error) { + console.error('DeBankAPI fetch error:', error); + throw error; + } +} + +export async function GET(request: Request) { + try { + const { searchParams } = new URL(request.url); + const address = searchParams.get("address"); + + if (!address) { + return NextResponse.json( + { error: "Wallet address is required" }, + { status: 400 } + ); + } + + if (!DEBANK_API_KEY) { + return NextResponse.json( + { error: "DeBankAPI key not configured" }, + { status: 500 } + ); + } + + // Fetch basic wallet info + const [totalBalance, tokens] = await Promise.all([ + fetchFromDeBank('/user/total_balance?id=', address), + fetchFromDeBank('/user/token_list?id=', address), + ]); + + return NextResponse.json({ + address, + totalBalance: { + total_usd_value: totalBalance?.total_usd_value || 0, + }, + tokens: tokens?.map((token: any) => ({ + chain: token.chain, + symbol: token.symbol, + balance: token.balance, + price: token.price, + usd_value: token.amount * token.price, + })) || [], + timestamp: new Date().toISOString(), + }); + + } catch (error) { + console.error("DeBankAPI error:", error); + return NextResponse.json( + { error: "Failed to fetch wallet data from DeBankAPI" }, + { status: 500 } + ); + } +} \ No newline at end of file diff --git a/app/api/wallet/info/route.ts b/app/api/wallet/info/route.ts new file mode 100644 index 0000000..b1052ae --- /dev/null +++ b/app/api/wallet/info/route.ts @@ -0,0 +1,107 @@ +import { NextResponse } from "next/server"; +import { createPublicClient, http, formatEther } from "viem"; +import { baseSepolia, base } from "viem/chains"; +import { rateLimit } from "@/lib/rate-limit"; + +const limiter = rateLimit({ + interval: 60 * 1000, + uniqueTokenPerInterval: 500, +}); + +// Initialize public clients for different networks +const clients = { + "base-sepolia": createPublicClient({ + chain: baseSepolia, + transport: http(), + }), + "base": createPublicClient({ + chain: base, + transport: http(), + }), +}; + +async function fetchTokenBalances(address: string) { + const DEBANK_API_KEY = process.env.DEBANK_API_KEY; + const DEBANK_BASE_URL = "https://pro-openapi.debank.com/v1"; + + try { + const response = await fetch( + `${DEBANK_BASE_URL}/user/token_list?id=${address}`, + { + headers: { + 'Accept': 'application/json', + 'AccessKey': DEBANK_API_KEY || '', + }, + } + ); + + if (!response.ok) { + throw new Error('DeBankAPI request failed'); + } + + const data = await response.json(); + return data; + } catch (error) { + console.error('DeBankAPI error:', error); + return []; + } +} + +export async function GET(request: Request) { + try { + await limiter.check(request, 10); + + const { searchParams } = new URL(request.url); + const address = searchParams.get("address"); + const network = searchParams.get("network") || "base-sepolia"; + + if (!address) { + return NextResponse.json( + { error: "Wallet address is required" }, + { status: 400 } + ); + } + + const client = clients[network as keyof typeof clients]; + if (!client) { + return NextResponse.json( + { error: "Invalid network specified" }, + { status: 400 } + ); + } + + // Fetch data in parallel + const [balance, tokens] = await Promise.all([ + client.getBalance({ address: address as `0x${string}` }), + fetchTokenBalances(address), + ]); + + // Format the response + const formattedBalance = formatEther(balance); + const formattedTokens = tokens.map((token: any) => ({ + chain: token.chain, + symbol: token.symbol, + balance: token.balance, + price: token.price, + usd_value: token.amount * token.price, + })); + + return NextResponse.json({ + address, + network, + nativeBalance: { + amount: formattedBalance, + symbol: network.includes('sepolia') ? 'SEP-ETH' : 'ETH', + }, + tokens: formattedTokens, + timestamp: new Date().toISOString(), + }); + + } catch (error) { + console.error("Wallet info error:", error); + return NextResponse.json( + { error: "Failed to fetch wallet information" }, + { status: 500 } + ); + } +} \ No newline at end of file diff --git a/app/auth/callback/route.ts b/app/auth/callback/route.ts deleted file mode 100644 index 8d5b3f5..0000000 --- a/app/auth/callback/route.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { cookies } from "next/headers"; -import { NextResponse } from "next/server"; - -import { createClient } from "@/lib/supabase/server"; - -export async function GET(request: Request) { - const requestUrl = new URL(request.url); - const code = requestUrl.searchParams.get("code"); - - if (code) { - const supabase = await createClient(); - const { error } = await supabase.auth.exchangeCodeForSession(code); - - if (!error) { - return NextResponse.redirect(requestUrl.origin); - } - } - - // Return the user to an error page with some instructions - return NextResponse.redirect(`${requestUrl.origin}/auth-error`); -} diff --git a/app/layout.tsx b/app/layout.tsx index f487c0b..2610ff8 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -1,6 +1,6 @@ import { Metadata } from "next"; - -import { RootProvider } from "@/components/providers/root-provider"; +import { ClerkProvider } from "@clerk/nextjs"; +import { ClientProviders } from '@/components/providers/client-providers' import "./globals.css"; import "../styles/dark-mode.css"; @@ -32,25 +32,13 @@ export default function RootLayout({ children: React.ReactNode; }) { return ( - - -