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:
mindesbunister
2025-11-19 18:07:07 +01:00
parent c42bf94c1f
commit ca7b49f745
7 changed files with 962 additions and 1 deletions

160
lib/drift/withdraw.ts Normal file
View 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,
}
}
}

View File

@@ -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<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)
}
}