mirror of
https://github.com/pezkuwichain/pezkuwi-wallet-android.git
synced 2026-07-22 14:55:48 +00:00
Fix Pending Signatures: query the bridge's own hardcoded multisig, not the generic multisig-account feature
The previous commit reused MultisigPendingOperationsService, but that service only syncs accounts formally registered as a MultisigMetaAccount in this wallet - it silently returns an empty NoOpSyncer for every other account type. The bridge's 5 signatories (Serok etc.) use their own regular wallets to approve calls against the bridge's separate, hardcoded multisig (BridgeMultisigConstants) - they never "are" that multisig account, so the feature was structurally guaranteed to show nothing, not just empty by coincidence. Fix queries the bridge's own multisig accounts directly, mirroring the exact pattern this screen's existing renewal-signing code already uses (BridgeMultisigRuntimeApi's runtime-metadata accessors, a local mirror of feature-account-impl's off-chain call-data indexer client - same module-boundary reasoning already documented in that file) rather than the generic, differently-scoped service: - BridgeMultisigInteractor.getPendingApprovals(): enumerates Multisig.Multisigs entries for both bridge multisig accounts via QueryableStorageEntry2.keys()/entries() (a general runtime-storage capability, not multisig-feature-specific), fetches real call content from the same off-chain indexer the generic feature uses (also general-purpose, keyed by address+hashes - never required the account to be a MultisigMetaAccount either), and filters to calls this signatory hasn't approved yet. - BridgeMultisigInteractor.submitApproval(): reuses composeBridgeMultisigAsMulti generalized to an arbitrary call+ timepoint (previously only used for the renewal call) - refuses to proceed if the call content is unknown, never blind-signs. - BridgeViewModel formats the real parsed transfer (amount+destination) via AssetSourceRegistry.tryParseTransfer, already used elsewhere for the exact same "decode a transfer call" job - no fabricated preview data. Reverts the feature-multisig:operations cross-feature dependency and FeatureApi wiring added in the previous commit - no longer needed since everything now lives in this screen's own existing self-contained domain layer, matching how the rest of BridgeMultisigInteractor already works (and already explains, in its own doc comments, why a synthetic MultisigMetaAccount was deliberately avoided for exactly this reason).
This commit is contained in:
@@ -108,8 +108,6 @@ import io.novafoundation.nova.feature_ledger_impl.presentation.account.addChain.
|
||||
import io.novafoundation.nova.feature_ledger_impl.presentation.account.common.selectLedger.SelectLedgerPayload
|
||||
import io.novafoundation.nova.feature_multisig_operations.presentation.created.MultisigCreatedBottomSheet
|
||||
import io.novafoundation.nova.feature_multisig_operations.presentation.created.MultisigCreatedPayload
|
||||
import io.novafoundation.nova.feature_multisig_operations.presentation.details.general.MultisigOperationDetailsFragment
|
||||
import io.novafoundation.nova.feature_multisig_operations.presentation.details.general.MultisigOperationDetailsPayload
|
||||
import io.novafoundation.nova.feature_onboarding_impl.OnboardingRouter
|
||||
import io.novafoundation.nova.feature_onboarding_impl.presentation.welcome.WelcomeFragment
|
||||
import io.novafoundation.nova.feature_swap_api.presentation.model.SwapSettingsPayload
|
||||
@@ -985,12 +983,6 @@ class Navigator(
|
||||
.navigateInFirstAttachedContext()
|
||||
}
|
||||
|
||||
override fun openMultisigOperationDetails(payload: MultisigOperationDetailsPayload) {
|
||||
navigationBuilder().action(R.id.action_multisigOperationDetailsFragment)
|
||||
.setArgs(MultisigOperationDetailsFragment.createPayload(payload))
|
||||
.navigateInFirstAttachedContext()
|
||||
}
|
||||
|
||||
override fun openMainWithFinishMultisigTransaction(accountWasSwitched: Boolean) {
|
||||
val payload = MultisigCreatedBottomSheet.createPayload(MultisigCreatedPayload(accountWasSwitched))
|
||||
openSplitScreenWithInstantAction(R.id.action_open_multisigCreatedDialog, nestedActionExtras = payload)
|
||||
|
||||
@@ -48,7 +48,6 @@ dependencies {
|
||||
implementation project(':feature-deep-linking')
|
||||
implementation project(':feature-ahm-api')
|
||||
implementation project(':feature-gift-api')
|
||||
implementation project(':feature-multisig:operations')
|
||||
|
||||
implementation kotlinDep
|
||||
|
||||
|
||||
+1
-3
@@ -53,7 +53,6 @@ import io.novafoundation.nova.feature_crowdloan_api.di.CrowdloanFeatureApi
|
||||
import io.novafoundation.nova.feature_currency_api.di.CurrencyFeatureApi
|
||||
import io.novafoundation.nova.feature_deep_linking.di.DeepLinkingFeatureApi
|
||||
import io.novafoundation.nova.feature_gift_api.di.GiftFeatureApi
|
||||
import io.novafoundation.nova.feature_multisig_operations.di.MultisigOperationsFeatureApi
|
||||
import io.novafoundation.nova.feature_nft_api.NftFeatureApi
|
||||
import io.novafoundation.nova.feature_staking_api.di.StakingFeatureApi
|
||||
import io.novafoundation.nova.feature_swap_api.di.SwapFeatureApi
|
||||
@@ -176,8 +175,7 @@ interface AssetsFeatureComponent : AssetsFeatureApi {
|
||||
BannersFeatureApi::class,
|
||||
DeepLinkingFeatureApi::class,
|
||||
ChainMigrationFeatureApi::class,
|
||||
GiftFeatureApi::class,
|
||||
MultisigOperationsFeatureApi::class
|
||||
GiftFeatureApi::class
|
||||
]
|
||||
)
|
||||
interface AssetsFeatureDependenciesComponent : AssetsFeatureDependencies
|
||||
|
||||
+3
-3
@@ -5,6 +5,7 @@ import coil.ImageLoader
|
||||
import com.google.gson.Gson
|
||||
import io.novafoundation.nova.common.address.AddressIconGenerator
|
||||
import io.novafoundation.nova.common.address.format.EthereumAddressFormat
|
||||
import io.novafoundation.nova.common.data.config.GlobalConfigDataSource
|
||||
import io.novafoundation.nova.common.data.memory.ComputationalCache
|
||||
import io.novafoundation.nova.common.data.network.AppLinksProvider
|
||||
import io.novafoundation.nova.common.data.network.HttpExceptionHandler
|
||||
@@ -99,7 +100,6 @@ import io.novafoundation.nova.feature_account_api.data.fee.capability.CustomFeeC
|
||||
import io.novafoundation.nova.feature_ahm_api.domain.ChainMigrationInfoUseCase
|
||||
import io.novafoundation.nova.feature_gift_api.domain.GiftsAccountSupportedUseCase
|
||||
import io.novafoundation.nova.feature_gift_api.domain.AvailableGiftAssetsUseCase
|
||||
import io.novafoundation.nova.feature_multisig_operations.presentation.callFormatting.MultisigCallFormatter
|
||||
import io.novafoundation.nova.feature_wallet_api.domain.SendUseCase
|
||||
import io.novafoundation.nova.feature_wallet_api.presentation.mixin.amountChooser.AmountChooserMixin
|
||||
import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.FeeLoaderMixin
|
||||
@@ -241,8 +241,6 @@ interface AssetsFeatureDependencies {
|
||||
|
||||
val multisigPendingOperationsService: MultisigPendingOperationsService
|
||||
|
||||
val multisigCallFormatter: MultisigCallFormatter
|
||||
|
||||
val automaticInteractionGate: AutomaticInteractionGate
|
||||
|
||||
val linkBuilderFactory: LinkBuilderFactory
|
||||
@@ -319,6 +317,8 @@ interface AssetsFeatureDependencies {
|
||||
|
||||
fun chainRegistry(): ChainRegistry
|
||||
|
||||
fun globalConfigDataSource(): GlobalConfigDataSource
|
||||
|
||||
@Named(REMOTE_STORAGE_SOURCE)
|
||||
fun remoteStorageSource(): StorageDataSource
|
||||
|
||||
|
||||
-2
@@ -15,7 +15,6 @@ import io.novafoundation.nova.feature_crowdloan_api.di.CrowdloanFeatureApi
|
||||
import io.novafoundation.nova.feature_currency_api.di.CurrencyFeatureApi
|
||||
import io.novafoundation.nova.feature_deep_linking.di.DeepLinkingFeatureApi
|
||||
import io.novafoundation.nova.feature_gift_api.di.GiftFeatureApi
|
||||
import io.novafoundation.nova.feature_multisig_operations.di.MultisigOperationsFeatureApi
|
||||
import io.novafoundation.nova.feature_nft_api.NftFeatureApi
|
||||
import io.novafoundation.nova.feature_staking_api.di.StakingFeatureApi
|
||||
import io.novafoundation.nova.feature_swap_api.di.SwapFeatureApi
|
||||
@@ -52,7 +51,6 @@ class AssetsFeatureHolder @Inject constructor(
|
||||
.deepLinkingFeatureApi(getFeature(DeepLinkingFeatureApi::class.java))
|
||||
.chainMigrationFeatureApi(getFeature(ChainMigrationFeatureApi::class.java))
|
||||
.giftFeatureApi(getFeature(GiftFeatureApi::class.java))
|
||||
.multisigOperationsFeatureApi(getFeature(MultisigOperationsFeatureApi::class.java))
|
||||
.build()
|
||||
|
||||
return DaggerAssetsFeatureComponent.factory()
|
||||
|
||||
+138
@@ -1,8 +1,10 @@
|
||||
package io.novafoundation.nova.feature_assets.domain.bridge.multisig
|
||||
|
||||
import io.novafoundation.nova.common.address.AccountIdKey
|
||||
import io.novafoundation.nova.common.address.fromHexOrNull
|
||||
import io.novafoundation.nova.common.address.intoKey
|
||||
import io.novafoundation.nova.common.address.toHexWithPrefix
|
||||
import io.novafoundation.nova.common.data.config.GlobalConfigDataSource
|
||||
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
|
||||
@@ -10,7 +12,10 @@ import io.novafoundation.nova.feature_account_api.data.ethereum.transaction.Tran
|
||||
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.data.multisig.model.MultisigTimePoint
|
||||
import io.novafoundation.nova.feature_account_api.domain.interfaces.SelectedAccountUseCase
|
||||
import io.novafoundation.nova.feature_account_api.domain.model.MetaAccount
|
||||
import io.novafoundation.nova.feature_account_api.domain.multisig.CallHash
|
||||
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
|
||||
@@ -18,6 +23,8 @@ 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.runtime.definitions.types.fromHexOrNull
|
||||
import io.novasama.substrate_sdk_android.runtime.definitions.types.generics.GenericCall
|
||||
import io.novasama.substrate_sdk_android.ss58.SS58Encoder.toAccountId
|
||||
import java.math.BigInteger
|
||||
import javax.inject.Inject
|
||||
@@ -31,6 +38,19 @@ data class BridgeSignerState(
|
||||
val approvalsSoFar: Int,
|
||||
)
|
||||
|
||||
/** A pending Multisig.as_multi call against one of the bridge's own multisig accounts (a real
|
||||
* user swap someone else submitted - NOT this wallet's own renewal signature, see
|
||||
* BridgeSignerState for that), that this signatory hasn't approved yet. [call] is null when the
|
||||
* off-chain indexer hasn't got the call content for this hash yet (or lookup failed) - callers
|
||||
* must treat that as "cannot show/sign this one", never fall back to hash-only approval. */
|
||||
data class PendingBridgeApproval(
|
||||
val chain: Chain,
|
||||
val callHash: CallHash,
|
||||
val timePoint: MultisigTimePoint,
|
||||
val approvalsCount: Int,
|
||||
val call: GenericCall.Instance?,
|
||||
)
|
||||
|
||||
interface BridgeMultisigInteractor {
|
||||
|
||||
/** Null if the currently selected wallet isn't one of the 5 known bridge signatories - the
|
||||
@@ -66,6 +86,19 @@ interface BridgeMultisigInteractor {
|
||||
/** Same as getWusdtRemainingAllowance but for the automation key's real USDT approval on
|
||||
* Polkadot Asset Hub - gates wUSDT->USDT withdrawal auto-pay. */
|
||||
suspend fun getPolkadotUsdtRemainingAllowance(): BigInteger
|
||||
|
||||
/** Every pending swap-approval call (on either chain) against the bridge's own multisig
|
||||
* accounts that this signatory hasn't approved yet - empty if the selected wallet isn't one
|
||||
* of the 5 known signatories. Unlike the app's generic multisig-operations feature (which
|
||||
* only tracks accounts formally added as a `MultisigMetaAccount`), this queries the bridge's
|
||||
* hardcoded multisig addresses directly, since individual signatories use their own regular
|
||||
* wallet to sign here - they never "are" the multisig account itself. */
|
||||
suspend fun getPendingApprovals(): List<PendingBridgeApproval>
|
||||
|
||||
/** Approves (contributes this wallet's signature to) an existing pending call. Refuses to
|
||||
* proceed if [PendingBridgeApproval.call] is null - approving a call whose content this
|
||||
* device can't verify would be blind-signing, never acceptable for a multisig approval. */
|
||||
suspend fun submitApproval(approval: PendingBridgeApproval): Result<ExtrinsicExecutionResult>
|
||||
}
|
||||
|
||||
@FeatureScope
|
||||
@@ -74,6 +107,8 @@ class RealBridgeMultisigInteractor @Inject constructor(
|
||||
private val selectedAccountUseCase: SelectedAccountUseCase,
|
||||
@Named(REMOTE_STORAGE_SOURCE) private val storageDataSource: StorageDataSource,
|
||||
private val extrinsicService: ExtrinsicService,
|
||||
private val bridgeMultisigOperationsApi: BridgeMultisigOperationsApi,
|
||||
private val globalConfigDataSource: GlobalConfigDataSource,
|
||||
) : BridgeMultisigInteractor {
|
||||
|
||||
override suspend fun getSignerState(): BridgeSignerState? {
|
||||
@@ -132,6 +167,109 @@ class RealBridgeMultisigInteractor @Inject constructor(
|
||||
return queryRemainingAllowance(chain, BridgeMultisigConstants.POLKADOT_USDT_ASSET_ID, BridgeMultisigConstants.AUTOMATION_KEY_ADDRESS_POLKADOT)
|
||||
}
|
||||
|
||||
override suspend fun getPendingApprovals(): List<PendingBridgeApproval> {
|
||||
val metaAccount = selectedAccountUseCase.getSelectedMetaAccount()
|
||||
|
||||
return listOf(
|
||||
ChainGeneses.PEZKUWI_ASSET_HUB to BridgeMultisigConstants.MULTISIG_ADDRESS,
|
||||
ChainGeneses.POLKADOT_ASSET_HUB to BridgeMultisigConstants.MULTISIG_ADDRESS_POLKADOT,
|
||||
).flatMap { (chainGenesis, multisigAddress) ->
|
||||
runCatching { getPendingApprovalsFor(chainGenesis, multisigAddress, metaAccount) }.getOrElse { emptyList() }
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun getPendingApprovalsFor(
|
||||
chainGenesis: String,
|
||||
multisigAddress: String,
|
||||
metaAccount: MetaAccount,
|
||||
): List<PendingBridgeApproval> {
|
||||
val chain = chainRegistry.getChain(chainGenesis)
|
||||
val myAccountId = metaAccount.accountIdIn(chain)?.intoKey() ?: return emptyList()
|
||||
|
||||
// Only the 5 known bridge signatories can ever have anything to approve here - same gate
|
||||
// as getSignerStateFor, matching how the rest of this screen already scopes itself.
|
||||
val isKnownSignatory = BridgeMultisigConstants.SIGNATORIES.any { it.address.toAccountId().intoKey() == myAccountId }
|
||||
if (!isKnownSignatory) return emptyList()
|
||||
|
||||
val multisigAccountId = multisigAddress.toAccountId().intoKey()
|
||||
|
||||
val keys = storageDataSource.query(chain.id) {
|
||||
runtime.metadata.bridgeMultisig().multisigs.keys(multisigAccountId)
|
||||
}
|
||||
if (keys.isEmpty()) return emptyList()
|
||||
|
||||
val entries = storageDataSource.query(chain.id) {
|
||||
runtime.metadata.bridgeMultisig().multisigs.entries(keys)
|
||||
}
|
||||
|
||||
val notYetApprovedByMe = entries.filterNot { (_, onChainMultisig) -> myAccountId in onChainMultisig.approvals }
|
||||
if (notYetApprovedByMe.isEmpty()) return emptyList()
|
||||
|
||||
val callHashes = notYetApprovedByMe.keys.map { it.second }
|
||||
val callDataByHash = fetchCallData(chain, multisigAccountId, callHashes)
|
||||
|
||||
return notYetApprovedByMe.map { (key, onChainMultisig) ->
|
||||
val callHash = key.second
|
||||
PendingBridgeApproval(
|
||||
chain = chain,
|
||||
callHash = callHash,
|
||||
timePoint = onChainMultisig.timePoint,
|
||||
approvalsCount = onChainMultisig.approvals.size,
|
||||
call = callDataByHash[callHash],
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun fetchCallData(
|
||||
chain: Chain,
|
||||
multisigAccountId: AccountIdKey,
|
||||
callHashes: List<CallHash>,
|
||||
): Map<CallHash, GenericCall.Instance?> {
|
||||
return runCatching {
|
||||
val globalConfig = globalConfigDataSource.getGlobalConfig()
|
||||
val request = BridgeOffChainCallDataRequest(multisigAccountId, callHashes, chain.id)
|
||||
val response = bridgeMultisigOperationsApi.getCallDatas(globalConfig.multisigsApiUrl, request)
|
||||
val runtime = chainRegistry.getRuntime(chain.id)
|
||||
|
||||
response.data.multisigOperations.nodes.mapNotNull { node ->
|
||||
val hash = CallHash.fromHexOrNull(node.callHash) ?: return@mapNotNull null
|
||||
val call = node.callData?.let { GenericCall.fromHexOrNull(runtime, it) }
|
||||
hash to call
|
||||
}.toMap()
|
||||
}.getOrElse { emptyMap() }
|
||||
}
|
||||
|
||||
override suspend fun submitApproval(approval: PendingBridgeApproval): Result<ExtrinsicExecutionResult> = runCatching {
|
||||
val call = requireNotNull(approval.call) {
|
||||
"Cannot approve a call whose content is unknown - refusing to blind-sign"
|
||||
}
|
||||
|
||||
val metaAccount = selectedAccountUseCase.getSelectedMetaAccount()
|
||||
val myAccountId = requireNotNull(metaAccount.accountIdIn(approval.chain)?.intoKey()) {
|
||||
"Selected account has no address on ${approval.chain.name}"
|
||||
}
|
||||
|
||||
val otherSignatories = BridgeMultisigConstants.SIGNATORIES
|
||||
.map { it.address.toAccountId().intoKey() }
|
||||
.filter { it != myAccountId }
|
||||
.sortedBy { it.toHexWithPrefix() }
|
||||
|
||||
extrinsicService.submitExtrinsicAndAwaitExecution(
|
||||
chain = approval.chain,
|
||||
origin = TransactionOrigin.WalletWithId(metaAccount.id)
|
||||
) {
|
||||
val multisigCall = runtime.composeBridgeMultisigAsMulti(
|
||||
threshold = BridgeMultisigConstants.THRESHOLD,
|
||||
otherSignatories = otherSignatories,
|
||||
maybeTimePoint = approval.timePoint,
|
||||
call = call,
|
||||
maxWeight = WeightV2(BigInteger.valueOf(1_000_000_000L), BigInteger.valueOf(200_000L)),
|
||||
)
|
||||
|
||||
call(multisigCall)
|
||||
}.getOrThrow().requireOk()
|
||||
}
|
||||
|
||||
/** Shared by both legs - the only differences between the wUSDT (Pezkuwi) and USDT (Polkadot)
|
||||
* renewal flows are which chain/asset/automation-key-address/threshold to use, the actual
|
||||
* on-chain call shape (Assets.approve_transfer wrapped in Multisig.as_multi) is identical. */
|
||||
|
||||
+61
@@ -0,0 +1,61 @@
|
||||
package io.novafoundation.nova.feature_assets.domain.bridge.multisig
|
||||
|
||||
import io.novafoundation.nova.common.address.AccountIdKey
|
||||
import io.novafoundation.nova.common.address.toHexWithPrefix
|
||||
import io.novafoundation.nova.common.data.network.subquery.SubQueryFilters
|
||||
import io.novafoundation.nova.common.data.network.subquery.SubQueryNodes
|
||||
import io.novafoundation.nova.common.data.network.subquery.SubQueryResponse
|
||||
import io.novafoundation.nova.common.utils.HexString
|
||||
import io.novafoundation.nova.feature_account_api.domain.multisig.CallHash
|
||||
import io.novafoundation.nova.runtime.multiNetwork.chain.model.ChainId
|
||||
import io.novasama.substrate_sdk_android.extensions.requireHexPrefix
|
||||
import retrofit2.http.Body
|
||||
import retrofit2.http.POST
|
||||
import retrofit2.http.Url
|
||||
|
||||
/**
|
||||
* Minimal, Bridge-screen-local mirror of feature-account-impl's `FindMultisigsApi.getCallDatas`/
|
||||
* `OffChainPendingMultisigInfoRequest`/`GetPedingMultisigOperationsResponse` - same module-
|
||||
* boundary reason as BridgeMultisigRuntimeApi.kt (feature-assets can't depend on
|
||||
* feature-account-impl). Only `getCallDatas` is mirrored - `findMultisigs` isn't needed here
|
||||
* since the bridge's multisig account is already known (BridgeMultisigConstants), not discovered.
|
||||
*/
|
||||
interface BridgeMultisigOperationsApi {
|
||||
|
||||
@POST
|
||||
suspend fun getCallDatas(
|
||||
@Url url: String,
|
||||
@Body body: BridgeOffChainCallDataRequest
|
||||
): SubQueryResponse<BridgeCallDataResponse>
|
||||
}
|
||||
|
||||
class BridgeOffChainCallDataRequest(
|
||||
accountIdKey: AccountIdKey,
|
||||
callHashes: Collection<CallHash>,
|
||||
chainId: ChainId
|
||||
) : SubQueryFilters {
|
||||
|
||||
@Transient
|
||||
private val callHashesHex = callHashes.map { it.toHexWithPrefix() }
|
||||
|
||||
val query = """
|
||||
query {
|
||||
multisigOperations(filter: {
|
||||
${"accountId" equalTo accountIdKey.toHexWithPrefix() }
|
||||
${"status" equalToEnum "pending"}
|
||||
${"callHash" presentIn callHashesHex}
|
||||
${"chainId" equalTo chainId.requireHexPrefix()}
|
||||
}) {
|
||||
nodes {
|
||||
callHash
|
||||
callData
|
||||
}
|
||||
}
|
||||
}
|
||||
""".trimIndent()
|
||||
}
|
||||
|
||||
class BridgeCallDataResponse(val multisigOperations: SubQueryNodes<BridgeOperationRemote>) {
|
||||
|
||||
class BridgeOperationRemote(val callHash: HexString, val callData: HexString?)
|
||||
}
|
||||
-3
@@ -11,7 +11,6 @@ import io.novafoundation.nova.feature_assets.presentation.tokens.add.enterInfo.A
|
||||
import io.novafoundation.nova.feature_assets.presentation.tokens.manage.chain.ManageChainTokensPayload
|
||||
import io.novafoundation.nova.feature_assets.presentation.trade.webInterface.TradeWebPayload
|
||||
import io.novafoundation.nova.feature_assets.presentation.transaction.filter.TransactionHistoryFilterPayload
|
||||
import io.novafoundation.nova.feature_multisig_operations.presentation.details.general.MultisigOperationDetailsPayload
|
||||
import io.novafoundation.nova.feature_swap_api.presentation.model.SwapSettingsPayload
|
||||
import io.novafoundation.nova.feature_wallet_api.presentation.model.AssetPayload
|
||||
|
||||
@@ -121,8 +120,6 @@ interface AssetsRouter {
|
||||
|
||||
fun openPendingMultisigOperations()
|
||||
|
||||
fun openMultisigOperationDetails(payload: MultisigOperationDetailsPayload)
|
||||
|
||||
fun openAssetDetailsFromDeepLink(payload: AssetPayload)
|
||||
|
||||
fun openGifts()
|
||||
|
||||
+2
-6
@@ -222,12 +222,8 @@ class BridgeFragment : BaseFragment<BridgeViewModel, FragmentBridgeBinding>() {
|
||||
val rowBinding = ItemPendingSignatureRowBinding.inflate(container.inflater(), container, false)
|
||||
|
||||
rowBinding.pendingSignatureChainIcon.loadChainIcon(model.chain.icon, imageLoader)
|
||||
// primaryValue (e.g. "100K USDT") is the amount - the whole point of this row - so it
|
||||
// takes the prominent title line; the call's own title/action name ("Transfer") isn't
|
||||
// shown at all here, only the destination (subtitle) is, to keep the row to two lines.
|
||||
rowBinding.pendingSignatureTitle.text = model.primaryValue ?: model.title
|
||||
rowBinding.pendingSignatureSubtitle.text = listOfNotNull(model.subtitle, model.chain.name, model.progress)
|
||||
.joinToString(separator = " • ")
|
||||
rowBinding.pendingSignatureTitle.text = model.amountText
|
||||
rowBinding.pendingSignatureSubtitle.text = "${model.destinationText} • ${model.chain.name} • ${model.progress}"
|
||||
rowBinding.pendingSignatureSignButton.setOnClickListener { viewModel.pendingSignatureSignClicked(model) }
|
||||
|
||||
container.addView(rowBinding.root)
|
||||
|
||||
+39
-30
@@ -10,35 +10,28 @@ import io.novafoundation.nova.common.utils.formatting.format
|
||||
import io.novafoundation.nova.common.utils.images.Icon
|
||||
import io.novafoundation.nova.common.view.ButtonState
|
||||
import io.novafoundation.nova.feature_account_api.data.mappers.mapChainToUi
|
||||
import io.novafoundation.nova.feature_account_api.data.multisig.MultisigPendingOperationsService
|
||||
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.userAction
|
||||
import io.novafoundation.nova.feature_account_api.domain.interfaces.SelectedAccountUseCase
|
||||
import io.novafoundation.nova.feature_account_api.domain.model.MetaAccount
|
||||
import io.novafoundation.nova.feature_account_api.domain.model.requireAccountIdKeyIn
|
||||
import io.novafoundation.nova.feature_account_api.presenatation.chain.getAssetIconOrFallback
|
||||
import io.novafoundation.nova.feature_assets.R
|
||||
import io.novafoundation.nova.feature_assets.domain.WalletInteractor
|
||||
import io.novafoundation.nova.feature_assets.domain.bridge.multisig.BridgeMultisigConstants
|
||||
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.domain.bridge.multisig.PendingBridgeApproval
|
||||
import io.novafoundation.nova.feature_assets.presentation.AssetsRouter
|
||||
import io.novafoundation.nova.feature_assets.presentation.bridge.execution.BridgeExecutionPayload
|
||||
import io.novafoundation.nova.feature_multisig_operations.presentation.callFormatting.MultisigCallFormatter
|
||||
import io.novafoundation.nova.feature_multisig_operations.presentation.common.MultisigOperationPayload
|
||||
import io.novafoundation.nova.feature_multisig_operations.presentation.common.fromOperationId
|
||||
import io.novafoundation.nova.feature_multisig_operations.presentation.details.general.MultisigOperationDetailsPayload
|
||||
import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.AssetSourceRegistry
|
||||
import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.tranfers.tryParseTransfer
|
||||
import io.novafoundation.nova.feature_wallet_api.domain.model.amountFromPlanks
|
||||
import io.novafoundation.nova.runtime.ext.ChainGeneses
|
||||
import io.novafoundation.nova.runtime.ext.addressOf
|
||||
import io.novafoundation.nova.runtime.ext.displayNameWithAssetStandard
|
||||
import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry
|
||||
import io.novasama.substrate_sdk_android.ss58.SS58Encoder.toAccountId
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.launch
|
||||
import java.math.BigDecimal
|
||||
import java.math.RoundingMode
|
||||
import java.text.NumberFormat
|
||||
|
||||
/**
|
||||
* DOT<->HEZ used to be a second pair here, retired 2026-07 in favor of the multisig-custodied
|
||||
@@ -62,9 +55,7 @@ class BridgeViewModel(
|
||||
private val assetIconProvider: AssetIconProvider,
|
||||
private val walletInteractor: WalletInteractor,
|
||||
private val bridgeMultisigInteractor: BridgeMultisigInteractor,
|
||||
private val multisigPendingOperationsService: MultisigPendingOperationsService,
|
||||
private val multisigCallFormatter: MultisigCallFormatter,
|
||||
private val selectedAccountUseCase: SelectedAccountUseCase,
|
||||
private val assetSourceRegistry: AssetSourceRegistry,
|
||||
) : BaseViewModel() {
|
||||
|
||||
companion object {
|
||||
@@ -483,32 +474,50 @@ class BridgeViewModel(
|
||||
|
||||
fun refreshPendingSignatures() {
|
||||
launch {
|
||||
val account = selectedAccountUseCase.getSelectedMetaAccount()
|
||||
|
||||
val models = multisigPendingOperationsService.pendingOperations().first()
|
||||
.filter { it.userAction() is MultisigAction.CanApprove }
|
||||
.map { it.toPendingSignatureUi(account) }
|
||||
val models = bridgeMultisigInteractor.getPendingApprovals()
|
||||
.mapNotNull { it.toPendingSignatureUiOrNull() }
|
||||
|
||||
_pendingSignatures.postValue(models)
|
||||
}
|
||||
}
|
||||
|
||||
private var pendingApprovalSubmission: Job? = null
|
||||
|
||||
fun pendingSignatureSignClicked(model: PendingSignatureModel) {
|
||||
val operationPayload = MultisigOperationPayload.fromOperationId(model.id)
|
||||
router.openMultisigOperationDetails(MultisigOperationDetailsPayload(operationPayload))
|
||||
if (pendingApprovalSubmission?.isActive == true) return
|
||||
|
||||
pendingApprovalSubmission = launch {
|
||||
bridgeMultisigInteractor.submitApproval(model.approval)
|
||||
.onFailure { showError(it.message ?: resourceManager.getString(R.string.bridge_sign_error)) }
|
||||
|
||||
refreshPendingSignatures()
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun PendingMultisigOperation.toPendingSignatureUi(selectedAccount: MetaAccount): PendingSignatureModel {
|
||||
val initialOrigin = selectedAccount.requireAccountIdKeyIn(chain)
|
||||
val formattedCall = multisigCallFormatter.formatPreview(call, initialOrigin, chain)
|
||||
/** Null whenever the call's actual content couldn't be recovered or isn't a plain transfer
|
||||
* this screen knows how to preview - never shown/approvable in that case (see
|
||||
* BridgeMultisigInteractor.submitApproval's own refusal to blind-sign for why this isn't
|
||||
* just a display-only gap: an unparseable row would have no safe "Sign" action anyway). */
|
||||
private suspend fun PendingBridgeApproval.toPendingSignatureUiOrNull(): PendingSignatureModel? {
|
||||
val call = call ?: return null
|
||||
|
||||
val assetId = if (chain.id == ChainGeneses.POLKADOT_ASSET_HUB) {
|
||||
BridgeMultisigConstants.POLKADOT_USDT_ASSET_ID
|
||||
} else {
|
||||
BridgeMultisigConstants.WUSDT_ASSET_ID
|
||||
}
|
||||
val asset = chain.assetsById[assetId] ?: return null
|
||||
val parsed = assetSourceRegistry.sourceFor(asset).transfers.tryParseTransfer(call, chain) ?: return null
|
||||
|
||||
val decimalAmount = asset.amountFromPlanks(parsed.amount.amount)
|
||||
val amountText = "${NumberFormat.getNumberInstance().format(decimalAmount)} ${asset.symbol.value}"
|
||||
|
||||
return PendingSignatureModel(
|
||||
id = operationId,
|
||||
approval = this,
|
||||
chain = mapChainToUi(chain),
|
||||
title = formattedCall.title,
|
||||
subtitle = formattedCall.subtitle,
|
||||
primaryValue = formattedCall.primaryValue,
|
||||
progress = resourceManager.getString(R.string.multisig_operations_progress, approvals.size.format(), threshold.format())
|
||||
amountText = amountText,
|
||||
destinationText = chain.addressOf(parsed.destination),
|
||||
progress = resourceManager.getString(R.string.multisig_operations_progress, approvalsCount.format(), BridgeMultisigConstants.THRESHOLD.format())
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
+4
-5
@@ -1,13 +1,12 @@
|
||||
package io.novafoundation.nova.feature_assets.presentation.bridge
|
||||
|
||||
import io.novafoundation.nova.feature_account_api.data.multisig.model.PendingMultisigOperationId
|
||||
import io.novafoundation.nova.feature_account_api.presenatation.chain.ChainUi
|
||||
import io.novafoundation.nova.feature_assets.domain.bridge.multisig.PendingBridgeApproval
|
||||
|
||||
data class PendingSignatureModel(
|
||||
val id: PendingMultisigOperationId,
|
||||
val approval: PendingBridgeApproval,
|
||||
val chain: ChainUi,
|
||||
val title: String,
|
||||
val subtitle: String?,
|
||||
val primaryValue: CharSequence?,
|
||||
val amountText: String,
|
||||
val destinationText: String,
|
||||
val progress: String,
|
||||
)
|
||||
|
||||
+22
-10
@@ -6,19 +6,21 @@ import androidx.lifecycle.ViewModelProvider
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.multibindings.IntoMap
|
||||
import io.novafoundation.nova.common.data.config.GlobalConfigDataSource
|
||||
import io.novafoundation.nova.common.data.network.NetworkApiCreator
|
||||
import io.novafoundation.nova.common.di.viewmodel.ViewModelKey
|
||||
import io.novafoundation.nova.common.di.viewmodel.ViewModelModule
|
||||
import io.novafoundation.nova.common.presentation.AssetIconProvider
|
||||
import io.novafoundation.nova.common.resources.ResourceManager
|
||||
import io.novafoundation.nova.feature_account_api.data.extrinsic.ExtrinsicService
|
||||
import io.novafoundation.nova.feature_account_api.data.multisig.MultisigPendingOperationsService
|
||||
import io.novafoundation.nova.feature_account_api.domain.interfaces.SelectedAccountUseCase
|
||||
import io.novafoundation.nova.feature_assets.domain.WalletInteractor
|
||||
import io.novafoundation.nova.feature_assets.domain.bridge.multisig.BridgeMultisigInteractor
|
||||
import io.novafoundation.nova.feature_assets.domain.bridge.multisig.BridgeMultisigOperationsApi
|
||||
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.feature_multisig_operations.presentation.callFormatting.MultisigCallFormatter
|
||||
import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.AssetSourceRegistry
|
||||
import io.novafoundation.nova.runtime.di.REMOTE_STORAGE_SOURCE
|
||||
import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry
|
||||
import io.novafoundation.nova.runtime.storage.source.StorageDataSource
|
||||
@@ -27,14 +29,28 @@ import javax.inject.Named
|
||||
@Module(includes = [ViewModelModule::class])
|
||||
class BridgeModule {
|
||||
|
||||
@Provides
|
||||
fun provideBridgeMultisigOperationsApi(apiCreator: NetworkApiCreator): BridgeMultisigOperationsApi {
|
||||
return apiCreator.create(BridgeMultisigOperationsApi::class.java)
|
||||
}
|
||||
|
||||
@Provides
|
||||
fun provideBridgeMultisigInteractor(
|
||||
chainRegistry: ChainRegistry,
|
||||
selectedAccountUseCase: SelectedAccountUseCase,
|
||||
@Named(REMOTE_STORAGE_SOURCE) storageDataSource: StorageDataSource,
|
||||
extrinsicService: ExtrinsicService
|
||||
extrinsicService: ExtrinsicService,
|
||||
bridgeMultisigOperationsApi: BridgeMultisigOperationsApi,
|
||||
globalConfigDataSource: GlobalConfigDataSource,
|
||||
): BridgeMultisigInteractor {
|
||||
return RealBridgeMultisigInteractor(chainRegistry, selectedAccountUseCase, storageDataSource, extrinsicService)
|
||||
return RealBridgeMultisigInteractor(
|
||||
chainRegistry,
|
||||
selectedAccountUseCase,
|
||||
storageDataSource,
|
||||
extrinsicService,
|
||||
bridgeMultisigOperationsApi,
|
||||
globalConfigDataSource,
|
||||
)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@@ -47,9 +63,7 @@ class BridgeModule {
|
||||
assetIconProvider: AssetIconProvider,
|
||||
walletInteractor: WalletInteractor,
|
||||
bridgeMultisigInteractor: BridgeMultisigInteractor,
|
||||
multisigPendingOperationsService: MultisigPendingOperationsService,
|
||||
multisigCallFormatter: MultisigCallFormatter,
|
||||
selectedAccountUseCase: SelectedAccountUseCase,
|
||||
assetSourceRegistry: AssetSourceRegistry,
|
||||
): ViewModel {
|
||||
return BridgeViewModel(
|
||||
router,
|
||||
@@ -58,9 +72,7 @@ class BridgeModule {
|
||||
assetIconProvider,
|
||||
walletInteractor,
|
||||
bridgeMultisigInteractor,
|
||||
multisigPendingOperationsService,
|
||||
multisigCallFormatter,
|
||||
selectedAccountUseCase,
|
||||
assetSourceRegistry,
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user