mirror of
https://github.com/NoFxAiOS/nofx.git
synced 2026-07-15 00:36:56 +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
|
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,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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"`
|
||||||
|
|||||||
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 { 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}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -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"
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
@@ -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('')
|
||||||
|
|||||||
@@ -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>
|
||||||
)
|
)
|
||||||
})}
|
})}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user