feat(admin): complete admin console with 100M WUSD faucet, market wizard, governance and adjudication
This commit is contained in:
Vendored
+5
@@ -0,0 +1,5 @@
|
||||
/// <reference types="next" />
|
||||
/// <reference types="next/image-types/global" />
|
||||
|
||||
// NOTE: This file should not be edited
|
||||
// see https://nextjs.org/docs/app/building-your-application/configuring/typescript for more information.
|
||||
+15
-2
@@ -10,10 +10,23 @@
|
||||
"dependencies": {
|
||||
"@wtfx/types": "workspace:*",
|
||||
"@wtfx/api-client": "workspace:*",
|
||||
"@wtfx/contracts": "workspace:*",
|
||||
"@wtfx/ui": "workspace:*",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"next": "^14.2.4",
|
||||
"ethers": "^6.13.0"
|
||||
"ethers": "^6.13.0",
|
||||
"lucide-react": "^0.395.0",
|
||||
"clsx": "^2.1.1",
|
||||
"tailwind-merge": "^2.3.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^20.14.2",
|
||||
"@types/react": "^18.3.3",
|
||||
"@types/react-dom": "^18.3.0",
|
||||
"autoprefixer": "^10.4.19",
|
||||
"postcss": "^8.4.38",
|
||||
"tailwindcss": "^3.4.4",
|
||||
"typescript": "^5.4.5"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
module.exports = {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,9 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
body {
|
||||
background-color: #090A0F;
|
||||
color: #F3F4F6;
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
import type { Metadata } from 'next';
|
||||
import './globals.css';
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: 'WTFX Admin Console',
|
||||
description: 'Protocol Governance, Contract Deployment & Market Risk Control',
|
||||
};
|
||||
|
||||
export default function RootLayout({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<html lang="en" className="dark">
|
||||
<body className="min-h-screen bg-background text-gray-100 flex flex-col">
|
||||
{children}
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
'use client';
|
||||
|
||||
import React, { useState } from 'react';
|
||||
import { Web3Provider } from '../context/Web3Context';
|
||||
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 { AdjudicationPanel } from '../features/adjudication/AdjudicationPanel';
|
||||
|
||||
export default function AdminPage() {
|
||||
const [activeTab, setActiveTab] = useState<string>('faucet');
|
||||
|
||||
return (
|
||||
<Web3Provider>
|
||||
<div className="min-h-screen bg-slate-950 text-slate-100 flex flex-col selection:bg-cyan-500 selection:text-white">
|
||||
<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">
|
||||
{activeTab === 'faucet' && <FaucetPanel />}
|
||||
{activeTab === 'wizard' && <MarketWizard />}
|
||||
{activeTab === 'governance' && <GovernancePanel />}
|
||||
{activeTab === 'adjudication' && <AdjudicationPanel />}
|
||||
</main>
|
||||
|
||||
<footer className="border-t border-slate-900 py-6 text-center text-xs text-slate-600">
|
||||
WTFX Protocol Governance & Administration Console • Robinhood Chain Testnet (ID: 46630)
|
||||
</footer>
|
||||
</div>
|
||||
</Web3Provider>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,157 @@
|
||||
import React, { createContext, useContext, useState, useEffect } from 'react';
|
||||
import { ethers } from 'ethers';
|
||||
import { DEPLOYMENTS, ABIS, ROBINHOOD_TESTNET_CHAIN } from '@wtfx/contracts';
|
||||
|
||||
interface Web3ContextType {
|
||||
account: string | null;
|
||||
provider: ethers.BrowserProvider | null;
|
||||
signer: ethers.Signer | null;
|
||||
chainId: number | null;
|
||||
isCorrectNetwork: boolean;
|
||||
connectWallet: () => Promise<void>;
|
||||
switchNetwork: () => Promise<void>;
|
||||
getControllerContract: () => ethers.Contract | null;
|
||||
getCollateralContract: () => ethers.Contract | null;
|
||||
}
|
||||
|
||||
const Web3Context = createContext<Web3ContextType>({
|
||||
account: null,
|
||||
provider: null,
|
||||
signer: null,
|
||||
chainId: null,
|
||||
isCorrectNetwork: false,
|
||||
connectWallet: async () => {},
|
||||
switchNetwork: async () => {},
|
||||
getControllerContract: () => null,
|
||||
getCollateralContract: () => null,
|
||||
});
|
||||
|
||||
export const Web3Provider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
|
||||
const [account, setAccount] = useState<string | null>(null);
|
||||
const [provider, setProvider] = useState<ethers.BrowserProvider | null>(null);
|
||||
const [signer, setSigner] = useState<ethers.Signer | null>(null);
|
||||
const [chainId, setChainId] = useState<number | null>(null);
|
||||
|
||||
const isCorrectNetwork = chainId === ROBINHOOD_TESTNET_CHAIN.id;
|
||||
|
||||
const initProvider = async () => {
|
||||
if (typeof window !== 'undefined' && (window as any).ethereum) {
|
||||
const browserProvider = new ethers.BrowserProvider((window as any).ethereum);
|
||||
setProvider(browserProvider);
|
||||
try {
|
||||
const network = await browserProvider.getNetwork();
|
||||
setChainId(Number(network.chainId));
|
||||
const accounts = await browserProvider.listAccounts();
|
||||
if (accounts.length > 0) {
|
||||
setAccount(accounts[0].address);
|
||||
setSigner(await browserProvider.getSigner());
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Error initializing web3 provider:', err);
|
||||
}
|
||||
|
||||
(window as any).ethereum.on('accountsChanged', async (accs: string[]) => {
|
||||
if (accs.length > 0) {
|
||||
setAccount(accs[0]);
|
||||
const currentSigner = await browserProvider.getSigner();
|
||||
setSigner(currentSigner);
|
||||
} else {
|
||||
setAccount(null);
|
||||
setSigner(null);
|
||||
}
|
||||
});
|
||||
|
||||
(window as any).ethereum.on('chainChanged', (cId: string) => {
|
||||
setChainId(parseInt(cId, 16));
|
||||
window.location.reload();
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
initProvider();
|
||||
}, []);
|
||||
|
||||
const connectWallet = async () => {
|
||||
if (typeof window !== 'undefined' && (window as any).ethereum) {
|
||||
try {
|
||||
const browserProvider = new ethers.BrowserProvider((window as any).ethereum);
|
||||
await browserProvider.send('eth_requestAccounts', []);
|
||||
const currentSigner = await browserProvider.getSigner();
|
||||
const address = await currentSigner.getAddress();
|
||||
const network = await browserProvider.getNetwork();
|
||||
setProvider(browserProvider);
|
||||
setSigner(currentSigner);
|
||||
setAccount(address);
|
||||
setChainId(Number(network.chainId));
|
||||
} catch (err) {
|
||||
console.error('Failed to connect wallet:', err);
|
||||
throw err;
|
||||
}
|
||||
} else {
|
||||
alert('请安装 MetaMask 或其他 Web3 钱包!');
|
||||
}
|
||||
};
|
||||
|
||||
const switchNetwork = async () => {
|
||||
if (typeof window !== 'undefined' && (window as any).ethereum) {
|
||||
const hexChainId = '0x' + ROBINHOOD_TESTNET_CHAIN.id.toString(16);
|
||||
try {
|
||||
await (window as any).ethereum.request({
|
||||
method: 'wallet_switchEthereumChain',
|
||||
params: [{ chainId: hexChainId }],
|
||||
});
|
||||
} catch (switchError: any) {
|
||||
if (switchError.code === 4902) {
|
||||
try {
|
||||
await (window as any).ethereum.request({
|
||||
method: 'wallet_addEthereumChain',
|
||||
params: [
|
||||
{
|
||||
chainId: hexChainId,
|
||||
chainName: ROBINHOOD_TESTNET_CHAIN.name,
|
||||
rpcUrls: ROBINHOOD_TESTNET_CHAIN.rpcUrls.default.http,
|
||||
nativeCurrency: ROBINHOOD_TESTNET_CHAIN.nativeCurrency,
|
||||
},
|
||||
],
|
||||
});
|
||||
} catch (addError) {
|
||||
console.error('Failed to add network:', addError);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const getControllerContract = () => {
|
||||
if (!provider) return null;
|
||||
const address = DEPLOYMENTS.robinhoodTestnet.contracts.controllerProxy;
|
||||
return new ethers.Contract(address, ABIS.WTFControllerV2, signer || provider);
|
||||
};
|
||||
|
||||
const getCollateralContract = () => {
|
||||
if (!provider) return null;
|
||||
const address = DEPLOYMENTS.robinhoodTestnet.contracts.collateral;
|
||||
return new ethers.Contract(address, ABIS.MockERC20, signer || provider);
|
||||
};
|
||||
|
||||
return (
|
||||
<Web3Context.Provider
|
||||
value={{
|
||||
account,
|
||||
provider,
|
||||
signer,
|
||||
chainId,
|
||||
isCorrectNetwork,
|
||||
connectWallet,
|
||||
switchNetwork,
|
||||
getControllerContract,
|
||||
getCollateralContract,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</Web3Context.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export const useWeb3 = () => useContext(Web3Context);
|
||||
@@ -0,0 +1,241 @@
|
||||
import React, { useState } from 'react';
|
||||
import { useWeb3 } from '../../context/Web3Context';
|
||||
import { ethers } from 'ethers';
|
||||
import { Scale, CheckCircle2, AlertOctagon, PauseCircle, PlayCircle, RefreshCw, Loader2, Check, AlertCircle } from 'lucide-react';
|
||||
|
||||
export const AdjudicationPanel: React.FC = () => {
|
||||
const { account, signer, getControllerContract, isCorrectNetwork } = useWeb3();
|
||||
|
||||
const [questionId, setQuestionId] = useState('');
|
||||
const [outcomeIndex, setOutcomeIndex] = useState<number>(0);
|
||||
const [marketAddress, setMarketAddress] = useState('');
|
||||
const [loading, setLoading] = useState<boolean>(false);
|
||||
const [statusMsg, setStatusMsg] = useState<{ type: 'success' | 'error'; text: string } | null>(null);
|
||||
|
||||
const handleResolve = async () => {
|
||||
if (!signer || !account) return;
|
||||
try {
|
||||
setLoading(true);
|
||||
setStatusMsg(null);
|
||||
const controller = getControllerContract();
|
||||
if (!controller) throw new Error('Controller not found');
|
||||
|
||||
const qId = questionId.startsWith('0x') ? questionId : ethers.keccak256(ethers.toUtf8Bytes(questionId));
|
||||
const tx = await controller.resolveOutcome(qId, outcomeIndex);
|
||||
setStatusMsg({ type: 'success', text: `预言机初步裁决交易已广播: ${tx.hash.slice(0, 10)}...` });
|
||||
await tx.wait();
|
||||
setStatusMsg({ type: 'success', text: `🎉 市场问题 ${qId.slice(0, 8)}... 已由预言机成功提交胜出结果 Outcome #${outcomeIndex}!进入争议公示期。` });
|
||||
} catch (err: any) {
|
||||
setStatusMsg({ type: 'error', text: err.reason || err.message || '裁决失败' });
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleFinalise = async () => {
|
||||
if (!signer || !account) return;
|
||||
try {
|
||||
setLoading(true);
|
||||
setStatusMsg(null);
|
||||
const controller = getControllerContract();
|
||||
if (!controller) throw new Error('Controller not found');
|
||||
|
||||
const qId = questionId.startsWith('0x') ? questionId : ethers.keccak256(ethers.toUtf8Bytes(questionId));
|
||||
const tx = await controller.finaliseOutcome(qId);
|
||||
setStatusMsg({ type: 'success', text: `最终结果敲定交易已广播: ${tx.hash.slice(0, 10)}...` });
|
||||
await tx.wait();
|
||||
setStatusMsg({ type: 'success', text: `🎉 市场问题 ${qId.slice(0, 8)}... 争议期结束,胜出结果已在链上永久生效,用户可兑现奖励!` });
|
||||
} catch (err: any) {
|
||||
setStatusMsg({ type: 'error', text: err.reason || err.message || '敲定失败' });
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleOverrideFinalise = async () => {
|
||||
if (!signer || !account) return;
|
||||
try {
|
||||
setLoading(true);
|
||||
setStatusMsg(null);
|
||||
const controller = getControllerContract();
|
||||
if (!controller) throw new Error('Controller not found');
|
||||
|
||||
const qId = questionId.startsWith('0x') ? questionId : ethers.keccak256(ethers.toUtf8Bytes(questionId));
|
||||
const tx = await controller.overrideFinalise(qId, outcomeIndex);
|
||||
setStatusMsg({ type: 'success', text: `管理员一票否决/强制裁决交易已广播: ${tx.hash.slice(0, 10)}...` });
|
||||
await tx.wait();
|
||||
setStatusMsg({ type: 'success', text: `🛡️ 管理员已强制将市场问题 ${qId.slice(0, 8)}... 敲定为 Outcome #${outcomeIndex}!` });
|
||||
} catch (err: any) {
|
||||
setStatusMsg({ type: 'error', text: err.reason || err.message || '强制裁决失败' });
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handlePause = async () => {
|
||||
if (!signer || !account) return;
|
||||
try {
|
||||
setLoading(true);
|
||||
setStatusMsg(null);
|
||||
const controller = getControllerContract();
|
||||
if (!controller) throw new Error('Controller not found');
|
||||
|
||||
const tx = await controller.pause();
|
||||
setStatusMsg({ type: 'success', text: `紧急熔断暂停交易已广播: ${tx.hash.slice(0, 10)}...` });
|
||||
await tx.wait();
|
||||
setStatusMsg({ type: 'success', text: `🚨 协议已进入全局紧急暂停 (PAUSED) 状态!` });
|
||||
} catch (err: any) {
|
||||
setStatusMsg({ type: 'error', text: err.reason || err.message || '暂停失败' });
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleUnpause = async () => {
|
||||
if (!signer || !account) return;
|
||||
try {
|
||||
setLoading(true);
|
||||
setStatusMsg(null);
|
||||
const controller = getControllerContract();
|
||||
if (!controller) throw new Error('Controller not found');
|
||||
|
||||
const tx = await controller.unpause();
|
||||
setStatusMsg({ type: 'success', text: `解除紧急暂停交易已广播: ${tx.hash.slice(0, 10)}...` });
|
||||
await tx.wait();
|
||||
setStatusMsg({ type: 'success', text: `✅ 协议已恢复正常运行!` });
|
||||
} catch (err: any) {
|
||||
setStatusMsg({ type: 'error', text: err.reason || err.message || '恢复失败' });
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<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="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-purple-500/10 rounded-xl border border-purple-500/20 text-purple-400">
|
||||
<Scale className="w-6 h-6" />
|
||||
</div>
|
||||
<div>
|
||||
<h2 className="text-xl font-bold text-white">市场风控与裁决中心 (Adjudication & Emergency)</h2>
|
||||
<p className="text-sm text-slate-400 mt-0.5">
|
||||
预言机初审、胜出结果终裁敲定、超管一票否决与全局熔断保护
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Question Outcome Adjudication */}
|
||||
<div className="bg-slate-950/50 border border-slate-800 rounded-xl p-5 space-y-4">
|
||||
<h3 className="text-sm font-semibold text-slate-200">1. 市场结果裁决 (Outcome Resolution)</h3>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
<div className="md:col-span-2">
|
||||
<label className="block text-xs font-medium text-slate-400 mb-1.5">Question ID (Bytes32 / 标题哈希)</label>
|
||||
<input
|
||||
type="text"
|
||||
value={questionId}
|
||||
onChange={(e) => setQuestionId(e.target.value)}
|
||||
placeholder="0x..."
|
||||
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>
|
||||
<label className="block text-xs font-medium text-slate-400 mb-1.5">胜出选项序号 (Winning Outcome Index)</label>
|
||||
<input
|
||||
type="number"
|
||||
min="0"
|
||||
max="7"
|
||||
value={outcomeIndex}
|
||||
onChange={(e) => setOutcomeIndex(Number(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>
|
||||
|
||||
<div className="flex flex-wrap gap-3 pt-2">
|
||||
<button
|
||||
onClick={handleResolve}
|
||||
disabled={loading || !account || !questionId}
|
||||
className="px-4 py-2.5 bg-indigo-600 hover:bg-indigo-500 text-white rounded-lg text-xs font-semibold flex items-center space-x-1.5 shadow-md shadow-indigo-600/20 disabled:opacity-50"
|
||||
>
|
||||
<CheckCircle2 className="w-4 h-4" />
|
||||
<span>预言机提交初审 (resolveOutcome)</span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={handleFinalise}
|
||||
disabled={loading || !account || !questionId}
|
||||
className="px-4 py-2.5 bg-emerald-600 hover:bg-emerald-500 text-white rounded-lg text-xs font-semibold flex items-center space-x-1.5 shadow-md shadow-emerald-600/20 disabled:opacity-50"
|
||||
>
|
||||
<Scale className="w-4 h-4" />
|
||||
<span>争议期后终裁敲定 (finaliseOutcome)</span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={handleOverrideFinalise}
|
||||
disabled={loading || !account || !questionId}
|
||||
className="px-4 py-2.5 bg-purple-600 hover:bg-purple-500 text-white rounded-lg text-xs font-semibold flex items-center space-x-1.5 shadow-md shadow-purple-600/20 disabled:opacity-50"
|
||||
>
|
||||
<AlertOctagon className="w-4 h-4" />
|
||||
<span>超管强制一票终决 (overrideFinalise)</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Protocol Pause Control */}
|
||||
<div className="bg-slate-950/50 border border-slate-800 rounded-xl p-5 space-y-4">
|
||||
<h3 className="text-sm font-semibold text-rose-400 flex items-center space-x-2">
|
||||
<AlertOctagon className="w-4 h-4" />
|
||||
<span>2. 协议紧急熔断控制 (Circuit Breaker)</span>
|
||||
</h3>
|
||||
<p className="text-xs text-slate-400">
|
||||
当链上异常或智能合约升级前夕,GUARDIAN_ROLE 与 ADMIN 可一键暂停全协议建盘与交易。
|
||||
</p>
|
||||
|
||||
<div className="flex gap-4 pt-1">
|
||||
<button
|
||||
onClick={handlePause}
|
||||
disabled={loading || !account}
|
||||
className="px-5 py-2.5 bg-rose-600/20 hover:bg-rose-600/30 text-rose-300 border border-rose-600/40 rounded-lg text-xs font-semibold flex items-center space-x-2 transition-colors disabled:opacity-50"
|
||||
>
|
||||
<PauseCircle className="w-4 h-4" />
|
||||
<span>紧急全局暂停 (Pause All Markets)</span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={handleUnpause}
|
||||
disabled={loading || !account}
|
||||
className="px-5 py-2.5 bg-emerald-600/20 hover:bg-emerald-600/30 text-emerald-300 border border-emerald-600/40 rounded-lg text-xs font-semibold flex items-center space-x-2 transition-colors disabled:opacity-50"
|
||||
>
|
||||
<PlayCircle className="w-4 h-4" />
|
||||
<span>恢复全协议运行 (Unpause Protocol)</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Status Message */}
|
||||
{statusMsg && (
|
||||
<div
|
||||
className={`p-4 rounded-xl border flex items-start space-x-3 text-sm ${
|
||||
statusMsg.type === 'success'
|
||||
? 'bg-emerald-950/30 border-emerald-800/50 text-emerald-300'
|
||||
: 'bg-rose-950/30 border-rose-800/50 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>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,92 @@
|
||||
import React from 'react';
|
||||
import { useWeb3 } from '../../context/Web3Context';
|
||||
import { DEPLOYMENTS } from '@wtfx/contracts';
|
||||
import { Wallet, ShieldAlert, CheckCircle2 } from 'lucide-react';
|
||||
|
||||
interface NavbarProps {
|
||||
activeTab: string;
|
||||
setActiveTab: (tab: string) => void;
|
||||
}
|
||||
|
||||
export const AdminNavbar: React.FC<NavbarProps> = ({ activeTab, setActiveTab }) => {
|
||||
const { account, isCorrectNetwork, connectWallet, switchNetwork } = useWeb3();
|
||||
|
||||
const navItems = [
|
||||
{ id: 'faucet', label: '🚰 WUSD 水龙头' },
|
||||
{ id: 'wizard', label: '🚀 创建市场向导' },
|
||||
{ id: 'governance', label: '⚙️ 协议参数治理' },
|
||||
{ id: 'adjudication', label: '⚖️ 市场风控与裁决' },
|
||||
];
|
||||
|
||||
return (
|
||||
<header className="bg-slate-900/80 backdrop-blur-md border-b border-slate-800 sticky top-0 z-50">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 h-16 flex items-center justify-between">
|
||||
<div className="flex items-center space-x-8">
|
||||
<div className="flex items-center space-x-3">
|
||||
<div className="w-9 h-9 bg-gradient-to-tr from-cyan-500 to-indigo-600 rounded-lg flex items-center justify-center font-black text-white shadow-lg shadow-cyan-500/20">
|
||||
W
|
||||
</div>
|
||||
<div>
|
||||
<span className="font-bold text-lg text-white tracking-wider">WTFX</span>
|
||||
<span className="ml-2 text-xs font-mono px-2 py-0.5 rounded bg-cyan-950 text-cyan-400 border border-cyan-800">
|
||||
ADMIN CONSOLE
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<nav className="flex space-x-1">
|
||||
{navItems.map((item) => (
|
||||
<button
|
||||
key={item.id}
|
||||
onClick={() => setActiveTab(item.id)}
|
||||
className={`px-3.5 py-2 rounded-lg text-sm font-medium transition-all ${
|
||||
activeTab === item.id
|
||||
? 'bg-slate-800 text-white shadow-sm border border-slate-700'
|
||||
: 'text-slate-400 hover:text-slate-200 hover:bg-slate-800/50'
|
||||
}`}
|
||||
>
|
||||
{item.label}
|
||||
</button>
|
||||
))}
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center space-x-4">
|
||||
{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"
|
||||
>
|
||||
<ShieldAlert className="w-4 h-4" />
|
||||
<span>切换至 Robinhood 测试网</span>
|
||||
</button>
|
||||
) : (
|
||||
<span className="flex items-center space-x-1 text-emerald-400 bg-emerald-950/40 border border-emerald-800/50 px-2.5 py-1 rounded-md text-xs font-mono">
|
||||
<CheckCircle2 className="w-3.5 h-3.5" />
|
||||
<span>Robinhood 46630</span>
|
||||
</span>
|
||||
)}
|
||||
|
||||
<div className="flex items-center space-x-2 bg-slate-800 border border-slate-700 px-3 py-1.5 rounded-lg">
|
||||
<div className="w-2 h-2 rounded-full bg-emerald-500"></div>
|
||||
<span className="font-mono text-xs text-slate-200">
|
||||
{account.slice(0, 6)}...{account.slice(-4)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<button
|
||||
onClick={connectWallet}
|
||||
className="flex items-center space-x-2 bg-cyan-600 hover:bg-cyan-500 text-white px-4 py-2 rounded-lg text-sm font-medium shadow-md shadow-cyan-600/20 transition-all"
|
||||
>
|
||||
<Wallet className="w-4 h-4" />
|
||||
<span>连接钱包</span>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,198 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { useWeb3 } from '../../context/Web3Context';
|
||||
import { DEPLOYMENTS } from '@wtfx/contracts';
|
||||
import { ethers } from 'ethers';
|
||||
import { Coins, ArrowUpRight, Check, AlertCircle, Loader2, Sparkles } from 'lucide-react';
|
||||
|
||||
export const FaucetPanel: React.FC = () => {
|
||||
const { account, signer, getCollateralContract, isCorrectNetwork } = useWeb3();
|
||||
const [balance, setBalance] = useState<string>('0');
|
||||
const [loading, setLoading] = useState<boolean>(false);
|
||||
const [customAmount, setCustomAmount] = useState<string>('100000000');
|
||||
const [targetAddress, setTargetAddress] = useState<string>('');
|
||||
const [txHash, setTxHash] = useState<string | null>(null);
|
||||
const [statusMsg, setStatusMsg] = useState<{ type: 'success' | 'error'; text: string } | null>(null);
|
||||
|
||||
const collateralAddress = DEPLOYMENTS.robinhoodTestnet.contracts.collateral;
|
||||
|
||||
const fetchBalance = async () => {
|
||||
if (!account) return;
|
||||
try {
|
||||
const contract = getCollateralContract();
|
||||
if (contract) {
|
||||
const bal = await contract.balanceOf(account);
|
||||
setBalance(ethers.formatUnits(bal, 18));
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Failed to fetch balance:', err);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (account) {
|
||||
setTargetAddress(account);
|
||||
fetchBalance();
|
||||
}
|
||||
}, [account]);
|
||||
|
||||
const handleMint = async (amountStr: string) => {
|
||||
if (!signer || !account) {
|
||||
setStatusMsg({ type: 'error', text: '请先连接管理员钱包!' });
|
||||
return;
|
||||
}
|
||||
if (!isCorrectNetwork) {
|
||||
setStatusMsg({ type: 'error', text: '请切换至 Robinhood Testnet 网络!' });
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
setLoading(true);
|
||||
setStatusMsg(null);
|
||||
setTxHash(null);
|
||||
|
||||
const contract = getCollateralContract();
|
||||
if (!contract) throw new Error('Contract not initialized');
|
||||
|
||||
const recipient = targetAddress.trim() || account;
|
||||
const amountWei = ethers.parseUnits(amountStr, 18);
|
||||
|
||||
const tx = await contract.mint(recipient, amountWei);
|
||||
setStatusMsg({ type: 'success', text: `交易已广播,等待链上确认... Tx: ${tx.hash.slice(0, 10)}...` });
|
||||
setTxHash(tx.hash);
|
||||
|
||||
await tx.wait();
|
||||
setStatusMsg({ type: 'success', text: `🎉 成功铸造 ${Number(amountStr).toLocaleString()} WUSD 到地址 ${recipient}!` });
|
||||
fetchBalance();
|
||||
} catch (err: any) {
|
||||
console.error('Mint error:', err);
|
||||
setStatusMsg({ type: 'error', text: err.reason || err.message || '铸造失败,请检查交易状态' });
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<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 relative overflow-hidden">
|
||||
<div className="absolute -right-10 -top-10 w-64 h-64 bg-cyan-500/10 rounded-full blur-3xl pointer-events-none"></div>
|
||||
|
||||
<div className="flex flex-col md:flex-row md:items-center justify-between gap-4 border-b border-slate-800 pb-6">
|
||||
<div className="flex items-center space-x-3">
|
||||
<div className="p-3 bg-cyan-500/10 rounded-xl border border-cyan-500/20 text-cyan-400">
|
||||
<Coins className="w-6 h-6" />
|
||||
</div>
|
||||
<div>
|
||||
<h2 className="text-xl font-bold text-white">WUSD 领水水龙头 (Testnet Faucet)</h2>
|
||||
<p className="text-sm text-slate-400 mt-0.5">
|
||||
Mock ERC-20 结算通证地址: <span className="font-mono text-cyan-400">{collateralAddress}</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-slate-950/60 border border-slate-800 rounded-xl px-4 py-2.5">
|
||||
<span className="text-xs text-slate-400 block">当前钱包余额</span>
|
||||
<span className="text-lg font-bold font-mono text-emerald-400">
|
||||
{Number(balance).toLocaleString(undefined, { maximumFractionDigits: 2 })} WUSD
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-6 space-y-6">
|
||||
{/* Quick Action */}
|
||||
<div className="bg-gradient-to-r from-slate-950 to-slate-900 border border-cyan-900/40 rounded-xl p-5 flex flex-col sm:flex-row items-center justify-between gap-4">
|
||||
<div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<Sparkles className="w-5 h-5 text-amber-400" />
|
||||
<h3 className="text-base font-semibold text-white">管理员一键领水 (100,000,000 WUSD)</h3>
|
||||
</div>
|
||||
<p className="text-xs text-slate-400 mt-1">
|
||||
为做市、流动性注入、初始化预测市场底仓提供充足的 1 亿测试代币。
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => handleMint('100000000')}
|
||||
disabled={loading || !account}
|
||||
className="w-full sm:w-auto px-6 py-3 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 justify-center space-x-2 transition-all disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
{loading ? (
|
||||
<>
|
||||
<Loader2 className="w-4 h-4 animate-spin" />
|
||||
<span>处理中...</span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Coins className="w-4 h-4" />
|
||||
<span>一键领取 1 亿 WUSD</span>
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Custom Mint Form */}
|
||||
<div className="bg-slate-950/40 border border-slate-800/80 rounded-xl p-5 space-y-4">
|
||||
<h3 className="text-sm font-semibold text-slate-300">自定义铸造 (Custom Mint)</h3>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="block text-xs font-medium text-slate-400 mb-1.5">接收地址 (Recipient Address)</label>
|
||||
<input
|
||||
type="text"
|
||||
value={targetAddress}
|
||||
onChange={(e) => setTargetAddress(e.target.value)}
|
||||
placeholder="0x..."
|
||||
className="w-full bg-slate-900 border border-slate-700 rounded-lg px-3.5 py-2.5 text-sm text-slate-100 font-mono focus:outline-none focus:border-cyan-500 transition-colors"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-xs font-medium text-slate-400 mb-1.5">铸造数量 (Amount WUSD)</label>
|
||||
<input
|
||||
type="number"
|
||||
value={customAmount}
|
||||
onChange={(e) => setCustomAmount(e.target.value)}
|
||||
placeholder="1000000"
|
||||
className="w-full bg-slate-900 border border-slate-700 rounded-lg px-3.5 py-2.5 text-sm text-slate-100 font-mono focus:outline-none focus:border-cyan-500 transition-colors"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end pt-2">
|
||||
<button
|
||||
onClick={() => handleMint(customAmount)}
|
||||
disabled={loading || !account || !customAmount}
|
||||
className="px-5 py-2.5 bg-slate-800 hover:bg-slate-700 text-slate-200 border border-slate-600 rounded-lg text-sm font-medium transition-colors flex items-center space-x-2 disabled:opacity-50"
|
||||
>
|
||||
{loading ? <Loader2 className="w-4 h-4 animate-spin" /> : <ArrowUpRight className="w-4 h-4" />}
|
||||
<span>铸造至指定地址</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Status Message */}
|
||||
{statusMsg && (
|
||||
<div
|
||||
className={`p-4 rounded-xl border flex items-start space-x-3 text-sm ${
|
||||
statusMsg.type === 'success'
|
||||
? 'bg-emerald-950/30 border-emerald-800/50 text-emerald-300'
|
||||
: 'bg-rose-950/30 border-rose-800/50 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>
|
||||
{txHash && (
|
||||
<p className="mt-1 text-xs text-slate-400">
|
||||
Tx Hash: <span className="font-mono text-cyan-400">{txHash}</span>
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,251 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { useWeb3 } from '../../context/Web3Context';
|
||||
import { DEPLOYMENTS } from '@wtfx/contracts';
|
||||
import { ethers } from 'ethers';
|
||||
import { Sliders, ShieldCheck, Percent, Wallet, Check, AlertCircle, Loader2, RefreshCw } from 'lucide-react';
|
||||
|
||||
export const GovernancePanel: React.FC = () => {
|
||||
const { account, signer, getControllerContract, isCorrectNetwork } = useWeb3();
|
||||
|
||||
const [treasury, setTreasury] = useState<string>('');
|
||||
const [defaultFeeRate, setDefaultFeeRate] = useState<string>('0.6');
|
||||
const [creatorShare, setCreatorShare] = useState<string>('50');
|
||||
const [centralWallet, setCentralWallet] = useState<string>('');
|
||||
const [loading, setLoading] = useState<boolean>(false);
|
||||
const [statusMsg, setStatusMsg] = useState<{ type: 'success' | 'error'; text: string } | null>(null);
|
||||
|
||||
const fetchGovParams = async () => {
|
||||
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);
|
||||
setCentralWallet(DEPLOYMENTS.robinhoodTestnet.governance.admin);
|
||||
} catch (err) {
|
||||
console.error('Fetch gov params error:', err);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetchGovParams();
|
||||
}, [account]);
|
||||
|
||||
const handleUpdateTreasury = async () => {
|
||||
if (!signer || !account) return;
|
||||
try {
|
||||
setLoading(true);
|
||||
setStatusMsg(null);
|
||||
const controller = getControllerContract();
|
||||
if (!controller) throw new Error('Controller not found');
|
||||
|
||||
const tx = await controller.setTreasury(treasury.trim());
|
||||
setStatusMsg({ type: 'success', text: `更新国库金库地址交易已广播: ${tx.hash.slice(0, 10)}...` });
|
||||
await tx.wait();
|
||||
setStatusMsg({ type: 'success', text: `🎉 协议国库金库地址已成功更新为: ${treasury}` });
|
||||
} catch (err: any) {
|
||||
setStatusMsg({ type: 'error', text: err.reason || err.message || '更新失败' });
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleUpdateFeeRate = async () => {
|
||||
if (!signer || !account) return;
|
||||
try {
|
||||
setLoading(true);
|
||||
setStatusMsg(null);
|
||||
const controller = getControllerContract();
|
||||
if (!controller) throw new Error('Controller not found');
|
||||
|
||||
const rateWei = ethers.parseUnits((Number(defaultFeeRate) / 100).toString(), 18);
|
||||
const tx = await controller.setFeeRateDefault(rateWei);
|
||||
setStatusMsg({ type: 'success', text: `更新默认手续费交易已广播: ${tx.hash.slice(0, 10)}...` });
|
||||
await tx.wait();
|
||||
setStatusMsg({ type: 'success', text: `🎉 全局默认协议手续费率已成功更新为: ${defaultFeeRate}%` });
|
||||
} catch (err: any) {
|
||||
setStatusMsg({ type: 'error', text: err.reason || err.message || '更新失败' });
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleUpdateCreatorShare = async () => {
|
||||
if (!signer || !account) return;
|
||||
try {
|
||||
setLoading(true);
|
||||
setStatusMsg(null);
|
||||
const controller = getControllerContract();
|
||||
if (!controller) throw new Error('Controller not found');
|
||||
|
||||
const shareWei = ethers.parseUnits((Number(creatorShare) / 100).toString(), 18);
|
||||
const tx = await controller.setCreatorShare(shareWei);
|
||||
setStatusMsg({ type: 'success', text: `更新建盘者手续费分成交易已广播: ${tx.hash.slice(0, 10)}...` });
|
||||
await tx.wait();
|
||||
setStatusMsg({ type: 'success', text: `🎉 建盘者手续费返佣分成比例已成功更新为: ${creatorShare}%` });
|
||||
} catch (err: any) {
|
||||
setStatusMsg({ type: 'error', text: err.reason || err.message || '更新失败' });
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleUpdateCentralWallet = async () => {
|
||||
if (!signer || !account) return;
|
||||
try {
|
||||
setLoading(true);
|
||||
setStatusMsg(null);
|
||||
const controller = getControllerContract();
|
||||
if (!controller) throw new Error('Controller not found');
|
||||
|
||||
const tx = await controller.setCentralWallet(centralWallet.trim());
|
||||
setStatusMsg({ type: 'success', text: `更新中央安全提币钱包交易已广播: ${tx.hash.slice(0, 10)}...` });
|
||||
await tx.wait();
|
||||
setStatusMsg({ type: 'success', text: `🎉 中央安全提币钱包已成功更新为: ${centralWallet}` });
|
||||
} catch (err: any) {
|
||||
setStatusMsg({ type: 'error', text: err.reason || err.message || '更新失败' });
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<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="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-amber-500/10 rounded-xl border border-amber-500/20 text-amber-400">
|
||||
<Sliders className="w-6 h-6" />
|
||||
</div>
|
||||
<div>
|
||||
<h2 className="text-xl font-bold text-white">协议治理看板 (Protocol Governance)</h2>
|
||||
<p className="text-sm text-slate-400 mt-0.5">
|
||||
WTFControllerV2 智能合约超管参数配置(仅 DEFAULT_ADMIN_ROLE / OPERATOR_ROLE 可执行)
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button
|
||||
onClick={fetchGovParams}
|
||||
className="p-2 text-slate-400 hover:text-slate-200 hover:bg-slate-800 rounded-lg transition-colors"
|
||||
>
|
||||
<RefreshCw className="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
{/* Treasury Setting */}
|
||||
<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">
|
||||
<ShieldCheck className="w-4 h-4 text-cyan-400" />
|
||||
<span>协议国库金库 (Treasury Address)</span>
|
||||
</div>
|
||||
<p className="text-xs text-slate-400">用于接收协议留存手续费分润的冷钱包/金库合约。</p>
|
||||
<input
|
||||
type="text"
|
||||
value={treasury}
|
||||
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"
|
||||
/>
|
||||
<button
|
||||
onClick={handleUpdateTreasury}
|
||||
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>
|
||||
|
||||
{/* 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>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Status Message */}
|
||||
{statusMsg && (
|
||||
<div
|
||||
className={`p-4 rounded-xl border flex items-start space-x-3 text-sm ${
|
||||
statusMsg.type === 'success'
|
||||
? 'bg-emerald-950/30 border-emerald-800/50 text-emerald-300'
|
||||
: 'bg-rose-950/30 border-rose-800/50 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>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,326 @@
|
||||
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 MarketWizard: React.FC = () => {
|
||||
const { account, signer, getControllerContract, getCollateralContract, isCorrectNetwork } = useWeb3();
|
||||
|
||||
const [question, setQuestion] = useState('');
|
||||
const [description, setDescription] = useState('');
|
||||
const [category, setCategory] = useState('Crypto');
|
||||
const [endTime, setEndTime] = useState(
|
||||
new Date(Date.now() + 7 * 24 * 3600 * 1000).toISOString().slice(0, 16)
|
||||
);
|
||||
const [outcomes, setOutcomes] = useState<string[]>(['YES', 'NO']);
|
||||
const [selectedTier, setSelectedTier] = useState<number>(1);
|
||||
const [seedAmount, setSeedAmount] = useState<string>('1000');
|
||||
const [loading, setLoading] = useState<boolean>(false);
|
||||
const [statusMsg, setStatusMsg] = useState<{ type: 'success' | 'error'; text: string } | null>(null);
|
||||
const [deployedMarketAddress, setDeployedMarketAddress] = useState<string | null>(null);
|
||||
|
||||
const addOutcome = () => {
|
||||
if (outcomes.length < 8) {
|
||||
setOutcomes([...outcomes, `OUTCOME ${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);
|
||||
setDeployedMarketAddress(null);
|
||||
|
||||
const controller = getControllerContract();
|
||||
const collateral = getCollateralContract();
|
||||
if (!controller || !collateral) throw new Error('Contracts not loaded');
|
||||
|
||||
const controllerAddress = await controller.getAddress();
|
||||
const seedWei = ethers.parseUnits(seedAmount || '0', 18);
|
||||
|
||||
// Step 1: Check and Approve Collateral if Seed > 0
|
||||
if (seedWei > 0n) {
|
||||
setStatusMsg({ type: 'success', text: '步骤 1/3: 正在授权底仓资金...' });
|
||||
const allowance = await collateral.allowance(account, controllerAddress);
|
||||
if (allowance < seedWei) {
|
||||
const appTx = await collateral.approve(controllerAddress, ethers.MaxUint256);
|
||||
await appTx.wait();
|
||||
}
|
||||
}
|
||||
|
||||
// Step 2: Prepare Question and Market Params
|
||||
setStatusMsg({ type: 'success', text: '步骤 2/3: 正在广播建盘交易到链上...' });
|
||||
const deadlineSec = Math.floor(new Date(endTime).getTime() / 1000);
|
||||
const questionId = ethers.keccak256(
|
||||
ethers.toUtf8Bytes(`${question}-${Date.now()}-${account}`)
|
||||
);
|
||||
|
||||
const questionParams = {
|
||||
questionId: questionId,
|
||||
question: question,
|
||||
ancillaryData: ethers.toUtf8Bytes(JSON.stringify({ description, category })),
|
||||
rewardToken: DEPLOYMENTS.robinhoodTestnet.contracts.collateral,
|
||||
reward: 0,
|
||||
proposalBond: 0,
|
||||
earlyResolutionBond: 0,
|
||||
settlementResolutionBond: 0,
|
||||
resolutionTime: deadlineSec,
|
||||
numOutcomes: outcomes.length,
|
||||
};
|
||||
|
||||
const marketParams = {
|
||||
tier: selectedTier,
|
||||
creator: account,
|
||||
feeRate: ethers.parseUnits('0.006', 18), // 0.6% default
|
||||
creatorShare: ethers.parseUnits('0.5', 18), // 50%
|
||||
curve: DEPLOYMENTS.robinhoodTestnet.contracts.powerLDACurve,
|
||||
collateral: DEPLOYMENTS.robinhoodTestnet.contracts.collateral,
|
||||
};
|
||||
|
||||
// Step 3: Call deployMarket
|
||||
const tx = await controller.deployMarket(
|
||||
questionParams,
|
||||
marketParams,
|
||||
account, // Oracle address (admin acts as oracle)
|
||||
seedWei
|
||||
);
|
||||
|
||||
setStatusMsg({ type: 'success', text: `步骤 3/3: 交易已广播 (Tx: ${tx.hash.slice(0, 10)}...),等待区块确认...` });
|
||||
const receipt = await tx.wait();
|
||||
|
||||
// Extract MarketDeployed event or address
|
||||
setStatusMsg({
|
||||
type: 'success',
|
||||
text: `🎉 预测市场创建成功!交易哈希: ${receipt.hash}`,
|
||||
});
|
||||
} catch (err: any) {
|
||||
console.error('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-slate-900 border border-slate-800 rounded-2xl p-6 shadow-xl 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-indigo-500/10 rounded-xl border border-indigo-500/20 text-indigo-400">
|
||||
<Rocket className="w-6 h-6" />
|
||||
</div>
|
||||
<div>
|
||||
<h2 className="text-xl font-bold text-white">创建预测市场向导 (Deploy Market Wizard)</h2>
|
||||
<p className="text-sm text-slate-400 mt-0.5">
|
||||
基于 WTFControllerV2 与 PowerLDACurveV2 幂律联合曲线一键创建全套预测市场
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Basic Info */}
|
||||
<div className="space-y-4">
|
||||
<h3 className="text-sm font-semibold text-slate-300 uppercase tracking-wider">1. 市场基本信息</h3>
|
||||
|
||||
<div>
|
||||
<label className="block text-xs font-medium text-slate-400 mb-1.5">市场标题 / 预测问题 (Question Title)</label>
|
||||
<input
|
||||
type="text"
|
||||
required
|
||||
value={question}
|
||||
onChange={(e) => setQuestion(e.target.value)}
|
||||
placeholder="例如:Will Bitcoin exceed $150,000 before December 31, 2026?"
|
||||
className="w-full bg-slate-950 border border-slate-700 rounded-lg px-3.5 py-2.5 text-sm text-slate-100 focus:outline-none focus:border-cyan-500 transition-colors"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="block text-xs font-medium text-slate-400 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-lg px-3.5 py-2.5 text-sm text-slate-100 focus:outline-none focus:border-cyan-500"
|
||||
>
|
||||
<option value="Crypto">Crypto (加密货币)</option>
|
||||
<option value="Politics">Politics (政治事件)</option>
|
||||
<option value="Macro">Macro (宏观经济)</option>
|
||||
<option value="Sports">Sports (体育赛事)</option>
|
||||
<option value="Tech">Tech (科技突破)</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-xs font-medium text-slate-400 mb-1.5">预测结算截止时间 (Resolution Time)</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-lg px-3.5 py-2.5 text-sm text-slate-100 focus:outline-none focus:border-cyan-500"
|
||||
>
|
||||
</input>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-xs font-medium text-slate-400 mb-1.5">补充裁决说明 / 数据源 (Resolution Criteria / Data Source)</label>
|
||||
<textarea
|
||||
rows={2}
|
||||
value={description}
|
||||
onChange={(e) => setDescription(e.target.value)}
|
||||
placeholder="例如:以 Binance 现货 BTC/USDT 交易对在 UTC 2026-12-31 23:59:59 前的最高成交价为准..."
|
||||
className="w-full bg-slate-950 border border-slate-700 rounded-lg px-3.5 py-2.5 text-sm text-slate-100 focus:outline-none focus:border-cyan-500"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Outcomes */}
|
||||
<div className="space-y-4 pt-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<h3 className="text-sm font-semibold text-slate-300 uppercase tracking-wider">2. 结果选项配置 (Outcome Options)</h3>
|
||||
<button
|
||||
type="button"
|
||||
onClick={addOutcome}
|
||||
disabled={outcomes.length >= 8}
|
||||
className="text-xs text-cyan-400 hover:text-cyan-300 flex items-center space-x-1 disabled:opacity-50"
|
||||
>
|
||||
<PlusCircle className="w-3.5 h-3.5" />
|
||||
<span>添加选项 (最多8个)</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
|
||||
{outcomes.map((outcome, idx) => (
|
||||
<div key={idx} className="flex items-center space-x-2 bg-slate-950 border border-slate-800 rounded-lg p-2">
|
||||
<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>
|
||||
|
||||
{/* Tier & Liquidity Seed */}
|
||||
<div className="space-y-4 pt-2">
|
||||
<h3 className="text-sm font-semibold text-slate-300 uppercase tracking-wider">3. 治理分级与初始底仓 (Tier & Seed)</h3>
|
||||
|
||||
<div className="grid grid-cols-1 sm:grid-cols-3 gap-3">
|
||||
{[
|
||||
{ tier: 1, name: 'Tier 1 (标准盘)', desc: '支持自由创建,毕业需 100K 资金池' },
|
||||
{ tier: 2, name: 'Tier 2 (精选盘)', desc: '具备更强流动性深度与做市扶持' },
|
||||
{ tier: 3, name: 'Tier 3 (旗舰盘)', desc: '极高容量上限与官方背书' },
|
||||
].map((t) => (
|
||||
<div
|
||||
key={t.tier}
|
||||
onClick={() => setSelectedTier(t.tier)}
|
||||
className={`p-3.5 rounded-xl border cursor-pointer transition-all ${
|
||||
selectedTier === t.tier
|
||||
? 'bg-cyan-950/40 border-cyan-500 text-white'
|
||||
: 'bg-slate-950/60 border-slate-800 text-slate-400 hover:border-slate-700'
|
||||
}`}
|
||||
>
|
||||
<div className="font-bold text-sm text-slate-200">{t.name}</div>
|
||||
<div className="text-xs text-slate-500 mt-1">{t.desc}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-xs font-medium text-slate-400 mb-1.5">初始底仓资金 (Initial Seed WUSD)</label>
|
||||
<input
|
||||
type="number"
|
||||
value={seedAmount}
|
||||
onChange={(e) => setSeedAmount(e.target.value)}
|
||||
placeholder="1000"
|
||||
className="w-full bg-slate-950 border border-slate-700 rounded-lg px-3.5 py-2.5 text-sm text-slate-100 font-mono focus:outline-none focus:border-cyan-500"
|
||||
/>
|
||||
<p className="text-xs text-slate-500 mt-1">底仓资金将用于均匀初始化各选项的初始保证金池。</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Submit */}
|
||||
<div className="pt-4 border-t border-slate-800 flex items-center justify-between">
|
||||
<div className="text-xs text-slate-500">
|
||||
管理员将默认作为预言机 (Oracle) 与治理执行人
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
disabled={loading || !account}
|
||||
className="px-6 py-3 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>
|
||||
|
||||
{/* Status Message */}
|
||||
{statusMsg && (
|
||||
<div
|
||||
className={`p-4 rounded-xl border flex items-start space-x-3 text-sm ${
|
||||
statusMsg.type === 'success'
|
||||
? 'bg-emerald-950/30 border-emerald-800/50 text-emerald-300'
|
||||
: 'bg-rose-950/30 border-rose-800/50 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>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,21 @@
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
module.exports = {
|
||||
content: [
|
||||
'./src/**/*.{js,ts,jsx,tsx,mdx}',
|
||||
'../../packages/ui/src/**/*.{js,ts,jsx,tsx,mdx}'
|
||||
],
|
||||
theme: {
|
||||
extend: {
|
||||
colors: {
|
||||
background: '#090A0F',
|
||||
card: '#12141F',
|
||||
border: '#1F2438',
|
||||
primary: '#3B82F6',
|
||||
success: '#10B981',
|
||||
danger: '#EF4444',
|
||||
warning: '#F59E0B'
|
||||
}
|
||||
}
|
||||
},
|
||||
plugins: []
|
||||
};
|
||||
@@ -0,0 +1,35 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"lib": [
|
||||
"dom",
|
||||
"dom.iterable",
|
||||
"esnext"
|
||||
],
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"strict": false,
|
||||
"noEmit": true,
|
||||
"incremental": true,
|
||||
"module": "esnext",
|
||||
"esModuleInterop": true,
|
||||
"moduleResolution": "node",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"jsx": "preserve",
|
||||
"plugins": [
|
||||
{
|
||||
"name": "next"
|
||||
}
|
||||
]
|
||||
},
|
||||
"include": [
|
||||
"next-env.d.ts",
|
||||
".next/types/**/*.ts",
|
||||
"**/*.ts",
|
||||
"**/*.tsx"
|
||||
],
|
||||
"exclude": [
|
||||
"node_modules"
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user