mirror of
https://github.com/pezkuwichain/pezkuwi-wallet-android.git
synced 2026-08-06 04:25:40 +00:00
fix(multisig): approve operations on Pezkuwi chains again by dropping the custom era extension
Approving a multisig operation on Pezkuwi Asset Hub failed with "Failed to encode
extension CheckMortality", so no signatory could approve from the wallet — the
bridge treasury included. Initiating an operation worked, which made the break look
narrower than it was.
Only multisig reaches the failing code. asMulti needs a max_weight, so
estimateCallWeight builds a throwaway signed extrinsic to measure the inner call.
Plain transfers never build one, which is why sending HEZ kept working.
PezkuwiCheckImmortal was the cause, not the cure. It passes a raw
DictEnum.Entry("Immortal", null) as the era, and the SDK rejects it:
EncodeDecodeException: Entry(name=Immortal, value=null) (Entry)
is not a valid instance of Era (EraType)
The premise it was written on — that Pezkuwi's pezsp_runtime Era breaks the standard
codec — does not hold. Measured on device against Pezkuwi Asset Hub, the standard
CheckMortality(Era.Immortal) encodes without complaint, and ExtrinsicBuilderFactory
has been signing every ordinary transfer through it with a mortal era all along. The
era type resolves from metadata by index, so the renamed module path never mattered.
So the chain gate goes and both custom extensions go with it: PezkuwiCheckImmortal
had one caller, PezkuwiCheckMortality had none. One path for every chain.
Verified on device (Pezkuwi Asset Hub, versionCode 340): a build that tried both
encodings logged the custom one failing and the standard one succeeding, then
carried a real 3-of-5 approval through to MultisigExecuted with the inner call Ok
and 10 HEZ leaving the multisig.
This commit is contained in:
-23
@@ -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<Any?>("Immortal", null) // Immortal variant - unit type with no value
|
||||
)
|
||||
-68
@@ -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<BigInteger> {
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user