mirror of
https://github.com/pezkuwichain/pezkuwi-wallet-android.git
synced 2026-04-24 15:57:55 +00:00
feat: Add Pezkuwi chain support with custom signed extensions
- Add PezkuwiAddressConstructor for custom address type handling - Add custom signed extensions (CheckNonZeroSender, CheckWeight, WeightReclaim, PezkuwiCheckMortality) - Add pezkuwi.json type definitions - Update RuntimeSnapshotExt for multiple address type lookups - Update CHAINS_URL to use GitHub-hosted chains.json with types config - Add feeViaRuntimeCall support for Pezkuwi chains - Add debug diagnostics for runtime type issues (to be cleaned before production) - Add CHANGELOG_PEZKUWI.md documenting all changes
This commit is contained in:
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"types": {
|
||||
"ExtrinsicSignature": "MultiSignature",
|
||||
"Address": "pezsp_runtime::multiaddress::MultiAddress",
|
||||
"LookupSource": "pezsp_runtime::multiaddress::MultiAddress"
|
||||
},
|
||||
"typesAlias": {
|
||||
"pezsp_runtime::multiaddress::MultiAddress": "MultiAddress",
|
||||
"pezsp_runtime::MultiSignature": "MultiSignature",
|
||||
"pezsp_runtime.generic.era.Era": "Era"
|
||||
}
|
||||
}
|
||||
+26
-8
@@ -1,12 +1,18 @@
|
||||
package io.novafoundation.nova.runtime.extrinsic
|
||||
|
||||
import android.util.Log
|
||||
import io.novafoundation.nova.runtime.extrinsic.extensions.AuthorizeCall
|
||||
import io.novafoundation.nova.runtime.extrinsic.extensions.ChargeAssetTxPayment
|
||||
import io.novafoundation.nova.runtime.extrinsic.extensions.CheckAppId
|
||||
import io.novafoundation.nova.runtime.extrinsic.extensions.CheckNonZeroSender
|
||||
import io.novafoundation.nova.runtime.extrinsic.extensions.CheckWeight
|
||||
import io.novafoundation.nova.runtime.extrinsic.extensions.WeightReclaim
|
||||
import io.novasama.substrate_sdk_android.runtime.RuntimeSnapshot
|
||||
import io.novasama.substrate_sdk_android.runtime.extrinsic.builder.ExtrinsicBuilder
|
||||
import io.novasama.substrate_sdk_android.runtime.extrinsic.v5.transactionExtension.TransactionExtension
|
||||
|
||||
private const val TAG = "CustomTxExtensions"
|
||||
|
||||
object CustomTransactionExtensions {
|
||||
|
||||
// PezkuwiChain genesis hashes (mainnet and teyrchains)
|
||||
@@ -34,21 +40,33 @@ object CustomTransactionExtensions {
|
||||
|
||||
fun defaultValues(runtime: RuntimeSnapshot): List<TransactionExtension> {
|
||||
val extensions = mutableListOf<TransactionExtension>()
|
||||
val isPezkuwi = isPezkuwiChain(runtime)
|
||||
|
||||
// Add AuthorizeCall only for PezkuwiChain networks
|
||||
if (isPezkuwiChain(runtime)) {
|
||||
Log.d(TAG, "isPezkuwiChain: $isPezkuwi")
|
||||
|
||||
if (isPezkuwi) {
|
||||
// Pezkuwi needs: AuthorizeCall, CheckNonZeroSender, CheckWeight, WeightReclaim
|
||||
// Other extensions (CheckMortality, CheckGenesis, etc.) are set in ExtrinsicBuilderFactory
|
||||
Log.d(TAG, "Adding Pezkuwi extensions: AuthorizeCall, CheckNonZeroSender, CheckWeight, WeightReclaim")
|
||||
extensions.add(AuthorizeCall())
|
||||
extensions.add(CheckNonZeroSender())
|
||||
extensions.add(CheckWeight())
|
||||
extensions.add(WeightReclaim())
|
||||
} else {
|
||||
// Other chains (Asset Hub, etc.) use ChargeAssetTxPayment and CheckAppId
|
||||
Log.d(TAG, "Adding default extensions: ChargeAssetTxPayment, CheckAppId")
|
||||
extensions.add(ChargeAssetTxPayment())
|
||||
extensions.add(CheckAppId())
|
||||
}
|
||||
|
||||
extensions.add(ChargeAssetTxPayment())
|
||||
extensions.add(CheckAppId())
|
||||
|
||||
return extensions
|
||||
}
|
||||
|
||||
private fun isPezkuwiChain(runtime: RuntimeSnapshot): Boolean {
|
||||
val genesisHash = runtime.metadata.extrinsic.signedExtensions
|
||||
.any { it.id == "AuthorizeCall" }
|
||||
return genesisHash
|
||||
val signedExtIds = runtime.metadata.extrinsic.signedExtensions.map { it.id }
|
||||
Log.d(TAG, "Metadata signed extensions: $signedExtIds")
|
||||
val hasAuthorizeCall = signedExtIds.any { it == "AuthorizeCall" }
|
||||
Log.d(TAG, "Has AuthorizeCall: $hasAuthorizeCall")
|
||||
return hasAuthorizeCall
|
||||
}
|
||||
}
|
||||
|
||||
+30
-1
@@ -1,12 +1,15 @@
|
||||
package io.novafoundation.nova.runtime.extrinsic
|
||||
|
||||
import android.util.Log
|
||||
import io.novafoundation.nova.common.utils.orZero
|
||||
import io.novafoundation.nova.runtime.ext.requireGenesisHash
|
||||
import io.novafoundation.nova.runtime.extrinsic.extensions.PezkuwiCheckMortality
|
||||
import io.novafoundation.nova.runtime.extrinsic.metadata.MetadataShortenerService
|
||||
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.novasama.substrate_sdk_android.extensions.fromHex
|
||||
import io.novasama.substrate_sdk_android.runtime.RuntimeSnapshot
|
||||
import io.novasama.substrate_sdk_android.runtime.extrinsic.BatchMode
|
||||
import io.novasama.substrate_sdk_android.runtime.extrinsic.ExtrinsicVersion
|
||||
import io.novasama.substrate_sdk_android.runtime.extrinsic.builder.ExtrinsicBuilder
|
||||
@@ -17,6 +20,8 @@ import io.novasama.substrate_sdk_android.runtime.extrinsic.v5.transactionExtensi
|
||||
import io.novasama.substrate_sdk_android.runtime.extrinsic.v5.transactionExtension.extensions.CheckTxVersion
|
||||
import io.novasama.substrate_sdk_android.runtime.extrinsic.v5.transactionExtension.extensions.checkMetadataHash.CheckMetadataHash
|
||||
|
||||
private const val TAG = "ExtrinsicBuilderFactory"
|
||||
|
||||
class ExtrinsicBuilderFactory(
|
||||
private val chainRegistry: ChainRegistry,
|
||||
private val mortalityConstructor: MortalityConstructor,
|
||||
@@ -40,16 +45,33 @@ class ExtrinsicBuilderFactory(
|
||||
): Sequence<ExtrinsicBuilder> {
|
||||
val runtime = chainRegistry.getRuntime(chain.id)
|
||||
|
||||
// Log metadata extensions
|
||||
val metadataExtensions = runtime.metadata.extrinsic.signedExtensions.map { it.id }
|
||||
Log.d(TAG, "Chain: ${chain.name}, Metadata extensions: $metadataExtensions")
|
||||
|
||||
val mortality = mortalityConstructor.constructMortality(chain.id)
|
||||
val metadataProof = metadataShortenerService.generateMetadataProof(chain.id)
|
||||
|
||||
// Log custom extensions
|
||||
val customExtensions = CustomTransactionExtensions.defaultValues(runtime).map { it.name }
|
||||
Log.d(TAG, "Custom extensions to add: $customExtensions")
|
||||
|
||||
val isPezkuwi = isPezkuwiChain(runtime)
|
||||
Log.d(TAG, "isPezkuwiChain: $isPezkuwi")
|
||||
|
||||
return generateSequence {
|
||||
ExtrinsicBuilder(
|
||||
runtime = runtime,
|
||||
extrinsicVersion = ExtrinsicVersion.V4,
|
||||
batchMode = options.batchMode,
|
||||
).apply {
|
||||
setTransactionExtension(CheckMortality(mortality.era, mortality.blockHash.fromHex()))
|
||||
// Use custom CheckMortality for Pezkuwi chains to avoid type lookup issues
|
||||
if (isPezkuwi) {
|
||||
Log.d(TAG, "Using PezkuwiCheckMortality for ${chain.name}")
|
||||
setTransactionExtension(PezkuwiCheckMortality(mortality.era, mortality.blockHash.fromHex()))
|
||||
} else {
|
||||
setTransactionExtension(CheckMortality(mortality.era, mortality.blockHash.fromHex()))
|
||||
}
|
||||
setTransactionExtension(CheckGenesis(chain.requireGenesisHash().fromHex()))
|
||||
setTransactionExtension(ChargeTransactionPayment(chain.additional?.defaultTip.orZero()))
|
||||
setTransactionExtension(CheckMetadataHash(metadataProof.checkMetadataHash))
|
||||
@@ -57,7 +79,14 @@ class ExtrinsicBuilderFactory(
|
||||
setTransactionExtension(CheckTxVersion(metadataProof.usedVersion.transactionVersion))
|
||||
|
||||
CustomTransactionExtensions.defaultValues(runtime).forEach(::setTransactionExtension)
|
||||
|
||||
Log.d(TAG, "All extensions set for ${chain.name}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun isPezkuwiChain(runtime: RuntimeSnapshot): Boolean {
|
||||
val signedExtIds = runtime.metadata.extrinsic.signedExtensions.map { it.id }
|
||||
return signedExtIds.any { it == "AuthorizeCall" }
|
||||
}
|
||||
}
|
||||
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
package io.novafoundation.nova.runtime.extrinsic.extensions
|
||||
|
||||
import io.novasama.substrate_sdk_android.runtime.extrinsic.v5.transactionExtension.extensions.FixedValueTransactionExtension
|
||||
|
||||
/**
|
||||
* Signed extension for PezkuwiChain that checks for non-zero sender.
|
||||
* This extension ensures the sender is not the zero address.
|
||||
*
|
||||
* In the runtime, CheckNonZeroSender is defined as:
|
||||
* pub struct CheckNonZeroSender<T>(core::marker::PhantomData<T>);
|
||||
*
|
||||
* It uses PhantomData internally, so it has no payload (empty encoding).
|
||||
*/
|
||||
class CheckNonZeroSender : FixedValueTransactionExtension(
|
||||
name = "CheckNonZeroSender",
|
||||
implicit = null,
|
||||
explicit = null // PhantomData encodes to nothing
|
||||
)
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
package io.novafoundation.nova.runtime.extrinsic.extensions
|
||||
|
||||
import io.novasama.substrate_sdk_android.runtime.extrinsic.v5.transactionExtension.extensions.FixedValueTransactionExtension
|
||||
|
||||
/**
|
||||
* Signed extension that checks weight limits.
|
||||
* This extension uses PhantomData internally, so it has no payload (empty encoding).
|
||||
*
|
||||
* In the runtime, CheckWeight is defined as:
|
||||
* pub struct CheckWeight<T>(core::marker::PhantomData<T>);
|
||||
*/
|
||||
class CheckWeight : FixedValueTransactionExtension(
|
||||
name = "CheckWeight",
|
||||
implicit = null,
|
||||
explicit = null // PhantomData encodes to nothing
|
||||
)
|
||||
+68
@@ -0,0 +1,68 @@
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
package io.novafoundation.nova.runtime.extrinsic.extensions
|
||||
|
||||
import io.novasama.substrate_sdk_android.runtime.extrinsic.v5.transactionExtension.extensions.FixedValueTransactionExtension
|
||||
|
||||
/**
|
||||
* Signed extension for PezkuwiChain that handles weight reclamation.
|
||||
* This extension reclaims unused weight after transaction execution.
|
||||
*
|
||||
* In the runtime, WeightReclaim is defined as:
|
||||
* pub struct WeightReclaim<T>(core::marker::PhantomData<T>);
|
||||
*
|
||||
* It uses PhantomData internally, so it has no payload (empty encoding).
|
||||
*/
|
||||
class WeightReclaim : FixedValueTransactionExtension(
|
||||
name = "WeightReclaim",
|
||||
implicit = null,
|
||||
explicit = null // PhantomData encodes to nothing
|
||||
)
|
||||
+23
-1
@@ -47,6 +47,10 @@ class RuntimeFactory(
|
||||
private val gson: Gson,
|
||||
private val concurrencyLimit: Int = 1
|
||||
) {
|
||||
companion object {
|
||||
@Volatile
|
||||
var lastDiagnostics: String = "not yet initialized"
|
||||
}
|
||||
|
||||
private val dispatcher = newLimitedThreadPoolExecutor(concurrencyLimit).asCoroutineDispatcher()
|
||||
private val semaphore = Semaphore(concurrencyLimit)
|
||||
@@ -88,9 +92,13 @@ class RuntimeFactory(
|
||||
)
|
||||
}
|
||||
|
||||
Log.d("RuntimeFactory", "DEBUG: TypesUsage for chain $chainId = $typesUsage")
|
||||
|
||||
val (types, baseHash, ownHash) = when (typesUsage) {
|
||||
TypesUsage.BASE -> {
|
||||
Log.d("RuntimeFactory", "DEBUG: Loading BASE types for $chainId")
|
||||
val (types, baseHash) = constructBaseTypes(typePreset)
|
||||
Log.d("RuntimeFactory", "DEBUG: BASE types loaded, hash=$baseHash, typeCount=${types.size}")
|
||||
|
||||
Triple(types, baseHash, null)
|
||||
}
|
||||
@@ -104,6 +112,15 @@ class RuntimeFactory(
|
||||
}
|
||||
|
||||
val typeRegistry = TypeRegistry(types, DynamicTypeResolver(DynamicTypeResolver.DEFAULT_COMPOUND_EXTENSIONS + GenericsExtension))
|
||||
|
||||
// DEBUG: Check for ExtrinsicSignature
|
||||
val hasExtrinsicSignature = typeRegistry["ExtrinsicSignature"] != null
|
||||
val hasMultiSignature = typeRegistry["MultiSignature"] != null
|
||||
Log.d("RuntimeFactory", "DEBUG: Chain $chainId - ExtrinsicSignature=$hasExtrinsicSignature, MultiSignature=$hasMultiSignature, typesUsage=$typesUsage, typeCount=${types.size}")
|
||||
|
||||
// Store diagnostic info for error messages
|
||||
lastDiagnostics = "typesUsage=$typesUsage, ExtrinsicSig=$hasExtrinsicSignature, MultiSig=$hasMultiSignature, typeCount=${types.size}"
|
||||
|
||||
val runtimeMetadata = VersionedRuntimeBuilder.buildMetadata(metadataReader, typeRegistry)
|
||||
|
||||
ConstructedRuntime(
|
||||
@@ -154,7 +171,12 @@ class RuntimeFactory(
|
||||
|
||||
private suspend fun constructBaseTypes(initialPreset: TypePreset): Pair<TypePreset, String> {
|
||||
val baseTypesRaw = runCatching { runtimeFilesCache.getBaseTypes() }
|
||||
.getOrElse { throw BaseTypesNotInCacheException }
|
||||
.getOrElse {
|
||||
Log.e("RuntimeFactory", "DEBUG: BaseTypes NOT in cache!")
|
||||
throw BaseTypesNotInCacheException
|
||||
}
|
||||
|
||||
Log.d("RuntimeFactory", "DEBUG: BaseTypes loaded, length=${baseTypesRaw.length}, contains ExtrinsicSignature=${baseTypesRaw.contains("ExtrinsicSignature")}")
|
||||
|
||||
val typePreset = parseBaseDefinitions(fromJson(baseTypesRaw), initialPreset)
|
||||
|
||||
|
||||
@@ -5,7 +5,14 @@ import io.novasama.substrate_sdk_android.runtime.definitions.types.primitives.Fi
|
||||
import io.novasama.substrate_sdk_android.runtime.definitions.types.skipAliases
|
||||
|
||||
fun RuntimeSnapshot.isEthereumAddress(): Boolean {
|
||||
val addressType = typeRegistry["Address"]!!.skipAliases()!!
|
||||
// Try different address type names used by different chains
|
||||
val addressType = typeRegistry["Address"]
|
||||
?: typeRegistry["MultiAddress"]
|
||||
?: typeRegistry["sp_runtime::multiaddress::MultiAddress"]
|
||||
?: typeRegistry["pezsp_runtime::multiaddress::MultiAddress"]
|
||||
?: return false // If no address type found, assume not Ethereum
|
||||
|
||||
return addressType is FixedByteArray && addressType.length == 20
|
||||
val resolvedType = addressType.skipAliases() ?: return false
|
||||
|
||||
return resolvedType is FixedByteArray && resolvedType.length == 20
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user