diff --git a/packages/router/src/grpc/test-utils.ts b/packages/router/src/grpc/test-utils.ts index cd6a7155da..96fd71c23b 100644 --- a/packages/router/src/grpc/test-utils.ts +++ b/packages/router/src/grpc/test-utils.ts @@ -22,6 +22,7 @@ export interface IndexedDbMock { subscribe?: (table: string) => Partial>; getSwapByCommitment?: Mock; getEpochByHeight?: Mock; + saveAssetsMetadata?: Mock; } export interface TendermintMock { broadcastTx?: Mock; diff --git a/packages/router/src/grpc/view-protocol-server/asset-metadata-by-id.test.ts b/packages/router/src/grpc/view-protocol-server/asset-metadata-by-id.test.ts index 723d652818..f26cf9b059 100644 --- a/packages/router/src/grpc/view-protocol-server/asset-metadata-by-id.test.ts +++ b/packages/router/src/grpc/view-protocol-server/asset-metadata-by-id.test.ts @@ -26,6 +26,7 @@ describe('AssetMetadataById request handler', () => { mockIndexedDb = { getAssetsMetadata: vi.fn(), + saveAssetsMetadata: vi.fn(), }; mockShieldedPool = { assetMetadata: vi.fn(), diff --git a/packages/router/src/grpc/view-protocol-server/asset-metadata-by-id.ts b/packages/router/src/grpc/view-protocol-server/asset-metadata-by-id.ts index c152b72ecd..b67b326933 100644 --- a/packages/router/src/grpc/view-protocol-server/asset-metadata-by-id.ts +++ b/packages/router/src/grpc/view-protocol-server/asset-metadata-by-id.ts @@ -4,9 +4,15 @@ import { getAssetMetadata } from './helpers'; export const assetMetadataById: Impl['assetMetadataById'] = async (req, ctx) => { if (!req.assetId) throw new Error('No asset id passed in request'); + if (!req.assetId.altBaseDenom && !req.assetId.altBech32m && !req.assetId.inner.length) { + throw new Error( + 'Either `inner` or `altBaseDenom` must be set on the asset ID passed in the `assetMetadataById` request', + ); + } const services = ctx.values.get(servicesCtx); const { indexedDb, querier } = await services.getWalletServices(); + const denomMetadata = await getAssetMetadata(req.assetId, indexedDb, querier); return { denomMetadata }; diff --git a/packages/router/src/grpc/view-protocol-server/helpers.ts b/packages/router/src/grpc/view-protocol-server/helpers.ts index 322b137d11..12cf138375 100644 --- a/packages/router/src/grpc/view-protocol-server/helpers.ts +++ b/packages/router/src/grpc/view-protocol-server/helpers.ts @@ -10,9 +10,19 @@ export const getAssetMetadata = async ( const localMetadata = await indexedDb.getAssetsMetadata(targetAsset); if (localMetadata) return localMetadata; - // If not available locally, query the metadata from the node. + // If not available locally, query the metadata from the node and save it to + // the internal database. const nodeMetadata = await querier.shieldedPool.assetMetadata(targetAsset); - if (nodeMetadata) return nodeMetadata; + if (nodeMetadata) { + /** + * @todo: If possible, save asset metadata proactively if we might need it + * for a token that the current user doesn't hold. For example, validator + * delegation tokens could be generated and saved to the database at each + * epoch boundary. + */ + void indexedDb.saveAssetsMetadata(nodeMetadata); + return nodeMetadata; + } // If the metadata is not found, throw an error with details about the asset. throw new Error(`No denom metadata found for asset: ${JSON.stringify(targetAsset.toJson())}`); diff --git a/packages/storage/src/indexed-db/index.ts b/packages/storage/src/indexed-db/index.ts index 97ccdcea5a..b657a8d953 100644 --- a/packages/storage/src/indexed-db/index.ts +++ b/packages/storage/src/indexed-db/index.ts @@ -49,7 +49,7 @@ import { IdbCursorSource } from './stream'; import '@penumbra-zone/polyfills/ReadableStream[Symbol.asyncIterator]'; import { ValidatorInfo } from '@buf/penumbra-zone_penumbra.bufbuild_es/penumbra/core/component/stake/v1/stake_pb'; -import { getIdentityKeyFromValidatorInfo } from '@penumbra-zone/getters'; +import { bech32AssetId, getIdentityKeyFromValidatorInfo } from '@penumbra-zone/getters'; import { Transaction } from '@buf/penumbra-zone_penumbra.bufbuild_es/penumbra/core/transaction/v1/transaction_pb'; interface IndexedDbProps { @@ -177,11 +177,41 @@ export class IndexedDb implements IndexedDbInterface { }); } + /** + * Gets metadata by asset ID. + * + * If possible, pass an `AssetId` with a populated `inner` property, as that + * is by far the fastest way to retrieve metadata. However, you can also pass + * an `AssetId` with either the `altBaseDenom` or `altBech32m` properties + * populated. In those cases, `getAssetsMetadata` will iterate over every + * metadata in the `ASSETS` table until it finds a match. + */ async getAssetsMetadata(assetId: AssetId): Promise { - const key = uint8ArrayToBase64(assetId.inner); - const json = await this.db.get('ASSETS', key); - if (!json) return undefined; - return Metadata.fromJson(json); + if (!assetId.inner.length && !assetId.altBaseDenom && !assetId.altBech32m) return undefined; + + if (assetId.inner.length) { + const key = uint8ArrayToBase64(assetId.inner); + const json = await this.db.get('ASSETS', key); + if (!json) return undefined; + return Metadata.fromJson(json); + } + + if (assetId.altBaseDenom || assetId.altBech32m) { + for await (const cursor of this.db.transaction('ASSETS').store) { + const metadata = Metadata.fromJson(cursor.value); + + if (metadata.base === assetId.altBaseDenom) return metadata; + + if ( + metadata.penumbraAssetId && + bech32AssetId(metadata.penumbraAssetId) === assetId.altBech32m + ) { + return metadata; + } + } + } + + return undefined; } async *iterateAssetsMetadata() {