UNPKG

@gear-js/api

Version:

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

262 lines (259 loc) 11 kB
import { ReplaySubject } from 'rxjs'; import '@polkadot/util'; import { SendMessageError, SendReplyError } from '../errors/message.errors.js'; import { RpcMethodNotSupportedError } from '../errors/rpc.js'; import { decodeAddress } from '../utils/address.js'; import { encodePayload } from '../utils/create-payload.js'; import '../utils/generate.js'; import { getExtrinsic } from '../utils/getExtrinsic.js'; import '../utils/reply-code.js'; import '@polkadot/util-crypto'; import { validateValue, validateGasLimit, validateMailboxItem } from '../utils/validate.js'; import { GearTransaction } from './Transaction.js'; class GearMessage extends GearTransaction { /** * ## Send Message * @param message * @param metaOrHexRegistry Metadata * @param typeIndexOrTypeName type index in registry or type name * @returns Submitable result */ send({ destination, value, gasLimit, payload, keepAlive }, metaOrHexRegistry, typeIndexOrTypeName) { validateValue(value, this._api); validateGasLimit(gasLimit, this._api); const _payload = encodePayload(payload, metaOrHexRegistry, 'handle', typeIndexOrTypeName); try { const txArgs = [destination, _payload, gasLimit, value || 0, keepAlive ?? true]; this.extrinsic = getExtrinsic(this._api, 'gear', 'sendMessage', txArgs); return this.extrinsic; } catch (error) { throw new SendMessageError(error.message); } } /** * Sends reply message * @param args Message parameters * @param metaOrHexRegistry Metadata * @param typeIndexOrTypeName type index in registry or type name * @returns Submittable result */ async sendReply({ value, gasLimit, replyToId, payload, account, keepAlive }, metaOrHexRegistry, typeIndexOrTypeName) { validateValue(value, this._api); validateGasLimit(gasLimit, this._api); if (account) { await validateMailboxItem(account, replyToId, this._api); } const _payload = encodePayload(payload, metaOrHexRegistry, 'reply', typeIndexOrTypeName); try { const txArgs = [replyToId, _payload, gasLimit, value || 0, keepAlive || true]; this.extrinsic = getExtrinsic(this._api, 'gear', 'sendReply', txArgs); return this.extrinsic; } catch (_) { throw new SendReplyError(); } } /** * ## Get event with reply message * @param msgId - id of sent message * @param txBlock - number or hash of block where the message was sent * @returns UserMessageSent event */ async getReplyEvent(programId, msgId, txBlock) { let unsub; const replyEvent = new Promise((resolve) => { unsub = this._api.gearEvents.subscribeToGearEvent('UserMessageSent', (event) => { if (event.data.message.source.eq(programId) === false) return; if (msgId === null) { resolve(event); } if (event.data.message.details.isSome && event.data.message.details.unwrap().to.toHex() === msgId) { resolve(event); } }, txBlock); }); (await unsub)(); return replyEvent; } /** * @deprecated Use `getReplyEvent` instead */ listenToReplies(programId, bufferSize = 5) { let unsub; const subject = new ReplaySubject(bufferSize); this._api.gearEvents .subscribeToGearEvent('UserMessageSent', ({ data }) => { if (data.message.source.eq(programId)) { if (data.message.details.isSome) { data.message.details.unwrap().to.toHex(); { subject.next([data.message.details.unwrap().to.toHex(), data]); } } } }) .then((result) => { unsub = result; }); return (messageId) => { return new Promise((resolve) => { subject.subscribe({ next: ([id, data]) => { if (id === messageId) { subject.complete(); unsub(); resolve(data); } }, }); }); }; } /** * ## Send message to the program and get the reply. * This method is immutable and doesn't send any extrinsic. * @param params Message parameters * @param meta (optional) Program metadata obtained using `ProgramMetadata.from` method. * @param typeIndexOrTypeName (optional) Index of type in the registry. If not specified the type index from `meta.handle.input` will be used instead. * @returns Reply info structure * * @example * ```javascript * const programId = '0x..'; * const origin = '0x...'; * const meta = ProgramMetadata.from('0x...'); * const result = await api.message.calculateReply({ * origin, * destination: programId, * payload: { myPayload: [] }, * value: 0 * }, meta); * * console.log(result.toJSON()); * console.log('reply payload:', meta.createType(meta.types.handle.output, result.payload).toJSON()); */ async calculateReply({ payload, origin, destination, value, gasLimit, at }, meta, typeIndexOrTypeName) { const _payload = encodePayload(payload, meta, 'handle', typeIndexOrTypeName); return await this._api.rpc.gear.calculateReplyForHandle(decodeAddress(origin), destination, _payload, gasLimit || this._api.blockGasLimit.toBigInt(), value || 0, at || null); } /** * ## Subscribe to User Message Sent Events * * Subscribe to real-time notifications of messages sent from programs to users. * Provides server-side filtering capabilities for efficient event tracking without client-side processing. * * The subscription automatically filters out acknowledgment messages and only returns actual message events. * * @param filter - Filter criteria for subscribed messages * @param filter.source - Optional: Program ID to filter messages from. If not specified, all programs are tracked. * @param filter.destination - Optional: User address to filter messages sent to. If not specified, all destinations are tracked. * @param filter.payloadFilters - Optional: Array of PayloadFilter objects to filter messages by their payload content. * @param filter.fromBlock - Optional: Block number to start listening from. Defaults to current block. * @param filter.finalizedOnly - Optional: If true, only process finalized blocks. Defaults to false. * * @param callback - Function called when a matching message is detected. * The callback receives a readonly UserMessageSentSubscriptionItem containing: * - id: Unique message identifier (HexString) * - source: Program ID that sent the message (HexString) * - destination: User address that received the message (HexString) * - payload: Message payload in hex format (HexString) * - value: Value transferred with the message (bigint) * - block: Block hash where the message was processed (HexString) * - index: Index of the message within the block (number) * - reply: Optional reply details if the message is a reply: * - to: Original message ID being replied to (HexString) * - code: Human-readable reply code/status (string) * - codeRaw: Raw reply code in hex format (HexString) * * @returns Unsubscribe function. Call this to stop receiving message notifications. * * @throws RpcMethodNotSupportedError - If the connected node does not support the subscription method. * * @example * ```typescript * // Subscribe to all messages from a specific program * const unsubscribe = await api.message.subscribeUserMessageSent( * { source: '0x...' }, * (item) => { * console.log('Message ID:', item.id); * console.log('From program:', item.source); * console.log('To user:', item.destination); * console.log('Payload:', item.payload); * } * ); * * // Later, stop listening * unsubscribe(); * ``` * * @example * ```typescript * // Subscribe with comprehensive filtering * const payloadFilter = new PayloadFilter(); * payloadFilter.setBytes('0xdeadbeef'); * * const unsubscribe = await api.message.subscribeUserMessageSent( * { * source: '0x...', * destination: '0x...', * payloadFilters: [payloadFilter], * fromBlock: 12345, * finalizedOnly: true * }, * async (item) => { * console.log(`Block: ${item.block}, Message: ${item.id}`); * if (item.reply) { * console.log(`This is a reply with code: ${item.reply.code}`); * } * } * ); * ``` */ async subscribeUserMessageSent(filter, callback) { const methodName = 'gear_subscribeUserMessageSent'; if (!this._api.rpcMethods.includes(methodName)) { throw new RpcMethodNotSupportedError(methodName); } const rpcFilter = { source: filter.source || null, destination: filter.destination || null, payload_filters: filter.payloadFilters?.map((filter) => filter.toJSON()) || null, from_block: filter.fromBlock ?? null, finalized_only: filter.finalizedOnly ?? false, }; let isUnsubscribed = false; const wrappedCallback = (result) => { if (result.ack?.isSome && result.ack.unwrap().isTrue) return; if (isUnsubscribed) return; const event = { id: result.id.toHex(), block: result.block.toHex(), index: result.index.toNumber(), source: result.source.toHex(), destination: result.destination.toHex(), payload: result.payload.toHex(), value: BigInt(result.value.toString()), }; if (result.reply?.isSome) { const reply = result.reply.unwrap(); event.reply = { to: reply.to.toHex(), codeDescription: reply.codeDescription.toString(), code: reply.code.toHex(), }; } callback(event); }; const unsubscribePromise = this._api.rpc.gear.subscribeUserMessageSent(rpcFilter, wrappedCallback); const unsubscribe = await unsubscribePromise; return () => { isUnsubscribed = true; unsubscribe(); }; } } export { GearMessage };