diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3cf8cec --- /dev/null +++ b/.gitignore @@ -0,0 +1,8 @@ +node_modules +.next +dist +.turbo +*.log +.env +.env*.local +pnpm-debug.log* diff --git a/apps/admin/next-env.d.ts b/apps/admin/next-env.d.ts new file mode 100644 index 0000000..40c3d68 --- /dev/null +++ b/apps/admin/next-env.d.ts @@ -0,0 +1,5 @@ +/// +/// + +// NOTE: This file should not be edited +// see https://nextjs.org/docs/app/building-your-application/configuring/typescript for more information. diff --git a/apps/admin/package.json b/apps/admin/package.json index ed1f7b9..a8a82f9 100644 --- a/apps/admin/package.json +++ b/apps/admin/package.json @@ -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" } -} \ No newline at end of file +} diff --git a/apps/admin/postcss.config.js b/apps/admin/postcss.config.js new file mode 100644 index 0000000..12a703d --- /dev/null +++ b/apps/admin/postcss.config.js @@ -0,0 +1,6 @@ +module.exports = { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +}; diff --git a/apps/admin/src/app/globals.css b/apps/admin/src/app/globals.css new file mode 100644 index 0000000..3f67e58 --- /dev/null +++ b/apps/admin/src/app/globals.css @@ -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; +} diff --git a/apps/admin/src/app/layout.tsx b/apps/admin/src/app/layout.tsx new file mode 100644 index 0000000..e1abab3 --- /dev/null +++ b/apps/admin/src/app/layout.tsx @@ -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 ( + + + {children} + + + ); +} diff --git a/apps/admin/src/app/page.tsx b/apps/admin/src/app/page.tsx new file mode 100644 index 0000000..5c191bd --- /dev/null +++ b/apps/admin/src/app/page.tsx @@ -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('faucet'); + + return ( + +
+ + +
+ {activeTab === 'faucet' && } + {activeTab === 'wizard' && } + {activeTab === 'governance' && } + {activeTab === 'adjudication' && } +
+ +
+ WTFX Protocol Governance & Administration Console • Robinhood Chain Testnet (ID: 46630) +
+
+
+ ); +} diff --git a/apps/admin/src/context/Web3Context.tsx b/apps/admin/src/context/Web3Context.tsx new file mode 100644 index 0000000..8ab525a --- /dev/null +++ b/apps/admin/src/context/Web3Context.tsx @@ -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; + switchNetwork: () => Promise; + getControllerContract: () => ethers.Contract | null; + getCollateralContract: () => ethers.Contract | null; +} + +const Web3Context = createContext({ + 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(null); + const [provider, setProvider] = useState(null); + const [signer, setSigner] = useState(null); + const [chainId, setChainId] = useState(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 ( + + {children} + + ); +}; + +export const useWeb3 = () => useContext(Web3Context); diff --git a/apps/admin/src/features/adjudication/AdjudicationPanel.tsx b/apps/admin/src/features/adjudication/AdjudicationPanel.tsx new file mode 100644 index 0000000..6297839 --- /dev/null +++ b/apps/admin/src/features/adjudication/AdjudicationPanel.tsx @@ -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(0); + const [marketAddress, setMarketAddress] = useState(''); + const [loading, setLoading] = useState(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 ( +
+
+
+
+
+ +
+
+

市场风控与裁决中心 (Adjudication & Emergency)

+

+ 预言机初审、胜出结果终裁敲定、超管一票否决与全局熔断保护 +

+
+
+
+ + {/* Question Outcome Adjudication */} +
+

1. 市场结果裁决 (Outcome Resolution)

+ +
+
+ + 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" + /> +
+
+ + 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" + /> +
+
+ +
+ + + + + +
+
+ + {/* Protocol Pause Control */} +
+

+ + 2. 协议紧急熔断控制 (Circuit Breaker) +

+

+ 当链上异常或智能合约升级前夕,GUARDIAN_ROLE 与 ADMIN 可一键暂停全协议建盘与交易。 +

+ +
+ + + +
+
+ + {/* Status Message */} + {statusMsg && ( +
+ {statusMsg.type === 'success' ? ( + + ) : ( + + )} +
+

{statusMsg.text}

+
+
+ )} +
+
+ ); +}; diff --git a/apps/admin/src/features/common/AdminNavbar.tsx b/apps/admin/src/features/common/AdminNavbar.tsx new file mode 100644 index 0000000..ce1dd19 --- /dev/null +++ b/apps/admin/src/features/common/AdminNavbar.tsx @@ -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 = ({ activeTab, setActiveTab }) => { + const { account, isCorrectNetwork, connectWallet, switchNetwork } = useWeb3(); + + const navItems = [ + { id: 'faucet', label: '🚰 WUSD 水龙头' }, + { id: 'wizard', label: '🚀 创建市场向导' }, + { id: 'governance', label: '⚙️ 协议参数治理' }, + { id: 'adjudication', label: '⚖️ 市场风控与裁决' }, + ]; + + return ( +
+
+
+
+
+ W +
+
+ WTFX + + ADMIN CONSOLE + +
+
+ + +
+ +
+ {account ? ( +
+ {!isCorrectNetwork ? ( + + ) : ( + + + Robinhood 46630 + + )} + +
+
+ + {account.slice(0, 6)}...{account.slice(-4)} + +
+
+ ) : ( + + )} +
+
+
+ ); +}; diff --git a/apps/admin/src/features/faucet/FaucetPanel.tsx b/apps/admin/src/features/faucet/FaucetPanel.tsx new file mode 100644 index 0000000..3de82f3 --- /dev/null +++ b/apps/admin/src/features/faucet/FaucetPanel.tsx @@ -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('0'); + const [loading, setLoading] = useState(false); + const [customAmount, setCustomAmount] = useState('100000000'); + const [targetAddress, setTargetAddress] = useState(''); + const [txHash, setTxHash] = useState(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 ( +
+
+
+ +
+
+
+ +
+
+

WUSD 领水水龙头 (Testnet Faucet)

+

+ Mock ERC-20 结算通证地址: {collateralAddress} +

+
+
+ +
+ 当前钱包余额 + + {Number(balance).toLocaleString(undefined, { maximumFractionDigits: 2 })} WUSD + +
+
+ +
+ {/* Quick Action */} +
+
+
+ +

管理员一键领水 (100,000,000 WUSD)

+
+

+ 为做市、流动性注入、初始化预测市场底仓提供充足的 1 亿测试代币。 +

+
+ +
+ + {/* Custom Mint Form */} +
+

自定义铸造 (Custom Mint)

+ +
+
+ + 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" + /> +
+
+ + 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" + /> +
+
+ +
+ +
+
+ + {/* Status Message */} + {statusMsg && ( +
+ {statusMsg.type === 'success' ? ( + + ) : ( + + )} +
+

{statusMsg.text}

+ {txHash && ( +

+ Tx Hash: {txHash} +

+ )} +
+
+ )} +
+
+
+ ); +}; diff --git a/apps/admin/src/features/governance/GovernancePanel.tsx b/apps/admin/src/features/governance/GovernancePanel.tsx new file mode 100644 index 0000000..dde0fc7 --- /dev/null +++ b/apps/admin/src/features/governance/GovernancePanel.tsx @@ -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(''); + const [defaultFeeRate, setDefaultFeeRate] = useState('0.6'); + const [creatorShare, setCreatorShare] = useState('50'); + const [centralWallet, setCentralWallet] = useState(''); + const [loading, setLoading] = useState(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 ( +
+
+
+
+
+ +
+
+

协议治理看板 (Protocol Governance)

+

+ WTFControllerV2 智能合约超管参数配置(仅 DEFAULT_ADMIN_ROLE / OPERATOR_ROLE 可执行) +

+
+
+ + +
+ +
+ {/* Treasury Setting */} +
+
+ + 协议国库金库 (Treasury Address) +
+

用于接收协议留存手续费分润的冷钱包/金库合约。

+ 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" + /> + +
+ + {/* Central Wallet */} +
+
+ + 中央安全提币钱包 (Central Wallet) +
+

由超管授权以自动化划拨协议手续费的专用钱包。

+ 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" + /> + +
+ + {/* Default Fee Rate */} +
+
+ + 默认协议手续费率 (Fee Rate %) +
+

新建市场默认费率(范围 [0.1%, 3.0%],默认 0.6%)。

+ 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" + /> + +
+ + {/* Creator Share */} +
+
+ + 建盘者返佣分成比例 (Creator Share %) +
+

交易产生的手续费中分给市场创建者的比例(默认 50%)。

+ 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" + /> + +
+
+ + {/* Status Message */} + {statusMsg && ( +
+ {statusMsg.type === 'success' ? ( + + ) : ( + + )} +
+

{statusMsg.text}

+
+
+ )} +
+
+ ); +}; diff --git a/apps/admin/src/features/wizard/MarketWizard.tsx b/apps/admin/src/features/wizard/MarketWizard.tsx new file mode 100644 index 0000000..9a6c847 --- /dev/null +++ b/apps/admin/src/features/wizard/MarketWizard.tsx @@ -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(['YES', 'NO']); + const [selectedTier, setSelectedTier] = useState(1); + const [seedAmount, setSeedAmount] = useState('1000'); + const [loading, setLoading] = useState(false); + const [statusMsg, setStatusMsg] = useState<{ type: 'success' | 'error'; text: string } | null>(null); + const [deployedMarketAddress, setDeployedMarketAddress] = useState(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 ( +
+
+
+
+
+ +
+
+

创建预测市场向导 (Deploy Market Wizard)

+

+ 基于 WTFControllerV2 与 PowerLDACurveV2 幂律联合曲线一键创建全套预测市场 +

+
+
+
+ + {/* Basic Info */} +
+

1. 市场基本信息

+ +
+ + 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" + /> +
+ +
+
+ + +
+ +
+ + 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" + > + +
+
+ +
+ +