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:
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>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user