Skip to content

Commit fa408af

Browse files
authored
fix(cash): use relaxed timeout on exchange rate retry in GiveBillTransactor (#899)
The retry path in start() resolved a fresh VerifiedState but still applied the strict billExchangeDataTimeout (30s) to it. When the server's rate timestamp was already >30s old (common after backgrounding), the freshly-fetched rate also failed the check. Now the retry passes null for billExchangeDataTimeout, falling back to the default 15-minute window. The initial check still uses the strict timeout — the relaxation only applies after we've just fetched fresh data via resolveVerifiedState. Closes: Bugsnag 6a1bd94017b0f7c09452ffd7 Signed-off-by: Brandon McAnsh <git@bmcreations.dev>
1 parent a04ac22 commit fa408af

2 files changed

Lines changed: 45 additions & 2 deletions

File tree

services/opencode/src/main/kotlin/com/getcode/opencode/internal/transactors/GiveBillTransactor.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -144,7 +144,7 @@ internal class GiveBillTransactor(
144144
val freshExchange = freshState.exchangeDataFor(
145145
amount = sendingAmount,
146146
mint = desiredToken.address,
147-
billExchangeDataTimeout = exchangeDataTimeout
147+
billExchangeDataTimeout = null // relaxed — we just fetched this rate
148148
) ?: return logAndFail(GiveTransactorError.ExchangeRateExpiredException())
149149

150150
freshState to freshExchange

services/opencode/src/test/kotlin/com/getcode/opencode/internal/transactors/GiveBillTransactorTest.kt

Lines changed: 44 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import io.mockk.every
1919
import io.mockk.mockk
2020
import io.mockk.mockkStatic
2121
import io.mockk.unmockkStatic
22+
import io.mockk.verify
2223
import kotlinx.coroutines.ExperimentalCoroutinesApi
2324
import kotlinx.coroutines.test.TestScope
2425
import kotlinx.coroutines.test.UnconfinedTestDispatcher
@@ -29,6 +30,8 @@ import kotlin.test.assertEquals
2930
import kotlin.test.assertIs
3031
import kotlin.test.assertNotEquals
3132
import kotlin.test.assertTrue
33+
import kotlin.time.Duration
34+
import kotlin.time.Duration.Companion.seconds
3235

3336
@OptIn(ExperimentalCoroutinesApi::class)
3437
class GiveBillTransactorTest {
@@ -134,6 +137,45 @@ class GiveBillTransactorTest {
134137
unmockkStatic("com.getcode.opencode.internal.extensions.VerifiedStateKt")
135138
}
136139

140+
@Test
141+
fun `retry uses relaxed timeout for freshly resolved exchange data`() = runTest {
142+
val transactor = createTransactor(this)
143+
val staleState = mockk<VerifiedState>(relaxed = true)
144+
// Set a strict 30s timeout — mirrors the server-configured value
145+
setupWith(transactor, verifiedState = staleState, billExchangeDataTimeout = 30.seconds)
146+
147+
mockkStatic("com.getcode.opencode.internal.extensions.VerifiedStateKt")
148+
// Initial: rate expired with strict 30s timeout
149+
every { staleState.exchangeDataFor(any<LocalFiat>(), any<Mint>(), any()) } returns null
150+
151+
// Fresh state: valid exchange data available
152+
val freshState = mockk<VerifiedState>(relaxed = true)
153+
every { freshState.exchangeDataFor(any<LocalFiat>(), any<Mint>(), any()) } returns mockk<ExchangeData.Verified>(relaxed = true)
154+
coEvery {
155+
verifiedFiatCalculator.resolveVerifiedState(any<CurrencyCode>(), any<Mint>())
156+
} returns freshState
157+
158+
coEvery {
159+
messagingController.sendRequestToGiveBill(any(), any(), any())
160+
} returns Result.success(mockk(relaxed = true))
161+
162+
coEvery {
163+
messagingController.awaitRequestToGrabBill(any(), any())
164+
} returns null
165+
166+
val result = transactor.start()
167+
168+
// Should proceed past exchange resolution and fail at grab — NOT ExchangeRateExpiredException
169+
assertTrue(result.isFailure)
170+
assertIs<GiveBillTransactor.GiveTransactorError.NoGrabReceived>(result.exceptionOrNull())
171+
172+
// Verify the retry path called exchangeDataFor with null (relaxed) timeout,
173+
// NOT the strict 30s billExchangeDataTimeout
174+
verify { freshState.exchangeDataFor(any<LocalFiat>(), any<Mint>(), isNull()) }
175+
176+
unmockkStatic("com.getcode.opencode.internal.extensions.VerifiedStateKt")
177+
}
178+
137179
@Test
138180
fun `start fails when send give bill fails`() = runTest {
139181
val transactor = createTransactor(this)
@@ -268,6 +310,7 @@ class GiveBillTransactorTest {
268310
transactor: GiveBillTransactor,
269311
verifiedState: VerifiedState? = null,
270312
nonce: List<Byte>? = null,
313+
billExchangeDataTimeout: Duration? = null,
271314
) {
272315
val token = mockk<Token>(relaxed = true) {
273316
every { address } returns Mint.usdf
@@ -288,7 +331,7 @@ class GiveBillTransactorTest {
288331
token = token,
289332
amount = amount,
290333
owner = owner,
291-
billExchangeDataTimeout = null,
334+
billExchangeDataTimeout = billExchangeDataTimeout,
292335
verifiedState = verifiedState,
293336
providedNonce = nonce,
294337
)

0 commit comments

Comments
 (0)