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
10 changes: 7 additions & 3 deletions @shared/api/helpers/getIconFromTokenList.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,11 @@ export const getIconFromTokenLists = async ({
}) => {
let verifiedToken = {} as AssetListReponseItem;
let canonicalAsset = undefined as string | undefined;
for (const data of assetsListsData) {
// The lists arrive in the user's configured order, which is a priority order:
// the first list carrying the asset wins. Without the labeled break below the
// outer loop runs to completion and the LAST matching list silently overwrites
// earlier, higher-priority entries.
listLoop: for (const data of assetsListsData) {
const list = data.assets;
if (list) {
for (const record of list) {
Expand All @@ -29,7 +33,7 @@ export const getIconFromTokenLists = async ({
if (record.contract && record.contract.match(regex) && record.icon) {
verifiedToken = record;
canonicalAsset = getCanonicalFromAsset(code, contractId);
break;
break listLoop;
}
}

Expand All @@ -42,7 +46,7 @@ export const getIconFromTokenLists = async ({
) {
verifiedToken = record;
canonicalAsset = getCanonicalFromAsset(code, issuerId);
break;
break listLoop;
}
}
}
Expand Down
28 changes: 28 additions & 0 deletions @shared/api/internal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2081,6 +2081,34 @@ export const getTokenIds = async ({
return tokenIdList;
};

export const getUsdt0LaunchBannerDismissed = async (): Promise<boolean> => {
const { isDismissed, error } = await sendMessageToBackground({
activePublicKey: null,
type: SERVICE_TYPES.GET_USDT0_LAUNCH_BANNER_DISMISSED,
});

if (error) {
throw new Error(error);
}

return !!isDismissed;
};

export const dismissUsdt0LaunchBanner = async (): Promise<{
isDismissed: boolean;
}> => {
const { isDismissed, error } = await sendMessageToBackground({
activePublicKey: null,
type: SERVICE_TYPES.DISMISS_USDT0_LAUNCH_BANNER,
});

if (error) {
throw new Error(error);
}

return { isDismissed: !!isDismissed };
};

export const removeTokenId = async ({
activePublicKey,
contractId,
Expand Down
10 changes: 10 additions & 0 deletions @shared/api/types/message-request.ts
Original file line number Diff line number Diff line change
Expand Up @@ -428,6 +428,14 @@ export interface GetHiddenAssetsMessage extends BaseMessage {
type: SERVICE_TYPES.GET_HIDDEN_ASSETS;
}

export interface GetUsdt0LaunchBannerDismissedMessage extends BaseMessage {
type: SERVICE_TYPES.GET_USDT0_LAUNCH_BANNER_DISMISSED;
}

export interface DismissUsdt0LaunchBannerMessage extends BaseMessage {
type: SERVICE_TYPES.DISMISS_USDT0_LAUNCH_BANNER;
}

export interface GetRecentProtocolsMessage extends BaseMessage {
type: SERVICE_TYPES.GET_RECENT_PROTOCOLS;
}
Expand Down Expand Up @@ -565,6 +573,8 @@ export type ServiceMessageRequest =
| GetIsAccountMismatchMessage
| ChangeAssetVisibilityMessage
| GetHiddenAssetsMessage
| GetUsdt0LaunchBannerDismissedMessage
| DismissUsdt0LaunchBannerMessage
| GetRecentProtocolsMessage
| AddRecentProtocolMessage
| ClearRecentProtocolsMessage
Expand Down
2 changes: 2 additions & 0 deletions @shared/constants/services.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,8 @@ export enum SERVICE_TYPES {
CHANGE_ASSET_VISIBILITY = "CHANGE_ASSET_VISIBILITY",
GET_HIDDEN_ASSETS = "GET_HIDDEN_ASSETS",
GET_IS_ACCOUNT_MISMATCH = "GET_IS_ACCOUNT_MISMATCH",
GET_USDT0_LAUNCH_BANNER_DISMISSED = "GET_USDT0_LAUNCH_BANNER_DISMISSED",
DISMISS_USDT0_LAUNCH_BANNER = "DISMISS_USDT0_LAUNCH_BANNER",
GET_BLOCKAID_DEBUG_OVERRIDE = "GET_BLOCKAID_DEBUG_OVERRIDE",
SAVE_BLOCKAID_DEBUG_OVERRIDE = "SAVE_BLOCKAID_DEBUG_OVERRIDE",
ADD_COLLECTIBLE = "ADD_COLLECTIBLE",
Expand Down
2 changes: 1 addition & 1 deletion extension/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "extension",
"version": "5.47.0",
"version": "5.48.0",
"license": "Apache-2.0",
"prettier": "../.prettierrc.yaml",
"scripts": {
Expand Down
4 changes: 2 additions & 2 deletions extension/public/static/manifest/v3.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "Freighter",
"version": "5.47.0",
"version_name": "5.47.0",
"version": "5.48.0",
Comment thread
CassioMG marked this conversation as resolved.
"version_name": "5.48.0",
"description": "Freighter is a non-custodial wallet extension that enables you to sign Stellar transactions via your browser.",
"browser_specific_settings": {
"gecko": {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { getCachedAssetIconList } from "../getCachedAssetIconList";

const USDT0 = "USDT0:GATISXX6BZ6NC7IKQBY37CJD4SOZL3CYZJWXEDG6JVIY4WBS6KXJHN6Q";
const USDC = "USDC:GA5ZSEJYB37JRC5AVCIA5MOP4RHTM335X2KGX3IHOJAPP5RE34K4KZVN";
const USDC_ICON = "https://centre.io/usdc.png";

const makeLocalStore = (assetIconCache: unknown) =>
({
getItem: jest.fn(async () => assetIconCache),
setItem: jest.fn(),
remove: jest.fn(),
clear: jest.fn(),
}) as any;

describe("getCachedAssetIconList", () => {
it("returns the icons it has", async () => {
const result = await getCachedAssetIconList({
localStore: makeLocalStore({ [USDC]: USDC_ICON }),
});

expect(result.icons).toEqual({ [USDC]: USDC_ICON });
});

it("leaves out assets recorded as having no icon", async () => {
// A null here means an earlier lookup came up empty, and getAssetIcons
// reads it as "already tried, don't look again". Because this cache is on
// disk that verdict outlived the session, so an asset whose icon failed
// once — USDT0, whose LOBSTR url 403s browsers — stayed iconless forever.
// Dropping nulls turns it back into an ordinary cache miss, and the fresh
// lookup overwrites the stale entry.
const result = await getCachedAssetIconList({
localStore: makeLocalStore({ [USDC]: USDC_ICON, [USDT0]: null }),
});

expect(result.icons).toEqual({ [USDC]: USDC_ICON });
});

it("returns an empty map when nothing is cached", async () => {
const result = await getCachedAssetIconList({
localStore: makeLocalStore(undefined),
});

expect(result.icons).toEqual({});
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { DataStorageAccess } from "background/helpers/dataStorageAccess";
import { USDT0_LAUNCH_BANNER_DISMISSED } from "constants/localStorageTypes";

export const dismissUsdt0LaunchBanner = async ({
localStore,
}: {
localStore: DataStorageAccess;
}): Promise<{ isDismissed: boolean }> => {
await localStore.setItem(USDT0_LAUNCH_BANNER_DISMISSED, true);
return { isDismissed: true };
};
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,52 @@ export const getCachedAssetIconList = async ({
(await localStore.getItem(CACHED_ASSET_ICONS_ID)) || {};

return {
icons: assetIconCache,
// A null entry records that a lookup came up empty, and getAssetIcons reads
// one as "already tried, don't look again". That was only ever meant to last
// a session, but this cache is on disk, so the verdict outlived it: an asset
// whose icon failed once stayed iconless for good, with no way back — no
// icon means no <img>, so nothing fires the error handler that would have
// retried. Dropping nulls here turns them back into ordinary cache misses,
// and the fresh lookup overwrites the stale entry.
//
// TODO: this is a read-side workaround, not the real fix. It hides bad data
// rather than stopping it being written, and it rules out ever storing a
// meaningful null here.
Comment thread
CassioMG marked this conversation as resolved.
//
// Retrying is the point: it is how an asset that got stuck gets its icon
// back. But the retry is not free, and for an asset with no icon anywhere
// it repeats forever without ever succeeding. Each attempt costs a
// token-list scan and, when that finds nothing, one batched Horizon call
// covering every such issuer plus a stellar.toml fetch for each issuer that
// publishes a home domain. (USDT0 stops at the Horizon call: its issuer
// publishes no home domain and, with its master key at weight 0, never
// can.)
//
// That cost is newly paid by the flows that load balances with icons —
// swap, send, manage assets, history — which previously skipped a
// null-marked asset outright. The Account view's own icon hook already
// retried regardless, since it passes an empty cache to its lookup pass, so
// nothing changes there. And nothing suppresses the retry from one popup to
// the next: closing the popup tears it down, taking Redux — the only record
// of what this session already resolved — with it, so every open starts
// from nothing.
//
// A negative cache that survives the popup, with a TTL so it expires rather
// than latching, is the sane way to stop the endless retry — and this
// filter would silently swallow one.
//
// To fix properly, in order:
// 1. Stop persisting nulls: retryAssetIcon sends `iconUrl: null` meaning
// "clear this", so cacheAssetIcon should delete the entry rather than
// store the null.
// 2. Add a migration to clear the nulls already on disk. It cannot be
// done by (1) alone, since nothing rewrites an entry the lookup skips.
// 3. Drop this filter, so the read path goes back to being a plain
// accessor and null is free to mean something again.
// Doing (2) without (1) is not enough on its own: every later icon failure
// writes a fresh null and puts that user straight back into the bug.
icons: Object.fromEntries(
Object.entries(assetIconCache).filter(([, iconUrl]) => iconUrl),
),
};
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { DataStorageAccess } from "background/helpers/dataStorageAccess";
import { USDT0_LAUNCH_BANNER_DISMISSED } from "constants/localStorageTypes";

export const getUsdt0LaunchBannerDismissed = async ({
localStore,
}: {
localStore: DataStorageAccess;
}): Promise<{ isDismissed: boolean }> => {
const dismissed = await localStore.getItem(USDT0_LAUNCH_BANNER_DISMISSED);
return { isDismissed: !!dismissed };
};
12 changes: 12 additions & 0 deletions extension/src/background/messageListener/popupMessageListener.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,8 @@ import { modifyAssetsList } from "./handlers/modifyAssetsList";
import { getIsAccountMismatch } from "./handlers/getIsAccountMismatch";
import { changeAssetVisibility } from "./handlers/changeAssetVisibility";
import { getHiddenAssets } from "./handlers/getHiddenAssets";
import { getUsdt0LaunchBannerDismissed } from "./handlers/getUsdt0LaunchBannerDismissed";
import { dismissUsdt0LaunchBanner } from "./handlers/dismissUsdt0LaunchBanner";
import { loadBackendSettings } from "./handlers/loadBackendSettings";
import { saveBlockaidOverrideState } from "./handlers/saveDebugOverride";
import { getBlockaidOverrideState } from "./handlers/getDebugOverride";
Expand Down Expand Up @@ -579,6 +581,16 @@ export const popupMessageListener = (
localStore,
});
}
case SERVICE_TYPES.GET_USDT0_LAUNCH_BANNER_DISMISSED: {
return getUsdt0LaunchBannerDismissed({
localStore,
});
}
case SERVICE_TYPES.DISMISS_USDT0_LAUNCH_BANNER: {
return dismissUsdt0LaunchBanner({
localStore,
});
}
case SERVICE_TYPES.GET_BLOCKAID_DEBUG_OVERRIDE: {
return getBlockaidOverrideState({
localStore,
Expand Down
1 change: 1 addition & 0 deletions extension/src/constants/localStorageTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ export const HIDDEN_ASSETS = "hiddenAssets";
export const HIDDEN_COLLECTIBLES = "hiddenCollectibles";
export const TEMPORARY_STORE_ID = "temporaryStore";
export const TEMPORARY_STORE_EXTRA_ID = "temporaryStoreExtra";
export const USDT0_LAUNCH_BANNER_DISMISSED = "usdt0LaunchBannerDismissed";
export const OVERRIDDEN_BLOCKAID_RESPONSE_ID = "overriddenBlockaidResponse";
export const COLLECTIBLES_ID = "collectibles";
export const IS_OPEN_SIDEBAR_BY_DEFAULT_ID = "isOpenSidebarByDefault";
Expand Down
Binary file added extension/src/popup/assets/logo-usdt0.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
20 changes: 20 additions & 0 deletions extension/src/popup/assets/usdt0-arcs.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Loading