From 7c1f895fb32129694984b15dd39ead0438321692 Mon Sep 17 00:00:00 2001 From: Brandon McAnsh Date: Mon, 11 May 2026 17:07:49 -0400 Subject: [PATCH 1/2] chore(ocp): reset orderId for coinbase onramp back to raw string Signed-off-by: Brandon McAnsh --- .../opencode/internal/network/extensions/ProtobufToLocal.kt | 4 ++-- .../getcode/opencode/model/transactions/SwapFundingSource.kt | 4 +--- .../com/getcode/opencode/model/transactions/SwapRequest.kt | 2 +- 3 files changed, 4 insertions(+), 6 deletions(-) diff --git a/services/opencode/src/main/kotlin/com/getcode/opencode/internal/network/extensions/ProtobufToLocal.kt b/services/opencode/src/main/kotlin/com/getcode/opencode/internal/network/extensions/ProtobufToLocal.kt index faf6479a4..40c049169 100644 --- a/services/opencode/src/main/kotlin/com/getcode/opencode/internal/network/extensions/ProtobufToLocal.kt +++ b/services/opencode/src/main/kotlin/com/getcode/opencode/internal/network/extensions/ProtobufToLocal.kt @@ -202,7 +202,7 @@ internal fun TransactionService.StatefulSwapRequest.Initiate.ReserveSwapClientPa TransactionService.FundingSource.FUNDING_SOURCE_SUBMIT_INTENT -> SwapFundingSource.SubmitIntent(PublicKey(fundingId).bytes) TransactionService.FundingSource.FUNDING_SOURCE_EXTERNAL_WALLET -> SwapFundingSource.ExternalWallet( fundingId.toByteArray().toList()) - TransactionService.FundingSource.FUNDING_SOURCE_COINBASE_ONRAMP -> SwapFundingSource.CoinbaseOnramp(fundingId.toByteArray().toList()) + TransactionService.FundingSource.FUNDING_SOURCE_COINBASE_ONRAMP -> SwapFundingSource.CoinbaseOnramp(fundingId) TransactionService.FundingSource.UNRECOGNIZED -> SwapFundingSource.Unknown } ) @@ -220,7 +220,7 @@ internal fun TransactionService.StatefulSwapRequest.Initiate.CoinbaseStableSwapp TransactionService.FundingSource.FUNDING_SOURCE_SUBMIT_INTENT -> SwapFundingSource.SubmitIntent(PublicKey(fundingId).bytes) TransactionService.FundingSource.FUNDING_SOURCE_EXTERNAL_WALLET -> SwapFundingSource.ExternalWallet( fundingId.toByteArray().toList()) - TransactionService.FundingSource.FUNDING_SOURCE_COINBASE_ONRAMP -> SwapFundingSource.CoinbaseOnramp(fundingId.toByteArray().toList()) + TransactionService.FundingSource.FUNDING_SOURCE_COINBASE_ONRAMP -> SwapFundingSource.CoinbaseOnramp(fundingId) TransactionService.FundingSource.UNRECOGNIZED -> SwapFundingSource.Unknown }, destinationOwner = destinationOwner.toPublicKey(), diff --git a/services/opencode/src/main/kotlin/com/getcode/opencode/model/transactions/SwapFundingSource.kt b/services/opencode/src/main/kotlin/com/getcode/opencode/model/transactions/SwapFundingSource.kt index e356d2279..98aed3e78 100644 --- a/services/opencode/src/main/kotlin/com/getcode/opencode/model/transactions/SwapFundingSource.kt +++ b/services/opencode/src/main/kotlin/com/getcode/opencode/model/transactions/SwapFundingSource.kt @@ -25,7 +25,5 @@ sealed class SwapFundingSource { * Represents a funding source where the user pays via a Coinbase onramp. * @param orderId The Coinbase onramp order ID. */ - data class CoinbaseOnramp(val orderId: List): SwapFundingSource() { - constructor(orderId: String): this(orderId.toByteArray().toList()) - } + data class CoinbaseOnramp(val orderId: String): SwapFundingSource() } \ No newline at end of file diff --git a/services/opencode/src/main/kotlin/com/getcode/opencode/model/transactions/SwapRequest.kt b/services/opencode/src/main/kotlin/com/getcode/opencode/model/transactions/SwapRequest.kt index 38fdc0c9d..0a6be9878 100644 --- a/services/opencode/src/main/kotlin/com/getcode/opencode/model/transactions/SwapRequest.kt +++ b/services/opencode/src/main/kotlin/com/getcode/opencode/model/transactions/SwapRequest.kt @@ -52,7 +52,7 @@ sealed interface SwapStartKind { get() = when (fundingSource) { is SwapFundingSource.ExternalWallet -> fundingSource.transactionSignature is SwapFundingSource.SubmitIntent -> fundingSource.id - is SwapFundingSource.CoinbaseOnramp -> fundingSource.orderId + is SwapFundingSource.CoinbaseOnramp -> fundingSource.orderId.toByteArray().toList() SwapFundingSource.Unknown -> throw IllegalArgumentException("Invalid funding source") } } From 2540b7be079c0842027637e46a6d8acb0ffedb5e Mon Sep 17 00:00:00 2001 From: Brandon McAnsh Date: Tue, 12 May 2026 09:44:57 -0400 Subject: [PATCH 2/2] feat(onramp): notify server of swap before Google Pay is presented Move the buy() call from after payment success to before Google Pay is shown, so the server can handle order polling and settlement. Remove the Processing intermediate state and make SwapRequest.fundingIntentId lazy to avoid eager evaluation for CoinbaseOnramp sources. Signed-off-by: Brandon McAnsh --- .../app/onramp/internal/OnRampViewModel.kt | 1 - .../app/onramp/CoinbaseOnRampController.kt | 62 ++++--------------- .../app/onramp/CoinbaseOnRampHandler.kt | 13 ---- .../app/onramp/CoinbaseOnRampState.kt | 3 +- .../model/transactions/SwapRequest.kt | 13 ++-- 5 files changed, 20 insertions(+), 72 deletions(-) diff --git a/apps/flipcash/features/onramp/src/main/kotlin/com/flipcash/app/onramp/internal/OnRampViewModel.kt b/apps/flipcash/features/onramp/src/main/kotlin/com/flipcash/app/onramp/internal/OnRampViewModel.kt index 6d3d18863..d3132ad3f 100644 --- a/apps/flipcash/features/onramp/src/main/kotlin/com/flipcash/app/onramp/internal/OnRampViewModel.kt +++ b/apps/flipcash/features/onramp/src/main/kotlin/com/flipcash/app/onramp/internal/OnRampViewModel.kt @@ -176,7 +176,6 @@ internal class OnRampViewModel @Inject constructor( dispatchEvent(Event.UpdateOrderLookupState()) } is CoinbaseOnRampState.Paying -> dispatchEvent(Event.UpdateOrderLookupState(loading = true)) - is CoinbaseOnRampState.Processing -> dispatchEvent(Event.UpdateOrderLookupState(loading = true)) } } .launchIn(viewModelScope) diff --git a/apps/flipcash/shared/onramp/coinbase/src/main/kotlin/com/flipcash/app/onramp/CoinbaseOnRampController.kt b/apps/flipcash/shared/onramp/coinbase/src/main/kotlin/com/flipcash/app/onramp/CoinbaseOnRampController.kt index a6095cad2..bfe128b0d 100644 --- a/apps/flipcash/shared/onramp/coinbase/src/main/kotlin/com/flipcash/app/onramp/CoinbaseOnRampController.kt +++ b/apps/flipcash/shared/onramp/coinbase/src/main/kotlin/com/flipcash/app/onramp/CoinbaseOnRampController.kt @@ -25,8 +25,6 @@ import com.getcode.opencode.model.financial.Token import com.getcode.opencode.model.financial.usdf import com.getcode.opencode.model.transactions.SwapFundingSource import com.getcode.solana.keys.base58 -import com.getcode.utils.network.pollUntil -import com.getcode.vendor.Base58 import com.flipcash.app.core.AppRoute import com.flipcash.app.onramp.internal.CoinbaseOnRampWebError import com.getcode.utils.CodeServerError @@ -46,9 +44,7 @@ import kotlinx.serialization.Serializable import kotlinx.serialization.json.Json import kotlinx.serialization.json.JsonIgnoreUnknownKeys import retrofit2.HttpException -import java.security.SecureRandom import javax.inject.Inject -import kotlin.time.Duration.Companion.seconds typealias OrderWithPaymentLink = Pair @@ -79,15 +75,15 @@ class CoinbaseOnRampController @Inject constructor( _pendingNavigation.tryEmit(route) } - fun startPayment(order: OnrampOrder, token: Token, amount: VerifiedFiat) { - _state.value = CoinbaseOnRampState.Paying(order, token, amount) + fun startPayment(order: OnrampOrder, token: Token, amount: VerifiedFiat, swapId: SwapId) { + _state.value = CoinbaseOnRampState.Paying(order, token, amount, swapId) } fun onPaymentSuccess(orderId: String) { val current = _state.value if (current is CoinbaseOnRampState.Paying) { _state.update { - CoinbaseOnRampState.Processing(orderId, current.token, current.amount) + CoinbaseOnRampState.Completed(current.swapId, current.token, current.amount) } } } @@ -118,56 +114,20 @@ class CoinbaseOnRampController @Inject constructor( } return placeOrderInclusiveOfFees(amount) - .map { (orderId, paymentLink) -> - val order = OnrampOrder(orderId, paymentLink.url) - startPayment(order, token, verifiedFiat) - } - } - - suspend fun processPayment(): Result { - val current = _state.value - if (current !is CoinbaseOnRampState.Processing) { - return Result.failure(IllegalStateException("Not in Processing state")) - } - - return pollUntil( - call = { lookupOrder(current.orderId).getOrThrow() }, - required = { order -> order.txHash != null }, - maxAttempts = 100, - interval = 3.seconds, - tag = "CoinbaseOrderPoller", - ).mapCatching { order -> - order.txHash ?: throw IllegalStateException("No hash provided from provider") - }.mapCatching { txHash -> + .mapCatching { (orderId, paymentLink) -> val owner = userManager.accountCluster ?: throw IllegalStateException("No account cluster") - transactionController.buy( + val swapId = transactionController.buy( owner = owner, - amount = current.amount, - of = current.token, - source = SwapFundingSource.ExternalWallet( - transactionSignature = runCatching { Base58.decode(txHash) } - .getOrElse { ByteArray(64).also { SecureRandom().nextBytes(it) } } - .toList() - ), + amount = verifiedFiat, + of = token, + source = SwapFundingSource.CoinbaseOnramp(orderId = orderId), fund = { Result.success(Unit) } ).getOrThrow() - } - .onSuccess { swapId -> - _state.update { CoinbaseOnRampState.Completed(swapId, current.token, current.amount) } - } - .onFailure { error -> - trace( - message = "Payment processing failed", - tag = "OnRamp", - metadata = { - "orderId" to current.orderId - "errorType" to error::class.simpleName.orEmpty() - }, - error = error, - ) - reset() + + val order = OnrampOrder(orderId, paymentLink.url) + startPayment(order, token, verifiedFiat, swapId) } } diff --git a/apps/flipcash/shared/onramp/coinbase/src/main/kotlin/com/flipcash/app/onramp/CoinbaseOnRampHandler.kt b/apps/flipcash/shared/onramp/coinbase/src/main/kotlin/com/flipcash/app/onramp/CoinbaseOnRampHandler.kt index 1cdb10fd5..3649590cb 100644 --- a/apps/flipcash/shared/onramp/coinbase/src/main/kotlin/com/flipcash/app/onramp/CoinbaseOnRampHandler.kt +++ b/apps/flipcash/shared/onramp/coinbase/src/main/kotlin/com/flipcash/app/onramp/CoinbaseOnRampHandler.kt @@ -37,19 +37,6 @@ fun CoinbaseOnRampHandler( ) } - is CoinbaseOnRampState.Processing -> { - LaunchedEffect(current) { - delay(400) // let the system payment sheet finish its dismiss animation - controller.processPayment() - .onFailure { - BottomBarManager.showError( - title = "Something Went Wrong", - message = "Failed to complete purchase. Please try again", - ) - } - } - } - is CoinbaseOnRampState.Completed -> { LaunchedEffect(current) { controller.emitPendingNavigation( diff --git a/apps/flipcash/shared/onramp/coinbase/src/main/kotlin/com/flipcash/app/onramp/CoinbaseOnRampState.kt b/apps/flipcash/shared/onramp/coinbase/src/main/kotlin/com/flipcash/app/onramp/CoinbaseOnRampState.kt index 3911e094f..1335aa5bd 100644 --- a/apps/flipcash/shared/onramp/coinbase/src/main/kotlin/com/flipcash/app/onramp/CoinbaseOnRampState.kt +++ b/apps/flipcash/shared/onramp/coinbase/src/main/kotlin/com/flipcash/app/onramp/CoinbaseOnRampState.kt @@ -16,8 +16,7 @@ data class OnrampOrder( sealed interface CoinbaseOnRampState { data object Idle : CoinbaseOnRampState - data class Paying(val order: OnrampOrder, val token: Token, val amount: VerifiedFiat) : CoinbaseOnRampState - data class Processing(val orderId: String, val token: Token, val amount: VerifiedFiat) : CoinbaseOnRampState + data class Paying(val order: OnrampOrder, val token: Token, val amount: VerifiedFiat, val swapId: SwapId) : CoinbaseOnRampState data class Completed(val swapId: SwapId, val token: Token, val amount: VerifiedFiat) : CoinbaseOnRampState data class Failed(val error: CoinbaseOnRampWebError) : CoinbaseOnRampState } diff --git a/services/opencode/src/main/kotlin/com/getcode/opencode/model/transactions/SwapRequest.kt b/services/opencode/src/main/kotlin/com/getcode/opencode/model/transactions/SwapRequest.kt index 0a6be9878..56e171f6a 100644 --- a/services/opencode/src/main/kotlin/com/getcode/opencode/model/transactions/SwapRequest.kt +++ b/services/opencode/src/main/kotlin/com/getcode/opencode/model/transactions/SwapRequest.kt @@ -18,10 +18,11 @@ data class SwapRequest( val swapId: SwapId, val verifiedState: VerifiedState, ) { - val fundingIntentId = when (kind) { - is SwapStartKind.Reserve -> kind.fundingIntentId - is SwapStartKind.Stablecoin -> kind.fundingIntentId - } + val fundingIntentId: List + get() = when (kind) { + is SwapStartKind.Reserve -> kind.fundingIntentId + is SwapStartKind.Stablecoin -> kind.fundingIntentId + } val totalTransferAmount: LocalFiat get() { @@ -52,7 +53,9 @@ sealed interface SwapStartKind { get() = when (fundingSource) { is SwapFundingSource.ExternalWallet -> fundingSource.transactionSignature is SwapFundingSource.SubmitIntent -> fundingSource.id - is SwapFundingSource.CoinbaseOnramp -> fundingSource.orderId.toByteArray().toList() + is SwapFundingSource.CoinbaseOnramp -> throw IllegalArgumentException( + "Coinbase onramp does not use a funding intent" + ) SwapFundingSource.Unknown -> throw IllegalArgumentException("Invalid funding source") } }