Skip to content

Commit 5537e63

Browse files
authored
Merge branch 'main' into docs/backend-env-example-completeness
2 parents 033907e + 4bd2cc2 commit 5537e63

54 files changed

Lines changed: 1578 additions & 274 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
-- Replace the unused (streamId, createdAt) composite with (streamId, timestamp),
2+
-- which matches streamId-scoped event listings that ORDER BY timestamp.
3+
4+
-- CreateIndex
5+
CREATE INDEX IF NOT EXISTS "StreamEvent_streamId_timestamp_idx" ON "StreamEvent"("streamId", "timestamp");
6+
7+
-- DropIndex
8+
DROP INDEX IF EXISTS "StreamEvent_streamId_createdAt_idx";
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
-- Convert on-chain stream identifiers from int4 to bigint (Soroban u64).
2+
-- Drop the FK first so both columns can be widened, then recreate it.
3+
4+
ALTER TABLE "StreamEvent" DROP CONSTRAINT IF EXISTS "StreamEvent_streamId_fkey";
5+
6+
ALTER TABLE "Stream" ALTER COLUMN "streamId" TYPE BIGINT USING ("streamId"::bigint);
7+
ALTER TABLE "StreamEvent" ALTER COLUMN "streamId" TYPE BIGINT USING ("streamId"::bigint);
8+
9+
ALTER TABLE "StreamEvent"
10+
ADD CONSTRAINT "StreamEvent_streamId_fkey"
11+
FOREIGN KEY ("streamId") REFERENCES "Stream"("streamId")
12+
ON DELETE RESTRICT ON UPDATE CASCADE;

backend/prisma/schema.prisma

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ model User {
2727
// Stream model - mirrors on-chain stream state for fast querying
2828
model Stream {
2929
id String @id @default(uuid())
30-
streamId Int @unique // On-chain stream ID
30+
streamId BigInt @unique // On-chain stream ID (Soroban u64)
3131
sender String // Sender's Stellar public key
3232
recipient String // Recipient's Stellar public key
3333
tokenAddress String // Token contract address
@@ -68,7 +68,7 @@ model IndexerState {
6868
// StreamEvent model - indexer events for tracking all on-chain stream activities
6969
model StreamEvent {
7070
id String @id @default(uuid())
71-
streamId Int // Reference to on-chain stream ID
71+
streamId BigInt // Reference to on-chain stream ID (Soroban u64)
7272
eventType String // EventType: "CREATED", "TOPPED_UP", "WITHDRAWN", "CANCELLED", "COMPLETED", "PAUSED", "RESUMED"
7373
amount String? // Amount involved in the event (for top-ups, withdrawals)
7474
transactionHash String // Stellar transaction hash
@@ -86,5 +86,6 @@ model StreamEvent {
8686
@@index([timestamp])
8787
@@index([transactionHash])
8888
@@index([createdAt])
89-
@@index([streamId, createdAt])
89+
@@index([streamId, timestamp])
90+
@@unique([transactionHash, eventType])
9091
}

backend/src/app.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import { requestIdMiddleware } from "./middleware/requestId.js";
1616
import v1Routes from "./routes/v1/index.js";
1717

1818
import healthRoutes from "./routes/health.routes.js";
19+
import "./lib/stream-id.js";
1920

2021
const app = express();
2122
const isProduction = process.env.NODE_ENV === "production";

backend/src/controllers/sse.controller.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,8 @@ export const subscribe = async (req: Request, res: Response) => {
3131

3232
try {
3333
const sourceIp = getClientIp(req);
34-
const capacity = sseService.checkCapacity(sourceIp);
34+
const authUserId = (req as AuthenticatedRequest).user?.publicKey;
35+
const capacity = sseService.checkCapacity(sourceIp, authUserId);
3536
if (!capacity.allowed) {
3637
if (capacity.retryAfterSeconds) {
3738
res.setHeader('Retry-After', String(capacity.retryAfterSeconds));
@@ -51,7 +52,7 @@ export const subscribe = async (req: Request, res: Response) => {
5152
where: { OR: [{ sender: publicKey }, { recipient: publicKey }] },
5253
select: { streamId: true, sender: true, recipient: true },
5354
});
54-
const ownedIds = new Set(ownedStreams.map((s: { streamId: number }) => String(s.streamId)));
55+
const ownedIds = new Set(ownedStreams.map((s: { streamId: bigint }) => String(s.streamId)));
5556
const allowedUserKeys = new Set<string>([publicKey]);
5657
for (const stream of ownedStreams) {
5758
allowedUserKeys.add(stream.sender);
@@ -87,7 +88,7 @@ export const subscribe = async (req: Request, res: Response) => {
8788
const requestId = requestContext.getStore()?.requestId;
8889
res.write(`data: ${JSON.stringify({ type: 'connected', clientId, requestId })}\n\n`);
8990

90-
sseService.addClient(clientId, res, subscriptions, sourceIp);
91+
sseService.addClient(clientId, res, subscriptions, sourceIp, publicKey);
9192
return;
9293
} catch (error: unknown) {
9394
if (error instanceof z.ZodError) {

backend/src/controllers/stream.controller.ts

Lines changed: 17 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import {
1313
resumeStream as sorobanResumeStream,
1414
} from "../services/sorobanService.js";
1515
import type { AuthenticatedRequest } from "../types/auth.types.js";
16+
import { parseStreamId } from "../lib/stream-id.js";
1617
import {
1718
DEFAULT_EVENTS_PAGE_SIZE,
1819
MAX_EVENTS_PAGE_SIZE,
@@ -128,10 +129,10 @@ export const createStream = async (req: Request, res: Response) => {
128129
});
129130
}
130131

131-
const parsedStreamId = Number.parseInt(streamId, 10);
132+
const parsedStreamId = parseStreamId(streamId);
132133
const parsedStartTime = Number.parseInt(startTime, 10);
133134

134-
if (!Number.isFinite(parsedStreamId)) {
135+
if (parsedStreamId === null) {
135136
return res
136137
.status(400)
137138
.json({ error: "Invalid streamId: must be a valid integer" });
@@ -347,9 +348,8 @@ export const getStream = async (req: Request, res: Response) => {
347348
const streamIdParam = Array.isArray(req.params.streamId)
348349
? req.params.streamId[0]
349350
: req.params.streamId;
350-
const parsedStreamId = Number.parseInt(streamIdParam ?? "", 10);
351-
352-
if (!Number.isFinite(parsedStreamId)) {
351+
const parsedStreamId = parseStreamId(streamIdParam);
352+
if (parsedStreamId === null) {
353353
return res.status(400).json({ error: "Invalid streamId parameter" });
354354
}
355355

@@ -398,9 +398,8 @@ export const getStreamEvents = async (req: Request, res: Response) => {
398398
const streamIdParam = Array.isArray(req.params.streamId)
399399
? req.params.streamId[0]
400400
: req.params.streamId;
401-
const parsedStreamId = Number.parseInt(streamIdParam ?? "", 10);
402-
403-
if (!Number.isFinite(parsedStreamId)) {
401+
const parsedStreamId = parseStreamId(streamIdParam);
402+
if (parsedStreamId === null) {
404403
return res.status(400).json({ error: "Invalid streamId parameter" });
405404
}
406405

@@ -489,9 +488,8 @@ export const getStreamClaimableAmount = async (req: Request, res: Response) => {
489488
const streamIdParam = Array.isArray(req.params.streamId)
490489
? req.params.streamId[0]
491490
: req.params.streamId;
492-
const parsedStreamId = Number.parseInt(streamIdParam ?? "", 10);
493-
494-
if (!Number.isFinite(parsedStreamId)) {
491+
const parsedStreamId = parseStreamId(streamIdParam);
492+
if (parsedStreamId === null) {
495493
return res.status(400).json({ error: "Invalid streamId parameter" });
496494
}
497495

@@ -686,13 +684,12 @@ const topUpBodySchema = z.object({
686684
* Adds tokens to a running stream. Only the stream sender may call this.
687685
*/
688686
export const topUpStreamHandler = async (req: Request, res: Response) => {
689-
const streamId = parseInt(
687+
const streamId = parseStreamId(
690688
Array.isArray(req.params.streamId)
691-
? req.params.streamId[0]!
692-
: (req.params.streamId ?? ""),
693-
10,
689+
? req.params.streamId[0]
690+
: req.params.streamId,
694691
);
695-
if (isNaN(streamId)) {
692+
if (streamId === null) {
696693
return res.status(400).json({ error: "Invalid streamId" });
697694
}
698695

@@ -769,9 +766,8 @@ export const pauseStream = async (req: Request, res: Response) => {
769766
const streamIdParam = Array.isArray(req.params.streamId)
770767
? req.params.streamId[0]
771768
: req.params.streamId;
772-
const parsedStreamId = Number.parseInt(streamIdParam ?? "", 10);
773-
774-
if (!Number.isFinite(parsedStreamId)) {
769+
const parsedStreamId = parseStreamId(streamIdParam);
770+
if (parsedStreamId === null) {
775771
return res.status(400).json({ error: "Invalid streamId parameter" });
776772
}
777773

@@ -861,9 +857,8 @@ export const resumeStream = async (req: Request, res: Response) => {
861857
const streamIdParam = Array.isArray(req.params.streamId)
862858
? req.params.streamId[0]
863859
: req.params.streamId;
864-
const parsedStreamId = Number.parseInt(streamIdParam ?? "", 10);
865-
866-
if (!Number.isFinite(parsedStreamId)) {
860+
const parsedStreamId = parseStreamId(streamIdParam);
861+
if (parsedStreamId === null) {
867862
return res.status(400).json({ error: "Invalid streamId parameter" });
868863
}
869864

backend/src/controllers/stream/cancel.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import logger from '../../logger.js';
44
import * as sorobanService from '../../services/sorobanService.js';
55
import type { AuthenticatedRequest } from '../../types/auth.types.js';
66
import * as streamRepository from '../../repositories/stream.repository.js';
7+
import { parseStreamId } from '../../lib/stream-id.js';
78

89
/**
910
* @openapi
@@ -55,7 +56,10 @@ export const cancelStreamHandler = async (req: AuthenticatedRequest, res: Respon
5556
return res.status(400).json({ error: 'Missing streamId parameter' });
5657
}
5758

58-
const parsedStreamId = parseInt(streamId, 10);
59+
const parsedStreamId = parseStreamId(streamId);
60+
if (parsedStreamId === null) {
61+
return res.status(400).json({ error: 'Invalid streamId parameter' });
62+
}
5963

6064
// 1. Fetch stream from DB
6165
const stream = await prisma.stream.findUnique({

backend/src/lib/stream-id.ts

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
/**
2+
* Helpers for on-chain stream IDs (Soroban u64).
3+
*
4+
* DB columns are Prisma BigInt / Postgres bigint so values above int4 max
5+
* (2_147_483_647) round-trip without overflow. Prefer bigint in application
6+
* code; never use Number()/parseInt for identifiers that may exceed 2^53-1.
7+
*/
8+
9+
const U64_DECIMAL = /^\d+$/;
10+
11+
/**
12+
* Parse a path/query/body streamId into a bigint.
13+
* Accepts decimal strings and non-negative integers / bigints.
14+
*/
15+
export function parseStreamId(raw: unknown): bigint | null {
16+
if (typeof raw === 'bigint') {
17+
return raw >= 0n ? raw : null;
18+
}
19+
if (typeof raw === 'number') {
20+
if (!Number.isInteger(raw) || raw < 0 || !Number.isSafeInteger(raw)) {
21+
return null;
22+
}
23+
return BigInt(raw);
24+
}
25+
if (typeof raw === 'string') {
26+
const trimmed = raw.trim();
27+
if (!U64_DECIMAL.test(trimmed)) return null;
28+
try {
29+
return BigInt(trimmed);
30+
} catch {
31+
return null;
32+
}
33+
}
34+
return null;
35+
}
36+
37+
/**
38+
* JSON-safe encoding: numbers stay numbers within Number.MAX_SAFE_INTEGER
39+
* (covers all historical int4 IDs and the >2^31 cases the bug cares about
40+
* until 2^53); larger u64 values become decimal strings.
41+
*/
42+
export function streamIdToJson(streamId: bigint): number | string {
43+
const asNumber = Number(streamId);
44+
return Number.isSafeInteger(asNumber) ? asNumber : streamId.toString();
45+
}
46+
47+
// Ensure Express / SSE JSON.stringify can serialize Prisma BigInt fields.
48+
const bigIntProto = BigInt.prototype as unknown as { toJSON?: () => number | string };
49+
if (typeof bigIntProto.toJSON !== 'function') {
50+
bigIntProto.toJSON = function bigIntToJSON(this: bigint) {
51+
return streamIdToJson(this);
52+
};
53+
}

backend/src/repositories/stream.repository.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { prisma } from '../lib/prisma.js';
33
/**
44
* Update the status and active flag of a stream in the database.
55
*/
6-
export const updateStatus = async (streamId: number, status: 'ACTIVE' | 'CANCELLED' | 'COMPLETED' | 'PAUSED') => {
6+
export const updateStatus = async (streamId: bigint, status: 'ACTIVE' | 'CANCELLED' | 'COMPLETED' | 'PAUSED') => {
77
return prisma.stream.update({
88
where: { streamId },
99
data: {

0 commit comments

Comments
 (0)