From 7fd3efb30c4974d3c6d2ec124641117f9ba57223 Mon Sep 17 00:00:00 2001 From: Brandon McAnsh Date: Mon, 11 May 2026 15:01:24 -0400 Subject: [PATCH] fix(cash): race between give transactor dispose and start on quick bg/fg When the user backgrounds and foregrounds quickly during an active give-bill flow, cancelAwaitForGrab() can dispose the transactor between with() and start(), nulling owner before start() reads it. Fix by making the transactor self-protecting: - Reorder dispose() to cancel the scope before nulling fields - Check scope.isActive at the top of start() to bail out if disposed Bugsnag: 6a021d748c3285d1a5987c43 Signed-off-by: Brandon McAnsh --- .../opencode/internal/transactors/GiveBillTransactor.kt | 7 ++++++- .../getcode/opencode/managers/BillTransactionManager.kt | 1 - 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/services/opencode/src/main/kotlin/com/getcode/opencode/internal/transactors/GiveBillTransactor.kt b/services/opencode/src/main/kotlin/com/getcode/opencode/internal/transactors/GiveBillTransactor.kt index 4299980af..575943330 100644 --- a/services/opencode/src/main/kotlin/com/getcode/opencode/internal/transactors/GiveBillTransactor.kt +++ b/services/opencode/src/main/kotlin/com/getcode/opencode/internal/transactors/GiveBillTransactor.kt @@ -21,6 +21,7 @@ import com.getcode.utils.TraceType import com.getcode.utils.trace import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.cancel +import kotlinx.coroutines.isActive import kotlin.time.Duration /** @@ -112,6 +113,10 @@ internal class GiveBillTransactor( * @return the confirmed [TransactionMetadata.SendPublicPayment] on success. */ suspend fun start(): Result { + if (!scope.isActive) { + return logAndFail(GiveTransactorError.Other(message = "Transactor was disposed")) + } + val ownerKey = owner ?: return logAndFail(GiveTransactorError.Other(message = "No owner key. Did you call with() first?")) val desiredToken = token @@ -212,13 +217,13 @@ internal class GiveBillTransactor( /** Cancels the coroutine scope and clears all held state. */ fun dispose() { + scope.cancel() owner = null presentationData = BillPresentationData(emptyList(), emptyList()) rendezvousKey = null receivingAccount = null token = null providedVerifiedState = null - scope.cancel() } sealed class GiveTransactorError( diff --git a/services/opencode/src/main/kotlin/com/getcode/opencode/managers/BillTransactionManager.kt b/services/opencode/src/main/kotlin/com/getcode/opencode/managers/BillTransactionManager.kt index 6f7857117..bae715b00 100644 --- a/services/opencode/src/main/kotlin/com/getcode/opencode/managers/BillTransactionManager.kt +++ b/services/opencode/src/main/kotlin/com/getcode/opencode/managers/BillTransactionManager.kt @@ -4,7 +4,6 @@ import com.getcode.opencode.controllers.AccountController import com.getcode.opencode.controllers.MessagingController import com.getcode.opencode.controllers.TransactionController import com.getcode.opencode.exchange.VerifiedFiatCalculator -import com.getcode.opencode.internal.domain.mapping.MintMapper import com.getcode.opencode.internal.manager.VerifiedState import com.getcode.opencode.internal.transactors.AccountClusterFactory import com.getcode.opencode.internal.transactors.BillPresentationData