mirror of
https://github.com/pezkuwichain/pezkuwi-wallet-android.git
synced 2026-04-22 02:07:58 +00:00
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:
@@ -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 +1,2 @@
|
||||
/build
|
||||
/rust/target/
|
||||
|
||||
+22
-6
@@ -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")
|
||||
|
||||
+24
@@ -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
|
||||
}
|
||||
|
||||
|
||||
+13
-2
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
@@ -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": [
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
+16
-25
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
+23
@@ -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
|
||||
)
|
||||
+8
-2
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
+316
@@ -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()
|
||||
}
|
||||
}
|
||||
+47
-6
@@ -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
|
||||
)
|
||||
|
||||
|
||||
+57
-11
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
+85
@@ -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
|
||||
}
|
||||
}
|
||||
+31
@@ -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
@@ -1 +1 @@
|
||||
VERSION_CODE=33
|
||||
VERSION_CODE=103
|
||||
Reference in New Issue
Block a user