diff --git a/backend/prisma/schema.prisma b/backend/prisma/schema.prisma index 4ff22965..4b856f82 100644 --- a/backend/prisma/schema.prisma +++ b/backend/prisma/schema.prisma @@ -87,5 +87,4 @@ model StreamEvent { @@index([transactionHash]) @@index([createdAt]) @@index([streamId, timestamp]) - @@unique([transactionHash, eventType]) } 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..09160f03 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() ?? 'unknown'; } return req.ip ?? 'unknown'; }, diff --git a/backend/src/middleware/auth.ts b/backend/src/middleware/auth.ts index 9402a943..7a21f0e4 100644 --- a/backend/src/middleware/auth.ts +++ b/backend/src/middleware/auth.ts @@ -31,6 +31,9 @@ if (isProduction) { const JWT_EXPIRY_SECONDS = 3600; // 1 hour max per spec +const JWT_ISSUER = process.env.JWT_ISSUER || 'flowfi-api'; +const JWT_AUDIENCE = process.env.JWT_AUDIENCE || 'flowfi-api'; + const STELLAR_NETWORK = process.env.STELLAR_NETWORK === 'mainnet' ? StellarSdk.Networks.PUBLIC @@ -130,6 +133,11 @@ export function verifyJwt(token: string): { publicKey: string } | null { return null; } + // Verify issuer and audience + if (payload.iss !== JWT_ISSUER || payload.aud !== JWT_AUDIENCE) { + return null; + } + return { publicKey: payload.sub }; } catch { return null; @@ -205,7 +213,7 @@ export function verifyChallenge(req: Request, res: Response): void { challenges.delete(publicKey); const now = Math.floor(Date.now() / 1000); - const token = signJwt({ sub: publicKey, iat: now, exp: now + JWT_EXPIRY_SECONDS }); + const token = signJwt({ sub: publicKey, iat: now, exp: now + JWT_EXPIRY_SECONDS, iss: JWT_ISSUER, aud: JWT_AUDIENCE }); res.json({ token, expiresIn: JWT_EXPIRY_SECONDS }); } catch (err) { logger.error('[Auth] verifyChallenge error:', err); diff --git a/backend/src/routes/health.routes.ts b/backend/src/routes/health.routes.ts index 3ecf39af..679f4ab9 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 ? '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..4d8a4fe9 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(String(startTimeRaw ?? timestamp)); if (!sender || !recipient || !tokenAddress || !ratePerSecond || !depositedAmount) return; diff --git a/backend/src/types/auth.types.ts b/backend/src/types/auth.types.ts index 274be4d4..8b9a3ebc 100644 --- a/backend/src/types/auth.types.ts +++ b/backend/src/types/auth.types.ts @@ -30,4 +30,6 @@ export interface SEP10TokenPayload { sub: string; // Stellar public key iat: number; // Issued at exp: number; // Expiration + iss: string; // Issuer + aud: string; // Audience } diff --git a/backend/src/workers/soroban-event-worker.ts b/backend/src/workers/soroban-event-worker.ts index 4abb54cc..b8b2a22a 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,13 +112,6 @@ 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 = process.env.SOROBAN_RPC_URL ?? "https://soroban-testnet.stellar.org"; @@ -1128,7 +1120,7 @@ export class SorobanEventWorker { // Calculate the duration of this pause interval let additionalPausedDuration = 0; if (currentStream.pausedAt) { - additionalPausedDuration = timestamp - currentStream.pausedAt; + additionalPausedDuration = timestamp - Number(currentStream.pausedAt); } const newTotalPausedDuration = diff --git a/backend/tests/auth-jwt.test.ts b/backend/tests/auth-jwt.test.ts index 3b32d043..8ce5b048 100644 --- a/backend/tests/auth-jwt.test.ts +++ b/backend/tests/auth-jwt.test.ts @@ -16,14 +16,14 @@ describe('JWT helpers', () => { it('round-trips through verifyJwt', async () => { const now = Math.floor(Date.now() / 1000); - const token = signJwt({ sub: 'GTESTPUBLICKEY123', iat: now, exp: now + 3600 }); + const token = signJwt({ sub: 'GTESTPUBLICKEY123', iat: now, exp: now + 3600, iss: 'flowfi-api', aud: 'flowfi-api' }); expect(verifyJwt(token)).toEqual({ publicKey: 'GTESTPUBLICKEY123' }); }); it('returns null for a tampered header', async () => { const now = Math.floor(Date.now() / 1000); - const token = signJwt({ sub: 'GTESTPUBLICKEY123', iat: now, exp: now + 3600 }); + const token = signJwt({ sub: 'GTESTPUBLICKEY123', iat: now, exp: now + 3600, iss: 'flowfi-api', aud: 'flowfi-api' }); const parts = token.split('.') as [string, string, string]; parts[0] = parts[0].slice(0, -1) + (parts[0].slice(-1) === 'A' ? 'B' : 'A'); @@ -32,7 +32,7 @@ describe('JWT helpers', () => { it('returns null for a tampered body', async () => { const now = Math.floor(Date.now() / 1000); - const token = signJwt({ sub: 'GTESTPUBLICKEY123', iat: now, exp: now + 3600 }); + const token = signJwt({ sub: 'GTESTPUBLICKEY123', iat: now, exp: now + 3600, iss: 'flowfi-api', aud: 'flowfi-api' }); const parts = token.split('.') as [string, string, string]; parts[1] = parts[1].slice(0, -1) + (parts[1].slice(-1) === 'A' ? 'B' : 'A'); @@ -41,7 +41,7 @@ describe('JWT helpers', () => { it('returns null for a tampered signature', async () => { const now = Math.floor(Date.now() / 1000); - const token = signJwt({ sub: 'GTESTPUBLICKEY123', iat: now, exp: now + 3600 }); + const token = signJwt({ sub: 'GTESTPUBLICKEY123', iat: now, exp: now + 3600, iss: 'flowfi-api', aud: 'flowfi-api' }); const parts = token.split('.') as [string, string, string]; // Replace the signature with invalid data to ensure verification fails parts[2] = 'invalid-signature-data-1234567890abcdef'; @@ -51,7 +51,21 @@ describe('JWT helpers', () => { it('returns null for an expired token', async () => { const now = Math.floor(Date.now() / 1000); - const token = signJwt({ sub: 'GTESTPUBLICKEY123', iat: now - 3600, exp: now - 1 }); + const token = signJwt({ sub: 'GTESTPUBLICKEY123', iat: now - 3600, exp: now - 1, iss: 'flowfi-api', aud: 'flowfi-api' }); + + expect(verifyJwt(token)).toBeNull(); + }); + + it('returns null for a token with wrong audience', async () => { + const now = Math.floor(Date.now() / 1000); + const token = signJwt({ sub: 'GTESTPUBLICKEY123', iat: now, exp: now + 3600, iss: 'flowfi-api', aud: 'wrong-audience' }); + + expect(verifyJwt(token)).toBeNull(); + }); + + it('returns null for a token with wrong issuer', async () => { + const now = Math.floor(Date.now() / 1000); + const token = signJwt({ sub: 'GTESTPUBLICKEY123', iat: now, exp: now + 3600, iss: 'wrong-issuer', aud: 'flowfi-api' }); expect(verifyJwt(token)).toBeNull(); }); diff --git a/backend/tests/auth.test.ts b/backend/tests/auth.test.ts index 03c29ac7..2540b47d 100644 --- a/backend/tests/auth.test.ts +++ b/backend/tests/auth.test.ts @@ -307,9 +307,13 @@ describe('Authentication & Middleware Tests', () => { it('test_admin_middleware_rejects_non_admin_token', async () => { const nonAdminKeypair = makeKeypair(); + const now = Math.floor(Date.now() / 1000); const token = signJwt({ sub: nonAdminKeypair.publicKey(), - exp: Math.floor(Date.now() / 1000) + 3600, + iat: now, + exp: now + 3600, + iss: 'flowfi-api', + aud: 'flowfi-api', }); // Set admin key to something else @@ -326,9 +330,13 @@ describe('Authentication & Middleware Tests', () => { it('test_admin_middleware_accepts_admin_token', async () => { const adminKeypair = makeKeypair(); + const now = Math.floor(Date.now() / 1000); const token = signJwt({ sub: adminKeypair.publicKey(), - exp: Math.floor(Date.now() / 1000) + 3600, + iat: now, + exp: now + 3600, + iss: 'flowfi-api', + aud: 'flowfi-api', }); process.env.ADMIN_PUBLIC_KEY = adminKeypair.publicKey(); @@ -343,9 +351,13 @@ describe('Authentication & Middleware Tests', () => { it('test_admin_middleware_fails_closed_when_key_unset', async () => { const keypair = makeKeypair(); + const now = Math.floor(Date.now() / 1000); const token = signJwt({ sub: keypair.publicKey(), - exp: Math.floor(Date.now() / 1000) + 3600, + iat: now, + exp: now + 3600, + iss: 'flowfi-api', + aud: 'flowfi-api', }); // Unset the admin key @@ -371,9 +383,13 @@ describe('Authentication & Middleware Tests', () => { it('test_events_endpoint_allows_authenticated_matching_address', async () => { const keypair = makeKeypair(); + const now = Math.floor(Date.now() / 1000); const token = signJwt({ sub: keypair.publicKey(), - exp: Math.floor(Date.now() / 1000) + 3600, + iat: now, + exp: now + 3600, + iss: 'flowfi-api', + aud: 'flowfi-api', }); const res = await request(app) @@ -388,9 +404,13 @@ describe('Authentication & Middleware Tests', () => { it('test_events_endpoint_rejects_authenticated_mismatched_address', async () => { const keypair = makeKeypair(); const otherKeypair = makeKeypair(); + const now = Math.floor(Date.now() / 1000); const token = signJwt({ sub: keypair.publicKey(), - exp: Math.floor(Date.now() / 1000) + 3600, + iat: now, + exp: now + 3600, + iss: 'flowfi-api', + aud: 'flowfi-api', }); const res = await request(app) diff --git a/backend/tests/claimable.service.test.ts b/backend/tests/claimable.service.test.ts index 5b0bb8e2..5ca929d9 100644 --- a/backend/tests/claimable.service.test.ts +++ b/backend/tests/claimable.service.test.ts @@ -3,7 +3,7 @@ import { ClaimableAmountService } from '../src/services/claimable.service.js'; function makeStreamState(overrides: Partial[0]> = {}) { return { - streamId: 1, + streamId: 1n, ratePerSecond: '10', depositedAmount: '100', withdrawnAmount: '0', @@ -35,7 +35,7 @@ describe('ClaimableAmountService', () => { const result = service.getClaimableAmount({ ...makeStreamState({ - streamId: 1, + streamId: 1n, ratePerSecond: '5', depositedAmount: '500', withdrawnAmount: '100', @@ -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,7 +146,7 @@ describe('ClaimableAmountService', () => { }); const preWithdrawalState = makeStreamState({ - streamId: 7, + streamId: 7n, ratePerSecond: '10', depositedAmount: '1000', withdrawnAmount: '0', @@ -166,7 +166,7 @@ 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', @@ -191,7 +191,7 @@ describe('ClaimableAmountService', () => { const result = service.getClaimableAmount({ ...makeStreamState({ - streamId: 6, + streamId: 6n, ratePerSecond: i128Max, depositedAmount: i128Max, withdrawnAmount: '42', @@ -228,7 +228,7 @@ describe('ClaimableAmountService', () => { const result = service.getClaimableAmount({ ...makeStreamState({ - streamId: 10_000 + iteration, + streamId: 10_000n + BigInt(iteration), ratePerSecond: rate.toString(), depositedAmount: deposited.toString(), withdrawnAmount: withdrawn.toString(), diff --git a/backend/tests/eventRace.test.ts b/backend/tests/eventRace.test.ts index 361f195d..a1ccb20d 100644 --- a/backend/tests/eventRace.test.ts +++ b/backend/tests/eventRace.test.ts @@ -54,7 +54,7 @@ describe('Action Controller vs Worker Event Write Race Guard (Issue #831)', () = it('withdrawHandler uses upsert on transactionHash_eventType preventing P2002 duplicate crashes when worker processes event first', async () => { const mockStream = { - streamId: 100, + streamId: 100n, recipient: 'GRECIPIENT', withdrawnAmount: '0', depositedAmount: '1000', @@ -77,7 +77,7 @@ describe('Action Controller vs Worker Event Write Race Guard (Issue #831)', () = }, }, create: expect.objectContaining({ - streamId: 100, + streamId: 100n, 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..acdacfd3 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..f56fd71d 100644 --- a/backend/tests/integration/admin-metrics.test.ts +++ b/backend/tests/integration/admin-metrics.test.ts @@ -51,6 +51,11 @@ const mocks = vi.hoisted(() => { }, $disconnect: vi.fn(), }, + pool: { + totalCount: 0, + idleCount: 0, + waitingCount: 0, + }, }; }); @@ -71,6 +76,7 @@ vi.mock('../../src/lib/redis.js', () => ({ vi.mock('../../src/lib/prisma.js', () => ({ default: mocks.prisma, prisma: mocks.prisma, + pool: mocks.pool, })); vi.mock('../../src/middleware/auth.js', async () => { diff --git a/backend/tests/integration/events-list.test.ts b/backend/tests/integration/events-list.test.ts index caeb4fe5..2ae1ac85 100644 --- a/backend/tests/integration/events-list.test.ts +++ b/backend/tests/integration/events-list.test.ts @@ -60,7 +60,7 @@ import app from '../../src/app.js'; import { signJwt } from '../../src/middleware/auth.js'; const ADDR = 'GABC123XYZ456DEF789GHI012JKL345MNO678PQR901STU234VWX567YZA'; -const token = signJwt({ sub: ADDR, iat: Math.floor(Date.now() / 1000), exp: Math.floor(Date.now() / 1000) + 3600 }); +const token = signJwt({ sub: ADDR, iat: Math.floor(Date.now() / 1000), exp: Math.floor(Date.now() / 1000) + 3600, iss: 'flowfi-api', aud: 'flowfi-api' }); function makeEvent(overrides: Partial> = {}) { return { @@ -101,6 +101,8 @@ describe('GET /v1/events', () => { sub: 'GOTHER123XYZ456DEF789GHI012JKL345MNO678PQR901STU234VWX567YZA', iat: Math.floor(Date.now() / 1000), exp: Math.floor(Date.now() / 1000) + 3600, + iss: 'flowfi-api', + aud: 'flowfi-api', }); const res = await request(app) .get(`/v1/events?address=${ADDR}`) diff --git a/backend/tests/integration/pause-resume.regression.test.ts b/backend/tests/integration/pause-resume.regression.test.ts index d823d143..c5076f24 100644 --- a/backend/tests/integration/pause-resume.regression.test.ts +++ b/backend/tests/integration/pause-resume.regression.test.ts @@ -82,7 +82,7 @@ describe('Regression #804: Pause/resume controller duplicate StreamEvent', () => it('pauses a stream and only writes one PAUSED event via indexer', async () => { const sender = makeKeypair(); const token = await getValidJwt(sender); - const streamId = 77; + const streamId = 77n; // 1. Controller flow mockPrisma.stream.findUnique.mockResolvedValue({ diff --git a/backend/tests/integration/sse-subscribe.test.ts b/backend/tests/integration/sse-subscribe.test.ts index 0ae0af25..38595087 100644 --- a/backend/tests/integration/sse-subscribe.test.ts +++ b/backend/tests/integration/sse-subscribe.test.ts @@ -58,7 +58,7 @@ import app from '../../src/app.js'; import { signJwt } from '../../src/middleware/auth.js'; const ADDR = 'GABC123XYZ456DEF789GHI012JKL345MNO678PQR901STU234VWX567YZA'; -const token = signJwt({ sub: ADDR, iat: Math.floor(Date.now() / 1000), exp: Math.floor(Date.now() / 1000) + 3600 }); +const token = signJwt({ sub: ADDR, iat: Math.floor(Date.now() / 1000), exp: Math.floor(Date.now() / 1000) + 3600, iss: 'flowfi-api', aud: 'flowfi-api' }); describe('GET /v1/events/subscribe', () => { beforeEach(() => { 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/streams.test.ts b/backend/tests/integration/streams.test.ts index c66e1e8b..d7f1eb14 100644 --- a/backend/tests/integration/streams.test.ts +++ b/backend/tests/integration/streams.test.ts @@ -233,13 +233,13 @@ describe('GET /v1/streams/:id/events — pagination and eventType filter', () => expect(res.body.hasMore).toBe(false); const callArgs = mockPrisma.streamEvent.findMany.mock.calls[0]![0] as { - where: { streamId: number }; - orderBy: { timestamp: string }; + where: { streamId: bigint }; + orderBy: Array<{ timestamp: string }>; take: number; skip: number; }; - expect(callArgs.where.streamId).toBe(1); - expect(callArgs.orderBy).toEqual({ timestamp: 'desc' }); + expect(callArgs.where.streamId).toBe(1n); + expect(callArgs.orderBy).toEqual([{ timestamp: 'desc' }, { id: 'desc' }]); expect(callArgs.take).toBe(10); expect(callArgs.skip).toBe(0); }); 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..6312c733 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(1700000000n + BigInt(Math.floor(1200 / 10)) + 0n); }); 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..a54a9c85 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,8 +191,8 @@ describe('Soroban Service', () => { ) ); - await expect(getStreamFromChain(7)).resolves.toEqual({ - streamId: 7, + await expect(getStreamFromChain(7n)).resolves.toEqual({ + streamId: 7n, sender, recipient, tokenAddress, @@ -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.controller.test.ts b/backend/tests/stream.controller.test.ts index 0310ed67..b2c5d843 100644 --- a/backend/tests/stream.controller.test.ts +++ b/backend/tests/stream.controller.test.ts @@ -5,6 +5,13 @@ import { claimableAmountService } from '../src/services/claimable.service.js'; import * as sorobanService from '../src/services/sorobanService.js'; import type { Request, Response } from 'express'; +type TestRequest = Partial & { + body: Record; + query: Record; + params: Record; + user?: { publicKey: string }; +}; + vi.mock('../src/lib/prisma.js', () => ({ prisma: { stream: { @@ -247,8 +254,8 @@ describe("Stream Controller", () => { expect(res.status).toHaveBeenCalledWith(200); expect(prisma.streamEvent.findMany).toHaveBeenCalledWith( expect.objectContaining({ - where: { streamId: 123 }, - orderBy: { timestamp: 'desc' }, + where: { streamId: 123n }, + orderBy: [{ timestamp: 'desc' }, { id: 'desc' }], take: 10, skip: 0, }), diff --git a/backend/tests/stream.test.ts b/backend/tests/stream.test.ts index cad05ac1..5896e8f9 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', @@ -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..ab53e837 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', () => {