Move Pending Signatures from the wallet home screen to the Bridge screen

Wrong screen in the previous commit - moved it to sit right below the
existing per-leg renewal sign buttons on the Bridge input screen, which
is where it was actually asked for: a signatory renewing their own
wUSDT/Polkadot USDT allowance there should also see (and be able to
jump to) other pending multisig operations - e.g. someone else's large
bridge swap - waiting on their signature, without navigating elsewhere.

BalanceListFragment/ViewModel/Module are reverted to their prior state.
The reusable pieces (PendingSignatureModel, item_pending_signature_row
layout) are kept and repurposed for the Bridge screen; the RecyclerView
SingleItemAdapter/card layout built for the home screen are removed
since Bridge is a plain (non-RecyclerView) fragment - rows are inflated
directly into a LinearLayout container instead, same underlying data
and MultisigCallFormatter reuse as before.

The feature-assets -> feature-multisig:operations dependency and the
FeatureApi wiring (AssetsFeatureComponent/Dependencies/Holder) from the
previous commit stay as-is - Bridge is the same Gradle module, so no
change needed there.
This commit is contained in:
2026-07-17 14:43:18 -07:00
parent 707d1baba4
commit 6f14c8bf05
10 changed files with 124 additions and 187 deletions
@@ -36,9 +36,6 @@ import android.content.Intent
import android.widget.Toast import android.widget.Toast
import io.novafoundation.nova.feature_assets.presentation.balance.list.view.PezkuwiDashboardAdapter import io.novafoundation.nova.feature_assets.presentation.balance.list.view.PezkuwiDashboardAdapter
import io.novafoundation.nova.feature_assets.presentation.balance.list.view.PezkuwiDashboardHolder import io.novafoundation.nova.feature_assets.presentation.balance.list.view.PezkuwiDashboardHolder
import io.novafoundation.nova.feature_assets.presentation.balance.list.view.PendingSignaturesAdapter
import io.novafoundation.nova.feature_assets.presentation.balance.list.view.PendingSignaturesHolder
import io.novafoundation.nova.feature_assets.presentation.balance.list.model.PendingSignatureModel
import io.novafoundation.nova.feature_assets.presentation.citizenship.CitizenshipBottomSheet import io.novafoundation.nova.feature_assets.presentation.citizenship.CitizenshipBottomSheet
import io.novafoundation.nova.feature_banners_api.presentation.BannerHolder import io.novafoundation.nova.feature_banners_api.presentation.BannerHolder
import io.novafoundation.nova.feature_banners_api.presentation.PromotionBannerAdapter import io.novafoundation.nova.feature_banners_api.presentation.PromotionBannerAdapter
@@ -52,7 +49,6 @@ class BalanceListFragment :
AssetsHeaderAdapter.Handler, AssetsHeaderAdapter.Handler,
ManageAssetsAdapter.Handler, ManageAssetsAdapter.Handler,
PezkuwiDashboardAdapter.Handler, PezkuwiDashboardAdapter.Handler,
PendingSignaturesAdapter.Handler,
CitizenshipBottomSheet.CitizenshipDismissListener { CitizenshipBottomSheet.CitizenshipDismissListener {
override fun createBinding() = FragmentBalanceListBinding.inflate(layoutInflater) override fun createBinding() = FragmentBalanceListBinding.inflate(layoutInflater)
@@ -70,10 +66,6 @@ class BalanceListFragment :
PezkuwiDashboardAdapter(this) PezkuwiDashboardAdapter(this)
} }
private val pendingSignaturesAdapter by lazy(LazyThreadSafetyMode.NONE) {
PendingSignaturesAdapter(imageLoader, this)
}
private val bannerAdapter: PromotionBannerAdapter by lazy(LazyThreadSafetyMode.NONE) { private val bannerAdapter: PromotionBannerAdapter by lazy(LazyThreadSafetyMode.NONE) {
PromotionBannerAdapter(closable = true) PromotionBannerAdapter(closable = true)
} }
@@ -94,15 +86,7 @@ class BalanceListFragment :
} }
private val adapter by lazy(LazyThreadSafetyMode.NONE) { private val adapter by lazy(LazyThreadSafetyMode.NONE) {
ConcatAdapter( ConcatAdapter(headerAdapter, pezkuwiDashboardAdapter, bannerAdapter, manageAssetsAdapter, emptyAssetsPlaceholder, assetsAdapter)
headerAdapter,
pezkuwiDashboardAdapter,
pendingSignaturesAdapter,
bannerAdapter,
manageAssetsAdapter,
emptyAssetsPlaceholder,
assetsAdapter
)
} }
override fun applyInsets(rootView: View) { override fun applyInsets(rootView: View) {
@@ -159,11 +143,6 @@ class BalanceListFragment :
} }
} }
viewModel.pendingSignaturesFlow.observe { models ->
pendingSignaturesAdapter.setModels(models)
pendingSignaturesAdapter.show(models.isNotEmpty())
}
viewModel.bannersMixin.bindWithAdapter(bannerAdapter) { viewModel.bannersMixin.bindWithAdapter(bannerAdapter) {
binder.balanceListAssets.invalidateItemDecorations() binder.balanceListAssets.invalidateItemDecorations()
} }
@@ -322,10 +301,6 @@ class BalanceListFragment :
viewModel.startTrackingClicked() viewModel.startTrackingClicked()
} }
override fun onSignClicked(model: PendingSignatureModel) {
viewModel.pendingSignatureSignClicked(model)
}
private fun setupRecyclerViewSpacing() { private fun setupRecyclerViewSpacing() {
binder.balanceListAssets.addSpaceItemDecoration { binder.balanceListAssets.addSpaceItemDecoration {
add(SpaceBetween(AssetsHeaderHolder, PezkuwiDashboardHolder, spaceDp = 8)) add(SpaceBetween(AssetsHeaderHolder, PezkuwiDashboardHolder, spaceDp = 8))
@@ -334,11 +309,6 @@ class BalanceListFragment :
add(SpaceBetween(AssetsHeaderHolder, BannerHolder, spaceDp = 4)) add(SpaceBetween(AssetsHeaderHolder, BannerHolder, spaceDp = 4))
add(SpaceBetween(BannerHolder, ManageAssetsHolder, spaceDp = 4)) add(SpaceBetween(BannerHolder, ManageAssetsHolder, spaceDp = 4))
add(SpaceBetween(AssetsHeaderHolder, ManageAssetsHolder, spaceDp = 24)) add(SpaceBetween(AssetsHeaderHolder, ManageAssetsHolder, spaceDp = 24))
add(SpaceBetween(AssetsHeaderHolder, PendingSignaturesHolder, spaceDp = 8))
add(SpaceBetween(PezkuwiDashboardHolder, PendingSignaturesHolder, spaceDp = 8))
add(SpaceBetween(PendingSignaturesHolder, BannerHolder, spaceDp = 4))
add(SpaceBetween(PendingSignaturesHolder, ManageAssetsHolder, spaceDp = 24))
} }
} }
@@ -20,15 +20,10 @@ import io.novafoundation.nova.common.utils.formatting.formatAsPercentage
import io.novafoundation.nova.common.utils.inBackground import io.novafoundation.nova.common.utils.inBackground
import io.novafoundation.nova.common.utils.launchUnit import io.novafoundation.nova.common.utils.launchUnit
import io.novafoundation.nova.common.utils.withSafeLoading import io.novafoundation.nova.common.utils.withSafeLoading
import io.novafoundation.nova.feature_account_api.data.mappers.mapChainToUi
import io.novafoundation.nova.feature_account_api.data.multisig.MultisigPendingOperationsService import io.novafoundation.nova.feature_account_api.data.multisig.MultisigPendingOperationsService
import io.novafoundation.nova.feature_account_api.data.multisig.model.MultisigAction
import io.novafoundation.nova.feature_account_api.data.multisig.model.PendingMultisigOperation
import io.novafoundation.nova.feature_account_api.data.multisig.model.userAction
import io.novafoundation.nova.feature_account_api.domain.interfaces.SelectedAccountUseCase import io.novafoundation.nova.feature_account_api.domain.interfaces.SelectedAccountUseCase
import io.novafoundation.nova.feature_account_api.domain.model.MetaAccount import io.novafoundation.nova.feature_account_api.domain.model.MetaAccount
import io.novafoundation.nova.feature_account_api.domain.model.defaultSubstrateAddress import io.novafoundation.nova.feature_account_api.domain.model.defaultSubstrateAddress
import io.novafoundation.nova.feature_account_api.domain.model.requireAccountIdKeyIn
import io.novafoundation.nova.feature_assets.R import io.novafoundation.nova.feature_assets.R
import io.novafoundation.nova.feature_assets.domain.WalletInteractor import io.novafoundation.nova.feature_assets.domain.WalletInteractor
import io.novafoundation.nova.feature_assets.domain.assets.list.AssetsListInteractor import io.novafoundation.nova.feature_assets.domain.assets.list.AssetsListInteractor
@@ -47,7 +42,6 @@ import io.novafoundation.nova.feature_assets.presentation.balance.common.buySell
import io.novafoundation.nova.feature_assets.presentation.balance.common.buySell.BuySellSelectorMixinFactory import io.novafoundation.nova.feature_assets.presentation.balance.common.buySell.BuySellSelectorMixinFactory
import io.novafoundation.nova.feature_assets.presentation.citizenship.PendingCitizenshipReferrer import io.novafoundation.nova.feature_assets.presentation.citizenship.PendingCitizenshipReferrer
import io.novafoundation.nova.feature_assets.presentation.balance.list.model.NftPreviewUi import io.novafoundation.nova.feature_assets.presentation.balance.list.model.NftPreviewUi
import io.novafoundation.nova.feature_assets.presentation.balance.list.model.PendingSignatureModel
import io.novafoundation.nova.feature_assets.presentation.balance.list.model.TotalBalanceModel import io.novafoundation.nova.feature_assets.presentation.balance.list.model.TotalBalanceModel
import io.novafoundation.nova.feature_assets.presentation.balance.list.view.AssetViewModeModel import io.novafoundation.nova.feature_assets.presentation.balance.list.view.AssetViewModeModel
import io.novafoundation.nova.feature_assets.presentation.balance.list.view.PendingOperationsCountModel import io.novafoundation.nova.feature_assets.presentation.balance.list.view.PendingOperationsCountModel
@@ -58,10 +52,6 @@ import io.novafoundation.nova.feature_banners_api.presentation.source.assetsSour
import io.novafoundation.nova.feature_currency_api.domain.CurrencyInteractor import io.novafoundation.nova.feature_currency_api.domain.CurrencyInteractor
import io.novafoundation.nova.feature_currency_api.domain.model.Currency import io.novafoundation.nova.feature_currency_api.domain.model.Currency
import io.novafoundation.nova.feature_currency_api.presentation.formatters.formatAsCurrency import io.novafoundation.nova.feature_currency_api.presentation.formatters.formatAsCurrency
import io.novafoundation.nova.feature_multisig_operations.presentation.callFormatting.MultisigCallFormatter
import io.novafoundation.nova.feature_multisig_operations.presentation.common.MultisigOperationPayload
import io.novafoundation.nova.feature_multisig_operations.presentation.common.fromOperationId
import io.novafoundation.nova.feature_multisig_operations.presentation.details.general.MultisigOperationDetailsPayload
import io.novafoundation.nova.feature_nft_api.data.model.Nft import io.novafoundation.nova.feature_nft_api.data.model.Nft
import io.novafoundation.nova.feature_swap_api.domain.interactor.SwapAvailabilityInteractor import io.novafoundation.nova.feature_swap_api.domain.interactor.SwapAvailabilityInteractor
import io.novafoundation.nova.feature_wallet_api.presentation.formatters.amount.AmountFormatter import io.novafoundation.nova.feature_wallet_api.presentation.formatters.amount.AmountFormatter
@@ -118,7 +108,6 @@ class BalanceListViewModel(
private val maskableValueFormatterProvider: MaskableValueFormatterProvider, private val maskableValueFormatterProvider: MaskableValueFormatterProvider,
private val buySellSelectorMixinFactory: BuySellSelectorMixinFactory, private val buySellSelectorMixinFactory: BuySellSelectorMixinFactory,
private val multisigPendingOperationsService: MultisigPendingOperationsService, private val multisigPendingOperationsService: MultisigPendingOperationsService,
private val multisigCallFormatter: MultisigCallFormatter,
private val novaCardRestrictionCheckMixin: NovaCardRestrictionCheckMixin, private val novaCardRestrictionCheckMixin: NovaCardRestrictionCheckMixin,
private val maskingModeUseCase: MaskingModeUseCase, private val maskingModeUseCase: MaskingModeUseCase,
private val giftsRestrictionCheckMixin: GiftsRestrictionCheckMixin, private val giftsRestrictionCheckMixin: GiftsRestrictionCheckMixin,
@@ -259,38 +248,6 @@ class BalanceListViewModel(
.combine(maskableAmountFormatterFlow, ::formatPendingOperationsCount) .combine(maskableAmountFormatterFlow, ::formatPendingOperationsCount)
.shareInBackground() .shareInBackground()
val pendingSignaturesFlow = multisigPendingOperationsService.pendingOperations()
.mapLatest { operations ->
val account = selectedMetaAccount.first()
// Only operations still awaiting THIS signatory's own approval - once signed, the
// item naturally drops out here on next sync (no client-side "turn green" state to
// maintain in parallel with the real, authoritative on-chain approval status).
operations
.filter { it.userAction() is MultisigAction.CanApprove }
.map { it.toPendingSignatureUi(account) }
}
.shareInBackground()
fun pendingSignatureSignClicked(model: PendingSignatureModel) {
val operationPayload = MultisigOperationPayload.fromOperationId(model.id)
router.openMultisigOperationDetails(MultisigOperationDetailsPayload(operationPayload))
}
private suspend fun PendingMultisigOperation.toPendingSignatureUi(selectedAccount: MetaAccount): PendingSignatureModel {
val initialOrigin = selectedAccount.requireAccountIdKeyIn(chain)
val formattedCall = multisigCallFormatter.formatPreview(call, initialOrigin, chain)
return PendingSignatureModel(
id = operationId,
chain = mapChainToUi(chain),
title = formattedCall.title,
subtitle = formattedCall.subtitle,
primaryValue = formattedCall.primaryValue,
progress = resourceManager.getString(R.string.multisig_operations_progress, approvals.size.format(), threshold.format())
)
}
val pezkuwiDashboardFlow = selectedMetaAccount val pezkuwiDashboardFlow = selectedMetaAccount
.combine(dashboardRefreshSignal) { account, _ -> account } .combine(dashboardRefreshSignal) { account, _ -> account }
.mapLatest { metaAccount -> .mapLatest { metaAccount ->
@@ -13,7 +13,6 @@ import io.novafoundation.nova.common.di.viewmodel.ViewModelModule
import io.novafoundation.nova.common.domain.usecase.MaskingModeUseCase import io.novafoundation.nova.common.domain.usecase.MaskingModeUseCase
import io.novafoundation.nova.common.resources.ResourceManager import io.novafoundation.nova.common.resources.ResourceManager
import io.novafoundation.nova.feature_account_api.data.multisig.MultisigPendingOperationsService import io.novafoundation.nova.feature_account_api.data.multisig.MultisigPendingOperationsService
import io.novafoundation.nova.feature_multisig_operations.presentation.callFormatting.MultisigCallFormatter
import io.novafoundation.nova.feature_account_api.domain.interfaces.AccountRepository import io.novafoundation.nova.feature_account_api.domain.interfaces.AccountRepository
import io.novafoundation.nova.feature_account_api.domain.interfaces.SelectedAccountUseCase import io.novafoundation.nova.feature_account_api.domain.interfaces.SelectedAccountUseCase
import io.novafoundation.nova.feature_assets.data.repository.PezkuwiDashboardRepository import io.novafoundation.nova.feature_assets.data.repository.PezkuwiDashboardRepository
@@ -123,7 +122,6 @@ class BalanceListModule {
amountFormatter: AmountFormatter, amountFormatter: AmountFormatter,
buySellSelectorMixinFactory: BuySellSelectorMixinFactory, buySellSelectorMixinFactory: BuySellSelectorMixinFactory,
multisigPendingOperationsService: MultisigPendingOperationsService, multisigPendingOperationsService: MultisigPendingOperationsService,
multisigCallFormatter: MultisigCallFormatter,
novaCardRestrictionCheckMixin: NovaCardRestrictionCheckMixin, novaCardRestrictionCheckMixin: NovaCardRestrictionCheckMixin,
maskableValueFormatterProvider: MaskableValueFormatterProvider, maskableValueFormatterProvider: MaskableValueFormatterProvider,
maskingModeUseCase: MaskingModeUseCase, maskingModeUseCase: MaskingModeUseCase,
@@ -150,7 +148,6 @@ class BalanceListModule {
maskableValueFormatterProvider = maskableValueFormatterProvider, maskableValueFormatterProvider = maskableValueFormatterProvider,
buySellSelectorMixinFactory = buySellSelectorMixinFactory, buySellSelectorMixinFactory = buySellSelectorMixinFactory,
multisigPendingOperationsService = multisigPendingOperationsService, multisigPendingOperationsService = multisigPendingOperationsService,
multisigCallFormatter = multisigCallFormatter,
novaCardRestrictionCheckMixin = novaCardRestrictionCheckMixin, novaCardRestrictionCheckMixin = novaCardRestrictionCheckMixin,
maskingModeUseCase = maskingModeUseCase, maskingModeUseCase = maskingModeUseCase,
fiatFormatter = fiatFormatter, fiatFormatter = fiatFormatter,
@@ -1,74 +0,0 @@
package io.novafoundation.nova.feature_assets.presentation.balance.list.view
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import coil.ImageLoader
import io.novafoundation.nova.common.list.SingleItemAdapter
import io.novafoundation.nova.common.utils.inflater
import io.novafoundation.nova.common.utils.recyclerView.WithViewType
import io.novafoundation.nova.feature_account_api.presenatation.chain.loadChainIcon
import io.novafoundation.nova.feature_assets.R
import io.novafoundation.nova.feature_assets.databinding.ItemPendingSignatureRowBinding
import io.novafoundation.nova.feature_assets.databinding.ItemPendingSignaturesCardBinding
import io.novafoundation.nova.feature_assets.presentation.balance.list.model.PendingSignatureModel
class PendingSignaturesAdapter(
private val imageLoader: ImageLoader,
private val handler: Handler
) : SingleItemAdapter<PendingSignaturesHolder>(isShownByDefault = false) {
interface Handler {
fun onSignClicked(model: PendingSignatureModel)
}
private var models: List<PendingSignatureModel> = emptyList()
fun setModels(models: List<PendingSignatureModel>) {
this.models = models
notifyChangedIfShown()
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): PendingSignaturesHolder {
val binding = ItemPendingSignaturesCardBinding.inflate(parent.inflater(), parent, false)
return PendingSignaturesHolder(binding, imageLoader, handler)
}
override fun onBindViewHolder(holder: PendingSignaturesHolder, position: Int) {
holder.bind(models)
}
override fun getItemViewType(position: Int): Int {
return PendingSignaturesHolder.viewType
}
}
class PendingSignaturesHolder(
private val binder: ItemPendingSignaturesCardBinding,
private val imageLoader: ImageLoader,
private val handler: PendingSignaturesAdapter.Handler
) : RecyclerView.ViewHolder(binder.root) {
companion object : WithViewType {
override val viewType: Int = R.layout.item_pending_signatures_card
}
fun bind(models: List<PendingSignatureModel>) {
val container = binder.pendingSignaturesRowsContainer
container.removeAllViews()
models.forEach { model ->
val rowBinding = ItemPendingSignatureRowBinding.inflate(container.inflater(), container, false)
rowBinding.pendingSignatureChainIcon.loadChainIcon(model.chain.icon, imageLoader)
// primaryValue (e.g. "100K USDT") is the amount - the whole point of this card - so it
// takes the prominent title line; the call's own title/action name ("Transfer") isn't
// shown at all here, only the destination (subtitle) is, to keep the row to two lines.
rowBinding.pendingSignatureTitle.text = model.primaryValue ?: model.title
rowBinding.pendingSignatureSubtitle.text = listOfNotNull(model.subtitle, model.chain.name, model.progress)
.joinToString(separator = "")
rowBinding.pendingSignatureSignButton.setOnClickListener { handler.onSignClicked(model) }
container.addView(rowBinding.root)
}
}
}
@@ -3,19 +3,27 @@ package io.novafoundation.nova.feature_assets.presentation.bridge
import android.text.Editable import android.text.Editable
import android.text.TextWatcher import android.text.TextWatcher
import android.view.View import android.view.View
import coil.ImageLoader
import io.novafoundation.nova.common.base.BaseFragment import io.novafoundation.nova.common.base.BaseFragment
import io.novafoundation.nova.common.di.FeatureUtils import io.novafoundation.nova.common.di.FeatureUtils
import io.novafoundation.nova.common.utils.inflater
import io.novafoundation.nova.common.utils.setVisible import io.novafoundation.nova.common.utils.setVisible
import io.novafoundation.nova.common.view.AlertView import io.novafoundation.nova.common.view.AlertView
import io.novafoundation.nova.common.view.setState import io.novafoundation.nova.common.view.setState
import io.novafoundation.nova.feature_account_api.presenatation.chain.loadChainIcon
import io.novafoundation.nova.feature_assets.R import io.novafoundation.nova.feature_assets.R
import io.novafoundation.nova.feature_assets.databinding.FragmentBridgeBinding import io.novafoundation.nova.feature_assets.databinding.FragmentBridgeBinding
import io.novafoundation.nova.feature_assets.databinding.ItemPendingSignatureRowBinding
import io.novafoundation.nova.feature_assets.di.AssetsFeatureApi import io.novafoundation.nova.feature_assets.di.AssetsFeatureApi
import io.novafoundation.nova.feature_assets.di.AssetsFeatureComponent import io.novafoundation.nova.feature_assets.di.AssetsFeatureComponent
import io.novafoundation.nova.feature_wallet_api.presentation.mixin.amountChooser.MaxActionAvailability import io.novafoundation.nova.feature_wallet_api.presentation.mixin.amountChooser.MaxActionAvailability
import javax.inject.Inject
class BridgeFragment : BaseFragment<BridgeViewModel, FragmentBridgeBinding>() { class BridgeFragment : BaseFragment<BridgeViewModel, FragmentBridgeBinding>() {
@Inject
lateinit var imageLoader: ImageLoader
override fun createBinding() = FragmentBridgeBinding.inflate(layoutInflater) override fun createBinding() = FragmentBridgeBinding.inflate(layoutInflater)
override fun initViews() { override fun initViews() {
@@ -196,6 +204,34 @@ class BridgeFragment : BaseFragment<BridgeViewModel, FragmentBridgeBinding>() {
viewModel.fillAmountEvent.observeEvent { amount -> viewModel.fillAmountEvent.observeEvent { amount ->
binder.bridgeFromCard.amountInput.setText(amount) binder.bridgeFromCard.amountInput.setText(amount)
} }
viewModel.pendingSignatures.observe { models ->
bindPendingSignatures(models)
}
}
private fun bindPendingSignatures(models: List<PendingSignatureModel>) {
val visibility = if (models.isEmpty()) View.GONE else View.VISIBLE
binder.bridgePendingSignaturesTitle.visibility = visibility
binder.bridgePendingSignaturesContainer.visibility = visibility
val container = binder.bridgePendingSignaturesContainer
container.removeAllViews()
models.forEach { model ->
val rowBinding = ItemPendingSignatureRowBinding.inflate(container.inflater(), container, false)
rowBinding.pendingSignatureChainIcon.loadChainIcon(model.chain.icon, imageLoader)
// primaryValue (e.g. "100K USDT") is the amount - the whole point of this row - so it
// takes the prominent title line; the call's own title/action name ("Transfer") isn't
// shown at all here, only the destination (subtitle) is, to keep the row to two lines.
rowBinding.pendingSignatureTitle.text = model.primaryValue ?: model.title
rowBinding.pendingSignatureSubtitle.text = listOfNotNull(model.subtitle, model.chain.name, model.progress)
.joinToString(separator = "")
rowBinding.pendingSignatureSignButton.setOnClickListener { viewModel.pendingSignatureSignClicked(model) }
container.addView(rowBinding.root)
}
} }
} }
@@ -6,8 +6,17 @@ import io.novafoundation.nova.common.base.BaseViewModel
import io.novafoundation.nova.common.presentation.AssetIconProvider import io.novafoundation.nova.common.presentation.AssetIconProvider
import io.novafoundation.nova.common.resources.ResourceManager import io.novafoundation.nova.common.resources.ResourceManager
import io.novafoundation.nova.common.utils.Event import io.novafoundation.nova.common.utils.Event
import io.novafoundation.nova.common.utils.formatting.format
import io.novafoundation.nova.common.utils.images.Icon import io.novafoundation.nova.common.utils.images.Icon
import io.novafoundation.nova.common.view.ButtonState import io.novafoundation.nova.common.view.ButtonState
import io.novafoundation.nova.feature_account_api.data.mappers.mapChainToUi
import io.novafoundation.nova.feature_account_api.data.multisig.MultisigPendingOperationsService
import io.novafoundation.nova.feature_account_api.data.multisig.model.MultisigAction
import io.novafoundation.nova.feature_account_api.data.multisig.model.PendingMultisigOperation
import io.novafoundation.nova.feature_account_api.data.multisig.model.userAction
import io.novafoundation.nova.feature_account_api.domain.interfaces.SelectedAccountUseCase
import io.novafoundation.nova.feature_account_api.domain.model.MetaAccount
import io.novafoundation.nova.feature_account_api.domain.model.requireAccountIdKeyIn
import io.novafoundation.nova.feature_account_api.presenatation.chain.getAssetIconOrFallback import io.novafoundation.nova.feature_account_api.presenatation.chain.getAssetIconOrFallback
import io.novafoundation.nova.feature_assets.R import io.novafoundation.nova.feature_assets.R
import io.novafoundation.nova.feature_assets.domain.WalletInteractor import io.novafoundation.nova.feature_assets.domain.WalletInteractor
@@ -16,12 +25,17 @@ import io.novafoundation.nova.feature_assets.domain.bridge.multisig.BridgeMultis
import io.novafoundation.nova.feature_assets.domain.bridge.multisig.BridgeSignerState import io.novafoundation.nova.feature_assets.domain.bridge.multisig.BridgeSignerState
import io.novafoundation.nova.feature_assets.presentation.AssetsRouter 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.BridgeExecutionPayload
import io.novafoundation.nova.feature_multisig_operations.presentation.callFormatting.MultisigCallFormatter
import io.novafoundation.nova.feature_multisig_operations.presentation.common.MultisigOperationPayload
import io.novafoundation.nova.feature_multisig_operations.presentation.common.fromOperationId
import io.novafoundation.nova.feature_multisig_operations.presentation.details.general.MultisigOperationDetailsPayload
import io.novafoundation.nova.runtime.ext.ChainGeneses import io.novafoundation.nova.runtime.ext.ChainGeneses
import io.novafoundation.nova.runtime.ext.addressOf import io.novafoundation.nova.runtime.ext.addressOf
import io.novafoundation.nova.runtime.ext.displayNameWithAssetStandard import io.novafoundation.nova.runtime.ext.displayNameWithAssetStandard
import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry
import io.novasama.substrate_sdk_android.ss58.SS58Encoder.toAccountId import io.novasama.substrate_sdk_android.ss58.SS58Encoder.toAccountId
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import java.math.BigDecimal import java.math.BigDecimal
import java.math.RoundingMode import java.math.RoundingMode
@@ -48,6 +62,9 @@ class BridgeViewModel(
private val assetIconProvider: AssetIconProvider, private val assetIconProvider: AssetIconProvider,
private val walletInteractor: WalletInteractor, private val walletInteractor: WalletInteractor,
private val bridgeMultisigInteractor: BridgeMultisigInteractor, private val bridgeMultisigInteractor: BridgeMultisigInteractor,
private val multisigPendingOperationsService: MultisigPendingOperationsService,
private val multisigCallFormatter: MultisigCallFormatter,
private val selectedAccountUseCase: SelectedAccountUseCase,
) : BaseViewModel() { ) : BaseViewModel() {
companion object { companion object {
@@ -130,6 +147,15 @@ class BridgeViewModel(
private val _polkadotSignButtonLabel = MutableLiveData("") private val _polkadotSignButtonLabel = MutableLiveData("")
val polkadotSignButtonLabel: LiveData<String> = _polkadotSignButtonLabel val polkadotSignButtonLabel: LiveData<String> = _polkadotSignButtonLabel
/** Multisig operations (any of them - not just bridge swaps, since a signatory of this bridge
* account may also be a signatory of other multisig accounts) still awaiting this wallet's
* own approval - shown below the renewal sign buttons above so a signatory doesn't have to
* separately navigate to the pending-operations list to find and sign a swap someone else
* submitted (see the "Sign" buttons above, which renew this wallet's own on-chain allowance -
* this is a different, complementary concern: approving *other people's* pending calls). */
private val _pendingSignatures = MutableLiveData<List<PendingSignatureModel>>(emptyList())
val pendingSignatures: LiveData<List<PendingSignatureModel>> = _pendingSignatures
private val _fromCard = MutableLiveData<BridgeAssetCardUi>() private val _fromCard = MutableLiveData<BridgeAssetCardUi>()
val fromCard: LiveData<BridgeAssetCardUi> = _fromCard val fromCard: LiveData<BridgeAssetCardUi> = _fromCard
@@ -452,6 +478,38 @@ class BridgeViewModel(
fun refreshBridgeStatus() { fun refreshBridgeStatus() {
fetchReserveStatus() fetchReserveStatus()
refreshSignerState() refreshSignerState()
refreshPendingSignatures()
}
fun refreshPendingSignatures() {
launch {
val account = selectedAccountUseCase.getSelectedMetaAccount()
val models = multisigPendingOperationsService.pendingOperations().first()
.filter { it.userAction() is MultisigAction.CanApprove }
.map { it.toPendingSignatureUi(account) }
_pendingSignatures.postValue(models)
}
}
fun pendingSignatureSignClicked(model: PendingSignatureModel) {
val operationPayload = MultisigOperationPayload.fromOperationId(model.id)
router.openMultisigOperationDetails(MultisigOperationDetailsPayload(operationPayload))
}
private suspend fun PendingMultisigOperation.toPendingSignatureUi(selectedAccount: MetaAccount): PendingSignatureModel {
val initialOrigin = selectedAccount.requireAccountIdKeyIn(chain)
val formattedCall = multisigCallFormatter.formatPreview(call, initialOrigin, chain)
return PendingSignatureModel(
id = operationId,
chain = mapChainToUi(chain),
title = formattedCall.title,
subtitle = formattedCall.subtitle,
primaryValue = formattedCall.primaryValue,
progress = resourceManager.getString(R.string.multisig_operations_progress, approvals.size.format(), threshold.format())
)
} }
fun refreshSignerState() { fun refreshSignerState() {
@@ -1,4 +1,4 @@
package io.novafoundation.nova.feature_assets.presentation.balance.list.model package io.novafoundation.nova.feature_assets.presentation.bridge
import io.novafoundation.nova.feature_account_api.data.multisig.model.PendingMultisigOperationId import io.novafoundation.nova.feature_account_api.data.multisig.model.PendingMultisigOperationId
import io.novafoundation.nova.feature_account_api.presenatation.chain.ChainUi import io.novafoundation.nova.feature_account_api.presenatation.chain.ChainUi
@@ -11,12 +11,14 @@ import io.novafoundation.nova.common.di.viewmodel.ViewModelModule
import io.novafoundation.nova.common.presentation.AssetIconProvider import io.novafoundation.nova.common.presentation.AssetIconProvider
import io.novafoundation.nova.common.resources.ResourceManager import io.novafoundation.nova.common.resources.ResourceManager
import io.novafoundation.nova.feature_account_api.data.extrinsic.ExtrinsicService import io.novafoundation.nova.feature_account_api.data.extrinsic.ExtrinsicService
import io.novafoundation.nova.feature_account_api.data.multisig.MultisigPendingOperationsService
import io.novafoundation.nova.feature_account_api.domain.interfaces.SelectedAccountUseCase 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.WalletInteractor
import io.novafoundation.nova.feature_assets.domain.bridge.multisig.BridgeMultisigInteractor 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.bridge.multisig.RealBridgeMultisigInteractor
import io.novafoundation.nova.feature_assets.presentation.AssetsRouter import io.novafoundation.nova.feature_assets.presentation.AssetsRouter
import io.novafoundation.nova.feature_assets.presentation.bridge.BridgeViewModel import io.novafoundation.nova.feature_assets.presentation.bridge.BridgeViewModel
import io.novafoundation.nova.feature_multisig_operations.presentation.callFormatting.MultisigCallFormatter
import io.novafoundation.nova.runtime.di.REMOTE_STORAGE_SOURCE import io.novafoundation.nova.runtime.di.REMOTE_STORAGE_SOURCE
import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry
import io.novafoundation.nova.runtime.storage.source.StorageDataSource import io.novafoundation.nova.runtime.storage.source.StorageDataSource
@@ -45,6 +47,9 @@ class BridgeModule {
assetIconProvider: AssetIconProvider, assetIconProvider: AssetIconProvider,
walletInteractor: WalletInteractor, walletInteractor: WalletInteractor,
bridgeMultisigInteractor: BridgeMultisigInteractor, bridgeMultisigInteractor: BridgeMultisigInteractor,
multisigPendingOperationsService: MultisigPendingOperationsService,
multisigCallFormatter: MultisigCallFormatter,
selectedAccountUseCase: SelectedAccountUseCase,
): ViewModel { ): ViewModel {
return BridgeViewModel( return BridgeViewModel(
router, router,
@@ -53,6 +58,9 @@ class BridgeModule {
assetIconProvider, assetIconProvider,
walletInteractor, walletInteractor,
bridgeMultisigInteractor, bridgeMultisigInteractor,
multisigPendingOperationsService,
multisigCallFormatter,
selectedAccountUseCase,
) )
} }
@@ -214,6 +214,26 @@
android:text="@string/bridge_sign_button" android:text="@string/bridge_sign_button"
android:visibility="gone" /> android:visibility="gone" />
<!-- Pending multisig operations awaiting this signatory's own approval (someone
else's swap, not this wallet's own renewal above) - one row per operation,
populated dynamically, see BridgeViewModel.pendingSignatures. -->
<TextView
android:id="@+id/bridgePendingSignaturesTitle"
style="@style/TextAppearance.NovaFoundation.SemiBold.Caption1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:text="@string/pending_signatures_title"
android:textColor="@color/text_secondary"
android:visibility="gone" />
<LinearLayout
android:id="@+id/bridgePendingSignaturesContainer"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:orientation="vertical" />
</LinearLayout> </LinearLayout>
</ScrollView> </ScrollView>
@@ -1,35 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<com.google.android.material.card.MaterialCardView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="16dp"
app:cardBackgroundColor="@android:color/transparent"
app:cardCornerRadius="20dp"
app:cardElevation="0dp"
app:strokeWidth="0dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@drawable/bg_pezkuwi_dashboard"
android:orientation="vertical"
android:paddingHorizontal="16dp"
android:paddingVertical="12dp">
<TextView
style="@style/TextAppearance.NovaFoundation.SemiBold.Caption1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="4dp"
android:text="@string/pending_signatures_title"
android:textColor="@color/text_secondary" />
<LinearLayout
android:id="@+id/pendingSignaturesRowsContainer"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical" />
</LinearLayout>
</com.google.android.material.card.MaterialCardView>