From a7f16064a9009408ce0fe29ecfb32e5bb2d11703 Mon Sep 17 00:00:00 2001 From: Alex Wang Date: Thu, 30 May 2019 02:14:42 +1200 Subject: [PATCH] update signature of doublemap storage function, add e2e test cases (#957) * update signature of doublemap storage function * add test cases * update doublemap implementation * update implementation * remove duplicate test case * update test cases --- packages/api/src/Base.ts | 44 ++-- packages/api/src/types.ts | 13 +- packages/api/test/e2e/promise-queries.spec.ts | 201 ++++++++++++++++-- .../src/fromMetadata/createFunction.spec.ts | 2 +- .../src/fromMetadata/createFunction.ts | 2 +- packages/types/src/codec/createType.spec.ts | 14 ++ packages/types/src/primitive/StorageKey.ts | 9 - 7 files changed, 241 insertions(+), 44 deletions(-) diff --git a/packages/api/src/Base.ts b/packages/api/src/Base.ts index 9b39dd2635..bf5603985c 100644 --- a/packages/api/src/Base.ts +++ b/packages/api/src/Base.ts @@ -573,8 +573,11 @@ export default abstract class ApiBase implement } return onCall( - (arg: CodecArg) => this._rpcRx.state - .subscribeStorage([[creator, arg]]) + (...args: CodecArg[]) => this._rpcRx.state + .subscribeStorage([ + creator.meta.type.isDoubleMap + ? [creator, args] + : [creator, ...args]]) .pipe( // state_storage returns an array of values, since we have just subscribed to // a single entry, we pull that from the array and return it as-is @@ -589,37 +592,48 @@ export default abstract class ApiBase implement decorated.creator = creator; - decorated.at = (hash: Hash | Uint8Array | string, arg?: CodecArg): C => + decorated.at = (hash: Hash | Uint8Array | string, arg1?: CodecArg, arg2?: CodecArg): C => onCall( - (arg: CodecArg) => this._rpcRx.state.getStorage([creator, arg], hash), - [arg] + (arg1?: CodecArg, arg2?: CodecArg) => this._rpcRx.state.getStorage( + creator.meta.type.isDoubleMap + ? [creator, [arg1, arg2]] + : [creator, arg1], + hash), + [arg1, arg2] ) as C; // FIXME The unknown cast is needed since the onCall result, `C | S` cannot // be converted from C to the actual result required - decorated.hash = (arg?: CodecArg): HashResult => + decorated.hash = (arg1?: CodecArg, arg2?: CodecArg): HashResult => onCall( - (arg: CodecArg) => this._rpcRx.state.getStorageHash([creator, arg]), - [arg] + (arg1?: CodecArg, arg2?: CodecArg) => this._rpcRx.state.getStorageHash( + creator.meta.type.isDoubleMap + ? [creator, [arg1, arg2]] + : [creator, arg1]), + [arg1, arg2] ) as unknown as HashResult; - decorated.key = (arg?: CodecArg): string => - u8aToHex(compactStripLength(creator(arg))[1]); + decorated.key = (arg1?: CodecArg, arg2?: CodecArg): string => + u8aToHex(compactStripLength(creator(creator.meta.type.isDoubleMap ? [arg1, arg2] : arg1))[1]); - decorated.multi = (args: Array, callback?: CodecCallback): S => + // When using double map storage function, user need to path double map key as an array + decorated.multi = (args: Array, callback?: CodecCallback): S => onCall( () => this._rpcRx.state - .subscribeStorage(args.map((arg) => [creator, arg])) + .subscribeStorage(args.map((arg: CodecArg[] | CodecArg) => [creator, arg])) .pipe(map((results) => new VectorAny(...results))), [], callback ) as unknown as S; - decorated.size = (arg?: CodecArg): U64Result => + decorated.size = (arg1?: CodecArg, arg2?: CodecArg): U64Result => onCall( - (arg: CodecArg) => this._rpcRx.state.getStorageSize([creator, arg]), - [arg] + (arg1?: CodecArg, arg2?: CodecArg) => this._rpcRx.state.getStorageSize( + creator.meta.type.isDoubleMap + ? [creator, [arg1, arg2]] + : [creator, arg1]), + [arg1, arg2] ) as unknown as U64Result; return this.decorateFunctionMeta(creator, decorated) as QueryableStorageFunction; diff --git a/packages/api/src/types.ts b/packages/api/src/types.ts index 526ac3c355..a5fa2f7a44 100644 --- a/packages/api/src/types.ts +++ b/packages/api/src/types.ts @@ -48,18 +48,19 @@ export type U64Result = : Promise; export interface QueryableStorageFunctionBase extends StorageFunction { - (arg?: CodecArg): CodecResult; - at: (hash: Hash | Uint8Array | string, arg?: CodecArg) => CodecResult; + (arg1?: CodecArg, arg2?: CodecArg): CodecResult; + at: (hash: Hash | Uint8Array | string, arg1?: CodecArg, arg2?: CodecArg) => CodecResult; creator: StorageFunction; - hash: (arg?: CodecArg) => HashResult; - key: (arg?: CodecArg) => string; - multi: (args: Array, callback?: CodecCallback) => SubscriptionResult; - size: (arg?: CodecArg) => U64Result; + hash: (arg1?: CodecArg, arg2?: CodecArg) => HashResult; + key: (arg1?: CodecArg, arg2?: CodecArg) => string; + multi: (args: Array, callback?: CodecCallback) => SubscriptionResult; + size: (arg1?: CodecArg, arg2?: CodecArg) => U64Result; } interface QueryableStorageFunctionPromise extends QueryableStorageFunctionBase { (callback: CodecCallback): SubscriptionResult; (arg: CodecArg, callback: CodecCallback): SubscriptionResult; + (arg1: CodecArg, arg2: CodecArg, callback: CodecCallback): SubscriptionResult; } export type QueryableStorageFunction = diff --git a/packages/api/test/e2e/promise-queries.spec.ts b/packages/api/test/e2e/promise-queries.spec.ts index e582ef691b..e3679abc21 100644 --- a/packages/api/test/e2e/promise-queries.spec.ts +++ b/packages/api/test/e2e/promise-queries.spec.ts @@ -7,7 +7,7 @@ import BN from 'bn.js'; import WsProvider from '@polkadot/rpc-provider/ws'; import testingPairs from '@polkadot/keyring/testingPairs'; import { LinkageResult } from '@polkadot/types/codec/Linkage'; -import { EventRecord, Header, Vector } from '@polkadot/types'; +import { Balance, EventRecord, Header, Option, Vector } from '@polkadot/types'; import Api from './../../src/promise'; @@ -177,20 +177,197 @@ describe.skip('Promise e2e queries', () => { ); }); - it('queries state using double map key', async () => { - // TODO Update ['any', '0x1234'] to the key of a known event topic and update '[]' to the expected value - const eventTopics = await api.query.system.eventTopics(['any', '0x1234']); + describe('with plain type', () => { + const EXISTENTIAL_DEPOSIT = 500; + it('queries correct value', async () => { + const existentialDeposit = await api.query.balances.existentialDeposit() as Balance; - expect(eventTopics.toString()).toEqual('[]'); + expect(existentialDeposit.toNumber()).toEqual(EXISTENTIAL_DEPOSIT); + }); + + it('queries correct value at a specified block', async () => { + const header = await api.rpc.chain.getHeader() as Header; + const existentialDepositAt = await api.query.balances.existentialDeposit.at(header.hash) as Balance; + + expect(existentialDepositAt.toNumber()).toEqual(EXISTENTIAL_DEPOSIT); + }); + + it('subscribes to query and get correct result', (done) => { + return api.query.balances.existentialDeposit((existentialDeposit) => { + expect(existentialDeposit.toNumber()).toEqual(EXISTENTIAL_DEPOSIT); + done(); + }); + }); + + it('queries correct hash', async () => { + const hash = await api.query.balances.existentialDeposit.hash(); + + expect(hash).toBeDefined(); + }); + + it('gets correct key', async () => { + const key = api.query.balances.existentialDeposit.key(); + const existentialDepositData = await api.rpc.state.getStorage(key) as Option; + const existentialDepositRPC = new Balance(existentialDepositData.unwrapOr(undefined)); + + expect(existentialDepositRPC.toNumber()).toEqual(EXISTENTIAL_DEPOSIT); + }); + + it('queries correct size', async () => { + const size = await api.query.balances.existentialDeposit.size(); + + expect(size.toNumber()).not.toEqual(0); + }); }); - it('subscribes to queries using double map key', async (done) => { - return ( - // TODO Update ['any', '0x1234'] to the key of a known event topic and update '[]' to the expected value - api.query.system.eventTopics(['any', '0x1234'], (eventTopics) => { - expect(eventTopics.toString()).toEqual('[]'); + describe('with map type', () => { + it('queries correct value', async () => { + const balance = await api.query.balances.freeBalance(keyring.alice.address()) as Balance; + + expect(balance.isZero()).toBe(false); + }); + + it('queries correct value at a specified block', async () => { + // assume the account Alice is only used in test(the balance of Alice does not change in this test case) + const balance = await api.query.balances.freeBalance(keyring.alice.address()); + const header = await api.rpc.chain.getHeader() as Header; + const balanceAt = await api.query.balances.freeBalance.at(header.hash, keyring.alice.address()) as Balance; + + expect(balanceAt.isZero()).toBe(false); + expect(balanceAt.toString()).toEqual(balance.toString()); + }); + + it('subscribes to query and get correct result', async (done) => { + // assume the account Alice is only used in test(the balance of Alice does not change in this test case) + const balance = await api.query.balances.freeBalance(keyring.alice.address()); + + return api.query.balances.freeBalance(keyring.alice.address(), (balanceSubscribed) => { + expect(balanceSubscribed.isZero()).toBe(false); + expect(balanceSubscribed.toString()).toEqual(balance.toString()); done(); - }) - ); + }); + }); + + it('queries correct hash', async () => { + const hash = await api.query.balances.freeBalance.hash(keyring.alice.address()); + + expect(hash).toBeDefined(); + }); + + it('gets correct key', async () => { + // assume the account Alice is only used in test(the balance of Alice does not change in this test case) + const key = api.query.balances.freeBalance.key(keyring.alice.address()); + const balanceData = await api.rpc.state.getStorage(key) as Option; + const balanceRPC = new Balance(balanceData.unwrapOr(undefined)); + + const balance = await api.query.balances.freeBalance(keyring.alice.address()); + + expect(balanceRPC.isZero()).toBe(false); + expect(balanceRPC.toString()).toEqual(balance.toString()); + }); + + it('queries multiple results', async () => { + // assume the account Alice and Bob are only used in test(the balance of them do not change in this test case) + const balanceAlice = await api.query.balances.freeBalance(keyring.alice.address()); + const balanceBob = await api.query.balances.freeBalance(keyring.bob.address()); + + const balances = await api.query.balances.freeBalance.multi([ + keyring.alice.address(), + keyring.bob.address() + ]); + + expect(balances).toHaveLength(2); + expect((balances as any)[0].toString()).toEqual(balanceAlice.toString()); + expect((balances as any)[1].toString()).toEqual(balanceBob.toString()); + }); + + it('subscribes to multiple queries and get correct results', async (done) => { + // assume the account Alice and Bob are only used in test(the balance of them do not change in this test case) + const balanceAlice = await api.query.balances.freeBalance(keyring.alice.address()); + const balanceBob = await api.query.balances.freeBalance(keyring.bob.address()); + + return api.query.balances.freeBalance.multi([ + keyring.alice.address(), + keyring.bob.address() + ], (balances) => { + expect(balances).toHaveLength(2); + expect(balances[0].toString()).toEqual(balanceAlice.toString()); + expect(balances[1].toString()).toEqual(balanceBob.toString()); + done(); + }); + }); + + it('queries correct size', async () => { + const size = await api.query.balances.freeBalance.size(keyring.alice.address()); + + expect(size.toNumber()).not.toEqual(0); + }); + }); + + // TODO Update ['any', '0x1234'] to the key of a known event topic and update EXPECTED_VALUE to the expected value + describe('with double map type', () => { + const KEY1 = 'any'; + const KEY2 = '0x1234'; + it('queries correct value', async () => { + const eventTopics = await api.query.system.eventTopics(KEY1, KEY2); + + expect(eventTopics.toJSON()).toEqual([]); + }); + + it('queries correct value at a specified block', async () => { + const header = await api.rpc.chain.getHeader() as Header; + + // TODO check: this will throw the error: Encoding for input doesn't match output, created 0x00 from 0x + const eventTopicsAt = await api.query.system.eventTopics.at(header.hash, KEY1, KEY2); + expect(eventTopicsAt).toEqual(undefined); + + // const eventTopicsAt = await api.query.system.eventTopics.at(header.hash, KEY1, KEY2); + // expect(eventTopicsAt.toJSON()).toEqual([]); + }); + + it('subscribes to query and get correct result', async (done) => { + return api.query.system.eventTopics(KEY1, KEY2, (eventTopicsAt) => { + expect(eventTopicsAt.toJSON()).toEqual([]); + done(); + }); + }); + + it('queries correct hash', async () => { + const hash = await api.query.system.eventTopics(KEY1, KEY2); + + expect(hash).toBeDefined(); + }); + + it('gets correct key', async () => { + const key = api.query.system.eventTopics.key(KEY1, KEY2); + const eventTopicsData = await api.rpc.state.getStorage(key) as Option; + + expect(eventTopicsData.unwrapOr(undefined)).toEqual(undefined); + }); + + it('queries multiple results', async () => { + const eventTopicsList = await api.query.system.eventTopics.multi([ + [KEY1, KEY2] + ]); + + expect(eventTopicsList).toHaveLength(1); + expect((eventTopicsList as any)[0].toJSON()).toEqual([]); + }); + + it('subscribes to multiple queries and get correct results', async (done) => { + return api.query.system.eventTopics.multi([ + [KEY1, KEY2] + ], (eventTopicsList) => { + expect(eventTopicsList).toHaveLength(1); + expect(eventTopicsList[0].toJSON()).toEqual([]); + done(); + }); + }); + + it('queries correct size', async () => { + const size = await api.query.system.eventTopics.size(KEY1, KEY2); + + expect(size.toNumber()).toEqual(0); + }); }); }); diff --git a/packages/type-storage/src/fromMetadata/createFunction.spec.ts b/packages/type-storage/src/fromMetadata/createFunction.spec.ts index b4fff1cacf..f4cedfd154 100644 --- a/packages/type-storage/src/fromMetadata/createFunction.spec.ts +++ b/packages/type-storage/src/fromMetadata/createFunction.spec.ts @@ -79,7 +79,7 @@ describe('createFunction', () => { it('should return correct key', () => { const result = storageFn(['5DXUeE5N5LtkW97F2PzqYPyqNkxqSWESdGSPTX6AvkUAhwKP', '5DXUeE5N5LtkW97F2PzqYPyqNkxqSWESdGSPTX6AvkUAhwKP']); - expect(u8aToHex(result)).toEqual('0x00fa40e72d7173e69ee54b980345ea01cb81e64258502e0247af4303dee91ec0aec2ecd3a60ab080cff7b52a8f6d543b'); + expect(u8aToHex(result)).toEqual('0xc000fa40e72d7173e69ee54b980345ea01cb81e64258502e0247af4303dee91ec0aec2ecd3a60ab080cff7b52a8f6d543b'); }); it('needs two arguments', () => { diff --git a/packages/type-storage/src/fromMetadata/createFunction.ts b/packages/type-storage/src/fromMetadata/createFunction.ts index 2e1fdcecb7..be3c08e67b 100644 --- a/packages/type-storage/src/fromMetadata/createFunction.ts +++ b/packages/type-storage/src/fromMetadata/createFunction.ts @@ -58,7 +58,7 @@ export default function createFunction (section: Text | string, method: Text | s const param1Hashed = hasher(param1Encoded); const param2Hashed = key2Hasher(createType(type2, arg[1]).toU8a(true)); - return u8aConcat(param1Hashed, param2Hashed); + return Compact.addLengthPrefix(u8aConcat(param1Hashed, param2Hashed)); } if (meta.type.isMap) { diff --git a/packages/types/src/codec/createType.spec.ts b/packages/types/src/codec/createType.spec.ts index 6491fab6c5..0d239d7224 100644 --- a/packages/types/src/codec/createType.spec.ts +++ b/packages/types/src/codec/createType.spec.ts @@ -232,4 +232,18 @@ describe('createType', () => { createType('{"_enum": {"A": null, "B": "u32", "C": null} }', 1).toJSON() ).toEqual({ B: 0 }); }); + + it('throw error when create base is a StorageData with null value and isPedantic is true' , () => { + const base = createType('StorageData', null); + expect( + () => createType('DoubleMap>', base, true) + ).toThrow(/Encoding for input doesn't match output, created 0x00 from 0x/); + }); + + it('throw error when create base is a StorageData with null value and isPedantic is true' , () => { + const base = createType('StorageData', null); + expect( + () => createType('Vec<(BlockNumber,EventIndex)>', base, true) + ).toThrow(/Encoding for input doesn't match output, created 0x00 from 0x/); + }); }); diff --git a/packages/types/src/primitive/StorageKey.ts b/packages/types/src/primitive/StorageKey.ts index 98ff4b4840..12da2b5230 100644 --- a/packages/types/src/primitive/StorageKey.ts +++ b/packages/types/src/primitive/StorageKey.ts @@ -4,7 +4,6 @@ import { assert, isFunction, isString, isU8a } from '@polkadot/util'; -import U8a from '../codec/U8a'; import { StorageFunctionMetadata as MetaV4 } from '../Metadata/v4/Storage'; import { AnyU8a } from '../types'; import Bytes from './Bytes'; @@ -70,14 +69,6 @@ export default class StorageKey extends Bytes { assert(isFunction(fn), 'Expected function input for key construction'); - if (fn.meta && fn.meta.type.isDoubleMap) { - return { - key: new U8a(fn(...arg)), // skip compact length check in decodeBytes - method: fn.method, - section: fn.section - }; - } - return { key: fn(...arg), method: fn.method,