From 437dfff81365e576e5ed04e1912a12cacae124e1 Mon Sep 17 00:00:00 2001 From: Satoshi Qazi Muhammed Date: Wed, 15 Jul 2026 07:59:35 -0700 Subject: [PATCH] Bridge: submit deposit via real submit-and-await execution, not generic Send swapClicked() used to hand off to the generic Send flow's confirm screen, which only fire-and-forget submits and pops back immediately regardless of real dispatch outcome - a real user hit exactly that: tapped confirm, the screen closed like it succeeded, funds never arrived. Now the bridge builds its own WeightedAssetTransfer and calls SendUseCase.performOnChainTransferAndAwaitExecution directly, mirroring the same real submit-and-await pattern the app's own Swap execution screen already uses, so Success/Error reflect what actually happened on-chain. --- .../presentation/bridge/BridgeViewModel.kt | 106 ++++++++++++++++-- .../presentation/bridge/di/BridgeModule.kt | 19 +++- 2 files changed, 114 insertions(+), 11 deletions(-) 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 83842d64..b8c3e335 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 @@ -9,19 +9,25 @@ 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.domain.interfaces.SelectedAccountUseCase +import io.novafoundation.nova.feature_account_api.data.fee.FeePaymentCurrency 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 import io.novafoundation.nova.feature_assets.domain.bridge.multisig.BridgeMultisigConstants import io.novafoundation.nova.feature_assets.domain.bridge.multisig.BridgeMultisigInteractor import io.novafoundation.nova.feature_assets.domain.bridge.multisig.BridgeSignerState +import io.novafoundation.nova.feature_assets.domain.send.SendInteractor import io.novafoundation.nova.feature_assets.presentation.AssetsRouter -import io.novafoundation.nova.feature_assets.presentation.send.amount.SendPayload -import io.novafoundation.nova.feature_wallet_api.presentation.model.AssetPayload +import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.tranfers.WeightedAssetTransfer +import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.tranfers.buildAssetTransfer +import io.novafoundation.nova.feature_wallet_api.domain.SendUseCase import io.novafoundation.nova.runtime.ext.ChainGeneses import io.novafoundation.nova.runtime.ext.addressOf import io.novafoundation.nova.runtime.ext.displayNameWithAssetStandard import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry +import io.novafoundation.nova.runtime.multiNetwork.ChainWithAsset +import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain import io.novasama.substrate_sdk_android.ss58.SS58Encoder.toAccountId import kotlinx.coroutines.Job import kotlinx.coroutines.flow.first @@ -45,7 +51,10 @@ class BridgeViewModel( private val chainRegistry: ChainRegistry, private val assetIconProvider: AssetIconProvider, private val walletInteractor: WalletInteractor, - private val bridgeMultisigInteractor: BridgeMultisigInteractor + private val bridgeMultisigInteractor: BridgeMultisigInteractor, + private val sendInteractor: SendInteractor, + private val sendUseCase: SendUseCase, + private val selectedAccountUseCase: SelectedAccountUseCase ) : BaseViewModel() { companion object { @@ -76,6 +85,10 @@ class BridgeViewModel( /** 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 + + /** Cosmetic only - the real completion signal is the awaited dispatch result, which can + * arrive before or after this visually elapses (same as the swap screen's own timer). */ + val SUBMIT_WAIT_TIMEOUT = 30.seconds } private val _pair = MutableLiveData(BridgePair.USDT) @@ -235,18 +248,93 @@ class BridgeViewModel( launch { val chain = chainRegistry.getChain(chainId) + val chainAsset = chain.assetsById.getValue(assetId) val accountId = BRIDGE_ADDRESS_GENERIC.toAccountId() val bridgeAddress = chain.addressOf(accountId) - val assetPayload = AssetPayload(chainId, assetId) - val sendPayload = SendPayload.SpecifiedOrigin(assetPayload) - - router.openSend(sendPayload, bridgeAddress, currentAmount) - - observeDepositCompletion(destChainId, destAssetId, expectedAmount) + submitBridgeTransfer(chain, chainAsset, bridgeAddress, expectedAmount, destChainId, destAssetId) } } + /** Submits the bridge deposit as a plain on-chain transfer and ACTUALLY AWAITS the real + * dispatch result - replacing the previous router.openSend(...) hand-off to the generic Send + * flow, whose confirm screen only submits (fire-and-forget) and pops back to this screen + * immediately, regardless of whether the transaction actually succeeded. A real user hit + * exactly that: "tapped confirm, the screen just closed like it succeeded" while the funds + * never arrived. This mirrors the same real submit-and-await pattern the app's own Swap + * execution screen already uses (SendUseCase.performOnChainTransferAndAwaitExecution, backed + * by ExtrinsicService.submitExtrinsicAndAwaitExecution - a genuine on-chain dispatch result, + * not a UI guess) instead of the shallow SendInteractor.performTransfer path. + * + * Two honest phases, each backed by a real signal: + * 1. Submit + await the ORIGIN chain transfer's own dispatch result (seconds, via the real + * extrinsic execution API) - Success/Error here reflect what actually happened on-chain. + * 2. Only once that's confirmed, wait for the DESTINATION balance to actually increase (the + * bridge's own off-chain relay step, which has no callback this wallet can observe + * directly - balance-delta is the most honest signal available for that specific step). + */ + private suspend fun submitBridgeTransfer( + chain: Chain, + chainAsset: Chain.Asset, + bridgeAddress: String, + amount: Double, + destChainId: String, + destAssetId: Int + ) { + _depositWaitLabelVisible.postValue(true) + _depositWaitState.postValue(ExecutionTimerView.State.CountdownTimer(SUBMIT_WAIT_TIMEOUT)) + + val chainWithAsset = ChainWithAsset(chain, chainAsset) + val bigDecimalAmount = BigDecimal.valueOf(amount) + + val submissionResult = runCatching { + val metaAccount = selectedAccountUseCase.getSelectedMetaAccount() + + val assetTransfer = buildAssetTransfer( + metaAccount = metaAccount, + feePaymentCurrency = FeePaymentCurrency.Native, + origin = chainWithAsset, + destination = chainWithAsset, // plain on-chain transfer, not a cross-chain route + amount = bigDecimalAmount, + transferringMaxAmount = false, + address = bridgeAddress, + ) + + val fee = sendInteractor.getFee(assetTransfer, viewModelScope) + + val weightedTransfer = WeightedAssetTransfer( + sender = metaAccount, + recipient = bridgeAddress, + originChain = chain, + destinationChain = chain, + destinationChainAsset = chainAsset, + originChainAsset = chainAsset, + amount = bigDecimalAmount, + feePaymentCurrency = FeePaymentCurrency.Native, + fee = fee.originFee, + transferringMaxAmount = false, + ) + + sendUseCase.performOnChainTransferAndAwaitExecution(weightedTransfer, fee.originFee.submissionFee, viewModelScope) + .getOrThrow() + } + + submissionResult.fold( + onSuccess = { + // Real on-chain dispatch confirmed on the origin chain - now (and only now) wait + // for the bridge to actually credit the destination. + observeDepositCompletion(destChainId, destAssetId, amount) + }, + onFailure = { e -> + _depositWaitState.postValue(ExecutionTimerView.State.Error) + _depositWaitLabelVisible.postValue(false) + _showWarning.postValue(true) + _warningBlocked.postValue(true) + _warningText.postValue(e.message ?: resourceManager.getString(R.string.bridge_deposit_pending_message)) + } + ) + } + /** 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 diff --git a/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/bridge/di/BridgeModule.kt b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/bridge/di/BridgeModule.kt index 31242e4c..9ac58f2d 100644 --- a/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/bridge/di/BridgeModule.kt +++ b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/bridge/di/BridgeModule.kt @@ -15,8 +15,10 @@ import io.novafoundation.nova.feature_account_api.domain.interfaces.SelectedAcco import io.novafoundation.nova.feature_assets.domain.WalletInteractor import io.novafoundation.nova.feature_assets.domain.bridge.multisig.BridgeMultisigInteractor import io.novafoundation.nova.feature_assets.domain.bridge.multisig.RealBridgeMultisigInteractor +import io.novafoundation.nova.feature_assets.domain.send.SendInteractor import io.novafoundation.nova.feature_assets.presentation.AssetsRouter import io.novafoundation.nova.feature_assets.presentation.bridge.BridgeViewModel +import io.novafoundation.nova.feature_wallet_api.domain.SendUseCase import io.novafoundation.nova.runtime.di.REMOTE_STORAGE_SOURCE import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry import io.novafoundation.nova.runtime.storage.source.StorageDataSource @@ -44,9 +46,22 @@ class BridgeModule { chainRegistry: ChainRegistry, assetIconProvider: AssetIconProvider, walletInteractor: WalletInteractor, - bridgeMultisigInteractor: BridgeMultisigInteractor + bridgeMultisigInteractor: BridgeMultisigInteractor, + sendInteractor: SendInteractor, + sendUseCase: SendUseCase, + selectedAccountUseCase: SelectedAccountUseCase ): ViewModel { - return BridgeViewModel(router, resourceManager, chainRegistry, assetIconProvider, walletInteractor, bridgeMultisigInteractor) + return BridgeViewModel( + router, + resourceManager, + chainRegistry, + assetIconProvider, + walletInteractor, + bridgeMultisigInteractor, + sendInteractor, + sendUseCase, + selectedAccountUseCase + ) } @Provides