Files
smom-dbis-138/frontend-dapp/src/features/trade/BinanceTradePanel.tsx
zaragoza444 3c7abd5a01
Some checks failed
CI/CD Pipeline / Solidity Contracts (push) Failing after 1m15s
CI/CD Pipeline / Security Scanning (push) Successful in 2m33s
CI/CD Pipeline / Lint and Format (push) Failing after 46s
CI/CD Pipeline / Terraform Validation (push) Failing after 22s
CI/CD Pipeline / Kubernetes Validation (push) Successful in 24s
Validation / validate-genesis (push) Successful in 25s
Validation / validate-terraform (push) Failing after 29s
Validation / validate-kubernetes (push) Failing after 10s
Validation / validate-smart-contracts (push) Failing after 11s
Validation / validate-security (push) Failing after 1m37s
Validation / validate-documentation (push) Failing after 20s
fix: replace near-black NASA dashboard void with mission blue panels
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-28 18:42:12 -07:00

341 lines
13 KiB
TypeScript

import { useEffect, useMemo, useState } from 'react';
import { useDbisSwap } from '../swap/useDbisSwap';
import { formatAmountFromWei, type DexToken } from '../../config/dex';
import TokenIcon from '../../components/ui/TokenIcon';
import NasaIcon from '../../components/icons/NasaIcon';
type Side = 'buy' | 'sell';
function MarketRow({
token,
quote,
active,
onClick,
}: {
token: DexToken;
quote: string;
active: boolean;
onClick: () => void;
}) {
return (
<button
type="button"
onClick={onClick}
className={`trade-market-row w-full text-left px-3 py-2 flex justify-between items-center text-sm ${
active ? 'trade-market-row--active' : ''
}`}
>
<span className="font-medium text-[#e8f4ff] flex items-center gap-1.5">
<NasaIcon name="money" size={12} className="nasa-icon--telemetry" />
{token.symbol}
<span className="text-[#7a9bb8]">/{quote}</span>
</span>
<span className="text-[#3dff8a] text-xs font-mono">M2</span>
</button>
);
}
function OrderBookMock({ price }: { price: string }) {
const base = parseFloat(price) || 1;
const rows = Array.from({ length: 8 }, (_, i) => {
const p = base * (1 + (4 - i) * 0.001);
const amt = (Math.random() * 2 + 0.1).toFixed(4);
return { price: p.toFixed(4), amount: amt, side: i < 4 ? 'ask' : 'bid' };
});
return (
<div className="trade-orderbook text-xs font-mono">
<div className="flex justify-between text-[#7a9bb8] px-2 py-1 border-b border-[#1e4a8a]">
<span>Price</span>
<span>Amount</span>
</div>
{rows.slice(0, 4).map((r) => (
<div key={`a-${r.price}`} className="flex justify-between px-2 py-0.5 text-[#f6465d]">
<span>{r.price}</span>
<span>{r.amount}</span>
</div>
))}
<div className="text-center py-2 text-lg font-semibold text-[#0ecb81] border-y border-[#2b3139] my-1">
{price || '—'}
</div>
{rows.slice(4).map((r) => (
<div key={`b-${r.price}`} className="flex justify-between px-2 py-0.5 text-[#0ecb81]">
<span>{r.price}</span>
<span>{r.amount}</span>
</div>
))}
</div>
);
}
export default function BinanceTradePanel() {
const swap = useDbisSwap();
const [side, setSide] = useState<Side>('buy');
const [marketSearch, setMarketSearch] = useState('');
const [orderType, setOrderType] = useState<'market' | 'limit'>('market');
useEffect(() => {
swap.loadMonitor();
}, [swap.loadMonitor]);
useEffect(() => {
if (swap.isConnected) swap.loadLedgers();
}, [swap.isConnected, swap.address, swap.loadLedgers]);
const quoteSymbol = 'cUSDT';
const quoteToken = swap.tokens.find((t) => t.symbol === quoteSymbol) ?? swap.tokens.find((t) => t.symbol.includes('USDT'));
const markets = useMemo(() => {
const q = marketSearch.trim().toLowerCase();
return swap.tokens.filter(
(t) =>
!t.native &&
t.symbol !== quoteToken?.symbol &&
(!q || t.symbol.toLowerCase().includes(q) || t.name.toLowerCase().includes(q)),
);
}, [swap.tokens, marketSearch, quoteToken]);
const selectMarket = (base: DexToken) => {
if (side === 'buy') {
swap.setTokenIn(quoteToken ?? swap.tokenIn);
swap.setTokenOut(base);
} else {
swap.setTokenIn(base);
swap.setTokenOut(quoteToken ?? swap.tokenOut);
}
};
const outDisplay =
swap.plan?.amountOut && swap.tokenOut
? formatAmountFromWei(String(swap.plan.amountOut), swap.tokenOut.decimals)
: '—';
const displayPrice =
swap.amountIn && outDisplay !== '—'
? (parseFloat(outDisplay) / parseFloat(swap.amountIn || '1')).toFixed(6)
: '—';
return (
<div className="trade-layout nasa-mission-root">
{/* Markets sidebar */}
<aside className="trade-markets hidden lg:flex flex-col border-r border-[#1e4a8a]">
<div className="p-3 border-b border-[#1e4a8a] flex items-center gap-2">
<NasaIcon name="search" size={14} className="nasa-icon--telemetry shrink-0" />
<input
type="search"
placeholder="Search markets"
value={marketSearch}
onChange={(e) => setMarketSearch(e.target.value)}
className="nasa-input !min-h-[36px] !text-sm flex-1"
/>
</div>
<div className="flex-1 overflow-y-auto">
<p className="px-3 py-2 text-xs text-[#7a9bb8] uppercase font-mono flex items-center gap-1.5">
<NasaIcon name="trade" size={12} className="nasa-icon--telemetry" />
M2 · {markets.length} pairs
</p>
{markets.map((t) => (
<MarketRow
key={t.address}
token={t}
quote={quoteToken?.symbol ?? 'USDT'}
active={swap.tokenOut.symbol === t.symbol || swap.tokenIn.symbol === t.symbol}
onClick={() => selectMarket(t)}
/>
))}
</div>
</aside>
{/* Chart + order book */}
<section className="trade-center flex flex-col min-w-0">
<div className="trade-pair-header flex flex-wrap items-center gap-4 px-4 py-3 border-b border-[#1e4a8a]">
<div className="flex items-center gap-2">
<TokenIcon symbol={swap.tokenOut.symbol} size={28} />
<div>
<p className="text-[0.65rem] font-mono uppercase tracking-widest text-[#7a9bb8] flex items-center gap-1">
<NasaIcon name="trade" size={12} className="nasa-icon--telemetry" />
TRD-138 · Exchange terminal
</p>
<h1 className="text-xl font-bold text-[#e8f4ff] font-mono uppercase tracking-wide">
{swap.tokenOut.symbol}/{quoteToken?.symbol ?? 'USDT'}
</h1>
</div>
</div>
<span className="text-2xl font-semibold text-[#3dff8a] font-mono">{displayPrice}</span>
<span className="nasa-badge nasa-badge--nominal text-xs">Office 24 · Chain 138</span>
<span className="text-xs text-[#7a9bb8] font-mono flex items-center gap-1">
<NasaIcon name="satellite" size={12} />
37 M2 tokens · tradable &amp; swappable
</span>
</div>
<div className="flex-1 grid grid-cols-1 xl:grid-cols-[1fr_280px] min-h-[320px]">
<div className="trade-chart border-b xl:border-b-0 xl:border-r border-[#2d6bb8] p-4">
<div className="trade-chart__canvas h-full flex items-end justify-around px-2 pb-2 gap-1">
{Array.from({ length: 24 }, (_, i) => (
<div
key={i}
className="flex-1 max-w-[12px] rounded-t opacity-80"
style={{
height: `${30 + Math.sin(i * 0.5) * 20 + Math.random() * 25}%`,
background: i % 3 === 0 ? '#f6465d' : '#0ecb81',
}}
/>
))}
</div>
<p className="text-xs text-[#848e9c] mt-2 text-center">Live chart · connect WebSocket feed for production</p>
</div>
<div className="trade-book hidden xl:block p-2">
<p className="text-xs text-[#848e9c] uppercase px-2 py-1">Order book</p>
<OrderBookMock price={displayPrice} />
</div>
</div>
<div className="trade-recent border-t border-[#2d6bb8] p-3 max-h-[140px] overflow-y-auto">
<p className="text-xs text-[#848e9c] uppercase mb-2">Recent trades</p>
<div className="grid grid-cols-3 gap-2 text-xs font-mono text-[#848e9c]">
{((swap.monitor as { swaps?: { status: string; amountIn: string; createdAt: string }[] })?.swaps ?? [])
.slice(0, 6)
.map((s, i) => (
<span key={i} className="col-span-3 flex justify-between">
<span className={s.status === 'FAILED' ? 'text-[#f6465d]' : 'text-[#0ecb81]'}>{s.status}</span>
<span>{s.amountIn?.slice(0, 12)}</span>
<span>{s.createdAt?.slice(11, 19)}</span>
</span>
))}
</div>
</div>
</section>
{/* Trade panel */}
<aside className="trade-panel w-full lg:w-[320px] shrink-0 border-l border-[#2d6bb8] flex flex-col">
<div className="flex border-b border-[#2d6bb8]">
<button
type="button"
className={`flex-1 py-3 text-sm font-semibold ${side === 'buy' ? 'text-[#0ecb81] border-b-2 border-[#0ecb81]' : 'text-[#848e9c]'}`}
onClick={() => {
setSide('buy');
if (quoteToken) swap.setTokenIn(quoteToken);
}}
>
Buy
</button>
<button
type="button"
className={`flex-1 py-3 text-sm font-semibold ${side === 'sell' ? 'text-[#f6465d] border-b-2 border-[#f6465d]' : 'text-[#848e9c]'}`}
onClick={() => {
setSide('sell');
if (quoteToken) swap.setTokenOut(quoteToken);
}}
>
Sell
</button>
</div>
<div className="p-4 flex-1 flex flex-col gap-4">
<div className="flex gap-2 text-xs">
{(['market', 'limit'] as const).map((t) => (
<button
key={t}
type="button"
onClick={() => setOrderType(t)}
className={`px-3 py-1 rounded capitalize ${orderType === t ? 'bg-[#2b3139] text-[#f0b90b]' : 'text-[#848e9c]'}`}
>
{t}
</button>
))}
</div>
<label className="block">
<span className="text-xs text-[#848e9c]">Pay with</span>
<select
className="w-full mt-1 bg-[#2b3139] border border-[#474d57] rounded px-3 py-2 text-sm"
value={swap.tokenIn.address}
onChange={(e) => {
const t = swap.tokens.find((x) => x.address === e.target.value);
if (t) swap.setTokenIn(t);
}}
>
{swap.tokens.map((t) => (
<option key={t.address} value={t.address}>
{t.symbol}
</option>
))}
</select>
</label>
<label className="block">
<span className="text-xs text-[#848e9c]">Receive</span>
<select
className="w-full mt-1 bg-[#2b3139] border border-[#474d57] rounded px-3 py-2 text-sm"
value={swap.tokenOut.address}
onChange={(e) => {
const t = swap.tokens.find((x) => x.address === e.target.value);
if (t) swap.setTokenOut(t);
}}
>
{swap.tokens.map((t) => (
<option key={t.address} value={t.address}>
{t.symbol}
</option>
))}
</select>
</label>
<label className="block">
<span className="text-xs text-[#848e9c]">Amount</span>
<input
type="text"
inputMode="decimal"
value={swap.amountIn}
onChange={(e) => swap.setAmountIn(e.target.value)}
className="w-full mt-1 bg-[#2b3139] border border-[#474d57] rounded px-3 py-3 text-lg font-semibold"
placeholder="0.00"
/>
</label>
<div className="text-sm text-[#848e9c]">
Est. receive: <span className="text-[#eaecef] font-medium">{outDisplay} {swap.tokenOut.symbol}</span>
</div>
<div className="flex gap-1">
{[25, 50, 100].map((bps) => (
<button
key={bps}
type="button"
onClick={() => swap.setSlippageBps(bps)}
className={`flex-1 py-1 text-xs rounded ${swap.slippageBps === bps ? 'bg-[#f0b90b] text-[#181a20]' : 'bg-[#2b3139] text-[#848e9c]'}`}
>
{(bps / 100).toFixed(1)}%
</button>
))}
</div>
{swap.error && <p className="text-xs text-[#f6465d]">{swap.error}</p>}
<button
type="button"
disabled={swap.loading || !swap.isConnected || !swap.amountIn}
onClick={() => swap.executeSwap()}
className={`w-full py-3 rounded font-bold text-[#181a20] ${
side === 'buy' ? 'bg-[#0ecb81] hover:opacity-90' : 'bg-[#f6465d] hover:opacity-90'
} disabled:opacity-50`}
>
{!swap.isConnected ? 'Connect wallet' : swap.loading ? 'Processing…' : side === 'buy' ? 'Buy' : 'Sell'}
</button>
{swap.txHash && (
<a
href={`https://explorer.d-bis.org/tx/${swap.txHash}`}
target="_blank"
rel="noreferrer"
className="text-xs text-[#f0b90b] break-all hover:underline"
>
Tx: {swap.txHash.slice(0, 18)}
</a>
)}
</div>
</aside>
</div>
);
}