Rebrand: polkadot → pezkuwi, substrate → bizinikiwi, kusama → dicle

This commit is contained in:
2026-01-07 02:29:40 +03:00
commit d5f038faea
1383 changed files with 1088018 additions and 0 deletions
+4
View File
@@ -0,0 +1,4 @@
// Copyright 2017-2025 @pezkuwi/api authors & contributors
// SPDX-License-Identifier: Apache-2.0
import '@pezkuwi/api-augment';
File diff suppressed because it is too large Load Diff
+91
View File
@@ -0,0 +1,91 @@
// Copyright 2017-2025 @pezkuwi/api authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { ApiInterfaceEvents } from '../types/index.js';
import { EventEmitter } from 'eventemitter3';
export class Events {
#eventemitter = new EventEmitter();
protected emit (type: ApiInterfaceEvents, ...args: unknown[]): boolean {
return this.#eventemitter.emit(type, ...args);
}
/**
* @description Attach an eventemitter handler to listen to a specific event
*
* @param type The type of event to listen to. Available events are `connected`, `disconnected`, `ready` and `error`
* @param handler The callback to be called when the event fires. Depending on the event type, it could fire with additional arguments.
*
* @example
* <BR>
*
* ```javascript
* api.on('connected', (): void => {
* console.log('API has been connected to the endpoint');
* });
*
* api.on('disconnected', (): void => {
* console.log('API has been disconnected from the endpoint');
* });
* ```
*/
public on (type: ApiInterfaceEvents, handler: (...args: any[]) => any): this {
this.#eventemitter.on(type, handler);
return this;
}
/**
* @description Remove the given eventemitter handler
*
* @param type The type of event the callback was attached to. Available events are `connected`, `disconnected`, `ready` and `error`
* @param handler The callback to unregister.
*
* @example
* <BR>
*
* ```javascript
* const handler = (): void => {
* console.log('Connected !);
* };
*
* // Start listening
* api.on('connected', handler);
*
* // Stop listening
* api.off('connected', handler);
* ```
*/
public off (type: ApiInterfaceEvents, handler: (...args: any[]) => any): this {
this.#eventemitter.removeListener(type, handler);
return this;
}
/**
* @description Attach an one-time eventemitter handler to listen to a specific event
*
* @param type The type of event to listen to. Available events are `connected`, `disconnected`, `ready` and `error`
* @param handler The callback to be called when the event fires. Depending on the event type, it could fire with additional arguments.
*
* @example
* <BR>
*
* ```javascript
* api.once('connected', (): void => {
* console.log('API has been connected to the endpoint');
* });
*
* api.once('disconnected', (): void => {
* console.log('API has been disconnected from the endpoint');
* });
* ```
*/
public once (type: ApiInterfaceEvents, handler: (...args: any[]) => any): this {
this.#eventemitter.once(type, handler);
return this;
}
}
+245
View File
@@ -0,0 +1,245 @@
// Copyright 2017-2025 @pezkuwi/api authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { RpcCoreStats, RpcInterface } from '@pezkuwi/rpc-core/types';
import type { Text } from '@pezkuwi/types';
import type { Hash, RuntimeVersion } from '@pezkuwi/types/interfaces';
import type { Metadata } from '@pezkuwi/types/metadata';
import type { CallFunction, RegistryError } from '@pezkuwi/types/types';
import type { ApiDecoration, ApiInterfaceRx, ApiTypes, DecoratedErrors, DecoratedEvents, DecoratedRpc, QueryableCalls, QueryableConsts, QueryableStorage, QueryableStorageMulti, SubmittableExtrinsics } from '../types/index.js';
import { packageInfo } from '../packageInfo.js';
import { findCall, findError } from './find.js';
import { Init } from './Init.js';
interface PkgJson {
name: string;
version: string;
}
function assertResult<T> (value: T | undefined): T {
if (value === undefined) {
throw new Error("Api interfaces needs to be initialized before using, wait for 'isReady'");
}
return value;
}
export abstract class Getters<ApiType extends ApiTypes> extends Init<ApiType> implements ApiDecoration<ApiType> {
/**
* @description Runtime call interfaces (currently untyped, only decorated via API options)
*/
public get call (): QueryableCalls<ApiType> {
return assertResult(this._call);
}
/**
* @description Contains the parameter types (constants) of all modules.
*
* The values are instances of the appropriate type and are accessible using `section`.`constantName`,
*
* @example
* <BR>
*
* ```javascript
* console.log(api.consts.democracy.enactmentPeriod.toString())
* ```
*/
public get consts (): QueryableConsts<ApiType> {
return assertResult(this._consts);
}
/**
* @description Derived results that are injected into the API, allowing for combinations of various query results.
*
* @example
* <BR>
*
* ```javascript
* api.derive.chain.bestNumber((number) => {
* console.log('best number', number);
* });
* ```
*/
public get derive (): ReturnType<Getters<ApiType>['_decorateDerive']> {
return assertResult(this._derive);
}
/**
* @description Errors from metadata
*/
public get errors (): DecoratedErrors<ApiType> {
return assertResult(this._errors);
}
/**
* @description Events from metadata
*/
public get events (): DecoratedEvents<ApiType> {
return assertResult(this._events);
}
/**
* @description Returns the version of extrinsics in-use on this chain
*/
public get extrinsicVersion (): number {
return this._extrinsicType;
}
/**
* @description Contains the genesis Hash of the attached chain. Apart from being useful to determine the actual chain, it can also be used to sign immortal transactions.
*/
public get genesisHash (): Hash {
return assertResult(this._genesisHash);
}
/**
* @description true is the underlying provider is connected
*/
public get isConnected (): boolean {
return this._isConnected.getValue();
}
/**
* @description The library information name & version (from package.json)
*/
public get libraryInfo (): string {
return `${(packageInfo as PkgJson).name} v${(packageInfo as PkgJson).version}`;
}
/**
* @description Contains all the chain state modules and their subsequent methods in the API. These are attached dynamically from the runtime metadata.
*
* All calls inside the namespace, is denoted by `section`.`method` and may take an optional query parameter. As an example, `api.query.timestamp.now()` (current block timestamp) does not take parameters, while `api.query.system.account(<accountId>)` (retrieving the associated nonce & balances for an account), takes the `AccountId` as a parameter.
*
* @example
* <BR>
*
* ```javascript
* api.query.system.account(<accountId>, ([nonce, balance]) => {
* console.log('new free balance', balance.free, 'new nonce', nonce);
* });
* ```
*/
public get query (): QueryableStorage<ApiType> {
return assertResult(this._query);
}
/**
* @description Allows for the querying of multiple storage entries and the combination thereof into a single result. This is a very optimal way to make multiple queries since it only makes a single connection to the node and retrieves the data over one subscription.
*
* @example
* <BR>
*
* ```javascript
* const unsub = await api.queryMulti(
* [
* // you can include the storage without any parameters
* api.query.balances.totalIssuance,
* // or you can pass parameters to the storage query
* [api.query.system.account, '5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY']
* ],
* ([existential, [, { free }]]) => {
* console.log(`You have ${free.sub(existential)} more than the existential deposit`);
*
* unsub();
* }
* );
* ```
*/
public get queryMulti (): QueryableStorageMulti<ApiType> {
return assertResult(this._queryMulti);
}
/**
* @description Contains all the raw rpc sections and their subsequent methods in the API as defined by the jsonrpc interface definitions. Unlike the dynamic `api.query` and `api.tx` sections, these methods are fixed (although extensible with node upgrades) and not determined by the runtime.
*
* RPC endpoints available here allow for the query of chain, node and system information, in addition to providing interfaces for the raw queries of state (using known keys) and the submission of transactions.
*
* @example
* <BR>
*
* ```javascript
* api.rpc.chain.subscribeNewHeads((header) => {
* console.log('new header', header);
* });
* ```
*/
public get rpc (): DecoratedRpc<ApiType, RpcInterface> {
return assertResult(this._rpc);
}
/**
* @description Contains the chain information for the current node.
*/
public get runtimeChain (): Text {
return assertResult(this._runtimeChain);
}
/**
* @description Yields the current attached runtime metadata. Generally this is only used to construct extrinsics & storage, but is useful for current runtime inspection.
*/
public get runtimeMetadata (): Metadata {
return assertResult(this._runtimeMetadata);
}
/**
* @description Contains the version information for the current runtime.
*/
public get runtimeVersion (): RuntimeVersion {
return assertResult(this._runtimeVersion);
}
/**
* @description The underlying Rx API interface
*/
public get rx (): Pick<ApiInterfaceRx, 'tx' | 'rpc' | 'query' | 'call'> {
return assertResult(this._rx as Pick<ApiInterfaceRx, 'tx' | 'rpc' | 'query' | 'call'>);
}
/**
* @description Returns the underlying provider stats
*/
public get stats (): RpcCoreStats | undefined {
return this._rpcCore.stats;
}
/**
* @description The type of this API instance, either 'rxjs' or 'promise'
*/
public get type (): ApiTypes {
return this._type;
}
/**
* @description Contains all the extrinsic modules and their subsequent methods in the API. It allows for the construction of transactions and the submission thereof. These are attached dynamically from the runtime metadata.
*
* @example
* <BR>
*
* ```javascript
* api.tx.balances
* .transferAllowDeath(<recipientId>, <balance>)
* .signAndSend(<keyPair>, ({status}) => {
* console.log('tx status', status.asFinalized.toHex());
* });
* ```
*/
public get tx (): SubmittableExtrinsics<ApiType> {
return assertResult(this._extrinsics);
}
/**
* @description Finds the definition for a specific [[CallFunction]] based on the index supplied
*/
public findCall (callIndex: Uint8Array | string): CallFunction {
return findCall(this.registry, callIndex);
}
/**
* @description Finds the definition for a specific [[RegistryError]] based on the index supplied
*/
public findError (errorIndex: Uint8Array | string): RegistryError {
return findError(this.registry, errorIndex);
}
}
+525
View File
@@ -0,0 +1,525 @@
// Copyright 2017-2025 @pezkuwi/api authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { Observable, Subscription } from 'rxjs';
import type { Bytes, Text, u32, Vec } from '@pezkuwi/types';
import type { ExtDef } from '@pezkuwi/types/extrinsic/signedExtensions/types';
import type { BlockHash, ChainProperties, Hash, HeaderPartial, RuntimeVersion, RuntimeVersionApi, RuntimeVersionPartial } from '@pezkuwi/types/interfaces';
import type { Registry } from '@pezkuwi/types/types';
import type { BN } from '@pezkuwi/util';
import type { HexString } from '@pezkuwi/util/types';
import type { ApiBase, ApiDecoration, ApiOptions, ApiTypes, DecorateMethod } from '../types/index.js';
import type { VersionedRegistry } from './types.js';
import { firstValueFrom, map, of, switchMap } from 'rxjs';
import { Metadata, TypeRegistry } from '@pezkuwi/types';
import { LATEST_EXTRINSIC_VERSION } from '@pezkuwi/types/extrinsic/constants';
import { getSpecAlias, getSpecExtensions, getSpecHasher, getSpecRpc, getSpecTypes, getUpgradeVersion } from '@pezkuwi/types-known';
import { assertReturn, BN_ZERO, isUndefined, logger, noop, objectSpread, u8aEq, u8aToHex, u8aToU8a } from '@pezkuwi/util';
import { blake2AsHex, cryptoWaitReady } from '@pezkuwi/util-crypto';
import { Decorate } from './Decorate.js';
const KEEPALIVE_INTERVAL = 10000;
const WITH_VERSION_SHORTCUT = false;
const SUPPORTED_METADATA_VERSIONS = [16, 15, 14];
const l = logger('api/init');
function textToString (t: Text): string {
return t.toString();
}
export abstract class Init<ApiType extends ApiTypes> extends Decorate<ApiType> {
#atLast: [string, ApiDecoration<ApiType>] | null = null;
#healthTimer: ReturnType<typeof setInterval> | null = null;
#registries: VersionedRegistry<ApiType>[] = [];
#updateSub?: Subscription | null = null;
#waitingRegistries: Record<HexString, Promise<VersionedRegistry<ApiType>>> = {};
constructor (options: ApiOptions, type: ApiTypes, decorateMethod: DecorateMethod<ApiType>) {
super(options, type, decorateMethod);
// all injected types added to the registry for overrides
this.registry.setKnownTypes(options);
// We only register the types (global) if this is not a cloned instance.
// Do right up-front, so we get in the user types before we are actually
// doing anything on-chain, this ensures we have the overrides in-place
if (!options.source) {
this.registerTypes(options.types);
} else {
this.#registries = options.source.#registries as VersionedRegistry<ApiType>[];
}
this._rpc = this._decorateRpc(this._rpcCore, this._decorateMethod);
this._rx.rpc = this._decorateRpc(this._rpcCore, this._rxDecorateMethod);
if (this.supportMulti) {
this._queryMulti = this._decorateMulti(this._decorateMethod);
this._rx.queryMulti = this._decorateMulti(this._rxDecorateMethod);
}
this._rx.signer = options.signer;
this._rpcCore.setRegistrySwap((blockHash: Uint8Array) => this.getBlockRegistry(blockHash));
this._rpcCore.setResolveBlockHash((blockNumber) => firstValueFrom(this._rpcCore.chain.getBlockHash(blockNumber)));
if (this.hasSubscriptions) {
this._rpcCore.provider.on('disconnected', () => this.#onProviderDisconnect());
this._rpcCore.provider.on('error', (e: Error) => this.#onProviderError(e));
this._rpcCore.provider.on('connected', () => this.#onProviderConnect());
} else if (!this._options.noInitWarn) {
l.warn('Api will be available in a limited mode since the provider does not support subscriptions');
}
// If the provider was instantiated earlier, and has already emitted a
// 'connected' event, then the `on('connected')` won't fire anymore. To
// cater for this case, we call manually `this._onProviderConnect`.
if (this._rpcCore.provider.isConnected) {
this.#onProviderConnect().catch(noop);
}
}
/**
* @description Decorates a registry based on the runtime version
*/
private _initRegistry (registry: Registry, chain: Text, version: { specName: Text, specVersion: BN }, metadata: Metadata, chainProps?: ChainProperties): void {
registry.clearCache();
registry.setChainProperties(chainProps || this.registry.getChainProperties());
registry.setKnownTypes(this._options);
registry.register(getSpecTypes(registry, chain, version.specName, version.specVersion));
registry.setHasher(getSpecHasher(registry, chain, version.specName));
// for bundled types, pull through the aliases defined
if (registry.knownTypes.typesBundle) {
registry.knownTypes.typesAlias = getSpecAlias(registry, chain, version.specName);
}
registry.setMetadata(metadata, undefined, objectSpread<ExtDef>({}, getSpecExtensions(registry, chain, version.specName), this._options.signedExtensions), this._options.noInitWarn);
}
/**
* @description Returns the default versioned registry
*/
private _getDefaultRegistry (): VersionedRegistry<ApiType> {
return assertReturn(this.#registries.find(({ isDefault }) => isDefault), 'Initialization error, cannot find the default registry');
}
/**
* @description Returns a decorated API instance at a specific point in time
*/
public async at (blockHash: Uint8Array | string, knownVersion?: RuntimeVersion): Promise<ApiDecoration<ApiType>> {
const u8aHash = u8aToU8a(blockHash);
const u8aHex = u8aToHex(u8aHash);
const registry = await this.getBlockRegistry(u8aHash, knownVersion);
if (!this.#atLast || this.#atLast[0] !== u8aHex) {
// always create a new decoration - since we are pointing to a specific hash, this
// means that all queries needs to use that hash (not a previous one already existing)
this.#atLast = [u8aHex, this._createDecorated(registry, true, null, u8aHash).decoratedApi];
}
return this.#atLast[1];
}
private async _createBlockRegistry (blockHash: Uint8Array, header: HeaderPartial, version: RuntimeVersionPartial): Promise<VersionedRegistry<ApiType>> {
const registry = new TypeRegistry(blockHash);
const metadata = await this._retrieveMetadata(version.apis, header.parentHash, registry);
const runtimeChain = this._runtimeChain;
if (!runtimeChain) {
throw new Error('Invalid initializion order, runtimeChain is not available');
}
this._initRegistry(registry, runtimeChain, version, metadata);
// add our new registry
const result = { counter: 0, lastBlockHash: blockHash, metadata, registry, runtimeVersion: version };
this.#registries.push(result);
return result;
}
private _cacheBlockRegistryProgress (key: HexString, creator: () => Promise<VersionedRegistry<ApiType>>): Promise<VersionedRegistry<ApiType>> {
// look for waiting resolves
let waiting = this.#waitingRegistries[key];
if (isUndefined(waiting)) {
// nothing waiting, construct new
waiting = this.#waitingRegistries[key] = new Promise<VersionedRegistry<ApiType>>((resolve, reject): void => {
creator()
.then((registry): void => {
delete this.#waitingRegistries[key];
resolve(registry);
})
.catch((error): void => {
delete this.#waitingRegistries[key];
reject(error);
});
});
}
return waiting;
}
private _getBlockRegistryViaVersion (blockHash: Uint8Array, version?: RuntimeVersionPartial): VersionedRegistry<ApiType> | null {
if (version) {
// check for pre-existing registries. We also check specName, e.g. it
// could be changed like in Westmint with upgrade from shell -> westmint
const existingViaVersion = this.#registries.find(({ runtimeVersion: { specName, specVersion } }) =>
specName.eq(version.specName) &&
specVersion.eq(version.specVersion)
);
if (existingViaVersion) {
existingViaVersion.counter++;
existingViaVersion.lastBlockHash = blockHash;
return existingViaVersion;
}
}
return null;
}
private async _getBlockRegistryViaHash (blockHash: Uint8Array): Promise<VersionedRegistry<ApiType>> {
// ensure we have everything required
if (!this._genesisHash || !this._runtimeVersion) {
throw new Error('Cannot retrieve data on an uninitialized chain');
}
// We have to assume that on the RPC layer the calls used here does not call back into
// the registry swap, so getHeader & getRuntimeVersion should not be historic
const header = this.registry.createType('HeaderPartial',
this._genesisHash.eq(blockHash)
? { number: BN_ZERO, parentHash: this._genesisHash }
: await firstValueFrom(this._rpcCore.chain.getHeader.raw(blockHash))
);
if (header.parentHash.isEmpty) {
l.warn(`Unable to retrieve header ${blockHash.toString()} and parent ${header.parentHash.toString()} from supplied hash`);
throw new Error('Unable to retrieve header and parent from supplied hash');
}
// get the runtime version, either on-chain or via an known upgrade history
const [firstVersion, lastVersion] = getUpgradeVersion(this._genesisHash, header.number);
const version = this.registry.createType('RuntimeVersionPartial',
WITH_VERSION_SHORTCUT && (
firstVersion && (
lastVersion ||
firstVersion.specVersion.eq(this._runtimeVersion.specVersion)
)
)
? { apis: firstVersion.apis, specName: this._runtimeVersion.specName, specVersion: firstVersion.specVersion }
: await firstValueFrom(this._rpcCore.state.getRuntimeVersion.raw(header.parentHash))
);
return (
// try to find via version
this._getBlockRegistryViaVersion(blockHash, version) ||
// return new or in-flight result
await this._cacheBlockRegistryProgress(version.toHex(), () => this._createBlockRegistry(blockHash, header, version))
);
}
/**
* @description Sets up a registry based on the block hash defined
*/
public async getBlockRegistry (blockHash: Uint8Array, knownVersion?: RuntimeVersion): Promise<VersionedRegistry<ApiType>> {
return (
// try to find via blockHash
this.#registries.find(({ lastBlockHash }) =>
lastBlockHash && u8aEq(lastBlockHash, blockHash)
) ||
// try to find via version
this._getBlockRegistryViaVersion(blockHash, knownVersion) ||
// return new or in-flight result
await this._cacheBlockRegistryProgress(u8aToHex(blockHash), () => this._getBlockRegistryViaHash(blockHash))
);
}
protected async _loadMeta (): Promise<boolean> {
// on re-connection to the same chain, we don't want to re-do everything from chain again
if (this._isReady) {
// on re-connection only re-subscribe to chain updates if we are not a clone
if (!this._options.source) {
this._subscribeUpdates();
}
return true;
}
this._unsubscribeUpdates();
// only load from on-chain if we are not a clone (default path), alternatively
// just use the values from the source instance provided
[this._genesisHash, this._runtimeMetadata] = this._options.source?._isReady
? await this._metaFromSource(this._options.source)
: await this._metaFromChain(this._options.metadata);
return this._initFromMeta(this._runtimeMetadata);
}
// eslint-disable-next-line @typescript-eslint/require-await
private async _metaFromSource (source: ApiBase<any>): Promise<[Hash, Metadata]> {
this._extrinsicType = source.extrinsicVersion;
this._runtimeChain = source.runtimeChain;
this._runtimeVersion = source.runtimeVersion;
// manually build a list of all available methods in this RPC, we are
// going to filter on it to align the cloned RPC without making a call
const sections = Object.keys(source.rpc);
const rpcs: string[] = [];
for (let s = 0, scount = sections.length; s < scount; s++) {
const section = sections[s];
const methods = Object.keys((source.rpc as unknown as Record<string, Record<string, unknown>>)[section]);
for (let m = 0, mcount = methods.length; m < mcount; m++) {
rpcs.push(`${section}_${methods[m]}`);
}
}
this._filterRpc(rpcs, getSpecRpc(this.registry, source.runtimeChain, source.runtimeVersion.specName));
return [source.genesisHash, source.runtimeMetadata];
}
// subscribe to metadata updates, inject the types on changes
private _subscribeUpdates (): void {
if (this.#updateSub || !this.hasSubscriptions) {
return;
}
this.#updateSub = this._rpcCore.state.subscribeRuntimeVersion().pipe(
switchMap((version: RuntimeVersion): Observable<boolean> =>
// only retrieve the metadata when the on-chain version has been changed
this._runtimeVersion?.specVersion.eq(version.specVersion)
? of(false)
: this._rpcCore.state.getMetadata().pipe(
map((metadata: Metadata): boolean => {
l.log(`Runtime version updated to spec=${version.specVersion.toString()}, tx=${version.transactionVersion.toString()}`);
this._runtimeMetadata = metadata;
this._runtimeVersion = version;
this._rx.runtimeVersion = version;
// update the default registry version
const thisRegistry = this._getDefaultRegistry();
const runtimeChain = this._runtimeChain;
if (!runtimeChain) {
throw new Error('Invalid initializion order, runtimeChain is not available');
}
// setup the data as per the current versions
thisRegistry.metadata = metadata;
thisRegistry.runtimeVersion = version;
this._initRegistry(this.registry, runtimeChain, version, metadata);
this._injectMetadata(thisRegistry, true);
return true;
})
)
)
).subscribe();
}
private async _metaFromChain (optMetadata?: Record<string, HexString>): Promise<[Hash, Metadata]> {
const [genesisHash, runtimeVersion, chain, chainProps, rpcMethods] = await Promise.all([
firstValueFrom(this._rpcCore.chain.getBlockHash(0)),
firstValueFrom(this._rpcCore.state.getRuntimeVersion()),
firstValueFrom(this._rpcCore.system.chain()),
firstValueFrom(this._rpcCore.system.properties()),
firstValueFrom(this._rpcCore.rpc.methods())
]);
// set our chain version & genesisHash as returned
this._runtimeChain = chain;
this._runtimeVersion = runtimeVersion;
this._rx.runtimeVersion = runtimeVersion;
// retrieve metadata, either from chain or as pass-in via options
const metadataKey = `${genesisHash.toHex() || '0x'}-${runtimeVersion.specVersion.toString()}`;
const metadata = optMetadata?.[metadataKey]
? new Metadata(this.registry, optMetadata[metadataKey])
: await this._retrieveMetadata(runtimeVersion.apis);
// initializes the registry & RPC
this._initRegistry(this.registry, chain, runtimeVersion, metadata, chainProps);
this._filterRpc(rpcMethods.methods.map(textToString), getSpecRpc(this.registry, chain, runtimeVersion.specName));
this._subscribeUpdates();
// setup the initial registry, when we have none
if (!this.#registries.length) {
this.#registries.push({ counter: 0, isDefault: true, metadata, registry: this.registry, runtimeVersion });
}
// get unique types & validate
metadata.getUniqTypes(this._options.throwOnUnknown || false);
return [genesisHash, metadata];
}
private _initFromMeta (metadata: Metadata): boolean {
const runtimeVersion = this._runtimeVersion;
if (!runtimeVersion) {
throw new Error('Invalid initializion order, runtimeVersion is not available');
}
// ExtrinsicV5 is not fully supported yet, for that reason we default to version 4
this._extrinsicType = metadata.asLatest.extrinsic.versions.at(0) || LATEST_EXTRINSIC_VERSION;
this._rx.extrinsicType = this._extrinsicType;
this._rx.genesisHash = this._genesisHash;
this._rx.runtimeVersion = runtimeVersion;
// inject metadata and adjust the types as detected
this._injectMetadata(this._getDefaultRegistry(), true);
// derive is last, since it uses the decorated rx
this._rx.derive = this._decorateDeriveRx(this._rxDecorateMethod);
this._derive = this._decorateDerive(this._decorateMethod);
return true;
}
/**
* @internal
*
* Tries to use runtime api calls to retrieve metadata. This ensures the api initializes with the latest metadata.
* If the runtime call is not there it will use the rpc method.
*/
private async _retrieveMetadata (apis: Vec<RuntimeVersionApi>, at?: BlockHash | string | Uint8Array, registry?: TypeRegistry): Promise<Metadata> {
let metadataVersion: u32 | null = null;
const metadataApi = apis.find(([a]) => a.eq(blake2AsHex('Metadata', 64)));
const typeRegistry = registry || this.registry;
// This chain does not have support for the metadataApi, or does not have the required version.
if (!metadataApi || metadataApi[1].toNumber() < 2) {
l.warn('MetadataApi not available, rpc::state::get_metadata will be used.');
return at
? new Metadata(typeRegistry, await firstValueFrom(this._rpcCore.state.getMetadata.raw<HexString>(at)))
: await firstValueFrom(this._rpcCore.state.getMetadata());
}
try {
const metadataVersionsAsBytes = at
? await firstValueFrom(this._rpcCore.state.call.raw('Metadata_metadata_versions', '0x', at))
: await firstValueFrom(this._rpcCore.state.call('Metadata_metadata_versions', '0x'));
const versions = typeRegistry.createType('Vec<u32>', metadataVersionsAsBytes);
// For unstable versions of the metadata the last value is set to u32 MAX in the runtime. This ensures only supported stable versions are used.
metadataVersion = versions.filter((ver) => SUPPORTED_METADATA_VERSIONS.includes(ver.toNumber())).reduce((largest, current) => current.gt(largest) ? current : largest);
} catch (e) {
l.debug((e as Error).message);
l.warn('error with state_call::Metadata_metadata_versions, rpc::state::get_metadata will be used');
}
// When the metadata version does not align with the latest supported versions we ensure not to call the metadata runtime call.
// I noticed on some previous runtimes that have support for `Metadata_metadata_at_version` that very irregular versions were being returned.
// This was evident with runtime 1000000 - it return a very large number. This ensures we always stick within what is supported.
if (metadataVersion && !SUPPORTED_METADATA_VERSIONS.includes(metadataVersion.toNumber())) {
metadataVersion = null;
}
if (metadataVersion) {
try {
const metadataBytes = at
? await firstValueFrom(this._rpcCore.state.call.raw<Bytes>('Metadata_metadata_at_version', u8aToHex(metadataVersion.toU8a()), at))
: await firstValueFrom(this._rpcCore.state.call('Metadata_metadata_at_version', u8aToHex(metadataVersion.toU8a())));
// When the metadata is called with `at` it is required to use `.raw`. Therefore since the length prefix is not present the
// need to create a `Raw` type is necessary before creating the `OpaqueMetadata` type or else there will be a magic number
// mismatch
const rawMeta = at
? typeRegistry.createType('Raw', metadataBytes).toU8a()
: metadataBytes;
const opaqueMetadata = typeRegistry.createType('Option<OpaqueMetadata>', rawMeta).unwrapOr(null);
if (opaqueMetadata) {
return new Metadata(typeRegistry, opaqueMetadata.toHex());
}
} catch (e) {
l.debug((e as Error).message);
l.warn('error with state_call::Metadata_metadata_at_version, rpc::state::get_metadata will be used');
}
}
return at
? new Metadata(typeRegistry, await firstValueFrom(this._rpcCore.state.getMetadata.raw<HexString>(at)))
: await firstValueFrom(this._rpcCore.state.getMetadata());
}
private _subscribeHealth (): void {
this._unsubscribeHealth();
// Only enable the health keepalive on WS, not needed on HTTP
this.#healthTimer = this.hasSubscriptions
? setInterval((): void => {
firstValueFrom(this._rpcCore.system.health.raw()).catch(noop);
}, KEEPALIVE_INTERVAL)
: null;
}
private _unsubscribeHealth (): void {
if (this.#healthTimer) {
clearInterval(this.#healthTimer);
this.#healthTimer = null;
}
}
private _unsubscribeUpdates (): void {
if (this.#updateSub) {
this.#updateSub.unsubscribe();
this.#updateSub = null;
}
}
protected _unsubscribe (): void {
this._unsubscribeHealth();
this._unsubscribeUpdates();
}
async #onProviderConnect (): Promise<void> {
this._isConnected.next(true);
this.emit('connected');
try {
const cryptoReady = this._options.initWasm === false
? true
: await cryptoWaitReady();
const hasMeta = await this._loadMeta();
this._subscribeHealth();
if (hasMeta && !this._isReady && cryptoReady) {
this._isReady = true;
this.emit('ready', this);
}
} catch (_error) {
const error = new Error(`FATAL: Unable to initialize the API: ${(_error as Error).message}`);
l.error(error);
this.emit('error', error);
}
}
#onProviderDisconnect (): void {
this._isConnected.next(false);
this._unsubscribe();
this.emit('disconnected');
}
#onProviderError (error: Error): void {
this.emit('error', error);
}
}
+14
View File
@@ -0,0 +1,14 @@
// Copyright 2017-2025 @pezkuwi/api authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { CallFunction, Registry, RegistryError } from '@pezkuwi/types/types';
import { u8aToU8a } from '@pezkuwi/util';
export function findCall (registry: Registry, callIndex: Uint8Array | string): CallFunction {
return registry.findMetaCall(u8aToU8a(callIndex));
}
export function findError (registry: Registry, errorIndex: Uint8Array | string): RegistryError {
return registry.findMetaError(u8aToU8a(errorIndex));
}
+85
View File
@@ -0,0 +1,85 @@
// Copyright 2017-2025 @pezkuwi/api authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { SignerPayloadRawBase } from '@pezkuwi/types/types';
import type { ApiOptions, ApiTypes, DecorateMethod, Signer } from '../types/index.js';
import { isString, objectSpread, u8aToHex, u8aToU8a } from '@pezkuwi/util';
import { Getters } from './Getters.js';
interface KeyringSigner {
sign (message: Uint8Array): Uint8Array;
}
interface SignerRawOptions {
signer?: Signer;
}
export abstract class ApiBase<ApiType extends ApiTypes> extends Getters<ApiType> {
/**
* @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 '@pezkuwi/api/promise';
*
* const api = new Api().isReady();
*
* api.rpc.subscribeNewHeads((header) => {
* console.log(`new block #${header.number.toNumber()}`);
* });
* ```
*/
constructor (options: ApiOptions = {}, type: ApiTypes, decorateMethod: DecorateMethod<ApiType>) {
super(options, type, decorateMethod);
}
/**
* @description Connect from the underlying provider, halting all network traffic
*/
public connect (): Promise<void> {
return this._rpcCore.connect();
}
/**
* @description Disconnect from the underlying provider, halting all network traffic
*/
public disconnect (): Promise<void> {
this._unsubscribe();
return this._rpcCore.disconnect();
}
/**
* @description Set an external signer which will be used to sign extrinsic when account passed in is not KeyringPair
*/
public setSigner (signer: Signer | undefined): void {
this._rx.signer = signer;
}
/**
* @description Signs a raw signer payload, string or Uint8Array
*/
public async sign (address: KeyringSigner | string, data: SignerPayloadRawBase, { signer }: SignerRawOptions = {}): Promise<string> {
if (isString(address)) {
const _signer = signer || this._rx.signer;
if (!_signer?.signRaw) {
throw new Error('No signer exists with a signRaw interface. You possibly need to pass through an explicit keypair for the origin so it can be used for signing.');
}
return (
await _signer.signRaw(
objectSpread({ type: 'bytes' }, data, { address })
)
).signature;
}
return u8aToHex(address.sign(u8aToU8a(data.data)));
}
}
+19
View File
@@ -0,0 +1,19 @@
// Copyright 2017-2025 @pezkuwi/api authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { Metadata } from '@pezkuwi/types';
import type { RuntimeVersionPartial } from '@pezkuwi/types/interfaces';
import type { DecoratedMeta } from '@pezkuwi/types/metadata/decorate/types';
import type { Registry } from '@pezkuwi/types/types';
import type { ApiDecoration, ApiTypes } from '../types/index.js';
export interface VersionedRegistry<ApiType extends ApiTypes> {
counter: number;
decoratedApi?: ApiDecoration<ApiType>;
decoratedMeta?: DecoratedMeta;
isDefault?: boolean;
lastBlockHash?: Uint8Array | null;
metadata: Metadata;
registry: Registry;
runtimeVersion: RuntimeVersionPartial;
}
+16
View File
@@ -0,0 +1,16 @@
// Copyright 2017-2025 @pezkuwi/api authors & contributors
// SPDX-License-Identifier: Apache-2.0
import '@pezkuwi/rpc-augment';
// all external
export { Keyring } from '@pezkuwi/keyring';
export { HttpProvider, ScProvider, WsProvider } from '@pezkuwi/rpc-provider';
// all named
export { packageInfo } from './packageInfo.js';
export { SubmittableResult } from './submittable/index.js';
// all starred
export * from './promise/index.js';
export * from './rx/index.js';
+323
View File
@@ -0,0 +1,323 @@
// Copyright 2017-2025 @pezkuwi/api authors & contributors
// SPDX-License-Identifier: Apache-2.0
// Simple non-runnable checks to test type definitions in the editor itself
import '@pezkuwi/api-augment';
import type { HeaderExtended } from '@pezkuwi/api-derive/types';
import type { TestKeyringMapBizinikiwi } from '@pezkuwi/keyring/testingPairs';
import type { StorageKey } from '@pezkuwi/types';
import type { AccountId, Balance, DispatchErrorModule, Event, Header, Index } from '@pezkuwi/types/interfaces';
import type { PezframeSystemAccountInfo } from '@pezkuwi/types/lookup';
import type { AnyTuple, IExtrinsic, IMethod } from '@pezkuwi/types/types';
import type { SubmittableResult } from './index.js';
import { ApiPromise } from '@pezkuwi/api';
import { createTestPairs } from '@pezkuwi/keyring/testingPairs';
import { createTypeUnsafe, TypeRegistry } from '@pezkuwi/types/create';
const registry = new TypeRegistry();
async function calls (api: ApiPromise): Promise<void> {
// it allows defaults
const testSetId = await api.call.grandpaApi.currentSetId();
// it allows type overrides (generally shouldn't be used, but available)
const testSetIdO = await api.call.grandpaApi.currentSetId<AccountId>();
// it allows actual params
const nonce = await api.call.accountNonceApi.accountNonce('5Test');
console.log(testSetId.toNumber(), testSetIdO.isAscii, nonce.toNumber());
}
function consts (api: ApiPromise): void {
// constants has actual value & metadata
console.log(
api.consts['notIn']['augmentation'],
api.consts.balances.existentialDeposit.toNumber(),
api.consts.balances.existentialDeposit.meta.docs.map((s) => s.toString()).join(''),
api.consts.system.blockWeights.maxBlock.refTime.toNumber()
);
}
async function derive (api: ApiPromise): Promise<void> {
await api.derive.chain.subscribeNewHeads((header: HeaderExtended): void => {
console.log('current author:', header.author);
});
const info = await api.derive.balances.account('0x1234');
console.log('info', info);
}
function errors (api: ApiPromise): void {
const someError = {} as DispatchErrorModule;
// existing
console.log(api.errors.vesting.AmountLow.is(someError));
// non-existing error, existing module
console.log(api.errors.vesting['NonAugmented'].is(someError));
// something random
console.log(api.errors['thisIsNot']['Augmented'].is(someError));
}
function events (api: ApiPromise): void {
const event = {} as Event;
// existing
if (api.events.balances.Transfer.is(event)) {
// the types are correctly expanded
const [afrom, ato, aamount] = event.data;
console.log(
afrom.toHuman(),
ato.toHuman(),
aamount.toBn()
);
// the types have getters
const { amount, from, to } = event.data;
console.log(
from.toHuman(),
to.toHuman(),
amount.toBn()
);
}
// something with only tuple data
if (api.events.staking.Bonded.is(event)) {
const [account, amount] = event.data;
console.log(account.toHuman(), amount.toBn());
}
// something random, just codec[]
if (api.events['not']['Augmented'].is(event)) {
const [a, b] = event.data;
console.log(a.toHuman(), b.toHuman());
}
}
async function query (api: ApiPromise, pairs: TestKeyringMapBizinikiwi): Promise<void> {
const intentions = await api.query.staking.bonded();
console.log('intentions:', intentions);
// api.query.*.* is well-typed
const bar = await api.query['notIn']['augmentation'](); // bar is Codec (unknown module)
const bal = await api.query.balances.totalIssuance(); // bal is Balance
const bal2 = await api.query.balances.totalIssuance('WRONG_ARG'); // bal2 is Codec (wrong args)
const override = await api.query.balances.totalIssuance<Header>(); // override is still available
// eslint-disable-next-line deprecation/deprecation
const oldBal = await api.query.balances.totalIssuance.at('abcd');
// For older queries we can cast with `<Balance>` (newer chain have multi typed)
const multia = await api.query.balances['freeBalance'].multi<Balance>([pairs.alice.address, pairs.bob.address]);
const multib = await api.query.system.account.multi([pairs.alice.address, pairs.bob.address]);
await api.query.system.account(pairs.alice.address);
await api.query.system.account<PezframeSystemAccountInfo>(pairs.alice.address);
console.log('query types:', bar, bal, bal2, override, oldBal, multia, multib);
}
async function queryExtra (api: ApiPromise): Promise<void> {
// events destructing
await api.query.system.events((records): void => {
records.forEach(({ event, phase }): void => {
if (phase.isApplyExtrinsic) {
// Dunno... this should work
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
const [accountId, value]: [AccountId, Balance] = event.data;
console.log(`${accountId.toString()} has ${value.toHuman()}`);
}
});
});
// at queries
// eslint-disable-next-line deprecation/deprecation
const events = await api.query.system.events.at('0x12345');
console.log(`Received ${events.length} events:`);
// check entries()
await api.query.system.account.entries(); // should not take a param
await api.query.staking.nominatorSlashInEra.entries(123); // should take a param
// nmap with keys
await api.query.assets.approvals.keys(123, 'blah');
await api.query.assets.account.keys(123);
await api.query.assets.account.entries(123);
await api.query.assets['notAugmented'].keys();
// is
const key = {} as StorageKey;
if (api.query.balances.account.is(key)) {
const [accountId] = key.args;
// should be AccountId type
console.log(accountId.toHuman());
}
}
async function queryMulti (api: ApiPromise, pairs: TestKeyringMapBizinikiwi): Promise<void> {
// check multi for unsub
const multiUnsub = await api.queryMulti([
[api.query.staking.validators],
[api.query.system.events]
], (values): void => {
console.log('values', values);
multiUnsub();
});
// check multi , Promise result
const multiRes = await api.queryMulti([
[api.query.system.account, pairs.eve.address],
// older chains only
[api.query.system['accountNonce'], pairs.eve.address]
]);
console.log(multiRes);
// check multi, via at
const apiAt = await api.at('0x12345678');
const multiResAt = await apiAt.queryMulti([
api.query.timestamp.now,
[apiAt.query.staking.validators],
[apiAt.query.system.account, pairs.eve.address]
]);
console.log(multiResAt);
}
async function rpc (api: ApiPromise): Promise<void> {
// defaults
await api.rpc.chain.subscribeNewHeads((header): void => {
console.log('current header #', header.number.toNumber());
});
// with generic params
await api.rpc.state.subscribeStorage<[Balance]>(['my_balance_key'], ([balance]): void => {
console.log('current balance:', balance.toString());
});
// using json & raw
await api.rpc.chain.getBlock.raw('0x123456');
// using raw subs
await api.rpc.chain.subscribeNewHeads.raw((result: Uint8Array): void => {
console.log(result);
});
// deprecated methods
// eslint-disable-next-line deprecation/deprecation
await api.rpc.state.getPairs('123');
}
function types (api: ApiPromise): void {
// check correct types with `createType`
const balance = registry.createType('Balance', 2);
const gas = registry.createType('Gas', 2);
const compact = registry.createType('Compact<u32>', 2);
const f32 = registry.createType('f32');
const u32 = registry.createType('u32');
const raw = registry.createType('Raw');
// const random = registry.createType('RandomType', 2); // This one should deliberately show a TS error
const gasUnsafe = createTypeUnsafe(registry, 'Gas', [2]);
const overriddenUnsafe = createTypeUnsafe<Header>(registry, 'Gas', [2]);
console.log(balance, gas, compact, gasUnsafe, overriddenUnsafe, u32.toNumber(), f32.toNumber(), api.createType('AccountData'), raw.subarray(0, 10));
}
async function tx (api: ApiPromise, pairs: TestKeyringMapBizinikiwi): Promise<void> {
// transfer, also allows for bigint inputs here
const transfer = api.tx.balances.transferAllowDeath(pairs.bob.address, BigInt(123456789));
console.log('transfer casted', transfer as IMethod<AnyTuple>, transfer as IExtrinsic<AnyTuple>);
// simple "return the hash" variant
console.log('hash:', (await transfer.signAndSend(pairs.alice)).toHex());
// passing options, but waiting for hash
const nonce = await api.query.system['accountNonce']<Index>(pairs.alice.address);
(await api.tx.balances
.transferAllowDeath(pairs.bob.address, 12345)
.signAndSend(pairs.alice, { nonce })
).toHex();
// just with the callback
await api.tx.balances
.transferAllowDeath(pairs.bob.address, 12345)
.signAndSend(pairs.alice, ({ status }: SubmittableResult) => console.log(status.type));
// with options and the callback
const nonce2 = await api.query.system['accountNonce'](pairs.alice.address);
const unsub2 = await api.tx.balances
.transferAllowDeath(pairs.bob.address, 12345)
.signAndSend(pairs.alice, { nonce: nonce2 }, ({ status }: SubmittableResult): void => {
console.log('transfer status:', status.type);
unsub2();
});
// it allows for query & then using the submittable
const second = api.tx.democracy.second(123);
await second.signAndSend('123', (result) => console.log(result));
// it handles enum inputs correctly
await api.tx.democracy['proxyVote'](123, { Split: { nay: 456, yay: 123 } }).signAndSend(pairs.alice);
// is
if (api.tx.balances.transferAllowDeath.is(second)) {
const [recipientId, balance] = second.args;
// should be LookupSource & Balance types
console.log(recipientId.toHuman(), balance.toNumber());
}
}
async function at (api: ApiPromise): Promise<void> {
const apiAt = await api.at('0x1234');
// get old balances
console.log(await apiAt.query.balances['freeBalance']('0x1234'));
// get some constants
console.log(apiAt.consts.balances.existentialDeposit);
}
async function main (): Promise<void> {
const api = await ApiPromise.create();
const pairs = createTestPairs();
await Promise.all([
calls(api),
consts(api),
derive(api),
errors(api),
events(api),
query(api, pairs),
queryExtra(api),
queryMulti(api, pairs),
rpc(api),
types(api),
tx(api, pairs),
at(api)
]);
}
main().catch(console.error);
+6
View File
@@ -0,0 +1,6 @@
// Copyright 2017-2025 @pezkuwi/api authors & contributors
// SPDX-License-Identifier: Apache-2.0
import './packageDetect.js';
export * from './bundle.js';
+4
View File
@@ -0,0 +1,4 @@
// Copyright 2017-2025 @pezkuwi/api authors & contributors
// SPDX-License-Identifier: Apache-2.0
export * from './index.js';
+16
View File
@@ -0,0 +1,16 @@
// Copyright 2017-2026 @pezkuwi/api authors & contributors
// SPDX-License-Identifier: Apache-2.0
// Do not edit, auto-generated by @pezkuwi/dev
// (packageInfo imports will be kept as-is, user-editable)
import { packageInfo as deriveInfo } from '@pezkuwi/api-derive/packageInfo';
import { packageInfo as coreInfo } from '@pezkuwi/rpc-core/packageInfo';
import { packageInfo as providerInfo } from '@pezkuwi/rpc-provider/packageInfo';
import { packageInfo as typesInfo } from '@pezkuwi/types/packageInfo';
import { packageInfo as knownInfo } from '@pezkuwi/types-known/packageInfo';
import { detectPackage } from '@pezkuwi/util';
import { packageInfo } from './packageInfo.js';
detectPackage(packageInfo, null, [coreInfo, deriveInfo, knownInfo, providerInfo, typesInfo]);
+6
View File
@@ -0,0 +1,6 @@
// Copyright 2017-2026 @pezkuwi/api authors & contributors
// SPDX-License-Identifier: Apache-2.0
// Do not edit, auto-generated by @pezkuwi/dev
export const packageInfo = { name: '@pezkuwi/api', path: 'auto', type: 'auto', version: '16.5.4' };
+214
View File
@@ -0,0 +1,214 @@
// Copyright 2017-2025 @pezkuwi/api authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { ApiOptions, UnsubscribePromise } from '../types/index.js';
import type { CombinatorCallback, CombinatorFunction } from './Combinator.js';
import { noop, objectSpread } from '@pezkuwi/util';
import { ApiBase } from '../base/index.js';
import { Combinator } from './Combinator.js';
import { promiseTracker, toPromiseMethod } from './decorateMethod.js';
/**
* # @pezkuwi/api/promise
*
* ## Overview
*
* @name ApiPromise
* @description
* ApiPromise is a standard JavaScript wrapper around the RPC and interfaces on the Pezkuwi network. As a full Promise-based, all interface calls return Promises, including the static `.create(...)`. Subscription calls utilise `(value) => {}` callbacks to pass through the latest values.
*
* The API is well suited to real-time applications where either the single-shot state is needed or use is to be made of the subscription-based features of Pezkuwi (and Bizinikiwi) clients.
*
* @see [[ApiRx]]
*
* ## Usage
*
* Making rpc calls -
* <BR>
*
* ```javascript
* import ApiPromise from '@pezkuwi/api/promise';
*
* // initialise via static create
* const api = await ApiPromise.create();
*
* // make a subscription to the network head
* api.rpc.chain.subscribeNewHeads((header) => {
* console.log(`Chain is at #${header.number}`);
* });
* ```
* <BR>
*
* Subscribing to chain state -
* <BR>
*
* ```javascript
* import { ApiPromise, WsProvider } from '@pezkuwi/api';
*
* // initialise a provider with a specific endpoint
* const provider = new WsProvider('wss://example.com:9944')
*
* // initialise via isReady & new with specific provider
* const api = await new ApiPromise({ provider }).isReady;
*
* // retrieve the block target time
* const blockPeriod = await api.query.timestamp.blockPeriod().toNumber();
* let last = 0;
*
* // subscribe to the current block timestamp, updates automatically (callback provided)
* api.query.timestamp.now((timestamp) => {
* const elapsed = last
* ? `, ${timestamp.toNumber() - last}s since last`
* : '';
*
* last = timestamp.toNumber();
* console.log(`timestamp ${timestamp}${elapsed} (${blockPeriod}s target)`);
* });
* ```
* <BR>
*
* Submitting a transaction -
* <BR>
*
* ```javascript
* import ApiPromise from '@pezkuwi/api/promise';
*
* ApiPromise.create().then((api) => {
* const [nonce] = await api.query.system.account(keyring.alice.address);
*
* api.tx.balances
* // create transfer
* transfer(keyring.bob.address, 12345)
* // sign the transcation
* .sign(keyring.alice, { nonce })
* // send the transaction (optional status callback)
* .send((status) => {
* console.log(`current status ${status.type}`);
* })
* // retrieve the submitted extrinsic hash
* .then((hash) => {
* console.log(`submitted with hash ${hash}`);
* });
* });
* ```
*/
export class ApiPromise extends ApiBase<'promise'> {
#isReadyPromise: Promise<ApiPromise>;
#isReadyOrErrorPromise: Promise<ApiPromise>;
/**
* @description Creates an instance of the ApiPromise class
* @param options Options to create an instance. This can be either [[ApiOptions]] or
* an [[WsProvider]].
* @example
* <BR>
*
* ```javascript
* import Api from '@pezkuwi/api/promise';
*
* new Api().isReady.then((api) => {
* api.rpc.subscribeNewHeads((header) => {
* console.log(`new block #${header.number.toNumber()}`);
* });
* });
* ```
*/
constructor (options?: ApiOptions) {
super(options, 'promise', toPromiseMethod);
this.#isReadyPromise = new Promise((resolve): void => {
super.once('ready', () => resolve(this));
});
this.#isReadyOrErrorPromise = new Promise((resolve, reject): void => {
const tracker = promiseTracker(resolve, reject);
super.once('ready', () => tracker.resolve(this));
super.once('error', (error: Error) => tracker.reject(error));
});
}
/**
* @description Creates an ApiPromise instance using the supplied provider. Returns an Promise containing the actual Api instance.
* @param options options that is passed to the class contructor. Can be either [[ApiOptions]] or a
* provider (see the constructor arguments)
* @example
* <BR>
*
* ```javascript
* import Api from '@pezkuwi/api/promise';
*
* Api.create().then(async (api) => {
* const timestamp = await api.query.timestamp.now();
*
* console.log(`lastest block timestamp ${timestamp}`);
* });
* ```
*/
public static create (options?: ApiOptions): Promise<ApiPromise> {
const instance = new ApiPromise(options);
if (options && options.throwOnConnect) {
return instance.isReadyOrError;
}
// Swallow any rejections on isReadyOrError
// (in Node 15.x this creates issues, when not being looked at)
instance.isReadyOrError.catch(noop);
return instance.isReady;
}
/**
* @description Promise that resolves the first time we are connected and loaded
*/
public get isReady (): Promise<ApiPromise> {
return this.#isReadyPromise;
}
/**
* @description Promise that resolves if we can connect, or reject if there is an error
*/
public get isReadyOrError (): Promise<ApiPromise> {
return this.#isReadyOrErrorPromise;
}
/**
* @description Returns a clone of this ApiPromise instance (new underlying provider connection)
*/
public clone (): ApiPromise {
return new ApiPromise(
objectSpread({}, this._options, { source: this })
);
}
/**
* @description Creates a combinator that can be used to combine the latest results from multiple subscriptions
* @param fns An array of function to combine, each in the form of `(cb: (value: void)) => void`
* @param callback A callback that will return an Array of all the values this combinator has been applied to
* @example
* <BR>
*
* ```javascript
* const address = '5DTestUPts3kjeXSTMyerHihn1uwMfLj8vU8sqF7qYrFacT7';
*
* // combines values from balance & nonce as it updates
* api.combineLatest([
* api.rpc.chain.subscribeNewHeads,
* (cb) => api.query.system.account(address, cb)
* ], ([head, [balance, nonce]]) => {
* console.log(`#${head.number}: You have ${balance.free} units, with ${nonce} transactions sent`);
* });
* ```
*/
// eslint-disable-next-line @typescript-eslint/require-await
public async combineLatest <T extends any[] = any[]> (fns: (CombinatorFunction | [CombinatorFunction, ...any[]])[], callback: CombinatorCallback<T>): UnsubscribePromise {
const combinator = new Combinator(fns, callback);
return (): void => {
combinator.unsubscribe();
};
}
}
+91
View File
@@ -0,0 +1,91 @@
// Copyright 2017-2025 @pezkuwi/api authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { Callback } from '@pezkuwi/types/types';
import type { UnsubscribePromise } from '../types/index.js';
import { isFunction, noop } from '@pezkuwi/util';
export type CombinatorCallback <T extends unknown[]> = Callback<T>;
export type CombinatorFunction = (cb: Callback<any>) => UnsubscribePromise;
export class Combinator<T extends unknown[] = unknown[]> {
#allHasFired = false;
#callback: CombinatorCallback<T>;
#fired: boolean[] = [];
#fns: CombinatorFunction[] = [];
#isActive = true;
#results: unknown[] = [];
#subscriptions: UnsubscribePromise[] = [];
constructor (fns: (CombinatorFunction | [CombinatorFunction, ...unknown[]])[], callback: CombinatorCallback<T>) {
this.#callback = callback;
// eslint-disable-next-line @typescript-eslint/no-floating-promises, @typescript-eslint/require-await
this.#subscriptions = fns.map(async (input, index): UnsubscribePromise => {
const [fn, ...args] = Array.isArray(input)
? input
: [input];
this.#fired.push(false);
this.#fns.push(fn);
// Not quite 100% how to have a variable number at the front here
// eslint-disable-next-line @typescript-eslint/no-unsafe-return,@typescript-eslint/ban-types
return (fn as Function)(...args, this._createCallback(index));
});
}
protected _allHasFired (): boolean {
this.#allHasFired ||= this.#fired.filter((hasFired): boolean => !hasFired).length === 0;
return this.#allHasFired;
}
protected _createCallback (index: number): (value: any) => void {
return (value: unknown): void => {
this.#fired[index] = true;
this.#results[index] = value;
this._triggerUpdate();
};
}
protected _triggerUpdate (): void {
if (!this.#isActive || !isFunction(this.#callback) || !this._allHasFired()) {
return;
}
try {
Promise
.resolve(this.#callback(this.#results as T))
.catch(noop);
} catch {
// swallow, we don't want the handler to trip us up
}
}
public unsubscribe (): void {
if (!this.#isActive) {
return;
}
this.#isActive = false;
Promise
.all(this.#subscriptions.map(async (subscription): Promise<void> => {
try {
const unsubscribe = await subscription;
if (isFunction(unsubscribe)) {
unsubscribe();
}
} catch {
// ignore
}
})).catch(() => {
// ignore, already ignored above, should never throw
});
}
}
@@ -0,0 +1,109 @@
// Copyright 2017-2025 @pezkuwi/api authors & contributors
// SPDX-License-Identifier: Apache-2.0
/// <reference types="@pezkuwi/dev-test/globals.d.ts" />
import type { UnsubscribePromise } from '../types/index.js';
import { Combinator } from './Combinator.js';
describe('Combinator', (): void => {
let fns: ((value: any) => void)[] = [];
// eslint-disable-next-line @typescript-eslint/require-await
const storeFn = async (cb: (value: any) => void): UnsubscribePromise => {
fns.push(cb);
return (): void => undefined;
};
beforeEach((): void => {
fns = [];
});
it('triggers on all values', async (): Promise<void> => {
await new Promise<boolean>((resolve) => {
let count = 0;
const combinator = new Combinator(
[storeFn],
(value: any[]): void => {
expect(value[0]).toEqual(`test${count}`);
count++;
if (count === 3) {
resolve(true);
}
}
);
fns[0]('test0');
fns[0]('test1');
fns[0]('test2');
expect(combinator).toBeDefined();
});
});
it('combines values from 2 sources, firing when it has all results', async (): Promise<void> => {
await new Promise<boolean>((resolve) => {
const combinator = new Combinator(
[storeFn, storeFn],
(value: any[]): void => {
expect(value).toEqual(['test0', 'test1']);
resolve(true);
}
);
fns[0]('test0');
fns[1]('test1');
expect(combinator).toBeDefined();
});
});
it('combines values from 2 sources, allowing multiple updates', async (): Promise<void> => {
await new Promise<boolean>((resolve) => {
let count = 0;
const combinator = new Combinator(
[storeFn, storeFn],
(value: any[]): void => {
expect(value).toEqual(
count === 0
? ['test0', 'test1']
: ['test2', 'test1']);
count++;
if (count === 2) {
resolve(true);
}
}
);
fns[0]('test0');
fns[1]('test1');
fns[0]('test2');
expect(combinator).toBeDefined();
});
});
// eslint-disable-next-line jest/expect-expect
it('unsubscribes as required', async (): Promise<void> => {
await new Promise<void>((resolve) => {
// eslint-disable-next-line @typescript-eslint/require-await
const mocker = () => Promise.resolve(resolve);
const combinator = new Combinator([
mocker,
// eslint-disable-next-line @typescript-eslint/require-await
async (): UnsubscribePromise => (): void => undefined
], (_: any[]): void => {
// ignore
});
combinator.unsubscribe();
});
});
});
+118
View File
@@ -0,0 +1,118 @@
// Copyright 2017-2025 @pezkuwi/api authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { Observable, Subscription } from 'rxjs';
import type { Callback, Codec } from '@pezkuwi/types/types';
import type { DecorateFn, DecorateMethodOptions, ObsInnerType, StorageEntryPromiseOverloads, UnsubscribePromise, VoidFn } from '../types/index.js';
import { catchError, EMPTY, tap } from 'rxjs';
import { isFunction, nextTick } from '@pezkuwi/util';
interface Tracker<T> {
reject: (value: Error) => Observable<never>;
resolve: (value: T) => void;
}
type CodecReturnType<T extends (...args: unknown[]) => Observable<Codec>> =
T extends (...args: any) => infer R
? R extends Observable<Codec>
? ObsInnerType<R>
: never
: never;
// a Promise completion tracker, wrapping an isComplete variable that ensures
// that the promise only resolves once
export function promiseTracker<T> (resolve: (value: T) => void, reject: (value: Error) => void): Tracker<T> {
let isCompleted = false;
return {
reject: (error: Error): Observable<never> => {
if (!isCompleted) {
isCompleted = true;
reject(error);
}
return EMPTY;
},
resolve: (value: T): void => {
if (!isCompleted) {
isCompleted = true;
resolve(value);
}
}
};
}
// extract the arguments and callback params from a value array possibly containing a callback
function extractArgs (args: unknown[], needsCallback: boolean): [unknown[], Callback<Codec> | undefined] {
const actualArgs = args.slice();
// If the last arg is a function, we pop it, put it into callback.
// actualArgs will then hold the actual arguments to be passed to `method`
const callback = (args.length && isFunction(args[args.length - 1]))
? actualArgs.pop() as Callback<Codec>
: undefined;
// When we need a subscription, ensure that a valid callback is actually passed
if (needsCallback && !isFunction(callback)) {
throw new Error('Expected a callback to be passed with subscriptions');
}
return [actualArgs, callback];
}
// Decorate a call for a single-shot result - retrieve and then immediate unsubscribe
function decorateCall<M extends DecorateFn<CodecReturnType<M>>> (method: M, args: unknown[]): Promise<CodecReturnType<M>> {
return new Promise((resolve, reject): void => {
// single result tracker - either reject with Error or resolve with Codec result
const tracker = promiseTracker(resolve, reject);
// encoding errors reject immediately, any result unsubscribes and resolves
const subscription: Subscription = method(...args)
.pipe(
catchError((error: Error) => tracker.reject(error))
)
.subscribe((result): void => {
tracker.resolve(result);
nextTick(() => subscription.unsubscribe());
});
});
}
// Decorate a subscription where we have a result callback specified
function decorateSubscribe<M extends DecorateFn<CodecReturnType<M>>> (method: M, args: unknown[], resultCb: Callback<Codec>): UnsubscribePromise {
return new Promise<VoidFn>((resolve, reject): void => {
// either reject with error or resolve with unsubscribe callback
const tracker = promiseTracker(resolve, reject);
// errors reject immediately, the first result resolves with an unsubscribe promise, all results via callback
const subscription: Subscription = method(...args)
.pipe(
catchError((error: Error) => tracker.reject(error)),
tap(() => tracker.resolve(() => subscription.unsubscribe()))
)
.subscribe((result): void => {
// queue result (back of queue to clear current)
nextTick(() => resultCb(result));
});
});
}
/**
* @description Decorate method for ApiPromise, where the results are converted to the Promise equivalent
*/
export function toPromiseMethod<M extends DecorateFn<CodecReturnType<M>>> (method: M, options?: DecorateMethodOptions): StorageEntryPromiseOverloads {
const needsCallback = !!(options?.methodName && options.methodName.includes('subscribe'));
return function (...args: unknown[]): Promise<CodecReturnType<M>> | UnsubscribePromise {
const [actualArgs, resultCb] = extractArgs(args, needsCallback);
return resultCb
? decorateSubscribe(method, actualArgs, resultCb)
: decorateCall((options?.overrideNoSub as M) || method, actualArgs);
} as StorageEntryPromiseOverloads;
}
+167
View File
@@ -0,0 +1,167 @@
// Copyright 2017-2025 @pezkuwi/api authors & contributors
// SPDX-License-Identifier: Apache-2.0
/// <reference types="@pezkuwi/dev-test/globals.d.ts" />
import type { HexString } from '@pezkuwi/util/types';
import type { SubmittableExtrinsic } from '../types/index.js';
import { createPair } from '@pezkuwi/keyring/pair';
import { createTestKeyring } from '@pezkuwi/keyring/testing';
import { MockProvider } from '@pezkuwi/rpc-provider/mock';
import { TypeRegistry } from '@pezkuwi/types';
import { hexToU8a } from '@pezkuwi/util';
import { SingleAccountSigner } from '../test/index.js';
import { ApiPromise } from './index.js';
const TRANSFER_SIG = '0xbb861f9c905d860d303101dfd23a6042251721ca65fb1a58e317d628f08484767a3604afeaede64a4116d08daae3c285ea2ea97c8b6c7b3548e90df327c4e60c';
describe('ApiPromise', (): void => {
const registry = new TypeRegistry();
const keyring = createTestKeyring({ type: 'ed25519' });
const aliceEd = keyring.addPair(
createPair({ toSS58: keyring.encodeAddress, type: 'ed25519' }, {
publicKey: hexToU8a('0x88dc3417d5058ec4b4503e0c12ea1a0a89be200fe98922423d4334014fa6b0ee'),
secretKey: hexToU8a('0xabf8e5bdbe30c65656c0a3cbd181ff8a56294a69dfedd27982aace4a7690911588dc3417d5058ec4b4503e0c12ea1a0a89be200fe98922423d4334014fa6b0ee')
})
);
let provider: MockProvider;
async function createTransfer (): Promise<{ api: ApiPromise; transfer: SubmittableExtrinsic<'promise'> }> {
provider.subscriptions.state_subscribeStorage.lastValue = {
changes: [
[
'0x26aa394eea5630e07c48ae0c9558cef79c2f82b23e5fd031fb54c292794b4cc4d560eb8d00e57357cf76492334e43bb2ecaa9f28df6a8c4426d7b6090f7ad3c9',
'0x00'
]
]
};
const signer = new SingleAccountSigner(registry, aliceEd);
const api = await ApiPromise.create({ provider, registry, signer, throwOnConnect: true });
const transfer = api.tx.balances.transferAllowDeath(keyring.getPair('0xe659a7a1628cdd93febc04a4e0646ea20e9f5f0ce097d9a05290d4a9e054df4e').address, 321564789876512345n);
return { api, transfer: await transfer.signAsync(aliceEd.address, {}) };
}
beforeEach((): void => {
provider = new MockProvider(registry);
});
afterEach(async () => {
await provider.disconnect();
});
describe('initialization', (): void => {
it('Create API instance with metadata map and makes the runtime, rpc, state & extrinsics available', async (): Promise<void> => {
const rpcData = await provider.send<HexString>('state_getMetadata', []);
const genesisHash = registry.createType('Hash', await provider.send('chain_getBlockHash', [])).toHex();
const specVersion = 0;
const api = await ApiPromise.create({ metadata: { [`${genesisHash}-${specVersion}`]: rpcData }, provider, registry, throwOnConnect: true });
expect(api.genesisHash).toBeDefined();
expect(api.runtimeMetadata).toBeDefined();
expect(api.runtimeVersion).toBeDefined();
expect(api.rpc).toBeDefined();
expect(api.query).toBeDefined();
expect(api.tx).toBeDefined();
expect(api.derive).toBeDefined();
await api.disconnect();
});
it('Create API instance without metadata and makes the runtime, rpc, state & extrinsics available', async (): Promise<void> => {
const metadata = {};
const api = await ApiPromise.create({ metadata, provider, registry, throwOnConnect: true });
expect(api.genesisHash).toBeDefined();
expect(api.runtimeMetadata).toBeDefined();
expect(api.runtimeVersion).toBeDefined();
expect(api.rpc).toBeDefined();
expect(api.query).toBeDefined();
expect(api.tx).toBeDefined();
expect(api.derive).toBeDefined();
await api.disconnect();
});
// eslint-disable-next-line jest/expect-expect
it('Create API instance will error on failure to await ready', async (): Promise<void> => {
class ErrorApiPromise extends ApiPromise {
constructor () {
super({ provider });
}
protected override _loadMeta (): Promise<boolean> {
throw new Error('Simulate failure to load meta');
}
}
try {
const api = await ErrorApiPromise.create({ provider, throwOnConnect: true });
await api.disconnect();
throw new Error('Expected an error but none occurred.');
} catch {
// Pass
}
});
});
describe('api.sign', (): void => {
const ADDR = '5FA9nQDVg267DEd8m1ZypXLBnvN7SFxYwV7ndqSYGiN9TTpu';
const TEST = { data: '0x0102030405060708090a0b0c0d0e0f112233445566778899aabbccddeeff' };
const SIG = '0x659effefbbe5ab4d7136ebb5084b959eb424e32b862307371be4721ac2c46334245af4f1476c36c5e5aff04396c2fdd2ce561ec90382821d4aa071b559b1db0f';
it('signs data using a specified keyring', async (): Promise<void> => {
const api = await ApiPromise.create({ provider, registry, throwOnConnect: true });
const sig = await api.sign(aliceEd, TEST);
expect(sig).toEqual(SIG);
await api.disconnect();
});
it('signs data using an external signer', async (): Promise<void> => {
const api = await ApiPromise.create({ provider, registry, signer: new SingleAccountSigner(registry, aliceEd), throwOnConnect: true });
const sig = await api.sign(ADDR, TEST);
expect(sig).toEqual(SIG);
await api.disconnect();
});
});
describe('decorator.signAsync', (): void => {
it('signs a transfer using an external signer', async (): Promise<void> => {
const { api, transfer } = await createTransfer();
expect(transfer.signature.toHex()).toEqual(TRANSFER_SIG);
await api.disconnect();
});
});
describe('api.tx(...)', (): void => {
it('allows construction from existing extrinsic', async (): Promise<void> => {
const { api, transfer } = await createTransfer();
expect(api.tx(transfer.toHex()).signature.toHex()).toEqual(TRANSFER_SIG);
expect(api.tx(transfer).signature.toHex()).toEqual(TRANSFER_SIG);
await api.disconnect();
});
});
describe('api.rpc(...)', (): void => {
it('allows sending rpc call', async (): Promise<void> => {
const { api } = await createTransfer();
expect(await api.rpc('dev_echo', 'hello', 'world')).toEqual(['hello', 'world']);
await api.disconnect();
});
});
});
+5
View File
@@ -0,0 +1,5 @@
// Copyright 2017-2025 @pezkuwi/api authors & contributors
// SPDX-License-Identifier: Apache-2.0
export { ApiPromise } from './Api.js';
export { toPromiseMethod } from './decorateMethod.js';
+9
View File
@@ -0,0 +1,9 @@
// Copyright 2017-2025 @pezkuwi/api authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { SubmittableExtrinsic as SubmittableExtrinsicBase } from '../submittable/types.js';
import type { QueryableStorageEntry as QueryableStorageEntryBase, SubmittableExtrinsicFunction as SubmittableExtrinsicFunctionBase } from '../types/index.js';
export type QueryableStorageEntry = QueryableStorageEntryBase<'promise'>;
export type SubmittableExtrinsic = SubmittableExtrinsicBase<'promise'>;
export type SubmittableExtrinsicFunction = SubmittableExtrinsicFunctionBase<'promise'>;
+186
View File
@@ -0,0 +1,186 @@
// Copyright 2017-2025 @pezkuwi/api authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { Observable } from 'rxjs';
import type { ApiOptions } from '../types/index.js';
import { from } from 'rxjs';
import { objectSpread } from '@pezkuwi/util';
import { ApiBase } from '../base/index.js';
import { toRxMethod } from './decorateMethod.js';
/**
* # @pezkuwi/api/rx
*
* ## Overview
*
* @name ApiRx
*
* @description
* ApiRx is a powerful RxJS Observable wrapper around the RPC and interfaces on the Pezkuwi network. As a full Observable API, all interface calls return RxJS Observables, including the static `.create(...)`. In the same fashion and subscription-based methods return long-running Observables that update with the latest values.
*
* The API is well suited to real-time applications where the latest state is needed, unlocking the subscription-based features of Pezkuwi (and Bizinikiwi) clients. Some familiarity with RxJS is a requirement to use the API, however just understanding `.subscribe` and `.pipe` on Observables will unlock full-scale use thereof.
*
* @see [[ApiPromise]]
*
* ## Usage
*
* Making rpc calls -
* <BR>
*
* ```javascript
* import ApiRx from '@pezkuwi/api/rx';
*
* // initialize via Promise & static create
* const api = await ApiRx.create().toPromise();
*
* // make a call to retrieve the current network head
* api.rpc.chain.subscribeNewHeads().subscribe((header) => {
* console.log(`Chain is at #${header.number}`);
* });
* ```
* <BR>
*
* Subscribing to chain state -
* <BR>
*
* ```javascript
* import { combineLatest, pairwise, switchMap } from 'rxjs';
* import { ApiRx, WsProvider } from '@pezkuwi/api';
*
*
* // initialize a provider with a specific endpoint
* const provider = new WsProvider('wss://example.com:9944')
*
* // initialize via isReady & new with specific provider
* new ApiRx({ provider })
* .isReady
* .pipe(
* switchMap((api) =>
* combineLatest([
* api.query.timestamp.blockPeriod(),
* api.query.timestamp.now().pipe(pairwise())
* ])
* )
* )
* .subscribe(([blockPeriod, timestamp]) => {
* const elapsed = timestamp[1].toNumber() - timestamp[0].toNumber();
* console.log(`timestamp ${timestamp[1]} \nelapsed ${elapsed} \n(${blockPeriod}s target)`);
* });
* ```
* <BR>
*
* Submitting a transaction -
* <BR>
*
* ```javascript
* import { first, switchMap } from 'rxjs';
* import ApiRx from '@pezkuwi/api/rx';
*
* // import the test keyring (already has dev keys for Alice, Bob, Charlie, Eve & Ferdie)
* import testingPairs from '@pezkuwi/keyring/testingPairs';
* const keyring = testingPairs();
*
* // get api via Promise
* const api = await ApiRx.create().toPromise();
*
* // retrieve nonce for the account
* api.query.system
* .account(keyring.alice.address)
* .pipe(
* first(),
* // pipe nonce into transfer
* switchMap(([nonce]) =>
* api.tx.balances
* // create transfer
* .transferAllowDeath(keyring.bob.address, 12345)
* // sign the transaction
* .sign(keyring.alice, { nonce })
* // send the transaction
* .send()
* )
* )
* // subscribe to overall result
* .subscribe(({ status }) => {
* if (status.isInBlock) {
* console.log('Completed at block hash', status.asFinalized.toHex());
* }
* });
* ```
*/
export class ApiRx extends ApiBase<'rxjs'> {
#isReadyRx: Observable<ApiRx>;
/**
* @description Create an instance of the ApiRx class
* @param options Options to create an instance. Can be either [[ApiOptions]] or [[WsProvider]]
* @example
* <BR>
*
* ```javascript
* import { switchMap } from 'rxjs';
* import Api from '@pezkuwi/api/rx';
*
* new Api().isReady
* .pipe(
* switchMap((api) =>
* api.rpc.chain.subscribeNewHeads()
* ))
* .subscribe((header) => {
* console.log(`new block #${header.number.toNumber()}`);
* });
* ```
*/
constructor (options?: ApiOptions) {
super(options, 'rxjs', toRxMethod);
this.#isReadyRx = from<Promise<ApiRx>>(
// You can create an observable from an event, however my mind groks this form better
new Promise((resolve): void => {
super.on('ready', () => resolve(this));
})
);
}
/**
* @description Creates an ApiRx instance using the supplied provider. Returns an Observable containing the actual Api instance.
* @param options options that is passed to the class constructor. Can be either [[ApiOptions]] or [[WsProvider]]
* @example
* <BR>
*
* ```javascript
* import { switchMap } from 'rxjs';
* import Api from '@pezkuwi/api/rx';
*
* Api.create()
* .pipe(
* switchMap((api) =>
* api.rpc.chain.subscribeNewHeads()
* ))
* .subscribe((header) => {
* console.log(`new block #${header.number.toNumber()}`);
* });
* ```
*/
public static create (options?: ApiOptions): Observable<ApiRx> {
return new ApiRx(options).isReady;
}
/**
* @description Observable that returns the first time we are connected and loaded
*/
public get isReady (): Observable<ApiRx> {
return this.#isReadyRx;
}
/**
* @description Returns a clone of this ApiRx instance (new underlying provider connection)
*/
public clone (): ApiRx {
return new ApiRx(
objectSpread({}, this._options, { source: this })
);
}
}
+9
View File
@@ -0,0 +1,9 @@
// Copyright 2017-2025 @pezkuwi/api authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { Codec } from '@pezkuwi/types/types';
import type { DecorateFn } from '../types/index.js';
export function toRxMethod <M extends DecorateFn<Codec>> (method: M): M {
return method;
}
+5
View File
@@ -0,0 +1,5 @@
// Copyright 2017-2025 @pezkuwi/api authors & contributors
// SPDX-License-Identifier: Apache-2.0
export { ApiRx } from './Api.js';
export { toRxMethod } from './decorateMethod.js';
+111
View File
@@ -0,0 +1,111 @@
// Copyright 2017-2025 @pezkuwi/api authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { BlockNumber, DispatchError, DispatchInfo, EventRecord, ExtrinsicStatus, Hash } from '@pezkuwi/types/interfaces';
import type { AnyJson, ISubmittableResult } from '@pezkuwi/types/types';
import type { SubmittableResultValue } from './types.js';
const recordIdentity = (record: EventRecord) => record;
function filterAndApply <T> (events: EventRecord[], section: string, methods: string[], onFound: (record: EventRecord) => T): T[] {
return events
.filter(({ event }) =>
section === event.section &&
methods.includes(event.method)
)
.map((record) => onFound(record));
}
function getDispatchError ({ event: { data: [dispatchError] } }: EventRecord): DispatchError {
return dispatchError as DispatchError;
}
function getDispatchInfo ({ event: { data, method } }: EventRecord): DispatchInfo {
return method === 'ExtrinsicSuccess'
? data[0] as DispatchInfo
: data[1] as DispatchInfo;
}
function extractError (events: EventRecord[] = []): DispatchError | undefined {
return filterAndApply(events, 'system', ['ExtrinsicFailed'], getDispatchError)[0];
}
function extractInfo (events: EventRecord[] = []): DispatchInfo | undefined {
return filterAndApply(events, 'system', ['ExtrinsicFailed', 'ExtrinsicSuccess'], getDispatchInfo)[0];
}
export class SubmittableResult implements ISubmittableResult {
readonly dispatchError?: DispatchError | undefined;
readonly dispatchInfo?: DispatchInfo | undefined;
readonly internalError?: Error | undefined;
readonly events: EventRecord[];
readonly status: ExtrinsicStatus;
readonly txHash: Hash;
readonly txIndex?: number | undefined;
readonly blockNumber?: BlockNumber | undefined;
constructor ({ blockNumber, dispatchError, dispatchInfo, events, internalError, status, txHash, txIndex }: SubmittableResultValue) {
this.dispatchError = dispatchError || extractError(events);
this.dispatchInfo = dispatchInfo || extractInfo(events);
this.events = events || [];
this.internalError = internalError;
this.status = status;
this.txHash = txHash;
this.txIndex = txIndex;
this.blockNumber = blockNumber;
}
public get isCompleted (): boolean {
return this.isError || this.status.isInBlock || this.status.isFinalized;
}
public get isError (): boolean {
return this.status.isDropped || this.status.isFinalityTimeout || this.status.isInvalid || this.status.isUsurped;
}
public get isFinalized (): boolean {
return this.status.isFinalized;
}
public get isInBlock (): boolean {
return this.status.isInBlock;
}
public get isWarning (): boolean {
return this.status.isRetracted;
}
/**
* @description Filters EventRecords for the specified method & section (there could be multiple)
*/
public filterRecords (section: string, method: string | string[]): EventRecord[] {
return filterAndApply(this.events, section, Array.isArray(method) ? method : [method], recordIdentity);
}
/**
* @description Finds an EventRecord for the specified method & section
*/
public findRecord (section: string, method: string | string[]): EventRecord | undefined {
return this.filterRecords(section, method)[0];
}
/**
* @description Creates a human representation of the output
*/
public toHuman (isExtended?: boolean): AnyJson {
return {
dispatchError: this.dispatchError?.toHuman(),
dispatchInfo: this.dispatchInfo?.toHuman(),
events: this.events.map((e) => e.toHuman(isExtended)),
internalError: this.internalError?.message.toString(),
status: this.status.toHuman(isExtended)
};
}
}
+438
View File
@@ -0,0 +1,438 @@
// Copyright 2017-2025 @pezkuwi/api authors & contributors
// SPDX-License-Identifier: Apache-2.0
/* eslint-disable no-dupe-class-members */
import type { Observable } from 'rxjs';
import type { Address, ApplyExtrinsicResult, Call, Extrinsic, ExtrinsicEra, ExtrinsicStatus, Hash, Header, Index, RuntimeDispatchInfo, SignerPayload } from '@pezkuwi/types/interfaces';
import type { Callback, Codec, CodecClass, ISubmittableResult, SignatureOptions } from '@pezkuwi/types/types';
import type { Registry } from '@pezkuwi/types-codec/types';
import type { HexString } from '@pezkuwi/util/types';
import type { ApiBase } from '../base/index.js';
import type { ApiInterfaceRx, ApiTypes, PromiseOrObs, SignerResult } from '../types/index.js';
import type { AddressOrPair, SignerOptions, SubmittableDryRunResult, SubmittableExtrinsic, SubmittablePaymentResult, SubmittableResultResult, SubmittableResultSubscription } from './types.js';
import { catchError, first, map, mergeMap, of, switchMap, tap } from 'rxjs';
import { identity, isBn, isFunction, isNumber, isString, isU8a, objectSpread } from '@pezkuwi/util';
import { filterEvents, isKeyringPair } from '../util/index.js';
import { SubmittableResult } from './Result.js';
interface SubmittableOptions<ApiType extends ApiTypes> {
api: ApiInterfaceRx;
apiType: ApiTypes;
blockHash?: Uint8Array | undefined;
decorateMethod: ApiBase<ApiType>['_decorateMethod'];
}
interface UpdateInfo {
options: SignatureOptions;
updateId: number;
signedTransaction: HexString | Uint8Array | null;
}
interface SignerInfo {
id: number;
signedTransaction?: HexString | Uint8Array;
}
function makeEraOptions (api: ApiInterfaceRx, registry: Registry, partialOptions: Partial<SignerOptions>, { header, mortalLength, nonce }: { header: Header | null; mortalLength: number; nonce: Index }): SignatureOptions {
if (!header) {
if (partialOptions.era && !partialOptions.blockHash) {
throw new Error('Expected blockHash to be passed alongside non-immortal era options');
}
if (isNumber(partialOptions.era)) {
// since we have no header, it is immortal, remove any option overrides
// so we only supply the genesisHash and no era to the construction
delete partialOptions.era;
delete partialOptions.blockHash;
}
return makeSignOptions(api, partialOptions, { nonce });
}
return makeSignOptions(api, partialOptions, {
blockHash: header.hash,
era: registry.createTypeUnsafe<ExtrinsicEra>('ExtrinsicEra', [{
current: header.number,
period: partialOptions.era || mortalLength
}]),
nonce
});
}
function makeSignAndSendOptions (partialOptions?: Partial<SignerOptions> | Callback<ISubmittableResult>, statusCb?: Callback<ISubmittableResult>): [Partial<SignerOptions>, Callback<ISubmittableResult> | undefined] {
let options: Partial<SignerOptions> = {};
if (isFunction(partialOptions)) {
statusCb = partialOptions;
} else {
options = objectSpread({}, partialOptions);
}
return [options, statusCb];
}
function makeSignOptions (api: ApiInterfaceRx, partialOptions: Partial<SignerOptions>, extras: { blockHash?: Hash; era?: ExtrinsicEra; nonce?: Index }): SignatureOptions {
return objectSpread(
{ blockHash: api.genesisHash, genesisHash: api.genesisHash },
partialOptions,
extras,
{ runtimeVersion: api.runtimeVersion, signedExtensions: api.registry.signedExtensions, version: api.extrinsicType }
);
}
function optionsOrNonce (partialOptions: Partial<SignerOptions> = {}): Partial<SignerOptions> {
return isBn(partialOptions) || isNumber(partialOptions)
? { nonce: partialOptions }
: partialOptions;
}
export function createClass <ApiType extends ApiTypes> ({ api, apiType, blockHash, decorateMethod }: SubmittableOptions<ApiType>): CodecClass<SubmittableExtrinsic<ApiType>> {
// an instance of the base extrinsic for us to extend
const ExtrinsicBase = api.registry.createClass('Extrinsic');
const extrinsicInfoMap = new WeakMap<SubmittableExtrinsic<ApiType>, UpdateInfo>();
class Submittable extends ExtrinsicBase implements SubmittableExtrinsic<ApiType> {
readonly #ignoreStatusCb: boolean;
#transformResult = identity<ISubmittableResult>;
constructor (registry: Registry, extrinsic: Call | Extrinsic | Uint8Array | string) {
super(registry, extrinsic, { version: api.extrinsicType });
this.#ignoreStatusCb = apiType === 'rxjs';
}
public get hasDryRun (): boolean {
return isFunction(api.rpc.system?.dryRun);
}
public get hasPaymentInfo (): boolean {
return isFunction(api.call.transactionPaymentApi?.queryInfo);
}
// dry run an extrinsic
public dryRun (account: AddressOrPair, optionsOrHash?: Partial<SignerOptions> | Uint8Array | string): SubmittableDryRunResult<ApiType> {
if (!this.hasDryRun) {
throw new Error('The system.dryRun RPC call is not available in your environment');
}
if (blockHash || isString(optionsOrHash) || isU8a(optionsOrHash)) {
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
return decorateMethod(
() => api.rpc.system.dryRun(this.toHex(), blockHash || optionsOrHash as string)
);
}
// eslint-disable-next-line @typescript-eslint/no-unsafe-return,@typescript-eslint/no-unsafe-call
return decorateMethod(
(): Observable<ApplyExtrinsicResult> =>
this.#observeSign(account, optionsOrHash).pipe(
switchMap(() => api.rpc.system.dryRun(this.toHex()))
)
)();
}
// calculate the payment info for this transaction (if signed and submitted)
public paymentInfo (account: AddressOrPair, optionsOrHash?: Partial<SignerOptions> | Uint8Array | string): SubmittablePaymentResult<ApiType> {
if (!this.hasPaymentInfo) {
throw new Error('The transactionPaymentApi.queryInfo runtime call is not available in your environment');
}
if (blockHash || isString(optionsOrHash) || isU8a(optionsOrHash)) {
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
return decorateMethod(
(): Observable<RuntimeDispatchInfo> =>
api.callAt(blockHash || optionsOrHash as string).pipe(
switchMap((callAt): Observable<RuntimeDispatchInfo> => {
const u8a = this.toU8a();
return callAt.transactionPaymentApi.queryInfo(u8a, u8a.length);
})
)
);
}
const [allOptions] = makeSignAndSendOptions(optionsOrHash);
const address = isKeyringPair(account) ? account.address : account.toString();
// eslint-disable-next-line @typescript-eslint/no-unsafe-return,@typescript-eslint/no-unsafe-call
return decorateMethod(
(): Observable<RuntimeDispatchInfo> =>
api.derive.tx.signingInfo(address, allOptions.nonce, allOptions.era).pipe(
first(),
switchMap((signingInfo): Observable<RuntimeDispatchInfo> => {
// setup our options (same way as in signAndSend)
const eraOptions = makeEraOptions(api, this.registry, allOptions, signingInfo);
const signOptions = makeSignOptions(api, eraOptions, {});
// 1. Don't use the internal objects inside the new tx (hence toU8a)
// 2. Don't override the data from existing signed extrinsics
// 3. Ensure that this object stays intact, with no new sign after operation
const u8a = api.tx(this.toU8a()).signFake(address, signOptions).toU8a();
return api.call.transactionPaymentApi.queryInfo(u8a, u8a.length);
})
)
)();
}
// send with an immediate Hash result
public send (): SubmittableResultResult<ApiType>;
// send with a status callback
public send (statusCb: Callback<ISubmittableResult>): SubmittableResultSubscription<ApiType>;
// send implementation for both immediate Hash and statusCb variants
public send (statusCb?: Callback<ISubmittableResult>): SubmittableResultResult<ApiType> | SubmittableResultSubscription<ApiType> {
const isSubscription = api.hasSubscriptions && (this.#ignoreStatusCb || !!statusCb);
const updatedInfo = extrinsicInfoMap.get(this);
extrinsicInfoMap.delete(this);
// eslint-disable-next-line @typescript-eslint/no-unsafe-return,@typescript-eslint/no-unsafe-call
return decorateMethod(
isSubscription
? () => this.#observeSubscribe(updatedInfo)
: () => this.#observeSend(updatedInfo)
)(statusCb);
}
/**
* @description Signs a transaction, returning `this` to allow chaining. E.g.: `signAsync(...).send()`. Like `.signAndSend` this will retrieve the nonce and blockHash to send the tx with.
*/
public signAsync (account: AddressOrPair, partialOptions?: Partial<SignerOptions>): PromiseOrObs<ApiType, this> {
// eslint-disable-next-line @typescript-eslint/no-unsafe-return,@typescript-eslint/no-unsafe-call
return decorateMethod(
(): Observable<this> =>
this.#observeSign(account, partialOptions).pipe(
map((info) => {
// If we got a full signed transaction from the signer, attach it
if (info.signedTransaction) {
const extrinsic = new Submittable(api.registry, info.signedTransaction);
extrinsicInfoMap.set(this, info);
return extrinsic as this;
}
// Fallback if signer didnt return signedTransaction
return this;
})
)
)();
}
// signAndSend with an immediate Hash result
public signAndSend (account: AddressOrPair, partialOptions?: Partial<SignerOptions>): SubmittableResultResult<ApiType>;
// signAndSend with a subscription, i.e. callback provided
public signAndSend (account: AddressOrPair, statusCb: Callback<ISubmittableResult>): SubmittableResultSubscription<ApiType>;
// signAndSend with options and a callback
public signAndSend (account: AddressOrPair, partialOptions: Partial<SignerOptions>, statusCb?: Callback<ISubmittableResult>): SubmittableResultSubscription<ApiType>;
// signAndSend implementation for all 3 cases above
public signAndSend (account: AddressOrPair, partialOptions?: Partial<SignerOptions> | Callback<ISubmittableResult>, optionalStatusCb?: Callback<ISubmittableResult>): SubmittableResultResult<ApiType> | SubmittableResultSubscription<ApiType> {
const [options, statusCb] = makeSignAndSendOptions(partialOptions, optionalStatusCb);
const isSubscription = api.hasSubscriptions && (this.#ignoreStatusCb || !!statusCb);
// eslint-disable-next-line @typescript-eslint/no-unsafe-return,@typescript-eslint/no-unsafe-call
return decorateMethod(
(): Observable<Codec> => (
this.#observeSign(account, options).pipe(
switchMap((info): Observable<ISubmittableResult> | Observable<Hash> =>
isSubscription
? this.#observeSubscribe(info)
: this.#observeSend(info)
)
) as Observable<Codec>) // FIXME This is wrong, SubmittableResult is _not_ a codec
)(statusCb);
}
// adds a transform to the result, applied before result is returned
withResultTransform (transform: (input: ISubmittableResult) => ISubmittableResult): this {
this.#transformResult = transform;
return this;
}
#observeSign = (account: AddressOrPair, partialOptions?: Partial<SignerOptions>): Observable<UpdateInfo> => {
const address = isKeyringPair(account) ? account.address : account.toString();
const options = optionsOrNonce(partialOptions);
return api.derive.tx.signingInfo(address, options.nonce, options.era).pipe(
first(),
mergeMap(async (signingInfo): Promise<UpdateInfo> => {
const eraOptions = makeEraOptions(api, this.registry, options, signingInfo);
let updateId = -1;
let signedTx = null;
if (isKeyringPair(account)) {
this.sign(account, eraOptions);
} else {
const result = await this.#signViaSigner(address, eraOptions, signingInfo.header);
updateId = result.id;
if (result.signedTransaction) {
signedTx = result.signedTransaction;
}
}
return { options: eraOptions, signedTransaction: signedTx, updateId };
})
);
};
#observeStatus = (txHash: Hash, status: ExtrinsicStatus): Observable<ISubmittableResult> => {
if (!status.isFinalized && !status.isInBlock) {
return of(this.#transformResult(new SubmittableResult({
status,
txHash
})));
}
const blockHash = status.isInBlock
? status.asInBlock
: status.asFinalized;
return api.derive.tx.events(blockHash).pipe(
map(({ block, events }): ISubmittableResult =>
this.#transformResult(new SubmittableResult({
...filterEvents(txHash, block, events, status),
status,
txHash
}))
),
catchError((internalError: Error) =>
of(this.#transformResult(new SubmittableResult({
internalError,
status,
txHash
})))
)
);
};
#observeSend = (info?: UpdateInfo): Observable<Hash> => {
return api.rpc.author.submitExtrinsic(info?.signedTransaction || this).pipe(
tap((hash): void => {
this.#updateSigner(hash, info);
})
);
};
#observeSubscribe = (info?: UpdateInfo): Observable<ISubmittableResult> => {
const txHash = this.hash;
return api.rpc.author.submitAndWatchExtrinsic(info?.signedTransaction || this).pipe(
switchMap((status): Observable<ISubmittableResult> =>
this.#observeStatus(txHash, status)
),
tap((status): void => {
this.#updateSigner(status, info);
})
);
};
#signViaSigner = async (address: Address | string | Uint8Array, options: SignatureOptions, header: Header | null): Promise<SignerInfo> => {
const signer = options.signer || api.signer;
const allowCallDataAlteration = options.allowCallDataAlteration ?? true;
if (!signer) {
throw new Error('No signer specified, either via api.setSigner or via sign options. You possibly need to pass through an explicit keypair for the origin so it can be used for signing.');
}
const payload = this.registry.createTypeUnsafe<SignerPayload>('SignerPayload', [objectSpread({}, options, {
address,
blockNumber: header ? header.number : 0,
method: this.method
})]);
let result: SignerResult;
if (isFunction(signer.signPayload)) {
result = await signer.signPayload(payload.toPayload());
if (result.signedTransaction && !options.withSignedTransaction) {
throw new Error('The `signedTransaction` field may not be submitted when `withSignedTransaction` is disabled');
}
if (result.signedTransaction && options.withSignedTransaction) {
const ext = this.registry.createTypeUnsafe<Extrinsic>('Extrinsic', [result.signedTransaction]);
const newSignerPayload = this.registry.createTypeUnsafe<SignerPayload>('SignerPayload', [objectSpread({}, {
address,
assetId: ext.assetId && ext.assetId.isSome ? ext.assetId.toHex() : null,
blockHash: payload.blockHash,
blockNumber: header ? header.number : 0,
era: ext.era.toHex(),
genesisHash: payload.genesisHash,
metadataHash: ext.metadataHash ? ext.metadataHash.toHex() : null,
method: ext.method.toHex(),
mode: ext.mode ? ext.mode.toHex() : null,
nonce: ext.nonce.toHex(),
runtimeVersion: payload.runtimeVersion,
signedExtensions: payload.signedExtensions,
tip: ext.tip ? ext.tip.toHex() : null,
version: payload.version
})]);
if (!ext.isSigned) {
throw new Error(`When using the signedTransaction field, the transaction must be signed. Recieved isSigned: ${ext.isSigned}`);
}
if (!allowCallDataAlteration) {
this.#validateSignedTransaction(payload, ext);
}
// This is only used for signAsync - signAndSend does not need to adjust the super payload or
// add the signature.
super.addSignature(address, result.signature, newSignerPayload.toPayload());
return { id: result.id, signedTransaction: result.signedTransaction };
}
} else if (isFunction(signer.signRaw)) {
result = await signer.signRaw(payload.toRaw());
} else {
throw new Error('Invalid signer interface, it should implement either signPayload or signRaw (or both)');
}
// Here we explicitly call `toPayload()` again instead of working with an object
// (reference) as passed to the signer. This means that we are sure that the
// payload data is not modified from our inputs, but the signer
super.addSignature(address, result.signature, payload.toPayload());
return { id: result.id };
};
#updateSigner = (status: Hash | ISubmittableResult, info?: UpdateInfo): void => {
if (info && (info.updateId !== -1)) {
const { options, updateId } = info;
const signer = options.signer || api.signer;
if (signer && isFunction(signer.update)) {
signer.update(updateId, status);
}
}
};
/**
* When a signer includes `signedTransaction` within the SignerResult this will validate
* specific fields within the signed extrinsic against the original payload that was passed
* to the signer.
*/
#validateSignedTransaction = (signerPayload: SignerPayload, signedExt: Extrinsic): void => {
const payload = signerPayload.toPayload();
const errMsg = (field: string) => `signAndSend: ${field} does not match the original payload`;
if (payload.method !== signedExt.method.toHex()) {
throw new Error(errMsg('call data'));
}
};
}
return Submittable;
}
@@ -0,0 +1,19 @@
// Copyright 2017-2025 @pezkuwi/api authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { Call, Extrinsic } from '@pezkuwi/types/interfaces';
import type { Registry } from '@pezkuwi/types-codec/types';
import type { ApiBase } from '../base/index.js';
import type { ApiInterfaceRx, ApiTypes } from '../types/index.js';
import type { SubmittableExtrinsic } from './types.js';
import { createClass } from './createClass.js';
type Creator<ApiType extends ApiTypes> = (extrinsic: Call | Uint8Array | string) => SubmittableExtrinsic<ApiType>;
export function createSubmittable<ApiType extends ApiTypes> (apiType: ApiTypes, api: ApiInterfaceRx, decorateMethod: ApiBase<ApiType>['_decorateMethod'], registry?: Registry, blockHash?: Uint8Array): Creator<ApiType> {
const Submittable = createClass<ApiType>({ api, apiType, blockHash, decorateMethod });
return (extrinsic: Call | Extrinsic | Uint8Array | string): SubmittableExtrinsic<ApiType> =>
new Submittable(registry || api.registry, extrinsic);
}
+5
View File
@@ -0,0 +1,5 @@
// Copyright 2017-2025 @pezkuwi/api authors & contributors
// SPDX-License-Identifier: Apache-2.0
export { createSubmittable } from './createSubmittable.js';
export { SubmittableResult } from './Result.js';
+4
View File
@@ -0,0 +1,4 @@
// Copyright 2017-2025 @pezkuwi/api authors & contributors
// SPDX-License-Identifier: Apache-2.0
export type { AddressOrPair, SignerOptions, SubmittableDryRunResult, SubmittableExtrinsic, SubmittablePaymentResult, SubmittableResultResult, SubmittableResultSubscription, SubmittableResultValue } from '@pezkuwi/api-base/types';
@@ -0,0 +1,53 @@
// Copyright 2017-2025 @pezkuwi/api authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { KeyringPair } from '@pezkuwi/keyring/types';
import type { Registry, SignerPayloadJSON, SignerPayloadRaw } from '@pezkuwi/types/types';
import type { Signer, SignerResult } from '../types/index.js';
import { hexToU8a, objectSpread, u8aToHex } from '@pezkuwi/util';
let id = 0;
export class SingleAccountSigner implements Signer {
readonly #keyringPair: KeyringPair;
readonly #registry: Registry;
readonly #signDelay: number;
constructor (registry: Registry, keyringPair: KeyringPair, signDelay = 0) {
this.#keyringPair = keyringPair;
this.#registry = registry;
this.#signDelay = signDelay;
}
public async signPayload (payload: SignerPayloadJSON): Promise<SignerResult> {
if (payload.address !== this.#keyringPair.address) {
throw new Error('Signer does not have the keyringPair');
}
return new Promise((resolve): void => {
setTimeout((): void => {
const signed = this.#registry.createType('ExtrinsicPayload', payload, { version: payload.version }).sign(this.#keyringPair);
resolve(objectSpread({ id: ++id }, signed));
}, this.#signDelay);
});
}
public async signRaw ({ address, data }: SignerPayloadRaw): Promise<SignerResult> {
if (address !== this.#keyringPair.address) {
throw new Error('Signer does not have the keyringPair');
}
return new Promise((resolve): void => {
setTimeout((): void => {
const signature = u8aToHex(this.#keyringPair.sign(hexToU8a(data)));
resolve({
id: ++id,
signature
});
}, this.#signDelay);
});
}
}
+5
View File
@@ -0,0 +1,5 @@
// Copyright 2017-2025 @pezkuwi/api authors & contributors
// SPDX-License-Identifier: Apache-2.0
export * from './logEvents.js';
export * from './SingleAccountSigner.js';
+24
View File
@@ -0,0 +1,24 @@
// Copyright 2017-2025 @pezkuwi/api authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { EventRecord } from '@pezkuwi/types/interfaces';
import type { SubmittableResult } from '../index.js';
// log all events for the transfers, calling done() when finalized
export const logEvents = (done: () => Record<string, unknown>): (r: SubmittableResult) => void =>
({ events, status }: SubmittableResult): void => {
console.log('Transaction status:', status.type);
if (status.isInBlock) {
console.log('Completed at block hash', status.value.toHex());
console.log('Events:');
events.forEach(({ event: { data, method, section }, phase }: EventRecord): void => {
console.log('\t', phase.toString(), `: ${section}.${method}`, data.toString());
});
if (events.length) {
done();
}
}
};
+4
View File
@@ -0,0 +1,4 @@
// Copyright 2017-2025 @pezkuwi/api authors & contributors
// SPDX-License-Identifier: Apache-2.0
export * from '@pezkuwi/api-base/types/calls';
+4
View File
@@ -0,0 +1,4 @@
// Copyright 2017-2025 @pezkuwi/api authors & contributors
// SPDX-License-Identifier: Apache-2.0
export * from '@pezkuwi/api-base/types/consts';
+4
View File
@@ -0,0 +1,4 @@
// Copyright 2017-2025 @pezkuwi/api authors & contributors
// SPDX-License-Identifier: Apache-2.0
export * from '@pezkuwi/api-base/types/errors';
+4
View File
@@ -0,0 +1,4 @@
// Copyright 2017-2025 @pezkuwi/api authors & contributors
// SPDX-License-Identifier: Apache-2.0
export * from '@pezkuwi/api-base/types/events';
+137
View File
@@ -0,0 +1,137 @@
// Copyright 2017-2025 @pezkuwi/api authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { ApiTypes, DeriveCustom, QueryableStorageMulti } from '@pezkuwi/api-base/types';
import type { ApiInterfaceRx as ApiInterfaceBase } from '@pezkuwi/api-base/types/api';
import type { QueryableCalls } from '@pezkuwi/api-base/types/calls';
import type { QueryableConsts } from '@pezkuwi/api-base/types/consts';
import type { DecoratedErrors } from '@pezkuwi/api-base/types/errors';
import type { DecoratedEvents } from '@pezkuwi/api-base/types/events';
import type { QueryableStorage } from '@pezkuwi/api-base/types/storage';
import type { ProviderInterface, ProviderInterfaceEmitted } from '@pezkuwi/rpc-provider/types';
import type { ExtDef } from '@pezkuwi/types/extrinsic/signedExtensions/types';
import type { Call, Extrinsic, Hash, RuntimeVersionPartial } from '@pezkuwi/types/interfaces';
import type { CallFunction, DefinitionRpc, DefinitionRpcSub, DefinitionsCall, RegisteredTypes, Registry, RegistryError, SignatureOptions, Signer } from '@pezkuwi/types/types';
import type { BN } from '@pezkuwi/util';
import type { HexString } from '@pezkuwi/util/types';
import type { ApiBase } from '../base/index.js';
import type { SubmittableExtrinsic } from '../types/submittable.js';
import type { AllDerives } from '../util/decorate.js';
// types
export type { Signer, SignerResult } from '@pezkuwi/types/types';
// all named
export { ApiBase } from '../base/index.js';
// all starred
// eslint-disable-next-line import/export
export * from '@pezkuwi/api/types/calls';
// eslint-disable-next-line import/export
export * from '@pezkuwi/api/types/consts';
// eslint-disable-next-line import/export
export * from '@pezkuwi/api/types/errors';
// eslint-disable-next-line import/export
export * from '@pezkuwi/api/types/events';
// eslint-disable-next-line import/export
export * from '@pezkuwi/api/types/storage';
// eslint-disable-next-line import/export
export * from '@pezkuwi/api/types/submittable';
// eslint-disable-next-line import/export
export * from '@pezkuwi/api-base/types';
// A smaller interface of ApiRx, used in derive and in SubmittableExtrinsic
export interface ApiInterfaceRx extends ApiInterfaceBase {
derive: AllDerives<'rxjs'>;
}
export interface ApiOptions extends RegisteredTypes {
/**
* @description Add custom derives to be injected
*/
derives?: DeriveCustom;
/**
* @description Control the initialization of the wasm libraries. When not specified, it defaults to `true`, initializing the wasm libraries, set to `false` to not initialize wasm. (No sr25519 support)
*/
initWasm?: boolean;
/**
* @description Controls the checking of storage values once they have been contructed. When not specified this defaults to `true`. Set to `false` to forgo any checking on storage results.
*/
isPedantic?: boolean;
/**
* @description pre-bundles is a map of 'genesis hash and runtime spec version' as key to a metadata hex string
* if genesis hash and runtime spec version matches, then use metadata, else fetch it from chain
*/
metadata?: Record<string, HexString>;
/**
* @description Don't display any warnings on initialization (missing RPC methods & runtime calls)
*/
noInitWarn?: boolean;
/**
* @description Transport Provider from rpc-provider. If not specified, it will default to
* connecting to a WsProvider connecting localhost with the default port, i.e. `ws://127.0.0.1:9944`
*/
provider?: ProviderInterface;
/**
* @description A type registry to use along with this instance
*/
registry?: Registry;
/**
* @description User-defined RPC methods
*/
rpc?: Record<string, Record<string, DefinitionRpc | DefinitionRpcSub>>;
/**
* @description Defines the size of the cache for the rpc-core. Defaults to 1024 * 10 * 10.
*/
rpcCacheCapacity?: number;
/**
* @description Overrides for state_call usage (this will be removed in some future version)
*/
runtime?: DefinitionsCall;
/**
* @description Any chain-specific signed extensions that are now well-known
*/
signedExtensions?: ExtDef;
/**
* @description An external signer which will be used to sign extrinsic when account passed in is not KeyringPair
*/
signer?: Signer;
/**
* @description The source object to use for runtime information (only used when cloning)
*/
source?: ApiBase<any>;
/**
* @description Throws an error when the initial connection fails (same as isReadyOrError)
*/
throwOnConnect?: boolean;
/**
* @description Throws an error when some types are unknown (useful with throwOnConnect)
*/
throwOnUnknown?: boolean;
}
export type ApiInterfaceEvents = ProviderInterfaceEmitted | 'ready' | 'decorated';
export interface SignerOptions extends SignatureOptions {
blockNumber: BN;
genesisHash: Hash;
}
export interface ApiDecoration<ApiType extends ApiTypes> {
call: QueryableCalls<ApiType>;
consts: QueryableConsts<ApiType>;
errors: DecoratedErrors<ApiType>;
events: DecoratedEvents<ApiType>;
query: QueryableStorage<ApiType>;
registry: Registry;
runtimeVersion: RuntimeVersionPartial;
rx: {
call: QueryableCalls<'rxjs'>;
query: QueryableStorage<'rxjs'>;
};
tx: (extrinsic: Call | Extrinsic | Uint8Array | string) => SubmittableExtrinsic<ApiType>;
findCall (callIndex: Uint8Array | string): CallFunction;
findError (errorIndex: Uint8Array | string): RegistryError;
queryMulti: QueryableStorageMulti<ApiType>;
}
+4
View File
@@ -0,0 +1,4 @@
// Copyright 2017-2025 @pezkuwi/api-augment authors & contributors
// SPDX-License-Identifier: Apache-2.0
export * from '@pezkuwi/api-base/types/storage';
+4
View File
@@ -0,0 +1,4 @@
// Copyright 2017-2025 @pezkuwi/api-augment authors & contributors
// SPDX-License-Identifier: Apache-2.0
export * from '@pezkuwi/api-base/types/submittable';
@@ -0,0 +1,54 @@
// Copyright 2017-2025 @pezkuwi/api authors & contributors
// SPDX-License-Identifier: Apache-2.0
/// <reference types="@pezkuwi/dev-test/globals.d.ts" />
import { augmentObject } from './augmentObject.js';
describe('augmentObject', (): void => {
let spy: ReturnType<typeof jest.spyOn>;
beforeEach((): void => {
spy = jest.spyOn(console, 'warn');
});
afterEach((): void => {
spy.mockRestore();
});
it('logs added/removed sections and methods', (): void => {
augmentObject(
'test',
{ bar: { b: 1 }, foo: { d: 1, f: 1 }, new: { z: 1 } },
{ bar: { a: 1, c: 1 }, baz: { a: 1 }, foo: { c: 1, f: 1 } }
);
expect(spy).toHaveBeenCalledWith(
expect.anything(),
expect.stringMatching(/API\/AUGMENT/),
'api.test: Found 1 added and 1 removed modules:\n\t added: new\n\tremoved: baz'
);
expect(spy).toHaveBeenCalledWith(
expect.anything(),
expect.stringMatching(/API\/AUGMENT/),
'api.test: Found 2 added and 3 removed calls:\n\t added: bar.b, foo.d\n\tremoved: bar.a, bar.c, foo.c'
);
});
it('copies sections to the dest', (): void => {
const src = { bar: { b: 1 }, foo: { a: 1 } };
expect(augmentObject('test', src, {})).toEqual(src);
});
it('adds fields to existing sections', (): void => {
const src = { bar: { b: 1 }, foo: { a: 1 } };
const dst = { baz: { c: 1 }, foo: { b: 2 } };
expect(augmentObject('test', src, dst)).toEqual({
bar: { b: 1 },
baz: { c: 1 },
foo: { a: 1, b: 2 }
});
});
});
+112
View File
@@ -0,0 +1,112 @@
// Copyright 2017-2025 @pezkuwi/api authors & contributors
// SPDX-License-Identifier: Apache-2.0
import { lazyMethods, logger, objectClear } from '@pezkuwi/util';
type Sections <T> = Record<string, Methods<T>>;
type Methods <T> = Record<string, T>;
type StringsStrings = [string[], string[]];
const l = logger('api/augment');
function logLength (type: 'added' | 'removed', values: string[], and: string[] = []): string {
return values.length
? ` ${values.length} ${type}${and.length ? ' and' : ''}`
: '';
}
function logValues (type: 'added' | 'removed', values: string[]): string {
return values.length
? `\n\t${type.padStart(7)}: ${values.sort().join(', ')}`
: '';
}
// log details to console
function warn (prefix: string, type: 'calls' | 'modules', [added, removed]: StringsStrings): void {
if (added.length || removed.length) {
l.warn(`api.${prefix}: Found${logLength('added', added, removed)}${logLength('removed', removed)} ${type}:${logValues('added', added)}${logValues('removed', removed)}`);
}
}
function findSectionExcludes (a: string[], b: string[]): string[] {
return a.filter((s) => !b.includes(s));
}
function findSectionIncludes (a: string[], b: string[]): string[] {
return a.filter((s) => b.includes(s));
}
function extractSections <T> (src: Sections<T>, dst: Sections<T>): StringsStrings {
const srcSections = Object.keys(src);
const dstSections = Object.keys(dst);
return [
findSectionExcludes(srcSections, dstSections),
findSectionExcludes(dstSections, srcSections)
];
}
function findMethodExcludes <T> (src: Sections<T>, dst: Sections<T>): string[] {
const srcSections = Object.keys(src);
const dstSections = findSectionIncludes(Object.keys(dst), srcSections);
const excludes: string[] = [];
for (let s = 0, scount = dstSections.length; s < scount; s++) {
const section = dstSections[s];
const srcMethods = Object.keys(src[section]);
const dstMethods = Object.keys(dst[section]);
for (let d = 0, mcount = dstMethods.length; d < mcount; d++) {
const method = dstMethods[d];
if (!srcMethods.includes(method)) {
excludes.push(`${section}.${method}`);
}
}
}
return excludes;
}
function extractMethods <T> (src: Sections<T>, dst: Sections<T>): StringsStrings {
return [
findMethodExcludes(dst, src),
findMethodExcludes(src, dst)
];
}
/**
* @description Takes a decorated api section (e.g. api.tx) and augment it with the details. It does not override what is
* already available, but rather just adds new missing items into the result object.
* @internal
*/
export function augmentObject <T> (prefix: string | null, src: Sections<T>, dst: Sections<T>, fromEmpty = false): Sections<T> {
fromEmpty && objectClear(dst);
// NOTE: This part is slightly problematic since it will get the
// values for at least all the sections and the names of the methods
// (Since methods won't be decorated before lazy, this _may_ be ok)
if (prefix && Object.keys(dst).length) {
warn(prefix, 'modules', extractSections(src, dst));
warn(prefix, 'calls', extractMethods(src, dst));
}
const sections = Object.keys(src);
for (let i = 0, count = sections.length; i < count; i++) {
const section = sections[i];
const methods = src[section];
// We don't set here with a lazy interface, we decorate based
// on the top-level structure (this bypasses adding lazy onto lazy)
if (!dst[section]) {
dst[section] = {};
}
lazyMethods(dst[section], Object.keys(methods), (m: string) => methods[m]);
}
return dst;
}
+43
View File
@@ -0,0 +1,43 @@
// Copyright 2017-2025 @pezkuwi/api authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { ExactDerive } from '@pezkuwi/api-derive';
import type { AnyFunction } from '@pezkuwi/types/types';
import type { ApiTypes, DecorateMethod, MethodResult } from '../types/index.js';
import { lazyDeriveSection } from '@pezkuwi/api-derive';
type AnyDeriveSection = Record<string, AnyFunction>;
// Most generic typings for `api.derive.*.*`
type AnyDerive = Record<string, AnyDeriveSection>;
// Exact typings for a particular section `api.derive.section.*`
type DeriveSection<ApiType extends ApiTypes, Section extends AnyDeriveSection> = {
[M in keyof Section]: MethodResult<ApiType, Section[M]>
};
// Exact typings for all sections `api.derive.*.*`
export type AllDerives<ApiType extends ApiTypes> = {
[S in keyof ExactDerive]: DeriveSection<ApiType, ExactDerive[S]>
};
/**
* This is a section decorator which keeps all type information.
*/
export function decorateDeriveSections<ApiType extends ApiTypes> (decorateMethod: DecorateMethod<ApiType>, derives: AnyDerive): AllDerives<ApiType> {
const getKeys = (s: string) =>
Object.keys(derives[s]);
const creator = (s: string, m: string) =>
decorateMethod(derives[s][m]) as AnyFunction;
const result: AnyDerive = {};
const names = Object.keys(derives);
for (let i = 0, count = names.length; i < count; i++) {
lazyDeriveSection(result, names[i], getKeys, creator);
}
return result as AllDerives<ApiType>;
}
+34
View File
@@ -0,0 +1,34 @@
// Copyright 2017-2025 @pezkuwi/api authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { BlockNumber, EventRecord, ExtrinsicStatus, H256, SignedBlock } from '@pezkuwi/types/interfaces';
import { isCompact } from '@pezkuwi/util';
import { l } from './logging.js';
export function filterEvents (txHash: H256, { block: { extrinsics, header } }: SignedBlock, allEvents: EventRecord[], status: ExtrinsicStatus): { events?: EventRecord[], txIndex?: number, blockNumber?: BlockNumber } {
// extrinsics to hashes
for (const [txIndex, x] of extrinsics.entries()) {
if (x.hash.eq(txHash)) {
return {
blockNumber: isCompact<BlockNumber>(header.number) ? header.number.unwrap() : header.number,
events: allEvents.filter(({ phase }) =>
phase.isApplyExtrinsic &&
phase.asApplyExtrinsic.eqn(txIndex)
),
txIndex
};
}
}
// if we do get the block after finalized, it _should_ be there
// only warn on filtering with isInBlock (finalization finalizes after)
if (status.isInBlock) {
const allHashes = extrinsics.map((x) => x.hash.toHex());
l.warn(`block ${header.hash.toHex()}: Unable to find extrinsic ${txHash.toHex()} inside ${allHashes.join(', ')}`);
}
return {};
}
+10
View File
@@ -0,0 +1,10 @@
// Copyright 2017-2025 @pezkuwi/api authors & contributors
// SPDX-License-Identifier: Apache-2.0
// all named
export { filterEvents } from './filterEvents.js';
export { isKeyringPair } from './isKeyringPair.js';
export { l } from './logging.js';
// all starred
export * from './decorate.js';
+11
View File
@@ -0,0 +1,11 @@
// Copyright 2017-2025 @pezkuwi/api authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { AccountId, Address } from '@pezkuwi/types/interfaces';
import type { IKeyringPair } from '@pezkuwi/types/types';
import { isFunction } from '@pezkuwi/util';
export function isKeyringPair (account: string | IKeyringPair | AccountId | Address): account is IKeyringPair {
return isFunction((account as IKeyringPair).sign);
}
+6
View File
@@ -0,0 +1,6 @@
// Copyright 2017-2025 @pezkuwi/api authors & contributors
// SPDX-License-Identifier: Apache-2.0
import { logger } from '@pezkuwi/util';
export const l = /*#__PURE__*/ logger('api/util');
+72
View File
@@ -0,0 +1,72 @@
// Copyright 2017-2025 @pezkuwi/api authors & contributors
// SPDX-License-Identifier: Apache-2.0
/// <reference types="@pezkuwi/dev-test/globals.d.ts" />
import { decorateStorage, Metadata, TypeRegistry } from '@pezkuwi/types';
import metaStatic from '@pezkuwi/types-support/metadata/static-bizinikiwi';
import { extractStorageArgs } from './validate.js';
describe('extractStorageArgs', (): void => {
const registry = new TypeRegistry();
const metadata = new Metadata(registry, metaStatic);
registry.setMetadata(metadata);
const storage = decorateStorage(registry, metadata.asLatest, metadata.version);
it('validates no-arg plain', (): void => {
expect(
extractStorageArgs(registry, storage['timestamp']['now'], [])
).toEqual([storage['timestamp']['now'], []]);
});
it('validates no-arg plain (with undefined, undefined)', (): void => {
expect(
extractStorageArgs(registry, storage['timestamp']['now'], [undefined, undefined])
).toEqual([storage['timestamp']['now'], []]);
});
it('validates no-arg plain (failing when there are args)', (): void => {
expect(
() => extractStorageArgs(registry, storage['timestamp']['now'], [123, 456])
).toThrow('timestamp.now() does not take any arguments, 2 found');
});
it('validates map, 1 arg', (): void => {
expect(
extractStorageArgs(registry, storage['staking']['payee'], ['abc'])
).toEqual([storage['staking']['payee'], ['abc']]);
});
it('validates map, 1 arg (failing with no args)', (): void => {
expect(
() => extractStorageArgs(registry, storage['staking']['payee'], [])
).toThrow('staking.payee(AccountId32) is a map, requiring 1 arguments, 0 found');
});
it('validates map, 1 arg (failing with more args)', (): void => {
expect(
() => extractStorageArgs(registry, storage['staking']['payee'], ['abc', 'def'])
).toThrow('staking.payee(AccountId32) is a map, requiring 1 arguments, 2 found');
});
it('validates doublemap, 2 args', (): void => {
expect(
extractStorageArgs(registry, storage['staking']['erasStakers'], [1, '0x1234'])
).toEqual([storage['staking']['erasStakers'], [1, '0x1234']]);
});
it('validates doublemap, 2 args (failing with no args)', (): void => {
expect(
() => extractStorageArgs(registry, storage['staking']['erasStakers'], [])
).toThrow('staking.erasStakers(u32, AccountId32) is a map, requiring 2 arguments, 0 found');
});
it('validates doublemap, 2 args (failing with 1 arg)', (): void => {
expect(
() => extractStorageArgs(registry, storage['staking']['erasStakers'], [123])
).toThrow('staking.erasStakers(u32, AccountId32) is a map, requiring 2 arguments, 1 found');
});
});
+36
View File
@@ -0,0 +1,36 @@
// Copyright 2017-2025 @pezkuwi/api authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { SiLookupTypeId } from '@pezkuwi/types/interfaces';
import type { StorageEntry } from '@pezkuwi/types/primitive/types';
import type { Registry } from '@pezkuwi/types/types';
import { isUndefined } from '@pezkuwi/util';
function sig ({ lookup }: Registry, { method, section }: StorageEntry, args: SiLookupTypeId[]): string {
return `${section}.${method}(${args.map((a) => lookup.getTypeDef(a).type).join(', ')})`;
}
// sets up the arguments in the form of [creator, args] ready to be used in a storage
// call. Additionally, it verifies that the correct number of arguments have been passed
export function extractStorageArgs (registry: Registry, creator: StorageEntry, _args: unknown[]): [StorageEntry, unknown[]] {
const args = _args.filter((a) => !isUndefined(a));
if (creator.meta.type.isPlain) {
if (args.length !== 0) {
throw new Error(`${sig(registry, creator, [])} does not take any arguments, ${args.length} found`);
}
} else {
const { hashers, key } = creator.meta.type.asMap;
const keys = hashers.length === 1
? [key]
: registry.lookup.getSiType(key).def.asTuple.map((t) => t);
if (args.length !== keys.length) {
throw new Error(`${sig(registry, creator, keys)} is a map, requiring ${keys.length} arguments, ${args.length} found`);
}
}
// pass as tuple
return [creator, args];
}