From 9a098b0f6a9961ab697a4f7923d3288fffac3c12 Mon Sep 17 00:00:00 2001 From: pope-h Date: Fri, 29 May 2026 02:56:05 +0100 Subject: [PATCH 1/4] fix(frontend): add AbortController cleanup to stream-detail fetches Pass abort signal to fetchStream and fetchEvents functions. Abort on cleanup or when streamId changes to prevent "setState on unmounted component" races and cancel stale in-flight requests. Ignore AbortError in catch blocks. Closes #510 --- frontend/src/app/streams/[id]/page.tsx | 23 ++++++++++++++++------- 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/frontend/src/app/streams/[id]/page.tsx b/frontend/src/app/streams/[id]/page.tsx index 37086c3a..9e6e0ef5 100644 --- a/frontend/src/app/streams/[id]/page.tsx +++ b/frontend/src/app/streams/[id]/page.tsx @@ -96,24 +96,26 @@ export default function StreamDetailsPage() { }); // Fetch stream data - const fetchStream = useCallback(async () => { + const fetchStream = useCallback(async (signal?: AbortSignal) => { if (!streamId) return; try { - const response = await fetch(`${API_BASE_URL}/streams/${streamId}`); + const response = await fetch(`${API_BASE_URL}/streams/${streamId}`, { signal }); if (!response.ok) throw new Error("Stream not found"); const data = await response.json(); setStream(data); } catch (err) { + if (err instanceof Error && err.name === "AbortError") return; setError(err instanceof Error ? err.message : "Failed to fetch stream"); } }, [streamId]); // Fetch events - const fetchEvents = useCallback(async (page: number) => { + const fetchEvents = useCallback(async (page: number, signal?: AbortSignal) => { if (!streamId) return; try { const response = await fetch( - `${API_BASE_URL}/streams/${streamId}/events?page=${page}&limit=${EVENTS_PER_PAGE}` + `${API_BASE_URL}/streams/${streamId}/events?page=${page}&limit=${EVENTS_PER_PAGE}`, + { signal } ); if (response.ok) { const data = await response.json(); @@ -121,6 +123,7 @@ export default function StreamDetailsPage() { setEventsTotal(data.total || 0); } } catch (err) { + if (err instanceof Error && err.name === "AbortError") return; console.error("Failed to fetch events:", err); } }, [streamId]); @@ -129,20 +132,26 @@ export default function StreamDetailsPage() { useEffect(() => { if (!isHydrated) return; + const controller = new AbortController(); + const loadData = async () => { setLoading(true); - await Promise.all([fetchStream(), fetchEvents(1)]); + await Promise.all([fetchStream(controller.signal), fetchEvents(1, controller.signal)]); setLoading(false); }; loadData(); + + return () => controller.abort(); }, [isHydrated, fetchStream, fetchEvents]); // Handle SSE events useEffect(() => { if (streamEvents.length > 0) { - fetchStream(); - fetchEvents(eventsPage); + const controller = new AbortController(); + fetchStream(controller.signal); + fetchEvents(eventsPage, controller.signal); + return () => controller.abort(); } }, [streamEvents, fetchStream, fetchEvents, eventsPage]); From 92b8cae47c01d84fbbc1a6f0c22892161517102d Mon Sep 17 00:00:00 2001 From: pope-h Date: Fri, 29 May 2026 02:56:10 +0100 Subject: [PATCH 2/4] fix(frontend): revoke object URL in downloadCSV Call URL.revokeObjectURL after download is triggered to free memory. Prevents object URL leaks on repeated CSV exports. Closes #511 --- frontend/src/utils/csvExport.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/frontend/src/utils/csvExport.ts b/frontend/src/utils/csvExport.ts index 59dc78d6..ae2e02c1 100644 --- a/frontend/src/utils/csvExport.ts +++ b/frontend/src/utils/csvExport.ts @@ -41,5 +41,6 @@ export const downloadCSV = (data: T[], filename: string) => { document.body.appendChild(link); link.click(); document.body.removeChild(link); + URL.revokeObjectURL(url); } }; From 116541462e9d0a6306f52e0d832b97dc8653edce Mon Sep 17 00:00:00 2001 From: pope-h Date: Fri, 29 May 2026 02:56:15 +0100 Subject: [PATCH 3/4] chore: remove inconsistent deploy.ts in favor of deploy.sh deploy.ts had incompatible differences with deploy.sh: - Argument format: --network=testnet vs --network testnet - Environment variable: STELLAR_SECRET_KEY vs DEPLOYER_SECRET - Network argument semantics: RPC URL vs network name The README documents deploy.sh as canonical. Keep deploy.sh as single source of truth for deployments. Closes #515 --- scripts/deploy.ts | 247 ---------------------------------------------- 1 file changed, 247 deletions(-) delete mode 100644 scripts/deploy.ts diff --git a/scripts/deploy.ts b/scripts/deploy.ts deleted file mode 100644 index ca71e571..00000000 --- a/scripts/deploy.ts +++ /dev/null @@ -1,247 +0,0 @@ -#!/usr/bin/env tsx - -/** - * FlowFi Contract Deployment Script - * - * This script automates the deployment and initialization of FlowFi smart contracts - * to both testnet and mainnet Stellar networks. - * - * Usage: - * npx tsx scripts/deploy.ts --network testnet - * npx tsx scripts/deploy.ts --network mainnet - * - * Environment Variables Required: - * - STELLAR_SECRET_KEY: Secret key for deployment account - * - ADMIN_ADDRESS: Admin address for contract initialization - * - TREASURY_ADDRESS: Treasury address for fee collection - * - FEE_RATE_BPS: Fee rate in basis points (e.g., 25 for 0.25%) - */ - -import { execSync } from 'child_process'; -import { writeFileSync, readFileSync, existsSync } from 'fs'; -import { join } from 'path'; - -interface DeploymentInfo { - network: string; - contractId: string; - deployedAt: string; - adminAddress: string; - treasuryAddress: string; - feeRateBps: number; - transactionHash: string; -} - -interface Config { - network: 'testnet' | 'mainnet'; - adminAddress: string; - treasuryAddress: string; - feeRateBps: number; - secretKey: string; -} - -// Parse command line arguments -function parseArgs(): Config { - const args = process.argv.slice(2); - const networkArg = args.find(arg => arg.startsWith('--network='))?.split('=')[1]; - - if (!networkArg || !['testnet', 'mainnet'].includes(networkArg)) { - console.error('āŒ Invalid or missing network. Use --network=testnet or --network=mainnet'); - process.exit(1); - } - - // Validate required environment variables - const requiredEnvVars = ['STELLAR_SECRET_KEY', 'ADMIN_ADDRESS', 'TREASURY_ADDRESS', 'FEE_RATE_BPS']; - const missingVars = requiredEnvVars.filter(varName => !process.env[varName]); - - if (missingVars.length > 0) { - console.error('āŒ Missing required environment variables:'); - missingVars.forEach(varName => console.error(` - ${varName}`)); - console.error('\nPlease set these environment variables before running the script.'); - process.exit(1); - } - - const feeRateBps = parseInt(process.env.FEE_RATE_BPS!); - if (isNaN(feeRateBps) || feeRateBps < 0 || feeRateBps > 10000) { - console.error('āŒ FEE_RATE_BPS must be a number between 0 and 10000 (0% to 100%)'); - process.exit(1); - } - - return { - network: networkArg as 'testnet' | 'mainnet', - adminAddress: process.env.ADMIN_ADDRESS!, - treasuryAddress: process.env.TREASURY_ADDRESS!, - feeRateBps, - secretKey: process.env.STELLAR_SECRET_KEY! - }; -} - -// Execute command and handle errors -function runCommand(command: string, description: string): void { - console.log(`šŸ”§ ${description}...`); - try { - execSync(command, { stdio: 'inherit', cwd: join(process.cwd(), 'contracts') }); - console.log(`āœ… ${description} completed`); - } catch (error) { - console.error(`āŒ ${description} failed:`, error); - process.exit(1); - } -} - -// Get network-specific configuration -function getNetworkConfig(network: string) { - const configs = { - testnet: { - rpcUrl: 'https://soroban-testnet.stellar.org', - horizonUrl: 'https://horizon-testnet.stellar.org', - friendbotUrl: 'https://friendbot.stellar.org', - networkPassphrase: 'Test SDF Network ; September 2015' - }, - mainnet: { - rpcUrl: 'https://soroban-rpc.stellar.org', - horizonUrl: 'https://horizon.stellar.org', - friendbotUrl: '', - networkPassphrase: 'Public Global Stellar Network ; September 2015' - } - }; - - return configs[network as keyof typeof configs]; -} - -// Save deployment information -function saveDeploymentInfo(info: DeploymentInfo): void { - const filePath = join(process.cwd(), 'deployment-info.json'); - const existingData = existsSync(filePath) ? JSON.parse(readFileSync(filePath, 'utf8')) : {}; - - // Update or add deployment info for this network - existingData[info.network] = info; - existingData.lastUpdated = new Date().toISOString(); - - writeFileSync(filePath, JSON.stringify(existingData, null, 2)); - console.log(`šŸ’¾ Deployment info saved to ${filePath}`); -} - -// Main deployment function -async function deploy(): Promise { - console.log('šŸš€ Starting FlowFi Contract Deployment...\n'); - - const config = parseArgs(); - const networkConfig = getNetworkConfig(config.network); - - console.log(`šŸ“‹ Configuration:`); - console.log(` Network: ${config.network}`); - console.log(` Admin: ${config.adminAddress}`); - console.log(` Treasury: ${config.treasuryAddress}`); - console.log(` Fee Rate: ${config.feeRateBps} bps (${config.feeRateBps / 100}%)`); - console.log(''); - - // Step 1: Build WASM - console.log('šŸ“¦ Step 1: Building WASM...'); - runCommand('cargo build --target wasm32-unknown-unknown --release', 'Building WASM'); - - // Step 2: Optimize WASM - console.log('\n⚔ Step 2: Optimizing WASM...'); - const wasmPath = join('contracts', 'target', 'wasm32-unknown-unknown', 'release', 'stream_contract.wasm'); - runCommand(`stellar contract optimize --wasm ${wasmPath}`, 'Optimizing WASM'); - - // Step 3: Deploy contract - console.log('\nšŸš€ Step 3: Deploying contract...'); - const optimizedWasmPath = wasmPath.replace('.wasm', '.optimized.wasm'); - - try { - const deployCommand = [ - 'stellar contract deploy', - `--wasm ${optimizedWasmPath}`, - `--source ${config.secretKey}`, - `--network ${networkConfig.rpcUrl}`, - '--network-passphrase "' + networkConfig.networkPassphrase + '"' - ].join(' '); - - console.log(`šŸ”§ Deploying contract...`); - const deployOutput = execSync(deployCommand, { - encoding: 'utf8', - cwd: join(process.cwd(), 'contracts') - }); - - // Extract contract ID from output - const contractIdMatch = deployOutput.match(/Contract ID: ([A-Z0-9]+)/); - if (!contractIdMatch) { - throw new Error('Could not extract contract ID from deployment output'); - } - - const contractId = contractIdMatch[1]; - console.log(`āœ… Contract deployed with ID: ${contractId}`); - - // Step 4: Initialize contract - console.log('\nāš™ļø Step 4: Initializing contract...'); - const initCommand = [ - 'stellar contract invoke', - `--id ${contractId}`, - `--source ${config.secretKey}`, - `--network ${networkConfig.rpcUrl}`, - '--network-passphrase "' + networkConfig.networkPassphrase + '"', - 'initialize', - `--admin ${config.adminAddress}`, - `--treasury ${config.treasuryAddress}`, - `--fee_rate_bps ${config.feeRateBps}` - ].join(' '); - - console.log(`šŸ”§ Initializing contract...`); - const initOutput = execSync(initCommand, { - encoding: 'utf8', - cwd: join(process.cwd(), 'contracts') - }); - - // Extract transaction hash from output - const txHashMatch = initOutput.match(/Transaction hash: ([A-Z0-9]+)/); - const txHash = txHashMatch ? txHashMatch[1] : 'unknown'; - - console.log(`āœ… Contract initialized successfully`); - - // Step 5: Save deployment info - const deploymentInfo: DeploymentInfo = { - network: config.network, - contractId, - deployedAt: new Date().toISOString(), - adminAddress: config.adminAddress, - treasuryAddress: config.treasuryAddress, - feeRateBps: config.feeRateBps, - transactionHash: txHash - }; - - saveDeploymentInfo(deploymentInfo); - - // Step 6: Display summary - console.log('\nšŸŽ‰ Deployment Summary:'); - console.log(` Network: ${config.network}`); - console.log(` Contract ID: ${contractId}`); - console.log(` Transaction Hash: ${txHash}`); - console.log(` Admin: ${config.adminAddress}`); - console.log(` Treasury: ${config.treasuryAddress}`); - console.log(` Fee Rate: ${config.feeRateBps} bps`); - console.log(` Deployed At: ${deploymentInfo.deployedAt}`); - console.log('\nāœ… Deployment completed successfully!'); - - } catch (error) { - console.error('āŒ Deployment failed:', error); - process.exit(1); - } -} - -// Handle errors gracefully -process.on('uncaughtException', (error) => { - console.error('āŒ Uncaught exception:', error); - process.exit(1); -}); - -process.on('unhandledRejection', (reason, promise) => { - console.error('āŒ Unhandled rejection at:', promise, 'reason:', reason); - process.exit(1); -}); - -// Run deployment -if (require.main === module) { - deploy().catch(error => { - console.error('āŒ Deployment failed:', error); - process.exit(1); - }); -} From 0e3ec77568c51584f76fc656406fff7e92ba6487 Mon Sep 17 00:00:00 2001 From: pope-h Date: Fri, 29 May 2026 02:56:19 +0100 Subject: [PATCH 4/4] fix(backend): remove computed-but-unused claimableOutTotal Remove unused loop that calculates claimableOutTotal in getUserStreamSummary. The value is never included in the returned summary object, so the computation is wasted per request. Closes #516 --- backend/src/controllers/stream.controller.ts | 7 ------- 1 file changed, 7 deletions(-) diff --git a/backend/src/controllers/stream.controller.ts b/backend/src/controllers/stream.controller.ts index c5182690..fda32d42 100644 --- a/backend/src/controllers/stream.controller.ts +++ b/backend/src/controllers/stream.controller.ts @@ -443,13 +443,6 @@ export const getUserStreamSummary = async (req: Request<{ address: string }>, re claimableInTotal += BigInt(claimable.claimableAmount); } - let claimableOutTotal = 0n; - for (const stream of outgoingStreams) { - // Outgoing streams also need to account for what the recipient can currently claim - const claimable = claimableAmountService.getClaimableAmount(stream as any, calculatedAt); - claimableOutTotal += BigInt(claimable.claimableAmount); - } - const totalStreamsCreated = outgoingStreams.length; const totalStreamedOut = sumStringI128(outgoingStreams.map((stream: any) => stream.withdrawnAmount)); const totalStreamedIn = sumStringI128(incomingStreams.map((stream: any) => stream.withdrawnAmount));