diff --git a/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/bridge/BridgeFragment.kt b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/bridge/BridgeFragment.kt index 0c883260..3cba5d5f 100644 --- a/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/bridge/BridgeFragment.kt +++ b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/bridge/BridgeFragment.kt @@ -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() { @@ -49,6 +50,10 @@ class BridgeFragment : BaseFragment() { } }) + binder.bridgeFromMaxAmount.setMaxActionAvailability( + MaxActionAvailability.Available { viewModel.maxClicked() } + ) + // Swap button binder.bridgeSwapButton.setOnClickListener { viewModel.swapClicked() @@ -105,6 +110,18 @@ class BridgeFragment : BaseFragment() { 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) + } } } 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 9f290dff..ae861797 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 @@ -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>(emptyList()) val pairOptions: LiveData> = _pairOptions + private val _maxAmountDisplay = MutableLiveData(null) + val maxAmountDisplay: LiveData = _maxAmountDisplay + + private val _insufficientBalanceError = MutableLiveData(null) + val insufficientBalanceError: LiveData = _insufficientBalanceError + + private val _fillAmountEvent = MutableLiveData>() + val fillAmountEvent: LiveData> = _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() { 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 bba4c549..e00811f7 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 @@ -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 diff --git a/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/bridge/view/BridgeAssetInputView.kt b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/bridge/view/BridgeAssetInputView.kt index b22ec0a1..86bb8649 100644 --- a/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/bridge/view/BridgeAssetInputView.kt +++ b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/bridge/view/BridgeAssetInputView.kt @@ -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) + } } diff --git a/feature-assets/src/main/res/layout/fragment_bridge.xml b/feature-assets/src/main/res/layout/fragment_bridge.xml index 21a95857..63fc046c 100644 --- a/feature-assets/src/main/res/layout/fragment_bridge.xml +++ b/feature-assets/src/main/res/layout/fragment_bridge.xml @@ -25,13 +25,27 @@ android:orientation="vertical" android:padding="16dp"> - + android:gravity="center_vertical" + android:orientation="horizontal"> + + + + + + + +