- You haven't created or received any payment streams yet.
+ You haven't created or received any payment streams yet.
Connect with others and start streaming tokens in real-time.
setShowWizard(true)} glow size="lg">
diff --git a/frontend/src/lib/amount.ts b/frontend/src/lib/amount.ts
new file mode 100644
index 0000000..56fddca
--- /dev/null
+++ b/frontend/src/lib/amount.ts
@@ -0,0 +1,169 @@
+/**
+ * Shared utilities for formatting and parsing token amounts
+ * Handles conversion between raw on-chain amounts (i128) and display values
+ */
+
+/**
+ * Format raw amount (bigint) to display string with proper decimal places
+ * @param raw - Raw amount as bigint
+ * @param decimals - Number of decimal places for the token
+ * @returns Formatted string
+ */
+export function formatAmount(raw: bigint, decimals: number): string {
+ if (raw === 0n) return '0';
+
+ const divisor = 10n ** BigInt(decimals);
+ const whole = raw / divisor;
+ const fractional = raw % divisor;
+
+ if (fractional === 0n) {
+ return whole.toString();
+ }
+
+ // Pad fractional part with leading zeros
+ const fractionalStr = fractional.toString().padStart(decimals, '0');
+
+ // Remove trailing zeros
+ const trimmedFractional = fractionalStr.replace(/0+$/, '');
+
+ return `${whole}.${trimmedFractional}`;
+}
+
+/**
+ * Parse display string back to raw amount (bigint)
+ * @param display - Display string (e.g., "1.234")
+ * @param decimals - Number of decimal places for the token
+ * @returns Raw amount as bigint
+ */
+export function parseAmount(display: string, decimals: number): bigint {
+ if (!display || display.trim() === '') return 0n;
+
+ const cleanDisplay = display.trim();
+ const divisor = 10n ** BigInt(decimals);
+
+ if (cleanDisplay.includes('.')) {
+ const [wholePart, fractionalPart] = cleanDisplay.split('.');
+ const whole = BigInt(wholePart || '0');
+
+ // Handle fractional part - pad or truncate to correct length
+ let fractional = fractionalPart || '';
+ if (fractional.length > decimals) {
+ // Truncate if too long
+ fractional = fractional.slice(0, decimals);
+ } else {
+ // Pad with zeros if too short
+ fractional = fractional.padEnd(decimals, '0');
+ }
+
+ const fractionalBig = BigInt(fractional || '0');
+ return whole * divisor + fractionalBig;
+ } else {
+ return BigInt(cleanDisplay) * divisor;
+ }
+}
+
+/**
+ * Format rate per second to human-readable string
+ * @param ratePerSec - Rate per second as bigint
+ * @param decimals - Number of decimal places for the token
+ * @param symbol - Token symbol (optional)
+ * @returns Formatted rate string
+ */
+export function formatRate(ratePerSec: bigint, decimals: number, symbol = ''): string {
+ if (ratePerSec === 0n) return '0';
+
+ const ratePerSecond = formatAmount(ratePerSec, decimals);
+ const ratePerDay = formatAmount(ratePerSec * 86400n, decimals); // 86400 seconds in a day
+
+ const symbolStr = symbol ? ` ${symbol}` : '';
+ return `${ratePerSecond}${symbolStr}/sec (${ratePerDay}${symbolStr}/day)`;
+}
+
+/**
+ * Check if input string has valid precision for the given decimals
+ * @param input - Input string to validate
+ * @param decimals - Maximum allowed decimal places
+ * @returns True if valid precision
+ */
+export function hasValidPrecision(input: string, decimals: number): boolean {
+ if (!input || input.trim() === '') return true; // Empty is valid (will be parsed as 0)
+
+ const cleanInput = input.trim();
+
+ // Check if it's a valid number format
+ if (!/^\d*\.?\d*$/.test(cleanInput)) return false;
+
+ if (cleanInput.includes('.')) {
+ const fractionalPart = cleanInput.split('.')[1];
+ return fractionalPart.length <= decimals;
+ }
+
+ return true;
+}
+
+/**
+ * Convert value to stroops (smallest unit, 7 decimal places for XLM)
+ * @param value - String value in XLM
+ * @returns Value in stroops as bigint
+ */
+export function toStroops(value: string): bigint {
+ return parseAmount(value, 7); // XLM uses 7 decimal places
+}
+
+/**
+ * Convert stroops back to XLM string
+ * @param stroops - Value in stroops as bigint
+ * @returns XLM string
+ */
+export function fromStroops(stroops: bigint): string {
+ return formatAmount(stroops, 7);
+}
+
+/**
+ * Truncate amount to specified decimal places without rounding
+ * @param amount - Amount as bigint
+ * @param decimals - Token decimals
+ * @param maxDisplayDecimals - Maximum decimal places to display
+ * @returns Truncated string
+ */
+export function truncateAmount(amount: bigint, decimals: number, maxDisplayDecimals: number): string {
+ if (amount === 0n) return '0';
+
+ const divisor = 10n ** BigInt(decimals);
+ const whole = amount / divisor;
+ const fractional = amount % divisor;
+
+ if (fractional === 0n) {
+ return whole.toString();
+ }
+
+ // Convert fractional to string and truncate
+ const fractionalStr = fractional.toString().padStart(decimals, '0');
+ const truncatedFractional = fractionalStr.slice(0, maxDisplayDecimals);
+
+ // Remove trailing zeros from truncated part
+ const trimmedFractional = truncatedFractional.replace(/0+$/, '');
+
+ if (trimmedFractional === '') {
+ return whole.toString();
+ }
+
+ return `${whole}.${trimmedFractional}`;
+}
+
+/**
+ * Format amount with compact notation (K, M, B) for large numbers
+ * @param amount - Amount as bigint
+ * @param decimals - Token decimals
+ * @returns Compact formatted string
+ */
+export function formatCompactAmount(amount: bigint, decimals: number): string {
+ const displayAmount = formatAmount(amount, decimals);
+ const num = parseFloat(displayAmount);
+
+ if (num === 0) return '0';
+ if (num < 1000) return displayAmount;
+ if (num < 1000000) return `${(num / 1000).toFixed(1)}K`;
+ if (num < 1000000000) return `${(num / 1000000).toFixed(1)}M`;
+ return `${(num / 1000000000).toFixed(1)}B`;
+}
diff --git a/package-lock.json b/package-lock.json
index aed7c65..e331c75 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -82,7 +82,6 @@
"version": "25.3.0",
"dev": true,
"license": "MIT",
- "peer": true,
"dependencies": {
"undici-types": "~7.18.0"
}
@@ -286,7 +285,6 @@
"integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==",
"dev": true,
"license": "MIT",
- "peer": true,
"dependencies": {
"esbuild": "^0.21.3",
"postcss": "^8.4.43",
@@ -430,7 +428,7 @@
"@types/react-dom": "^19",
"@vitejs/plugin-react": "^4.7.0",
"eslint": "^9",
- "eslint-config-next": "16.1.6",
+ "eslint-config-next": "^16.1.6",
"happy-dom": "^20.9.0",
"jsdom": "^27.0.1",
"tailwindcss": "^4",
@@ -11650,7 +11648,6 @@
"node_modules/zod": {
"version": "4.3.6",
"license": "MIT",
- "peer": true,
"funding": {
"url": "https://github.com/sponsors/colinhacks"
}
diff --git a/scripts/deploy.ts b/scripts/deploy.ts
new file mode 100644
index 0000000..ca71e57
--- /dev/null
+++ b/scripts/deploy.ts
@@ -0,0 +1,247 @@
+#!/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);
+ });
+}