UNPKG

zkverifyjs

Version:

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

243 lines 11.4 kB
"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