From e02ec5462949e3322997df78ddca9e0cf768cf85 Mon Sep 17 00:00:00 2001 From: alecdwm Date: Wed, 3 Jul 2024 01:35:51 +0000 Subject: [PATCH] feat: scale-ts --- .../balances-demo/src/components/Balances.tsx | 29 +- apps/balances-demo/src/main.tsx | 4 +- packages/balances-react/src/atoms/balances.ts | 23 +- .../src/atoms/cryptoWaitReady.ts | 5 +- packages/balances-react/src/index.tsx | 36 +- packages/balances/package.json | 2 +- packages/balances/src/BalanceModule.ts | 3 +- packages/balances/src/MiniMetadataUpdater.ts | 57 +- .../src/modules/SubstrateAssetsModule.ts | 296 +++++------ .../src/modules/SubstrateEquilibriumModule.ts | 260 ++++----- .../SubstrateNativeModule/QueryCache.ts | 365 ++++++------- .../modules/SubstrateNativeModule/index.ts | 466 ++++++++-------- .../util.ts} | 29 +- .../src/modules/SubstratePsp22Module.ts | 5 +- .../src/modules/SubstrateTokensModule.ts | 180 +++---- packages/balances/src/modules/index.ts | 2 - .../modules/util/InferBalanceModuleTypes.ts | 42 ++ .../src/modules/util/RpcStateQueryHelper.ts | 104 ++++ .../balances/src/modules/util/balances.ts | 56 ++ .../src/modules/util/buildStorageCoders.ts | 75 +++ .../src/modules/util/detectTransferMethod.ts | 29 + .../src/modules/util/findChainMeta.ts | 41 ++ .../src/modules/util/getUniqueChainIds.ts | 14 + packages/balances/src/modules/util/index.ts | 503 +----------------- .../{storage.ts => storageCompression.ts} | 0 packages/balances/src/types/minimetadatas.ts | 7 +- packages/chaindata-provider/src/constants.ts | 2 +- packages/extension-core/src/background.ts | 10 +- .../domains/accounts/helpers.onChainIds.ts | 8 +- .../src/domains/app/__tests__/handler.spec.ts | 19 +- .../src/domains/balances/pool.ts | 21 +- .../signing/__tests__/requestsStore.spec.ts | 11 +- .../sitesAuthorised/__tests__/handler.spec.ts | 9 +- .../src/handlers/Extension.spec.ts | 15 +- .../libs/migrations/legacyMigrations.spec.ts | 9 +- .../src/util/awaitKeyringLoaded.ts | 11 +- packages/on-chain-id/src/index.ts | 10 +- .../on-chain-id/src/util/addressesToNames.ts | 91 +--- packages/on-chain-id/src/util/types.ts | 4 - packages/scale/package.json | 12 +- packages/scale/src/capi/README.md | 3 - packages/scale/src/capi/crypto/base58.ts | 194 ------- packages/scale/src/capi/crypto/concat.ts | 26 - packages/scale/src/capi/crypto/hashers.ts | 154 ------ packages/scale/src/capi/crypto/index.ts | 8 - packages/scale/src/capi/crypto/ss58.ts | 141 ----- .../scale/src/capi/crypto/util/index.test.ts | 74 --- packages/scale/src/capi/crypto/util/index.ts | 12 - packages/scale/src/capi/crypto/util/nosimd.ts | 53 -- .../src/capi/frame_metadata/Extrinsic.ts | 154 ------ .../src/capi/frame_metadata/FrameMetadata.ts | 65 --- .../src/capi/frame_metadata/decodeMetadata.ts | 30 -- .../scale/src/capi/frame_metadata/index.ts | 6 - .../src/capi/frame_metadata/key_codecs.ts | 116 ---- .../scale/src/capi/frame_metadata/raw/v14.ts | 226 -------- packages/scale/src/capi/index.ts | 4 - packages/scale/src/capi/scale_info/index.ts | 6 - .../capi/scale_info/overrides/ChainError.ts | 30 -- .../src/capi/scale_info/overrides/Era.ts | 104 ---- .../src/capi/scale_info/overrides/index.ts | 5 - .../capi/scale_info/overrides/overrides.ts | 72 --- packages/scale/src/capi/scale_info/raw/Ty.ts | 81 --- .../scale/src/capi/scale_info/transformTys.ts | 289 ---------- packages/scale/src/capi/util/index.ts | 3 - packages/scale/src/capi/util/key.ts | 30 -- packages/scale/src/capi/util/normalize.ts | 105 ---- packages/scale/src/capi/util/state.ts | 53 -- packages/scale/src/index.ts | 9 +- packages/scale/src/metadata/index.ts | 2 - packages/scale/src/metadata/util.ts | 248 --------- packages/scale/src/papito.ts | 8 + packages/scale/src/storage/getShape.ts | 222 -------- packages/scale/src/storage/getTypeName.ts | 60 --- packages/scale/src/storage/index.ts | 12 - ...suppressPortableRegistryConsoleWarnings.ts | 26 - packages/scale/src/util/compactMetadata.ts | 271 ++++++++++ packages/scale/src/util/decodeMetadata.ts | 23 + packages/scale/src/util/decodeScale.ts | 20 + packages/scale/src/util/encodeMetadata.ts | 14 + packages/scale/src/util/encodeStateKey.ts | 18 + packages/scale/src/util/getMetadataVersion.ts | 17 + packages/scale/src/util/index.ts | 6 + pnpm-lock.yaml | 64 +-- 83 files changed, 1681 insertions(+), 4248 deletions(-) rename packages/balances/src/modules/{util/substrate-native.ts => SubstrateNativeModule/util.ts} (86%) create mode 100644 packages/balances/src/modules/util/InferBalanceModuleTypes.ts create mode 100644 packages/balances/src/modules/util/RpcStateQueryHelper.ts create mode 100644 packages/balances/src/modules/util/balances.ts create mode 100644 packages/balances/src/modules/util/buildStorageCoders.ts create mode 100644 packages/balances/src/modules/util/detectTransferMethod.ts create mode 100644 packages/balances/src/modules/util/findChainMeta.ts create mode 100644 packages/balances/src/modules/util/getUniqueChainIds.ts rename packages/balances/src/modules/util/{storage.ts => storageCompression.ts} (100%) delete mode 100644 packages/scale/src/capi/README.md delete mode 100644 packages/scale/src/capi/crypto/base58.ts delete mode 100644 packages/scale/src/capi/crypto/concat.ts delete mode 100644 packages/scale/src/capi/crypto/hashers.ts delete mode 100644 packages/scale/src/capi/crypto/index.ts delete mode 100644 packages/scale/src/capi/crypto/ss58.ts delete mode 100644 packages/scale/src/capi/crypto/util/index.test.ts delete mode 100644 packages/scale/src/capi/crypto/util/index.ts delete mode 100644 packages/scale/src/capi/crypto/util/nosimd.ts delete mode 100644 packages/scale/src/capi/frame_metadata/Extrinsic.ts delete mode 100644 packages/scale/src/capi/frame_metadata/FrameMetadata.ts delete mode 100644 packages/scale/src/capi/frame_metadata/decodeMetadata.ts delete mode 100644 packages/scale/src/capi/frame_metadata/index.ts delete mode 100644 packages/scale/src/capi/frame_metadata/key_codecs.ts delete mode 100644 packages/scale/src/capi/frame_metadata/raw/v14.ts delete mode 100644 packages/scale/src/capi/index.ts delete mode 100644 packages/scale/src/capi/scale_info/index.ts delete mode 100644 packages/scale/src/capi/scale_info/overrides/ChainError.ts delete mode 100644 packages/scale/src/capi/scale_info/overrides/Era.ts delete mode 100644 packages/scale/src/capi/scale_info/overrides/index.ts delete mode 100644 packages/scale/src/capi/scale_info/overrides/overrides.ts delete mode 100644 packages/scale/src/capi/scale_info/raw/Ty.ts delete mode 100644 packages/scale/src/capi/scale_info/transformTys.ts delete mode 100644 packages/scale/src/capi/util/index.ts delete mode 100644 packages/scale/src/capi/util/key.ts delete mode 100644 packages/scale/src/capi/util/normalize.ts delete mode 100644 packages/scale/src/capi/util/state.ts delete mode 100644 packages/scale/src/metadata/index.ts delete mode 100644 packages/scale/src/metadata/util.ts create mode 100644 packages/scale/src/papito.ts delete mode 100644 packages/scale/src/storage/getShape.ts delete mode 100644 packages/scale/src/storage/getTypeName.ts delete mode 100644 packages/scale/src/storage/index.ts delete mode 100644 packages/scale/src/suppressPortableRegistryConsoleWarnings.ts create mode 100644 packages/scale/src/util/compactMetadata.ts create mode 100644 packages/scale/src/util/decodeMetadata.ts create mode 100644 packages/scale/src/util/decodeScale.ts create mode 100644 packages/scale/src/util/encodeMetadata.ts create mode 100644 packages/scale/src/util/encodeStateKey.ts create mode 100644 packages/scale/src/util/getMetadataVersion.ts create mode 100644 packages/scale/src/util/index.ts diff --git a/apps/balances-demo/src/components/Balances.tsx b/apps/balances-demo/src/components/Balances.tsx index ea633fdca2..01642707ee 100644 --- a/apps/balances-demo/src/components/Balances.tsx +++ b/apps/balances-demo/src/components/Balances.tsx @@ -9,7 +9,16 @@ export const Balances = () => { const balances = useBalances() return ( -
+
+ <> +
Logo
+
Colour
+
Status
+
Chain
+
Total
+
Available
+
Account
+ {balances?.sorted.map((balance) => (
{ { ) : null} + + + {formatDecimals(balance.total.tokens)} {balance.token?.symbol} + + + {typeof balance.total.fiat("usd") === "number" + ? new Intl.NumberFormat(undefined, { + style: "currency", + currency: "usd", + currencyDisplay: "narrowSymbol", + }).format(balance.total.fiat("usd") || 0) + : " -"} + + + {formatDecimals(balance.transferable.tokens)} {balance.token?.symbol} diff --git a/apps/balances-demo/src/main.tsx b/apps/balances-demo/src/main.tsx index 8e8c4da972..4f6e4ec363 100644 --- a/apps/balances-demo/src/main.tsx +++ b/apps/balances-demo/src/main.tsx @@ -1,13 +1,15 @@ import "anylogger-loglevel" - import "./index.css" import { BalancesProvider } from "@talismn/balances-react" +import loglevel from "loglevel" import { StrictMode, useState } from "react" import { createRoot } from "react-dom/client" import { App } from "./App" +loglevel.setLevel("info") + const onfinalityApiKey = undefined const Root = () => { diff --git a/packages/balances-react/src/atoms/balances.ts b/packages/balances-react/src/atoms/balances.ts index 1ea61afb1c..5d2ca344fe 100644 --- a/packages/balances-react/src/atoms/balances.ts +++ b/packages/balances-react/src/atoms/balances.ts @@ -3,11 +3,9 @@ import { Balance, BalanceJson, Balances, - HydrateDb, balances as balancesFn, - deleteSubscriptionId, getBalanceId, - getValidSubscriptionIds, + HydrateDb, } from "@talismn/balances" import { Token } from "@talismn/chaindata-provider" import { isEthereumAddress } from "@talismn/util" @@ -45,11 +43,6 @@ export const allBalancesAtom = atom(async (get) => { get(balancesHydrateDataAtom), ]) - const subscriptionIds = getValidSubscriptionIds() - if (subscriptionIds.size < 1) { - Object.values(balances).forEach((b) => (b.status = "cache")) - } - return new Balances( Object.values(balances).filter((balance) => !!hydrateData?.tokens?.[balance.tokenId]), // hydrate balance chains, evmNetworks, tokens and tokenRates @@ -288,19 +281,6 @@ const balancesSubscriptionAtomEffect = atomEffect((get) => { }) }, 30_000) - // TODO: Create subscriptions in a service worker, where we can detect page closes - // and therefore reliably delete the subscriptionId when the user closes our dapp - // - // For more information, check out https://developer.chrome.com/blog/page-lifecycle-api/#faqs - // and scroll down to: - // - `What is the back/forward cache?`, and - // - `If I can't run asynchronous APIs in the frozen or terminated states, how can I save data to IndexedDB? - // - // For now, we'll just last-ditch remove the subscriptionId (it works surprisingly well!) in the beforeunload event - window.onbeforeunload = () => { - deleteSubscriptionId() - } - return balanceModules.map((balanceModule) => { const unsub = balancesFn( balanceModule, @@ -375,7 +355,6 @@ const balancesSubscriptionAtomEffect = atomEffect((get) => { unsubs?.forEach((unsub) => unsub()) }) abort.signal.addEventListener("abort", () => setTimeout(unsubscribe, 2_000)) - abort.signal.addEventListener("abort", () => deleteSubscriptionId()) return () => abort.abort("Unsubscribed") }) diff --git a/packages/balances-react/src/atoms/cryptoWaitReady.ts b/packages/balances-react/src/atoms/cryptoWaitReady.ts index c762f3c5fb..bce3f965f6 100644 --- a/packages/balances-react/src/atoms/cryptoWaitReady.ts +++ b/packages/balances-react/src/atoms/cryptoWaitReady.ts @@ -1,7 +1,4 @@ import { cryptoWaitReady } from "@polkadot/util-crypto" -import { watCryptoWaitReady } from "@talismn/scale" import { atom } from "jotai" -export const cryptoWaitReadyAtom = atom( - async () => await Promise.all([cryptoWaitReady(), watCryptoWaitReady()]) -) +export const cryptoWaitReadyAtom = atom(async () => await cryptoWaitReady()) diff --git a/packages/balances-react/src/index.tsx b/packages/balances-react/src/index.tsx index bc805e8936..47bcb8aa35 100644 --- a/packages/balances-react/src/index.tsx +++ b/packages/balances-react/src/index.tsx @@ -1,3 +1,26 @@ +import { AnyBalanceModule, Hydrate } from "@talismn/balances" +import { useSetAtom } from "jotai" +import { ReactNode, useEffect } from "react" + +import { + balanceModuleCreatorsAtom, + coingeckoConfigAtom, + enabledChainsAtom, + enabledTokensAtom, + enableTestnetsAtom, + onfinalityApiKeyAtom, +} from "./atoms/config" + +export { + evmErc20TokenId, + evmNativeTokenId, + subNativeTokenId, + subEquilibriumTokenId, + subAssetTokenId, + subPsp22TokenId, + subTokensTokenId, +} from "@talismn/balances" + export * from "./hooks/useBalances" export * from "./hooks/useChainConnectors" export * from "./hooks/useChaindata" @@ -13,19 +36,6 @@ export * from "./atoms/config" export * from "./atoms/cryptoWaitReady" export * from "./atoms/tokenRates" -import { AnyBalanceModule, Hydrate } from "@talismn/balances" -import { useSetAtom } from "jotai" -import { ReactNode, useEffect } from "react" - -import { - balanceModuleCreatorsAtom, - coingeckoConfigAtom, - enableTestnetsAtom, - enabledChainsAtom, - enabledTokensAtom, - onfinalityApiKeyAtom, -} from "./atoms/config" - export type BalancesConfig = { /** * Optionally provide your own array of BalanceModules, when you don't want to use the defaults. diff --git a/packages/balances/package.json b/packages/balances/package.json index d391e39fda..d08b20c873 100644 --- a/packages/balances/package.json +++ b/packages/balances/package.json @@ -32,7 +32,6 @@ "@talismn/chain-connector-evm": "workspace:*", "@talismn/chaindata-provider": "workspace:*", "@talismn/scale": "workspace:*", - "@talismn/subshape-fork": "^0.0.2", "@talismn/token-rates": "workspace:*", "@talismn/util": "workspace:*", "anylogger": "^1.0.11", @@ -41,6 +40,7 @@ "lodash": "4.17.21", "pako": "^2.1.0", "rxjs": "^7.8.1", + "scale-ts": "^1.6.0", "viem": "^2.8.18" }, "devDependencies": { diff --git a/packages/balances/src/BalanceModule.ts b/packages/balances/src/BalanceModule.ts index 0744be0575..dcdb25e94c 100644 --- a/packages/balances/src/BalanceModule.ts +++ b/packages/balances/src/BalanceModule.ts @@ -113,7 +113,8 @@ interface BalanceModuleSubstrate< fetchSubstrateChainMeta( chainId: ChainId, moduleConfig?: TModuleConfig, - metadataRpc?: `0x${string}` + metadataRpc?: `0x${string}`, + systemProperties?: Record // eslint-disable-line @typescript-eslint/no-explicit-any ): Promise /** Detects which tokens are available on a given substrate chain */ diff --git a/packages/balances/src/MiniMetadataUpdater.ts b/packages/balances/src/MiniMetadataUpdater.ts index 62d96e7c81..250ff1bd06 100644 --- a/packages/balances/src/MiniMetadataUpdater.ts +++ b/packages/balances/src/MiniMetadataUpdater.ts @@ -8,9 +8,11 @@ import { fetchInitMiniMetadatas, fetchMiniMetadatas, } from "@talismn/chaindata-provider" +import { toHex } from "@talismn/scale" import { liveQuery } from "dexie" import isEqual from "lodash/isEqual" import { from } from "rxjs" +import { Bytes, Option, u32 } from "scale-ts" import { ChainConnectors } from "./BalanceModule" import log from "./log" @@ -244,7 +246,7 @@ export class MiniMetadataUpdater { return [] }) - const concurrency = 4 + const concurrency = 12 ;( await PromisePool.withConcurrency(concurrency) .for(needUpdates) @@ -257,11 +259,43 @@ export class MiniMetadataUpdater { if (specName === null) return if (specVersion === null) return - const metadataRpc = await this.#chainConnectors.substrate?.send( - chainId, - "state_getMetadata", - [] - ) + const fetchMetadata = async () => { + const errors: { v15: null | unknown; v14: null | unknown } = { v15: null, v14: null } + + try { + const response = await this.#chainConnectors.substrate?.send(chainId, "state_call", [ + "Metadata_metadata_at_version", + toHex(u32.enc(15)), + ]) + const result = response ? Option(Bytes()).dec(response) : null + if (result) return result + } catch (v15Cause) { + errors.v15 = v15Cause + } + + try { + const response = await this.#chainConnectors.substrate?.send( + chainId, + "state_getMetadata", + [] + ) + if (response) return response + } catch (v14Cause) { + errors.v14 = v14Cause + } + + log.warn( + `Failed to fetch both metadata v15 and v14 for chain ${chainId}`, + errors.v15, + errors.v14 + ) + return null + } + + const [metadataRpc, systemProperties] = await Promise.all([ + fetchMetadata(), + this.#chainConnectors.substrate?.send(chainId, "system_properties", []), + ]) for (const mod of this.#balanceModules.filter((m) => m.type.startsWith("substrate-"))) { const balancesConfig = (chain.balancesConfig ?? []).find( @@ -269,8 +303,13 @@ export class MiniMetadataUpdater { ) const moduleConfig = balancesConfig?.moduleConfig ?? {} - const metadata = await mod.fetchSubstrateChainMeta(chainId, moduleConfig, metadataRpc) - const tokens = await mod.fetchSubstrateChainTokens(chainId, metadata, moduleConfig) + const chainMeta = await mod.fetchSubstrateChainMeta( + chainId, + moduleConfig, + metadataRpc, + systemProperties + ) + const tokens = await mod.fetchSubstrateChainTokens(chainId, chainMeta, moduleConfig) // update tokens in chaindata await this.#chaindataProvider.updateChainTokens( @@ -281,7 +320,7 @@ export class MiniMetadataUpdater { ) // update miniMetadatas - const { miniMetadata: data, metadataVersion: version, ...extra } = metadata ?? {} + const { miniMetadata: data, metadataVersion: version, ...extra } = chainMeta ?? {} await balancesDb.miniMetadatas.put({ id: deriveMiniMetadataId({ source: mod.type, diff --git a/packages/balances/src/modules/SubstrateAssetsModule.ts b/packages/balances/src/modules/SubstrateAssetsModule.ts index 0e7c32f843..80d17e44cf 100644 --- a/packages/balances/src/modules/SubstrateAssetsModule.ts +++ b/packages/balances/src/modules/SubstrateAssetsModule.ts @@ -1,6 +1,6 @@ -import { Metadata, TypeRegistry } from "@polkadot/types" +import { TypeRegistry } from "@polkadot/types" import { ExtDef } from "@polkadot/types/extrinsic/signedExtensions/types" -import { assert, BN } from "@polkadot/util" +import { assert } from "@polkadot/util" import { defineMethod } from "@substrate/txwrapper-core" import { BalancesConfigTokenParams, @@ -10,42 +10,32 @@ import { Token, } from "@talismn/chaindata-provider" import { - $metadataV14, - filterMetadataPalletsAndItems, - getMetadataVersion, - PalletMV14, - StorageEntryMV14, + Binary, + compactMetadata, + decodeMetadata, + decodeScale, + encodeMetadata, + getDynamicBuilder, } from "@talismn/scale" -import * as $ from "@talismn/subshape-fork" -import { decodeAnyAddress } from "@talismn/util" import { DefaultBalanceModule, NewBalanceModule, NewTransferParamsType } from "../BalanceModule" import log from "../log" import { db as balancesDb } from "../TalismanBalancesDatabase" import { AddressesByToken, AmountWithLabel, Balances, NewBalanceType } from "../types" -import { - buildStorageDecoders, - createTypeRegistryCache, - findChainMeta, - GetOrCreateTypeRegistry, - getUniqueChainIds, - RpcStateQuery, - RpcStateQueryHelper, - StorageHelper, -} from "./util" +import { buildStorageCoders, getUniqueChainIds, RpcStateQuery, RpcStateQueryHelper } from "./util" type ModuleType = "substrate-assets" const moduleType: ModuleType = "substrate-assets" export type SubAssetsToken = Extract -const subAssetTokenId = (chainId: ChainId, assetId: string, tokenSymbol: string) => +export const subAssetTokenId = (chainId: ChainId, assetId: string, tokenSymbol: string) => `${chainId}-substrate-assets-${assetId}-${tokenSymbol}`.toLowerCase().replace(/ /g, "-") export type SubAssetsChainMeta = { isTestnet: boolean - miniMetadata: `0x${string}` | null - metadataVersion: number + miniMetadata?: string + metadataVersion?: number } export type SubAssetsModuleConfig = { @@ -88,108 +78,92 @@ export const SubAssetsModule: NewBalanceModule< const chainConnector = chainConnectors.substrate assert(chainConnector, "This module requires a substrate chain connector") - const { getOrCreateTypeRegistry } = createTypeRegistryCache() - return { ...DefaultBalanceModule(moduleType), async fetchSubstrateChainMeta(chainId, moduleConfig, metadataRpc) { const isTestnet = (await chaindataProvider.chainById(chainId))?.isTestnet || false - if (metadataRpc === undefined) return { isTestnet, miniMetadata: null, metadataVersion: 0 } - - const metadataVersion = getMetadataVersion(metadataRpc) - if ((moduleConfig?.tokens ?? []).length < 1) - return { isTestnet, miniMetadata: null, metadataVersion } - - if (metadataVersion !== 14) return { isTestnet, miniMetadata: null, metadataVersion } + if (metadataRpc === undefined) return { isTestnet } + if ((moduleConfig?.tokens ?? []).length < 1) return { isTestnet } - const metadata = $metadataV14.decode($.decodeHex(metadataRpc)) + const { metadataVersion, metadata, tag } = decodeMetadata(metadataRpc) + if (!metadata) return { isTestnet } - const isAssetsPallet = (pallet: PalletMV14) => pallet.name === "Assets" - const isAccountItem = (item: StorageEntryMV14) => item.name === "Account" - const isAssetItem = (item: StorageEntryMV14) => item.name === "Asset" - const isMetadataItem = (item: StorageEntryMV14) => item.name === "Metadata" + compactMetadata(metadata, [{ pallet: "Assets", items: ["Account", "Asset", "Metadata"] }]) - // TODO: Handle metadata v15 - filterMetadataPalletsAndItems(metadata, [ - { pallet: isAssetsPallet, items: [isAccountItem, isAssetItem, isMetadataItem] }, - ]) - metadata.extrinsic.signedExtensions = [] + const miniMetadata = encodeMetadata(tag === "v15" ? { tag, metadata } : { tag, metadata }) - const miniMetadata = $.encodeHexPrefixed($metadataV14.encode(metadata)) as `0x${string}` - - return { - isTestnet, - miniMetadata, - metadataVersion, - } + return { isTestnet, miniMetadata, metadataVersion } }, async fetchSubstrateChainTokens(chainId, chainMeta, moduleConfig) { - const { isTestnet, miniMetadata: metadataRpc, metadataVersion } = chainMeta - if ((moduleConfig?.tokens ?? []).length < 1) return {} - const registry = new TypeRegistry() - if (metadataRpc !== null && metadataVersion >= 14) - registry.setMetadata(new Metadata(registry, metadataRpc)) + const { isTestnet, miniMetadata, metadataVersion } = chainMeta + if (miniMetadata === undefined || metadataVersion === undefined) return {} + if (metadataVersion < 14) return {} + + const { metadata } = decodeMetadata(miniMetadata) + if (metadata === undefined) return {} + + const scaleBuilder = getDynamicBuilder(metadata) const tokens: Record = {} for (const tokenConfig of moduleConfig?.tokens ?? []) { try { - const assetId = new BN(tokenConfig.assetId) - - const assetQuery = new StorageHelper(registry, "assets", "asset", assetId) - const metadataQuery = new StorageHelper(registry, "assets", "metadata", assetId) - - const [ - // e.g. - // Option<{ - // owner: HKKT5DjFaUE339m7ZWS2yutjecbUpBcDQZHw2EF7SFqSFJH - // issuer: HKKT5DjFaUE339m7ZWS2yutjecbUpBcDQZHw2EF7SFqSFJH - // admin: HKKT5DjFaUE339m7ZWS2yutjecbUpBcDQZHw2EF7SFqSFJH - // freezer: HKKT5DjFaUE339m7ZWS2yutjecbUpBcDQZHw2EF7SFqSFJH - // supply: 99,996,117,733,044,042 - // deposit: 1,000,000,000,000 - // minBalance: 100,000 - // isSufficient: true - // accounts: 6,032 - // sufficients: 1,542 - // approvals: 1 - // status: Live - // }> - assetsAsset, - - // e.g. - // { - // deposit: 6,693,333,000 - // name: RMRK.app - // symbol: RMRK - // decimals: 10 - // isFrozen: false - // } - assetsMetadata, - ] = await Promise.all([ + const assetId = + typeof tokenConfig.assetId === "number" + ? tokenConfig.assetId.toString() + : tokenConfig.assetId + + const assetCoder = scaleBuilder.buildStorage("Assets", "Asset") + const metadataCoder = scaleBuilder.buildStorage("Assets", "Metadata") + + const assetStateKey = + tryEncode(assetCoder, BigInt(assetId)) ?? tryEncode(assetCoder, assetId) + const metadataStateKey = + tryEncode(metadataCoder, BigInt(assetId)) ?? tryEncode(metadataCoder, assetId) + + if (assetStateKey === null || metadataStateKey === null) + throw new Error(`Failed to encode stateKey for asset ${assetId} on chain ${chainId}`) + + type AssetResult = { + accounts?: number + admin?: string + approvals?: number + deposit?: bigint + freezer?: string + is_sufficient?: boolean + issuer?: string + min_balance?: bigint + owner?: string + status?: unknown + sufficients?: number + supply?: bigint + } + type MetadataResult = { + decimals?: number + deposit?: bigint + is_frozen?: boolean + name?: Binary + symbol?: Binary + } + + const [assetsAsset, assetsMetadata] = await Promise.all([ chainConnector - .send(chainId, "state_getStorage", [assetQuery.stateKey]) - .then((result) => assetQuery.decode(result)), + .send(chainId, "state_getStorage", [assetStateKey]) + .then((result) => (assetCoder.dec(result) as AssetResult | undefined) ?? null), chainConnector - .send(chainId, "state_getStorage", [metadataQuery.stateKey]) - .then((result) => metadataQuery.decode(result)), + .send(chainId, "state_getStorage", [metadataStateKey]) + .then((result) => (metadataCoder.dec(result) as MetadataResult | undefined) ?? null), ]) - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const unsafeAssetsMetadata = assetsMetadata as any | undefined - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const unsafeAssetsAsset = assetsAsset as any | undefined + const existentialDeposit = assetsAsset?.min_balance?.toString?.() ?? "0" + const symbol = assetsMetadata?.symbol?.asText?.() ?? "Unit" + const decimals = assetsMetadata?.decimals ?? 0 + const isFrozen = assetsMetadata?.is_frozen ?? false - const existentialDeposit = - unsafeAssetsAsset?.value?.minBalance?.toBigInt?.()?.toString?.() ?? "0" - const symbol = unsafeAssetsMetadata?.symbol?.toHuman?.() ?? "Unit" - const decimals = unsafeAssetsMetadata?.decimals?.toNumber?.() ?? 0 - const isFrozen = unsafeAssetsMetadata?.isFrozen?.toHuman?.() ?? false - - const id = subAssetTokenId(chainId, assetId.toString(10), symbol) + const id = subAssetTokenId(chainId, assetId, symbol) const token: SubAssetsToken = { id, type: "substrate-assets", @@ -199,7 +173,7 @@ export const SubAssetsModule: NewBalanceModule< decimals, logo: tokenConfig?.logo || githubTokenLogoUrl(id), existentialDeposit, - assetId: assetId.toString(10), + assetId, isFrozen, chain: { id: chainId }, } @@ -224,20 +198,12 @@ export const SubAssetsModule: NewBalanceModule< // TODO: Don't create empty subscriptions async subscribeBalances({ addressesByToken }, callback) { - const queries = await buildQueries( - chaindataProvider, - getOrCreateTypeRegistry, - addressesByToken - ) + const queries = await buildQueries(chaindataProvider, addressesByToken) const unsubscribe = await new RpcStateQueryHelper(chainConnector, queries).subscribe( (error, result) => { - if (error) callback(error) - if (result) { - const balances = result.filter( - (balance): balance is SubAssetsBalance => balance !== null - ) - if (balances.length > 0) callback(null, new Balances(balances)) - } + if (error) return callback(error) + const balances = result?.filter((b): b is SubAssetsBalance => b !== null) ?? [] + if (balances.length > 0) callback(null, new Balances(balances)) } ) @@ -247,13 +213,9 @@ export const SubAssetsModule: NewBalanceModule< async fetchBalances(addressesByToken) { assert(chainConnectors.substrate, "This module requires a substrate chain connector") - const queries = await buildQueries( - chaindataProvider, - getOrCreateTypeRegistry, - addressesByToken - ) + const queries = await buildQueries(chaindataProvider, addressesByToken) const result = await new RpcStateQueryHelper(chainConnectors.substrate, queries).fetch() - const balances = result.filter((balance): balance is SubAssetsBalance => balance !== null) + const balances = result?.filter((b): b is SubAssetsBalance => b !== null) ?? [] return new Balances(balances) }, @@ -322,7 +284,6 @@ export const SubAssetsModule: NewBalanceModule< async function buildQueries( chaindataProvider: ChaindataProvider, - getOrCreateTypeRegistry: GetOrCreateTypeRegistry, addressesByToken: AddressesByToken ): Promise>> { const allChains = await chaindataProvider.chainsById() @@ -336,11 +297,12 @@ async function buildQueries( const uniqueChainIds = getUniqueChainIds(addressesByToken, tokens) const chains = Object.fromEntries(uniqueChainIds.map((chainId) => [chainId, allChains[chainId]])) - const chainStorageDecoders = buildStorageDecoders({ + const chainStorageCoders = buildStorageCoders({ + chainIds: uniqueChainIds, chains, miniMetadatas, moduleType: "substrate-assets", - decoders: { storageDecoder: ["assets", "account"] }, + coders: { storage: ["Assets", "Account"] }, }) return Object.entries(addressesByToken).flatMap(([tokenId, addresses]) => { @@ -349,63 +311,57 @@ async function buildQueries( log.warn(`Token ${tokenId} not found`) return [] } - if (token.type !== "substrate-assets") { log.debug(`This module doesn't handle tokens of type ${token.type}`) return [] } - const chainId = token.chain?.id if (!chainId) { log.warn(`Token ${tokenId} has no chain`) return [] } - const chain = chains[chainId] if (!chain) { log.warn(`Chain ${chainId} for token ${tokenId} not found`) return [] } - const [chainMeta] = findChainMeta( - miniMetadatas, - "substrate-assets", - chain - ) - const registry = - chainMeta?.miniMetadata !== undefined && - chainMeta?.miniMetadata !== null && - chainMeta?.metadataVersion >= 14 - ? getOrCreateTypeRegistry(chainId, chainMeta.miniMetadata) - : new TypeRegistry() - return addresses.flatMap((address): RpcStateQuery | [] => { - const storageHelper = new StorageHelper( - registry, - "assets", - "account", - token.assetId, - decodeAnyAddress(address) - ) - const storageDecoder = chainStorageDecoders.get(chainId)?.storageDecoder - const stateKey = storageHelper.stateKey - if (!stateKey) return [] + const scaleCoder = chainStorageCoders.get(chainId)?.storage + const stateKey = + tryEncode(scaleCoder, BigInt(token.assetId), address) ?? + tryEncode(scaleCoder, token.assetId, address) + if (!stateKey) { + log.warn( + `Invalid assetId / address in ${chainId} storage query ${token.assetId} / ${address}` + ) + return [] + } + const decodeResult = (change: string | null) => { - // e.g. - // Option<{ - // balance: 2,000,000,000 - // isFrozen: false - // reason: Sufficient - // extra: null - // }> - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const balance: any = ((storageDecoder && change !== null - ? storageDecoder.decode($.decodeHex(change)) - : // eslint-disable-next-line @typescript-eslint/no-explicit-any - null) as any) ?? { balance: 0n, status: "Liquid" } - - const isFrozen = balance?.status === "Frozen" - const amount = (balance?.balance ?? 0n).toString() + /** NOTE: This type is only a hint for typescript, the chain can actually return whatever it wants to */ + type DecodedType = { + balance?: bigint + is_frozen?: boolean + reason?: { type?: "Sufficient" } + status?: { type?: "Liquid" } | { type?: "Frozen" } + extra?: undefined + } + + const decoded = decodeScale( + scaleCoder, + change, + `Failed to decode substrate-assets balance on chain ${chainId}` + ) ?? { + balance: 0n, + is_frozen: false, + reason: { type: "Sufficient" }, + status: { type: "Liquid" }, + extra: undefined, + } + + const isFrozen = decoded?.status?.type === "Frozen" + const amount = (decoded?.balance ?? 0n).toString() // due to the following balance calculations, which are made in the `Balance` type: // @@ -442,3 +398,17 @@ async function buildQueries( }) }) } + +type ScaleStorageCoder = ReturnType["buildStorage"]> + +// NOTE: Different chains need different formats for assetId when encoding the stateKey +// E.g. Polkadot Asset Hub needs it to be a string, Astar needs it to be a bigint +// +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const tryEncode = (scaleCoder: ScaleStorageCoder | undefined, ...args: any[]) => { + try { + return scaleCoder?.enc?.(...args) + } catch { + return null + } +} diff --git a/packages/balances/src/modules/SubstrateEquilibriumModule.ts b/packages/balances/src/modules/SubstrateEquilibriumModule.ts index 8b7c4358c6..1c38733afc 100644 --- a/packages/balances/src/modules/SubstrateEquilibriumModule.ts +++ b/packages/balances/src/modules/SubstrateEquilibriumModule.ts @@ -1,4 +1,4 @@ -import { Metadata, TypeRegistry } from "@polkadot/types" +import { TypeRegistry } from "@polkadot/types" import { AbstractInt } from "@polkadot/types-codec" import { ExtDef } from "@polkadot/types/extrinsic/signedExtensions/types" import { assert } from "@polkadot/util" @@ -11,42 +11,33 @@ import { Token, } from "@talismn/chaindata-provider" import { - $metadataV14, - filterMetadataPalletsAndItems, - getMetadataVersion, - PalletMV14, - StorageEntryMV14, + compactMetadata, + decodeMetadata, + decodeScale, + encodeMetadata, + encodeStateKey, + getDynamicBuilder, } from "@talismn/scale" -import * as $ from "@talismn/subshape-fork" -import { decodeAnyAddress, isBigInt } from "@talismn/util" +import { isBigInt } from "@talismn/util" import { DefaultBalanceModule, NewBalanceModule, NewTransferParamsType } from "../BalanceModule" import log from "../log" import { db as balancesDb } from "../TalismanBalancesDatabase" import { AddressesByToken, Balances, NewBalanceType } from "../types" -import { - buildStorageDecoders, - createTypeRegistryCache, - findChainMeta, - GetOrCreateTypeRegistry, - getUniqueChainIds, - RpcStateQuery, - RpcStateQueryHelper, - StorageHelper, -} from "./util" +import { buildStorageCoders, getUniqueChainIds, RpcStateQuery, RpcStateQueryHelper } from "./util" type ModuleType = "substrate-equilibrium" const moduleType: ModuleType = "substrate-equilibrium" export type SubEquilibriumToken = Extract -const subEquilibriumTokenId = (chainId: ChainId, tokenSymbol: string) => +export const subEquilibriumTokenId = (chainId: ChainId, tokenSymbol: string) => `${chainId}-substrate-equilibrium-${tokenSymbol}`.toLowerCase().replace(/ /g, "-") export type SubEquilibriumChainMeta = { isTestnet: boolean - miniMetadata: `0x${string}` | null - metadataVersion: number + miniMetadata?: string + metadataVersion?: number } export type SubEquilibriumModuleConfig = { @@ -90,68 +81,68 @@ export const SubEquilibriumModule: NewBalanceModule< const chainConnector = chainConnectors.substrate assert(chainConnector, "This module requires a substrate chain connector") - const { getOrCreateTypeRegistry } = createTypeRegistryCache() - return { ...DefaultBalanceModule(moduleType), async fetchSubstrateChainMeta(chainId, moduleConfig, metadataRpc) { const isTestnet = (await chaindataProvider.chainById(chainId))?.isTestnet || false - if (metadataRpc === undefined) return { isTestnet, miniMetadata: null, metadataVersion: 0 } - - const metadataVersion = getMetadataVersion(metadataRpc) - // default to disabled - if (moduleConfig?.disable !== false) return { isTestnet, miniMetadata: null, metadataVersion } - - if (metadataVersion !== 14) return { isTestnet, miniMetadata: null, metadataVersion } + if (metadataRpc === undefined) return { isTestnet } + if (moduleConfig?.disable !== false) return { isTestnet } // default to disabled - const metadata = $metadataV14.decode($.decodeHex(metadataRpc)) + const { metadataVersion, metadata, tag } = decodeMetadata(metadataRpc) + if (!metadata) return { isTestnet } - const isEqAssetsPallet = (pallet: PalletMV14) => pallet.name === "EqAssets" - const isAssetsItem = (item: StorageEntryMV14) => item.name === "Assets" - - const isSystemPallet = (pallet: PalletMV14) => pallet.name === "System" - const isAccountItem = (item: StorageEntryMV14) => item.name === "Account" - - // TODO: Handle metadata v15 - filterMetadataPalletsAndItems(metadata, [ - { pallet: isEqAssetsPallet, items: [isAssetsItem] }, - { pallet: isSystemPallet, items: [isAccountItem] }, + compactMetadata(metadata, [ + { pallet: "EqAssets", items: ["Assets"] }, + { pallet: "System", items: ["Account"] }, ]) - metadata.extrinsic.signedExtensions = [] - const miniMetadata = $.encodeHexPrefixed($metadataV14.encode(metadata)) as `0x${string}` + const miniMetadata = encodeMetadata(tag === "v15" ? { tag, metadata } : { tag, metadata }) - return { - isTestnet, - miniMetadata, - metadataVersion, - } + return { isTestnet, miniMetadata, metadataVersion } }, async fetchSubstrateChainTokens(chainId, chainMeta, moduleConfig) { // default to disabled if (moduleConfig?.disable !== false) return {} - const { isTestnet, miniMetadata: metadataRpc, metadataVersion } = chainMeta + const { isTestnet, miniMetadata, metadataVersion } = chainMeta + if (miniMetadata === undefined || metadataVersion === undefined) return {} + if (metadataVersion < 14) return {} - const registry = new TypeRegistry() - if (metadataRpc !== null && metadataVersion >= 14) - registry.setMetadata(new Metadata(registry, metadataRpc)) - - const tokens: Record = {} + const { metadata } = decodeMetadata(miniMetadata) + if (metadata === undefined) return {} try { - const assetsQuery = new StorageHelper(registry, "eqAssets", "assets") + const scaleBuilder = getDynamicBuilder(metadata) + const assetsCoder = scaleBuilder.buildStorage("EqAssets", "Assets") + const stateKey = assetsCoder.enc() + + /** NOTE: Just a guideline, the RPC can return whatever it wants */ + type AssetsResult = Array< + Partial<{ + id: bigint + lot: bigint + price_step: bigint + maker_fee: number + taker_fee: number + asset_xcm_data: unknown + debt_weight: number + lending_debt_weight: number + buyout_priority: bigint + asset_type: unknown + is_dex_enabled: boolean + collateral_discount: number + }> + > const assetsResult = await chainConnector - .send(chainId, "state_getStorage", [assetsQuery.stateKey]) - .then((result) => assetsQuery.decode(result)) + .send(chainId, "state_getStorage", [stateKey]) + .then((result) => (assetsCoder.dec(result) as AssetsResult | undefined) ?? null) - // eslint-disable-next-line @typescript-eslint/no-explicit-any - ;[...((assetsResult as any)?.value ?? [])].map((asset: any) => { - if (!asset) return - if (!asset?.id) return + const tokens = (Array.isArray(assetsResult) ? assetsResult : []).flatMap((asset) => { + if (!asset) return [] + if (!asset?.id) return [] const assetId = asset.id.toString(10) const symbol = tokenSymbolFromU64Id(asset.id) @@ -181,30 +172,28 @@ export const SubEquilibriumModule: NewBalanceModule< if (tokenConfig?.dcentName) token.dcentName = tokenConfig?.dcentName if (tokenConfig?.mirrorOf) token.mirrorOf = tokenConfig?.mirrorOf - tokens[token.id] = token + return [[token.id, token]] }) + + return Object.fromEntries(tokens) } catch (error) { log.error( `Failed to build substrate-equilibrium tokens on ${chainId}`, (error as Error)?.message ?? error ) + return {} } - - return tokens }, // TODO: Don't create empty subscriptions async subscribeBalances({ addressesByToken }, callback) { - const queries = await buildQueries( - chaindataProvider, - getOrCreateTypeRegistry, - addressesByToken - ) + const queries = await buildQueries(chaindataProvider, addressesByToken) const unsubscribe = await new RpcStateQueryHelper(chainConnector, queries).subscribe( - (error, result) => - error - ? callback(error) - : callback(null, new Balances(result?.flatMap((balances) => balances) ?? [])) + (error, result) => { + if (error) return callback(error) + const balances = result?.flatMap((balances) => balances) ?? [] + if (balances.length > 0) callback(null, new Balances(balances)) + } ) return unsubscribe @@ -213,14 +202,10 @@ export const SubEquilibriumModule: NewBalanceModule< async fetchBalances(addressesByToken) { assert(chainConnectors.substrate, "This module requires a substrate chain connector") - const queries = await buildQueries( - chaindataProvider, - getOrCreateTypeRegistry, - addressesByToken - ) + const queries = await buildQueries(chaindataProvider, addressesByToken) const result = await new RpcStateQueryHelper(chainConnectors.substrate, queries).fetch() - - return new Balances(result.flatMap((balances) => balances) ?? []) + const balances = result?.flatMap((balances) => balances) ?? [] + return new Balances(balances) }, async transferToken({ @@ -293,7 +278,6 @@ export const SubEquilibriumModule: NewBalanceModule< async function buildQueries( chaindataProvider: ChaindataProvider, - getOrCreateTypeRegistry: GetOrCreateTypeRegistry, addressesByToken: AddressesByToken ): Promise>> { const allChains = await chaindataProvider.chainsById() @@ -307,11 +291,12 @@ async function buildQueries( const uniqueChainIds = getUniqueChainIds(addressesByToken, tokens) const chains = Object.fromEntries(uniqueChainIds.map((chainId) => [chainId, allChains[chainId]])) - const chainStorageDecoders = buildStorageDecoders({ + const chainStorageCoders = buildStorageCoders({ + chainIds: uniqueChainIds, chains, miniMetadatas, moduleType: "substrate-equilibrium", - decoders: { storageDecoder: ["system", "account"] }, + coders: { storage: ["System", "Account"] }, }) // equilibrium returns all chain tokens for each address in the one query @@ -342,75 +327,45 @@ async function buildQueries( return [] } - const [chainMeta] = findChainMeta( - miniMetadatas, - "substrate-equilibrium", - chain - ) - const registry = - chainMeta?.miniMetadata !== undefined && - chainMeta?.miniMetadata !== null && - chainMeta?.metadataVersion >= 14 - ? getOrCreateTypeRegistry(chainId, chainMeta.miniMetadata) - : new TypeRegistry() - return Array.from(addresses).flatMap((address): RpcStateQuery | [] => { - const storageHelper = new StorageHelper( - registry, - "system", - "account", - decodeAnyAddress(address) + const scaleCoder = chainStorageCoders.get(chainId)?.storage + const stateKey = encodeStateKey( + scaleCoder, + `Invalid address in ${chainId} storage query ${address}`, + address ) - const stateKey = storageHelper.stateKey - const storageDecoder = chainStorageDecoders.get(chainId)?.storageDecoder if (!stateKey) return [] + const decodeResult = (change: string | null) => { - // e.g. - // { - // nonce: 5 - // consumers: 0 - // providers: 2 - // sufficients: 0 - // data: { - // V0: { - // lock: 0 - // balance: [ - // [ - // 25,969 - // { - // Positive: 499,912,656,271 - // } - // ] - // [ - // 6,582,132 - // { - // Positive: 1,973,490,154 - // } - // ] - // [ - // 6,648,164 - // { - // Positive: 200,000,000 - // } - // ] - // [ - // 435,694,104,436 - // { - // Positive: 828,313,918 - // } - // ] - // ] - // } - // } - // } - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const balances: any = - storageDecoder && change !== null ? storageDecoder.decode($.decodeHex(change)) : null + /** NOTE: This type is only a hint for typescript, the chain can actually return whatever it wants to */ + type DecodedType = { + consumers?: number + nonce?: number + providers?: number + sufficients?: number + data?: { + type?: string + value?: { + balance?: Array< + [ + bigint, + { type?: "Positive"; value?: bigint } | { type?: "Negative"; value?: bigint } + ] + > + lock?: bigint + } + } + } + + const decoded = decodeScale( + scaleCoder, + change, + `Failed to decode eqBalances on chain ${chainId}` + ) const tokenBalances = Object.fromEntries( - (balances?.data?.balance ?? []) - // eslint-disable-next-line @typescript-eslint/no-explicit-any - .map((balance: any) => ({ + (decoded?.data?.value?.balance ?? []) + .map((balance) => ({ id: (balance?.[0] ?? 0n)?.toString?.(), free: balance?.[1]?.type === "Positive" @@ -419,11 +374,16 @@ async function buildQueries( ? ((balance?.[1]?.value ?? 0n) * -1n).toString() : "0", })) - .map(({ id, free }: { id?: string; free?: string }) => [id, free]) - .filter( - ([id, free]: [string | undefined, string | undefined]) => - id !== undefined && free !== undefined + .map( + ({ + id, + free, + }: { + id?: string + free?: string + }): [string | undefined, string | undefined] => [id, free] ) + .filter(([id, free]) => id !== undefined && free !== undefined) ) const result = Array.from(tokensByAddress.get(address) ?? []) diff --git a/packages/balances/src/modules/SubstrateNativeModule/QueryCache.ts b/packages/balances/src/modules/SubstrateNativeModule/QueryCache.ts index b1b335ec6b..fbb8329dc1 100644 --- a/packages/balances/src/modules/SubstrateNativeModule/QueryCache.ts +++ b/packages/balances/src/modules/SubstrateNativeModule/QueryCache.ts @@ -1,36 +1,34 @@ -import { TypeRegistry, createType, i128 } from "@polkadot/types" -import { Chain, ChainId, ChaindataProvider, Token } from "@talismn/chaindata-provider" -import * as $ from "@talismn/subshape-fork" -import { blake2Concat, decodeAnyAddress, firstThenDebounce, isEthereumAddress } from "@talismn/util" +import { Chain, ChaindataProvider, ChainId, Token } from "@talismn/chaindata-provider" +import { Binary, decodeScale, encodeStateKey } from "@talismn/scale" +import { blake2Concat, decodeAnyAddress, firstThenDebounce } from "@talismn/util" import { liveQuery } from "dexie" import isEqual from "lodash/isEqual" import { - Observable, - Subscription, combineLatestWith, distinctUntilChanged, filter, firstValueFrom, from, map, + Observable, pipe, shareReplay, + Subscription, } from "rxjs" +import { Struct, u32, u128 } from "scale-ts" +import { SubNativeBalance, SubNativeToken } from "." import log from "../../log" import { db as balancesDb } from "../../TalismanBalancesDatabase" -import { AddressesByToken, AmountWithLabel, MiniMetadata, getValueId } from "../../types" +import { AddressesByToken, AmountWithLabel, getValueId, MiniMetadata } from "../../types" import { - GetOrCreateTypeRegistry, - RpcStateQuery, - StorageDecoders, - StorageHelper, - buildStorageDecoders, + buildStorageCoders, findChainMeta, getUniqueChainIds, + RpcStateQuery, + StorageCoders, } from "../util" -import { getLockedType } from "../util/substrate-native" -import { SubNativeBalance, SubNativeToken } from "." +import { getLockedType } from "./util" type QueryKey = string @@ -74,23 +72,23 @@ type QueryCacheResults = { // Theory: new chains will be at least on metadata v14, and so we won't need to hardcode their AccountInfo type. // But for chains we want to support which aren't on metadata v14, hardcode them here: // If the chain upgrades to metadata v14, this override will be ignored :) -// -// TODO: Move the AccountInfoFallback configs for each chain into the ChainMeta section of chaindata -const RegularAccountInfoFallback = JSON.stringify({ - nonce: "u32", - consumers: "u32", - providers: "u32", - sufficients: "u32", - data: { free: "u128", reserved: "u128", miscFrozen: "u128", feeFrozen: "u128" }, +const RegularAccountInfoFallback = Struct({ + nonce: u32, + consumers: u32, + providers: u32, + sufficients: u32, + data: Struct({ free: u128, reserved: u128, miscFrozen: u128, feeFrozen: u128 }), }) -const NoSufficientsAccountInfoFallback = JSON.stringify({ - nonce: "u32", - consumers: "u32", - providers: "u32", - data: { free: "u128", reserved: "u128", miscFrozen: "u128", feeFrozen: "u128" }, +const NoSufficientsAccountInfoFallback = Struct({ + nonce: u32, + consumers: u32, + providers: u32, + data: Struct({ free: u128, reserved: u128, miscFrozen: u128, feeFrozen: u128 }), }) - -const AccountInfoOverrides: { [key: ChainId]: string } = { +const AccountInfoOverrides: Record< + string, + typeof RegularAccountInfoFallback | typeof NoSufficientsAccountInfoFallback | undefined +> = { // crown-sterlin is not yet on metadata v14 "crown-sterling": NoSufficientsAccountInfoFallback, @@ -109,14 +107,8 @@ let commonMetadataObservable: Observable> | null = nul export class QueryCache { private balanceQueryCache = new Map[]>() private metadataSub: Subscription - private getOrCreateTypeRegistry: GetOrCreateTypeRegistry - - constructor( - private chaindataProvider: ChaindataProvider, - getOrCreateTypeRegistry: GetOrCreateTypeRegistry - ) { - this.getOrCreateTypeRegistry = getOrCreateTypeRegistry + constructor(private chaindataProvider: ChaindataProvider) { if (!commonMetadataObservable) { commonMetadataObservable = from( liveQuery(() => @@ -196,13 +188,24 @@ export class QueryCache { // build queries for token/address pairs which have not been queried before const miniMetadatas = await firstValueFrom(commonMetadataObservable) const uniqueChainIds = getUniqueChainIds(queryResults.newAddressesByToken, tokens) - const chainStorageDecoders = this.getBaseStorageDecoders(uniqueChainIds, chains, miniMetadatas) + const chainStorageCoders = buildStorageCoders({ + chainIds: uniqueChainIds, + chains, + miniMetadatas, + moduleType: "substrate-native", + coders: { + base: ["System", "Account"], + reserves: ["Balances", "Reserves"], + holds: ["Balances", "Holds"], + locks: ["Balances", "Locks"], + freezes: ["Balances", "Freezes"], + }, + }) const queries = await buildQueries( chains, tokens, - chainStorageDecoders, + chainStorageCoders, miniMetadatas, - this.getOrCreateTypeRegistry, queryResults.newAddressesByToken ) // now update the cache @@ -211,58 +214,38 @@ export class QueryCache { }) return queryResults.existing.concat(Object.values(queries).flat()) } - - private getBaseStorageDecoders( - chainIds: string[], - allChains: Record, - miniMetadatas: Map - ) { - const chains = Object.fromEntries( - chainIds - .filter((chainId) => allChains[chainId]) - .map((chainId) => [chainId, allChains[chainId]]) - ) - return buildStorageDecoders({ - chains, - miniMetadatas, - moduleType: "substrate-native", - decoders: { - baseDecoder: ["system", "account"], - reservesDecoder: ["balances", "reserves"], - holdsDecoder: ["balances", "holds"], - locksDecoder: ["balances", "locks"], - freezesDecoder: ["balances", "freezes"], - }, - }) - } } async function buildQueries( chains: Record, tokens: Record, - chainStorageDecoders: StorageDecoders, + chainStorageCoders: StorageCoders<{ + base: ["System", "Account"] + reserves: ["Balances", "Reserves"] + holds: ["Balances", "Holds"] + locks: ["Balances", "Locks"] + freezes: ["Balances", "Freezes"] + }>, miniMetadatas: Map, - getOrCreateTypeRegistry: GetOrCreateTypeRegistry, addressesByToken: AddressesByToken ): Promise>>> { - return Object.entries(addressesByToken).reduce((outerResult, [tokenId, addresses]) => { + return Object.entries(addressesByToken).reduce< + Record>> + >((outerResult, [tokenId, addresses]) => { const token = tokens[tokenId] if (!token) { log.warn(`Token ${tokenId} not found`) return outerResult } - if (token.type !== "substrate-native") { log.debug(`This module doesn't handle tokens of type ${token.type}`) return outerResult } - const chainId = token.chain?.id if (!chainId) { log.warn(`Token ${tokenId} has no chain`) return outerResult } - const chain = chains[chainId] if (!chain) { log.warn(`Chain ${chainId} for token ${tokenId} not found`) @@ -270,17 +253,11 @@ async function buildQueries( } const [chainMeta] = findChainMeta(miniMetadatas, "substrate-native", chain) - const hasMetadataV14 = - chainMeta?.miniMetadata !== undefined && - chainMeta?.miniMetadata !== null && - chainMeta?.metadataVersion >= 14 - const typeRegistry = hasMetadataV14 - ? getOrCreateTypeRegistry(chainId, chainMeta.miniMetadata ?? undefined) - : new TypeRegistry() + const { useLegacyTransferableCalculation } = chainMeta ?? {} addresses.flat().forEach((address) => { const queryKey = `${tokenId}-${address}` - // We share the balanceJson between the base and the lock query for this address + // We share this balanceJson between the base and the lock query for this address const balanceJson: SubNativeBalance = { source: "substrate-native", status: "live", @@ -290,20 +267,13 @@ async function buildQueries( tokenId, values: [], } - if (chainMeta?.useLegacyTransferableCalculation) - balanceJson.useLegacyTransferableCalculation = true + if (useLegacyTransferableCalculation) balanceJson.useLegacyTransferableCalculation = true let locksQueryLocks: Array> = [] let freezesQueryLocks: Array> = [] const baseQuery: RpcStateQuery | undefined = (() => { - const storageHelper = new StorageHelper( - typeRegistry, - "system", - "account", - decodeAnyAddress(address) - ) - const storageDecoder = chainStorageDecoders.get(chainId)?.baseDecoder + // For chains which are using metadata < v14 const getFallbackStateKey = () => { const addressBytes = decodeAnyAddress(address) const addressHash = blake2Concat(addressBytes).replace(/^0x/, "") @@ -313,33 +283,25 @@ async function buildQueries( return `0x${moduleStorageHash}${addressHash}` } - /** - * NOTE: For many MetadataV14 chains, it is not valid to encode an ethereum address into this System.Account state call. - * However, because we have always made that state call in the past, existing users will have the result (a balance of `0`) - * cached in their BalancesDB. - * - * So, until we refactor the storage of this module in a way which nukes the existing cached balances, we'll need to continue - * making these invalid state calls to keep those balances from showing as `cached` or `stale`. - * - * Current logic: - * - * stateKey: string = hasMetadataV14 && storageHelper.stateKey ? storageHelper.stateKey : getFallbackStateKey() - * - * Future (ideal) logic: - * - * stateKey: string | undefined = hasMetadataV14 ? storageHelper.stateKey : getFallbackStateKey() - */ - const stateKey = - hasMetadataV14 && storageHelper.stateKey ? storageHelper.stateKey : getFallbackStateKey() + const scaleCoder = chainStorageCoders.get(chainId)?.base + // NOTE: Only use fallback key when `scaleCoder` is not defined + // i.e. when chain doesn't have metadata v14/v15 + const stateKey = scaleCoder + ? encodeStateKey( + scaleCoder, + `Invalid address in ${chainId} base query ${address}`, + address + ) + : getFallbackStateKey() + if (!stateKey) return const decodeResult = (change: string | null) => { // BEGIN: Handle chains which use metadata < v14 - // eslint-disable-next-line @typescript-eslint/no-explicit-any - let oldChainBalance: any = undefined - if (!hasMetadataV14) { - const accountInfoTypeDef = AccountInfoOverrides[chainId] - if (accountInfoTypeDef === undefined) { - // chain metadata version is < 14 and we also don't have an override hardcoded in + let oldChainBalance = null + if (!scaleCoder) { + const scaleAccountInfo = AccountInfoOverrides[chainId] + if (scaleAccountInfo === undefined) { + // chain metadata version is < 15 and we also don't have an override hardcoded in // the best way to handle this case: log a warning and return an empty balance log.debug( `Token ${tokenId} on chain ${chainId} has no balance type for decoding. Defaulting to a balance of 0 (zero).` @@ -349,7 +311,7 @@ async function buildQueries( try { // eslint-disable-next-line no-var - oldChainBalance = createType(typeRegistry, accountInfoTypeDef, change) + oldChainBalance = change === null ? null : scaleAccountInfo.dec(change) } catch (error) { log.warn( `Failed to create pre-metadataV14 balance type for token ${tokenId} on chain ${chainId}: ${error?.toString()}` @@ -359,32 +321,35 @@ async function buildQueries( } // END: Handle chains which use metadata < v14 - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const decoded: any = - hasMetadataV14 && storageDecoder - ? change === null - ? null - : storageDecoder.decode($.decodeHex(change)) - : oldChainBalance - - const bigIntOrCodecToBigInt = (value: bigint | i128): bigint => - typeof value === "bigint" ? value : value?.toBigInt?.() - - let free = bigIntOrCodecToBigInt(decoded?.data?.free) ?? 0n - let reserved = bigIntOrCodecToBigInt(decoded?.data?.reserved) ?? 0n - let miscFrozen = - (bigIntOrCodecToBigInt(decoded?.data?.miscFrozen) ?? 0n) + - // some chains don't split their `frozen` amount into `feeFrozen` and `miscFrozen`. - // for those chains, we'll use the `frozen` amount as `miscFrozen`. - (bigIntOrCodecToBigInt(decoded?.data?.frozen) ?? 0n) - - let feeFrozen = bigIntOrCodecToBigInt(decoded?.data?.feeFrozen) ?? 0n - - // we use the evm-native module to fetch native token balances for ethereum addresses on ethereum networks - // but on moonbeam, moonriver and other chains which use ethereum addresses instead of substrate addresses, - // we use both this module and the evm-native module - if (isEthereumAddress(address) && chain.account !== "secp256k1") - free = reserved = miscFrozen = feeFrozen = 0n + /** NOTE: This type is only a hint for typescript, the chain can actually return whatever it wants to */ + type DecodedType = { + data?: { + flags?: bigint + free?: bigint + frozen?: bigint + reserved?: bigint + + // deprecated fields (they only show up on old chains) + feeFrozen?: bigint + miscFrozen?: bigint + } + } + const decoded = + decodeScale( + scaleCoder, + change, + `Failed to decode balance on chain ${chainId}` + ) ?? oldChainBalance + + const free = (decoded?.data?.free ?? 0n).toString() + const reserved = (decoded?.data?.reserved ?? 0n).toString() + const miscLock = ( + (decoded?.data?.miscFrozen ?? 0n) + + // new chains don't split their `frozen` amount into `feeFrozen` and `miscFrozen`. + // for these chains, we'll use the `frozen` amount as `miscFrozen`. + ((decoded?.data as DecodedType["data"])?.frozen ?? 0n) + ).toString() + const feesLock = (decoded?.data?.feeFrozen ?? 0n).toString() // even if these values are 0, we still need to add them to the balanceJson.values array // so that the balance pool can handle newly zeroed balances @@ -394,8 +359,8 @@ async function buildQueries( const newValues: AmountWithLabel[] = [ { type: "free", label: "free", amount: free.toString() }, { type: "reserved", label: "reserved", amount: reserved.toString() }, - { type: "locked", label: "misc", amount: miscFrozen.toString() }, - { type: "locked", label: "fees", amount: feeFrozen.toString() }, + { type: "locked", label: "misc", amount: miscLock.toString() }, + { type: "locked", label: "fees", amount: feesLock.toString() }, ] const newValuesObj = Object.fromEntries(newValues.map((v) => [getValueId(v), v])) @@ -409,37 +374,40 @@ async function buildQueries( })() const locksQuery: RpcStateQuery | undefined = (() => { - const storageHelper = new StorageHelper( - typeRegistry, - "balances", - "locks", - decodeAnyAddress(address) + const scaleCoder = chainStorageCoders.get(chainId)?.locks + const stateKey = encodeStateKey( + scaleCoder, + `Invalid address in ${chainId} locks query ${address}`, + address ) - const storageDecoder = chainStorageDecoders.get(chainId)?.locksDecoder - const stateKey = storageHelper.stateKey if (!stateKey) return + const decodeResult = (change: string | null) => { - if (change === null) return balanceJson - - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const decoded: any = - storageDecoder && change !== null ? storageDecoder.decode($.decodeHex(change)) : null - if (decoded) { - locksQueryLocks = decoded - // eslint-disable-next-line @typescript-eslint/no-explicit-any - .map?.((lock: any) => ({ - type: "locked", - source: "substrate-native-locks", - label: getLockedType(lock?.id?.toUtf8?.()), - amount: lock.amount.toString(), - })) - - // locked values should be replaced entirely, not merged or appended - const nonLockValues = balanceJson.values.filter( - (v) => v.source !== "substrate-native-locks" - ) - balanceJson.values = nonLockValues.concat(locksQueryLocks) - } + /** NOTE: This type is only a hint for typescript, the chain can actually return whatever it wants to */ + type DecodedType = Array<{ + id?: Binary + amount?: bigint + }> + + const decoded = decodeScale( + scaleCoder, + change, + `Failed to decode lock on chain ${chainId}` + ) + if (!decoded) return balanceJson + + locksQueryLocks = decoded.map?.((lock) => ({ + type: "locked", + source: "substrate-native-locks", + label: getLockedType(lock?.id?.asText?.()), + amount: (lock?.amount ?? 0n).toString(), + })) + + // locked values should be replaced entirely, not merged or appended + const nonLockValues = balanceJson.values.filter( + (v) => v.source !== "substrate-native-locks" + ) + balanceJson.values = nonLockValues.concat(locksQueryLocks) return balanceJson } @@ -448,37 +416,42 @@ async function buildQueries( })() const freezesQuery: RpcStateQuery | undefined = (() => { - const storageHelper = new StorageHelper( - typeRegistry, - "balances", - "freezes", - decodeAnyAddress(address) + const scaleCoder = chainStorageCoders.get(chainId)?.freezes + const stateKey = encodeStateKey( + scaleCoder, + `Invalid address in ${chainId} freezes query ${address}`, + address ) - const storageDecoder = chainStorageDecoders.get(chainId)?.freezesDecoder - const stateKey = storageHelper.stateKey if (!stateKey) return + const decodeResult = (change: string | null) => { - if (change === null) return balanceJson - - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const decoded: any = - storageDecoder && change !== null ? storageDecoder.decode($.decodeHex(change)) : null - if (decoded) { - freezesQueryLocks = decoded - // eslint-disable-next-line @typescript-eslint/no-explicit-any - .map?.((lock: any) => ({ - type: "locked", - source: "substrate-native-freezes", - label: getLockedType(lock?.id?.type?.toLowerCase?.()), - amount: lock.amount.toString(), - })) - - // freezes values should be replaced entirely, not merged or appended - const nonFreezesValues = balanceJson.values.filter( - (v) => v.source !== "substrate-native-freezes" - ) - balanceJson.values = nonFreezesValues.concat(freezesQueryLocks) - } + /** NOTE: This type is only a hint for typescript, the chain can actually return whatever it wants to */ + type DecodedType = Array<{ + id?: { type?: string } + amount?: bigint + }> + + const decoded = decodeScale( + scaleCoder, + change, + `Failed to decode freeze on chain ${chainId}` + ) + + if (!decoded) return balanceJson + + freezesQueryLocks = decoded?.map?.((lock) => ({ + type: "locked", + source: "substrate-native-freezes", + label: getLockedType(lock?.id?.type?.toLowerCase?.()), + amount: lock?.amount?.toString?.() ?? "0", + })) + + // freezes values should be replaced entirely, not merged or appended + const nonFreezesValues = balanceJson.values.filter( + (v) => v.source !== "substrate-native-freezes" + ) + balanceJson.values = nonFreezesValues.concat(freezesQueryLocks) + return balanceJson } @@ -493,5 +466,5 @@ async function buildQueries( }) return outerResult - }, {} as Record>>) + }, {}) } diff --git a/packages/balances/src/modules/SubstrateNativeModule/index.ts b/packages/balances/src/modules/SubstrateNativeModule/index.ts index 6689a86521..9654649275 100644 --- a/packages/balances/src/modules/SubstrateNativeModule/index.ts +++ b/packages/balances/src/modules/SubstrateNativeModule/index.ts @@ -1,6 +1,6 @@ import { TypeRegistry } from "@polkadot/types" import { ExtDef } from "@polkadot/types/extrinsic/signedExtensions/types" -import { arrayChunk, assert, u8aToHex, u8aToString } from "@polkadot/util" +import { arrayChunk, assert, u8aToHex } from "@polkadot/util" import { defineMethod } from "@substrate/txwrapper-core" import PromisePool from "@supercharge/promise-pool" import { ChainConnectionError, ChainConnector } from "@talismn/chain-connector" @@ -13,14 +13,14 @@ import { TokenId, } from "@talismn/chaindata-provider" import { - $metadataV14, - filterMetadataPalletsAndItems, - getMetadataVersion, - PalletMV14, - StorageEntryMV14, - transformMetadataV14, + Binary, + compactMetadata, + decodeMetadata, + decodeScale, + encodeMetadata, + encodeStateKey, + getDynamicBuilder, } from "@talismn/scale" -import * as $ from "@talismn/subshape-fork" import { decodeAnyAddress, Deferred, isEthereumAddress } from "@talismn/util" import isEqual from "lodash/isEqual" import { @@ -41,6 +41,7 @@ import { takeUntil, withLatestFrom, } from "rxjs" +import { u128 } from "scale-ts" import { DefaultBalanceModule, NewBalanceModule, NewTransferParamsType } from "../../BalanceModule" import log from "../../log" @@ -54,22 +55,18 @@ import { SubscriptionCallback, } from "../../types" import { - buildStorageDecoders, - createTypeRegistryCache, + buildStorageCoders, detectTransferMethod, findChainMeta, - GetOrCreateTypeRegistry, getUniqueChainIds, RpcStateQuery, RpcStateQueryHelper, - StorageHelper, } from "../util" -import { - asObservable, - crowdloanFundContributionsChildKey, - nompoolStashAccountId, -} from "../util/substrate-native" import { QueryCache } from "./QueryCache" +import { asObservable, crowdloanFundContributionsChildKey, nompoolStashAccountId } from "./util" + +export type { BalanceLockType } from "./util" +export { getLockTitle, filterBaseLocks } from "./util" type ModuleType = "substrate-native" const moduleType: ModuleType = "substrate-native" @@ -77,6 +74,9 @@ const moduleType: ModuleType = "substrate-native" export type SubNativeToken = Extract export type CustomSubNativeToken = Extract +const DEFAULT_SYMBOL = "Unit" +const DEFAULT_DECIMALS = 0 + export const subNativeTokenId = (chainId: ChainId) => `${chainId}-substrate-native`.toLowerCase().replace(/ /g, "-") @@ -118,13 +118,13 @@ const mergeBalances = ( export type SubNativeChainMeta = { isTestnet: boolean useLegacyTransferableCalculation?: boolean - symbol: string - decimals: number - existentialDeposit: string | null - nominationPoolsPalletId: string | null - crowdloanPalletId: string | null - miniMetadata: `0x${string}` | null - metadataVersion: number + symbol?: string + decimals?: number + existentialDeposit?: string + nominationPoolsPalletId?: string + crowdloanPalletId?: string + miniMetadata?: string + metadataVersion?: number } export type SubNativeModuleConfig = { @@ -185,9 +185,7 @@ export const SubNativeModule: NewBalanceModule< const chainConnector = chainConnectors.substrate assert(chainConnector, "This module requires a substrate chain connector") - const { getOrCreateTypeRegistry } = createTypeRegistryCache() - - const queryCache = new QueryCache(chaindataProvider, getOrCreateTypeRegistry) + const queryCache = new QueryCache(chaindataProvider) const getModuleTokens = async () => { return (await chaindataProvider.tokensByIdForType(moduleType)) as Record @@ -196,107 +194,70 @@ export const SubNativeModule: NewBalanceModule< return { ...DefaultBalanceModule(moduleType), - async fetchSubstrateChainMeta(chainId, moduleConfig, metadataRpc) { + async fetchSubstrateChainMeta(chainId, moduleConfig, metadataRpc, systemProperties) { const isTestnet = (await chaindataProvider.chainById(chainId))?.isTestnet || false + if (moduleConfig?.disable === true || metadataRpc === undefined) return { isTestnet } - if (moduleConfig?.disable === true || metadataRpc === undefined) - return { - isTestnet, - symbol: "", - decimals: 0, - existentialDeposit: null, - nominationPoolsPalletId: null, - crowdloanPalletId: null, - miniMetadata: null, - metadataVersion: 0, - } + // + // extract system_properties + // - const chainProperties = await chainConnector.send(chainId, "system_properties", []) + const { tokenSymbol, tokenDecimals } = systemProperties ?? {} + const symbol: string = + (Array.isArray(tokenSymbol) ? tokenSymbol[0] : tokenSymbol) ?? DEFAULT_SYMBOL + const decimals: number = + (Array.isArray(tokenDecimals) ? tokenDecimals[0] : tokenDecimals) ?? DEFAULT_DECIMALS - const metadataVersion = getMetadataVersion(metadataRpc) + // + // process metadata into SCALE encoders/decoders + // - const { tokenSymbol, tokenDecimals } = chainProperties + const { metadataVersion, metadata, tag } = decodeMetadata(metadataRpc) + if (!metadata) return { isTestnet, symbol, decimals } - const symbol: string = (Array.isArray(tokenSymbol) ? tokenSymbol[0] : tokenSymbol) ?? "Unit" - const decimals: number = - (Array.isArray(tokenDecimals) ? tokenDecimals[0] : tokenDecimals) ?? 0 - - if (metadataVersion !== 14) - return { - isTestnet, - symbol, - decimals, - existentialDeposit: null, - nominationPoolsPalletId: null, - crowdloanPalletId: null, - miniMetadata: null, - metadataVersion, - } + // + // get runtime constants + // - const metadata = $metadataV14.decode($.decodeHex(metadataRpc)) - const subshape = transformMetadataV14(metadata) - - const existentialDeposit = ( - subshape.pallets.Balances?.constants.ExistentialDeposit?.codec.decode?.( - subshape.pallets.Balances.constants.ExistentialDeposit.value - ) ?? 0n - ).toString() - const nominationPoolsPalletId = subshape.pallets.NominationPools?.constants.PalletId?.value - ? u8aToHex(subshape.pallets.NominationPools?.constants.PalletId?.value) - : null - const crowdloanPalletId = subshape.pallets.Crowdloan?.constants.PalletId?.value - ? u8aToHex(subshape.pallets.Crowdloan?.constants.PalletId?.value) - : null - - const isSystemPallet = (pallet: PalletMV14) => pallet.name === "System" - const isAccountItem = (item: StorageEntryMV14) => item.name === "Account" - - const isBalancesPallet = (pallet: PalletMV14) => pallet.name === "Balances" - const isReservesItem = (item: StorageEntryMV14) => item.name === "Reserves" - const isHoldsItem = (item: StorageEntryMV14) => item.name === "Holds" - const isLocksItem = (item: StorageEntryMV14) => item.name === "Locks" - const isFreezesItem = (item: StorageEntryMV14) => item.name === "Freezes" - - const isNomPoolsPallet = (pallet: PalletMV14) => pallet.name === "NominationPools" - const isPoolMembersItem = (item: StorageEntryMV14) => item.name === "PoolMembers" - const isBondedPoolsItem = (item: StorageEntryMV14) => item.name === "BondedPools" - const isMetadataItem = (item: StorageEntryMV14) => item.name === "Metadata" - - const isStakingPallet = (pallet: PalletMV14) => pallet.name === "Staking" - const isLedgerItem = (item: StorageEntryMV14) => item.name === "Ledger" - - const isCrowdloanPallet = (pallet: PalletMV14) => pallet.name === "Crowdloan" - const isFundsItem = (item: StorageEntryMV14) => item.name === "Funds" - - const isParasPallet = (pallet: PalletMV14) => pallet.name === "Paras" - const isParachainsItem = (item: StorageEntryMV14) => item.name === "Parachains" - - // TODO: Handle metadata v15 - filterMetadataPalletsAndItems(metadata, [ - { pallet: isSystemPallet, items: [isAccountItem] }, - { - pallet: isBalancesPallet, - items: [isReservesItem, isHoldsItem, isLocksItem, isFreezesItem], - }, - { - pallet: isNomPoolsPallet, - items: [isPoolMembersItem, isBondedPoolsItem, isMetadataItem], - }, - { pallet: isStakingPallet, items: [isLedgerItem] }, - { pallet: isCrowdloanPallet, items: [isFundsItem] }, - { pallet: isParasPallet, items: [isParachainsItem] }, + const scaleBuilder = getDynamicBuilder(metadata) + const getConstantValue = (palletName: string, constantName: string) => { + const encodedValue = metadata.pallets + .find(({ name }) => name === palletName) + ?.constants.find(({ name }) => name === constantName)?.value + if (!encodedValue) return + + return scaleBuilder.buildConstant(palletName, constantName)?.dec(encodedValue) + } + + const existentialDeposit = getConstantValue("Balances", "ExistentialDeposit")?.toString() + const nominationPoolsPalletId = getConstantValue("NominationPools", "PalletId")?.asText() + const crowdloanPalletId = getConstantValue("Crowdloan", "PalletId")?.asText() + + // + // compact metadata into miniMetadata + // + + compactMetadata(metadata, [ + { pallet: "System", items: ["Account"] }, + { pallet: "Balances", items: ["Reserves", "Holds", "Locks", "Freezes"] }, + { pallet: "NominationPools", items: ["PoolMembers", "BondedPools", "Metadata"] }, + { pallet: "Staking", items: ["Ledger"] }, + { pallet: "Crowdloan", items: ["Funds"] }, + { pallet: "Paras", items: ["Parachains"] }, ]) - metadata.extrinsic.signedExtensions = [] - const miniMetadata = $.encodeHexPrefixed($metadataV14.encode(metadata)) as `0x${string}` + const miniMetadata = encodeMetadata(tag === "v15" ? { tag, metadata } : { tag, metadata }) const hasFreezesItem = Boolean( - metadata.pallets.find(isBalancesPallet)?.storage?.entries.find(isFreezesItem) + metadata.pallets + .find(({ name }) => name === "Balances") + ?.storage?.items.find(({ name }) => name === "Freezes") ) const useLegacyTransferableCalculation = !hasFreezesItem const chainMeta: SubNativeChainMeta = { isTestnet, + useLegacyTransferableCalculation, symbol, decimals, existentialDeposit, @@ -305,7 +266,7 @@ export const SubNativeModule: NewBalanceModule< miniMetadata, metadataVersion, } - if (useLegacyTransferableCalculation) chainMeta.useLegacyTransferableCalculation = true + if (!useLegacyTransferableCalculation) delete chainMeta.useLegacyTransferableCalculation return chainMeta }, @@ -322,8 +283,8 @@ export const SubNativeModule: NewBalanceModule< type: "substrate-native", isTestnet, isDefault: moduleConfig?.isDefault ?? true, - symbol, - decimals, + symbol: symbol ?? DEFAULT_SYMBOL, + decimals: decimals ?? DEFAULT_DECIMALS, logo: moduleConfig?.logo || githubTokenLogoUrl(id), existentialDeposit: existentialDeposit ?? "0", chain: { id: chainId }, @@ -465,25 +426,23 @@ export const SubNativeModule: NewBalanceModule< const unsubNompoolStaking = subscribeNompoolStaking( chaindataProvider, chainConnectors.substrate, - getOrCreateTypeRegistry, newAddressesByToken, handleUpdateForSource("nompools-staking") ) const unsubCrowdloans = subscribeCrowdloans( chaindataProvider, chainConnectors.substrate, - getOrCreateTypeRegistry, newAddressesByToken, handleUpdateForSource("crowdloan") ) - const subBase = subscribeBase( + const unsubBase = subscribeBase( baseQueries, chainConnectors.substrate, handleUpdateForSource("base") ) subscriber.add(async () => (await unsubNompoolStaking)()) subscriber.add(async () => (await unsubCrowdloans)()) - subscriber.add(async () => (await subBase)()) + subscriber.add(async () => (await unsubBase)()) }) }) ) @@ -652,7 +611,6 @@ export const SubNativeModule: NewBalanceModule< async function subscribeNompoolStaking( chaindataProvider: ChaindataProvider, chainConnector: ChainConnector, - getOrCreateTypeRegistry: GetOrCreateTypeRegistry, addressesByToken: AddressesByToken, callback: SubscriptionCallback ) { @@ -694,15 +652,16 @@ async function subscribeNompoolStaking( const chains = Object.fromEntries( Object.entries(allChains).filter(([chainId]) => uniqueChainIds.includes(chainId)) ) - const chainStorageDecoders = buildStorageDecoders({ + const chainStorageCoders = buildStorageCoders({ + chainIds: uniqueChainIds, chains, miniMetadatas, moduleType: "substrate-native", - decoders: { - poolMembersDecoder: ["nominationPools", "poolMembers"], - bondedPoolsDecoder: ["nominationPools", "bondedPools"], - ledgerDecoder: ["staking", "ledger"], - metadataDecoder: ["nominationPools", "metadata"], + coders: { + poolMembers: ["NominationPools", "PoolMembers"], + bondedPools: ["NominationPools", "BondedPools"], + ledger: ["Staking", "Ledger"], + metadata: ["NominationPools", "Metadata"], }, }) @@ -727,17 +686,13 @@ async function subscribeNompoolStaking( log.warn(`Chain ${chainId} for token ${tokenId} not found`) continue } + const [chainMeta] = findChainMeta( miniMetadatas, "substrate-native", chain ) - const typeRegistry = - chainMeta?.miniMetadata !== undefined && - chainMeta?.miniMetadata !== null && - chainMeta?.metadataVersion >= 14 - ? getOrCreateTypeRegistry(chainId, chainMeta.miniMetadata) - : new TypeRegistry() + const { nominationPoolsPalletId } = chainMeta ?? {} type PoolMembers = { tokenId: string @@ -750,34 +705,45 @@ async function subscribeNompoolStaking( addresses: string[], callback: SubscriptionCallback ) => { - const storageDecoder = chainStorageDecoders.get(chainId)?.poolMembersDecoder + const scaleCoder = chainStorageCoders.get(chainId)?.poolMembers const queries = addresses.flatMap((address): RpcStateQuery | [] => { - const storageHelper = new StorageHelper( - typeRegistry, - "nominationPools", - "poolMembers", - decodeAnyAddress(address) + const stateKey = encodeStateKey( + scaleCoder, + `Invalid address in ${chainId} poolMembers query ${address}`, + address ) - const stateKey = storageHelper.stateKey if (!stateKey) return [] + const decodeResult = (change: string | null) => { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const decoded: any = - storageDecoder && change !== null ? storageDecoder.decode($.decodeHex(change)) : null + /** NOTE: This type is only a hint for typescript, the chain can actually return whatever it wants to */ + type DecodedType = { + pool_id?: number + points?: bigint + last_recorded_reward_counter?: bigint + /** Array of `[Era, Amount]` */ + unbonding_eras?: Array<[number | undefined, bigint | undefined] | undefined> + } + + const decoded = decodeScale( + scaleCoder, + change, + `Failed to decode poolMembers on chain ${chainId}` + ) - const poolId: string | undefined = decoded?.poolId?.toString?.() + const poolId: string | undefined = decoded?.pool_id?.toString?.() const points: string | undefined = decoded?.points?.toString?.() - const unbondingEras: Array<{ era: string; amount: string }> = - // eslint-disable-next-line @typescript-eslint/no-explicit-any - Array.from(decoded?.unbondingEras ?? []).flatMap((entry: any) => { - const [key, value] = Array.from(entry) + const unbondingEras: Array<{ era: string; amount: string }> = Array.from( + decoded?.unbonding_eras ?? [] + ).flatMap((entry) => { + if (entry === undefined) return [] + const [key, value] = Array.from(entry) - const era = key?.toString?.() - const amount = value?.toString?.() - if (typeof era !== "string" || typeof amount !== "string") return [] + const era = key?.toString?.() + const amount = value?.toString?.() + if (typeof era !== "string" || typeof amount !== "string") return [] - return { era, amount } - }) + return { era, amount } + }) return { tokenId, address, poolId, points, unbondingEras } } @@ -796,20 +762,30 @@ async function subscribeNompoolStaking( ) => { if (poolIds.length === 0) callback(null, []) - const storageDecoder = chainStorageDecoders.get(chainId)?.bondedPoolsDecoder + const scaleCoder = chainStorageCoders.get(chainId)?.bondedPools const queries = poolIds.flatMap((poolId): RpcStateQuery | [] => { - const storageHelper = new StorageHelper( - typeRegistry, - "nominationPools", - "bondedPools", + const stateKey = encodeStateKey( + scaleCoder, + `Invalid poolId in ${chainId} bondedPools query ${poolId}`, poolId ) - const stateKey = storageHelper.stateKey if (!stateKey) return [] + const decodeResult = (change: string | null) => { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const decoded: any = - storageDecoder && change !== null ? storageDecoder.decode($.decodeHex(change)) : null + /** NOTE: This type is only a hint for typescript, the chain can actually return whatever it wants to */ + type DecodedType = { + commission?: unknown + member_counter?: number + points?: bigint + roles?: unknown + state?: unknown + } + + const decoded = decodeScale( + scaleCoder, + change, + `Failed to decode bondedPools on chain ${chainId}` + ) const points: string | undefined = decoded?.points?.toString?.() @@ -827,21 +803,32 @@ async function subscribeNompoolStaking( const subscribePoolStake = (poolIds: string[], callback: SubscriptionCallback) => { if (poolIds.length === 0) callback(null, []) - const storageDecoder = chainStorageDecoders.get(chainId)?.ledgerDecoder + const scaleCoder = chainStorageCoders.get(chainId)?.ledger const queries = poolIds.flatMap((poolId): RpcStateQuery | [] => { - if (!chainMeta?.nominationPoolsPalletId) return [] - const storageHelper = new StorageHelper( - typeRegistry, - "staking", - "ledger", - nompoolStashAccountId(typeRegistry, chainMeta?.nominationPoolsPalletId, poolId) + if (!nominationPoolsPalletId) return [] + const stashAddress = nompoolStashAccountId(nominationPoolsPalletId, poolId) + const stateKey = encodeStateKey( + scaleCoder, + `Invalid address in ${chainId} ledger query ${stashAddress}`, + stashAddress ) - const stateKey = storageHelper.stateKey if (!stateKey) return [] + const decodeResult = (change: string | null) => { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const decoded: any = - storageDecoder && change !== null ? storageDecoder.decode($.decodeHex(change)) : null + /** NOTE: This type is only a hint for typescript, the chain can actually return whatever it wants to */ + type DecodedType = { + active?: bigint + legacy_claimed_rewards?: number[] + stash?: string + total?: bigint + unlocking?: Array<{ value?: bigint; era?: number }> + } + + const decoded = decodeScale( + scaleCoder, + change, + `Failed to decode ledger on chain ${chainId}` + ) const activeStake: string | undefined = decoded?.active?.toString?.() @@ -862,18 +849,27 @@ async function subscribeNompoolStaking( ) => { if (poolIds.length === 0) callback(null, []) - const storageDecoder = chainStorageDecoders.get(chainId)?.metadataDecoder + const scaleCoder = chainStorageCoders.get(chainId)?.metadata const queries = poolIds.flatMap((poolId): RpcStateQuery | [] => { - if (!chainMeta?.nominationPoolsPalletId) return [] - const storageHelper = new StorageHelper(typeRegistry, "nominationPools", "metadata", poolId) - const stateKey = storageHelper.stateKey + if (!nominationPoolsPalletId) return [] + const stateKey = encodeStateKey( + scaleCoder, + `Invalid poolId in ${chainId} metadata query ${poolId}`, + poolId + ) if (!stateKey) return [] + const decodeResult = (change: string | null) => { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const decoded: any = - storageDecoder && change !== null ? storageDecoder.decode($.decodeHex(change)) : null + /** NOTE: This type is only a hint for typescript, the chain can actually return whatever it wants to */ + type DecodedType = Binary + + const decoded = decodeScale( + scaleCoder, + change, + `Failed to decode metadata on chain ${chainId}` + ) - const metadata = u8aToString(decoded) + const metadata = decoded?.asText?.() return { poolId, metadata } } @@ -1030,7 +1026,6 @@ async function subscribeNompoolStaking( async function subscribeCrowdloans( chaindataProvider: ChaindataProvider, chainConnector: ChainConnector, - getOrCreateTypeRegistry: GetOrCreateTypeRegistry, addressesByToken: AddressesByToken, callback: SubscriptionCallback ) { @@ -1073,11 +1068,15 @@ async function subscribeCrowdloans( const chains = Object.fromEntries( Object.entries(allChains).filter(([chainId]) => uniqueChainIds.includes(chainId)) ) - const chainStorageDecoders = buildStorageDecoders({ + const chainStorageCoders = buildStorageCoders({ + chainIds: uniqueChainIds, chains, miniMetadatas, moduleType: "substrate-native", - decoders: { parachainsDecoder: ["paras", "parachains"], fundsDecoder: ["crowdloan", "funds"] }, + coders: { + parachains: ["Paras", "Parachains"], + funds: ["Crowdloan", "Funds"], + }, }) const tokenSubscriptions: Array<() => void> = [] @@ -1101,30 +1100,22 @@ async function subscribeCrowdloans( log.warn(`Chain ${chainId} for token ${tokenId} not found`) continue } - const [chainMeta] = findChainMeta( - miniMetadatas, - "substrate-native", - chain - ) - const typeRegistry = - chainMeta?.miniMetadata !== undefined && - chainMeta?.miniMetadata !== null && - chainMeta?.metadataVersion >= 14 - ? getOrCreateTypeRegistry(chainId, chainMeta.miniMetadata) - : new TypeRegistry() const subscribeParaIds = (callback: SubscriptionCallback>) => { - const storageDecoder = chainStorageDecoders.get(chainId)?.parachainsDecoder + const scaleCoder = chainStorageCoders.get(chainId)?.parachains const queries = [0].flatMap((): RpcStateQuery | [] => { - const storageHelper = new StorageHelper(typeRegistry, "paras", "parachains") - const stateKey = storageHelper.stateKey + const stateKey = encodeStateKey(scaleCoder) if (!stateKey) return [] - const decodeResult = (change: string | null): number[] => { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const decoded: any = - storageDecoder && change !== null ? storageDecoder.decode($.decodeHex(change)) : null - // eslint-disable-next-line @typescript-eslint/no-explicit-any + const decodeResult = (change: string | null) => { + /** NOTE: This type is only a hint for typescript, the chain can actually return whatever it wants to */ + type DecodedType = number[] + const decoded = decodeScale( + scaleCoder, + change, + `Failed to decode parachains on chain ${chainId}` + ) + const paraIds = decoded ?? [] return paraIds @@ -1137,25 +1128,50 @@ async function subscribeCrowdloans( return () => subscription.then((unsubscribe) => unsubscribe()) } - type ParaFundIndex = { paraId: number; fundPeriod: string; fundIndex?: number[] } + type ParaFundIndex = { + paraId: number + fundPeriod: string + fundIndex?: number + } const subscribeParaFundIndexes = ( paraIds: number[], callback: SubscriptionCallback ) => { - const storageDecoder = chainStorageDecoders.get(chainId)?.fundsDecoder + const scaleCoder = chainStorageCoders.get(chainId)?.funds const queries = paraIds.flatMap((paraId): RpcStateQuery | [] => { - const storageHelper = new StorageHelper(typeRegistry, "crowdloan", "funds", paraId) - const stateKey = storageHelper.stateKey + const stateKey = encodeStateKey( + scaleCoder, + `Invalid paraId in ${chainId} funds query ${paraId}`, + paraId + ) if (!stateKey) return [] + const decodeResult = (change: string | null) => { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const decoded: any = - storageDecoder && change !== null ? storageDecoder.decode($.decodeHex(change)) : null + /** NOTE: This type is only a hint for typescript, the chain can actually return whatever it wants to */ + type DecodedType = { + cap?: bigint + deposit?: bigint + depositor?: string + end?: number + fund_index?: number + trie_index?: number + first_period?: number + last_period?: number + last_contribution?: unknown + raised?: bigint + verifier?: unknown + } + + const decoded = decodeScale( + scaleCoder, + change, + `Failed to decode paras on chain ${chainId}` + ) - const firstPeriod = decoded?.firstPeriod?.toString?.() ?? "" - const lastPeriod = decoded?.lastPeriod?.toString?.() ?? "" + const firstPeriod = decoded?.first_period?.toString?.() ?? "" + const lastPeriod = decoded?.last_period?.toString?.() ?? "" const fundPeriod = `${firstPeriod}-${lastPeriod}` - const fundIndex = decoded?.fundIndex ?? decoded?.trieIndex + const fundIndex = decoded?.fund_index ?? decoded?.trie_index return { paraId, fundPeriod, fundIndex } } @@ -1187,7 +1203,7 @@ async function subscribeCrowdloans( paraId, fundIndex, addresses, - childKey: crowdloanFundContributionsChildKey(typeRegistry, fundIndex), + childKey: crowdloanFundContributionsChildKey(fundIndex), storageKeys: addresses.map((address) => u8aToHex(decodeAnyAddress(address))), })) @@ -1202,24 +1218,26 @@ async function subscribeCrowdloans( paraId, fundIndex, addresses, - result: await chainConnector.send(chainId, "childstate_getStorageEntries", [ - childKey, - storageKeys, - ]), + result: await chainConnector.send | undefined>( + chainId, + "childstate_getStorageEntries", + [childKey, storageKeys] + ), })) ) const contributions = results.flatMap((queryResult) => { const { paraId, fundIndex, addresses, result } = queryResult - const storageDataVec = typeRegistry.createType("Vec>", result) - return storageDataVec.flatMap((storageData, index) => { - const balance = storageData?.isSome - ? typeRegistry.createType("Balance", storageData.unwrap()) - : typeRegistry.createType("Balance") - const amount = balance?.toString?.() + return (Array.isArray(result) ? result : []).flatMap((encoded, index) => { + const amount = (() => { + try { + return typeof encoded === "string" ? u128.dec(encoded) ?? 0n : 0n + } catch { + return 0n + } + })().toString() - if (amount === undefined || amount === "0") return [] return { paraId, fundIndex, diff --git a/packages/balances/src/modules/util/substrate-native.ts b/packages/balances/src/modules/SubstrateNativeModule/util.ts similarity index 86% rename from packages/balances/src/modules/util/substrate-native.ts rename to packages/balances/src/modules/SubstrateNativeModule/util.ts index c7f00480fe..5eaf3224cc 100644 --- a/packages/balances/src/modules/util/substrate-native.ts +++ b/packages/balances/src/modules/SubstrateNativeModule/util.ts @@ -1,8 +1,9 @@ -import type { Registry } from "@polkadot/types-codec/types" -import { BN, bnToU8a, hexToU8a, stringToU8a, u8aConcat, u8aToHex } from "@polkadot/util" +import { TypeRegistry } from "@polkadot/types" +import { BN, bnToU8a, stringToU8a, u8aConcat, u8aToHex } from "@polkadot/util" import { blake2AsU8a } from "@polkadot/util-crypto" import upperFirst from "lodash/upperFirst" import { Observable } from "rxjs" +import { u32 } from "scale-ts" import { Balance, @@ -35,16 +36,16 @@ export const asObservable = * Each nominationPool in the nominationPools pallet has access to some accountIds which have no * associated private key. Instead, they are derived from this function. */ -const nompoolAccountId = (registry: Registry, palletId: string, poolId: string, index: number) => { +const nompoolAccountId = (palletId: string, poolId: string, index: number) => { const EMPTY_H256 = new Uint8Array(32) const MOD_PREFIX = stringToU8a("modl") const U32_OPTS = { bitLength: 32, isLe: true } - return registry + return new TypeRegistry() .createType( "AccountId32", u8aConcat( MOD_PREFIX, - hexToU8a(palletId), + stringToU8a(palletId), new Uint8Array([index]), bnToU8a(new BN(poolId), U32_OPTS), EMPTY_H256 @@ -53,21 +54,18 @@ const nompoolAccountId = (registry: Registry, palletId: string, poolId: string, .toString() } /** The stash account for the nomination pool */ -export const nompoolStashAccountId = (registry: Registry, palletId: string, poolId: string) => - nompoolAccountId(registry, palletId, poolId, 0) +export const nompoolStashAccountId = (palletId: string, poolId: string) => + nompoolAccountId(palletId, poolId, 0) /** The rewards account for the nomination pool */ -export const nompoolRewardAccountId = (registry: Registry, palletId: string, poolId: string) => - nompoolAccountId(registry, palletId, poolId, 1) +export const nompoolRewardAccountId = (palletId: string, poolId: string) => + nompoolAccountId(palletId, poolId, 1) /** * Crowdloan contributions are stored in the `childstate` key returned by this function. */ -export const crowdloanFundContributionsChildKey = (registry: Registry, fundIndex: number) => +export const crowdloanFundContributionsChildKey = (fundIndex: number) => u8aToHex( - u8aConcat( - ":child_storage:default:", - blake2AsU8a(u8aConcat("crowdloan", registry.createType("u32", fundIndex).toU8a())) - ) + u8aConcat(":child_storage:default:", blake2AsU8a(u8aConcat("crowdloan", u32.enc(fundIndex)))) ) export type BalanceLockType = @@ -95,6 +93,7 @@ export const getLockedType = (input?: string): BalanceLockType => { if (input.includes("vesting")) return "vesting" if (input.includes("calamvst")) return "vesting" // vesting on manta network if (input.includes("ormlvest")) return "vesting" // vesting ORML tokens + if (input.includes("pyconvot")) return "democracy" if (input.includes("democrac")) return "democracy" if (input.includes("democracy")) return "democracy" if (input.includes("phrelect")) return "democracy" // specific to council @@ -119,6 +118,8 @@ export const getLockedType = (input?: string): BalanceLockType => { // ignore technical or undocumented lock types if (input.includes("pdexlock")) return getOtherType(input) if (input.includes("phala/sp")) return getOtherType(input) + if (input.includes("aca/earn")) return getOtherType(input) + if (input.includes("stk_stks")) return getOtherType(input) // eslint-disable-next-line no-console console.warn(`unknown locked type: ${input}`) diff --git a/packages/balances/src/modules/SubstratePsp22Module.ts b/packages/balances/src/modules/SubstratePsp22Module.ts index 4849993126..8c0ccf717f 100644 --- a/packages/balances/src/modules/SubstratePsp22Module.ts +++ b/packages/balances/src/modules/SubstratePsp22Module.ts @@ -17,14 +17,14 @@ import { DefaultBalanceModule, NewBalanceModule, NewTransferParamsType } from ". import log from "../log" import { AddressesByToken, BalanceJson, Balances, NewBalanceType } from "../types" import psp22Abi from "./abis/psp22.json" -import { makeContractCaller } from "./util/makeContractCaller" +import { makeContractCaller } from "./util" type ModuleType = "substrate-psp22" const moduleType: ModuleType = "substrate-psp22" export type SubPsp22Token = Extract -const subPsp22TokenId = (chainId: ChainId, tokenSymbol: string) => +export const subPsp22TokenId = (chainId: ChainId, tokenSymbol: string) => `${chainId}-substrate-psp22-${tokenSymbol}`.toLowerCase().replace(/ /g, "-") export type SubPsp22ChainMeta = { @@ -79,7 +79,6 @@ export const SubPsp22Module: NewBalanceModule< async fetchSubstrateChainMeta(chainId) { const isTestnet = (await chaindataProvider.chainById(chainId))?.isTestnet || false - return { isTestnet } }, diff --git a/packages/balances/src/modules/SubstrateTokensModule.ts b/packages/balances/src/modules/SubstrateTokensModule.ts index 4a597927e1..da1b78fd83 100644 --- a/packages/balances/src/modules/SubstrateTokensModule.ts +++ b/packages/balances/src/modules/SubstrateTokensModule.ts @@ -10,42 +10,31 @@ import { Token, } from "@talismn/chaindata-provider" import { - $metadataV14, - filterMetadataPalletsAndItems, - getMetadataVersion, - PalletMV14, - StorageEntryMV14, + compactMetadata, + decodeMetadata, + decodeScale, + encodeMetadata, + encodeStateKey, } from "@talismn/scale" -import * as $ from "@talismn/subshape-fork" -import { decodeAnyAddress } from "@talismn/util" import { DefaultBalanceModule, NewBalanceModule, NewTransferParamsType } from "../BalanceModule" import log from "../log" import { db as balancesDb } from "../TalismanBalancesDatabase" import { AddressesByToken, AmountWithLabel, Balances, NewBalanceType } from "../types" -import { - buildStorageDecoders, - createTypeRegistryCache, - findChainMeta, - GetOrCreateTypeRegistry, - getUniqueChainIds, - RpcStateQuery, - RpcStateQueryHelper, - StorageHelper, -} from "./util" +import { buildStorageCoders, getUniqueChainIds, RpcStateQuery, RpcStateQueryHelper } from "./util" type ModuleType = "substrate-tokens" const moduleType: ModuleType = "substrate-tokens" export type SubTokensToken = Extract -const subTokensTokenId = (chainId: ChainId, tokenSymbol: string) => +export const subTokensTokenId = (chainId: ChainId, tokenSymbol: string) => `${chainId}-substrate-tokens-${tokenSymbol}`.toLowerCase().replace(/ /g, "-") export type SubTokensChainMeta = { isTestnet: boolean - miniMetadata: `0x${string}` | null - metadataVersion: number + miniMetadata?: string + metadataVersion?: number } export type SubTokensModuleConfig = { @@ -91,37 +80,22 @@ export const SubTokensModule: NewBalanceModule< const chainConnector = chainConnectors.substrate assert(chainConnector, "This module requires a substrate chain connector") - const { getOrCreateTypeRegistry } = createTypeRegistryCache() - return { ...DefaultBalanceModule(moduleType), async fetchSubstrateChainMeta(chainId, moduleConfig, metadataRpc) { const isTestnet = (await chaindataProvider.chainById(chainId))?.isTestnet || false - if (metadataRpc === undefined) return { isTestnet, miniMetadata: null, metadataVersion: 0 } - - const metadataVersion = getMetadataVersion(metadataRpc) - if ((moduleConfig?.tokens ?? []).length < 1) - return { isTestnet, miniMetadata: null, metadataVersion } - - if (metadataVersion !== 14) return { isTestnet, miniMetadata: null, metadataVersion } + if (metadataRpc === undefined) return { isTestnet } + if ((moduleConfig?.tokens ?? []).length < 1) return { isTestnet } - const metadata = $metadataV14.decode($.decodeHex(metadataRpc)) + const { metadataVersion, metadata, tag } = decodeMetadata(metadataRpc) + if (!metadata) return { isTestnet } - const isTokensPallet = (pallet: PalletMV14) => pallet.name === "Tokens" - const isAccountsItem = (item: StorageEntryMV14) => item.name === "Accounts" + compactMetadata(metadata, [{ pallet: "Tokens", items: ["Accounts"] }]) - // TODO: Handle metadata v15 - filterMetadataPalletsAndItems(metadata, [{ pallet: isTokensPallet, items: [isAccountsItem] }]) - metadata.extrinsic.signedExtensions = [] + const miniMetadata = encodeMetadata(tag === "v15" ? { tag, metadata } : { tag, metadata }) - const miniMetadata = $.encodeHexPrefixed($metadataV14.encode(metadata)) as `0x${string}` - - return { - isTestnet, - miniMetadata, - metadataVersion, - } + return { isTestnet, miniMetadata, metadataVersion } }, async fetchSubstrateChainTokens(chainId, chainMeta, moduleConfig) { @@ -171,14 +145,10 @@ export const SubTokensModule: NewBalanceModule< // TODO: Don't create empty subscriptions async subscribeBalances({ addressesByToken }, callback) { - const queries = await buildQueries( - chaindataProvider, - getOrCreateTypeRegistry, - addressesByToken - ) + const queries = await buildQueries(chaindataProvider, addressesByToken) const unsubscribe = await new RpcStateQueryHelper(chainConnector, queries).subscribe( (error, result) => { - if (error) callback(error) + if (error) return callback(error) const balances = result?.filter((b): b is SubTokensBalance => b !== null) ?? [] if (balances.length > 0) callback(null, new Balances(balances)) } @@ -190,11 +160,7 @@ export const SubTokensModule: NewBalanceModule< async fetchBalances(addressesByToken) { assert(chainConnectors.substrate, "This module requires a substrate chain connector") - const queries = await buildQueries( - chaindataProvider, - getOrCreateTypeRegistry, - addressesByToken - ) + const queries = await buildQueries(chaindataProvider, addressesByToken) const result = await new RpcStateQueryHelper(chainConnectors.substrate, queries).fetch() const balances = result?.filter((b): b is SubTokensBalance => b !== null) ?? [] return new Balances(balances) @@ -315,7 +281,6 @@ export const SubTokensModule: NewBalanceModule< async function buildQueries( chaindataProvider: ChaindataProvider, - getOrCreateTypeRegistry: GetOrCreateTypeRegistry, addressesByToken: AddressesByToken ): Promise>> { const allChains = await chaindataProvider.chainsById() @@ -329,11 +294,12 @@ async function buildQueries( const uniqueChainIds = getUniqueChainIds(addressesByToken, tokens) const chains = Object.fromEntries(uniqueChainIds.map((chainId) => [chainId, allChains[chainId]])) - const chainStorageDecoders = buildStorageDecoders({ + const chainStorageCoders = buildStorageCoders({ + chainIds: uniqueChainIds, chains, miniMetadatas, moduleType: "substrate-tokens", - decoders: { storageDecoder: ["tokens", "accounts"] }, + coders: { storage: ["Tokens", "Accounts"] }, }) return Object.entries(addressesByToken).flatMap(([tokenId, addresses]) => { @@ -342,76 +308,59 @@ async function buildQueries( log.warn(`Token ${tokenId} not found`) return [] } - if (token.type !== "substrate-tokens") { log.debug(`This module doesn't handle tokens of type ${token.type}`) return [] } - const chainId = token.chain?.id if (!chainId) { log.warn(`Token ${tokenId} has no chain`) return [] } - const chain = chains[chainId] if (!chain) { log.warn(`Chain ${chainId} for token ${tokenId} not found`) return [] } - const [chainMeta] = findChainMeta( - miniMetadatas, - "substrate-tokens", - chain - ) - const registry = - chainMeta?.miniMetadata !== undefined && - chainMeta?.miniMetadata !== null && - chainMeta?.metadataVersion >= 14 - ? getOrCreateTypeRegistry(chainId, chainMeta.miniMetadata) - : new TypeRegistry() - return addresses.flatMap((address): RpcStateQuery | [] => { - const storageHelper = new StorageHelper( - registry, - "tokens", - "accounts", - decodeAnyAddress(address), - (() => { - try { - // `as string` doesn't matter here because we catch it if it throws - return JSON.parse(token.onChainId as string) - } catch (error) { - return token.onChainId - } - })() + const scaleCoder = chainStorageCoders.get(chainId)?.storage + const tokenOnChainId = (() => { + try { + // `as string` doesn't matter here because we catch it if it throws + return JSON.parse(token.onChainId as string) + } catch (error) { + return token.onChainId + } + })() + + const stateKey = encodeStateKey( + scaleCoder, + `Invalid address / token onChainId in ${chainId} storage query ${address} / ${JSON.stringify( + tokenOnChainId + )}`, + address, + assetIdToEnum(tokenOnChainId) ) - const storageDecoder = chainStorageDecoders.get(chainId)?.storageDecoder - const stateKey = storageHelper.stateKey if (!stateKey) return [] + const decodeResult = (change: string | null) => { - // e.g. - // { - // free: 33,765,103,752,560n - // reserved: 0n - // frozen: 0n - // } - const balance = - storageDecoder && change !== null - ? (storageDecoder.decode($.decodeHex(change)) as Record< - "free" | "reserved" | "frozen", - bigint - >) - : { - free: 0n, - reserved: 0n, - frozen: 0n, - } - - const free = (balance?.free ?? 0n).toString() - const reserved = (balance?.reserved ?? 0n).toString() - const frozen = (balance?.frozen ?? 0n).toString() + /** NOTE: This type is only a hint for typescript, the chain can actually return whatever it wants to */ + type DecodedType = { + free?: bigint + reserved?: bigint + frozen?: bigint + } + + const decoded = decodeScale( + scaleCoder, + change, + `Failed to decode substrate-tokens balance on chain ${chainId}` + ) ?? { free: 0n, reserved: 0n, frozen: 0n } + + const free = (decoded?.free ?? 0n).toString() + const reserved = (decoded?.reserved ?? 0n).toString() + const frozen = (decoded?.frozen ?? 0n).toString() const balanceValues: Array> = [ { type: "free", label: "free", amount: free.toString() }, @@ -434,3 +383,20 @@ async function buildQueries( }) }) } + +// TODO: See if this can be upstreamed / is actually necessary. +// There might be a better way to construct enums with polkadot-api. +// +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const assetIdToEnum = (assetId: any): any => { + if (typeof assetId !== "object" && typeof assetId !== "string") return assetId + + const keys = typeof assetId === "object" ? Object.keys(assetId) : [assetId] + if (keys.length !== 1) return assetId + + const key = keys[0] + return { + type: key, + value: typeof assetId === "object" ? assetIdToEnum(assetId[key]) : undefined, + } +} diff --git a/packages/balances/src/modules/index.ts b/packages/balances/src/modules/index.ts index 9afd7d4a17..fb13e24c8c 100644 --- a/packages/balances/src/modules/index.ts +++ b/packages/balances/src/modules/index.ts @@ -28,5 +28,3 @@ export * from "./SubstratePsp22Module" export * from "./SubstrateTokensModule" export * from "./util" -export * from "./util/substrate-native" -export * from "./util/storage" diff --git a/packages/balances/src/modules/util/InferBalanceModuleTypes.ts b/packages/balances/src/modules/util/InferBalanceModuleTypes.ts new file mode 100644 index 0000000000..3c60d35ec8 --- /dev/null +++ b/packages/balances/src/modules/util/InferBalanceModuleTypes.ts @@ -0,0 +1,42 @@ +import { BalanceModule, NewBalanceModule } from "../../BalanceModule" + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export type AnyBalanceModule = BalanceModule +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export type AnyNewBalanceModule = NewBalanceModule + +/** + * The following `Infer*` collection of generic types can be used when you want to + * extract one of the generic type arguments from an existing BalanceModule. + * + * For example, you might want to write a function which can accept any BalanceModule + * as an input, and then return the specific TokenType for that module: + * function getTokens(module: T): InferTokenType + * + * Or for another example, you might want a function which can take any BalanceModule `type` + * string as input, and then return some data associated with that module with the correct type: + * function getChainMeta(type: InferModuleType): InferChainMeta | undefined + */ +type InferBalanceModuleTypes = T extends NewBalanceModule< + infer TModuleType, + infer TTokenType, + infer TChainMeta, + infer TModuleConfig, + infer TTransferParams +> + ? { + TModuleType: TModuleType + TTokenType: TTokenType + TChainMeta: TChainMeta + TModuleConfig: TModuleConfig + TTransferParams: TTransferParams + } + : never +export type InferModuleType = + InferBalanceModuleTypes["TModuleType"] +export type InferTokenType = InferBalanceModuleTypes["TTokenType"] +export type InferChainMeta = InferBalanceModuleTypes["TChainMeta"] +export type InferModuleConfig = + InferBalanceModuleTypes["TModuleConfig"] +export type InferTransferParams = + InferBalanceModuleTypes["TTransferParams"] diff --git a/packages/balances/src/modules/util/RpcStateQueryHelper.ts b/packages/balances/src/modules/util/RpcStateQueryHelper.ts new file mode 100644 index 0000000000..f194d31306 --- /dev/null +++ b/packages/balances/src/modules/util/RpcStateQueryHelper.ts @@ -0,0 +1,104 @@ +import { ChainConnector } from "@talismn/chain-connector" +import { ChainId } from "@talismn/chaindata-provider" +import { hasOwnProperty } from "@talismn/util" +import groupBy from "lodash/groupBy" + +import log from "../../log" +import { SubscriptionCallback, UnsubscribeFn } from "../../types" + +/** + * Pass some these into an `RpcStateQueryHelper` in order to easily batch multiple state queries into the one rpc call. + */ +export type RpcStateQuery = { + chainId: string + stateKey: string + decodeResult: (change: string | null) => T +} + +/** + * Used by a variety of balance modules to help batch multiple state queries into the one rpc call. + */ +export class RpcStateQueryHelper { + #chainConnector: ChainConnector + #queries: Array> + + constructor(chainConnector: ChainConnector, queries: Array>) { + this.#chainConnector = chainConnector + this.#queries = queries + } + + async subscribe( + callback: SubscriptionCallback, + timeout: number | false = false, + subscribeMethod = "state_subscribeStorage", + responseMethod = "state_storage", + unsubscribeMethod = "state_unsubscribeStorage" + ): Promise { + const queriesByChain = groupBy(this.#queries, "chainId") + const subscriptions = Object.entries(queriesByChain).map(([chainId, queries]) => { + const params = [queries.map(({ stateKey }) => stateKey)] + + const unsub = this.#chainConnector.subscribe( + chainId, + subscribeMethod, + responseMethod, + params, + (error, result) => { + error + ? callback(error) + : callback(null, this.#distributeChangesToQueryDecoders.call(this, chainId, result)) + }, + timeout + ) + + return () => unsub.then((unsubscribe) => unsubscribe(unsubscribeMethod)) + }) + + return () => subscriptions.forEach((unsubscribe) => unsubscribe()) + } + + async fetch(method = "state_queryStorageAt"): Promise { + const queriesByChain = groupBy(this.#queries, "chainId") + + const resultsByChain = await Promise.all( + Object.entries(queriesByChain).map(async ([chainId, queries]) => { + const params = [queries.map(({ stateKey }) => stateKey)] + + const result = (await this.#chainConnector.send(chainId, method, params))[0] + return this.#distributeChangesToQueryDecoders.call(this, chainId, result) + }) + ) + + return resultsByChain.flatMap((result) => result) + } + + #distributeChangesToQueryDecoders(chainId: ChainId, result: unknown): T[] { + if (typeof result !== "object" || result === null) return [] + if (!hasOwnProperty(result, "changes") || typeof result.changes !== "object") return [] + if (!Array.isArray(result.changes)) return [] + + return result.changes.flatMap(([reference, change]: [unknown, unknown]): [T] | [] => { + if (typeof reference !== "string") { + log.warn(`Received non-string reference in RPC result: ${reference}`) + return [] + } + + if (typeof change !== "string" && change !== null) { + log.warn(`Received non-string and non-null change in RPC result: ${reference} | ${change}`) + return [] + } + + const query = this.#queries.find( + ({ chainId: cId, stateKey }) => cId === chainId && stateKey === reference + ) + if (!query) { + log.warn( + `Failed to find query:\n${reference} in\n${this.#queries.map(({ stateKey }) => stateKey)}` + ) + return [] + } + + return [query.decodeResult(change)] + }) + } +} diff --git a/packages/balances/src/modules/util/balances.ts b/packages/balances/src/modules/util/balances.ts new file mode 100644 index 0000000000..382594a109 --- /dev/null +++ b/packages/balances/src/modules/util/balances.ts @@ -0,0 +1,56 @@ +import { + BalanceModule, + DefaultChainMeta, + DefaultModuleConfig, + DefaultTransferParams, + ExtendableChainMeta, + ExtendableModuleConfig, + ExtendableTransferParams, + SelectableTokenType, + SubscriptionResultWithStatus, +} from "../../BalanceModule" +import { AddressesByToken, Balances, SubscriptionCallback, UnsubscribeFn } from "../../types" + +/** + * Wraps a BalanceModule's fetch/subscribe methods with a single `balances` method. + * This `balances` method will subscribe if a callback parameter is provided, or otherwise fetch. + */ +export async function balances< + TModuleType extends string, + TTokenType extends SelectableTokenType, + TChainMeta extends ExtendableChainMeta = DefaultChainMeta, + TModuleConfig extends ExtendableModuleConfig = DefaultModuleConfig, + TTransferParams extends ExtendableTransferParams = DefaultTransferParams +>( + balanceModule: BalanceModule, + addressesByToken: AddressesByToken +): Promise +export async function balances< + TModuleType extends string, + TTokenType extends SelectableTokenType, + TChainMeta extends ExtendableChainMeta = DefaultChainMeta, + TModuleConfig extends ExtendableModuleConfig = DefaultModuleConfig, + TTransferParams extends ExtendableTransferParams = DefaultTransferParams +>( + balanceModule: BalanceModule, + addressesByToken: AddressesByToken, + callback: SubscriptionCallback +): Promise +export async function balances< + TModuleType extends string, + TTokenType extends SelectableTokenType, + TChainMeta extends ExtendableChainMeta = DefaultChainMeta, + TModuleConfig extends ExtendableModuleConfig = DefaultModuleConfig, + TTransferParams extends ExtendableTransferParams = DefaultTransferParams +>( + balanceModule: BalanceModule, + addressesByToken: AddressesByToken, + callback?: SubscriptionCallback +): Promise { + // subscription request + if (callback !== undefined) + return await balanceModule.subscribeBalances({ addressesByToken }, callback) + + // one-off request + return await balanceModule.fetchBalances(addressesByToken) +} diff --git a/packages/balances/src/modules/util/buildStorageCoders.ts b/packages/balances/src/modules/util/buildStorageCoders.ts new file mode 100644 index 0000000000..16063c6b44 --- /dev/null +++ b/packages/balances/src/modules/util/buildStorageCoders.ts @@ -0,0 +1,75 @@ +import { ChainId, ChainList } from "@talismn/chaindata-provider" +import { decodeMetadata, getDynamicBuilder } from "@talismn/scale" + +import log from "../../log" +import { MiniMetadata } from "../../types" +import { findChainMeta } from "./findChainMeta" +import { AnyNewBalanceModule, InferModuleType } from "./InferBalanceModuleTypes" + +export type StorageCoders = Map< + string, + { + [Property in keyof TCoders]: + | ReturnType["buildStorage"]> + | undefined + } +> + +export const buildStorageCoders = < + TBalanceModule extends AnyNewBalanceModule, + TCoders extends { [key: string]: [string, string] } +>({ + chainIds, + chains, + miniMetadatas, + moduleType, + coders, +}: { + chainIds: ChainId[] + chains: ChainList + miniMetadatas: Map + moduleType: InferModuleType + coders: TCoders +}) => + new Map( + [...chainIds].flatMap((chainId) => { + const chain = chains[chainId] + if (!chain) return [] + + const [, miniMetadata] = findChainMeta(miniMetadatas, moduleType, chain) + if (!miniMetadata) return [] + if (!miniMetadata.data) return [] + + const { metadata, tag } = decodeMetadata(miniMetadata.data) + if (!metadata || !tag) return [] + + try { + const scaleBuilder = getDynamicBuilder(metadata) + const builtCoders = Object.fromEntries( + Object.entries(coders).flatMap( + ([key, [module, method]]: [keyof TCoders, [string, string]]) => { + try { + return [[key, scaleBuilder.buildStorage(module, method)] as const] + } catch (cause) { + log.warn( + `Failed to build SCALE coder for chain ${chainId} (${module}::${method})`, + cause + ) + return [] + } + } + ) + ) as { + [Property in keyof TCoders]: ReturnType<(typeof scaleBuilder)["buildStorage"]> | undefined + } + + return [[chainId, builtCoders]] + } catch (cause) { + log.error( + `Failed to build SCALE coders for chain ${chainId} (${JSON.stringify(coders)})`, + cause + ) + return [] + } + }) + ) diff --git a/packages/balances/src/modules/util/detectTransferMethod.ts b/packages/balances/src/modules/util/detectTransferMethod.ts new file mode 100644 index 0000000000..a794f08cb4 --- /dev/null +++ b/packages/balances/src/modules/util/detectTransferMethod.ts @@ -0,0 +1,29 @@ +import { Metadata, TypeRegistry } from "@polkadot/types" + +/** + * + * Detect Balances::transfer -> Balances::transfer_allow_death migration + * https://github.com/paritytech/substrate/pull/12951 + * + * `transfer_allow_death` is the preferred method, + * so if something goes wrong during detection, we should assume the chain has migrated + * @param metadataRpc string containing the hashed RPC metadata for the chain + * @returns + */ +export const detectTransferMethod = (metadataRpc: `0x${string}`) => { + const pjsMetadata = new Metadata(new TypeRegistry(), metadataRpc) + pjsMetadata.registry.setMetadata(pjsMetadata) + const balancesPallet = pjsMetadata.asLatest.pallets.find((pallet) => pallet.name.eq("Balances")) + + const balancesCallsTypeIndex = balancesPallet?.calls.value.type.toNumber() + const balancesCallsType = + balancesCallsTypeIndex !== undefined + ? pjsMetadata.asLatest.lookup.types[balancesCallsTypeIndex] + : undefined + const hasDeprecatedTransferCall = + balancesCallsType?.type.def.asVariant?.variants.find((variant) => + variant.name.eq("transfer") + ) !== undefined + + return hasDeprecatedTransferCall ? "transfer" : "transferAllowDeath" +} diff --git a/packages/balances/src/modules/util/findChainMeta.ts b/packages/balances/src/modules/util/findChainMeta.ts new file mode 100644 index 0000000000..6fa07b8f3b --- /dev/null +++ b/packages/balances/src/modules/util/findChainMeta.ts @@ -0,0 +1,41 @@ +import { Chain } from "@talismn/chaindata-provider" + +import { deriveMiniMetadataId, MiniMetadata } from "../../types" +import { AnyNewBalanceModule, InferChainMeta, InferModuleType } from "./InferBalanceModuleTypes" + +/** + * Given a `moduleType` and a `chain` from a chaindataProvider, this function will find the chainMeta + * associated with the given balanceModule for the given chain. + */ +export const findChainMeta = ( + miniMetadatas: Map, + moduleType: InferModuleType, + chain?: Chain +): [InferChainMeta | undefined, MiniMetadata | undefined] => { + if (!chain) return [undefined, undefined] + if (!chain.specName) return [undefined, undefined] + if (!chain.specVersion) return [undefined, undefined] + + // TODO: This is spaghetti to import this here, it should be injected into each balance module or something. + const metadataId = deriveMiniMetadataId({ + source: moduleType, + chainId: chain.id, + specName: chain.specName, + specVersion: chain.specVersion, + balancesConfig: JSON.stringify( + chain.balancesConfig?.find((config) => config.moduleType === moduleType)?.moduleConfig ?? {} + ), + }) + + // TODO: Fix this (needs to fetch miniMetadata without being async) + const miniMetadata = miniMetadatas.get(metadataId) + const chainMeta: InferChainMeta | undefined = miniMetadata + ? { + miniMetadata: miniMetadata.data, + metadataVersion: miniMetadata.version, + ...JSON.parse(miniMetadata.extra), + } + : undefined + + return [chainMeta, miniMetadata] +} diff --git a/packages/balances/src/modules/util/getUniqueChainIds.ts b/packages/balances/src/modules/util/getUniqueChainIds.ts new file mode 100644 index 0000000000..05e0c8834d --- /dev/null +++ b/packages/balances/src/modules/util/getUniqueChainIds.ts @@ -0,0 +1,14 @@ +import { ChainId, TokenList } from "@talismn/chaindata-provider" + +import { AddressesByToken } from "../../types" + +export const getUniqueChainIds = ( + addressesByToken: AddressesByToken<{ id: string }>, + tokens: TokenList +): ChainId[] => [ + ...new Set( + Object.keys(addressesByToken) + .map((tokenId) => tokens[tokenId]?.chain?.id) + .flatMap((chainId) => (chainId ? [chainId] : [])) + ), +] diff --git a/packages/balances/src/modules/util/index.ts b/packages/balances/src/modules/util/index.ts index c043ef28c7..ea5e677074 100644 --- a/packages/balances/src/modules/util/index.ts +++ b/packages/balances/src/modules/util/index.ts @@ -1,493 +1,10 @@ -import type { Registry } from "@polkadot/types-codec/types" -import { decorateStorage, Metadata, StorageKey, TypeRegistry } from "@polkadot/types" -import { ChainConnector } from "@talismn/chain-connector" -import { Chain, ChainId, ChainList, TokenList } from "@talismn/chaindata-provider" -import { $metadataV14, getShape } from "@talismn/scale" -import * as $ from "@talismn/subshape-fork" -import { hasOwnProperty } from "@talismn/util" -import camelCase from "lodash/camelCase" -import groupBy from "lodash/groupBy" - -import { - BalanceModule, - DefaultChainMeta, - DefaultModuleConfig, - DefaultTransferParams, - ExtendableChainMeta, - ExtendableModuleConfig, - ExtendableTransferParams, - NewBalanceModule, - SelectableTokenType, - SubscriptionResultWithStatus, -} from "../../BalanceModule" -import log from "../../log" -import { - AddressesByToken, - Balances, - deriveMiniMetadataId, - MiniMetadata, - SubscriptionCallback, - UnsubscribeFn, -} from "../../types" - -/** - * Wraps a BalanceModule's fetch/subscribe methods with a single `balances` method. - * This `balances` method will subscribe if a callback parameter is provided, or otherwise fetch. - */ -export async function balances< - TModuleType extends string, - TTokenType extends SelectableTokenType, - TChainMeta extends ExtendableChainMeta = DefaultChainMeta, - TModuleConfig extends ExtendableModuleConfig = DefaultModuleConfig, - TTransferParams extends ExtendableTransferParams = DefaultTransferParams ->( - balanceModule: BalanceModule, - addressesByToken: AddressesByToken -): Promise -export async function balances< - TModuleType extends string, - TTokenType extends SelectableTokenType, - TChainMeta extends ExtendableChainMeta = DefaultChainMeta, - TModuleConfig extends ExtendableModuleConfig = DefaultModuleConfig, - TTransferParams extends ExtendableTransferParams = DefaultTransferParams ->( - balanceModule: BalanceModule, - addressesByToken: AddressesByToken, - callback: SubscriptionCallback -): Promise -export async function balances< - TModuleType extends string, - TTokenType extends SelectableTokenType, - TChainMeta extends ExtendableChainMeta = DefaultChainMeta, - TModuleConfig extends ExtendableModuleConfig = DefaultModuleConfig, - TTransferParams extends ExtendableTransferParams = DefaultTransferParams ->( - balanceModule: BalanceModule, - addressesByToken: AddressesByToken, - callback?: SubscriptionCallback -): Promise { - // subscription request - if (callback !== undefined) - return await balanceModule.subscribeBalances({ addressesByToken }, callback) - - // one-off request - return await balanceModule.fetchBalances(addressesByToken) -} - -export type GetOrCreateTypeRegistry = (chainId: ChainId, metadataRpc?: `0x${string}`) => Registry - -export const createTypeRegistryCache = () => { - const typeRegistryCache: Map< - ChainId, - { metadataRpc: `0x${string}` | undefined; typeRegistry: Registry } - > = new Map() - - const getOrCreateTypeRegistry: GetOrCreateTypeRegistry = (chainId, metadataRpc) => { - const cached = typeRegistryCache.get(chainId) - if (cached && cached.metadataRpc === metadataRpc) return cached.typeRegistry - - const typeRegistry = new TypeRegistry() - if (typeof metadataRpc === "string") { - try { - const metadata = new Metadata(typeRegistry, metadataRpc) - metadata.registry.setMetadata(metadata) - } catch (cause) { - log.warn(new Error(`Failed to set metadata for chain ${chainId}`, { cause }), cause) - } - } - - typeRegistryCache.set(chainId, { metadataRpc, typeRegistry }) - - return typeRegistry - } - - return { getOrCreateTypeRegistry } -} - -// eslint-disable-next-line @typescript-eslint/no-explicit-any -export type AnyBalanceModule = BalanceModule -// eslint-disable-next-line @typescript-eslint/no-explicit-any -export type AnyNewBalanceModule = NewBalanceModule - -/** - * The following `Infer*` collection of generic types can be used when you want to - * extract one of the generic type arguments from an existing BalanceModule. - * - * For example, you might want to write a function which can accept any BalanceModule - * as an input, and then return the specific TokenType for that module: - * function getTokens(module: T): InferTokenType - * - * Or for another example, you might want a function which can take any BalanceModule `type` - * string as input, and then return some data associated with that module with the correct type: - * function getChainMeta(type: InferModuleType): InferChainMeta | undefined - */ -type InferBalanceModuleTypes = T extends NewBalanceModule< - infer TModuleType, - infer TTokenType, - infer TChainMeta, - infer TModuleConfig, - infer TTransferParams -> - ? { - TModuleType: TModuleType - TTokenType: TTokenType - TChainMeta: TChainMeta - TModuleConfig: TModuleConfig - TTransferParams: TTransferParams - } - : never -export type InferModuleType = - InferBalanceModuleTypes["TModuleType"] -export type InferTokenType = InferBalanceModuleTypes["TTokenType"] -export type InferChainMeta = InferBalanceModuleTypes["TChainMeta"] -export type InferModuleConfig = - InferBalanceModuleTypes["TModuleConfig"] -export type InferTransferParams = - InferBalanceModuleTypes["TTransferParams"] - -/** - * Given a `moduleType` and a `chain` from a chaindataProvider, this function will find the chainMeta - * associated with the given balanceModule for the given chain. - */ -export const findChainMeta = ( - miniMetadatas: Map, - moduleType: InferModuleType, - chain?: Chain -): [InferChainMeta | undefined, MiniMetadata | undefined] => { - if (!chain) return [undefined, undefined] - if (!chain.specName) return [undefined, undefined] - if (!chain.specVersion) return [undefined, undefined] - - // TODO: This is spaghetti to import this here, it should be injected into each balance module or something. - const metadataId = deriveMiniMetadataId({ - source: moduleType, - chainId: chain.id, - specName: chain.specName, - specVersion: chain.specVersion, - balancesConfig: JSON.stringify( - chain.balancesConfig?.find((config) => config.moduleType === moduleType)?.moduleConfig ?? {} - ), - }) - - // TODO: Fix this (needs to fetch miniMetadata without being async) - const miniMetadata = miniMetadatas.get(metadataId) - const chainMeta: InferChainMeta | undefined = miniMetadata - ? { - miniMetadata: miniMetadata.data, - metadataVersion: miniMetadata.version, - ...JSON.parse(miniMetadata.extra), - } - : undefined - - return [chainMeta, miniMetadata] -} - -/** - * Used by a variety of balance modules to help encode and decode substrate state calls. - */ -export class StorageHelper { - #registry - #storageKey - - #module - #method - #parameters - - // eslint-disable-next-line @typescript-eslint/no-explicit-any - tags: any = null - - // eslint-disable-next-line @typescript-eslint/no-explicit-any - constructor(registry: Registry, module: string, method: string, ...parameters: any[]) { - this.#registry = registry - - this.#module = module - this.#method = method - this.#parameters = parameters - - const _metadataVersion = 0 // is not used inside the decorateStorage function - let query - try { - query = decorateStorage(registry, registry.metadata, _metadataVersion) - } catch (error) { - log.debug(`Failed to decorate storage: ${(error as Error).message}`) - this.#storageKey = null - } - - try { - if (!query) throw new Error(`decoratedStorage unavailable`) - this.#storageKey = new StorageKey( - registry, - parameters ? [query[module][method], parameters] : query[module][method] - ) - } catch (error) { - log.debug( - `Failed to create storageKey ${module || "unknown"}.${method || "unknown"}: ${ - (error as Error).message - }` - ) - this.#storageKey = null - } - } - - get stateKey() { - return this.#storageKey?.toHex() - } - - get module() { - return this.#module - } - get method() { - return this.#method - } - get parameters() { - return this.#parameters - } - - // eslint-disable-next-line @typescript-eslint/no-explicit-any - tag(tags: any) { - this.tags = tags - return this - } - - decode(input?: string | null) { - if (!this.#storageKey) return - return this.#decodeStorageScaleResponse(this.#registry, this.#storageKey, input) - } - - #decodeStorageScaleResponse( - typeRegistry: Registry, - storageKey: StorageKey, - input?: string | null - ) { - if (input === undefined || input === null) return - - const type = storageKey.outputType || "Raw" - const meta = storageKey.meta || { - fallback: undefined, - modifier: { isOptional: true }, - type: { asMap: { linked: { isTrue: false } }, isMap: false }, - } - - try { - return typeRegistry.createTypeUnsafe( - type, - [ - meta.modifier.isOptional - ? typeRegistry.createTypeUnsafe(type, [input], { isPedantic: true }) - : input, - ], - { isOptional: meta.modifier.isOptional, isPedantic: !meta.modifier.isOptional } - ) - } catch (error) { - throw new Error( - `Unable to decode storage ${storageKey.section || "unknown"}.${ - storageKey.method || "unknown" - }: ${(error as Error).message}` - ) - } - } -} - -/** - * Pass some these into an `RpcStateQueryHelper` in order to easily batch multiple state queries into the one rpc call. - */ -export type RpcStateQuery = { - chainId: string - stateKey: string - decodeResult: (change: string | null) => T -} - -/** - * Used by a variety of balance modules to help batch multiple state queries into the one rpc call. - */ -export class RpcStateQueryHelper { - #chainConnector: ChainConnector - #queries: Array> - - constructor(chainConnector: ChainConnector, queries: Array>) { - this.#chainConnector = chainConnector - this.#queries = queries - } - - async subscribe( - callback: SubscriptionCallback, - timeout: number | false = false, - subscribeMethod = "state_subscribeStorage", - responseMethod = "state_storage", - unsubscribeMethod = "state_unsubscribeStorage" - ): Promise { - const queriesByChain = groupBy(this.#queries, "chainId") - const subscriptions = Object.entries(queriesByChain).map(([chainId, queries]) => { - const params = [queries.map(({ stateKey }) => stateKey)] - - const unsub = this.#chainConnector.subscribe( - chainId, - subscribeMethod, - responseMethod, - params, - (error, result) => { - error - ? callback(error) - : callback(null, this.#distributeChangesToQueryDecoders.call(this, chainId, result)) - }, - timeout - ) - - return () => unsub.then((unsubscribe) => unsubscribe(unsubscribeMethod)) - }) - - return () => subscriptions.forEach((unsubscribe) => unsubscribe()) - } - - async fetch(method = "state_queryStorageAt"): Promise { - const queriesByChain = groupBy(this.#queries, "chainId") - - const resultsByChain = await Promise.all( - Object.entries(queriesByChain).map(async ([chainId, queries]) => { - const params = [queries.map(({ stateKey }) => stateKey)] - - const result = (await this.#chainConnector.send(chainId, method, params))[0] - return this.#distributeChangesToQueryDecoders.call(this, chainId, result) - }) - ) - - return resultsByChain.flatMap((result) => result) - } - - #distributeChangesToQueryDecoders(chainId: ChainId, result: unknown): T[] { - if (typeof result !== "object" || result === null) return [] - if (!hasOwnProperty(result, "changes") || typeof result.changes !== "object") return [] - if (!Array.isArray(result.changes)) return [] - - return result.changes.flatMap(([reference, change]: [unknown, unknown]): [T] | [] => { - if (typeof reference !== "string") { - log.warn(`Received non-string reference in RPC result: ${reference}`) - return [] - } - - if (typeof change !== "string" && change !== null) { - log.warn(`Received non-string and non-null change in RPC result: ${reference} | ${change}`) - return [] - } - - const query = this.#queries.find( - ({ chainId: cId, stateKey }) => cId === chainId && stateKey === reference - ) - if (!query) { - log.warn( - `Failed to find query:\n${reference} in\n${this.#queries.map(({ stateKey }) => stateKey)}` - ) - return [] - } - - return [query.decodeResult(change)] - }) - } -} - -export const subshapeStorageDecoder = - // eslint-disable-next-line @typescript-eslint/no-explicit-any - (miniMetadata: MiniMetadata, module: string, method: string) => { - if (miniMetadata.version !== 14) { - log.warn( - `Cannot decode metadata version ${miniMetadata.version} (${miniMetadata.chainId} ${miniMetadata.source})` - ) - return - } - - module = camelCase(module) - method = camelCase(method) - - if (miniMetadata.data === null) return - const metadata = $metadataV14.decode($.decodeHex(miniMetadata.data)) - - const typeId = metadata.pallets - .find((pallet) => camelCase(pallet.name) === module) - ?.storage?.entries?.find((entry) => camelCase(entry.name) === method)?.value - - if (!typeId) { - log.warn( - `Type definition not found in metadata for ${module}::${method} (${miniMetadata.chainId} ${miniMetadata.source})` - ) - return - } - - return getShape(metadata, typeId) - } - -export const getUniqueChainIds = ( - addressesByToken: AddressesByToken<{ id: string }>, - tokens: TokenList -): ChainId[] => [ - ...new Set( - Object.keys(addressesByToken) - .map((tokenId) => tokens[tokenId]?.chain?.id) - .flatMap((chainId) => (chainId ? [chainId] : [])) - ), -] - -type StorageDecoderParams< - TBalanceModule extends AnyNewBalanceModule, - TDecoders extends { [key: string]: [string, string] } -> = { - chains: ChainList - miniMetadatas: Map - moduleType: InferModuleType - decoders: TDecoders -} - -export type StorageDecoders = Map> - -export const buildStorageDecoders = < - TBalanceModule extends AnyNewBalanceModule, - TDecoders extends { [key: string]: [string, string] } ->({ - chains, - miniMetadatas, - moduleType, - decoders, -}: StorageDecoderParams): StorageDecoders => - new Map( - Object.entries(chains).flatMap(([chainId, chain]) => { - const [, miniMetadata] = findChainMeta(miniMetadatas, moduleType, chain) - if (!miniMetadata) return [] - if (miniMetadata.version < 14) return [] - - const builtDecoders = Object.fromEntries( - Object.entries(decoders).map( - ([key, [module, method]]: [keyof TDecoders, [string, string]]) => - [key, subshapeStorageDecoder(miniMetadata, module, method)] as const - ) - ) as { [Property in keyof TDecoders]: $.AnyShape | undefined } - - return [[chainId, builtDecoders]] - }) - ) - -/** - * - * Detect Balances::transfer -> Balances::transfer_allow_death migration - * https://github.com/paritytech/substrate/pull/12951 - * - * `transfer_allow_death` is the preferred method, - * so if something goes wrong during detection, we should assume the chain has migrated - * @param metadataRpc string containing the hashed RPC metadata for the chain - * @returns - */ -export const detectTransferMethod = (metadataRpc: `0x${string}`) => { - const pjsMetadata: Metadata = new Metadata(new TypeRegistry(), metadataRpc) - pjsMetadata.registry.setMetadata(pjsMetadata) - const balancesPallet = pjsMetadata.asLatest.pallets.find((pallet) => pallet.name.eq("Balances")) - - const balancesCallsTypeIndex = balancesPallet?.calls.value.type.toNumber() - const balancesCallsType = - balancesCallsTypeIndex !== undefined - ? pjsMetadata.asLatest.lookup.types[balancesCallsTypeIndex] - : undefined - const hasDeprecatedTransferCall = - balancesCallsType?.type.def.asVariant?.variants.find((variant) => - variant.name.eq("transfer") - ) !== undefined - - return hasDeprecatedTransferCall ? "transfer" : "transferAllowDeath" -} +export * from "./balances" +export * from "./buildStorageCoders" +export * from "./decodeOutput" +export * from "./detectTransferMethod" +export * from "./findChainMeta" +export * from "./getUniqueChainIds" +export * from "./InferBalanceModuleTypes" +export * from "./makeContractCaller" +export * from "./RpcStateQueryHelper" +export * from "./storageCompression" diff --git a/packages/balances/src/modules/util/storage.ts b/packages/balances/src/modules/util/storageCompression.ts similarity index 100% rename from packages/balances/src/modules/util/storage.ts rename to packages/balances/src/modules/util/storageCompression.ts diff --git a/packages/balances/src/types/minimetadatas.ts b/packages/balances/src/types/minimetadatas.ts index 3d6dafaa84..e095102052 100644 --- a/packages/balances/src/types/minimetadatas.ts +++ b/packages/balances/src/types/minimetadatas.ts @@ -1,6 +1,6 @@ import { u8aToHex } from "@polkadot/util" +import { xxhashAsU8a } from "@polkadot/util-crypto" import { ChainId } from "@talismn/chaindata-provider" -import { twox64 } from "@talismn/scale" /** For fast db access, you can calculate the primary key for a miniMetadata using this method */ export const deriveMiniMetadataId = ({ @@ -14,8 +14,9 @@ export const deriveMiniMetadataId = ({ "source" | "chainId" | "specName" | "specVersion" | "balancesConfig" >): string => u8aToHex( - twox64.hash( - new TextEncoder().encode(`${source}${chainId}${specName}${specVersion}${balancesConfig}`) + xxhashAsU8a( + new TextEncoder().encode(`${source}${chainId}${specName}${specVersion}${balancesConfig}`), + 64 ), undefined, false diff --git a/packages/chaindata-provider/src/constants.ts b/packages/chaindata-provider/src/constants.ts index 4126252f53..f6b135788f 100644 --- a/packages/chaindata-provider/src/constants.ts +++ b/packages/chaindata-provider/src/constants.ts @@ -12,7 +12,7 @@ export const githubApi = "https://api.github.com" export const githubChaindataOrg = "TalismanSociety" export const githubChaindataRepo = "chaindata" export const githubChaindataBranch = CHAINDATA_BRANCH -export const githubChaindataDistDir = "pub/v1" +export const githubChaindataDistDir = "pub/v2" export const githubChaindataBaseUrl = `https://raw.githubusercontent.com/${githubChaindataOrg}/${githubChaindataRepo}/${githubChaindataBranch}` export const githubChaindataDistUrl = `${githubChaindataBaseUrl}/${githubChaindataDistDir}` diff --git a/packages/extension-core/src/background.ts b/packages/extension-core/src/background.ts index 8173eac6fc..dbf2ca709e 100644 --- a/packages/extension-core/src/background.ts +++ b/packages/extension-core/src/background.ts @@ -2,7 +2,6 @@ import { AccountsStore } from "@polkadot/extension-base/stores" import keyring from "@polkadot/ui-keyring" import { assert } from "@polkadot/util" import { cryptoWaitReady } from "@polkadot/util-crypto" -import { watCryptoWaitReady } from "@talismn/scale" import { DEBUG, PORT_CONTENT, PORT_EXTENSION } from "extension-shared" import { sentry } from "./config/sentry" @@ -93,12 +92,9 @@ chrome.runtime.onConnect.addListener((_port): void => { !DEBUG && chrome.runtime.setUninstallURL("https://goto.talisman.xyz/uninstall") // initial setup -Promise.all([ - // wait for `@polkadot/util-crypto` to be ready (it needs to load some wasm) - cryptoWaitReady(), - // wait for `@talismn/scale` to be ready (it needs to load some wasm) - watCryptoWaitReady(), -]) +// +// wait for `@polkadot/util-crypto` to be ready (it needs to load some wasm) +cryptoWaitReady() .then((): void => { // load all the keyring data keyring.loadAll({ diff --git a/packages/extension-core/src/domains/accounts/helpers.onChainIds.ts b/packages/extension-core/src/domains/accounts/helpers.onChainIds.ts index 3733c5a4d5..fc0d8e17e3 100644 --- a/packages/extension-core/src/domains/accounts/helpers.onChainIds.ts +++ b/packages/extension-core/src/domains/accounts/helpers.onChainIds.ts @@ -3,7 +3,6 @@ import { OnChainId, OnChainIds, ResolvedNames } from "@talismn/on-chain-id" import { chainConnectors } from "../../rpcs/balance-modules" import { getTypeRegistry } from "../../util/getTypeRegistry" -const chainIdPolkadot = "polkadot" const chainIdAlephZero = "aleph-zero" const aznsSupportedChainIdAlephZero = "alephzero" @@ -14,17 +13,12 @@ export const lookupAddresses = async (addresses: string[]): Promise (await getOnChainId()).lookupAddresses(addresses) const getOnChainId = async () => { - const [{ registry: registryPolkadot }, { registry: registryAlephZero }] = await Promise.all([ - getTypeRegistry(chainIdPolkadot), - getTypeRegistry(chainIdAlephZero), - ]) + const { registry: registryAlephZero } = await getTypeRegistry(chainIdAlephZero) return new OnChainId({ - registryPolkadot, registryAlephZero, chainConnectors, - chainIdPolkadot, chainIdAlephZero, aznsSupportedChainIdAlephZero, }) diff --git a/packages/extension-core/src/domains/app/__tests__/handler.spec.ts b/packages/extension-core/src/domains/app/__tests__/handler.spec.ts index 0ecc115998..260eb3aa33 100644 --- a/packages/extension-core/src/domains/app/__tests__/handler.spec.ts +++ b/packages/extension-core/src/domains/app/__tests__/handler.spec.ts @@ -4,14 +4,13 @@ import keyring from "@polkadot/ui-keyring" import { KeyringPairs$Json } from "@polkadot/ui-keyring/types" import { assert } from "@polkadot/util" import { cryptoWaitReady } from "@polkadot/util-crypto" -import { watCryptoWaitReady } from "@talismn/scale" import { getMessageSenderFn } from "../../../../tests/util" import Extension from "../../../handlers/Extension" import { - GettableStoreData, extensionStores, getLocalStorage, + GettableStoreData, setLocalStorage, } from "../../../handlers/stores" @@ -33,12 +32,8 @@ describe("App handler when password is not trimmed", () => { let mnemonicId: string async function createExtension(): Promise { - await Promise.all([ - // wait for `@polkadot/util-crypto` to be ready (it needs to load some wasm) - cryptoWaitReady(), - // wait for `@talismn/scale` to be ready (it needs to load some wasm) - watCryptoWaitReady(), - ]) + // wait for `@polkadot/util-crypto` to be ready (it needs to load some wasm) + await cryptoWaitReady() return new Extension(extensionStores) } @@ -186,12 +181,8 @@ describe("App handler when password is trimmed", () => { let mnemonicId: string async function createExtension(): Promise { - await Promise.all([ - // wait for `@polkadot/util-crypto` to be ready (it needs to load some wasm) - cryptoWaitReady(), - // wait for `@talismn/scale` to be ready (it needs to load some wasm) - watCryptoWaitReady(), - ]) + // wait for `@polkadot/util-crypto` to be ready (it needs to load some wasm) + await cryptoWaitReady() return new Extension(extensionStores) } diff --git a/packages/extension-core/src/domains/balances/pool.ts b/packages/extension-core/src/domains/balances/pool.ts index b6b1329bec..ef43072174 100644 --- a/packages/extension-core/src/domains/balances/pool.ts +++ b/packages/extension-core/src/domains/balances/pool.ts @@ -3,11 +3,11 @@ import { SingleAddress } from "@polkadot/ui-keyring/observable/types" import { assert } from "@polkadot/util" import { AddressesByToken, + db as balancesDb, + configureStore, MiniMetadata, StoredBalanceJson, - db as balancesDb, } from "@talismn/balances" -import { configureStore } from "@talismn/balances" import { Token } from "@talismn/chaindata-provider" import { Deferred, encodeAnyAddress, isEthereumAddress } from "@talismn/util" import { firstThenDebounce } from "@talismn/util/src/firstThenDebounce" @@ -18,12 +18,12 @@ import omit from "lodash/omit" import pick from "lodash/pick" import { BehaviorSubject, + combineLatest, + firstValueFrom, Observable, ReplaySubject, Subject, Subscription, - combineLatest, - firstValueFrom, } from "rxjs" import { debounceTime, map } from "rxjs/operators" @@ -45,8 +45,8 @@ import { AddressesAndTokens, Balance, BalanceJson, - BalanceSubscriptionResponse, Balances, + BalanceSubscriptionResponse, RequestBalance, RequestBalancesByParamsSubscribe, } from "./types" @@ -145,12 +145,11 @@ abstract class BalancePool { this.#persist = Boolean(persist) // check for use outside of the background/service worker - isBackgroundPage().then((backgroudPage) => { - if (!backgroudPage) { - throw new Error( - `Balances pool should only be used in the background page - used in: ${window.location.href}` - ) - } + isBackgroundPage().then((backgroundPage) => { + if (backgroundPage) return + throw new Error( + `Balances pool should only be used in the background page - used in: ${window.location.href}` + ) }) // subscribe this store to all of the inputs it depends on diff --git a/packages/extension-core/src/domains/signing/__tests__/requestsStore.spec.ts b/packages/extension-core/src/domains/signing/__tests__/requestsStore.spec.ts index 8d6c7e0060..18ecf86565 100644 --- a/packages/extension-core/src/domains/signing/__tests__/requestsStore.spec.ts +++ b/packages/extension-core/src/domains/signing/__tests__/requestsStore.spec.ts @@ -1,9 +1,8 @@ +import type { SignerPayloadJSON } from "@polkadot/types/types" import RequestExtrinsicSign from "@polkadot/extension-base/background/RequestExtrinsicSign" import { AccountsStore } from "@polkadot/extension-base/stores" -import type { SignerPayloadJSON } from "@polkadot/types/types" import keyring from "@polkadot/ui-keyring" import { cryptoWaitReady } from "@polkadot/util-crypto" -import { watCryptoWaitReady } from "@talismn/scale" import { waitFor } from "@testing-library/dom" import { AccountType } from "../../../domains/accounts/types" @@ -34,12 +33,8 @@ jest.mock("../../../libs/WindowManager", () => { describe("Signing requests store", () => { beforeAll(async () => { - await Promise.all([ - // wait for `@polkadot/util-crypto` to be ready (it needs to load some wasm) - cryptoWaitReady(), - // wait for `@talismn/scale` to be ready (it needs to load some wasm) - watCryptoWaitReady(), - ]) + // wait for `@polkadot/util-crypto` to be ready (it needs to load some wasm) + await cryptoWaitReady() keyring.loadAll({ store: new AccountsStore() }) }) diff --git a/packages/extension-core/src/domains/sitesAuthorised/__tests__/handler.spec.ts b/packages/extension-core/src/domains/sitesAuthorised/__tests__/handler.spec.ts index 0aa092c6b7..f1e06f58cc 100644 --- a/packages/extension-core/src/domains/sitesAuthorised/__tests__/handler.spec.ts +++ b/packages/extension-core/src/domains/sitesAuthorised/__tests__/handler.spec.ts @@ -2,7 +2,6 @@ import { AccountsStore } from "@polkadot/extension-base/stores" import keyring from "@polkadot/ui-keyring" import { cryptoWaitReady } from "@polkadot/util-crypto" -import { watCryptoWaitReady } from "@talismn/scale" /* eslint-disable @typescript-eslint/no-non-null-assertion */ // import Extension from "./Extension" import { TALISMAN_WEB_APP_DOMAIN } from "extension-shared" @@ -27,12 +26,8 @@ describe("Sites Authorised Handler", () => { let mnemonicId: string async function createExtension(): Promise { - await Promise.all([ - // wait for `@polkadot/util-crypto` to be ready (it needs to load some wasm) - cryptoWaitReady(), - // wait for `@talismn/scale` to be ready (it needs to load some wasm) - watCryptoWaitReady(), - ]) + // wait for `@polkadot/util-crypto` to be ready (it needs to load some wasm) + await cryptoWaitReady() return new Extension(extensionStores) } diff --git a/packages/extension-core/src/handlers/Extension.spec.ts b/packages/extension-core/src/handlers/Extension.spec.ts index c37fec0f87..15fafc985c 100644 --- a/packages/extension-core/src/handlers/Extension.spec.ts +++ b/packages/extension-core/src/handlers/Extension.spec.ts @@ -1,13 +1,12 @@ -import RequestExtrinsicSign from "@polkadot/extension-base/background/RequestExtrinsicSign" -import { AccountsStore } from "@polkadot/extension-base/stores" import type { MetadataDef } from "@polkadot/extension-inject/types" import type { KeyringPair } from "@polkadot/keyring/types" -import { TypeRegistry } from "@polkadot/types" import type { ExtDef } from "@polkadot/types/extrinsic/signedExtensions/types" import type { SignerPayloadJSON } from "@polkadot/types/types" +import RequestExtrinsicSign from "@polkadot/extension-base/background/RequestExtrinsicSign" +import { AccountsStore } from "@polkadot/extension-base/stores" +import { TypeRegistry } from "@polkadot/types" import keyring from "@polkadot/ui-keyring" import { cryptoWaitReady, signatureVerify } from "@polkadot/util-crypto" -import { watCryptoWaitReady } from "@talismn/scale" import { waitFor } from "@testing-library/dom" import { TALISMAN_WEB_APP_DOMAIN } from "extension-shared" @@ -43,12 +42,8 @@ describe("Extension", () => { let mnemonicId: string async function createExtension(): Promise { - await Promise.all([ - // wait for `@polkadot/util-crypto` to be ready (it needs to load some wasm) - cryptoWaitReady(), - // wait for `@talismn/scale` to be ready (it needs to load some wasm) - watCryptoWaitReady(), - ]) + // wait for `@polkadot/util-crypto` to be ready (it needs to load some wasm) + await cryptoWaitReady() keyring.loadAll({ store: new AccountsStore() }) diff --git a/packages/extension-core/src/libs/migrations/legacyMigrations.spec.ts b/packages/extension-core/src/libs/migrations/legacyMigrations.spec.ts index 4e1af3b1f2..5be14ed180 100644 --- a/packages/extension-core/src/libs/migrations/legacyMigrations.spec.ts +++ b/packages/extension-core/src/libs/migrations/legacyMigrations.spec.ts @@ -1,7 +1,6 @@ import { AccountsStore } from "@polkadot/extension-base/stores" import keyring from "@polkadot/ui-keyring" import { cryptoWaitReady } from "@polkadot/util-crypto" -import { watCryptoWaitReady } from "@talismn/scale" import { LegacyAccountType as AccountType, @@ -43,12 +42,8 @@ const createPair = ( describe("App migrations", () => { beforeAll(async () => { - await Promise.all([ - // wait for `@polkadot/util-crypto` to be ready (it needs to load some wasm) - cryptoWaitReady(), - // wait for `@talismn/scale` to be ready (it needs to load some wasm) - watCryptoWaitReady(), - ]) + // wait for `@polkadot/util-crypto` to be ready (it needs to load some wasm) + await cryptoWaitReady() keyring.loadAll({ store: new AccountsStore() }) }) diff --git a/packages/extension-core/src/util/awaitKeyringLoaded.ts b/packages/extension-core/src/util/awaitKeyringLoaded.ts index fc13cff59c..c71668c298 100644 --- a/packages/extension-core/src/util/awaitKeyringLoaded.ts +++ b/packages/extension-core/src/util/awaitKeyringLoaded.ts @@ -1,6 +1,5 @@ import keyring from "@polkadot/ui-keyring" import { cryptoWaitReady } from "@polkadot/util-crypto" -import { watCryptoWaitReady } from "@talismn/scale" /** * @function awaitKeyringLoaded @@ -8,13 +7,9 @@ import { watCryptoWaitReady } from "@talismn/scale" * This function is used to wait for the keyring to be loaded. It returns a promise which resolves to true once all accounts have been loaded into the keyring. */ export const awaitKeyringLoaded = async () => { - // the keyring does funky stuff when we try and access it before these are ready - await Promise.all([ - // wait for `@polkadot/util-crypto` to be ready (it needs to load some wasm) - cryptoWaitReady(), - // wait for `@talismn/scale` to be ready (it needs to load some wasm) - watCryptoWaitReady(), - ]) + // the keyring does funky stuff when we try and access it before this is ready + // wait for `@polkadot/util-crypto` to be ready (it needs to load some wasm) + await cryptoWaitReady() return new Promise((resolve) => { const keyringSubscription = keyring.accounts.subject.subscribe(async (addresses) => { diff --git a/packages/on-chain-id/src/index.ts b/packages/on-chain-id/src/index.ts index 2d378b098a..35a29c9f03 100644 --- a/packages/on-chain-id/src/index.ts +++ b/packages/on-chain-id/src/index.ts @@ -1,9 +1,4 @@ -import { - lookupAddresses, - lookupAznsAddresses, - lookupEnsAddresses, - lookupPolkadotAddresses, -} from "./util/addressesToNames" +import { lookupAddresses, lookupAznsAddresses, lookupEnsAddresses } from "./util/addressesToNames" import { resolveAznsNames, resolveEnsNames, resolveNames } from "./util/namesToAddresses" import { Config, DropFirst, OptionalConfig } from "./util/types" @@ -22,7 +17,6 @@ export class OnChainId { constructor(config: OptionalConfig) { this.#config = { ...config, - chainIdPolkadot: config.chainIdPolkadot ?? "polkadot", chainIdAlephZero: config.chainIdAlephZero ?? "aleph-zero", aznsSupportedChainIdAlephZero: config.aznsSupportedChainIdAlephZero ?? "alephzero", networkIdEthereum: config.networkIdEthereum ?? "1", @@ -38,8 +32,6 @@ export class OnChainId { lookupAddresses = (...args: DropFirst>) => lookupAddresses(this.#config, ...args) - lookupPolkadotAddresses = (...args: DropFirst>) => - lookupPolkadotAddresses(this.#config, ...args) lookupAznsAddresses = (...args: DropFirst>) => lookupAznsAddresses(this.#config, ...args) lookupEnsAddresses = (...args: DropFirst>) => diff --git a/packages/on-chain-id/src/util/addressesToNames.ts b/packages/on-chain-id/src/util/addressesToNames.ts index 3d24cd6e15..76a5b59439 100644 --- a/packages/on-chain-id/src/util/addressesToNames.ts +++ b/packages/on-chain-id/src/util/addressesToNames.ts @@ -1,33 +1,22 @@ import { resolveAddressToDomain } from "@azns/resolver-core" import { ApiPromise } from "@polkadot/api" -import { RpcStateQuery, RpcStateQueryHelper, StorageHelper } from "@talismn/balances" -import { decodeAnyAddress, isEthereumAddress } from "@talismn/util" +import { isEthereumAddress } from "@talismn/util" import log from "../log" import { Config, OnChainIds } from "./types" /** * Looks up the on-chain identifiers for some addresses. - * - * Prefers ENS, then AZNS, then falls back to Polkadot identities. - * - * Requires a TypeRegistry which has been instantiated on the Polkadot relay chain. - * Talisman Wallet developers can build one by using `/apps/extension/src/core/util/getTypeRegistry.ts`. + * Supports ENS and AZNS. */ export const lookupAddresses = async (config: Config, addresses: string[]): Promise => { const onChainIds: OnChainIds = new Map(addresses.map((address) => [address, null])) - const [/* polkadotIdentities, */ aznsDomains, ensDomains] = await Promise.all([ - // lookupPolkadotAddresses(config, addresses), + const [aznsDomains, ensDomains] = await Promise.all([ lookupAznsAddresses(config, addresses), lookupEnsAddresses(config, addresses), ]) - // polkadotIdentities.forEach((polkadotIdentity, address) => { - // if (!polkadotIdentity) return - // onChainIds.set(address, polkadotIdentity) - // }) - aznsDomains.forEach((domain, address) => { if (!domain) return onChainIds.set(address, domain) @@ -41,80 +30,6 @@ export const lookupAddresses = async (config: Config, addresses: string[]): Prom return onChainIds } -/** - * Looks up the on-chain Polkadot identities for some addresses. - * - * Requires a TypeRegistry which has been instantiated on the Polkadot relay chain. - * Talisman Wallet developers can build one by using `/apps/extension/src/core/util/getTypeRegistry.ts`. - */ -export const lookupPolkadotAddresses = async ( - config: Config, - addresses: string[] -): Promise => { - const onChainIds: OnChainIds = new Map(addresses.map((address) => [address, null])) - - if (!config.chainConnectors.substrate) { - log.warn(`Could not find Substrate chainConnector in OnChainId::lookupPolkadotAddresses`) - return onChainIds - } - - const queries = addresses.flatMap((address): RpcStateQuery<[string, string | null]> | [] => { - const storageHelper = new StorageHelper( - config.registryPolkadot, - "identity", - "identityOf", - decodeAnyAddress(address) - ) - - // filter out queries which we failed to encode (e.g. an invalid address was input) - const stateKey = storageHelper.stateKey - if (!stateKey) return [] - - const decodeResult = (change: string | null): [string, string | null] => { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const decoded: any = storageHelper.decode(change) - - // explicit null is required here to ensure the frontend knows that the address has been queried - const bytes = decoded?.value?.info?.display?.value - const bytesDecoded = new TextDecoder().decode(bytes) - - const judgements: string[] = - // eslint-disable-next-line @typescript-eslint/no-explicit-any - decoded?.value?.judgements?.flatMap?.((judgement: any) => { - if (judgement?.[1]?.isErroneous) return "Erroneous" - if (judgement?.[1]?.isFeePaid) return "FeePaid" - if (judgement?.[1]?.isKnownGood) return "KnownGood" - if (judgement?.[1]?.isLowQuality) return "LowQuality" - if (judgement?.[1]?.isOutOfDate) return "OutOfDate" - if (judgement?.[1]?.isReasonable) return "Reasonable" - if (judgement?.[1]?.isUnknown) return "Unknown" - - log.warn(`Unknown judgement type ${judgement?.toJSON?.() ?? String(judgement)}`) - return [] - }) ?? [] - if (judgements.length < 1) judgements.push("NoJudgement") - - const display = bytes ? `${bytesDecoded} (${judgements.join(", ")})` : null - - return [address, display] - } - - return { chainId: config.chainIdPolkadot, stateKey, decodeResult } - }) - - const identities = await new RpcStateQueryHelper( - config.chainConnectors.substrate, - queries - ).fetch() - - identities.forEach(([address, polkadotIdentity]) => { - if (!polkadotIdentity) return - onChainIds.set(address, polkadotIdentity) - }) - - return onChainIds -} - /** * Looks up the on-chain AZNS domains for some addresses. */ diff --git a/packages/on-chain-id/src/util/types.ts b/packages/on-chain-id/src/util/types.ts index 9f7a54d30b..9ba0916f73 100644 --- a/packages/on-chain-id/src/util/types.ts +++ b/packages/on-chain-id/src/util/types.ts @@ -17,12 +17,9 @@ export type NsLookupType = "ens" | "azns" export type Config = { // TODO: Create a package for `/apps/extension/src/core/util/getTypeRegistry.ts` which // can be used from outside of the wallet. - registryPolkadot: TypeRegistry registryAlephZero: TypeRegistry chainConnectors: ChainConnectors - /** Used for polkadot identity lookups */ - chainIdPolkadot: string /** Used for azns lookups */ chainIdAlephZero: string /** Used for azns lookups */ @@ -32,7 +29,6 @@ export type Config = { } export type OptionalConfigParams = - | "chainIdPolkadot" | "chainIdAlephZero" | "aznsSupportedChainIdAlephZero" | "networkIdEthereum" diff --git a/packages/scale/package.json b/packages/scale/package.json index 54a7c34ac1..492572ab5d 100644 --- a/packages/scale/package.json +++ b/packages/scale/package.json @@ -26,14 +26,13 @@ "clean": "rm -rf dist .turbo node_modules" }, "dependencies": { - "@talismn/subshape-fork": "^0.0.2", - "@talismn/util": "workspace:*", + "@polkadot-api/metadata-builders": "^0.2.0", + "@polkadot-api/substrate-bindings": "^0.2.0", + "@polkadot-api/utils": "^0.0.1", "anylogger": "^1.0.11", - "wasm-feature-detect": "^1.6.1", - "wat-the-crypto": "^0.0.3" + "scale-ts": "^1.6.0" }, "devDependencies": { - "@polkadot/util-crypto": "^12.3.2", "@talismn/eslint-config": "workspace:*", "@talismn/tsconfig": "workspace:*", "@types/jest": "^27.5.1", @@ -42,9 +41,6 @@ "ts-jest": "^29.1.1", "typescript": "^5.2.2" }, - "peerDependencies": { - "@polkadot/util-crypto": "12.x" - }, "eslintConfig": { "root": true, "extends": [ diff --git a/packages/scale/src/capi/README.md b/packages/scale/src/capi/README.md deleted file mode 100644 index de9402026c..0000000000 --- a/packages/scale/src/capi/README.md +++ /dev/null @@ -1,3 +0,0 @@ -# capi folder - -Code in this folder has been copied, and adapted for typescript, from capi and deno std library diff --git a/packages/scale/src/capi/crypto/base58.ts b/packages/scale/src/capi/crypto/base58.ts deleted file mode 100644 index cc7f5fa71e..0000000000 --- a/packages/scale/src/capi/crypto/base58.ts +++ /dev/null @@ -1,194 +0,0 @@ -// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license. -// This module is browser compatible. - -/** - * {@linkcode encode} and {@linkcode decode} for - * [base58](https://en.wikipedia.org/wiki/Binary-to-text_encoding#Base58) encoding. - * - * This module is browser compatible. - * - * @module - */ - -// deno-fmt-ignore -const mapBase58: Record = { - "1": 0, - "2": 1, - "3": 2, - "4": 3, - "5": 4, - "6": 5, - "7": 6, - "8": 7, - "9": 8, - "A": 9, - "B": 10, - "C": 11, - "D": 12, - "E": 13, - "F": 14, - "G": 15, - "H": 16, - "J": 17, - "K": 18, - "L": 19, - "M": 20, - "N": 21, - "P": 22, - "Q": 23, - "R": 24, - "S": 25, - "T": 26, - "U": 27, - "V": 28, - "W": 29, - "X": 30, - "Y": 31, - "Z": 32, - "a": 33, - "b": 34, - "c": 35, - "d": 36, - "e": 37, - "f": 38, - "g": 39, - "h": 40, - "i": 41, - "j": 42, - "k": 43, - "m": 44, - "n": 45, - "o": 46, - "p": 47, - "q": 48, - "r": 49, - "s": 50, - "t": 51, - "u": 52, - "v": 53, - "w": 54, - "x": 55, - "y": 56, - "z": 57, -} - -const base58alphabet = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz".split("") - -/** - * Encodes a given Uint8Array, ArrayBuffer or string into draft-mspotny-base58-03 RFC base58 representation: - * https://tools.ietf.org/id/draft-msporny-base58-01.html#rfc.section.1 - * - * @param data - * - * @returns Encoded value - */ -export function encode(data: ArrayBuffer | string): string { - const uint8tData = - typeof data === "string" - ? new TextEncoder().encode(data) - : data instanceof Uint8Array - ? data - : new Uint8Array(data) - - let length = 0 - let zeroes = 0 - - // Counting leading zeroes - let index = 0 - while (uint8tData[index] === 0) { - zeroes++ - index++ - } - - const notZeroUint8Data = uint8tData.slice(index) - - const size = Math.round((uint8tData.length * 138) / 100 + 1) - const b58Encoding: number[] = [] - - notZeroUint8Data.forEach((byte) => { - let i = 0 - let carry = byte - - for ( - let reverse_iterator = size - 1; - (carry > 0 || i < length) && reverse_iterator !== -1; - reverse_iterator--, i++ - ) { - carry += (b58Encoding[reverse_iterator] || 0) * 256 - b58Encoding[reverse_iterator] = Math.round(carry % 58) - carry = Math.floor(carry / 58) - } - - length = i - }) - - const strResult: string[] = Array.from({ - length: b58Encoding.length + zeroes, - }) - - if (zeroes > 0) { - strResult.fill("1", 0, zeroes) - } - - b58Encoding.forEach((byteValue) => strResult.push(base58alphabet[byteValue])) - - return strResult.join("") -} - -/** - * Decodes a given b58 string according to draft-mspotny-base58-03 RFC base58 representation: - * https://tools.ietf.org/id/draft-msporny-base58-01.html#rfc.section.1 - * - * @param b58 - * - * @returns Decoded value - */ -export function decode(b58: string): Uint8Array { - const splitInput = b58.trim().split("") - - let length = 0 - let ones = 0 - - // Counting leading ones - let index = 0 - while (splitInput[index] === "1") { - ones++ - index++ - } - - const notZeroData = splitInput.slice(index) - - const size = Math.round((b58.length * 733) / 1000 + 1) - const output: number[] = [] - - notZeroData.forEach((char, idx) => { - let carry = mapBase58[char] - let i = 0 - - if (carry === undefined) { - throw new Error(`Invalid base58 char at index ${idx} with value ${char}`) - } - - for ( - let reverse_iterator = size - 1; - (carry > 0 || i < length) && reverse_iterator !== -1; - reverse_iterator--, i++ - ) { - carry += 58 * (output[reverse_iterator] || 0) - output[reverse_iterator] = Math.round(carry % 256) - carry = Math.floor(carry / 256) - } - - length = i - }) - - const validOutput = output.filter((item) => item !== undefined) - - if (ones > 0) { - const onesResult = Array.from({ length: ones }).fill(0, 0, ones) - - return new Uint8Array([...onesResult, ...validOutput] as number[]) - } - - return new Uint8Array(validOutput) -} diff --git a/packages/scale/src/capi/crypto/concat.ts b/packages/scale/src/capi/crypto/concat.ts deleted file mode 100644 index 61ad803403..0000000000 --- a/packages/scale/src/capi/crypto/concat.ts +++ /dev/null @@ -1,26 +0,0 @@ -// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license. -// This module is browser compatible. - -/** Concatenate the given arrays into a new Uint8Array. - * - * ```ts - * import { concat } from "https://deno.land/std@$STD_VERSION/bytes/concat.ts"; - * const a = new Uint8Array([0, 1, 2]); - * const b = new Uint8Array([3, 4, 5]); - * console.log(concat(a, b)); // [0, 1, 2, 3, 4, 5] - */ -export function concat(...buf: Uint8Array[]): Uint8Array { - let length = 0 - for (const b of buf) { - length += b.length - } - - const output = new Uint8Array(length) - let index = 0 - for (const b of buf) { - output.set(b, index) - index += b.length - } - - return output -} diff --git a/packages/scale/src/capi/crypto/hashers.ts b/packages/scale/src/capi/crypto/hashers.ts deleted file mode 100644 index a1a251b3fd..0000000000 --- a/packages/scale/src/capi/crypto/hashers.ts +++ /dev/null @@ -1,154 +0,0 @@ -/** - * Adapted from https://github.com/paritytech/capi-old copyright Parity Technologies (APACHE License 2.0) - * Changes August 19th 2023 : - * - updated to use subshape for scale decoding - * - adapted from deno to typescript - * - Copyright 2023 Parity Technologies - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. - */ - -import * as $ from "@talismn/subshape-fork" -import { EncodeBuffer } from "@talismn/subshape-fork" - -import { Blake2b, Xxhash } from "./util" - -export abstract class Hasher { - abstract create(): Hashing - abstract digestLength: number - abstract concat: boolean - - $hash($inner: $.Shape): $.Shape { - return $hash(this, $inner) - } - - hash(data: Uint8Array): Uint8Array { - const output = new Uint8Array(this.digestLength + (this.concat ? data.length : 0)) - const hashing = this.create() - hashing.update(data) - hashing.digestInto(output) - hashing.dispose?.() - if (this.concat) { - output.set(data, this.digestLength) - } - return output - } -} - -export function $hash(hasher: Hasher, $inner: $.Shape): $.Shape { - return $.createShape({ - metadata: $.metadata("$hash", $hash, hasher, $inner), - staticSize: hasher.digestLength + $inner.staticSize, - subEncode(buffer, value) { - const hashArray = buffer.array.subarray(buffer.index, (buffer.index += hasher.digestLength)) - const cursor = hasher.concat - ? buffer.createCursor($inner.staticSize) - : new EncodeBuffer(buffer.stealAlloc($inner.staticSize)) - $inner.subEncode(cursor, value) - buffer.waitForBuffer(cursor, () => { - if (hasher.concat) (cursor as ReturnType).close() - else cursor._commitWritten() - const hashing = hasher.create() - updateHashing(hashing, cursor) - hashing.digestInto(hashArray) - hashing.dispose?.() - }) - }, - subDecode(buffer) { - if (!hasher.concat) throw new DecodeNonTransparentKeyError() - buffer.index += hasher.digestLength - return $inner.subDecode(buffer) - }, - subAssert(assert) { - $inner.subAssert(assert) - }, - }) -} - -export class Blake2Hasher extends Hasher { - digestLength - constructor(size: 64 | 128 | 256 | 512, public concat: boolean) { - super() - this.digestLength = size / 8 - } - - create(): Hashing { - return new Blake2b(this.digestLength) - } -} - -export class IdentityHasher extends Hasher { - digestLength = 0 - concat = true - - create(): Hashing { - return { - update() {}, - digestInto() {}, - } - } - - override $hash($inner: $.Shape): $.Shape { - return $inner - } - - override hash(data: Uint8Array): Uint8Array { - return data.slice() - } -} - -export class TwoxHasher extends Hasher { - digestLength - rounds - constructor(size: 64 | 128 | 256, public concat: boolean) { - super() - this.digestLength = size / 8 - this.rounds = size / 64 - } - - create(): Hashing { - return new Xxhash(this.rounds) - } -} - -export interface Hashing { - update(data: Uint8Array): void - digestInto(array: Uint8Array): void - dispose?(): void -} - -export const blake2_64 = new Blake2Hasher(64, false) -export const blake2_128 = new Blake2Hasher(128, false) -export const blake2_128Concat = new Blake2Hasher(128, true) -export const blake2_256 = new Blake2Hasher(256, false) -export const blake2_512 = new Blake2Hasher(512, false) -export const identity = new IdentityHasher() -export const twox64 = new TwoxHasher(64, false) -export const twox128 = new TwoxHasher(128, false) -export const twox256 = new TwoxHasher(256, false) -export const twox64Concat = new TwoxHasher(64, true) - -function updateHashing(hashing: Hashing, data: EncodeBuffer) { - for (const array of data.finishedArrays) { - if (array instanceof EncodeBuffer) { - updateHashing(hashing, array) - } else { - hashing.update(array) - } - } -} - -export class DecodeNonTransparentKeyError extends Error { - override readonly name = "DecodeNonTransparentKeyError" -} diff --git a/packages/scale/src/capi/crypto/index.ts b/packages/scale/src/capi/crypto/index.ts deleted file mode 100644 index 5c759e4c39..0000000000 --- a/packages/scale/src/capi/crypto/index.ts +++ /dev/null @@ -1,8 +0,0 @@ -export { watCryptoWaitReady } from "./util" - -//export * as hex from "./hex" -export * as ss58 from "./ss58" - -// moderate --exclude hex.ts ss58.ts - -export * from "./hashers" diff --git a/packages/scale/src/capi/crypto/ss58.ts b/packages/scale/src/capi/crypto/ss58.ts deleted file mode 100644 index 4baa85760f..0000000000 --- a/packages/scale/src/capi/crypto/ss58.ts +++ /dev/null @@ -1,141 +0,0 @@ -/* eslint-disable @typescript-eslint/no-non-null-assertion */ - -/** - * Adapted from https://github.com/paritytech/capi-old copyright Parity Technologies (APACHE License 2.0) - * Changes August 19th 2023 : - * - updated to use subshape for scale decoding - * - adapted from deno to typescript - * - Copyright 2023 Parity Technologies - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. - */ - -import * as base58 from "./base58" -import { Blake2b } from "./util" - -export function encode(prefix: number, payload: Uint8Array, checksumLength?: number): string { - return base58.encode(encodeRaw(prefix, payload, checksumLength)) -} - -export function encodeRaw( - prefix: number, - payload: Uint8Array, - checksumLength?: number -): Uint8Array { - checksumLength ??= DEFAULT_PAYLOAD_CHECKSUM_LENGTHS[payload.length] - if (!checksumLength) throw new InvalidPayloadLengthError() - const prefixBytes = - prefix < 64 - ? Uint8Array.of(prefix) - : Uint8Array.of( - ((prefix & 0b0000_0000_1111_1100) >> 2) | 0b0100_0000, - (prefix >> 8) | ((prefix & 0b0000_0000_0000_0011) << 6) - ) - const hasher = new Blake2b() - hasher.update(SS58PRE) - hasher.update(prefixBytes) - hasher.update(payload) - const digest = hasher.digest() - const checksum = digest.subarray(0, checksumLength) - hasher.dispose() - const address = new Uint8Array(prefixBytes.length + payload.length + checksumLength) - address.set(prefixBytes, 0) - address.set(payload, prefixBytes.length) - address.set(checksum, prefixBytes.length + payload.length) - return address -} - -export type EncodeError = InvalidPayloadLengthError -export class InvalidPayloadLengthError extends Error { - override readonly name = "InvalidPayloadLengthError" -} - -export type DecodeResult = [prefix: number, pubKey: Uint8Array] - -export function decode(address: string): DecodeResult { - return decodeRaw(base58.decode(address)) -} - -export function decodeRaw(address: Uint8Array): DecodeResult { - const checksumLength = VALID_ADDRESS_CHECKSUM_LENGTHS[address.length] - if (!checksumLength) throw new InvalidAddressLengthError() - const prefixLength = address[0]! & 0b0100_0000 ? 2 : 1 - const prefix: number = - prefixLength === 1 - ? address[0]! - : ((address[0]! & 0b0011_1111) << 2) | (address[1]! >> 6) | ((address[1]! & 0b0011_1111) << 8) - const hasher = new Blake2b() - hasher.update(SS58PRE) - hasher.update(address.subarray(0, address.length - checksumLength)) - const digest = hasher.digest() - const checksum = address.subarray(address.length - checksumLength) - hasher.dispose() - if (!isValidChecksum(digest, checksum, checksumLength)) { - throw new InvalidAddressChecksumError() - } - const pubKey = address.subarray(prefixLength, address.length - checksumLength) - return [prefix, pubKey] -} - -export type DecodeError = InvalidAddressLengthError | InvalidAddressChecksumError -export class InvalidAddressLengthError extends Error { - override readonly name = "InvalidAddressError" -} -export class InvalidAddressChecksumError extends Error { - override readonly name = "InvalidAddressChecksumError" -} - -// SS58PRE string (0x53533538505245 hex) encoded as Uint8Array -const SS58PRE = Uint8Array.of(83, 83, 53, 56, 80, 82, 69) -const VALID_ADDRESS_CHECKSUM_LENGTHS: Record = { - 3: 1, - 4: 1, - 5: 2, - 6: 1, - 7: 2, - 8: 3, - 9: 4, - 10: 1, - 11: 2, - 12: 3, - 13: 4, - 14: 5, - 15: 6, - 16: 7, - 17: 8, - 35: 2, - 36: 2, - 37: 2, -} -const DEFAULT_PAYLOAD_CHECKSUM_LENGTHS: Record = { - 1: 1, - 2: 1, - 4: 1, - 8: 1, - 32: 2, - 33: 2, -} - -function isValidChecksum( - digest: Uint8Array, - checksum: Uint8Array, - checksumLength: number -): boolean { - for (let i = 0; i < checksumLength; i++) { - if (digest[i] !== checksum[i]) { - return false - } - } - return true -} diff --git a/packages/scale/src/capi/crypto/util/index.test.ts b/packages/scale/src/capi/crypto/util/index.test.ts deleted file mode 100644 index c3936171b6..0000000000 --- a/packages/scale/src/capi/crypto/util/index.test.ts +++ /dev/null @@ -1,74 +0,0 @@ -import { Blake2b as NosimdBlake2b, Xxhash as NosimdXxhash } from "./nosimd" - -const testData = new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]) -const expectedTestDataHashes = { - blake2_64: "b4c0d262a419d0c3", - blake2_128: "6e920859d4dc94e6f701ae0b313094ac", - blake2_256: "e283ce217acedb1b0f71fc5ebff647a1a17a2492a6d2f34fb76b994a23ca8931", - blake2_512: - "b942c381affddcd41d125a09e5f8c7189cb5daa32f6cc67d763054f5221360fe4efde375c6853365bac58f61ed2a2e5f55248a5c4934bacf730bbe85e72bc8e6", - twox128: "d8849f717d1abec659326d69cec6c71d", - twox256: "d8849f717d1abec659326d69cec6c71dc33f0fb7207bfce38683b1a9a023a655", - twox64: "d8849f717d1abec6", -} - -describe("hashers", () => { - test("nosimd variants have expected outputs", () => { - const { nosimd } = getHashers() - - Object.values(nosimd).forEach((hasher) => hasher.update(testData)) - - expect(u8aToHex(nosimd.blake2_64.digest())).toEqual(expectedTestDataHashes.blake2_64) - expect(u8aToHex(nosimd.blake2_128.digest())).toEqual(expectedTestDataHashes.blake2_128) - expect(u8aToHex(nosimd.blake2_256.digest())).toEqual(expectedTestDataHashes.blake2_256) - expect(u8aToHex(nosimd.blake2_512.digest())).toEqual(expectedTestDataHashes.blake2_512) - expect(u8aToHex(nosimd.twox128.digest())).toEqual(expectedTestDataHashes.twox128) - expect(u8aToHex(nosimd.twox256.digest())).toEqual(expectedTestDataHashes.twox256) - expect(u8aToHex(nosimd.twox64.digest())).toEqual(expectedTestDataHashes.twox64) - }) - - test("nosimd digestInto correctly mutates the given Uint8Array", () => { - const { nosimd } = getHashers() - - Object.values(nosimd).forEach((hasher) => hasher.update(testData)) - - const digests = { - blake2_64: new Uint8Array(64 / 8), - blake2_128: new Uint8Array(128 / 8), - blake2_256: new Uint8Array(256 / 8), - blake2_512: new Uint8Array(512 / 8), - twox128: new Uint8Array(128 / 8), - twox256: new Uint8Array(256 / 8), - twox64: new Uint8Array(64 / 8), - } - - nosimd.blake2_64.digestInto(digests.blake2_64) - nosimd.blake2_128.digestInto(digests.blake2_128) - nosimd.blake2_256.digestInto(digests.blake2_256) - nosimd.blake2_512.digestInto(digests.blake2_512) - nosimd.twox128.digestInto(digests.twox128) - nosimd.twox256.digestInto(digests.twox256) - nosimd.twox64.digestInto(digests.twox64) - - expect(u8aToHex(digests.blake2_64)).toEqual(expectedTestDataHashes.blake2_64) - expect(u8aToHex(digests.blake2_128)).toEqual(expectedTestDataHashes.blake2_128) - expect(u8aToHex(digests.blake2_256)).toEqual(expectedTestDataHashes.blake2_256) - expect(u8aToHex(digests.blake2_512)).toEqual(expectedTestDataHashes.blake2_512) - expect(u8aToHex(digests.twox128)).toEqual(expectedTestDataHashes.twox128) - expect(u8aToHex(digests.twox256)).toEqual(expectedTestDataHashes.twox256) - expect(u8aToHex(digests.twox64)).toEqual(expectedTestDataHashes.twox64) - }) -}) - -const getHashers = () => ({ - nosimd: { - blake2_64: new NosimdBlake2b(64 / 8), - blake2_128: new NosimdBlake2b(128 / 8), - blake2_256: new NosimdBlake2b(256 / 8), - blake2_512: new NosimdBlake2b(512 / 8), - twox128: new NosimdXxhash(128 / 64), - twox256: new NosimdXxhash(256 / 64), - twox64: new NosimdXxhash(64 / 64), - }, -}) -const u8aToHex = (u8a: Uint8Array) => Buffer.from(u8a).toString("hex") diff --git a/packages/scale/src/capi/crypto/util/index.ts b/packages/scale/src/capi/crypto/util/index.ts deleted file mode 100644 index 07fa6bfba6..0000000000 --- a/packages/scale/src/capi/crypto/util/index.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { Hasher } from "wat-the-crypto/types/common/hasher" - -import * as nosimd from "./nosimd" - -// eslint-disable-next-line @typescript-eslint/no-explicit-any -export const Blake2b: new (...args: any[]) => Hasher = nosimd.Blake2b -// eslint-disable-next-line @typescript-eslint/no-explicit-any -export const Xxhash: new (...args: any[]) => Hasher = nosimd.Xxhash - -export const watCryptoWaitReady = async () => { - return -} diff --git a/packages/scale/src/capi/crypto/util/nosimd.ts b/packages/scale/src/capi/crypto/util/nosimd.ts deleted file mode 100644 index a9029dd324..0000000000 --- a/packages/scale/src/capi/crypto/util/nosimd.ts +++ /dev/null @@ -1,53 +0,0 @@ -import { blake2AsU8a, xxhashAsU8a } from "@polkadot/util-crypto" -import { Hasher } from "wat-the-crypto/types/common/hasher" - -type XxHashBitLength = Parameters[1] -type Blake2bBitLength = Parameters[1] - -export class Xxhash implements Hasher { - rounds: number - input: Uint8Array = new Uint8Array() - - constructor(rounds: number) { - this.rounds = rounds - } - - update(input: Uint8Array): void { - this.input = input - } - - digest(): Uint8Array { - return xxhashAsU8a(this.input, (this.rounds * 64) as XxHashBitLength) - } - digestInto(digest: Uint8Array): void { - digest.set(this.digest()) - } - dispose(): void { - this.input = new Uint8Array() - } -} - -export class Blake2b implements Hasher { - digestSize: number - input: Uint8Array = new Uint8Array() - - // digestSize defaults to 64 - // https://github.com/paritytech/wat-the-crypto/blob/a96745c57f597b35fe1461d0e643a34ba5e7bd85/blake2b/blake2b.ts#L31 - constructor(digestSize = 64) { - this.digestSize = digestSize - } - - update(input: Uint8Array): void { - this.input = input - } - - digest(): Uint8Array { - return blake2AsU8a(this.input, (this.digestSize * 8) as Blake2bBitLength) - } - digestInto(digest: Uint8Array): void { - digest.set(this.digest()) - } - dispose(): void { - this.input = new Uint8Array() - } -} diff --git a/packages/scale/src/capi/frame_metadata/Extrinsic.ts b/packages/scale/src/capi/frame_metadata/Extrinsic.ts deleted file mode 100644 index 7e8f73b9d4..0000000000 --- a/packages/scale/src/capi/frame_metadata/Extrinsic.ts +++ /dev/null @@ -1,154 +0,0 @@ -/* eslint-disable @typescript-eslint/no-non-null-assertion */ - -/** - * Adapted from https://github.com/paritytech/capi-old copyright Parity Technologies (APACHE License 2.0) - * Changes August 19th 2023 : - * - updated to use subshape for scale decoding - * - adapted from deno to typescript - * - Copyright 2023 Parity Technologies - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. - */ - -/* eslint-disable @typescript-eslint/no-explicit-any */ -import * as $ from "@talismn/subshape-fork" - -import { blake2_256 } from "../crypto/index" -import { FrameMetadata } from "./FrameMetadata" - -export interface Extrinsic { - protocolVersion: number - signature?: - | { - sender: { - address: $.Output - sign: Signer - } - extra: $.Output - additional: $.Output - sig?: never - } - | { - sender: { - address: $.Output - sign?: Signer - } - extra: $.Output - additional?: never - sig: $.Output - } - call: $.Output -} - -export type Signer = ( - message: Uint8Array, - fullData: Uint8Array -) => $.Output | Promise<$.Output> - -export function $extrinsic(metadata: M): $.Shape> { - const $sig = metadata.extrinsic.signature as $.Shape<$.Output> - const $sigPromise = $.promise($sig) - const $call = metadata.extrinsic.call as $.Shape<$.Output> - const $address = metadata.extrinsic.address as $.Shape<$.Output> - const $extra = metadata.extrinsic.extra as $.Shape<$.Output> - const $additional = metadata.extrinsic.additional as $.Shape< - $.Output - > - - const toSignSize = $call.staticSize + $extra.staticSize + $additional.staticSize - const totalSize = 1 + $address.staticSize + $sig.staticSize + toSignSize - - const $baseExtrinsic: $.Shape> = $.createShape({ - metadata: [], - staticSize: totalSize, - subEncode(buffer, extrinsic) { - const firstByte = (+!!extrinsic.signature << 7) | extrinsic.protocolVersion - buffer.array[buffer.index++] = firstByte - const { signature, call } = extrinsic - if (signature) { - $address.subEncode(buffer, signature.sender.address) - if (signature.additional) { - const toSignBuffer = new $.EncodeBuffer(buffer.stealAlloc(toSignSize)) - $call.subEncode(toSignBuffer, call) - const callEnd = toSignBuffer.finishedSize + toSignBuffer.index - $extra.subEncode(toSignBuffer, signature.extra) - const extraEnd = toSignBuffer.finishedSize + toSignBuffer.index - $additional.subEncode(toSignBuffer, signature.additional) - const toSignEncoded = toSignBuffer.finish() - const callEncoded = toSignEncoded.subarray(0, callEnd) - const extraEncoded = toSignEncoded.subarray(callEnd, extraEnd) - const toSign = toSignEncoded.length > 256 ? blake2_256.hash(toSignEncoded) : toSignEncoded - const sig = signature.sender.sign!(toSign, toSignEncoded) - if (sig instanceof Promise) { - $sigPromise.subEncode(buffer, sig) - } else { - $sig.subEncode(buffer, sig) - } - buffer.insertArray(extraEncoded) - buffer.insertArray(callEncoded) - } else { - $sig.subEncode(buffer, signature.sig!) - $extra.subEncode(buffer, signature.extra) - $call.subEncode(buffer, call) - } - } else { - $call.subEncode(buffer, call) - } - }, - subDecode(buffer) { - const firstByte = buffer.array[buffer.index++]! - const hasSignature = firstByte & (1 << 7) - const protocolVersion = firstByte & ~(1 << 7) - let signature: Extrinsic["signature"] - if (hasSignature) { - const address = $address.subDecode(buffer) - const sig = $sig.subDecode(buffer) - const extra = $extra.subDecode(buffer) - signature = { sender: { address }, sig, extra } - } - const call = $call.subDecode(buffer) - return { protocolVersion, signature, call } - }, - subAssert(assert) { - assert.typeof(this, "object") - assert.key(this, "protocolVersion").equals($.u8, 4) - const value_ = assert.value as any - $call.subAssert(assert.key(this, "call")) - if (value_.signature) { - const signatureAssertState = assert.key(this, "signature") - $address.subAssert(signatureAssertState.key(this, "sender").key(this, "address")) - $extra.subAssert(signatureAssertState.key(this, "extra")) - if ("additional" in value_.signature) { - $additional.subAssert(signatureAssertState.key(this, "additional")) - signatureAssertState.key(this, "sender").key(this, "sign").typeof(this, "function") - } else { - $sig.subAssert(signatureAssertState.key(this, "sig")) - } - } - }, - }) - - return $.withMetadata( - $.metadata("$extrinsic", $extrinsic, metadata), - $.lenPrefixed($baseExtrinsic) - ) -} - -export class SignerError extends Error { - override readonly name = "SignerError" - - constructor(readonly inner: unknown) { - super() - } -} diff --git a/packages/scale/src/capi/frame_metadata/FrameMetadata.ts b/packages/scale/src/capi/frame_metadata/FrameMetadata.ts deleted file mode 100644 index d1cb43106e..0000000000 --- a/packages/scale/src/capi/frame_metadata/FrameMetadata.ts +++ /dev/null @@ -1,65 +0,0 @@ -/** - * Adapted from https://github.com/paritytech/capi-old copyright Parity Technologies (APACHE License 2.0) - * Changes August 19th 2023 : - * - updated to use subshape for scale decoding - * - adapted from deno to typescript - * - Copyright 2023 Parity Technologies - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. - */ -import * as $ from "@talismn/subshape-fork" - -export interface FrameMetadata { - types: Record - paths: Record - pallets: Record - extrinsic: { - call: $.AnyShape - signature: $.AnyShape - address: $.AnyShape - extra: $.AnyShape - additional: $.AnyShape - } -} - -export interface Pallet { - id: number - name: string - storagePrefix: string - storage: Record - constants: Record - types: { - call?: $.AnyShape - event?: $.AnyShape - error?: $.AnyShape - } - docs: string -} - -export interface StorageEntry { - singular: boolean - name: string - key: $.AnyShape - partialKey: $.AnyShape - value: $.AnyShape - default?: Uint8Array - docs: string -} - -export interface Constant { - name: string - codec: $.AnyShape - value: Uint8Array - docs: string -} diff --git a/packages/scale/src/capi/frame_metadata/decodeMetadata.ts b/packages/scale/src/capi/frame_metadata/decodeMetadata.ts deleted file mode 100644 index df595b8c68..0000000000 --- a/packages/scale/src/capi/frame_metadata/decodeMetadata.ts +++ /dev/null @@ -1,30 +0,0 @@ -/** - * Adapted from https://github.com/paritytech/capi-old copyright Parity Technologies (APACHE License 2.0) - * Changes August 19th 2023 : - * - updated to use subshape for scale decoding - * - adapted from deno to typescript - * - Copyright 2023 Parity Technologies - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. - */ -import * as $ from "@talismn/subshape-fork" - -import { $metadata, transformMetadata } from "./raw/v14" - -export function decodeMetadata(encoded: Uint8Array) { - return transformMetadata($metadata.decode(encoded)) -} - -export const $metadataV14 = $metadata -export type MetadataV14 = $.Output diff --git a/packages/scale/src/capi/frame_metadata/index.ts b/packages/scale/src/capi/frame_metadata/index.ts deleted file mode 100644 index 72eb42b0ce..0000000000 --- a/packages/scale/src/capi/frame_metadata/index.ts +++ /dev/null @@ -1,6 +0,0 @@ -// moderate - -export * from "./decodeMetadata" -export * from "./Extrinsic" -export * from "./FrameMetadata" -export * from "./key_codecs" diff --git a/packages/scale/src/capi/frame_metadata/key_codecs.ts b/packages/scale/src/capi/frame_metadata/key_codecs.ts deleted file mode 100644 index 710fc80d77..0000000000 --- a/packages/scale/src/capi/frame_metadata/key_codecs.ts +++ /dev/null @@ -1,116 +0,0 @@ -/* eslint-disable @typescript-eslint/no-non-null-assertion */ - -/** - * Adapted from https://github.com/paritytech/capi-old copyright Parity Technologies (APACHE License 2.0) - * Changes August 19th 2023 : - * - updated to use subshape for scale decoding - * - adapted from deno to typescript - * - Copyright 2023 Parity Technologies - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. - */ -import * as $ from "@talismn/subshape-fork" - -import { twox128 } from "../crypto" - -export function $storageKey( - palletName: string, - entryName: string, - $key: $.Shape -): $.Shape { - const palletHash = twox128.hash(new TextEncoder().encode(palletName)) - const entryHash = twox128.hash(new TextEncoder().encode(entryName)) - return $.createShape({ - metadata: $.metadata("$storageKey", $storageKey, palletName, entryName, $key), - staticSize: $key.staticSize + 32, - subEncode(buffer, key) { - buffer.insertArray(palletHash) - buffer.insertArray(entryHash) - $key.subEncode(buffer, key) - }, - subDecode(buffer) { - // Ignore initial hashes - buffer.index += 32 - return $key.subDecode(buffer) - }, - subAssert(assert) { - $key.subAssert(assert) - }, - }) -} - -export const $emptyKey = $.withMetadata($.metadata("$emptyKey"), $.constant(undefined)) - -export const $partialEmptyKey = $.createShape({ - metadata: $.metadata("$partialEmptyKey"), - staticSize: 0, - subEncode() {}, - subDecode() { - throw new Error("Cannot decode partial key") - }, - subAssert(assert) { - if (assert.value != null) { - throw new $.ShapeAssertError(this, assert.value, `${assert.path} != null`) - } - }, -}) - -export function $partialSingleKey($inner: $.Shape): $.Shape { - return $.createShape({ - metadata: $.metadata("$partialSingleKey", $partialSingleKey, $inner), - staticSize: $inner.staticSize, - subEncode(buffer, key) { - if (key !== null) $inner.subEncode(buffer, key) - }, - subDecode() { - throw new Error("Cannot decode partial key") - }, - subAssert(assert) { - if (assert.value === null) return - $inner.subAssert(assert) - }, - }) -} - -// eslint-disable-next-line @typescript-eslint/no-explicit-any -export type PartialMultiKey = T extends [...infer A, any] - ? T | PartialMultiKey - : T | null - -export function $partialMultiKey( - ...keys: [...T] -): $.Shape>> -export function $partialMultiKey(...codecs: $.Shape[]): $.Shape { - return $.createShape({ - metadata: $.metadata("$partialMultiKey", $partialMultiKey, ...codecs), - staticSize: $.tuple(...codecs).staticSize, - subEncode(buffer, key) { - if (!key) return - for (let i = 0; i < key.length; i++) { - codecs[i]!.subEncode(buffer, key[i]!) - } - }, - subDecode() { - throw new Error("Cannot decode partial key") - }, - subAssert(assert) { - if (assert.value === null) return - assert.instanceof(this, Array) - const assertLength = assert.key(this, "length") - assertLength.typeof(this, "number") - const length = assertLength.value as number - $.tuple(...codecs.slice(0, length)).subAssert(assert) - }, - }) -} diff --git a/packages/scale/src/capi/frame_metadata/raw/v14.ts b/packages/scale/src/capi/frame_metadata/raw/v14.ts deleted file mode 100644 index 9efc343f11..0000000000 --- a/packages/scale/src/capi/frame_metadata/raw/v14.ts +++ /dev/null @@ -1,226 +0,0 @@ -/* eslint-disable @typescript-eslint/no-non-null-assertion */ - -/** - * Adapted from https://github.com/paritytech/capi-old copyright Parity Technologies (APACHE License 2.0) - * Changes August 19th 2023 : - * - updated to use subshape for scale decoding - * - adapted from deno to typescript - * - Copyright 2023 Parity Technologies - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. - */ - -import * as $ from "@talismn/subshape-fork" - -import { - blake2_128, - blake2_128Concat, - blake2_256, - identity, - twox128, - twox256, - twox64Concat, -} from "../../crypto" -import { $ty, $tyId } from "../../scale_info/raw/Ty" -import { $null, transformTys } from "../../scale_info/transformTys" -import { normalizeDocs, normalizeIdent } from "../../util/normalize" -import { Constant, FrameMetadata, Pallet, StorageEntry } from "../FrameMetadata" -import { - $emptyKey, - $partialEmptyKey, - $partialMultiKey, - $partialSingleKey, - $storageKey, -} from "../key_codecs" - -const hashers = { - blake2_128, - blake2_256, - blake2_128Concat, - twox128, - twox256, - twox64Concat, - identity, -} - -const $hasher = $.literalUnion([ - "blake2_128", - "blake2_256", - "blake2_128Concat", - "twox128", - "twox256", - "twox64Concat", - "identity", -]) - -const $storageEntry = $.object( - $.field("name", $.str), - $.field("modifier", $.literalUnion(["Optional", "Default"])), - $.taggedUnion("type", [ - $.variant("Plain", $.field("value", $tyId)), - $.variant( - "Map", - $.field("hashers", $.array($hasher)), - $.field("key", $tyId), - $.field("value", $tyId) - ), - ]), - $.field("default", $.uint8Array), - $.field("docs", $.array($.str)) -) - -const $constant = $.object( - $.field("name", $.str), - $.field("ty", $tyId), - $.field("value", $.uint8Array), - $.field("docs", $.array($.str)) -) - -const $pallet = $.object( - $.field("name", $.str), - $.optionalField( - "storage", - $.object($.field("prefix", $.str), $.field("entries", $.array($storageEntry))) - ), - $.optionalField("calls", $tyId), - $.optionalField("event", $tyId), - $.field("constants", $.array($constant)), - $.optionalField("error", $tyId), - $.field("id", $.u8) -) - -const $extrinsicDef = $.object( - $.field("ty", $tyId), - $.field("version", $.u8), - $.field( - "signedExtensions", - $.array( - $.object($.field("ident", $.str), $.field("ty", $tyId), $.field("additionalSigned", $tyId)) - ) - ) -) - -// https://docs.substrate.io/build/application-development/#metadata-system -const magicNumber = 1635018093 - -export const $metadata = $.object( - $.field("magicNumber", $.constant(magicNumber, $.u32)), - $.field("version", $.constant<14>(14, $.u8)), - $.field("tys", $.array($ty)), - $.field("pallets", $.array($pallet)), - $.field("extrinsic", $extrinsicDef), - // TODO: is this useful? - $.field("runtime", $tyId) -) - -export function transformMetadata(metadata: $.Output): FrameMetadata { - const { ids, types, paths } = transformTys(metadata.tys) - - return { - types, - paths, - pallets: Object.fromEntries( - metadata.pallets.map((pallet): [string, Pallet] => [ - pallet.name, - { - id: pallet.id, - name: pallet.name, - storagePrefix: pallet.storage?.prefix ?? pallet.name, - storage: Object.fromEntries( - pallet.storage?.entries.map((storage): [string, StorageEntry] => { - let key, partialKey - if (storage.type === "Plain") { - key = $emptyKey - partialKey = $partialEmptyKey - } else if (storage.hashers.length === 1) { - key = hashers[storage.hashers[0]!].$hash(ids[storage.key]!) - partialKey = $partialSingleKey(key) - } else { - const codecs = extractTupleMembersVisitor - .visit(ids[storage.key]!) - .map((codec, i) => hashers[storage.hashers[i]!].$hash(codec)) - key = $.tuple(...codecs) - partialKey = $partialMultiKey(...codecs) - } - return [ - storage.name, - { - singular: storage.type === "Plain", - name: storage.name, - key: $storageKey(pallet.name, storage.name, key), - partialKey: $storageKey(pallet.name, storage.name, partialKey), - value: ids[storage.value]!, - docs: normalizeDocs(storage.docs), - default: storage.modifier === "Default" ? storage.default : undefined, - }, - ] - }) ?? [] - ), - constants: Object.fromEntries( - pallet.constants.map((constant): [string, Constant] => [ - constant.name, - { - name: constant.name, - codec: ids[constant.ty]!, - value: constant.value, - docs: normalizeDocs(constant.docs), - }, - ]) - ), - types: { - call: ids[pallet.calls!], - error: ids[pallet.error!], - event: ids[pallet.event!], - }, - docs: "", - }, - ]) - ), - extrinsic: { - call: getExtrinsicParameter("call"), - signature: getExtrinsicParameter("signature"), - address: getExtrinsicParameter("address"), - extra: getExtensionsCodec("ty"), - additional: getExtensionsCodec("additionalSigned"), - }, - } - - function getExtrinsicParameter(key: "call" | "signature" | "address") { - if (!metadata.extrinsic.ty) return $.never - - const extrinsicTy = metadata.tys[metadata.extrinsic.ty] - if (!extrinsicTy) return $.never - - const id = extrinsicTy.params.find((x) => x.name.toLowerCase() === key)?.ty - if (id === undefined) return $.never - - return ids[id]! - } - function getExtensionsCodec(key: "ty" | "additionalSigned") { - return $.object( - ...(metadata.extrinsic.signedExtensions.flatMap((ext) => { - const codec = ids[ext[key]]! - if (codec === $null) return [] - return [$.field(normalizeIdent(ext.ident), codec)] - }) as any) // eslint-disable-line @typescript-eslint/no-explicit-any - ) - } -} - -// eslint-disable-next-line @typescript-eslint/no-explicit-any -const extractTupleMembersVisitor = new $.ShapeVisitor<$.Shape[]>().add( - // eslint-disable-next-line @typescript-eslint/no-explicit-any - $.tuple<$.Shape[]>, - (_codec, ...members) => members -) diff --git a/packages/scale/src/capi/index.ts b/packages/scale/src/capi/index.ts deleted file mode 100644 index 4c7e26654b..0000000000 --- a/packages/scale/src/capi/index.ts +++ /dev/null @@ -1,4 +0,0 @@ -export * from "./crypto" -export * from "./frame_metadata" -export * from "./scale_info" -export * from "./util" diff --git a/packages/scale/src/capi/scale_info/index.ts b/packages/scale/src/capi/scale_info/index.ts deleted file mode 100644 index 93d970e96d..0000000000 --- a/packages/scale/src/capi/scale_info/index.ts +++ /dev/null @@ -1,6 +0,0 @@ -// moderate - -export * from "./overrides/index" -export * from "./transformTys" -export * from "./overrides/Era" -export * from "./raw/Ty" diff --git a/packages/scale/src/capi/scale_info/overrides/ChainError.ts b/packages/scale/src/capi/scale_info/overrides/ChainError.ts deleted file mode 100644 index 8ae2cde2bb..0000000000 --- a/packages/scale/src/capi/scale_info/overrides/ChainError.ts +++ /dev/null @@ -1,30 +0,0 @@ -/** - * Adapted from https://github.com/paritytech/capi-old copyright Parity Technologies (APACHE License 2.0) - * Changes August 19th 2023 : - * - updated to use subshape for scale decoding - * - adapted from deno to typescript - * - Copyright 2023 Parity Technologies - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. - */ - -export class ChainError extends Error { - override readonly name = "ChainError" - constructor(public value: T) { - super() - this.stack = "ChainError: see error.value\n [error occurred on chain]" - } - - static toArgs = (x: ChainError): [T] => [x.value] -} diff --git a/packages/scale/src/capi/scale_info/overrides/Era.ts b/packages/scale/src/capi/scale_info/overrides/Era.ts deleted file mode 100644 index 4da1dc5c16..0000000000 --- a/packages/scale/src/capi/scale_info/overrides/Era.ts +++ /dev/null @@ -1,104 +0,0 @@ -/** - * Adapted from https://github.com/paritytech/capi-old copyright Parity Technologies (APACHE License 2.0) - * Changes August 19th 2023 : - * - updated to use subshape for scale decoding - * - adapted from deno to typescript - * - Copyright 2023 Parity Technologies - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. - */ -import * as $ from "@talismn/subshape-fork" - -export type Era = - | { type: "Immortal" } - | { - type: "Mortal" - period: bigint - phase: bigint - } - -// eslint-disable-next-line @typescript-eslint/no-namespace -export namespace Era { - export const Immortal: Era = { type: "Immortal" } - export function Mortal(period: bigint, current: bigint): Era { - const adjustedPeriod = minN(maxN(nextPowerOfTwo(period), 4n), 1n << 16n) - const phase = current % adjustedPeriod - const quantizeFactor = maxN(adjustedPeriod >> 12n, 1n) - const quantizedPhase = (phase / quantizeFactor) * quantizeFactor - return { type: "Mortal", period: adjustedPeriod, phase: quantizedPhase } - } -} - -export const $era: $.Shape = $.createShape({ - metadata: $.metadata("$era"), - staticSize: 2, - subEncode(buffer, value) { - if (value.type === "Immortal") { - buffer.array[buffer.index++] = 0 - } else { - const quantizeFactor = maxN(value.period >> 12n, 1n) - const encoded = - minN(maxN(trailingZeroes(value.period) - 1n, 1n), 15n) | - ((value.phase / quantizeFactor) << 4n) - $.u16.subEncode(buffer, Number(encoded)) - } - }, - subDecode(buffer) { - if (buffer.array[buffer.index] === 0) { - buffer.index++ - return { type: "Immortal" } - } else { - const encoded = BigInt($.u16.subDecode(buffer)) - const period = 2n << encoded % (1n << 4n) - const quantizeFactor = maxN(period >> 12n, 1n) - const phase = (encoded >> 4n) * quantizeFactor - if (period >= 4n && phase <= period) { - return { type: "Mortal", period, phase } - } else { - throw new Error("Invalid period and phase") - } - } - }, - subAssert: $.taggedUnion("type", [ - $.variant("Immortal"), - $.variant("Mortal", $.field("period", $.u64), $.field("phase", $.u64)), - ]).subAssert, -}) - -function maxN(a: bigint, b: bigint) { - return a > b ? a : b -} - -function minN(a: bigint, b: bigint) { - return a < b ? a : b -} - -function trailingZeroes(n: bigint) { - let i = 0n - while (!(n & 1n)) { - i++ - n >>= 1n - } - return i -} - -function nextPowerOfTwo(n: bigint) { - n-- - let p = 1n - while (n > 0n) { - p <<= 1n - n >>= 1n - } - return p -} diff --git a/packages/scale/src/capi/scale_info/overrides/index.ts b/packages/scale/src/capi/scale_info/overrides/index.ts deleted file mode 100644 index d467fc8a4c..0000000000 --- a/packages/scale/src/capi/scale_info/overrides/index.ts +++ /dev/null @@ -1,5 +0,0 @@ -// moderate - -//export * from "./ChainError" -export * from "./Era" -//export * from "./overrides" diff --git a/packages/scale/src/capi/scale_info/overrides/overrides.ts b/packages/scale/src/capi/scale_info/overrides/overrides.ts deleted file mode 100644 index 9a9a131fdc..0000000000 --- a/packages/scale/src/capi/scale_info/overrides/overrides.ts +++ /dev/null @@ -1,72 +0,0 @@ -/* eslint-disable @typescript-eslint/no-non-null-assertion */ - -/** - * Adapted from https://github.com/paritytech/capi-old copyright Parity Technologies (APACHE License 2.0) - * Changes August 19th 2023 : - * - updated to use subshape for scale decoding - * - adapted from deno to typescript - * - Copyright 2023 Parity Technologies - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. - */ - -/* eslint-disable @typescript-eslint/no-explicit-any */ -import * as $ from "@talismn/subshape-fork" - -import { Ty } from "../raw/Ty" -import { ChainError } from "./ChainError" -import { $era } from "./Era" - -const isResult = new $.ShapeVisitor() - .add($.result, () => true) - .fallback(() => false) - -const isOption = new $.ShapeVisitor() - .add($.option, () => true) - .fallback(() => false) - -export const overrides: Record $.AnyShape) => $.AnyShape> = { - "Option": (ty, visit) => { - let $some = visit(ty.params[0]!.ty!) - if (isOption.visit($some)) { - $some = $.tuple($some) - } - return $.option($some) - }, - "Result": (ty, visit) => { - let $ok = visit(ty.params[0]!.ty!) - if (isResult.visit($ok)) { - $ok = $.tuple($ok) - } - return $.result( - $ok, - $.instance(ChainError, $.tuple(visit(ty.params[1]!.ty!)), ChainError.toArgs) - ) - }, - "BTreeMap": (ty, visit) => { - return $.map(visit(ty.params[0]!.ty!) as any, visit(ty.params[1]!.ty!)) - }, - "BTreeSet": (ty, visit) => { - return $.set(visit(ty.params[0]!.ty!) as any) - }, - "frame_support::traits::misc::WrapperOpaque": (ty, visit) => { - return $.lenPrefixed(visit(ty.params[0]!.ty!)) - }, - "frame_support::traits::misc::WrapperKeepOpaque": (ty, visit) => { - return $.lenPrefixed(visit(ty.params[0]!.ty!)) - }, - "sp_runtime::generic::era::Era": () => { - return $era - }, -} diff --git a/packages/scale/src/capi/scale_info/raw/Ty.ts b/packages/scale/src/capi/scale_info/raw/Ty.ts deleted file mode 100644 index 9b0ade5fe3..0000000000 --- a/packages/scale/src/capi/scale_info/raw/Ty.ts +++ /dev/null @@ -1,81 +0,0 @@ -/** - * Adapted from https://github.com/paritytech/capi-old copyright Parity Technologies (APACHE License 2.0) - * Changes August 19th 2023 : - * - updated to use subshape for scale decoding - * - adapted from deno to typescript - * - Copyright 2023 Parity Technologies - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. - */ -import * as $ from "@talismn/subshape-fork" - -export const $tyId = $.compact($.u32) - -export const $field = $.object( - $.optionalField("name", $.str), - $.field("ty", $tyId), - $.optionalField("typeName", $.str), - $.field("docs", $.array($.str)) -) - -export const $primitiveKind = $.literalUnion([ - "bool", - "char", - "str", - "u8", - "u16", - "u32", - "u64", - "u128", - "u256", - "i8", - "i16", - "i32", - "i64", - "i128", - "i256", -]) - -export const $tyDef = $.taggedUnion("type", [ - $.variant("Struct", $.field("fields", $.array($field))), - $.variant( - "Union", - $.field( - "members", - $.array( - $.object( - $.field("name", $.str), - $.field("fields", $.array($field)), - $.field("index", $.u8), - $.field("docs", $.array($.str)) - ) - ) - ) - ), - $.variant("Sequence", $.field("typeParam", $tyId)), - $.variant("SizedArray", $.field("len", $.u32), $.field("typeParam", $tyId)), - $.variant("Tuple", $.field("fields", $.array($tyId))), - $.variant("Primitive", $.field("kind", $primitiveKind)), - $.variant("Compact", $.field("typeParam", $tyId)), - $.variant("BitSequence", $.field("bitOrderType", $tyId), $.field("bitStoreType", $tyId)), -]) - -export type Ty = $.Output -export const $ty = $.object( - $.field("id", $.compact($.u32)), - $.field("path", $.array($.str)), - $.field("params", $.array($.object($.field("name", $.str), $.optionalField("ty", $tyId)))), - $tyDef, - $.field("docs", $.array($.str)) -) diff --git a/packages/scale/src/capi/scale_info/transformTys.ts b/packages/scale/src/capi/scale_info/transformTys.ts deleted file mode 100644 index 831d02a056..0000000000 --- a/packages/scale/src/capi/scale_info/transformTys.ts +++ /dev/null @@ -1,289 +0,0 @@ -/* eslint-disable @typescript-eslint/no-non-null-assertion */ - -/** - * Adapted from https://github.com/paritytech/capi-old copyright Parity Technologies (APACHE License 2.0) - * Changes August 19th 2023 : - * - updated to use subshape for scale decoding - * - adapted from deno to typescript - * - Copyright 2023 Parity Technologies - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. - */ - -/* eslint-disable @typescript-eslint/no-explicit-any */ -import * as $ from "@talismn/subshape-fork" -import { AnyShape, Shape } from "@talismn/subshape-fork" - -import { getOrInit } from "../util" -import { normalizeDocs, normalizeIdent, normalizeTypeName } from "../util/normalize" -import { overrides } from "./overrides/overrides" -import { $field, Ty } from "./raw/Ty" - -/** - * All derived codecs for ZSTs will use this exact codec, - * so `derivedCodec === $null` is true iff the type is a ZST. - */ -export const $null = $.withMetadata($.metadata("$null"), $.constant(null)) - -export interface ScaleInfo { - ids: AnyShape[] - types: Record - paths: Record -} -export function transformTys(tys: Ty[]): ScaleInfo { - const tysMap = new Map(tys.map((ty) => [ty.id, ty])) - const memo = new Map() - const types: Record = {} - const paths: Record = {} - const seenPaths = new Map() - const includePaths = new Set() - const names = new Map() - const nameCounts = new Map>() - - for (const ty of tys) { - const path = ty.path.join("::") - if (!path) continue - const last = seenPaths.get(path) - if (last !== undefined) { - if (last === null || !eqTy(tysMap, last.id, ty.id)) { - seenPaths.set(path, null) - includePaths.delete(path) - } - continue - } - seenPaths.set(path, ty) - includePaths.add(path) - } - - for (const path of includePaths) { - const parts = path.split("::") - const name = parts.at(-1)! - const map = getOrInit(nameCounts, name, () => new Map()) - for (let i = 0; i < parts.length; i++) { - const pathPart = parts.slice(0, i).join("::") - map.set(pathPart, (map.get(pathPart) ?? 0) + 1) - } - } - - for (const path of includePaths) { - const parts = path.split("::") - const name = parts.at(-1)! - const map = nameCounts.get(name)! - const pathLength = parts.findIndex((_, i) => map.get(parts.slice(0, i).join("::")) === 1) - const newPath = [...parts.slice(0, pathLength), name].join("::") - const newName = normalizeTypeName(newPath) - names.set(path, newName) - } - - return { ids: tys.map((ty) => visit(ty.id)), types, paths } - - function visit(i: number): $.AnyShape { - return getOrInit(memo, i, () => { - memo.set( - i, - $.deferred(() => memo.get(i)!) - ) - const ty = tysMap.get(i)! - const path = ty.path.join("::") - const usePath = includePaths.has(path) - const name = names.get(path)! - if (usePath && types[name]) return types[name]! - const codec = withDocs(ty.docs, _visit(ty)) - if (usePath) return (types[name] ??= paths[path] = codec) - return codec - }) - } - - function _visit(ty: Ty): $.AnyShape { - const overrideFn = overrides[ty.path.join("::")] - if (overrideFn) return overrideFn(ty, visit) - if (ty.type === "Struct") { - if (ty.fields.length === 0) { - return $null - } else if (ty.fields[0]!.name === undefined) { - if (ty.fields.length === 1) { - // wrapper - return visit(ty.fields[0]!.ty) - } else { - return $.tuple(...ty.fields.map((x) => visit(x.ty))) - } - } else { - return $.object( - ...ty.fields.map((x) => - withDocs(x.docs, maybeOptionalField(normalizeIdent(x.name!), visit(x.ty))) - ) - ) - } - } else if (ty.type === "Tuple") { - if (ty.fields.length === 0) { - return $null - } else if (ty.fields.length === 1) { - // wrapper - return visit(ty.fields[0]!) - } else { - return $.tuple(...ty.fields.map((x) => visit(x))) - } - } else if (ty.type === "Union") { - if (ty.members.length === 0) { - return $.never as any - } else if (ty.members.every((x) => x.fields.length === 0)) { - const members: Record = {} - for (const { index, name } of ty.members) { - members[index] = normalizeIdent(name) - } - return $.literalUnion(members) - } else { - const members: Record> = {} - for (const { fields, name, index } of ty.members) { - let member: $.Variant - const type = normalizeIdent(name) - if (fields.length === 0) { - member = $.variant(type) - } else if (fields[0]!.name === undefined) { - // Tuple variant - const $value = - fields.length === 1 - ? visit(fields[0]!.ty) - : $.tuple(...fields.map((f) => visit(f.ty))) - member = $.variant(type, maybeOptionalField("value", $value)) - } else { - // Object variant - const memberFields = fields.map((field) => { - return withDocs( - field.docs, - maybeOptionalField(normalizeIdent(field.name!), visit(field.ty)) - ) - }) - member = $.variant(type, ...memberFields) - } - members[index] = member - } - return $.taggedUnion("type", members) - } - } else if (ty.type === "Sequence") { - const $inner = visit(ty.typeParam) - if ($inner === $.u8) { - return $.uint8Array - } else { - return $.array($inner) - } - } else if (ty.type === "SizedArray") { - const $inner = visit(ty.typeParam) - if ($inner === $.u8) { - return $.sizedUint8Array(ty.len) - } else { - return $.sizedArray($inner, ty.len) - } - } else if (ty.type === "Primitive") { - if (ty.kind === "char") return $.str - return $[ty.kind] - } else if (ty.type === "Compact") { - return $.compact(visit(ty.typeParam)) - } else if (ty.type === "BitSequence") { - return $.bitSequence - } else { - throw new Error("unreachable") - } - } -} - -function withDocs(_docs: string[], codec: Shape): Shape { - const docs = normalizeDocs(_docs) - if (docs) return $.documented(docs, codec) - return codec -} - -function eqTy(tysMap: Map, a: number, b: number) { - const seen = new Set() - return eqTy(a, b) - - function eqTy(ai: number, bi: number): boolean { - const key = `${ai}=${bi}` - if (seen.has(key)) return true - seen.add(key) - const a = tysMap.get(ai)! - const b = tysMap.get(bi)! - if (a.id === b.id) return true - if (a.type !== b.type) return false - if (a.path.join("::") !== b.path.join("::")) return false - if (normalizeDocs(a.docs) !== normalizeDocs(b.docs)) return false - if ( - !eqArray( - a.params, - b.params, - (a, b) => - a.name === b.name && - (a.ty == null) === (b.ty == null) && - (a.ty == null || eqTy(a.ty!, b.ty!)) - ) - ) { - return false - } - if (a.type === "BitSequence") { - return true - } - if (a.type === "Primitive" && b.type === "Primitive") { - return a.kind === b.kind - } - if ( - (a.type === "Compact" && b.type === "Compact") || - (a.type === "Sequence" && b.type === "Sequence") - ) { - return eqTy(a.typeParam, b.typeParam) - } - if (a.type === "SizedArray" && b.type === "SizedArray") { - return a.len === b.len && eqTy(a.typeParam, b.typeParam) - } - if (a.type === "Struct" && b.type === "Struct") { - return eqArray(a.fields, b.fields, eqField) - } - if (a.type === "Tuple" && b.type === "Tuple") { - return eqArray(a.fields, b.fields, eqTy) - } - if (a.type === "Union" && b.type === "Union") { - return eqArray( - a.members, - b.members, - (a, b) => - a.index === b.index && - a.name === b.name && - normalizeDocs(a.docs) === normalizeDocs(b.docs) && - eqArray(a.fields, b.fields, eqField) - ) - } - return false - } - - function eqField(a: $.Output, b: $.Output) { - return ( - a.name === b.name && a.typeName === b.typeName && eqDocs(a.docs, b.docs) && eqTy(a.ty, b.ty) - ) - } - - function eqDocs(a: string[], b: string[]) { - return normalizeDocs(a) === normalizeDocs(b) - } - - function eqArray(a: T[], b: T[], eqVal: (a: T, b: T) => boolean) { - return a.length === b.length && a.every((x, i) => eqVal(x, b[i]!)) - } -} - -const optionInnerVisitor = new $.ShapeVisitor<$.AnyShape | null>() - .add($.option, (_codec, $some) => $some) - .fallback(() => null) -function maybeOptionalField(key: PropertyKey, $value: $.AnyShape): $.AnyShape { - const $inner = optionInnerVisitor.visit($value) - return $inner ? $.optionalField(key, $inner) : $.field(key, $value) -} diff --git a/packages/scale/src/capi/util/index.ts b/packages/scale/src/capi/util/index.ts deleted file mode 100644 index 4c8cdbef8a..0000000000 --- a/packages/scale/src/capi/util/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export * from "./key" -export * from "./normalize" -export * from "./state" diff --git a/packages/scale/src/capi/util/key.ts b/packages/scale/src/capi/util/key.ts deleted file mode 100644 index b25a73ac9b..0000000000 --- a/packages/scale/src/capi/util/key.ts +++ /dev/null @@ -1,30 +0,0 @@ -/** - * Adapted from https://github.com/paritytech/capi-old copyright Parity Technologies (APACHE License 2.0) - * Changes August 19th 2023 : - * - updated to use subshape for scale decoding - * - adapted from deno to typescript - * - Copyright 2023 Parity Technologies - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. - */ - -const unquotedKey = /^(?!__proto__$)[$_\p{ID_Start}][$\p{ID_Continue}]+$|^\d+$/u - -export function stringifyKey(key: string) { - return unquotedKey.test(key) ? key : JSON.stringify(key) -} - -export function stringifyPropertyAccess(key: string) { - return unquotedKey.test(key) ? `.${key}` : `[${JSON.stringify(key)}]` -} diff --git a/packages/scale/src/capi/util/normalize.ts b/packages/scale/src/capi/util/normalize.ts deleted file mode 100644 index 6f5ccb15c1..0000000000 --- a/packages/scale/src/capi/util/normalize.ts +++ /dev/null @@ -1,105 +0,0 @@ -/** - * Adapted from https://github.com/paritytech/capi-old copyright Parity Technologies (APACHE License 2.0) - * Changes August 19th 2023 : - * - updated to use subshape for scale decoding - * - adapted from deno to typescript - * - Copyright 2023 Parity Technologies - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. - */ - -export function normalizeIdent(ident: string) { - if (ident.startsWith("r#")) ident = ident.slice(2) - return normalizeKeyword( - ident.replace(/(?:[^\p{ID_Continue}]|_)+(.)/gu, (_, $1: string) => $1.toUpperCase()) - ) -} - -export function normalizeDocs(docs: string[] | undefined): string { - let str = docs?.join("\n") ?? "" - str = str - .replace(/[^\S\n]+$/gm, "") // strip trailing whitespace - .replace(/^\n+|\n+$/g, "") // strip leading and trailing newlines - const match = /^([^\S\n]+).*(?:\n\1.*)*$/.exec(str) // find a common indent - if (match) { - const { 1: prefix } = match - str = str.replace(new RegExp(`^${prefix}`, "gm"), "") // strip the common indent - // this `new RegExp` is safe because `prefix` must be whitespace - } - return str -} - -export function normalizePackageName(name: string) { - return name.replace(/[A-Z]/g, (x) => `-` + x.toLowerCase()) -} - -export function normalizeTypeName(name: string) { - return normalizeIdent(name).replace(/^./, (x) => x.toUpperCase()) -} - -export function normalizeVariableName(name: string) { - return normalizeIdent(name).replace(/^./, (x) => x.toLowerCase()) -} - -// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Lexical_grammar#reserved_words -const keywords = [ - "break", - "case", - "catch", - "class", - "const", - "continue", - "debugger", - "delete", - "do", - "else", - "export", - "extends", - "false", - "finally", - "for", - "function", - "if", - "import", - "in", - "instanceof", - "new", - "null", - "return", - "super", - "switch", - "this", - "throw", - "true", - "try", - "typeof", - "var", - "void", - "while", - "with", - "let", - "static", - "yield", - "await", - "enum", - "implements", - "interface", - "package", - "private", - "protected", - "public", -] -function normalizeKeyword(ident: string) { - return keywords.includes(ident) ? ident + "_" : ident -} diff --git a/packages/scale/src/capi/util/state.ts b/packages/scale/src/capi/util/state.ts deleted file mode 100644 index 44261a5dcd..0000000000 --- a/packages/scale/src/capi/util/state.ts +++ /dev/null @@ -1,53 +0,0 @@ -/** - * Adapted from https://github.com/paritytech/capi-old copyright Parity Technologies (APACHE License 2.0) - * Changes August 19th 2023 : - * - updated to use subshape for scale decoding - * - adapted from deno to typescript - * - Copyright 2023 Parity Technologies - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. - */ - -export interface MapLike { - set(key: K, value: V): void - get(key: K): undefined | V -} - -export function getOrInit(container: MapLike, key: K, init: () => V): V { - let value = container.get(key) - if (value === undefined) { - value = init() - container.set(key, value) - } - return value -} - -// export class WeakRefMap { -// map = new Map>() -// finReg = new FinalizationRegistry((key) => this.map.delete(key)) -// get(key: K) { -// return this.map.get(key)?.deref() -// } -// set(key: K, value: V) { -// this.map.set(key, new WeakRef(value)) -// this.finReg.register(value, key, value) -// } -// delete(key: K) { -// const value = this.get(key) -// if (!value) return false -// this.map.delete(key) -// this.finReg.unregister(value) -// return true -// } -// } diff --git a/packages/scale/src/index.ts b/packages/scale/src/index.ts index 8371a4a2a7..2cd4b8d7e0 100644 --- a/packages/scale/src/index.ts +++ b/packages/scale/src/index.ts @@ -1,9 +1,4 @@ // TODO: Get the DX of this lib as close as possible to the DX of `const jsonResult = '{"someKey":"someValue"}'; JSON.decode(jsonResult)` -import { suppressPortableRegistryConsoleWarnings } from "./suppressPortableRegistryConsoleWarnings" - -export * from "./capi" -export * from "./metadata" -export * from "./storage" - -suppressPortableRegistryConsoleWarnings() +export * from "./papito" +export * from "./util" diff --git a/packages/scale/src/metadata/index.ts b/packages/scale/src/metadata/index.ts deleted file mode 100644 index 220210a54d..0000000000 --- a/packages/scale/src/metadata/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export { transformMetadata as transformMetadataV14 } from "../capi/frame_metadata/raw/v14" -export * from "./util" diff --git a/packages/scale/src/metadata/util.ts b/packages/scale/src/metadata/util.ts deleted file mode 100644 index ad25ecd209..0000000000 --- a/packages/scale/src/metadata/util.ts +++ /dev/null @@ -1,248 +0,0 @@ -import * as $ from "@talismn/subshape-fork" - -import { MetadataV14 } from "../capi" -import log from "../log" - -export type TyMV14 = MetadataV14["tys"][0] -export type PalletMV14 = MetadataV14["pallets"][0] -export type StorageEntryMV14 = NonNullable["entries"][0] - -export const getMetadataVersion = (metadataRpc: `0x${string}`) => { - // https://docs.substrate.io/build/application-development/#metadata-system - const magicNumber = 1635018093 - - const { version } = $.object( - $.field("magicNumber", $.constant(magicNumber, $.u32)), - $.field("version", $.u8) - ).decode($.decodeHex(metadataRpc)) - - return version -} - -export const filterMetadataPalletsAndItems = ( - metadata: MetadataV14, - palletsAndItems: Array<{ - pallet: (pallet: PalletMV14) => boolean - items: Array<(item: StorageEntryMV14) => boolean> - }>, - extraKeepTypes?: number[] -) => { - // remove pallets we don't care about - metadata.pallets = metadata.pallets.filter((pallet) => - // keep this pallet if it's listed in `palletsAndItems` - palletsAndItems.some(({ pallet: palletFilter }) => palletFilter(pallet)) - ) - - // remove fields we don't care about from each pallet, and extract types for each storage item we care about - const items = palletsAndItems.flatMap(({ pallet: palletFilter, items }) => { - const pallet = metadata.pallets.find(palletFilter) - if (!pallet) { - log.debug("Failed to find pallet", palletFilter) - return [] - } - - // remove fields we don't care about - pallet.calls = undefined - pallet.constants = [] - pallet.error = undefined - pallet.event = undefined - - if (!pallet.storage) return [] - - // filter and extract storage items we care about - pallet.storage.entries = pallet.storage.entries.filter((item) => - items.some((itemFilter) => itemFilter(item)) - ) - - return pallet.storage.entries - }) - - // this is a set of type ids which we plan to keep in our mutated metadata - // anything not in this set will be deleted - // we start off with just the types of the state calls we plan to make, - // then we run those types through a function (addDependentTypes) which will also include - // all of the types which those types depend on - recursively - const keepTypes = new Set( - items - .flatMap((item) => [ - // each type can be either "Plain" or "Map" - // if it's "Plain" we only need to get the value type - // if it's a "Map" we want to keep both the key AND the value types - item.type === "Map" && item.key, - item.value, - ]) - .filter((type): type is number => typeof type === "number") - ) - extraKeepTypes?.forEach((type) => keepTypes.add(type)) - - // recursively find all the types which our keepTypes depend on and add them to the keepTypes set - const metadataTysMap = new Map(metadata.tys.map((ty) => [ty.id, ty])) - addDependentTypes(metadataTysMap, keepTypes, [...keepTypes]) - - // ditch the types we aren't keeping - metadata.tys = metadata.tys.filter((type) => keepTypes.has(type.id)) - - // update all type ids to be sequential (fill the gaps left by the deleted types) - const newTypeIds = new Map() - metadata.tys.forEach((ty, index) => newTypeIds.set(ty.id, index)) - const getNewTypeId = (oldTypeId: number): number => { - const newTypeId = newTypeIds.get(oldTypeId) - if (typeof newTypeId !== "number") log.error(`Failed to find newTypeId for type ${oldTypeId}`) - return newTypeId ?? 0 - } - remapTypeIds(metadata, getNewTypeId) -} - -export const addDependentTypes = ( - metadataTysMap: Map, - keepTypes: Set, - types: number[], - // Prevent stack overflow when a type references itself - addedTypes: Set = new Set() -) => { - const addDependentSubTypes = (subTypes: number[]) => - addDependentTypes(metadataTysMap, keepTypes, subTypes, addedTypes) - - for (const typeId of types) { - const type = metadataTysMap.get(typeId) - if (!type) { - log.warn(`Unable to find type with id ${typeId}`) - continue - } - - if (addedTypes.has(type.id)) continue - keepTypes.add(type.id) - addedTypes.add(type.id) - - const paramTypes = type.params - .map((param) => param.ty) - .filter((type): type is number => typeof type === "number") - addDependentSubTypes(paramTypes) - - switch (type.type) { - case "SizedArray": - addDependentSubTypes([type.typeParam]) - break - - case "BitSequence": - addDependentSubTypes([type.bitOrderType, type.bitStoreType]) - break - - case "Compact": - addDependentSubTypes([type.typeParam]) - break - - case "Struct": - addDependentSubTypes( - type.fields.map((field) => field.ty).filter((ty): ty is number => typeof ty === "number") - ) - break - - case "Primitive": - break - - case "Sequence": - addDependentSubTypes([type.typeParam]) - break - - case "Tuple": - addDependentSubTypes(type.fields.filter((ty): ty is number => typeof ty === "number")) - break - - case "Union": - addDependentSubTypes( - type.members - .flatMap((member) => member.fields.map((field) => field.ty)) - .filter((ty): ty is number => typeof ty === "number") - ) - break - - default: { - // force compilation error if any types don't have a case - const exhaustiveCheck: never = type - log.error(`Unhandled TyMV14 type ${exhaustiveCheck}`) - } - } - } -} - -const remapTypeIds = (metadata: MetadataV14, getNewTypeId: (oldTypeId: number) => number) => { - remapLookupTypeIds(metadata, getNewTypeId) - remapStorageTypeIds(metadata, getNewTypeId) -} - -const remapLookupTypeIds = (metadata: MetadataV14, getNewTypeId: (oldTypeId: number) => number) => { - for (const type of metadata.tys) { - type.id = getNewTypeId(type.id) - - for (const param of type.params) { - if (typeof param.ty !== "number") continue - param.ty = getNewTypeId(param.ty) - } - - switch (type.type) { - case "SizedArray": - type.typeParam = getNewTypeId(type.typeParam) - break - - case "BitSequence": - type.bitOrderType = getNewTypeId(type.bitOrderType) - type.bitStoreType = getNewTypeId(type.bitStoreType) - break - - case "Compact": - type.typeParam = getNewTypeId(type.typeParam) - break - - case "Struct": - for (const field of type.fields) { - if (typeof field.ty !== "number") continue - field.ty = getNewTypeId(field.ty) - } - break - - case "Primitive": - break - - case "Sequence": - type.typeParam = getNewTypeId(type.typeParam) - break - - case "Tuple": - type.fields = type.fields.map((ty) => { - if (typeof ty !== "number") return ty - return getNewTypeId(ty) - }) - break - - case "Union": - for (const member of type.members) { - for (const field of member.fields) { - if (typeof field.ty !== "number") continue - field.ty = getNewTypeId(field.ty) - } - } - break - - default: { - // force compilation error if any types don't have a case - const exhaustiveCheck: never = type - log.error(`Unhandled TyMV14 type ${exhaustiveCheck}`) - } - } - } -} -const remapStorageTypeIds = ( - metadata: MetadataV14, - getNewTypeId: (oldTypeId: number) => number -) => { - for (const pallet of metadata.pallets) { - for (const item of pallet.storage?.entries ?? []) { - if (item.type === "Plain") item.value = getNewTypeId(item.value) - if (item.type === "Map") { - item.key = getNewTypeId(item.key) - item.value = getNewTypeId(item.value) - } - } - } -} diff --git a/packages/scale/src/papito.ts b/packages/scale/src/papito.ts new file mode 100644 index 0000000000..03146e3b8b --- /dev/null +++ b/packages/scale/src/papito.ts @@ -0,0 +1,8 @@ +export { getDynamicBuilder } from "@polkadot-api/metadata-builders" +export type { V14, V15, V15Extrinsic, Codec, Binary } from "@polkadot-api/substrate-bindings" +export { toHex, fromHex } from "@polkadot-api/utils" + +/** Constant: https://docs.substrate.io/build/application-development/#metadata-format */ +export const magicNumber = 1635018093 + +export { v14, v15, metadata } from "@polkadot-api/substrate-bindings" diff --git a/packages/scale/src/storage/getShape.ts b/packages/scale/src/storage/getShape.ts deleted file mode 100644 index e7d804a8c6..0000000000 --- a/packages/scale/src/storage/getShape.ts +++ /dev/null @@ -1,222 +0,0 @@ -/** - * This module is largely copied from https://github.com/0xKheops/substrate-metadata-explorer/blob/4b5a991e5ced45cad3b8675ff9104b8366d20429/packages/sme-codegen/src/types/getShape.ts - * - * The primary difference between this module and `sme-codegen` is in the output. - * - * The `sme-codegen` module exports typescript code as a string, which can then be interpretted in order to construct subshape objects. - * - * Whereas this module directly exports the subshape objects described by that code. - */ - -import * as $ from "@talismn/subshape-fork" - -import { MetadataV14, Ty, normalizeIdent } from "../capi" -import { getTypeName } from "./getTypeName" - -type GotShapes = Map - -export const getShape = ( - metadata: MetadataV14, - typeId: number, - gotShapes: GotShapes = new Map() -): $.AnyShape => { - const type = metadata.tys.find((type) => type.id === typeId) - if (!type) throw new Error(`Type not found (${typeId})`) - - // Short circuit if we've already encountered this shape - const typeName = getTypeName(metadata, type) - if (gotShapes.has(typeName)) return gotShapes.get(typeName)! - - // TODO: Use `$.deferred(() => Type)` for self-referential types - - // Get shape, add to gotShapes list, return shape - const shape = getTypeShape(metadata, type, gotShapes) - gotShapes.set(typeName, shape) - - return shape -} - -export const getTypeShape = (metadata: MetadataV14, type: Ty, gotShapes: GotShapes): $.AnyShape => { - const tyType = type.type - switch (tyType) { - case "Primitive": - return getPrimitiveShape(type) - case "SizedArray": - return getSizedArrayShape(metadata, type, gotShapes) - case "Compact": - return getCompactShape(metadata, type, gotShapes) - case "Sequence": - return getSequenceShape(metadata, type, gotShapes) - case "Struct": - return getStructShape(metadata, type, gotShapes) - case "Tuple": - return getTupleShape(metadata, type, gotShapes) - case "Union": - return getUnionShape(metadata, type, gotShapes) - case "BitSequence": - return $.bitSequence - default: { - // force compilation error if any types don't have a case - const exhaustiveCheck: never = tyType - throw new Error(`Unsupported type shape ${exhaustiveCheck}`) - } - } -} - -const tyIsU8 = (type?: Ty): boolean => type?.type === "Primitive" && type.kind === "u8" - -const getPrimitiveShape = (primitive: Extract): $.AnyShape => { - // TODO: Test that `char` and `$.u8` are equivalent (`$.char` does not exist) - if (primitive.kind === "char") return $.u8 - return $[primitive.kind] -} - -const getSizedArrayShape = ( - metadata: MetadataV14, - sizedArray: Extract, - gotShapes: GotShapes -): $.AnyShape => { - // Get the type definition for the items of this array from the metadata - const typeParam = metadata.tys.find(({ id }) => id === sizedArray.typeParam) - if (!typeParam) { - const typeName = getTypeName(metadata, sizedArray) - throw new Error(`Could not find typeParam ${sizedArray.typeParam} for sizedArray ${typeName}`) - } - - // Shortcut for uint8 arrays - if (tyIsU8(typeParam)) return $.sizedUint8Array(sizedArray.len) - - // Get the subshape object for the items of this array - const typeParamShape = getShape(metadata, typeParam.id, gotShapes) - - // Return a subshape sizedArray - return $.sizedArray(typeParamShape, sizedArray.len) -} - -const getCompactShape = ( - metadata: MetadataV14, - compact: Extract, - gotShapes: GotShapes -): $.AnyShape => { - // Get the type definition for the item of this compact from the metadata - const typeParam = metadata.tys.find(({ id }) => id === compact.typeParam) - if (!typeParam) { - const typeName = getTypeName(metadata, compact) - throw new Error(`Could not find typeParam ${compact.typeParam} for compact ${typeName}`) - } - - // Get the subshape object for the item of this compact - const typeParamShape = getShape(metadata, typeParam.id, gotShapes) - - // Return a subshape compact - return $.compact(typeParamShape) -} - -const getSequenceShape = ( - metadata: MetadataV14, - sequence: Extract, - gotShapes: GotShapes -): $.AnyShape => { - // Get the type definition for the items of this sequence from the metadata - const typeParam = metadata.tys.find(({ id }) => id === sequence.typeParam) - if (!typeParam) { - const typeName = getTypeName(metadata, sequence) - throw new Error(`Could not find typeParam ${sequence.typeParam} for sequence ${typeName}`) - } - - // Shortcut for uint8 sequences - if (tyIsU8(typeParam)) return $.uint8Array - - // Get the subshape object for the items of this sequence - const typeParamShape = getShape(metadata, typeParam.id, gotShapes) - - // Return a subshape sequence - return $.array(typeParamShape) -} - -const getStructShape = ( - metadata: MetadataV14, - struct: Extract, - gotShapes: GotShapes -): $.AnyShape => { - // If there's only one field and it has no name, don't wrap it in $.object - if (struct.fields.length === 1 && !struct.fields[0].name) - return getShape(metadata, struct.fields[0].ty, gotShapes) - - // Check that all fields have a name - if (!struct.fields.every((field) => field.name)) { - const typeName = getTypeName(metadata, struct) - throw new Error( - `Could not build subshape object for struct ${struct.id} (${typeName})): Not all fields have a name` - ) - } - - // Get the type definition for the fields of this struct from the metadata - const fieldsShape: $.AnyShape[] = struct.fields.map((field) => - $.field(normalizeIdent(field.name!), getShape(metadata, field.ty, gotShapes)) - ) - - return $.object(...fieldsShape) -} - -const getTupleShape = ( - metadata: MetadataV14, - tuple: Extract, - gotShapes: GotShapes -): $.AnyShape => - // Get the type definition for the fields of this tuple from the metadata and wrap them in `$.tuple` - $.tuple(...tuple.fields.map((type) => getShape(metadata, type, gotShapes))) - -const getUnionShape = ( - metadata: MetadataV14, - union: Extract, - gotShapes: GotShapes -): $.AnyShape => { - if (union.members.every((member) => !member.fields.length)) - return $.literalUnion( - Object.fromEntries(union.members.map((member) => [member.index, normalizeIdent(member.name)])) - ) - - // TODO: Check if invalid - if ( - union.members.length === 2 && - union.path[union.path.length - 1] === "Option" && - union.members[0]?.name === "None" && - union.members[1]?.name === "Some" - ) - return $.option(getShape(metadata, union.members[1].fields[0].ty, gotShapes)) - - return $.taggedUnion( - "type", - Object.fromEntries( - union.members.map((member) => { - const args: $.AnyShape[] = [] - - // invalid if only some fields (but not all of them) have a name - if ( - member.fields.some((field) => field.name) && - !member.fields.every((field) => field.name) - ) { - const typeName = getTypeName(metadata, union) - throw new Error( - `Could not build subshape object for union ${union.id} (${typeName}): Not all fields have a name` - ) - } - - if (member.fields.every((field) => field.name)) - for (const field of member.fields) - args.push($.field(normalizeIdent(field.name!), getShape(metadata, field.ty, gotShapes))) - else if (member.fields.length > 1) - args.push( - $.field( - "value", - $.tuple(...member.fields.map((field) => getShape(metadata, field.ty, gotShapes))) - ) - ) - else args.push($.field("value", getShape(metadata, member.fields[0].ty, gotShapes))) - - return [member.index, $.variant(normalizeIdent(member.name), ...args)] - }) - ) - ) -} diff --git a/packages/scale/src/storage/getTypeName.ts b/packages/scale/src/storage/getTypeName.ts deleted file mode 100644 index e107591dea..0000000000 --- a/packages/scale/src/storage/getTypeName.ts +++ /dev/null @@ -1,60 +0,0 @@ -/** - * This module is largely copied from https://github.com/0xKheops/substrate-metadata-explorer/blob/4b5a991e5ced45cad3b8675ff9104b8366d20429/packages/sme-codegen/src/types/getConstantVariableName.ts - * - * The primary difference between this module and `sme-codegen` is in the output. - * - * The `sme-codegen` module exports typescript code as a string, which can then be interpretted in order to construct subshape objects. - * - * Whereas this module directly exports the subshape objects described by that code. - */ - -import { MetadataV14, Ty, normalizeTypeName } from "../capi" - -/** Returns a unique name (relative to all of the types in `metadata`) to identify this type. */ -export const getTypeName = (metadata: MetadataV14, type: Ty) => { - const uniqueName = getUniqueTypeName(metadata, type) - return `${uniqueName.charAt(0).toUpperCase()}${uniqueName.substring(1)}` -} - -/** - * Tries each of `getSimpleTypeName`, `getSmartTypeName`, `getFullTypeName` in order and - * returns the first name which is unique in the collection of types in `metadata`. - */ -export const getUniqueTypeName = (metadata: MetadataV14, type: Ty) => { - const rawTypeName = getRawTypeName(type) - if (type.path.length < 1) return rawTypeName - - // use simpleName if it is unique - const simpleName = getSimpleTypeName(type) - if (!metadata.tys.some((t) => t.id !== type.id && getSimpleTypeName(t) === simpleName)) - return simpleName - - // use smartName if it is unique - const smartName = getSmartTypeName(type) - if (!metadata.tys.some((t) => t.id !== type.id && getSmartTypeName(t) === smartName)) - return smartName - - // use fullName if it is unique - const fullName = getFullTypeName(type) - if (!metadata.tys.some((t) => t.id !== type.id && getFullTypeName(t) === fullName)) - // return if fullName is unique - return fullName - - // use fullName + type number - return `${fullName}${type.id}` -} - -/** Gets "Type" + type number */ -export const getRawTypeName = (type: Ty) => `Type${type.id}` - -/** Gets the last element of `type.path` */ -export const getSimpleTypeName = (type: Ty) => type.path.slice(-1)[0] - -/** Gets the first two elements, and the last element, of `type.path` and joins them together with `::` */ -export const getSmartTypeName = (type: Ty) => - type.path.length > 3 - ? normalizeTypeName([...type.path.slice(0, 2), ...type.path.slice(-1)].join("::")) - : getFullTypeName(type) - -/** Gets all elements of `type.path` and joins them together with `::` */ -export const getFullTypeName = (type: Ty) => normalizeTypeName(type.path.join("::")) diff --git a/packages/scale/src/storage/index.ts b/packages/scale/src/storage/index.ts deleted file mode 100644 index 2cc5b6eb2c..0000000000 --- a/packages/scale/src/storage/index.ts +++ /dev/null @@ -1,12 +0,0 @@ -/** - * This module is largely copied from https://github.com/0xKheops/substrate-metadata-explorer/tree/4b5a991e5ced45cad3b8675ff9104b8366d20429/packages/sme-codegen - * - * The primary difference between this module and `sme-codegen` is in the output. - * - * The `sme-codegen` module exports typescript code as a string, which can then be interpretted in order to construct subshape objects. - * - * Whereas this module directly exports the subshape objects described by that code. - */ - -export * from "./getShape" -export * from "./getTypeName" diff --git a/packages/scale/src/suppressPortableRegistryConsoleWarnings.ts b/packages/scale/src/suppressPortableRegistryConsoleWarnings.ts deleted file mode 100644 index 0a5ddaef5f..0000000000 --- a/packages/scale/src/suppressPortableRegistryConsoleWarnings.ts +++ /dev/null @@ -1,26 +0,0 @@ -const ignoreModuleMessages: Record = { - "PORTABLEREGISTRY:": [ - "Unable to determine runtime Event type, cannot inspect frame_system::EventRecord", - "Unable to determine runtime Call type, cannot inspect sp_runtime::generic::unchecked_extrinsic::UncheckedExtrinsic", - ], -} - -export function suppressPortableRegistryConsoleWarnings() { - /* eslint-disable-next-line no-console */ - const originalWarn = console.warn - - /* eslint-disable-next-line no-console */ - console.warn = (...data: Parameters) => { - const [, dataModule, dataMessage] = data - const ignoreMessages = typeof dataModule === "string" && ignoreModuleMessages[dataModule] - if (Array.isArray(ignoreMessages) && ignoreMessages.includes(dataMessage)) return - if (data[0] === "Unable to map Bytes to a lookup index") return - if (data[0] === "Unable to map [u8; 32] to a lookup index") return - if (data[0] === "Unable to map u16 to a lookup index") return - if (data[0] === "Unable to map u32 to a lookup index") return - if (data[0] === "Unable to map u64 to a lookup index") return - if (data[0] === "Unable to map u128 to a lookup index") return - - originalWarn(...data) - } -} diff --git a/packages/scale/src/util/compactMetadata.ts b/packages/scale/src/util/compactMetadata.ts new file mode 100644 index 0000000000..7a924155c8 --- /dev/null +++ b/packages/scale/src/util/compactMetadata.ts @@ -0,0 +1,271 @@ +import log from "../log" +import { V14, V15 } from "../papito" + +export type V14Type = V14["lookup"][0] +export type V14Pallet = V14["pallets"][0] +export type V14StorageItem = NonNullable["items"][0] + +export type V15Type = V15["lookup"][0] +export type V15Pallet = V15["pallets"][0] +export type V15StorageItem = NonNullable["items"][0] + +/** + * Converts a `Metadata` into a `MiniMetadata`. + * + * A `MiniMetadata` only contains the types inside of its `lookup` which are relevant for + * the storage queries specified in `palletsAndItems`. + * + * E.g. if `palletsAndItems` is `{ pallet: "System", items: ["Account"] }`, then only the + * types used in the `System.Account` storage query will remain inside of metadata.lookups. + */ +export const compactMetadata = ( + metadata: V15 | V14, + palletsAndItems: Array<{ + pallet: string + items: string[] + }>, + extraKeepTypes?: number[] +) => { + // remove pallets we don't care about + metadata.pallets = metadata.pallets.filter((pallet) => + // keep this pallet if it's listed in `palletsAndItems` + palletsAndItems.some(({ pallet: palletName }) => pallet.name === palletName) + ) + + // remove fields we don't care about from each pallet, and extract types for each storage item we care about + const items = palletsAndItems.flatMap(({ pallet: palletName, items: itemNames }) => { + const pallet = metadata.pallets.find((pallet) => pallet.name === palletName) + if (!pallet) { + log.debug("Failed to find pallet", palletName) + return [] + } + + // remove pallet fields we don't care about + pallet.calls = undefined + pallet.constants = [] + // v15 (NOT v14) has docs + if ("docs" in pallet) pallet.docs = [] + pallet.errors = undefined + pallet.events = undefined + + if (!pallet.storage) return [] + + // filter and extract storage items we care about + pallet.storage.items = pallet.storage.items.filter((item) => + itemNames.some((itemName) => item.name === itemName) + ) + + return pallet.storage.items + }) + + // this is a set of type ids which we plan to keep in our compacted metadata + // anything not in this set will be deleted + // we start off with just the types of the state calls we plan to make, + // then we run those types through a function (addDependentTypes) which will also include + // all of the types which those types depend on - recursively + const keepTypes = new Set( + items + .flatMap((item) => [ + // each type can be either "Plain" or "Map" + // if it's "Plain" we only need to get the value type + // if it's a "Map" we want to keep both the key AND the value types + item.type.tag === "plain" && item.type.value, + item.type.tag === "map" && item.type.value.key, + item.type.tag === "map" && item.type.value.value, + ]) + .filter((type): type is number => typeof type === "number") + ) + extraKeepTypes?.forEach((type) => keepTypes.add(type)) + + // recursively find all the types which our keepTypes depend on and add them to the keepTypes set + const metadataTysMap = new Map(metadata.lookup.map((ty) => [ty.id, ty])) + addDependentTypes(metadataTysMap, keepTypes, [...keepTypes]) + + // ditch the types we aren't keeping + metadata.lookup = metadata.lookup.filter((type) => keepTypes.has(type.id)) + + // update all type ids to be sequential (fill the gaps left by the deleted types) + const newTypeIds = new Map() + metadata.lookup.forEach((type, index) => newTypeIds.set(type.id, index)) + const getNewTypeId = (oldTypeId: number): number => { + const newTypeId = newTypeIds.get(oldTypeId) + if (typeof newTypeId !== "number") log.error(`Failed to find newTypeId for type ${oldTypeId}`) + return newTypeId ?? 0 + } + remapTypeIds(metadata, getNewTypeId) + + // ditch the remaining data we don't need to keep in a miniMetata + if ("apis" in metadata) { + // metadata is v15 (NOT v14) + metadata.apis = [] + } + if ("address" in metadata.extrinsic) { + // metadata is v15 (NOT v14) + metadata.extrinsic.address = 0 + metadata.extrinsic.call = 0 + metadata.extrinsic.extra = 0 + metadata.extrinsic.signature = 0 + } + metadata.extrinsic.signedExtensions = [] + if ("outerEnums" in metadata) { + // metadata is v15 (NOT v14) + metadata.outerEnums.call = 0 + metadata.outerEnums.error = 0 + metadata.outerEnums.event = 0 + } +} + +const addDependentTypes = ( + metadataTysMap: Map, + keepTypes: Set, + types: number[], + // Prevent stack overflow when a type references itself + addedTypes: Set = new Set() +) => { + const addDependentSubTypes = (subTypes: number[]) => + addDependentTypes(metadataTysMap, keepTypes, subTypes, addedTypes) + + for (const typeId of types) { + const type = metadataTysMap.get(typeId) + if (!type) { + log.warn(`Unable to find type with id ${typeId}`) + continue + } + + if (addedTypes.has(type.id)) continue + keepTypes.add(type.id) + addedTypes.add(type.id) + + const paramTypes = type.params + .map((param) => param.type) + .filter((type): type is number => typeof type === "number") + addDependentSubTypes(paramTypes) + + switch (type.def.tag) { + case "array": + addDependentSubTypes([type.def.value.type]) + break + + case "bitSequence": + addDependentSubTypes([type.def.value.bitOrderType, type.def.value.bitStoreType]) + break + + case "compact": + addDependentSubTypes([type.def.value]) + break + + case "composite": + addDependentSubTypes( + type.def.value + .map((field) => field.type) + .filter((type): type is number => typeof type === "number") + ) + break + + case "primitive": + break + + case "sequence": + addDependentSubTypes([type.def.value]) + break + + case "tuple": + addDependentSubTypes( + type.def.value.filter((type): type is number => typeof type === "number") + ) + break + + case "variant": + addDependentSubTypes( + type.def.value + .flatMap((member) => member.fields.map((field) => field.type)) + .filter((type): type is number => typeof type === "number") + ) + break + + default: { + // force compilation error if any types don't have a case + const exhaustiveCheck: never = type.def + log.error(`Unhandled V15Type type ${exhaustiveCheck}`) + } + } + } +} + +const remapTypeIds = (metadata: V14 | V15, getNewTypeId: (oldTypeId: number) => number) => { + remapLookupTypeIds(metadata, getNewTypeId) + remapStorageTypeIds(metadata, getNewTypeId) +} + +const remapLookupTypeIds = (metadata: V14 | V15, getNewTypeId: (oldTypeId: number) => number) => { + for (const type of metadata.lookup) { + type.id = getNewTypeId(type.id) + + for (const param of type.params) { + if (typeof param.type !== "number") continue + param.type = getNewTypeId(param.type) + } + + switch (type.def.tag) { + case "array": + type.def.value.type = getNewTypeId(type.def.value.type) + break + + case "bitSequence": + type.def.value.bitOrderType = getNewTypeId(type.def.value.bitOrderType) + type.def.value.bitStoreType = getNewTypeId(type.def.value.bitStoreType) + break + + case "compact": + type.def.value = getNewTypeId(type.def.value) + break + + case "composite": + for (const field of type.def.value) { + if (typeof field.type !== "number") continue + field.type = getNewTypeId(field.type) + } + break + + case "primitive": + break + + case "sequence": + type.def.value = getNewTypeId(type.def.value) + break + + case "tuple": + type.def.value = type.def.value.map((type) => { + if (typeof type !== "number") return type + return getNewTypeId(type) + }) + break + + case "variant": + for (const member of type.def.value) { + for (const field of member.fields) { + if (typeof field.type !== "number") continue + field.type = getNewTypeId(field.type) + } + } + break + + default: { + // force compilation error if any types don't have a case + const exhaustiveCheck: never = type.def + log.error(`Unhandled V15Type type ${exhaustiveCheck}`) + } + } + } +} +const remapStorageTypeIds = (metadata: V14 | V15, getNewTypeId: (oldTypeId: number) => number) => { + for (const pallet of metadata.pallets) { + for (const item of pallet.storage?.items ?? []) { + if (item.type.tag === "plain") item.type.value = getNewTypeId(item.type.value) + if (item.type.tag === "map") { + item.type.value.key = getNewTypeId(item.type.value.key) + item.type.value.value = getNewTypeId(item.type.value.value) + } + } + } +} diff --git a/packages/scale/src/util/decodeMetadata.ts b/packages/scale/src/util/decodeMetadata.ts new file mode 100644 index 0000000000..a3b86aecf4 --- /dev/null +++ b/packages/scale/src/util/decodeMetadata.ts @@ -0,0 +1,23 @@ +import { V14, V15 } from "@polkadot-api/substrate-bindings" + +import { metadata as scaleMetadata } from "../papito" +import { getMetadataVersion } from "./getMetadataVersion" + +export const decodeMetadata = ( + metadataRpc: string +): { metadataVersion: number } & ( + | { metadata: V15; tag: "v15" } + | { metadata: V14; tag: "v14" } + | { metadata?: undefined; tag?: undefined } +) => { + const metadataVersion = getMetadataVersion(metadataRpc) + if (metadataVersion !== 15 && metadataVersion !== 14) return { metadataVersion } + + const decoded = scaleMetadata.dec(metadataRpc) + if (decoded.metadata.tag === "v15") + return { metadataVersion, metadata: decoded.metadata.value, tag: decoded.metadata.tag } + if (decoded.metadata.tag === "v14") + return { metadataVersion, metadata: decoded.metadata.value, tag: decoded.metadata.tag } + + return { metadataVersion } +} diff --git a/packages/scale/src/util/decodeScale.ts b/packages/scale/src/util/decodeScale.ts new file mode 100644 index 0000000000..1eb156795b --- /dev/null +++ b/packages/scale/src/util/decodeScale.ts @@ -0,0 +1,20 @@ +import { getDynamicBuilder } from "@polkadot-api/metadata-builders" + +import log from "../log" + +type ScaleStorageCoder = ReturnType["buildStorage"]> + +export const decodeScale = ( + scaleCoder: ScaleStorageCoder | undefined, + change: string | null, + error?: string +): T | null => { + if (change === null) return null + + try { + return (scaleCoder?.dec(change) as T | undefined) ?? null + } catch (cause) { + log.warn(error ?? `Failed to decode ${change}`, cause) + return null + } +} diff --git a/packages/scale/src/util/encodeMetadata.ts b/packages/scale/src/util/encodeMetadata.ts new file mode 100644 index 0000000000..2c6325a91d --- /dev/null +++ b/packages/scale/src/util/encodeMetadata.ts @@ -0,0 +1,14 @@ +import { V14, V15 } from "@polkadot-api/substrate-bindings" + +import { magicNumber, metadata as scaleMetadata, toHex } from "../papito" + +export const encodeMetadata = ({ + metadata, + tag, +}: { metadata: V15; tag: "v15" } | { metadata: V14; tag: "v14" }) => + toHex( + scaleMetadata.enc({ + magicNumber, + metadata: tag === "v15" ? { tag, value: metadata } : { tag, value: metadata }, + }) + ) diff --git a/packages/scale/src/util/encodeStateKey.ts b/packages/scale/src/util/encodeStateKey.ts new file mode 100644 index 0000000000..654426c228 --- /dev/null +++ b/packages/scale/src/util/encodeStateKey.ts @@ -0,0 +1,18 @@ +import { getDynamicBuilder } from "@polkadot-api/metadata-builders" + +import log from "../log" + +type ScaleStorageCoder = ReturnType["buildStorage"]> + +export const encodeStateKey = ( + scaleCoder: ScaleStorageCoder | undefined, + error?: string, + ...args: any[] // eslint-disable-line @typescript-eslint/no-explicit-any +): string | undefined => { + try { + return scaleCoder?.enc(...args) + } catch (cause) { + log.warn(error ?? `Failed to encode stateKey ${JSON.stringify(args)}`, cause) + return + } +} diff --git a/packages/scale/src/util/getMetadataVersion.ts b/packages/scale/src/util/getMetadataVersion.ts new file mode 100644 index 0000000000..551e4f5a5f --- /dev/null +++ b/packages/scale/src/util/getMetadataVersion.ts @@ -0,0 +1,17 @@ +import { Struct, u8, u32 } from "scale-ts" + +/** + * Extracts the `version` u8 from a SCALE-encoded metadata blob and returns it as a `number`. + * + * Only reads the first 40 bytes of the blob. + */ +export const getMetadataVersion = (metadataRpc: string | Uint8Array | ArrayBuffer) => { + try { + return Struct({ + magicNumber: u32, + version: u8, + }).dec(metadataRpc).version + } catch { + return 0 + } +} diff --git a/packages/scale/src/util/index.ts b/packages/scale/src/util/index.ts new file mode 100644 index 0000000000..0b5c399565 --- /dev/null +++ b/packages/scale/src/util/index.ts @@ -0,0 +1,6 @@ +export * from "./compactMetadata" +export * from "./decodeMetadata" +export * from "./decodeScale" +export * from "./encodeMetadata" +export * from "./encodeStateKey" +export * from "./getMetadataVersion" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index aa2af4a735..eb414938c0 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -876,9 +876,6 @@ importers: '@talismn/scale': specifier: workspace:* version: link:../scale - '@talismn/subshape-fork': - specifier: ^0.0.2 - version: 0.0.2 '@talismn/token-rates': specifier: workspace:* version: link:../token-rates @@ -903,6 +900,9 @@ importers: rxjs: specifier: ^7.8.1 version: 7.8.1 + scale-ts: + specifier: ^1.6.0 + version: 1.6.0 viem: specifier: ^2.8.18 version: 2.8.18(bufferutil@4.0.8)(typescript@5.3.3)(utf-8-validate@6.0.3) @@ -1574,25 +1574,22 @@ importers: packages/scale: dependencies: - '@talismn/subshape-fork': - specifier: ^0.0.2 - version: 0.0.2 - '@talismn/util': - specifier: workspace:* - version: link:../util + '@polkadot-api/metadata-builders': + specifier: ^0.2.0 + version: 0.2.0 + '@polkadot-api/substrate-bindings': + specifier: ^0.2.0 + version: 0.2.0 + '@polkadot-api/utils': + specifier: ^0.0.1 + version: 0.0.1 anylogger: specifier: ^1.0.11 version: 1.0.11 - wasm-feature-detect: - specifier: ^1.6.1 - version: 1.6.1 - wat-the-crypto: - specifier: ^0.0.3 - version: 0.0.3 + scale-ts: + specifier: ^1.6.0 + version: 1.6.0 devDependencies: - '@polkadot/util-crypto': - specifier: 12.6.2 - version: 12.6.2(@polkadot/util@12.6.2) '@talismn/eslint-config': specifier: workspace:* version: link:../../config/eslint-config @@ -3898,6 +3895,9 @@ packages: '@polkadot-api/metadata-builders@0.0.1': resolution: {integrity: sha512-GCI78BHDzXAF/L2pZD6Aod/yl82adqQ7ftNmKg51ixRL02JpWUA+SpUKTJE5MY1p8kiJJIo09P2um24SiJHxNA==} + '@polkadot-api/metadata-builders@0.2.0': + resolution: {integrity: sha512-Nyu2oC6ZG3vThO0Y5/hVsE5m+1DCBirkMdi2Kg9QiSrZpWDAif4STkUZHNZ5KJcFBtfooBMh64t/hpaDwokvBg==} + '@polkadot-api/observable-client@0.1.0': resolution: {integrity: sha512-GBCGDRztKorTLna/unjl/9SWZcRmvV58o9jwU2Y038VuPXZcr01jcw/1O3x+yeAuwyGzbucI/mLTDa1QoEml3A==} peerDependencies: @@ -3906,6 +3906,9 @@ packages: '@polkadot-api/substrate-bindings@0.0.1': resolution: {integrity: sha512-bAe7a5bOPnuFVmpv7y4BBMRpNTnMmE0jtTqRUw/+D8ZlEHNVEJQGr4wu3QQCl7k1GnSV1wfv3mzIbYjErEBocg==} + '@polkadot-api/substrate-bindings@0.2.0': + resolution: {integrity: sha512-lG47UIJwnesakR52IdymXmqdE/da29kvVFoGm2TenIPn9sVx5TqKQub2cXlkzzIt6xtlbvPm3Y4KI9vqMRpSjg==} + '@polkadot-api/substrate-client@0.0.1': resolution: {integrity: sha512-9Bg9SGc3AwE+wXONQoW8GC00N3v6lCZLW74HQzqB6ROdcm5VAHM4CB/xRzWSUF9CXL78ugiwtHx3wBcpx4H4Wg==} @@ -11857,12 +11860,6 @@ packages: walker@1.0.8: resolution: {integrity: sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==} - wasm-feature-detect@1.6.1: - resolution: {integrity: sha512-R1i9ED8UlLu/foILNB1ck9XS63vdtqU/tP1MCugVekETp/ySCrBZRk5I/zI67cI1wlQYeSonNm1PLjDHZDNg6g==} - - wat-the-crypto@0.0.3: - resolution: {integrity: sha512-Ob4HtNrDNUnk20CKZKBPaawn5R8wGqFhv1+RH/v3ypFyi0GnB1YE0xwrvW1JQwzoUHmcQh26tiO4seF4Qvy3Kw==} - watchpack@2.4.0: resolution: {integrity: sha512-Lcvm7MGST/4fup+ifyKi2hjyIAwcdI4HRgtvTpIUxBRhB+RFtUh8XtDOxUfctVCnhVi+QQj49i91OyvzkJl6cg==} engines: {node: '>=10.13.0'} @@ -15585,6 +15582,11 @@ snapshots: '@polkadot-api/utils': 0.0.1 optional: true + '@polkadot-api/metadata-builders@0.2.0': + dependencies: + '@polkadot-api/substrate-bindings': 0.2.0 + '@polkadot-api/utils': 0.0.1 + '@polkadot-api/observable-client@0.1.0(rxjs@7.8.1)': dependencies: '@polkadot-api/metadata-builders': 0.0.1 @@ -15602,11 +15604,17 @@ snapshots: scale-ts: 1.6.0 optional: true + '@polkadot-api/substrate-bindings@0.2.0': + dependencies: + '@noble/hashes': 1.3.3 + '@polkadot-api/utils': 0.0.1 + '@scure/base': 1.1.5 + scale-ts: 1.6.0 + '@polkadot-api/substrate-client@0.0.1': optional: true - '@polkadot-api/utils@0.0.1': - optional: true + '@polkadot-api/utils@0.0.1': {} '@polkadot/api-augment@12.0.2': dependencies: @@ -26352,10 +26360,6 @@ snapshots: dependencies: makeerror: 1.0.12 - wasm-feature-detect@1.6.1: {} - - wat-the-crypto@0.0.3: {} - watchpack@2.4.0: dependencies: glob-to-regexp: 0.4.1