From caa5e0f4634b7324200bc24793c12fead2d32018 Mon Sep 17 00:00:00 2001 From: Kurdistan Tech Ministry Date: Sat, 7 Feb 2026 04:59:27 +0300 Subject: [PATCH] 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 --- .../nova/PezkuwiIntegrationTest.kt | 185 +++++++++- bindings/sr25519-bizinikiwi/.gitignore | 1 + .../common/utils/PezkuwiAddressConstructor.kt | 28 +- .../data/extrinsic/RealExtrinsicService.kt | 24 ++ .../data/signer/secrets/SecretsSigner.kt | 15 +- pezkuwi-config/chains.json | 21 +- runtime/src/main/assets/types/default.json | 11 +- runtime/src/main/assets/types/pezkuwi.json | 8 +- .../extrinsic/CustomTransactionExtensions.kt | 41 +-- .../extensions/PezkuwiCheckImmortal.kt | 23 ++ .../metadata/MetadataShortenerService.kt | 10 +- .../signer/BizinikiwSr25519Signer.kt | 316 ++++++++++++++++++ .../extrinsic/signer/PezkuwiKeyPairSigner.kt | 53 ++- .../multiNetwork/runtime/RuntimeFactory.kt | 68 +++- .../pezkuwi/PezkuwiExtrinsicTypeMapping.kt | 85 +++++ .../custom/pezkuwi/PezkuwiPathTypeMapping.kt | 31 ++ .../nova/runtime/network/rpc/RpcCalls.kt | 7 + .../nova/runtime/util/AccountLookup.kt | 49 ++- version.properties | 2 +- 19 files changed, 885 insertions(+), 93 deletions(-) create mode 100644 runtime/src/main/java/io/novafoundation/nova/runtime/extrinsic/extensions/PezkuwiCheckImmortal.kt create mode 100644 runtime/src/main/java/io/novafoundation/nova/runtime/extrinsic/signer/BizinikiwSr25519Signer.kt create mode 100644 runtime/src/main/java/io/novafoundation/nova/runtime/multiNetwork/runtime/types/custom/pezkuwi/PezkuwiExtrinsicTypeMapping.kt create mode 100644 runtime/src/main/java/io/novafoundation/nova/runtime/multiNetwork/runtime/types/custom/pezkuwi/PezkuwiPathTypeMapping.kt diff --git a/app/src/androidTest/java/io/novafoundation/nova/PezkuwiIntegrationTest.kt b/app/src/androidTest/java/io/novafoundation/nova/PezkuwiIntegrationTest.kt index ef70359..c241aeb 100644 --- a/app/src/androidTest/java/io/novafoundation/nova/PezkuwiIntegrationTest.kt +++ b/app/src/androidTest/java/io/novafoundation/nova/PezkuwiIntegrationTest.kt @@ -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 + } + } } diff --git a/bindings/sr25519-bizinikiwi/.gitignore b/bindings/sr25519-bizinikiwi/.gitignore index 796b96d..c42713e 100644 --- a/bindings/sr25519-bizinikiwi/.gitignore +++ b/bindings/sr25519-bizinikiwi/.gitignore @@ -1 +1,2 @@ /build +/rust/target/ diff --git a/common/src/main/java/io/novafoundation/nova/common/utils/PezkuwiAddressConstructor.kt b/common/src/main/java/io/novafoundation/nova/common/utils/PezkuwiAddressConstructor.kt index c28ee35..9cab718 100644 --- a/common/src/main/java/io/novafoundation/nova/common/utils/PezkuwiAddressConstructor.kt +++ b/common/src/main/java/io/novafoundation/nova/common/utils/PezkuwiAddressConstructor.kt @@ -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") diff --git a/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/data/extrinsic/RealExtrinsicService.kt b/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/data/extrinsic/RealExtrinsicService.kt index 751882b..b59e96f 100644 --- a/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/data/extrinsic/RealExtrinsicService.kt +++ b/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/data/extrinsic/RealExtrinsicService.kt @@ -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 } diff --git a/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/data/signer/secrets/SecretsSigner.kt b/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/data/signer/secrets/SecretsSigner.kt index da35156..2336b9e 100644 --- a/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/data/signer/secrets/SecretsSigner.kt +++ b/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/data/signer/secrets/SecretsSigner.kt @@ -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 } diff --git a/pezkuwi-config/chains.json b/pezkuwi-config/chains.json index de7449c..482875a 100644 --- a/pezkuwi-config/chains.json +++ b/pezkuwi-config/chains.json @@ -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 } }, { diff --git a/runtime/src/main/assets/types/default.json b/runtime/src/main/assets/types/default.json index 6747b48..ceb2637 100644 --- a/runtime/src/main/assets/types/default.json +++ b/runtime/src/main/assets/types/default.json @@ -3550,7 +3550,16 @@ ] }, "ModuleId": "LockIdentifier", - "MultiAddress": "GenericMultiAddress", + "MultiAddress": { + "type": "enum", + "type_mapping": [ + ["Id", "AccountId"], + ["Index", "Compact"], + ["Raw", "Bytes"], + ["Address32", "[u8; 32]"], + ["Address20", "[u8; 20]"] + ] + }, "MultiSigner": { "type": "enum", "type_mapping": [ diff --git a/runtime/src/main/assets/types/pezkuwi.json b/runtime/src/main/assets/types/pezkuwi.json index 7e71938..cd90017 100644 --- a/runtime/src/main/assets/types/pezkuwi.json +++ b/runtime/src/main/assets/types/pezkuwi.json @@ -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" } } diff --git a/runtime/src/main/java/io/novafoundation/nova/runtime/extrinsic/CustomTransactionExtensions.kt b/runtime/src/main/java/io/novafoundation/nova/runtime/extrinsic/CustomTransactionExtensions.kt index 32558b9..317b2fd 100644 --- a/runtime/src/main/java/io/novafoundation/nova/runtime/extrinsic/CustomTransactionExtensions.kt +++ b/runtime/src/main/java/io/novafoundation/nova/runtime/extrinsic/CustomTransactionExtensions.kt @@ -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 { val extensions = mutableListOf() - 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 - } } 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 new file mode 100644 index 0000000..1cb3872 --- /dev/null +++ b/runtime/src/main/java/io/novafoundation/nova/runtime/extrinsic/extensions/PezkuwiCheckImmortal.kt @@ -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("Immortal", null) // Immortal variant - unit type with no value +) diff --git a/runtime/src/main/java/io/novafoundation/nova/runtime/extrinsic/metadata/MetadataShortenerService.kt b/runtime/src/main/java/io/novafoundation/nova/runtime/extrinsic/metadata/MetadataShortenerService.kt index 5fdf4ce..98244be 100644 --- a/runtime/src/main/java/io/novafoundation/nova/runtime/extrinsic/metadata/MetadataShortenerService.kt +++ b/runtime/src/main/java/io/novafoundation/nova/runtime/extrinsic/metadata/MetadataShortenerService.kt @@ -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 } } diff --git a/runtime/src/main/java/io/novafoundation/nova/runtime/extrinsic/signer/BizinikiwSr25519Signer.kt b/runtime/src/main/java/io/novafoundation/nova/runtime/extrinsic/signer/BizinikiwSr25519Signer.kt new file mode 100644 index 0000000..0f1681c --- /dev/null +++ b/runtime/src/main/java/io/novafoundation/nova/runtime/extrinsic/signer/BizinikiwSr25519Signer.kt @@ -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() + } +} diff --git a/runtime/src/main/java/io/novafoundation/nova/runtime/extrinsic/signer/PezkuwiKeyPairSigner.kt b/runtime/src/main/java/io/novafoundation/nova/runtime/extrinsic/signer/PezkuwiKeyPairSigner.kt index 9c18b9b..3da9637 100644 --- a/runtime/src/main/java/io/novafoundation/nova/runtime/extrinsic/signer/PezkuwiKeyPairSigner.kt +++ b/runtime/src/main/java/io/novafoundation/nova/runtime/extrinsic/signer/PezkuwiKeyPairSigner.kt @@ -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 ) diff --git a/runtime/src/main/java/io/novafoundation/nova/runtime/multiNetwork/runtime/RuntimeFactory.kt b/runtime/src/main/java/io/novafoundation/nova/runtime/multiNetwork/runtime/RuntimeFactory.kt index b0e118e..3b4dd8b 100644 --- a/runtime/src/main/java/io/novafoundation/nova/runtime/multiNetwork/runtime/RuntimeFactory.kt +++ b/runtime/src/main/java/io/novafoundation/nova/runtime/multiNetwork/runtime/RuntimeFactory.kt @@ -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 + } } diff --git a/runtime/src/main/java/io/novafoundation/nova/runtime/multiNetwork/runtime/types/custom/pezkuwi/PezkuwiExtrinsicTypeMapping.kt b/runtime/src/main/java/io/novafoundation/nova/runtime/multiNetwork/runtime/types/custom/pezkuwi/PezkuwiExtrinsicTypeMapping.kt new file mode 100644 index 0000000..89c5701 --- /dev/null +++ b/runtime/src/main/java/io/novafoundation/nova/runtime/multiNetwork/runtime/types/custom/pezkuwi/PezkuwiExtrinsicTypeMapping.kt @@ -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, + 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, + 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 + } +} diff --git a/runtime/src/main/java/io/novafoundation/nova/runtime/multiNetwork/runtime/types/custom/pezkuwi/PezkuwiPathTypeMapping.kt b/runtime/src/main/java/io/novafoundation/nova/runtime/multiNetwork/runtime/types/custom/pezkuwi/PezkuwiPathTypeMapping.kt new file mode 100644 index 0000000..9b5ec34 --- /dev/null +++ b/runtime/src/main/java/io/novafoundation/nova/runtime/multiNetwork/runtime/types/custom/pezkuwi/PezkuwiPathTypeMapping.kt @@ -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"), +) diff --git a/runtime/src/main/java/io/novafoundation/nova/runtime/network/rpc/RpcCalls.kt b/runtime/src/main/java/io/novafoundation/nova/runtime/network/rpc/RpcCalls.kt index 98e6d5b..004c259 100644 --- a/runtime/src/main/java/io/novafoundation/nova/runtime/network/rpc/RpcCalls.kt +++ b/runtime/src/main/java/io/novafoundation/nova/runtime/network/rpc/RpcCalls.kt @@ -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) diff --git a/runtime/src/main/java/io/novafoundation/nova/runtime/util/AccountLookup.kt b/runtime/src/main/java/io/novafoundation/nova/runtime/util/AccountLookup.kt index 5432593..4469ea3 100644 --- a/runtime/src/main/java/io/novafoundation/nova/runtime/util/AccountLookup.kt +++ b/runtime/src/main/java/io/novafoundation/nova/runtime/util/AccountLookup.kt @@ -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}") } } diff --git a/version.properties b/version.properties index a4e54cf..4b465c9 100644 --- a/version.properties +++ b/version.properties @@ -1 +1 @@ -VERSION_CODE=33 \ No newline at end of file +VERSION_CODE=103 \ No newline at end of file