mirror of
https://github.com/pezkuwichain/pezkuwi-wallet-android.git
synced 2026-07-22 14:55:48 +00:00
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:
@@ -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"
|
||||
|
||||
@@ -372,9 +372,12 @@
|
||||
<string name="bridge_sign_in_progress">Signing…</string>
|
||||
<string name="bridge_sign_error">Signing failed, tap to retry</string>
|
||||
|
||||
<string name="bridge_deposit_wait_label">Waiting for confirmation on Pezkuwi Asset Hub…</string>
|
||||
<string name="bridge_deposit_success_label">Bridge complete — funds delivered</string>
|
||||
<string name="bridge_deposit_pending_message">Still processing - this can take longer if it needs manual multisig review. Check back in a few minutes.</string>
|
||||
|
||||
<string name="bridge_execution_submitting_label">Submitting transfer…</string>
|
||||
<string name="bridge_execution_waiting_destination_label">Waiting for confirmation on %s…</string>
|
||||
|
||||
<string name="wallet_asset_buy_sell">Buy/Sell</string>
|
||||
|
||||
|
||||
|
||||
+3
@@ -35,6 +35,7 @@ import io.novafoundation.nova.feature_assets.presentation.tokens.manage.chain.di
|
||||
import io.novafoundation.nova.feature_assets.presentation.tokens.manage.di.ManageTokensComponent
|
||||
import io.novafoundation.nova.feature_assets.presentation.topup.TopUpAddressCommunicator
|
||||
import io.novafoundation.nova.feature_assets.presentation.bridge.di.BridgeComponent
|
||||
import io.novafoundation.nova.feature_assets.presentation.bridge.execution.di.BridgeExecutionComponent
|
||||
import io.novafoundation.nova.feature_assets.presentation.citizenship.di.CitizenshipComponent
|
||||
import io.novafoundation.nova.feature_assets.presentation.trade.sell.flow.asset.di.AssetSellFlowComponent
|
||||
import io.novafoundation.nova.feature_assets.presentation.trade.sell.flow.network.di.NetworkSellFlowComponent
|
||||
@@ -115,6 +116,8 @@ interface AssetsFeatureComponent : AssetsFeatureApi {
|
||||
|
||||
fun bridgeComponentFactory(): BridgeComponent.Factory
|
||||
|
||||
fun bridgeExecutionComponentFactory(): BridgeExecutionComponent.Factory
|
||||
|
||||
fun giftsFlowComponent(): AssetGiftsFlowComponent.Factory
|
||||
|
||||
fun tradeProviderListComponent(): TradeProviderListComponent.Factory
|
||||
|
||||
+9
-1
@@ -14,7 +14,15 @@ package io.novafoundation.nova.feature_assets.domain.bridge.multisig
|
||||
object BridgeMultisigConstants {
|
||||
|
||||
const val WUSDT_ASSET_ID = 1000
|
||||
const val POLKADOT_USDT_ASSET_ID = 1
|
||||
|
||||
/** The real on-chain Assets pallet id for USDT on Polkadot Asset Hub - NOT the wallet's own
|
||||
* internal chains.json ordinal (also confusingly "1" there; the real id lives under that
|
||||
* entry's `typeExtras.assetId`). This constant is used for a raw runtime storage query
|
||||
* (getPolkadotUsdtReserve), which bypasses the wallet's Chain.Asset abstraction entirely, so
|
||||
* it needs the real protocol id. Querying "1" always returned an empty/zero balance - the
|
||||
* multisig has never held anything at that id - which made the wUSDT->USDT reserve check
|
||||
* report 0 permanently regardless of the multisig's real (and growing) USDT holdings. */
|
||||
const val POLKADOT_USDT_ASSET_ID = 1984
|
||||
|
||||
const val MULTISIG_ADDRESS = "5GvwxmCDp3PC33KHoeWSgj3S7ocE7nzk1jiCCZMPSDBFeNcj"
|
||||
|
||||
|
||||
+3
@@ -1,6 +1,7 @@
|
||||
package io.novafoundation.nova.feature_assets.presentation
|
||||
|
||||
import android.os.Bundle
|
||||
import io.novafoundation.nova.feature_assets.presentation.bridge.execution.BridgeExecutionPayload
|
||||
import io.novafoundation.nova.feature_assets.presentation.flow.network.NetworkFlowPayload
|
||||
import io.novafoundation.nova.feature_assets.presentation.model.OperationParcelizeModel
|
||||
import io.novafoundation.nova.feature_assets.presentation.send.TransferDraft
|
||||
@@ -27,6 +28,8 @@ interface AssetsRouter {
|
||||
|
||||
fun openConfirmTransfer(transferDraft: TransferDraft)
|
||||
|
||||
fun openBridgeExecution(payload: BridgeExecutionPayload)
|
||||
|
||||
fun openTransferDetail(transaction: OperationParcelizeModel.Transfer)
|
||||
|
||||
fun openExtrinsicDetail(extrinsic: OperationParcelizeModel.Extrinsic)
|
||||
|
||||
+1
-13
@@ -71,6 +71,7 @@ class BridgeFragment : BaseFragment<BridgeViewModel, FragmentBridgeBinding>() {
|
||||
super.onResume()
|
||||
|
||||
viewModel.refreshBridgeStatus()
|
||||
viewModel.resetAmount()
|
||||
}
|
||||
|
||||
override fun inject() {
|
||||
@@ -156,19 +157,6 @@ class BridgeFragment : BaseFragment<BridgeViewModel, FragmentBridgeBinding>() {
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+46
-165
@@ -2,41 +2,27 @@ package io.novafoundation.nova.feature_assets.presentation.bridge
|
||||
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import io.novafoundation.nova.common.base.BaseViewModel
|
||||
import io.novafoundation.nova.common.presentation.AssetIconProvider
|
||||
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.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_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.feature_assets.presentation.bridge.execution.BridgeExecutionPayload
|
||||
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
|
||||
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
|
||||
@@ -45,6 +31,13 @@ import kotlin.time.Duration.Companion.seconds
|
||||
* (see /home/myhez/res/validators-tiki.md). Kept the BridgePair/pairOptions/picker structure
|
||||
* (rather than collapsing to a single hardcoded pair) since it costs nothing and is exactly the
|
||||
* seam a future new pair would reuse.
|
||||
*
|
||||
* This screen is deliberately input-only: it has no submit/execution logic at all, the same way
|
||||
* the app's own Swap flow keeps amount entry and execution as separate screens/ViewModels. See
|
||||
* BridgeExecutionViewModel for the actual submit + destination-wait, reached via swapClicked()
|
||||
* below. Amount-vs-balance/reserve validation here is a live *display* concern (the error text
|
||||
* under the input, the button's enabled look) - swapClicked() re-checks the same conditions for
|
||||
* real right before navigating, which is the actual confirmation gate.
|
||||
*/
|
||||
class BridgeViewModel(
|
||||
private val router: AssetsRouter,
|
||||
@@ -53,9 +46,6 @@ class BridgeViewModel(
|
||||
private val assetIconProvider: AssetIconProvider,
|
||||
private val walletInteractor: WalletInteractor,
|
||||
private val bridgeMultisigInteractor: BridgeMultisigInteractor,
|
||||
private val sendInteractor: SendInteractor,
|
||||
private val sendUseCase: SendUseCase,
|
||||
private val selectedAccountUseCase: SelectedAccountUseCase
|
||||
) : BaseViewModel() {
|
||||
|
||||
companion object {
|
||||
@@ -82,14 +72,6 @@ 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
|
||||
|
||||
/** 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)
|
||||
@@ -131,16 +113,6 @@ class BridgeViewModel(
|
||||
private val _signButtonLabel = MutableLiveData("")
|
||||
val signButtonLabel: LiveData<String> = _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<ExecutionTimerView.State?>(null)
|
||||
val depositWaitState: LiveData<ExecutionTimerView.State?> = _depositWaitState
|
||||
|
||||
private val _depositWaitLabelVisible = MutableLiveData(false)
|
||||
val depositWaitLabelVisible: LiveData<Boolean> = _depositWaitLabelVisible
|
||||
|
||||
private var depositWaitJob: Job? = null
|
||||
|
||||
private val _fromCard = MutableLiveData<BridgeAssetCardUi>()
|
||||
val fromCard: LiveData<BridgeAssetCardUi> = _fromCard
|
||||
|
||||
@@ -221,10 +193,38 @@ class BridgeViewModel(
|
||||
_fillAmountEvent.value = Event(availableBalance.stripTrailingZeros().toPlainString())
|
||||
}
|
||||
|
||||
/** Called every time this screen becomes visible again, including returning from a completed
|
||||
* execution - always starts the next operation from a clean slate rather than leaving the
|
||||
* just-spent amount sitting in the input (the entire class of bug that used to require a
|
||||
* hand-timed field-clear right after submit no longer exists once input and execution are
|
||||
* different screens: there is no "just submitted" moment on this screen anymore). */
|
||||
fun resetAmount() {
|
||||
currentAmount = 0.0
|
||||
_fillAmountEvent.value = Event("")
|
||||
calculateOutput()
|
||||
updateInsufficientBalanceState()
|
||||
updateWarningState()
|
||||
}
|
||||
|
||||
/** The actual confirmation gate: re-checks amount against the latest known balance/reserve
|
||||
* right before committing to a real on-chain transfer, rather than trusting the Swap
|
||||
* button's enabled look alone (a UI affordance, not a domain guarantee - see
|
||||
* BridgeExecutionViewModel's doc for why the previous single-screen design needed a
|
||||
* best-effort in-flight flag here instead of a real gate). */
|
||||
fun swapClicked() {
|
||||
val dir = _direction.value ?: return
|
||||
if (currentAmount <= 0) return
|
||||
|
||||
val requested = BigDecimal.valueOf(currentAmount)
|
||||
if (requested > availableBalance) {
|
||||
updateInsufficientBalanceState()
|
||||
return
|
||||
}
|
||||
if (dir == BridgeDirection.WUSDT_TO_USDT && requested > polkadotUsdtReserve) {
|
||||
updateWarningState()
|
||||
return
|
||||
}
|
||||
|
||||
val chainId = when (dir) {
|
||||
BridgeDirection.USDT_TO_WUSDT -> POLKADOT_ASSET_HUB_ID
|
||||
BridgeDirection.WUSDT_TO_USDT -> PEZKUWI_ASSET_HUB_ID
|
||||
@@ -245,142 +245,23 @@ class BridgeViewModel(
|
||||
BridgeDirection.WUSDT_TO_USDT -> POLKADOT_USDT_ASSET_ID
|
||||
}
|
||||
|
||||
val expectedAmount = currentAmount
|
||||
val amount = currentAmount
|
||||
|
||||
launch {
|
||||
val chain = chainRegistry.getChain(chainId)
|
||||
val chainAsset = chain.assetsById.getValue(assetId)
|
||||
val accountId = BRIDGE_ADDRESS_GENERIC.toAccountId()
|
||||
val bridgeAddress = chain.addressOf(accountId)
|
||||
|
||||
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,
|
||||
router.openBridgeExecution(
|
||||
BridgeExecutionPayload(
|
||||
originChainId = chainId,
|
||||
originAssetId = assetId,
|
||||
bridgeAddress = bridgeAddress,
|
||||
amount = amount,
|
||||
destChainId = destChainId,
|
||||
destAssetId = destAssetId,
|
||||
)
|
||||
)
|
||||
|
||||
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 = {
|
||||
// The origin transfer already happened - clear the stale entered amount so the
|
||||
// live balance-vs-amount check (updateInsufficientBalanceState/updateButtonState)
|
||||
// doesn't flag the now-lower post-transfer balance against an amount that was
|
||||
// already spent. Without this, a real successful transfer and a false "insufficient
|
||||
// balance" error appear side by side, which reads as if something failed twice.
|
||||
_fillAmountEvent.postValue(Event("0"))
|
||||
|
||||
// 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
|
||||
* 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))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
-8
@@ -15,10 +15,8 @@ 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
|
||||
@@ -47,9 +45,6 @@ class BridgeModule {
|
||||
assetIconProvider: AssetIconProvider,
|
||||
walletInteractor: WalletInteractor,
|
||||
bridgeMultisigInteractor: BridgeMultisigInteractor,
|
||||
sendInteractor: SendInteractor,
|
||||
sendUseCase: SendUseCase,
|
||||
selectedAccountUseCase: SelectedAccountUseCase
|
||||
): ViewModel {
|
||||
return BridgeViewModel(
|
||||
router,
|
||||
@@ -58,9 +53,6 @@ class BridgeModule {
|
||||
assetIconProvider,
|
||||
walletInteractor,
|
||||
bridgeMultisigInteractor,
|
||||
sendInteractor,
|
||||
sendUseCase,
|
||||
selectedAccountUseCase
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
+84
@@ -0,0 +1,84 @@
|
||||
package io.novafoundation.nova.feature_assets.presentation.bridge.execution
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import io.novafoundation.nova.common.base.BaseFragment
|
||||
import io.novafoundation.nova.common.di.FeatureUtils
|
||||
import io.novafoundation.nova.common.view.AlertView
|
||||
import io.novafoundation.nova.common.view.setState
|
||||
import io.novafoundation.nova.feature_assets.R
|
||||
import io.novafoundation.nova.feature_assets.databinding.FragmentBridgeExecutionBinding
|
||||
import io.novafoundation.nova.feature_assets.di.AssetsFeatureApi
|
||||
import io.novafoundation.nova.feature_assets.di.AssetsFeatureComponent
|
||||
|
||||
private const val KEY_PAYLOAD = "KEY_PAYLOAD"
|
||||
|
||||
class BridgeExecutionFragment : BaseFragment<BridgeExecutionViewModel, FragmentBridgeExecutionBinding>() {
|
||||
|
||||
companion object {
|
||||
|
||||
fun getBundle(payload: BridgeExecutionPayload) = Bundle().apply {
|
||||
putParcelable(KEY_PAYLOAD, payload)
|
||||
}
|
||||
}
|
||||
|
||||
override fun createBinding() = FragmentBridgeExecutionBinding.inflate(layoutInflater)
|
||||
|
||||
override fun initViews() {
|
||||
binder.bridgeExecutionToolbar.setHomeButtonListener { viewModel.doneClicked() }
|
||||
|
||||
binder.bridgeExecutionDoneButton.setOnClickListener { viewModel.doneClicked() }
|
||||
}
|
||||
|
||||
override fun inject() {
|
||||
val payload = argument<BridgeExecutionPayload>(KEY_PAYLOAD)
|
||||
|
||||
FeatureUtils.getFeature<AssetsFeatureComponent>(
|
||||
requireContext(),
|
||||
AssetsFeatureApi::class.java
|
||||
)
|
||||
.bridgeExecutionComponentFactory()
|
||||
.create(this, payload)
|
||||
.inject(this)
|
||||
}
|
||||
|
||||
override fun subscribe(viewModel: BridgeExecutionViewModel) {
|
||||
viewModel.label.observe { text ->
|
||||
binder.bridgeExecutionLabel.text = text
|
||||
}
|
||||
|
||||
viewModel.timerState.observe { state ->
|
||||
if (state == null) {
|
||||
binder.bridgeExecutionTimer.visibility = View.GONE
|
||||
} else {
|
||||
binder.bridgeExecutionTimer.visibility = View.VISIBLE
|
||||
binder.bridgeExecutionTimer.setState(state)
|
||||
}
|
||||
}
|
||||
|
||||
viewModel.doneButtonVisible.observe { visible ->
|
||||
binder.bridgeExecutionDoneButton.visibility = if (visible) View.VISIBLE else View.GONE
|
||||
}
|
||||
|
||||
// Single source of truth for the alert banner - only OriginFailed (error) and
|
||||
// DestinationPendingReview (warning) show it, everything else keeps it hidden. Avoids the
|
||||
// old screen's problem of several independent LiveData all touching the same view.
|
||||
viewModel.state.observe { state ->
|
||||
when (state) {
|
||||
is BridgeExecutionState.OriginFailed -> {
|
||||
binder.bridgeExecutionPendingReviewAlert.visibility = View.VISIBLE
|
||||
binder.bridgeExecutionPendingReviewAlert.setStylePreset(AlertView.StylePreset.ERROR)
|
||||
binder.bridgeExecutionPendingReviewAlert.setMessage(state.message)
|
||||
}
|
||||
BridgeExecutionState.DestinationPendingReview -> {
|
||||
binder.bridgeExecutionPendingReviewAlert.visibility = View.VISIBLE
|
||||
binder.bridgeExecutionPendingReviewAlert.setStylePreset(AlertView.StylePreset.WARNING)
|
||||
binder.bridgeExecutionPendingReviewAlert.setMessage(getString(R.string.bridge_deposit_pending_message))
|
||||
}
|
||||
else -> {
|
||||
binder.bridgeExecutionPendingReviewAlert.visibility = View.GONE
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
package io.novafoundation.nova.feature_assets.presentation.bridge.execution
|
||||
|
||||
import android.os.Parcelable
|
||||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
/**
|
||||
* Everything BridgeExecutionViewModel needs to submit the origin transfer and watch for the
|
||||
* destination credit - deliberately just ids/amounts (not display strings), matching this
|
||||
* codebase's AssetPayload convention: the execution screen re-derives chain/asset display data
|
||||
* itself from ChainRegistry rather than trusting stale strings carried across navigation.
|
||||
*/
|
||||
@Parcelize
|
||||
class BridgeExecutionPayload(
|
||||
val originChainId: String,
|
||||
val originAssetId: Int,
|
||||
val bridgeAddress: String,
|
||||
val amount: Double,
|
||||
val destChainId: String,
|
||||
val destAssetId: Int,
|
||||
) : Parcelable
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
package io.novafoundation.nova.feature_assets.presentation.bridge.execution
|
||||
|
||||
/**
|
||||
* Every outcome the bridge execution screen can actually be in - the single source of truth the
|
||||
* UI renders from, instead of several independent booleans/LiveData (isExecuting, showWarning,
|
||||
* warningBlocked, depositWaitLabelVisible, ...) that each had to be kept in sync by hand on every
|
||||
* code path. A state that isn't listed here isn't a state this screen can reach.
|
||||
*/
|
||||
sealed class BridgeExecutionState {
|
||||
|
||||
/** Submitting the real on-chain transfer on the origin chain and awaiting its own dispatch
|
||||
* result - nothing has left the wallet's control yet as far as this screen can tell. */
|
||||
object SubmittingOrigin : BridgeExecutionState()
|
||||
|
||||
/** The origin dispatch itself failed or was rejected (e.g. fee changed, node rejected it) -
|
||||
* no funds moved. Safe to retry from scratch. */
|
||||
data class OriginFailed(val message: String) : BridgeExecutionState()
|
||||
|
||||
/** Origin transfer confirmed on-chain - funds have left the wallet. Actively watching the
|
||||
* destination balance for a real increase; the bridge's off-chain relay step has no
|
||||
* callback this wallet can observe directly, so balance-delta is the most honest signal
|
||||
* available. */
|
||||
object WaitingForDestination : BridgeExecutionState()
|
||||
|
||||
/** Destination balance increase actually observed within the wait window. */
|
||||
object DestinationConfirmed : BridgeExecutionState()
|
||||
|
||||
/** Not confirmed within the wait window - NOT a failure (the origin transfer already
|
||||
* succeeded and is not reversible), just not observably complete yet. Could mean auto-pay
|
||||
* is still in flight, or the amount exceeded the bridge's auto-pay bounds/reserve and is
|
||||
* queued for manual 3-of-5 review, which can take longer than this screen waits. */
|
||||
object DestinationPendingReview : BridgeExecutionState()
|
||||
}
|
||||
+173
@@ -0,0 +1,173 @@
|
||||
package io.novafoundation.nova.feature_assets.presentation.bridge.execution
|
||||
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import io.novafoundation.nova.common.base.BaseViewModel
|
||||
import io.novafoundation.nova.common.resources.ResourceManager
|
||||
import io.novafoundation.nova.common.view.ExecutionTimerView
|
||||
import io.novafoundation.nova.feature_account_api.data.fee.FeePaymentCurrency
|
||||
import io.novafoundation.nova.feature_account_api.domain.interfaces.SelectedAccountUseCase
|
||||
import io.novafoundation.nova.feature_assets.R
|
||||
import io.novafoundation.nova.feature_assets.domain.WalletInteractor
|
||||
import io.novafoundation.nova.feature_assets.domain.send.SendInteractor
|
||||
import io.novafoundation.nova.feature_assets.presentation.AssetsRouter
|
||||
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.multiNetwork.ChainRegistry
|
||||
import io.novafoundation.nova.runtime.multiNetwork.ChainWithAsset
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.withTimeoutOrNull
|
||||
import java.math.BigDecimal
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
|
||||
/**
|
||||
* Dedicated execution screen for a bridge transfer - deliberately has no amount input and no live
|
||||
* balance-vs-amount validation, structurally the same way SwapExecutionViewModel has none: that
|
||||
* code simply doesn't exist here, so it can't race against a live balance update the way it did
|
||||
* when submit+wait lived on the same screen/ViewModel as the amount field (see
|
||||
* BridgeViewModel.swapClicked - the confirmation gate - for where amount/reserve validation now
|
||||
* happens instead, once, right before navigating here).
|
||||
*/
|
||||
class BridgeExecutionViewModel(
|
||||
private val payload: BridgeExecutionPayload,
|
||||
private val resourceManager: ResourceManager,
|
||||
private val chainRegistry: ChainRegistry,
|
||||
private val walletInteractor: WalletInteractor,
|
||||
private val sendInteractor: SendInteractor,
|
||||
private val sendUseCase: SendUseCase,
|
||||
private val selectedAccountUseCase: SelectedAccountUseCase,
|
||||
private val router: AssetsRouter,
|
||||
) : BaseViewModel() {
|
||||
|
||||
companion object {
|
||||
/** 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
|
||||
|
||||
/** 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
|
||||
|
||||
const val FEE_PERCENT = 0.001
|
||||
}
|
||||
|
||||
private val _state = MutableLiveData<BridgeExecutionState>(BridgeExecutionState.SubmittingOrigin)
|
||||
val state: LiveData<BridgeExecutionState> = _state
|
||||
|
||||
private val _label = MutableLiveData<String>()
|
||||
val label: LiveData<String> = _label
|
||||
|
||||
private val _timerState = MutableLiveData<ExecutionTimerView.State?>(null)
|
||||
val timerState: LiveData<ExecutionTimerView.State?> = _timerState
|
||||
|
||||
private val _doneButtonVisible = MutableLiveData(false)
|
||||
val doneButtonVisible: LiveData<Boolean> = _doneButtonVisible
|
||||
|
||||
init {
|
||||
launch {
|
||||
submit()
|
||||
}
|
||||
}
|
||||
|
||||
fun doneClicked() {
|
||||
router.back()
|
||||
}
|
||||
|
||||
private suspend fun submit() {
|
||||
_state.postValue(BridgeExecutionState.SubmittingOrigin)
|
||||
_label.postValue(resourceManager.getString(R.string.bridge_execution_submitting_label))
|
||||
_timerState.postValue(ExecutionTimerView.State.CountdownTimer(SUBMIT_WAIT_TIMEOUT))
|
||||
|
||||
val chain = chainRegistry.getChain(payload.originChainId)
|
||||
val chainAsset = chain.assetsById.getValue(payload.originAssetId)
|
||||
val chainWithAsset = ChainWithAsset(chain, chainAsset)
|
||||
val bigDecimalAmount = BigDecimal.valueOf(payload.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 = payload.bridgeAddress,
|
||||
)
|
||||
|
||||
val fee = sendInteractor.getFee(assetTransfer, viewModelScope)
|
||||
|
||||
val weightedTransfer = WeightedAssetTransfer(
|
||||
sender = metaAccount,
|
||||
recipient = payload.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 = { observeDepositCompletion() },
|
||||
onFailure = { e ->
|
||||
_state.postValue(BridgeExecutionState.OriginFailed(e.message ?: resourceManager.getString(R.string.bridge_deposit_pending_message)))
|
||||
_timerState.postValue(ExecutionTimerView.State.Error)
|
||||
_doneButtonVisible.postValue(true)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
/** Watches the DESTINATION balance for a real increase after the origin transfer already
|
||||
* confirmed, rather than a cosmetic timer that reports "done" regardless of whether funds
|
||||
* actually arrived - there is no completion callback for the bridge's own off-chain relay
|
||||
* step, 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 suspend fun observeDepositCompletion() {
|
||||
val balanceBefore = try {
|
||||
walletInteractor.assetFlow(payload.destChainId, payload.destAssetId).first().transferable
|
||||
} catch (e: Exception) {
|
||||
// Can't observe reliably - don't show a misleading progress state.
|
||||
_state.postValue(BridgeExecutionState.DestinationPendingReview)
|
||||
_timerState.postValue(null)
|
||||
_doneButtonVisible.postValue(true)
|
||||
return
|
||||
}
|
||||
|
||||
val destChainName = chainRegistry.getChain(payload.destChainId).name
|
||||
|
||||
_state.postValue(BridgeExecutionState.WaitingForDestination)
|
||||
_label.postValue(resourceManager.getString(R.string.bridge_execution_waiting_destination_label, destChainName))
|
||||
_timerState.postValue(ExecutionTimerView.State.CountdownTimer(DEPOSIT_WAIT_TIMEOUT))
|
||||
|
||||
val minExpectedIncrease = BigDecimal.valueOf(payload.amount * (1 - FEE_PERCENT * 2)) // fee + rounding slack
|
||||
|
||||
val confirmed = withTimeoutOrNull(DEPOSIT_WAIT_TIMEOUT.inWholeMilliseconds) {
|
||||
walletInteractor.assetFlow(payload.destChainId, payload.destAssetId)
|
||||
.first { it.transferable - balanceBefore >= minExpectedIncrease }
|
||||
} != null
|
||||
|
||||
if (confirmed) {
|
||||
_state.postValue(BridgeExecutionState.DestinationConfirmed)
|
||||
_label.postValue(resourceManager.getString(R.string.bridge_deposit_success_label))
|
||||
_timerState.postValue(ExecutionTimerView.State.Success)
|
||||
} else {
|
||||
// Not a failure - just not confirmed within the wait window.
|
||||
_state.postValue(BridgeExecutionState.DestinationPendingReview)
|
||||
_timerState.postValue(null)
|
||||
}
|
||||
|
||||
_doneButtonVisible.postValue(true)
|
||||
}
|
||||
}
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
package io.novafoundation.nova.feature_assets.presentation.bridge.execution.di
|
||||
|
||||
import androidx.fragment.app.Fragment
|
||||
import dagger.BindsInstance
|
||||
import dagger.Subcomponent
|
||||
import io.novafoundation.nova.common.di.scope.ScreenScope
|
||||
import io.novafoundation.nova.feature_assets.presentation.bridge.execution.BridgeExecutionFragment
|
||||
import io.novafoundation.nova.feature_assets.presentation.bridge.execution.BridgeExecutionPayload
|
||||
|
||||
@Subcomponent(
|
||||
modules = [
|
||||
BridgeExecutionModule::class
|
||||
]
|
||||
)
|
||||
@ScreenScope
|
||||
interface BridgeExecutionComponent {
|
||||
|
||||
@Subcomponent.Factory
|
||||
interface Factory {
|
||||
|
||||
fun create(
|
||||
@BindsInstance fragment: Fragment,
|
||||
@BindsInstance payload: BridgeExecutionPayload
|
||||
): BridgeExecutionComponent
|
||||
}
|
||||
|
||||
fun inject(fragment: BridgeExecutionFragment)
|
||||
}
|
||||
+56
@@ -0,0 +1,56 @@
|
||||
package io.novafoundation.nova.feature_assets.presentation.bridge.execution.di
|
||||
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.multibindings.IntoMap
|
||||
import io.novafoundation.nova.common.di.viewmodel.ViewModelKey
|
||||
import io.novafoundation.nova.common.di.viewmodel.ViewModelModule
|
||||
import io.novafoundation.nova.common.resources.ResourceManager
|
||||
import io.novafoundation.nova.feature_account_api.domain.interfaces.SelectedAccountUseCase
|
||||
import io.novafoundation.nova.feature_assets.domain.WalletInteractor
|
||||
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.execution.BridgeExecutionPayload
|
||||
import io.novafoundation.nova.feature_assets.presentation.bridge.execution.BridgeExecutionViewModel
|
||||
import io.novafoundation.nova.feature_wallet_api.domain.SendUseCase
|
||||
import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry
|
||||
|
||||
@Module(includes = [ViewModelModule::class])
|
||||
class BridgeExecutionModule {
|
||||
|
||||
@Provides
|
||||
@IntoMap
|
||||
@ViewModelKey(BridgeExecutionViewModel::class)
|
||||
fun provideViewModel(
|
||||
payload: BridgeExecutionPayload,
|
||||
resourceManager: ResourceManager,
|
||||
chainRegistry: ChainRegistry,
|
||||
walletInteractor: WalletInteractor,
|
||||
sendInteractor: SendInteractor,
|
||||
sendUseCase: SendUseCase,
|
||||
selectedAccountUseCase: SelectedAccountUseCase,
|
||||
router: AssetsRouter,
|
||||
): ViewModel {
|
||||
return BridgeExecutionViewModel(
|
||||
payload,
|
||||
resourceManager,
|
||||
chainRegistry,
|
||||
walletInteractor,
|
||||
sendInteractor,
|
||||
sendUseCase,
|
||||
selectedAccountUseCase,
|
||||
router
|
||||
)
|
||||
}
|
||||
|
||||
@Provides
|
||||
fun provideViewModelCreator(
|
||||
fragment: Fragment,
|
||||
viewModelFactory: ViewModelProvider.Factory
|
||||
): BridgeExecutionViewModel {
|
||||
return ViewModelProvider(fragment, viewModelFactory).get(BridgeExecutionViewModel::class.java)
|
||||
}
|
||||
}
|
||||
@@ -190,28 +190,6 @@
|
||||
android:text="@string/bridge_sign_button"
|
||||
android:visibility="gone" />
|
||||
|
||||
<!-- Deposit-wait progress: shown after tapping Swap, while we actively watch the
|
||||
destination balance for a real confirmed increase - not a cosmetic timer that
|
||||
completes regardless of whether funds actually arrived. Same reusable component
|
||||
already used for swap execution (common.view.ExecutionTimerView). -->
|
||||
<TextView
|
||||
android:id="@+id/bridgeDepositWaitLabel"
|
||||
style="@style/TextAppearance.NovaFoundation.Regular.Footnote"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="24dp"
|
||||
android:gravity="center"
|
||||
android:text="@string/bridge_deposit_wait_label"
|
||||
android:textColor="@color/text_secondary"
|
||||
android:visibility="gone" />
|
||||
|
||||
<io.novafoundation.nova.common.view.ExecutionTimerView
|
||||
android:id="@+id/bridgeExecutionTimer"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:visibility="gone" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</ScrollView>
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="@color/secondary_screen_background">
|
||||
|
||||
<io.novafoundation.nova.common.view.Toolbar
|
||||
android:id="@+id/bridgeExecutionToolbar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:titleText="@string/bridge_title" />
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
android:gravity="center"
|
||||
android:orientation="vertical"
|
||||
android:padding="24dp"
|
||||
app:layout_constraintBottom_toTopOf="@id/bridgeExecutionDoneButton"
|
||||
app:layout_constraintTop_toBottomOf="@id/bridgeExecutionToolbar">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/bridgeExecutionLabel"
|
||||
style="@style/TextAppearance.NovaFoundation.Regular.Footnote"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center"
|
||||
android:textColor="@color/text_secondary"
|
||||
tools:text="Waiting for confirmation on Pezkuwi Asset Hub…" />
|
||||
|
||||
<io.novafoundation.nova.common.view.ExecutionTimerView
|
||||
android:id="@+id/bridgeExecutionTimer"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="16dp" />
|
||||
|
||||
<io.novafoundation.nova.common.view.AlertView
|
||||
android:id="@+id/bridgeExecutionPendingReviewAlert"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="24dp"
|
||||
android:visibility="gone"
|
||||
app:alertMode="warning" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<io.novafoundation.nova.common.view.PrimaryButton
|
||||
android:id="@+id/bridgeExecutionDoneButton"
|
||||
style="@style/Widget.Nova.Button.Primary"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_margin="16dp"
|
||||
android:text="@string/common_done"
|
||||
android:visibility="gone"
|
||||
app:layout_constraintBottom_toBottomOf="parent" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
Reference in New Issue
Block a user