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 4456b720b..8852fbeac 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 @@ -144,7 +144,7 @@ internal class GiveBillTransactor( val freshExchange = freshState.exchangeDataFor( amount = sendingAmount, mint = desiredToken.address, - billExchangeDataTimeout = exchangeDataTimeout + billExchangeDataTimeout = null // relaxed — we just fetched this rate ) ?: return logAndFail(GiveTransactorError.ExchangeRateExpiredException()) freshState to freshExchange diff --git a/services/opencode/src/test/kotlin/com/getcode/opencode/internal/transactors/GiveBillTransactorTest.kt b/services/opencode/src/test/kotlin/com/getcode/opencode/internal/transactors/GiveBillTransactorTest.kt index b7ad77fd4..2f5953f4f 100644 --- a/services/opencode/src/test/kotlin/com/getcode/opencode/internal/transactors/GiveBillTransactorTest.kt +++ b/services/opencode/src/test/kotlin/com/getcode/opencode/internal/transactors/GiveBillTransactorTest.kt @@ -19,6 +19,7 @@ import io.mockk.every import io.mockk.mockk import io.mockk.mockkStatic import io.mockk.unmockkStatic +import io.mockk.verify import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.UnconfinedTestDispatcher @@ -29,6 +30,8 @@ import kotlin.test.assertEquals import kotlin.test.assertIs import kotlin.test.assertNotEquals import kotlin.test.assertTrue +import kotlin.time.Duration +import kotlin.time.Duration.Companion.seconds @OptIn(ExperimentalCoroutinesApi::class) class GiveBillTransactorTest { @@ -134,6 +137,45 @@ class GiveBillTransactorTest { unmockkStatic("com.getcode.opencode.internal.extensions.VerifiedStateKt") } + @Test + fun `retry uses relaxed timeout for freshly resolved exchange data`() = runTest { + val transactor = createTransactor(this) + val staleState = mockk(relaxed = true) + // Set a strict 30s timeout — mirrors the server-configured value + setupWith(transactor, verifiedState = staleState, billExchangeDataTimeout = 30.seconds) + + mockkStatic("com.getcode.opencode.internal.extensions.VerifiedStateKt") + // Initial: rate expired with strict 30s timeout + every { staleState.exchangeDataFor(any(), any(), any()) } returns null + + // Fresh state: valid exchange data available + val freshState = mockk(relaxed = true) + every { freshState.exchangeDataFor(any(), any(), any()) } returns mockk(relaxed = true) + coEvery { + verifiedFiatCalculator.resolveVerifiedState(any(), any()) + } returns freshState + + coEvery { + messagingController.sendRequestToGiveBill(any(), any(), any()) + } returns Result.success(mockk(relaxed = true)) + + coEvery { + messagingController.awaitRequestToGrabBill(any(), any()) + } returns null + + val result = transactor.start() + + // Should proceed past exchange resolution and fail at grab — NOT ExchangeRateExpiredException + assertTrue(result.isFailure) + assertIs(result.exceptionOrNull()) + + // Verify the retry path called exchangeDataFor with null (relaxed) timeout, + // NOT the strict 30s billExchangeDataTimeout + verify { freshState.exchangeDataFor(any(), any(), isNull()) } + + unmockkStatic("com.getcode.opencode.internal.extensions.VerifiedStateKt") + } + @Test fun `start fails when send give bill fails`() = runTest { val transactor = createTransactor(this) @@ -268,6 +310,7 @@ class GiveBillTransactorTest { transactor: GiveBillTransactor, verifiedState: VerifiedState? = null, nonce: List? = null, + billExchangeDataTimeout: Duration? = null, ) { val token = mockk(relaxed = true) { every { address } returns Mint.usdf @@ -288,7 +331,7 @@ class GiveBillTransactorTest { token = token, amount = amount, owner = owner, - billExchangeDataTimeout = null, + billExchangeDataTimeout = billExchangeDataTimeout, verifiedState = verifiedState, providedNonce = nonce, )