From 4ec42f17509e59bda3c3caa792ed96ba7b790a43 Mon Sep 17 00:00:00 2001 From: Jaco Greeff Date: Mon, 31 Aug 2020 08:59:20 +0200 Subject: [PATCH] Ensure non-destructive registry swap (#2543) * Ensure non-destructive registry swap * Update packages/api/src/base/Init.ts --- CHANGELOG.md | 2 +- packages/api-derive/src/chain/getHeader.ts | 2 +- packages/api/src/base/Decorate.ts | 8 -- packages/api/src/base/Init.ts | 36 +++---- packages/rpc-core/src/index.ts | 110 +++++++++------------ 5 files changed, 62 insertions(+), 96 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b48fa2447c..3b6d10a1c5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,7 +13,7 @@ Upgrade priority: Low, unless using the new `RewardDestination` or there is a ne - Add `toBigInt()` (JS built-in `BigInt`) on `Int/Uint`, & `Compact<*>` types - Support for `.concat` on Codec arrays - Support for serialization with `isBare` flag on enums -- Add `api.swapRegistry(blockHash?)` to manually swap to types for a specific block (no param swaps to default) +- Add `api.getBlockRegistry(blockHash?)` to create registry/types for a specific block - `derive.democracy.locks` now returns delegated locks for an account as well - Adjust unlocking derives with appropriate in-place additions (less object allocations) - `.sign` on submittables is marked deprecated (not due for removal, but rather use `.signAsync` for consistency with `.signAndSend`) diff --git a/packages/api-derive/src/chain/getHeader.ts b/packages/api-derive/src/chain/getHeader.ts index 7f11cb2779..7ba2997efc 100644 --- a/packages/api-derive/src/chain/getHeader.ts +++ b/packages/api-derive/src/chain/getHeader.ts @@ -11,7 +11,7 @@ import { HeaderExtended } from '../type'; import { memo } from '../util'; /** - * @name bestNumberFinalized + * @name getHeader * @param {( Uint8Array | string )} hash - A block hash as U8 array or string. * @returns An array containing the block header and the block author * @description Get a specific block header and extend it with the author diff --git a/packages/api/src/base/Decorate.ts b/packages/api/src/base/Decorate.ts index 9daf102527..8f427b758a 100644 --- a/packages/api/src/base/Decorate.ts +++ b/packages/api/src/base/Decorate.ts @@ -190,14 +190,6 @@ export default abstract class Decorate extends Events augmentObject('', decoratedMeta.consts, this._rx.consts, fromEmpty); } - public setRegistry (registry: Registry): Registry { - this.#registry = registry; - - this._rpcCore.setRegistry(registry); - - return this.#registry; - } - private _decorateFunctionMeta (input: MetaDecoration, output: MetaDecoration): MetaDecoration { output.meta = input.meta; output.method = input.method; diff --git a/packages/api/src/base/Init.ts b/packages/api/src/base/Init.ts index ba2ac451cf..260c5a33c6 100644 --- a/packages/api/src/base/Init.ts +++ b/packages/api/src/base/Init.ts @@ -32,8 +32,6 @@ export default abstract class Init extends Decorate) { @@ -43,10 +41,8 @@ export default abstract class Init extends Decorate extends Decorate this.swapRegistry(hash)); + this._rpcCore.setRegistrySwap((hash?: string | Uint8Array | null) => this.getBlockRegistry(hash)); this._rpcCore.provider.on('disconnected', this.#onProviderDisconnect); this._rpcCore.provider.on('error', this.#onProviderError); this._rpcCore.provider.on('connected', this.#onProviderConnect); @@ -77,16 +73,12 @@ export default abstract class Init extends Decorate { + public async getBlockRegistry (blockHash?: string | Uint8Array | null): Promise { if (!blockHash) { - return this.setRegistry(this.#registryDefault); + return this.registry; } // shortcut in the case where we have an immediate-same request @@ -94,7 +86,7 @@ export default abstract class Init extends Decorate r.lastBlockHash && u8aEq(lastBlockHash, r.lastBlockHash)); if (existingViaHash) { - return this.setRegistry(existingViaHash.registry); + return existingViaHash.registry; } // We have to assume that on the RPC layer the calls used here does not call back into @@ -111,17 +103,17 @@ export default abstract class Init extends Decorate extends Decorate extends Decorate Promise; + #getBlockRegistry?: (blockHash?: string | Uint8Array | null) => Promise; readonly #storageCache = new Map(); @@ -115,7 +115,7 @@ export default class Rpc implements RpcInterface { // eslint-disable-next-line @typescript-eslint/unbound-method assert(provider && isFunction(provider.send), 'Expected Provider to API create'); - this.#registry = registry; + this.#registryDefault = registry; this.provider = provider; const sectionNames = Object.keys(jsonrpc); @@ -133,18 +133,11 @@ export default class Rpc implements RpcInterface { this.provider.disconnect(); } - /** - * @description Set the registry to use for this RPC - */ - public setRegistry (registry: Registry): void { - this.#registry = registry; - } - /** * @description Sets a registry swap (typically from Api) */ public setRegistrySwap (registrySwap: (blockHash?: string | Uint8Array | null) => Promise): void { - this.#registrySwap = registrySwap; + this.#getBlockRegistry = registrySwap; } public addUserInterfaces
(userRpc: Record>): void { @@ -211,36 +204,19 @@ export default class Rpc implements RpcInterface { const hashIndex = def.params.findIndex(({ isHistoric }) => isHistoric); // execute the RPC call, doing a registry swap for historic as applicable - const callWithSwap = async (isRaw: boolean, values: any[]): Promise => { + const callWithRegistry = async (isRaw: boolean, values: any[]): Promise => { const hash = hashIndex === -1 ? undefined : values[hashIndex] as Uint8Array; + const registry = hash && this.#getBlockRegistry + ? await this.#getBlockRegistry(hash) + : this.#registryDefault; + const params = this._formatInputs(registry, def, values); + const data = await this.provider.send(rpcName, params.map((param): AnyJson => param.toJSON())) as AnyJson; - try { - if (hash && this.#registrySwap) { - await this.#registrySwap(hash); - } - - const params = this._formatInputs(def, values); - const data = await this.provider.send(rpcName, params.map((param): AnyJson => param.toJSON())) as AnyJson; - const result = isRaw - ? this.#registry.createType('Raw', data) - : this._formatOutput(method, def, params, data); - - if (hash && this.#registrySwap) { - await this.#registrySwap(); - } - - return result; - } catch (error) { - logErrorMessage(method, def, error); - - if (hash && this.#registrySwap) { - await this.#registrySwap(); - } - - throw error; - } + return isRaw + ? registry.createType('Raw', data) + : this._formatOutput(registry, method, def, params, data); }; const creator = (isRaw: boolean) => (...values: any[]): Observable => { @@ -251,7 +227,12 @@ export default class Rpc implements RpcInterface { // - first do `of(1)` - won't throw // - then do `map(()=>this.formatInputs)` - might throw, but inside Observable. return of(1).pipe( - switchMap(() => from(callWithSwap(isRaw, values))), + switchMap(() => from(callWithRegistry(isRaw, values))), + catchError((error): any => { + logErrorMessage(method, def, error); + + return throwError(error); + }), publishReplay(1), // create a Replay(1) refCount() // Unsubscribe WS when there are no more subscribers ); @@ -287,6 +268,7 @@ export default class Rpc implements RpcInterface { return new Observable((observer: Observer): VoidCallback => { // Have at least an empty promise, as used in the unsubscribe let subscriptionPromise: Promise = Promise.resolve(null); + const registry = this.#registryDefault; const errorHandler = (error: Error): void => { logErrorMessage(method, def, error); @@ -295,7 +277,7 @@ export default class Rpc implements RpcInterface { }; try { - const params = this._formatInputs(def, values); + const params = this._formatInputs(registry, def, values); const paramsJson = params.map((param): AnyJson => param.toJSON()); const update = (error?: Error | null, result?: any): void => { @@ -308,8 +290,8 @@ export default class Rpc implements RpcInterface { try { observer.next( isRaw - ? this.#registry.createType('Raw', result) - : this._formatOutput(method, def, params, result) + ? registry.createType('Raw', result) + : this._formatOutput(registry, method, def, params, result) ); } catch (error) { observer.error(error); @@ -359,7 +341,7 @@ export default class Rpc implements RpcInterface { return memoized; } - private _formatInputs (def: DefinitionRpc, inputs: any[]): Codec[] { + private _formatInputs (registry: Registry, def: DefinitionRpc, inputs: any[]): Codec[] { const reqArgCount = def.params.filter(({ isOptional }): boolean => !isOptional).length; const optText = reqArgCount === def.params.length ? '' @@ -368,7 +350,7 @@ export default class Rpc implements RpcInterface { assert(inputs.length >= reqArgCount && inputs.length <= def.params.length, `Expected ${def.params.length} parameters${optText}, ${inputs.length} found instead`); return inputs.map((input, index): Codec => - createTypeUnsafe(this.#registry, def.params[index].type, [input]) + createTypeUnsafe(registry, def.params[index].type, [input]) ); } @@ -378,12 +360,12 @@ export default class Rpc implements RpcInterface { return ['0x3a636f6465'].includes(key.toHex()); } - private _formatOutput (method: string, rpc: DefinitionRpc, params: Codec[], result?: any): Codec | Codec[] { + private _formatOutput (registry: Registry, method: string, rpc: DefinitionRpc, params: Codec[], result?: any): Codec | Codec[] { if (rpc.type === 'StorageData') { const key = params[0] as StorageKey; try { - return this._formatStorageData(key, result); + return this._formatStorageData(registry, key, result); } catch (error) { console.error(`Unable to decode storage ${key.section || 'unknown'}.${key.method || 'unknown'}:`, (error as Error).message); @@ -393,12 +375,12 @@ export default class Rpc implements RpcInterface { const keys = params[0] as Vec; return keys - ? this._formatStorageSet(keys, (result as StorageChangeSetJSON).changes) - : this.#registry.createType('StorageChangeSet', result); + ? this._formatStorageSet(registry, keys, (result as StorageChangeSetJSON).changes) + : registry.createType('StorageChangeSet', result); } else if (rpc.type === 'Vec') { const mapped = (result as StorageChangeSetJSON[]).map(({ block, changes }): [Hash, Codec[]] => [ - this.#registry.createType('Hash', block), - this._formatStorageSet(params[0] as Vec, changes) + registry.createType('Hash', block), + this._formatStorageSet(registry, params[0] as Vec, changes) ]); // we only query at a specific block, not a range - flatten @@ -407,10 +389,10 @@ export default class Rpc implements RpcInterface { : mapped as unknown as Codec[]; } - return createTypeUnsafe(this.#registry, rpc.type, [result]); + return createTypeUnsafe(registry, rpc.type, [result]); } - private _formatStorageData (key: StorageKey, value: string | null): Codec { + private _formatStorageData (registry: Registry, key: StorageKey, value: string | null): Codec { // single return value (via state.getStorage), decode the value based on the // outputType that we have specified. Fallback to Raw on nothing const type = key.outputType || 'Raw'; @@ -427,15 +409,15 @@ export default class Rpc implements RpcInterface { if (meta.modifier.isOptional) { return new Option( - this.#registry, - createClass(this.#registry, type), + registry, + createClass(registry, type), isEmpty ? null - : createTypeUnsafe(this.#registry, type, [input], true) + : createTypeUnsafe(registry, type, [input], true) ); } - return createTypeUnsafe(this.#registry, type, [ + return createTypeUnsafe(registry, type, [ isEmpty ? meta.fallback ? hexToU8a(meta.fallback.toHex()) @@ -444,7 +426,7 @@ export default class Rpc implements RpcInterface { ], true); } - private _formatStorageSet (keys: Vec, changes: [string, string | null][]): Codec[] { + private _formatStorageSet (registry: Registry, keys: Vec, changes: [string, string | null][]): Codec[] { // For StorageChangeSet, the changes has the [key, value] mappings const withCache = keys.length !== 1; @@ -454,7 +436,7 @@ export default class Rpc implements RpcInterface { // - null - The storage key is empty return keys.reduce((results: Codec[], key: StorageKey): Codec[] => { try { - results.push(this._formatStorageSetEntry(key, changes, withCache)); + results.push(this._formatStorageSetEntry(registry, key, changes, withCache)); } catch (error) { console.error(`Unable to decode storage ${key.section || 'unknown'}.${key.method || 'unknown'}:`, (error as Error).message); @@ -465,7 +447,7 @@ export default class Rpc implements RpcInterface { }, []); } - private _formatStorageSetEntry (key: StorageKey, changes: [string, string | null][], witCache: boolean): Codec { + private _formatStorageSetEntry (registry: Registry, key: StorageKey, changes: [string, string | null][], witCache: boolean): Codec { // Fallback to Raw (i.e. just the encoding) if we don't have a specific type const type = key.outputType || 'Raw'; const hexKey = key.toHex(); @@ -491,15 +473,15 @@ export default class Rpc implements RpcInterface { if (meta.modifier.isOptional) { return new Option( - this.#registry, - createClass(this.#registry, type), + registry, + createClass(registry, type), isEmpty ? null - : createTypeUnsafe(this.#registry, type, [input], true) + : createTypeUnsafe(registry, type, [input], true) ); } - return createTypeUnsafe(this.#registry, type, [ + return createTypeUnsafe(registry, type, [ isEmpty ? meta.fallback ? hexToU8a(meta.fallback.toHex())