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:
160
lib/drift/withdraw.ts
Normal file
160
lib/drift/withdraw.ts
Normal 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,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user