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

View File

@@ -14,15 +14,16 @@ type EquityStore struct {
// EquitySnapshot equity snapshot // EquitySnapshot equity snapshot
type EquitySnapshot struct { type EquitySnapshot struct {
ID int64 `gorm:"primaryKey;autoIncrement" json:"id"` ID int64 `gorm:"primaryKey;autoIncrement" json:"id"`
TraderID string `gorm:"column:trader_id;not null;index:idx_equity_trader_time" json:"trader_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"` 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"` TotalEquity float64 `gorm:"column:total_equity;not null;default:0" json:"total_equity"`
Balance float64 `gorm:"not null;default:0" json:"balance"` Balance float64 `gorm:"not null;default:0" json:"balance"`
UnrealizedPnL float64 `gorm:"column:unrealized_pnl;not null;default:0" json:"unrealized_pnl"` AvailableBalance float64 `gorm:"column:available_balance;not null;default:0" json:"available_balance"`
PositionCount int `gorm:"column:position_count;default:0" json:"position_count"` UnrealizedPnL float64 `gorm:"column:unrealized_pnl;not null;default:0" json:"unrealized_pnl"`
MarginUsedPct float64 `gorm:"column:margin_used_pct;default:0" json:"margin_used_pct"` PositionCount int `gorm:"column:position_count;default:0" json:"position_count"`
CreatedAt time.Time `json:"created_at"` 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" } func (EquitySnapshot) TableName() string { return "trader_equity_snapshots" }
@@ -98,6 +99,7 @@ func (s *EquityStore) GetAllTradersLatest() (map[string]*EquitySnapshot, error)
var snapshots []*EquitySnapshot var snapshots []*EquitySnapshot
err := s.db.Raw(` err := s.db.Raw(`
SELECT e.id, e.trader_id, e.timestamp, e.total_equity, e.balance, 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 e.unrealized_pnl, e.position_count, e.margin_used_pct, e.created_at
FROM trader_equity_snapshots e FROM trader_equity_snapshots e
INNER JOIN ( INNER JOIN (
@@ -159,12 +161,13 @@ func (s *EquityStore) MigrateFromDecision() (int64, error) {
result := s.db.Exec(` result := s.db.Exec(`
INSERT INTO trader_equity_snapshots ( INSERT INTO trader_equity_snapshots (
trader_id, timestamp, total_equity, balance, trader_id, timestamp, total_equity, balance,
unrealized_pnl, position_count, margin_used_pct available_balance, unrealized_pnl, position_count, margin_used_pct
) )
SELECT SELECT
dr.trader_id, dr.trader_id,
dr.timestamp, dr.timestamp,
das.total_balance, das.total_balance,
das.total_balance - das.total_unrealized_profit,
das.available_balance, das.available_balance,
das.total_unrealized_profit, das.total_unrealized_profit,
das.position_count, 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 // Check if this is a grid trading strategy
isGridStrategy := at.IsGridStrategy() isGridStrategy := at.IsGridStrategy()
if isGridStrategy { if isGridStrategy {
@@ -538,6 +535,7 @@ func (at *AutoTrader) Run() error {
} }
// Execute immediately on first run // Execute immediately on first run
at.logInfof("▶️ Running first trading cycle immediately; next cycle starts after %v", at.config.ScanInterval)
if isGridStrategy { if isGridStrategy {
if err := at.RunGridCycle(); err != nil { if err := at.RunGridCycle(); err != nil {
at.logErrorf("❌ Grid execution failed: %v", err) 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 { for {
at.isRunningMutex.RLock() at.isRunningMutex.RLock()
running := at.isRunning running := at.isRunning

View File

@@ -3,11 +3,11 @@ package trader
import ( import (
"fmt" "fmt"
"math" "math"
"nofx/telemetry"
"nofx/kernel" "nofx/kernel"
"nofx/logger" "nofx/logger"
"nofx/market" "nofx/market"
"nofx/store" "nofx/store"
"nofx/telemetry"
"time" "time"
) )
@@ -18,13 +18,14 @@ func (at *AutoTrader) saveEquitySnapshot(ctx *kernel.Context) {
} }
snapshot := &store.EquitySnapshot{ snapshot := &store.EquitySnapshot{
TraderID: at.id, TraderID: at.id,
Timestamp: time.Now().UTC(), Timestamp: time.Now().UTC(),
TotalEquity: ctx.Account.TotalEquity, TotalEquity: ctx.Account.TotalEquity,
Balance: ctx.Account.TotalEquity - ctx.Account.UnrealizedPnL, Balance: ctx.Account.TotalEquity - ctx.Account.UnrealizedPnL,
UnrealizedPnL: ctx.Account.UnrealizedPnL, AvailableBalance: ctx.Account.AvailableBalance,
PositionCount: ctx.Account.PositionCount, UnrealizedPnL: ctx.Account.UnrealizedPnL,
MarginUsedPct: ctx.Account.MarginUsedPct, PositionCount: ctx.Account.PositionCount,
MarginUsedPct: ctx.Account.MarginUsedPct,
} }
if err := at.store.Equity().Save(snapshot); err != nil { 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", logger.Infof(" └─ %s: size=%s, entryPx=%s, posValue=%s, pnl=%s",
pos.Position.Coin, pos.Position.Szi, entryPx, pos.Position.PositionValue, pos.Position.UnrealizedPnl) pos.Position.Coin, pos.Position.Szi, entryPx, pos.Position.PositionValue, pos.Position.UnrealizedPnl)
} }
xyzWalletBalance := xyzAccountValue - xyzUnrealizedPnl xyzMarginUsed := calculateXYZMarginUsed(xyzPositions)
balanceBreakdown := calculateHyperliquidBalanceBreakdown(
// Step 6: Correctly handle Spot + Perpetuals + xyz dex balance t.isUnifiedAccount,
// Important: Each account is independent, manual transfers required spotUSDCBalance,
totalWalletBalance := walletBalanceWithoutUnrealized + spotUSDCBalance + xyzWalletBalance accountValue,
totalUnrealizedPnlAll := totalUnrealizedPnl + xyzUnrealizedPnl totalUnrealizedPnl,
totalMarginUsed,
// Calculate total equity properly: perpAccountValue + spotUSDCBalance + xyzAccountValue availableBalance,
// Note: totalWalletBalance + totalUnrealizedPnlAll should equal this xyzAccountValue,
totalEquityCalculated := accountValue + spotUSDCBalance + 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 // 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 { if t.isUnifiedAccount && spotUSDCBalance > 0 {
// Add Spot balance to available balance for trading logger.Infof("✓ Unified Account: Spot %.2f USDC used as shared collateral (available: %.2f)",
availableBalance = availableBalance + spotUSDCBalance
logger.Infof("✓ Unified Account: Spot %.2f USDC added to available balance (total: %.2f)",
spotUSDCBalance, availableBalance) 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["xyzDexBalance"] = xyzAccountValue // xyz dex equity (stock perps, forex, commodities)
result["xyzDexUnrealizedPnl"] = xyzUnrealizedPnl // xyz dex unrealized PnL result["xyzDexUnrealizedPnl"] = xyzUnrealizedPnl // xyz dex unrealized PnL
result["perpAccountValue"] = accountValue // Perp account value for debugging result["perpAccountValue"] = accountValue // Perp account value for debugging
result["totalMarginUsed"] = balanceBreakdown.TotalMarginUsed
logger.Infof("✓ Hyperliquid complete account:") logger.Infof("✓ Hyperliquid complete account:")
logger.Infof(" • Spot balance: %.2f USDC", spotUSDCBalance) 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(" • Margin used: %.2f USDC", totalMarginUsed)
logger.Infof(" • xyz dex equity: %.2f USDC (wallet %.2f + unrealized %.2f)", logger.Infof(" • xyz dex equity: %.2f USDC (wallet %.2f + unrealized %.2f)",
xyzAccountValue, xyzAccountValue,
xyzWalletBalance, balanceBreakdown.XYZWalletBalance,
xyzUnrealizedPnl) xyzUnrealizedPnl)
logger.Infof(" • Total assets (Perp+Spot+xyz): %.2f USDC", totalWalletBalance) logger.Infof(" • Total wallet balance: %.2f USDC", totalWalletBalance)
logger.Infof(" ⭐ Total: %.2f USDC | Perp: %.2f | Spot: %.2f | xyz: %.2f", logger.Infof(" ⭐ Total equity: %.2f USDC | Available: %.2f | Spot: %.2f | xyz view: %.2f",
totalWalletBalance, availableBalance, spotUSDCBalance, xyzAccountValue) totalEquityCalculated, availableBalance, spotUSDCBalance, xyzAccountValue)
return result, nil 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 // xyzDexState represents the clearinghouse state for xyz dex
type xyzDexState struct { type xyzDexState struct {
MarginSummary *xyzMarginSummary `json:"marginSummary,omitempty"` 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 { useState, useEffect } from 'react'
import { useNavigate } from 'react-router-dom' import { useNavigate, useSearchParams } from 'react-router-dom'
import useSWR from 'swr' import useSWR from 'swr'
import { api } from '../../lib/api' import { api } from '../../lib/api'
import type { import type {
@@ -32,6 +32,7 @@ export function AITradersPage({ onTraderSelect }: AITradersPageProps) {
const { language } = useLanguage() const { language } = useLanguage()
const { user, token } = useAuth() const { user, token } = useAuth()
const navigate = useNavigate() const navigate = useNavigate()
const [searchParams, setSearchParams] = useSearchParams()
const [showCreateModal, setShowCreateModal] = useState(false) const [showCreateModal, setShowCreateModal] = useState(false)
const [showEditModal, setShowEditModal] = useState(false) const [showEditModal, setShowEditModal] = useState(false)
const [showModelModal, setShowModelModal] = useState(false) const [showModelModal, setShowModelModal] = useState(false)
@@ -39,6 +40,10 @@ export function AITradersPage({ onTraderSelect }: AITradersPageProps) {
const [showTelegramModal, setShowTelegramModal] = useState(false) const [showTelegramModal, setShowTelegramModal] = useState(false)
const [editingModel, setEditingModel] = useState<string | null>(null) const [editingModel, setEditingModel] = useState<string | null>(null)
const [editingExchange, setEditingExchange] = 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 [editingTrader, setEditingTrader] = useState<any>(null)
const [allModels, setAllModels] = useState<AIModel[]>([]) const [allModels, setAllModels] = useState<AIModel[]>([])
const [allExchanges, setAllExchanges] = useState<Exchange[]>([]) const [allExchanges, setAllExchanges] = useState<Exchange[]>([])
@@ -166,10 +171,6 @@ export function AITradersPage({ onTraderSelect }: AITradersPageProps) {
return true return true
}) || [] }) || []
const isModelInUse = (modelId: string) => {
return traders?.some((tr) => tr.ai_model === modelId && tr.is_running)
}
const getModelUsageInfo = (modelId: string) => { const getModelUsageInfo = (modelId: string) => {
const usingTraders = traders?.filter((tr) => tr.ai_model === modelId) || [] const usingTraders = traders?.filter((tr) => tr.ai_model === modelId) || []
const runningCount = usingTraders.filter((tr) => tr.is_running).length const runningCount = usingTraders.filter((tr) => tr.is_running).length
@@ -177,10 +178,6 @@ export function AITradersPage({ onTraderSelect }: AITradersPageProps) {
return { runningCount, totalCount, usingTraders } return { runningCount, totalCount, usingTraders }
} }
const isExchangeInUse = (exchangeId: string) => {
return traders?.some((tr) => tr.exchange_id === exchangeId && tr.is_running)
}
const getExchangeUsageInfo = (exchangeId: string) => { const getExchangeUsageInfo = (exchangeId: string) => {
const usingTraders = const usingTraders =
traders?.filter((tr) => tr.exchange_id === exchangeId) || [] traders?.filter((tr) => tr.exchange_id === exchangeId) || []
@@ -334,17 +331,15 @@ export function AITradersPage({ onTraderSelect }: AITradersPageProps) {
} }
const handleModelClick = (modelId: string) => { const handleModelClick = (modelId: string) => {
if (!isModelInUse(modelId)) { setInitialModelId(null)
setEditingModel(modelId) setEditingModel(modelId)
setShowModelModal(true) setShowModelModal(true)
}
} }
const handleExchangeClick = (exchangeId: string) => { const handleExchangeClick = (exchangeId: string) => {
if (!isExchangeInUse(exchangeId)) { setInitialExchangeType(null)
setEditingExchange(exchangeId) setEditingExchange(exchangeId)
setShowExchangeModal(true) setShowExchangeModal(true)
}
} }
const handleDeleteConfig = async <T extends { id: string }>(config: { const handleDeleteConfig = async <T extends { id: string }>(config: {
@@ -619,15 +614,63 @@ export function AITradersPage({ onTraderSelect }: AITradersPageProps) {
} }
const handleAddModel = () => { const handleAddModel = () => {
setInitialModelId(null)
setEditingModel(null) setEditingModel(null)
setShowModelModal(true) setShowModelModal(true)
} }
const handleAddExchange = () => { const handleAddExchange = () => {
setInitialExchangeType(null)
setEditingExchange(null) setEditingExchange(null)
setShowExchangeModal(true) 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 () => { const refreshLaunchState = async () => {
await Promise.all([loadConfigs(), mutateTraders()]) await Promise.all([loadConfigs(), mutateTraders()])
} }
@@ -715,9 +758,7 @@ export function AITradersPage({ onTraderSelect }: AITradersPageProps) {
visibleExchangeAddresses={visibleExchangeAddresses} visibleExchangeAddresses={visibleExchangeAddresses}
copiedId={copiedId} copiedId={copiedId}
language={language} language={language}
isModelInUse={isModelInUse}
getModelUsageInfo={getModelUsageInfo} getModelUsageInfo={getModelUsageInfo}
isExchangeInUse={isExchangeInUse}
getExchangeUsageInfo={getExchangeUsageInfo} getExchangeUsageInfo={getExchangeUsageInfo}
onModelClick={handleModelClick} onModelClick={handleModelClick}
onExchangeClick={handleExchangeClick} onExchangeClick={handleExchangeClick}
@@ -733,6 +774,8 @@ export function AITradersPage({ onTraderSelect }: AITradersPageProps) {
isLoggedIn={Boolean(user && token)} isLoggedIn={Boolean(user && token)}
language={language} language={language}
onRefresh={refreshLaunchState} onRefresh={refreshLaunchState}
onOpenClaw402Config={handleOpenClaw402Config}
onOpenHyperliquidConfig={handleOpenHyperliquidConfig}
/> />
{/* Traders List */} {/* Traders List */}
@@ -789,11 +832,13 @@ export function AITradersPage({ onTraderSelect }: AITradersPageProps) {
allModels={supportedModels} allModels={supportedModels}
configuredModels={allModels} configuredModels={allModels}
editingModelId={editingModel} editingModelId={editingModel}
initialModelId={initialModelId}
onSave={handleSaveModelConfig} onSave={handleSaveModelConfig}
onDelete={handleDeleteModelConfig} onDelete={handleDeleteModelConfig}
onClose={() => { onClose={() => {
setShowModelModal(false) setShowModelModal(false)
setEditingModel(null) setEditingModel(null)
setInitialModelId(null)
}} }}
language={language} language={language}
/> />
@@ -804,11 +849,13 @@ export function AITradersPage({ onTraderSelect }: AITradersPageProps) {
<ExchangeConfigModal <ExchangeConfigModal
allExchanges={allExchanges} allExchanges={allExchanges}
editingExchangeId={editingExchange} editingExchangeId={editingExchange}
initialExchangeType={initialExchangeType}
onSave={handleSaveExchangeConfig} onSave={handleSaveExchangeConfig}
onDelete={handleDeleteExchangeConfig} onDelete={handleDeleteExchangeConfig}
onClose={() => { onClose={() => {
setShowExchangeModal(false) setShowExchangeModal(false)
setEditingExchange(null) setEditingExchange(null)
setInitialExchangeType(null)
}} }}
language={language} language={language}
/> />

View File

@@ -37,6 +37,8 @@ interface AutopilotLaunchPanelProps {
isLoggedIn: boolean isLoggedIn: boolean
language: string language: string
onRefresh: () => Promise<void> onRefresh: () => Promise<void>
onOpenClaw402Config?: () => void
onOpenHyperliquidConfig?: () => void
} }
const MIN_AI_FEE_USDC = 1 const MIN_AI_FEE_USDC = 1
@@ -186,6 +188,8 @@ export function AutopilotLaunchPanel({
isLoggedIn, isLoggedIn,
language, language,
onRefresh, onRefresh,
onOpenClaw402Config,
onOpenHyperliquidConfig,
}: AutopilotLaunchPanelProps) { }: AutopilotLaunchPanelProps) {
const navigate = useNavigate() const navigate = useNavigate()
const [wallet, setWallet] = useState<CurrentBeginnerWalletResponse | null>( const [wallet, setWallet] = useState<CurrentBeginnerWalletResponse | null>(
@@ -371,15 +375,34 @@ export function AutopilotLaunchPanel({
? `${shortAddress(feeWalletAddress)} · ${formatUSDC(feeWalletBalance)} USDC` ? `${shortAddress(feeWalletAddress)} · ${formatUSDC(feeWalletBalance)} USDC`
: 'Base USDC wallet required', : 'Base USDC wallet required',
action: feeWalletAddress ? ( 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 <button
type="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" 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" /> <ArrowRight className="h-3.5 w-3.5" />
Copy Open
</button> </button>
) : undefined, ),
}, },
{ {
title: 'Hyperliquid trading wallet', title: 'Hyperliquid trading wallet',
@@ -389,6 +412,16 @@ export function AutopilotLaunchPanel({
meta: hyperliquidExchange?.hyperliquidWalletAddr meta: hyperliquidExchange?.hyperliquidWalletAddr
? `${shortAddress(hyperliquidExchange.hyperliquidWalletAddr)} · authorized` ? `${shortAddress(hyperliquidExchange.hyperliquidWalletAddr)} · authorized`
: 'Agent and trading authorization required', : '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', title: 'Trading balance',
@@ -421,10 +454,16 @@ export function AutopilotLaunchPanel({
return ( return (
<button <button
type="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" 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" /> <ArrowRight className="h-4 w-4" />
</button> </button>
) )
@@ -435,9 +474,13 @@ export function AutopilotLaunchPanel({
<button <button
type="button" type="button"
onClick={() => { onClick={() => {
document if (onOpenHyperliquidConfig) {
.getElementById('hyperliquid-quick-connect') onOpenHyperliquidConfig()
?.scrollIntoView({ behavior: 'smooth', block: 'start' }) } 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" 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> visibleExchangeAddresses: Set<string>
copiedId: string | null copiedId: string | null
language: Language language: Language
isModelInUse: (modelId: string) => boolean | undefined
getModelUsageInfo: (modelId: string) => UsageInfo getModelUsageInfo: (modelId: string) => UsageInfo
isExchangeInUse: (exchangeId: string) => boolean | undefined
getExchangeUsageInfo: (exchangeId: string) => UsageInfo getExchangeUsageInfo: (exchangeId: string) => UsageInfo
onModelClick: (modelId: string) => void onModelClick: (modelId: string) => void
onExchangeClick: (exchangeId: string) => void onExchangeClick: (exchangeId: string) => void
@@ -48,9 +46,7 @@ export function ConfigStatusGrid({
visibleExchangeAddresses, visibleExchangeAddresses,
copiedId, copiedId,
language, language,
isModelInUse,
getModelUsageInfo, getModelUsageInfo,
isExchangeInUse,
getExchangeUsageInfo, getExchangeUsageInfo,
onModelClick, onModelClick,
onExchangeClick, onExchangeClick,
@@ -112,14 +108,20 @@ export function ConfigStatusGrid({
<div className="p-4 space-y-3"> <div className="p-4 space-y-3">
{configuredModels.map((model) => { {configuredModels.map((model) => {
const inUse = isModelInUse(model.id)
const usageInfo = getModelUsageInfo(model.id) const usageInfo = getModelUsageInfo(model.id)
return ( return (
<div <div
key={model.id} 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' role="button"
} bg-black/20`} 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)} 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="flex items-center gap-4">
<div className="relative"> <div className="relative">
@@ -193,16 +195,22 @@ export function ConfigStatusGrid({
<div className="p-4 space-y-3"> <div className="p-4 space-y-3">
{configuredExchanges.map((exchange) => { {configuredExchanges.map((exchange) => {
const inUse = isExchangeInUse(exchange.id)
const usageInfo = getExchangeUsageInfo(exchange.id) const usageInfo = getExchangeUsageInfo(exchange.id)
const state = exchangeAccountStates?.[exchange.id] const state = exchangeAccountStates?.[exchange.id]
const stateMeta = getExchangeStateMeta(state) const stateMeta = getExchangeStateMeta(state)
return ( return (
<div <div
key={exchange.id} 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' role="button"
} bg-black/20`} 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)} 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="flex items-center gap-4 min-w-0">
<div className="relative"> <div className="relative">

View File

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

View File

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

View File

@@ -18,24 +18,34 @@ const setupSteps = [
detail: detail:
'Your account keeps the Autopilot configuration, wallet authorization state, and trading dashboard in one place.', 'Your account keeps the Autopilot configuration, wallet authorization state, and trading dashboard in one place.',
icon: KeyRound, icon: KeyRound,
action: 'Create account',
to: ROUTES.register,
}, },
{ {
title: 'Fund the AI fee wallet', title: 'Fund the AI fee wallet',
detail: detail:
'NOFX prepares a Base USDC wallet for Claw402.ai data and model calls. This wallet is separate from trading collateral.', 'NOFX prepares a Base USDC wallet for Claw402.ai data and model calls. This wallet is separate from trading collateral.',
icon: CircleDollarSign, icon: CircleDollarSign,
action: 'Open deposit QR',
to: ROUTES.login,
returnUrl: `${ROUTES.traders}?setup=claw402`,
}, },
{ {
title: 'Authorize Hyperliquid', title: 'Authorize Hyperliquid',
detail: detail:
'Connect your trading wallet, approve the NOFX Agent, and approve the builder fee. Funds remain in your Hyperliquid account.', 'Connect your trading wallet, approve the NOFX Agent, and approve the builder fee. Funds remain in your Hyperliquid account.',
icon: Wallet, icon: Wallet,
action: 'Connect exchange',
to: ROUTES.login,
returnUrl: `${ROUTES.traders}?setup=hyperliquid`,
}, },
{ {
title: 'Deposit trading USDC', title: 'Deposit trading USDC',
detail: detail:
'Add USDC on Hyperliquid, then start NOFX Autopilot. The strategy is created and launched automatically.', 'Add USDC on Hyperliquid, then start NOFX Autopilot. The strategy is created and launched automatically.',
icon: Zap, 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"> <div className="mt-7 flex flex-col gap-3 sm:flex-row">
<Link <Link
to={ROUTES.login} 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" 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 Start setup
@@ -83,11 +99,10 @@ export function TraderLaunchGuestPage() {
<div className="grid gap-3 sm:grid-cols-2"> <div className="grid gap-3 sm:grid-cols-2">
{setupSteps.map((step, index) => { {setupSteps.map((step, index) => {
const Icon = step.icon const Icon = step.icon
return ( const cardClass =
<div '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]'
key={step.title} const content = (
className="rounded-xl border border-white/10 bg-black/24 p-4" <>
>
<div className="mb-4 flex items-center justify-between"> <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"> <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" /> <Icon className="h-4 w-4" />
@@ -102,7 +117,44 @@ export function TraderLaunchGuestPage() {
<p className="mt-2 text-sm leading-6 text-zinc-500"> <p className="mt-2 text-sm leading-6 text-zinc-500">
{step.detail} {step.detail}
</p> </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> </div>