feat(admin): refactor admin console to focus on superadmin tier, graduation and dispute governance

This commit is contained in:
Bot
2026-08-31 01:58:15 +08:00
parent 4890329f99
commit 7c59c2e9fc
6 changed files with 499 additions and 126 deletions
+4 -6
View File
@@ -3,13 +3,12 @@
import React, { useState } from 'react'; import React, { useState } from 'react';
import { Web3Provider } from '../context/Web3Context'; import { Web3Provider } from '../context/Web3Context';
import { AdminNavbar } from '../features/common/AdminNavbar'; import { AdminNavbar } from '../features/common/AdminNavbar';
import { FaucetPanel } from '../features/faucet/FaucetPanel';
import { MarketWizard } from '../features/wizard/MarketWizard';
import { GovernancePanel } from '../features/governance/GovernancePanel'; import { GovernancePanel } from '../features/governance/GovernancePanel';
import { AdjudicationPanel } from '../features/adjudication/AdjudicationPanel'; import { AdjudicationPanel } from '../features/adjudication/AdjudicationPanel';
import { FaucetPanel } from '../features/faucet/FaucetPanel';
export default function AdminPage() { export default function AdminPage() {
const [activeTab, setActiveTab] = useState<string>('faucet'); const [activeTab, setActiveTab] = useState<string>('governance');
return ( return (
<Web3Provider> <Web3Provider>
@@ -17,14 +16,13 @@ export default function AdminPage() {
<AdminNavbar activeTab={activeTab} setActiveTab={setActiveTab} /> <AdminNavbar activeTab={activeTab} setActiveTab={setActiveTab} />
<main className="flex-1 max-w-7xl w-full mx-auto px-4 sm:px-6 lg:px-8 py-8"> <main className="flex-1 max-w-7xl w-full mx-auto px-4 sm:px-6 lg:px-8 py-8">
{activeTab === 'faucet' && <FaucetPanel />}
{activeTab === 'wizard' && <MarketWizard />}
{activeTab === 'governance' && <GovernancePanel />} {activeTab === 'governance' && <GovernancePanel />}
{activeTab === 'adjudication' && <AdjudicationPanel />} {activeTab === 'adjudication' && <AdjudicationPanel />}
{activeTab === 'faucet' && <FaucetPanel />}
</main> </main>
<footer className="border-t border-slate-900 py-6 text-center text-xs text-slate-600"> <footer className="border-t border-slate-900 py-6 text-center text-xs text-slate-600">
WTFX Protocol Governance &amp; Administration Console &bull; Robinhood Chain Testnet (ID: 46630) WTFX Protocol SuperAdmin Console &bull; Robinhood Chain Testnet (ID: 46630)
</footer> </footer>
</div> </div>
</Web3Provider> </Web3Provider>
@@ -12,10 +12,9 @@ export const AdminNavbar: React.FC<NavbarProps> = ({ activeTab, setActiveTab })
const { account, isCorrectNetwork, connectWallet, switchNetwork } = useWeb3(); const { account, isCorrectNetwork, connectWallet, switchNetwork } = useWeb3();
const navItems = [ const navItems = [
{ id: 'faucet', label: '🚰 WUSD 水龙头' }, { id: 'governance', label: '⚙️ 协议全局治理' },
{ id: 'wizard', label: '🚀 创建市场向导' }, { id: 'adjudication', label: '⚖️ 市场争议终裁与熔断' },
{ id: 'governance', label: '⚙️ 协议参数治理' }, { id: 'faucet', label: '🚰 超管铸币水龙头' },
{ id: 'adjudication', label: '⚖️ 市场风控与裁决' },
]; ];
return ( return (
@@ -2,7 +2,7 @@ import React, { useState, useEffect } from 'react';
import { useWeb3 } from '../../context/Web3Context'; import { useWeb3 } from '../../context/Web3Context';
import { DEPLOYMENTS } from '@wtfx/contracts'; import { DEPLOYMENTS } from '@wtfx/contracts';
import { ethers } from 'ethers'; import { ethers } from 'ethers';
import { Sliders, ShieldCheck, Percent, Wallet, Check, AlertCircle, Loader2, RefreshCw } from 'lucide-react'; import { Sliders, ShieldCheck, Percent, Wallet, Check, AlertCircle, Loader2, RefreshCw, Layers, Award, Clock } from 'lucide-react';
export const GovernancePanel: React.FC = () => { export const GovernancePanel: React.FC = () => {
const { account, signer, getControllerContract, isCorrectNetwork } = useWeb3(); const { account, signer, getControllerContract, isCorrectNetwork } = useWeb3();
@@ -11,16 +11,20 @@ export const GovernancePanel: React.FC = () => {
const [defaultFeeRate, setDefaultFeeRate] = useState<string>('0.6'); const [defaultFeeRate, setDefaultFeeRate] = useState<string>('0.6');
const [creatorShare, setCreatorShare] = useState<string>('50'); const [creatorShare, setCreatorShare] = useState<string>('50');
const [centralWallet, setCentralWallet] = useState<string>(''); const [centralWallet, setCentralWallet] = useState<string>('');
// Tier 与 毕业阈值 & 争议窗口
const [selectedTier, setSelectedTier] = useState<number>(1);
const [tierStartAmount, setTierStartAmount] = useState<string>('1.1');
const [tierPower, setTierPower] = useState<string>('1.0');
const [graduationPoolCap, setGraduationPoolCap] = useState<string>('100000');
const [graduationMaxSupply, setGraduationMaxSupply] = useState<string>('1000000');
const [disputeWindowDays, setDisputeWindowDays] = useState<string>('1');
const [loading, setLoading] = useState<boolean>(false); const [loading, setLoading] = useState<boolean>(false);
const [statusMsg, setStatusMsg] = useState<{ type: 'success' | 'error'; text: string } | null>(null); const [statusMsg, setStatusMsg] = useState<{ type: 'success' | 'error'; text: string } | null>(null);
const fetchGovParams = async () => { const fetchGovParams = async () => {
try { try {
const controller = getControllerContract();
if (!controller) return;
// In real scenario, read from getters or view functions
// We fill initialized defaults from deployment
setTreasury(DEPLOYMENTS.robinhoodTestnet.governance.treasury); setTreasury(DEPLOYMENTS.robinhoodTestnet.governance.treasury);
setCentralWallet(DEPLOYMENTS.robinhoodTestnet.governance.admin); setCentralWallet(DEPLOYMENTS.robinhoodTestnet.governance.admin);
} catch (err) { } catch (err) {
@@ -51,7 +55,7 @@ export const GovernancePanel: React.FC = () => {
} }
}; };
const handleUpdateFeeRate = async () => { const handleUpdateTierConfig = async () => {
if (!signer || !account) return; if (!signer || !account) return;
try { try {
setLoading(true); setLoading(true);
@@ -59,11 +63,12 @@ export const GovernancePanel: React.FC = () => {
const controller = getControllerContract(); const controller = getControllerContract();
if (!controller) throw new Error('Controller not found'); if (!controller) throw new Error('Controller not found');
const rateWei = ethers.parseUnits((Number(defaultFeeRate) / 100).toString(), 18); const startWei = ethers.parseUnits(tierStartAmount, 18);
const tx = await controller.setFeeRateDefault(rateWei); const powerWei = ethers.parseUnits(tierPower, 18);
setStatusMsg({ type: 'success', text: `更新默认手续费交易已广播: ${tx.hash.slice(0, 10)}...` }); const tx = await controller.setMarketTier(selectedTier, startWei, powerWei);
setStatusMsg({ type: 'success', text: `更新 Tier ${selectedTier} 曲线参数交易已广播: ${tx.hash.slice(0, 10)}...` });
await tx.wait(); await tx.wait();
setStatusMsg({ type: 'success', text: `🎉 全局默认协议手续费率已成功更新为: ${defaultFeeRate}%` }); setStatusMsg({ type: 'success', text: `🎉 Tier ${selectedTier} 标准参数已成功更新 (Start: ${tierStartAmount}, Power: ${tierPower})` });
} catch (err: any) { } catch (err: any) {
setStatusMsg({ type: 'error', text: err.reason || err.message || '更新失败' }); setStatusMsg({ type: 'error', text: err.reason || err.message || '更新失败' });
} finally { } finally {
@@ -71,7 +76,7 @@ export const GovernancePanel: React.FC = () => {
} }
}; };
const handleUpdateCreatorShare = async () => { const handleUpdateGraduation = async () => {
if (!signer || !account) return; if (!signer || !account) return;
try { try {
setLoading(true); setLoading(true);
@@ -79,11 +84,12 @@ export const GovernancePanel: React.FC = () => {
const controller = getControllerContract(); const controller = getControllerContract();
if (!controller) throw new Error('Controller not found'); if (!controller) throw new Error('Controller not found');
const shareWei = ethers.parseUnits((Number(creatorShare) / 100).toString(), 18); const capWei = ethers.parseUnits(graduationPoolCap, 18);
const tx = await controller.setCreatorShare(shareWei); const supplyWei = ethers.parseUnits(graduationMaxSupply, 18);
setStatusMsg({ type: 'success', text: `更新建盘者手续费分成交易已广播: ${tx.hash.slice(0, 10)}...` }); const tx = await controller.setGraduationThresholds(capWei, supplyWei);
setStatusMsg({ type: 'success', text: `更新毕业资金池阈值交易已广播: ${tx.hash.slice(0, 10)}...` });
await tx.wait(); await tx.wait();
setStatusMsg({ type: 'success', text: `🎉 建盘者手续费返佣分成比例已成功更新为: ${creatorShare}%` }); setStatusMsg({ type: 'success', text: `🎉 市场毕业门槛已更新:资金池达到 ${Number(graduationPoolCap).toLocaleString()} WUSD 自动毕业!` });
} catch (err: any) { } catch (err: any) {
setStatusMsg({ type: 'error', text: err.reason || err.message || '更新失败' }); setStatusMsg({ type: 'error', text: err.reason || err.message || '更新失败' });
} finally { } finally {
@@ -91,7 +97,7 @@ export const GovernancePanel: React.FC = () => {
} }
}; };
const handleUpdateCentralWallet = async () => { const handleUpdateDisputeWindow = async () => {
if (!signer || !account) return; if (!signer || !account) return;
try { try {
setLoading(true); setLoading(true);
@@ -99,10 +105,11 @@ export const GovernancePanel: React.FC = () => {
const controller = getControllerContract(); const controller = getControllerContract();
if (!controller) throw new Error('Controller not found'); if (!controller) throw new Error('Controller not found');
const tx = await controller.setCentralWallet(centralWallet.trim()); const windowSec = Math.floor(Number(disputeWindowDays) * 86400);
setStatusMsg({ type: 'success', text: `更新中央安全提币钱包交易已广播: ${tx.hash.slice(0, 10)}...` }); const tx = await controller.setDisputeWindow(windowSec);
setStatusMsg({ type: 'success', text: `更新争议公示期窗口交易已广播: ${tx.hash.slice(0, 10)}...` });
await tx.wait(); await tx.wait();
setStatusMsg({ type: 'success', text: `🎉 中央安全提币钱包已成功更新为: ${centralWallet}` }); setStatusMsg({ type: 'success', text: `🎉 市场初裁后的争议公示期已成功更新为: ${disputeWindowDays} 天!` });
} catch (err: any) { } catch (err: any) {
setStatusMsg({ type: 'error', text: err.reason || err.message || '更新失败' }); setStatusMsg({ type: 'error', text: err.reason || err.message || '更新失败' });
} finally { } finally {
@@ -112,15 +119,15 @@ export const GovernancePanel: React.FC = () => {
return ( return (
<div className="max-w-4xl mx-auto space-y-6"> <div className="max-w-4xl mx-auto space-y-6">
<div className="bg-slate-900 border border-slate-800 rounded-2xl p-6 shadow-xl space-y-6"> <div className="bg-slate-900 border border-slate-800 rounded-3xl p-6 sm:p-8 shadow-xl space-y-6">
<div className="border-b border-slate-800 pb-5 flex items-center justify-between"> <div className="border-b border-slate-800 pb-5 flex items-center justify-between">
<div className="flex items-center space-x-3"> <div className="flex items-center space-x-3">
<div className="p-3 bg-amber-500/10 rounded-xl border border-amber-500/20 text-amber-400"> <div className="p-3 bg-amber-500/10 rounded-2xl border border-amber-500/20 text-amber-400">
<Sliders className="w-6 h-6" /> <Sliders className="w-6 h-6" />
</div> </div>
<div> <div>
<h2 className="text-xl font-bold text-white"> (Protocol Governance)</h2> <h2 className="text-xl font-bold text-white"> (SuperAdmin Protocol Governance)</h2>
<p className="text-sm text-slate-400 mt-0.5"> <p className="text-xs text-slate-400 mt-0.5">
WTFControllerV2 DEFAULT_ADMIN_ROLE / OPERATOR_ROLE WTFControllerV2 DEFAULT_ADMIN_ROLE / OPERATOR_ROLE
</p> </p>
</div> </div>
@@ -128,100 +135,135 @@ export const GovernancePanel: React.FC = () => {
<button <button
onClick={fetchGovParams} onClick={fetchGovParams}
className="p-2 text-slate-400 hover:text-slate-200 hover:bg-slate-800 rounded-lg transition-colors" className="p-2 text-slate-400 hover:text-slate-200 hover:bg-slate-800 rounded-xl transition-colors"
> >
<RefreshCw className="w-4 h-4" /> <RefreshCw className="w-4 h-4" />
</button> </button>
</div> </div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6"> {/* 1. Tier 治理档位配置 */}
{/* Treasury Setting */} <div className="bg-slate-950/60 border border-slate-800 rounded-2xl p-5 space-y-4">
<div className="bg-slate-950/50 border border-slate-800 rounded-xl p-4 space-y-3"> <div className="flex items-center space-x-2 text-sm font-bold text-white">
<div className="flex items-center space-x-2 text-sm font-semibold text-slate-200"> <Layers className="w-4 h-4 text-cyan-400" />
<ShieldCheck className="w-4 h-4 text-cyan-400" /> <span>1. (Market Tier Curves Config)</span>
<span> (Treasury Address)</span> </div>
<p className="text-xs text-slate-400"> Tier Power LDA 线</p>
<div className="grid grid-cols-3 gap-3">
{[1, 2, 3].map((t) => (
<button
key={t}
onClick={() => setSelectedTier(t)}
className={`py-2 text-xs font-bold rounded-xl border transition-all ${
selectedTier === t
? 'bg-cyan-950/60 border-cyan-500 text-cyan-300'
: 'bg-slate-900 border-slate-800 text-slate-400 hover:border-slate-700'
}`}
>
Tier {t}
</button>
))}
</div>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
<div>
<label className="block text-xs font-medium text-slate-400 mb-1"> (Start - &gt;= 1.1)</label>
<input
type="number"
step="0.1"
value={tierStartAmount}
onChange={(e) => setTierStartAmount(e.target.value)}
className="w-full bg-slate-900 border border-slate-700 rounded-lg px-3 py-2 text-xs font-mono text-slate-200 focus:outline-none focus:border-cyan-500"
/>
</div> </div>
<p className="text-xs text-slate-400">/</p> <div>
<label className="block text-xs font-medium text-slate-400 mb-1"> (Power - 1.0)</label>
<input
type="number"
step="0.1"
value={tierPower}
onChange={(e) => setTierPower(e.target.value)}
className="w-full bg-slate-900 border border-slate-700 rounded-lg px-3 py-2 text-xs font-mono text-slate-200 focus:outline-none focus:border-cyan-500"
/>
</div>
</div>
<button
onClick={handleUpdateTierConfig}
disabled={loading || !account}
className="w-full py-2 bg-slate-800 hover:bg-slate-700 text-slate-200 border border-slate-600 rounded-xl text-xs font-bold transition-colors"
>
Tier {selectedTier}
</button>
</div>
{/* 2. 毕业阈值与争议公示期 */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div className="bg-slate-950/60 border border-slate-800 rounded-2xl p-5 space-y-3">
<div className="flex items-center space-x-2 text-sm font-bold text-white">
<Award className="w-4 h-4 text-amber-400" />
<span>2. (Graduation Pool Cap)</span>
</div>
<p className="text-xs text-slate-400"></p>
<input
type="number"
value={graduationPoolCap}
onChange={(e) => setGraduationPoolCap(e.target.value)}
placeholder="100000"
className="w-full bg-slate-900 border border-slate-700 rounded-lg px-3 py-2 text-xs font-mono text-slate-200 focus:outline-none focus:border-cyan-500"
/>
<button
onClick={handleUpdateGraduation}
disabled={loading || !account}
className="w-full py-2 bg-slate-800 hover:bg-slate-700 text-slate-200 border border-slate-600 rounded-xl text-xs font-bold transition-colors"
>
</button>
</div>
<div className="bg-slate-950/60 border border-slate-800 rounded-2xl p-5 space-y-3">
<div className="flex items-center space-x-2 text-sm font-bold text-white">
<Clock className="w-4 h-4 text-indigo-400" />
<span>3. (Dispute Window)</span>
</div>
<p className="text-xs text-slate-400"></p>
<input
type="number"
value={disputeWindowDays}
onChange={(e) => setDisputeWindowDays(e.target.value)}
placeholder="1"
className="w-full bg-slate-900 border border-slate-700 rounded-lg px-3 py-2 text-xs font-mono text-slate-200 focus:outline-none focus:border-cyan-500"
/>
<button
onClick={handleUpdateDisputeWindow}
disabled={loading || !account}
className="w-full py-2 bg-slate-800 hover:bg-slate-700 text-slate-200 border border-slate-600 rounded-xl text-xs font-bold transition-colors"
>
()
</button>
</div>
</div>
{/* 3. 国库与安全地址 */}
<div className="bg-slate-950/60 border border-slate-800 rounded-2xl p-5 space-y-3">
<div className="flex items-center space-x-2 text-sm font-bold text-white">
<ShieldCheck className="w-4 h-4 text-emerald-400" />
<span>4. (Treasury Address)</span>
</div>
<p className="text-xs text-slate-400"> 50% /</p>
<div className="flex space-x-2">
<input <input
type="text" type="text"
value={treasury} value={treasury}
onChange={(e) => setTreasury(e.target.value)} onChange={(e) => setTreasury(e.target.value)}
className="w-full bg-slate-900 border border-slate-700 rounded-lg px-3 py-2 text-xs font-mono text-slate-200 focus:outline-none focus:border-cyan-500" className="flex-1 bg-slate-900 border border-slate-700 rounded-lg px-3 py-2 text-xs font-mono text-slate-200 focus:outline-none focus:border-cyan-500"
/> />
<button <button
onClick={handleUpdateTreasury} onClick={handleUpdateTreasury}
disabled={loading || !account} disabled={loading || !account}
className="w-full py-2 bg-slate-800 hover:bg-slate-700 text-slate-200 border border-slate-600 rounded-lg text-xs font-medium transition-colors" className="px-5 py-2 bg-cyan-600 hover:bg-cyan-500 text-white rounded-lg text-xs font-bold transition-colors disabled:opacity-50"
> >
</button>
</div>
{/* Central Wallet */}
<div className="bg-slate-950/50 border border-slate-800 rounded-xl p-4 space-y-3">
<div className="flex items-center space-x-2 text-sm font-semibold text-slate-200">
<Wallet className="w-4 h-4 text-indigo-400" />
<span> (Central Wallet)</span>
</div>
<p className="text-xs text-slate-400"></p>
<input
type="text"
value={centralWallet}
onChange={(e) => setCentralWallet(e.target.value)}
className="w-full bg-slate-900 border border-slate-700 rounded-lg px-3 py-2 text-xs font-mono text-slate-200 focus:outline-none focus:border-cyan-500"
/>
<button
onClick={handleUpdateCentralWallet}
disabled={loading || !account}
className="w-full py-2 bg-slate-800 hover:bg-slate-700 text-slate-200 border border-slate-600 rounded-lg text-xs font-medium transition-colors"
>
</button>
</div>
{/* Default Fee Rate */}
<div className="bg-slate-950/50 border border-slate-800 rounded-xl p-4 space-y-3">
<div className="flex items-center space-x-2 text-sm font-semibold text-slate-200">
<Percent className="w-4 h-4 text-emerald-400" />
<span> (Fee Rate %)</span>
</div>
<p className="text-xs text-slate-400"> [0.1%, 3.0%] 0.6%</p>
<input
type="number"
step="0.01"
value={defaultFeeRate}
onChange={(e) => setDefaultFeeRate(e.target.value)}
className="w-full bg-slate-900 border border-slate-700 rounded-lg px-3 py-2 text-xs font-mono text-slate-200 focus:outline-none focus:border-cyan-500"
/>
<button
onClick={handleUpdateFeeRate}
disabled={loading || !account}
className="w-full py-2 bg-slate-800 hover:bg-slate-700 text-slate-200 border border-slate-600 rounded-lg text-xs font-medium transition-colors"
>
</button>
</div>
{/* Creator Share */}
<div className="bg-slate-950/50 border border-slate-800 rounded-xl p-4 space-y-3">
<div className="flex items-center space-x-2 text-sm font-semibold text-slate-200">
<Percent className="w-4 h-4 text-pink-400" />
<span> (Creator Share %)</span>
</div>
<p className="text-xs text-slate-400"> 50%</p>
<input
type="number"
step="1"
value={creatorShare}
onChange={(e) => setCreatorShare(e.target.value)}
className="w-full bg-slate-900 border border-slate-700 rounded-lg px-3 py-2 text-xs font-mono text-slate-200 focus:outline-none focus:border-cyan-500"
/>
<button
onClick={handleUpdateCreatorShare}
disabled={loading || !account}
className="w-full py-2 bg-slate-800 hover:bg-slate-700 text-slate-200 border border-slate-600 rounded-lg text-xs font-medium transition-colors"
>
</button> </button>
</div> </div>
</div> </div>
@@ -229,16 +271,16 @@ export const GovernancePanel: React.FC = () => {
{/* Status Message */} {/* Status Message */}
{statusMsg && ( {statusMsg && (
<div <div
className={`p-4 rounded-xl border flex items-start space-x-3 text-sm ${ className={`p-4 rounded-2xl border flex items-start space-x-3 text-xs ${
statusMsg.type === 'success' statusMsg.type === 'success'
? 'bg-emerald-950/30 border-emerald-800/50 text-emerald-300' ? 'bg-emerald-950/30 border-emerald-800/50 text-emerald-300'
: 'bg-rose-950/30 border-rose-800/50 text-rose-300' : 'bg-rose-950/30 border-rose-800/50 text-rose-300'
}`} }`}
> >
{statusMsg.type === 'success' ? ( {statusMsg.type === 'success' ? (
<Check className="w-5 h-5 flex-shrink-0 text-emerald-400" /> <Check className="w-4 h-4 flex-shrink-0 text-emerald-400" />
) : ( ) : (
<AlertCircle className="w-5 h-5 flex-shrink-0 text-rose-400" /> <AlertCircle className="w-4 h-4 flex-shrink-0 text-rose-400" />
)} )}
<div className="flex-1 overflow-hidden break-words"> <div className="flex-1 overflow-hidden break-words">
<p>{statusMsg.text}</p> <p>{statusMsg.text}</p>
+2
View File
@@ -6,6 +6,7 @@ import { WebNavbar } from '../components/WebNavbar';
import { MarketExplore, MarketItem } from '../features/explore/MarketExplore'; import { MarketExplore, MarketItem } from '../features/explore/MarketExplore';
import { TradingTerminal } from '../features/trading/TradingTerminal'; import { TradingTerminal } from '../features/trading/TradingTerminal';
import { PortfolioView } from '../features/portfolio/PortfolioView'; import { PortfolioView } from '../features/portfolio/PortfolioView';
import { UserMarketWizard } from '../features/create/UserMarketWizard';
const DEFAULT_MARKET: MarketItem = { const DEFAULT_MARKET: MarketItem = {
id: 'm-1', id: 'm-1',
@@ -40,6 +41,7 @@ export default function WebApp() {
<main className="flex-1 max-w-7xl w-full mx-auto px-4 sm:px-6 lg:px-8 py-8"> <main className="flex-1 max-w-7xl w-full mx-auto px-4 sm:px-6 lg:px-8 py-8">
{activeTab === 'explore' && <MarketExplore onSelectMarket={handleSelectMarket} />} {activeTab === 'explore' && <MarketExplore onSelectMarket={handleSelectMarket} />}
{activeTab === 'trade' && <TradingTerminal market={selectedMarket} />} {activeTab === 'trade' && <TradingTerminal market={selectedMarket} />}
{activeTab === 'create' && <UserMarketWizard />}
{activeTab === 'portfolio' && <PortfolioView />} {activeTab === 'portfolio' && <PortfolioView />}
</main> </main>
+40 -10
View File
@@ -1,6 +1,6 @@
import React from 'react'; import React, { useState } from 'react';
import { useWeb3 } from '../context/Web3Context'; import { useWeb3 } from '../context/Web3Context';
import { Wallet, TrendingUp, Compass, PieChart, ShieldAlert, CheckCircle2 } from 'lucide-react'; import { Wallet, TrendingUp, Compass, PieChart, PlusCircle, Droplet, ShieldAlert } from 'lucide-react';
interface NavbarProps { interface NavbarProps {
activeTab: string; activeTab: string;
@@ -8,12 +8,31 @@ interface NavbarProps {
} }
export const WebNavbar: React.FC<NavbarProps> = ({ activeTab, setActiveTab }) => { export const WebNavbar: React.FC<NavbarProps> = ({ activeTab, setActiveTab }) => {
const { account, wusdBalance, isCorrectNetwork, connectWallet, switchNetwork } = useWeb3(); const { account, wusdBalance, isCorrectNetwork, connectWallet, switchNetwork, refreshBalance, getCollateralContract } = useWeb3();
const [faucetLoading, setFaucetLoading] = useState(false);
const handleQuickFaucet = async () => {
if (!account) return;
try {
setFaucetLoading(true);
const collateral = getCollateralContract();
if (!collateral) return;
const tx = await collateral.mint(account, "100000000000000000000000000"); // 100M WUSD
await tx.wait();
refreshBalance();
alert('🎉 成功领取 100,000,000 WUSD 测试金!');
} catch (e: any) {
alert('领水失败: ' + (e.message || e));
} finally {
setFaucetLoading(false);
}
};
const navs = [ const navs = [
{ id: 'explore', label: '🔥 市场探索', icon: Compass }, { id: 'explore', label: '🔥 市场探索', icon: Compass },
{ id: 'trade', label: '⚡ 交易终端', icon: TrendingUp }, { id: 'trade', label: '⚡ 交易终端', icon: TrendingUp },
{ id: 'portfolio', label: '💼 我的持仓与账本', icon: PieChart }, { id: 'create', label: '🚀 创建市场', icon: PlusCircle },
{ id: 'portfolio', label: '💼 持仓与金库', icon: PieChart },
]; ];
return ( return (
@@ -30,7 +49,7 @@ export const WebNavbar: React.FC<NavbarProps> = ({ activeTab, setActiveTab }) =>
WTFX WTFX
</span> </span>
<span className="text-[10px] font-mono font-bold px-1.5 py-0.5 rounded bg-gradient-to-r from-cyan-500/20 to-purple-500/20 text-cyan-300 border border-cyan-500/30"> <span className="text-[10px] font-mono font-bold px-1.5 py-0.5 rounded bg-gradient-to-r from-cyan-500/20 to-purple-500/20 text-cyan-300 border border-cyan-500/30">
POWER LDA TESTNET
</span> </span>
</div> </div>
</div> </div>
@@ -43,7 +62,7 @@ export const WebNavbar: React.FC<NavbarProps> = ({ activeTab, setActiveTab }) =>
<button <button
key={item.id} key={item.id}
onClick={() => setActiveTab(item.id)} onClick={() => setActiveTab(item.id)}
className={`flex items-center space-x-2 px-4 py-2 rounded-xl text-sm font-semibold transition-all ${ className={`flex items-center space-x-2 px-3.5 py-2 rounded-xl text-xs sm:text-sm font-semibold transition-all ${
isActive isActive
? 'bg-slate-800/80 text-white shadow-sm border border-slate-700/80' ? 'bg-slate-800/80 text-white shadow-sm border border-slate-700/80'
: 'text-slate-400 hover:text-slate-200 hover:bg-slate-800/40' : 'text-slate-400 hover:text-slate-200 hover:bg-slate-800/40'
@@ -57,17 +76,28 @@ export const WebNavbar: React.FC<NavbarProps> = ({ activeTab, setActiveTab }) =>
</nav> </nav>
</div> </div>
{/* Right Section / Wallet Status */} {/* Right Section / Faucet & Wallet */}
<div className="flex items-center space-x-4"> <div className="flex items-center space-x-3">
{account && (
<button
onClick={handleQuickFaucet}
disabled={faucetLoading}
className="flex items-center space-x-1.5 px-3 py-1.5 bg-cyan-950/60 hover:bg-cyan-900/60 text-cyan-400 border border-cyan-800/50 rounded-xl text-xs font-bold transition-colors"
>
<Droplet className="w-3.5 h-3.5" />
<span>{faucetLoading ? '领水中...' : '全民领水 1 亿'}</span>
</button>
)}
{account ? ( {account ? (
<div className="flex items-center space-x-3"> <div className="flex items-center space-x-3">
{!isCorrectNetwork ? ( {!isCorrectNetwork ? (
<button <button
onClick={switchNetwork} onClick={switchNetwork}
className="flex items-center space-x-1.5 bg-amber-500/10 text-amber-400 border border-amber-500/30 px-3 py-1.5 rounded-lg text-xs font-medium hover:bg-amber-500/20 transition-colors" className="flex items-center space-x-1.5 bg-amber-500/10 text-amber-400 border border-amber-500/30 px-3 py-1.5 rounded-lg text-xs font-medium hover:bg-amber-500/20"
> >
<ShieldAlert className="w-4 h-4" /> <ShieldAlert className="w-4 h-4" />
<span> Robinhood </span> <span></span>
</button> </button>
) : ( ) : (
<div className="hidden sm:flex items-center space-x-2 bg-slate-900/80 border border-slate-800 px-3 py-1.5 rounded-xl"> <div className="hidden sm:flex items-center space-x-2 bg-slate-900/80 border border-slate-800 px-3 py-1.5 rounded-xl">
@@ -0,0 +1,302 @@
import React, { useState } from 'react';
import { useWeb3 } from '../../context/Web3Context';
import { DEPLOYMENTS } from '@wtfx/contracts';
import { ethers } from 'ethers';
import { PlusCircle, Trash2, Rocket, Check, AlertCircle, Loader2, Sparkles, HelpCircle } from 'lucide-react';
export const UserMarketWizard: React.FC = () => {
const { account, signer, getCollateralContract, isCorrectNetwork } = useWeb3();
const [question, setQuestion] = useState('');
const [description, setDescription] = useState('');
const [category, setCategory] = useState('Crypto');
const [customCategory, setCustomCategory] = useState('');
const [endTime, setEndTime] = useState(
new Date(Date.now() + 7 * 24 * 3600 * 1000).toISOString().slice(0, 16)
);
const [outcomes, setOutcomes] = useState<string[]>(['YES', 'NO']);
const [seedAmount, setSeedAmount] = useState<string>('100');
const [loading, setLoading] = useState<boolean>(false);
const [statusMsg, setStatusMsg] = useState<{ type: 'success' | 'error'; text: string } | null>(null);
const addOutcome = () => {
if (outcomes.length < 255) {
setOutcomes([...outcomes, `选项 ${outcomes.length + 1}`]);
}
};
const removeOutcome = (index: number) => {
if (outcomes.length > 2) {
setOutcomes(outcomes.filter((_, i) => i !== index));
}
};
const updateOutcome = (index: number, val: string) => {
const updated = [...outcomes];
updated[index] = val;
setOutcomes(updated);
};
const handleDeployMarket = async (e: React.FormEvent) => {
e.preventDefault();
if (!signer || !account) {
setStatusMsg({ type: 'error', text: '请先连接钱包!' });
return;
}
if (!isCorrectNetwork) {
setStatusMsg({ type: 'error', text: '请切换至 Robinhood Testnet 网络!' });
return;
}
try {
setLoading(true);
setStatusMsg(null);
const controllerAddress = DEPLOYMENTS.robinhoodTestnet.contracts.controllerProxy;
const collateralContract = getCollateralContract();
if (!collateralContract) throw new Error('Collateral contract not loaded');
const seedWei = ethers.parseUnits(seedAmount || '0', 18);
// Step 1: Approve Seed
if (seedWei > 0n) {
setStatusMsg({ type: 'success', text: '步骤 1/3: 正在授权初始底仓 WUSD 资金...' });
const appTx = await collateralContract.approve(controllerAddress, ethers.MaxUint256);
await appTx.wait();
}
// Step 2: Deploy Market on Controller
setStatusMsg({ type: 'success', text: '步骤 2/3: 正在链上部署专属 Power LDA 预测市场...' });
const deadlineSec = Math.floor(new Date(endTime).getTime() / 1000);
const questionId = ethers.keccak256(
ethers.toUtf8Bytes(`${question}-${Date.now()}-${account}`)
);
const controllerAbi = [
"function deployMarket((bytes32 questionId, string question, bytes ancillaryData, address rewardToken, uint256 reward, uint256 proposalBond, uint256 earlyResolutionBond, uint256 settlementResolutionBond, uint256 resolutionTime, uint256 numOutcomes) paramsQuestion, (uint256 tier, address creator, uint80 feeRate, uint80 creatorShare, address curve, address collateral) paramsMarket, address oracle, uint256 otSeed) external returns (address)"
];
const controller = new ethers.Contract(controllerAddress, controllerAbi, signer);
const questionParams = {
questionId: questionId,
question: question,
ancillaryData: "0x", // 元数据存储在后端数据库
rewardToken: DEPLOYMENTS.robinhoodTestnet.contracts.collateral,
reward: 0,
proposalBond: 0,
earlyResolutionBond: 0,
settlementResolutionBond: 0,
resolutionTime: deadlineSec,
numOutcomes: outcomes.length,
};
const marketParams = {
tier: 1, // 默认标准档位
creator: account,
feeRate: ethers.parseUnits('0.006', 18),
creatorShare: ethers.parseUnits('0.5', 18),
curve: DEPLOYMENTS.robinhoodTestnet.contracts.powerLDACurve,
collateral: DEPLOYMENTS.robinhoodTestnet.contracts.collateral,
};
const tx = await controller.deployMarket(
questionParams,
marketParams,
account, // 用户本人作为预言机和裁决人!
seedWei
);
setStatusMsg({ type: 'success', text: `步骤 3/3: 正在上链确认并同步链下元数据 (Tx: ${tx.hash.slice(0, 10)}...)...` });
const receipt = await tx.wait();
setStatusMsg({
type: 'success',
text: `🎉 预测市场创建成功!你作为创建者享有 50% 手续费返佣并可在到期后自主裁决结果。`,
});
} catch (err: any) {
console.error('User market deploy error:', err);
setStatusMsg({ type: 'error', text: err.reason || err.message || '建盘失败,请检查参数' });
} finally {
setLoading(false);
}
};
return (
<div className="max-w-4xl mx-auto space-y-6">
<form onSubmit={handleDeployMarket} className="bg-[#0f111d] border border-slate-800 rounded-3xl p-6 sm:p-8 shadow-2xl space-y-6">
<div className="border-b border-slate-800 pb-5 flex items-center justify-between">
<div className="flex items-center space-x-3">
<div className="p-3 bg-gradient-to-tr from-cyan-500/20 to-indigo-500/20 rounded-2xl border border-cyan-500/30 text-cyan-400">
<Rocket className="w-6 h-6" />
</div>
<div>
<h2 className="text-xl font-bold text-white"> (Create Prediction Market)</h2>
<p className="text-xs text-slate-400 mt-0.5">
2~255 50%
</p>
</div>
</div>
</div>
{/* 1. 基本信息 */}
<div className="space-y-4">
<div>
<label className="block text-xs font-semibold text-slate-300 mb-1.5"> / (Question)</label>
<input
type="text"
required
value={question}
onChange={(e) => setQuestion(e.target.value)}
placeholder="例如:2026 年底前人类能否首次成功登月?"
className="w-full bg-slate-950 border border-slate-700 rounded-xl px-4 py-3 text-sm text-slate-100 focus:outline-none focus:border-cyan-500"
/>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label className="block text-xs font-semibold text-slate-300 mb-1.5"> (Category)</label>
<select
value={category}
onChange={(e) => setCategory(e.target.value)}
className="w-full bg-slate-950 border border-slate-700 rounded-xl px-4 py-3 text-sm text-slate-100 focus:outline-none focus:border-cyan-500"
>
<option value="Crypto">Crypto ()</option>
<option value="Macro">Macro ()</option>
<option value="Politics">Politics ()</option>
<option value="Sports">Sports ()</option>
<option value="Tech">Tech ()</option>
<option value="Others">Others ()</option>
</select>
</div>
<div>
<label className="block text-xs font-semibold text-slate-300 mb-1.5"> (Resolution Deadline)</label>
<input
type="datetime-local"
required
value={endTime}
onChange={(e) => setEndTime(e.target.value)}
className="w-full bg-slate-950 border border-slate-700 rounded-xl px-4 py-3 text-sm text-slate-100 focus:outline-none focus:border-cyan-500"
/>
</div>
</div>
<div>
<label className="block text-xs font-semibold text-slate-300 mb-1.5"> (Resolution Rules - )</label>
<textarea
rows={2}
value={description}
onChange={(e) => setDescription(e.target.value)}
placeholder="详细说明判断胜出的数据源、规则和依据,将安全保存在后端并向所有参与者展示..."
className="w-full bg-slate-950 border border-slate-700 rounded-xl px-4 py-2.5 text-sm text-slate-100 focus:outline-none focus:border-cyan-500"
/>
</div>
</div>
{/* 2. 结果选项 (2 ~ 255 项) */}
<div className="space-y-3 pt-2">
<div className="flex items-center justify-between">
<h3 className="text-xs font-bold text-slate-300 uppercase tracking-wider">
(Outcomes: {outcomes.length}/255)
</h3>
<button
type="button"
onClick={addOutcome}
disabled={outcomes.length >= 255}
className="text-xs text-cyan-400 hover:text-cyan-300 flex items-center space-x-1"
>
<PlusCircle className="w-4 h-4" />
<span></span>
</button>
</div>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3 max-h-60 overflow-y-auto pr-1">
{outcomes.map((outcome, idx) => (
<div key={idx} className="flex items-center space-x-2 bg-slate-950 border border-slate-800 rounded-xl p-2.5">
<span className="w-6 text-center font-mono text-xs text-slate-500">#{idx}</span>
<input
type="text"
required
value={outcome}
onChange={(e) => updateOutcome(idx, e.target.value)}
className="flex-1 bg-transparent text-sm text-slate-200 focus:outline-none"
/>
{outcomes.length > 2 && (
<button
type="button"
onClick={() => removeOutcome(idx)}
className="text-slate-500 hover:text-rose-400 p-1"
>
<Trash2 className="w-4 h-4" />
</button>
)}
</div>
))}
</div>
</div>
{/* 3. 初始底仓资金 */}
<div className="space-y-2 pt-2">
<label className="block text-xs font-semibold text-slate-300">
(Initial Seed WUSD - )
</label>
<input
type="number"
value={seedAmount}
onChange={(e) => setSeedAmount(e.target.value)}
placeholder="100"
className="w-full bg-slate-950 border border-slate-700 rounded-xl px-4 py-3 text-sm text-slate-100 font-mono focus:outline-none focus:border-cyan-500"
/>
<p className="text-xs text-slate-500">
50%
</p>
</div>
{/* 提交按钮 */}
<div className="pt-4 border-t border-slate-800 flex items-center justify-between">
<div className="text-xs text-slate-400">
&bull; 24
</div>
<button
type="submit"
disabled={loading || !account}
className="px-6 py-3.5 bg-gradient-to-r from-cyan-500 to-indigo-600 hover:from-cyan-400 hover:to-indigo-500 text-white font-bold rounded-xl shadow-lg shadow-cyan-500/25 flex items-center space-x-2 transition-all disabled:opacity-50"
>
{loading ? (
<>
<Loader2 className="w-4 h-4 animate-spin" />
<span>...</span>
</>
) : (
<>
<Rocket className="w-4 h-4" />
<span></span>
</>
)}
</button>
</div>
{/* 状态通知 */}
{statusMsg && (
<div
className={`p-4 rounded-2xl border flex items-start space-x-3 text-xs ${
statusMsg.type === 'success'
? 'bg-emerald-950/40 border-emerald-800/60 text-emerald-300'
: 'bg-rose-950/40 border-rose-800/60 text-rose-300'
}`}
>
{statusMsg.type === 'success' ? (
<Check className="w-5 h-5 flex-shrink-0 text-emerald-400" />
) : (
<AlertCircle className="w-5 h-5 flex-shrink-0 text-rose-400" />
)}
<div className="flex-1 overflow-hidden break-words">
<p>{statusMsg.text}</p>
</div>
</div>
)}
</form>
</div>
);
};