mirror of
https://github.com/pezkuwichain/pezkuwi-wallet-android.git
synced 2026-07-22 13:45:48 +00:00
feat: block Bridge swap when send amount exceeds available balance
The Bridge screen let a user type any amount and enabled the Swap button regardless of what they actually held, unlike the Swap screen which shows a live "Max: X" and disables on overflow. The confirm-step ValidationSystem would still catch it before signing, but only after routing through Send - no immediate feedback on the Bridge screen itself. Wires WalletInteractor.assetFlow(chainId, assetId) to the currently selected origin side (re-subscribed on every direction/pair change), tracking Asset.transferable - the same field the real transfer validation checks. Reuses the existing MaxAmountView/MaxAvailableView widget the Swap screen already uses for the "Max: X" display, and adds a matching error-bordered state to BridgeAssetInputView. The "Insufficient balance" string (bridge_insufficient_balance) was already translated into every locale but never wired to any code.
This commit is contained in:
+17
@@ -10,6 +10,7 @@ import io.novafoundation.nova.common.view.setState
|
||||
import io.novafoundation.nova.feature_assets.databinding.FragmentBridgeBinding
|
||||
import io.novafoundation.nova.feature_assets.di.AssetsFeatureApi
|
||||
import io.novafoundation.nova.feature_assets.di.AssetsFeatureComponent
|
||||
import io.novafoundation.nova.feature_wallet_api.presentation.mixin.amountChooser.MaxActionAvailability
|
||||
|
||||
class BridgeFragment : BaseFragment<BridgeViewModel, FragmentBridgeBinding>() {
|
||||
|
||||
@@ -49,6 +50,10 @@ class BridgeFragment : BaseFragment<BridgeViewModel, FragmentBridgeBinding>() {
|
||||
}
|
||||
})
|
||||
|
||||
binder.bridgeFromMaxAmount.setMaxActionAvailability(
|
||||
MaxActionAvailability.Available { viewModel.maxClicked() }
|
||||
)
|
||||
|
||||
// Swap button
|
||||
binder.bridgeSwapButton.setOnClickListener {
|
||||
viewModel.swapClicked()
|
||||
@@ -105,6 +110,18 @@ class BridgeFragment : BaseFragment<BridgeViewModel, FragmentBridgeBinding>() {
|
||||
binder.bridgeWarningAlert.setMessage(text)
|
||||
}
|
||||
}
|
||||
|
||||
viewModel.maxAmountDisplay.observe { display ->
|
||||
binder.bridgeFromMaxAmount.setMaxAmountDisplay(display)
|
||||
}
|
||||
|
||||
viewModel.insufficientBalanceError.observe { error ->
|
||||
binder.bridgeFromCard.setError(error)
|
||||
}
|
||||
|
||||
viewModel.fillAmountEvent.observeEvent { amount ->
|
||||
binder.bridgeFromCard.amountInput.setText(amount)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+46
-1
@@ -5,10 +5,12 @@ import androidx.lifecycle.MutableLiveData
|
||||
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.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.presentation.AssetsRouter
|
||||
import io.novafoundation.nova.feature_assets.presentation.send.amount.SendPayload
|
||||
import io.novafoundation.nova.feature_wallet_api.presentation.model.AssetPayload
|
||||
@@ -18,6 +20,7 @@ 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.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.json.JSONObject
|
||||
@@ -29,7 +32,8 @@ class BridgeViewModel(
|
||||
private val router: AssetsRouter,
|
||||
private val resourceManager: ResourceManager,
|
||||
private val chainRegistry: ChainRegistry,
|
||||
private val assetIconProvider: AssetIconProvider
|
||||
private val assetIconProvider: AssetIconProvider,
|
||||
private val walletInteractor: WalletInteractor
|
||||
) : BaseViewModel() {
|
||||
|
||||
companion object {
|
||||
@@ -90,10 +94,21 @@ class BridgeViewModel(
|
||||
private val _pairOptions = MutableLiveData<List<BridgePairUi>>(emptyList())
|
||||
val pairOptions: LiveData<List<BridgePairUi>> = _pairOptions
|
||||
|
||||
private val _maxAmountDisplay = MutableLiveData<String?>(null)
|
||||
val maxAmountDisplay: LiveData<String?> = _maxAmountDisplay
|
||||
|
||||
private val _insufficientBalanceError = MutableLiveData<String?>(null)
|
||||
val insufficientBalanceError: LiveData<String?> = _insufficientBalanceError
|
||||
|
||||
private val _fillAmountEvent = MutableLiveData<Event<String>>()
|
||||
val fillAmountEvent: LiveData<Event<String>> = _fillAmountEvent
|
||||
|
||||
private var currentAmount: Double = 0.0
|
||||
private var dotToHezRate: Double = FALLBACK_RATE
|
||||
private var isHezToDotActive: Boolean = false
|
||||
private var isWusdtToUsdtActive: Boolean = false
|
||||
private var availableBalance: BigDecimal = BigDecimal.ZERO
|
||||
private var balanceJob: Job? = null
|
||||
|
||||
init {
|
||||
fetchExchangeRate()
|
||||
@@ -150,9 +165,14 @@ class BridgeViewModel(
|
||||
fun setAmount(amount: Double) {
|
||||
currentAmount = amount
|
||||
calculateOutput()
|
||||
updateInsufficientBalanceState()
|
||||
updateButtonState()
|
||||
}
|
||||
|
||||
fun maxClicked() {
|
||||
_fillAmountEvent.value = Event(availableBalance.stripTrailingZeros().toPlainString())
|
||||
}
|
||||
|
||||
fun swapClicked() {
|
||||
val dir = _direction.value ?: return
|
||||
if (currentAmount <= 0) return
|
||||
@@ -332,12 +352,21 @@ class BridgeViewModel(
|
||||
_buttonState.value = when {
|
||||
currentAmount <= 0 -> ButtonState.DISABLED
|
||||
currentAmount < minimum -> ButtonState.DISABLED
|
||||
BigDecimal.valueOf(currentAmount) > availableBalance -> ButtonState.DISABLED
|
||||
dir == BridgeDirection.HEZ_TO_DOT && !isHezToDotActive -> ButtonState.DISABLED
|
||||
dir == BridgeDirection.WUSDT_TO_USDT && !isWusdtToUsdtActive -> ButtonState.DISABLED
|
||||
else -> ButtonState.NORMAL
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateInsufficientBalanceState() {
|
||||
_insufficientBalanceError.value = if (currentAmount > 0 && BigDecimal.valueOf(currentAmount) > availableBalance) {
|
||||
resourceManager.getString(R.string.bridge_insufficient_balance)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
fun refreshBridgeStatus() {
|
||||
fetchBridgeStatus()
|
||||
}
|
||||
@@ -370,6 +399,22 @@ class BridgeViewModel(
|
||||
_fromCard.value = cardUiFor(originChainId, originAssetId)
|
||||
_toCard.value = cardUiFor(destChainId, destAssetId)
|
||||
}
|
||||
|
||||
observeOriginBalance(originChainId, originAssetId)
|
||||
}
|
||||
|
||||
private fun observeOriginBalance(chainId: String, assetId: Int) {
|
||||
balanceJob?.cancel()
|
||||
balanceJob = launch {
|
||||
walletInteractor.assetFlow(chainId, assetId).collect { asset ->
|
||||
availableBalance = asset.transferable
|
||||
_maxAmountDisplay.postValue(
|
||||
"${availableBalance.setScale(6, RoundingMode.DOWN).stripTrailingZeros().toPlainString()} ${asset.token.configuration.symbol.value}"
|
||||
)
|
||||
updateInsufficientBalanceState()
|
||||
updateButtonState()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun loadPairOptions() {
|
||||
|
||||
+4
-2
@@ -10,6 +10,7 @@ import io.novafoundation.nova.common.di.viewmodel.ViewModelKey
|
||||
import io.novafoundation.nova.common.di.viewmodel.ViewModelModule
|
||||
import io.novafoundation.nova.common.presentation.AssetIconProvider
|
||||
import io.novafoundation.nova.common.resources.ResourceManager
|
||||
import io.novafoundation.nova.feature_assets.domain.WalletInteractor
|
||||
import io.novafoundation.nova.feature_assets.presentation.AssetsRouter
|
||||
import io.novafoundation.nova.feature_assets.presentation.bridge.BridgeViewModel
|
||||
import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry
|
||||
@@ -24,9 +25,10 @@ class BridgeModule {
|
||||
router: AssetsRouter,
|
||||
resourceManager: ResourceManager,
|
||||
chainRegistry: ChainRegistry,
|
||||
assetIconProvider: AssetIconProvider
|
||||
assetIconProvider: AssetIconProvider,
|
||||
walletInteractor: WalletInteractor
|
||||
): ViewModel {
|
||||
return BridgeViewModel(router, resourceManager, chainRegistry, assetIconProvider)
|
||||
return BridgeViewModel(router, resourceManager, chainRegistry, assetIconProvider, walletInteractor)
|
||||
}
|
||||
|
||||
@Provides
|
||||
|
||||
+13
@@ -11,7 +11,9 @@ import io.novafoundation.nova.common.utils.WithContextExtensions
|
||||
import io.novafoundation.nova.common.utils.images.asUrlIcon
|
||||
import io.novafoundation.nova.common.utils.images.setIconOrMakeGone
|
||||
import io.novafoundation.nova.common.utils.inflater
|
||||
import io.novafoundation.nova.common.utils.setVisible
|
||||
import io.novafoundation.nova.common.view.shape.getInputBackground
|
||||
import io.novafoundation.nova.common.view.shape.getInputBackgroundError
|
||||
import io.novafoundation.nova.feature_account_api.presenatation.chain.setTokenIcon
|
||||
import io.novafoundation.nova.feature_assets.databinding.ViewBridgeAssetInputBinding
|
||||
import io.novafoundation.nova.feature_assets.presentation.bridge.BridgeAssetCardUi
|
||||
@@ -60,4 +62,15 @@ class BridgeAssetInputView @JvmOverloads constructor(
|
||||
binder.bridgeAssetInputSubtitle.text = model.chainName
|
||||
binder.bridgeAssetInputSubtitleImage.setIconOrMakeGone(model.chainIconUrl?.asUrlIcon(), imageLoader)
|
||||
}
|
||||
|
||||
fun setError(message: String?) {
|
||||
binder.bridgeAssetInputContainer.background = if (message != null) {
|
||||
context.getInputBackgroundError()
|
||||
} else {
|
||||
context.getInputBackground()
|
||||
}
|
||||
|
||||
binder.bridgeAssetInputError.text = message
|
||||
binder.bridgeAssetInputError.setVisible(message != null)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,13 +25,27 @@
|
||||
android:orientation="vertical"
|
||||
android:padding="16dp">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/bridgeFromLabel"
|
||||
style="@style/TextAppearance.NovaFoundation.Regular.Footnote"
|
||||
android:layout_width="wrap_content"
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/bridge_you_send"
|
||||
android:textColor="@color/text_secondary" />
|
||||
android:gravity="center_vertical"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/bridgeFromLabel"
|
||||
style="@style/TextAppearance.NovaFoundation.Regular.Footnote"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:text="@string/bridge_you_send"
|
||||
android:textColor="@color/text_secondary" />
|
||||
|
||||
<io.novafoundation.nova.feature_wallet_api.presentation.view.amount.MaxAmountView
|
||||
android:id="@+id/bridgeFromMaxAmount"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<io.novafoundation.nova.feature_assets.presentation.bridge.view.BridgeAssetInputView
|
||||
android:id="@+id/bridgeFromCard"
|
||||
|
||||
@@ -110,4 +110,15 @@
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/bridgeAssetInputError"
|
||||
style="@style/TextAppearance.NovaFoundation.Regular.Caption1"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:textColor="@color/text_negative"
|
||||
android:visibility="gone"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/bridgeAssetInputContainer" />
|
||||
|
||||
</merge>
|
||||
|
||||
Reference in New Issue
Block a user