mirror of
https://github.com/pezkuwichain/pezkuwi-wallet-android.git
synced 2026-07-22 13:45:48 +00:00
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:
@@ -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_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>
|
||||
|
||||
|
||||
|
||||
+73
@@ -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(),
|
||||
)
|
||||
)
|
||||
}
|
||||
+38
@@ -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"),
|
||||
)
|
||||
}
|
||||
+153
@@ -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()
|
||||
}
|
||||
}
|
||||
+78
@@ -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,
|
||||
)
|
||||
+32
@@ -50,6 +50,17 @@ class BridgeFragment : BaseFragment<BridgeViewModel, FragmentBridgeBinding>() {
|
||||
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<BridgeViewModel, FragmentBridgeBinding>() {
|
||||
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) {
|
||||
|
||||
+67
-1
@@ -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<String>()
|
||||
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 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))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+20
-2
@@ -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
|
||||
|
||||
@@ -330,6 +330,17 @@
|
||||
android:textSize="12sp"
|
||||
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>
|
||||
|
||||
</ScrollView>
|
||||
|
||||
Reference in New Issue
Block a user