Fix Claw402 autopilot launch and accounting

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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