diff --git a/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/data/extrinsic/ExtrinsicSplitter.kt b/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/data/extrinsic/ExtrinsicSplitter.kt index a1e06566..b3c93aec 100644 --- a/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/data/extrinsic/ExtrinsicSplitter.kt +++ b/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/data/extrinsic/ExtrinsicSplitter.kt @@ -8,10 +8,8 @@ import io.novafoundation.nova.feature_account_api.data.signer.SigningContext import io.novafoundation.nova.common.utils.min import io.novafoundation.nova.feature_account_api.data.extrinsic.ExtrinsicSplitter import io.novafoundation.nova.feature_account_api.data.extrinsic.SplitCalls -import io.novafoundation.nova.runtime.ext.isPezkuwiChain import io.novafoundation.nova.runtime.ext.requireGenesisHash import io.novafoundation.nova.runtime.extrinsic.CustomTransactionExtensions -import io.novafoundation.nova.runtime.extrinsic.extensions.PezkuwiCheckImmortal import io.novafoundation.nova.runtime.extrinsic.multi.CallBuilder import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain @@ -47,9 +45,6 @@ private typealias CallWeightsByType = Map> private const val LEAVE_SOME_SPACE_MULTIPLIER = 0.8 -// DIAGNOSTIC BUILD — remove with the probe in wrapInFakeExtrinsic. -private const val DIAG = "PezMsigDiag" - @FeatureScope internal class RealExtrinsicSplitter @Inject constructor( private val rpcCalls: RpcCalls, @@ -139,17 +134,10 @@ internal class RealExtrinsicSplitter @Inject constructor( } /** - * DIAGNOSTIC BUILD — not for release. + * A throwaway signed extrinsic, built only to measure the weight of `call`. * - * "Failed to encode extension CheckMortality" reproduces here, and only here, when - * approving a multisig operation on Pezkuwi Asset Hub. This path is multisig-only: - * asMulti needs a max_weight, so a throwaway signed extrinsic is built to measure - * the inner call. Plain transfers never reach it, which is why they succeed. - * - * Static reading could not tell which side of the isPezkuwiChain gate fails, so this - * build tries BOTH era encodings and reports the outcome of each. Read the PezMsigDiag - * lines to see which one the chain accepts, then wire that choice in permanently and - * delete this. + * Immortal era: this extrinsic is never submitted, so there is nothing for a mortal + * era to protect, and an immortal one needs no block hash lookup. */ private suspend fun wrapInFakeExtrinsic( signer: NovaSigner, @@ -158,68 +146,27 @@ internal class RealExtrinsicSplitter @Inject constructor( chain: Chain ): SendableExtrinsic { val genesisHash = chain.requireGenesisHash().fromHex() - val isPezkuwi = chain.isPezkuwiChain - android.util.Log.e( - DIAG, - "chain='${chain.name}' id=${chain.id} isPezkuwiChain=$isPezkuwi " + - "genesis=${chain.requireGenesisHash()}" - ) - android.util.Log.e( - DIAG, - "signedExtensions=${runtime.metadata.extrinsic.signedExtensions.map { it.id }}" - ) + val builder = ExtrinsicBuilder( + runtime = runtime, + extrinsicVersion = ExtrinsicVersion.V4, + batchMode = BatchMode.BATCH, + ).apply { + setTransactionExtension(CheckMortality(Era.Immortal, genesisHash)) + setTransactionExtension(CheckGenesis(chain.requireGenesisHash().fromHex())) + setTransactionExtension(ChargeTransactionPayment(BigInteger.ZERO)) + setTransactionExtension(CheckMetadataHash(CheckMetadataHashMode.Disabled)) + setTransactionExtension(CheckSpecVersion(0)) + setTransactionExtension(CheckTxVersion(0)) - // Builds the fake extrinsic with one specific era encoding. Kept as a local so the - // two attempts differ in exactly one thing and nothing else. - suspend fun attempt(usePezkuwiEra: Boolean): Result = runCatching { - ExtrinsicBuilder( - runtime = runtime, - extrinsicVersion = ExtrinsicVersion.V4, - batchMode = BatchMode.BATCH, - ).apply { - if (usePezkuwiEra) { - setTransactionExtension(PezkuwiCheckImmortal(genesisHash)) - } else { - setTransactionExtension(CheckMortality(Era.Immortal, genesisHash)) - } - setTransactionExtension(CheckGenesis(chain.requireGenesisHash().fromHex())) - setTransactionExtension(ChargeTransactionPayment(BigInteger.ZERO)) - setTransactionExtension(CheckMetadataHash(CheckMetadataHashMode.Disabled)) - setTransactionExtension(CheckSpecVersion(0)) - setTransactionExtension(CheckTxVersion(0)) + CustomTransactionExtensions.defaultValues(runtime).forEach(::setTransactionExtension) - CustomTransactionExtensions.defaultValues(runtime).forEach(::setTransactionExtension) + call(call) - call(call) - - val signingContext = signingContextFactory.default(chain) - signer.setSignerDataForFee(signingContext) - }.buildExtrinsic() + val signingContext = signingContextFactory.default(chain) + signer.setSignerDataForFee(signingContext) } - fun report(label: String, result: Result) { - result.fold( - onSuccess = { android.util.Log.e(DIAG, "$label -> OK") }, - onFailure = { e -> - // The message alone has been the whole diagnosis so far; the cause chain - // is what actually names the failing type. - val causes = generateSequence(e) { it.cause }.joinToString(" <- ") { - "${it::class.java.simpleName}: ${it.message}" - } - android.util.Log.e(DIAG, "$label -> FAIL $causes", e) - } - ) - } - - // Preferred first: whatever the current gate would have chosen on its own. - val preferred = attempt(usePezkuwiEra = isPezkuwi) - report(if (isPezkuwi) "PezkuwiCheckImmortal(gate choice)" else "CheckMortality(gate choice)", preferred) - preferred.getOrNull()?.let { return it } - - val alternative = attempt(usePezkuwiEra = !isPezkuwi) - report(if (isPezkuwi) "CheckMortality(alternative)" else "PezkuwiCheckImmortal(alternative)", alternative) - - return alternative.getOrElse { throw preferred.exceptionOrNull()!! } + return builder.buildExtrinsic() } } diff --git a/runtime/src/main/java/io/novafoundation/nova/runtime/extrinsic/extensions/PezkuwiCheckImmortal.kt b/runtime/src/main/java/io/novafoundation/nova/runtime/extrinsic/extensions/PezkuwiCheckImmortal.kt deleted file mode 100644 index 1cb3872d..00000000 --- a/runtime/src/main/java/io/novafoundation/nova/runtime/extrinsic/extensions/PezkuwiCheckImmortal.kt +++ /dev/null @@ -1,23 +0,0 @@ -package io.novafoundation.nova.runtime.extrinsic.extensions - -import io.novasama.substrate_sdk_android.runtime.definitions.types.composite.DictEnum -import io.novasama.substrate_sdk_android.runtime.extrinsic.v5.transactionExtension.extensions.FixedValueTransactionExtension - -/** - * Custom CheckMortality extension for Pezkuwi chains using IMMORTAL era. - * - * Pezkuwi uses pezsp_runtime.generic.era.Era which is a DictEnum with variants: - * - Immortal (encoded as 0x00) - * - Mortal1(u8), Mortal2(u8), ..., Mortal255(u8) - * - * This extension uses Immortal era with genesis hash, which matches how @pezkuwi/api signs. - * - * @param genesisHash The chain's genesis hash (32 bytes) for the signer payload - */ -class PezkuwiCheckImmortal( - genesisHash: ByteArray -) : FixedValueTransactionExtension( - name = "CheckMortality", - implicit = genesisHash, // Genesis hash goes into signer payload for immortal transactions - explicit = DictEnum.Entry("Immortal", null) // Immortal variant - unit type with no value -) diff --git a/runtime/src/main/java/io/novafoundation/nova/runtime/extrinsic/extensions/PezkuwiCheckMortality.kt b/runtime/src/main/java/io/novafoundation/nova/runtime/extrinsic/extensions/PezkuwiCheckMortality.kt deleted file mode 100644 index 520fbe78..00000000 --- a/runtime/src/main/java/io/novafoundation/nova/runtime/extrinsic/extensions/PezkuwiCheckMortality.kt +++ /dev/null @@ -1,68 +0,0 @@ -package io.novafoundation.nova.runtime.extrinsic.extensions - -import io.novasama.substrate_sdk_android.runtime.definitions.types.composite.DictEnum -import io.novasama.substrate_sdk_android.runtime.definitions.types.generics.Era -import io.novasama.substrate_sdk_android.runtime.extrinsic.v5.transactionExtension.extensions.FixedValueTransactionExtension -import java.math.BigInteger - -/** - * Custom CheckMortality extension for Pezkuwi chains. - * - * Pezkuwi uses pezsp_runtime.generic.era.Era which is a DictEnum with variants: - * - Immortal - * - Mortal1(u8), Mortal2(u8), ..., Mortal255(u8) - * - * The variant name is "MortalX" where X is the first byte of the encoded era, - * and the variant's value is the second byte (u8). - * - * @param era The mortal era from MortalityConstructor - * @param blockHash The block hash (32 bytes) for the signer payload - */ -class PezkuwiCheckMortality( - era: Era.Mortal, - blockHash: ByteArray -) : FixedValueTransactionExtension( - name = "CheckMortality", - implicit = blockHash, // blockHash goes into signer payload - explicit = createEraEntry(era) // Era as DictEnum.Entry -) { - companion object { - /** - * Creates a DictEnum.Entry for the Era. - * - * Standard Era encoding produces 2 bytes: - * - First byte determines the variant name (Mortal1, Mortal2, ..., Mortal255) - * - Second byte is the variant's value (u8) - */ - private fun createEraEntry(era: Era.Mortal): DictEnum.Entry { - val period = era.period.toLong() - val phase = era.phase.toLong() - val quantizeFactor = maxOf(period shr 12, 1) - - // Calculate the two-byte encoding - val encoded = ((countTrailingZeroBits(period) - 1).coerceIn(1, 15)) or - ((phase / quantizeFactor).toInt() shl 4) - - val firstByte = encoded and 0xFF - val secondByte = (encoded shr 8) and 0xFF - - // DictEnum variant: "MortalX" where X is the first byte - // Variant value: second byte as u8 (BigInteger) - return DictEnum.Entry( - name = "Mortal$firstByte", - value = BigInteger.valueOf(secondByte.toLong()) - ) - } - - private fun countTrailingZeroBits(value: Long): Int { - if (value == 0L) return 64 - var n = 0 - var x = value - while ((x and 1L) == 0L) { - n++ - x = x shr 1 - } - return n - } - } -}