@gear-js/api
Version:
A JavaScript library that provides functionality to connect GEAR Component APIs.
262 lines (259 loc) • 11 kB
JavaScript
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 };