Fix Claw402 autopilot launch and accounting

This commit is contained in:
tinkle-community
2026-06-28 11:36:56 +08:00
parent a4983d2cb0
commit c4e79d9579
12 changed files with 724 additions and 205 deletions

View File

@@ -165,34 +165,80 @@ func (s *Server) handleEquityHistory(c *gin.Context) {
MarginUsedPct float64 `json:"margin_used_pct"` // Margin used percentage
}
// Use the balance of the first record as initial balance to calculate return rate
initialBalance := snapshots[0].Balance
initialBalance := trader.InitialBalance
if initialBalance <= 0 {
initialBalance = snapshots[0].TotalEquity
}
if initialBalance == 0 {
initialBalance = 1 // Avoid division by zero
}
var history []EquityPoint
var lastSnapshotTime time.Time
for _, snap := range snapshots {
// Calculate PnL percentage
totalPnL := snap.TotalEquity - initialBalance
totalPnLPct := 0.0
if initialBalance > 0 {
totalPnLPct = (snap.UnrealizedPnL / initialBalance) * 100
totalPnLPct = (totalPnL / initialBalance) * 100
}
history = append(history, EquityPoint{
Timestamp: snap.Timestamp.Format("2006-01-02 15:04:05"),
TotalEquity: snap.TotalEquity,
AvailableBalance: snap.Balance,
TotalPnL: snap.UnrealizedPnL,
AvailableBalance: equitySnapshotAvailableBalance(snap),
TotalPnL: totalPnL,
TotalPnLPct: totalPnLPct,
PositionCount: snap.PositionCount,
MarginUsedPct: snap.MarginUsedPct,
})
if snap.Timestamp.After(lastSnapshotTime) {
lastSnapshotTime = snap.Timestamp
}
}
if runtimeTrader, err := s.traderManager.GetTrader(traderID); err == nil {
if accountInfo, err := runtimeTrader.GetAccountInfo(); err == nil && time.Since(lastSnapshotTime) > 30*time.Second {
totalEquity := floatFromMap(accountInfo, "total_equity")
totalPnL := totalEquity - initialBalance
totalPnLPct := 0.0
if initialBalance > 0 {
totalPnLPct = (totalPnL / initialBalance) * 100
}
history = append(history, EquityPoint{
Timestamp: time.Now().UTC().Format("2006-01-02 15:04:05"),
TotalEquity: totalEquity,
AvailableBalance: floatFromMap(accountInfo, "available_balance"),
TotalPnL: totalPnL,
TotalPnLPct: totalPnLPct,
PositionCount: int(floatFromMap(accountInfo, "position_count")),
MarginUsedPct: floatFromMap(accountInfo, "margin_used_pct"),
})
}
}
c.JSON(http.StatusOK, history)
}
func equitySnapshotAvailableBalance(snap *store.EquitySnapshot) float64 {
if snap == nil {
return 0
}
if snap.AvailableBalance != 0 || snap.PositionCount > 0 {
return snap.AvailableBalance
}
return snap.Balance
}
func floatFromMap(values map[string]interface{}, key string) float64 {
if value, ok := values[key].(float64); ok {
return value
}
if value, ok := values[key].(int); ok {
return float64(value)
}
return 0
}
// handlePublicTraderList Get public trader list (no authentication required)
func (s *Server) handlePublicTraderList(c *gin.Context) {
// Get trader information from all users
@@ -386,18 +432,20 @@ func (s *Server) getEquityHistoryForTraders(traderIDs []string, hours int) map[s
history := make([]map[string]interface{}, 0, len(snapshots)+1)
var lastSnapshotTime time.Time
for _, snap := range snapshots {
totalPnL := snap.TotalEquity - initialBalance
// Calculate PnL percentage: (current_equity - initial_balance) / initial_balance * 100
pnlPct := 0.0
if initialBalance > 0 {
pnlPct = (snap.TotalEquity - initialBalance) / initialBalance * 100
pnlPct = totalPnL / initialBalance * 100
}
history = append(history, map[string]interface{}{
"timestamp": snap.Timestamp,
"total_equity": snap.TotalEquity,
"total_pnl": snap.UnrealizedPnL,
"total_pnl_pct": pnlPct,
"balance": snap.Balance,
"timestamp": snap.Timestamp,
"total_equity": snap.TotalEquity,
"available_balance": equitySnapshotAvailableBalance(snap),
"total_pnl": totalPnL,
"total_pnl_pct": pnlPct,
"balance": snap.Balance,
})
if snap.Timestamp.After(lastSnapshotTime) {
lastSnapshotTime = snap.Timestamp
@@ -410,29 +458,21 @@ func (s *Server) getEquityHistoryForTraders(traderIDs []string, hours int) map[s
if accountInfo, err := trader.GetAccountInfo(); err == nil {
// Only append if it's been more than 30 seconds since last snapshot
if now.Sub(lastSnapshotTime) > 30*time.Second {
totalEquity := 0.0
if v, ok := accountInfo["total_equity"].(float64); ok {
totalEquity = v
}
totalPnL := 0.0
if v, ok := accountInfo["total_pnl"].(float64); ok {
totalPnL = v
}
walletBalance := 0.0
if v, ok := accountInfo["wallet_balance"].(float64); ok {
walletBalance = v
}
totalEquity := floatFromMap(accountInfo, "total_equity")
totalPnL := totalEquity - initialBalance
walletBalance := floatFromMap(accountInfo, "wallet_balance")
pnlPct := 0.0
if initialBalance > 0 {
pnlPct = (totalEquity - initialBalance) / initialBalance * 100
}
history = append(history, map[string]interface{}{
"timestamp": now,
"total_equity": totalEquity,
"total_pnl": totalPnL,
"total_pnl_pct": pnlPct,
"balance": walletBalance,
"timestamp": now,
"total_equity": totalEquity,
"available_balance": floatFromMap(accountInfo, "available_balance"),
"total_pnl": totalPnL,
"total_pnl_pct": pnlPct,
"balance": walletBalance,
})
}
}

View File

@@ -14,15 +14,16 @@ type EquityStore struct {
// EquitySnapshot equity snapshot
type EquitySnapshot struct {
ID int64 `gorm:"primaryKey;autoIncrement" json:"id"`
TraderID string `gorm:"column:trader_id;not null;index:idx_equity_trader_time" json:"trader_id"`
Timestamp time.Time `gorm:"not null;index:idx_equity_trader_time,sort:desc;index:idx_equity_timestamp,sort:desc" json:"timestamp"`
TotalEquity float64 `gorm:"column:total_equity;not null;default:0" json:"total_equity"`
Balance float64 `gorm:"not null;default:0" json:"balance"`
UnrealizedPnL float64 `gorm:"column:unrealized_pnl;not null;default:0" json:"unrealized_pnl"`
PositionCount int `gorm:"column:position_count;default:0" json:"position_count"`
MarginUsedPct float64 `gorm:"column:margin_used_pct;default:0" json:"margin_used_pct"`
CreatedAt time.Time `json:"created_at"`
ID int64 `gorm:"primaryKey;autoIncrement" json:"id"`
TraderID string `gorm:"column:trader_id;not null;index:idx_equity_trader_time" json:"trader_id"`
Timestamp time.Time `gorm:"not null;index:idx_equity_trader_time,sort:desc;index:idx_equity_timestamp,sort:desc" json:"timestamp"`
TotalEquity float64 `gorm:"column:total_equity;not null;default:0" json:"total_equity"`
Balance float64 `gorm:"not null;default:0" json:"balance"`
AvailableBalance float64 `gorm:"column:available_balance;not null;default:0" json:"available_balance"`
UnrealizedPnL float64 `gorm:"column:unrealized_pnl;not null;default:0" json:"unrealized_pnl"`
PositionCount int `gorm:"column:position_count;default:0" json:"position_count"`
MarginUsedPct float64 `gorm:"column:margin_used_pct;default:0" json:"margin_used_pct"`
CreatedAt time.Time `json:"created_at"`
}
func (EquitySnapshot) TableName() string { return "trader_equity_snapshots" }
@@ -98,6 +99,7 @@ func (s *EquityStore) GetAllTradersLatest() (map[string]*EquitySnapshot, error)
var snapshots []*EquitySnapshot
err := s.db.Raw(`
SELECT e.id, e.trader_id, e.timestamp, e.total_equity, e.balance,
e.available_balance,
e.unrealized_pnl, e.position_count, e.margin_used_pct, e.created_at
FROM trader_equity_snapshots e
INNER JOIN (
@@ -159,12 +161,13 @@ func (s *EquityStore) MigrateFromDecision() (int64, error) {
result := s.db.Exec(`
INSERT INTO trader_equity_snapshots (
trader_id, timestamp, total_equity, balance,
unrealized_pnl, position_count, margin_used_pct
available_balance, unrealized_pnl, position_count, margin_used_pct
)
SELECT
dr.trader_id,
dr.timestamp,
das.total_balance,
das.total_balance - das.total_unrealized_profit,
das.available_balance,
das.total_unrealized_profit,
das.position_count,

View File

@@ -524,9 +524,6 @@ func (at *AutoTrader) Run() error {
}
}
ticker := time.NewTicker(at.config.ScanInterval)
defer ticker.Stop()
// Check if this is a grid trading strategy
isGridStrategy := at.IsGridStrategy()
if isGridStrategy {
@@ -538,6 +535,7 @@ func (at *AutoTrader) Run() error {
}
// Execute immediately on first run
at.logInfof("▶️ Running first trading cycle immediately; next cycle starts after %v", at.config.ScanInterval)
if isGridStrategy {
if err := at.RunGridCycle(); err != nil {
at.logErrorf("❌ Grid execution failed: %v", err)
@@ -548,6 +546,9 @@ func (at *AutoTrader) Run() error {
}
}
ticker := time.NewTicker(at.config.ScanInterval)
defer ticker.Stop()
for {
at.isRunningMutex.RLock()
running := at.isRunning

View File

@@ -3,11 +3,11 @@ package trader
import (
"fmt"
"math"
"nofx/telemetry"
"nofx/kernel"
"nofx/logger"
"nofx/market"
"nofx/store"
"nofx/telemetry"
"time"
)
@@ -18,13 +18,14 @@ func (at *AutoTrader) saveEquitySnapshot(ctx *kernel.Context) {
}
snapshot := &store.EquitySnapshot{
TraderID: at.id,
Timestamp: time.Now().UTC(),
TotalEquity: ctx.Account.TotalEquity,
Balance: ctx.Account.TotalEquity - ctx.Account.UnrealizedPnL,
UnrealizedPnL: ctx.Account.UnrealizedPnL,
PositionCount: ctx.Account.PositionCount,
MarginUsedPct: ctx.Account.MarginUsedPct,
TraderID: at.id,
Timestamp: time.Now().UTC(),
TotalEquity: ctx.Account.TotalEquity,
Balance: ctx.Account.TotalEquity - ctx.Account.UnrealizedPnL,
AvailableBalance: ctx.Account.AvailableBalance,
UnrealizedPnL: ctx.Account.UnrealizedPnL,
PositionCount: ctx.Account.PositionCount,
MarginUsedPct: ctx.Account.MarginUsedPct,
}
if err := at.store.Equity().Save(snapshot); err != nil {

View File

@@ -129,23 +129,29 @@ func (t *HyperliquidTrader) GetBalance() (map[string]interface{}, error) {
logger.Infof(" └─ %s: size=%s, entryPx=%s, posValue=%s, pnl=%s",
pos.Position.Coin, pos.Position.Szi, entryPx, pos.Position.PositionValue, pos.Position.UnrealizedPnl)
}
xyzWalletBalance := xyzAccountValue - xyzUnrealizedPnl
// Step 6: Correctly handle Spot + Perpetuals + xyz dex balance
// Important: Each account is independent, manual transfers required
totalWalletBalance := walletBalanceWithoutUnrealized + spotUSDCBalance + xyzWalletBalance
totalUnrealizedPnlAll := totalUnrealizedPnl + xyzUnrealizedPnl
// Calculate total equity properly: perpAccountValue + spotUSDCBalance + xyzAccountValue
// Note: totalWalletBalance + totalUnrealizedPnlAll should equal this
totalEquityCalculated := accountValue + spotUSDCBalance + xyzAccountValue
xyzMarginUsed := calculateXYZMarginUsed(xyzPositions)
balanceBreakdown := calculateHyperliquidBalanceBreakdown(
t.isUnifiedAccount,
spotUSDCBalance,
accountValue,
totalUnrealizedPnl,
totalMarginUsed,
availableBalance,
xyzAccountValue,
xyzUnrealizedPnl,
xyzMarginUsed,
)
totalWalletBalance := balanceBreakdown.TotalWalletBalance
totalUnrealizedPnlAll := balanceBreakdown.TotalUnrealizedProfit
totalEquityCalculated := balanceBreakdown.TotalEquity
availableBalance = balanceBreakdown.AvailableBalance
// Step 7: Unified Account mode - Spot USDC is used as collateral for Perps
// In this mode, available balance includes Spot USDC since it can be used for Perp margin
// In this mode, xyz/core account values are collateral views backed by the
// same Spot USDC. They must not be added on top of Spot or the dashboard
// will double count equity after a position opens.
if t.isUnifiedAccount && spotUSDCBalance > 0 {
// Add Spot balance to available balance for trading
availableBalance = availableBalance + spotUSDCBalance
logger.Infof("✓ Unified Account: Spot %.2f USDC added to available balance (total: %.2f)",
logger.Infof("✓ Unified Account: Spot %.2f USDC used as shared collateral (available: %.2f)",
spotUSDCBalance, availableBalance)
}
@@ -160,6 +166,7 @@ func (t *HyperliquidTrader) GetBalance() (map[string]interface{}, error) {
result["xyzDexBalance"] = xyzAccountValue // xyz dex equity (stock perps, forex, commodities)
result["xyzDexUnrealizedPnl"] = xyzUnrealizedPnl // xyz dex unrealized PnL
result["perpAccountValue"] = accountValue // Perp account value for debugging
result["totalMarginUsed"] = balanceBreakdown.TotalMarginUsed
logger.Infof("✓ Hyperliquid complete account:")
logger.Infof(" • Spot balance: %.2f USDC", spotUSDCBalance)
@@ -171,15 +178,93 @@ func (t *HyperliquidTrader) GetBalance() (map[string]interface{}, error) {
logger.Infof(" • Margin used: %.2f USDC", totalMarginUsed)
logger.Infof(" • xyz dex equity: %.2f USDC (wallet %.2f + unrealized %.2f)",
xyzAccountValue,
xyzWalletBalance,
balanceBreakdown.XYZWalletBalance,
xyzUnrealizedPnl)
logger.Infof(" • Total assets (Perp+Spot+xyz): %.2f USDC", totalWalletBalance)
logger.Infof(" ⭐ Total: %.2f USDC | Perp: %.2f | Spot: %.2f | xyz: %.2f",
totalWalletBalance, availableBalance, spotUSDCBalance, xyzAccountValue)
logger.Infof(" • Total wallet balance: %.2f USDC", totalWalletBalance)
logger.Infof(" ⭐ Total equity: %.2f USDC | Available: %.2f | Spot: %.2f | xyz view: %.2f",
totalEquityCalculated, availableBalance, spotUSDCBalance, xyzAccountValue)
return result, nil
}
type hyperliquidBalanceBreakdown struct {
TotalWalletBalance float64
TotalEquity float64
AvailableBalance float64
TotalUnrealizedProfit float64
TotalMarginUsed float64
PerpWalletBalance float64
XYZWalletBalance float64
}
func calculateHyperliquidBalanceBreakdown(
isUnifiedAccount bool,
spotUSDCBalance float64,
perpAccountValue float64,
perpUnrealizedPnl float64,
perpMarginUsed float64,
perpWithdrawable float64,
xyzAccountValue float64,
xyzUnrealizedPnl float64,
xyzMarginUsed float64,
) hyperliquidBalanceBreakdown {
perpWalletBalance := perpAccountValue - perpUnrealizedPnl
xyzWalletBalance := xyzAccountValue - xyzUnrealizedPnl
totalUnrealizedPnl := perpUnrealizedPnl + xyzUnrealizedPnl
totalMarginUsed := perpMarginUsed + xyzMarginUsed
if isUnifiedAccount && spotUSDCBalance > 0 {
totalEquity := spotUSDCBalance + totalUnrealizedPnl
availableBalance := totalEquity - totalMarginUsed
if availableBalance < 0 {
availableBalance = 0
}
return hyperliquidBalanceBreakdown{
TotalWalletBalance: spotUSDCBalance,
TotalEquity: totalEquity,
AvailableBalance: availableBalance,
TotalUnrealizedProfit: totalUnrealizedPnl,
TotalMarginUsed: totalMarginUsed,
PerpWalletBalance: perpWalletBalance,
XYZWalletBalance: xyzWalletBalance,
}
}
availableBalance := perpWithdrawable
if availableBalance == 0 {
availableBalance = perpAccountValue - perpMarginUsed
}
if availableBalance < 0 {
availableBalance = 0
}
return hyperliquidBalanceBreakdown{
TotalWalletBalance: perpWalletBalance + spotUSDCBalance + xyzWalletBalance,
TotalEquity: perpAccountValue + spotUSDCBalance + xyzAccountValue,
AvailableBalance: availableBalance,
TotalUnrealizedProfit: totalUnrealizedPnl,
TotalMarginUsed: totalMarginUsed,
PerpWalletBalance: perpWalletBalance,
XYZWalletBalance: xyzWalletBalance,
}
}
func calculateXYZMarginUsed(positions []xyzAssetPosition) float64 {
total := 0.0
for _, pos := range positions {
positionValue, _ := strconv.ParseFloat(pos.Position.PositionValue, 64)
if positionValue < 0 {
positionValue = -positionValue
}
leverage := float64(pos.Position.Leverage.Value)
if leverage <= 0 {
leverage = 1
}
total += positionValue / leverage
}
return total
}
// xyzDexState represents the clearinghouse state for xyz dex
type xyzDexState struct {
MarginSummary *xyzMarginSummary `json:"marginSummary,omitempty"`

View File

@@ -0,0 +1,48 @@
package hyperliquid
import "testing"
func TestUnifiedAccountDoesNotDoubleCountXYZAccountValue(t *testing.T) {
breakdown := calculateHyperliquidBalanceBreakdown(
true,
26.33, // Spot USDC collateral
0,
0,
0,
0,
25.96, // xyz account value is a view of the same shared collateral
-0.32,
25.96,
)
if breakdown.TotalEquity < 25.99 || breakdown.TotalEquity > 26.02 {
t.Fatalf("expected total equity to be spot + unrealized pnl, got %.4f", breakdown.TotalEquity)
}
if breakdown.TotalEquity > 40 {
t.Fatalf("unified collateral was double-counted: %.4f", breakdown.TotalEquity)
}
if breakdown.AvailableBalance > 0.1 {
t.Fatalf("expected almost no free collateral with full-size margin, got %.4f", breakdown.AvailableBalance)
}
}
func TestSeparateAccountsStillAddIndependentBalances(t *testing.T) {
breakdown := calculateHyperliquidBalanceBreakdown(
false,
30,
10,
1,
2,
8,
5,
-0.5,
1,
)
if breakdown.TotalEquity != 45 {
t.Fatalf("expected independent accounts to add to 45, got %.4f", breakdown.TotalEquity)
}
if breakdown.TotalWalletBalance != 44.5 {
t.Fatalf("expected wallet balance 44.5, got %.4f", breakdown.TotalWalletBalance)
}
}

View File

@@ -1,5 +1,5 @@
import { useState, useEffect } from 'react'
import { useNavigate } from 'react-router-dom'
import { useNavigate, useSearchParams } from 'react-router-dom'
import useSWR from 'swr'
import { api } from '../../lib/api'
import type {
@@ -32,6 +32,7 @@ export function AITradersPage({ onTraderSelect }: AITradersPageProps) {
const { language } = useLanguage()
const { user, token } = useAuth()
const navigate = useNavigate()
const [searchParams, setSearchParams] = useSearchParams()
const [showCreateModal, setShowCreateModal] = useState(false)
const [showEditModal, setShowEditModal] = useState(false)
const [showModelModal, setShowModelModal] = useState(false)
@@ -39,6 +40,10 @@ export function AITradersPage({ onTraderSelect }: AITradersPageProps) {
const [showTelegramModal, setShowTelegramModal] = useState(false)
const [editingModel, setEditingModel] = useState<string | null>(null)
const [editingExchange, setEditingExchange] = useState<string | null>(null)
const [initialModelId, setInitialModelId] = useState<string | null>(null)
const [initialExchangeType, setInitialExchangeType] = useState<string | null>(
null
)
const [editingTrader, setEditingTrader] = useState<any>(null)
const [allModels, setAllModels] = useState<AIModel[]>([])
const [allExchanges, setAllExchanges] = useState<Exchange[]>([])
@@ -166,10 +171,6 @@ export function AITradersPage({ onTraderSelect }: AITradersPageProps) {
return true
}) || []
const isModelInUse = (modelId: string) => {
return traders?.some((tr) => tr.ai_model === modelId && tr.is_running)
}
const getModelUsageInfo = (modelId: string) => {
const usingTraders = traders?.filter((tr) => tr.ai_model === modelId) || []
const runningCount = usingTraders.filter((tr) => tr.is_running).length
@@ -177,10 +178,6 @@ export function AITradersPage({ onTraderSelect }: AITradersPageProps) {
return { runningCount, totalCount, usingTraders }
}
const isExchangeInUse = (exchangeId: string) => {
return traders?.some((tr) => tr.exchange_id === exchangeId && tr.is_running)
}
const getExchangeUsageInfo = (exchangeId: string) => {
const usingTraders =
traders?.filter((tr) => tr.exchange_id === exchangeId) || []
@@ -334,17 +331,15 @@ export function AITradersPage({ onTraderSelect }: AITradersPageProps) {
}
const handleModelClick = (modelId: string) => {
if (!isModelInUse(modelId)) {
setEditingModel(modelId)
setShowModelModal(true)
}
setInitialModelId(null)
setEditingModel(modelId)
setShowModelModal(true)
}
const handleExchangeClick = (exchangeId: string) => {
if (!isExchangeInUse(exchangeId)) {
setEditingExchange(exchangeId)
setShowExchangeModal(true)
}
setInitialExchangeType(null)
setEditingExchange(exchangeId)
setShowExchangeModal(true)
}
const handleDeleteConfig = async <T extends { id: string }>(config: {
@@ -619,15 +614,63 @@ export function AITradersPage({ onTraderSelect }: AITradersPageProps) {
}
const handleAddModel = () => {
setInitialModelId(null)
setEditingModel(null)
setShowModelModal(true)
}
const handleAddExchange = () => {
setInitialExchangeType(null)
setEditingExchange(null)
setShowExchangeModal(true)
}
const handleOpenClaw402Config = () => {
const configuredClaw402 = allModels?.find(
(model) => model.provider === 'claw402'
)
const supportedClaw402 = supportedModels?.find(
(model) => model.provider === 'claw402'
)
const modelId = configuredClaw402?.id || supportedClaw402?.id || 'claw402'
setEditingModel(configuredClaw402?.id || null)
setInitialModelId(modelId)
setShowModelModal(true)
}
const handleOpenHyperliquidConfig = () => {
const existingHyperliquid = allExchanges?.find(
(exchange) =>
exchange.exchange_type === 'hyperliquid' ||
exchange.id === 'hyperliquid'
)
setEditingExchange(existingHyperliquid?.id || null)
setInitialExchangeType(existingHyperliquid ? null : 'hyperliquid')
setShowExchangeModal(true)
}
useEffect(() => {
if (!user || !token) return
const setupTarget = searchParams.get('setup')
if (!setupTarget) return
if (setupTarget === 'claw402') {
if (supportedModels.length === 0 && allModels.length === 0) return
handleOpenClaw402Config()
} else if (setupTarget === 'hyperliquid') {
handleOpenHyperliquidConfig()
} else {
return
}
const nextParams = new URLSearchParams(searchParams)
nextParams.delete('setup')
setSearchParams(nextParams, { replace: true })
}, [allExchanges, allModels, searchParams, setSearchParams, supportedModels, token, user])
const refreshLaunchState = async () => {
await Promise.all([loadConfigs(), mutateTraders()])
}
@@ -715,9 +758,7 @@ export function AITradersPage({ onTraderSelect }: AITradersPageProps) {
visibleExchangeAddresses={visibleExchangeAddresses}
copiedId={copiedId}
language={language}
isModelInUse={isModelInUse}
getModelUsageInfo={getModelUsageInfo}
isExchangeInUse={isExchangeInUse}
getExchangeUsageInfo={getExchangeUsageInfo}
onModelClick={handleModelClick}
onExchangeClick={handleExchangeClick}
@@ -733,6 +774,8 @@ export function AITradersPage({ onTraderSelect }: AITradersPageProps) {
isLoggedIn={Boolean(user && token)}
language={language}
onRefresh={refreshLaunchState}
onOpenClaw402Config={handleOpenClaw402Config}
onOpenHyperliquidConfig={handleOpenHyperliquidConfig}
/>
{/* Traders List */}
@@ -789,11 +832,13 @@ export function AITradersPage({ onTraderSelect }: AITradersPageProps) {
allModels={supportedModels}
configuredModels={allModels}
editingModelId={editingModel}
initialModelId={initialModelId}
onSave={handleSaveModelConfig}
onDelete={handleDeleteModelConfig}
onClose={() => {
setShowModelModal(false)
setEditingModel(null)
setInitialModelId(null)
}}
language={language}
/>
@@ -804,11 +849,13 @@ export function AITradersPage({ onTraderSelect }: AITradersPageProps) {
<ExchangeConfigModal
allExchanges={allExchanges}
editingExchangeId={editingExchange}
initialExchangeType={initialExchangeType}
onSave={handleSaveExchangeConfig}
onDelete={handleDeleteExchangeConfig}
onClose={() => {
setShowExchangeModal(false)
setEditingExchange(null)
setInitialExchangeType(null)
}}
language={language}
/>

View File

@@ -37,6 +37,8 @@ interface AutopilotLaunchPanelProps {
isLoggedIn: boolean
language: string
onRefresh: () => Promise<void>
onOpenClaw402Config?: () => void
onOpenHyperliquidConfig?: () => void
}
const MIN_AI_FEE_USDC = 1
@@ -186,6 +188,8 @@ export function AutopilotLaunchPanel({
isLoggedIn,
language,
onRefresh,
onOpenClaw402Config,
onOpenHyperliquidConfig,
}: AutopilotLaunchPanelProps) {
const navigate = useNavigate()
const [wallet, setWallet] = useState<CurrentBeginnerWalletResponse | null>(
@@ -371,15 +375,34 @@ export function AutopilotLaunchPanel({
? `${shortAddress(feeWalletAddress)} · ${formatUSDC(feeWalletBalance)} USDC`
: 'Base USDC wallet required',
action: feeWalletAddress ? (
<div className="flex flex-wrap items-center gap-3">
<button
type="button"
onClick={() => onOpenClaw402Config?.()}
className="inline-flex items-center gap-1.5 text-xs font-semibold text-nofx-gold hover:text-yellow-300"
>
<CircleDollarSign className="h-3.5 w-3.5" />
Deposit
</button>
<button
type="button"
onClick={() => void copyText(feeWalletAddress, 'AI fee wallet')}
className="inline-flex items-center gap-1.5 text-xs font-semibold text-nofx-gold hover:text-yellow-300"
>
<Copy className="h-3.5 w-3.5" />
Copy
</button>
</div>
) : (
<button
type="button"
onClick={() => void copyText(feeWalletAddress, 'AI fee wallet')}
onClick={() => onOpenClaw402Config?.()}
className="inline-flex items-center gap-1.5 text-xs font-semibold text-nofx-gold hover:text-yellow-300"
>
<Copy className="h-3.5 w-3.5" />
Copy
<ArrowRight className="h-3.5 w-3.5" />
Open
</button>
) : undefined,
),
},
{
title: 'Hyperliquid trading wallet',
@@ -389,6 +412,16 @@ export function AutopilotLaunchPanel({
meta: hyperliquidExchange?.hyperliquidWalletAddr
? `${shortAddress(hyperliquidExchange.hyperliquidWalletAddr)} · authorized`
: 'Agent and trading authorization required',
action: (
<button
type="button"
onClick={() => onOpenHyperliquidConfig?.()}
className="inline-flex items-center gap-1.5 text-xs font-semibold text-nofx-gold hover:text-yellow-300"
>
<Wallet className="h-3.5 w-3.5" />
Open
</button>
),
},
{
title: 'Trading balance',
@@ -421,10 +454,16 @@ export function AutopilotLaunchPanel({
return (
<button
type="button"
onClick={() => navigate(ROUTES.welcome)}
onClick={() => {
if (onOpenClaw402Config) {
onOpenClaw402Config()
} else {
navigate(ROUTES.welcome)
}
}}
className="inline-flex items-center justify-center gap-2 rounded-lg bg-nofx-gold px-4 py-3 text-sm font-bold text-black hover:bg-yellow-400"
>
Prepare AI fee wallet
Open Claw402 wallet
<ArrowRight className="h-4 w-4" />
</button>
)
@@ -435,9 +474,13 @@ export function AutopilotLaunchPanel({
<button
type="button"
onClick={() => {
document
.getElementById('hyperliquid-quick-connect')
?.scrollIntoView({ behavior: 'smooth', block: 'start' })
if (onOpenHyperliquidConfig) {
onOpenHyperliquidConfig()
} else {
document
.getElementById('hyperliquid-quick-connect')
?.scrollIntoView({ behavior: 'smooth', block: 'start' })
}
}}
className="inline-flex items-center justify-center gap-2 rounded-lg bg-nofx-gold px-4 py-3 text-sm font-bold text-black hover:bg-yellow-400"
>

View File

@@ -30,9 +30,7 @@ interface ConfigStatusGridProps {
visibleExchangeAddresses: Set<string>
copiedId: string | null
language: Language
isModelInUse: (modelId: string) => boolean | undefined
getModelUsageInfo: (modelId: string) => UsageInfo
isExchangeInUse: (exchangeId: string) => boolean | undefined
getExchangeUsageInfo: (exchangeId: string) => UsageInfo
onModelClick: (modelId: string) => void
onExchangeClick: (exchangeId: string) => void
@@ -48,9 +46,7 @@ export function ConfigStatusGrid({
visibleExchangeAddresses,
copiedId,
language,
isModelInUse,
getModelUsageInfo,
isExchangeInUse,
getExchangeUsageInfo,
onModelClick,
onExchangeClick,
@@ -112,14 +108,20 @@ export function ConfigStatusGrid({
<div className="p-4 space-y-3">
{configuredModels.map((model) => {
const inUse = isModelInUse(model.id)
const usageInfo = getModelUsageInfo(model.id)
return (
<div
key={model.id}
className={`group relative flex items-center justify-between p-3 rounded-md transition-all border border-transparent ${inUse ? 'opacity-80' : 'hover:bg-white/5 hover:border-white/10 cursor-pointer'
} bg-black/20`}
role="button"
tabIndex={0}
className="group relative flex cursor-pointer items-center justify-between rounded-md border border-transparent bg-black/20 p-3 transition-all hover:border-white/10 hover:bg-white/5"
onClick={() => onModelClick(model.id)}
onKeyDown={(event) => {
if (event.key === 'Enter' || event.key === ' ') {
event.preventDefault()
onModelClick(model.id)
}
}}
>
<div className="flex items-center gap-4">
<div className="relative">
@@ -193,16 +195,22 @@ export function ConfigStatusGrid({
<div className="p-4 space-y-3">
{configuredExchanges.map((exchange) => {
const inUse = isExchangeInUse(exchange.id)
const usageInfo = getExchangeUsageInfo(exchange.id)
const state = exchangeAccountStates?.[exchange.id]
const stateMeta = getExchangeStateMeta(state)
return (
<div
key={exchange.id}
className={`group relative flex items-center justify-between p-3 rounded-md transition-all border border-transparent ${inUse ? 'opacity-80' : 'hover:bg-white/5 hover:border-white/10 cursor-pointer'
} bg-black/20`}
role="button"
tabIndex={0}
className="group relative flex cursor-pointer items-center justify-between rounded-md border border-transparent bg-black/20 p-3 transition-all hover:border-white/10 hover:bg-white/5"
onClick={() => onExchangeClick(exchange.id)}
onKeyDown={(event) => {
if (event.key === 'Enter' || event.key === ' ') {
event.preventDefault()
onExchangeClick(exchange.id)
}
}}
>
<div className="flex items-center gap-4 min-w-0">
<div className="relative">

View File

@@ -38,6 +38,7 @@ const SUPPORTED_EXCHANGE_TEMPLATES = [
interface ExchangeConfigModalProps {
allExchanges: Exchange[]
editingExchangeId: string | null
initialExchangeType?: string | null
onSave: (
exchangeId: string | null,
exchangeType: string,
@@ -148,6 +149,7 @@ function ExchangeCard({
export function ExchangeConfigModal({
allExchanges,
editingExchangeId,
initialExchangeType,
onSave,
onDelete,
onClose,
@@ -155,8 +157,12 @@ export function ExchangeConfigModal({
}: ExchangeConfigModalProps) {
const { user } = useAuth()
// Step: 0 = select exchange, 1 = configure
const [currentStep, setCurrentStep] = useState(editingExchangeId ? 1 : 0)
const [selectedExchangeType, setSelectedExchangeType] = useState('')
const [currentStep, setCurrentStep] = useState(
editingExchangeId || initialExchangeType ? 1 : 0
)
const [selectedExchangeType, setSelectedExchangeType] = useState(
initialExchangeType || ''
)
const [apiKey, setApiKey] = useState('')
const [secretKey, setSecretKey] = useState('')
const [passphrase, setPassphrase] = useState('')

View File

@@ -18,6 +18,7 @@ interface ModelConfigModalProps {
allModels: AIModel[]
configuredModels: AIModel[]
editingModelId: string | null
initialModelId?: string | null
onSave: (
modelId: string,
apiKey: string,
@@ -33,13 +34,18 @@ export function ModelConfigModal({
allModels,
configuredModels,
editingModelId,
initialModelId,
onSave,
onDelete,
onClose,
language,
}: ModelConfigModalProps) {
const [currentStep, setCurrentStep] = useState(editingModelId ? 1 : 0)
const [selectedModelId, setSelectedModelId] = useState(editingModelId || '')
const [currentStep, setCurrentStep] = useState(
editingModelId || initialModelId ? 1 : 0
)
const [selectedModelId, setSelectedModelId] = useState(
editingModelId || initialModelId || ''
)
const [apiKey, setApiKey] = useState('')
const [baseUrl, setBaseUrl] = useState('')
const [modelName, setModelName] = useState('')
@@ -75,12 +81,20 @@ export function ModelConfigModal({
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault()
if (!selectedModelId || !apiKey.trim()) return
onSave(selectedModelId, apiKey.trim(), baseUrl.trim() || undefined, modelName.trim() || undefined)
onSave(
selectedModelId,
apiKey.trim(),
baseUrl.trim() || undefined,
modelName.trim() || undefined
)
}
const availableModels = allModels || []
const configuredIds = new Set(configuredModels?.map(m => m.id) || [])
const stepLabels = [t('modelConfig.selectModel', language), t('modelConfig.configureApi', language)]
const configuredIds = new Set(configuredModels?.map((m) => m.id) || [])
const stepLabels = [
t('modelConfig.selectModel', language),
t('modelConfig.configureApi', language),
]
return (
<div className="fixed inset-0 bg-black/60 flex items-center justify-center z-50 p-4 overflow-y-auto backdrop-blur-sm">
@@ -168,18 +182,23 @@ export function ModelConfigModal({
)}
{/* Step 1: Configure — Claw402 Dedicated UI */}
{(currentStep === 1 || editingModelId) && selectedModel && (selectedModel.provider === 'claw402' || selectedModel.id === 'claw402') && (
<Claw402ConfigForm
apiKey={apiKey}
modelName={modelName}
editingModelId={editingModelId}
onApiKeyChange={setApiKey}
onModelNameChange={setModelName}
onBack={handleBack}
onSubmit={handleSubmit}
language={language}
/>
)}
{(currentStep === 1 || editingModelId) &&
selectedModel &&
(selectedModel.provider === 'claw402' ||
selectedModel.id === 'claw402') && (
<Claw402ConfigForm
apiKey={apiKey}
modelName={modelName}
editingModelId={editingModelId}
initialWalletAddress={selectedModel.walletAddress}
initialBalanceUsdc={selectedModel.balanceUsdc}
onApiKeyChange={setApiKey}
onModelNameChange={setModelName}
onBack={handleBack}
onSubmit={handleSubmit}
language={language}
/>
)}
{/* Step 1: Configure — Standard Providers (non-claw402) */}
{(currentStep === 1 || editingModelId) &&
@@ -228,11 +247,11 @@ function ModelSelectionStep({
</div>
{/* Claw402 Featured Card */}
{availableModels.some(m => m.provider === 'claw402') && (
{availableModels.some((m) => m.provider === 'claw402') && (
<button
type="button"
onClick={() => {
const claw = availableModels.find(m => m.provider === 'claw402')
const claw = availableModels.find((m) => m.provider === 'claw402')
if (claw) onSelectModel(claw.id)
}}
className="w-full p-5 rounded-xl text-left transition-all hover:scale-[1.01]"
@@ -278,8 +297,13 @@ function ModelSelectionStep({
</div>
</div>
<div className="flex items-center gap-2">
{configuredIds.has(availableModels.find(m => m.provider === 'claw402')?.id || '') && (
<div className="w-2 h-2 rounded-full" style={{ background: '#00E096' }} />
{configuredIds.has(
availableModels.find((m) => m.provider === 'claw402')?.id || ''
) && (
<div
className="w-2 h-2 rounded-full"
style={{ background: '#00E096' }}
/>
)}
<div
className="px-3 py-1.5 rounded-full text-xs font-bold"
@@ -308,35 +332,45 @@ function ModelSelectionStep({
)}
<div className="grid grid-cols-3 sm:grid-cols-4 gap-3">
{availableModels.filter(m => !m.provider?.startsWith('blockrun') && m.provider !== 'claw402').map((model) => (
<ModelCard
key={model.id}
model={model}
selected={selectedModelId === model.id}
onClick={() => onSelectModel(model.id)}
configured={configuredIds.has(model.id)}
/>
))}
{availableModels
.filter(
(m) =>
!m.provider?.startsWith('blockrun') && m.provider !== 'claw402'
)
.map((model) => (
<ModelCard
key={model.id}
model={model}
selected={selectedModelId === model.id}
onClick={() => onSelectModel(model.id)}
configured={configuredIds.has(model.id)}
/>
))}
</div>
{availableModels.some(m => m.provider?.startsWith('blockrun')) && (
{availableModels.some((m) => m.provider?.startsWith('blockrun')) && (
<>
<div className="flex items-center gap-3 pt-2">
<div className="flex-1 h-px" style={{ background: '#2B3139' }} />
<span className="text-xs font-medium px-2" style={{ color: '#848E9C' }}>
<span
className="text-xs font-medium px-2"
style={{ color: '#848E9C' }}
>
{t('modelConfig.viaBlockrunWallet', language)}
</span>
<div className="flex-1 h-px" style={{ background: '#2B3139' }} />
</div>
<div className="grid grid-cols-2 gap-3">
{availableModels.filter(m => m.provider?.startsWith('blockrun')).map((model) => (
<ModelCard
key={model.id}
model={model}
selected={selectedModelId === model.id}
onClick={() => onSelectModel(model.id)}
configured={configuredIds.has(model.id)}
/>
))}
{availableModels
.filter((m) => m.provider?.startsWith('blockrun'))
.map((model) => (
<ModelCard
key={model.id}
model={model}
selected={selectedModelId === model.id}
onClick={() => onSelectModel(model.id)}
configured={configuredIds.has(model.id)}
/>
))}
</div>
</>
)}
@@ -351,6 +385,8 @@ function Claw402ConfigForm({
apiKey,
modelName,
editingModelId,
initialWalletAddress,
initialBalanceUsdc,
onApiKeyChange,
onModelNameChange,
onBack,
@@ -360,18 +396,22 @@ function Claw402ConfigForm({
apiKey: string
modelName: string
editingModelId: string | null
initialWalletAddress?: string
initialBalanceUsdc?: string
onApiKeyChange: (value: string) => void
onModelNameChange: (value: string) => void
onBack: () => void
onSubmit: (e: React.FormEvent) => void
language: Language
}) {
const [walletAddress, setWalletAddress] = useState('')
const [walletAddress, setWalletAddress] = useState(initialWalletAddress || '')
const [copiedAddr, setCopiedAddr] = useState(false)
const [showDeposit, setShowDeposit] = useState(false)
const [showDeposit, setShowDeposit] = useState(Boolean(initialWalletAddress))
const [showNewWalletBackup, setShowNewWalletBackup] = useState(false)
const [newWalletKey, setNewWalletKey] = useState('')
const [usdcBalance, setUsdcBalance] = useState<string | null>(null)
const [usdcBalance, setUsdcBalance] = useState<string | null>(
initialBalanceUsdc || null
)
const [keyError, setKeyError] = useState('')
const [validating, setValidating] = useState(false)
const [claw402Status, setClaw402Status] = useState<string | null>(null)
@@ -400,17 +440,25 @@ function Claw402ConfigForm({
// Truncate address for display
// Debounced validation when apiKey changes
useEffect(() => {
setWalletAddress('')
setUsdcBalance(null)
setClaw402Status(null)
setTestResult(null)
const clientErr = getClientError(apiKey)
setKeyError(clientErr)
if (!apiKey) {
setWalletAddress(initialWalletAddress || '')
setUsdcBalance(initialBalanceUsdc || null)
setShowDeposit(Boolean(initialWalletAddress))
setValidating(false)
return
}
setWalletAddress('')
setUsdcBalance(null)
if (clientErr || !apiKey) {
setValidating(false)
return
@@ -441,7 +489,7 @@ function Claw402ConfigForm({
}, 500)
return () => clearTimeout(timer)
}, [apiKey])
}, [apiKey, initialBalanceUsdc, initialWalletAddress, language])
const handleTestConnection = async () => {
setTesting(true)
@@ -666,24 +714,33 @@ function Claw402ConfigForm({
: '1px solid #2B3139',
color: '#EAECEF',
}}
required
required={!walletAddress}
/>
{!apiKey && (
{!apiKey && !walletAddress && (
<button
type="button"
onClick={async () => {
try {
const res = await fetch('/api/wallet/generate', { method: 'POST' })
const res = await fetch('/api/wallet/generate', {
method: 'POST',
})
const data = await res.json()
if (data.private_key) {
onApiKeyChange(data.private_key)
setShowNewWalletBackup(true)
setNewWalletKey(data.private_key)
}
} catch { /* ignore */ }
} catch {
/* ignore */
}
}}
className="shrink-0 px-3 py-3 rounded-xl text-xs font-semibold transition-all hover:scale-[1.02]"
style={{ background: 'linear-gradient(135deg, #2563EB, #7C3AED)', color: '#fff', border: 'none', cursor: 'pointer' }}
style={{
background: 'linear-gradient(135deg, #2563EB, #7C3AED)',
color: '#fff',
border: 'none',
cursor: 'pointer',
}}
>
{language === 'zh' ? '🔑 创建钱包' : '🔑 Create Wallet'}
</button>
@@ -692,9 +749,21 @@ function Claw402ConfigForm({
{/* New wallet backup warning */}
{showNewWalletBackup && newWalletKey && (
<div className="p-3 rounded-xl" style={{ background: 'rgba(239, 68, 68, 0.08)', border: '1px solid rgba(239, 68, 68, 0.3)' }}>
<div className="text-xs font-bold mb-2" style={{ color: '#EF4444' }}>
🚨 {language === 'zh' ? '重要:请立即备份私钥!' : 'Important: Backup your private key NOW!'}
<div
className="p-3 rounded-xl"
style={{
background: 'rgba(239, 68, 68, 0.08)',
border: '1px solid rgba(239, 68, 68, 0.3)',
}}
>
<div
className="text-xs font-bold mb-2"
style={{ color: '#EF4444' }}
>
🚨{' '}
{language === 'zh'
? '重要:请立即备份私钥!'
: 'Important: Backup your private key NOW!'}
</div>
<div className="text-[11px] mb-2" style={{ color: '#F87171' }}>
{language === 'zh'
@@ -702,7 +771,10 @@ function Claw402ConfigForm({
: 'This is your wallet private key. If lost, it cannot be recovered and all assets will be permanently lost. Copy and save it securely.'}
</div>
<div className="flex items-center gap-2 mb-2">
<code className="text-[10px] font-mono break-all select-all flex-1 p-2 rounded" style={{ background: '#0B0E11', color: '#F87171' }}>
<code
className="text-[10px] font-mono break-all select-all flex-1 p-2 rounded"
style={{ background: '#0B0E11', color: '#F87171' }}
>
{newWalletKey}
</code>
<button
@@ -713,15 +785,38 @@ function Claw402ConfigForm({
setTimeout(() => setCopiedAddr(false), 2000)
}}
className="shrink-0 text-[10px] px-2 py-1 rounded"
style={{ background: 'rgba(239,68,68,0.15)', color: '#F87171', border: 'none', cursor: 'pointer' }}
style={{
background: 'rgba(239,68,68,0.15)',
color: '#F87171',
border: 'none',
cursor: 'pointer',
}}
>
{copiedAddr ? '✅ Copied' : '📋 Copy Key'}
</button>
</div>
<div className="text-[10px] space-y-1" style={{ color: '#848E9C' }}>
<div> {language === 'zh' ? '建议保存到密码管理器1Password / Bitwarden' : 'Save to a password manager (1Password / Bitwarden)'}</div>
<div> {language === 'zh' ? '或抄在纸上放安全的地方' : 'Or write it down and store it safely'}</div>
<div> {language === 'zh' ? '不要截图发给别人' : 'Do NOT screenshot or share with anyone'}</div>
<div
className="text-[10px] space-y-1"
style={{ color: '#848E9C' }}
>
<div>
{' '}
{language === 'zh'
? '建议保存到密码管理器1Password / Bitwarden'
: 'Save to a password manager (1Password / Bitwarden)'}
</div>
<div>
{' '}
{language === 'zh'
? '或抄在纸上放安全的地方'
: 'Or write it down and store it safely'}
</div>
<div>
{' '}
{language === 'zh'
? '不要截图发给别人'
: 'Do NOT screenshot or share with anyone'}
</div>
</div>
</div>
)}
@@ -736,7 +831,7 @@ function Claw402ConfigForm({
</div>
{/* Wallet Validation Results */}
{apiKey && (
{(apiKey || walletAddress) && (
<div className="space-y-2 pl-1">
{/* Validating spinner */}
{validating && (
@@ -792,15 +887,28 @@ function Claw402ConfigForm({
{copiedAddr ? '✅' : '📋'}
</button>
</div>
<code className="text-[11px] font-mono block select-all" style={{ color: '#60A5FA' }}>{walletAddress}</code>
<div className="text-[10px] mt-1.5" style={{ color: '#F59E0B' }}>
{language === 'zh' ? '请确认这是你的钱包地址(可在 MetaMask 中核对)' : 'Please confirm this is your wallet address (verify in MetaMask)'}
<code
className="text-[11px] font-mono block select-all"
style={{ color: '#60A5FA' }}
>
{walletAddress}
</code>
<div
className="text-[10px] mt-1.5"
style={{ color: '#F59E0B' }}
>
{' '}
{language === 'zh'
? '请确认这是你的钱包地址(可在 MetaMask 中核对)'
: 'Please confirm this is your wallet address (verify in MetaMask)'}
</div>
</div>
{usdcBalance !== null && (
<div className="flex items-center gap-2 text-xs">
<span>💰</span>
<span style={{ color: balanceNum > 0 ? '#00E096' : '#F59E0B' }}>
<span
style={{ color: balanceNum > 0 ? '#00E096' : '#F59E0B' }}
>
{t('modelConfig.usdcBalance', language)}: ${usdcBalance}
</span>
<button
@@ -842,7 +950,10 @@ function Claw402ConfigForm({
: 'Deposit USDC (Base Chain)'}
</div>
<div className="flex gap-3 items-start mb-3">
<div className="shrink-0 p-1.5 rounded-lg" style={{ background: '#fff' }}>
<div
className="shrink-0 p-1.5 rounded-lg"
style={{ background: '#fff' }}
>
<QRCodeSVG value={walletAddress} size={80} level="M" />
</div>
<div className="flex-1 min-w-0">
@@ -854,7 +965,12 @@ function Claw402ConfigForm({
? '扫码或复制地址转账'
: 'Scan QR or copy address to transfer'}
</div>
<code className="text-[10px] font-mono break-all select-all block mb-1.5" style={{ color: '#60A5FA' }}>{walletAddress}</code>
<code
className="text-[10px] font-mono break-all select-all block mb-1.5"
style={{ color: '#60A5FA' }}
>
{walletAddress}
</code>
<button
type="button"
onClick={() => {
@@ -1015,7 +1131,12 @@ function Claw402ConfigForm({
type="submit"
disabled={!isKeyValid}
className="flex-1 flex items-center justify-center gap-2 px-4 py-3 rounded-xl text-sm font-bold transition-all hover:scale-[1.02] disabled:opacity-50 disabled:cursor-not-allowed"
style={{ background: isKeyValid ? 'linear-gradient(135deg, #2563EB, #7C3AED)' : '#2B3139', color: '#fff' }}
style={{
background: isKeyValid
? 'linear-gradient(135deg, #2563EB, #7C3AED)'
: '#2B3139',
color: '#fff',
}}
>
{'🚀 ' + t('modelConfig.startTrading', language)}
</button>
@@ -1119,9 +1240,14 @@ function StandardProviderConfigForm({
{editingModelId && selectedModel && 'has_api_key' in selectedModel && (
<div
className="p-3 rounded-xl text-xs"
style={{ background: 'rgba(14, 203, 129, 0.08)', border: '1px solid rgba(14, 203, 129, 0.2)', color: '#9FE8C5' }}
style={{
background: 'rgba(14, 203, 129, 0.08)',
border: '1px solid rgba(14, 203, 129, 0.2)',
color: '#9FE8C5',
}}
>
{selectedModel.has_api_key ? '已配置 API Key' : '未配置 API Key'}
{selectedModel.has_api_key ? '已配置 API Key' : '未配置 API Key'}
</div>
)}
@@ -1156,10 +1282,10 @@ function StandardProviderConfigForm({
editingModelId && selectedModel.has_api_key
? '已保存,如需更换请重新输入'
: selectedModel.provider === 'blockrun-base'
? '0x... (EVM private key)'
: selectedModel.provider === 'blockrun-sol'
? 'bs58 encoded key (Solana)'
: t('enterAPIKey', language)
? '0x... (EVM private key)'
: selectedModel.provider === 'blockrun-sol'
? 'bs58 encoded key (Solana)'
: t('enterAPIKey', language)
}
className="w-full px-4 py-3 rounded-xl"
style={{
@@ -1174,9 +1300,23 @@ function StandardProviderConfigForm({
{/* Custom Base URL (hidden for BlockRun) */}
{!selectedModel.provider?.startsWith('blockrun') && (
<div className="space-y-2">
<label className="flex items-center gap-2 text-sm font-semibold" style={{ color: '#EAECEF' }}>
<svg className="w-4 h-4" style={{ color: '#A78BFA' }} fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13.828 10.172a4 4 0 00-5.656 0l-4 4a4 4 0 105.656 5.656l1.102-1.101m-.758-4.899a4 4 0 005.656 0l4-4a4 4 0 00-5.656-5.656l-1.1 1.1" />
<label
className="flex items-center gap-2 text-sm font-semibold"
style={{ color: '#EAECEF' }}
>
<svg
className="w-4 h-4"
style={{ color: '#A78BFA' }}
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M13.828 10.172a4 4 0 00-5.656 0l-4 4a4 4 0 105.656 5.656l1.102-1.101m-.758-4.899a4 4 0 005.656 0l4-4a4 4 0 00-5.656-5.656l-1.1 1.1"
/>
</svg>
{t('customBaseURL', language)}
</label>
@@ -1186,7 +1326,11 @@ function StandardProviderConfigForm({
onChange={(e) => onBaseUrlChange(e.target.value)}
placeholder={t('customBaseURLPlaceholder', language)}
className="w-full px-4 py-3 rounded-xl"
style={{ background: '#0B0E11', border: '1px solid #2B3139', color: '#EAECEF' }}
style={{
background: '#0B0E11',
border: '1px solid #2B3139',
color: '#EAECEF',
}}
/>
<div className="text-xs" style={{ color: '#848E9C' }}>
{t('leaveBlankForDefault', language)}
@@ -1197,9 +1341,23 @@ function StandardProviderConfigForm({
{/* Custom Model Name (hidden for BlockRun) */}
{!selectedModel.provider?.startsWith('blockrun') && (
<div className="space-y-2">
<label className="flex items-center gap-2 text-sm font-semibold" style={{ color: '#EAECEF' }}>
<svg className="w-4 h-4" style={{ color: '#A78BFA' }} fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M7 7h.01M7 3h5c.512 0 1.024.195 1.414.586l7 7a2 2 0 010 2.828l-7 7a2 2 0 01-2.828 0l-7-7A1.994 1.994 0 013 12V7a4 4 0 014-4z" />
<label
className="flex items-center gap-2 text-sm font-semibold"
style={{ color: '#EAECEF' }}
>
<svg
className="w-4 h-4"
style={{ color: '#A78BFA' }}
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M7 7h.01M7 3h5c.512 0 1.024.195 1.414.586l7 7a2 2 0 010 2.828l-7 7a2 2 0 01-2.828 0l-7-7A1.994 1.994 0 013 12V7a4 4 0 014-4z"
/>
</svg>
{t('customModelName', language)}
</label>
@@ -1209,7 +1367,11 @@ function StandardProviderConfigForm({
onChange={(e) => onModelNameChange(e.target.value)}
placeholder={t('customModelNamePlaceholder', language)}
className="w-full px-4 py-3 rounded-xl"
style={{ background: '#0B0E11', border: '1px solid #2B3139', color: '#EAECEF' }}
style={{
background: '#0B0E11',
border: '1px solid #2B3139',
color: '#EAECEF',
}}
/>
<div className="text-xs" style={{ color: '#848E9C' }}>
{t('leaveBlankForDefaultModel', language)}
@@ -1220,9 +1382,23 @@ function StandardProviderConfigForm({
{/* BlockRun Model Selector */}
{selectedModel.provider?.startsWith('blockrun') && (
<div className="space-y-2">
<label className="flex items-center gap-2 text-sm font-semibold" style={{ color: '#EAECEF' }}>
<svg className="w-4 h-4" style={{ color: '#A78BFA' }} fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9.75 17L9 20l-1 1h8l-1-1-.75-3M3 13h18M5 17h14a2 2 0 002-2V5a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z" />
<label
className="flex items-center gap-2 text-sm font-semibold"
style={{ color: '#EAECEF' }}
>
<svg
className="w-4 h-4"
style={{ color: '#A78BFA' }}
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M9.75 17L9 20l-1 1h8l-1-1-.75-3M3 13h18M5 17h14a2 2 0 002-2V5a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z"
/>
</svg>
{t('modelConfig.selectModelLabel', language)}
</label>
@@ -1236,14 +1412,23 @@ function StandardProviderConfigForm({
onClick={() => onModelNameChange(m.id)}
className="flex flex-col items-start px-3 py-2 rounded-xl text-left transition-all"
style={{
background: isSelected ? 'rgba(37, 99, 235, 0.2)' : '#0B0E11',
border: isSelected ? '1px solid #2563EB' : '1px solid #2B3139',
background: isSelected
? 'rgba(37, 99, 235, 0.2)'
: '#0B0E11',
border: isSelected
? '1px solid #2563EB'
: '1px solid #2B3139',
}}
>
<span className="text-xs font-semibold" style={{ color: isSelected ? '#60A5FA' : '#EAECEF' }}>
<span
className="text-xs font-semibold"
style={{ color: isSelected ? '#60A5FA' : '#EAECEF' }}
>
{m.name}
</span>
<span className="text-[10px]" style={{ color: '#848E9C' }}>{m.desc}</span>
<span className="text-[10px]" style={{ color: '#848E9C' }}>
{m.desc}
</span>
</button>
)
})}

View File

@@ -18,24 +18,34 @@ const setupSteps = [
detail:
'Your account keeps the Autopilot configuration, wallet authorization state, and trading dashboard in one place.',
icon: KeyRound,
action: 'Create account',
to: ROUTES.register,
},
{
title: 'Fund the AI fee wallet',
detail:
'NOFX prepares a Base USDC wallet for Claw402.ai data and model calls. This wallet is separate from trading collateral.',
icon: CircleDollarSign,
action: 'Open deposit QR',
to: ROUTES.login,
returnUrl: `${ROUTES.traders}?setup=claw402`,
},
{
title: 'Authorize Hyperliquid',
detail:
'Connect your trading wallet, approve the NOFX Agent, and approve the builder fee. Funds remain in your Hyperliquid account.',
icon: Wallet,
action: 'Connect exchange',
to: ROUTES.login,
returnUrl: `${ROUTES.traders}?setup=hyperliquid`,
},
{
title: 'Deposit trading USDC',
detail:
'Add USDC on Hyperliquid, then start NOFX Autopilot. The strategy is created and launched automatically.',
icon: Zap,
action: 'Open Hyperliquid',
href: 'https://app.hyperliquid.xyz/',
},
]
@@ -66,6 +76,12 @@ export function TraderLaunchGuestPage() {
<div className="mt-7 flex flex-col gap-3 sm:flex-row">
<Link
to={ROUTES.login}
onClick={() =>
sessionStorage.setItem(
'returnUrl',
`${ROUTES.traders}?setup=claw402`
)
}
className="inline-flex items-center justify-center gap-2 rounded-xl bg-nofx-gold px-5 py-3 text-sm font-bold text-black transition hover:bg-yellow-400"
>
Start setup
@@ -83,11 +99,10 @@ export function TraderLaunchGuestPage() {
<div className="grid gap-3 sm:grid-cols-2">
{setupSteps.map((step, index) => {
const Icon = step.icon
return (
<div
key={step.title}
className="rounded-xl border border-white/10 bg-black/24 p-4"
>
const cardClass =
'group rounded-xl border border-white/10 bg-black/24 p-4 text-left transition hover:border-nofx-gold/35 hover:bg-nofx-gold/[0.04]'
const content = (
<>
<div className="mb-4 flex items-center justify-between">
<div className="flex h-10 w-10 items-center justify-center rounded-xl border border-nofx-gold/20 bg-nofx-gold/10 text-nofx-gold">
<Icon className="h-4 w-4" />
@@ -102,7 +117,44 @@ export function TraderLaunchGuestPage() {
<p className="mt-2 text-sm leading-6 text-zinc-500">
{step.detail}
</p>
</div>
<div className="mt-4 inline-flex items-center gap-2 text-xs font-bold text-nofx-gold transition group-hover:text-yellow-300">
{step.action}
{step.href ? (
<ExternalLink className="h-3.5 w-3.5" />
) : (
<ArrowRight className="h-3.5 w-3.5" />
)}
</div>
</>
)
if (step.href) {
return (
<a
key={step.title}
href={step.href}
target="_blank"
rel="noreferrer"
className={cardClass}
>
{content}
</a>
)
}
return (
<Link
key={step.title}
to={step.to || ROUTES.login}
onClick={() => {
if (step.returnUrl) {
sessionStorage.setItem('returnUrl', step.returnUrl)
}
}}
className={cardClass}
>
{content}
</Link>
)
})}
</div>