diff --git a/CHANGELOG.md b/CHANGELOG.md index 76c5ed54bf..ee7e473ac9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,9 +4,10 @@ Changes: +- Allow storage queries with default keys values - Align identity `Data` to limit to 32 bytes in the `Raw` enum (as per Substrate) -- Cleanup internal tests (no dangling handles, cater for Jest 27) - Align static (test & docs-only) metadata with latest Substrate master +- Cleanup internal tests (no dangling handles, cater for Jest 27) ## 4.11.2 May 24, 2021 diff --git a/packages/api/test/api.spec.ts b/packages/api/test/api.spec.ts index 2b606bfb0d..6952c3246e 100644 --- a/packages/api/test/api.spec.ts +++ b/packages/api/test/api.spec.ts @@ -3,34 +3,33 @@ import { ApiPromise, WsProvider } from '@polkadot/api'; -function createApi (): Promise { - jest.setTimeout(30000); - process.env.NODE_ENV = 'test'; +describe('misc online tests', (): void => { + let api: ApiPromise; - const provider = new WsProvider('wss://kusama-rpc.polkadot.io'); - // const provider = new WsProvider('wss://westend-rpc.polkadot.io/'); - // const provider = new WsProvider('ws://127.0.0.1:9944/'); + beforeEach(async () => { + jest.setTimeout(30000); + process.env.NODE_ENV = 'test'; - return new ApiPromise({ provider }).isReady; -} + // const provider = new WsProvider('wss://kusama-rpc.polkadot.io'); + const provider = new WsProvider('wss://westend-rpc.polkadot.io/'); + + api = await ApiPromise.create({ provider }); + }); + + afterEach(async () => { + await api.disconnect(); + }); -describe('misc quick tests', (): void => { it.skip('retrieves balances correctly', async (): Promise => { - const api = await createApi(); - console.error(JSON.stringify( await api.query.system.account('FPzukZw2mphWsXDqdkNzLaxnanjZEWH9i2vqwobTdtde5me') )); console.error(JSON.stringify( await api.query.system.account('HUewJvzVuEeyaxH2vx9XiyAPKrpu1Zj5r5Pi9VrGiBVty7q') )); - - await api.disconnect(); }); it.skip('handles map keys', async (): Promise => { - const api = await createApi(); - console.time('map.keys'); const keys = await api.query.system.account.keys(); @@ -38,13 +37,9 @@ describe('misc quick tests', (): void => { console.error('# keys', keys.length); console.timeEnd('map.keys'); - - await api.disconnect(); }); it.skip('handles map entries', async (): Promise => { - const api = await createApi(); - console.time('map.entries'); const entries = await api.query.system.account.entries(); // api.query.indices.accounts.entries(); @@ -52,50 +47,25 @@ describe('misc quick tests', (): void => { console.error('# entries', entries.length); console.timeEnd('map.entries'); - - await api.disconnect(); }); it.skip('handles doublemap entries', async (): Promise => { - const api = await createApi(); const activeEra = await api.query.staking.activeEra(); console.error(JSON.stringify( await api.query.staking.erasStakers.entries(activeEra.unwrapOrDefault().index) )); - - await api.disconnect(); }); it.skip('does something in society', async (): Promise => { - const api = await createApi(); - console.error(JSON.stringify( await api.query.society.defenderVotes('Dab4bfYTZRUDMWjYAUQuFbDreQ9mt7nULWu3Dw7jodbzVe9') )); - - await api.disconnect(); }); - it.skip('allows for range queries', async (): Promise => { - const api = await createApi(); - const header = await api.rpc.chain.getHeader(); - - console.error(JSON.stringify( - (await api.query.staking.activeEra.range([header.parentHash, header.hash], 'Dab4bfYTZRUDMWjYAUQuFbDreQ9mt7nULWu3Dw7jodbzVe9')) - .map(([block, value]) => [block, value.toRawType(), value.toHuman()]) - )); - - await api.disconnect(); - }); - - it('expose rpc and rx definition', async (): Promise => { - const api = await createApi(); - + it('expose rpc and rx definition', (): void => { console.error(api.rpc.payment.queryFeeDetails.meta); console.error(api.rx.rpc.chain.getBlock.meta); - - await api.disconnect(); }); }); diff --git a/packages/metadata/src/decorate/storage/createFunction.ts b/packages/metadata/src/decorate/storage/createFunction.ts index b17a784c59..41dce19d59 100644 --- a/packages/metadata/src/decorate/storage/createFunction.ts +++ b/packages/metadata/src/decorate/storage/createFunction.ts @@ -1,7 +1,7 @@ // Copyright 2017-2021 @polkadot/metadata authors & contributors // SPDX-License-Identifier: Apache-2.0 -import type { StorageEntryMetadataLatest } from '@polkadot/types/interfaces/metadata'; +import type { StorageEntryMetadataLatest, StorageHasher } from '@polkadot/types/interfaces/metadata'; import type { StorageEntry } from '@polkadot/types/primitive/types'; import type { Codec, Registry } from '@polkadot/types/types'; @@ -9,7 +9,7 @@ import BN from 'bn.js'; import { Raw } from '@polkadot/types/codec'; import { StorageKey, Type } from '@polkadot/types/primitive'; -import { assert, compactAddLength, compactStripLength, isNull, isUndefined, stringCamelCase, stringify, stringLowerFirst, u8aConcat, u8aToU8a } from '@polkadot/util'; +import { assert, compactAddLength, compactStripLength, isUndefined, stringCamelCase, stringLowerFirst, u8aConcat, u8aToU8a } from '@polkadot/util'; import { xxhashAsU8a } from '@polkadot/util-crypto'; import { getHasher } from './getHasher'; @@ -31,33 +31,27 @@ interface IterFn { meta: StorageEntryMetadataLatest; } -type CreateArgType = boolean | string | number | null | BN | BigInt | Uint8Array | Codec; - -type U8aHasher = (input: Uint8Array) => Uint8Array; - -function createPrefixedKey ({ method, prefix }: CreateItemFn): Uint8Array { - return u8aConcat(xxhashAsU8a(prefix, 128), xxhashAsU8a(method, 128)); -} - -function createKeyRaw (registry: Registry, itemFn: CreateItemFn, keys: Type[], hashers: U8aHasher[], args: CreateArgType[]): Uint8Array { - const { method, section } = itemFn; +type Arg = boolean | string | number | null | BN | BigInt | Uint8Array | Codec; +/** @internal */ +function createKeyRaw (registry: Registry, itemFn: CreateItemFn, keys: Type[], hashers: StorageHasher[], args: Arg[]): Uint8Array { return u8aConcat( - createPrefixedKey(itemFn), - ...keys.map((type, index): Uint8Array => { - const arg = args[index]; - - assert(!isUndefined(arg) && !isNull(arg), () => `Call to ${stringCamelCase(section || 'unknown')}.${stringCamelCase(method || 'unknown')} has a null or undefined argument at position ${index}`); - - return hashers[index](registry.createType(type.toString() as 'Raw', arg).toU8a()); - }) + xxhashAsU8a(itemFn.prefix, 128), + xxhashAsU8a(itemFn.method, 128), + ...keys.map((type, index) => + getHasher(hashers[index])( + registry.createType(type.toString() as 'Raw', args[index]).toU8a() + ) + ) ); } -function createKey (registry: Registry, itemFn: CreateItemFn, keys: Type[], hashers: U8aHasher[], args: CreateArgType[]): Uint8Array { +/** @internal */ +function createKey (registry: Registry, itemFn: CreateItemFn, keys: Type[], hashers: StorageHasher[], args: Arg[]): Uint8Array { const { method, section } = itemFn; - assert(Array.isArray(args) && args.length === keys.length, () => `Call to ${stringCamelCase(section || 'unknown')}.${stringCamelCase(method || 'unknown')} needs ${keys.length} arguments, found ${stringify(args)}`); + assert(Array.isArray(args), () => `Call to ${stringCamelCase(section || 'unknown')}.${stringCamelCase(method || 'unknown')} needs ${keys.length} arguments, provided in tuple format`); + assert(args.filter((a) => !isUndefined(a)).length === keys.length, () => `Call to ${stringCamelCase(section || 'unknown')}.${stringCamelCase(method || 'unknown')} needs ${keys.length} arguments, found [${args.join(', ')}]`); // as per createKey, always add the length prefix (underlying it is Bytes) return compactAddLength( @@ -65,9 +59,8 @@ function createKey (registry: Registry, itemFn: CreateItemFn, keys: Type[], hash ); } -// attach the metadata to expand to a StorageFunction /** @internal */ -function expandWithMeta ({ meta, method, prefix, section }: CreateItemFn, _storageFn: (arg?: CreateArgType | CreateArgType[]) => Uint8Array): StorageEntry { +function expandWithMeta ({ meta, method, prefix, section }: CreateItemFn, _storageFn: (arg?: Arg | Arg[]) => Uint8Array): StorageEntry { const storageFn = _storageFn as StorageEntry; storageFn.meta = meta; @@ -107,7 +100,6 @@ function extendHeadMeta (registry: Registry, { meta: { documentation, name, type registry.createType('StorageKey', iterFn(...args), { method, section }); } -// attach the full list hashing for prefixed maps /** @internal */ function extendPrefixedMap (registry: Registry, itemFn: CreateItemFn, storageFn: StorageEntry): StorageEntry { const { meta: { type }, method, section } = itemFn; @@ -124,16 +116,16 @@ function extendPrefixedMap (registry: Registry, itemFn: CreateItemFn, storageFn: if (args.length) { if (type.isDoubleMap) { - return new Raw(registry, createKeyRaw(registry, itemFn, [type.asDoubleMap.key1], [getHasher(type.asDoubleMap.hasher)], args as CreateArgType[])); + return new Raw(registry, createKeyRaw(registry, itemFn, [type.asDoubleMap.key1], [type.asDoubleMap.hasher], args as Arg[])); } else if (type.isNMap) { const keys = [...type.asNMap.keyVec]; - const hashers = type.asNMap.hashers.map((h) => getHasher(h)); + const hashers = [...type.asNMap.hashers]; // remove the last entry keys.pop(); hashers.pop(); - return new Raw(registry, createKeyRaw(registry, itemFn, keys, hashers, args as CreateArgType[])); + return new Raw(registry, createKeyRaw(registry, itemFn, keys, hashers, args as Arg[])); } } @@ -151,16 +143,16 @@ export function createFunction (registry: Registry, itemFn: CreateItemFn, option // - storage.system.account(address) // - storage.timestamp.blockPeriod() // For higher-map queries the params are passed in as an tuple, [key1, key2] - const storageFn = expandWithMeta(itemFn, (arg?: CreateArgType | CreateArgType[]) => + const storageFn = expandWithMeta(itemFn, (arg?: Arg | Arg[]) => type.isPlain ? options.skipHashing ? compactAddLength(u8aToU8a(options.key)) : createKey(registry, itemFn, [], [], []) : type.isMap - ? createKey(registry, itemFn, [type.asMap.key], [getHasher(type.asMap.hasher)], [arg as CreateArgType]) + ? createKey(registry, itemFn, [type.asMap.key], [type.asMap.hasher], [arg as Arg]) : type.isDoubleMap - ? createKey(registry, itemFn, [type.asDoubleMap.key1, type.asDoubleMap.key2], [getHasher(type.asDoubleMap.hasher), getHasher(type.asDoubleMap.key2Hasher)], arg as CreateArgType[]) - : createKey(registry, itemFn, type.asNMap.keyVec, type.asNMap.hashers.map((h) => getHasher(h)), arg as CreateArgType[]) + ? createKey(registry, itemFn, [type.asDoubleMap.key1, type.asDoubleMap.key2], [type.asDoubleMap.hasher, type.asDoubleMap.key2Hasher], arg as Arg[]) + : createKey(registry, itemFn, type.asNMap.keyVec, type.asNMap.hashers, arg as Arg[]) ); if (type.isMap || type.isDoubleMap || type.isNMap) { diff --git a/packages/metadata/src/decorate/storage/index.spec.ts b/packages/metadata/src/decorate/storage/index.spec.ts index 8e0189b806..2d8ce8a6a7 100644 --- a/packages/metadata/src/decorate/storage/index.spec.ts +++ b/packages/metadata/src/decorate/storage/index.spec.ts @@ -12,34 +12,36 @@ import { decorateStorage } from '..'; const keyring = createTestPairs({ type: 'ed25519' }); describe('decorateStorage', (): void => { - describe('latest', (): void => { - const registry = new TypeRegistry(); - const metadata = new Metadata(registry, rpcMetadata); + const registry = new TypeRegistry(); + const metadata = new Metadata(registry, rpcMetadata); - registry.setMetadata(metadata); + registry.setMetadata(metadata); - const query = decorateStorage(registry, metadata.asLatest, metadata.version); + const query = decorateStorage(registry, metadata.asLatest, metadata.version); - it('should throw if the storage function expects an argument', (): void => { - expect(() => query.balances.account()).toThrowError('Call to balances.account has a null or undefined argument at position 0'); - }); + it('should throw if the storage function expects an argument', (): void => { + expect(() => query.balances.account()).toThrowError('Call to balances.account needs 1 arguments, found []'); + }); - it('should throw if the storage function expects multiple arguments', (): void => { - expect(() => query.staking.erasStakers([1])).toThrowError('Call to staking.erasStakers needs 2 arguments, found [1]'); - }); + it('should throw if the storage function expects multiple arguments', (): void => { + expect(() => query.staking.erasStakers([1])).toThrowError('Call to staking.erasStakers needs 2 arguments, found [1]'); + }); - it('should return a value if the storage function does not expect an argument', (): void => { - expect(() => query.timestamp.now()).not.toThrow(); - }); + it('should throw if the storage function expects tuple arguments', (): void => { + expect(() => query.staking.erasStakers(1)).toThrowError('Call to staking.erasStakers needs 2 arguments, provided in tuple format'); + }); - it('should return the correct length-prefixed storage key', (): void => { - expect( - u8aToHex( - query.system.account(keyring.alice.address) - ) - ).toEqual( - '0x410126aa394eea5630e07c48ae0c9558cef7b99d880ec681799c0cf30e8886371da9de1e86a9a8c739864cf3cc5ec2bea59fd43593c715fdd31c61141abd04a99fd6822c8558854ccde39a5684e7a56da27d' - ); - }); + it('should return a value if the storage function does not expect an argument', (): void => { + expect(() => query.timestamp.now()).not.toThrow(); + }); + + it('should return the correct length-prefixed storage key', (): void => { + expect( + u8aToHex( + query.system.account(keyring.alice.address) + ) + ).toEqual( + '0x410126aa394eea5630e07c48ae0c9558cef7b99d880ec681799c0cf30e8886371da9de1e86a9a8c739864cf3cc5ec2bea59fd43593c715fdd31c61141abd04a99fd6822c8558854ccde39a5684e7a56da27d' + ); }); }); diff --git a/packages/types/src/primitive/StorageKey.ts b/packages/types/src/primitive/StorageKey.ts index 268b32a682..79ed77fd4b 100644 --- a/packages/types/src/primitive/StorageKey.ts +++ b/packages/types/src/primitive/StorageKey.ts @@ -32,30 +32,23 @@ const HASHER_MAP: Record = { Twox64Concat: [8, true] }; -function getStorageType (type: StorageEntryTypeLatest): [boolean, string] { - if (type.isMap) { - return [false, type.asMap.value.toString()]; - } else if (type.isDoubleMap) { - return [false, type.asDoubleMap.value.toString()]; - } else if (type.isNMap) { - return [false, type.asNMap.value.toString()]; - } - - return [false, type.asPlain.toString()]; -} - -// we unwrap the type here, turning into an output usable for createType /** @internal */ export function unwrapStorageType (type: StorageEntryTypeLatest, isOptional?: boolean): keyof InterfaceTypes { - const [hasWrapper, outputType] = getStorageType(type); + const outputType = type.isPlain + ? type.asPlain.toString() + : type.isMap + ? type.asMap.value.toString() + : type.isDoubleMap + ? type.asDoubleMap.value.toString() + : type.asNMap.value.toString(); - return isOptional && !hasWrapper + return isOptional ? `Option<${outputType}>` as keyof InterfaceTypes : outputType as keyof InterfaceTypes; } /** @internal */ -function decodeStorageKey (value?: AnyU8a | StorageKey | StorageEntry | [StorageEntry, any]): Decoded { +function decodeStorageKey (value?: AnyU8a | StorageKey | StorageEntry | [StorageEntry, unknown]): Decoded { // eslint-disable-next-line @typescript-eslint/no-use-before-define if (value instanceof StorageKey) { return { @@ -73,12 +66,12 @@ function decodeStorageKey (value?: AnyU8a | StorageKey | StorageEntry | [Storage section: value.section }; } else if (Array.isArray(value)) { - const [fn, ...arg] = value as [StorageEntry, ...any[]]; + const [fn, arg] = value; assert(isFunction(fn), 'Expected function input for key construction'); return { - key: fn(...arg), + key: fn(arg), method: fn.method, section: fn.section }; @@ -134,7 +127,7 @@ function decodeArgsFromMeta (registry: Registry, value: Uin } /** @internal */ -function getMeta (value: StorageKey | StorageEntry | [StorageEntry, any]): StorageEntryMetadataLatest | undefined { +function getMeta (value: StorageKey | StorageEntry | [StorageEntry, unknown]): StorageEntryMetadataLatest | undefined { if (value instanceof StorageKey) { return value.meta; } else if (isFunction(value)) { @@ -149,7 +142,7 @@ function getMeta (value: StorageKey | StorageEntry | [StorageEntry, any]): Stora } /** @internal */ -function getType (value: StorageKey | StorageEntry | [StorageEntry, any]): string { +function getType (value: StorageKey | StorageEntry | [StorageEntry, unknown]): string { if (value instanceof StorageKey) { return value.outputType; } else if (isFunction(value)) { @@ -185,7 +178,7 @@ export class StorageKey extends Bytes implements private _section?: string; - constructor (registry: Registry, value?: AnyU8a | StorageKey | StorageEntry | [StorageEntry, any], override: Partial = {}) { + constructor (registry: Registry, value?: AnyU8a | StorageKey | StorageEntry | [StorageEntry, unknown], override: Partial = {}) { const { key, method, section } = decodeStorageKey(value); super(registry, key);