mirror of
https://github.com/pezkuwichain/pezkuwi-wallet-android.git
synced 2026-04-22 09:08:03 +00:00
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:
@@ -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())
|
||||
}
|
||||
}
|
||||
+49
@@ -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
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user