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" /> + + +