mirror of
https://github.com/pezkuwichain/pezkuwi-wallet-android.git
synced 2026-06-20 04:41:11 +00:00
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:
@@ -0,0 +1 @@
|
||||
/build
|
||||
@@ -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")
|
||||
}
|
||||
Vendored
+21
@@ -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>
|
||||
+41
@@ -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
|
||||
}
|
||||
+13
@@ -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>
|
||||
+114
@@ -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>
|
||||
+70
@@ -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()
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+27
@@ -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)
|
||||
}
|
||||
+99
@@ -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)))
|
||||
}
|
||||
+22
@@ -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)
|
||||
}
|
||||
+20
@@ -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
|
||||
}
|
||||
+21
@@ -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()
|
||||
}
|
||||
+16
@@ -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")
|
||||
}
|
||||
}
|
||||
+28
@@ -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())
|
||||
}
|
||||
+15
@@ -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")
|
||||
}
|
||||
+13
@@ -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
|
||||
}
|
||||
+9
@@ -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?
|
||||
}
|
||||
+8
@@ -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
|
||||
}
|
||||
+23
@@ -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
|
||||
}
|
||||
+24
@@ -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()
|
||||
)
|
||||
)
|
||||
}
|
||||
+188
@@ -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>
|
||||
+14
@@ -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)
|
||||
+46
@@ -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()
|
||||
}
|
||||
+14
@@ -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)
|
||||
}
|
||||
+8
@@ -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
|
||||
)
|
||||
+159
@@ -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)
|
||||
}
|
||||
+206
@@ -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)
|
||||
}
|
||||
}
|
||||
+18
@@ -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()
|
||||
}
|
||||
+14
@@ -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()
|
||||
}
|
||||
+28
@@ -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>>
|
||||
}
|
||||
+37
@@ -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"])
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
+25
@@ -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"])
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
+9
@@ -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
|
||||
}
|
||||
}
|
||||
+17
@@ -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
|
||||
}
|
||||
+9
@@ -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
|
||||
}
|
||||
}
|
||||
+23
@@ -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()
|
||||
}
|
||||
+29
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
+27
@@ -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"])
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
+42
@@ -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()
|
||||
}
|
||||
+17
@@ -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
|
||||
}
|
||||
+9
@@ -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
|
||||
}
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
package io.novafoundation.nova.feature_xcm_api.versions
|
||||
|
||||
interface VersionedToDynamicScaleInstance {
|
||||
|
||||
fun toEncodableInstance(xcmVersion: XcmVersion): Any?
|
||||
}
|
||||
+39
@@ -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>
|
||||
+30
@@ -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"
|
||||
}
|
||||
+18
@@ -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?
|
||||
}
|
||||
+29
@@ -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())
|
||||
}
|
||||
}
|
||||
}
|
||||
+86
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
/build
|
||||
@@ -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
|
||||
}
|
||||
Vendored
+21
@@ -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>
|
||||
+194
@@ -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
|
||||
+20
@@ -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)
|
||||
}
|
||||
}
|
||||
+19
@@ -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) }
|
||||
}
|
||||
}
|
||||
+91
@@ -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<*, *>
|
||||
)
|
||||
}
|
||||
+76
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
+42
@@ -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()
|
||||
}
|
||||
}
|
||||
+53
@@ -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)
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
+23
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
+42
@@ -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())
|
||||
}
|
||||
}
|
||||
+15
@@ -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) }
|
||||
}
|
||||
}
|
||||
+15
@@ -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() }
|
||||
}
|
||||
}
|
||||
+21
@@ -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)
|
||||
)
|
||||
}
|
||||
}
|
||||
+35
@@ -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
|
||||
}
|
||||
+11
@@ -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
|
||||
}
|
||||
+25
@@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
+11
@@ -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
|
||||
+38
@@ -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
|
||||
}
|
||||
+94
@@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
+52
@@ -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
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
+98
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
+215
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user