import { ApiPromise, WsProvider, Keyring } from '@polkadot/api';
import { HttpProvider } from '@polkadot/rpc-provider';
import { cryptoWaitReady } from '@polkadot/util-crypto';
import typesBundle from '@docknetwork/node-types';
import { KeyringPair } from "@polkadot/keyring/types"; // eslint-disable-line
import AnchorModule from './modules/anchor';
import BlobModule from './modules/blob';
import { DIDModule } from './modules/did';
import RevocationModule from './modules/revocation';
import TokenMigration from './modules/migration';
import StatusListCredentialModule from './modules/status-list-credential';
import BBSModule from './modules/bbs';
import BBSPlusModule from './modules/bbs-plus';
import LegacyBBSPlusModule from './modules/legacy-bbs-plus';
import PSModule from './modules/ps';
import OffchainSignaturesModule from './modules/offchain-signatures';
import AccumulatorModule from './modules/accumulator';
import PoaRpcDefs from './rpc-defs/poa-rpc-defs';
import PriceFeedRpcDefs from './rpc-defs/price-feed-rpc-defs';
import CoreModsRpcDefs from './rpc-defs/core-mods-rpc-defs';
import TrustRegistryModule from './modules/trust-registry';
import {
sendWithRetries,
patchQueryApi,
STANDARD_BLOCK_TIME_MS,
FASTBLOCK_TIME_MS,
FASTBLOCK_CONFIG,
STANDARD_CONFIG,
} from './dock-api-retry';
import { ensureExtrinsicSucceeded } from './utils/extrinsic';
/**
* @typedef {object} Options The Options to use in the function DockAPI.
* @property {string} [address] The node address to connect to.
* @property {object} [keyring] PolkadotJS keyring
* @property {object} [chainTypes] Types for the chain
* @property {object} [chainRpc] RPC definitions for the chain
* @property {Boolean} [loadPoaModules] Whether to load PoA modules or not. Defaults to true
*/
/** Helper class to interact with the Dock chain */
export default class DockAPI {
/**
* Creates a new instance of the DockAPI object, call init to initialize
* @param {function} [customSignTx] - Optional custom transaction sign method,
* a function that expects `extrinsic` as first argument and a dock api instance as second argument
* @constructor
*/
constructor(customSignTx) {
this.customSignTx = customSignTx;
this.anchorModule = new AnchorModule();
}
/**
* Initializes the SDK and connects to the node
* @param {Options} config - Configuration options
* @return {Promise} Promise for when SDK is ready for use
*/
/* eslint-disable sonarjs/cognitive-complexity */
async init(
{
address, keyring, chainTypes, chainRpc, loadPoaModules = true,
} = {
address: null,
keyring: null,
},
) {
if (this.api) {
if (this.api.isConnected) {
throw new Error('API is already connected');
} else {
await this.disconnect();
}
}
this.address = address || this.address;
const addressArray = Array.isArray(this.address) ? this.address : [this.address];
addressArray.forEach((addr) => {
if (
typeof addr === 'string'
&& addr.indexOf('wss://') === -1
&& addr.indexOf('https://') === -1
) {
console.warn(`WARNING: Using non-secure endpoint: ${addr}`);
}
});
// If RPC methods given, use them else set it to empty object.
let rpc = chainRpc || {};
// Initialize price feed rpc
rpc = Object.assign(rpc, PriceFeedRpcDefs);
// Initialize the RPC for core modules
rpc = Object.assign(rpc, CoreModsRpcDefs);
// If using PoA module, extend the RPC methods with PoA specific ones.
if (loadPoaModules) {
rpc = Object.assign(rpc, PoaRpcDefs);
}
// NOTE: The correct way to handle would be to raise error if a mix of URL types is provided or accept a preference of websocket vs http.
const addressStr = addressArray[0];
const isWebsocket = addressStr && addressStr.indexOf('http') === -1;
if (!isWebsocket && addressArray.length > 1) {
console.warn('WARNING: HTTP connections do not support more than one URL, ignoring rest');
}
const provider = isWebsocket
? new WsProvider(addressArray)
: new HttpProvider(addressStr);
const apiOptions = {
provider,
// @ts-ignore: TS2322
rpc,
};
if (chainTypes) {
apiOptions.types = chainTypes;
} else {
apiOptions.typesBundle = typesBundle;
}
this.api = await ApiPromise.create(apiOptions);
const runtimeVersion = await this.api.rpc.state.getRuntimeVersion();
const specVersion = runtimeVersion.specVersion.toNumber();
if (specVersion < 50) {
apiOptions.types = {
...(apiOptions.types || {}),
DidOrDidMethodKey: 'Did',
};
this.api = await ApiPromise.create(apiOptions);
}
this.api.specVersion = specVersion;
const blockTime = this.api.consts.babe.expectedBlockTime.toNumber();
if (
blockTime !== STANDARD_BLOCK_TIME_MS
&& blockTime !== FASTBLOCK_TIME_MS
) {
throw new Error(
`Unexpected block time: ${blockTime}, expected either ${STANDARD_BLOCK_TIME_MS} or ${FASTBLOCK_TIME_MS}`,
);
}
this.api.isFastBlock = blockTime === FASTBLOCK_TIME_MS;
await this.initKeyring(keyring);
patchQueryApi(this.api.query);
patchQueryApi(this.api.queryMulti);
this.anchorModule.setApi(this.api, this.signAndSend.bind(this));
this.blobModule = new BlobModule(this.api, this.signAndSend.bind(this));
this.didModule = new DIDModule(this.api, this.signAndSend.bind(this));
this.revocationModule = new RevocationModule(
this.api,
this.signAndSend.bind(this),
);
this.trustRegistryModule = new TrustRegistryModule(
this.api,
this.signAndSend.bind(this),
);
this.statusListCredentialModule = new StatusListCredentialModule(
this.api,
this.signAndSend.bind(this),
);
this.legacyBBSPlus = this.api.tx.offchainSignatures == null;
if (this.legacyBBSPlus) {
this.bbsPlusModule = new LegacyBBSPlusModule(
this.api,
this.signAndSend.bind(this),
);
} else {
this.offchainSignaturesModule = new OffchainSignaturesModule(
this.api,
this.signAndSend.bind(this),
);
this.bbsModule = new BBSModule(this.api, this.signAndSend.bind(this));
this.bbsPlusModule = new BBSPlusModule(
this.api,
this.signAndSend.bind(this),
);
this.psModule = new PSModule(this.api, this.signAndSend.bind(this));
}
this.accumulatorModule = new AccumulatorModule(
this.api,
this.signAndSend.bind(this),
);
if (loadPoaModules) {
this.migrationModule = new TokenMigration(this.api);
}
return this.api;
}
/* eslint-enable sonarjs/cognitive-complexity */
async initKeyring(keyring = null) {
if (!this.keyring || keyring) {
await cryptoWaitReady();
this.keyring = new Keyring(keyring || { type: 'sr25519' });
}
}
async disconnect() {
if (this.api) {
if (this.api.isConnected) {
await this.api.disconnect();
}
delete this.api;
delete this.blobModule;
delete this.didModule;
delete this.revocationModule;
delete this.offchainSignaturesModule;
delete this.bbsModule;
delete this.bbsPlusModule;
delete this.psModule;
delete this.accumulatorModule;
delete this.migrationModule;
delete this.legacyBBSPlus;
delete this.statusListCredentialModule;
delete this.trustRegistryModule;
}
}
isInitialized() {
return !!this.api;
}
/** TODO: Should probably use set/get and rename account to _account
* Sets the account used to sign transactions
* @param {KeyringPair} account - PolkadotJS Keyring account
*/
setAccount(account) {
this.account = account;
}
/**
* Gets the current account used to sign transactions
* @return {KeyringPair} PolkadotJS Keyring account
*/
getAccount() {
return this.account;
}
/**
* Signs an extrinsic with either the set account or a custom sign method (see constructor)
* @param {object} extrinsic - Extrinsic to send
* @param {object} params - An object used to parameters like nonce, etc to the extrinsic
* @return {Promise}
*/
async signExtrinsic(extrinsic, params = {}) {
if (this.customSignTx) {
return this.customSignTx(extrinsic, params, this);
}
return extrinsic.signAsync(this.getAccount(), params);
}
/**
* Helper function to sign and send transaction
* @param {object} extrinsic - Extrinsic to sign and send
* @param {Boolean} waitForFinalization - If true, waits for extrinsic's block to be finalized,
* else only wait to be included in block.
* @param {object} params - An object used to parameters like nonce, etc to the extrinsic
* @return {Promise}
*/
async signAndSend(extrinsic, waitForFinalization = true, params = {}) {
const signedExtrinsic = await this.signExtrinsic(extrinsic, params);
return await this.send(signedExtrinsic, waitForFinalization);
}
/**
* Helper function to send with retries a transaction that has already been signed.
* @param extrinsic - Extrinsic to send
* @param waitForFinalization - If true, waits for extrinsic's block to be finalized,
* else only wait to be included in the block.
* @returns {Promise<SubmittableResult>}
*/
async send(extrinsic, waitForFinalization = true) {
return await sendWithRetries(
this,
extrinsic,
waitForFinalization,
this.api.isFastBlock ? FASTBLOCK_CONFIG : STANDARD_CONFIG,
);
}
/**
* Helper function to send without retrying a transaction that has already been signed.
* @param {DockAPI} dock
* @param {*} extrinsic - Extrinsic to send
* @param {boolean} waitForFinalization - If true, waits for extrinsic's block to be finalized,
* else only wait to be included in the block.
* @returns {Promise<SubmittableResult>}
*/
sendNoRetries(extrinsic, waitForFinalization) {
let unsubscribe = null;
let unsubscribed = false;
const promise = new Promise((resolve, reject) => extrinsic
.send((extrResult) => {
const { events = [], status } = extrResult;
ensureExtrinsicSucceeded(this.api, events, status);
// If waiting for finalization or if not waiting for finalization, wait for inclusion in the block.
if (
(waitForFinalization && status.isFinalized)
|| (!waitForFinalization && status.isInBlock)
) {
resolve(extrResult);
}
})
.then((unsub) => {
if (typeof unsub === 'function') {
// `unsubscribed=true` here means that we unsubscribed from this function even before we had a callback set.
// Thus we just call this function to unsubscribe.
if (unsubscribed) {
unsub();
} else {
unsubscribe = unsub;
}
}
})
.catch(reject)).finally(() => void promise.unsubscribe());
promise.unsubscribe = () => {
if (unsubscribed) {
return false;
}
if (unsubscribe != null) {
try {
unsubscribe();
unsubscribed = true;
} catch (err) {
throw new Error(
`Failed to unsubscribe from watching extrinsic's status: \`${err}\``,
);
}
}
return true;
};
return promise;
}
/**
* Checks if the API instance is connected to the node
* @return {Boolean} The connection status
*/
get isConnected() {
if (!this.api) {
return false;
}
return this.api.isConnected;
}
/**
* Gets the SDK's Anchor module
* @return {AnchorModule} The module to use
*/
get anchor() {
return this.anchorModule;
}
/**
* Gets the SDK's Blob module
* @return {BlobModule} The module to use
*/
get blob() {
if (!this.blobModule) {
throw new Error('Unable to get Blob module, SDK is not initialised');
}
return this.blobModule;
}
/**
* Gets the SDK's DID module
* @return {DIDModule} The module to use
*/
get did() {
if (!this.didModule) {
throw new Error('Unable to get DID module, SDK is not initialised');
}
return this.didModule;
}
/**
* Gets the SDK's StatusListCredentialModule module
* @return {StatusListCredentialModule} The module to use
*/
get statusListCredential() {
if (!this.statusListCredentialModule) {
throw new Error(
'Unable to get StatusListCredentialModule module, SDK is not initialised',
);
}
return this.statusListCredentialModule;
}
/**
* Gets the SDK's TrustRegistryModule module
* @return {TrustRegistryModule} The module to use
*/
get trustRegistry() {
if (!this.trustRegistryModule) {
throw new Error(
'Unable to get TrustRegistryModule module, SDK is not initialised',
);
}
return this.trustRegistryModule;
}
/**
* Gets the SDK's OffchainSignaturesModule module
* @return {OffchainSignaturesModule} The module to use
*/
get offchainSignatures() {
if (!this.didModule) {
throw new Error(
'Unable to get OffchainSignatures module, SDK is not initialised',
);
}
return this.offchainSignaturesModule;
}
/**
* Gets the SDK's revocation module
* @return {RevocationModule} The module to use
*/
get revocation() {
if (!this.revocationModule) {
throw new Error(
'Unable to get revocation module, SDK is not initialised',
);
}
return this.revocationModule;
}
/**
* Gets the SDK's `BBS` module
* @return {BBSModule} The module to use
*/
get bbs() {
if (this.legacyBBSPlus) {
throw new Error("BBS isn't supported by the chain");
}
if (!this.bbsModule) {
throw new Error('Unable to get BBS module, SDK is not initialised');
}
return this.bbsModule;
}
/**
* Gets the SDK's `BBSPlus` module
* @return {BBSPlusModule} The module to use
*/
get bbsPlus() {
if (!this.bbsPlusModule) {
throw new Error('Unable to get BBS+ module, SDK is not initialised');
}
return this.bbsPlusModule;
}
/**
* Gets the SDK's `PS` module
* @return {PSModule} The module to use
*/
get ps() {
if (this.legacyBBSPlus) {
throw new Error("PS isn't supported by the chain");
}
if (!this.psModule) {
throw new Error('Unable to get PS module, SDK is not initialised');
}
return this.psModule;
}
/**
* Gets the SDK's `Accumulator` module
* @return {AccumulatorModule} The module to use
*/
get accumulator() {
if (!this.accumulatorModule) {
throw new Error(
'Unable to get Accumulator module, SDK is not initialised',
);
}
return this.bbsPlusModule;
}
}