From ca7b49f7458353cb362e570141647adc0d425530 Mon Sep 17 00:00:00 2001 From: mindesbunister Date: Wed, 19 Nov 2025 18:07:07 +0100 Subject: [PATCH] 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 --- app/api/withdrawals/execute/route.ts | 100 +++++++ app/api/withdrawals/settings/route.ts | 115 ++++++++ app/api/withdrawals/stats/route.ts | 83 ++++++ app/page.tsx | 29 +- app/withdrawals/page.tsx | 410 ++++++++++++++++++++++++++ lib/drift/withdraw.ts | 160 ++++++++++ lib/notifications/telegram.ts | 66 +++++ 7 files changed, 962 insertions(+), 1 deletion(-) create mode 100644 app/api/withdrawals/execute/route.ts create mode 100644 app/api/withdrawals/settings/route.ts create mode 100644 app/api/withdrawals/stats/route.ts create mode 100644 app/withdrawals/page.tsx create mode 100644 lib/drift/withdraw.ts diff --git a/app/api/withdrawals/execute/route.ts b/app/api/withdrawals/execute/route.ts new file mode 100644 index 0000000..00c2afd --- /dev/null +++ b/app/api/withdrawals/execute/route.ts @@ -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 }) + } +} diff --git a/app/api/withdrawals/settings/route.ts b/app/api/withdrawals/settings/route.ts new file mode 100644 index 0000000..c9d69e4 --- /dev/null +++ b/app/api/withdrawals/settings/route.ts @@ -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 = { + 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 }) + } +} diff --git a/app/api/withdrawals/stats/route.ts b/app/api/withdrawals/stats/route.ts new file mode 100644 index 0000000..285a185 --- /dev/null +++ b/app/api/withdrawals/stats/route.ts @@ -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, + }) + } +} diff --git a/app/page.tsx b/app/page.tsx index 962adc2..c865849 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -43,7 +43,7 @@ export default function HomePage() { {/* Navigation Cards */} -
+
{/* Analytics Card */} + {/* Withdrawals Card */} + +
+ +
+
+ 💸 +
+ +

Withdrawals

+ +

+ Configure automatic profit withdrawals to your wallet +

+ +
+ Manage Withdrawals + + + +
+
+
+ {/* Settings Card */} (null) + const [stats, setStats] = useState(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 ( +
+
Loading...
+
+ ) + } + + return ( +
+
+ {/* Header */} +
+

+ 💰 Automated Withdrawals +

+

+ Configure automatic profit withdrawal from your trading account +

+
+ + {/* Message */} + {message && ( +
+ {message.text} +
+ )} + + {/* Account Stats */} +
+

📊 Account Statistics

+
+
+
Current Balance
+
${stats.currentBalance.toFixed(2)}
+
+
+
Total Invested
+
${stats.totalInvested.toFixed(2)}
+
+
= 0 ? 'bg-green-500/20 border-green-500/30' : 'bg-red-500/20 border-red-500/30' + } rounded-xl p-6 border`}> +
= 0 ? 'text-green-300' : 'text-red-300'} text-sm mb-2`}> + Trading P&L +
+
+ ${stats.totalPnL >= 0 ? '+' : ''}{stats.totalPnL.toFixed(2)} +
+
+
+ +
+
+
Available Profit
+
${stats.availableProfit.toFixed(2)}
+
+ (Balance - Total Invested) +
+
+
+
Total Withdrawn
+
${stats.totalWithdrawn.toFixed(2)}
+
+
+ + {stats.nextWithdrawalAmount > 0 && settings.ENABLE_AUTO_WITHDRAWALS && ( +
+
Next Scheduled Withdrawal
+
+ ${stats.nextWithdrawalAmount.toFixed(2)} +
+ {stats.nextWithdrawalTime && ( +
+ Next withdrawal: {new Date(stats.nextWithdrawalTime).toLocaleString()} +
+ )} +
+ )} +
+ + {/* Withdrawal Settings */} +
+

âš™ī¸ Withdrawal Configuration

+ + {/* Enable Auto Withdrawals */} +
+
+
+
Enable Automatic Withdrawals
+
+ Automatically withdraw profits on schedule +
+
+ +
+
+ + {/* Withdrawal Interval */} +
+ +
+ 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" + /> +
+ {settings.WITHDRAWAL_INTERVAL_HOURS === 24 ? '(Daily)' : + settings.WITHDRAWAL_INTERVAL_HOURS === 168 ? '(Weekly)' : + settings.WITHDRAWAL_INTERVAL_HOURS === 720 ? '(Monthly)' : + `(Every ${settings.WITHDRAWAL_INTERVAL_HOURS}h)`} +
+
+
+ + + +
+
+ + {/* Profit Percentage */} +
+ +
+ 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" + /> +
+ {settings.WITHDRAWAL_PROFIT_PERCENT}% +
+
+
+ Withdraw {settings.WITHDRAWAL_PROFIT_PERCENT}% of available profit. + {stats.availableProfit > 0 && ( + + {' '}Next withdrawal: ${(stats.availableProfit * settings.WITHDRAWAL_PROFIT_PERCENT / 100).toFixed(2)} + + )} +
+ setSettings({ ...settings, WITHDRAWAL_PROFIT_PERCENT: parseFloat(e.target.value) })} + className="w-full mt-3" + min="0" + max="100" + step="5" + /> +
+ + {/* Minimum Withdrawal Amount */} +
+ + 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" + /> +
+ Skip withdrawal if amount is below this threshold +
+
+ + {/* Minimum Account Balance */} +
+ + 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" + /> +
+ Never withdraw if it would drop account below this amount (safety buffer) +
+
+ + {/* Destination Wallet */} +
+ + 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 + /> +
+ Using wallet from bot configuration. To change, update WALLET_PUBLIC_KEY in .env +
+
+
+ + {/* Action Buttons */} +
+ + + +
+ + {/* Warning */} + {stats.availableProfit <= 0 && ( +
+ âš ī¸ No profits available for withdrawal. Current P&L: ${stats.totalPnL.toFixed(2)} +
+ )} + + {/* Info Box */} +
+

â„šī¸ How It Works

+
    +
  • ✅ Available Profit = Current Balance - Total Invested
  • +
  • ✅ Withdrawal Amount = Available Profit × Percentage
  • +
  • ✅ Runs automatically on schedule when enabled
  • +
  • ✅ Skips withdrawal if amount below minimum threshold
  • +
  • ✅ Never withdraws if it would drop account below minimum balance
  • +
  • ✅ Telegram notifications for all withdrawals
  • +
  • ✅ Full transaction history tracked in database
  • +
+
+ + {/* Back Link */} +
+
+
+ ) +} diff --git a/lib/drift/withdraw.ts b/lib/drift/withdraw.ts new file mode 100644 index 0000000..6604c65 --- /dev/null +++ b/lib/drift/withdraw.ts @@ -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 { + 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, + } + } +} diff --git a/lib/notifications/telegram.ts b/lib/notifications/telegram.ts index 162c48b..46c59af 100644 --- a/lib/notifications/telegram.ts +++ b/lib/notifications/telegram.ts @@ -17,6 +17,16 @@ interface TelegramNotificationOptions { maxGain?: number } +interface TelegramWithdrawalOptions { + type: 'withdrawal' + amount: number + signature: string + availableProfit: number + totalWithdrawn: number +} + +type TelegramOptions = TelegramNotificationOptions | TelegramWithdrawalOptions + /** * Send Telegram notification for position closure */ @@ -107,3 +117,59 @@ function formatHoldTime(seconds: number): string { return `${secs}s` } } + +/** + * Send Telegram notification (supports both position closures and withdrawals) + */ +export async function sendTelegramNotification(options: TelegramOptions): Promise { + if ('type' in options && options.type === 'withdrawal') { + return sendWithdrawalNotification(options) + } + return sendPositionClosedNotification(options as TelegramNotificationOptions) +} + +/** + * Send withdrawal notification + */ +async function sendWithdrawalNotification(options: TelegramWithdrawalOptions): Promise { + 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: ${options.signature.substring(0, 8)}... + +✅ 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) + } +}