Initial commit: Pezkuwi Wallet Android

Complete rebrand of Nova Wallet for Pezkuwichain ecosystem.

## Features
- Full Pezkuwichain support (HEZ & PEZ tokens)
- Polkadot ecosystem compatibility
- Staking, Governance, DeFi, NFTs
- XCM cross-chain transfers
- Hardware wallet support (Ledger, Polkadot Vault)
- WalletConnect v2
- Push notifications

## Languages
- English, Turkish, Kurmanci (Kurdish), Spanish, French, German, Russian, Japanese, Chinese, Korean, Portuguese, Vietnamese

Based on Nova Wallet by Novasama Technologies GmbH
© Dijital Kurdistan Tech Institute 2026
This commit is contained in:
2026-01-23 01:31:12 +03:00
commit 31c8c5995f
7621 changed files with 425838 additions and 0 deletions
@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
</manifest>
@@ -0,0 +1,194 @@
package io.novafoundation.nova.feature_xcm_impl.builder
import io.novafoundation.nova.common.address.AccountIdKey
import io.novafoundation.nova.common.data.network.runtime.binding.BalanceOf
import io.novafoundation.nova.feature_xcm_api.asset.MultiAsset
import io.novafoundation.nova.feature_xcm_api.asset.MultiAssetFilter
import io.novafoundation.nova.feature_xcm_api.asset.MultiAssetId
import io.novafoundation.nova.feature_xcm_api.asset.MultiAssets
import io.novafoundation.nova.feature_xcm_api.asset.withAmount
import io.novafoundation.nova.feature_xcm_api.builder.XcmBuilder
import io.novafoundation.nova.feature_xcm_api.builder.fees.MeasureXcmFees
import io.novafoundation.nova.feature_xcm_api.builder.fees.PayFeesMode
import io.novafoundation.nova.feature_xcm_api.message.VersionedXcmMessage
import io.novafoundation.nova.feature_xcm_api.message.XcmInstruction
import io.novafoundation.nova.feature_xcm_api.message.XcmMessage
import io.novafoundation.nova.feature_xcm_api.message.asVersionedXcmMessage
import io.novafoundation.nova.feature_xcm_api.message.asXcmMessage
import io.novafoundation.nova.feature_xcm_api.multiLocation.AbsoluteMultiLocation
import io.novafoundation.nova.feature_xcm_api.multiLocation.AssetLocation
import io.novafoundation.nova.feature_xcm_api.multiLocation.ChainLocation
import io.novafoundation.nova.feature_xcm_api.multiLocation.multiAssetIdOn
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.versionedXcm
import io.novafoundation.nova.feature_xcm_api.weight.WeightLimit
internal class RealXcmBuilder(
initialLocation: ChainLocation,
override val xcmVersion: XcmVersion,
private val measureXcmFees: MeasureXcmFees,
) : XcmBuilder {
override var currentLocation: ChainLocation = initialLocation
private val previousContexts: MutableList<PendingContextInstructions> = mutableListOf()
private val currentLocationInstructions: MutableList<PendingInstruction> = mutableListOf()
override fun payFees(payFeesMode: PayFeesMode) {
currentLocationInstructions.add(PendingInstruction.PayFees(payFeesMode))
}
override fun withdrawAsset(assets: MultiAssets) {
addRegularInstruction(XcmInstruction.WithdrawAsset(assets))
}
override fun buyExecution(fees: MultiAsset, weightLimit: WeightLimit) {
addRegularInstruction(XcmInstruction.BuyExecution(fees, weightLimit))
}
override fun depositAsset(assets: MultiAssetFilter, beneficiary: AccountIdKey) {
addRegularInstruction(XcmInstruction.DepositAsset(assets, beneficiary.toMultiLocation()))
}
override fun transferReserveAsset(assets: MultiAssets, dest: ChainLocation) {
performContextSwitch(dest) { forwardedMessage, forwardingFrom ->
XcmInstruction.TransferReserveAsset(assets, dest.location.fromPointOfViewOf(forwardingFrom), forwardedMessage)
}
}
override fun initiateReserveWithdraw(assets: MultiAssetFilter, reserve: ChainLocation) {
performContextSwitch(reserve) { forwardedMessage, forwardingFrom ->
XcmInstruction.InitiateReserveWithdraw(assets, reserve.location.fromPointOfViewOf(forwardingFrom), forwardedMessage)
}
}
override fun depositReserveAsset(assets: MultiAssetFilter, dest: ChainLocation) {
performContextSwitch(dest) { forwardedMessage, forwardingFrom ->
XcmInstruction.DepositReserveAsset(assets, dest.location.fromPointOfViewOf(forwardingFrom), forwardedMessage)
}
}
override fun initiateTeleport(assets: MultiAssetFilter, dest: ChainLocation) {
performContextSwitch(dest) { forwardedMessage, forwardingFrom ->
XcmInstruction.InitiateTeleport(assets, dest.location.fromPointOfViewOf(forwardingFrom), forwardedMessage)
}
}
override suspend fun build(): VersionedXcmMessage {
val lastMessage = createXcmMessage(currentLocationInstructions, currentLocation)
return previousContexts.foldRight(lastMessage) { context, forwardedMessage ->
createXcmMessage(context, forwardedMessage)
}.versionedXcm(xcmVersion)
}
private fun addRegularInstruction(instruction: XcmInstruction) {
currentLocationInstructions.add(PendingInstruction.Regular(instruction))
}
private suspend fun createXcmMessage(
pendingContextInstructions: PendingContextInstructions,
forwardedMessage: XcmMessage
): XcmMessage {
val switchInstruction = pendingContextInstructions.contextSwitch(forwardedMessage, pendingContextInstructions.chainLocation.location)
val allInstructions = pendingContextInstructions.instructions + PendingInstruction.Regular(switchInstruction)
return createXcmMessage(allInstructions, pendingContextInstructions.chainLocation)
}
private suspend fun createXcmMessage(
pendingInstructions: List<PendingInstruction>,
chainLocation: ChainLocation,
): XcmMessage {
return pendingInstructions.map { pendingInstruction ->
pendingInstruction.constructSubmissionInstruction(pendingInstructions, chainLocation)
}.asXcmMessage()
}
private suspend fun PendingInstruction.constructSubmissionInstruction(
allInstructions: List<PendingInstruction>,
chainLocation: ChainLocation,
): XcmInstruction {
return when (this) {
is PendingInstruction.Regular -> instruction
is PendingInstruction.PayFees -> constructSubmissionInstruction(allInstructions, chainLocation)
}
}
private suspend fun PendingInstruction.PayFees.constructSubmissionInstruction(
allInstructions: List<PendingInstruction>,
chainLocation: ChainLocation,
): XcmInstruction.PayFees {
val fees = when (val mode = mode) {
is PayFeesMode.Exact -> mode.fee
is PayFeesMode.Measured -> measureFees(allInstructions, mode.feeAssetId, chainLocation)
}
return XcmInstruction.PayFees(fees)
}
private suspend fun measureFees(
allInstructions: List<PendingInstruction>,
feeAssetIdLocation: AssetLocation,
chainLocation: ChainLocation,
): MultiAsset {
val feeAssetId = feeAssetIdLocation.multiAssetIdOn(chainLocation)
val messageForEstimation = allInstructions.map { pendingInstruction ->
pendingInstruction.constructEstimationInstruction(feeAssetId)
}.asVersionedXcmMessage(xcmVersion)
require(chainLocation.chainId == feeAssetIdLocation.assetId.chainId) {
"""
Supplied fee asset does not belong to the current chain.
Expected: ${chainLocation.chainId}
Got: ${feeAssetIdLocation.assetId.chainId} (Asset id: ${feeAssetIdLocation.assetId.assetId})
""".trimIndent()
}
val measuredFees = measureXcmFees.measureFees(messageForEstimation, feeAssetIdLocation, chainLocation)
return feeAssetId.withAmount(measuredFees)
}
private fun PendingInstruction.constructEstimationInstruction(feeAssetId: MultiAssetId): XcmInstruction {
return when (this) {
is PendingInstruction.Regular -> instruction
is PendingInstruction.PayFees -> constructEstimationInstruction(feeAssetId)
}
}
private fun PendingInstruction.PayFees.constructEstimationInstruction(feeAssetId: MultiAssetId): XcmInstruction.PayFees {
val fees = when (val mode = mode) {
is PayFeesMode.Exact -> mode.fee
is PayFeesMode.Measured -> feeAssetId.withAmount(BalanceOf.ONE) // Use fake amount in pay fees instruction for fee estimation
}
return XcmInstruction.PayFees(fees)
}
private fun performContextSwitch(newLocation: ChainLocation, switch: PendingContextSwitch) {
val instructionsInCurrentContext = currentLocationInstructions.toList()
val pendingContextInstructions = PendingContextInstructions(instructionsInCurrentContext, currentLocation, switch)
previousContexts.add(pendingContextInstructions)
currentLocationInstructions.clear()
currentLocation = newLocation
}
private class PendingContextInstructions(
val instructions: List<PendingInstruction>,
val chainLocation: ChainLocation,
val contextSwitch: PendingContextSwitch
)
private sealed class PendingInstruction {
class Regular(val instruction: XcmInstruction) : PendingInstruction()
class PayFees(val mode: PayFeesMode) : PendingInstruction()
}
}
private typealias PendingContextSwitch = (forwardedXcm: XcmMessage, forwardingFrom: AbsoluteMultiLocation) -> XcmInstruction
@@ -0,0 +1,20 @@
package io.novafoundation.nova.feature_xcm_impl.builder
import io.novafoundation.nova.common.di.scope.FeatureScope
import io.novafoundation.nova.feature_xcm_api.builder.XcmBuilder
import io.novafoundation.nova.feature_xcm_api.builder.fees.MeasureXcmFees
import io.novafoundation.nova.feature_xcm_api.multiLocation.ChainLocation
import io.novafoundation.nova.feature_xcm_api.versions.XcmVersion
import javax.inject.Inject
@FeatureScope
internal class RealXcmBuilderFactory @Inject constructor() : XcmBuilder.Factory {
override fun create(
initial: ChainLocation,
xcmVersion: XcmVersion,
measureXcmFees: MeasureXcmFees
): XcmBuilder {
return RealXcmBuilder(initial, xcmVersion, measureXcmFees)
}
}
@@ -0,0 +1,19 @@
package io.novafoundation.nova.feature_xcm_impl.converter
import io.novafoundation.nova.common.utils.tryFindNonNull
import io.novafoundation.nova.feature_xcm_api.converter.MultiLocationConverter
import io.novafoundation.nova.feature_xcm_api.multiLocation.RelativeMultiLocation
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain
internal class CompoundMultiLocationConverter(
private vararg val delegates: MultiLocationConverter
) : MultiLocationConverter {
override suspend fun toMultiLocation(chainAsset: Chain.Asset): RelativeMultiLocation? {
return delegates.tryFindNonNull { it.toMultiLocation(chainAsset) }
}
override suspend fun toChainAsset(multiLocation: RelativeMultiLocation): Chain.Asset? {
return delegates.tryFindNonNull { it.toChainAsset(multiLocation) }
}
}
@@ -0,0 +1,91 @@
package io.novafoundation.nova.feature_xcm_impl.converter
import io.novafoundation.nova.feature_xcm_api.multiLocation.RelativeMultiLocation
import io.novafoundation.nova.feature_xcm_api.multiLocation.bindMultiLocation
import io.novafoundation.nova.common.utils.lazyAsync
import io.novafoundation.nova.common.utils.toHexUntypedOrNull
import io.novafoundation.nova.feature_xcm_api.converter.MultiLocationConverter
import io.novafoundation.nova.feature_xcm_api.versions.XcmVersion
import io.novafoundation.nova.feature_xcm_api.versions.detector.XcmVersionDetector
import io.novafoundation.nova.feature_xcm_api.versions.orDefault
import io.novafoundation.nova.runtime.ext.requireStatemine
import io.novafoundation.nova.runtime.ext.statemineOrNull
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain
import io.novafoundation.nova.runtime.multiNetwork.chain.model.StatemineAssetId
import io.novafoundation.nova.runtime.multiNetwork.chain.model.asScaleEncodedOrThrow
import io.novafoundation.nova.runtime.multiNetwork.chain.model.isScaleEncoded
import io.novafoundation.nova.runtime.multiNetwork.chain.model.prepareIdForEncoding
import io.novafoundation.nova.runtime.multiNetwork.chain.model.statemineAssetIdScaleType
import io.novasama.substrate_sdk_android.runtime.definitions.types.RuntimeType
private typealias ScaleEncodedMultiLocation = String
private typealias ForeignAssetsAssetId = ScaleEncodedMultiLocation
private typealias ForeignAssetsMappingKey = ForeignAssetsAssetId // we only allow one pallet so no need to include pallet name into key
private typealias ForeignAssetsMapping = Map<ForeignAssetsMappingKey, Chain.Asset>
private const val FOREIGN_ASSETS_PALLET_NAME = "ForeignAssets"
internal class ForeignAssetsLocationConverter(
private val chain: Chain,
private val runtime: RuntimeSource,
private val xcmVersionDetector: XcmVersionDetector,
) : MultiLocationConverter {
private val assetIdToAssetMapping by lazy { constructAssetIdToAssetMapping() }
private var assetIdEncodingContext = lazyAsync { constructAssetIdEncodingContext() }
override suspend fun toMultiLocation(chainAsset: Chain.Asset): RelativeMultiLocation? {
if (chainAsset.chainId != chain.id) return null
return chainAsset.extractMultiLocation()
}
override suspend fun toChainAsset(multiLocation: RelativeMultiLocation): Chain.Asset? {
val (xcmVersion, assetIdType) = assetIdEncodingContext.get() ?: return null
val encodableInstance = multiLocation.toEncodableInstance(xcmVersion)
val multiLocationHex = assetIdType.toHexUntypedOrNull(runtime.getRuntime(), encodableInstance) ?: return null
return assetIdToAssetMapping[multiLocationHex]
}
private fun constructAssetIdToAssetMapping(): ForeignAssetsMapping {
return chain.assets
.filter {
val type = it.type
type is Chain.Asset.Type.Statemine &&
type.palletName == FOREIGN_ASSETS_PALLET_NAME &&
type.id is StatemineAssetId.ScaleEncoded
}
.associateBy { statemineAsset ->
val assetsType = statemineAsset.requireStatemine()
assetsType.id.asScaleEncodedOrThrow()
}
}
private suspend fun Chain.Asset.extractMultiLocation(): RelativeMultiLocation? {
val assetsType = statemineOrNull() ?: return null
if (!assetsType.id.isScaleEncoded()) return null
return runCatching {
val encodableMultiLocation = assetsType.prepareIdForEncoding(runtime.getRuntime())
bindMultiLocation(encodableMultiLocation)
}.getOrNull()
}
private suspend fun constructAssetIdEncodingContext(): AssetIdEncodingContext? {
val assetIdType = statemineAssetIdScaleType(
runtime.getRuntime(),
FOREIGN_ASSETS_PALLET_NAME
) ?: return null
val xcmVersion = xcmVersionDetector.detectMultiLocationVersion(chain.id, assetIdType).orDefault()
return AssetIdEncodingContext(xcmVersion, assetIdType)
}
private data class AssetIdEncodingContext(
val xcmVersion: XcmVersion,
val assetIdType: RuntimeType<*, *>
)
}
@@ -0,0 +1,76 @@
package io.novafoundation.nova.feature_xcm_impl.converter
import io.novafoundation.nova.common.utils.PalletName
import io.novafoundation.nova.feature_xcm_api.converter.MultiLocationConverter
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.RelativeMultiLocation
import io.novafoundation.nova.feature_xcm_api.multiLocation.junctions
import io.novafoundation.nova.runtime.ext.palletNameOrDefault
import io.novafoundation.nova.runtime.ext.requireStatemine
import io.novafoundation.nova.runtime.ext.statemineOrNull
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain
import io.novafoundation.nova.runtime.multiNetwork.chain.model.StatemineAssetId
import io.novafoundation.nova.runtime.multiNetwork.chain.model.asNumberOrNull
import io.novafoundation.nova.runtime.multiNetwork.chain.model.asNumberOrThrow
import io.novasama.substrate_sdk_android.runtime.metadata.moduleOrNull
import java.math.BigInteger
private typealias LocalAssetsAssetId = BigInteger
private typealias LocalAssetsMappingKey = Pair<PalletName, LocalAssetsAssetId>
private typealias LocalAssetsMapping = Map<LocalAssetsMappingKey, Chain.Asset>
class LocalAssetsLocationConverter(
private val chain: Chain,
private val runtimeSource: RuntimeSource
) : MultiLocationConverter {
private val assetIdToAssetMapping by lazy { constructAssetIdToAssetMapping() }
override suspend fun toMultiLocation(chainAsset: Chain.Asset): RelativeMultiLocation? {
if (chainAsset.chainId != chain.id) return null
val assetsType = chainAsset.statemineOrNull() ?: return null
// LocalAssets converter only supports number ids to use as GeneralIndex
val index = assetsType.id.asNumberOrNull() ?: return null
val pallet = runtimeSource.getRuntime().metadata.moduleOrNull(assetsType.palletNameOrDefault()) ?: return null
return RelativeMultiLocation(
parents = 0, // For Local Assets chain serves as a reserve
interior = Junctions(
MultiLocation.Junction.PalletInstance(pallet.index),
MultiLocation.Junction.GeneralIndex(index)
)
)
}
override suspend fun toChainAsset(multiLocation: RelativeMultiLocation): Chain.Asset? {
// We only consider local reserves for LocalAssets
if (multiLocation.parents > 0) return null
val junctions = multiLocation.junctions
if (junctions.size != 2) return null
val (maybePalletInstance, maybeGeneralIndex) = junctions
if (maybePalletInstance !is MultiLocation.Junction.PalletInstance || maybeGeneralIndex !is MultiLocation.Junction.GeneralIndex) return null
val pallet = runtimeSource.getRuntime().metadata.moduleOrNull(maybePalletInstance.index.toInt()) ?: return null
val assetId = maybeGeneralIndex.index
return assetIdToAssetMapping[pallet.name to assetId]
}
private fun constructAssetIdToAssetMapping(): LocalAssetsMapping {
return chain.assets
.filter {
val type = it.type
type is Chain.Asset.Type.Statemine && type.id is StatemineAssetId.Number
}
.associateBy { statemineAsset ->
val assetsType = statemineAsset.requireStatemine()
val palletName = assetsType.palletNameOrDefault()
palletName to assetsType.id.asNumberOrThrow()
}
}
}
@@ -0,0 +1,42 @@
package io.novafoundation.nova.feature_xcm_impl.converter
import io.novafoundation.nova.feature_xcm_api.converter.MultiLocationConverter
import io.novafoundation.nova.feature_xcm_api.multiLocation.MultiLocation
import io.novafoundation.nova.feature_xcm_api.multiLocation.RelativeMultiLocation
import io.novafoundation.nova.feature_xcm_api.multiLocation.isHere
import io.novafoundation.nova.runtime.ext.isUtilityAsset
import io.novafoundation.nova.runtime.ext.relaychainAsNative
import io.novafoundation.nova.runtime.ext.utilityAsset
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain
internal class NativeAssetLocationConverter(
private val chain: Chain,
) : MultiLocationConverter {
override suspend fun toMultiLocation(chainAsset: Chain.Asset): RelativeMultiLocation? {
return if (chainAsset.chainId == chain.id && chainAsset.isUtilityAsset) {
RelativeMultiLocation(
parents = chain.expectedParentsInNativeInterior(),
interior = MultiLocation.Interior.Here
)
} else {
null
}
}
override suspend fun toChainAsset(multiLocation: RelativeMultiLocation): Chain.Asset? {
return if (chain.isNativeMultiLocation(multiLocation)) {
chain.utilityAsset
} else {
null
}
}
private fun Chain.expectedParentsInNativeInterior(): Int {
return if (additional.relaychainAsNative()) 1 else 0
}
private fun Chain.isNativeMultiLocation(multiLocation: RelativeMultiLocation): Boolean {
return multiLocation.interior.isHere() && multiLocation.parents == expectedParentsInNativeInterior()
}
}
@@ -0,0 +1,53 @@
package io.novafoundation.nova.feature_xcm_impl.converter
import io.novafoundation.nova.common.di.scope.FeatureScope
import io.novafoundation.nova.feature_xcm_api.converter.MultiLocationConverter
import io.novafoundation.nova.feature_xcm_api.converter.MultiLocationConverterFactory
import io.novafoundation.nova.feature_xcm_api.versions.detector.XcmVersionDetector
import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain
import io.novafoundation.nova.runtime.multiNetwork.getRuntime
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.async
import javax.inject.Inject
@FeatureScope
class RealMultiLocationConverterFactory @Inject constructor(
private val chainRegistry: ChainRegistry,
private val xcmVersionDetector: XcmVersionDetector,
) : MultiLocationConverterFactory {
override fun defaultAsync(chain: Chain, coroutineScope: CoroutineScope): MultiLocationConverter {
val runtimeAsync = coroutineScope.async { chainRegistry.getRuntime(chain.id) }
val runtimeSource = RuntimeSource.Async(runtimeAsync)
return CompoundMultiLocationConverter(
NativeAssetLocationConverter(chain),
LocalAssetsLocationConverter(chain, runtimeSource),
ForeignAssetsLocationConverter(chain, runtimeSource, xcmVersionDetector)
)
}
override suspend fun defaultSync(chain: Chain): MultiLocationConverter {
val runtimeAsync = chainRegistry.getRuntime(chain.id)
val runtimeSource = RuntimeSource.Sync(runtimeAsync)
return CompoundMultiLocationConverter(
NativeAssetLocationConverter(chain),
LocalAssetsLocationConverter(chain, runtimeSource),
ForeignAssetsLocationConverter(chain, runtimeSource, xcmVersionDetector)
)
}
override suspend fun resolveLocalAssets(chain: Chain): MultiLocationConverter {
val runtime = chainRegistry.getRuntime(chain.id)
return CompoundMultiLocationConverter(
NativeAssetLocationConverter(chain),
LocalAssetsLocationConverter(
chain,
RuntimeSource.Sync(runtime)
),
)
}
}
@@ -0,0 +1,23 @@
package io.novafoundation.nova.feature_xcm_impl.converter
import io.novasama.substrate_sdk_android.runtime.RuntimeSnapshot
import kotlinx.coroutines.Deferred
sealed interface RuntimeSource {
suspend fun getRuntime(): RuntimeSnapshot
class Sync(private val runtimeSnapshot: RuntimeSnapshot) : RuntimeSource {
override suspend fun getRuntime(): RuntimeSnapshot {
return runtimeSnapshot
}
}
class Async(private val runtimeSnapshotAsync: Deferred<RuntimeSnapshot>) : RuntimeSource {
override suspend fun getRuntime(): RuntimeSnapshot {
return runtimeSnapshotAsync.await()
}
}
}
@@ -0,0 +1,42 @@
package io.novafoundation.nova.feature_xcm_impl.converter.chain
import io.novafoundation.nova.feature_xcm_api.converter.chain.ChainMultiLocationConverter
import io.novafoundation.nova.feature_xcm_api.multiLocation.MultiLocation.Junction.ParachainId
import io.novafoundation.nova.feature_xcm_api.multiLocation.RelativeMultiLocation
import io.novafoundation.nova.feature_xcm_api.multiLocation.junctions
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.chain.model.ChainId
import io.novafoundation.nova.runtime.multiNetwork.getChainOrNull
import java.math.BigInteger
internal class ChildParachainLocationConverter(
private val relayChain: Chain,
private val chainRegistry: ChainRegistry
) : ChainMultiLocationConverter {
private val parachainIdToChainIdByRelay = mapOf(
Chain.Geneses.POLKADOT to mapOf(
1000 to Chain.Geneses.POLKADOT_ASSET_HUB
)
)
override suspend fun toChain(multiLocation: RelativeMultiLocation): Chain? {
// This is not a child parachain from relay point
if (multiLocation.parents != 0) return null
val junctions = multiLocation.junctions
// Child parachain has only 1 ParachainId junction
if (junctions.size != 1) return null
val parachainId = junctions.single() as? ParachainId ?: return null
val parachainChainId = getParachainChainId(parachainId.id) ?: return null
return chainRegistry.getChainOrNull(parachainChainId)
}
private fun getParachainChainId(parachainId: BigInteger): ChainId? {
return parachainIdToChainIdByRelay[relayChain.id]?.get(parachainId.toInt())
}
}
@@ -0,0 +1,15 @@
package io.novafoundation.nova.feature_xcm_impl.converter.chain
import io.novafoundation.nova.common.utils.tryFindNonNull
import io.novafoundation.nova.feature_xcm_api.converter.chain.ChainMultiLocationConverter
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain
import io.novafoundation.nova.feature_xcm_api.multiLocation.RelativeMultiLocation
internal class CompoundChainLocationConverter(
private vararg val delegates: ChainMultiLocationConverter
) : ChainMultiLocationConverter {
override suspend fun toChain(multiLocation: RelativeMultiLocation): Chain? {
return delegates.tryFindNonNull { it.toChain(multiLocation) }
}
}
@@ -0,0 +1,15 @@
package io.novafoundation.nova.feature_xcm_impl.converter.chain
import io.novafoundation.nova.feature_xcm_api.converter.chain.ChainMultiLocationConverter
import io.novafoundation.nova.feature_xcm_api.multiLocation.RelativeMultiLocation
import io.novafoundation.nova.feature_xcm_api.multiLocation.isHere
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain
internal class LocalChainMultiLocationConverter(
val chain: Chain
) : ChainMultiLocationConverter {
override suspend fun toChain(multiLocation: RelativeMultiLocation): Chain? {
return chain.takeIf { multiLocation.isHere() }
}
}
@@ -0,0 +1,21 @@
package io.novafoundation.nova.feature_xcm_impl.converter.chain
import io.novafoundation.nova.common.di.scope.FeatureScope
import io.novafoundation.nova.feature_xcm_api.converter.chain.ChainMultiLocationConverter
import io.novafoundation.nova.feature_xcm_api.converter.chain.ChainMultiLocationConverterFactory
import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain
import javax.inject.Inject
@FeatureScope
class RealChainMultiLocationConverterFactory @Inject constructor(
private val chainRegistry: ChainRegistry
) : ChainMultiLocationConverterFactory {
override fun resolveSelfAndChildrenParachains(self: Chain): ChainMultiLocationConverter {
return CompoundChainLocationConverter(
LocalChainMultiLocationConverter(self),
ChildParachainLocationConverter(self, chainRegistry)
)
}
}
@@ -0,0 +1,35 @@
package io.novafoundation.nova.feature_xcm_impl.di
import dagger.Component
import io.novafoundation.nova.common.di.CommonApi
import io.novafoundation.nova.common.di.scope.FeatureScope
import io.novafoundation.nova.feature_xcm_api.di.XcmFeatureApi
import io.novafoundation.nova.runtime.di.RuntimeApi
@Component(
dependencies = [
XcmFeatureDependencies::class,
],
modules = [
XcmFeatureModule::class
]
)
@FeatureScope
interface XcmFeatureComponent : XcmFeatureApi {
@Component.Factory
interface Factory {
fun create(
deps: XcmFeatureDependencies
): XcmFeatureComponent
}
@Component(
dependencies = [
CommonApi::class,
RuntimeApi::class,
]
)
interface XcmFeatureDependenciesComponent : XcmFeatureDependencies
}
@@ -0,0 +1,11 @@
package io.novafoundation.nova.feature_xcm_impl.di
import io.novafoundation.nova.runtime.call.MultiChainRuntimeCallsApi
import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry
interface XcmFeatureDependencies {
val chainRegistry: ChainRegistry
val runtimeCallApi: MultiChainRuntimeCallsApi
}
@@ -0,0 +1,25 @@
package io.novafoundation.nova.feature_xcm_impl.di
import io.novafoundation.nova.common.di.FeatureApiHolder
import io.novafoundation.nova.common.di.FeatureContainer
import io.novafoundation.nova.common.di.scope.ApplicationScope
import io.novafoundation.nova.runtime.di.RuntimeApi
import javax.inject.Inject
@ApplicationScope
class XcmFeatureHolder @Inject constructor(
featureContainer: FeatureContainer,
) : FeatureApiHolder(featureContainer) {
override fun initializeDependencies(): Any {
val xcmFeatureDependencies = DaggerXcmFeatureComponent_XcmFeatureDependenciesComponent.builder()
.commonApi(commonApi())
.runtimeApi(getFeature(RuntimeApi::class.java))
.build()
return DaggerXcmFeatureComponent.factory()
.create(
deps = xcmFeatureDependencies
)
}
}
@@ -0,0 +1,11 @@
package io.novafoundation.nova.feature_xcm_impl.di
import dagger.Module
import io.novafoundation.nova.feature_xcm_impl.di.modules.BindsModule
@Module(
includes = [
BindsModule::class
]
)
class XcmFeatureModule
@@ -0,0 +1,38 @@
package io.novafoundation.nova.feature_xcm_impl.di.modules
import dagger.Binds
import dagger.Module
import io.novafoundation.nova.feature_xcm_api.builder.XcmBuilder
import io.novafoundation.nova.feature_xcm_api.converter.MultiLocationConverterFactory
import io.novafoundation.nova.feature_xcm_api.converter.chain.ChainMultiLocationConverterFactory
import io.novafoundation.nova.feature_xcm_api.runtimeApi.dryRun.DryRunApi
import io.novafoundation.nova.feature_xcm_api.runtimeApi.xcmPayment.XcmPaymentApi
import io.novafoundation.nova.feature_xcm_api.versions.detector.XcmVersionDetector
import io.novafoundation.nova.feature_xcm_impl.builder.RealXcmBuilderFactory
import io.novafoundation.nova.feature_xcm_impl.converter.RealMultiLocationConverterFactory
import io.novafoundation.nova.feature_xcm_impl.converter.chain.RealChainMultiLocationConverterFactory
import io.novafoundation.nova.feature_xcm_impl.runtimeApi.dryRun.RealDryRunApi
import io.novafoundation.nova.feature_xcm_impl.runtimeApi.xcmPayment.RealXcmPaymentApi
import io.novafoundation.nova.feature_xcm_impl.versions.detector.RealXcmVersionDetector
@Module
internal interface BindsModule {
@Binds
fun bindXcmVersionDetector(real: RealXcmVersionDetector): XcmVersionDetector
@Binds
fun bindChainMultiLocationConverterFactory(real: RealChainMultiLocationConverterFactory): ChainMultiLocationConverterFactory
@Binds
fun bindAssetMultiLocationConverterFactory(real: RealMultiLocationConverterFactory): MultiLocationConverterFactory
@Binds
fun bindDryRunApi(real: RealDryRunApi): DryRunApi
@Binds
fun bindXcmPaymentApi(real: RealXcmPaymentApi): XcmPaymentApi
@Binds
fun bindXcmBuilderFactory(real: RealXcmBuilderFactory): XcmBuilder.Factory
}
@@ -0,0 +1,94 @@
package io.novafoundation.nova.feature_xcm_impl.runtimeApi.dryRun
import io.novafoundation.nova.common.data.network.runtime.binding.ScaleResult
import io.novafoundation.nova.common.di.scope.FeatureScope
import io.novafoundation.nova.common.utils.provideContext
import io.novafoundation.nova.feature_xcm_api.message.VersionedRawXcmMessage
import io.novafoundation.nova.feature_xcm_api.runtimeApi.dryRun.DryRunApi
import io.novafoundation.nova.feature_xcm_api.runtimeApi.dryRun.model.CallDryRunEffects
import io.novafoundation.nova.feature_xcm_api.runtimeApi.dryRun.model.DryRunEffectsResultErr
import io.novafoundation.nova.feature_xcm_api.runtimeApi.dryRun.model.OriginCaller
import io.novafoundation.nova.feature_xcm_api.runtimeApi.dryRun.model.XcmDryRunEffects
import io.novafoundation.nova.feature_xcm_api.versions.VersionedXcmLocation
import io.novafoundation.nova.feature_xcm_api.versions.XcmVersion
import io.novafoundation.nova.feature_xcm_api.versions.toEncodableInstance
import io.novafoundation.nova.runtime.call.MultiChainRuntimeCallsApi
import io.novafoundation.nova.runtime.call.RuntimeCallsApi
import io.novafoundation.nova.runtime.multiNetwork.chain.model.ChainId
import io.novasama.substrate_sdk_android.runtime.definitions.types.generics.GenericCall
import javax.inject.Inject
@FeatureScope
class RealDryRunApi @Inject constructor(
private val multiChainRuntimeCallsApi: MultiChainRuntimeCallsApi
) : DryRunApi {
override suspend fun dryRunXcm(
originLocation: VersionedXcmLocation,
xcm: VersionedRawXcmMessage,
chainId: ChainId
): Result<ScaleResult<XcmDryRunEffects, DryRunEffectsResultErr>> {
return multiChainRuntimeCallsApi.forChain(chainId).dryRunXcm(xcm, originLocation)
}
override suspend fun dryRunCall(
originCaller: OriginCaller,
call: GenericCall.Instance,
xcmResultsVersion: XcmVersion,
chainId: ChainId
): Result<ScaleResult<CallDryRunEffects, DryRunEffectsResultErr>> {
return multiChainRuntimeCallsApi.forChain(chainId).dryRunCall(originCaller, call, xcmResultsVersion)
}
private suspend fun RuntimeCallsApi.dryRunXcm(
xcm: VersionedRawXcmMessage,
origin: VersionedXcmLocation,
): Result<ScaleResult<XcmDryRunEffects, DryRunEffectsResultErr>> {
return runCatching {
call(
section = "DryRunApi",
method = "dry_run_xcm",
arguments = mapOf(
"origin_location" to origin.toEncodableInstance(),
"xcm" to xcm.toEncodableInstance()
),
returnBinding = {
runtime.provideContext {
ScaleResult.bind(
dynamicInstance = it,
bindOk = { XcmDryRunEffects.bind(it) },
bindError = DryRunEffectsResultErr::bind
)
}
}
)
}
}
private suspend fun RuntimeCallsApi.dryRunCall(
originCaller: OriginCaller,
call: GenericCall.Instance,
xcmResultsVersion: XcmVersion,
): Result<ScaleResult<CallDryRunEffects, DryRunEffectsResultErr>> {
return runCatching {
call(
section = "DryRunApi",
method = "dry_run_call",
arguments = mapOf(
"origin" to originCaller.toEncodableInstance(),
"call" to call,
"result_xcms_version" to xcmResultsVersion.version.toBigInteger()
),
returnBinding = {
runtime.provideContext {
ScaleResult.bind(
dynamicInstance = it,
bindOk = { CallDryRunEffects.bind(it) },
bindError = DryRunEffectsResultErr::bind
)
}
}
)
}
}
}
@@ -0,0 +1,52 @@
package io.novafoundation.nova.feature_xcm_impl.runtimeApi.xcmPayment
import io.novafoundation.nova.common.data.network.runtime.binding.ScaleResult
import io.novafoundation.nova.common.data.network.runtime.binding.WeightV2
import io.novafoundation.nova.common.data.network.runtime.binding.bindWeightV2
import io.novafoundation.nova.common.di.scope.FeatureScope
import io.novafoundation.nova.feature_xcm_api.message.VersionedXcmMessage
import io.novafoundation.nova.feature_xcm_api.runtimeApi.xcmPayment.XcmPaymentApi
import io.novafoundation.nova.feature_xcm_api.runtimeApi.xcmPayment.model.QueryXcmWeightErr
import io.novafoundation.nova.feature_xcm_api.versions.toEncodableInstance
import io.novafoundation.nova.runtime.call.MultiChainRuntimeCallsApi
import io.novafoundation.nova.runtime.call.RuntimeCallsApi
import io.novafoundation.nova.runtime.multiNetwork.chain.model.ChainId
import javax.inject.Inject
@FeatureScope
class RealXcmPaymentApi @Inject constructor(
private val multiChainRuntimeCallsApi: MultiChainRuntimeCallsApi,
) : XcmPaymentApi {
override suspend fun queryXcmWeight(
chainId: ChainId,
xcm: VersionedXcmMessage
): Result<ScaleResult<WeightV2, QueryXcmWeightErr>> {
return multiChainRuntimeCallsApi.forChain(chainId).queryXcmWeight(xcm)
}
override suspend fun isSupported(chainId: ChainId): Boolean {
return multiChainRuntimeCallsApi.isSupported(chainId, "XcmPaymentApi", "query_xcm_weight")
}
private suspend fun RuntimeCallsApi.queryXcmWeight(
xcm: VersionedXcmMessage,
): Result<ScaleResult<WeightV2, QueryXcmWeightErr>> {
return runCatching {
call(
section = "XcmPaymentApi",
method = "query_xcm_weight",
arguments = mapOf(
"message" to xcm.toEncodableInstance()
),
returnBinding = {
ScaleResult.bind(
dynamicInstance = it,
bindOk = ::bindWeightV2,
bindError = QueryXcmWeightErr::bind
)
}
)
}
}
}
@@ -0,0 +1,98 @@
package io.novafoundation.nova.feature_xcm_impl.versions.detector
import android.util.Log
import io.novafoundation.nova.common.di.scope.FeatureScope
import io.novafoundation.nova.common.utils.enumValueOfOrNull
import io.novafoundation.nova.common.utils.xcmPalletName
import io.novafoundation.nova.feature_xcm_api.versions.XcmVersion
import io.novafoundation.nova.feature_xcm_api.versions.detector.XcmVersionDetector
import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry
import io.novafoundation.nova.runtime.multiNetwork.chain.model.ChainId
import io.novafoundation.nova.runtime.multiNetwork.withRuntime
import io.novasama.substrate_sdk_android.runtime.definitions.types.RuntimeType
import io.novasama.substrate_sdk_android.runtime.definitions.types.composite.DictEnum
import io.novasama.substrate_sdk_android.runtime.definitions.types.skipAliases
import io.novasama.substrate_sdk_android.runtime.metadata.RuntimeMetadata
import io.novasama.substrate_sdk_android.runtime.metadata.callOrNull
import io.novasama.substrate_sdk_android.runtime.metadata.module.MetadataFunction
import io.novasama.substrate_sdk_android.runtime.metadata.moduleOrNull
import javax.inject.Inject
@FeatureScope
class RealXcmVersionDetector @Inject constructor(
private val chainRegistry: ChainRegistry
) : XcmVersionDetector {
override suspend fun lowestPresentMultiLocationVersion(chainId: ChainId): XcmVersion? {
return lowestPresentXcmTypeVersionFromCallArgument(
chainId = chainId,
getCall = { it.moduleOrNull(it.xcmPalletName())?.callOrNull("reserve_transfer_assets") },
argumentName = "dest"
)
}
override suspend fun lowestPresentMultiAssetsVersion(chainId: ChainId): XcmVersion? {
return lowestPresentXcmTypeVersionFromCallArgument(
chainId = chainId,
getCall = { it.moduleOrNull(it.xcmPalletName())?.callOrNull("reserve_transfer_assets") },
argumentName = "assets"
)
}
override suspend fun lowestPresentMultiAssetIdVersion(chainId: ChainId): XcmVersion? {
return lowestPresentXcmTypeVersionFromCallArgument(
chainId = chainId,
getCall = { it.moduleOrNull(it.xcmPalletName())?.callOrNull("transfer_assets_using_type_and_then") },
argumentName = "remote_fees_id"
)
}
override suspend fun lowestPresentMultiAssetVersion(chainId: ChainId): XcmVersion? {
return lowestPresentMultiAssetsVersion(chainId)
}
override suspend fun detectMultiLocationVersion(chainId: ChainId, multiLocationType: RuntimeType<*, *>?): XcmVersion? {
val actualCheckedType = multiLocationType?.skipAliases() ?: return null
val versionedType = getVersionedType(
chainId = chainId,
getCall = { moduleOrNull(xcmPalletName())?.callOrNull("reserve_transfer_assets") },
argumentName = "dest"
) ?: return null
val matchingEnumEntry = versionedType.elements.values.find { enumEntry -> enumEntry.value.skipAliases().value === actualCheckedType }
?: run {
Log.w("RealPalletXcmRepository", "Failed to find matching variant in versioned multiplication for type ${actualCheckedType.name}")
return null
}
return enumValueOfOrNull<XcmVersion>(matchingEnumEntry.name)?.also {
Log.d("RealPalletXcmRepository", "Identified xcm version for ${actualCheckedType.name} to be ${it.name}")
}
}
private suspend fun lowestPresentXcmTypeVersionFromCallArgument(
chainId: ChainId,
getCall: (RuntimeMetadata) -> MetadataFunction?,
argumentName: String,
): XcmVersion? {
val type = getVersionedType(chainId, getCall, argumentName) ?: return null
val allSupportedVersions = type.elements.values.map { it.name }
val leastSupportedVersion = allSupportedVersions.min()
return enumValueOfOrNull<XcmVersion>(leastSupportedVersion)
}
private suspend fun getVersionedType(
chainId: ChainId,
getCall: RuntimeMetadata.() -> MetadataFunction?,
argumentName: String,
): DictEnum? {
return chainRegistry.withRuntime(chainId) {
val call = getCall(runtime.metadata) ?: return@withRuntime null
val argument = call.arguments.find { it.name == argumentName } ?: return@withRuntime null
argument.type?.skipAliases() as? DictEnum
}
}
}
@@ -0,0 +1,215 @@
package io.novafoundation.nova.feature_xcm_impl.builder
import io.novafoundation.nova.common.address.intoKey
import io.novafoundation.nova.common.data.network.runtime.binding.BalanceOf
import io.novafoundation.nova.feature_xcm_api.asset.MultiAssetFilter.Wild.All
import io.novafoundation.nova.feature_xcm_api.asset.MultiAssetId
import io.novafoundation.nova.feature_xcm_api.asset.intoMultiAssets
import io.novafoundation.nova.feature_xcm_api.asset.withAmount
import io.novafoundation.nova.feature_xcm_api.builder.XcmBuilder
import io.novafoundation.nova.feature_xcm_api.builder.buyExecution
import io.novafoundation.nova.feature_xcm_api.builder.depositAllAssetsTo
import io.novafoundation.nova.feature_xcm_api.builder.fees.MeasureXcmFees
import io.novafoundation.nova.feature_xcm_api.builder.payFees
import io.novafoundation.nova.feature_xcm_api.builder.payFeesIn
import io.novafoundation.nova.feature_xcm_api.builder.transferReserveAsset
import io.novafoundation.nova.feature_xcm_api.builder.withdrawAsset
import io.novafoundation.nova.feature_xcm_api.message.VersionedXcmMessage
import io.novafoundation.nova.feature_xcm_api.message.XcmInstruction.BuyExecution
import io.novafoundation.nova.feature_xcm_api.message.XcmInstruction.DepositAsset
import io.novafoundation.nova.feature_xcm_api.message.XcmInstruction.DepositReserveAsset
import io.novafoundation.nova.feature_xcm_api.message.XcmInstruction.InitiateReserveWithdraw
import io.novafoundation.nova.feature_xcm_api.message.XcmInstruction.PayFees
import io.novafoundation.nova.feature_xcm_api.message.XcmInstruction.TransferReserveAsset
import io.novafoundation.nova.feature_xcm_api.message.XcmInstruction.WithdrawAsset
import io.novafoundation.nova.feature_xcm_api.message.XcmMessage
import io.novafoundation.nova.feature_xcm_api.multiLocation.AssetLocation
import io.novafoundation.nova.feature_xcm_api.multiLocation.ChainLocation
import io.novafoundation.nova.feature_xcm_api.multiLocation.MultiLocation.Interior.Here
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.versionedXcm
import io.novafoundation.nova.feature_xcm_api.weight.WeightLimit
import io.novafoundation.nova.runtime.ext.Geneses
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain
import io.novafoundation.nova.runtime.multiNetwork.chain.model.FullChainAssetId
import junit.framework.TestCase.assertEquals
import kotlinx.coroutines.runBlocking
import org.junit.Test
import java.math.BigInteger
class RealXcmBuilderTest {
val amount = 100000000000000.toBigInteger()
val buyExecutionFees = amount / 2.toBigInteger()
val testMeasuredFees = 10.toBigInteger()
val testExactFees = 11.toBigInteger()
val recipient = ByteArray(32) { 1 }.intoKey()
val polkadot = ChainLocation(Chain.Geneses.POLKADOT, Here.asLocation())
val pah = ChainLocation(Chain.Geneses.POLKADOT_ASSET_HUB, ParachainId(1000).asLocation())
val hydration = ChainLocation(Chain.Geneses.HYDRA_DX, ParachainId(2034).asLocation())
val dot = Here.asLocation()
val dotLocation = AssetLocation(FullChainAssetId(Chain.Geneses.POLKADOT, 0), dot)
val dotOnPolkadot = MultiAssetId(dot.fromPointOfViewOf(polkadot.location))
val dotOnPah = MultiAssetId(dot.fromPointOfViewOf(pah.location))
val dotOnHydration = MultiAssetId(dot.fromPointOfViewOf(hydration.location))
val xcmVersion = XcmVersion.V4
@Test
fun `should build empty message`() = runBlocking {
val expected = XcmMessage(emptyList())
val result = createBuilder(polkadot).build()
assertXcmMessageEquals(expected, result)
}
@Test
fun `should build single chain message`() = runBlocking {
val expected = XcmMessage(
BuyExecution(dotOnPolkadot.withAmount(amount), WeightLimit.Unlimited),
DepositAsset(All, recipient.toMultiLocation())
)
val result = createBuilder(polkadot).apply {
buyExecution(dot, amount, WeightLimit.Unlimited)
depositAllAssetsTo(recipient)
}.build()
assertXcmMessageEquals(expected, result)
}
@Test
fun `should perform single context switch`() = runBlocking {
val forwardedToHydration = XcmMessage(
BuyExecution(dotOnHydration.withAmount(buyExecutionFees), WeightLimit.Unlimited),
DepositAsset(All, recipient.toMultiLocation())
)
val expectedOnPolkadot = XcmMessage(
TransferReserveAsset(
assets = dotOnPolkadot.withAmount(amount).intoMultiAssets(),
dest = hydration.location.fromPointOfViewOf(polkadot.location),
xcm = forwardedToHydration
)
)
val result = createBuilder(polkadot).apply {
// polkadot
transferReserveAsset(dot, amount, hydration)
// hydration
buyExecution(dot, buyExecutionFees, WeightLimit.Unlimited)
depositAllAssetsTo(recipient)
}.build()
assertXcmMessageEquals(expectedOnPolkadot, result)
}
@Test
fun `should perform multiple context switches`() = runBlocking {
val forwardedToHydration = XcmMessage(
BuyExecution(dotOnHydration.withAmount(buyExecutionFees), WeightLimit.Unlimited),
DepositAsset(All, recipient.toMultiLocation())
)
val forwardedToPolkadot = XcmMessage(
BuyExecution(dotOnPolkadot.withAmount(buyExecutionFees), WeightLimit.Unlimited),
DepositReserveAsset(
assets = All,
dest = hydration.location.fromPointOfViewOf(polkadot.location),
xcm = forwardedToHydration
)
)
val expectedOnPah = XcmMessage(
WithdrawAsset(dotOnPah.withAmount(amount).intoMultiAssets()),
InitiateReserveWithdraw(
assets = All,
reserve = polkadot.location.fromPointOfViewOf(pah.location),
xcm = forwardedToPolkadot
)
)
val result = createBuilder(pah).apply {
// on Pah
withdrawAsset(dot, amount)
initiateReserveWithdraw(All, reserve = polkadot)
// on Polkadot
buyExecution(dot, buyExecutionFees, WeightLimit.Unlimited)
depositReserveAsset(All, dest = hydration)
// on Hydration
buyExecution(dot, buyExecutionFees, WeightLimit.Unlimited)
depositAsset(All, recipient)
}.build()
assertXcmMessageEquals(expectedOnPah, result)
}
@Test
fun `should set PayFees in exact mode`() = runBlocking {
val expected = XcmMessage(
PayFees(dotOnPolkadot.withAmount(testExactFees)),
DepositAsset(All, recipient.toMultiLocation())
)
val result = createBuilder(polkadot).apply {
payFees(dotOnPolkadot, testExactFees)
depositAllAssetsTo(recipient)
}.build()
assertXcmMessageEquals(expected, result)
}
@Test
fun `should set PayFees in measured mode`() = runBlocking {
val expected = XcmMessage(
PayFees(dotOnPolkadot.withAmount(testMeasuredFees)),
DepositAsset(All, recipient.toMultiLocation())
)
val expectedForMeasure = XcmMessage(
PayFees(dotOnPolkadot.withAmount(BigInteger.ONE)),
DepositAsset(All, recipient.toMultiLocation())
)
val result = createBuilder(polkadot, validateMeasuringMessage = expectedForMeasure).apply {
payFeesIn(dotLocation)
depositAllAssetsTo(recipient)
}.build()
assertXcmMessageEquals(expected, result)
}
private fun assertXcmMessageEquals(expected: XcmMessage, actual: VersionedXcmMessage) {
assertEquals(expected.versionedXcm(xcmVersion), actual)
}
private fun createBuilder(
origin: ChainLocation,
validateMeasuringMessage: XcmMessage? = null
): XcmBuilder {
return RealXcmBuilder(origin, XcmVersion.V4, TestMeasureFees(validateMeasuringMessage))
}
private inner class TestMeasureFees(
private val validateMeasuringMessage: XcmMessage?
) : MeasureXcmFees {
override suspend fun measureFees(
message: VersionedXcmMessage,
feeAsset: AssetLocation,
chainLocation: ChainLocation
): BalanceOf {
validateMeasuringMessage?.let { assertXcmMessageEquals(validateMeasuringMessage, message) }
return testMeasuredFees
}
}
}