fix: Use actual seed from SecretStoreV2 for Pezkuwi bizinikiwi signing

The keypair.privateKey from SecretStoreV2 is NOT the original 32-byte seed.
This was causing public key mismatch when expanding the keypair.

Changes:
- SecretsSigner now gets seed via getAccountSecrets().seed()
- PezkuwiKeyPairSigner.fromSeed() expands seed to proper keypair
- Fixes "bad signature" error on HEZ transfers
This commit is contained in:
2026-02-07 04:59:27 +03:00
parent c12dd79c74
commit caa5e0f463
19 changed files with 885 additions and 93 deletions
@@ -1,19 +1,44 @@
package io.novafoundation.nova
import android.content.Context
import android.util.Log
import androidx.test.core.app.ApplicationProvider
import io.novafoundation.nova.common.address.AccountIdKey
import io.novafoundation.nova.common.di.FeatureUtils
import io.novafoundation.nova.common.utils.balances
import io.novafoundation.nova.feature_account_api.data.signer.CallExecutionType
import io.novafoundation.nova.feature_account_api.data.signer.NovaSigner
import io.novafoundation.nova.feature_account_api.data.signer.SigningContext
import io.novafoundation.nova.feature_account_api.data.signer.SigningMode
import io.novafoundation.nova.feature_account_api.data.signer.SubmissionHierarchy
import io.novafoundation.nova.feature_account_api.data.signer.setSignerData
import io.novafoundation.nova.feature_account_api.domain.model.LightMetaAccount
import io.novafoundation.nova.feature_account_api.domain.model.MetaAccount
import io.novafoundation.nova.feature_account_impl.domain.account.model.DefaultMetaAccount
import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.nativeTransfer
import io.novafoundation.nova.feature_wallet_api.di.WalletFeatureApi
import io.novafoundation.nova.runtime.ext.Geneses
import io.novafoundation.nova.runtime.ext.utilityAsset
import io.novafoundation.nova.runtime.extrinsic.ExtrinsicBuilderFactory
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.encrypt.SignatureWrapper
import io.novasama.substrate_sdk_android.runtime.AccountId
import io.novasama.substrate_sdk_android.runtime.extrinsic.BatchMode
import io.novasama.substrate_sdk_android.ss58.SS58Encoder.toAccountId
import io.novafoundation.nova.runtime.ext.Geneses
import io.novasama.substrate_sdk_android.runtime.extrinsic.Nonce
import io.novasama.substrate_sdk_android.runtime.extrinsic.builder.ExtrinsicBuilder
import io.novasama.substrate_sdk_android.runtime.extrinsic.signer.SignedRaw
import io.novasama.substrate_sdk_android.runtime.extrinsic.signer.SignerPayloadRaw
import io.novasama.substrate_sdk_android.runtime.extrinsic.v5.transactionExtension.InheritedImplication
import io.novasama.substrate_sdk_android.runtime.extrinsic.v5.transactionExtension.extensions.CheckNonce.Companion.setNonce
import io.novasama.substrate_sdk_android.runtime.extrinsic.v5.transactionExtension.extensions.verifySignature.GeneralTransactionSigner
import io.novasama.substrate_sdk_android.runtime.extrinsic.v5.transactionExtension.extensions.verifySignature.VerifySignature.Companion.setVerifySignature
import io.novasama.substrate_sdk_android.runtime.metadata.callOrNull
import io.novasama.substrate_sdk_android.runtime.metadata.moduleOrNull
import org.junit.Assert.assertNotNull
import org.junit.Assert.assertTrue
import org.junit.Assert.fail
import org.junit.Test
import java.math.BigInteger
@@ -34,7 +59,7 @@ class PezkuwiIntegrationTest : BaseIntegrationTest() {
WalletFeatureApi::class.java
)
private val extrinsicBuilderFactory = runtimeApi.extrinsicBuilderFactory()
private val extrinsicBuilderFactory = runtimeApi.provideExtrinsicBuilderFactory()
private val rpcCalls = runtimeApi.rpcCalls()
/**
@@ -59,7 +84,6 @@ class PezkuwiIntegrationTest : BaseIntegrationTest() {
assertNotNull("Address type should exist", address)
println("Pezkuwi Mainnet: All required types present")
println("TypeRegistry size: ${runtime.typeRegistry.keys.size}")
}
/**
@@ -106,20 +130,13 @@ class PezkuwiIntegrationTest : BaseIntegrationTest() {
val runtime = chainRegistry.getRuntime(chain.id)
// Check if balances module exists
val balancesModule = runtime.metadata.modules.find {
it.name.equals("Balances", ignoreCase = true)
}
val balancesModule = runtime.metadata.moduleOrNull("Balances")
assertNotNull("Balances module should exist", balancesModule)
// Check transfer call exists
val hasTransferKeepAlive = balancesModule?.calls?.any {
it.key.equals("transfer_keep_alive", ignoreCase = true)
} ?: false
val hasTransferAllowDeath = balancesModule?.calls?.any {
it.key.equals("transfer_allow_death", ignoreCase = true) ||
it.key.equals("transfer", ignoreCase = true)
} ?: false
val hasTransferKeepAlive = balancesModule?.callOrNull("transfer_keep_alive") != null
val hasTransferAllowDeath = balancesModule?.callOrNull("transfer_allow_death") != null ||
balancesModule?.callOrNull("transfer") != null
assertTrue("Transfer call should exist", hasTransferKeepAlive || hasTransferAllowDeath)
@@ -162,8 +179,146 @@ class PezkuwiIntegrationTest : BaseIntegrationTest() {
println("Pezkuwi utility asset: ${utilityAsset.symbol}, precision: ${utilityAsset.precision}")
}
/**
* Test 7: Build and sign a transfer extrinsic (THIS IS THE CRITICAL TEST)
* This test will catch "TypeReference is null" errors during signing
*/
@Test
fun testPezkuwiBuildSignedTransferExtrinsic() = runTest {
val chain = chainRegistry.getChain(Chain.Geneses.PEZKUWI)
val signer = TestSigner()
val builder = extrinsicBuilderFactory.create(
chain = chain,
options = ExtrinsicBuilderFactory.Options(BatchMode.BATCH)
)
// Add transfer call
val recipientAccountId = ByteArray(32) { 2 }
builder.nativeTransfer(accountId = recipientAccountId, amount = BigInteger.ONE)
// Set signer data (this is where TypeReference errors can occur)
try {
with(builder) {
signer.setSignerData(TestSigningContext(chain), SigningMode.SUBMISSION)
}
Log.d("PezkuwiTest", "Signer data set successfully")
} catch (e: Exception) {
Log.e("PezkuwiTest", "Failed to set signer data", e)
fail("Failed to set signer data: ${e.message}")
}
// Build the extrinsic (this is where TypeReference errors can also occur)
try {
val extrinsic = builder.buildExtrinsic()
assertNotNull("Built extrinsic should not be null", extrinsic)
Log.d("PezkuwiTest", "Extrinsic built successfully: ${extrinsic.extrinsicHex}")
println("Pezkuwi: Transfer extrinsic built and signed successfully!")
} catch (e: Exception) {
Log.e("PezkuwiTest", "Failed to build extrinsic", e)
fail("Failed to build extrinsic: ${e.message}\nCause: ${e.cause?.message}")
}
}
/**
* Test 8: Build extrinsic for fee calculation (uses fake signature)
*/
@Test
fun testPezkuwiBuildFeeExtrinsic() = runTest {
val chain = chainRegistry.getChain(Chain.Geneses.PEZKUWI)
val signer = TestSigner()
val builder = extrinsicBuilderFactory.create(
chain = chain,
options = ExtrinsicBuilderFactory.Options(BatchMode.BATCH)
)
val recipientAccountId = ByteArray(32) { 2 }
builder.nativeTransfer(accountId = recipientAccountId, amount = BigInteger.ONE)
// Set signer data for FEE mode (uses fake signature)
try {
with(builder) {
signer.setSignerData(TestSigningContext(chain), SigningMode.FEE)
}
val extrinsic = builder.buildExtrinsic()
assertNotNull("Fee extrinsic should not be null", extrinsic)
println("Pezkuwi: Fee extrinsic built successfully!")
} catch (e: Exception) {
Log.e("PezkuwiTest", "Failed to build fee extrinsic", e)
fail("Failed to build fee extrinsic: ${e.message}")
}
}
// Helper extension
private suspend fun ChainRegistry.pezkuwiMainnet(): Chain {
return getChain(Chain.Geneses.PEZKUWI)
}
// Test signer for building extrinsics without real keys
private inner class TestSigner : NovaSigner, GeneralTransactionSigner {
val accountId = ByteArray(32) { 1 }
override suspend fun callExecutionType(): CallExecutionType {
return CallExecutionType.IMMEDIATE
}
override val metaAccount: MetaAccount = DefaultMetaAccount(
id = 0,
globallyUniqueId = "0",
substrateAccountId = accountId,
substrateCryptoType = null,
substratePublicKey = null,
ethereumAddress = null,
ethereumPublicKey = null,
isSelected = true,
name = "test",
type = LightMetaAccount.Type.SECRETS,
chainAccounts = emptyMap(),
status = LightMetaAccount.Status.ACTIVE,
parentMetaId = null
)
override suspend fun getSigningHierarchy(): SubmissionHierarchy {
return SubmissionHierarchy(metaAccount, CallExecutionType.IMMEDIATE)
}
override suspend fun signRaw(payload: SignerPayloadRaw): SignedRaw {
error("Not implemented")
}
context(ExtrinsicBuilder)
override suspend fun setSignerDataForSubmission(context: SigningContext) {
setNonce(BigInteger.ZERO)
setVerifySignature(this@TestSigner, accountId)
}
context(ExtrinsicBuilder)
override suspend fun setSignerDataForFee(context: SigningContext) {
setSignerDataForSubmission(context)
}
override suspend fun submissionSignerAccountId(chain: Chain): AccountId {
return accountId
}
override suspend fun maxCallsPerTransaction(): Int? {
return null
}
override suspend fun signInheritedImplication(
inheritedImplication: InheritedImplication,
accountId: AccountId
): SignatureWrapper {
// Return a fake Sr25519 signature for testing
return SignatureWrapper.Sr25519(ByteArray(64))
}
}
private class TestSigningContext(override val chain: Chain) : SigningContext {
override suspend fun getNonce(accountId: AccountIdKey): Nonce {
return Nonce.ZERO
}
}
}
+1
View File
@@ -1 +1,2 @@
/build
/rust/target/
@@ -48,9 +48,19 @@ object PezkuwiAddressConstructor {
// Check the actual type structure
return when (resolvedType) {
is DictEnum -> {
Log.d(TAG, "Type is DictEnum with variants: ${resolvedType.elements.keys}")
// MultiAddress type - wrap in Id variant
DictEnum.Entry(MULTI_ADDRESS_ID, accountId)
// Use the actual variant name from the type
// Standard chains use "Id", but Pezkuwi uses numeric variants like "0"
val variantNames = resolvedType.elements.values.map { it.name }
Log.d(TAG, "Type is DictEnum with variants: $variantNames")
// Use "Id" if available, otherwise use the first variant (index 0)
val idVariantName = if (variantNames.contains(MULTI_ADDRESS_ID)) {
MULTI_ADDRESS_ID
} else {
resolvedType.elements[0]?.name ?: MULTI_ADDRESS_ID
}
Log.d(TAG, "Using variant name: $idVariantName")
DictEnum.Entry(idVariantName, accountId)
}
is FixedByteArray -> {
Log.d(TAG, "Type is FixedByteArray with length: ${resolvedType.length}, returning raw accountId")
@@ -58,9 +68,15 @@ object PezkuwiAddressConstructor {
accountId
}
null -> {
Log.d(TAG, "Resolved type is null, returning raw accountId for Pezkuwi")
// For Pezkuwi, if alias doesn't resolve, try raw accountId
accountId
Log.d(TAG, "Resolved type is null for type: $foundTypeName")
// If this is a MultiAddress type that couldn't resolve, use variant "0"
if (foundTypeName?.contains("MultiAddress") == true || foundTypeName?.contains("multiaddress") == true) {
Log.d(TAG, "Type appears to be MultiAddress, using variant 0")
DictEnum.Entry("0", accountId)
} else {
Log.d(TAG, "Returning raw accountId")
accountId
}
}
else -> {
Log.d(TAG, "Unknown type: ${resolvedType.javaClass.simpleName}, returning raw accountId")
@@ -351,10 +351,34 @@ class RealExtrinsicService(
// Build extrinsic
val extrinsic = try {
Log.d("RealExtrinsicService", "Building extrinsic for chain ${chain.name} (${chain.id})")
extrinsicBuilder.buildExtrinsic()
} catch (e: Exception) {
Log.e("RealExtrinsicService", "Failed to build extrinsic for chain ${chain.name}", e)
Log.e("RealExtrinsicService", "SigningMode: $signingMode, Chain: ${chain.id}")
Log.e("RealExtrinsicService", "Exception class: ${e::class.java.name}")
Log.e("RealExtrinsicService", "Message: ${e.message}")
Log.e("RealExtrinsicService", "Cause: ${e.cause?.message}")
Log.e("RealExtrinsicService", "Full stack trace:", e)
// Get runtime diagnostics
try {
val runtime = chainRegistry.getRuntime(chain.id)
val typeRegistry = runtime.typeRegistry
val hasExtrinsicSignature = typeRegistry["ExtrinsicSignature"] != null
val hasMultiSignature = typeRegistry["MultiSignature"] != null
val hasMultiAddress = typeRegistry["MultiAddress"] != null
val hasAddress = typeRegistry["Address"] != null
Log.e("RealExtrinsicService",
"Types: ExtrinsicSig=$hasExtrinsicSignature, MultiSig=$hasMultiSignature, " +
"MultiAddress=$hasMultiAddress, Address=$hasAddress")
// Check extrinsic extensions
val signedExtensions = runtime.metadata.extrinsic.signedExtensions.map { it.id }
Log.e("RealExtrinsicService", "Signed extensions: $signedExtensions")
} catch (diagEx: Exception) {
Log.e("RealExtrinsicService", "Failed to get diagnostics: ${diagEx.message}")
}
throw e
}
@@ -2,8 +2,10 @@ package io.novafoundation.nova.feature_account_impl.data.signer.secrets
import io.novafoundation.nova.common.base.errors.SigningCancelledException
import io.novafoundation.nova.common.data.secrets.v2.SecretStoreV2
import io.novafoundation.nova.common.data.secrets.v2.getAccountSecrets
import io.novafoundation.nova.common.data.secrets.v2.getChainAccountKeypair
import io.novafoundation.nova.common.data.secrets.v2.getMetaAccountKeypair
import io.novafoundation.nova.common.data.secrets.v2.seed
import io.novafoundation.nova.common.di.scope.FeatureScope
import io.novafoundation.nova.common.sequrity.TwoFactorVerificationResult
import io.novafoundation.nova.common.sequrity.TwoFactorVerificationService
@@ -91,7 +93,9 @@ class SecretsSigner(
// Use PezkuwiKeyPairSigner for Pezkuwi chains (bizinikiwi context)
// Use standard KeyPairSigner for other chains (substrate context)
return if (chain?.isPezkuwiChain == true) {
val pezkuwiSigner = PezkuwiKeyPairSigner(keypair)
// Get the original seed for Pezkuwi signing
val seed = getSeed(accountId) ?: error("No seed found for Pezkuwi signing")
val pezkuwiSigner = PezkuwiKeyPairSigner.fromSeed(seed)
pezkuwiSigner.signInheritedImplication(inheritedImplication, accountId)
} else {
val delegate = createDelegate(accountId, keypair)
@@ -107,7 +111,9 @@ class SecretsSigner(
// Use PezkuwiKeyPairSigner for Pezkuwi chains (bizinikiwi context)
return if (chain?.isPezkuwiChain == true) {
val pezkuwiSigner = PezkuwiKeyPairSigner(keypair)
// Get the original seed for Pezkuwi signing
val seed = getSeed(payload.accountId) ?: error("No seed found for Pezkuwi signing")
val pezkuwiSigner = PezkuwiKeyPairSigner.fromSeed(seed)
pezkuwiSigner.signRaw(payload)
} else {
val delegate = createDelegate(payload.accountId, keypair)
@@ -115,6 +121,11 @@ class SecretsSigner(
}
}
private suspend fun getSeed(accountId: AccountId): ByteArray? {
val secrets = secretStoreV2.getAccountSecrets(metaAccount.id, accountId)
return secrets.seed()
}
override suspend fun maxCallsPerTransaction(): Int? {
return null
}
+6 -15
View File
@@ -53,9 +53,6 @@
"typeExtras": null
}
],
"types": {
"overridesCommon": false
},
"additional": {
"themeColor": "#009639",
"defaultBlockTimeMillis": 6000,
@@ -91,12 +88,10 @@
"icon": "https://pezkuwichain.io/tokens/HEZ.png"
}
],
"types": {
"overridesCommon": false
},
"additional": {
"themeColor": "#009639",
"feeViaRuntimeCall": true
"feeViaRuntimeCall": true,
"disabledCheckMetadataHash": true
}
},
{
@@ -173,13 +168,11 @@
}
}
],
"types": {
"overridesCommon": false
},
"additional": {
"themeColor": "#009639",
"defaultBlockTimeMillis": 6000,
"feeViaRuntimeCall": true
"feeViaRuntimeCall": true,
"disabledCheckMetadataHash": true
}
},
{
@@ -224,13 +217,11 @@
"typeExtras": null
}
],
"types": {
"overridesCommon": false
},
"additional": {
"themeColor": "#009639",
"defaultBlockTimeMillis": 6000,
"feeViaRuntimeCall": true
"feeViaRuntimeCall": true,
"disabledCheckMetadataHash": true
}
},
{
+10 -1
View File
@@ -3550,7 +3550,16 @@
]
},
"ModuleId": "LockIdentifier",
"MultiAddress": "GenericMultiAddress",
"MultiAddress": {
"type": "enum",
"type_mapping": [
["Id", "AccountId"],
["Index", "Compact<AccountIndex>"],
["Raw", "Bytes"],
["Address32", "[u8; 32]"],
["Address20", "[u8; 20]"]
]
},
"MultiSigner": {
"type": "enum",
"type_mapping": [
+4 -4
View File
@@ -1,12 +1,12 @@
{
"types": {
"ExtrinsicSignature": "MultiSignature",
"Address": "pezsp_runtime::multiaddress::MultiAddress",
"LookupSource": "pezsp_runtime::multiaddress::MultiAddress"
"Address": "MultiAddress",
"LookupSource": "MultiAddress"
},
"typesAlias": {
"pezsp_runtime::multiaddress::MultiAddress": "MultiAddress",
"pezsp_runtime::MultiSignature": "MultiSignature",
"pezsp_runtime.multiaddress.MultiAddress": "MultiAddress",
"pezsp_runtime.MultiSignature": "MultiSignature",
"pezsp_runtime.generic.era.Era": "Era"
}
}
@@ -15,14 +15,6 @@ private const val TAG = "CustomTxExtensions"
object CustomTransactionExtensions {
// PezkuwiChain genesis hashes (mainnet and teyrchains)
private val PEZKUWI_GENESIS_HASHES = setOf(
"bb4a61ab0c4b8c12f5eab71d0c86c482e03a275ecdafee678dea712474d33d75", // Pezkuwi Mainnet
"00d0e1d0581c3cd5c5768652d52f4520184018b44f56a2ae1e0dc9d65c00c948", // Asset Hub
"58269e9c184f721e0309332d90cafc410df1519a5dc27a5fd9b3bf5fd2d129f8", // People Chain
"96eb58af1bb7288115b5e4ff1590422533e749293f231974536dc6672417d06f" // Zagros Testnet
)
fun applyDefaultValues(builder: ExtrinsicBuilder) {
defaultValues().forEach(builder::setTransactionExtension)
}
@@ -40,33 +32,32 @@ object CustomTransactionExtensions {
fun defaultValues(runtime: RuntimeSnapshot): List<TransactionExtension> {
val extensions = mutableListOf<TransactionExtension>()
val isPezkuwi = isPezkuwiChain(runtime)
val signedExtIds = runtime.metadata.extrinsic.signedExtensions.map { it.id }
Log.d(TAG, "isPezkuwiChain: $isPezkuwi")
Log.d(TAG, "Metadata signed extensions: $signedExtIds")
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")
// Add extensions based on what the metadata requires
if ("AuthorizeCall" in signedExtIds) {
extensions.add(AuthorizeCall())
}
if ("CheckNonZeroSender" in signedExtIds) {
extensions.add(CheckNonZeroSender())
}
if ("CheckWeight" in signedExtIds) {
extensions.add(CheckWeight())
}
if ("WeightReclaim" in signedExtIds || "StorageWeightReclaim" in signedExtIds) {
extensions.add(WeightReclaim())
} else {
// Other chains (Asset Hub, etc.) use ChargeAssetTxPayment and CheckAppId
Log.d(TAG, "Adding default extensions: ChargeAssetTxPayment, CheckAppId")
}
if ("ChargeAssetTxPayment" in signedExtIds) {
// Default to native fee payment (null assetId)
extensions.add(ChargeAssetTxPayment())
}
if ("CheckAppId" in signedExtIds) {
extensions.add(CheckAppId())
}
Log.d(TAG, "Extensions to add: ${extensions.map { it.name }}")
return extensions
}
private fun isPezkuwiChain(runtime: RuntimeSnapshot): Boolean {
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
}
}
@@ -0,0 +1,23 @@
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
)
@@ -168,10 +168,16 @@ internal class RealMetadataShortenerService(
}
private fun shouldCalculateMetadataHash(runtimeMetadata: RuntimeMetadata, chain: Chain): Boolean {
val canBeEnabled = chain.additional.shouldDisableMetadataHashCheck().not()
val disabledByConfig = chain.additional.shouldDisableMetadataHashCheck()
val canBeEnabled = disabledByConfig.not()
val atLeastMinimumVersion = runtimeMetadata.metadataVersion >= MINIMUM_METADATA_VERSION_TO_CALCULATE_HASH
val hasSignedExtension = runtimeMetadata.extrinsic.hasSignedExtension(DefaultSignedExtensions.CHECK_METADATA_HASH)
return canBeEnabled && atLeastMinimumVersion && hasSignedExtension
Log.d("MetadataShortenerService", "Chain: ${chain.name}, disabledByConfig=$disabledByConfig, canBeEnabled=$canBeEnabled, atLeastMinimumVersion=$atLeastMinimumVersion, hasSignedExtension=$hasSignedExtension")
Log.d("MetadataShortenerService", "chain.additional: ${chain.additional}, disabledCheckMetadataHash=${chain.additional?.disabledCheckMetadataHash}")
val result = canBeEnabled && atLeastMinimumVersion && hasSignedExtension
Log.d("MetadataShortenerService", "shouldCalculateMetadataHash result: $result (will use ${if (result) "ENABLED" else "DISABLED"} mode)")
return result
}
}
@@ -0,0 +1,316 @@
package io.novafoundation.nova.runtime.extrinsic.signer
import org.bouncycastle.jcajce.provider.digest.Keccak
import java.math.BigInteger
import java.nio.ByteBuffer
import java.nio.ByteOrder
/**
* SR25519 signing implementation for PezkuwiChain using "bizinikiwi" signing context.
*
* This is a port of @pezkuwi/scure-sr25519 which uses Merlin transcripts
* (built on Strobe128) with a custom signing context.
*
* Standard Substrate uses "substrate" context, but Pezkuwi uses "bizinikiwi".
*/
object BizinikiwSr25519Signer {
private val BIZINIKIWI_CONTEXT = "bizinikiwi".toByteArray(Charsets.UTF_8)
// Ed25519 curve order
private val CURVE_ORDER = BigInteger("7237005577332262213973186563042994240857116359379907606001950938285454250989")
// Strobe128 constants
private const val STROBE_R = 166
/**
* Sign a message using SR25519 with "bizinikiwi" context.
*
* @param secretKey 64-byte secret key (32-byte scalar + 32-byte nonce)
* @param message The message to sign
* @return 64-byte signature with Schnorrkel marker
*/
fun sign(secretKey: ByteArray, message: ByteArray): ByteArray {
require(secretKey.size == 64) { "Secret key must be 64 bytes" }
// Create signing transcript
val transcript = SigningContext("SigningContext")
transcript.label(BIZINIKIWI_CONTEXT)
transcript.bytes(message)
// Extract key components
val keyScalar = decodeScalar(secretKey.copyOfRange(0, 32))
val nonce = secretKey.copyOfRange(32, 64)
val publicKey = getPublicKey(secretKey)
val pubPoint = RistrettoPoint.fromBytes(publicKey)
// Schnorrkel signing protocol
transcript.protoName("Schnorr-sig")
transcript.commitPoint("sign:pk", pubPoint)
val r = transcript.witnessScalar("signing", nonce)
val R = RistrettoPoint.BASE.multiply(r)
transcript.commitPoint("sign:R", R)
val k = transcript.challengeScalar("sign:c")
val s = (k.multiply(keyScalar).add(r)).mod(CURVE_ORDER)
// Build signature
val signature = ByteArray(64)
System.arraycopy(R.toBytes(), 0, signature, 0, 32)
System.arraycopy(scalarToBytes(s), 0, signature, 32, 32)
// Add Schnorrkel marker
signature[63] = (signature[63].toInt() or 0x80).toByte()
return signature
}
/**
* Get public key from secret key.
*/
fun getPublicKey(secretKey: ByteArray): ByteArray {
require(secretKey.size == 64) { "Secret key must be 64 bytes" }
val scalar = decodeScalar(secretKey.copyOfRange(0, 32))
return RistrettoPoint.BASE.multiply(scalar).toBytes()
}
/**
* Verify a signature.
*/
fun verify(message: ByteArray, signature: ByteArray, publicKey: ByteArray): Boolean {
require(signature.size == 64) { "Signature must be 64 bytes" }
require(publicKey.size == 32) { "Public key must be 32 bytes" }
// Check Schnorrkel marker
if ((signature[63].toInt() and 0x80) == 0) {
return false
}
// Extract R and s from signature
val sBytes = signature.copyOfRange(32, 64)
sBytes[31] = (sBytes[31].toInt() and 0x7F).toByte() // Remove marker
val R = RistrettoPoint.fromBytes(signature.copyOfRange(0, 32))
val s = bytesToScalar(sBytes)
// Reconstruct transcript
val transcript = SigningContext("SigningContext")
transcript.label(BIZINIKIWI_CONTEXT)
transcript.bytes(message)
val pubPoint = RistrettoPoint.fromBytes(publicKey)
if (pubPoint.isZero()) return false
transcript.protoName("Schnorr-sig")
transcript.commitPoint("sign:pk", pubPoint)
transcript.commitPoint("sign:R", R)
val k = transcript.challengeScalar("sign:c")
// Verify: R + k*P == s*G
val left = R.add(pubPoint.multiply(k))
val right = RistrettoPoint.BASE.multiply(s)
return left == right
}
private fun decodeScalar(bytes: ByteArray): BigInteger {
require(bytes.size == 32) { "Scalar must be 32 bytes" }
// Little-endian
val reversed = bytes.reversedArray()
return BigInteger(1, reversed).mod(CURVE_ORDER)
}
private fun bytesToScalar(bytes: ByteArray): BigInteger {
val reversed = bytes.reversedArray()
return BigInteger(1, reversed).mod(CURVE_ORDER)
}
private fun scalarToBytes(scalar: BigInteger): ByteArray {
val bytes = scalar.toByteArray()
val result = ByteArray(32)
// Handle sign byte and padding
val start = if (bytes[0] == 0.toByte() && bytes.size > 32) 1 else 0
val length = minOf(bytes.size - start, 32)
val offset = 32 - length
System.arraycopy(bytes, start, result, offset, length)
// Convert to little-endian
return result.reversedArray()
}
/**
* Strobe128 implementation for Merlin transcripts.
*/
private class Strobe128(protocolLabel: String) {
private val state = ByteArray(200)
private var pos = 0
private var posBegin = 0
private var curFlags = 0
init {
// Initialize state
state[0] = 1
state[1] = (STROBE_R + 2).toByte()
state[2] = 1
state[3] = 0
state[4] = 1
state[5] = 96
val strobeVersion = "STROBEv1.0.2".toByteArray(Charsets.UTF_8)
System.arraycopy(strobeVersion, 0, state, 6, strobeVersion.size)
keccakF1600()
metaAD(protocolLabel.toByteArray(Charsets.UTF_8), false)
}
private fun keccakF1600() {
// Keccak-f[1600] permutation
// Using BouncyCastle's Keccak implementation would be more efficient,
// but for now we'll use a simplified version
val keccak = org.bouncycastle.crypto.digests.KeccakDigest(1600)
keccak.update(state, 0, state.size)
// This is a placeholder - need proper Keccak-p implementation
}
fun metaAD(data: ByteArray, more: Boolean) {
absorb(data)
}
fun AD(data: ByteArray, more: Boolean) {
absorb(data)
}
fun PRF(length: Int): ByteArray {
val result = ByteArray(length)
squeeze(result)
return result
}
private fun absorb(data: ByteArray) {
for (byte in data) {
state[pos] = (state[pos].toInt() xor byte.toInt()).toByte()
pos++
if (pos == STROBE_R) {
runF()
}
}
}
private fun squeeze(out: ByteArray) {
for (i in out.indices) {
out[i] = state[pos]
state[pos] = 0
pos++
if (pos == STROBE_R) {
runF()
}
}
}
private fun runF() {
state[pos] = (state[pos].toInt() xor posBegin).toByte()
state[pos + 1] = (state[pos + 1].toInt() xor 0x04).toByte()
state[STROBE_R + 1] = (state[STROBE_R + 1].toInt() xor 0x80).toByte()
keccakF1600()
pos = 0
posBegin = 0
}
}
/**
* Merlin signing context/transcript.
*/
private class SigningContext(label: String) {
private val strobe = Strobe128(label)
fun label(data: ByteArray) {
val lengthBytes = ByteBuffer.allocate(4).order(ByteOrder.LITTLE_ENDIAN).putInt(data.size).array()
strobe.metaAD(lengthBytes, false)
strobe.metaAD(data, true)
}
fun bytes(data: ByteArray) {
val lengthBytes = ByteBuffer.allocate(4).order(ByteOrder.LITTLE_ENDIAN).putInt(data.size).array()
strobe.metaAD(lengthBytes, false)
strobe.AD(data, false)
}
fun protoName(name: String) {
val data = name.toByteArray(Charsets.UTF_8)
strobe.metaAD(data, false)
}
fun commitPoint(label: String, point: RistrettoPoint) {
strobe.metaAD(label.toByteArray(Charsets.UTF_8), false)
strobe.AD(point.toBytes(), false)
}
fun witnessScalar(label: String, nonce: ByteArray): BigInteger {
strobe.metaAD(label.toByteArray(Charsets.UTF_8), false)
strobe.AD(nonce, false)
val bytes = strobe.PRF(64)
return bytesToWideScalar(bytes)
}
fun challengeScalar(label: String): BigInteger {
strobe.metaAD(label.toByteArray(Charsets.UTF_8), false)
val bytes = strobe.PRF(64)
return bytesToWideScalar(bytes)
}
private fun bytesToWideScalar(bytes: ByteArray): BigInteger {
// Reduce 64 bytes to a scalar modulo curve order
val reversed = bytes.reversedArray()
return BigInteger(1, reversed).mod(CURVE_ORDER)
}
}
/**
* Ristretto255 point operations.
* This is a placeholder - full implementation requires Ed25519 curve math.
*/
private class RistrettoPoint private constructor(private val bytes: ByteArray) {
companion object {
val BASE: RistrettoPoint by lazy {
// Ed25519 base point in Ristretto encoding
val baseBytes = ByteArray(32)
// TODO: Set proper base point bytes
RistrettoPoint(baseBytes)
}
fun fromBytes(bytes: ByteArray): RistrettoPoint {
require(bytes.size == 32) { "Point must be 32 bytes" }
return RistrettoPoint(bytes.copyOf())
}
}
fun toBytes(): ByteArray = bytes.copyOf()
fun multiply(scalar: BigInteger): RistrettoPoint {
// TODO: Implement scalar multiplication
return this
}
fun add(other: RistrettoPoint): RistrettoPoint {
// TODO: Implement point addition
return this
}
fun isZero(): Boolean {
return bytes.all { it == 0.toByte() }
}
override fun equals(other: Any?): Boolean {
if (other !is RistrettoPoint) return false
return bytes.contentEquals(other.bytes)
}
override fun hashCode(): Int = bytes.contentHashCode()
}
}
@@ -1,5 +1,6 @@
package io.novafoundation.nova.runtime.extrinsic.signer
import android.util.Log
import io.novafoundation.nova.sr25519.BizinikiwSr25519
import io.novasama.substrate_sdk_android.encrypt.SignatureWrapper
import io.novasama.substrate_sdk_android.encrypt.keypair.Keypair
@@ -17,31 +18,71 @@ import io.novasama.substrate_sdk_android.runtime.extrinsic.v5.transactionExtensi
* (Pezkuwi, Pezkuwi Asset Hub, Pezkuwi People) which require signatures with "bizinikiwi"
* context instead of the standard "substrate" context used by Polkadot ecosystem chains.
*/
class PezkuwiKeyPairSigner(
private val keypair: Keypair
class PezkuwiKeyPairSigner private constructor(
private val secretKey: ByteArray,
private val publicKey: ByteArray
) : GeneralTransactionSigner {
companion object {
/**
* Create a PezkuwiKeyPairSigner from a 32-byte seed.
* The seed is expanded to a full keypair using BizinikiwSr25519.
*/
fun fromSeed(seed: ByteArray): PezkuwiKeyPairSigner {
require(seed.size == 32) { "Seed must be 32 bytes, got ${seed.size}" }
Log.d("PezkuwiSigner", "Creating signer from seed")
// Expand seed to 96-byte keypair
val expandedKeypair = BizinikiwSr25519.keypairFromSeed(seed)
Log.d("PezkuwiSigner", "Expanded keypair size: ${expandedKeypair.size}")
// Extract 64-byte secret key and 32-byte public key
val secretKey = BizinikiwSr25519.secretKeyFromKeypair(expandedKeypair)
val publicKey = BizinikiwSr25519.publicKeyFromKeypair(expandedKeypair)
Log.d("PezkuwiSigner", "Secret key size: ${secretKey.size}")
Log.d("PezkuwiSigner", "Public key: ${publicKey.toHex()}")
return PezkuwiKeyPairSigner(secretKey, publicKey)
}
private fun ByteArray.toHex(): String = joinToString("") { "%02x".format(it) }
}
override suspend fun signInheritedImplication(
inheritedImplication: InheritedImplication,
accountId: AccountId
): SignatureWrapper {
val payload = inheritedImplication.signingPayload()
Log.d("PezkuwiSigner", "=== SIGNING WITH BIZINIKIWI ===")
Log.d("PezkuwiSigner", "Payload size: ${payload.size}")
Log.d("PezkuwiSigner", "Payload: ${payload.toHex()}")
// Use BizinikiwSr25519 native library with "bizinikiwi" signing context
val signature = BizinikiwSr25519.sign(
publicKey = keypair.publicKey,
secretKey = keypair.privateKey,
publicKey = publicKey,
secretKey = secretKey,
message = payload
)
Log.d("PezkuwiSigner", "Signature: ${signature.toHex()}")
// Verify locally
val verified = BizinikiwSr25519.verify(signature, payload, publicKey)
Log.d("PezkuwiSigner", "Local verification: $verified")
return SignatureWrapper.Sr25519(signature)
}
private fun ByteArray.toHex(): String = joinToString("") { "%02x".format(it) }
suspend fun signRaw(payload: SignerPayloadRaw): SignedRaw {
// Use BizinikiwSr25519 native library with "bizinikiwi" signing context
val signature = BizinikiwSr25519.sign(
publicKey = keypair.publicKey,
secretKey = keypair.privateKey,
publicKey = publicKey,
secretKey = secretKey,
message = payload.message
)
@@ -6,6 +6,7 @@ import io.novafoundation.nova.common.utils.md5
import io.novafoundation.nova.common.utils.newLimitedThreadPoolExecutor
import io.novafoundation.nova.core_db.dao.ChainDao
import io.novafoundation.nova.runtime.multiNetwork.chain.model.TypesUsage
import io.novafoundation.nova.runtime.multiNetwork.runtime.types.custom.pezkuwi.PezkuwiPathTypeMapping
import io.novafoundation.nova.runtime.multiNetwork.runtime.types.custom.vote.SiVoteTypeMapping
import io.novasama.substrate_sdk_android.runtime.RuntimeSnapshot
import io.novasama.substrate_sdk_android.runtime.definitions.TypeDefinitionParser.parseBaseDefinitions
@@ -111,18 +112,15 @@ class RuntimeFactory(
TypesUsage.NONE -> Triple(typePreset, null, null)
}
val typeRegistry = TypeRegistry(types, DynamicTypeResolver(DynamicTypeResolver.DEFAULT_COMPOUND_EXTENSIONS + GenericsExtension))
// Add Pezkuwi type aliases for chains that use pezsp_* types
val finalTypes = addPezkuwiTypeAliases(types)
// DEBUG: Check for ExtrinsicSignature
val hasExtrinsicSignature = typeRegistry["ExtrinsicSignature"] != null
val hasMultiSignature = typeRegistry["MultiSignature"] != null
Log.d(
"RuntimeFactory",
"Chain $chainId - ExtrinsicSig=$hasExtrinsicSignature, MultiSig=$hasMultiSignature, types=$typesUsage"
)
val typeRegistry = TypeRegistry(finalTypes, DynamicTypeResolver(DynamicTypeResolver.DEFAULT_COMPOUND_EXTENSIONS + GenericsExtension))
// Store diagnostic info for error messages
lastDiagnostics = "typesUsage=$typesUsage, ExtrinsicSig=$hasExtrinsicSignature, MultiSig=$hasMultiSignature, typeCount=${types.size}"
val hasExtrinsicSignature = typeRegistry["ExtrinsicSignature"] != null
val hasAddress = typeRegistry["Address"] != null
lastDiagnostics = "typesUsage=$typesUsage, ExtrinsicSig=$hasExtrinsicSignature, Address=$hasAddress, typeCount=${finalTypes.size}"
val runtimeMetadata = VersionedRuntimeBuilder.buildMetadata(metadataReader, typeRegistry)
@@ -167,7 +165,13 @@ class RuntimeFactory(
val withoutVersioning = parseBaseDefinitions(ownTypesTree, baseTypes)
val typePreset = parseNetworkVersioning(ownTypesTree, withoutVersioning, runtimeVersion)
// Try to parse versioning, but if it fails (e.g., no versioning field), use base definitions
val typePreset = try {
parseNetworkVersioning(ownTypesTree, withoutVersioning, runtimeVersion)
} catch (e: IllegalArgumentException) {
Log.w("RuntimeFactory", "No versioning info in chain types for $chainId, using base definitions")
withoutVersioning
}
return typePreset to ownTypesRaw.md5()
}
@@ -188,5 +192,47 @@ class RuntimeFactory(
private fun fromJson(types: String): TypeDefinitionsTree = gson.fromJson(types, TypeDefinitionsTree::class.java)
private fun allSiTypeMappings() = SiTypeMapping.default() + SiVoteTypeMapping()
// NOTE: Don't use PezkuwiExtrinsicTypeMapping here - its aliases create broken TypeReferences.
// Instead, addPezkuwiTypeAliases() handles copying actual type instances in the RuntimeFactory.
private fun allSiTypeMappings() = SiTypeMapping.default() + PezkuwiPathTypeMapping() + SiVoteTypeMapping()
/**
* For Pezkuwi chains that use pezsp_* type paths, copy the actual type instances
* to the standard type names (Address, ExtrinsicSignature, etc.).
*
* Pezkuwi chains use pezsp_* prefixes instead of sp_* prefixes for their type paths.
* This function ensures that code looking for standard type names will find the
* correct Pezkuwi types.
*/
private fun addPezkuwiTypeAliases(types: TypePreset): TypePreset {
val hasPezspTypes = types.keys.any { it.startsWith("pezsp_") || it.startsWith("pezframe_") || it.startsWith("pezpallet_") }
if (!hasPezspTypes) {
return types
}
val mutableTypes = types.toMutableMap()
// Map Pezkuwi type paths to standard type names.
// These types are parsed as actual types from metadata (not aliased to built-ins),
// and we copy them to standard names so existing code can find them.
val pezkuwiTypeAliases = listOf(
"pezsp_runtime.multiaddress.MultiAddress" to "Address",
"pezsp_runtime.multiaddress.MultiAddress" to "MultiAddress",
"pezsp_runtime.MultiSignature" to "ExtrinsicSignature",
"pezsp_runtime.MultiSignature" to "MultiSignature",
"pezsp_runtime.generic.era.Era" to "Era",
// Fee-related types
"pezframe_support.dispatch.DispatchInfo" to "DispatchInfo",
"pezpallet_transaction_payment.types.RuntimeDispatchInfo" to "RuntimeDispatchInfo",
"pezframe_support.dispatch.DispatchClass" to "DispatchClass"
)
for ((pezspPath, standardName) in pezkuwiTypeAliases) {
types[pezspPath]?.let { pezspType ->
mutableTypes[standardName] = pezspType
}
}
return mutableTypes
}
}
@@ -0,0 +1,85 @@
package io.novafoundation.nova.runtime.multiNetwork.runtime.types.custom.pezkuwi
import android.util.Log
import io.novasama.substrate_sdk_android.runtime.definitions.registry.TypePresetBuilder
import io.novasama.substrate_sdk_android.runtime.definitions.registry.alias
import io.novasama.substrate_sdk_android.runtime.definitions.types.Type
import io.novasama.substrate_sdk_android.runtime.definitions.types.instances.ExtrinsicTypes
import io.novasama.substrate_sdk_android.runtime.definitions.v14.typeMapping.SiTypeMapping
import io.novasama.substrate_sdk_android.runtime.metadata.v14.PortableType
import io.novasama.substrate_sdk_android.runtime.metadata.v14.paramType
import io.novasama.substrate_sdk_android.runtime.metadata.v14.type
import io.novasama.substrate_sdk_android.scale.EncodableStruct
/**
* Custom SiTypeMapping for Pezkuwi chains that use pezsp_* package prefixes
* instead of the standard sp_* prefixes used by Polkadot/Substrate chains.
*
* This mapping detects `pezsp_runtime.generic.unchecked_extrinsic.UncheckedExtrinsic`
* and extracts the Address and Signature type parameters to register them as
* ExtrinsicTypes.ADDRESS and ExtrinsicTypes.SIGNATURE aliases.
*
* Without this mapping, Pezkuwi transactions would fail because the SDK's default
* AddExtrinsicTypesSiTypeMapping only looks for sp_runtime paths.
*/
private const val PEZSP_UNCHECKED_EXTRINSIC_TYPE = "pezsp_runtime.generic.unchecked_extrinsic.UncheckedExtrinsic"
class PezkuwiExtrinsicTypeMapping : SiTypeMapping {
override fun map(
originalDefinition: EncodableStruct<PortableType>,
suggestedTypeName: String,
typesBuilder: TypePresetBuilder
): Type<*>? {
// Log all type names that contain "pezsp" and "extrinsic" for debugging
if (suggestedTypeName.contains("pezsp", ignoreCase = true) && suggestedTypeName.contains("extrinsic", ignoreCase = true)) {
Log.d("PezkuwiExtrinsicMapping", "Seeing type: $suggestedTypeName")
}
if (suggestedTypeName == PEZSP_UNCHECKED_EXTRINSIC_TYPE) {
Log.d("PezkuwiExtrinsicMapping", "MATCHED! Processing UncheckedExtrinsic type")
// Extract Address type param and register as "Address" alias
val addressAdded = addTypeFromTypeParams(
originalDefinition = originalDefinition,
typesBuilder = typesBuilder,
typeParamName = "Address",
newTypeName = ExtrinsicTypes.ADDRESS
)
Log.d("PezkuwiExtrinsicMapping", "Address alias added: $addressAdded")
// Extract Signature type param and register as "ExtrinsicSignature" alias
val sigAdded = addTypeFromTypeParams(
originalDefinition = originalDefinition,
typesBuilder = typesBuilder,
typeParamName = "Signature",
newTypeName = ExtrinsicTypes.SIGNATURE
)
Log.d("PezkuwiExtrinsicMapping", "ExtrinsicSignature alias added: $sigAdded")
}
// We don't modify any existing type, just add aliases
return null
}
private fun addTypeFromTypeParams(
originalDefinition: EncodableStruct<PortableType>,
typesBuilder: TypePresetBuilder,
typeParamName: String,
newTypeName: String
): Boolean {
val paramType = originalDefinition.type.paramType(typeParamName)
Log.d("PezkuwiExtrinsicMapping", "Looking for param '$typeParamName', found: $paramType")
if (paramType == null) {
Log.w("PezkuwiExtrinsicMapping", "Could not find type param '$typeParamName' in UncheckedExtrinsic")
return false
}
// Type with type-id name is present in the registry as alias to fully qualified name
val targetType = paramType.toString()
Log.d("PezkuwiExtrinsicMapping", "Creating alias: $newTypeName -> $targetType")
typesBuilder.alias(newTypeName, targetType)
return true
}
}
@@ -0,0 +1,31 @@
package io.novafoundation.nova.runtime.multiNetwork.runtime.types.custom.pezkuwi
import io.novasama.substrate_sdk_android.runtime.definitions.v14.typeMapping.PathMatchTypeMapping
import io.novasama.substrate_sdk_android.runtime.definitions.v14.typeMapping.PathMatchTypeMapping.Replacement.AliasTo
/**
* PathMatchTypeMapping for Pezkuwi chains that use pezsp_* and pezframe_* package prefixes
* instead of the standard sp_* and frame_* prefixes used by Polkadot/Substrate chains.
*
* This maps specific Pezkuwi type paths to standard type names:
* - RuntimeCall/RuntimeEvent -> GenericCall/GenericEvent
*
* IMPORTANT: Era, MultiSignature, MultiAddress, and Weight types are NOT aliased here.
* They need to be parsed as actual types from metadata. RuntimeFactory.addPezkuwiTypeAliases()
* handles copying these types to standard names after parsing.
*
* NOTE: Weight types (pezsp_weights.weight_v2.Weight) are NOT aliased because the SDK
* doesn't have a WeightV1 type defined. They are parsed as structs from metadata.
*/
fun PezkuwiPathTypeMapping(): PathMatchTypeMapping = PathMatchTypeMapping(
// NOTE: Do NOT alias pezsp_runtime.generic.era.Era, pezsp_runtime.MultiSignature,
// pezsp_runtime.multiaddress.MultiAddress, or pezsp_weights.weight_v2.Weight here.
// These need to be parsed as actual types from metadata.
// RuntimeFactory.addPezkuwiTypeAliases() copies the parsed types to standard names.
// Runtime call/event types for Pezkuwi
"*.RuntimeCall" to AliasTo("GenericCall"),
"*.RuntimeEvent" to AliasTo("GenericEvent"),
"*_runtime.Call" to AliasTo("GenericCall"),
"*_runtime.Event" to AliasTo("GenericEvent"),
)
@@ -66,7 +66,14 @@ class RpcCalls(
val chainId = chain.id
val runtime = chainRegistry.getRuntime(chainId)
// Pezkuwi chains have issues with V15 automatic type resolution for RuntimeDispatchInfo
// Use pre-V15 method with explicit return type for Pezkuwi chains
val isPezkuwiChain = runtime.metadata.extrinsic.signedExtensions.any { it.id == "AuthorizeCall" }
return when {
// For Pezkuwi chains, prefer pre-V15 method which uses explicit return type
isPezkuwiChain && chain.additional.feeViaRuntimeCall() && runtime.hasFeeDecodeType() -> queryFeeViaRuntimeApiPreV15(chainId, extrinsic)
chain.additional.feeViaRuntimeCall() && runtime.metadata.hasDetectedPaymentApi() -> queryFeeViaRuntimeApiV15(chainId, extrinsic)
chain.additional.feeViaRuntimeCall() && runtime.hasFeeDecodeType() -> queryFeeViaRuntimeApiPreV15(chainId, extrinsic)
@@ -1,5 +1,6 @@
package io.novafoundation.nova.runtime.util
import android.util.Log
import io.novasama.substrate_sdk_android.runtime.AccountId
import io.novasama.substrate_sdk_android.runtime.definitions.types.RuntimeType
import io.novasama.substrate_sdk_android.runtime.definitions.types.composite.DictEnum
@@ -7,14 +8,52 @@ import io.novasama.substrate_sdk_android.runtime.definitions.types.generics.MULT
import io.novasama.substrate_sdk_android.runtime.definitions.types.primitives.FixedByteArray
import io.novasama.substrate_sdk_android.runtime.definitions.types.skipAliases
private const val TAG = "AccountLookup"
fun RuntimeType<*, *>.constructAccountLookupInstance(accountId: AccountId): Any {
return when (skipAliases()) {
is DictEnum -> { // MultiAddress
DictEnum.Entry(MULTI_ADDRESS_ID, accountId)
val resolvedType = skipAliases()
Log.d(TAG, "Type name: ${this.name}, resolved type: ${resolvedType?.javaClass?.simpleName}")
return when (resolvedType) {
is DictEnum -> {
// MultiAddress type - wrap in the appropriate variant
// Standard chains use "Id", but Pezkuwi uses numeric variants like "0"
val variantNames = resolvedType.elements.values.map { it.name }
Log.d(TAG, "DictEnum variants: $variantNames")
// Use "Id" if available (standard chains), otherwise use the first variant (index 0)
// which is always the AccountId variant in MultiAddress
val idVariantName = if (variantNames.contains(MULTI_ADDRESS_ID)) {
MULTI_ADDRESS_ID
} else {
// For chains like Pezkuwi that use numeric variant names
resolvedType.elements[0]?.name ?: MULTI_ADDRESS_ID
}
Log.d(TAG, "Using variant name: $idVariantName")
DictEnum.Entry(idVariantName, accountId)
}
is FixedByteArray -> { // GenericAccountId or similar
is FixedByteArray -> {
// GenericAccountId or similar - return raw accountId
Log.d(TAG, "FixedByteArray type, returning raw accountId")
accountId
}
null -> {
// For Pezkuwi chains where alias might not resolve properly
// Check if the original type name suggests MultiAddress
Log.d(TAG, "Resolved type is null, checking original type name: ${this.name}")
if (this.name?.contains("MultiAddress") == true || this.name?.contains("multiaddress") == true) {
// For unresolved MultiAddress types, use "0" which is the standard first variant (AccountId)
Log.d(TAG, "Type name contains MultiAddress, using DictEnum.Entry with variant 0")
DictEnum.Entry("0", accountId)
} else {
Log.d(TAG, "Unknown type with null resolution, returning raw accountId")
accountId
}
}
else -> {
// Unknown type - for Pezkuwi compatibility, try raw accountId instead of throwing
Log.w(TAG, "Unknown address type: ${this.name} (${resolvedType.javaClass.simpleName}), trying raw accountId")
accountId
}
else -> throw UnsupportedOperationException("Unknown address type: ${this.name}")
}
}
+1 -1
View File
@@ -1 +1 @@
VERSION_CODE=33
VERSION_CODE=103