Repository navigation
feat(indexer): decode predictify contract topics into typed events - #994
Merged
greatest0fallt1me merged 2 commits intoOct 21, 2026
Conversation
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.
|
@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! 🚀 |
…xer-event-decoder # Conflicts: # src/services/indexerService.ts # src/workers/predictionsConfirmer.ts # tests/indexerService.recovery.test.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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.maincreateSorobanRpcClientmapseventType: event.type"contract"; the topic (event.topic) is droppedpayload: event.valueJSON.stringifyof anxdr.ScValwrites the internal XDR layout toindexer_events.dataindexer_events.market_idfindConfirmedCandidateIdsinpredictionsConfirmertx_hashonly, so any event in the tx confirms a predictionevent_topic_catalog.rsbet_plc); the catalog lists long names (bet_placed)Solution
A pure decoder turns the topic symbol and
ScValpayload into typed, validated records, and the RPC client and confirmer use it. The decoder follows whatevents.rsactually 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 themarket_idSymbol and must equal the payloadmarket_id;mkt_restopic[2]must equalresolution_method. i128/u64 are decimal strings, never JS numbers; addresses must beG…/C…strkeys. Unknown topics becomeunknown:<symbol>, bad payloadsinvalid:<symbol>, unreadable XDRinvalid:topic; all keep the raw base64 indata.raw. Aliases:market_created,bet_placed,market_resolved,winnings_claimed.src/services/indexerService.ts:mapRpcEvent(used bycreateSorobanRpcClient) drops non-contractevents and events from other contracts, setseventTypeto the topic symbol, setsmarketId, and builds a JSON-safe payload. Persistsmarket_idanddata.src/workers/predictionsConfirmer.ts:findConfirmedCandidateIdsnow requires an event with the sametx_hash,event_type = 'bet_plc',canonical = true,market_idequal to the prediction's market, anddata->>'bettor'equal to the user'sstellar_address.src/metrics/registry.ts:indexer_events_decoded_total{event_type}andindexer_events_invalid_total{event_type}(unknown topics are labelledunknownto bound cardinality)..env.example: commented testnet defaults forSOROBAN_RPC_URL/PREDICTIFY_CONTRACT_ID.docs/indexer-events.md(new): topic table (symbol, topic layout, payload fields, target table), aliases, confirmer join, metrics, and thegrep -n 'symbol_short!'cross-check againstevents.rs(contracts commit78ce97e).tests/indexerEventDecoder.test.ts,tests/predictionsConfirmerJoin.test.ts(new);tests/indexerService.recovery.test.ts(one expected-params line).Regression Tests
bet_plcvalid payloadindexerEventDecoderwin_clmwithi128::MAX→170141183460469231731687303715884105727indexerEventDecodermkt_resthree-element topicindexerEventDecoderunknown:foo_bar, raw XDR keptindexerEventDecoderbet_plcmissingamount→invalid:bet_plcindexerEventDecoderbet_placeddecodes likebet_plcindexerEventDecoderindexerEventDecoderunknown:*event is not confirmedpredictionsConfirmerJoinbet_plcwith non-matching bettor is not confirmedpredictionsConfirmerJoinmkt_crtformkt_5cae97f9_0indexerEventDecoderRegression proof (row 8). With only
src/workers/predictionsConfirmer.tsreverted tomain, all 3 tests inpredictionsConfirmerJoin.test.tsfail (rows 8 and 9 and theincrementAttempts/markFailedtest); with the change restored they pass.Testing
Notes for Reviewers
getEventsforCCWRJX…VAFHCreturned 0 events across the entire RPC retention window (ledgers 5155054–5276013), so the fixture is built from theevents.rslayout and serialized to base64 asgetEventsreturns it. The test file says so in a comment.DrizzlePredictionsConfirmerRepoover a fake pg pool and assert the generated SQL and parameters; they do not execute against Postgres.canonicalbinding,market_idcolumn) 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.tsis unchanged frommain.incrementAttemptsandmarkFaileduse 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.< 500 msuninstrumented and relaxed to< 2500 mswhen 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 usescValToNative; no parity test was added).npm run indexer:devtestnet ingestion run (no Postgres here), and a measured coverage number for the touched confirmer code (the changed repo methods are all executed bypredictionsConfirmerJoin.test.ts).tests/indexerGapScan.test.tshas 2 failures in my local run.src/metrics/registry.tsonmainhas noindexerGapDetectedTotal, which I believe is the cause; I did not run that file onmain.tscerror count is identical before and after (352) with no new errors in touched files;npm run lintreports one error insrc/lib/http.ts, which this PR does not touch.Closes #979