Skip to content

Commit eab99d1

Browse files
authored
Merge branch 'main' into sorobanService_drop
2 parents c478565 + 8bc17f8 commit eab99d1

66 files changed

Lines changed: 2442 additions & 6876 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66

77
_Programmable, real-time payment streams and recurring subscriptions._
88

9+
i just need to create a draft pr
10+
911
## Overview
1012

1113
FlowFi allows users to create continuous payment streams and recurring subscriptions using stablecoins on the Stellar network. By leveraging Soroban smart contracts, FlowFi enables autonomous accurate-to-the-second distribution of funds.

backend/.env.example

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,9 @@ INDEXER_POLL_INTERVAL_MS=5000
3333
# Ledger sequence to start indexing from on first run (0 = latest)
3434
INDEXER_START_LEDGER=0
3535

36+
# Max stream creation requests per wallet per minute (default: 10)
37+
STREAM_CREATE_RATE_LIMIT=10
38+
3639
# Server-side Stellar secret key used to sign and submit on-chain transactions
3740
# (cancel_stream, top_up_stream). Must be funded on the target network.
3841
KEEPER_SECRET_KEY=

backend/package.json

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -23,38 +23,38 @@
2323
"author": "",
2424
"license": "ISC",
2525
"dependencies": {
26-
"@prisma/adapter-pg": "^7.4.1",
27-
"@stellar/stellar-sdk": "^14.5.0",
26+
"@prisma/adapter-pg": "^7.8.0",
27+
"@stellar/stellar-sdk": "^15.1.0",
2828
"cors": "^2.8.6",
2929
"dotenv": "^17.4.2",
3030
"express": "^5.2.1",
31-
"express-rate-limit": "^8.2.1",
32-
"ioredis": "^5.3.2",
33-
"pg": "^8.18.0",
31+
"express-rate-limit": "^8.5.2",
32+
"ioredis": "^5.11.0",
33+
"pg": "^8.21.0",
3434
"stellar-sdk": "^13.3.0",
35-
"swagger-jsdoc": "^6.2.8",
35+
"swagger-jsdoc": "^6.3.0",
3636
"swagger-ui-express": "^5.0.1",
3737
"winston": "^3.11.0",
38-
"zod": "^4.3.6"
38+
"zod": "^4.4.3"
3939
},
4040
"devDependencies": {
41-
"@prisma/client": "^7.4.1",
41+
"@prisma/client": "^7.8.0",
4242
"@types/cors": "^2.8.19",
4343
"@types/eventsource": "^1.1.15",
4444
"@types/express": "^5.0.6",
4545
"@types/node": "^25.2.3",
46-
"@types/pg": "^8.16.0",
46+
"@types/pg": "^8.20.0",
4747
"@types/supertest": "^7.2.0",
4848
"@types/swagger-jsdoc": "^6.0.4",
4949
"@types/swagger-ui-express": "^4.1.6",
5050
"@vitest/coverage-v8": "^2.1.8",
5151
"eventsource": "^2.0.2",
5252
"nodemon": "^3.1.11",
53-
"prisma": "^7.4.1",
54-
"rollup": "^4.60.2",
53+
"prisma": "^7.8.0",
54+
"rollup": "^4.61.0",
5555
"supertest": "^7.2.2",
5656
"ts-node": "^10.9.2",
57-
"tsx": "^4.19.2",
57+
"tsx": "^4.22.4",
5858
"typescript": "^5.9.3",
5959
"vitest": "^2.1.8"
6060
}

backend/prisma/seed.ts

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,23 +7,29 @@ const pool = new pg.Pool({ connectionString });
77
const adapter = new PrismaPg(pool);
88
const prisma = new PrismaClient({ adapter });
99

10+
// Use stable, checksum-valid Stellar testnet/demo addresses so seeded rows
11+
// render correctly in the frontend and resolve against TOKEN_ADDRESSES.
12+
const DEMO_SENDER_PUBLIC_KEY = 'GCM5WPR4DDR24FSAX5LIEM4J7AI3KOWJYANSXEPKYXCSZOTAYXE75AFN';
13+
const DEMO_RECIPIENT_PUBLIC_KEY = 'GBJCHUKZMTFSLOMNC7P4TS4VJJBTCYL3XKSOLXAUJSD56C4LHND5TWUC';
14+
const DEMO_TOKEN_ADDRESS = 'CBIELTK6YBZJU5UP2WWQEUCYKLPU6AUNZ2BQ4WWFEIE3USCIHMXQDAMA';
15+
1016
async function main() {
1117
console.log('Seeding database...');
1218

1319
// Create example users
1420
const user1 = await prisma.user.upsert({
15-
where: { publicKey: 'GBRPYH6QC6WGLH473XI3CL4B3I754SFSULN5K3X7G3X4I6SGRH3V3U12' },
21+
where: { publicKey: DEMO_SENDER_PUBLIC_KEY },
1622
update: {},
1723
create: {
18-
publicKey: 'GBRPYH6QC6WGLH473XI3CL4B3I754SFSULN5K3X7G3X4I6SGRH3V3U12',
24+
publicKey: DEMO_SENDER_PUBLIC_KEY,
1925
},
2026
});
2127

2228
const user2 = await prisma.user.upsert({
23-
where: { publicKey: 'GDRS6N3K7DQ6GKH47O6E5K5G7B7H7I7J7K7L7M7N7O7P7Q7R7S7T7U7V' },
29+
where: { publicKey: DEMO_RECIPIENT_PUBLIC_KEY },
2430
update: {},
2531
create: {
26-
publicKey: 'GDRS6N3K7DQ6GKH47O6E5K5G7B7H7I7J7K7L7M7N7O7P7Q7R7S7T7U7V',
32+
publicKey: DEMO_RECIPIENT_PUBLIC_KEY,
2733
},
2834
});
2935

@@ -37,7 +43,7 @@ async function main() {
3743
streamId: 101,
3844
sender: user1.publicKey,
3945
recipient: user2.publicKey,
40-
tokenAddress: 'CBTM5D262F6VQY4A6E4F6G7H8I9J0K1L2M3N4O5P6Q7R8S9T0U1V2W3X',
46+
tokenAddress: DEMO_TOKEN_ADDRESS,
4147
ratePerSecond: '100000000', // 10 XLM/sec if decimals=7
4248
depositedAmount: '1000000000000',
4349
withdrawnAmount: '0',

backend/src/app.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,7 @@ app.use((req: Request, res: Response, next: NextFunction) => {
102102
// This was a versioned request, route to v1 handlers
103103
return v1Routes(req, res, next);
104104
}
105-
next(); // Not versioned, continue to deprecated handlers
105+
return next(); // Not versioned, continue to deprecated handlers
106106
});
107107

108108
// Legacy routes (deprecated - redirect to v1)

backend/src/controllers/sse.controller.ts

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -47,27 +47,33 @@ export const subscribe = async (req: Request, res: Response) => {
4747
// Scope: only streams where the authenticated user is sender or recipient
4848
const ownedStreams = await prisma.stream.findMany({
4949
where: { OR: [{ sender: publicKey }, { recipient: publicKey }] },
50-
select: { streamId: true },
50+
select: { streamId: true, sender: true, recipient: true },
5151
});
5252
const ownedIds = new Set(ownedStreams.map((s: { streamId: number }) => String(s.streamId)));
53+
const allowedUserKeys = new Set<string>([publicKey]);
54+
for (const stream of ownedStreams) {
55+
allowedUserKeys.add(stream.sender);
56+
allowedUserKeys.add(stream.recipient);
57+
}
5358

5459
let subscriptions: string[];
5560
if (all) {
5661
// "all" still scoped to the user's own streams
57-
subscriptions = [...ownedIds] as string[];
62+
subscriptions = [...ownedIds];
5863
} else if (streams.length > 0) {
5964
// Only allow subscribing to streams the user owns
6065
subscriptions = streams.filter((id) => ownedIds.has(id));
6166
} else {
62-
subscriptions = [...ownedIds] as string[];
67+
subscriptions = [...ownedIds];
6368
}
6469

65-
subscriptions.push(...users.map((userKey) => `user:${userKey}`));
66-
67-
// Always add user-scoped subscription key
68-
subscriptions.push(`user:${publicKey}`);
70+
const userSubscriptions = new Set<string>([`user:${publicKey}`]);
71+
for (const key of users.filter((k) => allowedUserKeys.has(k))) {
72+
userSubscriptions.add(`user:${key}`);
73+
}
74+
subscriptions.push(...userSubscriptions);
6975

70-
const clientId = `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
76+
const clientId = `${Date.now()}-${Math.random().toString(36).slice(2, 11)}`;
7177

7278
res.writeHead(200, {
7379
'Content-Type': 'text/event-stream',
@@ -80,6 +86,7 @@ export const subscribe = async (req: Request, res: Response) => {
8086
res.write(`data: ${JSON.stringify({ type: 'connected', clientId, requestId })}\n\n`);
8187

8288
sseService.addClient(clientId, res, subscriptions, sourceIp);
89+
return;
8390
} catch (error: unknown) {
8491
if (error instanceof z.ZodError) {
8592
return res.status(400).json({

backend/src/controllers/stream.controller.ts

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,10 @@ import {
1212
resumeStream as sorobanResumeStream,
1313
} from '../services/sorobanService.js';
1414
import type { AuthenticatedRequest } from '../types/auth.types.js';
15+
import { DEFAULT_EVENTS_PAGE_SIZE, MAX_EVENTS_PAGE_SIZE } from '../routes/v1/events.routes.js';
16+
17+
const DEFAULT_STREAM_PAGE_SIZE = 20;
18+
const MAX_STREAM_PAGE_SIZE = 100;
1519

1620
interface UserStreamSummary {
1721
address: string;
@@ -170,8 +174,8 @@ export const listStreams = async (req: Request, res: Response) => {
170174

171175
// Validate and parse pagination parameters
172176
const parsedLimit = Math.min(
173-
typeof limit === 'string' ? (Number.parseInt(limit, 10) || 20) : 20,
174-
100
177+
typeof limit === 'string' ? (Number.parseInt(limit, 10) || DEFAULT_STREAM_PAGE_SIZE) : DEFAULT_STREAM_PAGE_SIZE,
178+
MAX_STREAM_PAGE_SIZE
175179
);
176180
const parsedOffset = typeof offset === 'string' ? (Number.parseInt(offset, 10) || 0) : 0;
177181

@@ -280,13 +284,12 @@ export const getStreamEvents = async (req: Request, res: Response) => {
280284
const rawOffset = req.query['offset'];
281285
const rawPage = req.query['page'];
282286
const cursor = typeof req.query['cursor'] === 'string' ? req.query['cursor'] : undefined;
283-
const direction = req.query['direction'] === 'asc' ? 'asc' as const : 'desc' as const;
284287
const order = req.query['order'] === 'asc' ? 'asc' as const : 'desc' as const;
285288
const eventType = typeof req.query['eventType'] === 'string' ? req.query['eventType'] : undefined;
286289

287290
const limit = Math.min(
288-
rawLimit && typeof rawLimit === 'string' ? (Number.parseInt(rawLimit, 10) || 50) : 50,
289-
500,
291+
rawLimit && typeof rawLimit === 'string' ? (Number.parseInt(rawLimit, 10) || DEFAULT_EVENTS_PAGE_SIZE) : DEFAULT_EVENTS_PAGE_SIZE,
292+
MAX_EVENTS_PAGE_SIZE,
290293
);
291294

292295
let offset = 0;

backend/src/controllers/user.controller.ts

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { prisma } from '../lib/prisma.js';
33
import logger from '../logger.js';
44
import { registerUserSchema } from '../validators/user.validator.js';
55
import type { AuthenticatedRequest } from '../types/auth.types.js';
6+
import { DEFAULT_EVENTS_PAGE_SIZE, MAX_EVENTS_PAGE_SIZE } from '../routes/v1/events.routes.js';
67

78
/**
89
* Register a new wallet public key
@@ -29,7 +30,7 @@ export const registerUser = async (req: Request, res: Response, next: NextFuncti
2930
logger.info(`User registered: ${publicKey}`);
3031
return res.status(201).json(user);
3132
} catch (error) {
32-
next(error);
33+
return next(error);
3334
}
3435
};
3536

@@ -60,7 +61,7 @@ export const getUser = async (req: Request, res: Response, next: NextFunction) =
6061

6162
return res.status(200).json(user);
6263
} catch (error) {
63-
next(error);
64+
return next(error);
6465
}
6566
};
6667

@@ -81,8 +82,8 @@ export const getUserEvents = async (req: Request, res: Response, next: NextFunct
8182
const rawOffset = req.query['offset'];
8283

8384
const limit = Math.min(
84-
rawLimit && typeof rawLimit === 'string' ? (Number.parseInt(rawLimit, 10) || 50) : 50,
85-
200
85+
rawLimit && typeof rawLimit === 'string' ? (Number.parseInt(rawLimit, 10) || DEFAULT_EVENTS_PAGE_SIZE) : DEFAULT_EVENTS_PAGE_SIZE,
86+
MAX_EVENTS_PAGE_SIZE
8687
);
8788
const offset = rawOffset && typeof rawOffset === 'string' ? (Number.parseInt(rawOffset, 10) || 0) : 0;
8889

@@ -118,7 +119,7 @@ export const getUserEvents = async (req: Request, res: Response, next: NextFunct
118119
offset
119120
});
120121
} catch (error) {
121-
next(error);
122+
return next(error);
122123
}
123124
};
124125

@@ -159,6 +160,6 @@ export const getCurrentUser = async (req: Request, res: Response, next: NextFunc
159160

160161
return res.status(200).json(user);
161162
} catch (error) {
162-
next(error);
163+
return next(error);
163164
}
164165
};

backend/src/lib/prisma-sandbox.ts

Lines changed: 0 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,10 @@
11
import { PrismaClient } from '../generated/prisma/index.js';
2-
import { getSandboxConfig } from '../config/sandbox.js';
3-
4-
/**
5-
* Sandbox Prisma Client
6-
*
7-
* Uses a separate database connection for sandbox mode to ensure
8-
* complete isolation from production data.
9-
*/
102

113
const globalForSandboxPrisma = globalThis as unknown as {
124
sandboxPrisma: PrismaClient | undefined;
135
};
146

15-
/**
16-
* Get sandbox Prisma client instance
17-
*
18-
* If SANDBOX_DATABASE_URL is set, uses that database.
19-
* Otherwise, uses the default DATABASE_URL with a sandbox suffix.
20-
*/
217
export function getSandboxPrisma(): PrismaClient {
22-
const config = getSandboxConfig();
23-
24-
// Use sandbox-specific database URL if provided
25-
const databaseUrl = config.databaseUrl ||
26-
(process.env.DATABASE_URL
27-
? `${process.env.DATABASE_URL}_sandbox`
28-
: 'file:./sandbox.db');
29-
308
if (globalForSandboxPrisma.sandboxPrisma) {
319
return globalForSandboxPrisma.sandboxPrisma;
3210
}

backend/src/middleware/error.middleware.ts

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
import type { Request, Response, NextFunction } from 'express';
22
import { Prisma } from '../generated/prisma/index.js';
3-
import { ZodError } from 'zod';
3+
import { ZodError, type ZodIssue } from 'zod';
44
import logger from '../logger.js';
55

66
/**
77
* Global error handler middleware
88
*/
99
export const errorHandler = (
10-
err: any,
10+
err: unknown,
1111
req: Request,
1212
res: Response,
1313
next: NextFunction
@@ -18,7 +18,7 @@ export const errorHandler = (
1818
if (err instanceof ZodError) {
1919
return res.status(400).json({
2020
error: 'Validation Error',
21-
details: err.issues.map((e: any) => ({
21+
details: err.issues.map((e: ZodIssue) => ({
2222
path: e.path.join('.'),
2323
message: e.message
2424
}))
@@ -28,28 +28,28 @@ export const errorHandler = (
2828
// Handle Prisma Errors
2929
if (err instanceof Prisma.PrismaClientKnownRequestError) {
3030
// Unique constraint violation
31-
if (err.code === 'P2002') {
32-
const target = (err.meta?.target as string[])?.join(', ') || 'field';
31+
if ((err as Prisma.PrismaClientKnownRequestError).code === 'P2002') {
32+
const target = ((err as Prisma.PrismaClientKnownRequestError).meta?.target as string[])?.join(', ') || 'field';
3333
return res.status(409).json({
3434
error: 'Conflict Error',
3535
message: `Record with this ${target} already exists.`
3636
});
3737
}
3838

3939
// Record not found
40-
if (err.code === 'P2025') {
40+
if ((err as Prisma.PrismaClientKnownRequestError).code === 'P2025') {
4141
return res.status(404).json({
4242
error: 'Not Found',
43-
message: err.message || 'The requested record was not found.'
43+
message: (err as Prisma.PrismaClientKnownRequestError).message || 'The requested record was not found.'
4444
});
4545
}
4646
}
4747

4848
// Default Error
49-
const statusCode = err.status || err.statusCode || 500;
50-
const message = err.message || 'Internal Server Error';
49+
const statusCode = (err instanceof Error && (err as any).status) || (err instanceof Error && (err as any).statusCode) || 500;
50+
const message = err instanceof Error ? err.message : 'Internal Server Error';
5151

52-
res.status(statusCode).json({
52+
return res.status(statusCode).json({
5353
error: statusCode === 500 ? 'Internal Server Error' : 'Error',
5454
message: statusCode === 500 ? 'A technical error occurred. Please try again later.' : message
5555
});

0 commit comments

Comments
 (0)