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
This commit is contained in:
Jaco Greeff
2019-05-15 08:07:23 +02:00
committed by GitHub
parent 3df5672159
commit 7b04ea0bfe
21 changed files with 479 additions and 212 deletions
+4 -1
View File
@@ -30,6 +30,7 @@ import { StorageFunction } from '@polkadot/types/primitive/StorageKey';
import { assert, compactStripLength, isFunction, isObject, isUndefined, logger, u8aToHex } from '@polkadot/util';
import { cryptoWaitReady } from '@polkadot/util-crypto';
import injectNodeCompat from './nodeCompat';
import createSubmittable, { SubmittableExtrinsic } from './SubmittableExtrinsic';
type MetaDecoration = {
@@ -398,6 +399,8 @@ export default abstract class ApiBase<CodecResult, SubscriptionResult> 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<CodecResult, SubscriptionResult> 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);
}
+48
View File
@@ -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<Compat> = [
{
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);
});
}
@@ -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();
});
});
+51 -89
View File
@@ -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();
});
});
});
@@ -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);
});
+23 -3
View File
@@ -7,8 +7,11 @@ import testingPairs from '@polkadot/keyring/testingPairs';
import { LinkageResult } from '@polkadot/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();
});
});
});
+72 -49
View File
@@ -14,6 +14,15 @@ import { ExtError, assert, isFunction, isNull, logger } from '@polkadot/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<StorageKey>).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<StorageKey>).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<Codec | null | undefined>);
return results;
}, [] as Array<Codec | undefined>);
}
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;
}
}
+2 -2
View File
@@ -12,7 +12,7 @@ import testKeyring from '@polkadot/keyring/testing';
import storage from '@polkadot/storage/static';
import { Codec } from '@polkadot/types/types';
import metadata from '@polkadot/types/Metadata/v0/static';
import { Header } from '@polkadot/types';
import { Header, RuntimeVersion } from '@polkadot/types';
import { bnToU8a, logger, u8aToHex } from '@polkadot/util';
import { randomAsU8a } from '@polkadot/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<any>): string => {
return u8aToHex(
storage[(params[0] as string)]
+24 -2
View File
@@ -11,6 +11,7 @@ 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';
@@ -32,6 +33,8 @@ export class StorageFunctionModifier extends Enum {
}
}
export class DoubleMapType extends Null {}
export class MapType extends Struct {
private _isLinked = false;
@@ -75,10 +78,20 @@ export class StorageFunctionType extends EnumType<PlainType | MapType> {
constructor (value?: any, index?: number) {
super({
PlainType,
MapType
MapType,
DoubleMapType
}, value, index);
}
/**
* @description The value as a mapped value
*/
get asDoubleMap (): DoubleMapType {
assert(this.isDoubleMap, `Cannot convert '${this.type}' via asDoubleMap`);
return this.value as DoubleMapType;
}
/**
* @description The value as a mapped value
*/
@@ -97,6 +110,13 @@ export class StorageFunctionType extends EnumType<PlainType | MapType> {
return this.value as PlainType;
}
/**
* @description `true` if the storage entry is a double map
*/
get isDoubleMap (): boolean {
return this.toNumber() === 2;
}
/**
* @description `true` if the storage entry is a map
*/
@@ -115,7 +135,9 @@ export class StorageFunctionType extends EnumType<PlainType | MapType> {
* @description Returns the string representation of the value
*/
toString (): string {
if (this.isMap) {
if (this.isDoubleMap) {
return `DoubleMap<${this.asDoubleMap.toString()}>`;
} else if (this.isMap) {
if (this.asMap.isLinked) {
return `(${this.asMap.value.toString()}, Linkage<${this.asMap.key.toString()}>)`;
}
+24 -2
View File
@@ -11,6 +11,7 @@ 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';
@@ -21,6 +22,8 @@ export {
StorageFunctionModifier
};
export class DoubleMapType extends Null {}
export class MapType extends Struct {
constructor (value?: any) {
super({
@@ -56,10 +59,20 @@ export class StorageFunctionType extends EnumType<PlainType | MapType> {
constructor (value?: any, index?: number) {
super({
PlainType,
MapType
MapType,
DoubleMapType
}, value, index);
}
/**
* @description The value as a mapped value
*/
get asDoubleMap (): DoubleMapType {
assert(this.isDoubleMap, `Cannot convert '${this.type}' via asDoubleMap`);
return this.value as DoubleMapType;
}
/**
* @description The value as a mapped value
*/
@@ -78,6 +91,13 @@ export class StorageFunctionType extends EnumType<PlainType | MapType> {
return this.value as PlainType;
}
/**
* @description `true` if the storage entry is a double map
*/
get isDoubleMap (): boolean {
return this.toNumber() === 2;
}
/**
* @description `true` if the storage entry is a map
*/
@@ -96,7 +116,9 @@ export class StorageFunctionType extends EnumType<PlainType | MapType> {
* @description Returns the string representation of the value
*/
toString (): string {
if (this.isMap) {
if (this.isDoubleMap) {
return `DoubleMap<${this.asDoubleMap.toString()}>`;
} else if (this.isMap) {
if (this.asMap.isLinked) {
return `(${this.asMap.value.toString()}, Linkage<${this.asMap.key.toString()}>)`;
}
+2 -4
View File
@@ -126,10 +126,8 @@ export class StorageFunctionType extends EnumType<PlainType | MapType | DoubleMa
*/
toString (): string {
if (this.isDoubleMap) {
return this.asDoubleMap.value.toString();
}
if (this.isMap) {
return `DoubleMap<${this.asDoubleMap.toString()}>`;
} else if (this.isMap) {
if (this.asMap.isLinked) {
return `(${this.asMap.value.toString()}, Linkage<${this.asMap.key.toString()}>)`;
}
@@ -14,6 +14,10 @@ import { MetadataCall } from '../v1/Calls';
import { MetadataEvent } from '../v1/Events';
import { StorageFunctionMetadata } from './Storage';
export {
MetadataEvent
};
/**
* @name MetadataModule
* @description
+7 -3
View File
@@ -4,7 +4,7 @@
import { Codec, Constructor, ConstructorDef } from '../types';
import { assert, hexToU8a, isHex, isNumber, isObject, isString, isU8a, u8aConcat, u8aToHex } from '@polkadot/util';
import { assert, hexToU8a, isHex, isNumber, isObject, isString, isU8a, isUndefined, u8aConcat, u8aToHex } from '@polkadot/util';
import Null from '../primitive/Null';
import Base from './Base';
@@ -98,15 +98,19 @@ export default class EnumType<T> extends Base<Codec> 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)
};
}
+7 -4
View File
@@ -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 '@polkadot/util';
import { u8aToU8a, assert } from '@polkadot/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<T extends Codec> extends AbstractArray<T> {
}
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<O extends Codec> (Type: Constructor<O>): Constructor<Vector<O>> {
+9 -2
View File
@@ -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 '@polkadot/util';
import { assert, isString, isU8a, u8aToU8a } from '@polkadot/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;
+10 -3
View File
@@ -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 '@polkadot/util';
import { assert, isString, stringToU8a, u8aToString, u8aToHex } from '@polkadot/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}`;
+70 -36
View File
@@ -4,30 +4,40 @@
import { Constructor } from '../types';
import { isUndefined, stringCamelCase, u8aToHex } from '@polkadot/util';
import { assert, isUndefined, stringCamelCase, u8aToHex } from '@polkadot/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<EventData> } = {};
/**
* @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<TypeDef>;
constructor (Types: Array<Constructor>, value: Uint8Array, typeDef: Array<TypeDef>, meta: EventMetadata, section: string, method: string) {
constructor (Types: Array<Constructor>, value: Uint8Array, typeDef: Array<TypeDef>, 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;
}
+2 -2
View File
@@ -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)
);
});
+48 -2
View File
@@ -6,8 +6,10 @@ import { assert } from '@polkadot/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<ApplyExtrinsic | Finalization> {
}
/**
* @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<Hash> {
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<Hash> {
return this.get('topics') as Vector<Hash>;
}
}
+10 -3
View File
@@ -5,7 +5,7 @@
import { KeyringPair } from '@polkadot/keyring/types';
import { AnyNumber, AnyU8a, ArgsDef, Codec, IExtrinsic, SignatureOptions } from '../types';
import { isHex, isU8a, u8aToHex, u8aToU8a } from '@polkadot/util';
import { assert, isHex, isU8a, u8aToHex, u8aToU8a } from '@polkadot/util';
import { blake2AsU8a } from '@polkadot/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
+2 -3
View File
@@ -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';