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,
}
}
}