Skip to content

Commit 73bfd48

Browse files
Merge PR #755
2 parents 5e903dd + c6a01a2 commit 73bfd48

9 files changed

Lines changed: 974 additions & 11 deletions

File tree

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
-- Migration: add confirm_attempts and last_error columns to predictions
2+
-- These columns support the predictionsConfirmer worker which transitions
3+
-- pending predictions to confirmed (or failed after max attempts) by joining
4+
-- against indexer_events on txHash.
5+
6+
ALTER TABLE "predictions"
7+
ADD COLUMN IF NOT EXISTS "confirm_attempts" integer NOT NULL DEFAULT 0,
8+
ADD COLUMN IF NOT EXISTS "last_error" text;

drizzle/meta/0003_snapshot.json

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
{
2+
"id": "f5a6b7c8-d9e0-1f2a-3b4c-5d6e7f8a9b0c",
3+
"prevId": "d4e5f6a7-b8c9-0d1e-2f3a-4b5c6d7e8f90",
4+
"version": "7",
5+
"dialect": "postgresql",
6+
"tables": {},
7+
"schemas": {},
8+
"_meta": {
9+
"columns": {},
10+
"schemas": {},
11+
"tables": {}
12+
}
13+
}

drizzle/meta/_journal.json

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,13 @@
3636
"when": 1753718153907,
3737
"tag": "0025_users_filter_idx",
3838
"breakpoints": true
39+
},
40+
{
41+
"idx": 26,
42+
"version": "7",
43+
"when": 1785335000000,
44+
"tag": "0026_add_predictions_confirmer",
45+
"breakpoints": true
3946
}
4047
]
4148
}

src/config/env-schema.ts

Lines changed: 4 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -101,16 +101,10 @@ const baseSchema = z.object({
101101
SLOW_QUERY_ALERTER_LIMIT: z.coerce.number().int().positive().default(10),
102102
SLOW_QUERY_ALERTER_QUERY_MAX_LENGTH: z.coerce.number().int().positive().default(1000),
103103

104-
// ── Per-user concurrency limiting ─────────────────────────
105-
/**
106-
* Maximum number of in-flight (concurrent) requests allowed for a single
107-
* authenticated user (or IP for anonymous callers) at any point in time.
108-
* Requests that exceed this cap receive HTTP 429 with a `Retry-After: 1`
109-
* header. Set to a large value (e.g. 1000) to effectively disable the
110-
* limit without removing the middleware.
111-
* Default: 10.
112-
*/
113-
MAX_CONCURRENT_REQUESTS_PER_USER: z.coerce.number().int().positive().default(10),
104+
// ── Predictions Confirmer ───────────────────────────────────
105+
PREDICTION_CONFIRM_INTERVAL_MS: z.coerce.number().int().positive().default(5_000),
106+
PREDICTION_CONFIRM_BATCH_SIZE: z.coerce.number().int().positive().default(1000),
107+
PREDICTION_CONFIRM_MAX_ATTEMPTS: z.coerce.number().int().positive().default(3),
114108

115109
// ── Metrics ───────────────────────────────────────────────
116110
/** Bearer token required to access /api/metrics. Empty string (default) means no auth. */

src/db/schema.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -184,6 +184,17 @@ export const predictions = pgTable("predictions", {
184184
createdAt: timestamp("created_at", { withTimezone: true })
185185
.notNull()
186186
.defaultNow(),
187+
/**
188+
* Number of confirmation attempts made by the predictionsConfirmer worker.
189+
* Incremented each tick when no matching indexer event is found.
190+
* After MAX_CONFIRM_ATTEMPTS (3) the prediction is marked as failed.
191+
*/
192+
confirmAttempts: integer("confirm_attempts").notNull().default(0),
193+
/**
194+
* Error message from the most recent failed confirmation attempt.
195+
* Set when the prediction transitions to failed after exhausting all attempts.
196+
*/
197+
lastError: text("last_error"),
187198
});
188199

189200
/**

src/server.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,14 @@ import { marketResolverWorker } from "./workers/marketResolver";
99
import { backupVerificationWorker } from "./workers/backupVerificationWorker";
1010
import { reconciliationWorker } from "./workers/reconciliationWorker";
1111
import { startSlowQueryAlerter } from "./workers/slowQueryAlerter";
12+
import { startPredictionsConfirmer } from "./workers/predictionsConfirmer";
1213
import { drainSearchRequests } from "./routes/search";
1314
import { drainExportsRequests } from "./routes/exports";
1415

1516
const app = createApp();
1617
let webhookWorker: WebhookWorker | null = null;
1718
let probeHandle: ReturnType<typeof setInterval> | null = null;
19+
let predictionsConfirmerHandle: ReturnType<typeof setInterval> | null = null;
1820

1921
connectWithRetry()
2022
.then(() => {
@@ -24,6 +26,7 @@ connectWithRetry()
2426
backupVerificationWorker.start();
2527
reconciliationWorker.start();
2628
startSlowQueryAlerter();
29+
predictionsConfirmerHandle = startPredictionsConfirmer();
2730
probeHandle = startIndexerHealthProbe();
2831

2932
const server = app.listen(env.PORT, () => {

src/services/webhookCatalog.ts

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,35 @@ export const DisputeOpenedPayloadSchema = WebhookEnvelopeSchema.extend({
113113

114114
export type DisputeOpenedPayload = z.infer<typeof DisputeOpenedPayloadSchema>;
115115

116+
// ---------------------------------------------------------------------------
117+
// prediction.confirmed
118+
// ---------------------------------------------------------------------------
119+
120+
/**
121+
* Emitted when the predictionsConfirmer worker detects a matching indexer
122+
* event for a pending prediction and transitions it to confirmed.
123+
*
124+
* The dispatcher fans this event out to every subscription which subscribes to
125+
* "prediction.confirmed" or "*".
126+
*/
127+
export const PredictionConfirmedPayloadSchema = WebhookEnvelopeSchema.extend({
128+
event: z.literal("prediction.confirmed"),
129+
/** UUID of the confirmed prediction. */
130+
predictionId: z.string().uuid(),
131+
/** Primary key of the market the prediction belongs to. */
132+
marketId: z.string(),
133+
/** UUID of the user who made the prediction. */
134+
userId: z.string().uuid(),
135+
/** The outcome the user predicted (e.g. "yes" or "no"). */
136+
outcome: z.string(),
137+
/** The amount staked on the prediction (string to preserve precision). */
138+
amount: z.string(),
139+
/** On-chain transaction hash of the bet_placed event. */
140+
txHash: z.string(),
141+
});
142+
143+
export type PredictionConfirmedPayload = z.infer<typeof PredictionConfirmedPayloadSchema>;
144+
116145
// ---------------------------------------------------------------------------
117146
// Catalog registry
118147
// ---------------------------------------------------------------------------
@@ -130,6 +159,7 @@ export type DisputeOpenedPayload = z.infer<typeof DisputeOpenedPayloadSchema>;
130159
export const WEBHOOK_EVENT_SCHEMAS = {
131160
"market.resolved": MarketResolvedPayloadSchema,
132161
"dispute.opened": DisputeOpenedPayloadSchema,
162+
"prediction.confirmed": PredictionConfirmedPayloadSchema,
133163
} as const;
134164

135165
/** All registered event-type strings, derived from the catalog at compile time. */
@@ -141,7 +171,8 @@ export type WebhookEventType = keyof typeof WEBHOOK_EVENT_SCHEMAS;
141171
/** Union of every concrete payload type. */
142172
export type WebhookPayload =
143173
| MarketResolvedPayload
144-
| DisputeOpenedPayload;
174+
| DisputeOpenedPayload
175+
| PredictionConfirmedPayload;
145176

146177
// ---------------------------------------------------------------------------
147178
// Validation helpers

0 commit comments

Comments
 (0)