Files
trading_bot_v4/app/withdrawals/page.tsx
mindesbunister ca7b49f745 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
2025-11-19 18:07:07 +01:00

411 lines
17 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 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>
)
}