diff --git a/packages/webapp/.env.example b/packages/webapp/.env.example index 282fe01e1d..4ebca51c5a 100644 --- a/packages/webapp/.env.example +++ b/packages/webapp/.env.example @@ -1 +1,2 @@ -PUBLIC_WALLETCONNECT_PROJECT_ID= \ No newline at end of file +PUBLIC_WALLETCONNECT_PROJECT_ID= +PUBLIC_MARKET_DATA_API_URL=http://127.0.0.1:8000 diff --git a/packages/webapp/ARCHITECTURE.md b/packages/webapp/ARCHITECTURE.md index 808e700414..4bfd43508a 100644 --- a/packages/webapp/ARCHITECTURE.md +++ b/packages/webapp/ARCHITECTURE.md @@ -77,6 +77,10 @@ nix develop -c npm run dev and bulk withdraw. - Detail route: `/vaults/[chainId]-[raindex]-[id]` (components handle the heavy lifting inside `ui-components`). +- `/markets` + - Public Raindex market page for direct quote-token markets discovered from + active Raindex orders. It renders the same SDK snapshot model served by the + market-data REST API, including 24-hour statistics and executable depth. - `/deploy` - Loads a dotrain registry (`?registry=` query param or default `REGISTRY_URL`), validates orders, and shows valid/invalid sections. @@ -130,6 +134,7 @@ Run inside a Nix shell for tool parity (`nix develop -c `): ```bash PUBLIC_WALLETCONNECT_PROJECT_ID= +PUBLIC_MARKET_DATA_API_URL=https://api.raindex.finance ``` - Notes diff --git a/packages/webapp/src/__tests__/Sidebar.test.ts b/packages/webapp/src/__tests__/Sidebar.test.ts index 103898e88f..ccfc22069c 100644 --- a/packages/webapp/src/__tests__/Sidebar.test.ts +++ b/packages/webapp/src/__tests__/Sidebar.test.ts @@ -48,7 +48,11 @@ vi.mock('svelte/store', async (importOriginal) => { }); const mockWindowSize = (width: number) => { - Object.defineProperty(window, 'innerWidth', { writable: true, configurable: true, value: width }); + Object.defineProperty(window, 'innerWidth', { + writable: true, + configurable: true, + value: width + }); window.dispatchEvent(new Event('resize')); }; @@ -72,6 +76,22 @@ describe('Sidebar', () => { expect(container).toBeTruthy(); }); + it('shows Markets immediately after Vaults', () => { + mockWindowSize(1025); + const mockColorTheme = writable('light'); + const mockPage = { + url: { pathname: '/markets' } + }; + render(Sidebar, { colorTheme: mockColorTheme, page: mockPage }); + + const vaults = screen.getByTestId('sidebar-vaults').closest('a'); + const markets = screen.getByTestId('sidebar-markets').closest('a'); + expect(vaults).not.toBeNull(); + expect(markets).not.toBeNull(); + expect(markets).toHaveAttribute('href', '/markets'); + expect(vaults!.compareDocumentPosition(markets!) & Node.DOCUMENT_POSITION_FOLLOWING).toBeTruthy(); + expect(vaults!.parentElement?.nextElementSibling).toContainElement(markets); + }); it('renders menu bars button when screen width is small', () => { // Mock small screen width mockWindowSize(500); diff --git a/packages/webapp/src/lib/components/MarketDetailPanel.svelte b/packages/webapp/src/lib/components/MarketDetailPanel.svelte new file mode 100644 index 0000000000..818df30802 --- /dev/null +++ b/packages/webapp/src/lib/components/MarketDetailPanel.svelte @@ -0,0 +1,226 @@ + + + diff --git a/packages/webapp/src/lib/components/MarketDetailPanel.test.ts b/packages/webapp/src/lib/components/MarketDetailPanel.test.ts new file mode 100644 index 0000000000..501a16a1f0 --- /dev/null +++ b/packages/webapp/src/lib/components/MarketDetailPanel.test.ts @@ -0,0 +1,112 @@ +import { render, screen } from '@testing-library/svelte'; +import { describe, expect, it, vi } from 'vitest'; +import type { RaindexMarketSnapshot } from '@rainlanguage/raindex'; +import MarketDetailPanel from './MarketDetailPanel.svelte'; + +const queryState = vi.hoisted(() => ({ value: {} as Record })); + +vi.mock('$env/dynamic/public', () => ({ env: {} })); + +vi.mock('@tanstack/svelte-query', () => ({ + createQuery: () => ({ + subscribe(run: (value: Record) => void) { + run(queryState.value); + return () => undefined; + } + }) +})); + +const snapshot: RaindexMarketSnapshot = { + market: { + id: '8453:base:quote', + tickerId: '0xbase_0xquote', + chainId: 8453, + base: { + chainId: 8453, + address: '0xbase', + name: 'Base', + symbol: 'BASE', + decimals: 18, + logoUri: 'https://assets.example/base.png', + variants: [] + }, + quote: { + chainId: 8453, + address: '0xquote', + name: 'Quote', + symbol: 'QUOTE', + decimals: 6, + logoUri: 'https://assets.example/quote.png', + variants: [] + }, + raindexAddresses: ['0xraindex'] + }, + orderbook: { + bestBid: '99', + bestAsk: '101', + bids: [ + { + price: '99', + baseQuantity: '1', + targetQuantity: '99', + chainId: 8453, + raindex: '0xraindex', + orderHash: '0xshared', + sourceToken: '0xinput-a', + blockNumber: 1 + }, + { + price: '98', + baseQuantity: '2', + targetQuantity: '196', + chainId: 8453, + raindex: '0xraindex', + orderHash: '0xshared', + sourceToken: '0xinput-b', + blockNumber: 1 + } + ], + asks: [] + }, + stats: { + lastPrice: '100.123456789', + high24h: '102', + low24h: '97', + baseVolume24h: '12.5', + targetVolume24h: '1250', + tradeCount24h: 3 + }, + recentTrades: [], + observedAt: 100, + errors: [] +}; + +describe('MarketDetailPanel', () => { + it('keeps cached detail visible after refresh failure and renders repeated order hashes', () => { + queryState.value = { + data: snapshot, + isPending: false, + isError: true, + isRefetchError: true, + refetch: vi.fn() + }; + + render(MarketDetailPanel, { + props: { tickerId: snapshot.market.tickerId } + }); + + expect(screen.getByText('Cached quote · refresh failed')).toBeInTheDocument(); + expect(screen.getByRole('heading', { name: 'Orderbook' })).toBeInTheDocument(); + expect( + document.querySelector('img[src="https://assets.example/base.png"]') + ).toBeInTheDocument(); + expect( + document.querySelector('img[src="https://assets.example/quote.png"]') + ).toBeInTheDocument(); + expect(screen.getByText('100.12346')).toBeInTheDocument(); + expect(screen.getByText('Exact API values')).toBeInTheDocument(); + expect(screen.getByText('100.123456789')).toBeInTheDocument(); + expect(screen.getByTitle('Price 99, size 1')).toBeInTheDocument(); + expect(screen.getByTitle('Price 98, size 2')).toBeInTheDocument(); + }); +}); diff --git a/packages/webapp/src/lib/components/MarketsTable.svelte b/packages/webapp/src/lib/components/MarketsTable.svelte new file mode 100644 index 0000000000..7158bc6335 --- /dev/null +++ b/packages/webapp/src/lib/components/MarketsTable.svelte @@ -0,0 +1,146 @@ + + +
+ + + + + + + + + + + + + {#each markets as snapshot (snapshot.market.id)} + + + + + + + + + {/each} + +
MarketLast priceInspect market
+
+ +
+
+ {snapshot.market.base.symbol}{" "}/{" "}{snapshot.market.quote.symbol} +
+ +
+
+
+ {formatPrice(snapshot.stats.lastPrice)} + + + +
+
diff --git a/packages/webapp/src/lib/components/MarketsTable.test.ts b/packages/webapp/src/lib/components/MarketsTable.test.ts new file mode 100644 index 0000000000..fb96e0e0d9 --- /dev/null +++ b/packages/webapp/src/lib/components/MarketsTable.test.ts @@ -0,0 +1,68 @@ +import { fireEvent, render, screen } from "@testing-library/svelte"; +import { describe, expect, it, vi } from "vitest"; +import MarketsTable from "./MarketsTable.svelte"; +import type { RaindexMarketSnapshot } from "@rainlanguage/raindex"; + +const snapshot: RaindexMarketSnapshot = { + market: { + id: "8453:base:quote", + tickerId: "0xbase_0xquote", + chainId: 8453, + base: { + chainId: 8453, + address: "0x1111111111111111111111111111111111111111", + name: "Base token", + symbol: "BASE", + decimals: 18, + logoUri: "https://assets.example/base.png", + variants: [], + }, + quote: { + chainId: 8453, + address: "0x2222222222222222222222222222222222222222", + name: "USD Coin", + symbol: "USDC", + decimals: 6, + logoUri: "https://assets.example/usdc.png", + variants: [], + }, + raindexAddresses: [], + }, + orderbook: { bids: [], asks: [] }, + stats: { + lastPrice: "184.2283035796", + high24h: "190", + low24h: "176", + baseVolume24h: "617.44", + targetVolume24h: "112854.02", + tradeCount24h: 190, + }, + recentTrades: [], + observedAt: 100, + errors: [], +}; + +describe("MarketsTable", () => { + it("shows active market statistics and selects executable depth", async () => { + const onSelect = vi.fn(); + render(MarketsTable, { + props: { markets: [snapshot], selectedTickerId: undefined, onSelect }, + }); + + const row = screen.getByTestId("market-row"); + expect(row).toHaveTextContent(/BASE.*USDC/); + expect(row).toHaveTextContent("112.854K USDC"); + expect(row).toHaveTextContent("190"); + expect( + row.querySelector('img[src="https://assets.example/base.png"]'), + ).toBeInTheDocument(); + expect( + row.querySelector('img[src="https://assets.example/usdc.png"]'), + ).toBeInTheDocument(); + + await fireEvent.click( + screen.getByRole("button", { name: "View BASE / USDC orderbook depth" }), + ); + expect(onSelect).toHaveBeenCalledWith(snapshot.market.tickerId); + }); +}); diff --git a/packages/webapp/src/lib/components/Sidebar.svelte b/packages/webapp/src/lib/components/Sidebar.svelte index 8950f60c59..1412160ebe 100644 --- a/packages/webapp/src/lib/components/Sidebar.svelte +++ b/packages/webapp/src/lib/components/Sidebar.svelte @@ -11,6 +11,7 @@ import { WalletSolid, ReceiptSolid, + ChartMixedSolid, FileLinesSolid, PlusOutline, BarsSolid @@ -113,6 +114,12 @@ + + + + + + diff --git a/packages/webapp/src/lib/components/TokenPairLogos.svelte b/packages/webapp/src/lib/components/TokenPairLogos.svelte new file mode 100644 index 0000000000..1f64911129 --- /dev/null +++ b/packages/webapp/src/lib/components/TokenPairLogos.svelte @@ -0,0 +1,34 @@ + + + diff --git a/packages/webapp/src/lib/constants.ts b/packages/webapp/src/lib/constants.ts index 7a29f27029..5fc8e7bfbd 100644 --- a/packages/webapp/src/lib/constants.ts +++ b/packages/webapp/src/lib/constants.ts @@ -1,2 +1,2 @@ export const REGISTRY_URL = - "https://raw.githubusercontent.com/rainlanguage/rain.strategies/2f170669a23407411d2ea403a0d0a397bf880980/registry"; + "https://raw.githubusercontent.com/rainlanguage/rain.strategies/de1d6990052d5003cadf4550fed4a2bfdf9560ff/registry"; diff --git a/packages/webapp/src/lib/market-api/client.test.ts b/packages/webapp/src/lib/market-api/client.test.ts new file mode 100644 index 0000000000..a5ab494402 --- /dev/null +++ b/packages/webapp/src/lib/market-api/client.test.ts @@ -0,0 +1,84 @@ +import { describe, expect, it, vi } from "vitest"; +import { getMarket, getMarkets, MarketApiError, marketApiUrl } from "./client"; + +vi.mock("$env/dynamic/public", () => ({ env: {} })); + +const fetcher = (response: Response) => + vi.fn().mockResolvedValue(response) as unknown as typeof fetch; + +describe("market API client", () => { + it("uses the configured public URL without a trailing slash", () => { + expect(marketApiUrl("https://markets.example/")).toBe( + "https://markets.example", + ); + }); + + it("defaults to the production market API", () => { + expect(marketApiUrl()).toBe("https://api.raindex.finance"); + }); + + it("loads the cached market overview", async () => { + const request = fetcher( + Response.json([{ market: { tickerId: "base_quote" } }]), + ); + + const markets = await getMarkets(request, "https://markets.example"); + + expect(markets).toHaveLength(1); + expect(request).toHaveBeenCalledWith("https://markets.example/v1/markets", { + headers: { Accept: "application/json" }, + signal: undefined, + }); + }); + + it("encodes ticker IDs when loading executable detail", async () => { + const request = fetcher( + Response.json([{ market: { tickerId: "BASE / QUOTE" } }]), + ); + + await getMarket("BASE / QUOTE", request, "https://markets.example"); + + expect(request).toHaveBeenCalledWith( + "https://markets.example/v1/markets?ticker_id=BASE%20%2F%20QUOTE", + expect.any(Object), + ); + }); + + it("forwards cancellation to the market service", async () => { + const request = fetcher(Response.json([])); + const controller = new AbortController(); + + await getMarkets(request, "https://markets.example", controller.signal); + + expect(request).toHaveBeenCalledWith( + "https://markets.example/v1/markets", + expect.objectContaining({ signal: controller.signal }), + ); + }); + + it("preserves structured service errors and request IDs", async () => { + const request = fetcher( + Response.json( + { + request_id: "request-1", + error: { + code: "UPSTREAM_UNAVAILABLE", + message: "market data is unavailable", + }, + }, + { status: 503 }, + ), + ); + + const error = await getMarkets(request, "https://markets.example").catch(( + reason, + ) => reason); + + expect(error).toBeInstanceOf(MarketApiError); + expect(error).toMatchObject({ + message: "market data is unavailable", + status: 503, + requestId: "request-1", + }); + }); +}); diff --git a/packages/webapp/src/lib/market-api/client.ts b/packages/webapp/src/lib/market-api/client.ts new file mode 100644 index 0000000000..87701c5fca --- /dev/null +++ b/packages/webapp/src/lib/market-api/client.ts @@ -0,0 +1,85 @@ +import { env } from "$env/dynamic/public"; +import type { RaindexMarketSnapshot } from "@rainlanguage/raindex"; + +type MarketApiErrorBody = { + request_id?: string; + error?: { + code?: string; + message?: string; + }; +}; + +export const DEFAULT_MARKET_API_URL = "https://api.raindex.finance"; + +export class MarketApiError extends Error { + constructor( + message: string, + public readonly status?: number, + public readonly requestId?: string, + ) { + super(message); + this.name = "MarketApiError"; + } +} + +export function marketApiUrl( + configured = env.PUBLIC_MARKET_DATA_API_URL, +): string { + return (configured?.trim() || DEFAULT_MARKET_API_URL).replace(/\/$/, ""); +} + +async function readError(response: Response): Promise { + const body = await response + .clone() + .json() + .catch(() => undefined) as MarketApiErrorBody | undefined; + return new MarketApiError( + body?.error?.message || + `Market API request failed with status ${response.status}`, + response.status, + body?.request_id, + ); +} + +async function getSnapshots( + path: string, + fetcher: typeof fetch = fetch, + baseUrl = marketApiUrl(), + signal?: AbortSignal, +): Promise { + const response = await fetcher(`${baseUrl}${path}`, { + headers: { Accept: "application/json" }, + signal, + }); + if (!response.ok) throw await readError(response); + const snapshots: unknown = await response.json(); + if (!Array.isArray(snapshots)) { + throw new MarketApiError("Market API returned an invalid response"); + } + return snapshots as RaindexMarketSnapshot[]; +} + +export function getMarkets( + fetcher?: typeof fetch, + baseUrl?: string, + signal?: AbortSignal, +): Promise { + return getSnapshots("/v1/markets", fetcher, baseUrl, signal); +} + +export async function getMarket( + tickerId: string, + fetcher?: typeof fetch, + baseUrl?: string, + signal?: AbortSignal, +): Promise { + const snapshots = await getSnapshots( + `/v1/markets?ticker_id=${encodeURIComponent(tickerId)}`, + fetcher, + baseUrl, + signal, + ); + const market = snapshots[0]; + if (!market) throw new MarketApiError(`Unknown market: ${tickerId}`, 404); + return market; +} diff --git a/packages/webapp/src/lib/market-api/format.test.ts b/packages/webapp/src/lib/market-api/format.test.ts new file mode 100644 index 0000000000..65d74f5f7f --- /dev/null +++ b/packages/webapp/src/lib/market-api/format.test.ts @@ -0,0 +1,26 @@ +import { describe, expect, it } from "vitest"; +import { + formatPrice, + formatSpread, + formatVolume, + shortAddress, +} from "./format"; + +describe("market display formatting", () => { + it("formats API prices for scanning and exposes missing values", () => { + expect(formatPrice("184.22830357961256795276")).toBe("184.2283"); + expect(formatPrice()).toBe("—"); + }); + + it("formats executable spread from bid and ask", () => { + expect(formatSpread("99", "101")).toBe("2.00%"); + expect(formatSpread(undefined, "101")).toBe("—"); + expect(formatSpread(null, "101")).toBe("—"); + expect(formatSpread("101", "99")).toBe("—"); + }); + + it("compacts API volumes and shortens contract addresses", () => { + expect(formatVolume("112854.021")).toBe("112.854K"); + expect(shortAddress("0x1234567890abcdef")).toBe("0x1234…cdef"); + }); +}); diff --git a/packages/webapp/src/lib/market-api/format.ts b/packages/webapp/src/lib/market-api/format.ts new file mode 100644 index 0000000000..b866fc8dd1 --- /dev/null +++ b/packages/webapp/src/lib/market-api/format.ts @@ -0,0 +1,40 @@ +export function formatPrice(value?: string | null): string { + if (!value) return "—"; + const number = Number(value); + return Number.isFinite(number) + ? new Intl.NumberFormat("en-US", { + maximumSignificantDigits: 8, + useGrouping: false, + }).format(number) + : value; +} + +export function formatVolume(value: string): string { + const number = Number(value); + return Number.isFinite(number) + ? new Intl.NumberFormat("en-US", { + notation: "compact", + maximumSignificantDigits: 6, + }).format(number) + : value; +} + +export function formatSpread( + bestBid?: string | null, + bestAsk?: string | null, +): string { + if (!bestBid || !bestAsk) return "—"; + const bid = Number(bestBid); + const ask = Number(bestAsk); + if (!Number.isFinite(bid) || !Number.isFinite(ask) || ask < bid) return "—"; + const midpoint = (bid + ask) / 2; + return midpoint > 0 + ? `${(((ask - bid) / midpoint) * 100).toFixed(2)}%` + : "—"; +} + +export function shortAddress(address: string): string { + return address.length > 12 + ? `${address.slice(0, 6)}…${address.slice(-4)}` + : address; +} diff --git a/packages/webapp/src/routes/markets/+page.svelte b/packages/webapp/src/routes/markets/+page.svelte new file mode 100644 index 0000000000..de54ea5b88 --- /dev/null +++ b/packages/webapp/src/routes/markets/+page.svelte @@ -0,0 +1,232 @@ + + + + +
+
+
+

+ Raindex market data +

+
+
+
+ {markets.length} +
+
Markets
+
+
+
+ {activeMarketCount} +
+
Traded 24h
+
+
+
+ {networkCount} +
+
Networks
+
+
+
+
+ +
+ +
+ {#if $marketsQuery.isRefetchError && markets.length > 0} + + + Cached · refresh failed + + {/if} + {#if latestObservation > 0} + Observed {new Date( + latestObservation * 1000, + ).toLocaleTimeString()} + {/if} + +
+
+ + {#if $marketsQuery.isPending} +
+
+ {#each [0, 1, 2, 3, 4, 5, 6] as skeleton (skeleton)} +
+ {/each} +
+
+ {:else if $marketsQuery.isError && markets.length === 0} + + {:else if markets.length === 0} +
+

+ No active Raindex markets +

+

+ No active Raindex orders currently provide a direct pair with the + configured quote token. +

+
+ {:else} +
+
+ {#if filteredMarkets.length > 0} + (selectedTickerId = tickerId)} + /> + {:else} +
+

+ No matching markets +

+

+ Try a token symbol, contract address, or chain ID. +

+
+ {/if} +
+
+ +
+
+ {/if} +
diff --git a/packages/webapp/src/routes/markets/page.test.ts b/packages/webapp/src/routes/markets/page.test.ts new file mode 100644 index 0000000000..a07f3a328d --- /dev/null +++ b/packages/webapp/src/routes/markets/page.test.ts @@ -0,0 +1,154 @@ +import { fireEvent, render, screen } from "@testing-library/svelte"; +import type { RaindexMarketSnapshot } from "@rainlanguage/raindex"; +import { tick } from "svelte"; +import { beforeEach, describe, expect, it, vi } from "vitest"; +import Page from "./+page.svelte"; + +const queryMock = vi.hoisted(() => { + let state: Record; + const subscribers = new Set<(value: Record) => void>(); + return { + reset() { + state = { + data: [], + isPending: false, + isError: false, + isFetching: false, + isRefetchError: false, + refetch: vi.fn(), + }; + for (const subscriber of subscribers) subscriber(state); + }, + setData(data: RaindexMarketSnapshot[]) { + state = { ...state, data }; + for (const subscriber of subscribers) subscriber(state); + }, + subscribe(run: (value: Record) => void) { + subscribers.add(run); + run(state); + return () => subscribers.delete(run); + }, + }; +}); + +vi.mock("$app/stores", () => ({ + page: { + subscribe(run: (value: { url: URL }) => void) { + run({ url: new URL("http://localhost/markets") }); + return () => undefined; + }, + }, +})); + +vi.mock("$env/dynamic/public", () => ({ env: {} })); + +vi.mock("@rainlanguage/ui-components", async () => { + const MockComponent = ( + await import("../../lib/__mocks__/MockComponent.svelte") + ).default; + return { PageHeader: MockComponent, getNetworkName: () => "Base" }; +}); + +vi.mock("@tanstack/svelte-query", () => ({ + createQuery: () => ({ subscribe: queryMock.subscribe }), +})); + +vi.mock("$lib/components/MarketDetailPanel.svelte", async () => ({ + default: (await import("../../lib/__mocks__/MockComponent.svelte")).default, +})); + +function snapshot( + symbol: string, + tradeCount24h: number, +): RaindexMarketSnapshot { + const addressDigit = symbol === "OLD" ? "1" : "2"; + const tickerId = `${symbol.toLowerCase()}_usdc`; + return { + market: { + id: `8453:${tickerId}`, + tickerId, + chainId: 8453, + base: { + chainId: 8453, + address: `0x${addressDigit.repeat(40)}`, + name: symbol, + symbol, + decimals: 18, + variants: [], + }, + quote: { + chainId: 8453, + address: "0x3333333333333333333333333333333333333333", + name: "USD Coin", + symbol: "USDC", + decimals: 6, + variants: [], + }, + raindexAddresses: [], + }, + orderbook: { bids: [], asks: [] }, + stats: { + lastPrice: "10", + baseVolume24h: "0", + targetVolume24h: "0", + tradeCount24h, + }, + recentTrades: [], + observedAt: 100, + errors: [], + }; +} + +describe("Raindex market data page", () => { + beforeEach(() => queryMock.reset()); + + it("treats an empty active market set as a healthy state", () => { + render(Page); + + expect( + screen.getByRole("heading", { name: "Raindex market data" }), + ).toBeInTheDocument(); + expect( + screen.getByRole("heading", { name: "No active Raindex markets" }), + ).toBeInTheDocument(); + expect( + screen.getByText( + "No active Raindex orders currently provide a direct pair with the configured quote token.", + ), + ).toBeInTheDocument(); + expect( + screen.queryByText("Market service is unavailable"), + ).not.toBeInTheDocument(); + }); + + it("counts only markets with trades in the last 24 hours", () => { + queryMock.setData([snapshot("OLD", 0), snapshot("LIVE", 2)]); + render(Page); + + expect(screen.getByText("Traded 24h").parentElement).toHaveTextContent("1"); + }); + + it("clears a selected market after it leaves the active overview", async () => { + const selected = snapshot("OLD", 1); + const remaining = snapshot("LIVE", 1); + queryMock.setData([selected, remaining]); + render(Page); + + await fireEvent.click( + screen.getByRole("button", { name: "View OLD / USDC orderbook depth" }), + ); + expect( + screen.getByRole("button", { name: "OLD / USDC depth selected" }), + ).toBeInTheDocument(); + + queryMock.setData([remaining]); + await tick(); + + expect( + screen.queryByRole("button", { name: "OLD / USDC depth selected" }), + ).not.toBeInTheDocument(); + expect( + screen.getByRole("button", { name: "View LIVE / USDC orderbook depth" }), + ).toBeInTheDocument(); + }); +});