@pezkuwi/rpc-provider 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 +68 -0
- package/build/bundle.d.ts +5 -0
- package/build/coder/error.d.ts +29 -0
- package/build/coder/index.d.ts +8 -0
- package/build/defaults.d.ts +5 -0
- package/build/http/index.d.ts +81 -0
- package/build/http/types.d.ts +7 -0
- package/build/index.d.ts +2 -0
- package/build/lru.d.ts +15 -0
- package/build/mock/index.d.ts +35 -0
- package/build/mock/mockHttp.d.ts +9 -0
- package/build/mock/mockWs.d.ts +26 -0
- package/build/mock/types.d.ts +23 -0
- package/build/packageDetect.d.ts +1 -0
- package/build/packageInfo.d.ts +6 -0
- package/build/substrate-connect/Health.d.ts +7 -0
- package/build/substrate-connect/index.d.ts +22 -0
- package/build/substrate-connect/types.d.ts +12 -0
- package/build/types.d.ts +85 -0
- package/build/ws/errors.d.ts +1 -0
- package/build/ws/index.d.ts +121 -0
- package/package.json +43 -0
- package/src/bundle.ts +8 -0
- package/src/coder/decodeResponse.spec.ts +70 -0
- package/src/coder/encodeJson.spec.ts +20 -0
- package/src/coder/encodeObject.spec.ts +25 -0
- package/src/coder/error.spec.ts +111 -0
- package/src/coder/error.ts +66 -0
- package/src/coder/index.ts +88 -0
- package/src/defaults.ts +10 -0
- package/src/http/index.spec.ts +72 -0
- package/src/http/index.ts +238 -0
- package/src/http/send.spec.ts +61 -0
- package/src/http/types.ts +11 -0
- package/src/index.ts +6 -0
- package/src/lru.spec.ts +74 -0
- package/src/lru.ts +197 -0
- package/src/mock/index.ts +259 -0
- package/src/mock/mockHttp.ts +35 -0
- package/src/mock/mockWs.ts +92 -0
- package/src/mock/on.spec.ts +43 -0
- package/src/mock/send.spec.ts +38 -0
- package/src/mock/subscribe.spec.ts +81 -0
- package/src/mock/types.ts +36 -0
- package/src/mock/unsubscribe.spec.ts +57 -0
- package/src/mod.ts +4 -0
- package/src/packageDetect.ts +12 -0
- package/src/packageInfo.ts +6 -0
- package/src/substrate-connect/Health.ts +325 -0
- package/src/substrate-connect/index.spec.ts +638 -0
- package/src/substrate-connect/index.ts +415 -0
- package/src/substrate-connect/types.ts +16 -0
- package/src/types.ts +101 -0
- package/src/ws/connect.spec.ts +167 -0
- package/src/ws/errors.ts +41 -0
- package/src/ws/index.spec.ts +97 -0
- package/src/ws/index.ts +652 -0
- package/src/ws/send.spec.ts +126 -0
- package/src/ws/state.spec.ts +20 -0
- package/src/ws/subscribe.spec.ts +68 -0
- package/src/ws/unsubscribe.spec.ts +100 -0
- package/tsconfig.build.json +17 -0
- package/tsconfig.build.tsbuildinfo +1 -0
- package/tsconfig.spec.json +18 -0
|
@@ -0,0 +1,415 @@
|
|
|
1
|
+
// Copyright 2017-2025 @polkadot/rpc-provider authors & contributors
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
|
|
4
|
+
import type * as ScType from '@substrate/connect';
|
|
5
|
+
import type { JsonRpcResponse, ProviderInterface, ProviderInterfaceCallback, ProviderInterfaceEmitCb, ProviderInterfaceEmitted } from '../types.js';
|
|
6
|
+
|
|
7
|
+
import { EventEmitter } from 'eventemitter3';
|
|
8
|
+
|
|
9
|
+
import { isError, isFunction, isObject, logger, noop, objectSpread } from '@pezkuwi/util';
|
|
10
|
+
|
|
11
|
+
import { RpcCoder } from '../coder/index.js';
|
|
12
|
+
import { healthChecker } from './Health.js';
|
|
13
|
+
|
|
14
|
+
type ResponseCallback = (response: string | Error) => void;
|
|
15
|
+
|
|
16
|
+
// We define the interface with items we use - this means that we don't really
|
|
17
|
+
// need to be passed a full `import * as Sc from '@ubstrate/connect'`, but can
|
|
18
|
+
// also make do with a { WellKnownChain, createScClient } interface
|
|
19
|
+
interface SubstrateConnect {
|
|
20
|
+
WellKnownChain: typeof ScType['WellKnownChain'];
|
|
21
|
+
createScClient: typeof ScType['createScClient'];
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const l = logger('api-substrate-connect');
|
|
25
|
+
|
|
26
|
+
// These methods have been taken from:
|
|
27
|
+
// https://github.com/paritytech/smoldot/blob/17425040ddda47d539556eeaf62b88c4240d1d42/src/json_rpc/methods.rs#L338-L462
|
|
28
|
+
// It's important to take into account that smoldot is adding support to the new
|
|
29
|
+
// json-rpc-interface https://paritytech.github.io/json-rpc-interface-spec/
|
|
30
|
+
// However, at the moment this list only includes methods that belong to the "old" API
|
|
31
|
+
const subscriptionUnsubscriptionMethods = new Map<string, string>([
|
|
32
|
+
['author_submitAndWatchExtrinsic', 'author_unwatchExtrinsic'],
|
|
33
|
+
['chain_subscribeAllHeads', 'chain_unsubscribeAllHeads'],
|
|
34
|
+
['chain_subscribeFinalizedHeads', 'chain_unsubscribeFinalizedHeads'],
|
|
35
|
+
['chain_subscribeFinalisedHeads', 'chain_subscribeFinalisedHeads'],
|
|
36
|
+
['chain_subscribeNewHeads', 'chain_unsubscribeNewHeads'],
|
|
37
|
+
['chain_subscribeNewHead', 'chain_unsubscribeNewHead'],
|
|
38
|
+
['chain_subscribeRuntimeVersion', 'chain_unsubscribeRuntimeVersion'],
|
|
39
|
+
['subscribe_newHead', 'unsubscribe_newHead'],
|
|
40
|
+
['state_subscribeRuntimeVersion', 'state_unsubscribeRuntimeVersion'],
|
|
41
|
+
['state_subscribeStorage', 'state_unsubscribeStorage']
|
|
42
|
+
]);
|
|
43
|
+
|
|
44
|
+
const scClients = new WeakMap<ScProvider, ScType.ScClient>();
|
|
45
|
+
|
|
46
|
+
interface ActiveSubs {
|
|
47
|
+
type: string,
|
|
48
|
+
method: string,
|
|
49
|
+
params: any[],
|
|
50
|
+
callback: ProviderInterfaceCallback
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export class ScProvider implements ProviderInterface {
|
|
54
|
+
readonly #Sc: SubstrateConnect;
|
|
55
|
+
readonly #coder: RpcCoder = new RpcCoder();
|
|
56
|
+
readonly #spec: string | ScType.WellKnownChain;
|
|
57
|
+
readonly #sharedSandbox?: ScProvider | undefined;
|
|
58
|
+
readonly #subscriptions = new Map<string, [ResponseCallback, { unsubscribeMethod: string; id: string | number }]>();
|
|
59
|
+
readonly #resubscribeMethods = new Map<string, ActiveSubs>();
|
|
60
|
+
readonly #requests = new Map<number, ResponseCallback>();
|
|
61
|
+
readonly #wellKnownChains: Set<ScType.WellKnownChain>;
|
|
62
|
+
readonly #eventemitter: EventEmitter = new EventEmitter();
|
|
63
|
+
|
|
64
|
+
#chain: Promise<ScType.Chain> | null = null;
|
|
65
|
+
#isChainReady = false;
|
|
66
|
+
|
|
67
|
+
public constructor (Sc: SubstrateConnect, spec: string | ScType.WellKnownChain, sharedSandbox?: ScProvider) {
|
|
68
|
+
if (!isObject(Sc) || !isObject(Sc.WellKnownChain) || !isFunction(Sc.createScClient)) {
|
|
69
|
+
throw new Error('Expected an @substrate/connect interface as first parameter to ScProvider');
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
this.#Sc = Sc;
|
|
73
|
+
this.#spec = spec;
|
|
74
|
+
this.#sharedSandbox = sharedSandbox;
|
|
75
|
+
this.#wellKnownChains = new Set(Object.values(Sc.WellKnownChain));
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
public get hasSubscriptions (): boolean {
|
|
79
|
+
// Indicates that subscriptions are supported
|
|
80
|
+
return !!true;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
public get isClonable (): boolean {
|
|
84
|
+
return !!false;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
public get isConnected (): boolean {
|
|
88
|
+
return !!this.#chain && this.#isChainReady;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
public clone (): ProviderInterface {
|
|
92
|
+
throw new Error('clone() is not supported.');
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Config details can be found in @substrate/connect repo following the link:
|
|
96
|
+
// https://github.com/paritytech/substrate-connect/blob/main/packages/connect/src/connector/index.ts
|
|
97
|
+
async connect (config?: ScType.Config, checkerFactory = healthChecker): Promise<void> {
|
|
98
|
+
if (this.isConnected) {
|
|
99
|
+
throw new Error('Already connected!');
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// it could happen that after emitting `disconnected` due to the fact that
|
|
103
|
+
// smoldot is syncing, the consumer tries to reconnect after a certain amount
|
|
104
|
+
// of time... In which case we want to make sure that we don't create a new
|
|
105
|
+
// chain.
|
|
106
|
+
if (this.#chain) {
|
|
107
|
+
await this.#chain;
|
|
108
|
+
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (this.#sharedSandbox && !this.#sharedSandbox.isConnected) {
|
|
113
|
+
await this.#sharedSandbox.connect();
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const client = this.#sharedSandbox
|
|
117
|
+
? scClients.get(this.#sharedSandbox)
|
|
118
|
+
: this.#Sc.createScClient(config);
|
|
119
|
+
|
|
120
|
+
if (!client) {
|
|
121
|
+
throw new Error('Unknown ScProvider!');
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
scClients.set(this, client);
|
|
125
|
+
|
|
126
|
+
const hc = checkerFactory();
|
|
127
|
+
|
|
128
|
+
const onResponse = (res: string): void => {
|
|
129
|
+
const hcRes = hc.responsePassThrough(res);
|
|
130
|
+
|
|
131
|
+
if (!hcRes) {
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const response = JSON.parse(hcRes) as JsonRpcResponse<string>;
|
|
136
|
+
let decodedResponse: string | Error;
|
|
137
|
+
|
|
138
|
+
try {
|
|
139
|
+
decodedResponse = this.#coder.decodeResponse(response);
|
|
140
|
+
} catch (e) {
|
|
141
|
+
decodedResponse = e as Error;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// It's not a subscription message, but rather a standar RPC response
|
|
145
|
+
if (response.params?.subscription === undefined || !response.method) {
|
|
146
|
+
return this.#requests.get(response.id)?.(decodedResponse);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// We are dealing with a subscription message
|
|
150
|
+
const subscriptionId = `${response.method}::${response.params.subscription}`;
|
|
151
|
+
|
|
152
|
+
const callback = this.#subscriptions.get(subscriptionId)?.[0];
|
|
153
|
+
|
|
154
|
+
callback?.(decodedResponse);
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
const addChain = this.#sharedSandbox
|
|
158
|
+
? (async (...args) => {
|
|
159
|
+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
160
|
+
const source = this.#sharedSandbox!;
|
|
161
|
+
|
|
162
|
+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
163
|
+
return (await source.#chain)!.addChain(...args);
|
|
164
|
+
}) as ScType.AddChain
|
|
165
|
+
: this.#wellKnownChains.has(this.#spec as ScType.WellKnownChain)
|
|
166
|
+
? client.addWellKnownChain
|
|
167
|
+
: client.addChain;
|
|
168
|
+
|
|
169
|
+
this.#chain = addChain(this.#spec as ScType.WellKnownChain, onResponse).then((chain) => {
|
|
170
|
+
hc.setSendJsonRpc(chain.sendJsonRpc);
|
|
171
|
+
|
|
172
|
+
this.#isChainReady = false;
|
|
173
|
+
|
|
174
|
+
const cleanup = () => {
|
|
175
|
+
// If there are any callbacks left, we have to reject/error them.
|
|
176
|
+
// Otherwise, that would cause a memory leak.
|
|
177
|
+
const disconnectionError = new Error('Disconnected');
|
|
178
|
+
|
|
179
|
+
this.#requests.forEach((cb) => cb(disconnectionError));
|
|
180
|
+
this.#subscriptions.forEach(([cb]) => cb(disconnectionError));
|
|
181
|
+
this.#subscriptions.clear();
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
const staleSubscriptions: {
|
|
185
|
+
unsubscribeMethod: string
|
|
186
|
+
id: number | string
|
|
187
|
+
}[] = [];
|
|
188
|
+
|
|
189
|
+
const killStaleSubscriptions = () => {
|
|
190
|
+
if (staleSubscriptions.length === 0) {
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const stale = staleSubscriptions.pop();
|
|
195
|
+
|
|
196
|
+
if (!stale) {
|
|
197
|
+
throw new Error('Unable to get stale subscription');
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
const { id, unsubscribeMethod } = stale;
|
|
201
|
+
|
|
202
|
+
Promise
|
|
203
|
+
.race([
|
|
204
|
+
this.send(unsubscribeMethod, [id]).catch(noop),
|
|
205
|
+
new Promise((resolve) => setTimeout(resolve, 500))
|
|
206
|
+
])
|
|
207
|
+
.then(killStaleSubscriptions)
|
|
208
|
+
.catch(noop);
|
|
209
|
+
};
|
|
210
|
+
|
|
211
|
+
hc.start((health) => {
|
|
212
|
+
const isReady =
|
|
213
|
+
!health.isSyncing && (health.peers > 0 || !health.shouldHavePeers);
|
|
214
|
+
|
|
215
|
+
// if it's the same as before, then nothing has changed and we are done
|
|
216
|
+
if (this.#isChainReady === isReady) {
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
this.#isChainReady = isReady;
|
|
221
|
+
|
|
222
|
+
if (!isReady) {
|
|
223
|
+
// If we've reached this point, that means that the chain used to be "ready"
|
|
224
|
+
// and now we are about to emit `disconnected`.
|
|
225
|
+
//
|
|
226
|
+
// This will cause the PolkadotJs API think that the connection is
|
|
227
|
+
// actually dead. In reality the smoldot chain is not dead, of course.
|
|
228
|
+
// However, we have to cleanup all the existing callbacks because when
|
|
229
|
+
// the smoldot chain stops syncing, then we will emit `connected` and
|
|
230
|
+
// the PolkadotJs API will try to re-create the previous
|
|
231
|
+
// subscriptions and requests. Although, now is not a good moment
|
|
232
|
+
// to be sending unsubscription messages to the smoldot chain, we
|
|
233
|
+
// should wait until is no longer syncing to send the unsubscription
|
|
234
|
+
// messages from the stale subscriptions of the previous connection.
|
|
235
|
+
//
|
|
236
|
+
// That's why -before we perform the cleanup of `this.#subscriptions`-
|
|
237
|
+
// we keep the necessary information that we will need later on to
|
|
238
|
+
// kill the stale subscriptions.
|
|
239
|
+
[...this.#subscriptions.values()].forEach((s) => {
|
|
240
|
+
staleSubscriptions.push(s[1]);
|
|
241
|
+
});
|
|
242
|
+
cleanup();
|
|
243
|
+
|
|
244
|
+
this.#eventemitter.emit('disconnected');
|
|
245
|
+
} else {
|
|
246
|
+
killStaleSubscriptions();
|
|
247
|
+
|
|
248
|
+
this.#eventemitter.emit('connected');
|
|
249
|
+
|
|
250
|
+
if (this.#resubscribeMethods.size) {
|
|
251
|
+
this.#resubscribe();
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
return objectSpread({}, chain, {
|
|
257
|
+
remove: () => {
|
|
258
|
+
hc.stop();
|
|
259
|
+
chain.remove();
|
|
260
|
+
cleanup();
|
|
261
|
+
},
|
|
262
|
+
sendJsonRpc: hc.sendJsonRpc.bind(hc)
|
|
263
|
+
});
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
try {
|
|
267
|
+
await this.#chain;
|
|
268
|
+
} catch (e) {
|
|
269
|
+
this.#chain = null;
|
|
270
|
+
this.#eventemitter.emit('error', e);
|
|
271
|
+
throw e;
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
#resubscribe = (): void => {
|
|
276
|
+
const promises: any[] = [];
|
|
277
|
+
|
|
278
|
+
this.#resubscribeMethods.forEach((subDetails: ActiveSubs): void => {
|
|
279
|
+
// only re-create subscriptions which are not in author (only area where
|
|
280
|
+
// transactions are created, i.e. submissions such as 'author_submitAndWatchExtrinsic'
|
|
281
|
+
// are not included (and will not be re-broadcast)
|
|
282
|
+
if (subDetails.type.startsWith('author_')) {
|
|
283
|
+
return;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
try {
|
|
287
|
+
const promise = new Promise<void>((resolve) => {
|
|
288
|
+
this.subscribe(subDetails.type, subDetails.method, subDetails.params, subDetails.callback).catch((error) => console.log(error));
|
|
289
|
+
resolve();
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
promises.push(promise);
|
|
293
|
+
} catch (error) {
|
|
294
|
+
l.error(error);
|
|
295
|
+
}
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
Promise.all(promises).catch((err) => l.log(err));
|
|
299
|
+
};
|
|
300
|
+
|
|
301
|
+
async disconnect (): Promise<void> {
|
|
302
|
+
if (!this.#chain) {
|
|
303
|
+
return;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
const chain = await this.#chain;
|
|
307
|
+
|
|
308
|
+
this.#chain = null;
|
|
309
|
+
this.#isChainReady = false;
|
|
310
|
+
|
|
311
|
+
try {
|
|
312
|
+
chain.remove();
|
|
313
|
+
} catch (_) {}
|
|
314
|
+
|
|
315
|
+
this.#eventemitter.emit('disconnected');
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
public on (type: ProviderInterfaceEmitted, sub: ProviderInterfaceEmitCb): () => void {
|
|
319
|
+
// It's possible. Although, quite unlikely, that by the time that polkadot
|
|
320
|
+
// subscribes to the `connected` event, the Provider is already connected.
|
|
321
|
+
// In that case, we must emit to let the consumer know that we are connected.
|
|
322
|
+
if (type === 'connected' && this.isConnected) {
|
|
323
|
+
sub();
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
this.#eventemitter.on(type, sub);
|
|
327
|
+
|
|
328
|
+
return (): void => {
|
|
329
|
+
this.#eventemitter.removeListener(type, sub);
|
|
330
|
+
};
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
public async send<T = any> (method: string, params: unknown[]): Promise<T> {
|
|
334
|
+
if (!this.isConnected || !this.#chain) {
|
|
335
|
+
throw new Error('Provider is not connected');
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
const chain = await this.#chain;
|
|
339
|
+
const [id, json] = this.#coder.encodeJson(method, params);
|
|
340
|
+
|
|
341
|
+
const result = new Promise<T>((resolve, reject): void => {
|
|
342
|
+
this.#requests.set(id, (response) => {
|
|
343
|
+
(isError(response) ? reject : resolve)(response as unknown as T);
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
try {
|
|
347
|
+
chain.sendJsonRpc(json);
|
|
348
|
+
} catch (e) {
|
|
349
|
+
this.#chain = null;
|
|
350
|
+
|
|
351
|
+
try {
|
|
352
|
+
chain.remove();
|
|
353
|
+
} catch (_) {}
|
|
354
|
+
|
|
355
|
+
this.#eventemitter.emit('error', e);
|
|
356
|
+
}
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
try {
|
|
360
|
+
return await result;
|
|
361
|
+
} finally {
|
|
362
|
+
// let's ensure that once the Promise is resolved/rejected, then we remove
|
|
363
|
+
// remove its entry from the internal #requests
|
|
364
|
+
this.#requests.delete(id);
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
public async subscribe (type: string, method: string, params: any[], callback: ProviderInterfaceCallback): Promise<number | string> {
|
|
369
|
+
if (!subscriptionUnsubscriptionMethods.has(method)) {
|
|
370
|
+
throw new Error(`Unsupported subscribe method: ${method}`);
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
const id = await this.send<number | string>(method, params);
|
|
374
|
+
const subscriptionId = `${type}::${id}`;
|
|
375
|
+
|
|
376
|
+
const cb = (response: Error | string) => {
|
|
377
|
+
if (response instanceof Error) {
|
|
378
|
+
callback(response, undefined);
|
|
379
|
+
} else {
|
|
380
|
+
callback(null, response);
|
|
381
|
+
}
|
|
382
|
+
};
|
|
383
|
+
|
|
384
|
+
const unsubscribeMethod = subscriptionUnsubscriptionMethods.get(method);
|
|
385
|
+
|
|
386
|
+
if (!unsubscribeMethod) {
|
|
387
|
+
throw new Error('Invalid unsubscribe method found');
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
this.#resubscribeMethods.set(subscriptionId, { callback, method, params, type });
|
|
391
|
+
|
|
392
|
+
this.#subscriptions.set(subscriptionId, [cb, { id, unsubscribeMethod }]);
|
|
393
|
+
|
|
394
|
+
return id;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
public unsubscribe (type: string, method: string, id: number | string): Promise<boolean> {
|
|
398
|
+
if (!this.isConnected) {
|
|
399
|
+
throw new Error('Provider is not connected');
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
const subscriptionId = `${type}::${id}`;
|
|
403
|
+
|
|
404
|
+
if (!this.#subscriptions.has(subscriptionId)) {
|
|
405
|
+
return Promise.reject(
|
|
406
|
+
new Error(`Unable to find active subscription=${subscriptionId}`)
|
|
407
|
+
);
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
this.#resubscribeMethods.delete(subscriptionId);
|
|
411
|
+
this.#subscriptions.delete(subscriptionId);
|
|
412
|
+
|
|
413
|
+
return this.send(method, [id]);
|
|
414
|
+
}
|
|
415
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
// Copyright 2017-2025 @polkadot/rpc-provider authors & contributors
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
|
|
4
|
+
export interface SmoldotHealth {
|
|
5
|
+
isSyncing: boolean
|
|
6
|
+
peers: number
|
|
7
|
+
shouldHavePeers: boolean
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface HealthChecker {
|
|
11
|
+
setSendJsonRpc(sendRequest: (request: string) => void): void
|
|
12
|
+
start(healthCallback: (health: SmoldotHealth) => void): void
|
|
13
|
+
stop(): void
|
|
14
|
+
sendJsonRpc(request: string): void
|
|
15
|
+
responsePassThrough(response: string): string | null
|
|
16
|
+
}
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
// Copyright 2017-2025 @polkadot/rpc-provider authors & contributors
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
|
|
4
|
+
export interface JsonRpcObject {
|
|
5
|
+
id: number;
|
|
6
|
+
jsonrpc: '2.0';
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface JsonRpcRequest extends JsonRpcObject {
|
|
10
|
+
method: string;
|
|
11
|
+
params: unknown[];
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface JsonRpcResponseBaseError {
|
|
15
|
+
code: number;
|
|
16
|
+
data?: number | string;
|
|
17
|
+
message: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface RpcErrorInterface<T> {
|
|
21
|
+
code: number;
|
|
22
|
+
data?: T;
|
|
23
|
+
message: string;
|
|
24
|
+
stack: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
interface JsonRpcResponseSingle<T> {
|
|
28
|
+
error?: JsonRpcResponseBaseError;
|
|
29
|
+
result: T;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
interface JsonRpcResponseSubscription<T> {
|
|
33
|
+
method?: string;
|
|
34
|
+
params: {
|
|
35
|
+
error?: JsonRpcResponseBaseError;
|
|
36
|
+
result: T;
|
|
37
|
+
subscription: number | string;
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export type JsonRpcResponseBase<T> = JsonRpcResponseSingle<T> & JsonRpcResponseSubscription<T>;
|
|
42
|
+
|
|
43
|
+
export type JsonRpcResponse<T> = JsonRpcObject & JsonRpcResponseBase<T>;
|
|
44
|
+
|
|
45
|
+
export type ProviderInterfaceCallback = (error: Error | null, result: any) => void;
|
|
46
|
+
|
|
47
|
+
export type ProviderInterfaceEmitted = 'connected' | 'disconnected' | 'error';
|
|
48
|
+
|
|
49
|
+
export type ProviderInterfaceEmitCb = (value?: any) => any;
|
|
50
|
+
|
|
51
|
+
export interface ProviderInterface {
|
|
52
|
+
/** true if the provider supports subscriptions (not available for HTTP) */
|
|
53
|
+
readonly hasSubscriptions: boolean;
|
|
54
|
+
/** true if the clone() functionality is available on the provider */
|
|
55
|
+
readonly isClonable: boolean;
|
|
56
|
+
/** true if the provider is currently connected (ws/sc has connection logic) */
|
|
57
|
+
readonly isConnected: boolean;
|
|
58
|
+
/** (optional) stats for the provider with connections/bytes */
|
|
59
|
+
readonly stats?: ProviderStats;
|
|
60
|
+
/** (optional) stats for the provider with connections/bytes */
|
|
61
|
+
readonly ttl?: number | null;
|
|
62
|
+
|
|
63
|
+
clone (): ProviderInterface;
|
|
64
|
+
connect (): Promise<void>;
|
|
65
|
+
disconnect (): Promise<void>;
|
|
66
|
+
on (type: ProviderInterfaceEmitted, sub: ProviderInterfaceEmitCb): () => void;
|
|
67
|
+
send <T = any> (method: string, params: unknown[], isCacheable?: boolean): Promise<T>;
|
|
68
|
+
subscribe (type: string, method: string, params: unknown[], cb: ProviderInterfaceCallback): Promise<number | string>;
|
|
69
|
+
unsubscribe (type: string, method: string, id: number | string): Promise<boolean>;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** Stats for a specific endpoint */
|
|
73
|
+
export interface EndpointStats {
|
|
74
|
+
/** The total number of bytes sent */
|
|
75
|
+
bytesRecv: number;
|
|
76
|
+
/** The total number of bytes received */
|
|
77
|
+
bytesSent: number;
|
|
78
|
+
/** The number of cached/in-progress requests made */
|
|
79
|
+
cached: number;
|
|
80
|
+
/** The number of errors found */
|
|
81
|
+
errors: number;
|
|
82
|
+
/** The number of requests */
|
|
83
|
+
requests: number;
|
|
84
|
+
/** The number of subscriptions */
|
|
85
|
+
subscriptions: number;
|
|
86
|
+
/** The number of request timeouts */
|
|
87
|
+
timeout: number;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** Overall stats for the provider */
|
|
91
|
+
export interface ProviderStats {
|
|
92
|
+
/** Details for the active/open requests */
|
|
93
|
+
active: {
|
|
94
|
+
/** Number of active requests */
|
|
95
|
+
requests: number;
|
|
96
|
+
/** Number of active subscriptions */
|
|
97
|
+
subscriptions: number;
|
|
98
|
+
};
|
|
99
|
+
/** The total requests that have been made */
|
|
100
|
+
total: EndpointStats;
|
|
101
|
+
}
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
// Copyright 2017-2025 @polkadot/rpc-provider authors & contributors
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
|
|
4
|
+
/// <reference types="@pezkuwi/dev-test/globals.d.ts" />
|
|
5
|
+
|
|
6
|
+
import type { Mock } from '../mock/types.js';
|
|
7
|
+
|
|
8
|
+
import { mockWs } from '../mock/mockWs.js';
|
|
9
|
+
import { WsProvider } from './index.js';
|
|
10
|
+
|
|
11
|
+
const TEST_WS_URL = 'ws://localhost-connect.spec.ts:9988';
|
|
12
|
+
|
|
13
|
+
function sleep (ms = 100): Promise<void> {
|
|
14
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
describe('onConnect', (): void => {
|
|
18
|
+
let mocks: Mock[];
|
|
19
|
+
let provider: WsProvider | null;
|
|
20
|
+
|
|
21
|
+
beforeEach((): void => {
|
|
22
|
+
mocks = [mockWs([], TEST_WS_URL)];
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
afterEach(async () => {
|
|
26
|
+
if (provider) {
|
|
27
|
+
await provider.disconnect();
|
|
28
|
+
await sleep();
|
|
29
|
+
|
|
30
|
+
provider = null;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
await Promise.all(mocks.map((m) => m.done()));
|
|
34
|
+
await sleep();
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it('Does not connect when autoConnect is false', async () => {
|
|
38
|
+
provider = new WsProvider(TEST_WS_URL, 0);
|
|
39
|
+
|
|
40
|
+
await sleep();
|
|
41
|
+
|
|
42
|
+
expect(provider.isConnected).toBe(false);
|
|
43
|
+
|
|
44
|
+
await provider.connect();
|
|
45
|
+
await sleep();
|
|
46
|
+
|
|
47
|
+
expect(provider.isConnected).toBe(true);
|
|
48
|
+
|
|
49
|
+
await provider.disconnect();
|
|
50
|
+
await sleep();
|
|
51
|
+
|
|
52
|
+
expect(provider.isConnected).toBe(false);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it('Does connect when autoConnect is true', async () => {
|
|
56
|
+
provider = new WsProvider(TEST_WS_URL, 1);
|
|
57
|
+
|
|
58
|
+
await sleep();
|
|
59
|
+
|
|
60
|
+
expect(provider.isConnected).toBe(true);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it('Creates a new WebSocket instance by calling the connect() method', async () => {
|
|
64
|
+
provider = new WsProvider(TEST_WS_URL, false);
|
|
65
|
+
|
|
66
|
+
expect(provider.isConnected).toBe(false);
|
|
67
|
+
expect(mocks[0].server.clients().length).toBe(0);
|
|
68
|
+
|
|
69
|
+
await provider.connect();
|
|
70
|
+
await sleep();
|
|
71
|
+
|
|
72
|
+
expect(provider.isConnected).toBe(true);
|
|
73
|
+
expect(mocks[0].server.clients()).toHaveLength(1);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it('Connects to first endpoint when an array is given', async () => {
|
|
77
|
+
provider = new WsProvider([TEST_WS_URL], 1);
|
|
78
|
+
|
|
79
|
+
await sleep();
|
|
80
|
+
|
|
81
|
+
expect(provider.isConnected).toBe(true);
|
|
82
|
+
expect(mocks[0].server.clients()).toHaveLength(1);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it('Does not allow connect() on already-connected', async () => {
|
|
86
|
+
provider = new WsProvider([TEST_WS_URL], 1);
|
|
87
|
+
|
|
88
|
+
await sleep();
|
|
89
|
+
|
|
90
|
+
expect(provider.isConnected).toBe(true);
|
|
91
|
+
|
|
92
|
+
await expect(
|
|
93
|
+
provider.connect()
|
|
94
|
+
).rejects.toThrow(/already connected/);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it('Connects to the second endpoint when the first is unreachable', async () => {
|
|
98
|
+
const endpoints: string[] = ['ws://localhost-unreachable-connect.spec.ts:9956', TEST_WS_URL];
|
|
99
|
+
|
|
100
|
+
provider = new WsProvider(endpoints, 1);
|
|
101
|
+
|
|
102
|
+
await sleep();
|
|
103
|
+
|
|
104
|
+
expect(mocks[0].server.clients()).toHaveLength(1);
|
|
105
|
+
expect(provider.isConnected).toBe(true);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it('Connects to the second endpoint when the first is dropped', async () => {
|
|
109
|
+
const endpoints: string[] = [TEST_WS_URL, 'ws://localhost-connect.spec.ts:9957'];
|
|
110
|
+
|
|
111
|
+
mocks.push(mockWs([], endpoints[1]));
|
|
112
|
+
|
|
113
|
+
provider = new WsProvider(endpoints, 1);
|
|
114
|
+
|
|
115
|
+
await sleep();
|
|
116
|
+
|
|
117
|
+
// Check that first server is connected
|
|
118
|
+
expect(mocks[0].server.clients()).toHaveLength(1);
|
|
119
|
+
expect(mocks[1].server.clients()).toHaveLength(0);
|
|
120
|
+
|
|
121
|
+
// Close connection from first server
|
|
122
|
+
mocks[0].server.clients()[0].close();
|
|
123
|
+
|
|
124
|
+
await sleep();
|
|
125
|
+
|
|
126
|
+
// Check that second server is connected
|
|
127
|
+
expect(mocks[1].server.clients()).toHaveLength(1);
|
|
128
|
+
expect(provider.isConnected).toBe(true);
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it('Round-robin of endpoints on WsProvider', async () => {
|
|
132
|
+
const endpoints: string[] = [
|
|
133
|
+
TEST_WS_URL,
|
|
134
|
+
'ws://localhost-connect.spec.ts:9956',
|
|
135
|
+
'ws://localhost-connect.spec.ts:9957',
|
|
136
|
+
'ws://invalid-connect.spec.ts:9956',
|
|
137
|
+
'ws://localhost-connect.spec.ts:9958'
|
|
138
|
+
];
|
|
139
|
+
|
|
140
|
+
mocks.push(mockWs([], endpoints[1]));
|
|
141
|
+
mocks.push(mockWs([], endpoints[2]));
|
|
142
|
+
mocks.push(mockWs([], endpoints[4]));
|
|
143
|
+
|
|
144
|
+
const mockNext = [
|
|
145
|
+
mocks[1],
|
|
146
|
+
mocks[2],
|
|
147
|
+
mocks[3],
|
|
148
|
+
mocks[0]
|
|
149
|
+
];
|
|
150
|
+
|
|
151
|
+
provider = new WsProvider(endpoints, 1);
|
|
152
|
+
|
|
153
|
+
for (let round = 0; round < 2; round++) {
|
|
154
|
+
for (let mock = 0; mock < mocks.length; mock++) {
|
|
155
|
+
await sleep();
|
|
156
|
+
|
|
157
|
+
// Wwe are connected, the current mock has the connection and the next doesn't
|
|
158
|
+
expect(provider.isConnected).toBe(true);
|
|
159
|
+
expect(mocks[mock].server.clients()).toHaveLength(1);
|
|
160
|
+
expect(mockNext[mock].server.clients()).toHaveLength(0);
|
|
161
|
+
|
|
162
|
+
// Close connection from first server
|
|
163
|
+
mocks[mock].server.clients()[0].close();
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
});
|
|
167
|
+
});
|