+
+
+
+ Market depth
+
+ {isFetching && !isLoading && (
+
+
+ Updating
+
+ )}
+
+
+
+
+
+ Bids
+
+
+
+ Asks
+
+
+
+
+ {/* Book summary above the chart (#1220) */}
+
+
+
+ Best bid
+
+
+ {hasData ? formatDepthPrice(model.bestBid) : '—'}
+
+
+
+
+
+ Best ask
+
+
+ {hasData ? formatDepthPrice(model.bestAsk) : '—'}
+
+
+
+
+
+ Mid price
+
+
+ {hasData ? formatDepthPrice(model.midPrice) : '—'}
+
+
+
+
+
+ Spread
+
+
+ {hasData ? spreadLabel : '—'}
+
+
+
+
+ {showSkeleton ? (
+
+
+
+ ) : showError ? (
+
+ Market depth is temporarily unavailable.
+
+ ) : showEmpty ? (
+
+ No open orders for this key yet
+
+ ) : (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ formatCompactNumber(value)
+ }
+ />
+
+ formatDepthPrice(Number(label))
+ }
+ formatter={(
+ value: unknown,
+ name: unknown,
+ item: unknown
+ ): [string, string] | null => {
+ if (value == null) return null;
+
+ const point = (item as { payload?: DepthChartPoint })
+ ?.payload;
+ const isBid = name === 'bidVolume';
+ const levelVolume = point
+ ? isBid
+ ? point.bidAmount
+ : point.askAmount
+ : 0;
+ const cumulative =
+ typeof value === 'number'
+ ? value
+ : Number(value);
+
+ return [
+ `${formatNumber(levelVolume)} keys · ${formatNumber(cumulative)} cumulative`,
+ isBid ? 'Bids' : 'Asks',
+ ];
+ }}
+ />
+
+ {model.midPrice != null && (
+
+ )}
+
+
+
+
+
+
+ {/* Mid-price marker and spread badge, centred on the plot (#1220) */}
+ {model.midPrice != null && (
+
+
+
+ Mid {formatDepthPrice(model.midPrice)}
+
+
+ Spread {formatDepthPrice(model.spread)}
+
+
+
+ )}
+
+ )}
+
+
+ Price per fraction (XLM) · Cumulative volume (keys) · refreshed every 5s
+
+
+ );
+}
+
+export default KeyDepthChart;
diff --git a/src/components/common/__tests__/KeyDepthChart.test.tsx b/src/components/common/__tests__/KeyDepthChart.test.tsx
new file mode 100644
index 00000000..5d55b21a
--- /dev/null
+++ b/src/components/common/__tests__/KeyDepthChart.test.tsx
@@ -0,0 +1,159 @@
+import { act, render, screen, waitFor } from '@testing-library/react';
+import type { ReactNode } from 'react';
+import { beforeEach, describe, expect, it, vi } from 'vitest';
+import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
+import { KeyDepthChart } from '../KeyDepthChart';
+import { fetchKeyDepth, type KeyDepthSnapshot } from '@/services/keyDepth.service';
+import { queryKeys } from '@/lib/queryKeys';
+
+vi.mock('@/services/keyDepth.service', () => ({
+ fetchKeyDepth: vi.fn(),
+ KEY_DEPTH_LEVELS: 20,
+}));
+
+const SNAPSHOT = {
+ bids: [
+ { price: 1.05, volume: 10 },
+ { price: 1.0, volume: 20 },
+ ],
+ asks: [
+ { price: 1.15, volume: 5 },
+ { price: 1.2, volume: 15 },
+ ],
+ midPrice: null,
+ spread: null,
+};
+
+function makeWrapper() {
+ const queryClient = new QueryClient({
+ defaultOptions: { queries: { retry: false } },
+ });
+
+ return {
+ queryClient,
+ wrapper: ({ children }: { children: ReactNode }) => (
+