From 81a1068c3361150f6cadf648bcb0a3498de12c5c Mon Sep 17 00:00:00 2001 From: brightfootlimited-collab Date: Sun, 26 Jul 2026 08:26:00 -0700 Subject: [PATCH] fix(auto-withdraw): eliminate stale closure in settings persistence updateSettings read from the closed-over settings snapshot rather than the current state, meaning a settings change made between renders could be silently overwritten in localStorage when a withdrawal history entry was added shortly after. Fix: - Add settingsRef (useRef) that is kept in sync with state on every write, giving all callbacks a live view of settings without adding them to dependency arrays. - Rewrite updateSettings to use the functional setSettings((prev) => ...) form so it merges from the actual current state, not a stale closure; remove the settings dep from its useCallback array. - ddWithdrawalHistory already used the functional updater correctly; add settingsRef.current = next so the ref stays in sync here too. - Rewrite calculateWithdrawAmount to read from settingsRef.current at call time instead of capturing settings in a closure; drop the [settings] dependency entirely so the callback is stable. Closes #276 --- hooks/use-auto-withdraw.ts | 36 +++++++++++++++++++++++++----------- 1 file changed, 25 insertions(+), 11 deletions(-) diff --git a/hooks/use-auto-withdraw.ts b/hooks/use-auto-withdraw.ts index 5cf8d13..6a5ced0 100644 --- a/hooks/use-auto-withdraw.ts +++ b/hooks/use-auto-withdraw.ts @@ -58,22 +58,32 @@ export function useAutoWithdraw(stream: StreamData | null) { const { network } = useNetwork(); const [settings, setSettings] = useState(DEFAULT_SETTINGS); + // Ref that always mirrors the latest settings so callbacks that need to + // persist to localStorage never read a stale closure value. + const settingsRef = useRef(DEFAULT_SETTINGS); const [lastAutoWithdraw, setLastAutoWithdraw] = useState(null); const [autoWithdrawPending, setAutoWithdrawPending] = useState(false); const intervalRef = useRef | null>(null); useEffect(() => { - if (stream) setSettings(loadSettings(stream.id)); + if (stream) { + const loaded = loadSettings(stream.id); + settingsRef.current = loaded; + setSettings(loaded); + } }, [stream?.id]); const updateSettings = useCallback( (update: Partial) => { if (!stream) return; - const next = { ...settings, ...update }; - setSettings(next); - saveSettings(stream.id, next); + setSettings((prev) => { + const next = { ...prev, ...update }; + settingsRef.current = next; + saveSettings(stream.id, next); + return next; + }); }, - [stream, settings], + [stream], ); const addWithdrawalHistory = useCallback( @@ -84,6 +94,7 @@ export function useAutoWithdraw(stream: StreamData | null) { ...prev, withdrawalHistory: [entry, ...prev.withdrawalHistory.slice(0, 99)], } as AutoWithdrawSettings; + settingsRef.current = next; saveSettings(stream.id, next); return next; }); @@ -93,25 +104,27 @@ export function useAutoWithdraw(stream: StreamData | null) { const calculateWithdrawAmount = useCallback( (withdrawable: bigint, stream: StreamData): bigint => { - const minAmount = BigInt(settings.minAmountRaw || "0"); - const maxLimit = BigInt(settings.maxSafetyLimitRaw || "0"); + // Always read from the ref so this never uses a stale closure snapshot. + const s = settingsRef.current; + const minAmount = BigInt(s.minAmountRaw || "0"); + const maxLimit = BigInt(s.maxSafetyLimitRaw || "0"); if (withdrawable <= 0n) return 0n; if (minAmount > 0n && withdrawable < minAmount) return 0n; let amount = withdrawable; - switch (settings.strategy) { + switch (s.strategy) { case "threshold-based": { const threshold = - (stream.depositedAmount * BigInt(settings.thresholdPercentage)) / + (stream.depositedAmount * BigInt(s.thresholdPercentage)) / 100n; if (withdrawable < threshold) return 0n; amount = withdrawable; break; } case "gas-optimized": { - const lastWithdraw = settings.withdrawalHistory[0]; + const lastWithdraw = s.withdrawalHistory[0]; const daysSinceLastWithdraw = lastWithdraw ? (Date.now() - lastWithdraw.timestamp) / (1000 * 60 * 60 * 24) : Infinity; @@ -136,7 +149,8 @@ export function useAutoWithdraw(stream: StreamData | null) { return amount; }, - [settings], + // No settings dependency — reads live value through ref instead. + [], ); useEffect(() => {