fix(web): real on-chain trading + honest 5s K-line
- TradingTerminal: allowance precheck (approve only when needed), exact OT estimation via calMintCostByOtDelta aligned to tick, real revert reason decoding, SELL requires OT balance - TradingViewChart: priceRef to avoid stale closure, no synthetic candles, 5s heartbeat with real marginal price - point default market to real WTFMarketV2 (0x048E..9039) instead of controller proxy
This commit is contained in:
@@ -10,16 +10,16 @@ import { UserMarketWizard } from '../features/create/UserMarketWizard';
|
|||||||
|
|
||||||
const DEFAULT_MARKET: MarketItem = {
|
const DEFAULT_MARKET: MarketItem = {
|
||||||
id: 'm-1',
|
id: 'm-1',
|
||||||
marketAddress: '0xc0E24E152771C588B21AEB654b30B1cBAf381c1a',
|
marketAddress: '0x048E9a90C25ba2c4410425D282b19A472e076039',
|
||||||
title: 'Will Bitcoin break above $120,000 before end of Q4 2026?',
|
title: 'Will Bitcoin break above $120,000 before end of Q4 2026?',
|
||||||
category: 'Crypto',
|
category: 'Crypto',
|
||||||
tier: 1,
|
tier: 1,
|
||||||
outcomes: [
|
outcomes: [
|
||||||
{ label: 'YES', price: 0.68, percentage: 68 },
|
{ label: 'YES', price: 0.50, percentage: 50 },
|
||||||
{ label: 'NO', price: 0.32, percentage: 32 }
|
{ label: 'NO', price: 0.50, percentage: 50 }
|
||||||
],
|
],
|
||||||
poolLiquidity: 1250000,
|
poolLiquidity: 1000,
|
||||||
volume24h: 384000,
|
volume24h: 0,
|
||||||
endTime: '2026-12-31',
|
endTime: '2026-12-31',
|
||||||
status: 'ACTIVE'
|
status: 'ACTIVE'
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,14 +1,19 @@
|
|||||||
import React, { useEffect, useRef, useState } from 'react';
|
import React, { useEffect, useRef, useState } from 'react';
|
||||||
import { createChart, ColorType, IChartApi, ISeriesApi, CandlestickSeries } from 'lightweight-charts';
|
import { createChart, ColorType, IChartApi, ISeriesApi, CandlestickSeries } from 'lightweight-charts';
|
||||||
import { Activity, RefreshCw } from 'lucide-react';
|
import { Activity } from 'lucide-react';
|
||||||
|
|
||||||
interface TradingViewChartProps {
|
interface TradingViewChartProps {
|
||||||
marketAddress: string;
|
marketAddress: string;
|
||||||
outcomeIndex: number;
|
outcomeIndex: number;
|
||||||
outcomeLabel: string;
|
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<TradingViewChartProps> = ({
|
export const TradingViewChart: React.FC<TradingViewChartProps> = ({
|
||||||
marketAddress,
|
marketAddress,
|
||||||
outcomeIndex,
|
outcomeIndex,
|
||||||
@@ -18,12 +23,32 @@ export const TradingViewChart: React.FC<TradingViewChartProps> = ({
|
|||||||
const chartContainerRef = useRef<HTMLDivElement>(null);
|
const chartContainerRef = useRef<HTMLDivElement>(null);
|
||||||
const chartRef = useRef<IChartApi | null>(null);
|
const chartRef = useRef<IChartApi | null>(null);
|
||||||
const seriesRef = useRef<ISeriesApi<'Candlestick'> | null>(null);
|
const seriesRef = useRef<ISeriesApi<'Candlestick'> | null>(null);
|
||||||
|
// 用 ref 保活最新真实价格,避免 5s 定时器闭包捕获到过期值
|
||||||
|
const priceRef = useRef<number>(currentPrice);
|
||||||
|
const lastTickRef = useRef<{ time: number; price: number } | null>(null);
|
||||||
const [timeframe, setTimeframe] = useState<string>('5s');
|
const [timeframe, setTimeframe] = useState<string>('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(() => {
|
useEffect(() => {
|
||||||
if (!chartContainerRef.current) return;
|
if (!chartContainerRef.current) return;
|
||||||
|
|
||||||
// 1. 初始化 TradingView Lightweight Chart
|
// 1. 初始化 TradingView 图表
|
||||||
const chart = createChart(chartContainerRef.current, {
|
const chart = createChart(chartContainerRef.current, {
|
||||||
layout: {
|
layout: {
|
||||||
background: { type: ColorType.Solid, color: '#090A0F' },
|
background: { type: ColorType.Solid, color: '#090A0F' },
|
||||||
@@ -54,10 +79,9 @@ export const TradingViewChart: React.FC<TradingViewChartProps> = ({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
// 2. 添加专业蜡烛图 (Candlestick Series - GMGN / Pump.fun 风格)
|
|
||||||
const candlestickSeries = chart.addSeries(CandlestickSeries, {
|
const candlestickSeries = chart.addSeries(CandlestickSeries, {
|
||||||
upColor: '#10B981', // 涨 (绿色)
|
upColor: '#10B981',
|
||||||
downColor: '#F43F5E', // 跌 (红色)
|
downColor: '#F43F5E',
|
||||||
borderVisible: false,
|
borderVisible: false,
|
||||||
wickUpColor: '#10B981',
|
wickUpColor: '#10B981',
|
||||||
wickDownColor: '#F43F5E',
|
wickDownColor: '#F43F5E',
|
||||||
@@ -66,26 +90,48 @@ export const TradingViewChart: React.FC<TradingViewChartProps> = ({
|
|||||||
chartRef.current = chart;
|
chartRef.current = chart;
|
||||||
seriesRef.current = candlestickSeries;
|
seriesRef.current = candlestickSeries;
|
||||||
|
|
||||||
// 3. 生成初始秒级 K 线柱子
|
// 2. 从后端 API 异步获取真实 K 线(Indexer 聚合链上成交)
|
||||||
const now = Math.floor(Date.now() / 1000);
|
const loadRealKlines = async () => {
|
||||||
const initialData = [];
|
try {
|
||||||
let p = currentPrice > 0 ? currentPrice : 0.50;
|
const res = await fetch(
|
||||||
|
`${API_BASE}/api/v1/charts/klines?market_address=${marketAddress}&outcome_index=${outcomeIndex}&timeframe=${timeframe}`
|
||||||
for (let i = 80; i >= 0; i--) {
|
);
|
||||||
const t = (now - i * 5) as any;
|
if (res.ok) {
|
||||||
const noise = (Math.sin(i * 0.3) * 0.02) + ((Math.random() - 0.5) * 0.015);
|
const data = await res.json();
|
||||||
const open = Math.max(0.01, Math.min(0.99, p + noise));
|
if (data.bars && data.bars.length > 0) {
|
||||||
const close = Math.max(0.01, Math.min(0.99, open + ((Math.random() - 0.48) * 0.02)));
|
candlestickSeries.setData(data.bars);
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
candlestickSeries.setData(initialData);
|
|
||||||
chart.timeScale().fitContent();
|
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 心跳用真实边际价打点
|
||||||
|
};
|
||||||
|
|
||||||
|
loadRealKlines();
|
||||||
|
|
||||||
|
// 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 = () => {
|
const handleResize = () => {
|
||||||
if (chartContainerRef.current && chartRef.current) {
|
if (chartContainerRef.current && chartRef.current) {
|
||||||
chartRef.current.applyOptions({ width: chartContainerRef.current.clientWidth });
|
chartRef.current.applyOptions({ width: chartContainerRef.current.clientWidth });
|
||||||
@@ -94,24 +140,13 @@ export const TradingViewChart: React.FC<TradingViewChartProps> = ({
|
|||||||
window.addEventListener('resize', handleResize);
|
window.addEventListener('resize', handleResize);
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
|
clearInterval(interval);
|
||||||
window.removeEventListener('resize', handleResize);
|
window.removeEventListener('resize', handleResize);
|
||||||
chart.remove();
|
chart.remove();
|
||||||
|
chartRef.current = null;
|
||||||
|
seriesRef.current = null;
|
||||||
};
|
};
|
||||||
}, [marketAddress, outcomeIndex]);
|
}, [marketAddress, outcomeIndex, timeframe]);
|
||||||
|
|
||||||
// 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]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="bg-[#0f111d] border border-slate-800 rounded-3xl p-5 shadow-2xl space-y-4">
|
<div className="bg-[#0f111d] border border-slate-800 rounded-3xl p-5 shadow-2xl space-y-4">
|
||||||
@@ -124,16 +159,16 @@ export const TradingViewChart: React.FC<TradingViewChartProps> = ({
|
|||||||
<div className="flex items-center space-x-2">
|
<div className="flex items-center space-x-2">
|
||||||
<span className="font-extrabold text-sm text-white">{outcomeLabel} 实时 K 线行情</span>
|
<span className="font-extrabold text-sm text-white">{outcomeLabel} 实时 K 线行情</span>
|
||||||
<span className="font-mono text-xs font-bold px-2 py-0.5 rounded bg-emerald-950/60 text-emerald-400 border border-emerald-800/50">
|
<span className="font-mono text-xs font-bold px-2 py-0.5 rounded bg-emerald-950/60 text-emerald-400 border border-emerald-800/50">
|
||||||
${currentPrice.toFixed(3)}
|
{currentPrice > 0 ? `$${currentPrice.toFixed(3)}` : '--'}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<span className="text-[11px] text-slate-400">TradingView 专业高频连续竞价图表</span>
|
<span className="text-[11px] text-slate-400">5秒1跳 • 真实链上边际价打点 • 无假数据</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Timeframes */}
|
{/* Timeframes */}
|
||||||
<div className="flex space-x-1 bg-slate-950 p-1 rounded-xl border border-slate-800 text-xs font-mono">
|
<div className="flex space-x-1 bg-slate-950 p-1 rounded-xl border border-slate-800 text-xs font-mono">
|
||||||
{['1s', '5s', '1m', '15m', '1h'].map((tf) => (
|
{['5s', '1m', '15m', '1h'].map((tf) => (
|
||||||
<button
|
<button
|
||||||
key={tf}
|
key={tf}
|
||||||
onClick={() => setTimeframe(tf)}
|
onClick={() => setTimeframe(tf)}
|
||||||
@@ -149,7 +184,7 @@ export const TradingViewChart: React.FC<TradingViewChartProps> = ({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* TradingView Chart Container */}
|
{/* Chart Container */}
|
||||||
<div ref={chartContainerRef} className="w-full rounded-2xl overflow-hidden" />
|
<div ref={chartContainerRef} className="w-full rounded-2xl overflow-hidden" />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -17,48 +17,18 @@ export interface MarketItem {
|
|||||||
const MOCK_FEATURED_MARKETS: MarketItem[] = [
|
const MOCK_FEATURED_MARKETS: MarketItem[] = [
|
||||||
{
|
{
|
||||||
id: 'm-1',
|
id: 'm-1',
|
||||||
marketAddress: '0xc0E24E152771C588B21AEB654b30B1cBAf381c1a',
|
marketAddress: '0x048E9a90C25ba2c4410425D282b19A472e076039',
|
||||||
title: 'Will Bitcoin break above $120,000 before end of Q4 2026?',
|
title: 'Will Bitcoin break above $120,000 before end of Q4 2026?',
|
||||||
category: 'Crypto',
|
category: 'Crypto',
|
||||||
tier: 1,
|
tier: 1,
|
||||||
outcomes: [
|
outcomes: [
|
||||||
{ label: 'YES', price: 0.68, percentage: 68 },
|
{ label: 'YES', price: 0.50, percentage: 50 },
|
||||||
{ label: 'NO', price: 0.32, percentage: 32 }
|
{ label: 'NO', price: 0.50, percentage: 50 }
|
||||||
],
|
],
|
||||||
poolLiquidity: 1250000,
|
poolLiquidity: 1000,
|
||||||
volume24h: 384000,
|
volume24h: 0,
|
||||||
endTime: '2026-12-31',
|
endTime: '2026-12-31',
|
||||||
status: 'ACTIVE'
|
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'
|
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
@@ -1,59 +1,163 @@
|
|||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect, useCallback } from 'react';
|
||||||
import { useWeb3 } from '../../context/Web3Context';
|
import { useWeb3 } from '../../context/Web3Context';
|
||||||
import { MarketItem } from '../explore/MarketExplore';
|
import { MarketItem } from '../explore/MarketExplore';
|
||||||
import { DEPLOYMENTS, ABIS } from '@wtfx/contracts';
|
import { DEPLOYMENTS, ABIS } from '@wtfx/contracts';
|
||||||
import { ethers } from 'ethers';
|
import { ethers } from 'ethers';
|
||||||
import { TradingViewChart } from '../../components/TradingViewChart';
|
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 {
|
interface TradingTerminalProps {
|
||||||
market: MarketItem;
|
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<TradingTerminalProps> = ({ market }) => {
|
export const TradingTerminal: React.FC<TradingTerminalProps> = ({ 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<number>(0);
|
const [selectedOutcomeIndex, setSelectedOutcomeIndex] = useState<number>(0);
|
||||||
const [tradeSide, setTradeSide] = useState<'BUY' | 'SELL'>('BUY');
|
const [tradeSide, setTradeSide] = useState<'BUY' | 'SELL'>('BUY');
|
||||||
const [amount, setAmount] = useState<string>('100');
|
const [amount, setAmount] = useState<string>('100');
|
||||||
const [loading, setLoading] = useState<boolean>(false);
|
const [loading, setLoading] = useState<boolean>(false);
|
||||||
const [statusMsg, setStatusMsg] = useState<{ type: 'success' | 'error'; text: string } | null>(null);
|
const [statusMsg, setStatusMsg] = useState<{ type: 'success' | 'error'; text: string } | null>(null);
|
||||||
const [dynamicPrice, setDynamicPrice] = useState<number>(market.outcomes[0]?.price || 0.50);
|
const [marginalPrices, setMarginalPrices] = useState<number[]>([]);
|
||||||
|
const [dynamicPrice, setDynamicPrice] = useState<number>(0);
|
||||||
|
const [otBalance, setOtBalance] = useState<string>('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 };
|
const activeOutcome = market.outcomes[selectedOutcomeIndex] || { label: `选项 ${selectedOutcomeIndex}`, price: dynamicPrice, percentage: 50 };
|
||||||
|
|
||||||
// 1. 读取 PowerLDACurveV2 链上实时边际计价 (calMarginalPrice)
|
const getCurveContract = useCallback(
|
||||||
const fetchLivePrice = async () => {
|
(runner: ethers.Provider | ethers.Signer) => new ethers.Contract(curveAddress, CURVE_ABI, runner),
|
||||||
|
[curveAddress]
|
||||||
|
);
|
||||||
|
|
||||||
|
// 1. 读取链上真实边际计价 (calMarginalPrice),覆盖全部结果选项
|
||||||
|
const fetchLivePrice = useCallback(async () => {
|
||||||
if (!provider || !market.marketAddress) return;
|
if (!provider || !market.marketAddress) return;
|
||||||
try {
|
try {
|
||||||
const curveAddr = DEPLOYMENTS.robinhoodTestnet.contracts.powerLDACurve;
|
const curve = getCurveContract(provider);
|
||||||
const curveAbi = [
|
const prices: number[] = [];
|
||||||
"function calMarginalPrice(address market, uint256 tokenId) external view returns (uint256 price)"
|
for (let i = 0; i < market.outcomes.length; i++) {
|
||||||
];
|
const tid = 1n << BigInt(i);
|
||||||
const curveContract = new ethers.Contract(curveAddr, curveAbi, provider);
|
const priceWei = await curve.calMarginalPrice(market.marketAddress, tid);
|
||||||
// tokenId = 2 ** outcomeIndex
|
prices[i] = priceWei > 0n ? parseFloat(ethers.formatUnits(priceWei, 18)) : 0;
|
||||||
const tokenId = 1n << BigInt(selectedOutcomeIndex);
|
}
|
||||||
const priceWei = await curveContract.calMarginalPrice(market.marketAddress, tokenId);
|
setMarginalPrices(prices);
|
||||||
const priceNum = parseFloat(ethers.formatUnits(priceWei, 18));
|
if (prices[selectedOutcomeIndex] && prices[selectedOutcomeIndex] > 0) {
|
||||||
if (priceNum > 0) {
|
setDynamicPrice(prices[selectedOutcomeIndex]);
|
||||||
setDynamicPrice(priceNum);
|
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} 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(() => {
|
useEffect(() => {
|
||||||
fetchLivePrice();
|
fetchLivePrice();
|
||||||
|
fetchOtBalance();
|
||||||
const timer = setInterval(fetchLivePrice, 4000);
|
const timer = setInterval(fetchLivePrice, 4000);
|
||||||
return () => clearInterval(timer);
|
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';
|
// 3. 实时精确预估:BUY = 用预算二分对齐 tick 求最大可得 OT;SELL = 输入 OT 对齐 tick 求赎回值
|
||||||
const estPayout = (Number(estShares) * 1.0).toFixed(2);
|
useEffect(() => {
|
||||||
const estReturn = amount && Number(amount) > 0 ? (((Number(estPayout) - Number(amount)) / Number(amount)) * 100).toFixed(1) : '0';
|
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) => {
|
const handleTrade = async (e: React.FormEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
if (!signer || !account) {
|
if (!signer || !account) {
|
||||||
@@ -72,56 +176,86 @@ export const TradingTerminal: React.FC<TradingTerminalProps> = ({ market }) => {
|
|||||||
const collateral = getCollateralContract();
|
const collateral = getCollateralContract();
|
||||||
if (!collateral) throw new Error('Collateral contract not loaded');
|
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 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') {
|
if (tradeSide === 'BUY') {
|
||||||
// Step 1: Approve Collateral to Market
|
const budgetWei = ethers.parseUnits(amount, 18);
|
||||||
setStatusMsg({ type: 'success', text: '步骤 1/2: 正在授权 WUSD 扣款...' });
|
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);
|
const appTx = await collateral.approve(market.marketAddress, ethers.MaxUint256);
|
||||||
await appTx.wait();
|
await appTx.wait();
|
||||||
|
}
|
||||||
|
|
||||||
// Step 2: 真实调用 WTFMarketV2.mintCollateralToExactOt()
|
// Step 2: 真实调用 WTFMarketV2.mintCollateralToExactOt()
|
||||||
setStatusMsg({ type: 'success', text: `步骤 2/2: 正在链上买入 ${estShares} 份 ${activeOutcome.label} 预测代币...` });
|
setStatusMsg({
|
||||||
const tx = await marketContract.mintCollateralToExactOt(
|
type: 'success',
|
||||||
account,
|
text: `步骤 2/2: 链上买入 ${fmt(otWei)} 份 ${activeOutcome.label},成本约 ${fmt(cost)} WUSD...`,
|
||||||
tokenId,
|
});
|
||||||
otDeltaOut,
|
const tx = await marketContract.mintCollateralToExactOt(account, tid, otWei, "0x");
|
||||||
"0x" // swapData
|
|
||||||
);
|
|
||||||
const receipt = await tx.wait();
|
const receipt = await tx.wait();
|
||||||
setStatusMsg({
|
setStatusMsg({
|
||||||
type: 'success',
|
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 {
|
} else {
|
||||||
// 卖出 / 赎回
|
// 卖出 = 精确赎回 OT -> WUSD
|
||||||
setStatusMsg({ type: 'success', text: '正在赎回预测代币并结转 WUSD 保证金...' });
|
const otWei = ethers.parseUnits(amount, 18);
|
||||||
const tx = await marketContract.redeemExactOtToCollateral(
|
if (otWei <= 0n || otWei % tick !== 0n) {
|
||||||
account,
|
throw new Error(`卖出数量必须为 tick(${tick.toString()})的整数倍`);
|
||||||
tokenId,
|
}
|
||||||
otDeltaOut,
|
const bal = await marketContract.balanceOf(account, tid);
|
||||||
"0x"
|
if (bal < otWei) {
|
||||||
);
|
throw new Error(`OT 余额不足:当前持有 ${fmt(bal)} 份,需卖出 ${fmt(otWei)} 份`);
|
||||||
await tx.wait();
|
}
|
||||||
setStatusMsg({ type: 'success', text: `🎉 成功卖出/赎回 ${estShares} 份预测代币!` });
|
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();
|
refreshBalance();
|
||||||
fetchLivePrice();
|
fetchLivePrice();
|
||||||
|
fetchOtBalance();
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
console.error('Trade execution error:', err);
|
console.error('Trade execution error:', err);
|
||||||
setStatusMsg({ type: 'error', text: err.reason || err.message || '交易上链失败,请检查资金与网络' });
|
setStatusMsg({ type: 'error', text: describeError(err) });
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const isBuy = tradeSide === 'BUY';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
||||||
{/* 左侧 2 列:市场头部 + TradingView 级 K 线图表 */}
|
{/* 左侧 2 列:市场头部 + TradingView 级 K 线图表 */}
|
||||||
@@ -151,8 +285,10 @@ export const TradingTerminal: React.FC<TradingTerminalProps> = ({ market }) => {
|
|||||||
<span className="text-sm font-bold font-mono text-cyan-400">${market.poolLiquidity.toLocaleString()}</span>
|
<span className="text-sm font-bold font-mono text-cyan-400">${market.poolLiquidity.toLocaleString()}</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="bg-slate-950/60 border border-slate-800/80 rounded-2xl p-3">
|
<div className="bg-slate-950/60 border border-slate-800/80 rounded-2xl p-3">
|
||||||
<span className="text-[11px] text-slate-400 block">实时边际价格</span>
|
<span className="text-[11px] text-slate-400 block">实时边际价格 (WUSD/OT)</span>
|
||||||
<span className="text-sm font-bold font-mono text-emerald-400">${dynamicPrice.toFixed(3)}</span>
|
<span className="text-sm font-bold font-mono text-emerald-400">
|
||||||
|
{dynamicPrice > 0 ? `$${dynamicPrice.toFixed(3)}` : '--'}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="bg-slate-950/60 border border-slate-800/80 rounded-2xl p-3">
|
<div className="bg-slate-950/60 border border-slate-800/80 rounded-2xl p-3">
|
||||||
<span className="text-[11px] text-slate-400 block">创作者返佣</span>
|
<span className="text-[11px] text-slate-400 block">创作者返佣</span>
|
||||||
@@ -161,7 +297,7 @@ export const TradingTerminal: React.FC<TradingTerminalProps> = ({ market }) => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* TradingView 专业图表组件 (GMGN / Pump.fun 风格) */}
|
{/* TradingView 专业图表组件 */}
|
||||||
<TradingViewChart
|
<TradingViewChart
|
||||||
marketAddress={market.marketAddress}
|
marketAddress={market.marketAddress}
|
||||||
outcomeIndex={selectedOutcomeIndex}
|
outcomeIndex={selectedOutcomeIndex}
|
||||||
@@ -170,7 +306,7 @@ export const TradingTerminal: React.FC<TradingTerminalProps> = ({ market }) => {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 右侧:极速下单与非托管金库连击 */}
|
{/* 右侧:极速下单面板 */}
|
||||||
<div className="bg-[#0f111d] border border-slate-800 rounded-3xl p-6 sm:p-7 shadow-xl space-y-6 flex flex-col justify-between">
|
<div className="bg-[#0f111d] border border-slate-800 rounded-3xl p-6 sm:p-7 shadow-xl space-y-6 flex flex-col justify-between">
|
||||||
<form onSubmit={handleTrade} className="space-y-5">
|
<form onSubmit={handleTrade} className="space-y-5">
|
||||||
<div className="border-b border-slate-800 pb-4 flex items-center justify-between">
|
<div className="border-b border-slate-800 pb-4 flex items-center justify-between">
|
||||||
@@ -185,7 +321,7 @@ export const TradingTerminal: React.FC<TradingTerminalProps> = ({ market }) => {
|
|||||||
type="button"
|
type="button"
|
||||||
onClick={() => setTradeSide('BUY')}
|
onClick={() => setTradeSide('BUY')}
|
||||||
className={`px-3 py-1 text-xs font-bold rounded-lg transition-all ${
|
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)
|
买入 (Buy)
|
||||||
@@ -194,7 +330,7 @@ export const TradingTerminal: React.FC<TradingTerminalProps> = ({ market }) => {
|
|||||||
type="button"
|
type="button"
|
||||||
onClick={() => setTradeSide('SELL')}
|
onClick={() => setTradeSide('SELL')}
|
||||||
className={`px-3 py-1 text-xs font-bold rounded-lg transition-all ${
|
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)
|
卖出 (Sell)
|
||||||
@@ -219,7 +355,7 @@ export const TradingTerminal: React.FC<TradingTerminalProps> = ({ market }) => {
|
|||||||
>
|
>
|
||||||
<div className="font-extrabold text-xs truncate">{outcome.label}</div>
|
<div className="font-extrabold text-xs truncate">{outcome.label}</div>
|
||||||
<div className="text-[11px] font-mono mt-1 text-cyan-400 font-bold">
|
<div className="text-[11px] font-mono mt-1 text-cyan-400 font-bold">
|
||||||
${selectedOutcomeIndex === idx ? dynamicPrice.toFixed(3) : outcome.price.toFixed(2)}
|
{marginalPrices[idx] > 0 ? `$${marginalPrices[idx].toFixed(3)}` : '--'}
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
))}
|
))}
|
||||||
@@ -229,24 +365,34 @@ export const TradingTerminal: React.FC<TradingTerminalProps> = ({ market }) => {
|
|||||||
{/* 交易金额输入 */}
|
{/* 交易金额输入 */}
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<div className="flex justify-between items-center text-xs">
|
<div className="flex justify-between items-center text-xs">
|
||||||
<span className="font-semibold text-slate-300">交易金额 (WUSD)</span>
|
<span className="font-semibold text-slate-300">
|
||||||
<span className="text-slate-400 font-mono">余额: {Number(wusdBalance).toFixed(2)} WUSD</span>
|
{isBuy ? '买入金额 (WUSD)' : '卖出份额 (OT)'}
|
||||||
|
</span>
|
||||||
|
<span className="text-slate-400 font-mono">
|
||||||
|
{isBuy
|
||||||
|
? `余额: ${Number(wusdBalance).toFixed(2)} WUSD`
|
||||||
|
: `持有: ${Number(otBalance).toFixed(4)} OT`}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<input
|
<input
|
||||||
type="number"
|
type="number"
|
||||||
required
|
required
|
||||||
|
min="0"
|
||||||
|
step={isBuy ? 'any' : '1'}
|
||||||
value={amount}
|
value={amount}
|
||||||
onChange={(e) => setAmount(e.target.value)}
|
onChange={(e) => 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"
|
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"
|
||||||
/>
|
/>
|
||||||
<div className="absolute right-3 top-3 flex space-x-1">
|
<div className="absolute right-3 top-3 flex space-x-1">
|
||||||
{['50', '100', '500', 'MAX'].map((preset) => (
|
{['50', '100', '500', isBuy ? 'MAX' : 'ALL'].map((preset) => (
|
||||||
<button
|
<button
|
||||||
key={preset}
|
key={preset}
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => setAmount(preset === 'MAX' ? wusdBalance : preset)}
|
onClick={() =>
|
||||||
|
setAmount(preset === 'MAX' ? wusdBalance : preset === 'ALL' ? otBalance : preset)
|
||||||
|
}
|
||||||
className="px-2 py-0.5 text-[10px] font-bold rounded-lg bg-slate-800 text-slate-300 hover:bg-slate-700 font-mono"
|
className="px-2 py-0.5 text-[10px] font-bold rounded-lg bg-slate-800 text-slate-300 hover:bg-slate-700 font-mono"
|
||||||
>
|
>
|
||||||
{preset}
|
{preset}
|
||||||
@@ -256,27 +402,55 @@ export const TradingTerminal: React.FC<TradingTerminalProps> = ({ market }) => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 实时预估 */}
|
{/* 实时预估(基于链上曲线精确计算) */}
|
||||||
<div className="bg-slate-950 border border-slate-800 rounded-2xl p-4 space-y-2.5 text-xs">
|
<div className="bg-slate-950 border border-slate-800 rounded-2xl p-4 space-y-2.5 text-xs">
|
||||||
|
{preview && preview.kind === 'buy' && (
|
||||||
|
<>
|
||||||
<div className="flex justify-between text-slate-400">
|
<div className="flex justify-between text-slate-400">
|
||||||
<span>预估成交份额</span>
|
<span>预估成交份额</span>
|
||||||
<span className="font-mono font-bold text-slate-200">{estShares} 份</span>
|
<span className="font-mono font-bold text-slate-200">{fmt(BigInt(preview.ot))} OT</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex justify-between text-slate-400">
|
<div className="flex justify-between text-slate-400">
|
||||||
<span>若获胜最终兑付</span>
|
<span>预估成本</span>
|
||||||
<span className="font-mono font-bold text-emerald-400">${estPayout} WUSD</span>
|
<span className="font-mono font-bold text-emerald-400">{fmt(BigInt(preview.cost))} WUSD</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex justify-between text-slate-400">
|
<div className="flex justify-between text-slate-400">
|
||||||
<span>预期收益率 (ROI)</span>
|
<span>其中手续费</span>
|
||||||
<span className="font-mono font-bold text-cyan-400">+{estReturn}%</span>
|
<span className="font-mono font-bold text-amber-400">{fmt(BigInt(preview.fee))} WUSD</span>
|
||||||
</div>
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{preview && preview.kind === 'sell' && (
|
||||||
|
<>
|
||||||
|
<div className="flex justify-between text-slate-400">
|
||||||
|
<span>卖出份额</span>
|
||||||
|
<span className="font-mono font-bold text-slate-200">{fmt(BigInt(preview.ot))} OT</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between text-slate-400">
|
||||||
|
<span>预估到账</span>
|
||||||
|
<span className="font-mono font-bold text-emerald-400">{fmt(BigInt(preview.cost))} WUSD</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between text-slate-400">
|
||||||
|
<span>其中手续费</span>
|
||||||
|
<span className="font-mono font-bold text-amber-400">{fmt(BigInt(preview.fee))} WUSD</span>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{preview && preview.kind === 'error' && (
|
||||||
|
<div className="text-rose-400 font-bold">金额不可用:请按链上 tick 与边际价调整</div>
|
||||||
|
)}
|
||||||
|
{!preview && <div className="text-slate-500">输入金额后自动按链上曲线精确估算...</div>}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 下单按钮 */}
|
{/* 下单按钮 */}
|
||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
disabled={loading || !amount || Number(amount) <= 0}
|
disabled={loading || !amount || Number(amount) <= 0}
|
||||||
className="w-full py-3.5 bg-gradient-to-r from-cyan-500 to-indigo-600 hover:from-cyan-400 hover:to-indigo-500 text-white font-bold rounded-xl shadow-lg shadow-cyan-500/25 flex items-center justify-center space-x-2 transition-all disabled:opacity-50"
|
className={`w-full py-3.5 bg-gradient-to-r text-white font-bold rounded-xl shadow-lg flex items-center justify-center space-x-2 transition-all disabled:opacity-50 ${
|
||||||
|
isBuy
|
||||||
|
? 'from-cyan-500 to-indigo-600 hover:from-cyan-400 hover:to-indigo-500 shadow-cyan-500/25'
|
||||||
|
: 'from-rose-500 to-orange-600 hover:from-rose-400 hover:to-orange-500 shadow-rose-500/25'
|
||||||
|
}`}
|
||||||
>
|
>
|
||||||
{loading ? (
|
{loading ? (
|
||||||
<>
|
<>
|
||||||
@@ -286,7 +460,7 @@ export const TradingTerminal: React.FC<TradingTerminalProps> = ({ market }) => {
|
|||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<Zap className="w-5 h-5 text-amber-300" />
|
<Zap className="w-5 h-5 text-amber-300" />
|
||||||
<span>立即买入 {activeOutcome.label}</span>
|
<span>{isBuy ? `立即买入 ${activeOutcome.label}` : `立即卖出 ${activeOutcome.label}`}</span>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -8,7 +8,8 @@
|
|||||||
"powerLDACurve": "0xF0E189974c413506098AFB6Bc6Bf4C6715fBf7B1",
|
"powerLDACurve": "0xF0E189974c413506098AFB6Bc6Bf4C6715fBf7B1",
|
||||||
"controllerImplementation": "0x2Aeca49669e9E4E6CdCe46A1bc9bf136765A4f75",
|
"controllerImplementation": "0x2Aeca49669e9E4E6CdCe46A1bc9bf136765A4f75",
|
||||||
"controllerProxy": "0xc0E24E152771C588B21AEB654b30B1cBAf381c1a",
|
"controllerProxy": "0xc0E24E152771C588B21AEB654b30B1cBAf381c1a",
|
||||||
"vaultFactory": "0x89401e07296267c01017cA150D5Ec8883a78e0B2"
|
"vaultFactory": "0x89401e07296267c01017cA150D5Ec8883a78e0B2",
|
||||||
|
"defaultMarket": "0x048E9a90C25ba2c4410425D282b19A472e076039"
|
||||||
},
|
},
|
||||||
"governance": {
|
"governance": {
|
||||||
"admin": "0x6cddF384792C77219fc25C454Cfe264757842830",
|
"admin": "0x6cddF384792C77219fc25C454Cfe264757842830",
|
||||||
|
|||||||
Reference in New Issue
Block a user