diff --git a/common/src/main/res/values/strings.xml b/common/src/main/res/values/strings.xml
index f614b976..d9baff5e 100644
--- a/common/src/main/res/values/strings.xml
+++ b/common/src/main/res/values/strings.xml
@@ -371,6 +371,12 @@
HEZ→DOT swaps are temporarily unavailable. Please try again when DOT liquidity is sufficient.
USDT(Pez)→USDT(Pol) swaps are temporarily unavailable. Waiting for 1:1 liquidity to be established.
+ Bridge allowance OK (%s)
+ Sign renewal (%s)
+ Signed — waiting for other signers
+ Signing…
+ Signing failed, tap to retry
+
Buy/Sell
diff --git a/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/domain/bridge/multisig/BridgeMultisigCalls.kt b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/domain/bridge/multisig/BridgeMultisigCalls.kt
new file mode 100644
index 00000000..4f134374
--- /dev/null
+++ b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/domain/bridge/multisig/BridgeMultisigCalls.kt
@@ -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,
+ * #[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,
+ 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(),
+ )
+ )
+}
diff --git a/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/domain/bridge/multisig/BridgeMultisigConstants.kt b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/domain/bridge/multisig/BridgeMultisigConstants.kt
new file mode 100644
index 00000000..685c058e
--- /dev/null
+++ b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/domain/bridge/multisig/BridgeMultisigConstants.kt
@@ -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"),
+ )
+}
diff --git a/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/domain/bridge/multisig/BridgeMultisigInteractor.kt b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/domain/bridge/multisig/BridgeMultisigInteractor.kt
new file mode 100644
index 00000000..cb6a77b5
--- /dev/null
+++ b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/domain/bridge/multisig/BridgeMultisigInteractor.kt
@@ -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
+}
+
+@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 = 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()
+ }
+}
diff --git a/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/domain/bridge/multisig/BridgeMultisigRuntimeApi.kt b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/domain/bridge/multisig/BridgeMultisigRuntimeApi.kt
new file mode 100644
index 00000000..ccdf0162
--- /dev/null
+++ b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/domain/bridge/multisig/BridgeMultisigRuntimeApi.kt
@@ -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
+ 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,
+ 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
+ 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,
+ )
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 a694cbcc..6a35d7ca 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
@@ -50,6 +50,17 @@ class BridgeFragment : BaseFragment() {
binder.bridgeSwapButton.setOnClickListener {
viewModel.swapClicked()
}
+
+ // Multisig signatory-only sign button
+ binder.bridgeSignButton.setOnClickListener {
+ viewModel.signClicked()
+ }
+ }
+
+ override fun onResume() {
+ super.onResume()
+
+ viewModel.refreshBridgeStatus()
}
override fun inject() {
@@ -104,6 +115,27 @@ class BridgeFragment : BaseFragment() {
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) {
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 8421c1e0..5ae43621 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
@@ -6,6 +6,8 @@ import io.novafoundation.nova.common.base.BaseViewModel
import io.novafoundation.nova.common.resources.ResourceManager
import io.novafoundation.nova.common.view.ButtonState
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.send.amount.SendPayload
import io.novafoundation.nova.feature_wallet_api.presentation.model.AssetPayload
@@ -24,7 +26,8 @@ import java.net.URL
class BridgeViewModel(
private val router: AssetsRouter,
private val resourceManager: ResourceManager,
- private val chainRegistry: ChainRegistry
+ private val chainRegistry: ChainRegistry,
+ private val bridgeMultisigInteractor: BridgeMultisigInteractor
) : BaseViewModel() {
companion object {
@@ -76,6 +79,18 @@ class BridgeViewModel(
private val _warningText = MutableLiveData()
val warningText: LiveData = _warningText
+ private val _signButtonVisible = MutableLiveData(false)
+ val signButtonVisible: LiveData = _signButtonVisible
+
+ private val _signButtonRed = MutableLiveData(false)
+ val signButtonRed: LiveData = _signButtonRed
+
+ private val _signButtonEnabled = MutableLiveData(false)
+ val signButtonEnabled: LiveData = _signButtonEnabled
+
+ private val _signButtonLabel = MutableLiveData("")
+ val signButtonLabel: LiveData = _signButtonLabel
+
private var currentAmount: Double = 0.0
private var dotToHezRate: Double = FALLBACK_RATE
private var isHezToDotActive: Boolean = false
@@ -84,6 +99,7 @@ class BridgeViewModel(
init {
fetchExchangeRate()
fetchBridgeStatus()
+ refreshSignerState()
}
fun setPair(newPair: BridgePair) {
@@ -321,5 +337,55 @@ class BridgeViewModel(
fun refreshBridgeStatus() {
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))
+ }
+ }
}
}
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 3f297d99..9b36f6ff 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
@@ -9,22 +9,40 @@ import dagger.multibindings.IntoMap
import io.novafoundation.nova.common.di.viewmodel.ViewModelKey
import io.novafoundation.nova.common.di.viewmodel.ViewModelModule
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.bridge.BridgeViewModel
+import io.novafoundation.nova.runtime.di.REMOTE_STORAGE_SOURCE
import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry
+import io.novafoundation.nova.runtime.storage.source.StorageDataSource
+import javax.inject.Named
@Module(includes = [ViewModelModule::class])
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
@IntoMap
@ViewModelKey(BridgeViewModel::class)
fun provideViewModel(
router: AssetsRouter,
resourceManager: ResourceManager,
- chainRegistry: ChainRegistry
+ chainRegistry: ChainRegistry,
+ bridgeMultisigInteractor: BridgeMultisigInteractor
): ViewModel {
- return BridgeViewModel(router, resourceManager, chainRegistry)
+ return BridgeViewModel(router, resourceManager, chainRegistry, bridgeMultisigInteractor)
}
@Provides
diff --git a/feature-assets/src/main/res/layout/fragment_bridge.xml b/feature-assets/src/main/res/layout/fragment_bridge.xml
index 4bf845b2..a9cd542f 100644
--- a/feature-assets/src/main/res/layout/fragment_bridge.xml
+++ b/feature-assets/src/main/res/layout/fragment_bridge.xml
@@ -330,6 +330,17 @@
android:textSize="12sp"
android:visibility="gone" />
+
+
+