diff --git a/client/app/agent/transaction/[id]/page.tsx b/client/app/agent/transaction/[id]/page.tsx index d04180f0..21170ff5 100644 --- a/client/app/agent/transaction/[id]/page.tsx +++ b/client/app/agent/transaction/[id]/page.tsx @@ -3,7 +3,6 @@ // app/agent/transaction/[id]/page.tsx "use client"; - import * as React from "react"; import { useParams, useRouter } from "next/navigation"; import { v4 as uuidv4 } from "uuid"; @@ -14,13 +13,7 @@ import { Separator } from "@/components/ui/separator"; import { Plus, Send, Home, Mic, Ban } from "lucide-react"; import { useAccount, useProvider } from "@starknet-react/core"; import { ConnectButton, DisconnectButton } from "@/lib/Connect"; -import { - Dialog, - DialogContent, - DialogHeader, - DialogTitle, - DialogTrigger, -} from "@/components/ui/dialog"; +import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog"; import Link from "next/link"; import { TransactionSuccess } from "@/components/TransactionSuccess"; import CommandList from "@/components/ui/command"; @@ -29,66 +22,63 @@ import ReactMarkdown from "react-markdown"; import remarkGfm from "remark-gfm"; interface UserPreferences { - riskTolerance: "low" | "medium" | "high"; - preferredAssets: string[]; - preferredChains: string[]; - investmentHorizon: "short" | "medium" | "long"; + riskTolerance: "low" | "medium" | "high"; + preferredAssets: string[]; + preferredChains: string[]; + investmentHorizon: "short" | "medium" | "long"; } - interface Message { - role: string; - id: string; - content: string; - timestamp: string; - user: string; - transaction?: { - data: { - transactions: Array<{ - contractAddress: string; - entrypoint: string; - calldata: string[]; - }>; - fromToken?: any; - toToken?: any; - fromAmount?: string; - toAmount?: string; - receiver?: string; - gasCostUSD?: string; - solver?: string; - }; - type: string; - }; - recommendations?: { - pools: Array<{ - name: string; - apy: number; - tvl: number; - riskLevel: string; - impermanentLoss: string; - chain: string; - protocol: string; - }>; - strategy: string; - }; + role: string; + id: string; + content: string; + timestamp: string; + user: string; + transaction?: { + data: { + transactions: Array<{ + contractAddress: string; + entrypoint: string; + calldata: string[]; + }>; + fromToken?: any; + toToken?: any; + fromAmount?: string; + toAmount?: string; + receiver?: string; + gasCostUSD?: string; + solver?: string; + }; + type: string; + }; + recommendations?: { + pools: Array<{ + name: string; + apy: number; + tvl: number; + riskLevel: string; + impermanentLoss: string; + chain: string; + protocol: string; + }>; + strategy: string; + }; } - interface TransactionHandlerProps { - transactions: Array<{ - contractAddress: string; - entrypoint: string; - calldata: string[]; - }>; - description: string; - onSuccess: (hash: string) => void; - onError: (error: any) => void; + transactions: Array<{ + contractAddress: string; + entrypoint: string; + calldata: string[]; + }>; + description: string; + onSuccess: (hash: string) => void; + onError: (error: any) => void; } - interface MessageContentProps { - message: Message; - onTransactionSuccess: (hash: string) => void; + message: Message; + onTransactionSuccess: (hash: string) => void; } @@ -108,104 +98,91 @@ const TransactionHandler: React.FC = ({ } - setIsProcessing(true); - try { - for (const tx of transactions) { - const response = await account.execute({ - contractAddress: tx.contractAddress, - entrypoint: tx.entrypoint, - calldata: tx.calldata, - }); - await account.waitForTransaction(response.transaction_hash); - if (tx === transactions[transactions.length - 1]) { - onSuccess(response.transaction_hash); - } - } - } catch (error) { - console.error("Transaction failed:", error); - onError(error); - } finally { - setIsProcessing(false); - } - }; - - - return ( -
-

{description}

- -
- ); + setIsProcessing(true); + try { + for (const tx of transactions) { + const response = await account.execute({ + contractAddress: tx.contractAddress, + entrypoint: tx.entrypoint, + calldata: tx.calldata, + }); + await account.waitForTransaction(response.transaction_hash); + if (tx === transactions[transactions.length - 1]) { + onSuccess(response.transaction_hash); + } + } + } catch (error) { + console.error("Transaction failed:", error); + onError(error); + } finally { + setIsProcessing(false); + } + }; + + return ( +
+

{description}

+ +
+ ); }; - const PreferencesDialog: React.FC<{ - open: boolean; - onClose: () => void; - onSubmit: (preferences: UserPreferences) => void; + open: boolean; + onClose: () => void; + onSubmit: (preferences: UserPreferences) => void; }> = ({ open, onClose, onSubmit }) => { - const [preferences, setPreferences] = useState({ - riskTolerance: "medium", - preferredAssets: [], - preferredChains: [], - investmentHorizon: "medium", - }); - - - return ( - - - - Investment Preferences - -
-
- - -
- {/* Add similar inputs for other preferences */} - -
-
-
- ); + const [preferences, setPreferences] = useState({ + riskTolerance: "medium", + preferredAssets: [], + preferredChains: [], + investmentHorizon: "medium", + }); + + return ( + + + + Investment Preferences + +
+
+ + +
+ {/* Add similar inputs for other preferences */} + +
+
+
+ ); }; - -const MessageContent: React.FC = ({ - message, - onTransactionSuccess, -}) => { - const [txHash, setTxHash] = React.useState(null); - +const MessageContent: React.FC = ({ message, onTransactionSuccess }) => { + const [txHash, setTxHash] = React.useState(null); if (message.recommendations) { return ( @@ -337,18 +314,16 @@ export default function TransactionPage() { }; - const handleTransactionSuccess = (hash: string) => { - const successMessage: Message = { - id: uuidv4(), - role: "agent", - content: - "Great! Would you like to perform another transaction? You can try swapping, transferring, depositing, or bridging tokens.", - timestamp: new Date().toLocaleTimeString(), - user: "Agent", - }; - setMessages((prev) => [...prev, successMessage]); - }; - + const handleTransactionSuccess = (hash: string) => { + const successMessage: Message = { + id: uuidv4(), + role: "agent", + content: "Great! Would you like to perform another transaction? You can try swapping, transferring, depositing, or bridging tokens.", + timestamp: new Date().toLocaleTimeString(), + user: "Agent", + }; + setMessages((prev) => [...prev, successMessage]); + }; const handleSendMessage = async () => { if (!inputValue.trim()) return; @@ -546,17 +521,16 @@ export default function TransactionPage() { } }; - return ( -
- {/* Dotted background */} -
- + return ( +
+ {/* Dotted background */} +
{/* Content wrapper */}
@@ -612,36 +586,30 @@ export default function TransactionPage() { */} - -
-

Transaction History

- -
+

Transaction History

+ +
- {[0, 1, 2, 4, 5].map((index) => ( -
- 0x86ecca95fec - - 12th Dec, 2025 - -
- ))} -
-
- + dark:[&::-webkit-scrollbar-thumb]:bg-neutral-500"> + {[0, 1, 2, 4, 5].map((index) => ( +
+ 0x86ecca95fec + 12th Dec, 2025 +
+ ))} +
+
@@ -650,31 +618,30 @@ export default function TransactionPage() {
- {/* Main Content */} -
- {/* Header */} -
-
- - - Home - -

StarkFinder - Transactions

-
-
- {address ? ( -
-
- {`${address?.slice(0, 5)}...${address?.slice(-3)}`} -
- -
- ) : ( - - )} -
-
- + {/* Main Content */} +
+ {/* Header */} +
+
+ + + Home + +

StarkFinder - Transactions

+
+
+ {address ? ( +
+
{`${address?.slice(0, 5)}...${address?.slice(-3)}`}
+ +
+ ) : ( + + )} +
+
{/* Chat Area */} @@ -783,17 +750,14 @@ export default function TransactionPage() { - setShowPreferences(false)} - onSubmit={(prefs) => { - setUserPreferences(prefs); - setShowPreferences(false); - }} - /> -
- ); + setShowPreferences(false)} + onSubmit={(prefs) => { + setUserPreferences(prefs); + setShowPreferences(false); + }} + /> +
+ ); } - - - diff --git a/client/app/api/transactions/helper.ts b/client/app/api/transactions/helper.ts index cc9fd7fc..1bb6941b 100644 --- a/client/app/api/transactions/helper.ts +++ b/client/app/api/transactions/helper.ts @@ -5,154 +5,141 @@ import { Pool } from "./types"; export const BRIAN_API_KEY = process.env.BRIAN_API_KEY || ""; export const OPENAI_API_KEY = process.env.OPENAI_API_KEY || ""; -export const BRIAN_API_URL = - "https://api.brianknows.org/api/v0/agent/knowledge"; -export const BRIAN_DEFAULT_RESPONSE = - "🤖 Sorry, I don't know how to answer. The AskBrian feature allows you to ask for information on a custom-built knowledge base of resources. Contact the Brian team if you want to add new resources!"; +export const BRIAN_API_URL = "https://api.brianknows.org/api/v0/agent/knowledge"; +export const BRIAN_TRANSACTION_API_URL = "https://api.brianknows.org/api/v0/agent/transaction"; +export const BRIAN_DEFAULT_RESPONSE = "🤖 Sorry, I don't know how to answer. The AskBrian feature allows you to ask for information on a custom-built knowledge base of resources. Contact the Brian team if you want to add new resources!"; export const YIELD_API_URL = "https://yields.llama.fi/pools"; export const TOKEN_API_URL = "https://starknet.api.avnu.fi/v1/starknet/tokens"; export async function fetchTokenData() { - try { - const response = await axios.get(TOKEN_API_URL, { - headers: { "Content-Type": "application/json" }, - }); - - if (!response.data?.content) { - throw new Error("Invalid token data response"); - } - - return response.data.content; - } catch (error) { - console.error("Error fetching token data:", error); - return []; - } + try { + const response = await axios.get(TOKEN_API_URL, { + headers: { "Content-Type": "application/json" }, + }); + + if (!response.data?.content) { + throw new Error("Invalid token data response"); + } + + return response.data.content; + } catch (error) { + console.error("Error fetching token data:", error); + return []; + } } export async function fetchYieldData(): Promise { - try { - const response = await axios.get(YIELD_API_URL, { - headers: { "Content-Type": "application/json" }, - }); - - if (!response.data?.data) { - throw new Error("Invalid yield data response"); - } - - const allPools = response.data.data; - const starknetPools = allPools - .filter((pool: any) => pool.chain?.toLowerCase() === "starknet") - .map((pool: any) => ({ - name: pool.pool, - apy: parseFloat(pool.apy) || 0, - tvl: parseFloat(pool.tvlUsd) || 0, - riskLevel: determineRiskLevel(pool.apy, pool.tvlUsd), - impermanentLoss: determineImpermanentLossRisk(pool.pool), - chain: "starknet", - protocol: pool.project, - })); - - return starknetPools.sort((a: Pool, b: Pool) => b.tvl - a.tvl); - } catch (error) { - console.error("Error fetching yield data:", error); - return []; - } + try { + const response = await axios.get(YIELD_API_URL, { + headers: { "Content-Type": "application/json" }, + }); + + if (!response.data?.data) { + throw new Error("Invalid yield data response"); + } + + const allPools = response.data.data; + const starknetPools = allPools + .filter((pool: any) => pool.chain?.toLowerCase() === "starknet") + .map((pool: any) => ({ + name: pool.pool, + apy: parseFloat(pool.apy) || 0, + tvl: parseFloat(pool.tvlUsd) || 0, + riskLevel: determineRiskLevel(pool.apy, pool.tvlUsd), + impermanentLoss: determineImpermanentLossRisk(pool.pool), + chain: "starknet", + protocol: pool.project, + })); + + return starknetPools.sort((a: Pool, b: Pool) => b.tvl - a.tvl); + } catch (error) { + console.error("Error fetching yield data:", error); + return []; + } } export async function getOrCreateUser(address: string) { - try { - let user = await prisma.user.findUnique({ - where: { id: address }, - }); - - if (!user) { - user = await prisma.user.create({ - data: { - id: address, - email: null, - name: null, - }, - }); - } - - return user; - } catch (error) { - console.error("Error in getOrCreateUser:", error); - throw error; - } + try { + let user = await prisma.user.findUnique({ + where: { id: address }, + }); + + if (!user) { + user = await prisma.user.create({ + data: { + id: address, + email: null, + name: null, + }, + }); + } + + return user; + } catch (error) { + console.error("Error in getOrCreateUser:", error); + throw error; + } } -export async function storeMessage({ - content, - chatId, - userId, - transactionId = null, - }: { - content: any[]; - chatId: string; - userId: string; - transactionId?: string | null; -}) { - try { - console.log(transactionId); - return await prisma.message.create({ - data: { - content, - chatId, - userId, - }, - }); - } catch (error) { - console.error("Error storing message:", error); - throw error; - } - } +export async function storeMessage({ content, chatId, userId, transactionId = null }: { content: any[]; chatId: string; userId: string; transactionId?: string | null }) { + try { + console.log(transactionId); + return await prisma.message.create({ + data: { + content, + chatId, + userId, + }, + }); + } catch (error) { + console.error("Error storing message:", error); + throw error; + } +} export async function createOrGetChat(userId: string) { - try { - await getOrCreateUser(userId); - return await prisma.chat.create({ - data: { userId }, - }); - } catch (error) { - console.error("Error creating chat:", error); - throw error; - } + try { + await getOrCreateUser(userId); + return await prisma.chat.create({ + data: { userId }, + }); + } catch (error) { + console.error("Error creating chat:", error); + throw error; + } } + + function determineRiskLevel(apy: number, tvl: number): string { - if (!apy || !tvl) return "unknown"; - - const tvlMillions = tvl / 1000000; - - if (apy > 100) { - return tvlMillions > 10 ? "medium-high" : "high"; - } else if (apy > 50) { - return tvlMillions > 20 ? "medium" : "medium-high"; - } else if (apy > 20) { - return tvlMillions > 50 ? "low-medium" : "medium"; - } else { - return tvlMillions > 100 ? "low" : "low-medium"; - } + if (!apy || !tvl) return "unknown"; + + const tvlMillions = tvl / 1000000; + + if (apy > 100) { + return tvlMillions > 10 ? "medium-high" : "high"; + } else if (apy > 50) { + return tvlMillions > 20 ? "medium" : "medium-high"; + } else if (apy > 20) { + return tvlMillions > 50 ? "low-medium" : "medium"; + } else { + return tvlMillions > 100 ? "low" : "low-medium"; + } } function determineImpermanentLossRisk(poolName: string): string { - const poolNameLower = poolName.toLowerCase(); - - // Check if it's a stablecoin pool - if ( - poolNameLower.includes("usdc") || - poolNameLower.includes("usdt") || - poolNameLower.includes("dai") - ) { - return "Very Low"; - } - - // Check if it's a volatile pair - if (poolNameLower.includes("eth") || poolNameLower.includes("btc")) { - return "Medium"; - } - - // Default for unknown compositions - return "Variable"; -} \ No newline at end of file + const poolNameLower = poolName.toLowerCase(); + + // Check if it's a stablecoin pool + if (poolNameLower.includes("usdc") || poolNameLower.includes("usdt") || poolNameLower.includes("dai")) { + return "Very Low"; + } + + // Check if it's a volatile pair + if (poolNameLower.includes("eth") || poolNameLower.includes("btc")) { + return "Medium"; + } + + // Default for unknown compositions + return "Variable"; +} diff --git a/client/app/api/transactions/route.ts b/client/app/api/transactions/route.ts index 74f04d30..4a94eab2 100644 --- a/client/app/api/transactions/route.ts +++ b/client/app/api/transactions/route.ts @@ -4,197 +4,156 @@ import { NextResponse, type NextRequest } from "next/server"; import { ChatOpenAI } from "@langchain/openai"; import { transactionProcessor } from "@/lib/transaction"; -import { - BrianResponse, - BrianTransactionData, -} from "@/lib/transaction/types"; -import { - TRANSACTION_INTENT_PROMPT, - transactionIntentPromptTemplate, - ASK_OPENAI_AGENT_PROMPT, - INVESTMENT_RECOMMENDATION_PROMPT, - investmentRecommendationPromptTemplate, -} from "@/prompts/prompts"; -import { - ChatPromptTemplate, - SystemMessagePromptTemplate, - HumanMessagePromptTemplate, -} from "@langchain/core/prompts"; -import { - START, - END, - MessagesAnnotation, - MemorySaver, - StateGraph, -} from "@langchain/langgraph"; +import { BrianResponse, BrianTransactionData } from "@/lib/transaction/types"; +import { TRANSACTION_INTENT_PROMPT, transactionIntentPromptTemplate, ASK_OPENAI_AGENT_PROMPT, INVESTMENT_RECOMMENDATION_PROMPT, investmentRecommendationPromptTemplate } from "@/prompts/prompts"; +import { ChatPromptTemplate, SystemMessagePromptTemplate, HumanMessagePromptTemplate } from "@langchain/core/prompts"; +import { START, END, MessagesAnnotation, MemorySaver, StateGraph } from "@langchain/langgraph"; import { StringOutputParser } from "@langchain/core/output_parsers"; import prisma from "@/lib/db"; import { TxType } from "@prisma/client"; import { UserPreferences, InvestmentRecommendation, Pool } from "./types"; -import { BRIAN_API_KEY, BRIAN_API_URL, BRIAN_DEFAULT_RESPONSE, createOrGetChat, fetchTokenData, fetchYieldData, getOrCreateUser, OPENAI_API_KEY, storeMessage } from "./helper"; +import { BRIAN_API_KEY, BRIAN_API_URL, BRIAN_TRANSACTION_API_URL, BRIAN_DEFAULT_RESPONSE, createOrGetChat, fetchTokenData, fetchYieldData, getOrCreateUser, OPENAI_API_KEY, storeMessage } from "./helper"; import axios from "axios"; // Initialize OpenAI models const agent = new ChatOpenAI({ - modelName: "gpt-4o", - temperature: 0.5, - openAIApiKey: OPENAI_API_KEY, - streaming: true, + modelName: "gpt-4o", + temperature: 0.5, + openAIApiKey: OPENAI_API_KEY, + streaming: true, }); const transactionLLM = new ChatOpenAI({ - model: "gpt-4", - apiKey: OPENAI_API_KEY, + model: "gpt-4", + apiKey: OPENAI_API_KEY, }); - -const systemPrompt = - ASK_OPENAI_AGENT_PROMPT + - "\nThe provided chat history includes a summary of the earlier conversation."; +const systemPrompt = ASK_OPENAI_AGENT_PROMPT + "\nThe provided chat history includes a summary of the earlier conversation."; const systemMessage = SystemMessagePromptTemplate.fromTemplate(systemPrompt); const userMessage = HumanMessagePromptTemplate.fromTemplate("{user_query}"); -const askAgentPromptTemplate = ChatPromptTemplate.fromMessages([ - systemMessage, - userMessage, -]); - - -async function getChatHistory( - chatId: string | { configurable?: { additional_args?: { chatId?: string } } } -) { - try { - const actualChatId = - typeof chatId === "object" && chatId.configurable?.additional_args?.chatId - ? chatId.configurable.additional_args.chatId - : chatId; - - if (!actualChatId || typeof actualChatId !== "string") { - console.warn("Invalid chat ID provided:", chatId); - return []; - } - - const messages = await prisma.message.findMany({ - where: { - chatId: actualChatId, - }, - orderBy: { - id: "asc", - }, - }); - - const formattedHistory = messages.flatMap((msg: any) => { - const content = msg.content as any[]; - return content.map((c) => ({ - role: c.role, - content: c.content, - })); - }); - - return formattedHistory; - } catch (error) { - console.error("Error fetching chat history:", error); - return []; - } +const askAgentPromptTemplate = ChatPromptTemplate.fromMessages([systemMessage, userMessage]); + +async function getChatHistory(chatId: string | { configurable?: { additional_args?: { chatId?: string } } }) { + try { + const actualChatId = typeof chatId === "object" && chatId.configurable?.additional_args?.chatId ? chatId.configurable.additional_args.chatId : chatId; + + if (!actualChatId || typeof actualChatId !== "string") { + console.warn("Invalid chat ID provided:", chatId); + return []; + } + + const messages = await prisma.message.findMany({ + where: { + chatId: actualChatId, + }, + orderBy: { + id: "asc", + }, + }); + + const formattedHistory = messages.flatMap((msg: any) => { + const content = msg.content as any[]; + return content.map((c) => ({ + role: c.role, + content: c.content, + })); + }); + + return formattedHistory; + } catch (error) { + console.error("Error fetching chat history:", error); + return []; + } } - - async function storeTransaction(userId: string, type: string, metadata: any) { - try { - const transaction = await prisma.transaction.create({ - data: { - userId, - type: type as TxType, - metadata, - }, - }); - return transaction; - } catch (error) { - console.error("Error storing transaction:", error); - throw error; - } + try { + const transaction = await prisma.transaction.create({ + data: { + userId, + type: type.toUpperCase() as TxType, + metadata, + }, + }); + return transaction; + } catch (error) { + console.error("Error storing transaction:", error); + throw error; + } +} + +async function getOrCreateTransactionChat(userId: string) { + try { + const chat = await prisma.chat.create({ + data: { + userId, + type: "TRANSACTION", + }, + }); + return chat; + } catch (error) { + console.error("Error creating transaction chat:", error); + throw error; + } } // LangChain workflow for Q&A const initialCallModel = async (state: typeof MessagesAnnotation.State) => { - const messages = [ - await systemMessage.format({ brianai_answer: BRIAN_DEFAULT_RESPONSE }), - ...state.messages, - ]; - const response = await agent.invoke(messages); - return { messages: response }; + const messages = [await systemMessage.format({ brianai_answer: BRIAN_DEFAULT_RESPONSE }), ...state.messages]; + const response = await agent.invoke(messages); + return { messages: response }; }; -const callModel = async ( - state: typeof MessagesAnnotation.State, - chatId?: any -) => { - if (!chatId) { - return await initialCallModel(state); - } +const callModel = async (state: typeof MessagesAnnotation.State, chatId?: any) => { + if (!chatId) { + return await initialCallModel(state); + } - const actualChatId = chatId?.configurable?.additional_args?.chatId || chatId; - const chatHistory = await getChatHistory(actualChatId); - const currentMessage = state.messages[state.messages.length - 1]; + const actualChatId = chatId?.configurable?.additional_args?.chatId || chatId; + const chatHistory = await getChatHistory(actualChatId); + const currentMessage = state.messages[state.messages.length - 1]; - if (chatHistory.length > 0) { - const summaryPrompt = ` + if (chatHistory.length > 0) { + const summaryPrompt = ` Distill the following chat history into a single summary message. Include as many specific details as you can. IMPORTANT NOTE: Include all information related to user's nature about trading and what kind of trader he/she is. `; - const summary = await agent.invoke([ - ...chatHistory, - { role: "user", content: summaryPrompt }, - ]); - - const response = await agent.invoke([ - await systemMessage.format({ brianai_answer: BRIAN_DEFAULT_RESPONSE }), - summary, - currentMessage, - ]); - - return { - messages: [summary, currentMessage, response], - }; - } else { - return await initialCallModel(state); - } + const summary = await agent.invoke([...chatHistory, { role: "user", content: summaryPrompt }]); + + const response = await agent.invoke([await systemMessage.format({ brianai_answer: BRIAN_DEFAULT_RESPONSE }), summary, currentMessage]); + + return { + messages: [summary, currentMessage, response], + }; + } else { + return await initialCallModel(state); + } }; -const workflow = new StateGraph(MessagesAnnotation) - .addNode("model", callModel) - .addEdge(START, "model") - .addEdge("model", END); +const workflow = new StateGraph(MessagesAnnotation).addNode("model", callModel).addEdge(START, "model").addEdge("model", END); const app = workflow.compile({ checkpointer: new MemorySaver() }); // Function to determine if a prompt is transaction-related async function isTransactionIntent(prompt: string, messages: any[]): Promise { - try { - // Simple heuristic check for transaction keywords - const transactionKeywords = [ - "swap", "transfer", "send", "bridge", "deposit", "withdraw", - "trade", "exchange", "transaction", "buy", "sell" - ]; - - // Check if any transaction keyword is in the prompt - const promptLower = prompt.toLowerCase(); - const hasTransactionKeyword = transactionKeywords.some(keyword => - promptLower.includes(keyword) - ); - - if (hasTransactionKeyword) { - return true; - } - - // If no clear keywords, use LLM to determine intent - const conversationHistory = messages - .map((msg) => `${msg.role}: ${msg.content}`) - .join("\n"); - - const checkPrompt = ` + try { + // Simple heuristic check for transaction keywords + const transactionKeywords = ["swap", "transfer", "send", "bridge", "deposit", "withdraw", "trade", "exchange", "transaction", "buy", "sell"]; + + // Check if any transaction keyword is in the prompt + const promptLower = prompt.toLowerCase(); + const hasTransactionKeyword = transactionKeywords.some((keyword) => promptLower.includes(keyword)); + + if (hasTransactionKeyword) { + return true; + } + + // If no clear keywords, use LLM to determine intent + const conversationHistory = messages.map((msg) => `${msg.role}: ${msg.content}`).join("\n"); + + const checkPrompt = ` Based on the following user message and conversation history, determine if the user is trying to perform a transaction-related action (like swapping, transferring, bridging, depositing, or withdrawing assets). User message: "${prompt}" @@ -204,543 +163,521 @@ async function isTransactionIntent(prompt: string, messages: any[]): Promise { - try { - const conversationHistory = messages - .map((msg) => `${msg.role}: ${msg.content}`) - .join("\n"); - - const formattedPrompt = await transactionIntentPromptTemplate.format({ - TRANSACTION_INTENT_PROMPT, - prompt, - chainId, - conversationHistory, - }); - - const jsonOutputParser = new StringOutputParser(); - const response = await transactionLLM.pipe(jsonOutputParser).invoke(formattedPrompt); - const intentData = JSON.parse(response); - - if (!intentData.isTransactionIntent) { - throw new Error("Not a transaction-related prompt"); - } - - const intentResponse: BrianResponse = { - solver: intentData.solver || "OpenAI-Intent-Recognizer", - action: intentData.action, - type: "write", - extractedParams: { - action: intentData.extractedParams.action, - token1: intentData.extractedParams.token1 || "", - token2: intentData.extractedParams.token2 || "", - chain: intentData.extractedParams.chain || "", - amount: intentData.extractedParams.amount || "", - protocol: intentData.extractedParams.protocol || "", - address: address, // should always be connected address - dest_chain: intentData.extractedParams.dest_chain || "", - destinationChain: intentData.extractedParams.dest_chain || "", - destinationAddress: - intentData.extractedParams.destinationAddress || - (intentData.extractedParams.same_network_type === "true" - ? address - : ""), - }, - data: {} as BrianTransactionData, - }; - - const value = 10 ** 18; - const weiAmount = BigInt(intentData.extractedParams.amount * value); - - switch (intentData.action) { - case "swap": - case "transfer": - intentResponse.data = { - description: intentData.data?.description || "", - steps: - intentData.extractedParams.transaction?.contractAddress || - intentData.extractedParams.transaction?.entrypoint || - intentData.extractedParams.transaction?.calldata - ? [ - { - contractAddress: - intentData.extractedParams.transaction.contractAddress, - entrypoint: - intentData.extractedParams.transaction.entrypoint, - calldata: [ - intentData.extractedParams.destinationAddress || - intentData.extractedParams.address, - weiAmount.toString(), - "0", - ], - }, - ] - : [], - fromToken: { - symbol: intentData.extractedParams.token1 || "", - address: intentData.extractedParams.address || "", - decimals: 1, - }, - toToken: { - symbol: intentData.extractedParams.token2 || "", - address: intentData.extractedParams.address || "", - decimals: 1, - }, - fromAmount: intentData.extractedParams.amount, - toAmount: intentData.extractedParams.amount, - receiver: intentData.extractedParams.address, - amountToApprove: intentData.data?.amountToApprove, - gasCostUSD: intentData.data?.gasCostUSD, - }; - break; - case "bridge": - intentResponse.data = { - description: "", - steps: [], - bridge: { - sourceNetwork: intentData.extractedParams.chain || "", - destinationNetwork: intentData.extractedParams.dest_chain || "", - sourceToken: intentData.extractedParams.token1 || "", - destinationToken: intentData.extractedParams.token2 || "", - amount: Number.parseFloat(intentData.extractedParams.amount || "0"), - sourceAddress: address, - destinationAddress: - intentData.extractedParams.destinationAddress || address, - }, - }; - break; - case "deposit": - case "withdraw": - intentResponse.data = { - description: "", - steps: [], - protocol: intentData.extractedParams.protocol || "", - fromAmount: intentData.extractedParams.amount, - toAmount: intentData.extractedParams.amount, - receiver: intentData.extractedParams.address || "", - }; - break; - default: - throw new Error(`Unsupported action type: ${intentData.action}`); - } - - return intentResponse; - } catch (error) { - console.error("Error fetching transaction intent:", error); - throw error; - } +async function getTransactionIntentFromOpenAI(prompt: string, address: string, chainId: string, messages: any[]): Promise { + try { + const conversationHistory = messages.map((msg) => `${msg.role}: ${msg.content}`).join("\n"); + + const formattedPrompt = await transactionIntentPromptTemplate.format({ + TRANSACTION_INTENT_PROMPT, + prompt, + chainId, + conversationHistory, + }); + + const jsonOutputParser = new StringOutputParser(); + const response = await transactionLLM.pipe(jsonOutputParser).invoke(formattedPrompt); + const intentData = JSON.parse(response); + + if (!intentData.isTransactionIntent) { + throw new Error("Not a transaction-related prompt"); + } + + const intentResponse: BrianResponse = { + solver: intentData.solver || "OpenAI-Intent-Recognizer", + action: intentData.action, + type: "write", + extractedParams: { + action: intentData.extractedParams.action, + token1: intentData.extractedParams.token1 || "", + token2: intentData.extractedParams.token2 || "", + chain: intentData.extractedParams.chain || "", + amount: intentData.extractedParams.amount || "", + protocol: intentData.extractedParams.protocol || "", + address: address, // should always be connected address + dest_chain: intentData.extractedParams.dest_chain || "", + destinationChain: intentData.extractedParams.dest_chain || "", + destinationAddress: intentData.extractedParams.destinationAddress || (intentData.extractedParams.same_network_type === "true" ? address : ""), + }, + data: {} as BrianTransactionData, + }; + + const value = 10 ** 18; + const weiAmount = BigInt(intentData.extractedParams.amount * value); + + switch (intentData.action) { + case "swap": + case "transfer": + intentResponse.data = { + description: intentData.data?.description || "", + steps: + intentData.extractedParams.transaction?.contractAddress || intentData.extractedParams.transaction?.entrypoint || intentData.extractedParams.transaction?.calldata + ? [ + { + contractAddress: intentData.extractedParams.transaction.contractAddress, + entrypoint: intentData.extractedParams.transaction.entrypoint, + calldata: [intentData.extractedParams.destinationAddress || intentData.extractedParams.address, weiAmount.toString(), "0"], + }, + ] + : [], + fromToken: { + symbol: intentData.extractedParams.token1 || "", + address: intentData.extractedParams.address || "", + decimals: 1, + }, + toToken: { + symbol: intentData.extractedParams.token2 || "", + address: intentData.extractedParams.address || "", + decimals: 1, + }, + fromAmount: intentData.extractedParams.amount, + toAmount: intentData.extractedParams.amount, + receiver: intentData.extractedParams.address, + amountToApprove: intentData.data?.amountToApprove, + gasCostUSD: intentData.data?.gasCostUSD, + }; + break; + case "bridge": + intentResponse.data = { + description: "", + steps: [], + bridge: { + sourceNetwork: intentData.extractedParams.chain || "", + destinationNetwork: intentData.extractedParams.dest_chain || "", + sourceToken: intentData.extractedParams.token1 || "", + destinationToken: intentData.extractedParams.token2 || "", + amount: Number.parseFloat(intentData.extractedParams.amount || "0"), + sourceAddress: address, + destinationAddress: intentData.extractedParams.destinationAddress || address, + }, + }; + break; + case "deposit": + case "withdraw": + intentResponse.data = { + description: "", + steps: [], + protocol: intentData.extractedParams.protocol || "", + fromAmount: intentData.extractedParams.amount, + toAmount: intentData.extractedParams.amount, + receiver: intentData.extractedParams.address || "", + }; + break; + default: + throw new Error(`Unsupported action type: ${intentData.action}`); + } + + return intentResponse; + } catch (error) { + console.error("Error fetching transaction intent:", error); + throw error; + } } // Function for BrianAI querying -async function queryBrianAI( - prompt: string, - chatId?: string, - streamCallback?: (chunk: string) => Promise -): Promise { - try { - const response = await axios.post( - BRIAN_API_URL, - { - prompt, - kb: "starknet_kb", - }, - { - headers: { - "Content-Type": "application/json", - "x-brian-api-key": BRIAN_API_KEY, - }, - } - ); - - const brianaiAnswer = response.data.result.answer; - const openaiAnswer = await queryOpenAI({ - brianaiResponse: brianaiAnswer, - userQuery: prompt, - chatId, - streamCallback, - }); - - return openaiAnswer; - } catch (error) { - console.error("Brian AI Error:", error); - if (streamCallback) { - throw error; - } - return "Sorry, I am unable to process your request at the moment."; - } +async function queryBrianAI(prompt: string, chatId?: string, streamCallback?: (chunk: string) => Promise): Promise { + try { + const response = await axios.post( + BRIAN_API_URL, + { + prompt, + kb: "starknet_kb", + }, + { + headers: { + "Content-Type": "application/json", + "x-brian-api-key": BRIAN_API_KEY, + }, + } + ); + + const brianaiAnswer = response.data.result.answer; + const openaiAnswer = await queryOpenAI({ + brianaiResponse: brianaiAnswer, + userQuery: prompt, + chatId, + streamCallback, + }); + + return openaiAnswer; + } catch (error) { + console.error("Brian AI Error:", error); + if (streamCallback) { + throw error; + } + return "Sorry, I am unable to process your request at the moment."; + } } // Function for OpenAI querying -async function queryOpenAI({ - userQuery, - brianaiResponse, - chatId, - streamCallback, -}: { - userQuery: string; - brianaiResponse: string; - chatId?: string; - streamCallback?: (chunk: string) => Promise; -}): Promise { - try { - if (streamCallback) { - const messages = [ - await systemMessage.format({ brianai_answer: brianaiResponse }), - { role: "user", content: userQuery }, - ]; - - let fullResponse = ""; - await agent.invoke(messages, { - callbacks: [ - { - handleLLMNewToken: async (token: string) => { - fullResponse += token; - await streamCallback(token); - }, - }, - ], - }); - - return fullResponse; - } - - const response = await app.invoke( - { - messages: [ - await askAgentPromptTemplate.format({ - brianai_answer: brianaiResponse, - user_query: userQuery, - }), - ], - }, - { - configurable: { - thread_id: chatId || "1", - additional_args: { chatId }, - }, - } - ); - - return response.messages[response.messages.length - 1].content as string; - } catch (error) { - console.error("OpenAI Error:", error); - return "Sorry, I am unable to process your request at the moment."; - } +async function queryOpenAI({ userQuery, brianaiResponse, chatId, streamCallback }: { userQuery: string; brianaiResponse: string; chatId?: string; streamCallback?: (chunk: string) => Promise }): Promise { + try { + if (streamCallback) { + const messages = [await systemMessage.format({ brianai_answer: brianaiResponse }), { role: "user", content: userQuery }]; + + let fullResponse = ""; + await agent.invoke(messages, { + callbacks: [ + { + handleLLMNewToken: async (token: string) => { + fullResponse += token; + await streamCallback(token); + }, + }, + ], + }); + + return fullResponse; + } + + const response = await app.invoke( + { + messages: [ + await askAgentPromptTemplate.format({ + brianai_answer: brianaiResponse, + user_query: userQuery, + }), + ], + }, + { + configurable: { + thread_id: chatId || "1", + additional_args: { chatId }, + }, + } + ); + + return response.messages[response.messages.length - 1].content as string; + } catch (error) { + console.error("OpenAI Error:", error); + return "Sorry, I am unable to process your request at the moment."; + } } // Function for investment recommendations -async function generateInvestmentRecommendations( - userPreferences: UserPreferences, - tokens: any[], - yields: any[], - messages: any[] = [] -): Promise { - try { - const conversationHistory = messages - .map((msg) => `${msg.role}: ${msg.content}`) - .join("\n"); - - const formattedPrompt = await investmentRecommendationPromptTemplate.format( - { - INVESTMENT_RECOMMENDATION_PROMPT, - userPreferences: JSON.stringify(userPreferences), - tokens: JSON.stringify(tokens), - yields: JSON.stringify(yields), - conversationHistory, - } - ); - - const jsonOutputParser = new StringOutputParser(); - const response = await agent.pipe(jsonOutputParser).invoke(formattedPrompt); - const recommendationData = JSON.parse(response); - - if (!recommendationData.data?.pools || !recommendationData.data?.strategy) { - throw new Error("Invalid recommendation format"); - } - - return { - solver: recommendationData.solver || "OpenAI-Investment-Advisor", - type: "recommendation", - extractedParams: { - riskTolerance: - recommendationData.extractedParams.riskTolerance || - userPreferences.riskTolerance, - investmentHorizon: - recommendationData.extractedParams.investmentHorizon || - userPreferences.investmentHorizon, - preferredAssets: - recommendationData.extractedParams.preferredAssets || - userPreferences.preferredAssets, - preferredChains: - recommendationData.extractedParams.preferredChains || - userPreferences.preferredChains, - }, - data: recommendationData.data, - }; - } catch (error) { - console.error("Error generating investment recommendations:", error); - throw error; - } +async function generateInvestmentRecommendations(userPreferences: UserPreferences, tokens: any[], yields: any[], messages: any[] = []): Promise { + try { + const conversationHistory = messages.map((msg) => `${msg.role}: ${msg.content}`).join("\n"); + + const formattedPrompt = await investmentRecommendationPromptTemplate.format({ + INVESTMENT_RECOMMENDATION_PROMPT, + userPreferences: JSON.stringify(userPreferences), + tokens: JSON.stringify(tokens), + yields: JSON.stringify(yields), + conversationHistory, + }); + + const jsonOutputParser = new StringOutputParser(); + const response = await agent.pipe(jsonOutputParser).invoke(formattedPrompt); + const recommendationData = JSON.parse(response); + + if (!recommendationData.data?.pools || !recommendationData.data?.strategy) { + throw new Error("Invalid recommendation format"); + } + + return { + solver: recommendationData.solver || "OpenAI-Investment-Advisor", + type: "recommendation", + extractedParams: { + riskTolerance: recommendationData.extractedParams.riskTolerance || userPreferences.riskTolerance, + investmentHorizon: recommendationData.extractedParams.investmentHorizon || userPreferences.investmentHorizon, + preferredAssets: recommendationData.extractedParams.preferredAssets || userPreferences.preferredAssets, + preferredChains: recommendationData.extractedParams.preferredChains || userPreferences.preferredChains, + }, + data: recommendationData.data, + }; + } catch (error) { + console.error("Error generating investment recommendations:", error); + throw error; + } +} + +async function callBrianAI(prompt: string, address: string, chainId: string, messages: any[] = []): Promise { + try { + // Format conversation history in the way Brian API expects + const conversationHistory = messages.map((msg) => ({ + sender: msg.role === "user" ? "user" : "brian", + content: msg.content, + })); + + // Prepare the request payload + const payload = { + prompt, + address, + chainId, + messages: conversationHistory, + }; + + // Call the Brian AI API + const response = await fetch(BRIAN_TRANSACTION_API_URL, { + method: "POST", + headers: { + "Content-Type": "application/json", + "x-brian-api-key": process.env.NEXT_PUBLIC_BRIAN_API_KEY || "", + }, + body: JSON.stringify(payload), + }); + + if (!response.ok) { + const errorData = await response.json(); + throw new Error(`Brian AI API error: ${errorData.error || response.statusText}`); + } + + console.log("response from BRIAN", response); + + const brianData = await response.json(); + + if (!brianData.result || !brianData.result.length) { + throw new Error("No result returned from Brian AI"); + } + + // Convert Brian API response to our BrianResponse format + const transactionResult = brianData.result[0]; + // console.log("transactionResult from BRIAN", transactionResult); + + // Map the response to our expected format + const brianResponse: BrianResponse = { + solver: transactionResult.solver, + action: transactionResult.action, + type: transactionResult.type, + extractedParams: transactionResult.extractedParams, + data: transactionResult.data, + }; + + return brianResponse; + } catch (error) { + console.error("Error calling Brian AI:", error); + throw error; + } } // Main API route handler export async function POST(request: Request) { - try { - const { - prompt, - address, - messages = [], - chatId, - stream = false, - userPreferences, - chainId = "4012", // Default Starknet chain ID - } = await request.json(); - - if (!prompt || !address) { - return NextResponse.json( - { error: "Missing required parameters (prompt or address)" }, - { status: 400 } - ); - } - - const userId = address || "0x0"; - await getOrCreateUser(userId); - - let currentChatId = chatId; - if (!currentChatId) { - const newChat = await createOrGetChat(userId); - currentChatId = newChat.id; - } - - // Process messages - const uniqueMessages = messages - .filter((msg: any) => msg.sender === "user") - .reduce((acc: any[], curr: any) => { - if (!acc.some((msg) => msg.content === curr.content)) { - acc.push({ - role: "user", - content: curr.content, - }); - } - return acc; - }, []); - - // Store user message - await storeMessage({ - content: uniqueMessages, - chatId: currentChatId, - userId, - }); - - // Determine if this is a transaction intent or a question - const isTransaction = await isTransactionIntent(prompt, uniqueMessages); - - // Handle investment recommendations - if ( - prompt.toLowerCase().includes("recommend") || - prompt.toLowerCase().includes("invest") - ) { - const tokens = await fetchTokenData(); - const yields = await fetchYieldData(); - const recommendations = await generateInvestmentRecommendations( - userPreferences, - tokens, - yields, - uniqueMessages - ); - - // Store the recommendations in the chat history - await storeMessage({ - content: [ - { - role: "assistant", - content: recommendations.data.description, - recommendationData: { - ...recommendations, - timestamp: new Date().toISOString(), - userId, - chatId: currentChatId, - }, - }, - ], - chatId: currentChatId, - userId, - }); - - const response = { - content: recommendations.data.description, - recommendations, - chatId: currentChatId, - }; - - if (stream) { - // Handle streaming response - const encoder = new TextEncoder(); - const stream = new TransformStream(); - const writer = stream.writable.getWriter(); - // Stream the response - await writer.write( - encoder.encode(`data: ${JSON.stringify(response)}\n\n`) - ); - await writer.close(); - return new Response(stream.readable, { - headers: { - "Content-Type": "text/event-stream", - "Cache-Control": "no-cache", - Connection: "keep-alive", - }, - }); - } - - return NextResponse.json(response); - } - // Handle transaction intents - else if (isTransaction) { - try { - const transactionIntent = await getTransactionIntentFromOpenAI( - prompt, - address, - chainId, - uniqueMessages - ); - - const processedTx = await transactionProcessor.processTransaction( - transactionIntent - ); - - if (["deposit", "withdraw"].includes(transactionIntent.action)) { - processedTx.receiver = address; - } - - const transaction = await storeTransaction( - userId, - transactionIntent.action, - { - ...processedTx, - chainId, - originalIntent: transactionIntent, - } - ); - - await storeMessage({ - content: [ - { - role: "assistant", - content: processedTx.description || JSON.stringify(processedTx), - transactionId: transaction.id, - }, - ], - chatId: currentChatId, - userId, - }); - - return NextResponse.json({ - result: [ - { - data: { - description: processedTx.description, - transaction: { - type: processedTx.action, - data: { - transactions: processedTx.transactions, - fromToken: processedTx.fromToken, - toToken: processedTx.toToken, - fromAmount: processedTx.fromAmount, - toAmount: processedTx.toAmount, - receiver: processedTx.receiver, - gasCostUSD: processedTx.estimatedGas, - solver: processedTx.solver, - protocol: processedTx.protocol, - bridge: processedTx.bridge, - }, - }, - }, - conversationHistory: messages, - chatId: currentChatId, - }, - ], - }); - } catch (error) { - console.error("Transaction processing error:", error); - // If transaction processing fails, fall back to BrianAI - const response = await queryBrianAI(prompt, currentChatId); - - await storeMessage({ - content: [ - { - role: "assistant", - content: response, - }, - ], - chatId: currentChatId, - userId, - }); - - return NextResponse.json({ - answer: response, - chatId: currentChatId, - }); - } - } - // Handle general questions - else { - // Non-streaming response - const response = await queryBrianAI(prompt, currentChatId); - - if (!response) { - throw new Error("Unexpected API response format"); - } - - await storeMessage({ - content: [ - { - role: "assistant", - content: response, - }, - ], - chatId: currentChatId, - userId, - }); - - return NextResponse.json({ - answer: response, - chatId: currentChatId, - }); - } - } catch (error: any) { - console.error("Error:", error); - if (error.code === "P2003") { - return NextResponse.json( - { - error: "User authentication required", - details: "Please ensure you are logged in.", - }, - { status: 401 } - ); - } - - return NextResponse.json( - { error: "Unable to process request", details: error.message }, - { status: 500 } - ); - } -} \ No newline at end of file + try { + const { + prompt, + address, + messages = [], + chatId, + stream = false, + userPreferences, + chainId = "4012", // Default Starknet chain ID + } = await request.json(); + + if (!prompt || !address) { + return NextResponse.json({ error: "Missing required parameters (prompt or address)" }, { status: 400 }); + } + + const userId = address || "0x0"; + await getOrCreateUser(userId); + + let currentChatId = chatId; + if (!currentChatId) { + const newChat = await createOrGetChat(userId); + currentChatId = newChat.id; + } + + // Process messages + const uniqueMessages = messages + .filter((msg: any) => msg.sender === "user") + .reduce((acc: any[], curr: any) => { + if (!acc.some((msg) => msg.content === curr.content)) { + acc.push({ + role: "user", + content: curr.content, + }); + } + return acc; + }, []); + + // Store user message + await storeMessage({ + content: uniqueMessages, + chatId: currentChatId, + userId, + }); + + // Determine if this is a transaction intent or a question + const isTransaction = await isTransactionIntent(prompt, uniqueMessages); + + // Handle investment recommendations + if (prompt.toLowerCase().includes("recommend") || prompt.toLowerCase().includes("invest")) { + const tokens = await fetchTokenData(); + const yields = await fetchYieldData(); + const recommendations = await generateInvestmentRecommendations(userPreferences, tokens, yields, uniqueMessages); + + // Store the recommendations in the chat history + await storeMessage({ + content: [ + { + role: "assistant", + content: recommendations.data.description, + recommendationData: { + ...recommendations, + timestamp: new Date().toISOString(), + userId, + chatId: currentChatId, + }, + }, + ], + chatId: currentChatId, + userId, + }); + + const response = { + content: recommendations.data.description, + recommendations, + chatId: currentChatId, + }; + + if (stream) { + // Handle streaming response + const encoder = new TextEncoder(); + const stream = new TransformStream(); + const writer = stream.writable.getWriter(); + // Stream the response + await writer.write(encoder.encode(`data: ${JSON.stringify(response)}\n\n`)); + await writer.close(); + return new Response(stream.readable, { + headers: { + "Content-Type": "text/event-stream", + "Cache-Control": "no-cache", + Connection: "keep-alive", + }, + }); + } + + return NextResponse.json(response); + } + // Handle transaction intents + else if (isTransaction) { + try { + // const transactionIntent = await getTransactionIntentFromOpenAI(prompt, address, chainId, uniqueMessages); + const transactionIntent = await callBrianAI(prompt, address, chainId, messages); + + const processedTx = await transactionProcessor.processTransaction(transactionIntent); + + if (["deposit", "withdraw"].includes(transactionIntent.action)) { + processedTx.receiver = address; + } + + const transaction = await storeTransaction(userId, transactionIntent.action, { + ...processedTx, + chainId, + originalIntent: transactionIntent, + }); + + await storeMessage({ + content: [ + { + role: "assistant", + content: processedTx.description || JSON.stringify(processedTx), + transactionId: transaction.id, + }, + ], + chatId: currentChatId, + userId, + }); + + return NextResponse.json({ + result: [ + { + data: { + description: processedTx.description, + transaction: { + type: processedTx.action, + data: { + transactions: processedTx.transactions, + fromToken: processedTx.fromToken, + toToken: processedTx.toToken, + fromAmount: processedTx.fromAmount, + toAmount: processedTx.toAmount, + receiver: processedTx.receiver, + gasCostUSD: processedTx.estimatedGas, + solver: processedTx.solver, + protocol: processedTx.protocol, + bridge: processedTx.bridge, + }, + }, + }, + conversationHistory: messages, + chatId: currentChatId, + }, + ], + }); + } catch (error: string | any) { + console.error("Transaction processing error:", error); + // If transaction processing fails, fall back to BrianAI + // const response = await queryBrianAI(prompt, currentChatId); + + // await storeMessage({ + // content: [ + // { + // role: "assistant", + // content: response, + // }, + // ], + // chatId: currentChatId, + // userId, + // }); + + return NextResponse.json({ + answer: error.message, + chatId: currentChatId, + }); + } + } + // Handle general questions + else { + // Non-streaming response + const response = await queryBrianAI(prompt, currentChatId); + + if (!response) { + throw new Error("Unexpected API response format"); + } + + await storeMessage({ + content: [ + { + role: "assistant", + content: response, + }, + ], + chatId: currentChatId, + userId, + }); + + return NextResponse.json({ + answer: response, + chatId: currentChatId, + }); + } + } catch (error: any) { + console.error("Error:", error); + if (error.code === "P2003") { + return NextResponse.json( + { + error: "User authentication required", + details: "Please ensure you are logged in.", + }, + { status: 401 } + ); + } + + return NextResponse.json({ error: "Unable to process request", details: error.message }, { status: 500 }); + } +} diff --git a/client/components/ui/swap.tsx b/client/components/ui/swap.tsx index 5121b29a..dfaece64 100644 --- a/client/components/ui/swap.tsx +++ b/client/components/ui/swap.tsx @@ -1,136 +1,216 @@ "use client"; -import React from "react"; +import React, { useEffect } from "react"; import Image from "next/image"; import { useState } from "react"; import { ArrowDownUp, ChevronDown } from "lucide-react"; import TokensModal from "./tokens-modal"; -import { CryptoCoin, CoinsLogo} from "../../types/crypto-coin"; +import { CryptoCoin, CoinsLogo } from "../../types/crypto-coin"; +import { useAccount } from "@starknet-react/core"; +import useDebounce from "../useDebounce"; interface SwapProps { - setSelectedCommand: React.Dispatch>; + setSelectedCommand: React.Dispatch>; } +const Swap: React.FC = ({ setSelectedCommand }) => { + const { address, account } = useAccount(); + const [fromAmount, setFromAmount] = useState(""); + const [toAmount, setToAmount] = useState(""); + const [fromCoin, setFromCoin] = useState(CoinsLogo[0]); + const [toCoin, setToCoin] = useState(CoinsLogo[3]); + const [showModal, setShowModal] = useState(false); + const [selectingCoinFor, setSelectingCoinFor] = useState<"from" | "to">("from"); + const [isLoading, setIsLoading] = useState(false); + /* eslint-disable */ + const [transactionDetails, setTransactionDetails] = useState(null); + const [txHash, setTxHash] = useState(null); -const Swap: React.FC = ({ setSelectedCommand }) => { - const [fromAmount, setFromAmount] = useState(""); - const [toAmount, setToAmount] = useState(""); - const [fromCoin, setFromCoin] = useState(CoinsLogo[0]); - const [toCoin, setToCoin] = useState(CoinsLogo[3]); - const [showModal, setShowModal] = useState(false); - const [selectingCoinFor, setSelectingCoinFor] = useState<"from" | "to">( - "from" - ); - - const openModal = (type: "from" | "to") => { - setSelectingCoinFor(type); - setShowModal(true); - }; - - const handleCoinSelect = (coin: CryptoCoin) => { - if (selectingCoinFor === "from") { - setFromCoin(coin); - } else { - setToCoin(coin); - } - setShowModal(false); - }; - - - const handleInputSwap = () => { - setFromAmount(toAmount); - setToAmount(fromAmount); - setFromCoin(toCoin); - setToCoin(fromCoin); - }; - return ( -
-
-
- -
-

- Swap Token -

-

Total Balance

-

$11,485.30

-
-
- -
-
-

From

- setFromAmount(e.target.value)} - className={`text-xl font-bold text-black outline-none bg-transparent w-full appearance-none [&::-webkit-inner-spin-button]:appearance-none [&::-webkit-outer-spin-button]:appearance-none`} - /> -
-
openModal("from")} - > - {fromCoin.name} -

{fromCoin.name}

- -
-
- -
- - - -
- -
-
-

To

- setToAmount(e.target.value)} - readOnly={true} - className={`text-xl font-bold text-black outline-none bg-transparent w-full appearance-none [&::-webkit-inner-spin-button]:appearance-none [&::-webkit-outer-spin-button]:appearance-none`} - /> -
-
openModal("to")} - > - {toCoin.name} -

{toCoin.name}

- -
-
- -
- -
- {showModal && ( - - )} -
-
- ); + // Debounce the fromAmount with a 1 second delay + const debouncedFromAmount = useDebounce(fromAmount, 1000); + + // Effect to trigger handleSwap when the debounced value changes + useEffect(() => { + if (debouncedFromAmount) { + handleSwap(debouncedFromAmount); + } + }, [debouncedFromAmount]); + + const openModal = (type: "from" | "to") => { + setSelectingCoinFor(type); + setShowModal(true); + }; + + const handleCoinSelect = (coin: CryptoCoin) => { + if (selectingCoinFor === "from") { + setFromCoin(coin); + } else { + setToCoin(coin); + } + setShowModal(false); + }; + + const handleInputSwap = () => { + setFromAmount(toAmount); + setToAmount(fromAmount); + setFromCoin(toCoin); + setToCoin(fromCoin); + }; + + const handleSwap = async (value?: string) => { + const prompt = `swap ${value} ${fromCoin.name} to ${toCoin.name}`; + const messages = [ + { + role: "user", + content: prompt, + }, + ]; + const requestBody = { + prompt, + address: address || "0x0", + messages, + chainId: "4012", + }; + setIsLoading(true); + const response = await fetch("/api/transactions", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(requestBody), + }); + + const data = await response.json(); + + if (data?.result) { + setIsLoading(false); + const resTx = data?.result; + const amt = resTx?.[0]?.data?.transaction?.data?.toAmount; + setTransactionDetails(resTx?.[0].data.transaction.data.transactions); + setToAmount(amt); + } else { + setIsLoading(false); + } + }; + + const handleExecuteTransaction = async () => { + try { + const txDetails = transactionDetails; + const result = await account?.execute(txDetails); + + if (result?.transaction_hash) { + setTxHash(result.transaction_hash); + console.log("Transaction submitted:", txHash); + } + } catch (error) { + console.error("Transaction failed:", error); + } + }; + + return ( +
+
+
+ +
+

Swap Token

+

Total Balance

+

$11,485.30

+
+
+ +
+
+

From

+ setFromAmount(e.target.value)} + className={`text-xl font-bold text-black outline-none bg-transparent w-full appearance-none [&::-webkit-inner-spin-button]:appearance-none [&::-webkit-outer-spin-button]:appearance-none`} + /> +
+
openModal("from")}> + {fromCoin.name} +

{fromCoin.name}

+ +
+
+ +
+ + + +
+ +
+
+

To

+ setToAmount(e.target.value)} + readOnly={true} + className={`text-xl font-bold text-black outline-none bg-transparent w-full appearance-none [&::-webkit-inner-spin-button]:appearance-none [&::-webkit-outer-spin-button]:appearance-none`} + /> +
+
openModal("to")}> + {toCoin.name} +

{toCoin.name}

+ +
+
+ +
+ {transactionDetails ? ( + + ) : ( + + )} +
+ + {txHash &&
Transaction submitted: {txHash}
} + {showModal && ( + + )} +
+
+ ); }; export default Swap; diff --git a/client/components/useDebounce.ts b/client/components/useDebounce.ts new file mode 100644 index 00000000..a7947dd9 --- /dev/null +++ b/client/components/useDebounce.ts @@ -0,0 +1,22 @@ +import { useState, useEffect } from "react"; + +// Custom hook for debouncing values +function useDebounce(value: T, delay: number): T { + const [debouncedValue, setDebouncedValue] = useState(value); + + useEffect(() => { + // Set a timer to update the debounced value after the specified delay + const timer = setTimeout(() => { + setDebouncedValue(value); + }, delay); + + // Clear the timer if the value changes before the delay has passed + return () => { + clearTimeout(timer); + }; + }, [value, delay]); + + return debouncedValue; +} + +export default useDebounce; diff --git a/client/lib/transaction/processor.ts b/client/lib/transaction/processor.ts index 16ffb432..4c255d88 100644 --- a/client/lib/transaction/processor.ts +++ b/client/lib/transaction/processor.ts @@ -1,173 +1,201 @@ // lib/transaction/processor.ts -import { Provider } from 'starknet'; -import type { BrianResponse, ProcessedTransaction, BrianTransactionData, TransactionAction } from './types'; -import { STARKNET_RPC_URL } from './config'; -import { - SwapHandler, - TransferHandler, - NostraDepositHandler, - NostraWithdrawHandler, - BridgeHandler, - type BaseTransactionHandler -} from './handlers'; +import { Provider } from "starknet"; +import type { BrianResponse, ProcessedTransaction, BrianTransactionData, TransactionAction } from "./types"; +import { STARKNET_RPC_URL } from "./config"; +import { SwapHandler, TransferHandler, NostraDepositHandler, NostraWithdrawHandler, BridgeHandler, type BaseTransactionHandler } from "./handlers"; export class TransactionProcessor { - private provider: Provider; - private handlers: Record; - - constructor() { - this.provider = new Provider({ nodeUrl: STARKNET_RPC_URL }); - - this.handlers = { - 'swap': new SwapHandler(), - 'transfer': new TransferHandler(), - 'deposit': new NostraDepositHandler(), - 'withdraw': new NostraWithdrawHandler(), - 'bridge': new BridgeHandler(process.env.LAYERSWAP_API_KEY || '') - }; - } - - validateBrianResponse(response: BrianResponse): void { - if (!response) { - throw new Error('Invalid response: Response is null or undefined'); - } - - if (!response.action) { - throw new Error('Invalid response: Missing action'); - } - - // For deposit/withdraw, validate required parameters - if (['deposit', 'withdraw'].includes(response.action)) { - if (!response.extractedParams?.protocol) { - throw new Error(`${response.action} requires a protocol parameter`); - } - - if (!response.extractedParams.token1) { - throw new Error(`${response.action} requires a token parameter`); - } - - if (!response.extractedParams.amount) { - throw new Error(`${response.action} requires an amount parameter`); - } - } else if (response.action === 'bridge') { - - if (!response.extractedParams?.token1) { - throw new Error('Bridge requires a source token parameter'); - } - if (!response.extractedParams?.token2) { - throw new Error('Bridge requires a destination token parameter'); - } - if (!response.extractedParams?.chain) { - throw new Error('Bridge requires a source chain parameter'); - } - if (!response.extractedParams?.dest_chain) { - throw new Error('Bridge requires a destination chain parameter'); - } - if (!response.extractedParams?.amount) { - throw new Error('Bridge requires an amount parameter'); - } - } else { - // For other actions, validate data and steps - if (!response.data?.steps) { - throw new Error('Invalid response: Missing steps array'); - } - } - } - - generateDescription(response: BrianResponse): string { - const params = response.extractedParams; - const action: TransactionAction = response.action; - - switch (action) { - case 'bridge': - return `Bridge ${params?.amount} ${params?.token1?.toUpperCase()} from ${params?.chain?.toUpperCase()} to ${params?.token2?.toUpperCase()} on ${params?.dest_chain?.toUpperCase()}`; - case 'deposit': - return `Deposit ${params?.amount} ${params?.token1?.toUpperCase()} to ${params?.protocol?.toUpperCase()}`; - case 'withdraw': - return `Withdraw ${params?.amount} ${params?.token1?.toUpperCase()} from ${params?.protocol?.toUpperCase()}`; - case 'transfer': - return `Transfer ${params?.amount} ${params?.token1?.toUpperCase()} to ${params?.address}`; - case 'swap': - return `Swap ${params?.amount} ${params?.token1?.toUpperCase()} for ${params?.token2?.toUpperCase()}`; - default: { - const actionStr = (action as string).toString(); - return `${actionStr.charAt(0).toUpperCase() + actionStr.slice(1)} transaction`; - } - } - } - - createTransactionData(response: BrianResponse): BrianTransactionData { - const action: TransactionAction = response.action; - - if (['deposit', 'withdraw'].includes(action) && response.extractedParams) { - return { - description: this.generateDescription(response), - steps: [], - fromAmount: response.extractedParams.amount, - toAmount: response.extractedParams.amount, - receiver: response.extractedParams.address, - protocol: response.extractedParams.protocol - }; - } - - if (action === 'bridge' && response.extractedParams) { - const params = response.extractedParams; - return { - description: this.generateDescription(response), - steps: [], - fromAmount: params.amount, - toAmount: params.amount, - bridge: { - sourceNetwork: params.chain, - destinationNetwork: params.dest_chain || '', - sourceToken: params.token1, - destinationToken: params.token2, - amount: parseFloat(params.amount), - sourceAddress: params.address || '', - destinationAddress: params.destinationAddress || params.address || '' - } - }; - } - - return response.data; - } - - - async processTransaction(response: BrianResponse): Promise { - try { - this.validateBrianResponse(response); - - const handler = this.handlers[response.action]; - if (!handler) { - throw new Error(`Unsupported action type: ${response.action}`); - } - - const transactionData = this.createTransactionData(response); - - const transactions = await handler.processSteps(transactionData, response.extractedParams); - - const description = this.generateDescription(response); - - return { - success: true, - description, - transactions, - action: response.action, - solver: response.solver, - fromToken: transactionData.fromToken, - toToken: transactionData.toToken, - fromAmount: response.extractedParams?.amount, - toAmount: response.extractedParams?.amount, - receiver: response.extractedParams?.address, - estimatedGas: '0', - protocol: response.extractedParams?.protocol, - bridge: transactionData.bridge // Include bridge data if present - }; - } catch (error) { - console.error('Error processing transaction:', error); - throw error; - } - } + private provider: Provider; + private handlers: Record; + + constructor() { + this.provider = new Provider({ nodeUrl: STARKNET_RPC_URL }); + + this.handlers = { + swap: new SwapHandler(), + transfer: new TransferHandler(), + deposit: new NostraDepositHandler(), + withdraw: new NostraWithdrawHandler(), + bridge: new BridgeHandler(process.env.LAYERSWAP_API_KEY || ""), + }; + } + + validateBrianResponse(response: BrianResponse): void { + if (!response) { + throw new Error("Invalid response: Response is null or undefined"); + } + + if (!response.action) { + throw new Error("Invalid response: Missing action"); + } + + // For deposit/withdraw, validate required parameters + if (["deposit", "withdraw"].includes(response.action)) { + if (!response.extractedParams?.protocol) { + throw new Error(`${response.action} requires a protocol parameter`); + } + + if (!response.extractedParams.token1) { + throw new Error(`${response.action} requires a token parameter`); + } + + if (!response.extractedParams.amount) { + throw new Error(`${response.action} requires an amount parameter`); + } + } else if (response.action === "bridge") { + if (!response.extractedParams?.token1) { + throw new Error("Bridge requires a source token parameter"); + } + if (!response.extractedParams?.token2) { + throw new Error("Bridge requires a destination token parameter"); + } + if (!response.extractedParams?.chain) { + throw new Error("Bridge requires a source chain parameter"); + } + if (!response.extractedParams?.dest_chain) { + throw new Error("Bridge requires a destination chain parameter"); + } + if (!response.extractedParams?.amount) { + throw new Error("Bridge requires an amount parameter"); + } + } else { + // For other actions, validate data and steps + if (!response.data?.steps) { + throw new Error("Invalid response: Missing steps array"); + } + } + } + + generateDescription(response: BrianResponse): string { + const params = response.extractedParams; + const action: TransactionAction = response.action; + + switch (action) { + case "bridge": + return `Bridge ${params?.amount} ${params?.token1?.toUpperCase()} from ${params?.chain?.toUpperCase()} to ${params?.token2?.toUpperCase()} on ${params?.dest_chain?.toUpperCase()}`; + case "deposit": + return `Deposit ${params?.amount} ${params?.token1?.toUpperCase()} to ${params?.protocol?.toUpperCase()}`; + case "withdraw": + return `Withdraw ${params?.amount} ${params?.token1?.toUpperCase()} from ${params?.protocol?.toUpperCase()}`; + case "transfer": + return `Transfer ${params?.amount} ${params?.token1?.toUpperCase()} to ${params?.address}`; + case "swap": + return `Swap ${params?.amount} ${params?.token1?.toUpperCase()} for ${params?.token2?.toUpperCase()}`; + default: { + const actionStr = (action as string).toString(); + return `${actionStr.charAt(0).toUpperCase() + actionStr.slice(1)} transaction`; + } + } + } + + createTransactionData(response: BrianResponse): BrianTransactionData { + const action: TransactionAction = response.action; + + if (["swap"].includes(action) && response.extractedParams) { + return { + description: this.generateDescription(response), + steps: response.data.steps, + fromAmount: response.extractedParams.amount, + toAmount: response.data.toAmount, + receiver: response.extractedParams.address, + protocol: response.extractedParams.protocol, + }; + } + + if (["deposit", "withdraw"].includes(action) && response.extractedParams) { + return { + description: this.generateDescription(response), + steps: [], + fromAmount: response.extractedParams.amount, + toAmount: response.data.toAmount, + receiver: response.extractedParams.address, + protocol: response.extractedParams.protocol, + }; + } + + if (action === "bridge" && response.extractedParams) { + const params = response.extractedParams; + return { + description: this.generateDescription(response), + steps: [], + fromAmount: params.amount, + toAmount: params.amount, + bridge: { + sourceNetwork: params.chain, + destinationNetwork: params.dest_chain || "", + sourceToken: params.token1, + destinationToken: params.token2, + amount: parseFloat(params.amount), + sourceAddress: params.address || "", + destinationAddress: params.destinationAddress || params.address || "", + }, + }; + } + + return response.data; + } + formatTokenAmount = (rawAmount: number, decimals: number) => { + if (!rawAmount || !decimals) return "0"; + + // Convert to string if it's not already + const amountStr = rawAmount.toString(); + + // If the amount is shorter than decimals, we need to pad with zeros + if (amountStr.length <= decimals) { + const padding = "0".repeat(decimals - amountStr.length + 1); + return `0.${padding}${amountStr}`.replace(/\.?0+$/, ""); + } + + // Insert decimal point at the right position + const decimalPosition = amountStr.length - decimals; + const wholeAmount = amountStr.substring(0, decimalPosition); + const fractionalAmount = amountStr.substring(decimalPosition); + + // Remove trailing zeros + const formattedFraction = fractionalAmount.replace(/0+$/, ""); + + return formattedFraction ? `${wholeAmount}.${formattedFraction}` : wholeAmount; + }; + + async processTransaction(response: BrianResponse): Promise { + try { + this.validateBrianResponse(response); + + const handler = this.handlers[response.action]; + if (!handler) { + throw new Error(`Unsupported action type: ${response.action}`); + } + + const transactionData = this.createTransactionData(response); + + const transactions = await handler.processSteps(transactionData, response.extractedParams); + + const description = this.generateDescription(response); + + const toAmount = transactionData.toAmount || 0; + + const res = this.formatTokenAmount(Number(toAmount), response.data.toToken?.decimals || 0); + + return { + success: true, + description, + transactions, + action: response.action, + solver: response.solver, + fromToken: transactionData.fromToken, + toToken: transactionData.toToken, + fromAmount: response.extractedParams?.amount, + toAmount: res, + receiver: response.extractedParams?.address, + estimatedGas: "0", + protocol: response.extractedParams?.protocol, + bridge: transactionData.bridge, // Include bridge data if present + }; + } catch (error) { + console.error("Error processing transaction:", error); + throw error; + } + } } export const transactionProcessor = new TransactionProcessor(); diff --git a/client/prisma/migrations/20250116191514_starkfinder01/migration.sql b/client/prisma/migrations/20250116191514_starkfinder01/migration.sql deleted file mode 100644 index 77be1b24..00000000 --- a/client/prisma/migrations/20250116191514_starkfinder01/migration.sql +++ /dev/null @@ -1,45 +0,0 @@ --- CreateEnum -CREATE TYPE "Role" AS ENUM ('USER', 'ASSISTANT'); - --- CreateTable -CREATE TABLE "User" ( - "id" TEXT NOT NULL, - "email" TEXT, - "name" TEXT, - - CONSTRAINT "User_pkey" PRIMARY KEY ("id") -); - --- CreateTable -CREATE TABLE "Chat" ( - "id" TEXT NOT NULL, - "title" TEXT, - "metadata" JSONB, - "userId" TEXT, - - CONSTRAINT "Chat_pkey" PRIMARY KEY ("id") -); - --- CreateTable -CREATE TABLE "Message" ( - "id" TEXT NOT NULL, - "content" TEXT NOT NULL, - "metadata" JSONB, - "replyTo" TEXT, - "chatId" TEXT NOT NULL, - "userId" TEXT NOT NULL, - - CONSTRAINT "Message_pkey" PRIMARY KEY ("id") -); - --- CreateIndex -CREATE UNIQUE INDEX "User_email_key" ON "User"("email"); - --- AddForeignKey -ALTER TABLE "Chat" ADD CONSTRAINT "Chat_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE; - --- AddForeignKey -ALTER TABLE "Message" ADD CONSTRAINT "Message_chatId_fkey" FOREIGN KEY ("chatId") REFERENCES "Chat"("id") ON DELETE RESTRICT ON UPDATE CASCADE; - --- AddForeignKey -ALTER TABLE "Message" ADD CONSTRAINT "Message_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE; diff --git a/client/prisma/migrations/20250121073900_starkfinder01/migration.sql b/client/prisma/migrations/20250121073900_starkfinder01/migration.sql deleted file mode 100644 index 7903a59d..00000000 --- a/client/prisma/migrations/20250121073900_starkfinder01/migration.sql +++ /dev/null @@ -1,9 +0,0 @@ -/* - Warnings: - - - The `content` column on the `Message` table would be dropped and recreated. This will lead to data loss if there is data in the column. - -*/ --- AlterTable -ALTER TABLE "Message" DROP COLUMN "content", -ADD COLUMN "content" JSONB[]; diff --git a/client/prisma/migrations/20250224193016_starkfinder01/migration.sql b/client/prisma/migrations/20250224193016_starkfinder01/migration.sql deleted file mode 100644 index 7c637cb9..00000000 --- a/client/prisma/migrations/20250224193016_starkfinder01/migration.sql +++ /dev/null @@ -1,74 +0,0 @@ -/* - Warnings: - - - A unique constraint covering the columns `[address]` on the table `User` will be added. If there are existing duplicate values, this will fail. - -*/ --- CreateEnum -CREATE TYPE "ChatType" AS ENUM ('GENERAL', 'TRANSACTION', 'ASK'); - --- CreateEnum -CREATE TYPE "TxType" AS ENUM ('SWAP', 'TRANSFER', 'BRIDGE', 'DEPOSIT', 'WITHDRAW'); - --- CreateEnum -CREATE TYPE "TxStatus" AS ENUM ('PENDING', 'COMPLETED', 'FAILED'); - --- AlterTable -ALTER TABLE "Chat" ADD COLUMN "type" "ChatType" NOT NULL DEFAULT 'GENERAL'; - --- AlterTable -ALTER TABLE "Message" ADD COLUMN "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP; - --- AlterTable -ALTER TABLE "User" ADD COLUMN "address" TEXT; - --- CreateTable -CREATE TABLE "Transaction" ( - "id" TEXT NOT NULL, - "type" "TxType" NOT NULL, - "status" "TxStatus" NOT NULL DEFAULT 'PENDING', - "metadata" JSONB, - "hash" TEXT, - "userId" TEXT NOT NULL, - "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, - "completedAt" TIMESTAMP(3), - - CONSTRAINT "Transaction_pkey" PRIMARY KEY ("id") -); - --- CreateTable -CREATE TABLE "GeneratedContract" ( - "id" TEXT NOT NULL, - "name" TEXT NOT NULL, - "sourceCode" TEXT NOT NULL, - "userId" TEXT NOT NULL, - "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, - - CONSTRAINT "GeneratedContract_pkey" PRIMARY KEY ("id") -); - --- CreateTable -CREATE TABLE "DeployedContract" ( - "id" TEXT NOT NULL, - "name" TEXT NOT NULL, - "sourceCode" TEXT NOT NULL, - "userId" TEXT NOT NULL, - "contractAddress" TEXT NOT NULL, - "classHash" TEXT NOT NULL, - "transactionHash" TEXT NOT NULL, - "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, - - CONSTRAINT "DeployedContract_pkey" PRIMARY KEY ("id") -); - --- CreateIndex -CREATE UNIQUE INDEX "User_address_key" ON "User"("address"); - --- AddForeignKey -ALTER TABLE "Transaction" ADD CONSTRAINT "Transaction_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE; - --- AddForeignKey -ALTER TABLE "GeneratedContract" ADD CONSTRAINT "GeneratedContract_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE; - --- AddForeignKey -ALTER TABLE "DeployedContract" ADD CONSTRAINT "DeployedContract_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE; diff --git a/client/prisma/migrations/20250223091422_askfs/migration.sql b/client/prisma/migrations/20250302165720_starkfinder02/migration.sql similarity index 59% rename from client/prisma/migrations/20250223091422_askfs/migration.sql rename to client/prisma/migrations/20250302165720_starkfinder02/migration.sql index 7c637cb9..bf6f4903 100644 --- a/client/prisma/migrations/20250223091422_askfs/migration.sql +++ b/client/prisma/migrations/20250302165720_starkfinder02/migration.sql @@ -1,9 +1,6 @@ -/* - Warnings: - - - A unique constraint covering the columns `[address]` on the table `User` will be added. If there are existing duplicate values, this will fail. +-- CreateEnum +CREATE TYPE "Role" AS ENUM ('USER', 'ASSISTANT'); -*/ -- CreateEnum CREATE TYPE "ChatType" AS ENUM ('GENERAL', 'TRANSACTION', 'ASK'); @@ -13,14 +10,39 @@ CREATE TYPE "TxType" AS ENUM ('SWAP', 'TRANSFER', 'BRIDGE', 'DEPOSIT', 'WITHDRAW -- CreateEnum CREATE TYPE "TxStatus" AS ENUM ('PENDING', 'COMPLETED', 'FAILED'); --- AlterTable -ALTER TABLE "Chat" ADD COLUMN "type" "ChatType" NOT NULL DEFAULT 'GENERAL'; +-- CreateTable +CREATE TABLE "User" ( + "id" TEXT NOT NULL, + "email" TEXT, + "name" TEXT, + "address" TEXT, --- AlterTable -ALTER TABLE "Message" ADD COLUMN "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP; + CONSTRAINT "User_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "Chat" ( + "id" TEXT NOT NULL, + "title" TEXT, + "metadata" JSONB, + "userId" TEXT, + "type" "ChatType" NOT NULL DEFAULT 'GENERAL', + + CONSTRAINT "Chat_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "Message" ( + "id" TEXT NOT NULL, + "content" JSONB[], + "metadata" JSONB, + "replyTo" TEXT, + "chatId" TEXT NOT NULL, + "userId" TEXT NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, --- AlterTable -ALTER TABLE "User" ADD COLUMN "address" TEXT; + CONSTRAINT "Message_pkey" PRIMARY KEY ("id") +); -- CreateTable CREATE TABLE "Transaction" ( @@ -61,9 +83,21 @@ CREATE TABLE "DeployedContract" ( CONSTRAINT "DeployedContract_pkey" PRIMARY KEY ("id") ); +-- CreateIndex +CREATE UNIQUE INDEX "User_email_key" ON "User"("email"); + -- CreateIndex CREATE UNIQUE INDEX "User_address_key" ON "User"("address"); +-- AddForeignKey +ALTER TABLE "Chat" ADD CONSTRAINT "Chat_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Message" ADD CONSTRAINT "Message_chatId_fkey" FOREIGN KEY ("chatId") REFERENCES "Chat"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Message" ADD CONSTRAINT "Message_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + -- AddForeignKey ALTER TABLE "Transaction" ADD CONSTRAINT "Transaction_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;