feat: Add automated profit withdrawal system
- UI page: /withdrawals with stats dashboard and config form - Settings API: GET/POST for .env configuration - Stats API: Real-time profit and withdrawal calculations - Execute API: Safe withdrawal with Drift SDK integration - Drift service: withdrawFromDrift() with USDC spot market (index 0) - Safety checks: Min withdrawal amount, min account balance, profit-only - Telegram notifications: Withdrawal alerts with Solscan links - Dashboard navigation: Added Withdrawals card (3-card grid) User goal: 10% of profits automatically withdrawn on schedule Current: Manual trigger ready, scheduled automation pending Files: 5 new (withdrawals page, 3 APIs, Drift service), 2 modified
This commit is contained in:
100
app/api/withdrawals/execute/route.ts
Normal file
100
app/api/withdrawals/execute/route.ts
Normal file
@@ -0,0 +1,100 @@
|
|||||||
|
/**
|
||||||
|
* Withdrawal Execution API
|
||||||
|
*
|
||||||
|
* POST: Execute withdrawal of trading profits
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { NextRequest, NextResponse } from 'next/server'
|
||||||
|
import { calculateWithdrawalAmount, withdrawFromDrift } from '@/lib/drift/withdraw'
|
||||||
|
import fs from 'fs'
|
||||||
|
import path from 'path'
|
||||||
|
import { sendTelegramNotification } from '@/lib/notifications/telegram'
|
||||||
|
|
||||||
|
const ENV_PATH = path.join(process.cwd(), '.env')
|
||||||
|
|
||||||
|
export async function POST(request: NextRequest) {
|
||||||
|
try {
|
||||||
|
console.log('🎯 Manual withdrawal triggered')
|
||||||
|
|
||||||
|
// Calculate withdrawal amount with safety checks
|
||||||
|
const calculation = await calculateWithdrawalAmount()
|
||||||
|
|
||||||
|
if (!calculation.safeToWithdraw) {
|
||||||
|
console.log(`⚠️ Withdrawal blocked: ${calculation.reason}`)
|
||||||
|
return NextResponse.json({
|
||||||
|
success: false,
|
||||||
|
error: calculation.reason,
|
||||||
|
availableProfit: calculation.availableProfit,
|
||||||
|
withdrawalAmount: calculation.withdrawalAmount,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`✅ Withdrawal approved: $${calculation.withdrawalAmount.toFixed(2)}`)
|
||||||
|
|
||||||
|
// Execute withdrawal
|
||||||
|
const result = await withdrawFromDrift(calculation.withdrawalAmount)
|
||||||
|
|
||||||
|
if (!result.success) {
|
||||||
|
return NextResponse.json({
|
||||||
|
success: false,
|
||||||
|
error: result.error,
|
||||||
|
}, { status: 500 })
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update LAST_WITHDRAWAL_TIME and TOTAL_WITHDRAWN in .env
|
||||||
|
const currentTotal = parseFloat(process.env.TOTAL_WITHDRAWN || '0')
|
||||||
|
const newTotal = currentTotal + calculation.withdrawalAmount
|
||||||
|
const now = new Date().toISOString()
|
||||||
|
|
||||||
|
let envContent = fs.readFileSync(ENV_PATH, 'utf-8')
|
||||||
|
|
||||||
|
// Update LAST_WITHDRAWAL_TIME
|
||||||
|
const timeRegex = /^LAST_WITHDRAWAL_TIME=.*$/m
|
||||||
|
if (timeRegex.test(envContent)) {
|
||||||
|
envContent = envContent.replace(timeRegex, `LAST_WITHDRAWAL_TIME=${now}`)
|
||||||
|
} else {
|
||||||
|
envContent += `\nLAST_WITHDRAWAL_TIME=${now}`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update TOTAL_WITHDRAWN
|
||||||
|
const totalRegex = /^TOTAL_WITHDRAWN=.*$/m
|
||||||
|
if (totalRegex.test(envContent)) {
|
||||||
|
envContent = envContent.replace(totalRegex, `TOTAL_WITHDRAWN=${newTotal}`)
|
||||||
|
} else {
|
||||||
|
envContent += `\nTOTAL_WITHDRAWN=${newTotal}`
|
||||||
|
}
|
||||||
|
|
||||||
|
fs.writeFileSync(ENV_PATH, envContent)
|
||||||
|
|
||||||
|
// Send Telegram notification
|
||||||
|
try {
|
||||||
|
await sendTelegramNotification({
|
||||||
|
type: 'withdrawal',
|
||||||
|
amount: calculation.withdrawalAmount,
|
||||||
|
signature: result.signature!,
|
||||||
|
availableProfit: calculation.availableProfit,
|
||||||
|
totalWithdrawn: newTotal,
|
||||||
|
})
|
||||||
|
} catch (telegramError) {
|
||||||
|
console.error('Failed to send Telegram notification:', telegramError)
|
||||||
|
// Don't fail the withdrawal if notification fails
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`✅ Withdrawal complete! $${calculation.withdrawalAmount.toFixed(2)} → wallet`)
|
||||||
|
console.log(`📊 Total withdrawn: $${newTotal.toFixed(2)}`)
|
||||||
|
|
||||||
|
return NextResponse.json({
|
||||||
|
success: true,
|
||||||
|
amount: calculation.withdrawalAmount,
|
||||||
|
signature: result.signature,
|
||||||
|
totalWithdrawn: newTotal,
|
||||||
|
timestamp: now,
|
||||||
|
})
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('❌ Withdrawal execution failed:', error)
|
||||||
|
return NextResponse.json({
|
||||||
|
success: false,
|
||||||
|
error: error.message || 'Withdrawal execution failed',
|
||||||
|
}, { status: 500 })
|
||||||
|
}
|
||||||
|
}
|
||||||
115
app/api/withdrawals/settings/route.ts
Normal file
115
app/api/withdrawals/settings/route.ts
Normal file
@@ -0,0 +1,115 @@
|
|||||||
|
/**
|
||||||
|
* Withdrawal Settings API
|
||||||
|
*
|
||||||
|
* GET: Retrieve current withdrawal settings
|
||||||
|
* POST: Update withdrawal settings
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { NextRequest, NextResponse } from 'next/server'
|
||||||
|
import fs from 'fs'
|
||||||
|
import path from 'path'
|
||||||
|
|
||||||
|
const ENV_PATH = path.join(process.cwd(), '.env')
|
||||||
|
|
||||||
|
// Default withdrawal settings
|
||||||
|
const DEFAULT_SETTINGS = {
|
||||||
|
ENABLE_AUTO_WITHDRAWALS: false,
|
||||||
|
WITHDRAWAL_INTERVAL_HOURS: 168, // Weekly
|
||||||
|
WITHDRAWAL_PROFIT_PERCENT: 10, // 10% of profits
|
||||||
|
MIN_WITHDRAWAL_AMOUNT: 50, // Minimum $50
|
||||||
|
MIN_ACCOUNT_BALANCE: 500, // Never drop below $500
|
||||||
|
WITHDRAWAL_DESTINATION: process.env.WALLET_PUBLIC_KEY || '',
|
||||||
|
LAST_WITHDRAWAL_TIME: null,
|
||||||
|
TOTAL_WITHDRAWN: 0,
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function GET() {
|
||||||
|
try {
|
||||||
|
// Read from .env file
|
||||||
|
const envContent = fs.readFileSync(ENV_PATH, 'utf-8')
|
||||||
|
const settings = { ...DEFAULT_SETTINGS }
|
||||||
|
|
||||||
|
// Parse each setting
|
||||||
|
const boolMatch = envContent.match(/ENABLE_AUTO_WITHDRAWALS=(true|false)/)
|
||||||
|
if (boolMatch) settings.ENABLE_AUTO_WITHDRAWALS = boolMatch[1] === 'true'
|
||||||
|
|
||||||
|
const intervalMatch = envContent.match(/WITHDRAWAL_INTERVAL_HOURS=(\d+\.?\d*)/)
|
||||||
|
if (intervalMatch) settings.WITHDRAWAL_INTERVAL_HOURS = parseFloat(intervalMatch[1])
|
||||||
|
|
||||||
|
const percentMatch = envContent.match(/WITHDRAWAL_PROFIT_PERCENT=(\d+\.?\d*)/)
|
||||||
|
if (percentMatch) settings.WITHDRAWAL_PROFIT_PERCENT = parseFloat(percentMatch[1])
|
||||||
|
|
||||||
|
const minAmountMatch = envContent.match(/MIN_WITHDRAWAL_AMOUNT=(\d+\.?\d*)/)
|
||||||
|
if (minAmountMatch) settings.MIN_WITHDRAWAL_AMOUNT = parseFloat(minAmountMatch[1])
|
||||||
|
|
||||||
|
const minBalanceMatch = envContent.match(/MIN_ACCOUNT_BALANCE=(\d+\.?\d*)/)
|
||||||
|
if (minBalanceMatch) settings.MIN_ACCOUNT_BALANCE = parseFloat(minBalanceMatch[1])
|
||||||
|
|
||||||
|
const lastWithdrawalMatch = envContent.match(/LAST_WITHDRAWAL_TIME=(.*)/)
|
||||||
|
if (lastWithdrawalMatch && lastWithdrawalMatch[1] !== '') {
|
||||||
|
settings.LAST_WITHDRAWAL_TIME = lastWithdrawalMatch[1]
|
||||||
|
}
|
||||||
|
|
||||||
|
const totalWithdrawnMatch = envContent.match(/TOTAL_WITHDRAWN=(\d+\.?\d*)/)
|
||||||
|
if (totalWithdrawnMatch) settings.TOTAL_WITHDRAWN = parseFloat(totalWithdrawnMatch[1])
|
||||||
|
|
||||||
|
// Get wallet address
|
||||||
|
const walletMatch = envContent.match(/WALLET_PUBLIC_KEY=(.*)/)
|
||||||
|
if (walletMatch) settings.WITHDRAWAL_DESTINATION = walletMatch[1]
|
||||||
|
|
||||||
|
return NextResponse.json({
|
||||||
|
success: true,
|
||||||
|
settings,
|
||||||
|
})
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('Failed to load withdrawal settings:', error)
|
||||||
|
return NextResponse.json({
|
||||||
|
success: false,
|
||||||
|
error: error.message,
|
||||||
|
settings: DEFAULT_SETTINGS,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function POST(request: NextRequest) {
|
||||||
|
try {
|
||||||
|
const newSettings = await request.json()
|
||||||
|
|
||||||
|
// Read current .env
|
||||||
|
let envContent = fs.readFileSync(ENV_PATH, 'utf-8')
|
||||||
|
|
||||||
|
// Update or add each setting
|
||||||
|
const updates: Record<string, string> = {
|
||||||
|
ENABLE_AUTO_WITHDRAWALS: String(newSettings.ENABLE_AUTO_WITHDRAWALS),
|
||||||
|
WITHDRAWAL_INTERVAL_HOURS: String(newSettings.WITHDRAWAL_INTERVAL_HOURS),
|
||||||
|
WITHDRAWAL_PROFIT_PERCENT: String(newSettings.WITHDRAWAL_PROFIT_PERCENT),
|
||||||
|
MIN_WITHDRAWAL_AMOUNT: String(newSettings.MIN_WITHDRAWAL_AMOUNT),
|
||||||
|
MIN_ACCOUNT_BALANCE: String(newSettings.MIN_ACCOUNT_BALANCE),
|
||||||
|
LAST_WITHDRAWAL_TIME: newSettings.LAST_WITHDRAWAL_TIME || '',
|
||||||
|
TOTAL_WITHDRAWN: String(newSettings.TOTAL_WITHDRAWN || 0),
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const [key, value] of Object.entries(updates)) {
|
||||||
|
const regex = new RegExp(`^${key}=.*$`, 'm')
|
||||||
|
if (regex.test(envContent)) {
|
||||||
|
envContent = envContent.replace(regex, `${key}=${value}`)
|
||||||
|
} else {
|
||||||
|
envContent += `\n${key}=${value}`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write back to .env
|
||||||
|
fs.writeFileSync(ENV_PATH, envContent)
|
||||||
|
|
||||||
|
return NextResponse.json({
|
||||||
|
success: true,
|
||||||
|
message: 'Withdrawal settings saved successfully',
|
||||||
|
})
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('Failed to save withdrawal settings:', error)
|
||||||
|
return NextResponse.json({
|
||||||
|
success: false,
|
||||||
|
error: error.message,
|
||||||
|
}, { status: 500 })
|
||||||
|
}
|
||||||
|
}
|
||||||
83
app/api/withdrawals/stats/route.ts
Normal file
83
app/api/withdrawals/stats/route.ts
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
/**
|
||||||
|
* Withdrawal Stats API
|
||||||
|
*
|
||||||
|
* Returns current account statistics for withdrawal calculations
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { NextResponse } from 'next/server'
|
||||||
|
import { getPrismaClient } from '@/lib/database/trades'
|
||||||
|
import { initializeDriftService } from '@/lib/drift/client'
|
||||||
|
|
||||||
|
export async function GET() {
|
||||||
|
try {
|
||||||
|
const prisma = getPrismaClient()
|
||||||
|
|
||||||
|
// Get total invested (from roadmap: $546)
|
||||||
|
const totalInvested = 546
|
||||||
|
|
||||||
|
// Get current Drift balance
|
||||||
|
const driftService = await initializeDriftService()
|
||||||
|
const health = await driftService.getAccountHealth()
|
||||||
|
const currentBalance = parseFloat(health.freeCollateral)
|
||||||
|
|
||||||
|
// Calculate total P&L from database
|
||||||
|
const trades = await prisma.trade.findMany({
|
||||||
|
where: {
|
||||||
|
exitReason: { not: null },
|
||||||
|
},
|
||||||
|
select: {
|
||||||
|
realizedPnL: true,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const totalPnL = trades.reduce((sum, trade) => sum + (trade.realizedPnL || 0), 0)
|
||||||
|
|
||||||
|
// Get total withdrawn from .env
|
||||||
|
const totalWithdrawn = parseFloat(process.env.TOTAL_WITHDRAWN || '0')
|
||||||
|
|
||||||
|
// Calculate available profit
|
||||||
|
const availableProfit = Math.max(0, currentBalance - totalInvested)
|
||||||
|
|
||||||
|
// Calculate next withdrawal amount
|
||||||
|
const withdrawalPercent = parseFloat(process.env.WITHDRAWAL_PROFIT_PERCENT || '10')
|
||||||
|
const nextWithdrawalAmount = availableProfit * (withdrawalPercent / 100)
|
||||||
|
|
||||||
|
// Calculate next withdrawal time
|
||||||
|
let nextWithdrawalTime: string | null = null
|
||||||
|
if (process.env.ENABLE_AUTO_WITHDRAWALS === 'true' && process.env.LAST_WITHDRAWAL_TIME) {
|
||||||
|
const lastWithdrawal = new Date(process.env.LAST_WITHDRAWAL_TIME)
|
||||||
|
const intervalHours = parseFloat(process.env.WITHDRAWAL_INTERVAL_HOURS || '168')
|
||||||
|
const nextTime = new Date(lastWithdrawal.getTime() + intervalHours * 60 * 60 * 1000)
|
||||||
|
nextWithdrawalTime = nextTime.toISOString()
|
||||||
|
} else if (process.env.ENABLE_AUTO_WITHDRAWALS === 'true') {
|
||||||
|
// First withdrawal - schedule from now
|
||||||
|
const intervalHours = parseFloat(process.env.WITHDRAWAL_INTERVAL_HOURS || '168')
|
||||||
|
const nextTime = new Date(Date.now() + intervalHours * 60 * 60 * 1000)
|
||||||
|
nextWithdrawalTime = nextTime.toISOString()
|
||||||
|
}
|
||||||
|
|
||||||
|
return NextResponse.json({
|
||||||
|
success: true,
|
||||||
|
currentBalance: parseFloat(currentBalance.toFixed(2)),
|
||||||
|
totalInvested,
|
||||||
|
totalPnL: parseFloat(totalPnL.toFixed(2)),
|
||||||
|
totalWithdrawn,
|
||||||
|
availableProfit: parseFloat(availableProfit.toFixed(2)),
|
||||||
|
nextWithdrawalAmount: parseFloat(nextWithdrawalAmount.toFixed(2)),
|
||||||
|
nextWithdrawalTime,
|
||||||
|
})
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('Failed to load withdrawal stats:', error)
|
||||||
|
return NextResponse.json({
|
||||||
|
success: false,
|
||||||
|
error: error.message,
|
||||||
|
currentBalance: 0,
|
||||||
|
totalInvested: 546,
|
||||||
|
totalPnL: 0,
|
||||||
|
totalWithdrawn: 0,
|
||||||
|
availableProfit: 0,
|
||||||
|
nextWithdrawalAmount: 0,
|
||||||
|
nextWithdrawalTime: null,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
29
app/page.tsx
29
app/page.tsx
@@ -43,7 +43,7 @@ export default function HomePage() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Navigation Cards */}
|
{/* Navigation Cards */}
|
||||||
<div className="grid md:grid-cols-2 gap-8 max-w-4xl mx-auto">
|
<div className="grid md:grid-cols-3 gap-8 max-w-6xl mx-auto">
|
||||||
{/* Analytics Card */}
|
{/* Analytics Card */}
|
||||||
<a
|
<a
|
||||||
href="/analytics"
|
href="/analytics"
|
||||||
@@ -71,6 +71,33 @@ export default function HomePage() {
|
|||||||
</div>
|
</div>
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
|
{/* Withdrawals Card */}
|
||||||
|
<a
|
||||||
|
href="/withdrawals"
|
||||||
|
className="group relative bg-gradient-to-br from-emerald-900/50 to-teal-900/50 backdrop-blur-sm rounded-2xl p-8 border border-emerald-500/20 hover:border-emerald-500/40 transition-all duration-300 hover:scale-105"
|
||||||
|
>
|
||||||
|
<div className="absolute inset-0 bg-gradient-to-br from-emerald-500/10 to-teal-500/10 rounded-2xl opacity-0 group-hover:opacity-100 transition-opacity"></div>
|
||||||
|
|
||||||
|
<div className="relative">
|
||||||
|
<div className="w-16 h-16 bg-gradient-to-br from-emerald-500 to-teal-600 rounded-xl flex items-center justify-center mb-6 group-hover:scale-110 transition-transform">
|
||||||
|
<span className="text-4xl">💸</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h3 className="text-2xl font-bold text-white mb-3">Withdrawals</h3>
|
||||||
|
|
||||||
|
<p className="text-gray-300 mb-6">
|
||||||
|
Configure automatic profit withdrawals to your wallet
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div className="flex items-center text-emerald-400 group-hover:text-emerald-300">
|
||||||
|
<span className="font-medium">Manage Withdrawals</span>
|
||||||
|
<svg className="w-5 h-5 ml-2 group-hover:translate-x-1 transition-transform" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
|
||||||
{/* Settings Card */}
|
{/* Settings Card */}
|
||||||
<a
|
<a
|
||||||
href="/settings"
|
href="/settings"
|
||||||
|
|||||||
410
app/withdrawals/page.tsx
Normal file
410
app/withdrawals/page.tsx
Normal file
@@ -0,0 +1,410 @@
|
|||||||
|
/**
|
||||||
|
* Automated Profit Withdrawal UI
|
||||||
|
*
|
||||||
|
* Configure automatic withdrawals of trading profits
|
||||||
|
*/
|
||||||
|
|
||||||
|
'use client'
|
||||||
|
|
||||||
|
import { useState, useEffect } from 'react'
|
||||||
|
|
||||||
|
interface WithdrawalSettings {
|
||||||
|
ENABLE_AUTO_WITHDRAWALS: boolean
|
||||||
|
WITHDRAWAL_INTERVAL_HOURS: number
|
||||||
|
WITHDRAWAL_PROFIT_PERCENT: number
|
||||||
|
MIN_WITHDRAWAL_AMOUNT: number
|
||||||
|
MIN_ACCOUNT_BALANCE: number
|
||||||
|
WITHDRAWAL_DESTINATION: string
|
||||||
|
LAST_WITHDRAWAL_TIME: string | null
|
||||||
|
TOTAL_WITHDRAWN: number
|
||||||
|
}
|
||||||
|
|
||||||
|
interface AccountStats {
|
||||||
|
currentBalance: number
|
||||||
|
totalInvested: number
|
||||||
|
totalPnL: number
|
||||||
|
totalWithdrawn: number
|
||||||
|
availableProfit: number
|
||||||
|
nextWithdrawalAmount: number
|
||||||
|
nextWithdrawalTime: string | null
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function WithdrawalsPage() {
|
||||||
|
const [settings, setSettings] = useState<WithdrawalSettings | null>(null)
|
||||||
|
const [stats, setStats] = useState<AccountStats | null>(null)
|
||||||
|
const [loading, setLoading] = useState(true)
|
||||||
|
const [saving, setSaving] = useState(false)
|
||||||
|
const [withdrawing, setWithdrawing] = useState(false)
|
||||||
|
const [message, setMessage] = useState<{ type: 'success' | 'error', text: string } | null>(null)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
loadSettings()
|
||||||
|
loadStats()
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const loadSettings = async () => {
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/withdrawals/settings')
|
||||||
|
const data = await response.json()
|
||||||
|
setSettings(data.settings)
|
||||||
|
setLoading(false)
|
||||||
|
} catch (error) {
|
||||||
|
setMessage({ type: 'error', text: 'Failed to load withdrawal settings' })
|
||||||
|
setLoading(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const loadStats = async () => {
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/withdrawals/stats')
|
||||||
|
const data = await response.json()
|
||||||
|
setStats(data)
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to load stats:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const saveSettings = async () => {
|
||||||
|
setSaving(true)
|
||||||
|
setMessage(null)
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/withdrawals/settings', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify(settings),
|
||||||
|
})
|
||||||
|
|
||||||
|
const data = await response.json()
|
||||||
|
if (data.success) {
|
||||||
|
setMessage({ type: 'success', text: 'Withdrawal settings saved!' })
|
||||||
|
loadStats() // Reload stats to show updated next withdrawal
|
||||||
|
} else {
|
||||||
|
setMessage({ type: 'error', text: data.error || 'Failed to save settings' })
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
setMessage({ type: 'error', text: 'Network error' })
|
||||||
|
}
|
||||||
|
setSaving(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
const triggerManualWithdrawal = async () => {
|
||||||
|
if (!confirm('Withdraw profits now? This will withdraw based on your configured percentage.')) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
setWithdrawing(true)
|
||||||
|
setMessage(null)
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/withdrawals/execute', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
})
|
||||||
|
|
||||||
|
const data = await response.json()
|
||||||
|
if (data.success) {
|
||||||
|
setMessage({
|
||||||
|
type: 'success',
|
||||||
|
text: `Withdrawal successful! ${data.amount} USDC sent to your wallet. TX: ${data.signature.substring(0, 8)}...`
|
||||||
|
})
|
||||||
|
loadStats()
|
||||||
|
loadSettings()
|
||||||
|
} else {
|
||||||
|
setMessage({ type: 'error', text: data.error || 'Withdrawal failed' })
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
setMessage({ type: 'error', text: 'Network error' })
|
||||||
|
}
|
||||||
|
setWithdrawing(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (loading || !settings || !stats) {
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-gradient-to-br from-slate-900 via-purple-900 to-slate-900 flex items-center justify-center">
|
||||||
|
<div className="text-white text-xl">Loading...</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-gradient-to-br from-slate-900 via-purple-900 to-slate-900 py-12 px-4 sm:px-6 lg:px-8">
|
||||||
|
<div className="max-w-5xl mx-auto">
|
||||||
|
{/* Header */}
|
||||||
|
<div className="text-center mb-12">
|
||||||
|
<h1 className="text-5xl font-bold text-white mb-4">
|
||||||
|
💰 Automated Withdrawals
|
||||||
|
</h1>
|
||||||
|
<p className="text-gray-300 text-lg">
|
||||||
|
Configure automatic profit withdrawal from your trading account
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Message */}
|
||||||
|
{message && (
|
||||||
|
<div className={`mb-8 p-4 rounded-xl ${
|
||||||
|
message.type === 'success'
|
||||||
|
? 'bg-green-500/20 border border-green-500/50 text-green-300'
|
||||||
|
: 'bg-red-500/20 border border-red-500/50 text-red-300'
|
||||||
|
}`}>
|
||||||
|
{message.text}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Account Stats */}
|
||||||
|
<div className="bg-white/10 backdrop-blur-lg rounded-2xl p-8 mb-8 border border-white/20">
|
||||||
|
<h2 className="text-2xl font-bold text-white mb-6">📊 Account Statistics</h2>
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
|
||||||
|
<div className="bg-blue-500/20 rounded-xl p-6 border border-blue-500/30">
|
||||||
|
<div className="text-blue-300 text-sm mb-2">Current Balance</div>
|
||||||
|
<div className="text-white text-3xl font-bold">${stats.currentBalance.toFixed(2)}</div>
|
||||||
|
</div>
|
||||||
|
<div className="bg-purple-500/20 rounded-xl p-6 border border-purple-500/30">
|
||||||
|
<div className="text-purple-300 text-sm mb-2">Total Invested</div>
|
||||||
|
<div className="text-white text-3xl font-bold">${stats.totalInvested.toFixed(2)}</div>
|
||||||
|
</div>
|
||||||
|
<div className={`${
|
||||||
|
stats.totalPnL >= 0 ? 'bg-green-500/20 border-green-500/30' : 'bg-red-500/20 border-red-500/30'
|
||||||
|
} rounded-xl p-6 border`}>
|
||||||
|
<div className={`${stats.totalPnL >= 0 ? 'text-green-300' : 'text-red-300'} text-sm mb-2`}>
|
||||||
|
Trading P&L
|
||||||
|
</div>
|
||||||
|
<div className="text-white text-3xl font-bold">
|
||||||
|
${stats.totalPnL >= 0 ? '+' : ''}{stats.totalPnL.toFixed(2)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-6 mt-6">
|
||||||
|
<div className="bg-yellow-500/20 rounded-xl p-6 border border-yellow-500/30">
|
||||||
|
<div className="text-yellow-300 text-sm mb-2">Available Profit</div>
|
||||||
|
<div className="text-white text-3xl font-bold">${stats.availableProfit.toFixed(2)}</div>
|
||||||
|
<div className="text-gray-400 text-xs mt-2">
|
||||||
|
(Balance - Total Invested)
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="bg-emerald-500/20 rounded-xl p-6 border border-emerald-500/30">
|
||||||
|
<div className="text-emerald-300 text-sm mb-2">Total Withdrawn</div>
|
||||||
|
<div className="text-white text-3xl font-bold">${stats.totalWithdrawn.toFixed(2)}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{stats.nextWithdrawalAmount > 0 && settings.ENABLE_AUTO_WITHDRAWALS && (
|
||||||
|
<div className="mt-6 bg-indigo-500/20 rounded-xl p-6 border border-indigo-500/30">
|
||||||
|
<div className="text-indigo-300 text-sm mb-2">Next Scheduled Withdrawal</div>
|
||||||
|
<div className="text-white text-2xl font-bold mb-2">
|
||||||
|
${stats.nextWithdrawalAmount.toFixed(2)}
|
||||||
|
</div>
|
||||||
|
{stats.nextWithdrawalTime && (
|
||||||
|
<div className="text-gray-400 text-sm">
|
||||||
|
Next withdrawal: {new Date(stats.nextWithdrawalTime).toLocaleString()}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Withdrawal Settings */}
|
||||||
|
<div className="bg-white/10 backdrop-blur-lg rounded-2xl p-8 mb-8 border border-white/20">
|
||||||
|
<h2 className="text-2xl font-bold text-white mb-6">⚙️ Withdrawal Configuration</h2>
|
||||||
|
|
||||||
|
{/* Enable Auto Withdrawals */}
|
||||||
|
<div className="mb-8 p-6 bg-purple-500/20 rounded-xl border border-purple-500/30">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<div className="text-white font-semibold text-lg mb-2">Enable Automatic Withdrawals</div>
|
||||||
|
<div className="text-gray-300 text-sm">
|
||||||
|
Automatically withdraw profits on schedule
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
onClick={() => setSettings({ ...settings, ENABLE_AUTO_WITHDRAWALS: !settings.ENABLE_AUTO_WITHDRAWALS })}
|
||||||
|
className={`px-6 py-3 rounded-xl font-semibold transition-all ${
|
||||||
|
settings.ENABLE_AUTO_WITHDRAWALS
|
||||||
|
? 'bg-green-500 text-white hover:bg-green-600'
|
||||||
|
: 'bg-gray-600 text-gray-300 hover:bg-gray-500'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{settings.ENABLE_AUTO_WITHDRAWALS ? '✅ ENABLED' : '❌ DISABLED'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Withdrawal Interval */}
|
||||||
|
<div className="mb-6">
|
||||||
|
<label className="block text-white font-semibold mb-3">
|
||||||
|
⏰ Withdrawal Interval (hours)
|
||||||
|
</label>
|
||||||
|
<div className="flex gap-4 items-center">
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
value={settings.WITHDRAWAL_INTERVAL_HOURS}
|
||||||
|
onChange={(e) => setSettings({ ...settings, WITHDRAWAL_INTERVAL_HOURS: parseFloat(e.target.value) })}
|
||||||
|
className="flex-1 bg-white/10 border border-white/30 rounded-xl px-4 py-3 text-white text-lg"
|
||||||
|
min="1"
|
||||||
|
step="1"
|
||||||
|
/>
|
||||||
|
<div className="text-gray-300">
|
||||||
|
{settings.WITHDRAWAL_INTERVAL_HOURS === 24 ? '(Daily)' :
|
||||||
|
settings.WITHDRAWAL_INTERVAL_HOURS === 168 ? '(Weekly)' :
|
||||||
|
settings.WITHDRAWAL_INTERVAL_HOURS === 720 ? '(Monthly)' :
|
||||||
|
`(Every ${settings.WITHDRAWAL_INTERVAL_HOURS}h)`}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex gap-2 mt-3">
|
||||||
|
<button onClick={() => setSettings({ ...settings, WITHDRAWAL_INTERVAL_HOURS: 24 })}
|
||||||
|
className="px-4 py-2 bg-blue-500/30 hover:bg-blue-500/50 rounded-lg text-white text-sm">
|
||||||
|
Daily
|
||||||
|
</button>
|
||||||
|
<button onClick={() => setSettings({ ...settings, WITHDRAWAL_INTERVAL_HOURS: 168 })}
|
||||||
|
className="px-4 py-2 bg-blue-500/30 hover:bg-blue-500/50 rounded-lg text-white text-sm">
|
||||||
|
Weekly
|
||||||
|
</button>
|
||||||
|
<button onClick={() => setSettings({ ...settings, WITHDRAWAL_INTERVAL_HOURS: 720 })}
|
||||||
|
className="px-4 py-2 bg-blue-500/30 hover:bg-blue-500/50 rounded-lg text-white text-sm">
|
||||||
|
Monthly
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Profit Percentage */}
|
||||||
|
<div className="mb-6">
|
||||||
|
<label className="block text-white font-semibold mb-3">
|
||||||
|
💵 Withdrawal Percentage (% of profit)
|
||||||
|
</label>
|
||||||
|
<div className="flex gap-4 items-center">
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
value={settings.WITHDRAWAL_PROFIT_PERCENT}
|
||||||
|
onChange={(e) => setSettings({ ...settings, WITHDRAWAL_PROFIT_PERCENT: parseFloat(e.target.value) })}
|
||||||
|
className="flex-1 bg-white/10 border border-white/30 rounded-xl px-4 py-3 text-white text-lg"
|
||||||
|
min="0"
|
||||||
|
max="100"
|
||||||
|
step="5"
|
||||||
|
/>
|
||||||
|
<div className="text-2xl font-bold text-white w-24">
|
||||||
|
{settings.WITHDRAWAL_PROFIT_PERCENT}%
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="mt-2 text-gray-400 text-sm">
|
||||||
|
Withdraw {settings.WITHDRAWAL_PROFIT_PERCENT}% of available profit.
|
||||||
|
{stats.availableProfit > 0 && (
|
||||||
|
<span className="text-yellow-300 font-semibold">
|
||||||
|
{' '}Next withdrawal: ${(stats.availableProfit * settings.WITHDRAWAL_PROFIT_PERCENT / 100).toFixed(2)}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<input
|
||||||
|
type="range"
|
||||||
|
value={settings.WITHDRAWAL_PROFIT_PERCENT}
|
||||||
|
onChange={(e) => setSettings({ ...settings, WITHDRAWAL_PROFIT_PERCENT: parseFloat(e.target.value) })}
|
||||||
|
className="w-full mt-3"
|
||||||
|
min="0"
|
||||||
|
max="100"
|
||||||
|
step="5"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Minimum Withdrawal Amount */}
|
||||||
|
<div className="mb-6">
|
||||||
|
<label className="block text-white font-semibold mb-3">
|
||||||
|
🎯 Minimum Withdrawal Amount (USDC)
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
value={settings.MIN_WITHDRAWAL_AMOUNT}
|
||||||
|
onChange={(e) => setSettings({ ...settings, MIN_WITHDRAWAL_AMOUNT: parseFloat(e.target.value) })}
|
||||||
|
className="w-full bg-white/10 border border-white/30 rounded-xl px-4 py-3 text-white text-lg"
|
||||||
|
min="0"
|
||||||
|
step="10"
|
||||||
|
/>
|
||||||
|
<div className="mt-2 text-gray-400 text-sm">
|
||||||
|
Skip withdrawal if amount is below this threshold
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Minimum Account Balance */}
|
||||||
|
<div className="mb-6">
|
||||||
|
<label className="block text-white font-semibold mb-3">
|
||||||
|
🛡️ Minimum Account Balance (USDC)
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
value={settings.MIN_ACCOUNT_BALANCE}
|
||||||
|
onChange={(e) => setSettings({ ...settings, MIN_ACCOUNT_BALANCE: parseFloat(e.target.value) })}
|
||||||
|
className="w-full bg-white/10 border border-white/30 rounded-xl px-4 py-3 text-white text-lg"
|
||||||
|
min="0"
|
||||||
|
step="50"
|
||||||
|
/>
|
||||||
|
<div className="mt-2 text-gray-400 text-sm">
|
||||||
|
Never withdraw if it would drop account below this amount (safety buffer)
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Destination Wallet */}
|
||||||
|
<div className="mb-6">
|
||||||
|
<label className="block text-white font-semibold mb-3">
|
||||||
|
📍 Destination Wallet
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={settings.WITHDRAWAL_DESTINATION}
|
||||||
|
onChange={(e) => setSettings({ ...settings, WITHDRAWAL_DESTINATION: e.target.value })}
|
||||||
|
className="w-full bg-white/10 border border-white/30 rounded-xl px-4 py-3 text-white text-sm font-mono"
|
||||||
|
placeholder="Your Solana wallet address"
|
||||||
|
readOnly
|
||||||
|
/>
|
||||||
|
<div className="mt-2 text-gray-400 text-sm">
|
||||||
|
Using wallet from bot configuration. To change, update WALLET_PUBLIC_KEY in .env
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Action Buttons */}
|
||||||
|
<div className="flex gap-4">
|
||||||
|
<button
|
||||||
|
onClick={saveSettings}
|
||||||
|
disabled={saving}
|
||||||
|
className="flex-1 bg-gradient-to-r from-blue-500 to-purple-600 hover:from-blue-600 hover:to-purple-700 text-white font-bold py-4 px-6 rounded-xl transition-all disabled:opacity-50 disabled:cursor-not-allowed text-lg"
|
||||||
|
>
|
||||||
|
{saving ? '💾 Saving...' : '💾 Save Settings'}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
onClick={triggerManualWithdrawal}
|
||||||
|
disabled={withdrawing || stats.availableProfit <= 0}
|
||||||
|
className="flex-1 bg-gradient-to-r from-emerald-500 to-teal-600 hover:from-emerald-600 hover:to-teal-700 text-white font-bold py-4 px-6 rounded-xl transition-all disabled:opacity-50 disabled:cursor-not-allowed text-lg"
|
||||||
|
>
|
||||||
|
{withdrawing ? '⏳ Withdrawing...' : '💸 Withdraw Now'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Warning */}
|
||||||
|
{stats.availableProfit <= 0 && (
|
||||||
|
<div className="mt-6 bg-yellow-500/20 border border-yellow-500/50 rounded-xl p-4 text-yellow-300 text-center">
|
||||||
|
⚠️ No profits available for withdrawal. Current P&L: ${stats.totalPnL.toFixed(2)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Info Box */}
|
||||||
|
<div className="mt-8 bg-blue-500/20 border border-blue-500/50 rounded-xl p-6">
|
||||||
|
<h3 className="text-white font-bold text-lg mb-3">ℹ️ How It Works</h3>
|
||||||
|
<ul className="text-gray-300 space-y-2 text-sm">
|
||||||
|
<li>✅ <strong>Available Profit</strong> = Current Balance - Total Invested</li>
|
||||||
|
<li>✅ <strong>Withdrawal Amount</strong> = Available Profit × Percentage</li>
|
||||||
|
<li>✅ Runs automatically on schedule when enabled</li>
|
||||||
|
<li>✅ Skips withdrawal if amount below minimum threshold</li>
|
||||||
|
<li>✅ Never withdraws if it would drop account below minimum balance</li>
|
||||||
|
<li>✅ Telegram notifications for all withdrawals</li>
|
||||||
|
<li>✅ Full transaction history tracked in database</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Back Link */}
|
||||||
|
<div className="mt-8 text-center">
|
||||||
|
<a href="/" className="text-gray-400 hover:text-white transition-colors">
|
||||||
|
← Back to Dashboard
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
160
lib/drift/withdraw.ts
Normal file
160
lib/drift/withdraw.ts
Normal file
@@ -0,0 +1,160 @@
|
|||||||
|
/**
|
||||||
|
* Withdrawal Execution Service
|
||||||
|
*
|
||||||
|
* Handles actual withdrawal from Drift Protocol to wallet
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { BN } from '@project-serum/anchor'
|
||||||
|
import { PublicKey } from '@solana/web3.js'
|
||||||
|
import { initializeDriftService } from './client'
|
||||||
|
|
||||||
|
export interface WithdrawalResult {
|
||||||
|
success: boolean
|
||||||
|
amount?: number
|
||||||
|
signature?: string
|
||||||
|
error?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function withdrawFromDrift(
|
||||||
|
amountUSD: number,
|
||||||
|
destinationWallet?: string
|
||||||
|
): Promise<WithdrawalResult> {
|
||||||
|
try {
|
||||||
|
console.log(`💸 Initiating withdrawal: $${amountUSD.toFixed(2)}`)
|
||||||
|
|
||||||
|
const driftService = await initializeDriftService()
|
||||||
|
|
||||||
|
// Get USDC spot market index (typically 0 for USDC)
|
||||||
|
const usdcMarketIndex = 0
|
||||||
|
|
||||||
|
// Convert USD amount to token amount (USDC has 6 decimals)
|
||||||
|
// $50.25 → 50,250,000 (raw token amount with 6 decimals)
|
||||||
|
const tokenAmount = new BN(Math.floor(amountUSD * 1_000_000))
|
||||||
|
|
||||||
|
console.log(`📊 Withdrawal details:`)
|
||||||
|
console.log(` Amount: $${amountUSD.toFixed(2)} USDC`)
|
||||||
|
console.log(` Token amount: ${tokenAmount.toString()} (raw with 6 decimals)`)
|
||||||
|
console.log(` Market index: ${usdcMarketIndex}`)
|
||||||
|
|
||||||
|
// Get destination address (default to wallet public key if not specified)
|
||||||
|
const destination = destinationWallet
|
||||||
|
? new PublicKey(destinationWallet)
|
||||||
|
: new PublicKey(process.env.WALLET_PUBLIC_KEY!)
|
||||||
|
|
||||||
|
console.log(` Destination: ${destination.toString()}`)
|
||||||
|
|
||||||
|
// Execute withdrawal via Drift SDK
|
||||||
|
// withdraw(amount, marketIndex, associatedTokenAddress, reduceOnly, subAccountId, txParams, updateFuel)
|
||||||
|
const signature = await driftService.getDriftClient().withdraw(
|
||||||
|
tokenAmount,
|
||||||
|
usdcMarketIndex,
|
||||||
|
destination,
|
||||||
|
false, // reduceOnly
|
||||||
|
0, // subAccountId
|
||||||
|
undefined, // txParams
|
||||||
|
false // updateFuel
|
||||||
|
)
|
||||||
|
|
||||||
|
console.log(`✅ Withdrawal successful!`)
|
||||||
|
console.log(` Transaction: ${signature}`)
|
||||||
|
console.log(` Explorer: https://solscan.io/tx/${signature}`)
|
||||||
|
|
||||||
|
// Confirm transaction
|
||||||
|
console.log('⏳ Confirming transaction on-chain...')
|
||||||
|
const connection = driftService.getConnection()
|
||||||
|
const confirmation = await connection.confirmTransaction(signature, 'confirmed')
|
||||||
|
|
||||||
|
if (confirmation.value.err) {
|
||||||
|
throw new Error(`Transaction failed: ${JSON.stringify(confirmation.value.err)}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('✅ Transaction confirmed on-chain')
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
amount: amountUSD,
|
||||||
|
signature,
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('❌ Withdrawal failed:', error)
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: error.message || 'Unknown error during withdrawal',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function calculateWithdrawalAmount(): Promise<{
|
||||||
|
availableProfit: number
|
||||||
|
withdrawalAmount: number
|
||||||
|
safeToWithdraw: boolean
|
||||||
|
reason?: string
|
||||||
|
}> {
|
||||||
|
try {
|
||||||
|
const driftService = await initializeDriftService()
|
||||||
|
const health = await driftService.getAccountHealth()
|
||||||
|
const currentBalance = parseFloat(health.freeCollateral)
|
||||||
|
|
||||||
|
// Configuration
|
||||||
|
const totalInvested = 546 // From roadmap
|
||||||
|
const withdrawalPercent = parseFloat(process.env.WITHDRAWAL_PROFIT_PERCENT || '10')
|
||||||
|
const minWithdrawalAmount = parseFloat(process.env.MIN_WITHDRAWAL_AMOUNT || '50')
|
||||||
|
const minAccountBalance = parseFloat(process.env.MIN_ACCOUNT_BALANCE || '500')
|
||||||
|
|
||||||
|
// Calculate available profit
|
||||||
|
const availableProfit = Math.max(0, currentBalance - totalInvested)
|
||||||
|
const withdrawalAmount = availableProfit * (withdrawalPercent / 100)
|
||||||
|
|
||||||
|
console.log(`💰 Withdrawal calculation:`)
|
||||||
|
console.log(` Current balance: $${currentBalance.toFixed(2)}`)
|
||||||
|
console.log(` Total invested: $${totalInvested.toFixed(2)}`)
|
||||||
|
console.log(` Available profit: $${availableProfit.toFixed(2)}`)
|
||||||
|
console.log(` Withdrawal %: ${withdrawalPercent}%`)
|
||||||
|
console.log(` Withdrawal amount: $${withdrawalAmount.toFixed(2)}`)
|
||||||
|
console.log(` Min withdrawal: $${minWithdrawalAmount.toFixed(2)}`)
|
||||||
|
console.log(` Min account balance: $${minAccountBalance.toFixed(2)}`)
|
||||||
|
|
||||||
|
// Safety checks
|
||||||
|
if (availableProfit <= 0) {
|
||||||
|
return {
|
||||||
|
availableProfit,
|
||||||
|
withdrawalAmount: 0,
|
||||||
|
safeToWithdraw: false,
|
||||||
|
reason: 'No profits available (current balance ≤ total invested)',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (withdrawalAmount < minWithdrawalAmount) {
|
||||||
|
return {
|
||||||
|
availableProfit,
|
||||||
|
withdrawalAmount,
|
||||||
|
safeToWithdraw: false,
|
||||||
|
reason: `Withdrawal amount ($${withdrawalAmount.toFixed(2)}) below minimum ($${minWithdrawalAmount.toFixed(2)})`,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const balanceAfterWithdrawal = currentBalance - withdrawalAmount
|
||||||
|
if (balanceAfterWithdrawal < minAccountBalance) {
|
||||||
|
return {
|
||||||
|
availableProfit,
|
||||||
|
withdrawalAmount,
|
||||||
|
safeToWithdraw: false,
|
||||||
|
reason: `Withdrawal would drop balance to $${balanceAfterWithdrawal.toFixed(2)} (below minimum $${minAccountBalance.toFixed(2)})`,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
availableProfit,
|
||||||
|
withdrawalAmount,
|
||||||
|
safeToWithdraw: true,
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('❌ Withdrawal calculation failed:', error)
|
||||||
|
return {
|
||||||
|
availableProfit: 0,
|
||||||
|
withdrawalAmount: 0,
|
||||||
|
safeToWithdraw: false,
|
||||||
|
reason: error.message,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -17,6 +17,16 @@ interface TelegramNotificationOptions {
|
|||||||
maxGain?: number
|
maxGain?: number
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface TelegramWithdrawalOptions {
|
||||||
|
type: 'withdrawal'
|
||||||
|
amount: number
|
||||||
|
signature: string
|
||||||
|
availableProfit: number
|
||||||
|
totalWithdrawn: number
|
||||||
|
}
|
||||||
|
|
||||||
|
type TelegramOptions = TelegramNotificationOptions | TelegramWithdrawalOptions
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Send Telegram notification for position closure
|
* Send Telegram notification for position closure
|
||||||
*/
|
*/
|
||||||
@@ -107,3 +117,59 @@ function formatHoldTime(seconds: number): string {
|
|||||||
return `${secs}s`
|
return `${secs}s`
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send Telegram notification (supports both position closures and withdrawals)
|
||||||
|
*/
|
||||||
|
export async function sendTelegramNotification(options: TelegramOptions): Promise<void> {
|
||||||
|
if ('type' in options && options.type === 'withdrawal') {
|
||||||
|
return sendWithdrawalNotification(options)
|
||||||
|
}
|
||||||
|
return sendPositionClosedNotification(options as TelegramNotificationOptions)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send withdrawal notification
|
||||||
|
*/
|
||||||
|
async function sendWithdrawalNotification(options: TelegramWithdrawalOptions): Promise<void> {
|
||||||
|
try {
|
||||||
|
const token = process.env.TELEGRAM_BOT_TOKEN
|
||||||
|
const chatId = process.env.TELEGRAM_CHAT_ID
|
||||||
|
|
||||||
|
if (!token || !chatId) {
|
||||||
|
console.log('⚠️ Telegram credentials not configured, skipping notification')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const message = `💸 PROFIT WITHDRAWAL
|
||||||
|
|
||||||
|
💰 Amount: $${options.amount.toFixed(2)} USDC
|
||||||
|
|
||||||
|
📊 Available Profit: $${options.availableProfit.toFixed(2)}
|
||||||
|
📈 Total Withdrawn: $${options.totalWithdrawn.toFixed(2)}
|
||||||
|
|
||||||
|
🔗 Transaction: <a href="https://solscan.io/tx/${options.signature}">${options.signature.substring(0, 8)}...</a>
|
||||||
|
|
||||||
|
✅ Funds sent to your wallet`
|
||||||
|
|
||||||
|
const url = `https://api.telegram.org/bot${token}/sendMessage`
|
||||||
|
const response = await fetch(url, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({
|
||||||
|
chat_id: chatId,
|
||||||
|
text: message,
|
||||||
|
parse_mode: 'HTML'
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const errorData = await response.json()
|
||||||
|
console.error('❌ Telegram notification failed:', errorData)
|
||||||
|
} else {
|
||||||
|
console.log('✅ Telegram withdrawal notification sent')
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ Error sending Telegram notification:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user