@pezkuwi/rpc-core 16.5.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -0
- package/build/bundle.d.ts +91 -0
- package/build/index.d.ts +2 -0
- package/build/packageDetect.d.ts +1 -0
- package/build/packageInfo.d.ts +6 -0
- package/build/types/base.d.ts +22 -0
- package/build/types/index.d.ts +2 -0
- package/build/types/jsonrpc.d.ts +2 -0
- package/build/util/drr.d.ts +15 -0
- package/build/util/index.d.ts +3 -0
- package/build/util/memo.d.ts +6 -0
- package/build/util/refCountDelay.d.ts +3 -0
- package/package.json +35 -0
- package/src/bundle.ts +535 -0
- package/src/cached.spec.ts +129 -0
- package/src/checkTypes.manual.ts +4 -0
- package/src/index.spec.ts +55 -0
- package/src/index.ts +6 -0
- package/src/methodSend.spec.ts +75 -0
- package/src/mod.ts +4 -0
- package/src/packageDetect.ts +13 -0
- package/src/packageInfo.ts +6 -0
- package/src/replay.spec.ts +73 -0
- package/src/types/base.ts +28 -0
- package/src/types/index.ts +8 -0
- package/src/types/jsonrpc.ts +7 -0
- package/src/util/drr.spec.ts +50 -0
- package/src/util/drr.ts +52 -0
- package/src/util/index.ts +6 -0
- package/src/util/memo.ts +36 -0
- package/src/util/refCountDelay.ts +45 -0
- package/tsconfig.build.json +17 -0
- package/tsconfig.build.tsbuildinfo +1 -0
- package/tsconfig.spec.json +20 -0
package/src/bundle.ts
ADDED
|
@@ -0,0 +1,535 @@
|
|
|
1
|
+
// Copyright 2017-2025 @polkadot/rpc-core authors & contributors
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
|
|
4
|
+
import type { Observer } from 'rxjs';
|
|
5
|
+
import type { ProviderInterface, ProviderInterfaceCallback } from '@pezkuwi/rpc-provider/types';
|
|
6
|
+
import type { StorageKey, Vec } from '@pezkuwi/types';
|
|
7
|
+
import type { Hash } from '@pezkuwi/types/interfaces';
|
|
8
|
+
import type { AnyJson, AnyNumber, Codec, DefinitionRpc, DefinitionRpcExt, DefinitionRpcSub, Registry } from '@pezkuwi/types/types';
|
|
9
|
+
import type { Memoized } from '@pezkuwi/util/types';
|
|
10
|
+
import type { RpcCoreStats, RpcInterfaceMethod } from './types/index.js';
|
|
11
|
+
|
|
12
|
+
import { Observable, publishReplay, refCount } from 'rxjs';
|
|
13
|
+
|
|
14
|
+
import { LRUCache } from '@pezkuwi/rpc-provider';
|
|
15
|
+
import { rpcDefinitions } from '@pezkuwi/types';
|
|
16
|
+
import { unwrapStorageSi } from '@pezkuwi/types/util';
|
|
17
|
+
import { hexToU8a, isFunction, isNull, isUndefined, lazyMethod, logger, memoize, objectSpread, u8aConcat, u8aToU8a } from '@pezkuwi/util';
|
|
18
|
+
|
|
19
|
+
import { drr, refCountDelay } from './util/index.js';
|
|
20
|
+
|
|
21
|
+
export { packageInfo } from './packageInfo.js';
|
|
22
|
+
export * from './util/index.js';
|
|
23
|
+
|
|
24
|
+
interface StorageChangeSetJSON {
|
|
25
|
+
block: string;
|
|
26
|
+
changes: [string, string | null][];
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
type MemoizedRpcInterfaceMethod = Memoized<RpcInterfaceMethod> & {
|
|
30
|
+
raw: Memoized<RpcInterfaceMethod>;
|
|
31
|
+
meta: DefinitionRpc;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
interface Options {
|
|
35
|
+
isPedantic?: boolean;
|
|
36
|
+
provider: ProviderInterface;
|
|
37
|
+
/**
|
|
38
|
+
* Custom size of the rpc LRUCache capacity. Defaults to `RPC_CORE_DEFAULT_CAPACITY` (1024 * 10 * 10)
|
|
39
|
+
*/
|
|
40
|
+
rpcCacheCapacity?: number;
|
|
41
|
+
ttl?: number | null;
|
|
42
|
+
userRpc?: Record<string, Record<string, DefinitionRpc | DefinitionRpcSub>>;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const l = logger('rpc-core');
|
|
46
|
+
|
|
47
|
+
const EMPTY_META = {
|
|
48
|
+
fallback: undefined,
|
|
49
|
+
modifier: { isOptional: true },
|
|
50
|
+
type: {
|
|
51
|
+
asMap: { linked: { isTrue: false } },
|
|
52
|
+
isMap: false
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
const RPC_CORE_DEFAULT_CAPACITY = 1024 * 10 * 10;
|
|
57
|
+
|
|
58
|
+
// utility method to create a nicely-formatted error
|
|
59
|
+
/** @internal */
|
|
60
|
+
function logErrorMessage (method: string, { noErrorLog, params, type }: DefinitionRpc, error: Error): void {
|
|
61
|
+
if (noErrorLog) {
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
l.error(`${method}(${
|
|
66
|
+
params.map(({ isOptional, name, type }): string =>
|
|
67
|
+
`${name}${isOptional ? '?' : ''}: ${type}`
|
|
68
|
+
).join(', ')
|
|
69
|
+
}): ${type}:: ${error.message}`);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function isTreatAsHex (key: StorageKey): boolean {
|
|
73
|
+
// :code is problematic - it does not have the length attached, which is
|
|
74
|
+
// unlike all other storage entries where it is indeed properly encoded
|
|
75
|
+
return ['0x3a636f6465'].includes(key.toHex());
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* @name Rpc
|
|
80
|
+
* @summary The API may use a HTTP or WebSockets provider.
|
|
81
|
+
* @description It allows for querying a Polkadot Client Node.
|
|
82
|
+
* WebSockets provider is recommended since HTTP provider only supports basic querying.
|
|
83
|
+
*
|
|
84
|
+
* ```mermaid
|
|
85
|
+
* graph LR;
|
|
86
|
+
* A[Api] --> |WebSockets| B[WsProvider];
|
|
87
|
+
* B --> |endpoint| C[ws://127.0.0.1:9944]
|
|
88
|
+
* ```
|
|
89
|
+
*
|
|
90
|
+
* @example
|
|
91
|
+
* <BR>
|
|
92
|
+
*
|
|
93
|
+
* ```javascript
|
|
94
|
+
* import Rpc from '@pezkuwi/rpc-core';
|
|
95
|
+
* import { WsProvider } from '@pezkuwi/rpc-provider/ws';
|
|
96
|
+
*
|
|
97
|
+
* const provider = new WsProvider('ws://127.0.0.1:9944');
|
|
98
|
+
* const rpc = new Rpc(provider);
|
|
99
|
+
* ```
|
|
100
|
+
*/
|
|
101
|
+
export class RpcCore {
|
|
102
|
+
readonly #instanceId: string;
|
|
103
|
+
readonly #isPedantic: boolean;
|
|
104
|
+
readonly #registryDefault: Registry;
|
|
105
|
+
readonly #storageCache: LRUCache;
|
|
106
|
+
#storageCacheHits = 0;
|
|
107
|
+
|
|
108
|
+
#getBlockRegistry?: (blockHash: Uint8Array) => Promise<{ registry: Registry }>;
|
|
109
|
+
#getBlockHash?: (blockNumber: AnyNumber) => Promise<Uint8Array>;
|
|
110
|
+
|
|
111
|
+
readonly mapping = new Map<string, DefinitionRpcExt>();
|
|
112
|
+
readonly provider: ProviderInterface;
|
|
113
|
+
readonly sections: string[] = [];
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* @constructor
|
|
117
|
+
* Default constructor for the core RPC handler
|
|
118
|
+
* @param {Registry} registry Type Registry
|
|
119
|
+
* @param {ProviderInterface} options.provider An API provider using any of the supported providers (HTTP, SC or WebSocket)
|
|
120
|
+
* @param {number} [options.rpcCacheCapacity] Custom size of the rpc LRUCache capacity. Defaults to `RPC_CORE_DEFAULT_CAPACITY` (1024 * 10 * 10)
|
|
121
|
+
*/
|
|
122
|
+
constructor (instanceId: string, registry: Registry, { isPedantic = true, provider, rpcCacheCapacity, ttl, userRpc = {} }: Options) {
|
|
123
|
+
if (!provider || !isFunction(provider.send)) {
|
|
124
|
+
throw new Error('Expected Provider to API create');
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
this.#instanceId = instanceId;
|
|
128
|
+
this.#isPedantic = isPedantic;
|
|
129
|
+
this.#registryDefault = registry;
|
|
130
|
+
this.provider = provider;
|
|
131
|
+
|
|
132
|
+
const sectionNames = Object.keys(rpcDefinitions);
|
|
133
|
+
|
|
134
|
+
// these are the base keys (i.e. part of jsonrpc)
|
|
135
|
+
this.sections.push(...sectionNames);
|
|
136
|
+
this.#storageCache = new LRUCache(rpcCacheCapacity || RPC_CORE_DEFAULT_CAPACITY, ttl);
|
|
137
|
+
// decorate all interfaces, defined and user on this instance
|
|
138
|
+
this.addUserInterfaces(userRpc);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* @description Returns the connected status of a provider
|
|
143
|
+
*/
|
|
144
|
+
public get isConnected (): boolean {
|
|
145
|
+
return this.provider.isConnected;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* @description Manually connect from the attached provider
|
|
150
|
+
*/
|
|
151
|
+
public connect (): Promise<void> {
|
|
152
|
+
return this.provider.connect();
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* @description Manually disconnect from the attached provider
|
|
157
|
+
*/
|
|
158
|
+
public async disconnect (): Promise<void> {
|
|
159
|
+
return this.provider.disconnect();
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* @description Returns the underlying core stats, including those from teh provider
|
|
164
|
+
*/
|
|
165
|
+
public get stats (): RpcCoreStats | undefined {
|
|
166
|
+
const stats = this.provider.stats;
|
|
167
|
+
|
|
168
|
+
return stats
|
|
169
|
+
? {
|
|
170
|
+
...stats,
|
|
171
|
+
core: {
|
|
172
|
+
cacheHits: this.#storageCacheHits,
|
|
173
|
+
cacheSize: this.#storageCache.length
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
: undefined;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* @description Sets a registry swap (typically from Api)
|
|
181
|
+
*/
|
|
182
|
+
public setRegistrySwap (registrySwap: (blockHash: Uint8Array) => Promise<{ registry: Registry }>): void {
|
|
183
|
+
this.#getBlockRegistry = memoize(registrySwap, {
|
|
184
|
+
getInstanceId: () => this.#instanceId
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* @description Sets a function to resolve block hash from block number
|
|
190
|
+
*/
|
|
191
|
+
public setResolveBlockHash (resolveBlockHash: (blockNumber: AnyNumber) => Promise<Uint8Array>): void {
|
|
192
|
+
this.#getBlockHash = memoize(resolveBlockHash, {
|
|
193
|
+
getInstanceId: () => this.#instanceId
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
public addUserInterfaces (userRpc: Record<string, Record<string, DefinitionRpc | DefinitionRpcSub>>): void {
|
|
198
|
+
// add any extra user-defined sections
|
|
199
|
+
this.sections.push(...Object.keys(userRpc).filter((k) => !this.sections.includes(k)));
|
|
200
|
+
|
|
201
|
+
for (let s = 0, scount = this.sections.length; s < scount; s++) {
|
|
202
|
+
const section = this.sections[s];
|
|
203
|
+
const defs = objectSpread<Record<string, DefinitionRpc | DefinitionRpcSub>>({}, rpcDefinitions[section as 'babe'], userRpc[section]);
|
|
204
|
+
const methods = Object.keys(defs);
|
|
205
|
+
|
|
206
|
+
for (let m = 0, mcount = methods.length; m < mcount; m++) {
|
|
207
|
+
const method = methods[m];
|
|
208
|
+
const def = defs[method];
|
|
209
|
+
const jsonrpc = def.endpoint || `${section}_${method}`;
|
|
210
|
+
|
|
211
|
+
if (!this.mapping.has(jsonrpc)) {
|
|
212
|
+
const isSubscription = !!(def as DefinitionRpcSub).pubsub;
|
|
213
|
+
|
|
214
|
+
if (!(this as Record<string, unknown>)[section]) {
|
|
215
|
+
(this as Record<string, unknown>)[section] = {};
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
this.mapping.set(jsonrpc, objectSpread({}, def, { isSubscription, jsonrpc, method, section }));
|
|
219
|
+
|
|
220
|
+
lazyMethod(this[section as 'connect'], method, () =>
|
|
221
|
+
isSubscription
|
|
222
|
+
? this._createMethodSubscribe(section, method, def as DefinitionRpcSub)
|
|
223
|
+
: this._createMethodSend(section, method, def)
|
|
224
|
+
);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
private _memomize (creator: <T> (isScale: boolean) => (...values: unknown[]) => Observable<T>, def: DefinitionRpc): MemoizedRpcInterfaceMethod {
|
|
231
|
+
const memoOpts = { getInstanceId: () => this.#instanceId };
|
|
232
|
+
const memoized = memoize(creator(true) as RpcInterfaceMethod, memoOpts);
|
|
233
|
+
|
|
234
|
+
memoized.raw = memoize(creator(false), memoOpts);
|
|
235
|
+
memoized.meta = def;
|
|
236
|
+
|
|
237
|
+
return memoized as MemoizedRpcInterfaceMethod;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
private _formatResult <T> (isScale: boolean, registry: Registry, blockHash: string | Uint8Array | null | undefined, method: string, def: DefinitionRpc, params: Codec[], result: unknown): T {
|
|
241
|
+
return isScale
|
|
242
|
+
? this._formatOutput(registry, blockHash, method, def, params, result) as unknown as T
|
|
243
|
+
: result as T;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
private _createMethodSend (section: string, method: string, def: DefinitionRpc): RpcInterfaceMethod {
|
|
247
|
+
const rpcName = def.endpoint || `${section}_${method}`;
|
|
248
|
+
const hashIndex = def.params.findIndex(({ isHistoric }) => isHistoric);
|
|
249
|
+
let memoized: null | MemoizedRpcInterfaceMethod = null;
|
|
250
|
+
|
|
251
|
+
// execute the RPC call, doing a registry swap for historic as applicable
|
|
252
|
+
const callWithRegistry = async <T> (isScale: boolean, values: unknown[]): Promise<T> => {
|
|
253
|
+
const blockId = hashIndex === -1
|
|
254
|
+
? null
|
|
255
|
+
: values[hashIndex];
|
|
256
|
+
|
|
257
|
+
const blockHash = blockId && def.params[hashIndex].type === 'BlockNumber'
|
|
258
|
+
? await this.#getBlockHash?.(blockId as AnyNumber)
|
|
259
|
+
: blockId as (Uint8Array | string | null | undefined);
|
|
260
|
+
|
|
261
|
+
const { registry } = isScale && blockHash && this.#getBlockRegistry
|
|
262
|
+
? await this.#getBlockRegistry(u8aToU8a(blockHash))
|
|
263
|
+
: { registry: this.#registryDefault };
|
|
264
|
+
|
|
265
|
+
const params = this._formatParams(registry, null, def, values);
|
|
266
|
+
|
|
267
|
+
// only cache .at(<blockHash>) queries, e.g. where valid blockHash was supplied
|
|
268
|
+
const result = await this.provider.send<AnyJson>(rpcName, params.map((p) => p.toJSON()), !!blockHash);
|
|
269
|
+
|
|
270
|
+
return this._formatResult(isScale, registry, blockHash, method, def, params, result);
|
|
271
|
+
};
|
|
272
|
+
|
|
273
|
+
const creator = <T> (isScale: boolean) => (...values: unknown[]): Observable<T> => {
|
|
274
|
+
const isDelayed = isScale && hashIndex !== -1 && !!values[hashIndex];
|
|
275
|
+
|
|
276
|
+
return new Observable((observer: Observer<T>): () => void => {
|
|
277
|
+
callWithRegistry<T>(isScale, values)
|
|
278
|
+
.then((value): void => {
|
|
279
|
+
observer.next(value);
|
|
280
|
+
observer.complete();
|
|
281
|
+
})
|
|
282
|
+
.catch((error: Error): void => {
|
|
283
|
+
logErrorMessage(method, def, error);
|
|
284
|
+
|
|
285
|
+
observer.error(error);
|
|
286
|
+
observer.complete();
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
return (): void => {
|
|
290
|
+
// delete old results from cache
|
|
291
|
+
if (isScale) {
|
|
292
|
+
memoized?.unmemoize(...values);
|
|
293
|
+
} else {
|
|
294
|
+
memoized?.raw.unmemoize(...values);
|
|
295
|
+
}
|
|
296
|
+
};
|
|
297
|
+
}).pipe(
|
|
298
|
+
// eslint-disable-next-line deprecation/deprecation
|
|
299
|
+
publishReplay(1), // create a Replay(1)
|
|
300
|
+
isDelayed
|
|
301
|
+
? refCountDelay() // Unsubscribe after delay
|
|
302
|
+
// eslint-disable-next-line deprecation/deprecation
|
|
303
|
+
: refCount()
|
|
304
|
+
);
|
|
305
|
+
};
|
|
306
|
+
|
|
307
|
+
memoized = this._memomize(creator, def);
|
|
308
|
+
|
|
309
|
+
return memoized;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
// create a subscriptor, it subscribes once and resolves with the id as subscribe
|
|
313
|
+
private _createSubscriber ({ paramsJson, subName, subType, update }: { subType: string; subName: string; paramsJson: AnyJson[]; update: ProviderInterfaceCallback }, errorHandler: (error: Error) => void): Promise<number | string> {
|
|
314
|
+
return new Promise((resolve, reject): void => {
|
|
315
|
+
this.provider
|
|
316
|
+
.subscribe(subType, subName, paramsJson, update)
|
|
317
|
+
.then(resolve)
|
|
318
|
+
.catch((error: Error): void => {
|
|
319
|
+
errorHandler(error);
|
|
320
|
+
reject(error);
|
|
321
|
+
});
|
|
322
|
+
});
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
private _createMethodSubscribe (section: string, method: string, def: DefinitionRpcSub): RpcInterfaceMethod {
|
|
326
|
+
const [updateType, subMethod, unsubMethod] = def.pubsub;
|
|
327
|
+
const subName = `${section}_${subMethod}`;
|
|
328
|
+
const unsubName = `${section}_${unsubMethod}`;
|
|
329
|
+
const subType = `${section}_${updateType}`;
|
|
330
|
+
let memoized: null | MemoizedRpcInterfaceMethod = null;
|
|
331
|
+
|
|
332
|
+
const creator = <T> (isScale: boolean) => (...values: unknown[]): Observable<T> => {
|
|
333
|
+
return new Observable((observer: Observer<T>): () => void => {
|
|
334
|
+
// Have at least an empty promise, as used in the unsubscribe
|
|
335
|
+
let subscriptionPromise: Promise<number | string | null> = Promise.resolve(null);
|
|
336
|
+
const registry = this.#registryDefault;
|
|
337
|
+
|
|
338
|
+
const errorHandler = (error: Error): void => {
|
|
339
|
+
logErrorMessage(method, def, error);
|
|
340
|
+
|
|
341
|
+
observer.error(error);
|
|
342
|
+
};
|
|
343
|
+
|
|
344
|
+
try {
|
|
345
|
+
const params = this._formatParams(registry, null, def, values);
|
|
346
|
+
|
|
347
|
+
const update = (error?: Error | null, result?: unknown): void => {
|
|
348
|
+
if (error) {
|
|
349
|
+
logErrorMessage(method, def, error);
|
|
350
|
+
|
|
351
|
+
return;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
try {
|
|
355
|
+
observer.next(this._formatResult(isScale, registry, null, method, def, params, result));
|
|
356
|
+
} catch (error) {
|
|
357
|
+
observer.error(error);
|
|
358
|
+
}
|
|
359
|
+
};
|
|
360
|
+
|
|
361
|
+
subscriptionPromise = this._createSubscriber({ paramsJson: params.map((p) => p.toJSON()), subName, subType, update }, errorHandler);
|
|
362
|
+
} catch (error) {
|
|
363
|
+
errorHandler(error as Error);
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
// Teardown logic
|
|
367
|
+
return (): void => {
|
|
368
|
+
// Delete from cache, so old results don't hang around
|
|
369
|
+
if (isScale) {
|
|
370
|
+
memoized?.unmemoize(...values);
|
|
371
|
+
} else {
|
|
372
|
+
memoized?.raw.unmemoize(...values);
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
// Unsubscribe from provider
|
|
376
|
+
subscriptionPromise
|
|
377
|
+
.then((subscriptionId): Promise<boolean> =>
|
|
378
|
+
isNull(subscriptionId)
|
|
379
|
+
? Promise.resolve(false)
|
|
380
|
+
: this.provider.unsubscribe(subType, unsubName, subscriptionId)
|
|
381
|
+
)
|
|
382
|
+
.catch((error: Error) => logErrorMessage(method, def, error));
|
|
383
|
+
};
|
|
384
|
+
}).pipe(drr());
|
|
385
|
+
};
|
|
386
|
+
|
|
387
|
+
memoized = this._memomize(creator, def);
|
|
388
|
+
|
|
389
|
+
return memoized;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
private _formatParams (registry: Registry, blockHash: Uint8Array | string | null | undefined, def: DefinitionRpc, inputs: unknown[]): Codec[] {
|
|
393
|
+
const count = inputs.length;
|
|
394
|
+
const reqCount = def.params.filter(({ isOptional }) => !isOptional).length;
|
|
395
|
+
|
|
396
|
+
if (count < reqCount || count > def.params.length) {
|
|
397
|
+
throw new Error(`Expected ${def.params.length} parameters${reqCount === def.params.length ? '' : ` (${def.params.length - reqCount} optional)`}, ${count} found instead`);
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
const params = new Array<Codec>(count);
|
|
401
|
+
|
|
402
|
+
for (let i = 0; i < count; i++) {
|
|
403
|
+
params[i] = registry.createTypeUnsafe(def.params[i].type, [inputs[i]], { blockHash });
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
return params;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
private _formatOutput (registry: Registry, blockHash: Uint8Array | string | null | undefined, method: string, rpc: DefinitionRpc, params: Codec[], result?: unknown): Codec | Codec[] {
|
|
410
|
+
if (rpc.type === 'StorageData') {
|
|
411
|
+
const key = params[0] as StorageKey;
|
|
412
|
+
|
|
413
|
+
return this._formatStorageData(registry, blockHash, key, result as string);
|
|
414
|
+
} else if (rpc.type === 'StorageChangeSet') {
|
|
415
|
+
const keys = params[0] as Vec<StorageKey>;
|
|
416
|
+
|
|
417
|
+
return keys
|
|
418
|
+
? this._formatStorageSet(registry, (result as StorageChangeSetJSON).block, keys, (result as StorageChangeSetJSON).changes)
|
|
419
|
+
: registry.createType('StorageChangeSet', result);
|
|
420
|
+
} else if (rpc.type === 'Vec<StorageChangeSet>') {
|
|
421
|
+
const jsonSet = (result as StorageChangeSetJSON[]);
|
|
422
|
+
const count = jsonSet.length;
|
|
423
|
+
const mapped = new Array<[Hash, Codec[]]>(count);
|
|
424
|
+
|
|
425
|
+
for (let i = 0; i < count; i++) {
|
|
426
|
+
const { block, changes } = jsonSet[i];
|
|
427
|
+
|
|
428
|
+
mapped[i] = [
|
|
429
|
+
registry.createType('BlockHash', block),
|
|
430
|
+
this._formatStorageSet(registry, block, params[0] as Vec<StorageKey>, changes)
|
|
431
|
+
];
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
// we only query at a specific block, not a range - flatten
|
|
435
|
+
return method === 'queryStorageAt'
|
|
436
|
+
? mapped[0][1]
|
|
437
|
+
: mapped as unknown as Codec[];
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
return registry.createTypeUnsafe(rpc.type, [result], { blockHash });
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
private _formatStorageData (registry: Registry, blockHash: Uint8Array | string | null | undefined, key: StorageKey, value: string | null): Codec {
|
|
444
|
+
const isEmpty = isNull(value);
|
|
445
|
+
|
|
446
|
+
// we convert to Uint8Array since it maps to the raw encoding, all
|
|
447
|
+
// data will be correctly encoded (incl. numbers, excl. :code)
|
|
448
|
+
const input = isEmpty
|
|
449
|
+
? null
|
|
450
|
+
: isTreatAsHex(key)
|
|
451
|
+
? value
|
|
452
|
+
: u8aToU8a(value);
|
|
453
|
+
|
|
454
|
+
return this._newType(registry, blockHash, key, input, isEmpty);
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
private _formatStorageSet (registry: Registry, blockHash: string, keys: Vec<StorageKey>, changes: [string, string | null][]): Codec[] {
|
|
458
|
+
// For StorageChangeSet, the changes has the [key, value] mappings
|
|
459
|
+
const count = keys.length;
|
|
460
|
+
const withCache = count !== 1;
|
|
461
|
+
const values = new Array<Codec>(count);
|
|
462
|
+
|
|
463
|
+
// multiple return values (via state.storage subscription), decode the
|
|
464
|
+
// values one at a time, all based on the supplied query types
|
|
465
|
+
for (let i = 0; i < count; i++) {
|
|
466
|
+
values[i] = this._formatStorageSetEntry(registry, blockHash, keys[i], changes, withCache, i);
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
return values;
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
private _formatStorageSetEntry (registry: Registry, blockHash: string, key: StorageKey, changes: [string, string | null][], withCache: boolean, entryIndex: number): Codec {
|
|
473
|
+
const hexKey = key.toHex();
|
|
474
|
+
const found = changes.find(([key]) => key === hexKey);
|
|
475
|
+
const isNotFound = isUndefined(found);
|
|
476
|
+
|
|
477
|
+
// if we don't find the value, this is our fallback
|
|
478
|
+
// - in the case of an array of values, fill the hole from the cache
|
|
479
|
+
// - if a single result value, don't fill - it is not an update hole
|
|
480
|
+
// - fallback to an empty option in all cases
|
|
481
|
+
if (isNotFound && withCache) {
|
|
482
|
+
const cached = this.#storageCache.get(hexKey) as Codec | undefined;
|
|
483
|
+
|
|
484
|
+
if (cached) {
|
|
485
|
+
this.#storageCacheHits++;
|
|
486
|
+
|
|
487
|
+
return cached;
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
const value = isNotFound
|
|
492
|
+
? null
|
|
493
|
+
: found[1];
|
|
494
|
+
const isEmpty = isNull(value);
|
|
495
|
+
const input = isEmpty || isTreatAsHex(key)
|
|
496
|
+
? value
|
|
497
|
+
: u8aToU8a(value);
|
|
498
|
+
const codec = this._newType(registry, blockHash, key, input, isEmpty, entryIndex);
|
|
499
|
+
|
|
500
|
+
this._setToCache(hexKey, codec);
|
|
501
|
+
|
|
502
|
+
return codec;
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
private _setToCache (key: string, value: Codec): void {
|
|
506
|
+
this.#storageCache.set(key, value);
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
private _newType (registry: Registry, blockHash: Uint8Array | string | null | undefined, key: StorageKey, input: string | Uint8Array | null, isEmpty: boolean, entryIndex = -1): Codec {
|
|
510
|
+
// single return value (via state.getStorage), decode the value based on the
|
|
511
|
+
// outputType that we have specified. Fallback to Raw on nothing
|
|
512
|
+
const type = key.meta ? registry.createLookupType(unwrapStorageSi(key.meta.type)) : (key.outputType || 'Raw');
|
|
513
|
+
const meta = key.meta || EMPTY_META;
|
|
514
|
+
const entryNum = entryIndex === -1
|
|
515
|
+
? ''
|
|
516
|
+
: ` entry ${entryIndex}:`;
|
|
517
|
+
|
|
518
|
+
try {
|
|
519
|
+
return registry.createTypeUnsafe(type, [
|
|
520
|
+
isEmpty
|
|
521
|
+
? meta.fallback
|
|
522
|
+
// For old-style Linkage, we add an empty linkage at the end
|
|
523
|
+
? type.includes('Linkage<')
|
|
524
|
+
? u8aConcat(hexToU8a(meta.fallback.toHex()), new Uint8Array(2))
|
|
525
|
+
: hexToU8a(meta.fallback.toHex())
|
|
526
|
+
: undefined
|
|
527
|
+
: meta.modifier.isOptional
|
|
528
|
+
? registry.createTypeUnsafe(type, [input], { blockHash, isPedantic: this.#isPedantic })
|
|
529
|
+
: input
|
|
530
|
+
], { blockHash, isFallback: isEmpty && !!meta.fallback, isOptional: meta.modifier.isOptional, isPedantic: this.#isPedantic && !meta.modifier.isOptional });
|
|
531
|
+
} catch (error) {
|
|
532
|
+
throw new Error(`Unable to decode storage ${key.section || 'unknown'}.${key.method || 'unknown'}:${entryNum}: ${(error as Error).message}`);
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
}
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
// Copyright 2017-2025 @polkadot/rpc-core authors & contributors
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
|
|
4
|
+
/// <reference types="@pezkuwi/dev-test/globals.d.ts" />
|
|
5
|
+
|
|
6
|
+
import type { RpcInterface } from './types/index.js';
|
|
7
|
+
|
|
8
|
+
import { createTestPairs } from '@pezkuwi/keyring/testingPairs';
|
|
9
|
+
import { MockProvider } from '@pezkuwi/rpc-provider/mock';
|
|
10
|
+
import { TypeRegistry } from '@pezkuwi/types/create';
|
|
11
|
+
|
|
12
|
+
import { RpcCore } from './index.js';
|
|
13
|
+
|
|
14
|
+
describe('Cached Observables', (): void => {
|
|
15
|
+
const registry = new TypeRegistry();
|
|
16
|
+
let rpc: RpcCore & RpcInterface;
|
|
17
|
+
let provider: MockProvider;
|
|
18
|
+
const keyring = createTestPairs();
|
|
19
|
+
|
|
20
|
+
beforeEach((): void => {
|
|
21
|
+
provider = new MockProvider(registry);
|
|
22
|
+
rpc = new RpcCore('123', registry, { provider }) as (RpcCore & RpcInterface);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
afterEach(async () => {
|
|
26
|
+
await provider.disconnect();
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it('creates a single observable for subscriptions (multiple calls)', (): void => {
|
|
30
|
+
const observable1 = rpc.state.subscribeStorage([123]);
|
|
31
|
+
const observable2 = rpc.state.subscribeStorage([123]);
|
|
32
|
+
|
|
33
|
+
expect(observable2).toBe(observable1);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it('creates a single observable for subscriptions (multiple calls, no arguments)', (): void => {
|
|
37
|
+
const observable1 = rpc.chain.subscribeNewHeads();
|
|
38
|
+
const observable2 = rpc.chain.subscribeNewHeads();
|
|
39
|
+
|
|
40
|
+
expect(observable2).toBe(observable1);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('creates a single observable (multiple calls, different arguments that should be cached together)', (): void => {
|
|
44
|
+
const observable1 = rpc.state.subscribeStorage([keyring.alice.address]);
|
|
45
|
+
const observable2 = rpc.state.subscribeStorage([registry.createType('AccountId', keyring.alice.address)]);
|
|
46
|
+
|
|
47
|
+
expect(observable2).toBe(observable1);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('creates multiple observables for different values', (): void => {
|
|
51
|
+
const observable1 = rpc.chain.getBlockHash(123);
|
|
52
|
+
const observable2 = rpc.chain.getBlockHash(456);
|
|
53
|
+
|
|
54
|
+
expect(observable2).not.toBe(observable1);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it('subscribes to the same one if within the period (unbsub delay)', async (): Promise<void> => {
|
|
58
|
+
const observable1 = rpc.chain.subscribeNewHeads();
|
|
59
|
+
const sub1 = observable1.subscribe();
|
|
60
|
+
|
|
61
|
+
sub1.unsubscribe();
|
|
62
|
+
|
|
63
|
+
await new Promise<boolean>((resolve) => {
|
|
64
|
+
setTimeout((): void => {
|
|
65
|
+
const observable2 = rpc.chain.subscribeNewHeads();
|
|
66
|
+
const sub2 = observable2.subscribe();
|
|
67
|
+
|
|
68
|
+
expect(observable1).toBe(observable2);
|
|
69
|
+
|
|
70
|
+
sub2.unsubscribe();
|
|
71
|
+
resolve(true);
|
|
72
|
+
}, 500);
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it('clears cache if there are no more subscribers', async (): Promise<void> => {
|
|
77
|
+
const observable1 = rpc.chain.subscribeNewHeads();
|
|
78
|
+
const observable2 = rpc.chain.subscribeNewHeads();
|
|
79
|
+
const sub1 = observable1.subscribe();
|
|
80
|
+
const sub2 = observable2.subscribe();
|
|
81
|
+
|
|
82
|
+
expect(observable1).toBe(observable2);
|
|
83
|
+
|
|
84
|
+
sub1.unsubscribe();
|
|
85
|
+
sub2.unsubscribe();
|
|
86
|
+
|
|
87
|
+
await new Promise<boolean>((resolve) => {
|
|
88
|
+
setTimeout((): void => {
|
|
89
|
+
// No more subscribers, now create a new observable
|
|
90
|
+
const observable3 = rpc.chain.subscribeNewHeads();
|
|
91
|
+
|
|
92
|
+
expect(observable3).not.toBe(observable1);
|
|
93
|
+
resolve(true);
|
|
94
|
+
}, 3500);
|
|
95
|
+
});
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it('creates different observables for different methods but same arguments', (): void => {
|
|
99
|
+
// params do not match here
|
|
100
|
+
const observable1 = rpc.chain.getHeader('123');
|
|
101
|
+
const observable2 = rpc.chain.getBlockHash('123');
|
|
102
|
+
|
|
103
|
+
expect(observable2).not.toBe(observable1);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it('creates single observables for subsequent one-shots', (): void => {
|
|
107
|
+
const observable1 = rpc.chain.getBlockHash(123);
|
|
108
|
+
const observable2 = rpc.chain.getBlockHash(123);
|
|
109
|
+
|
|
110
|
+
expect(observable2).toBe(observable1);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it('creates multiple observables for subsequent one-shots delayed', async (): Promise<void> => {
|
|
114
|
+
const observable1 = rpc.chain.getBlockHash(123);
|
|
115
|
+
|
|
116
|
+
const sub = observable1.subscribe((): void => {
|
|
117
|
+
sub.unsubscribe();
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
expect(rpc.chain.getBlockHash(123)).toBe(observable1);
|
|
121
|
+
|
|
122
|
+
await new Promise<boolean>((resolve) => {
|
|
123
|
+
setTimeout((): void => {
|
|
124
|
+
expect(rpc.chain.getBlockHash(123)).not.toBe(observable1);
|
|
125
|
+
resolve(true);
|
|
126
|
+
}, 3500);
|
|
127
|
+
});
|
|
128
|
+
});
|
|
129
|
+
});
|