Add signatory-only sign button to Bridge screen

Any of the USDT bridge multisig's 5 known signatories now sees a
status button on the Bridge screen: green when the automation key's
spending allowance is healthy, red with a Sign action once it drops
below threshold. Signing submits the renewal's as_multi approval
directly from the wallet, mirroring the same on-chain action already
available via pwap-web and pezbridge-sign.pex.mom - a third
independent channel for the same operation, none of which is a
single point of failure.
This commit is contained in:
2026-07-14 06:59:18 -07:00
parent ee63f15dd9
commit cfdeff8ab7
9 changed files with 478 additions and 3 deletions
+6
View File
@@ -371,6 +371,12 @@
<string name="bridge_hez_to_dot_blocked">HEZ→DOT swaps are temporarily unavailable. Please try again when DOT liquidity is sufficient.</string> <string name="bridge_hez_to_dot_blocked">HEZ→DOT swaps are temporarily unavailable. Please try again when DOT liquidity is sufficient.</string>
<string name="bridge_wusdt_to_usdt_blocked">USDT(Pez)→USDT(Pol) swaps are temporarily unavailable. Waiting for 1:1 liquidity to be established.</string> <string name="bridge_wusdt_to_usdt_blocked">USDT(Pez)→USDT(Pol) swaps are temporarily unavailable. Waiting for 1:1 liquidity to be established.</string>
<string name="bridge_sign_status_ok">Bridge allowance OK (%s)</string>
<string name="bridge_sign_button">Sign renewal (%s)</string>
<string name="bridge_sign_waiting_others">Signed — waiting for other signers</string>
<string name="bridge_sign_in_progress">Signing…</string>
<string name="bridge_sign_error">Signing failed, tap to retry</string>
<string name="wallet_asset_buy_sell">Buy/Sell</string> <string name="wallet_asset_buy_sell">Buy/Sell</string>
@@ -0,0 +1,73 @@
package io.novafoundation.nova.feature_assets.domain.bridge.multisig
import io.novafoundation.nova.common.address.AccountIdKey
import io.novafoundation.nova.common.data.network.runtime.binding.WeightV2
import io.novafoundation.nova.common.utils.Modules
import io.novafoundation.nova.common.utils.argumentType
import io.novafoundation.nova.common.utils.composeCall
import io.novafoundation.nova.feature_account_api.data.multisig.model.MultisigTimePoint
import io.novafoundation.nova.runtime.util.constructAccountLookupInstance
import io.novasama.substrate_sdk_android.runtime.RuntimeSnapshot
import io.novasama.substrate_sdk_android.runtime.definitions.types.generics.GenericCall
import io.novasama.substrate_sdk_android.runtime.metadata.call
import java.math.BigInteger
/**
* `Assets.approve_transfer(id, delegate, amount)` - field names verified against the actual
* pallet_assets source this ecosystem runs (bizinikiwi/pezframe/assets/src/lib.rs), not guessed:
* `pub fn approve_transfer(origin, id: T::AssetIdParameter, delegate: AccountIdLookupOf<T>,
* #[compact] amount: T::Balance)`. `delegate` is a lookup/MultiAddress field, so it goes through
* `constructAccountLookupInstance` - this chain uses non-standard numeric MultiAddress variant
* names in places (see runtime/util/AccountLookup.kt's own comment), so hand-rolling this
* wrapping instead of reusing the app's existing helper would be a real risk of a silently wrong
* encoding.
*/
fun RuntimeSnapshot.composeAssetsApproveTransfer(
assetId: Int,
delegate: AccountIdKey,
amount: BigInteger,
): GenericCall.Instance {
val delegateType = metadata.module(Modules.ASSETS).call("approve_transfer").argumentType("delegate")
return composeCall(
moduleName = Modules.ASSETS,
callName = "approve_transfer",
arguments = mapOf(
"id" to assetId.toBigInteger(),
"delegate" to delegateType.constructAccountLookupInstance(delegate.value),
"amount" to amount,
)
)
}
/**
* `Multisig.as_multi(threshold, other_signatories, maybe_timepoint, call, max_weight)` - a plain
* threshold/signatory-list version of feature-account-api's `composeMultisigAsMulti`, which
* requires a full `MultisigMetaAccount` (only ever reads its `.threshold`/`.otherSignatories`,
* but constructing one would mean either building a throwaway implementation of the whole
* `MetaAccount` interface chain or going through this app's "add multisig wallet" import flow -
* neither is needed here since this screen's multisig is fully known/hardcoded ahead of time).
* Passing the full call (never just its hash) is always correct regardless of whether this
* signature is the first, an intermediate, or the final approval - pallet_multisig's own
* `operate()` only actually dispatches the call once threshold is reached with this vote,
* otherwise it just records the approval, so there is no "hash-only" branch needed on this side.
*/
fun RuntimeSnapshot.composeBridgeMultisigAsMulti(
threshold: Int,
otherSignatories: List<AccountIdKey>,
maybeTimePoint: MultisigTimePoint?,
call: GenericCall.Instance,
maxWeight: WeightV2,
): GenericCall.Instance {
return composeCall(
moduleName = Modules.MULTISIG,
callName = "as_multi",
arguments = mapOf(
"threshold" to threshold.toBigInteger(),
"other_signatories" to otherSignatories.map { it.value },
"maybe_timepoint" to maybeTimePoint?.toEncodableInstance(),
"call" to call,
"max_weight" to maxWeight.toEncodableInstance(),
)
)
}
@@ -0,0 +1,38 @@
package io.novafoundation.nova.feature_assets.domain.bridge.multisig
/**
* Everything needed to identify the USDT bridge's 3-of-5 multisig and let any of its 5 real
* signatories renew the automation key's spending allowance directly from this app's Bridge
* screen - a third independent channel alongside pwap-web's `/multisig/pending` and
* pezbridge-sign.pex.mom, all three ultimately doing the same on-chain thing (a signer's own
* wallet contributing one `Multisig.as_multi` approval), so none of them is a single point of
* failure for "how do signers actually sign."
*
* See res/validators-tiki.md "USDT Bridge Custody Multisig" (not in this repo) for how these
* were derived/verified on-chain.
*/
object BridgeMultisigConstants {
const val WUSDT_ASSET_ID = 1000
const val MULTISIG_ADDRESS = "5GvwxmCDp3PC33KHoeWSgj3S7ocE7nzk1jiCCZMPSDBFeNcj"
const val AUTOMATION_KEY_ADDRESS = "5GQu4PFUb1f3MTJ7i7c1CtLgDk3TVvpSW1VbQCRmfkMoC8cM"
const val THRESHOLD = 3
/** Renewal is offered once the remaining allowance drops below this (6 decimals). */
const val RENEWAL_THRESHOLD = 3_000_000_000L // 3,000 wUSDT
/** The standard amount a renewal tops the allowance back up to (6 decimals). */
const val TOPUP_AMOUNT = 10_000_000_000L // 10,000 wUSDT
data class Signatory(val role: String, val address: String)
val SIGNATORIES = listOf(
Signatory("Serok", "5EfUVZ4HXG65WuqeG24z4Pmt11cNQTUacR3CKymKcsv1UTFU"),
Signatory("SerokiMeclise", "5CrB5BWJfLNWEZAsAXDKXdJUGzFMXKvYnwRX4DVMcgBwxSdx"),
Signatory("Xezinedar", "5GipBJs2uNWTCazyZQ2vG3DEqLz4tXNmNZtBAT1Mtm1orZ5i"),
Signatory("Berdevk", "5HWFZbhkZuTUySXu6ZXYKrTHBnWXHvWRKLozE22zhnwXGGxk"),
Signatory("Noter", "5ELgySrX5ZyK7EWXjj6bAedyTCcTNWDANbiiipsT5gnpoCEp"),
)
}
@@ -0,0 +1,153 @@
package io.novafoundation.nova.feature_assets.domain.bridge.multisig
import io.novafoundation.nova.common.address.AccountIdKey
import io.novafoundation.nova.common.address.intoKey
import io.novafoundation.nova.common.address.toHexWithPrefix
import io.novafoundation.nova.common.data.network.runtime.binding.WeightV2
import io.novafoundation.nova.common.di.scope.FeatureScope
import io.novafoundation.nova.common.utils.callHash
import io.novafoundation.nova.feature_account_api.data.ethereum.transaction.TransactionOrigin
import io.novafoundation.nova.feature_account_api.data.extrinsic.ExtrinsicService
import io.novafoundation.nova.feature_account_api.data.extrinsic.execution.ExtrinsicExecutionResult
import io.novafoundation.nova.feature_account_api.data.extrinsic.execution.requireOk
import io.novafoundation.nova.feature_account_api.domain.interfaces.SelectedAccountUseCase
import io.novafoundation.nova.feature_account_api.domain.model.accountIdIn
import io.novafoundation.nova.feature_account_api.domain.multisig.intoCallHash
import io.novafoundation.nova.runtime.di.REMOTE_STORAGE_SOURCE
import io.novafoundation.nova.runtime.ext.ChainGeneses
import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain
import io.novafoundation.nova.runtime.multiNetwork.getRuntime
import io.novafoundation.nova.runtime.storage.source.StorageDataSource
import io.novasama.substrate_sdk_android.ss58.SS58Encoder.toAccountId
import java.math.BigInteger
import javax.inject.Inject
import javax.inject.Named
data class BridgeSignerState(
val signatoryRole: String,
val remainingAllowance: BigInteger,
val needsRenewal: Boolean,
val alreadySignedPendingRenewal: Boolean,
val approvalsSoFar: Int,
)
interface BridgeMultisigInteractor {
/** Null if the currently selected wallet isn't one of the 5 known bridge signatories - the
* Bridge screen shows nothing in that case, this feature doesn't exist for anyone else. */
suspend fun getSignerState(): BridgeSignerState?
suspend fun submitRenewalSignature(): Result<ExtrinsicExecutionResult>
}
@FeatureScope
class RealBridgeMultisigInteractor @Inject constructor(
private val chainRegistry: ChainRegistry,
private val selectedAccountUseCase: SelectedAccountUseCase,
@Named(REMOTE_STORAGE_SOURCE) private val storageDataSource: StorageDataSource,
private val extrinsicService: ExtrinsicService,
) : BridgeMultisigInteractor {
override suspend fun getSignerState(): BridgeSignerState? {
val chain = chainRegistry.getChain(ChainGeneses.PEZKUWI_ASSET_HUB)
val metaAccount = selectedAccountUseCase.getSelectedMetaAccount()
val myAccountId = metaAccount.accountIdIn(chain)?.intoKey() ?: return null
val signatory = BridgeMultisigConstants.SIGNATORIES.firstOrNull {
it.address.toAccountId().intoKey() == myAccountId
} ?: return null
val remaining = queryRemainingAllowance(chain)
val needsRenewal = remaining < BigInteger.valueOf(BridgeMultisigConstants.RENEWAL_THRESHOLD)
var alreadySigned = false
var approvalsSoFar = 0
if (needsRenewal) {
val pending = queryPendingRenewal(chain)
if (pending != null) {
approvalsSoFar = pending.approvals.size
alreadySigned = pending.approvals.contains(myAccountId)
}
}
return BridgeSignerState(
signatoryRole = signatory.role,
remainingAllowance = remaining,
needsRenewal = needsRenewal,
alreadySignedPendingRenewal = alreadySigned,
approvalsSoFar = approvalsSoFar,
)
}
override suspend fun submitRenewalSignature(): Result<ExtrinsicExecutionResult> = runCatching {
val chain = chainRegistry.getChain(ChainGeneses.PEZKUWI_ASSET_HUB)
val metaAccount = selectedAccountUseCase.getSelectedMetaAccount()
val myAccountId = requireNotNull(metaAccount.accountIdIn(chain)?.intoKey()) {
"Selected account has no address on Pezkuwi Asset Hub"
}
val otherSignatories = BridgeMultisigConstants.SIGNATORIES
.map { it.address.toAccountId().intoKey() }
.filter { it != myAccountId }
.sortedBy { it.toHexWithPrefix() }
val pending = queryPendingRenewal(chain)
check(pending == null || !pending.approvals.contains(myAccountId)) {
"Already signed this renewal - waiting for other signers"
}
extrinsicService.submitExtrinsicAndAwaitExecution(
chain = chain,
origin = TransactionOrigin.WalletWithId(metaAccount.id)
) {
val approveTransferCall = runtime.composeAssetsApproveTransfer(
assetId = BridgeMultisigConstants.WUSDT_ASSET_ID,
delegate = BridgeMultisigConstants.AUTOMATION_KEY_ADDRESS.toAccountId().intoKey(),
amount = BigInteger.valueOf(BridgeMultisigConstants.TOPUP_AMOUNT),
)
val multisigCall = runtime.composeBridgeMultisigAsMulti(
threshold = BridgeMultisigConstants.THRESHOLD,
otherSignatories = otherSignatories,
maybeTimePoint = pending?.timePoint,
call = approveTransferCall,
maxWeight = WeightV2(BigInteger.valueOf(1_000_000_000L), BigInteger.valueOf(200_000L)),
)
call(multisigCall)
}.requireOk()
}
private suspend fun queryRemainingAllowance(chain: Chain): BigInteger {
val multisigAccountId = BridgeMultisigConstants.MULTISIG_ADDRESS.toAccountId().intoKey()
val delegateAccountId = BridgeMultisigConstants.AUTOMATION_KEY_ADDRESS.toAccountId().intoKey()
return storageDataSource.query(chain.id) {
runtime.metadata.bridgeAssets().approvalAmount.query(
BridgeMultisigConstants.WUSDT_ASSET_ID.toBigInteger(),
multisigAccountId,
delegateAccountId,
)
} ?: BigInteger.ZERO
}
private suspend fun queryPendingRenewal(chain: Chain): BridgeOnChainMultisig? {
val multisigAccountId = BridgeMultisigConstants.MULTISIG_ADDRESS.toAccountId().intoKey()
val callHash = renewalCallHash(chain)
return storageDataSource.query(chain.id) {
runtime.metadata.bridgeMultisig().multisigs.query(multisigAccountId, callHash)
}
}
private suspend fun renewalCallHash(chain: Chain): AccountIdKey {
val runtime = chainRegistry.getRuntime(chain.id)
val call = runtime.composeAssetsApproveTransfer(
assetId = BridgeMultisigConstants.WUSDT_ASSET_ID,
delegate = BridgeMultisigConstants.AUTOMATION_KEY_ADDRESS.toAccountId().intoKey(),
amount = BigInteger.valueOf(BridgeMultisigConstants.TOPUP_AMOUNT),
)
return call.callHash(runtime).intoCallHash()
}
}
@@ -0,0 +1,78 @@
package io.novafoundation.nova.feature_assets.domain.bridge.multisig
import io.novafoundation.nova.common.address.AccountIdKey
import io.novafoundation.nova.common.data.network.runtime.binding.bindAccountIdKey
import io.novafoundation.nova.common.data.network.runtime.binding.bindList
import io.novafoundation.nova.common.data.network.runtime.binding.bindNumber
import io.novafoundation.nova.common.data.network.runtime.binding.castToStruct
import io.novafoundation.nova.common.utils.Modules
import io.novafoundation.nova.feature_account_api.data.multisig.model.MultisigTimePoint
import io.novafoundation.nova.feature_account_api.domain.multisig.CallHash
import io.novafoundation.nova.runtime.storage.source.query.StorageQueryContext
import io.novafoundation.nova.runtime.storage.source.query.api.QueryableModule
import io.novafoundation.nova.runtime.storage.source.query.api.QueryableStorageEntry2
import io.novafoundation.nova.runtime.storage.source.query.api.QueryableStorageEntry3
import io.novafoundation.nova.runtime.storage.source.query.api.converters.scaleDecoder
import io.novafoundation.nova.runtime.storage.source.query.api.converters.scaleEncoder
import io.novafoundation.nova.runtime.storage.source.query.api.storage2
import io.novafoundation.nova.runtime.storage.source.query.api.storage3
import io.novasama.substrate_sdk_android.runtime.metadata.RuntimeMetadata
import io.novasama.substrate_sdk_android.runtime.metadata.module.Module
import java.math.BigInteger
/**
* Minimal, Bridge-screen-local mirrors of the equivalents already used by the multisig-operations
* feature (feature-account-impl's `MultisigRuntimeApi`/`OnChainMultisig`) - duplicated here rather
* than imported because feature-assets depends on feature-account-api, not feature-account-impl
* (standard module-boundary convention in this app: features don't reach into each other's impl
* modules). Only what this screen actually needs is modeled.
*/
@JvmInline
value class BridgeAssetsApi(override val module: Module) : QueryableModule
context(StorageQueryContext)
fun RuntimeMetadata.bridgeAssets(): BridgeAssetsApi = BridgeAssetsApi(module(Modules.ASSETS))
/** `Assets.Approvals(asset_id, owner, delegate) -> Approval { amount, deposit }` - only `amount`
* (the remaining spendable allowance) is needed here. */
context(StorageQueryContext)
val BridgeAssetsApi.approvalAmount: QueryableStorageEntry3<BigInteger, AccountIdKey, AccountIdKey, BigInteger>
get() = storage3(
name = "Approvals",
binding = { decoded, _, _, _ -> bindNumber(decoded.castToStruct()["amount"]) },
key2ToInternalConverter = AccountIdKey.scaleEncoder,
key3ToInternalConverter = AccountIdKey.scaleEncoder,
key2FromInternalConverter = AccountIdKey.scaleDecoder,
key3FromInternalConverter = AccountIdKey.scaleDecoder,
)
@JvmInline
value class BridgeMultisigApi(override val module: Module) : QueryableModule
context(StorageQueryContext)
fun RuntimeMetadata.bridgeMultisig(): BridgeMultisigApi = BridgeMultisigApi(module(Modules.MULTISIG))
class BridgeOnChainMultisig(
val approvals: List<AccountIdKey>,
val timePoint: MultisigTimePoint,
)
/** `Multisig.Multisigs(multisig_account, call_hash) -> Multisig { approvals, when, ... }` -
* mirrors `OnChainMultisig` but only carries what this screen needs (not deposit/depositor). */
context(StorageQueryContext)
val BridgeMultisigApi.multisigs: QueryableStorageEntry2<AccountIdKey, CallHash, BridgeOnChainMultisig>
get() = storage2(
name = "Multisigs",
binding = { decoded, _, _ ->
val struct = decoded.castToStruct()
BridgeOnChainMultisig(
approvals = bindList(struct["approvals"], ::bindAccountIdKey),
timePoint = MultisigTimePoint.bind(struct["when"]),
)
},
key1ToInternalConverter = AccountIdKey.scaleEncoder,
key1FromInternalConverter = AccountIdKey.scaleDecoder,
key2ToInternalConverter = CallHash.scaleEncoder,
key2FromInternalConverter = CallHash.scaleDecoder,
)
@@ -50,6 +50,17 @@ class BridgeFragment : BaseFragment<BridgeViewModel, FragmentBridgeBinding>() {
binder.bridgeSwapButton.setOnClickListener { binder.bridgeSwapButton.setOnClickListener {
viewModel.swapClicked() viewModel.swapClicked()
} }
// Multisig signatory-only sign button
binder.bridgeSignButton.setOnClickListener {
viewModel.signClicked()
}
}
override fun onResume() {
super.onResume()
viewModel.refreshBridgeStatus()
} }
override fun inject() { override fun inject() {
@@ -104,6 +115,27 @@ class BridgeFragment : BaseFragment<BridgeViewModel, FragmentBridgeBinding>() {
binder.bridgeHezToDotWarning.text = text binder.bridgeHezToDotWarning.text = text
} }
} }
viewModel.signButtonVisible.observe { visible ->
binder.bridgeSignButton.visibility = if (visible) View.VISIBLE else View.GONE
}
viewModel.signButtonRed.observe { red ->
val color = if (red) {
resources.getColor(R.color.error_border, null)
} else {
resources.getColor(R.color.text_positive, null)
}
binder.bridgeSignButton.setButtonColor(color)
}
viewModel.signButtonEnabled.observe { enabled ->
binder.bridgeSignButton.isEnabled = enabled
}
viewModel.signButtonLabel.observe { label ->
binder.bridgeSignButton.text = label
}
} }
private fun updatePairUI(pair: BridgePair) { private fun updatePairUI(pair: BridgePair) {
@@ -6,6 +6,8 @@ import io.novafoundation.nova.common.base.BaseViewModel
import io.novafoundation.nova.common.resources.ResourceManager import io.novafoundation.nova.common.resources.ResourceManager
import io.novafoundation.nova.common.view.ButtonState import io.novafoundation.nova.common.view.ButtonState
import io.novafoundation.nova.feature_assets.R import io.novafoundation.nova.feature_assets.R
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.presentation.AssetsRouter import io.novafoundation.nova.feature_assets.presentation.AssetsRouter
import io.novafoundation.nova.feature_assets.presentation.send.amount.SendPayload import io.novafoundation.nova.feature_assets.presentation.send.amount.SendPayload
import io.novafoundation.nova.feature_wallet_api.presentation.model.AssetPayload import io.novafoundation.nova.feature_wallet_api.presentation.model.AssetPayload
@@ -24,7 +26,8 @@ import java.net.URL
class BridgeViewModel( class BridgeViewModel(
private val router: AssetsRouter, private val router: AssetsRouter,
private val resourceManager: ResourceManager, private val resourceManager: ResourceManager,
private val chainRegistry: ChainRegistry private val chainRegistry: ChainRegistry,
private val bridgeMultisigInteractor: BridgeMultisigInteractor
) : BaseViewModel() { ) : BaseViewModel() {
companion object { companion object {
@@ -76,6 +79,18 @@ class BridgeViewModel(
private val _warningText = MutableLiveData<String>() private val _warningText = MutableLiveData<String>()
val warningText: LiveData<String> = _warningText val warningText: LiveData<String> = _warningText
private val _signButtonVisible = MutableLiveData(false)
val signButtonVisible: LiveData<Boolean> = _signButtonVisible
private val _signButtonRed = MutableLiveData(false)
val signButtonRed: LiveData<Boolean> = _signButtonRed
private val _signButtonEnabled = MutableLiveData(false)
val signButtonEnabled: LiveData<Boolean> = _signButtonEnabled
private val _signButtonLabel = MutableLiveData("")
val signButtonLabel: LiveData<String> = _signButtonLabel
private var currentAmount: Double = 0.0 private var currentAmount: Double = 0.0
private var dotToHezRate: Double = FALLBACK_RATE private var dotToHezRate: Double = FALLBACK_RATE
private var isHezToDotActive: Boolean = false private var isHezToDotActive: Boolean = false
@@ -84,6 +99,7 @@ class BridgeViewModel(
init { init {
fetchExchangeRate() fetchExchangeRate()
fetchBridgeStatus() fetchBridgeStatus()
refreshSignerState()
} }
fun setPair(newPair: BridgePair) { fun setPair(newPair: BridgePair) {
@@ -321,5 +337,55 @@ class BridgeViewModel(
fun refreshBridgeStatus() { fun refreshBridgeStatus() {
fetchBridgeStatus() fetchBridgeStatus()
refreshSignerState()
}
fun refreshSignerState() {
launch {
val state = bridgeMultisigInteractor.getSignerState()
applySignerState(state)
}
}
fun signClicked() {
if (_signButtonEnabled.value != true) return
_signButtonEnabled.postValue(false)
_signButtonLabel.postValue(resourceManager.getString(R.string.bridge_sign_in_progress))
launch {
bridgeMultisigInteractor.submitRenewalSignature()
.onFailure {
_signButtonLabel.postValue(resourceManager.getString(R.string.bridge_sign_error))
}
refreshSignerState()
}
}
private fun applySignerState(state: BridgeSignerState?) {
if (state == null) {
_signButtonVisible.postValue(false)
return
}
_signButtonVisible.postValue(true)
when {
!state.needsRenewal -> {
_signButtonRed.postValue(false)
_signButtonEnabled.postValue(false)
_signButtonLabel.postValue(resourceManager.getString(R.string.bridge_sign_status_ok, state.signatoryRole))
}
state.alreadySignedPendingRenewal -> {
_signButtonRed.postValue(true)
_signButtonEnabled.postValue(false)
_signButtonLabel.postValue(resourceManager.getString(R.string.bridge_sign_waiting_others))
}
else -> {
_signButtonRed.postValue(true)
_signButtonEnabled.postValue(true)
_signButtonLabel.postValue(resourceManager.getString(R.string.bridge_sign_button, state.signatoryRole))
}
}
} }
} }
@@ -9,22 +9,40 @@ import dagger.multibindings.IntoMap
import io.novafoundation.nova.common.di.viewmodel.ViewModelKey import io.novafoundation.nova.common.di.viewmodel.ViewModelKey
import io.novafoundation.nova.common.di.viewmodel.ViewModelModule import io.novafoundation.nova.common.di.viewmodel.ViewModelModule
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.domain.interfaces.SelectedAccountUseCase
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.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.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 javax.inject.Named
@Module(includes = [ViewModelModule::class]) @Module(includes = [ViewModelModule::class])
class BridgeModule { class BridgeModule {
@Provides
fun provideBridgeMultisigInteractor(
chainRegistry: ChainRegistry,
selectedAccountUseCase: SelectedAccountUseCase,
@Named(REMOTE_STORAGE_SOURCE) storageDataSource: StorageDataSource,
extrinsicService: ExtrinsicService
): BridgeMultisigInteractor {
return RealBridgeMultisigInteractor(chainRegistry, selectedAccountUseCase, storageDataSource, extrinsicService)
}
@Provides @Provides
@IntoMap @IntoMap
@ViewModelKey(BridgeViewModel::class) @ViewModelKey(BridgeViewModel::class)
fun provideViewModel( fun provideViewModel(
router: AssetsRouter, router: AssetsRouter,
resourceManager: ResourceManager, resourceManager: ResourceManager,
chainRegistry: ChainRegistry chainRegistry: ChainRegistry,
bridgeMultisigInteractor: BridgeMultisigInteractor
): ViewModel { ): ViewModel {
return BridgeViewModel(router, resourceManager, chainRegistry) return BridgeViewModel(router, resourceManager, chainRegistry, bridgeMultisigInteractor)
} }
@Provides @Provides
@@ -330,6 +330,17 @@
android:textSize="12sp" android:textSize="12sp"
android:visibility="gone" /> android:visibility="gone" />
<!-- Multisig signatory-only bridge allowance sign button. Only visible to a wallet
that is one of the bridge multisig's 5 known signatories. -->
<io.novafoundation.nova.common.view.PrimaryButton
android:id="@+id/bridgeSignButton"
style="@style/Widget.Nova.Button.Primary"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:text="@string/bridge_sign_button"
android:visibility="gone" />
</LinearLayout> </LinearLayout>
</ScrollView> </ScrollView>