commit 10673f4d1368167db2cdf84ccf43fc1a40e91f64 Author: Reihan Date: Mon Feb 17 15:21:20 2025 +0700 init diff --git a/.env b/.env new file mode 100644 index 0000000..1fce58b --- /dev/null +++ b/.env @@ -0,0 +1,9 @@ +POSTGRES_DB_URL=postgres://postgres:eMd9hliASrN1yuNOYSk7LtOdlLRnnlnhUF31JKww6zQ=@supabase-pgdb-1:5432/solana +NEXT_PUBLIC_PRIVY_APP_ID=cm6nnbni501avu3k6sfxrc8ts +PRIVY_APP_SECRET=2j5KefComuF3Fm3YbDPh2udnSjfi2uYrSihG7PmXij7JDC45sobRGCoYLSYS21sRyW3De4SbFmw9mtTiyqNtoHub +PRIVY_WALLET_SIGNING_KEY=wallet-auth:MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgDJtG6Ejxyshi9/g1Qb/qWaWpRNEoMcDnF1MhE8r/3SWhRANCAAQl03/3auCjNZm1GHjdpAyw73ZoMlAAVfnXtVlDRSJCheupk6A0PQNdy/NL7mOfspiLxKnvq/mfurI2HGfthVDI +OPENAI_API_KEY="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" +SOLSCAN_API_TOKEN=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJjcmVhdGVkQXQiOjE3MzAwNDg4Njc1ODUsImVtYWlsIjoic2hpYWxvdGhAZ21haWwuY29tIiwiYWN0aW9uIjoidG9rZW4tYXBpIiwiYXBpVmVyc2lvbiI6InYyIiwiaWF0IjoxNzMwMDQ4ODY3fQ.JKKAyBvlfB68zMidRipBfXv2l-a_eEIA3gAU5wxusyQ +BIRDEYE_API_KEY=26e4687aad6f4630a3cd76f85a147657 +RPC_URL=https://mainnet.helius-rpc.com/?api-key=22e4b61a-a7f2-47ab-bbc6-48d095a9ceb4 +HELIUS_API_KEY=22e4b61a-a7f2-47ab-bbc6-48d095a9ceb4 \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..50b2309 --- /dev/null +++ b/.gitignore @@ -0,0 +1,39 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +node_modules +/.pnp +.pnp.* +.yarn/* +!.yarn/patches +!.yarn/plugins +!.yarn/releases +!.yarn/versions + +# testing +/coverage + +# next.js +/.next/ +/out/ + +# production +/build + +# misc +.DS_Store +*.pem + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* +.pnpm-debug.log* + + +# vercel +.vercel + +# typescript +*.tsbuildinfo +next-env.d.ts diff --git a/README.md b/README.md new file mode 100644 index 0000000..eb8aec4 --- /dev/null +++ b/README.md @@ -0,0 +1,40 @@ +This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/pages/api-reference/create-next-app). + +## Getting Started + +First, run the development server: + +```bash +npm run dev +# or +yarn dev +# or +pnpm dev +# or +bun dev +``` + +Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. + +You can start editing the page by modifying `pages/index.js`. The page auto-updates as you edit the file. + +[API routes](https://nextjs.org/docs/pages/building-your-application/routing/api-routes) can be accessed on [http://localhost:3000/api/hello](http://localhost:3000/api/hello). This endpoint can be edited in `pages/api/hello.js`. + +The `pages/api` directory is mapped to `/api/*`. Files in this directory are treated as [API routes](https://nextjs.org/docs/pages/building-your-application/routing/api-routes) instead of React pages. + +This project uses [`next/font`](https://nextjs.org/docs/pages/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel. + +## Learn More + +To learn more about Next.js, take a look at the following resources: + +- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. +- [Learn Next.js](https://nextjs.org/learn-pages-router) - an interactive Next.js tutorial. + +You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome! + +## Deploy on Vercel + +The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. + +Check out our [Next.js deployment documentation](https://nextjs.org/docs/pages/building-your-application/deploying) for more details. diff --git a/SCHEMA.sql b/SCHEMA.sql new file mode 100644 index 0000000..0e4393e --- /dev/null +++ b/SCHEMA.sql @@ -0,0 +1,8 @@ +CREATE TABLE users ( + userid TEXT PRIMARY KEY, + publickey TEXT NOT NULL, + last_signedin_at TIMESTAMP, + created_at TIMESTAMP DEFAULT NOW(), + tg_userid TEXT DEFAULT NULL, + delegated BOOLEAN DEFAULT FALSE +); diff --git a/TODO b/TODO new file mode 100644 index 0000000..541a526 --- /dev/null +++ b/TODO @@ -0,0 +1,28 @@ +- show user wallet +- show assets +- add export walet hook +- add table to store chat ids for user + - user id + - chat id + - created at + - title +- create chat title ai function +- trim messages from ai node +- clean up code + +- transfer function: + - to: address or domain name + - mint: default native sol + - add clear errors sand artifacts + +- trade function: + - automatically check which platform if pf or jup + - from + - to + - usd value buy in + - token buy in + - slippage + + +- implement image upload for pump fun creation. local minio s3 +- create pf token \ No newline at end of file diff --git a/app/api/chat/route.js b/app/api/chat/route.js new file mode 100644 index 0000000..88c96cb --- /dev/null +++ b/app/api/chat/route.js @@ -0,0 +1,246 @@ +import { NextResponse } from "next/server"; +import { ulid } from "ulid"; +import { Command } from "@langchain/langgraph"; +import { isAIMessage } from "@langchain/core/messages"; +import checkpointer from "@/server/checkpointer"; +import { PrivyClient } from '@privy-io/server-auth'; +import { PublicKey } from '@solana/web3.js'; +import { SolanaAgentKit } from '@/solana-agent-kit'; +import pool from "@/server/db"; +import { createPrivyEmbeddedWallet } from '@/lib/solana/PrivyEmbeddedWallet'; +import getGraph from "@/server/graph"; + +export const dynamic = "force-dynamic"; + +const PRIVY_APP_ID = process.env.NEXT_PUBLIC_PRIVY_APP_ID; +const PRIVY_APP_SECRET = process.env.PRIVY_APP_SECRET; +const PRIVY_WALLET_SIGNING_KEY = process.env.PRIVY_WALLET_SIGNING_KEY; +const RPC_URL = process.env.RPC_URL; +const OPENAI_API_KEY = process.env.OPENAI_API_KEY; +const HELIUS_API_KEY = process.env.HELIUS_API_KEY; + +// TODO +const SYSTEMprompt = `You are a senior Solana software engineer and market data expert. You have access to Solana Defi information. You could also analyze github repos and tweet profiles and posts and search twitter. +Your goal is to give you clean and concise responses to users to help them on your Solana journey. You must analyze carefully the user input, then reason about the possible routes you could take using the tools available to you, dont return information not asked for by the user. All tool calls are visible to the user so DO NOT repeat what is said in the tools. +YOU MUST draw your own conclusions and analysis from the tool responses. Only your analysis must be show the user. Keep it concise and small. If user asks for list of tokens trending the charts will be showm, DO NOT LIST RETURN THEM TO THE USER! KEEP RESPONSES SHORT!`; + +export async function POST(req) { + try { + const body = await req.json(); + console.log(body); + + const { message, thread_id, approveObj = {} } = body; + console.log(thread_id); + + if (!thread_id) { + return NextResponse.json({ error: "Unauthorized. Please sign in" }, { status: 401 }); + } + if (!message && approveObj == {}) { + return NextResponse.json({ error: "Empty message" }, { status: 401 }); + } + + + const cookieAuthToken = req.cookies.get("privy-id-token"); + console.log(cookieAuthToken); + + if (!cookieAuthToken) { + console.log("No authentication token found."); + return NextResponse.json({ error: "Unauthorized: No auth token" }, { status: 401 }); + } + + const PRIVY_SERVER_CLIENT = new PrivyClient(PRIVY_APP_ID, PRIVY_APP_SECRET, { + walletApi: { + authorizationPrivateKey: PRIVY_WALLET_SIGNING_KEY, + }, + }); + + console.log("Verifying auth token..."); + const claims = await PRIVY_SERVER_CLIENT.verifyAuthToken(cookieAuthToken.value); + const userId = claims.userId; + console.log("Authenticated user ID:", userId); + + const dbRes = await pool.query("SELECT userid, publickey, tg_userid, delegated FROM users WHERE userid = $1", [userId]); + if (dbRes.rows.length === 1) { + const { userid, publickey } = dbRes.rows[0]; + console.log("User found in database:", userid, "with public key:", publickey); + + const walletAdapter = new createPrivyEmbeddedWallet( + PRIVY_SERVER_CLIENT, + new PublicKey(publickey) + ); + + const solAgentKit = new SolanaAgentKit(walletAdapter, RPC_URL, { + OPENAI_API_KEY, + HELIUS_API_KEY + }); + const graph = getGraph(solAgentKit) + + const input = { + role: "user", + content: message ?? "", + }; + + const config = { configurable: { thread_id } }; + let passToGraph = { messages: [input] }; + + const checkpointerData = await checkpointer.get(config); + + if ( + typeof checkpointerData?.channel_values === "object" && + "branch:agent:condition:checkApproval" in checkpointerData.channel_values + ) { + console.log("=".repeat(75)); + console.log(`${"=".repeat(25)} Graph interrupted for user input ${"=".repeat(25)}`); + if (Object.keys(approveObj).length == 0) { + return NextResponse.json({ error: "Please accept or reject the tools" }, { status: 401 }); + } + passToGraph = new Command({ resume: approveObj }); + } + + const transformStream = new ReadableStream({ + async start(controller) { + const textEncoder = new TextEncoder(); + try { + for await (const event of await graph.stream(passToGraph, config)) { + if (event.__interrupt__) { + console.log("=".repeat(75)); + console.log("=" + "=".repeat(25) + " Interrupted for permission " + "=".repeat(25)); + console.log(JSON.stringify(event.__interrupt__)); + console.log("=".repeat(75)); + const toolCalls = event.__interrupt__[0].value.map((tool) => ({ + id: tool.id, + name: tool.name, + args: Object.fromEntries(Object.entries(tool.args).filter(([key]) => key !== "debug")), + })); + console.log(toolCalls); + + controller.enqueue(`f:{"messageId":"${event.__interrupt__[0].ns[0]}"}\n`); + const interrupter_tool_id = ulid(); + controller.enqueue( + `9:${JSON.stringify({ + toolCallId: interrupter_tool_id, + toolName: "interrupter", + args: { toolCalls }, + })}\n`, + ); + + controller.enqueue( + `a:${JSON.stringify({ + toolCallId: interrupter_tool_id, + result: "Your input is required to proceed", + })}\n`, + ); + + controller.enqueue( + `e:${JSON.stringify({ + finishReason: "tool-calls", + usage: { promptTokens: 0, completionTokens: 0 }, + isContinued: false, + })}\n`, + ); + } else if (event.checkApproval) { + continue; + } else if (event.agent) { + console.log("=".repeat(75)); + console.log("=" + "=".repeat(25) + " Agent node " + "=".repeat(25)); + const message = event.agent.messages[0]; + console.log(message); + + if (message.content) { + controller.enqueue(`f:{"messageId":"${message.id}"}\n`); + controller.enqueue(`0:${JSON.stringify(message.content)}\n`); + controller.enqueue( + `e:${JSON.stringify({ + finishReason: "stop", + usage: { + promptTokens: message.usage_metadata.input_tokens, + completionTokens: message.usage_metadata.output_tokens, + }, + isContinued: false, + })}\n`, + ); + } + } else if (event.allToolsNode) { + console.log("=".repeat(75)); + console.log("=" + "=".repeat(25) + " All tools node " + "=".repeat(25)); + console.log(event.allToolsNode); + console.log("=".repeat(75)); + for (const toolMessage of event.allToolsNode.messages) { + controller.enqueue(`f:{"messageId":"${toolMessage.id}"}\n`); + + controller.enqueue( + textEncoder.encode( + `9:${JSON.stringify({ + toolCallId: toolMessage.tool_call_id, + toolName: toolMessage.name, + args: { artifact: toolMessage.artifact }, + })}\n`, + ), + ); + + controller.enqueue( + textEncoder.encode( + `a:${JSON.stringify({ + toolCallId: toolMessage.tool_call_id, + result: toolMessage.content, + })}\n`, + ), + ); + + controller.enqueue( + `e:${JSON.stringify({ + finishReason: "tool-calls", + usage: { promptTokens: 0, completionTokens: 0 }, + isContinued: false, + })}\n`, + ); + } + } + } + const checkpointerData = await checkpointer.get(config); + + if ( + typeof checkpointerData?.channel_values === "object" && + !("branch:agent:condition:checkApproval" in checkpointerData.channel_values) + ) { + const lastMessage = checkpointerData?.channel_values?.messages?.at(-1); + const lastMessage_hasToolCalls = lastMessage?.tool_calls?.length > 0; + const lastMessage_isAIMessageFlag = lastMessage ? isAIMessage(lastMessage) : false; + + if (lastMessage_isAIMessageFlag && !lastMessage_hasToolCalls) { + console.log("Breaking loop: Last message is an AI message with no tool calls."); + // console.log(checkpointerData); + controller.enqueue(`d:{"finishReason":"stop","usage":{"promptTokens":0,"completionTokens":0}}\n`); + } + } + } catch (error) { + controller.enqueue(`3:${JSON.stringify(error.message)}\n`); + controller.error(error); + } finally { + controller.close(); + } + }, + }); + + const response = new Response(transformStream, { + headers: { + "Content-Type": "text/event-stream", + "Cache-Control": "no-cache", + Connection: "keep-alive", + "x-vercel-ai-data-stream": "v1", + }, + }); + + return response; + + + + } else { + console.log("User not found in database."); + return NextResponse.json({ error: "User not found" }, { status: 404 }); + } + } catch (e) { + console.log(e); + return NextResponse.json({ error: "Error while processing your request. Please try again later" }, { status: 500 }); + } +} diff --git a/components.json b/components.json new file mode 100644 index 0000000..d76325a --- /dev/null +++ b/components.json @@ -0,0 +1,21 @@ +{ + "$schema": "https://ui.shadcn.com/schema.json", + "style": "new-york", + "rsc": false, + "tsx": false, + "tailwind": { + "config": "tailwind.config.mjs", + "css": "styles/globals.css", + "baseColor": "neutral", + "cssVariables": true, + "prefix": "" + }, + "aliases": { + "components": "@/components", + "utils": "@/lib/utils", + "ui": "@/components/ui", + "lib": "@/lib", + "hooks": "@/hooks" + }, + "iconLibrary": "lucide" +} \ No newline at end of file diff --git a/components/chat.jsx b/components/chat.jsx new file mode 100644 index 0000000..481bdbd --- /dev/null +++ b/components/chat.jsx @@ -0,0 +1,91 @@ +"use client" +import { + ChatInput, + ChatInputSubmit, + ChatInputTextArea +} from "@/components/ui/chat-input" +import { + ChatMessage, + ChatMessageAvatar, + ChatMessageContent +} from "@/components/ui/chat-message" +import { ChatMessageArea } from "@/components/ui/chat-message-area" +import { useChat } from "ai/react" + +export function Chat({ className,thread_id,initialMessages, ...props }) { + const { + messages, + input, + handleInputChange, + handleSubmit, + isLoading, + stop + } = useChat({ + id: thread_id, + api: "/api/chat", + sendExtraMessageFields: true, + experimental_prepareRequestBody({ messages, id, requestData }) { + console.log(requestData); + + return { message: messages[messages.length - 1].content, thread_id, approveObj:requestData }; + }, + initialMessages: initialMessages, + onFinish: message => { + console.log("onFinish", message, completion); + }, + onToolCall: tool => console.log(tool) + }) + console.log("messages",messages); + + const handleSubmitMessage = () => { + if (isLoading) { + return + } + handleSubmit() + } + + return ( +
+ +
+ {messages.map(message => { + + + if (message.role !== "user") { + return ( + + + + + ) + } + if (!message.content) return null; + + return ( + + + + ) + })} +
+
+
+ + + + +
+
+ ) +} diff --git a/components/nav-user.jsx b/components/nav-user.jsx new file mode 100644 index 0000000..6c2c42f --- /dev/null +++ b/components/nav-user.jsx @@ -0,0 +1,102 @@ +"use client" + +import { + BadgeCheck, + Bell, + ChevronsUpDown, + CreditCard, + LogOut, + Sparkles +} from "lucide-react" + +import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar" +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuGroup, + DropdownMenuItem, + DropdownMenuLabel, + DropdownMenuSeparator, + DropdownMenuTrigger +} from "@/components/ui/dropdown-menu" +import { + SidebarMenu, + SidebarMenuButton, + SidebarMenuItem, + useSidebar +} from "@/components/ui/sidebar" + +export function NavUser({ user }) { + const { isMobile } = useSidebar() + + return ( + + + + + + + + CN + +
+ {user.name} + {user.email} +
+ +
+
+ + +
+ + + CN + +
+ {user.name} + {user.email} +
+
+
+ + + + + Upgrade to Pro + + + + + + + Account + + + + Billing + + + + Notifications + + + + + + Log out + +
+
+
+
+ ) +} diff --git a/components/sidebar-app.jsx b/components/sidebar-app.jsx new file mode 100644 index 0000000..08a6663 --- /dev/null +++ b/components/sidebar-app.jsx @@ -0,0 +1,182 @@ +"use client" +import { Button } from "@/components/ui/button" +import { + Sidebar, + SidebarContent, + SidebarFooter, + SidebarGroup, + SidebarGroupLabel, + SidebarHeader, + SidebarMenu, + SidebarMenuButton, + SidebarMenuItem, + SidebarRail +} from "@/components/ui/sidebar" +import { + Tooltip, + TooltipContent, + TooltipProvider, + TooltipTrigger +} from "@/components/ui/tooltip" +import { NavUser } from "@/components/nav-user" +import { MessageCircle, SquarePen } from "lucide-react" + +// This is sample data. +const data = { + user: { + name: "John Doe", + email: "m@example.com", + avatar: "/avatar-1.png" + }, + recentChats: [ + { + title: "Project Planning Assistant", + date: new Date(2024, 2, 20), + url: "#" + }, + { + title: "Code Review Helper", + date: new Date(2024, 2, 19), + url: "#" + }, + { + title: "Bug Analysis Chat", + date: new Date(2024, 2, 18), + url: "#" + } + ], + lastWeekChats: [ + { + title: "API Design Discussion", + date: new Date(2024, 2, 15), + url: "#" + }, + { + title: "Database Schema Planning", + date: new Date(2024, 2, 14), + url: "#" + } + ], + lastMonthChats: [ + { + title: "Architecture Overview", + date: new Date(2024, 1, 28), + url: "#" + }, + { + title: "Performance Optimization", + date: new Date(2024, 1, 25), + url: "#" + } + ], + previousChats: [ + { + title: "Initial Project Setup", + date: new Date(2023, 11, 15), + url: "#" + }, + { + title: "Requirements Analysis", + date: new Date(2023, 11, 10), + url: "#" + } + ] +} + +export function SidebarApp({ ...props }) { + return ( + + +
+
+
+ +
+ simple-ai +
+ {/* New Chat Button */} + + + + + + +

New Chat

+
+
+
+
+
+ +
+ {/* Recent Chats */} + + Recent + + {data.recentChats.map(chat => ( + + + + {chat.title} + + + ))} + + + + {/* Previous 7 Days */} + + Previous 7 Days + + {data.lastWeekChats.map(chat => ( + + + + {chat.title} + + + ))} + + + + {/* Previous 30 Days */} + + Previous 30 Days + + {data.lastMonthChats.map(chat => ( + + + + {chat.title} + + + ))} + + + + {/* Previous Years */} + + Previous Years + + {data.previousChats.map(chat => ( + + + + {chat.title} + + + ))} + + +
+
+ + + + +
+ ) +} diff --git a/components/ui/avatar.jsx b/components/ui/avatar.jsx new file mode 100644 index 0000000..9a2f853 --- /dev/null +++ b/components/ui/avatar.jsx @@ -0,0 +1,33 @@ +import * as React from "react" +import * as AvatarPrimitive from "@radix-ui/react-avatar" + +import { cn } from "@/lib/utils" + +const Avatar = React.forwardRef(({ className, ...props }, ref) => ( + +)) +Avatar.displayName = AvatarPrimitive.Root.displayName + +const AvatarImage = React.forwardRef(({ className, ...props }, ref) => ( + +)) +AvatarImage.displayName = AvatarPrimitive.Image.displayName + +const AvatarFallback = React.forwardRef(({ className, ...props }, ref) => ( + +)) +AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName + +export { Avatar, AvatarImage, AvatarFallback } diff --git a/components/ui/breadcrumb.jsx b/components/ui/breadcrumb.jsx new file mode 100644 index 0000000..4d782a4 --- /dev/null +++ b/components/ui/breadcrumb.jsx @@ -0,0 +1,92 @@ +import * as React from "react" +import { Slot } from "@radix-ui/react-slot" +import { ChevronRight, MoreHorizontal } from "lucide-react" + +import { cn } from "@/lib/utils" + +const Breadcrumb = React.forwardRef( + ({ ...props }, ref) =>