@talismn/chain-connectors 0.1.1 → 1.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.
- package/LICENSE +253 -674
- package/dist/index.d.mts +117 -106
- package/dist/index.d.mts.map +1 -0
- package/dist/index.d.ts +117 -106
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +617 -1190
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +590 -1152
- package/dist/index.mjs.map +1 -1
- package/package.json +9 -17
package/dist/index.mjs
CHANGED
|
@@ -1,1178 +1,616 @@
|
|
|
1
|
-
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
// src/log.ts
|
|
1
|
+
import { createClient } from "@polkadot-api/substrate-client";
|
|
2
|
+
import { WsEvent, getWsProvider } from "@polkadot-api/ws-provider";
|
|
5
3
|
import anylogger from "anylogger";
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
type: "git",
|
|
20
|
-
url: "https://github.com/TalismanSociety/talisman.git"
|
|
21
|
-
},
|
|
22
|
-
main: "./dist/index.js",
|
|
23
|
-
module: "./dist/index.mjs",
|
|
24
|
-
files: [
|
|
25
|
-
"dist"
|
|
26
|
-
],
|
|
27
|
-
engines: {
|
|
28
|
-
node: ">=20"
|
|
29
|
-
},
|
|
30
|
-
scripts: {
|
|
31
|
-
test: "vitest run",
|
|
32
|
-
clean: "rm -rf dist .turbo node_modules",
|
|
33
|
-
build: "tsup --silent",
|
|
34
|
-
typecheck: "tsc --noEmit"
|
|
35
|
-
},
|
|
36
|
-
dependencies: {
|
|
37
|
-
"@solana/web3.js": "^1.98.2",
|
|
38
|
-
"@talismn/chaindata-provider": "workspace:*",
|
|
39
|
-
"@talismn/connection-meta": "workspace:*",
|
|
40
|
-
"@talismn/util": "workspace:*",
|
|
41
|
-
anylogger: "^1.0.11",
|
|
42
|
-
eventemitter3: "^5.0.0",
|
|
43
|
-
"lodash-es": "4.18.1",
|
|
44
|
-
viem: "2.52.2"
|
|
45
|
-
},
|
|
46
|
-
devDependencies: {
|
|
47
|
-
"@polkadot/rpc-provider": "16.1.2",
|
|
48
|
-
"@polkadot/x-global": "13.5.3",
|
|
49
|
-
"@polkadot/x-ws": "13.5.3",
|
|
50
|
-
"@talismn/tsconfig": "workspace:*",
|
|
51
|
-
typescript: "^6.0.3"
|
|
52
|
-
},
|
|
53
|
-
peerDependencies: {
|
|
54
|
-
"@polkadot/rpc-provider": "*",
|
|
55
|
-
"@polkadot/x-global": "*",
|
|
56
|
-
"@polkadot/x-ws": "*"
|
|
57
|
-
},
|
|
58
|
-
types: "./dist/index.d.ts",
|
|
59
|
-
exports: {
|
|
60
|
-
".": {
|
|
61
|
-
"@talismn/source": "./src/index.ts",
|
|
62
|
-
import: {
|
|
63
|
-
types: "./dist/index.d.mts",
|
|
64
|
-
default: "./dist/index.mjs"
|
|
65
|
-
},
|
|
66
|
-
require: {
|
|
67
|
-
types: "./dist/index.d.ts",
|
|
68
|
-
default: "./dist/index.js"
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
};
|
|
73
|
-
|
|
74
|
-
// src/log.ts
|
|
75
|
-
var log_default = anylogger(package_default.name);
|
|
76
|
-
|
|
77
|
-
// src/dot/Websocket.ts
|
|
78
|
-
import { RpcCoder } from "@polkadot/rpc-provider/coder";
|
|
79
|
-
import { getWSErrorString } from "@polkadot/rpc-provider/ws/errors";
|
|
80
|
-
import { xglobal } from "@polkadot/x-global";
|
|
81
|
-
import { WebSocket } from "@polkadot/x-ws";
|
|
82
|
-
import EventEmitter from "eventemitter3";
|
|
83
|
-
|
|
84
|
-
// src/dot/helpers.ts
|
|
85
|
-
var twoSecondsMs = 2 * 1e3;
|
|
86
|
-
var twoMinutesMs = 2 * 60 * 1e3;
|
|
87
|
-
var ExponentialBackoff = class {
|
|
88
|
-
#minInterval;
|
|
89
|
-
#maxInterval;
|
|
90
|
-
#nextInterval = 0;
|
|
91
|
-
#active = true;
|
|
92
|
-
constructor(maxIntervalMs = twoMinutesMs, minIntervalMs = twoSecondsMs) {
|
|
93
|
-
this.#minInterval = minIntervalMs;
|
|
94
|
-
this.#maxInterval = maxIntervalMs;
|
|
95
|
-
this.reset();
|
|
96
|
-
}
|
|
97
|
-
enable() {
|
|
98
|
-
this.#active = true;
|
|
99
|
-
}
|
|
100
|
-
disable() {
|
|
101
|
-
this.#active = false;
|
|
102
|
-
}
|
|
103
|
-
increase() {
|
|
104
|
-
if (this.#nextInterval === 0) this.#nextInterval = 1;
|
|
105
|
-
this.#nextInterval = this.#capMax(this.#capMin(this.#nextInterval * 2));
|
|
106
|
-
}
|
|
107
|
-
decrease() {
|
|
108
|
-
this.#nextInterval = this.#capMax(this.#capMin(this.#nextInterval / 2));
|
|
109
|
-
}
|
|
110
|
-
reset() {
|
|
111
|
-
this.#nextInterval = this.#minInterval;
|
|
112
|
-
}
|
|
113
|
-
resetTo(nextInterval) {
|
|
114
|
-
this.#nextInterval = this.#capMax(this.#capMin(nextInterval));
|
|
115
|
-
}
|
|
116
|
-
resetToMax() {
|
|
117
|
-
this.#nextInterval = this.#maxInterval;
|
|
118
|
-
}
|
|
119
|
-
get isActive() {
|
|
120
|
-
return this.#active;
|
|
121
|
-
}
|
|
122
|
-
get next() {
|
|
123
|
-
return this.#nextInterval;
|
|
124
|
-
}
|
|
125
|
-
get isMin() {
|
|
126
|
-
return this.#nextInterval === this.#minInterval;
|
|
127
|
-
}
|
|
128
|
-
get isMax() {
|
|
129
|
-
return this.#nextInterval === this.#maxInterval;
|
|
130
|
-
}
|
|
131
|
-
#capMin(value) {
|
|
132
|
-
return Math.max(this.#minInterval, value);
|
|
133
|
-
}
|
|
134
|
-
#capMax(value) {
|
|
135
|
-
return Math.min(this.#maxInterval, value);
|
|
136
|
-
}
|
|
137
|
-
};
|
|
138
|
-
|
|
139
|
-
// src/dot/Websocket.ts
|
|
140
|
-
var isChildClass = (Parent, Child) => Child != null && (Child === Parent || Child.prototype instanceof Parent);
|
|
141
|
-
var ALIASES = {
|
|
142
|
-
chain_finalisedHead: "chain_finalizedHead",
|
|
143
|
-
chain_subscribeFinalisedHeads: "chain_subscribeFinalizedHeads",
|
|
144
|
-
chain_unsubscribeFinalisedHeads: "chain_unsubscribeFinalizedHeads"
|
|
145
|
-
};
|
|
146
|
-
var DEFAULT_TIMEOUT_MS = 60 * 1e3;
|
|
147
|
-
var TIMEOUT_INTERVAL = 5e3;
|
|
148
|
-
function eraseRecord(record, cb) {
|
|
149
|
-
Object.keys(record).forEach((key) => {
|
|
150
|
-
if (cb) {
|
|
151
|
-
cb(record[key]);
|
|
152
|
-
}
|
|
153
|
-
delete record[key];
|
|
154
|
-
});
|
|
155
|
-
}
|
|
156
|
-
var Websocket = class _Websocket {
|
|
157
|
-
#coder;
|
|
158
|
-
#endpoints;
|
|
159
|
-
#headers;
|
|
160
|
-
#eventemitter;
|
|
161
|
-
#handlers = {};
|
|
162
|
-
#isReadyPromise;
|
|
163
|
-
#waitingForId = {};
|
|
164
|
-
#autoConnectBackoff;
|
|
165
|
-
#endpointIndex;
|
|
166
|
-
#endpointsTriedSinceLastConnection = 0;
|
|
167
|
-
#isConnected = false;
|
|
168
|
-
#subscriptions = {};
|
|
169
|
-
#timeoutId = null;
|
|
170
|
-
#websocket;
|
|
171
|
-
#timeout;
|
|
172
|
-
/**
|
|
173
|
-
* @param {string | string[]} endpoint The endpoint url. Usually `ws://ip:9944` or `wss://ip:9944`, may provide an array of endpoint strings.
|
|
174
|
-
* @param {Record<string, string>} headers The headers provided to the underlying WebSocket
|
|
175
|
-
* @param {number} [timeout] Custom timeout value used per request . Defaults to `DEFAULT_TIMEOUT_MS`
|
|
176
|
-
*/
|
|
177
|
-
constructor(endpoint, headers = {}, timeout, nextBackoffInterval) {
|
|
178
|
-
const endpoints = Array.isArray(endpoint) ? endpoint : [endpoint];
|
|
179
|
-
if (endpoints.length === 0) {
|
|
180
|
-
throw new Error("Websocket requires at least one Endpoint");
|
|
181
|
-
}
|
|
182
|
-
endpoints.forEach((endpoint2) => {
|
|
183
|
-
if (!/^(wss|ws):\/\//.test(endpoint2)) {
|
|
184
|
-
throw new Error(`Endpoint should start with 'ws://', received '${endpoint2}'`);
|
|
185
|
-
}
|
|
186
|
-
});
|
|
187
|
-
this.#eventemitter = new EventEmitter();
|
|
188
|
-
this.#autoConnectBackoff = new ExponentialBackoff();
|
|
189
|
-
if (nextBackoffInterval) this.#autoConnectBackoff.resetTo(nextBackoffInterval);
|
|
190
|
-
this.#coder = new RpcCoder();
|
|
191
|
-
this.#endpointIndex = -1;
|
|
192
|
-
this.#endpoints = endpoints;
|
|
193
|
-
this.#headers = headers;
|
|
194
|
-
this.#websocket = null;
|
|
195
|
-
this.#timeout = timeout || DEFAULT_TIMEOUT_MS;
|
|
196
|
-
if (this.#autoConnectBackoff.isActive) {
|
|
197
|
-
this.connectWithRetry().catch(() => {
|
|
198
|
-
});
|
|
199
|
-
}
|
|
200
|
-
this.#isReadyPromise = new Promise((resolve) => {
|
|
201
|
-
this.#eventemitter.once("connected", () => {
|
|
202
|
-
resolve(this);
|
|
203
|
-
});
|
|
204
|
-
});
|
|
205
|
-
}
|
|
206
|
-
/**
|
|
207
|
-
* @summary `true` when this provider supports subscriptions
|
|
208
|
-
*/
|
|
209
|
-
get hasSubscriptions() {
|
|
210
|
-
return true;
|
|
211
|
-
}
|
|
212
|
-
/**
|
|
213
|
-
* @summary `true` when this provider supports clone()
|
|
214
|
-
*/
|
|
215
|
-
get isClonable() {
|
|
216
|
-
return true;
|
|
217
|
-
}
|
|
218
|
-
/**
|
|
219
|
-
* @summary Whether the node is connected or not.
|
|
220
|
-
* @return {boolean} true if connected
|
|
221
|
-
*/
|
|
222
|
-
get isConnected() {
|
|
223
|
-
return this.#isConnected;
|
|
224
|
-
}
|
|
225
|
-
/**
|
|
226
|
-
* @description Promise that resolves the first time we are connected and loaded
|
|
227
|
-
*/
|
|
228
|
-
get isReady() {
|
|
229
|
-
return this.#isReadyPromise;
|
|
230
|
-
}
|
|
231
|
-
get endpoint() {
|
|
232
|
-
return this.#endpoints[this.#endpointIndex];
|
|
233
|
-
}
|
|
234
|
-
/**
|
|
235
|
-
* @description Returns a clone of the object
|
|
236
|
-
*/
|
|
237
|
-
clone() {
|
|
238
|
-
return new _Websocket(this.#endpoints);
|
|
239
|
-
}
|
|
240
|
-
selectEndpointIndex(endpoints) {
|
|
241
|
-
this.#endpointsTriedSinceLastConnection += 1;
|
|
242
|
-
return (this.#endpointIndex + 1) % endpoints.length;
|
|
243
|
-
}
|
|
244
|
-
/**
|
|
245
|
-
* @summary Manually connect
|
|
246
|
-
* @description The [[Websocket]] connects automatically by default, however if you decided otherwise, you may
|
|
247
|
-
* connect manually using this method.
|
|
248
|
-
*/
|
|
249
|
-
async connect() {
|
|
250
|
-
if (this.#websocket) {
|
|
251
|
-
throw new Error("WebSocket is already connected");
|
|
252
|
-
}
|
|
253
|
-
try {
|
|
254
|
-
this.#endpointIndex = this.selectEndpointIndex(this.#endpoints);
|
|
255
|
-
this.#websocket = typeof xglobal.WebSocket !== "undefined" && isChildClass(xglobal.WebSocket, WebSocket) ? new WebSocket(this.endpoint) : (
|
|
256
|
-
// @ts-expect-error - WS may be an instance of ws, which supports options
|
|
257
|
-
new WebSocket(this.endpoint, void 0, {
|
|
258
|
-
headers: this.#headers
|
|
259
|
-
})
|
|
260
|
-
);
|
|
261
|
-
if (this.#websocket) {
|
|
262
|
-
this.#websocket.onclose = this.#onSocketClose;
|
|
263
|
-
this.#websocket.onerror = this.#onSocketError;
|
|
264
|
-
this.#websocket.onmessage = this.#onSocketMessage;
|
|
265
|
-
this.#websocket.onopen = this.#onSocketOpen;
|
|
266
|
-
}
|
|
267
|
-
this.#timeoutId = setInterval(() => this.#timeoutHandlers(), TIMEOUT_INTERVAL);
|
|
268
|
-
} catch (error) {
|
|
269
|
-
log_default.error(error);
|
|
270
|
-
this.#emit("error", error);
|
|
271
|
-
throw error;
|
|
272
|
-
}
|
|
273
|
-
}
|
|
274
|
-
/**
|
|
275
|
-
* @description Connect, never throwing an error, but rather forcing a retry
|
|
276
|
-
*/
|
|
277
|
-
async connectWithRetry() {
|
|
278
|
-
if (!this.#autoConnectBackoff.isActive) return;
|
|
279
|
-
try {
|
|
280
|
-
await this.connect();
|
|
281
|
-
} catch {
|
|
282
|
-
this.scheduleNextRetry();
|
|
283
|
-
}
|
|
284
|
-
}
|
|
285
|
-
scheduleNextRetry() {
|
|
286
|
-
if (!this.#autoConnectBackoff.isActive) return;
|
|
287
|
-
const haveTriedAllEndpoints = this.#endpointsTriedSinceLastConnection > 0 && this.#endpointsTriedSinceLastConnection % this.#endpoints.length === 0;
|
|
288
|
-
setTimeout(
|
|
289
|
-
() => {
|
|
290
|
-
this.connectWithRetry().catch(() => {
|
|
291
|
-
});
|
|
292
|
-
},
|
|
293
|
-
haveTriedAllEndpoints ? this.#autoConnectBackoff.next : 0
|
|
294
|
-
);
|
|
295
|
-
if (haveTriedAllEndpoints) this.#autoConnectBackoff.increase();
|
|
296
|
-
if (haveTriedAllEndpoints)
|
|
297
|
-
this.#emit("stale-rpcs", { nextBackoffInterval: this.#autoConnectBackoff.next });
|
|
298
|
-
}
|
|
299
|
-
/**
|
|
300
|
-
* @description Manually disconnect from the connection, clearing auto-connect logic
|
|
301
|
-
*/
|
|
302
|
-
async disconnect() {
|
|
303
|
-
this.#autoConnectBackoff.disable();
|
|
304
|
-
try {
|
|
305
|
-
if (this.#websocket) {
|
|
306
|
-
this.#websocket.close(1e3);
|
|
307
|
-
}
|
|
308
|
-
} catch (error) {
|
|
309
|
-
log_default.error(error);
|
|
310
|
-
this.#emit("error", error);
|
|
311
|
-
throw error;
|
|
312
|
-
}
|
|
313
|
-
}
|
|
314
|
-
/**
|
|
315
|
-
* @summary Listens on events after having subscribed using the [[subscribe]] function.
|
|
316
|
-
* @param {ProviderInterfaceEmitted} type Event
|
|
317
|
-
* @param {ProviderInterfaceEmitCb} sub Callback
|
|
318
|
-
* @return unsubscribe function
|
|
319
|
-
*/
|
|
320
|
-
on(type, sub) {
|
|
321
|
-
this.#eventemitter.on(type, sub);
|
|
322
|
-
return () => {
|
|
323
|
-
this.#eventemitter.removeListener(type, sub);
|
|
324
|
-
};
|
|
325
|
-
}
|
|
326
|
-
/**
|
|
327
|
-
* @summary Send JSON data using WebSockets to configured HTTP Endpoint or queue.
|
|
328
|
-
* @param method The RPC methods to execute
|
|
329
|
-
* @param params Encoded parameters as applicable for the method
|
|
330
|
-
* @param subscription Subscription details (internally used)
|
|
331
|
-
*/
|
|
332
|
-
// biome-ignore lint/suspicious/noExplicitAny: legacy
|
|
333
|
-
send(method, params, _isCacheable, subscription) {
|
|
334
|
-
const [id, body] = this.#coder.encodeJson(method, params);
|
|
335
|
-
const resultPromise = this.#send(id, body, method, params, subscription);
|
|
336
|
-
return resultPromise;
|
|
337
|
-
}
|
|
338
|
-
async #send(id, body, method, params, subscription) {
|
|
339
|
-
return new Promise((resolve, reject) => {
|
|
340
|
-
try {
|
|
341
|
-
if (!this.isConnected || this.#websocket === null) {
|
|
342
|
-
throw new Error("WebSocket is not connected");
|
|
343
|
-
}
|
|
344
|
-
const callback = (error, result) => {
|
|
345
|
-
error ? reject(error) : resolve(result);
|
|
346
|
-
};
|
|
347
|
-
this.#handlers[id] = {
|
|
348
|
-
callback,
|
|
349
|
-
method,
|
|
350
|
-
params,
|
|
351
|
-
start: Date.now(),
|
|
352
|
-
subscription
|
|
353
|
-
};
|
|
354
|
-
this.#websocket.send(body);
|
|
355
|
-
} catch (error) {
|
|
356
|
-
reject(error);
|
|
357
|
-
}
|
|
358
|
-
});
|
|
359
|
-
}
|
|
360
|
-
/**
|
|
361
|
-
* @name subscribe
|
|
362
|
-
* @summary Allows subscribing to a specific event.
|
|
363
|
-
*
|
|
364
|
-
* @example
|
|
365
|
-
* <BR>
|
|
366
|
-
*
|
|
367
|
-
* ```javascript
|
|
368
|
-
* const provider = new Websocket('ws://127.0.0.1:9944');
|
|
369
|
-
* const rpc = new Rpc(provider);
|
|
370
|
-
*
|
|
371
|
-
* rpc.state.subscribeStorage([[storage.system.account, <Address>]], (_, values) => {
|
|
372
|
-
* console.log(values)
|
|
373
|
-
* }).then((subscriptionId) => {
|
|
374
|
-
* console.log('balance changes subscription id: ', subscriptionId)
|
|
375
|
-
* })
|
|
376
|
-
* ```
|
|
377
|
-
*/
|
|
378
|
-
subscribe(type, method, params, callback) {
|
|
379
|
-
return this.send(method, params, false, { callback, type });
|
|
380
|
-
}
|
|
381
|
-
/**
|
|
382
|
-
* @summary Allows unsubscribing to subscriptions made with [[subscribe]].
|
|
383
|
-
*/
|
|
384
|
-
async unsubscribe(type, method, id) {
|
|
385
|
-
const subscription = `${type}::${id}`;
|
|
386
|
-
if (this.#subscriptions[subscription] === void 0) {
|
|
387
|
-
return false;
|
|
388
|
-
}
|
|
389
|
-
delete this.#subscriptions[subscription];
|
|
390
|
-
try {
|
|
391
|
-
return this.isConnected && this.#websocket !== null ? this.send(method, [id]) : true;
|
|
392
|
-
} catch {
|
|
393
|
-
return false;
|
|
394
|
-
}
|
|
395
|
-
}
|
|
396
|
-
#emit = (type, ...args) => {
|
|
397
|
-
this.#eventemitter.emit(type, ...args);
|
|
398
|
-
};
|
|
399
|
-
#onSocketClose = (event) => {
|
|
400
|
-
const error = new Error(
|
|
401
|
-
`disconnected from ${this.endpoint}: ${event.code}:: ${event.reason || getWSErrorString(event.code)}`
|
|
402
|
-
);
|
|
403
|
-
if (this.#autoConnectBackoff.isActive) {
|
|
404
|
-
if (event.code !== 1e3) log_default.error(error.message);
|
|
405
|
-
}
|
|
406
|
-
this.#isConnected = false;
|
|
407
|
-
if (this.#websocket) {
|
|
408
|
-
this.#websocket.onclose = null;
|
|
409
|
-
this.#websocket.onerror = null;
|
|
410
|
-
this.#websocket.onmessage = null;
|
|
411
|
-
this.#websocket.onopen = null;
|
|
412
|
-
this.#websocket = null;
|
|
413
|
-
}
|
|
414
|
-
if (this.#timeoutId) {
|
|
415
|
-
clearInterval(this.#timeoutId);
|
|
416
|
-
this.#timeoutId = null;
|
|
417
|
-
}
|
|
418
|
-
eraseRecord(this.#handlers, (h) => {
|
|
419
|
-
try {
|
|
420
|
-
h.callback(error, void 0);
|
|
421
|
-
} catch (err) {
|
|
422
|
-
log_default.error(err);
|
|
423
|
-
}
|
|
424
|
-
});
|
|
425
|
-
eraseRecord(this.#waitingForId);
|
|
426
|
-
this.#emit("disconnected");
|
|
427
|
-
this.scheduleNextRetry();
|
|
428
|
-
};
|
|
429
|
-
#onSocketError = (error) => {
|
|
430
|
-
this.#emit("error", error);
|
|
431
|
-
};
|
|
432
|
-
#onSocketMessage = (message) => {
|
|
433
|
-
try {
|
|
434
|
-
const response = JSON.parse(message.data);
|
|
435
|
-
return response.method === void 0 ? this.#onSocketMessageResult(response) : this.#onSocketMessageSubscribe(response);
|
|
436
|
-
} catch (e) {
|
|
437
|
-
this.#emit("error", new Error("Invalid websocket message received", { cause: e }));
|
|
438
|
-
}
|
|
439
|
-
};
|
|
440
|
-
#onSocketMessageResult = (response) => {
|
|
441
|
-
const handler = this.#handlers[response.id];
|
|
442
|
-
if (!handler) {
|
|
443
|
-
return;
|
|
444
|
-
}
|
|
445
|
-
try {
|
|
446
|
-
const { method, params, subscription } = handler;
|
|
447
|
-
const result = this.#coder.decodeResponse(response);
|
|
448
|
-
handler.callback(null, result);
|
|
449
|
-
if (subscription) {
|
|
450
|
-
const subId = `${subscription.type}::${result}`;
|
|
451
|
-
this.#subscriptions[subId] = { ...subscription, method, params };
|
|
452
|
-
if (this.#waitingForId[subId]) {
|
|
453
|
-
this.#onSocketMessageSubscribe(this.#waitingForId[subId]);
|
|
454
|
-
}
|
|
455
|
-
}
|
|
456
|
-
} catch (error) {
|
|
457
|
-
handler.callback(error, void 0);
|
|
458
|
-
}
|
|
459
|
-
delete this.#handlers[response.id];
|
|
460
|
-
};
|
|
461
|
-
#onSocketMessageSubscribe = (response) => {
|
|
462
|
-
const method = ALIASES[response.method] || response.method || "invalid";
|
|
463
|
-
const subId = `${method}::${response.params.subscription}`;
|
|
464
|
-
const handler = this.#subscriptions[subId];
|
|
465
|
-
if (!handler) {
|
|
466
|
-
this.#waitingForId[subId] = response;
|
|
467
|
-
return;
|
|
468
|
-
}
|
|
469
|
-
delete this.#waitingForId[subId];
|
|
470
|
-
try {
|
|
471
|
-
const result = this.#coder.decodeResponse(response);
|
|
472
|
-
handler.callback(null, result);
|
|
473
|
-
} catch (error) {
|
|
474
|
-
handler.callback(error, void 0);
|
|
475
|
-
}
|
|
476
|
-
};
|
|
477
|
-
#onSocketOpen = () => {
|
|
478
|
-
if (this.#websocket === null) {
|
|
479
|
-
throw new Error("WebSocket cannot be null in onOpen");
|
|
480
|
-
}
|
|
481
|
-
this.#isConnected = true;
|
|
482
|
-
this.#endpointsTriedSinceLastConnection = 0;
|
|
483
|
-
this.#autoConnectBackoff.reset();
|
|
484
|
-
this.#resubscribe();
|
|
485
|
-
this.#emit("connected");
|
|
486
|
-
return true;
|
|
487
|
-
};
|
|
488
|
-
#resubscribe = () => {
|
|
489
|
-
const subscriptions = this.#subscriptions;
|
|
490
|
-
this.#subscriptions = {};
|
|
491
|
-
Promise.all(
|
|
492
|
-
Object.keys(subscriptions).map(async (id) => {
|
|
493
|
-
const { callback, method, params, type } = subscriptions[id];
|
|
494
|
-
if (type.startsWith("author_")) {
|
|
495
|
-
return;
|
|
496
|
-
}
|
|
497
|
-
try {
|
|
498
|
-
await this.subscribe(type, method, params, callback);
|
|
499
|
-
} catch (error) {
|
|
500
|
-
log_default.error(error);
|
|
501
|
-
}
|
|
502
|
-
})
|
|
503
|
-
).catch(log_default.error);
|
|
504
|
-
};
|
|
505
|
-
#timeoutHandlers = () => {
|
|
506
|
-
const now = Date.now();
|
|
507
|
-
const ids = Object.keys(this.#handlers);
|
|
508
|
-
for (let i = 0; i < ids.length; i++) {
|
|
509
|
-
const handler = this.#handlers[ids[i]];
|
|
510
|
-
if (now - handler.start > this.#timeout) {
|
|
511
|
-
try {
|
|
512
|
-
handler.callback(
|
|
513
|
-
new Error(`No response received from RPC endpoint in ${this.#timeout / 1e3}s`),
|
|
514
|
-
void 0
|
|
515
|
-
);
|
|
516
|
-
} catch {
|
|
517
|
-
}
|
|
518
|
-
delete this.#handlers[ids[i]];
|
|
519
|
-
}
|
|
520
|
-
}
|
|
521
|
-
};
|
|
522
|
-
};
|
|
523
|
-
|
|
524
|
-
// src/dot/ChainConnectorDot.ts
|
|
525
|
-
var BAD_RPC_ERRORS = {
|
|
526
|
-
"-32097": "Rate limit exceeded",
|
|
527
|
-
"-32098": "Capacity exceeded"
|
|
4
|
+
import { throwAfter } from "@talismn/util";
|
|
5
|
+
import { createPublicClient, createWalletClient, fallback, http } from "viem";
|
|
6
|
+
import { camelCase, fromPairs, toPairs } from "lodash-es";
|
|
7
|
+
import * as viemChains from "viem/chains";
|
|
8
|
+
import { createDefaultRpcTransport, createSolanaRpcFromTransport, isSolanaError } from "@solana/kit";
|
|
9
|
+
//#endregion
|
|
10
|
+
//#region src/log.ts
|
|
11
|
+
var log_default = anylogger("@talismn/chain-connectors");
|
|
12
|
+
//#endregion
|
|
13
|
+
//#region src/dot/ChainConnectorDot.ts
|
|
14
|
+
const BAD_RPC_ERRORS = {
|
|
15
|
+
"-32097": "Rate limit exceeded",
|
|
16
|
+
"-32098": "Capacity exceeded"
|
|
528
17
|
};
|
|
18
|
+
const RESPONSE_TIMEOUT = 3e4;
|
|
19
|
+
const KEEP_ALIVE_INTERVAL = 2e4;
|
|
20
|
+
/** in-flight requests reject with DestroyedError when a connection is torn down - expected, not worth logging */
|
|
21
|
+
const isDestroyedError = (error) => error instanceof Error && error.name === "DestroyedError";
|
|
22
|
+
const STALE_NOTIFY_TIMEOUT = 3e4;
|
|
529
23
|
var ChainConnectionError = class extends Error {
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
24
|
+
type;
|
|
25
|
+
chainId;
|
|
26
|
+
constructor(chainId, options) {
|
|
27
|
+
super(`Unable to connect to chain ${chainId}`, options);
|
|
28
|
+
this.type = "CHAIN_CONNECTION_ERROR";
|
|
29
|
+
this.chainId = chainId;
|
|
30
|
+
}
|
|
537
31
|
};
|
|
538
32
|
var StaleRpcError = class extends Error {
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
};
|
|
547
|
-
var WebsocketAllocationExhaustedError = class extends Error {
|
|
548
|
-
type;
|
|
549
|
-
chainId;
|
|
550
|
-
constructor(chainId, options) {
|
|
551
|
-
super(
|
|
552
|
-
`No websockets are available from the browser pool to connect to chain ${chainId}`,
|
|
553
|
-
options
|
|
554
|
-
);
|
|
555
|
-
this.type = "WEBSOCKET_ALLOCATION_EXHAUSTED_ERROR";
|
|
556
|
-
this.chainId = chainId;
|
|
557
|
-
}
|
|
558
|
-
};
|
|
559
|
-
var CallerUnsubscribedError = class extends Error {
|
|
560
|
-
type;
|
|
561
|
-
chainId;
|
|
562
|
-
unsubscribeMethod;
|
|
563
|
-
constructor(chainId, unsubscribeMethod, options) {
|
|
564
|
-
super(`Caller unsubscribed from ${chainId}`, options);
|
|
565
|
-
this.type = "CALLER_UNSUBSCRIBED_ERROR";
|
|
566
|
-
this.chainId = chainId;
|
|
567
|
-
this.unsubscribeMethod = unsubscribeMethod;
|
|
568
|
-
}
|
|
33
|
+
type;
|
|
34
|
+
chainId;
|
|
35
|
+
constructor(chainId, options) {
|
|
36
|
+
super(`RPCs are stale/unavailable for chain ${chainId}`, options);
|
|
37
|
+
this.type = "STALE_RPC_ERROR";
|
|
38
|
+
this.chainId = chainId;
|
|
39
|
+
}
|
|
569
40
|
};
|
|
41
|
+
/**
|
|
42
|
+
* ChainConnector provides an interface similar to a websocket JSON-RPC provider, but with three points of difference:
|
|
43
|
+
*
|
|
44
|
+
* 1. ChainConnector methods all accept a `chainId` instead of an array of RPCs. RPCs are then fetched internally from chaindata.
|
|
45
|
+
* 2. ChainConnector creates only one socket connection per chain (via polkadot-api's ws-provider, which handles
|
|
46
|
+
* endpoint rotation, reconnection and stale-socket detection) and ensures that all downstream requests to a chain
|
|
47
|
+
* share that connection.
|
|
48
|
+
* 3. Subscriptions return a callable `unsubscribe` method instead of an id, and are automatically re-established
|
|
49
|
+
* when the provider reconnects (possibly to another endpoint).
|
|
50
|
+
*
|
|
51
|
+
* Additionally, when run on the clientside of a dapp where `window.talismanSub` is available, instead of spinning up new websocket
|
|
52
|
+
* connections this class will forward all requests through to the wallet backend - where another instance of this class will
|
|
53
|
+
* handle the websocket connections.
|
|
54
|
+
*/
|
|
570
55
|
var ChainConnectorDot = class {
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
async disconnectChainSocket(chainId, socketUserId) {
|
|
868
|
-
this.removeSocketUser(chainId, socketUserId);
|
|
869
|
-
if (this.#socketUsers[chainId].length > 0) return;
|
|
870
|
-
if (!this.#socketConnections[chainId])
|
|
871
|
-
return log_default.warn(`Failed to disconnect socket: socket ${chainId} not found`);
|
|
872
|
-
try {
|
|
873
|
-
this.#socketConnections[chainId].disconnect();
|
|
874
|
-
} catch (error) {
|
|
875
|
-
log_default.warn(`Error occurred disconnecting socket ${chainId}`, error);
|
|
876
|
-
}
|
|
877
|
-
delete this.#socketConnections[chainId];
|
|
878
|
-
clearInterval(this.#socketKeepAliveIntervals[chainId]);
|
|
879
|
-
delete this.#socketKeepAliveIntervals[chainId];
|
|
880
|
-
}
|
|
881
|
-
addSocketUser(chainId) {
|
|
882
|
-
if (!Array.isArray(this.#socketUsers[chainId])) this.#socketUsers[chainId] = [];
|
|
883
|
-
const socketUserId = this.getExclusiveRandomId(this.#socketUsers[chainId]);
|
|
884
|
-
this.#socketUsers[chainId].push(socketUserId);
|
|
885
|
-
return socketUserId;
|
|
886
|
-
}
|
|
887
|
-
removeSocketUser(chainId, socketUserId) {
|
|
888
|
-
const userIndex = this.#socketUsers[chainId].indexOf(socketUserId);
|
|
889
|
-
if (userIndex === -1)
|
|
890
|
-
throw new Error(
|
|
891
|
-
`Can't remove user ${socketUserId} from socket ${chainId}: user not in list ${this.#socketUsers[chainId].join(", ")}`
|
|
892
|
-
);
|
|
893
|
-
this.#socketUsers[chainId].splice(userIndex, 1);
|
|
894
|
-
}
|
|
895
|
-
/** continues to generate a random number until it finds one which is not present in the exclude list */
|
|
896
|
-
getExclusiveRandomId(exclude = []) {
|
|
897
|
-
let id = this.getRandomId();
|
|
898
|
-
while (exclude.includes(id)) {
|
|
899
|
-
id = this.getRandomId();
|
|
900
|
-
}
|
|
901
|
-
return id;
|
|
902
|
-
}
|
|
903
|
-
/** generates a random number */
|
|
904
|
-
getRandomId() {
|
|
905
|
-
return Math.trunc(Math.random() * 10 ** 8);
|
|
906
|
-
}
|
|
907
|
-
getTalismanSub() {
|
|
908
|
-
const talismanSub = typeof window !== "undefined" && window.talismanSub;
|
|
909
|
-
const rpcByGenesisHashSend = talismanSub?.rpcByGenesisHashSend;
|
|
910
|
-
const rpcByGenesisHashSubscribe = talismanSub?.rpcByGenesisHashSubscribe;
|
|
911
|
-
const rpcByGenesisHashUnsubscribe = talismanSub?.rpcByGenesisHashUnsubscribe;
|
|
912
|
-
if (typeof rpcByGenesisHashSend !== "function") return;
|
|
913
|
-
if (typeof rpcByGenesisHashSubscribe !== "function") return;
|
|
914
|
-
if (typeof rpcByGenesisHashUnsubscribe !== "function") return;
|
|
915
|
-
return {
|
|
916
|
-
// biome-ignore lint/suspicious/noExplicitAny: legacy
|
|
917
|
-
send: (genesisHash, method, params) => rpcByGenesisHashSend(genesisHash, method, params),
|
|
918
|
-
subscribe: (genesisHash, subscribeMethod, responseMethod, params, callback, timeout) => rpcByGenesisHashSubscribe(
|
|
919
|
-
genesisHash,
|
|
920
|
-
subscribeMethod,
|
|
921
|
-
responseMethod,
|
|
922
|
-
params,
|
|
923
|
-
callback,
|
|
924
|
-
timeout
|
|
925
|
-
),
|
|
926
|
-
unsubscribe: (subscriptionId, unsubscribeMethod) => rpcByGenesisHashUnsubscribe(subscriptionId, unsubscribeMethod)
|
|
927
|
-
};
|
|
928
|
-
}
|
|
929
|
-
async updateRpcPriority(chainId, rpc, priority) {
|
|
930
|
-
if (!this.#connectionMetaDb) return;
|
|
931
|
-
const rpcs = await this.getEndpoints(chainId);
|
|
932
|
-
if (!rpcs.includes(rpc)) throw new Error(`Unknown rpc for chain ${chainId} : ${rpc}`);
|
|
933
|
-
const urls = rpcs.filter((r) => r !== rpc);
|
|
934
|
-
if (priority === "first") urls.unshift(rpc);
|
|
935
|
-
if (priority === "last") urls.push(rpc);
|
|
936
|
-
if (!isEqual(urls, rpcs)) {
|
|
937
|
-
await this.#connectionMetaDb.chainPriorityRpcs.put({ id: chainId, urls }, chainId);
|
|
938
|
-
}
|
|
939
|
-
}
|
|
940
|
-
async getEndpoints(chainId) {
|
|
941
|
-
const chain = await this.#chaindataChainProvider.getNetworkById(chainId, "polkadot");
|
|
942
|
-
if (!chain) throw new Error(`Chain ${chainId} not found in store`);
|
|
943
|
-
let rpcs = chain.rpcs.concat();
|
|
944
|
-
const priorityRpcs = this.#connectionMetaDb ? await this.#connectionMetaDb.chainPriorityRpcs.get(chainId) : void 0;
|
|
945
|
-
if (priorityRpcs) {
|
|
946
|
-
rpcs = [
|
|
947
|
-
...priorityRpcs.urls.filter((rpc) => rpcs.includes(rpc)),
|
|
948
|
-
...rpcs.filter((rpc) => !priorityRpcs.urls.includes(rpc))
|
|
949
|
-
];
|
|
950
|
-
}
|
|
951
|
-
return rpcs;
|
|
952
|
-
}
|
|
56
|
+
#chaindataChainProvider;
|
|
57
|
+
#connections = {};
|
|
58
|
+
#pendingConnections = {};
|
|
59
|
+
constructor(chaindataChainProvider) {
|
|
60
|
+
this.#chaindataChainProvider = chaindataChainProvider;
|
|
61
|
+
}
|
|
62
|
+
async send(chainId, method, params, _isCacheable, extraOptions) {
|
|
63
|
+
const talismanSub = this.getTalismanSub();
|
|
64
|
+
if (talismanSub !== void 0) try {
|
|
65
|
+
const genesisHash = await this.getGenesisHash(chainId);
|
|
66
|
+
return await talismanSub.send(genesisHash, method, params);
|
|
67
|
+
} catch (error) {
|
|
68
|
+
log_default.warn(`Failed to make wallet-proxied send request for chain ${chainId}. Falling back to plain websocket`, error);
|
|
69
|
+
}
|
|
70
|
+
let socketUserId;
|
|
71
|
+
let connection;
|
|
72
|
+
try {
|
|
73
|
+
[socketUserId, connection] = await this.acquireConnection(chainId);
|
|
74
|
+
} catch (error) {
|
|
75
|
+
throw new StaleRpcError(chainId, { cause: error });
|
|
76
|
+
}
|
|
77
|
+
try {
|
|
78
|
+
return await this.request(connection, method, params);
|
|
79
|
+
} catch (err) {
|
|
80
|
+
const error = err;
|
|
81
|
+
if (error?.message === "TIMEOUT") {
|
|
82
|
+
log_default.error(`ChainConnector timeout`, {
|
|
83
|
+
chainId,
|
|
84
|
+
error
|
|
85
|
+
});
|
|
86
|
+
connection.provider.switch();
|
|
87
|
+
throw new Error("Timeout");
|
|
88
|
+
}
|
|
89
|
+
const badRpcError = BAD_RPC_ERRORS[error?.code?.toString() ?? ""];
|
|
90
|
+
if (badRpcError) {
|
|
91
|
+
log_default.error(`ChainConnector ${badRpcError}`, {
|
|
92
|
+
error,
|
|
93
|
+
chainId
|
|
94
|
+
});
|
|
95
|
+
connection.provider.switch();
|
|
96
|
+
throw new Error(badRpcError);
|
|
97
|
+
}
|
|
98
|
+
if (!extraOptions?.expectErrors) log_default.error(`Failed to send ${method} on chain ${chainId}\nparams: ${JSON.stringify(params)}`, { error });
|
|
99
|
+
throw error;
|
|
100
|
+
} finally {
|
|
101
|
+
this.releaseConnection(chainId, socketUserId);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
async subscribe(chainId, subscribeMethod, responseMethod, params, callback, timeout = 3e4) {
|
|
105
|
+
const talismanSub = this.getTalismanSub();
|
|
106
|
+
if (talismanSub !== void 0) try {
|
|
107
|
+
const genesisHash = await this.getGenesisHash(chainId);
|
|
108
|
+
const subscriptionId = await talismanSub.subscribe(genesisHash, subscribeMethod, responseMethod, params, callback, timeout);
|
|
109
|
+
return (unsubscribeMethod) => talismanSub.unsubscribe(subscriptionId, unsubscribeMethod);
|
|
110
|
+
} catch (error) {
|
|
111
|
+
log_default.warn(`Failed to create wallet-proxied subscription for chain ${chainId}. Falling back to plain websocket`, error);
|
|
112
|
+
}
|
|
113
|
+
let socketUserId;
|
|
114
|
+
let connection;
|
|
115
|
+
try {
|
|
116
|
+
[socketUserId, connection] = await this.acquireConnection(chainId);
|
|
117
|
+
} catch (error) {
|
|
118
|
+
throw new StaleRpcError(chainId, { cause: error });
|
|
119
|
+
}
|
|
120
|
+
const subscription = {
|
|
121
|
+
subscribeMethod,
|
|
122
|
+
params,
|
|
123
|
+
callback,
|
|
124
|
+
cancelRequest: null,
|
|
125
|
+
stopFollow: null,
|
|
126
|
+
serverSubId: null,
|
|
127
|
+
unsubscribed: false
|
|
128
|
+
};
|
|
129
|
+
connection.subscriptions.add(subscription);
|
|
130
|
+
if (timeout && !connection.wasConnected) {
|
|
131
|
+
const staleWarning = setTimeout(() => {
|
|
132
|
+
if (!subscription.unsubscribed && !connection.wasConnected) callback(new StaleRpcError(chainId), null);
|
|
133
|
+
}, timeout);
|
|
134
|
+
const clear = () => clearTimeout(staleWarning);
|
|
135
|
+
const originalCallback = subscription.callback;
|
|
136
|
+
subscription.callback = (error, result) => {
|
|
137
|
+
clear();
|
|
138
|
+
subscription.callback = originalCallback;
|
|
139
|
+
originalCallback(error, result);
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
this.startSubscription(connection, subscription);
|
|
143
|
+
return (unsubscribeMethod) => {
|
|
144
|
+
if (subscription.unsubscribed) return;
|
|
145
|
+
subscription.unsubscribed = true;
|
|
146
|
+
subscription.cancelRequest?.();
|
|
147
|
+
subscription.stopFollow?.();
|
|
148
|
+
const current = this.#connections[chainId];
|
|
149
|
+
const isLastUser = !!current && current.users.size === 1 && current.users.has(socketUserId);
|
|
150
|
+
if (subscription.serverSubId !== null && current && !isLastUser) try {
|
|
151
|
+
current.client.request(unsubscribeMethod, [subscription.serverSubId]).catch((error) => {
|
|
152
|
+
if (!isDestroyedError(error)) log_default.warn(`Failed to unsubscribe from ${chainId}`, error);
|
|
153
|
+
});
|
|
154
|
+
} catch (error) {
|
|
155
|
+
if (!isDestroyedError(error)) log_default.warn(`Failed to unsubscribe from ${chainId}`, error);
|
|
156
|
+
}
|
|
157
|
+
current?.subscriptions.delete(subscription);
|
|
158
|
+
this.releaseConnection(chainId, socketUserId);
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* Kills and recreates the connection for a chain, if any.
|
|
163
|
+
* Useful after changing a network's rpcs to make sure the new list is applied for further requests.
|
|
164
|
+
* Active subscriptions are automatically re-established on the new connection.
|
|
165
|
+
*/
|
|
166
|
+
async reset(chainId) {
|
|
167
|
+
log_default.info("ChainConnector reset", chainId);
|
|
168
|
+
const connection = this.#connections[chainId];
|
|
169
|
+
if (!connection) return;
|
|
170
|
+
this.destroyConnection(connection);
|
|
171
|
+
delete this.#connections[chainId];
|
|
172
|
+
if (connection.subscriptions.size) try {
|
|
173
|
+
const fresh = await this.createConnection(chainId);
|
|
174
|
+
fresh.users = connection.users;
|
|
175
|
+
fresh.subscriptions = connection.subscriptions;
|
|
176
|
+
this.#connections[chainId] = fresh;
|
|
177
|
+
for (const subscription of fresh.subscriptions) this.startSubscription(fresh, subscription);
|
|
178
|
+
} catch (error) {
|
|
179
|
+
log_default.warn(`Failed to recreate connection for ${chainId} after reset`, error);
|
|
180
|
+
for (const subscription of connection.subscriptions) subscription.callback(new StaleRpcError(chainId, { cause: error }), null);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
/** Sends a request over a connection, throwing `Error("TIMEOUT")` if no response arrives in time */
|
|
184
|
+
request(connection, method, params, timeoutMs = RESPONSE_TIMEOUT) {
|
|
185
|
+
return new Promise((resolve, reject) => {
|
|
186
|
+
const timer = setTimeout(() => {
|
|
187
|
+
cancel();
|
|
188
|
+
reject(/* @__PURE__ */ new Error("TIMEOUT"));
|
|
189
|
+
}, timeoutMs);
|
|
190
|
+
const cancel = connection.client._request(method, params, {
|
|
191
|
+
onSuccess: (result) => {
|
|
192
|
+
clearTimeout(timer);
|
|
193
|
+
resolve(result);
|
|
194
|
+
},
|
|
195
|
+
onError: (error) => {
|
|
196
|
+
clearTimeout(timer);
|
|
197
|
+
reject(error);
|
|
198
|
+
}
|
|
199
|
+
});
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
/** Issues the subscribe request and routes notifications to the subscription callback */
|
|
203
|
+
startSubscription(connection, subscription) {
|
|
204
|
+
subscription.serverSubId = null;
|
|
205
|
+
subscription.stopFollow = null;
|
|
206
|
+
subscription.cancelRequest = connection.client._request(subscription.subscribeMethod, subscription.params, {
|
|
207
|
+
onSuccess: (serverSubId, follow) => {
|
|
208
|
+
subscription.cancelRequest = null;
|
|
209
|
+
if (subscription.unsubscribed) return;
|
|
210
|
+
subscription.serverSubId = serverSubId;
|
|
211
|
+
subscription.stopFollow = follow(serverSubId, {
|
|
212
|
+
next: (result) => subscription.callback(null, result),
|
|
213
|
+
error: (error) => subscription.callback(error, null)
|
|
214
|
+
});
|
|
215
|
+
},
|
|
216
|
+
onError: (error) => {
|
|
217
|
+
subscription.cancelRequest = null;
|
|
218
|
+
if (!subscription.unsubscribed) subscription.callback(error, null);
|
|
219
|
+
}
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
/**
|
|
223
|
+
* Get (or create) the shared connection for a chain.
|
|
224
|
+
*
|
|
225
|
+
* The caller must call releaseConnection with the returned SocketUserId once they are finished with it.
|
|
226
|
+
*/
|
|
227
|
+
async acquireConnection(chainId) {
|
|
228
|
+
let connection = this.#connections[chainId];
|
|
229
|
+
if (!connection) {
|
|
230
|
+
if (!this.#pendingConnections[chainId]) this.#pendingConnections[chainId] = this.createConnection(chainId).then((created) => {
|
|
231
|
+
this.#connections[chainId] = created;
|
|
232
|
+
return created;
|
|
233
|
+
}).finally(() => {
|
|
234
|
+
delete this.#pendingConnections[chainId];
|
|
235
|
+
});
|
|
236
|
+
connection = await this.#pendingConnections[chainId];
|
|
237
|
+
}
|
|
238
|
+
const socketUserId = this.getExclusiveRandomId([...connection.users]);
|
|
239
|
+
connection.users.add(socketUserId);
|
|
240
|
+
return [socketUserId, connection];
|
|
241
|
+
}
|
|
242
|
+
async createConnection(chainId) {
|
|
243
|
+
const chain = await this.#chaindataChainProvider.getNetworkById(chainId, "polkadot");
|
|
244
|
+
if (!chain) throw new Error(`Chain ${chainId} not found in store`);
|
|
245
|
+
const rpcs = chain.rpcs.concat();
|
|
246
|
+
if (!rpcs.length) throw new Error(`No healthy RPCs available for chain ${chainId}`);
|
|
247
|
+
let connection = null;
|
|
248
|
+
const provider = getWsProvider(rpcs, { onStatusChanged: (status) => this.handleStatusChange(connection, status) });
|
|
249
|
+
connection = {
|
|
250
|
+
chainId,
|
|
251
|
+
provider,
|
|
252
|
+
client: createClient(provider),
|
|
253
|
+
users: /* @__PURE__ */ new Set(),
|
|
254
|
+
subscriptions: /* @__PURE__ */ new Set(),
|
|
255
|
+
wasConnected: false,
|
|
256
|
+
keepAliveInterval: null,
|
|
257
|
+
staleTimeout: null
|
|
258
|
+
};
|
|
259
|
+
return connection;
|
|
260
|
+
}
|
|
261
|
+
handleStatusChange(connection, status) {
|
|
262
|
+
if (!connection) return;
|
|
263
|
+
if (this.#connections[connection.chainId] !== connection) return;
|
|
264
|
+
if (status.type === WsEvent.CONNECTED) {
|
|
265
|
+
if (connection.staleTimeout) {
|
|
266
|
+
clearTimeout(connection.staleTimeout);
|
|
267
|
+
connection.staleTimeout = null;
|
|
268
|
+
}
|
|
269
|
+
const isReconnect = connection.wasConnected;
|
|
270
|
+
connection.wasConnected = true;
|
|
271
|
+
if (connection.keepAliveInterval) clearInterval(connection.keepAliveInterval);
|
|
272
|
+
connection.keepAliveInterval = setInterval(() => {
|
|
273
|
+
this.request(connection, "system_health", [], KEEP_ALIVE_INTERVAL).catch((error) => {
|
|
274
|
+
if (!isDestroyedError(error)) log_default.warn(`Failed keep-alive for socket ${connection.chainId}`, error);
|
|
275
|
+
});
|
|
276
|
+
}, KEEP_ALIVE_INTERVAL);
|
|
277
|
+
if (isReconnect) for (const subscription of connection.subscriptions) {
|
|
278
|
+
if (subscription.unsubscribed) continue;
|
|
279
|
+
if (subscription.subscribeMethod.startsWith("author_")) continue;
|
|
280
|
+
subscription.stopFollow?.();
|
|
281
|
+
subscription.cancelRequest?.();
|
|
282
|
+
this.startSubscription(connection, subscription);
|
|
283
|
+
}
|
|
284
|
+
} else {
|
|
285
|
+
if (connection.keepAliveInterval) {
|
|
286
|
+
clearInterval(connection.keepAliveInterval);
|
|
287
|
+
connection.keepAliveInterval = null;
|
|
288
|
+
}
|
|
289
|
+
if (!connection.staleTimeout && connection.subscriptions.size) connection.staleTimeout = setTimeout(() => {
|
|
290
|
+
connection.staleTimeout = null;
|
|
291
|
+
for (const subscription of connection.subscriptions) if (!subscription.unsubscribed) subscription.callback(new StaleRpcError(connection.chainId), null);
|
|
292
|
+
}, STALE_NOTIFY_TIMEOUT);
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
releaseConnection(chainId, socketUserId) {
|
|
296
|
+
const connection = this.#connections[chainId];
|
|
297
|
+
if (!connection) return;
|
|
298
|
+
connection.users.delete(socketUserId);
|
|
299
|
+
if (connection.users.size > 0) return;
|
|
300
|
+
this.destroyConnection(connection);
|
|
301
|
+
delete this.#connections[chainId];
|
|
302
|
+
}
|
|
303
|
+
destroyConnection(connection) {
|
|
304
|
+
if (connection.keepAliveInterval) clearInterval(connection.keepAliveInterval);
|
|
305
|
+
if (connection.staleTimeout) clearTimeout(connection.staleTimeout);
|
|
306
|
+
connection.keepAliveInterval = null;
|
|
307
|
+
connection.staleTimeout = null;
|
|
308
|
+
for (const subscription of connection.subscriptions) {
|
|
309
|
+
subscription.stopFollow?.();
|
|
310
|
+
subscription.cancelRequest?.();
|
|
311
|
+
subscription.stopFollow = null;
|
|
312
|
+
subscription.cancelRequest = null;
|
|
313
|
+
subscription.serverSubId = null;
|
|
314
|
+
}
|
|
315
|
+
try {
|
|
316
|
+
connection.client.destroy();
|
|
317
|
+
} catch (error) {
|
|
318
|
+
log_default.warn(`Error occurred destroying connection ${connection.chainId}`, error);
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
async getGenesisHash(chainId) {
|
|
322
|
+
const chain = await this.#chaindataChainProvider.getNetworkById(chainId, "polkadot");
|
|
323
|
+
if (!chain) throw new Error(`Chain ${chainId} not found in store`);
|
|
324
|
+
const { genesisHash } = chain;
|
|
325
|
+
if (typeof genesisHash !== "string") throw new Error(`Chain ${chainId} has no genesisHash in store`);
|
|
326
|
+
return genesisHash;
|
|
327
|
+
}
|
|
328
|
+
/** continues to generate a random number until it finds one which is not present in the exclude list */
|
|
329
|
+
getExclusiveRandomId(exclude = []) {
|
|
330
|
+
let id = this.getRandomId();
|
|
331
|
+
while (exclude.includes(id)) id = this.getRandomId();
|
|
332
|
+
return id;
|
|
333
|
+
}
|
|
334
|
+
/** generates a random number */
|
|
335
|
+
getRandomId() {
|
|
336
|
+
return Math.trunc(Math.random() * 10 ** 8);
|
|
337
|
+
}
|
|
338
|
+
getTalismanSub() {
|
|
339
|
+
const talismanSub = typeof window !== "undefined" && window.talismanSub;
|
|
340
|
+
const rpcByGenesisHashSend = talismanSub?.rpcByGenesisHashSend;
|
|
341
|
+
const rpcByGenesisHashSubscribe = talismanSub?.rpcByGenesisHashSubscribe;
|
|
342
|
+
const rpcByGenesisHashUnsubscribe = talismanSub?.rpcByGenesisHashUnsubscribe;
|
|
343
|
+
if (typeof rpcByGenesisHashSend !== "function") return;
|
|
344
|
+
if (typeof rpcByGenesisHashSubscribe !== "function") return;
|
|
345
|
+
if (typeof rpcByGenesisHashUnsubscribe !== "function") return;
|
|
346
|
+
return {
|
|
347
|
+
send: (genesisHash, method, params) => rpcByGenesisHashSend(genesisHash, method, params),
|
|
348
|
+
subscribe: (genesisHash, subscribeMethod, responseMethod, params, callback, timeout) => rpcByGenesisHashSubscribe(genesisHash, subscribeMethod, responseMethod, params, callback, timeout),
|
|
349
|
+
unsubscribe: (subscriptionId, unsubscribeMethod) => rpcByGenesisHashUnsubscribe(subscriptionId, unsubscribeMethod)
|
|
350
|
+
};
|
|
351
|
+
}
|
|
953
352
|
};
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
import { WsProvider } from "@polkadot/rpc-provider";
|
|
958
|
-
import { throwAfter as throwAfter2 } from "@talismn/util";
|
|
959
|
-
var AUTO_CONNECT_TIMEOUT = 3e3;
|
|
960
|
-
var TIMEOUT = 1e4;
|
|
353
|
+
//#endregion
|
|
354
|
+
//#region src/dot/ChainConnectorDotStub.ts
|
|
355
|
+
const TIMEOUT = 1e4;
|
|
961
356
|
var ChainConnectorDotStub = class {
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
357
|
+
#client;
|
|
358
|
+
constructor(network) {
|
|
359
|
+
this.#client = createClient(getWsProvider(network.rpcs.concat()));
|
|
360
|
+
}
|
|
361
|
+
async send(_chainId, method, params, _isCacheable) {
|
|
362
|
+
return await Promise.race([this.#client.request(method, params), throwAfter(TIMEOUT, `Request ${method} timed out after ${TIMEOUT}ms`)]);
|
|
363
|
+
}
|
|
364
|
+
async subscribe(_chainId, subscribeMethod, _responseMethod, params, callback, timeout) {
|
|
365
|
+
let stopFollow = null;
|
|
366
|
+
let unsubscribed = false;
|
|
367
|
+
const serverSubId = await Promise.race([new Promise((resolve, reject) => {
|
|
368
|
+
this.#client._request(subscribeMethod, params, {
|
|
369
|
+
onSuccess: (subId, follow) => {
|
|
370
|
+
stopFollow = follow(subId, {
|
|
371
|
+
next: (result) => callback(null, result),
|
|
372
|
+
error: (error) => callback(error, null)
|
|
373
|
+
});
|
|
374
|
+
resolve(subId);
|
|
375
|
+
},
|
|
376
|
+
onError: reject
|
|
377
|
+
});
|
|
378
|
+
}), throwAfter(timeout || TIMEOUT, `Subscription timed out after ${timeout || TIMEOUT}ms`)]);
|
|
379
|
+
return (unsubscribeMethod) => {
|
|
380
|
+
if (unsubscribed) return;
|
|
381
|
+
unsubscribed = true;
|
|
382
|
+
stopFollow?.();
|
|
383
|
+
this.#client.request(unsubscribeMethod, [serverSubId]).catch(() => {});
|
|
384
|
+
};
|
|
385
|
+
}
|
|
386
|
+
reset() {
|
|
387
|
+
throw new Error("ChainConnectorDotStub does not implement reset");
|
|
388
|
+
}
|
|
389
|
+
destroy() {
|
|
390
|
+
this.#client.destroy();
|
|
391
|
+
}
|
|
989
392
|
};
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
return acc;
|
|
1003
|
-
},
|
|
1004
|
-
{}
|
|
1005
|
-
);
|
|
1006
|
-
var chainsCache = /* @__PURE__ */ new Map();
|
|
1007
|
-
var clearChainsCache = (networkId) => {
|
|
1008
|
-
if (networkId) chainsCache.delete(networkId);
|
|
1009
|
-
else chainsCache.clear();
|
|
393
|
+
//#endregion
|
|
394
|
+
//#region src/eth/getChainFromEvmNetwork.ts
|
|
395
|
+
const { zoraTestnet, ...validViemChains } = viemChains;
|
|
396
|
+
const VIEM_CHAINS = Object.keys(validViemChains).reduce((acc, curr) => {
|
|
397
|
+
const chain = validViemChains[curr];
|
|
398
|
+
acc[chain.id] = chain;
|
|
399
|
+
return acc;
|
|
400
|
+
}, {});
|
|
401
|
+
const chainsCache = /* @__PURE__ */ new Map();
|
|
402
|
+
const clearChainsCache = (networkId) => {
|
|
403
|
+
if (networkId) chainsCache.delete(networkId);
|
|
404
|
+
else chainsCache.clear();
|
|
1010
405
|
};
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
}
|
|
1038
|
-
};
|
|
1039
|
-
chainsCache.set(network.id, chain);
|
|
1040
|
-
}
|
|
1041
|
-
return chainsCache.get(network.id);
|
|
406
|
+
const getChainFromEvmNetwork = (network) => {
|
|
407
|
+
const { symbol, decimals } = network.nativeCurrency;
|
|
408
|
+
if (!chainsCache.has(network.id)) {
|
|
409
|
+
const chainRpcs = network.rpcs ?? [];
|
|
410
|
+
const viemChain = VIEM_CHAINS[Number(network.id)] ?? {};
|
|
411
|
+
const chain = {
|
|
412
|
+
...viemChain,
|
|
413
|
+
id: Number(network.id),
|
|
414
|
+
name: network.name ?? `Ethereum Chain ${network.id}`,
|
|
415
|
+
rpcUrls: {
|
|
416
|
+
public: { http: chainRpcs },
|
|
417
|
+
default: { http: chainRpcs }
|
|
418
|
+
},
|
|
419
|
+
nativeCurrency: {
|
|
420
|
+
symbol,
|
|
421
|
+
decimals,
|
|
422
|
+
name: symbol
|
|
423
|
+
},
|
|
424
|
+
contracts: {
|
|
425
|
+
...viemChain.contracts,
|
|
426
|
+
...network.contracts ? fromPairs(toPairs(network.contracts).map(([name, address]) => [camelCase(name), { address }])) : {}
|
|
427
|
+
}
|
|
428
|
+
};
|
|
429
|
+
chainsCache.set(network.id, chain);
|
|
430
|
+
}
|
|
431
|
+
return chainsCache.get(network.id);
|
|
1042
432
|
};
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
);
|
|
433
|
+
//#endregion
|
|
434
|
+
//#region src/eth/getTransportForEvmNetwork.ts
|
|
435
|
+
const getTransportForEvmNetwork = (evmNetwork, options = {}) => {
|
|
436
|
+
if (!evmNetwork.rpcs?.length) throw new Error("No RPCs found for EVM network");
|
|
437
|
+
const { batch } = options;
|
|
438
|
+
return fallback(evmNetwork.rpcs.map((url) => http(url, {
|
|
439
|
+
batch,
|
|
440
|
+
retryCount: 0
|
|
441
|
+
})), { retryCount: 0 });
|
|
1053
442
|
};
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
443
|
+
//#endregion
|
|
444
|
+
//#region src/eth/getEvmNetworkPublicClient.ts
|
|
445
|
+
const MUTLICALL_BATCH_WAIT = 25;
|
|
446
|
+
const MUTLICALL_BATCH_SIZE = 100;
|
|
447
|
+
const HTTP_BATCH_WAIT = 25;
|
|
448
|
+
const HTTP_BATCH_SIZE_WITH_MULTICALL = 10;
|
|
449
|
+
const HTTP_BATCH_SIZE_WITHOUT_MULTICALL = 30;
|
|
450
|
+
const publicClientCache = /* @__PURE__ */ new Map();
|
|
451
|
+
const clearPublicClientCache = (evmNetworkId) => {
|
|
452
|
+
clearChainsCache(evmNetworkId);
|
|
453
|
+
if (evmNetworkId) publicClientCache.delete(evmNetworkId);
|
|
454
|
+
else publicClientCache.clear();
|
|
1066
455
|
};
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
);
|
|
1087
|
-
}
|
|
1088
|
-
return publicClientCache.get(network.id);
|
|
456
|
+
const getEvmNetworkPublicClient = (network) => {
|
|
457
|
+
const chain = getChainFromEvmNetwork(network);
|
|
458
|
+
if (!publicClientCache.has(network.id)) {
|
|
459
|
+
if (!network.rpcs.length) throw new Error("No RPCs found for Ethereum network");
|
|
460
|
+
const batch = chain.contracts?.multicall3 ? { multicall: {
|
|
461
|
+
wait: MUTLICALL_BATCH_WAIT,
|
|
462
|
+
batchSize: MUTLICALL_BATCH_SIZE
|
|
463
|
+
} } : void 0;
|
|
464
|
+
const transport = getTransportForEvmNetwork(network, { batch: {
|
|
465
|
+
batchSize: chain.contracts?.multicall3 ? HTTP_BATCH_SIZE_WITH_MULTICALL : HTTP_BATCH_SIZE_WITHOUT_MULTICALL,
|
|
466
|
+
wait: HTTP_BATCH_WAIT
|
|
467
|
+
} });
|
|
468
|
+
publicClientCache.set(network.id, createPublicClient({
|
|
469
|
+
chain,
|
|
470
|
+
transport,
|
|
471
|
+
batch
|
|
472
|
+
}));
|
|
473
|
+
}
|
|
474
|
+
return publicClientCache.get(network.id);
|
|
1089
475
|
};
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
476
|
+
//#endregion
|
|
477
|
+
//#region src/eth/getEvmNetworkWalletClient.ts
|
|
478
|
+
const getEvmNetworkWalletClient = (network, options = {}) => {
|
|
479
|
+
return createWalletClient({
|
|
480
|
+
chain: getChainFromEvmNetwork(network),
|
|
481
|
+
transport: getTransportForEvmNetwork(network),
|
|
482
|
+
account: options.account
|
|
483
|
+
});
|
|
1097
484
|
};
|
|
1098
|
-
|
|
1099
|
-
|
|
485
|
+
//#endregion
|
|
486
|
+
//#region src/eth/ChainConnectorEth.ts
|
|
1100
487
|
var ChainConnectorEth = class {
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
488
|
+
#chaindataProvider;
|
|
489
|
+
constructor(chaindataProvider) {
|
|
490
|
+
this.#chaindataProvider = chaindataProvider;
|
|
491
|
+
}
|
|
492
|
+
async getPublicClientForEvmNetwork(evmNetworkId) {
|
|
493
|
+
const network = await this.#chaindataProvider.getNetworkById(evmNetworkId, "ethereum");
|
|
494
|
+
if (!network) return null;
|
|
495
|
+
return getEvmNetworkPublicClient(network);
|
|
496
|
+
}
|
|
497
|
+
async getWalletClientForEvmNetwork(evmNetworkId, account) {
|
|
498
|
+
const network = await this.#chaindataProvider.getNetworkById(evmNetworkId, "ethereum");
|
|
499
|
+
if (!network) return null;
|
|
500
|
+
return getEvmNetworkWalletClient(network, { account });
|
|
501
|
+
}
|
|
502
|
+
clearRpcProvidersCache(evmNetworkId) {
|
|
503
|
+
clearPublicClientCache(evmNetworkId);
|
|
504
|
+
}
|
|
1118
505
|
};
|
|
1119
|
-
|
|
1120
|
-
|
|
506
|
+
//#endregion
|
|
507
|
+
//#region src/eth/ChainConnectorEthStub.ts
|
|
1121
508
|
var ChainConnectorEthStub = class {
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
}
|
|
509
|
+
#network;
|
|
510
|
+
constructor(network) {
|
|
511
|
+
this.#network = network;
|
|
512
|
+
}
|
|
513
|
+
async getPublicClientForEvmNetwork() {
|
|
514
|
+
return getEvmNetworkPublicClient(this.#network);
|
|
515
|
+
}
|
|
516
|
+
async getWalletClientForEvmNetwork(_networkId, account) {
|
|
517
|
+
return getEvmNetworkWalletClient(this.#network, { account });
|
|
518
|
+
}
|
|
519
|
+
clearRpcProvidersCache() {}
|
|
1134
520
|
};
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
521
|
+
//#endregion
|
|
522
|
+
//#region src/sol/getSolRpc.ts
|
|
523
|
+
const MAX_429_RETRIES = 5;
|
|
524
|
+
const BASE_BACKOFF_MS = 500;
|
|
525
|
+
/** Returns the delay to wait before retrying, or `null` if the error is not a retryable 429. */
|
|
526
|
+
const get429RetryDelay = (error, attempt) => {
|
|
527
|
+
if (!isSolanaError(error)) return null;
|
|
528
|
+
const context = error.context;
|
|
529
|
+
if (context.statusCode !== 429) return null;
|
|
530
|
+
const retryAfter = Number(context.headers?.get("retry-after"));
|
|
531
|
+
if (Number.isFinite(retryAfter) && retryAfter > 0) return retryAfter * 1e3;
|
|
532
|
+
return BASE_BACKOFF_MS * 2 ** attempt;
|
|
1142
533
|
};
|
|
1143
|
-
|
|
1144
|
-
|
|
534
|
+
const sleep = (ms, signal) => new Promise((resolve, reject) => {
|
|
535
|
+
if (signal?.aborted) return reject(signal.reason);
|
|
536
|
+
const onAbort = () => {
|
|
537
|
+
clearTimeout(timeout);
|
|
538
|
+
reject(signal?.reason);
|
|
539
|
+
};
|
|
540
|
+
const timeout = setTimeout(() => {
|
|
541
|
+
signal?.removeEventListener("abort", onAbort);
|
|
542
|
+
resolve();
|
|
543
|
+
}, ms);
|
|
544
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
545
|
+
});
|
|
546
|
+
/**
|
|
547
|
+
* Wraps a transport so HTTP 429 responses are retried with backoff. Solana RPC requests are
|
|
548
|
+
* idempotent (`sendTransaction` is keyed by signature), so replaying a rate-limited request is safe.
|
|
549
|
+
*/
|
|
550
|
+
const withRetryOn429 = (transport) => {
|
|
551
|
+
const wrapped = async (config) => {
|
|
552
|
+
for (let attempt = 0;; attempt++) try {
|
|
553
|
+
return await transport(config);
|
|
554
|
+
} catch (error) {
|
|
555
|
+
const delay = attempt < MAX_429_RETRIES ? get429RetryDelay(error, attempt) : null;
|
|
556
|
+
if (delay === null) throw error;
|
|
557
|
+
await sleep(delay, config.signal);
|
|
558
|
+
}
|
|
559
|
+
};
|
|
560
|
+
return wrapped;
|
|
561
|
+
};
|
|
562
|
+
const getSolTransport = (_networkId, rpcs) => withRetryOn429(createDefaultRpcTransport({ url: rpcs[0] }));
|
|
563
|
+
const getSolRpc = (networkId, rpcs) => createSolanaRpcFromTransport(getSolTransport(networkId, rpcs));
|
|
564
|
+
//#endregion
|
|
565
|
+
//#region src/sol/ChainConnectorSol.ts
|
|
1145
566
|
var ChainConnectorSol = class {
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
567
|
+
#chaindataProvider;
|
|
568
|
+
#transports = /* @__PURE__ */ new Map();
|
|
569
|
+
#rpcs = /* @__PURE__ */ new Map();
|
|
570
|
+
constructor(chaindataProvider) {
|
|
571
|
+
this.#chaindataProvider = chaindataProvider;
|
|
572
|
+
}
|
|
573
|
+
async #getNetworkRpcs(networkId) {
|
|
574
|
+
const network = await this.#chaindataProvider.getNetworkById(networkId, "solana");
|
|
575
|
+
if (!network) throw new Error(`Network not found: ${networkId}`);
|
|
576
|
+
return network.rpcs;
|
|
577
|
+
}
|
|
578
|
+
async getTransport(networkId) {
|
|
579
|
+
if (!this.#transports.has(networkId)) this.#transports.set(networkId, getSolTransport(networkId, await this.#getNetworkRpcs(networkId)));
|
|
580
|
+
return this.#transports.get(networkId);
|
|
581
|
+
}
|
|
582
|
+
async getRpc(networkId) {
|
|
583
|
+
if (!this.#rpcs.has(networkId)) this.#rpcs.set(networkId, createSolanaRpcFromTransport(await this.getTransport(networkId)));
|
|
584
|
+
return this.#rpcs.get(networkId);
|
|
585
|
+
}
|
|
586
|
+
/** Drops cached transports/rpcs so the next call re-reads the network's rpcs from chaindata */
|
|
587
|
+
clearRpcProvidersCache(networkId) {
|
|
588
|
+
if (networkId) {
|
|
589
|
+
this.#transports.delete(networkId);
|
|
590
|
+
this.#rpcs.delete(networkId);
|
|
591
|
+
} else {
|
|
592
|
+
this.#transports.clear();
|
|
593
|
+
this.#rpcs.clear();
|
|
594
|
+
}
|
|
595
|
+
}
|
|
1155
596
|
};
|
|
1156
|
-
|
|
1157
|
-
|
|
597
|
+
//#endregion
|
|
598
|
+
//#region src/sol/ChainConnectorSolStub.ts
|
|
1158
599
|
var ChainConnectorSolStub = class {
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
ChainConnectorEth,
|
|
1172
|
-
ChainConnectorEthStub,
|
|
1173
|
-
ChainConnectorSol,
|
|
1174
|
-
ChainConnectorSolStub,
|
|
1175
|
-
StaleRpcError,
|
|
1176
|
-
WebsocketAllocationExhaustedError
|
|
600
|
+
#transport;
|
|
601
|
+
#rpc;
|
|
602
|
+
constructor(network) {
|
|
603
|
+
this.#transport = getSolTransport(network.id, network.rpcs);
|
|
604
|
+
this.#rpc = createSolanaRpcFromTransport(this.#transport);
|
|
605
|
+
}
|
|
606
|
+
async getRpc() {
|
|
607
|
+
return this.#rpc;
|
|
608
|
+
}
|
|
609
|
+
async getTransport() {
|
|
610
|
+
return this.#transport;
|
|
611
|
+
}
|
|
1177
612
|
};
|
|
613
|
+
//#endregion
|
|
614
|
+
export { ChainConnectionError, ChainConnectorDot, ChainConnectorDotStub, ChainConnectorEth, ChainConnectorEthStub, ChainConnectorSol, ChainConnectorSolStub, StaleRpcError, getSolRpc, getSolTransport };
|
|
615
|
+
|
|
1178
616
|
//# sourceMappingURL=index.mjs.map
|