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:
2026-07-13 23:03:36 -07:00
parent 438a4510c8
commit 68a5b3ed9e
8 changed files with 196 additions and 21 deletions
@@ -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,
)
}
@@ -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)
} }
@@ -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
) )
@@ -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 {
@@ -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,
) )
} }
@@ -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)
}
} }
@@ -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()
@@ -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> {