Files
pezkuwi-wallet-android/app/src/androidTest/java/io/novafoundation/nova/PezkuwiLiveTransferTest.kt
T
pezkuwichain a294aa1a6b Initial commit: Pezkuwi Wallet Android
Security hardened release:
- Code obfuscation enabled (minifyEnabled=true, shrinkResources=true)
- Sensitive files excluded (google-services.json, keystores)
- Branch.io key moved to BuildConfig placeholder
- Updated dependencies: OkHttp 4.12.0, Gson 2.10.1, BouncyCastle 1.77
- Comprehensive ProGuard rules for crypto wallet
- Navigation 2.7.7, Lifecycle 2.7.0, ConstraintLayout 2.1.4
2026-02-12 05:19:41 +03:00

431 lines
19 KiB
Kotlin

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.deriveSeed32
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.TransferMode
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.requireGenesisHash
import io.novafoundation.nova.runtime.extrinsic.ExtrinsicBuilderFactory
import io.novasama.substrate_sdk_android.extensions.fromHex
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.novafoundation.nova.runtime.network.rpc.RpcCalls
import io.novasama.substrate_sdk_android.encrypt.EncryptionType
import io.novasama.substrate_sdk_android.encrypt.MultiChainEncryption
import io.novasama.substrate_sdk_android.encrypt.SignatureWrapper
import io.novasama.substrate_sdk_android.encrypt.keypair.Keypair
import io.novasama.substrate_sdk_android.encrypt.keypair.substrate.SubstrateKeypairFactory
import io.novasama.substrate_sdk_android.encrypt.seed.substrate.SubstrateSeedFactory
import io.novasama.substrate_sdk_android.runtime.AccountId
import io.novasama.substrate_sdk_android.runtime.extrinsic.BatchMode
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.KeyPairSigner
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.extrinsic.v5.transactionExtension.signingPayload
import io.novasama.substrate_sdk_android.ss58.SS58Encoder.toAccountId
import io.novafoundation.nova.sr25519.BizinikiwSr25519
import org.junit.Assert.assertNotNull
import org.junit.Assert.fail
import org.junit.Test
import java.math.BigInteger
/**
* LIVE TRANSFER TEST - Transfers real HEZ tokens on Pezkuwi mainnet!
*
* Sender: 5DXv3Dc5xELckTgcYa2dm1TSZPgqDPxVDW3Cid4ALWpVjY3w
* Recipient: 5HdY6U2UQF8wPwczP3SoQz28kQu1WJSBqxKGePUKG4M5QYdV
* Amount: 5 HEZ
*
* Run with: ./gradlew :app:connectedAndroidTest -Pandroid.testInstrumentationRunnerArguments.class=io.novafoundation.nova.PezkuwiLiveTransferTest
*/
class PezkuwiLiveTransferTest : BaseIntegrationTest() {
companion object {
// Test wallet mnemonic
private const val TEST_MNEMONIC = "crucial surge north silly divert throw habit fury zebra fabric tank output"
// Sender address (derived from mnemonic)
private const val SENDER_ADDRESS = "5DXv3Dc5xELckTgcYa2dm1TSZPgqDPxVDW3Cid4ALWpVjY3w"
// Recipient address
private const val RECIPIENT_ADDRESS = "5HdY6U2UQF8wPwczP3SoQz28kQu1WJSBqxKGePUKG4M5QYdV"
// Amount: 5 HEZ (with 12 decimals)
private val TRANSFER_AMOUNT = BigInteger("5000000000000") // 5 * 10^12
}
private val walletApi = FeatureUtils.getFeature<WalletFeatureApi>(
ApplicationProvider.getApplicationContext<Context>(),
WalletFeatureApi::class.java
)
private val extrinsicBuilderFactory = runtimeApi.provideExtrinsicBuilderFactory()
private val rpcCalls = runtimeApi.rpcCalls()
/**
* LIVE TEST: Build and submit a real transfer on Pezkuwi mainnet
*/
@Test(timeout = 120000) // 2 minute timeout
fun testLiveTransfer5HEZ() = runTest {
Log.d("LiveTransferTest", "=== STARTING LIVE TRANSFER TEST ===")
Log.d("LiveTransferTest", "Sender: $SENDER_ADDRESS")
Log.d("LiveTransferTest", "Recipient: $RECIPIENT_ADDRESS")
Log.d("LiveTransferTest", "Amount: 5 HEZ")
// Request full sync for Pezkuwi chain specifically
Log.d("LiveTransferTest", "Requesting full sync for Pezkuwi chain...")
chainRegistry.enableFullSync(Chain.Geneses.PEZKUWI)
val chain = chainRegistry.getChain(Chain.Geneses.PEZKUWI)
Log.d("LiveTransferTest", "Chain: ${chain.name}")
// Create keypair from mnemonic
val keypair = createKeypairFromMnemonic(TEST_MNEMONIC)
Log.d("LiveTransferTest", "Keypair created, public key: ${keypair.publicKey.toHexString()}")
// Create signer
val signer = RealSigner(keypair, chain)
Log.d("LiveTransferTest", "Signer created")
// Get recipient account ID
val recipientAccountId = RECIPIENT_ADDRESS.toAccountId()
Log.d("LiveTransferTest", "Recipient AccountId: ${recipientAccountId.toHexString()}")
// Get current nonce using sender's SS58 address
val nonce = try {
rpcCalls.getNonce(chain.id, SENDER_ADDRESS)
} catch (e: Exception) {
Log.e("LiveTransferTest", "Failed to get nonce, using 0", e)
BigInteger.ZERO
}
Log.d("LiveTransferTest", "Current nonce: $nonce")
// Create extrinsic builder
val builder = extrinsicBuilderFactory.create(
chain = chain,
options = ExtrinsicBuilderFactory.Options(BatchMode.BATCH)
)
Log.d("LiveTransferTest", "ExtrinsicBuilder created")
// Use default MORTAL era (same as @pezkuwi/api)
Log.d("LiveTransferTest", "Using MORTAL era (default, same as @pezkuwi/api)")
// Add transfer call with KEEP_ALIVE mode (same as @pezkuwi/api uses)
builder.nativeTransfer(accountId = recipientAccountId, amount = TRANSFER_AMOUNT, mode = TransferMode.KEEP_ALIVE)
Log.d("LiveTransferTest", "Transfer call added")
// Set signer data for SUBMISSION (this is where TypeReference errors occur!)
try {
with(builder) {
signer.setSignerData(RealSigningContext(chain, nonce), SigningMode.SUBMISSION)
}
Log.d("LiveTransferTest", "Signer data set successfully")
} catch (e: Exception) {
Log.e("LiveTransferTest", "FAILED to set signer data!", e)
fail("Failed to set signer data: ${e.message}\nCause: ${e.cause?.message}\nStack: ${e.stackTraceToString()}")
return@runTest
}
// Build the extrinsic
val extrinsic = try {
builder.buildExtrinsic()
} catch (e: Exception) {
Log.e("LiveTransferTest", "FAILED to build extrinsic!", e)
fail("Failed to build extrinsic: ${e.message}\nCause: ${e.cause?.message}\nStack: ${e.stackTraceToString()}")
return@runTest
}
assertNotNull("Extrinsic should not be null", extrinsic)
Log.d("LiveTransferTest", "Extrinsic built: ${extrinsic.extrinsicHex}")
// Submit the extrinsic
Log.d("LiveTransferTest", "Submitting extrinsic to network...")
try {
val hash = rpcCalls.submitExtrinsic(chain.id, extrinsic)
Log.d("LiveTransferTest", "=== TRANSFER SUBMITTED SUCCESSFULLY ===")
Log.d("LiveTransferTest", "Transaction hash: $hash")
println("LIVE TRANSFER SUCCESS! TX Hash: $hash")
} catch (e: Exception) {
Log.e("LiveTransferTest", "FAILED to submit extrinsic!", e)
fail("Failed to submit extrinsic: ${e.message}")
}
}
/**
* Test to check type resolution in the runtime
*/
@Test(timeout = 120000)
fun testTypeResolution() = runTest {
Log.d("LiveTransferTest", "=== TESTING TYPE RESOLUTION ===")
// Request full sync for Pezkuwi chain
chainRegistry.enableFullSync(Chain.Geneses.PEZKUWI)
val chain = chainRegistry.getChain(Chain.Geneses.PEZKUWI)
val runtime = chainRegistry.getRuntime(chain.id)
// Check critical types for extrinsic encoding
val typesToCheck = listOf(
"Address",
"MultiAddress",
"GenericMultiAddress",
"ExtrinsicSignature",
"MultiSignature",
"pezsp_runtime::multiaddress::MultiAddress",
"pezsp_runtime::MultiSignature",
"pezsp_runtime.multiaddress.MultiAddress",
"pezsp_runtime.MultiSignature",
"GenericExtrinsic",
"Extrinsic"
)
val results = mutableListOf<String>()
for (typeName in typesToCheck) {
val type = runtime.typeRegistry[typeName]
val resolved = type?.let {
try {
// Try to get the actual type, not just alias
it.toString()
} catch (e: Exception) {
"ERROR: ${e.message}"
}
}
val status = if (type != null) "FOUND: $resolved" else "MISSING"
results.add(" $typeName: $status")
Log.d("LiveTransferTest", "$typeName: $status")
}
// Check if extrinsic signature type is defined in metadata
val extrinsicMeta = runtime.metadata.extrinsic
Log.d("LiveTransferTest", "Extrinsic version: ${extrinsicMeta.version}")
Log.d("LiveTransferTest", "Signed extensions: ${extrinsicMeta.signedExtensions.map { it.id }}")
// Log signed extension IDs
for (ext in extrinsicMeta.signedExtensions) {
Log.d("LiveTransferTest", "Extension: ${ext.id}")
}
// Just log the extension names - type access might be restricted
Log.d("LiveTransferTest", "Signed extensions count: ${extrinsicMeta.signedExtensions.size}")
// Log the extrinsic address type if available
Log.d("LiveTransferTest", "RuntimeFactory diagnostics: ${io.novafoundation.nova.runtime.multiNetwork.runtime.RuntimeFactory.lastDiagnostics}")
println("Type resolution results:\n${results.joinToString("\n")}")
}
/**
* Test fee calculation (doesn't submit, just builds for fee estimation)
*/
@Test(timeout = 120000)
fun testFeeCalculation() = runTest {
Log.d("LiveTransferTest", "=== TESTING FEE CALCULATION ===")
// Request full sync for Pezkuwi chain
chainRegistry.enableFullSync(Chain.Geneses.PEZKUWI)
val chain = chainRegistry.getChain(Chain.Geneses.PEZKUWI)
// First, log type registry state
val runtime = chainRegistry.getRuntime(chain.id)
Log.d("LiveTransferTest", "TypeRegistry has ExtrinsicSignature: ${runtime.typeRegistry["ExtrinsicSignature"] != null}")
Log.d("LiveTransferTest", "TypeRegistry has MultiSignature: ${runtime.typeRegistry["MultiSignature"] != null}")
Log.d("LiveTransferTest", "TypeRegistry has Address: ${runtime.typeRegistry["Address"] != null}")
Log.d("LiveTransferTest", "TypeRegistry has MultiAddress: ${runtime.typeRegistry["MultiAddress"] != null}")
val keypair = createKeypairFromMnemonic(TEST_MNEMONIC)
val signer = RealSigner(keypair, chain)
val recipientAccountId = RECIPIENT_ADDRESS.toAccountId()
val builder = extrinsicBuilderFactory.create(
chain = chain,
options = ExtrinsicBuilderFactory.Options(BatchMode.BATCH)
)
builder.nativeTransfer(accountId = recipientAccountId, amount = TRANSFER_AMOUNT)
// Set signer data for FEE mode
try {
with(builder) {
signer.setSignerData(RealSigningContext(chain, BigInteger.ZERO), SigningMode.FEE)
}
Log.d("LiveTransferTest", "Signer data set, building extrinsic...")
val extrinsic = builder.buildExtrinsic()
assertNotNull("Fee extrinsic should not be null", extrinsic)
Log.d("LiveTransferTest", "Extrinsic built, getting hex...")
// The error happens when accessing extrinsicHex
try {
val hex = extrinsic.extrinsicHex
Log.d("LiveTransferTest", "Fee extrinsic built: $hex")
println("Fee calculation test PASSED!")
} catch (e: Exception) {
Log.e("LiveTransferTest", "FAILED accessing extrinsicHex!", e)
fail("Failed to get extrinsic hex: ${e.message}\nCause: ${e.cause?.message}\nStack: ${e.stackTraceToString()}")
}
} catch (e: Exception) {
Log.e("LiveTransferTest", "Fee calculation FAILED!", e)
fail("Fee calculation failed: ${e.message}\nCause: ${e.cause?.message}")
}
}
// Helper to create keypair from mnemonic
private fun createKeypairFromMnemonic(mnemonic: String): Keypair {
val seedResult = SubstrateSeedFactory.deriveSeed32(mnemonic, password = null)
return SubstrateKeypairFactory.generate(EncryptionType.SR25519, seedResult.seed)
}
// Real signer using actual keypair with bizinikiwi context
private inner class RealSigner(
private val keypair: Keypair,
private val chain: Chain
) : NovaSigner, GeneralTransactionSigner {
val accountId: ByteArray = keypair.publicKey
// Generate proper 96-byte keypair using BizinikiwSr25519 native library
// This gives us the correct 64-byte secret key format for signing
private val bizinikiwKeypair: ByteArray by lazy {
val seedResult = SubstrateSeedFactory.deriveSeed32(TEST_MNEMONIC, password = null)
BizinikiwSr25519.keypairFromSeed(seedResult.seed)
}
// Extract 64-byte secret key (32-byte scalar + 32-byte nonce)
private val bizinikiwSecretKey: ByteArray by lazy {
BizinikiwSr25519.secretKeyFromKeypair(bizinikiwKeypair)
}
// Extract 32-byte public key
private val bizinikiwPublicKey: ByteArray by lazy {
BizinikiwSr25519.publicKeyFromKeypair(bizinikiwKeypair)
}
private val keyPairSigner = KeyPairSigner(
keypair,
MultiChainEncryption.Substrate(EncryptionType.SR25519)
)
override suspend fun callExecutionType(): CallExecutionType {
return CallExecutionType.IMMEDIATE
}
override val metaAccount: MetaAccount = DefaultMetaAccount(
id = 0,
globallyUniqueId = "test-wallet",
substrateAccountId = accountId,
substrateCryptoType = null,
substratePublicKey = keypair.publicKey,
ethereumAddress = null,
ethereumPublicKey = null,
isSelected = true,
name = "Test Wallet",
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 {
return keyPairSigner.signRaw(payload)
}
context(ExtrinsicBuilder)
override suspend fun setSignerDataForSubmission(context: SigningContext) {
val nonce = context.getNonce(AccountIdKey(accountId))
setNonce(nonce)
setVerifySignature(this@RealSigner, 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 {
// Get the SDK's signing payload (SCALE format - same as @pezkuwi/api)
val sdkPayloadBytes = inheritedImplication.signingPayload()
Log.d("LiveTransferTest", "=== SIGNING PAYLOAD (SDK - SCALE) ===")
Log.d("LiveTransferTest", "SDK Payload hex: ${sdkPayloadBytes.toHexString()}")
Log.d("LiveTransferTest", "SDK Payload length: ${sdkPayloadBytes.size} bytes")
// Debug: show first bytes to verify format
if (sdkPayloadBytes.size >= 42) {
val callData = sdkPayloadBytes.copyOfRange(0, 42)
val extensions = sdkPayloadBytes.copyOfRange(42, sdkPayloadBytes.size)
Log.d("LiveTransferTest", "Call data (42 bytes): ${callData.toHexString()}")
Log.d("LiveTransferTest", "Extensions (${extensions.size} bytes): ${extensions.toHexString()}")
}
// Use BizinikiwSr25519 native library with "bizinikiwi" signing context
Log.d("LiveTransferTest", "=== USING BIZINIKIWI CONTEXT ===")
Log.d("LiveTransferTest", "Bizinikiwi public key: ${bizinikiwPublicKey.toHexString()}")
Log.d("LiveTransferTest", "Bizinikiwi secret key size: ${bizinikiwSecretKey.size} bytes")
val signatureBytes = BizinikiwSr25519.sign(
publicKey = bizinikiwPublicKey,
secretKey = bizinikiwSecretKey,
message = sdkPayloadBytes
)
Log.d("LiveTransferTest", "=== SIGNATURE PRODUCED ===")
Log.d("LiveTransferTest", "Signature bytes: ${signatureBytes.toHexString()}")
Log.d("LiveTransferTest", "Signature length: ${signatureBytes.size} bytes")
// Verify the signature locally before sending
val verifyResult = BizinikiwSr25519.verify(signatureBytes, sdkPayloadBytes, bizinikiwPublicKey)
Log.d("LiveTransferTest", "Local verification: $verifyResult")
return SignatureWrapper.Sr25519(signatureBytes)
}
}
private class RealSigningContext(
override val chain: Chain,
private val nonceValue: BigInteger
) : SigningContext {
override suspend fun getNonce(accountId: AccountIdKey): Nonce {
return Nonce.ZERO + nonceValue
}
}
private fun ByteArray.toHexString(): String {
return joinToString("") { "%02x".format(it) }
}
}