diff --git a/app/src/androidTest/java/io/novafoundation/nova/CrossChainTransfersIntegrationTest.kt b/app/src/androidTest/java/io/novafoundation/nova/CrossChainTransfersIntegrationTest.kt index bc670314fd..611fa4b406 100644 --- a/app/src/androidTest/java/io/novafoundation/nova/CrossChainTransfersIntegrationTest.kt +++ b/app/src/androidTest/java/io/novafoundation/nova/CrossChainTransfersIntegrationTest.kt @@ -4,7 +4,7 @@ import android.content.Context import androidx.test.core.app.ApplicationProvider import io.novafoundation.nova.common.di.FeatureUtils import io.novafoundation.nova.common.utils.orZero -import io.novafoundation.nova.feature_wallet_api.data.network.crosschain.CrossChainFee +import io.novafoundation.nova.feature_wallet_api.data.network.crosschain.CrossChainFeeModel import io.novafoundation.nova.feature_wallet_api.di.WalletFeatureApi import io.novafoundation.nova.feature_wallet_api.domain.implementations.transferConfiguration import io.novafoundation.nova.feature_wallet_api.presentation.formatters.formatPlanks @@ -74,22 +74,9 @@ class CrossChainTransfersIntegrationTest : BaseIntegrationTest() { destinationParaId = parachainInfoRepository.paraId(destinationChain.id) )!! - val crossChainFee = crossChainWeigher.estimateFee(crossChainTransfer) + val crossChainFeeResult = runCatching { crossChainWeigher.estimateFee(BigInteger.ZERO, crossChainTransfer) } - error(crossChainFee.formatWith(asssetInOrigin)) + check(crossChainFeeResult.isSuccess) } } - - private fun CrossChainFee.formatWith( - transferringAsset: Chain.Asset - ): String { - fun BigInteger?.formatAmount() = this?.let { it.formatPlanks(transferringAsset) } - - return """ - - Destination Fee: ${destination?.formatAmount()} - Reserve Fee: ${reserve?.formatAmount()} - Total XCM Fee: ${(reserve.orZero() + destination.orZero()).formatAmount()} - """.trimIndent() - } } diff --git a/app/src/main/java/io/novafoundation/nova/app/root/presentation/deepLinks/handlers/DAppDeepLinkHandler.kt b/app/src/main/java/io/novafoundation/nova/app/root/presentation/deepLinks/handlers/DAppDeepLinkHandler.kt index 0afdc6a690..f67d8642d2 100644 --- a/app/src/main/java/io/novafoundation/nova/app/root/presentation/deepLinks/handlers/DAppDeepLinkHandler.kt +++ b/app/src/main/java/io/novafoundation/nova/app/root/presentation/deepLinks/handlers/DAppDeepLinkHandler.kt @@ -7,7 +7,6 @@ import io.novafoundation.nova.app.root.presentation.deepLinks.common.DeepLinkHan import io.novafoundation.nova.common.utils.Urls import io.novafoundation.nova.common.utils.sequrity.AutomaticInteractionGate import io.novafoundation.nova.common.utils.sequrity.awaitInteractionAllowed -import io.novafoundation.nova.feature_account_api.domain.interfaces.AccountRepository import io.novafoundation.nova.feature_dapp_api.data.repository.DAppMetadataRepository import io.novafoundation.nova.feature_dapp_impl.DAppRouter import kotlinx.coroutines.flow.Flow @@ -16,7 +15,6 @@ import kotlinx.coroutines.flow.emptyFlow private const val DAPP_DEEP_LINK_PREFIX = "/open/dapp" class DAppDeepLinkHandler( - private val accountRepository: AccountRepository, private val dappRepository: DAppMetadataRepository, private val dAppRouter: DAppRouter, private val automaticInteractionGate: AutomaticInteractionGate @@ -35,10 +33,14 @@ class DAppDeepLinkHandler( val url = data.getDappUrl() ?: throw DAppHandlingException.UrlIsInvalid val normalizedUrl = runCatching { Urls.normalizeUrl(url) }.getOrNull() ?: throw DAppHandlingException.UrlIsInvalid - val dAppMetadata = dappRepository.syncAndGetDapp(normalizedUrl) - if (dAppMetadata == null) throw DAppHandlingException.DomainIsNotMatched(normalizedUrl) + ensureDAppInCatalog(normalizedUrl) - dAppRouter.openDAppBrowser(normalizedUrl) + dAppRouter.openDAppBrowser(url) + } + + private suspend fun ensureDAppInCatalog(normalizedUrl: String) { + dappRepository.syncAndGetDapp(normalizedUrl) + ?: throw DAppHandlingException.DomainIsNotMatched(normalizedUrl) } private fun Uri.getDappUrl(): String? { diff --git a/app/src/main/java/io/novafoundation/nova/app/root/presentation/di/DeepLinkModule.kt b/app/src/main/java/io/novafoundation/nova/app/root/presentation/di/DeepLinkModule.kt index 37bf9c2d27..00158d5005 100644 --- a/app/src/main/java/io/novafoundation/nova/app/root/presentation/di/DeepLinkModule.kt +++ b/app/src/main/java/io/novafoundation/nova/app/root/presentation/di/DeepLinkModule.kt @@ -4,17 +4,17 @@ import dagger.Module import dagger.Provides import dagger.multibindings.IntoSet import io.novafoundation.nova.app.root.domain.RootInteractor +import io.novafoundation.nova.app.root.presentation.deepLinks.DeepLinkHandler +import io.novafoundation.nova.app.root.presentation.deepLinks.RootDeepLinkHandler import io.novafoundation.nova.app.root.presentation.deepLinks.handlers.BuyCallbackDeepLinkHandler import io.novafoundation.nova.app.root.presentation.deepLinks.handlers.DAppDeepLinkHandler -import io.novafoundation.nova.app.root.presentation.deepLinks.DeepLinkHandler import io.novafoundation.nova.app.root.presentation.deepLinks.handlers.ImportMnemonicDeepLinkHandler import io.novafoundation.nova.app.root.presentation.deepLinks.handlers.ReferendumDeepLinkHandler -import io.novafoundation.nova.app.root.presentation.deepLinks.RootDeepLinkHandler import io.novafoundation.nova.app.root.presentation.deepLinks.handlers.StakingDashboardDeepLinkHandler import io.novafoundation.nova.common.resources.ResourceManager import io.novafoundation.nova.common.utils.sequrity.AutomaticInteractionGate -import io.novafoundation.nova.feature_account_api.domain.interfaces.AccountRepository import io.novafoundation.nova.feature_account_api.domain.account.common.EncryptionDefaults +import io.novafoundation.nova.feature_account_api.domain.interfaces.AccountRepository import io.novafoundation.nova.feature_account_impl.presentation.AccountRouter import io.novafoundation.nova.feature_assets.presentation.AssetsRouter import io.novafoundation.nova.feature_dapp_api.data.repository.DAppMetadataRepository @@ -54,13 +54,11 @@ class DeepLinkModule { @Provides @IntoSet fun provideDappDeepLinkHandler( - accountRepository: AccountRepository, dAppMetadataRepository: DAppMetadataRepository, dAppRouter: DAppRouter, automaticInteractionGate: AutomaticInteractionGate ): DeepLinkHandler { return DAppDeepLinkHandler( - accountRepository, dAppMetadataRepository, dAppRouter, automaticInteractionGate diff --git a/build.gradle b/build.gradle index cc8262b6f2..97ba6cc3aa 100644 --- a/build.gradle +++ b/build.gradle @@ -1,8 +1,8 @@ buildscript { ext { // App version - versionName = '7.7.3' - versionCode = 111 + versionName = '7.7.4' + versionCode = 113 applicationId = "io.novafoundation.nova" releaseApplicationSuffix = "market" diff --git a/common/src/main/java/io/novafoundation/nova/common/resources/ResourceManager.kt b/common/src/main/java/io/novafoundation/nova/common/resources/ResourceManager.kt index 4e48df1a88..45f8b5f1d5 100644 --- a/common/src/main/java/io/novafoundation/nova/common/resources/ResourceManager.kt +++ b/common/src/main/java/io/novafoundation/nova/common/resources/ResourceManager.kt @@ -39,7 +39,7 @@ interface ResourceManager { fun getDimensionPixelSize(id: Int): Int - fun getFont(@FontRes fontRes: Int): Typeface + fun getFont(@FontRes fontRes: Int): Typeface? } fun ResourceManager.formatTimeLeft(elapsedTimeInMillis: Long): String { diff --git a/common/src/main/java/io/novafoundation/nova/common/resources/ResourceManagerImpl.kt b/common/src/main/java/io/novafoundation/nova/common/resources/ResourceManagerImpl.kt index 5a906061ac..a09693706d 100644 --- a/common/src/main/java/io/novafoundation/nova/common/resources/ResourceManagerImpl.kt +++ b/common/src/main/java/io/novafoundation/nova/common/resources/ResourceManagerImpl.kt @@ -4,11 +4,12 @@ import android.graphics.Typeface import android.graphics.drawable.Drawable import android.text.format.DateUtils import androidx.core.content.ContextCompat +import androidx.core.content.res.ResourcesCompat import io.novafoundation.nova.common.R import io.novafoundation.nova.common.di.scope.ApplicationScope import io.novafoundation.nova.common.utils.daysFromMillis -import io.novafoundation.nova.common.utils.formatting.duration.EstimatedDurationFormatter import io.novafoundation.nova.common.utils.formatting.baseDurationFormatter +import io.novafoundation.nova.common.utils.formatting.duration.EstimatedDurationFormatter import io.novafoundation.nova.common.utils.formatting.formatDateTime import io.novafoundation.nova.common.utils.getDrawableCompat import io.novafoundation.nova.common.utils.readText @@ -109,7 +110,7 @@ class ResourceManagerImpl( return contextManager.getApplicationContext().resources.getDimensionPixelSize(id) } - override fun getFont(fontRes: Int): Typeface { - return contextManager.getApplicationContext().resources.getFont(fontRes) + override fun getFont(fontRes: Int): Typeface? { + return ResourcesCompat.getFont(contextManager.getApplicationContext(), fontRes) } } diff --git a/common/src/main/java/io/novafoundation/nova/common/utils/BigRational.kt b/common/src/main/java/io/novafoundation/nova/common/utils/BigRational.kt new file mode 100644 index 0000000000..bdd7bf820e --- /dev/null +++ b/common/src/main/java/io/novafoundation/nova/common/utils/BigRational.kt @@ -0,0 +1,15 @@ +package io.novafoundation.nova.common.utils + +import java.math.BigInteger + +class BigRational(private val numerator: BigInteger, private val denominator: BigInteger) { + + val quotient: BigInteger + get() = numerator / denominator + + companion object +} + +fun BigRational.Companion.fixedU128(value: BigInteger): BigInteger { + return BigRational(value, BigInteger.TEN.pow(18)).quotient +} diff --git a/common/src/main/java/io/novafoundation/nova/common/utils/FearlessLibExt.kt b/common/src/main/java/io/novafoundation/nova/common/utils/FearlessLibExt.kt index fdf5dca621..18e77f571d 100644 --- a/common/src/main/java/io/novafoundation/nova/common/utils/FearlessLibExt.kt +++ b/common/src/main/java/io/novafoundation/nova/common/utils/FearlessLibExt.kt @@ -317,6 +317,14 @@ fun CallRepresentation.toCallInstance(): CallRepresentation.Instance? { return (this as? CallRepresentation.Instance) } +fun RuntimeMetadata.moduleOrFallback(name: String, vararg fallbacks: String): Module = modules[name] + ?: fallbacks.firstOrNull { modules[it] != null } + ?.let { modules[it] } ?: throw NoSuchElementException() + +fun Module.storageOrFallback(name: String, vararg fallbacks: String): StorageEntry = storage?.get(name) + ?: fallbacks.firstOrNull { storage?.get(it) != null } + ?.let { storage?.get(it) } ?: throw NoSuchElementException() + object Modules { const val VESTING: String = "Vesting" const val STAKING = "Staking" diff --git a/common/src/main/java/io/novafoundation/nova/common/utils/SpannableExt.kt b/common/src/main/java/io/novafoundation/nova/common/utils/SpannableExt.kt index b0d9b86a2f..6e1897caca 100644 --- a/common/src/main/java/io/novafoundation/nova/common/utils/SpannableExt.kt +++ b/common/src/main/java/io/novafoundation/nova/common/utils/SpannableExt.kt @@ -1,18 +1,23 @@ package io.novafoundation.nova.common.utils +import android.annotation.TargetApi import android.content.Context import android.graphics.Typeface import android.graphics.drawable.Drawable +import android.os.Build import android.text.Spannable import android.text.SpannableStringBuilder import android.text.SpannedString import android.text.TextPaint +import android.text.style.CharacterStyle import android.text.style.ClickableSpan import android.text.style.ForegroundColorSpan import android.text.style.ImageSpan +import android.text.style.MetricAffectingSpan import android.text.style.TypefaceSpan import android.view.View import androidx.annotation.FontRes +import androidx.core.content.res.ResourcesCompat import androidx.core.text.toSpannable import io.novafoundation.nova.common.resources.ResourceManager import io.novafoundation.nova.common.utils.formatting.spannable.SpannableFormatter @@ -64,12 +69,38 @@ fun colorSpan(color: Int) = ForegroundColorSpan(color) fun fontSpan(resourceManager: ResourceManager, @FontRes fontRes: Int) = fontSpan(resourceManager.getFont(fontRes)) -fun fontSpan(context: Context, @FontRes fontRes: Int) = fontSpan(context.resources.getFont(fontRes)) +fun fontSpan(context: Context, @FontRes fontRes: Int) = fontSpan(ResourcesCompat.getFont(context, fontRes)) -fun fontSpan(typeface: Typeface) = TypefaceSpan(typeface) +fun fontSpan(typeface: Typeface?): CharacterStyle { + return when { + typeface == null -> NoOpSpan() + + Build.VERSION.SDK_INT >= Build.VERSION_CODES.P -> typefaceSpanCompatV28(typeface) + + else -> CustomTypefaceSpan(typeface) + } +} fun drawableSpan(drawable: Drawable) = ImageSpan(drawable) fun CharSequence.formatAsSpannable(vararg args: Any): SpannedString { return SpannableFormatter.format(this, *args) } + +@TargetApi(Build.VERSION_CODES.P) +private fun typefaceSpanCompatV28(typeface: Typeface) = + TypefaceSpan(typeface) + +private class CustomTypefaceSpan(private val typeface: Typeface?) : MetricAffectingSpan() { + override fun updateDrawState(paint: TextPaint) { + paint.typeface = typeface + } + + override fun updateMeasureState(paint: TextPaint) { + paint.typeface = typeface + } +} + +private class NoOpSpan : CharacterStyle() { + override fun updateDrawState(tp: TextPaint?) {} +} diff --git a/feature-account-api/src/main/java/io/novafoundation/nova/feature_account_api/data/model/Fee.kt b/feature-account-api/src/main/java/io/novafoundation/nova/feature_account_api/data/model/Fee.kt index f818beb1e5..cc5f326e13 100644 --- a/feature-account-api/src/main/java/io/novafoundation/nova/feature_account_api/data/model/Fee.kt +++ b/feature-account-api/src/main/java/io/novafoundation/nova/feature_account_api/data/model/Fee.kt @@ -32,8 +32,12 @@ val Fee.requestedAccountPaysFees: Boolean get() = submissionOrigin.requestedOrigin.contentEquals(submissionOrigin.actualOrigin) val Fee.amountByRequestedAccount: BigInteger + get() = amount.asAmountByRequestedAccount + +context(Fee) +val BigInteger.asAmountByRequestedAccount: BigInteger get() = if (requestedAccountPaysFees) { - amount + this } else { BigInteger.ZERO } diff --git a/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/di/AssetsFeatureDependencies.kt b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/di/AssetsFeatureDependencies.kt index b056cc67c5..b74f4230ae 100644 --- a/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/di/AssetsFeatureDependencies.kt +++ b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/di/AssetsFeatureDependencies.kt @@ -72,6 +72,7 @@ import io.novafoundation.nova.runtime.ethereum.contract.erc20.Erc20Standard import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry import io.novafoundation.nova.runtime.multiNetwork.qr.MultiChainQrSharingFactory import io.novafoundation.nova.runtime.multiNetwork.runtime.repository.EventsRepository +import io.novafoundation.nova.runtime.repository.ChainStateRepository import io.novafoundation.nova.runtime.repository.ParachainInfoRepository import io.novafoundation.nova.runtime.storage.source.StorageDataSource import io.novafoundation.nova.web3names.domain.networking.Web3NamesInteractor @@ -235,4 +236,6 @@ interface AssetsFeatureDependencies { val swapRateFormatter: SwapRateFormatter val bottomSheetLauncher: DescriptionBottomSheetLauncher + + val chainStateRepository: ChainStateRepository } diff --git a/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/domain/send/SendInteractor.kt b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/domain/send/SendInteractor.kt index 38207b755d..9948a49881 100644 --- a/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/domain/send/SendInteractor.kt +++ b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/domain/send/SendInteractor.kt @@ -1,26 +1,32 @@ package io.novafoundation.nova.feature_assets.domain.send -import io.novafoundation.nova.common.utils.orZero import io.novafoundation.nova.feature_account_api.data.extrinsic.SubmissionOrigin import io.novafoundation.nova.feature_account_api.data.model.Fee import io.novafoundation.nova.feature_account_api.data.model.SubstrateFee import io.novafoundation.nova.feature_account_api.data.model.amountByRequestedAccount import io.novafoundation.nova.feature_account_api.domain.model.requireAccountIdIn +import io.novafoundation.nova.feature_assets.domain.send.model.TransferFeeModel import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.AssetSourceRegistry import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.tranfers.AssetTransfer import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.tranfers.WeightedAssetTransfer import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.tranfers.isCrossChain +import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.tranfers.senderAccountId +import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.types.Balance +import io.novafoundation.nova.feature_wallet_api.data.network.crosschain.CrossChainFeeModel import io.novafoundation.nova.feature_wallet_api.data.network.crosschain.CrossChainTransactor import io.novafoundation.nova.feature_wallet_api.data.network.crosschain.CrossChainTransfersRepository import io.novafoundation.nova.feature_wallet_api.data.network.crosschain.CrossChainWeigher import io.novafoundation.nova.feature_wallet_api.domain.implementations.transferConfiguration import io.novafoundation.nova.feature_wallet_api.domain.interfaces.WalletRepository import io.novafoundation.nova.feature_wallet_api.domain.model.CrossChainTransfersConfiguration +import io.novafoundation.nova.feature_wallet_api.domain.model.OriginDecimalFee +import io.novafoundation.nova.feature_wallet_api.domain.model.OriginFee import io.novafoundation.nova.feature_wallet_api.domain.model.RecipientSearchResult -import io.novafoundation.nova.feature_wallet_api.presentation.model.DecimalFee +import io.novafoundation.nova.feature_wallet_api.domain.model.networkFeePart import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain import io.novafoundation.nova.runtime.multiNetwork.chain.model.ChainId import io.novafoundation.nova.runtime.repository.ParachainInfoRepository +import jp.co.soramitsu.fearless_utils.runtime.AccountId import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext @@ -62,47 +68,42 @@ class SendInteractor( crossChainTransfersRepository.syncConfiguration() } - suspend fun getOriginFee(transfer: AssetTransfer): Fee = withContext(Dispatchers.Default) { + suspend fun getFee(amount: Balance, transfer: AssetTransfer): TransferFeeModel = withContext(Dispatchers.Default) { if (transfer.isCrossChain) { val config = crossChainTransfersRepository.getConfiguration().configurationFor(transfer)!! - crossChainTransactor.estimateOriginFee(config, transfer) - } else { - getAssetTransfers(transfer).calculateFee(transfer) - } - } + val originFee = crossChainTransactor.estimateOriginFee(config, transfer) + val crossChainFeeModel = crossChainWeigher.estimateFee(amount, config) - suspend fun getCrossChainFee(transfer: AssetTransfer): Fee? = if (transfer.isCrossChain) { - val feePlanks = withContext(Dispatchers.Default) { - val config = crossChainTransfersRepository.getConfiguration().configurationFor(transfer)!! - val crossChainFee = crossChainWeigher.estimateFee(config) + val deliveryPartFee = getDeliveryFee(crossChainFeeModel.senderPart, transfer.senderAccountId()) + val originFeeWithSenderPart = OriginFee(originFee, deliveryPartFee, transfer.commissionAssetToken.configuration) - crossChainFee.reserve.orZero() + crossChainFee.destination.orZero() + TransferFeeModel(originFeeWithSenderPart, crossChainFeeModel.toSubstrateFee(transfer)) + } else { + val originFee = getAssetTransfers(transfer).calculateFee(transfer) + TransferFeeModel( + OriginFee(originFee, null, transfer.commissionAssetToken.configuration), + null + ) } - - val submissionOriginId = transfer.sender.requireAccountIdIn(transfer.originChain) - val submissionOrigin = SubmissionOrigin.singleOrigin(submissionOriginId) // cross-chain fee is always paid by requested account id - - SubstrateFee(feePlanks, submissionOrigin) - } else { - null } suspend fun performTransfer( transfer: WeightedAssetTransfer, - originFee: DecimalFee, - crossChainFee: DecimalFee?, + originFee: OriginDecimalFee, + crossChainFee: Fee?, ): Result<*> = withContext(Dispatchers.Default) { if (transfer.isCrossChain) { - val crossChainFeePlanks = crossChainFee!!.networkFee.amountByRequestedAccount val config = crossChainTransfersRepository.getConfiguration().configurationFor(transfer)!! - crossChainTransactor.performTransfer(config, transfer, crossChainFeePlanks) + crossChainTransactor.performTransfer(config, transfer, crossChainFee!!.amountByRequestedAccount) } else { + val networkFee = originFee.networkFeePart() + getAssetTransfers(transfer).performTransfer(transfer) .onSuccess { submission -> // Insert used fee regardless of who paid it - walletRepository.insertPendingTransfer(submission.hash, transfer, originFee.networkFeeDecimalAmount) + walletRepository.insertPendingTransfer(submission.hash, transfer, networkFee.networkFeeDecimalAmount) } } } @@ -123,4 +124,16 @@ class SendInteractor( destinationChain = transfer.destinationChain, destinationParaId = parachainInfoRepository.paraId(transfer.destinationChain.id) ) + + private fun getDeliveryFee(amount: Balance, accountId: AccountId): Fee { + return SubstrateFee( + amount = amount, + submissionOrigin = SubmissionOrigin.singleOrigin(accountId) + ) + } + + private fun CrossChainFeeModel.toSubstrateFee(transfer: AssetTransfer) = SubstrateFee( + amount = holdingPart, + submissionOrigin = SubmissionOrigin.singleOrigin(transfer.sender.requireAccountIdIn(transfer.originChain)) + ) } diff --git a/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/domain/send/model/TransferFeeModel.kt b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/domain/send/model/TransferFeeModel.kt new file mode 100644 index 0000000000..2aecb37e46 --- /dev/null +++ b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/domain/send/model/TransferFeeModel.kt @@ -0,0 +1,9 @@ +package io.novafoundation.nova.feature_assets.domain.send.model + +import io.novafoundation.nova.feature_account_api.data.model.Fee +import io.novafoundation.nova.feature_wallet_api.domain.model.OriginFee + +class TransferFeeModel( + val originFee: OriginFee, + val crossChainFee: Fee? +) diff --git a/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/send/TransferAssetValidationFailureUi.kt b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/send/TransferAssetValidationFailureUi.kt index 24d4208e10..37ba799c44 100644 --- a/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/send/TransferAssetValidationFailureUi.kt +++ b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/send/TransferAssetValidationFailureUi.kt @@ -10,10 +10,11 @@ import io.novafoundation.nova.feature_account_api.domain.validation.handleSystem import io.novafoundation.nova.feature_assets.R import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.tranfers.AssetTransferPayload import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.tranfers.AssetTransferValidationFailure +import io.novafoundation.nova.feature_wallet_api.domain.model.OriginGenericFee import io.novafoundation.nova.feature_wallet_api.domain.validation.handleFeeSpikeDetected import io.novafoundation.nova.feature_wallet_api.domain.validation.handleNotEnoughFeeError import io.novafoundation.nova.feature_wallet_api.presentation.formatters.formatTokenAmount -import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.FeeLoaderMixin +import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.GenericFeeLoaderMixin import io.novafoundation.nova.feature_wallet_api.presentation.validation.handleInsufficientBalanceCommission import io.novafoundation.nova.feature_wallet_api.presentation.validation.handleNonPositiveAmount import kotlinx.coroutines.CoroutineScope @@ -22,7 +23,7 @@ fun CoroutineScope.mapAssetTransferValidationFailureToUI( resourceManager: ResourceManager, status: ValidationStatus.NotValid, actions: ValidationFlowActions<*>, - feeLoaderMixin: FeeLoaderMixin.Presentation, + feeLoaderMixin: GenericFeeLoaderMixin.Presentation, ): TransformedFailure? { return when (val reason = status.reason) { is AssetTransferValidationFailure.DeadRecipient.InCommissionAsset -> Default( diff --git a/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/send/TransferDraft.kt b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/send/TransferDraft.kt index ac49adb1cf..837630d994 100644 --- a/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/send/TransferDraft.kt +++ b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/send/TransferDraft.kt @@ -1,7 +1,6 @@ package io.novafoundation.nova.feature_assets.presentation.send import android.os.Parcelable -import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.FeeParcelModel import io.novafoundation.nova.feature_wallet_api.presentation.model.AssetPayload import kotlinx.android.parcel.Parcelize import java.math.BigDecimal @@ -9,8 +8,6 @@ import java.math.BigDecimal @Parcelize class TransferDraft( val amount: BigDecimal, - val originFee: FeeParcelModel, - val crossChainFee: FeeParcelModel?, val origin: AssetPayload, val destination: AssetPayload, val recipientAddress: String, diff --git a/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/send/amount/SelectSendViewModel.kt b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/send/amount/SelectSendViewModel.kt index 70e2e91241..f50d16cbde 100644 --- a/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/send/amount/SelectSendViewModel.kt +++ b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/send/amount/SelectSendViewModel.kt @@ -13,7 +13,6 @@ import io.novafoundation.nova.common.validation.ValidationExecutor import io.novafoundation.nova.common.validation.progressConsumer import io.novafoundation.nova.common.view.ButtonState import io.novafoundation.nova.feature_account_api.data.mappers.mapChainToUi -import io.novafoundation.nova.feature_account_api.data.model.Fee import io.novafoundation.nova.feature_account_api.domain.interfaces.MetaAccountGroupingInteractor import io.novafoundation.nova.feature_account_api.domain.interfaces.SelectedAccountUseCase import io.novafoundation.nova.feature_account_api.presenatation.account.wallet.list.SelectAddressForTransactionRequester @@ -29,23 +28,25 @@ import io.novafoundation.nova.feature_assets.presentation.send.TransferDraft import io.novafoundation.nova.feature_assets.presentation.send.amount.view.CrossChainDestinationModel import io.novafoundation.nova.feature_assets.presentation.send.amount.view.SelectCrossChainDestinationBottomSheet import io.novafoundation.nova.feature_assets.presentation.send.autoFixSendValidationPayload +import io.novafoundation.nova.feature_assets.presentation.send.common.buildAssetTransfer import io.novafoundation.nova.feature_assets.presentation.send.mapAssetTransferValidationFailureToUI import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.tranfers.AssetTransfer import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.tranfers.AssetTransferPayload -import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.tranfers.BaseAssetTransfer import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.tranfers.WeightedAssetTransfer import io.novafoundation.nova.feature_wallet_api.domain.interfaces.CrossChainTransfersUseCase import io.novafoundation.nova.feature_wallet_api.domain.model.Asset -import io.novafoundation.nova.feature_wallet_api.domain.model.Token +import io.novafoundation.nova.feature_wallet_api.domain.model.OriginGenericFee +import io.novafoundation.nova.feature_wallet_api.domain.model.planksFromAmount import io.novafoundation.nova.feature_wallet_api.presentation.mixin.amountChooser.AmountChooserMixin import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.FeeLoaderMixin +import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.SimpleFee +import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.SimpleGenericFee import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.awaitDecimalFee import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.awaitOptionalDecimalFee -import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.connectWith import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.create -import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.mapFeeToParcel import io.novafoundation.nova.feature_wallet_api.presentation.model.AssetPayload import io.novafoundation.nova.feature_wallet_api.presentation.model.mapAmountToAmountModel +import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.createGeneric import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry import io.novafoundation.nova.runtime.multiNetwork.ChainWithAsset import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain @@ -143,8 +144,8 @@ class SelectSendViewModel( private val commissionAssetFlow = originChain.flatMapLatest(interactor::commissionAssetFlow) .shareInBackground() - val originFeeMixin: FeeLoaderMixin.Presentation = feeLoaderMixinFactory.create(commissionAssetFlow) - val crossChainFeeMixin: FeeLoaderMixin.Presentation = feeLoaderMixinFactory.create(originAssetFlow) + val originFeeMixin = feeLoaderMixinFactory.createGeneric(commissionAssetFlow) + val crossChainFeeMixin = feeLoaderMixinFactory.create(originAssetFlow) val amountChooserMixin: AmountChooserMixin.Presentation = amountChooserMixinFactory.create( scope = this, @@ -301,32 +302,32 @@ class SelectSendViewModel( } private fun setupFees() { - originFeeMixin.setupFee { transfer -> sendInteractor.getOriginFee(transfer) } - crossChainFeeMixin.setupFee { transfer -> sendInteractor.getCrossChainFee(transfer) } - } - - private fun FeeLoaderMixin.Presentation.setupFee( - feeConstructor: suspend Token.(transfer: AssetTransfer) -> Fee? - ) { - connectWith( - inputSource1 = originChainWithAsset, - inputSource2 = destinationChainWithAsset, - inputSource3 = addressInputMixin.inputFlow, - inputSource4 = amountChooserMixin.backPressuredAmount, - scope = viewModelScope, - expectedChain = { originChain, _, _, _ -> originChain.chain.id }, - feeConstructor = { originChain, destinationChain, addressInput, amount -> - val transfer = buildTransfer(origin = originChain, destination = destinationChain, amount = amount, address = addressInput) - - feeConstructor(transfer) - } - ) + combine( + originChainWithAsset, + destinationChainWithAsset, + addressInputMixin.inputFlow, + amountChooserMixin.backPressuredAmount + ) { originAsset, destinationAsset, address, amount -> + originFeeMixin.invalidateFee() + crossChainFeeMixin.invalidateFee() + + val assetTransfer = buildTransfer(origin = originAsset, destination = destinationAsset, amount = amount, address = address) + val planks = originAsset.asset.planksFromAmount(amount) + + val transferFeeModel = sendInteractor.getFee(planks, assetTransfer) + val originFee = SimpleGenericFee(transferFeeModel.originFee) + val crossChainFee = transferFeeModel.crossChainFee?.let { SimpleFee(it) } + + originFeeMixin.setFee(originFee) + crossChainFeeMixin.setFee(crossChainFee) + } + .inBackground() + .launchIn(this) } private fun openConfirmScreen(validPayload: AssetTransferPayload) = launch { val transferDraft = TransferDraft( amount = validPayload.transfer.amount, - originFee = mapFeeToParcel(validPayload.transfer.decimalFee), origin = AssetPayload( chainId = validPayload.transfer.originChain.id, chainAssetId = validPayload.transfer.originChainAsset.id @@ -336,7 +337,6 @@ class SelectSendViewModel( chainAssetId = validPayload.transfer.destinationChainAsset.id ), recipientAddress = validPayload.transfer.recipient, - crossChainFee = validPayload.crossChainFee?.let(::mapFeeToParcel), openAssetDetailsOnCompletion = payload is SendPayload.SpecifiedOrigin ) @@ -351,15 +351,13 @@ class SelectSendViewModel( ): AssetTransfer { val commissionAsset = commissionAssetFlow.first { it.token.configuration.chainId == origin.chain.id } - return BaseAssetTransfer( - sender = selectedAccount.first(), - recipient = address, - originChain = origin.chain, - originChainAsset = origin.asset, - destinationChain = destination.chain, - destinationChainAsset = destination.asset, + return buildAssetTransfer( + metaAccount = selectedAccount.first(), + commissionAsset = commissionAsset, + origin = origin, + destination = destination, amount = amount, - commissionAssetToken = commissionAsset.token, + address = address ) } diff --git a/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/send/common/AssetTransferExt.kt b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/send/common/AssetTransferExt.kt new file mode 100644 index 0000000000..06c6fc06df --- /dev/null +++ b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/send/common/AssetTransferExt.kt @@ -0,0 +1,28 @@ +package io.novafoundation.nova.feature_assets.presentation.send.common + +import io.novafoundation.nova.feature_account_api.domain.model.MetaAccount +import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.tranfers.AssetTransfer +import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.tranfers.BaseAssetTransfer +import io.novafoundation.nova.feature_wallet_api.domain.model.Asset +import io.novafoundation.nova.runtime.multiNetwork.ChainWithAsset +import java.math.BigDecimal + +fun buildAssetTransfer( + metaAccount: MetaAccount, + commissionAsset: Asset, + origin: ChainWithAsset, + destination: ChainWithAsset, + amount: BigDecimal, + address: String, +): AssetTransfer { + return BaseAssetTransfer( + sender = metaAccount, + recipient = address, + originChain = origin.chain, + originChainAsset = origin.asset, + destinationChain = destination.chain, + destinationChainAsset = destination.asset, + amount = amount, + commissionAssetToken = commissionAsset.token, + ) +} diff --git a/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/send/confirm/ConfirmSendViewModel.kt b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/send/confirm/ConfirmSendViewModel.kt index 68b652068d..8e02ad81c4 100644 --- a/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/send/confirm/ConfirmSendViewModel.kt +++ b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/send/confirm/ConfirmSendViewModel.kt @@ -26,20 +26,29 @@ import io.novafoundation.nova.feature_assets.domain.send.SendInteractor import io.novafoundation.nova.feature_assets.presentation.AssetsRouter import io.novafoundation.nova.feature_assets.presentation.send.TransferDraft import io.novafoundation.nova.feature_assets.presentation.send.autoFixSendValidationPayload +import io.novafoundation.nova.feature_assets.presentation.send.common.buildAssetTransfer import io.novafoundation.nova.feature_assets.presentation.send.confirm.hints.ConfirmSendHintsMixinFactory import io.novafoundation.nova.feature_assets.presentation.send.isCrossChain import io.novafoundation.nova.feature_assets.presentation.send.mapAssetTransferValidationFailureToUI +import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.tranfers.AssetTransfer import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.tranfers.AssetTransferPayload import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.tranfers.WeightedAssetTransfer +import io.novafoundation.nova.feature_wallet_api.domain.model.OriginDecimalFee +import io.novafoundation.nova.feature_wallet_api.domain.model.OriginGenericFee +import io.novafoundation.nova.feature_wallet_api.domain.model.planksFromAmount import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.FeeLoaderMixin +import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.SimpleFee +import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.SimpleGenericFee import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.awaitDecimalFee +import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.awaitOptionalDecimalFee import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.create -import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.mapFeeFromParcel +import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.createGeneric import io.novafoundation.nova.feature_wallet_api.presentation.model.AmountSign import io.novafoundation.nova.feature_wallet_api.presentation.model.AssetPayload import io.novafoundation.nova.feature_wallet_api.presentation.model.DecimalFee import io.novafoundation.nova.feature_wallet_api.presentation.model.mapAmountToAmountModel import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry +import io.novafoundation.nova.runtime.multiNetwork.ChainWithAsset import io.novafoundation.nova.runtime.multiNetwork.asset import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain import kotlinx.coroutines.Deferred @@ -74,9 +83,6 @@ class ConfirmSendViewModel( ExternalActions by externalActions, Validatable by validationExecutor { - private val originFee = mapFeeFromParcel(transferDraft.originFee) - private val crossChainFee = transferDraft.crossChainFee?.let(::mapFeeFromParcel) - private val originChain by lazyAsync { chainRegistry.getChain(transferDraft.origin.chainId) } private val originAsset by lazyAsync { chainRegistry.asset(transferDraft.origin.chainId, transferDraft.origin.chainAssetId) } @@ -91,15 +97,15 @@ class ConfirmSendViewModel( .inBackground() .share() - val originFeeMixin: FeeLoaderMixin.Presentation = feeLoaderMixinFactory.create(commissionAssetFlow) - val crossChainFeeMixin: FeeLoaderMixin.Presentation = feeLoaderMixinFactory.create(assetFlow) - - val hintsMixin = hintsFactory.create(this) - private val currentAccount = selectedAccountUseCase.selectedMetaAccountFlow() .inBackground() .share() + val originFeeMixin = feeLoaderMixinFactory.createGeneric(commissionAssetFlow) + val crossChainFeeMixin = feeLoaderMixinFactory.create(assetFlow) + + val hintsMixin = hintsFactory.create(this) + val recipientModel = flowOf { createAddressModel( address = transferDraft.recipientAddress, @@ -142,7 +148,7 @@ class ConfirmSendViewModel( } init { - setInitialState() + setupFee() } fun backClicked() { @@ -185,9 +191,37 @@ class ConfirmSendViewModel( } } - private fun setInitialState() = launch { - originFeeMixin.setFee(originFee.genericFee) - crossChainFeeMixin.setFee(crossChainFee?.genericFee) + private fun setupFee() = launch { + launch { + val assetTransfer = buildTransfer() + val planks = originAsset().planksFromAmount(assetTransfer.amount) + + originFeeMixin.invalidateFee() + crossChainFeeMixin.invalidateFee() + + val transferFeeModel = sendInteractor.getFee(planks, assetTransfer) + val originFee = SimpleGenericFee(transferFeeModel.originFee) + val crossChainFee = transferFeeModel.crossChainFee?.let { SimpleFee(it) } + + originFeeMixin.setFee(originFee) + crossChainFeeMixin.setFee(crossChainFee) + } + } + + private suspend fun buildTransfer(): AssetTransfer { + val originChainWithAsset = ChainWithAsset(originChain(), originAsset()) + val destinationChainWithAsset = ChainWithAsset(destinationChain(), destinationChainAsset()) + val amount = transferDraft.amount + val address = transferDraft.recipientAddress + + return buildAssetTransfer( + metaAccount = selectedAccountUseCase.getSelectedMetaAccount(), + commissionAsset = commissionAssetFlow.first(), + origin = originChainWithAsset, + destination = destinationChainWithAsset, + amount = amount, + address = address + ) } private suspend fun createAddressModel( @@ -205,10 +239,10 @@ class ConfirmSendViewModel( private fun performTransfer( transfer: WeightedAssetTransfer, - originFee: DecimalFee, + originFee: OriginDecimalFee, crossChainFee: DecimalFee? ) = launch { - sendInteractor.performTransfer(transfer, originFee, crossChainFee) + sendInteractor.performTransfer(transfer, originFee, crossChainFee?.genericFee?.networkFee) .onSuccess { showMessage(resourceManager.getString(R.string.common_transaction_submitted)) @@ -250,7 +284,7 @@ class ConfirmSendViewModel( originFee = originFee, originCommissionAsset = commissionAssetFlow.first(), originUsedAsset = assetFlow.first(), - crossChainFee = crossChainFee + crossChainFee = crossChainFeeMixin.awaitOptionalDecimalFee() ) } diff --git a/feature-crowdloan-impl/src/main/java/io/novafoundation/nova/feature_crowdloan_impl/di/validations/ContributeValidationsModule.kt b/feature-crowdloan-impl/src/main/java/io/novafoundation/nova/feature_crowdloan_impl/di/validations/ContributeValidationsModule.kt index b34007ec5d..46081cabbd 100644 --- a/feature-crowdloan-impl/src/main/java/io/novafoundation/nova/feature_crowdloan_impl/di/validations/ContributeValidationsModule.kt +++ b/feature-crowdloan-impl/src/main/java/io/novafoundation/nova/feature_crowdloan_impl/di/validations/ContributeValidationsModule.kt @@ -54,7 +54,7 @@ class ContributeValidationsModule { walletConstants: WalletConstants, ) = ContributeExistentialDepositValidation( countableTowardsEdBalance = { it.asset.balanceCountedTowardsED() }, - feeProducer = { it.fee }, + feeProducer = { listOf(it.fee) }, extraAmountProducer = { it.contributionAmount }, existentialDeposit = { val inPlanks = walletConstants.existentialDeposit(it.asset.token.configuration.chainId) diff --git a/feature-crowdloan-impl/src/main/java/io/novafoundation/nova/feature_crowdloan_impl/domain/contribute/validations/Definitions.kt b/feature-crowdloan-impl/src/main/java/io/novafoundation/nova/feature_crowdloan_impl/domain/contribute/validations/Definitions.kt index 59726b2591..b6c170cca3 100644 --- a/feature-crowdloan-impl/src/main/java/io/novafoundation/nova/feature_crowdloan_impl/domain/contribute/validations/Definitions.kt +++ b/feature-crowdloan-impl/src/main/java/io/novafoundation/nova/feature_crowdloan_impl/domain/contribute/validations/Definitions.kt @@ -3,8 +3,9 @@ package io.novafoundation.nova.feature_crowdloan_impl.domain.contribute.validati import io.novafoundation.nova.common.validation.Validation import io.novafoundation.nova.feature_wallet_api.domain.validation.EnoughAmountToTransferValidation import io.novafoundation.nova.feature_wallet_api.domain.validation.ExistentialDepositValidation +import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.SimpleFee typealias ContributeValidation = Validation typealias ContributeEnoughToPayFeesValidation = EnoughAmountToTransferValidation -typealias ContributeExistentialDepositValidation = ExistentialDepositValidation +typealias ContributeExistentialDepositValidation = ExistentialDepositValidation diff --git a/feature-governance-impl/src/main/java/io/novafoundation/nova/feature_governance_impl/data/repository/v2/Gov2PreImageRepository.kt b/feature-governance-impl/src/main/java/io/novafoundation/nova/feature_governance_impl/data/repository/v2/Gov2PreImageRepository.kt index 1966b02950..ebd67d3353 100644 --- a/feature-governance-impl/src/main/java/io/novafoundation/nova/feature_governance_impl/data/repository/v2/Gov2PreImageRepository.kt +++ b/feature-governance-impl/src/main/java/io/novafoundation/nova/feature_governance_impl/data/repository/v2/Gov2PreImageRepository.kt @@ -7,6 +7,7 @@ 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.castOrNull import io.novafoundation.nova.common.utils.preImage +import io.novafoundation.nova.common.utils.storageOrFallback import io.novafoundation.nova.feature_governance_api.data.network.blockhain.model.PreImage import io.novafoundation.nova.feature_governance_api.data.repository.HexHash import io.novafoundation.nova.feature_governance_api.data.repository.PreImageRepository @@ -120,18 +121,20 @@ class Gov2PreImageRepository( } private suspend fun StorageQueryContext.fetchPreImageLength(callHash: ByteArray): BigInteger? { - return runtime.metadata.preImage().storage("StatusFor").query( - callHash, - binding = ::bindPreImageLength - ) + return runtime.metadata.preImage().storageOrFallback("RequestStatusFor", "StatusFor") + .query( + callHash, + binding = ::bindPreImageLength + ) } private suspend fun StorageQueryContext.fetchPreImagesLength(callHashes: Collection): Map { - return runtime.metadata.preImage().storage("StatusFor").entries( - keysArguments = callHashes.wrapSingleArgumentKeys(), - keyExtractor = { (callHash: ByteArray) -> callHash.toHexString() }, - binding = { decoded, _ -> bindPreImageLength(decoded) } - ) + return runtime.metadata.preImage().storageOrFallback("RequestStatusFor", "StatusFor") + .entries( + keysArguments = callHashes.wrapSingleArgumentKeys(), + keyExtractor = { (callHash: ByteArray) -> callHash.toHexString() }, + binding = { decoded, _ -> bindPreImageLength(decoded) } + ) } private fun bindPreImageLength(decoded: Any?): BigInteger? = runCatching { diff --git a/feature-governance-impl/src/main/java/io/novafoundation/nova/feature_governance_impl/domain/referendum/details/call/treasury/TreasurySpendAdapter.kt b/feature-governance-impl/src/main/java/io/novafoundation/nova/feature_governance_impl/domain/referendum/details/call/treasury/TreasurySpendAdapter.kt index 9dfd11ba9e..82ded15957 100644 --- a/feature-governance-impl/src/main/java/io/novafoundation/nova/feature_governance_impl/domain/referendum/details/call/treasury/TreasurySpendAdapter.kt +++ b/feature-governance-impl/src/main/java/io/novafoundation/nova/feature_governance_impl/domain/referendum/details/call/treasury/TreasurySpendAdapter.kt @@ -15,7 +15,8 @@ class TreasurySpendAdapter : ReferendumCallAdapter { call: GenericCall.Instance, context: ReferendumCallParseContext ): ReferendumCall? { - if (!call.instanceOf(Modules.TREASURY, "spend")) return null + // TODO: spend call is using MultiLocation now instead of MultiAddress so binding throws an exception + if (!call.instanceOf(Modules.TREASURY, "spend_local", "spend")) return null val amount = bindNonce(call.arguments["amount"]) val beneficiary = bindAccountIdentifier(call.arguments["beneficiary"]) diff --git a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/data/collators/KnownNovaCollators.kt b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/data/collators/KnownNovaCollators.kt new file mode 100644 index 0000000000..a2a09c628b --- /dev/null +++ b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/data/collators/KnownNovaCollators.kt @@ -0,0 +1,23 @@ +package io.novafoundation.nova.feature_staking_impl.data.collators + +import io.novafoundation.nova.runtime.ext.Geneses +import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain +import io.novafoundation.nova.runtime.multiNetwork.chain.model.ChainId + +interface KnownNovaCollators { + + fun getCollatorIds(chainId: ChainId): List +} + +class FixedKnownNovaCollators : KnownNovaCollators { + + private val novaValidators by lazy { + mapOf( + Chain.Geneses.POLIMEC to listOf("5A5Qgq3wn6JeH8Qtu7rakxULpBhtyqyX8iNj1XV8WFg3U58T") + ) + } + + override fun getCollatorIds(chainId: ChainId): List { + return novaValidators[chainId].orEmpty() + } +} diff --git a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/di/staking/parachain/ParachainStakingModule.kt b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/di/staking/parachain/ParachainStakingModule.kt index 2e5d6628ce..ec9645735e 100644 --- a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/di/staking/parachain/ParachainStakingModule.kt +++ b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/di/staking/parachain/ParachainStakingModule.kt @@ -9,6 +9,8 @@ import io.novafoundation.nova.common.resources.ResourceManager import io.novafoundation.nova.feature_account_api.data.repository.OnChainIdentityRepository import io.novafoundation.nova.feature_account_api.domain.interfaces.AccountRepository import io.novafoundation.nova.feature_staking_impl.data.StakingSharedState +import io.novafoundation.nova.feature_staking_impl.data.collators.FixedKnownNovaCollators +import io.novafoundation.nova.feature_staking_impl.data.collators.KnownNovaCollators import io.novafoundation.nova.feature_staking_impl.data.parachainStaking.RealRoundDurationEstimator import io.novafoundation.nova.feature_staking_impl.data.parachainStaking.RoundDurationEstimator import io.novafoundation.nova.feature_staking_impl.data.parachainStaking.repository.CandidatesRepository @@ -143,12 +145,17 @@ class ParachainStakingModule { roundDurationEstimator: RoundDurationEstimator ) = ParachainStakingHintsUseCase(stakingSharedState, resourceManager, roundDurationEstimator) + @Provides + @FeatureScope + fun provideKnownNovaCollators(): KnownNovaCollators = FixedKnownNovaCollators() + @Provides @FeatureScope fun provideCollatorRecommendatorFactory( collatorProvider: CollatorProvider, - computationalCache: ComputationalCache - ) = CollatorRecommendatorFactory(collatorProvider, computationalCache) + computationalCache: ComputationalCache, + knownNovaCollators: KnownNovaCollators + ) = CollatorRecommendatorFactory(collatorProvider, computationalCache, knownNovaCollators) @Provides @FeatureScope diff --git a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/parachainStaking/common/recommendations/CollatorRecommendator.kt b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/parachainStaking/common/recommendations/CollatorRecommendator.kt index d7d462ba32..eba508df93 100644 --- a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/parachainStaking/common/recommendations/CollatorRecommendator.kt +++ b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/parachainStaking/common/recommendations/CollatorRecommendator.kt @@ -1,16 +1,26 @@ package io.novafoundation.nova.feature_staking_impl.domain.parachainStaking.common.recommendations import io.novafoundation.nova.common.data.memory.ComputationalCache +import io.novafoundation.nova.common.utils.indexOfOrNull import io.novafoundation.nova.feature_staking_impl.data.StakingOption +import io.novafoundation.nova.feature_staking_impl.data.chain +import io.novafoundation.nova.feature_staking_impl.data.collators.KnownNovaCollators import io.novafoundation.nova.feature_staking_impl.domain.parachainStaking.common.CollatorProvider import io.novafoundation.nova.feature_staking_impl.domain.parachainStaking.common.CollatorProvider.CollatorSource import io.novafoundation.nova.feature_staking_impl.domain.parachainStaking.common.model.Collator import kotlinx.coroutines.CoroutineScope -class CollatorRecommendator(private val allCollators: List) { +class CollatorRecommendator(private val allCollators: List, private val novaCollatorIds: List) { fun recommendations(config: CollatorRecommendationConfig): List { return allCollators.sortedWith(config.sorting) + .sortedBy { novaCollatorIds.indexOfOrNull(it.address) ?: Int.MAX_VALUE } + } + + fun default(): Collator? { + val collatorByAddress = allCollators.associateBy { it.address } + return novaCollatorIds.firstOrNull { collatorByAddress.containsKey(it) } + ?.let { collatorByAddress.get(it) } } } @@ -18,12 +28,15 @@ private const val COLLATORS_CACHE = "COLLATORS_CACHE" class CollatorRecommendatorFactory( private val collatorProvider: CollatorProvider, - private val computationalCache: ComputationalCache + private val computationalCache: ComputationalCache, + private val knownNovaCollators: KnownNovaCollators ) { suspend fun create(stakingOption: StakingOption, scope: CoroutineScope) = computationalCache.useCache(COLLATORS_CACHE, scope) { val collators = collatorProvider.getCollators(stakingOption, CollatorSource.Elected) - CollatorRecommendator(collators) + val knownNovaCollators = knownNovaCollators.getCollatorIds(stakingOption.chain.id) + + CollatorRecommendator(collators, knownNovaCollators) } } diff --git a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/rewards/RewardCalculatorFactory.kt b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/rewards/RewardCalculatorFactory.kt index aa8b6ea46d..47f4327363 100644 --- a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/rewards/RewardCalculatorFactory.kt +++ b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/rewards/RewardCalculatorFactory.kt @@ -5,12 +5,15 @@ import io.novafoundation.nova.feature_staking_api.domain.api.StakingRepository import io.novafoundation.nova.feature_staking_api.domain.model.Exposure import io.novafoundation.nova.feature_staking_api.domain.model.ValidatorPrefs import io.novafoundation.nova.feature_staking_impl.data.StakingOption +import io.novafoundation.nova.feature_staking_impl.data.chain import io.novafoundation.nova.feature_staking_impl.data.repository.ParasRepository import io.novafoundation.nova.feature_staking_impl.data.stakingType import io.novafoundation.nova.feature_staking_impl.data.unwrapNominationPools import io.novafoundation.nova.feature_staking_impl.domain.common.StakingSharedComputation import io.novafoundation.nova.feature_staking_impl.domain.common.electedExposuresInActiveEra import io.novafoundation.nova.feature_staking_impl.domain.error.accountIdNotFound +import io.novafoundation.nova.runtime.ext.Geneses +import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain.Asset.StakingType.ALEPH_ZERO import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain.Asset.StakingType.NOMINATION_POOLS import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain.Asset.StakingType.PARACHAIN @@ -18,6 +21,7 @@ import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain.Asset.Staki import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain.Asset.StakingType.RELAYCHAIN_AURA import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain.Asset.StakingType.TURING import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain.Asset.StakingType.UNSUPPORTED +import io.novafoundation.nova.runtime.multiNetwork.chain.model.ChainId import io.novafoundation.nova.runtime.repository.TotalIssuanceRepository import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers @@ -65,7 +69,7 @@ class RewardCalculatorFactory( return when (unwrapNominationPools().stakingType) { RELAYCHAIN, RELAYCHAIN_AURA -> { val activePublicParachains = parasRepository.activePublicParachains(assetWithChain.chain.id) - val inflationConfig = InflationConfig.Default(activePublicParachains) + val inflationConfig = InflationConfig.create(chain.id, activePublicParachains) RewardCurveInflationRewardCalculator(validators, totalIssuance, inflationConfig) } @@ -73,4 +77,11 @@ class RewardCalculatorFactory( NOMINATION_POOLS, UNSUPPORTED, PARACHAIN, TURING -> throw IllegalStateException("Unknown staking type in RelaychainRewardFactory") } } + + private fun InflationConfig.Companion.create(chainId: ChainId, activePublicParachains: Int?): InflationConfig { + return when (chainId) { + Chain.Geneses.POLKADOT -> Polkadot(activePublicParachains) + else -> Default(activePublicParachains) + } + } } diff --git a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/rewards/RewardCurveInflationRewardCalculator.kt b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/rewards/RewardCurveInflationRewardCalculator.kt index 5c02bcdeae..f0636f8959 100644 --- a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/rewards/RewardCurveInflationRewardCalculator.kt +++ b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/rewards/RewardCurveInflationRewardCalculator.kt @@ -18,7 +18,8 @@ class InflationConfig( ) companion object { - // defaults based on Polkadot and Kusama runtime + + // defaults based on Kusama runtime fun Default(activePublicParachains: Int?) = InflationConfig( falloff = 0.05, maxInflation = 0.1, @@ -32,6 +33,21 @@ class InflationConfig( ) } ) + + // Polkadot has different `parachainReservedSupplyFraction` + fun Polkadot(activePublicParachains: Int?) = InflationConfig( + falloff = 0.05, + maxInflation = 0.1, + minInflation = 0.025, + stakeTarget = 0.75, + parachainAdjust = activePublicParachains?.let { + ParachainAdjust( + maxParachains = 60, + activePublicParachains = activePublicParachains, + parachainReservedSupplyFraction = 0.2 + ) + } + ) } } diff --git a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/parachainStaking/start/setup/StartParachainStakingViewModel.kt b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/parachainStaking/start/setup/StartParachainStakingViewModel.kt index f7fc9dc156..77e9569ffd 100644 --- a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/parachainStaking/start/setup/StartParachainStakingViewModel.kt +++ b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/parachainStaking/start/setup/StartParachainStakingViewModel.kt @@ -1,5 +1,6 @@ package io.novafoundation.nova.feature_staking_impl.presentation.parachainStaking.start.setup +import androidx.lifecycle.viewModelScope import io.novafoundation.nova.common.address.AddressIconGenerator import io.novafoundation.nova.common.base.BaseViewModel import io.novafoundation.nova.common.mixin.actionAwaitable.ActionAwaitableMixin @@ -9,6 +10,7 @@ import io.novafoundation.nova.common.presentation.DescriptiveButtonState import io.novafoundation.nova.common.resources.ResourceManager import io.novafoundation.nova.common.utils.findById import io.novafoundation.nova.common.utils.inBackground +import io.novafoundation.nova.common.utils.lazyAsync import io.novafoundation.nova.common.utils.orZero import io.novafoundation.nova.common.validation.ValidationExecutor import io.novafoundation.nova.common.validation.progressConsumer @@ -16,10 +18,12 @@ import io.novafoundation.nova.feature_staking_api.domain.model.parachain.Delegat import io.novafoundation.nova.feature_staking_api.domain.model.parachain.delegationAmountTo import io.novafoundation.nova.feature_staking_api.domain.model.parachain.stakeablePlanks import io.novafoundation.nova.feature_staking_impl.R +import io.novafoundation.nova.feature_staking_impl.data.StakingSharedState import io.novafoundation.nova.feature_staking_impl.domain.parachainStaking.common.CollatorsUseCase import io.novafoundation.nova.feature_staking_impl.domain.parachainStaking.common.DelegatorStateUseCase import io.novafoundation.nova.feature_staking_impl.domain.parachainStaking.common.model.Collator import io.novafoundation.nova.feature_staking_impl.domain.parachainStaking.common.model.SelectedCollator +import io.novafoundation.nova.feature_staking_impl.domain.parachainStaking.common.recommendations.CollatorRecommendatorFactory import io.novafoundation.nova.feature_staking_impl.domain.parachainStaking.start.DelegationsLimit import io.novafoundation.nova.feature_staking_impl.domain.parachainStaking.start.StartParachainStakingInteractor import io.novafoundation.nova.feature_staking_impl.domain.parachainStaking.start.validations.StartParachainStakingValidationPayload @@ -48,6 +52,7 @@ import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.connectW import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.mapFeeToParcel import io.novafoundation.nova.feature_wallet_api.presentation.model.DecimalFee import io.novafoundation.nova.feature_wallet_api.presentation.model.mapAmountToAmountModel +import io.novafoundation.nova.runtime.state.selectedOption import jp.co.soramitsu.fearless_utils.extensions.fromHex import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.MutableStateFlow @@ -76,6 +81,8 @@ class StartParachainStakingViewModel( private val actionAwaitableMixinFactory: ActionAwaitableMixin.Factory, private val collatorsUseCase: CollatorsUseCase, private val payload: StartParachainStakingPayload, + private val collatorRecommendatorFactory: CollatorRecommendatorFactory, + private val selectedAssetState: StakingSharedState, hintsMixinFactory: ConfirmStartParachainStakingHintsMixinFactory, amountChooserMixinFactory: AmountChooserMixin.Factory, ) : BaseViewModel(), @@ -83,6 +90,10 @@ class StartParachainStakingViewModel( Validatable by validationExecutor, FeeLoaderMixin by feeLoaderMixin { + private val collatorRecommendator by lazyAsync { + collatorRecommendatorFactory.create(selectedAssetState.selectedOption(), scope = viewModelScope) + } + private val validationInProgress = MutableStateFlow(false) private val assetFlow = assetUseCase.currentAssetFlow() @@ -187,6 +198,8 @@ class StartParachainStakingViewModel( listenCollatorChanges() setInitialCollator() + + setDefaultCollator() } fun selectCollatorClicked() = launch { @@ -308,4 +321,12 @@ class StartParachainStakingViewModel( router.openConfirmStartStaking(payload) } + + private fun setDefaultCollator() { + launch { + val defaultCollator = collatorRecommendator.await().default() + + selectedCollatorFlow.value = defaultCollator + } + } } diff --git a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/parachainStaking/start/setup/di/SetupStartParachainStakingModule.kt b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/parachainStaking/start/setup/di/SetupStartParachainStakingModule.kt index 91419c35fb..e705a64d75 100644 --- a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/parachainStaking/start/setup/di/SetupStartParachainStakingModule.kt +++ b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/parachainStaking/start/setup/di/SetupStartParachainStakingModule.kt @@ -16,6 +16,7 @@ import io.novafoundation.nova.common.validation.ValidationExecutor import io.novafoundation.nova.feature_staking_impl.data.StakingSharedState import io.novafoundation.nova.feature_staking_impl.domain.parachainStaking.common.CollatorsUseCase import io.novafoundation.nova.feature_staking_impl.domain.parachainStaking.common.DelegatorStateUseCase +import io.novafoundation.nova.feature_staking_impl.domain.parachainStaking.common.recommendations.CollatorRecommendatorFactory import io.novafoundation.nova.feature_staking_impl.domain.parachainStaking.rewards.ParachainStakingRewardCalculatorFactory import io.novafoundation.nova.feature_staking_impl.domain.parachainStaking.start.StartParachainStakingInteractor import io.novafoundation.nova.feature_staking_impl.domain.parachainStaking.start.validations.StartParachainStakingValidationSystem @@ -60,6 +61,8 @@ class SetupStartParachainStakingModule { actionAwaitableMixinFactory: ActionAwaitableMixin.Factory, hintsMixinFactory: ConfirmStartParachainStakingHintsMixinFactory, collatorsUseCase: CollatorsUseCase, + selectedAssetState: StakingSharedState, + collatorRecommendatorFactory: CollatorRecommendatorFactory, payload: StartParachainStakingPayload, ): ViewModel { return StartParachainStakingViewModel( @@ -78,6 +81,8 @@ class SetupStartParachainStakingModule { actionAwaitableMixinFactory = actionAwaitableMixinFactory, collatorsUseCase = collatorsUseCase, hintsMixinFactory = hintsMixinFactory, + selectedAssetState = selectedAssetState, + collatorRecommendatorFactory = collatorRecommendatorFactory, payload = payload ) } diff --git a/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/data/network/blockhain/assets/tranfers/AssetTransferValidations.kt b/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/data/network/blockhain/assets/tranfers/AssetTransferValidations.kt index 02e9d839fb..67d31b80ff 100644 --- a/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/data/network/blockhain/assets/tranfers/AssetTransferValidations.kt +++ b/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/data/network/blockhain/assets/tranfers/AssetTransferValidations.kt @@ -4,12 +4,16 @@ import io.novafoundation.nova.common.validation.Validation import io.novafoundation.nova.common.validation.ValidationSystem import io.novafoundation.nova.common.validation.ValidationSystemBuilder import io.novafoundation.nova.feature_wallet_api.domain.model.Asset +import io.novafoundation.nova.feature_wallet_api.domain.model.OriginDecimalFee +import io.novafoundation.nova.feature_wallet_api.domain.model.OriginGenericFee +import io.novafoundation.nova.feature_wallet_api.domain.model.intoDecimalFeeList import io.novafoundation.nova.feature_wallet_api.domain.model.planksFromAmount import io.novafoundation.nova.feature_wallet_api.domain.validation.FeeChangeDetectedFailure import io.novafoundation.nova.feature_wallet_api.domain.validation.InsufficientBalanceToStayAboveEDError import io.novafoundation.nova.feature_wallet_api.domain.validation.NotEnoughToPayFeesError -import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.SimpleFee +import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.GenericFee import io.novafoundation.nova.feature_wallet_api.presentation.model.DecimalFee +import io.novafoundation.nova.feature_wallet_api.presentation.model.GenericDecimalFee import io.novafoundation.nova.runtime.ext.commissionAsset import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain import java.math.BigDecimal @@ -61,27 +65,40 @@ sealed class AssetTransferValidationFailure { object RecipientCannotAcceptTransfer : AssetTransferValidationFailure() class FeeChangeDetected( - override val payload: FeeChangeDetectedFailure.Payload - ) : AssetTransferValidationFailure(), FeeChangeDetectedFailure + override val payload: FeeChangeDetectedFailure.Payload + ) : AssetTransferValidationFailure(), FeeChangeDetectedFailure object RecipientIsSystemAccount : AssetTransferValidationFailure() } data class AssetTransferPayload( val transfer: WeightedAssetTransfer, - val originFee: DecimalFee, + val originFee: OriginDecimalFee, val crossChainFee: DecimalFee?, val originCommissionAsset: Asset, val originUsedAsset: Asset ) +val AssetTransferPayload.commissionChainAsset: Chain.Asset + get() = originCommissionAsset.token.configuration + +val AssetTransferPayload.originFeeList: List> + get() = originFee.intoDecimalFeeList() + +val AssetTransferPayload.originFeeListInUsedAsset: List> + get() = if (isSendingCommissionAsset) { + originFeeList + } else { + emptyList() + } + val AssetTransferPayload.isSendingCommissionAsset get() = transfer.originChainAsset == transfer.originChain.commissionAsset val AssetTransferPayload.isReceivingCommissionAsset get() = transfer.destinationChainAsset == transfer.destinationChain.commissionAsset -val AssetTransferPayload.originFeeInUsedAsset: DecimalFee? +val AssetTransferPayload.originFeeInUsedAsset: OriginDecimalFee? get() = if (isSendingCommissionAsset) { originFee } else { diff --git a/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/data/network/blockhain/assets/tranfers/AssetTransfers.kt b/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/data/network/blockhain/assets/tranfers/AssetTransfers.kt index 31af3e14bd..7f9b395a8c 100644 --- a/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/data/network/blockhain/assets/tranfers/AssetTransfers.kt +++ b/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/data/network/blockhain/assets/tranfers/AssetTransfers.kt @@ -3,8 +3,9 @@ package io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets. import io.novafoundation.nova.feature_account_api.data.extrinsic.ExtrinsicSubmission import io.novafoundation.nova.feature_account_api.data.model.Fee import io.novafoundation.nova.feature_account_api.domain.model.MetaAccount +import io.novafoundation.nova.feature_account_api.domain.model.requireAccountIdIn +import io.novafoundation.nova.feature_wallet_api.domain.model.OriginDecimalFee import io.novafoundation.nova.feature_wallet_api.domain.model.Token -import io.novafoundation.nova.feature_wallet_api.presentation.model.DecimalFee import io.novafoundation.nova.runtime.ext.accountIdOrNull import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain import jp.co.soramitsu.fearless_utils.runtime.AccountId @@ -43,10 +44,10 @@ data class WeightedAssetTransfer( override val destinationChainAsset: Chain.Asset, override val commissionAssetToken: Token, override val amount: BigDecimal, - val decimalFee: DecimalFee, + val decimalFee: OriginDecimalFee, ) : AssetTransfer { - constructor(assetTransfer: AssetTransfer, fee: DecimalFee) : this( + constructor(assetTransfer: AssetTransfer, fee: OriginDecimalFee) : this( sender = assetTransfer.sender, recipient = assetTransfer.recipient, originChain = assetTransfer.originChain, @@ -66,6 +67,10 @@ fun AssetTransfer.recipientOrNull(): AccountId? { return destinationChain.accountIdOrNull(recipient) } +fun AssetTransfer.senderAccountId(): AccountId { + return sender.requireAccountIdIn(originChain) +} + interface AssetTransfers { val validationSystem: AssetTransfersValidationSystem diff --git a/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/data/network/crosschain/CrossChainFeeModel.kt b/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/data/network/crosschain/CrossChainFeeModel.kt new file mode 100644 index 0000000000..4fc8d6c765 --- /dev/null +++ b/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/data/network/crosschain/CrossChainFeeModel.kt @@ -0,0 +1,24 @@ +package io.novafoundation.nova.feature_wallet_api.data.network.crosschain + +import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.types.Balance +import java.math.BigInteger + +data class CrossChainFeeModel( + val senderPart: Balance = BigInteger.ZERO, + val holdingPart: Balance = BigInteger.ZERO +) { + companion object +} + +fun CrossChainFeeModel.Companion.zero() = CrossChainFeeModel() + +operator fun CrossChainFeeModel.plus(other: CrossChainFeeModel) = CrossChainFeeModel( + senderPart = senderPart + other.senderPart, + holdingPart = holdingPart + other.holdingPart +) + +fun CrossChainFeeModel?.orZero() = if (this == null) { + CrossChainFeeModel.zero() +} else { + this +} diff --git a/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/data/network/crosschain/CrossChainTransactor.kt b/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/data/network/crosschain/CrossChainTransactor.kt index 5fa0be13ca..033d7ffd99 100644 --- a/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/data/network/crosschain/CrossChainTransactor.kt +++ b/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/data/network/crosschain/CrossChainTransactor.kt @@ -4,8 +4,8 @@ import io.novafoundation.nova.feature_account_api.data.extrinsic.ExtrinsicSubmis import io.novafoundation.nova.feature_account_api.data.model.Fee import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.tranfers.AssetTransfer import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.tranfers.AssetTransfersValidationSystem +import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.types.Balance import io.novafoundation.nova.feature_wallet_api.domain.model.CrossChainTransferConfiguration -import java.math.BigInteger interface CrossChainTransactor { @@ -19,6 +19,6 @@ interface CrossChainTransactor { suspend fun performTransfer( configuration: CrossChainTransferConfiguration, transfer: AssetTransfer, - crossChainFee: BigInteger, + crossChainFee: Balance ): Result } diff --git a/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/data/network/crosschain/CrossChainWeighter.kt b/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/data/network/crosschain/CrossChainWeighter.kt index 711dd85f9a..e469a8b5b3 100644 --- a/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/data/network/crosschain/CrossChainWeighter.kt +++ b/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/data/network/crosschain/CrossChainWeighter.kt @@ -1,17 +1,12 @@ package io.novafoundation.nova.feature_wallet_api.data.network.crosschain import io.novafoundation.nova.common.data.network.runtime.binding.Weight +import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.types.Balance import io.novafoundation.nova.feature_wallet_api.domain.model.CrossChainTransferConfiguration -import java.math.BigInteger - -data class CrossChainFee( - val reserve: BigInteger?, - val destination: BigInteger? -) interface CrossChainWeigher { suspend fun estimateRequiredDestWeight(transferConfiguration: CrossChainTransferConfiguration): Weight - suspend fun estimateFee(transferConfiguration: CrossChainTransferConfiguration): CrossChainFee + suspend fun estimateFee(amount: Balance, config: CrossChainTransferConfiguration): CrossChainFeeModel } diff --git a/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/domain/implementations/CrossChainTransfersConfigurationExt.kt b/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/domain/implementations/CrossChainTransfersConfigurationExt.kt index 6f5f0b28a8..00086343c2 100644 --- a/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/domain/implementations/CrossChainTransfersConfigurationExt.kt +++ b/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/domain/implementations/CrossChainTransfersConfigurationExt.kt @@ -104,15 +104,23 @@ fun CrossChainTransfersConfiguration.transferConfiguration( val hasReserveFee = reserveAssetLocation.chainId !in setOf(originChain.id, destinationChain.id) val reserveFee = if (hasReserveFee) { // reserve fee must be present if there is at least one non-reserve transfer - matchInstructions(reserveAssetLocation.reserveFee!!, reserveAssetLocation.chainId) + matchInstructions(reserveAssetLocation.reserveFee!!, originChain.id, reserveAssetLocation.chainId) } else { null } + val destinationFee = matchInstructions( + destination.destination.fee, + if (hasReserveFee) reserveAssetLocation.chainId else originChain.id, + destination.destination.chainId + ) + return CrossChainTransferConfiguration( + originChainId = originChain.id, assetLocation = originAssetLocationOf(assetTransfers), + reserveChainLocation = reserveAssetLocation.multiLocation, destinationChainLocation = destinationLocation(originChain, destinationParaId), - destinationFee = matchInstructions(destination.destination.fee, destination.destination.chainId), + destinationFee = destinationFee, reserveFee = reserveFee, transferType = destination.type ) @@ -120,14 +128,21 @@ fun CrossChainTransfersConfiguration.transferConfiguration( private fun CrossChainTransfersConfiguration.matchInstructions( xcmFee: XcmFee, - chainId: ChainId, + fromChainId: ChainId, + toChainId: ChainId, ): CrossChainFeeConfiguration { return CrossChainFeeConfiguration( - chainId = chainId, - instructionWeight = instructionBaseWeights.getValue(chainId), - xcmFeeType = XcmFee( - mode = xcmFee.mode, - instructions = feeInstructions.getValue(xcmFee.instructions), + from = CrossChainFeeConfiguration.From( + chainId = fromChainId, + deliveryFeeConfiguration = deliveryFeeConfigurations[fromChainId], + ), + to = CrossChainFeeConfiguration.To( + chainId = toChainId, + instructionWeight = instructionBaseWeights.getValue(toChainId), + xcmFeeType = XcmFee( + mode = xcmFee.mode, + instructions = feeInstructions.getValue(xcmFee.instructions), + ) ) ) } diff --git a/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/domain/model/CrossChainTransfersConfiguration.kt b/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/domain/model/CrossChainTransfersConfiguration.kt index 1e7a9859c3..706c6c58ea 100644 --- a/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/domain/model/CrossChainTransfersConfiguration.kt +++ b/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/domain/model/CrossChainTransfersConfiguration.kt @@ -11,6 +11,7 @@ class CrossChainTransfersConfiguration( // Reserves locations from the Relaychain point of view val assetLocations: Map, val feeInstructions: Map>, + val deliveryFeeConfigurations: Map, val instructionBaseWeights: Map, val chains: Map> ) { @@ -70,8 +71,27 @@ enum class XCMInstructionType { ReserveAssetDeposited, ClearOrigin, BuyExecution, DepositAsset, WithdrawAsset, DepositReserveAsset, ReceiveTeleportedAsset, UNKNOWN } +class DeliveryFeeConfiguration( + val toParent: Type?, + val toParachain: Type? +) { + + sealed interface Type { + class Exponential( + val factorPallet: String, + val sizeBase: BigInteger, + val sizeFactor: BigInteger, + val alwaysHoldingPays: Boolean + ) : Type + + object Undefined : Type + } +} + class CrossChainTransferConfiguration( + val originChainId: ChainId, val assetLocation: MultiLocation, + val reserveChainLocation: MultiLocation, val destinationChainLocation: MultiLocation, val destinationFee: CrossChainFeeConfiguration, val reserveFee: CrossChainFeeConfiguration?, @@ -79,7 +99,15 @@ class CrossChainTransferConfiguration( ) class CrossChainFeeConfiguration( - val chainId: ChainId, - val instructionWeight: Weight, - val xcmFeeType: XcmFee> -) + val from: From, + val to: To +) { + + class From(val chainId: ChainId, val deliveryFeeConfiguration: DeliveryFeeConfiguration?) + + class To( + val chainId: ChainId, + val instructionWeight: Weight, + val xcmFeeType: XcmFee> + ) +} diff --git a/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/domain/model/OriginFee.kt b/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/domain/model/OriginFee.kt new file mode 100644 index 0000000000..a354c6910d --- /dev/null +++ b/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/domain/model/OriginFee.kt @@ -0,0 +1,42 @@ +package io.novafoundation.nova.feature_wallet_api.domain.model + +import io.novafoundation.nova.common.utils.orZero +import io.novafoundation.nova.feature_account_api.data.extrinsic.SubmissionOrigin +import io.novafoundation.nova.feature_account_api.data.model.Fee +import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.GenericFee +import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.SimpleGenericFee +import io.novafoundation.nova.feature_wallet_api.presentation.model.GenericDecimalFee +import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain +import java.math.BigInteger + +typealias OriginGenericFee = SimpleGenericFee + +typealias OriginDecimalFee = GenericDecimalFee + +data class OriginFee( + val networkFee: Fee, + val deliveryPart: Fee?, + val chainAsset: Chain.Asset +) : Fee { + + override val amount: BigInteger = networkFee.amount + deliveryPart?.amount.orZero() + + override val submissionOrigin: SubmissionOrigin = networkFee.submissionOrigin +} + +fun OriginDecimalFee.networkFeePart(): GenericDecimalFee { + return GenericDecimalFee.from(genericFee.networkFee.networkFee, genericFee.networkFee.chainAsset) +} + +fun OriginDecimalFee.deliveryFeePart(): GenericDecimalFee? { + return genericFee.networkFee.deliveryPart?.let { + GenericDecimalFee.from(genericFee.networkFee.deliveryPart, genericFee.networkFee.chainAsset) + } +} + +fun OriginDecimalFee.intoDecimalFeeList(): List> { + return listOfNotNull( + networkFeePart(), + deliveryFeePart() + ) +} diff --git a/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/domain/validation/EnoughAmountToTransferValidation.kt b/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/domain/validation/EnoughAmountToTransferValidation.kt index 11d334e60a..2d0b5b66ec 100644 --- a/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/domain/validation/EnoughAmountToTransferValidation.kt +++ b/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/domain/validation/EnoughAmountToTransferValidation.kt @@ -23,13 +23,27 @@ interface NotEnoughToPayFeesError { typealias EnoughAmountToTransferValidation = EnoughAmountToTransferValidationGeneric class EnoughAmountToTransferValidationGeneric( - private val feeExtractor: GenericFeeProducer, + private val feeListExtractor: GenericFeeListProducer, private val availableBalanceProducer: AmountProducer

, private val errorProducer: (ErrorContext

) -> E, private val skippable: Boolean = false, private val extraAmountExtractor: AmountProducer

= { BigDecimal.ZERO }, ) : Validation { + constructor( + extraAmountExtractor: AmountProducer

= { BigDecimal.ZERO }, + feeExtractor: GenericFeeProducer, + availableBalanceProducer: AmountProducer

, + errorProducer: (ErrorContext

) -> E, + skippable: Boolean = false, + ) : this( + feeListExtractor = { listOfNotNull(feeExtractor(it)) }, + extraAmountExtractor = extraAmountExtractor, + availableBalanceProducer = availableBalanceProducer, + errorProducer = errorProducer, + skippable = skippable + ) + class ErrorContext

( val payload: P, @@ -42,7 +56,7 @@ class EnoughAmountToTransferValidationGeneric( companion object; override suspend fun validate(value: P): ValidationStatus { - val fee = feeExtractor(value).networkFeeByRequestedAccountOrZero + val fee = feeListExtractor(value).sumOf { it.networkFeeByRequestedAccountOrZero } val available = availableBalanceProducer(value) val amount = extraAmountExtractor(value) @@ -58,6 +72,22 @@ class EnoughAmountToTransferValidationGeneric( } } +fun ValidationSystemBuilder.sufficientBalanceMultyFee( + feeExtractor: GenericFeeListProducer = { emptyList() }, + amount: AmountProducer

= { BigDecimal.ZERO }, + available: AmountProducer

, + error: (EnoughAmountToTransferValidationGeneric.ErrorContext

) -> E, + skippable: Boolean = false +) = validate( + EnoughAmountToTransferValidationGeneric( + feeListExtractor = feeExtractor, + extraAmountExtractor = amount, + errorProducer = error, + skippable = skippable, + availableBalanceProducer = available + ) +) + fun ValidationSystemBuilder.sufficientBalance( fee: FeeProducer

= { null }, amount: AmountProducer

= { BigDecimal.ZERO }, @@ -81,7 +111,7 @@ fun ValidationSystemBuilder.sufficientBalanceGeneri error: (EnoughAmountToTransferValidationGeneric.ErrorContext

) -> E, skippable: Boolean = false ) = validate( - EnoughAmountToTransferValidationGeneric( + EnoughAmountToTransferValidationGeneric( feeExtractor = fee, extraAmountExtractor = amount, errorProducer = error, diff --git a/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/domain/validation/EnoughBalanceToStayAboveEDValidation.kt b/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/domain/validation/EnoughBalanceToStayAboveEDValidation.kt index bde43a625f..f265c383ff 100644 --- a/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/domain/validation/EnoughBalanceToStayAboveEDValidation.kt +++ b/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/domain/validation/EnoughBalanceToStayAboveEDValidation.kt @@ -6,6 +6,7 @@ import io.novafoundation.nova.common.validation.ValidationSystemBuilder import io.novafoundation.nova.common.validation.validOrError import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.AssetSourceRegistry import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.existentialDeposit +import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.GenericFee import io.novafoundation.nova.feature_wallet_api.presentation.model.networkFeeByRequestedAccountOrZero import io.novafoundation.nova.runtime.multiNetwork.ChainWithAsset import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain @@ -15,9 +16,9 @@ interface InsufficientBalanceToStayAboveEDError { val asset: Chain.Asset } -class EnoughBalanceToStayAboveEDValidation( +class EnoughBalanceToStayAboveEDValidation( private val assetSourceRegistry: AssetSourceRegistry, - private val fee: FeeProducer

, + private val fee: GenericFeeProducer, private val balance: AmountProducer

, private val chainWithAsset: (P) -> ChainWithAsset, private val error: (P, BigDecimal) -> E @@ -35,12 +36,12 @@ class EnoughBalanceToStayAboveEDValidation( class EnoughTotalToStayAboveEDValidationFactory(private val assetSourceRegistry: AssetSourceRegistry) { - fun create( - fee: FeeProducer

, + fun create( + fee: GenericFeeProducer, balance: AmountProducer

, chainWithAsset: (P) -> ChainWithAsset, error: (P, BigDecimal) -> E - ): EnoughBalanceToStayAboveEDValidation { + ): EnoughBalanceToStayAboveEDValidation { return EnoughBalanceToStayAboveEDValidation( assetSourceRegistry = assetSourceRegistry, fee = fee, @@ -52,8 +53,8 @@ class EnoughTotalToStayAboveEDValidationFactory(private val assetSourceRegistry: } context(ValidationSystemBuilder) -fun EnoughTotalToStayAboveEDValidationFactory.validate( - fee: FeeProducer

, +fun EnoughTotalToStayAboveEDValidationFactory.validate( + fee: GenericFeeProducer, balance: AmountProducer

, chainWithAsset: (P) -> ChainWithAsset, error: (P, BigDecimal) -> E diff --git a/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/domain/validation/ExistentialDepositValidation.kt b/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/domain/validation/ExistentialDepositValidation.kt index 57277f61ff..77dd5d6950 100644 --- a/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/domain/validation/ExistentialDepositValidation.kt +++ b/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/domain/validation/ExistentialDepositValidation.kt @@ -4,14 +4,15 @@ import io.novafoundation.nova.common.validation.Validation import io.novafoundation.nova.common.validation.ValidationStatus import io.novafoundation.nova.common.validation.ValidationSystemBuilder import io.novafoundation.nova.common.validation.validOrWarning +import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.GenericFee import io.novafoundation.nova.feature_wallet_api.presentation.model.networkFeeByRequestedAccountOrZero import java.math.BigDecimal typealias ExistentialDepositError = (remainingAmount: BigDecimal, payload: P) -> E -class ExistentialDepositValidation( +class ExistentialDepositValidation( private val countableTowardsEdBalance: AmountProducer

, - private val feeProducer: FeeProducer

, + private val feeProducer: GenericFeeListProducer, private val extraAmountProducer: AmountProducer

, private val errorProducer: ExistentialDepositError, private val existentialDeposit: AmountProducer

@@ -21,10 +22,10 @@ class ExistentialDepositValidation( val existentialDeposit = existentialDeposit(value) val countableTowardsEd = countableTowardsEdBalance(value) - val fee = feeProducer(value) + val fee = feeProducer(value).sumOf { it.networkFeeByRequestedAccountOrZero } val extraAmount = extraAmountProducer(value) - val remainingAmount = countableTowardsEd - fee.networkFeeByRequestedAccountOrZero - extraAmount + val remainingAmount = countableTowardsEd - fee - extraAmount return validOrWarning(remainingAmount >= existentialDeposit) { errorProducer(remainingAmount, value) @@ -32,9 +33,9 @@ class ExistentialDepositValidation( } } -fun ValidationSystemBuilder.doNotCrossExistentialDeposit( +fun ValidationSystemBuilder.doNotCrossExistentialDepositMultyFee( countableTowardsEdBalance: AmountProducer

, - fee: FeeProducer

= { null }, + fee: GenericFeeListProducer = { emptyList() }, extraAmount: AmountProducer

= { BigDecimal.ZERO }, existentialDeposit: AmountProducer

, error: ExistentialDepositError, @@ -47,3 +48,19 @@ fun ValidationSystemBuilder.doNotCrossExistentialDeposit( existentialDeposit = existentialDeposit ) ) + +fun ValidationSystemBuilder.doNotCrossExistentialDepositInUsedAsset( + countableTowardsEdBalance: AmountProducer

, + fee: FeeProducer

= { null }, + extraAmount: AmountProducer

= { BigDecimal.ZERO }, + existentialDeposit: AmountProducer

, + error: ExistentialDepositError, +) = validate( + ExistentialDepositValidation( + countableTowardsEdBalance = countableTowardsEdBalance, + feeProducer = { listOfNotNull(fee(it)) }, + extraAmountProducer = extraAmount, + errorProducer = error, + existentialDeposit = existentialDeposit + ) +) diff --git a/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/domain/validation/Producers.kt b/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/domain/validation/Producers.kt index e2cf48675e..a6573fe692 100644 --- a/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/domain/validation/Producers.kt +++ b/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/domain/validation/Producers.kt @@ -12,3 +12,5 @@ typealias PlanksProducer

= suspend (P) -> BigInteger typealias FeeProducer

= suspend (P) -> DecimalFee? typealias GenericFeeProducer = suspend (P) -> GenericDecimalFee? + +typealias GenericFeeListProducer = suspend (P) -> List> diff --git a/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/presentation/mixin/fee/FeeLoaderMixin.kt b/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/presentation/mixin/fee/FeeLoaderMixin.kt index 4e261044e2..cd678f3201 100644 --- a/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/presentation/mixin/fee/FeeLoaderMixin.kt +++ b/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/presentation/mixin/fee/FeeLoaderMixin.kt @@ -41,6 +41,8 @@ interface GenericFee { @JvmInline value class SimpleFee(override val networkFee: Fee) : GenericFee +class SimpleGenericFee(override val networkFee: T) : GenericFee + interface GenericFeeLoaderMixin : Retriable { class Configuration( @@ -148,36 +150,10 @@ fun GenericFeeLoaderMixin.getDecimalFeeOrNull(): GenericDeci ?.decimalFee } +fun FeeLoaderMixin.Factory.createGeneric(assetFlow: Flow) = createGeneric(assetFlow.map { it.token }) fun FeeLoaderMixin.Factory.create(assetFlow: Flow) = create(assetFlow.map { it.token }) fun FeeLoaderMixin.Factory.create(tokenUseCase: TokenUseCase) = create(tokenUseCase.currentTokenFlow()) -fun FeeLoaderMixin.Presentation.connectWith( - inputSource1: Flow, - inputSource2: Flow, - inputSource3: Flow, - inputSource4: Flow, - scope: CoroutineScope, - expectedChain: ((I1, I2, I3, I4) -> ChainId)? = null, - feeConstructor: suspend Token.(input1: I1, input2: I2, input3: I3, input4: I4) -> Fee?, - onRetryCancelled: () -> Unit = {} -) { - combine( - inputSource1, - inputSource2, - inputSource3, - inputSource4 - ) { input1, input2, input3, input4 -> - loadFee( - coroutineScope = scope, - expectedChain = expectedChain?.invoke(input1, input2, input3, input4), - feeConstructor = { feeConstructor(it, input1, input2, input3, input4) }, - onRetryCancelled = onRetryCancelled - ) - } - .inBackground() - .launchIn(scope) -} - fun FeeLoaderMixin.Presentation.connectWith( inputSource: Flow, scope: CoroutineScope, diff --git a/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/presentation/model/FeeModel.kt b/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/presentation/model/FeeModel.kt index 204780318c..b05eb84d55 100644 --- a/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/presentation/model/FeeModel.kt +++ b/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/presentation/model/FeeModel.kt @@ -3,6 +3,7 @@ package io.novafoundation.nova.feature_wallet_api.presentation.model import io.novafoundation.nova.common.utils.orZero import io.novafoundation.nova.feature_account_api.data.model.Fee import io.novafoundation.nova.feature_account_api.data.model.requestedAccountPaysFees +import io.novafoundation.nova.feature_wallet_api.domain.model.amountFromPlanks import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.GenericFee import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.SimpleFee import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain @@ -22,6 +23,17 @@ class GenericDecimalFee( ) { val networkFee: Fee = genericFee.networkFee + + companion object { + fun from(genericFee: F, chainAsset: Chain.Asset): GenericDecimalFee { + val decimalAmount = chainAsset.amountFromPlanks(genericFee.networkFee.amount) + return GenericDecimalFee(genericFee, decimalAmount) + } + + fun from(fee: Fee, chainAsset: Chain.Asset): GenericDecimalFee { + return from(SimpleFee(fee), chainAsset) + } + } } val GenericDecimalFee.networkFeeByRequestedAccount: BigDecimal diff --git a/feature-wallet-impl/build.gradle b/feature-wallet-impl/build.gradle index d98096b74b..ff8927af58 100644 --- a/feature-wallet-impl/build.gradle +++ b/feature-wallet-impl/build.gradle @@ -18,7 +18,7 @@ android { buildConfigField "String", "EHTERSCAN_API_KEY_MOONRIVER", readStringSecret("EHTERSCAN_API_KEY_MOONRIVER") buildConfigField "String", "EHTERSCAN_API_KEY_ETHEREUM", readStringSecret("EHTERSCAN_API_KEY_ETHEREUM") - buildConfigField "String", "CROSS_CHAIN_CONFIG_URL", "\"https://raw.githubusercontent.com/novasamatech/nova-utils/master/xcm/v5/transfers_dev.json\"" + buildConfigField "String", "CROSS_CHAIN_CONFIG_URL", "\"https://raw.githubusercontent.com/novasamatech/nova-utils/master/xcm/v6/transfers_dev.json\"" } buildTypes { diff --git a/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/data/mappers/CrossChain.kt b/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/data/mappers/CrossChain.kt index 0273d28fad..669be219d0 100644 --- a/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/data/mappers/CrossChain.kt +++ b/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/data/mappers/CrossChain.kt @@ -8,11 +8,15 @@ import io.novafoundation.nova.feature_wallet_api.domain.model.CrossChainTransfer import io.novafoundation.nova.feature_wallet_api.domain.model.CrossChainTransfersConfiguration.XcmDestination import io.novafoundation.nova.feature_wallet_api.domain.model.CrossChainTransfersConfiguration.XcmFee import io.novafoundation.nova.feature_wallet_api.domain.model.CrossChainTransfersConfiguration.XcmTransfer +import io.novafoundation.nova.feature_wallet_api.domain.model.DeliveryFeeConfiguration +import io.novafoundation.nova.feature_wallet_api.domain.model.DeliveryFeeConfiguration.Type import io.novafoundation.nova.feature_wallet_api.domain.model.XCMInstructionType import io.novafoundation.nova.feature_wallet_api.domain.model.XcmTransferType import io.novafoundation.nova.feature_wallet_impl.data.network.crosschain.CrossChainOriginAssetRemote import io.novafoundation.nova.feature_wallet_impl.data.network.crosschain.CrossChainTransfersConfigRemote +import io.novafoundation.nova.feature_wallet_impl.data.network.crosschain.DeliveryFeeConfigRemote import io.novafoundation.nova.feature_wallet_impl.data.network.crosschain.JunctionsRemote +import io.novafoundation.nova.feature_wallet_impl.data.network.crosschain.NetworkDeliveryFeeRemote import io.novafoundation.nova.feature_wallet_impl.data.network.crosschain.ReserveLocationRemote import io.novafoundation.nova.feature_wallet_impl.data.network.crosschain.XcmDestinationRemote import io.novafoundation.nova.feature_wallet_impl.data.network.crosschain.XcmFeeRemote @@ -36,14 +40,41 @@ fun mapCrossChainConfigFromRemote(remote: CrossChainTransfersConfigRemote): Cros valueTransform = { it.assets.map(::mapAssetTransfersFromRemote) } ) + val networkDeliveryFee = remote.networkDeliveryFee.mapValues { (_, networkDeliveryFeeRemote) -> + mapNetworkDeliveryFeeFromRemote(networkDeliveryFeeRemote) + } + return CrossChainTransfersConfiguration( assetLocations = assetsLocations, feeInstructions = feeInstructions, instructionBaseWeights = remote.networkBaseWeight, + deliveryFeeConfigurations = networkDeliveryFee, chains = chains ) } +fun mapNetworkDeliveryFeeFromRemote(networkDeliveryFeeRemote: NetworkDeliveryFeeRemote): DeliveryFeeConfiguration { + return DeliveryFeeConfiguration( + toParent = mapDeliveryFeeConfigFromRemote(networkDeliveryFeeRemote.toParent), + toParachain = mapDeliveryFeeConfigFromRemote(networkDeliveryFeeRemote.toParachain) + ) +} + +fun mapDeliveryFeeConfigFromRemote(config: DeliveryFeeConfigRemote?): DeliveryFeeConfiguration.Type? { + if (config == null) return null + + return when (config.type) { + "exponential" -> DeliveryFeeConfiguration.Type.Exponential( + factorPallet = config.factorPallet, + sizeBase = config.sizeBase, + sizeFactor = config.sizeFactor, + alwaysHoldingPays = config.alwaysHoldingPays ?: false + ) + + else -> throw IllegalArgumentException("Unknown delivery fee config type: ${config.type}") + } +} + private fun mapReserveLocationFromRemote(reserveLocationRemote: ReserveLocationRemote): ReserveLocation { return ReserveLocation( chainId = reserveLocationRemote.chainId, @@ -61,6 +92,7 @@ private fun mapAssetTransfersFromRemote(remote: CrossChainOriginAssetRemote): As AssetLocationPath.Concrete(mapJunctionsRemoteToMultiLocation(junctionsRemote)) } + else -> throw IllegalArgumentException("Unknown asset type") } diff --git a/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/data/network/blockchain/assets/transfers/BaseAssetTransfers.kt b/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/data/network/blockchain/assets/transfers/BaseAssetTransfers.kt index df4967b931..c9d0df2806 100644 --- a/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/data/network/blockchain/assets/transfers/BaseAssetTransfers.kt +++ b/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/data/network/blockchain/assets/transfers/BaseAssetTransfers.kt @@ -12,10 +12,9 @@ import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.t import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.tranfers.AssetTransfersValidationSystem import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.tranfers.AssetTransfersValidationSystemBuilder import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.tranfers.WeightedAssetTransfer -import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.tranfers.originFeeInUsedAsset import io.novafoundation.nova.feature_wallet_api.domain.validation.EnoughTotalToStayAboveEDValidationFactory import io.novafoundation.nova.feature_wallet_api.domain.validation.PhishingValidationFactory -import io.novafoundation.nova.feature_wallet_impl.data.network.blockchain.assets.transfers.validations.doNotCrossExistentialDeposit +import io.novafoundation.nova.feature_wallet_impl.data.network.blockchain.assets.transfers.validations.doNotCrossExistentialDepositInUsedAsset import io.novafoundation.nova.feature_wallet_impl.data.network.blockchain.assets.transfers.validations.notDeadRecipientInCommissionAsset import io.novafoundation.nova.feature_wallet_impl.data.network.blockchain.assets.transfers.validations.notDeadRecipientInUsedAsset import io.novafoundation.nova.feature_wallet_impl.data.network.blockchain.assets.transfers.validations.notPhishingRecipient @@ -90,9 +89,8 @@ abstract class BaseAssetTransfers( recipientCanAcceptTransfer(assetSourceRegistry) } - private fun AssetTransfersValidationSystemBuilder.doNotCrossExistentialDeposit() = doNotCrossExistentialDeposit( + private fun AssetTransfersValidationSystemBuilder.doNotCrossExistentialDeposit() = doNotCrossExistentialDepositInUsedAsset( assetSourceRegistry = assetSourceRegistry, - fee = { it.originFeeInUsedAsset }, extraAmount = { it.transfer.amount }, ) } diff --git a/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/data/network/blockchain/assets/transfers/validations/Common.kt b/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/data/network/blockchain/assets/transfers/validations/Common.kt index 674c352673..69f00884f4 100644 --- a/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/data/network/blockchain/assets/transfers/validations/Common.kt +++ b/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/data/network/blockchain/assets/transfers/validations/Common.kt @@ -8,20 +8,24 @@ import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.t import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.tranfers.AssetTransferValidationFailure import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.tranfers.AssetTransferValidationFailure.WillRemoveAccount import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.tranfers.AssetTransfersValidationSystemBuilder +import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.tranfers.originFeeList +import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.tranfers.originFeeListInUsedAsset import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.tranfers.recipientOrNull import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.tranfers.sendingAmountInCommissionAsset import io.novafoundation.nova.feature_wallet_api.domain.model.balanceCountedTowardsED +import io.novafoundation.nova.feature_wallet_api.domain.model.networkFeePart import io.novafoundation.nova.feature_wallet_api.domain.validation.AmountProducer import io.novafoundation.nova.feature_wallet_api.domain.validation.EnoughTotalToStayAboveEDValidationFactory -import io.novafoundation.nova.feature_wallet_api.domain.validation.FeeProducer import io.novafoundation.nova.feature_wallet_api.domain.validation.PhishingValidationFactory -import io.novafoundation.nova.feature_wallet_api.domain.validation.checkForSimpleFeeChanges -import io.novafoundation.nova.feature_wallet_api.domain.validation.doNotCrossExistentialDeposit +import io.novafoundation.nova.feature_wallet_api.domain.validation.checkForFeeChanges +import io.novafoundation.nova.feature_wallet_api.domain.validation.doNotCrossExistentialDepositMultyFee import io.novafoundation.nova.feature_wallet_api.domain.validation.notPhishingAccount import io.novafoundation.nova.feature_wallet_api.domain.validation.positiveAmount import io.novafoundation.nova.feature_wallet_api.domain.validation.sufficientBalance +import io.novafoundation.nova.feature_wallet_api.domain.validation.sufficientBalanceMultyFee import io.novafoundation.nova.feature_wallet_api.domain.validation.validAddress import io.novafoundation.nova.feature_wallet_api.domain.validation.validate +import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.SimpleGenericFee import io.novafoundation.nova.runtime.ext.commissionAsset import io.novafoundation.nova.runtime.multiNetwork.ChainWithAsset import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain @@ -52,7 +56,7 @@ fun AssetTransfersValidationSystemBuilder.sufficientCommissionBalanceToStayAbove enoughTotalToStayAboveEDValidationFactory: EnoughTotalToStayAboveEDValidationFactory ) { enoughTotalToStayAboveEDValidationFactory.validate( - fee = { it.originFee }, + fee = { it.originFee.networkFeePart() }, balance = { it.originCommissionAsset.balanceCountedTowardsED() }, chainWithAsset = { ChainWithAsset(it.transfer.originChain, it.transfer.originChain.commissionAsset) }, error = { payload, _ -> AssetTransferValidationFailure.NotEnoughFunds.ToStayAboveED(payload.transfer.originChain.commissionAsset) } @@ -61,32 +65,32 @@ fun AssetTransfersValidationSystemBuilder.sufficientCommissionBalanceToStayAbove fun AssetTransfersValidationSystemBuilder.checkForFeeChanges( assetSourceRegistry: AssetSourceRegistry -) = checkForSimpleFeeChanges( - calculateFee = { - val transfers = assetSourceRegistry.sourceFor(it.transfer.originChainAsset).transfers - transfers.calculateFee(it.transfer) +) = checkForFeeChanges( + calculateFee = { payload -> + val transfers = assetSourceRegistry.sourceFor(payload.transfer.originChainAsset).transfers + val fee = transfers.calculateFee(payload.transfer) + SimpleGenericFee(payload.originFee.genericFee.networkFee.copy(networkFee = fee)) }, currentFee = { it.originFee }, chainAsset = { it.transfer.commissionAssetToken.configuration }, error = AssetTransferValidationFailure::FeeChangeDetected ) -fun AssetTransfersValidationSystemBuilder.doNotCrossExistentialDeposit( +fun AssetTransfersValidationSystemBuilder.doNotCrossExistentialDepositInUsedAsset( assetSourceRegistry: AssetSourceRegistry, - fee: FeeProducer, extraAmount: AmountProducer, -) = doNotCrossExistentialDeposit( +) = doNotCrossExistentialDepositMultyFee( countableTowardsEdBalance = { it.originUsedAsset.balanceCountedTowardsED() }, - fee = fee, + fee = { it.originFeeListInUsedAsset }, extraAmount = extraAmount, existentialDeposit = { assetSourceRegistry.existentialDepositForUsedAsset(it.transfer) }, error = { remainingAmount, payload -> payload.transfer.originChainAsset.existentialDepositError(remainingAmount) } ) -fun AssetTransfersValidationSystemBuilder.sufficientTransferableBalanceToPayOriginFee() = sufficientBalance( +fun AssetTransfersValidationSystemBuilder.sufficientTransferableBalanceToPayOriginFee() = sufficientBalanceMultyFee( available = { it.originCommissionAsset.transferable }, amount = { it.sendingAmountInCommissionAsset }, - fee = { it.originFee }, + feeExtractor = { it.originFeeList }, error = { context -> AssetTransferValidationFailure.NotEnoughFunds.InCommissionAsset( chainAsset = context.payload.transfer.originChain.commissionAsset, diff --git a/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/data/network/crosschain/CrossChainConfigRemote.kt b/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/data/network/crosschain/CrossChainConfigRemote.kt index 9296f9c5f6..50c86242fb 100644 --- a/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/data/network/crosschain/CrossChainConfigRemote.kt +++ b/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/data/network/crosschain/CrossChainConfigRemote.kt @@ -6,6 +6,7 @@ import java.math.BigInteger class CrossChainTransfersConfigRemote( val assetsLocation: Map, val instructions: Map>, + val networkDeliveryFee: Map, val networkBaseWeight: Map, val chains: List ) @@ -16,6 +17,19 @@ class ReserveLocationRemote( val multiLocation: JunctionsRemote ) +class NetworkDeliveryFeeRemote( + val toParent: DeliveryFeeConfigRemote?, + val toParachain: DeliveryFeeConfigRemote? +) + +class DeliveryFeeConfigRemote( + val type: String, + val factorPallet: String, + val sizeBase: BigInteger, + val sizeFactor: BigInteger, + val alwaysHoldingPays: Boolean? +) + class CrossChainOriginChainRemote( val chainId: ChainId, val assets: List diff --git a/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/data/network/crosschain/ExtrinsicBuilderExt.kt b/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/data/network/crosschain/ExtrinsicBuilderExt.kt index 245330300a..176f604b6c 100644 --- a/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/data/network/crosschain/ExtrinsicBuilderExt.kt +++ b/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/data/network/crosschain/ExtrinsicBuilderExt.kt @@ -53,7 +53,7 @@ private fun Type<*>.isWeightV1(): Boolean { return this is NumberType } -private fun VersionedXcm.toEncodableInstance() = when (this) { +fun VersionedXcm.toEncodableInstance() = when (this) { is VersionedXcm.V2 -> DictEnum.Entry( name = "V2", value = message.toEncodableInstance() @@ -87,6 +87,7 @@ private fun XcmV2Instruction.toEncodableInstance() = when (this) { name = "WithdrawAsset", value = assets.toEncodableInstance() ) + is XcmV2Instruction.DepositAsset -> DictEnum.Entry( name = "DepositAsset", value = structOf( @@ -95,6 +96,7 @@ private fun XcmV2Instruction.toEncodableInstance() = when (this) { "beneficiary" to beneficiary.toEncodableInstance() ) ) + is XcmV2Instruction.BuyExecution -> DictEnum.Entry( name = "BuyExecution", value = structOf( @@ -103,14 +105,17 @@ private fun XcmV2Instruction.toEncodableInstance() = when (this) { "weight_limit" to weightLimit.toV1EncodableInstance() ) ) + XcmV2Instruction.ClearOrigin -> DictEnum.Entry( name = "ClearOrigin", value = null ) + is XcmV2Instruction.ReserveAssetDeposited -> DictEnum.Entry( name = "ReserveAssetDeposited", value = assets.toEncodableInstance() ) + is XcmV2Instruction.DepositReserveAsset -> DictEnum.Entry( name = "DepositReserveAsset", value = structOf( @@ -120,6 +125,7 @@ private fun XcmV2Instruction.toEncodableInstance() = when (this) { "xcm" to xcm.toEncodableInstance() ) ) + is XcmV2Instruction.ReceiveTeleportedAsset -> DictEnum.Entry( name = "ReceiveTeleportedAsset", value = assets.toEncodableInstance() @@ -151,6 +157,7 @@ fun VersionedMultiAssets.toEncodableInstance() = when (this) { name = "V1", value = assets.toEncodableInstance() ) + is VersionedMultiAssets.V2 -> DictEnum.Entry( name = "V2", value = assets.toEncodableInstance() @@ -162,6 +169,7 @@ fun VersionedMultiAsset.toEncodableInstance() = when (this) { name = "V1", value = asset.toEncodableInstance() ) + is VersionedMultiAsset.V2 -> DictEnum.Entry( name = "V2", value = asset.toEncodableInstance() @@ -173,6 +181,7 @@ fun VersionedMultiLocation.toEncodableInstance() = when (this) { name = "V1", value = multiLocation.toEncodableInstance() ) + is VersionedMultiLocation.V2 -> DictEnum.Entry( name = "V2", value = multiLocation.toEncodableInstance() diff --git a/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/data/network/crosschain/RealCrossChainTransactor.kt b/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/data/network/crosschain/RealCrossChainTransactor.kt index 34e5f0b643..8055d58f97 100644 --- a/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/data/network/crosschain/RealCrossChainTransactor.kt +++ b/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/data/network/crosschain/RealCrossChainTransactor.kt @@ -1,6 +1,7 @@ package io.novafoundation.nova.feature_wallet_impl.data.network.crosschain import io.novafoundation.nova.common.data.network.runtime.binding.Weight +import io.novafoundation.nova.common.utils.orZero import io.novafoundation.nova.common.utils.xTokensName import io.novafoundation.nova.common.utils.xcmPalletName import io.novafoundation.nova.common.validation.ValidationSystem @@ -11,7 +12,6 @@ import io.novafoundation.nova.feature_account_api.data.model.Fee import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.AssetSourceRegistry import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.tranfers.AssetTransfer import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.tranfers.AssetTransfersValidationSystem -import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.tranfers.originFeeInUsedAsset import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.types.Balance import io.novafoundation.nova.feature_wallet_api.data.network.crosschain.CrossChainTransactor import io.novafoundation.nova.feature_wallet_api.data.network.crosschain.CrossChainWeigher @@ -22,8 +22,7 @@ import io.novafoundation.nova.feature_wallet_api.domain.model.XcmTransferType import io.novafoundation.nova.feature_wallet_api.domain.model.planksFromAmount import io.novafoundation.nova.feature_wallet_api.domain.validation.EnoughTotalToStayAboveEDValidationFactory import io.novafoundation.nova.feature_wallet_api.domain.validation.PhishingValidationFactory -import io.novafoundation.nova.feature_wallet_api.presentation.model.networkFeeByRequestedAccountOrZero -import io.novafoundation.nova.feature_wallet_impl.data.network.blockchain.assets.transfers.validations.doNotCrossExistentialDeposit +import io.novafoundation.nova.feature_wallet_impl.data.network.blockchain.assets.transfers.validations.doNotCrossExistentialDepositInUsedAsset import io.novafoundation.nova.feature_wallet_impl.data.network.blockchain.assets.transfers.validations.notDeadRecipientInCommissionAsset import io.novafoundation.nova.feature_wallet_impl.data.network.blockchain.assets.transfers.validations.notDeadRecipientInUsedAsset import io.novafoundation.nova.feature_wallet_impl.data.network.blockchain.assets.transfers.validations.notPhishingRecipient @@ -62,10 +61,9 @@ class RealCrossChainTransactor( sufficientTransferableBalanceToPayOriginFee() canPayCrossChainFee() - doNotCrossExistentialDeposit( + doNotCrossExistentialDepositInUsedAsset( assetSourceRegistry = assetSourceRegistry, - fee = { it.originFeeInUsedAsset }, - extraAmount = { it.transfer.amount + it.crossChainFee.networkFeeByRequestedAccountOrZero } + extraAmount = { it.transfer.amount + it.crossChainFee?.networkFeeDecimalAmount.orZero() } ) } @@ -78,7 +76,7 @@ class RealCrossChainTransactor( override suspend fun performTransfer( configuration: CrossChainTransferConfiguration, transfer: AssetTransfer, - crossChainFee: BigInteger + crossChainFee: Balance ): Result { return extrinsicService.submitExtrinsic(transfer.originChain, TransactionOrigin.SelectedWallet) { crossChainTransfer(configuration, transfer, crossChainFee) @@ -88,7 +86,7 @@ class RealCrossChainTransactor( private suspend fun ExtrinsicBuilder.crossChainTransfer( configuration: CrossChainTransferConfiguration, transfer: AssetTransfer, - crossChainFee: BigInteger + crossChainFee: Balance ) { when (configuration.transferType) { XcmTransferType.X_TOKENS -> xTokensTransfer(configuration, transfer, crossChainFee) @@ -101,7 +99,7 @@ class RealCrossChainTransactor( private suspend fun ExtrinsicBuilder.xTokensTransfer( configuration: CrossChainTransferConfiguration, assetTransfer: AssetTransfer, - crossChainFee: BigInteger + crossChainFee: Balance ) { val multiAsset = configuration.multiAssetFor(assetTransfer, crossChainFee) val fullDestinationLocation = configuration.destinationChainLocation + assetTransfer.beneficiaryLocation() @@ -128,7 +126,7 @@ class RealCrossChainTransactor( private suspend fun ExtrinsicBuilder.xcmPalletReserveTransfer( configuration: CrossChainTransferConfiguration, assetTransfer: AssetTransfer, - crossChainFee: BigInteger + crossChainFee: Balance ) { xcmPalletTransfer( configuration = configuration, @@ -141,7 +139,7 @@ class RealCrossChainTransactor( private suspend fun ExtrinsicBuilder.xcmPalletTeleport( configuration: CrossChainTransferConfiguration, assetTransfer: AssetTransfer, - crossChainFee: BigInteger + crossChainFee: Balance ) { xcmPalletTransfer( configuration = configuration, @@ -154,7 +152,7 @@ class RealCrossChainTransactor( private suspend fun ExtrinsicBuilder.xcmPalletTransfer( configuration: CrossChainTransferConfiguration, assetTransfer: AssetTransfer, - crossChainFee: BigInteger, + crossChainFee: Balance, callName: String ) { val lowestMultiLocationVersion = palletXcmRepository.lowestPresentMultiLocationVersion(assetTransfer.originChain.id) @@ -177,7 +175,7 @@ class RealCrossChainTransactor( private fun CrossChainTransferConfiguration.multiAssetFor( transfer: AssetTransfer, - crossChainFee: BigInteger + crossChainFee: Balance ): XcmMultiAsset { // we add cross chain fee top of entered amount so received amount will be no less than entered one val planks = transfer.originChainAsset.planksFromAmount(transfer.amount) + crossChainFee diff --git a/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/data/network/crosschain/RealCrossChainWeigher.kt b/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/data/network/crosschain/RealCrossChainWeigher.kt index ffb72de47f..c5dc950906 100644 --- a/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/data/network/crosschain/RealCrossChainWeigher.kt +++ b/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/data/network/crosschain/RealCrossChainWeigher.kt @@ -1,27 +1,51 @@ package io.novafoundation.nova.feature_wallet_impl.data.network.crosschain +import io.novafoundation.nova.common.data.network.runtime.binding.ParaId import io.novafoundation.nova.common.data.network.runtime.binding.Weight +import io.novafoundation.nova.common.data.network.runtime.binding.bindNumber +import io.novafoundation.nova.common.utils.BigRational +import io.novafoundation.nova.common.utils.argument +import io.novafoundation.nova.common.utils.fixedU128 import io.novafoundation.nova.common.utils.orZero +import io.novafoundation.nova.common.utils.requireActualType +import io.novafoundation.nova.common.utils.xcmPalletName import io.novafoundation.nova.feature_account_api.data.ethereum.transaction.TransactionOrigin import io.novafoundation.nova.feature_account_api.data.extrinsic.ExtrinsicService import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.types.Balance -import io.novafoundation.nova.feature_wallet_api.data.network.crosschain.CrossChainFee +import io.novafoundation.nova.feature_wallet_api.data.network.crosschain.CrossChainFeeModel import io.novafoundation.nova.feature_wallet_api.data.network.crosschain.CrossChainWeigher +import io.novafoundation.nova.feature_wallet_api.data.network.crosschain.orZero +import io.novafoundation.nova.feature_wallet_api.data.network.crosschain.plus +import io.novafoundation.nova.feature_wallet_api.data.network.crosschain.zero import io.novafoundation.nova.feature_wallet_api.domain.implementations.accountIdToMultiLocation import io.novafoundation.nova.feature_wallet_api.domain.implementations.weightToFee import io.novafoundation.nova.feature_wallet_api.domain.model.CrossChainFeeConfiguration import io.novafoundation.nova.feature_wallet_api.domain.model.CrossChainTransferConfiguration import io.novafoundation.nova.feature_wallet_api.domain.model.CrossChainTransfersConfiguration.XcmFee.Mode +import io.novafoundation.nova.feature_wallet_api.domain.model.DeliveryFeeConfiguration import io.novafoundation.nova.feature_wallet_api.domain.model.XCMInstructionType import io.novafoundation.nova.feature_wallet_impl.data.network.crosschain.XcmMultiAsset.Fungibility import io.novafoundation.nova.feature_wallet_impl.data.network.crosschain.XcmMultiAsset.Id import io.novafoundation.nova.runtime.ext.emptyAccountId 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.getRuntime import io.novafoundation.nova.runtime.multiNetwork.multiLocation.MultiLocation +import io.novafoundation.nova.runtime.multiNetwork.multiLocation.isHere +import io.novafoundation.nova.runtime.multiNetwork.multiLocation.paraIdOrNull +import io.novafoundation.nova.runtime.storage.source.StorageDataSource import java.math.BigInteger +import jp.co.soramitsu.fearless_utils.runtime.definitions.types.bytes +import jp.co.soramitsu.fearless_utils.runtime.metadata.call +import jp.co.soramitsu.fearless_utils.runtime.metadata.module +import jp.co.soramitsu.fearless_utils.runtime.metadata.storage + +// TODO: Currently message doesn't contain setTopic command in the end. It will come with XCMv3 support +private const val SET_TOPIC_SIZE = 33 class RealCrossChainWeigher( + private val storageDataSource: StorageDataSource, private val extrinsicService: ExtrinsicService, private val chainRegistry: ChainRegistry ) : CrossChainWeigher { @@ -33,53 +57,133 @@ class RealCrossChainWeigher( return destinationWeight.max(reserveWeight) } - override suspend fun estimateFee(transferConfiguration: CrossChainTransferConfiguration): CrossChainFee { - val destinationFee = with(transferConfiguration) { - feeFor(destinationFee) - } + override suspend fun estimateFee(amount: Balance, config: CrossChainTransferConfiguration): CrossChainFeeModel = with(config) { + // Reserve fee may be zero if xcm transfer doesn't reserve tokens + val reserveFeeAmount = calculateFee(amount, reserveFee, reserveChainLocation) + val destinationFeeAmount = calculateFee(amount, destinationFee, destinationChainLocation) - val reserveFee = with(transferConfiguration) { - reserveFee?.let { feeFor(it) } - } + return reserveFeeAmount + destinationFeeAmount + } - return CrossChainFee( - destination = destinationFee, - reserve = reserveFee - ) + private suspend fun CrossChainTransferConfiguration.calculateFee( + amount: Balance, + feeConfig: CrossChainFeeConfiguration?, + chainLocation: MultiLocation + ): CrossChainFeeModel { + return when (feeConfig) { + null -> CrossChainFeeModel.zero() + else -> { + val isSendingFromOrigin = originChainId == feeConfig.from.chainId + val feeAmount = feeFor(amount, feeConfig) + val deliveryFee = deliveryFeeFor(amount, feeConfig, chainLocation, isSendingFromOrigin = isSendingFromOrigin) + feeAmount.orZero() + deliveryFee.orZero() + } + } } - private suspend fun CrossChainTransferConfiguration.feeFor(feeConfig: CrossChainFeeConfiguration): BigInteger? { - val chain = chainRegistry.getChain(feeConfig.chainId) + private suspend fun CrossChainTransferConfiguration.feeFor(amount: Balance, feeConfig: CrossChainFeeConfiguration): CrossChainFeeModel { + val chain = chainRegistry.getChain(feeConfig.to.chainId) val maxWeight = feeConfig.estimatedWeight() - return when (val mode = feeConfig.xcmFeeType.mode) { - is Mode.Proportional -> mode.weightToFee(maxWeight) + return when (val mode = feeConfig.to.xcmFeeType.mode) { + is Mode.Proportional -> CrossChainFeeModel(holdingPart = mode.weightToFee(maxWeight)) Mode.Standard -> { - val xcmMessage = xcmMessage(feeConfig.xcmFeeType.instructions, chain) + val xcmMessage = xcmMessage(feeConfig.to.xcmFeeType.instructions, chain, amount) val paymentInfo = extrinsicService.paymentInfo(chain, TransactionOrigin.SelectedWallet) { xcmExecute(xcmMessage, maxWeight) } - paymentInfo.partialFee + CrossChainFeeModel(holdingPart = paymentInfo.partialFee) } - Mode.Unknown -> null + Mode.Unknown -> CrossChainFeeModel.zero() + } + } + + private suspend fun CrossChainTransferConfiguration.deliveryFeeFor( + amount: Balance, + config: CrossChainFeeConfiguration, + destinationChainLocation: MultiLocation, + isSendingFromOrigin: Boolean + ): CrossChainFeeModel { + val deliveryFeeConfiguration = config.from.deliveryFeeConfiguration ?: return CrossChainFeeModel.zero() + + val deliveryConfig = deliveryFeeConfiguration.getDeliveryConfig(destinationChainLocation) + + val deliveryFeeFactor: BigInteger = queryDeliveryFeeFactor(config.from.chainId, deliveryConfig.factorPallet, destinationChainLocation) + + val xcmMessageSize = getXcmMessageSize(amount, config) + val xcmMessageSizeWithTopic = xcmMessageSize + SET_TOPIC_SIZE.toBigInteger() + + val feeSize = (deliveryConfig.sizeBase + xcmMessageSizeWithTopic * deliveryConfig.sizeFactor) + val deliveryFee = BigRational.fixedU128(deliveryFeeFactor * feeSize) + + val isSenderPaysOriginDelivery = !deliveryConfig.alwaysHoldingPays + return if (isSenderPaysOriginDelivery && isSendingFromOrigin) { + CrossChainFeeModel(senderPart = deliveryFee) + } else { + CrossChainFeeModel(holdingPart = deliveryFee) + } + } + + private suspend fun CrossChainTransferConfiguration.getXcmMessageSize(amount: Balance, config: CrossChainFeeConfiguration): BigInteger { + val chain = chainRegistry.getChain(config.to.chainId) + val runtime = chainRegistry.getRuntime(config.to.chainId) + val xcmMessage = xcmMessage(config.to.xcmFeeType.instructions, chain, amount) + .toEncodableInstance() + + return runtime.metadata + .module(runtime.metadata.xcmPalletName()) + .call("execute") + .argument("message") + .requireActualType() + .bytes(runtime, xcmMessage) + .size.toBigInteger() + } + + private fun DeliveryFeeConfiguration.getDeliveryConfig(destinationChainLocation: MultiLocation): DeliveryFeeConfiguration.Type.Exponential { + val isParent = destinationChainLocation.interior.isHere() + + val configType = when { + isParent -> toParent + else -> toParachain + } + + return configType.asExponentialOrThrow() + } + + private fun DeliveryFeeConfiguration.Type?.asExponentialOrThrow(): DeliveryFeeConfiguration.Type.Exponential { + return this as? DeliveryFeeConfiguration.Type.Exponential ?: throw IllegalStateException("Unknown delivery fee type") + } + + private suspend fun queryDeliveryFeeFactor( + chainId: ChainId, + pallet: String, + destinationMultiLocation: MultiLocation, + ): BigInteger { + return when { + destinationMultiLocation.interior.isHere() -> xcmParentDeliveryFeeFactor(chainId, pallet) + else -> { + val paraId = destinationMultiLocation.interior.paraIdOrNull() ?: throw IllegalStateException("ParaId must be not null") + xcmParachainDeliveryFeeFactor(chainId, pallet, paraId) + } } } private fun CrossChainFeeConfiguration.estimatedWeight(): Weight { - val instructionTypes = xcmFeeType.instructions + val instructionTypes = to.xcmFeeType.instructions - return instructionWeight * instructionTypes.size.toBigInteger() + return to.instructionWeight * instructionTypes.size.toBigInteger() } private fun CrossChainTransferConfiguration.xcmMessage( instructionTypes: List, chain: Chain, + amount: Balance ): VersionedXcm { - val instructions = instructionTypes.mapNotNull { instructionType -> xcmInstruction(instructionType, chain) } + val instructions = instructionTypes.mapNotNull { instructionType -> xcmInstruction(instructionType, chain, amount) } return VersionedXcm.V2(XcmV2(instructions)) } @@ -87,37 +191,38 @@ class RealCrossChainWeigher( private fun CrossChainTransferConfiguration.xcmInstruction( instructionType: XCMInstructionType, chain: Chain, + amount: Balance ): XcmV2Instruction? { return when (instructionType) { - XCMInstructionType.ReserveAssetDeposited -> reserveAssetDeposited() + XCMInstructionType.ReserveAssetDeposited -> reserveAssetDeposited(amount) XCMInstructionType.ClearOrigin -> clearOrigin() - XCMInstructionType.BuyExecution -> buyExecution() + XCMInstructionType.BuyExecution -> buyExecution(amount) XCMInstructionType.DepositAsset -> depositAsset(chain) - XCMInstructionType.WithdrawAsset -> withdrawAsset() + XCMInstructionType.WithdrawAsset -> withdrawAsset(amount) XCMInstructionType.DepositReserveAsset -> depositReserveAsset() - XCMInstructionType.ReceiveTeleportedAsset -> receiveTeleportedAsset() + XCMInstructionType.ReceiveTeleportedAsset -> receiveTeleportedAsset(amount) XCMInstructionType.UNKNOWN -> null } } - private fun CrossChainTransferConfiguration.reserveAssetDeposited() = XcmV2Instruction.ReserveAssetDeposited( + private fun CrossChainTransferConfiguration.reserveAssetDeposited(amount: Balance) = XcmV2Instruction.ReserveAssetDeposited( assets = listOf( - sendingAssetAmountOf(BigInteger.ZERO) + sendingAssetAmountOf(amount) ) ) - private fun CrossChainTransferConfiguration.receiveTeleportedAsset() = XcmV2Instruction.ReceiveTeleportedAsset( + private fun CrossChainTransferConfiguration.receiveTeleportedAsset(amount: Balance) = XcmV2Instruction.ReceiveTeleportedAsset( assets = listOf( - sendingAssetAmountOf(BigInteger.ZERO) + sendingAssetAmountOf(amount) ) ) @Suppress("unused") private fun CrossChainTransferConfiguration.clearOrigin() = XcmV2Instruction.ClearOrigin - private fun CrossChainTransferConfiguration.buyExecution(): XcmV2Instruction.BuyExecution { + private fun CrossChainTransferConfiguration.buyExecution(amount: Balance): XcmV2Instruction.BuyExecution { return XcmV2Instruction.BuyExecution( - fees = sendingAssetAmountOf(Balance.ZERO), + fees = sendingAssetAmountOf(amount), weightLimit = WeightLimit.Unlimited ) } @@ -131,10 +236,10 @@ class RealCrossChainWeigher( ) } - private fun CrossChainTransferConfiguration.withdrawAsset(): XcmV2Instruction.WithdrawAsset { + private fun CrossChainTransferConfiguration.withdrawAsset(amount: Balance): XcmV2Instruction.WithdrawAsset { return XcmV2Instruction.WithdrawAsset( assets = listOf( - sendingAssetAmountOf(Balance.ZERO) + sendingAssetAmountOf(amount) ) ) } @@ -156,4 +261,21 @@ class RealCrossChainWeigher( } private fun Chain.emptyBeneficiaryMultiLocation(): MultiLocation = emptyAccountId().accountIdToMultiLocation() + + private suspend fun xcmParachainDeliveryFeeFactor(chainId: ChainId, moduleName: String, paraId: ParaId): BigInteger { + return storageDataSource.query(chainId) { + runtime.metadata.module(moduleName).storage("DeliveryFeeFactor") + .query( + paraId, + binding = ::bindNumber + ) + } + } + + private suspend fun xcmParentDeliveryFeeFactor(chainId: ChainId, moduleName: String): BigInteger { + return storageDataSource.query(chainId) { + runtime.metadata.module(moduleName).storage("UpwardDeliveryFeeFactor") + .query(binding = ::bindNumber) + } + } } diff --git a/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/data/network/crosschain/validations/CrossChainFeeValidation.kt b/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/data/network/crosschain/validations/CrossChainFeeValidation.kt index f3f21e1f85..6db57aa81a 100644 --- a/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/data/network/crosschain/validations/CrossChainFeeValidation.kt +++ b/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/data/network/crosschain/validations/CrossChainFeeValidation.kt @@ -6,16 +6,18 @@ import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.t import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.tranfers.AssetTransferValidationFailure import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.tranfers.AssetTransfersValidation import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.tranfers.AssetTransfersValidationSystemBuilder -import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.tranfers.originFeeInUsedAsset -import io.novafoundation.nova.feature_wallet_api.presentation.model.networkFeeByRequestedAccountOrZero +import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.tranfers.originFeeListInUsedAsset +import io.novafoundation.nova.feature_wallet_api.presentation.model.networkFeeByRequestedAccount +import io.novafoundation.nova.feature_wallet_api.presentation.model.networkFeeDecimalAmount class CrossChainFeeValidation : AssetTransfersValidation { override suspend fun validate(value: AssetTransferPayload): ValidationStatus { - val networkFeeInUsedAsset = value.originFeeInUsedAsset.networkFeeByRequestedAccountOrZero - val remainingBalanceAfterTransfer = value.originUsedAsset.transferable - value.transfer.amount - networkFeeInUsedAsset + val originFeeSum = value.originFeeListInUsedAsset.sumOf { it.networkFeeByRequestedAccount } - val crossChainFee = value.crossChainFee.networkFeeByRequestedAccountOrZero + val remainingBalanceAfterTransfer = value.originUsedAsset.transferable - value.transfer.amount - originFeeSum + + val crossChainFee = value.crossChainFee.networkFeeDecimalAmount val remainsEnoughToPayCrossChainFees = remainingBalanceAfterTransfer >= crossChainFee return remainsEnoughToPayCrossChainFees isTrueOrError { diff --git a/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/di/WalletFeatureModule.kt b/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/di/WalletFeatureModule.kt index d2a57fe0ca..0af3ab1fb5 100644 --- a/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/di/WalletFeatureModule.kt +++ b/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/di/WalletFeatureModule.kt @@ -231,9 +231,10 @@ class WalletFeatureModule { @Provides @FeatureScope fun provideCrossChainWeigher( + @Named(REMOTE_STORAGE_SOURCE) storageDataSource: StorageDataSource, extrinsicService: ExtrinsicService, chainRegistry: ChainRegistry - ): CrossChainWeigher = RealCrossChainWeigher(extrinsicService, chainRegistry) + ): CrossChainWeigher = RealCrossChainWeigher(storageDataSource, extrinsicService, chainRegistry) @Provides @FeatureScope diff --git a/runtime/src/main/java/io/novafoundation/nova/runtime/ext/ChainExt.kt b/runtime/src/main/java/io/novafoundation/nova/runtime/ext/ChainExt.kt index 685e5ee2d3..3094f513c9 100644 --- a/runtime/src/main/java/io/novafoundation/nova/runtime/ext/ChainExt.kt +++ b/runtime/src/main/java/io/novafoundation/nova/runtime/ext/ChainExt.kt @@ -290,6 +290,8 @@ object ChainGeneses { const val ALEPH_ZERO = "70255b4d28de0fc4e1a193d7e175ad1ccef431598211c55538f1018651a0344e" const val TERNOA = "6859c81ca95ef624c9dfe4dc6e3381c33e5d6509e35e147092bfbc780f777c4e" + const val POLIMEC = "7eb9354488318e7549c722669dcbdcdc526f1fef1420e7944667212f3601fdbd" + const val POLKADEX = "3920bcb4960a1eef5580cd5367ff3f430eef052774f78468852f7b9cb39f8a3c" const val CALAMARI = "4ac80c99289841dd946ef92765bf659a307d39189b3ce374a92b5f0415ee17a1" diff --git a/runtime/src/main/java/io/novafoundation/nova/runtime/extrinsic/multi/ExtrinsicSplitter.kt b/runtime/src/main/java/io/novafoundation/nova/runtime/extrinsic/multi/ExtrinsicSplitter.kt index 997480d0e1..713c9b1689 100644 --- a/runtime/src/main/java/io/novafoundation/nova/runtime/extrinsic/multi/ExtrinsicSplitter.kt +++ b/runtime/src/main/java/io/novafoundation/nova/runtime/extrinsic/multi/ExtrinsicSplitter.kt @@ -30,7 +30,7 @@ interface ExtrinsicSplitter { private typealias CallWeightsByType = Map> -private const val LEAVE_SOME_SPACE_MULTIPLIER = 0.8 +private const val LEAVE_SOME_SPACE_MULTIPLIER = 0.6 internal class RealExtrinsicSplitter( private val rpcCalls: RpcCalls, diff --git a/runtime/src/main/java/io/novafoundation/nova/runtime/multiNetwork/multiLocation/MultiLocation.kt b/runtime/src/main/java/io/novafoundation/nova/runtime/multiNetwork/multiLocation/MultiLocation.kt index 5afa08a01a..3b008fb217 100644 --- a/runtime/src/main/java/io/novafoundation/nova/runtime/multiNetwork/multiLocation/MultiLocation.kt +++ b/runtime/src/main/java/io/novafoundation/nova/runtime/multiNetwork/multiLocation/MultiLocation.kt @@ -75,6 +75,14 @@ fun List.toInterior() = when (size) { fun MultiLocation.Interior.isHere() = this is MultiLocation.Interior.Here +fun MultiLocation.Interior.paraIdOrNull(): ParaId? { + if (this !is MultiLocation.Interior.Junctions) return null + + return junctions.filterIsInstance() + .firstOrNull() + ?.id +} + private fun List.sorted(): List { return sortedBy(MultiLocation.Junction::order) }