Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,8 @@ import kotlinx.coroutines.launch
import java.util.concurrent.atomic.AtomicInteger
import javax.inject.Inject
import javax.inject.Singleton
import kotlin.reflect.KClass
import kotlin.reflect.safeCast
import kotlin.time.Clock
import kotlin.time.Duration

Expand Down Expand Up @@ -565,6 +567,7 @@ class TransactionController @Inject constructor(
): Result<TransactionMetadata> = repository.getIntentMetadata(intentId, owner)

suspend fun <T : TransactionMetadata> pollIntentMetadata(
type: KClass<T>,
intentId: PublicKey,
owner: KeyPair,
maxAttempts: Int = 10,
Expand Down Expand Up @@ -612,14 +615,18 @@ class TransactionController @Inject constructor(
type = TraceType.Process
)
}
metadata
type.safeCast(metadata)
?: throw GetIntentMetadataError.UnexpectedType(
expected = type,
actual = metadata::class,
)
},
onFailure = { error ->
if (error is GetIntentMetadataError.Denied) throw error
null
}
)
}.mapNotNull { it as? T }
}
.map { Result.success(it) }
.catch { emit(Result.failure(it)) }
.firstOrNull()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -219,6 +219,7 @@ internal class GiveBillTransactor(
).fold(
onSuccess = {
transactionController.pollIntentMetadata(
type = TransactionMetadata.SendPublicPayment::class,
owner = sendingVault.authority.keyPair,
intentId = it.id
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@ internal class GrabBillTransactor(
private suspend fun handleMultiMintScan(
ownerKey: AccountCluster,
data: OpenCodePayload
): Result<TransactionMetadata.SendPublicPayment> = timedTraceSuspend(
): Result<TransactionMetadata.PublicPayment> = timedTraceSuspend(
message = "handleMultiMintScan",
tag = tag,
) { onStep ->
Expand Down Expand Up @@ -157,8 +157,15 @@ internal class GrabBillTransactor(
onStep("createUserAccount (needed=$needsAccount)")

// 4. Send grab request and wait for confirmation
val result = requestGrab<TransactionMetadata.SendPublicPayment>(tokenizedCluster, data)
.map { it.copy(verifiedExchangeData = exchangeData) }
// The server may report the grab as either side of the payment (iOS
// accepts both too), so apply the give request's exchange data to whichever arrives.
val result = requestGrab(tokenizedCluster, data)
.map { metadata ->
when (metadata) {
is TransactionMetadata.SendPublicPayment -> metadata.copy(verifiedExchangeData = exchangeData)
is TransactionMetadata.ReceivePublicPayment -> metadata.copy(verifiedExchangeData = exchangeData)
}
}
.onSuccess {
// 5. Ack the receipt of the give request to clear it from the stream
messagingController.ackMessages(data.rendezvous, listOf(messageId))
Expand All @@ -172,17 +179,18 @@ internal class GrabBillTransactor(
return logAndFail(error)
}

private suspend fun <T : TransactionMetadata.PublicPayment> requestGrab(
private suspend fun requestGrab(
owner: AccountCluster,
data: OpenCodePayload
): Result<T> {
): Result<TransactionMetadata.PublicPayment> {
return messagingController.sendRequestToGrabBill(
destination = owner.vaultPublicKey,
payload = data
).fold(
onSuccess = {
// 5. Wait for confirmation
transactionController.pollIntentMetadata(
type = TransactionMetadata.PublicPayment::class,
owner = owner.authority.keyPair,
intentId = data.rendezvous.toPublicKey(),
debugLogs = true
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,11 @@ import com.getcode.opencode.model.core.errors.SubmitIntentError.Signature
import com.getcode.opencode.model.core.errors.SubmitIntentError.StaleState
import com.getcode.opencode.model.core.errors.SubmitIntentError.Unrecognized
import com.getcode.opencode.model.transactions.SwapState
import com.getcode.opencode.model.transactions.TransactionMetadata
import com.getcode.utils.CodeServerError
import com.getcode.utils.ConditionallyNotifiable
import com.getcode.utils.NotifiableError
import kotlin.reflect.KClass

sealed class CodeAccountCheckError(
override val message: String? = null,
Expand Down Expand Up @@ -225,10 +227,25 @@ sealed class GetIntentMetadataError(
class NotFound : GetIntentMetadataError("Not found")
class Denied : GetIntentMetadataError("Denied")
class Timeout : GetIntentMetadataError("Never received the desired metadata")
class UnexpectedType(
expected: KClass<out TransactionMetadata>,
actual: KClass<out TransactionMetadata>,
) : GetIntentMetadataError("Expected ${expected.label()} metadata but received ${actual.label()}"), NotifiableError
class Unrecognized : GetIntentMetadataError("Unrecognized"), NotifiableError
data class Other(override val cause: Throwable? = null) : GetIntentMetadataError(message = cause?.message, cause = cause), NotifiableError
}

// R8 renames these classes in release builds, so the error message spells them out.
private fun KClass<out TransactionMetadata>.label(): String = when (this) {
TransactionMetadata.OpenAccount::class -> "OpenAccount"
TransactionMetadata.PublicPayment::class -> "PublicPayment"
TransactionMetadata.SendPublicPayment::class -> "SendPublicPayment"
TransactionMetadata.ReceivePublicPayment::class -> "ReceivePublicPayment"
TransactionMetadata.PublicDistribution::class -> "PublicDistribution"
TransactionMetadata.Unknown::class -> "Unknown"
else -> "TransactionMetadata"
}

sealed class GetLimitsError(
override val message: String? = null,
override val cause: Throwable? = null
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
package com.getcode.opencode.controllers

import com.getcode.opencode.model.core.errors.GetIntentMetadataError
import com.getcode.opencode.model.transactions.TransactionMetadata
import com.getcode.opencode.repositories.TransactionRepository
import io.mockk.coEvery
import io.mockk.coVerify
import io.mockk.mockk
import kotlinx.coroutines.test.runTest
import org.junit.Test
import kotlin.test.assertEquals
import kotlin.test.assertIs
import kotlin.test.assertSame

class TransactionControllerPollIntentMetadataTest {

private val repository = mockk<TransactionRepository>(relaxed = true)
private val controller = TransactionController(
repository = repository,
swapRepository = mockk(relaxed = true),
accountController = mockk(relaxed = true),
)

private val received = TransactionMetadata.ReceivePublicPayment(
source = mockk(relaxed = true),
quarks = 1,
isIndirect = false,
exchangeData = mockk(relaxed = true),
mint = mockk(relaxed = true),
)

@Test
fun `returns metadata that matches the requested type`() = runTest {
coEvery { repository.getIntentMetadata(any(), any()) } returns Result.success(received)

val result = controller.pollIntentMetadata(
type = TransactionMetadata.PublicPayment::class,
intentId = mockk(relaxed = true),
owner = mockk(relaxed = true),
)

assertSame(received, result.getOrThrow())
}

@Test
fun `fails fast with UnexpectedType when metadata is a different type`() = runTest {
coEvery { repository.getIntentMetadata(any(), any()) } returns Result.success(received)

val result = controller.pollIntentMetadata(
type = TransactionMetadata.SendPublicPayment::class,
intentId = mockk(relaxed = true),
owner = mockk(relaxed = true),
)

val error = assertIs<GetIntentMetadataError.UnexpectedType>(result.exceptionOrNull())
assertEquals("Expected SendPublicPayment metadata but received ReceivePublicPayment", error.message)
coVerify(exactly = 1) { repository.getIntentMetadata(any(), any()) }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,9 @@ import com.getcode.opencode.model.core.OpenCodePayload
import com.getcode.opencode.model.core.PayloadKind
import com.getcode.opencode.model.core.errors.SubmitIntentError
import com.getcode.opencode.model.financial.Token
import com.getcode.opencode.model.transactions.ExchangeData
import com.getcode.opencode.model.transactions.GiveRequest
import com.getcode.opencode.model.transactions.TransactionMetadata
import com.getcode.opencode.providers.TokenMetadataProvider
import com.getcode.solana.keys.Key32
import com.getcode.solana.keys.Mint
Expand All @@ -21,6 +23,8 @@ import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.UnconfinedTestDispatcher
import kotlinx.coroutines.test.runTest
import org.junit.Test
import kotlin.test.assertIs
import kotlin.test.assertSame
import kotlin.test.assertTrue

@OptIn(ExperimentalCoroutinesApi::class)
Expand Down Expand Up @@ -161,6 +165,50 @@ class GrabBillTransactorTest {
}
}

@Test
fun `MultiMintCash succeeds when the server reports the grab as a receive payment`() = runTest {
val transactor = createTransactor(this)
setupWithMultiMint(transactor)

val mint = Mint("nonCoreMint11111111111111111111111111111111")
val token = mockk<Token>(relaxed = true) {
every { address } returns mint
}
val giveExchangeData = mockk<ExchangeData.Verified>(relaxed = true)
val giveRequest = GiveRequest(
messageId = Key32.mock,
mint = mint,
exchangeData = giveExchangeData,
tokenMetadata = token
)
val received = TransactionMetadata.ReceivePublicPayment(
source = Key32.mock,
quarks = 639_538_459,
isIndirect = true,
exchangeData = mockk(relaxed = true),
mint = mint,
)

coEvery { messagingController.pollForGiveRequest(any()) } returns Result.success(giveRequest)
coEvery { accountController.hasAccountFor(mint) } returns true
coEvery { messagingController.sendRequestToGrabBill(any(), any()) } returns Result.success(Key32.mock)
coEvery {
transactionController.pollIntentMetadata(
type = TransactionMetadata.PublicPayment::class,
intentId = any(),
owner = any(),
maxAttempts = any(),
debugLogs = any(),
)
} returns Result.success(received)

val result = transactor.start()

val metadata = result.getOrThrow()
assertIs<TransactionMetadata.ReceivePublicPayment>(metadata)
assertSame(giveExchangeData, metadata.verifiedExchangeData)
}

// endregion

// region dispose
Expand Down
Loading