Appearance
EventHandlerManager
Parses raw EVM log entries into typed subscription events and dispatches them as RPC calls to user portfolio Durable Objects.
High-Level Design
EventHandlerManager serves as the parse → dispatch layer of the SubscriptionEventWatcher. It owns the smart contract address (deployed via CREATE2 on all chains) and six event signatures computed at module load via keccak256.
Data Flow
Raw RpcLogEntry[]
│
▼
parseLogEntry() ← Decode topics + data into ParsedSubscriptionEvent
│ Returns null for wrong contract / unknown sig / missing user
▼
processEventBatch() ← Group events by transactionHash (lowercased)
│
├─ Find PaymentCharged in each tx group → attach as correlatedPayment
│
▼
Per-event dispatch ← RPC call to UserPaperTradePortfolio DOPayment Correlation
Within each transaction group, any PaymentCharged event becomes correlatedPayment context passed to every handler in that transaction. This allows handlers to attach billing details (amount, token, chain) to their RPC calls.
PaymentCharged is correlation-only — it does not trigger a standalone recordPayment() call. The correlated path produces richer payment records when attached to the actual subscription event.
Event Dispatch Table
| Event | Portfolio RPC | Notes |
|---|---|---|
Subscribed | createSubscription() | Fallback to resumeSubscription() if already active |
Unsubscribed | cancelSubscription() | Starts wind-down |
SubscriptionRenewed | renewSubscription() | Attaches correlated payment (defaults amount to "0") |
PaymentCharged | (none — correlation-only) | Logged; used as context for sibling events in same tx |
SubscriptionUpgraded | upgradePlan() | Defaults to amount "0" / token "USDC" without correlated payment |
SubscriptionDowngraded | schedulePlanChange() | Deferred to next cycle |
Unknown tier indices cause the event to be skipped.
Special Handling
Subscribed create-then-resume — If createSubscription() fails with "already has an active subscription", the handler falls back to resumeSubscription(). This handles the race where the frontend already created the subscription before the watcher processed the on-chain event.
Upgrade without payment — When no PaymentCharged exists in the same transaction (e.g. 100% discount), SubscriptionUpgraded still proceeds with amount: "0" and token: "USDC" defaults rather than being skipped.
Timestamp fallback — Only PaymentCharged carries a timestamp in its log data. All other events fall back to Math.floor(Date.now() / 1000).
Error Handling
parseLogEntry()returnsnullfor: missing/unknown topic0, wrong contract address, missing topic1 (user address)- Per-event try-catch in
processEventBatch()— one failing event does not block others - Only successfully dispatched events are returned in
processedEvents; the parent DO controls cursor advancement and deduplication marking