Skip to content

Commit 61684f2

Browse files
authored
Merge branch 'main' into feature/1255-rollbackup-fix
2 parents 4f15dbd + 6db14d7 commit 61684f2

116 files changed

Lines changed: 15372 additions & 865 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: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,17 @@ jobs:
8989
working-directory: backend
9090

9191

92+
- name: OpenAPI spec & API types drift check
93+
run: |
94+
cd backend
95+
npm run codegen:openapi
96+
cd ../frontend
97+
npm run codegen:api-types
98+
cd ..
99+
git diff --exit-code -- backend/swagger/flowfi.openapi.json frontend/src/lib/api-types.generated.ts
100+
101+
- name: Install Rollup Native Binding
102+
run: npm install @rollup/rollup-linux-x64-gnu --no-save
92103

93104
- name: Run Backend Tests
94105
run: |
@@ -189,8 +200,16 @@ jobs:
189200
run: cargo test
190201
working-directory: contracts
191202

203+
- name: Cache cargo-tarpaulin
204+
id: tarpaulin-cache
205+
uses: actions/cache@v4
206+
with:
207+
path: ~/.cargo/bin/cargo-tarpaulin
208+
key: cargo-tarpaulin-${{ runner.os }}-0.37.2
209+
192210
- name: Install cargo-tarpaulin
193-
run: cargo install cargo-tarpaulin --locked
211+
if: steps.tarpaulin-cache.outputs.cache-hit != 'true'
212+
run: cargo install cargo-tarpaulin@0.37.2 --locked
194213

195214
- name: Run Contract Coverage
196215
run: cargo tarpaulin --workspace --out Xml --output-dir coverage --fail-under 70
Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
# Contract Deployment Workflow for FlowFi
2+
#
3+
# Compiles the Soroban stream contract to optimized WASM, runs contract tests,
4+
# and deploys + initializes the contract on Stellar Testnet on demand (or on
5+
# Mainnet for release tags). The resulting contract ID is surfaced in the job
6+
# summary and published as a release artifact.
7+
name: Deploy Soroban Contracts
8+
9+
on:
10+
release:
11+
types: [published]
12+
workflow_dispatch:
13+
inputs:
14+
network:
15+
description: "Target network (testnet|mainnet)"
16+
required: true
17+
default: "testnet"
18+
type: choice
19+
options:
20+
- testnet
21+
- mainnet
22+
23+
concurrency:
24+
group: ${{ github.workflow }}-${{ inputs.network || github.ref }}
25+
cancel-in-progress: true
26+
27+
permissions:
28+
contents: write
29+
30+
jobs:
31+
deploy:
32+
name: Build & Deploy stream_contract
33+
runs-on: ubuntu-latest
34+
environment: ${{ github.event_name == 'release' && 'production' || 'staging' }}
35+
36+
env:
37+
NETWORK: ${{ inputs.network || (github.event_name == 'release' && 'mainnet' || 'testnet') }}
38+
DEPLOYER_SECRET: ${{ secrets.DEPLOYER_SECRET }}
39+
ADMIN_ADDRESS: ${{ secrets.ADMIN_ADDRESS }}
40+
TREASURY_ADDRESS: ${{ secrets.TREASURY_ADDRESS }}
41+
FEE_RATE_BPS: ${{ secrets.FEE_RATE_BPS }}
42+
43+
steps:
44+
- name: Checkout code
45+
uses: actions/checkout@v4
46+
47+
- name: Setup Rust toolchain
48+
uses: dtolnay/rust-toolchain@stable
49+
with:
50+
toolchain: stable
51+
targets: wasm32-unknown-unknown
52+
components: rustfmt, clippy
53+
54+
- name: Rust Cache
55+
uses: Swatinem/rust-cache@v2
56+
with:
57+
workspace: "contracts -> target"
58+
59+
- name: Install Stellar CLI
60+
run: |
61+
curl -fsSL https://github.com/stellar/stellar-cli/raw/main/install.sh | sh -s -- --install-deps
62+
echo "$HOME/.stellar-cli/bin" >> $GITHUB_PATH
63+
64+
- name: Run Contract Tests
65+
run: cargo test --package stream_contract
66+
working-directory: contracts
67+
68+
- name: Build & Optimize WASM
69+
run: |
70+
set -euo pipefail
71+
cd contracts
72+
cargo build --target wasm32-unknown-unknown --release
73+
RELEASE_DIR="target/wasm32-unknown-unknown/release"
74+
for w in "$RELEASE_DIR"/stream_contract.wasm; do
75+
stellar contract optimize --wasm "$w" --wasm-out "$RELEASE_DIR/stream_contract.optimized.wasm"
76+
done
77+
ls -la "$RELEASE_DIR"/*.wasm
78+
79+
- name: Inspect Contract Interface & WASM Size
80+
run: |
81+
set -euo pipefail
82+
WASM=contracts/target/wasm32-unknown-unknown/release/stream_contract.optimized.wasm
83+
stellar contract inspect --wasm "$WASM"
84+
SIZE=$(stat -c%s "$WASM")
85+
echo "Optimized WASM size: $SIZE bytes"
86+
if [ "$SIZE" -ge 65536 ]; then
87+
echo "ERROR: optimized WASM exceeds 64KB budget ($SIZE bytes)"
88+
exit 1
89+
fi
90+
echo "WASM_WASM_PATH=$WASM" >> $GITHUB_ENV
91+
92+
- name: Deploy & Initialize Contract
93+
run: ./scripts/deploy.sh --network "$NETWORK"
94+
95+
- name: Read Deployed Contract ID
96+
id: contract
97+
run: |
98+
set -euo pipefail
99+
CONTRACT_ID=$(jq -r --arg net "$NETWORK" '.[$net].contractId' deployment-info.json)
100+
echo "contract_id=$CONTRACT_ID" >> $GITHUB_OUTPUT
101+
echo "deployment-json=$(jq -c . deployment-info.json)" >> $GITHUB_OUTPUT
102+
103+
- name: Emit Deployment Summary
104+
if: always()
105+
run: |
106+
{
107+
echo "## Deployment Summary"
108+
echo ""
109+
echo "- **Network**: \`$NETWORK\`"
110+
echo "- **Contract ID**: \`${{ steps.contract.outputs.contract_id }}\`"
111+
echo "- **WASM**: \`${{ env.WASM_WASM_PATH }}\`"
112+
echo "- **Deployment info**: "
113+
echo '```json'
114+
echo "${{ steps.contract.outputs.deployment-json }}"
115+
echo '```'
116+
} >> "$GITHUB_STEP_SUMMARY"
117+
118+
- name: Upload Optimized WASM Artifact
119+
uses: actions/upload-artifact@v4
120+
with:
121+
name: stream-contract-${{ env.NETWORK }}
122+
path: contracts/target/wasm32-unknown-unknown/optimized/*.wasm
123+
if-no-files-found: error
124+
125+
- name: Upload Deployment Info
126+
uses: actions/upload-artifact@v4
127+
with:
128+
name: deployment-info-${{ env.NETWORK }}
129+
path: deployment-info.json
130+
if-no-files-found: error
131+
132+
- name: Commit Deployment Info
133+
if: github.event_name == 'release'
134+
env:
135+
NETWORK: ${{ env.NETWORK }}
136+
run: |
137+
set -euo pipefail
138+
git config user.name "github-actions[bot]"
139+
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
140+
git add deployment-info.json
141+
if git diff --cached --quiet; then
142+
echo "No deployment-info.json changes to commit"
143+
exit 0
144+
fi
145+
git commit -m "chore(contracts): record $NETWORK contract deployment"
146+
git push

.github/workflows/security.yml

Lines changed: 41 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ on:
88
push:
99
branches: [ main, develop ]
1010
pull_request:
11-
branches: [ main ]
11+
branches: [ main, develop ]
1212
schedule:
1313
- cron: '0 2 * * 0'
1414

@@ -41,6 +41,24 @@ jobs:
4141
- name: Check for known vulnerabilities in backend (production dependencies)
4242
run: npm audit --workspace=backend --omit=dev --audit-level=critical
4343

44+
- name: Setup Rust toolchain for contract audit
45+
uses: dtolnay/rust-toolchain@stable
46+
47+
- name: Cache cargo-audit
48+
id: cargo-audit-cache
49+
uses: actions/cache@v4
50+
with:
51+
path: ~/.cargo/bin/cargo-audit
52+
key: cargo-audit-${{ runner.os }}
53+
54+
- name: Install cargo-audit
55+
if: steps.cargo-audit-cache.outputs.cache-hit != 'true'
56+
run: cargo install cargo-audit --locked
57+
58+
- name: Check for known vulnerabilities in smart contracts (cargo audit)
59+
run: cargo audit
60+
working-directory: contracts
61+
4462
- name: Verify security setup
4563
run: npm run verify-security
4664

@@ -55,19 +73,40 @@ jobs:
5573
strategy:
5674
fail-fast: false
5775
matrix:
58-
language: [ 'javascript', 'typescript' ]
76+
language: [ 'javascript', 'typescript', 'rust' ]
5977

6078
steps:
6179
- name: Checkout repository
6280
uses: actions/checkout@v4
6381

82+
- name: Setup Rust toolchain
83+
if: matrix.language == 'rust'
84+
uses: dtolnay/rust-toolchain@stable
85+
with:
86+
toolchain: stable
87+
targets: wasm32-unknown-unknown
88+
components: clippy
89+
90+
- name: Rust Cache
91+
if: matrix.language == 'rust'
92+
uses: Swatinem/rust-cache@v2
93+
with:
94+
workspace: "contracts -> target"
95+
6496
- name: Initialize CodeQL
6597
uses: github/codeql-action/init@v3
6698
with:
6799
languages: ${{ matrix.language }}
68100

101+
- name: Build Rust contracts for CodeQL
102+
if: matrix.language == 'rust'
103+
run: cargo check --workspace --all-targets
104+
working-directory: contracts
105+
69106
- name: Autobuild
107+
if: matrix.language != 'rust'
70108
uses: github/codeql-action/autobuild@v3
71109

72110
- name: Perform CodeQL Analysis
73111
uses: github/codeql-action/analyze@v3
112+

backend/.env.example

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,11 @@ REDIS_URL=
8888
# Time in milliseconds between periodic sweeps to prune expired memory cache entries (default: 60000)
8989
MEMORY_CACHE_SWEEP_MS=60000
9090

91+
# Maximum number of entries kept in the in-memory cache. When exceeded, the
92+
# least-recently-used entries are evicted immediately so memory stays bounded
93+
# even between sweeps (default: 10000)
94+
MEMORY_CACHE_MAX_ITEMS=10000
95+
9196
# Cache time-to-live (TTL) for claimable amount calculations in milliseconds (default: 5000)
9297
CLAIMABLE_CACHE_TTL_MS=5000
9398

backend/docs/SSE_ARCHITECTURE.md

Lines changed: 27 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -3,27 +3,27 @@
33
## System Flow
44

55
```
6-
┌─────────────────┐
7-
│ Blockchain │
8-
│ Indexer │
9-
│ (Stellar) │
10-
└────────┬────────┘
11-
│ Events
12-
6+
┌─────────────────────────────────────────────────────────┐
7+
│ Stellar Blockchain / Soroban │
8+
│ - On-chain stream contract executions & ledger events │
9+
└────────────────────────────┬────────────────────────────┘
10+
│ On-Chain Events (via Soroban RPC poll)
11+
1312
┌─────────────────────────────────────────────────────────┐
1413
│ Backend Server │
1514
│ │
1615
│ ┌──────────────────────────────────────────────────┐ │
17-
│ │ Stream Controller │ │
18-
│ │ - Creates/updates streams │ │
19-
│ │ - Calls sseService.broadcast() │ │
16+
│ │ Soroban Event Worker (Indexer) │ │
17+
│ │ - Polls Soroban RPC for confirmed events │ │
18+
│ │ - Persists stream state & events to Database │ │
19+
│ │ - Calls sseService.broadcastToStream/Admin() │ │
2020
│ └──────────────┬───────────────────────────────────┘ │
21-
│ │
21+
│ │ Dispatch events (indexer-driven)
2222
│ ▼ │
2323
│ ┌──────────────────────────────────────────────────┐ │
2424
│ │ SSE Service │ │
2525
│ │ - Manages client connections │ │
26-
│ │ - Filters by subscription │ │
26+
│ │ - Filters by subscription (stream/user/admin) │ │
2727
│ │ - Broadcasts to matching clients │ │
2828
│ └──────────────┬───────────────────────────────────┘ │
2929
│ │ │
@@ -40,6 +40,8 @@
4040
└─────────────────────────────────────┘
4141
```
4242

43+
> **Note on Indexer-Driven Event Origin**: SSE broadcast events originate asynchronously from the background indexer worker (`SorobanEventWorker`) only after transaction confirmation on the Stellar ledger, not synchronously from HTTP API controllers (`stream.controller.ts`, etc.). When a user submits an action (create, pause, withdraw, top-up, cancel), API controllers do not broadcast SSE events directly; events are dispatched once the Soroban event is polled and confirmed on-chain. Additionally, background workers like `StreamRunwayWorker` may dispatch computed alerts (e.g. `STREAM_LOW_BALANCE`).
44+
4345
## Connection Flow
4446

4547
```
@@ -72,7 +74,7 @@ Client Server
7274
│ [Auto Reconnect - 1s] │
7375
│ │
7476
│ GET /events/subscribe │
75-
├──────────────────────────────>│
77+
├──────────────────────────────>│
7678
│ │
7779
│ 200 OK │
7880
│<──────────────────────────────┤
@@ -122,30 +124,37 @@ Client Server
122124
└─────────────────┘
123125
124126
Flow:
125-
1. Backend 1 receives stream creation
127+
1. Backend 1 (SorobanEventWorker) indexes confirmed on-chain event
126128
2. Backend 1 publishes to Redis: "stream-events"
127-
3. All backends (1, 2, 3) receive message
129+
3. All backends (1, 2, 3) receive message via Redis subscriber
128130
4. Each backend broadcasts to its connected clients
129131
5. Total: 33 clients receive the event
130132
```
131133

132134
## Event Broadcasting Logic
133135

134136
```typescript
135-
// Broadcast to specific stream
137+
// Broadcast to specific stream (called by SorobanEventWorker / StreamRunwayWorker)
136138
sseService.broadcastToStream("123", "stream.created", data)
137139
138140
Filter clients: subscription includes "123" or "*"
139141
140142
Send to matching clients
141143

142-
// Broadcast to user
143-
sseService.broadcastToUser("GABC...", "stream.created", data)
144+
// Broadcast to user (called by StreamRunwayWorker)
145+
sseService.broadcastToUser("GABC...", "STREAM_LOW_BALANCE", data)
144146
145147
Filter clients: subscription includes "user:GABC..." or "*"
146148
147149
Send to matching clients
148150

151+
// Broadcast to admin (called by SorobanEventWorker for admin/fee events)
152+
sseService.broadcastToAdmin("stream.fee_config_updated", data)
153+
154+
Filter clients: admin subscribers ("admin" or "*")
155+
156+
Send to matching clients
157+
149158
// Broadcast to all
150159
sseService.broadcast("stream.created", data)
151160

backend/docs/SSE_IMPLEMENTATION.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -193,7 +193,7 @@ import Redis from 'ioredis';
193193
const redis = new Redis(process.env.REDIS_URL);
194194
const subscriber = new Redis(process.env.REDIS_URL);
195195

196-
// Publisher (in stream controller)
196+
// Publisher (in Soroban event worker / indexer)
197197
redis.publish('stream-events', JSON.stringify({
198198
event: 'stream.created',
199199
data: mockStream,

backend/package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
"main": "index.js",
77
"scripts": {
88
"prebuild": "prisma generate",
9+
"pretest": "prisma generate",
910
"test": "vitest run",
1011
"test:unit": "vitest run --exclude='tests/integration/**'",
1112
"test:integration": "vitest run tests/integration",
@@ -17,7 +18,8 @@
1718
"prisma:migrate": "prisma migrate dev",
1819
"prisma:deploy": "prisma migrate deploy",
1920
"prisma:seed": "prisma db seed",
20-
"prisma:studio": "prisma studio"
21+
"prisma:studio": "prisma studio",
22+
"codegen:openapi": "tsx scripts/export-openapi.mts"
2123
},
2224
"prisma": {
2325
"seed": "tsx prisma/seed.ts"

0 commit comments

Comments
 (0)