diff --git a/packages/extension-base/src/background/KoniTypes.ts b/packages/extension-base/src/background/KoniTypes.ts index ff96b76aeb..2b892a8f47 100644 --- a/packages/extension-base/src/background/KoniTypes.ts +++ b/packages/extension-base/src/background/KoniTypes.ts @@ -277,6 +277,17 @@ export interface MetadataItem { types: Record | string>; userExtensions?: ExtDef; hexV15?: HexString; + tokenInfo?: { + ss58Format: number; + tokenDecimals: number; + tokenSymbol: string; + }; +} + +export interface MetadataV15Item { + genesisHash: string; + specVersion: string; + hexV15?: HexString; } export interface CrowdloanItem { diff --git a/packages/extension-base/src/koni/background/handlers/Extension.ts b/packages/extension-base/src/koni/background/handlers/Extension.ts index 7a33f1698c..647f0fcefe 100644 --- a/packages/extension-base/src/koni/background/handlers/Extension.ts +++ b/packages/extension-base/src/koni/background/handlers/Extension.ts @@ -55,7 +55,6 @@ import { CommonOptimalPath } from '@subwallet/extension-base/types/service-base' import { SwapPair, SwapQuoteResponse, SwapRequest, SwapRequestResult, SwapSubmitParams, ValidateSwapProcessParams } from '@subwallet/extension-base/types/swap'; import { _analyzeAddress, BN_ZERO, combineAllAccountProxy, createTransactionFromRLP, isSameAddress, MODULE_SUPPORT, reformatAddress, signatureToHex, Transaction as QrTransaction, transformAccounts, transformAddresses, uniqueStringArray } from '@subwallet/extension-base/utils'; import { parseContractInput, parseEvmRlp } from '@subwallet/extension-base/utils/eth/parseTransaction'; -import { metadataExpand } from '@subwallet/extension-chains'; import { MetadataDef } from '@subwallet/extension-inject/types'; import { getKeypairTypeByAddress, isAddress, isSubstrateAddress, isTonAddress } from '@subwallet/keyring'; import { EthereumKeypairTypes, SubstrateKeypairTypes, TonKeypairTypes } from '@subwallet/keyring/types'; @@ -71,12 +70,13 @@ import { combineLatest, Subject } from 'rxjs'; import { TransactionConfig } from 'web3-core'; import { SubmittableExtrinsic } from '@polkadot/api/types'; -import { Metadata, TypeRegistry } from '@polkadot/types'; -import { ChainProperties } from '@polkadot/types/interfaces'; +import { TypeRegistry } from '@polkadot/types'; import { AnyJson, Registry, SignerPayloadJSON, SignerPayloadRaw } from '@polkadot/types/types'; import { assert, hexStripPrefix, hexToU8a, isAscii, isHex, u8aToHex } from '@polkadot/util'; import { decodeAddress, isEthereumAddress } from '@polkadot/util-crypto'; +import { getSuitableRegistry, RegistrySource, setupApiRegistry, setupDappRegistry, setupDatabaseRegistry } from '../utils'; + export function isJsonPayload (value: SignerPayloadJSON | SignerPayloadRaw): value is SignerPayloadJSON { return (value as SignerPayloadJSON).genesisHash !== undefined; } @@ -2516,7 +2516,7 @@ export default class KoniExtension { } } - /// Signing substrate request + // Signing substrate request private async signingApprovePasswordV2 ({ id }: RequestSigningApprovePasswordV2): Promise { const queued = this.#koniState.getSignRequest(id); @@ -2527,7 +2527,7 @@ export default class KoniExtension { // unlike queued.account.address the following // address is encoded with the default prefix - // which what is used for password caching mapping + // which is used for password caching mapping const { address } = pair; if (!pair) { @@ -2542,77 +2542,29 @@ export default class KoniExtension { const { payload } = request; - let registry: Registry; + let registry: Registry = new TypeRegistry(); if (isJsonPayload(payload)) { const [, chainInfo] = this.#koniState.findNetworkKeyByGenesisHash(payload.genesisHash); - let metadata: MetadataDef | MetadataItem | undefined; - - /** - * Get the metadata for the genesisHash - * @todo: need to handle case metadata store in db - */ - metadata = this.#koniState.knownMetadata.find((meta: MetadataDef) => - meta.genesisHash === payload.genesisHash); - - if (metadata) { - // we have metadata, expand it and extract the info/registry - const expanded = metadataExpand(metadata, false); - registry = expanded.registry; - registry.setSignedExtensions(payload.signedExtensions, expanded.definition.userExtensions); + const allRegistry: RegistrySource[] = [ + setupApiRegistry(chainInfo, this.#koniState), + setupDatabaseRegistry( + await this.#koniState.chainService.getMetadataByHash(payload.genesisHash) as MetadataItem, + chainInfo, + payload + ), + setupDappRegistry( + this.#koniState.knownMetadata.find((meta: MetadataDef) => meta.genesisHash === payload.genesisHash) as MetadataDef, + payload + ) + ].filter((item): item is RegistrySource => item !== null && item.registry !== undefined); + + if (allRegistry.length === 0) { + registry.setSignedExtensions(payload.signedExtensions); } else { - metadata = await this.#koniState.chainService.getMetadataByHash(payload.genesisHash); - - if (metadata) { - registry = new TypeRegistry(); - - const _metadata = new Metadata(registry, metadata.hexValue); - - registry.register(metadata.types); - registry.setChainProperties(registry.createType('ChainProperties', { - ss58Format: chainInfo?.substrateInfo?.addressPrefix ?? 42, - tokenDecimals: chainInfo?.substrateInfo?.decimals, - tokenSymbol: chainInfo?.substrateInfo?.symbol - }) as unknown as ChainProperties); - registry.setMetadata(_metadata, payload.signedExtensions, metadata.userExtensions); - } else { - // we have no metadata, create a new registry - registry = new TypeRegistry(); - registry.setSignedExtensions(payload.signedExtensions); - } - } - - if (!metadata) { - /* - * Some networks must have metadata to signing, - * so if the chain not active (cannot use metadata from api), it must be rejected - * */ - if ( - chainInfo && - (_API_OPTIONS_CHAIN_GROUP.avail.includes(chainInfo.slug) || _API_OPTIONS_CHAIN_GROUP.goldberg.includes(chainInfo.slug)) // The special case for chains that need metadata to signing - ) { - // For case the chain does not have any provider - if (!Object.keys(chainInfo.providers).length) { - reject(new Error('{{chain}} network does not have any provider to connect, please update metadata from dApp'.replaceAll('{{chain}}', chainInfo.name))); - - return false; - } - - const isChainActive = this.#koniState.getChainStateByKey(chainInfo.slug).active; - - if (!isChainActive) { - reject(new Error('Please activate {{chain}} network before signing'.replaceAll('{{chain}}', chainInfo.name))); - - return false; - } - - registry = this.#koniState.getSubstrateApi(chainInfo.slug).api.registry as unknown as TypeRegistry; - } + registry = getSuitableRegistry(allRegistry, payload); } - } else { - // for non-payload, just create a registry to use - registry = new TypeRegistry(); } const result = request.sign(registry as unknown as TypeRegistry, pair); diff --git a/packages/extension-base/src/koni/background/utils.ts b/packages/extension-base/src/koni/background/utils.ts new file mode 100644 index 0000000000..d435b987bb --- /dev/null +++ b/packages/extension-base/src/koni/background/utils.ts @@ -0,0 +1,98 @@ +// Copyright 2019-2022 @subwallet/extension-koni authors & contributors +// SPDX-License-Identifier: Apache-2.0 + +import { _ChainInfo } from '@subwallet/chain-list/types'; +import { MetadataItem } from '@subwallet/extension-base/background/KoniTypes'; +import { metadataExpand } from '@subwallet/extension-chains/bundle'; +import { MetadataDef } from '@subwallet/extension-inject/types'; + +import { Metadata, TypeRegistry } from '@polkadot/types'; +import { ChainProperties } from '@polkadot/types/interfaces'; +import { Registry, SignerPayloadJSON } from '@polkadot/types/types'; + +import KoniState from './handlers/State'; + +export interface RegistrySource{ + registry: Registry, + specVersion: string | number, +} + +export function getSuitableRegistry (registries: RegistrySource[], payload: SignerPayloadJSON) { + const payloadSpecVersion = parseInt(payload.specVersion); + const sortedRegistries = registries + .filter((registrySource): registrySource is RegistrySource => registrySource.registry !== undefined) + .map((registrySource) => { + const specVersion = Number(registrySource.specVersion); + const distance = Math.abs(specVersion - payloadSpecVersion); + const isHigher = specVersion >= payloadSpecVersion; + + return { + registry: registrySource.registry, + specVersion, + distance, + isHigher + }; + }) + .sort((a, b) => { + if (a.distance !== b.distance) { + return a.distance - b.distance; + } + + return a.specVersion - b.specVersion; + }); + + return sortedRegistries[0].registry; +} + +export function setupApiRegistry (chainInfo: _ChainInfo | undefined, koniState: KoniState): RegistrySource | null { + if (!chainInfo) { + return null; + } + + const api = koniState.getSubstrateApi(chainInfo.slug).api; + const apiSpecVersion = api?.runtimeVersion.specVersion.toString(); + const registry = api?.registry as unknown as TypeRegistry; + + return { + registry, + specVersion: apiSpecVersion + }; +} + +export function setupDatabaseRegistry (metadata: MetadataItem, chainInfo: _ChainInfo | undefined, payload: SignerPayloadJSON): RegistrySource | null { + if (!metadata || !metadata.genesisHash || !chainInfo) { + return null; + } + + const registry = new TypeRegistry(); + const _metadata = new Metadata(registry, metadata.hexValue); + + registry.register(metadata.types); + registry.setChainProperties(registry.createType('ChainProperties', { + ss58Format: metadata.tokenInfo?.ss58Format, + tokenDecimals: metadata.tokenInfo?.tokenDecimals, + tokenSymbol: metadata.tokenInfo?.tokenSymbol + }) as unknown as ChainProperties); + registry.setMetadata(_metadata, payload.signedExtensions, metadata.userExtensions); + + return { + registry, + specVersion: metadata.specVersion + }; +} + +export function setupDappRegistry (metadata: MetadataDef, payload: SignerPayloadJSON): RegistrySource | null { + if (!metadata || !metadata.genesisHash) { + return null; + } + + const expanded = metadataExpand(metadata, false); + const registry = expanded.registry; + + registry.setSignedExtensions(payload.signedExtensions, expanded.definition.userExtensions); + + return { + registry, + specVersion: metadata.specVersion + }; +} diff --git a/packages/extension-base/src/services/chain-service/index.ts b/packages/extension-base/src/services/chain-service/index.ts index 5d5e3f7562..424dbd84c8 100644 --- a/packages/extension-base/src/services/chain-service/index.ts +++ b/packages/extension-base/src/services/chain-service/index.ts @@ -13,7 +13,7 @@ import { _CHAIN_VALIDATION_ERROR } from '@subwallet/extension-base/services/chai import { _ChainApiStatus, _ChainConnectionStatus, _ChainState, _CUSTOM_PREFIX, _DataMap, _EvmApi, _NetworkUpsertParams, _NFT_CONTRACT_STANDARDS, _SMART_CONTRACT_STANDARDS, _SmartContractTokenInfo, _SubstrateApi, _ValidateCustomAssetRequest, _ValidateCustomAssetResponse } from '@subwallet/extension-base/services/chain-service/types'; import { _isAssetAutoEnable, _isAssetCanPayTxFee, _isAssetFungibleToken, _isChainEnabled, _isCustomAsset, _isCustomChain, _isCustomProvider, _isEqualContractAddress, _isEqualSmartContractAsset, _isLocalToken, _isMantaZkAsset, _isPureEvmChain, _isPureSubstrateChain, _parseAssetRefKey, randomizeProvider, updateLatestChainInfo } from '@subwallet/extension-base/services/chain-service/utils'; import { EventService } from '@subwallet/extension-base/services/event-service'; -import { IChain, IMetadataItem } from '@subwallet/extension-base/services/storage-service/databases'; +import { IChain, IMetadataItem, IMetadataV15Item } from '@subwallet/extension-base/services/storage-service/databases'; import DatabaseService from '@subwallet/extension-base/services/storage-service/DatabaseService'; import AssetSettingStore from '@subwallet/extension-base/stores/AssetSetting'; import { addLazy, calculateMetadataHash, fetchStaticData, filterAssetsByChainAndType, getShortMetadata, MODULE_SUPPORT } from '@subwallet/extension-base/utils'; @@ -2070,6 +2070,14 @@ export class ChainService { return this.dbService.stores.metadata.upsertMetadata(chain, metadata); } + getMetadataV15 (chain: string) { + return this.dbService.stores.metadataV15.getMetadata(chain); + } + + upsertMetadataV15 (chain: string, metadata: IMetadataV15Item) { + return this.dbService.stores.metadataV15.upsertMetadata(chain, metadata); + } + getMetadataByHash (hash: string) { return this.dbService.stores.metadata.getMetadataByGenesisHash(hash); } diff --git a/packages/extension-base/src/services/storage-service/DatabaseService.ts b/packages/extension-base/src/services/storage-service/DatabaseService.ts index b0db514b1f..a91949bf05 100644 --- a/packages/extension-base/src/services/storage-service/DatabaseService.ts +++ b/packages/extension-base/src/services/storage-service/DatabaseService.ts @@ -6,7 +6,7 @@ import { APIItemState, ChainStakingMetadata, CrowdloanItem, MantaPayConfig, NftC import { EventService } from '@subwallet/extension-base/services/event-service'; import { _NotificationInfo } from '@subwallet/extension-base/services/inapp-notification-service/interfaces'; import KoniDatabase, { IBalance, ICampaign, IChain, ICrowdloanItem, INft } from '@subwallet/extension-base/services/storage-service/databases'; -import { AssetStore, BalanceStore, ChainStore, CrowdloanStore, MetadataStore, MigrationStore, NftCollectionStore, NftStore, PriceStore, StakingStore, TransactionStore } from '@subwallet/extension-base/services/storage-service/db-stores'; +import { AssetStore, BalanceStore, ChainStore, CrowdloanStore, MetadataStore, MetadataV15Store, MigrationStore, NftCollectionStore, NftStore, PriceStore, StakingStore, TransactionStore } from '@subwallet/extension-base/services/storage-service/db-stores'; import BaseStore from '@subwallet/extension-base/services/storage-service/db-stores/BaseStore'; import CampaignStore from '@subwallet/extension-base/services/storage-service/db-stores/Campaign'; import ChainStakingMetadataStore from '@subwallet/extension-base/services/storage-service/db-stores/ChainStakingMetadata'; @@ -55,6 +55,8 @@ export default class DatabaseService { migration: new MigrationStore(this._db.migrations), metadata: new MetadataStore(this._db.metadata), + metadataV15: new MetadataV15Store(this._db.metadataV15), + chain: new ChainStore(this._db.chain), asset: new AssetStore(this._db.asset), diff --git a/packages/extension-base/src/services/storage-service/databases/index.ts b/packages/extension-base/src/services/storage-service/databases/index.ts index 1cda2f9af1..9f4c525870 100644 --- a/packages/extension-base/src/services/storage-service/databases/index.ts +++ b/packages/extension-base/src/services/storage-service/databases/index.ts @@ -2,7 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 import { _AssetRef, _ChainAsset, _ChainInfo } from '@subwallet/chain-list/types'; -import { CampaignData, ChainStakingMetadata, CrowdloanItem, MetadataItem, NftCollection, NftItem, NominatorMetadata, PriceJson, StakingItem, TransactionHistoryItem } from '@subwallet/extension-base/background/KoniTypes'; +import { CampaignData, ChainStakingMetadata, CrowdloanItem, MetadataItem, MetadataV15Item, NftCollection, NftItem, NominatorMetadata, PriceJson, StakingItem, TransactionHistoryItem } from '@subwallet/extension-base/background/KoniTypes'; import { _NotificationInfo } from '@subwallet/extension-base/services/inapp-notification-service/interfaces'; import { BalanceItem, YieldPoolInfo, YieldPositionInfo } from '@subwallet/extension-base/types'; import Dexie, { Table, Transaction } from 'dexie'; @@ -41,6 +41,7 @@ export interface IMigration { } export interface IMetadataItem extends MetadataItem, DefaultChainDoc {} +export interface IMetadataV15Item extends MetadataV15Item, DefaultChainDoc {} export type IMantaPayLedger = any; @@ -62,6 +63,8 @@ export default class KoniDatabase extends Dexie { public migrations!: Table; public metadata!: Table; + public metadataV15!: Table; + public chain!: Table; public asset!: Table<_ChainAsset, object>; diff --git a/packages/extension-base/src/services/storage-service/db-stores/MetadataV15.ts b/packages/extension-base/src/services/storage-service/db-stores/MetadataV15.ts new file mode 100644 index 0000000000..a985c2927e --- /dev/null +++ b/packages/extension-base/src/services/storage-service/db-stores/MetadataV15.ts @@ -0,0 +1,24 @@ +// Copyright 2019-2022 @subwallet/extension-base authors & contributors +// SPDX-License-Identifier: Apache-2.0 + +import BaseStoreWithChain from '@subwallet/extension-base/services/storage-service/db-stores/BaseStoreWithChain'; + +import { IMetadataV15Item } from '../databases'; + +export default class MetadataV15Store extends BaseStoreWithChain { + getMetadata (chain: string) { + return this.table.where('chain').equals(chain).first(); + } + + upsertMetadata (chain: string, metadata: IMetadataV15Item) { + return this.table.put(metadata, chain); + } + + getMetadataByGenesisHash (genesisHash: string) { + return this.table.get(genesisHash); + } + + updateMetadataByGenesisHash (genesisHash: string, metadata: IMetadataV15Item) { + return this.table.put(metadata, genesisHash); + } +} diff --git a/packages/extension-base/src/services/storage-service/db-stores/index.ts b/packages/extension-base/src/services/storage-service/db-stores/index.ts index f26e20b498..d3e465eaaf 100644 --- a/packages/extension-base/src/services/storage-service/db-stores/index.ts +++ b/packages/extension-base/src/services/storage-service/db-stores/index.ts @@ -11,5 +11,7 @@ export { default as TransactionStore } from './Transaction'; export { default as MigrationStore } from './Migration'; export { default as MetadataStore } from './Metadata'; +export { default as MetadataV15Store } from './MetadataV15'; + export { default as ChainStore } from './Chain'; export { default as AssetStore } from './Asset'; diff --git a/packages/extension-base/src/utils/metadata.ts b/packages/extension-base/src/utils/metadata.ts index 41c6c47c2d..01653171a8 100644 --- a/packages/extension-base/src/utils/metadata.ts +++ b/packages/extension-base/src/utils/metadata.ts @@ -4,11 +4,27 @@ import { ChainService } from '@subwallet/extension-base/services/chain-service'; import { _SubstrateApi } from '@subwallet/extension-base/services/chain-service/types'; +import { ApiPromise } from '@polkadot/api'; +import { TypeRegistry } from '@polkadot/types'; import { getSpecExtensions, getSpecTypes } from '@polkadot/types-known'; -import { u8aToHex } from '@polkadot/util'; +import { formatBalance, isNumber, u8aToHex } from '@polkadot/util'; import { HexString } from '@polkadot/util/types'; +import { defaults as addressDefaults } from '@polkadot/util-crypto/address/defaults'; import { ExtraInfo, merkleizeMetadata } from '@polkadot-api/merkleize-metadata'; +interface Statics { + api: ApiPromise; + registry: TypeRegistry; +} + +export const statics = { + api: undefined, + registry: new TypeRegistry() +} as unknown as Statics; + +export const DEFAULT_DECIMALS = statics.registry.createType('u32', 12); +export const DEFAULT_SS58 = statics.registry.createType('u32', addressDefaults.prefix); + export const _isRuntimeUpdated = (signedExtensions?: string[]): boolean => { return signedExtensions ? signedExtensions.includes('CheckMetadataHash') : false; }; @@ -25,43 +41,87 @@ export const getShortMetadata = (blob: HexString, extraInfo: ExtraInfo, metadata return u8aToHex(_merkleizeMetadata.getProofForExtrinsicPayload(blob)); }; -export const cacheMetadata = ( - chain: string, - substrateApi: _SubstrateApi, - chainService?: ChainService -): void => { - // Update metadata to database with async methods - substrateApi.api.isReady.then(async (api) => { +const getMetadataV15 = async (chain: string, api: ApiPromise, chainService?: ChainService): Promise => { + try { const currentSpecVersion = api.runtimeVersion.specVersion.toString(); - const specName = api.runtimeVersion.specName.toString(); const genesisHash = api.genesisHash.toHex(); - const metadata = await chainService?.getMetadata(chain); + const metadata = await chainService?.getMetadataV15(chain); // Avoid date existed metadata if (metadata && metadata.specVersion === currentSpecVersion && metadata.genesisHash === genesisHash) { return; } - const systemChain = await api.rpc.system.chain(); - // const _metadata: Option = await api.call.metadata.metadataAtVersion(15); - // const metadataHex = _metadata.isSome ? _metadata.unwrap().toHex().slice(2) : ''; // Need unwrap to create metadata object - let hexV15: HexString | undefined; + if (api.call.metadata.metadataAtVersion) { + const metadataV15 = await api.call.metadata.metadataAtVersion(15); + + if (!metadataV15.isEmpty) { + const hexV15 = metadataV15.unwrap().toHex(); + const updateMetadata = { + chain: chain, + genesisHash: genesisHash, + specVersion: currentSpecVersion, + hexV15 - const metadataV15 = await api.call.metadata.metadataAtVersion(15); + }; - if (!metadataV15.isEmpty) { - hexV15 = metadataV15.unwrap().toHex(); + chainService?.upsertMetadataV15(chain, { ...updateMetadata }).catch(console.error); + } } + } catch (err) { + console.error('Error:', err); + } +}; + +const getMetadata = async ( + chain: string, + api: ApiPromise, + chainService?: ChainService +) => { + const currentSpecVersion = api.runtimeVersion.specVersion.toString(); + const genesisHash = api.genesisHash.toHex(); + const specName = api.runtimeVersion.specName.toString(); + const metadata = await chainService?.getMetadata(chain); + + // Avoid date existed metadata + if (metadata && metadata.specVersion === currentSpecVersion && metadata.genesisHash === genesisHash) { + return; + } + + const systemChain = api.runtimeChain; + const metadataHex = api.runtimeMetadata.toHex(); + const registry = api.registry; + + const tokenInfo = { + ss58Format: isNumber(registry.chainSS58) + ? registry.chainSS58 + : DEFAULT_SS58.toNumber(), + tokenDecimals: (registry.chainDecimals || [DEFAULT_DECIMALS.toNumber()])[0], + tokenSymbol: (registry.chainTokens || formatBalance.getDefaults().unit)[0] + }; + + const updateMetadata = { + chain: chain, + genesisHash: genesisHash, + specName: specName, + specVersion: currentSpecVersion, + hexValue: metadataHex, + types: getSpecTypes(api.registry, systemChain, api.runtimeVersion.specName, api.runtimeVersion.specVersion) as unknown as Record, + userExtensions: getSpecExtensions(api.registry, systemChain, api.runtimeVersion.specName), + tokenInfo + }; + + chainService?.upsertMetadata(chain, { ...updateMetadata }).catch(console.error); +}; - chainService?.upsertMetadata(chain, { - chain: chain, - genesisHash: genesisHash, - specName: specName, - specVersion: currentSpecVersion, - hexValue: api.runtimeMetadata.toHex(), - types: getSpecTypes(api.registry, systemChain, api.runtimeVersion.specName, api.runtimeVersion.specVersion) as unknown as Record, - userExtensions: getSpecExtensions(api.registry, systemChain, api.runtimeVersion.specName), - hexV15 - }).catch(console.error); +export const cacheMetadata = ( + chain: string, + substrateApi: _SubstrateApi, + chainService?: ChainService +): void => { + // Update metadata to database with async methods + substrateApi.api.isReady.then(async (api) => { + await getMetadata(chain, api, chainService); + await getMetadataV15(chain, api, chainService); }).catch(console.error); };