|
| 1 | +================================================================================ |
| 2 | + GITHUB ISSUE #39 — PART 2: IMPLEMENTATION COMPLETE |
| 3 | +================================================================================ |
| 4 | + |
| 5 | +DATE: 2026-08-29 |
| 6 | +ISSUE: #39 - Implement indexer handler for escrow_released and |
| 7 | + escrow_refunded events |
| 8 | +TASK: PART 2 OF 4 — IMPLEMENT HANDLERS |
| 9 | +STATUS: ✅ IMPLEMENTATION COMPLETE |
| 10 | + |
| 11 | +================================================================================ |
| 12 | + IMPLEMENTATION SUMMARY |
| 13 | +================================================================================ |
| 14 | + |
| 15 | +All handlers implemented following the EXACT pattern from Part 1 analysis: |
| 16 | + • Controller → Service → Model layered architecture |
| 17 | + • Event parsing with null-safe validation |
| 18 | + • Idempotent operations using transaction hashes |
| 19 | + • Distributed locking for concurrency control |
| 20 | + • Comprehensive error handling and logging |
| 21 | + |
| 22 | +================================================================================ |
| 23 | + FILES MODIFIED & CREATED |
| 24 | +================================================================================ |
| 25 | + |
| 26 | +1. src/indexer/escrowHandlers.ts (391 lines) |
| 27 | + ──────────────────────────────────────── |
| 28 | + NEW INTERFACES: |
| 29 | + ✓ EscrowReleasedEventData |
| 30 | + ✓ EscrowRefundedEventData |
| 31 | + |
| 32 | + NEW PARSER FUNCTIONS: |
| 33 | + ✓ parseEscrowReleasedEvent() — Extracts event data from Soroban XDR |
| 34 | + ✓ parseEscrowRefundedEvent() — Extracts event data from Soroban XDR |
| 35 | + |
| 36 | + NEW HANDLER FUNCTIONS: |
| 37 | + ✓ handleEscrowReleasedEvent() — Process single escrow_released event |
| 38 | + ✓ handleEscrowRefundedEvent() — Process single escrow_refunded event |
| 39 | + |
| 40 | + NEW SYNC FUNCTIONS: |
| 41 | + ✓ syncEscrowReleasedEvents() — Poll RPC and batch process events |
| 42 | + ✓ syncEscrowRefundedEvents() — Poll RPC and batch process events |
| 43 | + |
| 44 | + FLOW: |
| 45 | + Soroban RPC getEvents() → Parse → Handle → Service → Update MongoDB |
| 46 | + |
| 47 | + |
| 48 | +2. src/services/escrow.service.ts (300 lines) |
| 49 | + ───────────────────────────────────────── |
| 50 | + NEW INTERFACES: |
| 51 | + ✓ RefundEscrowInput — Input data for refund operations |
| 52 | + |
| 53 | + NEW SERVICE METHOD: |
| 54 | + ✓ refundEscrow(input: RefundEscrowInput): Promise<IEscrow> |
| 55 | + - Uses distributed locking (via Redis withLock) |
| 56 | + - Validates escrow state (must be LOCKED) |
| 57 | + - Prevents double-refund via transaction hash tracking |
| 58 | + - Updates escrow status to REFUNDED |
| 59 | + - Updates delivery status to CANCELLED |
| 60 | + - Idempotent — replaying same tx hash is a no-op |
| 61 | + |
| 62 | + PATTERN FOLLOWS: |
| 63 | + ✓ releaseEscrow() method already in service |
| 64 | + ✓ refundEscrow() mirrors the same pattern |
| 65 | + |
| 66 | +================================================================================ |
| 67 | + HANDLER IMPLEMENTATION DETAILS |
| 68 | +================================================================================ |
| 69 | + |
| 70 | +STEP 1: EVENT PARSING |
| 71 | +──────────────────── |
| 72 | + |
| 73 | +parseEscrowReleasedEvent(): |
| 74 | + • Extract deliveryId from event.topic[1] (XDR-decoded) |
| 75 | + • Extract amount from event.value (data map) |
| 76 | + • Handle bigint → number conversion |
| 77 | + • Validate required fields |
| 78 | + • Return typed EscrowReleasedEventData or null |
| 79 | + • Log parse failures without crashing indexer |
| 80 | + |
| 81 | +parseEscrowRefundedEvent(): |
| 82 | + • Same pattern as released |
| 83 | + • Extract refundedTo from data map |
| 84 | + • Null-safe extraction (try multiple field names) |
| 85 | + |
| 86 | +STEP 2: SINGLE EVENT HANDLING |
| 87 | +────────────────────────────── |
| 88 | + |
| 89 | +handleEscrowReleasedEvent(): |
| 90 | + 1. Parse event |
| 91 | + 2. If parse fails → return {status: 'ignored', reason: 'unparseable'} |
| 92 | + 3. Create ReleaseEscrowInput with: |
| 93 | + - escrowId: contractId |
| 94 | + - transactionHash: event.txHash |
| 95 | + - ledger: event.ledger |
| 96 | + - releasedBy: parsed.releasedTo |
| 97 | + 4. Call escrowService.releaseEscrow(input) |
| 98 | + 5. Return {status: 'processed', transactionHash} |
| 99 | + |
| 100 | +handleEscrowRefundedEvent(): |
| 101 | + 1. Parse event |
| 102 | + 2. If parse fails → return {status: 'ignored', reason: 'unparseable'} |
| 103 | + 3. Create RefundEscrowInput with: |
| 104 | + - escrowId: contractId |
| 105 | + - transactionHash: event.txHash |
| 106 | + - ledger: event.ledger |
| 107 | + - refundedBy: parsed.refundedTo |
| 108 | + 4. Call escrowService.refundEscrow(input) |
| 109 | + 5. Return {status: 'processed', transactionHash} |
| 110 | + |
| 111 | +STEP 3: BATCH POLLING & PROCESSING |
| 112 | +─────────────────────────────────── |
| 113 | + |
| 114 | +syncEscrowReleasedEvents(startLedger, contractId): |
| 115 | + 1. Validate contractId exists |
| 116 | + 2. Read ESCROW_RELEASED_EVENT_TOPIC from env (default: 'escrow_released') |
| 117 | + 3. Query sorobanRpcClient.getEvents() with: |
| 118 | + - startLedger: inclusive start position |
| 119 | + - filters: [{ type: 'contract', contractIds, topics: [eventSymbol, '*'] }] |
| 120 | + 4. For each event: |
| 121 | + - Call handleEscrowReleasedEvent(event, contractId) |
| 122 | + - Collect result |
| 123 | + 5. Return EscrowSyncSummary: |
| 124 | + - latestLedger: from RPC response |
| 125 | + - cursor: resume position for next sync |
| 126 | + - processed: count of successful handles |
| 127 | + - ignored: count of skipped events |
| 128 | + - results: array of individual results |
| 129 | + |
| 130 | +syncEscrowRefundedEvents(startLedger, contractId): |
| 131 | + • Same pattern as released |
| 132 | + • Reads ESCROW_REFUNDED_EVENT_TOPIC from env (default: 'escrow_refunded') |
| 133 | + |
| 134 | +================================================================================ |
| 135 | + SERVICE METHOD: refundEscrow() |
| 136 | +================================================================================ |
| 137 | + |
| 138 | +Method Signature: |
| 139 | +───────────────── |
| 140 | +async refundEscrow(input: RefundEscrowInput): Promise<IEscrow> |
| 141 | + |
| 142 | +Input: |
| 143 | +────── |
| 144 | +interface RefundEscrowInput { |
| 145 | + escrowId: string; // ObjectId or contractId |
| 146 | + transactionHash: string; // From event.txHash |
| 147 | + ledger?: number; // From event.ledger |
| 148 | + refundedBy?: string; // From parsed event data |
| 149 | +} |
| 150 | + |
| 151 | +Implementation: |
| 152 | +─────────────── |
| 153 | + |
| 154 | +1. VALIDATE INPUT |
| 155 | + - escrowId must be valid ObjectId or contract id (starts with 'C') |
| 156 | + - Throw AppError if invalid |
| 157 | + |
| 158 | +2. ACQUIRE DISTRIBUTED LOCK |
| 159 | + - Lock key: `escrow:refund:${escrowId}` |
| 160 | + - Redis withLock() handles acquire/release |
| 161 | + - Prevents concurrent refund operations |
| 162 | + |
| 163 | +3. FETCH ESCROW |
| 164 | + - If ObjectId → findById() |
| 165 | + - If contractId → findOne({ contractId }) |
| 166 | + - Throw if not found |
| 167 | + |
| 168 | +4. VALIDATE STATE TRANSITION |
| 169 | + - Reject if already REFUNDED |
| 170 | + - Reject if not LOCKED |
| 171 | + - Only LOCKED escrows can be refunded |
| 172 | + |
| 173 | +5. CHECK IDEMPOTENCY |
| 174 | + - Search transactions[] for matching hash |
| 175 | + - If found → return existing escrow (no-op) |
| 176 | + - Prevents duplicate processing |
| 177 | + |
| 178 | +6. RECORD TRANSACTION |
| 179 | + - Add to escrow.transactions[] |
| 180 | + - Type: 'refund' |
| 181 | + - Include hash, ledger, timestamp |
| 182 | + |
| 183 | +7. UPDATE ESCROW |
| 184 | + - lockStatus = REFUNDED |
| 185 | + - refundedAt = new Date() |
| 186 | + - Save to MongoDB |
| 187 | + |
| 188 | +8. UPDATE DELIVERY |
| 189 | + - Find delivery by escrow.delivery |
| 190 | + - Update status to CANCELLED |
| 191 | + - Save to MongoDB |
| 192 | + |
| 193 | +9. LOG & RETURN |
| 194 | + - Info log: successful refund |
| 195 | + - Return updated escrow |
| 196 | + |
| 197 | +State Machine: |
| 198 | +────────────── |
| 199 | +LOCKED --[refund]--> REFUNDED |
| 200 | +(Only valid transition for refund) |
| 201 | + |
| 202 | +Delivery Status Update: |
| 203 | +────────────────────── |
| 204 | +ANY --> CANCELLED (when escrow refunded) |
| 205 | + |
| 206 | +================================================================================ |
| 207 | + ERROR HANDLING |
| 208 | +================================================================================ |
| 209 | + |
| 210 | +Parse Errors: |
| 211 | +───────────── |
| 212 | +• Malformed XDR → return null |
| 213 | +• Missing required fields → return null |
| 214 | +• Type coercion failures → return null |
| 215 | +• Log warning, do NOT crash indexer |
| 216 | + |
| 217 | +Handler Errors: |
| 218 | +─────────────── |
| 219 | +• Service throws error → caught at handler level |
| 220 | +• Parse failure → status: 'ignored' |
| 221 | +• Service success → status: 'processed' |
| 222 | + |
| 223 | +Service Errors: |
| 224 | +─────────────── |
| 225 | +• Invalid format → AppError (BAD_REQUEST) |
| 226 | +• Escrow not found → AppError (NOT_FOUND) |
| 227 | +• State conflict → AppError (CONFLICT) |
| 228 | +• Lock failure → withLock() handles retry |
| 229 | + |
| 230 | +All errors include: |
| 231 | + • Human-readable message |
| 232 | + • Contextual data (escrowId, txHash, etc.) |
| 233 | + • Proper HTTP status code |
| 234 | + |
| 235 | +================================================================================ |
| 236 | + ENVIRONMENT VARIABLES |
| 237 | +================================================================================ |
| 238 | + |
| 239 | +REQUIRED (existing): |
| 240 | +──────────────────── |
| 241 | +ESCROW_CONTRACT_ID # Soroban contract id |
| 242 | + |
| 243 | +NEW (optional with defaults): |
| 244 | +────────────────────────────── |
| 245 | +ESCROW_RELEASED_EVENT_TOPIC # Default: 'escrow_released' |
| 246 | +ESCROW_REFUNDED_EVENT_TOPIC # Default: 'escrow_refunded' |
| 247 | + |
| 248 | +These are read at sync time from process.env with fallback defaults. |
| 249 | + |
| 250 | +================================================================================ |
| 251 | + ARCHITECTURE COMPLIANCE |
| 252 | +================================================================================ |
| 253 | + |
| 254 | +✓ Controller → Service → Model Pattern |
| 255 | + - Handlers delegate to service only |
| 256 | + - Service calls model (Escrow, Delivery) |
| 257 | + - No direct DB calls in handlers |
| 258 | + |
| 259 | +✓ Event-Driven Architecture |
| 260 | + - Events flow: RPC → Parse → Handle → Service → DB |
| 261 | + - Clear separation of concerns |
| 262 | + - Each layer has single responsibility |
| 263 | + |
| 264 | +✓ Idempotency & Concurrency |
| 265 | + - Transaction hashes prevent duplicates |
| 266 | + - Distributed Redis locks prevent race conditions |
| 267 | + - Replaying events is safe |
| 268 | + |
| 269 | +✓ Comprehensive Logging |
| 270 | + - Debug: lock acquisition, state transitions |
| 271 | + - Info: successful operations, processing stats |
| 272 | + - Warn: parse failures, conflicts |
| 273 | + - Error: exceptions with context |
| 274 | + |
| 275 | +✓ Type Safety |
| 276 | + - All event data typed (EscrowReleasedEventData, etc.) |
| 277 | + - Input interfaces (ReleaseEscrowInput, RefundEscrowInput) |
| 278 | + - Return types clearly defined |
| 279 | + |
| 280 | +✓ No Hardcoded Values |
| 281 | + - Event topics read from env |
| 282 | + - Contract id from config |
| 283 | + - All constants externalized |
| 284 | + |
| 285 | +================================================================================ |
| 286 | + INTEGRATION POINTS |
| 287 | +================================================================================ |
| 288 | + |
| 289 | +These handlers will be called by: |
| 290 | + • eventPoller.ts (to be updated in Part 3) |
| 291 | + • Direct indexing jobs |
| 292 | + • Manual re-syncing utilities |
| 293 | + |
| 294 | +The handlers integrate with: |
| 295 | + • EscrowService (existing & extended) |
| 296 | + • Escrow model (existing) |
| 297 | + • Delivery model (existing) |
| 298 | + • Soroban RPC client (existing) |
| 299 | + • Redis/distributed locking (existing) |
| 300 | + • Logger (existing) |
| 301 | + |
| 302 | +No new dependencies required. |
| 303 | +All integration points already exist in codebase. |
| 304 | + |
| 305 | +================================================================================ |
| 306 | + NEXT STEPS (PART 3) |
| 307 | +================================================================================ |
| 308 | + |
| 309 | +1. Update eventPoller.ts to call the new sync functions |
| 310 | +2. Register handlers with main indexer loop |
| 311 | +3. Add monitoring/metrics collection |
| 312 | +4. Create comprehensive test suite (Part 4) |
| 313 | + |
| 314 | +================================================================================ |
| 315 | + SUMMARY |
| 316 | +================================================================================ |
| 317 | + |
| 318 | +✅ IMPLEMENTATION COMPLETE |
| 319 | + |
| 320 | +All handlers implemented following exact Part 1 architecture: |
| 321 | + • 2 new event interfaces (Released, Refunded) |
| 322 | + • 2 parser functions (null-safe, comprehensive validation) |
| 323 | + • 2 handler functions (single event processing) |
| 324 | + • 2 sync functions (batch RPC polling) |
| 325 | + • 1 service method (refundEscrow with distributed locking) |
| 326 | + |
| 327 | +Code Quality: |
| 328 | + • 391 lines of handler code (well-structured, documented) |
| 329 | + • 300+ lines of service code (matches existing patterns) |
| 330 | + • Full TypeScript typing |
| 331 | + • Comprehensive error handling |
| 332 | + • Production-ready logging |
| 333 | + |
| 334 | +Architecture: |
| 335 | + • Strict Controller → Service → Model enforcement |
| 336 | + • Idempotent operations |
| 337 | + • Distributed locking for concurrency |
| 338 | + • Zero hardcoded values |
| 339 | + • Full compliance with existing patterns |
| 340 | + |
| 341 | +READY FOR PART 3: Integration & Testing |
| 342 | + |
| 343 | +================================================================================ |
0 commit comments