// Copyright 2017-2026 @pezkuwi/app-settings authors & contributors // SPDX-License-Identifier: Apache-2.0 import type { ApiPromise } from '@pezkuwi/api'; import type { InjectedExtension, InjectedMetadataKnown, MetadataDef } from '@pezkuwi/extension-inject/types'; import { useEffect, useMemo, useState } from 'react'; import store from 'store'; import { createNamedHook, useApi } from '@pezkuwi/react-hooks'; interface ExtensionKnown { extension: InjectedExtension; known: InjectedMetadataKnown[]; update: (def: MetadataDef) => Promise; } interface ExtensionInfo extends ExtensionKnown { current: InjectedMetadataKnown | null; } interface Extensions { count: number; extensions: ExtensionInfo[]; } interface ExtensionProperties { extensionVersion: string; tokenDecimals: number; tokenSymbol: string; ss58Format?: number; } type SavedProperties = Record; type TriggerFn = (counter: number) => void; let triggerCount = 0; const triggers = new Map(); function triggerAll (): void { [...triggers.values()].forEach((trigger) => trigger(Date.now())); } // save the properties for a specific extension function saveProperties (api: ApiPromise, { name, version }: InjectedExtension): void { const storeKey = `properties:${api.genesisHash.toHex()}`; const allProperties = store.get(storeKey, {}) as SavedProperties; allProperties[name] = { extensionVersion: version, ss58Format: api.registry.chainSS58, tokenDecimals: api.registry.chainDecimals[0], tokenSymbol: api.registry.chainTokens[0] }; store.set(storeKey, allProperties); } // determines if the extension has current properties function hasCurrentProperties (api: ApiPromise, { extension }: ExtensionKnown): boolean { const allProperties = store.get(`properties:${api.genesisHash.toHex()}`, {}) as SavedProperties; // when we don't have properties yet, assume nothing has changed and store if (!allProperties[extension.name]) { saveProperties(api, extension); return true; } const { ss58Format, tokenDecimals, tokenSymbol } = allProperties[extension.name]; return ss58Format === api.registry.chainSS58 && tokenDecimals === api.registry.chainDecimals[0] && tokenSymbol === api.registry.chainTokens[0]; } // filter extensions based on the properties we have available function filterAll (api: ApiPromise, all: ExtensionKnown[]): Extensions { const extensions = all .map((info): ExtensionInfo | null => { const current = info.known.find(({ genesisHash }) => api.genesisHash.eq(genesisHash)) || null; // if we cannot find it as known, or either the specVersion or properties mismatches, mark it as upgradable return !current || api.runtimeVersion.specVersion.gtn(current.specVersion) || !hasCurrentProperties(api, info) ? { ...info, current } : null; }) .filter((info): info is ExtensionInfo => !!info); return { count: extensions.length, extensions }; } async function getExtensionInfo (api: ApiPromise, extension: InjectedExtension): Promise { if (!extension.metadata) { return null; } try { const metadata = extension.metadata; const known = await metadata.get(); return { extension, known, update: async (def: MetadataDef): Promise => { let isOk = false; try { isOk = await metadata.provide(def); if (isOk) { saveProperties(api, extension); triggerAll(); } } catch { // ignore } return isOk; } }; } catch { return null; } } async function getKnown (api: ApiPromise, extensions: InjectedExtension[], _: number): Promise { const all = await Promise.all( extensions.map((extension) => getExtensionInfo(api, extension)) ); return all.filter((info): info is ExtensionKnown => !!info); } const EMPTY_STATE = { count: 0, extensions: [] }; function useExtensionsImpl (): Extensions { const { api, extensions, isApiReady, isDevelopment } = useApi(); const [all, setAll] = useState(); const [trigger, setTrigger] = useState(0); useEffect((): () => void => { const myId = `${++triggerCount}-${Date.now()}`; triggers.set(myId, setTrigger); return (): void => { triggers.delete(myId); }; }, []); useEffect((): void => { extensions && getKnown(api, extensions, trigger) .then(setAll) .catch(console.error); }, [api, extensions, trigger]); return useMemo( () => isDevelopment || !isApiReady || !all ? EMPTY_STATE : filterAll(api, all), [all, api, isApiReady, isDevelopment] ); } export default createNamedHook('useExtensions', useExtensionsImpl);