diff --git a/api/src/routes/settlements.ts b/api/src/routes/settlements.ts index cd2dd78e..aaa206f9 100644 --- a/api/src/routes/settlements.ts +++ b/api/src/routes/settlements.ts @@ -138,6 +138,7 @@ router.post('/simulate', (req: Request, res: Response) => { } // Basic input validation + const STELLAR_ADDRESS_RE = /^G[A-Z2-7]{55}$/; for (const r of remittances) { if ( typeof r !== 'object' || @@ -157,6 +158,18 @@ router.post('/simulate', (req: Request, res: Response) => { }; return res.status(400).json(err); } + const item = r as SimulateRemittanceInput; + if (!STELLAR_ADDRESS_RE.test(item.sender) || !STELLAR_ADDRESS_RE.test(item.agent)) { + const err: ErrorResponse = { + success: false, + error: { + message: 'sender and agent must be valid Stellar addresses (G... 56 characters)', + code: 'INVALID_INPUT', + }, + timestamp: new Date().toISOString(), + }; + return res.status(400).json(err); + } } const inputs = remittances as SimulateRemittanceInput[]; diff --git a/frontend/src/components/DisputeResolution.tsx b/frontend/src/components/DisputeResolution.tsx index 1d982e96..04eefe3e 100644 --- a/frontend/src/components/DisputeResolution.tsx +++ b/frontend/src/components/DisputeResolution.tsx @@ -74,8 +74,7 @@ export default function DisputeResolution() { const [auditLog, setAuditLog] = useState([]); const [resolving, setResolving] = useState(null); const [confirmOpen, setConfirmOpen] = useState(null); - const [page, setPage] = useState(1); - const [hasMore, setHasMore] = useState(false); + const [resolvedTxHash, setResolvedTxHash] = useState(null); useEffect(() => { void fetchDisputes(1); @@ -131,6 +130,7 @@ export default function DisputeResolution() { setConfirmOpen(null); setResolving(id); setError(null); + setResolvedTxHash(null); try { const res = await fetch(`${API_URL}/api/disputes/${id}/resolve`, { method: 'POST', @@ -138,7 +138,10 @@ export default function DisputeResolution() { body: JSON.stringify({ in_favour_of_sender: inFavourOfSender }), }); if (!res.ok) throw new Error(`HTTP ${res.status}`); - await fetchDisputes(page); + const data = await res.json() as Record; + const txHash = typeof data.tx_hash === 'string' ? data.tx_hash : null; + setResolvedTxHash(txHash); + await fetchDisputes(); await fetchAuditLog(); } catch (e: unknown) { setError(e instanceof Error ? e.message : 'Unknown error'); @@ -153,6 +156,21 @@ export default function DisputeResolution() { {error &&
{error}
} + {resolvedTxHash && ( +
+ ✅ Dispute resolved on-chain.{' '} + Tx:{' '} + + {resolvedTxHash} + +
+ )} + {confirmOpen && (
void; } +const DEFAULT_DURATION: Record = { + error: 5000, + success: 3000, + info: 4000, + warning: 4000, +}; + function ToastItem({ toast, onDismiss }: ToastItemProps) { - const duration = toast.duration ?? 4000; + const duration = toast.duration ?? DEFAULT_DURATION[toast.type]; const timerRef = useRef | null>(null); + const remainingRef = useRef(duration); + const startRef = useRef(0); - useEffect(() => { - if (duration > 0) { - timerRef.current = setTimeout(() => onDismiss(toast.id), duration); + const startTimer = useCallback(() => { + if (duration <= 0) return; + startRef.current = Date.now(); + timerRef.current = setTimeout(() => onDismiss(toast.id), remainingRef.current); + }, [duration, toast.id, onDismiss]); + + const pauseTimer = useCallback(() => { + if (timerRef.current) { + clearTimeout(timerRef.current); + remainingRef.current -= Date.now() - startRef.current; } + }, []); + + useEffect(() => { + startTimer(); return () => { if (timerRef.current) clearTimeout(timerRef.current); }; - }, [toast.id, duration, onDismiss]); + }, [startTimer]); return ( -
+
{toast.message}