From f4d5dd68e01d218472424953d25d92db173765ac Mon Sep 17 00:00:00 2001 From: Copilot <223556219+Copilot@users.noreply.github.com> Date: Thu, 4 Jun 2026 18:31:11 -0700 Subject: [PATCH 1/2] Sort token pickers by USD value via TokenList Apply sortBalancesByValue inside the TokenList component so that both the Send and Swap flow token pickers display tokens in descending USD value order, consistent with the Account view. This centralizes the ordering policy in the shared picker renderer, eliminating the class of bug where a new caller of TokenList forgets to sort. Add unit tests covering value-sorted rendering, empty-price no-op behavior, LP share exclusion, and hidden asset filtering. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../TokenList/__tests__/TokenList.test.tsx | 153 ++++++++++++++++++ .../InternalTransaction/TokenList/index.tsx | 6 +- 2 files changed, 157 insertions(+), 2 deletions(-) create mode 100644 extension/src/popup/components/InternalTransaction/TokenList/__tests__/TokenList.test.tsx diff --git a/extension/src/popup/components/InternalTransaction/TokenList/__tests__/TokenList.test.tsx b/extension/src/popup/components/InternalTransaction/TokenList/__tests__/TokenList.test.tsx new file mode 100644 index 0000000000..352d00ed8c --- /dev/null +++ b/extension/src/popup/components/InternalTransaction/TokenList/__tests__/TokenList.test.tsx @@ -0,0 +1,153 @@ +import React from "react"; +import { render, screen } from "@testing-library/react"; +import BigNumber from "bignumber.js"; + +import { defaultBlockaidScanAssetResult } from "@shared/helpers/stellar"; + +import { TokenList } from "../index"; + +jest.mock("react-i18next", () => ({ + useTranslation: () => ({ t: (key: string) => key }), +})); + +jest.mock("popup/components/account/AccountAssets", () => ({ + AssetIcon: () =>
, +})); + +const USDC_ISSUER = "GCK3D3V2XNLLKRFGFFFDEJXA4O2J4X36HET2FE446AV3M4U7DPHO3PEM"; +const AQUA_ISSUER = "GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5"; + +const USDC_CANONICAL = `USDC:${USDC_ISSUER}`; +const AQUA_CANONICAL = `AQUA:${AQUA_ISSUER}`; + +const XLM_BALANCE = { + token: { code: "XLM", type: "native" as const }, + total: new BigNumber("500"), + available: new BigNumber("500"), + blockaidData: defaultBlockaidScanAssetResult, +}; + +const USDC_BALANCE = { + token: { + code: "USDC", + issuer: { key: USDC_ISSUER }, + }, + total: new BigNumber("200"), + available: new BigNumber("200"), + blockaidData: defaultBlockaidScanAssetResult, +}; + +const AQUA_BALANCE = { + token: { + code: "AQUA", + issuer: { key: AQUA_ISSUER }, + }, + total: new BigNumber("10000"), + available: new BigNumber("10000"), + blockaidData: defaultBlockaidScanAssetResult, +}; + +const LP_BALANCE = { + liquidityPoolId: + "a468d41d8e9b8f3c7209651608b74b7db7ac9952dcae0cdf24871d1d9c7b0088", + total: new BigNumber("10"), + limit: new BigNumber("100"), +}; + +const TOKEN_PRICES = { + native: { currentPrice: "0.10" }, + [USDC_CANONICAL]: { currentPrice: "1.00" }, + [AQUA_CANONICAL]: { currentPrice: "0.005" }, +}; + +const defaultProps = { + icons: {}, + tokenPrices: TOKEN_PRICES, + onClickAsset: jest.fn(), + hiddenAssets: [] as string[], +}; + +describe("TokenList", () => { + describe("value sort ordering", () => { + it("renders tokens in descending USD value order", () => { + // USDC: 200 * 1.00 = $200 + // XLM: 500 * 0.10 = $50 + // AQUA: 10000 * 0.005 = $50 (tie with XLM, preserves input order) + const tokens = [XLM_BALANCE, USDC_BALANCE, AQUA_BALANCE] as any[]; + + render(); + + const rows = screen.getAllByTestId(/^SendRow-/); + expect(rows[0]).toHaveAttribute("data-testid", `SendRow-${USDC_CANONICAL}`); + expect(rows[1]).toHaveAttribute("data-testid", "SendRow-native"); + expect(rows[2]).toHaveAttribute("data-testid", `SendRow-${AQUA_CANONICAL}`); + }); + + it("preserves original order when tokenPrices is empty", () => { + const tokens = [XLM_BALANCE, USDC_BALANCE, AQUA_BALANCE] as any[]; + + render(); + + const rows = screen.getAllByTestId(/^SendRow-/); + expect(rows[0]).toHaveAttribute("data-testid", "SendRow-native"); + expect(rows[1]).toHaveAttribute("data-testid", `SendRow-${USDC_CANONICAL}`); + expect(rows[2]).toHaveAttribute("data-testid", `SendRow-${AQUA_CANONICAL}`); + }); + + it("sorts priced assets before unpriced ones", () => { + const partialPrices = { + [USDC_CANONICAL]: { currentPrice: "1.00" }, + }; + const tokens = [XLM_BALANCE, AQUA_BALANCE, USDC_BALANCE] as any[]; + + render( + , + ); + + const rows = screen.getAllByTestId(/^SendRow-/); + // USDC (priced) comes first + expect(rows[0]).toHaveAttribute("data-testid", `SendRow-${USDC_CANONICAL}`); + // Unpriced assets preserve input order + expect(rows[1]).toHaveAttribute("data-testid", "SendRow-native"); + expect(rows[2]).toHaveAttribute("data-testid", `SendRow-${AQUA_CANONICAL}`); + }); + }); + + describe("filtering", () => { + it("excludes LP share assets from the list", () => { + const tokens = [XLM_BALANCE, LP_BALANCE, USDC_BALANCE] as any[]; + + render(); + + const rows = screen.getAllByTestId(/^SendRow-/); + expect(rows).toHaveLength(2); + expect(rows[0]).toHaveAttribute("data-testid", `SendRow-${USDC_CANONICAL}`); + expect(rows[1]).toHaveAttribute("data-testid", "SendRow-native"); + }); + + it("excludes hidden assets from the list", () => { + const tokens = [XLM_BALANCE, USDC_BALANCE, AQUA_BALANCE] as any[]; + const hiddenAssets = [USDC_CANONICAL]; + + render( + , + ); + + const rows = screen.getAllByTestId(/^SendRow-/); + expect(rows).toHaveLength(2); + // USDC is hidden, XLM and AQUA remain (sorted by value) + expect(rows[0]).toHaveAttribute("data-testid", "SendRow-native"); + expect(rows[1]).toHaveAttribute("data-testid", `SendRow-${AQUA_CANONICAL}`); + }); + }); + + it("shows empty state when no tokens provided", () => { + render(); + + expect( + screen.getByText( + "You have no assets added. Get started by adding an asset.", + ), + ).toBeDefined(); + }); +}); diff --git a/extension/src/popup/components/InternalTransaction/TokenList/index.tsx b/extension/src/popup/components/InternalTransaction/TokenList/index.tsx index ec46737495..8c6770403e 100644 --- a/extension/src/popup/components/InternalTransaction/TokenList/index.tsx +++ b/extension/src/popup/components/InternalTransaction/TokenList/index.tsx @@ -11,6 +11,7 @@ import { getCanonicalFromAsset } from "helpers/stellar"; import { ApiTokenPrices, AssetIcons } from "@shared/api/types"; import { getAvailableBalance } from "popup/helpers/soroban"; import { formatAmount, roundUsdValue } from "popup/helpers/formatters"; +import { sortBalancesByValue } from "popup/helpers/balance"; import { AssetIcon } from "popup/components/account/AccountAssets"; import { title } from "helpers/transaction"; @@ -34,6 +35,7 @@ export const TokenList = ({ isShowingHeader, }: TokenListProps) => { const { t } = useTranslation(); + const sortedTokens = sortBalancesByValue(tokens, tokenPrices); return (
- {!tokens.length ? ( + {!sortedTokens.length ? (
{`${t("You have no assets added.")} ${t("Get started by adding an asset.")}`}
@@ -50,7 +52,7 @@ export const TokenList = ({ {isShowingHeader && (
{t("Your Tokens")}
)} - {tokens + {sortedTokens .filter( ( balance, From be99614c792df1bee4c41602af4a936cfa17a503 Mon Sep 17 00:00:00 2001 From: Copilot <223556219+Copilot@users.noreply.github.com> Date: Fri, 5 Jun 2026 10:11:12 -0700 Subject: [PATCH 2/2] Memoize sortBalancesByValue in TokenList Wrap the sortBalancesByValue call in useMemo to avoid resorting on unrelated re-renders (e.g., debounced keystroke re-renders in Swap). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../components/InternalTransaction/TokenList/index.tsx | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/extension/src/popup/components/InternalTransaction/TokenList/index.tsx b/extension/src/popup/components/InternalTransaction/TokenList/index.tsx index 8c6770403e..079685ac25 100644 --- a/extension/src/popup/components/InternalTransaction/TokenList/index.tsx +++ b/extension/src/popup/components/InternalTransaction/TokenList/index.tsx @@ -1,4 +1,4 @@ -import React from "react"; +import React, { useMemo } from "react"; import { useTranslation } from "react-i18next"; import BigNumber from "bignumber.js"; import classnames from "classnames"; @@ -35,7 +35,10 @@ export const TokenList = ({ isShowingHeader, }: TokenListProps) => { const { t } = useTranslation(); - const sortedTokens = sortBalancesByValue(tokens, tokenPrices); + const sortedTokens = useMemo( + () => sortBalancesByValue(tokens, tokenPrices), + [tokens, tokenPrices], + ); return (