@polkadot/api
Version:
Promise and RxJS wrappers around the Polkadot JS RPC
1,328 lines (1,311 loc) • 787 kB
JavaScript
(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);