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
13 changes: 13 additions & 0 deletions @shared/api/helpers/getIconFromTokenList.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ import {
import { NetworkDetails } from "@shared/constants/stellar";
import { getCanonicalFromAsset } from "@shared/helpers/stellar";

import { sendMessageToBackground } from "./extensionMessaging";
import { SERVICE_TYPES } from "../../constants/services";

export const getIconFromTokenLists = async ({
issuerId,
contractId,
Expand Down Expand Up @@ -36,6 +39,7 @@ export const getIconFromTokenLists = async ({
issuerId &&
record.issuer &&
record.issuer === issuerId &&
record.code === code &&
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is a small bug I found while testing - we need to check the asset issuer AND asset code because one G address could be the issuer for multiple assets

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

wdyt about adding a test that asserts that two assets from the same issuer get the correct icon?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ah nice, ty

record.icon
) {
verifiedToken = record;
Expand All @@ -46,6 +50,15 @@ export const getIconFromTokenLists = async ({
}
}

if (verifiedToken?.icon) {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if we find an icon, let's cache it in the background so in the future, we don't need to consult the token list at all; we'll already have the url in our background

await sendMessageToBackground({
activePublicKey: null,
assetCanonical: `${code}:${contractId || issuerId}`,
iconUrl: verifiedToken?.icon,
type: SERVICE_TYPES.CACHE_ASSET_ICON,
});
}

return {
icon: verifiedToken?.icon,
canonicalAsset,
Expand Down
9 changes: 5 additions & 4 deletions @shared/api/helpers/getIconUrlFromIssuer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,8 @@ export const getIconUrlFromIssuer = async ({

if (toml.CURRENCIES) {
/* If we find some currencies listed, check to see if they have the currency we're looking for listed */
toml.CURRENCIES.every(async ({ code: currencyCode, issuer, image }) => {
for (const currency of toml.CURRENCIES) {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

another small bug I found while testing - Array.every doesn't properly await promises. Changing this to a for loop

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should this break out of the loop once the icon has been found?

const { code: currencyCode, issuer, image } = currency;
if (currencyCode === code && issuer === key && image) {
/* We found the currency listing in the toml. 3. Get the image url from it */
iconUrl = image;
Expand All @@ -95,11 +96,11 @@ export const getIconUrlFromIssuer = async ({
iconUrl,
type: SERVICE_TYPES.CACHE_ASSET_ICON,
});
return false;
break;
}
return true;
});
}
}

/* Return the icon url to the UI, if we found it */
return iconUrl;
};
67 changes: 58 additions & 9 deletions @shared/api/internal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -948,7 +948,10 @@ export const getAccountBalances = async (
isMainnet,
});
}
return await getAccountIndexerBalances({ publicKey, networkDetails });
return await getAccountIndexerBalances({
publicKey,
networkDetails,
});
};

export const getTokenDetails = async ({
Expand Down Expand Up @@ -1034,24 +1037,54 @@ export const getTokenDetails = async ({
}
};

export const getAssetIconCache = async ({
activePublicKey,
}: {
activePublicKey: string | null;
}) => {
let icons = {};
try {
({ icons } = await sendMessageToBackground({
activePublicKey,
type: SERVICE_TYPES.GET_CACHED_ASSET_ICON_LIST,
}));
} catch (e) {
return { icons };
}
return { icons };
};
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm adding a method to grab the entire cached icon list. Previously, we were consulting this list one asset at a time


/*
getAssetIcons is used to get the icons for the assets in the balances.
It can be used with or without lookup. Lookup is keyed off of passing assetListsData and networkDetails.

Using this method without lookup is fastest as it just consults the cache and then returns the icons.
This is useful for quickly getting the icons without extra network calls.

Using this method with lookup is slower as it needs to fetch the token lists and the issuer.
This is useful for getting the icons for the first time. Once we've retrieved an icon,
we'll store it in the background's storage and then use this cache for future lookups.
*/

export const getAssetIcons = async ({
balances,
networkDetails,
assetsListsData,
cachedIcons,
}: {
balances: Balances;
networkDetails: NetworkDetails;
assetsListsData: AssetListResponse[];
cachedIcons: Record<string, string>;
networkDetails?: NetworkDetails;
assetsListsData?: AssetListResponse[];
cachedIcons: Record<string, string | null>;
}) => {
const assetIcons = {} as { [code: string]: string };
const assetIcons = {} as { [code: string]: string | null };
const skipLookup = !assetsListsData || !networkDetails;

if (balances) {
let icon = "";
const balanceValues = Object.values(balances);

for (let i = 0; i < balanceValues.length; i++) {
let icon = "";
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

another small bugfix. This just happened to work, but this icon needs to be reset on every loop

const { token, contractId } = balanceValues[i];
if (token && "issuer" in token) {
const {
Expand All @@ -1066,8 +1099,19 @@ export const getAssetIcons = async ({
continue;
}

icon = await getIconUrlFromIssuer({ key, code, networkDetails });
if (cachedIcon === null) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So in the case that an image URL didn't resolve temporarily for whatever reasons(network or something), then this cache will prevent the icon from ever being used in the future?

Maybe this cache should have a ttl?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, so something important here (that I can document better) is this null value is only used in Redux. We only ever store an icon in the "hard cache" in the background when we've successfully found an icon. So, if there were to be a networking issue, a user would re-fetch the next time they reload the app

// if we've tried to fetch this icon before and it wasn't found, we marked it as null
// don't bother trying to fetch it again
// this null value is only stored in Redux, so we will re-try on next app reload
continue;
}

if (skipLookup) {
continue;
}

if (!icon) {
// if we don't have the icon, we try to get it from the token lists
const tokenListIcon = await getIconFromTokenLists({
networkDetails,
issuerId: key,
Expand All @@ -1078,9 +1122,14 @@ export const getAssetIcons = async ({
if (tokenListIcon.icon && tokenListIcon.canonicalAsset) {
icon = tokenListIcon.icon;
canonical = tokenListIcon.canonicalAsset;
} else {
// if we still don't have the icon, we try to get it from the issuer
icon = await getIconUrlFromIssuer({ key, code, networkDetails });
Copy link
Contributor Author

@piyalbasu piyalbasu Oct 15, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

small adjustment here - look up the icon issuer last because fetching from the fetched token list should be faster. Let's save the most expensive call for last and only if needed

}
}
assetIcons[canonical] = icon;

// we assign null here if we checked all sources and still don't have the icon
assetIcons[canonical] = icon || null;
}
}
}
Expand All @@ -1096,7 +1145,7 @@ export const retryAssetIcon = async ({
}: {
key: string;
code: string;
assetIcons: { [code: string]: string };
assetIcons: { [code: string]: string | null };
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

there's a lot of these type updates in this PR - this just allows us to skip an unnecessary asset issuer lookup if previously in the user's session we weren't able to find the icon

networkDetails: NetworkDetails;
activePublicKey: string | null;
}) => {
Expand Down
5 changes: 5 additions & 0 deletions @shared/api/types/message-request.ts
Original file line number Diff line number Diff line change
Expand Up @@ -250,6 +250,10 @@ export interface LoadSettingsMessage extends BaseMessage {
type: SERVICE_TYPES.LOAD_SETTINGS;
}

export interface GetCachedAssetIconListMessage extends BaseMessage {
type: SERVICE_TYPES.GET_CACHED_ASSET_ICON_LIST;
}

export interface GetCachedAssetIconMessage extends BaseMessage {
type: SERVICE_TYPES.GET_CACHED_ASSET_ICON;
assetCanonical: string;
Expand Down Expand Up @@ -381,6 +385,7 @@ export type ServiceMessageRequest =
| SaveSettingsMessage
| SaveExperimentalFeaturesMessage
| LoadSettingsMessage
| GetCachedAssetIconListMessage
| GetCachedAssetIconMessage
| CacheAssetIconMessage
| GetCachedDomainMessage
Expand Down
3 changes: 2 additions & 1 deletion @shared/api/types/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ export interface Response {
accountName: string;
assetCode: string;
assetCanonical: string;
icons: {};
iconUrl: string;
network: string;
networkIndex: number;
Expand Down Expand Up @@ -214,7 +215,7 @@ export type Settings = {
} & Preferences;

export interface AssetIcons {
[code: string]: string;
[code: string]: string | null;
}

export interface AssetDomains {
Expand Down
1 change: 1 addition & 0 deletions @shared/constants/services.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ export enum SERVICE_TYPES {
SAVE_SETTINGS = "SAVE_SETTINGS",
SAVE_EXPERIMENTAL_FEATURES = "SAVE_EXPERIMENTAL_FEATURES",
LOAD_SETTINGS = "LOAD_SETTINGS",
GET_CACHED_ASSET_ICON_LIST = "GET_CACHED_ASSET_ICON_LIST",
GET_CACHED_ASSET_ICON = "GET_CACHED_ASSET_ICON",
CACHE_ASSET_ICON = "CACHE_ASSET_ICON",
GET_CACHED_ASSET_DOMAIN = "GET_CACHED_ASSET_DOMAIN",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { DataStorageAccess } from "background/helpers/dataStorageAccess";
import { CACHED_ASSET_ICONS_ID } from "constants/localStorageTypes";

export const getCachedAssetIconList = async ({
localStore,
}: {
localStore: DataStorageAccess;
}) => {
const assetIconCache =
(await localStore.getItem(CACHED_ASSET_ICONS_ID)) || {};

return {
icons: assetIconCache,
};
};
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ import { saveAllowList } from "./handlers/saveAllowList";
import { saveSettings } from "./handlers/saveSettings";
import { saveExperimentalFeatures } from "./handlers/saveExperimentalFeatures";
import { loadSettings } from "./handlers/loadSettings";
import { getCachedAssetIconList } from "./handlers/getCachedAssetIconList";
import { getCachedAssetIcon } from "./handlers/getCachedAssetIcons";
import { cacheAssetIcon } from "./handlers/cacheAssetIcon";
import { getCachedAssetDomain } from "./handlers/getCachedDomain";
Expand Down Expand Up @@ -359,6 +360,11 @@ export const popupMessageListener = (
localStore,
});
}
case SERVICE_TYPES.GET_CACHED_ASSET_ICON_LIST: {
return getCachedAssetIconList({
localStore,
});
}
case SERVICE_TYPES.GET_CACHED_ASSET_ICON: {
return getCachedAssetIcon({
request,
Expand Down
34 changes: 34 additions & 0 deletions extension/src/helpers/__tests__/useGetBalances.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -137,4 +137,38 @@ describe("useGetBalances (cached path)", () => {
expect(getCombinedAssetListData).not.toHaveBeenCalled();
expect(result.current.state.state).toBe<RequestState>(RequestState.SUCCESS);
});

it("skips trying to lookup an icon that was previously not found", async () => {
const preloadedStateWithNullIcon = {
cache: {
balanceData: {
[TESTNET_NETWORK_DETAILS.network]: { [publicKey]: cachedBalanceData },
},
icons: { "XLM:GABCDEF": null },
tokenLists: tokenListData,
},
settings: {
assetsLists: [],
},
};
const iconStore = makeDummyStore(preloadedStateWithNullIcon);

const { result } = renderHook(
() => useGetBalances({ showHidden: false, includeIcons: true }),
{ wrapper: Wrapper(iconStore) },
);

await act(async () => {
await result.current.fetchData(
publicKey,
true,
TESTNET_NETWORK_DETAILS,
true,
);
});

expect(getCombinedAssetListData).not.toHaveBeenCalled();
expect(getIconUrlFromIssuer).not.toHaveBeenCalled();
expect(result.current.state.state).toBe<RequestState>(RequestState.SUCCESS);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,7 @@ export function useGetAssetDomainsWithBalances(getBalancesOptions: {
domains.push({
code,
issuer: issuer.key,
image: icons[getCanonicalFromAsset(code, issuer.key)],
image: icons[getCanonicalFromAsset(code, issuer.key)] || null,
domain,
contract: contractId,
isSuspicious: isAssetSuspicious(blockaidData),
Expand Down
11 changes: 10 additions & 1 deletion extension/src/helpers/hooks/useGetBalances.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
getAccountBalances,
getAssetIcons,
getHiddenAssets,
getAssetIconCache,
} from "@shared/api/internal";
import { NetworkDetails } from "@shared/constants/stellar";
import { AssetIcons } from "@shared/api/types";
Expand Down Expand Up @@ -99,6 +100,14 @@ function useGetBalances(options: {
} as AccountBalances;

if (options.includeIcons) {
let cachedIconsFromCache = cachedIcons;
if (!Object.keys(cachedIcons).length) {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if we haven't hydrated redux yet (first load), use background storage's asset cache as the cache

const backgroundCachedIcons = await getAssetIconCache({
activePublicKey: publicKey,
});

cachedIconsFromCache = { ...backgroundCachedIcons.icons };
}
const assetsListsData =
useCache && cachedTokenLists.length
? cachedTokenLists
Expand All @@ -111,7 +120,7 @@ function useGetBalances(options: {
balances: accountBalances.balances,
networkDetails,
assetsListsData,
cachedIcons,
cachedIcons: cachedIconsFromCache,
});
payload.icons = icons;
reduxDispatch(saveTokenLists(assetsListsData));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,9 +33,9 @@ import { CopyValue } from "popup/components/CopyValue";
import "./styles.scss";

interface ReviewTxProps {
assetIcon: string;
assetIcon: string | null;
dstAsset?: {
icon: string;
icon: string | null;
canonical: string;
priceUsd: string | null;
amount: string;
Expand Down
Loading