From c4629eb9c9c99158819b028866949935e9b661d5 Mon Sep 17 00:00:00 2001 From: Xoulomon Date: Sun, 26 Jul 2026 17:01:50 +0100 Subject: [PATCH 1/4] fix(#281): remove unused useNotifications.ts duplicate hook and test The hooks/useNotifications.ts file is a dead scaffold with no importers. The actual implementation is in hooks/use-notifications.ts which is used by components/layout/notification-bell.tsx. Removing the duplicate reduces confusion for future contributors. --- __tests__/hooks/useNotifications.test.ts | 76 ----------------------- hooks/useNotifications.ts | 77 ------------------------ 2 files changed, 153 deletions(-) delete mode 100644 __tests__/hooks/useNotifications.test.ts delete mode 100644 hooks/useNotifications.ts diff --git a/__tests__/hooks/useNotifications.test.ts b/__tests__/hooks/useNotifications.test.ts deleted file mode 100644 index 9855119..0000000 --- a/__tests__/hooks/useNotifications.test.ts +++ /dev/null @@ -1,76 +0,0 @@ -import { describe, it, expect, vi, beforeEach } from 'vitest' -import { renderHook, act } from '@testing-library/react' -import { useNotifications } from '@/hooks/useNotifications' - -class MockNotification { - static permission = 'granted' - static requestPermission = vi.fn().mockResolvedValue('granted') - constructor(public title: string, public options?: NotificationOptions) {} -} - -describe('useNotifications', () => { - beforeEach(() => { - localStorage.clear() - vi.stubGlobal('Notification', MockNotification) - }) - - it('starts with no notifications and default preferences enabled', () => { - const { result } = renderHook(() => useNotifications()) - expect(result.current.notifications).toEqual([]) - expect(result.current.unreadCount).toBe(0) - expect(result.current.prefs.stream_received).toBe(true) - }) - - it('adds a notification and increments unreadCount', () => { - const { result } = renderHook(() => useNotifications()) - act(() => { - result.current.addNotification({ - type: 'stream_received', - message: 'You received a stream', - streamId: '1', - }) - }) - expect(result.current.notifications).toHaveLength(1) - expect(result.current.unreadCount).toBe(1) - }) - - it('does not add a notification when the type preference is disabled', () => { - const { result } = renderHook(() => useNotifications()) - act(() => { - result.current.updatePref('stream_received', false) - }) - act(() => { - result.current.addNotification({ - type: 'stream_received', - message: 'You received a stream', - streamId: '1', - }) - }) - expect(result.current.notifications).toHaveLength(0) - }) - - it('markAllRead marks every notification as read', () => { - const { result } = renderHook(() => useNotifications()) - act(() => { - result.current.addNotification({ - type: 'cliff_reached', - message: 'Cliff reached', - streamId: '1', - }) - }) - act(() => { - result.current.markAllRead() - }) - expect(result.current.unreadCount).toBe(0) - expect(result.current.notifications.every((n) => n.read)).toBe(true) - }) - - it('updatePref persists the preference to localStorage', () => { - const { result } = renderHook(() => useNotifications()) - act(() => { - result.current.updatePref('topup_received', false) - }) - const stored = JSON.parse(localStorage.getItem('flowstar_notif_prefs') ?? '{}') - expect(stored.topup_received).toBe(false) - }) -}) diff --git a/hooks/useNotifications.ts b/hooks/useNotifications.ts deleted file mode 100644 index 848e4f7..0000000 --- a/hooks/useNotifications.ts +++ /dev/null @@ -1,77 +0,0 @@ -import { useState, useEffect, useCallback } from 'react'; - -export type NotificationType = - | 'stream_received' | 'cliff_reached' | 'stream_completed' - | 'stream_cancelled' | 'stream_transferred' | 'topup_received'; - -export interface Notification { - id: string; - type: NotificationType; - message: string; - streamId: string; - read: boolean; - timestamp: string; -} - -const PREFS_KEY = 'flowstar_notif_prefs'; - -const defaultPrefs: Record = { - stream_received: true, - cliff_reached: true, - stream_completed: true, - stream_cancelled: true, - stream_transferred: true, - topup_received: true, -}; - -export function useNotifications() { - const [notifications, setNotifications] = useState([]); - const [prefs, setPrefs] = useState>(() => { - try { - return JSON.parse(localStorage.getItem(PREFS_KEY) ?? 'null') ?? defaultPrefs; - } catch { return defaultPrefs; } - }); - - const unreadCount = notifications.filter(n => !n.read).length; - - const addNotification = useCallback((notif: Omit) => { - if (!prefs[notif.type]) return; - const full: Notification = { - ...notif, - id: crypto.randomUUID(), - read: false, - timestamp: new Date().toISOString(), - }; - setNotifications(prev => [full, ...prev]); - - // Browser push - if (Notification.permission === 'granted') { - new Notification('FlowStar', { body: notif.message, icon: '/favicon.ico' }); - } - }, [prefs]); - - const markAllRead = () => - setNotifications(prev => prev.map(n => ({ ...n, read: true }))); - - const updatePref = (type: NotificationType, enabled: boolean) => { - const next = { ...prefs, [type]: enabled }; - setPrefs(next); - localStorage.setItem(PREFS_KEY, JSON.stringify(next)); - }; - - const requestPushPermission = () => Notification.requestPermission(); - - // Poll contract events every 30s - useEffect(() => { - const poll = async () => { - // Replace with your actual contract event polling logic - // const events = await fetchContractEvents(); - // events.forEach(e => addNotification(mapEventToNotification(e))); - }; - poll(); - const interval = setInterval(poll, 30_000); - return () => clearInterval(interval); - }, [addNotification]); - - return { notifications, unreadCount, addNotification, markAllRead, updatePref, prefs, requestPushPermission }; -} \ No newline at end of file From bd59df49318e511ad6c0094f2299ba0b7e4f5c40 Mon Sep 17 00:00:00 2001 From: Xoulomon Date: Sun, 26 Jul 2026 17:02:06 +0100 Subject: [PATCH 2/4] fix(#282): remove unused useBulkActions and useBulkSelect hooks Both hooks/useBulkActions.ts and hooks/useBulkSelect.ts have no importers outside their test files. Removing unused code reduces maintenance surface and prevents contributor confusion when searching for bulk-action logic. --- __tests__/hooks/useBulkActions.test.ts | 39 ---------------------- __tests__/hooks/useBulkSelect.test.ts | 45 -------------------------- hooks/useBulkActions.ts | 44 ------------------------- hooks/useBulkSelect.ts | 29 ----------------- 4 files changed, 157 deletions(-) delete mode 100644 __tests__/hooks/useBulkActions.test.ts delete mode 100644 __tests__/hooks/useBulkSelect.test.ts delete mode 100644 hooks/useBulkActions.ts delete mode 100644 hooks/useBulkSelect.ts diff --git a/__tests__/hooks/useBulkActions.test.ts b/__tests__/hooks/useBulkActions.test.ts deleted file mode 100644 index a5db6f4..0000000 --- a/__tests__/hooks/useBulkActions.test.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { describe, it, expect, vi } from 'vitest' -import { renderHook, act } from '@testing-library/react' -import { useBulkActions } from '@/hooks/useBulkActions' - -describe('useBulkActions', () => { - it('starts idle with empty progress and results', () => { - const { result } = renderHook(() => useBulkActions()) - expect(result.current.status).toBe('idle') - expect(result.current.progress).toEqual({ done: 0, total: 0 }) - expect(result.current.results).toEqual([]) - }) - - it('runs an action over every id and reports success', async () => { - const action = vi.fn().mockResolvedValue(undefined) - const { result } = renderHook(() => useBulkActions()) - await act(async () => { - await result.current.runBulk(['1', '2', '3'], action) - }) - expect(action).toHaveBeenCalledTimes(3) - expect(result.current.status).toBe('done') - expect(result.current.progress).toEqual({ done: 3, total: 3 }) - expect(result.current.succeeded).toBe(3) - expect(result.current.failed).toBe(0) - }) - - it('tracks failures separately from successes', async () => { - const action = vi - .fn() - .mockResolvedValueOnce(undefined) - .mockRejectedValueOnce(new Error('boom')) - const { result } = renderHook(() => useBulkActions()) - await act(async () => { - await result.current.runBulk(['1', '2'], action) - }) - expect(result.current.succeeded).toBe(1) - expect(result.current.failed).toBe(1) - expect(result.current.results.find((r) => r.id === '2')?.error).toBe('boom') - }) -}) diff --git a/__tests__/hooks/useBulkSelect.test.ts b/__tests__/hooks/useBulkSelect.test.ts deleted file mode 100644 index 7362b4e..0000000 --- a/__tests__/hooks/useBulkSelect.test.ts +++ /dev/null @@ -1,45 +0,0 @@ -import { describe, it, expect } from 'vitest' -import { renderHook, act } from '@testing-library/react' -import { useBulkSelect } from '@/hooks/useBulkSelect' - -const items = [{ id: '1' }, { id: '2' }, { id: '3' }] - -describe('useBulkSelect', () => { - it('starts with nothing selected', () => { - const { result } = renderHook(() => useBulkSelect(items)) - expect(result.current.someSelected).toBe(false) - expect(result.current.allSelected).toBe(false) - expect(result.current.selectedItems).toEqual([]) - }) - - it('toggle adds and removes an id', () => { - const { result } = renderHook(() => useBulkSelect(items)) - act(() => result.current.toggle('1')) - expect(result.current.selected.has('1')).toBe(true) - expect(result.current.selectedItems).toEqual([{ id: '1' }]) - act(() => result.current.toggle('1')) - expect(result.current.selected.has('1')).toBe(false) - }) - - it('toggleAll selects everything when nothing is fully selected', () => { - const { result } = renderHook(() => useBulkSelect(items)) - act(() => result.current.toggleAll()) - expect(result.current.allSelected).toBe(true) - expect(result.current.selectedItems).toHaveLength(3) - }) - - it('toggleAll clears selection when everything is already selected', () => { - const { result } = renderHook(() => useBulkSelect(items)) - act(() => result.current.toggleAll()) - act(() => result.current.toggleAll()) - expect(result.current.allSelected).toBe(false) - expect(result.current.selected.size).toBe(0) - }) - - it('clear empties the selection', () => { - const { result } = renderHook(() => useBulkSelect(items)) - act(() => result.current.toggle('1')) - act(() => result.current.clear()) - expect(result.current.someSelected).toBe(false) - }) -}) diff --git a/hooks/useBulkActions.ts b/hooks/useBulkActions.ts deleted file mode 100644 index b4234bc..0000000 --- a/hooks/useBulkActions.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { useState } from 'react'; - -export type BulkActionStatus = 'idle' | 'running' | 'done'; - -export interface StreamResult { - id: string; - success: boolean; - error?: string; -} - -export function useBulkActions() { - const [status, setStatus] = useState('idle'); - const [progress, setProgress] = useState({ done: 0, total: 0 }); - const [results, setResults] = useState([]); - - async function runBulk( - ids: string[], - action: (id: string) => Promise, - ) { - setStatus('running'); - setProgress({ done: 0, total: ids.length }); - setResults([]); - - const out: StreamResult[] = []; - - for (const id of ids) { - try { - await action(id); - out.push({ id, success: true }); - } catch (err) { - out.push({ id, success: false, error: (err as Error).message }); - } - setProgress(p => ({ ...p, done: p.done + 1 })); - setResults([...out]); - } - - setStatus('done'); - } - - const succeeded = results.filter(r => r.success).length; - const failed = results.filter(r => !r.success).length; - - return { status, progress, results, succeeded, failed, runBulk }; -} \ No newline at end of file diff --git a/hooks/useBulkSelect.ts b/hooks/useBulkSelect.ts deleted file mode 100644 index 9b9013d..0000000 --- a/hooks/useBulkSelect.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { useState, useCallback } from 'react'; - -export function useBulkSelect(items: T[]) { - const [selected, setSelected] = useState>(new Set()); - - const toggle = useCallback((id: string) => { - setSelected(prev => { - const next = new Set(prev); - next.has(id) ? next.delete(id) : next.add(id); - return next; - }); - }, []); - - const toggleAll = useCallback(() => { - setSelected(prev => - prev.size === items.length - ? new Set() - : new Set(items.map(i => i.id)) - ); - }, [items]); - - const clear = useCallback(() => setSelected(new Set()), []); - - const selectedItems = items.filter(i => selected.has(i.id)); - const allSelected = selected.size === items.length && items.length > 0; - const someSelected = selected.size > 0; - - return { selected, selectedItems, allSelected, someSelected, toggle, toggleAll, clear }; -} \ No newline at end of file From 88f61213ffd71c7adc33598e3e7e7ca798ae2f53 Mon Sep 17 00:00:00 2001 From: Xoulomon Date: Sun, 26 Jul 2026 17:03:57 +0100 Subject: [PATCH 3/4] fix(#283): handle unknown tokens gracefully in usePortfolioValue Previously, a single stream with an unknown token would set totalUsd to null for the entire portfolio, hiding USD values for all known tokens. Now: - Only sum USD values for streams with known/priced tokens (XLM, USDC, EURC) - Unknown tokens are silently skipped instead of nulling the aggregate - Returns null only when ALL streams use unknown tokens - Added test cases to verify the fix handles mixed known/unknown tokens --- __tests__/hooks/use-token-price.test.ts | 67 +++++++++++++++++++++++++ hooks/use-token-price.ts | 24 ++++++--- 2 files changed, 83 insertions(+), 8 deletions(-) diff --git a/__tests__/hooks/use-token-price.test.ts b/__tests__/hooks/use-token-price.test.ts index 01af681..89d9927 100644 --- a/__tests__/hooks/use-token-price.test.ts +++ b/__tests__/hooks/use-token-price.test.ts @@ -99,4 +99,71 @@ describe('usePortfolioValue', () => { expect(result.current.totalUsd).toBe(10_000) }) }) + + it('shows USD value for known tokens and ignores unknown tokens', async () => { + const streams: StreamData[] = [ + { + id: '1', + sender: 'GSENDER', + recipient: 'GRECIPIENT', + token: { address: 'CUSDC', symbol: 'USDC', decimals: 7 }, + depositedAmount: 5_000_0000000n, + withdrawnAmount: 0n, + startTime: 0n, + endTime: 9999999999n, + cliffTime: 0n, + cliffAmount: 0n, + amountPerSecond: 1n, + linearAmount: 5_000_0000000n, + duration: 9999999999n, + cancelled: false, + }, + { + id: '2', + sender: 'GSENDER', + recipient: 'GRECIPIENT', + token: { address: 'CUSTOM_TOKEN', symbol: 'CUSTOM', decimals: 6 }, + depositedAmount: 1_000_000000n, + withdrawnAmount: 0n, + startTime: 0n, + endTime: 9999999999n, + cliffTime: 0n, + cliffAmount: 0n, + amountPerSecond: 1n, + linearAmount: 1_000_000000n, + duration: 9999999999n, + cancelled: false, + }, + ] + const { result } = renderHook(() => usePortfolioValue(streams)) + await waitFor(() => { + // Should show 5000 USD from USDC, not null despite unknown CUSTOM token + expect(result.current.totalUsd).toBe(5_000) + }) + }) + + it('returns null when all streams use unknown tokens', async () => { + const streams: StreamData[] = [ + { + id: '1', + sender: 'GSENDER', + recipient: 'GRECIPIENT', + token: { address: 'CUSTOM_TOKEN', symbol: 'CUSTOM', decimals: 6 }, + depositedAmount: 1_000_000000n, + withdrawnAmount: 0n, + startTime: 0n, + endTime: 9999999999n, + cliffTime: 0n, + cliffAmount: 0n, + amountPerSecond: 1n, + linearAmount: 1_000_000000n, + duration: 9999999999n, + cancelled: false, + }, + ] + const { result } = renderHook(() => usePortfolioValue(streams)) + await waitFor(() => { + expect(result.current.totalUsd).toBeNull() + }) + }) }) diff --git a/hooks/use-token-price.ts b/hooks/use-token-price.ts index 7f56d77..29950cb 100644 --- a/hooks/use-token-price.ts +++ b/hooks/use-token-price.ts @@ -138,16 +138,24 @@ export function usePortfolioValue(streams: StreamData[]): PortfolioValue { ); let totalUsd: number | null = 0; + let hasKnownToken = false; + for (const stream of streams) { - const tp = priceMap[stream.token.symbol]; - if (!tp || tp.usdPrice === null) { - totalUsd = null; - break; + const symbol = stream.token.symbol; + const tp = priceMap[symbol]; + + // Only process known symbols with available prices + if (KNOWN_SYMBOLS.includes(symbol) && tp?.usdPrice !== null) { + hasKnownToken = true; + const locked = stream.depositedAmount - stream.withdrawnAmount; + const human = Number(locked) / Math.pow(10, stream.token.decimals); + totalUsd = (totalUsd ?? 0) + human * tp.usdPrice; } - const locked = stream.depositedAmount - stream.withdrawnAmount; - const human = Number(locked) / Math.pow(10, stream.token.decimals); - totalUsd = (totalUsd ?? 0) + human * tp.usdPrice; + // Skip unknown tokens — don't null out the entire portfolio } - return { totalUsd, loading, stale }; + // Only return null if there are no known tokens (all are custom/unknown) + const finalTotalUsd = hasKnownToken ? totalUsd : null; + + return { totalUsd: finalTotalUsd, loading, stale }; } From 92c36ab4ff52ca9a33872e3b6f0a0488244e5431 Mon Sep 17 00:00:00 2001 From: Xoulomon Date: Sun, 26 Jul 2026 17:05:38 +0100 Subject: [PATCH 4/4] fix(#287): add soroban contract error code pattern matching The error message mapper now recognizes and handles all Soroban contract error codes (Error(Contract, #N)) from the StreamError enum (1-20) with specific, actionable error messages for each code. This replaces the previous behavior where contract-level failures fell through to the generic 'Transaction failed' message. Each error now includes: - Descriptive message identifying the specific failure - Actionable suggestion for recovery - Appropriate category (user, contract, or network) - Fallback patterns preserved for non-contract errors Patterns match the format: Error(Contract, #N) with flexible whitespace. --- __tests__/lib/error-messages.test.ts | 127 ++++++++++++++++++ lib/error-messages.ts | 184 ++++++++++++++++++++++++++- 2 files changed, 310 insertions(+), 1 deletion(-) diff --git a/__tests__/lib/error-messages.test.ts b/__tests__/lib/error-messages.test.ts index 4cbb47b..337954f 100644 --- a/__tests__/lib/error-messages.test.ts +++ b/__tests__/lib/error-messages.test.ts @@ -125,6 +125,133 @@ describe('mapError', () => { const result = mapError(new Error('mystery error')) expect(result.details).toBe('mystery error') }) + + // Soroban contract error codes + it('maps Error(Contract, 1) - InvalidAmount', () => { + const result = mapError(new Error('Error(Contract, 1)')) + expect(result.message).toBe('Invalid stream amount') + expect(result.category).toBe('contract') + }) + + it('maps Error(Contract, 2) - InvalidTimeRange', () => { + const result = mapError(new Error('Error(Contract, 2)')) + expect(result.message).toBe('Invalid time range') + expect(result.category).toBe('contract') + }) + + it('maps Error(Contract, 3) - InvalidCliff', () => { + const result = mapError(new Error('Error(Contract, 3)')) + expect(result.message).toBe('Invalid cliff configuration') + expect(result.category).toBe('contract') + }) + + it('maps Error(Contract, 4) - SelfStream', () => { + const result = mapError(new Error('Error(Contract, 4)')) + expect(result.message).toBe('Cannot create self-directed stream') + expect(result.category).toBe('user') + }) + + it('maps Error(Contract, 5) - StreamNotFound', () => { + const result = mapError(new Error('Error(Contract, 5)')) + expect(result.message).toBe('Stream not found') + expect(result.category).toBe('contract') + }) + + it('maps Error(Contract, 6) - StreamCancelled', () => { + const result = mapError(new Error('Error(Contract, 6)')) + expect(result.message).toBe('Stream has been cancelled') + expect(result.category).toBe('contract') + }) + + it('maps Error(Contract, 7) - Unauthorized', () => { + const result = mapError(new Error('Error(Contract, 7)')) + expect(result.message).toBe('Unauthorized operation') + expect(result.category).toBe('user') + }) + + it('maps Error(Contract, 8) - InsufficientFunds', () => { + const result = mapError(new Error('Error(Contract, 8)')) + expect(result.message).toBe('Insufficient funds in stream') + expect(result.category).toBe('user') + }) + + it('maps Error(Contract, 9) - StreamEnded', () => { + const result = mapError(new Error('Error(Contract, 9)')) + expect(result.message).toBe('Stream has ended') + expect(result.category).toBe('contract') + }) + + it('maps Error(Contract, 10) - SameRecipient', () => { + const result = mapError(new Error('Error(Contract, 10)')) + expect(result.message).toBe('Recipient already set') + expect(result.category).toBe('user') + }) + + it('maps Error(Contract, 11) - BatchSizeExceeded', () => { + const result = mapError(new Error('Error(Contract, 11)')) + expect(result.message).toBe('Batch size exceeds maximum') + expect(result.category).toBe('user') + }) + + it('maps Error(Contract, 12) - BatchEmpty', () => { + const result = mapError(new Error('Error(Contract, 12)')) + expect(result.message).toBe('Batch cannot be empty') + expect(result.category).toBe('user') + }) + + it('maps Error(Contract, 13) - ArithmeticOverflow', () => { + const result = mapError(new Error('Error(Contract, 13)')) + expect(result.message).toBe('Arithmetic overflow in calculation') + expect(result.category).toBe('contract') + }) + + it('maps Error(Contract, 14) - AlreadyInitialized', () => { + const result = mapError(new Error('Error(Contract, 14)')) + expect(result.message).toBe('Contract already initialized') + expect(result.category).toBe('contract') + }) + + it('maps Error(Contract, 15) - NotInitialized', () => { + const result = mapError(new Error('Error(Contract, 15)')) + expect(result.message).toBe('Contract not initialized') + expect(result.category).toBe('contract') + }) + + it('maps Error(Contract, 16) - ContractPaused', () => { + const result = mapError(new Error('Error(Contract, 16)')) + expect(result.message).toBe('Contract is paused') + expect(result.category).toBe('network') + }) + + it('maps Error(Contract, 17) - DurationExceedsMaximum', () => { + const result = mapError(new Error('Error(Contract, 17)')) + expect(result.message).toBe('Stream duration exceeds maximum') + expect(result.category).toBe('user') + }) + + it('maps Error(Contract, 18) - InvalidRecipient', () => { + const result = mapError(new Error('Error(Contract, 18)')) + expect(result.message).toBe('Invalid recipient address') + expect(result.category).toBe('user') + }) + + it('maps Error(Contract, 19) - RateIsZero', () => { + const result = mapError(new Error('Error(Contract, 19)')) + expect(result.message).toBe('Stream amount too small for duration') + expect(result.category).toBe('user') + }) + + it('maps Error(Contract, 20) - StreamNotEligibleForCleanup', () => { + const result = mapError(new Error('Error(Contract, 20)')) + expect(result.message).toBe('Stream not eligible for cleanup') + expect(result.category).toBe('contract') + }) + + it('handles soroban error with extra whitespace', () => { + const result = mapError(new Error('Error(Contract, 1 )')) + expect(result.message).toBe('Invalid stream amount') + expect(result.category).toBe('contract') + }) }) // ─── categoryLabel ──────────────────────────────────────────────────────────── diff --git a/lib/error-messages.ts b/lib/error-messages.ts index fb6e495..eb28c6a 100644 --- a/lib/error-messages.ts +++ b/lib/error-messages.ts @@ -11,7 +11,189 @@ const ERROR_PATTERNS: Array<{ pattern: RegExp | string mapped: Omit }> = [ - // Insufficient balance + // Soroban contract errors: Error(Contract, #N) + // 1: InvalidAmount + { + pattern: /Error\(Contract,\s*1\)/, + mapped: { + message: 'Invalid stream amount', + suggestion: 'The amount is invalid. Check that it is positive and within valid bounds.', + category: 'contract', + }, + }, + // 2: InvalidTimeRange + { + pattern: /Error\(Contract,\s*2\)/, + mapped: { + message: 'Invalid time range', + suggestion: 'The start and end times are invalid. End time must be after start time.', + category: 'contract', + }, + }, + // 3: InvalidCliff + { + pattern: /Error\(Contract,\s*3\)/, + mapped: { + message: 'Invalid cliff configuration', + suggestion: 'The cliff time must be between start and end time.', + category: 'contract', + }, + }, + // 4: SelfStream + { + pattern: /Error\(Contract,\s*4\)/, + mapped: { + message: 'Cannot create self-directed stream', + suggestion: 'You cannot create a stream to yourself. Specify a different recipient.', + category: 'user', + }, + }, + // 5: StreamNotFound + { + pattern: /Error\(Contract,\s*5\)/, + mapped: { + message: 'Stream not found', + suggestion: 'This stream does not exist. It may have been deleted or the ID is incorrect.', + category: 'contract', + }, + }, + // 6: StreamCancelled + { + pattern: /Error\(Contract,\s*6\)/, + mapped: { + message: 'Stream has been cancelled', + suggestion: 'This stream was cancelled and no longer accepts withdrawals or modifications.', + category: 'contract', + }, + }, + // 7: Unauthorized + { + pattern: /Error\(Contract,\s*7\)/, + mapped: { + message: 'Unauthorized operation', + suggestion: 'You do not have permission to perform this action. Only the stream owner can modify it.', + category: 'user', + }, + }, + // 8: InsufficientFunds + { + pattern: /Error\(Contract,\s*8\)/, + mapped: { + message: 'Insufficient funds in stream', + suggestion: 'The stream balance is too low for this operation. Top up the stream or reduce the amount.', + category: 'user', + }, + }, + // 9: StreamEnded + { + pattern: /Error\(Contract,\s*9\)/, + mapped: { + message: 'Stream has ended', + suggestion: 'This stream has completed its term. You can only withdraw remaining funds.', + category: 'contract', + }, + }, + // 10: SameRecipient + { + pattern: /Error\(Contract,\s*10\)/, + mapped: { + message: 'Recipient already set', + suggestion: 'The recipient for this stream is already set and cannot be changed to the same address.', + category: 'user', + }, + }, + // 11: BatchSizeExceeded + { + pattern: /Error\(Contract,\s*11\)/, + mapped: { + message: 'Batch size exceeds maximum', + suggestion: 'You can create a maximum of 20 streams per batch. Split your request into smaller batches.', + category: 'user', + }, + }, + // 12: BatchEmpty + { + pattern: /Error\(Contract,\s*12\)/, + mapped: { + message: 'Batch cannot be empty', + suggestion: 'You must specify at least one stream to create in the batch.', + category: 'user', + }, + }, + // 13: ArithmeticOverflow + { + pattern: /Error\(Contract,\s*13\)/, + mapped: { + message: 'Arithmetic overflow in calculation', + suggestion: 'The stream parameters cause a calculation overflow. Try reducing the amount or duration.', + category: 'contract', + }, + }, + // 14: AlreadyInitialized + { + pattern: /Error\(Contract,\s*14\)/, + mapped: { + message: 'Contract already initialized', + suggestion: 'The contract has already been set up. Initialization can only happen once.', + category: 'contract', + }, + }, + // 15: NotInitialized + { + pattern: /Error\(Contract,\s*15\)/, + mapped: { + message: 'Contract not initialized', + suggestion: 'The contract has not been initialized yet. Please try again later.', + category: 'contract', + }, + }, + // 16: ContractPaused + { + pattern: /Error\(Contract,\s*16\)/, + mapped: { + message: 'Contract is paused', + suggestion: 'The FlowStar contract is currently paused for maintenance. Please try again later.', + category: 'network', + }, + }, + // 17: DurationExceedsMaximum + { + pattern: /Error\(Contract,\s*17\)/, + mapped: { + message: 'Stream duration exceeds maximum', + suggestion: 'The stream duration is too long. Use a shorter duration or split into multiple streams.', + category: 'user', + }, + }, + // 18: InvalidRecipient + { + pattern: /Error\(Contract,\s*18\)/, + mapped: { + message: 'Invalid recipient address', + suggestion: 'The recipient address is invalid or cannot receive the stream. Check the address and try again.', + category: 'user', + }, + }, + // 19: RateIsZero + { + pattern: /Error\(Contract,\s*19\)/, + mapped: { + message: 'Stream amount too small for duration', + suggestion: 'The amount is too small relative to the duration. Increase the amount or reduce the duration.', + category: 'user', + }, + }, + // 20: StreamNotEligibleForCleanup + { + pattern: /Error\(Contract,\s*20\)/, + mapped: { + message: 'Stream not eligible for cleanup', + suggestion: 'The stream must be fully cancelled or drained before cleanup. Wait until the stream ends.', + category: 'contract', + }, + }, + + // Insufficient balance (fallback for non-contract errors) { pattern: /insufficient balance|insufficient funds|balance.*too low|not enough/i, mapped: {