test in webdev
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β TRUTHBOUNTY API β
β (NestJS Application) β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β
βββββββββββββ΄ββββββββββββ
β β
βββββββββββββΌββββββββββ ββββββββββββΌβββββββββββ
β Existing API β β Event Indexer β
β (App Controller) β β (NEW COMPONENT) β
βββββββββββββββββββββββ ββββββββββββ¬ββββββββββ
β
βββββββββββββββββββββββββΌββββββββββββββββββββββββ
β β β
βΌ βΌ βΌ
ββββββββββββββββββββ ββββββββββββββββββββ ββββββββββββββββββββ
β Config Module β β Indexer Service β β Indexer Module β
β β β (Core Logic) β β (Lifecycle Mgmt) β
β β’ RPC URL β β β β β
β β’ Contracts β β β’ Event Polling β β β’ onModuleInit β
β β’ DB Params β β β’ RPC Queries β β β’ onModuleDestroyβ
β β’ Thresholds β β β’ Decoding β β β
ββββββββββββββββββββ β β’ Deduplication β ββββββββββββββββββββ
β β’ Reorg Safety β
β β’ Retry Logic β
ββββββββββββ¬ββββββββ
β
ββββββββββββββββββββββΌβββββββββββββββββββββ
β β β
βΌ βΌ βΌ
ββββββββββββββββββββ ββββββββββββββββββββ ββββββββββββββββββββ
β Indexer REST API β β Optimism RPC β β PostgreSQL DB β
β (Controller) β β β β β
β β β β’ eth_blockNumberβ β βββββββββββββββββββ
β β’ GET /status β β β’ eth_getLogs β β β indexed_events ββ
β β’ POST /restart β β β’ eth_getBlock β β β (event data) ββ
β β’ POST /backfill β β β β βββββββββββββββββββ
ββββββββββββββββββββ ββββββββββββββββββββ β βββββββββββββββββββ
β β indexing_state ββ
β β (progress) ββ
β βββββββββββββββββββ
ββββββββββββββββββββ
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β POLLING LOOP (Every 12s) β
β [EventIndexerService] β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β β
β 1. Get Current Block Number β
β βββ await provider.getBlockNumber() β
β β β
β 2. For Each Configured Contract β
β βββ Get lastProcessedBlockNumber from DB β
β β β
β 3. Fetch Events from RPC β
β βββ eth_getLogs(address, topics, fromBlock, toBlock) β
β β β
β 4. Process Each Event β
β βββ Check if already indexed (idempotency) β
β βββ Decode event using ethers.Interface β
β βββ Calculate confirmations β
β βββ Determine finalization status β
β βββ Store in indexed_events table β
β β β
β 5. Update Indexing State β
β βββ Set lastProcessedBlockNumber β
β βββ Mark status as 'idle' β
β β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β RECONCILIATION LOOP (During Polling) β
β [EventIndexerService.reconcileReorgs] β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β β
β For Each Finalized Event in Database: β
β β
β confirmations = currentBlockNumber - eventBlockNumber β
β β
β IF confirmations >= THRESHOLD (e.g., 12) β
β βββ Keep as finalized β β
β β
β ELSE IF confirmations < THRESHOLD β
β βββ Mark as unfinalized β
β βββ Reset isProcessed = false β
β βββ Clear processingError β
β βββ Allow re-processing ββ REORG RECOVERY β
β β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Event Deduplication Strategy β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β β
β Level 1: Application Check β
β ββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
β β SELECT * FROM indexed_events β β
β β WHERE transaction_hash = ? β β
β β AND log_index = ? β β
β β AND event_type = ? β β
β β LIMIT 1; β β
β β β β
β β IF found: SKIP (already indexed) β β
β β ELSE: proceed to Level 2 β β
β ββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
β β β
β Level 2: Database Constraint β
β ββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
β β UNIQUE (transaction_hash, log_index, event_type) β β
β β β β
β β Prevents any duplicate inserts at DB level β β
β ββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
β β
β Result: Guaranteed single processing even with: β
β β’ Duplicate RPC responses β
β β’ Service restarts β
β β’ Multiple indexer instances β
β β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
βββββββββββββββββββββββ
β RPC Event Fetched β
ββββββββββββ¬βββββββββββ
β
ββββββββββββΌβββββββββββ
β Check Idempotency β
β (Already indexed?) β
ββββββββββββ¬βββββββββββ
β
βββββββββββββββ΄βββββββββββββ
β β
YES β β NO
βΌ βΌ
ββββββββββββββββ ββββββββββββββββββββ
β SKIP β β Decode Event β
β (return) β β (Parse logs) β
ββββββββββββββββ ββββββββββ¬ββββββββββ
β
βββββββββΌβββββββββ
β Store in DB β
β indexed_events β
βββββββββ¬βββββββββ
β
ββββββββββββββββββΌβββββββββββββββββ
β Check Confirmations β
β threshold_met = (conf >= 12)? β
ββββββββββββββ¬βββββββββββββββββββββ
β
ββββββββββββββ΄βββββββββββββ
β β
YES β β NO
βΌ βΌ
ββββββββββββββββββββββ ββββββββββββββββββββββββ
β isFinalized=true β β isFinalized=false β
β Ready for syncing β β Await more blocks β
ββββββββββββββββββββββ ββββββββββββββββββββββββ
β β
ββββββββββββββ¬βββββββββββββ
β
ββββββββββββββββββββββββββΌβββββββββββββββββββββ
β Check Reorg Risk (Next Reconciliation Loop) β
β if conf < 12: mark unfinalized, reprocess β
βββββββββββββββββββββββββββββββββββββββββββββββ
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β IndexingState Table β
β βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
β β id (UUID) β β
β β chainId + contractAddress + eventType (UNIQUE) β β
β β lastProcessedBlockNumber βββ Resume point β β
β β status (idle/indexing/backfilling/error) β β
β β totalEventCount, processedEventCount, failedEventCount β β
β ββββββββββββββββββββββββ¬βββββββββββββββββββββββββββββββββ β
β β (1:N) β
β β β
β ββββββββββββββββββββββββΌβββββββββββββββββββββββββββββββββ β
β β IndexedEvent Table β β
β β ββββββββββββββββββββββββββββββββββββββββββββββββββββ β β
β β β id (UUID) β β β
β β β eventType (foreign key to IndexingState) β β β
β β β transactionHash + logIndex + eventType (UNIQUE) β β β
β β β blockNumber + logIndex (UNIQUE) β β β
β β β contractAddress β β β
β β β eventData (JSONB - raw RPC response) β β β
β β β parsedData (JSONB - decoded parameters) β β β
β β β confirmations β β β
β β β isFinalized (reorg safety) β β β
β β β isProcessed (downstream syncing) β β β
β β β processingError (error tracking) β β β
β β β retryAttempts (retry count) β β β
β β β createdAt, updatedAt (timestamps) β β β
β β ββββββββββββββββββββββββββββββββββββββββββββββββββββ β β
β ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
β β
β Indexes: β
β β’ (blockNumber, logIndex) - UNIQUE β
β β’ (transactionHash, logIndex, eventType) - UNIQUE β
β β’ (eventType, blockNumber) - for filtered queries β
β β’ (processedAt) - for time-based queries β
β β’ (isProcessed) - for finding unprocessed events β
β β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
βββββββββββββββββββββββ
β Optimism Chain β
β Contract Event β
β emitted() β
ββββββββββββ¬βββββββββββ
β
βΌ
ββββββββββββββββββββββββββββββββ
β Optimism RPC Node β
β (eth_getLogs API) β
ββββββββββββ¬ββββββββββββββββββββ
β
βΌ
ββββββββββββββββββββββββββββββββ
β EventIndexerService β
β β’ Fetch logs β
β β’ Parse/decode β
β β’ Check idempotency β
β β’ Calculate confirmations β
β β’ Handle errors/reorgs β
ββββββββββββ¬ββββββββββββββββββββ
β
βΌ
ββββββββββββββββββββββββββββββββ
β PostgreSQL indexed_events β
β β’ Raw event data β
β β’ Parsed parameters β
β β’ Finalization status β
β β’ Processing state β
ββββββββββββ¬ββββββββββββββββββββ
β
βΌ
ββββββββββββββββββββββββββββββββ
β Downstream Modules (Future) β
β β’ Reward Syncing β
β β’ Stake Syncing β
β β’ GraphQL API β
β β’ WebSocket Updates β
ββββββββββββββββββββββββββββββββ
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Production Setup β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β β
β ββββββββββββββββββββ ββββββββββββββββββββ β
β β Optimism RPC β β PostgreSQL β β
β β (Alchemy/ βββββββββββ (AWS RDS/ β β
β β Infura/Node) β β Docker/ β β
β β β β Self-hosted) β β
β ββββββββββββββββββββ ββββββββββββββββββββ β
β β² β² β
β β β β
β ββββββββββββββββββββ¬ββββββββββββ β
β β β
β ββββββββΌβββββββ β
β β NestJS App β β
β β β’ Indexer β β
β β β’ REST API β β
β ββββββββ¬βββββββ β
β β β
β βββββββββββ΄ββββββββββ β
β β β β
β βββββββΌβββββ βββββββΌβββββ β
β β Docker β β Load β β
β β Containerβ β Balancer β β
β ββββββββββββ ββββββββββββ β
β β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Configuration Loading Order β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β β
β 1. Environment Variables (highest priority) β
β ββ OPTIMISM_RPC_URL β
β ββ DATABASE_HOST β
β ββ INDEXED_CONTRACTS β
β β
β 2. .env.local (if exists) β
β ββ Local overrides β
β β
β 3. .env (default) β
β ββ Project defaults β
β β
β 4. Hardcoded Defaults in Code (lowest priority) β
β ββ If env var not found β
β ββ Uses sensible defaults β
β β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β TRANSACTIONAL OUTBOX FLOW β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β β
β 1. Domain Action (Prisma Transaction) β
β βββ Write Projection Data β
β βββ Write OutboxEvent (status = 'PENDING', idempotencyKey = sha256(...)) β
β β (Atomic Commit) β
β 2. OutboxScheduler (Cron Poller every 5s) β
β βββ Claims PENDING OutboxEvents β
β βββ Relays job to BullMQ 'notifications' queue β
β βββ Updates OutboxEvent status to 'DISPATCHED' β
β β β
β 3. NotificationProcessor (Worker) β
β βββ Step 3a: Redis SETNX Guard (key = idempotency:notification:${key}) β
β β’ Lock acquired β proceed to delivery β
β β’ Lock exists β suppress duplicate execution β
β βββ Step 3b: DB Fallback Guard (DeliveryHistory.findByIdempotencyKey) β
β β’ Status DELIVERED β suppress duplicate β
β βββ Step 3c: Deliver via Channel (WebSocket / Email / Webhook / InApp) β
β βββ Update DeliveryHistory status β
β β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
- Zero PII & Settlement Data:
OutboxEvent.payloadcontains opaque routing identifiers only (notificationId,channel,recipientIds). No claim body, settlement calculations, private keys, or credentials are path-logged or queued. - Protocol Boundary: API layer indexes, validates, and relays user-signed intent; it is never authoritative for settlement, rewards, or governance.
- EVM Semantics: Full compatibility with Optimism/EVM chain rules.
TypeORM/PostgreSQL is the persistence path for all new backend code. src/database/transaction.runner.ts is the shared transaction helper; use it rather than reaching for a raw DataSource or a second transaction abstraction.
Prisma (src/prisma/, prisma/schema.prisma) has pre-existing, real usage in a specific, closed list of modules: auth, notifications, outbox (see above), sybil-resistance, analytics, ai-assistant, and identity/worldcoin. That usage is grandfathered, not sanctioned for new work: it predates this boundary and migrating it off Prisma is a separate, larger effort, not part of this change.
Two things enforce the boundary going forward:
eslint.config.mjsrestricts importing@prisma/clientorprisma.serviceoutside the grandfathered file list; new files hit this at lint time.src/architecture.spec.tsasserts the same thing at test time, independent of whether lint runs.
Adding a file to either allowlist is a signal that the "TypeORM-only" boundary is being widened, not narrowed, so it should be treated the same as adding a new ORM: reviewed deliberately, not done to silence a lint error.