From 2630f378a400d3ad7b192e8f3de8e68c3249068e Mon Sep 17 00:00:00 2001 From: Jaco Greeff Date: Wed, 15 May 2019 08:07:23 +0200 Subject: [PATCH] new EventRecord with topics (#902) * new EventRecord with topics * Add additional decoding checks (from #612) * Slightly rework storage parsing (future checks) * Split storage result formatting (set vs data) * Allow v3 event parsing * Max Vector length check * assert during Event parsing * Well, HACK-y fallback * Revert "Well, HACK-y fallback" This reverts commit edbf61b99da2753bec3a535126660e71bdcd9861. * Node overrides... * Add implVersion * Register types * Update with polkadot as well * inject compatibility types * EventRecord77 in testing * Conversion with DoubleMap * E2E tested against v3 & v4 * Filter with some * Split archive/remove alex tests * beforeAll -> beforeEach * Remove .only * EventRecordV0 -> 77 --- packages/api/src/Base.ts | 5 +- packages/api/src/nodeCompat.ts | 48 ++++++ .../api/test/e2e/promise-alex-archive.spec.js | 58 ++++++++ packages/api/test/e2e/promise-alex.spec.js | 140 +++++++----------- .../api/test/e2e/promise-contract.spec.js | 4 +- packages/api/test/e2e/promise-queries.spec.js | 26 +++- packages/rpc-core/src/index.ts | 121 +++++++++------ packages/rpc-provider/src/mock/index.ts | 4 +- packages/types/src/Metadata/v0/Storage.ts | 30 ++-- packages/types/src/Metadata/v2/Storage.ts | 29 ++-- packages/types/src/Metadata/v3/Storage.ts | 6 +- packages/types/src/Metadata/v4/Metadata.ts | 4 + packages/types/src/codec/EnumType.ts | 10 +- packages/types/src/codec/Vector.ts | 11 +- packages/types/src/primitive/Bytes.ts | 11 +- packages/types/src/primitive/Text.ts | 13 +- packages/types/src/type/Event.ts | 106 ++++++++----- packages/types/src/type/EventRecord.spec.ts | 4 +- packages/types/src/type/EventRecord.ts | 50 ++++++- packages/types/src/type/Extrinsic.ts | 13 +- packages/types/src/type/index.ts | 5 +- 21 files changed, 463 insertions(+), 235 deletions(-) create mode 100644 packages/api/src/nodeCompat.ts create mode 100644 packages/api/test/e2e/promise-alex-archive.spec.js diff --git a/packages/api/src/Base.ts b/packages/api/src/Base.ts index 161dd94867..794f69ced8 100644 --- a/packages/api/src/Base.ts +++ b/packages/api/src/Base.ts @@ -30,6 +30,7 @@ import { StorageFunction } from '@plugnet/types/primitive/StorageKey'; import { assert, compactStripLength, isFunction, isObject, isUndefined, logger, u8aToHex } from '@plugnet/util'; import { cryptoWaitReady } from '@plugnet/util-crypto'; +import injectNodeCompat from './nodeCompat'; import createSubmittable, { SubmittableExtrinsic } from './SubmittableExtrinsic'; type MetaDecoration = { @@ -398,6 +399,8 @@ export default abstract class ApiBase implement this._genesisHash = this._options.source.genesisHash; } + injectNodeCompat(this._runtimeVersion as RuntimeVersion); + const extrinsics = extrinsicsFromMeta(this.runtimeMetadata.asV0); const storage = storageFromMeta(this.runtimeMetadata); @@ -413,7 +416,7 @@ export default abstract class ApiBase implement // only inject if we are not a clone (global init) if (!this._options.source) { - Event.injectMetadata(this.runtimeMetadata.asV0); + Event.injectMetadata(this.runtimeMetadata); Method.injectMethods(extrinsics); } diff --git a/packages/api/src/nodeCompat.ts b/packages/api/src/nodeCompat.ts new file mode 100644 index 0000000000..00f57cde0b --- /dev/null +++ b/packages/api/src/nodeCompat.ts @@ -0,0 +1,48 @@ +// Copyright 2017-2019 @polkadot/api authors & contributors +// This software may be modified and distributed under the terms +// of the Apache-2.0 license. See the LICENSE file for details. + +import { Constructor } from '@polkadot/types/types'; + +import { EventRecord77, RuntimeVersion, getTypeRegistry } from '@polkadot/types'; + +type Compat = { + nodeSpecs: Array<{ + name: string, + version: number + }>, + types: { + [index: string]: Constructor + } +}; + +const types: Array = [ + { + nodeSpecs: [ + { + name: 'node', + version: 77 + }, + { + name: 'polkadot', + version: 0xffff // don't have a spec version as of yet + } + ], + types: { + 'EventRecord': EventRecord77 + } + } +]; + +export default function injectNodeCompat ({ specName, specVersion }: RuntimeVersion): void { + types + .filter(({ nodeSpecs }) => + nodeSpecs.some((spec) => + specName.eq(spec.name) && + specVersion.ltn(spec.version) + ) + ) + .forEach(({ types }) => { + getTypeRegistry().register(types); + }); +} diff --git a/packages/api/test/e2e/promise-alex-archive.spec.js b/packages/api/test/e2e/promise-alex-archive.spec.js new file mode 100644 index 0000000000..8957e1a65b --- /dev/null +++ b/packages/api/test/e2e/promise-alex-archive.spec.js @@ -0,0 +1,58 @@ +// Copyright 2017-2019 @polkadot/api authors & contributors +// This software may be modified and distributed under the terms +// of the Apache-2.0 license. See the LICENSE file for details. + +import Api from '../../src/promise'; + +describe.skip('alex archive queries (local)', () => { + let api; + + beforeEach(() => { + jest.setTimeout(30000); + }); + + beforeEach(async () => { + api = await Api.create(); + + return api; + }); + + // https://github.com/polkadot-js/api/issues/845 + it('retrieves block with no issues', async () => { + const metadata = await api.rpc.state.getMetadata('0x6f6f9bba0eed8e3ae9446c37eee763f93118b52a315a7b46090453ba6288da1f'); + + console.error(JSON.stringify(metadata, null, 2)); + + const block = await api.rpc.chain.getBlock('0x6f6f9bba0eed8e3ae9446c37eee763f93118b52a315a7b46090453ba6288da1f') + + console.error(block); + + expect(block).toBeDefined(); + + return true; + }); + + // https://github.com/polkadot-js/api/issues/846 + it('handles toJSON with no issues', async (done) => { + const signedBlock = await api.rpc.chain.getBlock('0x85c62b581f38cb81c3e443d34392672beb1fb877017fd7237cc87704113259dc'); + const failed = signedBlock.block.extrinsics.filter((extrinsic) => { + try { + const json = extrinsic.method.toJSON(); + + console.error(json); + + return false; + } catch (error) { + console.log(extrinsic.method); + console.log(extrinsic.method.keys()); + console.error(error); + + return true; + } + }); + + expect(failed).toHaveLength(0); + + done(); + }); +}); diff --git a/packages/api/test/e2e/promise-alex.spec.js b/packages/api/test/e2e/promise-alex.spec.js index 4c7faf89ff..d48e1728b4 100644 --- a/packages/api/test/e2e/promise-alex.spec.js +++ b/packages/api/test/e2e/promise-alex.spec.js @@ -2,70 +2,46 @@ // This software may be modified and distributed under the terms // of the Apache-2.0 license. See the LICENSE file for details. +import { Hash, Header } from '@polkadot/types'; + import Api from '../../src/promise'; import WsProvider from '../../../rpc-provider/src/ws'; -import { Hash, Header } from '@polkadot/types'; +const WS_URL = 'wss://poc3-rpc.polkadot.io/'; +// const WS_URL = 'wss://substrate-rpc.parity.io/'; describe.skip('alex queries', () => { let api; beforeEach(() => { - jest.setTimeout(3000000); + jest.setTimeout(30000); }); - describe('Remote RPC queries', () => { - beforeAll(async () => { - api = await Api.create({ - provider: new WsProvider('wss://poc3-rpc.polkadot.io/') - }); - - return api; + beforeEach(async () => { + api = await Api.create({ + provider: new WsProvider(WS_URL) }); - it('retrieves the list of validators', (done) => { - api.query.staking.validators((res) => { - console.log('api.query.staking.validators():', res.toJSON()); + return api; + }); - done(); - }); + it('retrieves the list of validators', (done) => { + api.query.staking.validators((res) => { + console.error(res); + + console.log('api.query.staking.validators():', res.toJSON()); + + done(); }); + }); - it('retrieves the list of the controllers', (done) => { - api.derive.staking.constrollers((res) => { - console.log('api.query.staking.controllers():', res.toJSON()); + it('retrieves a single value', (done) => { + api.query.staking.validators('5DuiZFa184E9iCwbh4WjXYvJ88NHvWJbS8SARY8Ev1YEqrri', (res) => { + console.error(res); - expect(res.length).toHaveLength(2); - expect(res[0].length).not.toHaveLength(0); - expect(res[0].length).toEqual(res[1].length); + console.log('api.query.staking.validators(id):', res.toJSON()); - done(); - }); - }); - - it('retrieves a single value', (done) => { - api.query.staking.validators('5EF7wdkP9XZq38fu2ioAvxoEhHUJUc1E2KbuSQSMGofRKhCL', (res) => { - console.log('api.query.staking.validators(id):', res.toJSON()); - - done(); - }); - }); - - it('derives a list of the controllers', (done) => { - api.derive.staking.controllers((res) => { - console.log('api.derive.staking.controllers:', JSON.stringify(res)); - - done(); - }); - }); - - it.skip('retrieves the list of nominators', (done) => { - let count = 0; - api.query.staking.nominators((res) => { - console.log(`[${++count}]:: nominators(${res[0].length}):`, res.toJSON()); - - // done(); - }); + done(); }); it('Gets the hash of the last finalized header', async (done) => { @@ -83,50 +59,36 @@ describe.skip('alex queries', () => { }); }); - describe.skip('Local archive queries (for debug)', () => { - beforeAll(async () => { - api = await Api.create(); - - return api; - }); - - // https://github.com/polkadot-js/api/issues/845 - it('retrieves block with no issues', async () => { - const metadata = await api.rpc.state.getMetadata('0x6f6f9bba0eed8e3ae9446c37eee763f93118b52a315a7b46090453ba6288da1f'); - - console.error(JSON.stringify(metadata, null, 2)); - - const block = await api.rpc.chain.getBlock('0x6f6f9bba0eed8e3ae9446c37eee763f93118b52a315a7b46090453ba6288da1f') - - console.error(block); - - expect(block).toBeDefined(); - - return true; - }); - - // https://github.com/polkadot-js/api/issues/846 - it('handles toJSON with no issues', async (done) => { - const signedBlock = await api.rpc.chain.getBlock('0x85c62b581f38cb81c3e443d34392672beb1fb877017fd7237cc87704113259dc'); - const failed = signedBlock.block.extrinsics.filter((extrinsic) => { - try { - const json = extrinsic.method.toJSON(); - - console.error(json); - - return false; - } catch (error) { - console.log(extrinsic.method); - console.log(extrinsic.method.keys()); - console.error(error); - - return true; - } - }); - - expect(failed).toHaveLength(0); + it('derives a list of the controllers', (done) => { + api.derive.staking.controllers((res) => { + console.log('api.derive.staking.controllers:', JSON.stringify(res)); done(); }); }); + + + it('makes a query at a latest block (specified)', async () => { + const header = await api.rpc.chain.getHeader(); + const events = await api.query.system.events.at(header.hash); + + events.forEach(({ event: { data, method, section }, phase, topics }, index) => { + console.error(index, phase.toString(), `: ${section}.${method}`, data.toString(), topics.toString()); + }); + + expect(events.length).not.toEqual(0); + }); + + it('subscribes to events', (done) => { + api.query.system.events((events) => { + console.error(JSON.stringify(events)); + + events.forEach(({ event: { data, method, section }, phase, topics }, index) => { + console.error(index, phase.toString(), `: ${section}.${method}`, data.toString(), topics.toString()); + }); + + expect(events).not.toHaveLength(0); + done(); + }); + }); }); diff --git a/packages/api/test/e2e/promise-contract.spec.js b/packages/api/test/e2e/promise-contract.spec.js index 0f28eb1041..bc7cf8050f 100644 --- a/packages/api/test/e2e/promise-contract.spec.js +++ b/packages/api/test/e2e/promise-contract.spec.js @@ -32,7 +32,7 @@ describe.skip('e2e contracts', () => { describe('incrementer', () => { let abi; - beforeAll(() => { + beforeEach(() => { abi = new ContractAbi(incrementer); }); @@ -94,7 +94,7 @@ describe.skip('e2e contracts', () => { describe('erc20', () => { let abi; - beforeAll(() => { + beforeEach(() => { abi = abi = new ContractAbi(erc20); }); diff --git a/packages/api/test/e2e/promise-queries.spec.js b/packages/api/test/e2e/promise-queries.spec.js index 06e95bc7cd..0006cc5c6b 100644 --- a/packages/api/test/e2e/promise-queries.spec.js +++ b/packages/api/test/e2e/promise-queries.spec.js @@ -7,8 +7,11 @@ import testingPairs from '@plugnet/keyring/testingPairs'; import { LinkageResult } from '@plugnet/types/codec/Linkage'; import Api from '../../src/promise'; +import WsProvider from '../../../rpc-provider/src/ws'; const ZERO = new BN(0); +const WS_URL = 'ws://127.0.0.1:9944'; +// const WS_URL = 'wss://poc3-rpc.polkadot.io/'; describe.skip('e2e queries', () => { const keyring = testingPairs({ type: 'ed25519' }); @@ -16,7 +19,9 @@ describe.skip('e2e queries', () => { beforeEach(async (done) => { if (!api) { - api = await Api.create(); + api = await Api.create({ + provider: new WsProvider(WS_URL) + }); } jest.setTimeout(30000); @@ -108,7 +113,7 @@ describe.skip('e2e queries', () => { api.derive.balances.all( keyring.alice.address(), (all) => { - expect(all.accountId).toEqual(keyring.alice.address()); + expect(all.accountId.toString()).toEqual(keyring.alice.address()); expect(all.freeBalance).toBeDefined(); expect(all.freeBalance.gt(ZERO)).toBe(true); @@ -127,10 +132,25 @@ describe.skip('e2e queries', () => { ); }); - it('makes a query at a specific block', async () => { + it('makes a query at a latest block (specified)', async () => { const header = await api.rpc.chain.getHeader(); const events = await api.query.system.events.at(header.hash); + events.forEach(({ event: { data, method, section }, phase, topics }, index) => { + console.error(index, phase.toString(), `: ${section}.${method}`, data.toString(), topics.toString()); + }); + expect(events.length).not.toEqual(0); }); + + it('subscribes to events', (done) => { + api.query.system.events((events) => { + events.forEach(({ event: { data, method, section }, phase, topics }, index) => { + console.error(index, phase.toString(), `: ${section}.${method}`, data.toString(), topics.toString()); + }); + + expect(events).not.toHaveLength(0); + done(); + }); + }); }); diff --git a/packages/rpc-core/src/index.ts b/packages/rpc-core/src/index.ts index d39e11fc20..e09b8542b7 100644 --- a/packages/rpc-core/src/index.ts +++ b/packages/rpc-core/src/index.ts @@ -14,6 +14,15 @@ import { ExtError, assert, isFunction, isNull, logger } from '@plugnet/util'; const l = logger('rpc-core'); +const EMPTY_META = { + fallback: undefined, + modifier: { isOptional: true }, + type: { + asMap: { isLinked: false }, + isMap: false + } +}; + /** * @name Rpc * @summary The API may use a HTTP or WebSockets provider. @@ -187,64 +196,78 @@ export default class Rpc implements RpcInterface { const base = createType(method.type as string, result); if (method.type === 'StorageData') { - // single return value (via state.getStorage), decode the value based on the - // outputType that we have specified. Fallback to Data on nothing - const key = params[0] as StorageKey; - const type = key.outputType || 'Data'; - const Clazz = createClass(type); - const meta = key.meta || { fallback: undefined, modifier: { isOptional: true } }; - - if (key.meta && key.meta.type.isMap && key.meta.type.asMap.isLinked) { - // linked map - return new Clazz(base); - } else { - return meta.modifier.isOptional - ? new Option(Clazz, isNull(result) ? null : new Clazz(base)) - : new Clazz(base); - } + return this.formatStorageData(params[0] as StorageKey, base, isNull(result)); } else if (method.type === 'StorageChangeSet') { // multiple return values (via state.storage subscription), decode the values // one at a time, all based on the query types. Three values can be returned - // - Base - There is a valid value, non-empty // - null - The storage key is empty (but in the resultset) // - undefined - The storage value is not in the resultset - return (params[0] as Vector).reduce((result, key: StorageKey) => { - // Fallback to Data (i.e. just the encoding) if we don't have a specific type - const type = key.outputType || 'Data'; - const Clazz = createClass(type); + return (params[0] as Vector).reduce((results, key: StorageKey) => { + results.push(this.formatStorageSet(key, base as StorageChangeSet)); - // see if we have a result value for this specific key - const hexKey = key.toHex(); - const { value } = (base as StorageChangeSet).changes.find((item) => - item.key.toHex() === hexKey - ) || { value: null }; - const meta = key.meta || { fallback: undefined, modifier: { isOptional: true } }; - - if (!value) { - // if we don't have a value, do not fill in the entry, it will be up to the - // caller to sort this out, either ignoring or having a cache for older values - result.push(undefined); - } else { - if (key.meta && key.meta.type.isMap && key.meta.type.asMap.isLinked) { - // linked map - result.push(new Clazz(value.unwrapOr(null))); - } else { - if (meta.modifier.isOptional) { - // create option either with the existing value, or empty when - // there is no value returned - result.push(new Option(Clazz, value.isNone ? null : new Clazz(value.unwrap()))); - } else { - // for `null` we fallback to the default value, or create an empty type, - // otherwise we return the actual value as retrieved - result.push(new Clazz(value.unwrapOr(meta.fallback))); - } - } - } - - return result; - }, [] as Array); + return results; + }, [] as Array); } return base; } + + private formatStorageData (key: StorageKey, base: Codec, isNull: boolean): Codec { + // single return value (via state.getStorage), decode the value based on the + // outputType that we have specified. Fallback to Data on nothing + const type = key.outputType || 'Data'; + const Clazz = createClass(type); + const meta = key.meta || EMPTY_META; + let created: Codec; + + if (meta.type.isMap && meta.type.asMap.isLinked) { + // linked map + created = new Clazz(base); + } else { + created = meta.modifier.isOptional + ? new Option(Clazz, isNull ? null : new Clazz(base)) + : new Clazz(base); + } + + // TODO Check that the encoded created value matches that what was supplied + + return created; + } + + private formatStorageSet (key: StorageKey, base: StorageChangeSet): Codec | undefined { + // Fallback to Data (i.e. just the encoding) if we don't have a specific type + const type = key.outputType || 'Data'; + const Clazz = createClass(type); + + // see if we have a result value for this specific key + const hexKey = key.toHex(); + const { value } = base.changes.find((item) => + item.key.toHex() === hexKey + ) || { value: null }; + const meta = key.meta || EMPTY_META; + + // if we don't have a value, do not fill in the entry, it will be up to the + // caller to sort this out, either ignoring or having a cache for older values + let created: Codec | undefined; + + if (value) { + if (meta.type.isMap && meta.type.asMap.isLinked) { + // linked map + created = new Clazz(value.unwrapOr(null)); + } else if (meta.modifier.isOptional) { + // create option either with the existing value, or empty when + // there is no value returned + created = new Option(Clazz, value.isNone ? null : new Clazz(value.unwrap())); + } else { + // for `null` we fallback to the default value, or create an empty type, + // otherwise we return the actual value as retrieved + created = new Clazz(value.unwrapOr(meta.fallback)); + } + + // TODO Check that the encoded created value matches that what was supplied + } + + return created; + } } diff --git a/packages/rpc-provider/src/mock/index.ts b/packages/rpc-provider/src/mock/index.ts index 6661693701..9d4cf2351e 100644 --- a/packages/rpc-provider/src/mock/index.ts +++ b/packages/rpc-provider/src/mock/index.ts @@ -12,7 +12,7 @@ import testKeyring from '@plugnet/keyring/testing'; import storage from '@plugnet/storage/static'; import { Codec } from '@plugnet/types/types'; import metadata from '@plugnet/types/Metadata/v0/static'; -import { Header } from '@plugnet/types'; +import { Header, RuntimeVersion } from '@plugnet/types'; import { bnToU8a, logger, u8aToHex } from '@plugnet/util'; import { randomAsU8a } from '@plugnet/util-crypto'; @@ -43,7 +43,7 @@ export default class Mock implements ProviderInterface { public isUpdating: boolean = true; private requests: { [index: string]: (...params: any[]) => string } = { 'chain_getBlockHash': (blockNumber: number): string => '0x1234', - 'chain_getRuntimeVersion': (): string => '0x1234', + 'chain_getRuntimeVersion': (): string => new RuntimeVersion().toHex(), 'state_getStorage': (storage: MockState$Db, params: Array): string => { return u8aToHex( storage[(params[0] as string)] diff --git a/packages/types/src/Metadata/v0/Storage.ts b/packages/types/src/Metadata/v0/Storage.ts index b3e151878e..291dfdb317 100644 --- a/packages/types/src/Metadata/v0/Storage.ts +++ b/packages/types/src/Metadata/v0/Storage.ts @@ -11,9 +11,9 @@ import EnumType from '../../codec/EnumType'; import Struct from '../../codec/Struct'; import Vector from '../../codec/Vector'; import Bytes from '../../primitive/Bytes'; +import Null from '../../primitive/Null'; import Text from '../../primitive/Text'; import Type from '../../primitive/Type'; -import { DoubleMapType } from '../v4/Storage'; export class StorageFunctionModifier extends Enum { constructor (value?: any) { @@ -33,6 +33,8 @@ export class StorageFunctionModifier extends Enum { } } +export class DoubleMapType extends Null {} + export class MapType extends Struct { private _isLinked = false; @@ -72,7 +74,7 @@ export class MapType extends Struct { export class PlainType extends Type { } -export class StorageFunctionType extends EnumType { +export class StorageFunctionType extends EnumType { constructor (value?: any, index?: number) { super({ PlainType, @@ -82,16 +84,11 @@ export class StorageFunctionType extends EnumType`; + } else if (this.isMap) { if (this.asMap.isLinked) { return `(${this.asMap.value.toString()}, Linkage<${this.asMap.key.toString()}>)`; } diff --git a/packages/types/src/Metadata/v2/Storage.ts b/packages/types/src/Metadata/v2/Storage.ts index ecc68e1d3a..860068ee91 100644 --- a/packages/types/src/Metadata/v2/Storage.ts +++ b/packages/types/src/Metadata/v2/Storage.ts @@ -11,10 +11,10 @@ import Struct from '../../codec/Struct'; import Vector from '../../codec/Vector'; import Bool from '../../primitive/Bool'; import Bytes from '../../primitive/Bytes'; +import Null from '../../primitive/Null'; import Text from '../../primitive/Text'; import Type from '../../primitive/Type'; import { PlainType, StorageFunctionModifier } from '../v1/Storage'; -import { DoubleMapType } from './../v4/Storage'; // Re-export classes that haven't changed between V1 and V2 export { @@ -22,6 +22,8 @@ export { StorageFunctionModifier }; +export class DoubleMapType extends Null {} + export class MapType extends Struct { constructor (value?: any) { super({ @@ -53,7 +55,7 @@ export class MapType extends Struct { } } -export class StorageFunctionType extends EnumType { +export class StorageFunctionType extends EnumType { constructor (value?: any, index?: number) { super({ PlainType, @@ -63,16 +65,11 @@ export class StorageFunctionType extends EnumType`; + } else if (this.isMap) { if (this.asMap.isLinked) { return `(${this.asMap.value.toString()}, Linkage<${this.asMap.key.toString()}>)`; } diff --git a/packages/types/src/Metadata/v3/Storage.ts b/packages/types/src/Metadata/v3/Storage.ts index ad8f52807b..0ee5067aca 100644 --- a/packages/types/src/Metadata/v3/Storage.ts +++ b/packages/types/src/Metadata/v3/Storage.ts @@ -126,10 +126,8 @@ export class StorageFunctionType extends EnumType`; + } else if (this.isMap) { if (this.asMap.isLinked) { return `(${this.asMap.value.toString()}, Linkage<${this.asMap.key.toString()}>)`; } diff --git a/packages/types/src/Metadata/v4/Metadata.ts b/packages/types/src/Metadata/v4/Metadata.ts index 5d7127f924..dbb47a32c6 100644 --- a/packages/types/src/Metadata/v4/Metadata.ts +++ b/packages/types/src/Metadata/v4/Metadata.ts @@ -14,6 +14,10 @@ import { MetadataCall } from '../v1/Calls'; import { MetadataEvent } from '../v1/Events'; import { StorageFunctionMetadata } from './Storage'; +export { + MetadataEvent +}; + /** * @name MetadataModule * @description diff --git a/packages/types/src/codec/EnumType.ts b/packages/types/src/codec/EnumType.ts index abd331db60..2f61bde049 100644 --- a/packages/types/src/codec/EnumType.ts +++ b/packages/types/src/codec/EnumType.ts @@ -4,7 +4,7 @@ import { Codec, Constructor, ConstructorDef } from '../types'; -import { assert, hexToU8a, isHex, isNumber, isObject, isString, isU8a, u8aConcat, u8aToHex } from '@plugnet/util'; +import { assert, hexToU8a, isHex, isNumber, isObject, isString, isU8a, isUndefined, u8aConcat, u8aToHex } from '@plugnet/util'; import Null from '../primitive/Null'; import Base from './Base'; @@ -98,15 +98,19 @@ export default class EnumType extends Base implements Codec { const aliasKey = aliasses[keyLower] || keyLower; const index = keys.indexOf(aliasKey); - assert(index !== -1, `Cannot map input on JSON, unable to find '${key}' in ${keys.join(', ')}`); + assert(index !== -1, `Cannot map EnumType JSON, unable to find '${key}' in ${keys.join(', ')}`); return EnumType.createValue(def, index, value); } private static createValue (def: TypesDef, index: number = 0, value?: any): Decoded { + const Clazz = Object.values(def)[index]; + + assert(!isUndefined(Clazz), `Unable to create EnumType via index ${index}, in ${Object.keys(def).join(', ')}`); + return { index, - value: new (Object.values(def)[index])(value) + value: new Clazz(value) }; } diff --git a/packages/types/src/codec/Vector.ts b/packages/types/src/codec/Vector.ts index 6c341bde4f..abc01f3e7b 100644 --- a/packages/types/src/codec/Vector.ts +++ b/packages/types/src/codec/Vector.ts @@ -2,13 +2,15 @@ // This software may be modified and distributed under the terms // of the Apache-2.0 license. See the LICENSE file for details. -import { u8aToU8a } from '@plugnet/util'; +import { u8aToU8a, assert } from '@plugnet/util'; import Compact from './Compact'; import { Codec, Constructor } from '../types'; import { decodeU8a } from './utils'; import AbstractArray from './AbstractArray'; +const MAX_LENGTH = 32768; + /** * @name Vector * @description @@ -43,10 +45,11 @@ export default class Vector extends AbstractArray { } const u8a = u8aToU8a(value); - const [offset, _length] = Compact.decodeU8a(u8a); - const length = _length.toNumber(); + const [offset, length] = Compact.decodeU8a(u8a); - return decodeU8a(u8a.subarray(offset), new Array(length).fill(Type)) as T[]; + assert(length.lten(MAX_LENGTH), `Vector length ${length.toString()} exceeds ${MAX_LENGTH}`); + + return decodeU8a(u8a.subarray(offset), new Array(length.toNumber()).fill(Type)) as T[]; } static with (Type: Constructor): Constructor> { diff --git a/packages/types/src/primitive/Bytes.ts b/packages/types/src/primitive/Bytes.ts index 71f4ba4990..ea75f42595 100644 --- a/packages/types/src/primitive/Bytes.ts +++ b/packages/types/src/primitive/Bytes.ts @@ -2,7 +2,7 @@ // This software may be modified and distributed under the terms // of the Apache-2.0 license. See the LICENSE file for details. -import { isString, isU8a, u8aToU8a } from '@plugnet/util'; +import { assert, isString, isU8a, u8aToU8a } from '@plugnet/util'; import { AnyU8a } from '../types'; import Compact from '../codec/Compact'; @@ -46,10 +46,17 @@ export default class Bytes extends U8a { // i.e. new Bytes(new Bytes(...)) will work as expected return value; } else if (isU8a(value)) { + if (!value.length) { + return new Uint8Array(); + } + // handle all other Uint8Array inputs, these do have a length prefix const [offset, length] = Compact.decodeU8a(value); + const total = offset + length.toNumber(); - return value.subarray(offset, offset + length.toNumber()); + assert(total <= value.length, `Bytes: required length less than remainder, expected at least ${total}, found ${value.length}`); + + return value.subarray(offset, total); } return value; diff --git a/packages/types/src/primitive/Text.ts b/packages/types/src/primitive/Text.ts index d11a8ae55b..7f6247fe9a 100644 --- a/packages/types/src/primitive/Text.ts +++ b/packages/types/src/primitive/Text.ts @@ -2,7 +2,7 @@ // This software may be modified and distributed under the terms // of the Apache-2.0 license. See the LICENSE file for details. -import { isString, stringToU8a, u8aToString, u8aToHex } from '@plugnet/util'; +import { assert, isString, stringToU8a, u8aToString, u8aToHex } from '@plugnet/util'; import { AnyU8a, Codec } from '../types'; import Compact from '../codec/Compact'; @@ -31,9 +31,16 @@ export default class Text extends String implements Codec { if (isString(value)) { return value.toString(); } else if (value instanceof Uint8Array) { - const [offset, length] = Compact.decodeU8a(value); + if (!value.length) { + return ''; + } - return u8aToString(value.subarray(offset, offset + length.toNumber())); + const [offset, length] = Compact.decodeU8a(value); + const total = offset + length.toNumber(); + + assert(total <= value.length, `Text: required length less than remainder, expected at least ${total}, found ${value.length}`); + + return u8aToString(value.subarray(offset, total)); } return `${value}`; diff --git a/packages/types/src/type/Event.ts b/packages/types/src/type/Event.ts index 2aa075e2a1..95ced0765a 100644 --- a/packages/types/src/type/Event.ts +++ b/packages/types/src/type/Event.ts @@ -4,30 +4,40 @@ import { Constructor } from '../types'; -import { isUndefined, stringCamelCase, u8aToHex } from '@plugnet/util'; +import { assert, isUndefined, stringCamelCase, u8aToHex } from '@plugnet/util'; import Struct from '../codec/Struct'; import Tuple from '../codec/Tuple'; import U8aFixed from '../codec/U8aFixed'; -import Null from '../primitive/Null'; import { TypeDef, getTypeClass, getTypeDef } from '../codec/createType'; -import MetadataV0 from '../Metadata/v0'; -import { EventMetadata } from '../Metadata/v0/Events'; +import Null from '../primitive/Null'; +import U32 from '../primitive/U32'; +import Metadata from '../Metadata'; +import { EventMetadata as EventMetadataV0 } from '../Metadata/v0/Events'; +import { MetadataEvent as EventMetadataV4 } from '../Metadata/v1/Events'; const EventTypes: { [index: string]: Constructor } = {}; +/** + * @name EventIndex + * @description + * The Substrate EventIndex representation as a [[U32]]. + */ +export class EventIndex extends U32 { +} + /** * @name EventData * @description * Wrapper for the actual data that forms part of an [[Event]] */ export class EventData extends Tuple { - private _meta: EventMetadata; + private _meta: EventMetadataV0 | EventMetadataV4; private _method: string; private _section: string; private _typeDef: Array; - constructor (Types: Array, value: Uint8Array, typeDef: Array, meta: EventMetadata, section: string, method: string) { + constructor (Types: Array, value: Uint8Array, typeDef: Array, meta: EventMetadataV0 | EventMetadataV4, section: string, method: string) { super(Types, value); this._meta = meta; @@ -39,7 +49,7 @@ export class EventData extends Tuple { /** * @description The wrapped [[EventMetadata]] */ - get meta (): EventMetadata { + get meta (): EventMetadataV0 | EventMetadataV4 { return this._meta; } @@ -66,12 +76,12 @@ export class EventData extends Tuple { } /** - * @name EventIndex + * @name EventId * @description * This follows the same approach as in [[Method]], we have the `[sectionIndex, methodIndex]` pairing * that indicates the actual event fired */ -export class EventIndex extends U8aFixed { +export class EventId extends U8aFixed { constructor (value?: any) { super(value, 16); } @@ -90,23 +100,22 @@ export default class Event extends Struct { const { DataType, value } = Event.decodeEvent(_value); super({ - index: EventIndex, + index: EventId, data: DataType }, value); } static decodeEvent (value: Uint8Array = new Uint8Array()) { + if (!value.length) { + return { + DataType: Null + }; + } + const index = value.subarray(0, 2); const DataType = EventTypes[index.toString()]; - if (isUndefined(DataType)) { - console.error(`Unable to decode event for index ${u8aToHex(index)}`); - - return { - DataType: Null, - value: undefined - }; - } + assert(!isUndefined(DataType), `Unable to decode event for index ${u8aToHex(index)}`); return { DataType, @@ -119,23 +128,48 @@ export default class Event extends Struct { // This is called/injected by the API on init, allowing a snapshot of // the available system events to be used in lookups - static injectMetadata (metadata: MetadataV0): void { - metadata.events.forEach((section, sectionIndex) => { - const sectionName = stringCamelCase(section.name.toString()); + static injectMetadata (metadata: Metadata): void { + if (metadata.version === 4) { + let sectionIndex = 0; - section.events.forEach((meta, methodIndex) => { - const methodName = meta.name.toString(); - const eventIndex = new Uint8Array([sectionIndex, methodIndex]); - const typeDef = meta.arguments.map((arg) => getTypeDef(arg)); - const Types = typeDef.map(getTypeClass); + metadata.asV4.modules.forEach((section) => { + const sectionName = stringCamelCase(section.name.toString()); - EventTypes[eventIndex.toString()] = class extends EventData { - constructor (value: Uint8Array) { - super(Types, value, typeDef, meta, sectionName, methodName); - } - }; + if (!section.events.isNone && !section.events.isEmpty) { + section.events.unwrap().forEach((meta, methodIndex) => { + const methodName = meta.name.toString(); + const eventIndex = new Uint8Array([sectionIndex, methodIndex]); + const typeDef = meta.args.map((arg) => getTypeDef(arg)); + const Types = typeDef.map(getTypeClass); + + EventTypes[eventIndex.toString()] = class extends EventData { + constructor (value: Uint8Array) { + super(Types, value, typeDef, meta, sectionName, methodName); + } + }; + }); + + sectionIndex += 1; + } }); - }); + } else { + metadata.asV0.events.forEach((section, sectionIndex) => { + const sectionName = stringCamelCase(section.name.toString()); + + section.events.forEach((meta, methodIndex) => { + const methodName = meta.name.toString(); + const eventIndex = new Uint8Array([sectionIndex, methodIndex]); + const typeDef = meta.arguments.map((arg) => getTypeDef(arg)); + const Types = typeDef.map(getTypeClass); + + EventTypes[eventIndex.toString()] = class extends EventData { + constructor (value: Uint8Array) { + super(Types, value, typeDef, meta, sectionName, methodName); + } + }; + }); + }); + } } /** @@ -146,16 +180,16 @@ export default class Event extends Struct { } /** - * @description The [[EventIndex]], identifying the raw event + * @description The [[EventId]], identifying the raw event */ - get index (): EventIndex { - return this.get('index') as EventIndex; + get index (): EventId { + return this.get('index') as EventId; } /** * @description The [[EventMetadata]] with the documentation */ - get meta (): EventMetadata { + get meta (): EventMetadataV0 | EventMetadataV4 { return this.data.meta; } diff --git a/packages/types/src/type/EventRecord.spec.ts b/packages/types/src/type/EventRecord.spec.ts index 9137bc7510..e61e669346 100644 --- a/packages/types/src/type/EventRecord.spec.ts +++ b/packages/types/src/type/EventRecord.spec.ts @@ -18,7 +18,7 @@ describe('EventRecord', () => { describe('v0', () => { beforeEach(() => { Event.injectMetadata( - new Metadata(metadataV0).asV0 + new Metadata(metadataV0) ); }); @@ -53,7 +53,7 @@ describe('EventRecord', () => { describe.skip('v1', () => { beforeEach(() => { Event.injectMetadata( - new Metadata(metadataV1).asV0 + new Metadata(metadataV1) ); }); diff --git a/packages/types/src/type/EventRecord.ts b/packages/types/src/type/EventRecord.ts index 3a92cb5d76..6d7c6c3f1d 100644 --- a/packages/types/src/type/EventRecord.ts +++ b/packages/types/src/type/EventRecord.ts @@ -6,8 +6,10 @@ import { assert } from '@plugnet/util'; import EnumType from '../codec/EnumType'; import Struct from '../codec/Struct'; +import Vector from '../codec/Vector'; import Null from '../primitive/Null'; import U32 from '../primitive/U32'; +import Hash from './Hash'; import Event from './Event'; /** @@ -73,12 +75,12 @@ export class Phase extends EnumType { } /** - * @name EventRecord + * @name EventRecord77 * @description * A record for an [[Event]] (as specified by [[Metadata]]) with the specific [[Phase]] of * application. */ -export default class EventRecord extends Struct { +export class EventRecord77 extends Struct { constructor (value: any) { super({ phase: Phase, @@ -99,4 +101,48 @@ export default class EventRecord extends Struct { get phase (): Phase { return this.get('phase') as Phase; } + + /** + * @description The [[Hash]] topics for this event (empty, compat) + */ + get topics (): Vector { + return new (Vector.with(Hash))(); + } +} + +/** + * @name EventRecord + * @description + * A record for an [[Event]] (as specified by [[Metadata]]) with the specific [[Phase]] of + * application. + */ +export default class EventRecord extends Struct { + constructor (value: any) { + super({ + phase: Phase, + event: Event, + topics: Vector.with(Hash) + }, value); + } + + /** + * @description The [[Event]] this record refers to + */ + get event (): Event { + return this.get('event') as Event; + } + + /** + * @description The [[Phase]] where the event was generated + */ + get phase (): Phase { + return this.get('phase') as Phase; + } + + /** + * @description The [[Hash]] topics for this event + */ + get topics (): Vector { + return this.get('topics') as Vector; + } } diff --git a/packages/types/src/type/Extrinsic.ts b/packages/types/src/type/Extrinsic.ts index bd2518e550..cb042db97d 100644 --- a/packages/types/src/type/Extrinsic.ts +++ b/packages/types/src/type/Extrinsic.ts @@ -5,7 +5,7 @@ import { KeyringPair } from '@plugnet/keyring/types'; import { AnyNumber, AnyU8a, ArgsDef, Codec, IExtrinsic, SignatureOptions } from '../types'; -import { isHex, isU8a, u8aToHex, u8aToU8a } from '@plugnet/util'; +import { assert, isHex, isU8a, u8aToHex, u8aToU8a } from '@plugnet/util'; import { blake2AsU8a } from '@plugnet/util-crypto'; import Compact from '../codec/Compact'; @@ -58,9 +58,16 @@ export default class Extrinsic extends Struct implements IExtrinsic { : Compact.addLengthPrefix(u8a) ); } else if (isU8a(value)) { - const [offset, length] = Compact.decodeU8a(value); + if (!value.length) { + return new Uint8Array(); + } - return value.subarray(offset, offset + length.toNumber()); + const [offset, length] = Compact.decodeU8a(value); + const total = offset + length.toNumber(); + + assert(total <= value.length, `Extrinsic: required length less than remainder, expected at least ${total}, found ${value.length}`); + + return value.subarray(offset, total); } else if (value instanceof Method) { return { method: value diff --git a/packages/types/src/type/index.ts b/packages/types/src/type/index.ts index 5d0ad1e660..6d301f7a42 100644 --- a/packages/types/src/type/index.ts +++ b/packages/types/src/type/index.ts @@ -25,9 +25,8 @@ export { default as BlockNumber } from './BlockNumber'; export { default as CodeHash } from './CodeHash'; export { default as ContractInfo } from './ContractInfo'; export { default as Digest, DigestItem } from './Digest'; -export { default as Event } from './Event'; -export { default as EventIndex } from './EventIndex'; -export { default as EventRecord } from './EventRecord'; +export { default as Event, EventIndex } from './Event'; +export { default as EventRecord, EventRecord77 } from './EventRecord'; export { default as Exposure } from './Exposure'; export { default as Extrinsic } from './Extrinsic'; export { default as ExtrinsicEra } from './ExtrinsicEra';