mirror of
https://github.com/pezkuwichain/pezkuwi-wallet-android.git
synced 2026-07-22 20:45:53 +00:00
Bridge: add retry on origin failure, dedupe fee constant
Retry: OriginFailed only offered "Done" (start over from the input screen) - the origin transfer never left the wallet in that state, so retrying is just re-running submit() from scratch, same as Swap's own retryClicked(). The execution screen's action button now shows "Try again"/retries when OriginFailed, "Done" otherwise - derived from the same state observer already driving the alert banner, not a second LiveData to keep in sync. FEE_PERCENT was duplicated as separate 0.001 literals in BridgeViewModel and BridgeExecutionViewModel. Moved to BridgeMultisigConstants (already the shared home for this feature's cross-cutting constants) so there's one number to change if the server-side fee_basis_points ever does. Deliberately NOT touching the deeper architecture items (LiveData vs StateFlow, extracting submit logic to a domain interactor) - Bridge's sealed BridgeExecutionState already gives it the single-source-of-truth property that motivated Swap's StateFlow design, and its submit/wait logic isn't shared with any other screen, so there's no duplication or desync bug those changes would actually fix here. This scope is staying USDT.p<->USDT only going forward (other bridge/swap pairs route through the separate pezsnowbridge DApp) - the real trigger for that refactor (a second bridge pair reusing this logic) will not happen in this screen, so it's not deferred tech debt, it's a closed decision.
This commit is contained in:
+5
@@ -44,6 +44,11 @@ object BridgeMultisigConstants {
|
||||
|
||||
const val THRESHOLD = 3
|
||||
|
||||
/** Single source of truth for the bridge fee - was previously duplicated as separate literals
|
||||
* in BridgeViewModel and BridgeExecutionViewModel, both computing the same 1:1-minus-fee
|
||||
* output. Must match usdt-bridge's own `fee_basis_points` (10 = 0.1%) server-side. */
|
||||
const val FEE_PERCENT = 0.001
|
||||
|
||||
/** Hard per-transaction ceiling the automation key may ever execute on its own, regardless of
|
||||
* how much on-chain approval remains - must match usdt-bridge's own `max_single_tx`
|
||||
* (default_max_single_tx() in bridge_config.json). A single withdrawal/deposit above this is
|
||||
|
||||
+1
-2
@@ -69,7 +69,6 @@ class BridgeViewModel(
|
||||
const val POLKADOT_USDT_ASSET_ID = 1 // assetId in chains.json for Polkadot AH
|
||||
const val PEZKUWI_USDT_ASSET_ID = 1000 // assetId in chains.json for Pezkuwi AH
|
||||
|
||||
const val FEE_PERCENT = 0.001
|
||||
const val MIN_USDT = 1.0
|
||||
|
||||
// USDT has 6 decimals on both Polkadot and Pezkuwi Asset Hub.
|
||||
@@ -409,7 +408,7 @@ class BridgeViewModel(
|
||||
}
|
||||
|
||||
private fun calculateOutput() {
|
||||
val netOutput = currentAmount * (1 - FEE_PERCENT) // 1:1, fee-adjusted
|
||||
val netOutput = currentAmount * (1 - BridgeMultisigConstants.FEE_PERCENT) // 1:1, fee-adjusted
|
||||
|
||||
_outputAmount.value = if (netOutput > 0) {
|
||||
BigDecimal(netOutput).setScale(6, RoundingMode.DOWN).stripTrailingZeros().toPlainString()
|
||||
|
||||
+12
-2
@@ -33,8 +33,6 @@ class BridgeExecutionFragment : BaseFragment<BridgeExecutionViewModel, FragmentB
|
||||
|
||||
binder.bridgeExecutionToolbar.setHomeButtonVisibility(false)
|
||||
|
||||
binder.bridgeExecutionDoneButton.setOnClickListener { viewModel.doneClicked() }
|
||||
|
||||
binder.bridgeExecutionFromCard.setEditable(false)
|
||||
binder.bridgeExecutionToCard.setEditable(false)
|
||||
}
|
||||
@@ -93,20 +91,32 @@ class BridgeExecutionFragment : BaseFragment<BridgeExecutionViewModel, FragmentB
|
||||
// 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.
|
||||
//
|
||||
// Also the single source of truth for the action button's label/click target: only
|
||||
// OriginFailed offers a real retry (the origin transfer never left the wallet, so
|
||||
// re-running submit() from scratch is safe and matches SwapExecutionFragment's
|
||||
// "Try again" pattern) - every other resolved state means something already happened
|
||||
// on-chain, so the button can only ever mean "Done".
|
||||
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)
|
||||
binder.bridgeExecutionDoneButton.setText(R.string.common_try_again)
|
||||
binder.bridgeExecutionDoneButton.setOnClickListener { viewModel.retryClicked() }
|
||||
}
|
||||
BridgeExecutionState.DestinationPendingReview, BridgeExecutionState.AwaitingManualReview -> {
|
||||
binder.bridgeExecutionPendingReviewAlert.visibility = View.VISIBLE
|
||||
binder.bridgeExecutionPendingReviewAlert.setStylePreset(AlertView.StylePreset.WARNING)
|
||||
binder.bridgeExecutionPendingReviewAlert.setMessage(getString(R.string.bridge_deposit_pending_message))
|
||||
binder.bridgeExecutionDoneButton.setText(R.string.common_done)
|
||||
binder.bridgeExecutionDoneButton.setOnClickListener { viewModel.doneClicked() }
|
||||
}
|
||||
else -> {
|
||||
binder.bridgeExecutionPendingReviewAlert.visibility = View.GONE
|
||||
binder.bridgeExecutionDoneButton.setText(R.string.common_done)
|
||||
binder.bridgeExecutionDoneButton.setOnClickListener { viewModel.doneClicked() }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+12
-4
@@ -12,6 +12,7 @@ import io.novafoundation.nova.feature_account_api.domain.interfaces.SelectedAcco
|
||||
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.send.SendInteractor
|
||||
import io.novafoundation.nova.feature_assets.presentation.AssetsRouter
|
||||
import io.novafoundation.nova.feature_assets.presentation.bridge.BridgeAssetCardUi
|
||||
@@ -57,8 +58,6 @@ class BridgeExecutionViewModel(
|
||||
/** 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)
|
||||
@@ -98,6 +97,15 @@ class BridgeExecutionViewModel(
|
||||
router.back()
|
||||
}
|
||||
|
||||
/** Only meaningful from OriginFailed - the origin transfer never left the wallet (nothing to
|
||||
* reverse), so retrying is just re-running submit() from scratch, matching what the user
|
||||
* already confirmed on the input screen. Mirrors SwapExecutionViewModel.retryClicked(). */
|
||||
fun retryClicked() {
|
||||
launch {
|
||||
submit()
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun loadCards() {
|
||||
val originChain = chainRegistry.getChain(payload.originChainId)
|
||||
val destChain = chainRegistry.getChain(payload.destChainId)
|
||||
@@ -107,7 +115,7 @@ class BridgeExecutionViewModel(
|
||||
|
||||
_fromAmountText.postValue(BigDecimal.valueOf(payload.amount).stripTrailingZeros().toPlainString())
|
||||
|
||||
val netOutput = payload.amount * (1 - FEE_PERCENT)
|
||||
val netOutput = payload.amount * (1 - BridgeMultisigConstants.FEE_PERCENT)
|
||||
_toAmountText.postValue(BigDecimal(netOutput).setScale(6, RoundingMode.DOWN).stripTrailingZeros().toPlainString())
|
||||
}
|
||||
|
||||
@@ -211,7 +219,7 @@ class BridgeExecutionViewModel(
|
||||
_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 minExpectedIncrease = BigDecimal.valueOf(payload.amount * (1 - BridgeMultisigConstants.FEE_PERCENT * 2)) // fee + rounding slack
|
||||
|
||||
val confirmed = withTimeoutOrNull(DEPOSIT_WAIT_TIMEOUT.inWholeMilliseconds) {
|
||||
walletInteractor.assetFlow(payload.destChainId, payload.destAssetId)
|
||||
|
||||
Reference in New Issue
Block a user