Skip to content

Commit e37837f

Browse files
committed
Merge remote-tracking branch 'upstream/main' into fix/issues-547
2 parents 59d46b6 + 215b769 commit e37837f

97 files changed

Lines changed: 4079 additions & 7304 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.

.github/workflows/ci.yml

Lines changed: 22 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,9 @@ name: CI
44

55
on:
66
push:
7-
branches: [ main, develop ]
7+
branches: [main, develop]
88
pull_request:
9-
branches: [ main, develop ]
9+
branches: [main, develop]
1010

1111
jobs:
1212
frontend:
@@ -19,14 +19,13 @@ jobs:
1919
- name: Setup Node.js
2020
uses: actions/setup-node@v4
2121
with:
22-
node-version: '20'
23-
cache: 'npm'
22+
node-version: "20"
23+
cache: "npm"
2424
cache-dependency-path: package-lock.json
2525

2626
- name: Install dependencies
2727
run: npm ci
2828

29-
3029
- name: Lint
3130
run: npm run lint
3231
working-directory: frontend
@@ -68,8 +67,8 @@ jobs:
6867
- name: Setup Node.js
6968
uses: actions/setup-node@v4
7069
with:
71-
node-version: '20'
72-
cache: 'npm'
70+
node-version: "20"
71+
cache: "npm"
7372
cache-dependency-path: package-lock.json
7473

7574
- name: Install dependencies
@@ -128,7 +127,7 @@ jobs:
128127
- name: Rust Cache
129128
uses: Swatinem/rust-cache@v2
130129
with:
131-
workspaces: "contracts -> target"
130+
workspace: "contracts -> target"
132131

133132
- name: Check Formatting
134133
run: cargo fmt --all -- --check
@@ -167,19 +166,28 @@ jobs:
167166
- name: Install Stellar CLI
168167
run: |
169168
curl -fsSL https://github.com/stellar/stellar-cli/raw/main/install.sh | sh -s -- --install-deps
169+
echo "$HOME/.stellar-cli/bin" >> $GITHUB_PATH
170170
shell: bash
171171

172172
- name: Optimize WASM files
173173
run: |
174174
set -euo pipefail
175-
WASMS=$(find contracts/target -type f -name "*.wasm" -print)
175+
176+
# Target the precise release build directory
177+
RELEASE_DIR="contracts/target/wasm32-unknown-unknown/release"
178+
179+
# Use your strict selection logic to target un-optimized raw binaries only
180+
WASMS=$(find "$RELEASE_DIR" -maxdepth 1 -type f -name "*.wasm" ! -name "*.optimized.wasm")
181+
176182
if [ -z "$WASMS" ]; then
177-
echo "No wasm files found"
183+
echo "Error: No raw WASM files found in $RELEASE_DIR"
178184
exit 1
179185
fi
186+
180187
for w in $WASMS; do
181-
out="${w%%.wasm}.optimized.wasm"
182-
echo "Optimizing $w -> $out"
188+
filename=$(basename "$w")
189+
out="$RELEASE_DIR/${filename%.wasm}.optimized.wasm"
190+
echo "Optimizing $filename -> $(basename "$out")"
183191
stellar contract optimize --wasm "$w" --wasm-out "$out"
184192
done
185193
shell: bash
@@ -188,5 +196,5 @@ jobs:
188196
uses: actions/upload-artifact@v4
189197
with:
190198
name: optimized-wasm
191-
path: |
192-
contracts/target/**/**/*.optimized.wasm
199+
path: contracts/target/wasm32-unknown-unknown/release/*.optimized.wasm
200+
if-no-files-found: error

README.md

Lines changed: 3 additions & 4 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.
@@ -269,11 +271,8 @@ If you discover a security vulnerability, please see our [Security Policy](SECUR
269271

270272
## Community & Support
271273

272-
Have questions? Want to share ideas or projects? Join the conversation!
274+
Review the discussions guide before opening an issue or looking for community support.
273275

274-
- **[Ask Questions](https://github.com/flowfi/flowfi/discussions/categories/q-a)** - Get help in GitHub Discussions Q&A
275-
- **💡 [Share Ideas](https://github.com/flowfi/flowfi/discussions/categories/ideas)** - Propose features and discuss improvements
276-
- **🎪 [Show and Tell](https://github.com/flowfi/flowfi/discussions/categories/show-and-tell)** - Share projects and use cases built with FlowFi
277276
- **📖 [Discussions Guide](DISCUSSIONS.md)** - Learn when to use Discussions vs Issues.
278277

279278
## Contributors

backend/.env.example

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

36+
# Server-side Stellar secret key used to sign and submit on-chain transactions
37+
# (cancel_stream, top_up_stream). Must be funded on the target network.
38+
KEEPER_SECRET_KEY=
39+
3640
# ─── Auth ─────────────────────────────────────────────────────────────────────
3741
# Secret used to sign JWTs (generate with: openssl rand -hex 32)
3842
JWT_SECRET=
@@ -45,7 +49,9 @@ ADMIN_PUBLIC_KEY=
4549
# Leave empty to run in single-instance mode (no Redis required).
4650
REDIS_URL=
4751

48-
# ─── Admin ───────────────────────────────────────────────────────────────────
49-
# Bearer token required for GET /v1/admin/metrics.
50-
# Leave empty to disable the admin endpoint entirely.
51-
ADMIN_SECRET=
52+
# ─── Caching (optional) ───────────────────────────────────────────────────────
53+
# Time in milliseconds between periodic sweeps to prune expired memory cache
54+
# entries (default: 60000)
55+
MEMORY_CACHE_SWEEP_MS=60000
56+
57+

backend/package.json

Lines changed: 13 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
"type": "module",
66
"main": "index.js",
77
"scripts": {
8+
"prebuild": "prisma generate",
89
"test": "vitest run",
910
"dev": "nodemon",
1011
"build": "tsc",
@@ -22,38 +23,38 @@
2223
"author": "",
2324
"license": "ISC",
2425
"dependencies": {
25-
"@prisma/adapter-pg": "^7.4.1",
26-
"@stellar/stellar-sdk": "^14.5.0",
26+
"@prisma/adapter-pg": "^7.8.0",
27+
"@stellar/stellar-sdk": "^15.1.0",
2728
"cors": "^2.8.6",
2829
"dotenv": "^17.4.2",
2930
"express": "^5.2.1",
30-
"express-rate-limit": "^8.2.1",
31-
"ioredis": "^5.3.2",
32-
"pg": "^8.18.0",
31+
"express-rate-limit": "^8.5.2",
32+
"ioredis": "^5.11.0",
33+
"pg": "^8.21.0",
3334
"stellar-sdk": "^13.3.0",
34-
"swagger-jsdoc": "^6.2.8",
35+
"swagger-jsdoc": "^6.3.0",
3536
"swagger-ui-express": "^5.0.1",
3637
"winston": "^3.11.0",
37-
"zod": "^4.3.6"
38+
"zod": "^4.4.3"
3839
},
3940
"devDependencies": {
40-
"@prisma/client": "^7.4.1",
41+
"@prisma/client": "^7.8.0",
4142
"@types/cors": "^2.8.19",
4243
"@types/eventsource": "^1.1.15",
4344
"@types/express": "^5.0.6",
4445
"@types/node": "^25.2.3",
45-
"@types/pg": "^8.16.0",
46+
"@types/pg": "^8.20.0",
4647
"@types/supertest": "^7.2.0",
4748
"@types/swagger-jsdoc": "^6.0.4",
4849
"@types/swagger-ui-express": "^4.1.6",
4950
"@vitest/coverage-v8": "^2.1.8",
5051
"eventsource": "^2.0.2",
5152
"nodemon": "^3.1.11",
52-
"prisma": "^7.4.1",
53-
"rollup": "^4.60.2",
53+
"prisma": "^7.8.0",
54+
"rollup": "^4.61.0",
5455
"supertest": "^7.2.2",
5556
"ts-node": "^10.9.2",
56-
"tsx": "^4.19.2",
57+
"tsx": "^4.22.4",
5758
"typescript": "^5.9.3",
5859
"vitest": "^2.1.8"
5960
}

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: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -65,8 +65,8 @@ app.use(cors({
6565
}));
6666

6767
// Convert CORS errors into 403 responses so callers get a clear status code
68-
app.use((err: any, req: Request, res: Response, next: NextFunction) => {
69-
if (err && err.message === 'CORS origin not allowed') {
68+
app.use((err: unknown, req: Request, res: Response, next: NextFunction) => {
69+
if (err instanceof Error && err.message === 'CORS origin not allowed') {
7070
res.status(403).json({ error: 'CORS origin not allowed' });
7171
return;
7272
}
@@ -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/config/swagger.ts

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,18 @@ See [Sandbox Mode Documentation](../docs/SANDBOX_MODE.md) for details.`,
7676
scheme: 'bearer',
7777
bearerFormat: 'JWT',
7878
description: 'JSON Web Token issued by /v1/auth/verify after completing the SEP-10 challenge flow.'
79+
},
80+
bearerAuth: {
81+
type: 'http',
82+
scheme: 'bearer',
83+
bearerFormat: 'JWT',
84+
description: 'Alias for BearerAuth — used by route-level security annotations.'
85+
},
86+
adminAuth: {
87+
type: 'http',
88+
scheme: 'bearer',
89+
bearerFormat: 'JWT',
90+
description: 'Admin JWT — the token subject must match ADMIN_PUBLIC_KEY.'
7991
}
8092
},
8193
schemas: {
@@ -165,6 +177,28 @@ See [Sandbox Mode Documentation](../docs/SANDBOX_MODE.md) for details.`,
165177
description: 'Stream active status',
166178
example: true,
167179
},
180+
isPaused: {
181+
type: 'boolean',
182+
description: 'Whether the stream is currently paused',
183+
example: false,
184+
},
185+
pausedAt: {
186+
type: 'integer',
187+
nullable: true,
188+
description: 'Ledger timestamp when the stream was last paused (Unix), null if not paused',
189+
example: null,
190+
},
191+
totalPausedDuration: {
192+
type: 'integer',
193+
description: 'Cumulative seconds the stream has spent paused',
194+
example: 0,
195+
},
196+
endTime: {
197+
type: 'integer',
198+
nullable: true,
199+
description: 'Ledger timestamp when the stream ended (Unix), null if still active',
200+
example: null,
201+
},
168202
createdAt: {
169203
type: 'string',
170204
format: 'date-time',
@@ -189,7 +223,7 @@ See [Sandbox Mode Documentation](../docs/SANDBOX_MODE.md) for details.`,
189223
},
190224
eventType: {
191225
type: 'string',
192-
enum: ['CREATED', 'TOPPED_UP', 'WITHDRAWN', 'CANCELLED', 'COMPLETED'],
226+
enum: ['CREATED', 'TOPPED_UP', 'WITHDRAWN', 'CANCELLED', 'COMPLETED', 'PAUSED', 'RESUMED', 'FEE_COLLECTED'],
193227
description: 'Type of stream event',
194228
example: 'TOPPED_UP',
195229
},

backend/src/controllers/sse.controller.ts

Lines changed: 21 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { z } from 'zod';
77

88
const subscribeSchema = z.object({
99
streams: z.array(z.string()).optional().default([]),
10+
users: z.array(z.string()).optional().default([]),
1011
all: z.boolean().optional().default(false),
1112
});
1213

@@ -41,30 +42,38 @@ export const subscribe = async (req: Request, res: Response) => {
4142
}
4243

4344
const { publicKey } = (req as AuthenticatedRequest).user;
44-
const { streams, all } = subscribeSchema.parse(req.query);
45+
const { streams, users, all } = subscribeSchema.parse(req.query);
4546

4647
// Scope: only streams where the authenticated user is sender or recipient
4748
const ownedStreams = await prisma.stream.findMany({
4849
where: { OR: [{ sender: publicKey }, { recipient: publicKey }] },
49-
select: { streamId: true },
50+
select: { streamId: true, sender: true, recipient: true },
5051
});
51-
const ownedIds = new Set(ownedStreams.map((s: any) => String(s.streamId)));
52+
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+
}
5258

5359
let subscriptions: string[];
5460
if (all) {
5561
// "all" still scoped to the user's own streams
56-
subscriptions = [...ownedIds] as string[];
62+
subscriptions = [...ownedIds];
5763
} else if (streams.length > 0) {
5864
// Only allow subscribing to streams the user owns
5965
subscriptions = streams.filter((id) => ownedIds.has(id));
6066
} else {
61-
subscriptions = [...ownedIds] as string[];
67+
subscriptions = [...ownedIds];
6268
}
6369

64-
// Always add user-scoped subscription key
65-
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);
6675

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

6978
res.writeHead(200, {
7079
'Content-Type': 'text/event-stream',
@@ -77,11 +86,12 @@ export const subscribe = async (req: Request, res: Response) => {
7786
res.write(`data: ${JSON.stringify({ type: 'connected', clientId, requestId })}\n\n`);
7887

7988
sseService.addClient(clientId, res, subscriptions, sourceIp);
80-
} catch (error: any) {
81-
if (error.name === 'ZodError') {
89+
return;
90+
} catch (error: unknown) {
91+
if (error instanceof z.ZodError) {
8292
return res.status(400).json({
8393
message: 'Invalid subscription parameters',
84-
errors: error.errors,
94+
errors: error.issues,
8595
});
8696
}
8797
return res.status(500).json({ message: 'Internal server error' });

0 commit comments

Comments
 (0)