zkverifyjs
Version:
Submit proofs to zkVerify and query proof state with ease using our npm package.
158 lines • 6.3 kB
JavaScript
import { ZkVerifyEvents } from "../../enums.js";
/**
* Subscribes to `aggregation.NewAggregationReceipt` events and triggers the provided callback.
*
* - If both `domainId` and `aggregationId` are provided, the listener stops after the matching receipt is found.
* - If only `domainId` is provided, listens indefinitely for all receipts within that domain.
* - If neither is provided, listens to all receipts across all domains.
* - Throws if `aggregationId` is provided without a `domainId`.
*
* @param {ApiPromise} api - The Polkadot.js API instance.
* @param callback
* @param options - NewAggregationEventSubscriptionOptions containing domainId, aggregationId and optional timeout.
* @param emitter - EventEmitter
* @returns {EventEmitter} EventEmitter for listening to emitted events and unsubscribing.
*/
export async function subscribeToNewAggregationReceipts(api, callback, options = undefined, emitter) {
return new Promise((resolve, reject) => {
const DEFAULT_MATCH_TIMEOUT = 180000;
let domainId = undefined;
let aggregationId = undefined;
let timeoutId;
let unsubscribeFinalizedHeads;
let isResolved = false;
let isRejected = false;
const cleanup = () => {
if (timeoutId) {
clearTimeout(timeoutId);
timeoutId = undefined;
}
if (unsubscribeFinalizedHeads) {
try {
unsubscribeFinalizedHeads();
} catch (err) {
console.debug('Error during finalized heads cleanup:', err);
}
unsubscribeFinalizedHeads = undefined;
}
};
const safeResolve = value => {
if (!isResolved && !isRejected) {
isResolved = true;
cleanup();
resolve(value);
}
};
const safeReject = error => {
if (!isResolved && !isRejected) {
isRejected = true;
cleanup();
reject(error);
}
};
emitter._cleanup = cleanup;
emitter.once(ZkVerifyEvents.Unsubscribe, cleanup);
if (options) {
domainId = options.domainId?.toString().trim();
if ('aggregationId' in options) {
aggregationId = options.aggregationId?.toString().trim();
if (!domainId) {
safeReject(new Error('Cannot filter by aggregationId without also providing domainId.'));
return;
}
}
}
if (aggregationId && domainId) {
const timeoutValue = options && 'timeout' in options && typeof options.timeout === 'number' ? options.timeout : DEFAULT_MATCH_TIMEOUT;
timeoutId = setTimeout(() => {
unsubscribe(emitter);
safeReject(new Error(`Timeout exceeded: No event received within ${timeoutValue} ms`));
}, timeoutValue);
}
try {
const subscriptionResult = api.rpc.chain.subscribeFinalizedHeads(async header => {
const blockHash = header.hash.toHex();
const apiAt = await api.at(blockHash);
const events = await apiAt.query.system.events();
events.forEach(record => {
const {
event,
phase
} = record;
if (event.section === 'aggregate' && event.method === 'NewAggregationReceipt') {
let currentDomainId;
let currentAggregationId;
const eventData = event.data.toHuman ? event.data.toHuman() : Array.from(event.data, item => item.toString());
const eventObject = {
event: ZkVerifyEvents.NewAggregationReceipt,
blockHash,
data: eventData,
phase: phase && typeof phase.toJSON === 'function' ? phase.toJSON() : phase?.toString() || ''
};
try {
currentDomainId = event.data[0]?.toString();
currentAggregationId = event.data[1]?.toString();
if (!currentDomainId || !currentAggregationId) {
emitter.emit(ZkVerifyEvents.ErrorEvent, new Error('Event data is missing required fields: domainId or aggregationId.'));
safeReject(new Error('Event data is missing required fields: domainId or aggregationId.'));
return;
}
} catch (error) {
emitter.emit(ZkVerifyEvents.ErrorEvent, error);
safeReject(error);
return;
}
if (!options || !aggregationId && !domainId) {
emitter.emit(ZkVerifyEvents.NewAggregationReceipt, eventObject);
callback(eventObject);
} else if (domainId && !aggregationId && domainId === currentDomainId) {
emitter.emit(ZkVerifyEvents.NewAggregationReceipt, eventObject);
callback(eventObject);
} else if (domainId === currentDomainId && currentAggregationId === aggregationId) {
emitter.emit(ZkVerifyEvents.NewAggregationReceipt, eventObject);
callback(eventObject);
cleanup();
safeResolve(emitter);
return;
}
}
});
});
if (typeof subscriptionResult === 'function') {
unsubscribeFinalizedHeads = subscriptionResult;
} else if (subscriptionResult && typeof subscriptionResult.then === 'function') {
subscriptionResult.then(unsubscribeFn => {
if (!isResolved && !isRejected && typeof unsubscribeFn === 'function') {
unsubscribeFinalizedHeads = unsubscribeFn;
}
}).catch(error => {
if (!isResolved && !isRejected) {
emitter.emit(ZkVerifyEvents.ErrorEvent, error);
safeReject(error);
}
});
}
} catch (error) {
emitter.emit(ZkVerifyEvents.ErrorEvent, error);
safeReject(error);
}
return emitter;
});
}
/**
* Unsubscribes from all event tracking.
*
* - Emits a `ZkVerifyEvents.Unsubscribe` event before removing all listeners.
* - Use this to manually stop listening when not auto-unsubscribing on matched receipts.
*
* @param {EventEmitter} emitter - The EventEmitter instance returned by the subscription.
*/
export function unsubscribe(emitter) {
const emitterWithCleanup = emitter;
if (emitterWithCleanup._cleanup) {
emitterWithCleanup._cleanup();
delete emitterWithCleanup._cleanup;
}
emitter.emit(ZkVerifyEvents.Unsubscribe);
emitter.removeAllListeners();
}