Skip to content

feat(indexer): decode predictify contract topics into typed events - #994

Merged
greatest0fallt1me merged 2 commits into
Predictify-org:mainfrom
solaawojobi00-bit:feat/issue-979-indexer-event-decoder
Oct 21, 2026
Merged

greatest0fallt1me merged 2 commits into
Predictify-org:mainfrom
solaawojobi00-bit:feat/issue-979-indexer-event-decoder

Conversation

@solaawojobi00-bit

@solaawojobi00-bit solaawojobi00-bit commented Oct 19, 2026 •

Copy link
Copy Markdown
Contributor

feat(indexer): decode predictify contract topics into typed events

Problem

The indexer stores every contract event as event_type = "contract" with a JSON dump of an XDR object, so nothing downstream can tell a bet from a resolution.

# Scenario on main Result
1 createSorobanRpcClient maps eventType: event.type Always "contract"; the topic (event.topic) is dropped
2 payload: event.value JSON.stringify of an xdr.ScVal writes the internal XDR layout to indexer_events.data
3 indexer_events.market_id Column exists, never set
4 findConfirmedCandidateIds in predictionsConfirmer Joins on tx_hash only, so any event in the tx confirms a prediction
5 Emitted symbols vs. event_topic_catalog.rs Contract emits short symbols (bet_plc); the catalog lists long names (bet_placed)

Solution

A pure decoder turns the topic symbol and ScVal payload into typed, validated records, and the RPC client and confirmer use it. The decoder follows what events.rs actually publishes and accepts the catalog long names as aliases.

Changes

  • src/indexer/eventDecoder.ts (new): decodePredictifyEvent(raw), pure and non-throwing. One zod schema per topic (mkt_crt, bet_plc, bet_upd, mkt_res, win_clm). topic[1] is the market_id Symbol and must equal the payload market_id; mkt_res topic[2] must equal resolution_method. i128/u64 are decimal strings, never JS numbers; addresses must be G…/C… strkeys. Unknown topics become unknown:<symbol>, bad payloads invalid:<symbol>, unreadable XDR invalid:topic; all keep the raw base64 in data.raw. Aliases: market_created, bet_placed, market_resolved, winnings_claimed.
  • src/services/indexerService.ts: mapRpcEvent (used by createSorobanRpcClient) drops non-contract events and events from other contracts, sets eventType to the topic symbol, sets marketId, and builds a JSON-safe payload. Persists market_id and data.
  • src/workers/predictionsConfirmer.ts: findConfirmedCandidateIds now requires an event with the same tx_hash, event_type = 'bet_plc', canonical = true, market_id equal to the prediction's market, and data->>'bettor' equal to the user's stellar_address.
  • src/metrics/registry.ts: indexer_events_decoded_total{event_type} and indexer_events_invalid_total{event_type} (unknown topics are labelled unknown to bound cardinality).
  • .env.example: commented testnet defaults for SOROBAN_RPC_URL / PREDICTIFY_CONTRACT_ID.
  • docs/indexer-events.md (new): topic table (symbol, topic layout, payload fields, target table), aliases, confirmer join, metrics, and the grep -n 'symbol_short!' cross-check against events.rs (contracts commit 78ce97e).
  • tests/indexerEventDecoder.test.ts, tests/predictionsConfirmerJoin.test.ts (new); tests/indexerService.recovery.test.ts (one expected-params line).

Regression Tests

# Scenario Test file Result
1 bet_plc valid payload indexerEventDecoder pass
2 win_clm with i128::MAX → 170141183460469231731687303715884105727 indexerEventDecoder pass
3 mkt_res three-element topic indexerEventDecoder pass
4 Unknown topic → unknown:foo_bar, raw XDR kept indexerEventDecoder pass
5 bet_plc missing amount → invalid:bet_plc indexerEventDecoder pass
6 Alias bet_placed decodes like bet_plc indexerEventDecoder pass
7 Non-contract event filtered before persistence indexerEventDecoder pass
8 Confirmer: tx with only an unknown:* event is not confirmed predictionsConfirmerJoin pass
9 Confirmer: bet_plc with non-matching bettor is not confirmed predictionsConfirmerJoin pass
10 Testnet fixture decodes to mkt_crt for mkt_5cae97f9_0 indexerEventDecoder pass (fixture is synthetic, see Notes)

Regression proof (row 8). With only src/workers/predictionsConfirmer.ts reverted to main, all 3 tests in predictionsConfirmerJoin.test.ts fail (rows 8 and 9 and the incrementAttempts/markFailed test); with the change restored they pass.

Testing

tests/indexerEventDecoder.test.ts (run alone)
Tests: 43 passed, 43 total
eventDecoder.ts: 100% lines, 97.8% statements, 88.2% branches

Benchmark (10,000 decodes, 5 event kinds, best of 3, CPU time): 375 ms  (limit 500 ms)

Notes for Reviewers

  • Row 10 fixture is not captured from testnet. getEvents for CCWRJX…VAFHC returned 0 events across the entire RPC retention window (ledgers 5155054–5276013), so the fixture is built from the events.rs layout and serialized to base64 as getEvents returns it. The test file says so in a comment.
  • Rows 8 and 9 are verified at the SQL level only. The tests run the real DrizzlePredictionsConfirmerRepo over a fake pg pool and assert the generated SQL and parameters; they do not execute against Postgres.
  • Merged with fix(indexer): bind canonical column and derive event identity from TOID #996. Upstream's INSERT rewrite (TOID identity, canonical binding, market_id column) landed while this was in review; this PR now uses it as-is and only routes events through the decoder (mapRpcEvent), so it makes no INSERT changes. tests/indexerService.recovery.test.ts is unchanged from main.
  • Small extension of the "tighter join": incrementAttempts and markFailed use the same bet_plc/market/bettor predicate. Otherwise a prediction whose tx has only an unknown event would never confirm and never be marked failed.
  • Benchmark margin is thin on a loaded machine (484–563 ms under CPU contention/coverage; 375 ms alone). The assertion is < 500 ms uninstrumented and relaxed to < 2500 ms when coverage instrumentation is present. Per-field zod refinements were replaced by one transform per schema and address strkeys are memoized, with faster paths for Address/i128/symbol/string/u64 (other ScVal types still use scValToNative; no parity test was added).
  • Not done: the 5-minute npm run indexer:dev testnet ingestion run (no Postgres here), and a measured coverage number for the touched confirmer code (the changed repo methods are all executed by predictionsConfirmerJoin.test.ts).
  • tests/indexerGapScan.test.ts has 2 failures in my local run. src/metrics/registry.ts on main has no indexerGapDetectedTotal, which I believe is the cause; I did not run that file on main.
  • tsc error count is identical before and after (352) with no new errors in touched files; npm run lint reports one error in src/lib/http.ts, which this PR does not touch.
  • Decoded data uses camelCase keys, and every u64 (nonce, timestamp, end_time) is a decimal string like the i128 values.

Closes #979

Decode mkt_crt, bet_plc, bet_upd, mkt_res and win_clm topics and payloads into validated records with market_id set, and require a matching bet_plc event in the predictions confirmer join.
@drips-wave

drips-wave Bot commented Oct 19, 2026

Copy link
Copy Markdown

@solaawojobi00-bit Great news! 🎉 Based on an automated assessment of this PR, the linked Wave issue(s) no longer count against your application limits.

You can now already apply to more issues while waiting for a review of this PR. Keep up the great work! 🚀

Learn more about application limits

…xer-event-decoder

# Conflicts:
#	src/services/indexerService.ts
#	src/workers/predictionsConfirmer.ts
#	tests/indexerService.recovery.test.ts
@greatest0fallt1me
greatest0fallt1me merged commit 2a23d4c into Predictify-org:main Oct 21, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Decode Predictify contract event topics and payloads into typed indexer rows for the testnet deployment

2 participants