From f9a3f3e53fa69c049143fb8e6ce443dddaaa671c Mon Sep 17 00:00:00 2001 From: Jaco Greeff Date: Tue, 23 Jul 2019 10:56:45 +0200 Subject: [PATCH] Fix linked maps with defaults (#1162) * isPedantic matches [] with isEmpty * Fix undefined.undefined on headKey * Adjust fallback for linked maps * Add tests for the linked map fallbacks (head and value) --- .../storage/fromMetadata/createFunction.ts | 41 +++++++---- packages/rpc-core/src/formatting.spec.ts | 69 ++++++++++++++++--- packages/rpc-core/src/index.ts | 18 +++-- packages/types/src/primitive/StorageKey.ts | 11 ++- 4 files changed, 105 insertions(+), 34 deletions(-) diff --git a/packages/api-metadata/src/storage/fromMetadata/createFunction.ts b/packages/api-metadata/src/storage/fromMetadata/createFunction.ts index bc8d5d6e46..f80c35cd82 100644 --- a/packages/api-metadata/src/storage/fromMetadata/createFunction.ts +++ b/packages/api-metadata/src/storage/fromMetadata/createFunction.ts @@ -60,7 +60,6 @@ export default function createFunction ({ meta, method, prefix, section }: Creat assert(!isUndefined(arg) && !isNull(arg) && !isUndefined(arg[0]) && !isNull(arg[0]) && !isUndefined(arg[1]) && !isNull(arg[1]), `${meta.name} expects two arguments`); const type1 = meta.type.asDoubleMap.key1.toString(); const type2 = meta.type.asDoubleMap.key2.toString(); - const param1Encoded = u8aConcat(key, createType(type1, arg[0]).toU8a(true)); const param1Hashed = hasher(param1Encoded); const param2Hashed = key2Hasher(createType(type2, arg[1]).toU8a(true)); @@ -87,24 +86,38 @@ export default function createFunction ({ meta, method, prefix, section }: Creat const storageFn = _storageFn as StorageEntry; - if (meta.type.isMap && meta.type.asMap.isLinked) { - const keyHash = new U8a(hasher(`head of ${stringKey}`)); - const keyFn: any = (): U8a => keyHash; - keyFn.meta = new StorageEntryMetadata({ - name: meta.name, - modifier: new StorageEntryModifier('Required'), - type: new StorageEntryType(new PlainType(meta.type.asMap.key), 0), - fallback: new Bytes(), - documentation: meta.documentation - }); - storageFn.headKey = new StorageKey(keyFn); - } - storageFn.meta = meta; storageFn.method = stringLowerFirst(method); storageFn.prefix = prefix; storageFn.section = section; storageFn.toJSON = (): any => meta.toJSON(); + if (meta.type.isMap && meta.type.asMap.isLinked) { + const keyHash = new U8a(hasher(`head of ${stringKey}`)); + const keyFn: any = (): U8a => keyHash; + + // metadata with a flabbcak value using the type of the key, the normal + // meta fallback only applies to actual entry values, create one for head + keyFn.meta = new StorageEntryMetadata({ + name: meta.name, + modifier: new StorageEntryModifier('Required'), + type: new StorageEntryType(new PlainType(meta.type.asMap.key), 0), + fallback: new Bytes(createType(meta.type.asMap.key).toHex()), + documentation: meta.documentation + }); + + // here we pass the section/methos through as well - these are not on + // the function itself, so specify these explicitly to the constructor + storageFn.headKey = new StorageKey(keyFn, { + method: storageFn.method, + section: `head of ${storageFn.section}` + }); + + // adjust the fallback value - the metadata only specifies the value + // part, add a Linkage to the fallback aswell. The additional + // bytes here is a represnettaion of teh Options for next/prev + meta.set('fallback', new Bytes(meta.fallback.toHex().concat('0000'))); + } + return storageFn; } diff --git a/packages/rpc-core/src/formatting.spec.ts b/packages/rpc-core/src/formatting.spec.ts index 9b2f7aefbc..424dc981a1 100644 --- a/packages/rpc-core/src/formatting.spec.ts +++ b/packages/rpc-core/src/formatting.spec.ts @@ -2,10 +2,11 @@ // This software may be modified and distributed under the terms // of the Apache-2.0 license. See the LICENSE file for details. -import BN from 'bn.js'; +import { Codec } from '@polkadot/types/types'; import fromMetadata from '@polkadot/api-metadata/storage/fromMetadata'; import { Storage } from '@polkadot/api-metadata/storage/types'; +import { Balance } from '@polkadot/types'; import Metadata from '@polkadot/types/Metadata'; import { injectDefinitions } from '@polkadot/types/Metadata/util/testUtil'; import rpcMetadataV3 from '@polkadot/types/Metadata/v3/static'; @@ -42,21 +43,32 @@ function formattingTests (version: string, storage: Storage, encodedValues: [str ) ), subscribe: jest.fn((type, method, params, cb): void => { - // this emulates https://github.com/polkadot-js/api/issues/1051 - params[0][0] === CONTRACT_KEY - ? cb(null, { + if (params[0][0] === CONTRACT_KEY) { + // this emulates https://github.com/polkadot-js/api/issues/1051 + cb(null, { block: '0x2345', changes: [ [CONTRACT_KEY, OPTION_BYTES_HEX] ] - }) - : cb(null, { + }); + } else if (params[0][0] === ENC_ONE || params[0][0] === ENC_TWO) { + // known values for the balanaces + cb(null, { block: '0x1234', changes: [ [ENC_ONE, '0x01020000000000000000000000000000'], [ENC_TWO, '0x02010000000000000000000000000000'] ] }); + } else { + // return empty values for all else + cb(null, { + block: '0x3456', + changes: [ + [params[0][0], null] + ] + }); + } }) }; @@ -93,8 +105,8 @@ function formattingTests (version: string, storage: Storage, encodedValues: [str [storage.balances.freeBalance, ADDR_ONE], [storage.balances.freeBalance, ADDR_TWO] ] - ).subscribe((value: any): void => { - console.error(value); + ).subscribe((value: Balance[]): void => { + // console.error(value); expect( provider.subscribe @@ -105,7 +117,7 @@ function formattingTests (version: string, storage: Storage, encodedValues: [str expect.anything() ); expect( - value.map((balance: BN): number => + value.map((balance): number => balance.toNumber() ) ).toEqual([0x0201, 0x0102]); @@ -122,13 +134,50 @@ function formattingTests (version: string, storage: Storage, encodedValues: [str api.state .subscribeStorage([[call, '0x00']]) // eslint-disable-next-line @typescript-eslint/no-unused-vars - .subscribe((value: any): void => { + .subscribe((value: Codec[]): void => { + expect(value).toHaveLength(1); // console.error(value); // expect(value.toHex()).toBe(OPTION_BYTES_HEX); done(); }); }); + + it('handles fallbacks for linked heads (new metadata only)', (done): void => { + const headKey = storage.staking.validators && storage.staking.validators.headKey; + + // skip for old + if (!headKey) { + return done(); + } + + api.state + .subscribeStorage([headKey]) + .subscribe((value: Codec[]): void => { + expect(value).toHaveLength(1); + // console.error('head fallback', value); + + done(); + }); + }); + + it('handles fallbacks for linked maps (new metadata only)', (done): void => { + const headKey = storage.staking.validators && storage.staking.validators.headKey; + + // skip for old + if (!headKey) { + return done(); + } + + api.state + .subscribeStorage([[storage.staking.validators, '0x00']]) + .subscribe((value: Codec): void => { + expect(value).toBeDefined(); + // console.error('linked falklback', value); + + done(); + }); + }); }); } diff --git a/packages/rpc-core/src/index.ts b/packages/rpc-core/src/index.ts index 4063a0bd70..b8fbe3b145 100644 --- a/packages/rpc-core/src/index.ts +++ b/packages/rpc-core/src/index.ts @@ -173,7 +173,8 @@ export default class Rpc implements RpcInterface { const call = (...values: any[]): Observable => { return new Observable((observer: Observer): VoidCallback => { - let subscriptionPromise: Promise; + // Have at least an empty promise, as used in the unsubscribe + let subscriptionPromise: Promise = Promise.resolve(); const errorHandler = (error: Error): void => { const message = this.createErrorMessage(method, error); @@ -195,9 +196,12 @@ export default class Rpc implements RpcInterface { observer.next(this.formatOutput(method, params, result)); }; - subscriptionPromise = this.provider - .subscribe(subType, subName, paramsJson, update) - .catch(errorHandler); + // FIXME This is a work-around, provider.subscribe _should_ always return, + // however in some cases `this.provider.subscribe.catch` yields ".catch of + // undefined", so here we flatten via Promise.reolve (which doe sfollow) + subscriptionPromise = Promise.resolve( + this.provider.subscribe(subType, subName, paramsJson, update) + ).catch(errorHandler); } catch (error) { errorHandler(error); } @@ -217,7 +221,7 @@ export default class Rpc implements RpcInterface { memoized.delete(...values); // Unsubscribe from provider - subscriptionPromise && subscriptionPromise + subscriptionPromise .then((subscriptionId): Promise => isNumber(subscriptionId) ? this.provider.unsubscribe(subType, unsubName, subscriptionId) @@ -302,7 +306,7 @@ export default class Rpc implements RpcInterface { const meta = key.meta || EMPTY_META; if (meta.type.isMap && meta.type.asMap.isLinked) { - return createType(type, base, true); + return createType(type, isNull ? meta.fallback : base, true); } else if (meta.modifier.isOptional) { return new Option( createClass(type), @@ -331,7 +335,7 @@ export default class Rpc implements RpcInterface { this._storageCache.set(hexKey, value); if (meta.type.isMap && meta.type.asMap.isLinked) { - return createType(type, value.unwrapOr(null), true); + return createType(type, value.unwrapOr(meta.fallback), true); } else if (meta.modifier.isOptional) { return new Option( createClass(type), diff --git a/packages/types/src/primitive/StorageKey.ts b/packages/types/src/primitive/StorageKey.ts index 993436bbbd..5357a4d5e5 100644 --- a/packages/types/src/primitive/StorageKey.ts +++ b/packages/types/src/primitive/StorageKey.ts @@ -24,6 +24,11 @@ interface Decoded { section?: string; } +interface StorageKeyExtra { + method: string; + section: string; +} + /** * @name StorageKey * @description @@ -39,15 +44,15 @@ export default class StorageKey extends Bytes { private _section?: string; - public constructor (value?: AnyU8a | StorageKey | StorageEntry | [StorageEntry, any]) { + public constructor (value?: AnyU8a | StorageKey | StorageEntry | [StorageEntry, any], override: Partial = {}) { const { key, method, section } = StorageKey.decodeStorageKey(value); super(key); this._meta = StorageKey.getMeta(value as StorageKey); - this._method = method; + this._method = override.method || method; this._outputType = StorageKey.getType(value as StorageKey); - this._section = section; + this._section = override.section || section; } public static decodeStorageKey (value?: AnyU8a | StorageKey | StorageEntry | [StorageEntry, any]): Decoded {