@talismn/chain-connectors 0.0.0

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.
Files changed (29) hide show
  1. package/LICENSE +674 -0
  2. package/README.md +1 -0
  3. package/dist/declarations/src/dot/ChainConnectorDot.d.ts +79 -0
  4. package/dist/declarations/src/dot/ChainConnectorDotStub.d.ts +11 -0
  5. package/dist/declarations/src/dot/IChainConnectorDot.d.ts +10 -0
  6. package/dist/declarations/src/dot/Websocket.d.ts +111 -0
  7. package/dist/declarations/src/dot/helpers.d.ts +15 -0
  8. package/dist/declarations/src/dot/index.d.ts +3 -0
  9. package/dist/declarations/src/eth/ChainConnectorEth.d.ts +10 -0
  10. package/dist/declarations/src/eth/ChainConnectorEthStub.d.ts +10 -0
  11. package/dist/declarations/src/eth/IChainConnectorEth.d.ts +7 -0
  12. package/dist/declarations/src/eth/getChainFromEvmNetwork.d.ts +4 -0
  13. package/dist/declarations/src/eth/getEvmNetworkPublicClient.d.ts +4 -0
  14. package/dist/declarations/src/eth/getEvmNetworkWalletClient.d.ts +7 -0
  15. package/dist/declarations/src/eth/getTransportForEvmNetwork.d.ts +8 -0
  16. package/dist/declarations/src/eth/index.d.ts +3 -0
  17. package/dist/declarations/src/index.d.ts +3 -0
  18. package/dist/declarations/src/log.d.ts +2 -0
  19. package/dist/declarations/src/sol/ChainConnectorSol.d.ts +8 -0
  20. package/dist/declarations/src/sol/ChainConnectorSolStub.d.ts +8 -0
  21. package/dist/declarations/src/sol/IChainConnectorSol.d.ts +5 -0
  22. package/dist/declarations/src/sol/getSolConnection.d.ts +3 -0
  23. package/dist/declarations/src/sol/index.d.ts +3 -0
  24. package/dist/talismn-chain-connectors.cjs.d.ts +1 -0
  25. package/dist/talismn-chain-connectors.cjs.dev.js +1236 -0
  26. package/dist/talismn-chain-connectors.cjs.js +7 -0
  27. package/dist/talismn-chain-connectors.cjs.prod.js +1236 -0
  28. package/dist/talismn-chain-connectors.esm.js +1202 -0
  29. package/package.json +63 -0
@@ -0,0 +1,1236 @@
1
+ 'use strict';
2
+
3
+ var util$1 = require('@talismn/util');
4
+ var anylogger = require('anylogger');
5
+ var coder = require('@polkadot/rpc-provider/coder');
6
+ var errors = require('@polkadot/rpc-provider/ws/errors');
7
+ var util = require('@polkadot/util');
8
+ var xGlobal = require('@polkadot/x-global');
9
+ var xWs = require('@polkadot/x-ws');
10
+ var EventEmitter = require('eventemitter3');
11
+ var rpcProvider = require('@polkadot/rpc-provider');
12
+ var viem = require('viem');
13
+ var lodashEs = require('lodash-es');
14
+ var chains = require('viem/chains');
15
+ var web3_js = require('@solana/web3.js');
16
+
17
+ function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
18
+
19
+ function _interopNamespace(e) {
20
+ if (e && e.__esModule) return e;
21
+ var n = Object.create(null);
22
+ if (e) {
23
+ Object.keys(e).forEach(function (k) {
24
+ if (k !== 'default') {
25
+ var d = Object.getOwnPropertyDescriptor(e, k);
26
+ Object.defineProperty(n, k, d.get ? d : {
27
+ enumerable: true,
28
+ get: function () { return e[k]; }
29
+ });
30
+ }
31
+ });
32
+ }
33
+ n.default = e;
34
+ return Object.freeze(n);
35
+ }
36
+
37
+ var anylogger__default = /*#__PURE__*/_interopDefault(anylogger);
38
+ var EventEmitter__default = /*#__PURE__*/_interopDefault(EventEmitter);
39
+ var chains__namespace = /*#__PURE__*/_interopNamespace(chains);
40
+
41
+ var packageJson = {
42
+ name: "@talismn/chain-connectors"};
43
+
44
+ var log = anylogger__default.default(packageJson.name);
45
+
46
+ const twoSecondsMs = 2 * 1000;
47
+ const twoMinutesMs = 2 * 60 * 1000;
48
+ class ExponentialBackoff {
49
+ #minInterval;
50
+ #maxInterval;
51
+ #nextInterval = 0;
52
+ #active = true;
53
+ constructor(maxIntervalMs = twoMinutesMs, minIntervalMs = twoSecondsMs) {
54
+ this.#minInterval = minIntervalMs;
55
+ this.#maxInterval = maxIntervalMs;
56
+ this.reset();
57
+ }
58
+ enable() {
59
+ this.#active = true;
60
+ }
61
+ disable() {
62
+ this.#active = false;
63
+ }
64
+ increase() {
65
+ if (this.#nextInterval === 0) this.#nextInterval = 1;
66
+ this.#nextInterval = this.#capMax(this.#capMin(this.#nextInterval * 2));
67
+ }
68
+ decrease() {
69
+ this.#nextInterval = this.#capMax(this.#capMin(this.#nextInterval / 2));
70
+ }
71
+ reset() {
72
+ this.#nextInterval = this.#minInterval;
73
+ }
74
+ resetTo(nextInterval) {
75
+ this.#nextInterval = this.#capMax(this.#capMin(nextInterval));
76
+ }
77
+ resetToMax() {
78
+ this.#nextInterval = this.#maxInterval;
79
+ }
80
+ get isActive() {
81
+ return this.#active;
82
+ }
83
+ get next() {
84
+ return this.#nextInterval;
85
+ }
86
+ get isMin() {
87
+ return this.#nextInterval === this.#minInterval;
88
+ }
89
+ get isMax() {
90
+ return this.#nextInterval === this.#maxInterval;
91
+ }
92
+ #capMin(value) {
93
+ return Math.max(this.#minInterval, value);
94
+ }
95
+ #capMax(value) {
96
+ return Math.min(this.#maxInterval, value);
97
+ }
98
+ }
99
+
100
+ // to account for new requirement for generic arg in this type https://github.com/polkadot-js/api/commit/f4c2b150d3d69d43c56699613666b96dd0a763f4#diff-f87c17bc7fae027ec6d43bac5fc089614d9fa097f466aa2be333b44cee81f0fd
101
+ // TODO incrementally replace 'unknown' with proper types where possible
102
+
103
+ const ALIASES = {
104
+ chain_finalisedHead: "chain_finalizedHead",
105
+ chain_subscribeFinalisedHeads: "chain_subscribeFinalizedHeads",
106
+ chain_unsubscribeFinalisedHeads: "chain_unsubscribeFinalizedHeads"
107
+ };
108
+ const DEFAULT_TIMEOUT_MS = 60 * 1000;
109
+ const TIMEOUT_INTERVAL = 5_000;
110
+ function eraseRecord(record, cb) {
111
+ Object.keys(record).forEach(key => {
112
+ if (cb) {
113
+ cb(record[key]);
114
+ }
115
+ delete record[key];
116
+ });
117
+ }
118
+
119
+ /**
120
+ * # @talismn/chain-connector/Websocket
121
+ *
122
+ * @name Websocket
123
+ *
124
+ * @description The WebSocket Provider allows sending requests using WebSocket to a WebSocket RPC server TCP port. Unlike the [[HttpProvider]], it does support subscriptions and allows listening to events such as new blocks or balance changes.
125
+ *
126
+ * @example
127
+ * <BR>
128
+ *
129
+ * ```javascript
130
+ * import { Websocket } from '@talismn/chain-connector';
131
+ *
132
+ * const provider = new Websocket('ws://127.0.0.1:9944');
133
+ * ```
134
+ *
135
+ * @see [[HttpProvider]]
136
+ */
137
+ class Websocket {
138
+ #coder;
139
+ #endpoints;
140
+ #headers;
141
+ #eventemitter;
142
+ #handlers = {};
143
+ #isReadyPromise;
144
+ #waitingForId = {};
145
+ #autoConnectBackoff;
146
+ #endpointIndex;
147
+ #endpointsTriedSinceLastConnection = 0;
148
+ #isConnected = false;
149
+ #subscriptions = {};
150
+ #timeoutId = null;
151
+ #websocket;
152
+ #timeout;
153
+
154
+ /**
155
+ * @param {string | string[]} endpoint The endpoint url. Usually `ws://ip:9944` or `wss://ip:9944`, may provide an array of endpoint strings.
156
+ * @param {Record<string, string>} headers The headers provided to the underlying WebSocket
157
+ * @param {number} [timeout] Custom timeout value used per request . Defaults to `DEFAULT_TIMEOUT_MS`
158
+ */
159
+ constructor(endpoint, headers = {}, timeout, nextBackoffInterval) {
160
+ const endpoints = Array.isArray(endpoint) ? endpoint : [endpoint];
161
+ if (endpoints.length === 0) {
162
+ throw new Error("Websocket requires at least one Endpoint");
163
+ }
164
+ endpoints.forEach(endpoint => {
165
+ if (!/^(wss|ws):\/\//.test(endpoint)) {
166
+ throw new Error(`Endpoint should start with 'ws://', received '${endpoint}'`);
167
+ }
168
+ });
169
+ this.#eventemitter = new EventEmitter__default.default();
170
+ this.#autoConnectBackoff = new ExponentialBackoff();
171
+ if (nextBackoffInterval) this.#autoConnectBackoff.resetTo(nextBackoffInterval);
172
+ this.#coder = new coder.RpcCoder();
173
+ this.#endpointIndex = -1;
174
+ this.#endpoints = endpoints;
175
+ this.#headers = headers;
176
+ this.#websocket = null;
177
+ this.#timeout = timeout || DEFAULT_TIMEOUT_MS;
178
+ if (this.#autoConnectBackoff.isActive) {
179
+ this.connectWithRetry().catch(() => {
180
+ // does not throw
181
+ });
182
+ }
183
+ this.#isReadyPromise = new Promise(resolve => {
184
+ this.#eventemitter.once("connected", () => {
185
+ resolve(this);
186
+ });
187
+ });
188
+ }
189
+
190
+ /**
191
+ * @summary `true` when this provider supports subscriptions
192
+ */
193
+ get hasSubscriptions() {
194
+ return true;
195
+ }
196
+
197
+ /**
198
+ * @summary `true` when this provider supports clone()
199
+ */
200
+ get isClonable() {
201
+ return true;
202
+ }
203
+
204
+ /**
205
+ * @summary Whether the node is connected or not.
206
+ * @return {boolean} true if connected
207
+ */
208
+ get isConnected() {
209
+ return this.#isConnected;
210
+ }
211
+
212
+ /**
213
+ * @description Promise that resolves the first time we are connected and loaded
214
+ */
215
+ get isReady() {
216
+ return this.#isReadyPromise;
217
+ }
218
+ get endpoint() {
219
+ return this.#endpoints[this.#endpointIndex];
220
+ }
221
+
222
+ /**
223
+ * @description Returns a clone of the object
224
+ */
225
+ clone() {
226
+ return new Websocket(this.#endpoints);
227
+ }
228
+ selectEndpointIndex(endpoints) {
229
+ this.#endpointsTriedSinceLastConnection += 1;
230
+ return (this.#endpointIndex + 1) % endpoints.length;
231
+ }
232
+
233
+ /**
234
+ * @summary Manually connect
235
+ * @description The [[Websocket]] connects automatically by default, however if you decided otherwise, you may
236
+ * connect manually using this method.
237
+ */
238
+ // eslint-disable-next-line @typescript-eslint/require-await
239
+ async connect() {
240
+ if (this.#websocket) {
241
+ throw new Error("WebSocket is already connected");
242
+ }
243
+ try {
244
+ this.#endpointIndex = this.selectEndpointIndex(this.#endpoints);
245
+
246
+ // the as typeof WebSocket here is Deno-specific - not available on the globalThis
247
+ this.#websocket = typeof xGlobal.xglobal.WebSocket !== "undefined" && util.isChildClass(xGlobal.xglobal.WebSocket, xWs.WebSocket) ? new xWs.WebSocket(this.endpoint) :
248
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
249
+ // @ts-ignore - WS may be an instance of ws, which supports options
250
+ new xWs.WebSocket(this.endpoint, undefined, {
251
+ headers: this.#headers
252
+ });
253
+ if (this.#websocket) {
254
+ this.#websocket.onclose = this.#onSocketClose;
255
+ this.#websocket.onerror = this.#onSocketError;
256
+ this.#websocket.onmessage = this.#onSocketMessage;
257
+ this.#websocket.onopen = this.#onSocketOpen;
258
+ }
259
+
260
+ // timeout any handlers that have not had a response
261
+ this.#timeoutId = setInterval(() => this.#timeoutHandlers(), TIMEOUT_INTERVAL);
262
+ } catch (error) {
263
+ log.error(error);
264
+ this.#emit("error", error);
265
+ throw error;
266
+ }
267
+ }
268
+
269
+ /**
270
+ * @description Connect, never throwing an error, but rather forcing a retry
271
+ */
272
+ async connectWithRetry() {
273
+ if (!this.#autoConnectBackoff.isActive) return;
274
+ try {
275
+ await this.connect();
276
+ } catch (error) {
277
+ this.scheduleNextRetry();
278
+ }
279
+ }
280
+ scheduleNextRetry() {
281
+ if (!this.#autoConnectBackoff.isActive) return;
282
+ const haveTriedAllEndpoints = this.#endpointsTriedSinceLastConnection > 0 && this.#endpointsTriedSinceLastConnection % this.#endpoints.length === 0;
283
+ setTimeout(() => {
284
+ this.connectWithRetry().catch(() => {
285
+ // does not throw
286
+ });
287
+ }, haveTriedAllEndpoints ? this.#autoConnectBackoff.next : 0);
288
+
289
+ // Increase backoff when we've tried all endpoints
290
+ if (haveTriedAllEndpoints) this.#autoConnectBackoff.increase();
291
+
292
+ // Fire a stale-rpcs event when we've tried all endpoints in the list
293
+ // but haven't successfully connected to any of them
294
+ if (haveTriedAllEndpoints) this.#emit("stale-rpcs", {
295
+ nextBackoffInterval: this.#autoConnectBackoff.next
296
+ });
297
+ }
298
+
299
+ /**
300
+ * @description Manually disconnect from the connection, clearing auto-connect logic
301
+ */
302
+ // eslint-disable-next-line @typescript-eslint/require-await
303
+ async disconnect() {
304
+ // switch off autoConnect, we are in manual mode now
305
+ this.#autoConnectBackoff.disable();
306
+ try {
307
+ if (this.#websocket) {
308
+ // 1000 - Normal closure; the connection successfully completed
309
+ this.#websocket.close(1000);
310
+ }
311
+ } catch (error) {
312
+ log.error(error);
313
+ this.#emit("error", error);
314
+ throw error;
315
+ }
316
+ }
317
+
318
+ /**
319
+ * @summary Listens on events after having subscribed using the [[subscribe]] function.
320
+ * @param {ProviderInterfaceEmitted} type Event
321
+ * @param {ProviderInterfaceEmitCb} sub Callback
322
+ * @return unsubscribe function
323
+ */
324
+ on(type, sub) {
325
+ this.#eventemitter.on(type, sub);
326
+ return () => {
327
+ this.#eventemitter.removeListener(type, sub);
328
+ };
329
+ }
330
+
331
+ /**
332
+ * @summary Send JSON data using WebSockets to configured HTTP Endpoint or queue.
333
+ * @param method The RPC methods to execute
334
+ * @param params Encoded parameters as applicable for the method
335
+ * @param subscription Subscription details (internally used)
336
+ */
337
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
338
+ send(method, params, /** @deprecated \@talismn/chain-connector doesn't implement a cache */
339
+ isCacheable, subscription) {
340
+ const [id, body] = this.#coder.encodeJson(method, params);
341
+ const resultPromise = this.#send(id, body, method, params, subscription);
342
+ return resultPromise;
343
+ }
344
+ async #send(id, body, method, params, subscription) {
345
+ return new Promise((resolve, reject) => {
346
+ try {
347
+ if (!this.isConnected || this.#websocket === null) {
348
+ throw new Error("WebSocket is not connected");
349
+ }
350
+ const callback = (error, result) => {
351
+ error ? reject(error) : resolve(result);
352
+ };
353
+
354
+ // log.debug(() => ["calling", method, body])
355
+
356
+ this.#handlers[id] = {
357
+ callback,
358
+ method,
359
+ params,
360
+ start: Date.now(),
361
+ subscription
362
+ };
363
+ this.#websocket.send(body);
364
+ } catch (error) {
365
+ reject(error);
366
+ }
367
+ });
368
+ }
369
+
370
+ /**
371
+ * @name subscribe
372
+ * @summary Allows subscribing to a specific event.
373
+ *
374
+ * @example
375
+ * <BR>
376
+ *
377
+ * ```javascript
378
+ * const provider = new Websocket('ws://127.0.0.1:9944');
379
+ * const rpc = new Rpc(provider);
380
+ *
381
+ * rpc.state.subscribeStorage([[storage.system.account, <Address>]], (_, values) => {
382
+ * console.log(values)
383
+ * }).then((subscriptionId) => {
384
+ * console.log('balance changes subscription id: ', subscriptionId)
385
+ * })
386
+ * ```
387
+ */
388
+ subscribe(type, method, params, callback) {
389
+ return this.send(method, params, false, {
390
+ callback,
391
+ type
392
+ });
393
+ }
394
+
395
+ /**
396
+ * @summary Allows unsubscribing to subscriptions made with [[subscribe]].
397
+ */
398
+ async unsubscribe(type, method, id) {
399
+ const subscription = `${type}::${id}`;
400
+
401
+ // FIXME This now could happen with re-subscriptions. The issue is that with a re-sub
402
+ // the assigned id now does not match what the API user originally received. It has
403
+ // a slight complication in solving - since we cannot rely on the send id, but rather
404
+ // need to find the actual subscription id to map it
405
+ if (util.isUndefined(this.#subscriptions[subscription])) {
406
+ // log.debug(() => `Unable to find active subscription=${subscription}`)
407
+
408
+ return false;
409
+ }
410
+ delete this.#subscriptions[subscription];
411
+ try {
412
+ return this.isConnected && !util.isNull(this.#websocket) ? this.send(method, [id]) : true;
413
+ } catch (error) {
414
+ return false;
415
+ }
416
+ }
417
+ #emit = (type, ...args) => {
418
+ this.#eventemitter.emit(type, ...args);
419
+ };
420
+ #onSocketClose = event => {
421
+ const error = new Error(`disconnected from ${this.endpoint}: ${event.code}:: ${event.reason || errors.getWSErrorString(event.code)}`);
422
+ if (this.#autoConnectBackoff.isActive) {
423
+ // 1000 is a normal closure and should not be logged as an error
424
+ if (event.code !== 1000) log.error(error.message);
425
+ }
426
+ this.#isConnected = false;
427
+ if (this.#websocket) {
428
+ this.#websocket.onclose = null;
429
+ this.#websocket.onerror = null;
430
+ this.#websocket.onmessage = null;
431
+ this.#websocket.onopen = null;
432
+ this.#websocket = null;
433
+ }
434
+ if (this.#timeoutId) {
435
+ clearInterval(this.#timeoutId);
436
+ this.#timeoutId = null;
437
+ }
438
+
439
+ // reject all hanging requests
440
+ eraseRecord(this.#handlers, h => {
441
+ try {
442
+ h.callback(error, undefined);
443
+ } catch (err) {
444
+ // does not throw
445
+ log.error(err);
446
+ }
447
+ });
448
+ eraseRecord(this.#waitingForId);
449
+ this.#emit("disconnected");
450
+ this.scheduleNextRetry();
451
+ };
452
+ #onSocketError = error => {
453
+ // log.debug(() => ["socket error", error])
454
+ this.#emit("error", error);
455
+ };
456
+ #onSocketMessage = message => {
457
+ // log.debug(() => ["received", message.data])
458
+ try {
459
+ const response = JSON.parse(message.data);
460
+ return util.isUndefined(response.method) ? this.#onSocketMessageResult(response) : this.#onSocketMessageSubscribe(response);
461
+ } catch (e) {
462
+ this.#emit("error", new Error("Invalid websocket message received", {
463
+ cause: e
464
+ }));
465
+ }
466
+ };
467
+ #onSocketMessageResult = response => {
468
+ const handler = this.#handlers[response.id];
469
+ if (!handler) {
470
+ // log.debug(() => `Unable to find handler for id=${response.id}`)
471
+
472
+ return;
473
+ }
474
+ try {
475
+ const {
476
+ method,
477
+ params,
478
+ subscription
479
+ } = handler;
480
+ const result = this.#coder.decodeResponse(response);
481
+
482
+ // first send the result - in case of subs, we may have an update
483
+ // immediately if we have some queued results already
484
+ handler.callback(null, result);
485
+ if (subscription) {
486
+ const subId = `${subscription.type}::${result}`;
487
+ this.#subscriptions[subId] = util.objectSpread({}, subscription, {
488
+ method,
489
+ params
490
+ });
491
+
492
+ // if we have a result waiting for this subscription already
493
+ if (this.#waitingForId[subId]) {
494
+ this.#onSocketMessageSubscribe(this.#waitingForId[subId]);
495
+ }
496
+ }
497
+ } catch (error) {
498
+ handler.callback(error, undefined);
499
+ }
500
+ delete this.#handlers[response.id];
501
+ };
502
+ #onSocketMessageSubscribe = response => {
503
+ const method = ALIASES[response.method] || response.method || "invalid";
504
+ const subId = `${method}::${response.params.subscription}`;
505
+ const handler = this.#subscriptions[subId];
506
+ if (!handler) {
507
+ // store the JSON, we could have out-of-order subid coming in
508
+ this.#waitingForId[subId] = response;
509
+
510
+ // log.debug(() => `Unable to find handler for subscription=${subId}`)
511
+
512
+ return;
513
+ }
514
+
515
+ // housekeeping
516
+ delete this.#waitingForId[subId];
517
+ try {
518
+ const result = this.#coder.decodeResponse(response);
519
+ handler.callback(null, result);
520
+ } catch (error) {
521
+ handler.callback(error, undefined);
522
+ }
523
+ };
524
+ #onSocketOpen = () => {
525
+ if (this.#websocket === null) {
526
+ throw new Error("WebSocket cannot be null in onOpen");
527
+ }
528
+
529
+ // log.debug(() => ["connected to", this.endpoint])
530
+
531
+ this.#isConnected = true;
532
+ this.#endpointsTriedSinceLastConnection = 0;
533
+ this.#autoConnectBackoff.reset();
534
+ this.#resubscribe();
535
+ this.#emit("connected");
536
+ return true;
537
+ };
538
+ #resubscribe = () => {
539
+ const subscriptions = this.#subscriptions;
540
+ this.#subscriptions = {};
541
+ Promise.all(Object.keys(subscriptions).map(async id => {
542
+ const {
543
+ callback,
544
+ method,
545
+ params,
546
+ type
547
+ } = subscriptions[id];
548
+
549
+ // only re-create subscriptions which are not in author (only area where
550
+ // transactions are created, i.e. submissions such as 'author_submitAndWatchExtrinsic'
551
+ // are not included (and will not be re-broadcast)
552
+ if (type.startsWith("author_")) {
553
+ return;
554
+ }
555
+ try {
556
+ await this.subscribe(type, method, params, callback);
557
+ } catch (error) {
558
+ log.error(error);
559
+ }
560
+ })).catch(log.error);
561
+ };
562
+ #timeoutHandlers = () => {
563
+ const now = Date.now();
564
+ const ids = Object.keys(this.#handlers);
565
+ for (let i = 0; i < ids.length; i++) {
566
+ const handler = this.#handlers[ids[i]];
567
+ if (now - handler.start > this.#timeout) {
568
+ try {
569
+ handler.callback(new Error(`No response received from RPC endpoint in ${this.#timeout / 1000}s`), undefined);
570
+ } catch {
571
+ // ignore
572
+ }
573
+ delete this.#handlers[ids[i]];
574
+ }
575
+ }
576
+ };
577
+ }
578
+
579
+ // errors that require an rpc fallback
580
+ // https://docs.blastapi.io/blast-documentation/things-you-need-to-know/error-reference
581
+ const BAD_RPC_ERRORS = {
582
+ "-32097": "Rate limit exceeded",
583
+ "-32098": "Capacity exceeded"
584
+ };
585
+ class ChainConnectionError extends Error {
586
+ constructor(chainId, options) {
587
+ super(`Unable to connect to chain ${chainId}`, options);
588
+ this.type = "CHAIN_CONNECTION_ERROR";
589
+ this.chainId = chainId;
590
+ }
591
+ }
592
+ class StaleRpcError extends Error {
593
+ constructor(chainId, options) {
594
+ super(`RPCs are stale/unavailable for chain ${chainId}`, options);
595
+ this.type = "STALE_RPC_ERROR";
596
+ this.chainId = chainId;
597
+ }
598
+ }
599
+ class WebsocketAllocationExhaustedError extends Error {
600
+ constructor(chainId, options) {
601
+ super(`No websockets are available from the browser pool to connect to chain ${chainId}`, options);
602
+ this.type = "WEBSOCKET_ALLOCATION_EXHAUSTED_ERROR";
603
+ this.chainId = chainId;
604
+ }
605
+ }
606
+ class CallerUnsubscribedError extends Error {
607
+ constructor(chainId, unsubscribeMethod, options) {
608
+ super(`Caller unsubscribed from ${chainId}`, options);
609
+ this.type = "CALLER_UNSUBSCRIBED_ERROR";
610
+ this.chainId = chainId;
611
+ this.unsubscribeMethod = unsubscribeMethod;
612
+ }
613
+ }
614
+ /**
615
+ * ChainConnector provides an interface similar to WsProvider, but with three points of difference:
616
+ *
617
+ * 1. ChainConnector methods all accept a `chainId` instead of an array of RPCs. RPCs are then fetched internally from chaindata.
618
+ * 2. ChainConnector creates only one `WsProvider` per chain and ensures that all downstream requests to a chain share the one socket connection.
619
+ * 3. Subscriptions return a callable `unsubscribe` method instead of an id.
620
+ *
621
+ * Additionally, when run on the clientside of a dapp where `window.talismanSub` is available, instead of spinning up new websocket
622
+ * connections this class will forward all requests through to the wallet backend - where another instance of this class will
623
+ * handle the websocket connections.
624
+ */
625
+ class ChainConnectorDot {
626
+ #chaindataChainProvider;
627
+ #connectionMetaDb;
628
+ #socketConnections = {};
629
+ #socketKeepAliveIntervals = {};
630
+ #socketUsers = {};
631
+ constructor(chaindataChainProvider, connectionMetaDb) {
632
+ this.#chaindataChainProvider = chaindataChainProvider;
633
+ this.#connectionMetaDb = connectionMetaDb;
634
+ if (this.#connectionMetaDb) {
635
+ this.#chaindataChainProvider.getNetworkIds("polkadot").then(chainIds => {
636
+ // tidy up connectionMeta for chains which no longer exist
637
+ this.#connectionMetaDb?.chainPriorityRpcs.where("id").noneOf(chainIds).delete();
638
+ this.#connectionMetaDb?.chainBackoffInterval.where("id").noneOf(chainIds).delete();
639
+ });
640
+ }
641
+ }
642
+
643
+ /**
644
+ * Creates a facade over this ChainConnector which conforms to the PJS ProviderInterface
645
+ * @example // Using a chainConnector as a Provider for an ApiPromise
646
+ * const provider = chainConnector.asProvider('polkadot')
647
+ * const api = new ApiPromise({ provider })
648
+ */
649
+ asProvider(chainId) {
650
+ const unsubHandler = new Map();
651
+ const providerFacade = {
652
+ hasSubscriptions: true,
653
+ isClonable: false,
654
+ isConnected: true,
655
+ clone: () => providerFacade,
656
+ connect: () => Promise.resolve(),
657
+ disconnect: () => Promise.resolve(),
658
+ on: () => () => {},
659
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
660
+ send: async (method, params, isCacheable) => await this.send(chainId, method, params, isCacheable),
661
+ subscribe: async (type, method, params, cb) => {
662
+ const unsubscribe = await this.subscribe(chainId, method, type, params, cb);
663
+ const subscriptionId = this.getExclusiveRandomId([...unsubHandler.keys()].map(Number)).toString();
664
+ unsubHandler.set(subscriptionId, unsubscribe);
665
+ return subscriptionId;
666
+ },
667
+ unsubscribe: async (_type, unsubscribeMethod, subscriptionId) => {
668
+ unsubHandler.get(subscriptionId)?.(unsubscribeMethod);
669
+ unsubHandler.delete(subscriptionId);
670
+ return true;
671
+ }
672
+ };
673
+ return providerFacade;
674
+ }
675
+
676
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
677
+ async send(chainId, method, params, isCacheable, extraOptions) {
678
+ const talismanSub = this.getTalismanSub();
679
+ if (talismanSub !== undefined) {
680
+ try {
681
+ const chain = await this.#chaindataChainProvider.getNetworkById(chainId, "polkadot");
682
+ if (!chain) throw new Error(`Chain ${chainId} not found in store`);
683
+ const {
684
+ genesisHash
685
+ } = chain;
686
+ if (typeof genesisHash !== "string") throw new Error(`Chain ${chainId} has no genesisHash in store`);
687
+ return await talismanSub.send(genesisHash, method, params);
688
+ } catch (error) {
689
+ log.warn(`Failed to make wallet-proxied send request for chain ${chainId}. Falling back to plain websocket`, error);
690
+ }
691
+ }
692
+ try {
693
+ // eslint-disable-next-line no-var
694
+ var [socketUserId, ws] = await this.connectChainSocket(chainId);
695
+ } catch (error) {
696
+ throw new StaleRpcError(chainId, {
697
+ cause: error
698
+ });
699
+ }
700
+ try {
701
+ // wait for ws to be ready, but don't wait forever
702
+ const timeout = 15_000; // 15 seconds in milliseconds
703
+ await this.waitForWs(ws, timeout);
704
+ } catch (error) {
705
+ await this.disconnectChainSocket(chainId, socketUserId);
706
+ throw new ChainConnectionError(chainId, {
707
+ cause: error
708
+ });
709
+ }
710
+ try {
711
+ const timeout = 30_000; // throw after 30 seconds if no response
712
+ // eslint-disable-next-line no-var
713
+ var response = await Promise.race([ws.send(method, params, isCacheable), util$1.throwAfter(timeout, "TIMEOUT")]);
714
+ } catch (err) {
715
+ const error = err;
716
+ if (error?.message === "TIMEOUT") {
717
+ log.error(`ChainConnector timeout`, {
718
+ chainId,
719
+ endpoint: ws.endpoint,
720
+ error
721
+ });
722
+ await this.updateRpcPriority(chainId, ws.endpoint, "last");
723
+ await this.reset(chainId);
724
+ throw new Error("Timeout");
725
+ }
726
+ const badRpcError = BAD_RPC_ERRORS[error?.code?.toString() ?? ""];
727
+ if (badRpcError) {
728
+ log.error(`ChainConnector ${badRpcError}`, {
729
+ error,
730
+ chainId,
731
+ endpoint: ws.endpoint
732
+ });
733
+ await this.updateRpcPriority(chainId, ws.endpoint, "last");
734
+ await this.reset(chainId);
735
+ throw new Error(badRpcError);
736
+ }
737
+ if (!extraOptions?.expectErrors) log.error(`Failed to send ${method} on chain ${chainId}\nparams: ${JSON.stringify(params)}`, {
738
+ error,
739
+ endpoint: ws.endpoint
740
+ });
741
+ await this.disconnectChainSocket(chainId, socketUserId);
742
+ throw error;
743
+ }
744
+ await this.disconnectChainSocket(chainId, socketUserId);
745
+ return response;
746
+ }
747
+ async subscribe(chainId, subscribeMethod, responseMethod, params, callback, timeout = 30_000 // 30 seconds in milliseconds
748
+ ) {
749
+ const talismanSub = this.getTalismanSub();
750
+ if (talismanSub !== undefined) {
751
+ try {
752
+ const chain = await this.#chaindataChainProvider.getNetworkById(chainId, "polkadot");
753
+ if (!chain) throw new Error(`Chain ${chainId} not found in store`);
754
+ const {
755
+ genesisHash
756
+ } = chain;
757
+ if (typeof genesisHash !== "string") throw new Error(`Chain ${chainId} has no genesisHash in store`);
758
+ const subscriptionId = await talismanSub.subscribe(genesisHash, subscribeMethod, responseMethod, params, callback, timeout);
759
+ return unsubscribeMethod => talismanSub.unsubscribe(subscriptionId, unsubscribeMethod);
760
+ } catch (error) {
761
+ log.warn(`Failed to create wallet-proxied subscription for chain ${chainId}. Falling back to plain websocket`, error);
762
+ }
763
+ }
764
+ try {
765
+ // eslint-disable-next-line no-var
766
+ var [socketUserId, ws] = await this.connectChainSocket(chainId);
767
+ } catch (error) {
768
+ throw new StaleRpcError(chainId, {
769
+ cause: error
770
+ });
771
+ }
772
+
773
+ // by using this `Deferred` promise
774
+ // (a promise which can be resolved or rejected by code outside of the scope of the promise's constructor)
775
+ // we can queue up our async cleanup on the promise and then immediately return an unsubscribe method to the caller
776
+ const unsubDeferred = util$1.Deferred();
777
+ // we return this to the caller so that they can let us know when they're no longer interested in this subscription
778
+ const unsubscribe = unsubscribeMethod => unsubDeferred.reject(new CallerUnsubscribedError(chainId, unsubscribeMethod));
779
+ // we queue up our work to clean up our subscription when this promise rejects
780
+ const callerUnsubscribed = unsubDeferred.promise;
781
+
782
+ // used to detect when there are no more websockets available from the browser websocket pool
783
+ // in this scenario, we'll be waiting for ws.isReady until some existing sockets are closed
784
+ //
785
+ // while we're waiting, we'll send an error back to the caller so that they can show some useful
786
+ // info to the user
787
+ let noMoreSocketsTimeout = undefined
788
+
789
+ // create subscription asynchronously so that the caller can unsubscribe without waiting for
790
+ // the subscription to be created (which can take some time if e.g. the connection can't be established)
791
+ ;
792
+ (async () => {
793
+ // wait for ws to be ready, but don't wait forever
794
+ // if timeout is number, cancel when timeout is reached (or caller unsubscribes)
795
+ // if timeout is false, only cancel when the caller unsubscribes
796
+ let unsubRpcStatus = null;
797
+ try {
798
+ const unsubStale = ws.on("stale-rpcs", ({
799
+ nextBackoffInterval
800
+ } = {}) => {
801
+ callback(new StaleRpcError(chainId), null);
802
+ if (this.#connectionMetaDb && nextBackoffInterval) {
803
+ const id = chainId;
804
+ this.#connectionMetaDb.chainBackoffInterval.put({
805
+ id,
806
+ interval: nextBackoffInterval
807
+ }, id);
808
+ }
809
+ });
810
+ const unsubConnected = ws.on("connected", () => {
811
+ if (this.#connectionMetaDb) this.#connectionMetaDb.chainBackoffInterval.delete(chainId);
812
+ });
813
+ unsubRpcStatus = () => {
814
+ unsubStale();
815
+ unsubConnected();
816
+ };
817
+ noMoreSocketsTimeout = setTimeout(() => callback(new WebsocketAllocationExhaustedError(chainId), null), 30_000 // 30 seconds in ms
818
+ );
819
+ if (timeout) await Promise.race([this.waitForWs(ws, timeout), callerUnsubscribed]);else await Promise.race([ws.isReady, callerUnsubscribed]);
820
+ clearTimeout(noMoreSocketsTimeout);
821
+ } catch (error) {
822
+ clearTimeout(noMoreSocketsTimeout);
823
+ unsubRpcStatus && unsubRpcStatus();
824
+ await this.disconnectChainSocket(chainId, socketUserId);
825
+ return;
826
+ }
827
+
828
+ // create subscription on ws
829
+ // handle the scenarios where the caller unsubscribes before the subscription has been created and:
830
+ // - the subscriptionId is already set
831
+ // - the subscriptionId is not set yet, but will be
832
+ let subscriptionId = null;
833
+ let disconnected = false;
834
+ let unsubscribeMethod = undefined;
835
+ try {
836
+ await Promise.race([ws.subscribe(responseMethod, subscribeMethod, params, callback).then(id => {
837
+ if (disconnected) {
838
+ unsubscribeMethod && ws.unsubscribe(responseMethod, unsubscribeMethod, id);
839
+ } else subscriptionId = id;
840
+ }), callerUnsubscribed]);
841
+ } catch (error) {
842
+ if (error instanceof CallerUnsubscribedError) unsubscribeMethod = error.unsubscribeMethod;
843
+ unsubRpcStatus && unsubRpcStatus();
844
+ disconnected = true;
845
+ if (subscriptionId !== null && unsubscribeMethod) await ws.unsubscribe(responseMethod, unsubscribeMethod, subscriptionId);
846
+ await this.disconnectChainSocket(chainId, socketUserId);
847
+ return;
848
+ }
849
+
850
+ // unsubscribe from ws subscription when the caller has unsubscribed
851
+ callerUnsubscribed.catch(async error => {
852
+ let unsubscribeMethod = undefined;
853
+ if (error instanceof CallerUnsubscribedError) unsubscribeMethod = error.unsubscribeMethod;
854
+ unsubRpcStatus && unsubRpcStatus();
855
+ if (subscriptionId !== null && unsubscribeMethod) await ws.unsubscribe(responseMethod, unsubscribeMethod, subscriptionId);
856
+ await this.disconnectChainSocket(chainId, socketUserId);
857
+ }).catch(error => log.warn(error));
858
+ })();
859
+ return unsubscribe;
860
+ }
861
+
862
+ /**
863
+ * Kills current websocket if any
864
+ * Useful after changing rpc order to make sure it's applied for futher requests
865
+ */
866
+ async reset(chainId) {
867
+ log.info("ChainConnector reset", chainId);
868
+ const ws = this.#socketConnections[chainId];
869
+ if (!ws) return;
870
+ try {
871
+ clearTimeout(this.#socketKeepAliveIntervals[chainId]);
872
+ delete this.#socketConnections[chainId];
873
+ delete this.#socketUsers[chainId];
874
+ await ws.disconnect();
875
+ } catch (error) {
876
+ log.warn(`Error occurred reseting socket ${chainId}`, error);
877
+ }
878
+ }
879
+
880
+ /**
881
+ * Wait for websocket to be ready, but don't wait forever
882
+ */
883
+ async waitForWs(ws, timeout = 30_000 // 30 seconds in milliseconds
884
+ ) {
885
+ const timer = timeout ? util$1.sleep(timeout).then(() => {
886
+ throw new Error(`RPC connect timeout reached: ${ws.endpoint}`);
887
+ }) : false;
888
+ await Promise.race([ws.isReady, timer].filter(util$1.isTruthy));
889
+ }
890
+
891
+ /**
892
+ * Connect to an RPC via chainId
893
+ *
894
+ * The caller must call disconnectChainSocket with the returned SocketUserId once they are finished with it
895
+ */
896
+ async connectChainSocket(chainId) {
897
+ const rpcs = await this.getEndpoints(chainId);
898
+ const socketUserId = this.addSocketUser(chainId);
899
+
900
+ // retrieve next rpc backoff interval from connection meta db (if one exists)
901
+ let nextBackoffInterval = undefined;
902
+ if (this.#connectionMetaDb) nextBackoffInterval = (await this.#connectionMetaDb.chainBackoffInterval.get(chainId))?.interval;
903
+
904
+ // NOTE: Make sure there are no calls to `await` between this check and the
905
+ // next step where we assign a `new Websocket` to `this.#socketConnections[chainId]`
906
+ //
907
+ // If there is an `await` between these two steps then there will be a race condition introduced.
908
+ // The result of this race condition will be the unnecessary creation of multiple instances of
909
+ // `Websocket` per chain, rather than the intended behaviour where every call to send/subscribe
910
+ // shares a single `Websocket` per chain.
911
+ if (this.#socketConnections[chainId]) return [socketUserId, this.#socketConnections[chainId]];
912
+ if (rpcs.length) this.#socketConnections[chainId] = new Websocket(rpcs, undefined, undefined, nextBackoffInterval);else {
913
+ throw new Error(`No healthy RPCs available for chain ${chainId}`);
914
+ }
915
+
916
+ // on ws connected event, store current rpc as most recently connected rpc
917
+ if (this.#connectionMetaDb) {
918
+ this.#socketConnections[chainId].on("connected", () => {
919
+ if (!this.#connectionMetaDb) return;
920
+ const id = chainId;
921
+ const url = this.#socketConnections[chainId]?.endpoint;
922
+ if (!url) return;
923
+ this.updateRpcPriority(id, url, "first").catch(err => log.warn(`updateRpcPriority failed`, err));
924
+ });
925
+ }
926
+ (async () => {
927
+ if (!this.#socketConnections[chainId]) return log.warn(`ignoring ${chainId} rpc ws healthcheck initialization: ws is not defined`);
928
+ await this.#socketConnections[chainId].isReady;
929
+ if (this.#socketKeepAliveIntervals[chainId]) clearInterval(this.#socketKeepAliveIntervals[chainId]);
930
+ const intervalMs = 10_000; // 10,000ms = 10s
931
+ this.#socketKeepAliveIntervals[chainId] = setInterval(() => {
932
+ if (!this.#socketConnections[chainId]) return log.warn(`skipping ${chainId} rpc ws healthcheck: ws is not defined`);
933
+ if (!this.#socketConnections[chainId].isConnected) return log.warn(`skipping ${chainId} rpc ws healthcheck: ws is not connected`);
934
+ this.#socketConnections[chainId].send("system_health", []).catch(error => log.warn(`Failed keep-alive for socket ${chainId}`, error));
935
+ }, intervalMs);
936
+ })();
937
+ return [socketUserId, this.#socketConnections[chainId]];
938
+ }
939
+ async disconnectChainSocket(chainId, socketUserId) {
940
+ this.removeSocketUser(chainId, socketUserId);
941
+ if (this.#socketUsers[chainId].length > 0) return;
942
+ if (!this.#socketConnections[chainId]) return log.warn(`Failed to disconnect socket: socket ${chainId} not found`);
943
+ try {
944
+ this.#socketConnections[chainId].disconnect();
945
+ } catch (error) {
946
+ log.warn(`Error occurred disconnecting socket ${chainId}`, error);
947
+ }
948
+ delete this.#socketConnections[chainId];
949
+ clearInterval(this.#socketKeepAliveIntervals[chainId]);
950
+ delete this.#socketKeepAliveIntervals[chainId];
951
+ }
952
+ addSocketUser(chainId) {
953
+ if (!Array.isArray(this.#socketUsers[chainId])) this.#socketUsers[chainId] = [];
954
+ const socketUserId = this.getExclusiveRandomId(this.#socketUsers[chainId]);
955
+ this.#socketUsers[chainId].push(socketUserId);
956
+ return socketUserId;
957
+ }
958
+ removeSocketUser(chainId, socketUserId) {
959
+ const userIndex = this.#socketUsers[chainId].indexOf(socketUserId);
960
+ if (userIndex === -1) throw new Error(`Can't remove user ${socketUserId} from socket ${chainId}: user not in list ${this.#socketUsers[chainId].join(", ")}`);
961
+ this.#socketUsers[chainId].splice(userIndex, 1);
962
+ }
963
+
964
+ /** continues to generate a random number until it finds one which is not present in the exclude list */
965
+ getExclusiveRandomId(exclude = []) {
966
+ let id = this.getRandomId();
967
+ while (exclude.includes(id)) {
968
+ id = this.getRandomId();
969
+ }
970
+ return id;
971
+ }
972
+ /** generates a random number */
973
+ getRandomId() {
974
+ return Math.trunc(Math.random() * Math.pow(10, 8));
975
+ }
976
+ getTalismanSub() {
977
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
978
+ const talismanSub = typeof window !== "undefined" && window.talismanSub;
979
+
980
+ /* eslint-disable @typescript-eslint/no-unsafe-function-type */
981
+ const rpcByGenesisHashSend = talismanSub?.rpcByGenesisHashSend;
982
+ const rpcByGenesisHashSubscribe = talismanSub?.rpcByGenesisHashSubscribe;
983
+ const rpcByGenesisHashUnsubscribe = talismanSub?.rpcByGenesisHashUnsubscribe;
984
+ if (typeof rpcByGenesisHashSend !== "function") return;
985
+ if (typeof rpcByGenesisHashSubscribe !== "function") return;
986
+ if (typeof rpcByGenesisHashUnsubscribe !== "function") return;
987
+ return {
988
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
989
+ send: (genesisHash, method, params) => rpcByGenesisHashSend(genesisHash, method, params),
990
+ subscribe: (genesisHash, subscribeMethod, responseMethod, params, callback, timeout) => rpcByGenesisHashSubscribe(genesisHash, subscribeMethod, responseMethod, params, callback, timeout),
991
+ unsubscribe: (subscriptionId, unsubscribeMethod) => rpcByGenesisHashUnsubscribe(subscriptionId, unsubscribeMethod)
992
+ };
993
+ }
994
+ async updateRpcPriority(chainId, rpc, priority) {
995
+ if (!this.#connectionMetaDb) return;
996
+ const rpcs = await this.getEndpoints(chainId);
997
+ if (!rpcs.includes(rpc)) throw new Error(`Unknown rpc for chain ${chainId} : ${rpc}`);
998
+ const urls = rpcs.filter(r => r !== rpc);
999
+ if (priority === "first") urls.unshift(rpc);
1000
+ if (priority === "last") urls.push(rpc);
1001
+ if (!isEqual(urls, rpcs)) {
1002
+ // order may not change, especially if there is only one
1003
+ await this.#connectionMetaDb.chainPriorityRpcs.put({
1004
+ id: chainId,
1005
+ urls
1006
+ }, chainId);
1007
+ }
1008
+ }
1009
+ async getEndpoints(chainId) {
1010
+ const chain = await this.#chaindataChainProvider.getNetworkById(chainId, "polkadot");
1011
+ if (!chain) throw new Error(`Chain ${chainId} not found in store`);
1012
+ let rpcs = chain.rpcs.concat(); // clone to avoid mutating the original array
1013
+ const priorityRpcs = this.#connectionMetaDb ? await this.#connectionMetaDb.chainPriorityRpcs.get(chainId) : undefined;
1014
+ if (priorityRpcs) {
1015
+ // use existing priority list of rpcs that still exist, and include missing ones
1016
+ rpcs = [...priorityRpcs.urls.filter(rpc => rpcs.includes(rpc)), ...rpcs.filter(rpc => !priorityRpcs.urls.includes(rpc))];
1017
+ }
1018
+ return rpcs;
1019
+ }
1020
+ }
1021
+ const isEqual = (a, b) => a.length === b.length && a.every((v, i) => v === b[i]);
1022
+
1023
+ const AUTO_CONNECT_TIMEOUT = 3_000;
1024
+ const TIMEOUT = 10_000;
1025
+ class ChainConnectorDotStub {
1026
+ #network;
1027
+ #provider;
1028
+ constructor(network) {
1029
+ this.#network = network;
1030
+ this.#provider = new rpcProvider.WsProvider(network.rpcs, AUTO_CONNECT_TIMEOUT, undefined, TIMEOUT);
1031
+ }
1032
+ asProvider() {
1033
+ return this.#provider;
1034
+ }
1035
+ async send(chainId, method, params, isCacheable) {
1036
+ await this.#provider.isReady;
1037
+ return this.#provider.send(method, params, isCacheable);
1038
+ }
1039
+ async subscribe(chainId, subscribeMethod, responseMethod, params, callback, timeout) {
1040
+ await this.#provider.isReady;
1041
+ const subId = await Promise.race([util$1.throwAfter(timeout || TIMEOUT, `Subscription timed out after ${timeout}ms`), this.#provider.subscribe(responseMethod, subscribeMethod, params, callback)]);
1042
+ return unsubscribeMethod => {
1043
+ this.#provider.unsubscribe(responseMethod, unsubscribeMethod, subId);
1044
+ };
1045
+ }
1046
+ reset() {
1047
+ throw new Error("ChainConnectorDotStub does not implement reset");
1048
+ }
1049
+ }
1050
+
1051
+ // viem chains benefit from multicall config & other viem goodies
1052
+ const VIEM_CHAINS = Object.keys(chains__namespace).reduce((acc, curr) => {
1053
+ const chain = chains__namespace[curr];
1054
+ acc[chain.id] = chain;
1055
+ return acc;
1056
+ }, {});
1057
+ const chainsCache = new Map();
1058
+ const clearChainsCache = networkId => {
1059
+ if (networkId) chainsCache.delete(networkId);else chainsCache.clear();
1060
+ };
1061
+ const getChainFromEvmNetwork = network => {
1062
+ const {
1063
+ symbol,
1064
+ decimals
1065
+ } = network.nativeCurrency;
1066
+ if (!chainsCache.has(network.id)) {
1067
+ const chainRpcs = network.rpcs ?? [];
1068
+ const viemChain = VIEM_CHAINS[Number(network.id)] ?? {};
1069
+ const chain = {
1070
+ ...viemChain,
1071
+ id: Number(network.id),
1072
+ name: network.name ?? `Ethereum Chain ${network.id}`,
1073
+ rpcUrls: {
1074
+ public: {
1075
+ http: chainRpcs
1076
+ },
1077
+ default: {
1078
+ http: chainRpcs
1079
+ }
1080
+ },
1081
+ nativeCurrency: {
1082
+ symbol,
1083
+ decimals,
1084
+ name: symbol
1085
+ },
1086
+ contracts: {
1087
+ ...viemChain.contracts,
1088
+ ...(network.contracts ? lodashEs.fromPairs(lodashEs.toPairs(network.contracts).map(([name, address]) => [lodashEs.camelCase(name), {
1089
+ address
1090
+ }])) : {})
1091
+ }
1092
+ };
1093
+ chainsCache.set(network.id, chain);
1094
+ }
1095
+ return chainsCache.get(network.id);
1096
+ };
1097
+
1098
+ const getTransportForEvmNetwork = (evmNetwork, options = {}) => {
1099
+ if (!evmNetwork.rpcs?.length) throw new Error("No RPCs found for EVM network");
1100
+ const {
1101
+ batch
1102
+ } = options;
1103
+ return viem.fallback(evmNetwork.rpcs.map(url => viem.http(url, {
1104
+ batch,
1105
+ retryCount: 0
1106
+ })), {
1107
+ retryCount: 0
1108
+ });
1109
+ };
1110
+
1111
+ const MUTLICALL_BATCH_WAIT = 25;
1112
+ const MUTLICALL_BATCH_SIZE = 100;
1113
+ const HTTP_BATCH_WAIT = 25;
1114
+ const HTTP_BATCH_SIZE_WITH_MULTICALL = 10;
1115
+ const HTTP_BATCH_SIZE_WITHOUT_MULTICALL = 30;
1116
+
1117
+ // cache to reuse previously created public clients
1118
+ const publicClientCache = new Map();
1119
+ const clearPublicClientCache = evmNetworkId => {
1120
+ clearChainsCache(evmNetworkId);
1121
+ if (evmNetworkId) publicClientCache.delete(evmNetworkId);else publicClientCache.clear();
1122
+ };
1123
+ const getEvmNetworkPublicClient = network => {
1124
+ const chain = getChainFromEvmNetwork(network);
1125
+ if (!publicClientCache.has(network.id)) {
1126
+ if (!network.rpcs.length) throw new Error("No RPCs found for Ethereum network");
1127
+ const batch = chain.contracts?.multicall3 ? {
1128
+ multicall: {
1129
+ wait: MUTLICALL_BATCH_WAIT,
1130
+ batchSize: MUTLICALL_BATCH_SIZE
1131
+ }
1132
+ } : undefined;
1133
+ const transportOptions = {
1134
+ batch: {
1135
+ batchSize: chain.contracts?.multicall3 ? HTTP_BATCH_SIZE_WITH_MULTICALL : HTTP_BATCH_SIZE_WITHOUT_MULTICALL,
1136
+ wait: HTTP_BATCH_WAIT
1137
+ }
1138
+ };
1139
+ const transport = getTransportForEvmNetwork(network, transportOptions);
1140
+ publicClientCache.set(network.id, viem.createPublicClient({
1141
+ chain,
1142
+ transport,
1143
+ batch
1144
+ }));
1145
+ }
1146
+ return publicClientCache.get(network.id);
1147
+ };
1148
+
1149
+ const getEvmNetworkWalletClient = (network, options = {}) => {
1150
+ const chain = getChainFromEvmNetwork(network);
1151
+ const transport = getTransportForEvmNetwork(network);
1152
+ return viem.createWalletClient({
1153
+ chain,
1154
+ transport,
1155
+ account: options.account
1156
+ });
1157
+ };
1158
+
1159
+ class ChainConnectorEth {
1160
+ #chaindataProvider;
1161
+ constructor(chaindataProvider) {
1162
+ this.#chaindataProvider = chaindataProvider;
1163
+ }
1164
+ async getPublicClientForEvmNetwork(evmNetworkId) {
1165
+ const network = await this.#chaindataProvider.getNetworkById(evmNetworkId, "ethereum");
1166
+ if (!network) return null;
1167
+ return getEvmNetworkPublicClient(network);
1168
+ }
1169
+ async getWalletClientForEvmNetwork(evmNetworkId, account) {
1170
+ const network = await this.#chaindataProvider.getNetworkById(evmNetworkId, "ethereum");
1171
+ if (!network) return null;
1172
+ return getEvmNetworkWalletClient(network, {
1173
+ account
1174
+ });
1175
+ }
1176
+ clearRpcProvidersCache(evmNetworkId) {
1177
+ clearPublicClientCache(evmNetworkId);
1178
+ }
1179
+ }
1180
+
1181
+ class ChainConnectorEthStub {
1182
+ #network;
1183
+ constructor(network) {
1184
+ this.#network = network;
1185
+ }
1186
+ async getPublicClientForEvmNetwork() {
1187
+ return getEvmNetworkPublicClient(this.#network);
1188
+ }
1189
+ async getWalletClientForEvmNetwork(networkId, account) {
1190
+ return getEvmNetworkWalletClient(this.#network, {
1191
+ account
1192
+ });
1193
+ }
1194
+ clearRpcProvidersCache() {
1195
+ // No-op for stub
1196
+ }
1197
+ }
1198
+
1199
+ // TODO
1200
+ const getSolConnection = (_networkId, _rpcs) => {
1201
+ return new web3_js.Connection("https://solana-mainnet.g.alchemy.com/v2/FlflUnY6iZ98J9likA0ZdLSMfa6SqMya", {
1202
+ commitment: "confirmed"
1203
+ });
1204
+ };
1205
+
1206
+ class ChainConnectorSol {
1207
+ #chaindataProvider;
1208
+ constructor(chaindataProvider) {
1209
+ this.#chaindataProvider = chaindataProvider;
1210
+ }
1211
+ async getConnection(networkId) {
1212
+ const network = await this.#chaindataProvider.getNetworkById(networkId, "solana");
1213
+ if (!network) throw new Error(`Network not found: ${networkId}`);
1214
+ return getSolConnection(networkId, network.rpcs);
1215
+ }
1216
+ }
1217
+
1218
+ class ChainConnectorSolStub {
1219
+ #connection;
1220
+ constructor(network) {
1221
+ this.#connection = getSolConnection(network.id, network.rpcs);
1222
+ }
1223
+ async getConnection() {
1224
+ return this.#connection;
1225
+ }
1226
+ }
1227
+
1228
+ exports.ChainConnectionError = ChainConnectionError;
1229
+ exports.ChainConnectorDot = ChainConnectorDot;
1230
+ exports.ChainConnectorDotStub = ChainConnectorDotStub;
1231
+ exports.ChainConnectorEth = ChainConnectorEth;
1232
+ exports.ChainConnectorEthStub = ChainConnectorEthStub;
1233
+ exports.ChainConnectorSol = ChainConnectorSol;
1234
+ exports.ChainConnectorSolStub = ChainConnectorSolStub;
1235
+ exports.StaleRpcError = StaleRpcError;
1236
+ exports.WebsocketAllocationExhaustedError = WebsocketAllocationExhaustedError;