diff --git a/src/components/dashboard/MinerRatesTable.tsx b/src/components/dashboard/MinerRatesTable.tsx index d871005..a0b693d 100644 --- a/src/components/dashboard/MinerRatesTable.tsx +++ b/src/components/dashboard/MinerRatesTable.tsx @@ -356,16 +356,7 @@ const MinerRatesTable: React.FC = () => { minHeight: 0, }} > - + { sx={{ display: 'flex', alignItems: 'center', - justifyContent: 'flex-end', + justifyContent: 'space-between', gap: 1.5, flexWrap: 'wrap', - ml: 'auto', }} > { ), }} sx={{ - width: 200, - ml: 'auto', + width: 220, '& .MuiOutlinedInput-root': { fontFamily: FONTS.mono, fontSize: '0.75rem', diff --git a/src/components/dashboard/OrderbookDepth.tsx b/src/components/dashboard/OrderbookDepth.tsx index db64b4b..6643243 100644 --- a/src/components/dashboard/OrderbookDepth.tsx +++ b/src/components/dashboard/OrderbookDepth.tsx @@ -100,109 +100,98 @@ const OrderbookDepth: React.FC = () => { }; const { data: miners, isLoading } = useMiners(); - const [selectedPair, setSelectedPair] = useState(''); + type Direction = 'forward' | 'reverse'; + type DirectionOption = { + asset: string; + direction: Direction; + key: string; + label: string; + }; + const [selectedKey, setSelectedKey] = useState(''); - const uniqueAssets = useMemo(() => { - const assets = new Set(); + const directionOptions = useMemo(() => { + const seen = new Map(); miners?.forEach((m) => { const s = m.sourceChain?.toLowerCase(); const d = m.destChain?.toLowerCase(); - if (!s || !d) return; - // After canonicalization from the scraper, TAO is always destChain when present, - // so the asset side is always sourceChain. Keeping the tao→asset branch as a - // defensive fallback in case pre-migration rows are still in flight. - if (d === 'tao') assets.add(s.toUpperCase()); - else if (s === 'tao') assets.add(d.toUpperCase()); + if (!s || d !== 'tao' || s === 'tao') return; + const asset = s.toUpperCase(); + const fwd = m.rate ? parseFloat(m.rate) : 0; + const rev = m.counterRate ? parseFloat(m.counterRate) : 0; + if (fwd > 0) { + const key = `${asset}>forward`; + if (!seen.has(key)) + seen.set(key, { + asset, + direction: 'forward', + key, + label: `${asset} → TAO`, + }); + } + if (rev > 0) { + const key = `${asset}>reverse`; + if (!seen.has(key)) + seen.set(key, { + asset, + direction: 'reverse', + key, + label: `TAO → ${asset}`, + }); + } }); - return Array.from(assets).sort(); + return Array.from(seen.values()).sort((a, b) => + a.label.localeCompare(b.label), + ); }, [miners]); useEffect(() => { - if (!selectedPair && uniqueAssets.length > 0) { - setSelectedPair(uniqueAssets[0]); - } else if ( - uniqueAssets.length > 0 && - !uniqueAssets.includes(selectedPair) - ) { - setSelectedPair(uniqueAssets[0]); + if (directionOptions.length === 0) return; + if (!directionOptions.find((o) => o.key === selectedKey)) { + setSelectedKey(directionOptions[0].key); } - }, [uniqueAssets, selectedPair]); + }, [directionOptions, selectedKey]); - const depthData = useMemo(() => { - if (!miners?.length || !selectedPair) { - return []; - } + const selected = directionOptions.find((o) => o.key === selectedKey) ?? null; - const asset = selectedPair.toLowerCase(); - // Two independent rate ladders — the forward (asset→TAO) and reverse (TAO→asset) - // quotes are now distinct per miner, so they each get their own aggregation. - const forwardGroups: Record = {}; // key = rate, val = capacity TAO - const reverseGroups: Record = {}; // key = counterRate, val = capacity TAO + const depthData = useMemo(() => { + if (!miners?.length || !selected) return []; + const asset = selected.asset.toLowerCase(); + const groups: Record = {}; // key = rate, val = collateral TAO miners.forEach((m) => { if (!m.collateralRao) return; const s = m.sourceChain?.toLowerCase(); const d = m.destChain?.toLowerCase(); - // Canonical order: asset is source, tao is dest. rate = asset→TAO, counterRate = TAO→asset. if (s !== asset || d !== 'tao') return; - const capacityTao = parseInt(m.collateralRao, 10) / 1e9; if (isNaN(capacityTao) || capacityTao <= 0) return; - - const forward = m.rate ? parseFloat(m.rate) : 0; - if (!isNaN(forward) && forward > 0) { - const key = forward.toFixed(2); - forwardGroups[key] = (forwardGroups[key] || 0) + capacityTao; - } - - const reverse = m.counterRate ? parseFloat(m.counterRate) : 0; - if (!isNaN(reverse) && reverse > 0) { - const key = reverse.toFixed(2); - reverseGroups[key] = (reverseGroups[key] || 0) + capacityTao; - } + const raw = selected.direction === 'forward' ? m.rate : m.counterRate; + const r = raw ? parseFloat(raw) : 0; + if (!isFinite(r) || r <= 0) return; + const key = r.toFixed(2); + groups[key] = (groups[key] || 0) + capacityTao; }); - // Union the rate axis so both ladders render against the same rows. - const allRates = Array.from( - new Set([...Object.keys(forwardGroups), ...Object.keys(reverseGroups)]), - ).sort((a, b) => parseFloat(b) - parseFloat(a)); - - // Forward book cumulates top-down (best asset→TAO rate first = highest TAO per asset). - let cumAssetToTao = 0; - const forwardCum: number[] = []; - for (const key of allRates) { - cumAssetToTao += forwardGroups[key] || 0; - forwardCum.push(cumAssetToTao); - } - - // Reverse book cumulates bottom-up (best TAO→asset rate = lowest TAO per asset). - let cumTaoToAsset = 0; - const reverseCum = new Array(allRates.length); - for (let i = allRates.length - 1; i >= 0; i--) { - cumTaoToAsset += reverseGroups[allRates[i]] || 0; - reverseCum[i] = cumTaoToAsset; - } + // Best rate first: forward wants highest TAO/asset, reverse wants lowest. + const rates = Object.keys(groups).sort((a, b) => + selected.direction === 'forward' + ? parseFloat(b) - parseFloat(a) + : parseFloat(a) - parseFloat(b), + ); - return allRates.map((key, i) => ({ - rate: key, - forwardCapacity: forwardGroups[key] || 0, - reverseCapacity: reverseGroups[key] || 0, - cumAssetToTao: forwardCum[i], - cumTaoToAsset: reverseCum[i], - })); - }, [miners, selectedPair]); + let cum = 0; + return rates.map((key) => { + const capacity = groups[key]; + cum += capacity; + return { rate: key, capacity, cumCapacity: cum }; + }); + }, [miners, selected]); - const maxCum = useMemo(() => { - if (depthData.length === 0) return 1; - let m = 0; - for (const row of depthData) { - if (row.cumAssetToTao > m) m = row.cumAssetToTao; - if (row.cumTaoToAsset > m) m = row.cumTaoToAsset; - } - return m > 0 ? m : 1; - }, [depthData]); - const getAssetSymbol = () => - selectedPair ? selectedPair.replace('/TAO', '').trim() : ''; + const maxCum = useMemo( + () => + depthData.reduce((m, r) => (r.cumCapacity > m ? r.cumCapacity : m), 1), + [depthData], + ); return isLoading || !miners ? ( @@ -256,13 +245,13 @@ const OrderbookDepth: React.FC = () => { - {uniqueAssets.length > 0 && ( + {directionOptions.length > 0 && ( @@ -306,7 +295,7 @@ const OrderbookDepth: React.FC = () => { @@ -317,18 +306,18 @@ const OrderbookDepth: React.FC = () => { - Capacity + Capacity (TAO) @@ -340,25 +329,17 @@ const OrderbookDepth: React.FC = () => { cursor: 'help', }} > - - - - - - - - + {selected?.direction === 'reverse' ? ( + <> + {'→'} + + ) : selected ? ( + <> + {'→'} + + ) : ( + Cumulative + )} @@ -366,31 +347,21 @@ const OrderbookDepth: React.FC = () => { {depthData.map((row) => { - const pctAssetToTao = (row.cumAssetToTao / maxCum) * 100; - const pctTaoToAsset = (row.cumTaoToAsset / maxCum) * 100; - - const isBtc = getAssetSymbol().toUpperCase() === 'BTC'; - + const pct = (row.cumCapacity / maxCum) * 100; + const isBtc = selected?.asset.toUpperCase() === 'BTC'; const assetThemeColor = isBtc ? BTC_COLOR : theme.palette.primary.main; - const taoThemeColor = TAO_COLOR; - - const leftGradColor = `color-mix(in srgb, ${assetThemeColor} 10%, transparent)`; - const rightGradColor = - theme.palette.mode === 'dark' - ? 'color-mix(in srgb, var(--color-white) 10%, transparent)' - : 'color-mix(in srgb, var(--color-woodsmoke) 8%, transparent)'; + const fillColor = + selected?.direction === 'forward' ? assetThemeColor : TAO_COLOR; + const gradColor = `color-mix(in srgb, ${fillColor} 14%, transparent)`; return ( { sx={{ ...cellSx, color: 'text.primary' }} align="right" > - {(row.forwardCapacity + row.reverseCapacity).toFixed(2)} + {row.capacity.toFixed(2)} - - {row.cumAssetToTao > 0 - ? (row.cumAssetToTao / parseFloat(row.rate)).toFixed(6) - : '\u2014'} - - - {row.cumTaoToAsset > 0 - ? row.cumTaoToAsset.toFixed(2) - : '\u2014'} + + {row.cumCapacity.toFixed(2)} ); @@ -428,7 +386,7 @@ const OrderbookDepth: React.FC = () => { {depthData.length === 0 && (