diff --git a/docs/SYNTHETIC_EVENT_GENERATOR.md b/docs/SYNTHETIC_EVENT_GENERATOR.md new file mode 100644 index 00000000..2e61e178 --- /dev/null +++ b/docs/SYNTHETIC_EVENT_GENERATOR.md @@ -0,0 +1,32 @@ +# šŸŽ² Synthetic Event Generator for Local Development + +This document details the local event simulation and fixture generation utility for NotifyChain (Issue #699). + +--- + +## 1. Overview + +The synthetic event generator allows developers to generate schema-compliant Soroban contract events locally without depending on a live blockchain network or incurring testnet latency. + +### Safety Guarantee: +External notification delivery (e.g. Discord, Webhooks) is **strictly disabled** by default to prevent accidental spam during development. + +--- + +## 2. Usage + +### Generate events via npm: +```bash +cd listener + +# Generate 5 default synthetic events +npm run generate:events + +# Generate custom count +npx ts-node src/scripts/generate-synthetic-events.ts --count=10 +``` + +### Event Types Supported: +* `transfer`: Token payment operations. +* `task_created`: Task bounty lifecycle events. +* `bounty_awarded`: Point and settlement distributions. diff --git a/listener/package.json b/listener/package.json index cba77af1..dbd5447a 100644 --- a/listener/package.json +++ b/listener/package.json @@ -17,6 +17,7 @@ "typecheck": "node ./node_modules/typescript/bin/tsc --noEmit", "lint": "node ./node_modules/typescript/bin/tsc --noEmit", "check-migrations": "ts-node src/scripts/check-migrations.ts", + "generate:events": "ts-node src/scripts/generate-synthetic-events.ts", "validate:batch": "ts-node src/utils/batch-validator.ts" }, "keywords": [], diff --git a/listener/src/scripts/generate-synthetic-events.test.ts b/listener/src/scripts/generate-synthetic-events.test.ts new file mode 100644 index 00000000..1a87716d --- /dev/null +++ b/listener/src/scripts/generate-synthetic-events.test.ts @@ -0,0 +1,39 @@ +import { createSyntheticEvent, generateBatch } from './generate-synthetic-events'; + +describe('Synthetic Event Generator (Issue #699)', () => { + test('generates valid schema-compliant synthetic transfer events', () => { + const event = createSyntheticEvent('transfer'); + + expect(event.id).toMatch(/^syn-/); + expect(event.eventName).toBe('transfer'); + expect(event.type).toBe('contract'); + expect(event.topics).toContain('transfer'); + expect(event.data).toHaveProperty('from'); + expect(event.data).toHaveProperty('to'); + expect(event.data).toHaveProperty('amount'); + expect(event.timestamp).toBeDefined(); + }); + + test('generates batches of deterministic events', () => { + const batch = generateBatch({ count: 6 }); + + expect(batch.length).toBe(6); + expect(new Set(batch.map((e) => e.id)).size).toBe(6); + expect(new Set(batch.map((e) => e.txHash)).size).toBe(6); + }); + + test('respects custom contract addresses and event types', () => { + const customContract = 'CCONTRACTADDRESS1234567890123456789012345678901234567890'; + const batch = generateBatch({ + count: 3, + contractAddress: customContract, + eventType: 'bounty_awarded', + }); + + expect(batch.length).toBe(3); + batch.forEach((e) => { + expect(e.contractAddress).toBe(customContract); + expect(e.eventName).toBe('bounty_awarded'); + }); + }); +}); diff --git a/listener/src/scripts/generate-synthetic-events.ts b/listener/src/scripts/generate-synthetic-events.ts new file mode 100644 index 00000000..d2043567 --- /dev/null +++ b/listener/src/scripts/generate-synthetic-events.ts @@ -0,0 +1,134 @@ +#!/usr/bin/env ts-node +/** + * Synthetic Event Generator for Local Development (Issue #699) + * + * Generates schema-compliant synthetic Soroban events for local pipeline testing, + * local UI development, and benchmarking without dispatching external notifications. + */ + +import { randomUUID } from 'crypto'; + +export interface SyntheticEvent { + id: string; + contractAddress: string; + eventName: string; + ledger: number; + txHash: string; + type: 'contract' | 'system'; + topics: string[]; + data: Record; + timestamp: string; +} + +export interface GeneratorOptions { + count?: number; + contractAddress?: string; + eventType?: 'transfer' | 'task_created' | 'bounty_awarded' | 'random'; + dryRun?: boolean; +} + +const DEFAULT_CONTRACT = 'CA7QYNF7SOWQ3GLR2BGMZEHXAVIRZA4KVWLTJJFC7MGXUA64P7TV5A4W'; + +export function createSyntheticEvent( + type: 'transfer' | 'task_created' | 'bounty_awarded', + contractAddress = DEFAULT_CONTRACT, + ledgerBase = 100000 +): SyntheticEvent { + const id = `syn-${randomUUID()}`; + const txHash = randomUUID().replace(/-/g, '') + randomUUID().replace(/-/g, ''); + const timestamp = new Date().toISOString(); + const ledger = ledgerBase + Math.floor(Math.random() * 1000); + + switch (type) { + case 'transfer': + return { + id, + contractAddress, + eventName: 'transfer', + ledger, + txHash, + type: 'contract', + topics: ['transfer', 'tokens'], + data: { + from: 'GBRPYHIL2CI3WHGSUJGY6O7SROQOMJG7QBCACN4QPKUOQNXJDGONXHPA', + to: 'GDQPBW6B7G56J27V2W3K57XJ4L2J4P6Y3W56J27V2W3K57XJ4L2J4P6Y', + amount: '500.0000000', + asset: 'XLM', + }, + timestamp, + }; + + case 'task_created': + return { + id, + contractAddress, + eventName: 'task_created', + ledger, + txHash, + type: 'contract', + topics: ['task', 'created'], + data: { + taskId: Math.floor(Math.random() * 10000), + reward: '250.0000000', + deadline: Math.floor(Date.now() / 1000) + 86400 * 7, + creator: 'GBRPYHIL2CI3WHGSUJGY6O7SROQOMJG7QBCACN4QPKUOQNXJDGONXHPA', + }, + timestamp, + }; + + case 'bounty_awarded': + return { + id, + contractAddress, + eventName: 'bounty_awarded', + ledger, + txHash, + type: 'contract', + topics: ['bounty', 'awarded'], + data: { + bountyId: `bounty-${Math.floor(Math.random() * 500)}`, + recipient: 'GDQPBW6B7G56J27V2W3K57XJ4L2J4P6Y3W56J27V2W3K57XJ4L2J4P6Y', + points: 200, + }, + timestamp, + }; + } +} + +export function generateBatch(options: GeneratorOptions = {}): SyntheticEvent[] { + const count = options.count ?? 5; + const contract = options.contractAddress ?? DEFAULT_CONTRACT; + const eventTypes: Array<'transfer' | 'task_created' | 'bounty_awarded'> = [ + 'transfer', + 'task_created', + 'bounty_awarded', + ]; + + const events: SyntheticEvent[] = []; + for (let i = 0; i < count; i++) { + const selectedType = + options.eventType && options.eventType !== 'random' + ? options.eventType + : eventTypes[i % eventTypes.length]; + + events.push(createSyntheticEvent(selectedType, contract, 100000 + i * 10)); + } + + return events; +} + +function main() { + const args = process.argv.slice(2); + const countArg = args.find((a) => a.startsWith('--count=')); + const count = countArg ? parseInt(countArg.split('=')[1], 10) : 3; + + console.log(`\nšŸŽ² Generating ${count} schema-compliant synthetic events for local development...\n`); + const events = generateBatch({ count }); + + console.log(JSON.stringify(events, null, 2)); + console.log(`\nāœ… Generated ${events.length} events successfully. (External dispatch disabled)`); +} + +if (require.main === module) { + main(); +}