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,41 @@
package io.novafoundation.nova.feature_xcm_api.asset
import io.novafoundation.nova.common.data.network.runtime.binding.castToDictEnum
import io.novafoundation.nova.common.data.network.runtime.binding.castToStruct
import io.novafoundation.nova.feature_xcm_api.multiLocation.bindMultiLocation
import io.novafoundation.nova.feature_xcm_api.versions.VersionedXcm
import io.novafoundation.nova.feature_xcm_api.versions.XcmVersion
import io.novafoundation.nova.feature_xcm_api.versions.bindVersionedXcm
fun bindVersionedLocatableMultiAsset(decoded: Any?): VersionedXcm<LocatableMultiAsset> {
return bindVersionedXcm(decoded, ::bindLocatableMultiAsset)
}
fun bindLocatableMultiAsset(decoded: Any?, xcmVersion: XcmVersion): LocatableMultiAsset {
val asStruct = decoded.castToStruct()
return LocatableMultiAsset(
location = bindMultiLocation(asStruct["location"]),
assetId = bindMultiAssetId(asStruct["asset_id"], xcmVersion)
)
}
fun bindMultiAssetId(decoded: Any?, xcmVersion: XcmVersion): MultiAssetId {
// V4 removed variants of MultiAssetId, leaving only flattened value of Concrete
val locationInstance = if (xcmVersion >= XcmVersion.V4) {
decoded
} else {
extractConcreteLocation(decoded)
}
return MultiAssetId(bindMultiLocation(locationInstance))
}
private fun extractConcreteLocation(decoded: Any?): Any? {
val variant = decoded.castToDictEnum()
require(variant.name == "Concrete") {
"Asset ids besides Concrete are not supported"
}
return variant.value
}
@@ -0,0 +1,13 @@
package io.novafoundation.nova.feature_xcm_api.asset
import io.novafoundation.nova.feature_xcm_api.multiLocation.RelativeMultiLocation
import io.novafoundation.nova.feature_xcm_api.versions.VersionedXcm
class LocatableMultiAsset(
val location: RelativeMultiLocation,
val assetId: MultiAssetId
)
typealias VersionedLocatableMultiAsset = VersionedXcm<LocatableMultiAsset>
@@ -0,0 +1,114 @@
package io.novafoundation.nova.feature_xcm_api.asset
import io.novafoundation.nova.feature_xcm_api.multiLocation.RelativeMultiLocation
import io.novafoundation.nova.common.data.network.runtime.binding.BalanceOf
import io.novafoundation.nova.common.data.network.runtime.binding.bindList
import io.novafoundation.nova.common.data.network.runtime.binding.bindNumber
import io.novafoundation.nova.common.data.network.runtime.binding.cast
import io.novafoundation.nova.common.data.network.runtime.binding.castToDictEnum
import io.novafoundation.nova.common.data.network.runtime.binding.castToStruct
import io.novafoundation.nova.common.data.network.runtime.binding.incompatible
import io.novafoundation.nova.common.utils.scale.ToDynamicScaleInstance
import io.novafoundation.nova.common.utils.structOf
import io.novafoundation.nova.feature_xcm_api.versions.VersionedToDynamicScaleInstance
import io.novafoundation.nova.feature_xcm_api.versions.VersionedXcm
import io.novafoundation.nova.feature_xcm_api.versions.XcmVersion
import io.novafoundation.nova.feature_xcm_api.versions.bindVersionedXcm
import io.novasama.substrate_sdk_android.runtime.definitions.types.composite.DictEnum
import java.math.BigInteger
data class MultiAsset private constructor(
val id: MultiAssetId,
val fungibility: Fungibility,
) : VersionedToDynamicScaleInstance {
companion object {
fun bind(decodedInstance: Any?, xcmVersion: XcmVersion): MultiAsset {
val asStruct = decodedInstance.castToStruct()
return MultiAsset(
id = bindMultiAssetId(asStruct["id"], xcmVersion),
fungibility = Fungibility.bind(asStruct["fun"])
)
}
fun from(
multiLocation: RelativeMultiLocation,
amount: BalanceOf
): MultiAsset {
// Substrate doesn't allow zero balance starting from xcm v3
val positiveAmount = amount.coerceAtLeast(BigInteger.ONE)
return MultiAsset(
id = MultiAssetId(multiLocation),
fungibility = Fungibility.Fungible(positiveAmount)
)
}
}
sealed class Fungibility : ToDynamicScaleInstance {
companion object {
fun bind(decodedInstance: Any?): Fungibility {
val asEnum = decodedInstance.castToDictEnum()
return when (asEnum.name) {
"Fungible" -> Fungible(bindNumber(asEnum.value))
else -> incompatible()
}
}
}
data class Fungible(val amount: BalanceOf) : Fungibility() {
override fun toEncodableInstance(): Any {
return DictEnum.Entry(name = "Fungible", value = amount)
}
}
}
override fun toEncodableInstance(xcmVersion: XcmVersion): Any {
return structOf(
"fun" to fungibility.toEncodableInstance(),
"id" to id.toEncodableInstance(xcmVersion)
)
}
}
fun MultiAsset.requireFungible(): MultiAsset.Fungibility.Fungible {
return fungibility.cast()
}
@JvmInline
value class MultiAssets(val value: List<MultiAsset>) : VersionedToDynamicScaleInstance {
companion object {
fun bind(decodedInstance: Any?, xcmVersion: XcmVersion): MultiAssets {
val assets = bindList(decodedInstance) { MultiAsset.bind(it, xcmVersion) }
return MultiAssets(assets)
}
fun bindVersioned(decodedInstance: Any?): VersionedMultiAssets {
return bindVersionedXcm(decodedInstance, MultiAssets::bind)
}
}
constructor(vararg assets: MultiAsset) : this(assets.toList())
override fun toEncodableInstance(xcmVersion: XcmVersion): Any {
return value.map { it.toEncodableInstance(xcmVersion) }
}
}
fun List<MultiAsset>.intoMultiAssets(): MultiAssets {
return MultiAssets(this)
}
fun MultiAsset.intoMultiAssets(): MultiAssets {
return MultiAssets(listOf(this))
}
typealias VersionedMultiAsset = VersionedXcm<MultiAsset>
typealias VersionedMultiAssets = VersionedXcm<MultiAssets>
@@ -0,0 +1,70 @@
package io.novafoundation.nova.feature_xcm_api.asset
import io.novafoundation.nova.feature_xcm_api.versions.VersionedToDynamicScaleInstance
import io.novafoundation.nova.feature_xcm_api.versions.XcmVersion
import io.novasama.substrate_sdk_android.runtime.definitions.types.composite.DictEnum
sealed class MultiAssetFilter : VersionedToDynamicScaleInstance {
companion object {
fun singleCounted(): Wild.AllCounted {
return Wild.AllCounted(assetsCount = 1)
}
}
class Definite(val assets: MultiAssets) : MultiAssetFilter() {
constructor(asset: MultiAsset) : this(asset.intoMultiAssets())
override fun toEncodableInstance(xcmVersion: XcmVersion): Any? {
return DictEnum.Entry("Definite", assets.toEncodableInstance(xcmVersion))
}
}
sealed class Wild : MultiAssetFilter() {
/**
* Filter to use all assets from the holding register
*
* !!! Important !!!
* Weight of this instruction is bounded by maximum number of assets usable per instruction,
* which can be 100 in some runtimes.
* This might result in sever overestimation of instruction weight and thus, the fee.
*
* Please use other variations that put explicit desired bound like [AllCounted] whenever possible
*/
object All : Wild() {
override fun toString(): String {
return "All"
}
override fun toEncodableInstance(xcmVersion: XcmVersion): Any {
return DictEnum.Entry(
name = "Wild",
value = DictEnum.Entry(
name = "All",
value = null
)
)
}
}
/**
* Filter to use first [assetsCount] assets from the holding register
*/
data class AllCounted(val assetsCount: Int) : Wild() {
override fun toEncodableInstance(xcmVersion: XcmVersion): Any {
return DictEnum.Entry(
name = "Wild",
value = DictEnum.Entry(
name = "AllCounted",
value = assetsCount.toBigInteger()
)
)
}
}
}
}
@@ -0,0 +1,27 @@
package io.novafoundation.nova.feature_xcm_api.asset
import io.novafoundation.nova.common.data.network.runtime.binding.BalanceOf
import io.novafoundation.nova.feature_xcm_api.multiLocation.RelativeMultiLocation
import io.novafoundation.nova.feature_xcm_api.versions.VersionedToDynamicScaleInstance
import io.novafoundation.nova.feature_xcm_api.versions.XcmVersion
import io.novasama.substrate_sdk_android.runtime.definitions.types.composite.DictEnum
@JvmInline
value class MultiAssetId(val multiLocation: RelativeMultiLocation) : VersionedToDynamicScaleInstance {
override fun toEncodableInstance(xcmVersion: XcmVersion): Any? {
// V4 removed variants of MultiAssetId, leaving only flattened value of Concrete
return if (xcmVersion >= XcmVersion.V4) {
multiLocation.toEncodableInstance(xcmVersion)
} else {
DictEnum.Entry(
name = "Concrete",
value = multiLocation.toEncodableInstance(xcmVersion)
)
}
}
}
fun MultiAssetId.withAmount(amount: BalanceOf): MultiAsset {
return MultiAsset.from(multiLocation, amount)
}
@@ -0,0 +1,99 @@
package io.novafoundation.nova.feature_xcm_api.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.MultiAssetFilter.Wild.All
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.intoMultiAssets
import io.novafoundation.nova.feature_xcm_api.asset.withAmount
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.builder.fees.UnsupportedMeasureXcmFees
import io.novafoundation.nova.feature_xcm_api.message.VersionedXcmMessage
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.versions.XcmVersion
import io.novafoundation.nova.feature_xcm_api.weight.WeightLimit
interface XcmBuilder : XcmContext {
interface Factory {
fun create(
initial: ChainLocation,
xcmVersion: XcmVersion,
measureXcmFees: MeasureXcmFees
): XcmBuilder
}
fun payFees(payFeesMode: PayFeesMode)
fun withdrawAsset(assets: MultiAssets)
fun buyExecution(fees: MultiAsset, weightLimit: WeightLimit)
// We only support depositing to a accountId. We might extend it in the future with no issues
// but we keep the support limited to simplify implementation
fun depositAsset(assets: MultiAssetFilter, beneficiary: AccountIdKey)
// Performs context change
fun transferReserveAsset(assets: MultiAssets, dest: ChainLocation)
// Performs context change
fun initiateReserveWithdraw(assets: MultiAssetFilter, reserve: ChainLocation)
// Performs context change
fun depositReserveAsset(assets: MultiAssetFilter, dest: ChainLocation)
fun initiateTeleport(assets: MultiAssetFilter, dest: ChainLocation)
suspend fun build(): VersionedXcmMessage
}
/**
* Can be used when `payFees` is not expected to be used
*/
fun XcmBuilder.Factory.createWithoutFeesMeasurement(
initial: ChainLocation,
xcmVersion: XcmVersion,
): XcmBuilder {
return create(initial, xcmVersion, UnsupportedMeasureXcmFees())
}
suspend fun XcmBuilder.Factory.buildXcmWithoutFeesMeasurement(
initial: ChainLocation,
xcmVersion: XcmVersion,
building: XcmBuilder.() -> Unit
): VersionedXcmMessage {
return createWithoutFeesMeasurement(initial, xcmVersion)
.apply(building)
.build()
}
fun XcmBuilder.withdrawAsset(asset: AbsoluteMultiLocation, amount: BalanceOf) {
withdrawAsset(MultiAsset.from(asset.relativeToLocal(), amount).intoMultiAssets())
}
fun XcmBuilder.transferReserveAsset(asset: AbsoluteMultiLocation, amount: BalanceOf, dest: ChainLocation) {
transferReserveAsset(MultiAsset.from(asset.relativeToLocal(), amount).intoMultiAssets(), dest)
}
fun XcmBuilder.buyExecution(asset: AbsoluteMultiLocation, amount: BalanceOf, weightLimit: WeightLimit) {
buyExecution(MultiAsset.from(asset.relativeToLocal(), amount), weightLimit)
}
fun XcmBuilder.depositAllAssetsTo(beneficiary: AccountIdKey) {
depositAsset(All, beneficiary)
}
fun XcmBuilder.payFeesIn(assetId: AssetLocation) {
payFees(PayFeesMode.Measured(assetId))
}
fun XcmBuilder.payFees(assetId: MultiAssetId, exactFees: BalanceOf) {
payFees(PayFeesMode.Exact(assetId.withAmount(exactFees)))
}
@@ -0,0 +1,22 @@
package io.novafoundation.nova.feature_xcm_api.builder
import io.novafoundation.nova.feature_xcm_api.multiLocation.AbsoluteMultiLocation
import io.novafoundation.nova.feature_xcm_api.multiLocation.ChainLocation
import io.novafoundation.nova.feature_xcm_api.multiLocation.RelativeMultiLocation
import io.novafoundation.nova.feature_xcm_api.versions.XcmVersion
interface XcmContext {
val xcmVersion: XcmVersion
val currentLocation: ChainLocation
}
fun XcmContext.localViewOf(location: AbsoluteMultiLocation): RelativeMultiLocation {
return location.fromPointOfViewOf(currentLocation.location)
}
context(XcmContext)
fun AbsoluteMultiLocation.relativeToLocal(): RelativeMultiLocation {
return localViewOf(this)
}
@@ -0,0 +1,20 @@
package io.novafoundation.nova.feature_xcm_api.builder.fees
import io.novafoundation.nova.common.data.network.runtime.binding.BalanceOf
import io.novafoundation.nova.feature_xcm_api.builder.XcmBuilder
import io.novafoundation.nova.feature_xcm_api.message.VersionedXcmMessage
import io.novafoundation.nova.feature_xcm_api.multiLocation.AssetLocation
import io.novafoundation.nova.feature_xcm_api.multiLocation.ChainLocation
/**
* Measure fees for a given xcm message. Used by [XcmBuilder] when processing [XcmBuilder.payFees]
* with [PayFeesMode.Measured] specified
*/
interface MeasureXcmFees {
suspend fun measureFees(
message: VersionedXcmMessage,
feeAsset: AssetLocation,
chainLocation: ChainLocation,
): BalanceOf
}
@@ -0,0 +1,21 @@
package io.novafoundation.nova.feature_xcm_api.builder.fees
import io.novafoundation.nova.feature_xcm_api.asset.MultiAsset
import io.novafoundation.nova.feature_xcm_api.builder.XcmBuilder
import io.novafoundation.nova.feature_xcm_api.multiLocation.AssetLocation
/**
* Specifies how [XcmBuilder] should specify fees for PayFees instruction
*/
sealed class PayFeesMode {
/**
* Fees should be measured when building the xcm by calling provided [MeasureXcmFees] implementation
*/
class Measured(val feeAssetId: AssetLocation) : PayFeesMode()
/**
* Should use exactly [fee] when specifying fees
*/
class Exact(val fee: MultiAsset) : PayFeesMode()
}
@@ -0,0 +1,16 @@
package io.novafoundation.nova.feature_xcm_api.builder.fees
import io.novafoundation.nova.common.data.network.runtime.binding.BalanceOf
import io.novafoundation.nova.feature_xcm_api.message.VersionedXcmMessage
import io.novafoundation.nova.feature_xcm_api.multiLocation.AssetLocation
import io.novafoundation.nova.feature_xcm_api.multiLocation.ChainLocation
class UnsupportedMeasureXcmFees : MeasureXcmFees {
override suspend fun measureFees(
message: VersionedXcmMessage,
feeAsset: AssetLocation,
chainLocation: ChainLocation
): BalanceOf {
error("Measurement not supported")
}
}
@@ -0,0 +1,28 @@
package io.novafoundation.nova.feature_xcm_api.chain
import io.novafoundation.nova.feature_xcm_api.multiLocation.AbsoluteMultiLocation
import io.novafoundation.nova.feature_xcm_api.multiLocation.ChainLocation
import io.novafoundation.nova.feature_xcm_api.multiLocation.chainLocation
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain
import java.math.BigInteger
class XcmChain(
val parachainId: BigInteger?,
val chain: Chain
)
fun XcmChain.absoluteLocation(): AbsoluteMultiLocation {
return AbsoluteMultiLocation.chainLocation(parachainId)
}
fun XcmChain.isRelay(): Boolean {
return parachainId == null
}
fun XcmChain.isSystemChain(): Boolean {
return parachainId != null && parachainId.toInt() in 1000 until 2000
}
fun XcmChain.chainLocation(): ChainLocation {
return ChainLocation(chain.id, absoluteLocation())
}
@@ -0,0 +1,15 @@
package io.novafoundation.nova.feature_xcm_api.converter
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain
import io.novafoundation.nova.feature_xcm_api.multiLocation.RelativeMultiLocation
interface MultiLocationConverter {
suspend fun toMultiLocation(chainAsset: Chain.Asset): RelativeMultiLocation?
suspend fun toChainAsset(multiLocation: RelativeMultiLocation): Chain.Asset?
}
suspend fun MultiLocationConverter.toMultiLocationOrThrow(chainAsset: Chain.Asset): RelativeMultiLocation {
return toMultiLocation(chainAsset) ?: error("Failed to convert asset location")
}
@@ -0,0 +1,13 @@
package io.novafoundation.nova.feature_xcm_api.converter
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain
import kotlinx.coroutines.CoroutineScope
interface MultiLocationConverterFactory {
fun defaultAsync(chain: Chain, coroutineScope: CoroutineScope): MultiLocationConverter
suspend fun defaultSync(chain: Chain): MultiLocationConverter
suspend fun resolveLocalAssets(chain: Chain): MultiLocationConverter
}
@@ -0,0 +1,9 @@
package io.novafoundation.nova.feature_xcm_api.converter.chain
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain
import io.novafoundation.nova.feature_xcm_api.multiLocation.RelativeMultiLocation
interface ChainMultiLocationConverter {
suspend fun toChain(multiLocation: RelativeMultiLocation): Chain?
}
@@ -0,0 +1,8 @@
package io.novafoundation.nova.feature_xcm_api.converter.chain
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain
interface ChainMultiLocationConverterFactory {
fun resolveSelfAndChildrenParachains(self: Chain): ChainMultiLocationConverter
}
@@ -0,0 +1,23 @@
package io.novafoundation.nova.feature_xcm_api.di
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
interface XcmFeatureApi {
val assetMultiLocationConverterFactory: MultiLocationConverterFactory
val chainMultiLocationConverterFactory: ChainMultiLocationConverterFactory
val xcmVersionDetector: XcmVersionDetector
val dryRunApi: DryRunApi
val xcmPaymentApi: XcmPaymentApi
val xcmBuilderFactory: XcmBuilder.Factory
}
@@ -0,0 +1,24 @@
package io.novafoundation.nova.feature_xcm_api.extrinsic
import io.novafoundation.nova.common.data.network.runtime.binding.WeightV2
import io.novafoundation.nova.common.utils.RuntimeContext
import io.novafoundation.nova.common.utils.composeCall
import io.novafoundation.nova.common.utils.xcmPalletName
import io.novafoundation.nova.feature_xcm_api.message.VersionedXcmMessage
import io.novafoundation.nova.feature_xcm_api.versions.toEncodableInstance
import io.novasama.substrate_sdk_android.runtime.definitions.types.generics.GenericCall
context(RuntimeContext)
fun composeXcmExecute(
message: VersionedXcmMessage,
maxWeight: WeightV2,
): GenericCall.Instance {
return composeCall(
moduleName = runtime.metadata.xcmPalletName(),
callName = "execute",
arguments = mapOf(
"message" to message.toEncodableInstance(),
"max_weight" to maxWeight.toEncodableInstance()
)
)
}
@@ -0,0 +1,188 @@
package io.novafoundation.nova.feature_xcm_api.message
import io.novafoundation.nova.common.utils.structOf
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.MultiAssets
import io.novafoundation.nova.feature_xcm_api.multiLocation.RelativeMultiLocation
import io.novafoundation.nova.feature_xcm_api.versions.VersionedToDynamicScaleInstance
import io.novafoundation.nova.feature_xcm_api.versions.VersionedXcm
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.novasama.substrate_sdk_android.runtime.definitions.types.composite.DictEnum
import java.math.BigInteger
@JvmInline
value class XcmMessage(val instructions: List<XcmInstruction>) : VersionedToDynamicScaleInstance {
constructor(vararg instructions: XcmInstruction) : this(instructions.toList())
override fun toEncodableInstance(xcmVersion: XcmVersion): Any? {
return instructions.map { it.toEncodableInstance(xcmVersion) }
}
}
fun List<XcmInstruction>.asXcmMessage(): XcmMessage = XcmMessage(this)
fun List<XcmInstruction>.asVersionedXcmMessage(version: XcmVersion): VersionedXcmMessage = asXcmMessage().versionedXcm(version)
sealed class XcmInstruction : VersionedToDynamicScaleInstance {
data class WithdrawAsset(val assets: MultiAssets) : XcmInstruction() {
override fun toEncodableInstance(xcmVersion: XcmVersion): Any? {
return DictEnum.Entry(
name = "WithdrawAsset",
value = assets.toEncodableInstance(xcmVersion)
)
}
}
data class DepositAsset(
val assets: MultiAssetFilter,
val beneficiary: RelativeMultiLocation
) : XcmInstruction() {
override fun toEncodableInstance(xcmVersion: XcmVersion): Any? {
return DictEnum.Entry(
name = "DepositAsset",
value = structOf(
"assets" to assets.toEncodableInstance(xcmVersion),
"beneficiary" to beneficiary.toEncodableInstance(xcmVersion),
// Used in XCM V2 and below. We put 1 here since we only support cases for transferring a single asset
"max_assets" to BigInteger.ONE,
)
)
}
}
data class BuyExecution(val fees: MultiAsset, val weightLimit: WeightLimit) : XcmInstruction() {
override fun toEncodableInstance(xcmVersion: XcmVersion): Any {
return DictEnum.Entry(
name = "BuyExecution",
value = structOf(
"fees" to fees.toEncodableInstance(xcmVersion),
// xcm v2 always uses v1 weights
"weight_limit" to weightLimit.toEncodableInstance()
)
)
}
}
object ClearOrigin : XcmInstruction() {
override fun toEncodableInstance(xcmVersion: XcmVersion): Any {
return DictEnum.Entry(
name = "ClearOrigin",
value = null
)
}
}
data class ReserveAssetDeposited(val assets: MultiAssets) : XcmInstruction() {
override fun toEncodableInstance(xcmVersion: XcmVersion): Any {
return DictEnum.Entry(
name = "ReserveAssetDeposited",
value = assets.toEncodableInstance(xcmVersion)
)
}
}
data class ReceiveTeleportedAsset(val assets: MultiAssets) : XcmInstruction() {
override fun toEncodableInstance(xcmVersion: XcmVersion): Any {
return DictEnum.Entry(
name = "ReceiveTeleportedAsset",
value = assets.toEncodableInstance(xcmVersion)
)
}
}
data class InitiateReserveWithdraw(
val assets: MultiAssetFilter,
val reserve: RelativeMultiLocation,
val xcm: XcmMessage
) : XcmInstruction() {
override fun toEncodableInstance(xcmVersion: XcmVersion): Any {
return DictEnum.Entry(
name = "InitiateReserveWithdraw",
value = structOf(
"assets" to assets.toEncodableInstance(xcmVersion),
"reserve" to reserve.toEncodableInstance(xcmVersion),
"xcm" to xcm.toEncodableInstance(xcmVersion)
)
)
}
}
data class TransferReserveAsset(
val assets: MultiAssets,
val dest: RelativeMultiLocation,
val xcm: XcmMessage
) : XcmInstruction() {
override fun toEncodableInstance(xcmVersion: XcmVersion): Any {
return DictEnum.Entry(
name = "TransferReserveAsset",
value = structOf(
"assets" to assets.toEncodableInstance(xcmVersion),
"dest" to dest.toEncodableInstance(xcmVersion),
"xcm" to xcm.toEncodableInstance(xcmVersion)
)
)
}
}
data class DepositReserveAsset(
val assets: MultiAssetFilter,
val dest: RelativeMultiLocation,
val xcm: XcmMessage
) : XcmInstruction() {
override fun toEncodableInstance(xcmVersion: XcmVersion): Any {
return DictEnum.Entry(
name = "DepositReserveAsset",
value = structOf(
"assets" to assets.toEncodableInstance(xcmVersion),
// Used in XCM V2 and below. We put 1 here since we only support cases for transferring a single asset
"max_assets" to BigInteger.ONE,
"dest" to dest.toEncodableInstance(xcmVersion),
"xcm" to xcm.toEncodableInstance(xcmVersion)
)
)
}
}
data class PayFees(val fees: MultiAsset) : XcmInstruction() {
override fun toEncodableInstance(xcmVersion: XcmVersion): Any {
return DictEnum.Entry(
name = "PayFees",
value = structOf(
"fees" to fees.toEncodableInstance(xcmVersion)
)
)
}
}
data class InitiateTeleport(
val assets: MultiAssetFilter,
val dest: RelativeMultiLocation,
val xcm: XcmMessage
) : XcmInstruction() {
override fun toEncodableInstance(xcmVersion: XcmVersion): Any {
return DictEnum.Entry(
name = "InitiateTeleport",
value = structOf(
"assets" to assets.toEncodableInstance(xcmVersion),
"dest" to dest.toEncodableInstance(xcmVersion),
"xcm" to xcm.toEncodableInstance(xcmVersion)
)
)
}
}
}
typealias VersionedXcmMessage = VersionedXcm<XcmMessage>
@@ -0,0 +1,14 @@
package io.novafoundation.nova.feature_xcm_api.message
import io.novafoundation.nova.common.utils.scale.DynamicScaleInstance
import io.novafoundation.nova.feature_xcm_api.versions.VersionedXcm
import io.novafoundation.nova.feature_xcm_api.versions.bindVersionedXcm
typealias XcmMessageRaw = DynamicScaleInstance
typealias VersionedRawXcmMessage = VersionedXcm<XcmMessageRaw>
fun bindVersionedRawXcmMessage(decodedInstance: Any?) = bindVersionedXcm(decodedInstance) { inner, _ ->
DynamicScaleInstance(inner)
}
fun bindRawXcmMessage(decodedInstance: Any?) = DynamicScaleInstance(decodedInstance)
@@ -0,0 +1,46 @@
package io.novafoundation.nova.feature_xcm_api.multiLocation
import io.novafoundation.nova.common.data.network.runtime.binding.ParaId
import io.novafoundation.nova.common.utils.collectionIndexOrNull
class AbsoluteMultiLocation(
interior: Interior,
) : MultiLocation(interior) {
companion object;
constructor(vararg junctions: Junction) : this(junctions.toList().toInterior())
fun toRelative(): RelativeMultiLocation {
return RelativeMultiLocation(parents = 0, interior = interior)
}
/**
* Reanchor given location to a point of view of given `pov` location
* Basic algorithm idea:
* We find the last common ancestor and consider the target location to be "up to ancestor and down to self":
* 1. Find last common ancestor between `this` and `pov`
* 2. Use all junctions after common ancestor as result junctions
* 3. Use difference between len(target.junctions) and common_ancestor_idx
* to determine how many "up" hops are needed to reach common ancestor
*/
fun fromPointOfViewOf(pov: AbsoluteMultiLocation): RelativeMultiLocation {
val lastCommonIndex = findLastCommonJunctionIndex(pov)
val firstDistinctIndex = lastCommonIndex?.let { it + 1 } ?: 0
val parents = pov.junctions.size - firstDistinctIndex
val junctions = junctions.drop(firstDistinctIndex)
return RelativeMultiLocation(parents, junctions.toInterior())
}
private fun findLastCommonJunctionIndex(other: AbsoluteMultiLocation): Int? {
return junctions.zip(other.junctions).indexOfLast { (selfJunction, otherJunction) ->
selfJunction == otherJunction
}.collectionIndexOrNull()
}
}
fun AbsoluteMultiLocation.Companion.chainLocation(parachainId: ParaId?): AbsoluteMultiLocation {
return listOfNotNull(parachainId?.let(MultiLocation.Junction::ParachainId)).asLocation()
}
@@ -0,0 +1,14 @@
package io.novafoundation.nova.feature_xcm_api.multiLocation
import io.novafoundation.nova.feature_xcm_api.asset.MultiAssetId
import io.novafoundation.nova.runtime.multiNetwork.chain.model.FullChainAssetId
class AssetLocation(
val assetId: FullChainAssetId,
val location: AbsoluteMultiLocation
)
fun AssetLocation.multiAssetIdOn(chainLocation: ChainLocation): MultiAssetId {
val relativeMultiLocation = location.fromPointOfViewOf(chainLocation.location)
return MultiAssetId(relativeMultiLocation)
}
@@ -0,0 +1,8 @@
package io.novafoundation.nova.feature_xcm_api.multiLocation
import io.novafoundation.nova.runtime.multiNetwork.chain.model.ChainId
class ChainLocation(
val chainId: ChainId,
val location: AbsoluteMultiLocation
)
@@ -0,0 +1,159 @@
package io.novafoundation.nova.feature_xcm_api.multiLocation
import io.novafoundation.nova.common.address.AccountIdKey
import io.novafoundation.nova.common.data.network.runtime.binding.ParaId
import io.novafoundation.nova.common.utils.HexString
import io.novafoundation.nova.common.utils.isAscending
import io.novafoundation.nova.feature_xcm_api.multiLocation.MultiLocation.Junction
import io.novafoundation.nova.runtime.multiNetwork.chain.model.ChainId
import io.novasama.substrate_sdk_android.extensions.tryFindNonNull
import java.math.BigInteger
abstract class MultiLocation(
open val interior: Interior
) {
sealed class Interior {
object Here : Interior()
class Junctions(junctions: List<Junction>) : Interior() {
val junctions = junctions.sorted()
override fun equals(other: Any?): Boolean {
if (other !is Junctions) return false
return junctions == other.junctions
}
override fun hashCode(): Int {
return junctions.hashCode()
}
override fun toString(): String {
return junctions.toString()
}
}
}
sealed class Junction {
data class ParachainId(val id: ParaId) : Junction() {
constructor(id: Int) : this(id.toBigInteger())
}
data class GeneralKey(val key: HexString) : Junction()
data class PalletInstance(val index: BigInteger) : Junction()
data class GeneralIndex(val index: BigInteger) : Junction()
data class AccountKey20(val accountId: AccountIdKey) : Junction()
data class AccountId32(val accountId: AccountIdKey) : Junction()
data class GlobalConsensus(val networkId: NetworkId) : Junction()
object Unsupported : Junction()
}
sealed class NetworkId {
data class Substrate(val genesisHash: ChainId) : NetworkId()
data class Ethereum(val chainId: Int) : NetworkId()
}
}
val Junction.order
get() = when (this) {
is Junction.GlobalConsensus -> 0
is Junction.ParachainId -> 1
// All of these are on the same "level" - mutually exhaustive
is Junction.PalletInstance,
is Junction.AccountKey20,
is Junction.AccountId32 -> 2
is Junction.GeneralKey,
is Junction.GeneralIndex -> 3
Junction.Unsupported -> Int.MAX_VALUE
}
val MultiLocation.junctions: List<Junction>
get() = when (val interior = interior) {
MultiLocation.Interior.Here -> emptyList()
is MultiLocation.Interior.Junctions -> interior.junctions
}
fun List<Junction>.toInterior() = when (size) {
0 -> MultiLocation.Interior.Here
else -> MultiLocation.Interior.Junctions(this)
}
fun Junction.toInterior() = MultiLocation.Interior.Junctions(listOf(this))
fun MultiLocation.Interior.isHere() = this is MultiLocation.Interior.Here
fun MultiLocation.accountId(): AccountIdKey? {
return junctions.tryFindNonNull {
when (it) {
is Junction.AccountId32 -> it.accountId
is Junction.AccountKey20 -> it.accountId
else -> null
}
}
}
fun MultiLocation.Interior.asLocation(): AbsoluteMultiLocation {
return AbsoluteMultiLocation(this)
}
fun List<Junction>.asLocation(): AbsoluteMultiLocation {
return toInterior().asLocation()
}
fun Junction.asLocation(): AbsoluteMultiLocation {
return toInterior().asLocation()
}
operator fun RelativeMultiLocation.plus(suffix: RelativeMultiLocation): RelativeMultiLocation {
require(suffix.parents == 0) {
"Appending multi location that has parents is not supported"
}
val newJunctions = junctions + suffix.junctions
require(newJunctions.isAscending(compareBy { it.order })) {
"Cannot append this multi location due to conflicting junctions"
}
return RelativeMultiLocation(
parents = parents,
interior = newJunctions.toInterior()
)
}
fun AccountIdKey.toMultiLocation() = RelativeMultiLocation(
parents = 0,
interior = Junctions(
when (value.size) {
32 -> Junction.AccountId32(this)
20 -> Junction.AccountKey20(this)
else -> throw IllegalArgumentException("Unsupported account id length: ${value.size}")
}
)
)
fun Junctions(vararg junctions: Junction) = MultiLocation.Interior.Junctions(junctions.toList())
fun MultiLocation.paraIdOrNull(): ParaId? {
return junctions.filterIsInstance<Junction.ParachainId>()
.firstOrNull()
?.id
}
private fun List<Junction>.sorted(): List<Junction> {
return sortedBy(Junction::order)
}
@@ -0,0 +1,206 @@
package io.novafoundation.nova.feature_xcm_api.multiLocation
import io.novafoundation.nova.common.address.AccountIdKey
import io.novafoundation.nova.common.address.intoKey
import io.novafoundation.nova.common.data.network.runtime.binding.bindAccountId
import io.novafoundation.nova.common.data.network.runtime.binding.bindByteArray
import io.novafoundation.nova.common.data.network.runtime.binding.bindInt
import io.novafoundation.nova.common.data.network.runtime.binding.bindList
import io.novafoundation.nova.common.data.network.runtime.binding.bindNumber
import io.novafoundation.nova.common.data.network.runtime.binding.castToDictEnum
import io.novafoundation.nova.common.data.network.runtime.binding.castToStruct
import io.novafoundation.nova.common.utils.HexString
import io.novafoundation.nova.common.utils.padEnd
import io.novafoundation.nova.common.utils.structOf
import io.novafoundation.nova.feature_xcm_api.multiLocation.MultiLocation.Junction
import io.novafoundation.nova.feature_xcm_api.multiLocation.MultiLocation.NetworkId
import io.novafoundation.nova.feature_xcm_api.versions.VersionedXcm
import io.novafoundation.nova.feature_xcm_api.versions.XcmVersion
import io.novafoundation.nova.feature_xcm_api.versions.bindVersionedXcm
import io.novafoundation.nova.runtime.ext.Geneses
import io.novafoundation.nova.runtime.ext.Ids
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain
import io.novasama.substrate_sdk_android.encrypt.json.copyBytes
import io.novasama.substrate_sdk_android.extensions.fromHex
import io.novasama.substrate_sdk_android.extensions.toHexString
import io.novasama.substrate_sdk_android.runtime.definitions.types.composite.DictEnum
import io.novasama.substrate_sdk_android.runtime.definitions.types.composite.Struct
// ------ Decode ------
fun bindMultiLocation(instance: Any?): RelativeMultiLocation {
val asStruct = instance.castToStruct()
return RelativeMultiLocation(
parents = bindInt(asStruct["parents"]),
interior = bindInterior((asStruct["interior"]))
)
}
fun bindVersionedMultiLocation(instance: Any?): VersionedXcm<RelativeMultiLocation> {
return bindVersionedXcm(instance) { inner, _ -> bindMultiLocation(inner) }
}
private fun bindInterior(instance: Any?): MultiLocation.Interior {
val asDictEnum = instance.castToDictEnum()
return when (asDictEnum.name) {
"Here" -> MultiLocation.Interior.Here
else -> {
val junctions = bindJunctions(asDictEnum.value)
MultiLocation.Interior.Junctions(junctions)
}
}
}
private fun bindJunctions(instance: Any?): List<Junction> {
// Note that Interior.X1 is encoded differently in XCM v3 (a single junction) and V4 (single-element list)
if (instance is List<*>) {
return bindList(instance, ::bindJunction)
} else {
return listOf(bindJunction(instance))
}
}
private fun bindJunction(instance: Any?): Junction {
val asDictEnum = instance.castToDictEnum()
return when (asDictEnum.name) {
"GeneralKey" -> Junction.GeneralKey(bindGeneralKey(asDictEnum.value))
"PalletInstance" -> Junction.PalletInstance(bindNumber(asDictEnum.value))
"Parachain" -> Junction.ParachainId(bindNumber(asDictEnum.value))
"GeneralIndex" -> Junction.GeneralIndex(bindNumber(asDictEnum.value))
"GlobalConsensus" -> bindGlobalConsensusJunction(asDictEnum.value)
"AccountKey20" -> Junction.AccountKey20(bindAccountIdJunction(asDictEnum.value, accountIdKey = "key"))
"AccountId32" -> Junction.AccountId32(bindAccountIdJunction(asDictEnum.value, accountIdKey = "id"))
else -> Junction.Unsupported
}
}
private fun bindGeneralKey(instance: Any?): HexString {
val keyBytes = if (instance is Struct.Instance) {
// v3+ structure
val keyLength = bindInt(instance["length"])
val keyPadded = bindByteArray(instance["data"])
keyPadded.copyBytes(0, keyLength)
} else {
bindByteArray(instance)
}
return keyBytes.toHexString(withPrefix = true)
}
private fun bindAccountIdJunction(instance: Any?, accountIdKey: String): AccountIdKey {
val asStruct = instance.castToStruct()
return bindAccountId(asStruct[accountIdKey]).intoKey()
}
private fun bindGlobalConsensusJunction(instance: Any?): Junction {
val asDictEnum = instance.castToDictEnum()
return when (asDictEnum.name) {
"ByGenesis" -> {
val genesis = bindByteArray(asDictEnum.value).toHexString(withPrefix = false)
Junction.GlobalConsensus(networkId = NetworkId.Substrate(genesis))
}
"Polkadot" -> Junction.GlobalConsensus(NetworkId.Substrate(Chain.Geneses.POLKADOT))
"Kusama" -> Junction.GlobalConsensus(NetworkId.Substrate(Chain.Geneses.KUSAMA))
"Westend" -> Junction.GlobalConsensus(NetworkId.Substrate(Chain.Geneses.WESTEND))
"Ethereum" -> {
val chainId = bindInt(asDictEnum.value.castToStruct()["chain_id"])
Junction.GlobalConsensus(NetworkId.Ethereum(chainId))
}
else -> Junction.Unsupported
}
}
// ------ Encode ------
internal fun RelativeMultiLocation.toEncodableInstanceExt(xcmVersion: XcmVersion) = structOf(
"parents" to parents.toBigInteger(),
"interior" to interior.toEncodableInstance(xcmVersion)
)
private fun MultiLocation.Interior.toEncodableInstance(xcmVersion: XcmVersion) = when (this) {
MultiLocation.Interior.Here -> DictEnum.Entry("Here", null)
is MultiLocation.Interior.Junctions -> if (junctions.size == 1 && xcmVersion <= XcmVersion.V3) {
// X1 is encoded as a single junction in V3 and prior
DictEnum.Entry(
name = "X1",
value = junctions.single().toEncodableInstance(xcmVersion)
)
} else {
DictEnum.Entry(
name = "X${junctions.size}",
value = junctions.map { it.toEncodableInstance(xcmVersion) }
)
}
}
private fun Junction.toEncodableInstance(xcmVersion: XcmVersion) = when (this) {
is Junction.GeneralKey -> DictEnum.Entry("GeneralKey", encodableGeneralKey(xcmVersion, key))
is Junction.PalletInstance -> DictEnum.Entry("PalletInstance", index)
is Junction.ParachainId -> DictEnum.Entry("Parachain", id)
is Junction.AccountKey20 -> DictEnum.Entry("AccountKey20", accountId.toJunctionAccountIdInstance(accountIdKey = "key", xcmVersion))
is Junction.AccountId32 -> DictEnum.Entry("AccountId32", accountId.toJunctionAccountIdInstance(accountIdKey = "id", xcmVersion))
is Junction.GeneralIndex -> DictEnum.Entry("GeneralIndex", index)
is Junction.GlobalConsensus -> toEncodableInstance()
Junction.Unsupported -> error("Unsupported junction")
}
private fun encodableGeneralKey(xcmVersion: XcmVersion, generalKey: HexString): Any {
val bytes = generalKey.fromHex()
return if (xcmVersion >= XcmVersion.V3) {
structOf(
"length" to bytes.size.toBigInteger(),
"data" to bytes.padEnd(expectedSize = 32, padding = 0)
)
} else {
bytes
}
}
private fun Junction.GlobalConsensus.toEncodableInstance(): Any {
val innerValue = when (networkId) {
is NetworkId.Ethereum -> networkId.toEncodableInstance()
is NetworkId.Substrate -> networkId.toEncodableInstance()
}
return DictEnum.Entry("GlobalConsensus", innerValue)
}
private fun NetworkId.Ethereum.toEncodableInstance(): Any {
return DictEnum.Entry("Ethereum", structOf("chain_id" to chainId.toBigInteger()))
}
private fun NetworkId.Substrate.toEncodableInstance(): Any {
return when (genesisHash) {
Chain.Geneses.POLKADOT -> DictEnum.Entry("Polkadot", null)
Chain.Geneses.KUSAMA -> DictEnum.Entry("Kusama", null)
Chain.Geneses.WESTEND -> DictEnum.Entry("Westend", null)
Chain.Ids.ETHEREUM -> DictEnum.Entry("Ethereum", null)
else -> DictEnum.Entry("ByGenesis", genesisHash.fromHex())
}
}
private fun AccountIdKey.toJunctionAccountIdInstance(accountIdKey: String, xcmVersion: XcmVersion) = structOf(
"network" to emptyNetworkField(xcmVersion),
accountIdKey to value
)
private fun emptyNetworkField(xcmVersion: XcmVersion): Any? {
return if (xcmVersion >= XcmVersion.V3) {
// Network in V3+ is encoded as Option<NetworkId>
null
} else {
// Network in V2- is encoded as Enum with Any variant
DictEnum.Entry("Any", null)
}
}
@@ -0,0 +1,18 @@
package io.novafoundation.nova.feature_xcm_api.multiLocation
import io.novafoundation.nova.feature_xcm_api.versions.VersionedToDynamicScaleInstance
import io.novafoundation.nova.feature_xcm_api.versions.XcmVersion
data class RelativeMultiLocation(
val parents: Int,
override val interior: Interior
) : MultiLocation(interior), VersionedToDynamicScaleInstance {
override fun toEncodableInstance(xcmVersion: XcmVersion): Any {
return toEncodableInstanceExt(xcmVersion)
}
}
fun RelativeMultiLocation.isHere(): Boolean {
return parents == 0 && interior.isHere()
}
@@ -0,0 +1,14 @@
package io.novafoundation.nova.feature_xcm_api.runtimeApi
import android.util.Log
import io.novafoundation.nova.common.data.network.runtime.binding.ScaleResult
import io.novafoundation.nova.common.data.network.runtime.binding.ScaleResultError
import io.novafoundation.nova.common.data.network.runtime.binding.toResult
fun <T> Result<ScaleResult<T, *>>.getInnerSuccessOrThrow(errorLogTag: String?): T {
return getOrThrow()
.toResult()
.onFailure {
Log.e(errorLogTag, "Xcm api call failed: ${(it as ScaleResultError).content}")
}.getOrThrow()
}
@@ -0,0 +1,28 @@
package io.novafoundation.nova.feature_xcm_api.runtimeApi.dryRun
import io.novafoundation.nova.common.data.network.runtime.binding.ScaleResult
import io.novafoundation.nova.feature_xcm_api.message.VersionedRawXcmMessage
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.runtime.multiNetwork.chain.model.ChainId
import io.novasama.substrate_sdk_android.runtime.definitions.types.generics.GenericCall
interface DryRunApi {
suspend fun dryRunXcm(
originLocation: VersionedXcmLocation,
xcm: VersionedRawXcmMessage,
chainId: ChainId
): Result<ScaleResult<XcmDryRunEffects, DryRunEffectsResultErr>>
suspend fun dryRunCall(
originCaller: OriginCaller,
call: GenericCall.Instance,
xcmResultsVersion: XcmVersion,
chainId: ChainId
): Result<ScaleResult<CallDryRunEffects, DryRunEffectsResultErr>>
}
@@ -0,0 +1,37 @@
package io.novafoundation.nova.feature_xcm_api.runtimeApi.dryRun.model
import io.novafoundation.nova.common.data.network.runtime.binding.ScaleResult
import io.novafoundation.nova.common.data.network.runtime.binding.bindEvent
import io.novafoundation.nova.common.data.network.runtime.binding.bindList
import io.novafoundation.nova.common.data.network.runtime.binding.castToStruct
import io.novafoundation.nova.common.utils.RuntimeContext
import io.novafoundation.nova.feature_xcm_api.message.VersionedRawXcmMessage
import io.novafoundation.nova.feature_xcm_api.message.bindVersionedRawXcmMessage
import io.novasama.substrate_sdk_android.runtime.definitions.types.generics.GenericEvent
class CallDryRunEffects(
val executionResult: ScaleResult<DispatchPostInfo, DispatchErrorWithPostInfo>,
override val emittedEvents: List<GenericEvent.Instance>,
// We don't need to fully decode/encode intermediate xcm messages
val localXcm: VersionedRawXcmMessage?,
override val forwardedXcms: ForwardedXcms
) : DryRunEffects {
companion object {
context(RuntimeContext)
fun bind(decodedInstance: Any?): CallDryRunEffects {
val asStruct = decodedInstance.castToStruct()
return CallDryRunEffects(
executionResult = ScaleResult.bind(
dynamicInstance = asStruct["executionResult"],
bindOk = DispatchPostInfo::bind,
bindError = { DispatchErrorWithPostInfo.bind(it) }
),
emittedEvents = bindList(asStruct["emittedEvents"], ::bindEvent),
localXcm = asStruct.get<Any?>("localXcm")?.let { bindVersionedRawXcmMessage(it) },
forwardedXcms = bindForwardedXcms(asStruct["forwardedXcms"])
)
}
}
}
@@ -0,0 +1,25 @@
package io.novafoundation.nova.feature_xcm_api.runtimeApi.dryRun.model
import io.novafoundation.nova.common.data.network.runtime.binding.DispatchError
import io.novafoundation.nova.common.data.network.runtime.binding.bindDispatchError
import io.novafoundation.nova.common.data.network.runtime.binding.castToStruct
import io.novafoundation.nova.common.utils.RuntimeContext
class DispatchErrorWithPostInfo(
val postInfo: DispatchPostInfo,
val error: DispatchError
) {
companion object {
context(RuntimeContext)
fun bind(decodedInstance: Any?): DispatchErrorWithPostInfo {
val asStruct = decodedInstance.castToStruct()
return DispatchErrorWithPostInfo(
postInfo = DispatchPostInfo.bind(asStruct["post_info"]),
error = bindDispatchError(asStruct["error"])
)
}
}
}
@@ -0,0 +1,9 @@
package io.novafoundation.nova.feature_xcm_api.runtimeApi.dryRun.model
// Info not needed yet
object DispatchPostInfo {
fun bind(decodedInstance: Any?): DispatchPostInfo {
return this
}
}
@@ -0,0 +1,17 @@
package io.novafoundation.nova.feature_xcm_api.runtimeApi.dryRun.model
import io.novafoundation.nova.feature_xcm_api.versions.XcmVersion
import io.novasama.substrate_sdk_android.runtime.definitions.types.generics.GenericEvent
interface DryRunEffects {
val emittedEvents: List<GenericEvent.Instance>
val forwardedXcms: ForwardedXcms
}
fun DryRunEffects.senderXcmVersion(): XcmVersion {
// For referencing destination, dry run uses sender's xcm version
val (destination) = forwardedXcms.first()
return destination.version
}
@@ -0,0 +1,9 @@
package io.novafoundation.nova.feature_xcm_api.runtimeApi.dryRun.model
// Info not needed yet
object DryRunEffectsResultErr {
fun bind(decodedInstance: Any?): DryRunEffectsResultErr {
return this
}
}
@@ -0,0 +1,23 @@
package io.novafoundation.nova.feature_xcm_api.runtimeApi.dryRun.model
import io.novafoundation.nova.common.data.network.runtime.binding.bindList
import io.novafoundation.nova.common.data.network.runtime.binding.castToList
import io.novafoundation.nova.feature_xcm_api.message.VersionedRawXcmMessage
import io.novafoundation.nova.feature_xcm_api.message.bindVersionedRawXcmMessage
import io.novafoundation.nova.feature_xcm_api.multiLocation.bindVersionedMultiLocation
import io.novafoundation.nova.feature_xcm_api.versions.VersionedXcmLocation
typealias ForwardedXcms = List<Pair<VersionedXcmLocation, List<VersionedRawXcmMessage>>>
fun bindForwardedXcms(decodedInstance: Any?): ForwardedXcms {
return bindList(decodedInstance) { inner ->
val (locationRaw, messagesRaw) = inner.castToList()
val messages = bindList(messagesRaw, ::bindVersionedRawXcmMessage)
val location = bindVersionedMultiLocation(locationRaw)
location to messages
}
}
fun ForwardedXcms.getByLocation(location: VersionedXcmLocation): List<VersionedRawXcmMessage> {
return find { it.first == location }?.second.orEmpty()
}
@@ -0,0 +1,29 @@
package io.novafoundation.nova.feature_xcm_api.runtimeApi.dryRun.model
import io.novafoundation.nova.common.address.AccountIdKey
import io.novafoundation.nova.common.utils.scale.ToDynamicScaleInstance
import io.novasama.substrate_sdk_android.runtime.definitions.types.composite.DictEnum
sealed class OriginCaller : ToDynamicScaleInstance {
sealed class System : OriginCaller() {
object Root : System() {
override fun toEncodableInstance(): Any? {
return wrapInSystemDict(DictEnum.Entry("Root", null))
}
}
class Signed(val accountId: AccountIdKey) : System() {
override fun toEncodableInstance(): Any? {
return wrapInSystemDict(DictEnum.Entry("Signed", accountId.value))
}
}
protected fun wrapInSystemDict(inner: Any): DictEnum.Entry<*> {
return DictEnum.Entry("system", inner)
}
}
}
@@ -0,0 +1,27 @@
package io.novafoundation.nova.feature_xcm_api.runtimeApi.dryRun.model
import io.novafoundation.nova.common.data.network.runtime.binding.bindEvent
import io.novafoundation.nova.common.data.network.runtime.binding.bindList
import io.novafoundation.nova.common.data.network.runtime.binding.castToStruct
import io.novafoundation.nova.common.utils.RuntimeContext
import io.novasama.substrate_sdk_android.runtime.definitions.types.generics.GenericEvent
class XcmDryRunEffects(
val executionResult: XcmOutcome,
override val emittedEvents: List<GenericEvent.Instance>,
override val forwardedXcms: ForwardedXcms
) : DryRunEffects {
companion object {
context(RuntimeContext)
fun bind(decodedInstance: Any?): XcmDryRunEffects {
val asStruct = decodedInstance.castToStruct()
return XcmDryRunEffects(
executionResult = XcmOutcome.bind(asStruct["executionResult"]),
emittedEvents = bindList(asStruct["emittedEvents"], ::bindEvent),
forwardedXcms = bindForwardedXcms(asStruct["forwardedXcms"])
)
}
}
}
@@ -0,0 +1,42 @@
package io.novafoundation.nova.feature_xcm_api.runtimeApi.dryRun.model
import io.novafoundation.nova.common.data.network.runtime.binding.Weight
import io.novafoundation.nova.common.data.network.runtime.binding.bindWeight
import io.novafoundation.nova.common.data.network.runtime.binding.castToDictEnum
import io.novafoundation.nova.common.data.network.runtime.binding.castToStruct
import io.novafoundation.nova.common.data.network.runtime.binding.incompatible
import io.novafoundation.nova.common.utils.scale.DynamicScaleInstance
sealed class XcmOutcome {
companion object {
fun bind(decodedInstance: Any?): XcmOutcome {
val asEnum = decodedInstance.castToDictEnum()
val value = asEnum.value.castToStruct()
return when (asEnum.name) {
"Complete" -> Complete(
used = bindWeight(value["used"])
)
"Incomplete" -> Incomplete(
used = bindWeight(value["used"]),
error = DynamicScaleInstance(value["error"])
)
"Error" -> Error(
error = DynamicScaleInstance(value["error"])
)
else -> incompatible()
}
}
}
class Complete(val used: Weight) : XcmOutcome()
class Incomplete(val used: Weight, val error: DynamicScaleInstance) : XcmOutcome()
class Error(val error: DynamicScaleInstance) : XcmOutcome()
}
@@ -0,0 +1,17 @@
package io.novafoundation.nova.feature_xcm_api.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.feature_xcm_api.message.VersionedXcmMessage
import io.novafoundation.nova.feature_xcm_api.runtimeApi.xcmPayment.model.QueryXcmWeightErr
import io.novafoundation.nova.runtime.multiNetwork.chain.model.ChainId
interface XcmPaymentApi {
suspend fun queryXcmWeight(
chainId: ChainId,
xcm: VersionedXcmMessage,
): Result<ScaleResult<WeightV2, QueryXcmWeightErr>>
suspend fun isSupported(chainId: ChainId): Boolean
}
@@ -0,0 +1,9 @@
package io.novafoundation.nova.feature_xcm_api.runtimeApi.xcmPayment.model
// Info not needed yet
object QueryXcmWeightErr {
fun bind(decodedInstance: Any?): QueryXcmWeightErr {
return this
}
}
@@ -0,0 +1,6 @@
package io.novafoundation.nova.feature_xcm_api.versions
interface VersionedToDynamicScaleInstance {
fun toEncodableInstance(xcmVersion: XcmVersion): Any?
}
@@ -0,0 +1,39 @@
package io.novafoundation.nova.feature_xcm_api.versions
import io.novafoundation.nova.common.data.network.runtime.binding.castToDictEnum
import io.novafoundation.nova.common.utils.scale.DynamicScaleInstance
import io.novafoundation.nova.feature_xcm_api.multiLocation.RelativeMultiLocation
import io.novasama.substrate_sdk_android.runtime.definitions.types.composite.DictEnum
data class VersionedXcm<T>(
val xcm: T,
val version: XcmVersion
)
@JvmName("toEncodableInstanceVersioned")
fun VersionedXcm<out VersionedToDynamicScaleInstance>.toEncodableInstance(): DictEnum.Entry<*> {
return DictEnum.Entry(
name = version.enumerationKey(),
value = xcm.toEncodableInstance(version)
)
}
fun VersionedXcm<out DynamicScaleInstance>.toEncodableInstance(): DictEnum.Entry<*> {
return DictEnum.Entry(
name = version.enumerationKey(),
value = xcm.toEncodableInstance()
)
}
fun <T> bindVersionedXcm(instance: Any?, inner: (Any?, xcmVersion: XcmVersion) -> T): VersionedXcm<T> {
val versionEnum = instance.castToDictEnum()
val xcmVersion = XcmVersion.fromEnumerationKey(versionEnum.name)
return VersionedXcm(inner(versionEnum.value, xcmVersion), xcmVersion)
}
fun <T> T.versionedXcm(xcmVersion: XcmVersion): VersionedXcm<T> {
return VersionedXcm(this, xcmVersion)
}
typealias VersionedXcmLocation = VersionedXcm<RelativeMultiLocation>
@@ -0,0 +1,30 @@
package io.novafoundation.nova.feature_xcm_api.versions
enum class XcmVersion(val version: Int) {
V0(0), V1(1), V2(2), V3(3), V4(4), V5(5);
companion object {
fun fromVersion(version: Int): XcmVersion {
return values().find { it.version == version }
?: error("Unknown xcm version: $version")
}
val GLOBAL_DEFAULT = V2
}
}
// Return xcm version from a enumeration key in form of "V{version}"
fun XcmVersion.Companion.fromEnumerationKey(enumerationKey: String): XcmVersion {
val withoutPrefix = enumerationKey.removePrefix("V")
val version = withoutPrefix.toInt()
return fromVersion(version)
}
fun XcmVersion?.orDefault(): XcmVersion {
return this ?: XcmVersion.GLOBAL_DEFAULT
}
fun XcmVersion.enumerationKey(): String {
return "V$version"
}
@@ -0,0 +1,18 @@
package io.novafoundation.nova.feature_xcm_api.versions.detector
import io.novafoundation.nova.feature_xcm_api.versions.XcmVersion
import io.novafoundation.nova.runtime.multiNetwork.chain.model.ChainId
import io.novasama.substrate_sdk_android.runtime.definitions.types.RuntimeType
interface XcmVersionDetector {
suspend fun lowestPresentMultiLocationVersion(chainId: ChainId): XcmVersion?
suspend fun lowestPresentMultiAssetsVersion(chainId: ChainId): XcmVersion?
suspend fun lowestPresentMultiAssetIdVersion(chainId: ChainId): XcmVersion?
suspend fun lowestPresentMultiAssetVersion(chainId: ChainId): XcmVersion?
suspend fun detectMultiLocationVersion(chainId: ChainId, multiLocationType: RuntimeType<*, *>?): XcmVersion?
}
@@ -0,0 +1,29 @@
package io.novafoundation.nova.feature_xcm_api.weight
import io.novafoundation.nova.common.data.network.runtime.binding.WeightV2
import io.novafoundation.nova.common.utils.scale.ToDynamicScaleInstance
import io.novasama.substrate_sdk_android.runtime.definitions.types.composite.DictEnum
sealed class WeightLimit : ToDynamicScaleInstance {
companion object {
fun zero(): WeightLimit {
return Limited(WeightV2.zero())
}
}
object Unlimited : WeightLimit() {
override fun toEncodableInstance(): Any? {
return DictEnum.Entry("Unlimited", null)
}
}
class Limited(val weightV2: WeightV2) : WeightLimit() {
override fun toEncodableInstance(): Any? {
return DictEnum.Entry("Limited", weightV2.toEncodableInstance())
}
}
}
@@ -0,0 +1,86 @@
package io.novafoundation.nova.feature_xcm_api.multiLocation
import io.novafoundation.nova.feature_xcm_api.multiLocation.MultiLocation.Interior
import io.novafoundation.nova.feature_xcm_api.multiLocation.MultiLocation.Junction.ParachainId
import org.junit.Assert.assertEquals
import org.junit.Test
class AbsoluteMultiLocationTest {
@Test
fun `reanchor global pov should remain unchanged`() {
val initial = AbsoluteMultiLocation(ParachainId(1000))
val pov = AbsoluteMultiLocation(Interior.Here)
val expected = initial.toRelative()
val result = initial.fromPointOfViewOf(pov)
assertEquals(expected, result)
}
@Test
fun `reanchor no common junctions`() {
val initial = AbsoluteMultiLocation(ParachainId(1000))
val pov = AbsoluteMultiLocation(ParachainId(2000))
val expected = RelativeMultiLocation(parents=1, interior = Junctions(ParachainId(1000)))
val result = initial.fromPointOfViewOf(pov)
assertEquals(expected, result)
}
@Test
fun `reanchor one common junction`() {
val initial = AbsoluteMultiLocation(ParachainId(1000), ParachainId(2000))
val pov = AbsoluteMultiLocation(ParachainId(1000), ParachainId(3000))
val expected = RelativeMultiLocation(parents=1, interior = Junctions(ParachainId(2000)))
val result = initial.fromPointOfViewOf(pov)
assertEquals(expected, result)
}
@Test
fun `reanchor all common junction`() {
val initial = AbsoluteMultiLocation(ParachainId(1000), ParachainId(2000))
val pov = AbsoluteMultiLocation(ParachainId(1000), ParachainId(2000))
val expected = RelativeMultiLocation(parents=0, interior = Interior.Here)
val result = initial.fromPointOfViewOf(pov)
assertEquals(expected, result)
}
@Test
fun `reanchor global to global`() {
val initial = AbsoluteMultiLocation(Interior.Here)
val pov = AbsoluteMultiLocation(Interior.Here)
val expected = initial.toRelative()
val result = initial.fromPointOfViewOf(pov)
assertEquals(expected, result)
}
@Test
fun `reanchor pov is successor of initial`() {
val initial = AbsoluteMultiLocation(Interior.Here)
val pov = AbsoluteMultiLocation(ParachainId(1000))
val expected = RelativeMultiLocation(parents=1, interior = Interior.Here)
val result = initial.fromPointOfViewOf(pov)
assertEquals(expected, result)
}
@Test
fun `reanchor initial is successor of pov`() {
val initial = AbsoluteMultiLocation(ParachainId(1000), ParachainId(2000))
val pov = AbsoluteMultiLocation(ParachainId(1000))
val expected = RelativeMultiLocation(parents=0, interior = Junctions(ParachainId(2000)))
val result = initial.fromPointOfViewOf(pov)
assertEquals(expected, result)
}
}