UNPKG

zkverifyjs

Version:

Submit proofs to zkVerify and query proof state with ease using our npm package.

158 lines 6.3 kB
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(); }