zkverifyjs
Version:
Submit proofs to zkVerify and query proof state with ease using our npm package.
243 lines • 11.4 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.EventManager = void 0;
const index_js_1 = require("../../../api/aggregation/index.js");
const events_1 = require("events");
const enums_js_1 = require("../../../enums.js");
const RUNTIME_EVENT_MATCHERS = {
[enums_js_1.ZkVerifyEvents.ProofVerified]: /::ProofVerified/,
[enums_js_1.ZkVerifyEvents.CannotAggregate]: 'aggregate::CannotAggregate',
[enums_js_1.ZkVerifyEvents.NewProof]: 'aggregate::NewProof',
[enums_js_1.ZkVerifyEvents.VkRegistered]: /::VkRegistered/,
[enums_js_1.ZkVerifyEvents.AggregationComplete]: 'aggregate::AggregationComplete',
[enums_js_1.ZkVerifyEvents.NewDomain]: 'aggregate::NewDomain',
[enums_js_1.ZkVerifyEvents.DomainStateChanged]: 'aggregate::DomainStateChanged',
};
class EventManager {
constructor(connectionManager) {
this.unsubscribeFunctions = [];
// Per-event-type handlers. We only ever register a SINGLE
// api.query.system.events callback per EventManager and dispatch from there
// — registering one polkadot subscription per event type would decode the
// whole EventRecord vector once per subscriber on every block.
this.runtimeEventHandlers = new Map();
this.systemEventsSubscribed = false;
// Tracks which ZkVerifyEvents the caller has already subscribed to so a
// second subscribe() call doesn't double-register listeners (and double the
// codec/event traffic on every block).
this.subscribedEvents = new Set();
// True after unsubscribe() has been called; pending async unsubscribe fns
// must be invoked immediately rather than pushed into an array nothing
// iterates again. Reset to false at the top of subscribe() so the manager
// can be re-used after an unsubscribe()/subscribe() cycle.
this.isClosed = false;
// Bumped every time a NEW underlying api.query.system.events subscription
// is established. The handshake .then handler captures the generation it
// was set up under and bails (invoking the unsubscribe fn immediately) if
// the manager has moved on — closing this avoids cross-cycle leaks where
// an old handshake completes after a re-subscribe and orphans its sub.
this.systemEventsGeneration = 0;
this.connectionManager = connectionManager;
this.emitter = new events_1.EventEmitter();
}
/**
* Subscribes to specified ZkVerifyEvents.
* For `NewAggregationReceipt`, `options` can include `domainId` and `aggregationId`.
* For runtime events (e.g., ProofVerified), options are ignored.
*
* Idempotent: subscribing to an event already subscribed to is a no-op for
* that event (other entries in the same call are still processed).
*
* @param subscriptions - List of events to subscribe to with optional callback and filtering options.
* @returns EventEmitter to allow listening to additional internal events (e.g., `Unsubscribe`).
*/
subscribe(subscriptions) {
// Allow re-subscribing after a previous unsubscribe() — the generation
// counter on system.events guards stale handshakes from the prior cycle.
this.isClosed = false;
const { api } = this.connectionManager;
const eventsToSubscribe = subscriptions?.length
? subscriptions
: enums_js_1.PUBLIC_ZK_VERIFY_EVENTS.map((event) => ({
event,
callback: undefined,
options: event === enums_js_1.ZkVerifyEvents.NewAggregationReceipt
? undefined
: undefined,
}));
eventsToSubscribe.forEach(({ event, callback, options }) => {
if (this.subscribedEvents.has(event)) {
return;
}
switch (event) {
case enums_js_1.ZkVerifyEvents.NewAggregationReceipt:
this.subscribedEvents.add(event);
(0, index_js_1.subscribeToNewAggregationReceipts)(api, (data) => {
this.emitter.emit(event, data);
if (callback)
callback(data);
}, options, this.emitter);
break;
case enums_js_1.ZkVerifyEvents.ProofVerified:
case enums_js_1.ZkVerifyEvents.NewProof:
case enums_js_1.ZkVerifyEvents.VkRegistered:
case enums_js_1.ZkVerifyEvents.NewDomain:
case enums_js_1.ZkVerifyEvents.DomainStateChanged:
case enums_js_1.ZkVerifyEvents.AggregationComplete:
case enums_js_1.ZkVerifyEvents.CannotAggregate:
this.subscribedEvents.add(event);
this._registerRuntimeEvent(api, event, callback);
break;
default:
throw new Error(`Unsupported event type for subscription: ${event}`);
}
});
return this.emitter;
}
/**
* Registers a per-event handler against the shared system.events
* subscription, lazily creating that subscription on first use.
*/
_registerRuntimeEvent(api, eventType, callback) {
if (this.runtimeEventHandlers.has(eventType))
return;
const expected = RUNTIME_EVENT_MATCHERS[eventType];
if (!expected)
return;
const handler = (records) => {
for (const { event, phase } of records) {
const key = `${event.section}::${event.method}`;
const matches = (typeof expected === 'string' && key === expected) ||
(expected instanceof RegExp && expected.test(key));
if (!matches)
continue;
const parsedPhase = phase.toJSON ? phase.toJSON() : phase.toString();
const eventPayload = {
event: eventType,
data: event.data.toHuman?.() ?? event.data.toString(),
phase: parsedPhase,
};
this.emitter.emit(eventType, eventPayload);
if (callback)
callback(eventPayload);
}
};
this.runtimeEventHandlers.set(eventType, handler);
this._ensureSystemEventsSubscription(api);
}
_ensureSystemEventsSubscription(api) {
if (this.systemEventsSubscribed)
return;
this.systemEventsSubscribed = true;
const myGeneration = ++this.systemEventsGeneration;
const subscriptionResult = api.query.system.events((records) => {
// If a newer cycle has replaced this subscription (or the manager
// closed) but the polkadot unsubscribe fn hasn't fired yet, drop
// these records — the handlers Map may belong to a different cycle.
if (myGeneration !== this.systemEventsGeneration)
return;
for (const handler of this.runtimeEventHandlers.values()) {
try {
handler(records);
}
catch (err) {
this.emitter.emit(enums_js_1.ZkVerifyEvents.ErrorEvent, err);
}
}
});
const handleUnsubscribeFn = (fn) => {
if (typeof fn !== 'function')
return;
// Invoke the unsubscribe fn immediately if either (a) the manager has
// been closed since the handshake started, or (b) a newer system.events
// subscription has replaced this one. Either case would otherwise leave
// the polkadot subscription orphaned.
if (this.isClosed || myGeneration !== this.systemEventsGeneration) {
try {
fn();
}
catch (error) {
console.debug('Error during late runtime-event cleanup:', error);
}
return;
}
this.unsubscribeFunctions.push(fn);
};
if (subscriptionResult) {
if (typeof subscriptionResult === 'function') {
handleUnsubscribeFn(subscriptionResult);
}
else if (typeof subscriptionResult.then === 'function') {
subscriptionResult.then(handleUnsubscribeFn).catch((error) => {
this.emitter.emit(enums_js_1.ZkVerifyEvents.ErrorEvent, error);
});
}
}
}
/**
* Waits for a specific `NewAggregationReceipt` event and returns the result as a NewAggregationReceipt object.
*
* @param domainId - The domain ID to listen for.
* @param aggregationId - The aggregation ID to listen for.
* @param timeout - Optional timeout value in milliseconds.
* @returns {Promise<NewAggregationReceipt>} Resolves with the event data when found, or rejects on timeout/error.
*/
async waitForAggregationReceipt(domainId, aggregationId, timeout) {
const { api } = this.connectionManager;
const options = { domainId, aggregationId, timeout };
return new Promise((resolve, reject) => {
(0, index_js_1.subscribeToNewAggregationReceipts)(api, (eventObject) => {
const event = eventObject;
const data = event?.data;
const receiptData = Array.isArray(data)
? {
domainId: data[0],
aggregationId: data[1],
receipt: data[2],
}
: data;
if (event &&
receiptData &&
receiptData.domainId !== undefined &&
receiptData.aggregationId !== undefined &&
receiptData.receipt !== undefined) {
const result = {
blockHash: event.blockHash ?? null,
domainId: Number(receiptData.domainId),
aggregationId: Number(receiptData.aggregationId),
receipt: String(receiptData.receipt),
};
resolve(result);
}
else {
reject(new Error('Invalid event data structure'));
}
}, options, this.emitter).catch(reject);
});
}
/**
* Unsubscribes from all active subscriptions.
*/
unsubscribe() {
this.isClosed = true;
// Bumping the generation invalidates any in-flight system.events handshake
// from this cycle — when its .then resolves, handleUnsubscribeFn will see
// the mismatch and invoke the unsubscribe fn immediately.
this.systemEventsGeneration += 1;
this.unsubscribeFunctions.forEach((unsubscribeFn) => {
try {
unsubscribeFn();
}
catch (error) {
console.debug('Error during subscription cleanup:', error);
}
});
this.unsubscribeFunctions.length = 0;
this.runtimeEventHandlers.clear();
this.subscribedEvents.clear();
this.systemEventsSubscribed = false;
(0, index_js_1.unsubscribe)(this.emitter);
}
}
exports.EventManager = EventManager;
//# sourceMappingURL=index.js.map