UNPKG

@polkadot/api

Version:

Promise and RxJS wrappers around the Polkadot JS RPC

1,328 lines (1,311 loc) 787 kB
(function (global, factory) { typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports, require('@polkadot/keyring'), require('@polkadot/util'), require('@polkadot/types'), require('@polkadot/types/extrinsic/constants'), require('@polkadot/util-crypto')) : typeof define === 'function' && define.amd ? define(['exports', '@polkadot/keyring', '@polkadot/util', '@polkadot/types', '@polkadot/types/extrinsic/constants', '@polkadot/util-crypto'], factory) : (global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.polkadotApi = {}, global.polkadotKeyring, global.polkadotUtil, global.polkadotTypes, global.constants, global.polkadotUtilCrypto)); })(this, (function (exports, keyring, util, types, constants, utilCrypto) { 'use strict'; const global = typeof globalThis !== "undefined" ? globalThis : typeof self !== "undefined" ? self : window; var _documentCurrentScript = typeof document !== 'undefined' ? document.currentScript : null; function evaluateThis(fn) { return fn('return this'); } const xglobal = (typeof globalThis !== 'undefined' ? globalThis : typeof global !== 'undefined' ? global : typeof self !== 'undefined' ? self : typeof window !== 'undefined' ? window : evaluateThis(Function)); const fetch = xglobal.fetch; const UNKNOWN = -99999; function extend(that, name, value) { Object.defineProperty(that, name, { configurable: true, enumerable: false, value }); } class RpcError extends Error { code; data; message; name; stack; constructor(message = '', code = UNKNOWN, data) { super(); extend(this, 'message', String(message)); extend(this, 'name', this.constructor.name); extend(this, 'data', data); extend(this, 'code', code); if (util.isFunction(Error.captureStackTrace)) { Error.captureStackTrace(this, this.constructor); } else { const { stack } = new Error(message); stack && extend(this, 'stack', stack); } } static CODES = { ASSERT: -90009, INVALID_JSONRPC: -99998, METHOD_NOT_FOUND: -32601, UNKNOWN }; } function formatErrorData(data) { if (util.isUndefined(data)) { return ''; } const formatted = `: ${util.isString(data) ? data.replace(/Error\("/g, '').replace(/\("/g, '(').replace(/"\)/g, ')').replace(/\(/g, ', ').replace(/\)/g, '') : util.stringify(data)}`; return formatted.length <= 256 ? formatted : `${formatted.substring(0, 255)}…`; } function checkError(error) { if (error) { const { code, data, message } = error; throw new RpcError(`${code}: ${message}${formatErrorData(data)}`, code, data); } } class RpcCoder { #id = 0; decodeResponse(response) { if (!response || response.jsonrpc !== '2.0') { throw new Error('Invalid jsonrpc field in decoded object'); } const isSubscription = !util.isUndefined(response.params) && !util.isUndefined(response.method); if (!util.isNumber(response.id) && (!isSubscription || (!util.isNumber(response.params.subscription) && !util.isString(response.params.subscription)))) { throw new Error('Invalid id field in decoded object'); } checkError(response.error); if (response.result === undefined && !isSubscription) { throw new Error('No result found in jsonrpc response'); } if (isSubscription) { checkError(response.params.error); return response.params.result; } return response.result; } encodeJson(method, params) { const [id, data] = this.encodeObject(method, params); return [id, util.stringify(data)]; } encodeObject(method, params) { const id = ++this.#id; return [id, { id, jsonrpc: '2.0', method, params }]; } } const HTTP_URL = 'http://127.0.0.1:9933'; const WS_URL = 'ws://127.0.0.1:9944'; const defaults = { HTTP_URL, WS_URL }; const DEFAULT_CAPACITY = 1024; const DEFAULT_TTL = 30000; const DISABLED_TTL = 31_536_000_000; class LRUNode { key; #expires; #ttl; createdAt; next; prev; constructor(key, ttl) { this.key = key; this.#ttl = ttl; this.#expires = Date.now() + ttl; this.createdAt = Date.now(); this.next = this.prev = this; } refresh() { this.#expires = Date.now() + this.#ttl; } get expiry() { return this.#expires; } } class LRUCache { capacity; #data = new Map(); #refs = new Map(); #length = 0; #head; #tail; #ttl; constructor(capacity = DEFAULT_CAPACITY, ttl = DEFAULT_TTL) { this.capacity = capacity; ttl ? this.#ttl = ttl : this.#ttl = DISABLED_TTL; this.#head = this.#tail = new LRUNode('<empty>', this.#ttl); } get ttl() { return this.#ttl; } get length() { return this.#length; } get lengthData() { return this.#data.size; } get lengthRefs() { return this.#refs.size; } entries() { const keys = this.keys(); const count = keys.length; const entries = new Array(count); for (let i = 0; i < count; i++) { const key = keys[i]; entries[i] = [key, this.#data.get(key)]; } return entries; } keys() { const keys = []; if (this.#length) { let curr = this.#head; while (curr !== this.#tail) { keys.push(curr.key); curr = curr.next; } keys.push(curr.key); } return keys; } get(key) { const data = this.#data.get(key); if (data) { this.#toHead(key); this.#evictTTL(); return data; } this.#evictTTL(); return null; } set(key, value) { if (this.#data.has(key)) { this.#toHead(key); } else { const node = new LRUNode(key, this.#ttl); this.#refs.set(node.key, node); if (this.length === 0) { this.#head = this.#tail = node; } else { this.#head.prev = node; node.next = this.#head; this.#head = node; } if (this.#length === this.capacity) { this.#data.delete(this.#tail.key); this.#refs.delete(this.#tail.key); this.#tail = this.#tail.prev; this.#tail.next = this.#head; } else { this.#length += 1; } } this.#evictTTL(); this.#data.set(key, value); } #evictTTL() { while (this.#tail.expiry && this.#tail.expiry < Date.now() && this.#length > 0) { this.#refs.delete(this.#tail.key); this.#data.delete(this.#tail.key); this.#length -= 1; this.#tail = this.#tail.prev; this.#tail.next = this.#head; } if (this.#length === 0) { this.#head = this.#tail = new LRUNode('<empty>', this.#ttl); } } #toHead(key) { const ref = this.#refs.get(key); if (ref && ref !== this.#head) { ref.refresh(); ref.prev.next = ref.next; ref.next.prev = ref.prev; ref.next = this.#head; this.#head.prev = ref; this.#head = ref; } } } const ERROR_SUBSCRIBE = 'HTTP Provider does not have subscriptions, use WebSockets instead'; const l$7 = util.logger('api-http'); class HttpProvider { #callCache; #cacheCapacity; #coder; #endpoint; #headers; #stats; #ttl; constructor(endpoint = defaults.HTTP_URL, headers = {}, cacheCapacity, cacheTtl) { if (!/^(https|http):\/\//.test(endpoint)) { throw new Error(`Endpoint should start with 'http://' or 'https://', received '${endpoint}'`); } this.#coder = new RpcCoder(); this.#endpoint = endpoint; this.#headers = headers; this.#cacheCapacity = cacheCapacity === 0 ? 0 : cacheCapacity || DEFAULT_CAPACITY; const ttl = cacheTtl === undefined ? DEFAULT_TTL : cacheTtl; this.#callCache = new LRUCache(cacheCapacity === 0 ? 0 : cacheCapacity || DEFAULT_CAPACITY, ttl); this.#ttl = cacheTtl; this.#stats = { active: { requests: 0, subscriptions: 0 }, total: { bytesRecv: 0, bytesSent: 0, cached: 0, errors: 0, requests: 0, subscriptions: 0, timeout: 0 } }; } get hasSubscriptions() { return !!false; } clone() { return new HttpProvider(this.#endpoint, this.#headers); } async connect() { } async disconnect() { } get stats() { return this.#stats; } get ttl() { return this.#ttl; } get isClonable() { return !!true; } get isConnected() { return !!true; } on(_type, _sub) { l$7.error('HTTP Provider does not have \'on\' emitters, use WebSockets instead'); return util.noop; } async send(method, params, isCacheable) { this.#stats.total.requests++; const [, body] = this.#coder.encodeJson(method, params); if (this.#cacheCapacity === 0) { return this.#send(body); } const cacheKey = isCacheable ? `${method}::${util.stringify(params)}` : ''; let resultPromise = isCacheable ? this.#callCache.get(cacheKey) : null; if (!resultPromise) { resultPromise = this.#send(body); if (isCacheable) { this.#callCache.set(cacheKey, resultPromise); } } else { this.#stats.total.cached++; } return resultPromise; } async #send(body) { this.#stats.active.requests++; this.#stats.total.bytesSent += body.length; try { const response = await fetch(this.#endpoint, { body, headers: { Accept: 'application/json', 'Content-Length': `${body.length}`, 'Content-Type': 'application/json', ...this.#headers }, method: 'POST' }); if (!response.ok) { throw new Error(`[${response.status}]: ${response.statusText}`); } const result = await response.text(); this.#stats.total.bytesRecv += result.length; const decoded = this.#coder.decodeResponse(JSON.parse(result)); this.#stats.active.requests--; return decoded; } catch (e) { this.#stats.active.requests--; this.#stats.total.errors++; const { method, params } = JSON.parse(body); const rpcError = e; const failedRequest = `\nFailed HTTP Request: ${JSON.stringify({ method, params })}`; rpcError.message = `${rpcError.message}${failedRequest}`; throw rpcError; } } async subscribe(_types, _method, _params, _cb) { l$7.error(ERROR_SUBSCRIBE); throw new Error(ERROR_SUBSCRIBE); } async unsubscribe(_type, _method, _id) { l$7.error(ERROR_SUBSCRIBE); throw new Error(ERROR_SUBSCRIBE); } } function getDefaultExportFromCjs (x) { return x && x.__esModule && Object.prototype.hasOwnProperty.call(x, 'default') ? x['default'] : x; } var eventemitter3 = {exports: {}}; (function (module) { var has = Object.prototype.hasOwnProperty , prefix = '~'; function Events() {} if (Object.create) { Events.prototype = Object.create(null); if (!new Events().__proto__) prefix = false; } function EE(fn, context, once) { this.fn = fn; this.context = context; this.once = once || false; } function addListener(emitter, event, fn, context, once) { if (typeof fn !== 'function') { throw new TypeError('The listener must be a function'); } var listener = new EE(fn, context || emitter, once) , evt = prefix ? prefix + event : event; if (!emitter._events[evt]) emitter._events[evt] = listener, emitter._eventsCount++; else if (!emitter._events[evt].fn) emitter._events[evt].push(listener); else emitter._events[evt] = [emitter._events[evt], listener]; return emitter; } function clearEvent(emitter, evt) { if (--emitter._eventsCount === 0) emitter._events = new Events(); else delete emitter._events[evt]; } function EventEmitter() { this._events = new Events(); this._eventsCount = 0; } EventEmitter.prototype.eventNames = function eventNames() { var names = [] , events , name; if (this._eventsCount === 0) return names; for (name in (events = this._events)) { if (has.call(events, name)) names.push(prefix ? name.slice(1) : name); } if (Object.getOwnPropertySymbols) { return names.concat(Object.getOwnPropertySymbols(events)); } return names; }; EventEmitter.prototype.listeners = function listeners(event) { var evt = prefix ? prefix + event : event , handlers = this._events[evt]; if (!handlers) return []; if (handlers.fn) return [handlers.fn]; for (var i = 0, l = handlers.length, ee = new Array(l); i < l; i++) { ee[i] = handlers[i].fn; } return ee; }; EventEmitter.prototype.listenerCount = function listenerCount(event) { var evt = prefix ? prefix + event : event , listeners = this._events[evt]; if (!listeners) return 0; if (listeners.fn) return 1; return listeners.length; }; EventEmitter.prototype.emit = function emit(event, a1, a2, a3, a4, a5) { var evt = prefix ? prefix + event : event; if (!this._events[evt]) return false; var listeners = this._events[evt] , len = arguments.length , args , i; if (listeners.fn) { if (listeners.once) this.removeListener(event, listeners.fn, undefined, true); switch (len) { case 1: return listeners.fn.call(listeners.context), true; case 2: return listeners.fn.call(listeners.context, a1), true; case 3: return listeners.fn.call(listeners.context, a1, a2), true; case 4: return listeners.fn.call(listeners.context, a1, a2, a3), true; case 5: return listeners.fn.call(listeners.context, a1, a2, a3, a4), true; case 6: return listeners.fn.call(listeners.context, a1, a2, a3, a4, a5), true; } for (i = 1, args = new Array(len -1); i < len; i++) { args[i - 1] = arguments[i]; } listeners.fn.apply(listeners.context, args); } else { var length = listeners.length , j; for (i = 0; i < length; i++) { if (listeners[i].once) this.removeListener(event, listeners[i].fn, undefined, true); switch (len) { case 1: listeners[i].fn.call(listeners[i].context); break; case 2: listeners[i].fn.call(listeners[i].context, a1); break; case 3: listeners[i].fn.call(listeners[i].context, a1, a2); break; case 4: listeners[i].fn.call(listeners[i].context, a1, a2, a3); break; default: if (!args) for (j = 1, args = new Array(len -1); j < len; j++) { args[j - 1] = arguments[j]; } listeners[i].fn.apply(listeners[i].context, args); } } } return true; }; EventEmitter.prototype.on = function on(event, fn, context) { return addListener(this, event, fn, context, false); }; EventEmitter.prototype.once = function once(event, fn, context) { return addListener(this, event, fn, context, true); }; EventEmitter.prototype.removeListener = function removeListener(event, fn, context, once) { var evt = prefix ? prefix + event : event; if (!this._events[evt]) return this; if (!fn) { clearEvent(this, evt); return this; } var listeners = this._events[evt]; if (listeners.fn) { if ( listeners.fn === fn && (!once || listeners.once) && (!context || listeners.context === context) ) { clearEvent(this, evt); } } else { for (var i = 0, events = [], length = listeners.length; i < length; i++) { if ( listeners[i].fn !== fn || (once && !listeners[i].once) || (context && listeners[i].context !== context) ) { events.push(listeners[i]); } } if (events.length) this._events[evt] = events.length === 1 ? events[0] : events; else clearEvent(this, evt); } return this; }; EventEmitter.prototype.removeAllListeners = function removeAllListeners(event) { var evt; if (event) { evt = prefix ? prefix + event : event; if (this._events[evt]) clearEvent(this, evt); } else { this._events = new Events(); this._eventsCount = 0; } return this; }; EventEmitter.prototype.off = EventEmitter.prototype.removeListener; EventEmitter.prototype.addListener = EventEmitter.prototype.on; EventEmitter.prefixed = prefix; EventEmitter.EventEmitter = EventEmitter; { module.exports = EventEmitter; } } (eventemitter3)); var eventemitter3Exports = eventemitter3.exports; const EventEmitter = getDefaultExportFromCjs(eventemitter3Exports); function healthChecker() { let checker = null; let sendJsonRpc = null; return { responsePassThrough: (jsonRpcResponse) => { if (checker === null) { return jsonRpcResponse; } return checker.responsePassThrough(jsonRpcResponse); }, sendJsonRpc: (request) => { if (!sendJsonRpc) { throw new Error('setSendJsonRpc must be called before sending requests'); } if (checker === null) { sendJsonRpc(request); } else { checker.sendJsonRpc(request); } }, setSendJsonRpc: (cb) => { sendJsonRpc = cb; }, start: (healthCallback) => { if (checker !== null) { throw new Error("Can't start the health checker multiple times in parallel"); } else if (!sendJsonRpc) { throw new Error('setSendJsonRpc must be called before starting the health checks'); } checker = new InnerChecker(healthCallback, sendJsonRpc); checker.update(true); }, stop: () => { if (checker === null) { return; } checker.destroy(); checker = null; } }; } class InnerChecker { #healthCallback; #currentHealthCheckId = null; #currentHealthTimeout = null; #currentSubunsubRequestId = null; #currentSubscriptionId = null; #requestToSmoldot; #isSyncing = false; #nextRequestId = 0; constructor(healthCallback, requestToSmoldot) { this.#healthCallback = healthCallback; this.#requestToSmoldot = (request) => requestToSmoldot(util.stringify(request)); } sendJsonRpc = (request) => { let parsedRequest; try { parsedRequest = JSON.parse(request); } catch { return; } if (parsedRequest.id) { const newId = 'extern:' + util.stringify(parsedRequest.id); parsedRequest.id = newId; } this.#requestToSmoldot(parsedRequest); }; responsePassThrough = (jsonRpcResponse) => { let parsedResponse; try { parsedResponse = JSON.parse(jsonRpcResponse); } catch { return jsonRpcResponse; } if (parsedResponse.id && this.#currentHealthCheckId === parsedResponse.id) { this.#currentHealthCheckId = null; if (!parsedResponse.result) { this.update(false); return null; } this.#healthCallback(parsedResponse.result); this.#isSyncing = parsedResponse.result.isSyncing; this.update(false); return null; } if (parsedResponse.id && this.#currentSubunsubRequestId === parsedResponse.id) { this.#currentSubunsubRequestId = null; if (!parsedResponse.result) { this.update(false); return null; } if (this.#currentSubscriptionId) { this.#currentSubscriptionId = null; } else { this.#currentSubscriptionId = parsedResponse.result; } this.update(false); return null; } if (parsedResponse.params && this.#currentSubscriptionId && parsedResponse.params.subscription === this.#currentSubscriptionId) { this.update(true); return null; } if (parsedResponse.id) { const id = parsedResponse.id; if (!id.startsWith('extern:')) { throw new Error('State inconsistency in health checker'); } const newId = JSON.parse(id.slice('extern:'.length)); parsedResponse.id = newId; } return util.stringify(parsedResponse); }; update = (startNow) => { if (startNow && this.#currentHealthTimeout) { clearTimeout(this.#currentHealthTimeout); this.#currentHealthTimeout = null; } if (!this.#currentHealthTimeout) { const startHealthRequest = () => { this.#currentHealthTimeout = null; if (this.#currentHealthCheckId) { return; } this.#currentHealthCheckId = `health-checker:${this.#nextRequestId}`; this.#nextRequestId += 1; this.#requestToSmoldot({ id: this.#currentHealthCheckId, jsonrpc: '2.0', method: 'system_health', params: [] }); }; if (startNow) { startHealthRequest(); } else { this.#currentHealthTimeout = setTimeout(startHealthRequest, 1000); } } if (this.#isSyncing && !this.#currentSubscriptionId && !this.#currentSubunsubRequestId) { this.startSubscription(); } if (!this.#isSyncing && this.#currentSubscriptionId && !this.#currentSubunsubRequestId) { this.endSubscription(); } }; startSubscription = () => { if (this.#currentSubunsubRequestId || this.#currentSubscriptionId) { throw new Error('Internal error in health checker'); } this.#currentSubunsubRequestId = `health-checker:${this.#nextRequestId}`; this.#nextRequestId += 1; this.#requestToSmoldot({ id: this.#currentSubunsubRequestId, jsonrpc: '2.0', method: 'chain_subscribeNewHeads', params: [] }); }; endSubscription = () => { if (this.#currentSubunsubRequestId || !this.#currentSubscriptionId) { throw new Error('Internal error in health checker'); } this.#currentSubunsubRequestId = `health-checker:${this.#nextRequestId}`; this.#nextRequestId += 1; this.#requestToSmoldot({ id: this.#currentSubunsubRequestId, jsonrpc: '2.0', method: 'chain_unsubscribeNewHeads', params: [this.#currentSubscriptionId] }); }; destroy = () => { if (this.#currentHealthTimeout) { clearTimeout(this.#currentHealthTimeout); this.#currentHealthTimeout = null; } }; } const l$6 = util.logger('api-substrate-connect'); const subscriptionUnsubscriptionMethods = new Map([ ['author_submitAndWatchExtrinsic', 'author_unwatchExtrinsic'], ['chain_subscribeAllHeads', 'chain_unsubscribeAllHeads'], ['chain_subscribeFinalizedHeads', 'chain_unsubscribeFinalizedHeads'], ['chain_subscribeFinalisedHeads', 'chain_subscribeFinalisedHeads'], ['chain_subscribeNewHeads', 'chain_unsubscribeNewHeads'], ['chain_subscribeNewHead', 'chain_unsubscribeNewHead'], ['chain_subscribeRuntimeVersion', 'chain_unsubscribeRuntimeVersion'], ['subscribe_newHead', 'unsubscribe_newHead'], ['state_subscribeRuntimeVersion', 'state_unsubscribeRuntimeVersion'], ['state_subscribeStorage', 'state_unsubscribeStorage'] ]); const scClients = new WeakMap(); class ScProvider { #Sc; #coder = new RpcCoder(); #spec; #sharedSandbox; #subscriptions = new Map(); #resubscribeMethods = new Map(); #requests = new Map(); #wellKnownChains; #eventemitter = new EventEmitter(); #chain = null; #isChainReady = false; constructor(Sc, spec, sharedSandbox) { if (!util.isObject(Sc) || !util.isObject(Sc.WellKnownChain) || !util.isFunction(Sc.createScClient)) { throw new Error('Expected an @substrate/connect interface as first parameter to ScProvider'); } this.#Sc = Sc; this.#spec = spec; this.#sharedSandbox = sharedSandbox; this.#wellKnownChains = new Set(Object.values(Sc.WellKnownChain)); } get hasSubscriptions() { return !!true; } get isClonable() { return !!false; } get isConnected() { return !!this.#chain && this.#isChainReady; } clone() { throw new Error('clone() is not supported.'); } async connect(config, checkerFactory = healthChecker) { if (this.isConnected) { throw new Error('Already connected!'); } if (this.#chain) { await this.#chain; return; } if (this.#sharedSandbox && !this.#sharedSandbox.isConnected) { await this.#sharedSandbox.connect(); } const client = this.#sharedSandbox ? scClients.get(this.#sharedSandbox) : this.#Sc.createScClient(config); if (!client) { throw new Error('Unknown ScProvider!'); } scClients.set(this, client); const hc = checkerFactory(); const onResponse = (res) => { const hcRes = hc.responsePassThrough(res); if (!hcRes) { return; } const response = JSON.parse(hcRes); let decodedResponse; try { decodedResponse = this.#coder.decodeResponse(response); } catch (e) { decodedResponse = e; } if (response.params?.subscription === undefined || !response.method) { return this.#requests.get(response.id)?.(decodedResponse); } const subscriptionId = `${response.method}::${response.params.subscription}`; const callback = this.#subscriptions.get(subscriptionId)?.[0]; callback?.(decodedResponse); }; const addChain = this.#sharedSandbox ? (async (...args) => { const source = this.#sharedSandbox; return (await source.#chain).addChain(...args); }) : this.#wellKnownChains.has(this.#spec) ? client.addWellKnownChain : client.addChain; this.#chain = addChain(this.#spec, onResponse).then((chain) => { hc.setSendJsonRpc(chain.sendJsonRpc); this.#isChainReady = false; const cleanup = () => { const disconnectionError = new Error('Disconnected'); this.#requests.forEach((cb) => cb(disconnectionError)); this.#subscriptions.forEach(([cb]) => cb(disconnectionError)); this.#subscriptions.clear(); }; const staleSubscriptions = []; const killStaleSubscriptions = () => { if (staleSubscriptions.length === 0) { return; } const stale = staleSubscriptions.pop(); if (!stale) { throw new Error('Unable to get stale subscription'); } const { id, unsubscribeMethod } = stale; Promise .race([ this.send(unsubscribeMethod, [id]).catch(util.noop), new Promise((resolve) => setTimeout(resolve, 500)) ]) .then(killStaleSubscriptions) .catch(util.noop); }; hc.start((health) => { const isReady = !health.isSyncing && (health.peers > 0 || !health.shouldHavePeers); if (this.#isChainReady === isReady) { return; } this.#isChainReady = isReady; if (!isReady) { [...this.#subscriptions.values()].forEach((s) => { staleSubscriptions.push(s[1]); }); cleanup(); this.#eventemitter.emit('disconnected'); } else { killStaleSubscriptions(); this.#eventemitter.emit('connected'); if (this.#resubscribeMethods.size) { this.#resubscribe(); } } }); return util.objectSpread({}, chain, { remove: () => { hc.stop(); chain.remove(); cleanup(); }, sendJsonRpc: hc.sendJsonRpc.bind(hc) }); }); try { await this.#chain; } catch (e) { this.#chain = null; this.#eventemitter.emit('error', e); throw e; } } #resubscribe = () => { const promises = []; this.#resubscribeMethods.forEach((subDetails) => { if (subDetails.type.startsWith('author_')) { return; } try { const promise = new Promise((resolve) => { this.subscribe(subDetails.type, subDetails.method, subDetails.params, subDetails.callback).catch((error) => console.log(error)); resolve(); }); promises.push(promise); } catch (error) { l$6.error(error); } }); Promise.all(promises).catch((err) => l$6.log(err)); }; async disconnect() { if (!this.#chain) { return; } const chain = await this.#chain; this.#chain = null; this.#isChainReady = false; try { chain.remove(); } catch (_) { } this.#eventemitter.emit('disconnected'); } on(type, sub) { if (type === 'connected' && this.isConnected) { sub(); } this.#eventemitter.on(type, sub); return () => { this.#eventemitter.removeListener(type, sub); }; } async send(method, params) { if (!this.isConnected || !this.#chain) { throw new Error('Provider is not connected'); } const chain = await this.#chain; const [id, json] = this.#coder.encodeJson(method, params); const result = new Promise((resolve, reject) => { this.#requests.set(id, (response) => { (util.isError(response) ? reject : resolve)(response); }); try { chain.sendJsonRpc(json); } catch (e) { this.#chain = null; try { chain.remove(); } catch (_) { } this.#eventemitter.emit('error', e); } }); try { return await result; } finally { this.#requests.delete(id); } } async subscribe(type, method, params, callback) { if (!subscriptionUnsubscriptionMethods.has(method)) { throw new Error(`Unsupported subscribe method: ${method}`); } const id = await this.send(method, params); const subscriptionId = `${type}::${id}`; const cb = (response) => { if (response instanceof Error) { callback(response, undefined); } else { callback(null, response); } }; const unsubscribeMethod = subscriptionUnsubscriptionMethods.get(method); if (!unsubscribeMethod) { throw new Error('Invalid unsubscribe method found'); } this.#resubscribeMethods.set(subscriptionId, { callback, method, params, type }); this.#subscriptions.set(subscriptionId, [cb, { id, unsubscribeMethod }]); return id; } unsubscribe(type, method, id) { if (!this.isConnected) { throw new Error('Provider is not connected'); } const subscriptionId = `${type}::${id}`; if (!this.#subscriptions.has(subscriptionId)) { return Promise.reject(new Error(`Unable to find active subscription=${subscriptionId}`)); } this.#resubscribeMethods.delete(subscriptionId); this.#subscriptions.delete(subscriptionId); return this.send(method, [id]); } } const WebSocket = xglobal.WebSocket; const known = { 1000: 'Normal Closure', 1001: 'Going Away', 1002: 'Protocol Error', 1003: 'Unsupported Data', 1004: '(For future)', 1005: 'No Status Received', 1006: 'Abnormal Closure', 1007: 'Invalid frame payload data', 1008: 'Policy Violation', 1009: 'Message too big', 1010: 'Missing Extension', 1011: 'Internal Error', 1012: 'Service Restart', 1013: 'Try Again Later', 1014: 'Bad Gateway', 1015: 'TLS Handshake' }; function getWSErrorString(code) { if (code >= 0 && code <= 999) { return '(Unused)'; } else if (code >= 1016) { if (code <= 1999) { return '(For WebSocket standard)'; } else if (code <= 2999) { return '(For WebSocket extensions)'; } else if (code <= 3999) { return '(For libraries and frameworks)'; } else if (code <= 4999) { return '(For applications)'; } } return known[code] || '(Unknown)'; } const ALIASES = { chain_finalisedHead: 'chain_finalizedHead', chain_subscribeFinalisedHeads: 'chain_subscribeFinalizedHeads', chain_unsubscribeFinalisedHeads: 'chain_unsubscribeFinalizedHeads' }; const RETRY_DELAY = 2_500; const DEFAULT_TIMEOUT_MS = 60 * 1000; const TIMEOUT_INTERVAL = 5_000; const l$5 = util.logger('api-ws'); function eraseRecord(record, cb) { Object.keys(record).forEach((key) => { if (cb) { cb(record[key]); } delete record[key]; }); } function defaultEndpointStats() { return { bytesRecv: 0, bytesSent: 0, cached: 0, errors: 0, requests: 0, subscriptions: 0, timeout: 0 }; } class WsProvider { #callCache; #coder; #endpoints; #headers; #eventemitter; #handlers = {}; #isReadyPromise; #stats; #waitingForId = {}; #cacheCapacity; #ttl; #autoConnectMs; #endpointIndex; #endpointStats; #isConnected = false; #subscriptions = {}; #timeoutId = null; #websocket; #timeout; constructor(endpoint = defaults.WS_URL, autoConnectMs = RETRY_DELAY, headers = {}, timeout, cacheCapacity, cacheTtl) { const endpoints = Array.isArray(endpoint) ? endpoint : [endpoint]; if (endpoints.length === 0) { throw new Error('WsProvider requires at least one Endpoint'); } endpoints.forEach((endpoint) => { if (!/^(wss|ws):\/\//.test(endpoint)) { throw new Error(`Endpoint should start with 'ws://', received '${endpoint}'`); } }); const ttl = cacheTtl === undefined ? DEFAULT_TTL : cacheTtl; this.#callCache = new LRUCache(cacheCapacity === 0 ? 0 : cacheCapacity || DEFAULT_CAPACITY, ttl); this.#ttl = cacheTtl; this.#cacheCapacity = cacheCapacity || DEFAULT_CAPACITY; this.#eventemitter = new EventEmitter(); this.#autoConnectMs = autoConnectMs || 0; this.#coder = new RpcCoder(); this.#endpointIndex = -1; this.#endpoints = endpoints; this.#headers = headers; this.#websocket = null; this.#stats = { active: { requests: 0, subscriptions: 0 }, total: defaultEndpointStats() }; this.#endpointStats = defaultEndpointStats(); this.#timeout = timeout || DEFAULT_TIMEOUT_MS; if (autoConnectMs && autoConnectMs > 0) { this.connectWithRetry().catch(util.noop); } this.#isReadyPromise = new Promise((resolve) => { this.#eventemitter.once('connected', () => { resolve(this); }); }); } get hasSubscriptions() { return !!true; } get isClonable() { return !!true; } get isConnected() { return this.#isConnected; } get isReady() { return this.#isReadyPromise; } get endpoint() { return this.#endpoints[this.#endpointIndex]; } clone() { return new WsProvider(this.#endpoints); } selectEndpointIndex(endpoints) { return (this.#endpointIndex + 1) % endpoints.length; } async connect() { if (this.#websocket) { throw new Error('WebSocket is already connected'); } try { this.#endpointIndex = this.selectEndpointIndex(this.#endpoints); this.#websocket = typeof xglobal.WebSocket !== 'undefined' && util.isChildClass(xglobal.WebSocket, WebSocket) ? new WebSocket(this.endpoint) : new WebSocket(this.endpoint, undefined, { headers: this.#headers }); if (this.#websocket) { this.#websocket.onclose = this.#onSocketClose; this.#websocket.onerror = this.#onSocketError; this.#websocket.onmessage = this.#onSocketMessage; this.#websocket.onopen = this.#onSocketOpen; } this.#timeoutId = setInterval(() => this.#timeoutHandlers(), TIMEOUT_INTERVAL); } catch (error) { l$5.error(error); this.#emit('error', error); throw error; } } async connectWithRetry() { if (this.#autoConnectMs > 0) { try { await this.connect(); } catch { setTimeout(() => { this.connectWithRetry().catch(util.noop); }, this.#autoConnectMs); } } } async disconnect() { this.#autoConnectMs = 0; try { if (this.#websocket) { this.#websocket.close(1000); } } catch (error) { l$5.error(error); this.#emit('error', error); throw error; } } get stats() { return { active: { requests: Object.keys(this.#handlers).length, subscriptions: Object.keys(this.#subscriptions).length }, total: this.#stats.total }; } get ttl() { return this.#ttl; } get endpointStats() { return this.#endpointStats; } on(type, sub) { this.#eventemitter.on(type, sub); return () => { this.#eventemitter.removeListener(type, sub); }; } send(method, params, isCacheable, subscription) { this.#endpointStats.requests++; this.#stats.total.requests++; const [id, body] = this.#coder.encodeJson(method, params); if (this.#cacheCapacity === 0) { return this.#send(id, body, method, params, subscription); } const cacheKey = isCacheable ? `${method}::${util.stringify(params)}` : ''; let resultPromise = isCacheable ? this.#callCache.get(cacheKey) : null; if (!resultPromise) { resultPromise = this.#send(id, body, method, params, subscription); if (isCacheable) { this.#callCache.set(cacheKey, resultPromise); } } else { this.#endpointStats.cached++; this.#stats.total.cached++; } return resultPromise; } async #send(id, body, method, params, subscription) { return new Promise((resolve, reject) => { try { if (!this.isConnected || this.#websocket === null) { throw new Error('WebSocket is not connected'); } const callback = (error, result) => { error ? reject(error) : resolve(result); }; l$5.debug(() => ['calling', method, body]); this.#handlers[id] = { callback, method, params, start: Date.now(), subscription }; const bytesSent = body.length; this.#endpointStats.bytesSent += bytesSent; this.#stats.total.bytesSent += bytesSent; this.#websocket.send(body); } catch (error) { this.#endpointStats.errors++; this.#stats.total.errors++; const rpcError = error; const failedRequest = `\nFailed WS Request: ${JSON.stringify({ method, params })}`; rpcError.message = `${rpcError.message}${failedRequest}`; reject(rpcError); } }); } subscribe(type, method, params, callback) { this.#endpointStats.subscriptions++; this.#stats.total.subscriptions++; return this.send(method, params, false, { callback, type }); } async unsubscribe(type, method, id) { const subscription = `${type}::${id}`; if (util.isUndefined(this.#subscriptions[subscription])) { l$5.debug(() => `Unable to find active subscription=${subscription}`); return false; } delete this.#subscriptions[subscription]; try { return this.isConnected && !util.isNull(this.#websocket) ? this.send(method, [id]) : true; } catch { return false; } } #emit = (type, ...args) => { this.#eventemitter.emit(type, ...args); }; #onSocketClose = (event) => { const error = new Error(`disconnected from ${this.endpoint}: ${event.code}:: ${event.reason || getWSErrorString(event.code)}`); if (this.#autoConnectMs > 0) { l$5.error(error.message); } this.#isConnected = false; if (this.#websocket) { this.#websocket.onclose = null; this.#websocket.onerror = null; this.#websocket.onmessage = null; this.#websocket.onopen = null; this.#websocket = null; } if (this.#timeoutId) { clearInterval(this.#timeoutId); this.#timeoutId = null; } eraseRecord(this.#handlers, (h) => { try { h.callback(error, undefined); } catch (err) { l$5.error(err); } }); eraseRecord(this.#waitingForId); this.#endpointStats = defaultEndpointStats(); this.#emit('disconnected'); if (this.#autoConnectMs > 0) { setTimeout(() => { this.connectWithRetry().catch(util.noop); }, this.#autoConnectMs); } }; #onSocketError = (error) => { l$5.debug(() => ['socket error', error]); this.#emit('error', error);