diff --git a/services/opencode/src/main/kotlin/com/getcode/opencode/internal/transactors/ReceiveGiftCardTransactor.kt b/services/opencode/src/main/kotlin/com/getcode/opencode/internal/transactors/ReceiveGiftCardTransactor.kt index 533d075dc7..f2ece09f20 100644 --- a/services/opencode/src/main/kotlin/com/getcode/opencode/internal/transactors/ReceiveGiftCardTransactor.kt +++ b/services/opencode/src/main/kotlin/com/getcode/opencode/internal/transactors/ReceiveGiftCardTransactor.kt @@ -135,14 +135,19 @@ internal class ReceiveGiftCardTransactor( Result.success(Unit) } - return@timedTraceSuspend userAccountResult.map { - transactionController.receiveRemotely( - giftCard = giftCard, - amount = amount, - owner = requestingTokenOwner, - mint = token.address - ) - }.fold( + // fold rather than map: receiveRemotely returns its own Result, and + // mapping would nest it where its failure is never read. + return@timedTraceSuspend userAccountResult.fold( + onSuccess = { + transactionController.receiveRemotely( + giftCard = giftCard, + amount = amount, + owner = requestingTokenOwner, + mint = token.address + ) + }, + onFailure = { Result.failure(it) }, + ).fold( onSuccess = { onStep("intent") Result.success(token to amount) diff --git a/services/opencode/src/test/kotlin/com/getcode/opencode/internal/transactors/ReceiveGiftCardTransactorTest.kt b/services/opencode/src/test/kotlin/com/getcode/opencode/internal/transactors/ReceiveGiftCardTransactorTest.kt index 2bf6908c09..416dc32d9e 100644 --- a/services/opencode/src/test/kotlin/com/getcode/opencode/internal/transactors/ReceiveGiftCardTransactorTest.kt +++ b/services/opencode/src/test/kotlin/com/getcode/opencode/internal/transactors/ReceiveGiftCardTransactorTest.kt @@ -5,14 +5,23 @@ import com.getcode.opencode.controllers.TransactionController import com.getcode.opencode.managers.GiftCardManager import com.getcode.opencode.managers.MnemonicManager import com.getcode.opencode.model.accounts.AccountCluster +import com.getcode.opencode.model.accounts.AccountInfo +import com.getcode.opencode.model.accounts.AccountResponse +import com.getcode.opencode.model.core.errors.SubmitIntentError +import com.getcode.opencode.model.financial.DataSource import com.getcode.opencode.model.financial.Token +import com.getcode.opencode.model.financial.TokenResult +import com.getcode.opencode.model.transactions.ExchangeData import com.getcode.opencode.providers.TokenMetadataProvider import com.getcode.solana.keys.Key32 +import com.getcode.solana.keys.Mint +import io.mockk.coEvery import io.mockk.every import io.mockk.mockk import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.runTest import org.junit.Test +import kotlin.test.assertSame import kotlin.test.assertTrue @OptIn(ExperimentalCoroutinesApi::class) @@ -56,11 +65,41 @@ class ReceiveGiftCardTransactorTest { assertTrue(result.isFailure || result.getOrNull()?.isFailure == true) } - // Note: Tests that require `accountController.getAccounts()` to return specific - // Result.success(...) values are blocked by a MockK bug with Result inline class - // returns on suspend functions (https://github.com/mockk/mockk/issues/937). - // Those tests should use fake repositories + real controllers when the project - // establishes that pattern for transactor-level tests. + // endregion + + // region intent submission + + @Test + fun `start fails with the stale state when the gift card was already claimed`() = runTest { + setupWithOwner() + setupClaimableGiftCard() + val alreadyClaimed = SubmitIntentError.StaleState( + listOf("gift card balance has already been claimed") + ) + coEvery { + transactionController.receiveRemotely(any(), any(), any(), any()) + } returns Result.failure(alreadyClaimed) + + val result = transactor.start(claimIfOwned = false) + + assertTrue(result.isFailure) + assertSame(alreadyClaimed, result.exceptionOrNull()) + } + + @Test + fun `start fails when the receive intent is rejected`() = runTest { + setupWithOwner() + setupClaimableGiftCard() + val denied = SubmitIntentError.Denied(listOf("denied")) + coEvery { + transactionController.receiveRemotely(any(), any(), any(), any()) + } returns Result.failure(denied) + + val result = transactor.start(claimIfOwned = false) + + assertTrue(result.isFailure) + assertSame(denied, result.exceptionOrNull()) + } // endregion @@ -93,5 +132,30 @@ class ReceiveGiftCardTransactorTest { transactor.with(owner, "test-entropy") } + private fun setupClaimableGiftCard() { + val mint = Mint("giftMint11111111111111111111111111111111111") + val info = mockk(relaxed = true) { + every { claimState } returns AccountInfo.ClaimState.NotClaimed + every { isGiftCardIssuer } returns false + every { this@mockk.mint } returns mint + every { originalExchangeData } returns ExchangeData.WithRate( + currencyCode = "usd", + exchangeRate = 1.0, + nativeAmount = 5.0, + quarks = 5_000_000, + mint = mint, + ) + } + val token = mockk(relaxed = true) { + every { address } returns mint + } + coEvery { + accountController.getAccounts(any(), any(), any()) + } returns Result.success(AccountResponse(mapOf(Key32.mock to info))) + coEvery { tokenProvider.getTokenMetadata(mint) } returns + Result.success(TokenResult(token, DataSource.Network)) + every { accountController.hasAccountFor(mint) } returns true + } + // endregion }