From bcfdd04611df43265cc2dd423f9b3880fa4d20ba Mon Sep 17 00:00:00 2001 From: Peolite001 Date: Wed, 29 Jul 2026 22:17:56 +0100 Subject: [PATCH 01/11] Extract shared createRateLimiter factory --- .../src/middleware/rate-limiter.middleware.ts | 20 +++++++++++++++---- .../stream-rate-limiter.middleware.ts | 6 ++---- 2 files changed, 18 insertions(+), 8 deletions(-) diff --git a/backend/src/middleware/rate-limiter.middleware.ts b/backend/src/middleware/rate-limiter.middleware.ts index 61079a03..e35081d2 100644 --- a/backend/src/middleware/rate-limiter.middleware.ts +++ b/backend/src/middleware/rate-limiter.middleware.ts @@ -1,10 +1,22 @@ -import { rateLimit } from 'express-rate-limit'; +import { rateLimit, type Options } from 'express-rate-limit'; -export const globalRateLimiter = rateLimit({ +/** + * Shared factory to create an express-rate-limit instance with common configuration. + * + * @param options Configuration options for express-rate-limit + * @returns Express rate limit middleware + */ +export function createRateLimiter(options: Partial) { + return rateLimit({ + standardHeaders: true, // Return rate limit info in the `RateLimit-*` headers + legacyHeaders: false, // Disable the `X-RateLimit-*` headers + ...options, + }); +} + +export const globalRateLimiter = createRateLimiter({ windowMs: 1 * 60 * 1000, // 1 minute max: 100, // Limit each IP to 100 requests per `window` (here, per minute) - standardHeaders: true, // Return rate limit info in the `RateLimit-*` headers - legacyHeaders: false, // Disable the `X-RateLimit-*` headers message: { message: 'Too many requests, please try again later.', status: 429, diff --git a/backend/src/middleware/stream-rate-limiter.middleware.ts b/backend/src/middleware/stream-rate-limiter.middleware.ts index 19fe84a2..4b551a30 100644 --- a/backend/src/middleware/stream-rate-limiter.middleware.ts +++ b/backend/src/middleware/stream-rate-limiter.middleware.ts @@ -1,4 +1,4 @@ -import { rateLimit } from 'express-rate-limit'; +import { createRateLimiter } from './rate-limiter.middleware.js'; import { type Request, type Response, type NextFunction } from 'express'; import type { AuthenticatedRequest } from '../types/auth.types.js'; import logger from '../logger.js'; @@ -22,11 +22,9 @@ export function createStreamRateLimiter( // Read from environment variable, default to 10 if not set const max = options?.max ?? (process.env.STREAM_CREATE_RATE_LIMIT ? parseInt(process.env.STREAM_CREATE_RATE_LIMIT, 10) : 10); - return rateLimit({ + return createRateLimiter({ windowMs, max, - standardHeaders: true, // Return rate limit info in the `RateLimit-*` headers - legacyHeaders: false, // Disable the `X-RateLimit-*` headers message: { error: 'Too many stream creation requests - rate limit exceeded', message: 'You have exceeded the rate limit for stream creation. Please try again later.', From b2c5c891c63223a983a6b443dd39a7d3475a02cc Mon Sep 17 00:00:00 2001 From: Peolite001 Date: Fri, 31 Jul 2026 02:59:57 +0100 Subject: [PATCH 02/11] Fix typescript build errors, JSON BigInt serialization, frontend lint issues, and failing tests --- backend/src/app.ts | 7 ++ .../src/services/soroban-indexer.service.ts | 3 +- backend/src/workers/soroban-event-worker.ts | 8 +- backend/tests/events-wire-format.test.ts | 2 +- backend/tests/stream.test.ts | 24 ++-- backend/tests/stream.validator.test.ts | 2 +- contracts/stream_contract/src/test.rs | 11 +- .../components/dashboard/dashboard-view.tsx | 1 - frontend/src/hooks/useIncomingStreams.test.ts | 5 +- frontend/src/lib/dashboard.ts | 24 ++-- frontend/src/lib/logger.ts | 8 +- package-lock.json | 105 ------------------ 12 files changed, 47 insertions(+), 153 deletions(-) diff --git a/backend/src/app.ts b/backend/src/app.ts index 60f4610b..fe925785 100644 --- a/backend/src/app.ts +++ b/backend/src/app.ts @@ -17,6 +17,13 @@ import v1Routes from "./routes/v1/index.js"; import healthRoutes from "./routes/health.routes.js"; +// Patch BigInt serialization for JSON.stringify globally +if (!('toJSON' in BigInt.prototype)) { + (BigInt.prototype as any).toJSON = function () { + return Number(this.toString()); + }; +} + const app = express(); const isProduction = process.env.NODE_ENV === "production"; const rawCors = process.env.CORS_ALLOWED_ORIGINS ?? ""; diff --git a/backend/src/services/soroban-indexer.service.ts b/backend/src/services/soroban-indexer.service.ts index 53fd1a4c..deccadf5 100644 --- a/backend/src/services/soroban-indexer.service.ts +++ b/backend/src/services/soroban-indexer.service.ts @@ -179,8 +179,7 @@ export class SorobanIndexerService { const ratePerSecond = this.readString(value, 'rate_per_second', 'ratePerSecond'); const depositedAmount = this.readString(value, 'deposited_amount', 'depositedAmount'); const startTimeRaw = value.start_time ?? value.startTime ?? timestamp; - const startTime = BigInt(startTimeRaw ?? timestamp); - const timestampBigInt = BigInt(timestamp); + const startTime = BigInt(startTimeRaw as string | number); if (!sender || !recipient || !tokenAddress || !ratePerSecond || !depositedAmount) return; diff --git a/backend/src/workers/soroban-event-worker.ts b/backend/src/workers/soroban-event-worker.ts index ade69ae4..cdef1249 100644 --- a/backend/src/workers/soroban-event-worker.ts +++ b/backend/src/workers/soroban-event-worker.ts @@ -1024,13 +1024,13 @@ export class SorobanEventWorker { }); // Calculate the duration of this pause interval - let additionalPausedDuration = 0; + let additionalPausedDuration = 0n; if (currentStream.pausedAt) { - additionalPausedDuration = timestamp - currentStream.pausedAt; + additionalPausedDuration = BigInt(timestamp) - BigInt(currentStream.pausedAt); } const newTotalPausedDuration = - currentStream.totalPausedDuration + additionalPausedDuration; + Number(BigInt(currentStream.totalPausedDuration) + additionalPausedDuration); await tx.stream.update({ where: { streamId }, @@ -1073,7 +1073,7 @@ export class SorobanEventWorker { metadata: JSON.stringify({ sender, newEndTime, - pausedDuration: additionalPausedDuration, + pausedDuration: Number(additionalPausedDuration), totalPausedDuration: newTotalPausedDuration, }), }, diff --git a/backend/tests/events-wire-format.test.ts b/backend/tests/events-wire-format.test.ts index e7cd12fb..524b07ab 100644 --- a/backend/tests/events-wire-format.test.ts +++ b/backend/tests/events-wire-format.test.ts @@ -91,7 +91,7 @@ describe('event wire format', () => { expect(decodedKeys).toEqual([...allFields].sort()); - for (const field of HANDLER_READ_FIELDS[eventName]) { + for (const field of HANDLER_READ_FIELDS[eventName]!) { expect(decoded).toHaveProperty(field); } }, diff --git a/backend/tests/stream.test.ts b/backend/tests/stream.test.ts index cad05ac1..a0dbe230 100644 --- a/backend/tests/stream.test.ts +++ b/backend/tests/stream.test.ts @@ -65,8 +65,8 @@ describe('POST /v1/streams', () => { ratePerSecond: '100', depositedAmount: '86400', withdrawnAmount: '0', - startTime: 1700000000, - lastUpdateTime: 1700000000, + startTime: 1700000000n, + lastUpdateTime: 1700000000n, isPaused: false, pausedAt: null, totalPausedDuration: 0, @@ -247,8 +247,8 @@ describe('GET /v1/users/:address/summary', () => { ratePerSecond: '10', depositedAmount: '100', withdrawnAmount: '30', - startTime: 1000, - lastUpdateTime: 2000, + startTime: 1000n, + lastUpdateTime: 2000n, isPaused: false, endTime: null, pausedAt: null, @@ -266,8 +266,8 @@ describe('GET /v1/users/:address/summary', () => { ratePerSecond: '20', depositedAmount: '200', withdrawnAmount: '20', - startTime: 1000, - lastUpdateTime: 2000, + startTime: 1000n, + lastUpdateTime: 2000n, isPaused: false, endTime: null, pausedAt: null, @@ -287,8 +287,8 @@ describe('GET /v1/users/:address/summary', () => { ratePerSecond: '10', depositedAmount: '1000', withdrawnAmount: '100', - startTime: 1000, - lastUpdateTime: 0, + startTime: 1000n, + lastUpdateTime: 0n, isPaused: false, endTime: null, pausedAt: null, @@ -306,8 +306,8 @@ describe('GET /v1/users/:address/summary', () => { ratePerSecond: '5', depositedAmount: '500', withdrawnAmount: '0', - startTime: 1000, - lastUpdateTime: 0, + startTime: 1000n, + lastUpdateTime: 0n, isPaused: false, endTime: null, pausedAt: null, @@ -344,8 +344,8 @@ describe('GET /v1/users/:address/summary', () => { ratePerSecond: '1', depositedAmount: '100', withdrawnAmount: '1', - startTime: 1000, - lastUpdateTime: 2000, + startTime: 1000n, + lastUpdateTime: 2000n, isPaused: false, endTime: null, pausedAt: null, diff --git a/backend/tests/stream.validator.test.ts b/backend/tests/stream.validator.test.ts index f546a5d0..6c3a834a 100644 --- a/backend/tests/stream.validator.test.ts +++ b/backend/tests/stream.validator.test.ts @@ -42,7 +42,7 @@ describe('Stream Validator', () => { }; const result = createStreamSchema.safeParse(data); expect(result.success).toBe(false); - expect(result.error?.issues[0].message).toBe('Rate exceeds maximum allowed value'); + expect(result.error?.issues?.[0]?.message).toBe('Rate exceeds maximum allowed value'); }); it('should accept ratePerSecond at i128 max', () => { diff --git a/contracts/stream_contract/src/test.rs b/contracts/stream_contract/src/test.rs index 194d74be..74a87b12 100644 --- a/contracts/stream_contract/src/test.rs +++ b/contracts/stream_contract/src/test.rs @@ -433,8 +433,8 @@ fn test_backdated_start_time_would_immediately_vest_full_amount() { // env.ledger().timestamp() — but it demonstrates the risk that would exist // if a caller-supplied start_time were ever added. let mut stream = client.get_stream(&stream_id).unwrap(); - stream.start_time = 0; // backdated far into the past - stream.last_update_time = 0; // sync anchor to match + stream.start_time = 0; // backdated far into the past + stream.last_update_time = 0; // sync anchor to match env.as_contract(&client.address, || { env.storage() .persistent() @@ -2709,11 +2709,8 @@ fn test_cancel_state_committed_before_transfers_prevents_double_cancel() { fn event_field_names(env: &Env, payload: &soroban_sdk::Val) -> std::vec::Vec { let map = soroban_sdk::Map::::try_from_val(env, payload) .expect("event data is not a Map"); - let mut names: std::vec::Vec = map - .keys() - .iter() - .map(|sym| sym.to_string()) - .collect(); + let mut names: std::vec::Vec = + map.keys().iter().map(|sym| sym.to_string()).collect(); names.sort(); names } diff --git a/frontend/src/components/dashboard/dashboard-view.tsx b/frontend/src/components/dashboard/dashboard-view.tsx index 34568f84..63d337f4 100644 --- a/frontend/src/components/dashboard/dashboard-view.tsx +++ b/frontend/src/components/dashboard/dashboard-view.tsx @@ -18,7 +18,6 @@ import toast from "react-hot-toast"; import { getDashboardAnalytics, fetchDashboardData, - useDashboard, dashboardQueryKey, type DashboardSnapshot, type Stream, diff --git a/frontend/src/hooks/useIncomingStreams.test.ts b/frontend/src/hooks/useIncomingStreams.test.ts index 39a9b9d8..4cd94a8a 100644 --- a/frontend/src/hooks/useIncomingStreams.test.ts +++ b/frontend/src/hooks/useIncomingStreams.test.ts @@ -34,9 +34,8 @@ describe("useIncomingStreams hooks", () => { queryClient.clear(); }); - const wrapper = ({ children }: { children: React.ReactNode }) => ( - {children} - ); + const wrapper = ({ children }: { children: React.ReactNode }) => + React.createElement(QueryClientProvider, { client: queryClient }, children); describe("incomingStreamsQueryKey", () => { it("returns correct shape", () => { diff --git a/frontend/src/lib/dashboard.ts b/frontend/src/lib/dashboard.ts index 39a8814f..303f1289 100644 --- a/frontend/src/lib/dashboard.ts +++ b/frontend/src/lib/dashboard.ts @@ -88,21 +88,19 @@ async function fetchStreams( for (const endpoint of endpoints) { try { const response = await fetch(`${endpoint}?${params.toString()}`, { signal }); - if (response.ok) { - const payload = (await response.json()) as - | BackendStream[] - | { data?: BackendStream[] }; - return Array.isArray(payload) ? payload : payload.data ?? []; - } - - if (response.status === 404) { - lastError = new Error(`Endpoint not found: ${endpoint}`); - continue; - } + if (response.ok) { + const payload = (await response.json()) as + | BackendStream[] + | { data?: BackendStream[] }; + return Array.isArray(payload) ? payload : payload.data ?? []; + } - lastError = new Error(`Failed to fetch streams (${response.status}) from ${endpoint}`); - } + if (response.status === 404) { + lastError = new Error(`Endpoint not found: ${endpoint}`); + continue; + } + lastError = new Error(`Failed to fetch streams (${response.status}) from ${endpoint}`); } catch (err) { if (err instanceof Error && err.name === "AbortError") { throw err; diff --git a/frontend/src/lib/logger.ts b/frontend/src/lib/logger.ts index 496bfae5..0d1ad792 100644 --- a/frontend/src/lib/logger.ts +++ b/frontend/src/lib/logger.ts @@ -2,16 +2,16 @@ const isDev = process.env.NODE_ENV !== "production"; export const logger = { debug: (...args: unknown[]) => { - if (isDev) console.debug(...args); // eslint-disable-line no-console + if (isDev) console.debug(...args); }, info: (...args: unknown[]) => { - if (isDev) console.info(...args); // eslint-disable-line no-console + if (isDev) console.info(...args); }, warn: (...args: unknown[]) => { - if (isDev) console.warn(...args); // eslint-disable-line no-console + if (isDev) console.warn(...args); }, // errors always surface, even in production error: (...args: unknown[]) => { - console.error(...args); // eslint-disable-line no-console + console.error(...args); }, }; diff --git a/package-lock.json b/package-lock.json index 6f78cc24..193fdb5d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2398,9 +2398,6 @@ "cpu": [ "arm64" ], - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -2417,9 +2414,6 @@ "cpu": [ "arm64" ], - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -2436,9 +2430,6 @@ "cpu": [ "x64" ], - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -2455,9 +2446,6 @@ "cpu": [ "x64" ], - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -2896,9 +2884,6 @@ "arm64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -2917,9 +2902,6 @@ "arm64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -2938,9 +2920,6 @@ "ppc64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -2959,9 +2938,6 @@ "s390x" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -2980,9 +2956,6 @@ "x64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -3001,9 +2974,6 @@ "x64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -3207,9 +3177,6 @@ "arm" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -3224,9 +3191,6 @@ "arm" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -3241,9 +3205,6 @@ "arm64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -3258,9 +3219,6 @@ "arm64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -3275,9 +3233,6 @@ "loong64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -3292,9 +3247,6 @@ "loong64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -3309,9 +3261,6 @@ "ppc64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -3326,9 +3275,6 @@ "ppc64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -3343,9 +3289,6 @@ "riscv64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -3360,9 +3303,6 @@ "riscv64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -3377,9 +3317,6 @@ "s390x" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -3394,9 +3331,6 @@ "x64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -3411,9 +3345,6 @@ "x64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -3755,9 +3686,6 @@ "arm64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MPL-2.0", "optional": true, "os": [ @@ -3779,9 +3707,6 @@ "arm64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MPL-2.0", "optional": true, "os": [ @@ -3803,9 +3728,6 @@ "x64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MPL-2.0", "optional": true, "os": [ @@ -3976,9 +3898,6 @@ "arm64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -3996,9 +3915,6 @@ "arm64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -4015,9 +3931,6 @@ "cpu": [ "x64" ], - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -4035,9 +3948,6 @@ "x64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -9722,9 +9632,6 @@ "arm64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MPL-2.0", "optional": true, "os": [ @@ -9747,9 +9654,6 @@ "arm64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MPL-2.0", "optional": true, "os": [ @@ -9771,9 +9675,6 @@ "cpu": [ "x64" ], - "libc": [ - "glibc" - ], "license": "MPL-2.0", "optional": true, "os": [ @@ -9795,9 +9696,6 @@ "x64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MPL-2.0", "optional": true, "os": [ @@ -9886,9 +9784,6 @@ "x64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MPL-2.0", "optional": true, "os": [ From 3c6d41f8ae1bb3c3c5a6ed57a8000ba971c11e1e Mon Sep 17 00:00:00 2001 From: Peolite001 Date: Fri, 31 Jul 2026 04:05:05 +0100 Subject: [PATCH 03/11] Fix build errors, test types, and frontend test timeouts --- backend/src/app.ts | 7 +--- backend/src/lib/indexer-state.ts | 1 - .../admin-rate-limiter.middleware.ts | 2 +- backend/src/routes/health.routes.ts | 4 ++- backend/src/workers/soroban-event-worker.ts | 10 ++---- backend/tests/claimable.service.test.ts | 32 +++++++++---------- backend/tests/soroban.service.test.ts | 16 +++++----- backend/tests/stream.test.ts | 14 ++++---- .../src/hooks/useIncomingStreams.test.tsx | 15 ++++++--- 9 files changed, 48 insertions(+), 53 deletions(-) diff --git a/backend/src/app.ts b/backend/src/app.ts index 394195c6..db0dffb3 100644 --- a/backend/src/app.ts +++ b/backend/src/app.ts @@ -18,12 +18,7 @@ import v1Routes from "./routes/v1/index.js"; import healthRoutes from "./routes/health.routes.js"; import "./lib/stream-id.js"; -// Patch BigInt serialization for JSON.stringify globally -if (!('toJSON' in BigInt.prototype)) { - (BigInt.prototype as any).toJSON = function () { - return Number(this.toString()); - }; -} + const app = express(); const isProduction = process.env.NODE_ENV === "production"; diff --git a/backend/src/lib/indexer-state.ts b/backend/src/lib/indexer-state.ts index e6568647..92b3ae79 100644 --- a/backend/src/lib/indexer-state.ts +++ b/backend/src/lib/indexer-state.ts @@ -7,7 +7,6 @@ export interface IndexerStateRow { id: string; lastLedger: number; lastCursor: string | null; - createdAt: Date; updatedAt: Date; } diff --git a/backend/src/middleware/admin-rate-limiter.middleware.ts b/backend/src/middleware/admin-rate-limiter.middleware.ts index 1f665ef7..db6c44cc 100644 --- a/backend/src/middleware/admin-rate-limiter.middleware.ts +++ b/backend/src/middleware/admin-rate-limiter.middleware.ts @@ -15,7 +15,7 @@ export const adminRateLimiter = rateLimit({ // Use x-forwarded-for or remote address as key const forwarded = req.headers['x-forwarded-for']; if (typeof forwarded === 'string') { - return forwarded.split(',')[0].trim(); + return forwarded.split(',')[0]?.trim() || req.ip || 'unknown'; } return req.ip ?? 'unknown'; }, diff --git a/backend/src/routes/health.routes.ts b/backend/src/routes/health.routes.ts index 3ecf39af..e971e8f8 100644 --- a/backend/src/routes/health.routes.ts +++ b/backend/src/routes/health.routes.ts @@ -2,6 +2,8 @@ import { Router, type Request, type Response } from 'express'; import { prisma } from '../lib/prisma.js'; import { INDEXER_STATE_ID } from '../lib/indexer-state.js'; import { sorobanEventWorker } from '../workers/soroban-event-worker.js'; +import { isRedisAvailable } from '../lib/redis.js'; +import { checkRpcHealth } from '../services/sorobanService.js'; const router = Router(); @@ -161,7 +163,7 @@ router.get('/', async (_req: Request, res: Response) => { status: dbStatus === 'connected' ? 'ok' : 'down', }, indexer: { - status: !indexerEnabled ? 'disabled' : indexerDegraded ? 'degraded' : 'ok', + status: !indexerEnabled ? 'disabled' : (indexerLagDegraded || indexerFailureDegraded) ? 'degraded' : 'ok', enabled: indexerEnabled, lagSeconds: indexerLag === -1 ? null : indexerLag, }, diff --git a/backend/src/workers/soroban-event-worker.ts b/backend/src/workers/soroban-event-worker.ts index 33c559d4..cc2d00c3 100644 --- a/backend/src/workers/soroban-event-worker.ts +++ b/backend/src/workers/soroban-event-worker.ts @@ -1,9 +1,8 @@ -import { randomUUID } from "crypto"; import { rpc, xdr, StrKey } from "@stellar/stellar-sdk"; import { prisma } from "../lib/prisma.js"; import { INDEXER_STATE_ID, ensureIndexerState } from "../lib/indexer-state.js"; import { sseService } from "../services/sse.service.js"; -import logger, { requestContext } from "../logger.js"; +import logger from "../logger.js"; import { Prisma } from "../generated/prisma/index.js"; import "../lib/stream-id.js"; @@ -113,12 +112,7 @@ export class SorobanEventWorker { /** Recent attempt outcomes for sliding-window spike detection. */ private recentOutcomes: { ok: boolean; at: number }[] = []; - /** - * Stable id attached to every log line emitted by the background poll - * loop, since these callbacks fire outside of any HTTP request and would - * otherwise have no requestContext (and thus no correlation id) at all. - */ - private readonly workerId = `soroban-worker:${randomUUID()}`; + constructor() { const rpcUrl = diff --git a/backend/tests/claimable.service.test.ts b/backend/tests/claimable.service.test.ts index 5b0bb8e2..ec662504 100644 --- a/backend/tests/claimable.service.test.ts +++ b/backend/tests/claimable.service.test.ts @@ -3,12 +3,12 @@ import { ClaimableAmountService } from '../src/services/claimable.service.js'; function makeStreamState(overrides: Partial[0]> = {}) { return { - streamId: 1, + streamId: 1n, ratePerSecond: '10', depositedAmount: '100', withdrawnAmount: '0', - lastUpdateTime: 0, - startTime: 0, + lastUpdateTime: 0n, + startTime: 0n, isActive: true, isPaused: false, pausedAt: null, @@ -35,11 +35,11 @@ describe('ClaimableAmountService', () => { const result = service.getClaimableAmount({ ...makeStreamState({ - streamId: 1, + streamId: 1n, ratePerSecond: '5', depositedAmount: '500', withdrawnAmount: '100', - lastUpdateTime: 7, + lastUpdateTime: 7n, }), }); @@ -61,7 +61,7 @@ describe('ClaimableAmountService', () => { const result = service.getClaimableAmount({ ...makeStreamState({ - streamId: 2, + streamId: 2n, depositedAmount: '1000', withdrawnAmount: '900', }), @@ -80,7 +80,7 @@ describe('ClaimableAmountService', () => { const result = service.getClaimableAmount({ ...makeStreamState({ - streamId: 3, + streamId: 3n, withdrawnAmount: '100', isActive: false, }), @@ -98,7 +98,7 @@ describe('ClaimableAmountService', () => { const result = service.getClaimableAmount({ ...makeStreamState({ - streamId: 4, + streamId: 4n, withdrawnAmount: '150', }), }); @@ -114,7 +114,7 @@ describe('ClaimableAmountService', () => { }); const input = makeStreamState({ - streamId: 5, + streamId: 5n, ratePerSecond: '7', depositedAmount: '700', }); @@ -146,11 +146,11 @@ describe('ClaimableAmountService', () => { }); const preWithdrawalState = makeStreamState({ - streamId: 7, + streamId: 7n, ratePerSecond: '10', depositedAmount: '1000', withdrawnAmount: '0', - lastUpdateTime: 0, + lastUpdateTime: 0n, }); // Prime the cache with the pre-withdrawal state. @@ -166,11 +166,11 @@ describe('ClaimableAmountService', () => { // and lastUpdateTime are advanced on the stream row, exactly as // handleTokensWithdrawn does in soroban-event-worker.ts. const postWithdrawalState = makeStreamState({ - streamId: 7, + streamId: 7n, ratePerSecond: '10', depositedAmount: '1000', withdrawnAmount: '400', - lastUpdateTime: 40, + lastUpdateTime: 40n, }); // Well within the 60s TTL, so this only passes if the state change (not @@ -191,7 +191,7 @@ describe('ClaimableAmountService', () => { const result = service.getClaimableAmount({ ...makeStreamState({ - streamId: 6, + streamId: 6n, ratePerSecond: i128Max, depositedAmount: i128Max, withdrawnAmount: '42', @@ -228,11 +228,11 @@ describe('ClaimableAmountService', () => { const result = service.getClaimableAmount({ ...makeStreamState({ - streamId: 10_000 + iteration, + streamId: BigInt(10_000 + iteration), ratePerSecond: rate.toString(), depositedAmount: deposited.toString(), withdrawnAmount: withdrawn.toString(), - lastUpdateTime: 0, + lastUpdateTime: 0n, isPaused: paused, pausedAt: paused ? Number(pauseStart) : null, totalPausedDuration: paused ? Number(elapsed - pauseStart) : 0, diff --git a/backend/tests/soroban.service.test.ts b/backend/tests/soroban.service.test.ts index 649864b9..779138c7 100644 --- a/backend/tests/soroban.service.test.ts +++ b/backend/tests/soroban.service.test.ts @@ -165,7 +165,7 @@ describe('Soroban Service', () => { simulationSuccess(nativeToScVal(99n, { type: 'i128' })) ); - await getStreamFromChain(1); + await getStreamFromChain(1n); expect(mocks.server.simulateTransaction).toHaveBeenCalled(); }); @@ -191,15 +191,15 @@ describe('Soroban Service', () => { ) ); - await expect(getStreamFromChain(7)).resolves.toEqual({ - streamId: 7, + await expect(getStreamFromChain(7n)).resolves.toEqual({ + streamId: 7n, sender, recipient, tokenAddress, ratePerSecond: '25', depositedAmount: '1000', withdrawnAmount: '125', - startTime: 1_700_000_000, + startTime: 1_700_000_000n, isActive: true, }); }); @@ -211,7 +211,7 @@ describe('Soroban Service', () => { simulationSuccess(mapVal([['sender', nativeToScVal('not-an-address')]])) ); - await expect(getStreamFromChain(8)).resolves.toBeNull(); + await expect(getStreamFromChain(8n)).resolves.toBeNull(); }); it.skip('decodes getClaimableFromChain response', async () => { @@ -221,7 +221,7 @@ describe('Soroban Service', () => { simulationSuccess(nativeToScVal(99n, { type: 'i128' })) ); - await expect(getClaimableFromChain(9)).resolves.toBe('99'); + await expect(getClaimableFromChain(9n)).resolves.toBe('99'); }); it('returns null when getClaimableFromChain decoding fails', async () => { @@ -229,7 +229,7 @@ describe('Soroban Service', () => { mocks.server.simulateTransaction.mockResolvedValue(simulationSuccess(nativeToScVal(true))); - await expect(getClaimableFromChain(10)).resolves.toBeNull(); + await expect(getClaimableFromChain(10n)).resolves.toBeNull(); }); }); @@ -255,7 +255,7 @@ describe('Soroban Service', () => { it('throws when KEEPER_SECRET_KEY is unset', async () => { const { topUpStream } = await importService({ KEEPER_SECRET_KEY: undefined }); - await expect(topUpStream(1, 100n, Keypair.random().publicKey())).rejects.toThrow( + await expect(topUpStream(1n, 100n, Keypair.random().publicKey())).rejects.toThrow( 'KEEPER_SECRET_KEY not configured' ); expect(mocks.server.sendTransaction).not.toHaveBeenCalled(); diff --git a/backend/tests/stream.test.ts b/backend/tests/stream.test.ts index a0dbe230..fda43a27 100644 --- a/backend/tests/stream.test.ts +++ b/backend/tests/stream.test.ts @@ -58,7 +58,7 @@ describe('POST /v1/streams', () => { it('should return 201 when stream is created successfully', async () => { const mockStream = { id: 'uuid-123', - streamId: 1, + streamId: 1n, sender: 'GTEST_USER_PUBLIC_KEY', recipient: 'GDEF456ABC789GHI012JKL345MNO678PQR901STU234VWX567YZA123BCD', tokenAddress: 'CBCD789EFG012HIJ345KLM678NOP901QRS234TUV567WXY890ZAB123CDE', @@ -95,7 +95,7 @@ describe('POST /v1/streams', () => { expect(response.status).toBe(201); expect(response.body).toMatchObject({ - streamId: 1, + streamId: 1n, sender: validData.sender, recipient: validData.recipient, }); @@ -240,7 +240,7 @@ describe('GET /v1/users/:address/summary', () => { id: '1', createdAt: new Date(), updatedAt: new Date(), - streamId: 1, + streamId: 1n, sender: 'GSENDER', recipient: 'GRECIPIENT', tokenAddress: 'TOKEN', @@ -259,7 +259,7 @@ describe('GET /v1/users/:address/summary', () => { id: '2', createdAt: new Date(), updatedAt: new Date(), - streamId: 2, + streamId: 2n, sender: 'GSENDER2', recipient: 'GRECIPIENT2', tokenAddress: 'TOKEN2', @@ -280,7 +280,7 @@ describe('GET /v1/users/:address/summary', () => { id: '3', createdAt: new Date(), updatedAt: new Date(), - streamId: 11, + streamId: 11n, sender: 'GSENDER3', recipient: 'GRECIPIENT3', tokenAddress: 'TOKEN3', @@ -299,7 +299,7 @@ describe('GET /v1/users/:address/summary', () => { id: '4', createdAt: new Date(), updatedAt: new Date(), - streamId: 12, + streamId: 12n, sender: 'GSENDER4', recipient: 'GRECIPIENT4', tokenAddress: 'TOKEN4', @@ -337,7 +337,7 @@ describe('GET /v1/users/:address/summary', () => { id: '5', createdAt: new Date(), updatedAt: new Date(), - streamId: 13, + streamId: 13n, sender: 'GSENDER5', recipient: 'GRECIPIENT5', tokenAddress: 'TOKEN5', diff --git a/frontend/src/hooks/useIncomingStreams.test.tsx b/frontend/src/hooks/useIncomingStreams.test.tsx index 5e5bda0a..ff758bde 100644 --- a/frontend/src/hooks/useIncomingStreams.test.tsx +++ b/frontend/src/hooks/useIncomingStreams.test.tsx @@ -113,6 +113,7 @@ describe("useIncomingStreams hooks", () => { it("invalidates incomingStreamsQueryKey(publicKey) on success", async () => { vi.mocked(withdrawFromStream).mockResolvedValue({ success: true, txHash: "tx-hash" }); vi.mocked(fetchIncomingStreams).mockResolvedValue([]); + vi.useFakeTimers(); const { result } = renderHook( () => useWithdrawIncomingStream({} as unknown as WalletSession, "pubkey"), @@ -134,11 +135,15 @@ describe("useIncomingStreams hooks", () => { }); // Wait for pollIndexerForWithdraw to complete and call invalidateQueries - await waitFor(() => { - expect(invalidateSpy).toHaveBeenCalledWith({ - queryKey: incomingStreamsQueryKey("pubkey"), - }); - }, { timeout: 10000 }); + await act(async () => { + await vi.advanceTimersByTimeAsync(70000); // 63s max retries delay + }); + + expect(invalidateSpy).toHaveBeenCalledWith({ + queryKey: incomingStreamsQueryKey("pubkey"), + }); + + vi.useRealTimers(); }); }); }); From 824207bd9dced69b9af0a391db143292a1b67b3c Mon Sep 17 00:00:00 2001 From: Peolite001 Date: Fri, 31 Jul 2026 04:27:45 +0100 Subject: [PATCH 04/11] Fix remaining test assertions for BigInt migration --- backend/tests/integration/stream-lifecycle.test.ts | 12 ++++++------ backend/tests/soroban-event-worker.test.ts | 2 +- backend/tests/stream.test.ts | 8 ++++---- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/backend/tests/integration/stream-lifecycle.test.ts b/backend/tests/integration/stream-lifecycle.test.ts index 104a4a63..becad99c 100644 --- a/backend/tests/integration/stream-lifecycle.test.ts +++ b/backend/tests/integration/stream-lifecycle.test.ts @@ -88,7 +88,7 @@ function createStreamCreatedEvent( ]; const overrideEntries: [string, xdr.ScVal][] = Object.entries(overrides).map( - ([k, v]) => [k, nativeToScVal(v)], + ([k, v]) => [k, (v && typeof v === "object" && "switch" in v) ? v : nativeToScVal(v)], ); return { @@ -629,8 +629,8 @@ describe("Stream Lifecycle Integration Tests", () => { // ── Step 1: Create ────────────────────────────────────────────────── const createEvent = createStreamCreatedEvent(streamId, { - deposited_amount: BigInt(100_000), - rate_per_second: BigInt(100), + deposited_amount: scvI128(BigInt(100_000)), + rate_per_second: scvI128(BigInt(100)), }); await worker.processEvent(createEvent); @@ -768,7 +768,7 @@ describe("Stream Lifecycle Integration Tests", () => { expect(sseService.broadcastToStream).toHaveBeenCalledWith( streamId.toString(), "stream.created", - expect.objectContaining({ streamId }), + expect.objectContaining({ streamId: BigInt(streamId) }), ); // Verify event was received by client (if SSE service is real) @@ -819,7 +819,7 @@ describe("Stream Lifecycle Integration Tests", () => { expect(sseService.broadcastToStream).toHaveBeenCalledWith( streamId.toString(), "stream.topped_up", - expect.objectContaining({ streamId, amount: "1000" }), + expect.objectContaining({ streamId: BigInt(streamId), amount: "1000" }), ); }); @@ -866,7 +866,7 @@ describe("Stream Lifecycle Integration Tests", () => { expect(sseService.broadcastToStream).toHaveBeenCalledWith( streamId.toString(), "stream.cancelled", - expect.objectContaining({ streamId }), + expect.objectContaining({ streamId: BigInt(streamId) }), ); }); }); diff --git a/backend/tests/soroban-event-worker.test.ts b/backend/tests/soroban-event-worker.test.ts index bde02fb5..e6ec5a55 100644 --- a/backend/tests/soroban-event-worker.test.ts +++ b/backend/tests/soroban-event-worker.test.ts @@ -575,7 +575,7 @@ describe('SorobanEventWorker', () => { // Sanity check: depositedAmount/endTime from the (only) applied update // match what a single application should produce. expect(firstUpdateArgs.data.depositedAmount).toBe('1200'); - expect(expectedEndTime).toBe(1700000000 + Math.floor(1200 / 10) + 0); + expect(expectedEndTime).toBe(1700000120n); }); it('should process admin_transferred events successfully', async () => { diff --git a/backend/tests/stream.test.ts b/backend/tests/stream.test.ts index fda43a27..9f92fabc 100644 --- a/backend/tests/stream.test.ts +++ b/backend/tests/stream.test.ts @@ -58,7 +58,7 @@ describe('POST /v1/streams', () => { it('should return 201 when stream is created successfully', async () => { const mockStream = { id: 'uuid-123', - streamId: 1n, + streamId: 1, sender: 'GTEST_USER_PUBLIC_KEY', recipient: 'GDEF456ABC789GHI012JKL345MNO678PQR901STU234VWX567YZA123BCD', tokenAddress: 'CBCD789EFG012HIJ345KLM678NOP901QRS234TUV567WXY890ZAB123CDE', @@ -95,7 +95,7 @@ describe('POST /v1/streams', () => { expect(response.status).toBe(201); expect(response.body).toMatchObject({ - streamId: 1n, + streamId: 1, sender: validData.sender, recipient: validData.recipient, }); @@ -240,7 +240,7 @@ describe('GET /v1/users/:address/summary', () => { id: '1', createdAt: new Date(), updatedAt: new Date(), - streamId: 1n, + streamId: 1, sender: 'GSENDER', recipient: 'GRECIPIENT', tokenAddress: 'TOKEN', @@ -259,7 +259,7 @@ describe('GET /v1/users/:address/summary', () => { id: '2', createdAt: new Date(), updatedAt: new Date(), - streamId: 2n, + streamId: 2, sender: 'GSENDER2', recipient: 'GRECIPIENT2', tokenAddress: 'TOKEN2', From a8141b49ce753f6370c12ab2ea6e212c6cf4a5fb Mon Sep 17 00:00:00 2001 From: Peolite001 Date: Fri, 31 Jul 2026 04:33:52 +0100 Subject: [PATCH 05/11] Fix BigInt expectations in stream-lifecycle tests --- backend/tests/integration/stream-lifecycle.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/backend/tests/integration/stream-lifecycle.test.ts b/backend/tests/integration/stream-lifecycle.test.ts index becad99c..e3f12750 100644 --- a/backend/tests/integration/stream-lifecycle.test.ts +++ b/backend/tests/integration/stream-lifecycle.test.ts @@ -246,7 +246,7 @@ describe("Stream Lifecycle Integration Tests", () => { include: { senderUser: true, recipientUser: true }, }); expect(dbStream).toBeTruthy(); - expect(dbStream?.streamId).toBe(streamId); + expect(dbStream?.streamId).toBe(BigInt(streamId)); expect(dbStream?.sender).toBe(SENDER); expect(dbStream?.recipient).toBe(RECIPIENT); expect(dbStream?.isActive).toBe(true); @@ -513,7 +513,7 @@ describe("Stream Lifecycle Integration Tests", () => { expect(response.body.cached).toBe(false); // Verify RPC was called - expect(getClaimableFromChain).toHaveBeenCalledWith(streamId); + expect(getClaimableFromChain).toHaveBeenCalledWith(BigInt(streamId)); }); it("returns fresh data when not stale", async () => { From de002ee19260674d9432cbb2f4707872aeb6a1c2 Mon Sep 17 00:00:00 2001 From: Peolite001 Date: Fri, 31 Jul 2026 04:39:17 +0100 Subject: [PATCH 06/11] Fix Suspense warning in create page and BigInt test mock types --- backend/tests/stream.test.ts | 4 ++-- frontend/src/app/streams/create/page.tsx | 7 ++++++- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/backend/tests/stream.test.ts b/backend/tests/stream.test.ts index 9f92fabc..13410543 100644 --- a/backend/tests/stream.test.ts +++ b/backend/tests/stream.test.ts @@ -240,7 +240,7 @@ describe('GET /v1/users/:address/summary', () => { id: '1', createdAt: new Date(), updatedAt: new Date(), - streamId: 1, + streamId: 1n, sender: 'GSENDER', recipient: 'GRECIPIENT', tokenAddress: 'TOKEN', @@ -259,7 +259,7 @@ describe('GET /v1/users/:address/summary', () => { id: '2', createdAt: new Date(), updatedAt: new Date(), - streamId: 2, + streamId: 2n, sender: 'GSENDER2', recipient: 'GRECIPIENT2', tokenAddress: 'TOKEN2', diff --git a/frontend/src/app/streams/create/page.tsx b/frontend/src/app/streams/create/page.tsx index 66164f65..f0019891 100644 --- a/frontend/src/app/streams/create/page.tsx +++ b/frontend/src/app/streams/create/page.tsx @@ -1,4 +1,5 @@ import type { Metadata } from "next"; +import { Suspense } from "react"; import CreateStreamContent from "./create-stream-content"; export const metadata: Metadata = { @@ -7,5 +8,9 @@ export const metadata: Metadata = { }; export default function CreateStreamPage() { - return ; + return ( + Loading...}> + + + ); } From 9510492904ffc6b21c44401b7405299ab2e58f5e Mon Sep 17 00:00:00 2001 From: Peolite001 Date: Fri, 31 Jul 2026 04:46:21 +0100 Subject: [PATCH 07/11] Fix ERR_ERL_KEY_GEN_IPV6 in express-rate-limit by using ipKeyGenerator --- .../src/middleware/admin-rate-limiter.middleware.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/backend/src/middleware/admin-rate-limiter.middleware.ts b/backend/src/middleware/admin-rate-limiter.middleware.ts index db6c44cc..172df2c2 100644 --- a/backend/src/middleware/admin-rate-limiter.middleware.ts +++ b/backend/src/middleware/admin-rate-limiter.middleware.ts @@ -1,4 +1,4 @@ -import { rateLimit } from 'express-rate-limit'; +import { rateLimit, ipKeyGenerator } from 'express-rate-limit'; import type { Request, Response } from 'express'; export const adminRateLimiter = rateLimit({ @@ -11,13 +11,13 @@ export const adminRateLimiter = rateLimit({ message: 'You have exceeded the admin rate limit. Please try again later.', status: 429, }, - keyGenerator: (req: Request): string => { + keyGenerator: (req: Request, res: Response): string => { // Use x-forwarded-for or remote address as key const forwarded = req.headers['x-forwarded-for']; - if (typeof forwarded === 'string') { - return forwarded.split(',')[0]?.trim() || req.ip || 'unknown'; + if (typeof forwarded === 'string' && forwarded.trim()) { + return forwarded.split(',')[0]?.trim() || ipKeyGenerator(req, res); } - return req.ip ?? 'unknown'; + return ipKeyGenerator(req, res); }, skip: (req: Request): boolean => { // Skip rate limiting in test environment From f92d0360e108e0481dbd3c2a5bf3e0b960a5db18 Mon Sep 17 00:00:00 2001 From: Peolite001 Date: Fri, 31 Jul 2026 04:53:16 +0100 Subject: [PATCH 08/11] Fix TS2345 type error for ipKeyGenerator string arg --- backend/src/middleware/admin-rate-limiter.middleware.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/backend/src/middleware/admin-rate-limiter.middleware.ts b/backend/src/middleware/admin-rate-limiter.middleware.ts index 172df2c2..d770299d 100644 --- a/backend/src/middleware/admin-rate-limiter.middleware.ts +++ b/backend/src/middleware/admin-rate-limiter.middleware.ts @@ -15,9 +15,9 @@ export const adminRateLimiter = rateLimit({ // Use x-forwarded-for or remote address as key const forwarded = req.headers['x-forwarded-for']; if (typeof forwarded === 'string' && forwarded.trim()) { - return forwarded.split(',')[0]?.trim() || ipKeyGenerator(req, res); + return forwarded.split(',')[0]?.trim() || ipKeyGenerator(req.ip ?? 'unknown'); } - return ipKeyGenerator(req, res); + return ipKeyGenerator(req.ip ?? 'unknown'); }, skip: (req: Request): boolean => { // Skip rate limiting in test environment From 07ec4456719e24dd04ee0af2974cc39a710da1f3 Mon Sep 17 00:00:00 2001 From: Peolite001 Date: Fri, 31 Jul 2026 05:01:56 +0100 Subject: [PATCH 09/11] Fix integration test failures related to mocks and DB disconnects --- backend/tests/integration/admin-metrics.test.ts | 1 + backend/tests/integration/stream-lifecycle.test.ts | 3 +++ backend/tests/integration/streams/withdraw.test.ts | 5 +++-- 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/backend/tests/integration/admin-metrics.test.ts b/backend/tests/integration/admin-metrics.test.ts index 8ce5f7c2..ce77c025 100644 --- a/backend/tests/integration/admin-metrics.test.ts +++ b/backend/tests/integration/admin-metrics.test.ts @@ -71,6 +71,7 @@ vi.mock('../../src/lib/redis.js', () => ({ vi.mock('../../src/lib/prisma.js', () => ({ default: mocks.prisma, prisma: mocks.prisma, + pool: { totalCount: 0, idleCount: 0, waitingCount: 0 }, })); vi.mock('../../src/middleware/auth.js', async () => { diff --git a/backend/tests/integration/stream-lifecycle.test.ts b/backend/tests/integration/stream-lifecycle.test.ts index e3f12750..d9e61c26 100644 --- a/backend/tests/integration/stream-lifecycle.test.ts +++ b/backend/tests/integration/stream-lifecycle.test.ts @@ -229,6 +229,9 @@ describe("Stream Lifecycle Integration Tests", () => { }); } await cleanupDatabase(); + }); + + afterAll(async () => { await testPrisma.$disconnect(); }); diff --git a/backend/tests/integration/streams/withdraw.test.ts b/backend/tests/integration/streams/withdraw.test.ts index 44fdb4cc..22e5e30c 100644 --- a/backend/tests/integration/streams/withdraw.test.ts +++ b/backend/tests/integration/streams/withdraw.test.ts @@ -15,6 +15,7 @@ const { }, streamEvent: { create: vi.fn(), + upsert: vi.fn(), }, }, currentUser: { publicKey: '' }, @@ -116,9 +117,9 @@ describe('POST /api/v1/streams/:streamId/withdraw', () => { ); // Verify event creation - expect(mockPrisma.streamEvent.create).toHaveBeenCalledWith( + expect(mockPrisma.streamEvent.upsert).toHaveBeenCalledWith( expect.objectContaining({ - data: expect.objectContaining({ + create: expect.objectContaining({ eventType: 'WITHDRAWN', streamId: BigInt(streamId), transactionHash: 'withdraw-tx-hash', From b5556965d7589dde54445f1f89c6b008ef550eb0 Mon Sep 17 00:00:00 2001 From: Peolite001 Date: Fri, 31 Jul 2026 05:10:57 +0100 Subject: [PATCH 10/11] Fix BigInt conversions and mock variable hoisting in tests --- backend/tests/eventRace.test.ts | 2 +- backend/tests/indexer-state.test.ts | 6 ++++-- backend/tests/integration/pause-resume.regression.test.ts | 2 +- backend/tests/integration/stream-actions.test.ts | 5 +++-- 4 files changed, 9 insertions(+), 6 deletions(-) diff --git a/backend/tests/eventRace.test.ts b/backend/tests/eventRace.test.ts index 361f195d..4eb71145 100644 --- a/backend/tests/eventRace.test.ts +++ b/backend/tests/eventRace.test.ts @@ -77,7 +77,7 @@ describe('Action Controller vs Worker Event Write Race Guard (Issue #831)', () = }, }, create: expect.objectContaining({ - streamId: 100, + streamId: BigInt(100), eventType: 'WITHDRAWN', transactionHash: 'tx_race_123', }), diff --git a/backend/tests/indexer-state.test.ts b/backend/tests/indexer-state.test.ts index 05a92a38..4d103b7d 100644 --- a/backend/tests/indexer-state.test.ts +++ b/backend/tests/indexer-state.test.ts @@ -1,7 +1,9 @@ import { describe, it, expect, vi, beforeEach } from 'vitest'; -const mockFindUnique = vi.fn(); -const mockCreate = vi.fn(); +const { mockFindUnique, mockCreate } = vi.hoisted(() => ({ + mockFindUnique: vi.fn(), + mockCreate: vi.fn(), +})); vi.mock('../src/lib/prisma.js', () => ({ prisma: { diff --git a/backend/tests/integration/pause-resume.regression.test.ts b/backend/tests/integration/pause-resume.regression.test.ts index d823d143..dee367f6 100644 --- a/backend/tests/integration/pause-resume.regression.test.ts +++ b/backend/tests/integration/pause-resume.regression.test.ts @@ -144,7 +144,7 @@ describe('Regression #804: Pause/resume controller duplicate StreamEvent', () => expect(mockPrisma.stream.update).toHaveBeenCalledTimes(1); expect(mockPrisma.stream.update).toHaveBeenCalledWith( expect.objectContaining({ - where: { streamId }, + where: { streamId: BigInt(77) }, data: expect.objectContaining({ isPaused: true }), }), ); diff --git a/backend/tests/integration/stream-actions.test.ts b/backend/tests/integration/stream-actions.test.ts index 933fb2db..07211c3a 100644 --- a/backend/tests/integration/stream-actions.test.ts +++ b/backend/tests/integration/stream-actions.test.ts @@ -20,6 +20,7 @@ const { }, streamEvent: { create: vi.fn(), + upsert: vi.fn(), findMany: vi.fn().mockResolvedValue([]), count: vi.fn().mockResolvedValue(0), }, @@ -214,9 +215,9 @@ describe('stream action routes', () => { amount: '100', }); expect(mockWithdraw).toHaveBeenCalledWith(11n, recipient.publicKey()); - expect(mockPrisma.streamEvent.create).toHaveBeenCalledWith( + expect(mockPrisma.streamEvent.upsert).toHaveBeenCalledWith( expect.objectContaining({ - data: expect.objectContaining({ + create: expect.objectContaining({ eventType: 'WITHDRAWN', amount: '100', transactionHash: 'withdraw-tx-hash', From b534807b99039bfa23c3f4b1f5e87782887171fd Mon Sep 17 00:00:00 2001 From: Peolite001 Date: Fri, 31 Jul 2026 05:15:50 +0100 Subject: [PATCH 11/11] Import afterAll in stream-lifecycle.test.ts --- backend/tests/integration/stream-lifecycle.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/tests/integration/stream-lifecycle.test.ts b/backend/tests/integration/stream-lifecycle.test.ts index d9e61c26..272a3c2e 100644 --- a/backend/tests/integration/stream-lifecycle.test.ts +++ b/backend/tests/integration/stream-lifecycle.test.ts @@ -4,7 +4,7 @@ * These tests use real Postgres database and verify the complete pipeline: * event worker → DB update → controller response → SSE broadcast */ -import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"; +import { describe, it, expect, vi, beforeEach, afterEach, afterAll } from "vitest"; import request from "supertest"; import { PrismaClient } from "../../src/generated/prisma/index.js"; import pg from "pg";