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
Co-authored-by: Cursor <cursoragent@cursor.com>
341 lines
13 KiB
TypeScript
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 & 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>
|
|
);
|
|
}
|