mirror of
https://github.com/NoFxAiOS/nofx.git
synced 2026-07-03 02:50:59 +08:00
Fix Claw402 autopilot launch and accounting
This commit is contained in:
@@ -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,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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"`
|
||||
|
||||
48
trader/hyperliquid/trader_account_test.go
Normal file
48
trader/hyperliquid/trader_account_test.go
Normal 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)
|
||||
}
|
||||
}
|
||||
@@ -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}
|
||||
/>
|
||||
|
||||
@@ -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"
|
||||
>
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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('')
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
})}
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user