Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 32 additions & 0 deletions docs/SYNTHETIC_EVENT_GENERATOR.md
Original file line number Diff line number Diff line change
@@ -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.
1 change: 1 addition & 0 deletions listener/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": [],
Expand Down
39 changes: 39 additions & 0 deletions listener/src/scripts/generate-synthetic-events.test.ts
Original file line number Diff line number Diff line change
@@ -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');
});
});
});
134 changes: 134 additions & 0 deletions listener/src/scripts/generate-synthetic-events.ts
Original file line number Diff line number Diff line change
@@ -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<string, unknown>;
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();
}