Skip to content
Open
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
5 changes: 5 additions & 0 deletions .changeset/lwd-globalsearch-testnets.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"ledger-live-desktop": patch
---

Global Search now surfaces testnets when developer mode is enabled, mirroring the Receive flow's DADA query logic (`includeTestNetworks` + staging environment). Fixes LIVE-33220.
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
import { renderHook, withFlagOverrides } from "tests/testSetup";
import { setEnv } from "@ledgerhq/live-env";
import { useAssetsData } from "@ledgerhq/live-common/dada-client/hooks/useAssetsData";
import { selectCurrencyForMetaId } from "@ledgerhq/live-common/dada-client/utils/currencySelection";
import { useAssetSearchResultsViewModel } from "../useAssetSearchResultsViewModel";

jest.mock("@ledgerhq/live-common/dada-client/hooks/useAssetsData");
jest.mock("@ledgerhq/live-common/dada-client/utils/currencySelection");
jest.mock("@ledgerhq/live-common/counterValues/hooks/useUsdToFiatRate", () => ({
useUsdToFiatRate: () => ({ rate: 1, status: "ready" }),
}));

const mockedAssets = jest.mocked(useAssetsData);
const mockedSelectCurrency = jest.mocked(selectCurrencyForMetaId);

// Single-network search payload (the mapper falls back to the asset id when no currency resolves).
const buildSearchData = (network: string) =>
buildAssetsData({
data: {
currenciesOrder: { metaCurrencyIds: ["amdx"], key: "marketCap", order: "desc" },
cryptoAssets: {
amdx: {
id: "amdx",
name: "AMD xStock",
ticker: "AMDX",
assetsIds: { [network]: `${network}/erc20/amd` },
},
},
markets: {},
},
} as never);

const buildAssetsData = (
overrides: Partial<ReturnType<typeof useAssetsData>> = {},
): ReturnType<typeof useAssetsData> =>
({
data: undefined,
isLoading: false,
isFetchingNextPage: false,
error: undefined,
errorInfo: undefined,
loadNext: undefined,
isSuccess: true,
isError: false,
refetch: jest.fn(),
...overrides,
}) as ReturnType<typeof useAssetsData>;

describe("useAssetSearchResultsViewModel", () => {
beforeEach(() => {
jest.clearAllMocks();
setEnv("MANAGER_DEV_MODE", false);
mockedAssets.mockReturnValue(buildAssetsData());
mockedSelectCurrency.mockReturnValue(undefined);
});

afterEach(() => {
setEnv("MANAGER_DEV_MODE", false);
});

it("excludes testnets and uses the prod environment by default", () => {
renderHook(() => useAssetSearchResultsViewModel({ search: "btc" }));

expect(mockedAssets).toHaveBeenLastCalledWith(
expect.objectContaining({ search: "btc", includeTestNetworks: false, isStaging: false }),
);
});

it("includes testnets in the query only in developer mode", () => {
setEnv("MANAGER_DEV_MODE", true);

renderHook(() => useAssetSearchResultsViewModel({ search: "btc" }));

expect(mockedAssets).toHaveBeenLastCalledWith(
expect.objectContaining({ includeTestNetworks: true }),
);
});

it("derives isStaging from the lldModularDrawer backend environment", () => {
renderHook(() => useAssetSearchResultsViewModel({ search: "btc" }), {
initialState: withFlagOverrides({
lldModularDrawer: { enabled: true, params: { backendEnvironment: "STAGING" } },
}),
});

expect(mockedAssets).toHaveBeenLastCalledWith(expect.objectContaining({ isStaging: true }));
});

it("hides a result whose only network currency flag is off", () => {
mockedAssets.mockReturnValue(buildSearchData("robinhood_testnet"));

const { result } = renderHook(() => useAssetSearchResultsViewModel({ search: "amd" }));

expect(result.current.data).toHaveLength(0);
});
Comment on lines +89 to +95

it("shows the result when its network currency flag is on", () => {
mockedAssets.mockReturnValue(buildSearchData("robinhood_testnet"));

const { result } = renderHook(() => useAssetSearchResultsViewModel({ search: "amd" }), {
initialState: withFlagOverrides({ currencyRobinhoodTestnet: { enabled: true } }),
});

expect(result.current.data).toHaveLength(1);
});
});
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
import { useMemo } from "react";
import { useAssetsData } from "@ledgerhq/live-common/dada-client/hooks/useAssetsData";
import { useUsdToFiatRate } from "@ledgerhq/live-common/counterValues/hooks/useUsdToFiatRate";
import useEnv from "@ledgerhq/live-common/hooks/useEnv";
import { useCurrenciesUnderFeatureFlag } from "@ledgerhq/live-common/modularDrawer/hooks/useCurrenciesUnderFeatureFlag";
import type { MarketCurrencyData } from "@ledgerhq/live-common/market/utils/types";
import { useFeature } from "@features/platform-feature-flags";
import { useSelector } from "LLD/hooks/redux";
import { counterValueCurrencySelector } from "~/renderer/reducers/settings";
import { mapAssetsDataToMarketCurrencies } from "../utils/mapAssetsDataToMarketCurrencies";
Expand All @@ -23,18 +26,27 @@ type Result = {
export function useAssetSearchResultsViewModel({ search, skip }: Params): Result {
const counterCurrency = useSelector(counterValueCurrencySelector).ticker;

const modularDrawer = useFeature("lldModularDrawer");
const isStaging = modularDrawer?.params?.backendEnvironment === "STAGING";
const includeTestNetworks = useEnv("MANAGER_DEV_MODE");

const { data, isLoading, isError, loadNext, isFetchingNextPage } = useAssetsData({
product: "lld",
version: __APP_VERSION__,
search,
skip,
isStaging,
includeTestNetworks,
});

const { status: rateStatus, rate } = useUsdToFiatRate(counterCurrency);

// Hide currencies disabled by a feature flag, mirroring the receive flow.
const { deactivatedCurrencyIds } = useCurrenciesUnderFeatureFlag();

const results = useMemo<MarketCurrencyData[]>(
() => mapAssetsDataToMarketCurrencies(data, rate ?? 1),
[data, rate],
() => mapAssetsDataToMarketCurrencies(data, rate ?? 1, deactivatedCurrencyIds),
[data, rate, deactivatedCurrencyIds],
);

const hasData = !!data;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,13 @@ import { applyUsdRateToMarket } from "@ledgerhq/live-common/market/utils/applyUs
import { MarketCurrencyData } from "@ledgerhq/live-common/market/utils/types";
import { buildSearchMarketCurrencyData } from "./buildSearchMarketCurrencyData";

const NO_DEACTIVATED_CURRENCIES = new Set<string>();

/** DADA prices are USD-denominated; `usdToFiatRate` converts them into the counter currency (1 = no-op). */
export function mapAssetsDataToMarketCurrencies(
data: AssetsDataWithPagination | undefined,
usdToFiatRate = 1,
deactivatedCurrencyIds: ReadonlySet<string> = NO_DEACTIVATED_CURRENCIES,
): MarketCurrencyData[] {
if (!data) return [];
const { cryptoAssets, markets, currenciesOrder } = data;
Expand All @@ -17,6 +20,13 @@ export function mapAssetsDataToMarketCurrencies(
const meta = cryptoAssets[id];
if (!meta) return [];

// Hide an asset whose every network currency is disabled by a feature flag,
// mirroring how the receive flow filters out deactivated currencies.
const networks = Object.keys(meta.assetsIds);
if (networks.length > 0 && networks.every(network => deactivatedCurrencyIds.has(network))) {
return [];
}

const currency = selectCurrencyForMetaId(id, data);
const ledgerId = currency?.id ?? Object.values(meta.assetsIds)[0];
if (!ledgerId) return [];
Expand Down
Loading