Skip to content

Commit 5889f5f

Browse files
committed
fix(cash): detect expired blockhash and show accurate retry message
When a Phantom wallet round-trip exceeds ~60s, the blockhash baked into the transaction expires and Solana rejects it with "Blockhash not found". Previously this surfaced a misleading "Make sure you have enough USDC and SOL" error. - Add TransactionExpired error variant to DeeplinkOnRampErrors - Detect blockhash errors in sendSwapTransaction before the generic FailedToSendTransaction fallback - Move processing state and navigation to onSuccess so progress only starts after the user returns from Phantom - Observe PhantomCeremonyFailed on the processing screen so dismissing the alert exits instead of leaving the user stuck Signed-off-by: Brandon McAnsh <git@bmcreations.dev>
1 parent 478719a commit 5889f5f

12 files changed

Lines changed: 79 additions & 34 deletions

File tree

apps/flipcash/core/src/main/kotlin/com/flipcash/app/core/tokens/SwapStep.kt

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,5 @@ sealed interface SwapStep : FlowStep, Parcelable {
2828
data object PhantomConfirmTransaction: SwapStep
2929
@Parcelize
3030
@Serializable
31-
data class Processing(
32-
val swapId: SwapId,
33-
) : SwapStep, NonDismissableRoute, NonDraggableRoute
31+
data object Processing : SwapStep, NonDismissableRoute, NonDraggableRoute
3432
}

apps/flipcash/core/src/main/res/values/strings.xml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -234,6 +234,8 @@
234234
<string name="error_title_deeplinkOnRampFailedToSimulateTransaction">Something Went Wrong</string>
235235
<string name="error_title_deeplinkOnRampFailedToCreateDeeplink">Something Went Wrong</string>
236236
<string name="error_title_deeplinkOnRampFailedToSendTransaction">Transaction Failed</string>
237+
<string name="error_title_deeplinkOnRampTransactionExpired">Transaction Expired</string>
238+
<string name="error_description_deeplinkOnRampTransactionExpired">The transaction took too long to process. Please try again</string>
237239
<string name="error_title_deeplinkOnRampDisconnected">Something Went Wrong</string>
238240
<string name="error_title_deeplinkOnRampUnauthorized">Something Went Wrong</string>
239241
<string name="error_title_deeplinkOnRampUserRejected">Request Rejected in %1$s</string>

apps/flipcash/features/tokens/src/main/kotlin/com/flipcash/app/tokens/PhantomWalletScreens.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -132,8 +132,8 @@ internal fun PhantomTransactionConfirmationScreen() {
132132
LaunchedEffect(viewModel) {
133133
viewModel.eventFlow
134134
.filterIsInstance<SwapViewModel.Event.PhantomNavigateToProcessing>()
135-
.onEach { event ->
136-
flowNavigator.navigateTo(SwapStep.Processing(event.swapId))
135+
.onEach {
136+
flowNavigator.navigateTo(SwapStep.Processing)
137137
}.launchIn(this)
138138
}
139139

apps/flipcash/features/tokens/src/main/kotlin/com/flipcash/app/tokens/SwapEntryScreen.kt

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -89,9 +89,8 @@ internal fun SwapEntryScreen(
8989
LaunchedEffect(viewModel) {
9090
viewModel.eventFlow
9191
.filterIsInstance<SwapViewModel.Event.OnPurchaseSubmitted>()
92-
.map { it.swapId }
93-
.onEach { swapId ->
94-
flowNavigator.navigateTo(SwapStep.Processing(swapId))
92+
.onEach {
93+
flowNavigator.navigateTo(SwapStep.Processing)
9594
}.launchIn(this)
9695
}
9796

@@ -130,9 +129,7 @@ internal fun SwapEntryScreen(
130129
coinbaseOnRampController.pendingNavigation.collect { route ->
131130
if (route is AppRoute.Token.TxProcessing) {
132131
viewModel.dispatchEvent(SwapViewModel.Event.OnSwapIdChanged(route.swapId))
133-
flowNavigator.navigateTo(
134-
SwapStep.Processing(route.swapId)
135-
)
132+
flowNavigator.navigateTo(SwapStep.Processing)
136133
}
137134
}
138135
}

apps/flipcash/features/tokens/src/main/kotlin/com/flipcash/app/tokens/SwapFlowScreen.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ private fun swapEntryProvider(): (NavKey) -> NavEntry<NavKey> = entryProvider {
6161
annotatedEntry<SwapStep.PhantomConfirmTransaction> {
6262
PhantomTransactionConfirmationScreen()
6363
}
64-
annotatedEntry<SwapStep.Processing> { step ->
65-
SwapProcessingScreen(step.swapId)
64+
annotatedEntry<SwapStep.Processing> {
65+
SwapProcessingScreen()
6666
}
6767
}

apps/flipcash/features/tokens/src/main/kotlin/com/flipcash/app/tokens/TokenSellReceiptScreen.kt

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -52,9 +52,8 @@ internal fun SellReceiptScreen() {
5252
LaunchedEffect(viewModel) {
5353
viewModel.eventFlow
5454
.filterIsInstance<SwapViewModel.Event.OnSellSubmitted>()
55-
.map { it.swapId }
56-
.onEach { swapId ->
57-
flowNavigator.navigateTo(SwapStep.Processing(swapId))
55+
.onEach {
56+
flowNavigator.navigateTo(SwapStep.Processing)
5857
}.launchIn(this)
5958
}
6059
}

apps/flipcash/features/tokens/src/main/kotlin/com/flipcash/app/tokens/TokenTxProcessingScreen.kt

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -24,18 +24,12 @@ import kotlinx.coroutines.flow.onEach
2424
* Flow-aware swap processing content, used inside `SwapFlowScreen`.
2525
*/
2626
@Composable
27-
internal fun SwapProcessingScreen(
28-
swapId: SwapId,
29-
) {
27+
internal fun SwapProcessingScreen() {
3028
val flowNavigator = rememberFlowNavigator<SwapStep, SwapResult>()
3129
val viewModel = flowSharedViewModel<SwapViewModel>()
3230

3331
TokenTxProcessingScreen(viewModel = viewModel)
3432

35-
LaunchedEffect(viewModel) {
36-
viewModel.dispatchEvent(Event.UpdateProcessingState(loading = true))
37-
}
38-
3933
LaunchedEffect(viewModel) {
4034
viewModel.eventFlow
4135
.filterIsInstance<Event.OnTransactionSuccessful>()
@@ -52,6 +46,14 @@ internal fun SwapProcessingScreen(
5246
}.launchIn(this)
5347
}
5448

49+
LaunchedEffect(viewModel) {
50+
viewModel.eventFlow
51+
.filterIsInstance<Event.PhantomCeremonyFailed>()
52+
.onEach {
53+
flowNavigator.exitCanceled()
54+
}.launchIn(this)
55+
}
56+
5557
BackHandler { /* intercept */ }
5658
}
5759

apps/flipcash/shared/onramp/deeplinks/src/main/kotlin/com/flipcash/app/onramp/DeeplinkOnRampErrors.kt

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,11 @@ sealed class DeeplinkOnRampError(
2525
override val cause: Throwable? = null
2626
) : DeeplinkOnRampError(code = code, message = message, cause = cause)
2727

28+
class TransactionExpired(
29+
override val message: String?,
30+
override val cause: Throwable? = null
31+
) : DeeplinkOnRampError(message = message, cause = cause)
32+
2833
class FailedToSubmitBuyToServer(
2934
override val code: Long = -100,
3035
override val message: String?,
@@ -70,6 +75,7 @@ val DeeplinkOnRampError.isAlert: Boolean
7075
DeeplinkError.Disconnected,
7176
DeeplinkError.TransactionRejected,
7277
) || this is DeeplinkOnRampError.FailedToSendTransaction
78+
|| this is DeeplinkOnRampError.TransactionExpired
7379
|| this is DeeplinkOnRampError.InsufficientSol
7480
|| this is DeeplinkOnRampError.InsufficientUsdc
7581
|| (this is DeeplinkOnRampError.FailedToSimulateTransaction && cause?.isNetworkError() == true)
@@ -83,6 +89,7 @@ fun DeeplinkOnRampError.messaging(getString: (Int) -> String, provider: String):
8389
is DeeplinkOnRampError.FailedToCreateTransaction -> getString(R.string.error_title_deeplinkOnRampFailedToCreateTransaction) to getString(R.string.error_description_deeplinkOnRampFailedToCreateTransaction)
8490
is DeeplinkOnRampError.FailedToSimulateTransaction -> getString(R.string.error_title_deeplinkOnRampFailedToSimulateTransaction) to getString(R.string.error_description_deeplinkOnRampFailedToSimulateTransaction)
8591
is DeeplinkOnRampError.FailedToSendTransaction -> getString(R.string.error_title_deeplinkOnRampFailedToSendTransaction) to getString(R.string.error_description_deeplinkOnRampFailedToSendTransaction).format(provider)
92+
is DeeplinkOnRampError.TransactionExpired -> getString(R.string.error_title_deeplinkOnRampTransactionExpired) to getString(R.string.error_description_deeplinkOnRampTransactionExpired)
8693
is DeeplinkOnRampError.FailedToSubmitBuyToServer -> getString(R.string.error_title_deeplinkOnRampExternalFundBuy) to getString(R.string.error_description_deeplinkOnRampExternalFundBuy).format(provider)
8794
is DeeplinkOnRampError.InsufficientSol -> getString(R.string.error_title_deeplinkOnRampInsufficientSol) to getString(R.string.error_description_deeplinkOnRampInsufficientSol).format(provider)
8895
is DeeplinkOnRampError.InsufficientUsdc -> getString(R.string.error_title_deeplinkOnRampInsufficientUsdc) to getString(R.string.error_description_deeplinkOnRampInsufficientUsdc).format(provider)

apps/flipcash/shared/onramp/deeplinks/src/main/kotlin/com/flipcash/app/onramp/PhantomWalletController.kt

Lines changed: 24 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -277,14 +277,30 @@ class PhantomWalletController @Inject constructor(
277277
}
278278
}
279279
)
280-
ErrorUtils.handleError(error)
281-
return@withContext Result.failure(
282-
DeeplinkOnRampError.FailedToSendTransaction(
283-
code = code ?: -99L,
284-
message = error.message,
285-
cause = error,
286-
)
287-
)
280+
281+
when {
282+
// Detect expired blockhash — occurs when the Phantom wallet
283+
// round-trip exceeds ~60 seconds (Solana's blockhash lifetime).
284+
error is RpcException && error.isBlockhashNotFound -> {
285+
ErrorUtils.handleError(error)
286+
return@withContext Result.failure(
287+
DeeplinkOnRampError.TransactionExpired(
288+
message = error.message,
289+
cause = error,
290+
)
291+
)
292+
}
293+
else -> {
294+
ErrorUtils.handleError(error)
295+
return@withContext Result.failure(
296+
DeeplinkOnRampError.FailedToSendTransaction(
297+
code = code ?: -99L,
298+
message = error.message,
299+
cause = error,
300+
)
301+
)
302+
}
303+
}
288304
}
289305

290306
Result.success(Unit)

apps/flipcash/shared/onramp/deeplinks/src/test/kotlin/com/flipcash/app/onramp/DeeplinkErrorTest.kt

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@ class DeeplinkErrorTest {
7575
fun `DeeplinkOnRampError subtypes are Throwable`() {
7676
val errors: List<Throwable> = listOf(
7777
DeeplinkOnRampError.FailedToSendTransaction(message = "fail"),
78+
DeeplinkOnRampError.TransactionExpired(message = "expired"),
7879
DeeplinkOnRampError.WalletProvidedError(
7980
error = DeeplinkError.Disconnected,
8081
message = "disconnected"
@@ -87,6 +88,24 @@ class DeeplinkErrorTest {
8788
}
8889
}
8990

91+
@Test
92+
fun `TransactionExpired isAlert returns true`() {
93+
val error = DeeplinkOnRampError.TransactionExpired(message = "Blockhash not found")
94+
assertTrue(error.isAlert, "TransactionExpired should be an alert")
95+
}
96+
97+
@Test
98+
fun `TransactionExpired preserves cause chain`() {
99+
val cause = RuntimeException("Blockhash not found")
100+
val error = DeeplinkOnRampError.TransactionExpired(
101+
message = cause.message,
102+
cause = cause,
103+
)
104+
105+
assertEquals("Blockhash not found", error.message)
106+
assertEquals(cause, error.cause)
107+
}
108+
90109
@Test
91110
fun `WalletProvidedError code matches error code`() {
92111
val error = DeeplinkOnRampError.WalletProvidedError(

0 commit comments

Comments
 (0)