mirror of
https://github.com/NoFxAiOS/nofx.git
synced 2026-07-17 09:24:36 +08:00
feat: server-side launch preflight with structured checks and runtime AI wallet health
- POST /api/launch/preflight and GET /api/traders/:id/preflight run every launch prerequisite (AI model credential, claw402 wallet key + Base USDC balance, strategy, exchange config, live account state, funding minimums) and return a structured checklist with stable codes; minimums (1 USDC AI fee / 12 USDC trading) now live server-side and are exposed in the response. - POST /traders/:id/start enforces the preflight (400 with error_key trader.start.preflight_failed + full checklist; ?force=true to override). Funding gates use max(available, equity) so restarting a bot with deployed margin is not blocked; indeterminate probes (RPC outage) degrade to warnings instead of blocking. - Run loop now surfaces runtime health via GetStatus: safe_mode(+reason) and ai_wallet_status/balance (ok|low|empty|unknown), including typed detection of claw402 ErrInsufficientFunds instead of burying it in logs; safe-mode state is mutex-guarded for API readers. - Onboarding wallet endpoints switch to the cached, error-aware balance query and report balance_status so the UI can tell an RPC outage from an empty wallet.
This commit is contained in:
82
trader/runtime_health.go
Normal file
82
trader/runtime_health.go
Normal file
@@ -0,0 +1,82 @@
|
||||
package trader
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
// Runtime health state written by the run-loop goroutine and read by the API
|
||||
// layer (GetStatus). Everything here goes through runtimeHealthMu so the
|
||||
// dashboard can poll without racing the trading loop.
|
||||
|
||||
// AI fee wallet health statuses exposed via GetStatus as "ai_wallet_status".
|
||||
const (
|
||||
AIWalletStatusOK = "ok"
|
||||
AIWalletStatusLow = "low"
|
||||
AIWalletStatusEmpty = "empty"
|
||||
AIWalletStatusUnknown = "unknown"
|
||||
)
|
||||
|
||||
// aiWalletLowThresholdUSDC mirrors api.MinAIFeeUSDC: below this the wallet
|
||||
// cannot reliably pay for the next AI/data calls.
|
||||
const aiWalletLowThresholdUSDC = 1.0
|
||||
|
||||
func (at *AutoTrader) setSafeMode(active bool, reason string) {
|
||||
at.runtimeHealthMu.Lock()
|
||||
at.safeMode = active
|
||||
at.safeModeReason = reason
|
||||
at.runtimeHealthMu.Unlock()
|
||||
}
|
||||
|
||||
func (at *AutoTrader) isSafeMode() bool {
|
||||
at.runtimeHealthMu.RLock()
|
||||
defer at.runtimeHealthMu.RUnlock()
|
||||
return at.safeMode
|
||||
}
|
||||
|
||||
func (at *AutoTrader) safeModeState() (bool, string) {
|
||||
at.runtimeHealthMu.RLock()
|
||||
defer at.runtimeHealthMu.RUnlock()
|
||||
return at.safeMode, at.safeModeReason
|
||||
}
|
||||
|
||||
// setAIWalletHealth records a fresh balance observation for the claw402 wallet.
|
||||
func (at *AutoTrader) setAIWalletHealth(balance float64) {
|
||||
status := AIWalletStatusOK
|
||||
switch {
|
||||
case balance <= 0:
|
||||
status = AIWalletStatusEmpty
|
||||
case balance < aiWalletLowThresholdUSDC:
|
||||
status = AIWalletStatusLow
|
||||
}
|
||||
|
||||
at.runtimeHealthMu.Lock()
|
||||
at.aiWalletStatus = status
|
||||
at.aiWalletBalanceUSDC = balance
|
||||
at.aiWalletCheckedAt = time.Now().UTC()
|
||||
at.runtimeHealthMu.Unlock()
|
||||
}
|
||||
|
||||
// markAIWalletHealthUnknown keeps the last observed balance but flags that the
|
||||
// current reading could not be refreshed (e.g. Base RPC unreachable).
|
||||
func (at *AutoTrader) markAIWalletHealthUnknown() {
|
||||
at.runtimeHealthMu.Lock()
|
||||
at.aiWalletStatus = AIWalletStatusUnknown
|
||||
at.aiWalletCheckedAt = time.Now().UTC()
|
||||
at.runtimeHealthMu.Unlock()
|
||||
}
|
||||
|
||||
// markAIWalletEmptyFromPayment records a payment-layer rejection: the wallet
|
||||
// definitively could not cover an AI call.
|
||||
func (at *AutoTrader) markAIWalletEmptyFromPayment(balance float64) {
|
||||
at.runtimeHealthMu.Lock()
|
||||
at.aiWalletStatus = AIWalletStatusEmpty
|
||||
at.aiWalletBalanceUSDC = balance
|
||||
at.aiWalletCheckedAt = time.Now().UTC()
|
||||
at.runtimeHealthMu.Unlock()
|
||||
}
|
||||
|
||||
func (at *AutoTrader) aiWalletHealth() (status string, balance float64, checkedAt time.Time) {
|
||||
at.runtimeHealthMu.RLock()
|
||||
defer at.runtimeHealthMu.RUnlock()
|
||||
return at.aiWalletStatus, at.aiWalletBalanceUSDC, at.aiWalletCheckedAt
|
||||
}
|
||||
Reference in New Issue
Block a user