mirror of
https://github.com/pezkuwichain/pezkuwi-wallet-android.git
synced 2026-07-22 17:15:48 +00:00
Support first-signer case for /open/multisigOperation deep link
The pending-operation model assumed a call already exists on-chain (Multisig.Multisigs entry with a Timepoint) before it could be reviewed or signed. A signatory who is the first to see a brand-new call - reachable via the deep link before anyone has submitted anything - hit "operation not found" and the screen refused to open, even though the deep link already carried the call's data in its callData param (previously decoded only to format Executed/Rejected dialog text, then discarded for the Active case that needed it). - PendingMultisigOperation.timePoint/depositor are now nullable to represent "not yet submitted"; composeMultisigAsMulti's maybeTimePoint parameter already accepted null (used for ordinary first-time multisig-origin submissions via MultisigSigner), so the pallet-call layer needed no changes. - New PendingMultisigOperation.notYetSubmitted(...) factory and MultisigOperationDetailsInteractor.buildNotYetSubmittedOperation() build a synthetic operation from the deep link's callData, hash verified against the URL's callHash rather than trusted blindly. - OperationIsStillPendingValidation now skips its on-chain "still pending" check for a not-yet-submitted operation, since it would otherwise always fail and block the very first submission. - MultisigOperationPayload carries the callData through so both the details screen and the full-details screen (independently reachable via "Call Details", since that button is available whenever call data is known) can construct the synthetic operation.
This commit is contained in:
+53
-2
@@ -4,6 +4,7 @@ import io.novafoundation.nova.common.address.AccountIdKey
|
|||||||
import io.novafoundation.nova.common.address.toHex
|
import io.novafoundation.nova.common.address.toHex
|
||||||
import io.novafoundation.nova.common.utils.Identifiable
|
import io.novafoundation.nova.common.utils.Identifiable
|
||||||
import io.novafoundation.nova.feature_account_api.domain.model.MetaAccount
|
import io.novafoundation.nova.feature_account_api.domain.model.MetaAccount
|
||||||
|
import io.novafoundation.nova.feature_account_api.domain.model.MultisigMetaAccount
|
||||||
import io.novafoundation.nova.feature_account_api.domain.model.addressIn
|
import io.novafoundation.nova.feature_account_api.domain.model.addressIn
|
||||||
import io.novafoundation.nova.feature_account_api.domain.multisig.CallHash
|
import io.novafoundation.nova.feature_account_api.domain.multisig.CallHash
|
||||||
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain
|
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain
|
||||||
@@ -18,9 +19,24 @@ class PendingMultisigOperation(
|
|||||||
val call: GenericCall.Instance?,
|
val call: GenericCall.Instance?,
|
||||||
val callHash: CallHash,
|
val callHash: CallHash,
|
||||||
val chain: Chain,
|
val chain: Chain,
|
||||||
val timePoint: MultisigTimePoint,
|
/**
|
||||||
|
* Null means this call has never been submitted on-chain yet (no `Multisig.Multisigs` entry
|
||||||
|
* exists) - the "first signer" case reachable via a `/open/multisigOperation` deep link
|
||||||
|
* before anyone has signed. `composeMultisigAsMulti`'s `maybeTimePoint` parameter already
|
||||||
|
* accepts null for exactly this (see `MultisigSigner.wrapCallsInAsMulti`, which uses the
|
||||||
|
* same null-timepoint-for-first-submission pattern for ordinary multisig-origin calls) -
|
||||||
|
* this model just didn't have a way to represent that state before.
|
||||||
|
*/
|
||||||
|
val timePoint: MultisigTimePoint?,
|
||||||
val approvals: List<AccountIdKey>,
|
val approvals: List<AccountIdKey>,
|
||||||
val depositor: AccountIdKey,
|
/**
|
||||||
|
* Null for the same not-yet-submitted case as [timePoint] - nobody has deposited/proposed
|
||||||
|
* this call yet, so there is no depositor. [userAction] already handles this correctly:
|
||||||
|
* comparing [signatoryAccountId] to a null depositor is simply never true, so a signatory
|
||||||
|
* viewing a not-yet-submitted call is never offered "Reject" (rejecting something that was
|
||||||
|
* never proposed makes no sense - only cancel_as_multi on an *existing* operation does).
|
||||||
|
*/
|
||||||
|
val depositor: AccountIdKey?,
|
||||||
val deposit: BigInteger,
|
val deposit: BigInteger,
|
||||||
val signatoryAccountId: AccountIdKey,
|
val signatoryAccountId: AccountIdKey,
|
||||||
val signatoryMetaId: Long,
|
val signatoryMetaId: Long,
|
||||||
@@ -30,6 +46,9 @@ class PendingMultisigOperation(
|
|||||||
|
|
||||||
val operationId = PendingMultisigOperationId(multisigMetaId, chain.id, callHash.toHex())
|
val operationId = PendingMultisigOperationId(multisigMetaId, chain.id, callHash.toHex())
|
||||||
|
|
||||||
|
val isSubmittedOnChain: Boolean
|
||||||
|
get() = timePoint != null
|
||||||
|
|
||||||
override val identifier: String = operationId.identifier()
|
override val identifier: String = operationId.identifier()
|
||||||
|
|
||||||
override fun toString(): String {
|
override fun toString(): String {
|
||||||
@@ -78,3 +97,35 @@ fun PendingMultisigOperation.Companion.createOperationHash(metaAccount: MetaAcco
|
|||||||
fun PendingMultisigOperationId.Companion.create(metaAccount: MetaAccount, chain: Chain, callHash: String): PendingMultisigOperationId {
|
fun PendingMultisigOperationId.Companion.create(metaAccount: MetaAccount, chain: Chain, callHash: String): PendingMultisigOperationId {
|
||||||
return PendingMultisigOperationId(metaAccount.id, chain.id, callHash)
|
return PendingMultisigOperationId(metaAccount.id, chain.id, callHash)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Builds a synthetic [PendingMultisigOperation] for a call that has never been submitted
|
||||||
|
* on-chain - the deep-link "first signer" case (see `MultisigOperationDetailsDeepLinkHandler`
|
||||||
|
* and `RealMultisigOperationDetailsInteractor.buildNotYetSubmittedOperation`). Unlike
|
||||||
|
* `PendingMultisigOperation.from` (used by the chain-storage-driven syncer), this never touches
|
||||||
|
* chain state - everything it needs (threshold, other signatories) is already known locally
|
||||||
|
* from the already-added [MultisigMetaAccount], and [call] comes from the deep link's `callData`
|
||||||
|
* param, whose hash the caller must already have verified matches the link's `callHash`.
|
||||||
|
*/
|
||||||
|
fun PendingMultisigOperation.Companion.notYetSubmitted(
|
||||||
|
multisigMetaAccount: MultisigMetaAccount,
|
||||||
|
call: GenericCall.Instance,
|
||||||
|
callHash: CallHash,
|
||||||
|
chain: Chain,
|
||||||
|
timestamp: Duration,
|
||||||
|
): PendingMultisigOperation {
|
||||||
|
return PendingMultisigOperation(
|
||||||
|
multisigMetaId = multisigMetaAccount.id,
|
||||||
|
call = call,
|
||||||
|
callHash = callHash,
|
||||||
|
chain = chain,
|
||||||
|
timePoint = null,
|
||||||
|
approvals = emptyList(),
|
||||||
|
depositor = null,
|
||||||
|
deposit = BigInteger.ZERO,
|
||||||
|
signatoryAccountId = multisigMetaAccount.signatoryAccountId,
|
||||||
|
signatoryMetaId = multisigMetaAccount.signatoryMetaId,
|
||||||
|
threshold = multisigMetaAccount.threshold,
|
||||||
|
timestamp = timestamp,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|||||||
+1
-1
@@ -53,7 +53,7 @@ fun TableCellView.showWallet(walletModel: WalletModel) {
|
|||||||
walletModel.icon?.let(::setImage)
|
walletModel.icon?.let(::setImage)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun TableCellView.showAccountWithLoading(loadingState: ExtendedLoadingState<AccountModel>) {
|
fun TableCellView.showAccountWithLoading(loadingState: ExtendedLoadingState<AccountModel?>) {
|
||||||
showLoadingState(loadingState) {
|
showLoadingState(loadingState) {
|
||||||
showAccount(it)
|
showAccount(it)
|
||||||
}
|
}
|
||||||
|
|||||||
+44
-1
@@ -18,6 +18,7 @@ import io.novafoundation.nova.feature_account_api.data.multisig.composeMultisigC
|
|||||||
import io.novafoundation.nova.feature_account_api.data.multisig.model.MultisigAction
|
import io.novafoundation.nova.feature_account_api.data.multisig.model.MultisigAction
|
||||||
import io.novafoundation.nova.feature_account_api.data.multisig.model.PendingMultisigOperation
|
import io.novafoundation.nova.feature_account_api.data.multisig.model.PendingMultisigOperation
|
||||||
import io.novafoundation.nova.feature_account_api.data.multisig.model.PendingMultisigOperationId
|
import io.novafoundation.nova.feature_account_api.data.multisig.model.PendingMultisigOperationId
|
||||||
|
import io.novafoundation.nova.feature_account_api.data.multisig.model.notYetSubmitted
|
||||||
import io.novafoundation.nova.feature_account_api.data.multisig.model.userAction
|
import io.novafoundation.nova.feature_account_api.data.multisig.model.userAction
|
||||||
import io.novafoundation.nova.feature_account_api.data.multisig.repository.MultisigOperationLocalCallRepository
|
import io.novafoundation.nova.feature_account_api.data.multisig.repository.MultisigOperationLocalCallRepository
|
||||||
import io.novafoundation.nova.feature_account_api.domain.interfaces.AccountRepository
|
import io.novafoundation.nova.feature_account_api.domain.interfaces.AccountRepository
|
||||||
@@ -37,10 +38,12 @@ import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain
|
|||||||
import io.novafoundation.nova.runtime.multiNetwork.chain.model.ChainId
|
import io.novafoundation.nova.runtime.multiNetwork.chain.model.ChainId
|
||||||
import io.novafoundation.nova.runtime.multiNetwork.getRuntime
|
import io.novafoundation.nova.runtime.multiNetwork.getRuntime
|
||||||
import io.novasama.substrate_sdk_android.extensions.toHexString
|
import io.novasama.substrate_sdk_android.extensions.toHexString
|
||||||
|
import io.novasama.substrate_sdk_android.runtime.definitions.types.fromHex
|
||||||
import io.novasama.substrate_sdk_android.runtime.definitions.types.generics.GenericCall
|
import io.novasama.substrate_sdk_android.runtime.definitions.types.generics.GenericCall
|
||||||
import io.novasama.substrate_sdk_android.runtime.extrinsic.builder.ExtrinsicBuilder
|
import io.novasama.substrate_sdk_android.runtime.extrinsic.builder.ExtrinsicBuilder
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
import kotlin.time.Duration.Companion.milliseconds
|
||||||
|
|
||||||
interface MultisigOperationDetailsInteractor {
|
interface MultisigOperationDetailsInteractor {
|
||||||
|
|
||||||
@@ -67,6 +70,14 @@ interface MultisigOperationDetailsInteractor {
|
|||||||
suspend fun callDataAsString(call: GenericCall.Instance, chainId: ChainId): String
|
suspend fun callDataAsString(call: GenericCall.Instance, chainId: ChainId): String
|
||||||
|
|
||||||
suspend fun isOperationAvailable(operationId: PendingMultisigOperationId): Boolean
|
suspend fun isOperationAvailable(operationId: PendingMultisigOperationId): Boolean
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Builds a [PendingMultisigOperation] for a call that has never been submitted on-chain yet
|
||||||
|
* (no `Multisig.Multisigs` entry exists) - the "first signer" deep-link case. [callDataHex]
|
||||||
|
* must decode to a call whose hash matches [operationId]'s `callHash`; this is verified here
|
||||||
|
* rather than trusted blindly, since [callDataHex] ultimately comes from a URL.
|
||||||
|
*/
|
||||||
|
suspend fun buildNotYetSubmittedOperation(operationId: PendingMultisigOperationId, callDataHex: String): PendingMultisigOperation
|
||||||
}
|
}
|
||||||
|
|
||||||
private const val SKIP_REJECT_CONFIRMATION_KEY = "SKIP_REJECT_CONFIRMATION_KEY"
|
private const val SKIP_REJECT_CONFIRMATION_KEY = "SKIP_REJECT_CONFIRMATION_KEY"
|
||||||
@@ -118,6 +129,30 @@ class RealMultisigOperationDetailsInteractor @Inject constructor(
|
|||||||
return multisigDetailsRepository.hasMultisigOperation(chain, metaAccount.requireAccountIdKeyIn(chain), callHash)
|
return multisigDetailsRepository.hasMultisigOperation(chain, metaAccount.requireAccountIdKeyIn(chain), callHash)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override suspend fun buildNotYetSubmittedOperation(
|
||||||
|
operationId: PendingMultisigOperationId,
|
||||||
|
callDataHex: String
|
||||||
|
): PendingMultisigOperation {
|
||||||
|
val chain = chainRegistry.getChain(operationId.chainId)
|
||||||
|
val metaAccount = accountRepository.getMetaAccount(operationId.metaId) as MultisigMetaAccount
|
||||||
|
val runtime = chainRegistry.getRuntime(chain.id)
|
||||||
|
|
||||||
|
val call = GenericCall.fromHex(runtime, callDataHex)
|
||||||
|
val computedHash = call.callHash(runtime)
|
||||||
|
val expectedHash = operationId.callHash.intoCallHash()
|
||||||
|
require(computedHash.contentEquals(expectedHash.value)) {
|
||||||
|
"Call data does not match this operation: decoded hash does not equal the expected call hash"
|
||||||
|
}
|
||||||
|
|
||||||
|
return PendingMultisigOperation.notYetSubmitted(
|
||||||
|
multisigMetaAccount = metaAccount,
|
||||||
|
call = call,
|
||||||
|
callHash = expectedHash,
|
||||||
|
chain = chain,
|
||||||
|
timestamp = System.currentTimeMillis().milliseconds
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
override suspend fun estimateActionFee(operation: PendingMultisigOperation): Fee? {
|
override suspend fun estimateActionFee(operation: PendingMultisigOperation): Fee? {
|
||||||
val action = operation.userAction().toInternalAction() ?: return null
|
val action = operation.userAction().toInternalAction() ?: return null
|
||||||
|
|
||||||
@@ -223,10 +258,18 @@ class RealMultisigOperationDetailsInteractor @Inject constructor(
|
|||||||
|
|
||||||
private suspend fun ExtrinsicBuilder.reject(operation: PendingMultisigOperation) {
|
private suspend fun ExtrinsicBuilder.reject(operation: PendingMultisigOperation) {
|
||||||
val selectedAccount = accountRepository.getSelectedMetaAccount() as MultisigMetaAccount
|
val selectedAccount = accountRepository.getSelectedMetaAccount() as MultisigMetaAccount
|
||||||
|
// cancel_as_multi can only ever cancel an operation that already exists on-chain (you
|
||||||
|
// can't cancel something nobody has proposed yet) - userAction() already never returns
|
||||||
|
// CanReject for a not-yet-submitted operation (its depositor is null), so this should be
|
||||||
|
// unreachable in practice; checkNotNull fails loudly instead of silently misencoding a
|
||||||
|
// null timepoint if that invariant is ever violated.
|
||||||
|
val timePoint = checkNotNull(operation.timePoint) {
|
||||||
|
"Cannot reject an operation that has not been submitted on-chain yet"
|
||||||
|
}
|
||||||
|
|
||||||
val approveCall = runtime.composeMultisigCancelAsMulti(
|
val approveCall = runtime.composeMultisigCancelAsMulti(
|
||||||
multisigMetaAccount = selectedAccount,
|
multisigMetaAccount = selectedAccount,
|
||||||
maybeTimePoint = operation.timePoint,
|
maybeTimePoint = timePoint,
|
||||||
callHash = operation.callHash
|
callHash = operation.callHash
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
+9
@@ -12,6 +12,15 @@ class OperationIsStillPendingValidation @Inject constructor(
|
|||||||
) : ApproveMultisigOperationValidation {
|
) : ApproveMultisigOperationValidation {
|
||||||
|
|
||||||
override suspend fun validate(value: ApproveMultisigOperationValidationPayload): ValidationStatus<ApproveMultisigOperationValidationFailure> {
|
override suspend fun validate(value: ApproveMultisigOperationValidationPayload): ValidationStatus<ApproveMultisigOperationValidationFailure> {
|
||||||
|
// A not-yet-submitted operation (first signer, reached via deep link before anyone has
|
||||||
|
// signed - see PendingMultisigOperation.notYetSubmitted) has, by definition, no
|
||||||
|
// Multisig.Multisigs entry yet - that's exactly what this submission is about to
|
||||||
|
// create. Checking "is it still pending" only makes sense for an operation that was
|
||||||
|
// already pending; skip it here rather than failing a legitimate first submission.
|
||||||
|
if (!value.operation.isSubmittedOnChain) {
|
||||||
|
return ValidationStatus.Valid()
|
||||||
|
}
|
||||||
|
|
||||||
val hasPendingCallHash = multisigValidationsRepository.hasPendingCallHash(value.chain.id, value.multisigAccountId, value.operation.callHash)
|
val hasPendingCallHash = multisigValidationsRepository.hasPendingCallHash(value.chain.id, value.multisigAccountId, value.operation.callHash)
|
||||||
|
|
||||||
return hasPendingCallHash isTrueOrError {
|
return hasPendingCallHash isTrueOrError {
|
||||||
|
|||||||
+13
-3
@@ -8,16 +8,26 @@ import kotlinx.parcelize.Parcelize
|
|||||||
class MultisigOperationPayload(
|
class MultisigOperationPayload(
|
||||||
val chainId: String,
|
val chainId: String,
|
||||||
val metaId: Long,
|
val metaId: Long,
|
||||||
val callHash: String
|
val callHash: String,
|
||||||
|
/**
|
||||||
|
* Call data for a call that may not exist on-chain yet (the "first signer" deep-link case -
|
||||||
|
* see `MultisigOperationDetailsDeepLinkHandler`). Null for the normal case where the details
|
||||||
|
* screen sources everything from the chain-storage-driven sync service.
|
||||||
|
*/
|
||||||
|
val notSubmittedCallData: String? = null,
|
||||||
) : Parcelable {
|
) : Parcelable {
|
||||||
companion object;
|
companion object;
|
||||||
}
|
}
|
||||||
|
|
||||||
fun MultisigOperationPayload.Companion.fromOperationId(operationId: PendingMultisigOperationId): MultisigOperationPayload {
|
fun MultisigOperationPayload.Companion.fromOperationId(
|
||||||
|
operationId: PendingMultisigOperationId,
|
||||||
|
notSubmittedCallData: String? = null,
|
||||||
|
): MultisigOperationPayload {
|
||||||
return MultisigOperationPayload(
|
return MultisigOperationPayload(
|
||||||
chainId = operationId.chainId,
|
chainId = operationId.chainId,
|
||||||
metaId = operationId.metaId,
|
metaId = operationId.metaId,
|
||||||
callHash = operationId.callHash
|
callHash = operationId.callHash,
|
||||||
|
notSubmittedCallData = notSubmittedCallData,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+12
-1
@@ -67,7 +67,14 @@ class MultisigOperationDetailsDeepLinkHandler(
|
|||||||
null,
|
null,
|
||||||
MultisigOperationDeepLinkData.State.Active -> {
|
MultisigOperationDeepLinkData.State.Active -> {
|
||||||
val operationIdentifier = PendingMultisigOperationId.create(multisigMetaAccount, chain, callHash.removeHexPrefix())
|
val operationIdentifier = PendingMultisigOperationId.create(multisigMetaAccount, chain, callHash.removeHexPrefix())
|
||||||
val operationPayload = MultisigOperationPayload.fromOperationId(operationIdentifier)
|
// Threaded through so the details screen can build a synthetic operation for a
|
||||||
|
// call that hasn't been submitted on-chain yet (the first-signer case) - it used
|
||||||
|
// to be decoded here only to format Executed/Rejected dialog text, then silently
|
||||||
|
// dropped for the Active case that would actually need it.
|
||||||
|
val operationPayload = MultisigOperationPayload.fromOperationId(
|
||||||
|
operationIdentifier,
|
||||||
|
notSubmittedCallData = data.getRawCallData()
|
||||||
|
)
|
||||||
router.openMultisigOperationDetails(
|
router.openMultisigOperationDetails(
|
||||||
MultisigOperationDetailsPayload(
|
MultisigOperationDetailsPayload(
|
||||||
operationPayload,
|
operationPayload,
|
||||||
@@ -117,4 +124,8 @@ class MultisigOperationDetailsDeepLinkHandler(
|
|||||||
val runtime = chainRegistry.getRuntime(chainId)
|
val runtime = chainRegistry.getRuntime(chainId)
|
||||||
return GenericCall.fromHex(runtime, callDataString)
|
return GenericCall.fromHex(runtime, callDataString)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun Uri.getRawCallData(): String? {
|
||||||
|
return getQueryParameter(MultisigOperationDeepLinkConfigurator.CALL_DATA_PARAM)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+30
-5
@@ -11,6 +11,7 @@ import io.novafoundation.nova.common.utils.launchUnit
|
|||||||
import io.novafoundation.nova.common.utils.withSafeLoading
|
import io.novafoundation.nova.common.utils.withSafeLoading
|
||||||
import io.novafoundation.nova.common.view.bottomSheet.description.DescriptionBottomSheetLauncher
|
import io.novafoundation.nova.common.view.bottomSheet.description.DescriptionBottomSheetLauncher
|
||||||
import io.novafoundation.nova.feature_account_api.data.multisig.MultisigPendingOperationsService
|
import io.novafoundation.nova.feature_account_api.data.multisig.MultisigPendingOperationsService
|
||||||
|
import io.novafoundation.nova.feature_account_api.data.multisig.model.PendingMultisigOperation
|
||||||
import io.novafoundation.nova.feature_account_api.domain.interfaces.AccountUIUseCase
|
import io.novafoundation.nova.feature_account_api.domain.interfaces.AccountUIUseCase
|
||||||
import io.novafoundation.nova.feature_account_api.presenatation.actions.ExternalActions
|
import io.novafoundation.nova.feature_account_api.presenatation.actions.ExternalActions
|
||||||
import io.novafoundation.nova.feature_account_api.presenatation.actions.showAddressActions
|
import io.novafoundation.nova.feature_account_api.presenatation.actions.showAddressActions
|
||||||
@@ -24,10 +25,12 @@ import io.novafoundation.nova.feature_wallet_api.presentation.formatters.amount.
|
|||||||
import io.novafoundation.nova.feature_wallet_api.presentation.formatters.amount.formatAmountToAmountModel
|
import io.novafoundation.nova.feature_wallet_api.presentation.formatters.amount.formatAmountToAmountModel
|
||||||
import io.novafoundation.nova.runtime.ext.fullId
|
import io.novafoundation.nova.runtime.ext.fullId
|
||||||
import io.novafoundation.nova.runtime.ext.utilityAsset
|
import io.novafoundation.nova.runtime.ext.utilityAsset
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.combine
|
import kotlinx.coroutines.flow.combine
|
||||||
import kotlinx.coroutines.flow.filterNotNull
|
import kotlinx.coroutines.flow.filterNotNull
|
||||||
import kotlinx.coroutines.flow.first
|
import kotlinx.coroutines.flow.first
|
||||||
import kotlinx.coroutines.flow.map
|
import kotlinx.coroutines.flow.map
|
||||||
|
import kotlinx.coroutines.flow.merge
|
||||||
|
|
||||||
private const val CALL_HASH_SHOWN_SYMBOLS = 9
|
private const val CALL_HASH_SHOWN_SYMBOLS = 9
|
||||||
|
|
||||||
@@ -52,16 +55,38 @@ class MultisigOperationFullDetailsViewModel(
|
|||||||
router.back()
|
router.back()
|
||||||
}
|
}
|
||||||
|
|
||||||
private val operationFlow = multisigOperationsService.pendingOperationFlow(payload.toOperationId())
|
// Mirrors MultisigOperationDetailsViewModel's not-yet-submitted handling - this screen is
|
||||||
.filterNotNull()
|
// reachable (via "Call Details") for a first-signer deep-link operation too, since
|
||||||
.shareInBackground()
|
// callDetailsVisible there is based on `call != null`, which is always true here.
|
||||||
|
private val notYetSubmittedOperationFlow = MutableStateFlow<PendingMultisigOperation?>(null)
|
||||||
|
|
||||||
|
private val operationFlow = merge(
|
||||||
|
multisigOperationsService.pendingOperationFlow(payload.toOperationId()).filterNotNull(),
|
||||||
|
notYetSubmittedOperationFlow.filterNotNull()
|
||||||
|
).shareInBackground()
|
||||||
|
|
||||||
|
init {
|
||||||
|
checkOperationAvailability()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun checkOperationAvailability() = launchUnit {
|
||||||
|
val operationId = payload.toOperationId()
|
||||||
|
if (interactor.isOperationAvailable(operationId)) return@launchUnit
|
||||||
|
|
||||||
|
val notSubmittedCallData = payload.notSubmittedCallData ?: return@launchUnit
|
||||||
|
val builtOperation = runCatching {
|
||||||
|
interactor.buildNotYetSubmittedOperation(operationId, notSubmittedCallData)
|
||||||
|
}.getOrNull() ?: return@launchUnit
|
||||||
|
|
||||||
|
notYetSubmittedOperationFlow.value = builtOperation
|
||||||
|
}
|
||||||
|
|
||||||
private val tokenFlow = operationFlow.map {
|
private val tokenFlow = operationFlow.map {
|
||||||
arbitraryTokenUseCase.getToken(it.chain.utilityAsset.fullId)
|
arbitraryTokenUseCase.getToken(it.chain.utilityAsset.fullId)
|
||||||
}.shareInBackground()
|
}.shareInBackground()
|
||||||
|
|
||||||
val depositorAccountModel = operationFlow.map {
|
val depositorAccountModel = operationFlow.map { operation ->
|
||||||
accountUIUseCase.getAccountModel(it.depositor, it.chain)
|
operation.depositor?.let { accountUIUseCase.getAccountModel(it, operation.chain) }
|
||||||
}.withSafeLoading()
|
}.withSafeLoading()
|
||||||
.shareInBackground()
|
.shareInBackground()
|
||||||
|
|
||||||
|
|||||||
+34
-8
@@ -68,6 +68,7 @@ import kotlinx.coroutines.flow.filterNotNull
|
|||||||
import kotlinx.coroutines.flow.first
|
import kotlinx.coroutines.flow.first
|
||||||
import kotlinx.coroutines.flow.flatMapLatest
|
import kotlinx.coroutines.flow.flatMapLatest
|
||||||
import kotlinx.coroutines.flow.map
|
import kotlinx.coroutines.flow.map
|
||||||
|
import kotlinx.coroutines.flow.merge
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.suspendCancellableCoroutine
|
import kotlinx.coroutines.suspendCancellableCoroutine
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
@@ -99,9 +100,15 @@ class MultisigOperationDetailsViewModel(
|
|||||||
|
|
||||||
val operationNotFoundAwaitableAction = actionAwaitableMixinFactory.confirmingAction<ConfirmationDialogInfo>()
|
val operationNotFoundAwaitableAction = actionAwaitableMixinFactory.confirmingAction<ConfirmationDialogInfo>()
|
||||||
|
|
||||||
private val operationFlow = multisigOperationsService.pendingOperationFlow(payload.operation.toOperationId())
|
// Populated only for the "first signer" deep-link case: a call that has no
|
||||||
.filterNotNull()
|
// Multisig.Multisigs entry on chain yet, built directly from the deep link's callData
|
||||||
.shareInBackground()
|
// instead of the chain-storage-driven sync service (see checkOperationAvailability()).
|
||||||
|
private val notYetSubmittedOperationFlow = MutableStateFlow<PendingMultisigOperation?>(null)
|
||||||
|
|
||||||
|
private val operationFlow = merge(
|
||||||
|
multisigOperationsService.pendingOperationFlow(payload.operation.toOperationId()).filterNotNull(),
|
||||||
|
notYetSubmittedOperationFlow.filterNotNull()
|
||||||
|
).shareInBackground()
|
||||||
|
|
||||||
private val isLastOperationFlow = flowOf {
|
private val isLastOperationFlow = flowOf {
|
||||||
val operationsCount = multisigOperationsService.getPendingOperationsCount()
|
val operationsCount = multisigOperationsService.getPendingOperationsCount()
|
||||||
@@ -226,11 +233,26 @@ class MultisigOperationDetailsViewModel(
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun checkOperationAvailability() = launchUnit {
|
private fun checkOperationAvailability() = launchUnit {
|
||||||
val isOperationAvailable = interactor.isOperationAvailable(payload.operation.toOperationId())
|
val operationId = payload.operation.toOperationId()
|
||||||
|
val isOperationAvailable = interactor.isOperationAvailable(operationId)
|
||||||
|
|
||||||
if (!isOperationAvailable) {
|
if (isOperationAvailable) return@launchUnit
|
||||||
showErrorAndCloseScreen()
|
|
||||||
|
// Not on chain yet - only a real gap if this isn't the deep-link "first signer" case
|
||||||
|
// (no callData supplied means it really is an unknown/stale operation, same as before).
|
||||||
|
val notSubmittedCallData = payload.operation.notSubmittedCallData
|
||||||
|
if (notSubmittedCallData != null) {
|
||||||
|
val builtOperation = runCatching {
|
||||||
|
interactor.buildNotYetSubmittedOperation(operationId, notSubmittedCallData)
|
||||||
|
}.getOrNull()
|
||||||
|
|
||||||
|
if (builtOperation != null) {
|
||||||
|
notYetSubmittedOperationFlow.value = builtOperation
|
||||||
|
return@launchUnit
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
showErrorAndCloseScreen()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun enterCallDataClicked() {
|
fun enterCallDataClicked() {
|
||||||
@@ -251,8 +273,12 @@ class MultisigOperationDetailsViewModel(
|
|||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun PendingMultisigOperation.getDepositorName(): String {
|
private suspend fun PendingMultisigOperation.getDepositorName(): String {
|
||||||
val depositorAccount = withContext(Dispatchers.Default) { accountInteractor.findMetaAccount(chain, depositor.value) }
|
// Only called from actionClicked() when userAction() == CanReject, which never happens
|
||||||
return depositorAccount?.name ?: chain.addressOf(depositor)
|
// when depositor is null (a not-yet-submitted operation has nothing to reject) - fails
|
||||||
|
// loudly instead of silently if that invariant is ever violated.
|
||||||
|
val depositorId = checkNotNull(depositor) { "Cannot reject an operation with no depositor" }
|
||||||
|
val depositorAccount = withContext(Dispatchers.Default) { accountInteractor.findMetaAccount(chain, depositorId.value) }
|
||||||
|
return depositorAccount?.name ?: chain.addressOf(depositorId)
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun confirmReject(depositorName: String) = suspendCancellableCoroutine<Unit> {
|
private suspend fun confirmReject(depositorName: String) = suspendCancellableCoroutine<Unit> {
|
||||||
|
|||||||
Reference in New Issue
Block a user