feat(charts): integrate TradingView lightweight candlestick charts and live power lda pricing

This commit is contained in:
Bot
2026-08-31 02:14:03 +08:00
parent 7c59c2e9fc
commit 13c44afc51
4 changed files with 307 additions and 122 deletions
+130 -117
View File
@@ -1,27 +1,59 @@
import React, { useState } from 'react';
import React, { useState, useEffect } from 'react';
import { useWeb3 } from '../../context/Web3Context';
import { MarketItem } from '../explore/MarketExplore';
import { DEPLOYMENTS, ABIS } from '@wtfx/contracts';
import { ethers } from 'ethers';
import { TrendingUp, ArrowDownUp, Check, AlertCircle, Loader2, Sparkles, Activity, ShieldCheck } from 'lucide-react';
import { TradingViewChart } from '../../components/TradingViewChart';
import { TrendingUp, ArrowDownUp, Check, AlertCircle, Loader2, Sparkles, Activity, ShieldCheck, Zap } from 'lucide-react';
interface TradingTerminalProps {
market: MarketItem;
}
export const TradingTerminal: React.FC<TradingTerminalProps> = ({ market }) => {
const { account, signer, getCollateralContract, wusdBalance, refreshBalance, isCorrectNetwork } = useWeb3();
const { account, signer, provider, getCollateralContract, wusdBalance, vaultAddress, isSessionActive, refreshBalance, isCorrectNetwork } = useWeb3();
const [selectedOutcomeIndex, setSelectedOutcomeIndex] = useState<number>(0);
const [tradeSide, setTradeSide] = useState<'BUY' | 'SELL'>('BUY');
const [amount, setAmount] = useState<string>('100');
const [loading, setLoading] = useState<boolean>(false);
const [statusMsg, setStatusMsg] = useState<{ type: 'success' | 'error'; text: string } | null>(null);
const [dynamicPrice, setDynamicPrice] = useState<number>(market.outcomes[0]?.price || 0.50);
const activeOutcome = market.outcomes[selectedOutcomeIndex];
const estShares = amount && Number(amount) > 0 ? (Number(amount) / activeOutcome.price).toFixed(2) : '0';
const activeOutcome = market.outcomes[selectedOutcomeIndex] || { label: `选项 ${selectedOutcomeIndex}`, price: dynamicPrice, percentage: 50 };
// 1. 读取 PowerLDACurveV2 链上实时边际计价 (calMarginalPrice)
const fetchLivePrice = 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);
}
} catch (e) {
console.warn('Fallback to market default price:', e);
}
};
useEffect(() => {
fetchLivePrice();
const timer = setInterval(fetchLivePrice, 4000);
return () => clearInterval(timer);
}, [market.marketAddress, selectedOutcomeIndex, provider]);
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';
// 2. 真金白银链上下单 / 0 弹窗极速交易
const handleTrade = async (e: React.FormEvent) => {
e.preventDefault();
if (!signer || !account) {
@@ -41,18 +73,50 @@ export const TradingTerminal: React.FC<TradingTerminalProps> = ({ market }) => {
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);
// Step 1: Approve Collateral
setStatusMsg({ type: 'success', text: '步骤 1/2: 正在授权扣减 WUSD 保证金...' });
const appTx = await collateral.approve(market.marketAddress, ethers.MaxUint256);
await appTx.wait();
// WTFMarketV2 合约实例
const marketContract = new ethers.Contract(market.marketAddress, ABIS.WTFMarketV2, signer);
// Step 2: In web2-like demo / testnet, complete trade
setStatusMsg({ type: 'success', text: `🎉 订单已撮合成交!已以 $${activeOutcome.price} 买入 ${estShares}${activeOutcome.label} 预测代币。` });
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();
// Step 2: 真实调用 WTFMarketV2.mintCollateralToExactOt()
setStatusMsg({ type: 'success', text: `步骤 2/2: 正在链上买入 ${estShares}${activeOutcome.label} 预测代币...` });
const tx = await marketContract.mintCollateralToExactOt(
account,
tokenId,
otDeltaOut,
"0x" // swapData
);
const receipt = await tx.wait();
setStatusMsg({
type: 'success',
text: `🎉 真实链上下单成功!已通过 Power LDA 曲线买入 ${estShares} 份,哈希: ${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} 份预测代币!` });
}
// 刷新余额与实时曲线价格
refreshBalance();
fetchLivePrice();
} catch (err: any) {
console.error('Trade error:', err);
setStatusMsg({ type: 'error', text: err.reason || err.message || '交易失败' });
console.error('Trade execution error:', err);
setStatusMsg({ type: 'error', text: err.reason || err.message || '交易上链失败,请检查资金与网络' });
} finally {
setLoading(false);
}
@@ -60,13 +124,13 @@ export const TradingTerminal: React.FC<TradingTerminalProps> = ({ market }) => {
return (
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
{/* Left 2 Cols: Market Title, Chart, Curve Status */}
{/* 左侧 2 列:市场头部 + TradingView 级 K 线图表 */}
<div className="lg:col-span-2 space-y-6">
{/* Header Summary */}
<div className="bg-[#0f111d] border border-slate-800 rounded-2xl p-6 shadow-xl space-y-4">
<div className="bg-[#0f111d] border border-slate-800 rounded-3xl p-6 sm:p-7 shadow-xl space-y-4">
<div className="flex items-center justify-between">
<span className="text-xs font-bold px-2.5 py-1 rounded bg-cyan-950/60 text-cyan-400 border border-cyan-800/40">
{market.category} &bull; Tier {market.tier}
<span className="text-xs font-bold px-3 py-1 rounded-xl bg-cyan-950/60 text-cyan-400 border border-cyan-800/40">
{market.category} &bull; 线
</span>
<div className="flex items-center space-x-2 text-xs font-mono text-slate-400">
<span>: {market.endTime}</span>
@@ -78,97 +142,44 @@ export const TradingTerminal: React.FC<TradingTerminalProps> = ({ market }) => {
</h1>
<div className="grid grid-cols-2 sm:grid-cols-4 gap-3 pt-2">
<div className="bg-slate-950/60 border border-slate-800/80 rounded-xl p-3">
<span className="text-[11px] text-slate-500 block">24H </span>
<div className="bg-slate-950/60 border border-slate-800/80 rounded-2xl p-3">
<span className="text-[11px] text-slate-400 block">24H </span>
<span className="text-sm font-bold font-mono text-slate-200">${market.volume24h.toLocaleString()}</span>
</div>
<div className="bg-slate-950/60 border border-slate-800/80 rounded-xl p-3">
<span className="text-[11px] text-slate-500 block"></span>
<div className="bg-slate-950/60 border border-slate-800/80 rounded-2xl p-3">
<span className="text-[11px] text-slate-400 block"> (TVL)</span>
<span className="text-sm font-bold font-mono text-cyan-400">${market.poolLiquidity.toLocaleString()}</span>
</div>
<div className="bg-slate-950/60 border border-slate-800/80 rounded-xl p-3">
<span className="text-[11px] text-slate-500 block">线</span>
<span className="text-sm font-bold text-emerald-400">Power LDA ()</span>
<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-sm font-bold font-mono text-emerald-400">${dynamicPrice.toFixed(3)}</span>
</div>
<div className="bg-slate-950/60 border border-slate-800/80 rounded-xl p-3">
<span className="text-[11px] text-slate-500 block"></span>
<span className="text-sm font-bold font-mono text-slate-300">0.6%</span>
<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-sm font-bold font-mono text-pink-400">50% </span>
</div>
</div>
</div>
{/* High-frequency Chart Simulation */}
<div className="bg-[#0f111d] border border-slate-800 rounded-2xl p-6 shadow-xl space-y-4">
<div className="flex items-center justify-between border-b border-slate-800/80 pb-4">
<div className="flex items-center space-x-2">
<Activity className="w-5 h-5 text-cyan-400" />
<h3 className="font-bold text-sm text-white"> &amp; 线 (Real-Time Price &amp; Probability)</h3>
</div>
<div className="flex space-x-1.5 text-xs font-mono">
{['1H', '6H', '24H', '7D', 'ALL'].map((tf, i) => (
<button
key={tf}
className={`px-2.5 py-1 rounded-md transition-colors ${
i === 2 ? 'bg-cyan-500/20 text-cyan-400 font-bold' : 'text-slate-500 hover:text-slate-300'
}`}
>
{tf}
</button>
))}
</div>
</div>
{/* SVG Chart Graphic */}
<div className="h-64 w-full flex flex-col justify-between relative pt-4">
<svg className="w-full h-48 overflow-visible" viewBox="0 0 500 150">
<defs>
<linearGradient id="curveGradient" x1="0%" y1="0%" x2="0%" y2="100%">
<stop offset="0%" stopColor="#06b6d4" stopOpacity="0.3" />
<stop offset="100%" stopColor="#06b6d4" stopOpacity="0.0" />
</linearGradient>
</defs>
{/* Grid Lines */}
<line x1="0" y1="30" x2="500" y2="30" stroke="#1f2438" strokeDasharray="4" />
<line x1="0" y1="75" x2="500" y2="75" stroke="#1f2438" strokeDasharray="4" />
<line x1="0" y1="120" x2="500" y2="120" stroke="#1f2438" strokeDasharray="4" />
{/* Area Fill */}
<path
d="M 0 120 Q 80 110, 150 90 T 300 60 T 450 40 L 500 35 L 500 150 L 0 150 Z"
fill="url(#curveGradient)"
/>
{/* Main Stroke */}
<path
d="M 0 120 Q 80 110, 150 90 T 300 60 T 450 40 L 500 35"
fill="none"
stroke="#06b6d4"
strokeWidth="3"
/>
<circle cx="500" cy="35" r="4" fill="#38bdf8" className="animate-ping" />
<circle cx="500" cy="35" r="4" fill="#38bdf8" />
</svg>
<div className="flex justify-between text-[11px] font-mono text-slate-500 pt-2">
<span>00:00</span>
<span>06:00</span>
<span>12:00</span>
<span>18:00</span>
<span>LIVE</span>
</div>
</div>
</div>
{/* TradingView 专业图表组件 (GMGN / Pump.fun 风格) */}
<TradingViewChart
marketAddress={market.marketAddress}
outcomeIndex={selectedOutcomeIndex}
outcomeLabel={activeOutcome.label}
currentPrice={dynamicPrice}
/>
</div>
{/* Right Col: Instant Order Placement Panel */}
<div className="bg-[#0f111d] border border-slate-800 rounded-2xl p-6 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">
<div className="border-b border-slate-800 pb-4 flex items-center justify-between">
<h2 className="font-bold text-base text-white flex items-center space-x-2">
<TrendingUp className="w-5 h-5 text-cyan-400" />
<span> (Fast Order)</span>
<span></span>
</h2>
{/* Buy / Sell Tab */}
{/* 买卖切换 */}
<div className="flex bg-slate-950 p-1 rounded-xl border border-slate-800">
<button
type="button"
@@ -191,33 +202,35 @@ export const TradingTerminal: React.FC<TradingTerminalProps> = ({ market }) => {
</div>
</div>
{/* Outcome Selectors */}
{/* 预测结果选项 */}
<div className="space-y-2">
<label className="text-xs font-semibold text-slate-400"> (Outcome)</label>
<div className="grid grid-cols-2 gap-3">
<label className="text-xs font-semibold text-slate-300"></label>
<div className="grid grid-cols-2 gap-2.5 max-h-48 overflow-y-auto pr-1">
{market.outcomes.map((outcome, idx) => (
<button
key={idx}
type="button"
onClick={() => setSelectedOutcomeIndex(idx)}
className={`p-3.5 rounded-xl border text-left transition-all ${
className={`p-3 rounded-2xl border text-left transition-all ${
selectedOutcomeIndex === idx
? 'bg-cyan-950/40 border-cyan-500 text-white ring-1 ring-cyan-500'
? 'bg-cyan-950/50 border-cyan-500 text-white ring-1 ring-cyan-500'
: 'bg-slate-950 border-slate-800 text-slate-400 hover:border-slate-700'
}`}
>
<div className="font-extrabold text-sm">{outcome.label}</div>
<div className="text-xs font-mono mt-1 text-cyan-400 font-bold">${outcome.price} ({outcome.percentage}%)</div>
<div className="font-extrabold text-xs truncate">{outcome.label}</div>
<div className="text-[11px] font-mono mt-1 text-cyan-400 font-bold">
${selectedOutcomeIndex === idx ? dynamicPrice.toFixed(3) : outcome.price.toFixed(2)}
</div>
</button>
))}
</div>
</div>
{/* Amount Input */}
{/* 交易金额输入 */}
<div className="space-y-2">
<div className="flex justify-between items-center text-xs">
<span className="font-semibold text-slate-400"> (Amount WUSD)</span>
<span className="text-slate-500 font-mono">: {Number(wusdBalance).toFixed(2)} WUSD</span>
<span className="font-semibold text-slate-300"> (WUSD)</span>
<span className="text-slate-400 font-mono">: {Number(wusdBalance).toFixed(2)} WUSD</span>
</div>
<div className="relative">
<input
@@ -226,7 +239,7 @@ export const TradingTerminal: React.FC<TradingTerminalProps> = ({ market }) => {
value={amount}
onChange={(e) => setAmount(e.target.value)}
placeholder="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 transition-colors"
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">
{['50', '100', '500', 'MAX'].map((preset) => (
@@ -234,7 +247,7 @@ export const TradingTerminal: React.FC<TradingTerminalProps> = ({ market }) => {
key={preset}
type="button"
onClick={() => setAmount(preset === 'MAX' ? wusdBalance : preset)}
className="px-2 py-0.5 text-[10px] font-bold rounded 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}
</button>
@@ -243,23 +256,23 @@ export const TradingTerminal: React.FC<TradingTerminalProps> = ({ market }) => {
</div>
</div>
{/* Order Estimations */}
<div className="bg-slate-950 border border-slate-800/80 rounded-xl p-3.5 space-y-2 text-xs">
{/* 实时预估 */}
<div className="bg-slate-950 border border-slate-800 rounded-2xl p-4 space-y-2.5 text-xs">
<div className="flex justify-between text-slate-400">
<span> (Est. Shares)</span>
<span></span>
<span className="font-mono font-bold text-slate-200">{estShares} </span>
</div>
<div className="flex justify-between text-slate-400">
<span> (Max Payout)</span>
<span></span>
<span className="font-mono font-bold text-emerald-400">${estPayout} WUSD</span>
</div>
<div className="flex justify-between text-slate-400">
<span> (Est. ROI)</span>
<span> (ROI)</span>
<span className="font-mono font-bold text-cyan-400">+{estReturn}%</span>
</div>
</div>
{/* Submit Button */}
{/* 下单按钮 */}
<button
type="submit"
disabled={loading || !amount || Number(amount) <= 0}
@@ -268,30 +281,30 @@ export const TradingTerminal: React.FC<TradingTerminalProps> = ({ market }) => {
{loading ? (
<>
<Loader2 className="w-5 h-5 animate-spin" />
<span>...</span>
<span>...</span>
</>
) : (
<>
<TrendingUp className="w-5 h-5" />
<Zap className="w-5 h-5 text-amber-300" />
<span> {activeOutcome.label}</span>
</>
)}
</button>
</form>
{/* Status Message */}
{/* 状态通知 */}
{statusMsg && (
<div
className={`p-3.5 rounded-xl border flex items-start space-x-2.5 text-xs ${
className={`p-4 rounded-2xl border flex items-start space-x-3 text-xs ${
statusMsg.type === 'success'
? 'bg-emerald-950/30 border-emerald-800/50 text-emerald-300'
: 'bg-rose-950/30 border-rose-800/50 text-rose-300'
? 'bg-emerald-950/40 border-emerald-800/60 text-emerald-300'
: 'bg-rose-950/40 border-rose-800/60 text-rose-300'
}`}
>
{statusMsg.type === 'success' ? (
<Check className="w-4 h-4 flex-shrink-0 text-emerald-400" />
<Check className="w-5 h-5 flex-shrink-0 text-emerald-400" />
) : (
<AlertCircle className="w-4 h-4 flex-shrink-0 text-rose-400" />
<AlertCircle className="w-5 h-5 flex-shrink-0 text-rose-400" />
)}
<div className="flex-1 overflow-hidden break-words">
<p>{statusMsg.text}</p>