mirror of
https://github.com/pezkuwichain/pezkuwi-api.git
synced 2026-04-26 04:57:54 +00:00
Rebrand: polkadot → pezkuwi, substrate → bizinikiwi, kusama → dicle
This commit is contained in:
@@ -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();
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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';
|
||||
@@ -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'>;
|
||||
Reference in New Issue
Block a user