diff --git a/backend/src/app.ts b/backend/src/app.ts index 56c3035e..db0dffb3 100644 --- a/backend/src/app.ts +++ b/backend/src/app.ts @@ -18,6 +18,8 @@ import v1Routes from "./routes/v1/index.js"; import healthRoutes from "./routes/health.routes.js"; import "./lib/stream-id.js"; + + const app = express(); const isProduction = process.env.NODE_ENV === "production"; const rawCors = process.env.CORS_ALLOWED_ORIGINS ?? ""; 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..d770299d 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(); + if (typeof forwarded === 'string' && forwarded.trim()) { + return forwarded.split(',')[0]?.trim() || ipKeyGenerator(req.ip ?? 'unknown'); } - return req.ip ?? 'unknown'; + return ipKeyGenerator(req.ip ?? 'unknown'); }, skip: (req: Request): boolean => { // Skip rate limiting in test environment 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.', 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/services/soroban-indexer.service.ts b/backend/src/services/soroban-indexer.service.ts index 62e7a0fd..89487151 100644 --- a/backend/src/services/soroban-indexer.service.ts +++ b/backend/src/services/soroban-indexer.service.ts @@ -185,8 +185,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 4abb54cc..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 = @@ -1126,13 +1120,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 }, @@ -1175,7 +1169,7 @@ export class SorobanEventWorker { metadata: JSON.stringify({ sender, newEndTime, - pausedDuration: additionalPausedDuration, + pausedDuration: Number(additionalPausedDuration), totalPausedDuration: newTotalPausedDuration, }), }, 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/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/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/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/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/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', diff --git a/backend/tests/integration/stream-lifecycle.test.ts b/backend/tests/integration/stream-lifecycle.test.ts index 104a4a63..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"; @@ -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 { @@ -229,6 +229,9 @@ describe("Stream Lifecycle Integration Tests", () => { }); } await cleanupDatabase(); + }); + + afterAll(async () => { await testPrisma.$disconnect(); }); @@ -246,7 +249,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 +516,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 () => { @@ -629,8 +632,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 +771,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 +822,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 +869,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/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', 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/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 cad05ac1..13410543 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, @@ -240,15 +240,15 @@ describe('GET /v1/users/:address/summary', () => { id: '1', createdAt: new Date(), updatedAt: new Date(), - streamId: 1, + streamId: 1n, sender: 'GSENDER', recipient: 'GRECIPIENT', tokenAddress: 'TOKEN', ratePerSecond: '10', depositedAmount: '100', withdrawnAmount: '30', - startTime: 1000, - lastUpdateTime: 2000, + startTime: 1000n, + lastUpdateTime: 2000n, isPaused: false, endTime: null, pausedAt: null, @@ -259,15 +259,15 @@ describe('GET /v1/users/:address/summary', () => { id: '2', createdAt: new Date(), updatedAt: new Date(), - streamId: 2, + streamId: 2n, sender: 'GSENDER2', recipient: 'GRECIPIENT2', tokenAddress: 'TOKEN2', ratePerSecond: '20', depositedAmount: '200', withdrawnAmount: '20', - startTime: 1000, - lastUpdateTime: 2000, + startTime: 1000n, + lastUpdateTime: 2000n, isPaused: false, endTime: null, pausedAt: null, @@ -280,15 +280,15 @@ describe('GET /v1/users/:address/summary', () => { id: '3', createdAt: new Date(), updatedAt: new Date(), - streamId: 11, + streamId: 11n, sender: 'GSENDER3', recipient: 'GRECIPIENT3', tokenAddress: 'TOKEN3', ratePerSecond: '10', depositedAmount: '1000', withdrawnAmount: '100', - startTime: 1000, - lastUpdateTime: 0, + startTime: 1000n, + lastUpdateTime: 0n, isPaused: false, endTime: null, pausedAt: null, @@ -299,15 +299,15 @@ describe('GET /v1/users/:address/summary', () => { id: '4', createdAt: new Date(), updatedAt: new Date(), - streamId: 12, + streamId: 12n, sender: 'GSENDER4', recipient: 'GRECIPIENT4', tokenAddress: 'TOKEN4', ratePerSecond: '5', depositedAmount: '500', withdrawnAmount: '0', - startTime: 1000, - lastUpdateTime: 0, + startTime: 1000n, + lastUpdateTime: 0n, isPaused: false, endTime: null, pausedAt: null, @@ -337,15 +337,15 @@ describe('GET /v1/users/:address/summary', () => { id: '5', createdAt: new Date(), updatedAt: new Date(), - streamId: 13, + streamId: 13n, sender: 'GSENDER5', recipient: 'GRECIPIENT5', tokenAddress: 'TOKEN5', 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 01a39124..11e80fae 100644 --- a/backend/tests/stream.validator.test.ts +++ b/backend/tests/stream.validator.test.ts @@ -61,7 +61,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/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...}> + + + ); } diff --git a/frontend/src/hooks/useIncomingStreams.test.tsx b/frontend/src/hooks/useIncomingStreams.test.tsx index 73226205..ff758bde 100644 --- a/frontend/src/hooks/useIncomingStreams.test.tsx +++ b/frontend/src/hooks/useIncomingStreams.test.tsx @@ -35,9 +35,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", () => { @@ -114,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"), @@ -135,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(); }); }); }); 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); }, };