diff --git a/packages/api-metadata/src/storage/fromMetadata/createFunction.spec.ts b/packages/api-metadata/src/storage/fromMetadata/createFunction.spec.ts index c37f2faaac..2bdbe26e6f 100644 --- a/packages/api-metadata/src/storage/fromMetadata/createFunction.spec.ts +++ b/packages/api-metadata/src/storage/fromMetadata/createFunction.spec.ts @@ -95,7 +95,7 @@ describe('createFunction', (): void => { it('needs two arguments', (): void => { expect( (): Uint8Array => storageFn(['5DXUeE5N5LtkW97F2PzqYPyqNkxqSWESdGSPTX6AvkUAhwKP']) - ).toThrow(/metaName expects two arguments/); + ).toThrow(/requires two arguments/); }); }); diff --git a/packages/api-metadata/src/storage/fromMetadata/createFunction.ts b/packages/api-metadata/src/storage/fromMetadata/createFunction.ts index 9f1705637b..d2bd256867 100644 --- a/packages/api-metadata/src/storage/fromMetadata/createFunction.ts +++ b/packages/api-metadata/src/storage/fromMetadata/createFunction.ts @@ -27,75 +27,75 @@ export interface CreateItemFn { type CreateArgType = boolean | string | number | null | BN | Uint8Array | Codec; -/** - * From the schema of a function in the module's storage, generate the function - * that will return the correct storage key. - * - * @param schema - The function's definition schema to create the function from. - * The schema is taken from state_getMetadata. - * @param options - Additional options when creating the function. These options - * are not known at runtime (from state_getMetadata), they need to be supplied - * by us manually at compile time. - */ -export default function createFunction ({ meta, method, prefix, section }: CreateItemFn, options: CreateItemOptions = {}): StorageEntry { +const NULL_HASHER = (value: Uint8Array): Uint8Array => value; + +// with the prefix, method & options, create both the string & raw keys +function createKeys ({ method, prefix }: CreateItemFn, options: CreateItemOptions): [string, Uint8Array] { const stringKey = options.key ? options.key : `${prefix} ${method}`; - const rawKey = stringToU8a(stringKey); - // Get the hashing function - let hasher: HasherFunction; - let key2Hasher: HasherFunction; + return [ + stringKey, + stringToU8a(stringKey) + ]; +} - if (meta.type.isDoubleMap) { - hasher = getHasher(meta.type.asDoubleMap.hasher); - key2Hasher = getHasher(meta.type.asDoubleMap.key2Hasher); - } else if (meta.type.isMap) { - hasher = getHasher(meta.type.asMap.hasher); - } else { - hasher = getHasher(); +// get the hashers, the base (and in the case of DoubleMap), the second key +function getHashers ({ meta: { type } }: CreateItemFn): [HasherFunction, HasherFunction?] { + if (type.isDoubleMap) { + return [ + getHasher(type.asDoubleMap.hasher), + getHasher(type.asDoubleMap.key2Hasher) + ]; + } else if (type.isMap) { + return [getHasher(type.asMap.hasher)]; } - // Can only have zero or one argument: - // - storage.balances.freeBalance(address) - // - storage.timestamp.blockPeriod() - // For doublemap queries the params is passed in as an tuple, [key1, key2] - const _storageFn = (arg?: CreateArgType | [CreateArgType?, CreateArgType?]): Uint8Array => { - let key = rawKey; + // the default + return [getHasher()]; +} - if (meta.type.isDoubleMap) { - assert(Array.isArray(arg) && !isUndefined(arg[0]) && !isNull(arg[0]) && !isUndefined(arg[1]) && !isNull(arg[1]), `${meta.name} expects two arguments`); +// create a key for a DoubleMap type +function createKeyDoubleMap ({ meta: { name, type } }: CreateItemFn, rawKey: Uint8Array, args: [CreateArgType, CreateArgType], [hasher, key2Hasher]: [HasherFunction, HasherFunction?]): Uint8Array { + // since we are passing an almost-unknown through, trust, but verify + assert( + Array.isArray(args) && !isUndefined(args[0]) && !isNull(args[0]) && !isUndefined(args[1]) && !isNull(args[1]), + `${name} is a DoubleMap and requires two arguments` + ); - // we have checked that it is an array in the assert, so all ok - const [key1, key2] = arg as [CreateArgType, CreateArgType]; - const type1 = meta.type.asDoubleMap.key1.toString(); - const type2 = meta.type.asDoubleMap.key2.toString(); - const param1Encoded = u8aConcat(key, createTypeUnsafe(type1, [key1]).toU8a(true)); - const param1Hashed = hasher(param1Encoded); - const param2Hashed = key2Hasher(createTypeUnsafe(type2, [key2]).toU8a(true)); + const [key1, key2] = args; + const type1 = type.asDoubleMap.key1.toString(); + const type2 = type.asDoubleMap.key2.toString(); + const param1Encoded = u8aConcat(rawKey, createTypeUnsafe(type1, [key1]).toU8a(true)); + const param1Hashed = hasher(param1Encoded); - return Compact.addLengthPrefix(u8aConcat(param1Hashed, param2Hashed)); - } + // @ts-ignore If this fails it means the getHashers function failed - and we have much bigger issues + const param2Hashed = key2Hasher(createTypeUnsafe(type2, [key2]).toU8a(true)); - if (meta.type.isMap) { - assert(!isUndefined(arg) && !isNull(arg), `${meta.name} expects one argument`); + // as per createKey, always add the length prefix (underlying it is Bytes) + return Compact.addLengthPrefix(u8aConcat(param1Hashed, param2Hashed)); +} - const type = meta.type.asMap.key.toString(); - const param = createTypeUnsafe(type, [arg]).toU8a(); +// create a key for either a map or a plain value +function createKey ({ meta: { name, type } }: CreateItemFn, rawKey: Uint8Array, arg: CreateArgType, hasher: (value: Uint8Array) => Uint8Array): Uint8Array { + let key = rawKey; - key = u8aConcat(key, param); - } + if (type.isMap) { + assert(!isUndefined(arg) && !isNull(arg), `${name} is a Map and requires one argument`); - // StorageKey is a Bytes, so is length-prefixed - return Compact.addLengthPrefix( - options.skipHashing - ? key - : hasher(key) - ); - }; + const mapType = type.asMap.key.toString(); + const param = createTypeUnsafe(mapType, [arg]).toU8a(); - const storageFn = _storageFn as StorageEntry; + key = u8aConcat(key, param); + } + // StorageKey is a Bytes, so is length-prefixed + return Compact.addLengthPrefix(hasher(key)); +} + +// attach the metadata to expsnd to a StorageFunction +function expandWithMeta ({ meta, method, prefix, section }: CreateItemFn, storageFn: StorageEntry): StorageEntry { storageFn.meta = meta; storageFn.method = stringLowerFirst(method); storageFn.prefix = prefix; @@ -103,28 +103,68 @@ export default function createFunction ({ meta, method, prefix, section }: Creat // explicitly add the actual method in the toJSON, this gets used to determine caching and without it // instances (e.g. collective) will not work since it is only matched on param meta - storageFn.toJSON = (): any => ({ ...(meta.toJSON() as any), storage: { method, prefix, section } }); + storageFn.toJSON = (): any => ({ + ...(meta.toJSON() as any), + storage: { method, prefix, section } + }); - if (meta.type.isMap && meta.type.asMap.linked.isTrue) { - const headHash = new U8a(hasher(`head of ${stringKey}`)); - const headFn: any = (): U8a => headHash; + return storageFn; +} - // metadata with a fallback value using the type of the key, the normal - // meta fallback only applies to actual entry values, create one for head - headFn.meta = new StorageEntryMetadata({ - name: meta.name, - modifier: createType('StorageEntryModifierV7', 1), // required - type: new StorageEntryType(createType('PlainTypeV7', meta.type.asMap.key), 0), - fallback: new Bytes(createTypeUnsafe(meta.type.asMap.key.toString()).toHex()), - documentation: meta.documentation - }); +// attch the head key hashing for linked maps +function extendLinkedMap ({ meta: { documentation, name, type } }: CreateItemFn, storageFn: StorageEntry, stringKey: string, hasher: HasherFunction): StorageEntry { + const headHash = new U8a(hasher(`head of ${stringKey}`)); + const headFn: any = (): U8a => + headHash; - // here we pass the section/method through as well - these are not on - // the function itself, so specify these explicitly to the constructor - storageFn.headKey = new StorageKey(headFn, { - method: storageFn.method, - section: `head of ${storageFn.section}` - }); + // metadata with a fallback value using the type of the key, the normal + // meta fallback only applies to actual entry values, create one for head + headFn.meta = new StorageEntryMetadata({ + name, + modifier: createType('StorageEntryModifierV7', 1), // required + type: new StorageEntryType(createType('PlainTypeV7', type.asMap.key), 0), + fallback: new Bytes(createTypeUnsafe(type.asMap.key.toString()).toHex()), + documentation + }); + + // here we pass the section/method through as well - these are not on + // the function itself, so specify these explicitly to the constructor + storageFn.headKey = new StorageKey(headFn, { + method: storageFn.method, + section: `head of ${storageFn.section}` + }); + + return storageFn; +} + +/** + * From the schema of a function in the module's storage, generate the function + * that will return the correct storage key. + * + * @param item - The function's definition schema to create the function from. + * The schema is taken from state_getMetadata. + * @param options - Additional options when creating the function. These options + * are not known at runtime (from state_getMetadata), they need to be supplied + * by us manually at compile time. + */ +export default function createFunction (item: CreateItemFn, options: CreateItemOptions = {}): StorageEntry { + const { meta: { type } } = item; + const [stringKey, rawKey] = createKeys(item, options); + const [hasher, key2Hasher] = getHashers(item); + + // Can only have zero or one argument: + // - storage.balances.freeBalance(address) + // - storage.timestamp.blockPeriod() + // For doublemap queries the params is passed in as an tuple, [key1, key2] + const _storageFn = (arg?: CreateArgType | [CreateArgType?, CreateArgType?]): Uint8Array => + type.isDoubleMap + ? createKeyDoubleMap(item, rawKey, arg as [CreateArgType, CreateArgType], [hasher, key2Hasher]) + : createKey(item, rawKey, arg as CreateArgType, options.skipHashing ? NULL_HASHER : hasher); + + const storageFn = expandWithMeta(item, _storageFn as StorageEntry); + + if (type.isMap && type.asMap.linked.isTrue) { + extendLinkedMap(item, storageFn, stringKey, hasher); } return storageFn; diff --git a/packages/api-metadata/src/storage/fromMetadata/fromMetadata.spec.ts b/packages/api-metadata/src/storage/fromMetadata/fromMetadata.spec.ts index 24c3e03e24..2f1f6ef018 100644 --- a/packages/api-metadata/src/storage/fromMetadata/fromMetadata.spec.ts +++ b/packages/api-metadata/src/storage/fromMetadata/fromMetadata.spec.ts @@ -19,7 +19,7 @@ const storage = fromMetadata(metadata); describe('fromMetadata', (): void => { it('should throw if the storage function expects an argument', (): void => { - expect((): any => storage.balances.freeBalance()).toThrowError(/expects one argument/); + expect((): any => storage.balances.freeBalance()).toThrowError(/requires one argument/); }); it('should return a value if the storage function does not expect an argument', (): void => { diff --git a/packages/api-metadata/src/storage/fromMetadata/getHasher.ts b/packages/api-metadata/src/storage/fromMetadata/getHasher.ts index 57e6c6dee5..10158d2216 100644 --- a/packages/api-metadata/src/storage/fromMetadata/getHasher.ts +++ b/packages/api-metadata/src/storage/fromMetadata/getHasher.ts @@ -6,11 +6,12 @@ import { StorageHasher } from '@polkadot/types/primitive'; import { u8aConcat, u8aToU8a } from '@polkadot/util'; import { blake2AsU8a, xxhashAsU8a } from '@polkadot/util-crypto'; -type HasherInput = string | Buffer | Uint8Array; -type HasherCheck = 'isBlake2128' | 'isBlake2256' | 'isTwox128' | 'isTwox256' | 'isTwox64Concat'; +export type HasherInput = string | Buffer | Uint8Array; export type HasherFunction = (data: HasherInput) => Uint8Array; +type HasherCheck = 'isBlake2128' | 'isBlake2256' | 'isTwox128' | 'isTwox256' | 'isTwox64Concat'; + const DEFAULT = (data: HasherInput): Uint8Array => xxhashAsU8a(data, 128); const map: Record = {