diff --git a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/execution/ExecutionTimerView.kt b/common/src/main/java/io/novafoundation/nova/common/view/ExecutionTimerView.kt
similarity index 92%
rename from feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/execution/ExecutionTimerView.kt
rename to common/src/main/java/io/novafoundation/nova/common/view/ExecutionTimerView.kt
index 1520728a..fbf72e2d 100644
--- a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/execution/ExecutionTimerView.kt
+++ b/common/src/main/java/io/novafoundation/nova/common/view/ExecutionTimerView.kt
@@ -1,4 +1,4 @@
-package io.novafoundation.nova.feature_swap_impl.presentation.execution
+package io.novafoundation.nova.common.view
import android.content.Context
import android.os.CountDownTimer
@@ -14,13 +14,13 @@ import android.view.animation.RotateAnimation
import android.widget.TextSwitcher
import android.widget.TextView
import androidx.constraintlayout.widget.ConstraintLayout
+import io.novafoundation.nova.common.R
+import io.novafoundation.nova.common.databinding.ViewExecutionTimerBinding
import io.novafoundation.nova.common.utils.WithContextExtensions
import io.novafoundation.nova.common.utils.inflater
import io.novafoundation.nova.common.utils.makeGone
import io.novafoundation.nova.common.utils.makeVisible
import io.novafoundation.nova.common.utils.setTextColorRes
-import io.novafoundation.nova.feature_swap_impl.R
-import io.novafoundation.nova.feature_swap_impl.databinding.ViewExecutionTimerBinding
import kotlin.math.cos
import kotlin.time.Duration
import kotlin.time.Duration.Companion.milliseconds
@@ -28,6 +28,13 @@ import kotlin.time.Duration.Companion.milliseconds
private const val SECOND_MILLIS = 1000L
private const val HIDE_SCALE = 0.7f
+/**
+ * Reusable circular countdown/result indicator - originally swap-execution-only
+ * (feature_swap_impl.presentation.execution), moved here since it has no swap-specific coupling
+ * beyond a Duration input, so other features (e.g. the Bridge screen) can show the same
+ * waiting/success/error UX instead of leaving the user with no feedback after submitting an
+ * operation that takes real time to confirm.
+ */
class ExecutionTimerView @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
diff --git a/feature-swap-impl/src/main/res/layout/view_execution_timer.xml b/common/src/main/res/layout/view_execution_timer.xml
similarity index 99%
rename from feature-swap-impl/src/main/res/layout/view_execution_timer.xml
rename to common/src/main/res/layout/view_execution_timer.xml
index dd1197ef..9404bad7 100644
--- a/feature-swap-impl/src/main/res/layout/view_execution_timer.xml
+++ b/common/src/main/res/layout/view_execution_timer.xml
@@ -64,4 +64,4 @@
app:layout_constraintStart_toStartOf="@+id/executionProgress"
app:layout_constraintTop_toBottomOf="@+id/executionTimeSwitcher" />
-
\ No newline at end of file
+
diff --git a/common/src/main/res/values/strings.xml b/common/src/main/res/values/strings.xml
index a10dbc96..b6e2b622 100644
--- a/common/src/main/res/values/strings.xml
+++ b/common/src/main/res/values/strings.xml
@@ -372,6 +372,9 @@
Signing…
Signing failed, tap to retry
+ Waiting for confirmation on Pezkuwi Asset Hub…
+ Still processing - this can take longer if it needs manual multisig review. Check back in a few minutes.
+
Buy/Sell
diff --git a/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/bridge/BridgeFragment.kt b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/bridge/BridgeFragment.kt
index 2c89a028..c961a6aa 100644
--- a/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/bridge/BridgeFragment.kt
+++ b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/bridge/BridgeFragment.kt
@@ -156,6 +156,19 @@ class BridgeFragment : BaseFragment() {
viewModel.fillAmountEvent.observeEvent { amount ->
binder.bridgeFromCard.amountInput.setText(amount)
}
+
+ viewModel.depositWaitLabelVisible.observe { visible ->
+ binder.bridgeDepositWaitLabel.visibility = if (visible) View.VISIBLE else View.GONE
+ }
+
+ viewModel.depositWaitState.observe { state ->
+ if (state == null) {
+ binder.bridgeExecutionTimer.visibility = View.GONE
+ } else {
+ binder.bridgeExecutionTimer.visibility = View.VISIBLE
+ binder.bridgeExecutionTimer.setState(state)
+ }
+ }
}
}
diff --git a/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/bridge/BridgeViewModel.kt b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/bridge/BridgeViewModel.kt
index c3a4ed6b..83842d64 100644
--- a/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/bridge/BridgeViewModel.kt
+++ b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/bridge/BridgeViewModel.kt
@@ -8,6 +8,7 @@ import io.novafoundation.nova.common.resources.ResourceManager
import io.novafoundation.nova.common.utils.Event
import io.novafoundation.nova.common.utils.images.Icon
import io.novafoundation.nova.common.view.ButtonState
+import io.novafoundation.nova.common.view.ExecutionTimerView
import io.novafoundation.nova.feature_account_api.presenatation.chain.getAssetIconOrFallback
import io.novafoundation.nova.feature_assets.R
import io.novafoundation.nova.feature_assets.domain.WalletInteractor
@@ -23,9 +24,12 @@ import io.novafoundation.nova.runtime.ext.displayNameWithAssetStandard
import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry
import io.novasama.substrate_sdk_android.ss58.SS58Encoder.toAccountId
import kotlinx.coroutines.Job
+import kotlinx.coroutines.flow.first
import kotlinx.coroutines.launch
+import kotlinx.coroutines.withTimeoutOrNull
import java.math.BigDecimal
import java.math.RoundingMode
+import kotlin.time.Duration.Companion.seconds
/**
* DOT<->HEZ used to be a second pair here, retired 2026-07 in favor of the multisig-custodied
@@ -68,6 +72,10 @@ class BridgeViewModel(
// USDT has 6 decimals on both Polkadot and Pezkuwi Asset Hub.
val USDT_DECIMALS_DIVISOR: BigDecimal = BigDecimal.TEN.pow(6)
+
+ /** How long to actively watch the destination balance before admitting we can't confirm
+ * completion yet - not a claim about how long the bridge itself actually takes. */
+ val DEPOSIT_WAIT_TIMEOUT = 90.seconds
}
private val _pair = MutableLiveData(BridgePair.USDT)
@@ -109,6 +117,16 @@ class BridgeViewModel(
private val _signButtonLabel = MutableLiveData("")
val signButtonLabel: LiveData = _signButtonLabel
+ /** Null = hidden. Driven by an actual destination-balance observation after Swap is tapped -
+ * never just a cosmetic countdown that reports "done" regardless of whether funds arrived. */
+ private val _depositWaitState = MutableLiveData(null)
+ val depositWaitState: LiveData = _depositWaitState
+
+ private val _depositWaitLabelVisible = MutableLiveData(false)
+ val depositWaitLabelVisible: LiveData = _depositWaitLabelVisible
+
+ private var depositWaitJob: Job? = null
+
private val _fromCard = MutableLiveData()
val fromCard: LiveData = _fromCard
@@ -193,17 +211,29 @@ class BridgeViewModel(
val dir = _direction.value ?: return
if (currentAmount <= 0) return
+ val chainId = when (dir) {
+ BridgeDirection.USDT_TO_WUSDT -> POLKADOT_ASSET_HUB_ID
+ BridgeDirection.WUSDT_TO_USDT -> PEZKUWI_ASSET_HUB_ID
+ }
+
+ val assetId = when (dir) {
+ BridgeDirection.USDT_TO_WUSDT -> POLKADOT_USDT_ASSET_ID
+ BridgeDirection.WUSDT_TO_USDT -> PEZKUWI_USDT_ASSET_ID
+ }
+
+ val destChainId = when (dir) {
+ BridgeDirection.USDT_TO_WUSDT -> PEZKUWI_ASSET_HUB_ID
+ BridgeDirection.WUSDT_TO_USDT -> POLKADOT_ASSET_HUB_ID
+ }
+
+ val destAssetId = when (dir) {
+ BridgeDirection.USDT_TO_WUSDT -> PEZKUWI_USDT_ASSET_ID
+ BridgeDirection.WUSDT_TO_USDT -> POLKADOT_USDT_ASSET_ID
+ }
+
+ val expectedAmount = currentAmount
+
launch {
- val chainId = when (dir) {
- BridgeDirection.USDT_TO_WUSDT -> POLKADOT_ASSET_HUB_ID
- BridgeDirection.WUSDT_TO_USDT -> PEZKUWI_ASSET_HUB_ID
- }
-
- val assetId = when (dir) {
- BridgeDirection.USDT_TO_WUSDT -> POLKADOT_USDT_ASSET_ID
- BridgeDirection.WUSDT_TO_USDT -> PEZKUWI_USDT_ASSET_ID
- }
-
val chain = chainRegistry.getChain(chainId)
val accountId = BRIDGE_ADDRESS_GENERIC.toAccountId()
val bridgeAddress = chain.addressOf(accountId)
@@ -212,6 +242,49 @@ class BridgeViewModel(
val sendPayload = SendPayload.SpecifiedOrigin(assetPayload)
router.openSend(sendPayload, bridgeAddress, currentAmount)
+
+ observeDepositCompletion(destChainId, destAssetId, expectedAmount)
+ }
+ }
+
+ /** Watches the DESTINATION balance for a real increase after a swap is submitted, rather than
+ * a cosmetic timer that reports "done" regardless of whether funds actually arrived - there is
+ * no completion callback from the generic Send flow this screen delegates to (it just pops
+ * back to the previous screen on success), so balance-delta is the only honest signal
+ * available. Bounded to a reasonable wait; if it elapses without a confirmed increase, this
+ * says so plainly rather than implying success or failure it can't actually confirm - the
+ * swap may just need manual 3-of-5 review, which can take longer.
+ */
+ private fun observeDepositCompletion(destChainId: String, destAssetId: Int, expectedAmount: Double) {
+ depositWaitJob?.cancel()
+ depositWaitJob = launch {
+ val balanceBefore = try {
+ walletInteractor.assetFlow(destChainId, destAssetId).first().transferable
+ } catch (e: Exception) {
+ return@launch // Can't observe reliably - don't show a misleading progress state.
+ }
+
+ _depositWaitLabelVisible.postValue(true)
+ _depositWaitState.postValue(ExecutionTimerView.State.CountdownTimer(DEPOSIT_WAIT_TIMEOUT))
+
+ val minExpectedIncrease = BigDecimal.valueOf(expectedAmount * (1 - FEE_PERCENT * 2)) // fee + rounding slack
+
+ val confirmed = withTimeoutOrNull(DEPOSIT_WAIT_TIMEOUT.inWholeMilliseconds) {
+ walletInteractor.assetFlow(destChainId, destAssetId)
+ .first { it.transferable - balanceBefore >= minExpectedIncrease }
+ } != null
+
+ if (confirmed) {
+ _depositWaitState.postValue(ExecutionTimerView.State.Success)
+ } else {
+ // Not a failure - just not confirmed within the wait window. Hide the timer and
+ // say so plainly instead of showing a false success or a false error icon.
+ _depositWaitState.postValue(null)
+ _depositWaitLabelVisible.postValue(false)
+ _warningBlocked.postValue(false)
+ _showWarning.postValue(true)
+ _warningText.postValue(resourceManager.getString(R.string.bridge_deposit_pending_message))
+ }
}
}
diff --git a/feature-assets/src/main/res/layout/fragment_bridge.xml b/feature-assets/src/main/res/layout/fragment_bridge.xml
index 59feac79..2e72f57b 100644
--- a/feature-assets/src/main/res/layout/fragment_bridge.xml
+++ b/feature-assets/src/main/res/layout/fragment_bridge.xml
@@ -190,6 +190,28 @@
android:text="@string/bridge_sign_button"
android:visibility="gone" />
+
+
+
+
+
diff --git a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/execution/SwapExecutionFragment.kt b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/execution/SwapExecutionFragment.kt
index da85e2ba..9b6db9b5 100644
--- a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/execution/SwapExecutionFragment.kt
+++ b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/execution/SwapExecutionFragment.kt
@@ -12,6 +12,7 @@ import io.novafoundation.nova.common.utils.makeVisible
import io.novafoundation.nova.common.utils.setCurrentText
import io.novafoundation.nova.common.utils.setText
import io.novafoundation.nova.common.utils.setTextColorRes
+import io.novafoundation.nova.common.view.ExecutionTimerView
import io.novafoundation.nova.common.view.bottomSheet.description.observeDescription
import io.novafoundation.nova.common.view.shape.getBlockDrawable
import io.novafoundation.nova.common.view.shape.getRoundedCornerDrawable
diff --git a/feature-swap-impl/src/main/res/layout/fragment_swap_execution.xml b/feature-swap-impl/src/main/res/layout/fragment_swap_execution.xml
index d11ca294..72e6b705 100644
--- a/feature-swap-impl/src/main/res/layout/fragment_swap_execution.xml
+++ b/feature-swap-impl/src/main/res/layout/fragment_swap_execution.xml
@@ -27,7 +27,7 @@
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
-