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
This commit is contained in:
2026-02-12 05:19:41 +03:00
commit a294aa1a6b
7687 changed files with 441811 additions and 0 deletions
@@ -0,0 +1,50 @@
package io.novafoundation.nova
import android.content.Context
import androidx.test.core.app.ApplicationProvider
import androidx.test.ext.junit.runners.AndroidJUnit4
import io.novafoundation.nova.common.di.FeatureUtils
import io.novafoundation.nova.common.utils.withChildScope
import io.novafoundation.nova.runtime.di.RuntimeApi
import io.novafoundation.nova.runtime.di.RuntimeComponent
import io.novafoundation.nova.runtime.ext.Geneses
import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain
import io.novafoundation.nova.runtime.multiNetwork.connection.ChainConnection
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.runBlocking
import org.junit.Before
import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class)
open class BaseIntegrationTest {
protected val context: Context = ApplicationProvider.getApplicationContext()
protected val runtimeApi = FeatureUtils.getFeature<RuntimeComponent>(context, RuntimeApi::class.java)
val chainRegistry = runtimeApi.chainRegistry()
private val externalRequirementFlow = runtimeApi.externalRequirementFlow()
@Before
fun setup() = runBlocking {
externalRequirementFlow.emit(ChainConnection.ExternalRequirement.ALLOWED)
}
protected fun runTest(action: suspend CoroutineScope.() -> Unit) {
runBlocking {
withChildScope {
action()
}
}
}
protected suspend fun ChainRegistry.polkadot(): Chain {
return getChain(Chain.Geneses.POLKADOT)
}
protected suspend fun ChainRegistry.polkadotAssetHub(): Chain {
return getChain(Chain.Geneses.POLKADOT_ASSET_HUB)
}
}
@@ -0,0 +1,60 @@
package io.novafoundation.nova
import android.content.Context
import android.util.Log
import androidx.test.core.app.ApplicationProvider
import androidx.test.ext.junit.runners.AndroidJUnit4
import io.novafoundation.nova.common.data.network.runtime.binding.bindEventRecords
import io.novafoundation.nova.common.di.FeatureUtils
import io.novafoundation.nova.common.utils.LOG_TAG
import io.novafoundation.nova.common.utils.system
import io.novafoundation.nova.runtime.di.RuntimeApi
import io.novafoundation.nova.runtime.di.RuntimeComponent
import io.novafoundation.nova.runtime.multiNetwork.connection.ChainConnection
import io.novasama.substrate_sdk_android.runtime.metadata.storage
import io.novasama.substrate_sdk_android.runtime.metadata.storageKey
import kotlinx.coroutines.runBlocking
import org.junit.Test
import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class)
class BlockParsingIntegrationTest {
private val chainGenesis = "f1cf9022c7ebb34b162d5b5e34e705a5a740b2d0ecc1009fb89023e62a488108" // Shiden
private val runtimeApi = FeatureUtils.getFeature<RuntimeComponent>(
ApplicationProvider.getApplicationContext<Context>(),
RuntimeApi::class.java
)
private val chainRegistry = runtimeApi.chainRegistry()
private val externalRequirementFlow = runtimeApi.externalRequirementFlow()
private val rpcCalls = runtimeApi.rpcCalls()
private val remoteStorage = runtimeApi.remoteStorageSource()
@Test
fun testBlockParsing() = runBlocking {
externalRequirementFlow.emit(ChainConnection.ExternalRequirement.ALLOWED)
val chain = chainRegistry.getChain(chainGenesis)
val block = rpcCalls.getBlock(chain.id)
val logTag = this@BlockParsingIntegrationTest.LOG_TAG
Log.d(logTag, block.block.header.number.toString())
val events = remoteStorage.query(
chainId = chain.id,
keyBuilder = { it.metadata.system().storage("Events").storageKey() },
binding = { scale, runtime ->
Log.d(logTag, scale!!)
bindEventRecords(scale)
}
)
// val eventsRaw = "0x0800000000000000000000000000000002000000010000000000585f8f0900000000020000"
// val type = bindEventRecords(eventsRaw, chainRegistry.getRuntime(chain.id))
}
}
@@ -0,0 +1,129 @@
package io.novafoundation.nova
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.google.gson.Gson
import androidx.test.platform.app.InstrumentationRegistry
import dagger.Component
import io.novafoundation.nova.common.data.network.NetworkApiCreator
import io.novafoundation.nova.common.di.CommonApi
import io.novafoundation.nova.common.di.FeatureContainer
import io.novafoundation.nova.core_db.model.chain.ChainLocal
import io.novafoundation.nova.core_db.model.chain.NodeSelectionPreferencesLocal
import io.novafoundation.nova.runtime.di.RuntimeApi
import io.novafoundation.nova.runtime.multiNetwork.chain.mappers.RemoteToDomainChainMapperFacade
import io.novafoundation.nova.runtime.multiNetwork.chain.mappers.mapChainAssetToLocal
import io.novafoundation.nova.runtime.multiNetwork.chain.mappers.mapChainExplorerToLocal
import io.novafoundation.nova.runtime.multiNetwork.chain.mappers.mapChainExternalApiToLocal
import io.novafoundation.nova.runtime.multiNetwork.chain.mappers.mapChainLocalToChain
import io.novafoundation.nova.runtime.multiNetwork.chain.mappers.mapChainNodeToLocal
import io.novafoundation.nova.runtime.multiNetwork.chain.mappers.mapChainToLocal
import io.novafoundation.nova.runtime.multiNetwork.chain.mappers.mapExternalApisToLocal
import io.novafoundation.nova.runtime.multiNetwork.chain.mappers.mapNodeSelectionPreferencesToLocal
import io.novafoundation.nova.runtime.multiNetwork.chain.mappers.mapRemoteAssetToLocal
import io.novafoundation.nova.runtime.multiNetwork.chain.mappers.mapRemoteChainToLocal
import io.novafoundation.nova.runtime.multiNetwork.chain.mappers.mapRemoteExplorersToLocal
import io.novafoundation.nova.runtime.multiNetwork.chain.mappers.mapRemoteNodesToLocal
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain
import io.novafoundation.nova.runtime.multiNetwork.chain.remote.ChainFetcher
import io.novafoundation.nova.runtime.multiNetwork.chain.remote.model.ChainRemote
import io.novafoundation.nova.test_shared.assertAllItemsEquals
import javax.inject.Inject
import kotlinx.coroutines.runBlocking
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
@Component(
dependencies = [
CommonApi::class,
RuntimeApi::class
]
)
interface MappingTestAppComponent {
fun inject(test: ChainMappingIntegrationTest)
}
@RunWith(AndroidJUnit4::class)
class ChainMappingIntegrationTest {
private val context = InstrumentationRegistry.getInstrumentation().targetContext.applicationContext
private val featureContainer = context as FeatureContainer
@Inject
lateinit var networkApiCreator: NetworkApiCreator
@Inject
lateinit var remoteToDomainChainMapperFacade: RemoteToDomainChainMapperFacade
lateinit var chainFetcher: ChainFetcher
private val gson = Gson()
@Before
fun prepare() {
val component = DaggerMappingTestAppComponent.builder()
.commonApi(featureContainer.commonApi())
.runtimeApi(featureContainer.getFeature(RuntimeApi::class.java))
.build()
component.inject(this)
chainFetcher = networkApiCreator.create(ChainFetcher::class.java)
}
@Test
fun testChainMappingIsMatch() {
runBlocking {
val chainsRemote = chainFetcher.getChains()
val remoteToDomain = chainsRemote.map { mapRemoteToDomain(it) }
val remoteToLocalToDomain = chainsRemote.map { mapRemoteToLocalToDomain(it) }
val domainToLocalToDomain = remoteToDomain.map { mapDomainToLocalToDomain(it) }
assertAllItemsEquals(listOf(remoteToDomain, remoteToLocalToDomain, domainToLocalToDomain))
}
}
private fun mapRemoteToLocalToDomain(chainRemote: ChainRemote): Chain {
val chainLocal = mapRemoteChainToLocal(chainRemote, null, ChainLocal.Source.DEFAULT, gson)
val assetsLocal = chainRemote.assets.map { mapRemoteAssetToLocal(chainRemote, it, gson, isEnabled = true) }
val nodesLocal = mapRemoteNodesToLocal(chainRemote)
val explorersLocal = mapRemoteExplorersToLocal(chainRemote)
val externalApisLocal = mapExternalApisToLocal(chainRemote)
return mapChainLocalToChain(
chainLocal = chainLocal,
nodesLocal = nodesLocal,
nodeSelectionPreferences = NodeSelectionPreferencesLocal(chainLocal.id, autoBalanceEnabled = true, selectedNodeUrl = null),
assetsLocal = assetsLocal,
explorersLocal = explorersLocal,
externalApisLocal = externalApisLocal,
gson = gson
)
}
private fun mapRemoteToDomain(chainRemote: ChainRemote): Chain {
return remoteToDomainChainMapperFacade.mapRemoteChainToDomain(chainRemote, Chain.Source.DEFAULT)
}
private fun mapDomainToLocalToDomain(chain: Chain): Chain {
val chainLocal = mapChainToLocal(chain, gson)
val nodesLocal = chain.nodes.nodes.map { mapChainNodeToLocal(it) }
val nodeSelectionPreferencesLocal = mapNodeSelectionPreferencesToLocal(chain)
val assetsLocal = chain.assets.map { mapChainAssetToLocal(it, gson) }
val explorersLocal = chain.explorers.map { mapChainExplorerToLocal(it) }
val externalApisLocal = chain.externalApis.map { mapChainExternalApiToLocal(gson, chain.id, it) }
return mapChainLocalToChain(
chainLocal = chainLocal,
nodesLocal = nodesLocal,
nodeSelectionPreferences = nodeSelectionPreferencesLocal,
assetsLocal = assetsLocal,
explorersLocal = explorersLocal,
externalApisLocal = externalApisLocal,
gson = gson
)
}
}
@@ -0,0 +1,61 @@
package io.novafoundation.nova
import androidx.room.Room
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.platform.app.InstrumentationRegistry
import com.google.gson.Gson
import dagger.Component
import io.novafoundation.nova.common.data.network.NetworkApiCreator
import io.novafoundation.nova.common.di.CommonApi
import io.novafoundation.nova.common.di.FeatureContainer
import io.novafoundation.nova.core_db.AppDatabase
import io.novafoundation.nova.runtime.multiNetwork.chain.ChainSyncService
import io.novafoundation.nova.runtime.multiNetwork.chain.remote.ChainFetcher
import kotlinx.coroutines.runBlocking
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import javax.inject.Inject
@Component(
dependencies = [
CommonApi::class,
]
)
interface TestAppComponent {
fun inject(test: ChainSyncServiceIntegrationTest)
}
@RunWith(AndroidJUnit4::class)
class ChainSyncServiceIntegrationTest {
private val context = InstrumentationRegistry.getInstrumentation().targetContext.applicationContext
private val featureContainer = context as FeatureContainer
@Inject
lateinit var networkApiCreator: NetworkApiCreator
lateinit var chainSyncService: ChainSyncService
@Before
fun setup() {
val component = DaggerTestAppComponent.builder()
.commonApi(featureContainer.commonApi())
.build()
component.inject(this)
val chainDao = Room.inMemoryDatabaseBuilder(context, AppDatabase::class.java)
.build()
.chainDao()
chainSyncService = ChainSyncService(chainDao, networkApiCreator.create(ChainFetcher::class.java), Gson())
}
@Test
fun shouldFetchAndStoreRealChains() = runBlocking {
chainSyncService.syncUp()
}
}
@@ -0,0 +1,98 @@
package io.novafoundation.nova
import android.content.Context
import androidx.test.core.app.ApplicationProvider
import io.novafoundation.nova.common.address.intoKey
import io.novafoundation.nova.common.di.FeatureUtils
import io.novafoundation.nova.common.utils.emptySubstrateAccountId
import io.novafoundation.nova.feature_account_api.data.fee.FeePaymentCurrency
import io.novafoundation.nova.feature_account_api.domain.model.toDefaultSubstrateAddress
import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.tranfers.AssetTransferBase
import io.novafoundation.nova.feature_wallet_api.data.repository.getXcmChain
import io.novafoundation.nova.feature_wallet_api.di.WalletFeatureApi
import io.novafoundation.nova.feature_wallet_api.domain.model.xcm.legacy.transferConfiguration
import io.novafoundation.nova.feature_wallet_api.domain.model.xcm.transferConfiguration
import io.novafoundation.nova.runtime.ext.addressOf
import io.novafoundation.nova.runtime.ext.emptyAccountId
import io.novafoundation.nova.runtime.ext.normalizeTokenSymbol
import io.novafoundation.nova.runtime.multiNetwork.findChain
import kotlinx.coroutines.runBlocking
import org.junit.Test
import java.math.BigInteger
class CrossChainTransfersIntegrationTest : BaseIntegrationTest() {
private val walletApi = FeatureUtils.getFeature<WalletFeatureApi>(
ApplicationProvider.getApplicationContext<Context>(),
WalletFeatureApi::class.java
)
private val chainTransfersRepository = walletApi.crossChainTransfersRepository
private val crossChainWeigher = walletApi.crossChainWeigher
private val parachainInfoRepository = runtimeApi.parachainInfoRepository
@Test
fun testParachainToParachain() = performFeeTest(
from = "Moonriver",
what = "xcKAR",
to = "Karura"
)
@Test
fun testRelaychainToParachain() = performFeeTest(
from = "Kusama",
what = "KSM",
to = "Moonriver"
)
@Test
fun testParachainToRelaychain() = performFeeTest(
from = "Moonriver",
what = "xcKSM",
to = "Kusama"
)
@Test
fun testParachainToParachainNonReserve() = performFeeTest(
from = "Karura",
what = "BNC",
to = "Moonriver"
)
private fun performFeeTest(
from: String,
to: String,
what: String
) {
runBlocking {
val originChain = chainRegistry.findChain { it.name == from }!!
val asssetInOrigin = originChain.assets.first { it.symbol.value == what }
val destinationChain = chainRegistry.findChain { it.name == to }!!
val asssetInDestination = destinationChain.assets.first { normalizeTokenSymbol(it.symbol.value) == normalizeTokenSymbol(what) }
val crossChainConfig = chainTransfersRepository.getConfiguration()
val crossChainTransfer = crossChainConfig.transferConfiguration(
originChain = parachainInfoRepository.getXcmChain(originChain),
originAsset = asssetInOrigin,
destinationChain = parachainInfoRepository.getXcmChain(destinationChain),
)!!
val transfer = AssetTransferBase(
recipient = originChain.addressOf(originChain.emptyAccountId()),
originChain = originChain,
originChainAsset = asssetInOrigin,
destinationChain = destinationChain,
destinationChainAsset = asssetInDestination,
feePaymentCurrency = FeePaymentCurrency.Native,
amountPlanks = BigInteger.ZERO
)
val crossChainFeeResult = runCatching { crossChainWeigher.estimateFee(transfer, crossChainTransfer) }
check(crossChainFeeResult.isSuccess)
}
}
}
@@ -0,0 +1,100 @@
package io.novafoundation.nova
import android.content.Context
import androidx.test.core.app.ApplicationProvider
import io.novafoundation.nova.common.address.intoKey
import io.novafoundation.nova.common.data.network.runtime.binding.toResult
import io.novafoundation.nova.common.di.FeatureUtils
import io.novafoundation.nova.common.utils.composeCall
import io.novafoundation.nova.common.utils.xcmPalletName
import io.novafoundation.nova.feature_wallet_api.domain.model.planksFromAmount
import io.novafoundation.nova.feature_xcm_api.asset.MultiAsset
import io.novafoundation.nova.feature_xcm_api.asset.MultiAssets
import io.novafoundation.nova.feature_xcm_api.di.XcmFeatureApi
import io.novafoundation.nova.feature_xcm_api.runtimeApi.dryRun.model.OriginCaller
import io.novafoundation.nova.feature_xcm_api.runtimeApi.dryRun.model.getByLocation
import io.novafoundation.nova.feature_xcm_api.multiLocation.Junctions
import io.novafoundation.nova.feature_xcm_api.multiLocation.MultiLocation
import io.novafoundation.nova.feature_xcm_api.multiLocation.MultiLocation.Junction.ParachainId
import io.novafoundation.nova.feature_xcm_api.multiLocation.asLocation
import io.novafoundation.nova.feature_xcm_api.multiLocation.toMultiLocation
import io.novafoundation.nova.feature_xcm_api.versions.XcmVersion
import io.novafoundation.nova.feature_xcm_api.versions.toEncodableInstance
import io.novafoundation.nova.feature_xcm_api.versions.versionedXcm
import io.novafoundation.nova.feature_xcm_api.weight.WeightLimit
import io.novafoundation.nova.runtime.ext.utilityAsset
import io.novafoundation.nova.runtime.multiNetwork.getRuntime
import io.novasama.substrate_sdk_android.ss58.SS58Encoder.toAccountId
import org.junit.Test
import java.math.BigDecimal
import java.math.BigInteger
class DryRunIntegrationTest : BaseIntegrationTest() {
private val xcmApi = FeatureUtils.getFeature<XcmFeatureApi>(
ApplicationProvider.getApplicationContext<Context>(),
XcmFeatureApi::class.java
)
private val dryRunApi = xcmApi.dryRunApi
@Test
fun testDryRunXcmTeleport() = runTest {
val polkadot = chainRegistry.polkadot()
val polkadotAssetHub = chainRegistry.polkadotAssetHub()
val polkadotRuntime = chainRegistry.getRuntime(polkadot.id)
val polkadotLocation = MultiLocation.Interior.Here.asLocation()
val polkadotAssetHubLocation = Junctions(ParachainId(1000)).asLocation()
val dotLocation = polkadotLocation.toRelative()
val amount = polkadot.utilityAsset.planksFromAmount(BigDecimal.ONE)
val assets = MultiAsset.from(dotLocation, amount)
val origin = "16WWmr2Xqgy5fna35GsNHXMU7vDBM12gzHCFGibQjSmKpAN".toAccountId().intoKey()
val beneficiary = origin.toMultiLocation()
val xcmVersion = XcmVersion.V4
val pahVersionedLocation = polkadotAssetHubLocation.toRelative().versionedXcm(xcmVersion)
// Compose limited_teleport_assets call to execute on Polkadot
val call = polkadotRuntime.composeCall(
moduleName = polkadotRuntime.metadata.xcmPalletName(),
callName = "limited_teleport_assets",
arguments = mapOf(
"dest" to pahVersionedLocation.toEncodableInstance(),
"beneficiary" to beneficiary.versionedXcm(xcmVersion).toEncodableInstance(),
"assets" to MultiAssets(assets).versionedXcm(xcmVersion).toEncodableInstance(),
"fee_asset_item" to BigInteger.ZERO,
"weight_limit" to WeightLimit.Unlimited.toEncodableInstance()
)
)
// Dry run call execution
val dryRunEffects = dryRunApi.dryRunCall(
originCaller = OriginCaller.System.Signed(origin),
call = call,
chainId = polkadot.id,
xcmResultsVersion = XcmVersion.V4
)
.getOrThrow()
.toResult().getOrThrow()
// Find xcm forwarded to Polkadot Asset Hub
val forwardedXcm = dryRunEffects.forwardedXcms.getByLocation(pahVersionedLocation).first()
println(forwardedXcm)
// Dry run execution of forwarded message on Polkadot Asset Hub
val xcmDryRunEffects = dryRunApi.dryRunXcm(
xcm = forwardedXcm,
originLocation = polkadotLocation.fromPointOfViewOf(polkadotAssetHubLocation).versionedXcm(xcmVersion),
chainId = polkadotAssetHub.id
)
.getOrThrow()
.toResult().getOrThrow()
println(xcmDryRunEffects.emittedEvents)
}
}
@@ -0,0 +1,52 @@
package io.novafoundation.nova
import android.util.Log
import io.novafoundation.nova.common.utils.average
import io.novafoundation.nova.common.utils.divideToDecimal
import io.novafoundation.nova.runtime.ethereum.gas.LegacyGasPriceProvider
import io.novafoundation.nova.runtime.ethereum.gas.MaxPriorityFeeGasProvider
import io.novafoundation.nova.runtime.ext.Ids
import io.novafoundation.nova.runtime.multiNetwork.getCallEthereumApiOrThrow
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.take
import org.junit.Test
import java.math.BigInteger
class GasPriceProviderIntegrationTest : BaseIntegrationTest() {
@Test
fun compareLegacyAndImprovedGasPriceEstimations() = runTest {
val api = chainRegistry.getCallEthereumApiOrThrow(Chain.Ids.MOONBEAM)
val legacy = LegacyGasPriceProvider(api)
val improved = MaxPriorityFeeGasProvider(api)
val legacyStats = mutableSetOf<BigInteger>()
val improvedStats = mutableSetOf<BigInteger>()
api.newHeadsFlow().map {
legacyStats.add(legacy.getGasPrice())
improvedStats.add(improved.getGasPrice())
}
.take(1000)
.collect()
legacyStats.printStats("Legacy")
improvedStats.printStats("Improved")
}
private fun Set<BigInteger>.printStats(name: String) {
val min = min()
val max = max()
Log.d("GasPriceProviderIntegrationTest", """
Stats for $name source
Min: $min
Max: $max
Avg: ${average()}
Max/Min ratio: ${max.divideToDecimal(min)}
""")
}
}
@@ -0,0 +1,247 @@
package io.novafoundation.nova
import android.util.Log
import io.novafoundation.nova.common.di.FeatureUtils
import io.novafoundation.nova.common.utils.LOG_TAG
import io.novafoundation.nova.common.utils.firstLoaded
import io.novafoundation.nova.common.utils.inBackground
import io.novafoundation.nova.feature_account_api.di.AccountFeatureApi
import io.novafoundation.nova.feature_governance_api.data.network.blockhain.model.ReferendumId
import io.novafoundation.nova.feature_governance_api.data.network.blockhain.model.VoteType
import io.novafoundation.nova.feature_governance_api.data.source.SupportedGovernanceOption
import io.novafoundation.nova.feature_governance_api.di.GovernanceFeatureApi
import io.novafoundation.nova.feature_governance_api.domain.referendum.filters.ReferendumType
import io.novafoundation.nova.feature_governance_api.domain.referendum.filters.ReferendumTypeFilter
import io.novafoundation.nova.feature_governance_impl.data.RealGovernanceAdditionalState
import io.novafoundation.nova.runtime.ext.externalApi
import io.novafoundation.nova.runtime.ext.utilityAsset
import io.novafoundation.nova.runtime.multiNetwork.ChainWithAsset
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain
import io.novafoundation.nova.runtime.multiNetwork.chain.model.FullChainAssetId
import io.novasama.substrate_sdk_android.ss58.SS58Encoder.toAccountId
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.mapNotNull
import org.junit.Test
import java.math.BigInteger
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
class GovernanceIntegrationTest : BaseIntegrationTest() {
private val accountApi = FeatureUtils.getFeature<AccountFeatureApi>(context, AccountFeatureApi::class.java)
private val governanceApi = FeatureUtils.getFeature<GovernanceFeatureApi>(context, GovernanceFeatureApi::class.java)
@Test
fun shouldRetrieveOnChainReferenda() = runTest {
val chain = chain()
val selectedGovernance = supportedGovernanceOption(chain, Chain.Governance.V1)
val onChainReferendaRepository = source(selectedGovernance).referenda
val referenda = onChainReferendaRepository.getAllOnChainReferenda(chain.id)
Log.d(this@GovernanceIntegrationTest.LOG_TAG, referenda.toString())
}
@Test
fun shouldRetrieveConvictionVotes() = runTest {
val chain = chain()
val selectedGovernance = supportedGovernanceOption(chain, Chain.Governance.V1)
val convictionVotingRepository = source(selectedGovernance).convictionVoting
val accountId = "5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY".toAccountId()
val votes = convictionVotingRepository.votingFor(accountId, chain.id)
Log.d(this@GovernanceIntegrationTest.LOG_TAG, votes.toString())
}
@Test
fun shouldRetrieveTrackLocks() = runTest {
val chain = chain()
val selectedGovernance = supportedGovernanceOption(chain, Chain.Governance.V1)
val convictionVotingRepository = source(selectedGovernance).convictionVoting
val accountId = "5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY".toAccountId()
val fullChainAssetId = FullChainAssetId(chain.id, chain.utilityAsset.id)
val trackLocks = convictionVotingRepository.trackLocksFlow(accountId, fullChainAssetId).first()
Log.d(this@GovernanceIntegrationTest.LOG_TAG, trackLocks.toString())
}
@Test
fun shouldRetrieveReferendaTracks() = runTest {
val chain = chain()
val selectedGovernance = supportedGovernanceOption(chain, Chain.Governance.V1)
val onChainReferendaRepository = source(selectedGovernance).referenda
val tracks = onChainReferendaRepository.getTracks(chain.id)
Log.d(this@GovernanceIntegrationTest.LOG_TAG, tracks.toString())
}
@Test
fun shouldRetrieveDomainReferendaPreviews() = runTest {
val accountRepository = accountApi.provideAccountRepository()
val referendaListInteractor = governanceApi.referendaListInteractor
val updateSystem = governanceApi.governanceUpdateSystem
updateSystem.start()
.inBackground()
.launchIn(this)
val metaAccount = accountRepository.getSelectedMetaAccount()
val accountId = "5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY".toAccountId()
val chain = chain()
val selectedGovernance = supportedGovernanceOption(chain, Chain.Governance.V1)
val filterFlow: Flow<ReferendumTypeFilter> = flow {
val referendaFilter = ReferendumTypeFilter(ReferendumType.ALL)
emit(referendaFilter)
}
val referendaByGroup = referendaListInteractor.referendaListStateFlow(metaAccount, accountId, selectedGovernance, this, filterFlow).firstLoaded()
val referenda = referendaByGroup.groupedReferenda.values.flatten()
Log.d(this@GovernanceIntegrationTest.LOG_TAG, referenda.joinToString("\n"))
}
@Test
fun shouldRetrieveDomainReferendumDetails() = runTest {
val referendumDetailsInteractor = governanceApi.referendumDetailsInteractor
val updateSystem = governanceApi.governanceUpdateSystem
updateSystem.start()
.inBackground()
.launchIn(this)
val accountId = "5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY".toAccountId()
val referendumId = ReferendumId(BigInteger.ZERO)
val chain = chain()
val selectedGovernance = supportedGovernanceOption(chain, Chain.Governance.V1)
val referendumDetails = referendumDetailsInteractor.referendumDetailsFlow(referendumId, selectedGovernance, accountId, CoroutineScope(Dispatchers.Main))
.first()
Log.d(this@GovernanceIntegrationTest.LOG_TAG, referendumDetails.toString())
val callDetails = referendumDetailsInteractor.detailsFor(
preImage = referendumDetails?.onChainMetadata!!.preImage!!,
chain = chain
)
Log.d(this@GovernanceIntegrationTest.LOG_TAG, callDetails.toString())
}
@Test
fun shouldRetrieveVoters() = runTest {
val interactor = governanceApi.referendumVotersInteractor
val referendumId = ReferendumId(BigInteger.ZERO)
val referendumVoters = interactor.votersFlow(referendumId, VoteType.AYE)
.first()
Log.d(this@GovernanceIntegrationTest.LOG_TAG, referendumVoters.toString())
}
@Test
fun shouldFetchDelegatesList() = runTest {
val interactor = governanceApi.delegateListInteractor
val updateSystem = governanceApi.governanceUpdateSystem
updateSystem.start()
.inBackground()
.launchIn(this)
val chain = kusama()
val delegates = interactor.getDelegates(
governanceOption = supportedGovernanceOption(chain, Chain.Governance.V2),
scope = this
)
Log.d(this@GovernanceIntegrationTest.LOG_TAG, delegates.toString())
}
@Test
fun shouldFetchDelegateDetails() = runTest {
val delegateAccountId = "DCZyhphXsRLcW84G9WmWEXtAA8DKGtVGSFZLJYty8Ajjyfa".toAccountId() // ChaosDAO
val interactor = governanceApi.delegateDetailsInteractor
val updateSystem = governanceApi.governanceUpdateSystem
updateSystem.start()
.inBackground()
.launchIn(this)
val delegate = interactor.delegateDetailsFlow(delegateAccountId)
Log.d(this@GovernanceIntegrationTest.LOG_TAG, delegate.toString())
}
@Test
fun shouldFetchChooseTrackData() = runTest {
val interactor = governanceApi.newDelegationChooseTrackInteractor
val updateSystem = governanceApi.governanceUpdateSystem
updateSystem.start()
.inBackground()
.launchIn(this)
val trackData = interactor.observeNewDelegationTrackData().first()
Log.d(
this@GovernanceIntegrationTest.LOG_TAG,
"""
Available: ${trackData.trackPartition.available.size}
Already voted: ${trackData.trackPartition.alreadyVoted.size}
Already delegated: ${trackData.trackPartition.alreadyDelegated.size}
Presets: ${trackData.presets}
""".trimIndent()
)
}
@Test
fun shouldFetchDelegators() = runTest {
val delegateAddress = "DCZyhphXsRLcW84G9WmWEXtAA8DKGtVGSFZLJYty8Ajjyfa" // ChaosDAO
val interactor = governanceApi.delegateDelegatorsInteractor
val updateSystem = governanceApi.governanceUpdateSystem
updateSystem.start()
.inBackground()
.launchIn(this)
val delegators = interactor.delegatorsFlow(delegateAddress.toAccountId()).first()
Log.d(this@GovernanceIntegrationTest.LOG_TAG, delegators.toString())
}
private suspend fun source(supportedGovernance: SupportedGovernanceOption) = governanceApi.governanceSourceRegistry.sourceFor(supportedGovernance)
private fun supportedGovernanceOption(chain: Chain, governance: Chain.Governance) =
SupportedGovernanceOption(
ChainWithAsset(chain, chain.utilityAsset),
RealGovernanceAdditionalState(
governance,
false
)
)
private suspend fun chain(): Chain = chainRegistry.currentChains.map { chains ->
chains.find { it.governance.isNotEmpty() }
}
.filterNotNull()
.first()
private suspend fun kusama(): Chain = chainRegistry.currentChains.mapNotNull { chains ->
chains.find { it.externalApi<Chain.ExternalApi.GovernanceDelegations>() != null }
}.first()
}
File diff suppressed because one or more lines are too long
@@ -0,0 +1,60 @@
package io.novafoundation.nova
import android.content.Context
import androidx.test.core.app.ApplicationProvider
import io.novafoundation.nova.common.di.FeatureUtils
import io.novafoundation.nova.feature_account_api.di.AccountFeatureApi
import io.novafoundation.nova.feature_nft_api.NftFeatureApi
import io.novafoundation.nova.feature_nft_api.data.model.isFullySynced
import io.novafoundation.nova.runtime.di.RuntimeApi
import io.novafoundation.nova.runtime.di.RuntimeComponent
import io.novafoundation.nova.runtime.multiNetwork.connection.ChainConnection
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onCompletion
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.takeWhile
import kotlinx.coroutines.runBlocking
import org.junit.Test
class NftFullSyncIntegrationTest {
private val nftApi = FeatureUtils.getFeature<NftFeatureApi>(
ApplicationProvider.getApplicationContext<Context>(),
NftFeatureApi::class.java
)
private val accountApi = FeatureUtils.getFeature<AccountFeatureApi>(
ApplicationProvider.getApplicationContext<Context>(),
AccountFeatureApi::class.java
)
private val runtimeApi = FeatureUtils.getFeature<RuntimeComponent>(
ApplicationProvider.getApplicationContext<Context>(),
RuntimeApi::class.java
)
private val externalRequirementFlow = runtimeApi.externalRequirementFlow()
@Test
fun testFullSyncIntegration(): Unit = runBlocking {
externalRequirementFlow.emit(ChainConnection.ExternalRequirement.ALLOWED)
val metaAccount = accountApi.accountUseCase().getSelectedMetaAccount()
val nftRepository = nftApi.nftRepository
nftRepository.initialNftSync(metaAccount, true)
nftRepository.allNftFlow(metaAccount)
.map { nfts -> nfts.filter { !it.isFullySynced } }
.takeWhile { it.isNotEmpty() }
.onEach { unsyncedNfts ->
unsyncedNfts.forEach { nftRepository.fullNftSync(it) }
}
.onCompletion {
print("Full sync done")
}
.launchIn(this)
}
}
@@ -0,0 +1,181 @@
package io.novafoundation.nova
import android.content.Context
import android.util.Log
import androidx.test.core.app.ApplicationProvider
import io.novafoundation.nova.common.data.network.runtime.binding.bindAccountId
import io.novafoundation.nova.common.data.network.runtime.binding.bindBoolean
import io.novafoundation.nova.common.data.network.runtime.binding.bindString
import io.novafoundation.nova.common.data.network.runtime.binding.cast
import io.novafoundation.nova.common.data.network.runtime.binding.getTyped
import io.novafoundation.nova.common.di.FeatureUtils
import io.novafoundation.nova.common.utils.LOG_TAG
import io.novafoundation.nova.common.utils.uniques
import io.novafoundation.nova.runtime.di.RuntimeApi
import io.novafoundation.nova.runtime.di.RuntimeComponent
import io.novafoundation.nova.runtime.ext.addressOf
import io.novafoundation.nova.runtime.multiNetwork.connection.ChainConnection
import io.novafoundation.nova.runtime.storage.source.multi.MultiQueryBuilder
import io.novafoundation.nova.runtime.storage.source.query.multi
import io.novasama.substrate_sdk_android.runtime.AccountId
import io.novasama.substrate_sdk_android.runtime.definitions.types.composite.Struct
import io.novasama.substrate_sdk_android.runtime.metadata.storage
import io.novasama.substrate_sdk_android.ss58.SS58Encoder.toAccountId
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.runBlocking
import org.junit.Test
import java.math.BigInteger
data class UniquesClass(
val id: BigInteger,
val metadata: Metadata?,
val details: Details
) {
data class Metadata(
val deposit: BigInteger,
val data: String,
)
data class Details(
val instances: BigInteger,
val frozen: Boolean
)
}
data class UniquesInstance(
val collection: UniquesClass,
val id: BigInteger,
val metadata: Metadata?,
val details: Details
) {
data class Metadata(
val data: String,
)
data class Details(
val owner: String,
val frozen: Boolean,
)
}
class NftUniquesIntegrationTest {
private val chainGenesis = "48239ef607d7928874027a43a67689209727dfb3d3dc5e5b03a39bdc2eda771a"
private val runtimeApi = FeatureUtils.getFeature<RuntimeComponent>(
ApplicationProvider.getApplicationContext<Context>(),
RuntimeApi::class.java
)
private val chainRegistry = runtimeApi.chainRegistry()
private val externalRequirementFlow = runtimeApi.externalRequirementFlow()
private val storageRemoteSource = runtimeApi.remoteStorageSource()
@Test
fun testUniquesIntegration(): Unit = runBlocking {
chainRegistry.currentChains.first() // wait till chains are ready
externalRequirementFlow.emit(ChainConnection.ExternalRequirement.ALLOWED)
val chain = chainRegistry.getChain(chainGenesis)
val accountId = "JGKSibhyZgzY7jEe5a9gdybDEbqNNRSxYyJJmeeycbCbQ5v".toAccountId()
val instances = storageRemoteSource.query(chainGenesis) {
val classesWithInstances = runtime.metadata.uniques().storage("Account").keys(accountId)
.map { (_: AccountId, collection: BigInteger, instance: BigInteger) ->
listOf(collection, instance)
}
val classesIds = classesWithInstances.map { (collection, _) -> collection }.distinct()
val classDetailsDescriptor: MultiQueryBuilder.Descriptor<BigInteger, UniquesClass.Details>
val classMetadatasDescriptor: MultiQueryBuilder.Descriptor<BigInteger, UniquesClass.Metadata?>
val instancesDetailsDescriptor: MultiQueryBuilder.Descriptor<Pair<BigInteger, BigInteger>, UniquesInstance.Details>
val instancesMetadataDescriptor: MultiQueryBuilder.Descriptor<Pair<BigInteger, BigInteger>, UniquesInstance.Metadata?>
val multiQueryResults = multi {
classDetailsDescriptor = runtime.metadata.uniques().storage("Class").querySingleArgKeys(
keysArgs = classesIds,
keyExtractor = { it.component1<BigInteger>() },
binding = { parsedValue ->
val classDetailsStruct = parsedValue.cast<Struct.Instance>()
UniquesClass.Details(
instances = classDetailsStruct.getTyped("instances"),
frozen = classDetailsStruct.getTyped("isFrozen")
)
}
)
classMetadatasDescriptor = runtime.metadata.uniques().storage("ClassMetadataOf").querySingleArgKeys(
keysArgs = classesIds,
keyExtractor = { it.component1<BigInteger>() },
binding = { parsedValue ->
parsedValue?.cast<Struct.Instance>()?.let { classMetadataStruct ->
UniquesClass.Metadata(
deposit = classMetadataStruct.getTyped("deposit"),
data = bindString(classMetadataStruct["data"])
)
}
}
)
instancesDetailsDescriptor = runtime.metadata.uniques().storage("Asset").queryKeys(
keysArgs = classesWithInstances,
keyExtractor = { it.component1<BigInteger>() to it.component2<BigInteger>() },
binding = { parsedValue ->
val instanceDetailsStruct = parsedValue.cast<Struct.Instance>()
UniquesInstance.Details(
owner = chain.addressOf(bindAccountId(instanceDetailsStruct["owner"])),
frozen = bindBoolean(instanceDetailsStruct["isFrozen"])
)
}
)
instancesMetadataDescriptor = runtime.metadata.uniques().storage("InstanceMetadataOf").queryKeys(
keysArgs = classesWithInstances,
keyExtractor = { it.component1<BigInteger>() to it.component2<BigInteger>() },
binding = { parsedValue ->
parsedValue?.cast<Struct.Instance>()?.let {
UniquesInstance.Metadata(
data = bindString(it["data"])
)
}
}
)
}
val classDetails = multiQueryResults[classDetailsDescriptor]
val classMetadatas = multiQueryResults[classMetadatasDescriptor]
val instancesDetails = multiQueryResults[instancesDetailsDescriptor]
val instancesMetadatas = multiQueryResults[instancesMetadataDescriptor]
val classes = classesIds.associateWith { classId ->
UniquesClass(
id = classId,
metadata = classMetadatas[classId],
details = classDetails.getValue(classId)
)
}
classesWithInstances.map { (collectionId, instanceId) ->
val instanceKey = collectionId to instanceId
UniquesInstance(
collection = classes.getValue(collectionId),
id = instanceId,
metadata = instancesMetadatas[instanceKey],
details = instancesDetails.getValue(instanceKey)
)
}
}
Log.d(LOG_TAG, instances.toString())
}
}
@@ -0,0 +1,49 @@
package io.novafoundation.nova
import android.util.Log
import io.novafoundation.nova.common.di.FeatureUtils
import io.novafoundation.nova.common.utils.Fraction
import io.novafoundation.nova.common.utils.Perbill
import io.novafoundation.nova.feature_staking_api.di.StakingFeatureApi
import io.novafoundation.nova.feature_staking_api.domain.nominationPool.model.PoolId
import io.novafoundation.nova.feature_staking_impl.data.StakingOption
import io.novafoundation.nova.feature_staking_impl.data.StakingSharedState.OptionAdditionalData
import io.novafoundation.nova.feature_staking_impl.di.StakingFeatureComponent
import io.novafoundation.nova.feature_staking_impl.domain.nominationPools.common.rewards.NominationPoolRewardCalculator
import io.novafoundation.nova.runtime.ext.utilityAsset
import io.novafoundation.nova.runtime.multiNetwork.ChainWithAsset
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain.Asset.StakingType
import kotlinx.coroutines.flow.launchIn
import org.junit.Test
class NominationPoolsRewardCalculatorIntegrationTest : BaseIntegrationTest() {
private val stakingFeatureComponent = FeatureUtils.getFeature<StakingFeatureComponent>(context, StakingFeatureApi::class.java)
private val nominationPoolRewardCalculatorFactory = stakingFeatureComponent.nominationPoolRewardCalculatorFactory
private val stakingUpdateSystem = stakingFeatureComponent.stakingUpdateSystem
private val stakingSharedState = stakingFeatureComponent.stakingSharedState
@Test
fun testRewardCalculator() = runTest {
val polkadot = chainRegistry.polkadot()
val stakingOption = StakingOption(
assetWithChain = ChainWithAsset(polkadot, polkadot.utilityAsset),
additional = OptionAdditionalData(StakingType.NOMINATION_POOLS)
)
stakingSharedState.setSelectedOption(stakingOption)
stakingUpdateSystem.start()
.launchIn(this)
val rewardCalculator = nominationPoolRewardCalculatorFactory.create(stakingOption, sharedComputationScope = this)
Log.d("NominationPoolsRewardCalculatorIntegrationTest", "Max APY: ${rewardCalculator.maxAPY}")
Log.d("NominationPoolsRewardCalculatorIntegrationTest", "APY for Nova Pool: ${rewardCalculator.apyFor(54)}")
}
private fun NominationPoolRewardCalculator.apyFor(poolId: Int): Fraction? {
return apyFor(PoolId(poolId))
}
}
@@ -0,0 +1,324 @@
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.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
/**
* End-to-end integration tests for Pezkuwi chain compatibility.
* These tests verify that:
* 1. Runtime loads correctly with proper types
* 2. Extrinsics can be built
* 3. Fee calculation works
* 4. Transfer extrinsics can be created
*
* Run with: ./gradlew :app:connectedAndroidTest -Pandroid.testInstrumentationRunnerArguments.class=io.novafoundation.nova.PezkuwiIntegrationTest
*/
class PezkuwiIntegrationTest : BaseIntegrationTest() {
private val walletApi = FeatureUtils.getFeature<WalletFeatureApi>(
ApplicationProvider.getApplicationContext<Context>(),
WalletFeatureApi::class.java
)
private val extrinsicBuilderFactory = runtimeApi.provideExtrinsicBuilderFactory()
private val rpcCalls = runtimeApi.rpcCalls()
/**
* Test 1: Verify Pezkuwi Mainnet runtime loads with required types
*/
@Test
fun testPezkuwiMainnetRuntimeTypes() = runTest {
val chain = chainRegistry.getChain(Chain.Geneses.PEZKUWI)
val runtime = chainRegistry.getRuntime(chain.id)
// Verify critical types exist
val extrinsicSignature = runtime.typeRegistry["ExtrinsicSignature"]
assertNotNull("ExtrinsicSignature type should exist", extrinsicSignature)
val multiSignature = runtime.typeRegistry["MultiSignature"]
assertNotNull("MultiSignature type should exist", multiSignature)
val multiAddress = runtime.typeRegistry["MultiAddress"]
assertNotNull("MultiAddress type should exist", multiAddress)
val address = runtime.typeRegistry["Address"]
assertNotNull("Address type should exist", address)
println("Pezkuwi Mainnet: All required types present")
}
/**
* Test 2: Verify Pezkuwi Asset Hub runtime loads with required types
*/
@Test
fun testPezkuwiAssetHubRuntimeTypes() = runTest {
val chain = chainRegistry.getChain(Chain.Geneses.PEZKUWI_ASSET_HUB)
val runtime = chainRegistry.getRuntime(chain.id)
val extrinsicSignature = runtime.typeRegistry["ExtrinsicSignature"]
assertNotNull("ExtrinsicSignature type should exist", extrinsicSignature)
val address = runtime.typeRegistry["Address"]
assertNotNull("Address type should exist", address)
println("Pezkuwi Asset Hub: All required types present")
}
/**
* Test 3: Verify extrinsic builder can be created for Pezkuwi
*/
@Test
fun testPezkuwiExtrinsicBuilderCreation() = runTest {
val chain = chainRegistry.getChain(Chain.Geneses.PEZKUWI)
val builder = extrinsicBuilderFactory.create(
chain = chain,
options = io.novafoundation.nova.runtime.extrinsic.ExtrinsicBuilderFactory.Options(
batchMode = BatchMode.BATCH_ALL
)
)
assertNotNull("ExtrinsicBuilder should be created", builder)
println("Pezkuwi ExtrinsicBuilder created successfully")
}
/**
* Test 4: Verify transfer call can be constructed
*/
@Test
fun testPezkuwiTransferCallConstruction() = runTest {
val chain = chainRegistry.getChain(Chain.Geneses.PEZKUWI)
val runtime = chainRegistry.getRuntime(chain.id)
// Check if balances module exists
val balancesModule = runtime.metadata.moduleOrNull("Balances")
assertNotNull("Balances module should exist", balancesModule)
// Check transfer call exists
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)
println("Pezkuwi transfer call found: transfer_keep_alive=$hasTransferKeepAlive, transfer_allow_death=$hasTransferAllowDeath")
}
/**
* Test 5: Verify signed extensions are properly handled
*/
@Test
fun testPezkuwiSignedExtensions() = runTest {
val chain = chainRegistry.getChain(Chain.Geneses.PEZKUWI)
val runtime = chainRegistry.getRuntime(chain.id)
val signedExtensions = runtime.metadata.extrinsic.signedExtensions.map { it.id }
println("Pezkuwi signed extensions: $signedExtensions")
// Verify Pezkuwi-specific extensions
val hasAuthorizeCall = signedExtensions.contains("AuthorizeCall")
println("Has AuthorizeCall extension: $hasAuthorizeCall")
// Standard extensions should also be present
val hasCheckMortality = signedExtensions.contains("CheckMortality")
val hasCheckNonce = signedExtensions.contains("CheckNonce")
assertTrue("CheckMortality should exist", hasCheckMortality)
assertTrue("CheckNonce should exist", hasCheckNonce)
}
/**
* Test 6: Verify utility asset is properly configured
*/
@Test
fun testPezkuwiUtilityAsset() = runTest {
val chain = chainRegistry.getChain(Chain.Geneses.PEZKUWI)
val utilityAsset = chain.utilityAsset
assertNotNull("Utility asset should exist", utilityAsset)
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
}
}
}
@@ -0,0 +1,430 @@
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) }
}
}
@@ -0,0 +1,71 @@
package io.novafoundation.nova
import android.util.Log
import com.google.gson.GsonBuilder
import com.google.gson.JsonElement
import com.google.gson.JsonObject
import com.google.gson.JsonPrimitive
import com.google.gson.JsonSerializationContext
import com.google.gson.JsonSerializer
import io.novafoundation.nova.common.di.FeatureUtils
import io.novafoundation.nova.common.domain.ExtendedLoadingState
import io.novafoundation.nova.common.utils.inBackground
import io.novafoundation.nova.feature_staking_api.di.StakingFeatureApi
import io.novafoundation.nova.feature_staking_api.domain.dashboard.model.AggregatedStakingDashboardOption
import io.novafoundation.nova.feature_staking_api.domain.dashboard.model.StakingDashboard
import io.novafoundation.nova.feature_staking_api.domain.dashboard.model.isSyncing
import kotlinx.coroutines.flow.launchIn
import org.junit.Test
import java.lang.reflect.Type
class StakingDashboardIntegrationTest: BaseIntegrationTest() {
private val stakingApi = FeatureUtils.getFeature<StakingFeatureApi>(context, StakingFeatureApi::class.java)
private val interactor = stakingApi.dashboardInteractor
private val updateSystem = stakingApi.dashboardUpdateSystem
private val gson = GsonBuilder()
.registerTypeHierarchyAdapter(AggregatedStakingDashboardOption::class.java, AggregatedStakingDashboardOptionDesirializer())
.create()
@Test
fun syncStakingDashboard() = runTest {
updateSystem.start()
.inBackground()
.launchIn(this)
interactor.stakingDashboardFlow()
.inBackground()
.collect(::logDashboard)
}
private fun logDashboard(dashboard: ExtendedLoadingState<StakingDashboard>) {
if (dashboard !is ExtendedLoadingState.Loaded) return
val serialized = gson.toJson(dashboard)
val message = """
Dashboard state:
Syncing items: ${dashboard.data.syncingItemsCount()}
$serialized
""".trimIndent()
Log.d("StakingDashboardIntegrationTest", message)
}
private class AggregatedStakingDashboardOptionDesirializer : JsonSerializer<AggregatedStakingDashboardOption<*>> {
override fun serialize(src: AggregatedStakingDashboardOption<*>, typeOfSrc: Type, context: JsonSerializationContext): JsonElement {
return JsonObject().apply {
add("chain", JsonPrimitive(src.chain.name))
add("stakingState", context.serialize(src.stakingState))
add("syncing", context.serialize(src.syncingStage))
}
}
}
private fun StakingDashboard.syncingItemsCount(): Int {
return withoutStake.count { it.syncingStage.isSyncing() } + hasStake.count { it.syncingStage.isSyncing() }
}
}
@@ -0,0 +1,46 @@
package io.novafoundation.nova
import android.util.Log
import io.novafoundation.nova.common.di.FeatureUtils
import io.novafoundation.nova.common.utils.LOG_TAG
import io.novafoundation.nova.feature_staking_api.data.parachainStaking.turing.repository.AutomationAction
import io.novafoundation.nova.feature_staking_api.data.parachainStaking.turing.repository.OptimalAutomationRequest
import io.novafoundation.nova.feature_staking_api.di.StakingFeatureApi
import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry
import io.novafoundation.nova.runtime.multiNetwork.findChain
import kotlinx.coroutines.runBlocking
import org.junit.Test
import java.math.BigInteger
class TuringAutomationIntegrationTest : BaseIntegrationTest() {
private val stakingApi = FeatureUtils.getFeature<StakingFeatureApi>(context, StakingFeatureApi::class.java)
private val automationTasksRepository = stakingApi.turingAutomationRepository
@Test
fun calculateOptimalAutoCompounding(){
runBlocking {
val chain = chainRegistry.findTuringChain()
val request = OptimalAutomationRequest(
collator = "6AEG2WKRVvZteWWT3aMkk2ZE21FvURqiJkYpXimukub8Zb9C",
amount = BigInteger("1000000000000")
)
val response = automationTasksRepository.calculateOptimalAutomation(chain.id, request)
Log.d(LOG_TAG, response.toString())
}
}
@Test
fun calculateAutoCompoundExecutionFees(){
runBlocking {
val chain = chainRegistry.findTuringChain()
val fees = automationTasksRepository.getTimeAutomationFees(chain.id, AutomationAction.AUTO_COMPOUND_DELEGATED_STAKE, executions = 1)
Log.d(LOG_TAG, fees.toString())
}
}
private suspend fun ChainRegistry.findTuringChain() = findChain { it.name == "Turing" }!!
}
@@ -0,0 +1,137 @@
package io.novafoundation.nova
import android.util.Log
import io.novafoundation.nova.common.utils.LOG_TAG
import io.novafoundation.nova.common.utils.second
import io.novafoundation.nova.core.ethereum.Web3Api
import io.novafoundation.nova.core.ethereum.log.Topic
import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.types.Balance
import io.novafoundation.nova.runtime.ethereum.contract.base.querySingle
import io.novafoundation.nova.runtime.ethereum.contract.erc20.Erc20Queries
import io.novafoundation.nova.runtime.ethereum.contract.erc20.Erc20Standard
import io.novafoundation.nova.runtime.ethereum.sendSuspend
import io.novafoundation.nova.runtime.multiNetwork.getEthereumApiOrThrow
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.emitAll
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.merge
import kotlinx.coroutines.flow.take
import kotlinx.coroutines.flow.toList
import kotlinx.coroutines.reactive.asFlow
import kotlinx.coroutines.runBlocking
import org.junit.Test
import org.web3j.abi.EventEncoder
import org.web3j.abi.TypeEncoder
import org.web3j.abi.datatypes.Address
import org.web3j.protocol.core.DefaultBlockParameterName
import java.math.BigInteger
class Erc20Transfer(
val txHash: String,
val blockNumber: String,
val from: String,
val to: String,
val contract: String,
val amount: BigInteger,
)
class Web3jServiceIntegrationTest : BaseIntegrationTest() {
@Test
fun shouldFetchBalance(): Unit = runBlocking {
val web3j = moonbeamWeb3j()
val balance = web3j.ethGetBalance("0xf977814e90da44bfa03b6295a0616a897441acec", DefaultBlockParameterName.LATEST).sendSuspend()
Log.d(LOG_TAG, balance.balance.toString())
}
@Test
fun shouldFetchComplexStructure(): Unit = runBlocking {
val web3j = moonbeamWeb3j()
val block = web3j.ethGetBlockByNumber(DefaultBlockParameterName.LATEST, true).sendSuspend()
Log.d(LOG_TAG, block.block.hash)
}
@Test
fun shouldSubscribeToNewHeadEvents(): Unit = runBlocking {
val web3j = moonbeamWeb3j()
val newHead = web3j.newHeadsNotifications().asFlow().first()
Log.d(LOG_TAG, "New head appended to chain: ${newHead.params.result.hash}")
}
@Test
fun shouldSubscribeBalances(): Unit = runBlocking {
val web3j = moonbeamWeb3j()
val accountAddress = "0x4A43C16107591AE5Ec904e584ed4Bb05386F98f7"
val moonbeamUsdc = "0x818ec0a7fe18ff94269904fced6ae3dae6d6dc0b"
val balanceUpdates = web3j.erc20BalanceFlow(accountAddress, moonbeamUsdc).take(2).toList()
error("Initial balance: ${balanceUpdates.first()}, new balance: ${balanceUpdates.second()}")
}
private fun Web3Api.erc20BalanceFlow(account: String, contract: String): Flow<Balance> {
return flow {
val erc20 = Erc20Standard().querySingle(contract, web3j = this@erc20BalanceFlow)
val initialBalance = erc20.balanceOfAsync(account).await()
emit(initialBalance)
val changes = accountErcTransfersFlow(account).map {
erc20.balanceOfAsync(account).await()
}
emitAll(changes)
}
}
private fun Web3Api.accountErcTransfersFlow(address: String): Flow<Erc20Transfer> {
val addressTopic = TypeEncoder.encode(Address(address))
val transferEvent = Erc20Queries.TRANSFER_EVENT
val transferEventSignature = EventEncoder.encode(transferEvent)
val contractAddresses = emptyList<String>() // everything
val erc20SendTopic = listOf(
Topic.Single(transferEventSignature), // zero-th topic is event signature
Topic.AnyOf(addressTopic), // our account as `from`
)
val erc20ReceiveTopic = listOf(
Topic.Single(transferEventSignature), // zero-th topic is event signature
Topic.Any, // anyone is `from`
Topic.AnyOf(addressTopic) // out account as `to`
)
val receiveTransferNotifications = logsNotifications(contractAddresses, erc20ReceiveTopic)
val sendTransferNotifications = logsNotifications(contractAddresses, erc20SendTopic)
val transferNotifications = merge(receiveTransferNotifications, sendTransferNotifications)
return transferNotifications.map { logNotification ->
val log = logNotification.params.result
val contract = log.address
val event = Erc20Queries.parseTransferEvent(log)
Erc20Transfer(
txHash = log.transactionHash,
blockNumber = log.blockNumber,
from = event.from.value,
to = event.to.value,
contract = contract,
amount = event.amount.value,
)
}
}
private suspend fun moonbeamWeb3j(): Web3Api {
val moonbeamChainId = "fe58ea77779b7abda7da4ec526d14db9b1e9cd40a217c34892af80a9b332b76d"
return chainRegistry.getEthereumApiOrThrow(moonbeamChainId, Chain.Node.ConnectionType.WSS)
}
}
@@ -0,0 +1,164 @@
package io.novafoundation.nova.balances
import android.content.Context
import androidx.test.core.app.ApplicationProvider
import com.google.gson.Gson
import io.novafoundation.nova.common.data.network.runtime.binding.AccountInfo
import io.novafoundation.nova.common.data.network.runtime.binding.bindAccountInfo
import io.novafoundation.nova.common.di.FeatureUtils
import io.novafoundation.nova.common.utils.fromJson
import io.novafoundation.nova.common.utils.hasModule
import io.novafoundation.nova.common.utils.system
import io.novafoundation.nova.core.model.CryptoType
import io.novafoundation.nova.core_db.model.chain.account.MetaAccountLocal
import io.novafoundation.nova.feature_account_api.data.ethereum.transaction.TransactionOrigin
import io.novafoundation.nova.feature_account_api.di.AccountFeatureApi
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.di.AccountFeatureComponent
import io.novafoundation.nova.feature_account_impl.domain.account.model.DefaultMetaAccount
import io.novafoundation.nova.runtime.BuildConfig.TEST_CHAINS_URL
import io.novafoundation.nova.runtime.di.RuntimeApi
import io.novafoundation.nova.runtime.di.RuntimeComponent
import io.novafoundation.nova.runtime.extrinsic.systemRemark
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain
import io.novafoundation.nova.runtime.multiNetwork.connection.ChainConnection
import io.novafoundation.nova.runtime.multiNetwork.getSocket
import io.novasama.substrate_sdk_android.extensions.fromHex
import io.novasama.substrate_sdk_android.runtime.metadata.storage
import io.novasama.substrate_sdk_android.runtime.metadata.storageKey
import io.novasama.substrate_sdk_android.wsrpc.networkStateFlow
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withTimeout
import org.junit.Assert.assertTrue
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.Parameterized
import java.math.BigInteger
import java.math.BigInteger.ZERO
import java.net.URL
import kotlin.time.Duration.Companion.seconds
@RunWith(Parameterized::class)
class BalancesIntegrationTest(
private val testChainId: String,
private val testChainName: String,
private val testAccount: String
) {
companion object {
@JvmStatic
@Parameterized.Parameters(name = "{1}")
fun data(): List<Array<String?>> {
val arrayOfNetworks: Array<TestData> = Gson().fromJson(URL(TEST_CHAINS_URL).readText())
return arrayOfNetworks.map { arrayOf(it.chainId, it.name, it.account) }
}
class TestData(
val chainId: String,
val name: String,
val account: String?
)
}
private val maxAmount = BigInteger.valueOf(10).pow(30)
private val runtimeApi = FeatureUtils.getFeature<RuntimeComponent>(
ApplicationProvider.getApplicationContext<Context>(),
RuntimeApi::class.java
)
private val accountApi = FeatureUtils.getFeature<AccountFeatureComponent>(
ApplicationProvider.getApplicationContext<Context>(),
AccountFeatureApi::class.java
)
private val chainRegistry = runtimeApi.chainRegistry()
private val externalRequirementFlow = runtimeApi.externalRequirementFlow()
private val remoteStorage = runtimeApi.remoteStorageSource()
private val extrinsicService = accountApi.extrinsicService()
@Before
fun before() = runBlocking {
externalRequirementFlow.emit(ChainConnection.ExternalRequirement.ALLOWED)
}
@Test
fun testBalancesLoading() = runBlocking(Dispatchers.Default) {
val chains = chainRegistry.getChain(testChainId)
val freeBalance = testBalancesInChainAsync(chains, testAccount)?.data?.free ?: error("Balance was null")
assertTrue("Free balance: $freeBalance is less than $maxAmount", maxAmount > freeBalance)
assertTrue("Free balance: $freeBalance is greater than 0", ZERO < freeBalance)
}
@Test
fun testFeeLoading() = runBlocking(Dispatchers.Default) {
val chains = chainRegistry.getChain(testChainId)
testFeeLoadingAsync(chains)
Unit
}
private suspend fun testBalancesInChainAsync(chain: Chain, currentAccount: String): AccountInfo? {
return coroutineScope {
try {
withTimeout(80.seconds) {
remoteStorage.query(
chainId = chain.id,
keyBuilder = { it.metadata.system().storage("Account").storageKey(it, currentAccount.fromHex()) },
binding = { scale, runtime -> scale?.let { bindAccountInfo(scale, runtime) } }
)
}
} catch (e: Exception) {
throw Exception("Socket state: ${chainRegistry.getSocket(chain.id).networkStateFlow().first()}, error: ${e.message}", e)
}
}
}
private suspend fun testFeeLoadingAsync(chain: Chain) {
return coroutineScope {
withTimeout(80.seconds) {
extrinsicService.estimateFee(chain, testTransactionOrigin()) {
systemRemark(byteArrayOf(0))
val haveBatch = runtime.metadata.hasModule("Utility")
if (haveBatch) {
systemRemark(byteArrayOf(0))
}
}
}
}
}
private fun testTransactionOrigin(): TransactionOrigin = TransactionOrigin.Wallet(
createTestMetaAccount()
)
private fun createTestMetaAccount(): MetaAccount {
val metaAccount = DefaultMetaAccount(
id = 0,
globallyUniqueId = MetaAccountLocal.generateGloballyUniqueId(),
substratePublicKey = testAccount.fromHex(),
substrateCryptoType = CryptoType.SR25519,
substrateAccountId = testAccount.fromHex(),
ethereumAddress = testAccount.fromHex(),
ethereumPublicKey = testAccount.fromHex(),
isSelected = true,
name = "Test",
type = LightMetaAccount.Type.WATCH_ONLY,
status = LightMetaAccount.Status.ACTIVE,
chainAccounts = emptyMap(),
parentMetaId = null
)
return metaAccount
}
}