Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 7 additions & 19 deletions app/src/components/OrderBook.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { submitWithToast } from "@/lib/toastStore";
import { CopyButton } from "@/components/CopyButton";
import type { OrderInfo, TokenCurrency } from "@/lib/wallet-rpc";
import { rpc } from '@/lib/client-rpc';
import { getTokensInfoByIds } from '@/lib/token-info-client';
import { stakeTrade } from '@/lib/stake-trade-client';
import { TotpField } from '@/components/ui/TotpField';
import { aggregateLevels, fmtQty } from '@/components/orderBookDepth';
Expand Down Expand Up @@ -812,22 +813,15 @@ export default function OrderBook({ initialOwnOrders, balanceTokens = [], initia

// Resolve types and tickers for all favourites.
// NFTs are filtered out - the order book only supports fungible token pairs.
// node_get_tokens_info does not preserve input order, so call one at a time.
const allIds = favs.map(f => f.tokenId);
if (allIds.length === 0) { setFavourites(favs); return; }

try {
const results = await Promise.all(
allIds.map(id =>
rpc<[{ type: string; content: { token_ticker?: { text: string | null }; metadata?: { ticker?: { text: string | null }; name?: { text: string | null } } } } | null]>(
"node_get_tokens_info", { token_ids: [id] }
).then(([info]) => ({ id, info: info ?? null }))
.catch(() => ({ id, info: null as null }))
)
);
// Batched calls, aligned by each result's own token_id.
const infoMap = await getTokensInfoByIds(allIds);
const tickerUpdates = new Map<string, string>();
const nftIds = new Set<string>();
results.forEach(({ id, info }) => {
infoMap.forEach((info, id) => {
if (!info) return;
if (info.type !== "FungibleToken") { nftIds.add(id); return; }
const ticker = info.content.token_ticker?.text ?? null;
Expand Down Expand Up @@ -882,16 +876,10 @@ export default function OrderBook({ initialOwnOrders, balanceTokens = [], initia

unknownIds.forEach(id => resolvedTokenIds.current.add(id));

Promise.all(
unknownIds.map(id =>
rpc<[{ type: string; content: { token_ticker?: { text: string | null }; metadata?: { ticker?: { text: string | null }; name?: { text: string | null } } } } | null]>(
'node_get_tokens_info', { token_ids: [id] }
).then(([info]) => ({ id, info: info ?? null }))
.catch(() => ({ id, info: null as null }))
)
).then(results => {
// Batched calls, aligned by each result's own token_id.
getTokensInfoByIds(unknownIds).then(infoMap => {
const newMap = new Map<string, string>();
results.forEach(({ id, info }) => {
infoMap.forEach((info, id) => {
if (!info) return;
const ticker = info.type === 'FungibleToken'
? (info.content.token_ticker?.text ?? null)
Expand Down
94 changes: 94 additions & 0 deletions app/src/components/TokenSearch.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
// @vitest-environment jsdom
/** Tests for the verified-token bulk import section in TokenSearch. */

import { describe, it, expect, vi, beforeEach } from 'vitest';
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import TokenSearch from './TokenSearch';

vi.mock('@/lib/client-rpc', () => ({
rpc: vi.fn().mockResolvedValue([]),
}));

const verifiedRows = {
ok: true,
tokens: [
{
tokenId: 'tmlaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa1',
ticker: 'AAA',
displayName: 'Alpha Token',
categories: ['defi'],
circulatingSupply: '1,000',
decimals: 18,
},
{
tokenId: 'tmlbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb2',
ticker: 'BBB',
displayName: 'Beta Token',
categories: [],
circulatingSupply: '2,000',
decimals: 8,
},
],
};

const fetchMock = vi.fn();

function jsonResponse(body: unknown): Response {
return new Response(JSON.stringify(body), {
status: 200,
headers: { 'content-type': 'application/json' },
});
}

beforeEach(() => {
fetchMock.mockReset();
fetchMock.mockImplementation((url: string, init?: RequestInit) => {
if (url === '/api/indexer-status') return Promise.resolve(jsonResponse({ up: true }));
if (url === '/api/explorer-tokens') return Promise.resolve(jsonResponse(verifiedRows));
if (url === '/api/prefs') {
if (init?.method === 'POST') return Promise.resolve(jsonResponse({ ok: true }));
return Promise.resolve(jsonResponse({ ok: true, value: [] }));
}
return Promise.reject(new Error(`unexpected fetch ${url}`));
});
vi.stubGlobal('fetch', fetchMock);
});

describe('TokenSearch verified-token import', () => {
it('loads the verified list and imports the selection into favourites', async () => {
const user = userEvent.setup();
render(<TokenSearch network="mainnet" />);

await user.click(screen.getByRole('button', { name: 'Browse verified tokens' }));
expect(await screen.findByText('Alpha Token')).toBeInTheDocument();
expect(screen.getByText('Beta Token')).toBeInTheDocument();

await user.click(screen.getByRole('checkbox', { name: /select all/i }));
await user.click(screen.getByRole('button', { name: /import selected \(2\)/i }));

expect(await screen.findByText(/Imported 2 tokens\./)).toBeInTheDocument();

const post = fetchMock.mock.calls.find(([, init]) => init?.method === 'POST');
expect(post).toBeDefined();
const body = JSON.parse(String((post?.[1] as RequestInit).body)) as Array<Record<string, string>>;
expect(body.map((e) => e.ticker)).toEqual(['AAA', 'BBB']);
expect(body[0]?.tokenId).toContain('aaaa');
});

it('reports the unavailability of the explorer list', async () => {
const user = userEvent.setup();
fetchMock.mockImplementation((url: string) => {
if (url === '/api/indexer-status') return Promise.resolve(jsonResponse({ up: true }));
if (url === '/api/explorer-tokens') {
return Promise.resolve(jsonResponse({ ok: false, error: 'Token list is unavailable' }));
}
if (url === '/api/prefs') return Promise.resolve(jsonResponse({ ok: true, value: [] }));
return Promise.reject(new Error(`unexpected fetch ${url}`));
});

render(<TokenSearch network="mainnet" />);
await user.click(screen.getByRole('button', { name: 'Browse verified tokens' }));
expect(await screen.findByText('Token list is unavailable')).toBeInTheDocument();
});
});
Loading
Loading