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
+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'>;