Skip to content

Add a TTL to the cachedAssetIconsId storage cache #2992

Description

@aristidesstaffieri

Problem

cachedAssetIconsId in extension local storage maps CODE:ISSUER → icon URL. Entries are written on first resolution and never invalidated: no timestamp, no TTL, no expiry path.

getAssetIcons gates on the cache upstream of the token list data:

const cachedIcon = cachedIcons[canonical];
if (cachedIcon) { assetIcons[canonical] = cachedIcon; continue; }
// ...
const tokenListIcon = await getIconFromTokenLists({ ... });

@shared/api/internal.ts:1345

Because the gate is per-asset and sits before assetsListsData is read, re-fetching the token lists has no effect on an asset that resolved once. The write in getIconFromTokenLists (@shared/api/helpers/getIconFromTokenList.ts:52) is write-on-miss, not write-through-on-refresh.

Rationale

Token lists are mutable; we cache them as if they were immutable. List maintainers and issuers rotate icon URLs for rehosting, CDN migrations, and rebrands. Every rotation is invisible to any user who resolved that asset beforehand. Cache lifetime is effectively install lifetime.

There is no remedy for the user or for support. The only code that clears the key is localStore.clear() in resetExperimentalData, reachable only from the DEV_SERVER-gated /integration-test route. cache.clearAll has no callers. The only advice we can give today is "clear extension storage" or reinstall.

Failures latch permanently. retryAssetIcon fires only on <img> load error and persists iconUrl: null; getAssetIcons then treats null as do-not-retry. The comment there claims the null lives only in Redux, but cacheAssetIcon writes it to local storage and GET_CACHED_ASSET_ICON_LIST returns it verbatim, so one transient image failure suppresses that icon indefinitely. A TTL expires these nulls as a side effect.

The staleness is user-visible and self-contradictory. The Swap and Add-Asset pickers render record.icon straight from freshly fetched lists (popup/helpers/searchAsset.ts), bypassing this cache — so one session can show the new icon in the picker and the old one on the account list.

The map only grows. Entries for assets the user no longer holds are never pruned.

Proposed change

Expire the cache on a staleness window, reusing the <storageKey>_date convention already in background/helpers/cachedFetch.ts:

  • Write cachedAssetIconsId_date (epoch ms) alongside the map. On read, if it is older than the TTL, drop the map and let the existing write-on-miss path repopulate from the current lists.
  • Put the check in the background read handlers (getCachedAssetIcons.ts, getCachedAssetIconList.ts) so every consumer inherits it.
  • Map-level expiry needs no entry-shape migration, and an absent _date already reads as stale (Number(undefined || "")0), so existing installs expire on first read. Per-entry { url, updatedAt } is the alternative if we want staggered refresh, at the cost of a migration plus tolerant reads.
  • Suggested TTL: 7 days, matching cachedFetch. Icons change rarely and a miss costs one token list lookup we already perform.

Out of scope

getIconFromTokenLists has no outer-loop break, so the last matching list wins rather than the first — a separate correctness question worth its own issue.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions