UNPKG

@gear-js/api

Version:

A JavaScript library that provides functionality to connect GEAR Component APIs.

379 lines (376 loc) 15.2 kB
import '@polkadot/util'; import './utils/generate.js'; import '@polkadot/types'; import { decodeAddress } from './utils/address.js'; import '@polkadot/util-crypto'; import 'assert'; import './default/index.js'; import './metadata/programMetadata.js'; import { ReplyCode } from './utils/reply-code.js'; /** * # Program Class * @param _id - The ID of the program. * @param _api - An instance of the GearApi class * @param _account - (optional) The account or address to be used for transactions. * @param _signerOptions - (optional) Signer options for transactions. */ class BaseGearProgram { _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 BaseGearProgram(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); let continueInit = true; while (continueInit) { if (state.isNone) { throw new Error(`Program ${this._id} doesn't exist`); } if (state.unwrap().isExited) { this._id = state.unwrap().asExited.toHex(); continue; } if (state.unwrap().isTerminated) { throw new Error(`Program ${this._id} has been terminated`); } continueInit = false; } 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 callback - 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); }; } /** * ## Sets the account to be used for submitting transactions. * @param account * @param signerOptions * @returns */ setAccount(account, signerOptions) { this._account = account; this._signerOptions = signerOptions; return this; } 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 { event } of events) { if (eventsToBeReturned.includes(event.method)) { _events.push(event); } if (event.method === 'ExtrinsicSuccess') { resolve([true, undefined, status.asInBlock.toHex()]); } else if (event.method === 'ExtrinsicFailed') { resolve([false, this._api.getExtrinsicFailedError(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 decodeAddress(this._account); } else if ('address' in this._account) { return 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: minLimit, reserved, burned, may_be_returned: mayBeReturned, waited, } = await this._api.program.calculateGas.handle(this.accountAddress, this._id, payload, value, allowOtherPanics); return { minLimit: minLimit.toBigInt(), reserved: reserved.toBigInt(), burned: burned.toBigInt(), mayBeReturned: mayBeReturned.toBigInt(), waited: waited.isTrue, }; } /** * ## 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(reply.code.toU8a(), this._api.specVersion), }; } async sendMessage(paramsOrPayload, value, gasLimit, keepAlive) { await this.waitForInitialization; this._throwOnAccountNotSet(); const isPayloadDirect = typeof paramsOrPayload === 'string' || paramsOrPayload instanceof Uint8Array; const _payload = isPayloadDirect ? paramsOrPayload : paramsOrPayload.payload; const _value = (isPayloadDirect ? value : paramsOrPayload.value) ?? 0; const _keepAlive = (isPayloadDirect ? keepAlive : paramsOrPayload.keepAlive) ?? true; let _gasLimit = (isPayloadDirect ? gasLimit : paramsOrPayload.gasLimit) ?? 'auto'; _gasLimit = await this._resolveGasLimit(_payload, _value, gasLimit, _keepAlive); const tx = this._api.tx.gear.sendMessage(this._id, _payload, _gasLimit, _value, _keepAlive); const txResult = await this._submitTx(tx, ['MessageQueued']); if (txResult.success === true) { const messageQueuedEvent = txResult.eventsToReturn[0]; let msgId; if (messageQueuedEvent) { msgId = messageQueuedEvent.data[0].toHex(); } else { throw new Error('MessageQueued event not found in transaction result'); } return { success: true, txHash: txResult.txHash, blockHash: txResult.blockHash, blockNumber: txResult.blockNumber, id: msgId, response: this._waitForResponseHandler(msgId, txResult.blockNumber).bind(this), }; } else { return { success: false, error: txResult.error, txHash: txResult.txHash, blockHash: txResult.blockHash, blockNumber: txResult.blockNumber, }; } } /** * ## Sends multiple messages to the program in a single batch transaction. * @param messages - Array of message parameter objects to send in batch. Each object follows the ISendMessageParams interface. * @returns Transaction result with message IDs and their individual success status. */ async sendBatchMessages(messages) { await this.waitForInitialization; this._throwOnAccountNotSet(); const txs = await Promise.all(messages.map(async ({ payload, value = 0, gasLimit = 'auto', keepAlive = true }) => { const resolvedGasLimit = await this._resolveGasLimit(payload, value, gasLimit, keepAlive); return this._api.tx.gear.sendMessage(this._id, payload, resolvedGasLimit, value, keepAlive); })); const batchTx = this._api.tx.utility.batchAll(txs); const txResult = await this._submitTx(batchTx, ['MessageQueued', 'ItemCompleted', 'ItemFailed']); let sentMessages = []; if (txResult.success === true) { const mqEvents = txResult.eventsToReturn.filter(({ method }) => method == 'MessageQueued'); if (mqEvents.length == messages.length) { sentMessages = mqEvents.map((event) => { const id = event.data.id.toHex(); return { success: true, id, response: this._waitForResponseHandler(id, txResult.blockNumber).bind(this), }; }); } else { const statusEvents = txResult.eventsToReturn.filter(({ method }) => method === 'ItemCompleted' || method === 'ItemFailed'); const mqEvents = txResult.eventsToReturn.filter(({ method }) => method === 'MessageQueued'); let mqIndex = 0; for (let i = 0; i < messages.length; i++) { const statusEvent = statusEvents[i]; if (statusEvent.method === 'ItemCompleted') { const id = mqEvents[mqIndex].data.id.toHex(); sentMessages.push({ success: true, id, response: this._waitForResponseHandler(id, txResult.blockNumber).bind(this), }); mqIndex++; } else { sentMessages.push({ success: false, error: this._api.getExtrinsicFailedError(statusEvent).docs, }); } } } return { success: true, txHash: txResult.txHash, blockHash: txResult.blockHash, blockNumber: txResult.blockNumber, sentMessages, }; } else { return { success: false, error: txResult.error, txHash: txResult.txHash, blockHash: txResult.blockHash, blockNumber: txResult.blockNumber, }; } } _waitForResponseHandler(id, blockNumber) { return async () => { const { data: { message }, } = await this._api.message.getReplyEvent(this._id, id, blockNumber); return { id: message.id.toHex(), payload: message.payload.toHex(), value: message.value.toBigInt(), replyCode: new ReplyCode(message.details.unwrap().code.toU8a(), this._api.specVersion), }; }; } /** * Resolves the gas limit based on the provided value or calculates it if needed. * @param payload - The message payload. * @param value - The value to send with the message. * @param gasLimit - The requested gas limit or 'auto'/'max' option. * @param keepAlive - Whether the message uses keep-alive semantics. * @returns Resolved gas limit as a bigint. * @private */ async _resolveGasLimit(payload, value, gasLimit, keepAlive) { try { if (gasLimit === 'max') { return this._api.blockGasLimit.toBigInt(); } else if (!gasLimit || gasLimit === 'auto') { const gas = await this.calculateGas(payload, value, keepAlive); return gas.minLimit; } else { try { return BigInt(gasLimit); } catch (_) { throw new Error(`Invalid gas limit value: ${gasLimit}`); } } } catch (error) { throw new Error(`Failed to resolve gas limit: ${error.message}`); } } } /** * @deprecated - use BaseGearProgram instead */ class Program extends BaseGearProgram { } export { BaseGearProgram, Program };