feat(admin): refactor admin console to focus on superadmin tier, graduation and dispute governance
This commit is contained in:
@@ -6,6 +6,7 @@ import { WebNavbar } from '../components/WebNavbar';
|
||||
import { MarketExplore, MarketItem } from '../features/explore/MarketExplore';
|
||||
import { TradingTerminal } from '../features/trading/TradingTerminal';
|
||||
import { PortfolioView } from '../features/portfolio/PortfolioView';
|
||||
import { UserMarketWizard } from '../features/create/UserMarketWizard';
|
||||
|
||||
const DEFAULT_MARKET: MarketItem = {
|
||||
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">
|
||||
{activeTab === 'explore' && <MarketExplore onSelectMarket={handleSelectMarket} />}
|
||||
{activeTab === 'trade' && <TradingTerminal market={selectedMarket} />}
|
||||
{activeTab === 'create' && <UserMarketWizard />}
|
||||
{activeTab === 'portfolio' && <PortfolioView />}
|
||||
</main>
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React from 'react';
|
||||
import React, { useState } from 'react';
|
||||
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 {
|
||||
activeTab: string;
|
||||
@@ -8,12 +8,31 @@ interface NavbarProps {
|
||||
}
|
||||
|
||||
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 = [
|
||||
{ id: 'explore', label: '🔥 市场探索', icon: Compass },
|
||||
{ id: 'trade', label: '⚡ 交易终端', icon: TrendingUp },
|
||||
{ id: 'portfolio', label: '💼 我的持仓与账本', icon: PieChart },
|
||||
{ id: 'create', label: '🚀 创建市场', icon: PlusCircle },
|
||||
{ id: 'portfolio', label: '💼 持仓与金库', icon: PieChart },
|
||||
];
|
||||
|
||||
return (
|
||||
@@ -30,7 +49,7 @@ export const WebNavbar: React.FC<NavbarProps> = ({ activeTab, setActiveTab }) =>
|
||||
WTFX
|
||||
</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">
|
||||
POWER LDA
|
||||
TESTNET
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -43,7 +62,7 @@ export const WebNavbar: React.FC<NavbarProps> = ({ activeTab, setActiveTab }) =>
|
||||
<button
|
||||
key={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
|
||||
? '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'
|
||||
@@ -57,17 +76,28 @@ export const WebNavbar: React.FC<NavbarProps> = ({ activeTab, setActiveTab }) =>
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
{/* Right Section / Wallet Status */}
|
||||
<div className="flex items-center space-x-4">
|
||||
{/* Right Section / Faucet & Wallet */}
|
||||
<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 ? (
|
||||
<div className="flex items-center space-x-3">
|
||||
{!isCorrectNetwork ? (
|
||||
<button
|
||||
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" />
|
||||
<span>切换 Robinhood 测试网</span>
|
||||
<span>切换测试网</span>
|
||||
</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">
|
||||
|
||||
@@ -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">
|
||||
创建者即为市场初裁人 • 到期后进入 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>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user