fix: resolve asset by on-chain Statemine id, not local Chain.Asset.id

Root cause found via device-side Log.e tracing (now removed): the on-chain
query, off-chain call-data fetch, and call decode all worked correctly - a
real pending approval was found and its call decoded - but chain.assetsById
is keyed by this app's own local Chain.Asset.id, which is NOT the same as
the raw pallet_assets id these constants hold (e.g. USDT is local id 1 but
on-chain id 1984 on Polkadot Asset Hub - they only happened to match by
coincidence on Pezkuwi Asset Hub, masking the bug there). Every result was
silently dropped at this single lookup. Fixed by reverse-resolving through
findAssetByStatemineAssetId instead of indexing assetsById directly.
This commit is contained in:
Satoshi Qazi Muhammed committed 2026-07-17 20:30:53 -07:00
1 parent 63dd3e2d03
commit 40f8db48bc
2 files changed
+16 -39

No files matched your search

@@ -174,9 +174,7 @@ class RealBridgeMultisigInteractor @Inject constructor(
ChainGeneses.PEZKUWI_ASSET_HUB to BridgeMultisigConstants.MULTISIG_ADDRESS, ChainGeneses.PEZKUWI_ASSET_HUB to BridgeMultisigConstants.MULTISIG_ADDRESS,
ChainGeneses.POLKADOT_ASSET_HUB to BridgeMultisigConstants.MULTISIG_ADDRESS_POLKADOT, ChainGeneses.POLKADOT_ASSET_HUB to BridgeMultisigConstants.MULTISIG_ADDRESS_POLKADOT,
).flatMap { (chainGenesis, multisigAddress) -> ).flatMap { (chainGenesis, multisigAddress) ->
runCatching { getPendingApprovalsFor(chainGenesis, multisigAddress, metaAccount) } runCatching { getPendingApprovalsFor(chainGenesis, multisigAddress, metaAccount) }.getOrElse { emptyList() }
.onFailure { android.util.Log.e("BridgeDebug", "getPendingApprovalsFor($chainGenesis) threw", it) }
.getOrElse { emptyList() }
} }
} }
@@ -186,14 +184,11 @@ class RealBridgeMultisigInteractor @Inject constructor(
metaAccount: MetaAccount, metaAccount: MetaAccount,
): List<PendingBridgeApproval> { ): List<PendingBridgeApproval> {
val chain = chainRegistry.getChain(chainGenesis) val chain = chainRegistry.getChain(chainGenesis)
val myAccountId = metaAccount.accountIdIn(chain)?.intoKey() val myAccountId = metaAccount.accountIdIn(chain)?.intoKey() ?: return emptyList()
android.util.Log.e("BridgeDebug", "chain=$chainGenesis myAccountId=${myAccountId?.toHexWithPrefix()}")
if (myAccountId == null) return emptyList()
// Only the 5 known bridge signatories can ever have anything to approve here - same gate // 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. // as getSignerStateFor, matching how the rest of this screen already scopes itself.
val isKnownSignatory = BridgeMultisigConstants.SIGNATORIES.any { it.address.toAccountId().intoKey() == myAccountId } val isKnownSignatory = BridgeMultisigConstants.SIGNATORIES.any { it.address.toAccountId().intoKey() == myAccountId }
android.util.Log.e("BridgeDebug", "isKnownSignatory=$isKnownSignatory")
if (!isKnownSignatory) return emptyList() if (!isKnownSignatory) return emptyList()
val multisigAccountId = multisigAddress.toAccountId().intoKey() val multisigAccountId = multisigAddress.toAccountId().intoKey()
@@ -201,21 +196,17 @@ class RealBridgeMultisigInteractor @Inject constructor(
val keys = storageDataSource.query(chain.id) { val keys = storageDataSource.query(chain.id) {
runtime.metadata.bridgeMultisig().multisigs.keys(multisigAccountId) runtime.metadata.bridgeMultisig().multisigs.keys(multisigAccountId)
} }
android.util.Log.e("BridgeDebug", "keys.size=${keys.size}")
if (keys.isEmpty()) return emptyList() if (keys.isEmpty()) return emptyList()
val entries = storageDataSource.query(chain.id) { val entries = storageDataSource.query(chain.id) {
runtime.metadata.bridgeMultisig().multisigs.entries(keys) runtime.metadata.bridgeMultisig().multisigs.entries(keys)
} }
android.util.Log.e("BridgeDebug", "entries.size=${entries.size}")
val notYetApprovedByMe = entries.filterNot { (_, onChainMultisig) -> myAccountId in onChainMultisig.approvals } val notYetApprovedByMe = entries.filterNot { (_, onChainMultisig) -> myAccountId in onChainMultisig.approvals }
android.util.Log.e("BridgeDebug", "notYetApprovedByMe.size=${notYetApprovedByMe.size}")
if (notYetApprovedByMe.isEmpty()) return emptyList() if (notYetApprovedByMe.isEmpty()) return emptyList()
val callHashes = notYetApprovedByMe.keys.map { it.second } val callHashes = notYetApprovedByMe.keys.map { it.second }
val callDataByHash = fetchCallData(chain, multisigAccountId, callHashes) val callDataByHash = fetchCallData(chain, multisigAccountId, callHashes)
android.util.Log.e("BridgeDebug", "callDataByHash=${callDataByHash.mapValues { it.value != null }}")
return notYetApprovedByMe.map { (key, onChainMultisig) -> return notYetApprovedByMe.map { (key, onChainMultisig) ->
val callHash = key.second val callHash = key.second
@@ -236,25 +227,16 @@ class RealBridgeMultisigInteractor @Inject constructor(
): Map<CallHash, GenericCall.Instance?> { ): Map<CallHash, GenericCall.Instance?> {
return runCatching { return runCatching {
val globalConfig = globalConfigDataSource.getGlobalConfig() val globalConfig = globalConfigDataSource.getGlobalConfig()
android.util.Log.e("BridgeDebug", "multisigsApiUrl=${globalConfig.multisigsApiUrl}")
val request = BridgeOffChainCallDataRequest(multisigAccountId, callHashes, chain.id) val request = BridgeOffChainCallDataRequest(multisigAccountId, callHashes, chain.id)
val response = bridgeMultisigOperationsApi.getCallDatas(globalConfig.multisigsApiUrl, request) val response = bridgeMultisigOperationsApi.getCallDatas(globalConfig.multisigsApiUrl, request)
android.util.Log.e("BridgeDebug", "response nodes=${response.data.multisigOperations.nodes.size}")
val runtime = chainRegistry.getRuntime(chain.id) val runtime = chainRegistry.getRuntime(chain.id)
response.data.multisigOperations.nodes.mapNotNull { node -> response.data.multisigOperations.nodes.mapNotNull { node ->
val hash = CallHash.fromHexOrNull(node.callHash) ?: return@mapNotNull null val hash = CallHash.fromHexOrNull(node.callHash) ?: return@mapNotNull null
val call = node.callData?.let { val call = node.callData?.let { GenericCall.fromHexOrNull(runtime, it) }
runCatching { GenericCall.fromHexOrNull(runtime, it) }
.onFailure { e -> android.util.Log.e("BridgeDebug", "GenericCall decode failed for $it", e) }
.getOrNull()
}
android.util.Log.e("BridgeDebug", "hash=$hash callData=${node.callData} decodedCall=${call != null}")
hash to call hash to call
}.toMap() }.toMap()
} }.getOrElse { emptyMap() }
.onFailure { android.util.Log.e("BridgeDebug", "fetchCallData threw", it) }
.getOrElse { emptyMap() }
} }
override suspend fun submitApproval(approval: PendingBridgeApproval): Result<ExtrinsicExecutionResult> = runCatching { override suspend fun submitApproval(approval: PendingBridgeApproval): Result<ExtrinsicExecutionResult> = runCatching {
@@ -25,7 +25,9 @@ import io.novafoundation.nova.feature_wallet_api.domain.model.amountFromPlanks
import io.novafoundation.nova.runtime.ext.ChainGeneses import io.novafoundation.nova.runtime.ext.ChainGeneses
import io.novafoundation.nova.runtime.ext.addressOf import io.novafoundation.nova.runtime.ext.addressOf
import io.novafoundation.nova.runtime.ext.displayNameWithAssetStandard import io.novafoundation.nova.runtime.ext.displayNameWithAssetStandard
import io.novafoundation.nova.runtime.ext.findAssetByStatemineAssetId
import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry
import io.novafoundation.nova.runtime.multiNetwork.getRuntime
import io.novasama.substrate_sdk_android.ss58.SS58Encoder.toAccountId import io.novasama.substrate_sdk_android.ss58.SS58Encoder.toAccountId
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@@ -474,10 +476,8 @@ class BridgeViewModel(
fun refreshPendingSignatures() { fun refreshPendingSignatures() {
launch { launch {
val approvals = bridgeMultisigInteractor.getPendingApprovals() val models = bridgeMultisigInteractor.getPendingApprovals()
android.util.Log.e("BridgeDebug", "refreshPendingSignatures: approvals.size=${approvals.size}") .mapNotNull { it.toPendingSignatureUiOrNull() }
val models = approvals.mapNotNull { it.toPendingSignatureUiOrNull() }
android.util.Log.e("BridgeDebug", "refreshPendingSignatures: models.size=${models.size}")
_pendingSignatures.postValue(models) _pendingSignatures.postValue(models)
} }
@@ -501,24 +501,19 @@ class BridgeViewModel(
* BridgeMultisigInteractor.submitApproval's own refusal to blind-sign for why this isn't * 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). */ * just a display-only gap: an unparseable row would have no safe "Sign" action anyway). */
private suspend fun PendingBridgeApproval.toPendingSignatureUiOrNull(): PendingSignatureModel? { private suspend fun PendingBridgeApproval.toPendingSignatureUiOrNull(): PendingSignatureModel? {
val call = call ?: run { val call = call ?: return null
android.util.Log.e("BridgeDebug", "toPendingSignatureUiOrNull: call is null for hash=$callHash")
return null
}
val assetId = if (chain.id == ChainGeneses.POLKADOT_ASSET_HUB) { // These constants are the raw on-chain pallet_assets id (e.g. 1984 on Polkadot Asset Hub) -
// NOT this app's own local Chain.Asset.id (chain.assetsById is keyed by the latter, which
// can differ, so it must be resolved via the Statemine-id reverse lookup, not indexed directly).
val onChainAssetId = if (chain.id == ChainGeneses.POLKADOT_ASSET_HUB) {
BridgeMultisigConstants.POLKADOT_USDT_ASSET_ID BridgeMultisigConstants.POLKADOT_USDT_ASSET_ID
} else { } else {
BridgeMultisigConstants.WUSDT_ASSET_ID BridgeMultisigConstants.WUSDT_ASSET_ID
} }
val asset = chain.assetsById[assetId] ?: run { val runtime = chainRegistry.getRuntime(chain.id)
android.util.Log.e("BridgeDebug", "toPendingSignatureUiOrNull: asset $assetId not found on ${chain.name}") val asset = chain.findAssetByStatemineAssetId(runtime, onChainAssetId.toBigInteger()) ?: return null
return null val parsed = assetSourceRegistry.sourceFor(asset).transfers.tryParseTransfer(call, chain) ?: return null
}
val parsed = assetSourceRegistry.sourceFor(asset).transfers.tryParseTransfer(call, chain) ?: run {
android.util.Log.e("BridgeDebug", "toPendingSignatureUiOrNull: tryParseTransfer returned null, call=$call")
return null
}
val decimalAmount = asset.amountFromPlanks(parsed.amount.amount) val decimalAmount = asset.amountFromPlanks(parsed.amount.amount)
val amountText = "${NumberFormat.getNumberInstance().format(decimalAmount)} ${asset.symbol.value}" val amountText = "${NumberFormat.getNumberInstance().format(decimalAmount)} ${asset.symbol.value}"