feat: Add persistent settings and multiple layouts support

- Add settings manager to persist symbol, timeframe, and layouts
- Support multiple layouts for comprehensive chart analysis
- Remove debug screenshots for cleaner logs
- Update AI analysis with professional trading prompt
- Add multi-screenshot analysis for better trading insights
- Update analyze API to use saved settings and multiple layouts
This commit is contained in:
root
2025-07-09 14:24:48 +02:00
parent 6a1a4576a9
commit 3361359119
28 changed files with 1487 additions and 30 deletions

51
Dockerfile Normal file
View File

@@ -0,0 +1,51 @@
# Dockerfile for Next.js 15 + Puppeteer/Chromium + Prisma + Tailwind + OpenAI
FROM node:20-slim
# Install system dependencies for Chromium
RUN apt-get update && apt-get install -y \
wget \
ca-certificates \
fonts-liberation \
libappindicator3-1 \
libasound2 \
libatk-bridge2.0-0 \
libatk1.0-0 \
libcups2 \
libdbus-1-3 \
libdrm2 \
libgbm1 \
libnspr4 \
libnss3 \
libx11-xcb1 \
libxcomposite1 \
libxdamage1 \
libxrandr2 \
xdg-utils \
--no-install-recommends && \
rm -rf /var/lib/apt/lists/*
# Install Chromium (Debian 12+ uses 'chromium' instead of 'chromium-browser')
RUN apt-get update && apt-get install -y \
chromium \
--no-install-recommends && \
rm -rf /var/lib/apt/lists/*
# Set working directory
WORKDIR /app
# Copy package files and install dependencies
COPY package.json package-lock.json* pnpm-lock.yaml* yarn.lock* .npmrc* ./
RUN npm install
# Copy the rest of the app
COPY . .
# Expose port
EXPOSE 3000
# Set environment variables for Puppeteer
ENV PUPPETEER_SKIP_CHROMIUM_DOWNLOAD=true
ENV PUPPETEER_EXECUTABLE_PATH=/usr/bin/chromium
# Start the app
CMD ["npm", "run", "dev"]

53
app/api/analyze/route.ts Normal file
View File

@@ -0,0 +1,53 @@
import { NextRequest, NextResponse } from 'next/server'
import { aiAnalysisService } from '../../../lib/ai-analysis'
import { tradingViewCapture } from '../../../lib/tradingview'
import { settingsManager } from '../../../lib/settings'
import path from 'path'
export async function POST(req: NextRequest) {
try {
const { symbol, layouts, timeframe } = await req.json()
// Load current settings
const settings = await settingsManager.loadSettings()
// Use provided values or fall back to saved settings
const finalSymbol = symbol || settings.symbol
const finalTimeframe = timeframe || settings.timeframe
const finalLayouts = layouts || settings.layouts
if (!finalSymbol) {
return NextResponse.json({ error: 'Missing symbol' }, { status: 400 })
}
const baseFilename = `${finalSymbol}_${finalTimeframe}_${Date.now()}`
const screenshots = await tradingViewCapture.capture(finalSymbol, `${baseFilename}.png`, finalLayouts, finalTimeframe)
let result
if (screenshots.length === 1) {
// Single screenshot analysis
const filename = path.basename(screenshots[0])
result = await aiAnalysisService.analyzeScreenshot(filename)
} else {
// Multiple screenshots analysis
const filenames = screenshots.map(screenshot => path.basename(screenshot))
result = await aiAnalysisService.analyzeMultipleScreenshots(filenames)
}
if (!result) {
return NextResponse.json({ error: 'Analysis failed' }, { status: 500 })
}
return NextResponse.json({
...result,
settings: {
symbol: finalSymbol,
timeframe: finalTimeframe,
layouts: finalLayouts
},
screenshots: screenshots.map(s => path.basename(s))
})
} catch (e: any) {
return NextResponse.json({ error: e.message }, { status: 500 })
}
}

View File

@@ -0,0 +1,33 @@
import { NextRequest, NextResponse } from 'next/server'
import { getAutoTradingService } from '../../../lib/auto-trading'
const autoTradingService = getAutoTradingService()
export async function POST(req: NextRequest) {
try {
const { action, config } = await req.json()
if (action === 'start') {
autoTradingService.start()
return NextResponse.json({ status: 'started' })
}
if (action === 'stop') {
autoTradingService.stop()
return NextResponse.json({ status: 'stopped' })
}
if (action === 'config' && config) {
autoTradingService.setConfig(config)
return NextResponse.json({ status: 'config updated', config: autoTradingService })
}
return NextResponse.json({ error: 'Invalid action' }, { status: 400 })
} catch (e: any) {
return NextResponse.json({ error: e.message }, { status: 500 })
}
}
export async function GET() {
// Return current config/status
return NextResponse.json({
config: autoTradingService,
running: !!autoTradingService['intervalId']
})
}

View File

@@ -0,0 +1,15 @@
import { NextRequest, NextResponse } from 'next/server'
import { tradingViewCapture } from '../../../lib/tradingview'
export async function POST(req: NextRequest) {
try {
const { symbol, filename } = await req.json()
if (!symbol || !filename) {
return NextResponse.json({ error: 'Missing symbol or filename' }, { status: 400 })
}
const filePath = await tradingViewCapture.capture(symbol, filename)
return NextResponse.json({ filePath })
} catch (e: any) {
return NextResponse.json({ error: e.message }, { status: 500 })
}
}

View File

@@ -0,0 +1,14 @@
import prisma from '../../../lib/prisma'
import { NextResponse } from 'next/server'
export async function GET() {
try {
const trades = await prisma.trade.findMany({
orderBy: { executedAt: 'desc' },
take: 50
})
return NextResponse.json(trades)
} catch (e: any) {
return NextResponse.json({ error: e.message }, { status: 500 })
}
}

21
app/api/trading/route.ts Normal file
View File

@@ -0,0 +1,21 @@
import { NextRequest, NextResponse } from 'next/server'
import { driftTradingService } from '../../../lib/drift-trading'
export async function POST(req: NextRequest) {
try {
const params = await req.json()
const result = await driftTradingService.executeTrade(params)
return NextResponse.json(result)
} catch (e: any) {
return NextResponse.json({ error: e.message }, { status: 500 })
}
}
export async function GET() {
try {
const positions = await driftTradingService.getPositions()
return NextResponse.json({ positions })
} catch (e: any) {
return NextResponse.json({ error: e.message }, { status: 500 })
}
}

7
app/globals.css Normal file
View File

@@ -0,0 +1,7 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
body {
font-family: 'Inter', system-ui, sans-serif;
}

19
app/layout.tsx Normal file
View File

@@ -0,0 +1,19 @@
import './globals.css'
import type { Metadata } from 'next'
export const metadata: Metadata = {
title: 'Trading Bot Dashboard',
description: 'AI-powered trading bot dashboard with auto-trading, analysis, and developer tools.'
}
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<body className="bg-gray-950 text-gray-100 min-h-screen">
<main className="max-w-5xl mx-auto py-8">
{children}
</main>
</body>
</html>
)
}

11
app/page.tsx Normal file
View File

@@ -0,0 +1,11 @@
import AIAnalysisPanel from '../components/AIAnalysisPanel'
import Dashboard from '../components/Dashboard'
export default function HomePage() {
return (
<>
<AIAnalysisPanel />
<Dashboard />
</>
)
}

View File

@@ -0,0 +1,22 @@
import { NextRequest, NextResponse } from 'next/server'
import fs from 'fs/promises'
import path from 'path'
export async function GET(
request: NextRequest,
context: any
) {
try {
const screenshotsDir = path.join(process.cwd(), 'screenshots')
const filePath = path.join(screenshotsDir, context.params.filename)
const file = await fs.readFile(filePath)
return new NextResponse(file, {
headers: {
'Content-Type': 'image/png',
'Content-Disposition': `inline; filename="${context.params.filename}"`
}
})
} catch (e: any) {
return NextResponse.json({ error: e.message }, { status: 404 })
}
}

View File

@@ -0,0 +1,106 @@
"use client"
import React, { useState } from 'react'
const layouts = (process.env.NEXT_PUBLIC_TRADINGVIEW_LAYOUTS || 'ai,Diy module').split(',').map(l => l.trim())
const timeframes = [
{ label: '1m', value: '1' },
{ label: '5m', value: '5' },
{ label: '15m', value: '15' },
{ label: '1h', value: '60' },
{ label: '4h', value: '240' },
{ label: '1d', value: 'D' },
{ label: '1w', value: 'W' },
{ label: '1M', value: 'M' },
]
export default function AIAnalysisPanel() {
const [symbol, setSymbol] = useState('BTCUSD')
const [layout, setLayout] = useState(layouts[0])
const [timeframe, setTimeframe] = useState('60')
const [loading, setLoading] = useState(false)
const [result, setResult] = useState<any>(null)
const [error, setError] = useState<string | null>(null)
async function handleAnalyze() {
setLoading(true)
setError(null)
setResult(null)
try {
const res = await fetch('/api/analyze', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ symbol, layout, timeframe })
})
const data = await res.json()
if (!res.ok) throw new Error(data.error || 'Unknown error')
setResult(data)
} catch (e: any) {
setError(e.message)
}
setLoading(false)
}
return (
<div className="bg-gray-900 rounded-lg shadow p-6 mb-8">
<h2 className="text-xl font-bold mb-4 text-white">AI Chart Analysis</h2>
<div className="flex gap-2 mb-4">
<input
className="input input-bordered flex-1"
value={symbol}
onChange={e => setSymbol(e.target.value)}
placeholder="Symbol (e.g. BTCUSD)"
/>
<select
className="input input-bordered"
value={layout}
onChange={e => setLayout(e.target.value)}
>
{layouts.map(l => <option key={l} value={l}>{l}</option>)}
</select>
<select
className="input input-bordered"
value={timeframe}
onChange={e => setTimeframe(e.target.value)}
>
{timeframes.map(tf => <option key={tf.value} value={tf.value}>{tf.label}</option>)}
</select>
<button className="btn btn-primary" onClick={handleAnalyze} disabled={loading}>
{loading ? 'Analyzing...' : 'Analyze'}
</button>
</div>
{error && (
<div className="text-red-400 mb-2">
{error.includes('frame was detached') ? (
<>
TradingView chart could not be loaded. Please check your symbol and layout, or try again.<br />
<span className="text-xs">(Technical: {error})</span>
</>
) : error.includes('layout not found') ? (
<>
TradingView layout not found. Please select a valid layout.<br />
<span className="text-xs">(Technical: {error})</span>
</>
) : (
error
)}
</div>
)}
{loading && (
<div className="flex items-center gap-2 text-gray-300 mb-2">
<svg className="animate-spin h-5 w-5 text-blue-400" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle><path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8v8z"></path></svg>
Analyzing chart...
</div>
)}
{result && (
<div className="bg-gray-800 rounded p-4 mt-4">
<div><b>Summary:</b> {result.summary}</div>
<div><b>Sentiment:</b> {result.marketSentiment}</div>
<div><b>Recommendation:</b> {result.recommendation} ({result.confidence}%)</div>
<div><b>Support:</b> {result.keyLevels?.support?.join(', ')}</div>
<div><b>Resistance:</b> {result.keyLevels?.resistance?.join(', ')}</div>
<div><b>Reasoning:</b> {result.reasoning}</div>
</div>
)}
</div>
)
}

View File

@@ -0,0 +1,35 @@
"use client"
import React, { useState } from 'react'
export default function AutoTradingPanel() {
const [status, setStatus] = useState<'idle'|'running'|'stopped'>('idle')
const [message, setMessage] = useState<string>('')
async function handleAction(action: 'start'|'stop') {
setMessage('')
setStatus('idle')
const res = await fetch('/api/auto-trading', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ action })
})
if (res.ok) {
setStatus(action === 'start' ? 'running' : 'stopped')
setMessage(`Auto-trading ${action}ed`)
} else {
setMessage('Error: ' + (await res.text()))
}
}
return (
<div className="p-4 border rounded bg-gray-900">
<h2 className="text-lg font-bold mb-2">Auto-Trading Control</h2>
<div className="flex gap-2 mb-2">
<button className="btn btn-primary" onClick={() => handleAction('start')}>Start</button>
<button className="btn btn-secondary" onClick={() => handleAction('stop')}>Stop</button>
</div>
<div className="text-sm text-gray-400">Status: {status}</div>
{message && <div className="mt-2 text-yellow-400">{message}</div>}
</div>
)
}

View File

@@ -0,0 +1,11 @@
"use client"
import React from 'react'
export default function DashboardMinimal() {
return (
<div className="p-8 text-center text-gray-400">
<h1 className="text-2xl font-bold mb-4">Trading Bot Dashboard</h1>
<p>Welcome! Please select a feature from the menu.</p>
</div>
)
}

68
components/Dashboard.tsx Normal file
View File

@@ -0,0 +1,68 @@
"use client"
import React, { useEffect, useState } from 'react'
import AutoTradingPanel from './AutoTradingPanel'
import TradingHistory from './TradingHistory'
import DeveloperSettings from './DeveloperSettings'
export default function Dashboard() {
const [positions, setPositions] = useState<any[]>([])
const [loading, setLoading] = useState(true)
const [error, setError] = useState<string | null>(null)
useEffect(() => {
async function fetchPositions() {
try {
const res = await fetch('/api/trading')
if (res.ok) {
const data = await res.json()
setPositions(data.positions || [])
} else {
setError('Failed to load positions')
}
} catch (e) {
setError('Error loading positions')
}
setLoading(false)
}
fetchPositions()
}, [])
return (
<div className="grid grid-cols-1 md:grid-cols-2 gap-8 p-8 bg-gray-950 min-h-screen">
<div className="space-y-8">
<AutoTradingPanel />
<DeveloperSettings />
</div>
<div className="space-y-8">
<div className="bg-gray-900 rounded-lg shadow p-6">
<h2 className="text-xl font-bold mb-4 text-white">Open Positions</h2>
{loading ? <div className="text-gray-400">Loading...</div> : error ? <div className="text-red-400">{error}</div> : (
<table className="w-full text-sm text-left">
<thead className="text-gray-400 border-b border-gray-700">
<tr>
<th className="py-2">Symbol</th>
<th>Side</th>
<th>Size</th>
<th>Entry Price</th>
<th>Unrealized PnL</th>
</tr>
</thead>
<tbody>
{positions.map((pos, i) => (
<tr key={i} className="border-b border-gray-800 hover:bg-gray-800">
<td className="py-2">{pos.symbol}</td>
<td>{pos.side}</td>
<td>{pos.size}</td>
<td>{pos.entryPrice}</td>
<td>{pos.unrealizedPnl}</td>
</tr>
))}
</tbody>
</table>
)}
</div>
<TradingHistory />
</div>
</div>
)
}

View File

@@ -0,0 +1,27 @@
"use client"
import React, { useState } from 'react'
export default function DeveloperSettings() {
const [env, setEnv] = useState('')
const [message, setMessage] = useState('')
async function handleSave() {
// Example: Save env to localStorage or send to API
localStorage.setItem('devEnv', env)
setMessage('Settings saved!')
}
return (
<div className="p-4 border rounded bg-gray-900">
<h2 className="text-lg font-bold mb-2">Developer Settings</h2>
<input
className="input input-bordered w-full mb-2"
placeholder="Custom ENV value"
value={env}
onChange={e => setEnv(e.target.value)}
/>
<button className="btn btn-primary w-full" onClick={handleSave}>Save</button>
{message && <div className="mt-2 text-green-400">{message}</div>}
</div>
)
}

View File

@@ -0,0 +1,60 @@
"use client"
import React, { useEffect, useState } from 'react'
interface Trade {
id: string
symbol: string
side: string
amount: number
price: number
status: string
executedAt: string
}
export default function TradingHistory() {
const [trades, setTrades] = useState<Trade[]>([])
const [loading, setLoading] = useState(true)
useEffect(() => {
async function fetchTrades() {
const res = await fetch('/api/trading-history')
if (res.ok) {
setTrades(await res.json())
}
setLoading(false)
}
fetchTrades()
}, [])
return (
<div className="p-4 border rounded bg-gray-900">
<h2 className="text-lg font-bold mb-2">Trading History</h2>
{loading ? <div>Loading...</div> : (
<table className="w-full text-sm">
<thead>
<tr>
<th>Symbol</th>
<th>Side</th>
<th>Amount</th>
<th>Price</th>
<th>Status</th>
<th>Executed At</th>
</tr>
</thead>
<tbody>
{trades.map(trade => (
<tr key={trade.id}>
<td>{trade.symbol}</td>
<td>{trade.side}</td>
<td>{trade.amount}</td>
<td>{trade.price}</td>
<td>{trade.status}</td>
<td>{trade.executedAt}</td>
</tr>
))}
</tbody>
</table>
)}
</div>
)
}

20
docker-compose.yml Normal file
View File

@@ -0,0 +1,20 @@
version: '3.8'
services:
app:
build: .
ports:
- "3000:3000"
volumes:
- ./:/app
- /app/node_modules
- ./screenshots:/app/screenshots
environment:
- NODE_ENV=development
- PUPPETEER_SKIP_CHROMIUM_DOWNLOAD=true
- PUPPETEER_EXECUTABLE_PATH=/usr/bin/chromium
- TZ=Europe/Berlin
env_file:
- .env
# Uncomment for debugging
# command: ["npm", "run", "dev"]
# entrypoint: ["/bin/bash"]

208
lib/ai-analysis.ts Normal file
View File

@@ -0,0 +1,208 @@
import OpenAI from 'openai'
import fs from 'fs/promises'
import path from 'path'
const openai = new OpenAI({
apiKey: process.env.OPENAI_API_KEY,
})
export interface AnalysisResult {
summary: string
marketSentiment: 'BULLISH' | 'BEARISH' | 'NEUTRAL'
keyLevels: {
support: number[]
resistance: number[]
}
recommendation: 'BUY' | 'SELL' | 'HOLD'
confidence: number // 0-100
reasoning: string
}
export class AIAnalysisService {
async analyzeScreenshot(filename: string): Promise<AnalysisResult | null> {
try {
const screenshotsDir = path.join(process.cwd(), 'screenshots')
const imagePath = path.join(screenshotsDir, filename)
// Read image file
const imageBuffer = await fs.readFile(imagePath)
const base64Image = imageBuffer.toString('base64')
const prompt = `You are now a professional trading assistant focused on short-term crypto trading using 515min timeframes. You behave with the precision and decisiveness of a top proprietary desk trader. No vagueness, no fluff.
Analyze the attached TradingView chart screenshot and provide a detailed trading analysis.
### WHEN GIVING A TRADE SETUP:
Be 100% SPECIFIC. Provide:
1. **ENTRY**
- Exact price level (with a ± entry buffer if needed)
- Rationale: e.g., "Rejection from 15 EMA + VWAP confluence near intraday supply"
2. **STOP-LOSS (SL)**
- Exact level (not arbitrary)
- Explain *why* it's there: "Above VWAP + failed breakout zone"
3. **TAKE PROFITS**
- TP1: Immediate structure (ex: previous low at $149.20)
- TP2: Extended target if momentum continues (e.g., $148.00)
- Mention **expected RSI/OBV behavior** at each TP zone
4. **RISK-TO-REWARD**
- Show R:R. Ex: "1:2.5 — Risking $X to potentially gain $Y"
5. **CONFIRMATION TRIGGER**
- Exact signal to wait for: e.g., "Bearish engulfing candle on rejection from VWAP zone"
- OBV: "Must be making lower highs + dropping below 30min average"
- RSI: "Should remain under 50 on rejection. Overbought ≥70 = wait"
6. **INDICATOR ANALYSIS**
- **RSI**: If RSI crosses above 70 while price is under resistance → *wait*
- **VWAP**: If price retakes VWAP with bullish momentum → *consider invalidation*
- **OBV**: If OBV starts climbing while price stays flat → *early exit or reconsider bias*
Return your answer as a JSON object with the following structure:
{
"summary": "Brief market summary",
"marketSentiment": "BULLISH" | "BEARISH" | "NEUTRAL",
"keyLevels": {
"support": [number array],
"resistance": [number array]
},
"recommendation": "BUY" | "SELL" | "HOLD",
"confidence": number (0-100),
"reasoning": "Detailed reasoning with specific levels, indicators, and confirmation triggers"
}
Be concise but thorough. Only return valid JSON.`
const response = await openai.chat.completions.create({
model: "gpt-4o", // Updated to current vision model
messages: [
{
role: "user",
content: [
{ type: "text", text: prompt },
{ type: "image_url", image_url: { url: `data:image/png;base64,${base64Image}` } }
]
}
],
max_tokens: 1024
})
const content = response.choices[0]?.message?.content
if (!content) return null
// Extract JSON from response
const match = content.match(/\{[\s\S]*\}/)
if (!match) return null
const json = match[0]
const result = JSON.parse(json)
// Optionally: validate result structure here
return result as AnalysisResult
} catch (e) {
console.error('AI analysis error:', e)
return null
}
}
async analyzeMultipleScreenshots(filenames: string[]): Promise<AnalysisResult | null> {
try {
const screenshotsDir = path.join(process.cwd(), 'screenshots')
const images: any[] = []
for (const filename of filenames) {
const imagePath = path.join(screenshotsDir, filename)
const imageBuffer = await fs.readFile(imagePath)
const base64Image = imageBuffer.toString('base64')
images.push({ type: "image_url", image_url: { url: `data:image/png;base64,${base64Image}` } })
}
const prompt = `You are now a professional trading assistant focused on short-term crypto trading using 515min timeframes. You behave with the precision and decisiveness of a top proprietary desk trader. No vagueness, no fluff.
Analyze the attached TradingView chart screenshots (multiple layouts of the same symbol) and provide a comprehensive trading analysis by combining insights from all charts.
### WHEN GIVING A TRADE SETUP:
Be 100% SPECIFIC. Provide:
1. **ENTRY**
- Exact price level (with a ± entry buffer if needed)
- Rationale: e.g., "Rejection from 15 EMA + VWAP confluence near intraday supply"
2. **STOP-LOSS (SL)**
- Exact level (not arbitrary)
- Explain *why* it's there: "Above VWAP + failed breakout zone"
3. **TAKE PROFITS**
- TP1: Immediate structure (ex: previous low at $149.20)
- TP2: Extended target if momentum continues (e.g., $148.00)
- Mention **expected RSI/OBV behavior** at each TP zone
4. **RISK-TO-REWARD**
- Show R:R. Ex: "1:2.5 — Risking $X to potentially gain $Y"
5. **CONFIRMATION TRIGGER**
- Exact signal to wait for: e.g., "Bearish engulfing candle on rejection from VWAP zone"
- OBV: "Must be making lower highs + dropping below 30min average"
- RSI: "Should remain under 50 on rejection. Overbought ≥70 = wait"
6. **INDICATOR ANALYSIS**
- **RSI**: If RSI crosses above 70 while price is under resistance → *wait*
- **VWAP**: If price retakes VWAP with bullish momentum → *consider invalidation*
- **OBV**: If OBV starts climbing while price stays flat → *early exit or reconsider bias*
Cross-reference all layouts to provide the most accurate analysis. If layouts show conflicting signals, explain which one takes priority and why.
Return your answer as a JSON object with the following structure:
{
"summary": "Brief market summary combining all layouts",
"marketSentiment": "BULLISH" | "BEARISH" | "NEUTRAL",
"keyLevels": {
"support": [number array],
"resistance": [number array]
},
"recommendation": "BUY" | "SELL" | "HOLD",
"confidence": number (0-100),
"reasoning": "Detailed reasoning with specific levels, indicators, and confirmation triggers from all layouts"
}
Be concise but thorough. Only return valid JSON.`
const response = await openai.chat.completions.create({
model: "gpt-4o",
messages: [
{
role: "user",
content: [
{ type: "text", text: prompt },
...images
]
}
],
max_tokens: 1500,
temperature: 0.1
})
const content = response.choices[0]?.message?.content
if (!content) {
throw new Error('No content received from OpenAI')
}
// Parse the JSON response
const jsonMatch = content.match(/\{[\s\S]*\}/)
if (!jsonMatch) {
throw new Error('No JSON found in response')
}
const analysis = JSON.parse(jsonMatch[0])
// Validate the structure
if (!analysis.summary || !analysis.marketSentiment || !analysis.recommendation || !analysis.confidence) {
throw new Error('Invalid analysis structure')
}
return analysis
} catch (error) {
console.error('AI multi-analysis error:', error)
return null
}
}
}
export const aiAnalysisService = new AIAnalysisService()

82
lib/auto-trading.ts Normal file
View File

@@ -0,0 +1,82 @@
import { tradingViewCapture } from './tradingview'
import { aiAnalysisService } from './ai-analysis'
import prisma from './prisma'
export interface AutoTradingConfig {
enabled: boolean
symbols: string[]
intervalMinutes: number
maxDailyTrades: number
tradingAmount: number
confidenceThreshold: number
}
export class AutoTradingService {
private config: AutoTradingConfig
private intervalId: NodeJS.Timeout | null = null
private dailyTradeCount: Record<string, number> = {}
constructor(config: AutoTradingConfig) {
this.config = config
this.dailyTradeCount = {}
}
start() {
if (this.intervalId || !this.config.enabled) return
this.intervalId = setInterval(() => this.run(), this.config.intervalMinutes * 60 * 1000)
this.run() // Run immediately on start
}
stop() {
if (this.intervalId) {
clearInterval(this.intervalId)
this.intervalId = null
}
}
async run() {
if (!this.config.enabled) return
for (const symbol of this.config.symbols) {
if ((this.dailyTradeCount[symbol] || 0) >= this.config.maxDailyTrades) continue
// 1. Capture screenshot
const filename = `${symbol}_${Date.now()}.png`
const screenshotPath = await tradingViewCapture.capture(symbol, filename)
// 2. Analyze screenshot
const analysis = await aiAnalysisService.analyzeScreenshot(filename)
if (!analysis || analysis.confidence < this.config.confidenceThreshold) continue
// 3. Execute trade (stub: integrate with driftTradingService)
// const tradeResult = await driftTradingService.executeTrade({ ... })
// 4. Save trade to DB
await prisma.trade.create({
data: {
symbol,
side: analysis.recommendation === 'BUY' ? 'LONG' : analysis.recommendation === 'SELL' ? 'SHORT' : 'NONE',
amount: this.config.tradingAmount,
price: 0, // To be filled with actual execution price
status: 'PENDING',
screenshotUrl: screenshotPath,
aiAnalysis: JSON.stringify(analysis),
executedAt: new Date(),
userId: 'system', // Or actual user if available
}
})
this.dailyTradeCount[symbol] = (this.dailyTradeCount[symbol] || 0) + 1
}
}
setConfig(config: Partial<AutoTradingConfig>) {
this.config = { ...this.config, ...config }
}
}
export function getAutoTradingService() {
// Singleton pattern or similar
return new AutoTradingService({
enabled: false,
symbols: ['BTCUSD'],
intervalMinutes: 15,
maxDailyTrades: 10,
tradingAmount: 100,
confidenceThreshold: 80
})
}

13
lib/prisma.ts Normal file
View File

@@ -0,0 +1,13 @@
import { PrismaClient } from '@prisma/client'
declare global {
var prisma: PrismaClient | undefined
}
const prisma = globalThis.prisma || new PrismaClient()
if (process.env.NODE_ENV !== 'production') {
globalThis.prisma = prisma
}
export default prisma

88
lib/settings.ts Normal file
View File

@@ -0,0 +1,88 @@
import fs from 'fs/promises'
import path from 'path'
export interface TradingSettings {
symbol: string
timeframe: string
layouts: string[]
lastUpdated: number
}
const SETTINGS_FILE = path.join(process.cwd(), 'trading-settings.json')
export class SettingsManager {
private static instance: SettingsManager
private settings: TradingSettings = {
symbol: 'BTCUSD',
timeframe: '5',
layouts: ['ai'],
lastUpdated: Date.now()
}
private constructor() {}
static getInstance(): SettingsManager {
if (!SettingsManager.instance) {
SettingsManager.instance = new SettingsManager()
}
return SettingsManager.instance
}
async loadSettings(): Promise<TradingSettings> {
try {
const data = await fs.readFile(SETTINGS_FILE, 'utf-8')
this.settings = JSON.parse(data)
console.log('Loaded settings:', this.settings)
} catch (error) {
console.log('No existing settings found, using defaults')
await this.saveSettings()
}
return this.settings
}
async saveSettings(): Promise<void> {
try {
this.settings.lastUpdated = Date.now()
await fs.writeFile(SETTINGS_FILE, JSON.stringify(this.settings, null, 2))
console.log('Settings saved:', this.settings)
} catch (error) {
console.error('Failed to save settings:', error)
}
}
async updateSettings(updates: Partial<TradingSettings>): Promise<TradingSettings> {
this.settings = { ...this.settings, ...updates }
await this.saveSettings()
return this.settings
}
getSettings(): TradingSettings {
return this.settings
}
async setSymbol(symbol: string): Promise<void> {
await this.updateSettings({ symbol })
}
async setTimeframe(timeframe: string): Promise<void> {
await this.updateSettings({ timeframe })
}
async setLayouts(layouts: string[]): Promise<void> {
await this.updateSettings({ layouts })
}
async addLayout(layout: string): Promise<void> {
if (!this.settings.layouts.includes(layout)) {
const layouts = [...this.settings.layouts, layout]
await this.updateSettings({ layouts })
}
}
async removeLayout(layout: string): Promise<void> {
const layouts = this.settings.layouts.filter(l => l !== layout)
await this.updateSettings({ layouts })
}
}
export const settingsManager = SettingsManager.getInstance()

396
lib/tradingview.ts Normal file
View File

@@ -0,0 +1,396 @@
import puppeteer, { Browser, Page, Frame } from 'puppeteer'
import path from 'path'
import fs from 'fs/promises'
import { settingsManager } from './settings'
const TRADINGVIEW_EMAIL = process.env.TRADINGVIEW_EMAIL
const TRADINGVIEW_PASSWORD = process.env.TRADINGVIEW_PASSWORD
const TRADINGVIEW_LAYOUTS = (process.env.TRADINGVIEW_LAYOUTS || '').split(',').map(l => l.trim())
const PUPPETEER_EXECUTABLE_PATH = process.env.PUPPETEER_EXECUTABLE_PATH || '/usr/bin/chromium'
export class TradingViewCapture {
private browser: Browser | null = null
private page: Page | null = null
private loggedIn = false
async init() {
if (!this.browser) {
this.browser = await puppeteer.launch({
headless: true,
args: [
'--no-sandbox',
'--disable-setuid-sandbox',
'--disable-dev-shm-usage',
'--disable-accelerated-2d-canvas',
'--no-first-run',
'--no-zygote',
'--disable-gpu'
],
executablePath: PUPPETEER_EXECUTABLE_PATH
})
console.log('Puppeteer browser launched')
}
if (!this.page) {
this.page = await this.browser.newPage()
await this.page.setViewport({ width: 1920, height: 1080 })
console.log('Puppeteer page created')
}
if (!this.loggedIn) {
console.log('Logging in to TradingView...')
await this.login()
this.loggedIn = true
console.log('Logged in to TradingView')
}
return this.page
}
async login() {
if (!TRADINGVIEW_EMAIL || !TRADINGVIEW_PASSWORD) {
throw new Error('TradingView credentials not set in .env')
}
const page = this.page || (await this.browser!.newPage())
console.log('Navigating to TradingView login page...')
await page.goto('https://www.tradingview.com/#signin', { waitUntil: 'networkidle2' })
// Check if we're already logged in
try {
const loggedInIndicator = await page.waitForSelector('.tv-header__user-menu-button, [data-name="header-user-menu"]', { timeout: 3000 })
if (loggedInIndicator) {
console.log('Already logged in to TradingView')
return
}
} catch (e) {
console.log('Not logged in yet, proceeding with login...')
}
try {
// Wait for the login modal to appear and look for email input directly
console.log('Looking for email input field...')
// Try to find the email input field directly (new TradingView layout)
const emailInput = await page.waitForSelector('input[name="username"], input[name="email"], input[type="email"], input[placeholder*="email" i]', { timeout: 10000 })
if (emailInput) {
console.log('Found email input field directly')
await emailInput.click() // Click to focus
await emailInput.type(TRADINGVIEW_EMAIL, { delay: 50 })
// Find password field
const passwordInput = await page.waitForSelector('input[name="password"], input[type="password"], input[placeholder*="password" i]', { timeout: 5000 })
if (!passwordInput) {
throw new Error('Could not find password input field')
}
await passwordInput.click() // Click to focus
await passwordInput.type(TRADINGVIEW_PASSWORD, { delay: 50 })
// Find and click the sign in button
const signInButton = await page.waitForSelector('button[type="submit"]', { timeout: 5000 })
if (!signInButton) {
// Try to find button with sign in text
const buttons = await page.$$('button')
let foundButton = null
for (const btn of buttons) {
const text = await page.evaluate(el => el.innerText || el.textContent, btn)
if (text && (text.toLowerCase().includes('sign in') || text.toLowerCase().includes('login'))) {
foundButton = btn
break
}
}
if (!foundButton) {
throw new Error('Could not find sign in button')
}
await foundButton.click()
} else {
await signInButton.click()
}
} else {
throw new Error('Could not find email input field')
}
} catch (e) {
// Fallback: try to find email button first
console.log('Fallback: looking for email button...')
try {
await page.waitForSelector('button', { timeout: 15000 })
const buttons = await page.$$('button')
let emailBtn = null
// Look for email button with various text patterns
for (const btn of buttons) {
const text = await page.evaluate(el => el.innerText || el.textContent, btn)
if (text && (
text.trim().toLowerCase().includes('email') ||
text.trim().toLowerCase().includes('sign in with email') ||
text.trim().toLowerCase().includes('continue with email')
)) {
emailBtn = btn
break
}
}
if (emailBtn) {
console.log('Found email button, clicking...')
await emailBtn.click()
await new Promise(res => setTimeout(res, 1000))
// Now fill in the form
const emailInput = await page.waitForSelector('input[name="username"], input[name="email"], input[type="email"], input[placeholder*="email" i]', { timeout: 10000 })
if (!emailInput) {
throw new Error('Could not find email input field after clicking email button')
}
await emailInput.click() // Click to focus
await emailInput.type(TRADINGVIEW_EMAIL, { delay: 50 })
const passwordInput = await page.waitForSelector('input[name="password"], input[type="password"], input[placeholder*="password" i]', { timeout: 5000 })
if (!passwordInput) {
throw new Error('Could not find password input field after clicking email button')
}
await passwordInput.click() // Click to focus
await passwordInput.type(TRADINGVIEW_PASSWORD, { delay: 50 })
const signInButton = await page.waitForSelector('button[type="submit"]', { timeout: 5000 })
if (!signInButton) {
// Try to find button with sign in text
const buttons = await page.$$('button')
let foundButton = null
for (const btn of buttons) {
const text = await page.evaluate(el => el.innerText || el.textContent, btn)
if (text && (text.toLowerCase().includes('sign in') || text.toLowerCase().includes('login'))) {
foundButton = btn
break
}
}
if (!foundButton) {
throw new Error('Could not find sign in button after clicking email button')
}
await foundButton.click()
} else {
await signInButton.click()
}
} else {
throw new Error('Could not find email button')
}
} catch (e2) {
console.error('Could not find or click email button:', e2)
const errorMessage = e2 instanceof Error ? e2.message : String(e2)
throw new Error('Could not find or click email button on TradingView login page. ' + errorMessage)
}
}
// Wait for navigation or dashboard (main page)
try {
console.log('Waiting for login to complete...')
await page.waitForSelector('.tv-header__user-menu-button, .chart-container, [data-name="header-user-menu"]', { timeout: 30000 })
} catch (e) {
console.error('Login navigation did not complete.')
throw new Error('Login navigation did not complete.')
}
console.log('TradingView login complete')
}
async capture(symbol: string, filename: string, layouts?: string[], timeframe?: string) {
console.log('Working directory:', process.cwd())
// Load settings and update if provided
const settings = await settingsManager.loadSettings()
if (symbol && symbol !== settings.symbol) {
await settingsManager.setSymbol(symbol)
}
if (timeframe && timeframe !== settings.timeframe) {
await settingsManager.setTimeframe(timeframe)
}
if (layouts && JSON.stringify(layouts) !== JSON.stringify(settings.layouts)) {
await settingsManager.setLayouts(layouts)
}
// Use saved settings if not provided
const finalSymbol = symbol || settings.symbol
const finalTimeframe = timeframe || settings.timeframe
const finalLayouts = layouts || settings.layouts
console.log('Using settings:', { symbol: finalSymbol, timeframe: finalTimeframe, layouts: finalLayouts })
const page = await this.init()
// Add timeframe to TradingView URL if provided
let url = `https://www.tradingview.com/chart/?symbol=${finalSymbol}`
if (finalTimeframe) {
url += `&interval=${encodeURIComponent(finalTimeframe)}`
}
try {
console.log('Navigating to TradingView chart:', url)
await page.goto(url, { waitUntil: 'networkidle2', timeout: 60000 })
console.log('Successfully navigated to chart')
} catch (e: any) {
console.error('Failed to load TradingView chart page:', e)
throw new Error('Failed to load TradingView chart page: ' + (e.message || e))
}
// Capture screenshots for each layout
const screenshots: string[] = []
for (let i = 0; i < finalLayouts.length; i++) {
const layout = finalLayouts[i]
console.log(`Processing layout ${i + 1}/${finalLayouts.length}: ${layout}`)
// Load the layout
await this.loadLayout(page, layout)
// Wait for layout to load
await new Promise(res => setTimeout(res, 3000))
// Generate filename for this layout
const layoutFilename = filename.replace('.png', `_${layout}.png`)
const screenshotsDir = path.join(process.cwd(), 'screenshots')
await fs.mkdir(screenshotsDir, { recursive: true })
const filePath = path.join(screenshotsDir, layoutFilename)
try {
await page.screenshot({ path: filePath as `${string}.png`, type: 'png' })
console.log(`Screenshot saved for layout ${layout}:`, filePath)
screenshots.push(filePath)
} catch (e: any) {
const debugScreenshotErrorPath = path.resolve(`debug_screenshot_error_${layout}.png`) as `${string}.png`
await page.screenshot({ path: debugScreenshotErrorPath })
console.error(`Failed to capture screenshot for layout ${layout}:`, e)
console.error('Screenshot on screenshot error:', debugScreenshotErrorPath)
throw new Error(`Failed to capture screenshot for layout ${layout}: ` + (e.message || e))
}
}
return screenshots
}
private async loadLayout(page: Page, layout: string): Promise<void> {
try {
console.log('Trying to load layout:', layout)
// Try multiple selectors for the layout button
const layoutSelectors = [
'[data-name="load-chart-layout-dialog"]',
'[data-name="layouts-menu"]',
'[data-name="chart-layout-button"]',
'button[title*="Layout" i]',
'button[aria-label*="Layout" i]',
'[data-testid*="layout"]'
]
let layoutButton = null
for (const selector of layoutSelectors) {
try {
layoutButton = await page.waitForSelector(selector, { timeout: 3000 })
if (layoutButton) {
console.log('Found layout button with selector:', selector)
break
}
} catch (e) {
// Continue to next selector
}
}
if (!layoutButton) {
// Try to find layout button by text content
const buttons = await page.$$('button, [role="button"]')
for (const btn of buttons) {
const text = await page.evaluate(el => {
const element = el as HTMLElement
return element.innerText || element.textContent || element.title || element.getAttribute('aria-label')
}, btn)
if (text && text.toLowerCase().includes('layout')) {
layoutButton = btn
console.log('Found layout button by text:', text)
break
}
}
}
if (layoutButton) {
await layoutButton.click()
console.log('Clicked layout button')
await new Promise(res => setTimeout(res, 1000))
// Look for search input or layout items
const searchSelectors = [
'input[name="search"]',
'input[placeholder*="search" i]',
'input[type="search"]',
'input[data-name="search"]'
]
let searchInput = null
for (const selector of searchSelectors) {
try {
searchInput = await page.waitForSelector(selector, { timeout: 3000 })
if (searchInput) break
} catch (e) {
// Continue to next selector
}
}
if (searchInput) {
console.log('Found search input, typing layout name...')
await searchInput.type(layout, { delay: 50 })
await new Promise(res => setTimeout(res, 2000))
// Try to find and click the layout item
const layoutItemSelectors = [
'[data-name="chart-layout-list-item"]',
'[data-testid*="layout-item"]',
'.layout-item',
'[role="option"]'
]
let layoutItem = null
for (const selector of layoutItemSelectors) {
try {
const items = await page.$$(selector)
for (const item of items) {
const text = await page.evaluate(el => {
const element = el as HTMLElement
return element.innerText || element.textContent
}, item)
if (text && text.toLowerCase().includes(layout.toLowerCase())) {
layoutItem = item
break
}
}
if (layoutItem) break
} catch (e) {
// Continue to next selector
}
}
if (layoutItem) {
await layoutItem.click()
console.log('Clicked layout item:', layout)
} else {
console.log('Layout item not found, trying generic approach...')
await page.evaluate((layout) => {
const items = Array.from(document.querySelectorAll('*'))
const item = items.find(el => el.textContent?.toLowerCase().includes(layout.toLowerCase()))
if (item) (item as HTMLElement).click()
}, layout)
}
} else {
console.log('Search input not found, trying to find layout directly...')
await page.evaluate((layout) => {
const items = Array.from(document.querySelectorAll('*'))
const item = items.find(el => el.textContent?.toLowerCase().includes(layout.toLowerCase()))
if (item) (item as HTMLElement).click()
}, layout)
}
await new Promise(res => setTimeout(res, 4000))
console.log('Layout loaded:', layout)
} else {
console.log('Layout button not found, skipping layout loading')
}
} catch (e: any) {
const debugLayoutErrorPath = path.resolve('debug_layout_error.png') as `${string}.png`
await page.screenshot({ path: debugLayoutErrorPath })
console.error('TradingView layout not found or could not be loaded:', e)
console.log('Continuing without layout...')
// Don't throw error, just continue without layout
}
}
}
export const tradingViewCapture = new TradingViewCapture()

134
package-lock.json generated
View File

@@ -11,6 +11,7 @@
"@drift-labs/sdk": "^2.126.0-beta.14", "@drift-labs/sdk": "^2.126.0-beta.14",
"@prisma/client": "^6.11.1", "@prisma/client": "^6.11.1",
"@solana/web3.js": "^1.98.2", "@solana/web3.js": "^1.98.2",
"bs58": "^6.0.0",
"next": "15.3.5", "next": "15.3.5",
"openai": "^5.8.3", "openai": "^5.8.3",
"prisma": "^6.11.1", "prisma": "^6.11.1",
@@ -148,6 +149,15 @@
"@solana/web3.js": "^1.68.0" "@solana/web3.js": "^1.68.0"
} }
}, },
"node_modules/@coral-xyz/anchor-30/node_modules/bs58": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/bs58/-/bs58-4.0.1.tgz",
"integrity": "sha512-Ok3Wdf5vOIlBrgCvTq96gBkJw+JUEzdBgyaza5HLtPm7yTHkjRy8+JzNyHF7BHa0bNWOQIp3m5YF0nnFcOIKLw==",
"license": "MIT",
"dependencies": {
"base-x": "^3.0.2"
}
},
"node_modules/@coral-xyz/anchor-errors": { "node_modules/@coral-xyz/anchor-errors": {
"version": "0.30.1", "version": "0.30.1",
"resolved": "https://registry.npmjs.org/@coral-xyz/anchor-errors/-/anchor-errors-0.30.1.tgz", "resolved": "https://registry.npmjs.org/@coral-xyz/anchor-errors/-/anchor-errors-0.30.1.tgz",
@@ -156,6 +166,16 @@
"node": ">=10" "node": ">=10"
} }
}, },
"node_modules/@coral-xyz/anchor/node_modules/bs58": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/bs58/-/bs58-4.0.1.tgz",
"integrity": "sha512-Ok3Wdf5vOIlBrgCvTq96gBkJw+JUEzdBgyaza5HLtPm7yTHkjRy8+JzNyHF7BHa0bNWOQIp3m5YF0nnFcOIKLw==",
"license": "MIT",
"peer": true,
"dependencies": {
"base-x": "^3.0.2"
}
},
"node_modules/@coral-xyz/borsh": { "node_modules/@coral-xyz/borsh": {
"version": "0.28.0", "version": "0.28.0",
"resolved": "https://registry.npmjs.org/@coral-xyz/borsh/-/borsh-0.28.0.tgz", "resolved": "https://registry.npmjs.org/@coral-xyz/borsh/-/borsh-0.28.0.tgz",
@@ -294,6 +314,15 @@
"node": ">=14.0.0" "node": ">=14.0.0"
} }
}, },
"node_modules/@drift-labs/sdk/node_modules/bs58": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/bs58/-/bs58-4.0.1.tgz",
"integrity": "sha512-Ok3Wdf5vOIlBrgCvTq96gBkJw+JUEzdBgyaza5HLtPm7yTHkjRy8+JzNyHF7BHa0bNWOQIp3m5YF0nnFcOIKLw==",
"license": "MIT",
"dependencies": {
"base-x": "^3.0.2"
}
},
"node_modules/@drift-labs/sdk/node_modules/nanoid": { "node_modules/@drift-labs/sdk/node_modules/nanoid": {
"version": "3.3.4", "version": "3.3.4",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.4.tgz", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.4.tgz",
@@ -1509,6 +1538,15 @@
"@solana/web3.js": "^1.95.5" "@solana/web3.js": "^1.95.5"
} }
}, },
"node_modules/@openbook-dex/openbook-v2/node_modules/bs58": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/bs58/-/bs58-4.0.1.tgz",
"integrity": "sha512-Ok3Wdf5vOIlBrgCvTq96gBkJw+JUEzdBgyaza5HLtPm7yTHkjRy8+JzNyHF7BHa0bNWOQIp3m5YF0nnFcOIKLw==",
"license": "MIT",
"dependencies": {
"base-x": "^3.0.2"
}
},
"node_modules/@prisma/client": { "node_modules/@prisma/client": {
"version": "6.11.1", "version": "6.11.1",
"resolved": "https://registry.npmjs.org/@prisma/client/-/client-6.11.1.tgz", "resolved": "https://registry.npmjs.org/@prisma/client/-/client-6.11.1.tgz",
@@ -1602,6 +1640,15 @@
"node": ">=11" "node": ">=11"
} }
}, },
"node_modules/@project-serum/anchor/node_modules/bs58": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/bs58/-/bs58-4.0.1.tgz",
"integrity": "sha512-Ok3Wdf5vOIlBrgCvTq96gBkJw+JUEzdBgyaza5HLtPm7yTHkjRy8+JzNyHF7BHa0bNWOQIp3m5YF0nnFcOIKLw==",
"license": "MIT",
"dependencies": {
"base-x": "^3.0.2"
}
},
"node_modules/@project-serum/anchor/node_modules/camelcase": { "node_modules/@project-serum/anchor/node_modules/camelcase": {
"version": "5.3.1", "version": "5.3.1",
"resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz",
@@ -1807,6 +1854,15 @@
"@solana/web3.js": "^1.68.0" "@solana/web3.js": "^1.68.0"
} }
}, },
"node_modules/@pythnetwork/pyth-solana-receiver/node_modules/bs58": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/bs58/-/bs58-4.0.1.tgz",
"integrity": "sha512-Ok3Wdf5vOIlBrgCvTq96gBkJw+JUEzdBgyaza5HLtPm7yTHkjRy8+JzNyHF7BHa0bNWOQIp3m5YF0nnFcOIKLw==",
"license": "MIT",
"dependencies": {
"base-x": "^3.0.2"
}
},
"node_modules/@pythnetwork/solana-utils": { "node_modules/@pythnetwork/solana-utils": {
"version": "0.4.5", "version": "0.4.5",
"resolved": "https://registry.npmjs.org/@pythnetwork/solana-utils/-/solana-utils-0.4.5.tgz", "resolved": "https://registry.npmjs.org/@pythnetwork/solana-utils/-/solana-utils-0.4.5.tgz",
@@ -2333,6 +2389,15 @@
"@types/node": "*" "@types/node": "*"
} }
}, },
"node_modules/@solana/web3.js/node_modules/bs58": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/bs58/-/bs58-4.0.1.tgz",
"integrity": "sha512-Ok3Wdf5vOIlBrgCvTq96gBkJw+JUEzdBgyaza5HLtPm7yTHkjRy8+JzNyHF7BHa0bNWOQIp3m5YF0nnFcOIKLw==",
"license": "MIT",
"dependencies": {
"base-x": "^3.0.2"
}
},
"node_modules/@solana/web3.js/node_modules/eventemitter3": { "node_modules/@solana/web3.js/node_modules/eventemitter3": {
"version": "5.0.1", "version": "5.0.1",
"resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz", "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz",
@@ -2401,19 +2466,6 @@
"node": ">=20" "node": ">=20"
} }
}, },
"node_modules/@switchboard-xyz/common/node_modules/base-x": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/base-x/-/base-x-5.0.1.tgz",
"integrity": "sha512-M7uio8Zt++eg3jPj+rHMfCC+IuygQHHCOU+IYsVtik6FWjuYpVt/+MRKcgsAMHh8mMFAwnB+Bs+mTrFiXjMzKg=="
},
"node_modules/@switchboard-xyz/common/node_modules/bs58": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/bs58/-/bs58-6.0.0.tgz",
"integrity": "sha512-PD0wEnEYg6ijszw/u8s+iI3H17cTymlrwkKhDhPZq+Sokl3AU4htyBFTjAeNAlCCmg0f53g6ih3jATyCKftTfw==",
"dependencies": {
"base-x": "^5.0.0"
}
},
"node_modules/@switchboard-xyz/common/node_modules/js-sha256": { "node_modules/@switchboard-xyz/common/node_modules/js-sha256": {
"version": "0.11.1", "version": "0.11.1",
"resolved": "https://registry.npmjs.org/js-sha256/-/js-sha256-0.11.1.tgz", "resolved": "https://registry.npmjs.org/js-sha256/-/js-sha256-0.11.1.tgz",
@@ -2439,19 +2491,6 @@
"@switchboard-xyz/common": ">=3.0.0" "@switchboard-xyz/common": ">=3.0.0"
} }
}, },
"node_modules/@switchboard-xyz/on-demand/node_modules/base-x": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/base-x/-/base-x-5.0.1.tgz",
"integrity": "sha512-M7uio8Zt++eg3jPj+rHMfCC+IuygQHHCOU+IYsVtik6FWjuYpVt/+MRKcgsAMHh8mMFAwnB+Bs+mTrFiXjMzKg=="
},
"node_modules/@switchboard-xyz/on-demand/node_modules/bs58": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/bs58/-/bs58-6.0.0.tgz",
"integrity": "sha512-PD0wEnEYg6ijszw/u8s+iI3H17cTymlrwkKhDhPZq+Sokl3AU4htyBFTjAeNAlCCmg0f53g6ih3jATyCKftTfw==",
"dependencies": {
"base-x": "^5.0.0"
}
},
"node_modules/@tailwindcss/node": { "node_modules/@tailwindcss/node": {
"version": "4.1.11", "version": "4.1.11",
"resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.1.11.tgz", "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.1.11.tgz",
@@ -3841,6 +3880,15 @@
"text-encoding-utf-8": "^1.0.2" "text-encoding-utf-8": "^1.0.2"
} }
}, },
"node_modules/borsh/node_modules/bs58": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/bs58/-/bs58-4.0.1.tgz",
"integrity": "sha512-Ok3Wdf5vOIlBrgCvTq96gBkJw+JUEzdBgyaza5HLtPm7yTHkjRy8+JzNyHF7BHa0bNWOQIp3m5YF0nnFcOIKLw==",
"license": "MIT",
"dependencies": {
"base-x": "^3.0.2"
}
},
"node_modules/brace-expansion": { "node_modules/brace-expansion": {
"version": "1.1.12", "version": "1.1.12",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
@@ -3864,13 +3912,20 @@
} }
}, },
"node_modules/bs58": { "node_modules/bs58": {
"version": "4.0.1", "version": "6.0.0",
"resolved": "https://registry.npmjs.org/bs58/-/bs58-4.0.1.tgz", "resolved": "https://registry.npmjs.org/bs58/-/bs58-6.0.0.tgz",
"integrity": "sha512-Ok3Wdf5vOIlBrgCvTq96gBkJw+JUEzdBgyaza5HLtPm7yTHkjRy8+JzNyHF7BHa0bNWOQIp3m5YF0nnFcOIKLw==", "integrity": "sha512-PD0wEnEYg6ijszw/u8s+iI3H17cTymlrwkKhDhPZq+Sokl3AU4htyBFTjAeNAlCCmg0f53g6ih3jATyCKftTfw==",
"license": "MIT",
"dependencies": { "dependencies": {
"base-x": "^3.0.2" "base-x": "^5.0.0"
} }
}, },
"node_modules/bs58/node_modules/base-x": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/base-x/-/base-x-5.0.1.tgz",
"integrity": "sha512-M7uio8Zt++eg3jPj+rHMfCC+IuygQHHCOU+IYsVtik6FWjuYpVt/+MRKcgsAMHh8mMFAwnB+Bs+mTrFiXjMzKg==",
"license": "MIT"
},
"node_modules/buffer": { "node_modules/buffer": {
"version": "6.0.3", "version": "6.0.3",
"resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz",
@@ -6306,6 +6361,15 @@
"resolved": "https://registry.npmjs.org/superstruct/-/superstruct-0.14.2.tgz", "resolved": "https://registry.npmjs.org/superstruct/-/superstruct-0.14.2.tgz",
"integrity": "sha512-nPewA6m9mR3d6k7WkZ8N8zpTWfenFH3q9pA2PkuiZxINr9DKB2+40wEQf0ixn8VaGuJ78AB6iWOtStI+/4FKZQ==" "integrity": "sha512-nPewA6m9mR3d6k7WkZ8N8zpTWfenFH3q9pA2PkuiZxINr9DKB2+40wEQf0ixn8VaGuJ78AB6iWOtStI+/4FKZQ=="
}, },
"node_modules/jito-ts/node_modules/bs58": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/bs58/-/bs58-4.0.1.tgz",
"integrity": "sha512-Ok3Wdf5vOIlBrgCvTq96gBkJw+JUEzdBgyaza5HLtPm7yTHkjRy8+JzNyHF7BHa0bNWOQIp3m5YF0nnFcOIKLw==",
"license": "MIT",
"dependencies": {
"base-x": "^3.0.2"
}
},
"node_modules/jito-ts/node_modules/superstruct": { "node_modules/jito-ts/node_modules/superstruct": {
"version": "1.0.4", "version": "1.0.4",
"resolved": "https://registry.npmjs.org/superstruct/-/superstruct-1.0.4.tgz", "resolved": "https://registry.npmjs.org/superstruct/-/superstruct-1.0.4.tgz",
@@ -8173,6 +8237,16 @@
"node": ">= 10" "node": ">= 10"
} }
}, },
"node_modules/solana-bankrun/node_modules/bs58": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/bs58/-/bs58-4.0.1.tgz",
"integrity": "sha512-Ok3Wdf5vOIlBrgCvTq96gBkJw+JUEzdBgyaza5HLtPm7yTHkjRy8+JzNyHF7BHa0bNWOQIp3m5YF0nnFcOIKLw==",
"license": "MIT",
"peer": true,
"dependencies": {
"base-x": "^3.0.2"
}
},
"node_modules/source-map": { "node_modules/source-map": {
"version": "0.6.1", "version": "0.6.1",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",

View File

@@ -11,6 +11,7 @@
"@drift-labs/sdk": "^2.126.0-beta.14", "@drift-labs/sdk": "^2.126.0-beta.14",
"@prisma/client": "^6.11.1", "@prisma/client": "^6.11.1",
"@solana/web3.js": "^1.98.2", "@solana/web3.js": "^1.98.2",
"bs58": "^6.0.0",
"next": "15.3.5", "next": "15.3.5",
"openai": "^5.8.3", "openai": "^5.8.3",
"prisma": "^6.11.1", "prisma": "^6.11.1",

Binary file not shown.

After

Width:  |  Height:  |  Size: 198 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 206 KiB

View File

@@ -0,0 +1,11 @@
const bs58 = require('bs58');
const fs = require('fs');
// Paste your base58 private key string here
const base58 = process.argv[2];
if (!base58) {
console.error('Usage: node convert.js <base58key>');
process.exit(1);
}
const arr = Array.from(bs58.decode(base58));
console.log(JSON.stringify(arr));

View File

@@ -0,0 +1,11 @@
import bs58 from 'bs58';
import fs from 'fs';
// Paste your base58 private key string here
const base58 = process.argv[2];
if (!base58) {
console.error('Usage: node convert-solana-key.mjs <base58key>');
process.exit(1);
}
const arr = Array.from(bs58.decode(base58));
console.log(JSON.stringify(arr));