Split Bridge into input and dedicated execution screens, matching Swap

The single-screen design (amount input + live balance validation + submit
+ destination-wait, all in one ViewModel) was structurally fragile: the
live origin-balance observer and the amount-vs-balance validation ran in
the same reactive scope as execution, so a real successful transfer could
flash a false "insufficient balance" error the instant the balance dropped
post-submission. Patching that race (clearing the field, then an isExecuting
flag) kept fixing symptoms without addressing why the class of bug existed:
the app's own Swap flow avoids it entirely by keeping amount entry and
execution as separate screens/ViewModels, so validation code for the input
screen simply doesn't exist on the execution screen.

BridgeExecutionViewModel/Fragment now own the actual submit + destination-
wait, reached by navigating with a BridgeExecutionPayload once
BridgeViewModel.swapClicked() re-validates amount against the latest known
balance/reserve - a real confirmation gate, not just a UI button-disabled
look the domain layer never itself checked. A single BridgeExecutionState
sealed class (SubmittingOrigin/OriginFailed/WaitingForDestination/
DestinationConfirmed/DestinationPendingReview) replaces what used to be six
independently-updated LiveData flags that had to be kept in sync by hand.

Also fixed two real bugs found via live device testing with real funds:
- BridgeMultisigConstants.POLKADOT_USDT_ASSET_ID was 1 (the wallet's own
  chains.json ordinal for USDT on Polkadot Asset Hub) instead of 1984 (the
  real on-chain Assets pallet id, confirmed directly against the chain).
  getPolkadotUsdtReserve() queried asset 1, which the multisig has never
  held anything at, so the wUSDT->USDT reserve check always reported 0
  USDT available regardless of the multisig's real (and growing) holdings.
- The deposit-wait label's text was static XML, never updated on the
  success path - a genuinely completed bridge (destination balance
  confirmed, checkmark shown) still read "Waiting for confirmation..."
  forever, indistinguishable from actually being stuck. The new execution
  screen's label is driven by BridgeExecutionState instead.
This commit is contained in:
2026-07-15 16:12:03 -07:00
parent cb8f0d8aae
commit 52e5645409
17 changed files with 544 additions and 210 deletions
@@ -57,6 +57,8 @@ import io.novafoundation.nova.feature_ahm_impl.presentation.migrationDetails.Cha
import io.novafoundation.nova.feature_ahm_impl.presentation.migrationDetails.ChainMigrationDetailsPayload
import io.novafoundation.nova.feature_assets.presentation.AssetsRouter
import io.novafoundation.nova.feature_assets.presentation.balance.detail.BalanceDetailFragment
import io.novafoundation.nova.feature_assets.presentation.bridge.execution.BridgeExecutionFragment
import io.novafoundation.nova.feature_assets.presentation.bridge.execution.BridgeExecutionPayload
import io.novafoundation.nova.feature_assets.presentation.flow.network.NetworkFlowFragment
import io.novafoundation.nova.feature_assets.presentation.flow.network.NetworkFlowPayload
import io.novafoundation.nova.feature_assets.presentation.model.OperationParcelizeModel
@@ -304,6 +306,14 @@ class Navigator(
.navigateInFirstAttachedContext()
}
override fun openBridgeExecution(payload: BridgeExecutionPayload) {
val bundle = BridgeExecutionFragment.getBundle(payload)
navigationBuilder().action(R.id.action_bridge_to_execution)
.setArgs(bundle)
.navigateInFirstAttachedContext()
}
override fun openTransferDetail(transaction: OperationParcelizeModel.Transfer) {
val bundle = TransferDetailFragment.getBundle(transaction)
@@ -1201,8 +1201,22 @@
app:popEnterAnim="@anim/fragment_close_enter"
app:popExitAnim="@anim/fragment_close_exit" />
<action
android:id="@+id/action_bridge_to_execution"
app:destination="@id/bridgeExecutionFragment"
app:enterAnim="@anim/fragment_open_enter"
app:exitAnim="@anim/fragment_open_exit"
app:popEnterAnim="@anim/fragment_close_enter"
app:popExitAnim="@anim/fragment_close_exit" />
</fragment>
<fragment
android:id="@+id/bridgeExecutionFragment"
android:name="io.novafoundation.nova.feature_assets.presentation.bridge.execution.BridgeExecutionFragment"
android:label="BridgeExecutionFragment"
tools:layout="@layout/fragment_bridge_execution" />
<fragment
android:id="@+id/tradeProvidersFragment"
android:name="io.novafoundation.nova.feature_assets.presentation.trade.provider.TradeProviderListFragment"