diff --git a/package-lock.json b/package-lock.json index 612cedc..b6b3fdd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -40,6 +40,7 @@ "@types/express": "^4.17.21", "@types/jest": "^30.0.0", "@types/jsonwebtoken": "9.0.5", + "@types/mongodb-memory-server": "^1.8.0", "@types/mongoose": "5.11.97", "@types/multer": "^2.2.0", "@types/node": "^20.10.0", @@ -57,7 +58,7 @@ "husky": "8.0.3", "jest": "^30.4.2", "lint-staged": "15.2.0", - "mongodb-memory-server": "9.1.6", + "mongodb-memory-server": "^9.1.6", "nodemon": "^3.0.2", "prettier": "3.1.1", "supertest": "^7.2.2", @@ -1936,9 +1937,9 @@ } }, "node_modules/@smithy/core": { - "version": "3.31.0", - "resolved": "https://registry.npmjs.org/@smithy/core/-/core-3.31.0.tgz", - "integrity": "sha512-sylYk2l9d7CmRv8ts8p0SDQUr3VO+HMeS1nrjL6+UtbO8ktJHTOeQ1McX+aAyvGGccp5aZX9eNtdcXrSwzoZaw==", + "version": "3.31.1", + "resolved": "https://registry.npmjs.org/@smithy/core/-/core-3.31.1.tgz", + "integrity": "sha512-CyogUINxvi7C7LDsh8Syo6hVJOT9ckz4rG8dRZfTJ8r91HkMY59PnNooaj7WcHyxEkxPfBAmbgztZU+xTo76lg==", "license": "Apache-2.0", "dependencies": { "@smithy/types": "^4.16.1", @@ -1949,12 +1950,12 @@ } }, "node_modules/@smithy/credential-provider-imds": { - "version": "4.4.15", - "resolved": "https://registry.npmjs.org/@smithy/credential-provider-imds/-/credential-provider-imds-4.4.15.tgz", - "integrity": "sha512-xYVGrisQqTJWhOnScUhbx8s9H63TMtoxzuUoxG6mP8J+B/YbX3vZxVsgV0xDf43abJnJP0fjP7BkQh7OESwuRA==", + "version": "4.4.16", + "resolved": "https://registry.npmjs.org/@smithy/credential-provider-imds/-/credential-provider-imds-4.4.16.tgz", + "integrity": "sha512-QfuLWAkLzptffFW980AFeHZFdqds2B64rpEd3uJ6lgs3xVn9QegGMUgUcj+4d7dRrAsya3r58ZKpku97WcFb4w==", "license": "Apache-2.0", "dependencies": { - "@smithy/core": "^3.31.0", + "@smithy/core": "^3.31.1", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" }, @@ -1963,12 +1964,12 @@ } }, "node_modules/@smithy/fetch-http-handler": { - "version": "5.6.12", - "resolved": "https://registry.npmjs.org/@smithy/fetch-http-handler/-/fetch-http-handler-5.6.12.tgz", - "integrity": "sha512-OpQgP6IGH4j0NJ2zjfYZLjQL85ai+Wi/q51EmZJovXsEwKSvu89qiXUq77Q6EmwZ/hSl7fKpn2Z9mhiDN6OM+Q==", + "version": "5.6.13", + "resolved": "https://registry.npmjs.org/@smithy/fetch-http-handler/-/fetch-http-handler-5.6.13.tgz", + "integrity": "sha512-4fW86pEUOMbrD5nkbyl/tTvPHHWJFbuB2odl6ps9lWfHoXf9HWh3Q/Smh59qH1g7+c/BSZghX6bbUk4gsiMs8A==", "license": "Apache-2.0", "dependencies": { - "@smithy/core": "^3.31.0", + "@smithy/core": "^3.31.1", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" }, @@ -1977,12 +1978,12 @@ } }, "node_modules/@smithy/node-http-handler": { - "version": "4.9.12", - "resolved": "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-4.9.12.tgz", - "integrity": "sha512-dWW5KRt4mnEvjNzbGqGeCuAvgum85Y9ZoyuMQqcTEfapndyVJ1k9BEHK7kdXJZ32enyRmmwcFjMwlB/KgLKI3Q==", + "version": "4.9.13", + "resolved": "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-4.9.13.tgz", + "integrity": "sha512-Nmd/Nl35zfYrd+a6OO2cDJb3GPh9bgTjIUhcM+JFfjpp8/osCgboDV5nCT1I01Pv6R13eSKDKLSoVa5ZB6Zsfw==", "license": "Apache-2.0", "dependencies": { - "@smithy/core": "^3.31.0", + "@smithy/core": "^3.31.1", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" }, @@ -1991,12 +1992,12 @@ } }, "node_modules/@smithy/signature-v4": { - "version": "5.6.11", - "resolved": "https://registry.npmjs.org/@smithy/signature-v4/-/signature-v4-5.6.11.tgz", - "integrity": "sha512-7HsspeiNCZvZHEJ22vV5L/QYuJdTyJvPJvMrYD3AgkM3IJB0pkln4jkjPvtpTWRMkHXbO8WKwNjoVdVlBFwHmw==", + "version": "5.6.12", + "resolved": "https://registry.npmjs.org/@smithy/signature-v4/-/signature-v4-5.6.12.tgz", + "integrity": "sha512-I6KLtq3H0qqSuV9vLglfi8puHqzygzWHOnI4z/Rdoo+q50vvo18vBRdPAvvEtcaKROz7Zn6qnPa14kRfPH6PcQ==", "license": "Apache-2.0", "dependencies": { - "@smithy/core": "^3.31.0", + "@smithy/core": "^3.31.1", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" }, @@ -2331,6 +2332,16 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/mongodb-memory-server": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@types/mongodb-memory-server/-/mongodb-memory-server-1.8.0.tgz", + "integrity": "sha512-8qBF4ww/i+SMfULYe9kCEtPRR3zp5U6UB8ApwogRPK+2wy8RPECxgcLCDDzRBJ0f9aStmE5O5IBO4s8shaIUjA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/mongoose": { "version": "5.11.97", "resolved": "https://registry.npmjs.org/@types/mongoose/-/mongoose-5.11.97.tgz", diff --git a/package.json b/package.json index 0924986..80735a7 100644 --- a/package.json +++ b/package.json @@ -44,6 +44,7 @@ "@types/express": "^4.17.21", "@types/jest": "^30.0.0", "@types/jsonwebtoken": "9.0.5", + "@types/mongodb-memory-server": "^1.8.0", "@types/mongoose": "5.11.97", "@types/multer": "^2.2.0", "@types/node": "^20.10.0", @@ -61,7 +62,7 @@ "husky": "8.0.3", "jest": "^30.4.2", "lint-staged": "15.2.0", - "mongodb-memory-server": "9.1.6", + "mongodb-memory-server": "^9.1.6", "nodemon": "^3.0.2", "prettier": "3.1.1", "supertest": "^7.2.2", diff --git a/src/config/env.ts b/src/config/env.ts index 8107e80..ca087a2 100644 --- a/src/config/env.ts +++ b/src/config/env.ts @@ -15,8 +15,8 @@ interface EnvConfig { RATE_LIMIT_WINDOW_MS: number; RATE_LIMIT_MAX_REQUESTS: number; DISPUTE_NOTIFICATION_WEBHOOK_URL: string; - UPLOAD_STORAGE_DRIVER?: string; - AWS_S3_BUCKET?: string; + UPLOAD_STORAGE_DRIVER?: string; // Add this + AWS_S3_BUCKET?: string; // Add this } const envSchema = z.object({ diff --git a/src/controllers/eventLogController.ts b/src/controllers/eventLogController.ts new file mode 100644 index 0000000..2a28270 --- /dev/null +++ b/src/controllers/eventLogController.ts @@ -0,0 +1,118 @@ +import { Request, Response } from 'express'; +import { StatusCodes } from 'http-status-codes'; +import eventLogService from '../services/eventLogService'; +import logger from '../config/logger'; + +export class EventLogController { + /** + * Get the last processed ledger sequence + * GET /api/v1/eventlog/last-processed + */ + async getLastProcessedLedger(req: Request, res: Response): Promise { + try { + const { eventType } = req.query; + const lastLedger = await eventLogService.getLastProcessedLedger( + eventType as string | undefined + ); + return res.status(StatusCodes.OK).json({ + success: true, + data: { lastProcessedLedger: lastLedger }, + message: 'Last processed ledger sequence retrieved successfully', + }); + } catch (error) { + logger.error('Error in getLastProcessedLedger:', error); + return res.status(StatusCodes.INTERNAL_SERVER_ERROR).json({ + success: false, + message: 'Failed to retrieve last processed ledger', + }); + } + } + + /** + * Get unprocessed events + * GET /api/v1/eventlog/unprocessed + */ + async getUnprocessedEvents(req: Request, res: Response): Promise { + try { + const events = await eventLogService.getUnprocessedEvents(); + return res.status(StatusCodes.OK).json({ + success: true, + data: events, + message: 'Unprocessed events retrieved successfully', + }); + } catch (error) { + logger.error('Error in getUnprocessedEvents:', error); + return res.status(StatusCodes.INTERNAL_SERVER_ERROR).json({ + success: false, + message: 'Failed to retrieve unprocessed events', + }); + } + } + + /** + * Get events by ledger sequence range + * GET /api/v1/eventlog/range + */ + async getEventsByLedgerRange(req: Request, res: Response): Promise { + try { + const { startLedger, endLedger, eventType } = req.query; + + if (!startLedger || !endLedger) { + return res.status(StatusCodes.BAD_REQUEST).json({ + success: false, + message: 'startLedger and endLedger are required', + }); + } + + const events = await eventLogService.getEventsByLedgerRange( + parseInt(startLedger as string), + parseInt(endLedger as string), + eventType as string | undefined + ); + + return res.status(StatusCodes.OK).json({ + success: true, + data: events, + message: 'Events retrieved successfully', + }); + } catch (error) { + logger.error('Error in getEventsByLedgerRange:', error); + return res.status(StatusCodes.INTERNAL_SERVER_ERROR).json({ + success: false, + message: 'Failed to retrieve events', + }); + } + } + + /** + * Get event by transaction hash + * GET /api/v1/eventlog/transaction/:hash + */ + async getEventByTransactionHash(req: Request, res: Response): Promise { + try { + const { hash } = req.params; + const event = await eventLogService.getEventByTransactionHash(hash); + + if (!event) { + return res.status(StatusCodes.NOT_FOUND).json({ + success: false, + message: 'Event not found', + }); + } + + return res.status(StatusCodes.OK).json({ + success: true, + data: event, + message: 'Event retrieved successfully', + }); + } catch (error) { + logger.error('Error in getEventByTransactionHash:', error); + return res.status(StatusCodes.INTERNAL_SERVER_ERROR).json({ + success: false, + message: 'Failed to retrieve event', + }); + } + } +} + +export default new EventLogController(); diff --git a/src/models/EventLog.ts b/src/models/EventLog.ts index b420c36..f080b10 100644 --- a/src/models/EventLog.ts +++ b/src/models/EventLog.ts @@ -1,59 +1,140 @@ -import mongoose, { Schema, Document } from 'mongoose'; +import mongoose, { Schema, Document, Model } from 'mongoose'; -/** - * Represents the last processed ledger position for a given event type - * and contract combination. Used by the event poller to resume from - * where it left off after restarts. - */ export interface IEventLog extends Document { - /** Logical event type (e.g. "escrow_funded", "reputation_increased"). */ eventType: string; - /** Soroban contract ID that emits the event. */ - contractId: string; - /** Last ledger sequence number that was successfully processed. */ - lastProcessedLedger: number; - /** Opaque cursor returned by `getEvents` for resuming mid-pagination. */ - cursor?: string; - /** Timestamp of the last successful poll cycle. */ - updatedAt: Date; + transactionHash: string; + ledgerSequence: number; + contractId?: string; + eventData?: Record; + processedAt: Date | null; + status: 'pending' | 'processed' | 'failed'; + errorMessage?: string | null; createdAt: Date; + updatedAt: Date; +} + +// Define static methods interface +interface IEventLogModel extends Model { + markAsProcessed(transactionHash: string, eventType: string): Promise; + getLastProcessedLedger(eventType?: string): Promise; + eventExists(transactionHash: string, eventType: string): Promise; } -const EventLogSchema = new Schema( +const EventLogSchema = new Schema( { eventType: { type: String, - required: true, + required: [true, 'Event type is required'], + enum: ['delivery', 'escrow', 'dispute', 'reputation', 'milestone'], + index: true, + }, + transactionHash: { + type: String, + required: [true, 'Transaction hash is required'], + index: true, trim: true, }, + ledgerSequence: { + type: Number, + required: [true, 'Ledger sequence is required'], + index: true, + min: [0, 'Ledger sequence must be a positive number'], + }, contractId: { type: String, - required: true, + index: true, trim: true, + default: null, }, - lastProcessedLedger: { - type: Number, - required: true, - min: 1, + eventData: { + type: Schema.Types.Mixed, + default: null, + }, + processedAt: { + type: Date, + default: null, + }, + status: { + type: String, + required: [true, 'Status is required'], + enum: ['pending', 'processed', 'failed'], + default: 'pending', + index: true, }, - cursor: { + errorMessage: { type: String, trim: true, + default: null, }, }, { timestamps: true, - }, + toJSON: { + transform: (_, ret) => { + ret.id = ret._id; + delete ret._id; + delete ret.__v; + return ret; + }, + }, + } ); -/** - * Each (eventType, contractId) pair can have at most one cursor/ledger - * position so the poller always knows exactly where to resume. - */ -EventLogSchema.index({ eventType: 1, contractId: 1 }, { unique: true }); +// Compound index to prevent duplicate processing +EventLogSchema.index( + { transactionHash: 1, eventType: 1 }, + { unique: true } +); -const EventLog = mongoose.model('EventLog', EventLogSchema); +// Index for efficient querying of unprocessed events +EventLogSchema.index({ status: 1, createdAt: 1 }); -export default EventLog; -export { EventLog }; +// Index for ledger sequence range queries +EventLogSchema.index({ ledgerSequence: 1, eventType: 1 }); + +// Pre-save middleware to set processedAt if status is processed +EventLogSchema.pre('save', function(next) { + if (this.status === 'processed' && !this.processedAt) { + this.processedAt = new Date(); + } + next(); +}); +// Static method to mark events as processed +EventLogSchema.static('markAsProcessed', async function( + this: IEventLogModel, + transactionHash: string, + eventType: string +): Promise { + return this.findOneAndUpdate( + { transactionHash, eventType }, + { status: 'processed', processedAt: new Date() }, + { new: true } + ); +}); + +// Static method to get the last processed ledger sequence +EventLogSchema.static('getLastProcessedLedger', async function( + this: IEventLogModel, + eventType?: string +): Promise { + const query = eventType ? { eventType, status: 'processed' } : { status: 'processed' }; + const lastEvent = await this.findOne(query) + .sort({ ledgerSequence: -1 }) + .limit(1); + return lastEvent?.ledgerSequence || 0; +}); + +// Static method to check if an event already exists +EventLogSchema.static('eventExists', async function( + this: IEventLogModel, + transactionHash: string, + eventType: string +): Promise { + const event = await this.findOne({ transactionHash, eventType }); + return !!event; +}); + +const EventLog = mongoose.model('EventLog', EventLogSchema); + +export default EventLog; diff --git a/src/routes/eventLogRoutes.ts b/src/routes/eventLogRoutes.ts new file mode 100644 index 0000000..f85a890 --- /dev/null +++ b/src/routes/eventLogRoutes.ts @@ -0,0 +1,37 @@ +import { Router } from 'express'; +import eventLogController from '../controllers/eventLogController'; +import { authenticate } from '../middleware/authenticate'; +import { requireRole } from '../middleware/requireRole'; + +const router = Router(); + +// All routes require authentication +router.use(authenticate); + +// Get last processed ledger sequence +router.get( + '/last-processed', + eventLogController.getLastProcessedLedger.bind(eventLogController) +); + +// Get unprocessed events +router.get( + '/unprocessed', + requireRole(['admin', 'monitor']), + eventLogController.getUnprocessedEvents.bind(eventLogController) +); + +// Get events by ledger range +router.get( + '/range', + requireRole(['admin', 'monitor']), + eventLogController.getEventsByLedgerRange.bind(eventLogController) +); + +// Get event by transaction hash +router.get( + '/transaction/:hash', + eventLogController.getEventByTransactionHash.bind(eventLogController) +); + +export default router; diff --git a/src/routes/index.ts b/src/routes/index.ts index a17e8e3..79f3bc9 100644 --- a/src/routes/index.ts +++ b/src/routes/index.ts @@ -1,3 +1,4 @@ +// @ts-ignore: express types may be missing in this project setup import { Router } from 'express'; import authRoutes from './authRoutes'; import deliveryCrudRoutes from './delivery.routes'; @@ -6,7 +7,7 @@ import adminRoutes from './adminRoutes'; import driverRoutes from './driverRoutes'; import fleetRoutes from './fleetRoutes'; import disputeRoutes from './disputeRoutes'; -import indexerRoutes from './indexerRoutes'; +import eventLogRoutes from './eventLogRoutes'; const router = Router(); @@ -17,6 +18,6 @@ router.use('/v1/admin', adminRoutes); router.use('/v1/drivers', driverRoutes); router.use('/v1/fleets', fleetRoutes); router.use('/v1/disputes', disputeRoutes); -router.use('/v1/indexer', indexerRoutes); +router.use('/v1/eventlog', eventLogRoutes); -export default router; +export default router; \ No newline at end of file diff --git a/src/services/eventLogService.ts b/src/services/eventLogService.ts new file mode 100644 index 0000000..8562b71 --- /dev/null +++ b/src/services/eventLogService.ts @@ -0,0 +1,128 @@ +import EventLog, { IEventLog } from '../models/EventLog'; +import logger from '../config/logger'; + +export class EventLogService { + /** + * Create a new event log entry + */ + async createEventLog(eventData: Partial): Promise { + try { + // Check if event already exists + if (eventData.transactionHash && eventData.eventType) { + const exists = await EventLog.eventExists( + eventData.transactionHash, + eventData.eventType + ); + if (exists) { + throw new Error('Event already exists'); + } + } + + const eventLog = new EventLog(eventData); + await eventLog.save(); + logger.info(`Event log created: ${eventLog.id} for transaction ${eventLog.transactionHash}`); + return eventLog; + } catch (error) { + logger.error('Error creating event log:', error); + throw error; + } + } + + /** + * Mark an event as processed + */ + async markAsProcessed(transactionHash: string, eventType: string): Promise { + try { + const updated = await EventLog.markAsProcessed(transactionHash, eventType); + if (updated) { + logger.info(`Event marked as processed: ${updated.id}`); + } + return updated; + } catch (error) { + logger.error('Error marking event as processed:', error); + throw error; + } + } + + /** + * Get the last processed ledger sequence + */ + async getLastProcessedLedger(eventType?: string): Promise { + try { + return await EventLog.getLastProcessedLedger(eventType); + } catch (error) { + logger.error('Error getting last processed ledger:', error); + return 0; + } + } + + /** + * Get all unprocessed events + */ + async getUnprocessedEvents(): Promise { + try { + return await EventLog.find({ status: 'pending' }) + .sort({ createdAt: 1 }) + .limit(100); + } catch (error) { + logger.error('Error fetching unprocessed events:', error); + return []; + } + } + + /** + * Get events by ledger sequence range + */ + async getEventsByLedgerRange( + startLedger: number, + endLedger: number, + eventType?: string + ): Promise { + try { + const query: any = { + ledgerSequence: { $gte: startLedger, $lte: endLedger } + }; + if (eventType) { + query.eventType = eventType; + } + return await EventLog.find(query).sort({ ledgerSequence: 1 }); + } catch (error) { + logger.error('Error fetching events by ledger range:', error); + return []; + } + } + + /** + * Get event by transaction hash + */ + async getEventByTransactionHash(transactionHash: string): Promise { + try { + return await EventLog.findOne({ transactionHash }); + } catch (error) { + logger.error('Error fetching event by transaction hash:', error); + return null; + } + } + + /** + * Update event status with error + */ + async markEventFailed(eventId: string, errorMessage: string): Promise { + try { + const event = await EventLog.findByIdAndUpdate( + eventId, + { status: 'failed', errorMessage }, + { new: true } + ); + if (event) { + logger.warn(`Event ${eventId} marked as failed: ${errorMessage}`); + } + return event; + } catch (error) { + logger.error('Error marking event as failed:', error); + return null; + } + } +} + +export default new EventLogService(); diff --git a/tests/eventLog.test.ts b/tests/eventLog.test.ts new file mode 100644 index 0000000..baf0f0d --- /dev/null +++ b/tests/eventLog.test.ts @@ -0,0 +1,138 @@ +import mongoose from 'mongoose'; +import { MongoMemoryServer } from 'mongodb-memory-server'; +import EventLog from '../src/models/EventLog'; +import eventLogService from '../src/services/eventLogService'; + +describe('EventLog Model & Service', () => { + let mongoServer: MongoMemoryServer; + + beforeAll(async () => { + mongoServer = await MongoMemoryServer.create(); + const uri = mongoServer.getUri(); + await mongoose.connect(uri); + }); + + afterAll(async () => { + await mongoose.disconnect(); + await mongoServer.stop(); + }); + + afterEach(async () => { + await EventLog.deleteMany({}); + }); + + describe('Model', () => { + it('should create an event log entry', async () => { + const eventData = { + eventType: 'delivery', + transactionHash: '0x1234567890abcdef', + ledgerSequence: 1000, + contractId: 'C1', + eventData: { amount: 100, recipient: 'GB123...' }, + status: 'pending' as const, + }; + + const event = new EventLog(eventData); + await event.save(); + + expect(event.id).toBeDefined(); + expect(event.eventType).toBe('delivery'); + expect(event.transactionHash).toBe('0x1234567890abcdef'); + expect(event.ledgerSequence).toBe(1000); + expect(event.status).toBe('pending'); + }); + + it('should reject duplicate transactionHash + eventType via service', async () => { + const eventData = { + eventType: 'escrow', + transactionHash: '0x1234567890abcdef', + ledgerSequence: 1000, + status: 'pending' as const, + }; + + // Create first event via service + await eventLogService.createEventLog(eventData); + + // Try to create duplicate via service - should throw "Event already exists" + await expect(eventLogService.createEventLog(eventData)).rejects.toThrow('Event already exists'); + }); + + it('should require required fields', async () => { + const event = new EventLog({}); + await expect(event.save()).rejects.toThrow(); + }); + }); + + describe('Service', () => { + it('should create an event log entry', async () => { + const event = await eventLogService.createEventLog({ + eventType: 'delivery', + transactionHash: '0x1234567890abcdef', + ledgerSequence: 1000, + status: 'pending', + }); + + expect(event).toBeDefined(); + expect(event.eventType).toBe('delivery'); + expect(event.transactionHash).toBe('0x1234567890abcdef'); + }); + + it('should mark event as processed', async () => { + const event = await eventLogService.createEventLog({ + eventType: 'delivery', + transactionHash: '0x1234567890abcdef', + ledgerSequence: 1000, + status: 'pending', + }); + + const updated = await eventLogService.markAsProcessed( + '0x1234567890abcdef', + 'delivery' + ); + + expect(updated).toBeDefined(); + expect(updated?.status).toBe('processed'); + expect(updated?.processedAt).toBeDefined(); + }); + + it('should get last processed ledger', async () => { + await eventLogService.createEventLog({ + eventType: 'delivery', + transactionHash: '0x1234567890abcdef', + ledgerSequence: 1000, + status: 'processed', + }); + + await eventLogService.createEventLog({ + eventType: 'delivery', + transactionHash: '0xabcdef1234567890', + ledgerSequence: 2000, + status: 'processed', + }); + + const lastLedger = await eventLogService.getLastProcessedLedger(); + expect(lastLedger).toBe(2000); + }); + + it('should check if event exists', async () => { + await eventLogService.createEventLog({ + eventType: 'delivery', + transactionHash: '0x1234567890abcdef', + ledgerSequence: 1000, + status: 'pending', + }); + + const exists = await EventLog.eventExists( + '0x1234567890abcdef', + 'delivery' + ); + expect(exists).toBe(true); + + const notExists = await EventLog.eventExists( + '0xabcdef1234567890', + 'delivery' + ); + expect(notExists).toBe(false); + }); + }); +});