Skip to content

Commit af6bfcb

Browse files
authored
Merge pull request #388 from anonfedora/feature/status-filter_shared-token_live-notification_contract-deployment
feature:Status filter, shared token, live notification, contract depl…
2 parents 7b24e40 + 501382b commit af6bfcb

20 files changed

Lines changed: 810 additions & 120 deletions

File tree

README.md

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,96 @@ cd contracts
101101
cargo build --target wasm32-unknown-unknown --release
102102
```
103103

104+
## Deployment
105+
106+
### Contract Deployment
107+
108+
The FlowFi smart contracts can be deployed to both testnet and mainnet using the automated deployment script.
109+
110+
#### Prerequisites
111+
112+
- Stellar CLI installed and configured
113+
- Sufficient XLM in the deployment account for network fees
114+
- Required environment variables set
115+
116+
#### Environment Variables
117+
118+
Before deploying, set the following environment variables:
119+
120+
```bash
121+
export STELLAR_SECRET_KEY="your_secret_key_here"
122+
export ADMIN_ADDRESS="your_admin_address_here"
123+
export TREASURY_ADDRESS="your_treasury_address_here"
124+
export FEE_RATE_BPS="25" # 0.25% fee rate
125+
```
126+
127+
#### Deploy to Testnet
128+
129+
```bash
130+
npx tsx scripts/deploy.ts --network testnet
131+
```
132+
133+
#### Deploy to Mainnet
134+
135+
```bash
136+
npx tsx scripts/deploy.ts --network mainnet
137+
```
138+
139+
#### Deployment Process
140+
141+
The deployment script automates the following steps:
142+
143+
1. **Build WASM**: Compiles the Rust contract to WebAssembly
144+
2. **Optimize WASM**: Optimizes the WASM for deployment size
145+
3. **Deploy Contract**: Deploys the contract to the specified network
146+
4. **Initialize Contract**: Sets up admin, treasury, and fee rate parameters
147+
5. **Save Deployment Info**: Stores contract details in `deployment-info.json`
148+
149+
#### Deployment Information
150+
151+
After successful deployment, contract details are saved to `deployment-info.json`:
152+
153+
```json
154+
{
155+
"testnet": {
156+
"network": "testnet",
157+
"contractId": "CD...ID",
158+
"deployedAt": "2024-01-01T00:00:00.000Z",
159+
"adminAddress": "G...ADMIN",
160+
"treasuryAddress": "G...TREASURY",
161+
"feeRateBps": 25,
162+
"transactionHash": "TX...HASH"
163+
},
164+
"mainnet": {
165+
"network": "mainnet",
166+
"contractId": "CD...ID",
167+
"deployedAt": "2024-01-01T00:00:00.000Z",
168+
"adminAddress": "G...ADMIN",
169+
"treasuryAddress": "G...TREASURY",
170+
"feeRateBps": 25,
171+
"transactionHash": "TX...HASH"
172+
},
173+
"lastUpdated": "2024-01-01T00:00:00.000Z"
174+
}
175+
```
176+
177+
#### Manual Deployment
178+
179+
If you prefer to deploy manually, you can use the Stellar CLI directly:
180+
181+
```bash
182+
# Build and optimize
183+
cd contracts
184+
cargo build --target wasm32-unknown-unknown --release
185+
stellar contract optimize --wasm target/wasm32-unknown-unknown/release/stream_contract.wasm
186+
187+
# Deploy
188+
stellar contract deploy --wasm target/wasm32-unknown-unknown/release/stream_contract.optimized.wasm --source YOUR_SECRET_KEY --network https://soroban-testnet.stellar.org
189+
190+
# Initialize
191+
stellar contract invoke --id CONTRACT_ID --source YOUR_SECRET_KEY --network https://soroban-testnet.stellar.org initialize --admin ADMIN_ADDRESS --treasury TREASURY_ADDRESS --fee_rate_bps 25
192+
```
193+
104194
## API Documentation
105195

106196
The FlowFi backend API uses URL-based versioning. All endpoints are prefixed with a version (e.g., `/v1/streams`).

backend/src/controllers/stream.controller.ts

Lines changed: 84 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -79,26 +79,95 @@ export const createStream = async (req: Request, res: Response) => {
7979
};
8080

8181
/**
82-
* List streams by sender or recipient
82+
* List streams by sender, recipient, status, token with sorting and pagination
8383
*/
8484
export const listStreams = async (req: Request, res: Response) => {
8585
try {
86-
const { sender, recipient } = req.query;
86+
const {
87+
sender,
88+
recipient,
89+
status,
90+
token,
91+
sort = 'createdAt',
92+
order = 'desc',
93+
limit = '20',
94+
offset = '0'
95+
} = req.query;
8796

8897
const where: any = {};
8998
if (typeof sender === 'string') where.sender = sender;
9099
if (typeof recipient === 'string') where.recipient = recipient;
100+
if (typeof token === 'string') where.tokenAddress = token;
91101

92-
const streams = await prisma.stream.findMany({
93-
where,
94-
orderBy: { createdAt: 'desc' },
95-
include: {
96-
senderUser: true,
97-
recipientUser: true
102+
// Handle status filtering
103+
if (typeof status === 'string') {
104+
const validStatuses = ['active', 'cancelled', 'completed', 'paused'];
105+
if (!validStatuses.includes(status)) {
106+
return res.status(400).json({
107+
error: 'Invalid status parameter',
108+
message: `status must be one of: ${validStatuses.join(', ')}`
109+
});
98110
}
99-
});
100111

101-
return res.status(200).json(streams);
112+
// Map status to database conditions
113+
switch (status) {
114+
case 'active':
115+
where.isActive = true;
116+
break;
117+
case 'cancelled':
118+
where.isActive = false;
119+
// Additional check for cancelled events could be added here
120+
break;
121+
case 'completed':
122+
where.isActive = false;
123+
// Additional check for completed events could be added here
124+
break;
125+
case 'paused':
126+
where.isActive = false;
127+
// Additional check for paused events could be added here
128+
break;
129+
}
130+
}
131+
132+
// Validate and parse pagination parameters
133+
const parsedLimit = Math.min(
134+
typeof limit === 'string' ? (Number.parseInt(limit, 10) || 20) : 20,
135+
100
136+
);
137+
const parsedOffset = typeof offset === 'string' ? (Number.parseInt(offset, 10) || 0) : 0;
138+
139+
// Validate sort field
140+
const validSortFields = ['createdAt', 'startTime', 'lastUpdateTime', 'depositedAmount'];
141+
const sortField = validSortFields.includes(typeof sort === 'string' ? sort : 'createdAt')
142+
? (sort as 'createdAt' | 'startTime' | 'lastUpdateTime' | 'depositedAmount')
143+
: 'createdAt';
144+
145+
// Validate order
146+
const sortOrder = order === 'asc' ? 'asc' : 'desc';
147+
148+
const [streams, total] = await Promise.all([
149+
prisma.stream.findMany({
150+
where,
151+
orderBy: { [sortField]: sortOrder },
152+
take: parsedLimit,
153+
skip: parsedOffset,
154+
include: {
155+
senderUser: true,
156+
recipientUser: true
157+
}
158+
}),
159+
prisma.stream.count({ where })
160+
]);
161+
162+
const hasMore = parsedOffset + streams.length < total;
163+
164+
return res.status(200).json({
165+
data: streams,
166+
total,
167+
hasMore,
168+
limit: parsedLimit,
169+
offset: parsedOffset
170+
});
102171
} catch (error) {
103172
logger.error('Error listing streams:', error);
104173
return res.status(500).json({ error: 'Internal server error' });
@@ -302,7 +371,7 @@ export const getStreamClaimableAmount = async (req: Request, res: Response) => {
302371
*/
303372
export const getUserStreamSummary = async (req: Request, res: Response) => {
304373
try {
305-
const address = (req.params.address ?? '').trim();
374+
const address = Array.isArray(req.params.address) ? req.params.address[0] : (req.params.address ?? '').trim();
306375
if (!address) {
307376
return res.status(400).json({ error: 'Address is required' });
308377
}
@@ -339,10 +408,10 @@ export const getUserStreamSummary = async (req: Request, res: Response) => {
339408
]);
340409

341410
const totalStreamsCreated = outgoingStreams.length;
342-
const totalStreamedOut = sumStringI128(outgoingStreams.map((stream) => stream.withdrawnAmount));
343-
const totalStreamedIn = sumStringI128(incomingStreams.map((stream) => stream.withdrawnAmount));
344-
const activeOutgoingCount = outgoingStreams.filter((stream) => stream.isActive).length;
345-
const activeIncomingCount = incomingStreams.filter((stream) => stream.isActive).length;
411+
const totalStreamedOut = sumStringI128(outgoingStreams.map((stream: any) => stream.withdrawnAmount));
412+
const totalStreamedIn = sumStringI128(incomingStreams.map((stream: any) => stream.withdrawnAmount));
413+
const activeOutgoingCount = outgoingStreams.filter((stream: any) => stream.isActive).length;
414+
const activeIncomingCount = incomingStreams.filter((stream: any) => stream.isActive).length;
346415

347416
const calculatedAt = Math.floor(nowMs / 1000);
348417
let claimableTotal = 0n;

backend/src/lib/redis.ts

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,26 @@
1-
import type { Redis } from 'ioredis';
2-
import RedisClass from 'ioredis';
1+
const Redis = require('ioredis');
32
import logger from '../logger.js';
43

54
const REDIS_URL = process.env.REDIS_URL;
65

7-
let _publisher: Redis | null = null;
8-
let _subscriber: Redis | null = null;
6+
let _publisher: typeof Redis | null = null;
7+
let _subscriber: typeof Redis | null = null;
98
let _available = false;
109

11-
export function getPublisher(): Redis | null {
10+
export function getPublisher(): typeof Redis | null {
1211
return _publisher;
1312
}
1413

15-
export function getSubscriber(): Redis | null {
14+
export function getSubscriber(): typeof Redis | null {
1615
return _subscriber;
1716
}
1817

1918
export function isRedisAvailable(): boolean {
2019
return _available;
2120
}
2221

23-
function makeClient(url: string): Redis {
24-
return new RedisClass(url, {
22+
function makeClient(url: string): typeof Redis {
23+
return new Redis(url, {
2524
maxRetriesPerRequest: 3,
2625
retryStrategy: (times: number) =>
2726
times > 3 ? null : Math.min(times * 200, 2000),

backend/src/services/sse.service.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -183,4 +183,5 @@ class SSEService {
183183
}
184184
}
185185

186+
export { SSEService };
186187
export const sseService = new SSEService();

backend/src/workers/soroban-event-worker.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -133,7 +133,6 @@ export class SorobanEventWorker {
133133
logger.error('[SorobanWorker] Manual poll error:', err);
134134
}
135135
}
136-
}
137136

138137
// ─── Internal ──────────────────────────────────────────────────────────────
139138

backend/tests/sse.service.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
22
import { EventEmitter } from 'node:events';
3-
import { SSEService } from '../src/services/sse.service.js';
3+
import { SSEService, sseService } from '../src/services/sse.service.js';
44

55
function createMockResponse() {
66
const emitter = new EventEmitter();

backend/tests/stream.test.ts

Lines changed: 65 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -122,7 +122,7 @@ describe('GET /v1/users/:address/summary', () => {
122122
});
123123

124124
it('returns all-zero summary for addresses with no streams', async () => {
125-
prisma.stream.findMany
125+
vi.mocked(prisma.stream.findMany)
126126
.mockResolvedValueOnce([])
127127
.mockResolvedValueOnce([]);
128128

@@ -142,29 +142,69 @@ describe('GET /v1/users/:address/summary', () => {
142142
});
143143

144144
it('returns accurate outgoing/incoming aggregates and claimable sum', async () => {
145-
prisma.stream.findMany
145+
vi.mocked(prisma.stream.findMany)
146146
.mockResolvedValueOnce([
147-
{ withdrawnAmount: '30', isActive: true },
148-
{ withdrawnAmount: '20', isActive: false },
147+
{
148+
id: '1',
149+
createdAt: new Date(),
150+
updatedAt: new Date(),
151+
streamId: 1,
152+
sender: 'GSENDER',
153+
recipient: 'GRECIPIENT',
154+
tokenAddress: 'TOKEN',
155+
ratePerSecond: '10',
156+
depositedAmount: '100',
157+
withdrawnAmount: '30',
158+
startTime: 1000,
159+
lastUpdateTime: 2000,
160+
isActive: true
161+
},
162+
{
163+
id: '2',
164+
createdAt: new Date(),
165+
updatedAt: new Date(),
166+
streamId: 2,
167+
sender: 'GSENDER2',
168+
recipient: 'GRECIPIENT2',
169+
tokenAddress: 'TOKEN2',
170+
ratePerSecond: '20',
171+
depositedAmount: '200',
172+
withdrawnAmount: '20',
173+
startTime: 1000,
174+
lastUpdateTime: 2000,
175+
isActive: false
176+
},
149177
])
150178
.mockResolvedValueOnce([
151179
{
180+
id: '3',
181+
createdAt: new Date(),
182+
updatedAt: new Date(),
152183
streamId: 11,
184+
sender: 'GSENDER3',
185+
recipient: 'GRECIPIENT3',
186+
tokenAddress: 'TOKEN3',
153187
ratePerSecond: '10',
154188
depositedAmount: '1000',
155189
withdrawnAmount: '100',
190+
startTime: 1000,
156191
lastUpdateTime: 0,
157192
isActive: true,
158-
updatedAt: new Date(),
159193
},
160194
{
195+
id: '4',
196+
createdAt: new Date(),
197+
updatedAt: new Date(),
161198
streamId: 12,
162-
ratePerSecond: '1',
163-
depositedAmount: '200',
164-
withdrawnAmount: '50',
199+
sender: 'GSENDER4',
200+
recipient: 'GRECIPIENT4',
201+
tokenAddress: 'TOKEN4',
202+
ratePerSecond: '5',
203+
depositedAmount: '500',
204+
withdrawnAmount: '0',
205+
startTime: 1000,
165206
lastUpdateTime: 0,
166207
isActive: false,
167-
updatedAt: new Date(),
168208
},
169209
]);
170210

@@ -184,8 +224,22 @@ describe('GET /v1/users/:address/summary', () => {
184224
});
185225

186226
it('caches summary results for repeated requests within TTL', async () => {
187-
prisma.stream.findMany
188-
.mockResolvedValueOnce([{ withdrawnAmount: '1', isActive: true }])
227+
vi.mocked(prisma.stream.findMany)
228+
.mockResolvedValueOnce([{
229+
id: '5',
230+
createdAt: new Date(),
231+
updatedAt: new Date(),
232+
streamId: 13,
233+
sender: 'GSENDER5',
234+
recipient: 'GRECIPIENT5',
235+
tokenAddress: 'TOKEN5',
236+
ratePerSecond: '1',
237+
depositedAmount: '100',
238+
withdrawnAmount: '1',
239+
startTime: 1000,
240+
lastUpdateTime: 2000,
241+
isActive: true
242+
}])
189243
.mockResolvedValueOnce([]);
190244

191245
const address = 'GCACHE000000000000000000000000000000000000000000000000000000';

0 commit comments

Comments
 (0)