diff --git a/apps/web/src/app/page.tsx b/apps/web/src/app/page.tsx index e2b32d8..06aba0b 100644 --- a/apps/web/src/app/page.tsx +++ b/apps/web/src/app/page.tsx @@ -10,16 +10,16 @@ import { UserMarketWizard } from '../features/create/UserMarketWizard'; const DEFAULT_MARKET: MarketItem = { id: 'm-1', - marketAddress: '0xc0E24E152771C588B21AEB654b30B1cBAf381c1a', + marketAddress: '0x048E9a90C25ba2c4410425D282b19A472e076039', title: 'Will Bitcoin break above $120,000 before end of Q4 2026?', category: 'Crypto', tier: 1, outcomes: [ - { label: 'YES', price: 0.68, percentage: 68 }, - { label: 'NO', price: 0.32, percentage: 32 } + { label: 'YES', price: 0.50, percentage: 50 }, + { label: 'NO', price: 0.50, percentage: 50 } ], - poolLiquidity: 1250000, - volume24h: 384000, + poolLiquidity: 1000, + volume24h: 0, endTime: '2026-12-31', status: 'ACTIVE' }; diff --git a/apps/web/src/components/TradingViewChart.tsx b/apps/web/src/components/TradingViewChart.tsx index c08faf3..948606f 100644 --- a/apps/web/src/components/TradingViewChart.tsx +++ b/apps/web/src/components/TradingViewChart.tsx @@ -1,14 +1,19 @@ import React, { useEffect, useRef, useState } from 'react'; import { createChart, ColorType, IChartApi, ISeriesApi, CandlestickSeries } from 'lightweight-charts'; -import { Activity, RefreshCw } from 'lucide-react'; +import { Activity } from 'lucide-react'; interface TradingViewChartProps { marketAddress: string; outcomeIndex: number; outcomeLabel: string; - currentPrice: number; + currentPrice: number; // 真实链上边际价 (WUSD/OT),<= 0 表示尚未取得真实价格 } +const API_BASE = 'https://api.wtfx.app'; +const BAR_SECONDS = 5; + +const barTimeOfNow = () => Math.floor(Math.floor(Date.now() / 1000) / BAR_SECONDS) * BAR_SECONDS; + export const TradingViewChart: React.FC = ({ marketAddress, outcomeIndex, @@ -18,12 +23,32 @@ export const TradingViewChart: React.FC = ({ const chartContainerRef = useRef(null); const chartRef = useRef(null); const seriesRef = useRef | null>(null); + // 用 ref 保活最新真实价格,避免 5s 定时器闭包捕获到过期值 + const priceRef = useRef(currentPrice); + const lastTickRef = useRef<{ time: number; price: number } | null>(null); const [timeframe, setTimeframe] = useState('5s'); + // 同步最新真实价格到 ref(同时立即打点一次,保证首根柱出现) + useEffect(() => { + priceRef.current = currentPrice; + const series = seriesRef.current; + if (currentPrice > 0 && series) { + const barTime = barTimeOfNow(); + series.update({ + time: barTime as any, + open: currentPrice, + high: currentPrice, + low: currentPrice, + close: currentPrice, + }); + lastTickRef.current = { time: barTime, price: currentPrice }; + } + }, [currentPrice, outcomeIndex]); + useEffect(() => { if (!chartContainerRef.current) return; - // 1. 初始化 TradingView Lightweight Chart + // 1. 初始化 TradingView 图表 const chart = createChart(chartContainerRef.current, { layout: { background: { type: ColorType.Solid, color: '#090A0F' }, @@ -54,10 +79,9 @@ export const TradingViewChart: React.FC = ({ }, }); - // 2. 添加专业蜡烛图 (Candlestick Series - GMGN / Pump.fun 风格) const candlestickSeries = chart.addSeries(CandlestickSeries, { - upColor: '#10B981', // 涨 (绿色) - downColor: '#F43F5E', // 跌 (红色) + upColor: '#10B981', + downColor: '#F43F5E', borderVisible: false, wickUpColor: '#10B981', wickDownColor: '#F43F5E', @@ -66,26 +90,48 @@ export const TradingViewChart: React.FC = ({ chartRef.current = chart; seriesRef.current = candlestickSeries; - // 3. 生成初始秒级 K 线柱子 - const now = Math.floor(Date.now() / 1000); - const initialData = []; - let p = currentPrice > 0 ? currentPrice : 0.50; + // 2. 从后端 API 异步获取真实 K 线(Indexer 聚合链上成交) + const loadRealKlines = async () => { + try { + const res = await fetch( + `${API_BASE}/api/v1/charts/klines?market_address=${marketAddress}&outcome_index=${outcomeIndex}&timeframe=${timeframe}` + ); + if (res.ok) { + const data = await res.json(); + if (data.bars && data.bars.length > 0) { + candlestickSeries.setData(data.bars); + chart.timeScale().fitContent(); + const last = data.bars[data.bars.length - 1]; + lastTickRef.current = { time: last.time, price: Number(last.close) }; + return; + } + } + } catch (e) { + console.warn('Load klines failed:', e); + } + // 无历史成交:不画任何假柱,等 5s 心跳用真实边际价打点 + }; - for (let i = 80; i >= 0; i--) { - const t = (now - i * 5) as any; - const noise = (Math.sin(i * 0.3) * 0.02) + ((Math.random() - 0.5) * 0.015); - const open = Math.max(0.01, Math.min(0.99, p + noise)); - const close = Math.max(0.01, Math.min(0.99, open + ((Math.random() - 0.48) * 0.02))); - const high = Math.max(open, close) + Math.random() * 0.01; - const low = Math.min(open, close) - Math.random() * 0.01; - initialData.push({ time: t, open, high, low, close }); - p = close; - } + loadRealKlines(); - candlestickSeries.setData(initialData); - chart.timeScale().fitContent(); + // 3. 每 5 秒用最新真实边际价打点(价格未变化则不重复写入,绝不伪造曲线) + const interval = setInterval(() => { + const p = priceRef.current; + const series = seriesRef.current; + if (p <= 0 || !series) return; + const barTime = barTimeOfNow(); + const last = lastTickRef.current; + if (last && last.time === barTime && last.price === p) return; + series.update({ + time: barTime as any, + open: p, + high: p, + low: p, + close: p, + }); + lastTickRef.current = { time: barTime, price: p }; + }, BAR_SECONDS * 1000); - // 4. 自适应窗口大小 const handleResize = () => { if (chartContainerRef.current && chartRef.current) { chartRef.current.applyOptions({ width: chartContainerRef.current.clientWidth }); @@ -94,24 +140,13 @@ export const TradingViewChart: React.FC = ({ window.addEventListener('resize', handleResize); return () => { + clearInterval(interval); window.removeEventListener('resize', handleResize); chart.remove(); + chartRef.current = null; + seriesRef.current = null; }; - }, [marketAddress, outcomeIndex]); - - // 5. 实时价格脉冲更新 - useEffect(() => { - if (seriesRef.current && currentPrice > 0) { - const now = Math.floor(Date.now() / 1000) as any; - seriesRef.current.update({ - time: now, - open: currentPrice, - high: currentPrice * 1.01, - low: currentPrice * 0.99, - close: currentPrice, - }); - } - }, [currentPrice]); + }, [marketAddress, outcomeIndex, timeframe]); return (
@@ -124,16 +159,16 @@ export const TradingViewChart: React.FC = ({
{outcomeLabel} 实时 K 线行情 - ${currentPrice.toFixed(3)} + {currentPrice > 0 ? `$${currentPrice.toFixed(3)}` : '--'}
- TradingView 专业高频连续竞价图表 + 5秒1跳 • 真实链上边际价打点 • 无假数据
{/* Timeframes */}
- {['1s', '5s', '1m', '15m', '1h'].map((tf) => ( + {['5s', '1m', '15m', '1h'].map((tf) => (
- {/* TradingView Chart Container */} + {/* Chart Container */}
); diff --git a/apps/web/src/features/explore/MarketExplore.tsx b/apps/web/src/features/explore/MarketExplore.tsx index c07db72..a7fa54e 100644 --- a/apps/web/src/features/explore/MarketExplore.tsx +++ b/apps/web/src/features/explore/MarketExplore.tsx @@ -17,48 +17,18 @@ export interface MarketItem { const MOCK_FEATURED_MARKETS: MarketItem[] = [ { id: 'm-1', - marketAddress: '0xc0E24E152771C588B21AEB654b30B1cBAf381c1a', + marketAddress: '0x048E9a90C25ba2c4410425D282b19A472e076039', title: 'Will Bitcoin break above $120,000 before end of Q4 2026?', category: 'Crypto', tier: 1, outcomes: [ - { label: 'YES', price: 0.68, percentage: 68 }, - { label: 'NO', price: 0.32, percentage: 32 } + { label: 'YES', price: 0.50, percentage: 50 }, + { label: 'NO', price: 0.50, percentage: 50 } ], - poolLiquidity: 1250000, - volume24h: 384000, + poolLiquidity: 1000, + volume24h: 0, endTime: '2026-12-31', status: 'ACTIVE' - }, - { - id: 'm-2', - marketAddress: '0x2Aeca49669e9E4E6CdCe46A1bc9bf136765A4f75', - title: 'Ethereum Layer-2 Total TVL to reach $50 Billion in 2026?', - category: 'Crypto', - tier: 2, - outcomes: [ - { label: 'YES', price: 0.81, percentage: 81 }, - { label: 'NO', price: 0.19, percentage: 19 } - ], - poolLiquidity: 890000, - volume24h: 192000, - endTime: '2026-11-30', - status: 'ACTIVE' - }, - { - id: 'm-3', - marketAddress: '0xF0E189974c413506098AFB6Bc6Bf4C6715fBf7B1', - title: 'US Federal Reserve Interest Rate cut > 50 bps in next FOMC?', - category: 'Macro', - tier: 1, - outcomes: [ - { label: 'YES (>50bps)', price: 0.35, percentage: 35 }, - { label: 'NO (<=50bps)', price: 0.65, percentage: 65 } - ], - poolLiquidity: 620000, - volume24h: 98000, - endTime: '2026-09-30', - status: 'ACTIVE' } ]; diff --git a/apps/web/src/features/trading/TradingTerminal.tsx b/apps/web/src/features/trading/TradingTerminal.tsx index 070a335..619c433 100644 --- a/apps/web/src/features/trading/TradingTerminal.tsx +++ b/apps/web/src/features/trading/TradingTerminal.tsx @@ -1,59 +1,163 @@ -import React, { useState, useEffect } from 'react'; +import React, { useState, useEffect, useCallback } from 'react'; import { useWeb3 } from '../../context/Web3Context'; import { MarketItem } from '../explore/MarketExplore'; import { DEPLOYMENTS, ABIS } from '@wtfx/contracts'; import { ethers } from 'ethers'; import { TradingViewChart } from '../../components/TradingViewChart'; -import { TrendingUp, ArrowDownUp, Check, AlertCircle, Loader2, Sparkles, Activity, ShieldCheck, Zap } from 'lucide-react'; +import { TrendingUp, Check, AlertCircle, Loader2, Zap } from 'lucide-react'; interface TradingTerminalProps { market: MarketItem; } +// PowerLDACurveV2 读取接口(前端只读,用于精确估算) +const CURVE_ABI = [ + "function calMarginalPrice(address market, uint256 tokenId) view returns (uint256 price)", + "function calMintCostByOtDelta(address market, uint256 tokenId, uint256 otDelta, bytes data) view returns (uint256 collateralFromUser, uint256 collateralToTreasury)", + "function calRedeemValueByOtDelta(address market, uint256 tokenId, uint256 otDelta, bytes data) view returns (uint256 collateralToUser, uint256 collateralToTreasury)", + "function readMarketState(address market, uint256 tokenId) view returns (tuple(address redeem, uint256 premium, uint256 feeRate, uint256 otCurrent, uint256 tick))", +]; + +// 精确到小数点后 6 位的格式化(显示足够、避免浮点噪声) +const fmt = (wei: bigint) => ethers.formatUnits(wei, 18).slice(0, 18).replace(/(\.\d{6})\d+/, '$1').replace(/\.?0+$/, '') || '0'; + export const TradingTerminal: React.FC = ({ market }) => { - const { account, signer, provider, getCollateralContract, wusdBalance, vaultAddress, isSessionActive, refreshBalance, isCorrectNetwork } = useWeb3(); + const { account, signer, provider, getCollateralContract, wusdBalance, refreshBalance, isCorrectNetwork } = useWeb3(); const [selectedOutcomeIndex, setSelectedOutcomeIndex] = useState(0); const [tradeSide, setTradeSide] = useState<'BUY' | 'SELL'>('BUY'); const [amount, setAmount] = useState('100'); const [loading, setLoading] = useState(false); const [statusMsg, setStatusMsg] = useState<{ type: 'success' | 'error'; text: string } | null>(null); - const [dynamicPrice, setDynamicPrice] = useState(market.outcomes[0]?.price || 0.50); + const [marginalPrices, setMarginalPrices] = useState([]); + const [dynamicPrice, setDynamicPrice] = useState(0); + const [otBalance, setOtBalance] = useState('0'); + const [preview, setPreview] = useState<{ kind: 'buy' | 'sell' | 'error'; ot: string; cost: string; fee: string } | null>(null); + + const curveAddress = DEPLOYMENTS.robinhoodTestnet.contracts.powerLDACurve; + const tokenId = 1n << BigInt(selectedOutcomeIndex); const activeOutcome = market.outcomes[selectedOutcomeIndex] || { label: `选项 ${selectedOutcomeIndex}`, price: dynamicPrice, percentage: 50 }; - // 1. 读取 PowerLDACurveV2 链上实时边际计价 (calMarginalPrice) - const fetchLivePrice = async () => { + const getCurveContract = useCallback( + (runner: ethers.Provider | ethers.Signer) => new ethers.Contract(curveAddress, CURVE_ABI, runner), + [curveAddress] + ); + + // 1. 读取链上真实边际计价 (calMarginalPrice),覆盖全部结果选项 + const fetchLivePrice = useCallback(async () => { if (!provider || !market.marketAddress) return; try { - const curveAddr = DEPLOYMENTS.robinhoodTestnet.contracts.powerLDACurve; - const curveAbi = [ - "function calMarginalPrice(address market, uint256 tokenId) external view returns (uint256 price)" - ]; - const curveContract = new ethers.Contract(curveAddr, curveAbi, provider); - // tokenId = 2 ** outcomeIndex - const tokenId = 1n << BigInt(selectedOutcomeIndex); - const priceWei = await curveContract.calMarginalPrice(market.marketAddress, tokenId); - const priceNum = parseFloat(ethers.formatUnits(priceWei, 18)); - if (priceNum > 0) { - setDynamicPrice(priceNum); + const curve = getCurveContract(provider); + const prices: number[] = []; + for (let i = 0; i < market.outcomes.length; i++) { + const tid = 1n << BigInt(i); + const priceWei = await curve.calMarginalPrice(market.marketAddress, tid); + prices[i] = priceWei > 0n ? parseFloat(ethers.formatUnits(priceWei, 18)) : 0; + } + setMarginalPrices(prices); + if (prices[selectedOutcomeIndex] && prices[selectedOutcomeIndex] > 0) { + setDynamicPrice(prices[selectedOutcomeIndex]); } } catch (e) { - console.warn('Fallback to market default price:', e); + console.warn('Fetch live price failed:', e); } - }; + }, [provider, market.marketAddress, market.outcomes.length, selectedOutcomeIndex, getCurveContract]); + + // 2. 读取用户持有的该结果 OT 份额 (ERC6909) + const fetchOtBalance = useCallback(async () => { + if ((!provider && !signer) || !account || !market.marketAddress) return; + try { + const marketContract = new ethers.Contract(market.marketAddress, ABIS.WTFMarketV2, signer || provider); + const bal = await marketContract.balanceOf(account, tokenId); + setOtBalance(ethers.formatUnits(bal, 18)); + } catch (e) { + console.warn('Fetch OT balance failed:', e); + } + }, [provider, signer, account, market.marketAddress, tokenId]); useEffect(() => { fetchLivePrice(); + fetchOtBalance(); const timer = setInterval(fetchLivePrice, 4000); return () => clearInterval(timer); - }, [market.marketAddress, selectedOutcomeIndex, provider]); + }, [market.marketAddress, selectedOutcomeIndex, provider, fetchLivePrice, fetchOtBalance]); - const estShares = amount && Number(amount) > 0 ? (Number(amount) / (dynamicPrice || 0.5)).toFixed(2) : '0'; - const estPayout = (Number(estShares) * 1.0).toFixed(2); - const estReturn = amount && Number(amount) > 0 ? (((Number(estPayout) - Number(amount)) / Number(amount)) * 100).toFixed(1) : '0'; + // 3. 实时精确预估:BUY = 用预算二分对齐 tick 求最大可得 OT;SELL = 输入 OT 对齐 tick 求赎回值 + useEffect(() => { + let cancelled = false; + const timer = setTimeout(async () => { + if (!provider || !market.marketAddress || !amount || Number(amount) <= 0) { + setPreview(null); + return; + } + try { + const curve = getCurveContract(provider); + const tid = 1n << BigInt(selectedOutcomeIndex); + const state = await curve.readMarketState(market.marketAddress, tid); + const tick = BigInt(state.tick) > 0n ? BigInt(state.tick) : 1n; + const priceWei = await curve.calMarginalPrice(market.marketAddress, tid); + const budgetWei = ethers.parseUnits(amount, 18); - // 2. 真金白银链上下单 / 0 弹窗极速交易 + if (tradeSide === 'BUY') { + if (priceWei <= 0n) { + if (!cancelled) setPreview({ kind: 'error', ot: '', cost: '0', fee: '0' }); + return; + } + // 估算 OT = 预算 / 边际价,向下对齐 tick + let otWei = (budgetWei * 10n ** 18n) / priceWei; + otWei = (otWei / tick) * tick; + if (otWei <= 0n) { + if (!cancelled) setPreview({ kind: 'error', ot: '', cost: '0', fee: '0' }); + return; + } + let [cost, fee] = await curve.calMintCostByOtDelta(market.marketAddress, tid, otWei, "0x"); + if (cost > budgetWei && otWei > tick) { + otWei -= tick; + [cost, fee] = await curve.calMintCostByOtDelta(market.marketAddress, tid, otWei, "0x"); + } + if (!cancelled) setPreview({ kind: 'buy', ot: otWei.toString(), cost: cost.toString(), fee: fee.toString() }); + } else { + const otWei = ethers.parseUnits(amount, 18); + if (otWei <= 0n || otWei % tick !== 0n) { + if (!cancelled) setPreview({ kind: 'error', ot: '', cost: '0', fee: '0' }); + return; + } + const [toUser, fee] = await curve.calRedeemValueByOtDelta(market.marketAddress, tid, otWei, "0x"); + if (!cancelled) setPreview({ kind: 'sell', ot: otWei.toString(), cost: toUser.toString(), fee: fee.toString() }); + } + } catch (e) { + if (!cancelled) setPreview(null); + } + }, 300); + return () => { + cancelled = true; + clearTimeout(timer); + }; + }, [amount, tradeSide, selectedOutcomeIndex, market.marketAddress, provider, getCurveContract, marginalPrices]); + + // 4. 解码真实 revert 原因(自定义错误优先) + const describeError = (err: any): string => { + if (!err) return '未知错误'; + if (err.reason) return `链上拒绝:${err.reason}`; + if (err.shortMessage) return err.shortMessage; + if (err.data) { + try { + const marketContract = new ethers.Contract(market.marketAddress, ABIS.WTFMarketV2, signer || provider); + const parsed = marketContract.interface.parseError(err.data); + if (parsed) { + const args = parsed.args.map((a: any) => (typeof a === 'bigint' ? a.toString() : String(a))).join(', '); + return `链上拒绝:${parsed.name}(${args})`; + } + } catch { + /* ignore decode failure */ + } + } + const msg = err.message || String(err); + return msg.split('\n')[0]; + }; + + // 5. 真金白银链上下单 const handleTrade = async (e: React.FormEvent) => { e.preventDefault(); if (!signer || !account) { @@ -72,56 +176,86 @@ export const TradingTerminal: React.FC = ({ market }) => { const collateral = getCollateralContract(); if (!collateral) throw new Error('Collateral contract not loaded'); - const amountWei = ethers.parseUnits(amount, 18); - const tokenId = 1n << BigInt(selectedOutcomeIndex); - const otDeltaOut = ethers.parseUnits(estShares, 18); - - // WTFMarketV2 合约实例 const marketContract = new ethers.Contract(market.marketAddress, ABIS.WTFMarketV2, signer); + const curve = getCurveContract(signer); + const tid = 1n << BigInt(selectedOutcomeIndex); + const state = await curve.readMarketState(market.marketAddress, tid); + const tick = BigInt(state.tick) > 0n ? BigInt(state.tick) : 1n; if (tradeSide === 'BUY') { - // Step 1: Approve Collateral to Market - setStatusMsg({ type: 'success', text: '步骤 1/2: 正在授权 WUSD 扣款...' }); - const appTx = await collateral.approve(market.marketAddress, ethers.MaxUint256); - await appTx.wait(); + const budgetWei = ethers.parseUnits(amount, 18); + const priceWei = await curve.calMarginalPrice(market.marketAddress, tid); + if (priceWei <= 0n) throw new Error('边际价格不可用,请稍后重试'); + + let otWei = (budgetWei * 10n ** 18n) / priceWei; + otWei = (otWei / tick) * tick; + if (otWei <= 0n) { + const minCost = await curve.calMintCostByOtDelta(market.marketAddress, tid, tick, "0x"); + throw new Error(`金额过小:单笔最小为 1 tick(约 ${fmt(minCost[0])} WUSD)`); + } + let [cost, fee] = await curve.calMintCostByOtDelta(market.marketAddress, tid, otWei, "0x"); + if (cost > budgetWei && otWei > tick) { + otWei -= tick; + [cost, fee] = await curve.calMintCostByOtDelta(market.marketAddress, tid, otWei, "0x"); + } + if (cost > budgetWei) throw new Error('金额不足以覆盖最小 tick 成本'); + + // Step 1: 仅当授权不足时才触发 approve(避免每次都弹授权) + const allowance = await collateral.allowance(account, market.marketAddress); + if (allowance < cost) { + setStatusMsg({ type: 'success', text: '步骤 1/2: 请在钱包确认授权 WUSD 给市场合约(仅首次需要)' }); + const appTx = await collateral.approve(market.marketAddress, ethers.MaxUint256); + await appTx.wait(); + } // Step 2: 真实调用 WTFMarketV2.mintCollateralToExactOt() - setStatusMsg({ type: 'success', text: `步骤 2/2: 正在链上买入 ${estShares} 份 ${activeOutcome.label} 预测代币...` }); - const tx = await marketContract.mintCollateralToExactOt( - account, - tokenId, - otDeltaOut, - "0x" // swapData - ); + setStatusMsg({ + type: 'success', + text: `步骤 2/2: 链上买入 ${fmt(otWei)} 份 ${activeOutcome.label},成本约 ${fmt(cost)} WUSD...`, + }); + const tx = await marketContract.mintCollateralToExactOt(account, tid, otWei, "0x"); const receipt = await tx.wait(); setStatusMsg({ type: 'success', - text: `🎉 真实链上下单成功!已通过 Power LDA 曲线买入 ${estShares} 份,哈希: ${receipt.hash.slice(0, 10)}...` + text: `买入成功:${fmt(otWei)} 份 ${activeOutcome.label}(成本 ${fmt(cost)} WUSD,手续费 ${fmt(fee)}),哈希 ${receipt.hash.slice(0, 10)}...`, }); } else { - // 卖出 / 赎回 - setStatusMsg({ type: 'success', text: '正在赎回预测代币并结转 WUSD 保证金...' }); - const tx = await marketContract.redeemExactOtToCollateral( - account, - tokenId, - otDeltaOut, - "0x" - ); - await tx.wait(); - setStatusMsg({ type: 'success', text: `🎉 成功卖出/赎回 ${estShares} 份预测代币!` }); + // 卖出 = 精确赎回 OT -> WUSD + const otWei = ethers.parseUnits(amount, 18); + if (otWei <= 0n || otWei % tick !== 0n) { + throw new Error(`卖出数量必须为 tick(${tick.toString()})的整数倍`); + } + const bal = await marketContract.balanceOf(account, tid); + if (bal < otWei) { + throw new Error(`OT 余额不足:当前持有 ${fmt(bal)} 份,需卖出 ${fmt(otWei)} 份`); + } + const [toUser, fee] = await curve.calRedeemValueByOtDelta(market.marketAddress, tid, otWei, "0x"); + setStatusMsg({ + type: 'success', + text: `链上卖出 ${fmt(otWei)} 份 ${activeOutcome.label},预估到账 ${fmt(toUser)} WUSD...`, + }); + const tx = await marketContract.redeemExactOtToCollateral(account, tid, otWei, "0x"); + const receipt = await tx.wait(); + setStatusMsg({ + type: 'success', + text: `卖出成功:${fmt(otWei)} 份 → 到账 ${fmt(toUser)} WUSD(手续费 ${fmt(fee)}),哈希 ${receipt.hash.slice(0, 10)}...`, + }); } - // 刷新余额与实时曲线价格 + // 刷新余额与链上价格 refreshBalance(); fetchLivePrice(); + fetchOtBalance(); } catch (err: any) { console.error('Trade execution error:', err); - setStatusMsg({ type: 'error', text: err.reason || err.message || '交易上链失败,请检查资金与网络' }); + setStatusMsg({ type: 'error', text: describeError(err) }); } finally { setLoading(false); } }; + const isBuy = tradeSide === 'BUY'; + return (
{/* 左侧 2 列:市场头部 + TradingView 级 K 线图表 */} @@ -151,8 +285,10 @@ export const TradingTerminal: React.FC = ({ market }) => { ${market.poolLiquidity.toLocaleString()}
- 实时边际价格 - ${dynamicPrice.toFixed(3)} + 实时边际价格 (WUSD/OT) + + {dynamicPrice > 0 ? `$${dynamicPrice.toFixed(3)}` : '--'} +
创作者返佣 @@ -161,7 +297,7 @@ export const TradingTerminal: React.FC = ({ market }) => {
- {/* TradingView 专业图表组件 (GMGN / Pump.fun 风格) */} + {/* TradingView 专业图表组件 */} = ({ market }) => { /> - {/* 右侧:极速下单与非托管金库连击 */} + {/* 右侧:极速下单面板 */}
@@ -185,7 +321,7 @@ export const TradingTerminal: React.FC = ({ market }) => { type="button" onClick={() => setTradeSide('BUY')} className={`px-3 py-1 text-xs font-bold rounded-lg transition-all ${ - tradeSide === 'BUY' ? 'bg-cyan-600 text-white' : 'text-slate-400 hover:text-slate-200' + isBuy ? 'bg-cyan-600 text-white' : 'text-slate-400 hover:text-slate-200' }`} > 买入 (Buy) @@ -194,7 +330,7 @@ export const TradingTerminal: React.FC = ({ market }) => { type="button" onClick={() => setTradeSide('SELL')} className={`px-3 py-1 text-xs font-bold rounded-lg transition-all ${ - tradeSide === 'SELL' ? 'bg-rose-600 text-white' : 'text-slate-400 hover:text-slate-200' + !isBuy ? 'bg-rose-600 text-white' : 'text-slate-400 hover:text-slate-200' }`} > 卖出 (Sell) @@ -219,7 +355,7 @@ export const TradingTerminal: React.FC = ({ market }) => { >
{outcome.label}
- ${selectedOutcomeIndex === idx ? dynamicPrice.toFixed(3) : outcome.price.toFixed(2)} + {marginalPrices[idx] > 0 ? `$${marginalPrices[idx].toFixed(3)}` : '--'}
))} @@ -229,24 +365,34 @@ export const TradingTerminal: React.FC = ({ market }) => { {/* 交易金额输入 */}
- 交易金额 (WUSD) - 余额: {Number(wusdBalance).toFixed(2)} WUSD + + {isBuy ? '买入金额 (WUSD)' : '卖出份额 (OT)'} + + + {isBuy + ? `余额: ${Number(wusdBalance).toFixed(2)} WUSD` + : `持有: ${Number(otBalance).toFixed(4)} OT`} +
setAmount(e.target.value)} - placeholder="100" + placeholder={isBuy ? '100' : '100'} className="w-full bg-slate-950 border border-slate-700 rounded-xl px-4 py-3 text-base text-slate-100 font-mono focus:outline-none focus:border-cyan-500" />
- {['50', '100', '500', 'MAX'].map((preset) => ( + {['50', '100', '500', isBuy ? 'MAX' : 'ALL'].map((preset) => (
- {/* 实时预估 */} + {/* 实时预估(基于链上曲线精确计算) */}
-
- 预估成交份额 - {estShares} 份 -
-
- 若获胜最终兑付 - ${estPayout} WUSD -
-
- 预期收益率 (ROI) - +{estReturn}% -
+ {preview && preview.kind === 'buy' && ( + <> +
+ 预估成交份额 + {fmt(BigInt(preview.ot))} OT +
+
+ 预估成本 + {fmt(BigInt(preview.cost))} WUSD +
+
+ 其中手续费 + {fmt(BigInt(preview.fee))} WUSD +
+ + )} + {preview && preview.kind === 'sell' && ( + <> +
+ 卖出份额 + {fmt(BigInt(preview.ot))} OT +
+
+ 预估到账 + {fmt(BigInt(preview.cost))} WUSD +
+
+ 其中手续费 + {fmt(BigInt(preview.fee))} WUSD +
+ + )} + {preview && preview.kind === 'error' && ( +
金额不可用:请按链上 tick 与边际价调整
+ )} + {!preview &&
输入金额后自动按链上曲线精确估算...
}
{/* 下单按钮 */} diff --git a/packages/contracts/src/robinhoodTestnet.json b/packages/contracts/src/robinhoodTestnet.json index f576df4..20bbf0c 100644 --- a/packages/contracts/src/robinhoodTestnet.json +++ b/packages/contracts/src/robinhoodTestnet.json @@ -8,7 +8,8 @@ "powerLDACurve": "0xF0E189974c413506098AFB6Bc6Bf4C6715fBf7B1", "controllerImplementation": "0x2Aeca49669e9E4E6CdCe46A1bc9bf136765A4f75", "controllerProxy": "0xc0E24E152771C588B21AEB654b30B1cBAf381c1a", - "vaultFactory": "0x89401e07296267c01017cA150D5Ec8883a78e0B2" + "vaultFactory": "0x89401e07296267c01017cA150D5Ec8883a78e0B2", + "defaultMarket": "0x048E9a90C25ba2c4410425D282b19A472e076039" }, "governance": { "admin": "0x6cddF384792C77219fc25C454Cfe264757842830",