@polkadot/api
Version:
Promise and RxJS wrappers around the Polkadot JS RPC
575 lines (443 loc) • 23.3 kB
JavaScript
// Copyright 2017-2022 @polkadot/api authors & contributors
// SPDX-License-Identifier: Apache-2.0
import { BehaviorSubject, combineLatest, from, map, of, switchMap, tap, toArray } from 'rxjs';
import { getAvailableDerives } from '@polkadot/api-derive';
import { memo, RpcCore } from '@polkadot/rpc-core';
import { WsProvider } from '@polkadot/rpc-provider';
import { expandMetadata, TypeRegistry, unwrapStorageType } from '@polkadot/types';
import { arrayChunk, arrayFlatten, assert, assertReturn, BN, BN_ZERO, compactStripLength, lazyMethod, lazyMethods, logger, objectSpread, u8aToHex } from '@polkadot/util';
import { createSubmittable } from "../submittable/index.js";
import { augmentObject } from "../util/augmentObject.js";
import { decorateDeriveSections } from "../util/decorate.js";
import { extractStorageArgs } from "../util/validate.js";
import { Events } from "./Events.js";
import { findCall, findError } from "./find.js";
// the max amount of keys/values that we will retrieve at once
const PAGE_SIZE_K = 1000; // limit aligned with the 1k on the node (trie lookups are heavy)
const PAGE_SIZE_V = 250; // limited since the data may be very large (e.g. misfiring elections)
const l = logger('api/init');
let instanceCounter = 0;
function getAtQueryFn(api, {
method,
section
}) {
return assertReturn(api.rx.query[section] && api.rx.query[section][method], () => `query.${section}.${method} is not available in this version of the metadata`);
}
export class Decorate extends Events {
#instanceId;
#registry; // HACK Use BN import so decorateDerive works... yes, wtf.
__phantom = new BN(0);
_consts = {};
_errors = {};
_events = {};
_extrinsicType = 4; // latest extrinsic version
_isReady = false;
_query = {};
_rx = {
consts: {},
query: {},
tx: {}
};
/**
* @description Create an instance of the class
*
* @param options Options object to create API instance or a Provider instance
*
* @example
* <BR>
*
* ```javascript
* import Api from '@polkadot/api/promise';
*
* const api = new Api().isReady();
*
* api.rpc.subscribeNewHeads((header) => {
* console.log(`new block #${header.number.toNumber()}`);
* });
* ```
*/
constructor(options, type, decorateMethod) {
var _options$source;
super();
this.#instanceId = `${++instanceCounter}`;
this.#registry = ((_options$source = options.source) === null || _options$source === void 0 ? void 0 : _options$source.registry) || options.registry || new TypeRegistry();
this._rx.queryAt = (blockHash, knownVersion) => from(this.at(blockHash, knownVersion)).pipe(map(a => a.rx.query));
this._rx.registry = this.#registry;
const thisProvider = options.source ? options.source._rpcCore.provider.clone() : options.provider || new WsProvider();
this._decorateMethod = decorateMethod;
this._options = options;
this._type = type; // The RPC interface decorates the known interfaces on init
this._rpcCore = new RpcCore(this.#instanceId, this.#registry, thisProvider, this._options.rpc);
this._isConnected = new BehaviorSubject(this._rpcCore.provider.isConnected);
this._rx.hasSubscriptions = this._rpcCore.provider.hasSubscriptions;
}
/**
* @description Return the current used registry
*/
get registry() {
return this.#registry;
}
/**
* @description Creates an instance of a type as registered
*/
createType(type, ...params) {
return this.#registry.createType(type, ...params);
}
/**
* @description Register additional user-defined of chain-specific types in the type registry
*/
registerTypes(types) {
types && this.#registry.register(types);
}
/**
* @returns `true` if the API operates with subscriptions
*/
get hasSubscriptions() {
return this._rpcCore.provider.hasSubscriptions;
}
/**
* @returns `true` if the API decorate multi-key queries
*/
get supportMulti() {
return this._rpcCore.provider.hasSubscriptions || !!this._rpcCore.state.queryStorageAt;
}
_emptyDecorated(registry, blockHash) {
return {
consts: {},
errors: {},
events: {},
query: {},
registry,
rx: {
query: {}
},
tx: createSubmittable(this._type, this._rx, this._decorateMethod, registry, blockHash)
};
}
_createDecorated(registry, fromEmpty, decoratedApi, blockHash) {
if (!decoratedApi) {
decoratedApi = this._emptyDecorated(registry.registry, blockHash);
}
if (fromEmpty || !registry.decoratedMeta) {
registry.decoratedMeta = expandMetadata(registry.registry, registry.metadata);
}
const storage = this._decorateStorage(registry.decoratedMeta, this._decorateMethod, blockHash);
const storageRx = this._decorateStorage(registry.decoratedMeta, this._rxDecorateMethod, blockHash);
augmentObject('consts', registry.decoratedMeta.consts, decoratedApi.consts, fromEmpty);
augmentObject('errors', registry.decoratedMeta.errors, decoratedApi.errors, fromEmpty);
augmentObject('events', registry.decoratedMeta.events, decoratedApi.events, fromEmpty);
augmentObject('query', storage, decoratedApi.query, fromEmpty);
augmentObject('query', storageRx, decoratedApi.rx.query, fromEmpty);
decoratedApi.findCall = callIndex => findCall(registry.registry, callIndex);
decoratedApi.findError = errorIndex => findError(registry.registry, errorIndex);
decoratedApi.queryMulti = blockHash ? this._decorateMultiAt(decoratedApi, this._decorateMethod, blockHash) : this._decorateMulti(this._decorateMethod);
return {
decoratedApi,
decoratedMeta: registry.decoratedMeta
};
}
_injectMetadata(registry, fromEmpty = false) {
// clear the decoration, we are redoing it here
if (fromEmpty || !registry.decoratedApi) {
registry.decoratedApi = this._emptyDecorated(registry.registry);
}
const {
decoratedApi,
decoratedMeta
} = this._createDecorated(registry, fromEmpty, registry.decoratedApi);
this._consts = decoratedApi.consts;
this._errors = decoratedApi.errors;
this._events = decoratedApi.events;
this._query = decoratedApi.query;
this._rx.query = decoratedApi.rx.query;
const tx = this._decorateExtrinsics(decoratedMeta, this._decorateMethod);
const rxtx = this._decorateExtrinsics(decoratedMeta, this._rxDecorateMethod);
if (fromEmpty || !this._extrinsics) {
this._extrinsics = tx;
this._rx.tx = rxtx;
} else {
augmentObject('tx', tx, this._extrinsics, false);
augmentObject(null, rxtx, this._rx.tx, false);
}
augmentObject(null, decoratedMeta.consts, this._rx.consts, fromEmpty);
this.emit('decorated');
}
/**
* @deprecated
* backwards compatible endpoint for metadata injection, may be removed in the future (However, it is still useful for testing injection)
*/
injectMetadata(metadata, fromEmpty, registry) {
this._injectMetadata({
metadata,
registry: registry || this.#registry,
specName: this.#registry.createType('Text'),
specVersion: BN_ZERO
}, fromEmpty);
}
_decorateFunctionMeta(input, output) {
output.meta = input.meta;
output.method = input.method;
output.section = input.section;
output.toJSON = input.toJSON;
if (input.callIndex) {
output.callIndex = input.callIndex;
}
return output;
} // Filter all RPC methods based on the results of the rpc_methods call. We do this in the following
// manner to cater for both old and new:
// - when the number of entries are 0, only remove the ones with isOptional (account & contracts)
// - when non-zero, remove anything that is not in the array (we don't do this)
_filterRpc(methods, additional) {
// add any specific user-base RPCs
if (Object.keys(additional).length !== 0) {
this._rpcCore.addUserInterfaces(additional); // re-decorate, only adding any new additional interfaces
this._decorateRpc(this._rpcCore, this._decorateMethod, this._rpc);
this._decorateRpc(this._rpcCore, this._rxDecorateMethod, this._rx.rpc);
}
this._filterRpcMethods(methods);
}
_filterRpcMethods(exposed) {
const hasResults = exposed.length !== 0;
const allKnown = [...this._rpcCore.mapping.entries()];
const allKeys = [];
for (let i = 0; i < allKnown.length; i++) {
const [, {
alias,
endpoint,
method,
pubsub,
section
}] = allKnown[i];
allKeys.push(`${section}_${method}`);
if (pubsub) {
allKeys.push(`${section}_${pubsub[1]}`);
allKeys.push(`${section}_${pubsub[2]}`);
}
if (alias) {
allKeys.push(...alias);
}
if (endpoint) {
allKeys.push(endpoint);
}
}
const filterKey = k => !allKeys.includes(k);
const unknown = exposed.filter(filterKey);
if (unknown.length) {
l.warn(`RPC methods not decorated: ${unknown.join(', ')}`);
} // loop through all entries we have (populated in decorate) and filter as required
// only remove when we have results and method missing, or with no results if optional
for (let i = 0; i < allKnown.length; i++) {
const [k, {
method,
section
}] = allKnown[i];
if (hasResults && !exposed.includes(k) && k !== 'rpc_methods') {
if (this._rpc[section]) {
delete this._rpc[section][method];
delete this._rx.rpc[section][method];
}
}
}
}
_decorateRpc(rpc, decorateMethod, input = {}) {
const out = input;
const decorateFn = (section, method) => {
const source = rpc[section][method];
const fn = decorateMethod(source, {
methodName: method
});
fn.meta = source.meta;
fn.raw = decorateMethod(source.raw, {
methodName: method
});
return fn;
};
for (let s = 0; s < rpc.sections.length; s++) {
const section = rpc.sections[s];
if (!Object.prototype.hasOwnProperty.call(out, section)) {
const methods = Object.keys(rpc[section]);
const decorateInternal = method => decorateFn(section, method);
for (let m = 0; m < methods.length; m++) {
const method = methods[m]; // skip subscriptions where we have a non-subscribe interface
if (this.hasSubscriptions || !(method.startsWith('subscribe') || method.startsWith('unsubscribe'))) {
if (!Object.prototype.hasOwnProperty.call(out, section)) {
out[section] = {};
}
lazyMethod(out[section], method, decorateInternal);
}
}
}
}
return out;
} // only be called if supportMulti is true
_decorateMulti(decorateMethod) {
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
return decorateMethod(calls => (this.hasSubscriptions ? this._rpcCore.state.subscribeStorage : this._rpcCore.state.queryStorageAt)(calls.map(args => Array.isArray(args) ? args[0].creator.meta.type.isPlain ? [args[0].creator] : args[0].creator.meta.type.asMap.hashers.length === 1 ? [args[0].creator, args.slice(1)] : [args[0].creator, ...args.slice(1)] : [args.creator])));
}
_decorateMultiAt(atApi, decorateMethod, blockHash) {
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
return decorateMethod(calls => this._rpcCore.state.queryStorageAt(calls.map(args => {
if (Array.isArray(args)) {
const {
creator
} = getAtQueryFn(atApi, args[0].creator);
return creator.meta.type.isPlain ? [creator] : creator.meta.type.asMap.hashers.length === 1 ? [creator, args.slice(1)] : [creator, ...args.slice(1)];
}
return [getAtQueryFn(atApi, args.creator).creator];
}), blockHash));
}
_decorateExtrinsics({
tx
}, decorateMethod) {
const result = createSubmittable(this._type, this._rx, decorateMethod);
const lazySection = section => lazyMethods({}, Object.keys(tx[section]), method => this._decorateExtrinsicEntry(tx[section][method], result));
const sections = Object.keys(tx);
for (let i = 0; i < sections.length; i++) {
lazyMethod(result, sections[i], lazySection);
}
return result;
}
_decorateExtrinsicEntry(method, creator) {
const decorated = (...params) => creator(method(...params)); // pass through the `.is`
decorated.is = other => method.is(other); // eslint-disable-next-line @typescript-eslint/no-unsafe-return
return this._decorateFunctionMeta(method, decorated);
}
_decorateStorage({
query,
registry
}, decorateMethod, blockHash) {
const result = {};
const lazySection = section => lazyMethods({}, Object.keys(query[section]), method => blockHash ? this._decorateStorageEntryAt(registry, query[section][method], decorateMethod, blockHash) : this._decorateStorageEntry(query[section][method], decorateMethod));
const sections = Object.keys(query);
for (let i = 0; i < sections.length; i++) {
lazyMethod(result, sections[i], lazySection);
}
return result;
}
_decorateStorageEntry(creator, decorateMethod) {
const getArgs = (args, registry) => extractStorageArgs(registry || this.#registry, creator, args);
const getQueryAt = blockHash => from(this.at(blockHash)).pipe(map(api => getAtQueryFn(api, creator))); // Disable this where it occurs for each field we are decorating
/* eslint-disable @typescript-eslint/no-unsafe-member-access,@typescript-eslint/no-unsafe-assignment */
const decorated = this._decorateStorageCall(creator, decorateMethod);
decorated.creator = creator;
decorated.at = decorateMethod((blockHash, ...args) => getQueryAt(blockHash).pipe(switchMap(q => q(...args))));
decorated.hash = decorateMethod((...args) => this._rpcCore.state.getStorageHash(getArgs(args)));
decorated.is = key => key.section === creator.section && key.method === creator.method;
decorated.key = (...args) => u8aToHex(compactStripLength(creator(...args))[1]);
decorated.keyPrefix = (...args) => u8aToHex(creator.keyPrefix(...args));
decorated.range = decorateMethod((range, ...args) => this._decorateStorageRange(decorated, args, range));
decorated.size = decorateMethod((...args) => this._rpcCore.state.getStorageSize(getArgs(args)));
decorated.sizeAt = decorateMethod((blockHash, ...args) => getQueryAt(blockHash).pipe(switchMap(q => this._rpcCore.state.getStorageSize(getArgs(args, q.creator.meta.registry), blockHash)))); // .keys() & .entries() only available on map types
if (creator.iterKey && creator.meta.type.isMap) {
decorated.entries = decorateMethod(memo(this.#instanceId, (...args) => this._retrieveMapEntries(creator, null, args)));
decorated.entriesAt = decorateMethod(memo(this.#instanceId, (blockHash, ...args) => getQueryAt(blockHash).pipe(switchMap(q => this._retrieveMapEntries(q.creator, blockHash, args)))));
decorated.entriesPaged = decorateMethod(memo(this.#instanceId, opts => this._retrieveMapEntriesPaged(creator, undefined, opts)));
decorated.keys = decorateMethod(memo(this.#instanceId, (...args) => this._retrieveMapKeys(creator, null, args)));
decorated.keysAt = decorateMethod(memo(this.#instanceId, (blockHash, ...args) => getQueryAt(blockHash).pipe(switchMap(q => this._retrieveMapKeys(q.creator, blockHash, args)))));
decorated.keysPaged = decorateMethod(memo(this.#instanceId, opts => this._retrieveMapKeysPaged(creator, undefined, opts)));
}
if (this.supportMulti && creator.meta.type.isMap) {
// When using double map storage function, user need to pass double map key as an array
decorated.multi = decorateMethod(args => creator.meta.type.asMap.hashers.length === 1 ? this._retrieveMulti(args.map(a => [creator, [a]])) : this._retrieveMulti(args.map(a => [creator, a])));
}
/* eslint-enable @typescript-eslint/no-unsafe-member-access,@typescript-eslint/no-unsafe-assignment */
return this._decorateFunctionMeta(creator, decorated);
}
_decorateStorageEntryAt(registry, creator, decorateMethod, blockHash) {
const getArgs = args => extractStorageArgs(registry, creator, args); // Disable this where it occurs for each field we are decorating
/* eslint-disable @typescript-eslint/no-unsafe-member-access,@typescript-eslint/no-unsafe-assignment */
const decorated = decorateMethod((...args) => this._rpcCore.state.getStorage(getArgs(args), blockHash));
decorated.creator = creator;
decorated.hash = decorateMethod((...args) => this._rpcCore.state.getStorageHash(getArgs(args), blockHash));
decorated.is = key => key.section === creator.section && key.method === creator.method;
decorated.key = (...args) => u8aToHex(compactStripLength(creator(creator.meta.type.isPlain ? undefined : args))[1]);
decorated.keyPrefix = (...keys) => u8aToHex(creator.keyPrefix(...keys));
decorated.size = decorateMethod((...args) => this._rpcCore.state.getStorageSize(getArgs(args), blockHash)); // .keys() & .entries() only available on map types
if (creator.iterKey && creator.meta.type.isMap) {
decorated.entries = decorateMethod(memo(this.#instanceId, (...args) => this._retrieveMapEntries(creator, blockHash, args)));
decorated.entriesPaged = decorateMethod(memo(this.#instanceId, opts => this._retrieveMapEntriesPaged(creator, blockHash, opts)));
decorated.keys = decorateMethod(memo(this.#instanceId, (...args) => this._retrieveMapKeys(creator, blockHash, args)));
decorated.keysPaged = decorateMethod(memo(this.#instanceId, opts => this._retrieveMapKeysPaged(creator, blockHash, opts)));
}
if (this.supportMulti && creator.meta.type.isMap) {
// When using double map storage function, user need to pass double map key as an array
decorated.multi = decorateMethod(args => creator.meta.type.asMap.hashers.length === 1 ? this._retrieveMulti(args.map(a => [creator, [a]]), blockHash) : this._retrieveMulti(args.map(a => [creator, a]), blockHash));
}
/* eslint-enable @typescript-eslint/no-unsafe-member-access,@typescript-eslint/no-unsafe-assignment */
return this._decorateFunctionMeta(creator, decorated);
} // Decorate the base storage call. In the case or rxjs or promise-without-callback (await)
// we make a subscription, alternatively we push this through a single-shot query
_decorateStorageCall(creator, decorateMethod) {
return decorateMethod((...args) => {
return this.hasSubscriptions ? this._rpcCore.state.subscribeStorage([extractStorageArgs(this.#registry, creator, args)]).pipe(map(([data]) => data) // extract first/only result from list
) : this._rpcCore.state.getStorage(extractStorageArgs(this.#registry, creator, args));
}, {
methodName: creator.method,
overrideNoSub: (...args) => this._rpcCore.state.getStorage(extractStorageArgs(this.#registry, creator, args))
});
}
_decorateStorageRange(decorated, args, range) {
const outputType = unwrapStorageType(this.#registry, decorated.creator.meta.type, decorated.creator.meta.modifier.isOptional);
return this._rpcCore.state.queryStorage([decorated.key(...args)], ...range).pipe(map(result => result.map(([blockHash, [value]]) => [blockHash, this.createType(outputType, value.isSome ? value.unwrap().toHex() : undefined)])));
} // retrieve a set of values for a specific set of keys - here we chunk the keys into PAGE_SIZE sizes
_retrieveMulti(keys, blockHash) {
if (!keys.length) {
return of([]);
}
const queryCall = this.hasSubscriptions && !blockHash ? this._rpcCore.state.subscribeStorage : this._rpcCore.state.queryStorageAt;
return combineLatest(arrayChunk(keys, PAGE_SIZE_V).map(k => blockHash ? queryCall(k, blockHash) : queryCall(k))).pipe(map(arrayFlatten));
}
_retrieveMapKeys({
iterKey,
meta,
method,
section
}, at, args) {
assert(iterKey && meta.type.isMap, 'keys can only be retrieved on maps');
const headKey = iterKey(...args).toHex();
const startSubject = new BehaviorSubject(headKey);
const getKeysPaged = at ? startKey => this._rpcCore.state.getKeysPaged(headKey, PAGE_SIZE_K, startKey, at) : startKey => this._rpcCore.state.getKeysPaged(headKey, PAGE_SIZE_K, startKey);
const setMeta = key => key.setMeta(meta, section, method);
return startSubject.pipe(switchMap(getKeysPaged), map(keys => keys.map(setMeta)), tap(keys => {
setTimeout(() => {
keys.length === PAGE_SIZE_K ? startSubject.next(keys[PAGE_SIZE_K - 1].toHex()) : startSubject.complete();
}, 0);
}), toArray(), // toArray since we want to startSubject to be completed
map(arrayFlatten));
}
_retrieveMapKeysPaged({
iterKey,
meta,
method,
section
}, at, opts) {
assert(iterKey && meta.type.isMap, 'keys can only be retrieved on maps');
const setMeta = key => key.setMeta(meta, section, method);
const getKeysPaged = at ? headKey => this._rpcCore.state.getKeysPaged(headKey, opts.pageSize, opts.startKey || headKey, at) : headKey => this._rpcCore.state.getKeysPaged(headKey, opts.pageSize, opts.startKey || headKey);
return getKeysPaged(iterKey(...opts.args).toHex()).pipe(map(keys => keys.map(setMeta)));
}
_retrieveMapEntries(entry, at, args) {
const queryStorageAt = at ? keys => this._rpcCore.state.queryStorageAt(keys, at) : keys => this._rpcCore.state.queryStorageAt(keys);
return this._retrieveMapKeys(entry, at, args).pipe(switchMap(keys => keys.length ? combineLatest(arrayChunk(keys, PAGE_SIZE_V).map(queryStorageAt)).pipe(map(valsArr => arrayFlatten(valsArr).map((value, index) => [keys[index], value]))) : of([])));
}
_retrieveMapEntriesPaged(entry, at, opts) {
const queryStorageAt = at ? keys => this._rpcCore.state.queryStorageAt(keys, at) : keys => this._rpcCore.state.queryStorageAt(keys);
return this._retrieveMapKeysPaged(entry, at, opts).pipe(switchMap(keys => keys.length ? queryStorageAt(keys).pipe(map(valsArr => valsArr.map((value, index) => [keys[index], value]))) : of([])));
}
_decorateDeriveRx(decorateMethod) {
var _this$_runtimeVersion, _this$_options$typesB, _this$_options$typesB2, _this$_options$typesB3;
const specName = (_this$_runtimeVersion = this._runtimeVersion) === null || _this$_runtimeVersion === void 0 ? void 0 : _this$_runtimeVersion.specName.toString(); // Pull in derive from api-derive
const available = getAvailableDerives(this.#instanceId, this._rx, objectSpread({}, this._options.derives, (_this$_options$typesB = this._options.typesBundle) === null || _this$_options$typesB === void 0 ? void 0 : (_this$_options$typesB2 = _this$_options$typesB.spec) === null || _this$_options$typesB2 === void 0 ? void 0 : (_this$_options$typesB3 = _this$_options$typesB2[specName || '']) === null || _this$_options$typesB3 === void 0 ? void 0 : _this$_options$typesB3.derives));
return decorateDeriveSections(decorateMethod, available);
}
_decorateDerive(decorateMethod) {
return decorateDeriveSections(decorateMethod, this._rx.derive);
}
/**
* Put the `this.onCall` function of ApiRx here, because it is needed by
* `api._rx`.
*/
_rxDecorateMethod = method => {
return method;
};
}