@gear-js/api
Version:
A JavaScript library that provides functionality to connect GEAR Component APIs.
253 lines (249 loc) • 9.72 kB
JavaScript
;
require('@polkadot/util');
require('./utils/generate.js');
require('@polkadot/types');
var address = require('./utils/address.js');
require('@polkadot/util-crypto');
require('assert');
require('./default/index.js');
require('./metadata/programMetadata.js');
var replyCode = require('./utils/reply-code.js');
class Program {
_id;
_api;
_account;
_signerOptions;
_isInitialized = false;
waitForInitialization;
_events;
_storageUnsub;
constructor(_id, _api, _account, _signerOptions) {
this._id = _id;
this._api = _api;
this._account = _account;
this._signerOptions = _signerOptions;
this.waitForInitialization = this._init();
this._events = new EventTarget();
}
/**
* ## Creates a new instance of the Program class and initializes it.
* @param programId - The program ID.
* @param api - The GearApi instance.
* @param account - (optional) The account or address to be used for transactions.
* @param signerOptions - (optional) Signer options for transactions.
* @returns An initialized Program instance.
*/
static async new(programId, api, account, signerOptions) {
const program = new Program(programId, api, account, signerOptions);
await program.waitForInitialization;
return program;
}
throwOnAccountNotSet() {
if (!this._account) {
throw new Error('Account is not set');
}
}
async _init() {
if (this._isInitialized) {
return;
}
if (this._storageUnsub) {
this._storageUnsub();
this._storageUnsub = undefined;
}
const state = await this._api.query.gearProgram.programStorage(this._id);
if (state.isNone) {
throw new Error(`Program ${this._id} doesn't exist`);
}
if (state.unwrap().isExited) {
this._id = state.unwrap().asExited.toHex();
return this._init();
}
if (state.unwrap().isTerminated) {
throw new Error(`Program ${this._id} has been terminated`);
}
this._storageUnsub = await this._api.query.gearProgram.programStorage(this._id, (data) => {
if (data.unwrap().isExited) {
this._isInitialized = false;
this._id = data.unwrap().asExited.toHex();
this._events.dispatchEvent(new CustomEvent('programExited', { detail: { inheritorId: this._id } }));
this._init();
}
});
this._isInitialized = true;
}
/**
* ## Subscribes to a specific event emitted by the program.
* @param action - The name of the event to subscribe to (e.g., 'programExited').
* @param cb - The callback function to execute when the event is triggered. Receives the inheritor ID as a parameter.
* @returns A function to unsubscribe from the event.
*/
async on(action, callback) {
await this.waitForInitialization;
const listener = function (event) {
callback(event.detail.inheritorId);
};
this._events.addEventListener(action, listener);
return () => {
this._events.removeEventListener(action, listener);
};
}
async _submitTx(tx, eventsToBeReturned = []) {
const _events = [];
const [success, txError, blockHash] = await new Promise((resolve) => tx
.signAndSend(this._account, this._signerOptions, ({ events, status }) => {
if (status.isInBlock) {
for (const e of events) {
if (eventsToBeReturned.includes(e.event.method)) {
_events.push(e.event);
}
if (e.event.method === 'ExtrinsicSuccess') {
resolve([true, undefined, status.asInBlock.toHex()]);
}
else if (e.event.method === 'ExtrinsicFailed') {
resolve([false, this._api.getExtrinsicFailedError(e.event).docs, status.asInBlock.toHex()]);
}
}
}
})
.catch((error) => {
resolve([false, error.message]);
}));
return {
txHash: tx.hash.toHex(),
success,
eventsToReturn: _events,
error: txError,
blockHash,
blockNumber: blockHash ? (await this._api.blocks.getBlockNumber(blockHash)).toNumber() : undefined,
};
}
get _accountAddress() {
if (typeof this._account == 'string') {
return address.decodeAddress(this._account);
}
else if ('address' in this._account) {
return address.decodeAddress(this._account.address);
}
else {
return this._account.toHex();
}
}
/**
* ## Gets the current program ID.
* @returns The program ID as a HexString.
*/
get id() {
return this._id;
}
/**
* Retrieves the current balance of the program.
* @returns The program's balance as a bigint.
*/
async balance() {
await this.waitForInitialization;
const { data: balance } = await this._api.query.system.account(this._id);
return balance.free.toBigInt();
}
/**
* ## Transfers funds to the program to increase its balance.
* @param value - The amount to transfer as a bigint.
*/
async topUp(value) {
await this.waitForInitialization;
this.throwOnAccountNotSet();
const tx = this._api.tx.balances.transferKeepAlive(this._id, value);
return this._submitTx(tx);
}
/**
* ## Calculates the gas required for the message.
* @param payload - The encoded payload to send, as a HexString or Uint8Array.
* @param value - The value to send with the payload (default is 0).
* @param allowOtherPanics - Whether to allow panics in other programs during gas calculation (default is false).
* @returns Gas details.
*/
async calculateGas(payload, value = 0, allowOtherPanics = false) {
await this.waitForInitialization;
this.throwOnAccountNotSet();
const { min_limit, reserved, burned, may_be_returned, waited } = await this._api.program.calculateGas.handle(this._accountAddress, this._id, payload, value, allowOtherPanics);
return {
minLimit: min_limit.toBigInt(),
reserved: reserved.toBigInt(),
burned: burned.toBigInt(),
mayBeReturned: may_be_returned.toBigInt(),
waited: waited.isTrue,
};
}
/**
* ## Sends a message to the program.
* @param payload - The payload to send, as a HexString or Uint8Array.
* @param value - The value to send with the message (default is 0).
* @param gasLimit - The gas limit for the message ('max', 'auto', or a specific value). If 'auto', it will be calculated automatically.
* @param keepAlive - Whether to use keep-alive for the transaction (default is true).
*/
async sendMessage(payload, value = 0, gasLimit = 'auto', keepAlive = true) {
await this.waitForInitialization;
this.throwOnAccountNotSet();
if (gasLimit == 'max') {
gasLimit = this._api.blockGasLimit.toBigInt();
}
else if (gasLimit == 'auto') {
const gas = await this.calculateGas(payload, value, keepAlive);
gasLimit = gas.minLimit;
}
const tx = this._api.tx.gear.sendMessage(this._id, payload, gasLimit, value, keepAlive);
const { success, blockHash, blockNumber, eventsToReturn, txHash, error } = await this._submitTx(tx, [
'MessageQueued',
]);
let msgId;
let response;
if (success) {
const messageQueuedEvent = eventsToReturn.find((e) => e.method === 'MessageQueued');
if (messageQueuedEvent) {
msgId = messageQueuedEvent.data[0].toHex();
}
response = async () => {
const { data: { message }, } = await this._api.message.getReplyEvent(this._id, msgId, blockNumber);
return {
id: message.id.toHex(),
payload: message.payload.toHex(),
value: message.value.toBigInt(),
replyCode: new replyCode.ReplyCode(message.details.unwrap().code.toU8a(), this._api.specVersion),
};
};
}
return {
success,
blockHash,
blockNumber,
txHash,
error,
msgId,
response,
};
}
/**
* ## Calculates the reply for a given payload and value.
* @param payload - The payload to send, as a HexString or Uint8Array.
* @param value - The value to send with the payload (default is 0).
* @param gasLimit - The gas limit for the reply ('max' or a specific value).
* @returns Reply details.
*/
async calculateReply(payload, value = 0, gasLimit = 'max') {
await this.waitForInitialization;
this.throwOnAccountNotSet();
const reply = await this._api.message.calculateReply({
payload,
origin: this._accountAddress,
destination: this._id,
gasLimit: gasLimit == 'max' ? this._api.blockGasLimit : gasLimit,
value,
});
return {
payload: reply.payload.toHex(),
value: reply.value.toBigInt(),
code: new replyCode.ReplyCode(reply.code.toU8a(), this._api.specVersion),
};
}
}
exports.Program = Program;