diff --git a/packages/constants/assets/index.ts b/packages/constants/assets/index.ts index 012dfc0fb4..0564f1b643 100644 --- a/packages/constants/assets/index.ts +++ b/packages/constants/assets/index.ts @@ -3,19 +3,34 @@ import LocalAssetRegistry from './local-asset-registry.json'; import { JsonValue } from '@bufbuild/protobuf'; export interface AssetPattens { - lpNftPattern: RegExp; - delegationTokenPattern: RegExp; - proposalNftPattern: RegExp; - unbondingTokenPattern: RegExp; - votingReceiptPattern: RegExp; + lpNft: RegExp; + delegationToken: RegExp; + proposalNft: RegExp; + unbondingToken: RegExp; + votingReceipt: RegExp; } +export interface DelegationCaptureGroups { + id: string; + bech32IdentityKey: string; +} + +export interface UnbondingCaptureGroups { + epoch: string; + id: string; +} + +// Source of truth for regex patterns: https://github.com/penumbra-zone/penumbra/blob/main/crates/core/asset/src/asset/registry.rs export const assetPatterns: AssetPattens = { - lpNftPattern: new RegExp('^lpnft_'), - delegationTokenPattern: new RegExp('^delegation_'), - proposalNftPattern: new RegExp('^proposal_'), - unbondingTokenPattern: new RegExp('^unbonding_'), - votingReceiptPattern: new RegExp('^voted_on_'), + lpNft: new RegExp(/^lpnft_/), + delegationToken: new RegExp( + /.*delegation_(?penumbravalid1(?[a-zA-HJ-NP-Z0-9]+))$/, + ), + proposalNft: new RegExp(/^proposal_/), + unbondingToken: new RegExp( + /.*unbonding_epoch_(?[0-9]+)_penumbravalid1(?[a-zA-HJ-NP-Z0-9]+)$/, + ), + votingReceipt: new RegExp(/^voted_on_/), }; export const localAssets: Metadata[] = LocalAssetRegistry.map(a => diff --git a/packages/query/src/block-processor.ts b/packages/query/src/block-processor.ts index b22fa39211..21379d6a04 100644 --- a/packages/query/src/block-processor.ts +++ b/packages/query/src/block-processor.ts @@ -1,17 +1,12 @@ import { RootQuerier } from './root-querier'; -import { bech32 } from 'bech32'; - import { sha256Hash } from '@penumbra-zone/crypto-web'; import { BlockProcessorInterface, IndexedDbInterface, ViewServerInterface, } from '@penumbra-zone/types'; -import { assetPatterns } from '@penumbra-zone/constants'; import { computePositionId, decodeSctRoot, transactionInfo } from '@penumbra-zone/wasm'; - -import { Metadata } from '@buf/penumbra-zone_penumbra.bufbuild_es/penumbra/core/asset/v1/asset_pb'; import { PositionState, PositionState_PositionStateEnum, @@ -29,6 +24,7 @@ import { TransactionInfo, } from '@buf/penumbra-zone_penumbra.bufbuild_es/penumbra/view/v1/view_pb'; import { backOff } from 'exponential-backoff'; +import { customizeSymbol } from './customize-symbol'; interface QueryClientProps { fullViewingKey: string; @@ -247,43 +243,16 @@ export class BlockProcessor implements BlockProcessorInterface { for (const n of newNotes) { const assetId = n.note?.value?.assetId; if (!assetId) continue; - if (await this.indexedDb.getAssetsMetadata(assetId)) continue; - - let metadata: Metadata | undefined; - metadata = await this.querier.shieldedPool.assetMetadata(assetId); - - // If the metadata is for a delegation token, customize its symbol. - if (metadata && assetPatterns.delegationTokenPattern.test(metadata.display)) { - // We can't trust the validator's self-described name, so use their validator ID. - // We know it's delegation_penumbravalid1... so use substrings: - // TODO: what's the best way to handle delegation tokens to unknown validators? + const metadataInDb = await this.indexedDb.getAssetsMetadata(assetId); + if (metadataInDb) continue; - // Find the index of '1' in the string - const index = metadata.display.indexOf('1'); - // Get the first N characters after the '1' - const id = metadata.display.substring(index + 1, index + 1 + 24); + const metadataFromNode = await this.querier.shieldedPool.assetMetadata(assetId); - metadata.symbol = 'Delegated UM (' + id + '...)'; + if (metadataFromNode) { + customizeSymbol(metadataFromNode); + await this.indexedDb.saveAssetsMetadata(metadataFromNode); } - - // TODO: unbonding tokens? - - // Note: the below code is incorrect, the asset ID is the hash of the denom, - // so this is actually generating metadata for a different asset. Not sure - // when/if this is used. - if (!metadata) { - const UNNAMED_ASSET_PREFIX = 'passet'; - const denom = bech32.encode(UNNAMED_ASSET_PREFIX, bech32.toWords(assetId.inner)); - metadata = new Metadata({ - base: denom, - denomUnits: [{ aliases: [], denom, exponent: 0 }], - display: denom, - penumbraAssetId: assetId, - }); - } - - await this.indexedDb.saveAssetsMetadata(metadata); } } diff --git a/packages/query/src/customize-symbol.test.ts b/packages/query/src/customize-symbol.test.ts new file mode 100644 index 0000000000..b6b4661baf --- /dev/null +++ b/packages/query/src/customize-symbol.test.ts @@ -0,0 +1,32 @@ +import { describe, expect, test } from 'vitest'; +import { Metadata } from '@buf/penumbra-zone_penumbra.bufbuild_es/penumbra/core/asset/v1/asset_pb'; +import { customizeSymbol } from './customize-symbol'; + +describe('Customizing metadata', () => { + test('should work for delegation token', () => { + const metadata = new Metadata({ + display: + 'delegation_penumbravalid1fjuj67ayaqueqxg03d65ps5aah6m39u39qeacu3zv2cw3dzxssyq3yrcez', + }); + customizeSymbol(metadata); + expect(metadata.symbol).toBe('Delegated UM (fjuj67ayaqueqxg03d65ps5aa...)'); + }); + + test('should work for unbonding token', () => { + const metadata = new Metadata({ + display: + 'uunbonding_epoch_29_penumbravalid1fjuj67ayaqueqxg03d65ps5aah6m39u39qeacu3zv2cw3dzxssyq3yrcez', + }); + customizeSymbol(metadata); + expect(metadata.symbol).toBe('Unbonding UM, epoch 29 (fjuj67ayaqueqxg03d65ps5aa...)'); + }); + + test('should do nothing if no matches', () => { + const metadata = new Metadata({ + display: 'test_usd', + symbol: 'usdc', + }); + customizeSymbol(metadata); + expect(metadata.symbol).toBe('usdc'); + }); +}); diff --git a/packages/query/src/customize-symbol.ts b/packages/query/src/customize-symbol.ts new file mode 100644 index 0000000000..de711ae3e4 --- /dev/null +++ b/packages/query/src/customize-symbol.ts @@ -0,0 +1,27 @@ +import { Metadata } from '@buf/penumbra-zone_penumbra.bufbuild_es/penumbra/core/asset/v1/asset_pb'; +import { + assetPatterns, + DelegationCaptureGroups, + UnbondingCaptureGroups, +} from '@penumbra-zone/constants'; + +const DELEGATION_SYMBOL_LENGTH = 50 - 'delegation_penumbravalid1'.length; +const UNBONDING_SYMBOL_LENGTH = 41 - 'unbonding_epoch_'.length; + +// If the metadata is for a delegation or unbonding tokens, customize its symbol. +// We can't trust the validator's self-described name, so use their validator ID (in metadata.display). +export const customizeSymbol = (metadata: Metadata) => { + const delegationMatch = assetPatterns.delegationToken.exec(metadata.display); + if (delegationMatch) { + const { id } = delegationMatch.groups as unknown as DelegationCaptureGroups; + const shortenedId = id.slice(0, DELEGATION_SYMBOL_LENGTH); + metadata.symbol = `Delegated UM (${shortenedId}...)`; + } + + const unbondingMatch = assetPatterns.unbondingToken.exec(metadata.display); + if (unbondingMatch) { + const { id, epoch } = unbondingMatch.groups as unknown as UnbondingCaptureGroups; + const shortenedId = id.slice(0, UNBONDING_SYMBOL_LENGTH); + metadata.symbol = `Unbonding UM, epoch ${epoch} (${shortenedId}...)`; + } +}; diff --git a/packages/router/src/grpc/view-protocol-server/assets.ts b/packages/router/src/grpc/view-protocol-server/assets.ts index c69bd6798c..c9ec09e6a4 100644 --- a/packages/router/src/grpc/view-protocol-server/assets.ts +++ b/packages/router/src/grpc/view-protocol-server/assets.ts @@ -22,23 +22,23 @@ export const assets: Impl['assets'] = async function* (req, ctx) { }[] = [ { includeReq: includeLpNfts, - pattern: assetPatterns.lpNftPattern, + pattern: assetPatterns.lpNft, }, { includeReq: includeDelegationTokens, - pattern: assetPatterns.delegationTokenPattern, + pattern: assetPatterns.delegationToken, }, { includeReq: includeProposalNfts, - pattern: assetPatterns.proposalNftPattern, + pattern: assetPatterns.proposalNft, }, { includeReq: includeUnbondingTokens, - pattern: assetPatterns.unbondingTokenPattern, + pattern: assetPatterns.unbondingToken, }, { includeReq: includeVotingReceiptTokens, - pattern: assetPatterns.votingReceiptPattern, + pattern: assetPatterns.votingReceipt, }, ...includeSpecificDenominations.map(d => ({ includeReq: true, diff --git a/packages/storage/src/indexed-db/index.ts b/packages/storage/src/indexed-db/index.ts index 524e155ea4..44a3b54030 100644 --- a/packages/storage/src/indexed-db/index.ts +++ b/packages/storage/src/indexed-db/index.ts @@ -32,7 +32,7 @@ import { AddressIndex, IdentityKey, } from '@buf/penumbra-zone_penumbra.bufbuild_es/penumbra/core/keys/v1/keys_pb'; -import { assetPatterns, localAssets } from '@penumbra-zone/constants'; +import { assetPatterns, DelegationCaptureGroups, localAssets } from '@penumbra-zone/constants'; import { Position, PositionId, @@ -319,7 +319,7 @@ export class IndexedDb implements IndexedDbInterface { for await (const assetCursor of this.db.transaction('ASSETS').store) { const denomMetadata = Metadata.fromJson(assetCursor.value); if ( - assetPatterns.delegationTokenPattern.test(denomMetadata.display) && + assetPatterns.delegationToken.test(denomMetadata.display) && denomMetadata.penumbraAssetId ) { delegationAssets.set(uint8ArrayToHex(denomMetadata.penumbraAssetId.inner), denomMetadata); @@ -354,10 +354,10 @@ export class IndexedDb implements IndexedDbInterface { // delegation asset denom consists of prefix 'delegation_' and validator identity key in bech32m encoding // For example, in denom 'delegation_penumbravalid12s9lanucncnyasrsqgy6z532q7nwsw3aqzzeqqas55kkpyf6lhsqs2w0zar' // 'penumbravalid12s9lanucncnyasrsqgy6z532q7nwsw3aqzzeqas55kkpyf6lhsqs2w0zar' is validator identity key. - const bech32IdentityKey = asset?.display.replace(assetPatterns.delegationTokenPattern, ''); + const regexResult = assetPatterns.delegationToken.exec(asset?.display ?? ''); + if (!regexResult) throw new Error('expected delegation token identity key not present'); - if (!bech32IdentityKey) - throw new Error('expected delegation token identity key not present'); + const { bech32IdentityKey } = regexResult.groups as unknown as DelegationCaptureGroups; notesForVoting.push( new NotesForVotingResponse({