How to Implement Token Metadata Fetching and Caching in Nautilus Wallet: A Complete Guide
Nautilus Wallet implements token metadata fetching and caching using a layered architecture that combines a reactive in-memory Map with IndexedDB persistence and GraphQL network requests, ensuring fast UI rendering while minimizing redundant blockchain queries.
Nautilus Wallet, an open-source Ergo blockchain wallet, handles token metadata (names, decimals, artwork) through a sophisticated caching system. This article explains how the wallet implements token metadata fetching and caching to balance performance, offline support, and network efficiency.
Architecture Overview
The token metadata system in Nautilus Wallet follows a three-tier caching strategy:
- Reactive In-Memory Map – Provides instant access to metadata for UI rendering
- IndexedDB Persistence – Survives page reloads and enables offline functionality
- GraphQL Network Layer – Fetches unknown tokens from the Ergo explorer in chunked batches
This design ensures that wallet components in src/stores/walletStore.ts and src/stores/poolStore.ts can synchronously access metadata while the system asynchronously hydrates the cache from disk and network.
The Reactive In-Memory Cache
At the core of the system is a reactive Map stored in src/stores/assetsStore.ts:
// src/stores/assetsStore.ts – line 62
const metadata = shallowReactive(new Map([[ERG_TOKEN_ID, ERG_METADATA]]));
The metadata object is a reactive Map<TokenId, BasicAssetMetadata> exported from the store and consumed throughout the UI. Using shallowReactive ensures that modifications to the Map itself trigger re-renders, while nested object updates remain efficient.
Loading Metadata Workflow
The primary entry point for token metadata fetching and caching is the loadMetadata function:
// src/stores/assetsStore.ts – lines 32-55
async function loadMetadata(tokenIds: string[], options?: Partial<LoadMetadataOptions>) {
const { fetchInBackground, persist } = ensureDefaults(options, {
fetchInBackground: false,
persist: true
});
const unloaded = uniq(tokenIds).filter((x) => !metadata.has(x));
if (unloaded.length === 0) return;
This function implements deduplication using uniq and performs an early exit if all requested tokens already exist in the in-memory cache. The fetchInBackground option allows non-blocking network requests, while persist controls whether new data should be written to IndexedDB.
IndexedDB Persistence Layer
When metadata must survive browser refreshes, the system utilizes assetInfoDbService in src/database/assetInfoDbService.ts:
// src/database/assetInfoDbService.ts – lines 24-27
public async getAnyOf(ids: string[]): Promise<IAssetInfo[]> {
if (isEmpty(ids)) return [];
return await dbContext.assetInfo.where("id").anyOf(ids).toArray();
}
This service wraps a Dexie-based IndexedDB table (dbContext.assetInfo). During the loading workflow, the system calls assetInfoDbService.getAnyOf(unloaded) to retrieve locally persisted entries, then patches the in-memory cache via patchMetadata(dbMeta).
Network Fetching with GraphQL
For tokens not found in memory or IndexedDB, Nautilus Wallet queries the Ergo explorer using graphQLService in src/chains/ergo/services/graphQlService.ts:
// src/chains/ergo/services/graphQlService.ts – lines 54-73
async getAssetsMetadata(tokenIds: string[]): Promise<IAssetInfo[] | undefined> {
const metadataChunks: Token[][] = [];
const chunks = chunk(tokenIds, MAX_PARAMS_PER_REQUEST);
try {
for (const tokenIds of chunks) {
const chunkMetadata = await this.#getTokenMetadata({ tokenIds });
if (isEmpty(chunkMetadata.data.tokens)) return;
metadataChunks.push(chunkMetadata.data.tokens);
}
} catch (e) {
log.error("Failed to fetch metadata", tokenIds, e);
return;
}
return metadataChunks
.flat()
.map(parseEIP4Asset)
.filter((assetInfo) => assetInfo) as IAssetInfo[];
}
This method implements request chunking to respect the node's MAX_PARAMS_PER_REQUEST limit. The raw GraphQL responses are normalized through parseEIP4Asset, which converts Ergo's EIP-4 token standard data into the internal IAssetInfo format.
Updating the Cache with Remote Data
Once network data returns, loadRemoteMetadata handles cache updates and persistence:
// src/stores/assetsStore.ts – lines 56-71
async function loadRemoteMetadata(missing: string[], persist: boolean) {
const newMeta = await graphQLService.getAssetsMetadata(missing);
if (!newMeta) return;
if (missing.length > newMeta.length) {
// Fill in missing metadata with unknown placeholders
newMeta.push(...missing.filter((id) => !newMeta.some((x) => x.id === id)).map(unknownMetadata));
}
if (newMeta.length > 0) {
patchMetadata(newMeta);
if (persist) await assetInfoDbService.bulkPut(newMeta);
}
}
This function handles unknown tokens by generating placeholder metadata with type: AssetType.Unknown. It then patches the reactive map and optionally persists to IndexedDB via assetInfoDbService.bulkPut.
Patching the In-Memory Map
The patchMetadata function performs the actual cache update:
// src/stores/assetsStore.ts – lines 73-82
function patchMetadata(patch: IAssetInfo[]) {
if (patch.length === 0) return;
for (const info of patch) {
metadata.set(info.id, {
name: info.name,
decimals: info.decimals,
type: info.subtype,
artworkUrl: info.artworkCover ?? info.artworkUrl
});
}
}
Every call to patchMetadata instantly updates the reactive metadata map, triggering re-renders in UI components that depend on token data.
Integration with Wallet Stores
The metadata system is consumed by multiple stores throughout the application:
Wallet Store triggers metadata loading when asset balances change:
// src/stores/walletStore.ts – line 295
await assetsStore.loadMetadata(tokenIds, { fetchInBackground: opt.syncInBackground });
Pool Store ensures metadata is available before building transactions:
// src/stores/poolStore.ts – line 92
assets.loadMetadata(txns.flatMap((x) => x.delta.tokens.map((y) => y.tokenId)));
UI Components read from the cache for formatting and display:
// src/composables/useFormat.ts – lines 67-68
val.metadata?.name || val.tokenId,
maxLen: val.metadata?.name ? maxLen : Math.floor(maxLen / 2)
Code Example: Manually Fetching Token Metadata
Here is a complete example of using the metadata system in a Nautilus Wallet extension or plugin:
import { useAssetsStore } from '@/stores/assetsStore';
// Token IDs you need metadata for
const tokenIds = [
'0000000000000000000000000000000000000000000000000000000000000000', // ERG
'abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890'
];
// Load metadata (foreground, persisted)
await useAssetsStore().loadMetadata(tokenIds, { fetchInBackground: false, persist: true });
// After the call finishes, the map is populated:
const metaMap = useAssetsStore().metadata;
console.log(metaMap.get(tokenIds[1])?.name); // → "SigUSD" (if known)
This call automatically:
- Returns instantly for IDs already cached in memory
- Reads from IndexedDB for previously persisted data
- Queries the Ergo explorer for unknown IDs, updates the map, and stores results back to IndexedDB
Summary
Nautilus Wallet implements token metadata fetching and caching through a sophisticated multi-layer system:
- Reactive in-memory Map (
assetsStore.metadata) provides instantaneous UI access to token names, decimals, and artwork - IndexedDB persistence (
assetInfoDbService) ensures metadata survives browser refreshes and supports offline usage - Intelligent deduplication prevents redundant network requests by checking memory and disk before querying the blockchain
- Chunked GraphQL requests (
graphQLService.getAssetsMetadata) respect node limits while fetching unknown token data from the Ergo explorer - Automatic fallback handling generates placeholder metadata for unknown tokens to maintain UI consistency
Frequently Asked Questions
How does Nautilus Wallet handle offline token metadata access?
Nautilus Wallet persists all fetched token metadata to IndexedDB via assetInfoDbService. When the application loads or requests token data, it first checks the in-memory reactive Map, then falls back to IndexedDB using assetInfoDbService.getAnyOf(). This ensures that previously viewed tokens remain accessible even without an internet connection.
What happens when a token ID is not found in the cache or database?
When loadMetadata() encounters unknown token IDs, it filters out any entries already present in the in-memory cache or IndexedDB, then passes the remaining IDs to loadRemoteMetadata(). This function calls graphQLService.getAssetsMetadata() to query the Ergo explorer. If the network request returns fewer results than requested, the system generates placeholder metadata using unknownMetadata() to ensure every token ID has a corresponding entry in the UI.
How does the wallet prevent excessive network requests when loading many tokens?
The metadata system implements several optimization strategies. First, it deduplicates token IDs using uniq() and filters out already-cached entries before making network requests. Second, the GraphQL service chunks requests according to MAX_PARAMS_PER_REQUEST to respect node limits. Finally, the fetchInBackground option allows non-critical metadata to load asynchronously without blocking the UI, while the persist flag controls whether results should be written to IndexedDB.
Which stores trigger metadata loading in the application?
Two primary stores initiate metadata fetching. The walletStore (in src/stores/walletStore.ts) calls assetsStore.loadMetadata() whenever the wallet's asset list changes, passing the syncInBackground option to control blocking behavior. The poolStore (in src/stores/poolStore.ts) ensures metadata is available before building transactions by calling loadMetadata() with token IDs from transaction deltas. Additionally, UI components like useFormat.ts read from the metadata map for display purposes but do not trigger fetches.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →