From 5995fc6040c91771c453f9080e22284b2cedd89a Mon Sep 17 00:00:00 2001 From: Andrew Camilleri Date: Thu, 23 Apr 2026 10:26:27 +0200 Subject: [PATCH 1/2] fix: prevent swap autoclaim race by querying service worker state Use SwapManager.isProcessing() to check the service worker's actual swapsInProgress state before allowing a manual claim from the UI. The button reactively shows "Claiming..." and disables when the SwapManager is already processing the swap (auto-claim, autonomous action, etc). --- package.json | 2 +- src/screens/Apps/Boltz/Swap.tsx | 19 ++++++++++++++++--- 2 files changed, 17 insertions(+), 4 deletions(-) diff --git a/package.json b/package.json index 72afbd58f..2e9067b90 100644 --- a/package.json +++ b/package.json @@ -42,7 +42,7 @@ "test:e2e": "VITE_NOSTR_RELAY_URL=http://localhost:10547 playwright test", "test:e2e:ui": "VITE_NOSTR_RELAY_URL=http://localhost:10547 playwright test --ui", "test:codegen": "playwright codegen localhost:3002", - "lint": "eslint 'src/**/*.{ts,tsx,js,jsx}'", + "lint": "eslint \"src/**/*.{ts,tsx,js,jsx}\"", "lint:fix": "eslint 'src/**/*.{ts,tsx,js,jsx}' --fix", "format": "prettier --write src", "format:check": "prettier --check src", diff --git a/src/screens/Apps/Boltz/Swap.tsx b/src/screens/Apps/Boltz/Swap.tsx index 4597fbd5c..d06f72ddb 100644 --- a/src/screens/Apps/Boltz/Swap.tsx +++ b/src/screens/Apps/Boltz/Swap.tsx @@ -51,6 +51,7 @@ export default function AppBoltzSwap() { const [processing, setProcessing] = useState(false) const [opDone, setOpDone] = useState(false) const [success, setSuccess] = useState(false) + const [swapManagerProcessing, setSwapManagerProcessing] = useState(false) // Subscribe to real-time updates for this swap useEffect(() => { @@ -58,9 +59,17 @@ export default function AppBoltzSwap() { let unsub: (() => void) | null = null let cancelled = false + + swapManager.isProcessing(swapInfo.id).then((p) => { + if (!cancelled) setSwapManagerProcessing(p) + }) + swapManager .subscribeToSwapUpdates(swapInfo.id, (updatedSwap) => { setSwapInfo(updatedSwap) + swapManager.isProcessing(updatedSwap.id).then((p) => { + if (!cancelled) setSwapManagerProcessing(p) + }) }) .then((unsubscribe) => { if (cancelled) { @@ -147,11 +156,15 @@ export default function AppBoltzSwap() { const isRefundable = isSubmarineSwapRefundable(swapInfo) || isChainSwapRefundable(swapInfo) const isClaimable = isReverseSwapClaimable(swapInfo) || isChainSwapClaimable(swapInfo) - const buttonLabel = isClaimable ? 'Complete swap' : 'Refund swap' + const buttonLabel = swapManagerProcessing ? 'Claiming...' : isClaimable ? 'Complete swap' : 'Refund swap' const refunded = swapInfo.status === 'transaction.refunded' const buttonHandler = async () => { try { + if (swapManager) { + const alreadyProcessing = await swapManager.isProcessing(swapInfo.id) + if (alreadyProcessing) return + } setProcessing(true) if (isReverseSwapClaimable(swapInfo)) { await claimVHTLC(swapInfo) @@ -217,9 +230,9 @@ export default function AppBoltzSwap() { )} - {!success && (isRefundable || isClaimable) ? ( + {!success && (isRefundable || isClaimable || swapManagerProcessing) ? ( -