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
+1
View File
@@ -0,0 +1 @@
/build
+26
View File
@@ -0,0 +1,26 @@
apply plugin: 'kotlin-parcelize'
android {
namespace 'io.novafoundation.nova.feature_xcm_api'
defaultConfig {
}
}
dependencies {
implementation coroutinesDep
implementation project(':runtime')
implementation project(":common")
implementation daggerDep
ksp daggerCompiler
api substrateSdkDep
api project(':core-api')
testImplementation project(":test-shared")
}
View File
+21
View File
@@ -0,0 +1,21 @@
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile
@@ -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)
}
}
+1
View File
@@ -0,0 +1 @@
/build
+29
View File
@@ -0,0 +1,29 @@
apply plugin: 'kotlin-parcelize'
android {
namespace 'io.novafoundation.nova.feature_xcm_impl'
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
}
dependencies {
implementation project(':common')
implementation project(':runtime')
api project(":feature-xcm:api")
implementation kotlinDep
implementation substrateSdkDep
implementation daggerDep
ksp daggerCompiler
testImplementation jUnitDep
testImplementation mockitoDep
}
View File
+21
View File
@@ -0,0 +1,21 @@
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile
@@ -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
}
}
}