From 07bca4466baa963ca48966c0557c3c6675d01ee9 Mon Sep 17 00:00:00 2001 From: Good-Coded Date: Mon, 1 Jun 2026 07:07:04 +0000 Subject: [PATCH] fix: resolve issues #685 #686 #687 #688 - #685 Toast.tsx: add type-based auto-dismiss defaults (error=5s, success=3s) and pause-on-hover with remaining-time tracking - #686 DisputeResolution.tsx: capture tx_hash from resolve response and display it with a Stellar Expert explorer link - #687 sdk/src/test-utils.ts: extract makeProposalScVal into shared test utilities file and export it; update governance.test.ts import - #688 settlements.ts: validate sender/agent are valid Stellar addresses (G... 56 chars) before processing net settlements --- api/src/routes/settlements.ts | 13 +++++++ frontend/src/components/DisputeResolution.tsx | 20 ++++++++++ frontend/src/components/Toast.tsx | 39 ++++++++++++++++--- sdk/src/governance.test.ts | 19 +-------- sdk/src/test-utils.ts | 16 ++++++++ 5 files changed, 83 insertions(+), 24 deletions(-) create mode 100644 sdk/src/test-utils.ts 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 9cff8172..45b82d99 100644 --- a/frontend/src/components/DisputeResolution.tsx +++ b/frontend/src/components/DisputeResolution.tsx @@ -72,6 +72,7 @@ export default function DisputeResolution() { const [auditLog, setAuditLog] = useState([]); const [resolving, setResolving] = useState(null); const [confirmOpen, setConfirmOpen] = useState(null); + const [resolvedTxHash, setResolvedTxHash] = useState(null); useEffect(() => { void fetchDisputes(); @@ -122,6 +123,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', @@ -129,6 +131,9 @@ export default function DisputeResolution() { body: JSON.stringify({ in_favour_of_sender: inFavourOfSender }), }); if (!res.ok) throw new Error(`HTTP ${res.status}`); + 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) { @@ -144,6 +149,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}