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)
This commit is contained in:
Jaco Greeff
2019-07-23 10:56:45 +02:00
committed by GitHub
parent 6858cd0212
commit f9a3f3e53f
4 changed files with 105 additions and 34 deletions
@@ -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<Type> 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;
}
+59 -10
View File
@@ -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();
});
});
});
}
+11 -7
View File
@@ -173,7 +173,8 @@ export default class Rpc implements RpcInterface {
const call = (...values: any[]): Observable<any> => {
return new Observable((observer: Observer<any>): VoidCallback => {
let subscriptionPromise: Promise<number | void>;
// Have at least an empty promise, as used in the unsubscribe
let subscriptionPromise: Promise<number | void> = 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<boolean> =>
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),
+8 -3
View File
@@ -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<StorageKeyExtra> = {}) {
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 {