From 1733003bdc5cabe171811cf865b75c79498abe2a Mon Sep 17 00:00:00 2001 From: valentun Date: Tue, 4 Jun 2024 01:55:57 +0700 Subject: [PATCH 01/83] Xyk swaps --- .../nova/common/utils/FearlessLibExt.kt | 6 + .../assetExchange/hydraDx/HydraDxExchange.kt | 7 +- .../hydraDx/omnipool/OmniPoolSwapSource.kt | 6 + .../hydraDx/stableswap/StableSwapSource.kt | 6 +- .../hydraDx/stableswap/model/StablePool.kt | 6 +- .../data/assetExchange/hydraDx/xyk/XYKApi.kt | 34 +++ .../hydraDx/xyk/XYKSwapSource.kt | 217 ++++++++++++++++++ .../hydraDx/xyk/model/XYKFees.kt | 20 ++ .../hydraDx/xyk/model/XYKPool.kt | 108 +++++++++ .../hydraDx/xyk/model/XYKPoolInfo.kt | 13 ++ .../di/exchanges/HydraDxExchangeModule.kt | 15 ++ hydra-dx-math/bindings/src/lib.rs | 110 +++++++++ hydra-dx-math/build.gradle | 1 + .../hydra_dx_math/HydraDxMathConversions.kt | 11 + .../hydra_dx_math/xyk/HYKSwapMathBridge.java | 26 +++ 15 files changed, 576 insertions(+), 10 deletions(-) create mode 100644 feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/hydraDx/xyk/XYKApi.kt create mode 100644 feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/hydraDx/xyk/XYKSwapSource.kt create mode 100644 feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/hydraDx/xyk/model/XYKFees.kt create mode 100644 feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/hydraDx/xyk/model/XYKPool.kt create mode 100644 feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/hydraDx/xyk/model/XYKPoolInfo.kt create mode 100644 hydra-dx-math/src/main/java/io/novafoundation/nova/hydra_dx_math/HydraDxMathConversions.kt create mode 100644 hydra-dx-math/src/main/java/io/novafoundation/nova/hydra_dx_math/xyk/HYKSwapMathBridge.java 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 761879b125..9ea0038e25 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 @@ -251,6 +251,10 @@ fun RuntimeMetadata.omnipool() = module(Modules.OMNIPOOL) fun RuntimeMetadata.stableSwapOrNull() = moduleOrNull(Modules.STABLE_SWAP) +fun RuntimeMetadata.xykOrNull() = moduleOrNull(Modules.XYK) + +fun RuntimeMetadata.xyk() = module(Modules.XYK) + fun RuntimeMetadata.stableSwap() = module(Modules.STABLE_SWAP) fun RuntimeMetadata.dynamicFeesOrNull() = moduleOrNull(Modules.DYNAMIC_FEES) @@ -439,5 +443,7 @@ object Modules { const val STABLE_SWAP = "Stableswap" + const val XYK = "XYK" + const val ASSET_REGISTRY = "AssetRegistry" } diff --git a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/hydraDx/HydraDxExchange.kt b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/hydraDx/HydraDxExchange.kt index c2cd22a844..26e5f75602 100644 --- a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/hydraDx/HydraDxExchange.kt +++ b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/hydraDx/HydraDxExchange.kt @@ -40,6 +40,7 @@ import io.novafoundation.nova.feature_swap_impl.data.assetExchange.AssetExchange import io.novafoundation.nova.feature_swap_impl.data.assetExchange.hydraDx.omnipool.OmniPoolSwapSourceFactory import io.novafoundation.nova.feature_swap_impl.data.assetExchange.hydraDx.referrals.linkedAccounts import io.novafoundation.nova.feature_swap_impl.data.assetExchange.hydraDx.referrals.referralsOrNull +import io.novafoundation.nova.feature_swap_impl.data.assetExchange.hydraDx.stableswap.StableSwapSourceFactory import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.AssetSourceRegistry import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.HydraDxAssetId import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.HydraDxAssetIdConverter @@ -563,11 +564,9 @@ private class HydraDxExchange( return buildString { append(segment.sourceId) - val stableswapPoolId = segment.sourceParams["PoolId"] - if (stableswapPoolId != null) { - val onChainId = stableswapPoolId.toBigInteger() + if (segment.sourceId == StableSwapSourceFactory.ID) { + val onChainId = segment.sourceParams.getValue("PoolId").toBigInteger() val chainAsset = hydraDxAssetIdConverter.toChainAssetOrThrow(chain, onChainId) - append("[${chainAsset.symbol}]") } } diff --git a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/hydraDx/omnipool/OmniPoolSwapSource.kt b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/hydraDx/omnipool/OmniPoolSwapSource.kt index 67f9a1fb97..ff9c3c05dd 100644 --- a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/hydraDx/omnipool/OmniPoolSwapSource.kt +++ b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/hydraDx/omnipool/OmniPoolSwapSource.kt @@ -287,4 +287,10 @@ fun omniPoolAccountId(): AccountId { typealias RemoteAndLocalId = Pair +val RemoteAndLocalId.remoteId + get() = first + +val RemoteAndLocalId.localId + get() = second + typealias RemoteAndLocalIdOptional = Pair diff --git a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/hydraDx/stableswap/StableSwapSource.kt b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/hydraDx/stableswap/StableSwapSource.kt index 84c013fbcf..0d91610ca0 100644 --- a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/hydraDx/stableswap/StableSwapSource.kt +++ b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/hydraDx/stableswap/StableSwapSource.kt @@ -61,6 +61,10 @@ class StableSwapSourceFactory( private val chainStateRepository: ChainStateRepository ) : HydraDxSwapSource.Factory { + companion object { + const val ID = "StableSwap" + } + override fun create(chain: Chain): HydraDxSwapSource { return StableSwapSource( remoteStorageSource = remoteStorageSource, @@ -80,7 +84,7 @@ private class StableSwapSource( private val chainStateRepository: ChainStateRepository, ) : HydraDxSwapSource { - override val identifier: String = "StableSwap" + override val identifier: String = StableSwapSourceFactory.ID private val initialPoolsInfo: MutableSharedFlow> = singleReplaySharedFlow() diff --git a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/hydraDx/stableswap/model/StablePool.kt b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/hydraDx/stableswap/model/StablePool.kt index 38938fe033..76500f1f48 100644 --- a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/hydraDx/stableswap/model/StablePool.kt +++ b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/hydraDx/stableswap/model/StablePool.kt @@ -4,10 +4,10 @@ import com.google.gson.Gson import com.google.gson.annotations.SerializedName import io.novafoundation.nova.common.data.network.runtime.binding.BlockNumber import io.novafoundation.nova.common.utils.Perbill -import io.novafoundation.nova.common.utils.atLeastZero import io.novafoundation.nova.feature_swap_api.domain.model.SwapDirection import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.HydraDxAssetId import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.types.Balance +import io.novafoundation.nova.hydra_dx_math.HydraDxMathConversions.fromBridgeResultToBalance import io.novafoundation.nova.hydra_dx_math.stableswap.StableSwapMathBridge import java.math.BigInteger @@ -185,7 +185,3 @@ private class ReservesInput( val id: Int, val decimals: Int ) - -private fun String.fromBridgeResultToBalance(): Balance? { - return if (this == "-1") null else toBigInteger().atLeastZero() -} diff --git a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/hydraDx/xyk/XYKApi.kt b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/hydraDx/xyk/XYKApi.kt new file mode 100644 index 0000000000..f6145529f8 --- /dev/null +++ b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/hydraDx/xyk/XYKApi.kt @@ -0,0 +1,34 @@ +package io.novafoundation.nova.feature_swap_impl.data.assetExchange.hydraDx.xyk + +import io.novafoundation.nova.common.address.AccountIdKey +import io.novafoundation.nova.common.address.intoKey +import io.novafoundation.nova.common.data.network.runtime.binding.bindAccountId +import io.novafoundation.nova.common.utils.xyk +import io.novafoundation.nova.common.utils.xykOrNull +import io.novafoundation.nova.feature_swap_impl.data.assetExchange.hydraDx.xyk.model.XYKPoolInfo +import io.novafoundation.nova.feature_swap_impl.data.assetExchange.hydraDx.xyk.model.bindXYKPoolInfo +import io.novafoundation.nova.runtime.storage.source.query.StorageQueryContext +import io.novafoundation.nova.runtime.storage.source.query.api.QueryableModule +import io.novafoundation.nova.runtime.storage.source.query.api.QueryableStorageEntry1 +import io.novafoundation.nova.runtime.storage.source.query.api.storage1 +import io.novasama.substrate_sdk_android.runtime.metadata.RuntimeMetadata +import io.novasama.substrate_sdk_android.runtime.metadata.module.Module + +@JvmInline +value class XYKSwapApi(override val module: Module) : QueryableModule + +context(StorageQueryContext) +val RuntimeMetadata.xykOrNull: XYKSwapApi? + get() = xykOrNull()?.let(::XYKSwapApi) + +context(StorageQueryContext) +val RuntimeMetadata.xyk: XYKSwapApi + get() = XYKSwapApi(xyk()) + +context(StorageQueryContext) +val XYKSwapApi.poolAssets: QueryableStorageEntry1 + get() = storage1( + name = "PoolAssets", + keyBinding = { bindAccountId(it).intoKey() }, + binding = { decoded, _ -> bindXYKPoolInfo(decoded) }, + ) diff --git a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/hydraDx/xyk/XYKSwapSource.kt b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/hydraDx/xyk/XYKSwapSource.kt new file mode 100644 index 0000000000..8c6f172318 --- /dev/null +++ b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/hydraDx/xyk/XYKSwapSource.kt @@ -0,0 +1,217 @@ +package io.novafoundation.nova.feature_swap_impl.data.assetExchange.hydraDx.xyk + +import io.novafoundation.nova.common.address.AccountIdKey +import io.novafoundation.nova.common.utils.MultiMapList +import io.novafoundation.nova.common.utils.combine +import io.novafoundation.nova.common.utils.graph.Edge +import io.novafoundation.nova.common.utils.graph.GraphBuilder +import io.novafoundation.nova.common.utils.singleReplaySharedFlow +import io.novafoundation.nova.common.utils.xyk +import io.novafoundation.nova.core.updater.SharedRequestsBuilder +import io.novafoundation.nova.feature_swap_api.domain.model.SwapExecuteArgs +import io.novafoundation.nova.feature_swap_api.domain.model.SwapQuoteException +import io.novafoundation.nova.feature_swap_impl.data.assetExchange.hydraDx.HydraDxSwapSource +import io.novafoundation.nova.feature_swap_impl.data.assetExchange.hydraDx.HydraDxSwapSourceQuoteArgs +import io.novafoundation.nova.feature_swap_impl.data.assetExchange.hydraDx.HydraSwapDirection +import io.novafoundation.nova.feature_swap_impl.data.assetExchange.hydraDx.omnipool.RemoteAndLocalId +import io.novafoundation.nova.feature_swap_impl.data.assetExchange.hydraDx.omnipool.localId +import io.novafoundation.nova.feature_swap_impl.data.assetExchange.hydraDx.xyk.model.XYKPool +import io.novafoundation.nova.feature_swap_impl.data.assetExchange.hydraDx.xyk.model.XYKPoolAsset +import io.novafoundation.nova.feature_swap_impl.data.assetExchange.hydraDx.xyk.model.XYKPoolInfo +import io.novafoundation.nova.feature_swap_impl.data.assetExchange.hydraDx.xyk.model.XYKPools +import io.novafoundation.nova.feature_swap_impl.data.assetExchange.hydraDx.xyk.model.poolFeesConstant +import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.AssetSourceRegistry +import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.HydraDxAssetId +import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.HydraDxAssetIdConverter +import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.toOnChainIdOrThrow +import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.types.Balance +import io.novafoundation.nova.runtime.ext.fullId +import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain +import io.novafoundation.nova.runtime.multiNetwork.chain.model.FullChainAssetId +import io.novafoundation.nova.runtime.storage.source.StorageDataSource +import io.novasama.substrate_sdk_android.extensions.fromHex +import io.novasama.substrate_sdk_android.extensions.toHexString +import io.novasama.substrate_sdk_android.runtime.AccountId +import io.novasama.substrate_sdk_android.runtime.definitions.types.composite.DictEnum +import io.novasama.substrate_sdk_android.runtime.extrinsic.ExtrinsicBuilder +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.map + +private const val POOL_ID_PARAM_KEY = "PoolId" + +class XYKSwapSourceFactory( + private val remoteStorageSource: StorageDataSource, + private val hydraDxAssetIdConverter: HydraDxAssetIdConverter, + private val assetSourceRegistry: AssetSourceRegistry, +) : HydraDxSwapSource.Factory { + + override fun create(chain: Chain): HydraDxSwapSource { + return XYKSwapSource( + remoteStorageSource = remoteStorageSource, + hydraDxAssetIdConverter = hydraDxAssetIdConverter, + chain = chain, + assetSourceRegistry = assetSourceRegistry + ) + } +} + +private class XYKSwapSource( + private val remoteStorageSource: StorageDataSource, + private val hydraDxAssetIdConverter: HydraDxAssetIdConverter, + private val chain: Chain, + private val assetSourceRegistry: AssetSourceRegistry, +) : HydraDxSwapSource { + + override val identifier: String = "Xyk" + + private val initialPoolsInfo: MutableSharedFlow> = singleReplaySharedFlow() + + private val xykPools: MutableSharedFlow = singleReplaySharedFlow() + + override suspend fun availableSwapDirections(): MultiMapList { + val pools = getPools() + + val poolInitialInfo = pools.matchIdsWithLocal() + initialPoolsInfo.emit(poolInitialInfo) + + return poolInitialInfo.allPossibleDirections() + } + + override suspend fun ExtrinsicBuilder.executeSwap(args: SwapExecuteArgs) { + // We don't need a specific implementation for XYKSwap extrinsics since it is done by HydraDxExchange on the upper level via Router + } + + override suspend fun quote(args: HydraDxSwapSourceQuoteArgs): Balance { + val allPools = xykPools.first() + val poolAddress = args.params.poolAddressParam() + + val hydraDxAssetIdIn = hydraDxAssetIdConverter.toOnChainIdOrThrow(args.chainAssetIn) + val hydraDxAssetIdOut = hydraDxAssetIdConverter.toOnChainIdOrThrow(args.chainAssetOut) + + return allPools.quote(poolAddress, hydraDxAssetIdIn, hydraDxAssetIdOut, args.amount, args.swapDirection) + ?: throw SwapQuoteException.NotEnoughLiquidity + } + + private suspend fun subscribeToBalance( + assetId: RemoteAndLocalId, + poolAddress: AccountId, + subscriptionBuilder: SharedRequestsBuilder + ): Flow { + val chainAsset = chain.assetsById.getValue(assetId.localId.assetId) + val assetSource = assetSourceRegistry.sourceFor(chainAsset) + + return assetSource.balance.subscribeTransferableAccountBalance(chain, chainAsset, poolAddress, subscriptionBuilder) + } + + @OptIn(ExperimentalCoroutinesApi::class) + override suspend fun runSubscriptions( + userAccountId: AccountId, + subscriptionBuilder: SharedRequestsBuilder + ): Flow = coroutineScope { + xykPools.resetReplayCache() + + val initialPoolsInfo = initialPoolsInfo.first() + + val poolsSubscription = initialPoolsInfo.map { poolInfo -> + val firstBalanceFlow = subscribeToBalance(poolInfo.firstAsset, poolInfo.poolAddress, subscriptionBuilder) + val secondBalanceFlow = subscribeToBalance(poolInfo.secondAsset, poolInfo.poolAddress, subscriptionBuilder) + + firstBalanceFlow.combine(secondBalanceFlow) { firstBalance, secondBalance -> + XYKPool( + address = poolInfo.poolAddress, + firstAsset = XYKPoolAsset(firstBalance, poolInfo.firstAsset.first), + secondAsset = XYKPoolAsset(secondBalance, poolInfo.secondAsset.first), + ) + } + }.combine() + + val fees = remoteStorageSource.query(chain.id) { + runtime.metadata.xyk().poolFeesConstant(runtime) + } + + poolsSubscription.map { pools -> + val built = XYKPools(fees, pools) + xykPools.emit(built) + } + } + + override fun routerPoolTypeFor(params: Map): DictEnum.Entry<*> { + return DictEnum.Entry("XYK", null) + } + + private suspend fun getPools(): Map { + return remoteStorageSource.query(chain.id) { + runtime.metadata.xykOrNull?.poolAssets?.entries().orEmpty() + } + } + + private suspend fun Map.matchIdsWithLocal(): List { + val allOnChainIds = hydraDxAssetIdConverter.allOnChainIds(chain) + + fun matchId(remoteId: HydraDxAssetId): RemoteAndLocalId? { + return allOnChainIds[remoteId]?.fullId?.let { + remoteId to it + } + } + + return mapNotNull outer@{ (poolAddress, poolInfo) -> + PoolInitialInfo( + poolAddress = poolAddress.value, + firstAsset = matchId(poolInfo.firstAsset) ?: return@outer null, + secondAsset = matchId(poolInfo.secondAsset) ?: return@outer null, + ) + } + } + + private fun List.allPossibleDirections(): MultiMapList { + val builder = GraphBuilder() + + onEach { poolInfo -> + builder.addEdge( + from = poolInfo.firstAsset.localId, + to = HYKSwapDirection( + from = poolInfo.firstAsset.localId, + to = poolInfo.secondAsset.localId, + poolAddress = poolInfo.poolAddress + ) + ) + builder.addEdge( + from = poolInfo.secondAsset.localId, + to = HYKSwapDirection( + from = poolInfo.secondAsset.localId, + to = poolInfo.firstAsset.localId, + poolAddress = poolInfo.poolAddress + ) + ) + } + + return builder.build().adjacencyList + } + + private fun Map.poolAddressParam(): AccountId { + return getValue(POOL_ID_PARAM_KEY).fromHex() + } + + private class HYKSwapDirection( + override val from: FullChainAssetId, + override val to: FullChainAssetId, + poolAddress: AccountId + ) : HydraSwapDirection, Edge { + + val poolAddressRaw = poolAddress.toHexString() + + override val params: Map + get() = mapOf(POOL_ID_PARAM_KEY to poolAddressRaw) + } +} + +private class PoolInitialInfo( + val poolAddress: AccountId, + val firstAsset: RemoteAndLocalId, + val secondAsset: RemoteAndLocalId +) diff --git a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/hydraDx/xyk/model/XYKFees.kt b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/hydraDx/xyk/model/XYKFees.kt new file mode 100644 index 0000000000..781364fa16 --- /dev/null +++ b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/hydraDx/xyk/model/XYKFees.kt @@ -0,0 +1,20 @@ +package io.novafoundation.nova.feature_swap_impl.data.assetExchange.hydraDx.xyk.model + +import io.novafoundation.nova.common.data.network.runtime.binding.bindInt +import io.novafoundation.nova.common.data.network.runtime.binding.castToList +import io.novafoundation.nova.common.utils.constant +import io.novafoundation.nova.common.utils.decoded +import io.novasama.substrate_sdk_android.runtime.RuntimeSnapshot +import io.novasama.substrate_sdk_android.runtime.metadata.module.Module + +class XYKFees(val nominator: Int, val denominator: Int) + +fun bindXYKFees(decoded: Any?) : XYKFees { + val (first, second) = decoded.castToList() + + return XYKFees(bindInt(first), bindInt(second)) +} + +fun Module.poolFeesConstant(runtimeSnapshot: RuntimeSnapshot): XYKFees { + return bindXYKFees(constant("GetExchangeFee").decoded(runtimeSnapshot)) +} diff --git a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/hydraDx/xyk/model/XYKPool.kt b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/hydraDx/xyk/model/XYKPool.kt new file mode 100644 index 0000000000..b8c2a4cced --- /dev/null +++ b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/hydraDx/xyk/model/XYKPool.kt @@ -0,0 +1,108 @@ +package io.novafoundation.nova.feature_swap_impl.data.assetExchange.hydraDx.xyk.model + +import io.novafoundation.nova.common.utils.atLeastZero +import io.novafoundation.nova.feature_swap_api.domain.model.SwapDirection +import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.HydraDxAssetId +import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.types.Balance +import io.novafoundation.nova.hydra_dx_math.HydraDxMathConversions.fromBridgeResultToBalance +import io.novafoundation.nova.hydra_dx_math.xyk.HYKSwapMathBridge +import io.novasama.substrate_sdk_android.runtime.AccountId +import java.math.BigInteger + +class XYKPools( + val fees: XYKFees, + val pools: List +) { + + fun quote( + poolAddress: AccountId, + assetIdIn: HydraDxAssetId, + assetIdOut: HydraDxAssetId, + amount: Balance, + direction: SwapDirection + ) : Balance? { + val relevantPool = pools.first { it.address.contentEquals(poolAddress) } + + return relevantPool.quote(assetIdIn, assetIdOut, amount, direction, fees) + } +} + +class XYKPool( + val address: AccountId, + val firstAsset: XYKPoolAsset, + val secondAsset: XYKPoolAsset, +) { + + fun getAsset(assetId: HydraDxAssetId): XYKPoolAsset { + return when { + firstAsset.id == assetId -> firstAsset + secondAsset.id == assetId -> secondAsset + else -> error("Unknown asset for the pool") + } + } +} + +class XYKPoolAsset( + val balance: Balance, + val id: HydraDxAssetId, +) + +fun XYKPool.quote( + assetIdIn: HydraDxAssetId, + assetIdOut: HydraDxAssetId, + amount: Balance, + direction: SwapDirection, + fees: XYKFees +): Balance? { + return when (direction) { + SwapDirection.SPECIFIED_IN -> calculateOutGivenIn(assetIdIn, assetIdOut, amount, fees) + SwapDirection.SPECIFIED_OUT -> calculateInGivenOut(assetIdIn, assetIdOut, amount, fees) + } +} + +private fun XYKPool.calculateOutGivenIn( + assetIdIn: HydraDxAssetId, + assetIdOut: HydraDxAssetId, + amountIn: Balance, + feesConfig: XYKFees +): Balance? { + val assetIn = getAsset(assetIdIn) + val assetOut = getAsset(assetIdOut) + + val amountOut = HYKSwapMathBridge.calculate_out_given_in( + assetIn.balance.toString(), + assetOut.balance.toString(), + amountIn.toString() + ).fromBridgeResultToBalance() ?: return null + + val fees = feesConfig.feeFrom(amountOut) ?: return null + + return (amountOut - fees).atLeastZero() +} + +private fun XYKPool.calculateInGivenOut( + assetIdIn: HydraDxAssetId, + assetIdOut: HydraDxAssetId, + amountOut: Balance, + feesConfig: XYKFees, +): Balance? { + val assetIn = getAsset(assetIdIn) + val assetOut = getAsset(assetIdOut) + + val amountIn = HYKSwapMathBridge.calculate_in_given_out( + assetIn.balance.toString(), + assetOut.balance.toString(), + amountOut.toString() + ).fromBridgeResultToBalance() ?: return null + + val fees = feesConfig.feeFrom(amountIn) ?: return null + + return amountIn + fees +} + +private fun XYKFees.feeFrom(amount: BigInteger): Balance? { + return HYKSwapMathBridge.calculate_pool_trade_fee(amount.toString(), nominator.toString(), denominator.toString()) + .fromBridgeResultToBalance() +} + + diff --git a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/hydraDx/xyk/model/XYKPoolInfo.kt b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/hydraDx/xyk/model/XYKPoolInfo.kt new file mode 100644 index 0000000000..bc0c9cea6d --- /dev/null +++ b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/hydraDx/xyk/model/XYKPoolInfo.kt @@ -0,0 +1,13 @@ +package io.novafoundation.nova.feature_swap_impl.data.assetExchange.hydraDx.xyk.model + +import io.novafoundation.nova.common.data.network.runtime.binding.bindNumber +import io.novafoundation.nova.common.data.network.runtime.binding.castToList +import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.HydraDxAssetId + +class XYKPoolInfo(val firstAsset: HydraDxAssetId, val secondAsset: HydraDxAssetId) + +fun bindXYKPoolInfo(decoded: Any): XYKPoolInfo { + val (first, second) = decoded.castToList() + + return XYKPoolInfo(bindNumber(first), bindNumber(second)) +} diff --git a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/di/exchanges/HydraDxExchangeModule.kt b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/di/exchanges/HydraDxExchangeModule.kt index 869f9e6a8e..b27fb207b4 100644 --- a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/di/exchanges/HydraDxExchangeModule.kt +++ b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/di/exchanges/HydraDxExchangeModule.kt @@ -12,6 +12,7 @@ import io.novafoundation.nova.feature_swap_impl.data.assetExchange.hydraDx.Hydra import io.novafoundation.nova.feature_swap_impl.data.assetExchange.hydraDx.RealHydraDxNovaReferral import io.novafoundation.nova.feature_swap_impl.data.assetExchange.hydraDx.omnipool.OmniPoolSwapSourceFactory import io.novafoundation.nova.feature_swap_impl.data.assetExchange.hydraDx.stableswap.StableSwapSourceFactory +import io.novafoundation.nova.feature_swap_impl.data.assetExchange.hydraDx.xyk.XYKSwapSourceFactory import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.AssetSourceRegistry import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.HydraDxAssetIdConverter import io.novafoundation.nova.runtime.di.REMOTE_STORAGE_SOURCE @@ -62,6 +63,20 @@ class HydraDxExchangeModule { ) } + @Provides + @IntoSet + fun provideXykSwapSourceFactory( + @Named(REMOTE_STORAGE_SOURCE) remoteStorageSource: StorageDataSource, + hydraDxAssetIdConverter: HydraDxAssetIdConverter, + assetSourceRegistry: AssetSourceRegistry + ): HydraDxSwapSource.Factory { + return XYKSwapSourceFactory( + remoteStorageSource = remoteStorageSource, + hydraDxAssetIdConverter = hydraDxAssetIdConverter, + assetSourceRegistry = assetSourceRegistry + ) + } + @Provides @FeatureScope fun provideHydraDxExchangeFactory( diff --git a/hydra-dx-math/bindings/src/lib.rs b/hydra-dx-math/bindings/src/lib.rs index 7aed006e09..2e54ac104d 100644 --- a/hydra-dx-math/bindings/src/lib.rs +++ b/hydra-dx-math/bindings/src/lib.rs @@ -523,4 +523,114 @@ fn calculate_liquidity_out_one_asset( } else { error() } +} + +// ---------------- XYK ---------------------- + +#[no_mangle] +pub fn Java_io_novafoundation_nova_hydra_1dx_1math_xyk_HYKSwapMathBridge_calculate_1out_1given_1in<'a>( + jni_env: JNIEnv<'a>, + _: JClass, + balance_in: JString, + balance_out: JString, + amount_in: JString +) -> JString<'a> { + let balance_in: String = get_str(&jni_env,balance_in); + let balance_out: String = get_str(&jni_env,balance_out); + let amount_in: String = get_str(&jni_env,amount_in); + + let out = xyk_calculate_out_given_in(balance_in, balance_out, amount_in); + + return jni_env.new_string(out).unwrap() +} + + +fn xyk_calculate_out_given_in( + balance_in: String, + balance_out: String, + amount_in: String +) -> String { + let balance_in = parse_into!(u128, balance_in); + let balance_out = parse_into!(u128, balance_out); + let amount_in = parse_into!(u128, amount_in); + + let result = hydra_dx_math::xyk::calculate_out_given_in(balance_in, balance_out, amount_in); + + if let Ok(r) = result { + r.to_string() + } else { + error() + } +} + +#[no_mangle] +pub fn Java_io_novafoundation_nova_hydra_1dx_1math_xyk_HYKSwapMathBridge_calculate_1in_1given_1out<'a>( + jni_env: JNIEnv<'a>, + _: JClass, + balance_in: JString, + balance_out: JString, + amount_in: JString +) -> JString<'a> { + let balance_in: String = get_str(&jni_env,balance_in); + let balance_out: String = get_str(&jni_env,balance_out); + let amount_in: String = get_str(&jni_env,amount_in); + + let out = xyk_calculate_in_given_out(balance_in, balance_out, amount_in); + + return jni_env.new_string(out).unwrap() +} + + +fn xyk_calculate_in_given_out( + balance_in: String, + balance_out: String, + amount_out: String +) -> String { + let balance_in = parse_into!(u128, balance_in); + let balance_out = parse_into!(u128, balance_out); + let amount_out = parse_into!(u128, amount_out); + + let result = hydra_dx_math::xyk::calculate_in_given_out(balance_out, balance_in, amount_out); + + if let Ok(r) = result { + r.to_string() + } else { + error() + } +} + +#[no_mangle] +pub fn Java_io_novafoundation_nova_hydra_1dx_1math_xyk_HYKSwapMathBridge_calculate_1pool_1trade_1fee<'a>( + jni_env: JNIEnv<'a>, + _: JClass, + amount: JString, + fee_nominator: JString, + fee_denominator: JString +) -> JString<'a> { + let amount: String = get_str(&jni_env,amount); + let fee_nominator: String = get_str(&jni_env,fee_nominator); + let fee_denominator: String = get_str(&jni_env,fee_denominator); + + let out = calculate_pool_trade_fee(amount, fee_nominator, fee_denominator); + + return jni_env.new_string(out).unwrap() +} + + +fn calculate_pool_trade_fee( + amount: String, + fee_nominator: String, + fee_denominator: String +) -> String { + let amount = parse_into!(u128, amount); + let fee_nominator = parse_into!(u32, fee_nominator); + let fee_denominator = parse_into!(u32, fee_denominator); + + let result = hydra_dx_math::fee::calculate_pool_trade_fee(amount, (fee_nominator, fee_denominator)); + + if let Some(r) = result { + r.to_string() + } else { + error() + } } \ No newline at end of file diff --git a/hydra-dx-math/build.gradle b/hydra-dx-math/build.gradle index 858de23ee4..c8e0e6c5b9 100644 --- a/hydra-dx-math/build.gradle +++ b/hydra-dx-math/build.gradle @@ -34,6 +34,7 @@ android { dependencies { implementation kotlinDep + implementation project(':common') testImplementation jUnitDep diff --git a/hydra-dx-math/src/main/java/io/novafoundation/nova/hydra_dx_math/HydraDxMathConversions.kt b/hydra-dx-math/src/main/java/io/novafoundation/nova/hydra_dx_math/HydraDxMathConversions.kt new file mode 100644 index 0000000000..a2bc6c1bb7 --- /dev/null +++ b/hydra-dx-math/src/main/java/io/novafoundation/nova/hydra_dx_math/HydraDxMathConversions.kt @@ -0,0 +1,11 @@ +package io.novafoundation.nova.hydra_dx_math + +import io.novafoundation.nova.common.utils.atLeastZero +import java.math.BigInteger + +object HydraDxMathConversions { + + fun String.fromBridgeResultToBalance(): BigInteger? { + return if (this == "-1") null else toBigInteger().atLeastZero() + } +} diff --git a/hydra-dx-math/src/main/java/io/novafoundation/nova/hydra_dx_math/xyk/HYKSwapMathBridge.java b/hydra-dx-math/src/main/java/io/novafoundation/nova/hydra_dx_math/xyk/HYKSwapMathBridge.java new file mode 100644 index 0000000000..bef3c36920 --- /dev/null +++ b/hydra-dx-math/src/main/java/io/novafoundation/nova/hydra_dx_math/xyk/HYKSwapMathBridge.java @@ -0,0 +1,26 @@ +package io.novafoundation.nova.hydra_dx_math.xyk; + +public class HYKSwapMathBridge { + + static { + System.loadLibrary("hydra_dx_math_java"); + } + + public static native String calculate_out_given_in( + String balanceIn, + String balanceOut, + String amountIn + ); + + public static native String calculate_in_given_out( + String balanceIn, + String balanceOut, + String amountOut + ); + + public static native String calculate_pool_trade_fee( + String amount, + String feeNumerator, + String feeDenominator + ); +} From 33d9480dd94c9552e51545441fb6aee1c726b117 Mon Sep 17 00:00:00 2001 From: valentun Date: Tue, 4 Jun 2024 02:11:00 +0700 Subject: [PATCH 02/83] Some logging --- .../nova/feature_swap_impl/domain/swap/RealSwapService.kt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/domain/swap/RealSwapService.kt b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/domain/swap/RealSwapService.kt index 8574096247..ddcc443c28 100644 --- a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/domain/swap/RealSwapService.kt +++ b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/domain/swap/RealSwapService.kt @@ -110,6 +110,8 @@ internal class RealSwapService( priceImpact = args.calculatePriceImpact(amountIn, amountOut), path = quote.path ) + }.onFailure { + Log.e("RealSwapService", "Error while quoting", it) } } } From 1a04be9a8dafe17f9f0e610207a63e68da087568 Mon Sep 17 00:00:00 2001 From: valentun Date: Tue, 4 Jun 2024 02:17:26 +0700 Subject: [PATCH 03/83] Code style --- .../data/assetExchange/hydraDx/xyk/model/XYKFees.kt | 2 +- .../data/assetExchange/hydraDx/xyk/model/XYKPool.kt | 4 +--- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/hydraDx/xyk/model/XYKFees.kt b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/hydraDx/xyk/model/XYKFees.kt index 781364fa16..a778800482 100644 --- a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/hydraDx/xyk/model/XYKFees.kt +++ b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/hydraDx/xyk/model/XYKFees.kt @@ -9,7 +9,7 @@ import io.novasama.substrate_sdk_android.runtime.metadata.module.Module class XYKFees(val nominator: Int, val denominator: Int) -fun bindXYKFees(decoded: Any?) : XYKFees { +fun bindXYKFees(decoded: Any?): XYKFees { val (first, second) = decoded.castToList() return XYKFees(bindInt(first), bindInt(second)) diff --git a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/hydraDx/xyk/model/XYKPool.kt b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/hydraDx/xyk/model/XYKPool.kt index b8c2a4cced..d688339a35 100644 --- a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/hydraDx/xyk/model/XYKPool.kt +++ b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/hydraDx/xyk/model/XYKPool.kt @@ -20,7 +20,7 @@ class XYKPools( assetIdOut: HydraDxAssetId, amount: Balance, direction: SwapDirection - ) : Balance? { + ): Balance? { val relevantPool = pools.first { it.address.contentEquals(poolAddress) } return relevantPool.quote(assetIdIn, assetIdOut, amount, direction, fees) @@ -104,5 +104,3 @@ private fun XYKFees.feeFrom(amount: BigInteger): Balance? { return HYKSwapMathBridge.calculate_pool_trade_fee(amount.toString(), nominator.toString(), denominator.toString()) .fromBridgeResultToBalance() } - - From ced775c3e4ad199a45fb5c705c5591217824dfce Mon Sep 17 00:00:00 2001 From: valentun Date: Tue, 11 Jun 2024 11:14:32 +0700 Subject: [PATCH 04/83] WIP --- .../nova/common/utils/FlowExt.kt | 5 - .../nova/common/utils/KotlinExt.kt | 7 + .../nova/common/utils/graph/Graph.kt | 124 +++--- .../nova/common/utils/graph/GraphBuilder.kt | 34 +- .../domain/model/SwapGraph.kt | 30 ++ .../domain/model/SwapQuote.kt | 11 +- .../domain/model/SwapQuoteArgs.kt | 7 +- .../domain/model/SwapTransaction.kt | 23 ++ .../domain/swap/SwapService.kt | 7 +- .../data/assetExchange/AssetExchange.kt | 41 +- .../AssetConversionExchange.kt | 347 +++++++++-------- .../assetExchange/hydraDx/HydraDxExchange.kt | 360 ++++++------------ .../hydraDx/HydraDxSwapSource.kt | 21 +- .../hydraDx/omnipool/OmniPoolSwapSource.kt | 58 +-- .../domain/swap/RealSwapService.kt | 248 ++++++++++-- 15 files changed, 722 insertions(+), 601 deletions(-) create mode 100644 feature-swap-api/src/main/java/io/novafoundation/nova/feature_swap_api/domain/model/SwapGraph.kt create mode 100644 feature-swap-api/src/main/java/io/novafoundation/nova/feature_swap_api/domain/model/SwapTransaction.kt diff --git a/common/src/main/java/io/novafoundation/nova/common/utils/FlowExt.kt b/common/src/main/java/io/novafoundation/nova/common/utils/FlowExt.kt index 388e1e28dc..7cea52c2c9 100644 --- a/common/src/main/java/io/novafoundation/nova/common/utils/FlowExt.kt +++ b/common/src/main/java/io/novafoundation/nova/common/utils/FlowExt.kt @@ -122,11 +122,6 @@ fun List>.mergeIfMultiple(): Flow = when (size) { else -> merge() } -fun List>>.accumulateMaps(): Flow> { - return mergeIfMultiple() - .runningFold(emptyMap()) { acc, directions -> acc + directions } -} - inline fun withFlowScope(crossinline block: suspend (scope: CoroutineScope) -> Flow): Flow { return flowOfAll { val flowScope = CoroutineScope(coroutineContext) diff --git a/common/src/main/java/io/novafoundation/nova/common/utils/KotlinExt.kt b/common/src/main/java/io/novafoundation/nova/common/utils/KotlinExt.kt index 6eeb081a18..385dea4759 100644 --- a/common/src/main/java/io/novafoundation/nova/common/utils/KotlinExt.kt +++ b/common/src/main/java/io/novafoundation/nova/common/utils/KotlinExt.kt @@ -104,6 +104,13 @@ suspend fun Iterable.mapAsync(operation: suspend (T) -> R): List { }.awaitAll() } +suspend fun Iterable.flatMapAsync(operation: suspend (T) -> List): List { + return coroutineScope { + map { async { operation(it) } } + }.awaitAll().flatten() +} + + fun ByteArray.startsWith(prefix: ByteArray): Boolean { if (prefix.size > size) return false diff --git a/common/src/main/java/io/novafoundation/nova/common/utils/graph/Graph.kt b/common/src/main/java/io/novafoundation/nova/common/utils/graph/Graph.kt index 3ffdf28764..4d2c20643c 100644 --- a/common/src/main/java/io/novafoundation/nova/common/utils/graph/Graph.kt +++ b/common/src/main/java/io/novafoundation/nova/common/utils/graph/Graph.kt @@ -1,6 +1,5 @@ package io.novafoundation.nova.common.utils.graph -import io.novafoundation.nova.common.utils.MultiMap import io.novafoundation.nova.common.utils.MultiMapList import java.util.PriorityQueue @@ -12,71 +11,28 @@ interface Edge { } class Graph>( - val adjacencyList: Map> + val adjacencyList: MultiMapList ) { companion object; } -fun > Graph.Companion.create(vararg multiMaps: MultiMapList): Graph { - return create(multiMaps.toList()) -} - -fun > Graph.Companion.create(vararg adjacencyPairs: Pair>): Graph { - return create(adjacencyPairs.toMap()) -} - -fun > Graph.Companion.create(multiMaps: List>): Graph { - return GraphBuilder().apply { - multiMaps.forEach(::addEdges) - }.build() -} - typealias ConnectedComponent = List typealias Path = List -/** - * Find all connected components of the graph. - * Time Complexity is O(V+E) - * Space Complexity is O(V) - */ -fun > Graph.findConnectedComponents(): List> { - val visited = mutableSetOf() - val result = mutableListOf>() - - for (vertex in adjacencyList.keys) { - if (vertex in visited) continue - - val nextConnectedComponent = connectedComponentsDfs(vertex, adjacencyList, visited) - result.add(nextConnectedComponent) - } - - return result -} - -fun > Graph.findAllPossibleDirections(): MultiMap { - val connectedComponents = findConnectedComponents() - return connectedComponents.findAllPossibleDirections() +fun Graph.vertices(): Set { + return adjacencyList.keys } -fun > Graph.findAllPossibleDirectionsToList(): MultiMapList { - val connectedComponents = findConnectedComponents() - return connectedComponents.findAllPossibleDirectionsToList() -} - -fun List>.findAllPossibleDirections(): MultiMap { - val result = mutableMapOf>() - - forEach { connectedComponent -> - val asSet = connectedComponent.toSet() - - asSet.forEach { node -> - // in the connected component every node is connected to every other except itself - result[node] = asSet - node - } - } - - return result +/** + * Finds all nodes reachable from [origin] + * + * Works for both directed and undirected graphs + * + * Complexity: O(V + E) + */ +fun > Graph.findAllPossibleDestinations(origin: N): Set { + return reachabilityDfs(origin, adjacencyList).toSet() } fun List>.findAllPossibleDirectionsToList(): MultiMapList { @@ -94,6 +50,7 @@ fun List>.findAllPossibleDirectionsToList(): MultiMapL fun > Graph.findDijkstraPathsBetween(from: N, to: N, limit: Int): List> { data class QueueElement(val currentPath: Path, val nodeList: List, val score: Int) : Comparable { + override fun compareTo(other: QueueElement): Int { return score - other.score } @@ -137,20 +94,67 @@ fun > Graph.findDijkstraPathsBetween(from: N, to: N, limit: return paths } -private fun > connectedComponentsDfs( +private fun > reachabilityDfs( node: N, adjacencyList: Map>, - visited: MutableSet, + visited: MutableSet = mutableSetOf(), connectedComponentState: MutableList = mutableListOf() -): ConnectedComponent { +): List { visited.add(node) connectedComponentState.add(node) for (edge in adjacencyList.getValue(node)) { if (edge.to !in visited) { - connectedComponentsDfs(edge.to, adjacencyList, visited, connectedComponentState) + reachabilityDfs(edge.to, adjacencyList, visited, connectedComponentState) } } return connectedComponentState } + + +// TODO the commented code below doesn't work in a context of directed graphs !!! + +///** +// * Find all connected components of the graph. +// * Time Complexity is O(V+E) +// * Space Complexity is O(V) +// */ +//fun > Graph.findConnectedComponents(): List> { +// val visited = mutableSetOf() +// val result = mutableListOf>() +// +// for (vertex in adjacencyList.keys) { +// if (vertex in visited) continue +// +// val reachableNodes = reachabilityDfs(vertex, adjacencyList, visited) +// result.add(reachableNodes) +// } +// +// return result +//} + +//fun > Graph.findAllPossibleDirections(): MultiMap { +// val connectedComponents = findConnectedComponents() +// return connectedComponents.findAllPossibleDirections() +//} + +//fun > Graph.findAllPossibleDirectionsToList(): MultiMapList { +// val connectedComponents = findConnectedComponents() +// return connectedComponents.findAllPossibleDirectionsToList() +//} + +//fun List>.findAllPossibleDirections(): MultiMap { +// val result = mutableMapOf>() +// +// forEach { connectedComponent -> +// val asSet = connectedComponent.toSet() +// +// asSet.forEach { node -> +// // in the connected component every node is connected to every other except itself +// result[node] = asSet - node +// } +// } +// +// return result +//} diff --git a/common/src/main/java/io/novafoundation/nova/common/utils/graph/GraphBuilder.kt b/common/src/main/java/io/novafoundation/nova/common/utils/graph/GraphBuilder.kt index 9c7d68bf8a..16063709b6 100644 --- a/common/src/main/java/io/novafoundation/nova/common/utils/graph/GraphBuilder.kt +++ b/common/src/main/java/io/novafoundation/nova/common/utils/graph/GraphBuilder.kt @@ -6,8 +6,8 @@ class GraphBuilder> { private val adjacencyList: MutableMap> = mutableMapOf() - fun addEdge(from: N, to: E) { - val fromEdges = adjacencyList.getOrPut(from) { mutableListOf() } + fun addEdge(to: E) { + val fromEdges = adjacencyList.getOrPut(to.from) { mutableListOf() } fromEdges.add(to) } @@ -26,3 +26,33 @@ fun > GraphBuilder.addEdges(map: MultiMapList) { addEdges(fromNode, toNodes) } } + +fun > Graph.Companion.create(vararg multiMaps: MultiMapList): Graph { + return create(multiMaps.toList()) +} + +fun > Graph.Companion.create(vararg adjacencyPairs: Pair>): Graph { + return create(adjacencyPairs.toMap()) +} + +fun > Graph.Companion.create(multiMaps: List>): Graph { + return GraphBuilder().apply { + multiMaps.forEach(::addEdges) + }.build() +} + +fun > Graph.Companion.create(edges: List): Graph { + return build { + edges.forEach { + addEdge(it) + } + } +} + +inline fun > Graph.Companion.build(building: GraphBuilder.() -> Unit): Graph { + return GraphBuilder().apply(building).build() +} + +inline fun > Graph.Companion.buildAdjacencyList(building: GraphBuilder.() -> Unit): MultiMapList { + return build(building).adjacencyList +} diff --git a/feature-swap-api/src/main/java/io/novafoundation/nova/feature_swap_api/domain/model/SwapGraph.kt b/feature-swap-api/src/main/java/io/novafoundation/nova/feature_swap_api/domain/model/SwapGraph.kt new file mode 100644 index 0000000000..924a8a273d --- /dev/null +++ b/feature-swap-api/src/main/java/io/novafoundation/nova/feature_swap_api/domain/model/SwapGraph.kt @@ -0,0 +1,30 @@ +package io.novafoundation.nova.feature_swap_api.domain.model + +import io.novafoundation.nova.common.utils.graph.Edge +import io.novafoundation.nova.common.utils.graph.Graph +import io.novafoundation.nova.common.utils.graph.Path +import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.types.Balance +import io.novafoundation.nova.runtime.multiNetwork.chain.model.FullChainAssetId + +interface SwapGraphEdge : QuotableEdge { + + suspend fun beginTransaction(args: SwapTransactionArgs): SwapTransaction + + /** + * Append current swap edge execution to the existing transaction + * Return null if it is not possible, indicating that the new transaction should be initiated to handle this edge via + * [beginTransaction] + */ + suspend fun appendTransaction(currentTransaction: SwapTransaction, args: SwapTransactionArgs): SwapTransaction? +} + +interface QuotableEdge : Edge { + + suspend fun quote( + amount: Balance, + direction: SwapDirection + ): Balance +} + +typealias SwapGraph = Graph +typealias SwapPath = Path diff --git a/feature-swap-api/src/main/java/io/novafoundation/nova/feature_swap_api/domain/model/SwapQuote.kt b/feature-swap-api/src/main/java/io/novafoundation/nova/feature_swap_api/domain/model/SwapQuote.kt index be09630367..b87df6ea81 100644 --- a/feature-swap-api/src/main/java/io/novafoundation/nova/feature_swap_api/domain/model/SwapQuote.kt +++ b/feature-swap-api/src/main/java/io/novafoundation/nova/feature_swap_api/domain/model/SwapQuote.kt @@ -1,6 +1,7 @@ package io.novafoundation.nova.feature_swap_api.domain.model import io.novafoundation.nova.common.utils.Percent +import io.novafoundation.nova.common.utils.graph.Path import io.novafoundation.nova.feature_account_api.data.model.Fee import io.novafoundation.nova.feature_account_api.data.model.amountByRequestedAccount import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.types.Balance @@ -8,7 +9,6 @@ import io.novafoundation.nova.feature_wallet_api.domain.model.ChainAssetWithAmou 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.runtime.multiNetwork.chain.model.Chain -import io.novafoundation.nova.runtime.multiNetwork.chain.model.FullChainAssetId import java.math.BigDecimal data class SwapQuote( @@ -16,7 +16,7 @@ data class SwapQuote( val amountOut: ChainAssetWithAmount, val direction: SwapDirection, val priceImpact: Percent, - val path: QuotePath + val path: Path ) { val assetIn: Chain.Asset @@ -38,10 +38,11 @@ data class SwapQuote( } } -class QuotePath(val segments: List) { +class QuotedSwapEdge( + val edge: SwapGraphEdge, + val quote: Balance +) - class Segment(val from: FullChainAssetId, val to: FullChainAssetId, val sourceId: String, val sourceParams: Map) -} val SwapQuote.editedBalance: Balance get() = when (direction) { diff --git a/feature-swap-api/src/main/java/io/novafoundation/nova/feature_swap_api/domain/model/SwapQuoteArgs.kt b/feature-swap-api/src/main/java/io/novafoundation/nova/feature_swap_api/domain/model/SwapQuoteArgs.kt index ad674c28d6..b7f746d650 100644 --- a/feature-swap-api/src/main/java/io/novafoundation/nova/feature_swap_api/domain/model/SwapQuoteArgs.kt +++ b/feature-swap-api/src/main/java/io/novafoundation/nova/feature_swap_api/domain/model/SwapQuoteArgs.kt @@ -17,12 +17,9 @@ data class SwapQuoteArgs( ) class SwapExecuteArgs( - val assetIn: Chain.Asset, - val assetOut: Chain.Asset, - val customFeeAsset: Chain.Asset?, - val swapLimit: SwapLimit, + val quote: SwapQuote, val nativeAsset: Asset, - val path: QuotePath + val customFeeAsset: Chain.Asset?, ) val SwapExecuteArgs.feeAsset: Chain.Asset diff --git a/feature-swap-api/src/main/java/io/novafoundation/nova/feature_swap_api/domain/model/SwapTransaction.kt b/feature-swap-api/src/main/java/io/novafoundation/nova/feature_swap_api/domain/model/SwapTransaction.kt new file mode 100644 index 0000000000..fdea816cdc --- /dev/null +++ b/feature-swap-api/src/main/java/io/novafoundation/nova/feature_swap_api/domain/model/SwapTransaction.kt @@ -0,0 +1,23 @@ +package io.novafoundation.nova.feature_swap_api.domain.model + +import io.novafoundation.nova.feature_account_api.data.model.Fee +import io.novafoundation.nova.feature_wallet_api.domain.model.Asset +import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain + +interface SwapTransaction { + + suspend fun estimateFee(): SwapTransactionFee + + suspend fun submit(): Result<*> +} + +class SwapTransactionArgs( + val swapLimit: SwapLimit, + val customFeeAsset: Chain.Asset?, + val nativeAsset: Asset, +) + +class SwapTransactionFee( + val networkFee: Fee, + val minimumBalanceBuyIn: MinimumBalanceBuyIn +) diff --git a/feature-swap-api/src/main/java/io/novafoundation/nova/feature_swap_api/domain/swap/SwapService.kt b/feature-swap-api/src/main/java/io/novafoundation/nova/feature_swap_api/domain/swap/SwapService.kt index d56e2c79ae..3dad4b4e8a 100644 --- a/feature-swap-api/src/main/java/io/novafoundation/nova/feature_swap_api/domain/swap/SwapService.kt +++ b/feature-swap-api/src/main/java/io/novafoundation/nova/feature_swap_api/domain/swap/SwapService.kt @@ -22,9 +22,12 @@ interface SwapService { suspend fun canPayFeeInNonUtilityAsset(asset: Chain.Asset): Boolean - suspend fun quote(args: SwapQuoteArgs): Result + suspend fun quote( + args: SwapQuoteArgs, + computationSharingScope: CoroutineScope + ): Result - suspend fun estimateFee(args: SwapExecuteArgs): SwapFee + suspend fun estimateFee(quote: SwapQuote): SwapFee suspend fun swap(args: SwapExecuteArgs): Result diff --git a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/AssetExchange.kt b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/AssetExchange.kt index b11a1903a9..a537305ebc 100644 --- a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/AssetExchange.kt +++ b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/AssetExchange.kt @@ -1,19 +1,12 @@ package io.novafoundation.nova.feature_swap_impl.data.assetExchange -import io.novafoundation.nova.common.utils.MultiMap -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_swap_api.domain.model.MinimumBalanceBuyIn -import io.novafoundation.nova.feature_swap_api.domain.model.QuotePath import io.novafoundation.nova.feature_swap_api.domain.model.ReQuoteTrigger import io.novafoundation.nova.feature_swap_api.domain.model.SlippageConfig import io.novafoundation.nova.feature_swap_api.domain.model.SwapDirection -import io.novafoundation.nova.feature_swap_api.domain.model.SwapExecuteArgs -import io.novafoundation.nova.feature_swap_api.domain.model.SwapQuoteException +import io.novafoundation.nova.feature_swap_api.domain.model.SwapGraphEdge import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.types.Balance import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain -import io.novafoundation.nova.runtime.multiNetwork.chain.model.FullChainAssetId import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.Flow @@ -27,18 +20,11 @@ interface AssetExchange { /** * Implementations should expect `asset` to be non-utility asset, * e.g. they don't need to additionally check whether asset is utility or not - * They can also expect this method is called only when asset is present in [AssetExchange.availableSwapDirections] + * They can also expect this method is called only when asset is present in [AssetExchange.availableDirectSwapConnections] */ suspend fun canPayFeeInNonUtilityToken(asset: Chain.Asset): Boolean - suspend fun availableSwapDirections(): MultiMap - - @Throws(SwapQuoteException::class) - suspend fun quote(args: AssetExchangeQuoteArgs): AssetExchangeQuote - - suspend fun estimateFee(args: SwapExecuteArgs): AssetExchangeFee - - suspend fun swap(args: SwapExecuteArgs): Result + suspend fun availableDirectSwapConnections(): List suspend fun slippageConfig(): SlippageConfig @@ -52,25 +38,4 @@ data class AssetExchangeQuoteArgs( val swapDirection: SwapDirection, ) -class AssetExchangeQuote( - val direction: SwapDirection, - - val quote: Balance, - - val path: QuotePath -) : Comparable { - override fun compareTo(other: AssetExchangeQuote): Int { - return when (direction) { - // When we want to sell a token, the bigger the quote - the better - SwapDirection.SPECIFIED_IN -> (quote - other.quote).signum() - // When we want to buy a token, the smaller the quote - the better - SwapDirection.SPECIFIED_OUT -> (other.quote - quote).signum() - } - } -} - -class AssetExchangeFee( - val networkFee: Fee, - val minimumBalanceBuyIn: MinimumBalanceBuyIn -) diff --git a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/assetConversion/AssetConversionExchange.kt b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/assetConversion/AssetConversionExchange.kt index 048a99b223..fe0c0aae7f 100644 --- a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/assetConversion/AssetConversionExchange.kt +++ b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/assetConversion/AssetConversionExchange.kt @@ -2,28 +2,24 @@ package io.novafoundation.nova.feature_swap_impl.data.assetExchange.assetConvers import io.novafoundation.nova.common.data.network.runtime.binding.bindNumberOrNull import io.novafoundation.nova.common.utils.Modules -import io.novafoundation.nova.common.utils.MultiMap import io.novafoundation.nova.common.utils.assetConversion -import io.novafoundation.nova.common.utils.mutableMultiMapOf -import io.novafoundation.nova.common.utils.put 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_account_api.data.extrinsic.ExtrinsicSubmission 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.domain.model.MetaAccount import io.novafoundation.nova.feature_swap_api.domain.model.MinimumBalanceBuyIn -import io.novafoundation.nova.feature_swap_api.domain.model.QuotePath import io.novafoundation.nova.feature_swap_api.domain.model.ReQuoteTrigger import io.novafoundation.nova.feature_swap_api.domain.model.SlippageConfig import io.novafoundation.nova.feature_swap_api.domain.model.SwapDirection -import io.novafoundation.nova.feature_swap_api.domain.model.SwapExecuteArgs +import io.novafoundation.nova.feature_swap_api.domain.model.SwapGraphEdge import io.novafoundation.nova.feature_swap_api.domain.model.SwapLimit import io.novafoundation.nova.feature_swap_api.domain.model.SwapQuoteException +import io.novafoundation.nova.feature_swap_api.domain.model.SwapTransaction +import io.novafoundation.nova.feature_swap_api.domain.model.SwapTransactionArgs +import io.novafoundation.nova.feature_swap_api.domain.model.SwapTransactionFee import io.novafoundation.nova.feature_swap_impl.data.assetExchange.AssetExchange -import io.novafoundation.nova.feature_swap_impl.data.assetExchange.AssetExchangeFee -import io.novafoundation.nova.feature_swap_impl.data.assetExchange.AssetExchangeQuote -import io.novafoundation.nova.feature_swap_impl.data.assetExchange.AssetExchangeQuoteArgs +import io.novafoundation.nova.feature_swap_impl.domain.swap.BaseSwapGraphEdge import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.AssetSourceRegistry import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.types.Balance import io.novafoundation.nova.feature_wallet_api.domain.model.Asset @@ -35,7 +31,6 @@ import io.novafoundation.nova.runtime.ext.fullId import io.novafoundation.nova.runtime.ext.utilityAsset import io.novafoundation.nova.runtime.extrinsic.CustomSignedExtensions.assetTxPayment import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain -import io.novafoundation.nova.runtime.multiNetwork.chain.model.FullChainAssetId import io.novafoundation.nova.runtime.multiNetwork.multiLocation.MultiLocation import io.novafoundation.nova.runtime.multiNetwork.multiLocation.converter.MultiLocationConverter import io.novafoundation.nova.runtime.multiNetwork.multiLocation.converter.MultiLocationConverterFactory @@ -95,7 +90,7 @@ private class AssetConversionExchange( return true } - override suspend fun availableSwapDirections(): MultiMap { + override suspend fun availableDirectSwapConnections(): List { return remoteStorageSource.query(chain.id) { val allPools = metadata.assetConversionOrNull?.pools?.keys().orEmpty() @@ -103,44 +98,6 @@ private class AssetConversionExchange( } } - override suspend fun quote(args: AssetExchangeQuoteArgs): AssetExchangeQuote { - val runtimeCallsApi = multiChainRuntimeCallsApi.forChain(chain.id) - val quotedBalance = runtimeCallsApi.quote( - swapDirection = args.swapDirection, - assetIn = args.chainAssetIn, - assetOut = args.chainAssetOut, - amount = args.amount - ) ?: throw SwapQuoteException.NotEnoughLiquidity - - val quotePath = QuotePath( - segments = listOf( - QuotePath.Segment( - from = args.chainAssetIn.fullId, - to = args.chainAssetOut.fullId, - sourceId = SOURCE_ID, - sourceParams = emptyMap() - ) - ) - ) - - return AssetExchangeQuote(quote = quotedBalance, path = quotePath, direction = args.swapDirection) - } - - override suspend fun estimateFee(args: SwapExecuteArgs): AssetExchangeFee { - val nativeAssetFee = extrinsicService.estimateFee(chain, TransactionOrigin.SelectedWallet) { - executeSwap(args, sendTo = chain.emptyAccountId()) - } - - return convertNativeFeeToPayingTokenFee(nativeAssetFee, args) - } - - override suspend fun swap(args: SwapExecuteArgs): Result { - return extrinsicService.submitExtrinsic(chain, TransactionOrigin.SelectedWallet) { submissionOrigin -> - // Send swapped funds to the requested origin since it the account doing the swap - executeSwap(args, sendTo = submissionOrigin.requestedOrigin) - } - } - override suspend fun slippageConfig(): SlippageConfig { return SlippageConfig.default() } @@ -151,133 +108,16 @@ private class AssetConversionExchange( .map { ReQuoteTrigger } } - private suspend fun constructAllAvailableDirections(pools: List>): MultiMap { - val multiMap = mutableMultiMapOf() - - pools.forEach { (firstLocation, secondLocation) -> - val firstAsset = multiLocationConverter.toChainAsset(firstLocation) ?: return@forEach - val secondAsset = multiLocationConverter.toChainAsset(secondLocation) ?: return@forEach - - val firstAssetId = firstAsset.fullId - val secondAssetId = secondAsset.fullId - - multiMap.put(firstAssetId, secondAssetId) - multiMap.put(secondAssetId, firstAssetId) - } - - return multiMap - } - - private suspend fun convertNativeFeeToPayingTokenFee(nativeTokenFee: Fee, args: SwapExecuteArgs): AssetExchangeFee { - val customFeeAsset = args.customFeeAsset - - return if (customFeeAsset != null && !customFeeAsset.isCommissionAsset()) { - calculateCustomTokenFee(nativeTokenFee, args.nativeAsset, customFeeAsset) - } else { - AssetExchangeFee(nativeTokenFee, MinimumBalanceBuyIn.NoBuyInNeeded) - } - } - - // TODO we purposefully do not use `nativeTokenFee.amountByRequestedAccount` - // since we have disabled fee payment in custom tokens for accounts where the difference matters (e.g. proxy) - // We should adapt it if we decide to remove the restriction - private suspend fun calculateCustomTokenFee( - nativeTokenFee: Fee, - nativeAsset: Asset, - customFeeAsset: Chain.Asset - ): AssetExchangeFee { - val nativeChainAsset = nativeAsset.token.configuration - val runtimeCallsApi = multiChainRuntimeCallsApi.forChain(chain.id) - val assetBalances = assetSourceRegistry.sourceFor(nativeChainAsset).balance - - val minimumBalance = assetBalances.existentialDeposit(chain, nativeChainAsset) - // https://github.com/paritytech/polkadot-sdk/blob/39c04fdd9622792ba8478b1c1c300417943a034b/substrate/frame/transaction-payment/asset-conversion-tx-payment/src/payment.rs#L114 - val shouldBuyMinimumBalance = nativeAsset.balanceCountedTowardsEDInPlanks < minimumBalance + nativeTokenFee.amount - - val toBuyNativeFee = runtimeCallsApi.quoteFeeConversion(nativeTokenFee.amount, customFeeAsset) - - val minimumBalanceBuyIn = if (shouldBuyMinimumBalance) { - val totalConverted = nativeTokenFee.amount + minimumBalance - - val forFeesAndMinBalance = runtimeCallsApi.quoteFeeConversion(totalConverted, customFeeAsset) - val forMinBalance = forFeesAndMinBalance - toBuyNativeFee - - MinimumBalanceBuyIn.NeedsToBuyMinimumBalance( - nativeAsset = nativeAsset.token.configuration, - nativeMinimumBalance = minimumBalance, - commissionAsset = customFeeAsset, - commissionAssetToSpendOnBuyIn = forMinBalance - ) - } else { - MinimumBalanceBuyIn.NoBuyInNeeded - } - - return AssetExchangeFee( - networkFee = SubstrateFee(toBuyNativeFee, nativeTokenFee.submissionOrigin), - minimumBalanceBuyIn = minimumBalanceBuyIn - ) - } - - private suspend fun RuntimeCallsApi.quoteFeeConversion(commissionAmountOut: Balance, customFeeToken: Chain.Asset): Balance { - val quotedAmount = quote( - swapDirection = SwapDirection.SPECIFIED_OUT, - assetIn = customFeeToken, - assetOut = chain.utilityAsset, - amount = commissionAmountOut - ) - - return requireNotNull(quotedAmount) - } - - private suspend fun ExtrinsicBuilder.executeSwap(swapExecuteArgs: SwapExecuteArgs, sendTo: AccountId) { - val path = listOf(swapExecuteArgs.assetIn, swapExecuteArgs.assetOut) - .map { asset -> multiLocationConverter.encodableMultiLocationOf(asset) } - - val keepAlive = false - - when (val swapLimit = swapExecuteArgs.swapLimit) { - is SwapLimit.SpecifiedIn -> call( - moduleName = Modules.ASSET_CONVERSION, - callName = "swap_exact_tokens_for_tokens", - arguments = mapOf( - "path" to path, - "amount_in" to swapLimit.expectedAmountIn, - "amount_out_min" to swapLimit.amountOutMin, - "send_to" to sendTo, - "keep_alive" to keepAlive - ) - ) + private suspend fun constructAllAvailableDirections(pools: List>): List { + return buildList { + pools.forEach { (firstLocation, secondLocation) -> + val firstAsset = multiLocationConverter.toChainAsset(firstLocation) ?: return@forEach + val secondAsset = multiLocationConverter.toChainAsset(secondLocation) ?: return@forEach - is SwapLimit.SpecifiedOut -> call( - moduleName = Modules.ASSET_CONVERSION, - callName = "swap_tokens_for_exact_tokens", - arguments = mapOf( - "path" to path, - "amount_out" to swapLimit.expectedAmountOut, - "amount_in_max" to swapLimit.amountInMax, - "send_to" to sendTo, - "keep_alive" to keepAlive - ) - ) + add(AssetConversionEdge(firstAsset, secondAsset)) + add(AssetConversionEdge(secondAsset, firstAsset)) + } } - - setFeeAsset(swapExecuteArgs.customFeeAsset) - } - - private suspend fun ExtrinsicBuilder.setFeeAsset(feeAsset: Chain.Asset?) { - if (feeAsset == null || feeAsset.isCommissionAsset()) return - - val assetId = multiLocationConverter.encodableMultiLocationOf(feeAsset) - - assetTxPayment(assetId) - } - - private fun Chain.Asset.isCommissionAsset(): Boolean { - return fullId == chain.commissionAsset.fullId - } - - private suspend fun MultiLocationConverter.encodableMultiLocationOf(chainAsset: Chain.Asset): Any? { - return toMultiLocationOrThrow(chainAsset).toEncodableInstance() } private suspend fun RuntimeCallsApi.quote( @@ -319,4 +159,163 @@ private class AssetConversionExchange( return assetIdType.name } + + private inner class AssetConversionEdge(fromAsset: Chain.Asset, toAsset: Chain.Asset) : BaseSwapGraphEdge(fromAsset, toAsset) { + + override suspend fun beginTransaction(args: SwapTransactionArgs): SwapTransaction { + return AssetConversionTransaction(args, fromAsset, toAsset) + } + + override suspend fun appendTransaction(currentTransaction: SwapTransaction, args: SwapTransactionArgs): SwapTransaction? { + return null + } + + override suspend fun quote( + amount: Balance, + direction: SwapDirection + ): Balance { + val runtimeCallsApi = multiChainRuntimeCallsApi.forChain(chain.id) + + return runtimeCallsApi.quote( + swapDirection = direction, + assetIn = fromAsset, + assetOut = toAsset, + amount = amount + ) ?: throw SwapQuoteException.NotEnoughLiquidity + } + } + + class AssetConversionTransaction( + private val transactionArgs: SwapTransactionArgs, + private val fromAsset: Chain.Asset, + private val toAsset: Chain.Asset + ): SwapTransaction { + + override suspend fun estimateFee(): SwapTransactionFee { + val nativeAssetFee = extrinsicService.estimateFee(chain, TransactionOrigin.SelectedWallet) { + executeSwap(sendTo = chain.emptyAccountId()) + } + + return convertNativeFeeToPayingTokenFee(nativeAssetFee) + } + + override suspend fun submit(): Result<*> { + return extrinsicService.submitExtrinsic(chain, TransactionOrigin.SelectedWallet) { submissionOrigin -> + // Send swapped funds to the requested origin since it the account doing the swap + executeSwap(sendTo = submissionOrigin.requestedOrigin) + } + } + + private suspend fun convertNativeFeeToPayingTokenFee(nativeTokenFee: Fee): SwapTransactionFee { + val customFeeAsset = transactionArgs.customFeeAsset + + return if (customFeeAsset != null && !customFeeAsset.isCommissionAsset()) { + calculateCustomTokenFee(nativeTokenFee, transactionArgs.nativeAsset, customFeeAsset) + } else { + SwapTransactionFee(nativeTokenFee, MinimumBalanceBuyIn.NoBuyInNeeded) + } + } + + private suspend fun ExtrinsicBuilder.executeSwap(sendTo: AccountId) { + val path = listOf(fromAsset, toAsset) + .map { asset -> multiLocationConverter.encodableMultiLocationOf(asset) } + + val keepAlive = false + + when (val swapLimit = transactionArgs.swapLimit) { + is SwapLimit.SpecifiedIn -> call( + moduleName = Modules.ASSET_CONVERSION, + callName = "swap_exact_tokens_for_tokens", + arguments = mapOf( + "path" to path, + "amount_in" to swapLimit.expectedAmountIn, + "amount_out_min" to swapLimit.amountOutMin, + "send_to" to sendTo, + "keep_alive" to keepAlive + ) + ) + + is SwapLimit.SpecifiedOut -> call( + moduleName = Modules.ASSET_CONVERSION, + callName = "swap_tokens_for_exact_tokens", + arguments = mapOf( + "path" to path, + "amount_out" to swapLimit.expectedAmountOut, + "amount_in_max" to swapLimit.amountInMax, + "send_to" to sendTo, + "keep_alive" to keepAlive + ) + ) + } + + setFeeAsset(transactionArgs.customFeeAsset) + } + + private suspend fun ExtrinsicBuilder.setFeeAsset(feeAsset: Chain.Asset?) { + if (feeAsset == null || feeAsset.isCommissionAsset()) return + + val assetId = multiLocationConverter.encodableMultiLocationOf(feeAsset) + + assetTxPayment(assetId) + } + + // TODO we purposefully do not use `nativeTokenFee.amountByRequestedAccount` + // since we have disabled fee payment in custom tokens for accounts where the difference matters (e.g. proxy) + // We should adapt it if we decide to remove the restriction + private suspend fun calculateCustomTokenFee( + nativeTokenFee: Fee, + nativeAsset: Asset, + customFeeAsset: Chain.Asset + ): SwapTransactionFee { + val nativeChainAsset = nativeAsset.token.configuration + val runtimeCallsApi = multiChainRuntimeCallsApi.forChain(chain.id) + val assetBalances = assetSourceRegistry.sourceFor(nativeChainAsset).balance + + val minimumBalance = assetBalances.existentialDeposit(chain, nativeChainAsset) + // https://github.com/paritytech/polkadot-sdk/blob/39c04fdd9622792ba8478b1c1c300417943a034b/substrate/frame/transaction-payment/asset-conversion-tx-payment/src/payment.rs#L114 + val shouldBuyMinimumBalance = nativeAsset.balanceCountedTowardsEDInPlanks < minimumBalance + nativeTokenFee.amount + + val toBuyNativeFee = runtimeCallsApi.quoteFeeConversion(nativeTokenFee.amount, customFeeAsset) + + val minimumBalanceBuyIn = if (shouldBuyMinimumBalance) { + val totalConverted = nativeTokenFee.amount + minimumBalance + + val forFeesAndMinBalance = runtimeCallsApi.quoteFeeConversion(totalConverted, customFeeAsset) + val forMinBalance = forFeesAndMinBalance - toBuyNativeFee + + MinimumBalanceBuyIn.NeedsToBuyMinimumBalance( + nativeAsset = nativeAsset.token.configuration, + nativeMinimumBalance = minimumBalance, + commissionAsset = customFeeAsset, + commissionAssetToSpendOnBuyIn = forMinBalance + ) + } else { + MinimumBalanceBuyIn.NoBuyInNeeded + } + + return SwapTransactionFee( + networkFee = SubstrateFee(toBuyNativeFee, nativeTokenFee.submissionOrigin), + minimumBalanceBuyIn = minimumBalanceBuyIn + ) + } + + private suspend fun RuntimeCallsApi.quoteFeeConversion(commissionAmountOut: Balance, customFeeToken: Chain.Asset): Balance { + val quotedAmount = quote( + swapDirection = SwapDirection.SPECIFIED_OUT, + assetIn = customFeeToken, + assetOut = chain.utilityAsset, + amount = commissionAmountOut + ) + + return requireNotNull(quotedAmount) + } + + private fun Chain.Asset.isCommissionAsset(): Boolean { + return fullId == chain.commissionAsset.fullId + } + + private suspend fun MultiLocationConverter.encodableMultiLocationOf(chainAsset: Chain.Asset): Any? { + return toMultiLocationOrThrow(chainAsset).toEncodableInstance() + } + } } diff --git a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/hydraDx/HydraDxExchange.kt b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/hydraDx/HydraDxExchange.kt index 26e5f75602..8529402fdb 100644 --- a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/hydraDx/HydraDxExchange.kt +++ b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/hydraDx/HydraDxExchange.kt @@ -1,17 +1,12 @@ package io.novafoundation.nova.feature_swap_impl.data.assetExchange.hydraDx -import android.util.Log import io.novafoundation.nova.common.utils.Modules -import io.novafoundation.nova.common.utils.MultiMap import io.novafoundation.nova.common.utils.firstById import io.novafoundation.nova.common.utils.flatMap +import io.novafoundation.nova.common.utils.flatMapAsync import io.novafoundation.nova.common.utils.graph.Edge import io.novafoundation.nova.common.utils.graph.Graph import io.novafoundation.nova.common.utils.graph.Path -import io.novafoundation.nova.common.utils.graph.create -import io.novafoundation.nova.common.utils.graph.findAllPossibleDirections -import io.novafoundation.nova.common.utils.graph.findDijkstraPathsBetween -import io.novafoundation.nova.common.utils.mapAsync import io.novafoundation.nova.common.utils.mergeIfMultiple import io.novafoundation.nova.common.utils.orZero import io.novafoundation.nova.common.utils.singleReplaySharedFlow @@ -25,34 +20,30 @@ import io.novafoundation.nova.feature_account_api.data.model.SubstrateFee 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_swap_api.domain.model.MinimumBalanceBuyIn -import io.novafoundation.nova.feature_swap_api.domain.model.QuotePath +import io.novafoundation.nova.feature_swap_api.domain.model.QuotableEdge import io.novafoundation.nova.feature_swap_api.domain.model.ReQuoteTrigger import io.novafoundation.nova.feature_swap_api.domain.model.SlippageConfig import io.novafoundation.nova.feature_swap_api.domain.model.SwapDirection import io.novafoundation.nova.feature_swap_api.domain.model.SwapExecuteArgs +import io.novafoundation.nova.feature_swap_api.domain.model.SwapGraphEdge import io.novafoundation.nova.feature_swap_api.domain.model.SwapLimit -import io.novafoundation.nova.feature_swap_api.domain.model.SwapQuoteException -import io.novafoundation.nova.feature_swap_impl.BuildConfig +import io.novafoundation.nova.feature_swap_api.domain.model.SwapTransaction +import io.novafoundation.nova.feature_swap_api.domain.model.SwapTransactionArgs +import io.novafoundation.nova.feature_swap_api.domain.model.SwapTransactionFee import io.novafoundation.nova.feature_swap_impl.data.assetExchange.AssetExchange -import io.novafoundation.nova.feature_swap_impl.data.assetExchange.AssetExchangeFee -import io.novafoundation.nova.feature_swap_impl.data.assetExchange.AssetExchangeQuote import io.novafoundation.nova.feature_swap_impl.data.assetExchange.AssetExchangeQuoteArgs import io.novafoundation.nova.feature_swap_impl.data.assetExchange.hydraDx.omnipool.OmniPoolSwapSourceFactory import io.novafoundation.nova.feature_swap_impl.data.assetExchange.hydraDx.referrals.linkedAccounts import io.novafoundation.nova.feature_swap_impl.data.assetExchange.hydraDx.referrals.referralsOrNull -import io.novafoundation.nova.feature_swap_impl.data.assetExchange.hydraDx.stableswap.StableSwapSourceFactory import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.AssetSourceRegistry import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.HydraDxAssetId import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.HydraDxAssetIdConverter import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.existentialDepositInPlanks import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.isSystemAsset -import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.toChainAssetOrThrow import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.toOnChainIdOrThrow import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.types.Balance -import io.novafoundation.nova.feature_wallet_api.presentation.formatters.formatPlanks import io.novafoundation.nova.runtime.ethereum.StorageSharedRequestsBuilder import io.novafoundation.nova.runtime.ethereum.StorageSharedRequestsBuilderFactory -import io.novafoundation.nova.runtime.ext.fullId import io.novafoundation.nova.runtime.ext.isUtilityAsset import io.novafoundation.nova.runtime.ext.utilityAsset import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain @@ -65,14 +56,12 @@ import io.novasama.substrate_sdk_android.runtime.extrinsic.ExtrinsicBuilder import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableSharedFlow -import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onEach -private const val PATHS_LIMIT = 4 class HydraDxExchangeFactory( private val remoteStorageSource: StorageDataSource, @@ -110,7 +99,6 @@ private class HydraDxExchange( private val hydraDxNovaReferral: HydraDxNovaReferral, private val swapSourceFactories: Iterable, private val assetSourceRegistry: AssetSourceRegistry, - private val debug: Boolean = BuildConfig.DEBUG ) : AssetExchange { private val swapSources: List = createSources() @@ -119,10 +107,6 @@ private class HydraDxExchange( private val userReferralState: MutableSharedFlow = singleReplaySharedFlow() - private val quotePathsCache: MutableStateFlow?> = MutableStateFlow(null) - - private val graphState: MutableSharedFlow = singleReplaySharedFlow() - override suspend fun canPayFeeInNonUtilityToken(asset: Chain.Asset): Boolean { val onChainId = hydraDxAssetIdConverter.toOnChainIdOrThrow(asset) @@ -135,43 +119,13 @@ private class HydraDxExchange( return fallbackPrice != null } - override suspend fun availableSwapDirections(): MultiMap { - val allDirectDirections = swapSources.mapAsync { source -> - source.availableSwapDirections().mapValues { (from, directions) -> - directions.map { direction -> HydraDxSwapEdge(from, source.identifier, direction) } - } - } - - val graph = Graph.create(allDirectDirections).also { - graphState.emit(it) - } - - return graph.findAllPossibleDirections() - } - - override suspend fun quote(args: AssetExchangeQuoteArgs): AssetExchangeQuote { - val from = args.chainAssetIn.fullId - val to = args.chainAssetOut.fullId - - val paths = pathsFromCacheOrCompute(from, to) { - val graph = graphState.first() - - graph.findDijkstraPathsBetween(from, to, limit = PATHS_LIMIT) - } - - val quotedPaths = paths.mapNotNull { path -> quotePath(path, args.amount, args.swapDirection) } - if (paths.isEmpty()) { - throw SwapQuoteException.NotEnoughLiquidity + override suspend fun availableDirectSwapConnections(): List { + return swapSources.flatMapAsync { source -> + source.availableSwapDirections().map(::HydraDxSwapEdge) } - - if (debug) { - logQuotes(args, quotedPaths) - } - - return quotedPaths.max() } - override suspend fun estimateFee(args: SwapExecuteArgs): AssetExchangeFee { + override suspend fun estimateFee(args: SwapExecuteArgs): SwapTransactionFee { val expectedFeeAsset = args.usedFeeAsset val currentFeeTokenId = currentPaymentAsset.first() @@ -201,7 +155,7 @@ private class HydraDxExchange( submissionOrigin = swapFee.submissionOrigin ) - return AssetExchangeFee(networkFee = feeInExpectedCurrency, MinimumBalanceBuyIn.NoBuyInNeeded) + return SwapTransactionFee(networkFee = feeInExpectedCurrency, MinimumBalanceBuyIn.NoBuyInNeeded) } override suspend fun swap(args: SwapExecuteArgs): Result { @@ -259,50 +213,6 @@ private class HydraDxExchange( } } - private suspend fun quotePath( - path: Path, - amount: Balance, - swapDirection: SwapDirection - ): AssetExchangeQuote? { - val quote = when (swapDirection) { - SwapDirection.SPECIFIED_IN -> quotePathSell(path, amount) - SwapDirection.SPECIFIED_OUT -> quotePathBuy(path, amount) - } ?: return null - - return AssetExchangeQuote(swapDirection, quote, path.toQuotePath()) - } - - private suspend fun quotePathBuy(path: Path, amount: Balance): Balance? { - return runCatching { - path.foldRight(amount) { segment, currentAmount -> - val args = HydraDxSwapSourceQuoteArgs( - chainAssetIn = chain.assetsById.getValue(segment.from.assetId), - chainAssetOut = chain.assetsById.getValue(segment.to.assetId), - amount = currentAmount, - swapDirection = SwapDirection.SPECIFIED_OUT, - params = segment.direction.params - ) - - segment.swapSource().quote(args) - } - }.getOrNull() - } - - private suspend fun quotePathSell(path: Path, amount: Balance): Balance? { - return runCatching { - path.fold(amount) { currentAmount, segment -> - val args = HydraDxSwapSourceQuoteArgs( - chainAssetIn = chain.assetsById.getValue(segment.from.assetId), - chainAssetOut = chain.assetsById.getValue(segment.to.assetId), - amount = currentAmount, - swapDirection = SwapDirection.SPECIFIED_IN, - params = segment.direction.params - ) - - segment.swapSource().quote(args) - } - }.getOrNull() - } private val SwapExecuteArgs.usedFeeAsset: Chain.Asset get() = customFeeAsset ?: chain.utilityAsset @@ -353,29 +263,6 @@ private class HydraDxExchange( return expectedPaymentTokenId.takeIf { currentFeeTokenId != expectedPaymentTokenId } } - private suspend fun ExtrinsicBuilder.executeSwap( - args: SwapExecuteArgs, - justSetFeeCurrency: HydraDxAssetId?, - previousFeeCurrency: HydraDxAssetId - ) { - maybeSetReferral() - - addSwapCall(args) - - setFeeCurrencyToNative(justSetFeeCurrency, previousFeeCurrency) - } - - private suspend fun ExtrinsicBuilder.addSwapCall(args: SwapExecuteArgs) { - val sourceForOptimizedTrade = args.path.checkForOptimizedTrade() - - if (sourceForOptimizedTrade != null) { - with(sourceForOptimizedTrade) { - executeSwap(args) - } - } else { - executeRouterSwap(args) - } - } private fun ExtrinsicBuilder.setFeeCurrencyToNative(justSetFeeCurrency: HydraDxAssetId?, previousFeeCurrency: HydraDxAssetId) { val justSetFeeToNonNative = justSetFeeCurrency != null && justSetFeeCurrency != hydraDxAssetIdConverter.systemAssetId @@ -386,53 +273,6 @@ private class HydraDxExchange( } } - private suspend fun ExtrinsicBuilder.executeRouterSwap(args: SwapExecuteArgs) { - when (val limit = args.swapLimit) { - is SwapLimit.SpecifiedIn -> executeRouterSell(args, limit) - is SwapLimit.SpecifiedOut -> executeRouterBuy(args, limit) - } - } - - private suspend fun ExtrinsicBuilder.executeRouterBuy(args: SwapExecuteArgs, limit: SwapLimit.SpecifiedOut) { - call( - moduleName = Modules.ROUTER, - callName = "buy", - arguments = mapOf( - "asset_in" to hydraDxAssetIdConverter.toOnChainIdOrThrow(args.assetIn), - "asset_out" to hydraDxAssetIdConverter.toOnChainIdOrThrow(args.assetOut), - "amount_out" to limit.expectedAmountOut, - "max_amount_in" to limit.amountInMax, - "route" to args.path.convertToRouterTrade() - ) - ) - } - - private suspend fun ExtrinsicBuilder.executeRouterSell(args: SwapExecuteArgs, limit: SwapLimit.SpecifiedIn) { - call( - moduleName = Modules.ROUTER, - callName = "sell", - arguments = mapOf( - "asset_in" to hydraDxAssetIdConverter.toOnChainIdOrThrow(args.assetIn), - "asset_out" to hydraDxAssetIdConverter.toOnChainIdOrThrow(args.assetOut), - "amount_in" to limit.expectedAmountIn, - "min_amount_out" to limit.amountOutMin, - "route" to args.path.convertToRouterTrade() - ) - ) - } - - private suspend fun QuotePath.convertToRouterTrade(): List { - return segments.map { segment -> - val source = swapSources.firstById(segment.sourceId) - - structOf( - "pool" to source.routerPoolTypeFor(segment.sourceParams), - "assetIn" to hydraDxAssetIdConverter.toOnChainIdOrThrow(segment.from), - "assetOut" to hydraDxAssetIdConverter.toOnChainIdOrThrow(segment.to) - ) - } - } - private suspend fun HydraDxAssetIdConverter.toOnChainIdOrThrow(localId: FullChainAssetId): HydraDxAssetId { val chainAsset = chain.assetsById.getValue(localId.assetId) @@ -469,26 +309,6 @@ private class HydraDxExchange( ) } - private inline fun pathsFromCacheOrCompute( - from: FullChainAssetId, - to: FullChainAssetId, - computation: () -> List> - ): List> { - val mapKey = from to to - val cachedMap = quotePathsCache.value.orEmpty() - val cachedValue = cachedMap[mapKey] - - if (cachedValue != null) { - return cachedValue.paths - } - - val computedPaths = computation() - - val updatedMap = cachedMap + (mapKey to QuotePathsCache(computedPaths)) - quotePathsCache.value = updatedMap - - return computedPaths - } private enum class ReferralState { SET, NOT_SET, NOT_AVAILABLE @@ -502,21 +322,6 @@ private class HydraDxExchange( return swapSources.firstById(sourceId) } - private fun QuotePath.checkForOptimizedTrade(): HydraDxSwapSource? { - if (segments.size != 1) return null - - val onlySegment = segments.single() - - return if (onlySegment.canOptimizeSingleSegmentTrade()) { - swapSources.findOmniPool() - } else { - null - } - } - - private fun QuotePath.Segment.canOptimizeSingleSegmentTrade(): Boolean { - return sourceId == OmniPoolSwapSourceFactory.SOURCE_ID - } private fun Iterable.findOmniPool(): HydraDxSwapSource { return firstById(OmniPoolSwapSourceFactory.SOURCE_ID) @@ -526,53 +331,132 @@ private class HydraDxExchange( return swapSourceFactories.map { it.create(chain) } } - private suspend fun logQuotes(args: AssetExchangeQuoteArgs, quotes: List) { - val allCandidates = quotes.sortedDescending().map { - val formattedIn = args.amount.formatPlanks(args.chainAssetIn) - val formattedOut = it.quote.formatPlanks(args.chainAssetOut) - val formattedPath = formatPath(it.path) + private inner class HydraDxSwapEdge( + private val sourceQuotableEdge: HydraDxSourceEdge, + ) : SwapGraphEdge, QuotableEdge by sourceQuotableEdge { + + override suspend fun beginTransaction(args: SwapTransactionArgs): SwapTransaction { + return HydraDxSwapTransaction(HydraDxSwapTransactionSegment(sourceQuotableEdge, args)) + } - "$formattedIn to $formattedOut via $formattedPath" - }.joinToString(separator = "\n") + override suspend fun appendTransaction(currentTransaction: SwapTransaction, args: SwapTransactionArgs): SwapTransaction? { + if (currentTransaction !is HydraDxSwapTransaction) return null - Log.d("RealSwapService", "-------- New quote ----------") - Log.d("RealSwapService", allCandidates) - Log.d("RealSwapService", "-------- Done quote ----------\n\n\n") + return currentTransaction.appendSegment(HydraDxSwapTransactionSegment(sourceQuotableEdge, args)) + } } - private suspend fun formatPath(path: QuotePath): String { - val assets = chain.assetsById + inner class HydraDxSwapTransaction( + private val segments: List, + ) : SwapTransaction { + + constructor(segment: HydraDxSwapTransactionSegment) : this(listOf(segment)) + + fun appendSegment(nextSegment: HydraDxSwapTransactionSegment): HydraDxSwapTransaction { + return HydraDxSwapTransaction(segments + nextSegment) + } + + override suspend fun estimateFee(): SwapTransactionFee { + TODO("Not yet implemented") + } - return buildString { - val firstSegment = path.segments.first() + override suspend fun submit(): Result<*> { + TODO("Not yet implemented") + } - append(assets.getValue(firstSegment.from.assetId).symbol) + private suspend fun ExtrinsicBuilder.executeSwap( + args: SwapExecuteArgs, + justSetFeeCurrency: HydraDxAssetId?, + previousFeeCurrency: HydraDxAssetId + ) { + maybeSetReferral() - append(" -- ${formatSource(firstSegment)} --> ") + addSwapCall() - append(assets.getValue(firstSegment.to.assetId).symbol) + setFeeCurrencyToNative(justSetFeeCurrency, previousFeeCurrency) + } - path.segments.subList(1, path.segments.size).onEach { segment -> - append(" -- ${formatSource(segment)} --> ") + private suspend fun ExtrinsicBuilder.addSwapCall() { + val sourceForOptimizedTrade = checkForOptimizedTrade() - append(assets.getValue(segment.to.assetId).symbol) + if (sourceForOptimizedTrade != null) { + sourceForOptimizedTrade() + } else { + executeRouterSwap() } } - } - private suspend fun formatSource(segment: QuotePath.Segment): String { - return buildString { - append(segment.sourceId) + private fun checkForOptimizedTrade(): HydraDxStandaloneSwapBuilder? { + if (segments.size != 1) return null + + val onlySegment = segments.single() + return onlySegment.edge.standaloneSwapBuilder + } - if (segment.sourceId == StableSwapSourceFactory.ID) { - val onChainId = segment.sourceParams.getValue("PoolId").toBigInteger() - val chainAsset = hydraDxAssetIdConverter.toChainAssetOrThrow(chain, onChainId) - append("[${chainAsset.symbol}]") + private suspend fun ExtrinsicBuilder.executeRouterSwap() { + val firstLimit = segments + + when (val limit = args.swapLimit) { + is SwapLimit.SpecifiedIn -> executeRouterSell(args, limit) + is SwapLimit.SpecifiedOut -> executeRouterBuy(args, limit) + } + } + + private suspend fun ExtrinsicBuilder.executeRouterBuy( + firstEdge: HydraDxSourceEdge, + firstLimit: SwapLimit.SpecifiedOut, + lastEdge: HydraDxSourceEdge, + lastLimit: SwapLimit.SpecifiedOut + ) { + call( + moduleName = Modules.ROUTER, + callName = "buy", + arguments = mapOf( + "asset_in" to hydraDxAssetIdConverter.toOnChainIdOrThrow(firstEdge.from), + "asset_out" to hydraDxAssetIdConverter.toOnChainIdOrThrow(lastEdge.to), + "amount_out" to lastLimit.expectedAmountOut, + "max_amount_in" to firstLimit.amountInMax, + "route" to routerTradePath() + ) + ) + } + + private suspend fun ExtrinsicBuilder.executeRouterSell( + firstEdge: HydraDxSourceEdge, + firstLimit: SwapLimit.SpecifiedIn, + lastEdge: HydraDxSourceEdge, + lastLimit: SwapLimit.SpecifiedIn + ) { + call( + moduleName = Modules.ROUTER, + callName = "sell", + arguments = mapOf( + "asset_in" to hydraDxAssetIdConverter.toOnChainIdOrThrow(firstEdge.from), + "asset_out" to hydraDxAssetIdConverter.toOnChainIdOrThrow(lastEdge.to), + "amount_in" to firstLimit.expectedAmountIn, + "min_amount_out" to lastLimit.amountOutMin, + "route" to routerTradePath() + ) + ) + } + + private suspend fun routerTradePath(): List { + return segments.map { segment -> + structOf( + "pool" to segment.edge.routerPoolArgument(), + "assetIn" to hydraDxAssetIdConverter.toOnChainIdOrThrow(segment.edge.from), + "assetOut" to hydraDxAssetIdConverter.toOnChainIdOrThrow(segment.edge.to) + ) } } } } +private class HydraDxSwapTransactionSegment( + val edge: HydraDxSourceEdge, + val args: SwapTransactionArgs, +) + private class HydraDxSwapEdge( override val from: FullChainAssetId, val sourceId: HydraDxSwapSourceId, @@ -581,11 +465,3 @@ private class HydraDxSwapEdge( override val to: FullChainAssetId = direction.to } - -private fun Path.toQuotePath(): QuotePath { - val segments = map { - QuotePath.Segment(it.from, it.to, it.sourceId, it.direction.params) - } - - return QuotePath(segments) -} diff --git a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/hydraDx/HydraDxSwapSource.kt b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/hydraDx/HydraDxSwapSource.kt index 72443b4cfb..1291b2a9b0 100644 --- a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/hydraDx/HydraDxSwapSource.kt +++ b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/hydraDx/HydraDxSwapSource.kt @@ -1,11 +1,10 @@ package io.novafoundation.nova.feature_swap_impl.data.assetExchange.hydraDx import io.novafoundation.nova.common.utils.Identifiable -import io.novafoundation.nova.common.utils.MultiMapList import io.novafoundation.nova.core.updater.SharedRequestsBuilder +import io.novafoundation.nova.feature_swap_api.domain.model.QuotableEdge import io.novafoundation.nova.feature_swap_api.domain.model.SwapDirection import io.novafoundation.nova.feature_swap_api.domain.model.SwapExecuteArgs -import io.novafoundation.nova.feature_swap_api.domain.model.SwapQuoteException import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.types.Balance import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain import io.novafoundation.nova.runtime.multiNetwork.chain.model.FullChainAssetId @@ -15,23 +14,27 @@ import io.novasama.substrate_sdk_android.runtime.extrinsic.ExtrinsicBuilder import kotlinx.coroutines.flow.Flow typealias HydraDxSwapSourceId = String +typealias HydraDxStandaloneSwapBuilder = ExtrinsicBuilder.(args: SwapExecuteArgs) -> Unit -interface HydraDxSwapSource : Identifiable { +interface HydraDxSourceEdge : QuotableEdge { - suspend fun availableSwapDirections(): MultiMapList + fun routerPoolArgument(): DictEnum.Entry<*> - suspend fun ExtrinsicBuilder.executeSwap(args: SwapExecuteArgs) + /** + * Whether hydra swap source is able to perform optimized standalone swap without using Router + */ + val standaloneSwapBuilder: HydraDxStandaloneSwapBuilder? +} - @Throws(SwapQuoteException::class) - suspend fun quote(args: HydraDxSwapSourceQuoteArgs): Balance +interface HydraDxSwapSource : Identifiable { + + suspend fun availableSwapDirections(): Collection suspend fun runSubscriptions( userAccountId: AccountId, subscriptionBuilder: SharedRequestsBuilder ): Flow - fun routerPoolTypeFor(params: Map): DictEnum.Entry<*> - interface Factory { fun create(chain: Chain): HydraDxSwapSource diff --git a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/hydraDx/omnipool/OmniPoolSwapSource.kt b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/hydraDx/omnipool/OmniPoolSwapSource.kt index ff9c3c05dd..83020a377a 100644 --- a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/hydraDx/omnipool/OmniPoolSwapSource.kt +++ b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/hydraDx/omnipool/OmniPoolSwapSource.kt @@ -1,7 +1,6 @@ package io.novafoundation.nova.feature_swap_impl.data.assetExchange.hydraDx.omnipool import io.novafoundation.nova.common.utils.Modules -import io.novafoundation.nova.common.utils.MultiMapList import io.novafoundation.nova.common.utils.dynamicFees import io.novafoundation.nova.common.utils.numberConstant import io.novafoundation.nova.common.utils.omnipool @@ -10,13 +9,13 @@ import io.novafoundation.nova.common.utils.padEnd import io.novafoundation.nova.common.utils.singleReplaySharedFlow import io.novafoundation.nova.common.utils.toMultiSubscription import io.novafoundation.nova.core.updater.SharedRequestsBuilder +import io.novafoundation.nova.feature_swap_api.domain.model.QuotableEdge +import io.novafoundation.nova.feature_swap_api.domain.model.SwapDirection import io.novafoundation.nova.feature_swap_api.domain.model.SwapExecuteArgs import io.novafoundation.nova.feature_swap_api.domain.model.SwapLimit import io.novafoundation.nova.feature_swap_api.domain.model.SwapQuoteException import io.novafoundation.nova.feature_swap_impl.data.assetExchange.hydraDx.HydraDxSwapSource import io.novafoundation.nova.feature_swap_impl.data.assetExchange.hydraDx.HydraDxSwapSourceId -import io.novafoundation.nova.feature_swap_impl.data.assetExchange.hydraDx.HydraDxSwapSourceQuoteArgs -import io.novafoundation.nova.feature_swap_impl.data.assetExchange.hydraDx.HydraSwapDirection import io.novafoundation.nova.feature_swap_impl.data.assetExchange.hydraDx.omnipool.model.DynamicFee import io.novafoundation.nova.feature_swap_impl.data.assetExchange.hydraDx.omnipool.model.OmniPool import io.novafoundation.nova.feature_swap_impl.data.assetExchange.hydraDx.omnipool.model.OmniPoolFees @@ -24,6 +23,7 @@ import io.novafoundation.nova.feature_swap_impl.data.assetExchange.hydraDx.omnip import io.novafoundation.nova.feature_swap_impl.data.assetExchange.hydraDx.omnipool.model.OmnipoolAssetState import io.novafoundation.nova.feature_swap_impl.data.assetExchange.hydraDx.omnipool.model.feeParamsConstant import io.novafoundation.nova.feature_swap_impl.data.assetExchange.hydraDx.omnipool.model.quote +import io.novafoundation.nova.feature_swap_impl.domain.swap.QuotableEdge import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.AssetSourceRegistry import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.HydraDxAssetId import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.HydraDxAssetIdConverter @@ -82,25 +82,26 @@ private class OmniPoolSwapSource( override val identifier: HydraDxSwapSourceId = OmniPoolSwapSourceFactory.SOURCE_ID - private val pooledOnChainAssetIdsState: MutableSharedFlow> = singleReplaySharedFlow() + private val pooledOnChainAssetIdsState: MutableSharedFlow> = singleReplaySharedFlow() private val omniPoolFlow: MutableSharedFlow = singleReplaySharedFlow() - override suspend fun availableSwapDirections(): MultiMapList { + override suspend fun availableSwapDirections(): List { val pooledOnChainAssetIds = getPooledOnChainAssetIds() val pooledChainAssetsIds = matchKnownChainAssetIds(pooledOnChainAssetIds) pooledOnChainAssetIdsState.emit(pooledChainAssetsIds) - return pooledChainAssetsIds.associateBy( - keySelector = { it.second }, - valueTransform = { (_, currentId) -> + return pooledChainAssetsIds.flatMap { remoteAndLocal -> + pooledChainAssetsIds.mapNotNull { otherRemoteAndLocal -> // In OmniPool, each asset is tradable with any other except itself - pooledChainAssetsIds.mapNotNull { (_, otherId) -> - otherId.takeIf { currentId != otherId }?.let { OmniPoolSwapDirection(currentId, otherId) } + if (remoteAndLocal.second.id != otherRemoteAndLocal.second.id) { + OmniPoolSwapEdge(fromAsset = remoteAndLocal, toAsset = otherRemoteAndLocal) + } else { + null } } - ) + } } override suspend fun ExtrinsicBuilder.executeSwap(args: SwapExecuteArgs) { @@ -123,16 +124,6 @@ private class OmniPoolSwapSource( } } - override suspend fun quote(args: HydraDxSwapSourceQuoteArgs): Balance { - val omniPool = omniPoolFlow.first() - - val omniPoolTokenIdIn = hydraDxAssetIdConverter.toOnChainIdOrThrow(args.chainAssetIn) - val omniPoolTokenIdOut = hydraDxAssetIdConverter.toOnChainIdOrThrow(args.chainAssetOut) - - return omniPool.quote(omniPoolTokenIdIn, omniPoolTokenIdOut, args.amount, args.swapDirection) - ?: throw SwapQuoteException.NotEnoughLiquidity - } - @OptIn(ExperimentalCoroutinesApi::class) override suspend fun runSubscriptions( userAccountId: AccountId, @@ -153,8 +144,7 @@ private class OmniPoolSwapSource( val poolAccountId = omniPoolAccountId() - val omniPoolBalancesFlow = pooledAssets.map { (omniPoolTokenId, chainAssetId) -> - val chainAsset = chain.assetsById.getValue(chainAssetId.assetId) + val omniPoolBalancesFlow = pooledAssets.map { (omniPoolTokenId, chainAsset) -> val assetSource = assetSourceRegistry.sourceFor(chainAsset) assetSource.balance.subscribeTransferableAccountBalance(chain, chainAsset, poolAccountId, subscriptionBuilder).map { omniPoolTokenId to it @@ -193,13 +183,13 @@ private class OmniPoolSwapSource( } } - private suspend fun matchKnownChainAssetIds(onChainIds: List): List { + private suspend fun matchKnownChainAssetIds(onChainIds: List): List { val hydraDxAssetIds = hydraDxAssetIdConverter.allOnChainIds(chain) return onChainIds.mapNotNull { onChainId -> val asset = hydraDxAssetIds[onChainId] ?: return@mapNotNull null - onChainId to asset.fullId + onChainId to asset } } @@ -274,10 +264,21 @@ private class OmniPoolSwapSource( ) } - private class OmniPoolSwapDirection(override val from: FullChainAssetId, override val to: FullChainAssetId) : HydraSwapDirection { + private inner class OmniPoolSwapEdge( + private val fromAsset: RemoteIdAndLocalAsset, + private val toAsset: RemoteIdAndLocalAsset + ) : QuotableEdge { + + override val from: FullChainAssetId = fromAsset.second.fullId + + override val to: FullChainAssetId = fromAsset.second.fullId - override val params: Map - get() = emptyMap() + override suspend fun quote(amount: Balance, direction: SwapDirection): Balance { + val omniPool = omniPoolFlow.first() + + return omniPool.quote(fromAsset.first, toAsset.first, amount, direction) + ?: throw SwapQuoteException.NotEnoughLiquidity + } } } @@ -286,6 +287,7 @@ fun omniPoolAccountId(): AccountId { } typealias RemoteAndLocalId = Pair +typealias RemoteIdAndLocalAsset = Pair val RemoteAndLocalId.remoteId get() = first diff --git a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/domain/swap/RealSwapService.kt b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/domain/swap/RealSwapService.kt index ddcc443c28..c382e44d41 100644 --- a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/domain/swap/RealSwapService.kt +++ b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/domain/swap/RealSwapService.kt @@ -2,15 +2,20 @@ package io.novafoundation.nova.feature_swap_impl.domain.swap import android.util.Log import io.novafoundation.nova.common.data.memory.ComputationalCache -import io.novafoundation.nova.common.utils.MultiMap import io.novafoundation.nova.common.utils.Percent -import io.novafoundation.nova.common.utils.accumulateMaps import io.novafoundation.nova.common.utils.asPerbill import io.novafoundation.nova.common.utils.atLeastZero import io.novafoundation.nova.common.utils.filterNotNull import io.novafoundation.nova.common.utils.flatMap import io.novafoundation.nova.common.utils.flowOf +import io.novafoundation.nova.common.utils.graph.Graph +import io.novafoundation.nova.common.utils.graph.Path +import io.novafoundation.nova.common.utils.graph.create +import io.novafoundation.nova.common.utils.graph.findAllPossibleDestinations +import io.novafoundation.nova.common.utils.graph.findDijkstraPathsBetween +import io.novafoundation.nova.common.utils.graph.vertices import io.novafoundation.nova.common.utils.isZero +import io.novafoundation.nova.common.utils.mergeIfMultiple import io.novafoundation.nova.common.utils.throttleLast import io.novafoundation.nova.common.utils.toPercent import io.novafoundation.nova.common.utils.withFlowScope @@ -18,17 +23,22 @@ import io.novafoundation.nova.feature_account_api.data.extrinsic.ExtrinsicSubmis import io.novafoundation.nova.feature_account_api.domain.interfaces.AccountRepository import io.novafoundation.nova.feature_account_api.domain.model.MetaAccount import io.novafoundation.nova.feature_account_api.domain.model.requestedAccountPaysFees +import io.novafoundation.nova.feature_swap_api.domain.model.QuotableEdge import io.novafoundation.nova.feature_swap_api.domain.model.ReQuoteTrigger import io.novafoundation.nova.feature_swap_api.domain.model.SlippageConfig import io.novafoundation.nova.feature_swap_api.domain.model.SwapDirection import io.novafoundation.nova.feature_swap_api.domain.model.SwapExecuteArgs import io.novafoundation.nova.feature_swap_api.domain.model.SwapFee +import io.novafoundation.nova.feature_swap_api.domain.model.SwapGraph +import io.novafoundation.nova.feature_swap_api.domain.model.SwapGraphEdge +import io.novafoundation.nova.feature_swap_api.domain.model.SwapPath import io.novafoundation.nova.feature_swap_api.domain.model.SwapQuote import io.novafoundation.nova.feature_swap_api.domain.model.SwapQuoteArgs +import io.novafoundation.nova.feature_swap_api.domain.model.SwapQuoteException +import io.novafoundation.nova.feature_swap_api.domain.model.SwapTransaction import io.novafoundation.nova.feature_swap_api.domain.swap.SwapService +import io.novafoundation.nova.feature_swap_impl.BuildConfig import io.novafoundation.nova.feature_swap_impl.data.assetExchange.AssetExchange -import io.novafoundation.nova.feature_swap_impl.data.assetExchange.AssetExchangeQuote -import io.novafoundation.nova.feature_swap_impl.data.assetExchange.AssetExchangeQuoteArgs import io.novafoundation.nova.feature_swap_impl.data.assetExchange.assetConversion.AssetConversionExchangeFactory import io.novafoundation.nova.feature_swap_impl.data.assetExchange.hydraDx.HydraDxExchangeFactory import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.types.Balance @@ -48,6 +58,7 @@ import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.runningFold import kotlinx.coroutines.withContext import java.math.BigDecimal import kotlin.coroutines.coroutineContext @@ -56,12 +67,17 @@ import kotlin.time.Duration.Companion.milliseconds private const val ALL_DIRECTIONS_CACHE = "RealSwapService.ALL_DIRECTIONS" private const val EXCHANGES_CACHE = "RealSwapService.EXCHANGES" +private const val QUOTES_CACHE = "RealSwapService.QuotesCache" + +private const val PATHS_LIMIT = 4 + internal class RealSwapService( private val assetConversionFactory: AssetConversionExchangeFactory, private val hydraDxOmnipoolFactory: HydraDxExchangeFactory, private val computationalCache: ComputationalCache, private val chainRegistry: ChainRegistry, private val accountRepository: AccountRepository, + private val debug: Boolean = BuildConfig.DEBUG ) : SwapService { override suspend fun canPayFeeInNonUtilityAsset(asset: Chain.Asset): Boolean = withContext(Dispatchers.Default) { @@ -79,44 +95,36 @@ internal class RealSwapService( override suspend fun assetsAvailableForSwap( computationScope: CoroutineScope ): Flow> { - return allAvailableDirections(computationScope).map { it.keys } + return directionsGraph(computationScope).map { it.vertices() } } override suspend fun availableSwapDirectionsFor( asset: Chain.Asset, computationScope: CoroutineScope ): Flow> { - return allAvailableDirections(computationScope).map { it[asset.fullId].orEmpty() } + return directionsGraph(computationScope).map { it.findAllPossibleDestinations(asset.fullId) } } - override suspend fun quote(args: SwapQuoteArgs): Result { + override suspend fun quote( + args: SwapQuoteArgs, + computationSharingScope: CoroutineScope + ): Result { return withContext(Dispatchers.Default) { runCatching { - val exchange = exchanges(this).getValue(args.tokenIn.configuration.chainId) - val quoteArgs = AssetExchangeQuoteArgs( - chainAssetIn = args.tokenIn.configuration, - chainAssetOut = args.tokenOut.configuration, - amount = args.amount, - swapDirection = args.swapDirection - ) - val quote = exchange.quote(quoteArgs) - - val (amountIn, amountOut) = args.inAndOutAmounts(quote) - - SwapQuote( - amountIn = args.tokenIn.configuration.withAmount(amountIn), - amountOut = args.tokenOut.configuration.withAmount(amountOut), - direction = args.swapDirection, - priceImpact = args.calculatePriceImpact(amountIn, amountOut), - path = quote.path - ) + quoteInternal(args, computationSharingScope) }.onFailure { Log.e("RealSwapService", "Error while quoting", it) } } } - override suspend fun estimateFee(args: SwapExecuteArgs): SwapFee { + override suspend fun estimateFee(quote: SwapQuote): SwapFee { + + + val swapFee: SwapFee? = quote.path.fold(null) { acc, edge -> + val segmentFee = edge. + } + val computationScope = CoroutineScope(coroutineContext) val exchange = exchanges(computationScope).getValue(args.assetIn.chainId) @@ -125,6 +133,48 @@ internal class RealSwapService( return SwapFee(networkFee = assetExchangeFee.networkFee, minimumBalanceBuyIn = assetExchangeFee.minimumBalanceBuyIn) } + private fun QuotedPath.toTransactionList(): List { + val transactions = mutableListOf() + var currentTransaction: SwapTransaction? = null + + path.forEach { edge -> + if (currentTransaction == null) { + currentTransaction = edge.beginTransaction() + } + } + } + + private suspend fun quoteInternal( + args: SwapQuoteArgs, + computationSharingScope: CoroutineScope + ): SwapQuote { + val from = args.tokenIn.configuration.fullId + val to = args.tokenOut.configuration.fullId + + val paths = pathsFromCacheOrCompute(from, to, computationSharingScope) { + val graph = directionsGraph(computationSharingScope).first() + + graph.findDijkstraPathsBetween(from, to, limit = PATHS_LIMIT) + } + + val quotedPaths = paths.mapNotNull { path -> quotePath(path, args.amount, args.swapDirection) } + if (paths.isEmpty()) { + throw SwapQuoteException.NotEnoughLiquidity + } + + val bestPathQuote = quotedPaths.max() + + val (amountIn, amountOut) = args.inAndOutAmounts(bestPathQuote) + + return SwapQuote( + amountIn = args.tokenIn.configuration.withAmount(amountIn), + amountOut = args.tokenOut.configuration.withAmount(amountOut), + direction = args.swapDirection, + priceImpact = args.calculatePriceImpact(amountIn, amountOut), + path = bestPathQuote.path + ) + } + override suspend fun swap(args: SwapExecuteArgs): Result { val computationScope = CoroutineScope(coroutineContext) @@ -152,7 +202,7 @@ internal class RealSwapService( return calculatePriceImpact(fiatIn, fiatOut) } - private fun SwapQuoteArgs.inAndOutAmounts(quote: AssetExchangeQuote): Pair { + private fun SwapQuoteArgs.inAndOutAmounts(quote: QuotedPath): Pair { return when (swapDirection) { SwapDirection.SPECIFIED_IN -> amount to quote.quote SwapDirection.SPECIFIED_OUT -> quote.quote to amount @@ -167,22 +217,23 @@ internal class RealSwapService( return priceImpact.asPerbill().toPercent() } - private suspend fun allAvailableDirections(computationScope: CoroutineScope): Flow> { + private suspend fun directionsGraph(computationScope: CoroutineScope): Flow { return computationalCache.useSharedFlow(ALL_DIRECTIONS_CACHE, computationScope) { val exchanges = exchanges(computationScope) val directionsByExchange = exchanges.map { (chainId, exchange) -> - flowOf { exchange.availableSwapDirections() } + flowOf { exchange.availableDirectSwapConnections() } .catch { - emit(emptyMap()) + emit(emptyList()) Log.e("RealSwapService", "Failed to fetch directions for exchange ${exchange::class} in chain $chainId", it) } } directionsByExchange - .accumulateMaps() + .accumulateLists() .filter { it.isNotEmpty() } + .map { Graph.create(it) } } } @@ -208,4 +259,139 @@ internal class RealSwapService( return factory?.create(chain, computationScope) } + + // Assumes each flow will have only single element + private fun List>>.accumulateLists(): Flow> { + return mergeIfMultiple() + .runningFold(emptyList()) { acc, directions -> acc + directions } + } + + private suspend fun pathsFromCacheOrCompute( + from: FullChainAssetId, + to: FullChainAssetId, + scope: CoroutineScope, + computation: suspend () -> List> + ): List> { + val mapKey = from to to + val cacheKey = "$QUOTES_CACHE:$mapKey" + + return computationalCache.useCache(cacheKey, scope) { + computation() + } + } + + private suspend fun quotePath( + path: SwapPath, + amount: Balance, + swapDirection: SwapDirection + ): QuotedPath? { + val quote = when (swapDirection) { + SwapDirection.SPECIFIED_IN -> quotePathSell(path, amount) + SwapDirection.SPECIFIED_OUT -> quotePathBuy(path, amount) + } ?: return null + + return QuotedPath(swapDirection, quote, path) + } + + private suspend fun quotePathBuy(path: Path, amount: Balance): Balance? { + return runCatching { + path.foldRight(amount) { segment, currentAmount -> + segment.quote(currentAmount, SwapDirection.SPECIFIED_OUT) + } + }.getOrNull() + } + + private suspend fun quotePathSell(path: Path, amount: Balance): Balance? { + return runCatching { + path.fold(amount) { currentAmount, segment -> + segment.quote(currentAmount, SwapDirection.SPECIFIED_IN) + } + }.getOrNull() + } + + // TOOD rework path logging +// private suspend fun logQuotes(args: SwapQuoteArgs, quotes: List) { +// val allCandidates = quotes.sortedDescending().map { +// val formattedIn = args.amount.formatPlanks(args.tokenIn.configuration) +// val formattedOut = it.quote.formatPlanks(args.tokenOut.configuration) +// val formattedPath = formatPath(it.path) +// +// "$formattedIn to $formattedOut via $formattedPath" +// }.joinToString(separator = "\n") +// +// Log.d("RealSwapService", "-------- New quote ----------") +// Log.d("RealSwapService", allCandidates) +// Log.d("RealSwapService", "-------- Done quote ----------\n\n\n") +// } +// +// private suspend fun formatPath(path: QuotePath): String { +// val assets = chain.assetsById +// +// return buildString { +// val firstSegment = path.segments.first() +// +// append(assets.getValue(firstSegment.from.assetId).symbol) +// +// append(" -- ${formatSource(firstSegment)} --> ") +// +// append(assets.getValue(firstSegment.to.assetId).symbol) +// +// path.segments.subList(1, path.segments.size).onEach { segment -> +// append(" -- ${formatSource(segment)} --> ") +// +// append(assets.getValue(segment.to.assetId).symbol) +// } +// } +// } +// +// private suspend fun formatSource(segment: QuotePath.Segment): String { +// return buildString { +// append(segment.sourceId) +// +// if (segment.sourceId == StableSwapSourceFactory.ID) { +// val onChainId = segment.sourceParams.getValue("PoolId").toBigInteger() +// val chainAsset = hydraDxAssetIdConverter.toChainAssetOrThrow(chain, onChainId) +// append("[${chainAsset.symbol}]") +// } +// } +// } +} + +abstract class BaseSwapGraphEdge( + val fromAsset: Chain.Asset, + val toAsset: Chain.Asset +) : SwapGraphEdge { + + final override val from: FullChainAssetId = fromAsset.fullId + + final override val to: FullChainAssetId = toAsset.fullId +} + + +abstract class BaseQuotableEdge( + val fromAsset: Chain.Asset, + val toAsset: Chain.Asset +) : QuotableEdge { + + final override val from: FullChainAssetId = fromAsset.fullId + + final override val to: FullChainAssetId = toAsset.fullId +} + +private class QuotedPath( + val direction: SwapDirection, + + val quote: Balance, + + val path: SwapPath +) : Comparable { + + override fun compareTo(other: QuotedPath): Int { + return when (direction) { + // When we want to sell a token, the bigger the quote - the better + SwapDirection.SPECIFIED_IN -> (quote - other.quote).signum() + // When we want to buy a token, the smaller the quote - the better + SwapDirection.SPECIFIED_OUT -> (other.quote - quote).signum() + } + } } From 2c5b30de012bbd7196c535a6ad4604e7f6baf82f Mon Sep 17 00:00:00 2001 From: valentun Date: Wed, 12 Jun 2024 23:24:15 +0700 Subject: [PATCH 05/83] WIP SwapService interface finalized --- .../nova/common/utils/KotlinExt.kt | 4 + .../nova/common/utils/Percent.kt | 4 + .../domain/model/AtomicSwapOperation.kt | 32 +++++ .../domain/model/SwapGraph.kt | 6 +- .../domain/model/SwapQuote.kt | 19 ++- .../domain/model/SwapQuoteArgs.kt | 37 +++-- .../domain/model/SwapTransaction.kt | 23 --- .../domain/swap/SwapService.kt | 6 +- .../AssetConversionExchange.kt | 39 +++--- .../assetExchange/hydraDx/HydraDxExchange.kt | 64 +++++---- .../hydraDx/HydraDxSwapSource.kt | 4 +- .../domain/swap/RealSwapService.kt | 131 ++++++++++++------ 12 files changed, 227 insertions(+), 142 deletions(-) create mode 100644 feature-swap-api/src/main/java/io/novafoundation/nova/feature_swap_api/domain/model/AtomicSwapOperation.kt delete mode 100644 feature-swap-api/src/main/java/io/novafoundation/nova/feature_swap_api/domain/model/SwapTransaction.kt diff --git a/common/src/main/java/io/novafoundation/nova/common/utils/KotlinExt.kt b/common/src/main/java/io/novafoundation/nova/common/utils/KotlinExt.kt index 385dea4759..3eea9e51ca 100644 --- a/common/src/main/java/io/novafoundation/nova/common/utils/KotlinExt.kt +++ b/common/src/main/java/io/novafoundation/nova/common/utils/KotlinExt.kt @@ -225,6 +225,10 @@ fun Result.requireException() = exceptionOrNull()!! fun Result.requireValue() = getOrThrow()!! +fun Result.requireInnerNotNull(): Result { + return mapCatching { requireNotNull(it) } +} + /** * Given a list finds a partition point in O(log2(N)) given that there is only a single partition point present. * That is, there is only a single place in the whole array where the value of [partition] changes from false to true diff --git a/common/src/main/java/io/novafoundation/nova/common/utils/Percent.kt b/common/src/main/java/io/novafoundation/nova/common/utils/Percent.kt index 5ea66904ae..9ce2cc2e9d 100644 --- a/common/src/main/java/io/novafoundation/nova/common/utils/Percent.kt +++ b/common/src/main/java/io/novafoundation/nova/common/utils/Percent.kt @@ -36,6 +36,10 @@ value class Percent(val value: Double) : Comparable { override fun compareTo(other: Percent): Int { return value.compareTo(other.value) } + + operator fun div(divisor: Int): Percent { + return Percent(value / divisor) + } } val Percent.fraction: BigDecimal diff --git a/feature-swap-api/src/main/java/io/novafoundation/nova/feature_swap_api/domain/model/AtomicSwapOperation.kt b/feature-swap-api/src/main/java/io/novafoundation/nova/feature_swap_api/domain/model/AtomicSwapOperation.kt new file mode 100644 index 0000000000..bf993cac52 --- /dev/null +++ b/feature-swap-api/src/main/java/io/novafoundation/nova/feature_swap_api/domain/model/AtomicSwapOperation.kt @@ -0,0 +1,32 @@ +package io.novafoundation.nova.feature_swap_api.domain.model + +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_wallet_api.data.network.blockhain.types.Balance +import io.novafoundation.nova.feature_wallet_api.domain.model.Asset +import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain + +interface AtomicSwapOperation { + + suspend fun estimateFee(): AtomicSwapOperationFee + + suspend fun submit(previousStepCorrection: SwapExecutionCorrection?): Result +} + +class AtomicSwapOperationArgs( + val swapLimit: SwapLimit, + val customFeeAsset: Chain.Asset?, + val nativeAsset: Asset, +) + +class AtomicSwapOperationFee( + networkFee: Fee, + val minimumBalanceBuyIn: MinimumBalanceBuyIn +) : Fee by networkFee + +class SwapExecutionCorrection( + val actualFee: Balance, + val actualReceivedAmount: Balance, + val submission: ExtrinsicSubmission, + // TODO we may potentially adjust slippage as well... +) diff --git a/feature-swap-api/src/main/java/io/novafoundation/nova/feature_swap_api/domain/model/SwapGraph.kt b/feature-swap-api/src/main/java/io/novafoundation/nova/feature_swap_api/domain/model/SwapGraph.kt index 924a8a273d..0ae7aacfd2 100644 --- a/feature-swap-api/src/main/java/io/novafoundation/nova/feature_swap_api/domain/model/SwapGraph.kt +++ b/feature-swap-api/src/main/java/io/novafoundation/nova/feature_swap_api/domain/model/SwapGraph.kt @@ -8,14 +8,14 @@ import io.novafoundation.nova.runtime.multiNetwork.chain.model.FullChainAssetId interface SwapGraphEdge : QuotableEdge { - suspend fun beginTransaction(args: SwapTransactionArgs): SwapTransaction + suspend fun beginOperation(args: AtomicSwapOperationArgs): AtomicSwapOperation /** * Append current swap edge execution to the existing transaction * Return null if it is not possible, indicating that the new transaction should be initiated to handle this edge via - * [beginTransaction] + * [beginOperation] */ - suspend fun appendTransaction(currentTransaction: SwapTransaction, args: SwapTransactionArgs): SwapTransaction? + suspend fun appendToOperation(currentTransaction: AtomicSwapOperation, args: AtomicSwapOperationArgs): AtomicSwapOperation? } interface QuotableEdge : Edge { diff --git a/feature-swap-api/src/main/java/io/novafoundation/nova/feature_swap_api/domain/model/SwapQuote.kt b/feature-swap-api/src/main/java/io/novafoundation/nova/feature_swap_api/domain/model/SwapQuote.kt index b87df6ea81..9c81d896e0 100644 --- a/feature-swap-api/src/main/java/io/novafoundation/nova/feature_swap_api/domain/model/SwapQuote.kt +++ b/feature-swap-api/src/main/java/io/novafoundation/nova/feature_swap_api/domain/model/SwapQuote.kt @@ -39,8 +39,9 @@ data class SwapQuote( } class QuotedSwapEdge( - val edge: SwapGraphEdge, - val quote: Balance + val quotedAmount: Balance, + val quote: Balance, + val edge: SwapGraphEdge ) @@ -70,9 +71,17 @@ infix fun ChainAssetWithAmount.rateAgainst(assetOut: ChainAssetWithAmount): BigD } class SwapFee( - override val networkFee: Fee, - val minimumBalanceBuyIn: MinimumBalanceBuyIn, -) : GenericFee + val atomicOperationFees: List +) : GenericFee { + + // TODO handle multi-segment fee display + override val networkFee: Fee + get() = atomicOperationFees.first() + + // TODO handle multi-segment minimumBalanceBuyIn + val minimumBalanceBuyIn: MinimumBalanceBuyIn + get() = atomicOperationFees.first().minimumBalanceBuyIn +} val SwapFee.totalDeductedPlanks: Balance get() = networkFee.amountByRequestedAccount + minimumBalanceBuyIn.commissionAssetToSpendOnBuyIn diff --git a/feature-swap-api/src/main/java/io/novafoundation/nova/feature_swap_api/domain/model/SwapQuoteArgs.kt b/feature-swap-api/src/main/java/io/novafoundation/nova/feature_swap_api/domain/model/SwapQuoteArgs.kt index b7f746d650..71413a25fc 100644 --- a/feature-swap-api/src/main/java/io/novafoundation/nova/feature_swap_api/domain/model/SwapQuoteArgs.kt +++ b/feature-swap-api/src/main/java/io/novafoundation/nova/feature_swap_api/domain/model/SwapQuoteArgs.kt @@ -2,6 +2,7 @@ package io.novafoundation.nova.feature_swap_api.domain.model import io.novafoundation.nova.common.utils.Percent import io.novafoundation.nova.common.utils.fraction +import io.novafoundation.nova.common.utils.graph.Path import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.types.Balance import io.novafoundation.nova.feature_wallet_api.domain.model.Asset import io.novafoundation.nova.feature_wallet_api.domain.model.Token @@ -17,7 +18,13 @@ data class SwapQuoteArgs( ) class SwapExecuteArgs( - val quote: SwapQuote, + val slippage: Percent, + val executionPath: Path, + val direction: SwapDirection, +) + +class SegmentExecuteArgs( + val quotedSwapEdge: QuotedSwapEdge, val nativeAsset: Asset, val customFeeAsset: Chain.Asset?, ) @@ -25,19 +32,19 @@ class SwapExecuteArgs( val SwapExecuteArgs.feeAsset: Chain.Asset get() = customFeeAsset ?: assetIn -sealed class SwapLimit(val expectedAmountIn: Balance, val expectedAmountOut: Balance) { +sealed class SwapLimit { class SpecifiedIn( - expectedAmountIn: Balance, - expectedAmountOut: Balance, + val amountIn: Balance, + val amountOutQuote: Balance, val amountOutMin: Balance - ) : SwapLimit(expectedAmountIn, expectedAmountOut) + ) : SwapLimit() class SpecifiedOut( - expectedAmountIn: Balance, - expectedAmountOut: Balance, + val amountOut: Balance, + val amountInQuote: Balance, val amountInMax: Balance - ) : SwapLimit(expectedAmountIn, expectedAmountOut) + ) : SwapLimit() } fun SwapQuoteArgs.toExecuteArgs(quote: SwapQuote, customFeeAsset: Chain.Asset?, nativeAsset: Asset): SwapExecuteArgs { @@ -52,7 +59,11 @@ fun SwapQuoteArgs.toExecuteArgs(quote: SwapQuote, customFeeAsset: Chain.Asset?, } fun SwapQuoteArgs.swapLimits(quotedBalance: Balance): SwapLimit { - return when (swapDirection) { + return SwapLimit(swapDirection, amount, slippage, quotedBalance) +} + +fun SwapLimit(direction: SwapDirection, amount: Balance, slippage: Percent, quotedBalance: Balance): SwapLimit { + return when (direction) { SwapDirection.SPECIFIED_IN -> SpecifiedIn(amount, slippage, quotedBalance) SwapDirection.SPECIFIED_OUT -> SpecifiedOut(amount, slippage, quotedBalance) } @@ -64,8 +75,8 @@ private fun SpecifiedIn(amount: Balance, slippage: Percent, quotedBalance: Balan val amountOutMin = quotedBalance.toBigDecimal() * lessAmountCoefficient return SwapLimit.SpecifiedIn( - expectedAmountIn = amount, - expectedAmountOut = quotedBalance, + amountIn = amount, + amountOutQuote = quotedBalance, amountOutMin = amountOutMin.toBigInteger() ) } @@ -76,8 +87,8 @@ private fun SpecifiedOut(amount: Balance, slippage: Percent, quotedBalance: Bala val amountInMax = quotedBalance.toBigDecimal() * moreAmountCoefficient return SwapLimit.SpecifiedOut( - expectedAmountIn = quotedBalance, - expectedAmountOut = amount, + amountOut = amount, + amountInQuote = quotedBalance, amountInMax = amountInMax.toBigInteger() ) } diff --git a/feature-swap-api/src/main/java/io/novafoundation/nova/feature_swap_api/domain/model/SwapTransaction.kt b/feature-swap-api/src/main/java/io/novafoundation/nova/feature_swap_api/domain/model/SwapTransaction.kt deleted file mode 100644 index fdea816cdc..0000000000 --- a/feature-swap-api/src/main/java/io/novafoundation/nova/feature_swap_api/domain/model/SwapTransaction.kt +++ /dev/null @@ -1,23 +0,0 @@ -package io.novafoundation.nova.feature_swap_api.domain.model - -import io.novafoundation.nova.feature_account_api.data.model.Fee -import io.novafoundation.nova.feature_wallet_api.domain.model.Asset -import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain - -interface SwapTransaction { - - suspend fun estimateFee(): SwapTransactionFee - - suspend fun submit(): Result<*> -} - -class SwapTransactionArgs( - val swapLimit: SwapLimit, - val customFeeAsset: Chain.Asset?, - val nativeAsset: Asset, -) - -class SwapTransactionFee( - val networkFee: Fee, - val minimumBalanceBuyIn: MinimumBalanceBuyIn -) diff --git a/feature-swap-api/src/main/java/io/novafoundation/nova/feature_swap_api/domain/swap/SwapService.kt b/feature-swap-api/src/main/java/io/novafoundation/nova/feature_swap_api/domain/swap/SwapService.kt index 3dad4b4e8a..a640931fd6 100644 --- a/feature-swap-api/src/main/java/io/novafoundation/nova/feature_swap_api/domain/swap/SwapService.kt +++ b/feature-swap-api/src/main/java/io/novafoundation/nova/feature_swap_api/domain/swap/SwapService.kt @@ -1,10 +1,10 @@ package io.novafoundation.nova.feature_swap_api.domain.swap -import io.novafoundation.nova.feature_account_api.data.extrinsic.ExtrinsicSubmission import io.novafoundation.nova.feature_account_api.domain.model.MetaAccount import io.novafoundation.nova.feature_swap_api.domain.model.ReQuoteTrigger import io.novafoundation.nova.feature_swap_api.domain.model.SlippageConfig import io.novafoundation.nova.feature_swap_api.domain.model.SwapExecuteArgs +import io.novafoundation.nova.feature_swap_api.domain.model.SwapExecutionCorrection import io.novafoundation.nova.feature_swap_api.domain.model.SwapFee import io.novafoundation.nova.feature_swap_api.domain.model.SwapQuote import io.novafoundation.nova.feature_swap_api.domain.model.SwapQuoteArgs @@ -27,9 +27,9 @@ interface SwapService { computationSharingScope: CoroutineScope ): Result - suspend fun estimateFee(quote: SwapQuote): SwapFee + suspend fun estimateFee(executeArgs: SwapExecuteArgs): SwapFee - suspend fun swap(args: SwapExecuteArgs): Result + suspend fun swap(args: SwapExecuteArgs): Result suspend fun slippageConfig(chainId: ChainId): SlippageConfig? diff --git a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/assetConversion/AssetConversionExchange.kt b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/assetConversion/AssetConversionExchange.kt index fe0c0aae7f..d6a920c841 100644 --- a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/assetConversion/AssetConversionExchange.kt +++ b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/assetConversion/AssetConversionExchange.kt @@ -8,16 +8,17 @@ import io.novafoundation.nova.feature_account_api.data.extrinsic.ExtrinsicServic 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.domain.model.MetaAccount +import io.novafoundation.nova.feature_swap_api.domain.model.AtomicSwapOperation +import io.novafoundation.nova.feature_swap_api.domain.model.AtomicSwapOperationArgs +import io.novafoundation.nova.feature_swap_api.domain.model.AtomicSwapOperationFee import io.novafoundation.nova.feature_swap_api.domain.model.MinimumBalanceBuyIn import io.novafoundation.nova.feature_swap_api.domain.model.ReQuoteTrigger import io.novafoundation.nova.feature_swap_api.domain.model.SlippageConfig import io.novafoundation.nova.feature_swap_api.domain.model.SwapDirection +import io.novafoundation.nova.feature_swap_api.domain.model.SwapExecutionCorrection import io.novafoundation.nova.feature_swap_api.domain.model.SwapGraphEdge import io.novafoundation.nova.feature_swap_api.domain.model.SwapLimit import io.novafoundation.nova.feature_swap_api.domain.model.SwapQuoteException -import io.novafoundation.nova.feature_swap_api.domain.model.SwapTransaction -import io.novafoundation.nova.feature_swap_api.domain.model.SwapTransactionArgs -import io.novafoundation.nova.feature_swap_api.domain.model.SwapTransactionFee import io.novafoundation.nova.feature_swap_impl.data.assetExchange.AssetExchange import io.novafoundation.nova.feature_swap_impl.domain.swap.BaseSwapGraphEdge import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.AssetSourceRegistry @@ -162,11 +163,11 @@ private class AssetConversionExchange( private inner class AssetConversionEdge(fromAsset: Chain.Asset, toAsset: Chain.Asset) : BaseSwapGraphEdge(fromAsset, toAsset) { - override suspend fun beginTransaction(args: SwapTransactionArgs): SwapTransaction { - return AssetConversionTransaction(args, fromAsset, toAsset) + override suspend fun beginOperation(args: AtomicSwapOperationArgs): AtomicSwapOperation { + return AssetConversionOperation(args, fromAsset, toAsset) } - override suspend fun appendTransaction(currentTransaction: SwapTransaction, args: SwapTransactionArgs): SwapTransaction? { + override suspend fun appendToOperation(currentTransaction: AtomicSwapOperation, args: AtomicSwapOperationArgs): AtomicSwapOperation? { return null } @@ -185,13 +186,13 @@ private class AssetConversionExchange( } } - class AssetConversionTransaction( - private val transactionArgs: SwapTransactionArgs, + inner class AssetConversionOperation( + private val transactionArgs: AtomicSwapOperationArgs, private val fromAsset: Chain.Asset, private val toAsset: Chain.Asset - ): SwapTransaction { + ): AtomicSwapOperation { - override suspend fun estimateFee(): SwapTransactionFee { + override suspend fun estimateFee(): AtomicSwapOperationFee { val nativeAssetFee = extrinsicService.estimateFee(chain, TransactionOrigin.SelectedWallet) { executeSwap(sendTo = chain.emptyAccountId()) } @@ -199,20 +200,22 @@ private class AssetConversionExchange( return convertNativeFeeToPayingTokenFee(nativeAssetFee) } - override suspend fun submit(): Result<*> { - return extrinsicService.submitExtrinsic(chain, TransactionOrigin.SelectedWallet) { submissionOrigin -> + override suspend fun submit(previousStepCorrection: SwapExecutionCorrection?): Result { + // TODO use `previousStepCorrection` to correct used call arguments + // TODO implement watching for extrinsic events + return extrinsicService.submitAndWatchExtrinsic(chain, TransactionOrigin.SelectedWallet) { submissionOrigin -> // Send swapped funds to the requested origin since it the account doing the swap executeSwap(sendTo = submissionOrigin.requestedOrigin) } } - private suspend fun convertNativeFeeToPayingTokenFee(nativeTokenFee: Fee): SwapTransactionFee { + private suspend fun convertNativeFeeToPayingTokenFee(nativeTokenFee: Fee): AtomicSwapOperationFee { val customFeeAsset = transactionArgs.customFeeAsset return if (customFeeAsset != null && !customFeeAsset.isCommissionAsset()) { calculateCustomTokenFee(nativeTokenFee, transactionArgs.nativeAsset, customFeeAsset) } else { - SwapTransactionFee(nativeTokenFee, MinimumBalanceBuyIn.NoBuyInNeeded) + AtomicSwapOperationFee(nativeTokenFee, MinimumBalanceBuyIn.NoBuyInNeeded) } } @@ -228,7 +231,7 @@ private class AssetConversionExchange( callName = "swap_exact_tokens_for_tokens", arguments = mapOf( "path" to path, - "amount_in" to swapLimit.expectedAmountIn, + "amount_in" to swapLimit.amountIn, "amount_out_min" to swapLimit.amountOutMin, "send_to" to sendTo, "keep_alive" to keepAlive @@ -240,7 +243,7 @@ private class AssetConversionExchange( callName = "swap_tokens_for_exact_tokens", arguments = mapOf( "path" to path, - "amount_out" to swapLimit.expectedAmountOut, + "amount_out" to swapLimit.amountOut, "amount_in_max" to swapLimit.amountInMax, "send_to" to sendTo, "keep_alive" to keepAlive @@ -266,7 +269,7 @@ private class AssetConversionExchange( nativeTokenFee: Fee, nativeAsset: Asset, customFeeAsset: Chain.Asset - ): SwapTransactionFee { + ): AtomicSwapOperationFee { val nativeChainAsset = nativeAsset.token.configuration val runtimeCallsApi = multiChainRuntimeCallsApi.forChain(chain.id) val assetBalances = assetSourceRegistry.sourceFor(nativeChainAsset).balance @@ -293,7 +296,7 @@ private class AssetConversionExchange( MinimumBalanceBuyIn.NoBuyInNeeded } - return SwapTransactionFee( + return AtomicSwapOperationFee( networkFee = SubstrateFee(toBuyNativeFee, nativeTokenFee.submissionOrigin), minimumBalanceBuyIn = minimumBalanceBuyIn ) diff --git a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/hydraDx/HydraDxExchange.kt b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/hydraDx/HydraDxExchange.kt index 8529402fdb..39b7070f6f 100644 --- a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/hydraDx/HydraDxExchange.kt +++ b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/hydraDx/HydraDxExchange.kt @@ -27,9 +27,10 @@ import io.novafoundation.nova.feature_swap_api.domain.model.SwapDirection import io.novafoundation.nova.feature_swap_api.domain.model.SwapExecuteArgs import io.novafoundation.nova.feature_swap_api.domain.model.SwapGraphEdge import io.novafoundation.nova.feature_swap_api.domain.model.SwapLimit -import io.novafoundation.nova.feature_swap_api.domain.model.SwapTransaction -import io.novafoundation.nova.feature_swap_api.domain.model.SwapTransactionArgs -import io.novafoundation.nova.feature_swap_api.domain.model.SwapTransactionFee +import io.novafoundation.nova.feature_swap_api.domain.model.AtomicSwapOperation +import io.novafoundation.nova.feature_swap_api.domain.model.AtomicSwapOperationArgs +import io.novafoundation.nova.feature_swap_api.domain.model.AtomicSwapOperationFee +import io.novafoundation.nova.feature_swap_api.domain.model.SwapExecutionCorrection import io.novafoundation.nova.feature_swap_impl.data.assetExchange.AssetExchange import io.novafoundation.nova.feature_swap_impl.data.assetExchange.AssetExchangeQuoteArgs import io.novafoundation.nova.feature_swap_impl.data.assetExchange.hydraDx.omnipool.OmniPoolSwapSourceFactory @@ -125,7 +126,7 @@ private class HydraDxExchange( } } - override suspend fun estimateFee(args: SwapExecuteArgs): SwapTransactionFee { + override suspend fun estimateFee(args: SwapExecuteArgs): AtomicSwapOperationFee { val expectedFeeAsset = args.usedFeeAsset val currentFeeTokenId = currentPaymentAsset.first() @@ -155,7 +156,7 @@ private class HydraDxExchange( submissionOrigin = swapFee.submissionOrigin ) - return SwapTransactionFee(networkFee = feeInExpectedCurrency, MinimumBalanceBuyIn.NoBuyInNeeded) + return AtomicSwapOperationFee(networkFee = feeInExpectedCurrency, MinimumBalanceBuyIn.NoBuyInNeeded) } override suspend fun swap(args: SwapExecuteArgs): Result { @@ -335,37 +336,38 @@ private class HydraDxExchange( private val sourceQuotableEdge: HydraDxSourceEdge, ) : SwapGraphEdge, QuotableEdge by sourceQuotableEdge { - override suspend fun beginTransaction(args: SwapTransactionArgs): SwapTransaction { - return HydraDxSwapTransaction(HydraDxSwapTransactionSegment(sourceQuotableEdge, args)) + override suspend fun beginOperation(args: AtomicSwapOperationArgs): AtomicSwapOperation { + return HydraDxOperation(HydraDxSwapTransactionSegment(sourceQuotableEdge, args)) } - override suspend fun appendTransaction(currentTransaction: SwapTransaction, args: SwapTransactionArgs): SwapTransaction? { - if (currentTransaction !is HydraDxSwapTransaction) return null + override suspend fun appendToOperation(currentTransaction: AtomicSwapOperation, args: AtomicSwapOperationArgs): AtomicSwapOperation? { + if (currentTransaction !is HydraDxOperation) return null return currentTransaction.appendSegment(HydraDxSwapTransactionSegment(sourceQuotableEdge, args)) } } - inner class HydraDxSwapTransaction( + inner class HydraDxOperation( private val segments: List, - ) : SwapTransaction { + ) : AtomicSwapOperation { constructor(segment: HydraDxSwapTransactionSegment) : this(listOf(segment)) - fun appendSegment(nextSegment: HydraDxSwapTransactionSegment): HydraDxSwapTransaction { - return HydraDxSwapTransaction(segments + nextSegment) + fun appendSegment(nextSegment: HydraDxSwapTransactionSegment): HydraDxOperation { + return HydraDxOperation(segments + nextSegment) } - override suspend fun estimateFee(): SwapTransactionFee { + override suspend fun estimateFee(): AtomicSwapOperationFee { TODO("Not yet implemented") } - override suspend fun submit(): Result<*> { + override suspend fun submit(previousStepCorrection: SwapExecutionCorrection?): Result { + // TODO use `previousStepCorrection` to correct used call arguments + TODO("Not yet implemented") } private suspend fun ExtrinsicBuilder.executeSwap( - args: SwapExecuteArgs, justSetFeeCurrency: HydraDxAssetId?, previousFeeCurrency: HydraDxAssetId ) { @@ -377,28 +379,30 @@ private class HydraDxExchange( } private suspend fun ExtrinsicBuilder.addSwapCall() { - val sourceForOptimizedTrade = checkForOptimizedTrade() + val optimizationSucceeded = tryOptimizedSwap() - if (sourceForOptimizedTrade != null) { - sourceForOptimizedTrade() - } else { + if (!optimizationSucceeded) { executeRouterSwap() } } - private fun checkForOptimizedTrade(): HydraDxStandaloneSwapBuilder? { - if (segments.size != 1) return null + private fun ExtrinsicBuilder.tryOptimizedSwap(): Boolean { + if (segments.size != 1) return false val onlySegment = segments.single() - return onlySegment.edge.standaloneSwapBuilder + val standaloneSwapBuilder = onlySegment.edge.standaloneSwapBuilder ?: return false + + standaloneSwapBuilder(onlySegment.segmentArgs) + return true } private suspend fun ExtrinsicBuilder.executeRouterSwap() { - val firstLimit = segments + val firstSegment = segments.first() + val lastSegment = segments.last() - when (val limit = args.swapLimit) { - is SwapLimit.SpecifiedIn -> executeRouterSell(args, limit) - is SwapLimit.SpecifiedOut -> executeRouterBuy(args, limit) + when (val firstLimit = firstSegment.segmentArgs.swapLimit) { + is SwapLimit.SpecifiedIn -> executeRouterSell(firstSegment.edge, firstLimit, lastSegment.edge, lastSegment.segmentArgs.swapLimit as SwapLimit.SpecifiedIn) + is SwapLimit.SpecifiedOut -> executeRouterBuy(firstSegment.edge, firstLimit, lastSegment.edge, lastSegment.segmentArgs.swapLimit as SwapLimit.SpecifiedOut) } } @@ -414,7 +418,7 @@ private class HydraDxExchange( arguments = mapOf( "asset_in" to hydraDxAssetIdConverter.toOnChainIdOrThrow(firstEdge.from), "asset_out" to hydraDxAssetIdConverter.toOnChainIdOrThrow(lastEdge.to), - "amount_out" to lastLimit.expectedAmountOut, + "amount_out" to lastLimit.amountOut, "max_amount_in" to firstLimit.amountInMax, "route" to routerTradePath() ) @@ -433,7 +437,7 @@ private class HydraDxExchange( arguments = mapOf( "asset_in" to hydraDxAssetIdConverter.toOnChainIdOrThrow(firstEdge.from), "asset_out" to hydraDxAssetIdConverter.toOnChainIdOrThrow(lastEdge.to), - "amount_in" to firstLimit.expectedAmountIn, + "amount_in" to firstLimit.amountIn, "min_amount_out" to lastLimit.amountOutMin, "route" to routerTradePath() ) @@ -454,7 +458,7 @@ private class HydraDxExchange( private class HydraDxSwapTransactionSegment( val edge: HydraDxSourceEdge, - val args: SwapTransactionArgs, + val segmentArgs: AtomicSwapOperationArgs, ) private class HydraDxSwapEdge( diff --git a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/hydraDx/HydraDxSwapSource.kt b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/hydraDx/HydraDxSwapSource.kt index 1291b2a9b0..39aad751b9 100644 --- a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/hydraDx/HydraDxSwapSource.kt +++ b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/hydraDx/HydraDxSwapSource.kt @@ -2,9 +2,9 @@ package io.novafoundation.nova.feature_swap_impl.data.assetExchange.hydraDx import io.novafoundation.nova.common.utils.Identifiable import io.novafoundation.nova.core.updater.SharedRequestsBuilder +import io.novafoundation.nova.feature_swap_api.domain.model.AtomicSwapOperationArgs import io.novafoundation.nova.feature_swap_api.domain.model.QuotableEdge import io.novafoundation.nova.feature_swap_api.domain.model.SwapDirection -import io.novafoundation.nova.feature_swap_api.domain.model.SwapExecuteArgs import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.types.Balance import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain import io.novafoundation.nova.runtime.multiNetwork.chain.model.FullChainAssetId @@ -14,7 +14,7 @@ import io.novasama.substrate_sdk_android.runtime.extrinsic.ExtrinsicBuilder import kotlinx.coroutines.flow.Flow typealias HydraDxSwapSourceId = String -typealias HydraDxStandaloneSwapBuilder = ExtrinsicBuilder.(args: SwapExecuteArgs) -> Unit +typealias HydraDxStandaloneSwapBuilder = ExtrinsicBuilder.(args: AtomicSwapOperationArgs) -> Unit interface HydraDxSourceEdge : QuotableEdge { diff --git a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/domain/swap/RealSwapService.kt b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/domain/swap/RealSwapService.kt index c382e44d41..fc610aa327 100644 --- a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/domain/swap/RealSwapService.kt +++ b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/domain/swap/RealSwapService.kt @@ -15,27 +15,32 @@ import io.novafoundation.nova.common.utils.graph.findAllPossibleDestinations import io.novafoundation.nova.common.utils.graph.findDijkstraPathsBetween import io.novafoundation.nova.common.utils.graph.vertices import io.novafoundation.nova.common.utils.isZero +import io.novafoundation.nova.common.utils.mapAsync import io.novafoundation.nova.common.utils.mergeIfMultiple +import io.novafoundation.nova.common.utils.requireInnerNotNull import io.novafoundation.nova.common.utils.throttleLast import io.novafoundation.nova.common.utils.toPercent import io.novafoundation.nova.common.utils.withFlowScope -import io.novafoundation.nova.feature_account_api.data.extrinsic.ExtrinsicSubmission import io.novafoundation.nova.feature_account_api.domain.interfaces.AccountRepository import io.novafoundation.nova.feature_account_api.domain.model.MetaAccount import io.novafoundation.nova.feature_account_api.domain.model.requestedAccountPaysFees +import io.novafoundation.nova.feature_swap_api.domain.model.AtomicSwapOperation +import io.novafoundation.nova.feature_swap_api.domain.model.AtomicSwapOperationArgs import io.novafoundation.nova.feature_swap_api.domain.model.QuotableEdge +import io.novafoundation.nova.feature_swap_api.domain.model.QuotedSwapEdge import io.novafoundation.nova.feature_swap_api.domain.model.ReQuoteTrigger import io.novafoundation.nova.feature_swap_api.domain.model.SlippageConfig import io.novafoundation.nova.feature_swap_api.domain.model.SwapDirection import io.novafoundation.nova.feature_swap_api.domain.model.SwapExecuteArgs +import io.novafoundation.nova.feature_swap_api.domain.model.SwapExecutionCorrection import io.novafoundation.nova.feature_swap_api.domain.model.SwapFee import io.novafoundation.nova.feature_swap_api.domain.model.SwapGraph import io.novafoundation.nova.feature_swap_api.domain.model.SwapGraphEdge +import io.novafoundation.nova.feature_swap_api.domain.model.SwapLimit import io.novafoundation.nova.feature_swap_api.domain.model.SwapPath import io.novafoundation.nova.feature_swap_api.domain.model.SwapQuote import io.novafoundation.nova.feature_swap_api.domain.model.SwapQuoteArgs import io.novafoundation.nova.feature_swap_api.domain.model.SwapQuoteException -import io.novafoundation.nova.feature_swap_api.domain.model.SwapTransaction import io.novafoundation.nova.feature_swap_api.domain.swap.SwapService import io.novafoundation.nova.feature_swap_impl.BuildConfig import io.novafoundation.nova.feature_swap_impl.data.assetExchange.AssetExchange @@ -118,30 +123,60 @@ internal class RealSwapService( } } - override suspend fun estimateFee(quote: SwapQuote): SwapFee { + override suspend fun estimateFee(executeArgs: SwapExecuteArgs): SwapFee { + val atomicOperations = executeArgs.constructAtomicOperations() + val fees = atomicOperations.mapAsync { it.estimateFee() } - val swapFee: SwapFee? = quote.path.fold(null) { acc, edge -> - val segmentFee = edge. - } + return SwapFee(fees) + } - val computationScope = CoroutineScope(coroutineContext) - val exchange = exchanges(computationScope).getValue(args.assetIn.chainId) + override suspend fun swap(args: SwapExecuteArgs): Result { + val atomicOperations = args.constructAtomicOperations() - val assetExchangeFee = exchange.estimateFee(args) + val initialCorrection: Result = Result.success(null) - return SwapFee(networkFee = assetExchangeFee.networkFee, minimumBalanceBuyIn = assetExchangeFee.minimumBalanceBuyIn) + return atomicOperations.fold(initialCorrection) { prevStepCorrection, operation -> + prevStepCorrection.flatMap { operation.submit(it) } + }.requireInnerNotNull() } - private fun QuotedPath.toTransactionList(): List { - val transactions = mutableListOf() - var currentTransaction: SwapTransaction? = null + private suspend fun SwapExecuteArgs.constructAtomicOperations(): List { + var currentSwapTx: AtomicSwapOperation? = null + val finishedSwapTxs = mutableListOf() + + // TODO this will result in lower total slippage if some segments are appendable + val perSegmentSlippage = slippage / executionPath.size - path.forEach { edge -> - if (currentTransaction == null) { - currentTransaction = edge.beginTransaction() + executionPath.forEach { segmentExecuteArgs -> + val quotedEdge = segmentExecuteArgs.quotedSwapEdge + + val operationArgs = AtomicSwapOperationArgs( + swapLimit = SwapLimit(direction, quotedEdge.quotedAmount, perSegmentSlippage, quotedEdge.quote), + customFeeAsset = segmentExecuteArgs.customFeeAsset, + nativeAsset = segmentExecuteArgs.nativeAsset + ) + + // Initial case - begin first operation + if (currentSwapTx == null) { + currentSwapTx = quotedEdge.edge.beginOperation(operationArgs) + return@forEach + } + + // Try to append segment to current swap tx + val maybeAppendedCurrentTx = quotedEdge.edge.appendToOperation(currentSwapTx!!, operationArgs) + + currentSwapTx = if (maybeAppendedCurrentTx == null) { + finishedSwapTxs.add(currentSwapTx!!) + quotedEdge.edge.beginOperation(operationArgs) + } else { + maybeAppendedCurrentTx } } + + finishedSwapTxs.add(currentSwapTx!!) + + return finishedSwapTxs } private suspend fun quoteInternal( @@ -175,13 +210,6 @@ internal class RealSwapService( ) } - override suspend fun swap(args: SwapExecuteArgs): Result { - val computationScope = CoroutineScope(coroutineContext) - - return runCatching { exchanges(computationScope).getValue(args.assetIn.chainId) } - .flatMap { exchange -> exchange.swap(args) } - } - override suspend fun slippageConfig(chainId: ChainId): SlippageConfig? { val computationScope = CoroutineScope(coroutineContext) val exchanges = exchanges(computationScope) @@ -202,10 +230,10 @@ internal class RealSwapService( return calculatePriceImpact(fiatIn, fiatOut) } - private fun SwapQuoteArgs.inAndOutAmounts(quote: QuotedPath): Pair { + private fun SwapQuoteArgs.inAndOutAmounts(quote: QuotedTrade): Pair { return when (swapDirection) { - SwapDirection.SPECIFIED_IN -> amount to quote.quote - SwapDirection.SPECIFIED_OUT -> quote.quote to amount + SwapDirection.SPECIFIED_IN -> amount to quote.lastSegmentQuote + SwapDirection.SPECIFIED_OUT -> quote.firstSegmentQuote to amount } } @@ -284,28 +312,38 @@ internal class RealSwapService( path: SwapPath, amount: Balance, swapDirection: SwapDirection - ): QuotedPath? { + ): QuotedTrade? { val quote = when (swapDirection) { SwapDirection.SPECIFIED_IN -> quotePathSell(path, amount) SwapDirection.SPECIFIED_OUT -> quotePathBuy(path, amount) } ?: return null - return QuotedPath(swapDirection, quote, path) + return QuotedTrade(swapDirection, quote) } - private suspend fun quotePathBuy(path: Path, amount: Balance): Balance? { + private suspend fun quotePathBuy(path: Path, amount: Balance): Path? { return runCatching { - path.foldRight(amount) { segment, currentAmount -> - segment.quote(currentAmount, SwapDirection.SPECIFIED_OUT) - } + val initial = mutableListOf() to amount + + path.foldRight(initial) { segment, (quotedPath, currentAmount) -> + val segmentQuote = segment.quote(currentAmount, SwapDirection.SPECIFIED_OUT) + quotedPath.add(0, QuotedSwapEdge(currentAmount, segmentQuote, segment)) + + quotedPath to segmentQuote + }.first }.getOrNull() } - private suspend fun quotePathSell(path: Path, amount: Balance): Balance? { + private suspend fun quotePathSell(path: Path, amount: Balance): Path? { return runCatching { - path.fold(amount) { currentAmount, segment -> - segment.quote(currentAmount, SwapDirection.SPECIFIED_IN) - } + val initial = mutableListOf() to amount + + path.fold(initial) { (quotedPath, currentAmount), segment -> + val segmentQuote = segment.quote(currentAmount, SwapDirection.SPECIFIED_IN) + quotedPath.add(QuotedSwapEdge(currentAmount, segmentQuote, segment)) + + quotedPath to segmentQuote + }.first }.getOrNull() } @@ -378,20 +416,23 @@ abstract class BaseQuotableEdge( final override val to: FullChainAssetId = toAsset.fullId } -private class QuotedPath( +private class QuotedTrade( val direction: SwapDirection, + val path: Path +) : Comparable { - val quote: Balance, - - val path: SwapPath -) : Comparable { - - override fun compareTo(other: QuotedPath): Int { + override fun compareTo(other: QuotedTrade): Int { return when (direction) { // When we want to sell a token, the bigger the quote - the better - SwapDirection.SPECIFIED_IN -> (quote - other.quote).signum() + SwapDirection.SPECIFIED_IN -> (lastSegmentQuote - other.lastSegmentQuote).signum() // When we want to buy a token, the smaller the quote - the better - SwapDirection.SPECIFIED_OUT -> (other.quote - quote).signum() + SwapDirection.SPECIFIED_OUT -> (other.firstSegmentQuote - firstSegmentQuote).signum() } } } + +private val QuotedTrade.lastSegmentQuote: Balance + get() = path.last().quote + +private val QuotedTrade.firstSegmentQuote: Balance + get() = path.first().quote From cfb6623c40ccb0fd085250d2358a919d9502a859 Mon Sep 17 00:00:00 2001 From: Valentun Date: Thu, 4 Jul 2024 01:44:20 +0300 Subject: [PATCH 06/83] Hydra dx rewritten --- .../domain/model/AtomicSwapOperation.kt | 22 +- .../domain/model/SwapQuoteArgs.kt | 1 - .../data/assetExchange/AssetExchange.kt | 13 +- .../assetExchange/hydraDx/HydraDxExchange.kt | 317 ++++++++---------- .../hydraDx/HydraDxSwapSource.kt | 22 +- .../hydraDx/omnipool/OmniPoolSwapSource.kt | 74 ++-- .../hydraDx/stableswap/StableSwapSource.kt | 85 ++--- .../hydraDx/xyk/XYKSwapSource.kt | 103 +++--- .../domain/swap/RealSwapService.kt | 98 ++++-- 9 files changed, 351 insertions(+), 384 deletions(-) diff --git a/feature-swap-api/src/main/java/io/novafoundation/nova/feature_swap_api/domain/model/AtomicSwapOperation.kt b/feature-swap-api/src/main/java/io/novafoundation/nova/feature_swap_api/domain/model/AtomicSwapOperation.kt index bf993cac52..d52c5be4aa 100644 --- a/feature-swap-api/src/main/java/io/novafoundation/nova/feature_swap_api/domain/model/AtomicSwapOperation.kt +++ b/feature-swap-api/src/main/java/io/novafoundation/nova/feature_swap_api/domain/model/AtomicSwapOperation.kt @@ -1,9 +1,6 @@ package io.novafoundation.nova.feature_swap_api.domain.model -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_wallet_api.data.network.blockhain.types.Balance -import io.novafoundation.nova.feature_wallet_api.domain.model.Asset import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain interface AtomicSwapOperation { @@ -16,17 +13,18 @@ interface AtomicSwapOperation { class AtomicSwapOperationArgs( val swapLimit: SwapLimit, val customFeeAsset: Chain.Asset?, - val nativeAsset: Asset, ) class AtomicSwapOperationFee( - networkFee: Fee, - val minimumBalanceBuyIn: MinimumBalanceBuyIn + networkFee: Fee, val minimumBalanceBuyIn: MinimumBalanceBuyIn ) : Fee by networkFee -class SwapExecutionCorrection( - val actualFee: Balance, - val actualReceivedAmount: Balance, - val submission: ExtrinsicSubmission, - // TODO we may potentially adjust slippage as well... -) +// TODO this will later be used to perform more accurate non-atomic swaps +// So next segments can correct tx args based on outcome of previous segments +//class SwapExecutionCorrection( +// val actualFee: Balance, +// val actualReceivedAmount: Balance, +// val submission: ExtrinsicSubmission, +//) + +class SwapExecutionCorrection() diff --git a/feature-swap-api/src/main/java/io/novafoundation/nova/feature_swap_api/domain/model/SwapQuoteArgs.kt b/feature-swap-api/src/main/java/io/novafoundation/nova/feature_swap_api/domain/model/SwapQuoteArgs.kt index 71413a25fc..998b4a8419 100644 --- a/feature-swap-api/src/main/java/io/novafoundation/nova/feature_swap_api/domain/model/SwapQuoteArgs.kt +++ b/feature-swap-api/src/main/java/io/novafoundation/nova/feature_swap_api/domain/model/SwapQuoteArgs.kt @@ -25,7 +25,6 @@ class SwapExecuteArgs( class SegmentExecuteArgs( val quotedSwapEdge: QuotedSwapEdge, - val nativeAsset: Asset, val customFeeAsset: Chain.Asset?, ) diff --git a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/AssetExchange.kt b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/AssetExchange.kt index a537305ebc..947dba6dca 100644 --- a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/AssetExchange.kt +++ b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/AssetExchange.kt @@ -14,7 +14,16 @@ interface AssetExchange { interface Factory { - suspend fun create(chain: Chain, coroutineScope: CoroutineScope): AssetExchange? + suspend fun create( + chain: Chain, + parentQuoter: ParentQuoter, + coroutineScope: CoroutineScope + ): AssetExchange? + } + + interface ParentQuoter { + + suspend fun quote(quoteArgs: ParentQuoterArgs): Balance } /** @@ -31,7 +40,7 @@ interface AssetExchange { fun runSubscriptions(chain: Chain, metaAccount: MetaAccount): Flow } -data class AssetExchangeQuoteArgs( +data class ParentQuoterArgs( val chainAssetIn: Chain.Asset, val chainAssetOut: Chain.Asset, val amount: Balance, diff --git a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/hydraDx/HydraDxExchange.kt b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/hydraDx/HydraDxExchange.kt index 39b7070f6f..3562ea6676 100644 --- a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/hydraDx/HydraDxExchange.kt +++ b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/hydraDx/HydraDxExchange.kt @@ -1,39 +1,30 @@ package io.novafoundation.nova.feature_swap_impl.data.assetExchange.hydraDx import io.novafoundation.nova.common.utils.Modules -import io.novafoundation.nova.common.utils.firstById -import io.novafoundation.nova.common.utils.flatMap import io.novafoundation.nova.common.utils.flatMapAsync -import io.novafoundation.nova.common.utils.graph.Edge -import io.novafoundation.nova.common.utils.graph.Graph -import io.novafoundation.nova.common.utils.graph.Path import io.novafoundation.nova.common.utils.mergeIfMultiple -import io.novafoundation.nova.common.utils.orZero import io.novafoundation.nova.common.utils.singleReplaySharedFlow import io.novafoundation.nova.common.utils.structOf import io.novafoundation.nova.common.utils.withFlowScope 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_account_api.data.extrinsic.ExtrinsicSubmission import io.novafoundation.nova.feature_account_api.data.extrinsic.awaitInBlock import io.novafoundation.nova.feature_account_api.data.model.SubstrateFee 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_swap_api.domain.model.AtomicSwapOperation +import io.novafoundation.nova.feature_swap_api.domain.model.AtomicSwapOperationArgs +import io.novafoundation.nova.feature_swap_api.domain.model.AtomicSwapOperationFee import io.novafoundation.nova.feature_swap_api.domain.model.MinimumBalanceBuyIn import io.novafoundation.nova.feature_swap_api.domain.model.QuotableEdge import io.novafoundation.nova.feature_swap_api.domain.model.ReQuoteTrigger import io.novafoundation.nova.feature_swap_api.domain.model.SlippageConfig import io.novafoundation.nova.feature_swap_api.domain.model.SwapDirection -import io.novafoundation.nova.feature_swap_api.domain.model.SwapExecuteArgs +import io.novafoundation.nova.feature_swap_api.domain.model.SwapExecutionCorrection import io.novafoundation.nova.feature_swap_api.domain.model.SwapGraphEdge import io.novafoundation.nova.feature_swap_api.domain.model.SwapLimit -import io.novafoundation.nova.feature_swap_api.domain.model.AtomicSwapOperation -import io.novafoundation.nova.feature_swap_api.domain.model.AtomicSwapOperationArgs -import io.novafoundation.nova.feature_swap_api.domain.model.AtomicSwapOperationFee -import io.novafoundation.nova.feature_swap_api.domain.model.SwapExecutionCorrection import io.novafoundation.nova.feature_swap_impl.data.assetExchange.AssetExchange -import io.novafoundation.nova.feature_swap_impl.data.assetExchange.AssetExchangeQuoteArgs -import io.novafoundation.nova.feature_swap_impl.data.assetExchange.hydraDx.omnipool.OmniPoolSwapSourceFactory +import io.novafoundation.nova.feature_swap_impl.data.assetExchange.ParentQuoterArgs import io.novafoundation.nova.feature_swap_impl.data.assetExchange.hydraDx.referrals.linkedAccounts import io.novafoundation.nova.feature_swap_impl.data.assetExchange.hydraDx.referrals.referralsOrNull import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.AssetSourceRegistry @@ -74,7 +65,7 @@ class HydraDxExchangeFactory( private val assetSourceRegistry: AssetSourceRegistry, ) : AssetExchange.Factory { - override suspend fun create(chain: Chain, coroutineScope: CoroutineScope): AssetExchange { + override suspend fun create(chain: Chain, parentQuoter: AssetExchange.ParentQuoter, coroutineScope: CoroutineScope): AssetExchange { return HydraDxExchange( remoteStorageSource = remoteStorageSource, chain = chain, @@ -83,14 +74,12 @@ class HydraDxExchangeFactory( hydraDxAssetIdConverter = hydraDxAssetIdConverter, hydraDxNovaReferral = hydraDxNovaReferral, swapSourceFactories = swapSourceFactories, - assetSourceRegistry = assetSourceRegistry + assetSourceRegistry = assetSourceRegistry, + parentQuoter = parentQuoter ) } } -private typealias HydraSwapGraph = Graph -private typealias QuotePathsCacheKey = Pair - private class HydraDxExchange( private val remoteStorageSource: StorageDataSource, private val chain: Chain, @@ -100,6 +89,7 @@ private class HydraDxExchange( private val hydraDxNovaReferral: HydraDxNovaReferral, private val swapSourceFactories: Iterable, private val assetSourceRegistry: AssetSourceRegistry, + private val parentQuoter: AssetExchange.ParentQuoter, ) : AssetExchange { private val swapSources: List = createSources() @@ -126,60 +116,6 @@ private class HydraDxExchange( } } - override suspend fun estimateFee(args: SwapExecuteArgs): AtomicSwapOperationFee { - val expectedFeeAsset = args.usedFeeAsset - - val currentFeeTokenId = currentPaymentAsset.first() - val paymentCurrencyToSet = getPaymentCurrencyToSetIfNeeded(expectedFeeAsset, currentFeeTokenId) - - val setCurrencyFee = if (paymentCurrencyToSet != null) { - extrinsicService.estimateFee(chain, TransactionOrigin.SelectedWallet) { - setFeeCurrency(paymentCurrencyToSet) - } - } else { - null - } - - val swapFee = extrinsicService.estimateFee(chain, TransactionOrigin.SelectedWallet, BatchMode.FORCE_BATCH) { - executeSwap(args, paymentCurrencyToSet, currentFeeTokenId) - } - - val totalNativeFee = swapFee.amount + setCurrencyFee?.amount.orZero() - - val feeAmountInExpectedCurrency = if (!expectedFeeAsset.isUtilityAsset) { - convertNativeFeeToAssetFee(totalNativeFee, expectedFeeAsset) - } else { - totalNativeFee - } - val feeInExpectedCurrency = SubstrateFee( - amount = feeAmountInExpectedCurrency, - submissionOrigin = swapFee.submissionOrigin - ) - - return AtomicSwapOperationFee(networkFee = feeInExpectedCurrency, MinimumBalanceBuyIn.NoBuyInNeeded) - } - - override suspend fun swap(args: SwapExecuteArgs): Result { - val expectedFeeAsset = args.usedFeeAsset - - val currentFeeTokenId = currentPaymentAsset.first() - val paymentCurrencyToSet = getPaymentCurrencyToSetIfNeeded(expectedFeeAsset, currentFeeTokenId) - - val setCurrencyResult = if (paymentCurrencyToSet != null) { - extrinsicService.submitAndWatchExtrinsic(chain, TransactionOrigin.SelectedWallet) { - setFeeCurrency(paymentCurrencyToSet) - }.awaitInBlock() // we need to wait for tx execution for currency update changes to be taken into account by runtime with executing swap itself - } else { - Result.success(Unit) - } - - return setCurrencyResult.flatMap { - extrinsicService.submitExtrinsic(chain, TransactionOrigin.SelectedWallet, BatchMode.FORCE_BATCH) { - executeSwap(args, paymentCurrencyToSet, currentFeeTokenId) - } - } - } - override suspend fun slippageConfig(): SlippageConfig { return SlippageConfig.default() } @@ -215,9 +151,6 @@ private class HydraDxExchange( } - private val SwapExecuteArgs.usedFeeAsset: Chain.Asset - get() = customFeeAsset ?: chain.utilityAsset - @Suppress("IfThenToElvis") private suspend fun subscribeUserReferral( userAccountId: AccountId, @@ -236,43 +169,6 @@ private class HydraDxExchange( } } - private suspend fun convertNativeFeeToAssetFee( - nativeFeeAmount: Balance, - targetAsset: Chain.Asset - ): Balance { - val args = AssetExchangeQuoteArgs( - chainAssetIn = targetAsset, - chainAssetOut = chain.utilityAsset, - amount = nativeFeeAmount, - swapDirection = SwapDirection.SPECIFIED_OUT - ) - - val quotedFee = quote(args).quote - - // TODO - // There is a issue in Router implementation in Hydra that doesn't allow asset balance to go below ED. We add it to fee for simplicity instead - // of refactoring SwapExistentialDepositAwareMaxActionProvider - // This should be removed once Router issue is fixed - val existentialDeposit = assetSourceRegistry.existentialDepositInPlanks(chain, targetAsset) - - return quotedFee + existentialDeposit - } - - private suspend fun getPaymentCurrencyToSetIfNeeded(expectedPaymentAsset: Chain.Asset, currentFeeTokenId: HydraDxAssetId): HydraDxAssetId? { - val expectedPaymentTokenId = hydraDxAssetIdConverter.toOnChainIdOrThrow(expectedPaymentAsset) - - return expectedPaymentTokenId.takeIf { currentFeeTokenId != expectedPaymentTokenId } - } - - - private fun ExtrinsicBuilder.setFeeCurrencyToNative(justSetFeeCurrency: HydraDxAssetId?, previousFeeCurrency: HydraDxAssetId) { - val justSetFeeToNonNative = justSetFeeCurrency != null && justSetFeeCurrency != hydraDxAssetIdConverter.systemAssetId - val previousCurrencyRemainsNonNative = justSetFeeCurrency == null && previousFeeCurrency != hydraDxAssetIdConverter.systemAssetId - - if (justSetFeeToNonNative || previousCurrencyRemainsNonNative) { - setFeeCurrency(hydraDxAssetIdConverter.systemAssetId) - } - } private suspend fun HydraDxAssetIdConverter.toOnChainIdOrThrow(localId: FullChainAssetId): HydraDxAssetId { val chainAsset = chain.assetsById.getValue(localId.assetId) @@ -280,54 +176,11 @@ private class HydraDxExchange( return toOnChainIdOrThrow(chainAsset) } - private suspend fun ExtrinsicBuilder.maybeSetReferral() { - val referralState = userReferralState.first() - - if (referralState == ReferralState.NOT_SET) { - val novaReferralCode = hydraDxNovaReferral.getNovaReferralCode() - - linkCode(novaReferralCode) - } - } - - private fun ExtrinsicBuilder.linkCode(referralCode: String) { - call( - moduleName = Modules.REFERRALS, - callName = "link_code", - arguments = mapOf( - "code" to referralCode.encodeToByteArray() - ) - ) - } - - private fun ExtrinsicBuilder.setFeeCurrency(onChainId: HydraDxAssetId) { - call( - moduleName = Modules.MULTI_TRANSACTION_PAYMENT, - callName = "set_currency", - arguments = mapOf( - "currency" to onChainId - ) - ) - } - private enum class ReferralState { SET, NOT_SET, NOT_AVAILABLE } - private class QuotePathsCache( - val paths: List> - ) - - private fun HydraDxSwapEdge.swapSource(): HydraDxSwapSource { - return swapSources.firstById(sourceId) - } - - - private fun Iterable.findOmniPool(): HydraDxSwapSource { - return firstById(OmniPoolSwapSourceFactory.SOURCE_ID) - } - private fun createSources(): List { return swapSourceFactories.map { it.create(chain) } } @@ -347,35 +200,73 @@ private class HydraDxExchange( } } - inner class HydraDxOperation( - private val segments: List, + inner class HydraDxOperation private constructor( + val segments: List, ) : AtomicSwapOperation { + private val customFeeAsset: Chain.Asset? + get() = segments.first().segmentArgs.customFeeAsset + + private val usedFeeAsset: Chain.Asset + get() = customFeeAsset ?: chain.utilityAsset + constructor(segment: HydraDxSwapTransactionSegment) : this(listOf(segment)) fun appendSegment(nextSegment: HydraDxSwapTransactionSegment): HydraDxOperation { + require(customFeeAsset == nextSegment.segmentArgs.customFeeAsset) { + "Different fee assets between multiple hydra swap segments - os ot" + } + return HydraDxOperation(segments + nextSegment) } override suspend fun estimateFee(): AtomicSwapOperationFee { - TODO("Not yet implemented") + val nativeFee = extrinsicService.estimateFee(chain, TransactionOrigin.SelectedWallet, BatchMode.FORCE_BATCH) { + executeSwap() + } + + val feeAmountInExpectedCurrency = if (!usedFeeAsset.isUtilityAsset) { + convertNativeFeeToAssetFee(nativeFee.amount, usedFeeAsset) + } else { + nativeFee.amount + } + + val feeInExpectedCurrency = SubstrateFee( + amount = feeAmountInExpectedCurrency, + submissionOrigin = nativeFee.submissionOrigin + ) + + return AtomicSwapOperationFee(networkFee = feeInExpectedCurrency, MinimumBalanceBuyIn.NoBuyInNeeded) } override suspend fun submit(previousStepCorrection: SwapExecutionCorrection?): Result { - // TODO use `previousStepCorrection` to correct used call arguments - - TODO("Not yet implemented") + return extrinsicService.submitAndWatchExtrinsic(chain, TransactionOrigin.SelectedWallet, BatchMode.FORCE_BATCH) { + executeSwap() + }.awaitInBlock().map { + SwapExecutionCorrection() + } } - private suspend fun ExtrinsicBuilder.executeSwap( - justSetFeeCurrency: HydraDxAssetId?, - previousFeeCurrency: HydraDxAssetId - ) { + private suspend fun ExtrinsicBuilder.executeSwap() { + val currentFeeTokenId = currentPaymentAsset.first() + + val justSetFeeCurrency = maybeSetFeeCurrencyToTarget(currentFeeTokenId) + maybeSetReferral() addSwapCall() - setFeeCurrencyToNative(justSetFeeCurrency, previousFeeCurrency) + maybeSetFeeCurrencyToNative(justSetFeeCurrency, previousFeeCurrency = currentFeeTokenId) + } + + private suspend fun ExtrinsicBuilder.maybeSetFeeCurrencyToTarget(currentFeeTokenId: HydraDxAssetId): HydraDxAssetId? { + val paymentCurrencyToSet = getPaymentCurrencyToSetIfNeeded(usedFeeAsset, currentFeeTokenId) + + paymentCurrencyToSet?.let { + setFeeCurrency(paymentCurrencyToSet) + } + + return paymentCurrencyToSet } private suspend fun ExtrinsicBuilder.addSwapCall() { @@ -393,6 +284,7 @@ private class HydraDxExchange( val standaloneSwapBuilder = onlySegment.edge.standaloneSwapBuilder ?: return false standaloneSwapBuilder(onlySegment.segmentArgs) + return true } @@ -401,8 +293,19 @@ private class HydraDxExchange( val lastSegment = segments.last() when (val firstLimit = firstSegment.segmentArgs.swapLimit) { - is SwapLimit.SpecifiedIn -> executeRouterSell(firstSegment.edge, firstLimit, lastSegment.edge, lastSegment.segmentArgs.swapLimit as SwapLimit.SpecifiedIn) - is SwapLimit.SpecifiedOut -> executeRouterBuy(firstSegment.edge, firstLimit, lastSegment.edge, lastSegment.segmentArgs.swapLimit as SwapLimit.SpecifiedOut) + is SwapLimit.SpecifiedIn -> executeRouterSell( + firstSegment.edge, + firstLimit, + lastSegment.edge, + lastSegment.segmentArgs.swapLimit as SwapLimit.SpecifiedIn + ) + + is SwapLimit.SpecifiedOut -> executeRouterBuy( + firstSegment.edge, + firstLimit, + lastSegment.edge, + lastSegment.segmentArgs.swapLimit as SwapLimit.SpecifiedOut + ) } } @@ -453,6 +356,73 @@ private class HydraDxExchange( ) } } + + private suspend fun ExtrinsicBuilder.maybeSetReferral() { + val referralState = userReferralState.first() + + if (referralState == ReferralState.NOT_SET) { + val novaReferralCode = hydraDxNovaReferral.getNovaReferralCode() + + linkCode(novaReferralCode) + } + } + + private fun ExtrinsicBuilder.maybeSetFeeCurrencyToNative(justSetFeeCurrency: HydraDxAssetId?, previousFeeCurrency: HydraDxAssetId) { + val justSetFeeToNonNative = justSetFeeCurrency != null && justSetFeeCurrency != hydraDxAssetIdConverter.systemAssetId + val previousCurrencyRemainsNonNative = justSetFeeCurrency == null && previousFeeCurrency != hydraDxAssetIdConverter.systemAssetId + + if (justSetFeeToNonNative || previousCurrencyRemainsNonNative) { + setFeeCurrency(hydraDxAssetIdConverter.systemAssetId) + } + } + + private fun ExtrinsicBuilder.linkCode(referralCode: String) { + call( + moduleName = Modules.REFERRALS, + callName = "link_code", + arguments = mapOf( + "code" to referralCode.encodeToByteArray() + ) + ) + } + + private fun ExtrinsicBuilder.setFeeCurrency(onChainId: HydraDxAssetId) { + call( + moduleName = Modules.MULTI_TRANSACTION_PAYMENT, + callName = "set_currency", + arguments = mapOf( + "currency" to onChainId + ) + ) + } + + private suspend fun getPaymentCurrencyToSetIfNeeded(expectedPaymentAsset: Chain.Asset, currentFeeTokenId: HydraDxAssetId): HydraDxAssetId? { + val expectedPaymentTokenId = hydraDxAssetIdConverter.toOnChainIdOrThrow(expectedPaymentAsset) + + return expectedPaymentTokenId.takeIf { currentFeeTokenId != expectedPaymentTokenId } + } + + private suspend fun convertNativeFeeToAssetFee( + nativeFeeAmount: Balance, + targetAsset: Chain.Asset + ): Balance { + val args = ParentQuoterArgs( + chainAssetIn = targetAsset, + chainAssetOut = chain.utilityAsset, + amount = nativeFeeAmount, + swapDirection = SwapDirection.SPECIFIED_OUT + ) + + val quotedFee = parentQuoter.quote(args) + + // TODO + // There is a issue in Router implementation in Hydra that doesn't allow asset balance to go below ED. We add it to fee for simplicity instead + // of refactoring SwapExistentialDepositAwareMaxActionProvider + // This should be removed once Router issue is fixed + val existentialDeposit = assetSourceRegistry.existentialDepositInPlanks(chain, targetAsset) + + return quotedFee + existentialDeposit + } } } @@ -460,12 +430,3 @@ private class HydraDxSwapTransactionSegment( val edge: HydraDxSourceEdge, val segmentArgs: AtomicSwapOperationArgs, ) - -private class HydraDxSwapEdge( - override val from: FullChainAssetId, - val sourceId: HydraDxSwapSourceId, - val direction: HydraSwapDirection -) : Edge { - - override val to: FullChainAssetId = direction.to -} diff --git a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/hydraDx/HydraDxSwapSource.kt b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/hydraDx/HydraDxSwapSource.kt index 39aad751b9..61c7e42647 100644 --- a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/hydraDx/HydraDxSwapSource.kt +++ b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/hydraDx/HydraDxSwapSource.kt @@ -4,10 +4,7 @@ import io.novafoundation.nova.common.utils.Identifiable import io.novafoundation.nova.core.updater.SharedRequestsBuilder import io.novafoundation.nova.feature_swap_api.domain.model.AtomicSwapOperationArgs import io.novafoundation.nova.feature_swap_api.domain.model.QuotableEdge -import io.novafoundation.nova.feature_swap_api.domain.model.SwapDirection -import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.types.Balance import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain -import io.novafoundation.nova.runtime.multiNetwork.chain.model.FullChainAssetId import io.novasama.substrate_sdk_android.runtime.AccountId import io.novasama.substrate_sdk_android.runtime.definitions.types.composite.DictEnum import io.novasama.substrate_sdk_android.runtime.extrinsic.ExtrinsicBuilder @@ -28,7 +25,7 @@ interface HydraDxSourceEdge : QuotableEdge { interface HydraDxSwapSource : Identifiable { - suspend fun availableSwapDirections(): Collection + suspend fun availableSwapDirections(): Collection suspend fun runSubscriptions( userAccountId: AccountId, @@ -40,20 +37,3 @@ interface HydraDxSwapSource : Identifiable { fun create(chain: Chain): HydraDxSwapSource } } - -data class HydraDxSwapSourceQuoteArgs( - val chainAssetIn: Chain.Asset, - val chainAssetOut: Chain.Asset, - val amount: Balance, - val swapDirection: SwapDirection, - val params: Map -) - -interface HydraSwapDirection { - - val from: FullChainAssetId - - val to: FullChainAssetId - - val params: Map -} diff --git a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/hydraDx/omnipool/OmniPoolSwapSource.kt b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/hydraDx/omnipool/OmniPoolSwapSource.kt index 83020a377a..84281f0f82 100644 --- a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/hydraDx/omnipool/OmniPoolSwapSource.kt +++ b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/hydraDx/omnipool/OmniPoolSwapSource.kt @@ -9,11 +9,12 @@ import io.novafoundation.nova.common.utils.padEnd import io.novafoundation.nova.common.utils.singleReplaySharedFlow import io.novafoundation.nova.common.utils.toMultiSubscription import io.novafoundation.nova.core.updater.SharedRequestsBuilder -import io.novafoundation.nova.feature_swap_api.domain.model.QuotableEdge +import io.novafoundation.nova.feature_swap_api.domain.model.AtomicSwapOperationArgs import io.novafoundation.nova.feature_swap_api.domain.model.SwapDirection -import io.novafoundation.nova.feature_swap_api.domain.model.SwapExecuteArgs import io.novafoundation.nova.feature_swap_api.domain.model.SwapLimit import io.novafoundation.nova.feature_swap_api.domain.model.SwapQuoteException +import io.novafoundation.nova.feature_swap_impl.data.assetExchange.hydraDx.HydraDxSourceEdge +import io.novafoundation.nova.feature_swap_impl.data.assetExchange.hydraDx.HydraDxStandaloneSwapBuilder import io.novafoundation.nova.feature_swap_impl.data.assetExchange.hydraDx.HydraDxSwapSource import io.novafoundation.nova.feature_swap_impl.data.assetExchange.hydraDx.HydraDxSwapSourceId import io.novafoundation.nova.feature_swap_impl.data.assetExchange.hydraDx.omnipool.model.DynamicFee @@ -23,11 +24,9 @@ import io.novafoundation.nova.feature_swap_impl.data.assetExchange.hydraDx.omnip import io.novafoundation.nova.feature_swap_impl.data.assetExchange.hydraDx.omnipool.model.OmnipoolAssetState import io.novafoundation.nova.feature_swap_impl.data.assetExchange.hydraDx.omnipool.model.feeParamsConstant import io.novafoundation.nova.feature_swap_impl.data.assetExchange.hydraDx.omnipool.model.quote -import io.novafoundation.nova.feature_swap_impl.domain.swap.QuotableEdge import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.AssetSourceRegistry import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.HydraDxAssetId import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.HydraDxAssetIdConverter -import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.toOnChainIdOrThrow import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.types.Balance import io.novafoundation.nova.runtime.ext.fullId import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry @@ -86,7 +85,7 @@ private class OmniPoolSwapSource( private val omniPoolFlow: MutableSharedFlow = singleReplaySharedFlow() - override suspend fun availableSwapDirections(): List { + override suspend fun availableSwapDirections(): Collection { val pooledOnChainAssetIds = getPooledOnChainAssetIds() val pooledChainAssetsIds = matchKnownChainAssetIds(pooledOnChainAssetIds) @@ -104,26 +103,6 @@ private class OmniPoolSwapSource( } } - override suspend fun ExtrinsicBuilder.executeSwap(args: SwapExecuteArgs) { - val assetIdIn = hydraDxAssetIdConverter.toOnChainIdOrThrow(args.assetIn) - val assetIdOut = hydraDxAssetIdConverter.toOnChainIdOrThrow(args.assetOut) - - when (val limit = args.swapLimit) { - is SwapLimit.SpecifiedIn -> sell( - assetIdIn = assetIdIn, - assetIdOut = assetIdOut, - amountIn = limit.expectedAmountIn, - minBuyAmount = limit.amountOutMin - ) - is SwapLimit.SpecifiedOut -> buy( - assetIdIn = assetIdIn, - assetIdOut = assetIdOut, - amountOut = limit.expectedAmountOut, - maxSellAmount = limit.amountInMax - ) - } - } - @OptIn(ExperimentalCoroutinesApi::class) override suspend fun runSubscriptions( userAccountId: AccountId, @@ -169,10 +148,6 @@ private class OmniPoolSwapSource( .map { } } - override fun routerPoolTypeFor(params: Map): DictEnum.Entry<*> { - return DictEnum.Entry("Omnipool", null) - } - private suspend fun getPooledOnChainAssetIds(): List { return remoteStorageSource.query(chain.id) { val hubAssetId = metadata.omnipool().numberConstant("HubAssetId", runtime) @@ -266,12 +241,20 @@ private class OmniPoolSwapSource( private inner class OmniPoolSwapEdge( private val fromAsset: RemoteIdAndLocalAsset, - private val toAsset: RemoteIdAndLocalAsset - ) : QuotableEdge { + private val toAsset: RemoteIdAndLocalAsset, + ) : HydraDxSourceEdge { override val from: FullChainAssetId = fromAsset.second.fullId - override val to: FullChainAssetId = fromAsset.second.fullId + override val to: FullChainAssetId = toAsset.second.fullId + + override fun routerPoolArgument(): DictEnum.Entry<*> { + return DictEnum.Entry("Omnipool", null) + } + + override val standaloneSwapBuilder: HydraDxStandaloneSwapBuilder = { + executeSwap(it) + } override suspend fun quote(amount: Balance, direction: SwapDirection): Balance { val omniPool = omniPoolFlow.first() @@ -279,6 +262,26 @@ private class OmniPoolSwapSource( return omniPool.quote(fromAsset.first, toAsset.first, amount, direction) ?: throw SwapQuoteException.NotEnoughLiquidity } + + private fun ExtrinsicBuilder.executeSwap(args: AtomicSwapOperationArgs) { + val assetIdIn = fromAsset.first + val assetIdOut = toAsset.first + + when (val limit = args.swapLimit) { + is SwapLimit.SpecifiedIn -> sell( + assetIdIn = assetIdIn, + assetIdOut = assetIdOut, + amountIn = limit.amountIn, + minBuyAmount = limit.amountOutMin + ) + is SwapLimit.SpecifiedOut -> buy( + assetIdIn = assetIdIn, + assetIdOut = assetIdOut, + amountOut = limit.amountOut, + maxSellAmount = limit.amountInMax + ) + } + } } } @@ -288,6 +291,12 @@ fun omniPoolAccountId(): AccountId { typealias RemoteAndLocalId = Pair typealias RemoteIdAndLocalAsset = Pair +typealias RemoteAndLocalIdOptional = Pair + +@Suppress("UNCHECKED_CAST") +fun RemoteAndLocalIdOptional.flatten(): RemoteAndLocalId? { + return second?.let { this as RemoteAndLocalId } +} val RemoteAndLocalId.remoteId get() = first @@ -295,4 +304,3 @@ val RemoteAndLocalId.remoteId val RemoteAndLocalId.localId get() = second -typealias RemoteAndLocalIdOptional = Pair diff --git a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/hydraDx/stableswap/StableSwapSource.kt b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/hydraDx/stableswap/StableSwapSource.kt index 0d91610ca0..1f03219f80 100644 --- a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/hydraDx/stableswap/StableSwapSource.kt +++ b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/hydraDx/stableswap/StableSwapSource.kt @@ -3,21 +3,20 @@ package io.novafoundation.nova.feature_swap_impl.data.assetExchange.hydraDx.stab import com.google.gson.Gson import io.novafoundation.nova.common.data.network.runtime.binding.BlockNumber import io.novafoundation.nova.common.data.network.runtime.binding.orEmpty -import io.novafoundation.nova.common.utils.MultiMapList import io.novafoundation.nova.common.utils.filterNotNull import io.novafoundation.nova.common.utils.graph.Edge -import io.novafoundation.nova.common.utils.graph.Graph -import io.novafoundation.nova.common.utils.graph.create import io.novafoundation.nova.common.utils.orZero import io.novafoundation.nova.common.utils.singleReplaySharedFlow import io.novafoundation.nova.common.utils.toMultiSubscription import io.novafoundation.nova.core.updater.SharedRequestsBuilder -import io.novafoundation.nova.feature_swap_api.domain.model.SwapExecuteArgs +import io.novafoundation.nova.feature_swap_api.domain.model.SwapDirection import io.novafoundation.nova.feature_swap_api.domain.model.SwapQuoteException +import io.novafoundation.nova.feature_swap_impl.data.assetExchange.hydraDx.HydraDxSourceEdge +import io.novafoundation.nova.feature_swap_impl.data.assetExchange.hydraDx.HydraDxStandaloneSwapBuilder import io.novafoundation.nova.feature_swap_impl.data.assetExchange.hydraDx.HydraDxSwapSource -import io.novafoundation.nova.feature_swap_impl.data.assetExchange.hydraDx.HydraDxSwapSourceQuoteArgs -import io.novafoundation.nova.feature_swap_impl.data.assetExchange.hydraDx.HydraSwapDirection +import io.novafoundation.nova.feature_swap_impl.data.assetExchange.hydraDx.omnipool.RemoteAndLocalId import io.novafoundation.nova.feature_swap_impl.data.assetExchange.hydraDx.omnipool.RemoteAndLocalIdOptional +import io.novafoundation.nova.feature_swap_impl.data.assetExchange.hydraDx.omnipool.flatten import io.novafoundation.nova.feature_swap_impl.data.assetExchange.hydraDx.omnipool.omniPoolAccountId import io.novafoundation.nova.feature_swap_impl.data.assetExchange.hydraDx.stableswap.model.StablePool import io.novafoundation.nova.feature_swap_impl.data.assetExchange.hydraDx.stableswap.model.StablePoolAsset @@ -25,7 +24,6 @@ import io.novafoundation.nova.feature_swap_impl.data.assetExchange.hydraDx.stabl import io.novafoundation.nova.feature_swap_impl.data.assetExchange.hydraDx.stableswap.model.quote import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.HydraDxAssetId import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.HydraDxAssetIdConverter -import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.toOnChainIdOrThrow import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.types.Balance import io.novafoundation.nova.feature_wallet_api.domain.model.Asset import io.novafoundation.nova.feature_wallet_api.domain.model.Asset.Companion.calculateTransferable @@ -39,7 +37,6 @@ import io.novasama.substrate_sdk_android.encrypt.json.asLittleEndianBytes import io.novasama.substrate_sdk_android.hash.Hasher.blake2b256 import io.novasama.substrate_sdk_android.runtime.AccountId import io.novasama.substrate_sdk_android.runtime.definitions.types.composite.DictEnum -import io.novasama.substrate_sdk_android.runtime.extrinsic.ExtrinsicBuilder import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Deferred import kotlinx.coroutines.ExperimentalCoroutinesApi @@ -52,8 +49,6 @@ import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onEach -private const val POOL_ID_PARAM_KEY = "PoolId" - class StableSwapSourceFactory( private val remoteStorageSource: StorageDataSource, private val hydraDxAssetIdConverter: HydraDxAssetIdConverter, @@ -62,6 +57,7 @@ class StableSwapSourceFactory( ) : HydraDxSwapSource.Factory { companion object { + const val ID = "StableSwap" } @@ -90,7 +86,7 @@ private class StableSwapSource( private val stablePools: MutableSharedFlow> = singleReplaySharedFlow() - override suspend fun availableSwapDirections(): MultiMapList { + override suspend fun availableSwapDirections(): Collection { val pools = getPools() val poolInitialInfo = pools.matchIdsWithLocal() @@ -99,22 +95,6 @@ private class StableSwapSource( return poolInitialInfo.allPossibleDirections() } - override suspend fun ExtrinsicBuilder.executeSwap(args: SwapExecuteArgs) { - // We don't need a specific implementation for StableSwap extrinsics since it is done by HydraDxExchange on the upper level via Router - } - - override suspend fun quote(args: HydraDxSwapSourceQuoteArgs): Balance { - val allPools = stablePools.first() - val poolId = args.params.poolIdParam() - val relevantPool = allPools.first { it.sharedAsset.id == poolId } - - val hydraDxAssetIdIn = hydraDxAssetIdConverter.toOnChainIdOrThrow(args.chainAssetIn) - val hydraDxAssetIdOut = hydraDxAssetIdConverter.toOnChainIdOrThrow(args.chainAssetOut) - - return relevantPool.quote(hydraDxAssetIdIn, hydraDxAssetIdOut, args.amount, args.swapDirection) - ?: throw SwapQuoteException.NotEnoughLiquidity - } - @OptIn(ExperimentalCoroutinesApi::class) override suspend fun runSubscriptions( userAccountId: AccountId, @@ -240,11 +220,6 @@ private class StableSwapSource( return (prefix + suffix).blake2b256() } - override fun routerPoolTypeFor(params: Map): DictEnum.Entry<*> { - val poolId = params.getValue(POOL_ID_PARAM_KEY).toBigInteger() - - return DictEnum.Entry("Stableswap", poolId) - } private suspend fun getPools(): Map { return remoteStorageSource.query(chain.id) { @@ -271,42 +246,50 @@ private class StableSwapSource( } } - private fun List.allPossibleDirections(): MultiMapList { - val perPoolMaps = map { (poolAssetId, poolAssets) -> + private fun List.allPossibleDirections(): Collection { + return flatMap { (poolAssetId, poolAssets) -> val allPoolAssetIds = buildList { - addAll(poolAssets.mapNotNull { it.second }) + addAll(poolAssets.mapNotNull { it.flatten() }) - val sharedAssetId = poolAssetId.second + val sharedAssetId = poolAssetId.flatten() if (sharedAssetId != null) { add(sharedAssetId) } } - allPoolAssetIds.associateWith { assetId -> + allPoolAssetIds.flatMap { assetId -> allPoolAssetIds.mapNotNull { otherAssetId -> otherAssetId.takeIf { assetId != otherAssetId } - ?.let { StableSwapDirection(assetId, otherAssetId, poolAssetId.first) } + ?.let { StableSwapEdge(assetId, otherAssetId, poolAssetId.first) } } } } - - return Graph.create(perPoolMaps).adjacencyList } - private fun Map.poolIdParam(): HydraDxAssetId { - return getValue(POOL_ID_PARAM_KEY).toBigInteger() - } + inner class StableSwapEdge( + private val fromAsset: RemoteAndLocalId, + private val toAsset: RemoteAndLocalId, + private val poolId: HydraDxAssetId + ) : HydraDxSourceEdge, Edge { + + override val from: FullChainAssetId = fromAsset.second + + override val to: FullChainAssetId = toAsset.second - private class StableSwapDirection( - override val from: FullChainAssetId, - override val to: FullChainAssetId, - poolId: HydraDxAssetId - ) : HydraSwapDirection, Edge { - val poolIdRaw = poolId.toString() + override val standaloneSwapBuilder: HydraDxStandaloneSwapBuilder? = null - override val params: Map - get() = mapOf(POOL_ID_PARAM_KEY to poolIdRaw) + override fun routerPoolArgument(): DictEnum.Entry<*> { + return DictEnum.Entry("Stableswap", poolId) + } + + override suspend fun quote(amount: Balance, direction: SwapDirection): Balance { + val allPools = stablePools.first() + val relevantPool = allPools.first { it.sharedAsset.id == poolId } + + return relevantPool.quote(fromAsset.first, toAsset.first, amount, direction) + ?: throw SwapQuoteException.NotEnoughLiquidity + } } private data class PoolInitialInfo( diff --git a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/hydraDx/xyk/XYKSwapSource.kt b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/hydraDx/xyk/XYKSwapSource.kt index 8c6f172318..1f461eef18 100644 --- a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/hydraDx/xyk/XYKSwapSource.kt +++ b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/hydraDx/xyk/XYKSwapSource.kt @@ -1,18 +1,15 @@ package io.novafoundation.nova.feature_swap_impl.data.assetExchange.hydraDx.xyk import io.novafoundation.nova.common.address.AccountIdKey -import io.novafoundation.nova.common.utils.MultiMapList import io.novafoundation.nova.common.utils.combine -import io.novafoundation.nova.common.utils.graph.Edge -import io.novafoundation.nova.common.utils.graph.GraphBuilder import io.novafoundation.nova.common.utils.singleReplaySharedFlow import io.novafoundation.nova.common.utils.xyk import io.novafoundation.nova.core.updater.SharedRequestsBuilder -import io.novafoundation.nova.feature_swap_api.domain.model.SwapExecuteArgs +import io.novafoundation.nova.feature_swap_api.domain.model.SwapDirection import io.novafoundation.nova.feature_swap_api.domain.model.SwapQuoteException +import io.novafoundation.nova.feature_swap_impl.data.assetExchange.hydraDx.HydraDxSourceEdge +import io.novafoundation.nova.feature_swap_impl.data.assetExchange.hydraDx.HydraDxStandaloneSwapBuilder import io.novafoundation.nova.feature_swap_impl.data.assetExchange.hydraDx.HydraDxSwapSource -import io.novafoundation.nova.feature_swap_impl.data.assetExchange.hydraDx.HydraDxSwapSourceQuoteArgs -import io.novafoundation.nova.feature_swap_impl.data.assetExchange.hydraDx.HydraSwapDirection import io.novafoundation.nova.feature_swap_impl.data.assetExchange.hydraDx.omnipool.RemoteAndLocalId import io.novafoundation.nova.feature_swap_impl.data.assetExchange.hydraDx.omnipool.localId import io.novafoundation.nova.feature_swap_impl.data.assetExchange.hydraDx.xyk.model.XYKPool @@ -23,17 +20,13 @@ import io.novafoundation.nova.feature_swap_impl.data.assetExchange.hydraDx.xyk.m import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.AssetSourceRegistry import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.HydraDxAssetId import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.HydraDxAssetIdConverter -import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.toOnChainIdOrThrow import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.types.Balance import io.novafoundation.nova.runtime.ext.fullId import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain import io.novafoundation.nova.runtime.multiNetwork.chain.model.FullChainAssetId import io.novafoundation.nova.runtime.storage.source.StorageDataSource -import io.novasama.substrate_sdk_android.extensions.fromHex -import io.novasama.substrate_sdk_android.extensions.toHexString import io.novasama.substrate_sdk_android.runtime.AccountId import io.novasama.substrate_sdk_android.runtime.definitions.types.composite.DictEnum -import io.novasama.substrate_sdk_android.runtime.extrinsic.ExtrinsicBuilder import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.flow.Flow @@ -42,8 +35,6 @@ import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.map -private const val POOL_ID_PARAM_KEY = "PoolId" - class XYKSwapSourceFactory( private val remoteStorageSource: StorageDataSource, private val hydraDxAssetIdConverter: HydraDxAssetIdConverter, @@ -73,7 +64,7 @@ private class XYKSwapSource( private val xykPools: MutableSharedFlow = singleReplaySharedFlow() - override suspend fun availableSwapDirections(): MultiMapList { + override suspend fun availableSwapDirections(): Collection { val pools = getPools() val poolInitialInfo = pools.matchIdsWithLocal() @@ -82,21 +73,6 @@ private class XYKSwapSource( return poolInitialInfo.allPossibleDirections() } - override suspend fun ExtrinsicBuilder.executeSwap(args: SwapExecuteArgs) { - // We don't need a specific implementation for XYKSwap extrinsics since it is done by HydraDxExchange on the upper level via Router - } - - override suspend fun quote(args: HydraDxSwapSourceQuoteArgs): Balance { - val allPools = xykPools.first() - val poolAddress = args.params.poolAddressParam() - - val hydraDxAssetIdIn = hydraDxAssetIdConverter.toOnChainIdOrThrow(args.chainAssetIn) - val hydraDxAssetIdOut = hydraDxAssetIdConverter.toOnChainIdOrThrow(args.chainAssetOut) - - return allPools.quote(poolAddress, hydraDxAssetIdIn, hydraDxAssetIdOut, args.amount, args.swapDirection) - ?: throw SwapQuoteException.NotEnoughLiquidity - } - private suspend fun subscribeToBalance( assetId: RemoteAndLocalId, poolAddress: AccountId, @@ -140,10 +116,6 @@ private class XYKSwapSource( } } - override fun routerPoolTypeFor(params: Map): DictEnum.Entry<*> { - return DictEnum.Entry("XYK", null) - } - private suspend fun getPools(): Map { return remoteStorageSource.query(chain.id) { runtime.metadata.xykOrNull?.poolAssets?.entries().orEmpty() @@ -168,45 +140,50 @@ private class XYKSwapSource( } } - private fun List.allPossibleDirections(): MultiMapList { - val builder = GraphBuilder() - - onEach { poolInfo -> - builder.addEdge( - from = poolInfo.firstAsset.localId, - to = HYKSwapDirection( - from = poolInfo.firstAsset.localId, - to = poolInfo.secondAsset.localId, - poolAddress = poolInfo.poolAddress + private fun List.allPossibleDirections(): Collection { + return buildList { + this@allPossibleDirections.forEach { poolInfo -> + add( + HYKSwapDirection( + fromAsset = poolInfo.firstAsset, + toAsset = poolInfo.secondAsset, + poolAddress = poolInfo.poolAddress + ) ) - ) - builder.addEdge( - from = poolInfo.secondAsset.localId, - to = HYKSwapDirection( - from = poolInfo.secondAsset.localId, - to = poolInfo.firstAsset.localId, - poolAddress = poolInfo.poolAddress + + add( + HYKSwapDirection( + fromAsset = poolInfo.secondAsset, + toAsset = poolInfo.firstAsset, + poolAddress = poolInfo.poolAddress + ) ) - ) + } } - - return builder.build().adjacencyList } - private fun Map.poolAddressParam(): AccountId { - return getValue(POOL_ID_PARAM_KEY).fromHex() - } + inner class HYKSwapDirection( + private val fromAsset: RemoteAndLocalId, + private val toAsset: RemoteAndLocalId, + private val poolAddress: AccountId + ) : HydraDxSourceEdge { - private class HYKSwapDirection( - override val from: FullChainAssetId, - override val to: FullChainAssetId, - poolAddress: AccountId - ) : HydraSwapDirection, Edge { + override val from: FullChainAssetId = fromAsset.second - val poolAddressRaw = poolAddress.toHexString() + override val to: FullChainAssetId = toAsset.second - override val params: Map - get() = mapOf(POOL_ID_PARAM_KEY to poolAddressRaw) + override val standaloneSwapBuilder: HydraDxStandaloneSwapBuilder? = null + + override fun routerPoolArgument(): DictEnum.Entry<*> { + return DictEnum.Entry("XYK", null) + } + + override suspend fun quote(amount: Balance, direction: SwapDirection): Balance { + val allPools = xykPools.first() + + return allPools.quote(poolAddress, fromAsset.first, toAsset.first, amount, direction) + ?: throw SwapQuoteException.NotEnoughLiquidity + } } } diff --git a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/domain/swap/RealSwapService.kt b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/domain/swap/RealSwapService.kt index fc610aa327..744e08def5 100644 --- a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/domain/swap/RealSwapService.kt +++ b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/domain/swap/RealSwapService.kt @@ -44,6 +44,7 @@ import io.novafoundation.nova.feature_swap_api.domain.model.SwapQuoteException import io.novafoundation.nova.feature_swap_api.domain.swap.SwapService import io.novafoundation.nova.feature_swap_impl.BuildConfig import io.novafoundation.nova.feature_swap_impl.data.assetExchange.AssetExchange +import io.novafoundation.nova.feature_swap_impl.data.assetExchange.ParentQuoterArgs import io.novafoundation.nova.feature_swap_impl.data.assetExchange.assetConversion.AssetConversionExchangeFactory import io.novafoundation.nova.feature_swap_impl.data.assetExchange.hydraDx.HydraDxExchangeFactory import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.types.Balance @@ -154,7 +155,6 @@ internal class RealSwapService( val operationArgs = AtomicSwapOperationArgs( swapLimit = SwapLimit(direction, quotedEdge.quotedAmount, perSegmentSlippage, quotedEdge.quote), customFeeAsset = segmentExecuteArgs.customFeeAsset, - nativeAsset = segmentExecuteArgs.nativeAsset ) // Initial case - begin first operation @@ -183,30 +183,23 @@ internal class RealSwapService( args: SwapQuoteArgs, computationSharingScope: CoroutineScope ): SwapQuote { - val from = args.tokenIn.configuration.fullId - val to = args.tokenOut.configuration.fullId - - val paths = pathsFromCacheOrCompute(from, to, computationSharingScope) { - val graph = directionsGraph(computationSharingScope).first() - - graph.findDijkstraPathsBetween(from, to, limit = PATHS_LIMIT) - } - - val quotedPaths = paths.mapNotNull { path -> quotePath(path, args.amount, args.swapDirection) } - if (paths.isEmpty()) { - throw SwapQuoteException.NotEnoughLiquidity - } - - val bestPathQuote = quotedPaths.max() + val quotedTrade = quoteTrade( + chainAssetIn = args.tokenIn.configuration, + chainAssetOut = args.tokenOut.configuration, + amount = args.amount, + swapDirection = args.swapDirection, + computationSharingScope = computationSharingScope + ) - val (amountIn, amountOut) = args.inAndOutAmounts(bestPathQuote) + val amountIn = quotedTrade.amountIn() + val amountOut = quotedTrade.amountOut() return SwapQuote( amountIn = args.tokenIn.configuration.withAmount(amountIn), amountOut = args.tokenOut.configuration.withAmount(amountOut), direction = args.swapDirection, priceImpact = args.calculatePriceImpact(amountIn, amountOut), - path = bestPathQuote.path + path = quotedTrade.path ) } @@ -230,10 +223,24 @@ internal class RealSwapService( return calculatePriceImpact(fiatIn, fiatOut) } - private fun SwapQuoteArgs.inAndOutAmounts(quote: QuotedTrade): Pair { - return when (swapDirection) { - SwapDirection.SPECIFIED_IN -> amount to quote.lastSegmentQuote - SwapDirection.SPECIFIED_OUT -> quote.firstSegmentQuote to amount + private fun QuotedTrade.amountIn(): Balance { + return when (direction) { + SwapDirection.SPECIFIED_IN -> firstSegmentQuotedAmount + SwapDirection.SPECIFIED_OUT -> firstSegmentQuote + } + } + + private fun QuotedTrade.amountOut(): Balance { + return when (direction) { + SwapDirection.SPECIFIED_IN -> lastSegmentQuote + SwapDirection.SPECIFIED_OUT -> lastSegmentQuotedAmount + } + } + + private fun QuotedTrade.finalQuote(): Balance { + return when (direction) { + SwapDirection.SPECIFIED_IN -> lastSegmentQuote + SwapDirection.SPECIFIED_OUT -> firstSegmentQuote } } @@ -285,7 +292,7 @@ internal class RealSwapService( else -> null } - return factory?.create(chain, computationScope) + return factory?.create(chain, InnerParentQuoter(computationScope), computationScope) } // Assumes each flow will have only single element @@ -347,6 +354,45 @@ internal class RealSwapService( }.getOrNull() } + private suspend fun quoteTrade( + chainAssetIn: Chain.Asset, + chainAssetOut: Chain.Asset, + amount: Balance, + swapDirection: SwapDirection, + computationSharingScope: CoroutineScope + ): QuotedTrade { + val from = chainAssetIn.fullId + val to = chainAssetOut.fullId + + val paths = pathsFromCacheOrCompute(from, to, computationSharingScope) { + val graph = directionsGraph(computationSharingScope).first() + + graph.findDijkstraPathsBetween(from, to, limit = PATHS_LIMIT) + } + + val quotedPaths = paths.mapNotNull { path -> quotePath(path, amount, swapDirection) } + if (paths.isEmpty()) { + throw SwapQuoteException.NotEnoughLiquidity + } + + return quotedPaths.max() + } + + private inner class InnerParentQuoter( + private val computationScope: CoroutineScope + ) : AssetExchange.ParentQuoter { + + override suspend fun quote(quoteArgs: ParentQuoterArgs): Balance { + return quoteTrade( + chainAssetIn = quoteArgs.chainAssetIn, + chainAssetOut = quoteArgs.chainAssetOut, + amount = quoteArgs.amount, + swapDirection = quoteArgs.swapDirection, + computationSharingScope = computationScope + ).finalQuote() + } + } + // TOOD rework path logging // private suspend fun logQuotes(args: SwapQuoteArgs, quotes: List) { // val allCandidates = quotes.sortedDescending().map { @@ -431,8 +477,14 @@ private class QuotedTrade( } } +private val QuotedTrade.lastSegmentQuotedAmount: Balance + get() = path.last().quotedAmount + private val QuotedTrade.lastSegmentQuote: Balance get() = path.last().quote private val QuotedTrade.firstSegmentQuote: Balance get() = path.first().quote + +private val QuotedTrade.firstSegmentQuotedAmount: Balance + get() = path.first().quotedAmount From 11ac98ce1b9b4abd12957d2a55f98efb52829269 Mon Sep 17 00:00:00 2001 From: Valentun Date: Tue, 17 Sep 2024 11:57:58 +0300 Subject: [PATCH 07/83] Make the build work --- .../app/root/navigation/swap/SwapNavigator.kt | 9 +- .../nova/common/utils/graph/GraphBuilder.kt | 1 + .../presentation/pincode/di/PinCodeModule.kt | 6 +- .../domain/model/AtomicSwapOperation.kt | 4 +- .../domain/model/SwapQuote.kt | 37 +--- .../domain/model/SwapQuoteArgs.kt | 22 +-- .../AssetConversionExchange.kt | 52 ++---- .../assetExchange/hydraDx/HydraDxExchange.kt | 5 +- .../SwapTransactionHistoryRepository.kt | 39 ++-- .../feature_swap_impl/di/SwapFeatureModule.kt | 15 +- .../AssetConversionExchangeModule.kt | 2 - .../domain/interactor/SwapInteractor.kt | 118 ++++++------- .../domain/swap/RealSwapService.kt | 16 +- .../validation/SwapPayloadValidation.kt | 8 - .../validation/SwapValidationFailure.kt | 36 +--- .../utils/SharedQuoteValidationRetriever.kt | 4 +- .../SwapFeeSufficientBalanceValidation.kt | 19 +- .../validations/SwapRateChangesValidation.kt | 29 ++- .../SwapSmallRemainingBalanceValidation.kt | 21 +-- .../presentation/SwapRouter.kt | 3 +- .../presentation/common/state/SwapState.kt | 11 ++ .../common/state/SwapStateStore.kt | 29 +++ .../common/state/SwapStateStoreProvider.kt | 28 +++ .../confirmation/SwapConfirmationFragment.kt | 12 +- .../confirmation/SwapConfirmationViewModel.kt | 167 +++++++++--------- .../di/SwapConfirmationComponent.kt | 2 - .../confirmation/di/SwapConfirmationModule.kt | 49 +++-- .../payload/SwapConfirmationPayload.kt | 59 ------- .../SwapConfirmationPayloadFormatter.kt | 100 ----------- .../main/SwapMainSettingsFragment.kt | 3 - .../main/SwapMainSettingsViewModel.kt | 145 ++++++--------- .../main/SwapValidationFailureUi.kt | 56 ++---- .../main/di/SwapMainSettingsModule.kt | 8 +- .../layout/fragment_main_swap_settings.xml | 11 -- 34 files changed, 382 insertions(+), 744 deletions(-) create mode 100644 feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/common/state/SwapState.kt create mode 100644 feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/common/state/SwapStateStore.kt create mode 100644 feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/common/state/SwapStateStoreProvider.kt delete mode 100644 feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/confirmation/payload/SwapConfirmationPayload.kt delete mode 100644 feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/confirmation/payload/SwapConfirmationPayloadFormatter.kt diff --git a/app/src/main/java/io/novafoundation/nova/app/root/navigation/swap/SwapNavigator.kt b/app/src/main/java/io/novafoundation/nova/app/root/navigation/swap/SwapNavigator.kt index 99cbbb0e24..18edbd8244 100644 --- a/app/src/main/java/io/novafoundation/nova/app/root/navigation/swap/SwapNavigator.kt +++ b/app/src/main/java/io/novafoundation/nova/app/root/navigation/swap/SwapNavigator.kt @@ -4,13 +4,11 @@ import io.novafoundation.nova.app.R import io.novafoundation.nova.app.root.navigation.BaseNavigator import io.novafoundation.nova.app.root.navigation.NavigationHolder import io.novafoundation.nova.app.root.navigation.Navigator -import io.novafoundation.nova.feature_assets.presentation.send.amount.SendPayload import io.novafoundation.nova.feature_assets.presentation.balance.detail.BalanceDetailFragment +import io.novafoundation.nova.feature_assets.presentation.send.amount.SendPayload import io.novafoundation.nova.feature_assets.presentation.swap.AssetSwapFlowFragment import io.novafoundation.nova.feature_assets.presentation.swap.SwapFlowPayload import io.novafoundation.nova.feature_swap_impl.presentation.SwapRouter -import io.novafoundation.nova.feature_swap_impl.presentation.confirmation.SwapConfirmationFragment -import io.novafoundation.nova.feature_swap_impl.presentation.confirmation.payload.SwapConfirmationPayload import io.novafoundation.nova.feature_wallet_api.presentation.model.AssetPayload class SwapNavigator( @@ -18,10 +16,7 @@ class SwapNavigator( private val commonDelegate: Navigator ) : BaseNavigator(navigationHolder), SwapRouter { - override fun openSwapConfirmation(payload: SwapConfirmationPayload) { - val bundle = SwapConfirmationFragment.getBundle(payload) - navigationHolder.navController?.navigate(R.id.action_swapMainSettingsFragment_to_swapConfirmationFragment, bundle) - } + override fun openSwapConfirmation() = performNavigation(R.id.action_swapMainSettingsFragment_to_swapConfirmationFragment) override fun openSwapOptions() { navigationHolder.navController?.navigate(R.id.action_swapMainSettingsFragment_to_swapOptionsFragment) diff --git a/common/src/main/java/io/novafoundation/nova/common/utils/graph/GraphBuilder.kt b/common/src/main/java/io/novafoundation/nova/common/utils/graph/GraphBuilder.kt index 16063709b6..979dbbe1f3 100644 --- a/common/src/main/java/io/novafoundation/nova/common/utils/graph/GraphBuilder.kt +++ b/common/src/main/java/io/novafoundation/nova/common/utils/graph/GraphBuilder.kt @@ -41,6 +41,7 @@ fun > Graph.Companion.create(multiMaps: List>) }.build() } +@JvmName("createFromEdges") fun > Graph.Companion.create(edges: List): Graph { return build { edges.forEach { diff --git a/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/presentation/pincode/di/PinCodeModule.kt b/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/presentation/pincode/di/PinCodeModule.kt index e8ce03ffa7..72791afb6e 100644 --- a/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/presentation/pincode/di/PinCodeModule.kt +++ b/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/presentation/pincode/di/PinCodeModule.kt @@ -14,8 +14,10 @@ import io.novafoundation.nova.common.di.viewmodel.ViewModelModule import io.novafoundation.nova.common.io.MainThreadExecutor import io.novafoundation.nova.common.mixin.actionAwaitable.ActionAwaitableMixin import io.novafoundation.nova.common.resources.ResourceManager -import io.novafoundation.nova.common.sequrity.biometry.BiometricService import io.novafoundation.nova.common.sequrity.TwoFactorVerificationExecutor +import io.novafoundation.nova.common.sequrity.biometry.BiometricPromptFactory +import io.novafoundation.nova.common.sequrity.biometry.BiometricService +import io.novafoundation.nova.common.sequrity.biometry.BiometricServiceFactory import io.novafoundation.nova.common.utils.sequrity.BackgroundAccessObserver import io.novafoundation.nova.common.vibration.DeviceVibrator import io.novafoundation.nova.feature_account_api.domain.interfaces.AccountInteractor @@ -23,8 +25,6 @@ import io.novafoundation.nova.feature_account_impl.R import io.novafoundation.nova.feature_account_impl.presentation.AccountRouter import io.novafoundation.nova.feature_account_impl.presentation.pincode.PinCodeAction import io.novafoundation.nova.feature_account_impl.presentation.pincode.PinCodeViewModel -import io.novafoundation.nova.common.sequrity.biometry.BiometricPromptFactory -import io.novafoundation.nova.common.sequrity.biometry.BiometricServiceFactory @Module( includes = [ diff --git a/feature-swap-api/src/main/java/io/novafoundation/nova/feature_swap_api/domain/model/AtomicSwapOperation.kt b/feature-swap-api/src/main/java/io/novafoundation/nova/feature_swap_api/domain/model/AtomicSwapOperation.kt index d52c5be4aa..b5d31f0638 100644 --- a/feature-swap-api/src/main/java/io/novafoundation/nova/feature_swap_api/domain/model/AtomicSwapOperation.kt +++ b/feature-swap-api/src/main/java/io/novafoundation/nova/feature_swap_api/domain/model/AtomicSwapOperation.kt @@ -15,9 +15,7 @@ class AtomicSwapOperationArgs( val customFeeAsset: Chain.Asset?, ) -class AtomicSwapOperationFee( - networkFee: Fee, val minimumBalanceBuyIn: MinimumBalanceBuyIn -) : Fee by networkFee +typealias AtomicSwapOperationFee = Fee // TODO this will later be used to perform more accurate non-atomic swaps // So next segments can correct tx args based on outcome of previous segments diff --git a/feature-swap-api/src/main/java/io/novafoundation/nova/feature_swap_api/domain/model/SwapQuote.kt b/feature-swap-api/src/main/java/io/novafoundation/nova/feature_swap_api/domain/model/SwapQuote.kt index 9c81d896e0..6559b2a809 100644 --- a/feature-swap-api/src/main/java/io/novafoundation/nova/feature_swap_api/domain/model/SwapQuote.kt +++ b/feature-swap-api/src/main/java/io/novafoundation/nova/feature_swap_api/domain/model/SwapQuote.kt @@ -77,42 +77,7 @@ class SwapFee( // TODO handle multi-segment fee display override val networkFee: Fee get() = atomicOperationFees.first() - - // TODO handle multi-segment minimumBalanceBuyIn - val minimumBalanceBuyIn: MinimumBalanceBuyIn - get() = atomicOperationFees.first().minimumBalanceBuyIn } val SwapFee.totalDeductedPlanks: Balance - get() = networkFee.amountByRequestedAccount + minimumBalanceBuyIn.commissionAssetToSpendOnBuyIn - -sealed class MinimumBalanceBuyIn { - - class NeedsToBuyMinimumBalance( - val nativeAsset: Chain.Asset, - val nativeMinimumBalance: Balance, - val commissionAsset: Chain.Asset, - val commissionAssetToSpendOnBuyIn: Balance - ) : MinimumBalanceBuyIn() - - object NoBuyInNeeded : MinimumBalanceBuyIn() -} - -val MinimumBalanceBuyIn.commissionAssetToSpendOnBuyIn: Balance - get() = when (this) { - is MinimumBalanceBuyIn.NeedsToBuyMinimumBalance -> commissionAssetToSpendOnBuyIn - MinimumBalanceBuyIn.NoBuyInNeeded -> Balance.ZERO - } - -fun MinimumBalanceBuyIn.requireNativeAsset(): Chain.Asset { - return when (this) { - is MinimumBalanceBuyIn.NeedsToBuyMinimumBalance -> nativeAsset - MinimumBalanceBuyIn.NoBuyInNeeded -> throw IllegalStateException("No buy-in needed") - } -} - -val MinimumBalanceBuyIn.nativeMinimumBalance: Balance - get() = when (this) { - is MinimumBalanceBuyIn.NeedsToBuyMinimumBalance -> nativeMinimumBalance - MinimumBalanceBuyIn.NoBuyInNeeded -> Balance.ZERO - } + get() = networkFee.amountByRequestedAccount diff --git a/feature-swap-api/src/main/java/io/novafoundation/nova/feature_swap_api/domain/model/SwapQuoteArgs.kt b/feature-swap-api/src/main/java/io/novafoundation/nova/feature_swap_api/domain/model/SwapQuoteArgs.kt index 998b4a8419..9efda6e0db 100644 --- a/feature-swap-api/src/main/java/io/novafoundation/nova/feature_swap_api/domain/model/SwapQuoteArgs.kt +++ b/feature-swap-api/src/main/java/io/novafoundation/nova/feature_swap_api/domain/model/SwapQuoteArgs.kt @@ -4,9 +4,7 @@ import io.novafoundation.nova.common.utils.Percent import io.novafoundation.nova.common.utils.fraction import io.novafoundation.nova.common.utils.graph.Path import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.types.Balance -import io.novafoundation.nova.feature_wallet_api.domain.model.Asset import io.novafoundation.nova.feature_wallet_api.domain.model.Token -import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain import java.math.BigDecimal data class SwapQuoteArgs( @@ -14,7 +12,6 @@ data class SwapQuoteArgs( val tokenOut: Token, val amount: Balance, val swapDirection: SwapDirection, - val slippage: Percent, ) class SwapExecuteArgs( @@ -25,12 +22,8 @@ class SwapExecuteArgs( class SegmentExecuteArgs( val quotedSwapEdge: QuotedSwapEdge, - val customFeeAsset: Chain.Asset?, ) -val SwapExecuteArgs.feeAsset: Chain.Asset - get() = customFeeAsset ?: assetIn - sealed class SwapLimit { class SpecifiedIn( @@ -46,21 +39,14 @@ sealed class SwapLimit { ) : SwapLimit() } -fun SwapQuoteArgs.toExecuteArgs(quote: SwapQuote, customFeeAsset: Chain.Asset?, nativeAsset: Asset): SwapExecuteArgs { +fun SwapQuote.toExecuteArgs(slippage: Percent): SwapExecuteArgs { return SwapExecuteArgs( - assetIn = tokenIn.configuration, - assetOut = tokenOut.configuration, - swapLimit = swapLimits(quote.quotedBalance), - customFeeAsset = customFeeAsset, - nativeAsset = nativeAsset, - path = quote.path + slippage = slippage, + direction = direction, + executionPath = path.map { quotedSwapEdge -> SegmentExecuteArgs(quotedSwapEdge) } ) } -fun SwapQuoteArgs.swapLimits(quotedBalance: Balance): SwapLimit { - return SwapLimit(swapDirection, amount, slippage, quotedBalance) -} - fun SwapLimit(direction: SwapDirection, amount: Balance, slippage: Percent, quotedBalance: Balance): SwapLimit { return when (direction) { SwapDirection.SPECIFIED_IN -> SpecifiedIn(amount, slippage, quotedBalance) diff --git a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/assetConversion/AssetConversionExchange.kt b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/assetConversion/AssetConversionExchange.kt index d6a920c841..f2f4ae5879 100644 --- a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/assetConversion/AssetConversionExchange.kt +++ b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/assetConversion/AssetConversionExchange.kt @@ -5,13 +5,13 @@ import io.novafoundation.nova.common.utils.Modules import io.novafoundation.nova.common.utils.assetConversion 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_account_api.data.extrinsic.awaitInBlock 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.domain.model.MetaAccount import io.novafoundation.nova.feature_swap_api.domain.model.AtomicSwapOperation import io.novafoundation.nova.feature_swap_api.domain.model.AtomicSwapOperationArgs import io.novafoundation.nova.feature_swap_api.domain.model.AtomicSwapOperationFee -import io.novafoundation.nova.feature_swap_api.domain.model.MinimumBalanceBuyIn import io.novafoundation.nova.feature_swap_api.domain.model.ReQuoteTrigger import io.novafoundation.nova.feature_swap_api.domain.model.SlippageConfig import io.novafoundation.nova.feature_swap_api.domain.model.SwapDirection @@ -21,9 +21,7 @@ import io.novafoundation.nova.feature_swap_api.domain.model.SwapLimit import io.novafoundation.nova.feature_swap_api.domain.model.SwapQuoteException import io.novafoundation.nova.feature_swap_impl.data.assetExchange.AssetExchange import io.novafoundation.nova.feature_swap_impl.domain.swap.BaseSwapGraphEdge -import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.AssetSourceRegistry import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.types.Balance -import io.novafoundation.nova.feature_wallet_api.domain.model.Asset import io.novafoundation.nova.runtime.call.MultiChainRuntimeCallsApi import io.novafoundation.nova.runtime.call.RuntimeCallsApi import io.novafoundation.nova.runtime.ext.commissionAsset @@ -55,11 +53,14 @@ class AssetConversionExchangeFactory( private val remoteStorageSource: StorageDataSource, private val runtimeCallsApi: MultiChainRuntimeCallsApi, private val extrinsicService: ExtrinsicService, - private val assetSourceRegistry: AssetSourceRegistry, private val chainStateRepository: ChainStateRepository, ) : AssetExchange.Factory { - override suspend fun create(chain: Chain, coroutineScope: CoroutineScope): AssetExchange { + override suspend fun create( + chain: Chain, + parentQuoter: AssetExchange.ParentQuoter, + coroutineScope: CoroutineScope + ): AssetExchange { val converter = multiLocationConverterFactory.default(chain, coroutineScope) return AssetConversionExchange( @@ -68,21 +69,17 @@ class AssetConversionExchangeFactory( remoteStorageSource = remoteStorageSource, multiChainRuntimeCallsApi = runtimeCallsApi, extrinsicService = extrinsicService, - assetSourceRegistry = assetSourceRegistry, chainStateRepository = chainStateRepository ) } } -private const val SOURCE_ID = "AssetConversion" - private class AssetConversionExchange( private val chain: Chain, private val multiLocationConverter: MultiLocationConverter, private val remoteStorageSource: StorageDataSource, private val multiChainRuntimeCallsApi: MultiChainRuntimeCallsApi, private val extrinsicService: ExtrinsicService, - private val assetSourceRegistry: AssetSourceRegistry, private val chainStateRepository: ChainStateRepository, ) : AssetExchange { @@ -190,7 +187,7 @@ private class AssetConversionExchange( private val transactionArgs: AtomicSwapOperationArgs, private val fromAsset: Chain.Asset, private val toAsset: Chain.Asset - ): AtomicSwapOperation { + ) : AtomicSwapOperation { override suspend fun estimateFee(): AtomicSwapOperationFee { val nativeAssetFee = extrinsicService.estimateFee(chain, TransactionOrigin.SelectedWallet) { @@ -206,6 +203,8 @@ private class AssetConversionExchange( return extrinsicService.submitAndWatchExtrinsic(chain, TransactionOrigin.SelectedWallet) { submissionOrigin -> // Send swapped funds to the requested origin since it the account doing the swap executeSwap(sendTo = submissionOrigin.requestedOrigin) + }.awaitInBlock().map { + SwapExecutionCorrection() } } @@ -213,9 +212,9 @@ private class AssetConversionExchange( val customFeeAsset = transactionArgs.customFeeAsset return if (customFeeAsset != null && !customFeeAsset.isCommissionAsset()) { - calculateCustomTokenFee(nativeTokenFee, transactionArgs.nativeAsset, customFeeAsset) + calculateCustomTokenFee(nativeTokenFee, customFeeAsset) } else { - AtomicSwapOperationFee(nativeTokenFee, MinimumBalanceBuyIn.NoBuyInNeeded) + nativeTokenFee } } @@ -267,39 +266,12 @@ private class AssetConversionExchange( // We should adapt it if we decide to remove the restriction private suspend fun calculateCustomTokenFee( nativeTokenFee: Fee, - nativeAsset: Asset, customFeeAsset: Chain.Asset ): AtomicSwapOperationFee { - val nativeChainAsset = nativeAsset.token.configuration val runtimeCallsApi = multiChainRuntimeCallsApi.forChain(chain.id) - val assetBalances = assetSourceRegistry.sourceFor(nativeChainAsset).balance - - val minimumBalance = assetBalances.existentialDeposit(chain, nativeChainAsset) - // https://github.com/paritytech/polkadot-sdk/blob/39c04fdd9622792ba8478b1c1c300417943a034b/substrate/frame/transaction-payment/asset-conversion-tx-payment/src/payment.rs#L114 - val shouldBuyMinimumBalance = nativeAsset.balanceCountedTowardsEDInPlanks < minimumBalance + nativeTokenFee.amount - val toBuyNativeFee = runtimeCallsApi.quoteFeeConversion(nativeTokenFee.amount, customFeeAsset) - val minimumBalanceBuyIn = if (shouldBuyMinimumBalance) { - val totalConverted = nativeTokenFee.amount + minimumBalance - - val forFeesAndMinBalance = runtimeCallsApi.quoteFeeConversion(totalConverted, customFeeAsset) - val forMinBalance = forFeesAndMinBalance - toBuyNativeFee - - MinimumBalanceBuyIn.NeedsToBuyMinimumBalance( - nativeAsset = nativeAsset.token.configuration, - nativeMinimumBalance = minimumBalance, - commissionAsset = customFeeAsset, - commissionAssetToSpendOnBuyIn = forMinBalance - ) - } else { - MinimumBalanceBuyIn.NoBuyInNeeded - } - - return AtomicSwapOperationFee( - networkFee = SubstrateFee(toBuyNativeFee, nativeTokenFee.submissionOrigin), - minimumBalanceBuyIn = minimumBalanceBuyIn - ) + return SubstrateFee(toBuyNativeFee, nativeTokenFee.submissionOrigin) } private suspend fun RuntimeCallsApi.quoteFeeConversion(commissionAmountOut: Balance, customFeeToken: Chain.Asset): Balance { diff --git a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/hydraDx/HydraDxExchange.kt b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/hydraDx/HydraDxExchange.kt index 3562ea6676..b156bf8ded 100644 --- a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/hydraDx/HydraDxExchange.kt +++ b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/hydraDx/HydraDxExchange.kt @@ -15,7 +15,6 @@ import io.novafoundation.nova.feature_account_api.domain.model.requireAccountIdI import io.novafoundation.nova.feature_swap_api.domain.model.AtomicSwapOperation import io.novafoundation.nova.feature_swap_api.domain.model.AtomicSwapOperationArgs import io.novafoundation.nova.feature_swap_api.domain.model.AtomicSwapOperationFee -import io.novafoundation.nova.feature_swap_api.domain.model.MinimumBalanceBuyIn import io.novafoundation.nova.feature_swap_api.domain.model.QuotableEdge import io.novafoundation.nova.feature_swap_api.domain.model.ReQuoteTrigger import io.novafoundation.nova.feature_swap_api.domain.model.SlippageConfig @@ -231,12 +230,10 @@ private class HydraDxExchange( nativeFee.amount } - val feeInExpectedCurrency = SubstrateFee( + return SubstrateFee( amount = feeAmountInExpectedCurrency, submissionOrigin = nativeFee.submissionOrigin ) - - return AtomicSwapOperationFee(networkFee = feeInExpectedCurrency, MinimumBalanceBuyIn.NoBuyInNeeded) } override suspend fun submit(previousStepCorrection: SwapExecutionCorrection?): Result { diff --git a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/repository/SwapTransactionHistoryRepository.kt b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/repository/SwapTransactionHistoryRepository.kt index ffac578b81..0cd2f77efd 100644 --- a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/repository/SwapTransactionHistoryRepository.kt +++ b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/repository/SwapTransactionHistoryRepository.kt @@ -1,15 +1,11 @@ package io.novafoundation.nova.feature_swap_impl.data.repository import io.novafoundation.nova.core_db.dao.OperationDao -import io.novafoundation.nova.core_db.model.operation.OperationBaseLocal -import io.novafoundation.nova.core_db.model.operation.OperationLocal import io.novafoundation.nova.core_db.model.operation.SwapTypeLocal.AssetWithAmount import io.novafoundation.nova.feature_account_api.data.extrinsic.ExtrinsicSubmission import io.novafoundation.nova.feature_swap_api.domain.model.SwapExecuteArgs import io.novafoundation.nova.feature_swap_api.domain.model.SwapFee -import io.novafoundation.nova.feature_swap_api.domain.model.feeAsset import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.types.Balance -import io.novafoundation.nova.runtime.ext.addressOf import io.novafoundation.nova.runtime.ext.localId import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain @@ -35,23 +31,24 @@ class RealSwapTransactionHistoryRepository( fee: SwapFee, txSubmission: ExtrinsicSubmission ) { - val chain = chainRegistry.getChain(chainAsset.chainId) - - val localOperation = with(swapArgs) { - OperationLocal.manualSwap( - hash = txSubmission.hash, - originAddress = chain.addressOf(txSubmission.submissionOrigin.requestedOrigin), - assetId = chainAsset.localId, - // Insert fee regardless of who actually paid it - fee = feeAsset.withAmountLocal(fee.networkFee.amount), - amountIn = assetIn.withAmountLocal(swapLimit.expectedAmountIn), - amountOut = assetOut.withAmountLocal(swapLimit.expectedAmountOut), - status = OperationBaseLocal.Status.PENDING, - source = OperationBaseLocal.Source.APP - ) - } - - operationDao.insert(localOperation) + // TODO swap history +// val chain = chainRegistry.getChain(chainAsset.chainId) +// +// val localOperation = with(swapArgs) { +// OperationLocal.manualSwap( +// hash = txSubmission.hash, +// originAddress = chain.addressOf(txSubmission.submissionOrigin.requestedOrigin), +// assetId = chainAsset.localId, +// // Insert fee regardless of who actually paid it +// fee = feeAsset.withAmountLocal(fee.networkFee.amount), +// amountIn = assetIn.withAmountLocal(swapLimit.expectedAmountIn), +// amountOut = assetOut.withAmountLocal(swapLimit.expectedAmountOut), +// status = OperationBaseLocal.Status.PENDING, +// source = OperationBaseLocal.Source.APP +// ) +// } +// +// operationDao.insert(localOperation) } private fun Chain.Asset.withAmountLocal(amount: Balance): AssetWithAmount { diff --git a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/di/SwapFeatureModule.kt b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/di/SwapFeatureModule.kt index ba07b99b04..db71c3e42f 100644 --- a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/di/SwapFeatureModule.kt +++ b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/di/SwapFeatureModule.kt @@ -27,7 +27,8 @@ import io.novafoundation.nova.feature_swap_impl.presentation.common.PriceImpactF import io.novafoundation.nova.feature_swap_impl.presentation.common.RealPriceImpactFormatter import io.novafoundation.nova.feature_swap_impl.presentation.common.RealSwapRateFormatter import io.novafoundation.nova.feature_swap_impl.presentation.common.SlippageAlertMixinFactory -import io.novafoundation.nova.feature_swap_impl.presentation.confirmation.payload.SwapConfirmationPayloadFormatter +import io.novafoundation.nova.feature_swap_impl.presentation.common.state.RealSwapStateStoreProvider +import io.novafoundation.nova.feature_swap_impl.presentation.common.state.SwapStateStoreProvider import io.novafoundation.nova.feature_swap_impl.presentation.mixin.maxAction.MaxActionProviderFactory import io.novafoundation.nova.feature_swap_impl.presentation.state.RealSwapSettingsStateProvider import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.AssetSourceRegistry @@ -58,12 +59,6 @@ class SwapFeatureModule { ) } - @Provides - @FeatureScope - fun provideSwapConfirmationPayloadFormatter(chainRegistry: ChainRegistry): SwapConfirmationPayloadFormatter { - return SwapConfirmationPayloadFormatter(chainRegistry) - } - @Provides @FeatureScope fun provideSwapAvailabilityInteractor(chainRegistry: ChainRegistry, swapService: SwapService): SwapAvailabilityInteractor { @@ -175,4 +170,10 @@ class SwapFeatureModule { chainRegistry = chainRegistry ) } + + @Provides + @FeatureScope + fun provideSwapQuoteStoreProvider(computationalCache: ComputationalCache): SwapStateStoreProvider { + return RealSwapStateStoreProvider(computationalCache) + } } diff --git a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/di/exchanges/AssetConversionExchangeModule.kt b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/di/exchanges/AssetConversionExchangeModule.kt index 65d7648c92..11c066c56c 100644 --- a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/di/exchanges/AssetConversionExchangeModule.kt +++ b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/di/exchanges/AssetConversionExchangeModule.kt @@ -22,7 +22,6 @@ class AssetConversionExchangeModule { @Named(REMOTE_STORAGE_SOURCE) remoteStorageSource: StorageDataSource, runtimeCallsApi: MultiChainRuntimeCallsApi, extrinsicService: ExtrinsicService, - assetSourceRegistry: AssetSourceRegistry, multiLocationConverterFactory: MultiLocationConverterFactory, chainStateRepository: ChainStateRepository ): AssetConversionExchangeFactory { @@ -31,7 +30,6 @@ class AssetConversionExchangeModule { remoteStorageSource = remoteStorageSource, runtimeCallsApi = runtimeCallsApi, extrinsicService = extrinsicService, - assetSourceRegistry = assetSourceRegistry, multiLocationConverterFactory = multiLocationConverterFactory ) } diff --git a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/domain/interactor/SwapInteractor.kt b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/domain/interactor/SwapInteractor.kt index fcdaa6989a..02b2dbe039 100644 --- a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/domain/interactor/SwapInteractor.kt +++ b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/domain/interactor/SwapInteractor.kt @@ -2,7 +2,6 @@ package io.novafoundation.nova.feature_swap_impl.domain.interactor import io.novafoundation.nova.common.validation.ValidationSystem import io.novafoundation.nova.core.updater.UpdateSystem -import io.novafoundation.nova.feature_account_api.data.extrinsic.ExtrinsicSubmission import io.novafoundation.nova.feature_account_api.domain.interfaces.AccountRepository import io.novafoundation.nova.feature_account_api.domain.model.LightMetaAccount.Type import io.novafoundation.nova.feature_account_api.domain.model.MetaAccount @@ -11,15 +10,14 @@ import io.novafoundation.nova.feature_buy_api.domain.hasProvidersFor import io.novafoundation.nova.feature_swap_api.domain.model.ReQuoteTrigger import io.novafoundation.nova.feature_swap_api.domain.model.SlippageConfig import io.novafoundation.nova.feature_swap_api.domain.model.SwapExecuteArgs +import io.novafoundation.nova.feature_swap_api.domain.model.SwapExecutionCorrection import io.novafoundation.nova.feature_swap_api.domain.model.SwapFee import io.novafoundation.nova.feature_swap_api.domain.model.SwapQuote import io.novafoundation.nova.feature_swap_api.domain.model.SwapQuoteArgs -import io.novafoundation.nova.feature_swap_api.domain.model.toExecuteArgs import io.novafoundation.nova.feature_swap_api.domain.swap.SwapService import io.novafoundation.nova.feature_swap_impl.data.network.blockhain.updaters.SwapUpdateSystemFactory import io.novafoundation.nova.feature_swap_impl.data.repository.SwapTransactionHistoryRepository import io.novafoundation.nova.feature_swap_impl.domain.model.GetAssetInOption -import io.novafoundation.nova.feature_swap_impl.domain.validation.SwapValidationPayload import io.novafoundation.nova.feature_swap_impl.domain.validation.SwapValidationSystem import io.novafoundation.nova.feature_swap_impl.domain.validation.availableSlippage import io.novafoundation.nova.feature_swap_impl.domain.validation.checkForFeeChanges @@ -40,8 +38,6 @@ import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.A import io.novafoundation.nova.feature_wallet_api.domain.interfaces.CrossChainTransfersUseCase import io.novafoundation.nova.feature_wallet_api.domain.interfaces.WalletRepository import io.novafoundation.nova.feature_wallet_api.domain.interfaces.incomingCrossChainDirectionsAvailable -import io.novafoundation.nova.feature_wallet_api.presentation.model.GenericDecimalFee -import io.novafoundation.nova.runtime.ext.commissionAsset 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 @@ -51,6 +47,7 @@ import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.map import kotlinx.coroutines.withContext +import kotlin.coroutines.coroutineContext class SwapInteractor( private val swapService: SwapService, @@ -83,29 +80,26 @@ class SwapInteractor( } suspend fun quote(quoteArgs: SwapQuoteArgs): Result { - return swapService.quote(quoteArgs) + return swapService.quote(quoteArgs, CoroutineScope(coroutineContext)) } - suspend fun executeSwap( - swapExecuteArgs: SwapExecuteArgs, - decimalFee: GenericDecimalFee - ): Result = withContext(Dispatchers.IO) { + suspend fun executeSwap(swapExecuteArgs: SwapExecuteArgs): Result = withContext(Dispatchers.IO) { swapService.swap(swapExecuteArgs) - .onSuccess { submission -> - swapTransactionHistoryRepository.insertPendingSwap( - chainAsset = swapExecuteArgs.assetIn, - swapArgs = swapExecuteArgs, - fee = decimalFee.genericFee, - txSubmission = submission - ) - - swapTransactionHistoryRepository.insertPendingSwap( - chainAsset = swapExecuteArgs.assetOut, - swapArgs = swapExecuteArgs, - fee = decimalFee.genericFee, - txSubmission = submission - ) - } +// .onSuccess { submission -> +// swapTransactionHistoryRepository.insertPendingSwap( +// chainAsset = swapExecuteArgs.assetIn, +// swapArgs = swapExecuteArgs, +// fee = decimalFee.genericFee, +// txSubmission = submission +// ) +// +// swapTransactionHistoryRepository.insertPendingSwap( +// chainAsset = swapExecuteArgs.assetOut, +// swapArgs = swapExecuteArgs, +// fee = decimalFee.genericFee, +// txSubmission = submission +// ) +// } } suspend fun canPayFeeInCustomAsset(asset: Chain.Asset): Boolean { @@ -168,41 +162,41 @@ class SwapInteractor( } } - suspend fun getValidationPayload( - assetIn: Chain.Asset, - assetOut: Chain.Asset, - feeAsset: Chain.Asset, - quoteArgs: SwapQuoteArgs, - swapQuote: SwapQuote, - swapFee: GenericDecimalFee - ): SwapValidationPayload? { - val metaAccount = accountRepository.getSelectedMetaAccount() - val chainIn = chainRegistry.getChain(swapQuote.assetIn.chainId) - val chainOut = chainRegistry.getChain(swapQuote.assetOut.chainId) - val nativeChainAssetIn = chainIn.commissionAsset - - val executeArgs = quoteArgs.toExecuteArgs( - quote = swapQuote, - customFeeAsset = feeAsset, - nativeAsset = walletRepository.getAsset(metaAccount.id, nativeChainAssetIn) ?: return null - ) - return SwapValidationPayload( - detailedAssetIn = SwapValidationPayload.SwapAssetData( - chain = chainIn, - asset = walletRepository.getAsset(metaAccount.id, assetIn) ?: return null, - amountInPlanks = swapQuote.planksIn - ), - detailedAssetOut = SwapValidationPayload.SwapAssetData( - chain = chainOut, - asset = walletRepository.getAsset(metaAccount.id, assetOut) ?: return null, - amountInPlanks = swapQuote.planksOut - ), - slippage = quoteArgs.slippage, - feeAsset = walletRepository.getAsset(metaAccount.id, feeAsset) ?: return null, - decimalFee = swapFee, - swapQuote = swapQuote, - swapQuoteArgs = quoteArgs, - swapExecuteArgs = executeArgs - ) - } +// suspend fun getValidationPayload( +// assetIn: Chain.Asset, +// assetOut: Chain.Asset, +// feeAsset: Chain.Asset, +// quoteArgs: SwapQuoteArgs, +// swapQuote: SwapQuote, +// swapFee: GenericDecimalFee +// ): SwapValidationPayload? { +// val metaAccount = accountRepository.getSelectedMetaAccount() +// val chainIn = chainRegistry.getChain(swapQuote.assetIn.chainId) +// val chainOut = chainRegistry.getChain(swapQuote.assetOut.chainId) +// val nativeChainAssetIn = chainIn.commissionAsset +// +// val executeArgs = quoteArgs.toExecuteArgs( +// quote = swapQuote, +// customFeeAsset = feeAsset, +// nativeAsset = walletRepository.getAsset(metaAccount.id, nativeChainAssetIn) ?: return null +// ) +// return SwapValidationPayload( +// detailedAssetIn = SwapValidationPayload.SwapAssetData( +// chain = chainIn, +// asset = walletRepository.getAsset(metaAccount.id, assetIn) ?: return null, +// amountInPlanks = swapQuote.planksIn +// ), +// detailedAssetOut = SwapValidationPayload.SwapAssetData( +// chain = chainOut, +// asset = walletRepository.getAsset(metaAccount.id, assetOut) ?: return null, +// amountInPlanks = swapQuote.planksOut +// ), +// slippage = quoteArgs.slippage, +// feeAsset = walletRepository.getAsset(metaAccount.id, feeAsset) ?: return null, +// decimalFee = swapFee, +// swapQuote = swapQuote, +// swapQuoteArgs = quoteArgs, +// swapExecuteArgs = executeArgs +// ) +// } } diff --git a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/domain/swap/RealSwapService.kt b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/domain/swap/RealSwapService.kt index 744e08def5..e82f916697 100644 --- a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/domain/swap/RealSwapService.kt +++ b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/domain/swap/RealSwapService.kt @@ -26,7 +26,6 @@ import io.novafoundation.nova.feature_account_api.domain.model.MetaAccount import io.novafoundation.nova.feature_account_api.domain.model.requestedAccountPaysFees import io.novafoundation.nova.feature_swap_api.domain.model.AtomicSwapOperation import io.novafoundation.nova.feature_swap_api.domain.model.AtomicSwapOperationArgs -import io.novafoundation.nova.feature_swap_api.domain.model.QuotableEdge import io.novafoundation.nova.feature_swap_api.domain.model.QuotedSwapEdge import io.novafoundation.nova.feature_swap_api.domain.model.ReQuoteTrigger import io.novafoundation.nova.feature_swap_api.domain.model.SlippageConfig @@ -154,7 +153,8 @@ internal class RealSwapService( val operationArgs = AtomicSwapOperationArgs( swapLimit = SwapLimit(direction, quotedEdge.quotedAmount, perSegmentSlippage, quotedEdge.quote), - customFeeAsset = segmentExecuteArgs.customFeeAsset, + // TODO custom fee assets + customFeeAsset = null, ) // Initial case - begin first operation @@ -179,6 +179,7 @@ internal class RealSwapService( return finishedSwapTxs } + private suspend fun quoteInternal( args: SwapQuoteArgs, computationSharingScope: CoroutineScope @@ -451,17 +452,6 @@ abstract class BaseSwapGraphEdge( final override val to: FullChainAssetId = toAsset.fullId } - -abstract class BaseQuotableEdge( - val fromAsset: Chain.Asset, - val toAsset: Chain.Asset -) : QuotableEdge { - - final override val from: FullChainAssetId = fromAsset.fullId - - final override val to: FullChainAssetId = toAsset.fullId -} - private class QuotedTrade( val direction: SwapDirection, val path: Path diff --git a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/domain/validation/SwapPayloadValidation.kt b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/domain/validation/SwapPayloadValidation.kt index b585901d6e..177d77dea4 100644 --- a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/domain/validation/SwapPayloadValidation.kt +++ b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/domain/validation/SwapPayloadValidation.kt @@ -5,7 +5,6 @@ import io.novafoundation.nova.feature_swap_api.domain.model.SwapExecuteArgs import io.novafoundation.nova.feature_swap_api.domain.model.SwapFee import io.novafoundation.nova.feature_swap_api.domain.model.SwapQuote import io.novafoundation.nova.feature_swap_api.domain.model.SwapQuoteArgs -import io.novafoundation.nova.feature_swap_api.domain.model.commissionAssetToSpendOnBuyIn import io.novafoundation.nova.feature_swap_api.domain.model.totalDeductedPlanks import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.types.Balance import io.novafoundation.nova.feature_wallet_api.domain.model.Asset @@ -42,13 +41,6 @@ val SwapValidationPayload.swapAmountInFeeToken: Balance BigInteger.ZERO } -val SwapValidationPayload.toBuyAmountToKeepMainEDInFeeAsset: Balance - get() = if (isFeePayingByAssetIn) { - decimalFee.genericFee.minimumBalanceBuyIn.commissionAssetToSpendOnBuyIn - } else { - BigInteger.ZERO - } - val SwapValidationPayload.totalDeductedAmountInFeeToken: Balance get() = if (isFeePayingByAssetIn) { decimalFee.genericFee.totalDeductedPlanks diff --git a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/domain/validation/SwapValidationFailure.kt b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/domain/validation/SwapValidationFailure.kt index 14ca6704a7..66e491fabd 100644 --- a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/domain/validation/SwapValidationFailure.kt +++ b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/domain/validation/SwapValidationFailure.kt @@ -57,41 +57,17 @@ sealed class SwapValidationFailure { val swapFee: SwapFee ) : SwapValidationFailure() - class NoNeedsToBuyMainAssetED( + class CannotPayFee( val assetIn: Chain.Asset, val feeAsset: Chain.Asset, val maxSwapAmount: Balance, val fee: Fee ) : SwapValidationFailure() - - class NeedsToBuyMainAssetED( - val feeAsset: Chain.Asset, - val assetIn: Chain.Asset, - val nativeAsset: Chain.Asset, - val toBuyAmountToKeepEDInCommissionAsset: Balance, - val toSellAmountToKeepEDUsingAssetIn: Balance, - val maxSwapAmount: Balance, - val fee: Fee - ) : SwapValidationFailure() } - sealed class TooSmallRemainingBalance : SwapValidationFailure() { - - class NoNeedsToBuyMainAssetED( - val assetIn: Chain.Asset, - val remainingBalance: Balance, - val assetInExistentialDeposit: Balance - ) : SwapValidationFailure() - - class NeedsToBuyMainAssetED( - val feeAsset: Chain.Asset, - val assetIn: Chain.Asset, - val nativeAsset: Chain.Asset, - val assetInExistentialDeposit: Balance, - val toBuyAmountToKeepEDInCommissionAsset: Balance, - val toSellAmountToKeepEDUsingAssetIn: Balance, - val remainingBalance: Balance, - val fee: Fee - ) : SwapValidationFailure() - } + class TooSmallRemainingBalance( + val assetIn: Chain.Asset, + val remainingBalance: Balance, + val assetInExistentialDeposit: Balance + ): SwapValidationFailure() } diff --git a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/domain/validation/utils/SharedQuoteValidationRetriever.kt b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/domain/validation/utils/SharedQuoteValidationRetriever.kt index 7a14e47cbe..7f277ee689 100644 --- a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/domain/validation/utils/SharedQuoteValidationRetriever.kt +++ b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/domain/validation/utils/SharedQuoteValidationRetriever.kt @@ -3,6 +3,8 @@ package io.novafoundation.nova.feature_swap_impl.domain.validation.utils import io.novafoundation.nova.feature_swap_api.domain.model.SwapQuote import io.novafoundation.nova.feature_swap_api.domain.swap.SwapService import io.novafoundation.nova.feature_swap_impl.domain.validation.SwapValidationPayload +import kotlinx.coroutines.CoroutineScope +import kotlin.coroutines.coroutineContext class SharedQuoteValidationRetriever( private val swapService: SwapService @@ -12,7 +14,7 @@ class SharedQuoteValidationRetriever( suspend fun retrieveQuote(value: SwapValidationPayload): Result { if (result == null) { - result = swapService.quote(value.swapQuoteArgs) + result = swapService.quote(value.swapQuoteArgs, CoroutineScope(coroutineContext)) } return result!! diff --git a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/domain/validation/validations/SwapFeeSufficientBalanceValidation.kt b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/domain/validation/validations/SwapFeeSufficientBalanceValidation.kt index bd694f1636..e388f4f6e5 100644 --- a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/domain/validation/validations/SwapFeeSufficientBalanceValidation.kt +++ b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/domain/validation/validations/SwapFeeSufficientBalanceValidation.kt @@ -1,18 +1,14 @@ package io.novafoundation.nova.feature_swap_impl.domain.validation.validations -import io.novafoundation.nova.common.utils.isZero import io.novafoundation.nova.common.validation.ValidationStatus import io.novafoundation.nova.common.validation.valid import io.novafoundation.nova.common.validation.validationError -import io.novafoundation.nova.feature_swap_api.domain.model.nativeMinimumBalance -import io.novafoundation.nova.feature_swap_api.domain.model.requireNativeAsset import io.novafoundation.nova.feature_swap_impl.domain.validation.SwapValidation import io.novafoundation.nova.feature_swap_impl.domain.validation.SwapValidationFailure import io.novafoundation.nova.feature_swap_impl.domain.validation.SwapValidationFailure.InsufficientBalance import io.novafoundation.nova.feature_swap_impl.domain.validation.SwapValidationPayload import io.novafoundation.nova.feature_swap_impl.domain.validation.maxAmountToSwap import io.novafoundation.nova.feature_swap_impl.domain.validation.swapAmountInFeeToken -import io.novafoundation.nova.feature_swap_impl.domain.validation.toBuyAmountToKeepMainEDInFeeAsset import io.novafoundation.nova.feature_swap_impl.domain.validation.totalDeductedAmountInFeeToken class SwapFeeSufficientBalanceValidation : SwapValidation { @@ -25,21 +21,8 @@ class SwapFeeSufficientBalanceValidation : SwapValidation { val chainAssetIn = value.detailedAssetIn.asset.token.configuration val feeAsset = value.feeAsset.token.configuration val maxAmountToSwap = value.maxAmountToSwap - val toBuyAmountToKeepEDInFeeAsset = value.toBuyAmountToKeepMainEDInFeeAsset - return if (toBuyAmountToKeepEDInFeeAsset.isZero) { - InsufficientBalance.NoNeedsToBuyMainAssetED(chainAssetIn, feeAsset, maxAmountToSwap, value.decimalFee.networkFee).validationError() - } else { - InsufficientBalance.NeedsToBuyMainAssetED( - value.feeAsset.token.configuration, - chainAssetIn, - value.decimalFee.genericFee.minimumBalanceBuyIn.requireNativeAsset(), - toBuyAmountToKeepEDInCommissionAsset = value.decimalFee.genericFee.minimumBalanceBuyIn.nativeMinimumBalance, - toSellAmountToKeepEDUsingAssetIn = toBuyAmountToKeepEDInFeeAsset, - maxAmountToSwap, - value.decimalFee.networkFee - ).validationError() - } + return InsufficientBalance.CannotPayFee(chainAssetIn, feeAsset, maxAmountToSwap, value.decimalFee.networkFee).validationError() } return valid() diff --git a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/domain/validation/validations/SwapRateChangesValidation.kt b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/domain/validation/validations/SwapRateChangesValidation.kt index ca1e105e78..dbf77d1b8b 100644 --- a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/domain/validation/validations/SwapRateChangesValidation.kt +++ b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/domain/validation/validations/SwapRateChangesValidation.kt @@ -1,14 +1,11 @@ package io.novafoundation.nova.feature_swap_impl.domain.validation.validations import io.novafoundation.nova.common.validation.ValidationStatus -import io.novafoundation.nova.common.validation.validOrError +import io.novafoundation.nova.common.validation.valid import io.novafoundation.nova.feature_swap_api.domain.model.SwapLimit import io.novafoundation.nova.feature_swap_api.domain.model.SwapQuote -import io.novafoundation.nova.feature_swap_api.domain.model.quotedBalance -import io.novafoundation.nova.feature_swap_api.domain.model.swapRate import io.novafoundation.nova.feature_swap_impl.domain.validation.SwapValidation import io.novafoundation.nova.feature_swap_impl.domain.validation.SwapValidationFailure -import io.novafoundation.nova.feature_swap_impl.domain.validation.SwapValidationFailure.NewRateExceededSlippage import io.novafoundation.nova.feature_swap_impl.domain.validation.SwapValidationPayload import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.types.Balance @@ -17,17 +14,19 @@ class SwapRateChangesValidation( ) : SwapValidation { override suspend fun validate(value: SwapValidationPayload): ValidationStatus { - val newQuote = getNewRate(value) - val swapLimit = value.swapExecuteArgs.swapLimit - - return validOrError(swapLimit.isBalanceInSwapLimits(newQuote.quotedBalance)) { - NewRateExceededSlippage( - value.detailedAssetIn.asset.token.configuration, - value.detailedAssetOut.asset.token.configuration, - value.swapQuote.swapRate(), - newQuote.swapRate() - ) - } + return valid() + // TODO validations +// val newQuote = getNewRate(value) +// val swapLimit = value.swapExecuteArgs.swapLimit +// +// return validOrError(swapLimit.isBalanceInSwapLimits(newQuote.quotedBalance)) { +// NewRateExceededSlippage( +// value.detailedAssetIn.asset.token.configuration, +// value.detailedAssetOut.asset.token.configuration, +// value.swapQuote.swapRate(), +// newQuote.swapRate() +// ) +// } } } diff --git a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/domain/validation/validations/SwapSmallRemainingBalanceValidation.kt b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/domain/validation/validations/SwapSmallRemainingBalanceValidation.kt index 23f8a512e2..f41e872968 100644 --- a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/domain/validation/validations/SwapSmallRemainingBalanceValidation.kt +++ b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/domain/validation/validations/SwapSmallRemainingBalanceValidation.kt @@ -1,16 +1,12 @@ package io.novafoundation.nova.feature_swap_impl.domain.validation.validations -import io.novafoundation.nova.common.utils.isZero import io.novafoundation.nova.common.validation.ValidationStatus import io.novafoundation.nova.common.validation.valid import io.novafoundation.nova.common.validation.validationError -import io.novafoundation.nova.feature_swap_api.domain.model.nativeMinimumBalance -import io.novafoundation.nova.feature_swap_api.domain.model.requireNativeAsset import io.novafoundation.nova.feature_swap_impl.domain.validation.SwapValidation import io.novafoundation.nova.feature_swap_impl.domain.validation.SwapValidationFailure import io.novafoundation.nova.feature_swap_impl.domain.validation.SwapValidationFailure.TooSmallRemainingBalance import io.novafoundation.nova.feature_swap_impl.domain.validation.SwapValidationPayload -import io.novafoundation.nova.feature_swap_impl.domain.validation.toBuyAmountToKeepMainEDInFeeAsset import io.novafoundation.nova.feature_swap_impl.domain.validation.totalDeductedAmountInFeeToken import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.AssetSourceRegistry import io.novasama.substrate_sdk_android.hash.isPositive @@ -20,7 +16,6 @@ class SwapSmallRemainingBalanceValidation( ) : SwapValidation { override suspend fun validate(value: SwapValidationPayload): ValidationStatus { - val feeChainAsset = value.feeAsset.token.configuration val chainAssetIn = value.detailedAssetIn.asset.token.configuration val chainIn = value.detailedAssetIn.chain val assetBalances = assetSourceRegistry.sourceFor(chainAssetIn).balance @@ -32,21 +27,7 @@ class SwapSmallRemainingBalanceValidation( val remainingBalance = balanceCountedTowardsEd - swapAmount - totalDeductedAmount if (remainingBalance.isPositive() && remainingBalance < assetInExistentialDeposit) { - val toBuyAmountToKeepEDInFeeAsset = value.toBuyAmountToKeepMainEDInFeeAsset - return if (toBuyAmountToKeepEDInFeeAsset.isZero) { - TooSmallRemainingBalance.NoNeedsToBuyMainAssetED(chainAssetIn, remainingBalance, assetInExistentialDeposit).validationError() - } else { - TooSmallRemainingBalance.NeedsToBuyMainAssetED( - feeChainAsset, - chainAssetIn, - value.decimalFee.genericFee.minimumBalanceBuyIn.requireNativeAsset(), - assetInExistentialDeposit, - toBuyAmountToKeepEDInCommissionAsset = value.decimalFee.genericFee.minimumBalanceBuyIn.nativeMinimumBalance, - toSellAmountToKeepEDUsingAssetIn = toBuyAmountToKeepEDInFeeAsset, - remainingBalance, - value.decimalFee.networkFee - ).validationError() - } + return TooSmallRemainingBalance(chainAssetIn, remainingBalance, assetInExistentialDeposit).validationError() } return valid() diff --git a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/SwapRouter.kt b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/SwapRouter.kt index 3a3aa1e434..323caa4b7c 100644 --- a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/SwapRouter.kt +++ b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/SwapRouter.kt @@ -1,12 +1,11 @@ package io.novafoundation.nova.feature_swap_impl.presentation import io.novafoundation.nova.common.navigation.ReturnableRouter -import io.novafoundation.nova.feature_swap_impl.presentation.confirmation.payload.SwapConfirmationPayload import io.novafoundation.nova.feature_wallet_api.presentation.model.AssetPayload interface SwapRouter : ReturnableRouter { - fun openSwapConfirmation(payload: SwapConfirmationPayload) + fun openSwapConfirmation() fun selectAssetIn(selectedAsset: AssetPayload?) diff --git a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/common/state/SwapState.kt b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/common/state/SwapState.kt new file mode 100644 index 0000000000..a24f1e6065 --- /dev/null +++ b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/common/state/SwapState.kt @@ -0,0 +1,11 @@ +package io.novafoundation.nova.feature_swap_impl.presentation.common.state + +import io.novafoundation.nova.common.utils.Percent +import io.novafoundation.nova.feature_swap_api.domain.model.SwapFee +import io.novafoundation.nova.feature_swap_api.domain.model.SwapQuote + +class SwapState( + val quote: SwapQuote, + val fee: SwapFee, + val slippage: Percent, +) diff --git a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/common/state/SwapStateStore.kt b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/common/state/SwapStateStore.kt new file mode 100644 index 0000000000..27f894f668 --- /dev/null +++ b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/common/state/SwapStateStore.kt @@ -0,0 +1,29 @@ +package io.novafoundation.nova.feature_swap_impl.presentation.common.state + +interface SwapStateStore { + + fun setState(state: SwapState) + + fun getState(): SwapState? +} + +fun SwapStateStore.getStateOrThrow(): SwapState { + return requireNotNull(getState()) { + "Quote was not set" + } +} + +class InMemorySwapStateStore() : SwapStateStore { + + private var quote: SwapState? = null + + override fun setState(state: SwapState) { + this.quote = state + } + + override fun getState(): SwapState? { + return quote + } +} + + diff --git a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/common/state/SwapStateStoreProvider.kt b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/common/state/SwapStateStoreProvider.kt new file mode 100644 index 0000000000..9e3d8d6820 --- /dev/null +++ b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/common/state/SwapStateStoreProvider.kt @@ -0,0 +1,28 @@ +package io.novafoundation.nova.feature_swap_impl.presentation.common.state + +import io.novafoundation.nova.common.data.memory.ComputationalCache +import kotlinx.coroutines.CoroutineScope + +interface SwapStateStoreProvider { + + suspend fun getStore(computationScope: CoroutineScope): SwapStateStore +} + +class RealSwapStateStoreProvider( + private val computationalCache: ComputationalCache +): SwapStateStoreProvider { + + companion object { + private const val CACHE_TAG = "RealSwapQuoteStoreProvider" + } + + override suspend fun getStore(computationScope: CoroutineScope): SwapStateStore { + return computationalCache.useCache(CACHE_TAG, computationScope) { + InMemorySwapStateStore() + } + } +} + +suspend fun SwapStateStoreProvider.getStateOrThrow(computationScope: CoroutineScope): SwapState { + return getStore(computationScope).getStateOrThrow() +} diff --git a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/confirmation/SwapConfirmationFragment.kt b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/confirmation/SwapConfirmationFragment.kt index 798d0a4e9a..976cbc201b 100644 --- a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/confirmation/SwapConfirmationFragment.kt +++ b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/confirmation/SwapConfirmationFragment.kt @@ -4,14 +4,13 @@ import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup -import androidx.core.os.bundleOf import io.novafoundation.nova.common.base.BaseFragment import io.novafoundation.nova.common.di.FeatureUtils import io.novafoundation.nova.common.mixin.impl.observeValidations import io.novafoundation.nova.common.utils.applyStatusBarInsets import io.novafoundation.nova.common.view.bottomSheet.description.observeDescription -import io.novafoundation.nova.common.view.setProgressState import io.novafoundation.nova.common.view.setMessageOrHide +import io.novafoundation.nova.common.view.setProgressState import io.novafoundation.nova.common.view.showValueOrHide import io.novafoundation.nova.feature_account_api.presenatation.account.wallet.showWallet import io.novafoundation.nova.feature_account_api.presenatation.actions.setupExternalActions @@ -19,7 +18,6 @@ import io.novafoundation.nova.feature_account_api.view.showAddress import io.novafoundation.nova.feature_swap_api.di.SwapFeatureApi import io.novafoundation.nova.feature_swap_impl.R import io.novafoundation.nova.feature_swap_impl.di.SwapFeatureComponent -import io.novafoundation.nova.feature_swap_impl.presentation.confirmation.payload.SwapConfirmationPayload import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.setupFeeLoading import kotlinx.android.synthetic.main.fragment_swap_confirmation_settings.swapConfirmationAccount import kotlinx.android.synthetic.main.fragment_swap_confirmation_settings.swapConfirmationAlert @@ -34,12 +32,6 @@ import kotlinx.android.synthetic.main.fragment_swap_confirmation_settings.swapCo class SwapConfirmationFragment : BaseFragment() { - companion object { - private const val KEY_PAYLOAD = "SwapConfirmationFragment.Payload" - - fun getBundle(payload: SwapConfirmationPayload) = bundleOf(KEY_PAYLOAD to payload) - } - override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, @@ -66,7 +58,7 @@ class SwapConfirmationFragment : BaseFragment() { SwapFeatureApi::class.java ) .swapConfirmation() - .create(this, argument(KEY_PAYLOAD)) + .create(this) .inject(this) } diff --git a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/confirmation/SwapConfirmationViewModel.kt b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/confirmation/SwapConfirmationViewModel.kt index 573c2568a1..cdafad7429 100644 --- a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/confirmation/SwapConfirmationViewModel.kt +++ b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/confirmation/SwapConfirmationViewModel.kt @@ -7,15 +7,10 @@ import io.novafoundation.nova.common.base.BaseViewModel import io.novafoundation.nova.common.mixin.api.Validatable import io.novafoundation.nova.common.resources.ResourceManager import io.novafoundation.nova.common.utils.Percent -import io.novafoundation.nova.common.utils.asPercent import io.novafoundation.nova.common.utils.combineToPair import io.novafoundation.nova.common.utils.flowOf import io.novafoundation.nova.common.utils.formatting.format -import io.novafoundation.nova.common.validation.TransformedFailure import io.novafoundation.nova.common.validation.ValidationExecutor -import io.novafoundation.nova.common.validation.ValidationFlowActions -import io.novafoundation.nova.common.validation.ValidationStatus -import io.novafoundation.nova.common.validation.progressConsumer import io.novafoundation.nova.common.view.bottomSheet.description.DescriptionBottomSheetLauncher import io.novafoundation.nova.common.view.bottomSheet.description.launchNetworkFeeDescription import io.novafoundation.nova.feature_account_api.data.mappers.mapChainToUi @@ -32,6 +27,7 @@ import io.novafoundation.nova.feature_swap_api.domain.model.SwapFee import io.novafoundation.nova.feature_swap_api.domain.model.SwapQuote import io.novafoundation.nova.feature_swap_api.domain.model.SwapQuoteArgs import io.novafoundation.nova.feature_swap_api.domain.model.editedBalance +import io.novafoundation.nova.feature_swap_api.domain.model.swapRate import io.novafoundation.nova.feature_swap_api.domain.model.toExecuteArgs import io.novafoundation.nova.feature_swap_api.domain.model.totalDeductedPlanks import io.novafoundation.nova.feature_swap_api.presentation.formatters.SwapRateFormatter @@ -40,15 +36,12 @@ import io.novafoundation.nova.feature_swap_api.presentation.view.SwapAssetsView import io.novafoundation.nova.feature_swap_api.presentation.view.bottomSheet.description.launchSwapRateDescription import io.novafoundation.nova.feature_swap_impl.R import io.novafoundation.nova.feature_swap_impl.domain.interactor.SwapInteractor -import io.novafoundation.nova.feature_swap_impl.domain.validation.SwapValidationFailure -import io.novafoundation.nova.feature_swap_impl.domain.validation.SwapValidationPayload import io.novafoundation.nova.feature_swap_impl.presentation.SwapRouter import io.novafoundation.nova.feature_swap_impl.presentation.common.PriceImpactFormatter import io.novafoundation.nova.feature_swap_impl.presentation.common.SlippageAlertMixinFactory +import io.novafoundation.nova.feature_swap_impl.presentation.common.state.SwapStateStoreProvider +import io.novafoundation.nova.feature_swap_impl.presentation.common.state.getStateOrThrow import io.novafoundation.nova.feature_swap_impl.presentation.confirmation.model.SwapConfirmationDetailsModel -import io.novafoundation.nova.feature_swap_impl.presentation.confirmation.payload.SwapConfirmationPayload -import io.novafoundation.nova.feature_swap_impl.presentation.confirmation.payload.SwapConfirmationPayloadFormatter -import io.novafoundation.nova.feature_swap_impl.presentation.main.mapSwapValidationFailureToUI import io.novafoundation.nova.feature_swap_impl.presentation.mixin.maxAction.MaxActionProviderFactory import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.types.Balance import io.novafoundation.nova.feature_wallet_api.domain.ArbitraryAssetUseCase @@ -59,16 +52,12 @@ import io.novafoundation.nova.feature_wallet_api.presentation.mixin.amountChoose import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.FeeLoaderMixin import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.FeeStatus import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.GenericFeeLoaderMixin -import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.getDecimalFeeOrNull import io.novafoundation.nova.feature_wallet_api.presentation.model.AmountModel -import io.novafoundation.nova.feature_wallet_api.presentation.model.fullChainAssetId import io.novafoundation.nova.feature_wallet_api.presentation.model.mapAmountToAmountModel import io.novafoundation.nova.feature_wallet_api.presentation.model.toAssetPayload import io.novafoundation.nova.runtime.ext.fullId import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry -import io.novafoundation.nova.runtime.multiNetwork.asset import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain -import io.novafoundation.nova.runtime.multiNetwork.chainWithAsset import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.combine @@ -76,6 +65,7 @@ import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.filterNotNull import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.mapNotNull @@ -99,7 +89,6 @@ class SwapConfirmationViewModel( private val swapRouter: SwapRouter, private val swapInteractor: SwapInteractor, private val resourceManager: ResourceManager, - private val payload: SwapConfirmationPayload, private val walletRepository: WalletRepository, private val accountRepository: AccountRepository, private val chainRegistry: ChainRegistry, @@ -111,7 +100,7 @@ class SwapConfirmationViewModel( private val validationExecutor: ValidationExecutor, private val tokenRepository: TokenRepository, private val externalActions: ExternalActions.Presentation, - private val swapConfirmationPayloadFormatter: SwapConfirmationPayloadFormatter, + private val swapStateStoreProvider: SwapStateStoreProvider, private val feeLoaderMixinFactory: FeeLoaderMixin.Factory, private val descriptionBottomSheetLauncher: DescriptionBottomSheetLauncher, private val arbitraryAssetUseCase: ArbitraryAssetUseCase, @@ -131,32 +120,32 @@ class SwapConfirmationViewModel( .mapNotNull { swapInteractor.slippageConfig(it.swapQuote.assetIn.chainId) } .shareInBackground() - private val slippageFlow = flowOf { payload.slippage.asPercent() } + private val initialSwapState = flowOf { swapStateStoreProvider.getStateOrThrow(viewModelScope) } + + private val slippageFlow = initialSwapState.map { it.slippage } .shareInBackground() private val slippageAlertMixin = slippageAlertMixinFactory.create(slippageConfigFlow, slippageFlow) - private val chainWithAssetFlow = flowOf { - val assetIn = payload.swapQuoteModel.assetIn - chainRegistry.chainWithAsset(assetIn.chainId, assetIn.chainAssetId) + private val chainIn = initialSwapState.map { + chainRegistry.getChain(it.quote.assetIn.chainId) } .shareInBackground() - private val assetInFlow = arbitraryAssetUseCase.assetFlow( - payload.swapQuoteModel.assetIn.chainId, - payload.swapQuoteModel.assetIn.chainAssetId - ) + private val assetInFlow = initialSwapState.flatMapLatest { + arbitraryAssetUseCase.assetFlow(it.quote.assetIn) + } .shareInBackground() - private val assetOutFlow = arbitraryAssetUseCase.assetFlow( - payload.swapQuoteModel.assetOut.chainId, - payload.swapQuoteModel.assetOut.chainAssetId - ) + private val assetOutFlow = initialSwapState.flatMapLatest { + arbitraryAssetUseCase.assetFlow(it.quote.assetOut) + } .shareInBackground() private val maxActionFlow = MutableStateFlow(MaxAction.DISABLED) - private val feeTokenFlow = arbitraryAssetUseCase.assetFlow(payload.feeAsset.chainId, payload.feeAsset.chainAssetId) + // TODO multi chain fees + private val feeTokenFlow = assetInFlow .map { it.token } .shareInBackground() @@ -179,8 +168,8 @@ class SwapConfirmationViewModel( val wallet: Flow = walletUiUseCase.selectedWalletUiFlow() - val addressFlow: Flow = combine(chainWithAssetFlow, metaAccountFlow) { chainWithAsset, metaAccount -> - addressIconGenerator.createAccountAddressModel(chainWithAsset.chain, metaAccount) + val addressFlow: Flow = combine(chainIn, metaAccountFlow) { chainId, metaAccount -> + addressIconGenerator.createAccountAddressModel(chainId, metaAccount) } val slippageAlertMessage: Flow = slippageAlertMixin.slippageAlertMessage @@ -219,26 +208,28 @@ class SwapConfirmationViewModel( fun accountClicked() { launch { - val chainWithAsset = chainWithAssetFlow.first() + val chainIn = chainIn.first() val addressModel = addressFlow.first() - externalActions.showAddressActions(addressModel.address, chainWithAsset.chain) + externalActions.showAddressActions(addressModel.address, chainIn) } } fun confirmButtonClicked() { - launch { - val validationSystem = swapInteractor.validationSystem() - val payload = getValidationPayload() ?: return@launch - - validationExecutor.requireValid( - validationSystem = validationSystem, - payload = payload, - progressConsumer = _validationProgress.progressConsumer(), - validationFailureTransformerCustom = ::formatValidationFailure, - block = ::executeSwap - ) - } + // TODO swap validations + executeSwap() +// launch { +// val validationSystem = swapInteractor.validationSystem() +// val payload = getValidationPayload() ?: return@launch +// +// validationExecutor.requireValid( +// validationSystem = validationSystem, +// payload = payload, +// progressConsumer = _validationProgress.progressConsumer(), +// validationFailureTransformerCustom = ::formatValidationFailure, +// block = ::executeSwap +// ) +// } } private fun createMaxActionProvider(): MaxActionProvider { @@ -251,9 +242,13 @@ class SwapConfirmationViewModel( ) } - private fun executeSwap(validationPayload: SwapValidationPayload) = launch { - swapInteractor.executeSwap(validationPayload.swapExecuteArgs, validationPayload.decimalFee) - .onSuccess { navigateToNextScreen(validationPayload.swapExecuteArgs.assetIn) } + private fun executeSwap() = launch { + val quote = confirmationStateFlow.value?.swapQuote ?: return@launch + // TODO fees in sending asset + val executeArgs = quote.toExecuteArgs(slippage = initialSwapState.first().slippage) + + swapInteractor.executeSwap(executeArgs) + .onSuccess { navigateToNextScreen(quote.assetIn) } .onFailure(::showError) _validationProgress.value = false @@ -274,9 +269,9 @@ class SwapConfirmationViewModel( assetIn = formatAssetDetails(metaAccount, chainIn, assetIn, confirmationState.swapQuote.planksIn), assetOut = formatAssetDetails(metaAccount, chainOut, assetOut, confirmationState.swapQuote.planksOut) ), - rate = formatRate(payload.rate, assetIn, assetOut), + rate = formatRate(confirmationState.swapQuote.swapRate(), assetIn, assetOut), priceDifference = formatPriceDifference(confirmationState.swapQuote.priceImpact), - slippage = payload.slippage.asPercent().format() + slippage =slippageFlow.first().format() ) } @@ -307,32 +302,33 @@ class SwapConfirmationViewModel( return mapAmountToAmountModel(amount, asset.token, includeZeroFiat = false, estimatedFiat = true) } - private suspend fun getValidationPayload(): SwapValidationPayload? { - val confirmationState = confirmationStateFlow.value ?: return null - val swapFee = feeMixin.getDecimalFeeOrNull() ?: return null - return swapInteractor.getValidationPayload( - assetIn = confirmationState.swapQuote.assetIn, - assetOut = confirmationState.swapQuote.assetOut, - feeAsset = confirmationState.feeAsset, - quoteArgs = confirmationState.swapQuoteArgs, - swapQuote = confirmationState.swapQuote, - swapFee = swapFee - ) - } - - private fun formatValidationFailure( - status: ValidationStatus.NotValid, - actions: ValidationFlowActions - ): TransformedFailure? { - return viewModelScope.mapSwapValidationFailureToUI( - resourceManager, - status, - actions, - setNewFee = ::setNewFee, - amountInSwapMaxAction = ::setMaxAmountIn, - amountOutSwapMinAction = ::setMinAmountOut - ) - } + // TODO swap validations +// private suspend fun getValidationPayload(): SwapValidationPayload? { +// val confirmationState = confirmationStateFlow.value ?: return null +// val swapFee = feeMixin.getDecimalFeeOrNull() ?: return null +// return swapInteractor.getValidationPayload( +// assetIn = confirmationState.swapQuote.assetIn, +// assetOut = confirmationState.swapQuote.assetOut, +// feeAsset = confirmationState.feeAsset, +// quoteArgs = confirmationState.swapQuoteArgs, +// swapQuote = confirmationState.swapQuote, +// swapFee = swapFee +// ) +// } + +// private fun formatValidationFailure( +// status: ValidationStatus.NotValid, +// actions: ValidationFlowActions +// ): TransformedFailure? { +// return viewModelScope.mapSwapValidationFailureToUI( +// resourceManager, +// status, +// actions, +// setNewFee = ::setNewFee, +// amountInSwapMaxAction = ::setMaxAmountIn, +// amountOutSwapMinAction = ::setMinAmountOut +// ) +// } private fun setMaxAmountIn() { launch { @@ -353,14 +349,13 @@ class SwapConfirmationViewModel( private fun runQuoting(newSwapQuoteArgs: SwapQuoteArgs) { launch { - val metaAccount = metaAccountFlow.first() val confirmationState = confirmationStateFlow.value ?: return@launch val swapQuote = swapInteractor.quote(newSwapQuoteArgs) .onFailure { } .getOrNull() ?: return@launch - val nativeAsset = walletRepository.getAsset(metaAccount.id, newSwapQuoteArgs.tokenOut.configuration)!! - val executeArgs = newSwapQuoteArgs.toExecuteArgs(swapQuote, confirmationState.feeAsset, nativeAsset) + val executeArgs = swapQuote.toExecuteArgs(slippageFlow.first()) + feeMixin.loadFeeV2Generic( coroutineScope = viewModelScope, feeConstructor = { swapInteractor.estimateFee(executeArgs) }, @@ -379,25 +374,27 @@ class SwapConfirmationViewModel( private fun initConfirmationState() { launch { - val swapQuote = swapConfirmationPayloadFormatter.mapSwapQuoteFromModel(payload.swapQuoteModel) + val swapState = initialSwapState.first() + + val swapQuote = swapState.quote + val assetIn = swapQuote.assetIn val assetOut = swapQuote.assetOut - val swapFee = swapConfirmationPayloadFormatter.mapFeeFromModel(payload.swapFee) - val feeAsset = chainRegistry.asset(payload.feeAsset.fullChainAssetId) val quoteArgs = SwapQuoteArgs( tokenRepository.getToken(assetIn), tokenRepository.getToken(assetOut), swapQuote.editedBalance, swapQuote.direction, - slippageFlow.first() ) - feeMixin.setFee(swapFee) + feeMixin.setFee(swapState.fee) + confirmationStateFlow.value = SwapConfirmationState( swapQuoteArgs = quoteArgs, swapQuote = swapQuote, - feeAsset = feeAsset + // TOOD multichain fees + feeAsset = swapState.quote.assetIn ) } } diff --git a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/confirmation/di/SwapConfirmationComponent.kt b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/confirmation/di/SwapConfirmationComponent.kt index 779dde4647..49bed224e9 100644 --- a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/confirmation/di/SwapConfirmationComponent.kt +++ b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/confirmation/di/SwapConfirmationComponent.kt @@ -5,7 +5,6 @@ import dagger.BindsInstance import dagger.Subcomponent import io.novafoundation.nova.common.di.scope.ScreenScope import io.novafoundation.nova.feature_swap_impl.presentation.confirmation.SwapConfirmationFragment -import io.novafoundation.nova.feature_swap_impl.presentation.confirmation.payload.SwapConfirmationPayload @Subcomponent( modules = [ @@ -20,7 +19,6 @@ interface SwapConfirmationComponent { fun create( @BindsInstance fragment: Fragment, - @BindsInstance payload: SwapConfirmationPayload ): SwapConfirmationComponent } diff --git a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/confirmation/di/SwapConfirmationModule.kt b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/confirmation/di/SwapConfirmationModule.kt index 81af3688ca..4a4a53ed0d 100644 --- a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/confirmation/di/SwapConfirmationModule.kt +++ b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/confirmation/di/SwapConfirmationModule.kt @@ -20,13 +20,12 @@ import io.novafoundation.nova.feature_swap_impl.domain.interactor.SwapInteractor import io.novafoundation.nova.feature_swap_impl.presentation.SwapRouter import io.novafoundation.nova.feature_swap_impl.presentation.common.PriceImpactFormatter import io.novafoundation.nova.feature_swap_impl.presentation.common.SlippageAlertMixinFactory +import io.novafoundation.nova.feature_swap_impl.presentation.common.state.SwapStateStoreProvider import io.novafoundation.nova.feature_swap_impl.presentation.confirmation.SwapConfirmationViewModel -import io.novafoundation.nova.feature_swap_impl.presentation.confirmation.payload.SwapConfirmationPayload -import io.novafoundation.nova.feature_swap_impl.presentation.confirmation.payload.SwapConfirmationPayloadFormatter +import io.novafoundation.nova.feature_swap_impl.presentation.mixin.maxAction.MaxActionProviderFactory import io.novafoundation.nova.feature_wallet_api.domain.ArbitraryAssetUseCase import io.novafoundation.nova.feature_wallet_api.domain.interfaces.TokenRepository import io.novafoundation.nova.feature_wallet_api.domain.interfaces.WalletRepository -import io.novafoundation.nova.feature_swap_impl.presentation.mixin.maxAction.MaxActionProviderFactory import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.FeeLoaderMixin import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry @@ -40,7 +39,6 @@ class SwapConfirmationModule { swapRouter: SwapRouter, swapInteractor: SwapInteractor, resourceManager: ResourceManager, - swapConfirmationPayload: SwapConfirmationPayload, walletRepository: WalletRepository, accountRepository: AccountRepository, chainRegistry: ChainRegistry, @@ -52,33 +50,32 @@ class SwapConfirmationModule { validationExecutor: ValidationExecutor, tokenRepository: TokenRepository, externalActions: ExternalActions.Presentation, - swapConfirmationPayloadFormatter: SwapConfirmationPayloadFormatter, feeLoaderMixinFactory: FeeLoaderMixin.Factory, descriptionBottomSheetLauncher: DescriptionBottomSheetLauncher, assetUseCase: ArbitraryAssetUseCase, - maxActionProviderFactory: MaxActionProviderFactory + maxActionProviderFactory: MaxActionProviderFactory, + swapStateStoreProvider: SwapStateStoreProvider ): ViewModel { return SwapConfirmationViewModel( - swapRouter, - swapInteractor, - resourceManager, - swapConfirmationPayload, - walletRepository, - accountRepository, - chainRegistry, - swapRateFormatter, - priceImpactFormatter, - walletUiUseCase, - slippageAlertMixinFactory, - addressIconGenerator, - validationExecutor, - tokenRepository, - externalActions, - swapConfirmationPayloadFormatter, - feeLoaderMixinFactory, - descriptionBottomSheetLauncher, - assetUseCase, - maxActionProviderFactory + swapRouter = swapRouter, + swapInteractor = swapInteractor, + resourceManager = resourceManager, + walletRepository = walletRepository, + accountRepository = accountRepository, + chainRegistry = chainRegistry, + swapRateFormatter = swapRateFormatter, + priceImpactFormatter = priceImpactFormatter, + walletUiUseCase = walletUiUseCase, + slippageAlertMixinFactory = slippageAlertMixinFactory, + addressIconGenerator = addressIconGenerator, + validationExecutor = validationExecutor, + tokenRepository = tokenRepository, + externalActions = externalActions, + swapStateStoreProvider = swapStateStoreProvider, + feeLoaderMixinFactory = feeLoaderMixinFactory, + descriptionBottomSheetLauncher = descriptionBottomSheetLauncher, + arbitraryAssetUseCase = assetUseCase, + maxActionProviderFactory = maxActionProviderFactory ) } diff --git a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/confirmation/payload/SwapConfirmationPayload.kt b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/confirmation/payload/SwapConfirmationPayload.kt deleted file mode 100644 index 1fbe930532..0000000000 --- a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/confirmation/payload/SwapConfirmationPayload.kt +++ /dev/null @@ -1,59 +0,0 @@ -package io.novafoundation.nova.feature_swap_impl.presentation.confirmation.payload - -import android.os.Parcelable -import io.novafoundation.nova.feature_swap_api.presentation.model.SwapDirectionModel -import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.types.Balance -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 - -@Parcelize -class SwapConfirmationPayload( - val swapQuoteModel: SwapQuoteModel, - val rate: BigDecimal, - val slippage: Double, - val feeAsset: AssetPayload, - val swapFee: FeeDetails -) : Parcelable { - - @Parcelize - class SwapQuoteModel( - val assetIn: AssetPayload, - val assetOut: AssetPayload, - val planksIn: Balance, - val planksOut: Balance, - val direction: SwapDirectionModel, - val priceImpact: Double, - val path: List - ) : Parcelable - - @Parcelize - class SwapQuotePathModel( - val from: AssetPayload, - val to: AssetPayload, - val sourceId: String, - val sourceParams: Map - ) : Parcelable - - @Parcelize - class FeeDetails( - val networkFee: FeeParcelModel, - val minimumBalanceBuyIn: MinimumBalanceBuyIn - ) : Parcelable { - - sealed interface MinimumBalanceBuyIn : Parcelable { - - @Parcelize - class NeedsToBuyMinimumBalance( - val nativeAsset: AssetPayload, - val nativeMinimumBalance: Balance, - val commissionAsset: AssetPayload, - val commissionAssetToSpendOnBuyIn: Balance - ) : MinimumBalanceBuyIn - - @Parcelize - object NoBuyInNeeded : MinimumBalanceBuyIn - } - } -} diff --git a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/confirmation/payload/SwapConfirmationPayloadFormatter.kt b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/confirmation/payload/SwapConfirmationPayloadFormatter.kt deleted file mode 100644 index 0fe72da771..0000000000 --- a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/confirmation/payload/SwapConfirmationPayloadFormatter.kt +++ /dev/null @@ -1,100 +0,0 @@ -package io.novafoundation.nova.feature_swap_impl.presentation.confirmation.payload - -import io.novafoundation.nova.common.utils.asPercent -import io.novafoundation.nova.feature_swap_api.domain.model.MinimumBalanceBuyIn -import io.novafoundation.nova.feature_swap_api.domain.model.QuotePath -import io.novafoundation.nova.feature_swap_api.domain.model.SwapFee -import io.novafoundation.nova.feature_swap_api.domain.model.SwapQuote -import io.novafoundation.nova.feature_swap_api.presentation.model.mapFromModel -import io.novafoundation.nova.feature_swap_api.presentation.model.mapToModel -import io.novafoundation.nova.feature_wallet_api.domain.model.withAmount -import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.mapFeeFromParcel -import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.mapFeeToParcel -import io.novafoundation.nova.feature_wallet_api.presentation.model.GenericDecimalFee -import io.novafoundation.nova.feature_wallet_api.presentation.model.fullChainAssetId -import io.novafoundation.nova.feature_wallet_api.presentation.model.toAssetPayload -import io.novafoundation.nova.runtime.ext.fullId -import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry -import io.novafoundation.nova.runtime.multiNetwork.asset - -class SwapConfirmationPayloadFormatter( - private val chainRegistry: ChainRegistry -) { - - suspend fun mapSwapQuoteFromModel(model: SwapConfirmationPayload.SwapQuoteModel): SwapQuote { - return with(model) { - SwapQuote( - amountIn = chainRegistry.asset(assetIn.fullChainAssetId).withAmount(planksIn), - amountOut = chainRegistry.asset(assetOut.fullChainAssetId).withAmount(planksOut), - direction = model.direction.mapFromModel(), - priceImpact = model.priceImpact.asPercent(), - path = QuotePath( - segments = model.path.map { - QuotePath.Segment( - from = it.from.fullChainAssetId, - to = it.to.fullChainAssetId, - sourceId = it.sourceId, - sourceParams = it.sourceParams - ) - } - ) - ) - } - } - - fun mapSwapQuoteToModel(model: SwapQuote): SwapConfirmationPayload.SwapQuoteModel { - return SwapConfirmationPayload.SwapQuoteModel( - assetIn = model.assetIn.fullId.toAssetPayload(), - assetOut = model.assetOut.fullId.toAssetPayload(), - planksIn = model.planksIn, - planksOut = model.planksOut, - direction = model.direction.mapToModel(), - priceImpact = model.priceImpact.value, - path = model.path.segments.map { - SwapConfirmationPayload.SwapQuotePathModel( - from = it.from.toAssetPayload(), - to = it.to.toAssetPayload(), - sourceId = it.sourceId, - sourceParams = it.sourceParams - ) - } - ) - } - - suspend fun mapFeeFromModel(model: SwapConfirmationPayload.FeeDetails): SwapFee { - val minimumBalanceBuyIn = when (val minimumBalanceBuyIn = model.minimumBalanceBuyIn) { - is SwapConfirmationPayload.FeeDetails.MinimumBalanceBuyIn.NeedsToBuyMinimumBalance -> { - MinimumBalanceBuyIn.NeedsToBuyMinimumBalance( - chainRegistry.asset(minimumBalanceBuyIn.nativeAsset.fullChainAssetId), - minimumBalanceBuyIn.nativeMinimumBalance, - chainRegistry.asset(minimumBalanceBuyIn.commissionAsset.fullChainAssetId), - minimumBalanceBuyIn.commissionAssetToSpendOnBuyIn - ) - } - - SwapConfirmationPayload.FeeDetails.MinimumBalanceBuyIn.NoBuyInNeeded -> MinimumBalanceBuyIn.NoBuyInNeeded - } - - val decimalFee = mapFeeFromParcel(model.networkFee) - - return SwapFee(decimalFee.networkFee, minimumBalanceBuyIn) - } - - fun mapFeeToModel(swapFee: GenericDecimalFee): SwapConfirmationPayload.FeeDetails { - val minimumBalanceBuyIn = when (val minimumBalanceBuyIn = swapFee.genericFee.minimumBalanceBuyIn) { - is MinimumBalanceBuyIn.NeedsToBuyMinimumBalance -> { - val nativeAsset = minimumBalanceBuyIn.nativeAsset.fullId.toAssetPayload() - val commissionAsset = minimumBalanceBuyIn.commissionAsset.fullId.toAssetPayload() - SwapConfirmationPayload.FeeDetails.MinimumBalanceBuyIn.NeedsToBuyMinimumBalance( - nativeAsset, - minimumBalanceBuyIn.nativeMinimumBalance, - commissionAsset, - minimumBalanceBuyIn.commissionAssetToSpendOnBuyIn - ) - } - - MinimumBalanceBuyIn.NoBuyInNeeded -> SwapConfirmationPayload.FeeDetails.MinimumBalanceBuyIn.NoBuyInNeeded - } - return SwapConfirmationPayload.FeeDetails(mapFeeToParcel(swapFee), minimumBalanceBuyIn) - } -} diff --git a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/main/SwapMainSettingsFragment.kt b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/main/SwapMainSettingsFragment.kt index 0afa2ad160..fa55d086f0 100644 --- a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/main/SwapMainSettingsFragment.kt +++ b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/main/SwapMainSettingsFragment.kt @@ -32,7 +32,6 @@ import kotlinx.android.synthetic.main.fragment_main_swap_settings.swapMainSettin import kotlinx.android.synthetic.main.fragment_main_swap_settings.swapMainSettingsFlip import kotlinx.android.synthetic.main.fragment_main_swap_settings.swapMainSettingsGetAssetIn import kotlinx.android.synthetic.main.fragment_main_swap_settings.swapMainSettingsMaxAmount -import kotlinx.android.synthetic.main.fragment_main_swap_settings.swapMainSettingsMinBalanceAlert import kotlinx.android.synthetic.main.fragment_main_swap_settings.swapMainSettingsPayInput import kotlinx.android.synthetic.main.fragment_main_swap_settings.swapMainSettingsReceiveInput import kotlinx.android.synthetic.main.fragment_main_swap_settings.swapMainSettingsToolbar @@ -115,8 +114,6 @@ class SwapMainSettingsFragment : BaseFragment() { } } - viewModel.minimumBalanceBuyAlert.observe(swapMainSettingsMinBalanceAlert::setModel) - viewModel.canChangeFeeToken.observe { canChangeFeeToken -> if (canChangeFeeToken) { swapMainSettingsDetailsNetworkFee.setPrimaryValueStartIcon(R.drawable.ic_pencil_edit, R.color.icon_secondary) diff --git a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/main/SwapMainSettingsViewModel.kt b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/main/SwapMainSettingsViewModel.kt index 9e975ea494..cb0a679507 100644 --- a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/main/SwapMainSettingsViewModel.kt +++ b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/main/SwapMainSettingsViewModel.kt @@ -29,16 +29,11 @@ import io.novafoundation.nova.common.utils.zipWithPrevious import io.novafoundation.nova.common.validation.CompoundFieldValidator import io.novafoundation.nova.common.validation.FieldValidator import io.novafoundation.nova.common.validation.ValidationExecutor -import io.novafoundation.nova.common.validation.ValidationFlowActions -import io.novafoundation.nova.common.validation.ValidationStatus -import io.novafoundation.nova.common.validation.progressConsumer -import io.novafoundation.nova.common.view.SimpleAlertModel import io.novafoundation.nova.common.view.bottomSheet.description.DescriptionBottomSheetLauncher import io.novafoundation.nova.common.view.bottomSheet.description.launchNetworkFeeDescription import io.novafoundation.nova.feature_account_api.domain.interfaces.SelectedAccountUseCase import io.novafoundation.nova.feature_account_api.domain.model.addressIn import io.novafoundation.nova.feature_buy_api.presentation.mixin.BuyMixin -import io.novafoundation.nova.feature_swap_api.domain.model.MinimumBalanceBuyIn import io.novafoundation.nova.feature_swap_api.domain.model.SwapDirection import io.novafoundation.nova.feature_swap_api.domain.model.SwapFee import io.novafoundation.nova.feature_swap_api.domain.model.SwapQuote @@ -55,11 +50,9 @@ import io.novafoundation.nova.feature_swap_api.presentation.view.bottomSheet.des import io.novafoundation.nova.feature_swap_impl.R import io.novafoundation.nova.feature_swap_impl.domain.interactor.SwapInteractor import io.novafoundation.nova.feature_swap_impl.domain.model.GetAssetInOption -import io.novafoundation.nova.feature_swap_impl.domain.validation.SwapValidationFailure -import io.novafoundation.nova.feature_swap_impl.domain.validation.SwapValidationPayload import io.novafoundation.nova.feature_swap_impl.presentation.SwapRouter -import io.novafoundation.nova.feature_swap_impl.presentation.confirmation.payload.SwapConfirmationPayload -import io.novafoundation.nova.feature_swap_impl.presentation.confirmation.payload.SwapConfirmationPayloadFormatter +import io.novafoundation.nova.feature_swap_impl.presentation.common.state.SwapStateStoreProvider +import io.novafoundation.nova.feature_swap_impl.presentation.common.state.SwapState import io.novafoundation.nova.feature_swap_impl.presentation.fieldValidation.EnoughAmountToSwapValidatorFactory import io.novafoundation.nova.feature_swap_impl.presentation.fieldValidation.LiquidityFieldValidatorFactory import io.novafoundation.nova.feature_swap_impl.presentation.fieldValidation.SwapReceiveAmountAboveEDFieldValidatorFactory @@ -76,7 +69,6 @@ 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.amountFromPlanks import io.novafoundation.nova.feature_wallet_api.domain.model.planksFromAmount -import io.novafoundation.nova.feature_wallet_api.presentation.formatters.formatPlanks import io.novafoundation.nova.feature_wallet_api.presentation.mixin.amountChooser.AmountChooserMixinBase.AmountErrorState import io.novafoundation.nova.feature_wallet_api.presentation.mixin.amountChooser.AmountChooserMixinBase.InputState import io.novafoundation.nova.feature_wallet_api.presentation.mixin.amountChooser.AmountChooserMixinBase.InputState.InputKind @@ -86,12 +78,10 @@ import io.novafoundation.nova.feature_wallet_api.presentation.mixin.amountChoose import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.FeeLoaderMixin import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.FeeStatus import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.GenericFeeLoaderMixin -import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.loadedDecimalFeeOrNullFlow +import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.awaitDecimalFee import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.loadedFeeModelOrNullFlow -import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.loadedFeeOrNullFlow import io.novafoundation.nova.feature_wallet_api.presentation.model.AssetPayload import io.novafoundation.nova.feature_wallet_api.presentation.model.fullChainAssetId -import io.novafoundation.nova.feature_wallet_api.presentation.model.toAssetPayload import io.novafoundation.nova.runtime.ext.commissionAsset import io.novafoundation.nova.runtime.ext.fullId import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry @@ -143,8 +133,8 @@ class SwapMainSettingsViewModel( private val buyMixinFactory: BuyMixin.Factory, private val descriptionBottomSheetLauncher: DescriptionBottomSheetLauncher, private val swapRateFormatter: SwapRateFormatter, - private val swapConfirmationPayloadFormatter: SwapConfirmationPayloadFormatter, private val maxActionProviderFactory: MaxActionProviderFactory, + private val swapStateStoreProvider: SwapStateStoreProvider, swapAmountInputMixinFactory: SwapAmountInputMixinFactory, feeLoaderMixinFactory: FeeLoaderMixin.Factory, actionAwaitableFactory: ActionAwaitableMixin.Factory, @@ -248,10 +238,6 @@ class SwapMainSettingsViewModel( val swapDirectionFlipped: MutableLiveData> = MutableLiveData() - val minimumBalanceBuyAlert = feeMixin.loadedFeeOrNullFlow() - .map(::prepareMinimumBalanceBuyInAlertIfNeeded) - .shareInBackground() - val canChangeFeeToken = chainAssetIn .map(::isEditFeeTokenAvailable) .shareInBackground() @@ -339,18 +325,29 @@ class SwapMainSettingsViewModel( fun continueButtonClicked() { launch { - val validationSystem = swapInteractor.validationSystem() - val payload = getValidationPayload() ?: return@launch - - validationExecutor.requireValid( - validationSystem = validationSystem, - payload = payload, - progressConsumer = _validationProgress.progressConsumer(), - validationFailureTransformerCustom = ::formatValidationFailure, - ) { validPayload -> - _validationProgress.value = false - openSwapConfirmation(validPayload) - } + val quotingState = quotingState.value + if (quotingState !is QuotingState.Loaded) return@launch + + val swapState = SwapState( + quote = quotingState.value, + fee = feeMixin.awaitDecimalFee().genericFee, + slippage = swapSettings.first().slippage + ) + swapStateStoreProvider.getStore(viewModelScope).setState(swapState) + swapRouter.openSwapConfirmation() + +// val validationSystem = swapInteractor.validationSystem() +// val payload = getValidationPayload() ?: return@launch +// +// validationExecutor.requireValid( +// validationSystem = validationSystem, +// payload = payload, +// progressConsumer = _validationProgress.progressConsumer(), +// validationFailureTransformerCustom = ::formatValidationFailure, +// ) { validPayload -> +// _validationProgress.value = false +// openSwapConfirmation(validPayload) +// } } } @@ -534,10 +531,8 @@ class SwapMainSettingsViewModel( } } .mapLatest { quoteState -> - val swapArgs = quoteState.quoteArgs.toExecuteArgs( - quote = quoteState.value, - customFeeAsset = quoteState.feeAsset, - nativeAsset = nativeAssetFlow.first() + val swapArgs = quoteState.value.toExecuteArgs( + slippage = swapSettings.first().slippage ) loadFeeSuspending( @@ -691,32 +686,12 @@ class SwapMainSettingsViewModel( tokenOut = tokenOut(assetOut!!), amount = amount!!, swapDirection = swapDirection!!, - slippage = slippage ) } else { null } } - private fun prepareMinimumBalanceBuyInAlertIfNeeded(swapFee: SwapFee?): SimpleAlertModel? { - if (swapFee == null) return null - val minimumBalanceBuyIn = swapFee.minimumBalanceBuyIn - if (minimumBalanceBuyIn !is MinimumBalanceBuyIn.NeedsToBuyMinimumBalance) return null - - val feeAssetSymbol = minimumBalanceBuyIn.commissionAsset.symbol - val nativeAssetSymbol = minimumBalanceBuyIn.nativeAsset.symbol - val feeAssetNeededForBuyIn = minimumBalanceBuyIn.commissionAssetToSpendOnBuyIn.formatPlanks(minimumBalanceBuyIn.commissionAsset) - val nativeMinimumBalance = minimumBalanceBuyIn.nativeMinimumBalance.formatPlanks(minimumBalanceBuyIn.nativeAsset) - - return resourceManager.getString( - R.string.swap_minimum_balance_buy_in_alert, - feeAssetSymbol, - feeAssetNeededForBuyIn, - nativeMinimumBalance, - nativeAssetSymbol - ) - } - private fun handleInputChanges( amountInput: SwapAmountInputMixin.Presentation, chainAsset: (SwapSettings) -> Chain.Asset?, @@ -770,31 +745,31 @@ class SwapMainSettingsViewModel( return swapReceiveAmountAboveEDFieldValidatorFactory.create(assetOutFlow) } - private suspend fun getValidationPayload(): SwapValidationPayload? { - val quotingState = quotingState.value - if (quotingState !is QuotingState.Loaded) return null - val swapSettings = swapSettings.first() - return swapInteractor.getValidationPayload( - assetIn = swapSettings.assetIn ?: return null, - assetOut = swapSettings.assetOut ?: return null, - feeAsset = swapSettings.feeAsset ?: return null, - quoteArgs = quotingState.quoteArgs, - swapQuote = quotingState.value, - swapFee = feeMixin.loadedDecimalFeeOrNullFlow().first() ?: return null - ) - } - - private fun formatValidationFailure( - status: ValidationStatus.NotValid, - actions: ValidationFlowActions - ) = viewModelScope.mapSwapValidationFailureToUI( - resourceManager, - status, - actions, - ::setFee, - ::setMaxAvailableAmountIn, - ::setMinAmountOut, - ) +// private suspend fun getValidationPayload(): SwapValidationPayload? { +// val quotingState = quotingState.value +// if (quotingState !is QuotingState.Loaded) return null +// val swapSettings = swapSettings.first() +// return swapInteractor.getValidationPayload( +// assetIn = swapSettings.assetIn ?: return null, +// assetOut = swapSettings.assetOut ?: return null, +// feeAsset = swapSettings.feeAsset ?: return null, +// quoteArgs = quotingState.quoteArgs, +// swapQuote = quotingState.value, +// swapFee = feeMixin.loadedDecimalFeeOrNullFlow().first() ?: return null +// ) +// } + +// private fun formatValidationFailure( +// status: ValidationStatus.NotValid, +// actions: ValidationFlowActions +// ) = viewModelScope.mapSwapValidationFailureToUI( +// resourceManager, +// status, +// actions, +// ::setFee, +// ::setMaxAvailableAmountIn, +// ::setMinAmountOut, +// ) private fun setFee(swapFee: SwapFee) { launch { @@ -815,17 +790,5 @@ class SwapMainSettingsViewModel( } } - private fun openSwapConfirmation(validPayload: SwapValidationPayload) { - val payload = SwapConfirmationPayload( - swapQuoteModel = swapConfirmationPayloadFormatter.mapSwapQuoteToModel(validPayload.swapQuote), - feeAsset = validPayload.feeAsset.token.configuration.fullId.toAssetPayload(), - rate = validPayload.swapQuote.swapRate(), - slippage = validPayload.slippage.value, - swapFee = swapConfirmationPayloadFormatter.mapFeeToModel(validPayload.decimalFee) - ) - - swapRouter.openSwapConfirmation(payload) - } - private fun Flow.token(): Flow = map { it?.token } } diff --git a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/main/SwapValidationFailureUi.kt b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/main/SwapValidationFailureUi.kt index 775eb53192..e0551b128a 100644 --- a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/main/SwapValidationFailureUi.kt +++ b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/main/SwapValidationFailureUi.kt @@ -70,35 +70,7 @@ fun CoroutineScope.mapSwapValidationFailureToUI( is AmountOutIsTooLowToStayAboveED -> handleErrorToSwapMin(reason, resourceManager, amountOutSwapMinAction) - is TooSmallRemainingBalance.NoNeedsToBuyMainAssetED -> handleTooSmallRemainingBalance( - title = resourceManager.getString(R.string.swap_failure_too_small_remaining_balance_title), - message = resourceManager.getString( - R.string.swap_failure_too_small_remaining_balance_message, - reason.assetInExistentialDeposit.formatPlanks(reason.assetIn), - reason.remainingBalance.formatPlanks(reason.assetIn) - ), - resourceManager = resourceManager, - actions = actions, - positiveButtonClick = amountInSwapMaxAction - ) - - is TooSmallRemainingBalance.NeedsToBuyMainAssetED -> handleTooSmallRemainingBalance( - title = resourceManager.getString(R.string.swap_failure_too_small_remaining_balance_title), - message = resourceManager.getString( - R.string.swap_failure_too_small_remaining_balance_with_buy_ed_message, - reason.assetInExistentialDeposit.formatPlanks(reason.assetIn), - reason.fee.amountByRequestedAccount.formatPlanks(reason.feeAsset), - reason.toSellAmountToKeepEDUsingAssetIn.formatPlanks(reason.assetIn), - reason.toBuyAmountToKeepEDInCommissionAsset.formatPlanks(reason.nativeAsset), - reason.nativeAsset.symbol, - reason.remainingBalance.formatPlanks(reason.assetIn) - ), - resourceManager = resourceManager, - actions = actions, - positiveButtonClick = amountInSwapMaxAction - ) - - is InsufficientBalance.NoNeedsToBuyMainAssetED -> handleInsufficientBalance( + is InsufficientBalance.CannotPayFee -> handleInsufficientBalance( title = resourceManager.getString(R.string.common_not_enough_funds_title), message = resourceManager.getString( R.string.swap_failure_insufficient_balance_message, @@ -109,20 +81,6 @@ fun CoroutineScope.mapSwapValidationFailureToUI( positiveButtonClick = amountInSwapMaxAction ) - is InsufficientBalance.NeedsToBuyMainAssetED -> handleInsufficientBalance( - title = resourceManager.getString(R.string.common_not_enough_funds_title), - message = resourceManager.getString( - R.string.swap_failure_insufficient_balance_with_buy_ed_message, - reason.maxSwapAmount.formatPlanks(reason.assetIn), - reason.fee.amountByRequestedAccount.formatPlanks(reason.feeAsset), - reason.toSellAmountToKeepEDUsingAssetIn.formatPlanks(reason.assetIn), - reason.toBuyAmountToKeepEDInCommissionAsset.formatPlanks(reason.nativeAsset), - reason.nativeAsset.symbol - ), - resourceManager = resourceManager, - positiveButtonClick = amountInSwapMaxAction - ) - is InsufficientBalance.BalanceNotConsiderConsumers -> TitleAndMessage( resourceManager.getString(R.string.common_not_enough_funds_title), resourceManager.getString( @@ -147,6 +105,18 @@ fun CoroutineScope.mapSwapValidationFailureToUI( actions = actions, setFee = { setNewFee(it.newFee.genericFee) }, ) + + is TooSmallRemainingBalance -> handleTooSmallRemainingBalance( + title = resourceManager.getString(R.string.swap_failure_too_small_remaining_balance_title), + message = resourceManager.getString( + R.string.swap_failure_too_small_remaining_balance_message, + reason.assetInExistentialDeposit.formatPlanks(reason.assetIn), + reason.remainingBalance.formatPlanks(reason.assetIn) + ), + resourceManager = resourceManager, + actions = actions, + positiveButtonClick = amountInSwapMaxAction + ) } } diff --git a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/main/di/SwapMainSettingsModule.kt b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/main/di/SwapMainSettingsModule.kt index 51f0b15b37..d3c676f724 100644 --- a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/main/di/SwapMainSettingsModule.kt +++ b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/main/di/SwapMainSettingsModule.kt @@ -21,7 +21,7 @@ import io.novafoundation.nova.feature_swap_api.presentation.state.SwapSettingsSt import io.novafoundation.nova.feature_swap_impl.domain.interactor.SwapInteractor import io.novafoundation.nova.feature_swap_impl.presentation.SwapRouter import io.novafoundation.nova.feature_swap_impl.presentation.common.PriceImpactFormatter -import io.novafoundation.nova.feature_swap_impl.presentation.confirmation.payload.SwapConfirmationPayloadFormatter +import io.novafoundation.nova.feature_swap_impl.presentation.common.state.SwapStateStoreProvider import io.novafoundation.nova.feature_swap_impl.presentation.fieldValidation.EnoughAmountToSwapValidatorFactory import io.novafoundation.nova.feature_swap_impl.presentation.fieldValidation.LiquidityFieldValidatorFactory import io.novafoundation.nova.feature_swap_impl.presentation.fieldValidation.SwapReceiveAmountAboveEDFieldValidatorFactory @@ -95,8 +95,8 @@ class SwapMainSettingsModule { validationExecutor: ValidationExecutor, descriptionBottomSheetLauncher: DescriptionBottomSheetLauncher, swapRateFormatter: SwapRateFormatter, - swapConfirmationPayloadFormatter: SwapConfirmationPayloadFormatter, - maxActionProviderFactory: MaxActionProviderFactory + maxActionProviderFactory: MaxActionProviderFactory, + swapStateStoreProvider: SwapStateStoreProvider, ): ViewModel { return SwapMainSettingsViewModel( swapRouter = swapRouter, @@ -116,9 +116,9 @@ class SwapMainSettingsModule { swapReceiveAmountAboveEDFieldValidatorFactory = swapReceiveAmountAboveEDFieldValidatorFactory, descriptionBottomSheetLauncher = descriptionBottomSheetLauncher, swapRateFormatter = swapRateFormatter, - swapConfirmationPayloadFormatter = swapConfirmationPayloadFormatter, selectedAccountUseCase = accountUseCase, buyMixinFactory = buyMixinFactory, + swapStateStoreProvider = swapStateStoreProvider, maxActionProviderFactory = maxActionProviderFactory ) } diff --git a/feature-swap-impl/src/main/res/layout/fragment_main_swap_settings.xml b/feature-swap-impl/src/main/res/layout/fragment_main_swap_settings.xml index ec421bed9c..4fa5c29362 100644 --- a/feature-swap-impl/src/main/res/layout/fragment_main_swap_settings.xml +++ b/feature-swap-impl/src/main/res/layout/fragment_main_swap_settings.xml @@ -177,17 +177,6 @@ - - - From bae884e121f35bdb64c5ce4493df5d5ca1fce1e5 Mon Sep 17 00:00:00 2001 From: Valentun Date: Tue, 17 Sep 2024 12:30:03 +0300 Subject: [PATCH 08/83] Enable logging back --- .../domain/model/SwapGraph.kt | 2 + .../AssetConversionExchange.kt | 4 + .../assetExchange/hydraDx/HydraDxExchange.kt | 4 + .../hydraDx/HydraDxSwapSource.kt | 2 + .../hydraDx/omnipool/OmniPoolSwapSource.kt | 4 + .../hydraDx/stableswap/StableSwapSource.kt | 5 ++ .../hydraDx/xyk/XYKSwapSource.kt | 9 ++- .../domain/swap/RealSwapService.kt | 78 ++++++++----------- 8 files changed, 59 insertions(+), 49 deletions(-) diff --git a/feature-swap-api/src/main/java/io/novafoundation/nova/feature_swap_api/domain/model/SwapGraph.kt b/feature-swap-api/src/main/java/io/novafoundation/nova/feature_swap_api/domain/model/SwapGraph.kt index 0ae7aacfd2..52e586deef 100644 --- a/feature-swap-api/src/main/java/io/novafoundation/nova/feature_swap_api/domain/model/SwapGraph.kt +++ b/feature-swap-api/src/main/java/io/novafoundation/nova/feature_swap_api/domain/model/SwapGraph.kt @@ -16,6 +16,8 @@ interface SwapGraphEdge : QuotableEdge { * [beginOperation] */ suspend fun appendToOperation(currentTransaction: AtomicSwapOperation, args: AtomicSwapOperationArgs): AtomicSwapOperation? + + suspend fun debugLabel(): String } interface QuotableEdge : Edge { diff --git a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/assetConversion/AssetConversionExchange.kt b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/assetConversion/AssetConversionExchange.kt index f2f4ae5879..e01057751a 100644 --- a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/assetConversion/AssetConversionExchange.kt +++ b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/assetConversion/AssetConversionExchange.kt @@ -168,6 +168,10 @@ private class AssetConversionExchange( return null } + override suspend fun debugLabel(): String { + return "AssetConversion" + } + override suspend fun quote( amount: Balance, direction: SwapDirection diff --git a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/hydraDx/HydraDxExchange.kt b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/hydraDx/HydraDxExchange.kt index b156bf8ded..82ecfe36e8 100644 --- a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/hydraDx/HydraDxExchange.kt +++ b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/hydraDx/HydraDxExchange.kt @@ -197,6 +197,10 @@ private class HydraDxExchange( return currentTransaction.appendSegment(HydraDxSwapTransactionSegment(sourceQuotableEdge, args)) } + + override suspend fun debugLabel(): String { + return "Hydration.${sourceQuotableEdge.debugLabel()}" + } } inner class HydraDxOperation private constructor( diff --git a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/hydraDx/HydraDxSwapSource.kt b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/hydraDx/HydraDxSwapSource.kt index 61c7e42647..934ca1fb7c 100644 --- a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/hydraDx/HydraDxSwapSource.kt +++ b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/hydraDx/HydraDxSwapSource.kt @@ -21,6 +21,8 @@ interface HydraDxSourceEdge : QuotableEdge { * Whether hydra swap source is able to perform optimized standalone swap without using Router */ val standaloneSwapBuilder: HydraDxStandaloneSwapBuilder? + + suspend fun debugLabel(): String } interface HydraDxSwapSource : Identifiable { diff --git a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/hydraDx/omnipool/OmniPoolSwapSource.kt b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/hydraDx/omnipool/OmniPoolSwapSource.kt index 84281f0f82..1e829e2f31 100644 --- a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/hydraDx/omnipool/OmniPoolSwapSource.kt +++ b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/hydraDx/omnipool/OmniPoolSwapSource.kt @@ -256,6 +256,10 @@ private class OmniPoolSwapSource( executeSwap(it) } + override suspend fun debugLabel(): String { + return "OmniPool" + } + override suspend fun quote(amount: Balance, direction: SwapDirection): Balance { val omniPool = omniPoolFlow.first() diff --git a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/hydraDx/stableswap/StableSwapSource.kt b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/hydraDx/stableswap/StableSwapSource.kt index 1f03219f80..4ef3e121dc 100644 --- a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/hydraDx/stableswap/StableSwapSource.kt +++ b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/hydraDx/stableswap/StableSwapSource.kt @@ -24,6 +24,7 @@ import io.novafoundation.nova.feature_swap_impl.data.assetExchange.hydraDx.stabl import io.novafoundation.nova.feature_swap_impl.data.assetExchange.hydraDx.stableswap.model.quote import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.HydraDxAssetId import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.HydraDxAssetIdConverter +import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.toChainAssetOrThrow import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.types.Balance import io.novafoundation.nova.feature_wallet_api.domain.model.Asset import io.novafoundation.nova.feature_wallet_api.domain.model.Asset.Companion.calculateTransferable @@ -278,6 +279,10 @@ private class StableSwapSource( override val to: FullChainAssetId = toAsset.second override val standaloneSwapBuilder: HydraDxStandaloneSwapBuilder? = null + override suspend fun debugLabel(): String { + val poolAsset = hydraDxAssetIdConverter.toChainAssetOrThrow(chain, poolId) + return "StableSwap.${poolAsset.symbol}" + } override fun routerPoolArgument(): DictEnum.Entry<*> { return DictEnum.Entry("Stableswap", poolId) diff --git a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/hydraDx/xyk/XYKSwapSource.kt b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/hydraDx/xyk/XYKSwapSource.kt index 1f461eef18..6173dcd1c3 100644 --- a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/hydraDx/xyk/XYKSwapSource.kt +++ b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/hydraDx/xyk/XYKSwapSource.kt @@ -144,7 +144,7 @@ private class XYKSwapSource( return buildList { this@allPossibleDirections.forEach { poolInfo -> add( - HYKSwapDirection( + XYKSwapDirection( fromAsset = poolInfo.firstAsset, toAsset = poolInfo.secondAsset, poolAddress = poolInfo.poolAddress @@ -152,7 +152,7 @@ private class XYKSwapSource( ) add( - HYKSwapDirection( + XYKSwapDirection( fromAsset = poolInfo.secondAsset, toAsset = poolInfo.firstAsset, poolAddress = poolInfo.poolAddress @@ -162,7 +162,7 @@ private class XYKSwapSource( } } - inner class HYKSwapDirection( + inner class XYKSwapDirection( private val fromAsset: RemoteAndLocalId, private val toAsset: RemoteAndLocalId, private val poolAddress: AccountId @@ -173,6 +173,9 @@ private class XYKSwapSource( override val to: FullChainAssetId = toAsset.second override val standaloneSwapBuilder: HydraDxStandaloneSwapBuilder? = null + override suspend fun debugLabel(): String { + return "XYK" + } override fun routerPoolArgument(): DictEnum.Entry<*> { return DictEnum.Entry("XYK", null) diff --git a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/domain/swap/RealSwapService.kt b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/domain/swap/RealSwapService.kt index e82f916697..2cb74d51bf 100644 --- a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/domain/swap/RealSwapService.kt +++ b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/domain/swap/RealSwapService.kt @@ -48,11 +48,13 @@ import io.novafoundation.nova.feature_swap_impl.data.assetExchange.assetConversi import io.novafoundation.nova.feature_swap_impl.data.assetExchange.hydraDx.HydraDxExchangeFactory import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.types.Balance import io.novafoundation.nova.feature_wallet_api.domain.model.withAmount +import io.novafoundation.nova.feature_wallet_api.presentation.formatters.formatPlanks import io.novafoundation.nova.runtime.ext.assetConversionSupported import io.novafoundation.nova.runtime.ext.fullId import io.novafoundation.nova.runtime.ext.hydraDxSupported import io.novafoundation.nova.runtime.ext.isCommissionAsset import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry +import io.novafoundation.nova.runtime.multiNetwork.asset import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain import io.novafoundation.nova.runtime.multiNetwork.chain.model.ChainId import io.novafoundation.nova.runtime.multiNetwork.chain.model.FullChainAssetId @@ -372,6 +374,8 @@ internal class RealSwapService( } val quotedPaths = paths.mapNotNull { path -> quotePath(path, amount, swapDirection) } + logQuotes(quotedPaths) + if (paths.isEmpty()) { throw SwapQuoteException.NotEnoughLiquidity } @@ -394,52 +398,34 @@ internal class RealSwapService( } } - // TOOD rework path logging -// private suspend fun logQuotes(args: SwapQuoteArgs, quotes: List) { -// val allCandidates = quotes.sortedDescending().map { -// val formattedIn = args.amount.formatPlanks(args.tokenIn.configuration) -// val formattedOut = it.quote.formatPlanks(args.tokenOut.configuration) -// val formattedPath = formatPath(it.path) -// -// "$formattedIn to $formattedOut via $formattedPath" -// }.joinToString(separator = "\n") -// -// Log.d("RealSwapService", "-------- New quote ----------") -// Log.d("RealSwapService", allCandidates) -// Log.d("RealSwapService", "-------- Done quote ----------\n\n\n") -// } -// -// private suspend fun formatPath(path: QuotePath): String { -// val assets = chain.assetsById -// -// return buildString { -// val firstSegment = path.segments.first() -// -// append(assets.getValue(firstSegment.from.assetId).symbol) -// -// append(" -- ${formatSource(firstSegment)} --> ") -// -// append(assets.getValue(firstSegment.to.assetId).symbol) -// -// path.segments.subList(1, path.segments.size).onEach { segment -> -// append(" -- ${formatSource(segment)} --> ") -// -// append(assets.getValue(segment.to.assetId).symbol) -// } -// } -// } -// -// private suspend fun formatSource(segment: QuotePath.Segment): String { -// return buildString { -// append(segment.sourceId) -// -// if (segment.sourceId == StableSwapSourceFactory.ID) { -// val onChainId = segment.sourceParams.getValue("PoolId").toBigInteger() -// val chainAsset = hydraDxAssetIdConverter.toChainAssetOrThrow(chain, onChainId) -// append("[${chainAsset.symbol}]") -// } -// } -// } + private suspend fun logQuotes(quotedTrades: List) { + val allCandidates = quotedTrades.sortedDescending() + .map { trade -> formatTrade(trade) } + .joinToString(separator = "\n") + + Log.d("RealSwapService", "-------- New quote ----------") + Log.d("RealSwapService", allCandidates) + Log.d("RealSwapService", "-------- Done quote ----------\n\n\n") + } + + private suspend fun formatTrade(trade: QuotedTrade): String { + return buildString { + trade.path.onEachIndexed { index, quotedSwapEdge -> + if (index == 0) { + val assetIn = chainRegistry.asset(quotedSwapEdge.edge.from) + val initialAmount = quotedSwapEdge.quotedAmount.formatPlanks(assetIn) + append(initialAmount) + } + + append(" --- "+ quotedSwapEdge.edge.debugLabel() + " ---> ") + + val assetOut = chainRegistry.asset(quotedSwapEdge.edge.to) + val outAmount = quotedSwapEdge.quote.formatPlanks(assetOut) + + append(outAmount) + } + } + } } abstract class BaseSwapGraphEdge( From 9de6ccd7959f598a5e123751571ee19b5325b927 Mon Sep 17 00:00:00 2001 From: Valentun Date: Mon, 7 Oct 2024 17:59:22 +0300 Subject: [PATCH 09/83] Fix merge conflicts with dev --- .../nova/SwapServiceIntegrationTest.kt | 5 +- .../nova/app/di/deps/ComponentHolderModule.kt | 2 +- .../nova/common/utils/KotlinExt.kt | 2 +- .../capability/CustomFeeAvailabilityFacade.kt | 5 + feature-account-impl/build.gradle | 2 +- .../fee/chains/HydrationFeePaymentProvider.kt | 6 +- .../hydra/HydraDxQuoteSharedComputation.kt | 67 ++++ .../HydrationConversionFeePayment.kt | 38 ++- .../utils/HydraDxQuoteSharedComputation.kt | 86 ----- .../di/AccountFeatureComponent.kt | 2 +- .../di/AccountFeatureDependencies.kt | 11 +- .../di/AccountFeatureHolder.kt | 6 +- .../di/modules/CustomFeeModule.kt | 19 +- .../domain/model/SwapGraph.kt | 9 +- .../domain/model/SwapQuote.kt | 18 +- .../domain/model/SwapQuoteArgs.kt | 8 +- .../presentation/model/SwapDirectionModel.kt | 2 +- .../presentation/state/SwapSettings.kt | 2 +- .../presentation/state/SwapSettingsState.kt | 2 +- feature-swap-core/api/.gitignore | 1 + feature-swap-core/api/build.gradle | 41 +++ feature-swap-core/api/consumer-rules.pro | 0 feature-swap-core/api/proguard-rules.pro | 21 ++ .../api/src/main/AndroidManifest.xml | 4 + .../data/network/HydraDxAssetIdConverter.kt | 2 +- .../data/paths/PathQuoter.kt | 28 ++ .../data/paths/model/BestPathQuote.kt | 8 + .../data/paths/model/QuotedEdge.kt | 9 + .../data/paths/model/QuotedPath.kt | 38 +++ .../data/primitive/SwapQuoting.kt | 27 ++ .../primitive/errors}/SwapQuoteException.kt | 2 +- .../data/primitive/model/QuotableEdge.kt | 13 + .../data/primitive}/model/SwapDirection.kt | 2 +- .../data/types/hydra/HydraDxQuoting.kt | 14 + .../data/types/hydra/HydraDxQuotingSource.kt | 25 ++ .../feature_swap_core_api/di/SwapCoreApi.kt | 14 + feature-swap-core/build.gradle | 1 + .../conversion/AssetConversion.kt | 32 -- .../conversion/AssetExchangeQuote.kt | 23 -- .../conversion/AssetExchangeQuoteArgs.kt | 12 - .../types/hydra/HydraDXAssetConversion.kt | 7 - .../types/hydra/HydraDxConversionSource.kt | 40 --- .../conversion/types/hydra/HydraDxSwapEdge.kt | 25 -- .../types/hydra/HydraSwapDirection.kt | 12 - .../types/hydra/MultiTransactionPaymentApi.kt | 2 +- .../types/hydra/RealHydraDxAssetConversion.kt | 217 ------------- .../hydra/RealHydraDxAssetIdConverter.kt | 8 +- .../types/hydra/RealHydraDxQuoting.kt | 79 +++++ .../ConverionSourceBalanceUtils.kt | 4 +- .../omnipool/DynamicFeesApi.kt | 8 +- .../hydra/sources/omnipool/OmniPoolId.kt | 8 + .../sources/omnipool/OmniPoolQuotingSource.kt | 15 + .../{impl => sources}/omnipool/OmnipoolApi.kt | 8 +- .../omnipool/RealOmniPoolQuotingSource.kt} | 151 +++------ .../hydra/sources/omnipool/model/Aliases.kt | 20 ++ .../omnipool/model/DynamicFee.kt | 2 +- .../omnipool/model/OmniPool.kt | 6 +- .../omnipool/model/OmnipoolAssetState.kt | 4 +- .../omnipool/model/Tradeability.kt | 2 +- .../stableswap/AssetRegistryApi.kt | 4 +- .../RealStableSwapQuotingSource.kt} | 115 +++---- .../stableswap/StableSwapApi.kt | 8 +- .../stableswap/StableSwapQuotingSource.kt | 21 ++ .../{impl => sources}/stableswap/TokensApi.kt | 4 +- .../stableswap/model/StablePool.kt | 47 ++- .../stableswap/model/StableSwapPoolInfo.kt | 4 +- .../xyk/RealXYKSwapQuotingSource.kt} | 152 ++++----- .../sources/xyk/StableSwapQuotingSource.kt | 18 ++ .../hydra/{impl => sources}/xyk/XYKApi.kt | 6 +- .../{impl => sources}/xyk/model/XYKFees.kt | 2 +- .../{impl => sources}/xyk/model/XYKPool.kt | 6 +- .../xyk/model/XYKPoolInfo.kt | 4 +- .../network/HydrationExtrinsicBuilderExt.kt | 14 - .../nova/feature_swap_core/di/SwapCoreApi.kt | 11 - .../feature_swap_core/di/SwapCoreComponent.kt | 1 + .../di/SwapCoreDependencies.kt | 4 + .../feature_swap_core/di/SwapCoreModule.kt | 13 +- .../di/conversions/HydraDxConversionModule.kt | 32 +- .../domain/model/QuotePath.kt | 8 - .../domain/paths/RealPathQuoter.kt | 116 +++++++ .../data/assetExchange/AssetExchange.kt | 12 +- .../AssetConversionExchange.kt | 20 +- .../assetExchange/hydraDx/HydraDxExchange.kt | 67 ++-- .../hydraDx/HydraDxSwapSource.kt | 11 +- .../hydraDx/omnipool/OmniPoolSwapSource.kt | 299 ++++-------------- .../hydraDx/stableswap/StableSwapSource.kt | 288 ++--------------- .../data/assetExchange/hydraDx/xyk/XYKApi.kt | 34 -- .../hydraDx/xyk/XYKSwapSource.kt | 186 ++--------- .../hydraDx/xyk/model/XYKFees.kt | 20 -- .../hydraDx/xyk/model/XYKPool.kt | 106 ------- .../hydraDx/xyk/model/XYKPoolInfo.kt | 13 - .../di/SwapFeatureComponent.kt | 2 +- .../di/SwapFeatureDependencies.kt | 11 +- .../feature_swap_impl/di/SwapFeatureHolder.kt | 2 +- .../feature_swap_impl/di/SwapFeatureModule.kt | 9 +- .../di/exchanges/HydraDxExchangeModule.kt | 54 +--- .../domain/swap/RealSwapService.kt | 148 ++------- .../confirmation/SwapConfirmationViewModel.kt | 6 +- .../main/SwapMainSettingsFragment.kt | 2 +- .../main/SwapMainSettingsViewModel.kt | 2 +- .../state/RealSwapSettingsState.kt | 4 +- .../RealSubstrateRealtimeOperationFetcher.kt | 2 +- .../hydraDx/BaseHydraDxSwapExtractor.kt | 14 +- .../hydraDx/HydraDxOmniPoolSwapExtractor.kt | 2 +- .../hydraDx/HydraDxRouterSwapExtractor.kt | 2 +- .../di/WalletFeatureComponent.kt | 2 +- .../di/WalletFeatureDependencies.kt | 4 +- .../di/WalletFeatureHolder.kt | 2 +- .../di/WalletFeatureModule.kt | 2 +- settings.gradle | 1 + 110 files changed, 1188 insertions(+), 1956 deletions(-) create mode 100644 feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/data/fee/types/hydra/HydraDxQuoteSharedComputation.kt rename feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/data/fee/types/{ => hydra}/HydrationConversionFeePayment.kt (66%) delete mode 100644 feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/data/fee/utils/HydraDxQuoteSharedComputation.kt create mode 100644 feature-swap-core/api/.gitignore create mode 100644 feature-swap-core/api/build.gradle create mode 100644 feature-swap-core/api/consumer-rules.pro create mode 100644 feature-swap-core/api/proguard-rules.pro create mode 100644 feature-swap-core/api/src/main/AndroidManifest.xml rename feature-swap-core/{src/main/java/io/novafoundation/nova/feature_swap_core => api/src/main/java/io/novafoundation/nova/feature_swap_core_api}/data/network/HydraDxAssetIdConverter.kt (93%) create mode 100644 feature-swap-core/api/src/main/java/io/novafoundation/nova/feature_swap_core_api/data/paths/PathQuoter.kt create mode 100644 feature-swap-core/api/src/main/java/io/novafoundation/nova/feature_swap_core_api/data/paths/model/BestPathQuote.kt create mode 100644 feature-swap-core/api/src/main/java/io/novafoundation/nova/feature_swap_core_api/data/paths/model/QuotedEdge.kt create mode 100644 feature-swap-core/api/src/main/java/io/novafoundation/nova/feature_swap_core_api/data/paths/model/QuotedPath.kt create mode 100644 feature-swap-core/api/src/main/java/io/novafoundation/nova/feature_swap_core_api/data/primitive/SwapQuoting.kt rename feature-swap-core/{src/main/java/io/novafoundation/nova/feature_swap_core/domain/model => api/src/main/java/io/novafoundation/nova/feature_swap_core_api/data/primitive/errors}/SwapQuoteException.kt (58%) create mode 100644 feature-swap-core/api/src/main/java/io/novafoundation/nova/feature_swap_core_api/data/primitive/model/QuotableEdge.kt rename feature-swap-core/{src/main/java/io/novafoundation/nova/feature_swap_core/domain => api/src/main/java/io/novafoundation/nova/feature_swap_core_api/data/primitive}/model/SwapDirection.kt (78%) create mode 100644 feature-swap-core/api/src/main/java/io/novafoundation/nova/feature_swap_core_api/data/types/hydra/HydraDxQuoting.kt create mode 100644 feature-swap-core/api/src/main/java/io/novafoundation/nova/feature_swap_core_api/data/types/hydra/HydraDxQuotingSource.kt create mode 100644 feature-swap-core/api/src/main/java/io/novafoundation/nova/feature_swap_core_api/di/SwapCoreApi.kt delete mode 100644 feature-swap-core/src/main/java/io/novafoundation/nova/feature_swap_core/data/assetExchange/conversion/AssetConversion.kt delete mode 100644 feature-swap-core/src/main/java/io/novafoundation/nova/feature_swap_core/data/assetExchange/conversion/AssetExchangeQuote.kt delete mode 100644 feature-swap-core/src/main/java/io/novafoundation/nova/feature_swap_core/data/assetExchange/conversion/AssetExchangeQuoteArgs.kt delete mode 100644 feature-swap-core/src/main/java/io/novafoundation/nova/feature_swap_core/data/assetExchange/conversion/types/hydra/HydraDXAssetConversion.kt delete mode 100644 feature-swap-core/src/main/java/io/novafoundation/nova/feature_swap_core/data/assetExchange/conversion/types/hydra/HydraDxConversionSource.kt delete mode 100644 feature-swap-core/src/main/java/io/novafoundation/nova/feature_swap_core/data/assetExchange/conversion/types/hydra/HydraDxSwapEdge.kt delete mode 100644 feature-swap-core/src/main/java/io/novafoundation/nova/feature_swap_core/data/assetExchange/conversion/types/hydra/HydraSwapDirection.kt delete mode 100644 feature-swap-core/src/main/java/io/novafoundation/nova/feature_swap_core/data/assetExchange/conversion/types/hydra/RealHydraDxAssetConversion.kt create mode 100644 feature-swap-core/src/main/java/io/novafoundation/nova/feature_swap_core/data/assetExchange/conversion/types/hydra/RealHydraDxQuoting.kt rename feature-swap-core/src/main/java/io/novafoundation/nova/feature_swap_core/data/assetExchange/conversion/types/hydra/{impl => sources}/ConverionSourceBalanceUtils.kt (98%) rename feature-swap-core/src/main/java/io/novafoundation/nova/feature_swap_core/data/assetExchange/conversion/types/hydra/{impl => sources}/omnipool/DynamicFeesApi.kt (82%) create mode 100644 feature-swap-core/src/main/java/io/novafoundation/nova/feature_swap_core/data/assetExchange/conversion/types/hydra/sources/omnipool/OmniPoolId.kt create mode 100644 feature-swap-core/src/main/java/io/novafoundation/nova/feature_swap_core/data/assetExchange/conversion/types/hydra/sources/omnipool/OmniPoolQuotingSource.kt rename feature-swap-core/src/main/java/io/novafoundation/nova/feature_swap_core/data/assetExchange/conversion/types/hydra/{impl => sources}/omnipool/OmnipoolApi.kt (83%) rename feature-swap-core/src/main/java/io/novafoundation/nova/feature_swap_core/data/assetExchange/conversion/types/hydra/{impl/omnipool/OmniPoolConversionSource.kt => sources/omnipool/RealOmniPoolQuotingSource.kt} (59%) create mode 100644 feature-swap-core/src/main/java/io/novafoundation/nova/feature_swap_core/data/assetExchange/conversion/types/hydra/sources/omnipool/model/Aliases.kt rename feature-swap-core/src/main/java/io/novafoundation/nova/feature_swap_core/data/assetExchange/conversion/types/hydra/{impl => sources}/omnipool/model/DynamicFee.kt (96%) rename feature-swap-core/src/main/java/io/novafoundation/nova/feature_swap_core/data/assetExchange/conversion/types/hydra/{impl => sources}/omnipool/model/OmniPool.kt (93%) rename feature-swap-core/src/main/java/io/novafoundation/nova/feature_swap_core/data/assetExchange/conversion/types/hydra/{impl => sources}/omnipool/model/OmnipoolAssetState.kt (86%) rename feature-swap-core/src/main/java/io/novafoundation/nova/feature_swap_core/data/assetExchange/conversion/types/hydra/{impl => sources}/omnipool/model/Tradeability.kt (94%) rename feature-swap-core/src/main/java/io/novafoundation/nova/feature_swap_core/data/assetExchange/conversion/types/hydra/{impl => sources}/stableswap/AssetRegistryApi.kt (91%) rename feature-swap-core/src/main/java/io/novafoundation/nova/feature_swap_core/data/assetExchange/conversion/types/hydra/{impl/stableswap/StableConversionSource.kt => sources/stableswap/RealStableSwapQuotingSource.kt} (76%) rename feature-swap-core/src/main/java/io/novafoundation/nova/feature_swap_core/data/assetExchange/conversion/types/hydra/{impl => sources}/stableswap/StableSwapApi.kt (83%) create mode 100644 feature-swap-core/src/main/java/io/novafoundation/nova/feature_swap_core/data/assetExchange/conversion/types/hydra/sources/stableswap/StableSwapQuotingSource.kt rename feature-swap-core/src/main/java/io/novafoundation/nova/feature_swap_core/data/assetExchange/conversion/types/hydra/{impl => sources}/stableswap/TokensApi.kt (93%) rename feature-swap-core/src/main/java/io/novafoundation/nova/feature_swap_core/data/assetExchange/conversion/types/hydra/{impl => sources}/stableswap/model/StablePool.kt (86%) rename feature-swap-core/src/main/java/io/novafoundation/nova/feature_swap_core/data/assetExchange/conversion/types/hydra/{impl => sources}/stableswap/model/StableSwapPoolInfo.kt (90%) rename feature-swap-core/src/main/java/io/novafoundation/nova/feature_swap_core/data/assetExchange/conversion/types/hydra/{impl/xyk/XYKConversionSource.kt => sources/xyk/RealXYKSwapQuotingSource.kt} (58%) create mode 100644 feature-swap-core/src/main/java/io/novafoundation/nova/feature_swap_core/data/assetExchange/conversion/types/hydra/sources/xyk/StableSwapQuotingSource.kt rename feature-swap-core/src/main/java/io/novafoundation/nova/feature_swap_core/data/assetExchange/conversion/types/hydra/{impl => sources}/xyk/XYKApi.kt (90%) rename feature-swap-core/src/main/java/io/novafoundation/nova/feature_swap_core/data/assetExchange/conversion/types/hydra/{impl => sources}/xyk/model/XYKFees.kt (94%) rename feature-swap-core/src/main/java/io/novafoundation/nova/feature_swap_core/data/assetExchange/conversion/types/hydra/{impl => sources}/xyk/model/XYKPool.kt (93%) rename feature-swap-core/src/main/java/io/novafoundation/nova/feature_swap_core/data/assetExchange/conversion/types/hydra/{impl => sources}/xyk/model/XYKPoolInfo.kt (78%) delete mode 100644 feature-swap-core/src/main/java/io/novafoundation/nova/feature_swap_core/data/network/HydrationExtrinsicBuilderExt.kt delete mode 100644 feature-swap-core/src/main/java/io/novafoundation/nova/feature_swap_core/di/SwapCoreApi.kt delete mode 100644 feature-swap-core/src/main/java/io/novafoundation/nova/feature_swap_core/domain/model/QuotePath.kt create mode 100644 feature-swap-core/src/main/java/io/novafoundation/nova/feature_swap_core/domain/paths/RealPathQuoter.kt delete mode 100644 feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/hydraDx/xyk/XYKApi.kt delete mode 100644 feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/hydraDx/xyk/model/XYKFees.kt delete mode 100644 feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/hydraDx/xyk/model/XYKPool.kt delete mode 100644 feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/hydraDx/xyk/model/XYKPoolInfo.kt diff --git a/app/src/androidTest/java/io/novafoundation/nova/SwapServiceIntegrationTest.kt b/app/src/androidTest/java/io/novafoundation/nova/SwapServiceIntegrationTest.kt index 2ddfdc3e3e..2d589120dd 100644 --- a/app/src/androidTest/java/io/novafoundation/nova/SwapServiceIntegrationTest.kt +++ b/app/src/androidTest/java/io/novafoundation/nova/SwapServiceIntegrationTest.kt @@ -8,10 +8,9 @@ import io.novafoundation.nova.feature_swap_api.domain.model.SwapExecuteArgs import io.novafoundation.nova.feature_swap_api.domain.model.SwapLimit import io.novafoundation.nova.feature_swap_api.domain.model.SwapQuote import io.novafoundation.nova.feature_swap_api.domain.model.SwapQuoteArgs -import io.novafoundation.nova.feature_swap_core.domain.model.SwapQuoteException +import io.novafoundation.nova.feature_swap_core_api.data.primitive.errors.SwapQuoteException import io.novafoundation.nova.feature_swap_api.domain.model.swapRate -import io.novafoundation.nova.feature_swap_core.domain.model.QuotePath -import io.novafoundation.nova.feature_swap_core.domain.model.SwapDirection +import io.novafoundation.nova.feature_swap_core_api.data.primitive.model.SwapDirection import io.novafoundation.nova.feature_swap_impl.di.SwapFeatureComponent import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.types.Balance import io.novafoundation.nova.feature_wallet_api.di.WalletFeatureApi diff --git a/app/src/main/java/io/novafoundation/nova/app/di/deps/ComponentHolderModule.kt b/app/src/main/java/io/novafoundation/nova/app/di/deps/ComponentHolderModule.kt index 6189fc1e74..b5dbda7cc7 100644 --- a/app/src/main/java/io/novafoundation/nova/app/di/deps/ComponentHolderModule.kt +++ b/app/src/main/java/io/novafoundation/nova/app/di/deps/ComponentHolderModule.kt @@ -51,8 +51,8 @@ import io.novafoundation.nova.feature_settings_impl.di.SettingsFeatureHolder import io.novafoundation.nova.feature_staking_api.di.StakingFeatureApi import io.novafoundation.nova.feature_staking_impl.di.StakingFeatureHolder import io.novafoundation.nova.feature_swap_api.di.SwapFeatureApi -import io.novafoundation.nova.feature_swap_core.di.SwapCoreApi import io.novafoundation.nova.feature_swap_core.di.SwapCoreHolder +import io.novafoundation.nova.feature_swap_core_api.di.SwapCoreApi import io.novafoundation.nova.feature_swap_impl.di.SwapFeatureHolder import io.novafoundation.nova.feature_versions_api.di.VersionsFeatureApi import io.novafoundation.nova.feature_versions_impl.di.VersionsFeatureHolder diff --git a/common/src/main/java/io/novafoundation/nova/common/utils/KotlinExt.kt b/common/src/main/java/io/novafoundation/nova/common/utils/KotlinExt.kt index c977d16ba5..b5a416ada5 100644 --- a/common/src/main/java/io/novafoundation/nova/common/utils/KotlinExt.kt +++ b/common/src/main/java/io/novafoundation/nova/common/utils/KotlinExt.kt @@ -109,7 +109,7 @@ suspend fun Iterable.mapAsync(operation: suspend (T) -> R): List { }.awaitAll() } -suspend fun Iterable.flatMapAsync(operation: suspend (T) -> List): List { +suspend fun Iterable.flatMapAsync(operation: suspend (T) -> Collection): List { return coroutineScope { map { async { operation(it) } } }.awaitAll().flatten() diff --git a/feature-account-api/src/main/java/io/novafoundation/nova/feature_account_api/data/fee/capability/CustomFeeAvailabilityFacade.kt b/feature-account-api/src/main/java/io/novafoundation/nova/feature_account_api/data/fee/capability/CustomFeeAvailabilityFacade.kt index 0d4dbfc609..abfe77cc1c 100644 --- a/feature-account-api/src/main/java/io/novafoundation/nova/feature_account_api/data/fee/capability/CustomFeeAvailabilityFacade.kt +++ b/feature-account-api/src/main/java/io/novafoundation/nova/feature_account_api/data/fee/capability/CustomFeeAvailabilityFacade.kt @@ -4,6 +4,11 @@ import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain interface CustomFeeCapability { + /** + * Implementations should expect `asset` to be non-utility asset, + * e.g. they don't need to additionally check whether asset is utility or not + * They can also expect this method is called only when asset is present in [AssetExchange.availableDirectSwapConnections] + */ suspend fun canPayFeeInNonUtilityToken(chainAsset: Chain.Asset): Boolean } diff --git a/feature-account-impl/build.gradle b/feature-account-impl/build.gradle index e3ca62dd1b..537e8bbab2 100644 --- a/feature-account-impl/build.gradle +++ b/feature-account-impl/build.gradle @@ -45,7 +45,7 @@ dependencies { implementation project(':feature-versions-api') implementation project(':feature-proxy-api') implementation project(':feature-cloud-backup-api') - implementation project(":feature-swap-core") + implementation project(":feature-swap-core:api") implementation project(':web3names') implementation kotlinDep diff --git a/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/data/fee/chains/HydrationFeePaymentProvider.kt b/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/data/fee/chains/HydrationFeePaymentProvider.kt index 8b4b18625c..1f53209489 100644 --- a/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/data/fee/chains/HydrationFeePaymentProvider.kt +++ b/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/data/fee/chains/HydrationFeePaymentProvider.kt @@ -4,10 +4,10 @@ import io.novafoundation.nova.feature_account_api.data.fee.FeePayment import io.novafoundation.nova.feature_account_api.data.fee.FeePaymentCurrency import io.novafoundation.nova.feature_account_api.data.fee.FeePaymentProvider import io.novafoundation.nova.feature_account_api.domain.interfaces.AccountRepository -import io.novafoundation.nova.feature_account_impl.data.fee.types.HydrationConversionFeePayment import io.novafoundation.nova.feature_account_impl.data.fee.types.NativeFeePayment -import io.novafoundation.nova.feature_account_impl.data.fee.utils.HydraDxQuoteSharedComputation -import io.novafoundation.nova.feature_swap_core.data.network.HydraDxAssetIdConverter +import io.novafoundation.nova.feature_account_impl.data.fee.types.hydra.HydraDxQuoteSharedComputation +import io.novafoundation.nova.feature_account_impl.data.fee.types.hydra.HydrationConversionFeePayment +import io.novafoundation.nova.feature_swap_core_api.data.network.HydraDxAssetIdConverter import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry import kotlinx.coroutines.CoroutineScope diff --git a/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/data/fee/types/hydra/HydraDxQuoteSharedComputation.kt b/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/data/fee/types/hydra/HydraDxQuoteSharedComputation.kt new file mode 100644 index 0000000000..0d712270ff --- /dev/null +++ b/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/data/fee/types/hydra/HydraDxQuoteSharedComputation.kt @@ -0,0 +1,67 @@ +package io.novafoundation.nova.feature_account_impl.data.fee.types.hydra + +import io.novafoundation.nova.common.data.memory.ComputationalCache +import io.novafoundation.nova.common.utils.graph.Graph +import io.novafoundation.nova.common.utils.graph.create +import io.novafoundation.nova.feature_swap_core_api.data.paths.PathQuoter +import io.novafoundation.nova.feature_swap_core_api.data.primitive.SwapQuoting +import io.novafoundation.nova.feature_swap_core_api.data.primitive.model.QuotableEdge +import io.novafoundation.nova.feature_swap_core_api.data.types.hydra.HydraDxQuoting +import io.novafoundation.nova.runtime.ethereum.StorageSharedRequestsBuilderFactory +import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain +import io.novafoundation.nova.runtime.network.updaters.BlockNumberUpdater +import io.novasama.substrate_sdk_android.extensions.toHexString +import io.novasama.substrate_sdk_android.runtime.AccountId +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.launchIn + +class HydraDxQuoteSharedComputation( + private val computationalCache: ComputationalCache, + private val quotingFactory: HydraDxQuoting.Factory, + private val pathQuoterFactory: PathQuoter.Factory, + private val storageSharedRequestsBuilderFactory: StorageSharedRequestsBuilderFactory, + private val blockNumberUpdater: BlockNumberUpdater +) { + + suspend fun getQuoter( + chain: Chain, + accountId: AccountId, + scope: CoroutineScope + ): PathQuoter { + val key = "HydraDxQuoter:${chain.id}:${accountId.toHexString()}" + + return computationalCache.useCache(key, scope) { + val assetConversion = getSwapQuoting(chain, accountId, scope) + val edges = assetConversion.availableSwapDirections() + val graph = Graph.create(edges) + + pathQuoterFactory.create(graph, scope) + } + } + + + suspend fun getSwapQuoting( + chain: Chain, + accountId: AccountId, + scope: CoroutineScope + ): SwapQuoting { + val key = "HydraDxAssetConversion:${chain.id}:${accountId.toHexString()}" + + return computationalCache.useCache(key, scope) { + val subscriptionBuilder = storageSharedRequestsBuilderFactory.create(chain.id) + val hydraDxQuoting = quotingFactory.create(chain) + + // Required at least for stable swap + blockNumberUpdater.listenForUpdates(subscriptionBuilder, chain) + .launchIn(this) + + hydraDxQuoting.sync() + hydraDxQuoting.runSubscriptions(accountId, subscriptionBuilder) + .launchIn(this) + + subscriptionBuilder.subscribe(this) + + hydraDxQuoting + } + } +} diff --git a/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/data/fee/types/HydrationConversionFeePayment.kt b/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/data/fee/types/hydra/HydrationConversionFeePayment.kt similarity index 66% rename from feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/data/fee/types/HydrationConversionFeePayment.kt rename to feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/data/fee/types/hydra/HydrationConversionFeePayment.kt index c5a608a0a0..a537f4b630 100644 --- a/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/data/fee/types/HydrationConversionFeePayment.kt +++ b/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/data/fee/types/hydra/HydrationConversionFeePayment.kt @@ -1,16 +1,16 @@ -package io.novafoundation.nova.feature_account_impl.data.fee.types +package io.novafoundation.nova.feature_account_impl.data.fee.types.hydra +import io.novafoundation.nova.common.utils.Modules import io.novafoundation.nova.feature_account_api.data.fee.FeePayment 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.domain.interfaces.AccountRepository import io.novafoundation.nova.feature_account_api.domain.model.requireAccountIdIn -import io.novafoundation.nova.feature_account_impl.data.fee.utils.HydraDxQuoteSharedComputation -import io.novafoundation.nova.feature_swap_core.data.assetExchange.conversion.AssetExchangeQuoteArgs -import io.novafoundation.nova.feature_swap_core.data.network.HydraDxAssetIdConverter -import io.novafoundation.nova.feature_swap_core.data.network.setFeeCurrency -import io.novafoundation.nova.feature_swap_core.data.network.toOnChainIdOrThrow -import io.novafoundation.nova.feature_swap_core.domain.model.SwapDirection +import io.novafoundation.nova.feature_swap_core_api.data.network.HydraDxAssetId +import io.novafoundation.nova.feature_swap_core_api.data.network.HydraDxAssetIdConverter +import io.novafoundation.nova.feature_swap_core_api.data.network.toOnChainIdOrThrow +import io.novafoundation.nova.feature_swap_core_api.data.paths.model.quote +import io.novafoundation.nova.feature_swap_core_api.data.primitive.model.SwapDirection import io.novafoundation.nova.runtime.ext.commissionAsset import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain @@ -41,17 +41,19 @@ internal class HydrationConversionFeePayment( val accountId = metaAccount.requireAccountIdIn(chain) val fromAsset = chain.commissionAsset - val args = AssetExchangeQuoteArgs( + val quoter = hydraDxQuoteSharedComputation.getQuoter(chain, accountId, coroutineScope) + val quote = quoter.findBestPath( chainAssetIn = fromAsset, chainAssetOut = paymentAsset, amount = nativeFee.amount, swapDirection = SwapDirection.SPECIFIED_IN ) - val assetConversion = hydraDxQuoteSharedComputation.getAssetConversion(chain, accountId, coroutineScope) - val paths = hydraDxQuoteSharedComputation.paths(chain, args, accountId, coroutineScope) - val quote = assetConversion.quote(paths, args) - return SubstrateFee(quote.quote, nativeFee.submissionOrigin, paymentAsset) + return SubstrateFee( + amount = quote.bestPath.quote, + submissionOrigin = nativeFee.submissionOrigin, + asset = paymentAsset + ) } override suspend fun canPayFeeInNonUtilityToken(chainAsset: Chain.Asset): Boolean { @@ -59,7 +61,17 @@ internal class HydrationConversionFeePayment( val chain = chainRegistry.getChain(paymentAsset.chainId) val accountId = metaAccount.requireAccountIdIn(chain) - val assetConversion = hydraDxQuoteSharedComputation.getAssetConversion(chain, accountId, coroutineScope) + val assetConversion = hydraDxQuoteSharedComputation.getSwapQuoting(chain, accountId, coroutineScope) return assetConversion.canPayFeeInNonUtilityToken(paymentAsset) } + + private fun ExtrinsicBuilder.setFeeCurrency(onChainId: HydraDxAssetId) { + call( + moduleName = Modules.MULTI_TRANSACTION_PAYMENT, + callName = "set_currency", + arguments = mapOf( + "currency" to onChainId + ) + ) + } } diff --git a/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/data/fee/utils/HydraDxQuoteSharedComputation.kt b/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/data/fee/utils/HydraDxQuoteSharedComputation.kt deleted file mode 100644 index 56aba43172..0000000000 --- a/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/data/fee/utils/HydraDxQuoteSharedComputation.kt +++ /dev/null @@ -1,86 +0,0 @@ -package io.novafoundation.nova.feature_account_impl.data.fee.utils - -import io.novafoundation.nova.common.data.memory.ComputationalCache -import io.novafoundation.nova.common.utils.graph.Graph -import io.novafoundation.nova.common.utils.throttleLast -import io.novafoundation.nova.common.utils.graph.Path -import io.novafoundation.nova.feature_swap_core.data.assetExchange.conversion.AssetConversion -import io.novafoundation.nova.feature_swap_core.data.assetExchange.conversion.AssetExchangeQuoteArgs -import io.novafoundation.nova.feature_swap_core.data.assetExchange.conversion.types.hydra.HydraDxAssetConversionFactory -import io.novafoundation.nova.feature_swap_core.data.assetExchange.conversion.types.hydra.HydraDxSwapEdge -import io.novafoundation.nova.runtime.ethereum.StorageSharedRequestsBuilderFactory -import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain -import io.novafoundation.nova.runtime.multiNetwork.chain.model.FullChainAssetId -import io.novafoundation.nova.runtime.network.updaters.BlockNumberUpdater -import io.novasama.substrate_sdk_android.extensions.toHexString -import io.novasama.substrate_sdk_android.runtime.AccountId -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.flow.launchIn -import kotlin.time.Duration.Companion.milliseconds - -class HydraDxQuoteSharedComputation( - private val computationalCache: ComputationalCache, - private val assetConversionFactory: HydraDxAssetConversionFactory, - private val storageSharedRequestsBuilderFactory: StorageSharedRequestsBuilderFactory, - private val blockNumberUpdater: BlockNumberUpdater -) { - - suspend fun directions( - chain: Chain, - accountId: AccountId, - scope: CoroutineScope - ): Graph { - val key = "HydraDxDirections:${chain.id}:${accountId.toHexString()}" - - return computationalCache.useCache(key, scope) { - val assetConversion = getAssetConversion(chain, accountId, scope) - - assetConversion.availableSwapDirections() - } - } - - suspend fun paths( - chain: Chain, - args: AssetExchangeQuoteArgs, - accountId: AccountId, - scope: CoroutineScope - ): List> { - val key = "HydraDxPaths:${chain.id}:${argsToKey(args)}:${accountId.toHexString()}" - - return computationalCache.useCache(key, scope) { - val assetConversion = getAssetConversion(chain, accountId, scope) - val swapDirections = directions(chain, accountId, scope) - assetConversion.getPaths(swapDirections, args) - } - } - - suspend fun getAssetConversion( - chain: Chain, - accountId: AccountId, - scope: CoroutineScope - ): AssetConversion { - val key = "HydraDxAssetConversion:${chain.id}:${accountId.toHexString()}" - - return computationalCache.useCache(key, scope) { - val subscriptionBuilder = storageSharedRequestsBuilderFactory.create(chain.id) - val assetConversion = assetConversionFactory.create(chain) - - // Required at least for stable swap - blockNumberUpdater.listenForUpdates(subscriptionBuilder, chain) - .launchIn(this) - - assetConversion.sync() - assetConversion.runSubscriptions(accountId, subscriptionBuilder) - .throttleLast(500.milliseconds) - .launchIn(this) - - subscriptionBuilder.subscribe(this) - - assetConversion - } - } - - private fun argsToKey(args: AssetExchangeQuoteArgs) = args.apply { - "${chainAssetIn.id}:${chainAssetOut.id}:${swapDirection.name}" - } -} diff --git a/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/di/AccountFeatureComponent.kt b/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/di/AccountFeatureComponent.kt index 74adc28837..c6f99c58d0 100644 --- a/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/di/AccountFeatureComponent.kt +++ b/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/di/AccountFeatureComponent.kt @@ -60,7 +60,7 @@ import io.novafoundation.nova.feature_cloud_backup_api.di.CloudBackupFeatureApi import io.novafoundation.nova.feature_currency_api.di.CurrencyFeatureApi import io.novafoundation.nova.feature_ledger_core.di.LedgerCoreApi import io.novafoundation.nova.feature_proxy_api.di.ProxyFeatureApi -import io.novafoundation.nova.feature_swap_core.di.SwapCoreApi +import io.novafoundation.nova.feature_swap_core_api.di.SwapCoreApi import io.novafoundation.nova.feature_versions_api.di.VersionsFeatureApi import io.novafoundation.nova.runtime.di.RuntimeApi import io.novafoundation.nova.web3names.di.Web3NamesApi diff --git a/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/di/AccountFeatureDependencies.kt b/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/di/AccountFeatureDependencies.kt index 816a7850df..ccde5dec8c 100644 --- a/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/di/AccountFeatureDependencies.kt +++ b/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/di/AccountFeatureDependencies.kt @@ -42,8 +42,9 @@ import io.novafoundation.nova.feature_currency_api.domain.CurrencyInteractor import io.novafoundation.nova.feature_currency_api.domain.interfaces.CurrencyRepository import io.novafoundation.nova.feature_ledger_core.domain.LedgerMigrationTracker import io.novafoundation.nova.feature_proxy_api.data.repository.GetProxyRepository -import io.novafoundation.nova.feature_swap_core.data.assetExchange.conversion.types.hydra.HydraDxAssetConversionFactory -import io.novafoundation.nova.feature_swap_core.data.network.HydraDxAssetIdConverter +import io.novafoundation.nova.feature_swap_core_api.data.network.HydraDxAssetIdConverter +import io.novafoundation.nova.feature_swap_core_api.data.paths.PathQuoter +import io.novafoundation.nova.feature_swap_core_api.data.types.hydra.HydraDxQuoting import io.novafoundation.nova.feature_versions_api.domain.UpdateNotificationsInteractor import io.novafoundation.nova.runtime.call.MultiChainRuntimeCallsApi import io.novafoundation.nova.runtime.di.REMOTE_STORAGE_SOURCE @@ -144,12 +145,14 @@ interface AccountFeatureDependencies { fun multiChainRuntimeCallsApi(): MultiChainRuntimeCallsApi - fun hydraDxAssetConversionFactory(): HydraDxAssetConversionFactory + val hydraDxAssetConversionFactory: HydraDxQuoting.Factory + + val pathQuoterFactory: PathQuoter.Factory @Named(REMOTE_STORAGE_SOURCE) fun remoteStorageSource(): StorageDataSource - fun hydraDxAssetIdConverter(): HydraDxAssetIdConverter + val hydraDxAssetIdConverter: HydraDxAssetIdConverter val systemCallExecutor: SystemCallExecutor diff --git a/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/di/AccountFeatureHolder.kt b/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/di/AccountFeatureHolder.kt index 9975ffb27a..df2de10c08 100644 --- a/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/di/AccountFeatureHolder.kt +++ b/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/di/AccountFeatureHolder.kt @@ -6,19 +6,19 @@ import io.novafoundation.nova.common.di.scope.ApplicationScope import io.novafoundation.nova.common.sequrity.verification.PinCodeTwoFactorVerificationCommunicator import io.novafoundation.nova.core_db.di.DbApi import io.novafoundation.nova.feature_account_api.presenatation.account.wallet.list.SelectMultipleWalletsCommunicator -import io.novafoundation.nova.feature_account_api.presenatation.mixin.selectAddress.SelectAddressCommunicator import io.novafoundation.nova.feature_account_api.presenatation.cloudBackup.changePassword.ChangeBackupPasswordCommunicator import io.novafoundation.nova.feature_account_api.presenatation.cloudBackup.changePassword.RestoreBackupPasswordCommunicator +import io.novafoundation.nova.feature_account_api.presenatation.cloudBackup.createPassword.SyncWalletsBackupPasswordCommunicator +import io.novafoundation.nova.feature_account_api.presenatation.mixin.selectAddress.SelectAddressCommunicator import io.novafoundation.nova.feature_account_api.presenatation.mixin.selectWallet.SelectWalletCommunicator import io.novafoundation.nova.feature_account_api.presenatation.sign.LedgerSignCommunicator import io.novafoundation.nova.feature_account_impl.data.signer.paritySigner.PolkadotVaultVariantSignCommunicator import io.novafoundation.nova.feature_account_impl.presentation.AccountRouter -import io.novafoundation.nova.feature_account_api.presenatation.cloudBackup.createPassword.SyncWalletsBackupPasswordCommunicator import io.novafoundation.nova.feature_cloud_backup_api.di.CloudBackupFeatureApi import io.novafoundation.nova.feature_currency_api.di.CurrencyFeatureApi import io.novafoundation.nova.feature_ledger_core.di.LedgerCoreApi import io.novafoundation.nova.feature_proxy_api.di.ProxyFeatureApi -import io.novafoundation.nova.feature_swap_core.di.SwapCoreApi +import io.novafoundation.nova.feature_swap_core_api.di.SwapCoreApi import io.novafoundation.nova.feature_versions_api.di.VersionsFeatureApi import io.novafoundation.nova.runtime.di.RuntimeApi import io.novafoundation.nova.web3names.di.Web3NamesApi diff --git a/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/di/modules/CustomFeeModule.kt b/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/di/modules/CustomFeeModule.kt index c66590189f..c86d59e546 100644 --- a/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/di/modules/CustomFeeModule.kt +++ b/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/di/modules/CustomFeeModule.kt @@ -11,9 +11,10 @@ import io.novafoundation.nova.feature_account_impl.data.fee.RealFeePaymentProvid import io.novafoundation.nova.feature_account_impl.data.fee.chains.AssetHubFeePaymentProvider import io.novafoundation.nova.feature_account_impl.data.fee.chains.DefaultFeePaymentProvider import io.novafoundation.nova.feature_account_impl.data.fee.chains.HydrationFeePaymentProvider -import io.novafoundation.nova.feature_account_impl.data.fee.utils.HydraDxQuoteSharedComputation -import io.novafoundation.nova.feature_swap_core.data.assetExchange.conversion.types.hydra.HydraDxAssetConversionFactory -import io.novafoundation.nova.feature_swap_core.data.network.HydraDxAssetIdConverter +import io.novafoundation.nova.feature_account_impl.data.fee.types.hydra.HydraDxQuoteSharedComputation +import io.novafoundation.nova.feature_swap_core_api.data.network.HydraDxAssetIdConverter +import io.novafoundation.nova.feature_swap_core_api.data.paths.PathQuoter +import io.novafoundation.nova.feature_swap_core_api.data.types.hydra.HydraDxQuoting import io.novafoundation.nova.runtime.call.MultiChainRuntimeCallsApi import io.novafoundation.nova.runtime.di.REMOTE_STORAGE_SOURCE import io.novafoundation.nova.runtime.ethereum.StorageSharedRequestsBuilderFactory @@ -42,15 +43,17 @@ class CustomFeeModule { @FeatureScope fun provideHydraDxQuoteSharedComputation( computationalCache: ComputationalCache, - assetConversionFactory: HydraDxAssetConversionFactory, + quotingFactory: HydraDxQuoting.Factory, + pathQuoterFactory: PathQuoter.Factory, storageSharedRequestsBuilderFactory: StorageSharedRequestsBuilderFactory, blockNumberUpdater: BlockNumberUpdater ): HydraDxQuoteSharedComputation { return HydraDxQuoteSharedComputation( - computationalCache, - assetConversionFactory, - storageSharedRequestsBuilderFactory, - blockNumberUpdater + computationalCache = computationalCache, + quotingFactory = quotingFactory, + pathQuoterFactory = pathQuoterFactory, + storageSharedRequestsBuilderFactory = storageSharedRequestsBuilderFactory, + blockNumberUpdater = blockNumberUpdater ) } diff --git a/feature-swap-api/src/main/java/io/novafoundation/nova/feature_swap_api/domain/model/SwapGraph.kt b/feature-swap-api/src/main/java/io/novafoundation/nova/feature_swap_api/domain/model/SwapGraph.kt index 52e586deef..beacafcc27 100644 --- a/feature-swap-api/src/main/java/io/novafoundation/nova/feature_swap_api/domain/model/SwapGraph.kt +++ b/feature-swap-api/src/main/java/io/novafoundation/nova/feature_swap_api/domain/model/SwapGraph.kt @@ -1,9 +1,8 @@ package io.novafoundation.nova.feature_swap_api.domain.model -import io.novafoundation.nova.common.utils.graph.Edge import io.novafoundation.nova.common.utils.graph.Graph import io.novafoundation.nova.common.utils.graph.Path -import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.types.Balance +import io.novafoundation.nova.feature_swap_core_api.data.primitive.model.QuotableEdge import io.novafoundation.nova.runtime.multiNetwork.chain.model.FullChainAssetId interface SwapGraphEdge : QuotableEdge { @@ -20,13 +19,7 @@ interface SwapGraphEdge : QuotableEdge { suspend fun debugLabel(): String } -interface QuotableEdge : Edge { - suspend fun quote( - amount: Balance, - direction: SwapDirection - ): Balance -} typealias SwapGraph = Graph typealias SwapPath = Path diff --git a/feature-swap-api/src/main/java/io/novafoundation/nova/feature_swap_api/domain/model/SwapQuote.kt b/feature-swap-api/src/main/java/io/novafoundation/nova/feature_swap_api/domain/model/SwapQuote.kt index 157af7cd42..ade4b47f50 100644 --- a/feature-swap-api/src/main/java/io/novafoundation/nova/feature_swap_api/domain/model/SwapQuote.kt +++ b/feature-swap-api/src/main/java/io/novafoundation/nova/feature_swap_api/domain/model/SwapQuote.kt @@ -1,11 +1,10 @@ package io.novafoundation.nova.feature_swap_api.domain.model import io.novafoundation.nova.common.utils.Percent -import io.novafoundation.nova.common.utils.graph.Path import io.novafoundation.nova.feature_account_api.data.model.Fee import io.novafoundation.nova.feature_account_api.data.model.amountByRequestedAccount -import io.novafoundation.nova.feature_swap_core.domain.model.QuotePath -import io.novafoundation.nova.feature_swap_core.domain.model.SwapDirection +import io.novafoundation.nova.feature_swap_core_api.data.paths.model.QuotedPath +import io.novafoundation.nova.feature_swap_core_api.data.primitive.model.SwapDirection import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.types.Balance import io.novafoundation.nova.feature_wallet_api.domain.model.ChainAssetWithAmount import io.novafoundation.nova.feature_wallet_api.domain.model.amountFromPlanks @@ -16,9 +15,8 @@ import java.math.BigDecimal data class SwapQuote( val amountIn: ChainAssetWithAmount, val amountOut: ChainAssetWithAmount, - val direction: SwapDirection, val priceImpact: Percent, - val path: Path + val quotedPath: QuotedPath ) { val assetIn: Chain.Asset @@ -40,21 +38,15 @@ data class SwapQuote( } } -class QuotedSwapEdge( - val quotedAmount: Balance, - val quote: Balance, - val edge: SwapGraphEdge -) - val SwapQuote.editedBalance: Balance - get() = when (direction) { + get() = when (quotedPath.direction) { SwapDirection.SPECIFIED_IN -> planksIn SwapDirection.SPECIFIED_OUT -> planksOut } val SwapQuote.quotedBalance: Balance - get() = when (direction) { + get() = when (quotedPath.direction) { SwapDirection.SPECIFIED_IN -> planksOut SwapDirection.SPECIFIED_OUT -> planksIn } diff --git a/feature-swap-api/src/main/java/io/novafoundation/nova/feature_swap_api/domain/model/SwapQuoteArgs.kt b/feature-swap-api/src/main/java/io/novafoundation/nova/feature_swap_api/domain/model/SwapQuoteArgs.kt index 9efda6e0db..e1a3fb5c31 100644 --- a/feature-swap-api/src/main/java/io/novafoundation/nova/feature_swap_api/domain/model/SwapQuoteArgs.kt +++ b/feature-swap-api/src/main/java/io/novafoundation/nova/feature_swap_api/domain/model/SwapQuoteArgs.kt @@ -3,6 +3,8 @@ package io.novafoundation.nova.feature_swap_api.domain.model import io.novafoundation.nova.common.utils.Percent import io.novafoundation.nova.common.utils.fraction import io.novafoundation.nova.common.utils.graph.Path +import io.novafoundation.nova.feature_swap_core_api.data.paths.model.QuotedEdge +import io.novafoundation.nova.feature_swap_core_api.data.primitive.model.SwapDirection import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.types.Balance import io.novafoundation.nova.feature_wallet_api.domain.model.Token import java.math.BigDecimal @@ -21,7 +23,7 @@ class SwapExecuteArgs( ) class SegmentExecuteArgs( - val quotedSwapEdge: QuotedSwapEdge, + val quotedSwapEdge: QuotedEdge, ) sealed class SwapLimit { @@ -42,8 +44,8 @@ sealed class SwapLimit { fun SwapQuote.toExecuteArgs(slippage: Percent): SwapExecuteArgs { return SwapExecuteArgs( slippage = slippage, - direction = direction, - executionPath = path.map { quotedSwapEdge -> SegmentExecuteArgs(quotedSwapEdge) } + direction = quotedPath.direction, + executionPath = quotedPath.path.map { quotedSwapEdge -> SegmentExecuteArgs(quotedSwapEdge) } ) } diff --git a/feature-swap-api/src/main/java/io/novafoundation/nova/feature_swap_api/presentation/model/SwapDirectionModel.kt b/feature-swap-api/src/main/java/io/novafoundation/nova/feature_swap_api/presentation/model/SwapDirectionModel.kt index 42528b41ce..093a3e7e76 100644 --- a/feature-swap-api/src/main/java/io/novafoundation/nova/feature_swap_api/presentation/model/SwapDirectionModel.kt +++ b/feature-swap-api/src/main/java/io/novafoundation/nova/feature_swap_api/presentation/model/SwapDirectionModel.kt @@ -1,7 +1,7 @@ package io.novafoundation.nova.feature_swap_api.presentation.model import android.os.Parcelable -import io.novafoundation.nova.feature_swap_core.domain.model.SwapDirection +import io.novafoundation.nova.feature_swap_core_api.data.primitive.model.SwapDirection import kotlinx.android.parcel.Parcelize @Parcelize diff --git a/feature-swap-api/src/main/java/io/novafoundation/nova/feature_swap_api/presentation/state/SwapSettings.kt b/feature-swap-api/src/main/java/io/novafoundation/nova/feature_swap_api/presentation/state/SwapSettings.kt index 57d5f0dea4..dd0ec0d98e 100644 --- a/feature-swap-api/src/main/java/io/novafoundation/nova/feature_swap_api/presentation/state/SwapSettings.kt +++ b/feature-swap-api/src/main/java/io/novafoundation/nova/feature_swap_api/presentation/state/SwapSettings.kt @@ -1,7 +1,7 @@ package io.novafoundation.nova.feature_swap_api.presentation.state import io.novafoundation.nova.common.utils.Percent -import io.novafoundation.nova.feature_swap_core.domain.model.SwapDirection +import io.novafoundation.nova.feature_swap_core_api.data.primitive.model.SwapDirection import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.types.Balance import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain diff --git a/feature-swap-api/src/main/java/io/novafoundation/nova/feature_swap_api/presentation/state/SwapSettingsState.kt b/feature-swap-api/src/main/java/io/novafoundation/nova/feature_swap_api/presentation/state/SwapSettingsState.kt index 576b9f526f..e1a0b3fe31 100644 --- a/feature-swap-api/src/main/java/io/novafoundation/nova/feature_swap_api/presentation/state/SwapSettingsState.kt +++ b/feature-swap-api/src/main/java/io/novafoundation/nova/feature_swap_api/presentation/state/SwapSettingsState.kt @@ -1,7 +1,7 @@ package io.novafoundation.nova.feature_swap_api.presentation.state import io.novafoundation.nova.common.utils.Percent -import io.novafoundation.nova.feature_swap_core.domain.model.SwapDirection +import io.novafoundation.nova.feature_swap_core_api.data.primitive.model.SwapDirection import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.types.Balance import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain import io.novafoundation.nova.runtime.state.SelectedOptionSharedState diff --git a/feature-swap-core/api/.gitignore b/feature-swap-core/api/.gitignore new file mode 100644 index 0000000000..42afabfd2a --- /dev/null +++ b/feature-swap-core/api/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/feature-swap-core/api/build.gradle b/feature-swap-core/api/build.gradle new file mode 100644 index 0000000000..a90226a61a --- /dev/null +++ b/feature-swap-core/api/build.gradle @@ -0,0 +1,41 @@ +apply plugin: 'com.android.library' +apply plugin: 'kotlin-android' +apply plugin: 'kotlin-kapt' + +android { + namespace 'io.novafoundation.nova.feature_swap_core_api' + compileSdkVersion rootProject.compileSdkVersion + + defaultConfig { + minSdkVersion rootProject.minSdkVersion + targetSdkVersion rootProject.targetSdkVersion + + testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" + + } + + compileOptions { + sourceCompatibility 1.8 + targetCompatibility 1.8 + } + + kotlinOptions { + jvmTarget = '1.8' + freeCompilerArgs = ["-Xcontext-receivers"] + } +} + +dependencies { + implementation coroutinesDep + implementation project(':runtime') + implementation project(":common") + + implementation substrateSdkDep + + implementation daggerDep + kapt daggerKapt + + implementation androidDep + + api project(':core-api') +} \ No newline at end of file diff --git a/feature-swap-core/api/consumer-rules.pro b/feature-swap-core/api/consumer-rules.pro new file mode 100644 index 0000000000..e69de29bb2 diff --git a/feature-swap-core/api/proguard-rules.pro b/feature-swap-core/api/proguard-rules.pro new file mode 100644 index 0000000000..481bb43481 --- /dev/null +++ b/feature-swap-core/api/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/feature-swap-core/api/src/main/AndroidManifest.xml b/feature-swap-core/api/src/main/AndroidManifest.xml new file mode 100644 index 0000000000..a5918e68ab --- /dev/null +++ b/feature-swap-core/api/src/main/AndroidManifest.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/feature-swap-core/src/main/java/io/novafoundation/nova/feature_swap_core/data/network/HydraDxAssetIdConverter.kt b/feature-swap-core/api/src/main/java/io/novafoundation/nova/feature_swap_core_api/data/network/HydraDxAssetIdConverter.kt similarity index 93% rename from feature-swap-core/src/main/java/io/novafoundation/nova/feature_swap_core/data/network/HydraDxAssetIdConverter.kt rename to feature-swap-core/api/src/main/java/io/novafoundation/nova/feature_swap_core_api/data/network/HydraDxAssetIdConverter.kt index f2e8b53405..8c45a97355 100644 --- a/feature-swap-core/src/main/java/io/novafoundation/nova/feature_swap_core/data/network/HydraDxAssetIdConverter.kt +++ b/feature-swap-core/api/src/main/java/io/novafoundation/nova/feature_swap_core_api/data/network/HydraDxAssetIdConverter.kt @@ -1,4 +1,4 @@ -package io.novafoundation.nova.feature_swap_core.data.network +package io.novafoundation.nova.feature_swap_core_api.data.network import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain import java.math.BigInteger diff --git a/feature-swap-core/api/src/main/java/io/novafoundation/nova/feature_swap_core_api/data/paths/PathQuoter.kt b/feature-swap-core/api/src/main/java/io/novafoundation/nova/feature_swap_core_api/data/paths/PathQuoter.kt new file mode 100644 index 0000000000..ae30f7fdc4 --- /dev/null +++ b/feature-swap-core/api/src/main/java/io/novafoundation/nova/feature_swap_core_api/data/paths/PathQuoter.kt @@ -0,0 +1,28 @@ +package io.novafoundation.nova.feature_swap_core_api.data.paths + +import io.novafoundation.nova.common.utils.graph.Graph +import io.novafoundation.nova.feature_swap_core_api.data.paths.model.BestPathQuote +import io.novafoundation.nova.feature_swap_core_api.data.primitive.model.QuotableEdge +import io.novafoundation.nova.feature_swap_core_api.data.primitive.model.SwapDirection +import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain +import io.novafoundation.nova.runtime.multiNetwork.chain.model.FullChainAssetId +import kotlinx.coroutines.CoroutineScope +import java.math.BigInteger + +interface PathQuoter { + + interface Factory { + + fun create( + graph: Graph, + computationalScope: CoroutineScope + ): PathQuoter + } + + suspend fun findBestPath( + chainAssetIn: Chain.Asset, + chainAssetOut: Chain.Asset, + amount: BigInteger, + swapDirection: SwapDirection, + ): BestPathQuote +} diff --git a/feature-swap-core/api/src/main/java/io/novafoundation/nova/feature_swap_core_api/data/paths/model/BestPathQuote.kt b/feature-swap-core/api/src/main/java/io/novafoundation/nova/feature_swap_core_api/data/paths/model/BestPathQuote.kt new file mode 100644 index 0000000000..664fc5bb04 --- /dev/null +++ b/feature-swap-core/api/src/main/java/io/novafoundation/nova/feature_swap_core_api/data/paths/model/BestPathQuote.kt @@ -0,0 +1,8 @@ +package io.novafoundation.nova.feature_swap_core_api.data.paths.model + +class BestPathQuote( + val candidates: List> +) { + + val bestPath : QuotedPath = candidates.max() +} diff --git a/feature-swap-core/api/src/main/java/io/novafoundation/nova/feature_swap_core_api/data/paths/model/QuotedEdge.kt b/feature-swap-core/api/src/main/java/io/novafoundation/nova/feature_swap_core_api/data/paths/model/QuotedEdge.kt new file mode 100644 index 0000000000..9b255a3c5f --- /dev/null +++ b/feature-swap-core/api/src/main/java/io/novafoundation/nova/feature_swap_core_api/data/paths/model/QuotedEdge.kt @@ -0,0 +1,9 @@ +package io.novafoundation.nova.feature_swap_core_api.data.paths.model + +import java.math.BigInteger + +class QuotedEdge( + val quotedAmount: BigInteger, + val quote: BigInteger, + val edge: E +) diff --git a/feature-swap-core/api/src/main/java/io/novafoundation/nova/feature_swap_core_api/data/paths/model/QuotedPath.kt b/feature-swap-core/api/src/main/java/io/novafoundation/nova/feature_swap_core_api/data/paths/model/QuotedPath.kt new file mode 100644 index 0000000000..6f4d41c4ee --- /dev/null +++ b/feature-swap-core/api/src/main/java/io/novafoundation/nova/feature_swap_core_api/data/paths/model/QuotedPath.kt @@ -0,0 +1,38 @@ +package io.novafoundation.nova.feature_swap_core_api.data.paths.model + +import io.novafoundation.nova.common.utils.graph.Path +import io.novafoundation.nova.feature_swap_core_api.data.primitive.model.SwapDirection +import java.math.BigInteger + +class QuotedPath( + val direction: SwapDirection, + val path: Path> +) : Comparable> { + + override fun compareTo(other: QuotedPath): Int { + return when (direction) { + // When we want to sell a token, the bigger the quote - the better + SwapDirection.SPECIFIED_IN -> (lastSegmentQuote - other.lastSegmentQuote).signum() + // When we want to buy a token, the smaller the quote - the better + SwapDirection.SPECIFIED_OUT -> (other.firstSegmentQuote - firstSegmentQuote).signum() + } + } +} + +val QuotedPath<*>.quote: BigInteger + get() = when(direction) { + SwapDirection.SPECIFIED_IN -> lastSegmentQuote + SwapDirection.SPECIFIED_OUT -> firstSegmentQuote + } + +val QuotedPath<*>.lastSegmentQuotedAmount: BigInteger + get() = path.last().quotedAmount + +val QuotedPath<*>.lastSegmentQuote: BigInteger + get() = path.last().quote + +val QuotedPath<*>.firstSegmentQuote: BigInteger + get() = path.first().quote + +val QuotedPath<*>.firstSegmentQuotedAmount: BigInteger + get() = path.first().quotedAmount diff --git a/feature-swap-core/api/src/main/java/io/novafoundation/nova/feature_swap_core_api/data/primitive/SwapQuoting.kt b/feature-swap-core/api/src/main/java/io/novafoundation/nova/feature_swap_core_api/data/primitive/SwapQuoting.kt new file mode 100644 index 0000000000..d393dab247 --- /dev/null +++ b/feature-swap-core/api/src/main/java/io/novafoundation/nova/feature_swap_core_api/data/primitive/SwapQuoting.kt @@ -0,0 +1,27 @@ +package io.novafoundation.nova.feature_swap_core_api.data.primitive + +import io.novafoundation.nova.core.updater.SharedRequestsBuilder +import io.novafoundation.nova.feature_swap_core_api.data.primitive.model.QuotableEdge +import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain +import io.novasama.substrate_sdk_android.runtime.AccountId +import kotlinx.coroutines.flow.Flow + +interface SwapQuoting { + + /** + * Perform initial data sync needed to later perform [runSubscriptions] + * This is separated from [runSubscriptions] since [runSubscriptions] might be io-intense + * [sync] should be sufficient for [availableSwapDirections] to work + * whereas [runSubscriptions] should enable [QuotableEdge.quote] method to work + */ + suspend fun sync() + + suspend fun availableSwapDirections(): List + + suspend fun runSubscriptions( + userAccountId: AccountId, + subscriptionBuilder: SharedRequestsBuilder + ): Flow + + suspend fun canPayFeeInNonUtilityToken(chainAsset: Chain.Asset): Boolean +} diff --git a/feature-swap-core/src/main/java/io/novafoundation/nova/feature_swap_core/domain/model/SwapQuoteException.kt b/feature-swap-core/api/src/main/java/io/novafoundation/nova/feature_swap_core_api/data/primitive/errors/SwapQuoteException.kt similarity index 58% rename from feature-swap-core/src/main/java/io/novafoundation/nova/feature_swap_core/domain/model/SwapQuoteException.kt rename to feature-swap-core/api/src/main/java/io/novafoundation/nova/feature_swap_core_api/data/primitive/errors/SwapQuoteException.kt index 63d455a831..b9c6883467 100644 --- a/feature-swap-core/src/main/java/io/novafoundation/nova/feature_swap_core/domain/model/SwapQuoteException.kt +++ b/feature-swap-core/api/src/main/java/io/novafoundation/nova/feature_swap_core_api/data/primitive/errors/SwapQuoteException.kt @@ -1,4 +1,4 @@ -package io.novafoundation.nova.feature_swap_core.domain.model +package io.novafoundation.nova.feature_swap_core_api.data.primitive.errors sealed class SwapQuoteException : Exception() { diff --git a/feature-swap-core/api/src/main/java/io/novafoundation/nova/feature_swap_core_api/data/primitive/model/QuotableEdge.kt b/feature-swap-core/api/src/main/java/io/novafoundation/nova/feature_swap_core_api/data/primitive/model/QuotableEdge.kt new file mode 100644 index 0000000000..dd4b0f10a6 --- /dev/null +++ b/feature-swap-core/api/src/main/java/io/novafoundation/nova/feature_swap_core_api/data/primitive/model/QuotableEdge.kt @@ -0,0 +1,13 @@ +package io.novafoundation.nova.feature_swap_core_api.data.primitive.model + +import io.novafoundation.nova.common.utils.graph.Edge +import io.novafoundation.nova.runtime.multiNetwork.chain.model.FullChainAssetId +import java.math.BigInteger + +interface QuotableEdge : Edge { + + suspend fun quote( + amount: BigInteger, + direction: SwapDirection + ): BigInteger +} diff --git a/feature-swap-core/src/main/java/io/novafoundation/nova/feature_swap_core/domain/model/SwapDirection.kt b/feature-swap-core/api/src/main/java/io/novafoundation/nova/feature_swap_core_api/data/primitive/model/SwapDirection.kt similarity index 78% rename from feature-swap-core/src/main/java/io/novafoundation/nova/feature_swap_core/domain/model/SwapDirection.kt rename to feature-swap-core/api/src/main/java/io/novafoundation/nova/feature_swap_core_api/data/primitive/model/SwapDirection.kt index ead757c02d..e6574edb36 100644 --- a/feature-swap-core/src/main/java/io/novafoundation/nova/feature_swap_core/domain/model/SwapDirection.kt +++ b/feature-swap-core/api/src/main/java/io/novafoundation/nova/feature_swap_core_api/data/primitive/model/SwapDirection.kt @@ -1,4 +1,4 @@ -package io.novafoundation.nova.feature_swap_core.domain.model +package io.novafoundation.nova.feature_swap_core_api.data.primitive.model enum class SwapDirection { SPECIFIED_IN, SPECIFIED_OUT diff --git a/feature-swap-core/api/src/main/java/io/novafoundation/nova/feature_swap_core_api/data/types/hydra/HydraDxQuoting.kt b/feature-swap-core/api/src/main/java/io/novafoundation/nova/feature_swap_core_api/data/types/hydra/HydraDxQuoting.kt new file mode 100644 index 0000000000..572e6e7305 --- /dev/null +++ b/feature-swap-core/api/src/main/java/io/novafoundation/nova/feature_swap_core_api/data/types/hydra/HydraDxQuoting.kt @@ -0,0 +1,14 @@ +package io.novafoundation.nova.feature_swap_core_api.data.types.hydra + +import io.novafoundation.nova.feature_swap_core_api.data.primitive.SwapQuoting +import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain + +interface HydraDxQuoting: SwapQuoting { + + interface Factory { + + fun create(chain: Chain): HydraDxQuoting + } + + fun getSource(id: String): HydraDxQuotingSource<*> +} diff --git a/feature-swap-core/api/src/main/java/io/novafoundation/nova/feature_swap_core_api/data/types/hydra/HydraDxQuotingSource.kt b/feature-swap-core/api/src/main/java/io/novafoundation/nova/feature_swap_core_api/data/types/hydra/HydraDxQuotingSource.kt new file mode 100644 index 0000000000..b66fe7b2e9 --- /dev/null +++ b/feature-swap-core/api/src/main/java/io/novafoundation/nova/feature_swap_core_api/data/types/hydra/HydraDxQuotingSource.kt @@ -0,0 +1,25 @@ +package io.novafoundation.nova.feature_swap_core_api.data.types.hydra + +import io.novafoundation.nova.common.utils.Identifiable +import io.novafoundation.nova.core.updater.SharedRequestsBuilder +import io.novafoundation.nova.feature_swap_core_api.data.primitive.model.QuotableEdge +import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain +import io.novasama.substrate_sdk_android.runtime.AccountId +import kotlinx.coroutines.flow.Flow + +interface HydraDxQuotingSource : Identifiable { + + suspend fun sync() + + suspend fun availableSwapDirections(): Collection + + suspend fun runSubscriptions( + userAccountId: AccountId, + subscriptionBuilder: SharedRequestsBuilder + ): Flow + + interface Factory> { + + fun create(chain: Chain): S + } +} diff --git a/feature-swap-core/api/src/main/java/io/novafoundation/nova/feature_swap_core_api/di/SwapCoreApi.kt b/feature-swap-core/api/src/main/java/io/novafoundation/nova/feature_swap_core_api/di/SwapCoreApi.kt new file mode 100644 index 0000000000..174db12a55 --- /dev/null +++ b/feature-swap-core/api/src/main/java/io/novafoundation/nova/feature_swap_core_api/di/SwapCoreApi.kt @@ -0,0 +1,14 @@ +package io.novafoundation.nova.feature_swap_core_api.di + +import io.novafoundation.nova.feature_swap_core_api.data.network.HydraDxAssetIdConverter +import io.novafoundation.nova.feature_swap_core_api.data.paths.PathQuoter +import io.novafoundation.nova.feature_swap_core_api.data.types.hydra.HydraDxQuoting + +interface SwapCoreApi { + + val hydraDxQuotingFactory: HydraDxQuoting.Factory + + val hydraDxAssetIdConverter: HydraDxAssetIdConverter + + val pathQuoterFactory: PathQuoter.Factory +} diff --git a/feature-swap-core/build.gradle b/feature-swap-core/build.gradle index daa08eeb7b..dc2e084539 100644 --- a/feature-swap-core/build.gradle +++ b/feature-swap-core/build.gradle @@ -31,6 +31,7 @@ dependencies { implementation project(":common") implementation project(":bindings:hydra-dx-math") + api project(":feature-swap-core:api") implementation substrateSdkDep diff --git a/feature-swap-core/src/main/java/io/novafoundation/nova/feature_swap_core/data/assetExchange/conversion/AssetConversion.kt b/feature-swap-core/src/main/java/io/novafoundation/nova/feature_swap_core/data/assetExchange/conversion/AssetConversion.kt deleted file mode 100644 index 9795820f2c..0000000000 --- a/feature-swap-core/src/main/java/io/novafoundation/nova/feature_swap_core/data/assetExchange/conversion/AssetConversion.kt +++ /dev/null @@ -1,32 +0,0 @@ -package io.novafoundation.nova.feature_swap_core.data.assetExchange.conversion - -import io.novafoundation.nova.common.utils.graph.Edge -import io.novafoundation.nova.common.utils.graph.Graph -import io.novafoundation.nova.common.utils.graph.Path -import io.novafoundation.nova.core.updater.SharedRequestsBuilder -import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain -import io.novafoundation.nova.runtime.multiNetwork.chain.model.FullChainAssetId -import io.novasama.substrate_sdk_android.runtime.AccountId -import kotlinx.coroutines.flow.Flow - -interface AssetConversion> { - - interface Factory> { - fun create(chain: Chain): AssetConversion - } - - suspend fun sync() - - suspend fun availableSwapDirections(): Graph - - suspend fun getPaths(graph: Graph, args: AssetExchangeQuoteArgs): List> - - suspend fun quote(paths: List>, args: AssetExchangeQuoteArgs): AssetExchangeQuote - - suspend fun runSubscriptions( - userAccountId: AccountId, - subscriptionBuilder: SharedRequestsBuilder - ): Flow - - suspend fun canPayFeeInNonUtilityToken(chainAsset: Chain.Asset): Boolean -} diff --git a/feature-swap-core/src/main/java/io/novafoundation/nova/feature_swap_core/data/assetExchange/conversion/AssetExchangeQuote.kt b/feature-swap-core/src/main/java/io/novafoundation/nova/feature_swap_core/data/assetExchange/conversion/AssetExchangeQuote.kt deleted file mode 100644 index 3d3dbc3987..0000000000 --- a/feature-swap-core/src/main/java/io/novafoundation/nova/feature_swap_core/data/assetExchange/conversion/AssetExchangeQuote.kt +++ /dev/null @@ -1,23 +0,0 @@ -package io.novafoundation.nova.feature_swap_core.data.assetExchange.conversion - -import io.novafoundation.nova.feature_swap_core.domain.model.QuotePath -import io.novafoundation.nova.feature_swap_core.domain.model.SwapDirection -import java.math.BigInteger - -class AssetExchangeQuote( - val direction: SwapDirection, - - val quote: BigInteger, - - val path: QuotePath -) : Comparable { - - override fun compareTo(other: AssetExchangeQuote): Int { - return when (direction) { - // When we want to sell a token, the bigger the quote - the better - SwapDirection.SPECIFIED_IN -> (quote - other.quote).signum() - // When we want to buy a token, the smaller the quote - the better - SwapDirection.SPECIFIED_OUT -> (other.quote - quote).signum() - } - } -} diff --git a/feature-swap-core/src/main/java/io/novafoundation/nova/feature_swap_core/data/assetExchange/conversion/AssetExchangeQuoteArgs.kt b/feature-swap-core/src/main/java/io/novafoundation/nova/feature_swap_core/data/assetExchange/conversion/AssetExchangeQuoteArgs.kt deleted file mode 100644 index c61bb21ff5..0000000000 --- a/feature-swap-core/src/main/java/io/novafoundation/nova/feature_swap_core/data/assetExchange/conversion/AssetExchangeQuoteArgs.kt +++ /dev/null @@ -1,12 +0,0 @@ -package io.novafoundation.nova.feature_swap_core.data.assetExchange.conversion - -import io.novafoundation.nova.feature_swap_core.domain.model.SwapDirection -import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain -import java.math.BigInteger - -data class AssetExchangeQuoteArgs( - val chainAssetIn: Chain.Asset, - val chainAssetOut: Chain.Asset, - val amount: BigInteger, - val swapDirection: SwapDirection, -) diff --git a/feature-swap-core/src/main/java/io/novafoundation/nova/feature_swap_core/data/assetExchange/conversion/types/hydra/HydraDXAssetConversion.kt b/feature-swap-core/src/main/java/io/novafoundation/nova/feature_swap_core/data/assetExchange/conversion/types/hydra/HydraDXAssetConversion.kt deleted file mode 100644 index 5bd5ce635d..0000000000 --- a/feature-swap-core/src/main/java/io/novafoundation/nova/feature_swap_core/data/assetExchange/conversion/types/hydra/HydraDXAssetConversion.kt +++ /dev/null @@ -1,7 +0,0 @@ -package io.novafoundation.nova.feature_swap_core.data.assetExchange.conversion.types.hydra - -import io.novafoundation.nova.feature_swap_core.data.assetExchange.conversion.AssetConversion - -interface HydraDxAssetConversionFactory : AssetConversion.Factory - -interface HydraDXAssetConversion : AssetConversion diff --git a/feature-swap-core/src/main/java/io/novafoundation/nova/feature_swap_core/data/assetExchange/conversion/types/hydra/HydraDxConversionSource.kt b/feature-swap-core/src/main/java/io/novafoundation/nova/feature_swap_core/data/assetExchange/conversion/types/hydra/HydraDxConversionSource.kt deleted file mode 100644 index e2bfb36c6c..0000000000 --- a/feature-swap-core/src/main/java/io/novafoundation/nova/feature_swap_core/data/assetExchange/conversion/types/hydra/HydraDxConversionSource.kt +++ /dev/null @@ -1,40 +0,0 @@ -package io.novafoundation.nova.feature_swap_core.data.assetExchange.conversion.types.hydra - -import io.novafoundation.nova.common.utils.Identifiable -import io.novafoundation.nova.common.utils.MultiMapList -import io.novafoundation.nova.core.updater.SharedRequestsBuilder -import io.novafoundation.nova.feature_swap_core.domain.model.SwapDirection -import io.novafoundation.nova.feature_swap_core.domain.model.SwapQuoteException -import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain -import io.novafoundation.nova.runtime.multiNetwork.chain.model.FullChainAssetId -import io.novasama.substrate_sdk_android.runtime.AccountId -import java.math.BigInteger -import kotlinx.coroutines.flow.Flow - -interface HydraDxConversionSource : Identifiable { - - suspend fun sync() - - suspend fun availableSwapDirections(): MultiMapList - - @Throws(SwapQuoteException::class) - suspend fun quote(args: HydraDxConversionSourceQuoteArgs): BigInteger - - suspend fun runSubscriptions( - userAccountId: AccountId, - subscriptionBuilder: SharedRequestsBuilder - ): Flow - - interface Factory { - - fun create(chain: Chain): HydraDxConversionSource - } -} - -data class HydraDxConversionSourceQuoteArgs( - val chainAssetIn: Chain.Asset, - val chainAssetOut: Chain.Asset, - val amount: BigInteger, - val swapDirection: SwapDirection, - val params: Map -) diff --git a/feature-swap-core/src/main/java/io/novafoundation/nova/feature_swap_core/data/assetExchange/conversion/types/hydra/HydraDxSwapEdge.kt b/feature-swap-core/src/main/java/io/novafoundation/nova/feature_swap_core/data/assetExchange/conversion/types/hydra/HydraDxSwapEdge.kt deleted file mode 100644 index a23350caab..0000000000 --- a/feature-swap-core/src/main/java/io/novafoundation/nova/feature_swap_core/data/assetExchange/conversion/types/hydra/HydraDxSwapEdge.kt +++ /dev/null @@ -1,25 +0,0 @@ -package io.novafoundation.nova.feature_swap_core.data.assetExchange.conversion.types.hydra - -import io.novafoundation.nova.common.utils.graph.Edge -import io.novafoundation.nova.common.utils.graph.Path -import io.novafoundation.nova.feature_swap_core.domain.model.QuotePath -import io.novafoundation.nova.runtime.multiNetwork.chain.model.FullChainAssetId - -typealias HydraDxSwapSourceId = String - -class HydraDxSwapEdge( - override val from: FullChainAssetId, - val sourceId: HydraDxSwapSourceId, - val direction: HydraSwapDirection -) : Edge { - - override val to: FullChainAssetId = direction.to -} - -fun Path.toQuotePath(): QuotePath { - val segments = map { - QuotePath.Segment(it.from, it.to, it.sourceId, it.direction.params) - } - - return QuotePath(segments) -} diff --git a/feature-swap-core/src/main/java/io/novafoundation/nova/feature_swap_core/data/assetExchange/conversion/types/hydra/HydraSwapDirection.kt b/feature-swap-core/src/main/java/io/novafoundation/nova/feature_swap_core/data/assetExchange/conversion/types/hydra/HydraSwapDirection.kt deleted file mode 100644 index f4e4254613..0000000000 --- a/feature-swap-core/src/main/java/io/novafoundation/nova/feature_swap_core/data/assetExchange/conversion/types/hydra/HydraSwapDirection.kt +++ /dev/null @@ -1,12 +0,0 @@ -package io.novafoundation.nova.feature_swap_core.data.assetExchange.conversion.types.hydra - -import io.novafoundation.nova.runtime.multiNetwork.chain.model.FullChainAssetId - -interface HydraSwapDirection { - - val from: FullChainAssetId - - val to: FullChainAssetId - - val params: Map -} diff --git a/feature-swap-core/src/main/java/io/novafoundation/nova/feature_swap_core/data/assetExchange/conversion/types/hydra/MultiTransactionPaymentApi.kt b/feature-swap-core/src/main/java/io/novafoundation/nova/feature_swap_core/data/assetExchange/conversion/types/hydra/MultiTransactionPaymentApi.kt index ab6cf68b1e..c0c06243a7 100644 --- a/feature-swap-core/src/main/java/io/novafoundation/nova/feature_swap_core/data/assetExchange/conversion/types/hydra/MultiTransactionPaymentApi.kt +++ b/feature-swap-core/src/main/java/io/novafoundation/nova/feature_swap_core/data/assetExchange/conversion/types/hydra/MultiTransactionPaymentApi.kt @@ -4,7 +4,7 @@ package io.novafoundation.nova.feature_swap_core.data.assetExchange.conversion.t import io.novafoundation.nova.common.data.network.runtime.binding.bindNumber import io.novafoundation.nova.common.utils.multiTransactionPayment -import io.novafoundation.nova.feature_swap_core.data.network.HydraDxAssetId +import io.novafoundation.nova.feature_swap_core_api.data.network.HydraDxAssetId import io.novafoundation.nova.runtime.storage.source.query.StorageQueryContext import io.novafoundation.nova.runtime.storage.source.query.api.QueryableModule import io.novafoundation.nova.runtime.storage.source.query.api.QueryableStorageEntry1 diff --git a/feature-swap-core/src/main/java/io/novafoundation/nova/feature_swap_core/data/assetExchange/conversion/types/hydra/RealHydraDxAssetConversion.kt b/feature-swap-core/src/main/java/io/novafoundation/nova/feature_swap_core/data/assetExchange/conversion/types/hydra/RealHydraDxAssetConversion.kt deleted file mode 100644 index 3f1d6c5cbc..0000000000 --- a/feature-swap-core/src/main/java/io/novafoundation/nova/feature_swap_core/data/assetExchange/conversion/types/hydra/RealHydraDxAssetConversion.kt +++ /dev/null @@ -1,217 +0,0 @@ -package io.novafoundation.nova.feature_swap_core.data.assetExchange.conversion.types.hydra - -import android.util.Log -import io.novafoundation.nova.common.utils.firstById -import io.novafoundation.nova.common.utils.forEachAsync -import io.novafoundation.nova.common.utils.graph.Graph -import io.novafoundation.nova.common.utils.graph.Path -import io.novafoundation.nova.common.utils.graph.create -import io.novafoundation.nova.common.utils.graph.findDijkstraPathsBetween -import io.novafoundation.nova.common.utils.mapAsync -import io.novafoundation.nova.common.utils.mergeIfMultiple -import io.novafoundation.nova.common.utils.multiTransactionPayment -import io.novafoundation.nova.core.updater.SharedRequestsBuilder -import io.novafoundation.nova.feature_swap_core.BuildConfig -import io.novafoundation.nova.feature_swap_core.data.assetExchange.conversion.AssetExchangeQuote -import io.novafoundation.nova.feature_swap_core.data.assetExchange.conversion.AssetExchangeQuoteArgs -import io.novafoundation.nova.feature_swap_core.data.network.HydraDxAssetIdConverter -import io.novafoundation.nova.feature_swap_core.data.network.toChainAssetOrThrow -import io.novafoundation.nova.feature_swap_core.domain.model.QuotePath -import io.novafoundation.nova.feature_swap_core.domain.model.SwapDirection -import io.novafoundation.nova.feature_swap_core.domain.model.SwapQuoteException -import io.novafoundation.nova.feature_swap_core.data.assetExchange.conversion.types.hydra.impl.stableswap.StableConversionSourceFactory -import io.novafoundation.nova.feature_swap_core.data.network.isSystemAsset -import io.novafoundation.nova.feature_swap_core.data.network.toOnChainIdOrThrow -import io.novafoundation.nova.runtime.ext.fullId -import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain -import io.novafoundation.nova.runtime.multiNetwork.chain.model.FullChainAssetId -import io.novafoundation.nova.runtime.storage.source.StorageDataSource -import io.novafoundation.nova.runtime.storage.source.query.metadata -import io.novasama.substrate_sdk_android.runtime.AccountId -import java.math.BigInteger -import kotlinx.coroutines.flow.Flow - -private const val PATHS_LIMIT = 4 - -class RealHydraDxAssetConversionFactory( - private val remoteStorageSource: StorageDataSource, - private val conversionSourceFactories: Iterable, - private val hydraDxAssetIdConverter: HydraDxAssetIdConverter, -) : HydraDxAssetConversionFactory { - - override fun create(chain: Chain): HydraDXAssetConversion { - return RealHydraDxAssetConversion( - chain, - remoteStorageSource, - conversionSourceFactories, - hydraDxAssetIdConverter - ) - } -} - -class RealHydraDxAssetConversion( - private val chain: Chain, - private val remoteStorageSource: StorageDataSource, - private val swapSourceFactories: Iterable, - private val hydraDxAssetIdConverter: HydraDxAssetIdConverter, - private val debug: Boolean = BuildConfig.DEBUG -) : HydraDXAssetConversion { - - private val conversionSources: List = createSources() - - override suspend fun sync() { - conversionSources.forEachAsync { it.sync() } - } - - override suspend fun canPayFeeInNonUtilityToken(chainAsset: Chain.Asset): Boolean { - val onChainId = hydraDxAssetIdConverter.toOnChainIdOrThrow(chainAsset) - - if (hydraDxAssetIdConverter.isSystemAsset(onChainId)) return true - - val fallbackPrice = remoteStorageSource.query(chain.id) { - metadata.multiTransactionPayment.acceptedCurrencies.query(onChainId) - } - - return fallbackPrice != null - } - - override suspend fun availableSwapDirections(): Graph { - val allDirectDirections = conversionSources.mapAsync { source -> - source.availableSwapDirections().mapValues { (from, directions) -> - directions.map { direction -> HydraDxSwapEdge(from, source.identifier, direction) } - } - } - - return Graph.create(allDirectDirections) - } - - override suspend fun getPaths( - graph: Graph, - args: AssetExchangeQuoteArgs - ): List> { - val from = args.chainAssetIn.fullId - val to = args.chainAssetOut.fullId - - return graph.findDijkstraPathsBetween(from, to, limit = PATHS_LIMIT) - } - - override suspend fun quote(paths: List>, args: AssetExchangeQuoteArgs): AssetExchangeQuote { - val quotedPaths = paths.mapNotNull { path -> quotePath(path, args.amount, args.swapDirection) } - if (paths.isEmpty()) { - throw SwapQuoteException.NotEnoughLiquidity - } - - if (debug) { - logQuotes(args, quotedPaths) - } - - return quotedPaths.max() - } - - override suspend fun runSubscriptions(userAccountId: AccountId, subscriptionBuilder: SharedRequestsBuilder): Flow { - return conversionSources.map { - it.runSubscriptions(userAccountId, subscriptionBuilder) - }.mergeIfMultiple() - } - - private suspend fun quotePath( - path: Path, - amount: BigInteger, - swapDirection: SwapDirection - ): AssetExchangeQuote? { - val quote = when (swapDirection) { - SwapDirection.SPECIFIED_IN -> quotePathSell(path, amount) - SwapDirection.SPECIFIED_OUT -> quotePathBuy(path, amount) - } ?: return null - - return AssetExchangeQuote(swapDirection, quote, path.toQuotePath()) - } - - private suspend fun quotePathBuy(path: Path, amount: BigInteger): BigInteger? { - return runCatching { - path.foldRight(amount) { segment, currentAmount -> - val args = HydraDxConversionSourceQuoteArgs( - chainAssetIn = chain.assetsById.getValue(segment.from.assetId), - chainAssetOut = chain.assetsById.getValue(segment.to.assetId), - amount = currentAmount, - swapDirection = SwapDirection.SPECIFIED_OUT, - params = segment.direction.params - ) - - segment.swapSource().quote(args) - } - }.getOrNull() - } - - private suspend fun quotePathSell(path: Path, amount: BigInteger): BigInteger? { - return runCatching { - path.fold(amount) { currentAmount, segment -> - val args = HydraDxConversionSourceQuoteArgs( - chainAssetIn = chain.assetsById.getValue(segment.from.assetId), - chainAssetOut = chain.assetsById.getValue(segment.to.assetId), - amount = currentAmount, - swapDirection = SwapDirection.SPECIFIED_IN, - params = segment.direction.params - ) - - segment.swapSource().quote(args) - } - }.getOrNull() - } - - private fun createSources(): List { - return swapSourceFactories.map { it.create(chain) } - } - - private fun HydraDxSwapEdge.swapSource(): HydraDxConversionSource { - return conversionSources.firstById(sourceId) - } - - private suspend fun logQuotes(args: AssetExchangeQuoteArgs, quotes: List) { - val allCandidates = quotes.sortedDescending().map { - val chainAssetIn = args.chainAssetIn - val chainAssetOut = args.chainAssetOut - val formattedIn = args.amount.toBigDecimal(scale = chainAssetIn.precision.value).toString() + " " + chainAssetIn.symbol - val formattedOut = it.quote.toBigDecimal(scale = chainAssetOut.precision.value).toString() + " " + chainAssetOut.symbol - val formattedPath = formatPath(it.path) - - "$formattedIn to $formattedOut via $formattedPath" - }.joinToString(separator = "\n") - - Log.d("RealSwapService", "-------- New quote ----------") - Log.d("RealSwapService", allCandidates) - Log.d("RealSwapService", "-------- Done quote ----------\n\n\n") - } - - private suspend fun formatPath(path: QuotePath): String { - val assets = chain.assetsById - - return buildString { - val firstSegment = path.segments.first() - - append(assets.getValue(firstSegment.from.assetId).symbol) - - append(" -- ${formatSource(firstSegment)} --> ") - - append(assets.getValue(firstSegment.to.assetId).symbol) - - path.segments.subList(1, path.segments.size).onEach { segment -> - append(" -- ${formatSource(segment)} --> ") - - append(assets.getValue(segment.to.assetId).symbol) - } - } - } - - private suspend fun formatSource(segment: QuotePath.Segment): String { - return buildString { - append(segment.sourceId) - - if (segment.sourceId == StableConversionSourceFactory.ID) { - val onChainId = segment.sourceParams.getValue("PoolId").toBigInteger() - val chainAsset = hydraDxAssetIdConverter.toChainAssetOrThrow(chain, onChainId) - append("[${chainAsset.symbol}]") - } - } - } -} diff --git a/feature-swap-core/src/main/java/io/novafoundation/nova/feature_swap_core/data/assetExchange/conversion/types/hydra/RealHydraDxAssetIdConverter.kt b/feature-swap-core/src/main/java/io/novafoundation/nova/feature_swap_core/data/assetExchange/conversion/types/hydra/RealHydraDxAssetIdConverter.kt index 9b8ebd935c..c25e719b55 100644 --- a/feature-swap-core/src/main/java/io/novafoundation/nova/feature_swap_core/data/assetExchange/conversion/types/hydra/RealHydraDxAssetIdConverter.kt +++ b/feature-swap-core/src/main/java/io/novafoundation/nova/feature_swap_core/data/assetExchange/conversion/types/hydra/RealHydraDxAssetIdConverter.kt @@ -1,8 +1,8 @@ package io.novafoundation.nova.feature_swap_core.data.assetExchange.conversion.types.hydra import io.novafoundation.nova.common.data.network.runtime.binding.bindNumberOrNull -import io.novafoundation.nova.feature_swap_core.data.network.HydraDxAssetId -import io.novafoundation.nova.feature_swap_core.data.network.HydraDxAssetIdConverter +import io.novafoundation.nova.feature_swap_core_api.data.network.HydraDxAssetId +import io.novafoundation.nova.feature_swap_core_api.data.network.HydraDxAssetIdConverter import io.novafoundation.nova.runtime.ext.decodeOrNull import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain @@ -48,8 +48,4 @@ internal class RealHydraDxAssetIdConverter( else -> null } } - - private fun Chain.Asset.requireHydraDxAssetId(runtimeSnapshot: RuntimeSnapshot): HydraDxAssetId { - return requireNotNull(omniPoolTokenIdOrNull(runtimeSnapshot)) - } } diff --git a/feature-swap-core/src/main/java/io/novafoundation/nova/feature_swap_core/data/assetExchange/conversion/types/hydra/RealHydraDxQuoting.kt b/feature-swap-core/src/main/java/io/novafoundation/nova/feature_swap_core/data/assetExchange/conversion/types/hydra/RealHydraDxQuoting.kt new file mode 100644 index 0000000000..bdcfc965f2 --- /dev/null +++ b/feature-swap-core/src/main/java/io/novafoundation/nova/feature_swap_core/data/assetExchange/conversion/types/hydra/RealHydraDxQuoting.kt @@ -0,0 +1,79 @@ +package io.novafoundation.nova.feature_swap_core.data.assetExchange.conversion.types.hydra + +import io.novafoundation.nova.common.utils.flatMapAsync +import io.novafoundation.nova.common.utils.forEachAsync +import io.novafoundation.nova.common.utils.mergeIfMultiple +import io.novafoundation.nova.core.updater.SharedRequestsBuilder +import io.novafoundation.nova.feature_swap_core_api.data.network.HydraDxAssetIdConverter +import io.novafoundation.nova.feature_swap_core_api.data.network.isSystemAsset +import io.novafoundation.nova.feature_swap_core_api.data.network.toOnChainIdOrThrow +import io.novafoundation.nova.feature_swap_core_api.data.primitive.model.QuotableEdge +import io.novafoundation.nova.feature_swap_core_api.data.types.hydra.HydraDxQuoting +import io.novafoundation.nova.feature_swap_core_api.data.types.hydra.HydraDxQuotingSource +import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain +import io.novafoundation.nova.runtime.storage.source.StorageDataSource +import io.novafoundation.nova.runtime.storage.source.query.metadata +import io.novasama.substrate_sdk_android.runtime.AccountId +import kotlinx.coroutines.flow.Flow + + +class RealHydraDxQuotingFactory( + private val remoteStorageSource: StorageDataSource, + private val conversionSourceFactories: Iterable>, + private val hydraDxAssetIdConverter: HydraDxAssetIdConverter, +) : HydraDxQuoting.Factory { + + override fun create(chain: Chain): HydraDxQuoting { + return RealHydraDxQuoting( + chain = chain, + remoteStorageSource = remoteStorageSource, + quotingSourceFactories = conversionSourceFactories, + hydraDxAssetIdConverter = hydraDxAssetIdConverter + ) + } +} + +private class RealHydraDxQuoting( + private val chain: Chain, + private val remoteStorageSource: StorageDataSource, + private val quotingSourceFactories: Iterable>, + private val hydraDxAssetIdConverter: HydraDxAssetIdConverter, +) : HydraDxQuoting { + + private val quotingSources: Map> = createSources() + + override fun getSource(id: String): HydraDxQuotingSource<*> { + return quotingSources.getValue(id) + } + + override suspend fun sync() { + quotingSources.values.forEachAsync { it.sync() } + } + + override suspend fun canPayFeeInNonUtilityToken(chainAsset: Chain.Asset): Boolean { + val onChainId = hydraDxAssetIdConverter.toOnChainIdOrThrow(chainAsset) + + if (hydraDxAssetIdConverter.isSystemAsset(onChainId)) return true + + val fallbackPrice = remoteStorageSource.query(chain.id) { + metadata.multiTransactionPayment.acceptedCurrencies.query(onChainId) + } + + return fallbackPrice != null + } + + override suspend fun availableSwapDirections(): List { + return quotingSources.values.flatMapAsync { source -> source.availableSwapDirections() } + } + + override suspend fun runSubscriptions(userAccountId: AccountId, subscriptionBuilder: SharedRequestsBuilder): Flow { + return quotingSources.values.map { + it.runSubscriptions(userAccountId, subscriptionBuilder) + }.mergeIfMultiple() + } + + private fun createSources(): Map> { + return quotingSourceFactories.map { it.create(chain) } + .associateBy { it.identifier } + } +} diff --git a/feature-swap-core/src/main/java/io/novafoundation/nova/feature_swap_core/data/assetExchange/conversion/types/hydra/impl/ConverionSourceBalanceUtils.kt b/feature-swap-core/src/main/java/io/novafoundation/nova/feature_swap_core/data/assetExchange/conversion/types/hydra/sources/ConverionSourceBalanceUtils.kt similarity index 98% rename from feature-swap-core/src/main/java/io/novafoundation/nova/feature_swap_core/data/assetExchange/conversion/types/hydra/impl/ConverionSourceBalanceUtils.kt rename to feature-swap-core/src/main/java/io/novafoundation/nova/feature_swap_core/data/assetExchange/conversion/types/hydra/sources/ConverionSourceBalanceUtils.kt index c955bd7166..e7089c3f09 100644 --- a/feature-swap-core/src/main/java/io/novafoundation/nova/feature_swap_core/data/assetExchange/conversion/types/hydra/impl/ConverionSourceBalanceUtils.kt +++ b/feature-swap-core/src/main/java/io/novafoundation/nova/feature_swap_core/data/assetExchange/conversion/types/hydra/sources/ConverionSourceBalanceUtils.kt @@ -1,4 +1,4 @@ -package io.novafoundation.nova.feature_swap_core.data.assetExchange.conversion.types.hydra.impl +package io.novafoundation.nova.feature_swap_core.data.assetExchange.conversion.types.hydra.sources import io.novafoundation.nova.common.data.network.ext.transferableBalance import io.novafoundation.nova.common.data.network.runtime.binding.AccountInfo @@ -15,9 +15,9 @@ import io.novafoundation.nova.runtime.storage.typed.account import io.novafoundation.nova.runtime.storage.typed.system import io.novasama.substrate_sdk_android.runtime.AccountId import io.novasama.substrate_sdk_android.runtime.metadata.storage -import java.math.BigInteger import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.map +import java.math.BigInteger suspend fun StorageDataSource.subscribeToTransferableBalance( chainAsset: Chain.Asset, diff --git a/feature-swap-core/src/main/java/io/novafoundation/nova/feature_swap_core/data/assetExchange/conversion/types/hydra/impl/omnipool/DynamicFeesApi.kt b/feature-swap-core/src/main/java/io/novafoundation/nova/feature_swap_core/data/assetExchange/conversion/types/hydra/sources/omnipool/DynamicFeesApi.kt similarity index 82% rename from feature-swap-core/src/main/java/io/novafoundation/nova/feature_swap_core/data/assetExchange/conversion/types/hydra/impl/omnipool/DynamicFeesApi.kt rename to feature-swap-core/src/main/java/io/novafoundation/nova/feature_swap_core/data/assetExchange/conversion/types/hydra/sources/omnipool/DynamicFeesApi.kt index d0b2d4666f..438bd3f1ed 100644 --- a/feature-swap-core/src/main/java/io/novafoundation/nova/feature_swap_core/data/assetExchange/conversion/types/hydra/impl/omnipool/DynamicFeesApi.kt +++ b/feature-swap-core/src/main/java/io/novafoundation/nova/feature_swap_core/data/assetExchange/conversion/types/hydra/sources/omnipool/DynamicFeesApi.kt @@ -1,11 +1,11 @@ @file:Suppress("RedundantUnitExpression") -package io.novafoundation.nova.feature_swap_core.data.assetExchange.conversion.types.hydra.impl.omnipool +package io.novafoundation.nova.feature_swap_core.data.assetExchange.conversion.types.hydra.sources.omnipool import io.novafoundation.nova.common.utils.dynamicFees -import io.novafoundation.nova.feature_swap_core.data.assetExchange.conversion.types.hydra.impl.omnipool.model.DynamicFee -import io.novafoundation.nova.feature_swap_core.data.assetExchange.conversion.types.hydra.impl.omnipool.model.bindDynamicFee -import io.novafoundation.nova.feature_swap_core.data.network.HydraDxAssetId +import io.novafoundation.nova.feature_swap_core.data.assetExchange.conversion.types.hydra.sources.omnipool.model.DynamicFee +import io.novafoundation.nova.feature_swap_core.data.assetExchange.conversion.types.hydra.sources.omnipool.model.bindDynamicFee +import io.novafoundation.nova.feature_swap_core_api.data.network.HydraDxAssetId import io.novafoundation.nova.runtime.storage.source.query.StorageQueryContext import io.novafoundation.nova.runtime.storage.source.query.api.QueryableModule import io.novafoundation.nova.runtime.storage.source.query.api.QueryableStorageEntry1 diff --git a/feature-swap-core/src/main/java/io/novafoundation/nova/feature_swap_core/data/assetExchange/conversion/types/hydra/sources/omnipool/OmniPoolId.kt b/feature-swap-core/src/main/java/io/novafoundation/nova/feature_swap_core/data/assetExchange/conversion/types/hydra/sources/omnipool/OmniPoolId.kt new file mode 100644 index 0000000000..5d7b57132f --- /dev/null +++ b/feature-swap-core/src/main/java/io/novafoundation/nova/feature_swap_core/data/assetExchange/conversion/types/hydra/sources/omnipool/OmniPoolId.kt @@ -0,0 +1,8 @@ +package io.novafoundation.nova.feature_swap_core.data.assetExchange.conversion.types.hydra.sources.omnipool + +import io.novafoundation.nova.common.utils.padEnd +import io.novasama.substrate_sdk_android.runtime.AccountId + +fun omniPoolAccountId(): AccountId { + return "modlomnipool".encodeToByteArray().padEnd(expectedSize = 32, padding = 0) +} diff --git a/feature-swap-core/src/main/java/io/novafoundation/nova/feature_swap_core/data/assetExchange/conversion/types/hydra/sources/omnipool/OmniPoolQuotingSource.kt b/feature-swap-core/src/main/java/io/novafoundation/nova/feature_swap_core/data/assetExchange/conversion/types/hydra/sources/omnipool/OmniPoolQuotingSource.kt new file mode 100644 index 0000000000..680f76fc1b --- /dev/null +++ b/feature-swap-core/src/main/java/io/novafoundation/nova/feature_swap_core/data/assetExchange/conversion/types/hydra/sources/omnipool/OmniPoolQuotingSource.kt @@ -0,0 +1,15 @@ +package io.novafoundation.nova.feature_swap_core.data.assetExchange.conversion.types.hydra.sources.omnipool + +import io.novafoundation.nova.feature_swap_core.data.assetExchange.conversion.types.hydra.sources.omnipool.model.RemoteIdAndLocalAsset +import io.novafoundation.nova.feature_swap_core_api.data.primitive.model.QuotableEdge +import io.novafoundation.nova.feature_swap_core_api.data.types.hydra.HydraDxQuotingSource + +interface OmniPoolQuotingSource : HydraDxQuotingSource { + + interface Edge : QuotableEdge { + + val fromAsset: RemoteIdAndLocalAsset + + val toAsset: RemoteIdAndLocalAsset + } +} diff --git a/feature-swap-core/src/main/java/io/novafoundation/nova/feature_swap_core/data/assetExchange/conversion/types/hydra/impl/omnipool/OmnipoolApi.kt b/feature-swap-core/src/main/java/io/novafoundation/nova/feature_swap_core/data/assetExchange/conversion/types/hydra/sources/omnipool/OmnipoolApi.kt similarity index 83% rename from feature-swap-core/src/main/java/io/novafoundation/nova/feature_swap_core/data/assetExchange/conversion/types/hydra/impl/omnipool/OmnipoolApi.kt rename to feature-swap-core/src/main/java/io/novafoundation/nova/feature_swap_core/data/assetExchange/conversion/types/hydra/sources/omnipool/OmnipoolApi.kt index 764d186015..9a9017efe3 100644 --- a/feature-swap-core/src/main/java/io/novafoundation/nova/feature_swap_core/data/assetExchange/conversion/types/hydra/impl/omnipool/OmnipoolApi.kt +++ b/feature-swap-core/src/main/java/io/novafoundation/nova/feature_swap_core/data/assetExchange/conversion/types/hydra/sources/omnipool/OmnipoolApi.kt @@ -1,12 +1,12 @@ @file:Suppress("RedundantUnitExpression") -package io.novafoundation.nova.feature_swap_core.data.assetExchange.conversion.types.hydra.impl.omnipool +package io.novafoundation.nova.feature_swap_core.data.assetExchange.conversion.types.hydra.sources.omnipool import io.novafoundation.nova.common.utils.omnipool import io.novafoundation.nova.common.utils.omnipoolOrNull -import io.novafoundation.nova.feature_swap_core.data.assetExchange.conversion.types.hydra.impl.omnipool.model.OmnipoolAssetState -import io.novafoundation.nova.feature_swap_core.data.assetExchange.conversion.types.hydra.impl.omnipool.model.bindOmnipoolAssetState -import io.novafoundation.nova.feature_swap_core.data.network.HydraDxAssetId +import io.novafoundation.nova.feature_swap_core.data.assetExchange.conversion.types.hydra.sources.omnipool.model.OmnipoolAssetState +import io.novafoundation.nova.feature_swap_core.data.assetExchange.conversion.types.hydra.sources.omnipool.model.bindOmnipoolAssetState +import io.novafoundation.nova.feature_swap_core_api.data.network.HydraDxAssetId import io.novafoundation.nova.runtime.storage.source.query.StorageQueryContext import io.novafoundation.nova.runtime.storage.source.query.api.QueryableModule import io.novafoundation.nova.runtime.storage.source.query.api.QueryableStorageEntry1 diff --git a/feature-swap-core/src/main/java/io/novafoundation/nova/feature_swap_core/data/assetExchange/conversion/types/hydra/impl/omnipool/OmniPoolConversionSource.kt b/feature-swap-core/src/main/java/io/novafoundation/nova/feature_swap_core/data/assetExchange/conversion/types/hydra/sources/omnipool/RealOmniPoolQuotingSource.kt similarity index 59% rename from feature-swap-core/src/main/java/io/novafoundation/nova/feature_swap_core/data/assetExchange/conversion/types/hydra/impl/omnipool/OmniPoolConversionSource.kt rename to feature-swap-core/src/main/java/io/novafoundation/nova/feature_swap_core/data/assetExchange/conversion/types/hydra/sources/omnipool/RealOmniPoolQuotingSource.kt index b0bf3d46dc..d58250a118 100644 --- a/feature-swap-core/src/main/java/io/novafoundation/nova/feature_swap_core/data/assetExchange/conversion/types/hydra/impl/omnipool/OmniPoolConversionSource.kt +++ b/feature-swap-core/src/main/java/io/novafoundation/nova/feature_swap_core/data/assetExchange/conversion/types/hydra/sources/omnipool/RealOmniPoolQuotingSource.kt @@ -1,31 +1,26 @@ -package io.novafoundation.nova.feature_swap_core.data.assetExchange.conversion.types.hydra.impl.omnipool +package io.novafoundation.nova.feature_swap_core.data.assetExchange.conversion.types.hydra.sources.omnipool -import io.novafoundation.nova.common.utils.Modules -import io.novafoundation.nova.common.utils.MultiMapList import io.novafoundation.nova.common.utils.dynamicFees import io.novafoundation.nova.common.utils.numberConstant import io.novafoundation.nova.common.utils.omnipool import io.novafoundation.nova.common.utils.orZero -import io.novafoundation.nova.common.utils.padEnd import io.novafoundation.nova.common.utils.singleReplaySharedFlow import io.novafoundation.nova.common.utils.toMultiSubscription import io.novafoundation.nova.core.updater.SharedRequestsBuilder -import io.novafoundation.nova.feature_swap_core.data.assetExchange.conversion.types.hydra.HydraDxConversionSource -import io.novafoundation.nova.feature_swap_core.data.assetExchange.conversion.types.hydra.HydraDxConversionSourceQuoteArgs -import io.novafoundation.nova.feature_swap_core.domain.model.SwapQuoteException -import io.novafoundation.nova.feature_swap_core.data.assetExchange.conversion.types.hydra.HydraDxSwapSourceId -import io.novafoundation.nova.feature_swap_core.data.assetExchange.conversion.types.hydra.HydraSwapDirection -import io.novafoundation.nova.feature_swap_core.data.assetExchange.conversion.types.hydra.impl.omnipool.model.DynamicFee -import io.novafoundation.nova.feature_swap_core.data.assetExchange.conversion.types.hydra.impl.omnipool.model.OmniPool -import io.novafoundation.nova.feature_swap_core.data.assetExchange.conversion.types.hydra.impl.omnipool.model.OmniPoolFees -import io.novafoundation.nova.feature_swap_core.data.assetExchange.conversion.types.hydra.impl.omnipool.model.OmniPoolToken -import io.novafoundation.nova.feature_swap_core.data.assetExchange.conversion.types.hydra.impl.omnipool.model.OmnipoolAssetState -import io.novafoundation.nova.feature_swap_core.data.assetExchange.conversion.types.hydra.impl.omnipool.model.feeParamsConstant -import io.novafoundation.nova.feature_swap_core.data.assetExchange.conversion.types.hydra.impl.omnipool.model.quote -import io.novafoundation.nova.feature_swap_core.data.assetExchange.conversion.types.hydra.impl.subscribeToTransferableBalance -import io.novafoundation.nova.feature_swap_core.data.network.HydraDxAssetId -import io.novafoundation.nova.feature_swap_core.data.network.HydraDxAssetIdConverter -import io.novafoundation.nova.feature_swap_core.data.network.toOnChainIdOrThrow +import io.novafoundation.nova.feature_swap_core.data.assetExchange.conversion.types.hydra.sources.omnipool.model.DynamicFee +import io.novafoundation.nova.feature_swap_core.data.assetExchange.conversion.types.hydra.sources.omnipool.model.OmniPool +import io.novafoundation.nova.feature_swap_core.data.assetExchange.conversion.types.hydra.sources.omnipool.model.OmniPoolFees +import io.novafoundation.nova.feature_swap_core.data.assetExchange.conversion.types.hydra.sources.omnipool.model.OmniPoolToken +import io.novafoundation.nova.feature_swap_core.data.assetExchange.conversion.types.hydra.sources.omnipool.model.OmnipoolAssetState +import io.novafoundation.nova.feature_swap_core.data.assetExchange.conversion.types.hydra.sources.omnipool.model.RemoteIdAndLocalAsset +import io.novafoundation.nova.feature_swap_core.data.assetExchange.conversion.types.hydra.sources.omnipool.model.feeParamsConstant +import io.novafoundation.nova.feature_swap_core.data.assetExchange.conversion.types.hydra.sources.omnipool.model.quote +import io.novafoundation.nova.feature_swap_core.data.assetExchange.conversion.types.hydra.sources.subscribeToTransferableBalance +import io.novafoundation.nova.feature_swap_core_api.data.network.HydraDxAssetId +import io.novafoundation.nova.feature_swap_core_api.data.network.HydraDxAssetIdConverter +import io.novafoundation.nova.feature_swap_core_api.data.primitive.errors.SwapQuoteException +import io.novafoundation.nova.feature_swap_core_api.data.primitive.model.SwapDirection +import io.novafoundation.nova.feature_swap_core_api.data.types.hydra.HydraDxQuotingSource import io.novafoundation.nova.runtime.ext.fullId import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain @@ -35,7 +30,6 @@ import io.novafoundation.nova.runtime.storage.source.StorageDataSource import io.novafoundation.nova.runtime.storage.source.query.api.observeNonNull import io.novafoundation.nova.runtime.storage.source.query.metadata import io.novasama.substrate_sdk_android.runtime.AccountId -import io.novasama.substrate_sdk_android.runtime.extrinsic.ExtrinsicBuilder import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableSharedFlow @@ -45,37 +39,37 @@ import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onEach import java.math.BigInteger -class OmniPoolConversionSourceFactory( +class OmniPoolQuotingSourceFactory( private val remoteStorageSource: StorageDataSource, private val chainRegistry: ChainRegistry, private val hydraDxAssetIdConverter: HydraDxAssetIdConverter, -) : HydraDxConversionSource.Factory { +) : HydraDxQuotingSource.Factory { companion object { const val SOURCE_ID = "OmniPool" } - override fun create(chain: Chain): HydraDxConversionSource { - return OmniPoolConversionSource( + override fun create(chain: Chain): OmniPoolQuotingSource { + return RealOmniPoolQuotingSource( remoteStorageSource = remoteStorageSource, chainRegistry = chainRegistry, hydraDxAssetIdConverter = hydraDxAssetIdConverter, - chain = chain + chain = chain, ) } } -private class OmniPoolConversionSource( +private class RealOmniPoolQuotingSource( private val remoteStorageSource: StorageDataSource, private val chainRegistry: ChainRegistry, private val hydraDxAssetIdConverter: HydraDxAssetIdConverter, private val chain: Chain, -) : HydraDxConversionSource { +) : OmniPoolQuotingSource { - override val identifier: HydraDxSwapSourceId = OmniPoolConversionSourceFactory.SOURCE_ID + override val identifier = OmniPoolQuotingSourceFactory.SOURCE_ID - private val pooledOnChainAssetIdsState: MutableSharedFlow> = singleReplaySharedFlow() + private val pooledOnChainAssetIdsState: MutableSharedFlow> = singleReplaySharedFlow() private val omniPoolFlow: MutableSharedFlow = singleReplaySharedFlow() @@ -86,28 +80,19 @@ private class OmniPoolConversionSource( pooledOnChainAssetIdsState.emit(pooledChainAssetsIds) } - override suspend fun availableSwapDirections(): MultiMapList { - val pooledChainAssetsIds = pooledOnChainAssetIdsState.first() + override suspend fun availableSwapDirections(): Collection { + val pooledOnChainAssetIds = pooledOnChainAssetIdsState.first() - return pooledChainAssetsIds.associateBy( - keySelector = { it.second }, - valueTransform = { (_, currentId) -> + return pooledOnChainAssetIds.flatMap { remoteAndLocal -> + pooledOnChainAssetIds.mapNotNull { otherRemoteAndLocal -> // In OmniPool, each asset is tradable with any other except itself - pooledChainAssetsIds.mapNotNull { (_, otherId) -> - otherId.takeIf { currentId != otherId }?.let { OmniPoolSwapDirection(currentId, otherId) } + if (remoteAndLocal.second.id != otherRemoteAndLocal.second.id) { + RealOmniPoolQuotingEdge(fromAsset = remoteAndLocal, toAsset = otherRemoteAndLocal) + } else { + null } } - ) - } - - override suspend fun quote(args: HydraDxConversionSourceQuoteArgs): BigInteger { - val omniPool = omniPoolFlow.first() - - val omniPoolTokenIdIn = hydraDxAssetIdConverter.toOnChainIdOrThrow(args.chainAssetIn) - val omniPoolTokenIdOut = hydraDxAssetIdConverter.toOnChainIdOrThrow(args.chainAssetOut) - - return omniPool.quote(omniPoolTokenIdIn, omniPoolTokenIdOut, args.amount, args.swapDirection) - ?: throw SwapQuoteException.NotEnoughLiquidity + } } @OptIn(ExperimentalCoroutinesApi::class) @@ -130,8 +115,7 @@ private class OmniPoolConversionSource( val poolAccountId = omniPoolAccountId() - val omniPoolBalancesFlow = pooledAssets.map { (omniPoolTokenId, chainAssetId) -> - val chainAsset = chain.assetsById.getValue(chainAssetId.assetId) + val omniPoolBalancesFlow = pooledAssets.map { (omniPoolTokenId, chainAsset) -> remoteStorageSource.subscribeToTransferableBalance(chainAsset, poolAccountId, subscriptionBuilder).map { omniPoolTokenId to it } @@ -165,13 +149,13 @@ private class OmniPoolConversionSource( } } - private suspend fun matchKnownChainAssetIds(onChainIds: List): List { + private suspend fun matchKnownChainAssetIds(onChainIds: List): List { val hydraDxAssetIds = hydraDxAssetIdConverter.allOnChainIds(chain) return onChainIds.mapNotNull { onChainId -> val asset = hydraDxAssetIds[onChainId] ?: return@mapNotNull null - onChainId to asset.fullId + onChainId to asset } } @@ -198,42 +182,6 @@ private class OmniPoolConversionSource( return OmniPool(tokensState) } - private fun ExtrinsicBuilder.sell( - assetIdIn: HydraDxAssetId, - assetIdOut: HydraDxAssetId, - amountIn: BigInteger, - minBuyAmount: BigInteger - ) { - call( - moduleName = Modules.OMNIPOOL, - callName = "sell", - arguments = mapOf( - "asset_in" to assetIdIn, - "asset_out" to assetIdOut, - "amount" to amountIn, - "min_buy_amount" to minBuyAmount - ) - ) - } - - private fun ExtrinsicBuilder.buy( - assetIdIn: HydraDxAssetId, - assetIdOut: HydraDxAssetId, - amountOut: BigInteger, - maxSellAmount: BigInteger - ) { - call( - moduleName = Modules.OMNIPOOL, - callName = "buy", - arguments = mapOf( - "asset_out" to assetIdOut, - "asset_in" to assetIdIn, - "amount" to amountOut, - "max_sell_amount" to maxSellAmount - ) - ) - } - private suspend fun getDefaultFees(): OmniPoolFees { val runtime = chainRegistry.getRuntime(chain.id) @@ -246,23 +194,24 @@ private class OmniPoolConversionSource( ) } - private class OmniPoolSwapDirection(override val from: FullChainAssetId, override val to: FullChainAssetId) : HydraSwapDirection { + private inner class RealOmniPoolQuotingEdge( + override val fromAsset: RemoteIdAndLocalAsset, + override val toAsset: RemoteIdAndLocalAsset, + ) : OmniPoolQuotingSource.Edge { - override val params: Map - get() = emptyMap() - } -} + override val from: FullChainAssetId = fromAsset.second.fullId + + override val to: FullChainAssetId = toAsset.second.fullId + + override suspend fun quote(amount: BigInteger, direction: SwapDirection): BigInteger { + val omniPool = omniPoolFlow.first() -fun omniPoolAccountId(): AccountId { - return "modlomnipool".encodeToByteArray().padEnd(expectedSize = 32, padding = 0) + return omniPool.quote(fromAsset.first, toAsset.first, amount, direction) + ?: throw SwapQuoteException.NotEnoughLiquidity + } + } } -typealias RemoteAndLocalId = Pair -val RemoteAndLocalId.remoteId - get() = first -val RemoteAndLocalId.localId - get() = second -typealias RemoteAndLocalIdOptional = Pair diff --git a/feature-swap-core/src/main/java/io/novafoundation/nova/feature_swap_core/data/assetExchange/conversion/types/hydra/sources/omnipool/model/Aliases.kt b/feature-swap-core/src/main/java/io/novafoundation/nova/feature_swap_core/data/assetExchange/conversion/types/hydra/sources/omnipool/model/Aliases.kt new file mode 100644 index 0000000000..37f766b872 --- /dev/null +++ b/feature-swap-core/src/main/java/io/novafoundation/nova/feature_swap_core/data/assetExchange/conversion/types/hydra/sources/omnipool/model/Aliases.kt @@ -0,0 +1,20 @@ +package io.novafoundation.nova.feature_swap_core.data.assetExchange.conversion.types.hydra.sources.omnipool.model + +import io.novafoundation.nova.feature_swap_core_api.data.network.HydraDxAssetId +import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain +import io.novafoundation.nova.runtime.multiNetwork.chain.model.FullChainAssetId + +typealias RemoteAndLocalId = Pair +typealias RemoteIdAndLocalAsset = Pair +typealias RemoteAndLocalIdOptional = Pair + +@Suppress("UNCHECKED_CAST") +fun RemoteAndLocalIdOptional.flatten(): RemoteAndLocalId? { + return second?.let { this as RemoteAndLocalId } +} + +val RemoteAndLocalId.remoteId + get() = first + +val RemoteAndLocalId.localId + get() = second diff --git a/feature-swap-core/src/main/java/io/novafoundation/nova/feature_swap_core/data/assetExchange/conversion/types/hydra/impl/omnipool/model/DynamicFee.kt b/feature-swap-core/src/main/java/io/novafoundation/nova/feature_swap_core/data/assetExchange/conversion/types/hydra/sources/omnipool/model/DynamicFee.kt similarity index 96% rename from feature-swap-core/src/main/java/io/novafoundation/nova/feature_swap_core/data/assetExchange/conversion/types/hydra/impl/omnipool/model/DynamicFee.kt rename to feature-swap-core/src/main/java/io/novafoundation/nova/feature_swap_core/data/assetExchange/conversion/types/hydra/sources/omnipool/model/DynamicFee.kt index 61e3e6a508..b868f810ab 100644 --- a/feature-swap-core/src/main/java/io/novafoundation/nova/feature_swap_core/data/assetExchange/conversion/types/hydra/impl/omnipool/model/DynamicFee.kt +++ b/feature-swap-core/src/main/java/io/novafoundation/nova/feature_swap_core/data/assetExchange/conversion/types/hydra/sources/omnipool/model/DynamicFee.kt @@ -1,4 +1,4 @@ -package io.novafoundation.nova.feature_swap_core.data.assetExchange.conversion.types.hydra.impl.omnipool.model +package io.novafoundation.nova.feature_swap_core.data.assetExchange.conversion.types.hydra.sources.omnipool.model import io.novafoundation.nova.common.data.network.runtime.binding.bindPermill import io.novafoundation.nova.common.data.network.runtime.binding.castToStruct diff --git a/feature-swap-core/src/main/java/io/novafoundation/nova/feature_swap_core/data/assetExchange/conversion/types/hydra/impl/omnipool/model/OmniPool.kt b/feature-swap-core/src/main/java/io/novafoundation/nova/feature_swap_core/data/assetExchange/conversion/types/hydra/sources/omnipool/model/OmniPool.kt similarity index 93% rename from feature-swap-core/src/main/java/io/novafoundation/nova/feature_swap_core/data/assetExchange/conversion/types/hydra/impl/omnipool/model/OmniPool.kt rename to feature-swap-core/src/main/java/io/novafoundation/nova/feature_swap_core/data/assetExchange/conversion/types/hydra/sources/omnipool/model/OmniPool.kt index 1735a75ead..0b3bb2552c 100644 --- a/feature-swap-core/src/main/java/io/novafoundation/nova/feature_swap_core/data/assetExchange/conversion/types/hydra/impl/omnipool/model/OmniPool.kt +++ b/feature-swap-core/src/main/java/io/novafoundation/nova/feature_swap_core/data/assetExchange/conversion/types/hydra/sources/omnipool/model/OmniPool.kt @@ -1,8 +1,8 @@ -package io.novafoundation.nova.feature_swap_core.data.assetExchange.conversion.types.hydra.impl.omnipool.model +package io.novafoundation.nova.feature_swap_core.data.assetExchange.conversion.types.hydra.sources.omnipool.model import io.novafoundation.nova.common.utils.Perbill -import io.novafoundation.nova.feature_swap_core.domain.model.SwapDirection -import io.novafoundation.nova.feature_swap_core.data.network.HydraDxAssetId +import io.novafoundation.nova.feature_swap_core_api.data.network.HydraDxAssetId +import io.novafoundation.nova.feature_swap_core_api.data.primitive.model.SwapDirection import java.math.BigInteger import kotlin.math.floor diff --git a/feature-swap-core/src/main/java/io/novafoundation/nova/feature_swap_core/data/assetExchange/conversion/types/hydra/impl/omnipool/model/OmnipoolAssetState.kt b/feature-swap-core/src/main/java/io/novafoundation/nova/feature_swap_core/data/assetExchange/conversion/types/hydra/sources/omnipool/model/OmnipoolAssetState.kt similarity index 86% rename from feature-swap-core/src/main/java/io/novafoundation/nova/feature_swap_core/data/assetExchange/conversion/types/hydra/impl/omnipool/model/OmnipoolAssetState.kt rename to feature-swap-core/src/main/java/io/novafoundation/nova/feature_swap_core/data/assetExchange/conversion/types/hydra/sources/omnipool/model/OmnipoolAssetState.kt index c1bcaa6670..2c040c6dd2 100644 --- a/feature-swap-core/src/main/java/io/novafoundation/nova/feature_swap_core/data/assetExchange/conversion/types/hydra/impl/omnipool/model/OmnipoolAssetState.kt +++ b/feature-swap-core/src/main/java/io/novafoundation/nova/feature_swap_core/data/assetExchange/conversion/types/hydra/sources/omnipool/model/OmnipoolAssetState.kt @@ -1,8 +1,8 @@ -package io.novafoundation.nova.feature_swap_core.data.assetExchange.conversion.types.hydra.impl.omnipool.model +package io.novafoundation.nova.feature_swap_core.data.assetExchange.conversion.types.hydra.sources.omnipool.model import io.novafoundation.nova.common.data.network.runtime.binding.bindNumber import io.novafoundation.nova.common.data.network.runtime.binding.castToStruct -import io.novafoundation.nova.feature_swap_core.data.network.HydraDxAssetId +import io.novafoundation.nova.feature_swap_core_api.data.network.HydraDxAssetId import java.math.BigInteger class OmnipoolAssetState( diff --git a/feature-swap-core/src/main/java/io/novafoundation/nova/feature_swap_core/data/assetExchange/conversion/types/hydra/impl/omnipool/model/Tradeability.kt b/feature-swap-core/src/main/java/io/novafoundation/nova/feature_swap_core/data/assetExchange/conversion/types/hydra/sources/omnipool/model/Tradeability.kt similarity index 94% rename from feature-swap-core/src/main/java/io/novafoundation/nova/feature_swap_core/data/assetExchange/conversion/types/hydra/impl/omnipool/model/Tradeability.kt rename to feature-swap-core/src/main/java/io/novafoundation/nova/feature_swap_core/data/assetExchange/conversion/types/hydra/sources/omnipool/model/Tradeability.kt index 64858a4257..3c081d2bd4 100644 --- a/feature-swap-core/src/main/java/io/novafoundation/nova/feature_swap_core/data/assetExchange/conversion/types/hydra/impl/omnipool/model/Tradeability.kt +++ b/feature-swap-core/src/main/java/io/novafoundation/nova/feature_swap_core/data/assetExchange/conversion/types/hydra/sources/omnipool/model/Tradeability.kt @@ -1,4 +1,4 @@ -package io.novafoundation.nova.feature_swap_core.data.assetExchange.conversion.types.hydra.impl.omnipool.model +package io.novafoundation.nova.feature_swap_core.data.assetExchange.conversion.types.hydra.sources.omnipool.model import io.novafoundation.nova.common.data.network.runtime.binding.bindNumber import io.novafoundation.nova.common.data.network.runtime.binding.castToStruct diff --git a/feature-swap-core/src/main/java/io/novafoundation/nova/feature_swap_core/data/assetExchange/conversion/types/hydra/impl/stableswap/AssetRegistryApi.kt b/feature-swap-core/src/main/java/io/novafoundation/nova/feature_swap_core/data/assetExchange/conversion/types/hydra/sources/stableswap/AssetRegistryApi.kt similarity index 91% rename from feature-swap-core/src/main/java/io/novafoundation/nova/feature_swap_core/data/assetExchange/conversion/types/hydra/impl/stableswap/AssetRegistryApi.kt rename to feature-swap-core/src/main/java/io/novafoundation/nova/feature_swap_core/data/assetExchange/conversion/types/hydra/sources/stableswap/AssetRegistryApi.kt index ec1d599df8..fc8ef6e063 100644 --- a/feature-swap-core/src/main/java/io/novafoundation/nova/feature_swap_core/data/assetExchange/conversion/types/hydra/impl/stableswap/AssetRegistryApi.kt +++ b/feature-swap-core/src/main/java/io/novafoundation/nova/feature_swap_core/data/assetExchange/conversion/types/hydra/sources/stableswap/AssetRegistryApi.kt @@ -1,9 +1,9 @@ -package io.novafoundation.nova.feature_swap_core.data.assetExchange.conversion.types.hydra.impl.stableswap +package io.novafoundation.nova.feature_swap_core.data.assetExchange.conversion.types.hydra.sources.stableswap import io.novafoundation.nova.common.data.network.runtime.binding.bindInt import io.novafoundation.nova.common.data.network.runtime.binding.castToStruct import io.novafoundation.nova.common.utils.assetRegistry -import io.novafoundation.nova.feature_swap_core.data.network.HydraDxAssetId +import io.novafoundation.nova.feature_swap_core_api.data.network.HydraDxAssetId import io.novafoundation.nova.runtime.storage.source.query.StorageQueryContext import io.novafoundation.nova.runtime.storage.source.query.api.QueryableModule import io.novafoundation.nova.runtime.storage.source.query.api.QueryableStorageEntry1 diff --git a/feature-swap-core/src/main/java/io/novafoundation/nova/feature_swap_core/data/assetExchange/conversion/types/hydra/impl/stableswap/StableConversionSource.kt b/feature-swap-core/src/main/java/io/novafoundation/nova/feature_swap_core/data/assetExchange/conversion/types/hydra/sources/stableswap/RealStableSwapQuotingSource.kt similarity index 76% rename from feature-swap-core/src/main/java/io/novafoundation/nova/feature_swap_core/data/assetExchange/conversion/types/hydra/impl/stableswap/StableConversionSource.kt rename to feature-swap-core/src/main/java/io/novafoundation/nova/feature_swap_core/data/assetExchange/conversion/types/hydra/sources/stableswap/RealStableSwapQuotingSource.kt index 364c6a3423..a95f5e3e1c 100644 --- a/feature-swap-core/src/main/java/io/novafoundation/nova/feature_swap_core/data/assetExchange/conversion/types/hydra/impl/stableswap/StableConversionSource.kt +++ b/feature-swap-core/src/main/java/io/novafoundation/nova/feature_swap_core/data/assetExchange/conversion/types/hydra/sources/stableswap/RealStableSwapQuotingSource.kt @@ -1,32 +1,28 @@ -package io.novafoundation.nova.feature_swap_core.data.assetExchange.conversion.types.hydra.impl.stableswap +package io.novafoundation.nova.feature_swap_core.data.assetExchange.conversion.types.hydra.sources.stableswap import com.google.gson.Gson import io.novafoundation.nova.common.data.network.runtime.binding.BlockNumber import io.novafoundation.nova.common.data.network.runtime.binding.orEmpty import io.novafoundation.nova.common.domain.balance.TransferableMode import io.novafoundation.nova.common.domain.balance.calculateTransferable -import io.novafoundation.nova.common.utils.MultiMapList import io.novafoundation.nova.common.utils.filterNotNull -import io.novafoundation.nova.common.utils.graph.Edge -import io.novafoundation.nova.common.utils.graph.Graph -import io.novafoundation.nova.common.utils.graph.create import io.novafoundation.nova.common.utils.orZero import io.novafoundation.nova.common.utils.singleReplaySharedFlow import io.novafoundation.nova.common.utils.toMultiSubscription import io.novafoundation.nova.core.updater.SharedRequestsBuilder -import io.novafoundation.nova.feature_swap_core.data.assetExchange.conversion.types.hydra.HydraDxConversionSource -import io.novafoundation.nova.feature_swap_core.data.assetExchange.conversion.types.hydra.HydraDxConversionSourceQuoteArgs -import io.novafoundation.nova.feature_swap_core.domain.model.SwapQuoteException -import io.novafoundation.nova.feature_swap_core.data.assetExchange.conversion.types.hydra.HydraSwapDirection -import io.novafoundation.nova.feature_swap_core.data.assetExchange.conversion.types.hydra.impl.omnipool.RemoteAndLocalIdOptional -import io.novafoundation.nova.feature_swap_core.data.assetExchange.conversion.types.hydra.impl.omnipool.omniPoolAccountId -import io.novafoundation.nova.feature_swap_core.data.assetExchange.conversion.types.hydra.impl.stableswap.model.StablePool -import io.novafoundation.nova.feature_swap_core.data.assetExchange.conversion.types.hydra.impl.stableswap.model.StablePoolAsset -import io.novafoundation.nova.feature_swap_core.data.assetExchange.conversion.types.hydra.impl.stableswap.model.StableSwapPoolInfo -import io.novafoundation.nova.feature_swap_core.data.assetExchange.conversion.types.hydra.impl.stableswap.model.quote -import io.novafoundation.nova.feature_swap_core.data.network.HydraDxAssetId -import io.novafoundation.nova.feature_swap_core.data.network.HydraDxAssetIdConverter -import io.novafoundation.nova.feature_swap_core.data.network.toOnChainIdOrThrow +import io.novafoundation.nova.feature_swap_core.data.assetExchange.conversion.types.hydra.sources.omnipool.model.RemoteAndLocalId +import io.novafoundation.nova.feature_swap_core.data.assetExchange.conversion.types.hydra.sources.omnipool.model.RemoteAndLocalIdOptional +import io.novafoundation.nova.feature_swap_core.data.assetExchange.conversion.types.hydra.sources.omnipool.model.flatten +import io.novafoundation.nova.feature_swap_core.data.assetExchange.conversion.types.hydra.sources.omnipool.omniPoolAccountId +import io.novafoundation.nova.feature_swap_core.data.assetExchange.conversion.types.hydra.sources.stableswap.model.StableSwapPoolInfo +import io.novafoundation.nova.feature_swap_core_api.data.network.HydraDxAssetId +import io.novafoundation.nova.feature_swap_core_api.data.network.HydraDxAssetIdConverter +import io.novafoundation.nova.feature_swap_core_api.data.primitive.errors.SwapQuoteException +import io.novafoundation.nova.feature_swap_core_api.data.primitive.model.SwapDirection +import io.novafoundation.nova.feature_swap_core_api.data.types.hydra.HydraDxQuotingSource +import io.novafoundation.nova.feature_swap_core.data.assetExchange.conversion.types.hydra.sources.stableswap.model.StablePool +import io.novafoundation.nova.feature_swap_core.data.assetExchange.conversion.types.hydra.sources.stableswap.model.StablePoolAsset +import io.novafoundation.nova.feature_swap_core.data.assetExchange.conversion.types.hydra.sources.stableswap.model.quote import io.novafoundation.nova.runtime.ext.fullId import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain import io.novafoundation.nova.runtime.multiNetwork.chain.model.FullChainAssetId @@ -36,7 +32,6 @@ import io.novafoundation.nova.runtime.storage.source.query.metadata import io.novasama.substrate_sdk_android.encrypt.json.asLittleEndianBytes import io.novasama.substrate_sdk_android.hash.Hasher.blake2b256 import io.novasama.substrate_sdk_android.runtime.AccountId -import java.math.BigInteger import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Deferred import kotlinx.coroutines.ExperimentalCoroutinesApi @@ -48,22 +43,22 @@ import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onEach +import java.math.BigInteger -private const val POOL_ID_PARAM_KEY = "PoolId" - -class StableConversionSourceFactory( +class StableSwapQuotingSourceFactory( private val remoteStorageSource: StorageDataSource, private val hydraDxAssetIdConverter: HydraDxAssetIdConverter, private val gson: Gson, private val chainStateRepository: ChainStateRepository -) : HydraDxConversionSource.Factory { +) : HydraDxQuotingSource.Factory { companion object { + const val ID = "StableSwap" } - override fun create(chain: Chain): HydraDxConversionSource { - return StableConversionSource( + override fun create(chain: Chain): StableSwapQuotingSource { + return RealStableSwapQuotingSource( remoteStorageSource = remoteStorageSource, hydraDxAssetIdConverter = hydraDxAssetIdConverter, chain = chain, @@ -73,15 +68,15 @@ class StableConversionSourceFactory( } } -private class StableConversionSource( +private class RealStableSwapQuotingSource( private val remoteStorageSource: StorageDataSource, private val hydraDxAssetIdConverter: HydraDxAssetIdConverter, - private val chain: Chain, + override val chain: Chain, private val gson: Gson, private val chainStateRepository: ChainStateRepository, -) : HydraDxConversionSource { +) : StableSwapQuotingSource { - override val identifier: String = StableConversionSourceFactory.ID + override val identifier: String = StableSwapQuotingSourceFactory.ID private val initialPoolsInfo: MutableSharedFlow> = singleReplaySharedFlow() @@ -94,24 +89,12 @@ private class StableConversionSource( initialPoolsInfo.emit(poolInitialInfo) } - override suspend fun availableSwapDirections(): MultiMapList { + override suspend fun availableSwapDirections(): Collection { val poolInitialInfo = initialPoolsInfo.first() return poolInitialInfo.allPossibleDirections() } - override suspend fun quote(args: HydraDxConversionSourceQuoteArgs): BigInteger { - val allPools = stablePools.first() - val poolId = args.params.poolIdParam() - val relevantPool = allPools.first { it.sharedAsset.id == poolId } - - val hydraDxAssetIdIn = hydraDxAssetIdConverter.toOnChainIdOrThrow(args.chainAssetIn) - val hydraDxAssetIdOut = hydraDxAssetIdConverter.toOnChainIdOrThrow(args.chainAssetOut) - - return relevantPool.quote(hydraDxAssetIdIn, hydraDxAssetIdOut, args.amount, args.swapDirection) - ?: throw SwapQuoteException.NotEnoughLiquidity - } - @OptIn(ExperimentalCoroutinesApi::class) override suspend fun runSubscriptions( userAccountId: AccountId, @@ -266,46 +249,48 @@ private class StableConversionSource( } } - private fun Collection.allPossibleDirections(): MultiMapList { - val perPoolMaps = map { (poolAssetId, poolAssets) -> + private fun Collection.allPossibleDirections(): Collection { + return flatMap { (poolAssetId, poolAssets) -> val allPoolAssetIds = buildList { - addAll(poolAssets.mapNotNull { it.second }) + addAll(poolAssets.mapNotNull { it.flatten() }) - val sharedAssetId = poolAssetId.second + val sharedAssetId = poolAssetId.flatten() if (sharedAssetId != null) { add(sharedAssetId) } } - allPoolAssetIds.associateWith { assetId -> + allPoolAssetIds.flatMap { assetId -> allPoolAssetIds.mapNotNull { otherAssetId -> otherAssetId.takeIf { assetId != otherAssetId } - ?.let { StableSwapDirection(assetId, otherAssetId, poolAssetId.first) } + ?.let { RealStableSwapQuotingEdge(assetId, otherAssetId, poolAssetId.first) } } } } - - return Graph.create(perPoolMaps).adjacencyList - } - - private fun Map.poolIdParam(): HydraDxAssetId { - return getValue(POOL_ID_PARAM_KEY).toBigInteger() - } - - private class StableSwapDirection( - override val from: FullChainAssetId, - override val to: FullChainAssetId, - poolId: HydraDxAssetId - ) : HydraSwapDirection, Edge { - val poolIdRaw = poolId.toString() - - override val params: Map - get() = mapOf(POOL_ID_PARAM_KEY to poolIdRaw) } private data class PoolInitialInfo( val sharedAsset: RemoteAndLocalIdOptional, val poolAssets: List ) + + inner class RealStableSwapQuotingEdge( + override val fromAsset: RemoteAndLocalId, + override val toAsset: RemoteAndLocalId, + override val poolId: HydraDxAssetId + ) : StableSwapQuotingSource.Edge { + + override val from: FullChainAssetId = fromAsset.second + + override val to: FullChainAssetId = toAsset.second + + override suspend fun quote(amount: BigInteger, direction: SwapDirection): BigInteger { + val allPools = stablePools.first() + val relevantPool = allPools.first { it.sharedAsset.id == poolId } + + return relevantPool.quote(fromAsset.first, toAsset.first, amount, direction) + ?: throw SwapQuoteException.NotEnoughLiquidity + } + } } diff --git a/feature-swap-core/src/main/java/io/novafoundation/nova/feature_swap_core/data/assetExchange/conversion/types/hydra/impl/stableswap/StableSwapApi.kt b/feature-swap-core/src/main/java/io/novafoundation/nova/feature_swap_core/data/assetExchange/conversion/types/hydra/sources/stableswap/StableSwapApi.kt similarity index 83% rename from feature-swap-core/src/main/java/io/novafoundation/nova/feature_swap_core/data/assetExchange/conversion/types/hydra/impl/stableswap/StableSwapApi.kt rename to feature-swap-core/src/main/java/io/novafoundation/nova/feature_swap_core/data/assetExchange/conversion/types/hydra/sources/stableswap/StableSwapApi.kt index 45afc2242a..599003871f 100644 --- a/feature-swap-core/src/main/java/io/novafoundation/nova/feature_swap_core/data/assetExchange/conversion/types/hydra/impl/stableswap/StableSwapApi.kt +++ b/feature-swap-core/src/main/java/io/novafoundation/nova/feature_swap_core/data/assetExchange/conversion/types/hydra/sources/stableswap/StableSwapApi.kt @@ -1,10 +1,10 @@ -package io.novafoundation.nova.feature_swap_core.data.assetExchange.conversion.types.hydra.impl.stableswap +package io.novafoundation.nova.feature_swap_core.data.assetExchange.conversion.types.hydra.sources.stableswap import io.novafoundation.nova.common.utils.stableSwap import io.novafoundation.nova.common.utils.stableSwapOrNull -import io.novafoundation.nova.feature_swap_core.data.assetExchange.conversion.types.hydra.impl.stableswap.model.StableSwapPoolInfo -import io.novafoundation.nova.feature_swap_core.data.assetExchange.conversion.types.hydra.impl.stableswap.model.bindStablePoolInfo -import io.novafoundation.nova.feature_swap_core.data.network.HydraDxAssetId +import io.novafoundation.nova.feature_swap_core.data.assetExchange.conversion.types.hydra.sources.stableswap.model.StableSwapPoolInfo +import io.novafoundation.nova.feature_swap_core.data.assetExchange.conversion.types.hydra.sources.stableswap.model.bindStablePoolInfo +import io.novafoundation.nova.feature_swap_core_api.data.network.HydraDxAssetId import io.novafoundation.nova.runtime.storage.source.query.StorageQueryContext import io.novafoundation.nova.runtime.storage.source.query.api.QueryableModule import io.novafoundation.nova.runtime.storage.source.query.api.QueryableStorageEntry1 diff --git a/feature-swap-core/src/main/java/io/novafoundation/nova/feature_swap_core/data/assetExchange/conversion/types/hydra/sources/stableswap/StableSwapQuotingSource.kt b/feature-swap-core/src/main/java/io/novafoundation/nova/feature_swap_core/data/assetExchange/conversion/types/hydra/sources/stableswap/StableSwapQuotingSource.kt new file mode 100644 index 0000000000..6bcb772d39 --- /dev/null +++ b/feature-swap-core/src/main/java/io/novafoundation/nova/feature_swap_core/data/assetExchange/conversion/types/hydra/sources/stableswap/StableSwapQuotingSource.kt @@ -0,0 +1,21 @@ +package io.novafoundation.nova.feature_swap_core.data.assetExchange.conversion.types.hydra.sources.stableswap + +import io.novafoundation.nova.feature_swap_core.data.assetExchange.conversion.types.hydra.sources.omnipool.model.RemoteAndLocalId +import io.novafoundation.nova.feature_swap_core_api.data.network.HydraDxAssetId +import io.novafoundation.nova.feature_swap_core_api.data.primitive.model.QuotableEdge +import io.novafoundation.nova.feature_swap_core_api.data.types.hydra.HydraDxQuotingSource +import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain + +interface StableSwapQuotingSource : HydraDxQuotingSource { + + val chain: Chain + + interface Edge : QuotableEdge { + + val fromAsset: RemoteAndLocalId + + val toAsset: RemoteAndLocalId + + val poolId: HydraDxAssetId + } +} diff --git a/feature-swap-core/src/main/java/io/novafoundation/nova/feature_swap_core/data/assetExchange/conversion/types/hydra/impl/stableswap/TokensApi.kt b/feature-swap-core/src/main/java/io/novafoundation/nova/feature_swap_core/data/assetExchange/conversion/types/hydra/sources/stableswap/TokensApi.kt similarity index 93% rename from feature-swap-core/src/main/java/io/novafoundation/nova/feature_swap_core/data/assetExchange/conversion/types/hydra/impl/stableswap/TokensApi.kt rename to feature-swap-core/src/main/java/io/novafoundation/nova/feature_swap_core/data/assetExchange/conversion/types/hydra/sources/stableswap/TokensApi.kt index 065c91351c..4f7461cdbd 100644 --- a/feature-swap-core/src/main/java/io/novafoundation/nova/feature_swap_core/data/assetExchange/conversion/types/hydra/impl/stableswap/TokensApi.kt +++ b/feature-swap-core/src/main/java/io/novafoundation/nova/feature_swap_core/data/assetExchange/conversion/types/hydra/sources/stableswap/TokensApi.kt @@ -1,10 +1,10 @@ -package io.novafoundation.nova.feature_swap_core.data.assetExchange.conversion.types.hydra.impl.stableswap +package io.novafoundation.nova.feature_swap_core.data.assetExchange.conversion.types.hydra.sources.stableswap import io.novafoundation.nova.common.data.network.runtime.binding.AccountBalance import io.novafoundation.nova.common.data.network.runtime.binding.bindNumber import io.novafoundation.nova.common.data.network.runtime.binding.bindOrmlAccountData import io.novafoundation.nova.common.utils.tokens -import io.novafoundation.nova.feature_swap_core.data.network.HydraDxAssetId +import io.novafoundation.nova.feature_swap_core_api.data.network.HydraDxAssetId import io.novafoundation.nova.runtime.storage.source.query.StorageQueryContext import io.novafoundation.nova.runtime.storage.source.query.api.QueryableModule import io.novafoundation.nova.runtime.storage.source.query.api.QueryableStorageEntry1 diff --git a/feature-swap-core/src/main/java/io/novafoundation/nova/feature_swap_core/data/assetExchange/conversion/types/hydra/impl/stableswap/model/StablePool.kt b/feature-swap-core/src/main/java/io/novafoundation/nova/feature_swap_core/data/assetExchange/conversion/types/hydra/sources/stableswap/model/StablePool.kt similarity index 86% rename from feature-swap-core/src/main/java/io/novafoundation/nova/feature_swap_core/data/assetExchange/conversion/types/hydra/impl/stableswap/model/StablePool.kt rename to feature-swap-core/src/main/java/io/novafoundation/nova/feature_swap_core/data/assetExchange/conversion/types/hydra/sources/stableswap/model/StablePool.kt index 76500f1f48..66d6909531 100644 --- a/feature-swap-core/src/main/java/io/novafoundation/nova/feature_swap_core/data/assetExchange/conversion/types/hydra/impl/stableswap/model/StablePool.kt +++ b/feature-swap-core/src/main/java/io/novafoundation/nova/feature_swap_core/data/assetExchange/conversion/types/hydra/sources/stableswap/model/StablePool.kt @@ -1,19 +1,18 @@ -package io.novafoundation.nova.feature_swap_impl.data.assetExchange.hydraDx.stableswap.model +package io.novafoundation.nova.feature_swap_core.data.assetExchange.conversion.types.hydra.sources.stableswap.model import com.google.gson.Gson import com.google.gson.annotations.SerializedName import io.novafoundation.nova.common.data.network.runtime.binding.BlockNumber import io.novafoundation.nova.common.utils.Perbill -import io.novafoundation.nova.feature_swap_api.domain.model.SwapDirection -import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.HydraDxAssetId -import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.types.Balance +import io.novafoundation.nova.feature_swap_core_api.data.network.HydraDxAssetId +import io.novafoundation.nova.feature_swap_core_api.data.primitive.model.SwapDirection import io.novafoundation.nova.hydra_dx_math.HydraDxMathConversions.fromBridgeResultToBalance import io.novafoundation.nova.hydra_dx_math.stableswap.StableSwapMathBridge import java.math.BigInteger class StablePool( val sharedAsset: StablePoolAsset, - sharedAssetIssuance: Balance, + sharedAssetIssuance: BigInteger, val assets: List, val initialAmplification: BigInteger, val finalAmplification: BigInteger, @@ -48,7 +47,7 @@ class StablePool( } class StablePoolAsset( - val balance: Balance, + val balance: BigInteger, val id: HydraDxAssetId, val decimals: Int ) @@ -56,9 +55,9 @@ class StablePoolAsset( fun StablePool.quote( assetIdIn: HydraDxAssetId, assetIdOut: HydraDxAssetId, - amount: Balance, + amount: BigInteger, direction: SwapDirection -): Balance? { +): BigInteger? { return when (direction) { SwapDirection.SPECIFIED_IN -> calculateOutGivenIn(assetIdIn, assetIdOut, amount) SwapDirection.SPECIFIED_OUT -> calculateInGivenOut(assetIdIn, assetIdOut, amount) @@ -68,8 +67,8 @@ fun StablePool.quote( fun StablePool.calculateOutGivenIn( assetIn: HydraDxAssetId, assetOut: HydraDxAssetId, - amountIn: Balance, -): Balance? { + amountIn: BigInteger, +): BigInteger? { return when { assetIn == sharedAsset.id -> calculateWithdrawOneAsset(assetOut, amountIn) assetOut == sharedAsset.id -> calculateShares(assetIn, amountIn) @@ -80,8 +79,8 @@ fun StablePool.calculateOutGivenIn( fun StablePool.calculateInGivenOut( assetIn: HydraDxAssetId, assetOut: HydraDxAssetId, - amountOut: Balance, -): Balance? { + amountOut: BigInteger, +): BigInteger? { return when { assetOut == sharedAsset.id -> calculateAddOneAsset(assetIn, amountOut) assetIn == sharedAsset.id -> calculateSharesForAmount(assetOut, amountOut) @@ -91,8 +90,8 @@ fun StablePool.calculateInGivenOut( private fun StablePool.calculateAddOneAsset( assetIn: HydraDxAssetId, - amountOut: Balance, -): Balance? { + amountOut: BigInteger, +): BigInteger? { return StableSwapMathBridge.calculate_add_one_asset( reserves, amountOut.toString(), @@ -105,8 +104,8 @@ private fun StablePool.calculateAddOneAsset( private fun StablePool.calculateSharesForAmount( assetOut: HydraDxAssetId, - amountOut: Balance, -): Balance? { + amountOut: BigInteger, +): BigInteger? { return StableSwapMathBridge.calculate_shares_for_amount( reserves, assetOut.toInt(), @@ -120,8 +119,8 @@ private fun StablePool.calculateSharesForAmount( private fun StablePool.calculateIn( assetIn: HydraDxAssetId, assetOut: HydraDxAssetId, - amountOut: Balance, -): Balance? { + amountOut: BigInteger, +): BigInteger? { return StableSwapMathBridge.calculate_in_given_out( reserves, assetIn.toInt(), @@ -134,8 +133,8 @@ private fun StablePool.calculateIn( private fun StablePool.calculateWithdrawOneAsset( assetOut: HydraDxAssetId, - amountIn: Balance, -): Balance? { + amountIn: BigInteger, +): BigInteger? { return StableSwapMathBridge.calculate_liquidity_out_one_asset( reserves, amountIn.toString(), @@ -148,8 +147,8 @@ private fun StablePool.calculateWithdrawOneAsset( private fun StablePool.calculateShares( assetIn: HydraDxAssetId, - amountIn: Balance, -): Balance? { + amountIn: BigInteger, +): BigInteger? { val assets = listOf(SharesAssetInput(assetIn.toInt(), amountIn.toString())) val assetsJson = gson.toJson(assets) @@ -165,8 +164,8 @@ private fun StablePool.calculateShares( private fun StablePool.calculateOut( assetIn: HydraDxAssetId, assetOut: HydraDxAssetId, - amountIn: Balance, -): Balance? { + amountIn: BigInteger, +): BigInteger? { return StableSwapMathBridge.calculate_out_given_in( this.reserves, assetIn.toInt(), diff --git a/feature-swap-core/src/main/java/io/novafoundation/nova/feature_swap_core/data/assetExchange/conversion/types/hydra/impl/stableswap/model/StableSwapPoolInfo.kt b/feature-swap-core/src/main/java/io/novafoundation/nova/feature_swap_core/data/assetExchange/conversion/types/hydra/sources/stableswap/model/StableSwapPoolInfo.kt similarity index 90% rename from feature-swap-core/src/main/java/io/novafoundation/nova/feature_swap_core/data/assetExchange/conversion/types/hydra/impl/stableswap/model/StableSwapPoolInfo.kt rename to feature-swap-core/src/main/java/io/novafoundation/nova/feature_swap_core/data/assetExchange/conversion/types/hydra/sources/stableswap/model/StableSwapPoolInfo.kt index d0c0a38ed0..1612b47282 100644 --- a/feature-swap-core/src/main/java/io/novafoundation/nova/feature_swap_core/data/assetExchange/conversion/types/hydra/impl/stableswap/model/StableSwapPoolInfo.kt +++ b/feature-swap-core/src/main/java/io/novafoundation/nova/feature_swap_core/data/assetExchange/conversion/types/hydra/sources/stableswap/model/StableSwapPoolInfo.kt @@ -1,11 +1,11 @@ -package io.novafoundation.nova.feature_swap_core.data.assetExchange.conversion.types.hydra.impl.stableswap.model +package io.novafoundation.nova.feature_swap_core.data.assetExchange.conversion.types.hydra.sources.stableswap.model import io.novafoundation.nova.common.data.network.runtime.binding.bindList import io.novafoundation.nova.common.data.network.runtime.binding.bindNumber import io.novafoundation.nova.common.data.network.runtime.binding.bindPermill import io.novafoundation.nova.common.data.network.runtime.binding.castToStruct import io.novafoundation.nova.common.utils.Perbill -import io.novafoundation.nova.feature_swap_core.data.network.HydraDxAssetId +import io.novafoundation.nova.feature_swap_core_api.data.network.HydraDxAssetId import java.math.BigInteger class StableSwapPoolInfo( diff --git a/feature-swap-core/src/main/java/io/novafoundation/nova/feature_swap_core/data/assetExchange/conversion/types/hydra/impl/xyk/XYKConversionSource.kt b/feature-swap-core/src/main/java/io/novafoundation/nova/feature_swap_core/data/assetExchange/conversion/types/hydra/sources/xyk/RealXYKSwapQuotingSource.kt similarity index 58% rename from feature-swap-core/src/main/java/io/novafoundation/nova/feature_swap_core/data/assetExchange/conversion/types/hydra/impl/xyk/XYKConversionSource.kt rename to feature-swap-core/src/main/java/io/novafoundation/nova/feature_swap_core/data/assetExchange/conversion/types/hydra/sources/xyk/RealXYKSwapQuotingSource.kt index 39493a9784..addc2e77bb 100644 --- a/feature-swap-core/src/main/java/io/novafoundation/nova/feature_swap_core/data/assetExchange/conversion/types/hydra/impl/xyk/XYKConversionSource.kt +++ b/feature-swap-core/src/main/java/io/novafoundation/nova/feature_swap_core/data/assetExchange/conversion/types/hydra/sources/xyk/RealXYKSwapQuotingSource.kt @@ -1,36 +1,28 @@ -package io.novafoundation.nova.feature_swap_core.data.assetExchange.conversion.types.hydra.impl.xyk +package io.novafoundation.nova.feature_swap_core.data.assetExchange.conversion.types.hydra.sources.xyk import io.novafoundation.nova.common.address.AccountIdKey -import io.novafoundation.nova.common.utils.MultiMapList import io.novafoundation.nova.common.utils.combine -import io.novafoundation.nova.common.utils.graph.Edge -import io.novafoundation.nova.common.utils.graph.GraphBuilder import io.novafoundation.nova.common.utils.singleReplaySharedFlow import io.novafoundation.nova.common.utils.xyk import io.novafoundation.nova.core.updater.SharedRequestsBuilder -import io.novafoundation.nova.feature_swap_core.data.assetExchange.conversion.types.hydra.HydraDxConversionSource -import io.novafoundation.nova.feature_swap_core.data.assetExchange.conversion.types.hydra.HydraDxConversionSourceQuoteArgs -import io.novafoundation.nova.feature_swap_core.domain.model.SwapQuoteException -import io.novafoundation.nova.feature_swap_core.data.assetExchange.conversion.types.hydra.HydraSwapDirection -import io.novafoundation.nova.feature_swap_core.data.assetExchange.conversion.types.hydra.impl.omnipool.RemoteAndLocalId -import io.novafoundation.nova.feature_swap_core.data.assetExchange.conversion.types.hydra.impl.omnipool.localId -import io.novafoundation.nova.feature_swap_core.data.assetExchange.conversion.types.hydra.impl.subscribeToTransferableBalance -import io.novafoundation.nova.feature_swap_core.data.assetExchange.conversion.types.hydra.impl.xyk.model.XYKPool -import io.novafoundation.nova.feature_swap_core.data.assetExchange.conversion.types.hydra.impl.xyk.model.XYKPoolAsset -import io.novafoundation.nova.feature_swap_core.data.assetExchange.conversion.types.hydra.impl.xyk.model.XYKPoolInfo -import io.novafoundation.nova.feature_swap_core.data.assetExchange.conversion.types.hydra.impl.xyk.model.XYKPools -import io.novafoundation.nova.feature_swap_core.data.assetExchange.conversion.types.hydra.impl.xyk.model.poolFeesConstant -import io.novafoundation.nova.feature_swap_core.data.network.HydraDxAssetId -import io.novafoundation.nova.feature_swap_core.data.network.HydraDxAssetIdConverter -import io.novafoundation.nova.feature_swap_core.data.network.toOnChainIdOrThrow +import io.novafoundation.nova.feature_swap_core.data.assetExchange.conversion.types.hydra.sources.omnipool.model.RemoteAndLocalId +import io.novafoundation.nova.feature_swap_core.data.assetExchange.conversion.types.hydra.sources.omnipool.model.localId +import io.novafoundation.nova.feature_swap_core.data.assetExchange.conversion.types.hydra.sources.subscribeToTransferableBalance +import io.novafoundation.nova.feature_swap_core.data.assetExchange.conversion.types.hydra.sources.xyk.model.XYKPool +import io.novafoundation.nova.feature_swap_core.data.assetExchange.conversion.types.hydra.sources.xyk.model.XYKPoolAsset +import io.novafoundation.nova.feature_swap_core.data.assetExchange.conversion.types.hydra.sources.xyk.model.XYKPoolInfo +import io.novafoundation.nova.feature_swap_core.data.assetExchange.conversion.types.hydra.sources.xyk.model.XYKPools +import io.novafoundation.nova.feature_swap_core.data.assetExchange.conversion.types.hydra.sources.xyk.model.poolFeesConstant +import io.novafoundation.nova.feature_swap_core_api.data.network.HydraDxAssetId +import io.novafoundation.nova.feature_swap_core_api.data.network.HydraDxAssetIdConverter +import io.novafoundation.nova.feature_swap_core_api.data.primitive.errors.SwapQuoteException +import io.novafoundation.nova.feature_swap_core_api.data.primitive.model.SwapDirection +import io.novafoundation.nova.feature_swap_core_api.data.types.hydra.HydraDxQuotingSource import io.novafoundation.nova.runtime.ext.fullId import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain import io.novafoundation.nova.runtime.multiNetwork.chain.model.FullChainAssetId import io.novafoundation.nova.runtime.storage.source.StorageDataSource -import io.novasama.substrate_sdk_android.extensions.fromHex -import io.novasama.substrate_sdk_android.extensions.toHexString import io.novasama.substrate_sdk_android.runtime.AccountId -import java.math.BigInteger import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.flow.Flow @@ -38,16 +30,20 @@ import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.map +import java.math.BigInteger -private const val POOL_ID_PARAM_KEY = "PoolId" - -class XYKConversionSourceFactory( +class XYKSwapQuotingSourceFactory( private val remoteStorageSource: StorageDataSource, private val hydraDxAssetIdConverter: HydraDxAssetIdConverter -) : HydraDxConversionSource.Factory { +) : HydraDxQuotingSource.Factory { + + companion object { + + const val ID = "XYK" + } - override fun create(chain: Chain): HydraDxConversionSource { - return XYKConversionSource( + override fun create(chain: Chain): XYKSwapQuotingSource { + return RealXYKSwapQuotingSource( remoteStorageSource = remoteStorageSource, hydraDxAssetIdConverter = hydraDxAssetIdConverter, chain = chain @@ -55,13 +51,13 @@ class XYKConversionSourceFactory( } } -private class XYKConversionSource( +private class RealXYKSwapQuotingSource( private val remoteStorageSource: StorageDataSource, private val hydraDxAssetIdConverter: HydraDxAssetIdConverter, private val chain: Chain -) : HydraDxConversionSource { +) : XYKSwapQuotingSource { - override val identifier: String = "Xyk" + override val identifier: String = XYKSwapQuotingSourceFactory.ID private val initialPoolsInfo: MutableSharedFlow> = singleReplaySharedFlow() @@ -74,33 +70,12 @@ private class XYKConversionSource( initialPoolsInfo.emit(poolInitialInfo) } - override suspend fun availableSwapDirections(): MultiMapList { + override suspend fun availableSwapDirections(): Collection { val poolInitialInfo = initialPoolsInfo.first() return poolInitialInfo.allPossibleDirections() } - override suspend fun quote(args: HydraDxConversionSourceQuoteArgs): BigInteger { - val allPools = xykPools.first() - val poolAddress = args.params.poolAddressParam() - - val hydraDxAssetIdIn = hydraDxAssetIdConverter.toOnChainIdOrThrow(args.chainAssetIn) - val hydraDxAssetIdOut = hydraDxAssetIdConverter.toOnChainIdOrThrow(args.chainAssetOut) - - return allPools.quote(poolAddress, hydraDxAssetIdIn, hydraDxAssetIdOut, args.amount, args.swapDirection) - ?: throw SwapQuoteException.NotEnoughLiquidity - } - - private suspend fun subscribeToBalance( - assetId: RemoteAndLocalId, - poolAddress: AccountId, - subscriptionBuilder: SharedRequestsBuilder - ): Flow { - val chainAsset = chain.assetsById.getValue(assetId.localId.assetId) - - return remoteStorageSource.subscribeToTransferableBalance(chainAsset, poolAddress, subscriptionBuilder) - } - @OptIn(ExperimentalCoroutinesApi::class) override suspend fun runSubscriptions( userAccountId: AccountId, @@ -133,6 +108,16 @@ private class XYKConversionSource( } } + private suspend fun subscribeToBalance( + assetId: RemoteAndLocalId, + poolAddress: AccountId, + subscriptionBuilder: SharedRequestsBuilder + ): Flow { + val chainAsset = chain.assetsById.getValue(assetId.localId.assetId) + + return remoteStorageSource.subscribeToTransferableBalance(chainAsset, poolAddress, subscriptionBuilder) + } + private suspend fun getPools(): Map { return remoteStorageSource.query(chain.id) { runtime.metadata.xykOrNull?.poolAssets?.entries().orEmpty() @@ -157,45 +142,44 @@ private class XYKConversionSource( } } - private fun Collection.allPossibleDirections(): MultiMapList { - val builder = GraphBuilder() - - onEach { poolInfo -> - builder.addEdge( - from = poolInfo.firstAsset.localId, - to = HYKSwapDirection( - from = poolInfo.firstAsset.localId, - to = poolInfo.secondAsset.localId, - poolAddress = poolInfo.poolAddress + private fun Collection.allPossibleDirections(): Collection { + return buildList { + this@allPossibleDirections.forEach { poolInfo -> + add( + RealXYKSwapQuotingEdge( + fromAsset = poolInfo.firstAsset, + toAsset = poolInfo.secondAsset, + poolAddress = poolInfo.poolAddress + ) ) - ) - builder.addEdge( - from = poolInfo.secondAsset.localId, - to = HYKSwapDirection( - from = poolInfo.secondAsset.localId, - to = poolInfo.firstAsset.localId, - poolAddress = poolInfo.poolAddress + + add( + RealXYKSwapQuotingEdge( + fromAsset = poolInfo.secondAsset, + toAsset = poolInfo.firstAsset, + poolAddress = poolInfo.poolAddress + ) ) - ) + } } - - return builder.build().adjacencyList } - private fun Map.poolAddressParam(): AccountId { - return getValue(POOL_ID_PARAM_KEY).fromHex() - } + inner class RealXYKSwapQuotingEdge( + override val fromAsset: RemoteAndLocalId, + override val toAsset: RemoteAndLocalId, + override val poolAddress: AccountId + ) : XYKSwapQuotingSource.Edge { - private class HYKSwapDirection( - override val from: FullChainAssetId, - override val to: FullChainAssetId, - poolAddress: AccountId - ) : HydraSwapDirection, Edge { + override val from: FullChainAssetId = fromAsset.second - val poolAddressRaw = poolAddress.toHexString() + override val to: FullChainAssetId = toAsset.second - override val params: Map - get() = mapOf(POOL_ID_PARAM_KEY to poolAddressRaw) + override suspend fun quote(amount: BigInteger, direction: SwapDirection): BigInteger { + val allPools = xykPools.first() + + return allPools.quote(poolAddress, fromAsset.first, toAsset.first, amount, direction) + ?: throw SwapQuoteException.NotEnoughLiquidity + } } } diff --git a/feature-swap-core/src/main/java/io/novafoundation/nova/feature_swap_core/data/assetExchange/conversion/types/hydra/sources/xyk/StableSwapQuotingSource.kt b/feature-swap-core/src/main/java/io/novafoundation/nova/feature_swap_core/data/assetExchange/conversion/types/hydra/sources/xyk/StableSwapQuotingSource.kt new file mode 100644 index 0000000000..3494a7feab --- /dev/null +++ b/feature-swap-core/src/main/java/io/novafoundation/nova/feature_swap_core/data/assetExchange/conversion/types/hydra/sources/xyk/StableSwapQuotingSource.kt @@ -0,0 +1,18 @@ +package io.novafoundation.nova.feature_swap_core.data.assetExchange.conversion.types.hydra.sources.xyk + +import io.novafoundation.nova.feature_swap_core.data.assetExchange.conversion.types.hydra.sources.omnipool.model.RemoteAndLocalId +import io.novafoundation.nova.feature_swap_core_api.data.primitive.model.QuotableEdge +import io.novafoundation.nova.feature_swap_core_api.data.types.hydra.HydraDxQuotingSource +import io.novasama.substrate_sdk_android.runtime.AccountId + +interface XYKSwapQuotingSource : HydraDxQuotingSource { + + interface Edge : QuotableEdge { + + val fromAsset: RemoteAndLocalId + + val toAsset: RemoteAndLocalId + + val poolAddress: AccountId + } +} diff --git a/feature-swap-core/src/main/java/io/novafoundation/nova/feature_swap_core/data/assetExchange/conversion/types/hydra/impl/xyk/XYKApi.kt b/feature-swap-core/src/main/java/io/novafoundation/nova/feature_swap_core/data/assetExchange/conversion/types/hydra/sources/xyk/XYKApi.kt similarity index 90% rename from feature-swap-core/src/main/java/io/novafoundation/nova/feature_swap_core/data/assetExchange/conversion/types/hydra/impl/xyk/XYKApi.kt rename to feature-swap-core/src/main/java/io/novafoundation/nova/feature_swap_core/data/assetExchange/conversion/types/hydra/sources/xyk/XYKApi.kt index e463bfe3e1..63bfd9e2eb 100644 --- a/feature-swap-core/src/main/java/io/novafoundation/nova/feature_swap_core/data/assetExchange/conversion/types/hydra/impl/xyk/XYKApi.kt +++ b/feature-swap-core/src/main/java/io/novafoundation/nova/feature_swap_core/data/assetExchange/conversion/types/hydra/sources/xyk/XYKApi.kt @@ -1,12 +1,12 @@ -package io.novafoundation.nova.feature_swap_core.data.assetExchange.conversion.types.hydra.impl.xyk +package io.novafoundation.nova.feature_swap_core.data.assetExchange.conversion.types.hydra.sources.xyk import io.novafoundation.nova.common.address.AccountIdKey import io.novafoundation.nova.common.address.intoKey import io.novafoundation.nova.common.data.network.runtime.binding.bindAccountId import io.novafoundation.nova.common.utils.xyk import io.novafoundation.nova.common.utils.xykOrNull -import io.novafoundation.nova.feature_swap_core.data.assetExchange.conversion.types.hydra.impl.xyk.model.XYKPoolInfo -import io.novafoundation.nova.feature_swap_core.data.assetExchange.conversion.types.hydra.impl.xyk.model.bindXYKPoolInfo +import io.novafoundation.nova.feature_swap_core.data.assetExchange.conversion.types.hydra.sources.xyk.model.XYKPoolInfo +import io.novafoundation.nova.feature_swap_core.data.assetExchange.conversion.types.hydra.sources.xyk.model.bindXYKPoolInfo import io.novafoundation.nova.runtime.storage.source.query.StorageQueryContext import io.novafoundation.nova.runtime.storage.source.query.api.QueryableModule import io.novafoundation.nova.runtime.storage.source.query.api.QueryableStorageEntry1 diff --git a/feature-swap-core/src/main/java/io/novafoundation/nova/feature_swap_core/data/assetExchange/conversion/types/hydra/impl/xyk/model/XYKFees.kt b/feature-swap-core/src/main/java/io/novafoundation/nova/feature_swap_core/data/assetExchange/conversion/types/hydra/sources/xyk/model/XYKFees.kt similarity index 94% rename from feature-swap-core/src/main/java/io/novafoundation/nova/feature_swap_core/data/assetExchange/conversion/types/hydra/impl/xyk/model/XYKFees.kt rename to feature-swap-core/src/main/java/io/novafoundation/nova/feature_swap_core/data/assetExchange/conversion/types/hydra/sources/xyk/model/XYKFees.kt index d6159b8341..999376f0c7 100644 --- a/feature-swap-core/src/main/java/io/novafoundation/nova/feature_swap_core/data/assetExchange/conversion/types/hydra/impl/xyk/model/XYKFees.kt +++ b/feature-swap-core/src/main/java/io/novafoundation/nova/feature_swap_core/data/assetExchange/conversion/types/hydra/sources/xyk/model/XYKFees.kt @@ -1,4 +1,4 @@ -package io.novafoundation.nova.feature_swap_core.data.assetExchange.conversion.types.hydra.impl.xyk.model +package io.novafoundation.nova.feature_swap_core.data.assetExchange.conversion.types.hydra.sources.xyk.model import io.novafoundation.nova.common.data.network.runtime.binding.bindInt import io.novafoundation.nova.common.data.network.runtime.binding.castToList diff --git a/feature-swap-core/src/main/java/io/novafoundation/nova/feature_swap_core/data/assetExchange/conversion/types/hydra/impl/xyk/model/XYKPool.kt b/feature-swap-core/src/main/java/io/novafoundation/nova/feature_swap_core/data/assetExchange/conversion/types/hydra/sources/xyk/model/XYKPool.kt similarity index 93% rename from feature-swap-core/src/main/java/io/novafoundation/nova/feature_swap_core/data/assetExchange/conversion/types/hydra/impl/xyk/model/XYKPool.kt rename to feature-swap-core/src/main/java/io/novafoundation/nova/feature_swap_core/data/assetExchange/conversion/types/hydra/sources/xyk/model/XYKPool.kt index 52fdf1bdb1..73f5866c8c 100644 --- a/feature-swap-core/src/main/java/io/novafoundation/nova/feature_swap_core/data/assetExchange/conversion/types/hydra/impl/xyk/model/XYKPool.kt +++ b/feature-swap-core/src/main/java/io/novafoundation/nova/feature_swap_core/data/assetExchange/conversion/types/hydra/sources/xyk/model/XYKPool.kt @@ -1,8 +1,8 @@ -package io.novafoundation.nova.feature_swap_core.data.assetExchange.conversion.types.hydra.impl.xyk.model +package io.novafoundation.nova.feature_swap_core.data.assetExchange.conversion.types.hydra.sources.xyk.model import io.novafoundation.nova.common.utils.atLeastZero -import io.novafoundation.nova.feature_swap_core.domain.model.SwapDirection -import io.novafoundation.nova.feature_swap_core.data.network.HydraDxAssetId +import io.novafoundation.nova.feature_swap_core_api.data.network.HydraDxAssetId +import io.novafoundation.nova.feature_swap_core_api.data.primitive.model.SwapDirection import io.novafoundation.nova.hydra_dx_math.HydraDxMathConversions.fromBridgeResultToBalance import io.novafoundation.nova.hydra_dx_math.xyk.HYKSwapMathBridge import io.novasama.substrate_sdk_android.runtime.AccountId diff --git a/feature-swap-core/src/main/java/io/novafoundation/nova/feature_swap_core/data/assetExchange/conversion/types/hydra/impl/xyk/model/XYKPoolInfo.kt b/feature-swap-core/src/main/java/io/novafoundation/nova/feature_swap_core/data/assetExchange/conversion/types/hydra/sources/xyk/model/XYKPoolInfo.kt similarity index 78% rename from feature-swap-core/src/main/java/io/novafoundation/nova/feature_swap_core/data/assetExchange/conversion/types/hydra/impl/xyk/model/XYKPoolInfo.kt rename to feature-swap-core/src/main/java/io/novafoundation/nova/feature_swap_core/data/assetExchange/conversion/types/hydra/sources/xyk/model/XYKPoolInfo.kt index 45dac0ea37..d4ba0b5d33 100644 --- a/feature-swap-core/src/main/java/io/novafoundation/nova/feature_swap_core/data/assetExchange/conversion/types/hydra/impl/xyk/model/XYKPoolInfo.kt +++ b/feature-swap-core/src/main/java/io/novafoundation/nova/feature_swap_core/data/assetExchange/conversion/types/hydra/sources/xyk/model/XYKPoolInfo.kt @@ -1,8 +1,8 @@ -package io.novafoundation.nova.feature_swap_core.data.assetExchange.conversion.types.hydra.impl.xyk.model +package io.novafoundation.nova.feature_swap_core.data.assetExchange.conversion.types.hydra.sources.xyk.model import io.novafoundation.nova.common.data.network.runtime.binding.bindNumber import io.novafoundation.nova.common.data.network.runtime.binding.castToList -import io.novafoundation.nova.feature_swap_core.data.network.HydraDxAssetId +import io.novafoundation.nova.feature_swap_core_api.data.network.HydraDxAssetId class XYKPoolInfo(val firstAsset: HydraDxAssetId, val secondAsset: HydraDxAssetId) diff --git a/feature-swap-core/src/main/java/io/novafoundation/nova/feature_swap_core/data/network/HydrationExtrinsicBuilderExt.kt b/feature-swap-core/src/main/java/io/novafoundation/nova/feature_swap_core/data/network/HydrationExtrinsicBuilderExt.kt deleted file mode 100644 index 40a34c88d6..0000000000 --- a/feature-swap-core/src/main/java/io/novafoundation/nova/feature_swap_core/data/network/HydrationExtrinsicBuilderExt.kt +++ /dev/null @@ -1,14 +0,0 @@ -package io.novafoundation.nova.feature_swap_core.data.network - -import io.novafoundation.nova.common.utils.Modules -import io.novasama.substrate_sdk_android.runtime.extrinsic.ExtrinsicBuilder - -fun ExtrinsicBuilder.setFeeCurrency(onChainId: HydraDxAssetId) { - call( - moduleName = Modules.MULTI_TRANSACTION_PAYMENT, - callName = "set_currency", - arguments = mapOf( - "currency" to onChainId - ) - ) -} diff --git a/feature-swap-core/src/main/java/io/novafoundation/nova/feature_swap_core/di/SwapCoreApi.kt b/feature-swap-core/src/main/java/io/novafoundation/nova/feature_swap_core/di/SwapCoreApi.kt deleted file mode 100644 index b8f4949884..0000000000 --- a/feature-swap-core/src/main/java/io/novafoundation/nova/feature_swap_core/di/SwapCoreApi.kt +++ /dev/null @@ -1,11 +0,0 @@ -package io.novafoundation.nova.feature_swap_core.di - -import io.novafoundation.nova.feature_swap_core.data.assetExchange.conversion.types.hydra.HydraDxAssetConversionFactory -import io.novafoundation.nova.feature_swap_core.data.network.HydraDxAssetIdConverter - -interface SwapCoreApi { - - val hydraDxAssetIdConverter: HydraDxAssetIdConverter - - val hydraDxAssetConversionFactory: HydraDxAssetConversionFactory -} diff --git a/feature-swap-core/src/main/java/io/novafoundation/nova/feature_swap_core/di/SwapCoreComponent.kt b/feature-swap-core/src/main/java/io/novafoundation/nova/feature_swap_core/di/SwapCoreComponent.kt index 19329583ef..f5415fd1d4 100644 --- a/feature-swap-core/src/main/java/io/novafoundation/nova/feature_swap_core/di/SwapCoreComponent.kt +++ b/feature-swap-core/src/main/java/io/novafoundation/nova/feature_swap_core/di/SwapCoreComponent.kt @@ -3,6 +3,7 @@ package io.novafoundation.nova.feature_swap_core.di import dagger.Component import io.novafoundation.nova.common.di.CommonApi import io.novafoundation.nova.common.di.scope.FeatureScope +import io.novafoundation.nova.feature_swap_core_api.di.SwapCoreApi import io.novafoundation.nova.runtime.di.RuntimeApi @Component( diff --git a/feature-swap-core/src/main/java/io/novafoundation/nova/feature_swap_core/di/SwapCoreDependencies.kt b/feature-swap-core/src/main/java/io/novafoundation/nova/feature_swap_core/di/SwapCoreDependencies.kt index 8e7f0f9df6..c6e65ed2b3 100644 --- a/feature-swap-core/src/main/java/io/novafoundation/nova/feature_swap_core/di/SwapCoreDependencies.kt +++ b/feature-swap-core/src/main/java/io/novafoundation/nova/feature_swap_core/di/SwapCoreDependencies.kt @@ -1,6 +1,7 @@ package io.novafoundation.nova.feature_swap_core.di import com.google.gson.Gson +import io.novafoundation.nova.common.data.memory.ComputationalCache import io.novafoundation.nova.runtime.di.REMOTE_STORAGE_SOURCE import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry import io.novafoundation.nova.runtime.repository.ChainStateRepository @@ -17,4 +18,7 @@ interface SwapCoreDependencies { @Named(REMOTE_STORAGE_SOURCE) fun remoteStorageSource(): StorageDataSource + + val computationalCache: ComputationalCache + } diff --git a/feature-swap-core/src/main/java/io/novafoundation/nova/feature_swap_core/di/SwapCoreModule.kt b/feature-swap-core/src/main/java/io/novafoundation/nova/feature_swap_core/di/SwapCoreModule.kt index 63f93c0e3d..fe96ce0f12 100644 --- a/feature-swap-core/src/main/java/io/novafoundation/nova/feature_swap_core/di/SwapCoreModule.kt +++ b/feature-swap-core/src/main/java/io/novafoundation/nova/feature_swap_core/di/SwapCoreModule.kt @@ -2,10 +2,13 @@ package io.novafoundation.nova.feature_swap_core.di import dagger.Module import dagger.Provides +import io.novafoundation.nova.common.data.memory.ComputationalCache import io.novafoundation.nova.common.di.scope.FeatureScope import io.novafoundation.nova.feature_swap_core.data.assetExchange.conversion.types.hydra.RealHydraDxAssetIdConverter -import io.novafoundation.nova.feature_swap_core.data.network.HydraDxAssetIdConverter import io.novafoundation.nova.feature_swap_core.di.conversions.HydraDxConversionModule +import io.novafoundation.nova.feature_swap_core.domain.paths.RealPathQuoterFactory +import io.novafoundation.nova.feature_swap_core_api.data.network.HydraDxAssetIdConverter +import io.novafoundation.nova.feature_swap_core_api.data.paths.PathQuoter import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry @Module(includes = [HydraDxConversionModule::class]) @@ -18,4 +21,12 @@ class SwapCoreModule { ): HydraDxAssetIdConverter { return RealHydraDxAssetIdConverter(chainRegistry) } + + @Provides + @FeatureScope + fun providePathsQuoterFactory( + computationalCache: ComputationalCache + ): PathQuoter.Factory { + return RealPathQuoterFactory(computationalCache) + } } diff --git a/feature-swap-core/src/main/java/io/novafoundation/nova/feature_swap_core/di/conversions/HydraDxConversionModule.kt b/feature-swap-core/src/main/java/io/novafoundation/nova/feature_swap_core/di/conversions/HydraDxConversionModule.kt index c879fee75b..cc8a11693d 100644 --- a/feature-swap-core/src/main/java/io/novafoundation/nova/feature_swap_core/di/conversions/HydraDxConversionModule.kt +++ b/feature-swap-core/src/main/java/io/novafoundation/nova/feature_swap_core/di/conversions/HydraDxConversionModule.kt @@ -5,13 +5,13 @@ import dagger.Module import dagger.Provides import dagger.multibindings.IntoSet import io.novafoundation.nova.common.di.scope.FeatureScope -import io.novafoundation.nova.feature_swap_core.data.assetExchange.conversion.types.hydra.HydraDxAssetConversionFactory -import io.novafoundation.nova.feature_swap_core.data.assetExchange.conversion.types.hydra.HydraDxConversionSource -import io.novafoundation.nova.feature_swap_core.data.assetExchange.conversion.types.hydra.impl.omnipool.OmniPoolConversionSourceFactory -import io.novafoundation.nova.feature_swap_core.data.assetExchange.conversion.types.hydra.impl.stableswap.StableConversionSourceFactory -import io.novafoundation.nova.feature_swap_core.data.assetExchange.conversion.types.hydra.impl.xyk.XYKConversionSourceFactory -import io.novafoundation.nova.feature_swap_core.data.network.HydraDxAssetIdConverter -import io.novafoundation.nova.feature_swap_core.data.assetExchange.conversion.types.hydra.RealHydraDxAssetConversionFactory +import io.novafoundation.nova.feature_swap_core.data.assetExchange.conversion.types.hydra.RealHydraDxQuotingFactory +import io.novafoundation.nova.feature_swap_core.data.assetExchange.conversion.types.hydra.sources.omnipool.OmniPoolQuotingSourceFactory +import io.novafoundation.nova.feature_swap_core.data.assetExchange.conversion.types.hydra.sources.stableswap.StableSwapQuotingSourceFactory +import io.novafoundation.nova.feature_swap_core.data.assetExchange.conversion.types.hydra.sources.xyk.XYKSwapQuotingSourceFactory +import io.novafoundation.nova.feature_swap_core_api.data.network.HydraDxAssetIdConverter +import io.novafoundation.nova.feature_swap_core_api.data.types.hydra.HydraDxQuoting +import io.novafoundation.nova.feature_swap_core_api.data.types.hydra.HydraDxQuotingSource import io.novafoundation.nova.runtime.di.REMOTE_STORAGE_SOURCE import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry import io.novafoundation.nova.runtime.repository.ChainStateRepository @@ -27,8 +27,8 @@ class HydraDxConversionModule { @Named(REMOTE_STORAGE_SOURCE) remoteStorageSource: StorageDataSource, chainRegistry: ChainRegistry, hydraDxAssetIdConverter: HydraDxAssetIdConverter, - ): HydraDxConversionSource.Factory { - return OmniPoolConversionSourceFactory( + ): HydraDxQuotingSource.Factory<*> { + return OmniPoolQuotingSourceFactory( remoteStorageSource = remoteStorageSource, chainRegistry = chainRegistry, hydraDxAssetIdConverter = hydraDxAssetIdConverter @@ -42,8 +42,8 @@ class HydraDxConversionModule { hydraDxAssetIdConverter: HydraDxAssetIdConverter, gson: Gson, chainStateRepository: ChainStateRepository - ): HydraDxConversionSource.Factory { - return StableConversionSourceFactory( + ): HydraDxQuotingSource.Factory<*> { + return StableSwapQuotingSourceFactory( remoteStorageSource = remoteStorageSource, hydraDxAssetIdConverter = hydraDxAssetIdConverter, gson = gson, @@ -56,8 +56,8 @@ class HydraDxConversionModule { fun provideXykSwapSourceFactory( @Named(REMOTE_STORAGE_SOURCE) remoteStorageSource: StorageDataSource, hydraDxAssetIdConverter: HydraDxAssetIdConverter - ): HydraDxConversionSource.Factory { - return XYKConversionSourceFactory( + ): HydraDxQuotingSource.Factory<*> { + return XYKSwapQuotingSourceFactory( remoteStorageSource, hydraDxAssetIdConverter ) @@ -67,10 +67,10 @@ class HydraDxConversionModule { @FeatureScope fun provideHydraDxAssetConversionFactory( @Named(REMOTE_STORAGE_SOURCE) remoteStorageSource: StorageDataSource, - conversionSourceFactories: Set<@JvmSuppressWildcards HydraDxConversionSource.Factory>, + conversionSourceFactories: Set<@JvmSuppressWildcards HydraDxQuotingSource.Factory<*>>, hydraDxAssetIdConverter: HydraDxAssetIdConverter, - ): HydraDxAssetConversionFactory { - return RealHydraDxAssetConversionFactory( + ): HydraDxQuoting.Factory { + return RealHydraDxQuotingFactory( remoteStorageSource = remoteStorageSource, conversionSourceFactories = conversionSourceFactories, hydraDxAssetIdConverter = hydraDxAssetIdConverter diff --git a/feature-swap-core/src/main/java/io/novafoundation/nova/feature_swap_core/domain/model/QuotePath.kt b/feature-swap-core/src/main/java/io/novafoundation/nova/feature_swap_core/domain/model/QuotePath.kt deleted file mode 100644 index 1d6e899b67..0000000000 --- a/feature-swap-core/src/main/java/io/novafoundation/nova/feature_swap_core/domain/model/QuotePath.kt +++ /dev/null @@ -1,8 +0,0 @@ -package io.novafoundation.nova.feature_swap_core.domain.model - -import io.novafoundation.nova.runtime.multiNetwork.chain.model.FullChainAssetId - -class QuotePath(val segments: List) { - - class Segment(val from: FullChainAssetId, val to: FullChainAssetId, val sourceId: String, val sourceParams: Map) -} diff --git a/feature-swap-core/src/main/java/io/novafoundation/nova/feature_swap_core/domain/paths/RealPathQuoter.kt b/feature-swap-core/src/main/java/io/novafoundation/nova/feature_swap_core/domain/paths/RealPathQuoter.kt new file mode 100644 index 0000000000..55f7725169 --- /dev/null +++ b/feature-swap-core/src/main/java/io/novafoundation/nova/feature_swap_core/domain/paths/RealPathQuoter.kt @@ -0,0 +1,116 @@ +package io.novafoundation.nova.feature_swap_core.domain.paths + +import io.novafoundation.nova.common.data.memory.ComputationalCache +import io.novafoundation.nova.common.utils.graph.Graph +import io.novafoundation.nova.common.utils.graph.Path +import io.novafoundation.nova.common.utils.graph.findDijkstraPathsBetween +import io.novafoundation.nova.feature_swap_core_api.data.paths.PathQuoter +import io.novafoundation.nova.feature_swap_core_api.data.paths.model.BestPathQuote +import io.novafoundation.nova.feature_swap_core_api.data.paths.model.QuotedEdge +import io.novafoundation.nova.feature_swap_core_api.data.paths.model.QuotedPath +import io.novafoundation.nova.feature_swap_core_api.data.primitive.errors.SwapQuoteException +import io.novafoundation.nova.feature_swap_core_api.data.primitive.model.QuotableEdge +import io.novafoundation.nova.feature_swap_core_api.data.primitive.model.SwapDirection +import io.novafoundation.nova.runtime.ext.fullId +import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain +import io.novafoundation.nova.runtime.multiNetwork.chain.model.FullChainAssetId +import kotlinx.coroutines.CoroutineScope +import java.math.BigInteger + +private const val PATHS_LIMIT = 4 +private const val QUOTES_CACHE = "RealSwapService.QuotesCache" + +class RealPathQuoterFactory( + private val computationalCache: ComputationalCache, +): PathQuoter.Factory { + + override fun create( + graph: Graph, + computationalScope: CoroutineScope + ): PathQuoter { + return RealPathQuoter(computationalCache, graph, computationalScope) + } +} + +private class RealPathQuoter( + private val computationalCache: ComputationalCache, + private val graph: Graph, + private val computationalScope: CoroutineScope +): PathQuoter { + + override suspend fun findBestPath( + chainAssetIn: Chain.Asset, + chainAssetOut: Chain.Asset, + amount: BigInteger, + swapDirection: SwapDirection + ): BestPathQuote { + val from = chainAssetIn.fullId + val to = chainAssetOut.fullId + + val paths = pathsFromCacheOrCompute(from, to, computationalScope) { + graph.findDijkstraPathsBetween(from, to, limit = PATHS_LIMIT) + } + + val quotedPaths = paths.mapNotNull { path -> quotePath(path, amount, swapDirection) } + if (paths.isEmpty()) { + throw SwapQuoteException.NotEnoughLiquidity + } + + return BestPathQuote(quotedPaths) + } + + private suspend fun pathsFromCacheOrCompute( + from: FullChainAssetId, + to: FullChainAssetId, + scope: CoroutineScope, + computation: suspend () -> List> + ): List> { + val mapKey = from to to + val cacheKey = "$QUOTES_CACHE:$mapKey" + + return computationalCache.useCache(cacheKey, scope) { + computation() + } + } + + private suspend fun quotePath( + path: Path, + amount: BigInteger, + swapDirection: SwapDirection + ): QuotedPath? { + val quote = when (swapDirection) { + SwapDirection.SPECIFIED_IN -> quotePathSell(path, amount) + SwapDirection.SPECIFIED_OUT -> quotePathBuy(path, amount) + } ?: return null + + return QuotedPath(swapDirection, quote) + } + + private suspend fun quotePathBuy(path: Path, amount: BigInteger): Path>? { + return runCatching { + val initial = mutableListOf>() to amount + + path.foldRight(initial) { segment, (quotedPath, currentAmount) -> + val segmentQuote = segment.quote(currentAmount, SwapDirection.SPECIFIED_OUT) + quotedPath.add(0, QuotedEdge(currentAmount, segmentQuote, segment)) + + quotedPath to segmentQuote + }.first + }.getOrNull() + } + + private suspend fun quotePathSell(path: Path, amount: BigInteger): Path>? { + return runCatching { + val initial = mutableListOf>() to amount + + path.fold(initial) { (quotedPath, currentAmount), segment -> + val segmentQuote = segment.quote(currentAmount, SwapDirection.SPECIFIED_IN) + quotedPath.add(QuotedEdge(currentAmount, segmentQuote, segment)) + + quotedPath to segmentQuote + }.first + }.getOrNull() + } +} + + diff --git a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/AssetExchange.kt b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/AssetExchange.kt index 947dba6dca..b510795ba8 100644 --- a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/AssetExchange.kt +++ b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/AssetExchange.kt @@ -1,16 +1,17 @@ package io.novafoundation.nova.feature_swap_impl.data.assetExchange +import io.novafoundation.nova.feature_account_api.data.fee.capability.CustomFeeCapability import io.novafoundation.nova.feature_account_api.domain.model.MetaAccount import io.novafoundation.nova.feature_swap_api.domain.model.ReQuoteTrigger import io.novafoundation.nova.feature_swap_api.domain.model.SlippageConfig -import io.novafoundation.nova.feature_swap_api.domain.model.SwapDirection import io.novafoundation.nova.feature_swap_api.domain.model.SwapGraphEdge +import io.novafoundation.nova.feature_swap_core_api.data.primitive.model.SwapDirection import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.types.Balance import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.Flow -interface AssetExchange { +interface AssetExchange : CustomFeeCapability { interface Factory { @@ -26,12 +27,7 @@ interface AssetExchange { suspend fun quote(quoteArgs: ParentQuoterArgs): Balance } - /** - * Implementations should expect `asset` to be non-utility asset, - * e.g. they don't need to additionally check whether asset is utility or not - * They can also expect this method is called only when asset is present in [AssetExchange.availableDirectSwapConnections] - */ - suspend fun canPayFeeInNonUtilityToken(asset: Chain.Asset): Boolean + suspend fun sync() suspend fun availableDirectSwapConnections(): List diff --git a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/assetConversion/AssetConversionExchange.kt b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/assetConversion/AssetConversionExchange.kt index e01057751a..e62a6eb70d 100644 --- a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/assetConversion/AssetConversionExchange.kt +++ b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/assetConversion/AssetConversionExchange.kt @@ -3,6 +3,8 @@ package io.novafoundation.nova.feature_swap_impl.data.assetExchange.assetConvers import io.novafoundation.nova.common.data.network.runtime.binding.bindNumberOrNull import io.novafoundation.nova.common.utils.Modules import io.novafoundation.nova.common.utils.assetConversion +import io.novafoundation.nova.feature_account_api.data.conversion.assethub.assetConversionOrNull +import io.novafoundation.nova.feature_account_api.data.conversion.assethub.pools 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_account_api.data.extrinsic.awaitInBlock @@ -14,11 +16,11 @@ import io.novafoundation.nova.feature_swap_api.domain.model.AtomicSwapOperationA import io.novafoundation.nova.feature_swap_api.domain.model.AtomicSwapOperationFee import io.novafoundation.nova.feature_swap_api.domain.model.ReQuoteTrigger import io.novafoundation.nova.feature_swap_api.domain.model.SlippageConfig -import io.novafoundation.nova.feature_swap_api.domain.model.SwapDirection import io.novafoundation.nova.feature_swap_api.domain.model.SwapExecutionCorrection import io.novafoundation.nova.feature_swap_api.domain.model.SwapGraphEdge import io.novafoundation.nova.feature_swap_api.domain.model.SwapLimit -import io.novafoundation.nova.feature_swap_api.domain.model.SwapQuoteException +import io.novafoundation.nova.feature_swap_core_api.data.primitive.errors.SwapQuoteException +import io.novafoundation.nova.feature_swap_core_api.data.primitive.model.SwapDirection import io.novafoundation.nova.feature_swap_impl.data.assetExchange.AssetExchange import io.novafoundation.nova.feature_swap_impl.domain.swap.BaseSwapGraphEdge import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.types.Balance @@ -61,7 +63,7 @@ class AssetConversionExchangeFactory( parentQuoter: AssetExchange.ParentQuoter, coroutineScope: CoroutineScope ): AssetExchange { - val converter = multiLocationConverterFactory.default(chain, coroutineScope) + val converter = multiLocationConverterFactory.defaultAsync(chain, coroutineScope) return AssetConversionExchange( chain = chain, @@ -83,7 +85,11 @@ private class AssetConversionExchange( private val chainStateRepository: ChainStateRepository, ) : AssetExchange { - override suspend fun canPayFeeInNonUtilityToken(asset: Chain.Asset): Boolean { + override suspend fun sync() { + // nothing to sync + } + + override suspend fun canPayFeeInNonUtilityToken(chainAsset: Chain.Asset): Boolean { // any asset is usable as a fee as soon as it has associated pool return true } @@ -275,7 +281,11 @@ private class AssetConversionExchange( val runtimeCallsApi = multiChainRuntimeCallsApi.forChain(chain.id) val toBuyNativeFee = runtimeCallsApi.quoteFeeConversion(nativeTokenFee.amount, customFeeAsset) - return SubstrateFee(toBuyNativeFee, nativeTokenFee.submissionOrigin) + return SubstrateFee( + amount = toBuyNativeFee, + submissionOrigin = nativeTokenFee.submissionOrigin, + asset = customFeeAsset + ) } private suspend fun RuntimeCallsApi.quoteFeeConversion(commissionAmountOut: Balance, customFeeToken: Chain.Asset): Balance { diff --git a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/hydraDx/HydraDxExchange.kt b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/hydraDx/HydraDxExchange.kt index 82ecfe36e8..499326208f 100644 --- a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/hydraDx/HydraDxExchange.kt +++ b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/hydraDx/HydraDxExchange.kt @@ -2,6 +2,7 @@ package io.novafoundation.nova.feature_swap_impl.data.assetExchange.hydraDx import io.novafoundation.nova.common.utils.Modules import io.novafoundation.nova.common.utils.flatMapAsync +import io.novafoundation.nova.common.utils.forEachAsync import io.novafoundation.nova.common.utils.mergeIfMultiple import io.novafoundation.nova.common.utils.singleReplaySharedFlow import io.novafoundation.nova.common.utils.structOf @@ -15,23 +16,27 @@ import io.novafoundation.nova.feature_account_api.domain.model.requireAccountIdI import io.novafoundation.nova.feature_swap_api.domain.model.AtomicSwapOperation import io.novafoundation.nova.feature_swap_api.domain.model.AtomicSwapOperationArgs import io.novafoundation.nova.feature_swap_api.domain.model.AtomicSwapOperationFee -import io.novafoundation.nova.feature_swap_api.domain.model.QuotableEdge import io.novafoundation.nova.feature_swap_api.domain.model.ReQuoteTrigger import io.novafoundation.nova.feature_swap_api.domain.model.SlippageConfig -import io.novafoundation.nova.feature_swap_api.domain.model.SwapDirection import io.novafoundation.nova.feature_swap_api.domain.model.SwapExecutionCorrection import io.novafoundation.nova.feature_swap_api.domain.model.SwapGraphEdge import io.novafoundation.nova.feature_swap_api.domain.model.SwapLimit +import io.novafoundation.nova.feature_swap_core.data.assetExchange.conversion.types.hydra.accountCurrencyMap +import io.novafoundation.nova.feature_swap_core.data.assetExchange.conversion.types.hydra.multiTransactionPayment +import io.novafoundation.nova.feature_swap_core_api.data.network.HydraDxAssetId +import io.novafoundation.nova.feature_swap_core_api.data.network.HydraDxAssetIdConverter +import io.novafoundation.nova.feature_swap_core_api.data.network.toOnChainIdOrThrow +import io.novafoundation.nova.feature_swap_core_api.data.primitive.model.QuotableEdge +import io.novafoundation.nova.feature_swap_core_api.data.primitive.model.SwapDirection +import io.novafoundation.nova.feature_swap_core_api.data.types.hydra.HydraDxQuoting +import io.novafoundation.nova.feature_swap_core_api.data.types.hydra.HydraDxQuotingSource import io.novafoundation.nova.feature_swap_impl.data.assetExchange.AssetExchange import io.novafoundation.nova.feature_swap_impl.data.assetExchange.ParentQuoterArgs +import io.novafoundation.nova.feature_swap_impl.data.assetExchange.hydraDx.referrals.HydraDxNovaReferral import io.novafoundation.nova.feature_swap_impl.data.assetExchange.hydraDx.referrals.linkedAccounts import io.novafoundation.nova.feature_swap_impl.data.assetExchange.hydraDx.referrals.referralsOrNull import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.AssetSourceRegistry -import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.HydraDxAssetId -import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.HydraDxAssetIdConverter import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.existentialDepositInPlanks -import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.isSystemAsset -import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.toOnChainIdOrThrow import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.types.Balance import io.novafoundation.nova.runtime.ethereum.StorageSharedRequestsBuilder import io.novafoundation.nova.runtime.ethereum.StorageSharedRequestsBuilderFactory @@ -60,7 +65,8 @@ class HydraDxExchangeFactory( private val extrinsicService: ExtrinsicService, private val hydraDxAssetIdConverter: HydraDxAssetIdConverter, private val hydraDxNovaReferral: HydraDxNovaReferral, - private val swapSourceFactories: Iterable, + private val swapSourceFactories: Iterable>, + private val quotingFactory: HydraDxQuoting.Factory, private val assetSourceRegistry: AssetSourceRegistry, ) : AssetExchange.Factory { @@ -74,19 +80,21 @@ class HydraDxExchangeFactory( hydraDxNovaReferral = hydraDxNovaReferral, swapSourceFactories = swapSourceFactories, assetSourceRegistry = assetSourceRegistry, - parentQuoter = parentQuoter + parentQuoter = parentQuoter, + delegate = quotingFactory.create(chain) ) } } private class HydraDxExchange( + private val delegate: HydraDxQuoting, private val remoteStorageSource: StorageDataSource, private val chain: Chain, private val storageSharedRequestsBuilderFactory: StorageSharedRequestsBuilderFactory, private val extrinsicService: ExtrinsicService, private val hydraDxAssetIdConverter: HydraDxAssetIdConverter, private val hydraDxNovaReferral: HydraDxNovaReferral, - private val swapSourceFactories: Iterable, + private val swapSourceFactories: Iterable>, private val assetSourceRegistry: AssetSourceRegistry, private val parentQuoter: AssetExchange.ParentQuoter, ) : AssetExchange { @@ -97,16 +105,12 @@ private class HydraDxExchange( private val userReferralState: MutableSharedFlow = singleReplaySharedFlow() - override suspend fun canPayFeeInNonUtilityToken(asset: Chain.Asset): Boolean { - val onChainId = hydraDxAssetIdConverter.toOnChainIdOrThrow(asset) - - if (hydraDxAssetIdConverter.isSystemAsset(onChainId)) return true - - val fallbackPrice = remoteStorageSource.query(chain.id) { - metadata.multiTransactionPayment.acceptedCurrencies.query(onChainId) - } + override suspend fun sync() { + return swapSources.forEachAsync { it.sync() } + } - return fallbackPrice != null + override suspend fun canPayFeeInNonUtilityToken(chainAsset: Chain.Asset): Boolean { + return delegate.canPayFeeInNonUtilityToken(chainAsset) } override suspend fun availableDirectSwapConnections(): List { @@ -180,8 +184,14 @@ private class HydraDxExchange( SET, NOT_SET, NOT_AVAILABLE } + @Suppress("UNCHECKED_CAST") private fun createSources(): List { - return swapSourceFactories.map { it.create(chain) } + return swapSourceFactories.map { + val sourceDelegate = delegate.getSource(it.identifier) + + // Cast should be safe as long as identifiers between delegates and wrappers match + (it as HydraDxSwapSource.Factory>).create(sourceDelegate) + } } private inner class HydraDxSwapEdge( @@ -224,7 +234,13 @@ private class HydraDxExchange( } override suspend fun estimateFee(): AtomicSwapOperationFee { - val nativeFee = extrinsicService.estimateFee(chain, TransactionOrigin.SelectedWallet, BatchMode.FORCE_BATCH) { + val nativeFee = extrinsicService.estimateFee( + chain = chain, + origin = TransactionOrigin.SelectedWallet, + submissionOptions = ExtrinsicService.SubmissionOptions( + batchMode = BatchMode.FORCE_BATCH + ) + ) { executeSwap() } @@ -236,12 +252,19 @@ private class HydraDxExchange( return SubstrateFee( amount = feeAmountInExpectedCurrency, - submissionOrigin = nativeFee.submissionOrigin + submissionOrigin = nativeFee.submissionOrigin, + asset = usedFeeAsset ) } override suspend fun submit(previousStepCorrection: SwapExecutionCorrection?): Result { - return extrinsicService.submitAndWatchExtrinsic(chain, TransactionOrigin.SelectedWallet, BatchMode.FORCE_BATCH) { + return extrinsicService.submitAndWatchExtrinsic( + chain = chain, + origin = TransactionOrigin.SelectedWallet, + submissionOptions = ExtrinsicService.SubmissionOptions( + batchMode = BatchMode.FORCE_BATCH + ) + ) { executeSwap() }.awaitInBlock().map { SwapExecutionCorrection() diff --git a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/hydraDx/HydraDxSwapSource.kt b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/hydraDx/HydraDxSwapSource.kt index 934ca1fb7c..42a0c01969 100644 --- a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/hydraDx/HydraDxSwapSource.kt +++ b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/hydraDx/HydraDxSwapSource.kt @@ -3,14 +3,13 @@ package io.novafoundation.nova.feature_swap_impl.data.assetExchange.hydraDx import io.novafoundation.nova.common.utils.Identifiable import io.novafoundation.nova.core.updater.SharedRequestsBuilder import io.novafoundation.nova.feature_swap_api.domain.model.AtomicSwapOperationArgs -import io.novafoundation.nova.feature_swap_api.domain.model.QuotableEdge -import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain +import io.novafoundation.nova.feature_swap_core_api.data.primitive.model.QuotableEdge +import io.novafoundation.nova.feature_swap_core_api.data.types.hydra.HydraDxQuotingSource import io.novasama.substrate_sdk_android.runtime.AccountId import io.novasama.substrate_sdk_android.runtime.definitions.types.composite.DictEnum import io.novasama.substrate_sdk_android.runtime.extrinsic.ExtrinsicBuilder import kotlinx.coroutines.flow.Flow -typealias HydraDxSwapSourceId = String typealias HydraDxStandaloneSwapBuilder = ExtrinsicBuilder.(args: AtomicSwapOperationArgs) -> Unit interface HydraDxSourceEdge : QuotableEdge { @@ -27,6 +26,8 @@ interface HydraDxSourceEdge : QuotableEdge { interface HydraDxSwapSource : Identifiable { + suspend fun sync() + suspend fun availableSwapDirections(): Collection suspend fun runSubscriptions( @@ -34,8 +35,8 @@ interface HydraDxSwapSource : Identifiable { subscriptionBuilder: SharedRequestsBuilder ): Flow - interface Factory { + interface Factory> : Identifiable { - fun create(chain: Chain): HydraDxSwapSource + fun create(delegate: D): HydraDxSwapSource } } diff --git a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/hydraDx/omnipool/OmniPoolSwapSource.kt b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/hydraDx/omnipool/OmniPoolSwapSource.kt index 1e829e2f31..cd1e33b155 100644 --- a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/hydraDx/omnipool/OmniPoolSwapSource.kt +++ b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/hydraDx/omnipool/OmniPoolSwapSource.kt @@ -1,252 +1,54 @@ package io.novafoundation.nova.feature_swap_impl.data.assetExchange.hydraDx.omnipool +import io.novafoundation.nova.common.utils.Identifiable import io.novafoundation.nova.common.utils.Modules -import io.novafoundation.nova.common.utils.dynamicFees -import io.novafoundation.nova.common.utils.numberConstant -import io.novafoundation.nova.common.utils.omnipool -import io.novafoundation.nova.common.utils.orZero -import io.novafoundation.nova.common.utils.padEnd -import io.novafoundation.nova.common.utils.singleReplaySharedFlow -import io.novafoundation.nova.common.utils.toMultiSubscription import io.novafoundation.nova.core.updater.SharedRequestsBuilder import io.novafoundation.nova.feature_swap_api.domain.model.AtomicSwapOperationArgs -import io.novafoundation.nova.feature_swap_api.domain.model.SwapDirection import io.novafoundation.nova.feature_swap_api.domain.model.SwapLimit -import io.novafoundation.nova.feature_swap_api.domain.model.SwapQuoteException +import io.novafoundation.nova.feature_swap_core.data.assetExchange.conversion.types.hydra.sources.omnipool.OmniPoolQuotingSource +import io.novafoundation.nova.feature_swap_core.data.assetExchange.conversion.types.hydra.sources.omnipool.OmniPoolQuotingSourceFactory +import io.novafoundation.nova.feature_swap_core_api.data.network.HydraDxAssetId +import io.novafoundation.nova.feature_swap_core_api.data.primitive.model.QuotableEdge import io.novafoundation.nova.feature_swap_impl.data.assetExchange.hydraDx.HydraDxSourceEdge import io.novafoundation.nova.feature_swap_impl.data.assetExchange.hydraDx.HydraDxStandaloneSwapBuilder import io.novafoundation.nova.feature_swap_impl.data.assetExchange.hydraDx.HydraDxSwapSource -import io.novafoundation.nova.feature_swap_impl.data.assetExchange.hydraDx.HydraDxSwapSourceId -import io.novafoundation.nova.feature_swap_impl.data.assetExchange.hydraDx.omnipool.model.DynamicFee -import io.novafoundation.nova.feature_swap_impl.data.assetExchange.hydraDx.omnipool.model.OmniPool -import io.novafoundation.nova.feature_swap_impl.data.assetExchange.hydraDx.omnipool.model.OmniPoolFees -import io.novafoundation.nova.feature_swap_impl.data.assetExchange.hydraDx.omnipool.model.OmniPoolToken -import io.novafoundation.nova.feature_swap_impl.data.assetExchange.hydraDx.omnipool.model.OmnipoolAssetState -import io.novafoundation.nova.feature_swap_impl.data.assetExchange.hydraDx.omnipool.model.feeParamsConstant -import io.novafoundation.nova.feature_swap_impl.data.assetExchange.hydraDx.omnipool.model.quote -import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.AssetSourceRegistry -import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.HydraDxAssetId -import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.HydraDxAssetIdConverter import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.types.Balance -import io.novafoundation.nova.runtime.ext.fullId -import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry -import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain -import io.novafoundation.nova.runtime.multiNetwork.chain.model.FullChainAssetId -import io.novafoundation.nova.runtime.multiNetwork.getRuntime -import io.novafoundation.nova.runtime.storage.source.StorageDataSource -import io.novafoundation.nova.runtime.storage.source.query.api.observeNonNull -import io.novafoundation.nova.runtime.storage.source.query.metadata import io.novasama.substrate_sdk_android.runtime.AccountId import io.novasama.substrate_sdk_android.runtime.definitions.types.composite.DictEnum import io.novasama.substrate_sdk_android.runtime.extrinsic.ExtrinsicBuilder -import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.MutableSharedFlow -import kotlinx.coroutines.flow.combine -import kotlinx.coroutines.flow.first -import kotlinx.coroutines.flow.map -import kotlinx.coroutines.flow.onEach -import java.math.BigInteger -class OmniPoolSwapSourceFactory( - private val remoteStorageSource: StorageDataSource, - private val chainRegistry: ChainRegistry, - private val assetSourceRegistry: AssetSourceRegistry, - private val hydraDxAssetIdConverter: HydraDxAssetIdConverter, -) : HydraDxSwapSource.Factory { +class OmniPoolSwapSourceFactory : HydraDxSwapSource.Factory { - companion object { + override val identifier: String = OmniPoolQuotingSourceFactory.SOURCE_ID - const val SOURCE_ID = "OmniPool" - } - - override fun create(chain: Chain): HydraDxSwapSource { - return OmniPoolSwapSource( - remoteStorageSource = remoteStorageSource, - chainRegistry = chainRegistry, - assetSourceRegistry = assetSourceRegistry, - hydraDxAssetIdConverter = hydraDxAssetIdConverter, - chain = chain - ) + override fun create(delegate: OmniPoolQuotingSource): HydraDxSwapSource { + return OmniPoolSwapSource(delegate) } } private class OmniPoolSwapSource( - private val remoteStorageSource: StorageDataSource, - private val chainRegistry: ChainRegistry, - private val assetSourceRegistry: AssetSourceRegistry, - private val hydraDxAssetIdConverter: HydraDxAssetIdConverter, - private val chain: Chain, -) : HydraDxSwapSource { - - override val identifier: HydraDxSwapSourceId = OmniPoolSwapSourceFactory.SOURCE_ID + private val delegate: OmniPoolQuotingSource, +) : HydraDxSwapSource, Identifiable by delegate { - private val pooledOnChainAssetIdsState: MutableSharedFlow> = singleReplaySharedFlow() - - private val omniPoolFlow: MutableSharedFlow = singleReplaySharedFlow() + override suspend fun sync() { + return delegate.sync() + } override suspend fun availableSwapDirections(): Collection { - val pooledOnChainAssetIds = getPooledOnChainAssetIds() - - val pooledChainAssetsIds = matchKnownChainAssetIds(pooledOnChainAssetIds) - pooledOnChainAssetIdsState.emit(pooledChainAssetsIds) - - return pooledChainAssetsIds.flatMap { remoteAndLocal -> - pooledChainAssetsIds.mapNotNull { otherRemoteAndLocal -> - // In OmniPool, each asset is tradable with any other except itself - if (remoteAndLocal.second.id != otherRemoteAndLocal.second.id) { - OmniPoolSwapEdge(fromAsset = remoteAndLocal, toAsset = otherRemoteAndLocal) - } else { - null - } - } - } + return delegate.availableSwapDirections().map(::OmniPoolSwapEdge) } - @OptIn(ExperimentalCoroutinesApi::class) override suspend fun runSubscriptions( userAccountId: AccountId, subscriptionBuilder: SharedRequestsBuilder ): Flow { - omniPoolFlow.resetReplayCache() - - val pooledAssets = pooledOnChainAssetIdsState.first() - - val omniPoolStateFlow = pooledAssets.map { (onChainId, _) -> - remoteStorageSource.subscribe(chain.id, subscriptionBuilder) { - metadata.omnipool.assets.observeNonNull(onChainId).map { - onChainId to it - } - } - } - .toMultiSubscription(pooledAssets.size) - - val poolAccountId = omniPoolAccountId() - - val omniPoolBalancesFlow = pooledAssets.map { (omniPoolTokenId, chainAsset) -> - val assetSource = assetSourceRegistry.sourceFor(chainAsset) - assetSource.balance.subscribeTransferableAccountBalance(chain, chainAsset, poolAccountId, subscriptionBuilder).map { - omniPoolTokenId to it - } - } - .toMultiSubscription(pooledAssets.size) - - val feesFlow = pooledAssets.map { (omniPoolTokenId, _) -> - remoteStorageSource.subscribe(chain.id, subscriptionBuilder) { - metadata.dynamicFeesApi.assetFee.observe(omniPoolTokenId).map { - omniPoolTokenId to it - } - } - }.toMultiSubscription(pooledAssets.size) - - val defaultFees = getDefaultFees() - - return combine(omniPoolStateFlow, omniPoolBalancesFlow, feesFlow) { poolState, poolBalances, fees -> - createOmniPool(poolState, poolBalances, fees, defaultFees) - } - .onEach(omniPoolFlow::emit) - .map { } - } - - private suspend fun getPooledOnChainAssetIds(): List { - return remoteStorageSource.query(chain.id) { - val hubAssetId = metadata.omnipool().numberConstant("HubAssetId", runtime) - val allAssets = runtime.metadata.omnipoolOrNull?.assets?.keys().orEmpty() - - // remove hubAssetId from trading paths - allAssets.filter { it != hubAssetId } - } - } - - private suspend fun matchKnownChainAssetIds(onChainIds: List): List { - val hydraDxAssetIds = hydraDxAssetIdConverter.allOnChainIds(chain) - - return onChainIds.mapNotNull { onChainId -> - val asset = hydraDxAssetIds[onChainId] ?: return@mapNotNull null - - onChainId to asset - } - } - - private fun createOmniPool( - poolAssetStates: Map, - poolBalances: Map, - fees: Map, - defaultFees: OmniPoolFees, - ): OmniPool { - val tokensState = poolAssetStates.mapValues { (tokenId, poolAssetState) -> - val assetBalance = poolBalances[tokenId].orZero() - val tokenFees = fees[tokenId]?.let { OmniPoolFees(it.protocolFee, it.assetFee) } ?: defaultFees - - OmniPoolToken( - hubReserve = poolAssetState.hubReserve, - shares = poolAssetState.shares, - protocolShares = poolAssetState.protocolShares, - tradeability = poolAssetState.tradeability, - balance = assetBalance, - fees = tokenFees - ) - } - - return OmniPool(tokensState) - } - - private fun ExtrinsicBuilder.sell( - assetIdIn: HydraDxAssetId, - assetIdOut: HydraDxAssetId, - amountIn: Balance, - minBuyAmount: Balance - ) { - call( - moduleName = Modules.OMNIPOOL, - callName = "sell", - arguments = mapOf( - "asset_in" to assetIdIn, - "asset_out" to assetIdOut, - "amount" to amountIn, - "min_buy_amount" to minBuyAmount - ) - ) - } - - private fun ExtrinsicBuilder.buy( - assetIdIn: HydraDxAssetId, - assetIdOut: HydraDxAssetId, - amountOut: Balance, - maxSellAmount: Balance - ) { - call( - moduleName = Modules.OMNIPOOL, - callName = "buy", - arguments = mapOf( - "asset_out" to assetIdOut, - "asset_in" to assetIdIn, - "amount" to amountOut, - "max_sell_amount" to maxSellAmount - ) - ) - } - - private suspend fun getDefaultFees(): OmniPoolFees { - val runtime = chainRegistry.getRuntime(chain.id) - - val assetFeeParams = runtime.metadata.dynamicFees().feeParamsConstant("AssetFeeParameters", runtime) - val protocolFeeParams = runtime.metadata.dynamicFees().feeParamsConstant("ProtocolFeeParameters", runtime) - - return OmniPoolFees( - protocolFee = protocolFeeParams.minFee, - assetFee = assetFeeParams.minFee - ) + return delegate.runSubscriptions(userAccountId, subscriptionBuilder) } private inner class OmniPoolSwapEdge( - private val fromAsset: RemoteIdAndLocalAsset, - private val toAsset: RemoteIdAndLocalAsset, - ) : HydraDxSourceEdge { - - override val from: FullChainAssetId = fromAsset.second.fullId - - override val to: FullChainAssetId = toAsset.second.fullId + private val delegate: OmniPoolQuotingSource.Edge + ) : HydraDxSourceEdge, QuotableEdge by delegate { override fun routerPoolArgument(): DictEnum.Entry<*> { return DictEnum.Entry("Omnipool", null) @@ -260,16 +62,9 @@ private class OmniPoolSwapSource( return "OmniPool" } - override suspend fun quote(amount: Balance, direction: SwapDirection): Balance { - val omniPool = omniPoolFlow.first() - - return omniPool.quote(fromAsset.first, toAsset.first, amount, direction) - ?: throw SwapQuoteException.NotEnoughLiquidity - } - private fun ExtrinsicBuilder.executeSwap(args: AtomicSwapOperationArgs) { - val assetIdIn = fromAsset.first - val assetIdOut = toAsset.first + val assetIdIn = delegate.fromAsset.first + val assetIdOut = delegate.toAsset.first when (val limit = args.swapLimit) { is SwapLimit.SpecifiedIn -> sell( @@ -278,6 +73,7 @@ private class OmniPoolSwapSource( amountIn = limit.amountIn, minBuyAmount = limit.amountOutMin ) + is SwapLimit.SpecifiedOut -> buy( assetIdIn = assetIdIn, assetIdOut = assetIdOut, @@ -286,25 +82,42 @@ private class OmniPoolSwapSource( ) } } - } -} - -fun omniPoolAccountId(): AccountId { - return "modlomnipool".encodeToByteArray().padEnd(expectedSize = 32, padding = 0) -} -typealias RemoteAndLocalId = Pair -typealias RemoteIdAndLocalAsset = Pair -typealias RemoteAndLocalIdOptional = Pair + private fun ExtrinsicBuilder.sell( + assetIdIn: HydraDxAssetId, + assetIdOut: HydraDxAssetId, + amountIn: Balance, + minBuyAmount: Balance + ) { + call( + moduleName = Modules.OMNIPOOL, + callName = "sell", + arguments = mapOf( + "asset_in" to assetIdIn, + "asset_out" to assetIdOut, + "amount" to amountIn, + "min_buy_amount" to minBuyAmount + ) + ) + } -@Suppress("UNCHECKED_CAST") -fun RemoteAndLocalIdOptional.flatten(): RemoteAndLocalId? { - return second?.let { this as RemoteAndLocalId } + private fun ExtrinsicBuilder.buy( + assetIdIn: HydraDxAssetId, + assetIdOut: HydraDxAssetId, + amountOut: Balance, + maxSellAmount: Balance + ) { + call( + moduleName = Modules.OMNIPOOL, + callName = "buy", + arguments = mapOf( + "asset_out" to assetIdOut, + "asset_in" to assetIdIn, + "amount" to amountOut, + "max_sell_amount" to maxSellAmount + ) + ) + } + } } -val RemoteAndLocalId.remoteId - get() = first - -val RemoteAndLocalId.localId - get() = second - diff --git a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/hydraDx/stableswap/StableSwapSource.kt b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/hydraDx/stableswap/StableSwapSource.kt index 4ef3e121dc..633b65a8c7 100644 --- a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/hydraDx/stableswap/StableSwapSource.kt +++ b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/hydraDx/stableswap/StableSwapSource.kt @@ -1,304 +1,68 @@ package io.novafoundation.nova.feature_swap_impl.data.assetExchange.hydraDx.stableswap -import com.google.gson.Gson -import io.novafoundation.nova.common.data.network.runtime.binding.BlockNumber -import io.novafoundation.nova.common.data.network.runtime.binding.orEmpty -import io.novafoundation.nova.common.utils.filterNotNull -import io.novafoundation.nova.common.utils.graph.Edge -import io.novafoundation.nova.common.utils.orZero -import io.novafoundation.nova.common.utils.singleReplaySharedFlow -import io.novafoundation.nova.common.utils.toMultiSubscription +import io.novafoundation.nova.common.utils.Identifiable import io.novafoundation.nova.core.updater.SharedRequestsBuilder -import io.novafoundation.nova.feature_swap_api.domain.model.SwapDirection -import io.novafoundation.nova.feature_swap_api.domain.model.SwapQuoteException +import io.novafoundation.nova.feature_swap_core.data.assetExchange.conversion.types.hydra.sources.stableswap.StableSwapQuotingSource +import io.novafoundation.nova.feature_swap_core.data.assetExchange.conversion.types.hydra.sources.stableswap.StableSwapQuotingSourceFactory +import io.novafoundation.nova.feature_swap_core_api.data.network.HydraDxAssetIdConverter +import io.novafoundation.nova.feature_swap_core_api.data.network.toChainAssetOrThrow +import io.novafoundation.nova.feature_swap_core_api.data.primitive.model.QuotableEdge import io.novafoundation.nova.feature_swap_impl.data.assetExchange.hydraDx.HydraDxSourceEdge import io.novafoundation.nova.feature_swap_impl.data.assetExchange.hydraDx.HydraDxStandaloneSwapBuilder import io.novafoundation.nova.feature_swap_impl.data.assetExchange.hydraDx.HydraDxSwapSource -import io.novafoundation.nova.feature_swap_impl.data.assetExchange.hydraDx.omnipool.RemoteAndLocalId -import io.novafoundation.nova.feature_swap_impl.data.assetExchange.hydraDx.omnipool.RemoteAndLocalIdOptional -import io.novafoundation.nova.feature_swap_impl.data.assetExchange.hydraDx.omnipool.flatten -import io.novafoundation.nova.feature_swap_impl.data.assetExchange.hydraDx.omnipool.omniPoolAccountId -import io.novafoundation.nova.feature_swap_impl.data.assetExchange.hydraDx.stableswap.model.StablePool -import io.novafoundation.nova.feature_swap_impl.data.assetExchange.hydraDx.stableswap.model.StablePoolAsset -import io.novafoundation.nova.feature_swap_impl.data.assetExchange.hydraDx.stableswap.model.StableSwapPoolInfo -import io.novafoundation.nova.feature_swap_impl.data.assetExchange.hydraDx.stableswap.model.quote -import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.HydraDxAssetId -import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.HydraDxAssetIdConverter -import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.toChainAssetOrThrow -import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.types.Balance -import io.novafoundation.nova.feature_wallet_api.domain.model.Asset -import io.novafoundation.nova.feature_wallet_api.domain.model.Asset.Companion.calculateTransferable -import io.novafoundation.nova.runtime.ext.fullId -import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain -import io.novafoundation.nova.runtime.multiNetwork.chain.model.FullChainAssetId -import io.novafoundation.nova.runtime.repository.ChainStateRepository -import io.novafoundation.nova.runtime.storage.source.StorageDataSource -import io.novafoundation.nova.runtime.storage.source.query.metadata -import io.novasama.substrate_sdk_android.encrypt.json.asLittleEndianBytes -import io.novasama.substrate_sdk_android.hash.Hasher.blake2b256 import io.novasama.substrate_sdk_android.runtime.AccountId import io.novasama.substrate_sdk_android.runtime.definitions.types.composite.DictEnum -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Deferred -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.async -import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.MutableSharedFlow -import kotlinx.coroutines.flow.combine -import kotlinx.coroutines.flow.first -import kotlinx.coroutines.flow.map -import kotlinx.coroutines.flow.onEach class StableSwapSourceFactory( - private val remoteStorageSource: StorageDataSource, private val hydraDxAssetIdConverter: HydraDxAssetIdConverter, - private val gson: Gson, - private val chainStateRepository: ChainStateRepository -) : HydraDxSwapSource.Factory { +) : HydraDxSwapSource.Factory { - companion object { - - const val ID = "StableSwap" - } - - override fun create(chain: Chain): HydraDxSwapSource { + override fun create(delegate: StableSwapQuotingSource): HydraDxSwapSource { return StableSwapSource( - remoteStorageSource = remoteStorageSource, - hydraDxAssetIdConverter = hydraDxAssetIdConverter, - chain = chain, - gson = gson, - chainStateRepository = chainStateRepository + delegate = delegate, + hydraDxAssetIdConverter = hydraDxAssetIdConverter ) } + + override val identifier: String = StableSwapQuotingSourceFactory.ID } private class StableSwapSource( - private val remoteStorageSource: StorageDataSource, + private val delegate: StableSwapQuotingSource, private val hydraDxAssetIdConverter: HydraDxAssetIdConverter, - private val chain: Chain, - private val gson: Gson, - private val chainStateRepository: ChainStateRepository, -) : HydraDxSwapSource { +) : HydraDxSwapSource, Identifiable by delegate { - override val identifier: String = StableSwapSourceFactory.ID + private val chain = delegate.chain - private val initialPoolsInfo: MutableSharedFlow> = singleReplaySharedFlow() - - private val stablePools: MutableSharedFlow> = singleReplaySharedFlow() + override suspend fun sync() { + return delegate.sync() + } override suspend fun availableSwapDirections(): Collection { - val pools = getPools() - - val poolInitialInfo = pools.matchIdsWithLocal() - initialPoolsInfo.emit(poolInitialInfo) - - return poolInitialInfo.allPossibleDirections() + return delegate.availableSwapDirections().map(::StableSwapEdge) } - @OptIn(ExperimentalCoroutinesApi::class) override suspend fun runSubscriptions( userAccountId: AccountId, subscriptionBuilder: SharedRequestsBuilder - ): Flow = coroutineScope { - stablePools.resetReplayCache() - - val initialPoolsInfo = initialPoolsInfo.first() - - val poolInfoSubscriptions = initialPoolsInfo.map { poolInfo -> - remoteStorageSource.subscribe(chain.id, subscriptionBuilder) { - runtime.metadata.stableSwap.pools.observe(poolInfo.sharedAsset.first).map { - poolInfo.sharedAsset.first to it - } - } - }.toMultiSubscription(initialPoolsInfo.size) - - val omniPoolAccountId = omniPoolAccountId() - - val poolSharedAssetBalanceSubscriptions = initialPoolsInfo.map { poolInfo -> - val sharedAssetRemoteId = poolInfo.sharedAsset.first - - subscribeTransferableBalance(subscriptionBuilder, omniPoolAccountId, sharedAssetRemoteId).map { - sharedAssetRemoteId to it - } - }.toMultiSubscription(initialPoolsInfo.size) - - val totalPooledAssets = initialPoolsInfo.sumOf { it.poolAssets.size } - - val poolParticipatingAssetsBalanceSubscription = initialPoolsInfo.flatMap { poolInfo -> - val poolAccountId = stableSwapPoolAccountId(poolInfo.sharedAsset.first) - - poolInfo.poolAssets.map { poolAsset -> - subscribeTransferableBalance(subscriptionBuilder, poolAccountId, poolAsset.first).map { - val key = poolInfo.sharedAsset.first to poolAsset.first - key to it - } - } - }.toMultiSubscription(totalPooledAssets) - - val totalIssuanceSubscriptions = initialPoolsInfo.map { poolInfo -> - remoteStorageSource.subscribe(chain.id, subscriptionBuilder) { - runtime.metadata.hydraTokens.totalIssuance.observe(poolInfo.sharedAsset.first).map { - poolInfo.sharedAsset.first to it.orZero() - } - } - }.toMultiSubscription(initialPoolsInfo.size) - - val precisions = fetchAssetsPrecisionsAsync() - - combine( - poolInfoSubscriptions, - poolSharedAssetBalanceSubscriptions, - poolParticipatingAssetsBalanceSubscription, - totalIssuanceSubscriptions, - chainStateRepository.currentBlockNumberFlow(chain.id), - ) { poolInfos, poolSharedAssetBalances, poolParticipatingAssetBalances, totalIssuances, currentBlock -> - createStableSwapPool(poolInfos, poolSharedAssetBalances, poolParticipatingAssetBalances, totalIssuances, currentBlock, precisions.await()) - } - .onEach(stablePools::emit) - .map { } - } - - private suspend fun subscribeTransferableBalance(subscriptionBuilder: SharedRequestsBuilder, account: AccountId, assetId: HydraDxAssetId): Flow { - // We cant use AssetSource since it require Chain.Asset which might not always be present in case some stable pool assets are not yet in Nova configs - return remoteStorageSource.subscribe(chain.id, subscriptionBuilder) { - metadata.hydraTokens.accounts.observe(account, assetId).map { - Asset.TransferableMode.REGULAR.calculateTransferable(it.orEmpty()) - } - } - } - - private fun createStableSwapPool( - poolInfos: Map, - poolSharedAssetBalances: Map, - poolParticipatingAssetBalances: Map, Balance>, - totalIssuances: Map, - currentBlock: BlockNumber, - precisions: Map - ): List { - return poolInfos.mapNotNull outer@{ (poolId, poolInfo) -> - if (poolInfo == null) return@outer null - - val sharedAssetBalance = poolSharedAssetBalances[poolId].orZero() - val sharedChainAssetPrecision = precisions[poolId] ?: return@outer null - val sharedAsset = StablePoolAsset(sharedAssetBalance, poolId, sharedChainAssetPrecision) - val sharedAssetIssuance = totalIssuances[poolId].orZero() - - val pooledAssets = poolInfo.assets.mapNotNull { pooledAssetId -> - val pooledAssetBalance = poolParticipatingAssetBalances[poolId to pooledAssetId].orZero() - val decimals = precisions[pooledAssetId] ?: return@mapNotNull null - - StablePoolAsset(pooledAssetBalance, pooledAssetId, decimals) - } - - StablePool( - sharedAsset = sharedAsset, - assets = pooledAssets, - initialAmplification = poolInfo.initialAmplification, - finalAmplification = poolInfo.finalAmplification, - initialBlock = poolInfo.initialBlock, - finalBlock = poolInfo.finalBlock, - fee = poolInfo.fee, - sharedAssetIssuance = sharedAssetIssuance, - gson = gson, - currentBlock = currentBlock - ) - } - } - - private fun CoroutineScope.fetchAssetsPrecisionsAsync(): Deferred> { - return async { - remoteStorageSource.query(chain.id) { - metadata.assetRegistry.assetMetadataMap.entries().filterNotNull() - } - } - } - - private fun stableSwapPoolAccountId(poolId: HydraDxAssetId): AccountId { - val prefix = "sts".encodeToByteArray() - val suffix = poolId.toInt().asLittleEndianBytes() - - return (prefix + suffix).blake2b256() - } - - - private suspend fun getPools(): Map { - return remoteStorageSource.query(chain.id) { - runtime.metadata.stableSwapOrNull?.pools?.entries().orEmpty() - } - } - - private suspend fun Map.matchIdsWithLocal(): List { - val allOnChainIds = hydraDxAssetIdConverter.allOnChainIds(chain) - - return mapNotNull outer@{ (poolAssetId, poolInfo) -> - val poolAssetMatchedId = allOnChainIds[poolAssetId]?.fullId - - val participatingAssetsMatchedIds = poolInfo.assets.map { assetId -> - val localId = allOnChainIds[assetId]?.fullId - - assetId to localId - } - - PoolInitialInfo( - sharedAsset = poolAssetId to poolAssetMatchedId, - poolAssets = participatingAssetsMatchedIds - ) - } - } - - private fun List.allPossibleDirections(): Collection { - return flatMap { (poolAssetId, poolAssets) -> - val allPoolAssetIds = buildList { - addAll(poolAssets.mapNotNull { it.flatten() }) - - val sharedAssetId = poolAssetId.flatten() - - if (sharedAssetId != null) { - add(sharedAssetId) - } - } - - allPoolAssetIds.flatMap { assetId -> - allPoolAssetIds.mapNotNull { otherAssetId -> - otherAssetId.takeIf { assetId != otherAssetId } - ?.let { StableSwapEdge(assetId, otherAssetId, poolAssetId.first) } - } - } - } + ): Flow { + return delegate.runSubscriptions(userAccountId, subscriptionBuilder) } inner class StableSwapEdge( - private val fromAsset: RemoteAndLocalId, - private val toAsset: RemoteAndLocalId, - private val poolId: HydraDxAssetId - ) : HydraDxSourceEdge, Edge { - - override val from: FullChainAssetId = fromAsset.second - - override val to: FullChainAssetId = toAsset.second + private val delegate: StableSwapQuotingSource.Edge + ) : HydraDxSourceEdge, QuotableEdge by delegate { override val standaloneSwapBuilder: HydraDxStandaloneSwapBuilder? = null + override suspend fun debugLabel(): String { - val poolAsset = hydraDxAssetIdConverter.toChainAssetOrThrow(chain, poolId) + val poolAsset = hydraDxAssetIdConverter.toChainAssetOrThrow(chain, delegate.poolId) return "StableSwap.${poolAsset.symbol}" } override fun routerPoolArgument(): DictEnum.Entry<*> { - return DictEnum.Entry("Stableswap", poolId) - } - - override suspend fun quote(amount: Balance, direction: SwapDirection): Balance { - val allPools = stablePools.first() - val relevantPool = allPools.first { it.sharedAsset.id == poolId } - - return relevantPool.quote(fromAsset.first, toAsset.first, amount, direction) - ?: throw SwapQuoteException.NotEnoughLiquidity + return DictEnum.Entry("Stableswap", delegate.poolId) } } - - private data class PoolInitialInfo( - val sharedAsset: RemoteAndLocalIdOptional, - val poolAssets: List - ) } diff --git a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/hydraDx/xyk/XYKApi.kt b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/hydraDx/xyk/XYKApi.kt deleted file mode 100644 index f6145529f8..0000000000 --- a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/hydraDx/xyk/XYKApi.kt +++ /dev/null @@ -1,34 +0,0 @@ -package io.novafoundation.nova.feature_swap_impl.data.assetExchange.hydraDx.xyk - -import io.novafoundation.nova.common.address.AccountIdKey -import io.novafoundation.nova.common.address.intoKey -import io.novafoundation.nova.common.data.network.runtime.binding.bindAccountId -import io.novafoundation.nova.common.utils.xyk -import io.novafoundation.nova.common.utils.xykOrNull -import io.novafoundation.nova.feature_swap_impl.data.assetExchange.hydraDx.xyk.model.XYKPoolInfo -import io.novafoundation.nova.feature_swap_impl.data.assetExchange.hydraDx.xyk.model.bindXYKPoolInfo -import io.novafoundation.nova.runtime.storage.source.query.StorageQueryContext -import io.novafoundation.nova.runtime.storage.source.query.api.QueryableModule -import io.novafoundation.nova.runtime.storage.source.query.api.QueryableStorageEntry1 -import io.novafoundation.nova.runtime.storage.source.query.api.storage1 -import io.novasama.substrate_sdk_android.runtime.metadata.RuntimeMetadata -import io.novasama.substrate_sdk_android.runtime.metadata.module.Module - -@JvmInline -value class XYKSwapApi(override val module: Module) : QueryableModule - -context(StorageQueryContext) -val RuntimeMetadata.xykOrNull: XYKSwapApi? - get() = xykOrNull()?.let(::XYKSwapApi) - -context(StorageQueryContext) -val RuntimeMetadata.xyk: XYKSwapApi - get() = XYKSwapApi(xyk()) - -context(StorageQueryContext) -val XYKSwapApi.poolAssets: QueryableStorageEntry1 - get() = storage1( - name = "PoolAssets", - keyBinding = { bindAccountId(it).intoKey() }, - binding = { decoded, _ -> bindXYKPoolInfo(decoded) }, - ) diff --git a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/hydraDx/xyk/XYKSwapSource.kt b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/hydraDx/xyk/XYKSwapSource.kt index 6173dcd1c3..839d9fdc85 100644 --- a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/hydraDx/xyk/XYKSwapSource.kt +++ b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/hydraDx/xyk/XYKSwapSource.kt @@ -1,197 +1,57 @@ package io.novafoundation.nova.feature_swap_impl.data.assetExchange.hydraDx.xyk -import io.novafoundation.nova.common.address.AccountIdKey -import io.novafoundation.nova.common.utils.combine -import io.novafoundation.nova.common.utils.singleReplaySharedFlow -import io.novafoundation.nova.common.utils.xyk +import io.novafoundation.nova.common.utils.Identifiable import io.novafoundation.nova.core.updater.SharedRequestsBuilder -import io.novafoundation.nova.feature_swap_api.domain.model.SwapDirection -import io.novafoundation.nova.feature_swap_api.domain.model.SwapQuoteException +import io.novafoundation.nova.feature_swap_core.data.assetExchange.conversion.types.hydra.sources.xyk.XYKSwapQuotingSource +import io.novafoundation.nova.feature_swap_core.data.assetExchange.conversion.types.hydra.sources.xyk.XYKSwapQuotingSourceFactory +import io.novafoundation.nova.feature_swap_core_api.data.primitive.model.QuotableEdge import io.novafoundation.nova.feature_swap_impl.data.assetExchange.hydraDx.HydraDxSourceEdge import io.novafoundation.nova.feature_swap_impl.data.assetExchange.hydraDx.HydraDxStandaloneSwapBuilder import io.novafoundation.nova.feature_swap_impl.data.assetExchange.hydraDx.HydraDxSwapSource -import io.novafoundation.nova.feature_swap_impl.data.assetExchange.hydraDx.omnipool.RemoteAndLocalId -import io.novafoundation.nova.feature_swap_impl.data.assetExchange.hydraDx.omnipool.localId -import io.novafoundation.nova.feature_swap_impl.data.assetExchange.hydraDx.xyk.model.XYKPool -import io.novafoundation.nova.feature_swap_impl.data.assetExchange.hydraDx.xyk.model.XYKPoolAsset -import io.novafoundation.nova.feature_swap_impl.data.assetExchange.hydraDx.xyk.model.XYKPoolInfo -import io.novafoundation.nova.feature_swap_impl.data.assetExchange.hydraDx.xyk.model.XYKPools -import io.novafoundation.nova.feature_swap_impl.data.assetExchange.hydraDx.xyk.model.poolFeesConstant -import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.AssetSourceRegistry -import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.HydraDxAssetId -import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.HydraDxAssetIdConverter -import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.types.Balance -import io.novafoundation.nova.runtime.ext.fullId -import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain -import io.novafoundation.nova.runtime.multiNetwork.chain.model.FullChainAssetId -import io.novafoundation.nova.runtime.storage.source.StorageDataSource import io.novasama.substrate_sdk_android.runtime.AccountId import io.novasama.substrate_sdk_android.runtime.definitions.types.composite.DictEnum -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.MutableSharedFlow -import kotlinx.coroutines.flow.combine -import kotlinx.coroutines.flow.first -import kotlinx.coroutines.flow.map -class XYKSwapSourceFactory( - private val remoteStorageSource: StorageDataSource, - private val hydraDxAssetIdConverter: HydraDxAssetIdConverter, - private val assetSourceRegistry: AssetSourceRegistry, -) : HydraDxSwapSource.Factory { +class XYKSwapSourceFactory: HydraDxSwapSource.Factory { - override fun create(chain: Chain): HydraDxSwapSource { - return XYKSwapSource( - remoteStorageSource = remoteStorageSource, - hydraDxAssetIdConverter = hydraDxAssetIdConverter, - chain = chain, - assetSourceRegistry = assetSourceRegistry - ) + override val identifier: String = XYKSwapQuotingSourceFactory.ID + + override fun create(delegate: XYKSwapQuotingSource): HydraDxSwapSource { + return XYKSwapSource(delegate) } } private class XYKSwapSource( - private val remoteStorageSource: StorageDataSource, - private val hydraDxAssetIdConverter: HydraDxAssetIdConverter, - private val chain: Chain, - private val assetSourceRegistry: AssetSourceRegistry, -) : HydraDxSwapSource { - - override val identifier: String = "Xyk" - - private val initialPoolsInfo: MutableSharedFlow> = singleReplaySharedFlow() - - private val xykPools: MutableSharedFlow = singleReplaySharedFlow() - - override suspend fun availableSwapDirections(): Collection { - val pools = getPools() + private val delegate: XYKSwapQuotingSource, +) : HydraDxSwapSource, Identifiable by delegate { - val poolInitialInfo = pools.matchIdsWithLocal() - initialPoolsInfo.emit(poolInitialInfo) - - return poolInitialInfo.allPossibleDirections() + override suspend fun sync() { + return delegate.sync() } - private suspend fun subscribeToBalance( - assetId: RemoteAndLocalId, - poolAddress: AccountId, - subscriptionBuilder: SharedRequestsBuilder - ): Flow { - val chainAsset = chain.assetsById.getValue(assetId.localId.assetId) - val assetSource = assetSourceRegistry.sourceFor(chainAsset) - - return assetSource.balance.subscribeTransferableAccountBalance(chain, chainAsset, poolAddress, subscriptionBuilder) + override suspend fun availableSwapDirections(): Collection { + return delegate.availableSwapDirections().map(::XYKSwapEdge) } - @OptIn(ExperimentalCoroutinesApi::class) override suspend fun runSubscriptions( userAccountId: AccountId, subscriptionBuilder: SharedRequestsBuilder - ): Flow = coroutineScope { - xykPools.resetReplayCache() - - val initialPoolsInfo = initialPoolsInfo.first() - - val poolsSubscription = initialPoolsInfo.map { poolInfo -> - val firstBalanceFlow = subscribeToBalance(poolInfo.firstAsset, poolInfo.poolAddress, subscriptionBuilder) - val secondBalanceFlow = subscribeToBalance(poolInfo.secondAsset, poolInfo.poolAddress, subscriptionBuilder) - - firstBalanceFlow.combine(secondBalanceFlow) { firstBalance, secondBalance -> - XYKPool( - address = poolInfo.poolAddress, - firstAsset = XYKPoolAsset(firstBalance, poolInfo.firstAsset.first), - secondAsset = XYKPoolAsset(secondBalance, poolInfo.secondAsset.first), - ) - } - }.combine() - - val fees = remoteStorageSource.query(chain.id) { - runtime.metadata.xyk().poolFeesConstant(runtime) - } - - poolsSubscription.map { pools -> - val built = XYKPools(fees, pools) - xykPools.emit(built) - } + ): Flow { + return delegate.runSubscriptions(userAccountId, subscriptionBuilder) } - private suspend fun getPools(): Map { - return remoteStorageSource.query(chain.id) { - runtime.metadata.xykOrNull?.poolAssets?.entries().orEmpty() - } - } - - private suspend fun Map.matchIdsWithLocal(): List { - val allOnChainIds = hydraDxAssetIdConverter.allOnChainIds(chain) - - fun matchId(remoteId: HydraDxAssetId): RemoteAndLocalId? { - return allOnChainIds[remoteId]?.fullId?.let { - remoteId to it - } - } - - return mapNotNull outer@{ (poolAddress, poolInfo) -> - PoolInitialInfo( - poolAddress = poolAddress.value, - firstAsset = matchId(poolInfo.firstAsset) ?: return@outer null, - secondAsset = matchId(poolInfo.secondAsset) ?: return@outer null, - ) - } - } - - private fun List.allPossibleDirections(): Collection { - return buildList { - this@allPossibleDirections.forEach { poolInfo -> - add( - XYKSwapDirection( - fromAsset = poolInfo.firstAsset, - toAsset = poolInfo.secondAsset, - poolAddress = poolInfo.poolAddress - ) - ) - - add( - XYKSwapDirection( - fromAsset = poolInfo.secondAsset, - toAsset = poolInfo.firstAsset, - poolAddress = poolInfo.poolAddress - ) - ) - } - } - } - - inner class XYKSwapDirection( - private val fromAsset: RemoteAndLocalId, - private val toAsset: RemoteAndLocalId, - private val poolAddress: AccountId - ) : HydraDxSourceEdge { - - override val from: FullChainAssetId = fromAsset.second - - override val to: FullChainAssetId = toAsset.second - - override val standaloneSwapBuilder: HydraDxStandaloneSwapBuilder? = null - override suspend fun debugLabel(): String { - return "XYK" - } + inner class XYKSwapEdge( + private val delegate: XYKSwapQuotingSource.Edge + ) : HydraDxSourceEdge, QuotableEdge by delegate { override fun routerPoolArgument(): DictEnum.Entry<*> { return DictEnum.Entry("XYK", null) } - override suspend fun quote(amount: Balance, direction: SwapDirection): Balance { - val allPools = xykPools.first() + override val standaloneSwapBuilder: HydraDxStandaloneSwapBuilder? = null - return allPools.quote(poolAddress, fromAsset.first, toAsset.first, amount, direction) - ?: throw SwapQuoteException.NotEnoughLiquidity + override suspend fun debugLabel(): String { + return "XYK" } } } - -private class PoolInitialInfo( - val poolAddress: AccountId, - val firstAsset: RemoteAndLocalId, - val secondAsset: RemoteAndLocalId -) diff --git a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/hydraDx/xyk/model/XYKFees.kt b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/hydraDx/xyk/model/XYKFees.kt deleted file mode 100644 index a778800482..0000000000 --- a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/hydraDx/xyk/model/XYKFees.kt +++ /dev/null @@ -1,20 +0,0 @@ -package io.novafoundation.nova.feature_swap_impl.data.assetExchange.hydraDx.xyk.model - -import io.novafoundation.nova.common.data.network.runtime.binding.bindInt -import io.novafoundation.nova.common.data.network.runtime.binding.castToList -import io.novafoundation.nova.common.utils.constant -import io.novafoundation.nova.common.utils.decoded -import io.novasama.substrate_sdk_android.runtime.RuntimeSnapshot -import io.novasama.substrate_sdk_android.runtime.metadata.module.Module - -class XYKFees(val nominator: Int, val denominator: Int) - -fun bindXYKFees(decoded: Any?): XYKFees { - val (first, second) = decoded.castToList() - - return XYKFees(bindInt(first), bindInt(second)) -} - -fun Module.poolFeesConstant(runtimeSnapshot: RuntimeSnapshot): XYKFees { - return bindXYKFees(constant("GetExchangeFee").decoded(runtimeSnapshot)) -} diff --git a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/hydraDx/xyk/model/XYKPool.kt b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/hydraDx/xyk/model/XYKPool.kt deleted file mode 100644 index d688339a35..0000000000 --- a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/hydraDx/xyk/model/XYKPool.kt +++ /dev/null @@ -1,106 +0,0 @@ -package io.novafoundation.nova.feature_swap_impl.data.assetExchange.hydraDx.xyk.model - -import io.novafoundation.nova.common.utils.atLeastZero -import io.novafoundation.nova.feature_swap_api.domain.model.SwapDirection -import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.HydraDxAssetId -import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.types.Balance -import io.novafoundation.nova.hydra_dx_math.HydraDxMathConversions.fromBridgeResultToBalance -import io.novafoundation.nova.hydra_dx_math.xyk.HYKSwapMathBridge -import io.novasama.substrate_sdk_android.runtime.AccountId -import java.math.BigInteger - -class XYKPools( - val fees: XYKFees, - val pools: List -) { - - fun quote( - poolAddress: AccountId, - assetIdIn: HydraDxAssetId, - assetIdOut: HydraDxAssetId, - amount: Balance, - direction: SwapDirection - ): Balance? { - val relevantPool = pools.first { it.address.contentEquals(poolAddress) } - - return relevantPool.quote(assetIdIn, assetIdOut, amount, direction, fees) - } -} - -class XYKPool( - val address: AccountId, - val firstAsset: XYKPoolAsset, - val secondAsset: XYKPoolAsset, -) { - - fun getAsset(assetId: HydraDxAssetId): XYKPoolAsset { - return when { - firstAsset.id == assetId -> firstAsset - secondAsset.id == assetId -> secondAsset - else -> error("Unknown asset for the pool") - } - } -} - -class XYKPoolAsset( - val balance: Balance, - val id: HydraDxAssetId, -) - -fun XYKPool.quote( - assetIdIn: HydraDxAssetId, - assetIdOut: HydraDxAssetId, - amount: Balance, - direction: SwapDirection, - fees: XYKFees -): Balance? { - return when (direction) { - SwapDirection.SPECIFIED_IN -> calculateOutGivenIn(assetIdIn, assetIdOut, amount, fees) - SwapDirection.SPECIFIED_OUT -> calculateInGivenOut(assetIdIn, assetIdOut, amount, fees) - } -} - -private fun XYKPool.calculateOutGivenIn( - assetIdIn: HydraDxAssetId, - assetIdOut: HydraDxAssetId, - amountIn: Balance, - feesConfig: XYKFees -): Balance? { - val assetIn = getAsset(assetIdIn) - val assetOut = getAsset(assetIdOut) - - val amountOut = HYKSwapMathBridge.calculate_out_given_in( - assetIn.balance.toString(), - assetOut.balance.toString(), - amountIn.toString() - ).fromBridgeResultToBalance() ?: return null - - val fees = feesConfig.feeFrom(amountOut) ?: return null - - return (amountOut - fees).atLeastZero() -} - -private fun XYKPool.calculateInGivenOut( - assetIdIn: HydraDxAssetId, - assetIdOut: HydraDxAssetId, - amountOut: Balance, - feesConfig: XYKFees, -): Balance? { - val assetIn = getAsset(assetIdIn) - val assetOut = getAsset(assetIdOut) - - val amountIn = HYKSwapMathBridge.calculate_in_given_out( - assetIn.balance.toString(), - assetOut.balance.toString(), - amountOut.toString() - ).fromBridgeResultToBalance() ?: return null - - val fees = feesConfig.feeFrom(amountIn) ?: return null - - return amountIn + fees -} - -private fun XYKFees.feeFrom(amount: BigInteger): Balance? { - return HYKSwapMathBridge.calculate_pool_trade_fee(amount.toString(), nominator.toString(), denominator.toString()) - .fromBridgeResultToBalance() -} diff --git a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/hydraDx/xyk/model/XYKPoolInfo.kt b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/hydraDx/xyk/model/XYKPoolInfo.kt deleted file mode 100644 index bc0c9cea6d..0000000000 --- a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/hydraDx/xyk/model/XYKPoolInfo.kt +++ /dev/null @@ -1,13 +0,0 @@ -package io.novafoundation.nova.feature_swap_impl.data.assetExchange.hydraDx.xyk.model - -import io.novafoundation.nova.common.data.network.runtime.binding.bindNumber -import io.novafoundation.nova.common.data.network.runtime.binding.castToList -import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.HydraDxAssetId - -class XYKPoolInfo(val firstAsset: HydraDxAssetId, val secondAsset: HydraDxAssetId) - -fun bindXYKPoolInfo(decoded: Any): XYKPoolInfo { - val (first, second) = decoded.castToList() - - return XYKPoolInfo(bindNumber(first), bindNumber(second)) -} diff --git a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/di/SwapFeatureComponent.kt b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/di/SwapFeatureComponent.kt index 02bb360cbb..2bb8733c5f 100644 --- a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/di/SwapFeatureComponent.kt +++ b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/di/SwapFeatureComponent.kt @@ -8,7 +8,7 @@ import io.novafoundation.nova.core_db.di.DbApi import io.novafoundation.nova.feature_account_api.di.AccountFeatureApi import io.novafoundation.nova.feature_buy_api.di.BuyFeatureApi import io.novafoundation.nova.feature_swap_api.di.SwapFeatureApi -import io.novafoundation.nova.feature_swap_core.di.SwapCoreApi +import io.novafoundation.nova.feature_swap_core_api.di.SwapCoreApi import io.novafoundation.nova.feature_swap_impl.presentation.SwapRouter import io.novafoundation.nova.feature_swap_impl.presentation.confirmation.di.SwapConfirmationComponent import io.novafoundation.nova.feature_swap_impl.presentation.main.di.SwapMainSettingsComponent diff --git a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/di/SwapFeatureDependencies.kt b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/di/SwapFeatureDependencies.kt index a16bd0c308..61d59a9cac 100644 --- a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/di/SwapFeatureDependencies.kt +++ b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/di/SwapFeatureDependencies.kt @@ -15,6 +15,7 @@ import io.novafoundation.nova.common.view.input.chooser.ListChooserMixin import io.novafoundation.nova.core.storage.StorageCache import io.novafoundation.nova.core_db.dao.OperationDao import io.novafoundation.nova.feature_account_api.data.extrinsic.ExtrinsicService +import io.novafoundation.nova.feature_account_api.data.fee.capability.CustomFeeCapabilityFacade 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_account_api.domain.interfaces.SelectedAccountUseCase @@ -24,10 +25,10 @@ import io.novafoundation.nova.feature_account_api.presenatation.mixin.identity.I import io.novafoundation.nova.feature_buy_api.domain.BuyTokenRegistry import io.novafoundation.nova.feature_buy_api.presentation.mixin.BuyMixin import io.novafoundation.nova.feature_buy_api.presentation.mixin.BuyMixinUi -import io.novafoundation.nova.feature_account_api.data.fee.capability.CustomFeeCapabilityFacade -import io.novafoundation.nova.feature_swap_core.data.assetExchange.conversion.types.hydra.HydraDxAssetConversionFactory +import io.novafoundation.nova.feature_swap_core_api.data.network.HydraDxAssetIdConverter +import io.novafoundation.nova.feature_swap_core_api.data.paths.PathQuoter +import io.novafoundation.nova.feature_swap_core_api.data.types.hydra.HydraDxQuoting import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.AssetSourceRegistry -import io.novafoundation.nova.feature_swap_core.data.network.HydraDxAssetIdConverter import io.novafoundation.nova.feature_wallet_api.data.network.crosschain.CrossChainTransfersRepository import io.novafoundation.nova.feature_wallet_api.domain.ArbitraryAssetUseCase import io.novafoundation.nova.feature_wallet_api.domain.interfaces.CrossChainTransfersUseCase @@ -99,7 +100,7 @@ interface SwapFeatureDependencies { val hydraDxAssetIdConverter: HydraDxAssetIdConverter - val hydraDxAssetConversionFactory: HydraDxAssetConversionFactory + val hydraDxQuotingFactory: HydraDxQuoting.Factory val runtimeCallsApi: MultiChainRuntimeCallsApi @@ -128,4 +129,6 @@ interface SwapFeatureDependencies { val gson: Gson val customFeeCapabilityFacade: CustomFeeCapabilityFacade + + val quoterFactory: PathQuoter.Factory } diff --git a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/di/SwapFeatureHolder.kt b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/di/SwapFeatureHolder.kt index 6a03a805f3..a8a425c488 100644 --- a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/di/SwapFeatureHolder.kt +++ b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/di/SwapFeatureHolder.kt @@ -6,7 +6,7 @@ import io.novafoundation.nova.common.di.scope.ApplicationScope import io.novafoundation.nova.core_db.di.DbApi import io.novafoundation.nova.feature_account_api.di.AccountFeatureApi import io.novafoundation.nova.feature_buy_api.di.BuyFeatureApi -import io.novafoundation.nova.feature_swap_core.di.SwapCoreApi +import io.novafoundation.nova.feature_swap_core_api.di.SwapCoreApi import io.novafoundation.nova.feature_swap_impl.presentation.SwapRouter import io.novafoundation.nova.feature_wallet_api.di.WalletFeatureApi import io.novafoundation.nova.runtime.di.RuntimeApi diff --git a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/di/SwapFeatureModule.kt b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/di/SwapFeatureModule.kt index 85b72472b4..2fa1a3606e 100644 --- a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/di/SwapFeatureModule.kt +++ b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/di/SwapFeatureModule.kt @@ -7,13 +7,14 @@ import io.novafoundation.nova.common.di.scope.FeatureScope import io.novafoundation.nova.common.resources.ResourceManager import io.novafoundation.nova.core.storage.StorageCache import io.novafoundation.nova.core_db.dao.OperationDao +import io.novafoundation.nova.feature_account_api.data.fee.capability.CustomFeeCapabilityFacade import io.novafoundation.nova.feature_account_api.domain.interfaces.AccountRepository import io.novafoundation.nova.feature_buy_api.domain.BuyTokenRegistry import io.novafoundation.nova.feature_swap_api.domain.interactor.SwapAvailabilityInteractor import io.novafoundation.nova.feature_swap_api.domain.swap.SwapService import io.novafoundation.nova.feature_swap_api.presentation.formatters.SwapRateFormatter import io.novafoundation.nova.feature_swap_api.presentation.state.SwapSettingsStateProvider -import io.novafoundation.nova.feature_account_api.data.fee.capability.CustomFeeCapabilityFacade +import io.novafoundation.nova.feature_swap_core_api.data.paths.PathQuoter import io.novafoundation.nova.feature_swap_impl.data.assetExchange.assetConversion.AssetConversionExchangeFactory import io.novafoundation.nova.feature_swap_impl.data.assetExchange.hydraDx.HydraDxExchangeFactory import io.novafoundation.nova.feature_swap_impl.data.network.blockhain.updaters.SwapUpdateSystemFactory @@ -45,17 +46,19 @@ class SwapFeatureModule { @FeatureScope @Provides fun provideSwapService( - assetConversionExchangeFactory: AssetConversionExchangeFactory, + assetConversionFactory: AssetConversionExchangeFactory, hydraDxExchangeFactory: HydraDxExchangeFactory, computationalCache: ComputationalCache, chainRegistry: ChainRegistry, + quoterFactory: PathQuoter.Factory, customFeeCapabilityFacade: CustomFeeCapabilityFacade ): SwapService { return RealSwapService( - assetConversionFactory = assetConversionExchangeFactory, + assetConversionFactory = assetConversionFactory, hydraDxExchangeFactory = hydraDxExchangeFactory, computationalCache = computationalCache, chainRegistry = chainRegistry, + quoterFactory = quoterFactory, customFeeCapabilityFacade = customFeeCapabilityFacade ) } diff --git a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/di/exchanges/HydraDxExchangeModule.kt b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/di/exchanges/HydraDxExchangeModule.kt index b27fb207b4..563b878994 100644 --- a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/di/exchanges/HydraDxExchangeModule.kt +++ b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/di/exchanges/HydraDxExchangeModule.kt @@ -1,24 +1,22 @@ package io.novafoundation.nova.feature_swap_impl.di.exchanges -import com.google.gson.Gson import dagger.Module import dagger.Provides import dagger.multibindings.IntoSet import io.novafoundation.nova.common.di.scope.FeatureScope import io.novafoundation.nova.feature_account_api.data.extrinsic.ExtrinsicService +import io.novafoundation.nova.feature_swap_core_api.data.network.HydraDxAssetIdConverter +import io.novafoundation.nova.feature_swap_core_api.data.types.hydra.HydraDxQuoting import io.novafoundation.nova.feature_swap_impl.data.assetExchange.hydraDx.HydraDxExchangeFactory -import io.novafoundation.nova.feature_swap_impl.data.assetExchange.hydraDx.HydraDxNovaReferral import io.novafoundation.nova.feature_swap_impl.data.assetExchange.hydraDx.HydraDxSwapSource -import io.novafoundation.nova.feature_swap_impl.data.assetExchange.hydraDx.RealHydraDxNovaReferral import io.novafoundation.nova.feature_swap_impl.data.assetExchange.hydraDx.omnipool.OmniPoolSwapSourceFactory +import io.novafoundation.nova.feature_swap_impl.data.assetExchange.hydraDx.referrals.HydraDxNovaReferral +import io.novafoundation.nova.feature_swap_impl.data.assetExchange.hydraDx.referrals.RealHydraDxNovaReferral import io.novafoundation.nova.feature_swap_impl.data.assetExchange.hydraDx.stableswap.StableSwapSourceFactory import io.novafoundation.nova.feature_swap_impl.data.assetExchange.hydraDx.xyk.XYKSwapSourceFactory import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.AssetSourceRegistry -import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.HydraDxAssetIdConverter import io.novafoundation.nova.runtime.di.REMOTE_STORAGE_SOURCE import io.novafoundation.nova.runtime.ethereum.StorageSharedRequestsBuilderFactory -import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry -import io.novafoundation.nova.runtime.repository.ChainStateRepository import io.novafoundation.nova.runtime.storage.source.StorageDataSource import javax.inject.Named @@ -33,48 +31,22 @@ class HydraDxExchangeModule { @Provides @IntoSet - fun provideOmniPoolSourceFactory( - @Named(REMOTE_STORAGE_SOURCE) remoteStorageSource: StorageDataSource, - chainRegistry: ChainRegistry, - assetSourceRegistry: AssetSourceRegistry, - hydraDxAssetIdConverter: HydraDxAssetIdConverter, - ): HydraDxSwapSource.Factory { - return OmniPoolSwapSourceFactory( - remoteStorageSource = remoteStorageSource, - chainRegistry = chainRegistry, - assetSourceRegistry = assetSourceRegistry, - hydraDxAssetIdConverter = hydraDxAssetIdConverter - ) + fun provideOmniPoolSourceFactory(): HydraDxSwapSource.Factory<*> { + return OmniPoolSwapSourceFactory() } @Provides @IntoSet fun provideStableSwapSourceFactory( - @Named(REMOTE_STORAGE_SOURCE) remoteStorageSource: StorageDataSource, hydraDxAssetIdConverter: HydraDxAssetIdConverter, - gson: Gson, - chainStateRepository: ChainStateRepository - ): HydraDxSwapSource.Factory { - return StableSwapSourceFactory( - remoteStorageSource = remoteStorageSource, - hydraDxAssetIdConverter = hydraDxAssetIdConverter, - gson = gson, - chainStateRepository = chainStateRepository - ) + ): HydraDxSwapSource.Factory<*> { + return StableSwapSourceFactory(hydraDxAssetIdConverter) } @Provides @IntoSet - fun provideXykSwapSourceFactory( - @Named(REMOTE_STORAGE_SOURCE) remoteStorageSource: StorageDataSource, - hydraDxAssetIdConverter: HydraDxAssetIdConverter, - assetSourceRegistry: AssetSourceRegistry - ): HydraDxSwapSource.Factory { - return XYKSwapSourceFactory( - remoteStorageSource = remoteStorageSource, - hydraDxAssetIdConverter = hydraDxAssetIdConverter, - assetSourceRegistry = assetSourceRegistry - ) + fun provideXykSwapSourceFactory(): HydraDxSwapSource.Factory<*> { + return XYKSwapSourceFactory() } @Provides @@ -85,7 +57,8 @@ class HydraDxExchangeModule { extrinsicService: ExtrinsicService, hydraDxAssetIdConverter: HydraDxAssetIdConverter, hydraDxNovaReferral: HydraDxNovaReferral, - swapSourceFactories: Set<@JvmSuppressWildcards HydraDxSwapSource.Factory>, + swapSourceFactories: Set<@JvmSuppressWildcards HydraDxSwapSource.Factory<*>>, + quotingFactory: HydraDxQuoting.Factory, assetSourceRegistry: AssetSourceRegistry, ): HydraDxExchangeFactory { return HydraDxExchangeFactory( @@ -95,7 +68,8 @@ class HydraDxExchangeModule { hydraDxAssetIdConverter = hydraDxAssetIdConverter, hydraDxNovaReferral = hydraDxNovaReferral, swapSourceFactories = swapSourceFactories, - assetSourceRegistry = assetSourceRegistry + assetSourceRegistry = assetSourceRegistry, + quotingFactory = quotingFactory ) } } diff --git a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/domain/swap/RealSwapService.kt b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/domain/swap/RealSwapService.kt index 2cb74d51bf..f510d4a6ab 100644 --- a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/domain/swap/RealSwapService.kt +++ b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/domain/swap/RealSwapService.kt @@ -8,11 +8,10 @@ import io.novafoundation.nova.common.utils.atLeastZero import io.novafoundation.nova.common.utils.filterNotNull import io.novafoundation.nova.common.utils.flatMap import io.novafoundation.nova.common.utils.flowOf +import io.novafoundation.nova.common.utils.forEachAsync import io.novafoundation.nova.common.utils.graph.Graph -import io.novafoundation.nova.common.utils.graph.Path import io.novafoundation.nova.common.utils.graph.create import io.novafoundation.nova.common.utils.graph.findAllPossibleDestinations -import io.novafoundation.nova.common.utils.graph.findDijkstraPathsBetween import io.novafoundation.nova.common.utils.graph.vertices import io.novafoundation.nova.common.utils.isZero import io.novafoundation.nova.common.utils.mapAsync @@ -21,26 +20,28 @@ import io.novafoundation.nova.common.utils.requireInnerNotNull import io.novafoundation.nova.common.utils.throttleLast import io.novafoundation.nova.common.utils.toPercent import io.novafoundation.nova.common.utils.withFlowScope -import io.novafoundation.nova.feature_account_api.domain.interfaces.AccountRepository +import io.novafoundation.nova.feature_account_api.data.fee.capability.CustomFeeCapabilityFacade import io.novafoundation.nova.feature_account_api.domain.model.MetaAccount -import io.novafoundation.nova.feature_account_api.domain.model.requestedAccountPaysFees import io.novafoundation.nova.feature_swap_api.domain.model.AtomicSwapOperation import io.novafoundation.nova.feature_swap_api.domain.model.AtomicSwapOperationArgs -import io.novafoundation.nova.feature_swap_api.domain.model.QuotedSwapEdge import io.novafoundation.nova.feature_swap_api.domain.model.ReQuoteTrigger import io.novafoundation.nova.feature_swap_api.domain.model.SlippageConfig -import io.novafoundation.nova.feature_swap_api.domain.model.SwapDirection import io.novafoundation.nova.feature_swap_api.domain.model.SwapExecuteArgs import io.novafoundation.nova.feature_swap_api.domain.model.SwapExecutionCorrection import io.novafoundation.nova.feature_swap_api.domain.model.SwapFee import io.novafoundation.nova.feature_swap_api.domain.model.SwapGraph import io.novafoundation.nova.feature_swap_api.domain.model.SwapGraphEdge import io.novafoundation.nova.feature_swap_api.domain.model.SwapLimit -import io.novafoundation.nova.feature_swap_api.domain.model.SwapPath import io.novafoundation.nova.feature_swap_api.domain.model.SwapQuote import io.novafoundation.nova.feature_swap_api.domain.model.SwapQuoteArgs -import io.novafoundation.nova.feature_swap_api.domain.model.SwapQuoteException import io.novafoundation.nova.feature_swap_api.domain.swap.SwapService +import io.novafoundation.nova.feature_swap_core_api.data.paths.PathQuoter +import io.novafoundation.nova.feature_swap_core_api.data.paths.model.QuotedPath +import io.novafoundation.nova.feature_swap_core_api.data.paths.model.firstSegmentQuote +import io.novafoundation.nova.feature_swap_core_api.data.paths.model.firstSegmentQuotedAmount +import io.novafoundation.nova.feature_swap_core_api.data.paths.model.lastSegmentQuote +import io.novafoundation.nova.feature_swap_core_api.data.paths.model.lastSegmentQuotedAmount +import io.novafoundation.nova.feature_swap_core_api.data.primitive.model.SwapDirection import io.novafoundation.nova.feature_swap_impl.BuildConfig import io.novafoundation.nova.feature_swap_impl.data.assetExchange.AssetExchange import io.novafoundation.nova.feature_swap_impl.data.assetExchange.ParentQuoterArgs @@ -52,7 +53,6 @@ import io.novafoundation.nova.feature_wallet_api.presentation.formatters.formatP import io.novafoundation.nova.runtime.ext.assetConversionSupported import io.novafoundation.nova.runtime.ext.fullId import io.novafoundation.nova.runtime.ext.hydraDxSupported -import io.novafoundation.nova.runtime.ext.isCommissionAsset import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry import io.novafoundation.nova.runtime.multiNetwork.asset import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain @@ -73,17 +73,16 @@ import kotlin.time.Duration.Companion.milliseconds private const val ALL_DIRECTIONS_CACHE = "RealSwapService.ALL_DIRECTIONS" private const val EXCHANGES_CACHE = "RealSwapService.EXCHANGES" +private const val QUOTER_CACHE = "RealSwapService.QUOTER" -private const val QUOTES_CACHE = "RealSwapService.QuotesCache" - -private const val PATHS_LIMIT = 4 internal class RealSwapService( private val assetConversionFactory: AssetConversionExchangeFactory, - private val hydraDxOmnipoolFactory: HydraDxExchangeFactory, + private val hydraDxExchangeFactory: HydraDxExchangeFactory, private val computationalCache: ComputationalCache, private val chainRegistry: ChainRegistry, - private val accountRepository: AccountRepository, + private val quoterFactory: PathQuoter.Factory, + private val customFeeCapabilityFacade: CustomFeeCapabilityFacade, private val debug: Boolean = BuildConfig.DEBUG ) : SwapService { @@ -91,12 +90,13 @@ internal class RealSwapService( val computationScope = CoroutineScope(coroutineContext) val exchange = exchanges(computationScope).getValue(asset.chainId) - val isCustomFeeToken = !asset.isCommissionAsset - val currentMetaAccount = accountRepository.getSelectedMetaAccount() + customFeeCapabilityFacade.canPayFeeInNonUtilityToken(asset, exchange) + } - // TODO we disable custom fee tokens payment for account types where current account is not the one who pays fees (e.g. it is proxied). - // This restriction can be removed once we consider all corner-cases - isCustomFeeToken && exchange.canPayFeeInNonUtilityToken(asset) && currentMetaAccount.type.requestedAccountPaysFees() + override suspend fun sync(coroutineScope: CoroutineScope) { + exchanges(coroutineScope) + .values + .forEachAsync { it.sync() } } override suspend fun assetsAvailableForSwap( @@ -200,9 +200,8 @@ internal class RealSwapService( return SwapQuote( amountIn = args.tokenIn.configuration.withAmount(amountIn), amountOut = args.tokenOut.configuration.withAmount(amountOut), - direction = args.swapDirection, priceImpact = args.calculatePriceImpact(amountIn, amountOut), - path = quotedTrade.path + quotedPath = quotedTrade ) } @@ -291,7 +290,7 @@ internal class RealSwapService( private suspend fun createExchange(computationScope: CoroutineScope, chain: Chain): AssetExchange? { val factory = when { chain.swap.assetConversionSupported() -> assetConversionFactory - chain.swap.hydraDxSupported() -> hydraDxOmnipoolFactory + chain.swap.hydraDxSupported() -> hydraDxExchangeFactory else -> null } @@ -304,58 +303,6 @@ internal class RealSwapService( .runningFold(emptyList()) { acc, directions -> acc + directions } } - private suspend fun pathsFromCacheOrCompute( - from: FullChainAssetId, - to: FullChainAssetId, - scope: CoroutineScope, - computation: suspend () -> List> - ): List> { - val mapKey = from to to - val cacheKey = "$QUOTES_CACHE:$mapKey" - - return computationalCache.useCache(cacheKey, scope) { - computation() - } - } - - private suspend fun quotePath( - path: SwapPath, - amount: Balance, - swapDirection: SwapDirection - ): QuotedTrade? { - val quote = when (swapDirection) { - SwapDirection.SPECIFIED_IN -> quotePathSell(path, amount) - SwapDirection.SPECIFIED_OUT -> quotePathBuy(path, amount) - } ?: return null - - return QuotedTrade(swapDirection, quote) - } - - private suspend fun quotePathBuy(path: Path, amount: Balance): Path? { - return runCatching { - val initial = mutableListOf() to amount - - path.foldRight(initial) { segment, (quotedPath, currentAmount) -> - val segmentQuote = segment.quote(currentAmount, SwapDirection.SPECIFIED_OUT) - quotedPath.add(0, QuotedSwapEdge(currentAmount, segmentQuote, segment)) - - quotedPath to segmentQuote - }.first - }.getOrNull() - } - - private suspend fun quotePathSell(path: Path, amount: Balance): Path? { - return runCatching { - val initial = mutableListOf() to amount - - path.fold(initial) { (quotedPath, currentAmount), segment -> - val segmentQuote = segment.quote(currentAmount, SwapDirection.SPECIFIED_IN) - quotedPath.add(QuotedSwapEdge(currentAmount, segmentQuote, segment)) - - quotedPath to segmentQuote - }.first - }.getOrNull() - } private suspend fun quoteTrade( chainAssetIn: Chain.Asset, @@ -364,23 +311,21 @@ internal class RealSwapService( swapDirection: SwapDirection, computationSharingScope: CoroutineScope ): QuotedTrade { - val from = chainAssetIn.fullId - val to = chainAssetOut.fullId + val quoter = getPathQuoter(computationSharingScope) - val paths = pathsFromCacheOrCompute(from, to, computationSharingScope) { - val graph = directionsGraph(computationSharingScope).first() - - graph.findDijkstraPathsBetween(from, to, limit = PATHS_LIMIT) + val bestPathQuote = quoter.findBestPath(chainAssetIn, chainAssetOut, amount, swapDirection) + if (debug) { + logQuotes(bestPathQuote.candidates) } - val quotedPaths = paths.mapNotNull { path -> quotePath(path, amount, swapDirection) } - logQuotes(quotedPaths) + return bestPathQuote.bestPath + } - if (paths.isEmpty()) { - throw SwapQuoteException.NotEnoughLiquidity + private suspend fun getPathQuoter(computationScope: CoroutineScope): PathQuoter { + return computationalCache.useCache(QUOTER_CACHE, computationScope) { + val graph = directionsGraph(computationScope).first() + quoterFactory.create(graph, computationScope) } - - return quotedPaths.max() } private inner class InnerParentQuoter( @@ -417,7 +362,7 @@ internal class RealSwapService( append(initialAmount) } - append(" --- "+ quotedSwapEdge.edge.debugLabel() + " ---> ") + append(" --- " + quotedSwapEdge.edge.debugLabel() + " ---> ") val assetOut = chainRegistry.asset(quotedSwapEdge.edge.to) val outAmount = quotedSwapEdge.quote.formatPlanks(assetOut) @@ -428,6 +373,8 @@ internal class RealSwapService( } } +private typealias QuotedTrade = QuotedPath + abstract class BaseSwapGraphEdge( val fromAsset: Chain.Asset, val toAsset: Chain.Asset @@ -437,30 +384,3 @@ abstract class BaseSwapGraphEdge( final override val to: FullChainAssetId = toAsset.fullId } - -private class QuotedTrade( - val direction: SwapDirection, - val path: Path -) : Comparable { - - override fun compareTo(other: QuotedTrade): Int { - return when (direction) { - // When we want to sell a token, the bigger the quote - the better - SwapDirection.SPECIFIED_IN -> (lastSegmentQuote - other.lastSegmentQuote).signum() - // When we want to buy a token, the smaller the quote - the better - SwapDirection.SPECIFIED_OUT -> (other.firstSegmentQuote - firstSegmentQuote).signum() - } - } -} - -private val QuotedTrade.lastSegmentQuotedAmount: Balance - get() = path.last().quotedAmount - -private val QuotedTrade.lastSegmentQuote: Balance - get() = path.last().quote - -private val QuotedTrade.firstSegmentQuote: Balance - get() = path.first().quote - -private val QuotedTrade.firstSegmentQuotedAmount: Balance - get() = path.first().quotedAmount diff --git a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/confirmation/SwapConfirmationViewModel.kt b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/confirmation/SwapConfirmationViewModel.kt index ac5a708fe3..4787f41422 100644 --- a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/confirmation/SwapConfirmationViewModel.kt +++ b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/confirmation/SwapConfirmationViewModel.kt @@ -22,7 +22,6 @@ import io.novafoundation.nova.feature_account_api.presenatation.account.wallet.W import io.novafoundation.nova.feature_account_api.presenatation.actions.ExternalActions import io.novafoundation.nova.feature_account_api.presenatation.actions.showAddressActions import io.novafoundation.nova.feature_account_api.presenatation.chain.icon -import io.novafoundation.nova.feature_swap_core.domain.model.SwapDirection import io.novafoundation.nova.feature_swap_api.domain.model.SwapFee import io.novafoundation.nova.feature_swap_api.domain.model.SwapQuote import io.novafoundation.nova.feature_swap_api.domain.model.SwapQuoteArgs @@ -34,6 +33,7 @@ import io.novafoundation.nova.feature_swap_api.presentation.formatters.SwapRateF import io.novafoundation.nova.feature_swap_api.presentation.view.SwapAssetView import io.novafoundation.nova.feature_swap_api.presentation.view.SwapAssetsView import io.novafoundation.nova.feature_swap_api.presentation.view.bottomSheet.description.launchSwapRateDescription +import io.novafoundation.nova.feature_swap_core_api.data.primitive.model.SwapDirection import io.novafoundation.nova.feature_swap_impl.R import io.novafoundation.nova.feature_swap_impl.domain.interactor.SwapInteractor import io.novafoundation.nova.feature_swap_impl.presentation.SwapRouter @@ -51,9 +51,7 @@ import io.novafoundation.nova.feature_wallet_api.domain.model.Asset import io.novafoundation.nova.feature_wallet_api.presentation.mixin.amountChooser.maxAction.MaxActionProvider import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.FeeLoaderMixin import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.FeeStatus -import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.GenericFeeLoaderMixin import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.GenericFeeLoaderMixin.Configuration -import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.getDecimalFeeOrNull import io.novafoundation.nova.feature_wallet_api.presentation.model.AmountModel import io.novafoundation.nova.feature_wallet_api.presentation.model.mapAmountToAmountModel import io.novafoundation.nova.feature_wallet_api.presentation.model.toAssetPayload @@ -389,7 +387,7 @@ class SwapConfirmationViewModel( tokenRepository.getToken(assetIn), tokenRepository.getToken(assetOut), swapQuote.editedBalance, - swapQuote.direction, + swapQuote.quotedPath.direction, ) feeMixin.setFee(swapState.fee) diff --git a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/main/SwapMainSettingsFragment.kt b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/main/SwapMainSettingsFragment.kt index 463cf99e0d..609837884d 100644 --- a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/main/SwapMainSettingsFragment.kt +++ b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/main/SwapMainSettingsFragment.kt @@ -17,7 +17,7 @@ import io.novafoundation.nova.common.view.setState import io.novafoundation.nova.common.view.showLoadingValue import io.novafoundation.nova.feature_buy_api.presentation.mixin.BuyMixinUi import io.novafoundation.nova.feature_swap_api.di.SwapFeatureApi -import io.novafoundation.nova.feature_swap_core.domain.model.SwapDirection +import io.novafoundation.nova.feature_swap_core_api.data.primitive.model.SwapDirection import io.novafoundation.nova.feature_swap_api.presentation.model.SwapSettingsPayload import io.novafoundation.nova.feature_swap_impl.R import io.novafoundation.nova.feature_swap_impl.di.SwapFeatureComponent diff --git a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/main/SwapMainSettingsViewModel.kt b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/main/SwapMainSettingsViewModel.kt index 7728d29b9b..f7ddc6853e 100644 --- a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/main/SwapMainSettingsViewModel.kt +++ b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/main/SwapMainSettingsViewModel.kt @@ -34,7 +34,7 @@ import io.novafoundation.nova.common.view.bottomSheet.description.launchNetworkF import io.novafoundation.nova.feature_account_api.domain.interfaces.SelectedAccountUseCase import io.novafoundation.nova.feature_account_api.domain.model.addressIn import io.novafoundation.nova.feature_buy_api.presentation.mixin.BuyMixin -import io.novafoundation.nova.feature_swap_core.domain.model.SwapDirection +import io.novafoundation.nova.feature_swap_core_api.data.primitive.model.SwapDirection import io.novafoundation.nova.feature_swap_api.domain.model.SwapFee import io.novafoundation.nova.feature_swap_api.domain.model.SwapQuote import io.novafoundation.nova.feature_swap_api.domain.model.SwapQuoteArgs diff --git a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/state/RealSwapSettingsState.kt b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/state/RealSwapSettingsState.kt index 37d79c556c..e976682944 100644 --- a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/state/RealSwapSettingsState.kt +++ b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/state/RealSwapSettingsState.kt @@ -1,8 +1,8 @@ package io.novafoundation.nova.feature_swap_impl.presentation.state import io.novafoundation.nova.common.utils.Percent -import io.novafoundation.nova.feature_swap_core.domain.model.SwapDirection -import io.novafoundation.nova.feature_swap_core.domain.model.flip +import io.novafoundation.nova.feature_swap_core_api.data.primitive.model.SwapDirection +import io.novafoundation.nova.feature_swap_core_api.data.primitive.model.flip import io.novafoundation.nova.feature_swap_api.presentation.state.SwapSettings import io.novafoundation.nova.feature_swap_api.presentation.state.SwapSettingsState import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.types.Balance diff --git a/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/data/network/blockchain/assets/history/realtime/substrate/RealSubstrateRealtimeOperationFetcher.kt b/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/data/network/blockchain/assets/history/realtime/substrate/RealSubstrateRealtimeOperationFetcher.kt index bdd02a8183..98813da927 100644 --- a/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/data/network/blockchain/assets/history/realtime/substrate/RealSubstrateRealtimeOperationFetcher.kt +++ b/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/data/network/blockchain/assets/history/realtime/substrate/RealSubstrateRealtimeOperationFetcher.kt @@ -1,6 +1,6 @@ package io.novafoundation.nova.feature_wallet_impl.data.network.blockchain.assets.history.realtime.substrate -import io.novafoundation.nova.feature_swap_core.data.network.HydraDxAssetIdConverter +import io.novafoundation.nova.feature_swap_core_api.data.network.HydraDxAssetIdConverter import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.history.realtime.RealtimeHistoryUpdate import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.history.realtime.substrate.SubstrateRealtimeOperationFetcher import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.history.realtime.substrate.SubstrateRealtimeOperationFetcher.Extractor diff --git a/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/data/network/blockchain/assets/history/realtime/substrate/hydraDx/BaseHydraDxSwapExtractor.kt b/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/data/network/blockchain/assets/history/realtime/substrate/hydraDx/BaseHydraDxSwapExtractor.kt index 9f6938b38b..8a652bf916 100644 --- a/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/data/network/blockchain/assets/history/realtime/substrate/hydraDx/BaseHydraDxSwapExtractor.kt +++ b/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/data/network/blockchain/assets/history/realtime/substrate/hydraDx/BaseHydraDxSwapExtractor.kt @@ -2,8 +2,8 @@ package io.novafoundation.nova.feature_wallet_impl.data.network.blockchain.asset import io.novafoundation.nova.common.data.network.runtime.binding.bindNumber import io.novafoundation.nova.common.utils.Modules -import io.novafoundation.nova.feature_swap_core.data.network.HydraDxAssetId -import io.novafoundation.nova.feature_swap_core.data.network.HydraDxAssetIdConverter +import io.novafoundation.nova.feature_swap_core_api.data.network.HydraDxAssetId +import io.novafoundation.nova.feature_swap_core_api.data.network.HydraDxAssetIdConverter import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.history.realtime.RealtimeHistoryUpdate import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.history.realtime.substrate.SubstrateRealtimeOperationFetcher import io.novafoundation.nova.feature_wallet_api.domain.model.ChainAssetWithAmount @@ -15,7 +15,7 @@ import io.novafoundation.nova.runtime.multiNetwork.runtime.repository.requireNat import io.novasama.substrate_sdk_android.runtime.definitions.types.generics.GenericCall abstract class BaseHydraDxSwapExtractor( - private val hydraDxAssetIdConverter: io.novafoundation.nova.feature_swap_core.data.network.HydraDxAssetIdConverter, + private val hydraDxAssetIdConverter: HydraDxAssetIdConverter, ) : SubstrateRealtimeOperationFetcher.Extractor { abstract fun isSwap(call: GenericCall.Instance): Boolean @@ -61,9 +61,9 @@ abstract class BaseHydraDxSwapExtractor( } protected data class SwapArgs( - val assetIn: io.novafoundation.nova.feature_swap_core.data.network.HydraDxAssetId, - val assetOut: io.novafoundation.nova.feature_swap_core.data.network.HydraDxAssetId, - val amountIn: io.novafoundation.nova.feature_swap_core.data.network.HydraDxAssetId, - val amountOut: io.novafoundation.nova.feature_swap_core.data.network.HydraDxAssetId + val assetIn: HydraDxAssetId, + val assetOut: HydraDxAssetId, + val amountIn: HydraDxAssetId, + val amountOut: HydraDxAssetId ) } diff --git a/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/data/network/blockchain/assets/history/realtime/substrate/hydraDx/HydraDxOmniPoolSwapExtractor.kt b/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/data/network/blockchain/assets/history/realtime/substrate/hydraDx/HydraDxOmniPoolSwapExtractor.kt index df1764135b..846e814cf4 100644 --- a/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/data/network/blockchain/assets/history/realtime/substrate/hydraDx/HydraDxOmniPoolSwapExtractor.kt +++ b/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/data/network/blockchain/assets/history/realtime/substrate/hydraDx/HydraDxOmniPoolSwapExtractor.kt @@ -2,7 +2,7 @@ package io.novafoundation.nova.feature_wallet_impl.data.network.blockchain.asset import io.novafoundation.nova.common.data.network.runtime.binding.bindNumber import io.novafoundation.nova.common.utils.Modules -import io.novafoundation.nova.feature_swap_core.data.network.HydraDxAssetIdConverter +import io.novafoundation.nova.feature_swap_core_api.data.network.HydraDxAssetIdConverter import io.novafoundation.nova.runtime.extrinsic.visitor.api.ExtrinsicVisit import io.novafoundation.nova.runtime.multiNetwork.runtime.repository.findEvent import io.novasama.substrate_sdk_android.runtime.definitions.types.generics.GenericCall diff --git a/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/data/network/blockchain/assets/history/realtime/substrate/hydraDx/HydraDxRouterSwapExtractor.kt b/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/data/network/blockchain/assets/history/realtime/substrate/hydraDx/HydraDxRouterSwapExtractor.kt index 6b8f469187..d20f53e0cc 100644 --- a/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/data/network/blockchain/assets/history/realtime/substrate/hydraDx/HydraDxRouterSwapExtractor.kt +++ b/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/data/network/blockchain/assets/history/realtime/substrate/hydraDx/HydraDxRouterSwapExtractor.kt @@ -2,7 +2,7 @@ package io.novafoundation.nova.feature_wallet_impl.data.network.blockchain.asset import io.novafoundation.nova.common.data.network.runtime.binding.bindNumber import io.novafoundation.nova.common.utils.Modules -import io.novafoundation.nova.feature_swap_core.data.network.HydraDxAssetIdConverter +import io.novafoundation.nova.feature_swap_core_api.data.network.HydraDxAssetIdConverter import io.novafoundation.nova.runtime.extrinsic.visitor.api.ExtrinsicVisit import io.novafoundation.nova.runtime.multiNetwork.runtime.repository.findEvent import io.novasama.substrate_sdk_android.runtime.definitions.types.generics.GenericCall diff --git a/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/di/WalletFeatureComponent.kt b/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/di/WalletFeatureComponent.kt index f72b008bc5..e1a5543770 100644 --- a/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/di/WalletFeatureComponent.kt +++ b/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/di/WalletFeatureComponent.kt @@ -6,7 +6,7 @@ import io.novafoundation.nova.common.di.scope.FeatureScope import io.novafoundation.nova.core_db.di.DbApi import io.novafoundation.nova.feature_account_api.di.AccountFeatureApi import io.novafoundation.nova.feature_currency_api.di.CurrencyFeatureApi -import io.novafoundation.nova.feature_swap_core.di.SwapCoreApi +import io.novafoundation.nova.feature_swap_core_api.di.SwapCoreApi import io.novafoundation.nova.feature_wallet_api.di.WalletFeatureApi import io.novafoundation.nova.feature_wallet_impl.di.modules.AssetsModule import io.novafoundation.nova.feature_wallet_impl.di.modules.BalanceLocksModule diff --git a/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/di/WalletFeatureDependencies.kt b/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/di/WalletFeatureDependencies.kt index 8b79b7f3eb..aadc4385fb 100644 --- a/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/di/WalletFeatureDependencies.kt +++ b/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/di/WalletFeatureDependencies.kt @@ -32,14 +32,14 @@ import io.novafoundation.nova.core_db.dao.TokenDao import io.novafoundation.nova.feature_account_api.data.ethereum.transaction.EvmTransactionService import io.novafoundation.nova.feature_account_api.data.extrinsic.ExtrinsicService import io.novafoundation.nova.feature_account_api.data.fee.FeePaymentProviderRegistry +import io.novafoundation.nova.feature_account_api.data.fee.capability.CustomFeeCapabilityFacade import io.novafoundation.nova.feature_account_api.domain.interfaces.AccountRepository import io.novafoundation.nova.feature_account_api.domain.interfaces.SelectedAccountUseCase import io.novafoundation.nova.feature_account_api.domain.updaters.AccountUpdateScope import io.novafoundation.nova.feature_account_api.presenatation.account.AddressDisplayUseCase import io.novafoundation.nova.feature_account_api.presenatation.actions.ExternalActions import io.novafoundation.nova.feature_currency_api.domain.interfaces.CurrencyRepository -import io.novafoundation.nova.feature_account_api.data.fee.capability.CustomFeeCapabilityFacade -import io.novafoundation.nova.feature_swap_core.data.network.HydraDxAssetIdConverter +import io.novafoundation.nova.feature_swap_core_api.data.network.HydraDxAssetIdConverter import io.novafoundation.nova.runtime.di.LOCAL_STORAGE_SOURCE import io.novafoundation.nova.runtime.di.REMOTE_STORAGE_SOURCE import io.novafoundation.nova.runtime.ethereum.StorageSharedRequestsBuilderFactory diff --git a/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/di/WalletFeatureHolder.kt b/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/di/WalletFeatureHolder.kt index e82c73d135..9271621e06 100644 --- a/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/di/WalletFeatureHolder.kt +++ b/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/di/WalletFeatureHolder.kt @@ -6,7 +6,7 @@ import io.novafoundation.nova.common.di.scope.ApplicationScope import io.novafoundation.nova.core_db.di.DbApi import io.novafoundation.nova.feature_account_api.di.AccountFeatureApi import io.novafoundation.nova.feature_currency_api.di.CurrencyFeatureApi -import io.novafoundation.nova.feature_swap_core.di.SwapCoreApi +import io.novafoundation.nova.feature_swap_core_api.di.SwapCoreApi import io.novafoundation.nova.runtime.di.RuntimeApi import javax.inject.Inject 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 c1dbadcdbb..53ddc14d29 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 @@ -26,7 +26,7 @@ import io.novafoundation.nova.feature_account_api.domain.interfaces.AccountRepos import io.novafoundation.nova.feature_account_api.domain.updaters.AccountUpdateScope import io.novafoundation.nova.feature_currency_api.domain.interfaces.CurrencyRepository import io.novafoundation.nova.feature_account_api.data.fee.capability.CustomFeeCapabilityFacade -import io.novafoundation.nova.feature_swap_core.data.network.HydraDxAssetIdConverter +import io.novafoundation.nova.feature_swap_core_api.data.network.HydraDxAssetIdConverter import io.novafoundation.nova.feature_wallet_api.data.cache.AssetCache import io.novafoundation.nova.feature_wallet_api.data.cache.CoinPriceLocalDataSourceImpl import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.AssetSourceRegistry diff --git a/settings.gradle b/settings.gradle index e55d641dd6..6e7f714343 100644 --- a/settings.gradle +++ b/settings.gradle @@ -47,3 +47,4 @@ include ':feature-cloud-backup-test' include ':bindings:metadata_shortener' include ':feature-ledger-core' include ':feature-swap-core' +include ':feature-swap-core:api' From 523e77001c8f8d4d0e6ac84588f4bf46f123e807 Mon Sep 17 00:00:00 2001 From: Valentun Date: Tue, 8 Oct 2024 10:00:28 +0300 Subject: [PATCH 10/83] Optimized method for swap availability --- .../novafoundation/nova/common/utils/graph/Graph.kt | 5 +++++ .../nova/feature_swap_api/domain/swap/SwapService.kt | 2 ++ .../feature_swap_core/domain/paths/RealPathQuoter.kt | 12 +++++++++++- .../interactor/RealSwapAvailabilityInteractor.kt | 3 +-- .../feature_swap_impl/domain/swap/RealSwapService.kt | 9 +++++++++ 5 files changed, 28 insertions(+), 3 deletions(-) diff --git a/common/src/main/java/io/novafoundation/nova/common/utils/graph/Graph.kt b/common/src/main/java/io/novafoundation/nova/common/utils/graph/Graph.kt index 4d2c20643c..e7e34a0af3 100644 --- a/common/src/main/java/io/novafoundation/nova/common/utils/graph/Graph.kt +++ b/common/src/main/java/io/novafoundation/nova/common/utils/graph/Graph.kt @@ -35,6 +35,11 @@ fun > Graph.findAllPossibleDestinations(origin: N): Set return reachabilityDfs(origin, adjacencyList).toSet() } +fun > Graph.hasOutcomingDirections(origin: N): Boolean { + val vertices = adjacencyList[origin] ?: return false + return vertices.isNotEmpty() +} + fun List>.findAllPossibleDirectionsToList(): MultiMapList { val result = mutableMapOf>() diff --git a/feature-swap-api/src/main/java/io/novafoundation/nova/feature_swap_api/domain/swap/SwapService.kt b/feature-swap-api/src/main/java/io/novafoundation/nova/feature_swap_api/domain/swap/SwapService.kt index 967beab62c..4f59e9ce70 100644 --- a/feature-swap-api/src/main/java/io/novafoundation/nova/feature_swap_api/domain/swap/SwapService.kt +++ b/feature-swap-api/src/main/java/io/novafoundation/nova/feature_swap_api/domain/swap/SwapService.kt @@ -22,6 +22,8 @@ interface SwapService { suspend fun availableSwapDirectionsFor(asset: Chain.Asset, computationScope: CoroutineScope): Flow> + suspend fun hasAvailableSwapDirections(asset: Chain.Asset, computationScope: CoroutineScope): Flow + suspend fun canPayFeeInNonUtilityAsset(asset: Chain.Asset): Boolean suspend fun quote( diff --git a/feature-swap-core/src/main/java/io/novafoundation/nova/feature_swap_core/domain/paths/RealPathQuoter.kt b/feature-swap-core/src/main/java/io/novafoundation/nova/feature_swap_core/domain/paths/RealPathQuoter.kt index 55f7725169..706f278089 100644 --- a/feature-swap-core/src/main/java/io/novafoundation/nova/feature_swap_core/domain/paths/RealPathQuoter.kt +++ b/feature-swap-core/src/main/java/io/novafoundation/nova/feature_swap_core/domain/paths/RealPathQuoter.kt @@ -1,5 +1,6 @@ package io.novafoundation.nova.feature_swap_core.domain.paths +import android.util.Log import io.novafoundation.nova.common.data.memory.ComputationalCache import io.novafoundation.nova.common.utils.graph.Graph import io.novafoundation.nova.common.utils.graph.Path @@ -16,6 +17,8 @@ import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain import io.novafoundation.nova.runtime.multiNetwork.chain.model.FullChainAssetId import kotlinx.coroutines.CoroutineScope import java.math.BigInteger +import kotlin.time.ExperimentalTime +import kotlin.time.measureTimedValue private const val PATHS_LIMIT = 4 private const val QUOTES_CACHE = "RealSwapService.QuotesCache" @@ -38,6 +41,7 @@ private class RealPathQuoter( private val computationalScope: CoroutineScope ): PathQuoter { + @OptIn(ExperimentalTime::class) override suspend fun findBestPath( chainAssetIn: Chain.Asset, chainAssetOut: Chain.Asset, @@ -48,7 +52,13 @@ private class RealPathQuoter( val to = chainAssetOut.fullId val paths = pathsFromCacheOrCompute(from, to, computationalScope) { - graph.findDijkstraPathsBetween(from, to, limit = PATHS_LIMIT) + val (paths, duration) = measureTimedValue { + graph.findDijkstraPathsBetween(from, to, limit = PATHS_LIMIT) + } + + Log.d("Swaps", "${chainAssetIn.symbol} -> ${chainAssetOut.symbol}: finding ${paths.size} paths took $duration") + + paths } val quotedPaths = paths.mapNotNull { path -> quotePath(path, amount, swapDirection) } diff --git a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/domain/interactor/RealSwapAvailabilityInteractor.kt b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/domain/interactor/RealSwapAvailabilityInteractor.kt index 905d415032..e40b5b8f79 100644 --- a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/domain/interactor/RealSwapAvailabilityInteractor.kt +++ b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/domain/interactor/RealSwapAvailabilityInteractor.kt @@ -24,7 +24,6 @@ class RealSwapAvailabilityInteractor( } override suspend fun swapAvailableFlow(asset: Chain.Asset, coroutineScope: CoroutineScope): Flow { - return swapService.availableSwapDirectionsFor(asset, coroutineScope) - .map { it.isNotEmpty() } + return swapService.hasAvailableSwapDirections(asset, coroutineScope) } } diff --git a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/domain/swap/RealSwapService.kt b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/domain/swap/RealSwapService.kt index f510d4a6ab..08506e8cab 100644 --- a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/domain/swap/RealSwapService.kt +++ b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/domain/swap/RealSwapService.kt @@ -12,6 +12,7 @@ import io.novafoundation.nova.common.utils.forEachAsync import io.novafoundation.nova.common.utils.graph.Graph import io.novafoundation.nova.common.utils.graph.create import io.novafoundation.nova.common.utils.graph.findAllPossibleDestinations +import io.novafoundation.nova.common.utils.graph.hasOutcomingDirections import io.novafoundation.nova.common.utils.graph.vertices import io.novafoundation.nova.common.utils.isZero import io.novafoundation.nova.common.utils.mapAsync @@ -94,6 +95,8 @@ internal class RealSwapService( } override suspend fun sync(coroutineScope: CoroutineScope) { + Log.d("Swaps", "Syncing swap service") + exchanges(coroutineScope) .values .forEachAsync { it.sync() } @@ -112,6 +115,10 @@ internal class RealSwapService( return directionsGraph(computationScope).map { it.findAllPossibleDestinations(asset.fullId) } } + override suspend fun hasAvailableSwapDirections(asset: Chain.Asset, computationScope: CoroutineScope): Flow { + return directionsGraph(computationScope).map { it.hasOutcomingDirections(asset.fullId) } + } + override suspend fun quote( args: SwapQuoteArgs, computationSharingScope: CoroutineScope @@ -213,6 +220,8 @@ internal class RealSwapService( override fun runSubscriptions(chainIn: Chain, metaAccount: MetaAccount): Flow { return withFlowScope { scope -> + Log.d("Swaps", "Starting new subscriptions") + val exchanges = exchanges(scope) exchanges.getValue(chainIn.id).runSubscriptions(chainIn, metaAccount) }.throttleLast(500.milliseconds) From 281124d7e6705a1e244c5505063bd7031d52b941 Mon Sep 17 00:00:00 2001 From: Valentun Date: Tue, 8 Oct 2024 10:32:14 +0300 Subject: [PATCH 11/83] Optimize Dijkstra --- .../nova/common/utils/graph/Graph.kt | 42 +++++---- .../nova/common/utils/graph/GraphKtTest.kt | 90 ++++++------------- 2 files changed, 51 insertions(+), 81 deletions(-) diff --git a/common/src/main/java/io/novafoundation/nova/common/utils/graph/Graph.kt b/common/src/main/java/io/novafoundation/nova/common/utils/graph/Graph.kt index e7e34a0af3..56fd04482f 100644 --- a/common/src/main/java/io/novafoundation/nova/common/utils/graph/Graph.kt +++ b/common/src/main/java/io/novafoundation/nova/common/utils/graph/Graph.kt @@ -40,25 +40,21 @@ fun > Graph.hasOutcomingDirections(origin: N): Boolean { return vertices.isNotEmpty() } -fun List>.findAllPossibleDirectionsToList(): MultiMapList { - val result = mutableMapOf>() - - forEach { connectedComponent -> - connectedComponent.forEach { node -> - // in the connected component every node is connected to every other except itself - result[node] = connectedComponent - node - } - } - - return result -} fun > Graph.findDijkstraPathsBetween(from: N, to: N, limit: Int): List> { - data class QueueElement(val currentPath: Path, val nodeList: List, val score: Int) : Comparable { + data class QueueElement(val currentPath: Path, val score: Int) : Comparable { override fun compareTo(other: QueueElement): Int { return score - other.score } + + fun lastNode(): N { + return if (currentPath.isNotEmpty()) currentPath.last().to else from + } + + operator fun contains(node: N): Boolean { + return currentPath.any { it.from == node || it.to == node } + } } val paths = mutableListOf>() @@ -67,11 +63,11 @@ fun > Graph.findDijkstraPathsBetween(from: N, to: N, limit: adjacencyList.keys.forEach { count[it] = 0 } val heap = PriorityQueue() - heap.add(QueueElement(currentPath = emptyList(), nodeList = listOf(from), score = 0)) + heap.add(QueueElement(currentPath = emptyList(), score = 0)) while (heap.isNotEmpty() && paths.size < limit) { val minimumQueueElement = heap.poll()!! - val lastNode = minimumQueueElement.nodeList.last() + val lastNode = minimumQueueElement.lastNode() val newCount = count.getValue(lastNode) + 1 count[lastNode] = newCount @@ -83,11 +79,10 @@ fun > Graph.findDijkstraPathsBetween(from: N, to: N, limit: if (newCount <= limit) { adjacencyList.getValue(lastNode).forEach { edge -> - if (edge.to in minimumQueueElement.nodeList) return@forEach + if (edge.to in minimumQueueElement) return@forEach val newElement = QueueElement( currentPath = minimumQueueElement.currentPath + edge, - nodeList = minimumQueueElement.nodeList + edge.to, score = minimumQueueElement.score + 1 ) @@ -139,6 +134,19 @@ private fun > reachabilityDfs( // return result //} +//fun List>.findAllPossibleDirectionsToList(): MultiMapList { +// val result = mutableMapOf>() +// +// forEach { connectedComponent -> +// connectedComponent.forEach { node -> +// // in the connected component every node is connected to every other except itself +// result[node] = connectedComponent - node +// } +// } +// +// return result +//} + //fun > Graph.findAllPossibleDirections(): MultiMap { // val connectedComponents = findConnectedComponents() // return connectedComponents.findAllPossibleDirections() diff --git a/common/src/test/java/io/novafoundation/nova/common/utils/graph/GraphKtTest.kt b/common/src/test/java/io/novafoundation/nova/common/utils/graph/GraphKtTest.kt index 6da292b72f..d098332a82 100644 --- a/common/src/test/java/io/novafoundation/nova/common/utils/graph/GraphKtTest.kt +++ b/common/src/test/java/io/novafoundation/nova/common/utils/graph/GraphKtTest.kt @@ -1,64 +1,12 @@ package io.novafoundation.nova.common.utils.graph -import io.novafoundation.nova.common.utils.mapToSet import io.novafoundation.nova.test_shared.assertListEquals -import io.novafoundation.nova.test_shared.assertMapEquals -import io.novafoundation.nova.test_shared.assertSetEquals import org.junit.Test +import kotlin.time.ExperimentalTime +import kotlin.time.measureTime internal class GraphKtTest { - @Test - fun shouldFindConnectedComponents() { - // 3 and 2 are connected through 1 - testConnectedComponents( - 1 to listOf(2, 3), - 2 to listOf(1), - 3 to listOf(1), - expectedComponents = listOf(listOf(1, 2, 3)) - ) - - testConnectedComponents( - 1 to listOf(2), - 2 to listOf(1), - 3 to emptyList(), - expectedComponents = listOf(listOf(1, 2), listOf(3)) - ) - - testConnectedComponents( - 1 to listOf(2, 3), - 2 to listOf(1), - 3 to listOf(1), - 4 to listOf(5), - 5 to listOf(4), - 6 to emptyList(), - expectedComponents = listOf(listOf(1, 2, 3), listOf(4, 5), listOf(6)) - ) - } - - @Test - fun shouldFindAllPossibleDirections() { - val graph = Graph.createSimple( - 1 to listOf(2, 3), - 2 to listOf(1), - 3 to listOf(1), - 4 to listOf(5), - 5 to listOf(4), - 6 to emptyList(), - ) - val actual = graph.findAllPossibleDirections() - val expected = mapOf( - 1 to setOf(2, 3), - 2 to setOf(1, 3), - 3 to setOf(1, 2), - 4 to setOf(5), - 5 to setOf(4), - 6 to emptySet() - ) - - assertMapEquals(expected, actual) - } - @Test fun shouldFindPaths() { val graph = Graph.createSimple( @@ -84,18 +32,32 @@ internal class GraphKtTest { assertListEquals(expected, actual) } - private fun testConnectedComponents( - vararg adjacencyPairs: Pair>, - expectedComponents: List> - ) { - val graph = Graph.createSimple(*adjacencyPairs) - val actualComponents = graph.findConnectedComponents().unordered() - val expectedUnordered = expectedComponents.unordered() + @OptIn(ExperimentalTime::class) + @Test + fun testPerformance() { + val graphSize = 200 + val graph = fullyConnectedGraph(graphSize) + + val time = measureTime { + repeat(100) { i -> + graph.findDijkstraPathsBetween(i, graphSize - i, limit = 10) + } + + } - assertSetEquals(expectedUnordered, actualComponents) + print("Execution time: ${time / 100}") } - private fun Iterable>.unordered(): Set> { - return mapToSet { it.toSet() } + private fun fullyConnectedGraph(size: Int): SimpleGraph { + return Graph.build { + (0..size).onEach { i -> + (0..size).onEach { j -> + if (i != j) { + val edge = SimpleEdge(i, j) + addEdge(edge) + } + } + } + } } } From 6a65d028b03cb4cc04ec361333870c0df4e762a2 Mon Sep 17 00:00:00 2001 From: Valentun Date: Tue, 8 Oct 2024 10:45:35 +0300 Subject: [PATCH 12/83] Small tweaks --- .../feature_swap_core/domain/paths/RealPathQuoter.kt | 9 +++++++-- .../domain/interactor/SwapInteractor.kt | 5 ++--- .../confirmation/SwapConfirmationViewModel.kt | 2 +- .../presentation/main/SwapMainSettingsViewModel.kt | 8 ++++---- 4 files changed, 14 insertions(+), 10 deletions(-) diff --git a/feature-swap-core/src/main/java/io/novafoundation/nova/feature_swap_core/domain/paths/RealPathQuoter.kt b/feature-swap-core/src/main/java/io/novafoundation/nova/feature_swap_core/domain/paths/RealPathQuoter.kt index 706f278089..77931a958e 100644 --- a/feature-swap-core/src/main/java/io/novafoundation/nova/feature_swap_core/domain/paths/RealPathQuoter.kt +++ b/feature-swap-core/src/main/java/io/novafoundation/nova/feature_swap_core/domain/paths/RealPathQuoter.kt @@ -75,13 +75,18 @@ private class RealPathQuoter( scope: CoroutineScope, computation: suspend () -> List> ): List> { - val mapKey = from to to - val cacheKey = "$QUOTES_CACHE:$mapKey" + val cacheKey = "$QUOTES_CACHE:${pathsCacheKey(from, to)}" return computationalCache.useCache(cacheKey, scope) { computation() } } + private fun pathsCacheKey(from: FullChainAssetId, to: FullChainAssetId): String { + val fromKey = "${from.chainId}:${from.assetId}" + val toKey = "${to.chainId}:${to.assetId}" + + return "${fromKey}:${toKey}" + } private suspend fun quotePath( path: Path, diff --git a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/domain/interactor/SwapInteractor.kt b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/domain/interactor/SwapInteractor.kt index df79ab3380..7d676791de 100644 --- a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/domain/interactor/SwapInteractor.kt +++ b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/domain/interactor/SwapInteractor.kt @@ -47,7 +47,6 @@ import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.map import kotlinx.coroutines.withContext -import kotlin.coroutines.coroutineContext class SwapInteractor( private val swapService: SwapService, @@ -83,8 +82,8 @@ class SwapInteractor( } } - suspend fun quote(quoteArgs: SwapQuoteArgs): Result { - return swapService.quote(quoteArgs, CoroutineScope(coroutineContext)) + suspend fun quote(quoteArgs: SwapQuoteArgs, computationalScope: CoroutineScope): Result { + return swapService.quote(quoteArgs, computationalScope) } suspend fun executeSwap(swapExecuteArgs: SwapExecuteArgs): Result = withContext(Dispatchers.IO) { diff --git a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/confirmation/SwapConfirmationViewModel.kt b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/confirmation/SwapConfirmationViewModel.kt index 4787f41422..153e1ba30e 100644 --- a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/confirmation/SwapConfirmationViewModel.kt +++ b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/confirmation/SwapConfirmationViewModel.kt @@ -352,7 +352,7 @@ class SwapConfirmationViewModel( private fun runQuoting(newSwapQuoteArgs: SwapQuoteArgs) { launch { val confirmationState = confirmationStateFlow.value ?: return@launch - val swapQuote = swapInteractor.quote(newSwapQuoteArgs) + val swapQuote = swapInteractor.quote(newSwapQuoteArgs, viewModelScope) .onFailure { } .getOrNull() ?: return@launch diff --git a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/main/SwapMainSettingsViewModel.kt b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/main/SwapMainSettingsViewModel.kt index f7ddc6853e..7e48ff4015 100644 --- a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/main/SwapMainSettingsViewModel.kt +++ b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/main/SwapMainSettingsViewModel.kt @@ -33,8 +33,8 @@ import io.novafoundation.nova.common.view.bottomSheet.description.DescriptionBot import io.novafoundation.nova.common.view.bottomSheet.description.launchNetworkFeeDescription import io.novafoundation.nova.feature_account_api.domain.interfaces.SelectedAccountUseCase import io.novafoundation.nova.feature_account_api.domain.model.addressIn +import io.novafoundation.nova.feature_account_api.presenatation.fee.select.FeeAssetSelectorBottomSheet import io.novafoundation.nova.feature_buy_api.presentation.mixin.BuyMixin -import io.novafoundation.nova.feature_swap_core_api.data.primitive.model.SwapDirection import io.novafoundation.nova.feature_swap_api.domain.model.SwapFee import io.novafoundation.nova.feature_swap_api.domain.model.SwapQuote import io.novafoundation.nova.feature_swap_api.domain.model.SwapQuoteArgs @@ -47,19 +47,19 @@ import io.novafoundation.nova.feature_swap_api.presentation.model.mapFromModel import io.novafoundation.nova.feature_swap_api.presentation.state.SwapSettings import io.novafoundation.nova.feature_swap_api.presentation.state.SwapSettingsStateProvider import io.novafoundation.nova.feature_swap_api.presentation.view.bottomSheet.description.launchSwapRateDescription +import io.novafoundation.nova.feature_swap_core_api.data.primitive.model.SwapDirection import io.novafoundation.nova.feature_swap_impl.R import io.novafoundation.nova.feature_swap_impl.domain.interactor.SwapInteractor import io.novafoundation.nova.feature_swap_impl.domain.model.GetAssetInOption import io.novafoundation.nova.feature_swap_impl.presentation.SwapRouter -import io.novafoundation.nova.feature_swap_impl.presentation.common.state.SwapStateStoreProvider import io.novafoundation.nova.feature_swap_impl.presentation.common.state.SwapState +import io.novafoundation.nova.feature_swap_impl.presentation.common.state.SwapStateStoreProvider import io.novafoundation.nova.feature_swap_impl.presentation.fieldValidation.EnoughAmountToSwapValidatorFactory import io.novafoundation.nova.feature_swap_impl.presentation.fieldValidation.LiquidityFieldValidatorFactory import io.novafoundation.nova.feature_swap_impl.presentation.fieldValidation.SwapReceiveAmountAboveEDFieldValidatorFactory import io.novafoundation.nova.feature_swap_impl.presentation.main.input.SwapAmountInputMixin import io.novafoundation.nova.feature_swap_impl.presentation.main.input.SwapAmountInputMixinFactory import io.novafoundation.nova.feature_swap_impl.presentation.main.input.SwapInputMixinPriceImpactFiatFormatterFactory -import io.novafoundation.nova.feature_account_api.presenatation.fee.select.FeeAssetSelectorBottomSheet import io.novafoundation.nova.feature_swap_impl.presentation.main.view.GetAssetInBottomSheet import io.novafoundation.nova.feature_swap_impl.presentation.mixin.maxAction.MaxActionProviderFactory import io.novafoundation.nova.feature_swap_impl.presentation.state.swapSettingsFlow @@ -653,7 +653,7 @@ class SwapMainSettingsViewModel( quotingState.value = QuotingState.Loading } - val quote = swapInteractor.quote(swapQuoteArgs) + val quote = swapInteractor.quote(swapQuoteArgs, viewModelScope) quotingState.value = quote.fold( onSuccess = { QuotingState.Loaded(it, swapQuoteArgs, swapSettings.feeAsset!!) }, From f877f3abc1a300df1c6117e4ee74a347e081d365 Mon Sep 17 00:00:00 2001 From: Valentun Date: Tue, 8 Oct 2024 13:34:30 +0300 Subject: [PATCH 13/83] Cross chain quoting WIP --- .../nova/common/utils/FlowExt.kt | 4 + .../nova/common/utils/graph/Graph.kt | 28 ++++- .../nova/common/utils/graph/GraphBuilder.kt | 7 ++ .../send/amount/SelectSendViewModel.kt | 3 +- .../domain/model/SwapQuote.kt | 6 - .../domain/swap/SwapService.kt | 4 +- .../data/primitive/model/QuotableEdge.kt | 10 +- .../conversion/types/hydra/sources/Wegiths.kt | 25 ++++ .../omnipool/RealOmniPoolQuotingSource.kt | 5 + .../stableswap/RealStableSwapQuotingSource.kt | 10 +- .../sources/xyk/RealXYKSwapQuotingSource.kt | 4 + .../domain/paths/RealPathQuoter.kt | 25 +++- .../data/assetExchange/AssetExchange.kt | 14 ++- .../AssetConversionExchange.kt | 13 +- .../compound/CompoundAssetExchange.kt | 32 +++++ .../CrossChainTransferAssetExchange.kt | 117 ++++++++++++++++++ .../assetExchange/hydraDx/HydraDxExchange.kt | 9 +- .../feature_swap_impl/di/SwapFeatureModule.kt | 8 +- .../CrossChainTransferExchangeModule.kt | 24 ++++ .../domain/interactor/SwapInteractor.kt | 6 +- .../domain/swap/RealSwapService.kt | 82 ++++++++---- .../SwapSlippageRangeValidation.kt | 2 +- .../main/SwapMainSettingsViewModel.kt | 21 ++-- .../CrossChainTransfersConfigurationExt.kt | 17 +++ .../interfaces/CrossChainTransfersUseCase.kt | 8 +- .../domain/RealCrossChainTransfersUseCase.kt | 13 +- 26 files changed, 411 insertions(+), 86 deletions(-) create mode 100644 feature-swap-core/src/main/java/io/novafoundation/nova/feature_swap_core/data/assetExchange/conversion/types/hydra/sources/Wegiths.kt create mode 100644 feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/compound/CompoundAssetExchange.kt create mode 100644 feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/crossChain/CrossChainTransferAssetExchange.kt create mode 100644 feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/di/exchanges/CrossChainTransferExchangeModule.kt diff --git a/common/src/main/java/io/novafoundation/nova/common/utils/FlowExt.kt b/common/src/main/java/io/novafoundation/nova/common/utils/FlowExt.kt index 0d14360116..70c795f5ad 100644 --- a/common/src/main/java/io/novafoundation/nova/common/utils/FlowExt.kt +++ b/common/src/main/java/io/novafoundation/nova/common/utils/FlowExt.kt @@ -259,6 +259,10 @@ fun Flow>.diffed(): Flow> { } } +suspend inline fun Flow.awaitTrue() { + first { it } +} + fun Flow.zipWithPrevious(): Flow> = flow { var current: T? = null diff --git a/common/src/main/java/io/novafoundation/nova/common/utils/graph/Graph.kt b/common/src/main/java/io/novafoundation/nova/common/utils/graph/Graph.kt index 56fd04482f..fd76e36359 100644 --- a/common/src/main/java/io/novafoundation/nova/common/utils/graph/Graph.kt +++ b/common/src/main/java/io/novafoundation/nova/common/utils/graph/Graph.kt @@ -1,5 +1,6 @@ package io.novafoundation.nova.common.utils.graph +import android.util.Log import io.novafoundation.nova.common.utils.MultiMapList import java.util.PriorityQueue @@ -10,6 +11,12 @@ interface Edge { val to: N } +interface WeightedEdge : Edge { + + // Smaller the better + val weight: Int +} + class Graph>( val adjacencyList: MultiMapList ) { @@ -41,7 +48,9 @@ fun > Graph.hasOutcomingDirections(origin: N): Boolean { } -fun > Graph.findDijkstraPathsBetween(from: N, to: N, limit: Int): List> { +fun > Graph.findDijkstraPathsBetween( + from: N, to: N, limit: Int +): List> { data class QueueElement(val currentPath: Path, val score: Int) : Comparable { override fun compareTo(other: QueueElement): Int { @@ -63,7 +72,7 @@ fun > Graph.findDijkstraPathsBetween(from: N, to: N, limit: adjacencyList.keys.forEach { count[it] = 0 } val heap = PriorityQueue() - heap.add(QueueElement(currentPath = emptyList(), score = 0)) + heap.add(QueueElement(currentPath = emptyList(), score = 0)) while (heap.isNotEmpty() && paths.size < limit) { val minimumQueueElement = heap.poll()!! @@ -81,10 +90,17 @@ fun > Graph.findDijkstraPathsBetween(from: N, to: N, limit: adjacencyList.getValue(lastNode).forEach { edge -> if (edge.to in minimumQueueElement) return@forEach - val newElement = QueueElement( - currentPath = minimumQueueElement.currentPath + edge, - score = minimumQueueElement.score + 1 - ) + val newElement: QueueElement + + try { + newElement = QueueElement( + currentPath = minimumQueueElement.currentPath + edge, + score = minimumQueueElement.score + edge.weight + ) + } catch (e: AbstractMethodError) { + Log.e("Swaps", "Asbract method in ${edge::class.java}") + throw e + } heap.add(newElement) } diff --git a/common/src/main/java/io/novafoundation/nova/common/utils/graph/GraphBuilder.kt b/common/src/main/java/io/novafoundation/nova/common/utils/graph/GraphBuilder.kt index 979dbbe1f3..11ff791f9f 100644 --- a/common/src/main/java/io/novafoundation/nova/common/utils/graph/GraphBuilder.kt +++ b/common/src/main/java/io/novafoundation/nova/common/utils/graph/GraphBuilder.kt @@ -8,17 +8,24 @@ class GraphBuilder> { fun addEdge(to: E) { val fromEdges = adjacencyList.getOrPut(to.from) { mutableListOf() } + initializeVertex(to.to) fromEdges.add(to) } fun addEdges(from: N, to: List) { val fromEdges = adjacencyList.getOrPut(from) { mutableListOf() } + to.onEach { initializeVertex(it.to) } + fromEdges.addAll(to) } fun build(): Graph { return Graph(adjacencyList) } + + private fun initializeVertex(v: N) { + adjacencyList.computeIfAbsent(v) { mutableListOf() } + } } fun > GraphBuilder.addEdges(map: MultiMapList) { 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 f9f6c3713f..8161f456fd 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 @@ -55,7 +55,6 @@ import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.create import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.createGenericChangeableFee import io.novafoundation.nova.feature_wallet_api.presentation.model.AssetPayload import io.novafoundation.nova.feature_wallet_api.presentation.model.mapAmountToAmountModel -import io.novafoundation.nova.runtime.ext.commissionAsset import io.novafoundation.nova.runtime.ext.isEnabled import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry import io.novafoundation.nova.runtime.multiNetwork.ChainWithAsset @@ -543,7 +542,7 @@ class SelectSendViewModel( private fun availableOutDirections(): Flow> { return originChainAsset.flatMapLatest { - crossChainTransfersUseCase.outcomingCrossChainDirections(it) + crossChainTransfersUseCase.outcomingCrossChainDirectionsFlow(it) .filterList { it.chain.isEnabled } .mapList { incomingDirection -> CrossChainDirection( diff --git a/feature-swap-api/src/main/java/io/novafoundation/nova/feature_swap_api/domain/model/SwapQuote.kt b/feature-swap-api/src/main/java/io/novafoundation/nova/feature_swap_api/domain/model/SwapQuote.kt index ade4b47f50..cf808792d4 100644 --- a/feature-swap-api/src/main/java/io/novafoundation/nova/feature_swap_api/domain/model/SwapQuote.kt +++ b/feature-swap-api/src/main/java/io/novafoundation/nova/feature_swap_api/domain/model/SwapQuote.kt @@ -30,12 +30,6 @@ data class SwapQuote( val planksOut: Balance get() = amountOut.amount - - init { - require(assetIn.chainId == assetOut.chainId) { - "Cross-chain swaps are not yet implemented" - } - } } diff --git a/feature-swap-api/src/main/java/io/novafoundation/nova/feature_swap_api/domain/swap/SwapService.kt b/feature-swap-api/src/main/java/io/novafoundation/nova/feature_swap_api/domain/swap/SwapService.kt index 4f59e9ce70..c1547a8732 100644 --- a/feature-swap-api/src/main/java/io/novafoundation/nova/feature_swap_api/domain/swap/SwapService.kt +++ b/feature-swap-api/src/main/java/io/novafoundation/nova/feature_swap_api/domain/swap/SwapService.kt @@ -35,7 +35,7 @@ interface SwapService { suspend fun swap(args: SwapExecuteArgs): Result - suspend fun slippageConfig(chainId: ChainId): SlippageConfig? + suspend fun defaultSlippageConfig(chainId: ChainId): SlippageConfig - fun runSubscriptions(chainIn: Chain, metaAccount: MetaAccount): Flow + fun runSubscriptions(metaAccount: MetaAccount): Flow } diff --git a/feature-swap-core/api/src/main/java/io/novafoundation/nova/feature_swap_core_api/data/primitive/model/QuotableEdge.kt b/feature-swap-core/api/src/main/java/io/novafoundation/nova/feature_swap_core_api/data/primitive/model/QuotableEdge.kt index dd4b0f10a6..0e187be3ae 100644 --- a/feature-swap-core/api/src/main/java/io/novafoundation/nova/feature_swap_core_api/data/primitive/model/QuotableEdge.kt +++ b/feature-swap-core/api/src/main/java/io/novafoundation/nova/feature_swap_core_api/data/primitive/model/QuotableEdge.kt @@ -1,10 +1,16 @@ package io.novafoundation.nova.feature_swap_core_api.data.primitive.model -import io.novafoundation.nova.common.utils.graph.Edge +import io.novafoundation.nova.common.utils.graph.WeightedEdge import io.novafoundation.nova.runtime.multiNetwork.chain.model.FullChainAssetId import java.math.BigInteger -interface QuotableEdge : Edge { +interface QuotableEdge : WeightedEdge { + + companion object { + + // Allow [0..10] precision for smaller weights + const val DEFAULT_SEGMENT_WEIGHT = 10 + } suspend fun quote( amount: BigInteger, diff --git a/feature-swap-core/src/main/java/io/novafoundation/nova/feature_swap_core/data/assetExchange/conversion/types/hydra/sources/Wegiths.kt b/feature-swap-core/src/main/java/io/novafoundation/nova/feature_swap_core/data/assetExchange/conversion/types/hydra/sources/Wegiths.kt new file mode 100644 index 0000000000..7ba9c553b0 --- /dev/null +++ b/feature-swap-core/src/main/java/io/novafoundation/nova/feature_swap_core/data/assetExchange/conversion/types/hydra/sources/Wegiths.kt @@ -0,0 +1,25 @@ +package io.novafoundation.nova.feature_swap_core.data.assetExchange.conversion.types.hydra.sources + +import io.novafoundation.nova.feature_swap_core_api.data.primitive.model.QuotableEdge + +object Weights { + + object Hydra { + + const val OMNIPOOL = QuotableEdge.DEFAULT_SEGMENT_WEIGHT + + const val STABLESWAP = QuotableEdge.DEFAULT_SEGMENT_WEIGHT - 1 + + const val XYK = QuotableEdge.DEFAULT_SEGMENT_WEIGHT + 1 + } + + object AssetConversion { + + const val SWAP = QuotableEdge.DEFAULT_SEGMENT_WEIGHT + 2 + } + + object CrossChainTransfer { + + const val TRANSFER = QuotableEdge.DEFAULT_SEGMENT_WEIGHT + } +} diff --git a/feature-swap-core/src/main/java/io/novafoundation/nova/feature_swap_core/data/assetExchange/conversion/types/hydra/sources/omnipool/RealOmniPoolQuotingSource.kt b/feature-swap-core/src/main/java/io/novafoundation/nova/feature_swap_core/data/assetExchange/conversion/types/hydra/sources/omnipool/RealOmniPoolQuotingSource.kt index d58250a118..12a1a3b3dc 100644 --- a/feature-swap-core/src/main/java/io/novafoundation/nova/feature_swap_core/data/assetExchange/conversion/types/hydra/sources/omnipool/RealOmniPoolQuotingSource.kt +++ b/feature-swap-core/src/main/java/io/novafoundation/nova/feature_swap_core/data/assetExchange/conversion/types/hydra/sources/omnipool/RealOmniPoolQuotingSource.kt @@ -7,6 +7,7 @@ import io.novafoundation.nova.common.utils.orZero import io.novafoundation.nova.common.utils.singleReplaySharedFlow import io.novafoundation.nova.common.utils.toMultiSubscription import io.novafoundation.nova.core.updater.SharedRequestsBuilder +import io.novafoundation.nova.feature_swap_core.data.assetExchange.conversion.types.hydra.sources.Weights import io.novafoundation.nova.feature_swap_core.data.assetExchange.conversion.types.hydra.sources.omnipool.model.DynamicFee import io.novafoundation.nova.feature_swap_core.data.assetExchange.conversion.types.hydra.sources.omnipool.model.OmniPool import io.novafoundation.nova.feature_swap_core.data.assetExchange.conversion.types.hydra.sources.omnipool.model.OmniPoolFees @@ -203,12 +204,16 @@ private class RealOmniPoolQuotingSource( override val to: FullChainAssetId = toAsset.second.fullId + override val weight: Int + get() = Weights.Hydra.OMNIPOOL + override suspend fun quote(amount: BigInteger, direction: SwapDirection): BigInteger { val omniPool = omniPoolFlow.first() return omniPool.quote(fromAsset.first, toAsset.first, amount, direction) ?: throw SwapQuoteException.NotEnoughLiquidity } + } } diff --git a/feature-swap-core/src/main/java/io/novafoundation/nova/feature_swap_core/data/assetExchange/conversion/types/hydra/sources/stableswap/RealStableSwapQuotingSource.kt b/feature-swap-core/src/main/java/io/novafoundation/nova/feature_swap_core/data/assetExchange/conversion/types/hydra/sources/stableswap/RealStableSwapQuotingSource.kt index a95f5e3e1c..bda2287406 100644 --- a/feature-swap-core/src/main/java/io/novafoundation/nova/feature_swap_core/data/assetExchange/conversion/types/hydra/sources/stableswap/RealStableSwapQuotingSource.kt +++ b/feature-swap-core/src/main/java/io/novafoundation/nova/feature_swap_core/data/assetExchange/conversion/types/hydra/sources/stableswap/RealStableSwapQuotingSource.kt @@ -10,19 +10,20 @@ import io.novafoundation.nova.common.utils.orZero import io.novafoundation.nova.common.utils.singleReplaySharedFlow import io.novafoundation.nova.common.utils.toMultiSubscription import io.novafoundation.nova.core.updater.SharedRequestsBuilder +import io.novafoundation.nova.feature_swap_core.data.assetExchange.conversion.types.hydra.sources.Weights import io.novafoundation.nova.feature_swap_core.data.assetExchange.conversion.types.hydra.sources.omnipool.model.RemoteAndLocalId import io.novafoundation.nova.feature_swap_core.data.assetExchange.conversion.types.hydra.sources.omnipool.model.RemoteAndLocalIdOptional import io.novafoundation.nova.feature_swap_core.data.assetExchange.conversion.types.hydra.sources.omnipool.model.flatten import io.novafoundation.nova.feature_swap_core.data.assetExchange.conversion.types.hydra.sources.omnipool.omniPoolAccountId +import io.novafoundation.nova.feature_swap_core.data.assetExchange.conversion.types.hydra.sources.stableswap.model.StablePool +import io.novafoundation.nova.feature_swap_core.data.assetExchange.conversion.types.hydra.sources.stableswap.model.StablePoolAsset import io.novafoundation.nova.feature_swap_core.data.assetExchange.conversion.types.hydra.sources.stableswap.model.StableSwapPoolInfo +import io.novafoundation.nova.feature_swap_core.data.assetExchange.conversion.types.hydra.sources.stableswap.model.quote import io.novafoundation.nova.feature_swap_core_api.data.network.HydraDxAssetId import io.novafoundation.nova.feature_swap_core_api.data.network.HydraDxAssetIdConverter import io.novafoundation.nova.feature_swap_core_api.data.primitive.errors.SwapQuoteException import io.novafoundation.nova.feature_swap_core_api.data.primitive.model.SwapDirection import io.novafoundation.nova.feature_swap_core_api.data.types.hydra.HydraDxQuotingSource -import io.novafoundation.nova.feature_swap_core.data.assetExchange.conversion.types.hydra.sources.stableswap.model.StablePool -import io.novafoundation.nova.feature_swap_core.data.assetExchange.conversion.types.hydra.sources.stableswap.model.StablePoolAsset -import io.novafoundation.nova.feature_swap_core.data.assetExchange.conversion.types.hydra.sources.stableswap.model.quote import io.novafoundation.nova.runtime.ext.fullId import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain import io.novafoundation.nova.runtime.multiNetwork.chain.model.FullChainAssetId @@ -285,6 +286,9 @@ private class RealStableSwapQuotingSource( override val to: FullChainAssetId = toAsset.second + override val weight: Int + get() = Weights.Hydra.STABLESWAP + override suspend fun quote(amount: BigInteger, direction: SwapDirection): BigInteger { val allPools = stablePools.first() val relevantPool = allPools.first { it.sharedAsset.id == poolId } diff --git a/feature-swap-core/src/main/java/io/novafoundation/nova/feature_swap_core/data/assetExchange/conversion/types/hydra/sources/xyk/RealXYKSwapQuotingSource.kt b/feature-swap-core/src/main/java/io/novafoundation/nova/feature_swap_core/data/assetExchange/conversion/types/hydra/sources/xyk/RealXYKSwapQuotingSource.kt index addc2e77bb..936b02d221 100644 --- a/feature-swap-core/src/main/java/io/novafoundation/nova/feature_swap_core/data/assetExchange/conversion/types/hydra/sources/xyk/RealXYKSwapQuotingSource.kt +++ b/feature-swap-core/src/main/java/io/novafoundation/nova/feature_swap_core/data/assetExchange/conversion/types/hydra/sources/xyk/RealXYKSwapQuotingSource.kt @@ -5,6 +5,7 @@ import io.novafoundation.nova.common.utils.combine import io.novafoundation.nova.common.utils.singleReplaySharedFlow import io.novafoundation.nova.common.utils.xyk import io.novafoundation.nova.core.updater.SharedRequestsBuilder +import io.novafoundation.nova.feature_swap_core.data.assetExchange.conversion.types.hydra.sources.Weights import io.novafoundation.nova.feature_swap_core.data.assetExchange.conversion.types.hydra.sources.omnipool.model.RemoteAndLocalId import io.novafoundation.nova.feature_swap_core.data.assetExchange.conversion.types.hydra.sources.omnipool.model.localId import io.novafoundation.nova.feature_swap_core.data.assetExchange.conversion.types.hydra.sources.subscribeToTransferableBalance @@ -174,6 +175,9 @@ private class RealXYKSwapQuotingSource( override val to: FullChainAssetId = toAsset.second + override val weight: Int + get() = Weights.Hydra.XYK + override suspend fun quote(amount: BigInteger, direction: SwapDirection): BigInteger { val allPools = xykPools.first() diff --git a/feature-swap-core/src/main/java/io/novafoundation/nova/feature_swap_core/domain/paths/RealPathQuoter.kt b/feature-swap-core/src/main/java/io/novafoundation/nova/feature_swap_core/domain/paths/RealPathQuoter.kt index 77931a958e..a2c9518935 100644 --- a/feature-swap-core/src/main/java/io/novafoundation/nova/feature_swap_core/domain/paths/RealPathQuoter.kt +++ b/feature-swap-core/src/main/java/io/novafoundation/nova/feature_swap_core/domain/paths/RealPathQuoter.kt @@ -5,6 +5,7 @@ import io.novafoundation.nova.common.data.memory.ComputationalCache import io.novafoundation.nova.common.utils.graph.Graph import io.novafoundation.nova.common.utils.graph.Path import io.novafoundation.nova.common.utils.graph.findDijkstraPathsBetween +import io.novafoundation.nova.common.utils.mapAsync import io.novafoundation.nova.feature_swap_core_api.data.paths.PathQuoter import io.novafoundation.nova.feature_swap_core_api.data.paths.model.BestPathQuote import io.novafoundation.nova.feature_swap_core_api.data.paths.model.QuotedEdge @@ -61,8 +62,10 @@ private class RealPathQuoter( paths } - val quotedPaths = paths.mapNotNull { path -> quotePath(path, amount, swapDirection) } - if (paths.isEmpty()) { + val quotedPaths = paths.mapAsync { path -> quotePath(path, amount, swapDirection) } + .filterNotNull() + + if (quotedPaths.isEmpty()) { throw SwapQuoteException.NotEnoughLiquidity } @@ -106,12 +109,19 @@ private class RealPathQuoter( val initial = mutableListOf>() to amount path.foldRight(initial) { segment, (quotedPath, currentAmount) -> + Log.d("Swaps", "Started quoting ${segment::class.simpleName}") + val segmentQuote = segment.quote(currentAmount, SwapDirection.SPECIFIED_OUT) + + Log.d("Swaps", "Finished quoting ${segment::class.simpleName}") + quotedPath.add(0, QuotedEdge(currentAmount, segmentQuote, segment)) quotedPath to segmentQuote }.first - }.getOrNull() + } + .onFailure { Log.w("Swaps", "Failed to quote path", it) } + .getOrNull() } private suspend fun quotePathSell(path: Path, amount: BigInteger): Path>? { @@ -119,12 +129,19 @@ private class RealPathQuoter( val initial = mutableListOf>() to amount path.fold(initial) { (quotedPath, currentAmount), segment -> + Log.d("Swaps", "Started quoting ${segment::class.simpleName}") + val segmentQuote = segment.quote(currentAmount, SwapDirection.SPECIFIED_IN) + + Log.d("Swaps", "Finished quoting ${segment::class.simpleName}") + quotedPath.add(QuotedEdge(currentAmount, segmentQuote, segment)) quotedPath to segmentQuote }.first - }.getOrNull() + } + .onFailure { Log.w("Swaps", "Failed to quote path", it) } + .getOrNull() } } diff --git a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/AssetExchange.kt b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/AssetExchange.kt index b510795ba8..a0e7213bf0 100644 --- a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/AssetExchange.kt +++ b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/AssetExchange.kt @@ -3,7 +3,6 @@ package io.novafoundation.nova.feature_swap_impl.data.assetExchange import io.novafoundation.nova.feature_account_api.data.fee.capability.CustomFeeCapability import io.novafoundation.nova.feature_account_api.domain.model.MetaAccount import io.novafoundation.nova.feature_swap_api.domain.model.ReQuoteTrigger -import io.novafoundation.nova.feature_swap_api.domain.model.SlippageConfig import io.novafoundation.nova.feature_swap_api.domain.model.SwapGraphEdge import io.novafoundation.nova.feature_swap_core_api.data.primitive.model.SwapDirection import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.types.Balance @@ -13,7 +12,7 @@ import kotlinx.coroutines.flow.Flow interface AssetExchange : CustomFeeCapability { - interface Factory { + interface SingleChainFactory { suspend fun create( chain: Chain, @@ -22,6 +21,13 @@ interface AssetExchange : CustomFeeCapability { ): AssetExchange? } + interface MultiChainFactory { + + suspend fun create( + parentQuoter: ParentQuoter, + coroutineScope: CoroutineScope + ): AssetExchange? + } interface ParentQuoter { suspend fun quote(quoteArgs: ParentQuoterArgs): Balance @@ -31,9 +37,7 @@ interface AssetExchange : CustomFeeCapability { suspend fun availableDirectSwapConnections(): List - suspend fun slippageConfig(): SlippageConfig - - fun runSubscriptions(chain: Chain, metaAccount: MetaAccount): Flow + fun runSubscriptions(metaAccount: MetaAccount): Flow } data class ParentQuoterArgs( diff --git a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/assetConversion/AssetConversionExchange.kt b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/assetConversion/AssetConversionExchange.kt index e62a6eb70d..0c0157b4cf 100644 --- a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/assetConversion/AssetConversionExchange.kt +++ b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/assetConversion/AssetConversionExchange.kt @@ -15,10 +15,10 @@ import io.novafoundation.nova.feature_swap_api.domain.model.AtomicSwapOperation import io.novafoundation.nova.feature_swap_api.domain.model.AtomicSwapOperationArgs import io.novafoundation.nova.feature_swap_api.domain.model.AtomicSwapOperationFee import io.novafoundation.nova.feature_swap_api.domain.model.ReQuoteTrigger -import io.novafoundation.nova.feature_swap_api.domain.model.SlippageConfig import io.novafoundation.nova.feature_swap_api.domain.model.SwapExecutionCorrection import io.novafoundation.nova.feature_swap_api.domain.model.SwapGraphEdge import io.novafoundation.nova.feature_swap_api.domain.model.SwapLimit +import io.novafoundation.nova.feature_swap_core.data.assetExchange.conversion.types.hydra.sources.Weights import io.novafoundation.nova.feature_swap_core_api.data.primitive.errors.SwapQuoteException import io.novafoundation.nova.feature_swap_core_api.data.primitive.model.SwapDirection import io.novafoundation.nova.feature_swap_impl.data.assetExchange.AssetExchange @@ -56,7 +56,7 @@ class AssetConversionExchangeFactory( private val runtimeCallsApi: MultiChainRuntimeCallsApi, private val extrinsicService: ExtrinsicService, private val chainStateRepository: ChainStateRepository, -) : AssetExchange.Factory { +) : AssetExchange.SingleChainFactory { override suspend fun create( chain: Chain, @@ -102,11 +102,7 @@ private class AssetConversionExchange( } } - override suspend fun slippageConfig(): SlippageConfig { - return SlippageConfig.default() - } - - override fun runSubscriptions(chain: Chain, metaAccount: MetaAccount): Flow { + override fun runSubscriptions(metaAccount: MetaAccount): Flow { return chainStateRepository.currentBlockNumberFlow(chain.id) .drop(1) // skip immediate value from the cache to not perform double-quote on chain change .map { ReQuoteTrigger } @@ -191,6 +187,9 @@ private class AssetConversionExchange( amount = amount ) ?: throw SwapQuoteException.NotEnoughLiquidity } + + override val weight: Int + get() = Weights.AssetConversion.SWAP } inner class AssetConversionOperation( diff --git a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/compound/CompoundAssetExchange.kt b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/compound/CompoundAssetExchange.kt new file mode 100644 index 0000000000..35209d4552 --- /dev/null +++ b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/compound/CompoundAssetExchange.kt @@ -0,0 +1,32 @@ +package io.novafoundation.nova.feature_swap_impl.data.assetExchange.compound + +import io.novafoundation.nova.common.utils.forEachAsync +import io.novafoundation.nova.common.utils.mergeIfMultiple +import io.novafoundation.nova.feature_account_api.domain.model.MetaAccount +import io.novafoundation.nova.feature_swap_api.domain.model.ReQuoteTrigger +import io.novafoundation.nova.feature_swap_api.domain.model.SwapGraphEdge +import io.novafoundation.nova.feature_swap_impl.data.assetExchange.AssetExchange +import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain +import kotlinx.coroutines.flow.Flow + +class CompoundAssetExchange( + private val delegates: List +): AssetExchange { + + override suspend fun sync() { + delegates.forEachAsync { it.sync() } + } + + override suspend fun availableDirectSwapConnections(): List { + return delegates.flatMap { it.availableDirectSwapConnections() } + } + + override fun runSubscriptions(metaAccount: MetaAccount): Flow { + return delegates.map { it.runSubscriptions(metaAccount) } + .mergeIfMultiple() + } + + override suspend fun canPayFeeInNonUtilityToken(chainAsset: Chain.Asset): Boolean { + return delegates.all { it.canPayFeeInNonUtilityToken(chainAsset) } + } +} diff --git a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/crossChain/CrossChainTransferAssetExchange.kt b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/crossChain/CrossChainTransferAssetExchange.kt new file mode 100644 index 0000000000..abdf7b10ec --- /dev/null +++ b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/crossChain/CrossChainTransferAssetExchange.kt @@ -0,0 +1,117 @@ +package io.novafoundation.nova.feature_swap_impl.data.assetExchange.crossChain + +import io.novafoundation.nova.common.utils.awaitTrue +import io.novafoundation.nova.common.utils.emptySubstrateAccountId +import io.novafoundation.nova.common.utils.graph.Edge +import io.novafoundation.nova.feature_account_api.data.extrinsic.SubmissionOrigin +import io.novafoundation.nova.feature_account_api.data.model.SubstrateFee +import io.novafoundation.nova.feature_account_api.domain.model.MetaAccount +import io.novafoundation.nova.feature_swap_api.domain.model.AtomicSwapOperation +import io.novafoundation.nova.feature_swap_api.domain.model.AtomicSwapOperationArgs +import io.novafoundation.nova.feature_swap_api.domain.model.AtomicSwapOperationFee +import io.novafoundation.nova.feature_swap_api.domain.model.ReQuoteTrigger +import io.novafoundation.nova.feature_swap_api.domain.model.SwapExecutionCorrection +import io.novafoundation.nova.feature_swap_api.domain.model.SwapGraphEdge +import io.novafoundation.nova.feature_swap_core.data.assetExchange.conversion.types.hydra.sources.Weights +import io.novafoundation.nova.feature_swap_core_api.data.primitive.model.SwapDirection +import io.novafoundation.nova.feature_swap_impl.data.assetExchange.AssetExchange +import io.novafoundation.nova.feature_wallet_api.domain.interfaces.CrossChainTransfersUseCase +import io.novafoundation.nova.runtime.ext.utilityAsset +import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry +import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain +import io.novafoundation.nova.runtime.multiNetwork.chain.model.FullChainAssetId +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.emptyFlow +import java.math.BigInteger + +class CrossChainTransferAssetExchangeFactory( + private val crossChainTransfersUseCase: CrossChainTransfersUseCase, + private val chainRegistry: ChainRegistry +) : AssetExchange.MultiChainFactory { + + override suspend fun create( + parentQuoter: AssetExchange.ParentQuoter, + coroutineScope: CoroutineScope + ): AssetExchange { + + return CrossChainTransferAssetExchange( + crossChainTransfersUseCase = crossChainTransfersUseCase, + chainRegistry = chainRegistry + ) + } +} + +class CrossChainTransferAssetExchange( + private val crossChainTransfersUseCase: CrossChainTransfersUseCase, + private val chainRegistry: ChainRegistry +) : AssetExchange { + + private val synced = MutableStateFlow(false) + + override suspend fun sync() { + crossChainTransfersUseCase.syncCrossChainConfig() + synced.value = true + } + + override suspend fun availableDirectSwapConnections(): List { + synced.awaitTrue() + + return crossChainTransfersUseCase.allDirections().map(::CrossChainTransferEdge) + } + + override fun runSubscriptions(metaAccount: MetaAccount): Flow { + return emptyFlow() + } + + override suspend fun canPayFeeInNonUtilityToken(chainAsset: Chain.Asset): Boolean { + return false + } + + inner class CrossChainTransferEdge( + val delegate: Edge + ) : SwapGraphEdge, Edge by delegate { + + override val weight: Int + get() = Weights.CrossChainTransfer.TRANSFER + + override suspend fun beginOperation(args: AtomicSwapOperationArgs): AtomicSwapOperation { + return CrossChainTransferOperation(args, this) + } + + override suspend fun appendToOperation(currentTransaction: AtomicSwapOperation, args: AtomicSwapOperationArgs): AtomicSwapOperation? { + return null + } + + override suspend fun debugLabel(): String { + return "Cross-chain Transfer" + } + + override suspend fun quote(amount: BigInteger, direction: SwapDirection): BigInteger { + // TODO include delivery & execution fees + return amount + } + } + + inner class CrossChainTransferOperation( + private val transactionArgs: AtomicSwapOperationArgs, + private val edge: Edge + ) : AtomicSwapOperation { + + override suspend fun estimateFee(): AtomicSwapOperationFee { + val chain = chainRegistry.getChain(edge.from.chainId) + + // TODO + return SubstrateFee( + amount = BigInteger.ZERO, + submissionOrigin = SubmissionOrigin.singleOrigin(emptySubstrateAccountId()), + asset = chain.utilityAsset + ) + } + + override suspend fun submit(previousStepCorrection: SwapExecutionCorrection?): Result { + return Result.failure(UnsupportedOperationException("TODO")) + } + } +} diff --git a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/hydraDx/HydraDxExchange.kt b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/hydraDx/HydraDxExchange.kt index 499326208f..50f65b5786 100644 --- a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/hydraDx/HydraDxExchange.kt +++ b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/hydraDx/HydraDxExchange.kt @@ -17,7 +17,6 @@ import io.novafoundation.nova.feature_swap_api.domain.model.AtomicSwapOperation import io.novafoundation.nova.feature_swap_api.domain.model.AtomicSwapOperationArgs import io.novafoundation.nova.feature_swap_api.domain.model.AtomicSwapOperationFee import io.novafoundation.nova.feature_swap_api.domain.model.ReQuoteTrigger -import io.novafoundation.nova.feature_swap_api.domain.model.SlippageConfig import io.novafoundation.nova.feature_swap_api.domain.model.SwapExecutionCorrection import io.novafoundation.nova.feature_swap_api.domain.model.SwapGraphEdge import io.novafoundation.nova.feature_swap_api.domain.model.SwapLimit @@ -68,7 +67,7 @@ class HydraDxExchangeFactory( private val swapSourceFactories: Iterable>, private val quotingFactory: HydraDxQuoting.Factory, private val assetSourceRegistry: AssetSourceRegistry, -) : AssetExchange.Factory { +) : AssetExchange.SingleChainFactory { override suspend fun create(chain: Chain, parentQuoter: AssetExchange.ParentQuoter, coroutineScope: CoroutineScope): AssetExchange { return HydraDxExchange( @@ -119,11 +118,7 @@ private class HydraDxExchange( } } - override suspend fun slippageConfig(): SlippageConfig { - return SlippageConfig.default() - } - - override fun runSubscriptions(chain: Chain, metaAccount: MetaAccount): Flow { + override fun runSubscriptions(metaAccount: MetaAccount): Flow { return withFlowScope { scope -> val subscriptionBuilder = storageSharedRequestsBuilderFactory.create(chain.id) val userAccountId = metaAccount.requireAccountIdIn(chain) diff --git a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/di/SwapFeatureModule.kt b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/di/SwapFeatureModule.kt index 2fa1a3606e..28e52ea4ea 100644 --- a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/di/SwapFeatureModule.kt +++ b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/di/SwapFeatureModule.kt @@ -16,11 +16,13 @@ import io.novafoundation.nova.feature_swap_api.presentation.formatters.SwapRateF import io.novafoundation.nova.feature_swap_api.presentation.state.SwapSettingsStateProvider import io.novafoundation.nova.feature_swap_core_api.data.paths.PathQuoter import io.novafoundation.nova.feature_swap_impl.data.assetExchange.assetConversion.AssetConversionExchangeFactory +import io.novafoundation.nova.feature_swap_impl.data.assetExchange.crossChain.CrossChainTransferAssetExchangeFactory import io.novafoundation.nova.feature_swap_impl.data.assetExchange.hydraDx.HydraDxExchangeFactory import io.novafoundation.nova.feature_swap_impl.data.network.blockhain.updaters.SwapUpdateSystemFactory import io.novafoundation.nova.feature_swap_impl.data.repository.RealSwapTransactionHistoryRepository import io.novafoundation.nova.feature_swap_impl.data.repository.SwapTransactionHistoryRepository import io.novafoundation.nova.feature_swap_impl.di.exchanges.AssetConversionExchangeModule +import io.novafoundation.nova.feature_swap_impl.di.exchanges.CrossChainTransferExchangeModule import io.novafoundation.nova.feature_swap_impl.di.exchanges.HydraDxExchangeModule import io.novafoundation.nova.feature_swap_impl.domain.interactor.RealSwapAvailabilityInteractor import io.novafoundation.nova.feature_swap_impl.domain.interactor.SwapInteractor @@ -40,7 +42,7 @@ import io.novafoundation.nova.feature_wallet_api.domain.updater.AccountInfoUpdat import io.novafoundation.nova.runtime.ethereum.StorageSharedRequestsBuilderFactory import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry -@Module(includes = [HydraDxExchangeModule::class, AssetConversionExchangeModule::class]) +@Module(includes = [HydraDxExchangeModule::class, AssetConversionExchangeModule::class, CrossChainTransferExchangeModule::class]) class SwapFeatureModule { @FeatureScope @@ -48,14 +50,16 @@ class SwapFeatureModule { fun provideSwapService( assetConversionFactory: AssetConversionExchangeFactory, hydraDxExchangeFactory: HydraDxExchangeFactory, + crossChainTransferAssetExchangeFactory: CrossChainTransferAssetExchangeFactory, computationalCache: ComputationalCache, chainRegistry: ChainRegistry, quoterFactory: PathQuoter.Factory, - customFeeCapabilityFacade: CustomFeeCapabilityFacade + customFeeCapabilityFacade: CustomFeeCapabilityFacade, ): SwapService { return RealSwapService( assetConversionFactory = assetConversionFactory, hydraDxExchangeFactory = hydraDxExchangeFactory, + crossChainTransferFactory = crossChainTransferAssetExchangeFactory, computationalCache = computationalCache, chainRegistry = chainRegistry, quoterFactory = quoterFactory, diff --git a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/di/exchanges/CrossChainTransferExchangeModule.kt b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/di/exchanges/CrossChainTransferExchangeModule.kt new file mode 100644 index 0000000000..4348bcf697 --- /dev/null +++ b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/di/exchanges/CrossChainTransferExchangeModule.kt @@ -0,0 +1,24 @@ +package io.novafoundation.nova.feature_swap_impl.di.exchanges + +import dagger.Module +import dagger.Provides +import io.novafoundation.nova.common.di.scope.FeatureScope +import io.novafoundation.nova.feature_swap_impl.data.assetExchange.crossChain.CrossChainTransferAssetExchangeFactory +import io.novafoundation.nova.feature_wallet_api.domain.interfaces.CrossChainTransfersUseCase +import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry + +@Module +class CrossChainTransferExchangeModule { + + @Provides + @FeatureScope + fun provideAssetConversionExchangeFactory( + crossChainTransfersUseCase: CrossChainTransfersUseCase, + chainRegistry: ChainRegistry + ): CrossChainTransferAssetExchangeFactory { + return CrossChainTransferAssetExchangeFactory( + crossChainTransfersUseCase = crossChainTransfersUseCase, + chainRegistry = chainRegistry + ) + } +} diff --git a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/domain/interactor/SwapInteractor.kt b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/domain/interactor/SwapInteractor.kt index 7d676791de..3521c0a41d 100644 --- a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/domain/interactor/SwapInteractor.kt +++ b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/domain/interactor/SwapInteractor.kt @@ -114,11 +114,11 @@ class SwapInteractor( } suspend fun slippageConfig(chainId: ChainId): SlippageConfig? { - return swapService.slippageConfig(chainId) + return swapService.defaultSlippageConfig(chainId) } - fun runSubscriptions(chainIn: Chain, metaAccount: MetaAccount): Flow { - return swapService.runSubscriptions(chainIn, metaAccount) + fun runSubscriptions(metaAccount: MetaAccount): Flow { + return swapService.runSubscriptions(metaAccount) } private fun buyAvailable(chainAssetFlow: Flow): Flow { diff --git a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/domain/swap/RealSwapService.kt b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/domain/swap/RealSwapService.kt index 08506e8cab..8bf32ed2b5 100644 --- a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/domain/swap/RealSwapService.kt +++ b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/domain/swap/RealSwapService.kt @@ -47,6 +47,8 @@ import io.novafoundation.nova.feature_swap_impl.BuildConfig import io.novafoundation.nova.feature_swap_impl.data.assetExchange.AssetExchange import io.novafoundation.nova.feature_swap_impl.data.assetExchange.ParentQuoterArgs import io.novafoundation.nova.feature_swap_impl.data.assetExchange.assetConversion.AssetConversionExchangeFactory +import io.novafoundation.nova.feature_swap_impl.data.assetExchange.compound.CompoundAssetExchange +import io.novafoundation.nova.feature_swap_impl.data.assetExchange.crossChain.CrossChainTransferAssetExchangeFactory import io.novafoundation.nova.feature_swap_impl.data.assetExchange.hydraDx.HydraDxExchangeFactory import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.types.Balance import io.novafoundation.nova.feature_wallet_api.domain.model.withAmount @@ -69,7 +71,6 @@ import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.runningFold import kotlinx.coroutines.withContext import java.math.BigDecimal -import kotlin.coroutines.coroutineContext import kotlin.time.Duration.Companion.milliseconds private const val ALL_DIRECTIONS_CACHE = "RealSwapService.ALL_DIRECTIONS" @@ -80,6 +81,7 @@ private const val QUOTER_CACHE = "RealSwapService.QUOTER" internal class RealSwapService( private val assetConversionFactory: AssetConversionExchangeFactory, private val hydraDxExchangeFactory: HydraDxExchangeFactory, + private val crossChainTransferFactory: CrossChainTransferAssetExchangeFactory, private val computationalCache: ComputationalCache, private val chainRegistry: ChainRegistry, private val quoterFactory: PathQuoter.Factory, @@ -90,15 +92,15 @@ internal class RealSwapService( override suspend fun canPayFeeInNonUtilityAsset(asset: Chain.Asset): Boolean = withContext(Dispatchers.Default) { val computationScope = CoroutineScope(coroutineContext) - val exchange = exchanges(computationScope).getValue(asset.chainId) + val exchange = exchangeRegistry(computationScope).getExchange(asset.chainId) customFeeCapabilityFacade.canPayFeeInNonUtilityToken(asset, exchange) } override suspend fun sync(coroutineScope: CoroutineScope) { Log.d("Swaps", "Syncing swap service") - exchanges(coroutineScope) - .values + exchangeRegistry(coroutineScope) + .allExchanges() .forEachAsync { it.sync() } } @@ -112,11 +114,13 @@ internal class RealSwapService( asset: Chain.Asset, computationScope: CoroutineScope ): Flow> { - return directionsGraph(computationScope).map { it.findAllPossibleDestinations(asset.fullId) } + return directionsGraph(computationScope).map { + it.findAllPossibleDestinations(asset.fullId) + } } override suspend fun hasAvailableSwapDirections(asset: Chain.Asset, computationScope: CoroutineScope): Flow { - return directionsGraph(computationScope).map { it.hasOutcomingDirections(asset.fullId) } + return directionsGraph(computationScope).map { it.hasOutcomingDirections(asset.fullId) } } override suspend fun quote( @@ -212,18 +216,17 @@ internal class RealSwapService( ) } - override suspend fun slippageConfig(chainId: ChainId): SlippageConfig? { - val computationScope = CoroutineScope(coroutineContext) - val exchanges = exchanges(computationScope) - return exchanges[chainId]?.slippageConfig() + override suspend fun defaultSlippageConfig(chainId: ChainId): SlippageConfig { + return SlippageConfig.default() } - override fun runSubscriptions(chainIn: Chain, metaAccount: MetaAccount): Flow { + override fun runSubscriptions( metaAccount: MetaAccount): Flow { return withFlowScope { scope -> - Log.d("Swaps", "Starting new subscriptions") + val exchangeRegistry = exchangeRegistry(scope) - val exchanges = exchanges(scope) - exchanges.getValue(chainIn.id).runSubscriptions(chainIn, metaAccount) + exchangeRegistry.allExchanges() + .map { it.runSubscriptions(metaAccount) } + .mergeIfMultiple() }.throttleLast(500.milliseconds) } @@ -265,14 +268,14 @@ internal class RealSwapService( private suspend fun directionsGraph(computationScope: CoroutineScope): Flow { return computationalCache.useSharedFlow(ALL_DIRECTIONS_CACHE, computationScope) { - val exchanges = exchanges(computationScope) + val exchangeRegistry = exchangeRegistry(computationScope) - val directionsByExchange = exchanges.map { (chainId, exchange) -> + val directionsByExchange = exchangeRegistry.allExchanges().map { exchange -> flowOf { exchange.availableDirectSwapConnections() } .catch { emit(emptyList()) - Log.e("RealSwapService", "Failed to fetch directions for exchange ${exchange::class} in chain $chainId", it) + Log.e("RealSwapService", "Failed to fetch directions for exchange ${exchange::class}", it) } } @@ -283,20 +286,29 @@ internal class RealSwapService( } } - private suspend fun exchanges(computationScope: CoroutineScope): Map { + private suspend fun exchangeRegistry(computationScope: CoroutineScope): ExchangeRegistry { return computationalCache.useCache(EXCHANGES_CACHE, computationScope) { - createExchanges(computationScope) + createExchangeRegistry(this) } } - private suspend fun createExchanges(coroutineScope: CoroutineScope): Map { + private suspend fun createExchangeRegistry(coroutineScope: CoroutineScope): ExchangeRegistry { + return ExchangeRegistry( + singleChainExchanges = createIndividualChainExchanges(coroutineScope), + multiChainExchanges = listOf( + crossChainTransferFactory.create(InnerParentQuoter(coroutineScope), coroutineScope) + ) + ) + } + + private suspend fun createIndividualChainExchanges(coroutineScope: CoroutineScope): Map { return chainRegistry.chainsById.first().mapValues { (_, chain) -> - createExchange(coroutineScope, chain) + createSingleExchange(coroutineScope, chain) } .filterNotNull() } - private suspend fun createExchange(computationScope: CoroutineScope, chain: Chain): AssetExchange? { + private suspend fun createSingleExchange(computationScope: CoroutineScope, chain: Chain): AssetExchange? { val factory = when { chain.swap.assetConversionSupported() -> assetConversionFactory chain.swap.hydraDxSupported() -> hydraDxExchangeFactory @@ -380,6 +392,32 @@ internal class RealSwapService( } } } + + private class ExchangeRegistry( + private val singleChainExchanges: Map, + private val multiChainExchanges: List, + ) { + + fun getExchange(chainId: ChainId): AssetExchange { + val relevantExchanges = buildList { + singleChainExchanges[chainId]?.let { add(it) } + addAll(multiChainExchanges) + } + + return when(relevantExchanges.size) { + 0 -> error("No exchanges found") + 1 -> relevantExchanges.single() + else -> CompoundAssetExchange(relevantExchanges) + } + } + + fun allExchanges(): List { + return buildList { + addAll(singleChainExchanges.values) + addAll(multiChainExchanges) + } + } + } } private typealias QuotedTrade = QuotedPath diff --git a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/domain/validation/validations/SwapSlippageRangeValidation.kt b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/domain/validation/validations/SwapSlippageRangeValidation.kt index 3c803970a1..1a3e53863c 100644 --- a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/domain/validation/validations/SwapSlippageRangeValidation.kt +++ b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/domain/validation/validations/SwapSlippageRangeValidation.kt @@ -14,7 +14,7 @@ class SwapSlippageRangeValidation( ) : SwapValidation { override suspend fun validate(value: SwapValidationPayload): ValidationStatus { - val slippageConfig = swapService.slippageConfig(value.detailedAssetIn.chain.id)!! + val slippageConfig = swapService.defaultSlippageConfig(value.detailedAssetIn.chain.id)!! if (value.slippage.value !in slippageConfig.minAvailableSlippage.value..slippageConfig.maxAvailableSlippage.value) { return InvalidSlippage(slippageConfig.minAvailableSlippage, slippageConfig.maxAvailableSlippage).validationError() diff --git a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/main/SwapMainSettingsViewModel.kt b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/main/SwapMainSettingsViewModel.kt index 7e48ff4015..f9cc52b312 100644 --- a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/main/SwapMainSettingsViewModel.kt +++ b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/main/SwapMainSettingsViewModel.kt @@ -14,6 +14,7 @@ import io.novafoundation.nova.common.utils.LOG_TAG import io.novafoundation.nova.common.utils.accumulate import io.novafoundation.nova.common.utils.combineToPair import io.novafoundation.nova.common.utils.event +import io.novafoundation.nova.common.utils.flowOfAll import io.novafoundation.nova.common.utils.formatting.CompoundNumberFormatter import io.novafoundation.nova.common.utils.formatting.DynamicPrecisionFormatter import io.novafoundation.nova.common.utils.formatting.FixedPrecisionFormatter @@ -627,20 +628,16 @@ class SwapMainSettingsViewModel( } private fun setupSubscriptionQuoting() { - swapSettings.mapNotNull { it.assetIn?.chainId } - .distinctUntilChanged() - .flatMapLatest { chainId -> - val chain = chainRegistry.getChain(chainId) + flowOfAll { + swapInteractor.runSubscriptions(selectedAccountUseCase.getSelectedMetaAccount()) + .catch { Log.e(this@SwapMainSettingsViewModel.LOG_TAG, "Failure during subscriptions run", it) } + }.onEach { + Log.d("Swap", "ReQuote triggered from subscription") - swapInteractor.runSubscriptions(chain, selectedAccountUseCase.getSelectedMetaAccount()) - .catch { Log.e(this@SwapMainSettingsViewModel.LOG_TAG, "Failure during subscriptions run", it) } - }.onEach { - Log.d("Swap", "ReQuote triggered from subscription") - - val currentSwapSettings = swapSettings.first() + val currentSwapSettings = swapSettings.first() - performQuote(currentSwapSettings, shouldShowLoading = false) - }.launchIn(viewModelScope) + performQuote(currentSwapSettings, shouldShowLoading = false) + }.launchIn(viewModelScope) } private suspend fun performQuote(swapSettings: SwapSettings, shouldShowLoading: Boolean) { 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 00086343c2..76adf4ce66 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 @@ -2,6 +2,8 @@ package io.novafoundation.nova.feature_wallet_api.domain.implementations 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.utils.graph.Edge +import io.novafoundation.nova.common.utils.graph.SimpleEdge import io.novafoundation.nova.common.utils.isAscending import io.novafoundation.nova.feature_wallet_api.domain.model.AssetLocationPath import io.novafoundation.nova.feature_wallet_api.domain.model.CrossChainFeeConfiguration @@ -80,6 +82,21 @@ fun CrossChainTransfersConfiguration.availableInDestinations(destination: Chain. } } +fun CrossChainTransfersConfiguration.availableInDestinations(): List> { + return chains.flatMap { (originChainId, chainTransfers) -> + chainTransfers.flatMap { originAssetTransfers -> + originAssetTransfers.xcmTransfers.mapNotNull { + if (it.type == XcmTransferType.UNKNOWN ) return@mapNotNull null + + val from = FullChainAssetId(originChainId, originAssetTransfers.assetId) + val to = FullChainAssetId(it.destination.chainId, it.destination.assetId) + + SimpleEdge(from, to) + } + } + } +} + fun ByteArray.accountIdToMultiLocation() = MultiLocation( parents = BigInteger.ZERO, interior = Junctions( diff --git a/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/domain/interfaces/CrossChainTransfersUseCase.kt b/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/domain/interfaces/CrossChainTransfersUseCase.kt index dc6b3f0f98..2f13a592d5 100644 --- a/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/domain/interfaces/CrossChainTransfersUseCase.kt +++ b/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/domain/interfaces/CrossChainTransfersUseCase.kt @@ -1,8 +1,10 @@ package io.novafoundation.nova.feature_wallet_api.domain.interfaces +import io.novafoundation.nova.common.utils.graph.Edge import io.novafoundation.nova.feature_wallet_api.domain.model.Asset import io.novafoundation.nova.runtime.multiNetwork.ChainWithAsset import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain +import io.novafoundation.nova.runtime.multiNetwork.chain.model.FullChainAssetId import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.map @@ -15,9 +17,13 @@ typealias OutcomingDirection = ChainWithAsset interface CrossChainTransfersUseCase { + suspend fun syncCrossChainConfig() + fun incomingCrossChainDirections(destination: Flow): Flow> - fun outcomingCrossChainDirections(origin: Chain.Asset): Flow> + fun outcomingCrossChainDirectionsFlow(origin: Chain.Asset): Flow> + + suspend fun allDirections(): List> } fun CrossChainTransfersUseCase.incomingCrossChainDirectionsAvailable(destination: Flow): Flow { diff --git a/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/domain/RealCrossChainTransfersUseCase.kt b/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/domain/RealCrossChainTransfersUseCase.kt index 532817806b..ef1bc43173 100644 --- a/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/domain/RealCrossChainTransfersUseCase.kt +++ b/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/domain/RealCrossChainTransfersUseCase.kt @@ -2,6 +2,7 @@ package io.novafoundation.nova.feature_wallet_impl.domain import io.novafoundation.nova.common.data.memory.ComputationalCache import io.novafoundation.nova.common.utils.combineToPair +import io.novafoundation.nova.common.utils.graph.Edge import io.novafoundation.nova.common.utils.isPositive import io.novafoundation.nova.common.utils.withFlowScope import io.novafoundation.nova.feature_account_api.domain.interfaces.AccountRepository @@ -16,6 +17,7 @@ import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry import io.novafoundation.nova.runtime.multiNetwork.ChainWithAsset import io.novafoundation.nova.runtime.multiNetwork.assets import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain +import io.novafoundation.nova.runtime.multiNetwork.chain.model.FullChainAssetId import io.novafoundation.nova.runtime.multiNetwork.chainsById import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.catch @@ -35,6 +37,10 @@ internal class RealCrossChainTransfersUseCase( private val computationalCache: ComputationalCache, ) : CrossChainTransfersUseCase { + override suspend fun syncCrossChainConfig() { + crossChainTransfersRepository.syncConfiguration() + } + override fun incomingCrossChainDirections(destination: Flow): Flow> { return withFlowScope { scope -> computationalCache.useSharedFlow(INCOMING_DIRECTIONS, scope) { @@ -59,7 +65,7 @@ internal class RealCrossChainTransfersUseCase( }.catch { emit(emptyList()) } } - override fun outcomingCrossChainDirections(origin: Chain.Asset): Flow> { + override fun outcomingCrossChainDirectionsFlow(origin: Chain.Asset): Flow> { return withFlowScope { scope -> scope.launch { crossChainTransfersRepository.syncConfiguration() } @@ -75,4 +81,9 @@ internal class RealCrossChainTransfersUseCase( } }.catch { emit(emptyList()) } } + + override suspend fun allDirections(): List> { + val config = crossChainTransfersRepository.getConfiguration() + return config.availableInDestinations() + } } From 4a9fc753f617d45b6297ca82081def86fe77255e Mon Sep 17 00:00:00 2001 From: Valentun Date: Wed, 9 Oct 2024 17:07:11 +0300 Subject: [PATCH 14/83] Multi-chain swap fees mechanism (domain) --- .../data/extrinsic/ExtrinsicService.kt | 14 +- .../data/extrinsic/ExtrinsicServiceExt.kt | 6 + .../data/fee/FeePaymentCurrency.kt | 13 +- .../CustomOrNativeFeePaymentProvider.kt | 21 ++ .../data/fee/types/NativeFeePayment.kt | 4 +- .../fee/types/hydra/HydrationFeeInjector.kt | 45 +++++ .../di/AccountFeatureApi.kt | 5 +- .../extrinsic/RealExtrinsicServiceFactory.kt | 35 +++- .../fee/chains/AssetHubFeePaymentProvider.kt | 29 ++- .../fee/chains/DefaultFeePaymentProvider.kt | 2 +- .../fee/chains/HydrationFeePaymentProvider.kt | 33 ++-- .../hydra/HydrationConversionFeePayment.kt | 31 +-- .../types/hydra/RealHydrationFeeInjector.kt | 121 ++++++++++++ .../di/modules/CustomFeeModule.kt | 14 +- .../domain/model/AtomicSwapOperation.kt | 4 +- .../domain/model/SwapQuote.kt | 3 + .../domain/model/SwapQuoteArgs.kt | 7 +- .../domain/paths/RealPathQuoter.kt | 10 - .../AssetConversionExchange.kt | 89 +++------ .../CrossChainTransferAssetExchange.kt | 22 +-- .../assetExchange/hydraDx/HydraDxExchange.kt | 181 +++++++++--------- .../di/SwapFeatureDependencies.kt | 5 +- .../AssetConversionExchangeModule.kt | 7 +- .../di/exchanges/HydraDxExchangeModule.kt | 9 +- .../domain/swap/RealSwapService.kt | 46 ++++- .../confirmation/SwapConfirmationViewModel.kt | 12 +- .../presentation/main/QuotingState.kt | 2 +- .../main/SwapMainSettingsViewModel.kt | 3 +- .../assets/transfers/BaseAssetTransfers.kt | 5 +- 29 files changed, 490 insertions(+), 288 deletions(-) create mode 100644 feature-account-api/src/main/java/io/novafoundation/nova/feature_account_api/data/fee/chains/CustomOrNativeFeePaymentProvider.kt rename {feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl => feature-account-api/src/main/java/io/novafoundation/nova/feature_account_api}/data/fee/types/NativeFeePayment.kt (84%) create mode 100644 feature-account-api/src/main/java/io/novafoundation/nova/feature_account_api/data/fee/types/hydra/HydrationFeeInjector.kt create mode 100644 feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/data/fee/types/hydra/RealHydrationFeeInjector.kt diff --git a/feature-account-api/src/main/java/io/novafoundation/nova/feature_account_api/data/extrinsic/ExtrinsicService.kt b/feature-account-api/src/main/java/io/novafoundation/nova/feature_account_api/data/extrinsic/ExtrinsicService.kt index 7475ff9c65..7a7a73cb31 100644 --- a/feature-account-api/src/main/java/io/novafoundation/nova/feature_account_api/data/extrinsic/ExtrinsicService.kt +++ b/feature-account-api/src/main/java/io/novafoundation/nova/feature_account_api/data/extrinsic/ExtrinsicService.kt @@ -3,11 +3,13 @@ package io.novafoundation.nova.feature_account_api.data.extrinsic import io.novafoundation.nova.common.data.network.runtime.model.FeeResponse import io.novafoundation.nova.common.utils.multiResult.RetriableMultiResult import io.novafoundation.nova.feature_account_api.data.ethereum.transaction.TransactionOrigin +import io.novafoundation.nova.feature_account_api.data.fee.FeePaymentCurrency +import io.novafoundation.nova.feature_account_api.data.fee.FeePaymentProvider +import io.novafoundation.nova.feature_account_api.data.fee.FeePaymentProviderRegistry import io.novafoundation.nova.feature_account_api.data.model.Fee import io.novafoundation.nova.runtime.extrinsic.ExtrinsicStatus import io.novafoundation.nova.runtime.extrinsic.multi.CallBuilder import io.novafoundation.nova.runtime.extrinsic.signer.FeeSigner -import io.novafoundation.nova.feature_account_api.data.fee.FeePaymentCurrency import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain import io.novasama.substrate_sdk_android.runtime.AccountId import io.novasama.substrate_sdk_android.runtime.extrinsic.BatchMode @@ -48,7 +50,7 @@ interface ExtrinsicService { interface Factory { - fun create(coroutineScope: CoroutineScope): ExtrinsicService + fun create(feeConfig: FeePaymentConfig): ExtrinsicService } class SubmissionOptions( @@ -56,6 +58,14 @@ interface ExtrinsicService { val batchMode: BatchMode = DEFAULT_BATCH_MODE, ) + class FeePaymentConfig( + val coroutineScope: CoroutineScope, + /** + * Specify to use it instead of default [FeePaymentProviderRegistry] to perform fee computations + */ + val customFeePaymentProvider: FeePaymentProvider? = null, + ) + suspend fun submitExtrinsic( chain: Chain, origin: TransactionOrigin, diff --git a/feature-account-api/src/main/java/io/novafoundation/nova/feature_account_api/data/extrinsic/ExtrinsicServiceExt.kt b/feature-account-api/src/main/java/io/novafoundation/nova/feature_account_api/data/extrinsic/ExtrinsicServiceExt.kt index 7cc932d323..887f7c6885 100644 --- a/feature-account-api/src/main/java/io/novafoundation/nova/feature_account_api/data/extrinsic/ExtrinsicServiceExt.kt +++ b/feature-account-api/src/main/java/io/novafoundation/nova/feature_account_api/data/extrinsic/ExtrinsicServiceExt.kt @@ -1,6 +1,8 @@ package io.novafoundation.nova.feature_account_api.data.extrinsic +import io.novafoundation.nova.feature_account_api.data.extrinsic.ExtrinsicService.FeePaymentConfig import io.novafoundation.nova.runtime.extrinsic.ExtrinsicStatus +import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.filterIsInstance import kotlinx.coroutines.flow.first @@ -8,3 +10,7 @@ import kotlinx.coroutines.flow.first suspend fun Result>.awaitInBlock(): Result = mapCatching { it.filterIsInstance().first() } + +fun ExtrinsicService.Factory.createDefault(coroutineScope: CoroutineScope): ExtrinsicService { + return create(FeePaymentConfig(coroutineScope)) +} diff --git a/feature-account-api/src/main/java/io/novafoundation/nova/feature_account_api/data/fee/FeePaymentCurrency.kt b/feature-account-api/src/main/java/io/novafoundation/nova/feature_account_api/data/fee/FeePaymentCurrency.kt index 06b42c11dd..cbbd741f86 100644 --- a/feature-account-api/src/main/java/io/novafoundation/nova/feature_account_api/data/fee/FeePaymentCurrency.kt +++ b/feature-account-api/src/main/java/io/novafoundation/nova/feature_account_api/data/fee/FeePaymentCurrency.kt @@ -1,5 +1,6 @@ package io.novafoundation.nova.feature_account_api.data.fee +import io.novafoundation.nova.runtime.ext.fullId import io.novafoundation.nova.runtime.ext.isCommissionAsset import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain @@ -17,7 +18,17 @@ sealed interface FeePaymentCurrency { * * The actual asset used to pay fees will be available in [Fee.asset] */ - data class Asset(val asset: Chain.Asset) : FeePaymentCurrency + class Asset(val asset: Chain.Asset) : FeePaymentCurrency { + + override fun equals(other: Any?): Boolean { + if (other !is Asset) return false + return asset.fullId == other.asset.fullId + } + + override fun hashCode(): Int { + return asset.hashCode() + } + } companion object } diff --git a/feature-account-api/src/main/java/io/novafoundation/nova/feature_account_api/data/fee/chains/CustomOrNativeFeePaymentProvider.kt b/feature-account-api/src/main/java/io/novafoundation/nova/feature_account_api/data/fee/chains/CustomOrNativeFeePaymentProvider.kt new file mode 100644 index 0000000000..b11831c1be --- /dev/null +++ b/feature-account-api/src/main/java/io/novafoundation/nova/feature_account_api/data/fee/chains/CustomOrNativeFeePaymentProvider.kt @@ -0,0 +1,21 @@ +package io.novafoundation.nova.feature_account_api.data.fee.chains + +import io.novafoundation.nova.feature_account_api.data.fee.FeePayment +import io.novafoundation.nova.feature_account_api.data.fee.FeePaymentCurrency +import io.novafoundation.nova.feature_account_api.data.fee.FeePaymentProvider +import io.novafoundation.nova.feature_account_api.data.fee.types.NativeFeePayment +import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain +import kotlinx.coroutines.CoroutineScope + +abstract class CustomOrNativeFeePaymentProvider : FeePaymentProvider { + + protected abstract suspend fun feePaymentFor(customFeeAsset: Chain.Asset, coroutineScope: CoroutineScope?): FeePayment + + final override suspend fun feePaymentFor(feePaymentCurrency: FeePaymentCurrency, coroutineScope: CoroutineScope?): FeePayment { + return when (feePaymentCurrency) { + is FeePaymentCurrency.Asset -> feePaymentFor(feePaymentCurrency.asset, coroutineScope) + + FeePaymentCurrency.Native -> NativeFeePayment() + } + } +} diff --git a/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/data/fee/types/NativeFeePayment.kt b/feature-account-api/src/main/java/io/novafoundation/nova/feature_account_api/data/fee/types/NativeFeePayment.kt similarity index 84% rename from feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/data/fee/types/NativeFeePayment.kt rename to feature-account-api/src/main/java/io/novafoundation/nova/feature_account_api/data/fee/types/NativeFeePayment.kt index ea5812c4cd..c6c0c25522 100644 --- a/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/data/fee/types/NativeFeePayment.kt +++ b/feature-account-api/src/main/java/io/novafoundation/nova/feature_account_api/data/fee/types/NativeFeePayment.kt @@ -1,11 +1,11 @@ -package io.novafoundation.nova.feature_account_impl.data.fee.types +package io.novafoundation.nova.feature_account_api.data.fee.types import io.novafoundation.nova.feature_account_api.data.fee.FeePayment import io.novafoundation.nova.feature_account_api.data.model.Fee import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain import io.novasama.substrate_sdk_android.runtime.extrinsic.ExtrinsicBuilder -internal class NativeFeePayment : FeePayment { +class NativeFeePayment : FeePayment { override suspend fun modifyExtrinsic(extrinsicBuilder: ExtrinsicBuilder) { // no modifications needed diff --git a/feature-account-api/src/main/java/io/novafoundation/nova/feature_account_api/data/fee/types/hydra/HydrationFeeInjector.kt b/feature-account-api/src/main/java/io/novafoundation/nova/feature_account_api/data/fee/types/hydra/HydrationFeeInjector.kt new file mode 100644 index 0000000000..f3adcf526b --- /dev/null +++ b/feature-account-api/src/main/java/io/novafoundation/nova/feature_account_api/data/fee/types/hydra/HydrationFeeInjector.kt @@ -0,0 +1,45 @@ +package io.novafoundation.nova.feature_account_api.data.fee.types.hydra + +import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain +import io.novasama.substrate_sdk_android.runtime.extrinsic.ExtrinsicBuilder +import java.math.BigInteger + +interface HydrationFeeInjector { + + class SetFeesMode( + val setMode: SetMode, + val resetMode: ResetMode + ) + + sealed class SetMode { + + /** + * Always sets the fee to the required token, regardless of whether fees are already in the needed state or not + */ + object Always : SetMode() + + /** + * Sets the fee token to the required one only the current fee payment asset is different + */ + class Lazy(val currentlySetFeeAsset: BigInteger) : SetMode() + } + + sealed class ResetMode { + + /** + * Always resets the fee to the native token, regardless of whether fees are already in the needed state or not + */ + object ToNative : ResetMode() + + /** + * Resets the the fee to the native one only the current fee payment asset is different + */ + class ToNativeLazily(val feeAssetBeforeTransaction: BigInteger) : ResetMode() + } + + suspend fun setFees( + extrinsicBuilder: ExtrinsicBuilder, + paymentAsset: Chain.Asset, + mode: SetFeesMode, + ) +} diff --git a/feature-account-api/src/main/java/io/novafoundation/nova/feature_account_api/di/AccountFeatureApi.kt b/feature-account-api/src/main/java/io/novafoundation/nova/feature_account_api/di/AccountFeatureApi.kt index 29bc79e517..c1e3ebce27 100644 --- a/feature-account-api/src/main/java/io/novafoundation/nova/feature_account_api/di/AccountFeatureApi.kt +++ b/feature-account-api/src/main/java/io/novafoundation/nova/feature_account_api/di/AccountFeatureApi.kt @@ -7,6 +7,8 @@ import io.novafoundation.nova.feature_account_api.data.ethereum.transaction.EvmT import io.novafoundation.nova.feature_account_api.data.events.MetaAccountChangesEventBus import io.novafoundation.nova.feature_account_api.data.extrinsic.ExtrinsicService import io.novafoundation.nova.feature_account_api.data.fee.FeePaymentProviderRegistry +import io.novafoundation.nova.feature_account_api.data.fee.capability.CustomFeeCapabilityFacade +import io.novafoundation.nova.feature_account_api.data.fee.types.hydra.HydrationFeeInjector import io.novafoundation.nova.feature_account_api.data.proxy.ProxySyncService import io.novafoundation.nova.feature_account_api.data.proxy.validation.ProxyExtrinsicValidationRequestBus import io.novafoundation.nova.feature_account_api.data.repository.OnChainIdentityRepository @@ -37,7 +39,6 @@ import io.novafoundation.nova.feature_account_api.presenatation.mixin.identity.I import io.novafoundation.nova.feature_account_api.presenatation.mixin.importType.ImportTypeChooserMixin import io.novafoundation.nova.feature_account_api.presenatation.mixin.selectAddress.SelectAddressMixin import io.novafoundation.nova.feature_account_api.presenatation.mixin.selectWallet.SelectWalletMixin -import io.novafoundation.nova.feature_account_api.data.fee.capability.CustomFeeCapabilityFacade interface AccountFeatureApi { @@ -118,4 +119,6 @@ interface AccountFeatureApi { val feePaymentProviderRegistry: FeePaymentProviderRegistry val customFeeCapabilityFacade: CustomFeeCapabilityFacade + + val hydrationFeeInjector: HydrationFeeInjector } diff --git a/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/data/extrinsic/RealExtrinsicServiceFactory.kt b/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/data/extrinsic/RealExtrinsicServiceFactory.kt index 2c4280be33..dabfc7665f 100644 --- a/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/data/extrinsic/RealExtrinsicServiceFactory.kt +++ b/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/data/extrinsic/RealExtrinsicServiceFactory.kt @@ -1,14 +1,15 @@ package io.novafoundation.nova.feature_account_impl.data.extrinsic import io.novafoundation.nova.feature_account_api.data.extrinsic.ExtrinsicService +import io.novafoundation.nova.feature_account_api.data.fee.FeePaymentProvider import io.novafoundation.nova.feature_account_api.data.fee.FeePaymentProviderRegistry import io.novafoundation.nova.feature_account_api.data.signer.SignerProvider import io.novafoundation.nova.feature_account_api.domain.interfaces.AccountRepository import io.novafoundation.nova.runtime.extrinsic.ExtrinsicBuilderFactory import io.novafoundation.nova.runtime.extrinsic.multi.ExtrinsicSplitter import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry +import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain import io.novafoundation.nova.runtime.network.rpc.RpcCalls -import kotlinx.coroutines.CoroutineScope class RealExtrinsicServiceFactory( private val rpcCalls: RpcCalls, @@ -20,16 +21,30 @@ class RealExtrinsicServiceFactory( private val feePaymentProviderRegistry: FeePaymentProviderRegistry ) : ExtrinsicService.Factory { - override fun create(coroutineScope: CoroutineScope): ExtrinsicService { + override fun create(feeConfig: ExtrinsicService.FeePaymentConfig): ExtrinsicService { + val registry = getRegistry(feeConfig) return RealExtrinsicService( - rpcCalls, - chainRegistry, - accountRepository, - extrinsicBuilderFactory, - signerProvider, - extrinsicSplitter, - feePaymentProviderRegistry, - coroutineScope + rpcCalls = rpcCalls, + chainRegistry = chainRegistry, + accountRepository = accountRepository, + extrinsicBuilderFactory = extrinsicBuilderFactory, + signerProvider = signerProvider, + extrinsicSplitter = extrinsicSplitter, + feePaymentProviderRegistry = registry, + coroutineScope = feeConfig.coroutineScope ) } + + private fun getRegistry(config: ExtrinsicService.FeePaymentConfig): FeePaymentProviderRegistry { + return config.customFeePaymentProvider?.let(::FixedFeePaymentProviderRegistry) ?: feePaymentProviderRegistry + } + + private class FixedFeePaymentProviderRegistry( + private val provider: FeePaymentProvider + ) : FeePaymentProviderRegistry { + + override suspend fun providerFor(chain: Chain): FeePaymentProvider { + return provider + } + } } diff --git a/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/data/fee/chains/AssetHubFeePaymentProvider.kt b/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/data/fee/chains/AssetHubFeePaymentProvider.kt index 26ac8b56ac..fc71731bbc 100644 --- a/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/data/fee/chains/AssetHubFeePaymentProvider.kt +++ b/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/data/fee/chains/AssetHubFeePaymentProvider.kt @@ -1,12 +1,11 @@ package io.novafoundation.nova.feature_account_impl.data.fee.chains import io.novafoundation.nova.feature_account_api.data.fee.FeePayment -import io.novafoundation.nova.feature_account_api.data.fee.FeePaymentCurrency -import io.novafoundation.nova.feature_account_api.data.fee.FeePaymentProvider +import io.novafoundation.nova.feature_account_api.data.fee.chains.CustomOrNativeFeePaymentProvider import io.novafoundation.nova.feature_account_impl.data.fee.types.AssetConversionFeePayment -import io.novafoundation.nova.feature_account_impl.data.fee.types.NativeFeePayment import io.novafoundation.nova.runtime.call.MultiChainRuntimeCallsApi import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry +import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain import io.novafoundation.nova.runtime.multiNetwork.multiLocation.converter.MultiLocationConverterFactory import io.novafoundation.nova.runtime.storage.source.StorageDataSource import kotlinx.coroutines.CoroutineScope @@ -16,21 +15,15 @@ class AssetHubFeePaymentProvider( private val multiChainRuntimeCallsApi: MultiChainRuntimeCallsApi, private val remoteStorageSource: StorageDataSource, private val multiLocationConverterFactory: MultiLocationConverterFactory, -) : FeePaymentProvider { +) : CustomOrNativeFeePaymentProvider() { - override suspend fun feePaymentFor(feePaymentCurrency: FeePaymentCurrency, coroutineScope: CoroutineScope?): FeePayment { - return when (feePaymentCurrency) { - is FeePaymentCurrency.Asset -> { - val chain = chainRegistry.getChain(feePaymentCurrency.asset.chainId) - AssetConversionFeePayment( - paymentAsset = feePaymentCurrency.asset, - multiChainRuntimeCallsApi = multiChainRuntimeCallsApi, - remoteStorageSource = remoteStorageSource, - multiLocationConverter = multiLocationConverterFactory.defaultAsync(chain, coroutineScope!!) - ) - } - - FeePaymentCurrency.Native -> NativeFeePayment() - } + override suspend fun feePaymentFor(customFeeAsset: Chain.Asset, coroutineScope: CoroutineScope?): FeePayment { + val chain = chainRegistry.getChain(customFeeAsset.chainId) + return AssetConversionFeePayment( + paymentAsset = customFeeAsset, + multiChainRuntimeCallsApi = multiChainRuntimeCallsApi, + remoteStorageSource = remoteStorageSource, + multiLocationConverter = multiLocationConverterFactory.defaultAsync(chain, coroutineScope!!) + ) } } diff --git a/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/data/fee/chains/DefaultFeePaymentProvider.kt b/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/data/fee/chains/DefaultFeePaymentProvider.kt index 0ca285698f..695a74db1a 100644 --- a/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/data/fee/chains/DefaultFeePaymentProvider.kt +++ b/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/data/fee/chains/DefaultFeePaymentProvider.kt @@ -3,7 +3,7 @@ package io.novafoundation.nova.feature_account_impl.data.fee.chains import io.novafoundation.nova.feature_account_api.data.fee.FeePayment import io.novafoundation.nova.feature_account_api.data.fee.FeePaymentCurrency import io.novafoundation.nova.feature_account_api.data.fee.FeePaymentProvider -import io.novafoundation.nova.feature_account_impl.data.fee.types.NativeFeePayment +import io.novafoundation.nova.feature_account_api.data.fee.types.NativeFeePayment import kotlinx.coroutines.CoroutineScope class DefaultFeePaymentProvider : FeePaymentProvider { diff --git a/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/data/fee/chains/HydrationFeePaymentProvider.kt b/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/data/fee/chains/HydrationFeePaymentProvider.kt index 1f53209489..fefcb20657 100644 --- a/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/data/fee/chains/HydrationFeePaymentProvider.kt +++ b/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/data/fee/chains/HydrationFeePaymentProvider.kt @@ -1,35 +1,30 @@ package io.novafoundation.nova.feature_account_impl.data.fee.chains import io.novafoundation.nova.feature_account_api.data.fee.FeePayment -import io.novafoundation.nova.feature_account_api.data.fee.FeePaymentCurrency -import io.novafoundation.nova.feature_account_api.data.fee.FeePaymentProvider +import io.novafoundation.nova.feature_account_api.data.fee.chains.CustomOrNativeFeePaymentProvider +import io.novafoundation.nova.feature_account_api.data.fee.types.hydra.HydrationFeeInjector import io.novafoundation.nova.feature_account_api.domain.interfaces.AccountRepository -import io.novafoundation.nova.feature_account_impl.data.fee.types.NativeFeePayment import io.novafoundation.nova.feature_account_impl.data.fee.types.hydra.HydraDxQuoteSharedComputation import io.novafoundation.nova.feature_account_impl.data.fee.types.hydra.HydrationConversionFeePayment -import io.novafoundation.nova.feature_swap_core_api.data.network.HydraDxAssetIdConverter import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry +import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain import kotlinx.coroutines.CoroutineScope class HydrationFeePaymentProvider( private val chainRegistry: ChainRegistry, - private val hydraDxAssetIdConverter: HydraDxAssetIdConverter, private val hydraDxQuoteSharedComputation: HydraDxQuoteSharedComputation, + private val hydrationFeeInjector: HydrationFeeInjector, private val accountRepository: AccountRepository -) : FeePaymentProvider { +) : CustomOrNativeFeePaymentProvider() { - override suspend fun feePaymentFor(feePaymentCurrency: FeePaymentCurrency, coroutineScope: CoroutineScope?): FeePayment { - return when (feePaymentCurrency) { - is FeePaymentCurrency.Asset -> HydrationConversionFeePayment( - paymentAsset = feePaymentCurrency.asset, - chainRegistry = chainRegistry, - hydraDxAssetIdConverter = hydraDxAssetIdConverter, - hydraDxQuoteSharedComputation = hydraDxQuoteSharedComputation, - accountRepository = accountRepository, - coroutineScope = coroutineScope!! - ) - - FeePaymentCurrency.Native -> NativeFeePayment() - } + override suspend fun feePaymentFor(customFeeAsset: Chain.Asset, coroutineScope: CoroutineScope?): FeePayment { + return HydrationConversionFeePayment( + paymentAsset = customFeeAsset, + chainRegistry = chainRegistry, + hydrationFeeInjector = hydrationFeeInjector, + hydraDxQuoteSharedComputation = hydraDxQuoteSharedComputation, + accountRepository = accountRepository, + coroutineScope = coroutineScope!! + ) } } diff --git a/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/data/fee/types/hydra/HydrationConversionFeePayment.kt b/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/data/fee/types/hydra/HydrationConversionFeePayment.kt index a537f4b630..6ac96c3750 100644 --- a/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/data/fee/types/hydra/HydrationConversionFeePayment.kt +++ b/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/data/fee/types/hydra/HydrationConversionFeePayment.kt @@ -1,14 +1,14 @@ package io.novafoundation.nova.feature_account_impl.data.fee.types.hydra -import io.novafoundation.nova.common.utils.Modules import io.novafoundation.nova.feature_account_api.data.fee.FeePayment +import io.novafoundation.nova.feature_account_api.data.fee.types.hydra.HydrationFeeInjector +import io.novafoundation.nova.feature_account_api.data.fee.types.hydra.HydrationFeeInjector.ResetMode +import io.novafoundation.nova.feature_account_api.data.fee.types.hydra.HydrationFeeInjector.SetFeesMode +import io.novafoundation.nova.feature_account_api.data.fee.types.hydra.HydrationFeeInjector.SetMode 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.domain.interfaces.AccountRepository import io.novafoundation.nova.feature_account_api.domain.model.requireAccountIdIn -import io.novafoundation.nova.feature_swap_core_api.data.network.HydraDxAssetId -import io.novafoundation.nova.feature_swap_core_api.data.network.HydraDxAssetIdConverter -import io.novafoundation.nova.feature_swap_core_api.data.network.toOnChainIdOrThrow import io.novafoundation.nova.feature_swap_core_api.data.paths.model.quote import io.novafoundation.nova.feature_swap_core_api.data.primitive.model.SwapDirection import io.novafoundation.nova.runtime.ext.commissionAsset @@ -20,19 +20,18 @@ import kotlinx.coroutines.CoroutineScope internal class HydrationConversionFeePayment( private val paymentAsset: Chain.Asset, private val chainRegistry: ChainRegistry, - private val hydraDxAssetIdConverter: HydraDxAssetIdConverter, + private val hydrationFeeInjector: HydrationFeeInjector, private val hydraDxQuoteSharedComputation: HydraDxQuoteSharedComputation, private val accountRepository: AccountRepository, private val coroutineScope: CoroutineScope ) : FeePayment { override suspend fun modifyExtrinsic(extrinsicBuilder: ExtrinsicBuilder) { - val baseCall = extrinsicBuilder.getCall() - extrinsicBuilder.resetCalls() - - extrinsicBuilder.setFeeCurrency(hydraDxAssetIdConverter.toOnChainIdOrThrow(paymentAsset)) - extrinsicBuilder.call(baseCall) - extrinsicBuilder.setFeeCurrency(hydraDxAssetIdConverter.systemAssetId) + val setFeesMode = SetFeesMode( + setMode = SetMode.Always, + resetMode = ResetMode.ToNative + ) + hydrationFeeInjector.setFees(extrinsicBuilder, paymentAsset, setFeesMode) } override suspend fun convertNativeFee(nativeFee: Fee): Fee { @@ -64,14 +63,4 @@ internal class HydrationConversionFeePayment( val assetConversion = hydraDxQuoteSharedComputation.getSwapQuoting(chain, accountId, coroutineScope) return assetConversion.canPayFeeInNonUtilityToken(paymentAsset) } - - private fun ExtrinsicBuilder.setFeeCurrency(onChainId: HydraDxAssetId) { - call( - moduleName = Modules.MULTI_TRANSACTION_PAYMENT, - callName = "set_currency", - arguments = mapOf( - "currency" to onChainId - ) - ) - } } diff --git a/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/data/fee/types/hydra/RealHydrationFeeInjector.kt b/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/data/fee/types/hydra/RealHydrationFeeInjector.kt new file mode 100644 index 0000000000..69ace37045 --- /dev/null +++ b/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/data/fee/types/hydra/RealHydrationFeeInjector.kt @@ -0,0 +1,121 @@ +package io.novafoundation.nova.feature_account_impl.data.fee.types.hydra + +import io.novafoundation.nova.common.utils.Modules +import io.novafoundation.nova.feature_account_api.data.fee.types.hydra.HydrationFeeInjector +import io.novafoundation.nova.feature_swap_core_api.data.network.HydraDxAssetId +import io.novafoundation.nova.feature_swap_core_api.data.network.HydraDxAssetIdConverter +import io.novafoundation.nova.feature_swap_core_api.data.network.toOnChainIdOrThrow +import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain +import io.novasama.substrate_sdk_android.runtime.extrinsic.ExtrinsicBuilder + +internal class RealHydrationFeeInjector( + private val hydraDxAssetIdConverter: HydraDxAssetIdConverter, +) : HydrationFeeInjector { + + override suspend fun setFees( + extrinsicBuilder: ExtrinsicBuilder, + paymentAsset: Chain.Asset, + mode: HydrationFeeInjector.SetFeesMode + ) { + val baseCall = extrinsicBuilder.getCall() + extrinsicBuilder.resetCalls() + + val justSetFees = getSetPhase(mode.setMode).setFees(extrinsicBuilder, paymentAsset) + extrinsicBuilder.call(baseCall) + getResetPhase(mode.resetMode).resetFees(extrinsicBuilder, justSetFees) + } + + private fun getSetPhase(mode: HydrationFeeInjector.SetMode): SetPhase { + return when(mode) { + HydrationFeeInjector.SetMode.Always -> AlwaysSetPhase() + is HydrationFeeInjector.SetMode.Lazy -> LazySetPhase(mode.currentlySetFeeAsset) + } + } + + private fun getResetPhase(mode: HydrationFeeInjector.ResetMode): ResetPhase { + return when(mode) { + HydrationFeeInjector.ResetMode.ToNative -> AlwaysResetPhase() + is HydrationFeeInjector.ResetMode.ToNativeLazily -> LazyResetPhase(mode.feeAssetBeforeTransaction) + } + } + + private interface SetPhase { + + /** + * @return just set on-chain asset id, if changed + */ + suspend fun setFees(extrinsicBuilder: ExtrinsicBuilder, paymentAsset: Chain.Asset): HydraDxAssetId? + } + + private interface ResetPhase { + + suspend fun resetFees( + extrinsicBuilder: ExtrinsicBuilder, + feesModifiedInSetPhase: HydraDxAssetId? + ) + } + + private inner class AlwaysSetPhase : SetPhase { + + override suspend fun setFees(extrinsicBuilder: ExtrinsicBuilder, paymentAsset: Chain.Asset): HydraDxAssetId { + val onChainId = hydraDxAssetIdConverter.toOnChainIdOrThrow(paymentAsset) + extrinsicBuilder.setFeeCurrency(onChainId) + return onChainId + } + } + + private inner class LazySetPhase( + private val currentFeeTokenId: HydraDxAssetId, + ) : SetPhase { + + override suspend fun setFees(extrinsicBuilder: ExtrinsicBuilder, paymentAsset: Chain.Asset): HydraDxAssetId? { + val paymentCurrencyToSet = getPaymentCurrencyToSetIfNeeded(paymentAsset) + + paymentCurrencyToSet?.let { + extrinsicBuilder.setFeeCurrency(paymentCurrencyToSet) + } + + return paymentCurrencyToSet + } + + private suspend fun getPaymentCurrencyToSetIfNeeded(expectedPaymentAsset: Chain.Asset): HydraDxAssetId? { + val expectedPaymentTokenId = hydraDxAssetIdConverter.toOnChainIdOrThrow(expectedPaymentAsset) + + return expectedPaymentTokenId.takeIf { currentFeeTokenId != expectedPaymentTokenId } + } + } + + private inner class AlwaysResetPhase : ResetPhase { + + override suspend fun resetFees( + extrinsicBuilder: ExtrinsicBuilder, + feesModifiedInSetPhase: HydraDxAssetId? + ) { + extrinsicBuilder.setFeeCurrency(hydraDxAssetIdConverter.systemAssetId) + } + } + + private inner class LazyResetPhase( + private val previousFeeCurrency: HydraDxAssetId + ) : ResetPhase { + + override suspend fun resetFees(extrinsicBuilder: ExtrinsicBuilder, feesModifiedInSetPhase: HydraDxAssetId?) { + val justSetFeeToNonNative = feesModifiedInSetPhase != null && feesModifiedInSetPhase != hydraDxAssetIdConverter.systemAssetId + val previousCurrencyRemainsNonNative = feesModifiedInSetPhase == null && previousFeeCurrency != hydraDxAssetIdConverter.systemAssetId + + if (justSetFeeToNonNative || previousCurrencyRemainsNonNative) { + extrinsicBuilder.setFeeCurrency(hydraDxAssetIdConverter.systemAssetId) + } + } + } + + private fun ExtrinsicBuilder.setFeeCurrency(onChainId: HydraDxAssetId) { + call( + moduleName = Modules.MULTI_TRANSACTION_PAYMENT, + callName = "set_currency", + arguments = mapOf( + "currency" to onChainId + ) + ) + } +} diff --git a/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/di/modules/CustomFeeModule.kt b/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/di/modules/CustomFeeModule.kt index c86d59e546..5828296d5e 100644 --- a/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/di/modules/CustomFeeModule.kt +++ b/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/di/modules/CustomFeeModule.kt @@ -6,12 +6,14 @@ import io.novafoundation.nova.common.data.memory.ComputationalCache import io.novafoundation.nova.common.di.scope.FeatureScope import io.novafoundation.nova.core.storage.StorageCache import io.novafoundation.nova.feature_account_api.data.fee.FeePaymentProviderRegistry +import io.novafoundation.nova.feature_account_api.data.fee.types.hydra.HydrationFeeInjector import io.novafoundation.nova.feature_account_api.domain.interfaces.AccountRepository import io.novafoundation.nova.feature_account_impl.data.fee.RealFeePaymentProviderRegistry import io.novafoundation.nova.feature_account_impl.data.fee.chains.AssetHubFeePaymentProvider import io.novafoundation.nova.feature_account_impl.data.fee.chains.DefaultFeePaymentProvider import io.novafoundation.nova.feature_account_impl.data.fee.chains.HydrationFeePaymentProvider import io.novafoundation.nova.feature_account_impl.data.fee.types.hydra.HydraDxQuoteSharedComputation +import io.novafoundation.nova.feature_account_impl.data.fee.types.hydra.RealHydrationFeeInjector import io.novafoundation.nova.feature_swap_core_api.data.network.HydraDxAssetIdConverter import io.novafoundation.nova.feature_swap_core_api.data.paths.PathQuoter import io.novafoundation.nova.feature_swap_core_api.data.types.hydra.HydraDxQuoting @@ -71,17 +73,25 @@ class CustomFeeModule { multiLocationConverterFactory ) + @Provides + @FeatureScope + fun provideHydraFeesInjector( + hydraDxAssetIdConverter: HydraDxAssetIdConverter, + ): HydrationFeeInjector = RealHydrationFeeInjector( + hydraDxAssetIdConverter, + ) + @Provides @FeatureScope fun provideHydrationFeePaymentProvider( chainRegistry: ChainRegistry, - hydraDxAssetIdConverter: HydraDxAssetIdConverter, hydraDxQuoteSharedComputation: HydraDxQuoteSharedComputation, + hydrationFeeInjector: HydrationFeeInjector, accountRepository: AccountRepository ) = HydrationFeePaymentProvider( chainRegistry, - hydraDxAssetIdConverter, hydraDxQuoteSharedComputation, + hydrationFeeInjector, accountRepository ) diff --git a/feature-swap-api/src/main/java/io/novafoundation/nova/feature_swap_api/domain/model/AtomicSwapOperation.kt b/feature-swap-api/src/main/java/io/novafoundation/nova/feature_swap_api/domain/model/AtomicSwapOperation.kt index b5d31f0638..28748f068c 100644 --- a/feature-swap-api/src/main/java/io/novafoundation/nova/feature_swap_api/domain/model/AtomicSwapOperation.kt +++ b/feature-swap-api/src/main/java/io/novafoundation/nova/feature_swap_api/domain/model/AtomicSwapOperation.kt @@ -1,7 +1,7 @@ package io.novafoundation.nova.feature_swap_api.domain.model +import io.novafoundation.nova.feature_account_api.data.fee.FeePaymentCurrency import io.novafoundation.nova.feature_account_api.data.model.Fee -import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain interface AtomicSwapOperation { @@ -12,7 +12,7 @@ interface AtomicSwapOperation { class AtomicSwapOperationArgs( val swapLimit: SwapLimit, - val customFeeAsset: Chain.Asset?, + val feePaymentCurrency: FeePaymentCurrency, ) typealias AtomicSwapOperationFee = Fee diff --git a/feature-swap-api/src/main/java/io/novafoundation/nova/feature_swap_api/domain/model/SwapQuote.kt b/feature-swap-api/src/main/java/io/novafoundation/nova/feature_swap_api/domain/model/SwapQuote.kt index cf808792d4..6c2ac64048 100644 --- a/feature-swap-api/src/main/java/io/novafoundation/nova/feature_swap_api/domain/model/SwapQuote.kt +++ b/feature-swap-api/src/main/java/io/novafoundation/nova/feature_swap_api/domain/model/SwapQuote.kt @@ -62,6 +62,9 @@ class SwapFee( val atomicOperationFees: List ) : GenericFee { + val firstSegmentFee: Fee + get() = atomicOperationFees.first() + // TODO handle multi-segment fee display override val networkFee: Fee get() = atomicOperationFees.first() diff --git a/feature-swap-api/src/main/java/io/novafoundation/nova/feature_swap_api/domain/model/SwapQuoteArgs.kt b/feature-swap-api/src/main/java/io/novafoundation/nova/feature_swap_api/domain/model/SwapQuoteArgs.kt index e1a3fb5c31..d56ed9a952 100644 --- a/feature-swap-api/src/main/java/io/novafoundation/nova/feature_swap_api/domain/model/SwapQuoteArgs.kt +++ b/feature-swap-api/src/main/java/io/novafoundation/nova/feature_swap_api/domain/model/SwapQuoteArgs.kt @@ -7,6 +7,7 @@ import io.novafoundation.nova.feature_swap_core_api.data.paths.model.QuotedEdge import io.novafoundation.nova.feature_swap_core_api.data.primitive.model.SwapDirection import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.types.Balance import io.novafoundation.nova.feature_wallet_api.domain.model.Token +import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain import java.math.BigDecimal data class SwapQuoteArgs( @@ -20,6 +21,7 @@ class SwapExecuteArgs( val slippage: Percent, val executionPath: Path, val direction: SwapDirection, + val firstSegmentFees: Chain.Asset ) class SegmentExecuteArgs( @@ -41,11 +43,12 @@ sealed class SwapLimit { ) : SwapLimit() } -fun SwapQuote.toExecuteArgs(slippage: Percent): SwapExecuteArgs { +fun SwapQuote.toExecuteArgs(slippage: Percent, firstSegmentFees: Chain.Asset): SwapExecuteArgs { return SwapExecuteArgs( slippage = slippage, direction = quotedPath.direction, - executionPath = quotedPath.path.map { quotedSwapEdge -> SegmentExecuteArgs(quotedSwapEdge) } + executionPath = quotedPath.path.map { quotedSwapEdge -> SegmentExecuteArgs(quotedSwapEdge) }, + firstSegmentFees = firstSegmentFees ) } diff --git a/feature-swap-core/src/main/java/io/novafoundation/nova/feature_swap_core/domain/paths/RealPathQuoter.kt b/feature-swap-core/src/main/java/io/novafoundation/nova/feature_swap_core/domain/paths/RealPathQuoter.kt index a2c9518935..e1d2b8d1c2 100644 --- a/feature-swap-core/src/main/java/io/novafoundation/nova/feature_swap_core/domain/paths/RealPathQuoter.kt +++ b/feature-swap-core/src/main/java/io/novafoundation/nova/feature_swap_core/domain/paths/RealPathQuoter.kt @@ -109,12 +109,7 @@ private class RealPathQuoter( val initial = mutableListOf>() to amount path.foldRight(initial) { segment, (quotedPath, currentAmount) -> - Log.d("Swaps", "Started quoting ${segment::class.simpleName}") - val segmentQuote = segment.quote(currentAmount, SwapDirection.SPECIFIED_OUT) - - Log.d("Swaps", "Finished quoting ${segment::class.simpleName}") - quotedPath.add(0, QuotedEdge(currentAmount, segmentQuote, segment)) quotedPath to segmentQuote @@ -129,12 +124,7 @@ private class RealPathQuoter( val initial = mutableListOf>() to amount path.fold(initial) { (quotedPath, currentAmount), segment -> - Log.d("Swaps", "Started quoting ${segment::class.simpleName}") - val segmentQuote = segment.quote(currentAmount, SwapDirection.SPECIFIED_IN) - - Log.d("Swaps", "Finished quoting ${segment::class.simpleName}") - quotedPath.add(QuotedEdge(currentAmount, segmentQuote, segment)) quotedPath to segmentQuote diff --git a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/assetConversion/AssetConversionExchange.kt b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/assetConversion/AssetConversionExchange.kt index 0c0157b4cf..2b13ced626 100644 --- a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/assetConversion/AssetConversionExchange.kt +++ b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/assetConversion/AssetConversionExchange.kt @@ -8,8 +8,7 @@ import io.novafoundation.nova.feature_account_api.data.conversion.assethub.pools 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_account_api.data.extrinsic.awaitInBlock -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.extrinsic.createDefault import io.novafoundation.nova.feature_account_api.domain.model.MetaAccount import io.novafoundation.nova.feature_swap_api.domain.model.AtomicSwapOperation import io.novafoundation.nova.feature_swap_api.domain.model.AtomicSwapOperationArgs @@ -26,11 +25,7 @@ import io.novafoundation.nova.feature_swap_impl.domain.swap.BaseSwapGraphEdge import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.types.Balance import io.novafoundation.nova.runtime.call.MultiChainRuntimeCallsApi import io.novafoundation.nova.runtime.call.RuntimeCallsApi -import io.novafoundation.nova.runtime.ext.commissionAsset import io.novafoundation.nova.runtime.ext.emptyAccountId -import io.novafoundation.nova.runtime.ext.fullId -import io.novafoundation.nova.runtime.ext.utilityAsset -import io.novafoundation.nova.runtime.extrinsic.CustomSignedExtensions.assetTxPayment import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain import io.novafoundation.nova.runtime.multiNetwork.multiLocation.MultiLocation import io.novafoundation.nova.runtime.multiNetwork.multiLocation.converter.MultiLocationConverter @@ -54,7 +49,7 @@ class AssetConversionExchangeFactory( private val multiLocationConverterFactory: MultiLocationConverterFactory, private val remoteStorageSource: StorageDataSource, private val runtimeCallsApi: MultiChainRuntimeCallsApi, - private val extrinsicService: ExtrinsicService, + private val extrinsicServiceFactory: ExtrinsicService.Factory, private val chainStateRepository: ChainStateRepository, ) : AssetExchange.SingleChainFactory { @@ -70,8 +65,9 @@ class AssetConversionExchangeFactory( multiLocationConverter = converter, remoteStorageSource = remoteStorageSource, multiChainRuntimeCallsApi = runtimeCallsApi, - extrinsicService = extrinsicService, - chainStateRepository = chainStateRepository + coroutineScope = coroutineScope, + chainStateRepository = chainStateRepository, + extrinsicServiceFactory = extrinsicServiceFactory ) } } @@ -81,10 +77,13 @@ private class AssetConversionExchange( private val multiLocationConverter: MultiLocationConverter, private val remoteStorageSource: StorageDataSource, private val multiChainRuntimeCallsApi: MultiChainRuntimeCallsApi, - private val extrinsicService: ExtrinsicService, + private val extrinsicServiceFactory: ExtrinsicService.Factory, private val chainStateRepository: ChainStateRepository, + coroutineScope: CoroutineScope ) : AssetExchange { + private val extrinsicService = extrinsicServiceFactory.createDefault(coroutineScope) + override suspend fun sync() { // nothing to sync } @@ -199,17 +198,27 @@ private class AssetConversionExchange( ) : AtomicSwapOperation { override suspend fun estimateFee(): AtomicSwapOperationFee { - val nativeAssetFee = extrinsicService.estimateFee(chain, TransactionOrigin.SelectedWallet) { + return extrinsicService.estimateFee( + chain = chain, + origin = TransactionOrigin.SelectedWallet, + submissionOptions = ExtrinsicService.SubmissionOptions( + feePaymentCurrency = transactionArgs.feePaymentCurrency + ) + ) { executeSwap(sendTo = chain.emptyAccountId()) } - - return convertNativeFeeToPayingTokenFee(nativeAssetFee) } override suspend fun submit(previousStepCorrection: SwapExecutionCorrection?): Result { // TODO use `previousStepCorrection` to correct used call arguments // TODO implement watching for extrinsic events - return extrinsicService.submitAndWatchExtrinsic(chain, TransactionOrigin.SelectedWallet) { submissionOrigin -> + return extrinsicService.submitAndWatchExtrinsic( + chain = chain, + origin = TransactionOrigin.SelectedWallet, + submissionOptions = ExtrinsicService.SubmissionOptions( + feePaymentCurrency = transactionArgs.feePaymentCurrency + ) + ) { submissionOrigin -> // Send swapped funds to the requested origin since it the account doing the swap executeSwap(sendTo = submissionOrigin.requestedOrigin) }.awaitInBlock().map { @@ -217,16 +226,6 @@ private class AssetConversionExchange( } } - private suspend fun convertNativeFeeToPayingTokenFee(nativeTokenFee: Fee): AtomicSwapOperationFee { - val customFeeAsset = transactionArgs.customFeeAsset - - return if (customFeeAsset != null && !customFeeAsset.isCommissionAsset()) { - calculateCustomTokenFee(nativeTokenFee, customFeeAsset) - } else { - nativeTokenFee - } - } - private suspend fun ExtrinsicBuilder.executeSwap(sendTo: AccountId) { val path = listOf(fromAsset, toAsset) .map { asset -> multiLocationConverter.encodableMultiLocationOf(asset) } @@ -258,48 +257,6 @@ private class AssetConversionExchange( ) ) } - - setFeeAsset(transactionArgs.customFeeAsset) - } - - private suspend fun ExtrinsicBuilder.setFeeAsset(feeAsset: Chain.Asset?) { - if (feeAsset == null || feeAsset.isCommissionAsset()) return - - val assetId = multiLocationConverter.encodableMultiLocationOf(feeAsset) - - assetTxPayment(assetId) - } - - // TODO we purposefully do not use `nativeTokenFee.amountByRequestedAccount` - // since we have disabled fee payment in custom tokens for accounts where the difference matters (e.g. proxy) - // We should adapt it if we decide to remove the restriction - private suspend fun calculateCustomTokenFee( - nativeTokenFee: Fee, - customFeeAsset: Chain.Asset - ): AtomicSwapOperationFee { - val runtimeCallsApi = multiChainRuntimeCallsApi.forChain(chain.id) - val toBuyNativeFee = runtimeCallsApi.quoteFeeConversion(nativeTokenFee.amount, customFeeAsset) - - return SubstrateFee( - amount = toBuyNativeFee, - submissionOrigin = nativeTokenFee.submissionOrigin, - asset = customFeeAsset - ) - } - - private suspend fun RuntimeCallsApi.quoteFeeConversion(commissionAmountOut: Balance, customFeeToken: Chain.Asset): Balance { - val quotedAmount = quote( - swapDirection = SwapDirection.SPECIFIED_OUT, - assetIn = customFeeToken, - assetOut = chain.utilityAsset, - amount = commissionAmountOut - ) - - return requireNotNull(quotedAmount) - } - - private fun Chain.Asset.isCommissionAsset(): Boolean { - return fullId == chain.commissionAsset.fullId } private suspend fun MultiLocationConverter.encodableMultiLocationOf(chainAsset: Chain.Asset): Any? { diff --git a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/crossChain/CrossChainTransferAssetExchange.kt b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/crossChain/CrossChainTransferAssetExchange.kt index abdf7b10ec..effb138684 100644 --- a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/crossChain/CrossChainTransferAssetExchange.kt +++ b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/crossChain/CrossChainTransferAssetExchange.kt @@ -1,9 +1,9 @@ package io.novafoundation.nova.feature_swap_impl.data.assetExchange.crossChain -import io.novafoundation.nova.common.utils.awaitTrue import io.novafoundation.nova.common.utils.emptySubstrateAccountId import io.novafoundation.nova.common.utils.graph.Edge import io.novafoundation.nova.feature_account_api.data.extrinsic.SubmissionOrigin +import io.novafoundation.nova.feature_account_api.data.fee.FeePaymentCurrency import io.novafoundation.nova.feature_account_api.data.model.SubstrateFee import io.novafoundation.nova.feature_account_api.domain.model.MetaAccount import io.novafoundation.nova.feature_swap_api.domain.model.AtomicSwapOperation @@ -22,7 +22,6 @@ import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain import io.novafoundation.nova.runtime.multiNetwork.chain.model.FullChainAssetId import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.emptyFlow import java.math.BigInteger @@ -48,16 +47,11 @@ class CrossChainTransferAssetExchange( private val chainRegistry: ChainRegistry ) : AssetExchange { - private val synced = MutableStateFlow(false) - override suspend fun sync() { crossChainTransfersUseCase.syncCrossChainConfig() - synced.value = true } override suspend fun availableDirectSwapConnections(): List { - synced.awaitTrue() - return crossChainTransfersUseCase.allDirections().map(::CrossChainTransferEdge) } @@ -85,11 +79,10 @@ class CrossChainTransferAssetExchange( } override suspend fun debugLabel(): String { - return "Cross-chain Transfer" + return "Transfer" } override suspend fun quote(amount: BigInteger, direction: SwapDirection): BigInteger { - // TODO include delivery & execution fees return amount } } @@ -100,18 +93,23 @@ class CrossChainTransferAssetExchange( ) : AtomicSwapOperation { override suspend fun estimateFee(): AtomicSwapOperationFee { - val chain = chainRegistry.getChain(edge.from.chainId) - // TODO return SubstrateFee( amount = BigInteger.ZERO, submissionOrigin = SubmissionOrigin.singleOrigin(emptySubstrateAccountId()), - asset = chain.utilityAsset + asset = paymentAsset() ) } override suspend fun submit(previousStepCorrection: SwapExecutionCorrection?): Result { return Result.failure(UnsupportedOperationException("TODO")) } + + private suspend fun paymentAsset(): Chain.Asset { + return when(val currency = transactionArgs.feePaymentCurrency) { + is FeePaymentCurrency.Asset -> currency.asset + FeePaymentCurrency.Native -> chainRegistry.getChain(edge.from.chainId).utilityAsset + } + } } } diff --git a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/hydraDx/HydraDxExchange.kt b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/hydraDx/HydraDxExchange.kt index 50f65b5786..b7b59191c6 100644 --- a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/hydraDx/HydraDxExchange.kt +++ b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/hydraDx/HydraDxExchange.kt @@ -10,6 +10,14 @@ import io.novafoundation.nova.common.utils.withFlowScope 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_account_api.data.extrinsic.awaitInBlock +import io.novafoundation.nova.feature_account_api.data.fee.FeePayment +import io.novafoundation.nova.feature_account_api.data.fee.FeePaymentCurrency +import io.novafoundation.nova.feature_account_api.data.fee.chains.CustomOrNativeFeePaymentProvider +import io.novafoundation.nova.feature_account_api.data.fee.types.hydra.HydrationFeeInjector +import io.novafoundation.nova.feature_account_api.data.fee.types.hydra.HydrationFeeInjector.ResetMode +import io.novafoundation.nova.feature_account_api.data.fee.types.hydra.HydrationFeeInjector.SetFeesMode +import io.novafoundation.nova.feature_account_api.data.fee.types.hydra.HydrationFeeInjector.SetMode +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.domain.model.MetaAccount import io.novafoundation.nova.feature_account_api.domain.model.requireAccountIdIn @@ -36,10 +44,8 @@ import io.novafoundation.nova.feature_swap_impl.data.assetExchange.hydraDx.refer import io.novafoundation.nova.feature_swap_impl.data.assetExchange.hydraDx.referrals.referralsOrNull import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.AssetSourceRegistry import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.existentialDepositInPlanks -import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.types.Balance import io.novafoundation.nova.runtime.ethereum.StorageSharedRequestsBuilder import io.novafoundation.nova.runtime.ethereum.StorageSharedRequestsBuilderFactory -import io.novafoundation.nova.runtime.ext.isUtilityAsset import io.novafoundation.nova.runtime.ext.utilityAsset import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain import io.novafoundation.nova.runtime.multiNetwork.chain.model.FullChainAssetId @@ -61,12 +67,13 @@ import kotlinx.coroutines.flow.onEach class HydraDxExchangeFactory( private val remoteStorageSource: StorageDataSource, private val sharedRequestsBuilderFactory: StorageSharedRequestsBuilderFactory, - private val extrinsicService: ExtrinsicService, + private val extrinsicServiceFactory: ExtrinsicService.Factory, private val hydraDxAssetIdConverter: HydraDxAssetIdConverter, private val hydraDxNovaReferral: HydraDxNovaReferral, private val swapSourceFactories: Iterable>, private val quotingFactory: HydraDxQuoting.Factory, private val assetSourceRegistry: AssetSourceRegistry, + private val hydrationFeeInjector: HydrationFeeInjector ) : AssetExchange.SingleChainFactory { override suspend fun create(chain: Chain, parentQuoter: AssetExchange.ParentQuoter, coroutineScope: CoroutineScope): AssetExchange { @@ -74,13 +81,15 @@ class HydraDxExchangeFactory( remoteStorageSource = remoteStorageSource, chain = chain, storageSharedRequestsBuilderFactory = sharedRequestsBuilderFactory, - extrinsicService = extrinsicService, + extrinsicServiceFactory = extrinsicServiceFactory, hydraDxAssetIdConverter = hydraDxAssetIdConverter, hydraDxNovaReferral = hydraDxNovaReferral, swapSourceFactories = swapSourceFactories, assetSourceRegistry = assetSourceRegistry, parentQuoter = parentQuoter, - delegate = quotingFactory.create(chain) + hydrationFeeInjector = hydrationFeeInjector, + delegate = quotingFactory.create(chain), + coroutineScope = coroutineScope ) } } @@ -90,14 +99,23 @@ private class HydraDxExchange( private val remoteStorageSource: StorageDataSource, private val chain: Chain, private val storageSharedRequestsBuilderFactory: StorageSharedRequestsBuilderFactory, - private val extrinsicService: ExtrinsicService, + private val extrinsicServiceFactory: ExtrinsicService.Factory, private val hydraDxAssetIdConverter: HydraDxAssetIdConverter, private val hydraDxNovaReferral: HydraDxNovaReferral, private val swapSourceFactories: Iterable>, private val assetSourceRegistry: AssetSourceRegistry, private val parentQuoter: AssetExchange.ParentQuoter, + private val hydrationFeeInjector: HydrationFeeInjector, + coroutineScope: CoroutineScope, ) : AssetExchange { + private val extrinsicService = extrinsicServiceFactory.create( + ExtrinsicService.FeePaymentConfig( + coroutineScope = coroutineScope, + customFeePaymentProvider = ReusableQuoteFeePaymentProvider() + ) + ) + private val swapSources: List = createSources() private val currentPaymentAsset: MutableSharedFlow = singleReplaySharedFlow() @@ -194,13 +212,13 @@ private class HydraDxExchange( ) : SwapGraphEdge, QuotableEdge by sourceQuotableEdge { override suspend fun beginOperation(args: AtomicSwapOperationArgs): AtomicSwapOperation { - return HydraDxOperation(HydraDxSwapTransactionSegment(sourceQuotableEdge, args)) + return HydraDxOperation(sourceQuotableEdge, args) } override suspend fun appendToOperation(currentTransaction: AtomicSwapOperation, args: AtomicSwapOperationArgs): AtomicSwapOperation? { if (currentTransaction !is HydraDxOperation) return null - return currentTransaction.appendSegment(HydraDxSwapTransactionSegment(sourceQuotableEdge, args)) + return currentTransaction.appendSegment(sourceQuotableEdge, args) } override suspend fun debugLabel(): String { @@ -210,46 +228,30 @@ private class HydraDxExchange( inner class HydraDxOperation private constructor( val segments: List, + val feePaymentCurrency: FeePaymentCurrency, ) : AtomicSwapOperation { - private val customFeeAsset: Chain.Asset? - get() = segments.first().segmentArgs.customFeeAsset - - private val usedFeeAsset: Chain.Asset - get() = customFeeAsset ?: chain.utilityAsset + constructor(sourceEdge: HydraDxSourceEdge, args: AtomicSwapOperationArgs) + : this(listOf(HydraDxSwapTransactionSegment(sourceEdge, args.swapLimit)), args.feePaymentCurrency) - constructor(segment: HydraDxSwapTransactionSegment) : this(listOf(segment)) + fun appendSegment(nextEdge: HydraDxSourceEdge, nextSwapArgs: AtomicSwapOperationArgs): HydraDxOperation { + val nextSegment = HydraDxSwapTransactionSegment(nextEdge, nextSwapArgs.swapLimit) - fun appendSegment(nextSegment: HydraDxSwapTransactionSegment): HydraDxOperation { - require(customFeeAsset == nextSegment.segmentArgs.customFeeAsset) { - "Different fee assets between multiple hydra swap segments - os ot" - } - - return HydraDxOperation(segments + nextSegment) + // Ignore nextSwapArgs.feePaymentCurrency - we are using configuration from the very first segment + return HydraDxOperation(segments + nextSegment, feePaymentCurrency) } override suspend fun estimateFee(): AtomicSwapOperationFee { - val nativeFee = extrinsicService.estimateFee( + return extrinsicService.estimateFee( chain = chain, origin = TransactionOrigin.SelectedWallet, submissionOptions = ExtrinsicService.SubmissionOptions( - batchMode = BatchMode.FORCE_BATCH + batchMode = BatchMode.FORCE_BATCH, + feePaymentCurrency = feePaymentCurrency ) ) { executeSwap() } - - val feeAmountInExpectedCurrency = if (!usedFeeAsset.isUtilityAsset) { - convertNativeFeeToAssetFee(nativeFee.amount, usedFeeAsset) - } else { - nativeFee.amount - } - - return SubstrateFee( - amount = feeAmountInExpectedCurrency, - submissionOrigin = nativeFee.submissionOrigin, - asset = usedFeeAsset - ) } override suspend fun submit(previousStepCorrection: SwapExecutionCorrection?): Result { @@ -257,7 +259,8 @@ private class HydraDxExchange( chain = chain, origin = TransactionOrigin.SelectedWallet, submissionOptions = ExtrinsicService.SubmissionOptions( - batchMode = BatchMode.FORCE_BATCH + batchMode = BatchMode.FORCE_BATCH, + feePaymentCurrency = feePaymentCurrency ) ) { executeSwap() @@ -267,25 +270,9 @@ private class HydraDxExchange( } private suspend fun ExtrinsicBuilder.executeSwap() { - val currentFeeTokenId = currentPaymentAsset.first() - - val justSetFeeCurrency = maybeSetFeeCurrencyToTarget(currentFeeTokenId) - maybeSetReferral() addSwapCall() - - maybeSetFeeCurrencyToNative(justSetFeeCurrency, previousFeeCurrency = currentFeeTokenId) - } - - private suspend fun ExtrinsicBuilder.maybeSetFeeCurrencyToTarget(currentFeeTokenId: HydraDxAssetId): HydraDxAssetId? { - val paymentCurrencyToSet = getPaymentCurrencyToSetIfNeeded(usedFeeAsset, currentFeeTokenId) - - paymentCurrencyToSet?.let { - setFeeCurrency(paymentCurrencyToSet) - } - - return paymentCurrencyToSet } private suspend fun ExtrinsicBuilder.addSwapCall() { @@ -302,7 +289,8 @@ private class HydraDxExchange( val onlySegment = segments.single() val standaloneSwapBuilder = onlySegment.edge.standaloneSwapBuilder ?: return false - standaloneSwapBuilder(onlySegment.segmentArgs) + val args = AtomicSwapOperationArgs(onlySegment.swapLimit, feePaymentCurrency) + standaloneSwapBuilder(args) return true } @@ -311,19 +299,19 @@ private class HydraDxExchange( val firstSegment = segments.first() val lastSegment = segments.last() - when (val firstLimit = firstSegment.segmentArgs.swapLimit) { + when (val firstLimit = firstSegment.swapLimit) { is SwapLimit.SpecifiedIn -> executeRouterSell( - firstSegment.edge, - firstLimit, - lastSegment.edge, - lastSegment.segmentArgs.swapLimit as SwapLimit.SpecifiedIn + firstEdge = firstSegment.edge, + firstLimit = firstLimit, + lastEdge = lastSegment.edge, + lastLimit = lastSegment.swapLimit as SwapLimit.SpecifiedIn ) is SwapLimit.SpecifiedOut -> executeRouterBuy( - firstSegment.edge, - firstLimit, - lastSegment.edge, - lastSegment.segmentArgs.swapLimit as SwapLimit.SpecifiedOut + firstEdge = firstSegment.edge, + firstLimit = firstLimit, + lastEdge = lastSegment.edge, + lastLimit = lastSegment.swapLimit as SwapLimit.SpecifiedOut ) } } @@ -386,15 +374,6 @@ private class HydraDxExchange( } } - private fun ExtrinsicBuilder.maybeSetFeeCurrencyToNative(justSetFeeCurrency: HydraDxAssetId?, previousFeeCurrency: HydraDxAssetId) { - val justSetFeeToNonNative = justSetFeeCurrency != null && justSetFeeCurrency != hydraDxAssetIdConverter.systemAssetId - val previousCurrencyRemainsNonNative = justSetFeeCurrency == null && previousFeeCurrency != hydraDxAssetIdConverter.systemAssetId - - if (justSetFeeToNonNative || previousCurrencyRemainsNonNative) { - setFeeCurrency(hydraDxAssetIdConverter.systemAssetId) - } - } - private fun ExtrinsicBuilder.linkCode(referralCode: String) { call( moduleName = Modules.REFERRALS, @@ -404,31 +383,36 @@ private class HydraDxExchange( ) ) } + } - private fun ExtrinsicBuilder.setFeeCurrency(onChainId: HydraDxAssetId) { - call( - moduleName = Modules.MULTI_TRANSACTION_PAYMENT, - callName = "set_currency", - arguments = mapOf( - "currency" to onChainId - ) - ) + // This is an optimization to reuse swap quoting state for hydra fee estimation instead of letting ExtrinsicService to spin up its own quoting + private inner class ReusableQuoteFeePaymentProvider : CustomOrNativeFeePaymentProvider() { + + override suspend fun feePaymentFor(customFeeAsset: Chain.Asset, coroutineScope: CoroutineScope?): FeePayment { + return ReusableQuoteFeePayment(customFeeAsset) } + } - private suspend fun getPaymentCurrencyToSetIfNeeded(expectedPaymentAsset: Chain.Asset, currentFeeTokenId: HydraDxAssetId): HydraDxAssetId? { - val expectedPaymentTokenId = hydraDxAssetIdConverter.toOnChainIdOrThrow(expectedPaymentAsset) + private inner class ReusableQuoteFeePayment( + private val customFeeAsset: Chain.Asset + ) : FeePayment { - return expectedPaymentTokenId.takeIf { currentFeeTokenId != expectedPaymentTokenId } + override suspend fun modifyExtrinsic(extrinsicBuilder: ExtrinsicBuilder) { + val currentFeeTokenId = currentPaymentAsset.first() + + val setFeesMode = SetFeesMode( + setMode = SetMode.Lazy(currentFeeTokenId), + resetMode = ResetMode.ToNativeLazily(currentFeeTokenId) + ) + + hydrationFeeInjector.setFees(extrinsicBuilder, customFeeAsset, setFeesMode) } - private suspend fun convertNativeFeeToAssetFee( - nativeFeeAmount: Balance, - targetAsset: Chain.Asset - ): Balance { + override suspend fun convertNativeFee(nativeFee: Fee): Fee { val args = ParentQuoterArgs( - chainAssetIn = targetAsset, + chainAssetIn = customFeeAsset, chainAssetOut = chain.utilityAsset, - amount = nativeFeeAmount, + amount = nativeFee.amount, swapDirection = SwapDirection.SPECIFIED_OUT ) @@ -438,14 +422,23 @@ private class HydraDxExchange( // There is a issue in Router implementation in Hydra that doesn't allow asset balance to go below ED. We add it to fee for simplicity instead // of refactoring SwapExistentialDepositAwareMaxActionProvider // This should be removed once Router issue is fixed - val existentialDeposit = assetSourceRegistry.existentialDepositInPlanks(chain, targetAsset) + val existentialDeposit = assetSourceRegistry.existentialDepositInPlanks(chain, customFeeAsset) + val fee = quotedFee + existentialDeposit + + return SubstrateFee( + amount = fee, + submissionOrigin = nativeFee.submissionOrigin, + asset = customFeeAsset + ) + } - return quotedFee + existentialDeposit + override suspend fun canPayFeeInNonUtilityToken(chainAsset: Chain.Asset): Boolean { + return delegate.canPayFeeInNonUtilityToken(chainAsset) } } -} -private class HydraDxSwapTransactionSegment( - val edge: HydraDxSourceEdge, - val segmentArgs: AtomicSwapOperationArgs, -) + class HydraDxSwapTransactionSegment( + val edge: HydraDxSourceEdge, + val swapLimit: SwapLimit, + ) +} diff --git a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/di/SwapFeatureDependencies.kt b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/di/SwapFeatureDependencies.kt index 61d59a9cac..54622fc5ab 100644 --- a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/di/SwapFeatureDependencies.kt +++ b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/di/SwapFeatureDependencies.kt @@ -16,6 +16,7 @@ import io.novafoundation.nova.core.storage.StorageCache import io.novafoundation.nova.core_db.dao.OperationDao import io.novafoundation.nova.feature_account_api.data.extrinsic.ExtrinsicService import io.novafoundation.nova.feature_account_api.data.fee.capability.CustomFeeCapabilityFacade +import io.novafoundation.nova.feature_account_api.data.fee.types.hydra.HydrationFeeInjector 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_account_api.domain.interfaces.SelectedAccountUseCase @@ -77,7 +78,7 @@ interface SwapFeatureDependencies { val amountMixinFactory: AmountChooserMixin.Factory - val extrinsicService: ExtrinsicService + val extrinsicServiceFactory: ExtrinsicService.Factory val resourceHintsMixinFactory: ResourcesHintsMixinFactory @@ -131,4 +132,6 @@ interface SwapFeatureDependencies { val customFeeCapabilityFacade: CustomFeeCapabilityFacade val quoterFactory: PathQuoter.Factory + + val hydrationFeeInjector: HydrationFeeInjector } diff --git a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/di/exchanges/AssetConversionExchangeModule.kt b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/di/exchanges/AssetConversionExchangeModule.kt index 11c066c56c..ec99f08908 100644 --- a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/di/exchanges/AssetConversionExchangeModule.kt +++ b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/di/exchanges/AssetConversionExchangeModule.kt @@ -5,7 +5,6 @@ import dagger.Provides import io.novafoundation.nova.common.di.scope.FeatureScope import io.novafoundation.nova.feature_account_api.data.extrinsic.ExtrinsicService import io.novafoundation.nova.feature_swap_impl.data.assetExchange.assetConversion.AssetConversionExchangeFactory -import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.AssetSourceRegistry import io.novafoundation.nova.runtime.call.MultiChainRuntimeCallsApi import io.novafoundation.nova.runtime.di.REMOTE_STORAGE_SOURCE import io.novafoundation.nova.runtime.multiNetwork.multiLocation.converter.MultiLocationConverterFactory @@ -21,15 +20,15 @@ class AssetConversionExchangeModule { fun provideAssetConversionExchangeFactory( @Named(REMOTE_STORAGE_SOURCE) remoteStorageSource: StorageDataSource, runtimeCallsApi: MultiChainRuntimeCallsApi, - extrinsicService: ExtrinsicService, multiLocationConverterFactory: MultiLocationConverterFactory, - chainStateRepository: ChainStateRepository + chainStateRepository: ChainStateRepository, + extrinsicServiceFactory: ExtrinsicService.Factory, ): AssetConversionExchangeFactory { return AssetConversionExchangeFactory( chainStateRepository = chainStateRepository, remoteStorageSource = remoteStorageSource, runtimeCallsApi = runtimeCallsApi, - extrinsicService = extrinsicService, + extrinsicServiceFactory = extrinsicServiceFactory, multiLocationConverterFactory = multiLocationConverterFactory ) } diff --git a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/di/exchanges/HydraDxExchangeModule.kt b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/di/exchanges/HydraDxExchangeModule.kt index 563b878994..778e00f0b0 100644 --- a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/di/exchanges/HydraDxExchangeModule.kt +++ b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/di/exchanges/HydraDxExchangeModule.kt @@ -5,6 +5,7 @@ import dagger.Provides import dagger.multibindings.IntoSet import io.novafoundation.nova.common.di.scope.FeatureScope import io.novafoundation.nova.feature_account_api.data.extrinsic.ExtrinsicService +import io.novafoundation.nova.feature_account_api.data.fee.types.hydra.HydrationFeeInjector import io.novafoundation.nova.feature_swap_core_api.data.network.HydraDxAssetIdConverter import io.novafoundation.nova.feature_swap_core_api.data.types.hydra.HydraDxQuoting import io.novafoundation.nova.feature_swap_impl.data.assetExchange.hydraDx.HydraDxExchangeFactory @@ -54,22 +55,24 @@ class HydraDxExchangeModule { fun provideHydraDxExchangeFactory( @Named(REMOTE_STORAGE_SOURCE) remoteStorageSource: StorageDataSource, sharedRequestsBuilderFactory: StorageSharedRequestsBuilderFactory, - extrinsicService: ExtrinsicService, hydraDxAssetIdConverter: HydraDxAssetIdConverter, hydraDxNovaReferral: HydraDxNovaReferral, swapSourceFactories: Set<@JvmSuppressWildcards HydraDxSwapSource.Factory<*>>, + extrinsicServiceFactory: ExtrinsicService.Factory, quotingFactory: HydraDxQuoting.Factory, assetSourceRegistry: AssetSourceRegistry, + hydrationFeeInjector: HydrationFeeInjector ): HydraDxExchangeFactory { return HydraDxExchangeFactory( remoteStorageSource = remoteStorageSource, sharedRequestsBuilderFactory = sharedRequestsBuilderFactory, - extrinsicService = extrinsicService, + extrinsicServiceFactory = extrinsicServiceFactory, hydraDxAssetIdConverter = hydraDxAssetIdConverter, hydraDxNovaReferral = hydraDxNovaReferral, swapSourceFactories = swapSourceFactories, assetSourceRegistry = assetSourceRegistry, - quotingFactory = quotingFactory + quotingFactory = quotingFactory, + hydrationFeeInjector = hydrationFeeInjector ) } } diff --git a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/domain/swap/RealSwapService.kt b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/domain/swap/RealSwapService.kt index 8bf32ed2b5..6e8fab5acb 100644 --- a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/domain/swap/RealSwapService.kt +++ b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/domain/swap/RealSwapService.kt @@ -18,10 +18,11 @@ import io.novafoundation.nova.common.utils.isZero import io.novafoundation.nova.common.utils.mapAsync import io.novafoundation.nova.common.utils.mergeIfMultiple import io.novafoundation.nova.common.utils.requireInnerNotNull -import io.novafoundation.nova.common.utils.throttleLast import io.novafoundation.nova.common.utils.toPercent import io.novafoundation.nova.common.utils.withFlowScope +import io.novafoundation.nova.feature_account_api.data.fee.FeePaymentCurrency import io.novafoundation.nova.feature_account_api.data.fee.capability.CustomFeeCapabilityFacade +import io.novafoundation.nova.feature_account_api.data.fee.toFeePaymentCurrency import io.novafoundation.nova.feature_account_api.domain.model.MetaAccount import io.novafoundation.nova.feature_swap_api.domain.model.AtomicSwapOperation import io.novafoundation.nova.feature_swap_api.domain.model.AtomicSwapOperationArgs @@ -65,6 +66,7 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.catch +import kotlinx.coroutines.flow.debounce import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.map @@ -141,7 +143,7 @@ internal class RealSwapService( val fees = atomicOperations.mapAsync { it.estimateFee() } - return SwapFee(fees) + return SwapFee(fees).also(::logFee) } override suspend fun swap(args: SwapExecuteArgs): Result { @@ -161,19 +163,21 @@ internal class RealSwapService( // TODO this will result in lower total slippage if some segments are appendable val perSegmentSlippage = slippage / executionPath.size - executionPath.forEach { segmentExecuteArgs -> + executionPath.forEachIndexed { index, segmentExecuteArgs -> val quotedEdge = segmentExecuteArgs.quotedSwapEdge val operationArgs = AtomicSwapOperationArgs( swapLimit = SwapLimit(direction, quotedEdge.quotedAmount, perSegmentSlippage, quotedEdge.quote), - // TODO custom fee assets - customFeeAsset = null, + feePaymentCurrency = segmentExecuteArgs.quotedSwapEdge.edge.identifySegmentCurrency( + isFirstSegment = index == 0, + firstSegmentFees = firstSegmentFees + ) ) // Initial case - begin first operation if (currentSwapTx == null) { currentSwapTx = quotedEdge.edge.beginOperation(operationArgs) - return@forEach + return@forEachIndexed } // Try to append segment to current swap tx @@ -192,6 +196,18 @@ internal class RealSwapService( return finishedSwapTxs } + private suspend fun SwapGraphEdge.identifySegmentCurrency( + isFirstSegment: Boolean, + firstSegmentFees: Chain.Asset + ): FeePaymentCurrency { + return if (isFirstSegment) { + firstSegmentFees.toFeePaymentCurrency() + } else { + // When executing intermediate segments, always pay in sending asset + chainRegistry.asset(from).toFeePaymentCurrency() + } + } + private suspend fun quoteInternal( args: SwapQuoteArgs, @@ -227,7 +243,7 @@ internal class RealSwapService( exchangeRegistry.allExchanges() .map { it.runSubscriptions(metaAccount) } .mergeIfMultiple() - }.throttleLast(500.milliseconds) + }.debounce(500.milliseconds) } private fun SwapQuoteArgs.calculatePriceImpact(amountIn: Balance, amountOut: Balance): Percent { @@ -330,12 +346,13 @@ internal class RealSwapService( chainAssetOut: Chain.Asset, amount: Balance, swapDirection: SwapDirection, - computationSharingScope: CoroutineScope + computationSharingScope: CoroutineScope, + logQuotes: Boolean = true ): QuotedTrade { val quoter = getPathQuoter(computationSharingScope) val bestPathQuote = quoter.findBestPath(chainAssetIn, chainAssetOut, amount, swapDirection) - if (debug) { + if (debug && logQuotes) { logQuotes(bestPathQuote.candidates) } @@ -359,11 +376,20 @@ internal class RealSwapService( chainAssetOut = quoteArgs.chainAssetOut, amount = quoteArgs.amount, swapDirection = quoteArgs.swapDirection, - computationSharingScope = computationScope + computationSharingScope = computationScope, + logQuotes = false ).finalQuote() } } + private fun logFee(fee: SwapFee) { + val route = fee.atomicOperationFees.joinToString() { + it.amount.formatPlanks(it.asset) + } + + Log.d("Swaps", "Fee: $route") + } + private suspend fun logQuotes(quotedTrades: List) { val allCandidates = quotedTrades.sortedDescending() .map { trade -> formatTrade(trade) } diff --git a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/confirmation/SwapConfirmationViewModel.kt b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/confirmation/SwapConfirmationViewModel.kt index 153e1ba30e..0467ed87ae 100644 --- a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/confirmation/SwapConfirmationViewModel.kt +++ b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/confirmation/SwapConfirmationViewModel.kt @@ -246,8 +246,11 @@ class SwapConfirmationViewModel( private fun executeSwap() = launch { val quote = confirmationStateFlow.value?.swapQuote ?: return@launch - // TODO fees in sending asset - val executeArgs = quote.toExecuteArgs(slippage = initialSwapState.first().slippage) + val swapState = initialSwapState.first() + val executeArgs = quote.toExecuteArgs( + slippage = swapState.slippage, + firstSegmentFees = swapState.fee.firstSegmentFee.asset + ) swapInteractor.executeSwap(executeArgs) .onSuccess { navigateToNextScreen(quote.assetIn) } @@ -356,7 +359,10 @@ class SwapConfirmationViewModel( .onFailure { } .getOrNull() ?: return@launch - val executeArgs = swapQuote.toExecuteArgs(slippageFlow.first()) + val executeArgs = swapQuote.toExecuteArgs( + slippage = slippageFlow.first(), + firstSegmentFees = initialSwapState.first().fee.firstSegmentFee.asset + ) feeMixin.loadFeeV2Generic( coroutineScope = viewModelScope, diff --git a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/main/QuotingState.kt b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/main/QuotingState.kt index 4fdcebbfd2..96b3edca35 100644 --- a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/main/QuotingState.kt +++ b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/main/QuotingState.kt @@ -12,5 +12,5 @@ sealed class QuotingState { object NotAvailable : QuotingState() - data class Loaded(val value: SwapQuote, val quoteArgs: SwapQuoteArgs, val feeAsset: Chain.Asset) : QuotingState() + data class Loaded(val value: SwapQuote, val quoteArgs: SwapQuoteArgs, val firstSegmentFeeAsset: Chain.Asset) : QuotingState() } diff --git a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/main/SwapMainSettingsViewModel.kt b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/main/SwapMainSettingsViewModel.kt index f9cc52b312..c70fac4ab8 100644 --- a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/main/SwapMainSettingsViewModel.kt +++ b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/main/SwapMainSettingsViewModel.kt @@ -540,7 +540,8 @@ class SwapMainSettingsViewModel( } .mapLatest { quoteState -> val swapArgs = quoteState.value.toExecuteArgs( - slippage = swapSettings.first().slippage + slippage = swapSettings.first().slippage, + firstSegmentFees = quoteState.firstSegmentFeeAsset ) loadFeeSuspending( 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 3b21917548..e825fb8811 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 @@ -5,6 +5,7 @@ import io.novafoundation.nova.feature_account_api.data.ethereum.transaction.Tran import io.novafoundation.nova.feature_account_api.data.ethereum.transaction.intoOrigin import io.novafoundation.nova.feature_account_api.data.extrinsic.ExtrinsicService import io.novafoundation.nova.feature_account_api.data.extrinsic.ExtrinsicSubmission +import io.novafoundation.nova.feature_account_api.data.extrinsic.createDefault import io.novafoundation.nova.feature_account_api.data.fee.toFeePaymentCurrency import io.novafoundation.nova.feature_account_api.data.model.Fee import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.AssetSourceRegistry @@ -55,7 +56,7 @@ abstract class BaseAssetTransfers( val submissionOptions = ExtrinsicService.SubmissionOptions(feePaymentCurrency) return extrinsicServiceFactory - .create(coroutineScope) + .createDefault(coroutineScope) .submitExtrinsic(transfer.originChain, transfer.sender.intoOrigin(), submissionOptions = submissionOptions) { transfer(transfer) } @@ -66,7 +67,7 @@ abstract class BaseAssetTransfers( val submissionOptions = ExtrinsicService.SubmissionOptions(feePaymentCurrency) return extrinsicServiceFactory - .create(coroutineScope) + .createDefault(coroutineScope) .estimateFee(transfer.originChain, TransactionOrigin.SelectedWallet, submissionOptions = submissionOptions) { transfer(transfer) } From 8061115e7e19cbe30e0373534abcb9f0f342b7e2 Mon Sep 17 00:00:00 2001 From: Valentun Date: Fri, 11 Oct 2024 12:46:29 +0300 Subject: [PATCH 15/83] Cross chain fees work --- .../data/extrinsic/ExtrinsicService.kt | 3 +- .../feature_account_api/data/model/Fee.kt | 21 ++++- .../extrinsic/RealExtrinsicServiceFactory.kt | 13 +-- .../feature_assets/di/modules/SendModule.kt | 7 +- .../domain/send/SendInteractor.kt | 16 ++-- .../domain/model/AtomicSwapOperation.kt | 8 +- .../domain/model/SwapQuote.kt | 4 +- .../data/assetExchange/AssetExchange.kt | 17 +++- .../AssetConversionExchange.kt | 11 ++- .../compound/CompoundAssetExchange.kt | 5 ++ .../CrossChainTransferAssetExchange.kt | 75 ++++++++++++---- .../assetExchange/hydraDx/HydraDxExchange.kt | 35 ++++---- .../di/SwapFeatureDependencies.kt | 3 + .../feature_swap_impl/di/SwapFeatureModule.kt | 8 +- .../CrossChainTransferExchangeModule.kt | 7 +- .../domain/swap/RealSwapService.kt | 88 ++++++++++++++++--- .../assets/tranfers/AssetTransfers.kt | 52 ++++++++++- .../network/crosschain/CrossChainFeeModel.kt | 23 +++-- .../crosschain/CrossChainTransactor.kt | 11 +-- .../interfaces/CrossChainTransfersUseCase.kt | 9 ++ .../domain/model/CrossChainFee.kt | 27 ++++++ .../crosschain/RealCrossChainTransactor.kt | 47 ++++++---- .../crosschain/RealCrossChainWeigher.kt | 8 +- .../di/WalletFeatureDependencies.kt | 3 + .../di/WalletFeatureModule.kt | 13 ++- .../domain/RealCrossChainTransfersUseCase.kt | 55 +++++++++++- 26 files changed, 442 insertions(+), 127 deletions(-) create mode 100644 feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/domain/model/CrossChainFee.kt diff --git a/feature-account-api/src/main/java/io/novafoundation/nova/feature_account_api/data/extrinsic/ExtrinsicService.kt b/feature-account-api/src/main/java/io/novafoundation/nova/feature_account_api/data/extrinsic/ExtrinsicService.kt index 7a7a73cb31..4f8e9f1d61 100644 --- a/feature-account-api/src/main/java/io/novafoundation/nova/feature_account_api/data/extrinsic/ExtrinsicService.kt +++ b/feature-account-api/src/main/java/io/novafoundation/nova/feature_account_api/data/extrinsic/ExtrinsicService.kt @@ -4,7 +4,6 @@ import io.novafoundation.nova.common.data.network.runtime.model.FeeResponse import io.novafoundation.nova.common.utils.multiResult.RetriableMultiResult import io.novafoundation.nova.feature_account_api.data.ethereum.transaction.TransactionOrigin import io.novafoundation.nova.feature_account_api.data.fee.FeePaymentCurrency -import io.novafoundation.nova.feature_account_api.data.fee.FeePaymentProvider import io.novafoundation.nova.feature_account_api.data.fee.FeePaymentProviderRegistry import io.novafoundation.nova.feature_account_api.data.model.Fee import io.novafoundation.nova.runtime.extrinsic.ExtrinsicStatus @@ -63,7 +62,7 @@ interface ExtrinsicService { /** * Specify to use it instead of default [FeePaymentProviderRegistry] to perform fee computations */ - val customFeePaymentProvider: FeePaymentProvider? = null, + val customFeePaymentRegistry: FeePaymentProviderRegistry? = null, ) suspend fun submitExtrinsic( 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 d70098b36a..0cdbe332f8 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 @@ -6,16 +6,26 @@ import io.novafoundation.nova.runtime.ext.utilityAsset import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain import java.math.BigInteger -interface Fee { +// TODO rename FeeBase -> Fee and use SubmissionFee everywhere Fee is currently used +typealias Fee = SubmissionFee - companion object +interface SubmissionFee : FeeBase { - val amount: BigInteger + companion object /** * Information about origin that is supposed to send the transaction fee was calculated against */ val submissionOrigin: SubmissionOrigin +} + +/** + * Fee that doesn't have a particular origin + * For example, fees paid during cross chain transfers do not have a specific account that pays them + */ +interface FeeBase { + + val amount: BigInteger val asset: Chain.Asset } @@ -35,6 +45,11 @@ class SubstrateFee( override val asset: Chain.Asset ) : Fee +class SubstrateFeeBase( + override val amount: BigInteger, + override val asset: Chain.Asset +): FeeBase + val Fee.requestedAccountPaysFees: Boolean get() = submissionOrigin.requestedOrigin.contentEquals(submissionOrigin.actualOrigin) diff --git a/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/data/extrinsic/RealExtrinsicServiceFactory.kt b/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/data/extrinsic/RealExtrinsicServiceFactory.kt index dabfc7665f..21f017a727 100644 --- a/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/data/extrinsic/RealExtrinsicServiceFactory.kt +++ b/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/data/extrinsic/RealExtrinsicServiceFactory.kt @@ -1,14 +1,12 @@ package io.novafoundation.nova.feature_account_impl.data.extrinsic import io.novafoundation.nova.feature_account_api.data.extrinsic.ExtrinsicService -import io.novafoundation.nova.feature_account_api.data.fee.FeePaymentProvider import io.novafoundation.nova.feature_account_api.data.fee.FeePaymentProviderRegistry import io.novafoundation.nova.feature_account_api.data.signer.SignerProvider import io.novafoundation.nova.feature_account_api.domain.interfaces.AccountRepository import io.novafoundation.nova.runtime.extrinsic.ExtrinsicBuilderFactory import io.novafoundation.nova.runtime.extrinsic.multi.ExtrinsicSplitter import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry -import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain import io.novafoundation.nova.runtime.network.rpc.RpcCalls class RealExtrinsicServiceFactory( @@ -36,15 +34,6 @@ class RealExtrinsicServiceFactory( } private fun getRegistry(config: ExtrinsicService.FeePaymentConfig): FeePaymentProviderRegistry { - return config.customFeePaymentProvider?.let(::FixedFeePaymentProviderRegistry) ?: feePaymentProviderRegistry - } - - private class FixedFeePaymentProviderRegistry( - private val provider: FeePaymentProvider - ) : FeePaymentProviderRegistry { - - override suspend fun providerFor(chain: Chain): FeePaymentProvider { - return provider - } + return config.customFeePaymentRegistry ?: feePaymentProviderRegistry } } diff --git a/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/di/modules/SendModule.kt b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/di/modules/SendModule.kt index b93e424870..72b9560a2d 100644 --- a/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/di/modules/SendModule.kt +++ b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/di/modules/SendModule.kt @@ -3,6 +3,7 @@ package io.novafoundation.nova.feature_assets.di.modules import dagger.Module import dagger.Provides import io.novafoundation.nova.common.di.scope.FeatureScope +import io.novafoundation.nova.feature_account_api.data.extrinsic.ExtrinsicService import io.novafoundation.nova.feature_assets.domain.send.SendInteractor import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.AssetSourceRegistry import io.novafoundation.nova.feature_wallet_api.data.network.crosschain.CrossChainTransactor @@ -22,13 +23,15 @@ class SendModule { crossChainTransfersRepository: CrossChainTransfersRepository, crossChainWeigher: CrossChainWeigher, crossChainTransactor: CrossChainTransactor, - parachainInfoRepository: ParachainInfoRepository + parachainInfoRepository: ParachainInfoRepository, + extrinsicService: ExtrinsicService, ) = SendInteractor( walletRepository, assetSourceRegistry, crossChainWeigher, crossChainTransactor, crossChainTransfersRepository, - parachainInfoRepository + parachainInfoRepository, + extrinsicService ) } 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 bb73ee4caa..091d618c8b 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,5 +1,6 @@ package io.novafoundation.nova.feature_assets.domain.send +import io.novafoundation.nova.feature_account_api.data.extrinsic.ExtrinsicService 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 @@ -38,7 +39,8 @@ class SendInteractor( private val crossChainWeigher: CrossChainWeigher, private val crossChainTransactor: CrossChainTransactor, private val crossChainTransfersRepository: CrossChainTransfersRepository, - private val parachainInfoRepository: ParachainInfoRepository + private val parachainInfoRepository: ParachainInfoRepository, + private val extrinsicService: ExtrinsicService, ) { // TODO wallet @@ -70,10 +72,12 @@ class SendInteractor( if (transfer.isCrossChain) { val config = crossChainTransfersRepository.getConfiguration().configurationFor(transfer)!! - val originFee = crossChainTransactor.estimateOriginFee(config, transfer) + val originFee = with(crossChainTransactor) { + extrinsicService.estimateOriginFee(config, transfer) + } val crossChainFeeModel = crossChainWeigher.estimateFee(amount, config) - val deliveryPartFee = getDeliveryFee(transfer.originChain, crossChainFeeModel.senderPart, transfer.senderAccountId()) + val deliveryPartFee = getDeliveryFee(transfer.originChain, crossChainFeeModel.paidByOrigin, transfer.senderAccountId()) val originFeeWithSenderPart = OriginFee(originFee, deliveryPartFee, transfer.commissionAssetToken.configuration) TransferFeeModel(originFeeWithSenderPart, crossChainFeeModel.toSubstrateFee(transfer)) @@ -95,7 +99,9 @@ class SendInteractor( if (transfer.isCrossChain) { val config = crossChainTransfersRepository.getConfiguration().configurationFor(transfer)!! - crossChainTransactor.performTransfer(config, transfer, crossChainFee!!.amountByRequestedAccount) + with(crossChainTransactor) { + extrinsicService.performTransfer(config, transfer, crossChainFee!!.amountByRequestedAccount) + } } else { val networkFee = originFee.networkFeePart() @@ -133,7 +139,7 @@ class SendInteractor( } private fun CrossChainFeeModel.toSubstrateFee(transfer: AssetTransfer) = SubstrateFee( - amount = holdingPart, + amount = paidFromHoldingRegister, submissionOrigin = SubmissionOrigin.singleOrigin(transfer.sender.requireAccountIdIn(transfer.originChain)), asset = transfer.originChain.commissionAsset // TODO: Support custom assets for xcm transfers ) diff --git a/feature-swap-api/src/main/java/io/novafoundation/nova/feature_swap_api/domain/model/AtomicSwapOperation.kt b/feature-swap-api/src/main/java/io/novafoundation/nova/feature_swap_api/domain/model/AtomicSwapOperation.kt index 28748f068c..8be6fcd5e9 100644 --- a/feature-swap-api/src/main/java/io/novafoundation/nova/feature_swap_api/domain/model/AtomicSwapOperation.kt +++ b/feature-swap-api/src/main/java/io/novafoundation/nova/feature_swap_api/domain/model/AtomicSwapOperation.kt @@ -1,7 +1,8 @@ package io.novafoundation.nova.feature_swap_api.domain.model import io.novafoundation.nova.feature_account_api.data.fee.FeePaymentCurrency -import io.novafoundation.nova.feature_account_api.data.model.Fee +import io.novafoundation.nova.feature_account_api.data.model.FeeBase +import io.novafoundation.nova.feature_account_api.data.model.SubmissionFee interface AtomicSwapOperation { @@ -15,7 +16,10 @@ class AtomicSwapOperationArgs( val feePaymentCurrency: FeePaymentCurrency, ) -typealias AtomicSwapOperationFee = Fee +class AtomicSwapOperationFee( + val submissionFee: SubmissionFee, + val additionalFees: List = emptyList() +) // TODO this will later be used to perform more accurate non-atomic swaps // So next segments can correct tx args based on outcome of previous segments diff --git a/feature-swap-api/src/main/java/io/novafoundation/nova/feature_swap_api/domain/model/SwapQuote.kt b/feature-swap-api/src/main/java/io/novafoundation/nova/feature_swap_api/domain/model/SwapQuote.kt index 6c2ac64048..c4f857cbc5 100644 --- a/feature-swap-api/src/main/java/io/novafoundation/nova/feature_swap_api/domain/model/SwapQuote.kt +++ b/feature-swap-api/src/main/java/io/novafoundation/nova/feature_swap_api/domain/model/SwapQuote.kt @@ -63,11 +63,11 @@ class SwapFee( ) : GenericFee { val firstSegmentFee: Fee - get() = atomicOperationFees.first() + get() = atomicOperationFees.first().submissionFee // TODO handle multi-segment fee display override val networkFee: Fee - get() = atomicOperationFees.first() + get() = firstSegmentFee } val SwapFee.totalDeductedPlanks: Balance diff --git a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/AssetExchange.kt b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/AssetExchange.kt index a0e7213bf0..09faacfe99 100644 --- a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/AssetExchange.kt +++ b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/AssetExchange.kt @@ -1,5 +1,7 @@ package io.novafoundation.nova.feature_swap_impl.data.assetExchange +import io.novafoundation.nova.feature_account_api.data.extrinsic.ExtrinsicService +import io.novafoundation.nova.feature_account_api.data.fee.FeePaymentProvider import io.novafoundation.nova.feature_account_api.data.fee.capability.CustomFeeCapability import io.novafoundation.nova.feature_account_api.domain.model.MetaAccount import io.novafoundation.nova.feature_swap_api.domain.model.ReQuoteTrigger @@ -16,7 +18,7 @@ interface AssetExchange : CustomFeeCapability { suspend fun create( chain: Chain, - parentQuoter: ParentQuoter, + parentQuoter: SwapHost, coroutineScope: CoroutineScope ): AssetExchange? } @@ -24,22 +26,31 @@ interface AssetExchange : CustomFeeCapability { interface MultiChainFactory { suspend fun create( - parentQuoter: ParentQuoter, + swapHost: SwapHost, coroutineScope: CoroutineScope ): AssetExchange? } - interface ParentQuoter { + interface SwapHost { suspend fun quote(quoteArgs: ParentQuoterArgs): Balance + + suspend fun extrinsicService(): ExtrinsicService } suspend fun sync() suspend fun availableDirectSwapConnections(): List + fun feePaymentOverrides(): List + fun runSubscriptions(metaAccount: MetaAccount): Flow } +data class FeePaymentProviderOverride( + val provider: FeePaymentProvider, + val chain: Chain +) + data class ParentQuoterArgs( val chainAssetIn: Chain.Asset, val chainAssetOut: Chain.Asset, diff --git a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/assetConversion/AssetConversionExchange.kt b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/assetConversion/AssetConversionExchange.kt index 2b13ced626..810b36183a 100644 --- a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/assetConversion/AssetConversionExchange.kt +++ b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/assetConversion/AssetConversionExchange.kt @@ -21,6 +21,7 @@ import io.novafoundation.nova.feature_swap_core.data.assetExchange.conversion.ty import io.novafoundation.nova.feature_swap_core_api.data.primitive.errors.SwapQuoteException import io.novafoundation.nova.feature_swap_core_api.data.primitive.model.SwapDirection import io.novafoundation.nova.feature_swap_impl.data.assetExchange.AssetExchange +import io.novafoundation.nova.feature_swap_impl.data.assetExchange.FeePaymentProviderOverride import io.novafoundation.nova.feature_swap_impl.domain.swap.BaseSwapGraphEdge import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.types.Balance import io.novafoundation.nova.runtime.call.MultiChainRuntimeCallsApi @@ -55,7 +56,7 @@ class AssetConversionExchangeFactory( override suspend fun create( chain: Chain, - parentQuoter: AssetExchange.ParentQuoter, + parentQuoter: AssetExchange.SwapHost, coroutineScope: CoroutineScope ): AssetExchange { val converter = multiLocationConverterFactory.defaultAsync(chain, coroutineScope) @@ -101,6 +102,10 @@ private class AssetConversionExchange( } } + override fun feePaymentOverrides(): List { + return emptyList() + } + override fun runSubscriptions(metaAccount: MetaAccount): Flow { return chainStateRepository.currentBlockNumberFlow(chain.id) .drop(1) // skip immediate value from the cache to not perform double-quote on chain change @@ -198,7 +203,7 @@ private class AssetConversionExchange( ) : AtomicSwapOperation { override suspend fun estimateFee(): AtomicSwapOperationFee { - return extrinsicService.estimateFee( + val submissionFee = extrinsicService.estimateFee( chain = chain, origin = TransactionOrigin.SelectedWallet, submissionOptions = ExtrinsicService.SubmissionOptions( @@ -207,6 +212,8 @@ private class AssetConversionExchange( ) { executeSwap(sendTo = chain.emptyAccountId()) } + + return AtomicSwapOperationFee(submissionFee) } override suspend fun submit(previousStepCorrection: SwapExecutionCorrection?): Result { diff --git a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/compound/CompoundAssetExchange.kt b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/compound/CompoundAssetExchange.kt index 35209d4552..def1eb1ed6 100644 --- a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/compound/CompoundAssetExchange.kt +++ b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/compound/CompoundAssetExchange.kt @@ -6,6 +6,7 @@ import io.novafoundation.nova.feature_account_api.domain.model.MetaAccount import io.novafoundation.nova.feature_swap_api.domain.model.ReQuoteTrigger import io.novafoundation.nova.feature_swap_api.domain.model.SwapGraphEdge import io.novafoundation.nova.feature_swap_impl.data.assetExchange.AssetExchange +import io.novafoundation.nova.feature_swap_impl.data.assetExchange.FeePaymentProviderOverride import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain import kotlinx.coroutines.flow.Flow @@ -21,6 +22,10 @@ class CompoundAssetExchange( return delegates.flatMap { it.availableDirectSwapConnections() } } + override fun feePaymentOverrides(): List { + return delegates.flatMap { it.feePaymentOverrides() } + } + override fun runSubscriptions(metaAccount: MetaAccount): Flow { return delegates.map { it.runSubscriptions(metaAccount) } .mergeIfMultiple() diff --git a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/crossChain/CrossChainTransferAssetExchange.kt b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/crossChain/CrossChainTransferAssetExchange.kt index effb138684..8db1cc1625 100644 --- a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/crossChain/CrossChainTransferAssetExchange.kt +++ b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/crossChain/CrossChainTransferAssetExchange.kt @@ -1,25 +1,27 @@ package io.novafoundation.nova.feature_swap_impl.data.assetExchange.crossChain -import io.novafoundation.nova.common.utils.emptySubstrateAccountId import io.novafoundation.nova.common.utils.graph.Edge -import io.novafoundation.nova.feature_account_api.data.extrinsic.SubmissionOrigin -import io.novafoundation.nova.feature_account_api.data.fee.FeePaymentCurrency -import io.novafoundation.nova.feature_account_api.data.model.SubstrateFee +import io.novafoundation.nova.feature_account_api.domain.interfaces.AccountRepository import io.novafoundation.nova.feature_account_api.domain.model.MetaAccount +import io.novafoundation.nova.feature_account_api.domain.model.requireAddressIn import io.novafoundation.nova.feature_swap_api.domain.model.AtomicSwapOperation import io.novafoundation.nova.feature_swap_api.domain.model.AtomicSwapOperationArgs import io.novafoundation.nova.feature_swap_api.domain.model.AtomicSwapOperationFee import io.novafoundation.nova.feature_swap_api.domain.model.ReQuoteTrigger import io.novafoundation.nova.feature_swap_api.domain.model.SwapExecutionCorrection import io.novafoundation.nova.feature_swap_api.domain.model.SwapGraphEdge +import io.novafoundation.nova.feature_swap_api.domain.model.SwapLimit import io.novafoundation.nova.feature_swap_core.data.assetExchange.conversion.types.hydra.sources.Weights import io.novafoundation.nova.feature_swap_core_api.data.primitive.model.SwapDirection import io.novafoundation.nova.feature_swap_impl.data.assetExchange.AssetExchange +import io.novafoundation.nova.feature_swap_impl.data.assetExchange.FeePaymentProviderOverride +import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.tranfers.AssetTransferBase +import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.types.Balance import io.novafoundation.nova.feature_wallet_api.domain.interfaces.CrossChainTransfersUseCase -import io.novafoundation.nova.runtime.ext.utilityAsset import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain import io.novafoundation.nova.runtime.multiNetwork.chain.model.FullChainAssetId +import io.novafoundation.nova.runtime.multiNetwork.chainWithAsset import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.emptyFlow @@ -27,24 +29,31 @@ import java.math.BigInteger class CrossChainTransferAssetExchangeFactory( private val crossChainTransfersUseCase: CrossChainTransfersUseCase, - private val chainRegistry: ChainRegistry + private val chainRegistry: ChainRegistry, + private val accountRepository: AccountRepository, ) : AssetExchange.MultiChainFactory { override suspend fun create( - parentQuoter: AssetExchange.ParentQuoter, + swapHost: AssetExchange.SwapHost, coroutineScope: CoroutineScope ): AssetExchange { return CrossChainTransferAssetExchange( crossChainTransfersUseCase = crossChainTransfersUseCase, - chainRegistry = chainRegistry + chainRegistry = chainRegistry, + accountRepository = accountRepository, + computationalScope = coroutineScope, + swapHost = swapHost ) } } class CrossChainTransferAssetExchange( private val crossChainTransfersUseCase: CrossChainTransfersUseCase, - private val chainRegistry: ChainRegistry + private val chainRegistry: ChainRegistry, + private val accountRepository: AccountRepository, + private val computationalScope: CoroutineScope, + private val swapHost: AssetExchange.SwapHost, ) : AssetExchange { override suspend fun sync() { @@ -55,6 +64,10 @@ class CrossChainTransferAssetExchange( return crossChainTransfersUseCase.allDirections().map(::CrossChainTransferEdge) } + override fun feePaymentOverrides(): List { + return emptyList() + } + override fun runSubscriptions(metaAccount: MetaAccount): Flow { return emptyFlow() } @@ -93,11 +106,18 @@ class CrossChainTransferAssetExchange( ) : AtomicSwapOperation { override suspend fun estimateFee(): AtomicSwapOperationFee { - // TODO - return SubstrateFee( - amount = BigInteger.ZERO, - submissionOrigin = SubmissionOrigin.singleOrigin(emptySubstrateAccountId()), - asset = paymentAsset() + val transfer = createTransfer(amount = transactionArgs.swapLimit.crossChainTransferAmount) + + val crossChainFee = with(crossChainTransfersUseCase) { + swapHost.extrinsicService().estimateFee(transfer, computationalScope) + } + + return AtomicSwapOperationFee( + submissionFee = crossChainFee.fromOriginInFeeCurrency, + additionalFees = listOfNotNull( + crossChainFee.fromOriginInNativeCurrency, + crossChainFee.fromHoldingRegister + ) ) } @@ -105,11 +125,28 @@ class CrossChainTransferAssetExchange( return Result.failure(UnsupportedOperationException("TODO")) } - private suspend fun paymentAsset(): Chain.Asset { - return when(val currency = transactionArgs.feePaymentCurrency) { - is FeePaymentCurrency.Asset -> currency.asset - FeePaymentCurrency.Native -> chainRegistry.getChain(edge.from.chainId).utilityAsset - } + private suspend fun createTransfer(amount: Balance): AssetTransferBase { + val (originChain, originAsset) = chainRegistry.chainWithAsset(edge.from) + val (destinationChain, destinationAsset) = chainRegistry.chainWithAsset(edge.to) + + val selectedAccount = accountRepository.getSelectedMetaAccount() + + return AssetTransferBase( + recipient = selectedAccount.requireAddressIn(destinationChain), + originChain = originChain, + originChainAsset = originAsset, + destinationChain = destinationChain, + destinationChainAsset = destinationAsset, + feePaymentCurrency = transactionArgs.feePaymentCurrency, + amountPlanks = amount + ) } + + private val SwapLimit.crossChainTransferAmount: Balance + get() = when (this) { + // We cannot use slippage since we cannot guarantee slippage compliance in transfers + is SwapLimit.SpecifiedIn -> amountOutQuote + is SwapLimit.SpecifiedOut -> amountInQuote + } } } diff --git a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/hydraDx/HydraDxExchange.kt b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/hydraDx/HydraDxExchange.kt index b7b59191c6..3b59190765 100644 --- a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/hydraDx/HydraDxExchange.kt +++ b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/hydraDx/HydraDxExchange.kt @@ -38,6 +38,7 @@ import io.novafoundation.nova.feature_swap_core_api.data.primitive.model.SwapDir import io.novafoundation.nova.feature_swap_core_api.data.types.hydra.HydraDxQuoting import io.novafoundation.nova.feature_swap_core_api.data.types.hydra.HydraDxQuotingSource import io.novafoundation.nova.feature_swap_impl.data.assetExchange.AssetExchange +import io.novafoundation.nova.feature_swap_impl.data.assetExchange.FeePaymentProviderOverride import io.novafoundation.nova.feature_swap_impl.data.assetExchange.ParentQuoterArgs import io.novafoundation.nova.feature_swap_impl.data.assetExchange.hydraDx.referrals.HydraDxNovaReferral import io.novafoundation.nova.feature_swap_impl.data.assetExchange.hydraDx.referrals.linkedAccounts @@ -76,20 +77,18 @@ class HydraDxExchangeFactory( private val hydrationFeeInjector: HydrationFeeInjector ) : AssetExchange.SingleChainFactory { - override suspend fun create(chain: Chain, parentQuoter: AssetExchange.ParentQuoter, coroutineScope: CoroutineScope): AssetExchange { + override suspend fun create(chain: Chain, parentQuoter: AssetExchange.SwapHost, coroutineScope: CoroutineScope): AssetExchange { return HydraDxExchange( remoteStorageSource = remoteStorageSource, chain = chain, storageSharedRequestsBuilderFactory = sharedRequestsBuilderFactory, - extrinsicServiceFactory = extrinsicServiceFactory, hydraDxAssetIdConverter = hydraDxAssetIdConverter, hydraDxNovaReferral = hydraDxNovaReferral, swapSourceFactories = swapSourceFactories, assetSourceRegistry = assetSourceRegistry, - parentQuoter = parentQuoter, + swapHost = parentQuoter, hydrationFeeInjector = hydrationFeeInjector, delegate = quotingFactory.create(chain), - coroutineScope = coroutineScope ) } } @@ -99,23 +98,14 @@ private class HydraDxExchange( private val remoteStorageSource: StorageDataSource, private val chain: Chain, private val storageSharedRequestsBuilderFactory: StorageSharedRequestsBuilderFactory, - private val extrinsicServiceFactory: ExtrinsicService.Factory, private val hydraDxAssetIdConverter: HydraDxAssetIdConverter, private val hydraDxNovaReferral: HydraDxNovaReferral, private val swapSourceFactories: Iterable>, private val assetSourceRegistry: AssetSourceRegistry, - private val parentQuoter: AssetExchange.ParentQuoter, + private val swapHost: AssetExchange.SwapHost, private val hydrationFeeInjector: HydrationFeeInjector, - coroutineScope: CoroutineScope, ) : AssetExchange { - private val extrinsicService = extrinsicServiceFactory.create( - ExtrinsicService.FeePaymentConfig( - coroutineScope = coroutineScope, - customFeePaymentProvider = ReusableQuoteFeePaymentProvider() - ) - ) - private val swapSources: List = createSources() private val currentPaymentAsset: MutableSharedFlow = singleReplaySharedFlow() @@ -136,6 +126,15 @@ private class HydraDxExchange( } } + override fun feePaymentOverrides(): List { + return listOf( + FeePaymentProviderOverride( + provider = ReusableQuoteFeePaymentProvider(), + chain = chain + ) + ) + } + override fun runSubscriptions(metaAccount: MetaAccount): Flow { return withFlowScope { scope -> val subscriptionBuilder = storageSharedRequestsBuilderFactory.create(chain.id) @@ -242,7 +241,7 @@ private class HydraDxExchange( } override suspend fun estimateFee(): AtomicSwapOperationFee { - return extrinsicService.estimateFee( + val submissionFee = swapHost.extrinsicService().estimateFee( chain = chain, origin = TransactionOrigin.SelectedWallet, submissionOptions = ExtrinsicService.SubmissionOptions( @@ -252,10 +251,12 @@ private class HydraDxExchange( ) { executeSwap() } + + return AtomicSwapOperationFee(submissionFee) } override suspend fun submit(previousStepCorrection: SwapExecutionCorrection?): Result { - return extrinsicService.submitAndWatchExtrinsic( + return swapHost.extrinsicService().submitAndWatchExtrinsic( chain = chain, origin = TransactionOrigin.SelectedWallet, submissionOptions = ExtrinsicService.SubmissionOptions( @@ -416,7 +417,7 @@ private class HydraDxExchange( swapDirection = SwapDirection.SPECIFIED_OUT ) - val quotedFee = parentQuoter.quote(args) + val quotedFee = swapHost.quote(args) // TODO // There is a issue in Router implementation in Hydra that doesn't allow asset balance to go below ED. We add it to fee for simplicity instead diff --git a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/di/SwapFeatureDependencies.kt b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/di/SwapFeatureDependencies.kt index 54622fc5ab..8796c831fe 100644 --- a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/di/SwapFeatureDependencies.kt +++ b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/di/SwapFeatureDependencies.kt @@ -15,6 +15,7 @@ import io.novafoundation.nova.common.view.input.chooser.ListChooserMixin import io.novafoundation.nova.core.storage.StorageCache import io.novafoundation.nova.core_db.dao.OperationDao import io.novafoundation.nova.feature_account_api.data.extrinsic.ExtrinsicService +import io.novafoundation.nova.feature_account_api.data.fee.FeePaymentProviderRegistry import io.novafoundation.nova.feature_account_api.data.fee.capability.CustomFeeCapabilityFacade import io.novafoundation.nova.feature_account_api.data.fee.types.hydra.HydrationFeeInjector import io.novafoundation.nova.feature_account_api.data.repository.OnChainIdentityRepository @@ -134,4 +135,6 @@ interface SwapFeatureDependencies { val quoterFactory: PathQuoter.Factory val hydrationFeeInjector: HydrationFeeInjector + + val defaultFeePaymentRegistry: FeePaymentProviderRegistry } diff --git a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/di/SwapFeatureModule.kt b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/di/SwapFeatureModule.kt index 28e52ea4ea..4593c7a030 100644 --- a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/di/SwapFeatureModule.kt +++ b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/di/SwapFeatureModule.kt @@ -7,6 +7,8 @@ import io.novafoundation.nova.common.di.scope.FeatureScope import io.novafoundation.nova.common.resources.ResourceManager import io.novafoundation.nova.core.storage.StorageCache import io.novafoundation.nova.core_db.dao.OperationDao +import io.novafoundation.nova.feature_account_api.data.extrinsic.ExtrinsicService +import io.novafoundation.nova.feature_account_api.data.fee.FeePaymentProviderRegistry import io.novafoundation.nova.feature_account_api.data.fee.capability.CustomFeeCapabilityFacade import io.novafoundation.nova.feature_account_api.domain.interfaces.AccountRepository import io.novafoundation.nova.feature_buy_api.domain.BuyTokenRegistry @@ -55,6 +57,8 @@ class SwapFeatureModule { chainRegistry: ChainRegistry, quoterFactory: PathQuoter.Factory, customFeeCapabilityFacade: CustomFeeCapabilityFacade, + extrinsicServiceFactory: ExtrinsicService.Factory, + defaultFeePaymentRegistry: FeePaymentProviderRegistry ): SwapService { return RealSwapService( assetConversionFactory = assetConversionFactory, @@ -63,7 +67,9 @@ class SwapFeatureModule { computationalCache = computationalCache, chainRegistry = chainRegistry, quoterFactory = quoterFactory, - customFeeCapabilityFacade = customFeeCapabilityFacade + customFeeCapabilityFacade = customFeeCapabilityFacade, + extrinsicServiceFactory = extrinsicServiceFactory, + defaultFeePaymentProviderRegistry = defaultFeePaymentRegistry ) } diff --git a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/di/exchanges/CrossChainTransferExchangeModule.kt b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/di/exchanges/CrossChainTransferExchangeModule.kt index 4348bcf697..69f6ad23da 100644 --- a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/di/exchanges/CrossChainTransferExchangeModule.kt +++ b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/di/exchanges/CrossChainTransferExchangeModule.kt @@ -3,6 +3,7 @@ package io.novafoundation.nova.feature_swap_impl.di.exchanges import dagger.Module import dagger.Provides import io.novafoundation.nova.common.di.scope.FeatureScope +import io.novafoundation.nova.feature_account_api.domain.interfaces.AccountRepository import io.novafoundation.nova.feature_swap_impl.data.assetExchange.crossChain.CrossChainTransferAssetExchangeFactory import io.novafoundation.nova.feature_wallet_api.domain.interfaces.CrossChainTransfersUseCase import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry @@ -14,11 +15,13 @@ class CrossChainTransferExchangeModule { @FeatureScope fun provideAssetConversionExchangeFactory( crossChainTransfersUseCase: CrossChainTransfersUseCase, - chainRegistry: ChainRegistry + chainRegistry: ChainRegistry, + accountRepository: AccountRepository ): CrossChainTransferAssetExchangeFactory { return CrossChainTransferAssetExchangeFactory( crossChainTransfersUseCase = crossChainTransfersUseCase, - chainRegistry = chainRegistry + chainRegistry = chainRegistry, + accountRepository = accountRepository ) } } diff --git a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/domain/swap/RealSwapService.kt b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/domain/swap/RealSwapService.kt index 6e8fab5acb..5a58165816 100644 --- a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/domain/swap/RealSwapService.kt +++ b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/domain/swap/RealSwapService.kt @@ -20,7 +20,10 @@ import io.novafoundation.nova.common.utils.mergeIfMultiple import io.novafoundation.nova.common.utils.requireInnerNotNull import io.novafoundation.nova.common.utils.toPercent import io.novafoundation.nova.common.utils.withFlowScope +import io.novafoundation.nova.feature_account_api.data.extrinsic.ExtrinsicService import io.novafoundation.nova.feature_account_api.data.fee.FeePaymentCurrency +import io.novafoundation.nova.feature_account_api.data.fee.FeePaymentProvider +import io.novafoundation.nova.feature_account_api.data.fee.FeePaymentProviderRegistry import io.novafoundation.nova.feature_account_api.data.fee.capability.CustomFeeCapabilityFacade import io.novafoundation.nova.feature_account_api.data.fee.toFeePaymentCurrency import io.novafoundation.nova.feature_account_api.domain.model.MetaAccount @@ -46,6 +49,7 @@ import io.novafoundation.nova.feature_swap_core_api.data.paths.model.lastSegment import io.novafoundation.nova.feature_swap_core_api.data.primitive.model.SwapDirection import io.novafoundation.nova.feature_swap_impl.BuildConfig import io.novafoundation.nova.feature_swap_impl.data.assetExchange.AssetExchange +import io.novafoundation.nova.feature_swap_impl.data.assetExchange.FeePaymentProviderOverride import io.novafoundation.nova.feature_swap_impl.data.assetExchange.ParentQuoterArgs import io.novafoundation.nova.feature_swap_impl.data.assetExchange.assetConversion.AssetConversionExchangeFactory import io.novafoundation.nova.feature_swap_impl.data.assetExchange.compound.CompoundAssetExchange @@ -77,6 +81,7 @@ import kotlin.time.Duration.Companion.milliseconds private const val ALL_DIRECTIONS_CACHE = "RealSwapService.ALL_DIRECTIONS" private const val EXCHANGES_CACHE = "RealSwapService.EXCHANGES" +private const val EXTRINSIC_SERVICE_CACHE = "RealSwapService.ExtrinsicService" private const val QUOTER_CACHE = "RealSwapService.QUOTER" @@ -88,6 +93,8 @@ internal class RealSwapService( private val chainRegistry: ChainRegistry, private val quoterFactory: PathQuoter.Factory, private val customFeeCapabilityFacade: CustomFeeCapabilityFacade, + private val extrinsicServiceFactory: ExtrinsicService.Factory, + private val defaultFeePaymentProviderRegistry: FeePaymentProviderRegistry, private val debug: Boolean = BuildConfig.DEBUG ) : SwapService { @@ -170,8 +177,8 @@ internal class RealSwapService( swapLimit = SwapLimit(direction, quotedEdge.quotedAmount, perSegmentSlippage, quotedEdge.quote), feePaymentCurrency = segmentExecuteArgs.quotedSwapEdge.edge.identifySegmentCurrency( isFirstSegment = index == 0, - firstSegmentFees = firstSegmentFees - ) + firstSegmentFees = firstSegmentFees, + ), ) // Initial case - begin first operation @@ -236,7 +243,7 @@ internal class RealSwapService( return SlippageConfig.default() } - override fun runSubscriptions( metaAccount: MetaAccount): Flow { + override fun runSubscriptions(metaAccount: MetaAccount): Flow { return withFlowScope { scope -> val exchangeRegistry = exchangeRegistry(scope) @@ -308,11 +315,29 @@ internal class RealSwapService( } } + private suspend fun extrinsicService(computationScope: CoroutineScope): ExtrinsicService { + return computationalCache.useCache(EXTRINSIC_SERVICE_CACHE, computationScope) { + createExtrinsicService(this) + } + } + private suspend fun createExchangeRegistry(coroutineScope: CoroutineScope): ExchangeRegistry { return ExchangeRegistry( singleChainExchanges = createIndividualChainExchanges(coroutineScope), multiChainExchanges = listOf( - crossChainTransferFactory.create(InnerParentQuoter(coroutineScope), coroutineScope) + crossChainTransferFactory.create(InnerSwapHost(coroutineScope), coroutineScope) + ) + ) + } + + private suspend fun createExtrinsicService(coroutineScope: CoroutineScope): ExtrinsicService { + val exchangeRegistry = exchangeRegistry(coroutineScope) + val feePaymentRegistry = exchangeRegistry.getFeePaymentRegistry() + + return extrinsicServiceFactory.create( + ExtrinsicService.FeePaymentConfig( + coroutineScope = coroutineScope, + customFeePaymentRegistry = feePaymentRegistry ) ) } @@ -331,7 +356,7 @@ internal class RealSwapService( else -> null } - return factory?.create(chain, InnerParentQuoter(computationScope), computationScope) + return factory?.create(chain, InnerSwapHost(computationScope), computationScope) } // Assumes each flow will have only single element @@ -366,9 +391,9 @@ internal class RealSwapService( } } - private inner class InnerParentQuoter( + private inner class InnerSwapHost( private val computationScope: CoroutineScope - ) : AssetExchange.ParentQuoter { + ) : AssetExchange.SwapHost { override suspend fun quote(quoteArgs: ParentQuoterArgs): Balance { return quoteTrade( @@ -380,14 +405,25 @@ internal class RealSwapService( logQuotes = false ).finalQuote() } + + override suspend fun extrinsicService(): ExtrinsicService { + return extrinsicService(computationScope) + } } private fun logFee(fee: SwapFee) { - val route = fee.atomicOperationFees.joinToString() { - it.amount.formatPlanks(it.asset) + val route = fee.atomicOperationFees.joinToString(separator = "\n") { + val allFees = buildList { + add(it.submissionFee) + addAll(it.additionalFees) + } + + allFees.joinToString { it.amount.formatPlanks(it.asset) } } - Log.d("Swaps", "Fee: $route") + Log.d("Swaps", "---- Fees -----") + Log.d("Swaps", route) + Log.d("Swaps", "---- End Fees -----") } private suspend fun logQuotes(quotedTrades: List) { @@ -419,30 +455,58 @@ internal class RealSwapService( } } - private class ExchangeRegistry( + private inner class ExchangeRegistry( private val singleChainExchanges: Map, private val multiChainExchanges: List, ) { + private val feePaymentRegistry = SwapFeePaymentRegistry() + fun getExchange(chainId: ChainId): AssetExchange { val relevantExchanges = buildList { singleChainExchanges[chainId]?.let { add(it) } addAll(multiChainExchanges) } - return when(relevantExchanges.size) { + return when (relevantExchanges.size) { 0 -> error("No exchanges found") 1 -> relevantExchanges.single() else -> CompoundAssetExchange(relevantExchanges) } } + fun getFeePaymentRegistry(): FeePaymentProviderRegistry { + return feePaymentRegistry + } + fun allExchanges(): List { return buildList { addAll(singleChainExchanges.values) addAll(multiChainExchanges) } } + + private inner class SwapFeePaymentRegistry : FeePaymentProviderRegistry { + + private val paymentRegistryOverrides = createFeePaymentOverrides() + + override suspend fun providerFor(chain: Chain): FeePaymentProvider { + return paymentRegistryOverrides.find { it.chain.id == chain.id }?.provider + ?: defaultFeePaymentProviderRegistry.providerFor(chain) + } + + private fun createFeePaymentOverrides(): List { + return buildList { + singleChainExchanges.values.onEach { singleChainExchange -> + addAll(singleChainExchange.feePaymentOverrides()) + } + + multiChainExchanges.onEach { multiChainExchange -> + addAll(multiChainExchange.feePaymentOverrides()) + } + } + } + } } } 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 740e7cec47..425c5a62d7 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 @@ -1,28 +1,74 @@ package io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.tranfers import io.novafoundation.nova.feature_account_api.data.extrinsic.ExtrinsicSubmission +import io.novafoundation.nova.feature_account_api.data.fee.FeePaymentCurrency +import io.novafoundation.nova.feature_account_api.data.fee.toFeePaymentCurrency 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.data.network.blockhain.types.Balance 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.domain.model.planksFromAmount import io.novafoundation.nova.runtime.ext.accountIdOrNull import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain import io.novasama.substrate_sdk_android.runtime.AccountId +import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.flowOf import java.math.BigDecimal -import kotlinx.coroutines.CoroutineScope -interface AssetTransfer { - val sender: MetaAccount +interface AssetTransferBase { + val recipient: String + val originChain: Chain + val originChainAsset: Chain.Asset + val destinationChain: Chain + val destinationChainAsset: Chain.Asset + + val feePaymentCurrency: FeePaymentCurrency + + val amountPlanks: Balance +} + +// TODO this is too specialized for this module +interface AssetTransfer : AssetTransferBase { + + val sender: MetaAccount + val commissionAssetToken: Token + val amount: BigDecimal + + override val amountPlanks: Balance + get() = originChainAsset.planksFromAmount(amount) + + override val feePaymentCurrency: FeePaymentCurrency + get() = commissionAssetToken.configuration.toFeePaymentCurrency() +} + +fun AssetTransferBase( + recipient: String, + originChain: Chain, + originChainAsset: Chain.Asset, + destinationChain: Chain, + destinationChainAsset: Chain.Asset, + feePaymentCurrency: FeePaymentCurrency, + amountPlanks: Balance +): AssetTransferBase { + return object : AssetTransferBase { + override val recipient: String = recipient + override val originChain: Chain = originChain + override val originChainAsset: Chain.Asset = originChainAsset + override val destinationChain: Chain = destinationChain + override val destinationChainAsset: Chain.Asset = destinationChainAsset + override val feePaymentCurrency: FeePaymentCurrency = feePaymentCurrency + override val amountPlanks: Balance = amountPlanks + } } class BaseAssetTransfer( 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 index 4fc8d6c765..6325248173 100644 --- 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 @@ -1,24 +1,29 @@ package io.novafoundation.nova.feature_wallet_api.data.network.crosschain +import io.novafoundation.nova.common.utils.isZero 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 + val paidByOrigin: Balance = BigInteger.ZERO, + val paidFromHoldingRegister: Balance = BigInteger.ZERO ) { companion object } +fun CrossChainFeeModel.paidByOriginOrNull(): Balance? { + return if (paidByOrigin.isZero) { + null + } else { + paidByOrigin + } +} + fun CrossChainFeeModel.Companion.zero() = CrossChainFeeModel() operator fun CrossChainFeeModel.plus(other: CrossChainFeeModel) = CrossChainFeeModel( - senderPart = senderPart + other.senderPart, - holdingPart = holdingPart + other.holdingPart + paidByOrigin = paidByOrigin + other.paidByOrigin, + paidFromHoldingRegister = paidFromHoldingRegister + other.paidFromHoldingRegister ) -fun CrossChainFeeModel?.orZero() = if (this == null) { - CrossChainFeeModel.zero() -} else { - this -} +fun CrossChainFeeModel?.orZero() = this ?: CrossChainFeeModel.zero() 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 033d7ffd99..37f8680551 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 @@ -1,8 +1,9 @@ package io.novafoundation.nova.feature_wallet_api.data.network.crosschain +import io.novafoundation.nova.feature_account_api.data.extrinsic.ExtrinsicService 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_wallet_api.data.network.blockhain.assets.tranfers.AssetTransfer +import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.tranfers.AssetTransferBase 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 @@ -11,14 +12,14 @@ interface CrossChainTransactor { val validationSystem: AssetTransfersValidationSystem - suspend fun estimateOriginFee( + suspend fun ExtrinsicService.estimateOriginFee( configuration: CrossChainTransferConfiguration, - transfer: AssetTransfer + transfer: AssetTransferBase ): Fee - suspend fun performTransfer( + suspend fun ExtrinsicService.performTransfer( configuration: CrossChainTransferConfiguration, - transfer: AssetTransfer, + transfer: AssetTransferBase, crossChainFee: Balance ): Result } diff --git a/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/domain/interfaces/CrossChainTransfersUseCase.kt b/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/domain/interfaces/CrossChainTransfersUseCase.kt index 2f13a592d5..0c0fe7d258 100644 --- a/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/domain/interfaces/CrossChainTransfersUseCase.kt +++ b/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/domain/interfaces/CrossChainTransfersUseCase.kt @@ -1,10 +1,14 @@ package io.novafoundation.nova.feature_wallet_api.domain.interfaces import io.novafoundation.nova.common.utils.graph.Edge +import io.novafoundation.nova.feature_account_api.data.extrinsic.ExtrinsicService +import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.tranfers.AssetTransferBase import io.novafoundation.nova.feature_wallet_api.domain.model.Asset +import io.novafoundation.nova.feature_wallet_api.domain.model.CrossChainTransferFee import io.novafoundation.nova.runtime.multiNetwork.ChainWithAsset import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain import io.novafoundation.nova.runtime.multiNetwork.chain.model.FullChainAssetId +import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.map @@ -24,6 +28,11 @@ interface CrossChainTransfersUseCase { fun outcomingCrossChainDirectionsFlow(origin: Chain.Asset): Flow> suspend fun allDirections(): List> + + suspend fun ExtrinsicService.estimateFee( + transfer: AssetTransferBase, + computationalScope: CoroutineScope + ): CrossChainTransferFee } fun CrossChainTransfersUseCase.incomingCrossChainDirectionsAvailable(destination: Flow): Flow { diff --git a/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/domain/model/CrossChainFee.kt b/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/domain/model/CrossChainFee.kt new file mode 100644 index 0000000000..b0c381dc9a --- /dev/null +++ b/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/domain/model/CrossChainFee.kt @@ -0,0 +1,27 @@ +package io.novafoundation.nova.feature_wallet_api.domain.model + +import io.novafoundation.nova.feature_account_api.data.fee.FeePaymentCurrency +import io.novafoundation.nova.feature_account_api.data.model.FeeBase +import io.novafoundation.nova.feature_account_api.data.model.SubmissionFee +import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain + +class CrossChainTransferFee( + /** + * Deducted upon initial transaction submission from the origin chain. Asset can be controlled with [FeePaymentCurrency] + */ + val fromOriginInFeeCurrency: SubmissionFee, + + /** + * Deducted upon initial transaction submission from the origin chain, e.g. to pay for delivery fees. Cannot be controlled with [FeePaymentCurrency] + * and is always paid in native currency + * + */ + val fromOriginInNativeCurrency: FeeBase?, + + /** + * Total sum of all execution and delivery fees paid from holding register throughout xcm transfer + * Paid (at the moment) in a sending asset. There might be multiple [Chain.Asset] that represent the same logical asset, + * the asset here indicates the first one, on the origin chain + */ + val fromHoldingRegister: FeeBase, +) 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 1c8532640d..88f46a2b66 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 @@ -10,7 +10,7 @@ import io.novafoundation.nova.feature_account_api.data.extrinsic.ExtrinsicServic 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_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.AssetTransferBase 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.data.network.crosschain.CrossChainTransactor @@ -19,7 +19,6 @@ import io.novafoundation.nova.feature_wallet_api.domain.implementations.accountI import io.novafoundation.nova.feature_wallet_api.domain.implementations.plus import io.novafoundation.nova.feature_wallet_api.domain.model.CrossChainTransferConfiguration 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_impl.data.network.blockchain.assets.transfers.validations.doNotCrossExistentialDepositInUsedAsset @@ -40,11 +39,10 @@ import java.math.BigInteger class RealCrossChainTransactor( private val weigher: CrossChainWeigher, - private val extrinsicService: ExtrinsicService, private val assetSourceRegistry: AssetSourceRegistry, private val phishingValidationFactory: PhishingValidationFactory, private val palletXcmRepository: PalletXcmRepository, - private val enoughTotalToStayAboveEDValidationFactory: EnoughTotalToStayAboveEDValidationFactory + private val enoughTotalToStayAboveEDValidationFactory: EnoughTotalToStayAboveEDValidationFactory, ) : CrossChainTransactor { override val validationSystem: AssetTransfersValidationSystem = ValidationSystem { @@ -70,25 +68,40 @@ class RealCrossChainTransactor( ) } - override suspend fun estimateOriginFee(configuration: CrossChainTransferConfiguration, transfer: AssetTransfer): Fee { - return extrinsicService.estimateFee(transfer.originChain, TransactionOrigin.SelectedWallet) { + override suspend fun ExtrinsicService.estimateOriginFee( + configuration: CrossChainTransferConfiguration, + transfer: AssetTransferBase + ): Fee { + return estimateFee( + chain = transfer.originChain, + origin = TransactionOrigin.SelectedWallet, + submissionOptions = ExtrinsicService.SubmissionOptions( + feePaymentCurrency = transfer.feePaymentCurrency + ) + ) { crossChainTransfer(configuration, transfer, crossChainFee = Balance.ZERO) } } - override suspend fun performTransfer( + override suspend fun ExtrinsicService.performTransfer( configuration: CrossChainTransferConfiguration, - transfer: AssetTransfer, + transfer: AssetTransferBase, crossChainFee: Balance ): Result { - return extrinsicService.submitExtrinsic(transfer.originChain, TransactionOrigin.SelectedWallet) { + return submitExtrinsic( + chain = transfer.originChain, + origin = TransactionOrigin.SelectedWallet, + submissionOptions = ExtrinsicService.SubmissionOptions( + feePaymentCurrency = transfer.feePaymentCurrency + ) + ) { crossChainTransfer(configuration, transfer, crossChainFee) } } private suspend fun ExtrinsicBuilder.crossChainTransfer( configuration: CrossChainTransferConfiguration, - transfer: AssetTransfer, + transfer: AssetTransferBase, crossChainFee: Balance ) { when (configuration.transferType) { @@ -101,7 +114,7 @@ class RealCrossChainTransactor( private suspend fun ExtrinsicBuilder.xTokensTransfer( configuration: CrossChainTransferConfiguration, - assetTransfer: AssetTransfer, + assetTransfer: AssetTransferBase, crossChainFee: Balance ) { val multiAsset = configuration.multiAssetFor(assetTransfer, crossChainFee) @@ -128,7 +141,7 @@ class RealCrossChainTransactor( private fun destWeightEncodable(weight: Weight): Any = weight private suspend fun ExtrinsicBuilder.xcmPalletReserveTransfer( configuration: CrossChainTransferConfiguration, - assetTransfer: AssetTransfer, + assetTransfer: AssetTransferBase, crossChainFee: Balance ) { xcmPalletTransfer( @@ -141,7 +154,7 @@ class RealCrossChainTransactor( private suspend fun ExtrinsicBuilder.xcmPalletTeleport( configuration: CrossChainTransferConfiguration, - assetTransfer: AssetTransfer, + assetTransfer: AssetTransferBase, crossChainFee: Balance ) { xcmPalletTransfer( @@ -154,7 +167,7 @@ class RealCrossChainTransactor( private suspend fun ExtrinsicBuilder.xcmPalletTransfer( configuration: CrossChainTransferConfiguration, - assetTransfer: AssetTransfer, + assetTransfer: AssetTransferBase, crossChainFee: Balance, callName: String ) { @@ -177,16 +190,16 @@ class RealCrossChainTransactor( } private fun CrossChainTransferConfiguration.multiAssetFor( - transfer: AssetTransfer, + transfer: AssetTransferBase, 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 + val planks = transfer.amountPlanks + crossChainFee return XcmMultiAsset.from(assetLocation, planks) } - private fun AssetTransfer.beneficiaryLocation(): MultiLocation { + private fun AssetTransferBase.beneficiaryLocation(): MultiLocation { val accountId = destinationChain.accountIdOrDefault(recipient) return accountId.accountIdToMultiLocation() 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 08d6417287..2f1a2e0c3c 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 @@ -86,7 +86,7 @@ class RealCrossChainWeigher( val maxWeight = feeConfig.estimatedWeight() return when (val mode = feeConfig.to.xcmFeeType.mode) { - is Mode.Proportional -> CrossChainFeeModel(holdingPart = mode.weightToFee(maxWeight)) + is Mode.Proportional -> CrossChainFeeModel(paidFromHoldingRegister = mode.weightToFee(maxWeight)) Mode.Standard -> { val xcmMessage = xcmMessage(feeConfig.to.xcmFeeType.instructions, chain, amount) @@ -95,7 +95,7 @@ class RealCrossChainWeigher( xcmExecute(xcmMessage, maxWeight) } - CrossChainFeeModel(holdingPart = paymentInfo.partialFee) + CrossChainFeeModel(paidFromHoldingRegister = paymentInfo.partialFee) } Mode.Unknown -> CrossChainFeeModel.zero() @@ -122,9 +122,9 @@ class RealCrossChainWeigher( val isSenderPaysOriginDelivery = !deliveryConfig.alwaysHoldingPays return if (isSenderPaysOriginDelivery && isSendingFromOrigin) { - CrossChainFeeModel(senderPart = deliveryFee) + CrossChainFeeModel(paidByOrigin = deliveryFee) } else { - CrossChainFeeModel(holdingPart = deliveryFee) + CrossChainFeeModel(paidFromHoldingRegister = deliveryFee) } } diff --git a/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/di/WalletFeatureDependencies.kt b/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/di/WalletFeatureDependencies.kt index aadc4385fb..ebd117e44d 100644 --- a/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/di/WalletFeatureDependencies.kt +++ b/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/di/WalletFeatureDependencies.kt @@ -48,6 +48,7 @@ import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry import io.novafoundation.nova.runtime.multiNetwork.multiLocation.converter.MultiLocationConverterFactory import io.novafoundation.nova.runtime.multiNetwork.runtime.repository.EventsRepository import io.novafoundation.nova.runtime.network.rpc.RpcCalls +import io.novafoundation.nova.runtime.repository.ParachainInfoRepository import io.novafoundation.nova.runtime.storage.source.StorageDataSource import io.novasama.substrate_sdk_android.encrypt.Signer import io.novasama.substrate_sdk_android.icon.IconGenerator @@ -161,4 +162,6 @@ interface WalletFeatureDependencies { val actionAwaitableMixinFactory: ActionAwaitableMixin.Factory val customFeeCapabilityFacade: CustomFeeCapabilityFacade + + val parachainInfoRepository: ParachainInfoRepository } 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 53ddc14d29..fe90c4cad5 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 @@ -22,10 +22,10 @@ import io.novafoundation.nova.core_db.dao.PhishingAddressDao import io.novafoundation.nova.core_db.dao.TokenDao import io.novafoundation.nova.feature_account_api.data.extrinsic.ExtrinsicService import io.novafoundation.nova.feature_account_api.data.fee.FeePaymentProviderRegistry +import io.novafoundation.nova.feature_account_api.data.fee.capability.CustomFeeCapabilityFacade import io.novafoundation.nova.feature_account_api.domain.interfaces.AccountRepository import io.novafoundation.nova.feature_account_api.domain.updaters.AccountUpdateScope import io.novafoundation.nova.feature_currency_api.domain.interfaces.CurrencyRepository -import io.novafoundation.nova.feature_account_api.data.fee.capability.CustomFeeCapabilityFacade import io.novafoundation.nova.feature_swap_core_api.data.network.HydraDxAssetIdConverter import io.novafoundation.nova.feature_wallet_api.data.cache.AssetCache import io.novafoundation.nova.feature_wallet_api.data.cache.CoinPriceLocalDataSourceImpl @@ -88,6 +88,7 @@ import io.novafoundation.nova.runtime.extrinsic.visitor.api.ExtrinsicWalk import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry import io.novafoundation.nova.runtime.multiNetwork.multiLocation.converter.MultiLocationConverterFactory import io.novafoundation.nova.runtime.multiNetwork.runtime.repository.EventsRepository +import io.novafoundation.nova.runtime.repository.ParachainInfoRepository import io.novafoundation.nova.runtime.storage.source.StorageDataSource import javax.inject.Named @@ -285,14 +286,12 @@ class WalletFeatureModule { @FeatureScope fun provideCrossChainTransactor( weigher: CrossChainWeigher, - extrinsicService: ExtrinsicService, assetSourceRegistry: AssetSourceRegistry, phishingValidationFactory: PhishingValidationFactory, palletXcmRepository: PalletXcmRepository, enoughTotalToStayAboveEDValidationFactory: EnoughTotalToStayAboveEDValidationFactory ): CrossChainTransactor = RealCrossChainTransactor( weigher = weigher, - extrinsicService = extrinsicService, assetSourceRegistry = assetSourceRegistry, phishingValidationFactory = phishingValidationFactory, palletXcmRepository = palletXcmRepository, @@ -365,13 +364,19 @@ class WalletFeatureModule { chainRegistry: ChainRegistry, accountRepository: AccountRepository, computationalCache: ComputationalCache, + crossChainWeigher: CrossChainWeigher, + crossChainTransactor: CrossChainTransactor, + parachainInfoRepository: ParachainInfoRepository, ): CrossChainTransfersUseCase { return RealCrossChainTransfersUseCase( crossChainTransfersRepository = crossChainTransfersRepository, walletRepository = walletRepository, chainRegistry = chainRegistry, accountRepository = accountRepository, - computationalCache = computationalCache + computationalCache = computationalCache, + crossChainWeigher = crossChainWeigher, + crossChainTransactor = crossChainTransactor, + parachainInfoRepository = parachainInfoRepository ) } diff --git a/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/domain/RealCrossChainTransfersUseCase.kt b/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/domain/RealCrossChainTransfersUseCase.kt index ef1bc43173..32b49bb4d6 100644 --- a/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/domain/RealCrossChainTransfersUseCase.kt +++ b/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/domain/RealCrossChainTransfersUseCase.kt @@ -5,20 +5,33 @@ import io.novafoundation.nova.common.utils.combineToPair import io.novafoundation.nova.common.utils.graph.Edge import io.novafoundation.nova.common.utils.isPositive import io.novafoundation.nova.common.utils.withFlowScope +import io.novafoundation.nova.feature_account_api.data.extrinsic.ExtrinsicService +import io.novafoundation.nova.feature_account_api.data.model.SubstrateFee +import io.novafoundation.nova.feature_account_api.data.model.SubstrateFeeBase import io.novafoundation.nova.feature_account_api.domain.interfaces.AccountRepository +import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.tranfers.AssetTransferBase +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.data.network.crosschain.paidByOriginOrNull import io.novafoundation.nova.feature_wallet_api.domain.implementations.availableInDestinations import io.novafoundation.nova.feature_wallet_api.domain.implementations.availableOutDestinations +import io.novafoundation.nova.feature_wallet_api.domain.implementations.transferConfiguration import io.novafoundation.nova.feature_wallet_api.domain.interfaces.CrossChainTransfersUseCase import io.novafoundation.nova.feature_wallet_api.domain.interfaces.IncomingDirection import io.novafoundation.nova.feature_wallet_api.domain.interfaces.OutcomingDirection import io.novafoundation.nova.feature_wallet_api.domain.interfaces.WalletRepository +import io.novafoundation.nova.feature_wallet_api.domain.model.CrossChainTransferFee +import io.novafoundation.nova.feature_wallet_api.domain.model.CrossChainTransfersConfiguration +import io.novafoundation.nova.runtime.ext.commissionAsset import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry import io.novafoundation.nova.runtime.multiNetwork.ChainWithAsset import io.novafoundation.nova.runtime.multiNetwork.assets import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain import io.novafoundation.nova.runtime.multiNetwork.chain.model.FullChainAssetId import io.novafoundation.nova.runtime.multiNetwork.chainsById +import io.novafoundation.nova.runtime.repository.ParachainInfoRepository +import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.first @@ -28,6 +41,7 @@ import kotlinx.coroutines.flow.map import kotlinx.coroutines.launch private const val INCOMING_DIRECTIONS = "RealCrossChainTransfersUseCase.INCOMING_DIRECTIONS" +private const val CONFIGURATION_CACHE = "RealCrossChainTransfersUseCase.CONFIGURATION" internal class RealCrossChainTransfersUseCase( private val crossChainTransfersRepository: CrossChainTransfersRepository, @@ -35,6 +49,9 @@ internal class RealCrossChainTransfersUseCase( private val chainRegistry: ChainRegistry, private val accountRepository: AccountRepository, private val computationalCache: ComputationalCache, + private val crossChainWeigher: CrossChainWeigher, + private val crossChainTransactor: CrossChainTransactor, + private val parachainInfoRepository: ParachainInfoRepository, ) : CrossChainTransfersUseCase { override suspend fun syncCrossChainConfig() { @@ -46,7 +63,7 @@ internal class RealCrossChainTransfersUseCase( computationalCache.useSharedFlow(INCOMING_DIRECTIONS, scope) { scope.launch { crossChainTransfersRepository.syncConfiguration() } - combineToPair(destination, crossChainTransfersRepository.configurationFlow()).flatMapLatest { (destinationAsset, crossChainConfig) -> + combineToPair(destination, cachedConfigurationFlow(scope)).flatMapLatest { (destinationAsset, crossChainConfig) -> if (destinationAsset == null) return@flatMapLatest flowOf(emptyList()) val selectedMetaAccountId = accountRepository.getSelectedMetaAccount().id @@ -86,4 +103,40 @@ internal class RealCrossChainTransfersUseCase( val config = crossChainTransfersRepository.getConfiguration() return config.availableInDestinations() } + + override suspend fun ExtrinsicService.estimateFee( + transfer: AssetTransferBase, + computationalScope: CoroutineScope + ): CrossChainTransferFee { + val configuration = cachedConfigurationFlow(computationalScope).first() + val transferConfiguration = configuration.transferConfiguration( + originChain = transfer.originChain, + originAsset = transfer.originChainAsset, + destinationChain = transfer.destinationChain, + destinationParaId = parachainInfoRepository.paraId(transfer.destinationChain.id) + )!! + + val originFee = with(crossChainTransactor) { + estimateOriginFee(transferConfiguration, transfer) + } + + val crossChainFee = crossChainWeigher.estimateFee(transfer.amountPlanks, transferConfiguration) + + return CrossChainTransferFee( + fromOriginInFeeCurrency = originFee, + fromOriginInNativeCurrency = crossChainFee.paidByOriginOrNull()?.let { + SubstrateFee(it, originFee.submissionOrigin, transfer.originChain.commissionAsset) + }, + fromHoldingRegister = SubstrateFeeBase( + amount = crossChainFee.paidFromHoldingRegister, + asset = transfer.originChainAsset, + ), + ) + } + + private fun cachedConfigurationFlow(computationScope: CoroutineScope): Flow { + return computationalCache.useSharedFlow(CONFIGURATION_CACHE, computationScope) { + crossChainTransfersRepository.configurationFlow() + } + } } From 1205c0d02d6c9ea08d469765bb73a4399a581f86 Mon Sep 17 00:00:00 2001 From: Valentun Date: Fri, 11 Oct 2024 12:57:16 +0300 Subject: [PATCH 16/83] Determine ability to pay initial submission fee based on extrinsic service --- .../feature_swap_impl/data/assetExchange/AssetExchange.kt | 3 +-- .../assetConversion/AssetConversionExchange.kt | 5 ----- .../data/assetExchange/compound/CompoundAssetExchange.kt | 5 ----- .../crossChain/CrossChainTransferAssetExchange.kt | 5 ----- .../data/assetExchange/hydraDx/HydraDxExchange.kt | 4 ---- .../nova/feature_swap_impl/domain/swap/RealSwapService.kt | 8 ++++++-- 6 files changed, 7 insertions(+), 23 deletions(-) diff --git a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/AssetExchange.kt b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/AssetExchange.kt index 09faacfe99..da801b087e 100644 --- a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/AssetExchange.kt +++ b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/AssetExchange.kt @@ -2,7 +2,6 @@ package io.novafoundation.nova.feature_swap_impl.data.assetExchange import io.novafoundation.nova.feature_account_api.data.extrinsic.ExtrinsicService import io.novafoundation.nova.feature_account_api.data.fee.FeePaymentProvider -import io.novafoundation.nova.feature_account_api.data.fee.capability.CustomFeeCapability import io.novafoundation.nova.feature_account_api.domain.model.MetaAccount import io.novafoundation.nova.feature_swap_api.domain.model.ReQuoteTrigger import io.novafoundation.nova.feature_swap_api.domain.model.SwapGraphEdge @@ -12,7 +11,7 @@ import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.Flow -interface AssetExchange : CustomFeeCapability { +interface AssetExchange { interface SingleChainFactory { diff --git a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/assetConversion/AssetConversionExchange.kt b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/assetConversion/AssetConversionExchange.kt index 810b36183a..a96722f4b4 100644 --- a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/assetConversion/AssetConversionExchange.kt +++ b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/assetConversion/AssetConversionExchange.kt @@ -89,11 +89,6 @@ private class AssetConversionExchange( // nothing to sync } - override suspend fun canPayFeeInNonUtilityToken(chainAsset: Chain.Asset): Boolean { - // any asset is usable as a fee as soon as it has associated pool - return true - } - override suspend fun availableDirectSwapConnections(): List { return remoteStorageSource.query(chain.id) { val allPools = metadata.assetConversionOrNull?.pools?.keys().orEmpty() diff --git a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/compound/CompoundAssetExchange.kt b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/compound/CompoundAssetExchange.kt index def1eb1ed6..c146b3a816 100644 --- a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/compound/CompoundAssetExchange.kt +++ b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/compound/CompoundAssetExchange.kt @@ -7,7 +7,6 @@ import io.novafoundation.nova.feature_swap_api.domain.model.ReQuoteTrigger import io.novafoundation.nova.feature_swap_api.domain.model.SwapGraphEdge import io.novafoundation.nova.feature_swap_impl.data.assetExchange.AssetExchange import io.novafoundation.nova.feature_swap_impl.data.assetExchange.FeePaymentProviderOverride -import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain import kotlinx.coroutines.flow.Flow class CompoundAssetExchange( @@ -30,8 +29,4 @@ class CompoundAssetExchange( return delegates.map { it.runSubscriptions(metaAccount) } .mergeIfMultiple() } - - override suspend fun canPayFeeInNonUtilityToken(chainAsset: Chain.Asset): Boolean { - return delegates.all { it.canPayFeeInNonUtilityToken(chainAsset) } - } } diff --git a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/crossChain/CrossChainTransferAssetExchange.kt b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/crossChain/CrossChainTransferAssetExchange.kt index 8db1cc1625..1757504dab 100644 --- a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/crossChain/CrossChainTransferAssetExchange.kt +++ b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/crossChain/CrossChainTransferAssetExchange.kt @@ -19,7 +19,6 @@ import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.t import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.types.Balance import io.novafoundation.nova.feature_wallet_api.domain.interfaces.CrossChainTransfersUseCase import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry -import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain import io.novafoundation.nova.runtime.multiNetwork.chain.model.FullChainAssetId import io.novafoundation.nova.runtime.multiNetwork.chainWithAsset import kotlinx.coroutines.CoroutineScope @@ -72,10 +71,6 @@ class CrossChainTransferAssetExchange( return emptyFlow() } - override suspend fun canPayFeeInNonUtilityToken(chainAsset: Chain.Asset): Boolean { - return false - } - inner class CrossChainTransferEdge( val delegate: Edge ) : SwapGraphEdge, Edge by delegate { diff --git a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/hydraDx/HydraDxExchange.kt b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/hydraDx/HydraDxExchange.kt index 3b59190765..b5104b79eb 100644 --- a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/hydraDx/HydraDxExchange.kt +++ b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/hydraDx/HydraDxExchange.kt @@ -116,10 +116,6 @@ private class HydraDxExchange( return swapSources.forEachAsync { it.sync() } } - override suspend fun canPayFeeInNonUtilityToken(chainAsset: Chain.Asset): Boolean { - return delegate.canPayFeeInNonUtilityToken(chainAsset) - } - override suspend fun availableDirectSwapConnections(): List { return swapSources.flatMapAsync { source -> source.availableSwapDirections().map(::HydraDxSwapEdge) diff --git a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/domain/swap/RealSwapService.kt b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/domain/swap/RealSwapService.kt index 5a58165816..9bf060f5ea 100644 --- a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/domain/swap/RealSwapService.kt +++ b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/domain/swap/RealSwapService.kt @@ -100,9 +100,13 @@ internal class RealSwapService( override suspend fun canPayFeeInNonUtilityAsset(asset: Chain.Asset): Boolean = withContext(Dispatchers.Default) { val computationScope = CoroutineScope(coroutineContext) + val exchangeRegistry = exchangeRegistry(computationScope) + val paymentRegistry = exchangeRegistry.getFeePaymentRegistry() - val exchange = exchangeRegistry(computationScope).getExchange(asset.chainId) - customFeeCapabilityFacade.canPayFeeInNonUtilityToken(asset, exchange) + val chain = chainRegistry.getChain(asset.chainId) + val feePayment = paymentRegistry.providerFor(chain).feePaymentFor(asset.toFeePaymentCurrency(), computationScope) + + customFeeCapabilityFacade.canPayFeeInNonUtilityToken(asset, feePayment) } override suspend fun sync(coroutineScope: CoroutineScope) { From bcfb482319527050aa5ea1d1964f92235e0f8387 Mon Sep 17 00:00:00 2001 From: Valentun Date: Wed, 16 Oct 2024 16:18:25 +0300 Subject: [PATCH 17/83] Filter out directions that do not support fee payment --- .../nova/common/utils/graph/Graph.kt | 52 ++++++++----- .../data/fee/FeePayment.kt | 9 ++- .../capability/CustomFeeAvailabilityFacade.kt | 7 +- .../data/extrinsic/RealExtrinsicService.kt | 8 +- .../data/fee/FeePaymentProviderRegistry.kt | 11 +-- .../fee/chains/AssetHubFeePaymentProvider.kt | 36 +++++++-- .../fee/chains/DefaultFeePaymentProvider.kt | 13 ++++ .../fee/chains/HydrationFeePaymentProvider.kt | 21 ++++-- .../AssetConversionFeePayment.kt | 31 ++------ .../AssetHubFastLookupFeeCapability.kt | 24 ++++++ .../assetHub/AssetHubFeePaymentFetcher.kt | 60 +++++++++++++++ .../di/modules/CustomFeeModule.kt | 32 +++++--- .../data/paths/PathQuoter.kt | 4 +- .../domain/paths/RealPathQuoter.kt | 18 +++-- .../compound/CompoundAssetExchange.kt | 32 -------- .../assetExchange/hydraDx/HydraDxExchange.kt | 30 ++++++++ .../domain/swap/RealSwapService.kt | 75 ++++++++++++++----- .../domain/fee/RealCustomFeeInteractor.kt | 7 +- .../nova/runtime/ext/ChainExt.kt | 3 + .../ForeignAssetsLocationConverter.kt | 11 +-- .../MultiLocationConverterFactory.kt | 16 +++- 21 files changed, 342 insertions(+), 158 deletions(-) rename feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/data/fee/types/{ => assetHub}/AssetConversionFeePayment.kt (76%) create mode 100644 feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/data/fee/types/assetHub/AssetHubFastLookupFeeCapability.kt create mode 100644 feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/data/fee/types/assetHub/AssetHubFeePaymentFetcher.kt delete mode 100644 feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/compound/CompoundAssetExchange.kt diff --git a/common/src/main/java/io/novafoundation/nova/common/utils/graph/Graph.kt b/common/src/main/java/io/novafoundation/nova/common/utils/graph/Graph.kt index fd76e36359..eb6e8d7a79 100644 --- a/common/src/main/java/io/novafoundation/nova/common/utils/graph/Graph.kt +++ b/common/src/main/java/io/novafoundation/nova/common/utils/graph/Graph.kt @@ -1,6 +1,5 @@ package io.novafoundation.nova.common.utils.graph -import android.util.Log import io.novafoundation.nova.common.utils.MultiMapList import java.util.PriorityQueue @@ -31,6 +30,15 @@ fun Graph.vertices(): Set { return adjacencyList.keys } +fun Graph<*, *>.numberOfEdges(): Int { + return adjacencyList.values.sumOf { it.size } +} + +fun interface NodeVisitFilter { + + suspend fun shouldVisit(node: N): Boolean +} + /** * Finds all nodes reachable from [origin] * @@ -38,8 +46,13 @@ fun Graph.vertices(): Set { * * Complexity: O(V + E) */ -fun > Graph.findAllPossibleDestinations(origin: N): Set { - return reachabilityDfs(origin, adjacencyList).toSet() +suspend fun > Graph.findAllPossibleDestinations( + origin: N, + nodeVisitFilter: NodeVisitFilter? = null +): Set { + val actualNodeListFilter = nodeVisitFilter ?: NodeVisitFilter { true } + + return reachabilityDfs(origin, adjacencyList, actualNodeListFilter).toSet() } fun > Graph.hasOutcomingDirections(origin: N): Boolean { @@ -48,9 +61,14 @@ fun > Graph.hasOutcomingDirections(origin: N): Boolean { } -fun > Graph.findDijkstraPathsBetween( - from: N, to: N, limit: Int +suspend fun > Graph.findDijkstraPathsBetween( + from: N, + to: N, + limit: Int, + nodeVisitFilter: NodeVisitFilter? ): List> { + val actualNodeListFilter = nodeVisitFilter ?: NodeVisitFilter { true } + data class QueueElement(val currentPath: Path, val score: Int) : Comparable { override fun compareTo(other: QueueElement): Int { @@ -88,19 +106,12 @@ fun > Graph.findDijkstraPathsBetween( if (newCount <= limit) { adjacencyList.getValue(lastNode).forEach { edge -> - if (edge.to in minimumQueueElement) return@forEach - - val newElement: QueueElement + if (edge.to in minimumQueueElement || !actualNodeListFilter.shouldVisit(edge.to)) return@forEach - try { - newElement = QueueElement( - currentPath = minimumQueueElement.currentPath + edge, - score = minimumQueueElement.score + edge.weight - ) - } catch (e: AbstractMethodError) { - Log.e("Swaps", "Asbract method in ${edge::class.java}") - throw e - } + val newElement = QueueElement( + currentPath = minimumQueueElement.currentPath + edge, + score = minimumQueueElement.score + edge.weight + ) heap.add(newElement) } @@ -110,9 +121,10 @@ fun > Graph.findDijkstraPathsBetween( return paths } -private fun > reachabilityDfs( +private suspend fun > reachabilityDfs( node: N, adjacencyList: Map>, + nodeVisitFilter: NodeVisitFilter, visited: MutableSet = mutableSetOf(), connectedComponentState: MutableList = mutableListOf() ): List { @@ -120,8 +132,8 @@ private fun > reachabilityDfs( connectedComponentState.add(node) for (edge in adjacencyList.getValue(node)) { - if (edge.to !in visited) { - reachabilityDfs(edge.to, adjacencyList, visited, connectedComponentState) + if (edge.to !in visited && nodeVisitFilter.shouldVisit(node)) { + reachabilityDfs(edge.to, adjacencyList, nodeVisitFilter, visited, connectedComponentState) } } diff --git a/feature-account-api/src/main/java/io/novafoundation/nova/feature_account_api/data/fee/FeePayment.kt b/feature-account-api/src/main/java/io/novafoundation/nova/feature_account_api/data/fee/FeePayment.kt index c5b548f8dd..aca3d30cd6 100644 --- a/feature-account-api/src/main/java/io/novafoundation/nova/feature_account_api/data/fee/FeePayment.kt +++ b/feature-account-api/src/main/java/io/novafoundation/nova/feature_account_api/data/fee/FeePayment.kt @@ -1,8 +1,9 @@ package io.novafoundation.nova.feature_account_api.data.fee -import io.novafoundation.nova.feature_account_api.data.model.Fee import io.novafoundation.nova.feature_account_api.data.fee.capability.CustomFeeCapability -import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain +import io.novafoundation.nova.feature_account_api.data.fee.capability.FastLookupCustomFeeCapability +import io.novafoundation.nova.feature_account_api.data.model.Fee +import io.novafoundation.nova.runtime.multiNetwork.chain.model.ChainId import io.novasama.substrate_sdk_android.runtime.extrinsic.ExtrinsicBuilder import kotlinx.coroutines.CoroutineScope @@ -16,9 +17,11 @@ interface FeePayment : CustomFeeCapability { interface FeePaymentProvider { suspend fun feePaymentFor(feePaymentCurrency: FeePaymentCurrency, coroutineScope: CoroutineScope?): FeePayment + + suspend fun fastLookupCustomFeeCapability(): FastLookupCustomFeeCapability? } interface FeePaymentProviderRegistry { - suspend fun providerFor(chain: Chain): FeePaymentProvider + suspend fun providerFor(chainId: ChainId): FeePaymentProvider } diff --git a/feature-account-api/src/main/java/io/novafoundation/nova/feature_account_api/data/fee/capability/CustomFeeAvailabilityFacade.kt b/feature-account-api/src/main/java/io/novafoundation/nova/feature_account_api/data/fee/capability/CustomFeeAvailabilityFacade.kt index abfe77cc1c..aa8d694eb2 100644 --- a/feature-account-api/src/main/java/io/novafoundation/nova/feature_account_api/data/fee/capability/CustomFeeAvailabilityFacade.kt +++ b/feature-account-api/src/main/java/io/novafoundation/nova/feature_account_api/data/fee/capability/CustomFeeAvailabilityFacade.kt @@ -1,17 +1,22 @@ package io.novafoundation.nova.feature_account_api.data.fee.capability import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain +import io.novafoundation.nova.runtime.multiNetwork.chain.model.ChainAssetId interface CustomFeeCapability { /** * Implementations should expect `asset` to be non-utility asset, * e.g. they don't need to additionally check whether asset is utility or not - * They can also expect this method is called only when asset is present in [AssetExchange.availableDirectSwapConnections] */ suspend fun canPayFeeInNonUtilityToken(chainAsset: Chain.Asset): Boolean } +interface FastLookupCustomFeeCapability { + + suspend fun canPayFeeInNonUtilityToken(chainAssetId: ChainAssetId): Boolean +} + interface CustomFeeCapabilityFacade { suspend fun canPayFeeInNonUtilityToken(chainAsset: Chain.Asset, customFeeCapability: CustomFeeCapability): Boolean diff --git a/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/data/extrinsic/RealExtrinsicService.kt b/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/data/extrinsic/RealExtrinsicService.kt index 7f7c00370a..6b31989f87 100644 --- a/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/data/extrinsic/RealExtrinsicService.kt +++ b/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/data/extrinsic/RealExtrinsicService.kt @@ -135,7 +135,7 @@ class RealExtrinsicService( ): Fee { val nativeFee = estimateNativeFee(chain, extrinsic, usedSigner.submissionOrigin(chain)) - val feePaymentProvider = feePaymentProviderRegistry.providerFor(chain) + val feePaymentProvider = feePaymentProviderRegistry.providerFor(chain.id) val feePayment = feePaymentProvider.feePaymentFor(submissionOptions.feePaymentCurrency, coroutineScope) return feePayment.convertNativeFee(nativeFee) @@ -176,7 +176,7 @@ class RealExtrinsicService( submissionOptions.feePaymentCurrency.toFeePaymentAsset(chain) ) - val feePaymentProvider = feePaymentProviderRegistry.providerFor(chain) + val feePaymentProvider = feePaymentProviderRegistry.providerFor(chain.id) val feePayment = feePaymentProvider.feePaymentFor(submissionOptions.feePaymentCurrency, coroutineScope) return feePayment.convertNativeFee(totalNativeFee) @@ -216,7 +216,7 @@ class RealExtrinsicService( val callBuilder = SimpleCallBuilder(runtime).apply { formExtrinsic() } val splitCalls = extrinsicSplitter.split(feeSigner, callBuilder, chain) - val feePaymentProvider = feePaymentProviderRegistry.providerFor(chain) + val feePaymentProvider = feePaymentProviderRegistry.providerFor(chain.id) val feePayment = feePaymentProvider.feePaymentFor(submissionOptions.feePaymentCurrency, coroutineScope) val extrinsicBuilderIterator = extrinsicBuilderSequence.iterator() @@ -250,7 +250,7 @@ class RealExtrinsicService( val extrinsicBuilder = extrinsicBuilderFactory.create(chain, signer, requestedOrigin) extrinsicBuilder.formExtrinsic(submissionOrigin) - val feePaymentProvider = feePaymentProviderRegistry.providerFor(chain) + val feePaymentProvider = feePaymentProviderRegistry.providerFor(chain.id) val feePayment = feePaymentProvider.feePaymentFor(submissionOptions.feePaymentCurrency, coroutineScope) feePayment.modifyExtrinsic(extrinsicBuilder) diff --git a/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/data/fee/FeePaymentProviderRegistry.kt b/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/data/fee/FeePaymentProviderRegistry.kt index 741b50e489..79408ceb7a 100644 --- a/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/data/fee/FeePaymentProviderRegistry.kt +++ b/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/data/fee/FeePaymentProviderRegistry.kt @@ -2,21 +2,22 @@ package io.novafoundation.nova.feature_account_impl.data.fee import io.novafoundation.nova.feature_account_api.data.fee.FeePaymentProvider import io.novafoundation.nova.feature_account_api.data.fee.FeePaymentProviderRegistry -import io.novafoundation.nova.feature_account_impl.data.fee.chains.AssetHubFeePaymentProvider +import io.novafoundation.nova.feature_account_impl.data.fee.chains.AssetHubFeePaymentProviderFactory import io.novafoundation.nova.feature_account_impl.data.fee.chains.DefaultFeePaymentProvider import io.novafoundation.nova.feature_account_impl.data.fee.chains.HydrationFeePaymentProvider 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 internal class RealFeePaymentProviderRegistry( private val default: DefaultFeePaymentProvider, - private val assetHub: AssetHubFeePaymentProvider, + private val assetHubFactory: AssetHubFeePaymentProviderFactory, private val hydration: HydrationFeePaymentProvider ) : FeePaymentProviderRegistry { - override suspend fun providerFor(chain: Chain): FeePaymentProvider { - return when (chain.id) { - Chain.Geneses.POLKADOT_ASSET_HUB -> assetHub + override suspend fun providerFor(chainId: ChainId): FeePaymentProvider { + return when (chainId) { + Chain.Geneses.POLKADOT_ASSET_HUB -> assetHubFactory.create(chainId) Chain.Geneses.HYDRA_DX -> hydration else -> default } diff --git a/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/data/fee/chains/AssetHubFeePaymentProvider.kt b/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/data/fee/chains/AssetHubFeePaymentProvider.kt index fc71731bbc..4855c18ae1 100644 --- a/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/data/fee/chains/AssetHubFeePaymentProvider.kt +++ b/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/data/fee/chains/AssetHubFeePaymentProvider.kt @@ -1,29 +1,51 @@ package io.novafoundation.nova.feature_account_impl.data.fee.chains import io.novafoundation.nova.feature_account_api.data.fee.FeePayment +import io.novafoundation.nova.feature_account_api.data.fee.capability.FastLookupCustomFeeCapability import io.novafoundation.nova.feature_account_api.data.fee.chains.CustomOrNativeFeePaymentProvider -import io.novafoundation.nova.feature_account_impl.data.fee.types.AssetConversionFeePayment +import io.novafoundation.nova.feature_account_impl.data.fee.types.assetHub.AssetConversionFeePayment +import io.novafoundation.nova.feature_account_impl.data.fee.types.assetHub.AssetHubFastLookupFeeCapability +import io.novafoundation.nova.feature_account_impl.data.fee.types.assetHub.AssetHubFeePaymentAssetsFetcherFactory import io.novafoundation.nova.runtime.call.MultiChainRuntimeCallsApi 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.multiLocation.converter.MultiLocationConverterFactory -import io.novafoundation.nova.runtime.storage.source.StorageDataSource import kotlinx.coroutines.CoroutineScope +class AssetHubFeePaymentProviderFactory( + private val multiChainRuntimeCallsApi: MultiChainRuntimeCallsApi, + private val multiLocationConverterFactory: MultiLocationConverterFactory, + private val assetHubFeePaymentAssetsFetcher: AssetHubFeePaymentAssetsFetcherFactory, + private val chainRegistry: ChainRegistry +) { + + suspend fun create(chainId: ChainId): AssetHubFeePaymentProvider { + val chain = chainRegistry.getChain(chainId) + return AssetHubFeePaymentProvider(chain, multiChainRuntimeCallsApi, multiLocationConverterFactory, assetHubFeePaymentAssetsFetcher) + } +} + class AssetHubFeePaymentProvider( - private val chainRegistry: ChainRegistry, + private val chain: Chain, private val multiChainRuntimeCallsApi: MultiChainRuntimeCallsApi, - private val remoteStorageSource: StorageDataSource, private val multiLocationConverterFactory: MultiLocationConverterFactory, + private val assetHubFeePaymentAssetsFetcher: AssetHubFeePaymentAssetsFetcherFactory ) : CustomOrNativeFeePaymentProvider() { override suspend fun feePaymentFor(customFeeAsset: Chain.Asset, coroutineScope: CoroutineScope?): FeePayment { - val chain = chainRegistry.getChain(customFeeAsset.chainId) + val multiLocationConverter = multiLocationConverterFactory.defaultSync(chain) + return AssetConversionFeePayment( paymentAsset = customFeeAsset, multiChainRuntimeCallsApi = multiChainRuntimeCallsApi, - remoteStorageSource = remoteStorageSource, - multiLocationConverter = multiLocationConverterFactory.defaultAsync(chain, coroutineScope!!) + multiLocationConverter = multiLocationConverter, + assetHubFeePaymentAssetsFetcher = assetHubFeePaymentAssetsFetcher.create(chain, multiLocationConverter) ) } + + override suspend fun fastLookupCustomFeeCapability(): FastLookupCustomFeeCapability { + val fetcher = assetHubFeePaymentAssetsFetcher.create(chain) + return AssetHubFastLookupFeeCapability(fetcher) + } } diff --git a/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/data/fee/chains/DefaultFeePaymentProvider.kt b/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/data/fee/chains/DefaultFeePaymentProvider.kt index 695a74db1a..711542b140 100644 --- a/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/data/fee/chains/DefaultFeePaymentProvider.kt +++ b/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/data/fee/chains/DefaultFeePaymentProvider.kt @@ -3,7 +3,9 @@ package io.novafoundation.nova.feature_account_impl.data.fee.chains import io.novafoundation.nova.feature_account_api.data.fee.FeePayment import io.novafoundation.nova.feature_account_api.data.fee.FeePaymentCurrency import io.novafoundation.nova.feature_account_api.data.fee.FeePaymentProvider +import io.novafoundation.nova.feature_account_api.data.fee.capability.FastLookupCustomFeeCapability import io.novafoundation.nova.feature_account_api.data.fee.types.NativeFeePayment +import io.novafoundation.nova.runtime.multiNetwork.chain.model.ChainAssetId import kotlinx.coroutines.CoroutineScope class DefaultFeePaymentProvider : FeePaymentProvider { @@ -11,4 +13,15 @@ class DefaultFeePaymentProvider : FeePaymentProvider { override suspend fun feePaymentFor(feePaymentCurrency: FeePaymentCurrency, coroutineScope: CoroutineScope?): FeePayment { return NativeFeePayment() } + + override suspend fun fastLookupCustomFeeCapability(): FastLookupCustomFeeCapability { + return DefaultFastLookupCustomFeeCapability() + } +} + +class DefaultFastLookupCustomFeeCapability(): FastLookupCustomFeeCapability { + + override suspend fun canPayFeeInNonUtilityToken(chainAssetId: ChainAssetId): Boolean { + return false + } } diff --git a/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/data/fee/chains/HydrationFeePaymentProvider.kt b/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/data/fee/chains/HydrationFeePaymentProvider.kt index fefcb20657..b038f52076 100644 --- a/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/data/fee/chains/HydrationFeePaymentProvider.kt +++ b/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/data/fee/chains/HydrationFeePaymentProvider.kt @@ -1,6 +1,7 @@ package io.novafoundation.nova.feature_account_impl.data.fee.chains import io.novafoundation.nova.feature_account_api.data.fee.FeePayment +import io.novafoundation.nova.feature_account_api.data.fee.capability.FastLookupCustomFeeCapability import io.novafoundation.nova.feature_account_api.data.fee.chains.CustomOrNativeFeePaymentProvider import io.novafoundation.nova.feature_account_api.data.fee.types.hydra.HydrationFeeInjector import io.novafoundation.nova.feature_account_api.domain.interfaces.AccountRepository @@ -18,13 +19,17 @@ class HydrationFeePaymentProvider( ) : CustomOrNativeFeePaymentProvider() { override suspend fun feePaymentFor(customFeeAsset: Chain.Asset, coroutineScope: CoroutineScope?): FeePayment { - return HydrationConversionFeePayment( - paymentAsset = customFeeAsset, - chainRegistry = chainRegistry, - hydrationFeeInjector = hydrationFeeInjector, - hydraDxQuoteSharedComputation = hydraDxQuoteSharedComputation, - accountRepository = accountRepository, - coroutineScope = coroutineScope!! - ) + return HydrationConversionFeePayment( + paymentAsset = customFeeAsset, + chainRegistry = chainRegistry, + hydrationFeeInjector = hydrationFeeInjector, + hydraDxQuoteSharedComputation = hydraDxQuoteSharedComputation, + accountRepository = accountRepository, + coroutineScope = coroutineScope!! + ) + } + + override suspend fun fastLookupCustomFeeCapability(): FastLookupCustomFeeCapability? { + return null } } diff --git a/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/data/fee/types/AssetConversionFeePayment.kt b/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/data/fee/types/assetHub/AssetConversionFeePayment.kt similarity index 76% rename from feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/data/fee/types/AssetConversionFeePayment.kt rename to feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/data/fee/types/assetHub/AssetConversionFeePayment.kt index 91fb362748..c7f9321664 100644 --- a/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/data/fee/types/AssetConversionFeePayment.kt +++ b/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/data/fee/types/assetHub/AssetConversionFeePayment.kt @@ -1,25 +1,20 @@ -package io.novafoundation.nova.feature_account_impl.data.fee.types +package io.novafoundation.nova.feature_account_impl.data.fee.types.assetHub import android.util.Log import io.novafoundation.nova.common.data.network.runtime.binding.bindNumberOrNull import io.novafoundation.nova.common.utils.LOG_TAG import io.novafoundation.nova.common.utils.Modules import io.novafoundation.nova.common.utils.structOf -import io.novafoundation.nova.feature_account_api.data.conversion.assethub.assetConversionOrNull -import io.novafoundation.nova.feature_account_api.data.conversion.assethub.pools import io.novafoundation.nova.feature_account_api.data.fee.FeePayment import io.novafoundation.nova.feature_account_api.data.model.Fee import io.novafoundation.nova.feature_account_api.data.model.SubstrateFee import io.novafoundation.nova.runtime.call.MultiChainRuntimeCallsApi import io.novafoundation.nova.runtime.call.RuntimeCallsApi -import io.novafoundation.nova.runtime.ext.isUtilityAsset import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain import io.novafoundation.nova.runtime.multiNetwork.multiLocation.MultiLocation import io.novafoundation.nova.runtime.multiNetwork.multiLocation.converter.MultiLocationConverter import io.novafoundation.nova.runtime.multiNetwork.multiLocation.converter.toMultiLocationOrThrow import io.novafoundation.nova.runtime.multiNetwork.multiLocation.toEncodableInstance -import io.novafoundation.nova.runtime.storage.source.StorageDataSource -import io.novafoundation.nova.runtime.storage.source.query.metadata import io.novasama.substrate_sdk_android.runtime.definitions.types.primitives.BooleanType import io.novasama.substrate_sdk_android.runtime.extrinsic.ExtrinsicBuilder import io.novasama.substrate_sdk_android.runtime.metadata.RuntimeMetadata @@ -31,8 +26,8 @@ import java.math.BigInteger internal class AssetConversionFeePayment( private val paymentAsset: Chain.Asset, private val multiChainRuntimeCallsApi: MultiChainRuntimeCallsApi, - private val remoteStorageSource: StorageDataSource, - private val multiLocationConverter: MultiLocationConverter + private val multiLocationConverter: MultiLocationConverter, + private val assetHubFeePaymentAssetsFetcher: AssetHubFeePaymentAssetsFetcher, ) : FeePayment { override suspend fun modifyExtrinsic(extrinsicBuilder: ExtrinsicBuilder) { @@ -51,24 +46,8 @@ internal class AssetConversionFeePayment( } override suspend fun canPayFeeInNonUtilityToken(chainAsset: Chain.Asset): Boolean { - val availableFeeAssets = remoteStorageSource.query(paymentAsset.chainId) { - val allPools = metadata.assetConversionOrNull?.pools?.keys().orEmpty() - - constructAvailableCustomFeeAssets(allPools) - } - - return availableFeeAssets.containsKey(chainAsset.id) - } - - private suspend fun constructAvailableCustomFeeAssets(pools: List>): Map { - return pools.mapNotNull { (firstLocation, secondLocation) -> - val firstAsset = multiLocationConverter.toChainAsset(firstLocation) ?: return@mapNotNull null - val secondAsset = multiLocationConverter.toChainAsset(secondLocation) ?: return@mapNotNull null - - if (!firstAsset.isUtilityAsset) return@mapNotNull null - - secondAsset - }.associateBy { it.id } + val availableFeeAssets = assetHubFeePaymentAssetsFetcher.fetchAvailablePaymentAssets() + return chainAsset.id in availableFeeAssets } private suspend fun encodableAssetId(): Any { diff --git a/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/data/fee/types/assetHub/AssetHubFastLookupFeeCapability.kt b/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/data/fee/types/assetHub/AssetHubFastLookupFeeCapability.kt new file mode 100644 index 0000000000..8ee8c0b67f --- /dev/null +++ b/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/data/fee/types/assetHub/AssetHubFastLookupFeeCapability.kt @@ -0,0 +1,24 @@ +package io.novafoundation.nova.feature_account_impl.data.fee.types.assetHub + +import io.novafoundation.nova.feature_account_api.data.fee.capability.FastLookupCustomFeeCapability +import io.novafoundation.nova.runtime.multiNetwork.chain.model.ChainAssetId + +class AssetHubFastLookupFeeCapability( + private val assetsFetcher: AssetHubFeePaymentAssetsFetcher, +): FastLookupCustomFeeCapability { + + private var cachedAssets: Set? = null + + override suspend fun canPayFeeInNonUtilityToken(chainAssetId: ChainAssetId): Boolean { + return chainAssetId in getAllowedFeePaymentAssets() + } + + private suspend fun getAllowedFeePaymentAssets(): Set { + // We are not guarding it with mutex to make it more optimized and avoid synchronisation overhead + if (cachedAssets == null) { + cachedAssets = assetsFetcher.fetchAvailablePaymentAssets() + } + + return cachedAssets!! + } +} diff --git a/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/data/fee/types/assetHub/AssetHubFeePaymentFetcher.kt b/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/data/fee/types/assetHub/AssetHubFeePaymentFetcher.kt new file mode 100644 index 0000000000..f0f4290299 --- /dev/null +++ b/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/data/fee/types/assetHub/AssetHubFeePaymentFetcher.kt @@ -0,0 +1,60 @@ +package io.novafoundation.nova.feature_account_impl.data.fee.types.assetHub + +import io.novafoundation.nova.common.utils.mapNotNullToSet +import io.novafoundation.nova.feature_account_api.data.conversion.assethub.assetConversionOrNull +import io.novafoundation.nova.feature_account_api.data.conversion.assethub.pools +import io.novafoundation.nova.runtime.ext.isUtilityAsset +import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain +import io.novafoundation.nova.runtime.multiNetwork.chain.model.ChainAssetId +import io.novafoundation.nova.runtime.multiNetwork.multiLocation.MultiLocation +import io.novafoundation.nova.runtime.multiNetwork.multiLocation.converter.MultiLocationConverter +import io.novafoundation.nova.runtime.multiNetwork.multiLocation.converter.MultiLocationConverterFactory +import io.novafoundation.nova.runtime.storage.source.StorageDataSource +import io.novafoundation.nova.runtime.storage.source.query.metadata + +interface AssetHubFeePaymentAssetsFetcher { + + suspend fun fetchAvailablePaymentAssets(): Set +} + +class AssetHubFeePaymentAssetsFetcherFactory( + private val remoteStorageSource: StorageDataSource, + private val multiLocationConverterFactory: MultiLocationConverterFactory +) { + + suspend fun create(chain: Chain): AssetHubFeePaymentAssetsFetcher { + val multiLocationConverter = multiLocationConverterFactory.defaultSync(chain) + + return RealAssetHubFeePaymentAssetsFetcher(remoteStorageSource, multiLocationConverter, chain) + } + + fun create(chain: Chain, multiLocationConverter: MultiLocationConverter): AssetHubFeePaymentAssetsFetcher { + return RealAssetHubFeePaymentAssetsFetcher(remoteStorageSource, multiLocationConverter, chain) + } +} + +private class RealAssetHubFeePaymentAssetsFetcher( + private val remoteStorageSource: StorageDataSource, + private val multiLocationConverter: MultiLocationConverter, + private val chain: Chain, +) : AssetHubFeePaymentAssetsFetcher { + + override suspend fun fetchAvailablePaymentAssets(): Set { + return remoteStorageSource.query(chain.id) { + val allPools = metadata.assetConversionOrNull?.pools?.keys().orEmpty() + + constructAvailableCustomFeeAssets(allPools) + } + } + + private suspend fun constructAvailableCustomFeeAssets(pools: List>): Set { + return pools.mapNotNullToSet { (firstLocation, secondLocation) -> + val firstAsset = multiLocationConverter.toChainAsset(firstLocation) ?: return@mapNotNullToSet null + if (!firstAsset.isUtilityAsset) return@mapNotNullToSet null + + val secondAsset = multiLocationConverter.toChainAsset(secondLocation) ?: return@mapNotNullToSet null + + secondAsset.id + } + } +} diff --git a/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/di/modules/CustomFeeModule.kt b/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/di/modules/CustomFeeModule.kt index 5828296d5e..2108f7dd5b 100644 --- a/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/di/modules/CustomFeeModule.kt +++ b/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/di/modules/CustomFeeModule.kt @@ -9,9 +9,10 @@ import io.novafoundation.nova.feature_account_api.data.fee.FeePaymentProviderReg import io.novafoundation.nova.feature_account_api.data.fee.types.hydra.HydrationFeeInjector import io.novafoundation.nova.feature_account_api.domain.interfaces.AccountRepository import io.novafoundation.nova.feature_account_impl.data.fee.RealFeePaymentProviderRegistry -import io.novafoundation.nova.feature_account_impl.data.fee.chains.AssetHubFeePaymentProvider +import io.novafoundation.nova.feature_account_impl.data.fee.chains.AssetHubFeePaymentProviderFactory import io.novafoundation.nova.feature_account_impl.data.fee.chains.DefaultFeePaymentProvider import io.novafoundation.nova.feature_account_impl.data.fee.chains.HydrationFeePaymentProvider +import io.novafoundation.nova.feature_account_impl.data.fee.types.assetHub.AssetHubFeePaymentAssetsFetcherFactory import io.novafoundation.nova.feature_account_impl.data.fee.types.hydra.HydraDxQuoteSharedComputation import io.novafoundation.nova.feature_account_impl.data.fee.types.hydra.RealHydrationFeeInjector import io.novafoundation.nova.feature_swap_core_api.data.network.HydraDxAssetIdConverter @@ -61,16 +62,25 @@ class CustomFeeModule { @Provides @FeatureScope - fun provideAssetHubFeePaymentProvider( - chainRegistry: ChainRegistry, - multiChainRuntimeCallsApi: MultiChainRuntimeCallsApi, + fun provideAssetHubFeePaymentAssetsFetcher( @Named(REMOTE_STORAGE_SOURCE) remoteStorageSource: StorageDataSource, + multiLocationConverterFactory: MultiLocationConverterFactory + ): AssetHubFeePaymentAssetsFetcherFactory { + return AssetHubFeePaymentAssetsFetcherFactory(remoteStorageSource, multiLocationConverterFactory) + } + + @Provides + @FeatureScope + fun provideAssetHubFeePaymentProviderFactory( + multiChainRuntimeCallsApi: MultiChainRuntimeCallsApi, multiLocationConverterFactory: MultiLocationConverterFactory, - ) = AssetHubFeePaymentProvider( - chainRegistry, - multiChainRuntimeCallsApi, - remoteStorageSource, - multiLocationConverterFactory + assetHubFeePaymentAssetsFetcher: AssetHubFeePaymentAssetsFetcherFactory, + chainRegistry: ChainRegistry + ) = AssetHubFeePaymentProviderFactory( + multiChainRuntimeCallsApi = multiChainRuntimeCallsApi, + multiLocationConverterFactory = multiLocationConverterFactory, + assetHubFeePaymentAssetsFetcher = assetHubFeePaymentAssetsFetcher, + chainRegistry = chainRegistry ) @Provides @@ -103,11 +113,11 @@ class CustomFeeModule { @FeatureScope fun provideFeePaymentProviderRegistry( defaultFeePaymentProvider: DefaultFeePaymentProvider, - assetHubFeePaymentProvider: AssetHubFeePaymentProvider, + assetHubFeePaymentProviderFactory: AssetHubFeePaymentProviderFactory, hydrationFeePaymentProvider: HydrationFeePaymentProvider ): FeePaymentProviderRegistry = RealFeePaymentProviderRegistry( defaultFeePaymentProvider, - assetHubFeePaymentProvider, + assetHubFeePaymentProviderFactory, hydrationFeePaymentProvider ) } diff --git a/feature-swap-core/api/src/main/java/io/novafoundation/nova/feature_swap_core_api/data/paths/PathQuoter.kt b/feature-swap-core/api/src/main/java/io/novafoundation/nova/feature_swap_core_api/data/paths/PathQuoter.kt index ae30f7fdc4..55f7b192bd 100644 --- a/feature-swap-core/api/src/main/java/io/novafoundation/nova/feature_swap_core_api/data/paths/PathQuoter.kt +++ b/feature-swap-core/api/src/main/java/io/novafoundation/nova/feature_swap_core_api/data/paths/PathQuoter.kt @@ -1,6 +1,7 @@ package io.novafoundation.nova.feature_swap_core_api.data.paths import io.novafoundation.nova.common.utils.graph.Graph +import io.novafoundation.nova.common.utils.graph.NodeVisitFilter import io.novafoundation.nova.feature_swap_core_api.data.paths.model.BestPathQuote import io.novafoundation.nova.feature_swap_core_api.data.primitive.model.QuotableEdge import io.novafoundation.nova.feature_swap_core_api.data.primitive.model.SwapDirection @@ -15,7 +16,8 @@ interface PathQuoter { fun create( graph: Graph, - computationalScope: CoroutineScope + computationalScope: CoroutineScope, + filter: NodeVisitFilter? = null ): PathQuoter } diff --git a/feature-swap-core/src/main/java/io/novafoundation/nova/feature_swap_core/domain/paths/RealPathQuoter.kt b/feature-swap-core/src/main/java/io/novafoundation/nova/feature_swap_core/domain/paths/RealPathQuoter.kt index e1d2b8d1c2..9fea3a9002 100644 --- a/feature-swap-core/src/main/java/io/novafoundation/nova/feature_swap_core/domain/paths/RealPathQuoter.kt +++ b/feature-swap-core/src/main/java/io/novafoundation/nova/feature_swap_core/domain/paths/RealPathQuoter.kt @@ -3,6 +3,7 @@ package io.novafoundation.nova.feature_swap_core.domain.paths import android.util.Log import io.novafoundation.nova.common.data.memory.ComputationalCache import io.novafoundation.nova.common.utils.graph.Graph +import io.novafoundation.nova.common.utils.graph.NodeVisitFilter import io.novafoundation.nova.common.utils.graph.Path import io.novafoundation.nova.common.utils.graph.findDijkstraPathsBetween import io.novafoundation.nova.common.utils.mapAsync @@ -26,35 +27,37 @@ private const val QUOTES_CACHE = "RealSwapService.QuotesCache" class RealPathQuoterFactory( private val computationalCache: ComputationalCache, -): PathQuoter.Factory { +) : PathQuoter.Factory { override fun create( graph: Graph, - computationalScope: CoroutineScope + computationalScope: CoroutineScope, + filter: NodeVisitFilter? ): PathQuoter { - return RealPathQuoter(computationalCache, graph, computationalScope) + return RealPathQuoter(computationalCache, graph, computationalScope, filter) } } private class RealPathQuoter( private val computationalCache: ComputationalCache, private val graph: Graph, - private val computationalScope: CoroutineScope -): PathQuoter { + private val computationalScope: CoroutineScope, + private val filter: NodeVisitFilter? +) : PathQuoter { @OptIn(ExperimentalTime::class) override suspend fun findBestPath( chainAssetIn: Chain.Asset, chainAssetOut: Chain.Asset, amount: BigInteger, - swapDirection: SwapDirection + swapDirection: SwapDirection, ): BestPathQuote { val from = chainAssetIn.fullId val to = chainAssetOut.fullId val paths = pathsFromCacheOrCompute(from, to, computationalScope) { val (paths, duration) = measureTimedValue { - graph.findDijkstraPathsBetween(from, to, limit = PATHS_LIMIT) + graph.findDijkstraPathsBetween(from, to, limit = PATHS_LIMIT, filter) } Log.d("Swaps", "${chainAssetIn.symbol} -> ${chainAssetOut.symbol}: finding ${paths.size} paths took $duration") @@ -84,6 +87,7 @@ private class RealPathQuoter( computation() } } + private fun pathsCacheKey(from: FullChainAssetId, to: FullChainAssetId): String { val fromKey = "${from.chainId}:${from.assetId}" val toKey = "${to.chainId}:${to.assetId}" diff --git a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/compound/CompoundAssetExchange.kt b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/compound/CompoundAssetExchange.kt deleted file mode 100644 index c146b3a816..0000000000 --- a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/compound/CompoundAssetExchange.kt +++ /dev/null @@ -1,32 +0,0 @@ -package io.novafoundation.nova.feature_swap_impl.data.assetExchange.compound - -import io.novafoundation.nova.common.utils.forEachAsync -import io.novafoundation.nova.common.utils.mergeIfMultiple -import io.novafoundation.nova.feature_account_api.domain.model.MetaAccount -import io.novafoundation.nova.feature_swap_api.domain.model.ReQuoteTrigger -import io.novafoundation.nova.feature_swap_api.domain.model.SwapGraphEdge -import io.novafoundation.nova.feature_swap_impl.data.assetExchange.AssetExchange -import io.novafoundation.nova.feature_swap_impl.data.assetExchange.FeePaymentProviderOverride -import kotlinx.coroutines.flow.Flow - -class CompoundAssetExchange( - private val delegates: List -): AssetExchange { - - override suspend fun sync() { - delegates.forEachAsync { it.sync() } - } - - override suspend fun availableDirectSwapConnections(): List { - return delegates.flatMap { it.availableDirectSwapConnections() } - } - - override fun feePaymentOverrides(): List { - return delegates.flatMap { it.feePaymentOverrides() } - } - - override fun runSubscriptions(metaAccount: MetaAccount): Flow { - return delegates.map { it.runSubscriptions(metaAccount) } - .mergeIfMultiple() - } -} diff --git a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/hydraDx/HydraDxExchange.kt b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/hydraDx/HydraDxExchange.kt index b5104b79eb..4a5acb61e1 100644 --- a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/hydraDx/HydraDxExchange.kt +++ b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/hydraDx/HydraDxExchange.kt @@ -12,6 +12,7 @@ import io.novafoundation.nova.feature_account_api.data.extrinsic.ExtrinsicServic import io.novafoundation.nova.feature_account_api.data.extrinsic.awaitInBlock import io.novafoundation.nova.feature_account_api.data.fee.FeePayment import io.novafoundation.nova.feature_account_api.data.fee.FeePaymentCurrency +import io.novafoundation.nova.feature_account_api.data.fee.capability.FastLookupCustomFeeCapability import io.novafoundation.nova.feature_account_api.data.fee.chains.CustomOrNativeFeePaymentProvider import io.novafoundation.nova.feature_account_api.data.fee.types.hydra.HydrationFeeInjector import io.novafoundation.nova.feature_account_api.data.fee.types.hydra.HydrationFeeInjector.ResetMode @@ -28,6 +29,7 @@ import io.novafoundation.nova.feature_swap_api.domain.model.ReQuoteTrigger import io.novafoundation.nova.feature_swap_api.domain.model.SwapExecutionCorrection import io.novafoundation.nova.feature_swap_api.domain.model.SwapGraphEdge import io.novafoundation.nova.feature_swap_api.domain.model.SwapLimit +import io.novafoundation.nova.feature_swap_core.data.assetExchange.conversion.types.hydra.acceptedCurrencies import io.novafoundation.nova.feature_swap_core.data.assetExchange.conversion.types.hydra.accountCurrencyMap import io.novafoundation.nova.feature_swap_core.data.assetExchange.conversion.types.hydra.multiTransactionPayment import io.novafoundation.nova.feature_swap_core_api.data.network.HydraDxAssetId @@ -49,6 +51,7 @@ import io.novafoundation.nova.runtime.ethereum.StorageSharedRequestsBuilder import io.novafoundation.nova.runtime.ethereum.StorageSharedRequestsBuilderFactory import io.novafoundation.nova.runtime.ext.utilityAsset import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain +import io.novafoundation.nova.runtime.multiNetwork.chain.model.ChainAssetId import io.novafoundation.nova.runtime.multiNetwork.chain.model.FullChainAssetId import io.novafoundation.nova.runtime.storage.source.StorageDataSource import io.novafoundation.nova.runtime.storage.source.query.metadata @@ -388,6 +391,10 @@ private class HydraDxExchange( override suspend fun feePaymentFor(customFeeAsset: Chain.Asset, coroutineScope: CoroutineScope?): FeePayment { return ReusableQuoteFeePayment(customFeeAsset) } + + override suspend fun fastLookupCustomFeeCapability(): FastLookupCustomFeeCapability { + return HydrationFastLookupFeeCapability() + } } private inner class ReusableQuoteFeePayment( @@ -434,6 +441,29 @@ private class HydraDxExchange( } } + private inner class HydrationFastLookupFeeCapability: FastLookupCustomFeeCapability { + + private var acceptedCurrenciesCache: Set? = null + + override suspend fun canPayFeeInNonUtilityToken(chainAssetId: ChainAssetId): Boolean { + val asset = chain.assetsById[chainAssetId] ?: return false + val onChainId = hydraDxAssetIdConverter.toOnChainIdOrThrow(asset) + + val acceptedCurrencies = getAcceptedCurrencies() + return onChainId in acceptedCurrencies + } + + private suspend fun getAcceptedCurrencies(): Set { + if (acceptedCurrenciesCache != null) return acceptedCurrenciesCache!! + + acceptedCurrenciesCache = remoteStorageSource.query(chain.id) { + metadata.multiTransactionPayment.acceptedCurrencies.keys() + }.toSet() + + return acceptedCurrenciesCache!! + } + } + class HydraDxSwapTransactionSegment( val edge: HydraDxSourceEdge, val swapLimit: SwapLimit, diff --git a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/domain/swap/RealSwapService.kt b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/domain/swap/RealSwapService.kt index 9bf060f5ea..be5341b75b 100644 --- a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/domain/swap/RealSwapService.kt +++ b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/domain/swap/RealSwapService.kt @@ -10,6 +10,7 @@ import io.novafoundation.nova.common.utils.flatMap import io.novafoundation.nova.common.utils.flowOf import io.novafoundation.nova.common.utils.forEachAsync import io.novafoundation.nova.common.utils.graph.Graph +import io.novafoundation.nova.common.utils.graph.NodeVisitFilter import io.novafoundation.nova.common.utils.graph.create import io.novafoundation.nova.common.utils.graph.findAllPossibleDestinations import io.novafoundation.nova.common.utils.graph.hasOutcomingDirections @@ -25,6 +26,7 @@ import io.novafoundation.nova.feature_account_api.data.fee.FeePaymentCurrency import io.novafoundation.nova.feature_account_api.data.fee.FeePaymentProvider import io.novafoundation.nova.feature_account_api.data.fee.FeePaymentProviderRegistry import io.novafoundation.nova.feature_account_api.data.fee.capability.CustomFeeCapabilityFacade +import io.novafoundation.nova.feature_account_api.data.fee.capability.FastLookupCustomFeeCapability import io.novafoundation.nova.feature_account_api.data.fee.toFeePaymentCurrency import io.novafoundation.nova.feature_account_api.domain.model.MetaAccount import io.novafoundation.nova.feature_swap_api.domain.model.AtomicSwapOperation @@ -52,7 +54,6 @@ import io.novafoundation.nova.feature_swap_impl.data.assetExchange.AssetExchange import io.novafoundation.nova.feature_swap_impl.data.assetExchange.FeePaymentProviderOverride import io.novafoundation.nova.feature_swap_impl.data.assetExchange.ParentQuoterArgs import io.novafoundation.nova.feature_swap_impl.data.assetExchange.assetConversion.AssetConversionExchangeFactory -import io.novafoundation.nova.feature_swap_impl.data.assetExchange.compound.CompoundAssetExchange import io.novafoundation.nova.feature_swap_impl.data.assetExchange.crossChain.CrossChainTransferAssetExchangeFactory import io.novafoundation.nova.feature_swap_impl.data.assetExchange.hydraDx.HydraDxExchangeFactory import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.types.Balance @@ -61,6 +62,7 @@ import io.novafoundation.nova.feature_wallet_api.presentation.formatters.formatP import io.novafoundation.nova.runtime.ext.assetConversionSupported import io.novafoundation.nova.runtime.ext.fullId import io.novafoundation.nova.runtime.ext.hydraDxSupported +import io.novafoundation.nova.runtime.ext.isUtility import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry import io.novafoundation.nova.runtime.multiNetwork.asset import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain @@ -83,6 +85,7 @@ private const val ALL_DIRECTIONS_CACHE = "RealSwapService.ALL_DIRECTIONS" private const val EXCHANGES_CACHE = "RealSwapService.EXCHANGES" private const val EXTRINSIC_SERVICE_CACHE = "RealSwapService.ExtrinsicService" private const val QUOTER_CACHE = "RealSwapService.QUOTER" +private const val NODE_VISIT_FILTER = "RealSwapService.NodeVisitFilter" internal class RealSwapService( @@ -104,7 +107,7 @@ internal class RealSwapService( val paymentRegistry = exchangeRegistry.getFeePaymentRegistry() val chain = chainRegistry.getChain(asset.chainId) - val feePayment = paymentRegistry.providerFor(chain).feePaymentFor(asset.toFeePaymentCurrency(), computationScope) + val feePayment = paymentRegistry.providerFor(chain.id).feePaymentFor(asset.toFeePaymentCurrency(), computationScope) customFeeCapabilityFacade.canPayFeeInNonUtilityToken(asset, feePayment) } @@ -128,7 +131,8 @@ internal class RealSwapService( computationScope: CoroutineScope ): Flow> { return directionsGraph(computationScope).map { - it.findAllPossibleDestinations(asset.fullId) + val filter = canPayFeeNodeFilter(computationScope) + it.findAllPossibleDestinations(asset.fullId, filter) } } @@ -319,6 +323,13 @@ internal class RealSwapService( } } + + private suspend fun canPayFeeNodeFilter(computationScope: CoroutineScope): NodeVisitFilter { + return computationalCache.useCache(NODE_VISIT_FILTER, computationScope) { + CanPayFeeNodeVisitFilter(this) + } + } + private suspend fun extrinsicService(computationScope: CoroutineScope): ExtrinsicService { return computationalCache.useCache(EXTRINSIC_SERVICE_CACHE, computationScope) { createExtrinsicService(this) @@ -391,7 +402,9 @@ internal class RealSwapService( private suspend fun getPathQuoter(computationScope: CoroutineScope): PathQuoter { return computationalCache.useCache(QUOTER_CACHE, computationScope) { val graph = directionsGraph(computationScope).first() - quoterFactory.create(graph, computationScope) + val filter = canPayFeeNodeFilter(computationScope) + + quoterFactory.create(graph, this, filter) } } @@ -466,19 +479,6 @@ internal class RealSwapService( private val feePaymentRegistry = SwapFeePaymentRegistry() - fun getExchange(chainId: ChainId): AssetExchange { - val relevantExchanges = buildList { - singleChainExchanges[chainId]?.let { add(it) } - addAll(multiChainExchanges) - } - - return when (relevantExchanges.size) { - 0 -> error("No exchanges found") - 1 -> relevantExchanges.single() - else -> CompoundAssetExchange(relevantExchanges) - } - } - fun getFeePaymentRegistry(): FeePaymentProviderRegistry { return feePaymentRegistry } @@ -494,9 +494,9 @@ internal class RealSwapService( private val paymentRegistryOverrides = createFeePaymentOverrides() - override suspend fun providerFor(chain: Chain): FeePaymentProvider { - return paymentRegistryOverrides.find { it.chain.id == chain.id }?.provider - ?: defaultFeePaymentProviderRegistry.providerFor(chain) + override suspend fun providerFor(chainId: ChainId): FeePaymentProvider { + return paymentRegistryOverrides.find { it.chain.id == chainId }?.provider + ?: defaultFeePaymentProviderRegistry.providerFor(chainId) } private fun createFeePaymentOverrides(): List { @@ -512,6 +512,41 @@ internal class RealSwapService( } } } + + /** + * Check that it is possible to pay fees in moving asset + */ + private inner class CanPayFeeNodeVisitFilter(val computationScope: CoroutineScope) : NodeVisitFilter { + + private val feePaymentCapabilityCache: MutableMap = mutableMapOf() + + override suspend fun shouldVisit(node: FullChainAssetId): Boolean { + if (node.isUtility) return true + + val feeCapability = getFeeCustomFeeCapability(node.chainId) + return feeCapability != null && feeCapability.canPayFeeInNonUtilityToken(node.assetId) + } + + private suspend fun getFeeCustomFeeCapability(chainId: ChainId): FastLookupCustomFeeCapability? { + val fromCache = feePaymentCapabilityCache.getOrPut(chainId) { + createFastLookupFeeCapability(chainId, computationScope).boxNullable() + } + + return fromCache.unboxNullable() + } + + private suspend fun createFastLookupFeeCapability(chainId: ChainId, computationScope: CoroutineScope): FastLookupCustomFeeCapability? { + val feePaymentRegistry = exchangeRegistry(computationScope).getFeePaymentRegistry() + return feePaymentRegistry.providerFor(chainId).fastLookupCustomFeeCapability() + } + } + + private object NULL + + fun T.boxNullable(): Any = this ?: NULL + + @Suppress("UNCHECKED_CAST") + fun Any.unboxNullable(): T? = if (this == NULL) null else this as T } private typealias QuotedTrade = QuotedPath diff --git a/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/domain/fee/RealCustomFeeInteractor.kt b/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/domain/fee/RealCustomFeeInteractor.kt index 2a3ba1710b..bbe8aa36da 100644 --- a/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/domain/fee/RealCustomFeeInteractor.kt +++ b/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/domain/fee/RealCustomFeeInteractor.kt @@ -1,18 +1,18 @@ package io.novafoundation.nova.feature_wallet_impl.domain.fee import io.novafoundation.nova.feature_account_api.data.fee.FeePaymentProviderRegistry +import io.novafoundation.nova.feature_account_api.data.fee.capability.CustomFeeCapabilityFacade import io.novafoundation.nova.feature_account_api.data.fee.toFeePaymentCurrency import io.novafoundation.nova.feature_account_api.domain.interfaces.AccountRepository -import io.novafoundation.nova.feature_account_api.data.fee.capability.CustomFeeCapabilityFacade import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.AssetSourceRegistry import io.novafoundation.nova.feature_wallet_api.domain.fee.CustomFeeInteractor import io.novafoundation.nova.feature_wallet_api.domain.interfaces.WalletRepository import io.novafoundation.nova.feature_wallet_api.domain.model.Asset import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain -import java.math.BigInteger import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.Flow +import java.math.BigInteger class RealCustomFeeInteractor( private val feePaymentProviderRegistry: FeePaymentProviderRegistry, @@ -24,10 +24,9 @@ class RealCustomFeeInteractor( ) : CustomFeeInteractor { override suspend fun canPayFeeInNonUtilityAsset(chainAsset: Chain.Asset, coroutineScope: CoroutineScope): Boolean { - val chain = chainRegistry.getChain(chainAsset.chainId) val feePaymentCurrency = chainAsset.toFeePaymentCurrency() - val feePayment = feePaymentProviderRegistry.providerFor(chain) + val feePayment = feePaymentProviderRegistry.providerFor(chainAsset.chainId) .feePaymentFor(feePaymentCurrency, coroutineScope) return customFeeCapabilityFacade.canPayFeeInNonUtilityToken(chainAsset, feePayment) 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 8d084be570..65642ef1b0 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 @@ -196,6 +196,9 @@ val Chain.Asset.isUtilityAsset: Boolean inline val Chain.Asset.isCommissionAsset: Boolean get() = isUtilityAsset +inline val FullChainAssetId.isUtility: Boolean + get() = assetId == UTILITY_ASSET_ID + private const val MOONBEAM_XC_PREFIX = "xc" fun Chain.Asset.unifiedSymbol(): String { diff --git a/runtime/src/main/java/io/novafoundation/nova/runtime/multiNetwork/multiLocation/converter/ForeignAssetsLocationConverter.kt b/runtime/src/main/java/io/novafoundation/nova/runtime/multiNetwork/multiLocation/converter/ForeignAssetsLocationConverter.kt index c145209869..151c103970 100644 --- a/runtime/src/main/java/io/novafoundation/nova/runtime/multiNetwork/multiLocation/converter/ForeignAssetsLocationConverter.kt +++ b/runtime/src/main/java/io/novafoundation/nova/runtime/multiNetwork/multiLocation/converter/ForeignAssetsLocationConverter.kt @@ -1,6 +1,5 @@ package io.novafoundation.nova.runtime.multiNetwork.multiLocation.converter -import io.novafoundation.nova.common.utils.invoke import io.novafoundation.nova.common.utils.toHexUntypedOrNull import io.novafoundation.nova.runtime.ext.requireStatemine import io.novafoundation.nova.runtime.ext.statemineOrNull @@ -13,8 +12,6 @@ import io.novafoundation.nova.runtime.multiNetwork.chain.model.statemineAssetIdS import io.novafoundation.nova.runtime.multiNetwork.multiLocation.MultiLocation import io.novafoundation.nova.runtime.multiNetwork.multiLocation.bindMultiLocation import io.novafoundation.nova.runtime.multiNetwork.multiLocation.toEncodableInstance -import io.novasama.substrate_sdk_android.runtime.RuntimeSnapshot -import kotlinx.coroutines.Deferred private typealias ScaleEncodedMultiLocation = String private typealias ForeignAssetsAssetId = ScaleEncodedMultiLocation @@ -25,7 +22,7 @@ private const val FOREIGN_ASSETS_PALLET_NAME = "ForeignAssets" internal class ForeignAssetsLocationConverter( private val chain: Chain, - private val runtime: Deferred + private val runtime: RuntimeSource ) : MultiLocationConverter { private val assetIdToAssetMapping by lazy { constructAssetIdToAssetMapping() } @@ -37,9 +34,9 @@ internal class ForeignAssetsLocationConverter( } override suspend fun toChainAsset(multiLocation: MultiLocation): Chain.Asset? { - val assetIdType = statemineAssetIdScaleType(runtime(), FOREIGN_ASSETS_PALLET_NAME) ?: return null + val assetIdType = statemineAssetIdScaleType(runtime.getRuntime(), FOREIGN_ASSETS_PALLET_NAME) ?: return null val encodableInstance = multiLocation.toEncodableInstance() - val multiLocationHex = assetIdType.toHexUntypedOrNull(runtime(), encodableInstance) ?: return null + val multiLocationHex = assetIdType.toHexUntypedOrNull(runtime.getRuntime(), encodableInstance) ?: return null return assetIdToAssetMapping[multiLocationHex] } @@ -63,7 +60,7 @@ internal class ForeignAssetsLocationConverter( if (!assetsType.id.isScaleEncoded()) return null return runCatching { - val encodableMultiLocation = assetsType.prepareIdForEncoding(runtime()) + val encodableMultiLocation = assetsType.prepareIdForEncoding(runtime.getRuntime()) bindMultiLocation(encodableMultiLocation) }.getOrNull() } diff --git a/runtime/src/main/java/io/novafoundation/nova/runtime/multiNetwork/multiLocation/converter/MultiLocationConverterFactory.kt b/runtime/src/main/java/io/novafoundation/nova/runtime/multiNetwork/multiLocation/converter/MultiLocationConverterFactory.kt index e94ef67217..2ab25f62fb 100644 --- a/runtime/src/main/java/io/novafoundation/nova/runtime/multiNetwork/multiLocation/converter/MultiLocationConverterFactory.kt +++ b/runtime/src/main/java/io/novafoundation/nova/runtime/multiNetwork/multiLocation/converter/MultiLocationConverterFactory.kt @@ -10,11 +10,23 @@ class MultiLocationConverterFactory(private val chainRegistry: ChainRegistry) { fun defaultAsync(chain: Chain, coroutineScope: CoroutineScope): MultiLocationConverter { val runtimeAsync = coroutineScope.async { chainRegistry.getRuntime(chain.id) } + val runtimeSource = RuntimeSource.Async(runtimeAsync) return CompoundMultiLocationConverter( NativeAssetLocationConverter(chain), - LocalAssetsLocationConverter(chain, RuntimeSource.Async(runtimeAsync)), - ForeignAssetsLocationConverter(chain, runtimeAsync) + LocalAssetsLocationConverter(chain, runtimeSource), + ForeignAssetsLocationConverter(chain, runtimeSource) + ) + } + + suspend fun defaultSync(chain: Chain): MultiLocationConverter { + val runtimeAsync = chainRegistry.getRuntime(chain.id) + val runtimeSource = RuntimeSource.Sync(runtimeAsync) + + return CompoundMultiLocationConverter( + NativeAssetLocationConverter(chain), + LocalAssetsLocationConverter(chain, runtimeSource), + ForeignAssetsLocationConverter(chain, runtimeSource) ) } From db1e3744a1c084b382291a4bb6bb60ec5e45172b Mon Sep 17 00:00:00 2001 From: Valentun Date: Wed, 16 Oct 2024 18:14:49 +0300 Subject: [PATCH 18/83] Allow edges to ignore fee requirement based on the predecessor --- .../nova/common/utils/KotlinExt.kt | 11 ++++++++ .../nova/common/utils/graph/Graph.kt | 25 +++++++++++-------- .../swap/AssetSwapFlowViewModel.kt | 4 ++- .../domain/model/SwapGraph.kt | 6 +++++ .../data/paths/PathQuoter.kt | 4 +-- .../domain/paths/RealPathQuoter.kt | 14 ++++------- .../AssetConversionExchange.kt | 4 +++ .../CrossChainTransferAssetExchange.kt | 4 +++ .../assetExchange/hydraDx/HydraDxExchange.kt | 5 ++++ .../domain/swap/RealSwapService.kt | 25 +++++++++++-------- 10 files changed, 69 insertions(+), 33 deletions(-) diff --git a/common/src/main/java/io/novafoundation/nova/common/utils/KotlinExt.kt b/common/src/main/java/io/novafoundation/nova/common/utils/KotlinExt.kt index b5a416ada5..229efaffbb 100644 --- a/common/src/main/java/io/novafoundation/nova/common/utils/KotlinExt.kt +++ b/common/src/main/java/io/novafoundation/nova/common/utils/KotlinExt.kt @@ -1,6 +1,7 @@ package io.novafoundation.nova.common.utils import android.net.Uri +import android.util.Log import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Deferred import kotlinx.coroutines.Job @@ -29,6 +30,8 @@ import kotlin.contracts.contract import kotlin.coroutines.CoroutineContext import kotlin.coroutines.EmptyCoroutineContext import kotlin.math.sqrt +import kotlin.time.ExperimentalTime +import kotlin.time.measureTimedValue private val PERCENTAGE_MULTIPLIER = 100.toBigDecimal() @@ -65,6 +68,14 @@ inline fun Result.mapError(transform: (throwable: Throwable) -> Throwable } } +@OptIn(ExperimentalTime::class) +inline fun measureExecution(label: String, function: () -> R): R { + val (value, time) = measureTimedValue(function) + Log.d("Performance", "$label took $time") + + return value +} + inline fun Result.mapErrorNotInstance(transform: (throwable: Throwable) -> Throwable): Result { return mapError { throwable -> if (throwable !is E) { diff --git a/common/src/main/java/io/novafoundation/nova/common/utils/graph/Graph.kt b/common/src/main/java/io/novafoundation/nova/common/utils/graph/Graph.kt index eb6e8d7a79..f0baf5211b 100644 --- a/common/src/main/java/io/novafoundation/nova/common/utils/graph/Graph.kt +++ b/common/src/main/java/io/novafoundation/nova/common/utils/graph/Graph.kt @@ -34,9 +34,9 @@ fun Graph<*, *>.numberOfEdges(): Int { return adjacencyList.values.sumOf { it.size } } -fun interface NodeVisitFilter { +fun interface EdgeVisitFilter> { - suspend fun shouldVisit(node: N): Boolean + suspend fun shouldVisit(edge: E, pathPredecessor: E?): Boolean } /** @@ -48,11 +48,11 @@ fun interface NodeVisitFilter { */ suspend fun > Graph.findAllPossibleDestinations( origin: N, - nodeVisitFilter: NodeVisitFilter? = null + nodeVisitFilter: EdgeVisitFilter? = null ): Set { - val actualNodeListFilter = nodeVisitFilter ?: NodeVisitFilter { true } + val actualNodeListFilter = nodeVisitFilter ?: EdgeVisitFilter { _, _ -> true } - return reachabilityDfs(origin, adjacencyList, actualNodeListFilter).toSet() + return reachabilityDfs(origin, adjacencyList, actualNodeListFilter, predecessor = null).toSet() } fun > Graph.hasOutcomingDirections(origin: N): Boolean { @@ -65,9 +65,9 @@ suspend fun > Graph.findDijkstraPathsBetween( from: N, to: N, limit: Int, - nodeVisitFilter: NodeVisitFilter? + nodeVisitFilter: EdgeVisitFilter? ): List> { - val actualNodeListFilter = nodeVisitFilter ?: NodeVisitFilter { true } + val actualNodeListFilter = nodeVisitFilter ?: EdgeVisitFilter { _, _ -> true } data class QueueElement(val currentPath: Path, val score: Int) : Comparable { @@ -99,6 +99,8 @@ suspend fun > Graph.findDijkstraPathsBetween( val newCount = count.getValue(lastNode) + 1 count[lastNode] = newCount + val predecessor = minimumQueueElement.currentPath.lastOrNull() + if (lastNode == to) { paths.add(minimumQueueElement.currentPath) continue @@ -106,7 +108,7 @@ suspend fun > Graph.findDijkstraPathsBetween( if (newCount <= limit) { adjacencyList.getValue(lastNode).forEach { edge -> - if (edge.to in minimumQueueElement || !actualNodeListFilter.shouldVisit(edge.to)) return@forEach + if (edge.to in minimumQueueElement || !actualNodeListFilter.shouldVisit(edge, predecessor)) return@forEach val newElement = QueueElement( currentPath = minimumQueueElement.currentPath + edge, @@ -124,7 +126,8 @@ suspend fun > Graph.findDijkstraPathsBetween( private suspend fun > reachabilityDfs( node: N, adjacencyList: Map>, - nodeVisitFilter: NodeVisitFilter, + nodeVisitFilter: EdgeVisitFilter, + predecessor: E?, visited: MutableSet = mutableSetOf(), connectedComponentState: MutableList = mutableListOf() ): List { @@ -132,8 +135,8 @@ private suspend fun > reachabilityDfs( connectedComponentState.add(node) for (edge in adjacencyList.getValue(node)) { - if (edge.to !in visited && nodeVisitFilter.shouldVisit(node)) { - reachabilityDfs(edge.to, adjacencyList, nodeVisitFilter, visited, connectedComponentState) + if (edge.to !in visited && nodeVisitFilter.shouldVisit(edge, predecessor)) { + reachabilityDfs(edge.to, adjacencyList, nodeVisitFilter, predecessor = edge, visited, connectedComponentState) } } diff --git a/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/swap/AssetSwapFlowViewModel.kt b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/swap/AssetSwapFlowViewModel.kt index 53635a063f..cfa0346e5f 100644 --- a/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/swap/AssetSwapFlowViewModel.kt +++ b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/swap/AssetSwapFlowViewModel.kt @@ -45,7 +45,9 @@ class AssetSwapFlowViewModel( init { launch { - swapAvailabilityInteractor.sync(viewModelScope) + if (payload is SwapFlowPayload.InitialSelecting) { + swapAvailabilityInteractor.sync(viewModelScope) + } } } diff --git a/feature-swap-api/src/main/java/io/novafoundation/nova/feature_swap_api/domain/model/SwapGraph.kt b/feature-swap-api/src/main/java/io/novafoundation/nova/feature_swap_api/domain/model/SwapGraph.kt index beacafcc27..d917caa32d 100644 --- a/feature-swap-api/src/main/java/io/novafoundation/nova/feature_swap_api/domain/model/SwapGraph.kt +++ b/feature-swap-api/src/main/java/io/novafoundation/nova/feature_swap_api/domain/model/SwapGraph.kt @@ -17,6 +17,12 @@ interface SwapGraphEdge : QuotableEdge { suspend fun appendToOperation(currentTransaction: AtomicSwapOperation, args: AtomicSwapOperationArgs): AtomicSwapOperation? suspend fun debugLabel(): String + + /** + * Whether this Edge fee check should be skipped when adding to after a specified [predecessor] + * + */ + suspend fun shouldIgnoreFeeRequirementAfter(predecessor: SwapGraphEdge): Boolean } diff --git a/feature-swap-core/api/src/main/java/io/novafoundation/nova/feature_swap_core_api/data/paths/PathQuoter.kt b/feature-swap-core/api/src/main/java/io/novafoundation/nova/feature_swap_core_api/data/paths/PathQuoter.kt index 55f7b192bd..78bb2b8308 100644 --- a/feature-swap-core/api/src/main/java/io/novafoundation/nova/feature_swap_core_api/data/paths/PathQuoter.kt +++ b/feature-swap-core/api/src/main/java/io/novafoundation/nova/feature_swap_core_api/data/paths/PathQuoter.kt @@ -1,7 +1,7 @@ package io.novafoundation.nova.feature_swap_core_api.data.paths +import io.novafoundation.nova.common.utils.graph.EdgeVisitFilter import io.novafoundation.nova.common.utils.graph.Graph -import io.novafoundation.nova.common.utils.graph.NodeVisitFilter import io.novafoundation.nova.feature_swap_core_api.data.paths.model.BestPathQuote import io.novafoundation.nova.feature_swap_core_api.data.primitive.model.QuotableEdge import io.novafoundation.nova.feature_swap_core_api.data.primitive.model.SwapDirection @@ -17,7 +17,7 @@ interface PathQuoter { fun create( graph: Graph, computationalScope: CoroutineScope, - filter: NodeVisitFilter? = null + filter: EdgeVisitFilter? = null ): PathQuoter } diff --git a/feature-swap-core/src/main/java/io/novafoundation/nova/feature_swap_core/domain/paths/RealPathQuoter.kt b/feature-swap-core/src/main/java/io/novafoundation/nova/feature_swap_core/domain/paths/RealPathQuoter.kt index 9fea3a9002..eec69ed3b2 100644 --- a/feature-swap-core/src/main/java/io/novafoundation/nova/feature_swap_core/domain/paths/RealPathQuoter.kt +++ b/feature-swap-core/src/main/java/io/novafoundation/nova/feature_swap_core/domain/paths/RealPathQuoter.kt @@ -2,11 +2,12 @@ package io.novafoundation.nova.feature_swap_core.domain.paths import android.util.Log import io.novafoundation.nova.common.data.memory.ComputationalCache +import io.novafoundation.nova.common.utils.graph.EdgeVisitFilter import io.novafoundation.nova.common.utils.graph.Graph -import io.novafoundation.nova.common.utils.graph.NodeVisitFilter import io.novafoundation.nova.common.utils.graph.Path import io.novafoundation.nova.common.utils.graph.findDijkstraPathsBetween import io.novafoundation.nova.common.utils.mapAsync +import io.novafoundation.nova.common.utils.measureExecution import io.novafoundation.nova.feature_swap_core_api.data.paths.PathQuoter import io.novafoundation.nova.feature_swap_core_api.data.paths.model.BestPathQuote import io.novafoundation.nova.feature_swap_core_api.data.paths.model.QuotedEdge @@ -19,8 +20,6 @@ import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain import io.novafoundation.nova.runtime.multiNetwork.chain.model.FullChainAssetId import kotlinx.coroutines.CoroutineScope import java.math.BigInteger -import kotlin.time.ExperimentalTime -import kotlin.time.measureTimedValue private const val PATHS_LIMIT = 4 private const val QUOTES_CACHE = "RealSwapService.QuotesCache" @@ -32,7 +31,7 @@ class RealPathQuoterFactory( override fun create( graph: Graph, computationalScope: CoroutineScope, - filter: NodeVisitFilter? + filter: EdgeVisitFilter? ): PathQuoter { return RealPathQuoter(computationalCache, graph, computationalScope, filter) } @@ -42,10 +41,9 @@ private class RealPathQuoter( private val computationalCache: ComputationalCache, private val graph: Graph, private val computationalScope: CoroutineScope, - private val filter: NodeVisitFilter? + private val filter: EdgeVisitFilter? ) : PathQuoter { - @OptIn(ExperimentalTime::class) override suspend fun findBestPath( chainAssetIn: Chain.Asset, chainAssetOut: Chain.Asset, @@ -56,12 +54,10 @@ private class RealPathQuoter( val to = chainAssetOut.fullId val paths = pathsFromCacheOrCompute(from, to, computationalScope) { - val (paths, duration) = measureTimedValue { + val paths = measureExecution("Finding ${chainAssetIn.symbol} -> ${chainAssetOut.symbol} paths") { graph.findDijkstraPathsBetween(from, to, limit = PATHS_LIMIT, filter) } - Log.d("Swaps", "${chainAssetIn.symbol} -> ${chainAssetOut.symbol}: finding ${paths.size} paths took $duration") - paths } diff --git a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/assetConversion/AssetConversionExchange.kt b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/assetConversion/AssetConversionExchange.kt index a96722f4b4..874f967c01 100644 --- a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/assetConversion/AssetConversionExchange.kt +++ b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/assetConversion/AssetConversionExchange.kt @@ -173,6 +173,10 @@ private class AssetConversionExchange( return "AssetConversion" } + override suspend fun shouldIgnoreFeeRequirementAfter(predecessor: SwapGraphEdge): Boolean { + return false + } + override suspend fun quote( amount: Balance, direction: SwapDirection diff --git a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/crossChain/CrossChainTransferAssetExchange.kt b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/crossChain/CrossChainTransferAssetExchange.kt index 1757504dab..41bd3ccecf 100644 --- a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/crossChain/CrossChainTransferAssetExchange.kt +++ b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/crossChain/CrossChainTransferAssetExchange.kt @@ -90,6 +90,10 @@ class CrossChainTransferAssetExchange( return "Transfer" } + override suspend fun shouldIgnoreFeeRequirementAfter(predecessor: SwapGraphEdge): Boolean { + return false + } + override suspend fun quote(amount: BigInteger, direction: SwapDirection): BigInteger { return amount } diff --git a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/hydraDx/HydraDxExchange.kt b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/hydraDx/HydraDxExchange.kt index 4a5acb61e1..ce1a552219 100644 --- a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/hydraDx/HydraDxExchange.kt +++ b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/hydraDx/HydraDxExchange.kt @@ -222,6 +222,11 @@ private class HydraDxExchange( override suspend fun debugLabel(): String { return "Hydration.${sourceQuotableEdge.debugLabel()}" } + + override suspend fun shouldIgnoreFeeRequirementAfter(predecessor: SwapGraphEdge): Boolean { + // When staking multiple hydra edges together, the fee is always paid with the starting edge + return predecessor is HydraDxSwapEdge + } } inner class HydraDxOperation private constructor( diff --git a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/domain/swap/RealSwapService.kt b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/domain/swap/RealSwapService.kt index be5341b75b..e6d71e4604 100644 --- a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/domain/swap/RealSwapService.kt +++ b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/domain/swap/RealSwapService.kt @@ -9,8 +9,8 @@ import io.novafoundation.nova.common.utils.filterNotNull import io.novafoundation.nova.common.utils.flatMap import io.novafoundation.nova.common.utils.flowOf import io.novafoundation.nova.common.utils.forEachAsync +import io.novafoundation.nova.common.utils.graph.EdgeVisitFilter import io.novafoundation.nova.common.utils.graph.Graph -import io.novafoundation.nova.common.utils.graph.NodeVisitFilter import io.novafoundation.nova.common.utils.graph.create import io.novafoundation.nova.common.utils.graph.findAllPossibleDestinations import io.novafoundation.nova.common.utils.graph.hasOutcomingDirections @@ -324,7 +324,7 @@ internal class RealSwapService( } - private suspend fun canPayFeeNodeFilter(computationScope: CoroutineScope): NodeVisitFilter { + private suspend fun canPayFeeNodeFilter(computationScope: CoroutineScope): EdgeVisitFilter { return computationalCache.useCache(NODE_VISIT_FILTER, computationScope) { CanPayFeeNodeVisitFilter(this) } @@ -516,17 +516,10 @@ internal class RealSwapService( /** * Check that it is possible to pay fees in moving asset */ - private inner class CanPayFeeNodeVisitFilter(val computationScope: CoroutineScope) : NodeVisitFilter { + private inner class CanPayFeeNodeVisitFilter(val computationScope: CoroutineScope) : EdgeVisitFilter { private val feePaymentCapabilityCache: MutableMap = mutableMapOf() - override suspend fun shouldVisit(node: FullChainAssetId): Boolean { - if (node.isUtility) return true - - val feeCapability = getFeeCustomFeeCapability(node.chainId) - return feeCapability != null && feeCapability.canPayFeeInNonUtilityToken(node.assetId) - } - private suspend fun getFeeCustomFeeCapability(chainId: ChainId): FastLookupCustomFeeCapability? { val fromCache = feePaymentCapabilityCache.getOrPut(chainId) { createFastLookupFeeCapability(chainId, computationScope).boxNullable() @@ -539,6 +532,18 @@ internal class RealSwapService( val feePaymentRegistry = exchangeRegistry(computationScope).getFeePaymentRegistry() return feePaymentRegistry.providerFor(chainId).fastLookupCustomFeeCapability() } + + override suspend fun shouldVisit(edge: SwapGraphEdge, pathPredecessor: SwapGraphEdge?): Boolean { + // Utility payments and first path segments are always allowed + if (edge.from.isUtility || pathPredecessor == null) return true + + // Edge might request us to ignore the default requirement based on its direct predecessor + if (edge.shouldIgnoreFeeRequirementAfter(pathPredecessor)) return true + + val feeCapability = getFeeCustomFeeCapability(edge.from.chainId) + + return feeCapability != null && feeCapability.canPayFeeInNonUtilityToken(edge.from.assetId) + } } private object NULL From fbca0f7b607f3775beeeb68691da47e806b8f626 Mon Sep 17 00:00:00 2001 From: Valentun Date: Wed, 16 Oct 2024 18:47:27 +0300 Subject: [PATCH 19/83] Filter out cross chain directions with delivery fees --- .../domain/model/SwapGraph.kt | 9 ++++++-- .../AssetConversionExchange.kt | 4 ++++ .../CrossChainTransferAssetExchange.kt | 23 ++++++++++++++++++- .../assetExchange/hydraDx/HydraDxExchange.kt | 6 ++++- .../domain/swap/RealSwapService.kt | 1 + .../interfaces/CrossChainTransfersUseCase.kt | 5 ++-- .../domain/RealCrossChainTransfersUseCase.kt | 8 +++---- 7 files changed, 44 insertions(+), 12 deletions(-) diff --git a/feature-swap-api/src/main/java/io/novafoundation/nova/feature_swap_api/domain/model/SwapGraph.kt b/feature-swap-api/src/main/java/io/novafoundation/nova/feature_swap_api/domain/model/SwapGraph.kt index d917caa32d..7493c7df71 100644 --- a/feature-swap-api/src/main/java/io/novafoundation/nova/feature_swap_api/domain/model/SwapGraph.kt +++ b/feature-swap-api/src/main/java/io/novafoundation/nova/feature_swap_api/domain/model/SwapGraph.kt @@ -1,7 +1,6 @@ package io.novafoundation.nova.feature_swap_api.domain.model import io.novafoundation.nova.common.utils.graph.Graph -import io.novafoundation.nova.common.utils.graph.Path import io.novafoundation.nova.feature_swap_core_api.data.primitive.model.QuotableEdge import io.novafoundation.nova.runtime.multiNetwork.chain.model.FullChainAssetId @@ -20,12 +19,18 @@ interface SwapGraphEdge : QuotableEdge { /** * Whether this Edge fee check should be skipped when adding to after a specified [predecessor] + * Note that returning true here means that [canPayNonNativeFeesInIntermediatePosition] wont be called and checked * */ suspend fun shouldIgnoreFeeRequirementAfter(predecessor: SwapGraphEdge): Boolean + + /** + * Can be used to define additional restrictions on top of default one, "is able to pay submission fee on origin" + * This will only be called for intermediate hops for non-utility assets since other cases are always payable + */ + suspend fun canPayNonNativeFeesInIntermediatePosition(): Boolean } typealias SwapGraph = Graph -typealias SwapPath = Path diff --git a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/assetConversion/AssetConversionExchange.kt b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/assetConversion/AssetConversionExchange.kt index 874f967c01..acbe7604ff 100644 --- a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/assetConversion/AssetConversionExchange.kt +++ b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/assetConversion/AssetConversionExchange.kt @@ -177,6 +177,10 @@ private class AssetConversionExchange( return false } + override suspend fun canPayNonNativeFeesInIntermediatePosition(): Boolean { + return true + } + override suspend fun quote( amount: Balance, direction: SwapDirection diff --git a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/crossChain/CrossChainTransferAssetExchange.kt b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/crossChain/CrossChainTransferAssetExchange.kt index 41bd3ccecf..28f54bd9e7 100644 --- a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/crossChain/CrossChainTransferAssetExchange.kt +++ b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/crossChain/CrossChainTransferAssetExchange.kt @@ -1,5 +1,7 @@ package io.novafoundation.nova.feature_swap_impl.data.assetExchange.crossChain +import android.util.Log +import io.novafoundation.nova.common.utils.firstNotNull import io.novafoundation.nova.common.utils.graph.Edge import io.novafoundation.nova.feature_account_api.domain.interfaces.AccountRepository import io.novafoundation.nova.feature_account_api.domain.model.MetaAccount @@ -17,12 +19,15 @@ import io.novafoundation.nova.feature_swap_impl.data.assetExchange.AssetExchange import io.novafoundation.nova.feature_swap_impl.data.assetExchange.FeePaymentProviderOverride import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.tranfers.AssetTransferBase import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.types.Balance +import io.novafoundation.nova.feature_wallet_api.domain.implementations.availableInDestinations import io.novafoundation.nova.feature_wallet_api.domain.interfaces.CrossChainTransfersUseCase +import io.novafoundation.nova.feature_wallet_api.domain.model.CrossChainTransfersConfiguration import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry import io.novafoundation.nova.runtime.multiNetwork.chain.model.FullChainAssetId import io.novafoundation.nova.runtime.multiNetwork.chainWithAsset import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.emptyFlow import java.math.BigInteger @@ -55,12 +60,17 @@ class CrossChainTransferAssetExchange( private val swapHost: AssetExchange.SwapHost, ) : AssetExchange { + private val crossChainConfig = MutableStateFlow(null) + override suspend fun sync() { crossChainTransfersUseCase.syncCrossChainConfig() + + crossChainConfig.emit(crossChainTransfersUseCase.getConfiguration()) } override suspend fun availableDirectSwapConnections(): List { - return crossChainTransfersUseCase.allDirections().map(::CrossChainTransferEdge) + val config = crossChainConfig.firstNotNull() + return config.availableInDestinations().map(::CrossChainTransferEdge) } override fun feePaymentOverrides(): List { @@ -94,6 +104,17 @@ class CrossChainTransferAssetExchange( return false } + override suspend fun canPayNonNativeFeesInIntermediatePosition(): Boolean { + val config = crossChainConfig.value ?: return false + + // Delivery fees cannot be paid in non-native assets + return (delegate.from.chainId !in config.deliveryFeeConfigurations).also { + if (!it) { + Log.d("Swaps", "Filtered out $delegate due to delivery fee restrictions") + } + } + } + override suspend fun quote(amount: BigInteger, direction: SwapDirection): BigInteger { return amount } diff --git a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/hydraDx/HydraDxExchange.kt b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/hydraDx/HydraDxExchange.kt index ce1a552219..466f20f1ee 100644 --- a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/hydraDx/HydraDxExchange.kt +++ b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/hydraDx/HydraDxExchange.kt @@ -224,9 +224,13 @@ private class HydraDxExchange( } override suspend fun shouldIgnoreFeeRequirementAfter(predecessor: SwapGraphEdge): Boolean { - // When staking multiple hydra edges together, the fee is always paid with the starting edge + // When chaining multiple hydra edges together, the fee is always paid with the starting edge return predecessor is HydraDxSwapEdge } + + override suspend fun canPayNonNativeFeesInIntermediatePosition(): Boolean { + return true + } } inner class HydraDxOperation private constructor( diff --git a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/domain/swap/RealSwapService.kt b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/domain/swap/RealSwapService.kt index e6d71e4604..103e353dcd 100644 --- a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/domain/swap/RealSwapService.kt +++ b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/domain/swap/RealSwapService.kt @@ -543,6 +543,7 @@ internal class RealSwapService( val feeCapability = getFeeCustomFeeCapability(edge.from.chainId) return feeCapability != null && feeCapability.canPayFeeInNonUtilityToken(edge.from.assetId) + && edge.canPayNonNativeFeesInIntermediatePosition() } } diff --git a/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/domain/interfaces/CrossChainTransfersUseCase.kt b/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/domain/interfaces/CrossChainTransfersUseCase.kt index 0c0fe7d258..199255531d 100644 --- a/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/domain/interfaces/CrossChainTransfersUseCase.kt +++ b/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/domain/interfaces/CrossChainTransfersUseCase.kt @@ -1,13 +1,12 @@ package io.novafoundation.nova.feature_wallet_api.domain.interfaces -import io.novafoundation.nova.common.utils.graph.Edge import io.novafoundation.nova.feature_account_api.data.extrinsic.ExtrinsicService import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.tranfers.AssetTransferBase import io.novafoundation.nova.feature_wallet_api.domain.model.Asset import io.novafoundation.nova.feature_wallet_api.domain.model.CrossChainTransferFee +import io.novafoundation.nova.feature_wallet_api.domain.model.CrossChainTransfersConfiguration import io.novafoundation.nova.runtime.multiNetwork.ChainWithAsset import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain -import io.novafoundation.nova.runtime.multiNetwork.chain.model.FullChainAssetId import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.map @@ -27,7 +26,7 @@ interface CrossChainTransfersUseCase { fun outcomingCrossChainDirectionsFlow(origin: Chain.Asset): Flow> - suspend fun allDirections(): List> + suspend fun getConfiguration(): CrossChainTransfersConfiguration suspend fun ExtrinsicService.estimateFee( transfer: AssetTransferBase, diff --git a/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/domain/RealCrossChainTransfersUseCase.kt b/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/domain/RealCrossChainTransfersUseCase.kt index 32b49bb4d6..3e938a2102 100644 --- a/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/domain/RealCrossChainTransfersUseCase.kt +++ b/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/domain/RealCrossChainTransfersUseCase.kt @@ -2,7 +2,6 @@ package io.novafoundation.nova.feature_wallet_impl.domain import io.novafoundation.nova.common.data.memory.ComputationalCache import io.novafoundation.nova.common.utils.combineToPair -import io.novafoundation.nova.common.utils.graph.Edge import io.novafoundation.nova.common.utils.isPositive import io.novafoundation.nova.common.utils.withFlowScope import io.novafoundation.nova.feature_account_api.data.extrinsic.ExtrinsicService @@ -28,7 +27,6 @@ import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry import io.novafoundation.nova.runtime.multiNetwork.ChainWithAsset import io.novafoundation.nova.runtime.multiNetwork.assets import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain -import io.novafoundation.nova.runtime.multiNetwork.chain.model.FullChainAssetId import io.novafoundation.nova.runtime.multiNetwork.chainsById import io.novafoundation.nova.runtime.repository.ParachainInfoRepository import kotlinx.coroutines.CoroutineScope @@ -99,11 +97,11 @@ internal class RealCrossChainTransfersUseCase( }.catch { emit(emptyList()) } } - override suspend fun allDirections(): List> { - val config = crossChainTransfersRepository.getConfiguration() - return config.availableInDestinations() + override suspend fun getConfiguration(): CrossChainTransfersConfiguration { + return crossChainTransfersRepository.getConfiguration() } + override suspend fun ExtrinsicService.estimateFee( transfer: AssetTransferBase, computationalScope: CoroutineScope From 0411604d2c2569941866fcaa286bd7488c47327f Mon Sep 17 00:00:00 2001 From: Valentun Date: Mon, 21 Oct 2024 11:34:48 +0300 Subject: [PATCH 20/83] Submit hydration extrinsic and better fees --- .../nova/SwapServiceIntegrationTest.kt | 6 +- build.gradle | 2 +- .../data/extrinsic/ExtrinsicService.kt | 29 +--- .../data/extrinsic/SubmissionOrigin.kt | 39 +++++ .../data/extrinsic/execution/DispatchError.kt | 65 ++++++++ .../extrinsic/execution/ExtrinsicDispatch.kt | 45 +++++ .../feature_account_api/data/model/Fee.kt | 55 ++++++- .../data/extrinsic/RealExtrinsicService.kt | 76 ++++++++- .../extrinsic/RealExtrinsicServiceFactory.kt | 3 + .../types/hydra/RealHydrationFeeInjector.kt | 4 +- .../di/AccountFeatureDependencies.kt | 3 + .../di/AccountFeatureModule.kt | 41 +++-- .../domain/model/AtomicSwapOperation.kt | 56 +++++-- .../feature_swap_api/domain/model/SwapFee.kt | 57 +++++++ .../domain/model/SwapProgress.kt | 10 ++ .../domain/model/SwapQuote.kt | 12 -- .../domain/model/SwapQuoteArgs.kt | 61 ++++++- .../domain/swap/SwapService.kt | 8 +- .../data/assetExchange/AssetExchange.kt | 2 +- .../AssetConversionExchange.kt | 40 ++++- .../CrossChainTransferAssetExchange.kt | 45 +++-- .../assetExchange/hydraDx/HydraDxExchange.kt | 155 +++++++++++++----- .../hydraDx/HydraDxSwapSource.kt | 12 +- .../hydraDx/omnipool/OmniPoolSwapSource.kt | 27 ++- .../hydraDx/stableswap/StableSwapSource.kt | 3 +- .../hydraDx/xyk/XYKSwapSource.kt | 3 +- .../SwapTransactionHistoryRepository.kt | 6 +- .../domain/interactor/SwapInteractor.kt | 10 +- .../domain/swap/RealSwapService.kt | 95 +++++++++-- .../validation/SwapPayloadValidation.kt | 4 +- .../confirmation/SwapConfirmationViewModel.kt | 42 +++-- .../main/SwapMainSettingsViewModel.kt | 2 - .../maxAction/MaxActionProviderFactory.kt | 8 +- .../domain/model/CrossChainFee.kt | 2 +- .../maxAction/FeeAwareMaxActionProvider.kt | 36 ++++ .../maxAction/MaxActionProvider.kt | 8 +- .../domain/RealCrossChainTransfersUseCase.kt | 5 +- .../runtime/repository/EventsRepository.kt | 51 ++++++ .../runtime/repository/ExtrinsicWithEvents.kt | 14 ++ 39 files changed, 931 insertions(+), 211 deletions(-) create mode 100644 feature-account-api/src/main/java/io/novafoundation/nova/feature_account_api/data/extrinsic/SubmissionOrigin.kt create mode 100644 feature-account-api/src/main/java/io/novafoundation/nova/feature_account_api/data/extrinsic/execution/DispatchError.kt create mode 100644 feature-account-api/src/main/java/io/novafoundation/nova/feature_account_api/data/extrinsic/execution/ExtrinsicDispatch.kt create mode 100644 feature-swap-api/src/main/java/io/novafoundation/nova/feature_swap_api/domain/model/SwapFee.kt create mode 100644 feature-swap-api/src/main/java/io/novafoundation/nova/feature_swap_api/domain/model/SwapProgress.kt diff --git a/app/src/androidTest/java/io/novafoundation/nova/SwapServiceIntegrationTest.kt b/app/src/androidTest/java/io/novafoundation/nova/SwapServiceIntegrationTest.kt index 2d589120dd..c4c00215bc 100644 --- a/app/src/androidTest/java/io/novafoundation/nova/SwapServiceIntegrationTest.kt +++ b/app/src/androidTest/java/io/novafoundation/nova/SwapServiceIntegrationTest.kt @@ -4,7 +4,7 @@ import android.util.Log import io.novafoundation.nova.common.di.FeatureUtils import io.novafoundation.nova.common.utils.Percent import io.novafoundation.nova.feature_swap_api.di.SwapFeatureApi -import io.novafoundation.nova.feature_swap_api.domain.model.SwapExecuteArgs +import io.novafoundation.nova.feature_swap_api.domain.model.SwapFeeArgs import io.novafoundation.nova.feature_swap_api.domain.model.SwapLimit import io.novafoundation.nova.feature_swap_api.domain.model.SwapQuote import io.novafoundation.nova.feature_swap_api.domain.model.SwapQuoteArgs @@ -79,7 +79,7 @@ class SwapServiceIntegrationTest : BaseIntegrationTest() { val wnd = westmint.wnd() val siri = westmint.siri() - val swapArgs = SwapExecuteArgs( + val swapArgs = SwapFeeArgs( assetIn = wnd, assetOut = siri, swapLimit = SwapLimit.SpecifiedIn( @@ -103,7 +103,7 @@ class SwapServiceIntegrationTest : BaseIntegrationTest() { val wnd = westmint.wnd() val siri = westmint.siri() - val swapArgs = SwapExecuteArgs( + val swapArgs = SwapFeeArgs( assetIn = siri, assetOut = wnd, swapLimit = SwapLimit.SpecifiedIn( diff --git a/build.gradle b/build.gradle index 0c680b1617..0914967bdf 100644 --- a/build.gradle +++ b/build.gradle @@ -51,7 +51,7 @@ buildscript { web3jVersion = '4.9.5' - substrateSdkVersion = '2.2.0' + substrateSdkVersion = '2.2.1' gifVersion = '1.2.19' diff --git a/feature-account-api/src/main/java/io/novafoundation/nova/feature_account_api/data/extrinsic/ExtrinsicService.kt b/feature-account-api/src/main/java/io/novafoundation/nova/feature_account_api/data/extrinsic/ExtrinsicService.kt index 4f8e9f1d61..4be4b05671 100644 --- a/feature-account-api/src/main/java/io/novafoundation/nova/feature_account_api/data/extrinsic/ExtrinsicService.kt +++ b/feature-account-api/src/main/java/io/novafoundation/nova/feature_account_api/data/extrinsic/ExtrinsicService.kt @@ -3,6 +3,7 @@ package io.novafoundation.nova.feature_account_api.data.extrinsic import io.novafoundation.nova.common.data.network.runtime.model.FeeResponse import io.novafoundation.nova.common.utils.multiResult.RetriableMultiResult import io.novafoundation.nova.feature_account_api.data.ethereum.transaction.TransactionOrigin +import io.novafoundation.nova.feature_account_api.data.extrinsic.execution.ExtrinsicExecutionResult import io.novafoundation.nova.feature_account_api.data.fee.FeePaymentCurrency import io.novafoundation.nova.feature_account_api.data.fee.FeePaymentProviderRegistry import io.novafoundation.nova.feature_account_api.data.model.Fee @@ -10,10 +11,8 @@ import io.novafoundation.nova.runtime.extrinsic.ExtrinsicStatus import io.novafoundation.nova.runtime.extrinsic.multi.CallBuilder import io.novafoundation.nova.runtime.extrinsic.signer.FeeSigner import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain -import io.novasama.substrate_sdk_android.runtime.AccountId import io.novasama.substrate_sdk_android.runtime.extrinsic.BatchMode import io.novasama.substrate_sdk_android.runtime.extrinsic.ExtrinsicBuilder -import io.novasama.substrate_sdk_android.runtime.extrinsic.signer.Signer import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.Flow @@ -22,25 +21,6 @@ typealias FormExtrinsicWithOrigin = suspend ExtrinsicBuilder.(origin: Submission typealias FormMultiExtrinsicWithOrigin = suspend CallBuilder.(origin: SubmissionOrigin) -> Unit typealias FormMultiExtrinsic = suspend CallBuilder.() -> Unit -class SubmissionOrigin( - /** - * Origin that was originally requested to sign the transaction - */ - val requestedOrigin: AccountId, - - /** - * Origin that was actually used to sign the transaction. - * It might differ from [requestedOrigin] if [Signer] modified the origin, for example in the case of Proxied wallet - */ - val actualOrigin: AccountId -) { - - companion object { - - fun singleOrigin(origin: AccountId) = SubmissionOrigin(origin, origin) - } -} - class ExtrinsicSubmission(val hash: String, val submissionOrigin: SubmissionOrigin) private val DEFAULT_BATCH_MODE = BatchMode.BATCH_ALL @@ -79,6 +59,13 @@ interface ExtrinsicService { formExtrinsic: FormExtrinsicWithOrigin ): Result> + suspend fun submitExtrinsicAndAwaitExecution( + chain: Chain, + origin: TransactionOrigin, + submissionOptions: SubmissionOptions = SubmissionOptions(), + formExtrinsic: FormExtrinsicWithOrigin + ): Result + suspend fun submitMultiExtrinsicAwaitingInclusion( chain: Chain, origin: TransactionOrigin, diff --git a/feature-account-api/src/main/java/io/novafoundation/nova/feature_account_api/data/extrinsic/SubmissionOrigin.kt b/feature-account-api/src/main/java/io/novafoundation/nova/feature_account_api/data/extrinsic/SubmissionOrigin.kt new file mode 100644 index 0000000000..99aafde1ec --- /dev/null +++ b/feature-account-api/src/main/java/io/novafoundation/nova/feature_account_api/data/extrinsic/SubmissionOrigin.kt @@ -0,0 +1,39 @@ +package io.novafoundation.nova.feature_account_api.data.extrinsic + +import io.novasama.substrate_sdk_android.runtime.AccountId +import io.novasama.substrate_sdk_android.runtime.extrinsic.signer.Signer + +data class SubmissionOrigin( + /** + * Origin that was originally requested to sign the transaction + */ + val requestedOrigin: AccountId, + + /** + * Origin that was actually used to sign the transaction. + * It might differ from [requestedOrigin] if [Signer] modified the origin, for example in the case of Proxied wallet + */ + val actualOrigin: AccountId +) { + + companion object { + + fun singleOrigin(origin: AccountId) = SubmissionOrigin(origin, origin) + } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as SubmissionOrigin + + if (!requestedOrigin.contentEquals(other.requestedOrigin)) return false + return actualOrigin.contentEquals(other.actualOrigin) + } + + override fun hashCode(): Int { + var result = requestedOrigin.contentHashCode() + result = 31 * result + actualOrigin.contentHashCode() + return result + } +} diff --git a/feature-account-api/src/main/java/io/novafoundation/nova/feature_account_api/data/extrinsic/execution/DispatchError.kt b/feature-account-api/src/main/java/io/novafoundation/nova/feature_account_api/data/extrinsic/execution/DispatchError.kt new file mode 100644 index 0000000000..6dce7ac5b5 --- /dev/null +++ b/feature-account-api/src/main/java/io/novafoundation/nova/feature_account_api/data/extrinsic/execution/DispatchError.kt @@ -0,0 +1,65 @@ +package io.novafoundation.nova.feature_account_api.data.extrinsic.execution + +import io.novafoundation.nova.common.data.network.runtime.binding.bindInt +import io.novafoundation.nova.common.data.network.runtime.binding.castToDictEnum +import io.novafoundation.nova.common.data.network.runtime.binding.castToStruct +import io.novasama.substrate_sdk_android.runtime.RuntimeSnapshot +import io.novasama.substrate_sdk_android.runtime.metadata.error +import io.novasama.substrate_sdk_android.runtime.metadata.module +import io.novasama.substrate_sdk_android.runtime.metadata.module.ErrorMetadata +import io.novasama.substrate_sdk_android.runtime.metadata.module.Module as RuntimeModule + +sealed class DispatchError: Throwable() { + + data class Module(val module: RuntimeModule, val error: ErrorMetadata) : DispatchError() { + + override val message: String + get() = toString() + + override fun toString(): String { + return "${module.name}.${error.name}" + } + } + + object Token : DispatchError() { + + override val message: String + get() = toString() + + override fun toString(): String { + return "Not enough tokens" + } + } + + object Unknown : DispatchError() +} + +fun bindDispatchError(decoded: Any?, runtimeSnapshot: RuntimeSnapshot): DispatchError { + val asDictEnum = decoded.castToDictEnum() + + return when (asDictEnum.name) { + "Module" -> { + val moduleErrorStruct = asDictEnum.value.castToStruct() + + val moduleIndex = bindInt(moduleErrorStruct["index"]) + val errorIndex = bindModuleError(moduleErrorStruct["error"]) + + val module = runtimeSnapshot.metadata.module(moduleIndex) + val error = module.error(errorIndex) + + DispatchError.Module(module, error) + } + + "Token" -> DispatchError.Token + + else -> DispatchError.Unknown + } +} + +private fun bindModuleError(errorEncoded: ByteArray?): Int { + requireNotNull(errorEncoded) { + "Error should exist" + } + + return errorEncoded[0].toInt() +} diff --git a/feature-account-api/src/main/java/io/novafoundation/nova/feature_account_api/data/extrinsic/execution/ExtrinsicDispatch.kt b/feature-account-api/src/main/java/io/novafoundation/nova/feature_account_api/data/extrinsic/execution/ExtrinsicDispatch.kt new file mode 100644 index 0000000000..c666c42d40 --- /dev/null +++ b/feature-account-api/src/main/java/io/novafoundation/nova/feature_account_api/data/extrinsic/execution/ExtrinsicDispatch.kt @@ -0,0 +1,45 @@ +package io.novafoundation.nova.feature_account_api.data.extrinsic.execution + +import io.novafoundation.nova.common.data.network.runtime.binding.BlockHash +import io.novasama.substrate_sdk_android.runtime.definitions.types.generics.GenericEvent + +data class ExtrinsicExecutionResult( + val extrinsicHash: String, + val blockHash: BlockHash, + val outcome: ExtrinsicDispatch +) + +sealed interface ExtrinsicDispatch { + + data class Ok(val emittedEvents: List): ExtrinsicDispatch + + data class Failed(val error: DispatchError) : ExtrinsicDispatch + + object Unknown : ExtrinsicDispatch +} + +fun ExtrinsicExecutionResult.requireOk(): ExtrinsicDispatch.Ok { + return when(outcome) { + is ExtrinsicDispatch.Failed -> throw outcome.error + is ExtrinsicDispatch.Ok -> outcome + ExtrinsicDispatch.Unknown -> throw IllegalArgumentException("Unknown extrinsic execution result") + } +} + +fun Result.requireOk(): Result { + return mapCatching { + it.requireOk() + } +} + + +fun ExtrinsicDispatch.isOk(): Boolean { + return this is ExtrinsicDispatch.Ok +} + +fun ExtrinsicDispatch.isModuleError(moduleName: String, errorName: String): Boolean { + return this is ExtrinsicDispatch.Failed + && error is DispatchError.Module + && error.module.name == moduleName + && error.error.name == errorName +} 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 0cdbe332f8..885c28cf12 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 @@ -2,8 +2,10 @@ package io.novafoundation.nova.feature_account_api.data.model import io.novafoundation.nova.feature_account_api.data.extrinsic.SubmissionOrigin import io.novafoundation.nova.feature_account_api.data.fee.FeePaymentCurrency +import io.novafoundation.nova.runtime.ext.fullId import io.novafoundation.nova.runtime.ext.utilityAsset import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain +import io.novasama.substrate_sdk_android.runtime.AccountId import java.math.BigInteger // TODO rename FeeBase -> Fee and use SubmissionFee everywhere Fee is currently used @@ -30,12 +32,33 @@ interface FeeBase { val asset: Chain.Asset } +infix fun FeeBase.hasSameAssetAs(other: FeeBase): Boolean { + return asset.fullId == other.asset.fullId +} + +infix fun Fee.addPreservingOrigin(other: FeeBase): Fee { + require(this hasSameAssetAs other) { + "Cannot sum fees with different assets" + } + + return addPlanks(other.amount) +} + +infix fun Fee.addPlanks(planks: BigInteger): Fee { + return SubstrateFee(amount + planks, submissionOrigin, asset) +} + +fun Fee.replacePlanks(newPlanks: BigInteger): Fee { + return SubstrateFee(newPlanks, submissionOrigin, asset) +} + data class EvmFee( val gasLimit: BigInteger, val gasPrice: BigInteger, override val submissionOrigin: SubmissionOrigin, override val asset: Chain.Asset ) : Fee { + override val amount = gasLimit * gasPrice } @@ -48,7 +71,7 @@ class SubstrateFee( class SubstrateFeeBase( override val amount: BigInteger, override val asset: Chain.Asset -): FeeBase +) : FeeBase val Fee.requestedAccountPaysFees: Boolean get() = submissionOrigin.requestedOrigin.contentEquals(submissionOrigin.actualOrigin) @@ -56,6 +79,36 @@ val Fee.requestedAccountPaysFees: Boolean val Fee.amountByRequestedAccount: BigInteger get() = amount.asAmountByRequestedAccount +fun List.totalAmount(chainAsset: Chain.Asset): BigInteger { + return sumOf { it.getAmount(chainAsset) } +} + +fun List.totalAmount(chainAsset: Chain.Asset, origin: AccountId): BigInteger { + return sumOf { it.getAmount(chainAsset, origin) } +} + +fun List.totalPlanksEnsuringAsset(requireAsset: Chain.Asset): BigInteger { + return sumOf { + require(it.asset.fullId == requireAsset.fullId) { + "Atomic operation fee contains fee in different assets" + } + + it.amount + } +} + +fun SubmissionFee.getAmount(chainAsset: Chain.Asset, origin: AccountId): BigInteger { + return if (asset.fullId == chainAsset.fullId && submissionOrigin.actualOrigin.contentEquals(origin)) { + amount + } else { + BigInteger.ZERO + } +} + +fun FeeBase.getAmount(expectedAsset: Chain.Asset): BigInteger { + return if (expectedAsset.fullId == asset.fullId) amount else BigInteger.ZERO +} + context(Fee) val BigInteger.asAmountByRequestedAccount: BigInteger get() = if (requestedAccountPaysFees) { diff --git a/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/data/extrinsic/RealExtrinsicService.kt b/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/data/extrinsic/RealExtrinsicService.kt index 6b31989f87..14eb7776ce 100644 --- a/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/data/extrinsic/RealExtrinsicService.kt +++ b/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/data/extrinsic/RealExtrinsicService.kt @@ -1,6 +1,8 @@ package io.novafoundation.nova.feature_account_impl.data.extrinsic +import android.util.Log import io.novafoundation.nova.common.data.network.runtime.model.FeeResponse +import io.novafoundation.nova.common.utils.LOG_TAG import io.novafoundation.nova.common.utils.multiResult.RetriableMultiResult import io.novafoundation.nova.common.utils.multiResult.runMultiCatching import io.novafoundation.nova.common.utils.orZero @@ -15,6 +17,11 @@ import io.novafoundation.nova.feature_account_api.data.extrinsic.FormExtrinsicWi import io.novafoundation.nova.feature_account_api.data.extrinsic.FormMultiExtrinsic import io.novafoundation.nova.feature_account_api.data.extrinsic.FormMultiExtrinsicWithOrigin import io.novafoundation.nova.feature_account_api.data.extrinsic.SubmissionOrigin +import io.novafoundation.nova.feature_account_api.data.extrinsic.awaitInBlock +import io.novafoundation.nova.feature_account_api.data.extrinsic.execution.DispatchError +import io.novafoundation.nova.feature_account_api.data.extrinsic.execution.ExtrinsicDispatch +import io.novafoundation.nova.feature_account_api.data.extrinsic.execution.ExtrinsicExecutionResult +import io.novafoundation.nova.feature_account_api.data.extrinsic.execution.bindDispatchError import io.novafoundation.nova.feature_account_api.data.fee.FeePaymentProviderRegistry import io.novafoundation.nova.feature_account_api.data.model.Fee import io.novafoundation.nova.feature_account_api.data.model.SubstrateFee @@ -33,9 +40,15 @@ import io.novafoundation.nova.runtime.extrinsic.signer.FeeSigner import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain import io.novafoundation.nova.runtime.multiNetwork.getRuntime +import io.novafoundation.nova.runtime.multiNetwork.runtime.repository.EventsRepository +import io.novafoundation.nova.runtime.multiNetwork.runtime.repository.ExtrinsicWithEvents +import io.novafoundation.nova.runtime.multiNetwork.runtime.repository.findExtrinsicFailureOrThrow +import io.novafoundation.nova.runtime.multiNetwork.runtime.repository.isSuccess import io.novafoundation.nova.runtime.network.rpc.RpcCalls +import io.novasama.substrate_sdk_android.runtime.RuntimeSnapshot import io.novasama.substrate_sdk_android.runtime.definitions.types.fromHex import io.novasama.substrate_sdk_android.runtime.definitions.types.generics.Extrinsic +import io.novasama.substrate_sdk_android.runtime.definitions.types.generics.GenericEvent import io.novasama.substrate_sdk_android.runtime.extrinsic.ExtrinsicBuilder import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.coroutineScope @@ -52,6 +65,7 @@ class RealExtrinsicService( private val signerProvider: SignerProvider, private val extrinsicSplitter: ExtrinsicSplitter, private val feePaymentProviderRegistry: FeePaymentProviderRegistry, + private val eventsRepository: EventsRepository, private val coroutineScope: CoroutineScope? // TODO: Make it non-nullable ) : ExtrinsicService { @@ -99,6 +113,17 @@ class RealExtrinsicService( .takeWhileInclusive { !it.terminal } } + override suspend fun submitExtrinsicAndAwaitExecution( + chain: Chain, + origin: TransactionOrigin, + submissionOptions: SubmissionOptions, + formExtrinsic: FormExtrinsicWithOrigin + ): Result { + return submitAndWatchExtrinsic(chain, origin, submissionOptions, formExtrinsic) + .awaitInBlock() + .map { determineExtrinsicOutcome(it, chain) } + } + override suspend fun paymentInfo( chain: Chain, origin: TransactionOrigin, @@ -122,9 +147,15 @@ class RealExtrinsicService( val signer = getFeeSigner(chain, origin) val extrinsicBuilder = extrinsicBuilderFactory.createForFee(signer, chain) extrinsicBuilder.formExtrinsic() + + val feePaymentProvider = feePaymentProviderRegistry.providerFor(chain.id) + val feePayment = feePaymentProvider.feePaymentFor(submissionOptions.feePaymentCurrency, coroutineScope) + + feePayment.modifyExtrinsic(extrinsicBuilder) val extrinsic = extrinsicBuilder.buildExtrinsic(submissionOptions.batchMode).extrinsicHex - return estimateFee(chain, extrinsic, signer, submissionOptions) + val nativeFee = estimateNativeFee(chain, extrinsic, signer.submissionOrigin(chain)) + return feePayment.convertNativeFee(nativeFee) } override suspend fun estimateFee( @@ -182,6 +213,49 @@ class RealExtrinsicService( return feePayment.convertNativeFee(totalNativeFee) } + private suspend fun determineExtrinsicOutcome( + inBlock: ExtrinsicStatus.InBlock, + chain: Chain + ): ExtrinsicExecutionResult { + val outcome = runCatching { + val extrinsicWithEvents = eventsRepository.getExtrinsicWithEvents(chain.id, inBlock.extrinsicHash, inBlock.blockHash) + val runtime = chainRegistry.getRuntime(chain.id) + + requireNotNull(extrinsicWithEvents) { + "No extrinsic included into expected block" + } + + extrinsicWithEvents.determineOutcome(runtime) + }.getOrElse { + Log.w(LOG_TAG, "Failed to determine extrinsic outcome", it) + + ExtrinsicDispatch.Unknown + } + + return ExtrinsicExecutionResult( + extrinsicHash = inBlock.extrinsicHash, + blockHash = inBlock.blockHash, + outcome = outcome + ) + } + + private fun ExtrinsicWithEvents.determineOutcome(runtimeSnapshot: RuntimeSnapshot): ExtrinsicDispatch { + return if (isSuccess()) { + ExtrinsicDispatch.Ok(events) + } else { + val errorEvent = events.findExtrinsicFailureOrThrow() + val dispatchError = parseErrorEvent(errorEvent, runtimeSnapshot) + + ExtrinsicDispatch.Failed(dispatchError) + } + } + + private fun parseErrorEvent(errorEvent: GenericEvent.Instance, runtimeSnapshot: RuntimeSnapshot): DispatchError { + val dispatchError = errorEvent.arguments.first() + + return bindDispatchError(dispatchError, runtimeSnapshot) + } + private suspend fun constructSplitExtrinsicsForSubmission( chain: Chain, origin: TransactionOrigin, diff --git a/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/data/extrinsic/RealExtrinsicServiceFactory.kt b/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/data/extrinsic/RealExtrinsicServiceFactory.kt index 21f017a727..ebfb001f89 100644 --- a/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/data/extrinsic/RealExtrinsicServiceFactory.kt +++ b/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/data/extrinsic/RealExtrinsicServiceFactory.kt @@ -7,6 +7,7 @@ import io.novafoundation.nova.feature_account_api.domain.interfaces.AccountRepos import io.novafoundation.nova.runtime.extrinsic.ExtrinsicBuilderFactory import io.novafoundation.nova.runtime.extrinsic.multi.ExtrinsicSplitter import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry +import io.novafoundation.nova.runtime.multiNetwork.runtime.repository.EventsRepository import io.novafoundation.nova.runtime.network.rpc.RpcCalls class RealExtrinsicServiceFactory( @@ -16,6 +17,7 @@ class RealExtrinsicServiceFactory( private val extrinsicBuilderFactory: ExtrinsicBuilderFactory, private val signerProvider: SignerProvider, private val extrinsicSplitter: ExtrinsicSplitter, + private val eventsRepository: EventsRepository, private val feePaymentProviderRegistry: FeePaymentProviderRegistry ) : ExtrinsicService.Factory { @@ -29,6 +31,7 @@ class RealExtrinsicServiceFactory( signerProvider = signerProvider, extrinsicSplitter = extrinsicSplitter, feePaymentProviderRegistry = registry, + eventsRepository = eventsRepository, coroutineScope = feeConfig.coroutineScope ) } diff --git a/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/data/fee/types/hydra/RealHydrationFeeInjector.kt b/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/data/fee/types/hydra/RealHydrationFeeInjector.kt index 69ace37045..1b89b05a42 100644 --- a/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/data/fee/types/hydra/RealHydrationFeeInjector.kt +++ b/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/data/fee/types/hydra/RealHydrationFeeInjector.kt @@ -17,11 +17,11 @@ internal class RealHydrationFeeInjector( paymentAsset: Chain.Asset, mode: HydrationFeeInjector.SetFeesMode ) { - val baseCall = extrinsicBuilder.getCall() + val baseCalls = extrinsicBuilder.getCalls() extrinsicBuilder.resetCalls() val justSetFees = getSetPhase(mode.setMode).setFees(extrinsicBuilder, paymentAsset) - extrinsicBuilder.call(baseCall) + extrinsicBuilder.calls(baseCalls) getResetPhase(mode.resetMode).resetFees(extrinsicBuilder, justSetFees) } diff --git a/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/di/AccountFeatureDependencies.kt b/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/di/AccountFeatureDependencies.kt index ccde5dec8c..69883326e6 100644 --- a/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/di/AccountFeatureDependencies.kt +++ b/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/di/AccountFeatureDependencies.kt @@ -57,6 +57,7 @@ import io.novafoundation.nova.runtime.extrinsic.multi.ExtrinsicSplitter import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry import io.novafoundation.nova.runtime.multiNetwork.multiLocation.converter.MultiLocationConverterFactory import io.novafoundation.nova.runtime.multiNetwork.qr.MultiChainQrSharingFactory +import io.novafoundation.nova.runtime.multiNetwork.runtime.repository.EventsRepository import io.novafoundation.nova.runtime.network.rpc.RpcCalls import io.novafoundation.nova.runtime.storage.source.StorageDataSource import io.novafoundation.nova.web3names.domain.networking.Web3NamesInteractor @@ -189,4 +190,6 @@ interface AccountFeatureDependencies { val storageSharedRequestsBuilderFactory: StorageSharedRequestsBuilderFactory val storageCache: StorageCache + + val eventsRepository: EventsRepository } diff --git a/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/di/AccountFeatureModule.kt b/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/di/AccountFeatureModule.kt index 7f958def26..283f760bb4 100644 --- a/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/di/AccountFeatureModule.kt +++ b/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/di/AccountFeatureModule.kt @@ -24,9 +24,12 @@ import io.novafoundation.nova.core.model.CryptoType import io.novafoundation.nova.core_db.dao.AccountDao import io.novafoundation.nova.core_db.dao.MetaAccountDao import io.novafoundation.nova.core_db.dao.NodeDao +import io.novafoundation.nova.feature_account_api.data.cloudBackup.LocalAccountsCloudBackupFacade import io.novafoundation.nova.feature_account_api.data.ethereum.transaction.EvmTransactionService import io.novafoundation.nova.feature_account_api.data.events.MetaAccountChangesEventBus import io.novafoundation.nova.feature_account_api.data.extrinsic.ExtrinsicService +import io.novafoundation.nova.feature_account_api.data.fee.FeePaymentProviderRegistry +import io.novafoundation.nova.feature_account_api.data.fee.capability.CustomFeeCapabilityFacade import io.novafoundation.nova.feature_account_api.data.proxy.MetaAccountsUpdatesRegistry import io.novafoundation.nova.feature_account_api.data.proxy.ProxySyncService import io.novafoundation.nova.feature_account_api.data.repository.OnChainIdentityRepository @@ -35,12 +38,14 @@ import io.novafoundation.nova.feature_account_api.data.signer.SignerProvider import io.novafoundation.nova.feature_account_api.domain.account.common.EncryptionDefaults import io.novafoundation.nova.feature_account_api.domain.account.identity.IdentityProvider import io.novafoundation.nova.feature_account_api.domain.account.identity.OnChainIdentity +import io.novafoundation.nova.feature_account_api.domain.cloudBackup.ApplyLocalSnapshotToCloudBackupUseCase import io.novafoundation.nova.feature_account_api.domain.interfaces.AccountInteractor import io.novafoundation.nova.feature_account_api.domain.interfaces.AccountRepository 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.domain.updaters.AccountUpdateScope import io.novafoundation.nova.feature_account_api.presenatation.account.AddressDisplayUseCase +import io.novafoundation.nova.feature_account_api.presenatation.account.common.listing.MetaAccountTypePresentationMapper import io.novafoundation.nova.feature_account_api.presenatation.account.polkadotVault.config.PolkadotVaultVariantConfigProvider import io.novafoundation.nova.feature_account_api.presenatation.account.wallet.WalletUiUseCase import io.novafoundation.nova.feature_account_api.presenatation.actions.ExternalActions @@ -57,13 +62,11 @@ import io.novafoundation.nova.feature_account_api.presenatation.mixin.selectWall import io.novafoundation.nova.feature_account_impl.BuildConfig import io.novafoundation.nova.feature_account_impl.RealBiometricServiceFactory import io.novafoundation.nova.feature_account_impl.data.cloudBackup.CloudBackupAccountsModificationsTracker -import io.novafoundation.nova.feature_account_api.data.cloudBackup.LocalAccountsCloudBackupFacade -import io.novafoundation.nova.feature_account_api.data.fee.FeePaymentProviderRegistry -import io.novafoundation.nova.feature_account_impl.data.fee.capability.RealCustomCustomFeeCapabilityFacade -import io.novafoundation.nova.feature_account_api.domain.cloudBackup.ApplyLocalSnapshotToCloudBackupUseCase import io.novafoundation.nova.feature_account_impl.data.ethereum.transaction.RealEvmTransactionService import io.novafoundation.nova.feature_account_impl.data.events.RealMetaAccountChangesEventBus import io.novafoundation.nova.feature_account_impl.data.extrinsic.RealExtrinsicService +import io.novafoundation.nova.feature_account_impl.data.extrinsic.RealExtrinsicServiceFactory +import io.novafoundation.nova.feature_account_impl.data.fee.capability.RealCustomCustomFeeCapabilityFacade import io.novafoundation.nova.feature_account_impl.data.mappers.AccountMappers import io.novafoundation.nova.feature_account_impl.data.network.blockchain.AccountSubstrateSource import io.novafoundation.nova.feature_account_impl.data.network.blockchain.AccountSubstrateSourceImpl @@ -82,6 +85,7 @@ import io.novafoundation.nova.feature_account_impl.data.repository.datasource.mi import io.novafoundation.nova.feature_account_impl.data.secrets.AccountSecretsFactory import io.novafoundation.nova.feature_account_impl.di.modules.AdvancedEncryptionStoreModule import io.novafoundation.nova.feature_account_impl.di.modules.CloudBackupModule +import io.novafoundation.nova.feature_account_impl.di.modules.CustomFeeModule import io.novafoundation.nova.feature_account_impl.di.modules.IdentityProviderModule import io.novafoundation.nova.feature_account_impl.di.modules.ParitySignerModule import io.novafoundation.nova.feature_account_impl.di.modules.ProxySigningModule @@ -92,25 +96,22 @@ import io.novafoundation.nova.feature_account_impl.domain.MetaAccountGroupingInt import io.novafoundation.nova.feature_account_impl.domain.NodeHostValidator import io.novafoundation.nova.feature_account_impl.domain.account.add.AddAccountInteractor import io.novafoundation.nova.feature_account_impl.domain.account.advancedEncryption.AdvancedEncryptionInteractor +import io.novafoundation.nova.feature_account_impl.domain.account.cloudBackup.RealApplyLocalSnapshotToCloudBackupUseCase import io.novafoundation.nova.feature_account_impl.domain.account.details.WalletDetailsInteractor +import io.novafoundation.nova.feature_account_impl.domain.account.export.CommonExportSecretsInteractor +import io.novafoundation.nova.feature_account_impl.domain.account.export.RealCommonExportSecretsInteractor import io.novafoundation.nova.feature_account_impl.domain.cloudBackup.createPassword.CreateCloudBackupPasswordInteractor import io.novafoundation.nova.feature_account_impl.domain.cloudBackup.createPassword.RealCreateCloudBackupPasswordInteractor -import io.novafoundation.nova.feature_account_impl.domain.cloudBackup.enterPassword.RealEnterCloudBackupInteractor import io.novafoundation.nova.feature_account_impl.domain.cloudBackup.enterPassword.EnterCloudBackupInteractor -import io.novafoundation.nova.feature_account_impl.domain.startCreateWallet.RealStartCreateWalletInteractor -import io.novafoundation.nova.feature_account_impl.domain.startCreateWallet.StartCreateWalletInteractor -import io.novafoundation.nova.feature_account_impl.presentation.AccountRouter -import io.novafoundation.nova.feature_account_impl.presentation.account.common.listing.DelegatedMetaAccountUpdatesListingMixinFactory -import io.novafoundation.nova.feature_account_api.presenatation.account.common.listing.MetaAccountTypePresentationMapper -import io.novafoundation.nova.feature_account_impl.data.extrinsic.RealExtrinsicServiceFactory -import io.novafoundation.nova.feature_account_impl.di.modules.CustomFeeModule -import io.novafoundation.nova.feature_account_impl.domain.account.cloudBackup.RealApplyLocalSnapshotToCloudBackupUseCase -import io.novafoundation.nova.feature_account_impl.domain.account.export.CommonExportSecretsInteractor -import io.novafoundation.nova.feature_account_impl.domain.account.export.RealCommonExportSecretsInteractor +import io.novafoundation.nova.feature_account_impl.domain.cloudBackup.enterPassword.RealEnterCloudBackupInteractor import io.novafoundation.nova.feature_account_impl.domain.manualBackup.ManualBackupSelectAccountInteractor import io.novafoundation.nova.feature_account_impl.domain.manualBackup.ManualBackupSelectWalletInteractor import io.novafoundation.nova.feature_account_impl.domain.manualBackup.RealManualBackupSelectAccountInteractor import io.novafoundation.nova.feature_account_impl.domain.manualBackup.RealManualBackupSelectWalletInteractor +import io.novafoundation.nova.feature_account_impl.domain.startCreateWallet.RealStartCreateWalletInteractor +import io.novafoundation.nova.feature_account_impl.domain.startCreateWallet.StartCreateWalletInteractor +import io.novafoundation.nova.feature_account_impl.presentation.AccountRouter +import io.novafoundation.nova.feature_account_impl.presentation.account.common.listing.DelegatedMetaAccountUpdatesListingMixinFactory import io.novafoundation.nova.feature_account_impl.presentation.account.common.listing.MetaAccountWithBalanceListingMixinFactory import io.novafoundation.nova.feature_account_impl.presentation.account.common.listing.ProxyFormatter import io.novafoundation.nova.feature_account_impl.presentation.account.common.listing.RealMetaAccountTypePresentationMapper @@ -132,13 +133,13 @@ import io.novafoundation.nova.feature_cloud_backup_api.presenter.mixin.CloudBack import io.novafoundation.nova.feature_currency_api.domain.interfaces.CurrencyRepository import io.novafoundation.nova.feature_ledger_core.domain.LedgerMigrationTracker import io.novafoundation.nova.feature_proxy_api.data.repository.GetProxyRepository -import io.novafoundation.nova.feature_account_api.data.fee.capability.CustomFeeCapabilityFacade import io.novafoundation.nova.runtime.di.REMOTE_STORAGE_SOURCE import io.novafoundation.nova.runtime.ethereum.gas.GasPriceProviderFactory import io.novafoundation.nova.runtime.extrinsic.ExtrinsicBuilderFactory import io.novafoundation.nova.runtime.extrinsic.multi.ExtrinsicSplitter 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.network.rpc.RpcCalls import io.novafoundation.nova.runtime.storage.source.StorageDataSource import io.novafoundation.nova.web3names.domain.networking.Web3NamesInteractor @@ -187,7 +188,8 @@ class AccountFeatureModule { chainRegistry: ChainRegistry, signerProvider: SignerProvider, extrinsicSplitter: ExtrinsicSplitter, - feePaymentProviderRegistry: FeePaymentProviderRegistry + feePaymentProviderRegistry: FeePaymentProviderRegistry, + eventsRepository: EventsRepository, ): ExtrinsicService.Factory = RealExtrinsicServiceFactory( rpcCalls, chainRegistry, @@ -195,6 +197,7 @@ class AccountFeatureModule { extrinsicBuilderFactory, signerProvider, extrinsicSplitter, + eventsRepository, feePaymentProviderRegistry ) @@ -207,7 +210,8 @@ class AccountFeatureModule { chainRegistry: ChainRegistry, signerProvider: SignerProvider, extrinsicSplitter: ExtrinsicSplitter, - feePaymentProviderRegistry: FeePaymentProviderRegistry + feePaymentProviderRegistry: FeePaymentProviderRegistry, + eventsRepository: EventsRepository, ): ExtrinsicService = RealExtrinsicService( rpcCalls, chainRegistry, @@ -216,6 +220,7 @@ class AccountFeatureModule { signerProvider, extrinsicSplitter, feePaymentProviderRegistry, + eventsRepository, coroutineScope = null ) diff --git a/feature-swap-api/src/main/java/io/novafoundation/nova/feature_swap_api/domain/model/AtomicSwapOperation.kt b/feature-swap-api/src/main/java/io/novafoundation/nova/feature_swap_api/domain/model/AtomicSwapOperation.kt index 8be6fcd5e9..722f76d8cd 100644 --- a/feature-swap-api/src/main/java/io/novafoundation/nova/feature_swap_api/domain/model/AtomicSwapOperation.kt +++ b/feature-swap-api/src/main/java/io/novafoundation/nova/feature_swap_api/domain/model/AtomicSwapOperation.kt @@ -3,30 +3,62 @@ package io.novafoundation.nova.feature_swap_api.domain.model import io.novafoundation.nova.feature_account_api.data.fee.FeePaymentCurrency import io.novafoundation.nova.feature_account_api.data.model.FeeBase import io.novafoundation.nova.feature_account_api.data.model.SubmissionFee +import io.novafoundation.nova.feature_account_api.data.model.totalPlanksEnsuringAsset +import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.types.Balance interface AtomicSwapOperation { + val estimatedSwapLimit: SwapLimit + suspend fun estimateFee(): AtomicSwapOperationFee - suspend fun submit(previousStepCorrection: SwapExecutionCorrection?): Result + suspend fun requiredAmountInToGetAmountOut(extraOutAmount: Balance): Balance + + // TODO this is a temporarily function until we developer Operation Manager + suspend fun inProgressLabel(): String + + suspend fun submit(args: AtomicSwapOperationSubmissionArgs): Result } +class AtomicSwapOperationSubmissionArgs( + val actualSwapLimit: SwapLimit, +) + class AtomicSwapOperationArgs( - val swapLimit: SwapLimit, + val estimatedSwapLimit: SwapLimit, val feePaymentCurrency: FeePaymentCurrency, ) class AtomicSwapOperationFee( + /** + * Fee that is paid when submitting transaction + */ val submissionFee: SubmissionFee, - val additionalFees: List = emptyList() -) -// TODO this will later be used to perform more accurate non-atomic swaps -// So next segments can correct tx args based on outcome of previous segments -//class SwapExecutionCorrection( -// val actualFee: Balance, -// val actualReceivedAmount: Balance, -// val submission: ExtrinsicSubmission, -//) + val postSubmissionFees: PostSubmissionFees = PostSubmissionFees(), +) { + + class PostSubmissionFees( + /** + * Post-submission fees paid by (some) origin account. + * This is typed as `SubmissionFee` as those fee might still use different accounts (e.g. delivery fees are always paid from requested account) + */ + val paidByAccount: List = emptyList(), -class SwapExecutionCorrection() + /** + * Post-submission fees paid from swapping amount directly. Its payment is isolated and does not involve any withdrawals from accounts + */ + val paidFromAmount: List = emptyList() + ) +} + +fun AtomicSwapOperationFee.totalFeeEnsuringSubmissionAsset(): Balance { + val postSubmissionFeesByAccount = postSubmissionFees.paidByAccount.totalPlanksEnsuringAsset(submissionFee.asset) + val postSubmissionFeesFromHolding = postSubmissionFees.paidByAccount.totalPlanksEnsuringAsset(submissionFee.asset) + + return submissionFee.amount + postSubmissionFeesByAccount + postSubmissionFeesFromHolding +} + +class SwapExecutionCorrection( + val actualReceivedAmount: Balance +) diff --git a/feature-swap-api/src/main/java/io/novafoundation/nova/feature_swap_api/domain/model/SwapFee.kt b/feature-swap-api/src/main/java/io/novafoundation/nova/feature_swap_api/domain/model/SwapFee.kt new file mode 100644 index 0000000000..b47e0edb29 --- /dev/null +++ b/feature-swap-api/src/main/java/io/novafoundation/nova/feature_swap_api/domain/model/SwapFee.kt @@ -0,0 +1,57 @@ +package io.novafoundation.nova.feature_swap_api.domain.model + +import io.novafoundation.nova.feature_account_api.data.model.Fee +import io.novafoundation.nova.feature_account_api.data.model.FeeBase +import io.novafoundation.nova.feature_account_api.data.model.SubstrateFeeBase +import io.novafoundation.nova.feature_account_api.data.model.getAmount +import io.novafoundation.nova.feature_account_api.data.model.replacePlanks +import io.novafoundation.nova.feature_account_api.data.model.totalAmount +import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.types.Balance +import io.novafoundation.nova.feature_wallet_api.presentation.mixin.amountChooser.maxAction.MaxAvailableDeduction +import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.GenericFee +import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain + +class SwapFee( + val segments: List, + /** + * Fees for second and subsequent segments converted to assetIn + */ + val intermediateSegmentFeesInAssetIn: FeeBase +) : GenericFee, MaxAvailableDeduction { + + data class SwapSegment(val fee: AtomicSwapOperationFee, val operation: AtomicSwapOperation) + + private val firstSegmentFee = segments.first().fee + + private val submissionFee = firstSegmentFee.submissionFee + private val postSubmissionFees = firstSegmentFee.postSubmissionFees + + private val assetIn = intermediateSegmentFeesInAssetIn.asset + + val additionalAmountForSwap = additionalAmountForSwap() + + // TODO better multi fee display with `segmentsFees` + override val networkFee: Fee = determineNetworkFee() + + override fun deductionFor(amountAsset: Chain.Asset): Balance { + val requestedAccount = submissionFee.submissionOrigin.requestedOrigin + + val submissionFeeAmount = submissionFee.getAmount(amountAsset, requestedAccount) + val additionalFeesAmount = postSubmissionFees.paidByAccount.totalAmount(amountAsset, requestedAccount) + + return submissionFeeAmount + additionalFeesAmount + additionalAmountForSwap.getAmount(amountAsset) + } + + private fun additionalAmountForSwap(): FeeBase { + val amountTakenFromAssetIn = postSubmissionFees.paidFromAmount.totalAmount(assetIn) + val totalFutureFeeInAssetIn = amountTakenFromAssetIn + intermediateSegmentFeesInAssetIn.amount + + return SubstrateFeeBase(totalFutureFeeInAssetIn, assetIn) + } + + // TODO this is for simpler understanding of real fee until multi-chain view is developed + private fun determineNetworkFee(): Fee { + val submissionFeeAsset = submissionFee.asset + return submissionFee.replacePlanks(newPlanks = deductionFor(submissionFeeAsset)) + } +} diff --git a/feature-swap-api/src/main/java/io/novafoundation/nova/feature_swap_api/domain/model/SwapProgress.kt b/feature-swap-api/src/main/java/io/novafoundation/nova/feature_swap_api/domain/model/SwapProgress.kt new file mode 100644 index 0000000000..18efaf590b --- /dev/null +++ b/feature-swap-api/src/main/java/io/novafoundation/nova/feature_swap_api/domain/model/SwapProgress.kt @@ -0,0 +1,10 @@ +package io.novafoundation.nova.feature_swap_api.domain.model + +sealed class SwapProgress { + + class StepStarted(val step: String): SwapProgress() + + class Failure(val error: Throwable): SwapProgress() + + object Done: SwapProgress() +} diff --git a/feature-swap-api/src/main/java/io/novafoundation/nova/feature_swap_api/domain/model/SwapQuote.kt b/feature-swap-api/src/main/java/io/novafoundation/nova/feature_swap_api/domain/model/SwapQuote.kt index c4f857cbc5..a5cf72ebb9 100644 --- a/feature-swap-api/src/main/java/io/novafoundation/nova/feature_swap_api/domain/model/SwapQuote.kt +++ b/feature-swap-api/src/main/java/io/novafoundation/nova/feature_swap_api/domain/model/SwapQuote.kt @@ -1,14 +1,12 @@ package io.novafoundation.nova.feature_swap_api.domain.model import io.novafoundation.nova.common.utils.Percent -import io.novafoundation.nova.feature_account_api.data.model.Fee import io.novafoundation.nova.feature_account_api.data.model.amountByRequestedAccount import io.novafoundation.nova.feature_swap_core_api.data.paths.model.QuotedPath import io.novafoundation.nova.feature_swap_core_api.data.primitive.model.SwapDirection import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.types.Balance import io.novafoundation.nova.feature_wallet_api.domain.model.ChainAssetWithAmount 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.runtime.multiNetwork.chain.model.Chain import java.math.BigDecimal @@ -58,17 +56,7 @@ infix fun ChainAssetWithAmount.rateAgainst(assetOut: ChainAssetWithAmount): BigD return amountOut / amountIn } -class SwapFee( - val atomicOperationFees: List -) : GenericFee { - val firstSegmentFee: Fee - get() = atomicOperationFees.first().submissionFee - - // TODO handle multi-segment fee display - override val networkFee: Fee - get() = firstSegmentFee -} val SwapFee.totalDeductedPlanks: Balance get() = networkFee.amountByRequestedAccount diff --git a/feature-swap-api/src/main/java/io/novafoundation/nova/feature_swap_api/domain/model/SwapQuoteArgs.kt b/feature-swap-api/src/main/java/io/novafoundation/nova/feature_swap_api/domain/model/SwapQuoteArgs.kt index d56ed9a952..298093e6a6 100644 --- a/feature-swap-api/src/main/java/io/novafoundation/nova/feature_swap_api/domain/model/SwapQuoteArgs.kt +++ b/feature-swap-api/src/main/java/io/novafoundation/nova/feature_swap_api/domain/model/SwapQuoteArgs.kt @@ -17,7 +17,8 @@ data class SwapQuoteArgs( val swapDirection: SwapDirection, ) -class SwapExecuteArgs( +open class SwapFeeArgs( + val assetIn: Chain.Asset, val slippage: Percent, val executionPath: Path, val direction: SwapDirection, @@ -30,21 +31,71 @@ class SegmentExecuteArgs( sealed class SwapLimit { - class SpecifiedIn( + data class SpecifiedIn( val amountIn: Balance, val amountOutQuote: Balance, val amountOutMin: Balance ) : SwapLimit() - class SpecifiedOut( + data class SpecifiedOut( val amountOut: Balance, val amountInQuote: Balance, val amountInMax: Balance ) : SwapLimit() } -fun SwapQuote.toExecuteArgs(slippage: Percent, firstSegmentFees: Chain.Asset): SwapExecuteArgs { - return SwapExecuteArgs( +/** + * Adjusts SwapLimit to the [newAmountIn] based on the quoted swap rate + * This is only suitable for small changes amount in, as it implicitly assumes the swap rate stays the same + */ +fun SwapLimit.replaceAmountIn(newAmountIn: Balance): SwapLimit { + return when(this) { + is SwapLimit.SpecifiedIn -> updateInAmount(newAmountIn) + is SwapLimit.SpecifiedOut -> updateInAmount(newAmountIn) + } +} + +private fun SwapLimit.SpecifiedIn.replaceInMultiplier(amount: Balance): BigDecimal { + val amountDecimal = amount.toBigDecimal() + val amountInDecimal = amountIn.toBigDecimal() + + return amountDecimal / amountInDecimal +} + +private fun SwapLimit.SpecifiedIn.replacingInAmount(newInAmount: Balance, replacingAmount: Balance): Balance { + return (replaceInMultiplier(replacingAmount) * newInAmount.toBigDecimal()).toBigInteger() +} + +private fun SwapLimit.SpecifiedIn.updateInAmount(newAmountIn: Balance): SwapLimit.SpecifiedIn { + return SwapLimit.SpecifiedIn( + amountIn = newAmountIn, + amountOutQuote = replacingInAmount(newAmountIn, replacingAmount = amountOutQuote), + amountOutMin = replacingInAmount(newAmountIn, replacingAmount = amountOutMin) + ) +} + +private fun SwapLimit.SpecifiedOut.replaceInQuoteMultiplier(amount: Balance): BigDecimal { + val amountDecimal = amount.toBigDecimal() + val amountInQuoteDecimal = amountInQuote.toBigDecimal() + + return amountDecimal / amountInQuoteDecimal +} + +private fun SwapLimit.SpecifiedOut.replacedInQuoteAmount(newInQuoteAmount: Balance, replacingAmount: Balance): Balance { + return (replaceInQuoteMultiplier(replacingAmount) * newInQuoteAmount.toBigDecimal()).toBigInteger() +} + +private fun SwapLimit.SpecifiedOut.updateInAmount(newAmountInQuote: Balance): SwapLimit.SpecifiedOut { + return SwapLimit.SpecifiedOut( + amountOut = replacedInQuoteAmount(newAmountInQuote, amountOut), + amountInQuote = newAmountInQuote, + amountInMax = replacedInQuoteAmount(newAmountInQuote, amountInMax) + ) +} + +fun SwapQuote.toExecuteArgs(slippage: Percent, firstSegmentFees: Chain.Asset): SwapFeeArgs { + return SwapFeeArgs( + assetIn = amountIn.chainAsset, slippage = slippage, direction = quotedPath.direction, executionPath = quotedPath.path.map { quotedSwapEdge -> SegmentExecuteArgs(quotedSwapEdge) }, diff --git a/feature-swap-api/src/main/java/io/novafoundation/nova/feature_swap_api/domain/swap/SwapService.kt b/feature-swap-api/src/main/java/io/novafoundation/nova/feature_swap_api/domain/swap/SwapService.kt index c1547a8732..d7c38c421e 100644 --- a/feature-swap-api/src/main/java/io/novafoundation/nova/feature_swap_api/domain/swap/SwapService.kt +++ b/feature-swap-api/src/main/java/io/novafoundation/nova/feature_swap_api/domain/swap/SwapService.kt @@ -3,9 +3,9 @@ package io.novafoundation.nova.feature_swap_api.domain.swap import io.novafoundation.nova.feature_account_api.domain.model.MetaAccount import io.novafoundation.nova.feature_swap_api.domain.model.ReQuoteTrigger import io.novafoundation.nova.feature_swap_api.domain.model.SlippageConfig -import io.novafoundation.nova.feature_swap_api.domain.model.SwapExecuteArgs -import io.novafoundation.nova.feature_swap_api.domain.model.SwapExecutionCorrection import io.novafoundation.nova.feature_swap_api.domain.model.SwapFee +import io.novafoundation.nova.feature_swap_api.domain.model.SwapFeeArgs +import io.novafoundation.nova.feature_swap_api.domain.model.SwapProgress import io.novafoundation.nova.feature_swap_api.domain.model.SwapQuote import io.novafoundation.nova.feature_swap_api.domain.model.SwapQuoteArgs import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain @@ -31,9 +31,9 @@ interface SwapService { computationSharingScope: CoroutineScope ): Result - suspend fun estimateFee(executeArgs: SwapExecuteArgs): SwapFee + suspend fun estimateFee(executeArgs: SwapFeeArgs): SwapFee - suspend fun swap(args: SwapExecuteArgs): Result + suspend fun swap(calculatedFee: SwapFee): Flow suspend fun defaultSlippageConfig(chainId: ChainId): SlippageConfig diff --git a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/AssetExchange.kt b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/AssetExchange.kt index da801b087e..42574525a1 100644 --- a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/AssetExchange.kt +++ b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/AssetExchange.kt @@ -17,7 +17,7 @@ interface AssetExchange { suspend fun create( chain: Chain, - parentQuoter: SwapHost, + swapHost: SwapHost, coroutineScope: CoroutineScope ): AssetExchange? } diff --git a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/assetConversion/AssetConversionExchange.kt b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/assetConversion/AssetConversionExchange.kt index acbe7604ff..c5078bd54f 100644 --- a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/assetConversion/AssetConversionExchange.kt +++ b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/assetConversion/AssetConversionExchange.kt @@ -13,6 +13,7 @@ import io.novafoundation.nova.feature_account_api.domain.model.MetaAccount import io.novafoundation.nova.feature_swap_api.domain.model.AtomicSwapOperation import io.novafoundation.nova.feature_swap_api.domain.model.AtomicSwapOperationArgs import io.novafoundation.nova.feature_swap_api.domain.model.AtomicSwapOperationFee +import io.novafoundation.nova.feature_swap_api.domain.model.AtomicSwapOperationSubmissionArgs import io.novafoundation.nova.feature_swap_api.domain.model.ReQuoteTrigger import io.novafoundation.nova.feature_swap_api.domain.model.SwapExecutionCorrection import io.novafoundation.nova.feature_swap_api.domain.model.SwapGraphEdge @@ -22,6 +23,7 @@ import io.novafoundation.nova.feature_swap_core_api.data.primitive.errors.SwapQu import io.novafoundation.nova.feature_swap_core_api.data.primitive.model.SwapDirection import io.novafoundation.nova.feature_swap_impl.data.assetExchange.AssetExchange import io.novafoundation.nova.feature_swap_impl.data.assetExchange.FeePaymentProviderOverride +import io.novafoundation.nova.feature_swap_impl.data.assetExchange.ParentQuoterArgs import io.novafoundation.nova.feature_swap_impl.domain.swap.BaseSwapGraphEdge import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.types.Balance import io.novafoundation.nova.runtime.call.MultiChainRuntimeCallsApi @@ -56,7 +58,7 @@ class AssetConversionExchangeFactory( override suspend fun create( chain: Chain, - parentQuoter: AssetExchange.SwapHost, + swapHost: AssetExchange.SwapHost, coroutineScope: CoroutineScope ): AssetExchange { val converter = multiLocationConverterFactory.defaultAsync(chain, coroutineScope) @@ -68,6 +70,7 @@ class AssetConversionExchangeFactory( multiChainRuntimeCallsApi = runtimeCallsApi, coroutineScope = coroutineScope, chainStateRepository = chainStateRepository, + swapHost = swapHost, extrinsicServiceFactory = extrinsicServiceFactory ) } @@ -80,6 +83,7 @@ private class AssetConversionExchange( private val multiChainRuntimeCallsApi: MultiChainRuntimeCallsApi, private val extrinsicServiceFactory: ExtrinsicService.Factory, private val chainStateRepository: ChainStateRepository, + private val swapHost: AssetExchange.SwapHost, coroutineScope: CoroutineScope ) : AssetExchange { @@ -205,6 +209,8 @@ private class AssetConversionExchange( private val toAsset: Chain.Asset ) : AtomicSwapOperation { + override val estimatedSwapLimit: SwapLimit = transactionArgs.estimatedSwapLimit + override suspend fun estimateFee(): AtomicSwapOperationFee { val submissionFee = extrinsicService.estimateFee( chain = chain, @@ -213,15 +219,28 @@ private class AssetConversionExchange( feePaymentCurrency = transactionArgs.feePaymentCurrency ) ) { - executeSwap(sendTo = chain.emptyAccountId()) + executeSwap(swapLimit = estimatedSwapLimit, sendTo = chain.emptyAccountId()) } return AtomicSwapOperationFee(submissionFee) } - override suspend fun submit(previousStepCorrection: SwapExecutionCorrection?): Result { - // TODO use `previousStepCorrection` to correct used call arguments - // TODO implement watching for extrinsic events + override suspend fun requiredAmountInToGetAmountOut(extraOutAmount: Balance): Balance { + val quoteArgs = ParentQuoterArgs( + chainAssetIn =fromAsset, + chainAssetOut = toAsset, + amount = extraOutAmount, + swapDirection = SwapDirection.SPECIFIED_OUT + ) + + return swapHost.quote(quoteArgs) + } + + override suspend fun inProgressLabel(): String { + return "Swapping ${fromAsset.symbol} to ${toAsset.symbol} on ${chain.name}" + } + + override suspend fun submit(args: AtomicSwapOperationSubmissionArgs): Result { return extrinsicService.submitAndWatchExtrinsic( chain = chain, origin = TransactionOrigin.SelectedWallet, @@ -230,19 +249,22 @@ private class AssetConversionExchange( ) ) { submissionOrigin -> // Send swapped funds to the requested origin since it the account doing the swap - executeSwap(sendTo = submissionOrigin.requestedOrigin) + executeSwap(swapLimit = args.actualSwapLimit, sendTo = submissionOrigin.requestedOrigin) }.awaitInBlock().map { - SwapExecutionCorrection() + TODO() } } - private suspend fun ExtrinsicBuilder.executeSwap(sendTo: AccountId) { + private suspend fun ExtrinsicBuilder.executeSwap( + swapLimit: SwapLimit, + sendTo: AccountId + ) { val path = listOf(fromAsset, toAsset) .map { asset -> multiLocationConverter.encodableMultiLocationOf(asset) } val keepAlive = false - when (val swapLimit = transactionArgs.swapLimit) { + when (swapLimit) { is SwapLimit.SpecifiedIn -> call( moduleName = Modules.ASSET_CONVERSION, callName = "swap_exact_tokens_for_tokens", diff --git a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/crossChain/CrossChainTransferAssetExchange.kt b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/crossChain/CrossChainTransferAssetExchange.kt index 28f54bd9e7..5d68dfe8a4 100644 --- a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/crossChain/CrossChainTransferAssetExchange.kt +++ b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/crossChain/CrossChainTransferAssetExchange.kt @@ -1,6 +1,5 @@ package io.novafoundation.nova.feature_swap_impl.data.assetExchange.crossChain -import android.util.Log import io.novafoundation.nova.common.utils.firstNotNull import io.novafoundation.nova.common.utils.graph.Edge import io.novafoundation.nova.feature_account_api.domain.interfaces.AccountRepository @@ -9,6 +8,7 @@ import io.novafoundation.nova.feature_account_api.domain.model.requireAddressIn import io.novafoundation.nova.feature_swap_api.domain.model.AtomicSwapOperation import io.novafoundation.nova.feature_swap_api.domain.model.AtomicSwapOperationArgs import io.novafoundation.nova.feature_swap_api.domain.model.AtomicSwapOperationFee +import io.novafoundation.nova.feature_swap_api.domain.model.AtomicSwapOperationSubmissionArgs import io.novafoundation.nova.feature_swap_api.domain.model.ReQuoteTrigger import io.novafoundation.nova.feature_swap_api.domain.model.SwapExecutionCorrection import io.novafoundation.nova.feature_swap_api.domain.model.SwapGraphEdge @@ -23,6 +23,7 @@ import io.novafoundation.nova.feature_wallet_api.domain.implementations.availabl import io.novafoundation.nova.feature_wallet_api.domain.interfaces.CrossChainTransfersUseCase import io.novafoundation.nova.feature_wallet_api.domain.model.CrossChainTransfersConfiguration import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry +import io.novafoundation.nova.runtime.multiNetwork.asset import io.novafoundation.nova.runtime.multiNetwork.chain.model.FullChainAssetId import io.novafoundation.nova.runtime.multiNetwork.chainWithAsset import kotlinx.coroutines.CoroutineScope @@ -108,11 +109,7 @@ class CrossChainTransferAssetExchange( val config = crossChainConfig.value ?: return false // Delivery fees cannot be paid in non-native assets - return (delegate.from.chainId !in config.deliveryFeeConfigurations).also { - if (!it) { - Log.d("Swaps", "Filtered out $delegate due to delivery fee restrictions") - } - } + return delegate.from.chainId !in config.deliveryFeeConfigurations } override suspend fun quote(amount: BigInteger, direction: SwapDirection): BigInteger { @@ -122,11 +119,13 @@ class CrossChainTransferAssetExchange( inner class CrossChainTransferOperation( private val transactionArgs: AtomicSwapOperationArgs, - private val edge: Edge + private val edge: Edge, ) : AtomicSwapOperation { + override val estimatedSwapLimit: SwapLimit = transactionArgs.estimatedSwapLimit + override suspend fun estimateFee(): AtomicSwapOperationFee { - val transfer = createTransfer(amount = transactionArgs.swapLimit.crossChainTransferAmount) + val transfer = createTransfer(amount = estimatedSwapLimit.crossChainTransferAmount) val crossChainFee = with(crossChainTransfersUseCase) { swapHost.extrinsicService().estimateFee(transfer, computationalScope) @@ -134,14 +133,29 @@ class CrossChainTransferAssetExchange( return AtomicSwapOperationFee( submissionFee = crossChainFee.fromOriginInFeeCurrency, - additionalFees = listOfNotNull( - crossChainFee.fromOriginInNativeCurrency, - crossChainFee.fromHoldingRegister - ) + postSubmissionFees = AtomicSwapOperationFee.PostSubmissionFees( + paidByAccount = listOfNotNull( + crossChainFee.fromOriginInNativeCurrency, + ), + paidFromAmount = listOf( + crossChainFee.fromHoldingRegister + ) + ), ) } - override suspend fun submit(previousStepCorrection: SwapExecutionCorrection?): Result { + override suspend fun requiredAmountInToGetAmountOut(extraOutAmount: Balance): Balance { + return extraOutAmount + } + + override suspend fun inProgressLabel(): String { + val chainTo = chainRegistry.getChain(edge.to.chainId) + val assetFrom = chainRegistry.asset(edge.from) + + return "Transferring ${assetFrom.symbol} to ${chainTo.name}" + } + + override suspend fun submit(args: AtomicSwapOperationSubmissionArgs): Result { return Result.failure(UnsupportedOperationException("TODO")) } @@ -164,9 +178,8 @@ class CrossChainTransferAssetExchange( private val SwapLimit.crossChainTransferAmount: Balance get() = when (this) { - // We cannot use slippage since we cannot guarantee slippage compliance in transfers - is SwapLimit.SpecifiedIn -> amountOutQuote - is SwapLimit.SpecifiedOut -> amountInQuote + is SwapLimit.SpecifiedIn -> amountIn + is SwapLimit.SpecifiedOut -> amountOut } } } diff --git a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/hydraDx/HydraDxExchange.kt b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/hydraDx/HydraDxExchange.kt index 466f20f1ee..baf9239102 100644 --- a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/hydraDx/HydraDxExchange.kt +++ b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/hydraDx/HydraDxExchange.kt @@ -1,5 +1,6 @@ package io.novafoundation.nova.feature_swap_impl.data.assetExchange.hydraDx +import io.novafoundation.nova.common.data.network.runtime.binding.bindNumber import io.novafoundation.nova.common.utils.Modules import io.novafoundation.nova.common.utils.flatMapAsync import io.novafoundation.nova.common.utils.forEachAsync @@ -9,7 +10,7 @@ import io.novafoundation.nova.common.utils.structOf import io.novafoundation.nova.common.utils.withFlowScope 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_account_api.data.extrinsic.awaitInBlock +import io.novafoundation.nova.feature_account_api.data.extrinsic.execution.requireOk import io.novafoundation.nova.feature_account_api.data.fee.FeePayment import io.novafoundation.nova.feature_account_api.data.fee.FeePaymentCurrency import io.novafoundation.nova.feature_account_api.data.fee.capability.FastLookupCustomFeeCapability @@ -25,6 +26,7 @@ import io.novafoundation.nova.feature_account_api.domain.model.requireAccountIdI import io.novafoundation.nova.feature_swap_api.domain.model.AtomicSwapOperation import io.novafoundation.nova.feature_swap_api.domain.model.AtomicSwapOperationArgs import io.novafoundation.nova.feature_swap_api.domain.model.AtomicSwapOperationFee +import io.novafoundation.nova.feature_swap_api.domain.model.AtomicSwapOperationSubmissionArgs import io.novafoundation.nova.feature_swap_api.domain.model.ReQuoteTrigger import io.novafoundation.nova.feature_swap_api.domain.model.SwapExecutionCorrection import io.novafoundation.nova.feature_swap_api.domain.model.SwapGraphEdge @@ -47,15 +49,19 @@ import io.novafoundation.nova.feature_swap_impl.data.assetExchange.hydraDx.refer import io.novafoundation.nova.feature_swap_impl.data.assetExchange.hydraDx.referrals.referralsOrNull import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.AssetSourceRegistry import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.existentialDepositInPlanks +import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.types.Balance import io.novafoundation.nova.runtime.ethereum.StorageSharedRequestsBuilder import io.novafoundation.nova.runtime.ethereum.StorageSharedRequestsBuilderFactory import io.novafoundation.nova.runtime.ext.utilityAsset import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain import io.novafoundation.nova.runtime.multiNetwork.chain.model.ChainAssetId import io.novafoundation.nova.runtime.multiNetwork.chain.model.FullChainAssetId +import io.novafoundation.nova.runtime.multiNetwork.runtime.repository.findEvent +import io.novafoundation.nova.runtime.multiNetwork.runtime.repository.findEventOrThrow import io.novafoundation.nova.runtime.storage.source.StorageDataSource import io.novafoundation.nova.runtime.storage.source.query.metadata import io.novasama.substrate_sdk_android.runtime.AccountId +import io.novasama.substrate_sdk_android.runtime.definitions.types.generics.GenericEvent import io.novasama.substrate_sdk_android.runtime.extrinsic.BatchMode import io.novasama.substrate_sdk_android.runtime.extrinsic.ExtrinsicBuilder import kotlinx.coroutines.CoroutineScope @@ -80,7 +86,7 @@ class HydraDxExchangeFactory( private val hydrationFeeInjector: HydrationFeeInjector ) : AssetExchange.SingleChainFactory { - override suspend fun create(chain: Chain, parentQuoter: AssetExchange.SwapHost, coroutineScope: CoroutineScope): AssetExchange { + override suspend fun create(chain: Chain, swapHost: AssetExchange.SwapHost, coroutineScope: CoroutineScope): AssetExchange { return HydraDxExchange( remoteStorageSource = remoteStorageSource, chain = chain, @@ -89,13 +95,15 @@ class HydraDxExchangeFactory( hydraDxNovaReferral = hydraDxNovaReferral, swapSourceFactories = swapSourceFactories, assetSourceRegistry = assetSourceRegistry, - swapHost = parentQuoter, + swapHost = swapHost, hydrationFeeInjector = hydrationFeeInjector, delegate = quotingFactory.create(chain), ) } } +private const val ROUTE_EXECUTED_AMOUNT_OUT_IDX = 3 + private class HydraDxExchange( private val delegate: HydraDxQuoting, private val remoteStorageSource: StorageDataSource, @@ -238,11 +246,13 @@ private class HydraDxExchange( val feePaymentCurrency: FeePaymentCurrency, ) : AtomicSwapOperation { + override val estimatedSwapLimit: SwapLimit = aggregatedSwapLimit() + constructor(sourceEdge: HydraDxSourceEdge, args: AtomicSwapOperationArgs) - : this(listOf(HydraDxSwapTransactionSegment(sourceEdge, args.swapLimit)), args.feePaymentCurrency) + : this(listOf(HydraDxSwapTransactionSegment(sourceEdge, args.estimatedSwapLimit)), args.feePaymentCurrency) fun appendSegment(nextEdge: HydraDxSourceEdge, nextSwapArgs: AtomicSwapOperationArgs): HydraDxOperation { - val nextSegment = HydraDxSwapTransactionSegment(nextEdge, nextSwapArgs.swapLimit) + val nextSegment = HydraDxSwapTransactionSegment(nextEdge, nextSwapArgs.estimatedSwapLimit) // Ignore nextSwapArgs.feePaymentCurrency - we are using configuration from the very first segment return HydraDxOperation(segments + nextSegment, feePaymentCurrency) @@ -257,14 +267,41 @@ private class HydraDxExchange( feePaymentCurrency = feePaymentCurrency ) ) { - executeSwap() + executeSwap(estimatedSwapLimit) } return AtomicSwapOperationFee(submissionFee) } - override suspend fun submit(previousStepCorrection: SwapExecutionCorrection?): Result { - return swapHost.extrinsicService().submitAndWatchExtrinsic( + override suspend fun requiredAmountInToGetAmountOut(extraOutAmount: Balance): Balance { + val assetInId = segments.first().edge.from.assetId + val assetIn = chain.assetsById.getValue(assetInId) + + val assetOutId = segments.last().edge.to.assetId + val assetOut = chain.assetsById.getValue(assetOutId) + + val quoteArgs = ParentQuoterArgs( + chainAssetIn = assetIn, + chainAssetOut = assetOut, + amount = extraOutAmount, + swapDirection = SwapDirection.SPECIFIED_OUT + ) + + return swapHost.quote(quoteArgs) + } + + override suspend fun inProgressLabel(): String { + val assetInId = segments.first().edge.from.assetId + val assetIn = chain.assetsById.getValue(assetInId) + + val assetOutId = segments.last().edge.to.assetId + val assetOut = chain.assetsById.getValue(assetOutId) + + return "Swapping ${assetIn.symbol} to ${assetOut.symbol} on ${chain.name}" + } + + override suspend fun submit(args: AtomicSwapOperationSubmissionArgs): Result { + return swapHost.extrinsicService().submitExtrinsicAndAwaitExecution( chain = chain, origin = TransactionOrigin.SelectedWallet, submissionOptions = ExtrinsicService.SubmissionOptions( @@ -272,64 +309,80 @@ private class HydraDxExchange( feePaymentCurrency = feePaymentCurrency ) ) { - executeSwap() - }.awaitInBlock().map { - SwapExecutionCorrection() + executeSwap(args.actualSwapLimit) + }.requireOk().mapCatching { (events) -> + SwapExecutionCorrection( + actualReceivedAmount = events.determineActualSwappedAmount() + ) } } - private suspend fun ExtrinsicBuilder.executeSwap() { + private fun List.determineActualSwappedAmount(): Balance { + val standaloneHydraSwap = getStandaloneSwap() + if (standaloneHydraSwap != null) { + return standaloneHydraSwap.extractReceivedAmount(this) + } + + val swapExecutedEvent = findEvent(Modules.ROUTER, "RouteExecuted") + ?: findEventOrThrow(Modules.ROUTER, "Executed") + + val amountOut = swapExecutedEvent.arguments[ROUTE_EXECUTED_AMOUNT_OUT_IDX] + return bindNumber(amountOut) + } + + private suspend fun ExtrinsicBuilder.executeSwap(actualSwapLimit: SwapLimit) { maybeSetReferral() - addSwapCall() + addSwapCall(actualSwapLimit) } - private suspend fun ExtrinsicBuilder.addSwapCall() { - val optimizationSucceeded = tryOptimizedSwap() + private suspend fun ExtrinsicBuilder.addSwapCall(actualSwapLimit: SwapLimit) { + val optimizationSucceeded = tryOptimizedSwap(actualSwapLimit) if (!optimizationSucceeded) { - executeRouterSwap() + executeRouterSwap(actualSwapLimit) } } - private fun ExtrinsicBuilder.tryOptimizedSwap(): Boolean { - if (segments.size != 1) return false - - val onlySegment = segments.single() - val standaloneSwapBuilder = onlySegment.edge.standaloneSwapBuilder ?: return false + private fun ExtrinsicBuilder.tryOptimizedSwap(actualSwapLimit: SwapLimit): Boolean { + val standaloneSwap = getStandaloneSwap() ?: return false - val args = AtomicSwapOperationArgs(onlySegment.swapLimit, feePaymentCurrency) - standaloneSwapBuilder(args) + val args = AtomicSwapOperationArgs(actualSwapLimit, feePaymentCurrency) + standaloneSwap.addSwapCall(args) return true } - private suspend fun ExtrinsicBuilder.executeRouterSwap() { + private fun getStandaloneSwap(): StandaloneHydraSwap? { + if (segments.size != 1) return null + + val onlySegment = segments.single() + return onlySegment.edge.standaloneSwap + } + + private suspend fun ExtrinsicBuilder.executeRouterSwap(actualSwapLimit: SwapLimit) { val firstSegment = segments.first() val lastSegment = segments.last() - when (val firstLimit = firstSegment.swapLimit) { + when (actualSwapLimit) { is SwapLimit.SpecifiedIn -> executeRouterSell( firstEdge = firstSegment.edge, - firstLimit = firstLimit, lastEdge = lastSegment.edge, - lastLimit = lastSegment.swapLimit as SwapLimit.SpecifiedIn + limit = actualSwapLimit, ) is SwapLimit.SpecifiedOut -> executeRouterBuy( firstEdge = firstSegment.edge, - firstLimit = firstLimit, lastEdge = lastSegment.edge, - lastLimit = lastSegment.swapLimit as SwapLimit.SpecifiedOut + limit = actualSwapLimit, ) } } private suspend fun ExtrinsicBuilder.executeRouterBuy( firstEdge: HydraDxSourceEdge, - firstLimit: SwapLimit.SpecifiedOut, lastEdge: HydraDxSourceEdge, - lastLimit: SwapLimit.SpecifiedOut + limit: SwapLimit.SpecifiedOut, ) { call( moduleName = Modules.ROUTER, @@ -337,8 +390,8 @@ private class HydraDxExchange( arguments = mapOf( "asset_in" to hydraDxAssetIdConverter.toOnChainIdOrThrow(firstEdge.from), "asset_out" to hydraDxAssetIdConverter.toOnChainIdOrThrow(lastEdge.to), - "amount_out" to lastLimit.amountOut, - "max_amount_in" to firstLimit.amountInMax, + "amount_out" to limit.amountOut, + "max_amount_in" to limit.amountInMax, "route" to routerTradePath() ) ) @@ -346,9 +399,8 @@ private class HydraDxExchange( private suspend fun ExtrinsicBuilder.executeRouterSell( firstEdge: HydraDxSourceEdge, - firstLimit: SwapLimit.SpecifiedIn, lastEdge: HydraDxSourceEdge, - lastLimit: SwapLimit.SpecifiedIn + limit: SwapLimit.SpecifiedIn, ) { call( moduleName = Modules.ROUTER, @@ -356,8 +408,8 @@ private class HydraDxExchange( arguments = mapOf( "asset_in" to hydraDxAssetIdConverter.toOnChainIdOrThrow(firstEdge.from), "asset_out" to hydraDxAssetIdConverter.toOnChainIdOrThrow(lastEdge.to), - "amount_in" to firstLimit.amountIn, - "min_amount_out" to lastLimit.amountOutMin, + "amount_in" to limit.amountIn, + "min_amount_out" to limit.amountOutMin, "route" to routerTradePath() ) ) @@ -392,6 +444,33 @@ private class HydraDxExchange( ) ) } + + private fun aggregatedSwapLimit(): SwapLimit { + val firstSegment = segments.first() + val lastSegment = segments.last() + + return when (val firstLimit = firstSegment.swapLimit) { + is SwapLimit.SpecifiedIn -> { + val lastLimit = lastSegment.swapLimit as SwapLimit.SpecifiedIn + + SwapLimit.SpecifiedIn( + amountIn = firstLimit.amountIn, + amountOutQuote = lastLimit.amountOutQuote, + amountOutMin = lastLimit.amountOutMin + ) + } + + is SwapLimit.SpecifiedOut -> { + val lastLimit = lastSegment.swapLimit as SwapLimit.SpecifiedOut + + SwapLimit.SpecifiedOut( + amountOut = lastLimit.amountOut, + amountInQuote = firstLimit.amountInQuote, + amountInMax = firstLimit.amountInMax + ) + } + } + } } // This is an optimization to reuse swap quoting state for hydra fee estimation instead of letting ExtrinsicService to spin up its own quoting @@ -450,7 +529,7 @@ private class HydraDxExchange( } } - private inner class HydrationFastLookupFeeCapability: FastLookupCustomFeeCapability { + private inner class HydrationFastLookupFeeCapability : FastLookupCustomFeeCapability { private var acceptedCurrenciesCache: Set? = null diff --git a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/hydraDx/HydraDxSwapSource.kt b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/hydraDx/HydraDxSwapSource.kt index 42a0c01969..dacd66c1b3 100644 --- a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/hydraDx/HydraDxSwapSource.kt +++ b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/hydraDx/HydraDxSwapSource.kt @@ -5,12 +5,20 @@ import io.novafoundation.nova.core.updater.SharedRequestsBuilder import io.novafoundation.nova.feature_swap_api.domain.model.AtomicSwapOperationArgs import io.novafoundation.nova.feature_swap_core_api.data.primitive.model.QuotableEdge import io.novafoundation.nova.feature_swap_core_api.data.types.hydra.HydraDxQuotingSource +import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.types.Balance import io.novasama.substrate_sdk_android.runtime.AccountId import io.novasama.substrate_sdk_android.runtime.definitions.types.composite.DictEnum +import io.novasama.substrate_sdk_android.runtime.definitions.types.generics.GenericEvent import io.novasama.substrate_sdk_android.runtime.extrinsic.ExtrinsicBuilder import kotlinx.coroutines.flow.Flow -typealias HydraDxStandaloneSwapBuilder = ExtrinsicBuilder.(args: AtomicSwapOperationArgs) -> Unit +interface StandaloneHydraSwap { + + context(ExtrinsicBuilder) + fun addSwapCall(args: AtomicSwapOperationArgs) + + fun extractReceivedAmount(events: List): Balance +} interface HydraDxSourceEdge : QuotableEdge { @@ -19,7 +27,7 @@ interface HydraDxSourceEdge : QuotableEdge { /** * Whether hydra swap source is able to perform optimized standalone swap without using Router */ - val standaloneSwapBuilder: HydraDxStandaloneSwapBuilder? + val standaloneSwap: StandaloneHydraSwap? suspend fun debugLabel(): String } diff --git a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/hydraDx/omnipool/OmniPoolSwapSource.kt b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/hydraDx/omnipool/OmniPoolSwapSource.kt index cd1e33b155..98303b836a 100644 --- a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/hydraDx/omnipool/OmniPoolSwapSource.kt +++ b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/hydraDx/omnipool/OmniPoolSwapSource.kt @@ -1,5 +1,6 @@ package io.novafoundation.nova.feature_swap_impl.data.assetExchange.hydraDx.omnipool +import io.novafoundation.nova.common.data.network.runtime.binding.bindNumber import io.novafoundation.nova.common.utils.Identifiable import io.novafoundation.nova.common.utils.Modules import io.novafoundation.nova.core.updater.SharedRequestsBuilder @@ -10,14 +11,19 @@ import io.novafoundation.nova.feature_swap_core.data.assetExchange.conversion.ty import io.novafoundation.nova.feature_swap_core_api.data.network.HydraDxAssetId import io.novafoundation.nova.feature_swap_core_api.data.primitive.model.QuotableEdge import io.novafoundation.nova.feature_swap_impl.data.assetExchange.hydraDx.HydraDxSourceEdge -import io.novafoundation.nova.feature_swap_impl.data.assetExchange.hydraDx.HydraDxStandaloneSwapBuilder import io.novafoundation.nova.feature_swap_impl.data.assetExchange.hydraDx.HydraDxSwapSource +import io.novafoundation.nova.feature_swap_impl.data.assetExchange.hydraDx.StandaloneHydraSwap import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.types.Balance +import io.novafoundation.nova.runtime.multiNetwork.runtime.repository.findEvent +import io.novafoundation.nova.runtime.multiNetwork.runtime.repository.findEventOrThrow import io.novasama.substrate_sdk_android.runtime.AccountId import io.novasama.substrate_sdk_android.runtime.definitions.types.composite.DictEnum +import io.novasama.substrate_sdk_android.runtime.definitions.types.generics.GenericEvent import io.novasama.substrate_sdk_android.runtime.extrinsic.ExtrinsicBuilder import kotlinx.coroutines.flow.Flow +private const val AMOUNT_OUT_POSITION = 4 + class OmniPoolSwapSourceFactory : HydraDxSwapSource.Factory { override val identifier: String = OmniPoolQuotingSourceFactory.SOURCE_ID @@ -48,25 +54,24 @@ private class OmniPoolSwapSource( private inner class OmniPoolSwapEdge( private val delegate: OmniPoolQuotingSource.Edge - ) : HydraDxSourceEdge, QuotableEdge by delegate { + ) : HydraDxSourceEdge, QuotableEdge by delegate, StandaloneHydraSwap { override fun routerPoolArgument(): DictEnum.Entry<*> { return DictEnum.Entry("Omnipool", null) } - override val standaloneSwapBuilder: HydraDxStandaloneSwapBuilder = { - executeSwap(it) - } + override val standaloneSwap = this override suspend fun debugLabel(): String { return "OmniPool" } - private fun ExtrinsicBuilder.executeSwap(args: AtomicSwapOperationArgs) { + context(ExtrinsicBuilder) + override fun addSwapCall(args: AtomicSwapOperationArgs) { val assetIdIn = delegate.fromAsset.first val assetIdOut = delegate.toAsset.first - when (val limit = args.swapLimit) { + when (val limit = args.estimatedSwapLimit) { is SwapLimit.SpecifiedIn -> sell( assetIdIn = assetIdIn, assetIdOut = assetIdOut, @@ -83,6 +88,14 @@ private class OmniPoolSwapSource( } } + override fun extractReceivedAmount(events: List): Balance { + val swapExecutedEvent = events.findEvent(Modules.OMNIPOOL, "BuyExecuted") + ?: events.findEventOrThrow(Modules.OMNIPOOL, "SellExecuted") + + val amountOut = swapExecutedEvent.arguments[AMOUNT_OUT_POSITION] + return bindNumber(amountOut) + } + private fun ExtrinsicBuilder.sell( assetIdIn: HydraDxAssetId, assetIdOut: HydraDxAssetId, diff --git a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/hydraDx/stableswap/StableSwapSource.kt b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/hydraDx/stableswap/StableSwapSource.kt index 633b65a8c7..259b06f705 100644 --- a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/hydraDx/stableswap/StableSwapSource.kt +++ b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/hydraDx/stableswap/StableSwapSource.kt @@ -8,7 +8,6 @@ import io.novafoundation.nova.feature_swap_core_api.data.network.HydraDxAssetIdC import io.novafoundation.nova.feature_swap_core_api.data.network.toChainAssetOrThrow import io.novafoundation.nova.feature_swap_core_api.data.primitive.model.QuotableEdge import io.novafoundation.nova.feature_swap_impl.data.assetExchange.hydraDx.HydraDxSourceEdge -import io.novafoundation.nova.feature_swap_impl.data.assetExchange.hydraDx.HydraDxStandaloneSwapBuilder import io.novafoundation.nova.feature_swap_impl.data.assetExchange.hydraDx.HydraDxSwapSource import io.novasama.substrate_sdk_android.runtime.AccountId import io.novasama.substrate_sdk_android.runtime.definitions.types.composite.DictEnum @@ -54,7 +53,7 @@ private class StableSwapSource( private val delegate: StableSwapQuotingSource.Edge ) : HydraDxSourceEdge, QuotableEdge by delegate { - override val standaloneSwapBuilder: HydraDxStandaloneSwapBuilder? = null + override val standaloneSwap = null override suspend fun debugLabel(): String { val poolAsset = hydraDxAssetIdConverter.toChainAssetOrThrow(chain, delegate.poolId) diff --git a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/hydraDx/xyk/XYKSwapSource.kt b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/hydraDx/xyk/XYKSwapSource.kt index 839d9fdc85..ce9c986db4 100644 --- a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/hydraDx/xyk/XYKSwapSource.kt +++ b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/hydraDx/xyk/XYKSwapSource.kt @@ -6,7 +6,6 @@ import io.novafoundation.nova.feature_swap_core.data.assetExchange.conversion.ty import io.novafoundation.nova.feature_swap_core.data.assetExchange.conversion.types.hydra.sources.xyk.XYKSwapQuotingSourceFactory import io.novafoundation.nova.feature_swap_core_api.data.primitive.model.QuotableEdge import io.novafoundation.nova.feature_swap_impl.data.assetExchange.hydraDx.HydraDxSourceEdge -import io.novafoundation.nova.feature_swap_impl.data.assetExchange.hydraDx.HydraDxStandaloneSwapBuilder import io.novafoundation.nova.feature_swap_impl.data.assetExchange.hydraDx.HydraDxSwapSource import io.novasama.substrate_sdk_android.runtime.AccountId import io.novasama.substrate_sdk_android.runtime.definitions.types.composite.DictEnum @@ -48,7 +47,7 @@ private class XYKSwapSource( return DictEnum.Entry("XYK", null) } - override val standaloneSwapBuilder: HydraDxStandaloneSwapBuilder? = null + override val standaloneSwap = null override suspend fun debugLabel(): String { return "XYK" diff --git a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/repository/SwapTransactionHistoryRepository.kt b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/repository/SwapTransactionHistoryRepository.kt index 0cd2f77efd..9e97f19b66 100644 --- a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/repository/SwapTransactionHistoryRepository.kt +++ b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/repository/SwapTransactionHistoryRepository.kt @@ -3,7 +3,7 @@ package io.novafoundation.nova.feature_swap_impl.data.repository import io.novafoundation.nova.core_db.dao.OperationDao import io.novafoundation.nova.core_db.model.operation.SwapTypeLocal.AssetWithAmount import io.novafoundation.nova.feature_account_api.data.extrinsic.ExtrinsicSubmission -import io.novafoundation.nova.feature_swap_api.domain.model.SwapExecuteArgs +import io.novafoundation.nova.feature_swap_api.domain.model.SwapFeeArgs import io.novafoundation.nova.feature_swap_api.domain.model.SwapFee import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.types.Balance import io.novafoundation.nova.runtime.ext.localId @@ -14,7 +14,7 @@ interface SwapTransactionHistoryRepository { suspend fun insertPendingSwap( chainAsset: Chain.Asset, - swapArgs: SwapExecuteArgs, + swapArgs: SwapFeeArgs, fee: SwapFee, txSubmission: ExtrinsicSubmission ) @@ -27,7 +27,7 @@ class RealSwapTransactionHistoryRepository( override suspend fun insertPendingSwap( chainAsset: Chain.Asset, - swapArgs: SwapExecuteArgs, + swapArgs: SwapFeeArgs, fee: SwapFee, txSubmission: ExtrinsicSubmission ) { diff --git a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/domain/interactor/SwapInteractor.kt b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/domain/interactor/SwapInteractor.kt index 3521c0a41d..2b1a0542fe 100644 --- a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/domain/interactor/SwapInteractor.kt +++ b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/domain/interactor/SwapInteractor.kt @@ -9,9 +9,9 @@ import io.novafoundation.nova.feature_buy_api.domain.BuyTokenRegistry import io.novafoundation.nova.feature_buy_api.domain.hasProvidersFor import io.novafoundation.nova.feature_swap_api.domain.model.ReQuoteTrigger import io.novafoundation.nova.feature_swap_api.domain.model.SlippageConfig -import io.novafoundation.nova.feature_swap_api.domain.model.SwapExecuteArgs -import io.novafoundation.nova.feature_swap_api.domain.model.SwapExecutionCorrection import io.novafoundation.nova.feature_swap_api.domain.model.SwapFee +import io.novafoundation.nova.feature_swap_api.domain.model.SwapFeeArgs +import io.novafoundation.nova.feature_swap_api.domain.model.SwapProgress import io.novafoundation.nova.feature_swap_api.domain.model.SwapQuote import io.novafoundation.nova.feature_swap_api.domain.model.SwapQuoteArgs import io.novafoundation.nova.feature_swap_api.domain.swap.SwapService @@ -86,8 +86,8 @@ class SwapInteractor( return swapService.quote(quoteArgs, computationalScope) } - suspend fun executeSwap(swapExecuteArgs: SwapExecuteArgs): Result = withContext(Dispatchers.IO) { - swapService.swap(swapExecuteArgs) + suspend fun executeSwap(calculatedFee: SwapFee): Flow = withContext(Dispatchers.IO) { + swapService.swap(calculatedFee) // .onSuccess { submission -> // swapTransactionHistoryRepository.insertPendingSwap( // chainAsset = swapExecuteArgs.assetIn, @@ -109,7 +109,7 @@ class SwapInteractor( return swapService.canPayFeeInNonUtilityAsset(asset) } - suspend fun estimateFee(executeArgs: SwapExecuteArgs): SwapFee { + suspend fun estimateFee(executeArgs: SwapFeeArgs): SwapFee { return swapService.estimateFee(executeArgs) } diff --git a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/domain/swap/RealSwapService.kt b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/domain/swap/RealSwapService.kt index 103e353dcd..86843a2fdc 100644 --- a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/domain/swap/RealSwapService.kt +++ b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/domain/swap/RealSwapService.kt @@ -18,7 +18,6 @@ import io.novafoundation.nova.common.utils.graph.vertices import io.novafoundation.nova.common.utils.isZero import io.novafoundation.nova.common.utils.mapAsync import io.novafoundation.nova.common.utils.mergeIfMultiple -import io.novafoundation.nova.common.utils.requireInnerNotNull import io.novafoundation.nova.common.utils.toPercent import io.novafoundation.nova.common.utils.withFlowScope import io.novafoundation.nova.feature_account_api.data.extrinsic.ExtrinsicService @@ -28,19 +27,25 @@ import io.novafoundation.nova.feature_account_api.data.fee.FeePaymentProviderReg import io.novafoundation.nova.feature_account_api.data.fee.capability.CustomFeeCapabilityFacade import io.novafoundation.nova.feature_account_api.data.fee.capability.FastLookupCustomFeeCapability import io.novafoundation.nova.feature_account_api.data.fee.toFeePaymentCurrency +import io.novafoundation.nova.feature_account_api.data.model.FeeBase +import io.novafoundation.nova.feature_account_api.data.model.SubstrateFeeBase import io.novafoundation.nova.feature_account_api.domain.model.MetaAccount import io.novafoundation.nova.feature_swap_api.domain.model.AtomicSwapOperation import io.novafoundation.nova.feature_swap_api.domain.model.AtomicSwapOperationArgs +import io.novafoundation.nova.feature_swap_api.domain.model.AtomicSwapOperationSubmissionArgs import io.novafoundation.nova.feature_swap_api.domain.model.ReQuoteTrigger import io.novafoundation.nova.feature_swap_api.domain.model.SlippageConfig -import io.novafoundation.nova.feature_swap_api.domain.model.SwapExecuteArgs import io.novafoundation.nova.feature_swap_api.domain.model.SwapExecutionCorrection import io.novafoundation.nova.feature_swap_api.domain.model.SwapFee +import io.novafoundation.nova.feature_swap_api.domain.model.SwapFeeArgs import io.novafoundation.nova.feature_swap_api.domain.model.SwapGraph import io.novafoundation.nova.feature_swap_api.domain.model.SwapGraphEdge import io.novafoundation.nova.feature_swap_api.domain.model.SwapLimit +import io.novafoundation.nova.feature_swap_api.domain.model.SwapProgress import io.novafoundation.nova.feature_swap_api.domain.model.SwapQuote import io.novafoundation.nova.feature_swap_api.domain.model.SwapQuoteArgs +import io.novafoundation.nova.feature_swap_api.domain.model.replaceAmountIn +import io.novafoundation.nova.feature_swap_api.domain.model.totalFeeEnsuringSubmissionAsset import io.novafoundation.nova.feature_swap_api.domain.swap.SwapService import io.novafoundation.nova.feature_swap_core_api.data.paths.PathQuoter import io.novafoundation.nova.feature_swap_core_api.data.paths.model.QuotedPath @@ -68,6 +73,7 @@ import io.novafoundation.nova.runtime.multiNetwork.asset import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain import io.novafoundation.nova.runtime.multiNetwork.chain.model.ChainId import io.novafoundation.nova.runtime.multiNetwork.chain.model.FullChainAssetId +import io.novasama.substrate_sdk_android.hash.isPositive import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.Flow @@ -75,10 +81,12 @@ import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.debounce import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.runningFold import kotlinx.coroutines.withContext import java.math.BigDecimal +import java.math.BigInteger import kotlin.time.Duration.Companion.milliseconds private const val ALL_DIRECTIONS_CACHE = "RealSwapService.ALL_DIRECTIONS" @@ -153,36 +161,86 @@ internal class RealSwapService( } } - override suspend fun estimateFee(executeArgs: SwapExecuteArgs): SwapFee { + override suspend fun estimateFee(executeArgs: SwapFeeArgs): SwapFee { val atomicOperations = executeArgs.constructAtomicOperations() - val fees = atomicOperations.mapAsync { it.estimateFee() } + val fees = atomicOperations.mapAsync { SwapFee.SwapSegment(it.estimateFee(), it) } + val convertedFees = fees.convertIntermediateSegmentsFeesToAssetIn(executeArgs.assetIn) - return SwapFee(fees).also(::logFee) + return SwapFee(segments = fees, intermediateSegmentFeesInAssetIn = convertedFees).also(::logFee) } - override suspend fun swap(args: SwapExecuteArgs): Result { - val atomicOperations = args.constructAtomicOperations() + override suspend fun swap(calculatedFee: SwapFee): Flow { + val atomicOperations = calculatedFee.segments val initialCorrection: Result = Result.success(null) - return atomicOperations.fold(initialCorrection) { prevStepCorrection, operation -> - prevStepCorrection.flatMap { operation.submit(it) } - }.requireInnerNotNull() + return flow { + // Zip assumes atomicOperations and atomicOperationFees were constructed the same way + atomicOperations.fold(initialCorrection) { prevStepCorrection, (_, operation) -> + prevStepCorrection.flatMap { correction -> + emit(SwapProgress.StepStarted(operation.inProgressLabel())) + + val newAmountIn = if (correction != null) { + correction.actualReceivedAmount + } else { + val amountIn = operation.estimatedSwapLimit.estimatedAmountIn() + amountIn + calculatedFee.additionalAmountForSwap.amount + } + + val actualSwapLimit = operation.estimatedSwapLimit.replaceAmountIn(newAmountIn) + val segmentSubmissionArgs = AtomicSwapOperationSubmissionArgs(actualSwapLimit) + + Log.d("Swaps", operation.inProgressLabel() + " with $actualSwapLimit") + + operation.submit(segmentSubmissionArgs).onFailure { + Log.e("Swaps", "Swap failed on stage '${operation.inProgressLabel()}'", it) + + emit(SwapProgress.Failure(it)) + } + } + }.onSuccess { + emit(SwapProgress.Done) + } + } + } + + private fun SwapLimit.estimatedAmountIn(): Balance { + return when (this) { + is SwapLimit.SpecifiedIn -> amountIn + is SwapLimit.SpecifiedOut -> amountInQuote + } } - private suspend fun SwapExecuteArgs.constructAtomicOperations(): List { + private suspend fun List.convertIntermediateSegmentsFeesToAssetIn(assetIn: Chain.Asset): FeeBase { + val convertedFees = foldRightIndexed(BigInteger.ZERO) { index, (operationFee, swapOperation), futureFeePlanks -> + val amountInToGetFeesForOut = if (futureFeePlanks.isPositive()) { + swapOperation.requiredAmountInToGetAmountOut(futureFeePlanks) + } else { + BigInteger.ZERO + } + + amountInToGetFeesForOut + if (index != 0) { + // Ensure everything is in the same asset + operationFee.totalFeeEnsuringSubmissionAsset() + } else { + // First segment is not included + BigInteger.ZERO + } + } + + return SubstrateFeeBase(convertedFees, assetIn) + } + + private suspend fun SwapFeeArgs.constructAtomicOperations(): List { var currentSwapTx: AtomicSwapOperation? = null val finishedSwapTxs = mutableListOf() - // TODO this will result in lower total slippage if some segments are appendable - val perSegmentSlippage = slippage / executionPath.size - executionPath.forEachIndexed { index, segmentExecuteArgs -> val quotedEdge = segmentExecuteArgs.quotedSwapEdge val operationArgs = AtomicSwapOperationArgs( - swapLimit = SwapLimit(direction, quotedEdge.quotedAmount, perSegmentSlippage, quotedEdge.quote), + estimatedSwapLimit = SwapLimit(direction, quotedEdge.quotedAmount, slippage, quotedEdge.quote), feePaymentCurrency = segmentExecuteArgs.quotedSwapEdge.edge.identifySegmentCurrency( isFirstSegment = index == 0, firstSegmentFees = firstSegmentFees, @@ -429,10 +487,11 @@ internal class RealSwapService( } private fun logFee(fee: SwapFee) { - val route = fee.atomicOperationFees.joinToString(separator = "\n") { + val route = fee.segments.joinToString(separator = "\n") { segment -> val allFees = buildList { - add(it.submissionFee) - addAll(it.additionalFees) + add(segment.fee.submissionFee) + addAll(segment.fee.postSubmissionFees.paidByAccount) + addAll(segment.fee.postSubmissionFees.paidFromAmount) } allFees.joinToString { it.amount.formatPlanks(it.asset) } diff --git a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/domain/validation/SwapPayloadValidation.kt b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/domain/validation/SwapPayloadValidation.kt index 177d77dea4..59c6d54945 100644 --- a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/domain/validation/SwapPayloadValidation.kt +++ b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/domain/validation/SwapPayloadValidation.kt @@ -1,7 +1,7 @@ package io.novafoundation.nova.feature_swap_impl.domain.validation import io.novafoundation.nova.common.utils.Percent -import io.novafoundation.nova.feature_swap_api.domain.model.SwapExecuteArgs +import io.novafoundation.nova.feature_swap_api.domain.model.SwapFeeArgs import io.novafoundation.nova.feature_swap_api.domain.model.SwapFee import io.novafoundation.nova.feature_swap_api.domain.model.SwapQuote import io.novafoundation.nova.feature_swap_api.domain.model.SwapQuoteArgs @@ -21,7 +21,7 @@ data class SwapValidationPayload( val decimalFee: GenericDecimalFee, val swapQuote: SwapQuote, val swapQuoteArgs: SwapQuoteArgs, - val swapExecuteArgs: SwapExecuteArgs + val swapExecuteArgs: SwapFeeArgs ) { data class SwapAssetData( diff --git a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/confirmation/SwapConfirmationViewModel.kt b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/confirmation/SwapConfirmationViewModel.kt index 0467ed87ae..4e04c1c55f 100644 --- a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/confirmation/SwapConfirmationViewModel.kt +++ b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/confirmation/SwapConfirmationViewModel.kt @@ -23,12 +23,12 @@ import io.novafoundation.nova.feature_account_api.presenatation.actions.External import io.novafoundation.nova.feature_account_api.presenatation.actions.showAddressActions import io.novafoundation.nova.feature_account_api.presenatation.chain.icon import io.novafoundation.nova.feature_swap_api.domain.model.SwapFee +import io.novafoundation.nova.feature_swap_api.domain.model.SwapProgress import io.novafoundation.nova.feature_swap_api.domain.model.SwapQuote import io.novafoundation.nova.feature_swap_api.domain.model.SwapQuoteArgs import io.novafoundation.nova.feature_swap_api.domain.model.editedBalance import io.novafoundation.nova.feature_swap_api.domain.model.swapRate import io.novafoundation.nova.feature_swap_api.domain.model.toExecuteArgs -import io.novafoundation.nova.feature_swap_api.domain.model.totalDeductedPlanks import io.novafoundation.nova.feature_swap_api.presentation.formatters.SwapRateFormatter import io.novafoundation.nova.feature_swap_api.presentation.view.SwapAssetView import io.novafoundation.nova.feature_swap_api.presentation.view.SwapAssetsView @@ -52,6 +52,7 @@ import io.novafoundation.nova.feature_wallet_api.presentation.mixin.amountChoose import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.FeeLoaderMixin import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.FeeStatus import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.GenericFeeLoaderMixin.Configuration +import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.awaitDecimalFee import io.novafoundation.nova.feature_wallet_api.presentation.model.AmountModel import io.novafoundation.nova.feature_wallet_api.presentation.model.mapAmountToAmountModel import io.novafoundation.nova.feature_wallet_api.presentation.model.toAssetPayload @@ -69,6 +70,7 @@ import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.mapNotNull +import kotlinx.coroutines.flow.onCompletion import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch import java.math.BigDecimal @@ -160,9 +162,9 @@ class SwapConfirmationViewModel( private val maxActionProvider = createMaxActionProvider() - private val _validationProgress = MutableStateFlow(false) + private val _submissionInProgress = MutableStateFlow(false) - val validationProgress = _validationProgress + val validationProgress = _submissionInProgress val swapDetails = confirmationStateFlow.filterNotNull().map { formatToSwapDetailsModel(it) @@ -240,23 +242,26 @@ class SwapConfirmationViewModel( assetOutFlow = assetOutFlow, field = Asset::transferableInPlanks, feeLoaderMixin = feeMixin, - extractTotalFee = SwapFee::totalDeductedPlanks ) } private fun executeSwap() = launch { - val quote = confirmationStateFlow.value?.swapQuote ?: return@launch - val swapState = initialSwapState.first() - val executeArgs = quote.toExecuteArgs( - slippage = swapState.slippage, - firstSegmentFees = swapState.fee.firstSegmentFee.asset - ) - - swapInteractor.executeSwap(executeArgs) - .onSuccess { navigateToNextScreen(quote.assetIn) } - .onFailure(::showError) - - _validationProgress.value = false + val fee = feeMixin.awaitDecimalFee().genericFee + val quote = confirmationStateFlow.first()?.swapQuote ?: return@launch + + _submissionInProgress.value = true + + swapInteractor.executeSwap(fee) + .onEach { progressResult -> + when(progressResult) { + SwapProgress.Done -> navigateToNextScreen(quote.assetOut) + is SwapProgress.Failure -> showError(progressResult.error) + is SwapProgress.StepStarted -> showMessage(progressResult.step) + } + } + .onCompletion { + _submissionInProgress.value = false + }.launchIn(viewModelScope) } private fun navigateToNextScreen(asset: Chain.Asset) { @@ -353,6 +358,9 @@ class SwapConfirmationViewModel( } private fun runQuoting(newSwapQuoteArgs: SwapQuoteArgs) { + // TODO + return + launch { val confirmationState = confirmationStateFlow.value ?: return@launch val swapQuote = swapInteractor.quote(newSwapQuoteArgs, viewModelScope) @@ -361,7 +369,7 @@ class SwapConfirmationViewModel( val executeArgs = swapQuote.toExecuteArgs( slippage = slippageFlow.first(), - firstSegmentFees = initialSwapState.first().fee.firstSegmentFee.asset + firstSegmentFees = initialSwapState.first().fee.intermediateSegmentFeesInAssetIn.asset ) feeMixin.loadFeeV2Generic( diff --git a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/main/SwapMainSettingsViewModel.kt b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/main/SwapMainSettingsViewModel.kt index c70fac4ab8..678fce400d 100644 --- a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/main/SwapMainSettingsViewModel.kt +++ b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/main/SwapMainSettingsViewModel.kt @@ -41,7 +41,6 @@ import io.novafoundation.nova.feature_swap_api.domain.model.SwapQuote import io.novafoundation.nova.feature_swap_api.domain.model.SwapQuoteArgs import io.novafoundation.nova.feature_swap_api.domain.model.swapRate import io.novafoundation.nova.feature_swap_api.domain.model.toExecuteArgs -import io.novafoundation.nova.feature_swap_api.domain.model.totalDeductedPlanks import io.novafoundation.nova.feature_swap_api.presentation.formatters.SwapRateFormatter import io.novafoundation.nova.feature_swap_api.presentation.model.SwapSettingsPayload import io.novafoundation.nova.feature_swap_api.presentation.model.mapFromModel @@ -401,7 +400,6 @@ class SwapMainSettingsViewModel( assetOutFlow = assetOutFlow, field = Asset::transferableInPlanks, feeLoaderMixin = feeMixin, - extractTotalFee = SwapFee::totalDeductedPlanks ) } diff --git a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/mixin/maxAction/MaxActionProviderFactory.kt b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/mixin/maxAction/MaxActionProviderFactory.kt index adf4bdadd7..9f4106c393 100644 --- a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/mixin/maxAction/MaxActionProviderFactory.kt +++ b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/mixin/maxAction/MaxActionProviderFactory.kt @@ -6,6 +6,7 @@ import io.novafoundation.nova.feature_wallet_api.domain.model.Asset import io.novafoundation.nova.feature_wallet_api.presentation.mixin.amountChooser.maxAction.MaxActionProvider import io.novafoundation.nova.feature_wallet_api.presentation.mixin.amountChooser.maxAction.MaxActionProviderDsl.deductFee import io.novafoundation.nova.feature_wallet_api.presentation.mixin.amountChooser.maxAction.MaxActionProviderDsl.providingMaxOf +import io.novafoundation.nova.feature_wallet_api.presentation.mixin.amountChooser.maxAction.MaxAvailableDeduction import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.GenericFee import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.GenericFeeLoaderMixin import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry @@ -16,16 +17,15 @@ class MaxActionProviderFactory( private val chainRegistry: ChainRegistry, ) { - fun create( + fun create( assetInFlow: Flow, assetOutFlow: Flow, field: (Asset) -> Balance, feeLoaderMixin: GenericFeeLoaderMixin, - extractTotalFee: (F) -> Balance, allowMaxAction: Boolean = true - ): MaxActionProvider { + ): MaxActionProvider where F : GenericFee, F : MaxAvailableDeduction { return assetInFlow.providingMaxOf(field, allowMaxAction) - .deductFee(feeLoaderMixin, extractTotalFee) + .deductFee(feeLoaderMixin) .disallowReapingIfHasDependents(assetOutFlow, assetSourceRegistry, chainRegistry) } } diff --git a/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/domain/model/CrossChainFee.kt b/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/domain/model/CrossChainFee.kt index b0c381dc9a..04d1a9b604 100644 --- a/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/domain/model/CrossChainFee.kt +++ b/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/domain/model/CrossChainFee.kt @@ -16,7 +16,7 @@ class CrossChainTransferFee( * and is always paid in native currency * */ - val fromOriginInNativeCurrency: FeeBase?, + val fromOriginInNativeCurrency: SubmissionFee?, /** * Total sum of all execution and delivery fees paid from holding register throughout xcm transfer diff --git a/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/presentation/mixin/amountChooser/maxAction/FeeAwareMaxActionProvider.kt b/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/presentation/mixin/amountChooser/maxAction/FeeAwareMaxActionProvider.kt index 7e3266669f..d563e4af58 100644 --- a/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/presentation/mixin/amountChooser/maxAction/FeeAwareMaxActionProvider.kt +++ b/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/presentation/mixin/amountChooser/maxAction/FeeAwareMaxActionProvider.kt @@ -6,6 +6,7 @@ import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.FeeStatu import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.GenericFee import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.GenericFeeLoaderMixin import io.novafoundation.nova.runtime.ext.fullId +import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.combine @@ -46,3 +47,38 @@ class FeeAwareMaxActionProvider( } } } + +interface MaxAvailableDeduction { + + fun deductionFor(amountAsset: Chain.Asset): Balance +} + +class MultiFeeAwareMaxActionProvider( + feeInputMixin: GenericFeeLoaderMixin, + inner: MaxActionProvider, +) : MaxActionProvider where F : GenericFee, F : MaxAvailableDeduction { + + // Fee is not deducted for display + override val maxAvailableForDisplay: Flow = inner.maxAvailableForDisplay + + override val maxAvailableForAction: Flow = combine( + inner.maxAvailableForAction, + feeInputMixin.feeLiveData.asFlow() + ) { maxAvailable, newFeeStatus -> + if (maxAvailable == null) return@combine null + + when (newFeeStatus) { + // do not block in case there is no fee or fee is not yet present + FeeStatus.Error, FeeStatus.NoFee -> maxAvailable + + is FeeStatus.Loaded -> { + val amountAsset = maxAvailable.chainAsset + val deduction = newFeeStatus.feeModel.decimalFee.genericFee.deductionFor(amountAsset) + + maxAvailable - deduction + } + + FeeStatus.Loading -> null + } + } +} diff --git a/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/presentation/mixin/amountChooser/maxAction/MaxActionProvider.kt b/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/presentation/mixin/amountChooser/maxAction/MaxActionProvider.kt index d88e94df0d..d1bf11488b 100644 --- a/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/presentation/mixin/amountChooser/maxAction/MaxActionProvider.kt +++ b/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/presentation/mixin/amountChooser/maxAction/MaxActionProvider.kt @@ -6,8 +6,8 @@ import io.novafoundation.nova.feature_wallet_api.domain.model.Asset import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.GenericFee import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.GenericFeeLoaderMixin import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain -import java.math.BigInteger import kotlinx.coroutines.flow.Flow +import java.math.BigInteger interface MaxActionProvider { @@ -30,6 +30,12 @@ object MaxActionProviderDsl { ): MaxActionProvider { return FeeAwareMaxActionProvider(feeLoaderMixin, extractTotalFee, inner = this) } + + fun MaxActionProvider.deductFee( + feeLoaderMixin: GenericFeeLoaderMixin, + ): MaxActionProvider where F : GenericFee, F : MaxAvailableDeduction { + return MultiFeeAwareMaxActionProvider(feeLoaderMixin, inner = this) + } } infix operator fun MaxActionProvider.MaxAvailableForAction?.minus(other: BigInteger?): MaxActionProvider.MaxAvailableForAction? { diff --git a/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/domain/RealCrossChainTransfersUseCase.kt b/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/domain/RealCrossChainTransfersUseCase.kt index 3e938a2102..77788444f3 100644 --- a/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/domain/RealCrossChainTransfersUseCase.kt +++ b/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/domain/RealCrossChainTransfersUseCase.kt @@ -5,6 +5,7 @@ import io.novafoundation.nova.common.utils.combineToPair import io.novafoundation.nova.common.utils.isPositive import io.novafoundation.nova.common.utils.withFlowScope import io.novafoundation.nova.feature_account_api.data.extrinsic.ExtrinsicService +import io.novafoundation.nova.feature_account_api.data.extrinsic.SubmissionOrigin import io.novafoundation.nova.feature_account_api.data.model.SubstrateFee import io.novafoundation.nova.feature_account_api.data.model.SubstrateFeeBase import io.novafoundation.nova.feature_account_api.domain.interfaces.AccountRepository @@ -123,7 +124,9 @@ internal class RealCrossChainTransfersUseCase( return CrossChainTransferFee( fromOriginInFeeCurrency = originFee, fromOriginInNativeCurrency = crossChainFee.paidByOriginOrNull()?.let { - SubstrateFee(it, originFee.submissionOrigin, transfer.originChain.commissionAsset) + // Delivery fees are also paid by an actual account + val submissionOrigin = SubmissionOrigin.singleOrigin(originFee.submissionOrigin.actualOrigin) + SubstrateFee(it, submissionOrigin, transfer.originChain.commissionAsset) }, fromHoldingRegister = SubstrateFeeBase( amount = crossChainFee.paidFromHoldingRegister, diff --git a/runtime/src/main/java/io/novafoundation/nova/runtime/multiNetwork/runtime/repository/EventsRepository.kt b/runtime/src/main/java/io/novafoundation/nova/runtime/multiNetwork/runtime/repository/EventsRepository.kt index 40cfe4bfbc..b154cfa4b9 100644 --- a/runtime/src/main/java/io/novafoundation/nova/runtime/multiNetwork/runtime/repository/EventsRepository.kt +++ b/runtime/src/main/java/io/novafoundation/nova/runtime/multiNetwork/runtime/repository/EventsRepository.kt @@ -12,6 +12,7 @@ import io.novafoundation.nova.runtime.multiNetwork.getRuntime import io.novafoundation.nova.runtime.network.rpc.RpcCalls import io.novafoundation.nova.runtime.storage.source.StorageDataSource import io.novafoundation.nova.runtime.storage.source.queryNonNull +import io.novasama.substrate_sdk_android.extensions.tryFindNonNull import io.novasama.substrate_sdk_android.runtime.definitions.types.fromHexOrNull import io.novasama.substrate_sdk_android.runtime.definitions.types.generics.Extrinsic import io.novasama.substrate_sdk_android.runtime.definitions.types.generics.GenericEvent @@ -31,6 +32,18 @@ interface EventsRepository { * Unparsed events & extrinsics are not included */ suspend fun getExtrinsicsWithEvents(chainId: ChainId, blockHash: BlockHash? = null): List + + suspend fun getExtrinsicWithEvents(chainId: ChainId, extrinsicHash: String, blockHash: BlockHash? = null): ExtrinsicWithEvents? +} + +suspend fun EventsRepository.getExtrinsicWithEvents( + chainId: ChainId, + extrinsicHash: String, + blockHash: BlockHash +): ExtrinsicWithEvents? { + val allExtrinsics = getExtrinsicsWithEvents(chainId, blockHash) + + return allExtrinsics.find { it.extrinsicHash == extrinsicHash } } class RemoteEventsRepository( @@ -82,4 +95,42 @@ class RemoteEventsRepository( } }.filterNotNull() } + + override suspend fun getExtrinsicWithEvents( + chainId: ChainId, + extrinsicHash: String, + blockHash: BlockHash? + ): ExtrinsicWithEvents? { + val runtime = chainRegistry.getRuntime(chainId) + + val block = rpcCalls.getBlock(chainId, blockHash) + val events = getEventsInBlock(chainId, blockHash) + + return block.block.extrinsics.withIndex().tryFindNonNull { (index, extrinsicScale) -> + val hash = extrinsicScale.extrinsicHash() + if (hash != extrinsicHash) return@tryFindNonNull null + + val extrinsic = Extrinsic.fromHexOrNull(runtime, extrinsicScale) ?: return@tryFindNonNull null + + val extrinsicEvents = events.findByExtrinsicIndex(index) + + ExtrinsicWithEvents( + extrinsicHash = hash, + extrinsic = extrinsic, + events = extrinsicEvents + ) + } + } + + private fun List.findByExtrinsicIndex(index: Int): List { + return mapNotNull { eventRecord -> + val phase = eventRecord.phase + if (phase !is Phase.ApplyExtrinsic) return@mapNotNull null + + val extrinsicIndex = phase.extrinsicId.toInt() + if (extrinsicIndex != index) return@mapNotNull null + + eventRecord.event + } + } } diff --git a/runtime/src/main/java/io/novafoundation/nova/runtime/multiNetwork/runtime/repository/ExtrinsicWithEvents.kt b/runtime/src/main/java/io/novafoundation/nova/runtime/multiNetwork/runtime/repository/ExtrinsicWithEvents.kt index b0c42889e9..a5ffbf95b0 100644 --- a/runtime/src/main/java/io/novafoundation/nova/runtime/multiNetwork/runtime/repository/ExtrinsicWithEvents.kt +++ b/runtime/src/main/java/io/novafoundation/nova/runtime/multiNetwork/runtime/repository/ExtrinsicWithEvents.kt @@ -61,10 +61,24 @@ fun List.requireNativeFee(): BigInteger { } } +fun List.findExtrinsicFailure(): GenericEvent.Instance? { + return findEvent(Modules.SYSTEM, FAILURE_EVENT) +} + +fun List.findExtrinsicFailureOrThrow(): GenericEvent.Instance { + return requireNotNull(findExtrinsicFailure()) { + "No Extrinsic Failure event found" + } +} + fun List.findEvent(module: String, event: String): GenericEvent.Instance? { return find { it.instanceOf(module, event) } } +fun List.findEventOrThrow(module: String, event: String): GenericEvent.Instance { + return first { it.instanceOf(module, event) } +} + fun List.findLastEvent(module: String, event: String): GenericEvent.Instance? { return findLast { it.instanceOf(module, event) } } From 31ae3a86f9a3176c12cfe9a716671368153b7b6e Mon Sep 17 00:00:00 2001 From: Valentun Date: Mon, 21 Oct 2024 11:49:08 +0300 Subject: [PATCH 21/83] Improve logging --- .../CrossChainTransferAssetExchange.kt | 2 +- .../assetExchange/hydraDx/HydraDxExchange.kt | 2 +- .../domain/swap/RealSwapService.kt | 18 ++++++++++++++++-- 3 files changed, 18 insertions(+), 4 deletions(-) diff --git a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/crossChain/CrossChainTransferAssetExchange.kt b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/crossChain/CrossChainTransferAssetExchange.kt index 5d68dfe8a4..e03fd2b79f 100644 --- a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/crossChain/CrossChainTransferAssetExchange.kt +++ b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/crossChain/CrossChainTransferAssetExchange.kt @@ -98,7 +98,7 @@ class CrossChainTransferAssetExchange( } override suspend fun debugLabel(): String { - return "Transfer" + return "To ${chainRegistry.getChain(delegate.to.chainId).name}" } override suspend fun shouldIgnoreFeeRequirementAfter(predecessor: SwapGraphEdge): Boolean { diff --git a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/hydraDx/HydraDxExchange.kt b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/hydraDx/HydraDxExchange.kt index baf9239102..40514be296 100644 --- a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/hydraDx/HydraDxExchange.kt +++ b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/hydraDx/HydraDxExchange.kt @@ -228,7 +228,7 @@ private class HydraDxExchange( } override suspend fun debugLabel(): String { - return "Hydration.${sourceQuotableEdge.debugLabel()}" + return sourceQuotableEdge.debugLabel() } override suspend fun shouldIgnoreFeeRequirementAfter(predecessor: SwapGraphEdge): Boolean { diff --git a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/domain/swap/RealSwapService.kt b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/domain/swap/RealSwapService.kt index 86843a2fdc..2bec0e96fa 100644 --- a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/domain/swap/RealSwapService.kt +++ b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/domain/swap/RealSwapService.kt @@ -515,16 +515,30 @@ internal class RealSwapService( private suspend fun formatTrade(trade: QuotedTrade): String { return buildString { trade.path.onEachIndexed { index, quotedSwapEdge -> + val amountIn: Balance + val amountOut: Balance + + when(trade.direction) { + SwapDirection.SPECIFIED_IN -> { + amountIn = quotedSwapEdge.quotedAmount + amountOut = quotedSwapEdge.quote + } + SwapDirection.SPECIFIED_OUT -> { + amountIn = quotedSwapEdge.quote + amountOut = quotedSwapEdge.quotedAmount + } + } + if (index == 0) { val assetIn = chainRegistry.asset(quotedSwapEdge.edge.from) - val initialAmount = quotedSwapEdge.quotedAmount.formatPlanks(assetIn) + val initialAmount = amountIn.formatPlanks(assetIn) append(initialAmount) } append(" --- " + quotedSwapEdge.edge.debugLabel() + " ---> ") val assetOut = chainRegistry.asset(quotedSwapEdge.edge.to) - val outAmount = quotedSwapEdge.quote.formatPlanks(assetOut) + val outAmount = amountOut.formatPlanks(assetOut) append(outAmount) } From 59b9048fd5607f6d55a87a6d06f042029d55ebfa Mon Sep 17 00:00:00 2001 From: Valentun Date: Mon, 21 Oct 2024 12:07:52 +0300 Subject: [PATCH 22/83] Improve logging --- .../domain/model/AtomicSwapOperation.kt | 8 +++---- .../domain/model/FeeLabels.kt | 22 +++++++++++++++++++ .../AssetConversionExchange.kt | 3 ++- .../CrossChainTransferAssetExchange.kt | 8 ++++--- .../assetExchange/hydraDx/HydraDxExchange.kt | 3 ++- .../domain/swap/RealSwapService.kt | 2 +- 6 files changed, 35 insertions(+), 11 deletions(-) create mode 100644 feature-swap-api/src/main/java/io/novafoundation/nova/feature_swap_api/domain/model/FeeLabels.kt diff --git a/feature-swap-api/src/main/java/io/novafoundation/nova/feature_swap_api/domain/model/AtomicSwapOperation.kt b/feature-swap-api/src/main/java/io/novafoundation/nova/feature_swap_api/domain/model/AtomicSwapOperation.kt index 722f76d8cd..4530ec8e17 100644 --- a/feature-swap-api/src/main/java/io/novafoundation/nova/feature_swap_api/domain/model/AtomicSwapOperation.kt +++ b/feature-swap-api/src/main/java/io/novafoundation/nova/feature_swap_api/domain/model/AtomicSwapOperation.kt @@ -1,8 +1,6 @@ package io.novafoundation.nova.feature_swap_api.domain.model import io.novafoundation.nova.feature_account_api.data.fee.FeePaymentCurrency -import io.novafoundation.nova.feature_account_api.data.model.FeeBase -import io.novafoundation.nova.feature_account_api.data.model.SubmissionFee import io.novafoundation.nova.feature_account_api.data.model.totalPlanksEnsuringAsset import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.types.Balance @@ -33,7 +31,7 @@ class AtomicSwapOperationFee( /** * Fee that is paid when submitting transaction */ - val submissionFee: SubmissionFee, + val submissionFee: SubmissionFeeWithLabel, val postSubmissionFees: PostSubmissionFees = PostSubmissionFees(), ) { @@ -43,12 +41,12 @@ class AtomicSwapOperationFee( * Post-submission fees paid by (some) origin account. * This is typed as `SubmissionFee` as those fee might still use different accounts (e.g. delivery fees are always paid from requested account) */ - val paidByAccount: List = emptyList(), + val paidByAccount: List = emptyList(), /** * Post-submission fees paid from swapping amount directly. Its payment is isolated and does not involve any withdrawals from accounts */ - val paidFromAmount: List = emptyList() + val paidFromAmount: List = emptyList() ) } diff --git a/feature-swap-api/src/main/java/io/novafoundation/nova/feature_swap_api/domain/model/FeeLabels.kt b/feature-swap-api/src/main/java/io/novafoundation/nova/feature_swap_api/domain/model/FeeLabels.kt new file mode 100644 index 0000000000..b833d17aad --- /dev/null +++ b/feature-swap-api/src/main/java/io/novafoundation/nova/feature_swap_api/domain/model/FeeLabels.kt @@ -0,0 +1,22 @@ +package io.novafoundation.nova.feature_swap_api.domain.model + +import io.novafoundation.nova.feature_account_api.data.model.FeeBase +import io.novafoundation.nova.feature_account_api.data.model.SubmissionFee + +interface WithDebugLabel { + val debugLabel: String +} + +class SubmissionFeeWithLabel( + val fee: SubmissionFee, + override val debugLabel: String = "Submission" +): WithDebugLabel, SubmissionFee by fee + +fun SubmissionFeeWithLabel(fee: SubmissionFee?, debugLabel: String): SubmissionFeeWithLabel? { + return fee?.let { SubmissionFeeWithLabel(it, debugLabel) } +} + +class FeeWithLabel( + val fee: FeeBase, + override val debugLabel: String +): WithDebugLabel, FeeBase by fee diff --git a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/assetConversion/AssetConversionExchange.kt b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/assetConversion/AssetConversionExchange.kt index c5078bd54f..e06149327d 100644 --- a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/assetConversion/AssetConversionExchange.kt +++ b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/assetConversion/AssetConversionExchange.kt @@ -15,6 +15,7 @@ import io.novafoundation.nova.feature_swap_api.domain.model.AtomicSwapOperationA import io.novafoundation.nova.feature_swap_api.domain.model.AtomicSwapOperationFee import io.novafoundation.nova.feature_swap_api.domain.model.AtomicSwapOperationSubmissionArgs import io.novafoundation.nova.feature_swap_api.domain.model.ReQuoteTrigger +import io.novafoundation.nova.feature_swap_api.domain.model.SubmissionFeeWithLabel import io.novafoundation.nova.feature_swap_api.domain.model.SwapExecutionCorrection import io.novafoundation.nova.feature_swap_api.domain.model.SwapGraphEdge import io.novafoundation.nova.feature_swap_api.domain.model.SwapLimit @@ -222,7 +223,7 @@ private class AssetConversionExchange( executeSwap(swapLimit = estimatedSwapLimit, sendTo = chain.emptyAccountId()) } - return AtomicSwapOperationFee(submissionFee) + return AtomicSwapOperationFee(SubmissionFeeWithLabel(submissionFee)) } override suspend fun requiredAmountInToGetAmountOut(extraOutAmount: Balance): Balance { diff --git a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/crossChain/CrossChainTransferAssetExchange.kt b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/crossChain/CrossChainTransferAssetExchange.kt index e03fd2b79f..64cf0fbc9f 100644 --- a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/crossChain/CrossChainTransferAssetExchange.kt +++ b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/crossChain/CrossChainTransferAssetExchange.kt @@ -9,7 +9,9 @@ import io.novafoundation.nova.feature_swap_api.domain.model.AtomicSwapOperation import io.novafoundation.nova.feature_swap_api.domain.model.AtomicSwapOperationArgs import io.novafoundation.nova.feature_swap_api.domain.model.AtomicSwapOperationFee import io.novafoundation.nova.feature_swap_api.domain.model.AtomicSwapOperationSubmissionArgs +import io.novafoundation.nova.feature_swap_api.domain.model.FeeWithLabel import io.novafoundation.nova.feature_swap_api.domain.model.ReQuoteTrigger +import io.novafoundation.nova.feature_swap_api.domain.model.SubmissionFeeWithLabel import io.novafoundation.nova.feature_swap_api.domain.model.SwapExecutionCorrection import io.novafoundation.nova.feature_swap_api.domain.model.SwapGraphEdge import io.novafoundation.nova.feature_swap_api.domain.model.SwapLimit @@ -132,13 +134,13 @@ class CrossChainTransferAssetExchange( } return AtomicSwapOperationFee( - submissionFee = crossChainFee.fromOriginInFeeCurrency, + submissionFee = SubmissionFeeWithLabel(crossChainFee.fromOriginInFeeCurrency), postSubmissionFees = AtomicSwapOperationFee.PostSubmissionFees( paidByAccount = listOfNotNull( - crossChainFee.fromOriginInNativeCurrency, + SubmissionFeeWithLabel(crossChainFee.fromOriginInNativeCurrency, debugLabel = "Delivery"), ), paidFromAmount = listOf( - crossChainFee.fromHoldingRegister + FeeWithLabel(crossChainFee.fromHoldingRegister, debugLabel = "Execution") ) ), ) diff --git a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/hydraDx/HydraDxExchange.kt b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/hydraDx/HydraDxExchange.kt index 40514be296..b84f765b1a 100644 --- a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/hydraDx/HydraDxExchange.kt +++ b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/hydraDx/HydraDxExchange.kt @@ -28,6 +28,7 @@ import io.novafoundation.nova.feature_swap_api.domain.model.AtomicSwapOperationA import io.novafoundation.nova.feature_swap_api.domain.model.AtomicSwapOperationFee import io.novafoundation.nova.feature_swap_api.domain.model.AtomicSwapOperationSubmissionArgs import io.novafoundation.nova.feature_swap_api.domain.model.ReQuoteTrigger +import io.novafoundation.nova.feature_swap_api.domain.model.SubmissionFeeWithLabel import io.novafoundation.nova.feature_swap_api.domain.model.SwapExecutionCorrection import io.novafoundation.nova.feature_swap_api.domain.model.SwapGraphEdge import io.novafoundation.nova.feature_swap_api.domain.model.SwapLimit @@ -270,7 +271,7 @@ private class HydraDxExchange( executeSwap(estimatedSwapLimit) } - return AtomicSwapOperationFee(submissionFee) + return AtomicSwapOperationFee(SubmissionFeeWithLabel(submissionFee)) } override suspend fun requiredAmountInToGetAmountOut(extraOutAmount: Balance): Balance { diff --git a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/domain/swap/RealSwapService.kt b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/domain/swap/RealSwapService.kt index 2bec0e96fa..f7b9521919 100644 --- a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/domain/swap/RealSwapService.kt +++ b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/domain/swap/RealSwapService.kt @@ -494,7 +494,7 @@ internal class RealSwapService( addAll(segment.fee.postSubmissionFees.paidFromAmount) } - allFees.joinToString { it.amount.formatPlanks(it.asset) } + allFees.joinToString { "${it.amount.formatPlanks(it.asset)} (${it.debugLabel})" } } Log.d("Swaps", "---- Fees -----") From 295672e01957c8db3118ff63100e76c2faac17de Mon Sep 17 00:00:00 2001 From: Valentun Date: Mon, 21 Oct 2024 19:47:40 +0300 Subject: [PATCH 23/83] Cross chain transfers submission --- .../nova/common/utils/FlowExt.kt | 34 +++++- .../nova/common/utils/graph/Graph.kt | 7 +- .../domain/model/AtomicSwapOperation.kt | 6 + .../domain/model/SwapQuoteArgs.kt | 11 +- .../CrossChainTransferAssetExchange.kt | 10 +- .../domain/swap/RealSwapService.kt | 7 +- .../blockhain/assets/AssetSourceRegistry.kt | 3 + .../blockhain/assets/balances/AssetBalance.kt | 6 +- .../model/TransferableBalanceUpdate.kt | 9 ++ .../assets/events/AssetEventDetector.kt | 13 ++ .../assets/events/model/DepositEvent.kt | 9 ++ .../assets/events/model/TransferEvent.kt | 10 ++ .../assets/tranfers/AssetTransfers.kt | 4 + .../assets/txPayment/SubstrateTxPayment.kt | 9 -- .../crosschain/CrossChainTransactor.kt | 9 ++ .../interfaces/CrossChainTransfersUseCase.kt | 9 ++ .../assets/TypeBasedAssetSourceRegistry.kt | 25 +++- .../balances/UnsupportedAssetBalance.kt | 6 +- .../equilibrium/EquilibriumAssetBalance.kt | 6 +- .../balances/evmErc20/EvmErc20AssetBalance.kt | 5 +- .../evmNative/EvmNativeAssetBalance.kt | 5 +- .../assets/balances/orml/OrmlAssetBalance.kt | 17 +-- .../statemine/StatemineAssetBalance.kt | 48 ++++++-- .../balances/utility/NativeAssetBalance.kt | 14 ++- .../assets/events/UnsupportedEventDetector.kt | 12 ++ .../events/orml/OrmlAssetEventDetector.kt | 55 +++++++++ .../utility/NativeAssetEventDetector.kt | 27 ++++ .../crosschain/RealCrossChainTransactor.kt | 115 ++++++++++++++++++ .../di/WalletFeatureModule.kt | 6 +- .../di/modules/AssetsModule.kt | 10 +- .../di/modules/NativeAssetsModule.kt | 5 + .../di/modules/OrmlAssetsModule.kt | 5 + .../domain/RealCrossChainTransfersUseCase.kt | 16 +++ .../runtime/repository/EventsRepository.kt | 20 +-- .../storage/source/BaseStorageSource.kt | 2 +- .../storage/source/StorageDataSource.kt | 2 +- .../source/query/BaseStorageQueryContext.kt | 23 ++-- .../source/query/LocalStorageQueryContext.kt | 6 +- .../source/query/RemoteStorageQueryContext.kt | 18 ++- .../storage/source/query/WithRawValue.kt | 3 +- 40 files changed, 519 insertions(+), 88 deletions(-) create mode 100644 feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/data/network/blockhain/assets/balances/model/TransferableBalanceUpdate.kt create mode 100644 feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/data/network/blockhain/assets/events/AssetEventDetector.kt create mode 100644 feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/data/network/blockhain/assets/events/model/DepositEvent.kt create mode 100644 feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/data/network/blockhain/assets/events/model/TransferEvent.kt delete mode 100644 feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/data/network/blockhain/assets/txPayment/SubstrateTxPayment.kt create mode 100644 feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/data/network/blockchain/assets/events/UnsupportedEventDetector.kt create mode 100644 feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/data/network/blockchain/assets/events/orml/OrmlAssetEventDetector.kt create mode 100644 feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/data/network/blockchain/assets/events/utility/NativeAssetEventDetector.kt diff --git a/common/src/main/java/io/novafoundation/nova/common/utils/FlowExt.kt b/common/src/main/java/io/novafoundation/nova/common/utils/FlowExt.kt index 70c795f5ad..6ba6d3857d 100644 --- a/common/src/main/java/io/novafoundation/nova/common/utils/FlowExt.kt +++ b/common/src/main/java/io/novafoundation/nova/common/utils/FlowExt.kt @@ -15,6 +15,7 @@ import io.novafoundation.nova.common.utils.input.modifyInput import io.novafoundation.nova.common.utils.input.valueOrNull import io.novafoundation.nova.common.view.InsertableInputField import io.novafoundation.nova.common.view.input.seekbar.Seekbar +import kotlinx.coroutines.CancellationException import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi @@ -44,12 +45,12 @@ import kotlinx.coroutines.flow.merge import kotlinx.coroutines.flow.onCompletion import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onStart -import kotlinx.coroutines.flow.runningFold import kotlinx.coroutines.flow.transform import kotlinx.coroutines.flow.transformLatest import kotlinx.coroutines.flow.transformWhile import kotlinx.coroutines.launch import kotlin.coroutines.coroutineContext +import kotlin.experimental.ExperimentalTypeInference import kotlin.time.Duration inline fun Flow>.filterList(crossinline handler: suspend (T) -> Boolean) = map { list -> @@ -233,6 +234,37 @@ fun Flow.withLoadingSingle(sourceSupplier: suspend (T) -> R): Flow Flow.wrapInResult(): Flow> { + return map { Result.success(it) } + .catch { emit(Result.failure(it)) } +} + +@Suppress("UNCHECKED_CAST") +@OptIn(ExperimentalTypeInference::class) +inline fun Flow>.transformResult( + @BuilderInference crossinline transform: suspend FlowCollector.(value: T) -> Unit +): Flow> { + return transform { upstream -> + upstream.onFailure { + emit(upstream as Result) + }.onSuccess { + val innerCollector = FlowCollector { + emit(Result.success(it)) + } + + runCatching { + transform(innerCollector, it) + }.onFailure { + if (it is CancellationException) { + throw it + } + + emit(Result.failure(it)) + } + } + } +} + fun Flow.withLoadingResult(source: suspend (T) -> Result): Flow> { return transformLatest { item -> emit(ExtendedLoadingState.Loading) diff --git a/common/src/main/java/io/novafoundation/nova/common/utils/graph/Graph.kt b/common/src/main/java/io/novafoundation/nova/common/utils/graph/Graph.kt index f0baf5211b..baad51955c 100644 --- a/common/src/main/java/io/novafoundation/nova/common/utils/graph/Graph.kt +++ b/common/src/main/java/io/novafoundation/nova/common/utils/graph/Graph.kt @@ -52,7 +52,10 @@ suspend fun > Graph.findAllPossibleDestinations( ): Set { val actualNodeListFilter = nodeVisitFilter ?: EdgeVisitFilter { _, _ -> true } - return reachabilityDfs(origin, adjacencyList, actualNodeListFilter, predecessor = null).toSet() + val reachableNodes = reachabilityDfs(origin, adjacencyList, actualNodeListFilter, predecessor = null) + reachableNodes.removeAt(reachableNodes.indexOf(origin)) + + return reachableNodes.toSet() } fun > Graph.hasOutcomingDirections(origin: N): Boolean { @@ -130,7 +133,7 @@ private suspend fun > reachabilityDfs( predecessor: E?, visited: MutableSet = mutableSetOf(), connectedComponentState: MutableList = mutableListOf() -): List { +): MutableList { visited.add(node) connectedComponentState.add(node) diff --git a/feature-swap-api/src/main/java/io/novafoundation/nova/feature_swap_api/domain/model/AtomicSwapOperation.kt b/feature-swap-api/src/main/java/io/novafoundation/nova/feature_swap_api/domain/model/AtomicSwapOperation.kt index 4530ec8e17..7b55678fea 100644 --- a/feature-swap-api/src/main/java/io/novafoundation/nova/feature_swap_api/domain/model/AtomicSwapOperation.kt +++ b/feature-swap-api/src/main/java/io/novafoundation/nova/feature_swap_api/domain/model/AtomicSwapOperation.kt @@ -1,6 +1,7 @@ package io.novafoundation.nova.feature_swap_api.domain.model import io.novafoundation.nova.feature_account_api.data.fee.FeePaymentCurrency +import io.novafoundation.nova.feature_account_api.data.model.totalAmount import io.novafoundation.nova.feature_account_api.data.model.totalPlanksEnsuringAsset import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.types.Balance @@ -50,6 +51,11 @@ class AtomicSwapOperationFee( ) } +fun AtomicSwapOperationFee.amountToLeaveOnOriginToPayTxFees(): Balance { + val submissionAsset = submissionFee.asset + return submissionFee.amount + postSubmissionFees.paidByAccount.totalAmount(submissionAsset, submissionFee.submissionOrigin.requestedOrigin) +} + fun AtomicSwapOperationFee.totalFeeEnsuringSubmissionAsset(): Balance { val postSubmissionFeesByAccount = postSubmissionFees.paidByAccount.totalPlanksEnsuringAsset(submissionFee.asset) val postSubmissionFeesFromHolding = postSubmissionFees.paidByAccount.totalPlanksEnsuringAsset(submissionFee.asset) diff --git a/feature-swap-api/src/main/java/io/novafoundation/nova/feature_swap_api/domain/model/SwapQuoteArgs.kt b/feature-swap-api/src/main/java/io/novafoundation/nova/feature_swap_api/domain/model/SwapQuoteArgs.kt index 298093e6a6..04c4ef5538 100644 --- a/feature-swap-api/src/main/java/io/novafoundation/nova/feature_swap_api/domain/model/SwapQuoteArgs.kt +++ b/feature-swap-api/src/main/java/io/novafoundation/nova/feature_swap_api/domain/model/SwapQuoteArgs.kt @@ -1,6 +1,7 @@ package io.novafoundation.nova.feature_swap_api.domain.model import io.novafoundation.nova.common.utils.Percent +import io.novafoundation.nova.common.utils.divideToDecimal import io.novafoundation.nova.common.utils.fraction import io.novafoundation.nova.common.utils.graph.Path import io.novafoundation.nova.feature_swap_core_api.data.paths.model.QuotedEdge @@ -56,10 +57,7 @@ fun SwapLimit.replaceAmountIn(newAmountIn: Balance): SwapLimit { } private fun SwapLimit.SpecifiedIn.replaceInMultiplier(amount: Balance): BigDecimal { - val amountDecimal = amount.toBigDecimal() - val amountInDecimal = amountIn.toBigDecimal() - - return amountDecimal / amountInDecimal + return amount.divideToDecimal(amountIn) } private fun SwapLimit.SpecifiedIn.replacingInAmount(newInAmount: Balance, replacingAmount: Balance): Balance { @@ -75,10 +73,7 @@ private fun SwapLimit.SpecifiedIn.updateInAmount(newAmountIn: Balance): SwapLimi } private fun SwapLimit.SpecifiedOut.replaceInQuoteMultiplier(amount: Balance): BigDecimal { - val amountDecimal = amount.toBigDecimal() - val amountInQuoteDecimal = amountInQuote.toBigDecimal() - - return amountDecimal / amountInQuoteDecimal + return amount.divideToDecimal(amountInQuote) } private fun SwapLimit.SpecifiedOut.replacedInQuoteAmount(newInQuoteAmount: Balance, replacingAmount: Balance): Balance { diff --git a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/crossChain/CrossChainTransferAssetExchange.kt b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/crossChain/CrossChainTransferAssetExchange.kt index 64cf0fbc9f..8750e4dbb4 100644 --- a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/crossChain/CrossChainTransferAssetExchange.kt +++ b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/crossChain/CrossChainTransferAssetExchange.kt @@ -158,7 +158,15 @@ class CrossChainTransferAssetExchange( } override suspend fun submit(args: AtomicSwapOperationSubmissionArgs): Result { - return Result.failure(UnsupportedOperationException("TODO")) + val transfer = createTransfer(amount = args.actualSwapLimit.crossChainTransferAmount) + + val outcome = with(crossChainTransfersUseCase) { + swapHost.extrinsicService().performTransfer(transfer, computationalScope) + } + + return outcome.map { receivedAmount -> + SwapExecutionCorrection(receivedAmount) + } } private suspend fun createTransfer(amount: Balance): AssetTransferBase { diff --git a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/domain/swap/RealSwapService.kt b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/domain/swap/RealSwapService.kt index f7b9521919..a56c5b18d7 100644 --- a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/domain/swap/RealSwapService.kt +++ b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/domain/swap/RealSwapService.kt @@ -44,6 +44,7 @@ import io.novafoundation.nova.feature_swap_api.domain.model.SwapLimit import io.novafoundation.nova.feature_swap_api.domain.model.SwapProgress import io.novafoundation.nova.feature_swap_api.domain.model.SwapQuote import io.novafoundation.nova.feature_swap_api.domain.model.SwapQuoteArgs +import io.novafoundation.nova.feature_swap_api.domain.model.amountToLeaveOnOriginToPayTxFees import io.novafoundation.nova.feature_swap_api.domain.model.replaceAmountIn import io.novafoundation.nova.feature_swap_api.domain.model.totalFeeEnsuringSubmissionAsset import io.novafoundation.nova.feature_swap_api.domain.swap.SwapService @@ -140,7 +141,7 @@ internal class RealSwapService( ): Flow> { return directionsGraph(computationScope).map { val filter = canPayFeeNodeFilter(computationScope) - it.findAllPossibleDestinations(asset.fullId, filter) + it.findAllPossibleDestinations(asset.fullId, filter) - asset.fullId } } @@ -177,12 +178,12 @@ internal class RealSwapService( return flow { // Zip assumes atomicOperations and atomicOperationFees were constructed the same way - atomicOperations.fold(initialCorrection) { prevStepCorrection, (_, operation) -> + atomicOperations.fold(initialCorrection) { prevStepCorrection, (segmentFee, operation) -> prevStepCorrection.flatMap { correction -> emit(SwapProgress.StepStarted(operation.inProgressLabel())) val newAmountIn = if (correction != null) { - correction.actualReceivedAmount + correction.actualReceivedAmount - segmentFee.amountToLeaveOnOriginToPayTxFees() } else { val amountIn = operation.estimatedSwapLimit.estimatedAmountIn() amountIn + calculatedFee.additionalAmountForSwap.amount diff --git a/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/data/network/blockhain/assets/AssetSourceRegistry.kt b/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/data/network/blockhain/assets/AssetSourceRegistry.kt index 0f1569ff9d..39ac149590 100644 --- a/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/data/network/blockhain/assets/AssetSourceRegistry.kt +++ b/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/data/network/blockhain/assets/AssetSourceRegistry.kt @@ -1,8 +1,11 @@ package io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets +import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.events.AssetEventDetector import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain interface AssetSourceRegistry { fun sourceFor(chainAsset: Chain.Asset): AssetSource + + suspend fun getEventDetector(chainAsset: Chain.Asset): AssetEventDetector } diff --git a/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/data/network/blockhain/assets/balances/AssetBalance.kt b/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/data/network/blockhain/assets/balances/AssetBalance.kt index c362318087..59f6d75ce6 100644 --- a/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/data/network/blockhain/assets/balances/AssetBalance.kt +++ b/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/data/network/blockhain/assets/balances/AssetBalance.kt @@ -4,8 +4,8 @@ import io.novafoundation.nova.common.data.network.runtime.binding.AccountBalance import io.novafoundation.nova.common.data.network.runtime.binding.BlockHash import io.novafoundation.nova.core.updater.SharedRequestsBuilder import io.novafoundation.nova.feature_account_api.domain.model.MetaAccount +import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.balances.model.TransferableBalanceUpdate import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.history.realtime.RealtimeHistoryUpdate -import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.types.Balance import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain import io.novasama.substrate_sdk_android.runtime.AccountId import kotlinx.coroutines.flow.Flow @@ -56,8 +56,8 @@ interface AssetBalance { chain: Chain, chainAsset: Chain.Asset, accountId: AccountId, - sharedSubscriptionBuilder: SharedRequestsBuilder, - ): Flow + sharedSubscriptionBuilder: SharedRequestsBuilder?, + ): Flow suspend fun queryTotalBalance( chain: Chain, diff --git a/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/data/network/blockhain/assets/balances/model/TransferableBalanceUpdate.kt b/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/data/network/blockhain/assets/balances/model/TransferableBalanceUpdate.kt new file mode 100644 index 0000000000..dcade3f1f1 --- /dev/null +++ b/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/data/network/blockhain/assets/balances/model/TransferableBalanceUpdate.kt @@ -0,0 +1,9 @@ +package io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.balances.model + +import io.novafoundation.nova.common.data.network.runtime.binding.BlockHash +import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.types.Balance + +data class TransferableBalanceUpdate( + val newBalance: Balance, + val updatedAt: BlockHash? +) diff --git a/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/data/network/blockhain/assets/events/AssetEventDetector.kt b/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/data/network/blockhain/assets/events/AssetEventDetector.kt new file mode 100644 index 0000000000..6735541ce6 --- /dev/null +++ b/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/data/network/blockhain/assets/events/AssetEventDetector.kt @@ -0,0 +1,13 @@ +package io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.events + +import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.events.model.DepositEvent +import io.novasama.substrate_sdk_android.runtime.definitions.types.generics.GenericEvent + +interface AssetEventDetector { + + fun detectDeposit(event: GenericEvent.Instance): DepositEvent? +} + +fun AssetEventDetector.tryDetectDeposit(event: GenericEvent.Instance): DepositEvent? { + return runCatching { detectDeposit(event) }.getOrNull() +} diff --git a/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/data/network/blockhain/assets/events/model/DepositEvent.kt b/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/data/network/blockhain/assets/events/model/DepositEvent.kt new file mode 100644 index 0000000000..58412b9424 --- /dev/null +++ b/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/data/network/blockhain/assets/events/model/DepositEvent.kt @@ -0,0 +1,9 @@ +package io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.events.model + +import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.types.Balance +import io.novasama.substrate_sdk_android.runtime.AccountId + +class DepositEvent( + val destination: AccountId, + val amount: Balance, +) diff --git a/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/data/network/blockhain/assets/events/model/TransferEvent.kt b/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/data/network/blockhain/assets/events/model/TransferEvent.kt new file mode 100644 index 0000000000..95084e0680 --- /dev/null +++ b/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/data/network/blockhain/assets/events/model/TransferEvent.kt @@ -0,0 +1,10 @@ +package io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.events.model + +import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.types.Balance +import io.novasama.substrate_sdk_android.runtime.AccountId + +class TransferEvent( + val from: AccountId, + val to: AccountId, + val amount: Balance +) 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 425c5a62d7..f0dceaab87 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 @@ -10,6 +10,7 @@ import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.types.Ba 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.domain.model.planksFromAmount +import io.novafoundation.nova.runtime.ext.accountIdOf import io.novafoundation.nova.runtime.ext.accountIdOrNull import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain import io.novasama.substrate_sdk_android.runtime.AccountId @@ -20,6 +21,9 @@ import java.math.BigDecimal interface AssetTransferBase { + val recipientAccountId: AccountId + get() = destinationChain.accountIdOf(recipient) + val recipient: String val originChain: Chain diff --git a/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/data/network/blockhain/assets/txPayment/SubstrateTxPayment.kt b/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/data/network/blockhain/assets/txPayment/SubstrateTxPayment.kt deleted file mode 100644 index b095d02689..0000000000 --- a/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/data/network/blockhain/assets/txPayment/SubstrateTxPayment.kt +++ /dev/null @@ -1,9 +0,0 @@ -package io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.txPayment - -import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain -import io.novasama.substrate_sdk_android.runtime.extrinsic.ExtrinsicBuilder - -interface SubstrateTxPayment { - - suspend fun ExtrinsicBuilder.setFeeAsset(asset: Chain.Asset) -} 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 37f8680551..328dde235b 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 @@ -22,4 +22,13 @@ interface CrossChainTransactor { transfer: AssetTransferBase, crossChainFee: Balance ): Result + + /** + * @return result of actual received balance on destination + */ + context(ExtrinsicService) + suspend fun performAndTrackTransfer( + configuration: CrossChainTransferConfiguration, + transfer: AssetTransferBase, + ): Result } diff --git a/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/domain/interfaces/CrossChainTransfersUseCase.kt b/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/domain/interfaces/CrossChainTransfersUseCase.kt index 199255531d..b65b963fed 100644 --- a/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/domain/interfaces/CrossChainTransfersUseCase.kt +++ b/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/domain/interfaces/CrossChainTransfersUseCase.kt @@ -2,6 +2,7 @@ package io.novafoundation.nova.feature_wallet_api.domain.interfaces import io.novafoundation.nova.feature_account_api.data.extrinsic.ExtrinsicService import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.tranfers.AssetTransferBase +import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.types.Balance import io.novafoundation.nova.feature_wallet_api.domain.model.Asset import io.novafoundation.nova.feature_wallet_api.domain.model.CrossChainTransferFee import io.novafoundation.nova.feature_wallet_api.domain.model.CrossChainTransfersConfiguration @@ -32,6 +33,14 @@ interface CrossChainTransfersUseCase { transfer: AssetTransferBase, computationalScope: CoroutineScope ): CrossChainTransferFee + + /** + * @return result of actual received balance on destination + */ + suspend fun ExtrinsicService.performTransfer( + transfer: AssetTransferBase, + computationalScope: CoroutineScope + ): Result } fun CrossChainTransfersUseCase.incomingCrossChainDirectionsAvailable(destination: Flow): Flow { diff --git a/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/data/network/blockchain/assets/TypeBasedAssetSourceRegistry.kt b/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/data/network/blockchain/assets/TypeBasedAssetSourceRegistry.kt index 7dc8b2c00b..b217e450e0 100644 --- a/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/data/network/blockchain/assets/TypeBasedAssetSourceRegistry.kt +++ b/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/data/network/blockchain/assets/TypeBasedAssetSourceRegistry.kt @@ -4,14 +4,18 @@ import dagger.Lazy import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.AssetSource import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.AssetSourceRegistry import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.balances.AssetBalance +import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.events.AssetEventDetector import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.history.AssetHistory import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.tranfers.AssetTransfers +import io.novafoundation.nova.feature_wallet_impl.data.network.blockchain.assets.events.UnsupportedEventDetector +import io.novafoundation.nova.feature_wallet_impl.data.network.blockchain.assets.events.orml.OrmlAssetEventDetectorFactory +import io.novafoundation.nova.feature_wallet_impl.data.network.blockchain.assets.events.utility.NativeAssetEventDetector import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain class StaticAssetSource( override val transfers: AssetTransfers, override val balance: AssetBalance, - override val history: AssetHistory + override val history: AssetHistory, ) : AssetSource // Use lazy to resolve possible circular dependencies @@ -23,6 +27,9 @@ class TypeBasedAssetSourceRegistry( private val evmNativeSource: Lazy, private val equilibriumAssetSource: Lazy, private val unsupportedBalanceSource: AssetSource, + + private val nativeAssetEventDetector: NativeAssetEventDetector, + private val ormlAssetEventDetectorFactory: OrmlAssetEventDetectorFactory ) : AssetSourceRegistry { override fun sourceFor(chainAsset: Chain.Asset): AssetSource { @@ -36,4 +43,20 @@ class TypeBasedAssetSourceRegistry( Chain.Asset.Type.Unsupported -> unsupportedBalanceSource } } + + override suspend fun getEventDetector(chainAsset: Chain.Asset): AssetEventDetector { + return when (chainAsset.type) { + is Chain.Asset.Type.Equilibrium, + is Chain.Asset.Type.EvmErc20, + Chain.Asset.Type.EvmNative, + + // TODO implement statemine + is Chain.Asset.Type.Statemine, + Chain.Asset.Type.Unsupported -> UnsupportedEventDetector() + + is Chain.Asset.Type.Orml -> ormlAssetEventDetectorFactory.create(chainAsset) + + Chain.Asset.Type.Native -> nativeAssetEventDetector + } + } } diff --git a/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/data/network/blockchain/assets/balances/UnsupportedAssetBalance.kt b/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/data/network/blockchain/assets/balances/UnsupportedAssetBalance.kt index 99995fbd9c..4e27ec5162 100644 --- a/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/data/network/blockchain/assets/balances/UnsupportedAssetBalance.kt +++ b/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/data/network/blockchain/assets/balances/UnsupportedAssetBalance.kt @@ -4,7 +4,7 @@ import io.novafoundation.nova.core.updater.SharedRequestsBuilder import io.novafoundation.nova.feature_account_api.domain.model.MetaAccount import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.balances.AssetBalance import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.balances.BalanceSyncUpdate -import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.types.Balance +import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.balances.model.TransferableBalanceUpdate import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain import io.novasama.substrate_sdk_android.runtime.AccountId import kotlinx.coroutines.flow.Flow @@ -29,8 +29,8 @@ class UnsupportedAssetBalance : AssetBalance { chain: Chain, chainAsset: Chain.Asset, accountId: AccountId, - sharedSubscriptionBuilder: SharedRequestsBuilder - ): Flow = unsupported() + sharedSubscriptionBuilder: SharedRequestsBuilder? + ): Flow = unsupported() override suspend fun queryTotalBalance(chain: Chain, chainAsset: Chain.Asset, accountId: AccountId) = unsupported() diff --git a/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/data/network/blockchain/assets/balances/equilibrium/EquilibriumAssetBalance.kt b/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/data/network/blockchain/assets/balances/equilibrium/EquilibriumAssetBalance.kt index ba5c7fdda9..7939a9278e 100644 --- a/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/data/network/blockchain/assets/balances/equilibrium/EquilibriumAssetBalance.kt +++ b/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/data/network/blockchain/assets/balances/equilibrium/EquilibriumAssetBalance.kt @@ -31,7 +31,7 @@ import io.novafoundation.nova.feature_account_api.domain.model.MetaAccount import io.novafoundation.nova.feature_wallet_api.data.cache.AssetCache import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.balances.AssetBalance import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.balances.BalanceSyncUpdate -import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.types.Balance +import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.balances.model.TransferableBalanceUpdate import io.novafoundation.nova.feature_wallet_impl.data.network.blockchain.assets.balances.bindEquilibriumBalanceLocks import io.novafoundation.nova.feature_wallet_impl.data.network.blockchain.assets.balances.updateLocks import io.novafoundation.nova.runtime.ext.isUtilityAsset @@ -136,8 +136,8 @@ class EquilibriumAssetBalance( chain: Chain, chainAsset: Chain.Asset, accountId: AccountId, - sharedSubscriptionBuilder: SharedRequestsBuilder - ): Flow { + sharedSubscriptionBuilder: SharedRequestsBuilder? + ): Flow { TODO("Not yet implemented") } diff --git a/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/data/network/blockchain/assets/balances/evmErc20/EvmErc20AssetBalance.kt b/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/data/network/blockchain/assets/balances/evmErc20/EvmErc20AssetBalance.kt index 4f0130e4f6..3603b0626f 100644 --- a/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/data/network/blockchain/assets/balances/evmErc20/EvmErc20AssetBalance.kt +++ b/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/data/network/blockchain/assets/balances/evmErc20/EvmErc20AssetBalance.kt @@ -10,6 +10,7 @@ import io.novafoundation.nova.feature_wallet_api.data.cache.AssetCache import io.novafoundation.nova.feature_wallet_api.data.cache.updateNonLockableAsset import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.balances.AssetBalance import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.balances.BalanceSyncUpdate +import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.balances.model.TransferableBalanceUpdate import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.history.realtime.RealtimeHistoryUpdate import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.types.Balance import io.novafoundation.nova.feature_wallet_api.domain.model.Operation @@ -83,8 +84,8 @@ class EvmErc20AssetBalance( chain: Chain, chainAsset: Chain.Asset, accountId: AccountId, - sharedSubscriptionBuilder: SharedRequestsBuilder - ): Flow { + sharedSubscriptionBuilder: SharedRequestsBuilder? + ): Flow { TODO("Not yet implemented") } diff --git a/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/data/network/blockchain/assets/balances/evmNative/EvmNativeAssetBalance.kt b/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/data/network/blockchain/assets/balances/evmNative/EvmNativeAssetBalance.kt index 6e9a2abd05..14c8bb8075 100644 --- a/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/data/network/blockchain/assets/balances/evmNative/EvmNativeAssetBalance.kt +++ b/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/data/network/blockchain/assets/balances/evmNative/EvmNativeAssetBalance.kt @@ -8,6 +8,7 @@ import io.novafoundation.nova.feature_wallet_api.data.cache.AssetCache import io.novafoundation.nova.feature_wallet_api.data.cache.updateNonLockableAsset import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.balances.AssetBalance import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.balances.BalanceSyncUpdate +import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.balances.model.TransferableBalanceUpdate import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.types.Balance import io.novafoundation.nova.runtime.ethereum.sendSuspend import io.novafoundation.nova.runtime.ext.addressOf @@ -65,8 +66,8 @@ class EvmNativeAssetBalance( chain: Chain, chainAsset: Chain.Asset, accountId: AccountId, - sharedSubscriptionBuilder: SharedRequestsBuilder - ): Flow { + sharedSubscriptionBuilder: SharedRequestsBuilder? + ): Flow { TODO("Not yet implemented") } diff --git a/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/data/network/blockchain/assets/balances/orml/OrmlAssetBalance.kt b/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/data/network/blockchain/assets/balances/orml/OrmlAssetBalance.kt index dcf5198a47..8bad79c524 100644 --- a/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/data/network/blockchain/assets/balances/orml/OrmlAssetBalance.kt +++ b/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/data/network/blockchain/assets/balances/orml/OrmlAssetBalance.kt @@ -2,6 +2,8 @@ package io.novafoundation.nova.feature_wallet_impl.data.network.blockchain.asset import io.novafoundation.nova.common.data.network.runtime.binding.AccountBalance import io.novafoundation.nova.common.data.network.runtime.binding.bindOrmlAccountBalanceOrEmpty +import io.novafoundation.nova.common.domain.balance.TransferableMode +import io.novafoundation.nova.common.domain.balance.calculateTransferable import io.novafoundation.nova.common.utils.decodeValue import io.novafoundation.nova.common.utils.tokens import io.novafoundation.nova.core.updater.SharedRequestsBuilder @@ -12,9 +14,7 @@ import io.novafoundation.nova.feature_account_api.domain.model.MetaAccount import io.novafoundation.nova.feature_wallet_api.data.cache.AssetCache import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.balances.AssetBalance import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.balances.BalanceSyncUpdate -import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.types.Balance -import io.novafoundation.nova.common.domain.balance.TransferableMode -import io.novafoundation.nova.common.domain.balance.calculateTransferable +import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.balances.model.TransferableBalanceUpdate import io.novafoundation.nova.feature_wallet_impl.data.network.blockchain.assets.balances.bindBalanceLocks import io.novafoundation.nova.feature_wallet_impl.data.network.blockchain.assets.balances.updateLocks import io.novafoundation.nova.runtime.ext.ormlCurrencyId @@ -79,15 +79,18 @@ class OrmlAssetBalance( chain: Chain, chainAsset: Chain.Asset, accountId: AccountId, - sharedSubscriptionBuilder: SharedRequestsBuilder - ): Flow { + sharedSubscriptionBuilder: SharedRequestsBuilder? + ): Flow { return remoteStorageSource.subscribe(chain.id, sharedSubscriptionBuilder) { - metadata.tokens().storage("Accounts").observe( + metadata.tokens().storage("Accounts").observeWithRaw( accountId, chainAsset.ormlCurrencyId(runtime), binding = ::bindOrmlAccountBalanceOrEmpty ).map { - TransferableMode.REGULAR.calculateTransferable(it) + TransferableBalanceUpdate( + newBalance = TransferableMode.REGULAR.calculateTransferable(it.value), + updatedAt = it.at + ) } } } diff --git a/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/data/network/blockchain/assets/balances/statemine/StatemineAssetBalance.kt b/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/data/network/blockchain/assets/balances/statemine/StatemineAssetBalance.kt index 020c418162..7e61a0840d 100644 --- a/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/data/network/blockchain/assets/balances/statemine/StatemineAssetBalance.kt +++ b/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/data/network/blockchain/assets/balances/statemine/StatemineAssetBalance.kt @@ -1,6 +1,8 @@ package io.novafoundation.nova.feature_wallet_impl.data.network.blockchain.assets.balances.statemine import io.novafoundation.nova.common.data.network.runtime.binding.AccountBalance +import io.novafoundation.nova.common.domain.balance.TransferableMode +import io.novafoundation.nova.common.domain.balance.calculateTransferable import io.novafoundation.nova.common.utils.decodeValue import io.novafoundation.nova.core.storage.StorageCache import io.novafoundation.nova.core.updater.SharedRequestsBuilder @@ -10,6 +12,7 @@ import io.novafoundation.nova.feature_account_api.domain.model.MetaAccount import io.novafoundation.nova.feature_wallet_api.data.cache.AssetCache import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.balances.AssetBalance import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.balances.BalanceSyncUpdate +import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.balances.model.TransferableBalanceUpdate import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.types.Balance import io.novafoundation.nova.feature_wallet_api.domain.model.BalanceLock import io.novafoundation.nova.feature_wallet_impl.data.network.blockchain.assets.common.bindAssetAccountOrEmpty @@ -70,28 +73,49 @@ class StatemineAssetBalance( ) } - val frozenBalance = if (assetAccount.isBalanceFrozen) { - assetAccount.balance + return assetAccount.toAccountBalance() + } + + override suspend fun subscribeTransferableAccountBalance( + chain: Chain, + chainAsset: Chain.Asset, + accountId: AccountId, + sharedSubscriptionBuilder: SharedRequestsBuilder? + ): Flow { + val statemineType = chainAsset.requireStatemine() + + return remoteStorage.subscribe(chain.id, sharedSubscriptionBuilder) { + val encodableId = statemineType.prepareIdForEncoding(runtime) + + runtime.metadata.statemineModule(statemineType).storage("Account").observeWithRaw( + encodableId, + accountId, + binding = ::bindAssetAccountOrEmpty + ).map { + val transferable = it.value.transferableBalance() + TransferableBalanceUpdate(transferable, updatedAt = it.at) + } + } + } + + private fun AssetAccount.transferableBalance(): Balance { + return TransferableMode.REGULAR.calculateTransferable(toAccountBalance()) + } + + private fun AssetAccount.toAccountBalance(): AccountBalance { + val frozenBalance = if (isBalanceFrozen) { + balance } else { BigInteger.ZERO } return AccountBalance( - free = assetAccount.balance, + free = balance, reserved = BigInteger.ZERO, frozen = frozenBalance ) } - override suspend fun subscribeTransferableAccountBalance( - chain: Chain, - chainAsset: Chain.Asset, - accountId: AccountId, - sharedSubscriptionBuilder: SharedRequestsBuilder - ): Flow { - TODO("Not yet implemented") - } - override suspend fun queryTotalBalance(chain: Chain, chainAsset: Chain.Asset, accountId: AccountId): BigInteger { return queryAccountBalance(chain, chainAsset, accountId).free } diff --git a/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/data/network/blockchain/assets/balances/utility/NativeAssetBalance.kt b/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/data/network/blockchain/assets/balances/utility/NativeAssetBalance.kt index 555cf791a0..a0ca630954 100644 --- a/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/data/network/blockchain/assets/balances/utility/NativeAssetBalance.kt +++ b/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/data/network/blockchain/assets/balances/utility/NativeAssetBalance.kt @@ -23,6 +23,7 @@ import io.novafoundation.nova.feature_wallet_api.data.cache.bindAccountInfoOrDef import io.novafoundation.nova.feature_wallet_api.data.cache.updateAsset import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.balances.AssetBalance import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.balances.BalanceSyncUpdate +import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.balances.model.TransferableBalanceUpdate import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.types.Balance import io.novafoundation.nova.feature_wallet_api.domain.model.BalanceHold import io.novafoundation.nova.feature_wallet_impl.data.network.blockchain.SubstrateRemoteSource @@ -110,13 +111,16 @@ class NativeAssetBalance( chain: Chain, chainAsset: Chain.Asset, accountId: AccountId, - sharedSubscriptionBuilder: SharedRequestsBuilder - ): Flow { + sharedSubscriptionBuilder: SharedRequestsBuilder? + ): Flow { return remoteStorage.subscribe(chain.id, sharedSubscriptionBuilder) { - metadata.system.account.observe(accountId).map { - val accountInfo = it ?: AccountInfo.empty() + metadata.system.account.observeWithRaw(accountId).map { + val accountInfo = it.value ?: AccountInfo.empty() - accountInfo.transferableBalance() + TransferableBalanceUpdate( + newBalance = accountInfo.transferableBalance(), + updatedAt = it.at + ) } } } diff --git a/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/data/network/blockchain/assets/events/UnsupportedEventDetector.kt b/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/data/network/blockchain/assets/events/UnsupportedEventDetector.kt new file mode 100644 index 0000000000..ca5e954503 --- /dev/null +++ b/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/data/network/blockchain/assets/events/UnsupportedEventDetector.kt @@ -0,0 +1,12 @@ +package io.novafoundation.nova.feature_wallet_impl.data.network.blockchain.assets.events + +import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.events.AssetEventDetector +import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.events.model.DepositEvent +import io.novasama.substrate_sdk_android.runtime.definitions.types.generics.GenericEvent + +class UnsupportedEventDetector : AssetEventDetector { + + override fun detectDeposit(event: GenericEvent.Instance): DepositEvent? { + return null + } +} diff --git a/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/data/network/blockchain/assets/events/orml/OrmlAssetEventDetector.kt b/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/data/network/blockchain/assets/events/orml/OrmlAssetEventDetector.kt new file mode 100644 index 0000000000..f62d159a5b --- /dev/null +++ b/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/data/network/blockchain/assets/events/orml/OrmlAssetEventDetector.kt @@ -0,0 +1,55 @@ +package io.novafoundation.nova.feature_wallet_impl.data.network.blockchain.assets.events.orml + +import io.novafoundation.nova.common.data.network.runtime.binding.bindAccountId +import io.novafoundation.nova.common.data.network.runtime.binding.bindNumber +import io.novafoundation.nova.common.utils.Modules +import io.novafoundation.nova.common.utils.instanceOf +import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.events.AssetEventDetector +import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.events.model.DepositEvent +import io.novafoundation.nova.runtime.ext.requireOrml +import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry +import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain +import io.novafoundation.nova.runtime.multiNetwork.getRuntime +import io.novasama.substrate_sdk_android.extensions.requireHexPrefix +import io.novasama.substrate_sdk_android.runtime.RuntimeSnapshot +import io.novasama.substrate_sdk_android.runtime.definitions.types.generics.GenericEvent +import io.novasama.substrate_sdk_android.runtime.definitions.types.toHexUntyped + +class OrmlAssetEventDetectorFactory( + private val chainRegistry: ChainRegistry, +) { + + suspend fun create(chainAsset: Chain.Asset): AssetEventDetector { + val ormlType = chainAsset.requireOrml() + val runtime = chainRegistry.getRuntime(chainAsset.chainId) + + return OrmlAssetEventDetector(runtime, ormlType) + } +} + +private class OrmlAssetEventDetector( + private val runtimeSnapshot: RuntimeSnapshot, + private val ormlType: Chain.Asset.Type.Orml, +): AssetEventDetector { + + private val targetCurrencyId = ormlType.currencyIdScale.requireHexPrefix() + + override fun detectDeposit(event: GenericEvent.Instance): DepositEvent? { + return detectTokensDeposited(event) + } + + private fun detectTokensDeposited(event: GenericEvent.Instance): DepositEvent? { + if (!event.instanceOf(Modules.TOKENS, "Deposited")) return null + + val (currencyId, who, amount) = event.arguments + + val currencyIdType = runtimeSnapshot.typeRegistry[ormlType.currencyIdType]!! + val currencyIdEncoded = currencyIdType.toHexUntyped(runtimeSnapshot, currencyId).requireHexPrefix() + if (currencyIdEncoded != targetCurrencyId) return null + + return DepositEvent( + destination = bindAccountId(who), + amount = bindNumber(amount) + ) + } +} diff --git a/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/data/network/blockchain/assets/events/utility/NativeAssetEventDetector.kt b/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/data/network/blockchain/assets/events/utility/NativeAssetEventDetector.kt new file mode 100644 index 0000000000..3e01eff0c1 --- /dev/null +++ b/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/data/network/blockchain/assets/events/utility/NativeAssetEventDetector.kt @@ -0,0 +1,27 @@ +package io.novafoundation.nova.feature_wallet_impl.data.network.blockchain.assets.events.utility + +import io.novafoundation.nova.common.data.network.runtime.binding.bindAccountId +import io.novafoundation.nova.common.data.network.runtime.binding.bindNumber +import io.novafoundation.nova.common.utils.Modules +import io.novafoundation.nova.common.utils.instanceOf +import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.events.AssetEventDetector +import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.events.model.DepositEvent +import io.novasama.substrate_sdk_android.runtime.definitions.types.generics.GenericEvent + +class NativeAssetEventDetector : AssetEventDetector { + + override fun detectDeposit(event: GenericEvent.Instance): DepositEvent? { + return detectMinted(event) + } + + private fun detectMinted(event: GenericEvent.Instance): DepositEvent? { + if (!event.instanceOf(Modules.BALANCES, "Minted")) return null + + val (who, amount) = event.arguments + + return DepositEvent( + destination = bindAccountId(who), + amount = bindNumber(amount) + ) + } +} 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 88f46a2b66..e0bb2a2fb7 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,15 +1,23 @@ package io.novafoundation.nova.feature_wallet_impl.data.network.crosschain +import android.util.Log import io.novafoundation.nova.common.data.network.runtime.binding.Weight +import io.novafoundation.nova.common.utils.flatMap import io.novafoundation.nova.common.utils.orZero +import io.novafoundation.nova.common.utils.transformResult +import io.novafoundation.nova.common.utils.wrapInResult import io.novafoundation.nova.common.utils.xTokensName import io.novafoundation.nova.common.utils.xcmPalletName import io.novafoundation.nova.common.validation.ValidationSystem 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_account_api.data.extrinsic.ExtrinsicSubmission +import io.novafoundation.nova.feature_account_api.data.extrinsic.execution.ExtrinsicExecutionResult +import io.novafoundation.nova.feature_account_api.data.extrinsic.execution.requireOk 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.balances.model.TransferableBalanceUpdate +import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.events.tryDetectDeposit import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.tranfers.AssetTransferBase 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 @@ -34,8 +42,20 @@ import io.novafoundation.nova.feature_wallet_impl.data.network.crosschain.valida import io.novafoundation.nova.feature_wallet_impl.data.network.crosschain.validations.cannotDropBelowEdBeforePayingDeliveryFee import io.novafoundation.nova.runtime.ext.accountIdOrDefault import io.novafoundation.nova.runtime.multiNetwork.multiLocation.MultiLocation +import io.novafoundation.nova.runtime.multiNetwork.runtime.repository.EventsRepository +import io.novafoundation.nova.runtime.multiNetwork.runtime.repository.getInherentEvents +import io.novafoundation.nova.runtime.multiNetwork.runtime.repository.hasEvent +import io.novasama.substrate_sdk_android.runtime.definitions.types.generics.GenericEvent import io.novasama.substrate_sdk_android.runtime.extrinsic.ExtrinsicBuilder +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.shareIn +import kotlinx.coroutines.withTimeout import java.math.BigInteger +import kotlin.coroutines.coroutineContext +import kotlin.time.Duration.Companion.seconds class RealCrossChainTransactor( private val weigher: CrossChainWeigher, @@ -43,6 +63,7 @@ class RealCrossChainTransactor( private val phishingValidationFactory: PhishingValidationFactory, private val palletXcmRepository: PalletXcmRepository, private val enoughTotalToStayAboveEDValidationFactory: EnoughTotalToStayAboveEDValidationFactory, + private val eventsRepository: EventsRepository ) : CrossChainTransactor { override val validationSystem: AssetTransfersValidationSystem = ValidationSystem { @@ -99,6 +120,100 @@ class RealCrossChainTransactor( } } + context(ExtrinsicService) + override suspend fun performAndTrackTransfer( + configuration: CrossChainTransferConfiguration, + transfer: AssetTransferBase, + ): Result { + // Start balances updates eagerly to not to miss events in case tx has been included to block right after submission + val balancesUpdates = observeTransferableBalance(transfer) + .wrapInResult() + .shareIn(CoroutineScope(coroutineContext), SharingStarted.Eagerly, replay = 100) + + Log.d("CrossChain", "Starting cross-chain transfer") + + return performTransferOfExactAmount(configuration, transfer) + .requireOk() + .flatMap { + Log.d("CrossChain", "Cross chain transfer for successfully executed on origin, waiting for destination") + + balancesUpdates.awaitCrossChainArrival(transfer) + } + } + + private suspend fun Flow>.awaitCrossChainArrival(transfer: AssetTransferBase): Result { + return runCatching { + withTimeout(30.seconds) { + transformResult { balanceUpdate -> + Log.d("CrossChain", "Destination balance update detected: $balanceUpdate") + + val updatedAt = balanceUpdate.updatedAt + + if (updatedAt == null) { + Log.w("CrossChain", "Update block hash was not present, maybe wrong datasource is used?") + return@transformResult + } + + val inherentEvents = eventsRepository.getInherentEvents(transfer.destinationChain.id, updatedAt) + + val xcmArrivedDeposit = searchForXcmArrival(inherentEvents.initialization, transfer) + ?: searchForXcmArrival(inherentEvents.finalization, transfer) + + if (xcmArrivedDeposit != null) { + Log.d("CrossChain", "Found destination xcm arrival event, amount is $xcmArrivedDeposit") + + emit(xcmArrivedDeposit) + } else { + Log.d("CrossChain", "No destination xcm arrival event found for the received balance update") + } + } + .first() + .getOrThrow() + } + } + } + + private suspend fun searchForXcmArrival( + events: List, + transfer: AssetTransferBase + ): Balance? { + if (!events.hasEvent("MessageQueue", "Processed")) return null + + val eventDetector = assetSourceRegistry.getEventDetector(transfer.destinationChainAsset) + + val depositEvent = events.mapNotNull { event -> eventDetector.tryDetectDeposit(event) } + .find { it.destination.contentEquals(transfer.recipientAccountId) } + + return depositEvent?.amount + } + + private suspend fun ExtrinsicService.performTransferOfExactAmount( + configuration: CrossChainTransferConfiguration, + transfer: AssetTransferBase, + ): Result { + return submitExtrinsicAndAwaitExecution( + chain = transfer.originChain, + origin = TransactionOrigin.SelectedWallet, + submissionOptions = ExtrinsicService.SubmissionOptions( + feePaymentCurrency = transfer.feePaymentCurrency + ) + ) { + // We are transferring the exact amount, so we should nothing on top of the transfer amount + crossChainTransfer(configuration, transfer, crossChainFee = Balance.ZERO) + } + } + + private suspend fun observeTransferableBalance(transfer: AssetTransferBase): Flow { + val destinationAssetBalances = assetSourceRegistry.sourceFor(transfer.destinationChainAsset) + + return destinationAssetBalances.balance.subscribeTransferableAccountBalance( + chain = transfer.destinationChain, + chainAsset = transfer.destinationChainAsset, + accountId = transfer.recipientAccountId, + sharedSubscriptionBuilder = null + ) + } + private suspend fun ExtrinsicBuilder.crossChainTransfer( configuration: CrossChainTransferConfiguration, transfer: AssetTransferBase, 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 fe90c4cad5..86ab51d0b9 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 @@ -289,13 +289,15 @@ class WalletFeatureModule { assetSourceRegistry: AssetSourceRegistry, phishingValidationFactory: PhishingValidationFactory, palletXcmRepository: PalletXcmRepository, - enoughTotalToStayAboveEDValidationFactory: EnoughTotalToStayAboveEDValidationFactory + enoughTotalToStayAboveEDValidationFactory: EnoughTotalToStayAboveEDValidationFactory, + eventsRepository: EventsRepository ): CrossChainTransactor = RealCrossChainTransactor( weigher = weigher, assetSourceRegistry = assetSourceRegistry, phishingValidationFactory = phishingValidationFactory, palletXcmRepository = palletXcmRepository, - enoughTotalToStayAboveEDValidationFactory = enoughTotalToStayAboveEDValidationFactory + enoughTotalToStayAboveEDValidationFactory = enoughTotalToStayAboveEDValidationFactory, + eventsRepository = eventsRepository ) @Provides diff --git a/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/di/modules/AssetsModule.kt b/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/di/modules/AssetsModule.kt index 0e13daa0fc..e6b894d5d1 100644 --- a/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/di/modules/AssetsModule.kt +++ b/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/di/modules/AssetsModule.kt @@ -7,6 +7,8 @@ import io.novafoundation.nova.common.di.scope.FeatureScope import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.AssetSource import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.AssetSourceRegistry import io.novafoundation.nova.feature_wallet_impl.data.network.blockchain.assets.TypeBasedAssetSourceRegistry +import io.novafoundation.nova.feature_wallet_impl.data.network.blockchain.assets.events.orml.OrmlAssetEventDetectorFactory +import io.novafoundation.nova.feature_wallet_impl.data.network.blockchain.assets.events.utility.NativeAssetEventDetector @Module( includes = [ @@ -31,6 +33,9 @@ class AssetsModule { @EvmNativeAssets evmNative: Lazy, @EquilibriumAsset equilibrium: Lazy, @UnsupportedAssets unsupported: AssetSource, + + nativeAssetEventDetector: NativeAssetEventDetector, + ormlAssetEventDetectorFactory: OrmlAssetEventDetectorFactory, ): AssetSourceRegistry = TypeBasedAssetSourceRegistry( nativeSource = native, statemineSource = statemine, @@ -38,6 +43,9 @@ class AssetsModule { evmErc20Source = evmErc20, evmNativeSource = evmNative, equilibriumAssetSource = equilibrium, - unsupportedBalanceSource = unsupported + unsupportedBalanceSource = unsupported, + + nativeAssetEventDetector = nativeAssetEventDetector, + ormlAssetEventDetectorFactory = ormlAssetEventDetectorFactory ) } diff --git a/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/di/modules/NativeAssetsModule.kt b/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/di/modules/NativeAssetsModule.kt index e83b360193..967f84ae3f 100644 --- a/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/di/modules/NativeAssetsModule.kt +++ b/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/di/modules/NativeAssetsModule.kt @@ -17,6 +17,7 @@ import io.novafoundation.nova.feature_wallet_api.domain.validation.PhishingValid import io.novafoundation.nova.feature_wallet_impl.data.network.blockchain.SubstrateRemoteSource import io.novafoundation.nova.feature_wallet_impl.data.network.blockchain.assets.StaticAssetSource import io.novafoundation.nova.feature_wallet_impl.data.network.blockchain.assets.balances.utility.NativeAssetBalance +import io.novafoundation.nova.feature_wallet_impl.data.network.blockchain.assets.events.utility.NativeAssetEventDetector import io.novafoundation.nova.feature_wallet_impl.data.network.blockchain.assets.history.utility.NativeAssetHistory import io.novafoundation.nova.feature_wallet_impl.data.network.blockchain.assets.transfers.utility.NativeAssetTransfers import io.novafoundation.nova.feature_wallet_impl.data.network.subquery.SubQueryOperationsApi @@ -100,4 +101,8 @@ class NativeAssetsModule { balance = nativeAssetBalance, history = nativeAssetHistory ) + + @Provides + @FeatureScope + fun provideNativeAssetEventsDetector() = NativeAssetEventDetector() } diff --git a/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/di/modules/OrmlAssetsModule.kt b/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/di/modules/OrmlAssetsModule.kt index 9f7facea93..ae5f9e010f 100644 --- a/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/di/modules/OrmlAssetsModule.kt +++ b/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/di/modules/OrmlAssetsModule.kt @@ -14,6 +14,7 @@ import io.novafoundation.nova.feature_wallet_api.domain.validation.EnoughTotalTo import io.novafoundation.nova.feature_wallet_api.domain.validation.PhishingValidationFactory import io.novafoundation.nova.feature_wallet_impl.data.network.blockchain.assets.StaticAssetSource import io.novafoundation.nova.feature_wallet_impl.data.network.blockchain.assets.balances.orml.OrmlAssetBalance +import io.novafoundation.nova.feature_wallet_impl.data.network.blockchain.assets.events.orml.OrmlAssetEventDetectorFactory import io.novafoundation.nova.feature_wallet_impl.data.network.blockchain.assets.history.orml.OrmlAssetHistory import io.novafoundation.nova.feature_wallet_impl.data.network.blockchain.assets.transfers.orml.OrmlAssetTransfers import io.novafoundation.nova.feature_wallet_impl.data.network.subquery.SubQueryOperationsApi @@ -77,4 +78,8 @@ class OrmlAssetsModule { balance = ormlAssetBalance, history = ormlAssetHistory ) + + @Provides + @FeatureScope + fun provideOrmlAssetEventDetectorFactory(chainRegistry: ChainRegistry) = OrmlAssetEventDetectorFactory(chainRegistry) } diff --git a/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/domain/RealCrossChainTransfersUseCase.kt b/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/domain/RealCrossChainTransfersUseCase.kt index 77788444f3..32e2aa36b2 100644 --- a/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/domain/RealCrossChainTransfersUseCase.kt +++ b/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/domain/RealCrossChainTransfersUseCase.kt @@ -10,6 +10,7 @@ import io.novafoundation.nova.feature_account_api.data.model.SubstrateFee import io.novafoundation.nova.feature_account_api.data.model.SubstrateFeeBase import io.novafoundation.nova.feature_account_api.domain.interfaces.AccountRepository import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.tranfers.AssetTransferBase +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.CrossChainTransfersRepository import io.novafoundation.nova.feature_wallet_api.data.network.crosschain.CrossChainWeigher @@ -135,6 +136,21 @@ internal class RealCrossChainTransfersUseCase( ) } + override suspend fun ExtrinsicService.performTransfer( + transfer: AssetTransferBase, + computationalScope: CoroutineScope + ): Result { + val configuration = cachedConfigurationFlow(computationalScope).first() + val transferConfiguration = configuration.transferConfiguration( + originChain = transfer.originChain, + originAsset = transfer.originChainAsset, + destinationChain = transfer.destinationChain, + destinationParaId = parachainInfoRepository.paraId(transfer.destinationChain.id) + )!! + + return crossChainTransactor.performAndTrackTransfer(transferConfiguration, transfer) + } + private fun cachedConfigurationFlow(computationScope: CoroutineScope): Flow { return computationalCache.useSharedFlow(CONFIGURATION_CACHE, computationScope) { crossChainTransfersRepository.configurationFlow() diff --git a/runtime/src/main/java/io/novafoundation/nova/runtime/multiNetwork/runtime/repository/EventsRepository.kt b/runtime/src/main/java/io/novafoundation/nova/runtime/multiNetwork/runtime/repository/EventsRepository.kt index b154cfa4b9..f5e44816dc 100644 --- a/runtime/src/main/java/io/novafoundation/nova/runtime/multiNetwork/runtime/repository/EventsRepository.kt +++ b/runtime/src/main/java/io/novafoundation/nova/runtime/multiNetwork/runtime/repository/EventsRepository.kt @@ -36,14 +36,18 @@ interface EventsRepository { suspend fun getExtrinsicWithEvents(chainId: ChainId, extrinsicHash: String, blockHash: BlockHash? = null): ExtrinsicWithEvents? } -suspend fun EventsRepository.getExtrinsicWithEvents( - chainId: ChainId, - extrinsicHash: String, - blockHash: BlockHash -): ExtrinsicWithEvents? { - val allExtrinsics = getExtrinsicsWithEvents(chainId, blockHash) - - return allExtrinsics.find { it.extrinsicHash == extrinsicHash } +class InherentEvents( + val initialization: List, + val finalization: List +) + +suspend fun EventsRepository.getInherentEvents(chainId: ChainId, at: BlockHash) : InherentEvents { + val allEvents = getEventsInBlock(chainId, at) + + return InherentEvents( + initialization = allEvents.mapNotNull { record -> record.event.takeIf { record.phase is Phase.Initialization } }, + finalization = allEvents.mapNotNull { record -> record.event.takeIf { record.phase is Phase.Finalization } }, + ) } class RemoteEventsRepository( diff --git a/runtime/src/main/java/io/novafoundation/nova/runtime/storage/source/BaseStorageSource.kt b/runtime/src/main/java/io/novafoundation/nova/runtime/storage/source/BaseStorageSource.kt index c0a5354f07..db2097a87f 100644 --- a/runtime/src/main/java/io/novafoundation/nova/runtime/storage/source/BaseStorageSource.kt +++ b/runtime/src/main/java/io/novafoundation/nova/runtime/storage/source/BaseStorageSource.kt @@ -110,7 +110,7 @@ abstract class BaseStorageSource( override suspend fun subscribe( chainId: String, - subscriptionBuilder: SubstrateSubscriptionBuilder, + subscriptionBuilder: SubstrateSubscriptionBuilder?, at: BlockHash?, subscribe: suspend StorageQueryContext.() -> Flow ): Flow { diff --git a/runtime/src/main/java/io/novafoundation/nova/runtime/storage/source/StorageDataSource.kt b/runtime/src/main/java/io/novafoundation/nova/runtime/storage/source/StorageDataSource.kt index 178beaf781..a56952717f 100644 --- a/runtime/src/main/java/io/novafoundation/nova/runtime/storage/source/StorageDataSource.kt +++ b/runtime/src/main/java/io/novafoundation/nova/runtime/storage/source/StorageDataSource.kt @@ -49,7 +49,7 @@ interface StorageDataSource { suspend fun subscribe( chainId: String, - subscriptionBuilder: SubstrateSubscriptionBuilder, + subscriptionBuilder: SubstrateSubscriptionBuilder?, at: BlockHash? = null, subscribe: suspend StorageQueryContext.() -> Flow ): Flow diff --git a/runtime/src/main/java/io/novafoundation/nova/runtime/storage/source/query/BaseStorageQueryContext.kt b/runtime/src/main/java/io/novafoundation/nova/runtime/storage/source/query/BaseStorageQueryContext.kt index 9e864503be..7303d6df13 100644 --- a/runtime/src/main/java/io/novafoundation/nova/runtime/storage/source/query/BaseStorageQueryContext.kt +++ b/runtime/src/main/java/io/novafoundation/nova/runtime/storage/source/query/BaseStorageQueryContext.kt @@ -43,7 +43,7 @@ abstract class BaseStorageQueryContext( protected abstract suspend fun queryKey(key: String, at: BlockHash?): String? - protected abstract fun observeKey(key: String): Flow + protected abstract fun observeKey(key: String): Flow protected abstract suspend fun observeKeys(keys: List): Flow> @@ -156,8 +156,8 @@ abstract class BaseStorageQueryContext( ): Flow { val storageKey = storageKeyWith(keyArguments) - return observeKey(storageKey).map { scale -> - decodeStorageValue(scale, binding) + return observeKey(storageKey).map { storageUpdate -> + decodeStorageValue(storageUpdate.value, binding) } } @@ -167,13 +167,14 @@ abstract class BaseStorageQueryContext( ): Flow> { val storageKey = storageKeyWith(keyArguments) - return observeKey(storageKey).map { scale -> - val decoded = decodeStorageValue(scale, binding) + return observeKey(storageKey).map { storageUpdate -> + val decoded = decodeStorageValue(storageUpdate.value, binding) WithRawValue( - raw = StorageEntryValue(storageKey, scale), + raw = StorageEntryValue(storageKey, storageUpdate.value), chainId = chainId, - value = decoded + value = decoded, + at = storageUpdate.at, ) } } @@ -263,6 +264,13 @@ abstract class BaseStorageQueryContext( } } + + protected class StorageUpdate( + val value: String?, + // Might be null in case the source does not support identifying the block at which value was changed + val at: BlockHash? + ) + private fun StorageEntry.takeDefaultIfAllowed(): Any? { if (!applyStorageDefault) return null @@ -271,6 +279,7 @@ abstract class BaseStorageQueryContext( @JvmInline private value class MultiQueryResult(val delegate: Map, Map>) : MultiQueryBuilder.Result { + @Suppress("UNCHECKED_CAST") override fun get(descriptor: MultiQueryBuilder.Descriptor): Map { return delegate.getValue(descriptor) as Map diff --git a/runtime/src/main/java/io/novafoundation/nova/runtime/storage/source/query/LocalStorageQueryContext.kt b/runtime/src/main/java/io/novafoundation/nova/runtime/storage/source/query/LocalStorageQueryContext.kt index 7da53f83b8..c25ddb2b31 100644 --- a/runtime/src/main/java/io/novafoundation/nova/runtime/storage/source/query/LocalStorageQueryContext.kt +++ b/runtime/src/main/java/io/novafoundation/nova/runtime/storage/source/query/LocalStorageQueryContext.kt @@ -36,8 +36,10 @@ class LocalStorageQueryContext( return storageCache.getEntry(key, chainId).content } - override fun observeKey(key: String): Flow { - return storageCache.observeEntry(key, chainId).map { it.content } + override fun observeKey(key: String): Flow { + return storageCache.observeEntry(key, chainId).map { + StorageUpdate(it.content, at = null) + } } override suspend fun observeKeys(keys: List): Flow> { diff --git a/runtime/src/main/java/io/novafoundation/nova/runtime/storage/source/query/RemoteStorageQueryContext.kt b/runtime/src/main/java/io/novafoundation/nova/runtime/storage/source/query/RemoteStorageQueryContext.kt index 7c927641a1..8e8c7b5f33 100644 --- a/runtime/src/main/java/io/novafoundation/nova/runtime/storage/source/query/RemoteStorageQueryContext.kt +++ b/runtime/src/main/java/io/novafoundation/nova/runtime/storage/source/query/RemoteStorageQueryContext.kt @@ -41,12 +41,24 @@ class RemoteStorageQueryContext( } @Suppress("IfThenToElvis") - override fun observeKey(key: String): Flow { + override fun observeKey(key: String): Flow { return if (subscriptionBuilder != null) { - subscriptionBuilder.subscribe(key).map { it.value } + subscriptionBuilder.subscribe(key).map { + StorageUpdate( + value = it.value, + at = it.block + ) + } } else { socketService.subscriptionFlow(SubscribeStorageRequest(key)) - .map { it.storageChange().getSingleChange() } + .map { + val storageChange = it.storageChange() + + StorageUpdate( + value = storageChange.getSingleChange(), + at = storageChange.block + ) + } } } diff --git a/runtime/src/main/java/io/novafoundation/nova/runtime/storage/source/query/WithRawValue.kt b/runtime/src/main/java/io/novafoundation/nova/runtime/storage/source/query/WithRawValue.kt index 01aae10511..0327b53796 100644 --- a/runtime/src/main/java/io/novafoundation/nova/runtime/storage/source/query/WithRawValue.kt +++ b/runtime/src/main/java/io/novafoundation/nova/runtime/storage/source/query/WithRawValue.kt @@ -1,6 +1,7 @@ package io.novafoundation.nova.runtime.storage.source.query +import io.novafoundation.nova.common.data.network.runtime.binding.BlockHash import io.novafoundation.nova.core.model.StorageEntry import io.novafoundation.nova.runtime.multiNetwork.chain.model.ChainId -class WithRawValue(val raw: StorageEntry, val chainId: ChainId, val value: T) +class WithRawValue(val at: BlockHash?, val raw: StorageEntry, val chainId: ChainId, val value: T) From adc0a8c978d91e1c9cb2c4aebec5ce60b4b1d7f2 Mon Sep 17 00:00:00 2001 From: Valentun Date: Tue, 22 Oct 2024 11:01:30 +0300 Subject: [PATCH 24/83] Change batch type to fix tx inclusion on hydra --- .../data/assetExchange/hydraDx/HydraDxExchange.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/hydraDx/HydraDxExchange.kt b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/hydraDx/HydraDxExchange.kt index b84f765b1a..7ccf5e0eb4 100644 --- a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/hydraDx/HydraDxExchange.kt +++ b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/hydraDx/HydraDxExchange.kt @@ -264,7 +264,7 @@ private class HydraDxExchange( chain = chain, origin = TransactionOrigin.SelectedWallet, submissionOptions = ExtrinsicService.SubmissionOptions( - batchMode = BatchMode.FORCE_BATCH, + batchMode = BatchMode.BATCH_ALL, feePaymentCurrency = feePaymentCurrency ) ) { @@ -306,7 +306,7 @@ private class HydraDxExchange( chain = chain, origin = TransactionOrigin.SelectedWallet, submissionOptions = ExtrinsicService.SubmissionOptions( - batchMode = BatchMode.FORCE_BATCH, + batchMode = BatchMode.BATCH_ALL, feePaymentCurrency = feePaymentCurrency ) ) { From 394dfcfabe1934e9a909a7bd5e0e9f83fb3b2e32 Mon Sep 17 00:00:00 2001 From: Valentun Date: Tue, 22 Oct 2024 12:20:37 +0300 Subject: [PATCH 25/83] Change terminology in SubmissionOrigin --- .../data/extrinsic/SubmissionOrigin.kt | 19 ++++++++++--------- .../feature_account_api/data/model/Fee.kt | 16 ++++++++-------- .../data/extrinsic/RealExtrinsicService.kt | 4 ++-- .../domain/send/SendInteractor.kt | 4 ++-- ...RealNewDelegationChooseAmountInteractor.kt | 2 +- .../RealRemoveTrackVotesInteractor.kt | 2 +- .../unlock/GovernanceUnlockInteractor.kt | 2 +- .../PoolAvailableBalanceValidation.kt | 4 ++-- .../AvailableBalanceGapValidation.kt | 4 ++-- .../domain/model/AtomicSwapOperation.kt | 2 +- .../feature_swap_api/domain/model/SwapFee.kt | 6 +++--- .../domain/model/SwapQuote.kt | 4 ++-- .../domain/paths/RealPathQuoter.kt | 2 +- .../AssetConversionExchange.kt | 2 +- .../assetExchange/hydraDx/HydraDxExchange.kt | 10 +--------- ...onsideringNonSufficientAssetsValidation.kt | 4 ++-- ...tBalanceToPayFeeConsideringEDValidation.kt | 4 ++-- .../main/SwapValidationFailureUi.kt | 6 +++--- .../presentation/mixin/fee/FeeParcelModel.kt | 8 ++++---- .../presentation/model/FeeModel.kt | 4 ++-- ...pBelowEdWhenPayingDeliveryFeeValidation.kt | 6 +++--- .../domain/RealCrossChainTransfersUseCase.kt | 2 +- 22 files changed, 55 insertions(+), 62 deletions(-) diff --git a/feature-account-api/src/main/java/io/novafoundation/nova/feature_account_api/data/extrinsic/SubmissionOrigin.kt b/feature-account-api/src/main/java/io/novafoundation/nova/feature_account_api/data/extrinsic/SubmissionOrigin.kt index 99aafde1ec..4384b75fad 100644 --- a/feature-account-api/src/main/java/io/novafoundation/nova/feature_account_api/data/extrinsic/SubmissionOrigin.kt +++ b/feature-account-api/src/main/java/io/novafoundation/nova/feature_account_api/data/extrinsic/SubmissionOrigin.kt @@ -5,15 +5,16 @@ import io.novasama.substrate_sdk_android.runtime.extrinsic.signer.Signer data class SubmissionOrigin( /** - * Origin that was originally requested to sign the transaction + * Account on which behalf the operation will be executed */ - val requestedOrigin: AccountId, + val executingAccount: AccountId, /** - * Origin that was actually used to sign the transaction. - * It might differ from [requestedOrigin] if [Signer] modified the origin, for example in the case of Proxied wallet + * Account that will sign and submit transaction + * It might differ from [executingAccount] if [Signer] modified the origin. + * For example in the case of Proxied wallet [executingAccount] is proxied and [signingAccount] is proxy */ - val actualOrigin: AccountId + val signingAccount: AccountId ) { companion object { @@ -27,13 +28,13 @@ data class SubmissionOrigin( other as SubmissionOrigin - if (!requestedOrigin.contentEquals(other.requestedOrigin)) return false - return actualOrigin.contentEquals(other.actualOrigin) + if (!executingAccount.contentEquals(other.executingAccount)) return false + return signingAccount.contentEquals(other.signingAccount) } override fun hashCode(): Int { - var result = requestedOrigin.contentHashCode() - result = 31 * result + actualOrigin.contentHashCode() + var result = executingAccount.contentHashCode() + result = 31 * result + signingAccount.contentHashCode() return result } } 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 885c28cf12..864af3480e 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 @@ -73,11 +73,11 @@ class SubstrateFeeBase( override val asset: Chain.Asset ) : FeeBase -val Fee.requestedAccountPaysFees: Boolean - get() = submissionOrigin.requestedOrigin.contentEquals(submissionOrigin.actualOrigin) +val Fee.executingAccountPaysFee: Boolean + get() = submissionOrigin.executingAccount.contentEquals(submissionOrigin.signingAccount) -val Fee.amountByRequestedAccount: BigInteger - get() = amount.asAmountByRequestedAccount +val Fee.amountByExecutingAccount: BigInteger + get() = amount.asAmountByExecutingAccount fun List.totalAmount(chainAsset: Chain.Asset): BigInteger { return sumOf { it.getAmount(chainAsset) } @@ -90,7 +90,7 @@ fun List.totalAmount(chainAsset: Chain.Asset, origin: AccountId): fun List.totalPlanksEnsuringAsset(requireAsset: Chain.Asset): BigInteger { return sumOf { require(it.asset.fullId == requireAsset.fullId) { - "Atomic operation fee contains fee in different assets" + "Fees contain fee in different assets: ${it.asset.fullId}" } it.amount @@ -98,7 +98,7 @@ fun List.totalPlanksEnsuringAsset(requireAsset: Chain.Asset): BigIntege } fun SubmissionFee.getAmount(chainAsset: Chain.Asset, origin: AccountId): BigInteger { - return if (asset.fullId == chainAsset.fullId && submissionOrigin.actualOrigin.contentEquals(origin)) { + return if (asset.fullId == chainAsset.fullId && submissionOrigin.signingAccount.contentEquals(origin)) { amount } else { BigInteger.ZERO @@ -110,8 +110,8 @@ fun FeeBase.getAmount(expectedAsset: Chain.Asset): BigInteger { } context(Fee) -val BigInteger.asAmountByRequestedAccount: BigInteger - get() = if (requestedAccountPaysFees) { +val BigInteger.asAmountByExecutingAccount: BigInteger + get() = if (executingAccountPaysFee) { this } else { BigInteger.ZERO diff --git a/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/data/extrinsic/RealExtrinsicService.kt b/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/data/extrinsic/RealExtrinsicService.kt index 14eb7776ce..479edb861a 100644 --- a/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/data/extrinsic/RealExtrinsicService.kt +++ b/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/data/extrinsic/RealExtrinsicService.kt @@ -267,7 +267,7 @@ class RealExtrinsicService( val requestedOrigin = metaAccount.requireAccountIdIn(chain) val actualOrigin = signer.signerAccountId(chain) - val submissionOrigin = SubmissionOrigin(requestedOrigin = requestedOrigin, actualOrigin = actualOrigin) + val submissionOrigin = SubmissionOrigin(executingAccount = requestedOrigin, signingAccount = actualOrigin) val extrinsicBuilderSequence = extrinsicBuilderFactory.createMulti(chain, signer, requestedOrigin) @@ -319,7 +319,7 @@ class RealExtrinsicService( val requestedOrigin = metaAccount.requireAccountIdIn(chain) val actualOrigin = signer.signerAccountId(chain) - val submissionOrigin = SubmissionOrigin(requestedOrigin = requestedOrigin, actualOrigin = actualOrigin) + val submissionOrigin = SubmissionOrigin(executingAccount = requestedOrigin, signingAccount = actualOrigin) val extrinsicBuilder = extrinsicBuilderFactory.create(chain, signer, requestedOrigin) extrinsicBuilder.formExtrinsic(submissionOrigin) 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 091d618c8b..a0bfaf6b62 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 @@ -4,7 +4,7 @@ import io.novafoundation.nova.feature_account_api.data.extrinsic.ExtrinsicServic 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.data.model.amountByExecutingAccount 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 @@ -100,7 +100,7 @@ class SendInteractor( val config = crossChainTransfersRepository.getConfiguration().configurationFor(transfer)!! with(crossChainTransactor) { - extrinsicService.performTransfer(config, transfer, crossChainFee!!.amountByRequestedAccount) + extrinsicService.performTransfer(config, transfer, crossChainFee!!.amountByExecutingAccount) } } else { val networkFee = originFee.networkFeePart() diff --git a/feature-governance-impl/src/main/java/io/novafoundation/nova/feature_governance_impl/domain/delegation/delegation/create/chooseAmount/RealNewDelegationChooseAmountInteractor.kt b/feature-governance-impl/src/main/java/io/novafoundation/nova/feature_governance_impl/domain/delegation/delegation/create/chooseAmount/RealNewDelegationChooseAmountInteractor.kt index d0ce741495..fcfc3fb52d 100644 --- a/feature-governance-impl/src/main/java/io/novafoundation/nova/feature_governance_impl/domain/delegation/delegation/create/chooseAmount/RealNewDelegationChooseAmountInteractor.kt +++ b/feature-governance-impl/src/main/java/io/novafoundation/nova/feature_governance_impl/domain/delegation/delegation/create/chooseAmount/RealNewDelegationChooseAmountInteractor.kt @@ -80,7 +80,7 @@ class RealNewDelegationChooseAmountInteractor( amount = amount, conviction = conviction, delegate = delegate, - user = origin.requestedOrigin, + user = origin.executingAccount, chain = chain, tracks = tracks, shouldRemoveOtherTracks = shouldRemoveOtherTracks diff --git a/feature-governance-impl/src/main/java/io/novafoundation/nova/feature_governance_impl/domain/delegation/delegation/removeVotes/RealRemoveTrackVotesInteractor.kt b/feature-governance-impl/src/main/java/io/novafoundation/nova/feature_governance_impl/domain/delegation/delegation/removeVotes/RealRemoveTrackVotesInteractor.kt index 34b0728b2e..d4cea57d17 100644 --- a/feature-governance-impl/src/main/java/io/novafoundation/nova/feature_governance_impl/domain/delegation/delegation/removeVotes/RealRemoveTrackVotesInteractor.kt +++ b/feature-governance-impl/src/main/java/io/novafoundation/nova/feature_governance_impl/domain/delegation/delegation/removeVotes/RealRemoveTrackVotesInteractor.kt @@ -41,7 +41,7 @@ class RealRemoveTrackVotesInteractor( val (chain, governance) = useSelectedGovernance() extrinsicService.submitAndWatchExtrinsic(chain, TransactionOrigin.SelectedWallet) { origin -> - governance.removeVotes(trackIds, extrinsicBuilder = this, chain.id, accountIdToRemoveVotes = origin.requestedOrigin) + governance.removeVotes(trackIds, extrinsicBuilder = this, chain.id, accountIdToRemoveVotes = origin.executingAccount) }.awaitInBlock() } diff --git a/feature-governance-impl/src/main/java/io/novafoundation/nova/feature_governance_impl/domain/referendum/unlock/GovernanceUnlockInteractor.kt b/feature-governance-impl/src/main/java/io/novafoundation/nova/feature_governance_impl/domain/referendum/unlock/GovernanceUnlockInteractor.kt index a3a9987cc8..0d20ee562d 100644 --- a/feature-governance-impl/src/main/java/io/novafoundation/nova/feature_governance_impl/domain/referendum/unlock/GovernanceUnlockInteractor.kt +++ b/feature-governance-impl/src/main/java/io/novafoundation/nova/feature_governance_impl/domain/referendum/unlock/GovernanceUnlockInteractor.kt @@ -99,7 +99,7 @@ class RealGovernanceUnlockInteractor( extrinsicService.submitAndWatchExtrinsic(chain, TransactionOrigin.SelectedWallet) { origin -> if (claimable == null) error("Nothing to claim") - executeUnlock(accountIdToUnlock = origin.requestedOrigin, governanceSelectedOption, claimable) + executeUnlock(accountIdToUnlock = origin.executingAccount, governanceSelectedOption, claimable) }.awaitInBlock() } diff --git a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/nominationPools/common/validations/PoolAvailableBalanceValidation.kt b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/nominationPools/common/validations/PoolAvailableBalanceValidation.kt index 4e3ec72755..fdd7ba5749 100644 --- a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/nominationPools/common/validations/PoolAvailableBalanceValidation.kt +++ b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/nominationPools/common/validations/PoolAvailableBalanceValidation.kt @@ -10,7 +10,7 @@ import io.novafoundation.nova.common.validation.ValidationFlowActions import io.novafoundation.nova.common.validation.ValidationStatus import io.novafoundation.nova.common.validation.ValidationSystemBuilder import io.novafoundation.nova.common.validation.isTrueOrWarning -import io.novafoundation.nova.feature_account_api.data.model.amountByRequestedAccount +import io.novafoundation.nova.feature_account_api.data.model.amountByExecutingAccount import io.novafoundation.nova.feature_staking_impl.R import io.novafoundation.nova.feature_staking_impl.domain.staking.start.common.NominationPoolsAvailableBalanceResolver import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.types.Balance @@ -72,7 +72,7 @@ class PoolAvailableBalanceValidation( val asset = asset(value) val chainAsset = asset.token.configuration - val fee = fee(value)?.networkFee?.amountByRequestedAccount.orZero() + val fee = fee(value)?.networkFee?.amountByExecutingAccount.orZero() val availableBalance = poolsAvailableBalanceResolver.availableBalanceToStartStaking(asset) val maxToStake = poolsAvailableBalanceResolver.maximumBalanceToStake(asset, fee) val enteredAmount = chainAsset.planksFromAmount(amount(value)) diff --git a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/staking/start/common/validations/AvailableBalanceGapValidation.kt b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/staking/start/common/validations/AvailableBalanceGapValidation.kt index 09ad392904..49919e7db6 100644 --- a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/staking/start/common/validations/AvailableBalanceGapValidation.kt +++ b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/staking/start/common/validations/AvailableBalanceGapValidation.kt @@ -3,7 +3,7 @@ package io.novafoundation.nova.feature_staking_impl.domain.staking.start.common. import io.novafoundation.nova.common.validation.ValidationStatus import io.novafoundation.nova.common.validation.valid import io.novafoundation.nova.common.validation.validationError -import io.novafoundation.nova.feature_account_api.data.model.amountByRequestedAccount +import io.novafoundation.nova.feature_account_api.data.model.amountByExecutingAccount import io.novafoundation.nova.feature_staking_impl.data.asset import io.novafoundation.nova.feature_staking_impl.data.chain import io.novafoundation.nova.feature_staking_impl.data.stakingType @@ -19,7 +19,7 @@ class AvailableBalanceGapValidation( override suspend fun validate(value: StartMultiStakingValidationPayload): ValidationStatus { val amount = value.selection.stake val stakingOption = value.selection.stakingOption - val fee = value.fee.networkFee.amountByRequestedAccount + val fee = value.fee.networkFee.amountByExecutingAccount val maxToStakeWithMinStakes = candidates.map { val maximumToStake = it.maximumToStake(value.asset, fee) diff --git a/feature-swap-api/src/main/java/io/novafoundation/nova/feature_swap_api/domain/model/AtomicSwapOperation.kt b/feature-swap-api/src/main/java/io/novafoundation/nova/feature_swap_api/domain/model/AtomicSwapOperation.kt index 7b55678fea..3425ea37a8 100644 --- a/feature-swap-api/src/main/java/io/novafoundation/nova/feature_swap_api/domain/model/AtomicSwapOperation.kt +++ b/feature-swap-api/src/main/java/io/novafoundation/nova/feature_swap_api/domain/model/AtomicSwapOperation.kt @@ -53,7 +53,7 @@ class AtomicSwapOperationFee( fun AtomicSwapOperationFee.amountToLeaveOnOriginToPayTxFees(): Balance { val submissionAsset = submissionFee.asset - return submissionFee.amount + postSubmissionFees.paidByAccount.totalAmount(submissionAsset, submissionFee.submissionOrigin.requestedOrigin) + return submissionFee.amount + postSubmissionFees.paidByAccount.totalAmount(submissionAsset, submissionFee.submissionOrigin.executingAccount) } fun AtomicSwapOperationFee.totalFeeEnsuringSubmissionAsset(): Balance { diff --git a/feature-swap-api/src/main/java/io/novafoundation/nova/feature_swap_api/domain/model/SwapFee.kt b/feature-swap-api/src/main/java/io/novafoundation/nova/feature_swap_api/domain/model/SwapFee.kt index b47e0edb29..1861502419 100644 --- a/feature-swap-api/src/main/java/io/novafoundation/nova/feature_swap_api/domain/model/SwapFee.kt +++ b/feature-swap-api/src/main/java/io/novafoundation/nova/feature_swap_api/domain/model/SwapFee.kt @@ -34,10 +34,10 @@ class SwapFee( override val networkFee: Fee = determineNetworkFee() override fun deductionFor(amountAsset: Chain.Asset): Balance { - val requestedAccount = submissionFee.submissionOrigin.requestedOrigin + val executingAccount = submissionFee.submissionOrigin.executingAccount - val submissionFeeAmount = submissionFee.getAmount(amountAsset, requestedAccount) - val additionalFeesAmount = postSubmissionFees.paidByAccount.totalAmount(amountAsset, requestedAccount) + val submissionFeeAmount = submissionFee.getAmount(amountAsset, executingAccount) + val additionalFeesAmount = postSubmissionFees.paidByAccount.totalAmount(amountAsset, executingAccount) return submissionFeeAmount + additionalFeesAmount + additionalAmountForSwap.getAmount(amountAsset) } diff --git a/feature-swap-api/src/main/java/io/novafoundation/nova/feature_swap_api/domain/model/SwapQuote.kt b/feature-swap-api/src/main/java/io/novafoundation/nova/feature_swap_api/domain/model/SwapQuote.kt index a5cf72ebb9..2eb624e7a3 100644 --- a/feature-swap-api/src/main/java/io/novafoundation/nova/feature_swap_api/domain/model/SwapQuote.kt +++ b/feature-swap-api/src/main/java/io/novafoundation/nova/feature_swap_api/domain/model/SwapQuote.kt @@ -1,7 +1,7 @@ package io.novafoundation.nova.feature_swap_api.domain.model import io.novafoundation.nova.common.utils.Percent -import io.novafoundation.nova.feature_account_api.data.model.amountByRequestedAccount +import io.novafoundation.nova.feature_account_api.data.model.amountByExecutingAccount import io.novafoundation.nova.feature_swap_core_api.data.paths.model.QuotedPath import io.novafoundation.nova.feature_swap_core_api.data.primitive.model.SwapDirection import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.types.Balance @@ -59,4 +59,4 @@ infix fun ChainAssetWithAmount.rateAgainst(assetOut: ChainAssetWithAmount): BigD val SwapFee.totalDeductedPlanks: Balance - get() = networkFee.amountByRequestedAccount + get() = networkFee.amountByExecutingAccount diff --git a/feature-swap-core/src/main/java/io/novafoundation/nova/feature_swap_core/domain/paths/RealPathQuoter.kt b/feature-swap-core/src/main/java/io/novafoundation/nova/feature_swap_core/domain/paths/RealPathQuoter.kt index eec69ed3b2..3cb45bb45d 100644 --- a/feature-swap-core/src/main/java/io/novafoundation/nova/feature_swap_core/domain/paths/RealPathQuoter.kt +++ b/feature-swap-core/src/main/java/io/novafoundation/nova/feature_swap_core/domain/paths/RealPathQuoter.kt @@ -41,7 +41,7 @@ private class RealPathQuoter( private val computationalCache: ComputationalCache, private val graph: Graph, private val computationalScope: CoroutineScope, - private val filter: EdgeVisitFilter? + private val filter: EdgeVisitFilter?, ) : PathQuoter { override suspend fun findBestPath( diff --git a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/assetConversion/AssetConversionExchange.kt b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/assetConversion/AssetConversionExchange.kt index e06149327d..7cd36e3d25 100644 --- a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/assetConversion/AssetConversionExchange.kt +++ b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/assetConversion/AssetConversionExchange.kt @@ -250,7 +250,7 @@ private class AssetConversionExchange( ) ) { submissionOrigin -> // Send swapped funds to the requested origin since it the account doing the swap - executeSwap(swapLimit = args.actualSwapLimit, sendTo = submissionOrigin.requestedOrigin) + executeSwap(swapLimit = args.actualSwapLimit, sendTo = submissionOrigin.executingAccount) }.awaitInBlock().map { TODO() } diff --git a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/hydraDx/HydraDxExchange.kt b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/hydraDx/HydraDxExchange.kt index 7ccf5e0eb4..0fa0380c37 100644 --- a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/hydraDx/HydraDxExchange.kt +++ b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/hydraDx/HydraDxExchange.kt @@ -49,7 +49,6 @@ import io.novafoundation.nova.feature_swap_impl.data.assetExchange.hydraDx.refer import io.novafoundation.nova.feature_swap_impl.data.assetExchange.hydraDx.referrals.linkedAccounts import io.novafoundation.nova.feature_swap_impl.data.assetExchange.hydraDx.referrals.referralsOrNull import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.AssetSourceRegistry -import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.existentialDepositInPlanks import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.types.Balance import io.novafoundation.nova.runtime.ethereum.StorageSharedRequestsBuilder import io.novafoundation.nova.runtime.ethereum.StorageSharedRequestsBuilderFactory @@ -511,15 +510,8 @@ private class HydraDxExchange( val quotedFee = swapHost.quote(args) - // TODO - // There is a issue in Router implementation in Hydra that doesn't allow asset balance to go below ED. We add it to fee for simplicity instead - // of refactoring SwapExistentialDepositAwareMaxActionProvider - // This should be removed once Router issue is fixed - val existentialDeposit = assetSourceRegistry.existentialDepositInPlanks(chain, customFeeAsset) - val fee = quotedFee + existentialDeposit - return SubstrateFee( - amount = fee, + amount = quotedFee, submissionOrigin = nativeFee.submissionOrigin, asset = customFeeAsset ) diff --git a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/domain/validation/SufficientBalanceConsideringNonSufficientAssetsValidation.kt b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/domain/validation/SufficientBalanceConsideringNonSufficientAssetsValidation.kt index 38e7960e03..23a7b04c96 100644 --- a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/domain/validation/SufficientBalanceConsideringNonSufficientAssetsValidation.kt +++ b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/domain/validation/SufficientBalanceConsideringNonSufficientAssetsValidation.kt @@ -3,7 +3,7 @@ package io.novafoundation.nova.feature_swap_impl.domain.validation import io.novafoundation.nova.common.validation.ValidationStatus import io.novafoundation.nova.common.validation.valid import io.novafoundation.nova.common.validation.validOrError -import io.novafoundation.nova.feature_account_api.data.model.amountByRequestedAccount +import io.novafoundation.nova.feature_account_api.data.model.amountByExecutingAccount import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.AssetSourceRegistry import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.existentialDepositInPlanks import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.isSelfSufficientAsset @@ -22,7 +22,7 @@ class SufficientBalanceConsideringNonSufficientAssetsValidation( if (!isSelfSufficientAssetOut && assetIn.token.configuration.isCommissionAsset) { val existentialDeposit = assetSourceRegistry.existentialDepositInPlanks(value.detailedAssetIn.chain, assetIn.token.configuration) - val fee = value.decimalFee.networkFee.amountByRequestedAccount + val fee = value.decimalFee.networkFee.amountByExecutingAccount return validOrError(assetIn.balanceCountedTowardsEDInPlanks - existentialDeposit >= amount + fee) { SwapValidationFailure.InsufficientBalance.BalanceNotConsiderInsufficientReceiveAsset( diff --git a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/domain/validation/validations/EnoughNativeAssetBalanceToPayFeeConsideringEDValidation.kt b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/domain/validation/validations/EnoughNativeAssetBalanceToPayFeeConsideringEDValidation.kt index 2142a9562f..f287e5888f 100644 --- a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/domain/validation/validations/EnoughNativeAssetBalanceToPayFeeConsideringEDValidation.kt +++ b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/domain/validation/validations/EnoughNativeAssetBalanceToPayFeeConsideringEDValidation.kt @@ -3,7 +3,7 @@ package io.novafoundation.nova.feature_swap_impl.domain.validation.validations import io.novafoundation.nova.common.validation.ValidationStatus import io.novafoundation.nova.common.validation.valid import io.novafoundation.nova.common.validation.validOrError -import io.novafoundation.nova.feature_account_api.data.model.amountByRequestedAccount +import io.novafoundation.nova.feature_account_api.data.model.amountByExecutingAccount import io.novafoundation.nova.feature_swap_impl.domain.validation.SwapValidation import io.novafoundation.nova.feature_swap_impl.domain.validation.SwapValidationFailure import io.novafoundation.nova.feature_swap_impl.domain.validation.SwapValidationFailure.NotEnoughFunds @@ -28,7 +28,7 @@ class EnoughNativeAssetBalanceToPayFeeConsideringEDValidation( val chain = chainRegistry.getChain(feeChainAsset.chainId) val existentialDeposit = assetSourceRegistry.existentialDepositInPlanks(chain, feeChainAsset) val availableBalance = value.feeAsset.balanceCountedTowardsEDInPlanks - val fee = value.decimalFee.networkFee.amountByRequestedAccount + val fee = value.decimalFee.networkFee.amountByExecutingAccount return validOrError(availableBalance - fee >= existentialDeposit) { val minRequiredBalance = existentialDeposit + fee NotEnoughFunds.ToPayFeeAndStayAboveED( diff --git a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/main/SwapValidationFailureUi.kt b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/main/SwapValidationFailureUi.kt index e0551b128a..c454653e05 100644 --- a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/main/SwapValidationFailureUi.kt +++ b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/main/SwapValidationFailureUi.kt @@ -8,7 +8,7 @@ import io.novafoundation.nova.common.validation.TransformedFailure import io.novafoundation.nova.common.validation.ValidationFlowActions import io.novafoundation.nova.common.validation.ValidationStatus import io.novafoundation.nova.common.validation.asDefault -import io.novafoundation.nova.feature_account_api.data.model.amountByRequestedAccount +import io.novafoundation.nova.feature_account_api.data.model.amountByExecutingAccount import io.novafoundation.nova.feature_swap_api.domain.model.SwapFee import io.novafoundation.nova.feature_swap_impl.domain.validation.SwapValidationFailure import io.novafoundation.nova.feature_swap_impl.domain.validation.SwapValidationFailure.AmountOutIsTooLowToStayAboveED @@ -75,7 +75,7 @@ fun CoroutineScope.mapSwapValidationFailureToUI( message = resourceManager.getString( R.string.swap_failure_insufficient_balance_message, reason.maxSwapAmount.formatPlanks(reason.assetIn), - reason.fee.amountByRequestedAccount.formatPlanks(reason.feeAsset) + reason.fee.amountByExecutingAccount.formatPlanks(reason.feeAsset) ), resourceManager = resourceManager, positiveButtonClick = amountInSwapMaxAction @@ -86,7 +86,7 @@ fun CoroutineScope.mapSwapValidationFailureToUI( resourceManager.getString( R.string.swap_failure_balance_not_consider_consumers, reason.existentialDeposit.formatPlanks(reason.nativeAsset), - reason.swapFee.networkFee.amountByRequestedAccount.formatPlanks(reason.feeAsset) + reason.swapFee.networkFee.amountByExecutingAccount.formatPlanks(reason.feeAsset) ) ).asDefault() diff --git a/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/presentation/mixin/fee/FeeParcelModel.kt b/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/presentation/mixin/fee/FeeParcelModel.kt index 7796a04172..e441b7c10c 100644 --- a/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/presentation/mixin/fee/FeeParcelModel.kt +++ b/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/presentation/mixin/fee/FeeParcelModel.kt @@ -23,8 +23,8 @@ sealed interface FeeParcelModel : Parcelable { @Parcelize class SubmissionOriginParcelModel( - val requested: AccountId, - val actual: AccountId + val executingAccount: AccountId, + val signingAccount: AccountId ) : Parcelable @Parcelize @@ -67,7 +67,7 @@ fun mapFeeToParcel(decimalFee: GenericDecimalFee<*>): FeeParcelModel { } private fun mapSubmissionOriginToParcel(submissionOrigin: SubmissionOrigin): SubmissionOriginParcelModel { - return with(submissionOrigin) { SubmissionOriginParcelModel(requested = requestedOrigin, actual = actualOrigin) } + return with(submissionOrigin) { SubmissionOriginParcelModel(executingAccount = executingAccount, signingAccount = signingAccount) } } fun mapFeeFromParcel(parcelFee: FeeParcelModel): DecimalFee { @@ -88,5 +88,5 @@ fun mapFeeFromParcel(parcelFee: FeeParcelModel): DecimalFee { } private fun mapSubmissionOriginFromParcel(submissionOrigin: SubmissionOriginParcelModel): SubmissionOrigin { - return with(submissionOrigin) { SubmissionOrigin(requestedOrigin = requested, actualOrigin = actual) } + return with(submissionOrigin) { SubmissionOrigin(executingAccount = executingAccount, signingAccount = signingAccount) } } 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 b05eb84d55..465f77a4d0 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 @@ -2,7 +2,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_account_api.data.model.executingAccountPaysFee 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 @@ -37,7 +37,7 @@ class GenericDecimalFee( } val GenericDecimalFee.networkFeeByRequestedAccount: BigDecimal - get() = if (networkFee.requestedAccountPaysFees) networkFeeDecimalAmount else BigDecimal.ZERO + get() = if (networkFee.executingAccountPaysFee) networkFeeDecimalAmount else BigDecimal.ZERO val GenericDecimalFee?.networkFeeByRequestedAccountOrZero: BigDecimal get() = this?.networkFeeByRequestedAccount.orZero() diff --git a/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/data/network/crosschain/validations/CannotDropBelowEdWhenPayingDeliveryFeeValidation.kt b/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/data/network/crosschain/validations/CannotDropBelowEdWhenPayingDeliveryFeeValidation.kt index ddc4904cde..5409e6c4cd 100644 --- a/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/data/network/crosschain/validations/CannotDropBelowEdWhenPayingDeliveryFeeValidation.kt +++ b/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/data/network/crosschain/validations/CannotDropBelowEdWhenPayingDeliveryFeeValidation.kt @@ -5,7 +5,7 @@ import io.novafoundation.nova.common.utils.orZero import io.novafoundation.nova.common.validation.ValidationStatus import io.novafoundation.nova.common.validation.valid import io.novafoundation.nova.common.validation.validationError -import io.novafoundation.nova.feature_account_api.data.model.amountByRequestedAccount +import io.novafoundation.nova.feature_account_api.data.model.amountByExecutingAccount import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.AssetSourceRegistry import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.existentialDepositInPlanks import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.tranfers.AssetTransferPayload @@ -31,8 +31,8 @@ class CannotDropBelowEdWhenPayingDeliveryFeeValidation( val deliveryFeePart = value.originFee.deliveryFeePart()?.networkFee?.amount.orZero() val paysDeliveryFee = deliveryFeePart.isPositive() - val networkFeePlanks = value.originFee.networkFeePart().networkFee.amountByRequestedAccount - val crossChainFeePlanks = value.crossChainFee?.networkFee?.amountByRequestedAccount.orZero() + val networkFeePlanks = value.originFee.networkFeePart().networkFee.amountByExecutingAccount + val crossChainFeePlanks = value.crossChainFee?.networkFee?.amountByExecutingAccount.orZero() val sendingAmount = value.transfer.amountInPlanks + crossChainFeePlanks val requiredAmountWhenPayingDeliveryFee = sendingAmount + networkFeePlanks + deliveryFeePart + existentialDeposit diff --git a/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/domain/RealCrossChainTransfersUseCase.kt b/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/domain/RealCrossChainTransfersUseCase.kt index 32e2aa36b2..660512b144 100644 --- a/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/domain/RealCrossChainTransfersUseCase.kt +++ b/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/domain/RealCrossChainTransfersUseCase.kt @@ -126,7 +126,7 @@ internal class RealCrossChainTransfersUseCase( fromOriginInFeeCurrency = originFee, fromOriginInNativeCurrency = crossChainFee.paidByOriginOrNull()?.let { // Delivery fees are also paid by an actual account - val submissionOrigin = SubmissionOrigin.singleOrigin(originFee.submissionOrigin.actualOrigin) + val submissionOrigin = SubmissionOrigin.singleOrigin(originFee.submissionOrigin.signingAccount) SubstrateFee(it, submissionOrigin, transfer.originChain.commissionAsset) }, fromHoldingRegister = SubstrateFeeBase( From ae045473f19e2dcfc09c31fb52a9fd6803f7d281 Mon Sep 17 00:00:00 2001 From: Valentun Date: Tue, 22 Oct 2024 12:58:51 +0300 Subject: [PATCH 26/83] Fixes --- .../feature_account_api/data/model/Fee.kt | 23 ++++--------------- .../send/amount/SelectSendViewModel.kt | 2 +- .../domain/swap/RealSwapService.kt | 4 ++-- 3 files changed, 8 insertions(+), 21 deletions(-) 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 864af3480e..5a5b91e8a1 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 @@ -21,6 +21,9 @@ interface SubmissionFee : FeeBase { val submissionOrigin: SubmissionOrigin } +val SubmissionFee.submissionFeesPayer: AccountId + get() = submissionOrigin.signingAccount + /** * Fee that doesn't have a particular origin * For example, fees paid during cross chain transfers do not have a specific account that pays them @@ -32,22 +35,6 @@ interface FeeBase { val asset: Chain.Asset } -infix fun FeeBase.hasSameAssetAs(other: FeeBase): Boolean { - return asset.fullId == other.asset.fullId -} - -infix fun Fee.addPreservingOrigin(other: FeeBase): Fee { - require(this hasSameAssetAs other) { - "Cannot sum fees with different assets" - } - - return addPlanks(other.amount) -} - -infix fun Fee.addPlanks(planks: BigInteger): Fee { - return SubstrateFee(amount + planks, submissionOrigin, asset) -} - fun Fee.replacePlanks(newPlanks: BigInteger): Fee { return SubstrateFee(newPlanks, submissionOrigin, asset) } @@ -74,7 +61,7 @@ class SubstrateFeeBase( ) : FeeBase val Fee.executingAccountPaysFee: Boolean - get() = submissionOrigin.executingAccount.contentEquals(submissionOrigin.signingAccount) + get() = submissionOrigin.executingAccount.contentEquals(submissionFeesPayer) val Fee.amountByExecutingAccount: BigInteger get() = amount.asAmountByExecutingAccount @@ -98,7 +85,7 @@ fun List.totalPlanksEnsuringAsset(requireAsset: Chain.Asset): BigIntege } fun SubmissionFee.getAmount(chainAsset: Chain.Asset, origin: AccountId): BigInteger { - return if (asset.fullId == chainAsset.fullId && submissionOrigin.signingAccount.contentEquals(origin)) { + return if (asset.fullId == chainAsset.fullId && submissionFeesPayer.contentEquals(origin)) { amount } else { BigInteger.ZERO 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 8161f456fd..4618abc630 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 @@ -162,7 +162,7 @@ class SelectSendViewModel( coroutineScope, configuration = Configuration( initialState = Configuration.InitialState( - supportCustomFee = true + supportCustomFee = false ) ) ) diff --git a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/domain/swap/RealSwapService.kt b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/domain/swap/RealSwapService.kt index a56c5b18d7..0574c4a58b 100644 --- a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/domain/swap/RealSwapService.kt +++ b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/domain/swap/RealSwapService.kt @@ -192,10 +192,10 @@ internal class RealSwapService( val actualSwapLimit = operation.estimatedSwapLimit.replaceAmountIn(newAmountIn) val segmentSubmissionArgs = AtomicSwapOperationSubmissionArgs(actualSwapLimit) - Log.d("Swaps", operation.inProgressLabel() + " with $actualSwapLimit") + Log.d("SwapSubmission", operation.inProgressLabel() + " with $actualSwapLimit") operation.submit(segmentSubmissionArgs).onFailure { - Log.e("Swaps", "Swap failed on stage '${operation.inProgressLabel()}'", it) + Log.e("SwapSubmission", "Swap failed on stage '${operation.inProgressLabel()}'", it) emit(SwapProgress.Failure(it)) } From 8fc264277d5ad01a5b43c799d3b046055e6f9a2a Mon Sep 17 00:00:00 2001 From: Valentun Date: Tue, 22 Oct 2024 13:18:37 +0300 Subject: [PATCH 27/83] Increase timeout --- .../data/network/crosschain/RealCrossChainTransactor.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 e0bb2a2fb7..7f4037344e 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 @@ -143,7 +143,7 @@ class RealCrossChainTransactor( private suspend fun Flow>.awaitCrossChainArrival(transfer: AssetTransferBase): Result { return runCatching { - withTimeout(30.seconds) { + withTimeout(60.seconds) { transformResult { balanceUpdate -> Log.d("CrossChain", "Destination balance update detected: $balanceUpdate") From 1ab7257c45f7c8855884866b26b0009eeabce765 Mon Sep 17 00:00:00 2001 From: Valentun Date: Tue, 22 Oct 2024 14:15:52 +0300 Subject: [PATCH 28/83] Add buffer to hydra fee conversion --- ...ydraDxExchange.kt => HydraDxAssetExchange.kt} | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) rename feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/hydraDx/{HydraDxExchange.kt => HydraDxAssetExchange.kt} (98%) diff --git a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/hydraDx/HydraDxExchange.kt b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/hydraDx/HydraDxAssetExchange.kt similarity index 98% rename from feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/hydraDx/HydraDxExchange.kt rename to feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/hydraDx/HydraDxAssetExchange.kt index 0fa0380c37..3630ca5b16 100644 --- a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/hydraDx/HydraDxExchange.kt +++ b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/hydraDx/HydraDxAssetExchange.kt @@ -7,6 +7,7 @@ import io.novafoundation.nova.common.utils.forEachAsync import io.novafoundation.nova.common.utils.mergeIfMultiple import io.novafoundation.nova.common.utils.singleReplaySharedFlow import io.novafoundation.nova.common.utils.structOf +import io.novafoundation.nova.common.utils.times import io.novafoundation.nova.common.utils.withFlowScope import io.novafoundation.nova.feature_account_api.data.ethereum.transaction.TransactionOrigin import io.novafoundation.nova.feature_account_api.data.extrinsic.ExtrinsicService @@ -48,7 +49,6 @@ import io.novafoundation.nova.feature_swap_impl.data.assetExchange.ParentQuoterA import io.novafoundation.nova.feature_swap_impl.data.assetExchange.hydraDx.referrals.HydraDxNovaReferral import io.novafoundation.nova.feature_swap_impl.data.assetExchange.hydraDx.referrals.linkedAccounts import io.novafoundation.nova.feature_swap_impl.data.assetExchange.hydraDx.referrals.referralsOrNull -import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.AssetSourceRegistry import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.types.Balance import io.novafoundation.nova.runtime.ethereum.StorageSharedRequestsBuilder import io.novafoundation.nova.runtime.ethereum.StorageSharedRequestsBuilderFactory @@ -77,24 +77,21 @@ import kotlinx.coroutines.flow.onEach class HydraDxExchangeFactory( private val remoteStorageSource: StorageDataSource, private val sharedRequestsBuilderFactory: StorageSharedRequestsBuilderFactory, - private val extrinsicServiceFactory: ExtrinsicService.Factory, private val hydraDxAssetIdConverter: HydraDxAssetIdConverter, private val hydraDxNovaReferral: HydraDxNovaReferral, private val swapSourceFactories: Iterable>, private val quotingFactory: HydraDxQuoting.Factory, - private val assetSourceRegistry: AssetSourceRegistry, private val hydrationFeeInjector: HydrationFeeInjector ) : AssetExchange.SingleChainFactory { override suspend fun create(chain: Chain, swapHost: AssetExchange.SwapHost, coroutineScope: CoroutineScope): AssetExchange { - return HydraDxExchange( + return HydraDxAssetExchange( remoteStorageSource = remoteStorageSource, chain = chain, storageSharedRequestsBuilderFactory = sharedRequestsBuilderFactory, hydraDxAssetIdConverter = hydraDxAssetIdConverter, hydraDxNovaReferral = hydraDxNovaReferral, swapSourceFactories = swapSourceFactories, - assetSourceRegistry = assetSourceRegistry, swapHost = swapHost, hydrationFeeInjector = hydrationFeeInjector, delegate = quotingFactory.create(chain), @@ -103,8 +100,9 @@ class HydraDxExchangeFactory( } private const val ROUTE_EXECUTED_AMOUNT_OUT_IDX = 3 +private const val FEE_QUOTE_BUFFER = 1.1 -private class HydraDxExchange( +private class HydraDxAssetExchange( private val delegate: HydraDxQuoting, private val remoteStorageSource: StorageDataSource, private val chain: Chain, @@ -112,7 +110,6 @@ private class HydraDxExchange( private val hydraDxAssetIdConverter: HydraDxAssetIdConverter, private val hydraDxNovaReferral: HydraDxNovaReferral, private val swapSourceFactories: Iterable>, - private val assetSourceRegistry: AssetSourceRegistry, private val swapHost: AssetExchange.SwapHost, private val hydrationFeeInjector: HydrationFeeInjector, ) : AssetExchange { @@ -510,8 +507,11 @@ private class HydraDxExchange( val quotedFee = swapHost.quote(args) + // Fees in non-native assets are especially volatile since conversion happens through swaps so we add some buffer to mitigate volatility + val quotedFeeWithBuffer = quotedFee * FEE_QUOTE_BUFFER + return SubstrateFee( - amount = quotedFee, + amount = quotedFeeWithBuffer, submissionOrigin = nativeFee.submissionOrigin, asset = customFeeAsset ) From 6ab5c4612d90cb7549b58e75b8b0021a353137a4 Mon Sep 17 00:00:00 2001 From: Valentun Date: Wed, 23 Oct 2024 13:20:56 +0300 Subject: [PATCH 29/83] Finish asset conversion swaps and assets cross chain deposits --- .../domain/model/AtomicSwapOperation.kt | 6 ++ .../feature_swap_api/domain/model/SwapFee.kt | 10 ++- .../AssetConversionExchange.kt | 26 +++++-- .../CrossChainTransferAssetExchange.kt | 5 ++ .../hydraDx/HydraDxAssetExchange.kt | 5 ++ .../di/exchanges/HydraDxExchangeModule.kt | 6 -- .../domain/swap/RealSwapService.kt | 8 +- .../crosschain/CrossChainTransactor.kt | 3 + .../interfaces/CrossChainTransfersUseCase.kt | 5 ++ .../assets/TypeBasedAssetSourceRegistry.kt | 8 +- .../statemine/StatemineAssetEventDetector.kt | 74 +++++++++++++++++++ .../substrate/AssetConversionSwapExtractor.kt | 3 +- .../history/realtime/substrate/Common.kt | 7 +- .../crosschain/RealCrossChainTransactor.kt | 5 ++ .../di/modules/AssetsModule.kt | 5 +- .../di/modules/StatemineAssetsModule.kt | 5 ++ .../domain/RealCrossChainTransfersUseCase.kt | 4 + .../runtime/repository/ExtrinsicWithEvents.kt | 4 + 18 files changed, 164 insertions(+), 25 deletions(-) create mode 100644 feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/data/network/blockchain/assets/events/statemine/StatemineAssetEventDetector.kt diff --git a/feature-swap-api/src/main/java/io/novafoundation/nova/feature_swap_api/domain/model/AtomicSwapOperation.kt b/feature-swap-api/src/main/java/io/novafoundation/nova/feature_swap_api/domain/model/AtomicSwapOperation.kt index 3425ea37a8..9349c9f478 100644 --- a/feature-swap-api/src/main/java/io/novafoundation/nova/feature_swap_api/domain/model/AtomicSwapOperation.kt +++ b/feature-swap-api/src/main/java/io/novafoundation/nova/feature_swap_api/domain/model/AtomicSwapOperation.kt @@ -13,6 +13,12 @@ interface AtomicSwapOperation { suspend fun requiredAmountInToGetAmountOut(extraOutAmount: Balance): Balance + /** + * Additional amount that max amount calculation should leave aside for the **first** operation in the swap + * One example is Existential Deposit in case operation executes in "keep alive" manner + */ + suspend fun additionalMaxAmountDeduction(): Balance + // TODO this is a temporarily function until we developer Operation Manager suspend fun inProgressLabel(): String diff --git a/feature-swap-api/src/main/java/io/novafoundation/nova/feature_swap_api/domain/model/SwapFee.kt b/feature-swap-api/src/main/java/io/novafoundation/nova/feature_swap_api/domain/model/SwapFee.kt index 1861502419..db542fb537 100644 --- a/feature-swap-api/src/main/java/io/novafoundation/nova/feature_swap_api/domain/model/SwapFee.kt +++ b/feature-swap-api/src/main/java/io/novafoundation/nova/feature_swap_api/domain/model/SwapFee.kt @@ -16,7 +16,9 @@ class SwapFee( /** * Fees for second and subsequent segments converted to assetIn */ - val intermediateSegmentFeesInAssetIn: FeeBase + val intermediateSegmentFeesInAssetIn: FeeBase, + + private val additionalMaxAmountDeduction: Balance, ) : GenericFee, MaxAvailableDeduction { data class SwapSegment(val fee: AtomicSwapOperationFee, val operation: AtomicSwapOperation) @@ -34,6 +36,10 @@ class SwapFee( override val networkFee: Fee = determineNetworkFee() override fun deductionFor(amountAsset: Chain.Asset): Balance { + return totalFeeAmount(amountAsset) + additionalMaxAmountDeduction + } + + private fun totalFeeAmount(amountAsset: Chain.Asset): Balance { val executingAccount = submissionFee.submissionOrigin.executingAccount val submissionFeeAmount = submissionFee.getAmount(amountAsset, executingAccount) @@ -52,6 +58,6 @@ class SwapFee( // TODO this is for simpler understanding of real fee until multi-chain view is developed private fun determineNetworkFee(): Fee { val submissionFeeAsset = submissionFee.asset - return submissionFee.replacePlanks(newPlanks = deductionFor(submissionFeeAsset)) + return submissionFee.replacePlanks(newPlanks = totalFeeAmount(submissionFeeAsset)) } } diff --git a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/assetConversion/AssetConversionExchange.kt b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/assetConversion/AssetConversionExchange.kt index 7cd36e3d25..67c34a1783 100644 --- a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/assetConversion/AssetConversionExchange.kt +++ b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/assetConversion/AssetConversionExchange.kt @@ -1,5 +1,6 @@ package io.novafoundation.nova.feature_swap_impl.data.assetExchange.assetConversion +import io.novafoundation.nova.common.data.network.runtime.binding.bindNumber import io.novafoundation.nova.common.data.network.runtime.binding.bindNumberOrNull import io.novafoundation.nova.common.utils.Modules import io.novafoundation.nova.common.utils.assetConversion @@ -7,8 +8,8 @@ import io.novafoundation.nova.feature_account_api.data.conversion.assethub.asset import io.novafoundation.nova.feature_account_api.data.conversion.assethub.pools 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_account_api.data.extrinsic.awaitInBlock import io.novafoundation.nova.feature_account_api.data.extrinsic.createDefault +import io.novafoundation.nova.feature_account_api.data.extrinsic.execution.requireOk import io.novafoundation.nova.feature_account_api.domain.model.MetaAccount import io.novafoundation.nova.feature_swap_api.domain.model.AtomicSwapOperation import io.novafoundation.nova.feature_swap_api.domain.model.AtomicSwapOperationArgs @@ -36,10 +37,12 @@ import io.novafoundation.nova.runtime.multiNetwork.multiLocation.converter.Multi import io.novafoundation.nova.runtime.multiNetwork.multiLocation.converter.MultiLocationConverterFactory import io.novafoundation.nova.runtime.multiNetwork.multiLocation.converter.toMultiLocationOrThrow import io.novafoundation.nova.runtime.multiNetwork.multiLocation.toEncodableInstance +import io.novafoundation.nova.runtime.multiNetwork.runtime.repository.findEventOrThrow import io.novafoundation.nova.runtime.repository.ChainStateRepository import io.novafoundation.nova.runtime.storage.source.StorageDataSource import io.novafoundation.nova.runtime.storage.source.query.metadata import io.novasama.substrate_sdk_android.runtime.AccountId +import io.novasama.substrate_sdk_android.runtime.definitions.types.generics.GenericEvent import io.novasama.substrate_sdk_android.runtime.definitions.types.primitives.BooleanType import io.novasama.substrate_sdk_android.runtime.extrinsic.ExtrinsicBuilder import io.novasama.substrate_sdk_android.runtime.metadata.RuntimeMetadata @@ -237,25 +240,38 @@ private class AssetConversionExchange( return swapHost.quote(quoteArgs) } + override suspend fun additionalMaxAmountDeduction(): Balance { + return Balance.ZERO + } + override suspend fun inProgressLabel(): String { return "Swapping ${fromAsset.symbol} to ${toAsset.symbol} on ${chain.name}" } override suspend fun submit(args: AtomicSwapOperationSubmissionArgs): Result { - return extrinsicService.submitAndWatchExtrinsic( + return extrinsicService.submitExtrinsicAndAwaitExecution( chain = chain, origin = TransactionOrigin.SelectedWallet, submissionOptions = ExtrinsicService.SubmissionOptions( feePaymentCurrency = transactionArgs.feePaymentCurrency ) ) { submissionOrigin -> - // Send swapped funds to the requested origin since it the account doing the swap + // Send swapped funds to the executingAccount since it the account doing the swap executeSwap(swapLimit = args.actualSwapLimit, sendTo = submissionOrigin.executingAccount) - }.awaitInBlock().map { - TODO() + }.requireOk().mapCatching { + SwapExecutionCorrection( + actualReceivedAmount = it.emittedEvents.determineActualSwappedAmount() + ) } } + private fun List.determineActualSwappedAmount() : Balance { + val swap = findEventOrThrow(Modules.ASSET_CONVERSION, "SwapExecuted") + val (_, _, _, amountOut) = swap.arguments + + return bindNumber(amountOut) + } + private suspend fun ExtrinsicBuilder.executeSwap( swapLimit: SwapLimit, sendTo: AccountId diff --git a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/crossChain/CrossChainTransferAssetExchange.kt b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/crossChain/CrossChainTransferAssetExchange.kt index 8750e4dbb4..d96089aa16 100644 --- a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/crossChain/CrossChainTransferAssetExchange.kt +++ b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/crossChain/CrossChainTransferAssetExchange.kt @@ -150,6 +150,11 @@ class CrossChainTransferAssetExchange( return extraOutAmount } + override suspend fun additionalMaxAmountDeduction(): Balance { + val (chain, chainAsset) = chainRegistry.chainWithAsset(edge.from) + return crossChainTransfersUseCase.requiredRemainingAmountAfterTransfer(chainAsset, chain) + } + override suspend fun inProgressLabel(): String { val chainTo = chainRegistry.getChain(edge.to.chainId) val assetFrom = chainRegistry.asset(edge.from) diff --git a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/hydraDx/HydraDxAssetExchange.kt b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/hydraDx/HydraDxAssetExchange.kt index 3630ca5b16..ca23a5e0cb 100644 --- a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/hydraDx/HydraDxAssetExchange.kt +++ b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/hydraDx/HydraDxAssetExchange.kt @@ -72,6 +72,7 @@ import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onEach +import java.math.BigInteger class HydraDxExchangeFactory( @@ -287,6 +288,10 @@ private class HydraDxAssetExchange( return swapHost.quote(quoteArgs) } + override suspend fun additionalMaxAmountDeduction(): Balance { + return BigInteger.ZERO + } + override suspend fun inProgressLabel(): String { val assetInId = segments.first().edge.from.assetId val assetIn = chain.assetsById.getValue(assetInId) diff --git a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/di/exchanges/HydraDxExchangeModule.kt b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/di/exchanges/HydraDxExchangeModule.kt index 778e00f0b0..8123eb1287 100644 --- a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/di/exchanges/HydraDxExchangeModule.kt +++ b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/di/exchanges/HydraDxExchangeModule.kt @@ -4,7 +4,6 @@ import dagger.Module import dagger.Provides import dagger.multibindings.IntoSet import io.novafoundation.nova.common.di.scope.FeatureScope -import io.novafoundation.nova.feature_account_api.data.extrinsic.ExtrinsicService import io.novafoundation.nova.feature_account_api.data.fee.types.hydra.HydrationFeeInjector import io.novafoundation.nova.feature_swap_core_api.data.network.HydraDxAssetIdConverter import io.novafoundation.nova.feature_swap_core_api.data.types.hydra.HydraDxQuoting @@ -15,7 +14,6 @@ import io.novafoundation.nova.feature_swap_impl.data.assetExchange.hydraDx.refer import io.novafoundation.nova.feature_swap_impl.data.assetExchange.hydraDx.referrals.RealHydraDxNovaReferral import io.novafoundation.nova.feature_swap_impl.data.assetExchange.hydraDx.stableswap.StableSwapSourceFactory import io.novafoundation.nova.feature_swap_impl.data.assetExchange.hydraDx.xyk.XYKSwapSourceFactory -import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.AssetSourceRegistry import io.novafoundation.nova.runtime.di.REMOTE_STORAGE_SOURCE import io.novafoundation.nova.runtime.ethereum.StorageSharedRequestsBuilderFactory import io.novafoundation.nova.runtime.storage.source.StorageDataSource @@ -58,19 +56,15 @@ class HydraDxExchangeModule { hydraDxAssetIdConverter: HydraDxAssetIdConverter, hydraDxNovaReferral: HydraDxNovaReferral, swapSourceFactories: Set<@JvmSuppressWildcards HydraDxSwapSource.Factory<*>>, - extrinsicServiceFactory: ExtrinsicService.Factory, quotingFactory: HydraDxQuoting.Factory, - assetSourceRegistry: AssetSourceRegistry, hydrationFeeInjector: HydrationFeeInjector ): HydraDxExchangeFactory { return HydraDxExchangeFactory( remoteStorageSource = remoteStorageSource, sharedRequestsBuilderFactory = sharedRequestsBuilderFactory, - extrinsicServiceFactory = extrinsicServiceFactory, hydraDxAssetIdConverter = hydraDxAssetIdConverter, hydraDxNovaReferral = hydraDxNovaReferral, swapSourceFactories = swapSourceFactories, - assetSourceRegistry = assetSourceRegistry, quotingFactory = quotingFactory, hydrationFeeInjector = hydrationFeeInjector ) diff --git a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/domain/swap/RealSwapService.kt b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/domain/swap/RealSwapService.kt index 0574c4a58b..20324d391b 100644 --- a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/domain/swap/RealSwapService.kt +++ b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/domain/swap/RealSwapService.kt @@ -168,7 +168,13 @@ internal class RealSwapService( val fees = atomicOperations.mapAsync { SwapFee.SwapSegment(it.estimateFee(), it) } val convertedFees = fees.convertIntermediateSegmentsFeesToAssetIn(executeArgs.assetIn) - return SwapFee(segments = fees, intermediateSegmentFeesInAssetIn = convertedFees).also(::logFee) + val firstOperation = atomicOperations.first() + + return SwapFee( + segments = fees, + intermediateSegmentFeesInAssetIn = convertedFees, + additionalMaxAmountDeduction = firstOperation.additionalMaxAmountDeduction() + ).also(::logFee) } override suspend fun swap(calculatedFee: SwapFee): Flow { 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 328dde235b..fbc96287d2 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 @@ -7,6 +7,7 @@ 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.types.Balance import io.novafoundation.nova.feature_wallet_api.domain.model.CrossChainTransferConfiguration +import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain interface CrossChainTransactor { @@ -23,6 +24,8 @@ interface CrossChainTransactor { crossChainFee: Balance ): Result + suspend fun requiredRemainingAmountAfterTransfer(sendingAsset: Chain.Asset, originChain: Chain): Balance + /** * @return result of actual received balance on destination */ diff --git a/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/domain/interfaces/CrossChainTransfersUseCase.kt b/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/domain/interfaces/CrossChainTransfersUseCase.kt index b65b963fed..8bc0f6ab1d 100644 --- a/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/domain/interfaces/CrossChainTransfersUseCase.kt +++ b/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/domain/interfaces/CrossChainTransfersUseCase.kt @@ -29,6 +29,11 @@ interface CrossChainTransfersUseCase { suspend fun getConfiguration(): CrossChainTransfersConfiguration + suspend fun requiredRemainingAmountAfterTransfer( + sendingAsset: Chain.Asset, + originChain: Chain + ): Balance + suspend fun ExtrinsicService.estimateFee( transfer: AssetTransferBase, computationalScope: CoroutineScope diff --git a/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/data/network/blockchain/assets/TypeBasedAssetSourceRegistry.kt b/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/data/network/blockchain/assets/TypeBasedAssetSourceRegistry.kt index b217e450e0..d42daa2d7d 100644 --- a/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/data/network/blockchain/assets/TypeBasedAssetSourceRegistry.kt +++ b/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/data/network/blockchain/assets/TypeBasedAssetSourceRegistry.kt @@ -9,6 +9,7 @@ import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.h import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.tranfers.AssetTransfers import io.novafoundation.nova.feature_wallet_impl.data.network.blockchain.assets.events.UnsupportedEventDetector import io.novafoundation.nova.feature_wallet_impl.data.network.blockchain.assets.events.orml.OrmlAssetEventDetectorFactory +import io.novafoundation.nova.feature_wallet_impl.data.network.blockchain.assets.events.statemine.StatemineAssetEventDetectorFactory import io.novafoundation.nova.feature_wallet_impl.data.network.blockchain.assets.events.utility.NativeAssetEventDetector import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain @@ -29,7 +30,8 @@ class TypeBasedAssetSourceRegistry( private val unsupportedBalanceSource: AssetSource, private val nativeAssetEventDetector: NativeAssetEventDetector, - private val ormlAssetEventDetectorFactory: OrmlAssetEventDetectorFactory + private val ormlAssetEventDetectorFactory: OrmlAssetEventDetectorFactory, + private val statemineAssetEventDetectorFactory: StatemineAssetEventDetectorFactory, ) : AssetSourceRegistry { override fun sourceFor(chainAsset: Chain.Asset): AssetSource { @@ -50,10 +52,10 @@ class TypeBasedAssetSourceRegistry( is Chain.Asset.Type.EvmErc20, Chain.Asset.Type.EvmNative, - // TODO implement statemine - is Chain.Asset.Type.Statemine, Chain.Asset.Type.Unsupported -> UnsupportedEventDetector() + is Chain.Asset.Type.Statemine -> statemineAssetEventDetectorFactory.create(chainAsset) + is Chain.Asset.Type.Orml -> ormlAssetEventDetectorFactory.create(chainAsset) Chain.Asset.Type.Native -> nativeAssetEventDetector diff --git a/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/data/network/blockchain/assets/events/statemine/StatemineAssetEventDetector.kt b/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/data/network/blockchain/assets/events/statemine/StatemineAssetEventDetector.kt new file mode 100644 index 0000000000..1b7e877ea0 --- /dev/null +++ b/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/data/network/blockchain/assets/events/statemine/StatemineAssetEventDetector.kt @@ -0,0 +1,74 @@ +package io.novafoundation.nova.feature_wallet_impl.data.network.blockchain.assets.events.statemine + +import io.novafoundation.nova.common.data.network.runtime.binding.bindAccountId +import io.novafoundation.nova.common.data.network.runtime.binding.bindNumber +import io.novafoundation.nova.common.utils.instanceOf +import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.events.AssetEventDetector +import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.events.model.DepositEvent +import io.novafoundation.nova.runtime.ext.palletNameOrDefault +import io.novafoundation.nova.runtime.ext.requireStatemine +import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry +import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain +import io.novafoundation.nova.runtime.multiNetwork.chain.model.StatemineAssetId +import io.novafoundation.nova.runtime.multiNetwork.getRuntime +import io.novasama.substrate_sdk_android.extensions.requireHexPrefix +import io.novasama.substrate_sdk_android.runtime.RuntimeSnapshot +import io.novasama.substrate_sdk_android.runtime.definitions.types.RuntimeType +import io.novasama.substrate_sdk_android.runtime.definitions.types.generics.GenericEvent +import io.novasama.substrate_sdk_android.runtime.definitions.types.toHexUntyped +import java.math.BigInteger + +class StatemineAssetEventDetectorFactory( + private val chainRegistry: ChainRegistry, +) { + + suspend fun create(chainAsset: Chain.Asset): AssetEventDetector { + val assetType = chainAsset.requireStatemine() + val runtime = chainRegistry.getRuntime(chainAsset.chainId) + + return StatemineAssetEventDetector(runtime, assetType) + } +} + + +class StatemineAssetEventDetector( + private val runtimeSnapshot: RuntimeSnapshot, + private val assetType: Chain.Asset.Type.Statemine, +) : AssetEventDetector { + + private val targetAssetId = assetType.id.stringAssetId() + + override fun detectDeposit(event: GenericEvent.Instance): DepositEvent? { + return detectTokensDeposited(event) + } + + private fun detectTokensDeposited(event: GenericEvent.Instance): DepositEvent? { + if (!event.instanceOf(assetType.palletNameOrDefault(), "Issued")) return null + + val (assetId, who, amount) = event.arguments + + val assetIdType = event.event.arguments.first()!! + val assetIdAsString = decodedAssetItToString(assetId, assetIdType) + if (assetIdAsString != targetAssetId) return null + + return DepositEvent( + destination = bindAccountId(who), + amount = bindNumber(amount) + ) + } + + private fun decodedAssetItToString(assetId: Any?, assetIdType: RuntimeType<*, *>): String { + return if (assetId is BigInteger) { + assetId.toString() + } else { + assetIdType.toHexUntyped(runtimeSnapshot, assetId).requireHexPrefix() + } + } + + private fun StatemineAssetId.stringAssetId(): String { + return when (this) { + is StatemineAssetId.Number -> value.toString() + is StatemineAssetId.ScaleEncoded -> scaleHex + } + } +} diff --git a/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/data/network/blockchain/assets/history/realtime/substrate/AssetConversionSwapExtractor.kt b/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/data/network/blockchain/assets/history/realtime/substrate/AssetConversionSwapExtractor.kt index f684f1ee96..bee0c836bf 100644 --- a/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/data/network/blockchain/assets/history/realtime/substrate/AssetConversionSwapExtractor.kt +++ b/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/data/network/blockchain/assets/history/realtime/substrate/AssetConversionSwapExtractor.kt @@ -14,6 +14,7 @@ import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain import io.novafoundation.nova.runtime.multiNetwork.multiLocation.bindMultiLocation import io.novafoundation.nova.runtime.multiNetwork.multiLocation.converter.MultiLocationConverter import io.novafoundation.nova.runtime.multiNetwork.multiLocation.converter.MultiLocationConverterFactory +import io.novafoundation.nova.runtime.multiNetwork.runtime.repository.assetTxFeePaidEvent import io.novafoundation.nova.runtime.multiNetwork.runtime.repository.findAllOfType import io.novafoundation.nova.runtime.multiNetwork.runtime.repository.requireNativeFee import io.novasama.substrate_sdk_android.runtime.definitions.types.generics.GenericCall @@ -85,7 +86,7 @@ class AssetConversionSwapExtractor( return when { // successful swap, extract from event swapExecutedEvent != null -> { - val (_, _, _, amountIn, amountOut) = swapExecutedEvent.arguments + val (_, _, amountIn, amountOut) = swapExecutedEvent.arguments bindNumber(amountIn) to bindNumber(amountOut) } diff --git a/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/data/network/blockchain/assets/history/realtime/substrate/Common.kt b/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/data/network/blockchain/assets/history/realtime/substrate/Common.kt index 7c28a7b8f1..7e201491b0 100644 --- a/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/data/network/blockchain/assets/history/realtime/substrate/Common.kt +++ b/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/data/network/blockchain/assets/history/realtime/substrate/Common.kt @@ -1,11 +1,10 @@ package io.novafoundation.nova.feature_wallet_impl.data.network.blockchain.assets.history.realtime.substrate import io.novafoundation.nova.common.data.network.runtime.binding.bindNumber -import io.novafoundation.nova.common.utils.Modules import io.novafoundation.nova.feature_wallet_api.domain.model.ChainAssetWithAmount import io.novafoundation.nova.runtime.multiNetwork.multiLocation.bindMultiLocation import io.novafoundation.nova.runtime.multiNetwork.multiLocation.converter.MultiLocationConverter -import io.novafoundation.nova.runtime.multiNetwork.runtime.repository.findEvent +import io.novafoundation.nova.runtime.multiNetwork.runtime.repository.assetTxFeePaidEvent import io.novasama.substrate_sdk_android.runtime.definitions.types.generics.GenericEvent suspend fun List.assetFee(multiLocationConverter: MultiLocationConverter): ChainAssetWithAmount? { @@ -16,7 +15,3 @@ suspend fun List.assetFee(multiLocationConverter: MultiLo return ChainAssetWithAmount(chainAsset, totalFee) } - -fun List.assetTxFeePaidEvent(): GenericEvent.Instance? { - return findEvent(Modules.ASSET_TX_PAYMENT, "AssetTxFeePaid") -} 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 7f4037344e..6fcb1a9f00 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 @@ -41,6 +41,7 @@ import io.novafoundation.nova.feature_wallet_impl.data.network.blockchain.assets import io.novafoundation.nova.feature_wallet_impl.data.network.crosschain.validations.canPayCrossChainFee import io.novafoundation.nova.feature_wallet_impl.data.network.crosschain.validations.cannotDropBelowEdBeforePayingDeliveryFee import io.novafoundation.nova.runtime.ext.accountIdOrDefault +import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain import io.novafoundation.nova.runtime.multiNetwork.multiLocation.MultiLocation import io.novafoundation.nova.runtime.multiNetwork.runtime.repository.EventsRepository import io.novafoundation.nova.runtime.multiNetwork.runtime.repository.getInherentEvents @@ -120,6 +121,10 @@ class RealCrossChainTransactor( } } + override suspend fun requiredRemainingAmountAfterTransfer(sendingAsset: Chain.Asset, originChain: Chain): Balance { + return assetSourceRegistry.sourceFor(sendingAsset).balance.existentialDeposit(originChain, sendingAsset) + } + context(ExtrinsicService) override suspend fun performAndTrackTransfer( configuration: CrossChainTransferConfiguration, diff --git a/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/di/modules/AssetsModule.kt b/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/di/modules/AssetsModule.kt index e6b894d5d1..e779f0afe9 100644 --- a/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/di/modules/AssetsModule.kt +++ b/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/di/modules/AssetsModule.kt @@ -8,6 +8,7 @@ import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.A import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.AssetSourceRegistry import io.novafoundation.nova.feature_wallet_impl.data.network.blockchain.assets.TypeBasedAssetSourceRegistry import io.novafoundation.nova.feature_wallet_impl.data.network.blockchain.assets.events.orml.OrmlAssetEventDetectorFactory +import io.novafoundation.nova.feature_wallet_impl.data.network.blockchain.assets.events.statemine.StatemineAssetEventDetectorFactory import io.novafoundation.nova.feature_wallet_impl.data.network.blockchain.assets.events.utility.NativeAssetEventDetector @Module( @@ -36,6 +37,7 @@ class AssetsModule { nativeAssetEventDetector: NativeAssetEventDetector, ormlAssetEventDetectorFactory: OrmlAssetEventDetectorFactory, + statemineAssetEventDetectorFactory: StatemineAssetEventDetectorFactory, ): AssetSourceRegistry = TypeBasedAssetSourceRegistry( nativeSource = native, statemineSource = statemine, @@ -46,6 +48,7 @@ class AssetsModule { unsupportedBalanceSource = unsupported, nativeAssetEventDetector = nativeAssetEventDetector, - ormlAssetEventDetectorFactory = ormlAssetEventDetectorFactory + ormlAssetEventDetectorFactory = ormlAssetEventDetectorFactory, + statemineAssetEventDetectorFactory = statemineAssetEventDetectorFactory ) } diff --git a/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/di/modules/StatemineAssetsModule.kt b/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/di/modules/StatemineAssetsModule.kt index ff34fb0597..42f5f3625c 100644 --- a/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/di/modules/StatemineAssetsModule.kt +++ b/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/di/modules/StatemineAssetsModule.kt @@ -14,6 +14,7 @@ import io.novafoundation.nova.feature_wallet_api.domain.validation.EnoughTotalTo import io.novafoundation.nova.feature_wallet_api.domain.validation.PhishingValidationFactory import io.novafoundation.nova.feature_wallet_impl.data.network.blockchain.assets.StaticAssetSource import io.novafoundation.nova.feature_wallet_impl.data.network.blockchain.assets.balances.statemine.StatemineAssetBalance +import io.novafoundation.nova.feature_wallet_impl.data.network.blockchain.assets.events.statemine.StatemineAssetEventDetectorFactory import io.novafoundation.nova.feature_wallet_impl.data.network.blockchain.assets.history.statemine.StatemineAssetHistory import io.novafoundation.nova.feature_wallet_impl.data.network.blockchain.assets.transfers.statemine.StatemineAssetTransfers import io.novafoundation.nova.feature_wallet_impl.data.network.subquery.SubQueryOperationsApi @@ -93,4 +94,8 @@ class StatemineAssetsModule { balance = statemineAssetBalance, history = statemineAssetHistory ) + + @Provides + @FeatureScope + fun provideStatemineAssetEventDetectorFactory(chainRegistry: ChainRegistry) = StatemineAssetEventDetectorFactory(chainRegistry) } diff --git a/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/domain/RealCrossChainTransfersUseCase.kt b/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/domain/RealCrossChainTransfersUseCase.kt index 660512b144..7409010a37 100644 --- a/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/domain/RealCrossChainTransfersUseCase.kt +++ b/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/domain/RealCrossChainTransfersUseCase.kt @@ -103,6 +103,10 @@ internal class RealCrossChainTransfersUseCase( return crossChainTransfersRepository.getConfiguration() } + override suspend fun requiredRemainingAmountAfterTransfer(sendingAsset: Chain.Asset, originChain: Chain): Balance { + return crossChainTransactor.requiredRemainingAmountAfterTransfer(sendingAsset, originChain) + } + override suspend fun ExtrinsicService.estimateFee( transfer: AssetTransferBase, diff --git a/runtime/src/main/java/io/novafoundation/nova/runtime/multiNetwork/runtime/repository/ExtrinsicWithEvents.kt b/runtime/src/main/java/io/novafoundation/nova/runtime/multiNetwork/runtime/repository/ExtrinsicWithEvents.kt index a5ffbf95b0..fbc643728b 100644 --- a/runtime/src/main/java/io/novafoundation/nova/runtime/multiNetwork/runtime/repository/ExtrinsicWithEvents.kt +++ b/runtime/src/main/java/io/novafoundation/nova/runtime/multiNetwork/runtime/repository/ExtrinsicWithEvents.kt @@ -55,6 +55,10 @@ fun List.nativeFee(): BigInteger? { return bindNumber(actualFee) + bindNumber(tip) } +fun List.assetTxFeePaidEvent(): GenericEvent.Instance? { + return findEvent(Modules.ASSET_TX_PAYMENT, "AssetTxFeePaid") +} + fun List.requireNativeFee(): BigInteger { return requireNotNull(nativeFee()) { "No native fee event found" From 7a32fec2cdb9ffaddbb88628b160547bff29a73d Mon Sep 17 00:00:00 2001 From: Valentun Date: Wed, 23 Oct 2024 16:51:26 +0300 Subject: [PATCH 30/83] Roughly estimate fees for quotes for choosing best candidate --- .../domain/model/AmountWithAssetId.kt | 9 + .../model/AtomicSwapOperationPrototype.kt | 20 +++ .../domain/model/SwapGraph.kt | 17 ++ .../data/paths/PathFeeEstimator.kt | 11 ++ .../data/paths/PathQuoter.kt | 1 + .../paths/model/PathRoughFeeEstimation.kt | 14 ++ .../data/paths/model/QuotedPath.kt | 10 +- .../domain/paths/RealPathQuoter.kt | 14 +- .../AssetConversionExchange.kt | 32 +++- .../CrossChainTransferAssetExchange.kt | 42 +++++ .../hydraDx/HydraDxAssetExchange.kt | 24 +++ .../feature_swap_impl/di/SwapFeatureModule.kt | 7 +- .../domain/swap/RealSwapService.kt | 158 +++++++++++++++++- .../assets/tranfers/AssetTransfers.kt | 15 +- .../domain/interfaces/TokenRepository.kt | 2 + .../domain/model/CoinRate.kt | 11 ++ .../feature_wallet_api/domain/model/Token.kt | 3 + .../data/repository/TokenRepositoryImpl.kt | 23 +++ .../nova/runtime/ext/ChainExt.kt | 2 + .../runtime/multiNetwork/chain/model/Chain.kt | 5 +- 20 files changed, 398 insertions(+), 22 deletions(-) create mode 100644 feature-swap-api/src/main/java/io/novafoundation/nova/feature_swap_api/domain/model/AmountWithAssetId.kt create mode 100644 feature-swap-api/src/main/java/io/novafoundation/nova/feature_swap_api/domain/model/AtomicSwapOperationPrototype.kt create mode 100644 feature-swap-core/api/src/main/java/io/novafoundation/nova/feature_swap_core_api/data/paths/PathFeeEstimator.kt create mode 100644 feature-swap-core/api/src/main/java/io/novafoundation/nova/feature_swap_core_api/data/paths/model/PathRoughFeeEstimation.kt diff --git a/feature-swap-api/src/main/java/io/novafoundation/nova/feature_swap_api/domain/model/AmountWithAssetId.kt b/feature-swap-api/src/main/java/io/novafoundation/nova/feature_swap_api/domain/model/AmountWithAssetId.kt new file mode 100644 index 0000000000..3ddc487b0e --- /dev/null +++ b/feature-swap-api/src/main/java/io/novafoundation/nova/feature_swap_api/domain/model/AmountWithAssetId.kt @@ -0,0 +1,9 @@ +package io.novafoundation.nova.feature_swap_api.domain.model + +import io.novafoundation.nova.runtime.multiNetwork.chain.model.FullChainAssetId +import java.math.BigDecimal + +class AmountWithAssetId( + val assetId: FullChainAssetId, + val amount: BigDecimal +) diff --git a/feature-swap-api/src/main/java/io/novafoundation/nova/feature_swap_api/domain/model/AtomicSwapOperationPrototype.kt b/feature-swap-api/src/main/java/io/novafoundation/nova/feature_swap_api/domain/model/AtomicSwapOperationPrototype.kt new file mode 100644 index 0000000000..f3444f41f5 --- /dev/null +++ b/feature-swap-api/src/main/java/io/novafoundation/nova/feature_swap_api/domain/model/AtomicSwapOperationPrototype.kt @@ -0,0 +1,20 @@ +package io.novafoundation.nova.feature_swap_api.domain.model + +import io.novafoundation.nova.runtime.multiNetwork.chain.model.ChainId +import java.math.BigDecimal + +interface AtomicSwapOperationPrototype { + + val fromChain: ChainId + + /** + * Roughly estimate fees for the current operation in any asset + * Implementations should favour speed instead of precision as this is called for each quoting action + */ + suspend fun roughlyEstimateNativeFee(usdConverter: UsdConverter): BigDecimal +} + +interface UsdConverter { + + suspend fun nativeAssetEquivalentOf(usdAmount: Double): BigDecimal +} diff --git a/feature-swap-api/src/main/java/io/novafoundation/nova/feature_swap_api/domain/model/SwapGraph.kt b/feature-swap-api/src/main/java/io/novafoundation/nova/feature_swap_api/domain/model/SwapGraph.kt index 7493c7df71..8ac1ff1a1a 100644 --- a/feature-swap-api/src/main/java/io/novafoundation/nova/feature_swap_api/domain/model/SwapGraph.kt +++ b/feature-swap-api/src/main/java/io/novafoundation/nova/feature_swap_api/domain/model/SwapGraph.kt @@ -6,6 +6,9 @@ import io.novafoundation.nova.runtime.multiNetwork.chain.model.FullChainAssetId interface SwapGraphEdge : QuotableEdge { + /** + * Begin a fully-constructed, ready to submit operation + */ suspend fun beginOperation(args: AtomicSwapOperationArgs): AtomicSwapOperation /** @@ -15,6 +18,20 @@ interface SwapGraphEdge : QuotableEdge { */ suspend fun appendToOperation(currentTransaction: AtomicSwapOperation, args: AtomicSwapOperationArgs): AtomicSwapOperation? + /** + * Begin a operation prototype that should reflect similar structure to [beginOperation] and [appendToOperation] but is limited to available functionality + * Used during quoting to construct the operations array when not all parameters are still known + */ + suspend fun beginOperationPrototype(): AtomicSwapOperationPrototype + + /** + * Append current swap edge execution to the existing transaction prototype + * Return null if it is not possible, indicating that the new transaction should be initiated to handle this edge via + * [beginOperationPrototype] + */ + suspend fun appendToOperationPrototype(currentTransaction: AtomicSwapOperationPrototype): AtomicSwapOperationPrototype? + + suspend fun debugLabel(): String /** diff --git a/feature-swap-core/api/src/main/java/io/novafoundation/nova/feature_swap_core_api/data/paths/PathFeeEstimator.kt b/feature-swap-core/api/src/main/java/io/novafoundation/nova/feature_swap_core_api/data/paths/PathFeeEstimator.kt new file mode 100644 index 0000000000..2ef2f1d02b --- /dev/null +++ b/feature-swap-core/api/src/main/java/io/novafoundation/nova/feature_swap_core_api/data/paths/PathFeeEstimator.kt @@ -0,0 +1,11 @@ +package io.novafoundation.nova.feature_swap_core_api.data.paths + +import io.novafoundation.nova.common.utils.graph.Path +import io.novafoundation.nova.feature_swap_core_api.data.paths.model.PathRoughFeeEstimation +import io.novafoundation.nova.feature_swap_core_api.data.paths.model.QuotedEdge + +interface PathFeeEstimator { + + suspend fun roughlyEstimateFee(path: Path>): PathRoughFeeEstimation +} + diff --git a/feature-swap-core/api/src/main/java/io/novafoundation/nova/feature_swap_core_api/data/paths/PathQuoter.kt b/feature-swap-core/api/src/main/java/io/novafoundation/nova/feature_swap_core_api/data/paths/PathQuoter.kt index 78bb2b8308..9780e1fa57 100644 --- a/feature-swap-core/api/src/main/java/io/novafoundation/nova/feature_swap_core_api/data/paths/PathQuoter.kt +++ b/feature-swap-core/api/src/main/java/io/novafoundation/nova/feature_swap_core_api/data/paths/PathQuoter.kt @@ -17,6 +17,7 @@ interface PathQuoter { fun create( graph: Graph, computationalScope: CoroutineScope, + pathFeeEstimation: PathFeeEstimator? = null, filter: EdgeVisitFilter? = null ): PathQuoter } diff --git a/feature-swap-core/api/src/main/java/io/novafoundation/nova/feature_swap_core_api/data/paths/model/PathRoughFeeEstimation.kt b/feature-swap-core/api/src/main/java/io/novafoundation/nova/feature_swap_core_api/data/paths/model/PathRoughFeeEstimation.kt new file mode 100644 index 0000000000..b0881bdf1e --- /dev/null +++ b/feature-swap-core/api/src/main/java/io/novafoundation/nova/feature_swap_core_api/data/paths/model/PathRoughFeeEstimation.kt @@ -0,0 +1,14 @@ +package io.novafoundation.nova.feature_swap_core_api.data.paths.model + +import io.novafoundation.nova.common.data.network.runtime.binding.BalanceOf +import java.math.BigInteger + +class PathRoughFeeEstimation(val inAssetOut: BalanceOf, val inAssetIn: BalanceOf) { + + companion object { + + fun zero(): PathRoughFeeEstimation { + return PathRoughFeeEstimation(BigInteger.ZERO, BigInteger.ZERO) + } + } +} diff --git a/feature-swap-core/api/src/main/java/io/novafoundation/nova/feature_swap_core_api/data/paths/model/QuotedPath.kt b/feature-swap-core/api/src/main/java/io/novafoundation/nova/feature_swap_core_api/data/paths/model/QuotedPath.kt index 6f4d41c4ee..85b0bf6575 100644 --- a/feature-swap-core/api/src/main/java/io/novafoundation/nova/feature_swap_core_api/data/paths/model/QuotedPath.kt +++ b/feature-swap-core/api/src/main/java/io/novafoundation/nova/feature_swap_core_api/data/paths/model/QuotedPath.kt @@ -6,15 +6,19 @@ import java.math.BigInteger class QuotedPath( val direction: SwapDirection, - val path: Path> + val path: Path>, + val roughFeeEstimation: PathRoughFeeEstimation, ) : Comparable> { + private val amountOutAfterFees: BigInteger = lastSegmentQuote - roughFeeEstimation.inAssetOut + private val amountInAfterFees: BigInteger = firstSegmentQuote + roughFeeEstimation.inAssetIn + override fun compareTo(other: QuotedPath): Int { return when (direction) { // When we want to sell a token, the bigger the quote - the better - SwapDirection.SPECIFIED_IN -> (lastSegmentQuote - other.lastSegmentQuote).signum() + SwapDirection.SPECIFIED_IN -> (amountOutAfterFees - other.amountOutAfterFees).signum() // When we want to buy a token, the smaller the quote - the better - SwapDirection.SPECIFIED_OUT -> (other.firstSegmentQuote - firstSegmentQuote).signum() + SwapDirection.SPECIFIED_OUT -> (other.amountInAfterFees - amountInAfterFees).signum() } } } diff --git a/feature-swap-core/src/main/java/io/novafoundation/nova/feature_swap_core/domain/paths/RealPathQuoter.kt b/feature-swap-core/src/main/java/io/novafoundation/nova/feature_swap_core/domain/paths/RealPathQuoter.kt index 3cb45bb45d..eedbe1988b 100644 --- a/feature-swap-core/src/main/java/io/novafoundation/nova/feature_swap_core/domain/paths/RealPathQuoter.kt +++ b/feature-swap-core/src/main/java/io/novafoundation/nova/feature_swap_core/domain/paths/RealPathQuoter.kt @@ -8,8 +8,10 @@ import io.novafoundation.nova.common.utils.graph.Path import io.novafoundation.nova.common.utils.graph.findDijkstraPathsBetween import io.novafoundation.nova.common.utils.mapAsync import io.novafoundation.nova.common.utils.measureExecution +import io.novafoundation.nova.feature_swap_core_api.data.paths.PathFeeEstimator import io.novafoundation.nova.feature_swap_core_api.data.paths.PathQuoter import io.novafoundation.nova.feature_swap_core_api.data.paths.model.BestPathQuote +import io.novafoundation.nova.feature_swap_core_api.data.paths.model.PathRoughFeeEstimation import io.novafoundation.nova.feature_swap_core_api.data.paths.model.QuotedEdge import io.novafoundation.nova.feature_swap_core_api.data.paths.model.QuotedPath import io.novafoundation.nova.feature_swap_core_api.data.primitive.errors.SwapQuoteException @@ -31,9 +33,10 @@ class RealPathQuoterFactory( override fun create( graph: Graph, computationalScope: CoroutineScope, + pathFeeEstimation: PathFeeEstimator?, filter: EdgeVisitFilter? ): PathQuoter { - return RealPathQuoter(computationalCache, graph, computationalScope, filter) + return RealPathQuoter(computationalCache, graph, computationalScope, pathFeeEstimation, filter) } } @@ -41,6 +44,7 @@ private class RealPathQuoter( private val computationalCache: ComputationalCache, private val graph: Graph, private val computationalScope: CoroutineScope, + private val pathFeeEstimation: PathFeeEstimator?, private val filter: EdgeVisitFilter?, ) : PathQuoter { @@ -101,7 +105,13 @@ private class RealPathQuoter( SwapDirection.SPECIFIED_OUT -> quotePathBuy(path, amount) } ?: return null - return QuotedPath(swapDirection, quote) + val pathRoughFeeEstimation = pathFeeEstimation.roughlyEstimateFeeOrZero(quote) + + return QuotedPath(swapDirection, quote, pathRoughFeeEstimation) + } + + private suspend fun PathFeeEstimator?.roughlyEstimateFeeOrZero(quote: Path>): PathRoughFeeEstimation { + return this?.roughlyEstimateFee(quote) ?: PathRoughFeeEstimation.zero() } private suspend fun quotePathBuy(path: Path, amount: BigInteger): Path>? { diff --git a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/assetConversion/AssetConversionExchange.kt b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/assetConversion/AssetConversionExchange.kt index 67c34a1783..aa240bdb76 100644 --- a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/assetConversion/AssetConversionExchange.kt +++ b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/assetConversion/AssetConversionExchange.kt @@ -14,12 +14,14 @@ import io.novafoundation.nova.feature_account_api.domain.model.MetaAccount import io.novafoundation.nova.feature_swap_api.domain.model.AtomicSwapOperation import io.novafoundation.nova.feature_swap_api.domain.model.AtomicSwapOperationArgs import io.novafoundation.nova.feature_swap_api.domain.model.AtomicSwapOperationFee +import io.novafoundation.nova.feature_swap_api.domain.model.AtomicSwapOperationPrototype import io.novafoundation.nova.feature_swap_api.domain.model.AtomicSwapOperationSubmissionArgs import io.novafoundation.nova.feature_swap_api.domain.model.ReQuoteTrigger import io.novafoundation.nova.feature_swap_api.domain.model.SubmissionFeeWithLabel import io.novafoundation.nova.feature_swap_api.domain.model.SwapExecutionCorrection import io.novafoundation.nova.feature_swap_api.domain.model.SwapGraphEdge import io.novafoundation.nova.feature_swap_api.domain.model.SwapLimit +import io.novafoundation.nova.feature_swap_api.domain.model.UsdConverter import io.novafoundation.nova.feature_swap_core.data.assetExchange.conversion.types.hydra.sources.Weights import io.novafoundation.nova.feature_swap_core_api.data.primitive.errors.SwapQuoteException import io.novafoundation.nova.feature_swap_core_api.data.primitive.model.SwapDirection @@ -32,6 +34,7 @@ import io.novafoundation.nova.runtime.call.MultiChainRuntimeCallsApi import io.novafoundation.nova.runtime.call.RuntimeCallsApi import io.novafoundation.nova.runtime.ext.emptyAccountId import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain +import io.novafoundation.nova.runtime.multiNetwork.chain.model.ChainId import io.novafoundation.nova.runtime.multiNetwork.multiLocation.MultiLocation import io.novafoundation.nova.runtime.multiNetwork.multiLocation.converter.MultiLocationConverter import io.novafoundation.nova.runtime.multiNetwork.multiLocation.converter.MultiLocationConverterFactory @@ -51,6 +54,7 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.drop import kotlinx.coroutines.flow.map +import java.math.BigDecimal class AssetConversionExchangeFactory( private val multiLocationConverterFactory: MultiLocationConverterFactory, @@ -177,6 +181,14 @@ private class AssetConversionExchange( return null } + override suspend fun beginOperationPrototype(): AtomicSwapOperationPrototype { + return AssetConversionOperationPrototype(fromAsset.chainId) + } + + override suspend fun appendToOperationPrototype(currentTransaction: AtomicSwapOperationPrototype): AtomicSwapOperationPrototype? { + return null + } + override suspend fun debugLabel(): String { return "AssetConversion" } @@ -207,6 +219,14 @@ private class AssetConversionExchange( get() = Weights.AssetConversion.SWAP } + inner class AssetConversionOperationPrototype(override val fromChain: ChainId) : AtomicSwapOperationPrototype { + + override suspend fun roughlyEstimateNativeFee(usdConverter: UsdConverter): BigDecimal { + // in DOT + return 0.0015.toBigDecimal() + } + } + inner class AssetConversionOperation( private val transactionArgs: AtomicSwapOperationArgs, private val fromAsset: Chain.Asset, @@ -231,7 +251,7 @@ private class AssetConversionExchange( override suspend fun requiredAmountInToGetAmountOut(extraOutAmount: Balance): Balance { val quoteArgs = ParentQuoterArgs( - chainAssetIn =fromAsset, + chainAssetIn = fromAsset, chainAssetOut = toAsset, amount = extraOutAmount, swapDirection = SwapDirection.SPECIFIED_OUT @@ -245,7 +265,7 @@ private class AssetConversionExchange( } override suspend fun inProgressLabel(): String { - return "Swapping ${fromAsset.symbol} to ${toAsset.symbol} on ${chain.name}" + return "Swapping ${fromAsset.symbol} to ${toAsset.symbol} on ${chain.name}" } override suspend fun submit(args: AtomicSwapOperationSubmissionArgs): Result { @@ -259,13 +279,13 @@ private class AssetConversionExchange( // Send swapped funds to the executingAccount since it the account doing the swap executeSwap(swapLimit = args.actualSwapLimit, sendTo = submissionOrigin.executingAccount) }.requireOk().mapCatching { - SwapExecutionCorrection( - actualReceivedAmount = it.emittedEvents.determineActualSwappedAmount() - ) + SwapExecutionCorrection( + actualReceivedAmount = it.emittedEvents.determineActualSwappedAmount() + ) } } - private fun List.determineActualSwappedAmount() : Balance { + private fun List.determineActualSwappedAmount(): Balance { val swap = findEventOrThrow(Modules.ASSET_CONVERSION, "SwapExecuted") val (_, _, _, amountOut) = swap.arguments diff --git a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/crossChain/CrossChainTransferAssetExchange.kt b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/crossChain/CrossChainTransferAssetExchange.kt index d96089aa16..dd4e69bfca 100644 --- a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/crossChain/CrossChainTransferAssetExchange.kt +++ b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/crossChain/CrossChainTransferAssetExchange.kt @@ -8,6 +8,7 @@ import io.novafoundation.nova.feature_account_api.domain.model.requireAddressIn import io.novafoundation.nova.feature_swap_api.domain.model.AtomicSwapOperation import io.novafoundation.nova.feature_swap_api.domain.model.AtomicSwapOperationArgs import io.novafoundation.nova.feature_swap_api.domain.model.AtomicSwapOperationFee +import io.novafoundation.nova.feature_swap_api.domain.model.AtomicSwapOperationPrototype import io.novafoundation.nova.feature_swap_api.domain.model.AtomicSwapOperationSubmissionArgs import io.novafoundation.nova.feature_swap_api.domain.model.FeeWithLabel import io.novafoundation.nova.feature_swap_api.domain.model.ReQuoteTrigger @@ -15,6 +16,7 @@ import io.novafoundation.nova.feature_swap_api.domain.model.SubmissionFeeWithLab import io.novafoundation.nova.feature_swap_api.domain.model.SwapExecutionCorrection import io.novafoundation.nova.feature_swap_api.domain.model.SwapGraphEdge import io.novafoundation.nova.feature_swap_api.domain.model.SwapLimit +import io.novafoundation.nova.feature_swap_api.domain.model.UsdConverter import io.novafoundation.nova.feature_swap_core.data.assetExchange.conversion.types.hydra.sources.Weights import io.novafoundation.nova.feature_swap_core_api.data.primitive.model.SwapDirection import io.novafoundation.nova.feature_swap_impl.data.assetExchange.AssetExchange @@ -24,14 +26,18 @@ import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.types.Ba import io.novafoundation.nova.feature_wallet_api.domain.implementations.availableInDestinations import io.novafoundation.nova.feature_wallet_api.domain.interfaces.CrossChainTransfersUseCase import io.novafoundation.nova.feature_wallet_api.domain.model.CrossChainTransfersConfiguration +import io.novafoundation.nova.runtime.ext.Geneses import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry import io.novafoundation.nova.runtime.multiNetwork.asset +import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain +import io.novafoundation.nova.runtime.multiNetwork.chain.model.ChainId import io.novafoundation.nova.runtime.multiNetwork.chain.model.FullChainAssetId import io.novafoundation.nova.runtime.multiNetwork.chainWithAsset import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.emptyFlow +import java.math.BigDecimal import java.math.BigInteger class CrossChainTransferAssetExchangeFactory( @@ -99,6 +105,14 @@ class CrossChainTransferAssetExchange( return null } + override suspend fun beginOperationPrototype(): AtomicSwapOperationPrototype { + return CrossChainTransferOperationPrototype(from.chainId, to.chainId) + } + + override suspend fun appendToOperationPrototype(currentTransaction: AtomicSwapOperationPrototype): AtomicSwapOperationPrototype? { + return null + } + override suspend fun debugLabel(): String { return "To ${chainRegistry.getChain(delegate.to.chainId).name}" } @@ -119,6 +133,34 @@ class CrossChainTransferAssetExchange( } } + inner class CrossChainTransferOperationPrototype( + override val fromChain: ChainId, + private val toChain: ChainId, + ): AtomicSwapOperationPrototype { + + override suspend fun roughlyEstimateNativeFee(usdConverter: UsdConverter): BigDecimal { + var totalAmount = usdConverter.nativeAssetEquivalentOf(0.15) + + if (isChainWithExpensiveCrossChain(fromChain)) { + totalAmount += usdConverter.nativeAssetEquivalentOf(0.15) + } + + if (isChainWithExpensiveCrossChain(toChain)) { + totalAmount += usdConverter.nativeAssetEquivalentOf(0.1) + } + + if (!(isChainWithExpensiveCrossChain(fromChain) || isChainWithExpensiveCrossChain(toChain))) { + totalAmount += usdConverter.nativeAssetEquivalentOf(0.01) + } + + return totalAmount + } + + private fun isChainWithExpensiveCrossChain(chainId: ChainId): Boolean { + return (chainId == Chain.Geneses.POLKADOT) or (chainId == Chain.Geneses.POLKADOT_ASSET_HUB) + } + } + inner class CrossChainTransferOperation( private val transactionArgs: AtomicSwapOperationArgs, private val edge: Edge, diff --git a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/hydraDx/HydraDxAssetExchange.kt b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/hydraDx/HydraDxAssetExchange.kt index ca23a5e0cb..307e2b805f 100644 --- a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/hydraDx/HydraDxAssetExchange.kt +++ b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/hydraDx/HydraDxAssetExchange.kt @@ -27,12 +27,14 @@ import io.novafoundation.nova.feature_account_api.domain.model.requireAccountIdI import io.novafoundation.nova.feature_swap_api.domain.model.AtomicSwapOperation import io.novafoundation.nova.feature_swap_api.domain.model.AtomicSwapOperationArgs import io.novafoundation.nova.feature_swap_api.domain.model.AtomicSwapOperationFee +import io.novafoundation.nova.feature_swap_api.domain.model.AtomicSwapOperationPrototype import io.novafoundation.nova.feature_swap_api.domain.model.AtomicSwapOperationSubmissionArgs import io.novafoundation.nova.feature_swap_api.domain.model.ReQuoteTrigger import io.novafoundation.nova.feature_swap_api.domain.model.SubmissionFeeWithLabel import io.novafoundation.nova.feature_swap_api.domain.model.SwapExecutionCorrection import io.novafoundation.nova.feature_swap_api.domain.model.SwapGraphEdge import io.novafoundation.nova.feature_swap_api.domain.model.SwapLimit +import io.novafoundation.nova.feature_swap_api.domain.model.UsdConverter import io.novafoundation.nova.feature_swap_core.data.assetExchange.conversion.types.hydra.acceptedCurrencies import io.novafoundation.nova.feature_swap_core.data.assetExchange.conversion.types.hydra.accountCurrencyMap import io.novafoundation.nova.feature_swap_core.data.assetExchange.conversion.types.hydra.multiTransactionPayment @@ -55,6 +57,7 @@ import io.novafoundation.nova.runtime.ethereum.StorageSharedRequestsBuilderFacto import io.novafoundation.nova.runtime.ext.utilityAsset import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain import io.novafoundation.nova.runtime.multiNetwork.chain.model.ChainAssetId +import io.novafoundation.nova.runtime.multiNetwork.chain.model.ChainId import io.novafoundation.nova.runtime.multiNetwork.chain.model.FullChainAssetId import io.novafoundation.nova.runtime.multiNetwork.runtime.repository.findEvent import io.novafoundation.nova.runtime.multiNetwork.runtime.repository.findEventOrThrow @@ -72,6 +75,7 @@ import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onEach +import java.math.BigDecimal import java.math.BigInteger @@ -225,6 +229,18 @@ private class HydraDxAssetExchange( return currentTransaction.appendSegment(sourceQuotableEdge, args) } + override suspend fun beginOperationPrototype(): AtomicSwapOperationPrototype { + return HydraDxOperationPrototype(from.chainId) + } + + override suspend fun appendToOperationPrototype(currentTransaction: AtomicSwapOperationPrototype): AtomicSwapOperationPrototype? { + return if (currentTransaction is HydraDxOperationPrototype) { + currentTransaction + } else { + null + } + } + override suspend fun debugLabel(): String { return sourceQuotableEdge.debugLabel() } @@ -239,6 +255,14 @@ private class HydraDxAssetExchange( } } + inner class HydraDxOperationPrototype(override val fromChain: ChainId) : AtomicSwapOperationPrototype { + + override suspend fun roughlyEstimateNativeFee(usdConverter: UsdConverter): BigDecimal { + // in HDX + return 0.5.toBigDecimal() + } + } + inner class HydraDxOperation private constructor( val segments: List, val feePaymentCurrency: FeePaymentCurrency, diff --git a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/di/SwapFeatureModule.kt b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/di/SwapFeatureModule.kt index 4593c7a030..b260a0dae9 100644 --- a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/di/SwapFeatureModule.kt +++ b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/di/SwapFeatureModule.kt @@ -39,6 +39,7 @@ import io.novafoundation.nova.feature_swap_impl.presentation.mixin.maxAction.Max import io.novafoundation.nova.feature_swap_impl.presentation.state.RealSwapSettingsStateProvider import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.AssetSourceRegistry import io.novafoundation.nova.feature_wallet_api.domain.interfaces.CrossChainTransfersUseCase +import io.novafoundation.nova.feature_wallet_api.domain.interfaces.TokenRepository import io.novafoundation.nova.feature_wallet_api.domain.interfaces.WalletRepository import io.novafoundation.nova.feature_wallet_api.domain.updater.AccountInfoUpdaterFactory import io.novafoundation.nova.runtime.ethereum.StorageSharedRequestsBuilderFactory @@ -58,7 +59,8 @@ class SwapFeatureModule { quoterFactory: PathQuoter.Factory, customFeeCapabilityFacade: CustomFeeCapabilityFacade, extrinsicServiceFactory: ExtrinsicService.Factory, - defaultFeePaymentRegistry: FeePaymentProviderRegistry + defaultFeePaymentRegistry: FeePaymentProviderRegistry, + tokenRepository: TokenRepository, ): SwapService { return RealSwapService( assetConversionFactory = assetConversionFactory, @@ -69,7 +71,8 @@ class SwapFeatureModule { quoterFactory = quoterFactory, customFeeCapabilityFacade = customFeeCapabilityFacade, extrinsicServiceFactory = extrinsicServiceFactory, - defaultFeePaymentProviderRegistry = defaultFeePaymentRegistry + defaultFeePaymentProviderRegistry = defaultFeePaymentRegistry, + tokenRepository = tokenRepository ) } diff --git a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/domain/swap/RealSwapService.kt b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/domain/swap/RealSwapService.kt index 20324d391b..d22b2b3947 100644 --- a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/domain/swap/RealSwapService.kt +++ b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/domain/swap/RealSwapService.kt @@ -11,6 +11,7 @@ import io.novafoundation.nova.common.utils.flowOf import io.novafoundation.nova.common.utils.forEachAsync import io.novafoundation.nova.common.utils.graph.EdgeVisitFilter import io.novafoundation.nova.common.utils.graph.Graph +import io.novafoundation.nova.common.utils.graph.Path import io.novafoundation.nova.common.utils.graph.create import io.novafoundation.nova.common.utils.graph.findAllPossibleDestinations import io.novafoundation.nova.common.utils.graph.hasOutcomingDirections @@ -18,6 +19,7 @@ import io.novafoundation.nova.common.utils.graph.vertices import io.novafoundation.nova.common.utils.isZero import io.novafoundation.nova.common.utils.mapAsync import io.novafoundation.nova.common.utils.mergeIfMultiple +import io.novafoundation.nova.common.utils.orZero import io.novafoundation.nova.common.utils.toPercent import io.novafoundation.nova.common.utils.withFlowScope import io.novafoundation.nova.feature_account_api.data.extrinsic.ExtrinsicService @@ -32,6 +34,7 @@ import io.novafoundation.nova.feature_account_api.data.model.SubstrateFeeBase import io.novafoundation.nova.feature_account_api.domain.model.MetaAccount import io.novafoundation.nova.feature_swap_api.domain.model.AtomicSwapOperation import io.novafoundation.nova.feature_swap_api.domain.model.AtomicSwapOperationArgs +import io.novafoundation.nova.feature_swap_api.domain.model.AtomicSwapOperationPrototype import io.novafoundation.nova.feature_swap_api.domain.model.AtomicSwapOperationSubmissionArgs import io.novafoundation.nova.feature_swap_api.domain.model.ReQuoteTrigger import io.novafoundation.nova.feature_swap_api.domain.model.SlippageConfig @@ -44,11 +47,15 @@ import io.novafoundation.nova.feature_swap_api.domain.model.SwapLimit import io.novafoundation.nova.feature_swap_api.domain.model.SwapProgress import io.novafoundation.nova.feature_swap_api.domain.model.SwapQuote import io.novafoundation.nova.feature_swap_api.domain.model.SwapQuoteArgs +import io.novafoundation.nova.feature_swap_api.domain.model.UsdConverter import io.novafoundation.nova.feature_swap_api.domain.model.amountToLeaveOnOriginToPayTxFees import io.novafoundation.nova.feature_swap_api.domain.model.replaceAmountIn import io.novafoundation.nova.feature_swap_api.domain.model.totalFeeEnsuringSubmissionAsset import io.novafoundation.nova.feature_swap_api.domain.swap.SwapService +import io.novafoundation.nova.feature_swap_core_api.data.paths.PathFeeEstimator import io.novafoundation.nova.feature_swap_core_api.data.paths.PathQuoter +import io.novafoundation.nova.feature_swap_core_api.data.paths.model.PathRoughFeeEstimation +import io.novafoundation.nova.feature_swap_core_api.data.paths.model.QuotedEdge import io.novafoundation.nova.feature_swap_core_api.data.paths.model.QuotedPath import io.novafoundation.nova.feature_swap_core_api.data.paths.model.firstSegmentQuote import io.novafoundation.nova.feature_swap_core_api.data.paths.model.firstSegmentQuotedAmount @@ -63,12 +70,18 @@ import io.novafoundation.nova.feature_swap_impl.data.assetExchange.assetConversi import io.novafoundation.nova.feature_swap_impl.data.assetExchange.crossChain.CrossChainTransferAssetExchangeFactory import io.novafoundation.nova.feature_swap_impl.data.assetExchange.hydraDx.HydraDxExchangeFactory import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.types.Balance +import io.novafoundation.nova.feature_wallet_api.domain.interfaces.TokenRepository +import io.novafoundation.nova.feature_wallet_api.domain.model.Token +import io.novafoundation.nova.feature_wallet_api.domain.model.planksFromFiatOrZero import io.novafoundation.nova.feature_wallet_api.domain.model.withAmount import io.novafoundation.nova.feature_wallet_api.presentation.formatters.formatPlanks +import io.novafoundation.nova.runtime.ext.Geneses import io.novafoundation.nova.runtime.ext.assetConversionSupported import io.novafoundation.nova.runtime.ext.fullId import io.novafoundation.nova.runtime.ext.hydraDxSupported import io.novafoundation.nova.runtime.ext.isUtility +import io.novafoundation.nova.runtime.ext.utilityAsset +import io.novafoundation.nova.runtime.ext.utilityAssetOf import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry import io.novafoundation.nova.runtime.multiNetwork.asset import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain @@ -88,6 +101,7 @@ import kotlinx.coroutines.flow.runningFold import kotlinx.coroutines.withContext import java.math.BigDecimal import java.math.BigInteger +import java.math.MathContext import kotlin.time.Duration.Companion.milliseconds private const val ALL_DIRECTIONS_CACHE = "RealSwapService.ALL_DIRECTIONS" @@ -107,6 +121,7 @@ internal class RealSwapService( private val customFeeCapabilityFacade: CustomFeeCapabilityFacade, private val extrinsicServiceFactory: ExtrinsicService.Factory, private val defaultFeePaymentProviderRegistry: FeePaymentProviderRegistry, + private val tokenRepository: TokenRepository, private val debug: Boolean = BuildConfig.DEBUG ) : SwapService { @@ -469,7 +484,129 @@ internal class RealSwapService( val graph = directionsGraph(computationScope).first() val filter = canPayFeeNodeFilter(computationScope) - quoterFactory.create(graph, this, filter) + quoterFactory.create(graph, this, SwapPathFeeEstimator(), filter) + } + } + + private inner class SwapPathFeeEstimator : PathFeeEstimator { + + override suspend fun roughlyEstimateFee(path: Path>): PathRoughFeeEstimation { + // USDT is used to determine usd to selected currency rate without making a separate request to price api + val usdtOnAssetHub = chainRegistry.getUSDTOnAssetHub() ?: return PathRoughFeeEstimation.zero() + + val operationPrototypes = path.constructAtomicOperationPrototypes() + + val nativeAssetsSegments = operationPrototypes.allNativeAssets() + val assetIn = chainRegistry.asset(path.first().edge.from) + val assetOut = chainRegistry.asset(path.last().edge.to) + + val prices = getTokens(assetIn = assetIn, assetOut = assetOut, usdTiedAsset = usdtOnAssetHub, fees = nativeAssetsSegments) + + val totalFiat = operationPrototypes.estimateTotalFeeInFiat(prices, usdtOnAssetHub.fullId) + + return PathRoughFeeEstimation( + inAssetIn = prices.fiatToPlanks(totalFiat, assetIn), + inAssetOut = prices.fiatToPlanks(totalFiat, assetOut) + ) + } + + private suspend fun ChainRegistry.getUSDTOnAssetHub(): Chain.Asset? { + val assetHub = getChain(Chain.Geneses.POLKADOT_ASSET_HUB) + return assetHub.assets.find { it.symbol.value == "USDT" } + } + + private fun Map.fiatToPlanks(fiat: BigDecimal, chainAsset: Chain.Asset): Balance { + val token = get(chainAsset.fullId) ?: return Balance.ZERO + + return token.planksFromFiatOrZero(fiat) + } + + private suspend fun getTokens( + assetIn: Chain.Asset, + assetOut: Chain.Asset, + usdTiedAsset: Chain.Asset, + fees: List + ): Map { + val allTokensToRequestPrices = buildList { + addAll(fees) + add(assetIn) + add(usdTiedAsset) + add(assetOut) + } + + return tokenRepository.getTokens(allTokensToRequestPrices) + } + + private suspend fun List.allNativeAssets(): List { + return map { + val chain = chainRegistry.getChain(it.fromChain) + chain.utilityAsset + } + } + + private suspend fun List.estimateTotalFeeInFiat( + prices: Map, + usdTiedAsset: FullChainAssetId + ): BigDecimal { + return sumOf { + val nativeAssetId = FullChainAssetId.utilityAssetOf(it.fromChain) + val token = prices[nativeAssetId] ?: return@sumOf BigDecimal.ZERO + + val usdConverter = PriceBasedUsdConverter(prices, nativeAssetId, usdTiedAsset) + + val roughFee = it.roughlyEstimateNativeFee(usdConverter) + token.amountToFiat(roughFee) + } + } + + private suspend fun Path>.constructAtomicOperationPrototypes(): List { + var currentSwapTx: AtomicSwapOperationPrototype? = null + val finishedSwapTxs = mutableListOf() + + forEach { quotedEdge -> + // Initial case - begin first operation + if (currentSwapTx == null) { + currentSwapTx = quotedEdge.edge.beginOperationPrototype() + return@forEach + } + + // Try to append segment to current swap tx + val maybeAppendedCurrentTx = quotedEdge.edge.appendToOperationPrototype(currentSwapTx!!) + + currentSwapTx = if (maybeAppendedCurrentTx == null) { + finishedSwapTxs.add(currentSwapTx!!) + quotedEdge.edge.beginOperationPrototype() + } else { + maybeAppendedCurrentTx + } + } + + finishedSwapTxs.add(currentSwapTx!!) + + return finishedSwapTxs + } + + private inner class PriceBasedUsdConverter( + private val prices: Map, + private val nativeAsset: FullChainAssetId, + private val usdTiedAsset: FullChainAssetId, + ) : UsdConverter { + + val currencyToUsdRate = determineCurrencyToUsdRate() + + override suspend fun nativeAssetEquivalentOf(usdAmount: Double): BigDecimal { + val priceInCurrency = prices[nativeAsset]?.coinRate?.rate ?: return BigDecimal.ZERO + val priceInUsd = priceInCurrency * currencyToUsdRate + return usdAmount.toBigDecimal() / priceInUsd + } + + private fun determineCurrencyToUsdRate(): BigDecimal { + val usdTiedAssetPrice = prices[usdTiedAsset] ?: return BigDecimal.ZERO + val rate = usdTiedAssetPrice.coinRate?.rate.orZero() + if (rate.isZero) return BigDecimal.ZERO + + return BigDecimal.ONE.divide(rate, MathContext.DECIMAL64) + } } } @@ -525,11 +662,12 @@ internal class RealSwapService( val amountIn: Balance val amountOut: Balance - when(trade.direction) { + when (trade.direction) { SwapDirection.SPECIFIED_IN -> { amountIn = quotedSwapEdge.quotedAmount amountOut = quotedSwapEdge.quote } + SwapDirection.SPECIFIED_OUT -> { amountIn = quotedSwapEdge.quote amountOut = quotedSwapEdge.quotedAmount @@ -540,6 +678,13 @@ internal class RealSwapService( val assetIn = chainRegistry.asset(quotedSwapEdge.edge.from) val initialAmount = amountIn.formatPlanks(assetIn) append(initialAmount) + + if (trade.direction == SwapDirection.SPECIFIED_OUT) { + val roughFeesInAssetIn = trade.roughFeeEstimation.inAssetIn + val roughFeesInAssetInAmount = roughFeesInAssetIn.formatPlanks(assetIn) + + append(" (+${roughFeesInAssetInAmount} fees) ") + } } append(" --- " + quotedSwapEdge.edge.debugLabel() + " ---> ") @@ -548,6 +693,15 @@ internal class RealSwapService( val outAmount = amountOut.formatPlanks(assetOut) append(outAmount) + + if (index == trade.path.size - 1) { + if (trade.direction == SwapDirection.SPECIFIED_IN) { + val roughFeesInAssetOut = trade.roughFeeEstimation.inAssetOut + val roughFeesInAssetOutAmount = roughFeesInAssetOut.formatPlanks(assetOut) + + append(" (-${roughFeesInAssetOutAmount} fees)") + } + } } } } 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 f0dceaab87..61fbef0826 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 @@ -19,12 +19,7 @@ import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.flowOf import java.math.BigDecimal -interface AssetTransferBase { - - val recipientAccountId: AccountId - get() = destinationChain.accountIdOf(recipient) - - val recipient: String +interface AssetTransferDirection { val originChain: Chain @@ -33,6 +28,14 @@ interface AssetTransferBase { val destinationChain: Chain val destinationChainAsset: Chain.Asset +} + +interface AssetTransferBase : AssetTransferDirection { + + val recipientAccountId: AccountId + get() = destinationChain.accountIdOf(recipient) + + val recipient: String val feePaymentCurrency: FeePaymentCurrency diff --git a/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/domain/interfaces/TokenRepository.kt b/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/domain/interfaces/TokenRepository.kt index 0be579409a..dd9150be09 100644 --- a/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/domain/interfaces/TokenRepository.kt +++ b/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/domain/interfaces/TokenRepository.kt @@ -13,6 +13,8 @@ interface TokenRepository { */ suspend fun observeTokens(chainAssets: List): Flow> + suspend fun getTokens(chainAsset: List): Map + suspend fun getToken(chainAsset: Chain.Asset): Token suspend fun getTokenOrNull(chainAsset: Chain.Asset): Token? diff --git a/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/domain/model/CoinRate.kt b/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/domain/model/CoinRate.kt index 2397ccc3f1..7c8df64d45 100644 --- a/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/domain/model/CoinRate.kt +++ b/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/domain/model/CoinRate.kt @@ -1,6 +1,7 @@ package io.novafoundation.nova.feature_wallet_api.domain.model import io.novafoundation.nova.common.utils.binarySearchFloor +import io.novafoundation.nova.common.utils.isZero import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain import java.math.BigDecimal import java.math.BigInteger @@ -15,6 +16,16 @@ class HistoricalCoinRate(val timestamp: Long, override val rate: BigDecimal) : C fun CoinRate.convertAmount(amount: BigDecimal) = amount * rate +fun CoinRate.convertFiatToAmount(fiat: BigDecimal): BigDecimal { + if (rate.isZero) return BigDecimal.ZERO + + return fiat / rate +} + +fun CoinRate.convertFiatToPlanks(asset: Chain.Asset, fiat: BigDecimal): BigInteger { + return asset.planksFromAmount(convertFiatToAmount(fiat)) +} + fun CoinRate.convertPlanks(asset: Chain.Asset, amount: BigInteger) = convertAmount(asset.amountFromPlanks(amount)) fun List.findNearestCoinRate(timestamp: Long): HistoricalCoinRate? { diff --git a/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/domain/model/Token.kt b/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/domain/model/Token.kt index 93a731a2c5..f9d7085e5b 100644 --- a/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/domain/model/Token.kt +++ b/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/domain/model/Token.kt @@ -4,6 +4,7 @@ import io.novafoundation.nova.common.utils.amountFromPlanks import io.novafoundation.nova.common.utils.orZero import io.novafoundation.nova.common.utils.planksFromAmount import io.novafoundation.nova.feature_currency_api.domain.model.Currency +import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.types.Balance import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain import java.math.BigDecimal import java.math.BigInteger @@ -39,6 +40,8 @@ data class HistoricalToken( fun TokenBase.toFiatOrNull(tokenAmount: BigDecimal): BigDecimal? = coinRate?.convertAmount(tokenAmount) +fun TokenBase.planksFromFiatOrZero(fiat: BigDecimal): Balance = coinRate?.convertFiatToPlanks(configuration, fiat).orZero() + fun TokenBase.planksToFiatOrNull(tokenAmountPlanks: BigInteger): BigDecimal? = coinRate?.convertPlanks(configuration, tokenAmountPlanks) fun TokenBase.amountFromPlanks(amountInPlanks: BigInteger) = configuration.amountFromPlanks(amountInPlanks) diff --git a/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/data/repository/TokenRepositoryImpl.kt b/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/data/repository/TokenRepositoryImpl.kt index bce2036938..2c11f472e6 100644 --- a/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/data/repository/TokenRepositoryImpl.kt +++ b/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/data/repository/TokenRepositoryImpl.kt @@ -1,5 +1,6 @@ package io.novafoundation.nova.feature_wallet_impl.data.repository +import io.novafoundation.nova.common.utils.mapToSet import io.novafoundation.nova.core_db.dao.TokenDao import io.novafoundation.nova.feature_wallet_api.domain.interfaces.TokenRepository import io.novafoundation.nova.feature_wallet_api.domain.model.Token @@ -40,6 +41,28 @@ class TokenRepositoryImpl( } } + override suspend fun getTokens(chainAssets: List): Map { + if (chainAssets.isEmpty()) return emptyMap() + + val symbols = chainAssets.mapToSet { it.symbol.value }.distinct() + + val tokens = tokenDao.getTokensWithCurrency(symbols) + + val tokensBySymbol = tokens.associateBy { it.token?.tokenSymbol } + val currency = tokens.first().currency + + return chainAssets.associateBy( + keySelector = { chainAsset -> chainAsset.fullId }, + valueTransform = { chainAsset -> + mapTokenLocalToToken( + tokenLocal = tokensBySymbol[chainAsset.symbol.value]?.token, + currencyLocal = currency, + chainAsset = chainAsset + ) + } + ) + } + override suspend fun getToken(chainAsset: Chain.Asset): Token = getTokenOrNull(chainAsset)!! override suspend fun getTokenOrNull(chainAsset: Chain.Asset): Token? = withContext(Dispatchers.Default) { 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 65642ef1b0..54ff122ac4 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 @@ -520,3 +520,5 @@ fun Chain.hasReferendaSummaryApi(): Boolean { fun Chain.summaryApiOrNull(): Chain.ExternalApi.ReferendumSummary? { return externalApi() } + +fun FullChainAssetId.Companion.utilityAssetOf(chainId: ChainId) = FullChainAssetId(chainId, UTILITY_ASSET_ID) diff --git a/runtime/src/main/java/io/novafoundation/nova/runtime/multiNetwork/chain/model/Chain.kt b/runtime/src/main/java/io/novafoundation/nova/runtime/multiNetwork/chain/model/Chain.kt index 76b61f9ae2..5e38120679 100644 --- a/runtime/src/main/java/io/novafoundation/nova/runtime/multiNetwork/chain/model/Chain.kt +++ b/runtime/src/main/java/io/novafoundation/nova/runtime/multiNetwork/chain/model/Chain.kt @@ -15,7 +15,10 @@ typealias ExplorerTemplateExtractor = (Chain.Explorer) -> StringTemplate? typealias BuyProviderId = String typealias BuyProviderArguments = Map -data class FullChainAssetId(val chainId: ChainId, val assetId: ChainAssetId) +data class FullChainAssetId(val chainId: ChainId, val assetId: ChainAssetId) { + + companion object +} data class Chain( val id: ChainId, From 5f7877620e207d1800d6c63309d59ac96532bf75 Mon Sep 17 00:00:00 2001 From: Valentun Date: Wed, 23 Oct 2024 17:03:29 +0300 Subject: [PATCH 31/83] Fixes --- .../feature_swap_api/domain/model/AmountWithAssetId.kt | 9 --------- .../crossChain/CrossChainTransferAssetExchange.kt | 2 +- 2 files changed, 1 insertion(+), 10 deletions(-) delete mode 100644 feature-swap-api/src/main/java/io/novafoundation/nova/feature_swap_api/domain/model/AmountWithAssetId.kt diff --git a/feature-swap-api/src/main/java/io/novafoundation/nova/feature_swap_api/domain/model/AmountWithAssetId.kt b/feature-swap-api/src/main/java/io/novafoundation/nova/feature_swap_api/domain/model/AmountWithAssetId.kt deleted file mode 100644 index 3ddc487b0e..0000000000 --- a/feature-swap-api/src/main/java/io/novafoundation/nova/feature_swap_api/domain/model/AmountWithAssetId.kt +++ /dev/null @@ -1,9 +0,0 @@ -package io.novafoundation.nova.feature_swap_api.domain.model - -import io.novafoundation.nova.runtime.multiNetwork.chain.model.FullChainAssetId -import java.math.BigDecimal - -class AmountWithAssetId( - val assetId: FullChainAssetId, - val amount: BigDecimal -) diff --git a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/crossChain/CrossChainTransferAssetExchange.kt b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/crossChain/CrossChainTransferAssetExchange.kt index dd4e69bfca..15d0ad3ce0 100644 --- a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/crossChain/CrossChainTransferAssetExchange.kt +++ b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/crossChain/CrossChainTransferAssetExchange.kt @@ -139,7 +139,7 @@ class CrossChainTransferAssetExchange( ): AtomicSwapOperationPrototype { override suspend fun roughlyEstimateNativeFee(usdConverter: UsdConverter): BigDecimal { - var totalAmount = usdConverter.nativeAssetEquivalentOf(0.15) + var totalAmount = BigDecimal.ZERO if (isChainWithExpensiveCrossChain(fromChain)) { totalAmount += usdConverter.nativeAssetEquivalentOf(0.15) From 4cab1cf21362f237c884e15e73a0a7052de2797b Mon Sep 17 00:00:00 2001 From: Valentun Date: Wed, 23 Oct 2024 17:44:24 +0300 Subject: [PATCH 32/83] Only use BUY for the first segment and use SELL for the rest. Use new Fraction abstraction for slippage --- .../nova/common/utils/Fraction.kt | 75 +++++++++++++++++++ .../nova/common/utils/Percent.kt | 8 +- .../utils/formatting/NumberFormatters.kt | 13 ++-- .../domain/model/SlippageConfig.kt | 27 +++---- .../domain/model/SwapQuoteArgs.kt | 49 ++++++++---- .../presentation/state/SwapSettings.kt | 7 +- .../presentation/state/SwapSettingsState.kt | 4 +- .../domain/swap/RealSwapService.kt | 5 +- .../validation/SwapPayloadValidation.kt | 6 +- .../validation/SwapValidationFailure.kt | 4 +- .../SwapSlippageRangeValidation.kt | 2 +- .../presentation/common/SlippageAlertMixin.kt | 5 +- .../presentation/common/state/SwapState.kt | 4 +- .../confirmation/SwapConfirmationViewModel.kt | 8 +- .../fieldValidation/SlippageFieldValidator.kt | 17 +++-- .../main/SwapValidationFailureUi.kt | 4 +- .../options/SwapOptionsViewModel.kt | 20 ++--- .../state/RealSwapSettingsState.kt | 3 +- 18 files changed, 182 insertions(+), 79 deletions(-) create mode 100644 common/src/main/java/io/novafoundation/nova/common/utils/Fraction.kt diff --git a/common/src/main/java/io/novafoundation/nova/common/utils/Fraction.kt b/common/src/main/java/io/novafoundation/nova/common/utils/Fraction.kt new file mode 100644 index 0000000000..5ebe93727a --- /dev/null +++ b/common/src/main/java/io/novafoundation/nova/common/utils/Fraction.kt @@ -0,0 +1,75 @@ +package io.novafoundation.nova.common.utils + +import java.math.BigDecimal + +@JvmInline +value class Fraction private constructor(private val value: Double): Comparable { + + companion object { + + val ZERO: Fraction = Fraction(0.0) + + fun Double.toFraction(unit: FractionUnit): Fraction { + return Fraction(unit.convertToFraction(this)) + } + + val Double.percents: Fraction + get() = toFraction(FractionUnit.PERCENT) + + val BigDecimal.percents: Fraction + get() = toDouble().percents + + val BigDecimal.fractions: Fraction + get() = toDouble().fractions + + val Double.fractions: Fraction + get() = toFraction(FractionUnit.FRACTION) + + val Int.percents: Fraction + get() = toDouble().toFraction(FractionUnit.PERCENT) + } + + val inPercents: Double + get() = FractionUnit.PERCENT.convertFromFraction(value) + + val inFraction: Double + get() = FractionUnit.FRACTION.convertFromFraction(value) + + val inWholePercents: Int + get() = FractionUnit.PERCENT.convertFromFractionWhole(value) + + override fun compareTo(other: Fraction): Int { + return value.compareTo(other.value) + } +} + +enum class FractionUnit { + + /** + * Default range: 0..1 + */ + FRACTION, + + /** + * Default range: 0..100 + */ + PERCENT +} + +private fun FractionUnit.convertToFraction(value: Double): Double { + return when (this) { + FractionUnit.FRACTION -> value + FractionUnit.PERCENT -> value / 100 + } +} + +private fun FractionUnit.convertFromFraction(value: Double): Double { + return when (this) { + FractionUnit.FRACTION -> value + FractionUnit.PERCENT -> value * 100 + } +} + +private fun FractionUnit.convertFromFractionWhole(value: Double): Int { + return convertFromFraction(value).toInt() +} diff --git a/common/src/main/java/io/novafoundation/nova/common/utils/Percent.kt b/common/src/main/java/io/novafoundation/nova/common/utils/Percent.kt index 9ce2cc2e9d..4ec0b258b7 100644 --- a/common/src/main/java/io/novafoundation/nova/common/utils/Percent.kt +++ b/common/src/main/java/io/novafoundation/nova/common/utils/Percent.kt @@ -9,6 +9,7 @@ import java.math.BigDecimal * Thus, 0.1 will represent equivalent to 10% */ @JvmInline +@Deprecated("Use Fraction which offers much easier and understandable abstraction over fractions") value class Perbill(val value: Double) : Comparable { companion object { @@ -26,6 +27,7 @@ value class Perbill(val value: Double) : Comparable { * E.g. Percent(10) represents value of 10% */ @JvmInline +@Deprecated("Use Fraction which offers much easier and understandable abstraction over fractions") value class Percent(val value: Double) : Comparable { companion object { @@ -42,19 +44,13 @@ value class Percent(val value: Double) : Comparable { } } -val Percent.fraction: BigDecimal - get() = toPerbill().value.toBigDecimal() inline fun BigDecimal.asPerbill(): Perbill = Perbill(this.toDouble()) -inline fun BigDecimal.asPercent(): Percent = Percent(this.toDouble()) - inline fun Double.asPerbill(): Perbill = Perbill(this) inline fun Double.asPercent(): Percent = Percent(this) -inline fun Percent.toPerbill(): Perbill = Perbill(value / 100) - inline fun Perbill.toPercent(): Percent = Percent(value * 100) inline fun Perbill?.orZero(): Perbill = this ?: Perbill(0.0) diff --git a/common/src/main/java/io/novafoundation/nova/common/utils/formatting/NumberFormatters.kt b/common/src/main/java/io/novafoundation/nova/common/utils/formatting/NumberFormatters.kt index b0f219daa3..f5b02fada1 100644 --- a/common/src/main/java/io/novafoundation/nova/common/utils/formatting/NumberFormatters.kt +++ b/common/src/main/java/io/novafoundation/nova/common/utils/formatting/NumberFormatters.kt @@ -4,6 +4,7 @@ import android.content.Context import android.text.format.DateUtils import io.novafoundation.nova.common.R import io.novafoundation.nova.common.resources.ResourceManager +import io.novafoundation.nova.common.utils.Fraction import io.novafoundation.nova.common.utils.Perbill import io.novafoundation.nova.common.utils.Percent import io.novafoundation.nova.common.utils.daysFromMillis @@ -104,22 +105,22 @@ fun BigDecimal.formatAsChange(): String { return prefix + formatAsPercentage() } -fun BigDecimal.formatAsPercentage(): String { - return defaultAbbreviationFormatter.format(this) + "%" +fun BigDecimal.formatAsPercentage(includeSymbol: Boolean = true): String { + return defaultAbbreviationFormatter.format(this) + if (includeSymbol) "%" else "" } fun Percent.format(): String { return value.toBigDecimal().formatAsPercentage() } -fun Percent.formatWithoutSymbol(): String { - return defaultAbbreviationFormatter.format(value.toBigDecimal()) -} - fun Perbill.format(): String { return toPercent().format() } +fun Fraction.formatPercents(includeSymbol: Boolean = true): String { + return inPercents.toBigDecimal().formatAsPercentage(includeSymbol) +} + fun BigDecimal.formatFractionAsPercentage(): String { return fractionToPercentage().formatAsPercentage() } diff --git a/feature-swap-api/src/main/java/io/novafoundation/nova/feature_swap_api/domain/model/SlippageConfig.kt b/feature-swap-api/src/main/java/io/novafoundation/nova/feature_swap_api/domain/model/SlippageConfig.kt index 8a89ea9768..14035ec326 100644 --- a/feature-swap-api/src/main/java/io/novafoundation/nova/feature_swap_api/domain/model/SlippageConfig.kt +++ b/feature-swap-api/src/main/java/io/novafoundation/nova/feature_swap_api/domain/model/SlippageConfig.kt @@ -1,26 +1,27 @@ package io.novafoundation.nova.feature_swap_api.domain.model -import io.novafoundation.nova.common.utils.Percent +import io.novafoundation.nova.common.utils.Fraction +import io.novafoundation.nova.common.utils.Fraction.Companion.percents class SlippageConfig( - val defaultSlippage: Percent, - val slippageTips: List, - val minAvailableSlippage: Percent, - val maxAvailableSlippage: Percent, - val smallSlippage: Percent, - val bigSlippage: Percent + val defaultSlippage: Fraction, + val slippageTips: List, + val minAvailableSlippage: Fraction, + val maxAvailableSlippage: Fraction, + val smallSlippage: Fraction, + val bigSlippage: Fraction ) { companion object { fun default(): SlippageConfig { return SlippageConfig( - defaultSlippage = Percent(0.5), - slippageTips = listOf(Percent(0.1), Percent(0.5), Percent(1.0)), - minAvailableSlippage = Percent(0.01), - maxAvailableSlippage = Percent(50.0), - smallSlippage = Percent(0.05), - bigSlippage = Percent(1.0) + defaultSlippage = 0.5.percents, + slippageTips = listOf(0.1.percents, 0.5.percents, 1.0.percents), + minAvailableSlippage = 0.01.percents, + maxAvailableSlippage = 50.0.percents, + smallSlippage = 0.05.percents, + bigSlippage = 1.0.percents ) } } diff --git a/feature-swap-api/src/main/java/io/novafoundation/nova/feature_swap_api/domain/model/SwapQuoteArgs.kt b/feature-swap-api/src/main/java/io/novafoundation/nova/feature_swap_api/domain/model/SwapQuoteArgs.kt index 04c4ef5538..63c13587b7 100644 --- a/feature-swap-api/src/main/java/io/novafoundation/nova/feature_swap_api/domain/model/SwapQuoteArgs.kt +++ b/feature-swap-api/src/main/java/io/novafoundation/nova/feature_swap_api/domain/model/SwapQuoteArgs.kt @@ -1,9 +1,11 @@ package io.novafoundation.nova.feature_swap_api.domain.model -import io.novafoundation.nova.common.utils.Percent +import io.novafoundation.nova.common.utils.Fraction +import io.novafoundation.nova.common.utils.Fraction.Companion.fractions +import io.novafoundation.nova.common.utils.atLeastZero import io.novafoundation.nova.common.utils.divideToDecimal -import io.novafoundation.nova.common.utils.fraction import io.novafoundation.nova.common.utils.graph.Path +import io.novafoundation.nova.common.utils.isZero import io.novafoundation.nova.feature_swap_core_api.data.paths.model.QuotedEdge import io.novafoundation.nova.feature_swap_core_api.data.primitive.model.SwapDirection import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.types.Balance @@ -20,7 +22,7 @@ data class SwapQuoteArgs( open class SwapFeeArgs( val assetIn: Chain.Asset, - val slippage: Percent, + val slippage: Fraction, val executionPath: Path, val direction: SwapDirection, val firstSegmentFees: Chain.Asset @@ -49,13 +51,34 @@ sealed class SwapLimit { * Adjusts SwapLimit to the [newAmountIn] based on the quoted swap rate * This is only suitable for small changes amount in, as it implicitly assumes the swap rate stays the same */ -fun SwapLimit.replaceAmountIn(newAmountIn: Balance): SwapLimit { - return when(this) { +fun SwapLimit.replaceAmountIn(newAmountIn: Balance, shouldReplaceBuyWithSell: Boolean): SwapLimit { + return when (this) { is SwapLimit.SpecifiedIn -> updateInAmount(newAmountIn) - is SwapLimit.SpecifiedOut -> updateInAmount(newAmountIn) + is SwapLimit.SpecifiedOut -> { + if (shouldReplaceBuyWithSell) { + updateInAmountChangingToSell(newAmountIn) + } else { + updateInAmount(newAmountIn) + } + } } } +private fun SwapLimit.SpecifiedOut.updateInAmountChangingToSell(newAmountIn: Balance): SwapLimit { + val slippage = slippage() + + val inferredQuotedBalance = replacedInQuoteAmount(newAmountIn, amountOut) + + return SpecifiedIn(amount = newAmountIn, slippage, quotedBalance = inferredQuotedBalance) +} + +private fun SwapLimit.SpecifiedOut.slippage(): Fraction { + if (amountInQuote.isZero) return Fraction.ZERO + + val slippageAsFraction = (amountInMax.divideToDecimal(amountInQuote) - BigDecimal.ONE).atLeastZero() + return slippageAsFraction.fractions +} + private fun SwapLimit.SpecifiedIn.replaceInMultiplier(amount: Balance): BigDecimal { return amount.divideToDecimal(amountIn) } @@ -88,7 +111,7 @@ private fun SwapLimit.SpecifiedOut.updateInAmount(newAmountInQuote: Balance): Sw ) } -fun SwapQuote.toExecuteArgs(slippage: Percent, firstSegmentFees: Chain.Asset): SwapFeeArgs { +fun SwapQuote.toExecuteArgs(slippage: Fraction, firstSegmentFees: Chain.Asset): SwapFeeArgs { return SwapFeeArgs( assetIn = amountIn.chainAsset, slippage = slippage, @@ -98,16 +121,15 @@ fun SwapQuote.toExecuteArgs(slippage: Percent, firstSegmentFees: Chain.Asset): S ) } -fun SwapLimit(direction: SwapDirection, amount: Balance, slippage: Percent, quotedBalance: Balance): SwapLimit { +fun SwapLimit(direction: SwapDirection, amount: Balance, slippage: Fraction, quotedBalance: Balance): SwapLimit { return when (direction) { SwapDirection.SPECIFIED_IN -> SpecifiedIn(amount, slippage, quotedBalance) SwapDirection.SPECIFIED_OUT -> SpecifiedOut(amount, slippage, quotedBalance) } } -@Suppress("FunctionName") -private fun SpecifiedIn(amount: Balance, slippage: Percent, quotedBalance: Balance): SwapLimit.SpecifiedIn { - val lessAmountCoefficient = BigDecimal.ONE - slippage.fraction +private fun SpecifiedIn(amount: Balance, slippage: Fraction, quotedBalance: Balance): SwapLimit.SpecifiedIn { + val lessAmountCoefficient = BigDecimal.ONE - slippage.inFraction.toBigDecimal() val amountOutMin = quotedBalance.toBigDecimal() * lessAmountCoefficient return SwapLimit.SpecifiedIn( @@ -117,9 +139,8 @@ private fun SpecifiedIn(amount: Balance, slippage: Percent, quotedBalance: Balan ) } -@Suppress("FunctionName") -private fun SpecifiedOut(amount: Balance, slippage: Percent, quotedBalance: Balance): SwapLimit.SpecifiedOut { - val moreAmountCoefficient = BigDecimal.ONE + slippage.fraction +private fun SpecifiedOut(amount: Balance, slippage: Fraction, quotedBalance: Balance): SwapLimit.SpecifiedOut { + val moreAmountCoefficient = BigDecimal.ONE + slippage.inFraction.toBigDecimal() val amountInMax = quotedBalance.toBigDecimal() * moreAmountCoefficient return SwapLimit.SpecifiedOut( diff --git a/feature-swap-api/src/main/java/io/novafoundation/nova/feature_swap_api/presentation/state/SwapSettings.kt b/feature-swap-api/src/main/java/io/novafoundation/nova/feature_swap_api/presentation/state/SwapSettings.kt index dd0ec0d98e..ce0915d3c1 100644 --- a/feature-swap-api/src/main/java/io/novafoundation/nova/feature_swap_api/presentation/state/SwapSettings.kt +++ b/feature-swap-api/src/main/java/io/novafoundation/nova/feature_swap_api/presentation/state/SwapSettings.kt @@ -1,11 +1,12 @@ package io.novafoundation.nova.feature_swap_api.presentation.state -import io.novafoundation.nova.common.utils.Percent +import io.novafoundation.nova.common.utils.Fraction +import io.novafoundation.nova.common.utils.Fraction.Companion.percents import io.novafoundation.nova.feature_swap_core_api.data.primitive.model.SwapDirection import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.types.Balance import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain -val DEFAULT_SLIPPAGE = Percent(0.5) +val DEFAULT_SLIPPAGE = 0.5.percents data class SwapSettings( val assetIn: Chain.Asset? = null, @@ -13,5 +14,5 @@ data class SwapSettings( val feeAsset: Chain.Asset? = null, val amount: Balance? = null, val swapDirection: SwapDirection? = null, - val slippage: Percent + val slippage: Fraction ) diff --git a/feature-swap-api/src/main/java/io/novafoundation/nova/feature_swap_api/presentation/state/SwapSettingsState.kt b/feature-swap-api/src/main/java/io/novafoundation/nova/feature_swap_api/presentation/state/SwapSettingsState.kt index e1a0b3fe31..7244c76c35 100644 --- a/feature-swap-api/src/main/java/io/novafoundation/nova/feature_swap_api/presentation/state/SwapSettingsState.kt +++ b/feature-swap-api/src/main/java/io/novafoundation/nova/feature_swap_api/presentation/state/SwapSettingsState.kt @@ -1,6 +1,6 @@ package io.novafoundation.nova.feature_swap_api.presentation.state -import io.novafoundation.nova.common.utils.Percent +import io.novafoundation.nova.common.utils.Fraction import io.novafoundation.nova.feature_swap_core_api.data.primitive.model.SwapDirection import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.types.Balance import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain @@ -16,7 +16,7 @@ interface SwapSettingsState : SelectedOptionSharedState { fun setAmount(amount: Balance?, swapDirection: SwapDirection) - fun setSlippage(slippage: Percent) + fun setSlippage(slippage: Fraction) suspend fun flipAssets(): SwapSettings diff --git a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/domain/swap/RealSwapService.kt b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/domain/swap/RealSwapService.kt index d22b2b3947..694692bd27 100644 --- a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/domain/swap/RealSwapService.kt +++ b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/domain/swap/RealSwapService.kt @@ -198,7 +198,6 @@ internal class RealSwapService( val initialCorrection: Result = Result.success(null) return flow { - // Zip assumes atomicOperations and atomicOperationFees were constructed the same way atomicOperations.fold(initialCorrection) { prevStepCorrection, (segmentFee, operation) -> prevStepCorrection.flatMap { correction -> emit(SwapProgress.StepStarted(operation.inProgressLabel())) @@ -210,7 +209,9 @@ internal class RealSwapService( amountIn + calculatedFee.additionalAmountForSwap.amount } - val actualSwapLimit = operation.estimatedSwapLimit.replaceAmountIn(newAmountIn) + // We cannot execute buy for segments after first one since we deal with actualReceivedAmount there + val shouldReplaceBuyWithSell = correction != null + val actualSwapLimit = operation.estimatedSwapLimit.replaceAmountIn(newAmountIn, shouldReplaceBuyWithSell) val segmentSubmissionArgs = AtomicSwapOperationSubmissionArgs(actualSwapLimit) Log.d("SwapSubmission", operation.inProgressLabel() + " with $actualSwapLimit") diff --git a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/domain/validation/SwapPayloadValidation.kt b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/domain/validation/SwapPayloadValidation.kt index 59c6d54945..8cd42de8f7 100644 --- a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/domain/validation/SwapPayloadValidation.kt +++ b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/domain/validation/SwapPayloadValidation.kt @@ -1,8 +1,8 @@ package io.novafoundation.nova.feature_swap_impl.domain.validation -import io.novafoundation.nova.common.utils.Percent -import io.novafoundation.nova.feature_swap_api.domain.model.SwapFeeArgs +import io.novafoundation.nova.common.utils.Fraction import io.novafoundation.nova.feature_swap_api.domain.model.SwapFee +import io.novafoundation.nova.feature_swap_api.domain.model.SwapFeeArgs import io.novafoundation.nova.feature_swap_api.domain.model.SwapQuote import io.novafoundation.nova.feature_swap_api.domain.model.SwapQuoteArgs import io.novafoundation.nova.feature_swap_api.domain.model.totalDeductedPlanks @@ -16,7 +16,7 @@ import java.math.BigInteger data class SwapValidationPayload( val detailedAssetIn: SwapAssetData, val detailedAssetOut: SwapAssetData, - val slippage: Percent, + val slippage: Fraction, val feeAsset: Asset, val decimalFee: GenericDecimalFee, val swapQuote: SwapQuote, diff --git a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/domain/validation/SwapValidationFailure.kt b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/domain/validation/SwapValidationFailure.kt index 66e491fabd..f8f8c6557a 100644 --- a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/domain/validation/SwapValidationFailure.kt +++ b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/domain/validation/SwapValidationFailure.kt @@ -1,6 +1,6 @@ package io.novafoundation.nova.feature_swap_impl.domain.validation -import io.novafoundation.nova.common.utils.Percent +import io.novafoundation.nova.common.utils.Fraction import io.novafoundation.nova.feature_account_api.data.model.Fee import io.novafoundation.nova.feature_swap_api.domain.model.SwapFee import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.types.Balance @@ -15,7 +15,7 @@ sealed class SwapValidationFailure { object NonPositiveAmount : SwapValidationFailure() - class InvalidSlippage(val minSlippage: Percent, val maxSlippage: Percent) : SwapValidationFailure() + class InvalidSlippage(val minSlippage: Fraction, val maxSlippage: Fraction) : SwapValidationFailure() class NewRateExceededSlippage( val assetIn: Chain.Asset, diff --git a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/domain/validation/validations/SwapSlippageRangeValidation.kt b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/domain/validation/validations/SwapSlippageRangeValidation.kt index 1a3e53863c..107b4ffc3c 100644 --- a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/domain/validation/validations/SwapSlippageRangeValidation.kt +++ b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/domain/validation/validations/SwapSlippageRangeValidation.kt @@ -16,7 +16,7 @@ class SwapSlippageRangeValidation( override suspend fun validate(value: SwapValidationPayload): ValidationStatus { val slippageConfig = swapService.defaultSlippageConfig(value.detailedAssetIn.chain.id)!! - if (value.slippage.value !in slippageConfig.minAvailableSlippage.value..slippageConfig.maxAvailableSlippage.value) { + if (value.slippage !in slippageConfig.minAvailableSlippage..slippageConfig.maxAvailableSlippage) { return InvalidSlippage(slippageConfig.minAvailableSlippage, slippageConfig.maxAvailableSlippage).validationError() } diff --git a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/common/SlippageAlertMixin.kt b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/common/SlippageAlertMixin.kt index 7164ba4eba..22477c9f90 100644 --- a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/common/SlippageAlertMixin.kt +++ b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/common/SlippageAlertMixin.kt @@ -1,6 +1,7 @@ package io.novafoundation.nova.feature_swap_impl.presentation.common import io.novafoundation.nova.common.resources.ResourceManager +import io.novafoundation.nova.common.utils.Fraction import io.novafoundation.nova.common.utils.Percent import io.novafoundation.nova.feature_swap_api.domain.model.SlippageConfig import io.novafoundation.nova.feature_swap_impl.R @@ -11,7 +12,7 @@ class SlippageAlertMixinFactory( private val resourceManager: ResourceManager ) { - fun create(slippageConfig: Flow, slippageFlow: Flow): SlippageAlertMixin { + fun create(slippageConfig: Flow, slippageFlow: Flow): SlippageAlertMixin { return RealSlippageAlertMixin( resourceManager, slippageConfig, @@ -27,7 +28,7 @@ interface SlippageAlertMixin { class RealSlippageAlertMixin( val resourceManager: ResourceManager, slippageConfig: Flow, - slippageFlow: Flow + slippageFlow: Flow ) : SlippageAlertMixin { override val slippageAlertMessage: Flow = combine(slippageConfig, slippageFlow) { slippageConfig, slippage -> diff --git a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/common/state/SwapState.kt b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/common/state/SwapState.kt index a24f1e6065..cdfbdfe872 100644 --- a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/common/state/SwapState.kt +++ b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/common/state/SwapState.kt @@ -1,11 +1,11 @@ package io.novafoundation.nova.feature_swap_impl.presentation.common.state -import io.novafoundation.nova.common.utils.Percent +import io.novafoundation.nova.common.utils.Fraction import io.novafoundation.nova.feature_swap_api.domain.model.SwapFee import io.novafoundation.nova.feature_swap_api.domain.model.SwapQuote class SwapState( val quote: SwapQuote, val fee: SwapFee, - val slippage: Percent, + val slippage: Fraction, ) diff --git a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/confirmation/SwapConfirmationViewModel.kt b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/confirmation/SwapConfirmationViewModel.kt index 4e04c1c55f..26a811434a 100644 --- a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/confirmation/SwapConfirmationViewModel.kt +++ b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/confirmation/SwapConfirmationViewModel.kt @@ -9,7 +9,7 @@ import io.novafoundation.nova.common.resources.ResourceManager import io.novafoundation.nova.common.utils.Percent import io.novafoundation.nova.common.utils.combineToPair import io.novafoundation.nova.common.utils.flowOf -import io.novafoundation.nova.common.utils.formatting.format +import io.novafoundation.nova.common.utils.formatting.formatPercents import io.novafoundation.nova.common.validation.ValidationExecutor import io.novafoundation.nova.common.view.bottomSheet.description.DescriptionBottomSheetLauncher import io.novafoundation.nova.common.view.bottomSheet.description.launchNetworkFeeDescription @@ -129,7 +129,7 @@ class SwapConfirmationViewModel( private val slippageAlertMixin = slippageAlertMixinFactory.create(slippageConfigFlow, slippageFlow) - private val chainIn = initialSwapState.map { + private val chainIn = initialSwapState.map { chainRegistry.getChain(it.quote.assetIn.chainId) } .shareInBackground() @@ -253,7 +253,7 @@ class SwapConfirmationViewModel( swapInteractor.executeSwap(fee) .onEach { progressResult -> - when(progressResult) { + when (progressResult) { SwapProgress.Done -> navigateToNextScreen(quote.assetOut) is SwapProgress.Failure -> showError(progressResult.error) is SwapProgress.StepStarted -> showMessage(progressResult.step) @@ -281,7 +281,7 @@ class SwapConfirmationViewModel( ), rate = formatRate(confirmationState.swapQuote.swapRate(), assetIn, assetOut), priceDifference = formatPriceDifference(confirmationState.swapQuote.priceImpact), - slippage =slippageFlow.first().format() + slippage = slippageFlow.first().formatPercents() ) } diff --git a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/fieldValidation/SlippageFieldValidator.kt b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/fieldValidation/SlippageFieldValidator.kt index d6d007bae4..3f04f97287 100644 --- a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/fieldValidation/SlippageFieldValidator.kt +++ b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/fieldValidation/SlippageFieldValidator.kt @@ -1,8 +1,9 @@ package io.novafoundation.nova.feature_swap_impl.presentation.fieldValidation import io.novafoundation.nova.common.resources.ResourceManager -import io.novafoundation.nova.common.utils.Percent -import io.novafoundation.nova.common.utils.formatting.format +import io.novafoundation.nova.common.utils.Fraction +import io.novafoundation.nova.common.utils.Fraction.Companion.percents +import io.novafoundation.nova.common.utils.formatting.formatPercents import io.novafoundation.nova.common.validation.FieldValidationResult import io.novafoundation.nova.common.validation.MapFieldValidator import io.novafoundation.nova.feature_swap_api.domain.model.SlippageConfig @@ -10,7 +11,7 @@ import io.novafoundation.nova.feature_swap_impl.R class SlippageFieldValidatorFactory(private val resourceManager: ResourceManager) { - suspend fun create(slippageConfig: SlippageConfig): SlippageFieldValidator { + fun create(slippageConfig: SlippageConfig): SlippageFieldValidator { return SlippageFieldValidator(slippageConfig, resourceManager) } } @@ -22,16 +23,18 @@ class SlippageFieldValidator( override suspend fun validate(input: String): FieldValidationResult { val value = input.toPercent() + return when { input.isEmpty() -> FieldValidationResult.Ok + value == null -> FieldValidationResult.Ok value !in slippageConfig.minAvailableSlippage..slippageConfig.maxAvailableSlippage -> { FieldValidationResult.Error( resourceManager.getString( R.string.swap_slippage_error_not_in_available_range, - slippageConfig.minAvailableSlippage.format(), - slippageConfig.maxAvailableSlippage.format() + slippageConfig.minAvailableSlippage.formatPercents(), + slippageConfig.maxAvailableSlippage.formatPercents() ) ) } @@ -40,7 +43,7 @@ class SlippageFieldValidator( } } - private fun String.toPercent(): Percent? { - return toDoubleOrNull()?.let { Percent(it) } + private fun String.toPercent(): Fraction? { + return toDoubleOrNull()?.percents } } diff --git a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/main/SwapValidationFailureUi.kt b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/main/SwapValidationFailureUi.kt index c454653e05..4ed1423743 100644 --- a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/main/SwapValidationFailureUi.kt +++ b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/main/SwapValidationFailureUi.kt @@ -3,7 +3,7 @@ package io.novafoundation.nova.feature_swap_impl.presentation.main import io.novafoundation.nova.common.base.TitleAndMessage import io.novafoundation.nova.common.mixin.api.CustomDialogDisplayer import io.novafoundation.nova.common.resources.ResourceManager -import io.novafoundation.nova.common.utils.formatting.format +import io.novafoundation.nova.common.utils.formatting.formatPercents import io.novafoundation.nova.common.validation.TransformedFailure import io.novafoundation.nova.common.validation.ValidationFlowActions import io.novafoundation.nova.common.validation.ValidationStatus @@ -51,7 +51,7 @@ fun CoroutineScope.mapSwapValidationFailureToUI( is InvalidSlippage -> TitleAndMessage( resourceManager.getString(R.string.swap_invalid_slippage_failure_title), - resourceManager.getString(R.string.swap_invalid_slippage_failure_message, reason.minSlippage.format(), reason.maxSlippage.format()) + resourceManager.getString(R.string.swap_invalid_slippage_failure_message, reason.minSlippage.formatPercents(), reason.maxSlippage.formatPercents()) ).asDefault() is NewRateExceededSlippage -> TitleAndMessage( diff --git a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/options/SwapOptionsViewModel.kt b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/options/SwapOptionsViewModel.kt index 73cf3df7a4..a07a4fb7dc 100644 --- a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/options/SwapOptionsViewModel.kt +++ b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/options/SwapOptionsViewModel.kt @@ -6,10 +6,11 @@ import io.novafoundation.nova.common.presentation.DescriptiveButtonState import io.novafoundation.nova.common.presentation.DescriptiveButtonState.Disabled import io.novafoundation.nova.common.presentation.DescriptiveButtonState.Enabled import io.novafoundation.nova.common.resources.ResourceManager -import io.novafoundation.nova.common.utils.Percent +import io.novafoundation.nova.common.utils.Fraction +import io.novafoundation.nova.common.utils.Fraction.Companion.percents import io.novafoundation.nova.common.utils.flowOfAll -import io.novafoundation.nova.common.utils.formatting.format -import io.novafoundation.nova.common.utils.formatting.formatWithoutSymbol +import io.novafoundation.nova.common.utils.formatting.formatPercents +import io.novafoundation.nova.common.utils.mapList import io.novafoundation.nova.common.validation.FieldValidationResult import io.novafoundation.nova.common.view.bottomSheet.description.DescriptionBottomSheetLauncher import io.novafoundation.nova.feature_swap_api.domain.model.SlippageConfig @@ -75,17 +76,17 @@ class SwapOptionsViewModel( } val defaultSlippage = slippageConfig.map { it.defaultSlippage } - .map { it.format() } + .map { it.formatPercents() } val slippageTips = slippageConfig.map { it.slippageTips } - .map { it.map { it.format() } } + .mapList { it.formatPercents() } init { launch { val selectedSlippage = swapSettingsStateFlow.first().slippage val defaultSlippage = slippageConfig.first().defaultSlippage if (selectedSlippage != defaultSlippage) { - slippageInput.value = selectedSlippage.formatWithoutSymbol() + slippageInput.value = selectedSlippage.formatPercents(includeSymbol = false) } } } @@ -100,7 +101,7 @@ class SwapOptionsViewModel( fun tipClicked(index: Int) { launch { val slippageTips = slippageConfig.first().slippageTips - slippageInput.value = slippageTips[index].formatWithoutSymbol() + slippageInput.value = slippageTips[index].formatPercents(includeSymbol = false) } } @@ -120,12 +121,13 @@ class SwapOptionsViewModel( swapRouter.back() } - private suspend fun String.formatToPercent(): Percent? { + private suspend fun String.formatToPercent(): Fraction? { val defaultSlippage = slippageConfig.first().defaultSlippage + return if (isEmpty()) { defaultSlippage } else { - return this.toDoubleOrNull()?.let { Percent(it) } + return toDoubleOrNull()?.percents } } diff --git a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/state/RealSwapSettingsState.kt b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/state/RealSwapSettingsState.kt index e976682944..116d0b19e5 100644 --- a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/state/RealSwapSettingsState.kt +++ b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/state/RealSwapSettingsState.kt @@ -1,5 +1,6 @@ package io.novafoundation.nova.feature_swap_impl.presentation.state +import io.novafoundation.nova.common.utils.Fraction import io.novafoundation.nova.common.utils.Percent import io.novafoundation.nova.feature_swap_core_api.data.primitive.model.SwapDirection import io.novafoundation.nova.feature_swap_core_api.data.primitive.model.flip @@ -56,7 +57,7 @@ class RealSwapSettingsState( selectedOption.value = selectedOption.value.copy(amount = amount, swapDirection = swapDirection) } - override fun setSlippage(slippage: Percent) { + override fun setSlippage(slippage: Fraction) { selectedOption.value = selectedOption.value.copy(slippage = slippage) } From 9af28696860967b457b4460944c640d8f0d27ea4 Mon Sep 17 00:00:00 2001 From: Valentun Date: Tue, 29 Oct 2024 10:58:57 +0300 Subject: [PATCH 33/83] Filter out non-sufficient assets --- .../feature_swap_impl/di/SwapFeatureModule.kt | 4 +- .../domain/swap/RealSwapService.kt | 51 +++++++++++++------ .../blockhain/assets/balances/AssetBalance.kt | 2 +- .../balances/UnsupportedAssetBalance.kt | 2 +- .../equilibrium/EquilibriumAssetBalance.kt | 2 +- .../balances/evmErc20/EvmErc20AssetBalance.kt | 2 +- .../evmNative/EvmNativeAssetBalance.kt | 2 +- .../assets/balances/orml/OrmlAssetBalance.kt | 2 +- .../statemine/StatemineAssetBalance.kt | 4 +- .../balances/utility/NativeAssetBalance.kt | 2 +- .../nova/runtime/multiNetwork/ChainsById.kt | 13 +++++ .../chain/mappers/ChainMappersConstants.kt | 3 ++ .../chain/mappers/DomainToLocalChainMapper.kt | 3 +- .../chain/mappers/LocalToDomainChainMapper.kt | 3 +- .../runtime/multiNetwork/chain/model/Chain.kt | 3 +- 15 files changed, 70 insertions(+), 28 deletions(-) diff --git a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/di/SwapFeatureModule.kt b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/di/SwapFeatureModule.kt index b260a0dae9..ffd38e92df 100644 --- a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/di/SwapFeatureModule.kt +++ b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/di/SwapFeatureModule.kt @@ -61,6 +61,7 @@ class SwapFeatureModule { extrinsicServiceFactory: ExtrinsicService.Factory, defaultFeePaymentRegistry: FeePaymentProviderRegistry, tokenRepository: TokenRepository, + assetSourceRegistry: AssetSourceRegistry ): SwapService { return RealSwapService( assetConversionFactory = assetConversionFactory, @@ -72,7 +73,8 @@ class SwapFeatureModule { customFeeCapabilityFacade = customFeeCapabilityFacade, extrinsicServiceFactory = extrinsicServiceFactory, defaultFeePaymentProviderRegistry = defaultFeePaymentRegistry, - tokenRepository = tokenRepository + tokenRepository = tokenRepository, + assetSourceRegistry = assetSourceRegistry ) } diff --git a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/domain/swap/RealSwapService.kt b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/domain/swap/RealSwapService.kt index 694692bd27..e53dc99383 100644 --- a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/domain/swap/RealSwapService.kt +++ b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/domain/swap/RealSwapService.kt @@ -69,6 +69,7 @@ import io.novafoundation.nova.feature_swap_impl.data.assetExchange.ParentQuoterA import io.novafoundation.nova.feature_swap_impl.data.assetExchange.assetConversion.AssetConversionExchangeFactory import io.novafoundation.nova.feature_swap_impl.data.assetExchange.crossChain.CrossChainTransferAssetExchangeFactory import io.novafoundation.nova.feature_swap_impl.data.assetExchange.hydraDx.HydraDxExchangeFactory +import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.AssetSourceRegistry import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.types.Balance import io.novafoundation.nova.feature_wallet_api.domain.interfaces.TokenRepository import io.novafoundation.nova.feature_wallet_api.domain.model.Token @@ -83,10 +84,13 @@ import io.novafoundation.nova.runtime.ext.isUtility import io.novafoundation.nova.runtime.ext.utilityAsset import io.novafoundation.nova.runtime.ext.utilityAssetOf import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry +import io.novafoundation.nova.runtime.multiNetwork.ChainsById import io.novafoundation.nova.runtime.multiNetwork.asset import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain import io.novafoundation.nova.runtime.multiNetwork.chain.model.ChainId import io.novafoundation.nova.runtime.multiNetwork.chain.model.FullChainAssetId +import io.novafoundation.nova.runtime.multiNetwork.chainWithAssetOrNull +import io.novafoundation.nova.runtime.multiNetwork.chainsById import io.novasama.substrate_sdk_android.hash.isPositive import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers @@ -121,6 +125,7 @@ internal class RealSwapService( private val customFeeCapabilityFacade: CustomFeeCapabilityFacade, private val extrinsicServiceFactory: ExtrinsicService.Factory, private val defaultFeePaymentProviderRegistry: FeePaymentProviderRegistry, + private val assetSourceRegistry: AssetSourceRegistry, private val tokenRepository: TokenRepository, private val debug: Boolean = BuildConfig.DEBUG ) : SwapService { @@ -407,7 +412,7 @@ internal class RealSwapService( private suspend fun canPayFeeNodeFilter(computationScope: CoroutineScope): EdgeVisitFilter { return computationalCache.useCache(NODE_VISIT_FILTER, computationScope) { - CanPayFeeNodeVisitFilter(this) + CanPayFeeNodeVisitFilter(this, chainRegistry.chainsById()) } } @@ -751,27 +756,20 @@ internal class RealSwapService( /** * Check that it is possible to pay fees in moving asset */ - private inner class CanPayFeeNodeVisitFilter(val computationScope: CoroutineScope) : EdgeVisitFilter { + private inner class CanPayFeeNodeVisitFilter( + val computationScope: CoroutineScope, + val chainsById: ChainsById, + ) : EdgeVisitFilter { private val feePaymentCapabilityCache: MutableMap = mutableMapOf() - private suspend fun getFeeCustomFeeCapability(chainId: ChainId): FastLookupCustomFeeCapability? { - val fromCache = feePaymentCapabilityCache.getOrPut(chainId) { - createFastLookupFeeCapability(chainId, computationScope).boxNullable() - } - - return fromCache.unboxNullable() - } - - private suspend fun createFastLookupFeeCapability(chainId: ChainId, computationScope: CoroutineScope): FastLookupCustomFeeCapability? { - val feePaymentRegistry = exchangeRegistry(computationScope).getFeePaymentRegistry() - return feePaymentRegistry.providerFor(chainId).fastLookupCustomFeeCapability() - } - override suspend fun shouldVisit(edge: SwapGraphEdge, pathPredecessor: SwapGraphEdge?): Boolean { // Utility payments and first path segments are always allowed if (edge.from.isUtility || pathPredecessor == null) return true + // Destination asset must be sufficient + if (!isSufficient(edge.to)) return false + // Edge might request us to ignore the default requirement based on its direct predecessor if (edge.shouldIgnoreFeeRequirementAfter(pathPredecessor)) return true @@ -780,6 +778,29 @@ internal class RealSwapService( return feeCapability != null && feeCapability.canPayFeeInNonUtilityToken(edge.from.assetId) && edge.canPayNonNativeFeesInIntermediatePosition() } + + private fun isSufficient(fullChainAssetId: FullChainAssetId): Boolean { + val (chain, chainAsset) = chainsById.chainWithAssetOrNull(fullChainAssetId) ?: return false + val balance = assetSourceRegistry.sourceFor(chainAsset).balance + return balance.isSelfSufficient(chainAsset).also { isSufficient -> + if (!isSufficient) { + Log.d("Swaps", "${chainAsset.symbol} (${chain.name} is not sufficient)") + } + } + } + + private suspend fun getFeeCustomFeeCapability(chainId: ChainId): FastLookupCustomFeeCapability? { + val fromCache = feePaymentCapabilityCache.getOrPut(chainId) { + createFastLookupFeeCapability(chainId, computationScope).boxNullable() + } + + return fromCache.unboxNullable() + } + + private suspend fun createFastLookupFeeCapability(chainId: ChainId, computationScope: CoroutineScope): FastLookupCustomFeeCapability? { + val feePaymentRegistry = exchangeRegistry(computationScope).getFeePaymentRegistry() + return feePaymentRegistry.providerFor(chainId).fastLookupCustomFeeCapability() + } } private object NULL diff --git a/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/data/network/blockhain/assets/balances/AssetBalance.kt b/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/data/network/blockhain/assets/balances/AssetBalance.kt index 59f6d75ce6..7a4fcdb14b 100644 --- a/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/data/network/blockhain/assets/balances/AssetBalance.kt +++ b/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/data/network/blockhain/assets/balances/AssetBalance.kt @@ -39,7 +39,7 @@ interface AssetBalance { subscriptionBuilder: SharedRequestsBuilder ): Flow<*> = emptyFlow() - suspend fun isSelfSufficient(chainAsset: Chain.Asset): Boolean + fun isSelfSufficient(chainAsset: Chain.Asset): Boolean suspend fun existentialDeposit( chain: Chain, diff --git a/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/data/network/blockchain/assets/balances/UnsupportedAssetBalance.kt b/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/data/network/blockchain/assets/balances/UnsupportedAssetBalance.kt index 4e27ec5162..1ed459371d 100644 --- a/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/data/network/blockchain/assets/balances/UnsupportedAssetBalance.kt +++ b/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/data/network/blockchain/assets/balances/UnsupportedAssetBalance.kt @@ -20,7 +20,7 @@ class UnsupportedAssetBalance : AssetBalance { subscriptionBuilder: SharedRequestsBuilder ) = unsupported() - override suspend fun isSelfSufficient(chainAsset: Chain.Asset) = unsupported() + override fun isSelfSufficient(chainAsset: Chain.Asset) = unsupported() override suspend fun existentialDeposit(chain: Chain, chainAsset: Chain.Asset) = unsupported() diff --git a/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/data/network/blockchain/assets/balances/equilibrium/EquilibriumAssetBalance.kt b/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/data/network/blockchain/assets/balances/equilibrium/EquilibriumAssetBalance.kt index 7939a9278e..e306db081c 100644 --- a/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/data/network/blockchain/assets/balances/equilibrium/EquilibriumAssetBalance.kt +++ b/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/data/network/blockchain/assets/balances/equilibrium/EquilibriumAssetBalance.kt @@ -89,7 +89,7 @@ class EquilibriumAssetBalance( } } - override suspend fun isSelfSufficient(chainAsset: Chain.Asset): Boolean { + override fun isSelfSufficient(chainAsset: Chain.Asset): Boolean { return true } diff --git a/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/data/network/blockchain/assets/balances/evmErc20/EvmErc20AssetBalance.kt b/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/data/network/blockchain/assets/balances/evmErc20/EvmErc20AssetBalance.kt index 3603b0626f..eef9eeaa49 100644 --- a/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/data/network/blockchain/assets/balances/evmErc20/EvmErc20AssetBalance.kt +++ b/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/data/network/blockchain/assets/balances/evmErc20/EvmErc20AssetBalance.kt @@ -57,7 +57,7 @@ class EvmErc20AssetBalance( return emptyFlow() } - override suspend fun isSelfSufficient(chainAsset: Chain.Asset): Boolean { + override fun isSelfSufficient(chainAsset: Chain.Asset): Boolean { return true } diff --git a/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/data/network/blockchain/assets/balances/evmNative/EvmNativeAssetBalance.kt b/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/data/network/blockchain/assets/balances/evmNative/EvmNativeAssetBalance.kt index 14c8bb8075..c29dd157e0 100644 --- a/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/data/network/blockchain/assets/balances/evmNative/EvmNativeAssetBalance.kt +++ b/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/data/network/blockchain/assets/balances/evmNative/EvmNativeAssetBalance.kt @@ -42,7 +42,7 @@ class EvmNativeAssetBalance( return emptyFlow() } - override suspend fun isSelfSufficient(chainAsset: Chain.Asset): Boolean { + override fun isSelfSufficient(chainAsset: Chain.Asset): Boolean { return true } diff --git a/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/data/network/blockchain/assets/balances/orml/OrmlAssetBalance.kt b/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/data/network/blockchain/assets/balances/orml/OrmlAssetBalance.kt index 8bad79c524..e5778219d3 100644 --- a/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/data/network/blockchain/assets/balances/orml/OrmlAssetBalance.kt +++ b/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/data/network/blockchain/assets/balances/orml/OrmlAssetBalance.kt @@ -59,7 +59,7 @@ class OrmlAssetBalance( } } - override suspend fun isSelfSufficient(chainAsset: Chain.Asset): Boolean { + override fun isSelfSufficient(chainAsset: Chain.Asset): Boolean { return true } diff --git a/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/data/network/blockchain/assets/balances/statemine/StatemineAssetBalance.kt b/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/data/network/blockchain/assets/balances/statemine/StatemineAssetBalance.kt index 7e61a0840d..a383f27b0c 100644 --- a/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/data/network/blockchain/assets/balances/statemine/StatemineAssetBalance.kt +++ b/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/data/network/blockchain/assets/balances/statemine/StatemineAssetBalance.kt @@ -52,8 +52,8 @@ class StatemineAssetBalance( return emptyFlow() } - override suspend fun isSelfSufficient(chainAsset: Chain.Asset): Boolean { - return queryAssetDetails(chainAsset).isSufficient + override fun isSelfSufficient(chainAsset: Chain.Asset): Boolean { + return chainAsset.requireStatemine().isSufficient } override suspend fun existentialDeposit(chain: Chain, chainAsset: Chain.Asset): BigInteger { diff --git a/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/data/network/blockchain/assets/balances/utility/NativeAssetBalance.kt b/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/data/network/blockchain/assets/balances/utility/NativeAssetBalance.kt index a0ca630954..5749de0999 100644 --- a/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/data/network/blockchain/assets/balances/utility/NativeAssetBalance.kt +++ b/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/data/network/blockchain/assets/balances/utility/NativeAssetBalance.kt @@ -93,7 +93,7 @@ class NativeAssetBalance( } } - override suspend fun isSelfSufficient(chainAsset: Chain.Asset): Boolean { + override fun isSelfSufficient(chainAsset: Chain.Asset): Boolean { return true } diff --git a/runtime/src/main/java/io/novafoundation/nova/runtime/multiNetwork/ChainsById.kt b/runtime/src/main/java/io/novafoundation/nova/runtime/multiNetwork/ChainsById.kt index 76801ec028..14926a63cd 100644 --- a/runtime/src/main/java/io/novafoundation/nova/runtime/multiNetwork/ChainsById.kt +++ b/runtime/src/main/java/io/novafoundation/nova/runtime/multiNetwork/ChainsById.kt @@ -3,6 +3,7 @@ package io.novafoundation.nova.runtime.multiNetwork import io.novafoundation.nova.common.utils.removeHexPrefix import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain import io.novafoundation.nova.runtime.multiNetwork.chain.model.ChainId +import io.novafoundation.nova.runtime.multiNetwork.chain.model.FullChainAssetId @JvmInline value class ChainsById(val value: Map) : Map by value { @@ -16,3 +17,15 @@ value class ChainsById(val value: Map) : Map by inline fun Map.asChainsById(): ChainsById { return ChainsById(this) } + + +fun ChainsById.assetOrNull(id: FullChainAssetId): Chain.Asset? { + return get(id.chainId)?.assetsById?.get(id.assetId) +} + +fun ChainsById.chainWithAssetOrNull(id: FullChainAssetId): ChainWithAsset? { + val chain = get(id.chainId) ?: return null + val asset = chain.assetsById[id.assetId] ?: return null + + return ChainWithAsset(chain, asset) +} diff --git a/runtime/src/main/java/io/novafoundation/nova/runtime/multiNetwork/chain/mappers/ChainMappersConstants.kt b/runtime/src/main/java/io/novafoundation/nova/runtime/multiNetwork/chain/mappers/ChainMappersConstants.kt index 8212be4ddd..15daf0aff9 100644 --- a/runtime/src/main/java/io/novafoundation/nova/runtime/multiNetwork/chain/mappers/ChainMappersConstants.kt +++ b/runtime/src/main/java/io/novafoundation/nova/runtime/multiNetwork/chain/mappers/ChainMappersConstants.kt @@ -13,6 +13,9 @@ const val ASSET_EQUILIBRIUM_ON_CHAIN_ID = "assetId" const val STATEMINE_EXTRAS_ID = "assetId" const val STATEMINE_EXTRAS_PALLET_NAME = "palletName" +const val STATEMINE_IS_SUFFICIENT = "isSufficient" + +const val STATEMINE_IS_SUFFICIENT_DEFAULT = false const val ORML_EXTRAS_CURRENCY_ID_SCALE = "currencyIdScale" const val ORML_EXTRAS_CURRENCY_TYPE = "currencyIdType" diff --git a/runtime/src/main/java/io/novafoundation/nova/runtime/multiNetwork/chain/mappers/DomainToLocalChainMapper.kt b/runtime/src/main/java/io/novafoundation/nova/runtime/multiNetwork/chain/mappers/DomainToLocalChainMapper.kt index fb90529a77..b265b1e791 100644 --- a/runtime/src/main/java/io/novafoundation/nova/runtime/multiNetwork/chain/mappers/DomainToLocalChainMapper.kt +++ b/runtime/src/main/java/io/novafoundation/nova/runtime/multiNetwork/chain/mappers/DomainToLocalChainMapper.kt @@ -42,7 +42,8 @@ fun mapChainAssetTypeToRaw(type: Chain.Asset.Type): Pair ASSET_STATEMINE to mapOf( STATEMINE_EXTRAS_ID to mapStatemineAssetIdToRaw(type.id), - STATEMINE_EXTRAS_PALLET_NAME to type.palletName + STATEMINE_EXTRAS_PALLET_NAME to type.palletName, + STATEMINE_IS_SUFFICIENT to type.isSufficient ) is Chain.Asset.Type.Orml -> ASSET_ORML to mapOf( diff --git a/runtime/src/main/java/io/novafoundation/nova/runtime/multiNetwork/chain/mappers/LocalToDomainChainMapper.kt b/runtime/src/main/java/io/novafoundation/nova/runtime/multiNetwork/chain/mappers/LocalToDomainChainMapper.kt index 81ce937317..7ac5aaca5c 100644 --- a/runtime/src/main/java/io/novafoundation/nova/runtime/multiNetwork/chain/mappers/LocalToDomainChainMapper.kt +++ b/runtime/src/main/java/io/novafoundation/nova/runtime/multiNetwork/chain/mappers/LocalToDomainChainMapper.kt @@ -62,8 +62,9 @@ private fun mapChainAssetTypeFromRaw(type: String?, typeExtras: Map { diff --git a/runtime/src/main/java/io/novafoundation/nova/runtime/multiNetwork/chain/model/Chain.kt b/runtime/src/main/java/io/novafoundation/nova/runtime/multiNetwork/chain/model/Chain.kt index 5e38120679..464f1d0f26 100644 --- a/runtime/src/main/java/io/novafoundation/nova/runtime/multiNetwork/chain/model/Chain.kt +++ b/runtime/src/main/java/io/novafoundation/nova/runtime/multiNetwork/chain/model/Chain.kt @@ -91,7 +91,8 @@ data class Chain( data class Statemine( val id: StatemineAssetId, - val palletName: String? + val palletName: String?, + val isSufficient: Boolean, ) : Type() data class Orml( From 17a9c617911868782167d9c591ca233e2ff95708 Mon Sep 17 00:00:00 2001 From: Valentun Date: Tue, 29 Oct 2024 18:51:16 +0300 Subject: [PATCH 34/83] Remove GenericFee and DecimalFee --- .../feature_account_api/data/model/Fee.kt | 28 ++--- .../feature_assets/di/modules/SendModule.kt | 8 +- .../domain/send/SendInteractor.kt | 87 ++++----------- .../domain/send/model/TransferFeeModel.kt | 4 +- .../send/TransferAssetValidationFailureUi.kt | 6 +- .../send/amount/SelectSendViewModel.kt | 35 +++--- .../send/confirm/ConfirmSendViewModel.kt | 39 +++---- .../ContributeValidationPayload.kt | 4 +- .../contribute/validations/Definitions.kt | 4 +- .../custom/moonbeam/MoonbeamTermsPayload.kt | 4 +- .../confirm/ConfirmContributeViewModel.kt | 2 +- .../terms/MoonbeamCrowdloanTermsViewModel.kt | 9 +- .../select/CrowdloanContributeViewModel.kt | 9 +- .../domain/sign/Validations.kt | 22 +--- .../domain/sign/evm/EvmSignInteractor.kt | 6 +- .../signExtrinsic/ExternaSignViewModel.kt | 8 +- ...ChooseDelegationAmountValidationPayload.kt | 4 +- .../RemoveVotesValidationPayload.kt | 4 +- .../RevokeDelegationValidationPayload.kt | 4 +- .../UnlockReferendumValidationPayload.kt | 4 +- .../common/VoteValidationPayload.kt | 4 +- .../VoteReferendaValidationPayload.kt | 4 +- .../VoteTinderGovValidationPayload.kt | 4 +- .../NewDelegationChooseAmountViewModel.kt | 4 +- .../confirm/NewDelegationConfirmViewModel.kt | 2 +- .../removeVotes/RemoveVotesViewModel.kt | 4 +- .../RevokeDelegationConfirmViewModel.kt | 4 +- .../confirm/ConfirmReferendumVoteViewModel.kt | 6 +- .../vote/setup/common/SetupVoteViewModel.kt | 4 +- .../confirm/ConfirmTinderGovVoteViewModel.kt | 20 ++-- .../ConfirmGovernanceUnlockViewModel.kt | 4 +- .../validations/RebagValidationPayload.kt | 4 +- .../validation/ProfitableActionValidation.kt | 11 +- ...ominationPoolsBondMoreValidationPayload.kt | 4 +- ...ationPoolsClaimRewardsValidationPayload.kt | 4 +- .../PoolAvailableBalanceValidation.kt | 8 +- .../NominationPoolsRedeemValidationPayload.kt | 4 +- .../NominationPoolsUnbondValidationPayload.kt | 4 +- ...ParachainStakingRebondValidationPayload.kt | 4 +- ...ParachainStakingRedeemValidationPayload.kt | 4 +- .../StartParachainStakingValidationPayload.kt | 4 +- ...ParachainStakingUnbondValidationPayload.kt | 4 +- .../validations/FirstTaskCanExecute.kt | 7 +- .../YieldBoostValidationPayload.kt | 4 +- .../AvailableBalanceGapValidation.kt | 2 +- .../StartMultiStakingValidationPayload.kt | 4 +- .../bond/BondMoreValidationPayload.kt | 4 +- .../SetControllerValidationPayload.kt | 4 +- .../add/AddStakingProxyValidationPayload.kt | 4 +- .../RemoveStakingProxyValidationPayload.kt | 4 +- .../validations/payout/MakePayoutPayload.kt | 4 +- .../rebond/RebondValidationPayload.kt | 4 +- .../reedeem/RedeemValidationPayload.kt | 4 +- .../RewardDestinationValidationPayload.kt | 4 +- .../validations/setup/SetupStakingPayload.kt | 4 +- .../unbond/UnbondValidationPayload.kt | 4 +- .../bagList/rebag/RebagViewModel.kt | 4 +- ...NominationPoolsConfirmBondMoreViewModel.kt | 6 +- .../NominationPoolsSetupBondMoreViewModel.kt | 4 +- .../NominationPoolsClaimRewardsViewModel.kt | 4 +- .../redeem/NominationPoolsRedeemViewModel.kt | 4 +- .../NominationPoolsConfirmUnbondViewModel.kt | 2 +- .../NominationPoolsSetupUnbondViewModel.kt | 4 +- .../rebond/ParachainStakingRebondViewModel.kt | 4 +- .../redeem/ParachainStakingRedeemViewModel.kt | 4 +- .../ConfirmStartParachainStakingViewModel.kt | 2 +- .../setup/StartParachainStakingViewModel.kt | 8 +- .../ParachainStakingUnbondConfirmViewModel.kt | 2 +- .../setup/ParachainStakingUnbondViewModel.kt | 8 +- .../confirm/YieldBoostConfirmViewModel.kt | 2 +- .../setup/SetupYieldBoostViewModel.kt | 8 +- .../payouts/confirm/ConfirmPayoutViewModel.kt | 4 +- .../bond/confirm/ConfirmBondMoreViewModel.kt | 2 +- .../bond/select/SelectBondMoreViewModel.kt | 4 +- .../confirm/ConfirmSetControllerViewModel.kt | 2 +- .../controller/set/SetControllerViewModel.kt | 4 +- .../ConfirmAddStakingProxyViewModel.kt | 8 +- .../proxy/add/set/AddStakingProxyViewModel.kt | 4 +- .../ConfirmRemoveStakingProxyViewModel.kt | 4 +- .../rebond/confirm/ConfirmRebondViewModel.kt | 4 +- .../rebond/custom/CustomRebondViewModel.kt | 4 +- .../staking/redeem/RedeemViewModel.kt | 4 +- .../ConfirmRewardDestinationViewModel.kt | 2 +- .../SelectRewardDestinationViewModel.kt | 8 +- .../confirm/ConfirmMultiStakingViewModel.kt | 2 +- .../SetupAmountMultiStakingViewModel.kt | 4 +- .../unbond/confirm/ConfirmUnbondViewModel.kt | 2 +- .../unbond/select/SelectUnbondViewModel.kt | 4 +- .../ConfirmChangeValidatorsViewModel.kt | 4 +- .../feature_swap_api/domain/model/SwapFee.kt | 20 ++-- .../domain/model/SwapQuote.kt | 6 -- .../CrossChainTransferAssetExchange.kt | 6 +- .../domain/interactor/SwapInteractor.kt | 4 +- ...onsideringNonSufficientAssetsValidation.kt | 2 +- .../validation/SwapPayloadValidation.kt | 17 +-- .../domain/validation/SwapValidations.kt | 6 +- ...tBalanceToPayFeeConsideringEDValidation.kt | 2 +- .../SwapFeeSufficientBalanceValidation.kt | 2 +- .../confirmation/SwapConfirmationViewModel.kt | 6 +- .../main/SwapMainSettingsViewModel.kt | 9 +- .../main/SwapValidationFailureUi.kt | 4 +- .../maxAction/MaxActionProviderFactory.kt | 4 +- .../feature_wallet_api/data/mappers/Fee.kt | 29 ++--- .../tranfers/AssetTransferValidations.kt | 24 ++--- .../assets/tranfers/AssetTransfers.kt | 8 +- .../network/crosschain/CrossChainFeeModel.kt | 14 +-- .../interfaces/CrossChainTransfersUseCase.kt | 5 +- .../domain/model/CrossChainFee.kt | 9 +- .../domain/model/OriginFee.kt | 34 ++---- .../EnoughAmountToTransferValidation.kt | 27 +++-- .../EnoughBalanceToStayAboveEDValidation.kt | 19 ++-- .../ExistentialDepositValidation.kt | 30 ++---- .../domain/validation/FeeChangeValidation.kt | 66 ++++-------- .../HasEnoughFreeBalanceValidation.kt | 9 +- .../domain/validation/Producers.kt | 11 +- .../maxAction/FeeAwareMaxActionProvider.kt | 47 +------- .../maxAction/MaxActionProvider.kt | 13 +-- .../presentation/mixin/fee/FeeLoaderMixin.kt | 101 +++++------------- .../presentation/mixin/fee/FeeParcelModel.kt | 40 +++---- .../provider/ChangeableFeeLoaderProvider.kt | 31 +----- .../fee/provider/FeeLoaderProviderFactory.kt | 26 +---- .../fee/provider/GenericFeeLoaderProvider.kt | 14 +-- .../presentation/model/FeeModel.kt | 44 +------- .../evmErc20/EvmErc20AssetTransfers.kt | 2 +- .../evmNative/EvmNativeAssetTransfers.kt | 2 +- .../assets/transfers/validations/Common.kt | 16 ++- .../crosschain/RealCrossChainTransactor.kt | 3 +- .../crosschain/RealCrossChainWeigher.kt | 8 +- ...pBelowEdWhenPayingDeliveryFeeValidation.kt | 8 +- .../validations/CrossChainFeeValidation.kt | 9 +- .../domain/RealCrossChainTransfersUseCase.kt | 22 ++-- 131 files changed, 507 insertions(+), 851 deletions(-) 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 5a5b91e8a1..68115ce069 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 @@ -1,11 +1,13 @@ package io.novafoundation.nova.feature_account_api.data.model +import io.novafoundation.nova.common.utils.amountFromPlanks import io.novafoundation.nova.feature_account_api.data.extrinsic.SubmissionOrigin import io.novafoundation.nova.feature_account_api.data.fee.FeePaymentCurrency import io.novafoundation.nova.runtime.ext.fullId import io.novafoundation.nova.runtime.ext.utilityAsset import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain import io.novasama.substrate_sdk_android.runtime.AccountId +import java.math.BigDecimal import java.math.BigInteger // TODO rename FeeBase -> Fee and use SubmissionFee everywhere Fee is currently used @@ -35,9 +37,8 @@ interface FeeBase { val asset: Chain.Asset } -fun Fee.replacePlanks(newPlanks: BigInteger): Fee { - return SubstrateFee(newPlanks, submissionOrigin, asset) -} +val FeeBase.decimalAmount: BigDecimal + get() = amount.amountFromPlanks(asset.precision) data class EvmFee( val gasLimit: BigInteger, @@ -64,7 +65,18 @@ val Fee.executingAccountPaysFee: Boolean get() = submissionOrigin.executingAccount.contentEquals(submissionFeesPayer) val Fee.amountByExecutingAccount: BigInteger - get() = amount.asAmountByExecutingAccount + get() = if (executingAccountPaysFee) { + amount + } else { + BigInteger.ZERO + } + +val Fee.decimalAmountByExecutingAccount: BigDecimal + get() = if (executingAccountPaysFee) { + decimalAmount + } else { + BigDecimal.ZERO + } fun List.totalAmount(chainAsset: Chain.Asset): BigInteger { return sumOf { it.getAmount(chainAsset) } @@ -96,14 +108,6 @@ fun FeeBase.getAmount(expectedAsset: Chain.Asset): BigInteger { return if (expectedAsset.fullId == asset.fullId) amount else BigInteger.ZERO } -context(Fee) -val BigInteger.asAmountByExecutingAccount: BigInteger - get() = if (executingAccountPaysFee) { - this - } else { - BigInteger.ZERO - } - fun FeePaymentCurrency.toFeePaymentAsset(chain: Chain): Chain.Asset { return when (this) { is FeePaymentCurrency.Asset -> asset diff --git a/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/di/modules/SendModule.kt b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/di/modules/SendModule.kt index 72b9560a2d..476eb134f3 100644 --- a/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/di/modules/SendModule.kt +++ b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/di/modules/SendModule.kt @@ -8,7 +8,7 @@ import io.novafoundation.nova.feature_assets.domain.send.SendInteractor import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.AssetSourceRegistry 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.interfaces.CrossChainTransfersUseCase import io.novafoundation.nova.feature_wallet_api.domain.interfaces.WalletRepository import io.novafoundation.nova.runtime.repository.ParachainInfoRepository @@ -21,17 +21,17 @@ class SendModule { walletRepository: WalletRepository, assetSourceRegistry: AssetSourceRegistry, crossChainTransfersRepository: CrossChainTransfersRepository, - crossChainWeigher: CrossChainWeigher, crossChainTransactor: CrossChainTransactor, parachainInfoRepository: ParachainInfoRepository, extrinsicService: ExtrinsicService, + crossChainTransfersUseCase: CrossChainTransfersUseCase ) = SendInteractor( walletRepository, assetSourceRegistry, - crossChainWeigher, crossChainTransactor, crossChainTransfersRepository, parachainInfoRepository, - extrinsicService + crossChainTransfersUseCase, + extrinsicService, ) } 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 a0bfaf6b62..a85599d964 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,34 +1,22 @@ package io.novafoundation.nova.feature_assets.domain.send import io.novafoundation.nova.feature_account_api.data.extrinsic.ExtrinsicService -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.amountByExecutingAccount -import io.novafoundation.nova.feature_account_api.domain.model.requireAccountIdIn +import io.novafoundation.nova.feature_account_api.data.model.FeeBase +import io.novafoundation.nova.feature_account_api.data.model.decimalAmount 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.CrossChainTransfersUseCase 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.domain.model.networkFeePart -import io.novafoundation.nova.runtime.ext.commissionAsset 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 io.novasama.substrate_sdk_android.runtime.AccountId import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext @@ -36,51 +24,26 @@ import kotlinx.coroutines.withContext class SendInteractor( private val walletRepository: WalletRepository, private val assetSourceRegistry: AssetSourceRegistry, - private val crossChainWeigher: CrossChainWeigher, private val crossChainTransactor: CrossChainTransactor, private val crossChainTransfersRepository: CrossChainTransfersRepository, private val parachainInfoRepository: ParachainInfoRepository, + private val crossChainTransfersUseCase: CrossChainTransfersUseCase, private val extrinsicService: ExtrinsicService, ) { - // TODO wallet - suspend fun getRecipients(query: String, chainId: ChainId): RecipientSearchResult { -// val metaAccount = accountRepository.getSelectedMetaAccount() -// val chain = chainRegistry.getChain(chainId) -// val accountId = metaAccount.accountIdIn(chain)!! -// -// val contacts = walletRepository.getContacts(accountId, chain, query) -// val myAccounts = accountRepository.getMyAccounts(query, chain.id) -// -// return withContext(Dispatchers.Default) { -// val contactsWithoutMyAccounts = contacts - myAccounts.map { it.address } -// val myAddressesWithoutCurrent = myAccounts - metaAccount -// -// RecipientSearchResult( -// myAddressesWithoutCurrent.toList().map { mapAccountToWalletAccount(chain, it) }, -// contactsWithoutMyAccounts.toList() -// ) -// } - - return RecipientSearchResult( - myAccounts = emptyList(), - contacts = emptyList() - ) - } - - suspend fun getFee(amount: Balance, transfer: AssetTransfer, coroutineScope: CoroutineScope): TransferFeeModel = withContext(Dispatchers.Default) { + suspend fun getFee(transfer: AssetTransfer, coroutineScope: CoroutineScope): TransferFeeModel = withContext(Dispatchers.Default) { if (transfer.isCrossChain) { - val config = crossChainTransfersRepository.getConfiguration().configurationFor(transfer)!! - - val originFee = with(crossChainTransactor) { - extrinsicService.estimateOriginFee(config, transfer) + val fees = with(crossChainTransfersUseCase) { + extrinsicService.estimateFee(transfer, cachingScope = null) } - val crossChainFeeModel = crossChainWeigher.estimateFee(amount, config) - val deliveryPartFee = getDeliveryFee(transfer.originChain, crossChainFeeModel.paidByOrigin, transfer.senderAccountId()) - val originFeeWithSenderPart = OriginFee(originFee, deliveryPartFee, transfer.commissionAssetToken.configuration) + val originFee = OriginFee( + submissionFee = fees.submissionFee, + deliveryFee = fees.deliveryFee, + chainAsset = transfer.commissionAssetToken.configuration + ) - TransferFeeModel(originFeeWithSenderPart, crossChainFeeModel.toSubstrateFee(transfer)) + TransferFeeModel(originFee, fees.executionFee) } else { val nativeFee = getAssetTransfers(transfer).calculateFee(transfer, coroutineScope = coroutineScope) TransferFeeModel( @@ -92,23 +55,23 @@ class SendInteractor( suspend fun performTransfer( transfer: WeightedAssetTransfer, - originFee: OriginDecimalFee, - crossChainFee: Fee?, + originFee: OriginFee, + crossChainFee: FeeBase?, coroutineScope: CoroutineScope ): Result<*> = withContext(Dispatchers.Default) { if (transfer.isCrossChain) { val config = crossChainTransfersRepository.getConfiguration().configurationFor(transfer)!! with(crossChainTransactor) { - extrinsicService.performTransfer(config, transfer, crossChainFee!!.amountByExecutingAccount) + extrinsicService.performTransfer(config, transfer, crossChainFee!!.amount) } } else { - val networkFee = originFee.networkFeePart() + val networkFee = originFee.submissionFee getAssetTransfers(transfer).performTransfer(transfer, coroutineScope) .onSuccess { submission -> // Insert used fee regardless of who paid it - walletRepository.insertPendingTransfer(submission.hash, transfer, networkFee.networkFeeDecimalAmount) + walletRepository.insertPendingTransfer(submission.hash, transfer, networkFee.decimalAmount) } } } @@ -129,18 +92,4 @@ class SendInteractor( destinationChain = transfer.destinationChain, destinationParaId = parachainInfoRepository.paraId(transfer.destinationChain.id) ) - - private fun getDeliveryFee(chain: Chain, amount: Balance, accountId: AccountId): Fee { - return SubstrateFee( - amount = amount, - submissionOrigin = SubmissionOrigin.singleOrigin(accountId), - asset = chain.commissionAsset - ) - } - - private fun CrossChainFeeModel.toSubstrateFee(transfer: AssetTransfer) = SubstrateFee( - amount = paidFromHoldingRegister, - submissionOrigin = SubmissionOrigin.singleOrigin(transfer.sender.requireAccountIdIn(transfer.originChain)), - asset = transfer.originChain.commissionAsset // TODO: Support custom assets for xcm transfers - ) } 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 index 2aecb37e46..b8f69bc67d 100644 --- 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 @@ -1,9 +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_account_api.data.model.FeeBase import io.novafoundation.nova.feature_wallet_api.domain.model.OriginFee class TransferFeeModel( val originFee: OriginFee, - val crossChainFee: Fee? + val crossChainFee: FeeBase? ) 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 34478860fb..9cd65474fb 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,7 +10,7 @@ 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.model.OriginFee 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.formatPlanks @@ -24,7 +24,7 @@ fun CoroutineScope.mapAssetTransferValidationFailureToUI( resourceManager: ResourceManager, status: ValidationStatus.NotValid, actions: ValidationFlowActions<*>, - feeLoaderMixin: GenericFeeLoaderMixin.Presentation, + feeLoaderMixin: GenericFeeLoaderMixin.Presentation, ): TransformedFailure? { return when (val reason = status.reason) { is AssetTransferValidationFailure.DeadRecipient.InCommissionAsset -> Default( @@ -120,7 +120,7 @@ fun autoFixSendValidationPayload( is AssetTransferValidationFailure.FeeChangeDetected -> payload.copy( transfer = payload.transfer.copy( - decimalFee = failureReason.payload.newFee + fee = failureReason.payload.newFee ) ) 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 4618abc630..49e8a2f848 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 @@ -40,19 +40,16 @@ import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.t import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.tranfers.commissionAsset 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.OriginGenericFee -import io.novafoundation.nova.feature_wallet_api.domain.model.planksFromAmount +import io.novafoundation.nova.feature_wallet_api.domain.model.OriginFee 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.FeeStatus +import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.GenericFeeLoaderMixin import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.GenericFeeLoaderMixin.Configuration -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.awaitFee +import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.awaitOptionalFee import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.commissionAsset -import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.create -import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.createGenericChangeableFee +import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.createChangeable +import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.createSimple import io.novafoundation.nova.feature_wallet_api.presentation.model.AssetPayload import io.novafoundation.nova.feature_wallet_api.presentation.model.mapAmountToAmountModel import io.novafoundation.nova.runtime.ext.isEnabled @@ -87,7 +84,7 @@ class SelectSendViewModel( private val crossChainTransfersUseCase: CrossChainTransfersUseCase, private val accountRepository: AccountRepository, actionAwaitableMixinFactory: ActionAwaitableMixin.Factory, - feeLoaderMixinFactory: FeeLoaderMixin.Factory, + feeLoaderMixinFactory: GenericFeeLoaderMixin.Factory, selectedAccountUseCase: SelectedAccountUseCase, addressInputMixinFactory: AddressInputMixinFactory, amountChooserMixinFactory: AmountChooserMixin.Factory, @@ -157,7 +154,7 @@ class SelectSendViewModel( private val originAssetFlow = originChainAsset.flatMapLatest(interactor::assetFlow) .shareInBackground() - val originFeeMixin = feeLoaderMixinFactory.createGenericChangeableFee( + val originFeeMixin = feeLoaderMixinFactory.createChangeable( originAssetFlow, coroutineScope, configuration = Configuration( @@ -167,7 +164,7 @@ class SelectSendViewModel( ) ) - val crossChainFeeMixin = feeLoaderMixinFactory.create(originAssetFlow) + val crossChainFeeMixin = feeLoaderMixinFactory.createSimple(originAssetFlow) val amountChooserMixin: AmountChooserMixin.Presentation = amountChooserMixinFactory.create( scope = this, @@ -198,8 +195,8 @@ class SelectSendViewModel( fun nextClicked() = launch { sendInProgressFlow.value = true - val originFee = originFeeMixin.awaitDecimalFee() - val crossChainFee = crossChainFeeMixin.awaitOptionalDecimalFee() + val originFee = originFeeMixin.awaitFee() + val crossChainFee = crossChainFeeMixin.awaitOptionalFee() val transfer = buildTransfer( origin = originChainWithAsset.first(), @@ -362,14 +359,10 @@ class SelectSendViewModel( address = address ) - val planks = originAsset.asset.planksFromAmount(amount) + val transferFeeModel = sendInteractor.getFee(assetTransfer, viewModelScope) - val transferFeeModel = sendInteractor.getFee(planks, assetTransfer, viewModelScope) - val originFee = SimpleGenericFee(transferFeeModel.originFee) - val crossChainFee = transferFeeModel.crossChainFee?.let { SimpleFee(it) } - - originFeeMixin.setFee(originFee) - crossChainFeeMixin.setFee(crossChainFee) + originFeeMixin.setFee(transferFeeModel.originFee) + crossChainFeeMixin.setFee(transferFeeModel.crossChainFee) } catch (e: Exception) { e.printStackTrace() 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 f90a5a0067..ec5c896d9c 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 @@ -13,6 +13,7 @@ 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.FeeBase import io.novafoundation.nova.feature_account_api.domain.interfaces.SelectedAccountUseCase import io.novafoundation.nova.feature_account_api.domain.model.requireAddressIn import io.novafoundation.nova.feature_account_api.presenatation.account.AddressDisplayUseCase @@ -33,19 +34,14 @@ import io.novafoundation.nova.feature_assets.presentation.send.mapAssetTransferV 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.domain.model.OriginFee 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.awaitFee +import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.awaitOptionalFee import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.createGeneric +import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.createSimple 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 @@ -101,8 +97,8 @@ class ConfirmSendViewModel( .inBackground() .share() - val originFeeMixin = feeLoaderMixinFactory.createGeneric(commissionAssetFlow) - val crossChainFeeMixin = feeLoaderMixinFactory.create(assetFlow) + val originFeeMixin = feeLoaderMixinFactory.createGeneric(commissionAssetFlow) + val crossChainFeeMixin = feeLoaderMixinFactory.createSimple(assetFlow) val hintsMixin = hintsFactory.create(this) @@ -194,17 +190,14 @@ class ConfirmSendViewModel( private fun setupFee() = launch { launch { val assetTransfer = buildTransfer() - val planks = originAsset().planksFromAmount(assetTransfer.amount) originFeeMixin.invalidateFee() crossChainFeeMixin.invalidateFee() - val transferFeeModel = sendInteractor.getFee(planks, assetTransfer, viewModelScope) - val originFee = SimpleGenericFee(transferFeeModel.originFee) - val crossChainFee = transferFeeModel.crossChainFee?.let { SimpleFee(it) } + val transferFeeModel = sendInteractor.getFee(assetTransfer, viewModelScope) - originFeeMixin.setFee(originFee) - crossChainFeeMixin.setFee(crossChainFee) + originFeeMixin.setFee(transferFeeModel.originFee) + crossChainFeeMixin.setFee(transferFeeModel.crossChainFee) } } @@ -239,10 +232,10 @@ class ConfirmSendViewModel( private fun performTransfer( transfer: WeightedAssetTransfer, - originFee: OriginDecimalFee, - crossChainFee: DecimalFee? + originFee: OriginFee, + crossChainFee: FeeBase? ) = launch { - sendInteractor.performTransfer(transfer, originFee, crossChainFee?.genericFee?.networkFee, viewModelScope) + sendInteractor.performTransfer(transfer, originFee, crossChainFee, viewModelScope) .onSuccess { showMessage(resourceManager.getString(R.string.common_transaction_submitted)) @@ -267,7 +260,7 @@ class ConfirmSendViewModel( val chain = originChain() val chainAsset = originAsset() - val originFee = originFeeMixin.awaitDecimalFee() + val originFee = originFeeMixin.awaitFee() return AssetTransferPayload( transfer = WeightedAssetTransfer( @@ -279,12 +272,12 @@ class ConfirmSendViewModel( originChainAsset = chainAsset, amount = transferDraft.amount, commissionAssetToken = commissionAssetFlow.first().token, - decimalFee = originFee, + fee = originFee, ), originFee = originFee, originCommissionAsset = commissionAssetFlow.first(), originUsedAsset = assetFlow.first(), - crossChainFee = crossChainFeeMixin.awaitOptionalDecimalFee() + crossChainFee = crossChainFeeMixin.awaitOptionalFee() ) } diff --git a/feature-crowdloan-impl/src/main/java/io/novafoundation/nova/feature_crowdloan_impl/domain/contribute/validations/ContributeValidationPayload.kt b/feature-crowdloan-impl/src/main/java/io/novafoundation/nova/feature_crowdloan_impl/domain/contribute/validations/ContributeValidationPayload.kt index 8c40bc06ee..0fe5d2ec70 100644 --- a/feature-crowdloan-impl/src/main/java/io/novafoundation/nova/feature_crowdloan_impl/domain/contribute/validations/ContributeValidationPayload.kt +++ b/feature-crowdloan-impl/src/main/java/io/novafoundation/nova/feature_crowdloan_impl/domain/contribute/validations/ContributeValidationPayload.kt @@ -4,14 +4,14 @@ import android.os.Parcelable import io.novafoundation.nova.feature_crowdloan_impl.domain.main.Crowdloan import io.novafoundation.nova.feature_crowdloan_impl.presentation.contribute.custom.BonusPayload import io.novafoundation.nova.feature_wallet_api.domain.model.Asset -import io.novafoundation.nova.feature_wallet_api.presentation.model.DecimalFee +import io.novafoundation.nova.feature_account_api.data.model.Fee import java.math.BigDecimal class ContributeValidationPayload( val crowdloan: Crowdloan, val customizationPayload: Parcelable?, val asset: Asset, - val fee: DecimalFee, + val fee: Fee, val bonusPayload: BonusPayload?, val contributionAmount: BigDecimal, ) 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 b6c170cca3..b6c9edc558 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 @@ -1,11 +1,11 @@ package io.novafoundation.nova.feature_crowdloan_impl.domain.contribute.validations import io.novafoundation.nova.common.validation.Validation +import io.novafoundation.nova.feature_account_api.data.model.Fee 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-crowdloan-impl/src/main/java/io/novafoundation/nova/feature_crowdloan_impl/domain/contribute/validations/custom/moonbeam/MoonbeamTermsPayload.kt b/feature-crowdloan-impl/src/main/java/io/novafoundation/nova/feature_crowdloan_impl/domain/contribute/validations/custom/moonbeam/MoonbeamTermsPayload.kt index 02723cab9d..d96fdd2734 100644 --- a/feature-crowdloan-impl/src/main/java/io/novafoundation/nova/feature_crowdloan_impl/domain/contribute/validations/custom/moonbeam/MoonbeamTermsPayload.kt +++ b/feature-crowdloan-impl/src/main/java/io/novafoundation/nova/feature_crowdloan_impl/domain/contribute/validations/custom/moonbeam/MoonbeamTermsPayload.kt @@ -1,9 +1,9 @@ package io.novafoundation.nova.feature_crowdloan_impl.domain.contribute.validations.custom.moonbeam import io.novafoundation.nova.feature_wallet_api.domain.model.Asset -import io.novafoundation.nova.feature_wallet_api.presentation.model.DecimalFee +import io.novafoundation.nova.feature_account_api.data.model.Fee class MoonbeamTermsPayload( - val fee: DecimalFee, + val fee: Fee, val asset: Asset ) diff --git a/feature-crowdloan-impl/src/main/java/io/novafoundation/nova/feature_crowdloan_impl/presentation/contribute/confirm/ConfirmContributeViewModel.kt b/feature-crowdloan-impl/src/main/java/io/novafoundation/nova/feature_crowdloan_impl/presentation/contribute/confirm/ConfirmContributeViewModel.kt index c580567744..f2084b5ae3 100644 --- a/feature-crowdloan-impl/src/main/java/io/novafoundation/nova/feature_crowdloan_impl/presentation/contribute/confirm/ConfirmContributeViewModel.kt +++ b/feature-crowdloan-impl/src/main/java/io/novafoundation/nova/feature_crowdloan_impl/presentation/contribute/confirm/ConfirmContributeViewModel.kt @@ -85,7 +85,7 @@ class ConfirmContributeViewModel( val selectedAmount = payload.amount.toString() val feeFlow = assetFlow.map { asset -> - val feeModel = mapFeeToFeeModel(decimalFee.genericFee, asset.token) + val feeModel = mapFeeToFeeModel(decimalFee, asset.token) FeeStatus.Loaded(feeModel) } diff --git a/feature-crowdloan-impl/src/main/java/io/novafoundation/nova/feature_crowdloan_impl/presentation/contribute/custom/moonbeam/terms/MoonbeamCrowdloanTermsViewModel.kt b/feature-crowdloan-impl/src/main/java/io/novafoundation/nova/feature_crowdloan_impl/presentation/contribute/custom/moonbeam/terms/MoonbeamCrowdloanTermsViewModel.kt index 9aa1a6871a..be64c02e8f 100644 --- a/feature-crowdloan-impl/src/main/java/io/novafoundation/nova/feature_crowdloan_impl/presentation/contribute/custom/moonbeam/terms/MoonbeamCrowdloanTermsViewModel.kt +++ b/feature-crowdloan-impl/src/main/java/io/novafoundation/nova/feature_crowdloan_impl/presentation/contribute/custom/moonbeam/terms/MoonbeamCrowdloanTermsViewModel.kt @@ -10,6 +10,7 @@ import io.novafoundation.nova.common.utils.flowOf import io.novafoundation.nova.common.utils.inBackground import io.novafoundation.nova.common.validation.ValidationExecutor import io.novafoundation.nova.common.validation.progressConsumer +import io.novafoundation.nova.feature_account_api.data.model.Fee import io.novafoundation.nova.feature_crowdloan_impl.R import io.novafoundation.nova.feature_crowdloan_impl.domain.contribute.custom.moonbeam.MoonbeamCrowdloanInteractor import io.novafoundation.nova.feature_crowdloan_impl.domain.contribute.validations.custom.moonbeam.MoonbeamTermsPayload @@ -20,8 +21,7 @@ import io.novafoundation.nova.feature_crowdloan_impl.presentation.contribute.sel import io.novafoundation.nova.feature_wallet_api.domain.AssetUseCase import io.novafoundation.nova.feature_wallet_api.domain.getCurrentAsset import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.FeeLoaderMixin -import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.awaitDecimalFee -import io.novafoundation.nova.feature_wallet_api.presentation.model.DecimalFee +import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.awaitFee import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.first @@ -97,12 +97,11 @@ class MoonbeamCrowdloanTermsViewModel( fun submitClicked() = launch { submittingInProgressFlow.value = true - val fee = feeLoaderMixin.awaitDecimalFee() - + val fee = feeLoaderMixin.awaitFee() submitAfterValidation(fee) } - private fun submitAfterValidation(fee: DecimalFee) = launch { + private fun submitAfterValidation(fee: Fee) = launch { val validationPayload = MoonbeamTermsPayload( fee = fee, asset = assetUseCase.getCurrentAsset() diff --git a/feature-crowdloan-impl/src/main/java/io/novafoundation/nova/feature_crowdloan_impl/presentation/contribute/select/CrowdloanContributeViewModel.kt b/feature-crowdloan-impl/src/main/java/io/novafoundation/nova/feature_crowdloan_impl/presentation/contribute/select/CrowdloanContributeViewModel.kt index 89d25edf56..c22be932c6 100644 --- a/feature-crowdloan-impl/src/main/java/io/novafoundation/nova/feature_crowdloan_impl/presentation/contribute/select/CrowdloanContributeViewModel.kt +++ b/feature-crowdloan-impl/src/main/java/io/novafoundation/nova/feature_crowdloan_impl/presentation/contribute/select/CrowdloanContributeViewModel.kt @@ -38,8 +38,7 @@ import io.novafoundation.nova.feature_wallet_api.data.mappers.mapAssetToAssetMod import io.novafoundation.nova.feature_wallet_api.domain.AssetUseCase 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.SimpleFee -import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.awaitDecimalFee +import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.awaitFee import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.mapFeeToParcel import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow @@ -270,14 +269,12 @@ class CrowdloanContributeViewModel( feeConstructor = { val crowdloan = crowdloanFlow.first() - val fee = contributionInteractor.estimateFee( + contributionInteractor.estimateFee( crowdloan, amount, bonusActiveState?.payload, customizationPayload, ) - - SimpleFee(fee) }, onRetryCancelled = ::backClicked ) @@ -297,7 +294,7 @@ class CrowdloanContributeViewModel( val validationPayload = ContributeValidationPayload( crowdloan = crowdloanFlow.first(), customizationPayload = customizationPayload, - fee = feeLoaderMixin.awaitDecimalFee(), + fee = feeLoaderMixin.awaitFee(), asset = assetFlow.first(), bonusPayload = router.latestCustomBonus, contributionAmount = contributionAmount diff --git a/feature-external-sign-impl/src/main/java/io/novafoundation/nova/feature_external_sign_impl/domain/sign/Validations.kt b/feature-external-sign-impl/src/main/java/io/novafoundation/nova/feature_external_sign_impl/domain/sign/Validations.kt index 3db7d7273a..8e9d3c10f4 100644 --- a/feature-external-sign-impl/src/main/java/io/novafoundation/nova/feature_external_sign_impl/domain/sign/Validations.kt +++ b/feature-external-sign-impl/src/main/java/io/novafoundation/nova/feature_external_sign_impl/domain/sign/Validations.kt @@ -1,34 +1,20 @@ package io.novafoundation.nova.feature_external_sign_impl.domain.sign import io.novafoundation.nova.common.validation.ValidationSystem +import io.novafoundation.nova.feature_account_api.data.model.Fee import io.novafoundation.nova.feature_wallet_api.domain.model.Token -import io.novafoundation.nova.feature_wallet_api.domain.model.amountFromPlanks import io.novafoundation.nova.feature_wallet_api.domain.validation.FeeChangeDetectedFailure -import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.SimpleFee -import io.novafoundation.nova.feature_wallet_api.presentation.model.DecimalFee -import java.math.BigDecimal -import java.math.BigInteger sealed class ConfirmDAppOperationValidationFailure { - class FeeSpikeDetected(override val payload: FeeChangeDetectedFailure.Payload) : + class FeeSpikeDetected(override val payload: FeeChangeDetectedFailure.Payload) : ConfirmDAppOperationValidationFailure(), - FeeChangeDetectedFailure + FeeChangeDetectedFailure } data class ConfirmDAppOperationValidationPayload( val token: Token?, - val decimalFee: DecimalFee? + val fee: Fee? ) -inline fun ConfirmDAppOperationValidationPayload.convertingToAmount(planks: () -> BigInteger): BigDecimal { - require(token != null) { - "Invalid state - token should be present for validate transaction payload" - } - - val feeInPlanks = planks() - - return token.amountFromPlanks(feeInPlanks) -} - typealias ConfirmDAppOperationValidationSystem = ValidationSystem diff --git a/feature-external-sign-impl/src/main/java/io/novafoundation/nova/feature_external_sign_impl/domain/sign/evm/EvmSignInteractor.kt b/feature-external-sign-impl/src/main/java/io/novafoundation/nova/feature_external_sign_impl/domain/sign/evm/EvmSignInteractor.kt index fd8cea9162..0adc4bb03f 100644 --- a/feature-external-sign-impl/src/main/java/io/novafoundation/nova/feature_external_sign_impl/domain/sign/evm/EvmSignInteractor.kt +++ b/feature-external-sign-impl/src/main/java/io/novafoundation/nova/feature_external_sign_impl/domain/sign/evm/EvmSignInteractor.kt @@ -42,7 +42,7 @@ import io.novafoundation.nova.feature_external_sign_impl.domain.sign.ConfirmDApp import io.novafoundation.nova.feature_external_sign_impl.domain.sign.ExternalSignInteractor import io.novafoundation.nova.feature_wallet_api.domain.interfaces.TokenRepository import io.novafoundation.nova.feature_wallet_api.domain.model.Token -import io.novafoundation.nova.feature_wallet_api.domain.validation.checkForSimpleFeeChanges +import io.novafoundation.nova.feature_wallet_api.domain.validation.checkForFeeChanges import io.novafoundation.nova.runtime.ext.utilityAsset import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain @@ -116,9 +116,9 @@ class EvmSignInteractor( override val validationSystem: ConfirmDAppOperationValidationSystem = ValidationSystem { if (payload is ConfirmTx) { - checkForSimpleFeeChanges( + checkForFeeChanges( calculateFee = { calculateFee()!! }, - currentFee = { it.decimalFee }, + currentFee = { it.fee }, chainAsset = { it.token!!.configuration }, error = ConfirmDAppOperationValidationFailure::FeeSpikeDetected ) diff --git a/feature-external-sign-impl/src/main/java/io/novafoundation/nova/feature_external_sign_impl/presentation/signExtrinsic/ExternaSignViewModel.kt b/feature-external-sign-impl/src/main/java/io/novafoundation/nova/feature_external_sign_impl/presentation/signExtrinsic/ExternaSignViewModel.kt index 9b678e0d37..e8311f4991 100644 --- a/feature-external-sign-impl/src/main/java/io/novafoundation/nova/feature_external_sign_impl/presentation/signExtrinsic/ExternaSignViewModel.kt +++ b/feature-external-sign-impl/src/main/java/io/novafoundation/nova/feature_external_sign_impl/presentation/signExtrinsic/ExternaSignViewModel.kt @@ -27,7 +27,7 @@ import io.novafoundation.nova.feature_wallet_api.domain.validation.handleFeeSpik 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.mixin.fee.WithFeeLoaderMixin -import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.awaitOptionalDecimalFee +import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.awaitOptionalFee import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow @@ -102,7 +102,7 @@ class ExternaSignViewModel( val validationPayload = ConfirmDAppOperationValidationPayload( token = commissionTokenFlow?.first(), - decimalFee = originFeeMixin?.awaitOptionalDecimalFee() + fee = originFeeMixin?.awaitOptionalFee() ) validationExecutor.requireValid( @@ -112,7 +112,7 @@ class ExternaSignViewModel( autoFixPayload = ::autoFixPayload, progressConsumer = _performingOperationInProgress.progressConsumer() ) { - performOperation(it.decimalFee?.networkFee) + performOperation(it.fee) } } @@ -192,7 +192,7 @@ class ExternaSignViewModel( failure: ConfirmDAppOperationValidationFailure ): ConfirmDAppOperationValidationPayload { return when (failure) { - is ConfirmDAppOperationValidationFailure.FeeSpikeDetected -> payload.copy(decimalFee = failure.payload.newFee) + is ConfirmDAppOperationValidationFailure.FeeSpikeDetected -> payload.copy(fee = failure.payload.newFee) } } diff --git a/feature-governance-impl/src/main/java/io/novafoundation/nova/feature_governance_impl/domain/delegation/delegation/create/chooseAmount/validation/ChooseDelegationAmountValidationPayload.kt b/feature-governance-impl/src/main/java/io/novafoundation/nova/feature_governance_impl/domain/delegation/delegation/create/chooseAmount/validation/ChooseDelegationAmountValidationPayload.kt index c3307026e8..fd11e8acd6 100644 --- a/feature-governance-impl/src/main/java/io/novafoundation/nova/feature_governance_impl/domain/delegation/delegation/create/chooseAmount/validation/ChooseDelegationAmountValidationPayload.kt +++ b/feature-governance-impl/src/main/java/io/novafoundation/nova/feature_governance_impl/domain/delegation/delegation/create/chooseAmount/validation/ChooseDelegationAmountValidationPayload.kt @@ -1,12 +1,12 @@ package io.novafoundation.nova.feature_governance_impl.domain.delegation.delegation.create.chooseAmount.validation import io.novafoundation.nova.feature_wallet_api.domain.model.Asset -import io.novafoundation.nova.feature_wallet_api.presentation.model.DecimalFee +import io.novafoundation.nova.feature_account_api.data.model.Fee import io.novasama.substrate_sdk_android.runtime.AccountId import java.math.BigDecimal class ChooseDelegationAmountValidationPayload( - val fee: DecimalFee, + val fee: Fee, val asset: Asset, val amount: BigDecimal, val delegate: AccountId diff --git a/feature-governance-impl/src/main/java/io/novafoundation/nova/feature_governance_impl/domain/delegation/delegation/removeVotes/validations/RemoveVotesValidationPayload.kt b/feature-governance-impl/src/main/java/io/novafoundation/nova/feature_governance_impl/domain/delegation/delegation/removeVotes/validations/RemoveVotesValidationPayload.kt index 57e0eb1ed6..1518a557d5 100644 --- a/feature-governance-impl/src/main/java/io/novafoundation/nova/feature_governance_impl/domain/delegation/delegation/removeVotes/validations/RemoveVotesValidationPayload.kt +++ b/feature-governance-impl/src/main/java/io/novafoundation/nova/feature_governance_impl/domain/delegation/delegation/removeVotes/validations/RemoveVotesValidationPayload.kt @@ -1,9 +1,9 @@ package io.novafoundation.nova.feature_governance_impl.domain.delegation.delegation.removeVotes.validations +import io.novafoundation.nova.feature_account_api.data.model.Fee import io.novafoundation.nova.feature_wallet_api.domain.model.Asset -import io.novafoundation.nova.feature_wallet_api.presentation.model.DecimalFee class RemoveVotesValidationPayload( - val fee: DecimalFee, + val fee: Fee, val asset: Asset, ) diff --git a/feature-governance-impl/src/main/java/io/novafoundation/nova/feature_governance_impl/domain/delegation/delegation/revoke/validations/RevokeDelegationValidationPayload.kt b/feature-governance-impl/src/main/java/io/novafoundation/nova/feature_governance_impl/domain/delegation/delegation/revoke/validations/RevokeDelegationValidationPayload.kt index aecd5446f5..50fe4b3c9d 100644 --- a/feature-governance-impl/src/main/java/io/novafoundation/nova/feature_governance_impl/domain/delegation/delegation/revoke/validations/RevokeDelegationValidationPayload.kt +++ b/feature-governance-impl/src/main/java/io/novafoundation/nova/feature_governance_impl/domain/delegation/delegation/revoke/validations/RevokeDelegationValidationPayload.kt @@ -1,9 +1,9 @@ package io.novafoundation.nova.feature_governance_impl.domain.delegation.delegation.revoke.validations import io.novafoundation.nova.feature_wallet_api.domain.model.Asset -import io.novafoundation.nova.feature_wallet_api.presentation.model.DecimalFee +import io.novafoundation.nova.feature_account_api.data.model.Fee class RevokeDelegationValidationPayload( - val fee: DecimalFee, + val fee: Fee, val asset: Asset, ) diff --git a/feature-governance-impl/src/main/java/io/novafoundation/nova/feature_governance_impl/domain/referendum/unlock/validations/UnlockReferendumValidationPayload.kt b/feature-governance-impl/src/main/java/io/novafoundation/nova/feature_governance_impl/domain/referendum/unlock/validations/UnlockReferendumValidationPayload.kt index 087dffe4a7..48b0f662f5 100644 --- a/feature-governance-impl/src/main/java/io/novafoundation/nova/feature_governance_impl/domain/referendum/unlock/validations/UnlockReferendumValidationPayload.kt +++ b/feature-governance-impl/src/main/java/io/novafoundation/nova/feature_governance_impl/domain/referendum/unlock/validations/UnlockReferendumValidationPayload.kt @@ -1,9 +1,9 @@ package io.novafoundation.nova.feature_governance_impl.domain.referendum.unlock.validations import io.novafoundation.nova.feature_wallet_api.domain.model.Asset -import io.novafoundation.nova.feature_wallet_api.presentation.model.DecimalFee +import io.novafoundation.nova.feature_account_api.data.model.Fee class UnlockReferendumValidationPayload( - val fee: DecimalFee, + val fee: Fee, val asset: Asset, ) diff --git a/feature-governance-impl/src/main/java/io/novafoundation/nova/feature_governance_impl/domain/referendum/vote/validations/common/VoteValidationPayload.kt b/feature-governance-impl/src/main/java/io/novafoundation/nova/feature_governance_impl/domain/referendum/vote/validations/common/VoteValidationPayload.kt index d1dfbd38ae..2c6a6253d0 100644 --- a/feature-governance-impl/src/main/java/io/novafoundation/nova/feature_governance_impl/domain/referendum/vote/validations/common/VoteValidationPayload.kt +++ b/feature-governance-impl/src/main/java/io/novafoundation/nova/feature_governance_impl/domain/referendum/vote/validations/common/VoteValidationPayload.kt @@ -3,7 +3,7 @@ package io.novafoundation.nova.feature_governance_impl.domain.referendum.vote.va import io.novafoundation.nova.feature_governance_api.data.network.blockhain.model.OnChainReferendum import io.novafoundation.nova.feature_governance_api.data.network.blockhain.model.Voting import io.novafoundation.nova.feature_wallet_api.domain.model.Asset -import io.novafoundation.nova.feature_wallet_api.presentation.model.DecimalFee +import io.novafoundation.nova.feature_account_api.data.model.Fee import java.math.BigDecimal interface VoteValidationPayload { @@ -16,5 +16,5 @@ interface VoteValidationPayload { val maxAmount: BigDecimal - val fee: DecimalFee + val fee: Fee } diff --git a/feature-governance-impl/src/main/java/io/novafoundation/nova/feature_governance_impl/domain/referendum/vote/validations/referendum/VoteReferendaValidationPayload.kt b/feature-governance-impl/src/main/java/io/novafoundation/nova/feature_governance_impl/domain/referendum/vote/validations/referendum/VoteReferendaValidationPayload.kt index 3122360996..5badace898 100644 --- a/feature-governance-impl/src/main/java/io/novafoundation/nova/feature_governance_impl/domain/referendum/vote/validations/referendum/VoteReferendaValidationPayload.kt +++ b/feature-governance-impl/src/main/java/io/novafoundation/nova/feature_governance_impl/domain/referendum/vote/validations/referendum/VoteReferendaValidationPayload.kt @@ -5,7 +5,7 @@ import io.novafoundation.nova.feature_governance_api.data.network.blockhain.mode import io.novafoundation.nova.feature_governance_api.data.network.blockhain.model.Voting import io.novafoundation.nova.feature_governance_impl.domain.referendum.vote.validations.common.VoteValidationPayload import io.novafoundation.nova.feature_wallet_api.domain.model.Asset -import io.novafoundation.nova.feature_wallet_api.presentation.model.DecimalFee +import io.novafoundation.nova.feature_account_api.data.model.Fee import io.novafoundation.nova.runtime.multiNetwork.runtime.types.custom.vote.Conviction import java.math.BigDecimal @@ -16,5 +16,5 @@ data class VoteReferendaValidationPayload( override val maxAmount: BigDecimal, val voteType: VoteType?, val conviction: Conviction?, - override val fee: DecimalFee + override val fee: Fee ) : VoteValidationPayload diff --git a/feature-governance-impl/src/main/java/io/novafoundation/nova/feature_governance_impl/domain/referendum/vote/validations/tindergov/VoteTinderGovValidationPayload.kt b/feature-governance-impl/src/main/java/io/novafoundation/nova/feature_governance_impl/domain/referendum/vote/validations/tindergov/VoteTinderGovValidationPayload.kt index fd4905f2fb..57f1c186aa 100644 --- a/feature-governance-impl/src/main/java/io/novafoundation/nova/feature_governance_impl/domain/referendum/vote/validations/tindergov/VoteTinderGovValidationPayload.kt +++ b/feature-governance-impl/src/main/java/io/novafoundation/nova/feature_governance_impl/domain/referendum/vote/validations/tindergov/VoteTinderGovValidationPayload.kt @@ -6,14 +6,14 @@ import io.novafoundation.nova.feature_governance_api.data.network.blockhain.mode import io.novafoundation.nova.feature_governance_impl.domain.referendum.vote.validations.common.VoteValidationPayload import io.novafoundation.nova.feature_wallet_api.domain.model.Asset import io.novafoundation.nova.feature_wallet_api.domain.model.amountFromPlanks -import io.novafoundation.nova.feature_wallet_api.presentation.model.DecimalFee +import io.novafoundation.nova.feature_account_api.data.model.Fee import java.math.BigDecimal data class VoteTinderGovValidationPayload( override val onChainReferenda: List, override val asset: Asset, override val trackVoting: List, - override val fee: DecimalFee, + override val fee: Fee, val basket: List ) : VoteValidationPayload { diff --git a/feature-governance-impl/src/main/java/io/novafoundation/nova/feature_governance_impl/presentation/delegation/delegation/create/chooseAmount/NewDelegationChooseAmountViewModel.kt b/feature-governance-impl/src/main/java/io/novafoundation/nova/feature_governance_impl/presentation/delegation/delegation/create/chooseAmount/NewDelegationChooseAmountViewModel.kt index 6c3cffde48..fea9d4e08d 100644 --- a/feature-governance-impl/src/main/java/io/novafoundation/nova/feature_governance_impl/presentation/delegation/delegation/create/chooseAmount/NewDelegationChooseAmountViewModel.kt +++ b/feature-governance-impl/src/main/java/io/novafoundation/nova/feature_governance_impl/presentation/delegation/delegation/create/chooseAmount/NewDelegationChooseAmountViewModel.kt @@ -31,7 +31,7 @@ import io.novafoundation.nova.feature_wallet_api.presentation.mixin.amountChoose import io.novafoundation.nova.feature_wallet_api.presentation.mixin.amountChooser.setAmountInput import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.FeeLoaderMixin import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.WithFeeLoaderMixin -import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.awaitDecimalFee +import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.awaitFee 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 @@ -161,7 +161,7 @@ class NewDelegationChooseAmountViewModel( val payload = ChooseDelegationAmountValidationPayload( asset = selectedAsset.first(), - fee = originFeeMixin.awaitDecimalFee(), + fee = originFeeMixin.awaitFee(), amount = amountChooserMixin.amount.first(), delegate = payload.delegate ) diff --git a/feature-governance-impl/src/main/java/io/novafoundation/nova/feature_governance_impl/presentation/delegation/delegation/create/confirm/NewDelegationConfirmViewModel.kt b/feature-governance-impl/src/main/java/io/novafoundation/nova/feature_governance_impl/presentation/delegation/delegation/create/confirm/NewDelegationConfirmViewModel.kt index 37b3eb9e35..c5cdf33551 100644 --- a/feature-governance-impl/src/main/java/io/novafoundation/nova/feature_governance_impl/presentation/delegation/delegation/create/confirm/NewDelegationConfirmViewModel.kt +++ b/feature-governance-impl/src/main/java/io/novafoundation/nova/feature_governance_impl/presentation/delegation/delegation/create/confirm/NewDelegationConfirmViewModel.kt @@ -189,7 +189,7 @@ class NewDelegationConfirmViewModel( } private fun setFee() = launch { - originFeeMixin.setFee(decimalFee.genericFee) + originFeeMixin.setFee(decimalFee) } private fun performDelegate(amountPlanks: Balance) = launch { diff --git a/feature-governance-impl/src/main/java/io/novafoundation/nova/feature_governance_impl/presentation/delegation/delegation/removeVotes/RemoveVotesViewModel.kt b/feature-governance-impl/src/main/java/io/novafoundation/nova/feature_governance_impl/presentation/delegation/delegation/removeVotes/RemoveVotesViewModel.kt index 61f3b5a42e..cdf9b74cd4 100644 --- a/feature-governance-impl/src/main/java/io/novafoundation/nova/feature_governance_impl/presentation/delegation/delegation/removeVotes/RemoveVotesViewModel.kt +++ b/feature-governance-impl/src/main/java/io/novafoundation/nova/feature_governance_impl/presentation/delegation/delegation/removeVotes/RemoveVotesViewModel.kt @@ -27,7 +27,7 @@ import io.novafoundation.nova.feature_governance_impl.presentation.track.formatT import io.novafoundation.nova.feature_wallet_api.domain.AssetUseCase import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.FeeLoaderMixin import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.WithFeeLoaderMixin -import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.awaitDecimalFee +import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.awaitFee import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.create import io.novafoundation.nova.runtime.state.chain import io.novafoundation.nova.runtime.state.chainAsset @@ -107,7 +107,7 @@ class RemoveVotesViewModel( _showNextProgress.value = true val validationPayload = RemoveVotesValidationPayload( - fee = originFeeMixin.awaitDecimalFee(), + fee = originFeeMixin.awaitFee(), asset = assetFlow.first() ) diff --git a/feature-governance-impl/src/main/java/io/novafoundation/nova/feature_governance_impl/presentation/delegation/delegation/revoke/confirm/RevokeDelegationConfirmViewModel.kt b/feature-governance-impl/src/main/java/io/novafoundation/nova/feature_governance_impl/presentation/delegation/delegation/revoke/confirm/RevokeDelegationConfirmViewModel.kt index f63e39812e..1f3cb1e8ec 100644 --- a/feature-governance-impl/src/main/java/io/novafoundation/nova/feature_governance_impl/presentation/delegation/delegation/revoke/confirm/RevokeDelegationConfirmViewModel.kt +++ b/feature-governance-impl/src/main/java/io/novafoundation/nova/feature_governance_impl/presentation/delegation/delegation/revoke/confirm/RevokeDelegationConfirmViewModel.kt @@ -35,7 +35,7 @@ import io.novafoundation.nova.feature_governance_impl.presentation.track.TrackFo import io.novafoundation.nova.feature_wallet_api.domain.AssetUseCase import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.FeeLoaderMixin import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.WithFeeLoaderMixin -import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.awaitDecimalFee +import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.awaitFee import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.create import io.novafoundation.nova.runtime.state.chain import io.novafoundation.nova.runtime.state.chainAsset @@ -150,7 +150,7 @@ class RevokeDelegationConfirmViewModel( _showNextProgress.value = true val validationPayload = RevokeDelegationValidationPayload( - fee = originFeeMixin.awaitDecimalFee(), + fee = originFeeMixin.awaitFee(), asset = assetFlow.first() ) diff --git a/feature-governance-impl/src/main/java/io/novafoundation/nova/feature_governance_impl/presentation/referenda/vote/confirm/ConfirmReferendumVoteViewModel.kt b/feature-governance-impl/src/main/java/io/novafoundation/nova/feature_governance_impl/presentation/referenda/vote/confirm/ConfirmReferendumVoteViewModel.kt index 800dac3c34..a45e6deab4 100644 --- a/feature-governance-impl/src/main/java/io/novafoundation/nova/feature_governance_impl/presentation/referenda/vote/confirm/ConfirmReferendumVoteViewModel.kt +++ b/feature-governance-impl/src/main/java/io/novafoundation/nova/feature_governance_impl/presentation/referenda/vote/confirm/ConfirmReferendumVoteViewModel.kt @@ -28,7 +28,7 @@ import io.novafoundation.nova.feature_wallet_api.domain.AssetUseCase import io.novafoundation.nova.feature_wallet_api.domain.model.Asset 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.awaitDecimalFee +import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.awaitFee import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.mapFeeFromParcel import io.novafoundation.nova.feature_wallet_api.presentation.model.AmountModel import io.novafoundation.nova.feature_wallet_api.presentation.model.mapAmountToAmountModel @@ -145,7 +145,7 @@ class ConfirmReferendumVoteViewModel( } private fun setFee() = launch { - originFeeMixin.setFee(mapFeeFromParcel(payload.fee).genericFee) + originFeeMixin.setFee(mapFeeFromParcel(payload.fee)) } private suspend fun getValidationPayload(): VoteReferendaValidationPayload { @@ -158,7 +158,7 @@ class ConfirmReferendumVoteViewModel( maxAmount = payload.vote.amount, conviction = payload.vote.conviction, voteType = payload.vote.voteType, - fee = originFeeMixin.awaitDecimalFee() + fee = originFeeMixin.awaitFee() ) } } diff --git a/feature-governance-impl/src/main/java/io/novafoundation/nova/feature_governance_impl/presentation/referenda/vote/setup/common/SetupVoteViewModel.kt b/feature-governance-impl/src/main/java/io/novafoundation/nova/feature_governance_impl/presentation/referenda/vote/setup/common/SetupVoteViewModel.kt index 2959dc4813..ea76c38c9a 100644 --- a/feature-governance-impl/src/main/java/io/novafoundation/nova/feature_governance_impl/presentation/referenda/vote/setup/common/SetupVoteViewModel.kt +++ b/feature-governance-impl/src/main/java/io/novafoundation/nova/feature_governance_impl/presentation/referenda/vote/setup/common/SetupVoteViewModel.kt @@ -31,7 +31,7 @@ import io.novafoundation.nova.feature_wallet_api.presentation.mixin.amountChoose import io.novafoundation.nova.feature_wallet_api.presentation.mixin.amountChooser.setAmountInput import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.FeeLoaderMixin import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.WithFeeLoaderMixin -import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.awaitDecimalFee +import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.awaitFee 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.runtime.multiNetwork.runtime.types.custom.vote.Conviction @@ -159,7 +159,7 @@ abstract class SetupVoteViewModel( maxAmount = amount, voteType = voteType, conviction = conviction, - fee = originFeeMixin.awaitDecimalFee() + fee = originFeeMixin.awaitFee() ) validationExecutor.requireValid( diff --git a/feature-governance-impl/src/main/java/io/novafoundation/nova/feature_governance_impl/presentation/tindergov/confirm/ConfirmTinderGovVoteViewModel.kt b/feature-governance-impl/src/main/java/io/novafoundation/nova/feature_governance_impl/presentation/tindergov/confirm/ConfirmTinderGovVoteViewModel.kt index e54116bfd4..0c29504398 100644 --- a/feature-governance-impl/src/main/java/io/novafoundation/nova/feature_governance_impl/presentation/tindergov/confirm/ConfirmTinderGovVoteViewModel.kt +++ b/feature-governance-impl/src/main/java/io/novafoundation/nova/feature_governance_impl/presentation/tindergov/confirm/ConfirmTinderGovVoteViewModel.kt @@ -13,10 +13,10 @@ import io.novafoundation.nova.feature_account_api.presenatation.actions.External import io.novafoundation.nova.feature_governance_api.data.model.TinderGovBasketItem import io.novafoundation.nova.feature_governance_api.data.model.accountVote import io.novafoundation.nova.feature_governance_api.domain.referendum.vote.VoteReferendumInteractor -import io.novafoundation.nova.feature_governance_impl.domain.referendum.tindergov.TinderGovBasketInteractor -import io.novafoundation.nova.feature_governance_impl.domain.referendum.tindergov.TinderGovInteractor import io.novafoundation.nova.feature_governance_impl.R import io.novafoundation.nova.feature_governance_impl.data.GovernanceSharedState +import io.novafoundation.nova.feature_governance_impl.domain.referendum.tindergov.TinderGovBasketInteractor +import io.novafoundation.nova.feature_governance_impl.domain.referendum.tindergov.TinderGovInteractor import io.novafoundation.nova.feature_governance_impl.domain.referendum.vote.validations.tindergov.VoteTinderGovValidationPayload import io.novafoundation.nova.feature_governance_impl.domain.referendum.vote.validations.tindergov.VoteTinderGovValidationSystem import io.novafoundation.nova.feature_governance_impl.domain.referendum.vote.validations.tindergov.handleVoteTinderGovValidationFailure @@ -26,11 +26,9 @@ import io.novafoundation.nova.feature_governance_impl.presentation.referenda.vot import io.novafoundation.nova.feature_governance_impl.presentation.referenda.vote.hints.ReferendumVoteHintsMixinFactory import io.novafoundation.nova.feature_wallet_api.domain.AssetUseCase 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.awaitDecimalFee +import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.awaitFee import io.novafoundation.nova.feature_wallet_api.presentation.model.AmountModel import io.novafoundation.nova.feature_wallet_api.presentation.model.mapAmountToAmountModel -import java.math.BigInteger import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.combine @@ -39,6 +37,7 @@ import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.map import kotlinx.coroutines.launch import kotlinx.coroutines.withContext +import java.math.BigInteger class ConfirmTinderGovVoteViewModel( private val router: GovernanceRouter, @@ -111,13 +110,8 @@ class ConfirmTinderGovVoteViewModel( launch { originFeeMixin.loadFeeSuspending( retryScope = this, - feeConstructor = { - val fee = interactor.estimateFee(votesFlow.first()) - SimpleFee(fee) - }, - onRetryCancelled = { - router.back() - } + feeConstructor = { interactor.estimateFee(votesFlow.first()) }, + onRetryCancelled = router::back ) } } @@ -175,7 +169,7 @@ class ConfirmTinderGovVoteViewModel( onChainReferenda = voteAssistant.onChainReferenda, asset = assetFlow.first(), trackVoting = voteAssistant.trackVoting, - fee = originFeeMixin.awaitDecimalFee(), + fee = originFeeMixin.awaitFee(), basket = basket ) } diff --git a/feature-governance-impl/src/main/java/io/novafoundation/nova/feature_governance_impl/presentation/unlock/confirm/ConfirmGovernanceUnlockViewModel.kt b/feature-governance-impl/src/main/java/io/novafoundation/nova/feature_governance_impl/presentation/unlock/confirm/ConfirmGovernanceUnlockViewModel.kt index 00de568851..9711e983f3 100644 --- a/feature-governance-impl/src/main/java/io/novafoundation/nova/feature_governance_impl/presentation/unlock/confirm/ConfirmGovernanceUnlockViewModel.kt +++ b/feature-governance-impl/src/main/java/io/novafoundation/nova/feature_governance_impl/presentation/unlock/confirm/ConfirmGovernanceUnlockViewModel.kt @@ -30,7 +30,7 @@ import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.types.Ba import io.novafoundation.nova.feature_wallet_api.domain.AssetUseCase import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.FeeLoaderMixin import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.WithFeeLoaderMixin -import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.awaitDecimalFee +import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.awaitFee 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.model.mapAmountToAmountModel @@ -143,7 +143,7 @@ class ConfirmGovernanceUnlockViewModel( val validationPayload = UnlockReferendumValidationPayload( asset = assetFlow.first(), - fee = originFeeMixin.awaitDecimalFee() + fee = originFeeMixin.awaitFee() ) validationExecutor.requireValid( diff --git a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/bagList/rebag/validations/RebagValidationPayload.kt b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/bagList/rebag/validations/RebagValidationPayload.kt index 6dbd99c370..095fae673d 100644 --- a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/bagList/rebag/validations/RebagValidationPayload.kt +++ b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/bagList/rebag/validations/RebagValidationPayload.kt @@ -1,9 +1,9 @@ package io.novafoundation.nova.feature_staking_impl.domain.bagList.rebag.validations import io.novafoundation.nova.feature_wallet_api.domain.model.Asset -import io.novafoundation.nova.feature_wallet_api.presentation.model.DecimalFee +import io.novafoundation.nova.feature_account_api.data.model.Fee class RebagValidationPayload( - val fee: DecimalFee, + val fee: Fee, val asset: Asset, ) diff --git a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/common/validation/ProfitableActionValidation.kt b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/common/validation/ProfitableActionValidation.kt index b15fe9f596..4724762d1f 100644 --- a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/common/validation/ProfitableActionValidation.kt +++ b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/common/validation/ProfitableActionValidation.kt @@ -1,22 +1,23 @@ package io.novafoundation.nova.feature_staking_impl.domain.common.validation +import io.novafoundation.nova.common.utils.orZero 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.isTrueOrWarning -import io.novafoundation.nova.feature_wallet_api.domain.validation.FeeProducer -import io.novafoundation.nova.feature_wallet_api.presentation.model.networkFeeDecimalAmount +import io.novafoundation.nova.feature_account_api.data.model.decimalAmount +import io.novafoundation.nova.feature_wallet_api.domain.validation.SimpleFeeProducer import java.math.BigDecimal class ProfitableActionValidation( val amount: P.() -> BigDecimal, - val fee: FeeProducer

, + val fee: SimpleFeeProducer

, val error: (P) -> E ) : Validation { override suspend fun validate(value: P): ValidationStatus { // No matter who paid the fee we check that it is profitable - val isProfitable = fee(value).networkFeeDecimalAmount < value.amount() + val isProfitable = fee(value)?.decimalAmount.orZero() < value.amount() return isProfitable isTrueOrWarning { error(value) @@ -26,7 +27,7 @@ class ProfitableActionValidation( fun ValidationSystemBuilder.profitableAction( amount: P.() -> BigDecimal, - fee: FeeProducer

, + fee: SimpleFeeProducer

, error: (P) -> E ) { validate(ProfitableActionValidation(amount, fee, error)) diff --git a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/nominationPools/bondMore/validations/NominationPoolsBondMoreValidationPayload.kt b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/nominationPools/bondMore/validations/NominationPoolsBondMoreValidationPayload.kt index 4941a86eba..145366af4c 100644 --- a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/nominationPools/bondMore/validations/NominationPoolsBondMoreValidationPayload.kt +++ b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/nominationPools/bondMore/validations/NominationPoolsBondMoreValidationPayload.kt @@ -2,12 +2,12 @@ package io.novafoundation.nova.feature_staking_impl.domain.nominationPools.bondM import io.novafoundation.nova.feature_staking_impl.data.nominationPools.network.blockhain.models.PoolMember import io.novafoundation.nova.feature_wallet_api.domain.model.Asset -import io.novafoundation.nova.feature_wallet_api.presentation.model.DecimalFee +import io.novafoundation.nova.feature_account_api.data.model.Fee import java.math.BigDecimal data class NominationPoolsBondMoreValidationPayload( val poolMember: PoolMember, val amount: BigDecimal, - val fee: DecimalFee, + val fee: Fee, val asset: Asset, ) diff --git a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/nominationPools/claimRewards/validations/NominationPoolsClaimRewardsValidationPayload.kt b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/nominationPools/claimRewards/validations/NominationPoolsClaimRewardsValidationPayload.kt index 55e58fafce..160823396a 100644 --- a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/nominationPools/claimRewards/validations/NominationPoolsClaimRewardsValidationPayload.kt +++ b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/nominationPools/claimRewards/validations/NominationPoolsClaimRewardsValidationPayload.kt @@ -4,12 +4,12 @@ import io.novafoundation.nova.feature_staking_impl.data.nominationPools.network. import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.types.Balance import io.novafoundation.nova.feature_wallet_api.domain.model.Asset import io.novafoundation.nova.feature_wallet_api.domain.model.amountFromPlanks -import io.novafoundation.nova.feature_wallet_api.presentation.model.DecimalFee +import io.novafoundation.nova.feature_account_api.data.model.Fee import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain import java.math.BigDecimal class NominationPoolsClaimRewardsValidationPayload( - val fee: DecimalFee, + val fee: Fee, val pendingRewardsPlanks: Balance, val asset: Asset, val chain: Chain, diff --git a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/nominationPools/common/validations/PoolAvailableBalanceValidation.kt b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/nominationPools/common/validations/PoolAvailableBalanceValidation.kt index fdd7ba5749..3029b1517b 100644 --- a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/nominationPools/common/validations/PoolAvailableBalanceValidation.kt +++ b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/nominationPools/common/validations/PoolAvailableBalanceValidation.kt @@ -17,7 +17,7 @@ import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.types.Ba import io.novafoundation.nova.feature_wallet_api.domain.model.Asset import io.novafoundation.nova.feature_wallet_api.domain.model.amountFromPlanks import io.novafoundation.nova.feature_wallet_api.domain.model.planksFromAmount -import io.novafoundation.nova.feature_wallet_api.domain.validation.FeeProducer +import io.novafoundation.nova.feature_wallet_api.domain.validation.SimpleFeeProducer import io.novafoundation.nova.feature_wallet_api.presentation.formatters.formatPlanks import io.novafoundation.nova.feature_wallet_api.presentation.formatters.formatTokenAmount import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain @@ -31,7 +31,7 @@ class PoolAvailableBalanceValidationFactory( context(ValidationSystemBuilder) fun enoughAvailableBalanceToStake( asset: (P) -> Asset, - fee: FeeProducer

, + fee: SimpleFeeProducer

, amount: (P) -> BigDecimal, error: (PoolAvailableBalanceValidation.ValidationError.Context) -> E ) { @@ -50,7 +50,7 @@ class PoolAvailableBalanceValidationFactory( class PoolAvailableBalanceValidation( private val poolsAvailableBalanceResolver: NominationPoolsAvailableBalanceResolver, private val asset: (P) -> Asset, - private val fee: FeeProducer

, + private val fee: SimpleFeeProducer

, private val amount: (P) -> BigDecimal, private val error: (ValidationError.Context) -> E ) : Validation { @@ -72,7 +72,7 @@ class PoolAvailableBalanceValidation( val asset = asset(value) val chainAsset = asset.token.configuration - val fee = fee(value)?.networkFee?.amountByExecutingAccount.orZero() + val fee = fee(value)?.amountByExecutingAccount.orZero() val availableBalance = poolsAvailableBalanceResolver.availableBalanceToStartStaking(asset) val maxToStake = poolsAvailableBalanceResolver.maximumBalanceToStake(asset, fee) val enteredAmount = chainAsset.planksFromAmount(amount(value)) diff --git a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/nominationPools/redeem/validations/NominationPoolsRedeemValidationPayload.kt b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/nominationPools/redeem/validations/NominationPoolsRedeemValidationPayload.kt index 1553fe3853..075e2a789c 100644 --- a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/nominationPools/redeem/validations/NominationPoolsRedeemValidationPayload.kt +++ b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/nominationPools/redeem/validations/NominationPoolsRedeemValidationPayload.kt @@ -2,11 +2,11 @@ package io.novafoundation.nova.feature_staking_impl.domain.nominationPools.redee import io.novafoundation.nova.feature_staking_impl.data.nominationPools.network.blockhain.models.PoolMember import io.novafoundation.nova.feature_wallet_api.domain.model.Asset -import io.novafoundation.nova.feature_wallet_api.presentation.model.DecimalFee +import io.novafoundation.nova.feature_account_api.data.model.Fee import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain class NominationPoolsRedeemValidationPayload( - val fee: DecimalFee, + val fee: Fee, val asset: Asset, val chain: Chain, val poolMember: PoolMember, diff --git a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/nominationPools/unbond/validations/NominationPoolsUnbondValidationPayload.kt b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/nominationPools/unbond/validations/NominationPoolsUnbondValidationPayload.kt index dbdeadec6b..21b890ecf3 100644 --- a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/nominationPools/unbond/validations/NominationPoolsUnbondValidationPayload.kt +++ b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/nominationPools/unbond/validations/NominationPoolsUnbondValidationPayload.kt @@ -2,7 +2,7 @@ package io.novafoundation.nova.feature_staking_impl.domain.nominationPools.unbon import io.novafoundation.nova.feature_staking_impl.data.nominationPools.network.blockhain.models.PoolMember import io.novafoundation.nova.feature_wallet_api.domain.model.Asset -import io.novafoundation.nova.feature_wallet_api.presentation.model.DecimalFee +import io.novafoundation.nova.feature_account_api.data.model.Fee import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain import kotlinx.coroutines.CoroutineScope import java.math.BigDecimal @@ -11,7 +11,7 @@ data class NominationPoolsUnbondValidationPayload( val poolMember: PoolMember, val stakedBalance: BigDecimal, val amount: BigDecimal, - val fee: DecimalFee, + val fee: Fee, val asset: Asset, val chain: Chain, val sharedComputationScope: CoroutineScope, diff --git a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/parachainStaking/rebond/validations/ParachainStakingRebondValidationPayload.kt b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/parachainStaking/rebond/validations/ParachainStakingRebondValidationPayload.kt index b4f9c8857b..13e569c04e 100644 --- a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/parachainStaking/rebond/validations/ParachainStakingRebondValidationPayload.kt +++ b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/parachainStaking/rebond/validations/ParachainStakingRebondValidationPayload.kt @@ -1,9 +1,9 @@ package io.novafoundation.nova.feature_staking_impl.domain.parachainStaking.rebond.validations import io.novafoundation.nova.feature_wallet_api.domain.model.Asset -import io.novafoundation.nova.feature_wallet_api.presentation.model.DecimalFee +import io.novafoundation.nova.feature_account_api.data.model.Fee class ParachainStakingRebondValidationPayload( val asset: Asset, - val fee: DecimalFee, + val fee: Fee, ) diff --git a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/parachainStaking/redeem/validations/ParachainStakingRedeemValidationPayload.kt b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/parachainStaking/redeem/validations/ParachainStakingRedeemValidationPayload.kt index a6fa8e5b20..8a8380c7d3 100644 --- a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/parachainStaking/redeem/validations/ParachainStakingRedeemValidationPayload.kt +++ b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/parachainStaking/redeem/validations/ParachainStakingRedeemValidationPayload.kt @@ -1,9 +1,9 @@ package io.novafoundation.nova.feature_staking_impl.domain.parachainStaking.redeem.validations import io.novafoundation.nova.feature_wallet_api.domain.model.Asset -import io.novafoundation.nova.feature_wallet_api.presentation.model.DecimalFee +import io.novafoundation.nova.feature_account_api.data.model.Fee class ParachainStakingRedeemValidationPayload( val asset: Asset, - val fee: DecimalFee, + val fee: Fee, ) diff --git a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/parachainStaking/start/validations/StartParachainStakingValidationPayload.kt b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/parachainStaking/start/validations/StartParachainStakingValidationPayload.kt index e1f7b3fa8d..b50d7667b0 100644 --- a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/parachainStaking/start/validations/StartParachainStakingValidationPayload.kt +++ b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/parachainStaking/start/validations/StartParachainStakingValidationPayload.kt @@ -3,12 +3,12 @@ package io.novafoundation.nova.feature_staking_impl.domain.parachainStaking.star import io.novafoundation.nova.feature_staking_api.domain.model.parachain.DelegatorState import io.novafoundation.nova.feature_staking_impl.domain.parachainStaking.common.model.Collator import io.novafoundation.nova.feature_wallet_api.domain.model.Asset -import io.novafoundation.nova.feature_wallet_api.presentation.model.DecimalFee +import io.novafoundation.nova.feature_account_api.data.model.Fee import java.math.BigDecimal class StartParachainStakingValidationPayload( val amount: BigDecimal, - val fee: DecimalFee, + val fee: Fee, val collator: Collator, val asset: Asset, val delegatorState: DelegatorState, diff --git a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/parachainStaking/unbond/validations/flow/ParachainStakingUnbondValidationPayload.kt b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/parachainStaking/unbond/validations/flow/ParachainStakingUnbondValidationPayload.kt index a12ede1cfb..4d5a2f90a9 100644 --- a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/parachainStaking/unbond/validations/flow/ParachainStakingUnbondValidationPayload.kt +++ b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/parachainStaking/unbond/validations/flow/ParachainStakingUnbondValidationPayload.kt @@ -2,12 +2,12 @@ package io.novafoundation.nova.feature_staking_impl.domain.parachainStaking.unbo import io.novafoundation.nova.feature_staking_impl.domain.parachainStaking.common.model.Collator import io.novafoundation.nova.feature_wallet_api.domain.model.Asset -import io.novafoundation.nova.feature_wallet_api.presentation.model.DecimalFee +import io.novafoundation.nova.feature_account_api.data.model.Fee import java.math.BigDecimal data class ParachainStakingUnbondValidationPayload( val amount: BigDecimal, - val fee: DecimalFee, + val fee: Fee, val collator: Collator, val asset: Asset, ) diff --git a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/parachainStaking/yieldBoost/validations/FirstTaskCanExecute.kt b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/parachainStaking/yieldBoost/validations/FirstTaskCanExecute.kt index 13306d0f5f..c0806c1a7b 100644 --- a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/parachainStaking/yieldBoost/validations/FirstTaskCanExecute.kt +++ b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/parachainStaking/yieldBoost/validations/FirstTaskCanExecute.kt @@ -3,6 +3,7 @@ package io.novafoundation.nova.feature_staking_impl.domain.parachainStaking.yiel import io.novafoundation.nova.common.validation.ValidationStatus import io.novafoundation.nova.common.validation.valid import io.novafoundation.nova.common.validation.validationError +import io.novafoundation.nova.feature_account_api.data.model.decimalAmountByExecutingAccount import io.novafoundation.nova.feature_staking_api.data.parachainStaking.turing.repository.AutomationAction import io.novafoundation.nova.feature_staking_api.data.parachainStaking.turing.repository.TuringAutomationTasksRepository import io.novafoundation.nova.feature_staking_impl.domain.parachainStaking.yieldBoost.YieldBoostConfiguration @@ -20,7 +21,7 @@ class FirstTaskCanExecute( val token = value.asset.token val balanceBeforeTransaction = value.asset.transferable - val balanceAfterTransaction = balanceBeforeTransaction - value.fee.networkFeeDecimalAmount + val balanceAfterTransaction = balanceBeforeTransaction - value.fee.decimalAmountByExecutingAccount val chainId = value.asset.token.configuration.chainId @@ -32,7 +33,7 @@ class FirstTaskCanExecute( return when { taskExecutionFee > balanceAfterTransaction -> YieldBoostValidationFailure.FirstTaskCannotExecute( minimumBalanceRequired = taskExecutionFee, - networkFee = value.fee.networkFeeDecimalAmount, + networkFee = value.fee.decimalAmountByExecutingAccount, availableBalanceBeforeFees = balanceBeforeTransaction, type = EXECUTION_FEE, chainAsset = token.configuration @@ -40,7 +41,7 @@ class FirstTaskCanExecute( threshold > balanceAfterTransaction -> YieldBoostValidationFailure.FirstTaskCannotExecute( minimumBalanceRequired = threshold, - networkFee = value.fee.networkFeeDecimalAmount, + networkFee = value.fee.decimalAmountByExecutingAccount, availableBalanceBeforeFees = balanceBeforeTransaction, type = THRESHOLD, chainAsset = token.configuration diff --git a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/parachainStaking/yieldBoost/validations/YieldBoostValidationPayload.kt b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/parachainStaking/yieldBoost/validations/YieldBoostValidationPayload.kt index 6211cabd9c..f74fd27dfc 100644 --- a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/parachainStaking/yieldBoost/validations/YieldBoostValidationPayload.kt +++ b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/parachainStaking/yieldBoost/validations/YieldBoostValidationPayload.kt @@ -4,12 +4,12 @@ import io.novafoundation.nova.feature_staking_impl.domain.parachainStaking.commo import io.novafoundation.nova.feature_staking_impl.domain.parachainStaking.yieldBoost.YieldBoostConfiguration import io.novafoundation.nova.feature_staking_impl.domain.parachainStaking.yieldBoost.YieldBoostTask import io.novafoundation.nova.feature_wallet_api.domain.model.Asset -import io.novafoundation.nova.feature_wallet_api.presentation.model.DecimalFee +import io.novafoundation.nova.feature_account_api.data.model.Fee class YieldBoostValidationPayload( val collator: Collator, val activeTasks: List, val configuration: YieldBoostConfiguration, val asset: Asset, - val fee: DecimalFee, + val fee: Fee, ) diff --git a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/staking/start/common/validations/AvailableBalanceGapValidation.kt b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/staking/start/common/validations/AvailableBalanceGapValidation.kt index 49919e7db6..5793f3065a 100644 --- a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/staking/start/common/validations/AvailableBalanceGapValidation.kt +++ b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/staking/start/common/validations/AvailableBalanceGapValidation.kt @@ -19,7 +19,7 @@ class AvailableBalanceGapValidation( override suspend fun validate(value: StartMultiStakingValidationPayload): ValidationStatus { val amount = value.selection.stake val stakingOption = value.selection.stakingOption - val fee = value.fee.networkFee.amountByExecutingAccount + val fee = value.fee.amountByExecutingAccount val maxToStakeWithMinStakes = candidates.map { val maximumToStake = it.maximumToStake(value.asset, fee) diff --git a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/staking/start/common/validations/StartMultiStakingValidationPayload.kt b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/staking/start/common/validations/StartMultiStakingValidationPayload.kt index 4f93bd0247..442a81745a 100644 --- a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/staking/start/common/validations/StartMultiStakingValidationPayload.kt +++ b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/staking/start/common/validations/StartMultiStakingValidationPayload.kt @@ -3,13 +3,13 @@ package io.novafoundation.nova.feature_staking_impl.domain.staking.start.common. import io.novafoundation.nova.feature_staking_impl.domain.staking.start.common.selection.RecommendableMultiStakingSelection import io.novafoundation.nova.feature_wallet_api.domain.model.Asset import io.novafoundation.nova.feature_wallet_api.domain.model.amountFromPlanks -import io.novafoundation.nova.feature_wallet_api.presentation.model.DecimalFee +import io.novafoundation.nova.feature_account_api.data.model.Fee import java.math.BigInteger data class StartMultiStakingValidationPayload( val recommendableSelection: RecommendableMultiStakingSelection, val asset: Asset, - val fee: DecimalFee, + val fee: Fee, ) { val selection = recommendableSelection.selection } diff --git a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/validations/bond/BondMoreValidationPayload.kt b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/validations/bond/BondMoreValidationPayload.kt index 3dd0939360..5e29e2e3cb 100644 --- a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/validations/bond/BondMoreValidationPayload.kt +++ b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/validations/bond/BondMoreValidationPayload.kt @@ -1,12 +1,12 @@ package io.novafoundation.nova.feature_staking_impl.domain.validations.bond import io.novafoundation.nova.feature_wallet_api.domain.model.Asset -import io.novafoundation.nova.feature_wallet_api.presentation.model.DecimalFee +import io.novafoundation.nova.feature_account_api.data.model.Fee import java.math.BigDecimal class BondMoreValidationPayload( val stashAddress: String, - val fee: DecimalFee, + val fee: Fee, val amount: BigDecimal, val stashAsset: Asset, ) diff --git a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/validations/delegation/controller/SetControllerValidationPayload.kt b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/validations/delegation/controller/SetControllerValidationPayload.kt index c0b3046448..bc143884f7 100644 --- a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/validations/delegation/controller/SetControllerValidationPayload.kt +++ b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/validations/delegation/controller/SetControllerValidationPayload.kt @@ -1,11 +1,11 @@ package io.novafoundation.nova.feature_staking_impl.domain.validations.delegation.controller -import io.novafoundation.nova.feature_wallet_api.presentation.model.DecimalFee +import io.novafoundation.nova.feature_account_api.data.model.Fee import java.math.BigDecimal class SetControllerValidationPayload( val stashAddress: String, val controllerAddress: String, - val fee: DecimalFee, + val fee: Fee, val transferable: BigDecimal ) diff --git a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/validations/delegation/proxy/add/AddStakingProxyValidationPayload.kt b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/validations/delegation/proxy/add/AddStakingProxyValidationPayload.kt index b8ff5f4c5c..ac9f09289e 100644 --- a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/validations/delegation/proxy/add/AddStakingProxyValidationPayload.kt +++ b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/validations/delegation/proxy/add/AddStakingProxyValidationPayload.kt @@ -2,7 +2,7 @@ package io.novafoundation.nova.feature_staking_impl.domain.validations.delegatio import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.types.Balance import io.novafoundation.nova.feature_wallet_api.domain.model.Asset -import io.novafoundation.nova.feature_wallet_api.presentation.model.DecimalFee +import io.novafoundation.nova.feature_account_api.data.model.Fee import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain import io.novasama.substrate_sdk_android.runtime.AccountId @@ -11,7 +11,7 @@ class AddStakingProxyValidationPayload( val asset: Asset, val proxiedAccountId: AccountId, val proxyAddress: String, - val fee: DecimalFee, + val fee: Fee, val deltaDeposit: Balance, val currentQuantity: Int ) diff --git a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/validations/delegation/proxy/remove/RemoveStakingProxyValidationPayload.kt b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/validations/delegation/proxy/remove/RemoveStakingProxyValidationPayload.kt index 2dc0a18d5f..9779811d11 100644 --- a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/validations/delegation/proxy/remove/RemoveStakingProxyValidationPayload.kt +++ b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/validations/delegation/proxy/remove/RemoveStakingProxyValidationPayload.kt @@ -1,7 +1,7 @@ package io.novafoundation.nova.feature_staking_impl.domain.validations.delegation.proxy.remove import io.novafoundation.nova.feature_wallet_api.domain.model.Asset -import io.novafoundation.nova.feature_wallet_api.presentation.model.DecimalFee +import io.novafoundation.nova.feature_account_api.data.model.Fee import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain import io.novasama.substrate_sdk_android.runtime.AccountId @@ -10,5 +10,5 @@ class RemoveStakingProxyValidationPayload( val asset: Asset, val proxiedAccountId: AccountId, val proxyAddress: String, - val fee: DecimalFee + val fee: Fee ) diff --git a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/validations/payout/MakePayoutPayload.kt b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/validations/payout/MakePayoutPayload.kt index d08cf3ca22..a869d389fa 100644 --- a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/validations/payout/MakePayoutPayload.kt +++ b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/validations/payout/MakePayoutPayload.kt @@ -2,12 +2,12 @@ package io.novafoundation.nova.feature_staking_impl.domain.validations.payout import io.novafoundation.nova.feature_staking_impl.data.model.Payout import io.novafoundation.nova.feature_wallet_api.domain.model.Asset -import io.novafoundation.nova.feature_wallet_api.presentation.model.DecimalFee +import io.novafoundation.nova.feature_account_api.data.model.Fee import java.math.BigDecimal class MakePayoutPayload( val originAddress: String, - val fee: DecimalFee, + val fee: Fee, val totalReward: BigDecimal, val asset: Asset, val payouts: List diff --git a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/validations/rebond/RebondValidationPayload.kt b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/validations/rebond/RebondValidationPayload.kt index aacb003041..1ea4f022b5 100644 --- a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/validations/rebond/RebondValidationPayload.kt +++ b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/validations/rebond/RebondValidationPayload.kt @@ -1,11 +1,11 @@ package io.novafoundation.nova.feature_staking_impl.domain.validations.rebond import io.novafoundation.nova.feature_wallet_api.domain.model.Asset -import io.novafoundation.nova.feature_wallet_api.presentation.model.DecimalFee +import io.novafoundation.nova.feature_account_api.data.model.Fee import java.math.BigDecimal class RebondValidationPayload( val controllerAsset: Asset, - val fee: DecimalFee, + val fee: Fee, val rebondAmount: BigDecimal ) diff --git a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/validations/reedeem/RedeemValidationPayload.kt b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/validations/reedeem/RedeemValidationPayload.kt index f8cf40c06f..791049fd0f 100644 --- a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/validations/reedeem/RedeemValidationPayload.kt +++ b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/validations/reedeem/RedeemValidationPayload.kt @@ -1,9 +1,9 @@ package io.novafoundation.nova.feature_staking_impl.domain.validations.reedeem import io.novafoundation.nova.feature_wallet_api.domain.model.Asset -import io.novafoundation.nova.feature_wallet_api.presentation.model.DecimalFee +import io.novafoundation.nova.feature_account_api.data.model.Fee class RedeemValidationPayload( - val fee: DecimalFee, + val fee: Fee, val asset: Asset ) diff --git a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/validations/rewardDestination/RewardDestinationValidationPayload.kt b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/validations/rewardDestination/RewardDestinationValidationPayload.kt index afa7a67d6e..3e85613d7a 100644 --- a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/validations/rewardDestination/RewardDestinationValidationPayload.kt +++ b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/validations/rewardDestination/RewardDestinationValidationPayload.kt @@ -1,11 +1,11 @@ package io.novafoundation.nova.feature_staking_impl.domain.validations.rewardDestination import io.novafoundation.nova.feature_staking_api.domain.model.relaychain.StakingState -import io.novafoundation.nova.feature_wallet_api.presentation.model.DecimalFee +import io.novafoundation.nova.feature_account_api.data.model.Fee import java.math.BigDecimal class RewardDestinationValidationPayload( val availableControllerBalance: BigDecimal, - val fee: DecimalFee, + val fee: Fee, val stashState: StakingState.Stash ) diff --git a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/validations/setup/SetupStakingPayload.kt b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/validations/setup/SetupStakingPayload.kt index 47837216d2..b631032f01 100644 --- a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/validations/setup/SetupStakingPayload.kt +++ b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/validations/setup/SetupStakingPayload.kt @@ -1,9 +1,9 @@ package io.novafoundation.nova.feature_staking_impl.domain.validations.setup +import io.novafoundation.nova.feature_account_api.data.model.Fee import io.novafoundation.nova.feature_wallet_api.domain.model.Asset -import io.novafoundation.nova.feature_wallet_api.presentation.model.DecimalFee class SetupStakingPayload( - val maxFee: DecimalFee, + val maxFee: Fee, val controllerAsset: Asset, ) diff --git a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/validations/unbond/UnbondValidationPayload.kt b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/validations/unbond/UnbondValidationPayload.kt index a2b4b3eec6..bdbb3000fe 100644 --- a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/validations/unbond/UnbondValidationPayload.kt +++ b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/validations/unbond/UnbondValidationPayload.kt @@ -2,12 +2,12 @@ package io.novafoundation.nova.feature_staking_impl.domain.validations.unbond import io.novafoundation.nova.feature_staking_api.domain.model.relaychain.StakingState import io.novafoundation.nova.feature_wallet_api.domain.model.Asset -import io.novafoundation.nova.feature_wallet_api.presentation.model.DecimalFee +import io.novafoundation.nova.feature_account_api.data.model.Fee import java.math.BigDecimal data class UnbondValidationPayload( val stash: StakingState.Stash, - val fee: DecimalFee, + val fee: Fee, val amount: BigDecimal, val asset: Asset, ) diff --git a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/bagList/rebag/RebagViewModel.kt b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/bagList/rebag/RebagViewModel.kt index 9008950530..ded1ff7991 100644 --- a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/bagList/rebag/RebagViewModel.kt +++ b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/bagList/rebag/RebagViewModel.kt @@ -25,7 +25,7 @@ import io.novafoundation.nova.feature_staking_impl.presentation.bagList.rebag.mo import io.novafoundation.nova.feature_wallet_api.presentation.formatters.formatPlanksRange import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.FeeLoaderMixin import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.WithFeeLoaderMixin -import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.awaitDecimalFee +import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.awaitFee import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.create import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain import io.novafoundation.nova.runtime.state.chain @@ -115,7 +115,7 @@ class RebagViewModel( _showNextProgress.value = true val validationPayload = RebagValidationPayload( - fee = originFeeMixin.awaitDecimalFee(), + fee = originFeeMixin.awaitFee(), asset = stashAsset.first() ) diff --git a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/nominationPools/bondMore/confirm/NominationPoolsConfirmBondMoreViewModel.kt b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/nominationPools/bondMore/confirm/NominationPoolsConfirmBondMoreViewModel.kt index e87d40133b..0e3c061655 100644 --- a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/nominationPools/bondMore/confirm/NominationPoolsConfirmBondMoreViewModel.kt +++ b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/nominationPools/bondMore/confirm/NominationPoolsConfirmBondMoreViewModel.kt @@ -52,7 +52,7 @@ class NominationPoolsConfirmBondMoreViewModel( ExternalActions by externalActions, Validatable by validationExecutor { - private val decimalFee = mapFeeFromParcel(payload.fee) + private val submissionFee = mapFeeFromParcel(payload.fee) private val _showNextProgress = MutableStateFlow(false) val showNextProgress: Flow = _showNextProgress @@ -71,7 +71,7 @@ class NominationPoolsConfirmBondMoreViewModel( .shareInBackground() val feeStatusFlow = assetFlow.map { asset -> - val feeModel = mapFeeToFeeModel(decimalFee.genericFee, asset.token) + val feeModel = mapFeeToFeeModel(submissionFee, asset.token) FeeStatus.Loaded(feeModel) } @@ -101,7 +101,7 @@ class NominationPoolsConfirmBondMoreViewModel( private fun maybeGoToNext() = launch { val payload = NominationPoolsBondMoreValidationPayload( - fee = decimalFee, + fee = submissionFee, amount = amountFlow.first(), poolMember = poolMember.first(), asset = assetFlow.first() diff --git a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/nominationPools/bondMore/setup/NominationPoolsSetupBondMoreViewModel.kt b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/nominationPools/bondMore/setup/NominationPoolsSetupBondMoreViewModel.kt index 9e77db7116..349636c457 100644 --- a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/nominationPools/bondMore/setup/NominationPoolsSetupBondMoreViewModel.kt +++ b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/nominationPools/bondMore/setup/NominationPoolsSetupBondMoreViewModel.kt @@ -20,7 +20,7 @@ import io.novafoundation.nova.feature_wallet_api.domain.model.Asset import io.novafoundation.nova.feature_wallet_api.presentation.mixin.amountChooser.AmountChooserMixin import io.novafoundation.nova.feature_wallet_api.presentation.mixin.amountChooser.setAmount import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.FeeLoaderMixin -import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.awaitDecimalFee +import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.awaitFee 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 @@ -97,7 +97,7 @@ class NominationPoolsSetupBondMoreViewModel( private fun maybeGoToNext() = launch { showNextProgress.value = true - val fee = originFeeMixin.awaitDecimalFee() + val fee = originFeeMixin.awaitFee() val payload = NominationPoolsBondMoreValidationPayload( fee = fee, diff --git a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/nominationPools/claimRewards/NominationPoolsClaimRewardsViewModel.kt b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/nominationPools/claimRewards/NominationPoolsClaimRewardsViewModel.kt index fbb695455d..161f0f7ced 100644 --- a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/nominationPools/claimRewards/NominationPoolsClaimRewardsViewModel.kt +++ b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/nominationPools/claimRewards/NominationPoolsClaimRewardsViewModel.kt @@ -19,7 +19,7 @@ import io.novafoundation.nova.feature_staking_impl.domain.nominationPools.claimR import io.novafoundation.nova.feature_staking_impl.presentation.NominationPoolsRouter import io.novafoundation.nova.feature_wallet_api.domain.AssetUseCase import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.FeeLoaderMixin -import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.awaitDecimalFee +import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.awaitFee 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.model.mapAmountToAmountModel @@ -103,7 +103,7 @@ class NominationPoolsClaimRewardsViewModel( val pendingRewards = pendingRewards.first() val payload = NominationPoolsClaimRewardsValidationPayload( - fee = feeLoaderMixin.awaitDecimalFee(), + fee = feeLoaderMixin.awaitFee(), pendingRewardsPlanks = pendingRewards.amount, asset = assetFlow.first(), chain = stakingSharedState.chain(), diff --git a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/nominationPools/redeem/NominationPoolsRedeemViewModel.kt b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/nominationPools/redeem/NominationPoolsRedeemViewModel.kt index ee58bc0738..9ed8f5c364 100644 --- a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/nominationPools/redeem/NominationPoolsRedeemViewModel.kt +++ b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/nominationPools/redeem/NominationPoolsRedeemViewModel.kt @@ -20,7 +20,7 @@ import io.novafoundation.nova.feature_staking_impl.domain.nominationPools.redeem import io.novafoundation.nova.feature_staking_impl.presentation.StakingRouter import io.novafoundation.nova.feature_wallet_api.domain.AssetUseCase import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.FeeLoaderMixin -import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.awaitDecimalFee +import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.awaitFee 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.model.mapAmountToAmountModel @@ -106,7 +106,7 @@ class NominationPoolsRedeemViewModel( _showNextProgress.value = true val payload = NominationPoolsRedeemValidationPayload( - fee = feeLoaderMixin.awaitDecimalFee(), + fee = feeLoaderMixin.awaitFee(), asset = assetFlow.first(), chain = stakingSharedState.chain(), poolMember = poolMemberFlow.first() diff --git a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/nominationPools/unbond/confirm/NominationPoolsConfirmUnbondViewModel.kt b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/nominationPools/unbond/confirm/NominationPoolsConfirmUnbondViewModel.kt index d54df9df77..ab1093e3e0 100644 --- a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/nominationPools/unbond/confirm/NominationPoolsConfirmUnbondViewModel.kt +++ b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/nominationPools/unbond/confirm/NominationPoolsConfirmUnbondViewModel.kt @@ -69,7 +69,7 @@ class NominationPoolsConfirmUnbondViewModel( .shareInBackground() val feeStatusFlow = assetFlow.map { asset -> - val feeModel = mapFeeToFeeModel(decimalFee.genericFee, asset.token) + val feeModel = mapFeeToFeeModel(decimalFee, asset.token) FeeStatus.Loaded(feeModel) } diff --git a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/nominationPools/unbond/setup/NominationPoolsSetupUnbondViewModel.kt b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/nominationPools/unbond/setup/NominationPoolsSetupUnbondViewModel.kt index 9eafdda2f2..ca2e26fbf9 100644 --- a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/nominationPools/unbond/setup/NominationPoolsSetupUnbondViewModel.kt +++ b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/nominationPools/unbond/setup/NominationPoolsSetupUnbondViewModel.kt @@ -21,7 +21,7 @@ import io.novafoundation.nova.feature_wallet_api.domain.AssetUseCase import io.novafoundation.nova.feature_wallet_api.domain.model.amountFromPlanks 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.awaitDecimalFee +import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.awaitFee 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 @@ -112,7 +112,7 @@ class NominationPoolsSetupUnbondViewModel( val stakedBalance = asset.token.amountFromPlanks(stakedBalance.first()) val payload = NominationPoolsUnbondValidationPayload( - fee = originFeeMixin.awaitDecimalFee(), + fee = originFeeMixin.awaitFee(), poolMember = poolMemberFlow.first(), stakedBalance = stakedBalance, asset = asset, diff --git a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/parachainStaking/rebond/ParachainStakingRebondViewModel.kt b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/parachainStaking/rebond/ParachainStakingRebondViewModel.kt index d11875b3bf..29143cce2a 100644 --- a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/parachainStaking/rebond/ParachainStakingRebondViewModel.kt +++ b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/parachainStaking/rebond/ParachainStakingRebondViewModel.kt @@ -25,7 +25,7 @@ import io.novafoundation.nova.feature_staking_impl.presentation.parachainStaking import io.novafoundation.nova.feature_staking_impl.presentation.validators.details.StakeTargetDetailsPayload import io.novafoundation.nova.feature_wallet_api.domain.AssetUseCase import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.FeeLoaderMixin -import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.awaitDecimalFee +import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.awaitFee import io.novafoundation.nova.feature_wallet_api.presentation.model.mapAmountToAmountModel import io.novafoundation.nova.runtime.state.AnySelectedAssetOptionSharedState import io.novafoundation.nova.runtime.state.chain @@ -129,7 +129,7 @@ class ParachainStakingRebondViewModel( _showNextProgress.value = true val payload = ParachainStakingRebondValidationPayload( - fee = feeLoaderMixin.awaitDecimalFee(), + fee = feeLoaderMixin.awaitFee(), asset = assetFlow.first() ) diff --git a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/parachainStaking/redeem/ParachainStakingRedeemViewModel.kt b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/parachainStaking/redeem/ParachainStakingRedeemViewModel.kt index c816c8c289..0e699b6648 100644 --- a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/parachainStaking/redeem/ParachainStakingRedeemViewModel.kt +++ b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/parachainStaking/redeem/ParachainStakingRedeemViewModel.kt @@ -20,7 +20,7 @@ import io.novafoundation.nova.feature_staking_impl.domain.parachainStaking.redee import io.novafoundation.nova.feature_staking_impl.presentation.StakingRouter import io.novafoundation.nova.feature_wallet_api.domain.AssetUseCase import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.FeeLoaderMixin -import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.awaitDecimalFee +import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.awaitFee import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.connectWith import io.novafoundation.nova.feature_wallet_api.presentation.model.mapAmountToAmountModel import io.novafoundation.nova.runtime.state.AnySelectedAssetOptionSharedState @@ -109,7 +109,7 @@ class ParachainStakingRedeemViewModel( _showNextProgress.value = true val payload = ParachainStakingRedeemValidationPayload( - fee = feeLoaderMixin.awaitDecimalFee(), + fee = feeLoaderMixin.awaitFee(), asset = assetFlow.first() ) diff --git a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/parachainStaking/start/confirm/ConfirmStartParachainStakingViewModel.kt b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/parachainStaking/start/confirm/ConfirmStartParachainStakingViewModel.kt index ee6cbc3bfe..7e06de9fc1 100644 --- a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/parachainStaking/start/confirm/ConfirmStartParachainStakingViewModel.kt +++ b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/parachainStaking/start/confirm/ConfirmStartParachainStakingViewModel.kt @@ -151,7 +151,7 @@ class ConfirmStartParachainStakingViewModel( } private fun setInitialFee() = launch { - feeLoaderMixin.setFee(decimalFee.genericFee) + feeLoaderMixin.setFee(decimalFee) } private fun sendTransactionIfValid() = launch { 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 94aef07b3d..cae81a898c 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 @@ -14,6 +14,7 @@ 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 +import io.novafoundation.nova.feature_account_api.data.model.Fee import io.novafoundation.nova.feature_staking_api.domain.model.parachain.DelegatorState import io.novafoundation.nova.feature_staking_api.domain.model.parachain.delegationAmountTo import io.novafoundation.nova.feature_staking_api.domain.model.parachain.stakeablePlanks @@ -47,10 +48,9 @@ import io.novafoundation.nova.feature_wallet_api.domain.AssetUseCase import io.novafoundation.nova.feature_wallet_api.domain.model.amountFromPlanks 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.awaitDecimalFee +import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.awaitFee import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.connectWith 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 io.novasama.substrate_sdk_android.extensions.fromHex @@ -287,7 +287,7 @@ class StartParachainStakingViewModel( val payload = StartParachainStakingValidationPayload( amount = amount, - fee = feeLoaderMixin.awaitDecimalFee(), + fee = feeLoaderMixin.awaitFee(), asset = assetFlow.first(), collator = collator, delegatorState = currentDelegatorStateFlow.first(), @@ -306,7 +306,7 @@ class StartParachainStakingViewModel( } private fun goToNextStep( - fee: DecimalFee, + fee: Fee, amount: BigDecimal, collator: Collator, ) = launch { diff --git a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/parachainStaking/unbond/confirm/ParachainStakingUnbondConfirmViewModel.kt b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/parachainStaking/unbond/confirm/ParachainStakingUnbondConfirmViewModel.kt index 4564bdab98..2296b9b331 100644 --- a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/parachainStaking/unbond/confirm/ParachainStakingUnbondConfirmViewModel.kt +++ b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/parachainStaking/unbond/confirm/ParachainStakingUnbondConfirmViewModel.kt @@ -128,7 +128,7 @@ class ParachainStakingUnbondConfirmViewModel( } private fun setInitialFee() = launch { - feeLoaderMixin.setFee(decimalFee.genericFee) + feeLoaderMixin.setFee(decimalFee) } private fun sendTransactionIfValid() = launch { diff --git a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/parachainStaking/unbond/setup/ParachainStakingUnbondViewModel.kt b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/parachainStaking/unbond/setup/ParachainStakingUnbondViewModel.kt index c46c250514..625d577fb6 100644 --- a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/parachainStaking/unbond/setup/ParachainStakingUnbondViewModel.kt +++ b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/parachainStaking/unbond/setup/ParachainStakingUnbondViewModel.kt @@ -12,6 +12,7 @@ import io.novafoundation.nova.common.utils.orZero import io.novafoundation.nova.common.utils.singleReplaySharedFlow import io.novafoundation.nova.common.validation.ValidationExecutor import io.novafoundation.nova.common.validation.progressConsumer +import io.novafoundation.nova.feature_account_api.data.model.Fee import io.novafoundation.nova.feature_staking_api.domain.model.parachain.DelegatorState import io.novafoundation.nova.feature_staking_api.domain.model.parachain.delegationAmountTo import io.novafoundation.nova.feature_staking_impl.R @@ -36,10 +37,9 @@ import io.novafoundation.nova.feature_wallet_api.domain.AssetUseCase import io.novafoundation.nova.feature_wallet_api.domain.model.Asset 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.awaitDecimalFee +import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.awaitFee import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.connectWith 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.feature_wallet_api.presentation.model.transferableAmountModel import io.novasama.substrate_sdk_android.extensions.fromHex @@ -214,7 +214,7 @@ class ParachainStakingUnbondViewModel( val payload = ParachainStakingUnbondValidationPayload( amount = amountChooserMixin.amount.first(), - fee = feeLoaderMixin.awaitDecimalFee(), + fee = feeLoaderMixin.awaitFee(), asset = assetFlow.first(), collator = selectedCollatorFlow.first() ) @@ -233,7 +233,7 @@ class ParachainStakingUnbondViewModel( } private fun goToNextStep( - fee: DecimalFee, + fee: Fee, amount: BigDecimal, collator: Collator, ) = launch { diff --git a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/parachainStaking/yieldBoost/confirm/YieldBoostConfirmViewModel.kt b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/parachainStaking/yieldBoost/confirm/YieldBoostConfirmViewModel.kt index f274da32b0..ea4138ace2 100644 --- a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/parachainStaking/yieldBoost/confirm/YieldBoostConfirmViewModel.kt +++ b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/parachainStaking/yieldBoost/confirm/YieldBoostConfirmViewModel.kt @@ -141,7 +141,7 @@ class YieldBoostConfirmViewModel( } private fun setInitialFee() = launch { - feeLoaderMixin.setFee(decimalFee.genericFee) + feeLoaderMixin.setFee(decimalFee) } private fun sendTransactionIfValid() = launch { diff --git a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/parachainStaking/yieldBoost/setup/SetupYieldBoostViewModel.kt b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/parachainStaking/yieldBoost/setup/SetupYieldBoostViewModel.kt index 688236578c..a3cc5d902d 100644 --- a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/parachainStaking/yieldBoost/setup/SetupYieldBoostViewModel.kt +++ b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/parachainStaking/yieldBoost/setup/SetupYieldBoostViewModel.kt @@ -16,6 +16,7 @@ import io.novafoundation.nova.common.utils.mapToSet import io.novafoundation.nova.common.utils.orZero import io.novafoundation.nova.common.validation.ValidationExecutor import io.novafoundation.nova.common.validation.progressConsumer +import io.novafoundation.nova.feature_account_api.data.model.Fee import io.novafoundation.nova.feature_staking_api.domain.model.parachain.DelegatorState import io.novafoundation.nova.feature_staking_api.domain.model.parachain.delegationAmountTo import io.novafoundation.nova.feature_staking_impl.R @@ -53,10 +54,9 @@ 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.amountChooser.setAmountInput import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.FeeLoaderMixin -import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.awaitDecimalFee +import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.awaitFee import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.connectWith import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.mapFeeToParcel -import io.novafoundation.nova.feature_wallet_api.presentation.model.DecimalFee import io.novasama.substrate_sdk_android.extensions.toHexString import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.FlowPreview @@ -339,7 +339,7 @@ class SetupYieldBoostViewModel( val payload = YieldBoostValidationPayload( collator = selectedCollatorFlow.first(), configuration = modifiedYieldBoostConfiguration.first(), - fee = feeLoaderMixin.awaitDecimalFee(), + fee = feeLoaderMixin.awaitFee(), activeTasks = activeTasksFlow.first(), asset = assetFlow.first() ) @@ -357,7 +357,7 @@ class SetupYieldBoostViewModel( } private fun goToNextStep( - fee: DecimalFee, + fee: Fee, configuration: YieldBoostConfiguration, collator: Collator, ) = launch { diff --git a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/payouts/confirm/ConfirmPayoutViewModel.kt b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/payouts/confirm/ConfirmPayoutViewModel.kt index c5ee482861..2a6351b55e 100644 --- a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/payouts/confirm/ConfirmPayoutViewModel.kt +++ b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/payouts/confirm/ConfirmPayoutViewModel.kt @@ -26,7 +26,7 @@ import io.novafoundation.nova.feature_staking_impl.presentation.payouts.confirm. import io.novafoundation.nova.feature_staking_impl.presentation.payouts.model.mapPendingPayoutParcelToPayout import io.novafoundation.nova.feature_wallet_api.domain.model.amountFromPlanks import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.FeeLoaderMixin -import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.awaitDecimalFee +import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.awaitFee import io.novafoundation.nova.feature_wallet_api.presentation.model.mapAmountToAmountModel import io.novafoundation.nova.runtime.state.AnySelectedAssetOptionSharedState import io.novafoundation.nova.runtime.state.chain @@ -108,7 +108,7 @@ class ConfirmPayoutViewModel( val makePayoutPayload = MakePayoutPayload( originAddress = accountAddress, - fee = feeLoaderMixin.awaitDecimalFee(), + fee = feeLoaderMixin.awaitFee(), totalReward = amount, asset = asset, payouts = payouts diff --git a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/staking/bond/confirm/ConfirmBondMoreViewModel.kt b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/staking/bond/confirm/ConfirmBondMoreViewModel.kt index 10fa478f0d..785f978900 100644 --- a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/staking/bond/confirm/ConfirmBondMoreViewModel.kt +++ b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/staking/bond/confirm/ConfirmBondMoreViewModel.kt @@ -71,7 +71,7 @@ class ConfirmBondMoreViewModel( .shareInBackground() val feeStatusFlow = stashAssetFlow.map { asset -> - val feeModel = mapFeeToFeeModel(decimalFee.genericFee, asset.token) + val feeModel = mapFeeToFeeModel(decimalFee, asset.token) FeeStatus.Loaded(feeModel) } diff --git a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/staking/bond/select/SelectBondMoreViewModel.kt b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/staking/bond/select/SelectBondMoreViewModel.kt index 9d29e40aec..f3f966a7f3 100644 --- a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/staking/bond/select/SelectBondMoreViewModel.kt +++ b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/staking/bond/select/SelectBondMoreViewModel.kt @@ -24,7 +24,7 @@ import io.novafoundation.nova.feature_wallet_api.domain.model.Asset 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.awaitDecimalFee +import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.awaitFee import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.mapFeeToParcel import kotlinx.coroutines.flow.filterIsInstance import kotlinx.coroutines.flow.first @@ -109,7 +109,7 @@ class SelectBondMoreViewModel( val payload = BondMoreValidationPayload( stashAddress = stashAddress(), - fee = feeLoaderMixin.awaitDecimalFee(), + fee = feeLoaderMixin.awaitFee(), amount = amountChooserMixin.amount.first(), stashAsset = assetFlow.first() ) diff --git a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/staking/delegation/controller/confirm/ConfirmSetControllerViewModel.kt b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/staking/delegation/controller/confirm/ConfirmSetControllerViewModel.kt index c88f923a09..cbd32e8fe4 100644 --- a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/staking/delegation/controller/confirm/ConfirmSetControllerViewModel.kt +++ b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/staking/delegation/controller/confirm/ConfirmSetControllerViewModel.kt @@ -50,7 +50,7 @@ class ConfirmSetControllerViewModel( .share() val feeStatusFlow = assetFlow.map { asset -> - val feeModel = mapFeeToFeeModel(decimalFee.genericFee, asset.token) + val feeModel = mapFeeToFeeModel(decimalFee, asset.token) FeeStatus.Loaded(feeModel) } diff --git a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/staking/delegation/controller/set/SetControllerViewModel.kt b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/staking/delegation/controller/set/SetControllerViewModel.kt index d8d64f4e1a..cd25aa4e88 100644 --- a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/staking/delegation/controller/set/SetControllerViewModel.kt +++ b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/staking/delegation/controller/set/SetControllerViewModel.kt @@ -36,7 +36,7 @@ import io.novafoundation.nova.feature_staking_impl.domain.validations.delegation import io.novafoundation.nova.feature_staking_impl.presentation.StakingRouter import io.novafoundation.nova.feature_staking_impl.presentation.staking.delegation.controller.confirm.ConfirmSetControllerPayload import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.FeeLoaderMixin -import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.awaitDecimalFee +import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.awaitFee import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.mapFeeToParcel import io.novafoundation.nova.runtime.state.AnySelectedAssetOptionSharedState import io.novafoundation.nova.runtime.state.chain @@ -209,7 +209,7 @@ class SetControllerViewModel( val payload = SetControllerValidationPayload( stashAddress = stashAddress(), controllerAddress = controllerAddress, - fee = feeLoaderMixin.awaitDecimalFee(), + fee = feeLoaderMixin.awaitFee(), transferable = assetFlow.first().transferable ) diff --git a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/staking/delegation/proxy/add/confirm/ConfirmAddStakingProxyViewModel.kt b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/staking/delegation/proxy/add/confirm/ConfirmAddStakingProxyViewModel.kt index 448e11db6e..cc26192da9 100644 --- a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/staking/delegation/proxy/add/confirm/ConfirmAddStakingProxyViewModel.kt +++ b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/staking/delegation/proxy/add/confirm/ConfirmAddStakingProxyViewModel.kt @@ -69,7 +69,7 @@ class ConfirmAddStakingProxyViewModel( .flatMapLatest { assetUseCase.assetFlow(it) } .shareInBackground() - private val decimalFeeFlow = flowOf { mapFeeFromParcel(payload.fee) } + private val feeFlow = flowOf { mapFeeFromParcel(payload.fee) } .shareInBackground() val chainModel = chainFlow.map { chain -> @@ -88,8 +88,8 @@ class ConfirmAddStakingProxyViewModel( mapAmountToAmountModel(payload.deltaDeposit, asset) } - val feeModelFlow = combine(assetFlow, decimalFeeFlow) { asset, decimalFee -> - mapAmountToAmountModel(decimalFee.networkFee.amount, asset) + val feeModelFlow = combine(assetFlow, feeFlow) { asset, fee -> + mapAmountToAmountModel(fee.amount, asset) } val proxyAccountModel = chainFlow.map { chain -> @@ -110,7 +110,7 @@ class ConfirmAddStakingProxyViewModel( asset = assetFlow.first(), proxyAddress = payload.proxyAddress, proxiedAccountId = metaAccount.requireAccountIdIn(chain), - fee = decimalFeeFlow.first(), + fee = feeFlow.first(), deltaDeposit = payload.deltaDeposit, currentQuantity = payload.currentQuantity ) diff --git a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/staking/delegation/proxy/add/set/AddStakingProxyViewModel.kt b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/staking/delegation/proxy/add/set/AddStakingProxyViewModel.kt index cff0784102..65ad35751c 100644 --- a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/staking/delegation/proxy/add/set/AddStakingProxyViewModel.kt +++ b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/staking/delegation/proxy/add/set/AddStakingProxyViewModel.kt @@ -30,7 +30,7 @@ import io.novafoundation.nova.feature_staking_impl.presentation.staking.delegati import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.types.Balance import io.novafoundation.nova.feature_wallet_api.domain.ArbitraryAssetUseCase import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.FeeLoaderMixin -import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.awaitDecimalFee +import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.awaitFee 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.AmountModel @@ -183,7 +183,7 @@ class AddStakingProxyViewModel( asset = selectedAssetFlow.first(), proxyAddress = addressInputMixin.getAddress(), proxiedAccountId = metaAccount.requireAccountIdIn(chain), - fee = feeMixin.awaitDecimalFee(), + fee = feeMixin.awaitFee(), deltaDeposit = proxyDepositDelta.first(), currentQuantity = getProxyRepository.getProxiesQuantity(chain.id, proxiedAccountId) ) diff --git a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/staking/delegation/proxy/revoke/ConfirmRemoveStakingProxyViewModel.kt b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/staking/delegation/proxy/revoke/ConfirmRemoveStakingProxyViewModel.kt index cf93fc44b7..e6c085af0c 100644 --- a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/staking/delegation/proxy/revoke/ConfirmRemoveStakingProxyViewModel.kt +++ b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/staking/delegation/proxy/revoke/ConfirmRemoveStakingProxyViewModel.kt @@ -20,7 +20,7 @@ import io.novafoundation.nova.feature_staking_impl.presentation.StakingRouter import io.novafoundation.nova.feature_staking_impl.presentation.staking.delegation.proxy.common.mapRemoveStakingProxyValidationFailureToUi import io.novafoundation.nova.feature_wallet_api.domain.ArbitraryAssetUseCase import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.FeeLoaderMixin -import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.awaitDecimalFee +import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.awaitFee import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.create import io.novafoundation.nova.runtime.ext.accountIdOf import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain @@ -112,7 +112,7 @@ class ConfirmRemoveStakingProxyViewModel( asset = assetFlow.first(), proxyAddress = payload.proxyAddress, proxiedAccountId = metaAccount.requireAccountIdIn(chain), - fee = feeMixin.awaitDecimalFee() + fee = feeMixin.awaitFee() ) validationExecutor.requireValid( diff --git a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/staking/rebond/confirm/ConfirmRebondViewModel.kt b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/staking/rebond/confirm/ConfirmRebondViewModel.kt index cc5a82b565..1145227d25 100644 --- a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/staking/rebond/confirm/ConfirmRebondViewModel.kt +++ b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/staking/rebond/confirm/ConfirmRebondViewModel.kt @@ -24,7 +24,7 @@ import io.novafoundation.nova.feature_staking_impl.presentation.StakingRouter import io.novafoundation.nova.feature_staking_impl.presentation.staking.rebond.rebondValidationFailure 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.awaitDecimalFee +import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.awaitFee import io.novafoundation.nova.feature_wallet_api.presentation.model.mapAmountToAmountModel import io.novafoundation.nova.runtime.state.AnySelectedAssetOptionSharedState import io.novafoundation.nova.runtime.state.chain @@ -120,7 +120,7 @@ class ConfirmRebondViewModel( _showNextProgress.value = true val payload = RebondValidationPayload( - fee = feeLoaderMixin.awaitDecimalFee(), + fee = feeLoaderMixin.awaitFee(), rebondAmount = payload.amount, controllerAsset = assetFlow.first() ) diff --git a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/staking/rebond/custom/CustomRebondViewModel.kt b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/staking/rebond/custom/CustomRebondViewModel.kt index 8c980c1d70..c14edcb3c6 100644 --- a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/staking/rebond/custom/CustomRebondViewModel.kt +++ b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/staking/rebond/custom/CustomRebondViewModel.kt @@ -22,7 +22,7 @@ import io.novafoundation.nova.feature_wallet_api.domain.model.Asset 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.awaitDecimalFee +import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.awaitFee import io.novafoundation.nova.feature_wallet_api.presentation.model.transferableAmountModelOf import kotlinx.coroutines.flow.filterIsInstance import kotlinx.coroutines.flow.first @@ -106,7 +106,7 @@ class CustomRebondViewModel( private fun maybeGoToNext() = launch { val payload = RebondValidationPayload( - fee = feeLoaderMixin.awaitDecimalFee(), + fee = feeLoaderMixin.awaitFee(), rebondAmount = amountChooserMixin.amount.first(), controllerAsset = assetFlow.first() ) diff --git a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/staking/redeem/RedeemViewModel.kt b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/staking/redeem/RedeemViewModel.kt index 76ebc73767..1639b64746 100644 --- a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/staking/redeem/RedeemViewModel.kt +++ b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/staking/redeem/RedeemViewModel.kt @@ -20,7 +20,7 @@ import io.novafoundation.nova.feature_staking_impl.domain.validations.reedeem.Re import io.novafoundation.nova.feature_staking_impl.domain.validations.reedeem.RedeemValidationSystem import io.novafoundation.nova.feature_staking_impl.presentation.StakingRouter import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.FeeLoaderMixin -import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.awaitDecimalFee +import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.awaitFee import io.novafoundation.nova.feature_wallet_api.presentation.model.mapAmountToAmountModel import io.novafoundation.nova.runtime.state.AnySelectedAssetOptionSharedState import io.novafoundation.nova.runtime.state.chain @@ -104,7 +104,7 @@ class RedeemViewModel( val asset = assetFlow.first() val validationPayload = RedeemValidationPayload( - fee = feeLoaderMixin.awaitDecimalFee(), + fee = feeLoaderMixin.awaitFee(), asset = asset ) diff --git a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/staking/rewardDestination/confirm/ConfirmRewardDestinationViewModel.kt b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/staking/rewardDestination/confirm/ConfirmRewardDestinationViewModel.kt index 23471c377f..e2ae564892 100644 --- a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/staking/rewardDestination/confirm/ConfirmRewardDestinationViewModel.kt +++ b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/staking/rewardDestination/confirm/ConfirmRewardDestinationViewModel.kt @@ -81,7 +81,7 @@ class ConfirmRewardDestinationViewModel( .shareInBackground() val feeStatusFlow = controllerAssetFlow.map { - FeeStatus.Loaded(mapFeeToFeeModel(decimalFee.genericFee, it.token)) + FeeStatus.Loaded(mapFeeToFeeModel(decimalFee, it.token)) } .shareInBackground() diff --git a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/staking/rewardDestination/select/SelectRewardDestinationViewModel.kt b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/staking/rewardDestination/select/SelectRewardDestinationViewModel.kt index dd1cecdc08..cf7f4e844e 100644 --- a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/staking/rewardDestination/select/SelectRewardDestinationViewModel.kt +++ b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/staking/rewardDestination/select/SelectRewardDestinationViewModel.kt @@ -10,6 +10,7 @@ import io.novafoundation.nova.common.resources.ResourceManager import io.novafoundation.nova.common.utils.inBackground import io.novafoundation.nova.common.validation.ValidationExecutor import io.novafoundation.nova.common.validation.progressConsumer +import io.novafoundation.nova.feature_account_api.data.model.Fee import io.novafoundation.nova.feature_staking_api.domain.model.RewardDestination import io.novafoundation.nova.feature_staking_api.domain.model.relaychain.StakingState import io.novafoundation.nova.feature_staking_impl.data.StakingSharedState @@ -25,9 +26,8 @@ import io.novafoundation.nova.feature_staking_impl.presentation.common.rewardDes import io.novafoundation.nova.feature_staking_impl.presentation.staking.rewardDestination.confirm.parcel.ConfirmRewardDestinationPayload import io.novafoundation.nova.feature_staking_impl.presentation.staking.rewardDestination.confirm.parcel.RewardDestinationParcelModel import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.FeeLoaderMixin -import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.awaitDecimalFee +import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.awaitFee 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.runtime.state.selectedOption import kotlinx.coroutines.async import kotlinx.coroutines.flow.combine @@ -118,7 +118,7 @@ class SelectRewardDestinationViewModel( val payload = RewardDestinationValidationPayload( availableControllerBalance = controllerAssetFlow.first().transferable, - fee = feeLoaderMixin.awaitDecimalFee(), + fee = feeLoaderMixin.awaitFee(), stashState = stashStateFlow.first() ) @@ -136,7 +136,7 @@ class SelectRewardDestinationViewModel( } } - private fun goToNextStep(rewardDestination: RewardDestinationModel, fee: DecimalFee) { + private fun goToNextStep(rewardDestination: RewardDestinationModel, fee: Fee) { val payload = ConfirmRewardDestinationPayload( fee = mapFeeToParcel(fee), rewardDestination = mapRewardDestinationModelToRewardDestinationParcelModel(rewardDestination) diff --git a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/staking/start/confirm/ConfirmMultiStakingViewModel.kt b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/staking/start/confirm/ConfirmMultiStakingViewModel.kt index 485ba0204e..2c2fd7ead5 100644 --- a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/staking/start/confirm/ConfirmMultiStakingViewModel.kt +++ b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/staking/start/confirm/ConfirmMultiStakingViewModel.kt @@ -104,7 +104,7 @@ class ConfirmMultiStakingViewModel( .shareInBackground() val feeStatusFlow = assetFlow.map { asset -> - val feeModel = mapFeeToFeeModel(decimalFee.genericFee, asset.token) + val feeModel = mapFeeToFeeModel(decimalFee, asset.token) FeeStatus.Loaded(feeModel) } diff --git a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/staking/start/setupAmount/SetupAmountMultiStakingViewModel.kt b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/staking/start/setupAmount/SetupAmountMultiStakingViewModel.kt index b9b4c85041..a608be391a 100644 --- a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/staking/start/setupAmount/SetupAmountMultiStakingViewModel.kt +++ b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/staking/start/setupAmount/SetupAmountMultiStakingViewModel.kt @@ -28,7 +28,7 @@ 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.amountChooser.setAmount import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.FeeLoaderMixin -import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.awaitDecimalFee +import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.awaitFee 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 @@ -158,7 +158,7 @@ class SetupAmountMultiStakingViewModel( val payload = StartMultiStakingValidationPayload( recommendableSelection = recommendableSelection, asset = currentAssetFlow.first(), - fee = feeLoaderMixin.awaitDecimalFee() + fee = feeLoaderMixin.awaitFee() ) validationExecutor.requireValid( diff --git a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/staking/unbond/confirm/ConfirmUnbondViewModel.kt b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/staking/unbond/confirm/ConfirmUnbondViewModel.kt index 0073a07f6e..42e01883cc 100644 --- a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/staking/unbond/confirm/ConfirmUnbondViewModel.kt +++ b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/staking/unbond/confirm/ConfirmUnbondViewModel.kt @@ -80,7 +80,7 @@ class ConfirmUnbondViewModel( .shareInBackground() val feeStatusLiveData = assetFlow.map { asset -> - val feeModel = mapFeeToFeeModel(decimalFee.genericFee, asset.token) + val feeModel = mapFeeToFeeModel(decimalFee, asset.token) FeeStatus.Loaded(feeModel) } diff --git a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/staking/unbond/select/SelectUnbondViewModel.kt b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/staking/unbond/select/SelectUnbondViewModel.kt index 09a2044f1a..fc4291d0db 100644 --- a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/staking/unbond/select/SelectUnbondViewModel.kt +++ b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/staking/unbond/select/SelectUnbondViewModel.kt @@ -22,7 +22,7 @@ import io.novafoundation.nova.feature_wallet_api.domain.model.Asset 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.awaitDecimalFee +import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.awaitFee import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.mapFeeToParcel import io.novafoundation.nova.feature_wallet_api.presentation.model.transferableAmountModelOf import kotlinx.coroutines.flow.filterIsInstance @@ -110,7 +110,7 @@ class SelectUnbondViewModel( val payload = UnbondValidationPayload( stash = accountStakingFlow.first(), asset = asset, - fee = feeLoaderMixin.awaitDecimalFee(), + fee = feeLoaderMixin.awaitFee(), amount = amountMixin.amount.first(), ) diff --git a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/validators/change/confirm/ConfirmChangeValidatorsViewModel.kt b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/validators/change/confirm/ConfirmChangeValidatorsViewModel.kt index 6dbdb7054e..740c60a999 100644 --- a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/validators/change/confirm/ConfirmChangeValidatorsViewModel.kt +++ b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/validators/change/confirm/ConfirmChangeValidatorsViewModel.kt @@ -35,7 +35,7 @@ import io.novafoundation.nova.feature_staking_impl.presentation.validators.chang import io.novafoundation.nova.feature_staking_impl.presentation.validators.change.confirm.hints.ConfirmStakeHintsMixinFactory import io.novafoundation.nova.feature_staking_impl.presentation.validators.change.reset import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.FeeLoaderMixin -import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.awaitDecimalFee +import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.awaitFee import io.novafoundation.nova.runtime.state.AnySelectedAssetOptionSharedState import io.novafoundation.nova.runtime.state.chain import kotlinx.coroutines.flow.filterIsInstance @@ -140,7 +140,7 @@ class ConfirmChangeValidatorsViewModel( _showNextProgress.value = true val payload = SetupStakingPayload( - maxFee = feeLoaderMixin.awaitDecimalFee(), + maxFee = feeLoaderMixin.awaitFee(), controllerAsset = controllerAssetFlow.first() ) diff --git a/feature-swap-api/src/main/java/io/novafoundation/nova/feature_swap_api/domain/model/SwapFee.kt b/feature-swap-api/src/main/java/io/novafoundation/nova/feature_swap_api/domain/model/SwapFee.kt index db542fb537..9a059ba983 100644 --- a/feature-swap-api/src/main/java/io/novafoundation/nova/feature_swap_api/domain/model/SwapFee.kt +++ b/feature-swap-api/src/main/java/io/novafoundation/nova/feature_swap_api/domain/model/SwapFee.kt @@ -1,15 +1,15 @@ package io.novafoundation.nova.feature_swap_api.domain.model +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.FeeBase import io.novafoundation.nova.feature_account_api.data.model.SubstrateFeeBase import io.novafoundation.nova.feature_account_api.data.model.getAmount -import io.novafoundation.nova.feature_account_api.data.model.replacePlanks import io.novafoundation.nova.feature_account_api.data.model.totalAmount import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.types.Balance import io.novafoundation.nova.feature_wallet_api.presentation.mixin.amountChooser.maxAction.MaxAvailableDeduction -import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.GenericFee import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain +import java.math.BigInteger class SwapFee( val segments: List, @@ -19,7 +19,7 @@ class SwapFee( val intermediateSegmentFeesInAssetIn: FeeBase, private val additionalMaxAmountDeduction: Balance, -) : GenericFee, MaxAvailableDeduction { +) : Fee, MaxAvailableDeduction { data class SwapSegment(val fee: AtomicSwapOperationFee, val operation: AtomicSwapOperation) @@ -30,10 +30,9 @@ class SwapFee( private val assetIn = intermediateSegmentFeesInAssetIn.asset - val additionalAmountForSwap = additionalAmountForSwap() + val deductionForAssetIn: Balance = deductionFor(assetIn) - // TODO better multi fee display with `segmentsFees` - override val networkFee: Fee = determineNetworkFee() + val additionalAmountForSwap = additionalAmountForSwap() override fun deductionFor(amountAsset: Chain.Asset): Balance { return totalFeeAmount(amountAsset) + additionalMaxAmountDeduction @@ -55,9 +54,8 @@ class SwapFee( return SubstrateFeeBase(totalFutureFeeInAssetIn, assetIn) } - // TODO this is for simpler understanding of real fee until multi-chain view is developed - private fun determineNetworkFee(): Fee { - val submissionFeeAsset = submissionFee.asset - return submissionFee.replacePlanks(newPlanks = totalFeeAmount(submissionFeeAsset)) - } + // TODO this is until multi-chain fees are ready + override val submissionOrigin: SubmissionOrigin = submissionFee.submissionOrigin + override val amount: BigInteger = totalFeeAmount(submissionFee.asset) + override val asset: Chain.Asset = submissionFee.asset } diff --git a/feature-swap-api/src/main/java/io/novafoundation/nova/feature_swap_api/domain/model/SwapQuote.kt b/feature-swap-api/src/main/java/io/novafoundation/nova/feature_swap_api/domain/model/SwapQuote.kt index 2eb624e7a3..8da0885905 100644 --- a/feature-swap-api/src/main/java/io/novafoundation/nova/feature_swap_api/domain/model/SwapQuote.kt +++ b/feature-swap-api/src/main/java/io/novafoundation/nova/feature_swap_api/domain/model/SwapQuote.kt @@ -1,7 +1,6 @@ package io.novafoundation.nova.feature_swap_api.domain.model import io.novafoundation.nova.common.utils.Percent -import io.novafoundation.nova.feature_account_api.data.model.amountByExecutingAccount import io.novafoundation.nova.feature_swap_core_api.data.paths.model.QuotedPath import io.novafoundation.nova.feature_swap_core_api.data.primitive.model.SwapDirection import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.types.Balance @@ -55,8 +54,3 @@ infix fun ChainAssetWithAmount.rateAgainst(assetOut: ChainAssetWithAmount): BigD return amountOut / amountIn } - - - -val SwapFee.totalDeductedPlanks: Balance - get() = networkFee.amountByExecutingAccount diff --git a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/crossChain/CrossChainTransferAssetExchange.kt b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/crossChain/CrossChainTransferAssetExchange.kt index 15d0ad3ce0..5df72e08b8 100644 --- a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/crossChain/CrossChainTransferAssetExchange.kt +++ b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/crossChain/CrossChainTransferAssetExchange.kt @@ -176,13 +176,13 @@ class CrossChainTransferAssetExchange( } return AtomicSwapOperationFee( - submissionFee = SubmissionFeeWithLabel(crossChainFee.fromOriginInFeeCurrency), + submissionFee = SubmissionFeeWithLabel(crossChainFee.submissionFee), postSubmissionFees = AtomicSwapOperationFee.PostSubmissionFees( paidByAccount = listOfNotNull( - SubmissionFeeWithLabel(crossChainFee.fromOriginInNativeCurrency, debugLabel = "Delivery"), + SubmissionFeeWithLabel(crossChainFee.deliveryFee, debugLabel = "Delivery"), ), paidFromAmount = listOf( - FeeWithLabel(crossChainFee.fromHoldingRegister, debugLabel = "Execution") + FeeWithLabel(crossChainFee.executionFee, debugLabel = "Execution") ) ), ) diff --git a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/domain/interactor/SwapInteractor.kt b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/domain/interactor/SwapInteractor.kt index 2b1a0542fe..1864a93990 100644 --- a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/domain/interactor/SwapInteractor.kt +++ b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/domain/interactor/SwapInteractor.kt @@ -92,14 +92,14 @@ class SwapInteractor( // swapTransactionHistoryRepository.insertPendingSwap( // chainAsset = swapExecuteArgs.assetIn, // swapArgs = swapExecuteArgs, -// fee = decimalFee.genericFee, +// fee = decimalFee, // txSubmission = submission // ) // // swapTransactionHistoryRepository.insertPendingSwap( // chainAsset = swapExecuteArgs.assetOut, // swapArgs = swapExecuteArgs, -// fee = decimalFee.genericFee, +// fee = decimalFee, // txSubmission = submission // ) // } diff --git a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/domain/validation/SufficientBalanceConsideringNonSufficientAssetsValidation.kt b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/domain/validation/SufficientBalanceConsideringNonSufficientAssetsValidation.kt index 23a7b04c96..8dea37b3da 100644 --- a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/domain/validation/SufficientBalanceConsideringNonSufficientAssetsValidation.kt +++ b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/domain/validation/SufficientBalanceConsideringNonSufficientAssetsValidation.kt @@ -22,7 +22,7 @@ class SufficientBalanceConsideringNonSufficientAssetsValidation( if (!isSelfSufficientAssetOut && assetIn.token.configuration.isCommissionAsset) { val existentialDeposit = assetSourceRegistry.existentialDepositInPlanks(value.detailedAssetIn.chain, assetIn.token.configuration) - val fee = value.decimalFee.networkFee.amountByExecutingAccount + val fee = value.fee.amountByExecutingAccount return validOrError(assetIn.balanceCountedTowardsEDInPlanks - existentialDeposit >= amount + fee) { SwapValidationFailure.InsufficientBalance.BalanceNotConsiderInsufficientReceiveAsset( diff --git a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/domain/validation/SwapPayloadValidation.kt b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/domain/validation/SwapPayloadValidation.kt index 8cd42de8f7..dfb762c908 100644 --- a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/domain/validation/SwapPayloadValidation.kt +++ b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/domain/validation/SwapPayloadValidation.kt @@ -5,10 +5,8 @@ import io.novafoundation.nova.feature_swap_api.domain.model.SwapFee import io.novafoundation.nova.feature_swap_api.domain.model.SwapFeeArgs import io.novafoundation.nova.feature_swap_api.domain.model.SwapQuote import io.novafoundation.nova.feature_swap_api.domain.model.SwapQuoteArgs -import io.novafoundation.nova.feature_swap_api.domain.model.totalDeductedPlanks import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.types.Balance import io.novafoundation.nova.feature_wallet_api.domain.model.Asset -import io.novafoundation.nova.feature_wallet_api.presentation.model.GenericDecimalFee import io.novafoundation.nova.runtime.ext.fullId import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain import java.math.BigInteger @@ -18,7 +16,7 @@ data class SwapValidationPayload( val detailedAssetOut: SwapAssetData, val slippage: Fraction, val feeAsset: Asset, - val decimalFee: GenericDecimalFee, + val fee: SwapFee, val swapQuote: SwapQuote, val swapQuoteArgs: SwapQuoteArgs, val swapExecuteArgs: SwapFeeArgs @@ -42,16 +40,7 @@ val SwapValidationPayload.swapAmountInFeeToken: Balance } val SwapValidationPayload.totalDeductedAmountInFeeToken: Balance - get() = if (isFeePayingByAssetIn) { - decimalFee.genericFee.totalDeductedPlanks - } else { - BigInteger.ZERO - } + get() = fee.deductionFor(fee.asset) val SwapValidationPayload.maxAmountToSwap: Balance - get() = if (isFeePayingByAssetIn) { - val maxAmount = detailedAssetIn.asset.transferableInPlanks - totalDeductedAmountInFeeToken - maxAmount.coerceAtLeast(BigInteger.ZERO) - } else { - detailedAssetIn.asset.transferableInPlanks - } + get() = detailedAssetIn.asset.transferableInPlanks - fee.deductionForAssetIn diff --git a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/domain/validation/SwapValidations.kt b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/domain/validation/SwapValidations.kt index 5f4e7b206b..fdb9ba4d70 100644 --- a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/domain/validation/SwapValidations.kt +++ b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/domain/validation/SwapValidations.kt @@ -52,7 +52,7 @@ fun SwapValidationSystemBuilder.sufficientBalanceConsideringConsumersValidation( SwapValidationFailure.InsufficientBalance.BalanceNotConsiderConsumers( nativeAsset = payload.detailedAssetIn.asset.token.configuration, feeAsset = payload.feeAsset.token.configuration, - swapFee = payload.decimalFee.genericFee, + swapFee = payload.fee, existentialDeposit = existentialDeposit ) } @@ -69,7 +69,7 @@ fun SwapValidationSystemBuilder.enoughLiquidity(sharedQuoteValidationRetriever: fun SwapValidationSystemBuilder.sufficientBalanceInFeeAsset() = sufficientBalanceGeneric( available = { it.feeAsset.transferable }, amount = { BigDecimal.ZERO }, - fee = { it.decimalFee }, + fee = { it.fee }, error = { SwapValidationFailure.NotEnoughFunds.ToPayFee } ) @@ -88,7 +88,7 @@ fun SwapValidationSystemBuilder.checkForFeeChanges( swapService: SwapService ) = checkForFeeChanges( calculateFee = { swapService.estimateFee(it.swapExecuteArgs) }, - currentFee = { it.decimalFee }, + currentFee = { it.fee }, chainAsset = { it.feeAsset.token.configuration }, error = SwapValidationFailure::FeeChangeDetected ) diff --git a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/domain/validation/validations/EnoughNativeAssetBalanceToPayFeeConsideringEDValidation.kt b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/domain/validation/validations/EnoughNativeAssetBalanceToPayFeeConsideringEDValidation.kt index f287e5888f..1fbb1c4de1 100644 --- a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/domain/validation/validations/EnoughNativeAssetBalanceToPayFeeConsideringEDValidation.kt +++ b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/domain/validation/validations/EnoughNativeAssetBalanceToPayFeeConsideringEDValidation.kt @@ -28,7 +28,7 @@ class EnoughNativeAssetBalanceToPayFeeConsideringEDValidation( val chain = chainRegistry.getChain(feeChainAsset.chainId) val existentialDeposit = assetSourceRegistry.existentialDepositInPlanks(chain, feeChainAsset) val availableBalance = value.feeAsset.balanceCountedTowardsEDInPlanks - val fee = value.decimalFee.networkFee.amountByExecutingAccount + val fee = value.fee.amountByExecutingAccount return validOrError(availableBalance - fee >= existentialDeposit) { val minRequiredBalance = existentialDeposit + fee NotEnoughFunds.ToPayFeeAndStayAboveED( diff --git a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/domain/validation/validations/SwapFeeSufficientBalanceValidation.kt b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/domain/validation/validations/SwapFeeSufficientBalanceValidation.kt index e388f4f6e5..73afd98bfd 100644 --- a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/domain/validation/validations/SwapFeeSufficientBalanceValidation.kt +++ b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/domain/validation/validations/SwapFeeSufficientBalanceValidation.kt @@ -22,7 +22,7 @@ class SwapFeeSufficientBalanceValidation : SwapValidation { val feeAsset = value.feeAsset.token.configuration val maxAmountToSwap = value.maxAmountToSwap - return InsufficientBalance.CannotPayFee(chainAssetIn, feeAsset, maxAmountToSwap, value.decimalFee.networkFee).validationError() + return InsufficientBalance.CannotPayFee(chainAssetIn, feeAsset, maxAmountToSwap, value.fee).validationError() } return valid() diff --git a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/confirmation/SwapConfirmationViewModel.kt b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/confirmation/SwapConfirmationViewModel.kt index 26a811434a..7057f50157 100644 --- a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/confirmation/SwapConfirmationViewModel.kt +++ b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/confirmation/SwapConfirmationViewModel.kt @@ -52,7 +52,7 @@ import io.novafoundation.nova.feature_wallet_api.presentation.mixin.amountChoose import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.FeeLoaderMixin import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.FeeStatus import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.GenericFeeLoaderMixin.Configuration -import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.awaitDecimalFee +import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.awaitFee import io.novafoundation.nova.feature_wallet_api.presentation.model.AmountModel import io.novafoundation.nova.feature_wallet_api.presentation.model.mapAmountToAmountModel import io.novafoundation.nova.feature_wallet_api.presentation.model.toAssetPayload @@ -246,7 +246,7 @@ class SwapConfirmationViewModel( } private fun executeSwap() = launch { - val fee = feeMixin.awaitDecimalFee().genericFee + val fee = feeMixin.awaitFee() val quote = confirmationStateFlow.first()?.swapQuote ?: return@launch _submissionInProgress.value = true @@ -372,7 +372,7 @@ class SwapConfirmationViewModel( firstSegmentFees = initialSwapState.first().fee.intermediateSegmentFeesInAssetIn.asset ) - feeMixin.loadFeeV2Generic( + feeMixin.loadFee( coroutineScope = viewModelScope, feeConstructor = { swapInteractor.estimateFee(executeArgs) }, onRetryCancelled = { } diff --git a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/main/SwapMainSettingsViewModel.kt b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/main/SwapMainSettingsViewModel.kt index 678fce400d..8dc0f87396 100644 --- a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/main/SwapMainSettingsViewModel.kt +++ b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/main/SwapMainSettingsViewModel.kt @@ -32,6 +32,7 @@ import io.novafoundation.nova.common.validation.FieldValidator import io.novafoundation.nova.common.validation.ValidationExecutor import io.novafoundation.nova.common.view.bottomSheet.description.DescriptionBottomSheetLauncher import io.novafoundation.nova.common.view.bottomSheet.description.launchNetworkFeeDescription +import io.novafoundation.nova.feature_account_api.data.model.decimalAmount import io.novafoundation.nova.feature_account_api.domain.interfaces.SelectedAccountUseCase import io.novafoundation.nova.feature_account_api.domain.model.addressIn import io.novafoundation.nova.feature_account_api.presenatation.fee.select.FeeAssetSelectorBottomSheet @@ -79,7 +80,7 @@ import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.FeeLoade import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.FeeStatus import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.GenericFeeLoaderMixin import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.GenericFeeLoaderMixin.Configuration -import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.awaitDecimalFee +import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.awaitFee import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.loadedFeeModelOrNullFlow import io.novafoundation.nova.feature_wallet_api.presentation.model.AssetPayload import io.novafoundation.nova.feature_wallet_api.presentation.model.fullChainAssetId @@ -337,7 +338,7 @@ class SwapMainSettingsViewModel( val swapState = SwapState( quote = quotingState.value, - fee = feeMixin.awaitDecimalFee().genericFee, + fee = feeMixin.awaitFee(), slippage = swapSettings.first().slippage ) swapStateStoreProvider.getStore(viewModelScope).setState(swapState) @@ -499,8 +500,8 @@ class SwapMainSettingsViewModel( if (nativeAsset.transferable.isZero) return@filter true if (feeModel == null) return@filter false - val isFeePaidInNativeAsset = nativeAsset.token.configuration.fullId == feeModel.chainAsset.fullId - val notEnoughNativeToPayFee = nativeAsset.transferable < feeModel.decimalFee.networkFeeDecimalAmount + val isFeePaidInNativeAsset = nativeAsset.token.configuration.fullId == feeModel.fee.asset.fullId + val notEnoughNativeToPayFee = nativeAsset.transferable < feeModel.fee.decimalAmount isFeePaidInNativeAsset && notEnoughNativeToPayFee }.onEach { diff --git a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/main/SwapValidationFailureUi.kt b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/main/SwapValidationFailureUi.kt index 4ed1423743..7eeb887857 100644 --- a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/main/SwapValidationFailureUi.kt +++ b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/main/SwapValidationFailureUi.kt @@ -86,7 +86,7 @@ fun CoroutineScope.mapSwapValidationFailureToUI( resourceManager.getString( R.string.swap_failure_balance_not_consider_consumers, reason.existentialDeposit.formatPlanks(reason.nativeAsset), - reason.swapFee.networkFee.amountByExecutingAccount.formatPlanks(reason.feeAsset) + reason.swapFee.amountByExecutingAccount.formatPlanks(reason.feeAsset) ) ).asDefault() @@ -103,7 +103,7 @@ fun CoroutineScope.mapSwapValidationFailureToUI( error = reason, resourceManager = resourceManager, actions = actions, - setFee = { setNewFee(it.newFee.genericFee) }, + setFee = { setNewFee(it.newFee) }, ) is TooSmallRemainingBalance -> handleTooSmallRemainingBalance( diff --git a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/mixin/maxAction/MaxActionProviderFactory.kt b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/mixin/maxAction/MaxActionProviderFactory.kt index 9f4106c393..92a4cedde9 100644 --- a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/mixin/maxAction/MaxActionProviderFactory.kt +++ b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/mixin/maxAction/MaxActionProviderFactory.kt @@ -1,5 +1,6 @@ package io.novafoundation.nova.feature_swap_impl.presentation.mixin.maxAction +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.types.Balance import io.novafoundation.nova.feature_wallet_api.domain.model.Asset @@ -7,7 +8,6 @@ import io.novafoundation.nova.feature_wallet_api.presentation.mixin.amountChoose import io.novafoundation.nova.feature_wallet_api.presentation.mixin.amountChooser.maxAction.MaxActionProviderDsl.deductFee import io.novafoundation.nova.feature_wallet_api.presentation.mixin.amountChooser.maxAction.MaxActionProviderDsl.providingMaxOf import io.novafoundation.nova.feature_wallet_api.presentation.mixin.amountChooser.maxAction.MaxAvailableDeduction -import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.GenericFee import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.GenericFeeLoaderMixin import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry import kotlinx.coroutines.flow.Flow @@ -23,7 +23,7 @@ class MaxActionProviderFactory( field: (Asset) -> Balance, feeLoaderMixin: GenericFeeLoaderMixin, allowMaxAction: Boolean = true - ): MaxActionProvider where F : GenericFee, F : MaxAvailableDeduction { + ): MaxActionProvider where F : Fee, F : MaxAvailableDeduction { return assetInFlow.providingMaxOf(field, allowMaxAction) .deductFee(feeLoaderMixin) .disallowReapingIfHasDependents(assetOutFlow, assetSourceRegistry, chainRegistry) diff --git a/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/data/mappers/Fee.kt b/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/data/mappers/Fee.kt index 2f0db67ad0..f9d2c3f5c8 100644 --- a/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/data/mappers/Fee.kt +++ b/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/data/mappers/Fee.kt @@ -1,35 +1,20 @@ package io.novafoundation.nova.feature_wallet_api.data.mappers -import io.novafoundation.nova.feature_account_api.data.model.Fee +import io.novafoundation.nova.feature_account_api.data.model.FeeBase import io.novafoundation.nova.feature_wallet_api.domain.model.Token -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.feature_wallet_api.presentation.model.GenericDecimalFee -import io.novafoundation.nova.feature_wallet_api.presentation.model.GenericFeeModel +import io.novafoundation.nova.feature_wallet_api.presentation.model.FeeModel import io.novafoundation.nova.feature_wallet_api.presentation.model.mapAmountToAmountModel -fun mapFeeToFeeModel( +fun mapFeeToFeeModel( fee: F, token: Token, includeZeroFiat: Boolean = true -) = GenericFeeModel( - decimalFee = GenericDecimalFee( - genericFee = fee, - networkFeeDecimalAmount = token.amountFromPlanks(fee.networkFee.amount), - ), - chainAsset = token.configuration, +): FeeModel = FeeModel( display = mapAmountToAmountModel( - amountInPlanks = fee.networkFee.amount, + amountInPlanks = fee.amount, token = token, includeZeroFiat = includeZeroFiat - ) + ), + fee = fee ) -@Suppress("DeprecatedCallableAddReplaceWith") -@Deprecated("Backward-compatible adapter") -fun mapFeeToFeeModel( - fee: Fee, - token: Token, - includeZeroFiat: Boolean = true -) = mapFeeToFeeModel(SimpleFee(fee), token, includeZeroFiat) 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 01cf2e579b..20b505c182 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 @@ -3,18 +3,16 @@ package io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets. 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_account_api.data.model.Fee +import io.novafoundation.nova.feature_account_api.data.model.FeeBase import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.types.Balance 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.OriginFee +import io.novafoundation.nova.feature_wallet_api.domain.model.intoFeeList 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.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 @@ -73,16 +71,16 @@ 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: OriginDecimalFee, - val crossChainFee: DecimalFee?, + val originFee: OriginFee, + val crossChainFee: FeeBase?, val originCommissionAsset: Asset, val originUsedAsset: Asset ) @@ -90,10 +88,10 @@ data class AssetTransferPayload( val AssetTransferPayload.commissionChainAsset: Chain.Asset get() = originCommissionAsset.token.configuration -val AssetTransferPayload.originFeeList: List> - get() = originFee.intoDecimalFeeList() +val AssetTransferPayload.originFeeList: List + get() = originFee.intoFeeList() -val AssetTransferPayload.originFeeListInUsedAsset: List> +val AssetTransferPayload.originFeeListInUsedAsset: List get() = if (isSendingCommissionAsset) { originFeeList } 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 61fbef0826..fbc5c73c69 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 @@ -7,7 +7,7 @@ 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.data.network.blockhain.types.Balance -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.Token import io.novafoundation.nova.feature_wallet_api.domain.model.planksFromAmount import io.novafoundation.nova.runtime.ext.accountIdOf @@ -98,10 +98,10 @@ data class WeightedAssetTransfer( override val destinationChainAsset: Chain.Asset, override val commissionAssetToken: Token, override val amount: BigDecimal, - val decimalFee: OriginDecimalFee, + val fee: OriginFee, ) : AssetTransfer { - constructor(assetTransfer: AssetTransfer, fee: OriginDecimalFee) : this( + constructor(assetTransfer: AssetTransfer, fee: OriginFee) : this( sender = assetTransfer.sender, recipient = assetTransfer.recipient, originChain = assetTransfer.originChain, @@ -110,7 +110,7 @@ data class WeightedAssetTransfer( destinationChainAsset = assetTransfer.destinationChainAsset, commissionAssetToken = assetTransfer.commissionAssetToken, amount = assetTransfer.amount, - decimalFee = fee + fee = fee ) } 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 index 6325248173..9505c94f32 100644 --- 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 @@ -5,25 +5,25 @@ import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.types.Ba import java.math.BigInteger data class CrossChainFeeModel( - val paidByOrigin: Balance = BigInteger.ZERO, - val paidFromHoldingRegister: Balance = BigInteger.ZERO + val deliveryFees: Balance = BigInteger.ZERO, + val executionFees: Balance = BigInteger.ZERO ) { companion object } -fun CrossChainFeeModel.paidByOriginOrNull(): Balance? { - return if (paidByOrigin.isZero) { +fun CrossChainFeeModel.deliveryFeesOrNull(): Balance? { + return if (deliveryFees.isZero) { null } else { - paidByOrigin + deliveryFees } } fun CrossChainFeeModel.Companion.zero() = CrossChainFeeModel() operator fun CrossChainFeeModel.plus(other: CrossChainFeeModel) = CrossChainFeeModel( - paidByOrigin = paidByOrigin + other.paidByOrigin, - paidFromHoldingRegister = paidFromHoldingRegister + other.paidFromHoldingRegister + deliveryFees = deliveryFees + other.deliveryFees, + executionFees = executionFees + other.executionFees ) fun CrossChainFeeModel?.orZero() = this ?: CrossChainFeeModel.zero() diff --git a/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/domain/interfaces/CrossChainTransfersUseCase.kt b/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/domain/interfaces/CrossChainTransfersUseCase.kt index 8bc0f6ab1d..7e637f2375 100644 --- a/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/domain/interfaces/CrossChainTransfersUseCase.kt +++ b/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/domain/interfaces/CrossChainTransfersUseCase.kt @@ -34,9 +34,12 @@ interface CrossChainTransfersUseCase { originChain: Chain ): Balance + /** + * @param cachingScope - a scope that will be registered as a dependency for internal caching. If null is passed, no caching will be used + */ suspend fun ExtrinsicService.estimateFee( transfer: AssetTransferBase, - computationalScope: CoroutineScope + cachingScope: CoroutineScope? ): CrossChainTransferFee /** diff --git a/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/domain/model/CrossChainFee.kt b/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/domain/model/CrossChainFee.kt index 04d1a9b604..d68657780a 100644 --- a/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/domain/model/CrossChainFee.kt +++ b/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/domain/model/CrossChainFee.kt @@ -9,19 +9,18 @@ class CrossChainTransferFee( /** * Deducted upon initial transaction submission from the origin chain. Asset can be controlled with [FeePaymentCurrency] */ - val fromOriginInFeeCurrency: SubmissionFee, + val submissionFee: SubmissionFee, /** - * Deducted upon initial transaction submission from the origin chain, e.g. to pay for delivery fees. Cannot be controlled with [FeePaymentCurrency] + * Deducted upon initial transaction submission from the origin chain. Cannot be controlled with [FeePaymentCurrency] * and is always paid in native currency - * */ - val fromOriginInNativeCurrency: SubmissionFee?, + val deliveryFee: SubmissionFee?, /** * Total sum of all execution and delivery fees paid from holding register throughout xcm transfer * Paid (at the moment) in a sending asset. There might be multiple [Chain.Asset] that represent the same logical asset, * the asset here indicates the first one, on the origin chain */ - val fromHoldingRegister: FeeBase, + val executionFee: FeeBase, ) 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 index 71b040a51d..99ed7f5506 100644 --- 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 @@ -3,42 +3,22 @@ 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 submissionFee: Fee, + val deliveryFee: Fee?, val chainAsset: Chain.Asset ) : Fee { - override val amount: BigInteger = networkFee.amount + deliveryPart?.amount.orZero() - - override val submissionOrigin: SubmissionOrigin = networkFee.submissionOrigin + override val amount: BigInteger = submissionFee.amount + deliveryFee?.amount.orZero() - override val asset = networkFee.asset -} - -fun OriginDecimalFee.networkFeePart(): GenericDecimalFee { - return GenericDecimalFee.from(genericFee.networkFee.networkFee, genericFee.networkFee.chainAsset) -} + override val submissionOrigin: SubmissionOrigin = submissionFee.submissionOrigin -fun OriginDecimalFee.deliveryFeePart(): GenericDecimalFee? { - return genericFee.networkFee.deliveryPart?.let { - GenericDecimalFee.from(genericFee.networkFee.deliveryPart, genericFee.networkFee.chainAsset) - } + override val asset = submissionFee.asset } -fun OriginDecimalFee.intoDecimalFeeList(): List> { - return listOfNotNull( - networkFeePart(), - deliveryFeePart() - ) +fun OriginFee.intoFeeList(): List { + return listOfNotNull(submissionFee, deliveryFee) } 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 c175306857..52b9d3efe2 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 @@ -6,11 +6,10 @@ import io.novafoundation.nova.common.validation.DefaultFailureLevel 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.feature_account_api.data.model.Fee +import io.novafoundation.nova.feature_account_api.data.model.decimalAmountByExecutingAccount import io.novafoundation.nova.feature_wallet_api.R import io.novafoundation.nova.feature_wallet_api.presentation.formatters.formatTokenAmount -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.feature_wallet_api.presentation.model.networkFeeByRequestedAccountOrZero import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain import java.math.BigDecimal @@ -20,10 +19,10 @@ interface NotEnoughToPayFeesError { val fee: BigDecimal } -typealias EnoughAmountToTransferValidation = EnoughAmountToTransferValidationGeneric +typealias EnoughAmountToTransferValidation = EnoughAmountToTransferValidationGeneric -class EnoughAmountToTransferValidationGeneric( - private val feeListExtractor: GenericFeeListProducer, +class EnoughAmountToTransferValidationGeneric( + private val feeListExtractor: FeeListProducer, private val availableBalanceProducer: AmountProducer

, private val errorProducer: (ErrorContext

) -> E, private val skippable: Boolean = false, @@ -32,7 +31,7 @@ class EnoughAmountToTransferValidationGeneric( constructor( extraAmountExtractor: AmountProducer

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

, errorProducer: (ErrorContext

) -> E, skippable: Boolean = false, @@ -56,7 +55,7 @@ class EnoughAmountToTransferValidationGeneric( companion object; override suspend fun validate(value: P): ValidationStatus { - val fee = feeListExtractor(value).sumOf { it.networkFeeByRequestedAccountOrZero } + val fee = feeListExtractor(value).sumOf { it.decimalAmountByExecutingAccount } val available = availableBalanceProducer(value) val amount = extraAmountExtractor(value) @@ -72,8 +71,8 @@ class EnoughAmountToTransferValidationGeneric( } } -fun ValidationSystemBuilder.sufficientBalanceMultyFee( - feeExtractor: GenericFeeListProducer = { emptyList() }, +fun ValidationSystemBuilder.sufficientBalanceMultiFee( + feeExtractor: FeeListProducer = { emptyList() }, amount: AmountProducer

= { BigDecimal.ZERO }, available: AmountProducer

, error: (EnoughAmountToTransferValidationGeneric.ErrorContext

) -> E, @@ -89,7 +88,7 @@ fun ValidationSystemBuilder.sufficientBalanceMultyF ) fun ValidationSystemBuilder.sufficientBalance( - fee: FeeProducer

= { null }, + fee: SimpleFeeProducer

= { null }, amount: AmountProducer

= { BigDecimal.ZERO }, available: AmountProducer

, error: (EnoughAmountToTransferValidationGeneric.ErrorContext

) -> E, @@ -104,14 +103,14 @@ fun ValidationSystemBuilder.sufficientBalance( ) ) -fun ValidationSystemBuilder.sufficientBalanceGeneric( - fee: GenericFeeProducer = { null }, +fun ValidationSystemBuilder.sufficientBalanceGeneric( + fee: OptionalFeeProducer = { null }, amount: AmountProducer

= { BigDecimal.ZERO }, available: AmountProducer

, 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 7310422501..8231aa6c73 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 @@ -1,14 +1,15 @@ package io.novafoundation.nova.feature_wallet_api.domain.validation +import io.novafoundation.nova.common.utils.orZero 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.validOrError +import io.novafoundation.nova.feature_account_api.data.model.Fee +import io.novafoundation.nova.feature_account_api.data.model.decimalAmountByExecutingAccount 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.domain.validation.InsufficientBalanceToStayAboveEDError.ErrorModel -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 import java.math.BigDecimal @@ -25,9 +26,9 @@ interface InsufficientBalanceToStayAboveEDError { ) } -class EnoughBalanceToStayAboveEDValidation( +class EnoughBalanceToStayAboveEDValidation( private val assetSourceRegistry: AssetSourceRegistry, - private val fee: GenericFeeProducer, + private val fee: OptionalFeeProducer, private val balance: AmountProducer

, private val chainWithAsset: (P) -> ChainWithAsset, private val error: (P, ErrorModel) -> E @@ -38,7 +39,7 @@ class EnoughBalanceToStayAboveEDValidation( val asset = chainWithAsset(value).asset val existentialDeposit = assetSourceRegistry.existentialDeposit(chain, asset) val balance = balance(value) - val fee = fee(value).networkFeeByRequestedAccountOrZero + val fee = fee(value)?.decimalAmountByExecutingAccount.orZero() return validOrError(balance - fee >= existentialDeposit) { val minRequired = existentialDeposit + fee error( @@ -55,8 +56,8 @@ class EnoughBalanceToStayAboveEDValidation( class EnoughTotalToStayAboveEDValidationFactory(private val assetSourceRegistry: AssetSourceRegistry) { - fun create( - fee: GenericFeeProducer, + fun create( + fee: OptionalFeeProducer, balance: AmountProducer

, chainWithAsset: (P) -> ChainWithAsset, error: (P, ErrorModel) -> E @@ -72,8 +73,8 @@ class EnoughTotalToStayAboveEDValidationFactory(private val assetSourceRegistry: } context(ValidationSystemBuilder) -fun EnoughTotalToStayAboveEDValidationFactory.validate( - fee: GenericFeeProducer, +fun EnoughTotalToStayAboveEDValidationFactory.validate( + fee: OptionalFeeProducer, balance: AmountProducer

, chainWithAsset: (P) -> ChainWithAsset, error: (P, ErrorModel) -> 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 77dd5d6950..9cbf2b49e9 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,15 +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 io.novafoundation.nova.feature_account_api.data.model.Fee +import io.novafoundation.nova.feature_account_api.data.model.decimalAmountByExecutingAccount import java.math.BigDecimal typealias ExistentialDepositError = (remainingAmount: BigDecimal, payload: P) -> E -class ExistentialDepositValidation( +class ExistentialDepositValidation( private val countableTowardsEdBalance: AmountProducer

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

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

@@ -22,7 +22,7 @@ class ExistentialDepositValidation( val existentialDeposit = existentialDeposit(value) val countableTowardsEd = countableTowardsEdBalance(value) - val fee = feeProducer(value).sumOf { it.networkFeeByRequestedAccountOrZero } + val fee = feeProducer(value).sumOf { it.decimalAmountByExecutingAccount } val extraAmount = extraAmountProducer(value) val remainingAmount = countableTowardsEd - fee - extraAmount @@ -33,9 +33,9 @@ class ExistentialDepositValidation( } } -fun ValidationSystemBuilder.doNotCrossExistentialDepositMultyFee( +fun ValidationSystemBuilder.doNotCrossExistentialDepositMultiFee( countableTowardsEdBalance: AmountProducer

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

= { BigDecimal.ZERO }, existentialDeposit: AmountProducer

, error: ExistentialDepositError, @@ -48,19 +48,3 @@ fun ValidationSystemBuilder.doNotCrossExistentialDe 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/FeeChangeValidation.kt b/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/domain/validation/FeeChangeValidation.kt index ddf3fb4154..e58cf21a65 100644 --- a/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/domain/validation/FeeChangeValidation.kt +++ b/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/domain/validation/FeeChangeValidation.kt @@ -4,6 +4,7 @@ import io.novafoundation.nova.common.mixin.api.CustomDialogDisplayer.Payload import io.novafoundation.nova.common.mixin.api.CustomDialogDisplayer.Payload.DialogAction import io.novafoundation.nova.common.resources.ResourceManager import io.novafoundation.nova.common.utils.hasTheSaveValueAs +import io.novafoundation.nova.common.utils.orZero import io.novafoundation.nova.common.validation.TransformedFailure import io.novafoundation.nova.common.validation.Validation import io.novafoundation.nova.common.validation.ValidationFlowActions @@ -11,15 +12,11 @@ import io.novafoundation.nova.common.validation.ValidationStatus import io.novafoundation.nova.common.validation.ValidationSystemBuilder import io.novafoundation.nova.common.validation.isTrueOrError import io.novafoundation.nova.feature_account_api.data.model.Fee +import io.novafoundation.nova.feature_account_api.data.model.SubmissionFee +import io.novafoundation.nova.feature_account_api.data.model.decimalAmountByExecutingAccount import io.novafoundation.nova.feature_wallet_api.R -import io.novafoundation.nova.feature_wallet_api.domain.model.amountFromPlanks import io.novafoundation.nova.feature_wallet_api.presentation.formatters.formatTokenAmount -import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.GenericFee import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.GenericFeeLoaderMixin -import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.SimpleFee -import io.novafoundation.nova.feature_wallet_api.presentation.model.GenericDecimalFee -import io.novafoundation.nova.feature_wallet_api.presentation.model.networkFeeByRequestedAccount -import io.novafoundation.nova.feature_wallet_api.presentation.model.networkFeeByRequestedAccountOrZero import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch @@ -27,9 +24,9 @@ import java.math.BigDecimal private val FEE_RATIO_THRESHOLD = 1.5.toBigDecimal() -class FeeChangeValidation( - private val calculateFee: suspend (P) -> GenericDecimalFee, - private val currentFee: GenericFeeProducer, +class FeeChangeValidation( + private val calculateFee: FeeProducer, + private val currentFee: OptionalFeeProducer, private val chainAsset: (P) -> Chain.Asset, private val error: (FeeChangeDetectedFailure.Payload) -> E ) : Validation { @@ -38,8 +35,8 @@ class FeeChangeValidation( val oldFee = currentFee(value) val newFee = calculateFee(value) - val oldAmount = oldFee.networkFeeByRequestedAccountOrZero - val newAmount = newFee.networkFeeByRequestedAccount + val oldAmount = oldFee?.decimalAmountByExecutingAccount.orZero() + val newAmount = newFee.decimalAmountByExecutingAccount val areFeesSame = oldAmount hasTheSaveValueAs newAmount @@ -56,54 +53,33 @@ class FeeChangeValidation( } } -interface FeeChangeDetectedFailure { +interface FeeChangeDetectedFailure { - class Payload( + class Payload( val needsUserAttention: Boolean, val oldFee: BigDecimal, - val newFee: GenericDecimalFee, + val newFee: F, val chainAsset: Chain.Asset, ) - val payload: Payload + val payload: Payload } -fun ValidationSystemBuilder.checkForSimpleFeeChanges( - calculateFee: suspend (P) -> Fee, - currentFee: FeeProducer

, - chainAsset: (P) -> Chain.Asset, - error: (FeeChangeDetectedFailure.Payload) -> E -) { - checkForFeeChanges( - calculateFee = { payload -> SimpleFee(calculateFee(payload)) }, - currentFee = currentFee, - chainAsset = chainAsset, - error = error - ) -} - -fun ValidationSystemBuilder.checkForFeeChanges( +fun ValidationSystemBuilder.checkForFeeChanges( calculateFee: suspend (P) -> F, - currentFee: GenericFeeProducer, + currentFee: OptionalFeeProducer, chainAsset: (P) -> Chain.Asset, error: (FeeChangeDetectedFailure.Payload) -> E ) = validate( FeeChangeValidation( - calculateFee = { payload -> - val newFee = calculateFee(payload) - - GenericDecimalFee( - genericFee = calculateFee(payload), - networkFeeDecimalAmount = chainAsset(payload).amountFromPlanks(newFee.networkFee.amount) - ) - }, + calculateFee = calculateFee, currentFee = currentFee, error = error, chainAsset = chainAsset ) ) -fun CoroutineScope.handleFeeSpikeDetected( +fun CoroutineScope.handleFeeSpikeDetected( error: FeeChangeDetectedFailure, resourceManager: ResourceManager, feeLoaderMixin: GenericFeeLoaderMixin.Presentation, @@ -112,14 +88,14 @@ fun CoroutineScope.handleFeeSpikeDetected( error = error, resourceManager = resourceManager, actions = actions, - setFee = { feeLoaderMixin.setFee(it.newFee.genericFee) } + setFee = { feeLoaderMixin.setFee(it.newFee) } ) -fun CoroutineScope.handleFeeSpikeDetected( - error: FeeChangeDetectedFailure, +fun CoroutineScope.handleFeeSpikeDetected( + error: FeeChangeDetectedFailure, resourceManager: ResourceManager, actions: ValidationFlowActions<*>, - setFee: suspend (error: FeeChangeDetectedFailure.Payload) -> Unit, + setFee: suspend (error: FeeChangeDetectedFailure.Payload) -> Unit, ): TransformedFailure? { if (!error.payload.needsUserAttention) { actions.resumeFlow() @@ -128,7 +104,7 @@ fun CoroutineScope.handleFeeSpikeDetected( val chainAsset = error.payload.chainAsset val oldFee = error.payload.oldFee.formatTokenAmount(chainAsset) - val newFee = error.payload.newFee.networkFeeByRequestedAccount.formatTokenAmount(chainAsset) + val newFee = error.payload.newFee.decimalAmountByExecutingAccount.formatTokenAmount(chainAsset) return TransformedFailure.Custom( Payload( diff --git a/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/domain/validation/HasEnoughFreeBalanceValidation.kt b/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/domain/validation/HasEnoughFreeBalanceValidation.kt index 9f27ed4184..14f985efe9 100644 --- a/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/domain/validation/HasEnoughFreeBalanceValidation.kt +++ b/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/domain/validation/HasEnoughFreeBalanceValidation.kt @@ -4,14 +4,15 @@ import androidx.annotation.StringRes import io.novafoundation.nova.common.base.TitleAndMessage import io.novafoundation.nova.common.resources.ResourceManager import io.novafoundation.nova.common.utils.atLeastZero +import io.novafoundation.nova.common.utils.orZero 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.isTrueOrError +import io.novafoundation.nova.feature_account_api.data.model.decimalAmountByExecutingAccount import io.novafoundation.nova.feature_wallet_api.R import io.novafoundation.nova.feature_wallet_api.domain.model.Asset import io.novafoundation.nova.feature_wallet_api.presentation.formatters.formatTokenAmount -import io.novafoundation.nova.feature_wallet_api.presentation.model.networkFeeByRequestedAccountOrZero import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain import java.math.BigDecimal @@ -24,13 +25,13 @@ interface NotEnoughFreeBalanceError { class HasEnoughFreeBalanceValidation( private val asset: (P) -> Asset, - private val fee: FeeProducer

, + private val fee: SimpleFeeProducer

, private val requestedAmount: AmountProducer

, private val error: HasEnoughFreeBalanceErrorProducer ) : Validation { override suspend fun validate(value: P): ValidationStatus { - val freeBalanceAfterFees = asset(value).free - fee(value).networkFeeByRequestedAccountOrZero + val freeBalanceAfterFees = asset(value).free - fee(value)?.decimalAmountByExecutingAccount.orZero() return (freeBalanceAfterFees >= requestedAmount(value)) isTrueOrError { error(asset(value).token.configuration, freeBalanceAfterFees.atLeastZero()) @@ -40,7 +41,7 @@ class HasEnoughFreeBalanceValidation( fun ValidationSystemBuilder.hasEnoughFreeBalance( asset: (P) -> Asset, - fee: FeeProducer

, + fee: SimpleFeeProducer

, requestedAmount: AmountProducer

, error: HasEnoughFreeBalanceErrorProducer ) { 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 a6573fe692..093b887f0e 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 @@ -1,7 +1,6 @@ package io.novafoundation.nova.feature_wallet_api.domain.validation -import io.novafoundation.nova.feature_wallet_api.presentation.model.DecimalFee -import io.novafoundation.nova.feature_wallet_api.presentation.model.GenericDecimalFee +import io.novafoundation.nova.feature_account_api.data.model.Fee import java.math.BigDecimal import java.math.BigInteger @@ -9,8 +8,10 @@ typealias AmountProducer

= suspend (P) -> BigDecimal typealias PlanksProducer

= suspend (P) -> BigInteger -typealias FeeProducer

= suspend (P) -> DecimalFee? +typealias SimpleFeeProducer

= OptionalFeeProducer -typealias GenericFeeProducer = suspend (P) -> GenericDecimalFee? +typealias OptionalFeeProducer = suspend (P) -> F? -typealias GenericFeeListProducer = suspend (P) -> List> +typealias FeeProducer = suspend (P) -> F + +typealias FeeListProducer = suspend (P) -> List diff --git a/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/presentation/mixin/amountChooser/maxAction/FeeAwareMaxActionProvider.kt b/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/presentation/mixin/amountChooser/maxAction/FeeAwareMaxActionProvider.kt index d563e4af58..f9349324f0 100644 --- a/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/presentation/mixin/amountChooser/maxAction/FeeAwareMaxActionProvider.kt +++ b/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/presentation/mixin/amountChooser/maxAction/FeeAwareMaxActionProvider.kt @@ -1,62 +1,23 @@ package io.novafoundation.nova.feature_wallet_api.presentation.mixin.amountChooser.maxAction import androidx.lifecycle.asFlow +import io.novafoundation.nova.feature_account_api.data.model.Fee import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.types.Balance import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.FeeStatus -import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.GenericFee import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.GenericFeeLoaderMixin -import io.novafoundation.nova.runtime.ext.fullId import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.combine -class FeeAwareMaxActionProvider( - feeInputMixin: GenericFeeLoaderMixin, - private val extractTotalFee: (F) -> Balance, - inner: MaxActionProvider, -) : MaxActionProvider { - - // Fee is not deducted for display - override val maxAvailableForDisplay: Flow = inner.maxAvailableForDisplay - - override val maxAvailableForAction: Flow = combine( - inner.maxAvailableForAction, - feeInputMixin.feeLiveData.asFlow() - ) { maxAvailable, newFeeStatus -> - if (maxAvailable == null) return@combine null - - when (newFeeStatus) { - // do not block in case there is no fee or fee is not yet present - FeeStatus.Error, FeeStatus.NoFee -> maxAvailable - - is FeeStatus.Loaded -> { - val feeAsset = newFeeStatus.feeModel.chainAsset - val amountAsset = maxAvailable.chainAsset - - if (feeAsset.fullId == amountAsset.fullId) { - val genericFee = newFeeStatus.feeModel.decimalFee.genericFee - val extractedFee = extractTotalFee(genericFee) - - maxAvailable - extractedFee - } else { - maxAvailable - } - } - - FeeStatus.Loading -> null - } - } -} - interface MaxAvailableDeduction { fun deductionFor(amountAsset: Chain.Asset): Balance } -class MultiFeeAwareMaxActionProvider( +class ComplexFeeAwareMaxActionProvider( feeInputMixin: GenericFeeLoaderMixin, inner: MaxActionProvider, -) : MaxActionProvider where F : GenericFee, F : MaxAvailableDeduction { +) : MaxActionProvider where F : Fee, F : MaxAvailableDeduction { // Fee is not deducted for display override val maxAvailableForDisplay: Flow = inner.maxAvailableForDisplay @@ -73,7 +34,7 @@ class MultiFeeAwareMaxActionProvider( is FeeStatus.Loaded -> { val amountAsset = maxAvailable.chainAsset - val deduction = newFeeStatus.feeModel.decimalFee.genericFee.deductionFor(amountAsset) + val deduction = newFeeStatus.feeModel.fee.deductionFor(amountAsset) maxAvailable - deduction } diff --git a/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/presentation/mixin/amountChooser/maxAction/MaxActionProvider.kt b/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/presentation/mixin/amountChooser/maxAction/MaxActionProvider.kt index d1bf11488b..948adae7e2 100644 --- a/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/presentation/mixin/amountChooser/maxAction/MaxActionProvider.kt +++ b/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/presentation/mixin/amountChooser/maxAction/MaxActionProvider.kt @@ -1,9 +1,9 @@ package io.novafoundation.nova.feature_wallet_api.presentation.mixin.amountChooser.maxAction import io.novafoundation.nova.common.utils.atLeastZero +import io.novafoundation.nova.feature_account_api.data.model.Fee import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.types.Balance import io.novafoundation.nova.feature_wallet_api.domain.model.Asset -import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.GenericFee import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.GenericFeeLoaderMixin import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain import kotlinx.coroutines.flow.Flow @@ -24,17 +24,10 @@ object MaxActionProviderDsl { return AssetMaxActionProvider(this, field, allowMaxAction) } - fun MaxActionProvider.deductFee( - feeLoaderMixin: GenericFeeLoaderMixin, - extractTotalFee: (F) -> Balance - ): MaxActionProvider { - return FeeAwareMaxActionProvider(feeLoaderMixin, extractTotalFee, inner = this) - } - fun MaxActionProvider.deductFee( feeLoaderMixin: GenericFeeLoaderMixin, - ): MaxActionProvider where F : GenericFee, F : MaxAvailableDeduction { - return MultiFeeAwareMaxActionProvider(feeLoaderMixin, inner = this) + ): MaxActionProvider where F : Fee, F : MaxAvailableDeduction { + return ComplexFeeAwareMaxActionProvider(feeLoaderMixin, inner = this) } } 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 a00b9fdac6..05284b0bdc 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 @@ -6,12 +6,12 @@ import io.novafoundation.nova.common.mixin.api.Retriable import io.novafoundation.nova.common.utils.castOrNull import io.novafoundation.nova.common.utils.inBackground import io.novafoundation.nova.feature_account_api.data.model.Fee +import io.novafoundation.nova.feature_account_api.data.model.FeeBase import io.novafoundation.nova.feature_wallet_api.domain.TokenUseCase 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.presentation.mixin.fee.GenericFeeLoaderMixin.Configuration -import io.novafoundation.nova.feature_wallet_api.presentation.model.GenericDecimalFee -import io.novafoundation.nova.feature_wallet_api.presentation.model.GenericFeeModel +import io.novafoundation.nova.feature_wallet_api.presentation.model.FeeModel import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.Flow @@ -23,10 +23,10 @@ import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.transform -sealed class FeeStatus { +sealed class FeeStatus { object Loading : FeeStatus() - class Loaded(val feeModel: GenericFeeModel) : FeeStatus() + class Loaded(val feeModel: FeeModel) : FeeStatus() object NoFee : FeeStatus() @@ -40,23 +40,15 @@ sealed interface ChangeFeeTokenState { object NotSupported : ChangeFeeTokenState } -interface GenericFee { - val networkFee: Fee -} - -@JvmInline -value class SimpleFee(override val networkFee: Fee) : GenericFee - -class SimpleGenericFee(override val networkFee: T) : GenericFee - -interface GenericFeeLoaderMixin : Retriable { +interface GenericFeeLoaderMixin : Retriable { - class Configuration( + class Configuration( val showZeroFiat: Boolean = true, val initialState: InitialState = InitialState() ) { - class InitialState( + + class InitialState( val supportCustomFee: Boolean = false, val feeStatus: FeeStatus? = null ) @@ -68,7 +60,7 @@ interface GenericFeeLoaderMixin : Retriable { fun setCommissionAsset(chainAsset: Chain.Asset) - interface Presentation : GenericFeeLoaderMixin { + interface Presentation : GenericFeeLoaderMixin { suspend fun loadFeeSuspending( retryScope: CoroutineScope, @@ -76,11 +68,7 @@ interface GenericFeeLoaderMixin : Retriable { onRetryCancelled: () -> Unit, ) - /** - * @param expectedChain - Specify to force `feeConstructor` to wait until Token corresponds to the given `expectedChain` - * Useful when `tokenFlow` that mixin was initialized with can switch chains - */ - fun loadFeeV2Generic( + fun loadFee( coroutineScope: CoroutineScope, feeConstructor: suspend (Token) -> F?, onRetryCancelled: () -> Unit, @@ -100,12 +88,12 @@ interface GenericFeeLoaderMixin : Retriable { interface Factory { @Deprecated("Use createChangeableFeeGeneric instead") - fun createGeneric( + fun createGeneric( tokenFlow: Flow, configuration: Configuration = Configuration() ): Presentation - fun createChangeableFeeGeneric( + fun createChangeable( tokenFlow: Flow, coroutineScope: CoroutineScope, configuration: Configuration = Configuration() @@ -113,83 +101,50 @@ interface GenericFeeLoaderMixin : Retriable { } } -interface FeeLoaderMixin : GenericFeeLoaderMixin { - - interface Presentation : GenericFeeLoaderMixin.Presentation, FeeLoaderMixin { +@Deprecated("Use GenericFeeLoaderMixin instead") +interface FeeLoaderMixin : GenericFeeLoaderMixin { - fun loadFee( - coroutineScope: CoroutineScope, - feeConstructor: suspend (Token) -> Fee?, - onRetryCancelled: () -> Unit, - ) = loadFeeV2Generic( - coroutineScope = coroutineScope, - feeConstructor = { token -> feeConstructor(token)?.let(::SimpleFee) }, - onRetryCancelled = onRetryCancelled - ) - } + interface Presentation : GenericFeeLoaderMixin.Presentation, FeeLoaderMixin interface Factory : GenericFeeLoaderMixin.Factory { - @Deprecated("Use createChangeableFee instead") fun create( tokenFlow: Flow, - configuration: Configuration = Configuration() - ): Presentation - - fun createChangeableFee( - tokenFlow: Flow, - coroutineScope: CoroutineScope, - configuration: Configuration = Configuration() + configuration: Configuration = Configuration() ): Presentation } } -suspend fun GenericFeeLoaderMixin.awaitDecimalFee(): GenericDecimalFee = feeLiveData.asFlow() +suspend fun GenericFeeLoaderMixin.awaitFee(): F = feeLiveData.asFlow() .filterIsInstance>() - .first().feeModel.decimalFee + .first() + .feeModel.fee -suspend fun GenericFeeLoaderMixin.awaitOptionalDecimalFee(): GenericDecimalFee? = feeLiveData.asFlow() +suspend fun GenericFeeLoaderMixin.awaitOptionalFee(): F? = feeLiveData.asFlow() .transform { feeStatus -> when (feeStatus) { - is FeeStatus.Loaded -> emit(feeStatus.feeModel.decimalFee) + is FeeStatus.Loaded -> emit(feeStatus.feeModel.fee) FeeStatus.NoFee -> emit(null) else -> {} // skip } }.first() -fun GenericFeeLoaderMixin.loadedFeeOrNullFlow(): Flow { - return feeLiveData.asFlow().map { - it.castOrNull>()?.feeModel?.decimalFee?.genericFee - } -} - -fun GenericFeeLoaderMixin.loadedDecimalFeeOrNullFlow(): Flow?> { - return feeLiveData.asFlow().map { - it.castOrNull>()?.feeModel?.decimalFee - } -} - -fun GenericFeeLoaderMixin.loadedFeeModelOrNullFlow(): Flow?> { +fun GenericFeeLoaderMixin.loadedFeeModelOrNullFlow(): Flow?> { return feeLiveData .asFlow() .map { it.castOrNull>()?.feeModel } } -fun GenericFeeLoaderMixin.getDecimalFeeOrNull(): GenericDecimalFee? { - return feeLiveData.value - .castOrNull>() - ?.feeModel - ?.decimalFee -} - @Deprecated("Use createGenericChangeableFee instead") -fun FeeLoaderMixin.Factory.createGeneric(assetFlow: Flow) = createGeneric(assetFlow.map { it.token }) +fun GenericFeeLoaderMixin.Factory.createGeneric(assetFlow: Flow): GenericFeeLoaderMixin.Presentation = createGeneric(assetFlow.map { it.token }) + +fun GenericFeeLoaderMixin.Factory.createSimple(assetFlow: Flow) = createGeneric(assetFlow.map { it.token }) -fun FeeLoaderMixin.Factory.createGenericChangeableFee( +fun GenericFeeLoaderMixin.Factory.createChangeable( assetFlow: Flow, coroutineScope: CoroutineScope, - configuration: Configuration = Configuration() -) = createChangeableFeeGeneric(assetFlow.map { it.token }, coroutineScope, configuration) + configuration: Configuration = Configuration() +) : GenericFeeLoaderMixin.Presentation = createChangeable(assetFlow.map { it.token }, coroutineScope, configuration) @Deprecated("Use createChangeableFee instead") fun FeeLoaderMixin.Factory.create(assetFlow: Flow) = create(assetFlow.map { it.token }) diff --git a/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/presentation/mixin/fee/FeeParcelModel.kt b/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/presentation/mixin/fee/FeeParcelModel.kt index e441b7c10c..b328d265ab 100644 --- a/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/presentation/mixin/fee/FeeParcelModel.kt +++ b/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/presentation/mixin/fee/FeeParcelModel.kt @@ -3,18 +3,16 @@ package io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee import android.os.Parcelable import io.novafoundation.nova.feature_account_api.data.extrinsic.SubmissionOrigin import io.novafoundation.nova.feature_account_api.data.model.EvmFee +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_wallet_api.presentation.model.DecimalFee -import io.novafoundation.nova.feature_wallet_api.presentation.model.GenericDecimalFee import io.novafoundation.nova.runtime.util.ChainAssetParcel import io.novasama.substrate_sdk_android.runtime.AccountId import kotlinx.android.parcel.Parcelize -import java.math.BigDecimal import java.math.BigInteger sealed interface FeeParcelModel : Parcelable { - val amount: BigDecimal + val amount: BigInteger val submissionOrigin: SubmissionOriginParcelModel @@ -31,37 +29,35 @@ class SubmissionOriginParcelModel( class EvmFeeParcelModel( val gasLimit: BigInteger, val gasPrice: BigInteger, - override val amount: BigDecimal, + override val amount: BigInteger, override val submissionOrigin: SubmissionOriginParcelModel, override val asset: ChainAssetParcel ) : FeeParcelModel @Parcelize class SimpleFeeParcelModel( - val planks: BigInteger, - override val amount: BigDecimal, + override val amount: BigInteger, override val submissionOrigin: SubmissionOriginParcelModel, override val asset: ChainAssetParcel ) : FeeParcelModel -fun mapFeeToParcel(decimalFee: GenericDecimalFee<*>): FeeParcelModel { - val submissionOrigin = mapSubmissionOriginToParcel(decimalFee.networkFee.submissionOrigin) - val assetParcel = ChainAssetParcel(decimalFee.networkFee.asset) +fun mapFeeToParcel(fee: Fee): FeeParcelModel { + val submissionOrigin = mapSubmissionOriginToParcel(fee.submissionOrigin) + val assetParcel = ChainAssetParcel(fee.asset) - return when (val fee = decimalFee.networkFee) { + return when (fee) { is EvmFee -> EvmFeeParcelModel( gasLimit = fee.gasLimit, gasPrice = fee.gasPrice, - amount = decimalFee.networkFeeDecimalAmount, - submissionOrigin, - assetParcel + amount = fee.amount, + submissionOrigin = submissionOrigin, + asset = assetParcel ) else -> SimpleFeeParcelModel( - decimalFee.networkFee.amount, - decimalFee.networkFeeDecimalAmount, - submissionOrigin, - assetParcel + amount = fee.amount, + submissionOrigin = submissionOrigin, + asset = assetParcel ) } } @@ -70,10 +66,10 @@ private fun mapSubmissionOriginToParcel(submissionOrigin: SubmissionOrigin): Sub return with(submissionOrigin) { SubmissionOriginParcelModel(executingAccount = executingAccount, signingAccount = signingAccount) } } -fun mapFeeFromParcel(parcelFee: FeeParcelModel): DecimalFee { +fun mapFeeFromParcel(parcelFee: FeeParcelModel): Fee { val submissionOrigin = mapSubmissionOriginFromParcel(parcelFee.submissionOrigin) - val fee = when (parcelFee) { + return when (parcelFee) { is EvmFeeParcelModel -> EvmFee( gasLimit = parcelFee.gasLimit, gasPrice = parcelFee.gasPrice, @@ -81,10 +77,8 @@ fun mapFeeFromParcel(parcelFee: FeeParcelModel): DecimalFee { parcelFee.asset.value ) - is SimpleFeeParcelModel -> SubstrateFee(parcelFee.planks, submissionOrigin, parcelFee.asset.value) + is SimpleFeeParcelModel -> SubstrateFee(parcelFee.amount, submissionOrigin, parcelFee.asset.value) } - - return DecimalFee(SimpleFee(fee), parcelFee.amount) } private fun mapSubmissionOriginFromParcel(submissionOrigin: SubmissionOriginParcelModel): SubmissionOrigin { diff --git a/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/presentation/mixin/fee/provider/ChangeableFeeLoaderProvider.kt b/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/presentation/mixin/fee/provider/ChangeableFeeLoaderProvider.kt index 652215a957..af26c5fdfb 100644 --- a/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/presentation/mixin/fee/provider/ChangeableFeeLoaderProvider.kt +++ b/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/presentation/mixin/fee/provider/ChangeableFeeLoaderProvider.kt @@ -7,6 +7,7 @@ import io.novafoundation.nova.common.resources.ResourceManager import io.novafoundation.nova.common.utils.Event import io.novafoundation.nova.common.utils.asLiveData import io.novafoundation.nova.common.utils.singleReplaySharedFlow +import io.novafoundation.nova.feature_account_api.data.model.FeeBase import io.novafoundation.nova.feature_account_api.presenatation.fee.select.FeeAssetSelectorBottomSheet import io.novafoundation.nova.feature_wallet_api.R import io.novafoundation.nova.feature_wallet_api.data.mappers.mapFeeToFeeModel @@ -14,11 +15,8 @@ import io.novafoundation.nova.feature_wallet_api.domain.fee.CustomFeeInteractor 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.presentation.mixin.fee.ChangeFeeTokenState -import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.FeeLoaderMixin import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.FeeStatus -import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.GenericFee import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.GenericFeeLoaderMixin -import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.SimpleFee import io.novafoundation.nova.runtime.ext.commissionAsset import io.novafoundation.nova.runtime.ext.fullId import io.novafoundation.nova.runtime.ext.isCommissionAsset @@ -40,26 +38,7 @@ import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch import kotlinx.coroutines.withContext -internal class ChangeableFeeLoaderProviderPresentation( - customFeeInteractor: CustomFeeInteractor, - chainRegistry: ChainRegistry, - resourceManager: ResourceManager, - configuration: GenericFeeLoaderMixin.Configuration, - actionAwaitableMixinFactory: ActionAwaitableMixin.Factory, - tokenFlow: Flow, - coroutineScope: CoroutineScope -) : ChangeableFeeLoaderProvider( - customFeeInteractor, - chainRegistry, - resourceManager, - configuration, - actionAwaitableMixinFactory, - tokenFlow, - coroutineScope -), - FeeLoaderMixin.Presentation - -internal open class ChangeableFeeLoaderProvider( +internal open class ChangeableFeeLoaderProvider( private val interactor: CustomFeeInteractor, private val chainRegistry: ChainRegistry, private val resourceManager: ResourceManager, @@ -130,7 +109,7 @@ internal open class ChangeableFeeLoaderProvider( value?.run { postFeeState(this) } } - override fun loadFeeV2Generic( + override fun loadFee( coroutineScope: CoroutineScope, feeConstructor: suspend (Token) -> F?, onRetryCancelled: () -> Unit @@ -198,7 +177,7 @@ internal open class ChangeableFeeLoaderProvider( RetryPayload( title = resourceManager.getString(R.string.choose_amount_network_error), message = resourceManager.getString(R.string.choose_amount_error_fee), - onRetry = { loadFeeV2Generic(retryScope, feeConstructor, onRetryCancelled) }, + onRetry = { loadFee(retryScope, feeConstructor, onRetryCancelled) }, onCancel = onRetryCancelled ) ) @@ -242,7 +221,7 @@ internal open class ChangeableFeeLoaderProvider( val selectedAssetIsAvailableToPayFee = canPayFeeInCustomAssetFlow.first() if (commissionAsset.isCommissionAsset() && selectedAssetIsAvailableToPayFee) { - val feeAmount = feeStatus.feeModel.decimalFee.networkFee.amount + val feeAmount = feeStatus.feeModel.fee.amount if (interactor.hasEnoughBalanceToPayFee(commissionAsset, feeAmount)) { feeLiveData.postValue(feeStatus) } else { diff --git a/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/presentation/mixin/fee/provider/FeeLoaderProviderFactory.kt b/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/presentation/mixin/fee/provider/FeeLoaderProviderFactory.kt index 0a5bf1bfb4..ed82bf3a0a 100644 --- a/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/presentation/mixin/fee/provider/FeeLoaderProviderFactory.kt +++ b/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/presentation/mixin/fee/provider/FeeLoaderProviderFactory.kt @@ -2,12 +2,12 @@ package io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.provide import io.novafoundation.nova.common.mixin.actionAwaitable.ActionAwaitableMixin import io.novafoundation.nova.common.resources.ResourceManager +import io.novafoundation.nova.feature_account_api.data.model.Fee +import io.novafoundation.nova.feature_account_api.data.model.FeeBase import io.novafoundation.nova.feature_wallet_api.domain.fee.CustomFeeInteractor import io.novafoundation.nova.feature_wallet_api.domain.model.Token import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.FeeLoaderMixin -import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.GenericFee import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.GenericFeeLoaderMixin -import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.SimpleFee import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.Flow @@ -21,35 +21,19 @@ class FeeLoaderProviderFactory( override fun create( tokenFlow: Flow, - configuration: GenericFeeLoaderMixin.Configuration + configuration: GenericFeeLoaderMixin.Configuration ): FeeLoaderMixin.Presentation { return GenericFeeLoaderProviderPresentation(resourceManager, configuration, tokenFlow) } - override fun createGeneric( + override fun createGeneric( tokenFlow: Flow, configuration: GenericFeeLoaderMixin.Configuration ): GenericFeeLoaderMixin.Presentation { return GenericFeeLoaderProvider(resourceManager, configuration, tokenFlow) } - override fun createChangeableFee( - tokenFlow: Flow, - coroutineScope: CoroutineScope, - configuration: GenericFeeLoaderMixin.Configuration - ): FeeLoaderMixin.Presentation { - return ChangeableFeeLoaderProviderPresentation( - customFeeInteractor, - chainRegistry, - resourceManager, - configuration, - actionAwaitableMixinFactory, - tokenFlow, - coroutineScope - ) - } - - override fun createChangeableFeeGeneric( + override fun createChangeable( tokenFlow: Flow, coroutineScope: CoroutineScope, configuration: GenericFeeLoaderMixin.Configuration diff --git a/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/presentation/mixin/fee/provider/GenericFeeLoaderProvider.kt b/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/presentation/mixin/fee/provider/GenericFeeLoaderProvider.kt index fd3dfb6bde..6da99419ea 100644 --- a/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/presentation/mixin/fee/provider/GenericFeeLoaderProvider.kt +++ b/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/presentation/mixin/fee/provider/GenericFeeLoaderProvider.kt @@ -7,6 +7,8 @@ import io.novafoundation.nova.common.mixin.api.RetryPayload import io.novafoundation.nova.common.resources.ResourceManager import io.novafoundation.nova.common.utils.Event import io.novafoundation.nova.common.utils.firstNotNull +import io.novafoundation.nova.feature_account_api.data.model.Fee +import io.novafoundation.nova.feature_account_api.data.model.FeeBase import io.novafoundation.nova.feature_wallet_api.R import io.novafoundation.nova.feature_wallet_api.data.mappers.mapFeeToFeeModel import io.novafoundation.nova.feature_wallet_api.domain.model.Asset @@ -14,9 +16,7 @@ import io.novafoundation.nova.feature_wallet_api.domain.model.Token import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.ChangeFeeTokenState import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.FeeLoaderMixin import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.FeeStatus -import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.GenericFee import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.GenericFeeLoaderMixin -import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.SimpleFee import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain import kotlinx.coroutines.CancellationException import kotlinx.coroutines.CoroutineScope @@ -28,12 +28,12 @@ import kotlinx.coroutines.withContext @Deprecated("Use ChangeableFeeLoaderProviderPresentation instead") internal class GenericFeeLoaderProviderPresentation( resourceManager: ResourceManager, - configuration: GenericFeeLoaderMixin.Configuration, + configuration: GenericFeeLoaderMixin.Configuration, tokenFlow: Flow, -) : GenericFeeLoaderProvider(resourceManager, configuration, tokenFlow), FeeLoaderMixin.Presentation +) : GenericFeeLoaderProvider(resourceManager, configuration, tokenFlow), FeeLoaderMixin.Presentation @Deprecated("Use ChangeableFeeLoaderProvider instead") -internal open class GenericFeeLoaderProvider( +internal open class GenericFeeLoaderProvider( protected val resourceManager: ResourceManager, protected val configuration: GenericFeeLoaderMixin.Configuration, protected val tokenFlow: Flow, @@ -67,7 +67,7 @@ internal open class GenericFeeLoaderProvider( value?.run { feeLiveData.postValue(this) } } - override fun loadFeeV2Generic( + override fun loadFee( coroutineScope: CoroutineScope, feeConstructor: suspend (Token) -> F?, onRetryCancelled: () -> Unit @@ -131,7 +131,7 @@ internal open class GenericFeeLoaderProvider( RetryPayload( title = resourceManager.getString(R.string.choose_amount_network_error), message = resourceManager.getString(R.string.choose_amount_error_fee), - onRetry = { loadFeeV2Generic(retryScope, feeConstructor, onRetryCancelled) }, + onRetry = { loadFee(retryScope, feeConstructor, onRetryCancelled) }, onCancel = onRetryCancelled ) ) 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 465f77a4d0..fbf30a1c31 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 @@ -1,46 +1,8 @@ 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.executingAccountPaysFee -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 -import java.math.BigDecimal +import io.novafoundation.nova.feature_account_api.data.model.FeeBase -typealias DecimalFee = GenericDecimalFee - -class GenericFeeModel( - val decimalFee: GenericDecimalFee, +class FeeModel( + val fee: F, val display: AmountModel, - val chainAsset: Chain.Asset ) - -class GenericDecimalFee( - val genericFee: F, - val networkFeeDecimalAmount: BigDecimal -) { - - 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 - get() = if (networkFee.executingAccountPaysFee) networkFeeDecimalAmount else BigDecimal.ZERO - -val GenericDecimalFee?.networkFeeByRequestedAccountOrZero: BigDecimal - get() = this?.networkFeeByRequestedAccount.orZero() - -val GenericDecimalFee?.networkFeeDecimalAmount: BigDecimal - get() = this?.networkFeeDecimalAmount.orZero() diff --git a/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/data/network/blockchain/assets/transfers/evmErc20/EvmErc20AssetTransfers.kt b/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/data/network/blockchain/assets/transfers/evmErc20/EvmErc20AssetTransfers.kt index 0782efef18..7579b4b5dc 100644 --- a/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/data/network/blockchain/assets/transfers/evmErc20/EvmErc20AssetTransfers.kt +++ b/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/data/network/blockchain/assets/transfers/evmErc20/EvmErc20AssetTransfers.kt @@ -56,7 +56,7 @@ class EvmErc20AssetTransfers( override suspend fun performTransfer(transfer: WeightedAssetTransfer, coroutineScope: CoroutineScope): Result { return evmTransactionService.transact( chainId = transfer.originChain.id, - presetFee = transfer.decimalFee.networkFee, + presetFee = transfer.fee.submissionFee, fallbackGasLimit = ERC_20_UPPER_GAS_LIMIT ) { transfer(transfer) diff --git a/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/data/network/blockchain/assets/transfers/evmNative/EvmNativeAssetTransfers.kt b/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/data/network/blockchain/assets/transfers/evmNative/EvmNativeAssetTransfers.kt index 4d453aba61..e816d23534 100644 --- a/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/data/network/blockchain/assets/transfers/evmNative/EvmNativeAssetTransfers.kt +++ b/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/data/network/blockchain/assets/transfers/evmNative/EvmNativeAssetTransfers.kt @@ -53,7 +53,7 @@ class EvmNativeAssetTransfers( return evmTransactionService.transact( chainId = transfer.originChain.id, fallbackGasLimit = NATIVE_COIN_TRANSFER_GAS_LIMIT, - presetFee = transfer.decimalFee.networkFee, + presetFee = transfer.fee.submissionFee, ) { nativeTransfer(transfer) } 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 f0e7f1f7b2..79cebde270 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 @@ -14,24 +14,22 @@ import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.t 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.PhishingValidationFactory 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.doNotCrossExistentialDepositMultiFee 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.sufficientBalanceMultiFee 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.multiNetwork.ChainWithAsset import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain.Asset.Type -import java.math.BigDecimal import kotlinx.coroutines.CoroutineScope +import java.math.BigDecimal fun AssetTransfersValidationSystemBuilder.positiveAmount() = positiveAmount( amount = { it.transfer.amount }, @@ -57,7 +55,7 @@ fun AssetTransfersValidationSystemBuilder.sufficientCommissionBalanceToStayAbove enoughTotalToStayAboveEDValidationFactory: EnoughTotalToStayAboveEDValidationFactory ) { enoughTotalToStayAboveEDValidationFactory.validate( - fee = { it.originFee.networkFeePart() }, + fee = { it.originFee.submissionFee }, balance = { it.originCommissionAsset.balanceCountedTowardsED() }, chainWithAsset = { ChainWithAsset(it.transfer.originChain, it.commissionChainAsset) }, error = { payload, error -> AssetTransferValidationFailure.NotEnoughFunds.ToStayAboveED(payload.commissionChainAsset, error) } @@ -71,7 +69,7 @@ fun AssetTransfersValidationSystemBuilder.checkForFeeChanges( calculateFee = { payload -> val transfers = assetSourceRegistry.sourceFor(payload.transfer.originChainAsset).transfers val fee = transfers.calculateFee(payload.transfer, coroutineScope) - SimpleGenericFee(payload.originFee.genericFee.networkFee.copy(networkFee = fee)) + payload.originFee.copy(submissionFee = fee) }, currentFee = { it.originFee }, chainAsset = { it.commissionChainAsset }, @@ -81,7 +79,7 @@ fun AssetTransfersValidationSystemBuilder.checkForFeeChanges( fun AssetTransfersValidationSystemBuilder.doNotCrossExistentialDepositInUsedAsset( assetSourceRegistry: AssetSourceRegistry, extraAmount: AmountProducer, -) = doNotCrossExistentialDepositMultyFee( +) = doNotCrossExistentialDepositMultiFee( countableTowardsEdBalance = { it.originUsedAsset.balanceCountedTowardsED() }, fee = { it.originFeeListInUsedAsset }, extraAmount = extraAmount, @@ -89,7 +87,7 @@ fun AssetTransfersValidationSystemBuilder.doNotCrossExistentialDepositInUsedAsse error = { remainingAmount, payload -> payload.transfer.originChainAsset.existentialDepositError(remainingAmount) } ) -fun AssetTransfersValidationSystemBuilder.sufficientTransferableBalanceToPayOriginFee() = sufficientBalanceMultyFee( +fun AssetTransfersValidationSystemBuilder.sufficientTransferableBalanceToPayOriginFee() = sufficientBalanceMultiFee( available = { it.originCommissionAsset.transferable }, amount = { it.sendingAmountInCommissionAsset }, feeExtractor = { it.originFeeList }, 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 6fcb1a9f00..bbe471b7a9 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 @@ -15,6 +15,7 @@ import io.novafoundation.nova.feature_account_api.data.extrinsic.ExtrinsicSubmis import io.novafoundation.nova.feature_account_api.data.extrinsic.execution.ExtrinsicExecutionResult import io.novafoundation.nova.feature_account_api.data.extrinsic.execution.requireOk import io.novafoundation.nova.feature_account_api.data.model.Fee +import io.novafoundation.nova.feature_account_api.data.model.decimalAmount import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.AssetSourceRegistry import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.balances.model.TransferableBalanceUpdate import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.events.tryDetectDeposit @@ -86,7 +87,7 @@ class RealCrossChainTransactor( doNotCrossExistentialDepositInUsedAsset( assetSourceRegistry = assetSourceRegistry, - extraAmount = { it.transfer.amount + it.crossChainFee?.networkFeeDecimalAmount.orZero() } + extraAmount = { it.transfer.amount + it.crossChainFee?.decimalAmount.orZero() } ) } 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 2f1a2e0c3c..d442520dce 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 @@ -86,7 +86,7 @@ class RealCrossChainWeigher( val maxWeight = feeConfig.estimatedWeight() return when (val mode = feeConfig.to.xcmFeeType.mode) { - is Mode.Proportional -> CrossChainFeeModel(paidFromHoldingRegister = mode.weightToFee(maxWeight)) + is Mode.Proportional -> CrossChainFeeModel(executionFees = mode.weightToFee(maxWeight)) Mode.Standard -> { val xcmMessage = xcmMessage(feeConfig.to.xcmFeeType.instructions, chain, amount) @@ -95,7 +95,7 @@ class RealCrossChainWeigher( xcmExecute(xcmMessage, maxWeight) } - CrossChainFeeModel(paidFromHoldingRegister = paymentInfo.partialFee) + CrossChainFeeModel(executionFees = paymentInfo.partialFee) } Mode.Unknown -> CrossChainFeeModel.zero() @@ -122,9 +122,9 @@ class RealCrossChainWeigher( val isSenderPaysOriginDelivery = !deliveryConfig.alwaysHoldingPays return if (isSenderPaysOriginDelivery && isSendingFromOrigin) { - CrossChainFeeModel(paidByOrigin = deliveryFee) + CrossChainFeeModel(deliveryFees = deliveryFee) } else { - CrossChainFeeModel(paidFromHoldingRegister = deliveryFee) + CrossChainFeeModel(executionFees = deliveryFee) } } diff --git a/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/data/network/crosschain/validations/CannotDropBelowEdWhenPayingDeliveryFeeValidation.kt b/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/data/network/crosschain/validations/CannotDropBelowEdWhenPayingDeliveryFeeValidation.kt index 5409e6c4cd..b4031592c2 100644 --- a/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/data/network/crosschain/validations/CannotDropBelowEdWhenPayingDeliveryFeeValidation.kt +++ b/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/data/network/crosschain/validations/CannotDropBelowEdWhenPayingDeliveryFeeValidation.kt @@ -15,8 +15,6 @@ import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.t 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.amountInPlanks import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.tranfers.isSendingCommissionAsset -import io.novafoundation.nova.feature_wallet_api.domain.model.deliveryFeePart -import io.novafoundation.nova.feature_wallet_api.domain.model.networkFeePart import io.novasama.substrate_sdk_android.hash.isPositive class CannotDropBelowEdWhenPayingDeliveryFeeValidation( @@ -28,11 +26,11 @@ class CannotDropBelowEdWhenPayingDeliveryFeeValidation( val existentialDeposit = assetSourceRegistry.existentialDepositInPlanks(value.transfer.originChain, value.transfer.originChainAsset) - val deliveryFeePart = value.originFee.deliveryFeePart()?.networkFee?.amount.orZero() + val deliveryFeePart = value.originFee.deliveryFee?.amount.orZero() val paysDeliveryFee = deliveryFeePart.isPositive() - val networkFeePlanks = value.originFee.networkFeePart().networkFee.amountByExecutingAccount - val crossChainFeePlanks = value.crossChainFee?.networkFee?.amountByExecutingAccount.orZero() + val networkFeePlanks = value.originFee.submissionFee.amountByExecutingAccount + val crossChainFeePlanks = value.crossChainFee?.amount.orZero() val sendingAmount = value.transfer.amountInPlanks + crossChainFeePlanks val requiredAmountWhenPayingDeliveryFee = sendingAmount + networkFeePlanks + deliveryFeePart + existentialDeposit 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 6db57aa81a..48357431c0 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 @@ -1,23 +1,24 @@ package io.novafoundation.nova.feature_wallet_impl.data.network.crosschain.validations +import io.novafoundation.nova.common.utils.orZero import io.novafoundation.nova.common.validation.ValidationStatus import io.novafoundation.nova.common.validation.isTrueOrError +import io.novafoundation.nova.feature_account_api.data.model.decimalAmount +import io.novafoundation.nova.feature_account_api.data.model.decimalAmountByExecutingAccount 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.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.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 originFeeSum = value.originFeeListInUsedAsset.sumOf { it.networkFeeByRequestedAccount } + val originFeeSum = value.originFeeListInUsedAsset.sumOf { it.decimalAmountByExecutingAccount } val remainingBalanceAfterTransfer = value.originUsedAsset.transferable - value.transfer.amount - originFeeSum - val crossChainFee = value.crossChainFee.networkFeeDecimalAmount + val crossChainFee = value.crossChainFee?.decimalAmount.orZero() val remainsEnoughToPayCrossChainFees = remainingBalanceAfterTransfer >= crossChainFee return remainsEnoughToPayCrossChainFees isTrueOrError { diff --git a/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/domain/RealCrossChainTransfersUseCase.kt b/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/domain/RealCrossChainTransfersUseCase.kt index 7409010a37..3f9de8d140 100644 --- a/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/domain/RealCrossChainTransfersUseCase.kt +++ b/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/domain/RealCrossChainTransfersUseCase.kt @@ -14,7 +14,7 @@ import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.types.Ba 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.data.network.crosschain.paidByOriginOrNull +import io.novafoundation.nova.feature_wallet_api.data.network.crosschain.deliveryFeesOrNull import io.novafoundation.nova.feature_wallet_api.domain.implementations.availableInDestinations import io.novafoundation.nova.feature_wallet_api.domain.implementations.availableOutDestinations import io.novafoundation.nova.feature_wallet_api.domain.implementations.transferConfiguration @@ -110,9 +110,9 @@ internal class RealCrossChainTransfersUseCase( override suspend fun ExtrinsicService.estimateFee( transfer: AssetTransferBase, - computationalScope: CoroutineScope + cachingScope: CoroutineScope? ): CrossChainTransferFee { - val configuration = cachedConfigurationFlow(computationalScope).first() + val configuration = cachedConfigurationFlow(cachingScope).first() val transferConfiguration = configuration.transferConfiguration( originChain = transfer.originChain, originAsset = transfer.originChainAsset, @@ -127,14 +127,14 @@ internal class RealCrossChainTransfersUseCase( val crossChainFee = crossChainWeigher.estimateFee(transfer.amountPlanks, transferConfiguration) return CrossChainTransferFee( - fromOriginInFeeCurrency = originFee, - fromOriginInNativeCurrency = crossChainFee.paidByOriginOrNull()?.let { + submissionFee = originFee, + deliveryFee = crossChainFee.deliveryFeesOrNull()?.let { // Delivery fees are also paid by an actual account val submissionOrigin = SubmissionOrigin.singleOrigin(originFee.submissionOrigin.signingAccount) SubstrateFee(it, submissionOrigin, transfer.originChain.commissionAsset) }, - fromHoldingRegister = SubstrateFeeBase( - amount = crossChainFee.paidFromHoldingRegister, + executionFee = SubstrateFeeBase( + amount = crossChainFee.executionFees, asset = transfer.originChainAsset, ), ) @@ -155,8 +155,12 @@ internal class RealCrossChainTransfersUseCase( return crossChainTransactor.performAndTrackTransfer(transferConfiguration, transfer) } - private fun cachedConfigurationFlow(computationScope: CoroutineScope): Flow { - return computationalCache.useSharedFlow(CONFIGURATION_CACHE, computationScope) { + private fun cachedConfigurationFlow(cachingScope: CoroutineScope?): Flow { + if (cachingScope == null) { + return crossChainTransfersRepository.configurationFlow() + } + + return computationalCache.useSharedFlow(CONFIGURATION_CACHE, cachingScope) { crossChainTransfersRepository.configurationFlow() } } From e5e9f5a0ec9e5f0f24569a61af6690038ac4650a Mon Sep 17 00:00:00 2001 From: Valentun Date: Tue, 29 Oct 2024 19:02:27 +0300 Subject: [PATCH 35/83] Fix --- .../nova/feature_swap_api/domain/model/SwapFee.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/feature-swap-api/src/main/java/io/novafoundation/nova/feature_swap_api/domain/model/SwapFee.kt b/feature-swap-api/src/main/java/io/novafoundation/nova/feature_swap_api/domain/model/SwapFee.kt index 9a059ba983..f5229d9958 100644 --- a/feature-swap-api/src/main/java/io/novafoundation/nova/feature_swap_api/domain/model/SwapFee.kt +++ b/feature-swap-api/src/main/java/io/novafoundation/nova/feature_swap_api/domain/model/SwapFee.kt @@ -30,9 +30,10 @@ class SwapFee( private val assetIn = intermediateSegmentFeesInAssetIn.asset + val additionalAmountForSwap = additionalAmountForSwap() + val deductionForAssetIn: Balance = deductionFor(assetIn) - val additionalAmountForSwap = additionalAmountForSwap() override fun deductionFor(amountAsset: Chain.Asset): Balance { return totalFeeAmount(amountAsset) + additionalMaxAmountDeduction From 2282899aaa28e2f8536989ec951dac9ac48445c2 Mon Sep 17 00:00:00 2001 From: Valentun Date: Thu, 31 Oct 2024 13:03:54 +0300 Subject: [PATCH 36/83] Fees refactoring --- .../nova/common/mixin/impl/RetriableUi.kt | 18 +- .../fee/FeePaymentCurrencyParcel.kt | 29 ++ .../di/AssetsFeatureDependencies.kt | 3 + .../domain/send/SendInteractor.kt | 20 +- .../domain/send/model/TransferFee.kt | 28 ++ .../domain/send/model/TransferFeeModel.kt | 9 - .../send/TransferAssetValidationFailureUi.kt | 10 +- .../presentation/send/TransferDraft.kt | 4 +- .../send/amount/SelectSendFragment.kt | 5 +- .../send/amount/SelectSendViewModel.kt | 129 +++----- .../send/amount/di/SelectSendModule.kt | 4 +- .../send/common/AssetTransferExt.kt | 6 +- .../presentation/send/common/fee/Factory.kt | 23 ++ .../common/fee/TransferFeeBalanceExtractor.kt | 13 + .../send/common/fee/TransferFeeDisplay.kt | 12 + .../common/fee/TransferFeeDisplayFormatter.kt | 31 ++ .../presentation/send/common/fee/Ui.kt | 29 ++ .../send/confirm/ConfirmSendFragment.kt | 9 +- .../send/confirm/ConfirmSendViewModel.kt | 63 ++-- .../send/confirm/di/ConfirmSendModule.kt | 4 +- .../confirm/ConfirmContributeViewModel.kt | 2 +- .../signExtrinsic/ExternaSignViewModel.kt | 2 +- .../vote/setup/common/SetupVoteViewModel.kt | 4 +- ...NominationPoolsConfirmBondMoreViewModel.kt | 2 +- .../NominationPoolsConfirmUnbondViewModel.kt | 2 +- .../bond/confirm/ConfirmBondMoreViewModel.kt | 2 +- .../confirm/ConfirmSetControllerViewModel.kt | 2 +- .../ConfirmRewardDestinationViewModel.kt | 2 +- .../confirm/ConfirmMultiStakingViewModel.kt | 2 +- .../unbond/confirm/ConfirmUnbondViewModel.kt | 2 +- .../domain/model/AtomicSwapOperation.kt | 12 + .../feature_swap_api/domain/model/SwapFee.kt | 13 +- .../di/SwapFeatureDependencies.kt | 3 + .../feature_swap_impl/di/SwapFeatureModule.kt | 7 +- .../domain/interactor/SwapInteractor.kt | 26 +- ...onsideringNonSufficientAssetsValidation.kt | 3 +- .../validation/SwapPayloadValidation.kt | 4 +- .../domain/validation/SwapValidations.kt | 8 +- .../SwapFeeSufficientBalanceValidation.kt | 17 +- .../common/fee/SwapFeeBalanceExtractor.kt | 13 + .../common/fee/SwapFeeFormatter.kt | 30 ++ .../confirmation/SwapConfirmationFragment.kt | 5 +- .../confirmation/SwapConfirmationViewModel.kt | 30 +- .../confirmation/di/SwapConfirmationModule.kt | 4 +- .../main/SwapMainSettingsFragment.kt | 31 +- .../main/SwapMainSettingsViewModel.kt | 107 ++----- .../main/di/SwapMainSettingsModule.kt | 4 +- .../maxAction/MaxActionProviderFactory.kt | 9 +- .../feature_wallet_api/data/mappers/Fee.kt | 10 +- .../tranfers/AssetTransferValidations.kt | 5 +- .../assets/tranfers/AssetTransfers.kt | 21 +- .../feature_wallet_api/di/WalletFeatureApi.kt | 3 + .../domain/interfaces/WalletRepository.kt | 4 +- .../domain/model/FiatAmount.kt | 9 + .../domain/model/OriginFee.kt | 26 +- .../feature_wallet_api/domain/model/Token.kt | 7 + .../domain/validation/FeeChangeValidation.kt | 6 +- .../maxAction/FeeAwareMaxActionProvider.kt | 20 +- .../maxAction/MaxActionProvider.kt | 9 +- .../presentation/mixin/fee/FeeLoaderMixin.kt | 95 +----- .../presentation/mixin/fee/FeeUI.kt | 37 --- .../presentation/mixin/fee/SetFee.kt | 6 + .../fee/amount/DefaultFeeBalanceExtractor.kt | 13 + .../mixin/fee/amount/FeeBalanceExtractor.kt | 9 + .../fee/formatter/DefaultFeeFormatter.kt | 26 ++ .../mixin/fee/formatter/FeeFormatter.kt | 40 +++ .../fee/model/ChooseFeeCurrencyPayload.kt | 5 + .../presentation/mixin/fee/model/FeeModel.kt | 17 + .../presentation/mixin/fee/model/FeeStatus.kt | 43 +++ .../fee/model/PaymentCurrencySelectionMode.kt | 34 ++ .../provider/ChangeableFeeLoaderProvider.kt | 262 ---------------- .../fee/provider/FeeLoaderProviderFactory.kt | 31 -- .../fee/provider/GenericFeeLoaderProvider.kt | 87 +++-- .../mixin/fee/v2/FeeLoaderMixinV2.kt | 89 ++++++ .../mixin/fee/v2/FeeLoaderMixinV2Ext.kt | 56 ++++ .../mixin/fee/v2/FeeLoaderV2Factory.kt | 39 +++ .../mixin/fee/v2/FeeLoaderV2Provider.kt | 296 ++++++++++++++++++ .../presentation/mixin/fee/v2/FeeUI.kt | 58 ++++ .../presentation/model/FeeModel.kt | 8 - .../presentation/view/FeeView.kt | 19 +- .../GenericExtrinsicInformationView.kt | 5 +- .../assets/transfers/BaseAssetTransfers.kt | 8 +- .../assets/transfers/validations/Common.kt | 5 +- .../data/repository/TokenRepositoryImpl.kt | 2 +- .../data/repository/WalletRepositoryImpl.kt | 9 +- .../di/WalletFeatureModule.kt | 23 +- 86 files changed, 1343 insertions(+), 896 deletions(-) create mode 100644 feature-account-api/src/main/java/io/novafoundation/nova/feature_account_api/presenatation/fee/FeePaymentCurrencyParcel.kt create mode 100644 feature-assets/src/main/java/io/novafoundation/nova/feature_assets/domain/send/model/TransferFee.kt delete mode 100644 feature-assets/src/main/java/io/novafoundation/nova/feature_assets/domain/send/model/TransferFeeModel.kt create mode 100644 feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/send/common/fee/Factory.kt create mode 100644 feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/send/common/fee/TransferFeeBalanceExtractor.kt create mode 100644 feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/send/common/fee/TransferFeeDisplay.kt create mode 100644 feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/send/common/fee/TransferFeeDisplayFormatter.kt create mode 100644 feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/send/common/fee/Ui.kt create mode 100644 feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/common/fee/SwapFeeBalanceExtractor.kt create mode 100644 feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/common/fee/SwapFeeFormatter.kt create mode 100644 feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/domain/model/FiatAmount.kt create mode 100644 feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/presentation/mixin/fee/SetFee.kt create mode 100644 feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/presentation/mixin/fee/amount/DefaultFeeBalanceExtractor.kt create mode 100644 feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/presentation/mixin/fee/amount/FeeBalanceExtractor.kt create mode 100644 feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/presentation/mixin/fee/formatter/DefaultFeeFormatter.kt create mode 100644 feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/presentation/mixin/fee/formatter/FeeFormatter.kt create mode 100644 feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/presentation/mixin/fee/model/ChooseFeeCurrencyPayload.kt create mode 100644 feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/presentation/mixin/fee/model/FeeModel.kt create mode 100644 feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/presentation/mixin/fee/model/FeeStatus.kt create mode 100644 feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/presentation/mixin/fee/model/PaymentCurrencySelectionMode.kt delete mode 100644 feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/presentation/mixin/fee/provider/ChangeableFeeLoaderProvider.kt create mode 100644 feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/presentation/mixin/fee/v2/FeeLoaderMixinV2.kt create mode 100644 feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/presentation/mixin/fee/v2/FeeLoaderMixinV2Ext.kt create mode 100644 feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/presentation/mixin/fee/v2/FeeLoaderV2Factory.kt create mode 100644 feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/presentation/mixin/fee/v2/FeeLoaderV2Provider.kt create mode 100644 feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/presentation/mixin/fee/v2/FeeUI.kt delete mode 100644 feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/presentation/model/FeeModel.kt diff --git a/common/src/main/java/io/novafoundation/nova/common/mixin/impl/RetriableUi.kt b/common/src/main/java/io/novafoundation/nova/common/mixin/impl/RetriableUi.kt index 6c3c0b21a0..11ff5eea13 100644 --- a/common/src/main/java/io/novafoundation/nova/common/mixin/impl/RetriableUi.kt +++ b/common/src/main/java/io/novafoundation/nova/common/mixin/impl/RetriableUi.kt @@ -10,14 +10,16 @@ fun BaseFragmentMixin<*>.observeRetries( retriable: Retriable, context: Context = fragment.requireContext(), ) { - retriable.retryEvent.observeEvent { - retryDialog( - context = context, - onRetry = it.onRetry, - onCancel = it.onCancel - ) { - setTitle(it.title) - setMessage(it.message) + with(retriable) { + retryEvent.observeEvent { + retryDialog( + context = context, + onRetry = it.onRetry, + onCancel = it.onCancel + ) { + setTitle(it.title) + setMessage(it.message) + } } } } diff --git a/feature-account-api/src/main/java/io/novafoundation/nova/feature_account_api/presenatation/fee/FeePaymentCurrencyParcel.kt b/feature-account-api/src/main/java/io/novafoundation/nova/feature_account_api/presenatation/fee/FeePaymentCurrencyParcel.kt new file mode 100644 index 0000000000..e17c1a48f8 --- /dev/null +++ b/feature-account-api/src/main/java/io/novafoundation/nova/feature_account_api/presenatation/fee/FeePaymentCurrencyParcel.kt @@ -0,0 +1,29 @@ +package io.novafoundation.nova.feature_account_api.presenatation.fee + +import android.os.Parcelable +import io.novafoundation.nova.feature_account_api.data.fee.FeePaymentCurrency +import io.novafoundation.nova.runtime.util.ChainAssetParcel +import kotlinx.android.parcel.Parcelize + +sealed class FeePaymentCurrencyParcel : Parcelable { + + @Parcelize + object Native : FeePaymentCurrencyParcel() + + @Parcelize + class Asset(val asset: ChainAssetParcel): FeePaymentCurrencyParcel() +} + +fun FeePaymentCurrency.toParcel(): FeePaymentCurrencyParcel { + return when(this) { + is FeePaymentCurrency.Asset -> FeePaymentCurrencyParcel.Asset(ChainAssetParcel(asset)) + FeePaymentCurrency.Native -> FeePaymentCurrencyParcel.Native + } +} + +fun FeePaymentCurrencyParcel.toDomain(): FeePaymentCurrency { + return when(this) { + is FeePaymentCurrencyParcel.Asset -> FeePaymentCurrency.Asset(asset.value) + FeePaymentCurrencyParcel.Native -> FeePaymentCurrency.Native + } +} 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 1ce1f7e787..44b1f87bf1 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 @@ -68,6 +68,7 @@ import io.novafoundation.nova.feature_wallet_api.domain.interfaces.WalletConstan import io.novafoundation.nova.feature_wallet_api.domain.interfaces.WalletRepository 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.v2.FeeLoaderMixinV2 import io.novafoundation.nova.feature_wallet_connect_api.domain.sessions.WalletConnectSessionsUseCase import io.novafoundation.nova.runtime.di.LOCAL_STORAGE_SOURCE import io.novafoundation.nova.runtime.di.REMOTE_STORAGE_SOURCE @@ -250,4 +251,6 @@ interface AssetsFeatureDependencies { val holdsDao: HoldsDao val coinGeckoLinkParser: CoinGeckoLinkParser + + val feeLoaderMixinV2Factory: FeeLoaderMixinV2.Factory } 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 a85599d964..a69374d323 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 @@ -2,8 +2,7 @@ package io.novafoundation.nova.feature_assets.domain.send import io.novafoundation.nova.feature_account_api.data.extrinsic.ExtrinsicService import io.novafoundation.nova.feature_account_api.data.model.FeeBase -import io.novafoundation.nova.feature_account_api.data.model.decimalAmount -import io.novafoundation.nova.feature_assets.domain.send.model.TransferFeeModel +import io.novafoundation.nova.feature_assets.domain.send.model.TransferFee 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 @@ -31,7 +30,7 @@ class SendInteractor( private val extrinsicService: ExtrinsicService, ) { - suspend fun getFee(transfer: AssetTransfer, coroutineScope: CoroutineScope): TransferFeeModel = withContext(Dispatchers.Default) { + suspend fun getFee(transfer: AssetTransfer, coroutineScope: CoroutineScope): TransferFee = withContext(Dispatchers.Default) { if (transfer.isCrossChain) { val fees = with(crossChainTransfersUseCase) { extrinsicService.estimateFee(transfer, cachingScope = null) @@ -40,15 +39,14 @@ class SendInteractor( val originFee = OriginFee( submissionFee = fees.submissionFee, deliveryFee = fees.deliveryFee, - chainAsset = transfer.commissionAssetToken.configuration ) - TransferFeeModel(originFee, fees.executionFee) + TransferFee(originFee, fees.executionFee) } else { - val nativeFee = getAssetTransfers(transfer).calculateFee(transfer, coroutineScope = coroutineScope) - TransferFeeModel( - OriginFee(nativeFee, null, transfer.commissionAssetToken.configuration), - null + val submissionFee = getAssetTransfers(transfer).calculateFee(transfer, coroutineScope = coroutineScope) + TransferFee( + originFee = OriginFee(submissionFee, null), + crossChainFee = null ) } } @@ -66,12 +64,12 @@ class SendInteractor( extrinsicService.performTransfer(config, transfer, crossChainFee!!.amount) } } else { - val networkFee = originFee.submissionFee + val submissionFee = originFee.submissionFee getAssetTransfers(transfer).performTransfer(transfer, coroutineScope) .onSuccess { submission -> // Insert used fee regardless of who paid it - walletRepository.insertPendingTransfer(submission.hash, transfer, networkFee.decimalAmount) + walletRepository.insertPendingTransfer(submission.hash, transfer, submissionFee) } } } diff --git a/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/domain/send/model/TransferFee.kt b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/domain/send/model/TransferFee.kt new file mode 100644 index 0000000000..55887498bd --- /dev/null +++ b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/domain/send/model/TransferFee.kt @@ -0,0 +1,28 @@ +package io.novafoundation.nova.feature_assets.domain.send.model + +import io.novafoundation.nova.common.utils.orZero +import io.novafoundation.nova.feature_account_api.data.model.FeeBase +import io.novafoundation.nova.feature_account_api.data.model.SubmissionFee +import io.novafoundation.nova.feature_account_api.data.model.getAmount +import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.types.Balance +import io.novafoundation.nova.feature_wallet_api.domain.model.OriginFee +import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain + +data class TransferFee( + val originFee: OriginFee, + val crossChainFee: FeeBase? +) { + + fun totalFeeByExecutingAccount(chainAsset: Chain.Asset): Balance { + val accountThatPaysFees = originFee.submissionFee.submissionOrigin.executingAccount + + val submission = originFee.submissionFee.getAmount(chainAsset, accountThatPaysFees) + val delivery = originFee.deliveryFee?.getAmount(chainAsset, accountThatPaysFees).orZero() + + return submission + delivery + } + + fun replaceSubmission(newSubmissionFee: SubmissionFee): TransferFee { + return copy(originFee = originFee.copy(newSubmissionFee)) + } +} 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 deleted file mode 100644 index b8f69bc67d..0000000000 --- a/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/domain/send/model/TransferFeeModel.kt +++ /dev/null @@ -1,9 +0,0 @@ -package io.novafoundation.nova.feature_assets.domain.send.model - -import io.novafoundation.nova.feature_account_api.data.model.FeeBase -import io.novafoundation.nova.feature_wallet_api.domain.model.OriginFee - -class TransferFeeModel( - val originFee: OriginFee, - val crossChainFee: FeeBase? -) 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 9cd65474fb..c1faa8b015 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 @@ -6,16 +6,16 @@ import io.novafoundation.nova.common.validation.TransformedFailure.Default import io.novafoundation.nova.common.validation.ValidationFlowActions import io.novafoundation.nova.common.validation.ValidationStatus import io.novafoundation.nova.common.validation.asDefault +import io.novafoundation.nova.feature_account_api.data.model.SubmissionFee import io.novafoundation.nova.feature_account_api.domain.validation.handleSystemAccountValidationFailure 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.OriginFee 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.formatPlanks import io.novafoundation.nova.feature_wallet_api.presentation.formatters.formatTokenAmount -import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.GenericFeeLoaderMixin +import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.SetFee import io.novafoundation.nova.feature_wallet_api.presentation.validation.handleInsufficientBalanceCommission import io.novafoundation.nova.feature_wallet_api.presentation.validation.handleNonPositiveAmount import kotlinx.coroutines.CoroutineScope @@ -24,7 +24,7 @@ fun CoroutineScope.mapAssetTransferValidationFailureToUI( resourceManager: ResourceManager, status: ValidationStatus.NotValid, actions: ValidationFlowActions<*>, - feeLoaderMixin: GenericFeeLoaderMixin.Presentation, + setFee: SetFee, ): TransformedFailure? { return when (val reason = status.reason) { is AssetTransferValidationFailure.DeadRecipient.InCommissionAsset -> Default( @@ -98,7 +98,7 @@ fun CoroutineScope.mapAssetTransferValidationFailureToUI( is AssetTransferValidationFailure.FeeChangeDetected -> handleFeeSpikeDetected( error = reason, resourceManager = resourceManager, - feeLoaderMixin = feeLoaderMixin, + setFee = setFee, actions = actions, ) @@ -120,7 +120,7 @@ fun autoFixSendValidationPayload( is AssetTransferValidationFailure.FeeChangeDetected -> payload.copy( transfer = payload.transfer.copy( - fee = failureReason.payload.newFee + fee = payload.transfer.fee.replaceSubmissionFee(failureReason.payload.newFee) ) ) 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 9f490818af..31272304f6 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,6 +1,8 @@ package io.novafoundation.nova.feature_assets.presentation.send import android.os.Parcelable +import io.novafoundation.nova.feature_account_api.data.fee.FeePaymentCurrency +import io.novafoundation.nova.feature_account_api.presenatation.fee.FeePaymentCurrencyParcel import io.novafoundation.nova.feature_wallet_api.presentation.model.AssetPayload import kotlinx.android.parcel.Parcelize import java.math.BigDecimal @@ -9,7 +11,7 @@ import java.math.BigDecimal class TransferDraft( val amount: BigDecimal, val origin: AssetPayload, - val commission: AssetPayload, + val feePaymentCurrency: FeePaymentCurrencyParcel, val destination: AssetPayload, val recipientAddress: String, val openAssetDetailsOnCompletion: Boolean, diff --git a/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/send/amount/SelectSendFragment.kt b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/send/amount/SelectSendFragment.kt index 506710e212..d8e180a77e 100644 --- a/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/send/amount/SelectSendFragment.kt +++ b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/send/amount/SelectSendFragment.kt @@ -20,8 +20,8 @@ import io.novafoundation.nova.feature_assets.R import io.novafoundation.nova.feature_assets.di.AssetsFeatureApi import io.novafoundation.nova.feature_assets.di.AssetsFeatureComponent import io.novafoundation.nova.feature_assets.presentation.send.amount.view.SelectCrossChainDestinationBottomSheet +import io.novafoundation.nova.feature_assets.presentation.send.common.fee.setupFeeLoading import io.novafoundation.nova.feature_wallet_api.presentation.mixin.amountChooser.setupAmountChooser -import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.setupFeeLoading import kotlinx.android.synthetic.main.fragment_select_send.chooseAmountContainer import kotlinx.android.synthetic.main.fragment_select_send.selectSendAmount import kotlinx.android.synthetic.main.fragment_select_send.selectSendCrossChainFee @@ -85,8 +85,7 @@ class SelectSendFragment : BaseFragment() { observeValidations(viewModel) - setupFeeLoading(viewModel.originFeeMixin, selectSendOriginFee) - setupFeeLoading(viewModel.crossChainFeeMixin, selectSendCrossChainFee) + viewModel.feeMixin.setupFeeLoading(selectSendOriginFee, selectSendCrossChainFee) setupAmountChooser(viewModel.amountChooserMixin, selectSendAmount) setupAddressInput(viewModel.addressInputMixin, selectSendRecipient) 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 49e8a2f848..8ee2903f66 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 @@ -14,12 +14,14 @@ import io.novafoundation.nova.common.utils.singleReplaySharedFlow 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.fee.FeePaymentCurrency import io.novafoundation.nova.feature_account_api.data.mappers.mapChainToUi import io.novafoundation.nova.feature_account_api.domain.filter.selectAddress.SelectAddressAccountFilter import io.novafoundation.nova.feature_account_api.domain.interfaces.AccountRepository import io.novafoundation.nova.feature_account_api.domain.interfaces.SelectedAccountUseCase import io.novafoundation.nova.feature_account_api.domain.model.requireAccountIdIn import io.novafoundation.nova.feature_account_api.presenatation.actions.ExternalActions +import io.novafoundation.nova.feature_account_api.presenatation.fee.toParcel import io.novafoundation.nova.feature_account_api.presenatation.mixin.addressInput.AddressInputMixinFactory import io.novafoundation.nova.feature_account_api.presenatation.mixin.selectAddress.SelectAddressMixin import io.novafoundation.nova.feature_account_api.view.ChainChipModel @@ -33,23 +35,19 @@ import io.novafoundation.nova.feature_assets.presentation.send.amount.view.Cross 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.common.fee.TransferFeeDisplayFormatter +import io.novafoundation.nova.feature_assets.presentation.send.common.fee.createForTransfer 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.data.network.blockhain.assets.tranfers.commissionAsset 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.OriginFee import io.novafoundation.nova.feature_wallet_api.presentation.mixin.amountChooser.AmountChooserMixin -import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.FeeStatus -import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.GenericFeeLoaderMixin -import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.GenericFeeLoaderMixin.Configuration -import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.awaitFee -import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.awaitOptionalFee -import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.commissionAsset -import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.createChangeable -import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.createSimple +import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.model.PaymentCurrencySelectionMode +import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.v2.FeeLoaderMixinV2 +import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.v2.awaitFee +import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.v2.connectWith import io.novafoundation.nova.feature_wallet_api.presentation.model.AssetPayload import io.novafoundation.nova.feature_wallet_api.presentation.model.mapAmountToAmountModel import io.novafoundation.nova.runtime.ext.isEnabled @@ -84,7 +82,7 @@ class SelectSendViewModel( private val crossChainTransfersUseCase: CrossChainTransfersUseCase, private val accountRepository: AccountRepository, actionAwaitableMixinFactory: ActionAwaitableMixin.Factory, - feeLoaderMixinFactory: GenericFeeLoaderMixin.Factory, + feeLoaderMixinFactory: FeeLoaderMixinV2.Factory, selectedAccountUseCase: SelectedAccountUseCase, addressInputMixinFactory: AddressInputMixinFactory, amountChooserMixinFactory: AmountChooserMixin.Factory, @@ -112,7 +110,11 @@ class SelectSendViewModel( ) } - val selectAddressMixin = selectAddressMixinFactory.create(this, selectAddressPayloadFlow, ::onAddressSelect) + val selectAddressMixin = selectAddressMixinFactory.create( + coroutineScope = this, + payloadFlow = selectAddressPayloadFlow, + onAddressSelect = ::onAddressSelect + ) val addressInputMixin = with(addressInputMixinFactory) { val destinationChain = destinationChainWithAsset.map { it.chain } @@ -154,17 +156,8 @@ class SelectSendViewModel( private val originAssetFlow = originChainAsset.flatMapLatest(interactor::assetFlow) .shareInBackground() - val originFeeMixin = feeLoaderMixinFactory.createChangeable( - originAssetFlow, - coroutineScope, - configuration = Configuration( - initialState = Configuration.InitialState( - supportCustomFee = false - ) - ) - ) - - val crossChainFeeMixin = feeLoaderMixinFactory.createSimple(originAssetFlow) + private val feeFormatter = TransferFeeDisplayFormatter() + val feeMixin = feeLoaderMixinFactory.createForTransfer(originChainAsset, feeFormatter) val amountChooserMixin: AmountChooserMixin.Presentation = amountChooserMixinFactory.create( scope = this, @@ -195,25 +188,24 @@ class SelectSendViewModel( fun nextClicked() = launch { sendInProgressFlow.value = true - val originFee = originFeeMixin.awaitFee() - val crossChainFee = crossChainFeeMixin.awaitOptionalFee() + val fee = feeMixin.awaitFee() val transfer = buildTransfer( origin = originChainWithAsset.first(), destination = destinationChainWithAsset.first(), amount = amountChooserMixin.amountState.first().value ?: return@launch, - commissionAsset = originFeeMixin.commissionAsset(), + feePaymentCurrency = feeMixin.feePaymentCurrency(), address = addressInputMixin.getAddress(), ) val payload = AssetTransferPayload( transfer = WeightedAssetTransfer( assetTransfer = transfer, - fee = originFee, + fee = fee.originFee, ), - crossChainFee = crossChainFee, - originFee = originFee, - originCommissionAsset = originFeeMixin.commissionAsset(), + crossChainFee = fee.crossChainFee, + originFee = fee.originFee, + originCommissionAsset = feeMixin.feeAsset(), originUsedAsset = originAssetFlow.first() ) @@ -227,7 +219,7 @@ class SelectSendViewModel( resourceManager = resourceManager, status = status, actions = actions, - feeLoaderMixin = originFeeMixin + setFee = { feeMixin.setFee(fee.replaceSubmission(it)) } ) }, ) { @@ -317,59 +309,38 @@ class SelectSendViewModel( } private fun setupFees() { - combine( + feeMixin.connectWith( originChainWithAsset, - originFeeMixin.commissionAssetFlow(), destinationChainWithAsset, addressInputMixin.inputFlow, amountChooserMixin.backPressuredAmount, - ::recalculateFee - ) - .inBackground() - .launchIn(this) - - // Enable custom fee only for on chain transfers - combine(originChain, destinationChain) { originChain, destinationChain -> - originFeeMixin.setSupportCustomFee(originChain.id == destinationChain.id) - }.launchIn(this) - } - - private suspend fun recalculateFee( - originAsset: ChainWithAsset, - originCommissionAsset: Asset, - destinationAsset: ChainWithAsset, - address: String, - amount: BigDecimal - ) { - originFeeMixin.invalidateFee() - val hasCrossChainFee = originAsset.chain.id != destinationAsset.chain.id - - if (hasCrossChainFee) { - crossChainFeeMixin.invalidateFee() - } else { - crossChainFeeMixin.setFee(null) - } - - try { + ) { paymentCurrency, originAsset, destinationAsset, address, amount -> val assetTransfer = buildTransfer( origin = originAsset, destination = destinationAsset, amount = amount, - commissionAsset = originCommissionAsset, + feePaymentCurrency = paymentCurrency, address = address ) - val transferFeeModel = sendInteractor.getFee(assetTransfer, viewModelScope) + sendInteractor.getFee(assetTransfer, viewModelScope) + } - originFeeMixin.setFee(transferFeeModel.originFee) - crossChainFeeMixin.setFee(transferFeeModel.crossChainFee) - } catch (e: Exception) { - e.printStackTrace() + combine(originChain, destinationChain) { originChain, destinationChain -> + val isCrossChain = originChain.id != destinationChain.id + val mode = determineFeeSelectionMode(isCrossChain) - originFeeMixin.setFeeStatus(FeeStatus.Error) - if (hasCrossChainFee) { - crossChainFeeMixin.setFeeStatus(FeeStatus.Error) - } + feeFormatter.crossChainFeeShown = isCrossChain + feeMixin.setPaymentCurrencySelectionMode(mode) + }.launchIn(this) + } + + private fun determineFeeSelectionMode(isCrossChain: Boolean): PaymentCurrencySelectionMode { + // Enable custom fee only for on chain transfers + return if (isCrossChain) { + PaymentCurrencySelectionMode.DISABLED + } else { + PaymentCurrencySelectionMode.ENABLED } } @@ -380,10 +351,7 @@ class SelectSendViewModel( chainId = validPayload.transfer.originChain.id, chainAssetId = validPayload.transfer.originChainAsset.id ), - commission = AssetPayload( - chainId = validPayload.transfer.commissionAsset.chainId, - chainAssetId = validPayload.transfer.commissionAsset.id - ), + feePaymentCurrency = validPayload.transfer.feePaymentCurrency.toParcel(), destination = AssetPayload( chainId = validPayload.transfer.destinationChain.id, chainAssetId = validPayload.transfer.destinationChainAsset.id @@ -397,14 +365,14 @@ class SelectSendViewModel( private suspend fun buildTransfer( origin: ChainWithAsset, - commissionAsset: Asset, + feePaymentCurrency: FeePaymentCurrency, destination: ChainWithAsset, amount: BigDecimal, address: String, ): AssetTransfer { return buildAssetTransfer( metaAccount = selectedAccount.first(), - commissionAsset = commissionAsset, + feePaymentCurrency = feePaymentCurrency, origin = origin, destination = destination, amount = amount, @@ -568,10 +536,3 @@ class SelectSendViewModel( val balances: Asset? ) } - -private class FeePayload( - val originAsset: ChainWithAsset, - val destinationAsset: ChainWithAsset, - val address: String, - val amount: BigDecimal -) diff --git a/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/send/amount/di/SelectSendModule.kt b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/send/amount/di/SelectSendModule.kt index 01c16a80f5..6ee4437573 100644 --- a/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/send/amount/di/SelectSendModule.kt +++ b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/send/amount/di/SelectSendModule.kt @@ -23,7 +23,7 @@ import io.novafoundation.nova.feature_assets.presentation.send.amount.SelectSend import io.novafoundation.nova.feature_assets.presentation.send.amount.SendPayload import io.novafoundation.nova.feature_wallet_api.domain.interfaces.CrossChainTransfersUseCase 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.v2.FeeLoaderMixinV2 import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry @Module(includes = [ViewModelModule::class]) @@ -44,7 +44,7 @@ class SelectSendModule { externalActions: ExternalActions.Presentation, crossChainTransfersUseCase: CrossChainTransfersUseCase, actionAwaitableMixinFactory: ActionAwaitableMixin.Factory, - feeLoaderMixinFactory: FeeLoaderMixin.Factory, + feeLoaderMixinFactory: FeeLoaderMixinV2.Factory, selectedAccountUseCase: SelectedAccountUseCase, addressInputMixinFactory: AddressInputMixinFactory, amountChooserMixinFactory: AmountChooserMixin.Factory, 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 index 06c6fc06df..192afe7ae6 100644 --- 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 @@ -1,15 +1,15 @@ package io.novafoundation.nova.feature_assets.presentation.send.common +import io.novafoundation.nova.feature_account_api.data.fee.FeePaymentCurrency 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, + feePaymentCurrency: FeePaymentCurrency, origin: ChainWithAsset, destination: ChainWithAsset, amount: BigDecimal, @@ -23,6 +23,6 @@ fun buildAssetTransfer( destinationChain = destination.chain, destinationChainAsset = destination.asset, amount = amount, - commissionAssetToken = commissionAsset.token, + feePaymentCurrency = feePaymentCurrency ) } diff --git a/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/send/common/fee/Factory.kt b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/send/common/fee/Factory.kt new file mode 100644 index 0000000000..475327c608 --- /dev/null +++ b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/send/common/fee/Factory.kt @@ -0,0 +1,23 @@ +package io.novafoundation.nova.feature_assets.presentation.send.common.fee + +import androidx.lifecycle.viewModelScope +import io.novafoundation.nova.common.base.BaseViewModel +import io.novafoundation.nova.feature_assets.domain.send.model.TransferFee +import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.v2.FeeLoaderMixinV2 +import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain +import kotlinx.coroutines.flow.Flow + +typealias TransferFeeLoaderMixin = FeeLoaderMixinV2.Presentation + +context(BaseViewModel) +fun FeeLoaderMixinV2.Factory.createForTransfer( + originChainAsset: Flow, + formatter: TransferFeeDisplayFormatter, +): TransferFeeLoaderMixin { + return create( + scope = viewModelScope, + selectedChainAssetFlow = originChainAsset, + feeFormatter = formatter, + feeBalanceExtractor = TransferFeeBalanceExtractor(), + ) +} diff --git a/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/send/common/fee/TransferFeeBalanceExtractor.kt b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/send/common/fee/TransferFeeBalanceExtractor.kt new file mode 100644 index 0000000000..e425ed780f --- /dev/null +++ b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/send/common/fee/TransferFeeBalanceExtractor.kt @@ -0,0 +1,13 @@ +package io.novafoundation.nova.feature_assets.presentation.send.common.fee + +import io.novafoundation.nova.feature_assets.domain.send.model.TransferFee +import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.types.Balance +import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.amount.FeeBalanceExtractor +import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain + +class TransferFeeBalanceExtractor : FeeBalanceExtractor { + + override fun requiredBalanceToPayFee(fee: TransferFee, chainAsset: Chain.Asset): Balance { + return fee.totalFeeByExecutingAccount(chainAsset) + } +} diff --git a/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/send/common/fee/TransferFeeDisplay.kt b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/send/common/fee/TransferFeeDisplay.kt new file mode 100644 index 0000000000..5dfe0451b6 --- /dev/null +++ b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/send/common/fee/TransferFeeDisplay.kt @@ -0,0 +1,12 @@ +package io.novafoundation.nova.feature_assets.presentation.send.common.fee + +import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.model.FeeDisplay + +class TransferFeeDisplay( + val originFee: FeeDisplay, + val crossChainFee: FeeDisplay? +) + +class TransferFeeLoading( + val showCrossChain: Boolean +) diff --git a/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/send/common/fee/TransferFeeDisplayFormatter.kt b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/send/common/fee/TransferFeeDisplayFormatter.kt new file mode 100644 index 0000000000..3d3e893cc4 --- /dev/null +++ b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/send/common/fee/TransferFeeDisplayFormatter.kt @@ -0,0 +1,31 @@ +package io.novafoundation.nova.feature_assets.presentation.send.common.fee + +import io.novafoundation.nova.feature_account_api.data.model.FeeBase +import io.novafoundation.nova.feature_assets.domain.send.model.TransferFee +import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.formatter.DefaultFeeFormatter +import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.formatter.FeeFormatter +import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.model.FeeDisplay +import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.model.FeeStatus + +class TransferFeeDisplayFormatter( + var crossChainFeeShown: Boolean = false, + + private val componentDelegate: FeeFormatter = DefaultFeeFormatter() +) : FeeFormatter { + + + override suspend fun formatFee( + fee: TransferFee, + configuration: FeeFormatter.Configuration, + context: FeeFormatter.Context + ): TransferFeeDisplay { + return TransferFeeDisplay( + originFee = componentDelegate.formatFee(fee.originFee.totalInSubmissionAsset, configuration, context), + crossChainFee = fee.crossChainFee?.let { componentDelegate.formatFee(it, configuration, context) } + ) + } + + override suspend fun createLoadingStatus(): FeeStatus.Loading { + return FeeStatus.Loading(visibleDuringProgress = crossChainFeeShown) + } +} diff --git a/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/send/common/fee/Ui.kt b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/send/common/fee/Ui.kt new file mode 100644 index 0000000000..93e0aeb96d --- /dev/null +++ b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/send/common/fee/Ui.kt @@ -0,0 +1,29 @@ +package io.novafoundation.nova.feature_assets.presentation.send.common.fee + +import io.novafoundation.nova.common.base.BaseFragment +import io.novafoundation.nova.common.base.BaseViewModel +import io.novafoundation.nova.feature_assets.domain.send.model.TransferFee +import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.model.mapDisplay +import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.model.mapProgress +import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.v2.FeeLoaderMixinV2 +import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.v2.setupFeeLoading +import io.novafoundation.nova.feature_wallet_api.presentation.view.FeeView + +context(BaseFragment) +fun FeeLoaderMixinV2.setupFeeLoading(originFeeView: FeeView, crossChainFeeView: FeeView) { + setupFeeLoading( + setFeeStatus = { + // We only apply `visibleInProgress` for cross-chain fee. This can be handled better with generic argument for Loading payload + val originFee = it.mapDisplay(TransferFeeDisplay::originFee).mapProgress { true } + val crossChainFee = it.mapDisplay(TransferFeeDisplay::crossChainFee) + + originFeeView.setFeeStatus(originFee) + crossChainFeeView.setFeeStatus(crossChainFee) + }, + setUserCanChangeFeeAsset = { + originFeeView.setFeeEditable(it) { + changePaymentCurrencyClicked() + } + } + ) +} diff --git a/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/send/confirm/ConfirmSendFragment.kt b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/send/confirm/ConfirmSendFragment.kt index 7c404e524c..dfe0af8ed8 100644 --- a/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/send/confirm/ConfirmSendFragment.kt +++ b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/send/confirm/ConfirmSendFragment.kt @@ -17,8 +17,8 @@ import io.novafoundation.nova.feature_assets.R import io.novafoundation.nova.feature_assets.di.AssetsFeatureApi import io.novafoundation.nova.feature_assets.di.AssetsFeatureComponent import io.novafoundation.nova.feature_assets.presentation.send.TransferDraft -import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.FeeStatus -import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.setupFeeLoading +import io.novafoundation.nova.feature_assets.presentation.send.common.fee.setupFeeLoading +import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.model.FeeStatus import kotlinx.android.synthetic.main.fragment_confirm_send.confirmSendAmount import kotlinx.android.synthetic.main.fragment_confirm_send.confirmSendConfirm import kotlinx.android.synthetic.main.fragment_confirm_send.confirmSendContainer @@ -79,8 +79,9 @@ class ConfirmSendFragment : BaseFragment() { override fun subscribe(viewModel: ConfirmSendViewModel) { setupExternalActions(viewModel) observeValidations(viewModel) - setupFeeLoading(viewModel.originFeeMixin, confirmSendOriginFee) - setupFeeLoading(viewModel.crossChainFeeMixin, confirmSendCrossChainFee) + + viewModel.feeMixin.setupFeeLoading(confirmSendOriginFee, confirmSendCrossChainFee) + observeHints(viewModel.hintsMixin, confirmSendHints) viewModel.recipientModel.observe(confirmSendRecipient::showAddress) 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 ec5c896d9c..97091a26ff 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 @@ -21,13 +21,17 @@ import io.novafoundation.nova.feature_account_api.presenatation.account.icon.cre import io.novafoundation.nova.feature_account_api.presenatation.account.wallet.WalletUiUseCase import io.novafoundation.nova.feature_account_api.presenatation.actions.ExternalActions import io.novafoundation.nova.feature_account_api.presenatation.chain.ChainUi +import io.novafoundation.nova.feature_account_api.presenatation.fee.toDomain import io.novafoundation.nova.feature_assets.R import io.novafoundation.nova.feature_assets.domain.WalletInteractor import io.novafoundation.nova.feature_assets.domain.send.SendInteractor +import io.novafoundation.nova.feature_assets.domain.send.model.TransferFee 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.common.fee.TransferFeeDisplayFormatter +import io.novafoundation.nova.feature_assets.presentation.send.common.fee.createForTransfer 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 @@ -35,11 +39,8 @@ import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.t 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.OriginFee -import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.FeeLoaderMixin -import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.awaitFee -import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.awaitOptionalFee -import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.createGeneric -import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.createSimple +import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.v2.FeeLoaderMixinV2 +import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.v2.awaitFee 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.mapAmountToAmountModel @@ -73,12 +74,14 @@ class ConfirmSendViewModel( private val validationExecutor: ValidationExecutor, private val walletUiUseCase: WalletUiUseCase, private val hintsFactory: ConfirmSendHintsMixinFactory, - feeLoaderMixinFactory: FeeLoaderMixin.Factory, + feeLoaderMixinFactory: FeeLoaderMixinV2.Factory, val transferDraft: TransferDraft, ) : BaseViewModel(), ExternalActions by externalActions, Validatable by validationExecutor { + private val isCrossChain = transferDraft.origin.chainId != transferDraft.destination.chainId + private val originChain by lazyAsync { chainRegistry.getChain(transferDraft.origin.chainId) } private val originAsset by lazyAsync { chainRegistry.asset(transferDraft.origin.chainId, transferDraft.origin.chainAssetId) } @@ -89,16 +92,15 @@ class ConfirmSendViewModel( .inBackground() .share() - private val commissionAssetFlow = interactor.assetFlow(transferDraft.commission.chainId, transferDraft.commission.chainAssetId) - .inBackground() - .share() - private val currentAccount = selectedAccountUseCase.selectedMetaAccountFlow() .inBackground() .share() - val originFeeMixin = feeLoaderMixinFactory.createGeneric(commissionAssetFlow) - val crossChainFeeMixin = feeLoaderMixinFactory.createSimple(assetFlow) + private val formatter = TransferFeeDisplayFormatter(crossChainFeeShown = isCrossChain) + val feeMixin = feeLoaderMixinFactory.createForTransfer( + originChainAsset = flowOf { originAsset() }, + formatter = formatter + ) val hintsMixin = hintsFactory.create(this) @@ -179,7 +181,10 @@ class ConfirmSendViewModel( resourceManager = resourceManager, status = status, actions = actions, - feeLoaderMixin = originFeeMixin + setFee = { + val newOriginFee = payload.transferFee().replaceSubmission(it) + feeMixin.setFee(newOriginFee) + } ) }, ) { validPayload -> @@ -187,17 +192,14 @@ class ConfirmSendViewModel( } } - private fun setupFee() = launch { - launch { - val assetTransfer = buildTransfer() - - originFeeMixin.invalidateFee() - crossChainFeeMixin.invalidateFee() - - val transferFeeModel = sendInteractor.getFee(assetTransfer, viewModelScope) + private fun AssetTransferPayload.transferFee(): TransferFee { + return TransferFee(originFee, crossChainFee) + } - originFeeMixin.setFee(transferFeeModel.originFee) - crossChainFeeMixin.setFee(transferFeeModel.crossChainFee) + private fun setupFee() { + feeMixin.loadFee { + val assetTransfer = buildTransfer() + sendInteractor.getFee(assetTransfer, viewModelScope) } } @@ -209,7 +211,7 @@ class ConfirmSendViewModel( return buildAssetTransfer( metaAccount = selectedAccountUseCase.getSelectedMetaAccount(), - commissionAsset = commissionAssetFlow.first(), + feePaymentCurrency = transferDraft.feePaymentCurrency.toDomain(), origin = originChainWithAsset, destination = destinationChainWithAsset, amount = amount, @@ -260,7 +262,7 @@ class ConfirmSendViewModel( val chain = originChain() val chainAsset = originAsset() - val originFee = originFeeMixin.awaitFee() + val fee = feeMixin.awaitFee() return AssetTransferPayload( transfer = WeightedAssetTransfer( @@ -271,16 +273,17 @@ class ConfirmSendViewModel( destinationChainAsset = destinationChainAsset(), originChainAsset = chainAsset, amount = transferDraft.amount, - commissionAssetToken = commissionAssetFlow.first().token, - fee = originFee, + feePaymentCurrency = transferDraft.feePaymentCurrency.toDomain(), + fee = fee.originFee, ), - originFee = originFee, - originCommissionAsset = commissionAssetFlow.first(), + originFee = fee.originFee, + originCommissionAsset = feeMixin.feeAsset(), originUsedAsset = assetFlow.first(), - crossChainFee = crossChainFeeMixin.awaitOptionalFee() + crossChainFee = fee.crossChainFee ) } + private suspend fun createTransferDirectionModel() = if (transferDraft.isCrossChain) { ConfirmSendChainsModel( origin = mapChainToUi(originChain()), diff --git a/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/send/confirm/di/ConfirmSendModule.kt b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/send/confirm/di/ConfirmSendModule.kt index ad12af7255..9d3b4f45e6 100644 --- a/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/send/confirm/di/ConfirmSendModule.kt +++ b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/send/confirm/di/ConfirmSendModule.kt @@ -23,7 +23,7 @@ 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.confirm.ConfirmSendViewModel import io.novafoundation.nova.feature_assets.presentation.send.confirm.hints.ConfirmSendHintsMixinFactory -import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.FeeLoaderMixin +import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.v2.FeeLoaderMixinV2 import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry @Module(includes = [ViewModelModule::class]) @@ -51,7 +51,7 @@ class ConfirmSendModule { externalActions: ExternalActions.Presentation, selectedAccountUseCase: SelectedAccountUseCase, addressDisplayUseCase: AddressDisplayUseCase, - feeLoaderMixinFactory: FeeLoaderMixin.Factory, + feeLoaderMixinFactory: FeeLoaderMixinV2.Factory, resourceManager: ResourceManager, transferDraft: TransferDraft, chainRegistry: ChainRegistry, diff --git a/feature-crowdloan-impl/src/main/java/io/novafoundation/nova/feature_crowdloan_impl/presentation/contribute/confirm/ConfirmContributeViewModel.kt b/feature-crowdloan-impl/src/main/java/io/novafoundation/nova/feature_crowdloan_impl/presentation/contribute/confirm/ConfirmContributeViewModel.kt index f2084b5ae3..072a8a0789 100644 --- a/feature-crowdloan-impl/src/main/java/io/novafoundation/nova/feature_crowdloan_impl/presentation/contribute/confirm/ConfirmContributeViewModel.kt +++ b/feature-crowdloan-impl/src/main/java/io/novafoundation/nova/feature_crowdloan_impl/presentation/contribute/confirm/ConfirmContributeViewModel.kt @@ -32,7 +32,7 @@ import io.novafoundation.nova.feature_currency_api.presentation.formatters.forma import io.novafoundation.nova.feature_wallet_api.data.mappers.mapAssetToAssetModel import io.novafoundation.nova.feature_wallet_api.data.mappers.mapFeeToFeeModel import io.novafoundation.nova.feature_wallet_api.domain.AssetUseCase -import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.FeeStatus +import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.model.FeeStatus import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.mapFeeFromParcel import io.novafoundation.nova.runtime.state.SingleAssetSharedState import io.novafoundation.nova.runtime.state.chain diff --git a/feature-external-sign-impl/src/main/java/io/novafoundation/nova/feature_external_sign_impl/presentation/signExtrinsic/ExternaSignViewModel.kt b/feature-external-sign-impl/src/main/java/io/novafoundation/nova/feature_external_sign_impl/presentation/signExtrinsic/ExternaSignViewModel.kt index e8311f4991..3835d1c1fb 100644 --- a/feature-external-sign-impl/src/main/java/io/novafoundation/nova/feature_external_sign_impl/presentation/signExtrinsic/ExternaSignViewModel.kt +++ b/feature-external-sign-impl/src/main/java/io/novafoundation/nova/feature_external_sign_impl/presentation/signExtrinsic/ExternaSignViewModel.kt @@ -180,7 +180,7 @@ class ExternaSignViewModel( handleFeeSpikeDetected( error = reason, resourceManager = resourceManager, - feeLoaderMixin = originFeeMixin, + setFee = originFeeMixin, actions = actions ) } diff --git a/feature-governance-impl/src/main/java/io/novafoundation/nova/feature_governance_impl/presentation/referenda/vote/setup/common/SetupVoteViewModel.kt b/feature-governance-impl/src/main/java/io/novafoundation/nova/feature_governance_impl/presentation/referenda/vote/setup/common/SetupVoteViewModel.kt index ea76c38c9a..e89527aaee 100644 --- a/feature-governance-impl/src/main/java/io/novafoundation/nova/feature_governance_impl/presentation/referenda/vote/setup/common/SetupVoteViewModel.kt +++ b/feature-governance-impl/src/main/java/io/novafoundation/nova/feature_governance_impl/presentation/referenda/vote/setup/common/SetupVoteViewModel.kt @@ -35,8 +35,6 @@ import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.awaitFee 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.runtime.multiNetwork.runtime.types.custom.vote.Conviction -import java.math.BigDecimal -import java.math.BigInteger import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.combine @@ -44,6 +42,8 @@ import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.mapNotNull import kotlinx.coroutines.launch +import java.math.BigDecimal +import java.math.BigInteger abstract class SetupVoteViewModel( private val feeLoaderMixinFactory: FeeLoaderMixin.Factory, diff --git a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/nominationPools/bondMore/confirm/NominationPoolsConfirmBondMoreViewModel.kt b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/nominationPools/bondMore/confirm/NominationPoolsConfirmBondMoreViewModel.kt index 0e3c061655..9afaa18374 100644 --- a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/nominationPools/bondMore/confirm/NominationPoolsConfirmBondMoreViewModel.kt +++ b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/nominationPools/bondMore/confirm/NominationPoolsConfirmBondMoreViewModel.kt @@ -22,7 +22,7 @@ import io.novafoundation.nova.feature_staking_impl.presentation.nominationPools. import io.novafoundation.nova.feature_wallet_api.data.mappers.mapFeeToFeeModel import io.novafoundation.nova.feature_wallet_api.domain.AssetUseCase import io.novafoundation.nova.feature_wallet_api.domain.model.planksFromAmount -import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.FeeStatus +import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.model.FeeStatus import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.mapFeeFromParcel import io.novafoundation.nova.feature_wallet_api.presentation.model.mapAmountToAmountModel import io.novafoundation.nova.runtime.state.chain diff --git a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/nominationPools/unbond/confirm/NominationPoolsConfirmUnbondViewModel.kt b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/nominationPools/unbond/confirm/NominationPoolsConfirmUnbondViewModel.kt index ab1093e3e0..b0ccf2d4bc 100644 --- a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/nominationPools/unbond/confirm/NominationPoolsConfirmUnbondViewModel.kt +++ b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/nominationPools/unbond/confirm/NominationPoolsConfirmUnbondViewModel.kt @@ -23,7 +23,7 @@ import io.novafoundation.nova.feature_wallet_api.data.mappers.mapFeeToFeeModel import io.novafoundation.nova.feature_wallet_api.domain.AssetUseCase import io.novafoundation.nova.feature_wallet_api.domain.model.amountFromPlanks import io.novafoundation.nova.feature_wallet_api.domain.model.planksFromAmount -import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.FeeStatus +import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.model.FeeStatus import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.mapFeeFromParcel import io.novafoundation.nova.feature_wallet_api.presentation.model.mapAmountToAmountModel import io.novafoundation.nova.runtime.state.chain diff --git a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/staking/bond/confirm/ConfirmBondMoreViewModel.kt b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/staking/bond/confirm/ConfirmBondMoreViewModel.kt index 785f978900..0a1442161c 100644 --- a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/staking/bond/confirm/ConfirmBondMoreViewModel.kt +++ b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/staking/bond/confirm/ConfirmBondMoreViewModel.kt @@ -23,7 +23,7 @@ import io.novafoundation.nova.feature_staking_impl.presentation.StakingRouter import io.novafoundation.nova.feature_staking_impl.presentation.staking.bond.bondMoreValidationFailure import io.novafoundation.nova.feature_wallet_api.data.mappers.mapFeeToFeeModel import io.novafoundation.nova.feature_wallet_api.domain.model.planksFromAmount -import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.FeeStatus +import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.model.FeeStatus import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.mapFeeFromParcel import io.novafoundation.nova.feature_wallet_api.presentation.model.mapAmountToAmountModel import io.novafoundation.nova.runtime.state.AnySelectedAssetOptionSharedState diff --git a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/staking/delegation/controller/confirm/ConfirmSetControllerViewModel.kt b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/staking/delegation/controller/confirm/ConfirmSetControllerViewModel.kt index cbd32e8fe4..baedfa1502 100644 --- a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/staking/delegation/controller/confirm/ConfirmSetControllerViewModel.kt +++ b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/staking/delegation/controller/confirm/ConfirmSetControllerViewModel.kt @@ -19,8 +19,8 @@ import io.novafoundation.nova.feature_staking_impl.domain.validations.delegation import io.novafoundation.nova.feature_staking_impl.presentation.StakingRouter import io.novafoundation.nova.feature_staking_impl.presentation.staking.delegation.controller.set.bondSetControllerValidationFailure import io.novafoundation.nova.feature_wallet_api.data.mappers.mapFeeToFeeModel -import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.FeeStatus import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.mapFeeFromParcel +import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.model.FeeStatus import io.novafoundation.nova.runtime.state.AnySelectedAssetOptionSharedState import io.novafoundation.nova.runtime.state.chain import kotlinx.coroutines.flow.MutableStateFlow diff --git a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/staking/rewardDestination/confirm/ConfirmRewardDestinationViewModel.kt b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/staking/rewardDestination/confirm/ConfirmRewardDestinationViewModel.kt index e2ae564892..9dc6f78279 100644 --- a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/staking/rewardDestination/confirm/ConfirmRewardDestinationViewModel.kt +++ b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/staking/rewardDestination/confirm/ConfirmRewardDestinationViewModel.kt @@ -28,7 +28,7 @@ import io.novafoundation.nova.feature_staking_impl.presentation.staking.rewardDe import io.novafoundation.nova.feature_staking_impl.presentation.staking.rewardDestination.confirm.parcel.RewardDestinationParcelModel import io.novafoundation.nova.feature_staking_impl.presentation.staking.rewardDestination.select.rewardDestinationValidationFailure import io.novafoundation.nova.feature_wallet_api.data.mappers.mapFeeToFeeModel -import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.FeeStatus +import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.model.FeeStatus import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.mapFeeFromParcel import io.novafoundation.nova.runtime.state.AnySelectedAssetOptionSharedState import io.novafoundation.nova.runtime.state.chain diff --git a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/staking/start/confirm/ConfirmMultiStakingViewModel.kt b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/staking/start/confirm/ConfirmMultiStakingViewModel.kt index 2c2fd7ead5..0e6084d365 100644 --- a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/staking/start/confirm/ConfirmMultiStakingViewModel.kt +++ b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/staking/start/confirm/ConfirmMultiStakingViewModel.kt @@ -29,7 +29,7 @@ import io.novafoundation.nova.feature_staking_impl.presentation.staking.start.co import io.novafoundation.nova.feature_staking_impl.presentation.staking.start.confirm.types.ConfirmMultiStakingTypeFactory import io.novafoundation.nova.feature_wallet_api.data.mappers.mapFeeToFeeModel import io.novafoundation.nova.feature_wallet_api.domain.ArbitraryAssetUseCase -import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.FeeStatus +import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.model.FeeStatus import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.mapFeeFromParcel import io.novafoundation.nova.feature_wallet_api.presentation.model.mapAmountToAmountModel import kotlinx.coroutines.flow.Flow diff --git a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/staking/unbond/confirm/ConfirmUnbondViewModel.kt b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/staking/unbond/confirm/ConfirmUnbondViewModel.kt index 42e01883cc..1fd6dc6516 100644 --- a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/staking/unbond/confirm/ConfirmUnbondViewModel.kt +++ b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/staking/unbond/confirm/ConfirmUnbondViewModel.kt @@ -25,7 +25,7 @@ import io.novafoundation.nova.feature_staking_impl.presentation.staking.unbond.h import io.novafoundation.nova.feature_staking_impl.presentation.staking.unbond.unbondValidationFailure import io.novafoundation.nova.feature_wallet_api.data.mappers.mapFeeToFeeModel import io.novafoundation.nova.feature_wallet_api.domain.model.planksFromAmount -import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.FeeStatus +import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.model.FeeStatus import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.mapFeeFromParcel import io.novafoundation.nova.feature_wallet_api.presentation.model.mapAmountToAmountModel import io.novafoundation.nova.runtime.state.AnySelectedAssetOptionSharedState diff --git a/feature-swap-api/src/main/java/io/novafoundation/nova/feature_swap_api/domain/model/AtomicSwapOperation.kt b/feature-swap-api/src/main/java/io/novafoundation/nova/feature_swap_api/domain/model/AtomicSwapOperation.kt index 9349c9f478..6ff904cf07 100644 --- a/feature-swap-api/src/main/java/io/novafoundation/nova/feature_swap_api/domain/model/AtomicSwapOperation.kt +++ b/feature-swap-api/src/main/java/io/novafoundation/nova/feature_swap_api/domain/model/AtomicSwapOperation.kt @@ -1,6 +1,7 @@ package io.novafoundation.nova.feature_swap_api.domain.model import io.novafoundation.nova.feature_account_api.data.fee.FeePaymentCurrency +import io.novafoundation.nova.feature_account_api.data.model.FeeBase import io.novafoundation.nova.feature_account_api.data.model.totalAmount import io.novafoundation.nova.feature_account_api.data.model.totalPlanksEnsuringAsset import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.types.Balance @@ -69,6 +70,17 @@ fun AtomicSwapOperationFee.totalFeeEnsuringSubmissionAsset(): Balance { return submissionFee.amount + postSubmissionFeesByAccount + postSubmissionFeesFromHolding } +/** + * Collects all [FeeBase] instances from fee components + */ +fun AtomicSwapOperationFee.allBasicFees(): List { + return buildList { + add(submissionFee) + postSubmissionFees.paidByAccount.onEach(::add) + postSubmissionFees.paidFromAmount.onEach(::add) + } +} + class SwapExecutionCorrection( val actualReceivedAmount: Balance ) diff --git a/feature-swap-api/src/main/java/io/novafoundation/nova/feature_swap_api/domain/model/SwapFee.kt b/feature-swap-api/src/main/java/io/novafoundation/nova/feature_swap_api/domain/model/SwapFee.kt index f5229d9958..89f1446560 100644 --- a/feature-swap-api/src/main/java/io/novafoundation/nova/feature_swap_api/domain/model/SwapFee.kt +++ b/feature-swap-api/src/main/java/io/novafoundation/nova/feature_swap_api/domain/model/SwapFee.kt @@ -13,6 +13,7 @@ import java.math.BigInteger class SwapFee( val segments: List, + /** * Fees for second and subsequent segments converted to assetIn */ @@ -30,15 +31,19 @@ class SwapFee( private val assetIn = intermediateSegmentFeesInAssetIn.asset + // Always in asset in val additionalAmountForSwap = additionalAmountForSwap() - val deductionForAssetIn: Balance = deductionFor(assetIn) - + val maxAmountDeductionForAssetIn: Balance = maxAmountDeductionFor(assetIn) - override fun deductionFor(amountAsset: Chain.Asset): Balance { + override fun maxAmountDeductionFor(amountAsset: Chain.Asset): Balance { return totalFeeAmount(amountAsset) + additionalMaxAmountDeduction } + fun allBasicFees(): List { + return segments.flatMap { it.fee.allBasicFees() } + } + private fun totalFeeAmount(amountAsset: Chain.Asset): Balance { val executingAccount = submissionFee.submissionOrigin.executingAccount @@ -55,7 +60,7 @@ class SwapFee( return SubstrateFeeBase(totalFutureFeeInAssetIn, assetIn) } - // TODO this is until multi-chain fees are ready + // TODO remove this after reworking swap validations override val submissionOrigin: SubmissionOrigin = submissionFee.submissionOrigin override val amount: BigInteger = totalFeeAmount(submissionFee.asset) override val asset: Chain.Asset = submissionFee.asset diff --git a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/di/SwapFeatureDependencies.kt b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/di/SwapFeatureDependencies.kt index 8796c831fe..2fd2cb21f1 100644 --- a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/di/SwapFeatureDependencies.kt +++ b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/di/SwapFeatureDependencies.kt @@ -38,6 +38,7 @@ import io.novafoundation.nova.feature_wallet_api.domain.interfaces.TokenReposito import io.novafoundation.nova.feature_wallet_api.domain.interfaces.WalletRepository 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.v2.FeeLoaderMixinV2 import io.novafoundation.nova.runtime.call.MultiChainRuntimeCallsApi import io.novafoundation.nova.runtime.di.REMOTE_STORAGE_SOURCE import io.novafoundation.nova.runtime.ethereum.StorageSharedRequestsBuilderFactory @@ -137,4 +138,6 @@ interface SwapFeatureDependencies { val hydrationFeeInjector: HydrationFeeInjector val defaultFeePaymentRegistry: FeePaymentProviderRegistry + + val feeLoaderMixinV2Factory: FeeLoaderMixinV2.Factory } diff --git a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/di/SwapFeatureModule.kt b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/di/SwapFeatureModule.kt index ffd38e92df..09c7d0dd8c 100644 --- a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/di/SwapFeatureModule.kt +++ b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/di/SwapFeatureModule.kt @@ -40,7 +40,6 @@ import io.novafoundation.nova.feature_swap_impl.presentation.state.RealSwapSetti import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.AssetSourceRegistry import io.novafoundation.nova.feature_wallet_api.domain.interfaces.CrossChainTransfersUseCase import io.novafoundation.nova.feature_wallet_api.domain.interfaces.TokenRepository -import io.novafoundation.nova.feature_wallet_api.domain.interfaces.WalletRepository import io.novafoundation.nova.feature_wallet_api.domain.updater.AccountInfoUpdaterFactory import io.novafoundation.nova.runtime.ethereum.StorageSharedRequestsBuilderFactory import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry @@ -99,7 +98,7 @@ class SwapFeatureModule { swapService: SwapService, assetSourceRegistry: AssetSourceRegistry, chainRegistry: ChainRegistry, - walletRepository: WalletRepository, + tokenRepository: TokenRepository, accountRepository: AccountRepository, buyTokenRegistry: BuyTokenRegistry, crossChainTransfersUseCase: CrossChainTransfersUseCase, @@ -114,8 +113,8 @@ class SwapFeatureModule { accountRepository = accountRepository, chainRegistry = chainRegistry, swapTransactionHistoryRepository = swapTransactionHistoryRepository, - walletRepository = walletRepository, - swapUpdateSystemFactory = swapUpdateSystemFactory + swapUpdateSystemFactory = swapUpdateSystemFactory, + tokenRepository = tokenRepository ) } diff --git a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/domain/interactor/SwapInteractor.kt b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/domain/interactor/SwapInteractor.kt index 1864a93990..bfa81594b7 100644 --- a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/domain/interactor/SwapInteractor.kt +++ b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/domain/interactor/SwapInteractor.kt @@ -36,8 +36,10 @@ import io.novafoundation.nova.feature_swap_impl.domain.validation.utils.SharedQu import io.novafoundation.nova.feature_swap_impl.domain.validation.validations.sufficientNativeBalanceToPayFeeConsideringED import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.AssetSourceRegistry import io.novafoundation.nova.feature_wallet_api.domain.interfaces.CrossChainTransfersUseCase -import io.novafoundation.nova.feature_wallet_api.domain.interfaces.WalletRepository +import io.novafoundation.nova.feature_wallet_api.domain.interfaces.TokenRepository import io.novafoundation.nova.feature_wallet_api.domain.interfaces.incomingCrossChainDirectionsAvailable +import io.novafoundation.nova.feature_wallet_api.domain.model.FiatAmount +import io.novafoundation.nova.runtime.ext.fullId 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 @@ -47,6 +49,7 @@ import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.map import kotlinx.coroutines.withContext +import java.math.BigDecimal class SwapInteractor( private val swapService: SwapService, @@ -55,11 +58,30 @@ class SwapInteractor( private val assetSourceRegistry: AssetSourceRegistry, private val accountRepository: AccountRepository, private val chainRegistry: ChainRegistry, - private val walletRepository: WalletRepository, + private val tokenRepository: TokenRepository, private val swapUpdateSystemFactory: SwapUpdateSystemFactory, private val swapTransactionHistoryRepository: SwapTransactionHistoryRepository ) { + suspend fun calculateTotalFiatPrice(swapFee: SwapFee): FiatAmount { + return withContext(Dispatchers.Default) { + val basicFees = swapFee.allBasicFees() + val chainAssets = basicFees.map { it.asset } + val tokens = tokenRepository.getTokens(chainAssets) + + val totalFiat = basicFees.sumOf { basicFee -> + val token = tokens[basicFee.asset.fullId] ?: return@sumOf BigDecimal.ZERO + token.planksToFiat(basicFee.amount) + } + + FiatAmount( + currency = tokens.values.first().currency, + price = totalFiat + ) + } + + } + suspend fun sync(coroutineScope: CoroutineScope) { swapService.sync(coroutineScope) } diff --git a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/domain/validation/SufficientBalanceConsideringNonSufficientAssetsValidation.kt b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/domain/validation/SufficientBalanceConsideringNonSufficientAssetsValidation.kt index 8dea37b3da..7d255c9c0a 100644 --- a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/domain/validation/SufficientBalanceConsideringNonSufficientAssetsValidation.kt +++ b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/domain/validation/SufficientBalanceConsideringNonSufficientAssetsValidation.kt @@ -3,7 +3,6 @@ package io.novafoundation.nova.feature_swap_impl.domain.validation import io.novafoundation.nova.common.validation.ValidationStatus import io.novafoundation.nova.common.validation.valid import io.novafoundation.nova.common.validation.validOrError -import io.novafoundation.nova.feature_account_api.data.model.amountByExecutingAccount import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.AssetSourceRegistry import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.existentialDepositInPlanks import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.isSelfSufficientAsset @@ -22,7 +21,7 @@ class SufficientBalanceConsideringNonSufficientAssetsValidation( if (!isSelfSufficientAssetOut && assetIn.token.configuration.isCommissionAsset) { val existentialDeposit = assetSourceRegistry.existentialDepositInPlanks(value.detailedAssetIn.chain, assetIn.token.configuration) - val fee = value.fee.amountByExecutingAccount + val fee = value.fee.maxAmountDeductionForAssetIn return validOrError(assetIn.balanceCountedTowardsEDInPlanks - existentialDeposit >= amount + fee) { SwapValidationFailure.InsufficientBalance.BalanceNotConsiderInsufficientReceiveAsset( diff --git a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/domain/validation/SwapPayloadValidation.kt b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/domain/validation/SwapPayloadValidation.kt index dfb762c908..9d90631938 100644 --- a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/domain/validation/SwapPayloadValidation.kt +++ b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/domain/validation/SwapPayloadValidation.kt @@ -40,7 +40,7 @@ val SwapValidationPayload.swapAmountInFeeToken: Balance } val SwapValidationPayload.totalDeductedAmountInFeeToken: Balance - get() = fee.deductionFor(fee.asset) + get() = TODO() val SwapValidationPayload.maxAmountToSwap: Balance - get() = detailedAssetIn.asset.transferableInPlanks - fee.deductionForAssetIn + get() = detailedAssetIn.asset.transferableInPlanks - fee.maxAmountDeductionForAssetIn diff --git a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/domain/validation/SwapValidations.kt b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/domain/validation/SwapValidations.kt index fdb9ba4d70..24e9013a1c 100644 --- a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/domain/validation/SwapValidations.kt +++ b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/domain/validation/SwapValidations.kt @@ -69,7 +69,7 @@ fun SwapValidationSystemBuilder.enoughLiquidity(sharedQuoteValidationRetriever: fun SwapValidationSystemBuilder.sufficientBalanceInFeeAsset() = sufficientBalanceGeneric( available = { it.feeAsset.transferable }, amount = { BigDecimal.ZERO }, - fee = { it.fee }, + fee = { TODO() }, error = { SwapValidationFailure.NotEnoughFunds.ToPayFee } ) @@ -87,10 +87,10 @@ fun SwapValidationSystemBuilder.sufficientAssetOutBalanceToStayAboveED( fun SwapValidationSystemBuilder.checkForFeeChanges( swapService: SwapService ) = checkForFeeChanges( - calculateFee = { swapService.estimateFee(it.swapExecuteArgs) }, - currentFee = { it.fee }, + calculateFee = { TODO() /*swapService.estimateFee(it.swapExecuteArgs)*/ }, + currentFee = { TODO() /*it.fee*/ }, chainAsset = { it.feeAsset.token.configuration }, - error = SwapValidationFailure::FeeChangeDetected + error = TODO() ) fun SwapValidationSystemBuilder.positiveAmountIn() = positiveAmount( diff --git a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/domain/validation/validations/SwapFeeSufficientBalanceValidation.kt b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/domain/validation/validations/SwapFeeSufficientBalanceValidation.kt index 73afd98bfd..4a34523770 100644 --- a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/domain/validation/validations/SwapFeeSufficientBalanceValidation.kt +++ b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/domain/validation/validations/SwapFeeSufficientBalanceValidation.kt @@ -2,12 +2,9 @@ package io.novafoundation.nova.feature_swap_impl.domain.validation.validations import io.novafoundation.nova.common.validation.ValidationStatus import io.novafoundation.nova.common.validation.valid -import io.novafoundation.nova.common.validation.validationError import io.novafoundation.nova.feature_swap_impl.domain.validation.SwapValidation import io.novafoundation.nova.feature_swap_impl.domain.validation.SwapValidationFailure -import io.novafoundation.nova.feature_swap_impl.domain.validation.SwapValidationFailure.InsufficientBalance import io.novafoundation.nova.feature_swap_impl.domain.validation.SwapValidationPayload -import io.novafoundation.nova.feature_swap_impl.domain.validation.maxAmountToSwap import io.novafoundation.nova.feature_swap_impl.domain.validation.swapAmountInFeeToken import io.novafoundation.nova.feature_swap_impl.domain.validation.totalDeductedAmountInFeeToken @@ -17,13 +14,13 @@ class SwapFeeSufficientBalanceValidation : SwapValidation { val swapAmountInFeeToken = value.swapAmountInFeeToken val totalDeductedAmount = value.totalDeductedAmountInFeeToken - if (value.feeAsset.transferableInPlanks < swapAmountInFeeToken + totalDeductedAmount) { - val chainAssetIn = value.detailedAssetIn.asset.token.configuration - val feeAsset = value.feeAsset.token.configuration - val maxAmountToSwap = value.maxAmountToSwap - - return InsufficientBalance.CannotPayFee(chainAssetIn, feeAsset, maxAmountToSwap, value.fee).validationError() - } +// if (value.feeAsset.transferableInPlanks < swapAmountInFeeToken + totalDeductedAmount) { +// val chainAssetIn = value.detailedAssetIn.asset.token.configuration +// val feeAsset = value.feeAsset.token.configuration +// val maxAmountToSwap = value.maxAmountToSwap +// +// return InsufficientBalance.CannotPayFee(chainAssetIn, feeAsset, maxAmountToSwap, value.fee).validationError() +// } return valid() } diff --git a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/common/fee/SwapFeeBalanceExtractor.kt b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/common/fee/SwapFeeBalanceExtractor.kt new file mode 100644 index 0000000000..73be76f8ca --- /dev/null +++ b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/common/fee/SwapFeeBalanceExtractor.kt @@ -0,0 +1,13 @@ +package io.novafoundation.nova.feature_swap_impl.presentation.common.fee + +import io.novafoundation.nova.feature_swap_api.domain.model.SwapFee +import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.types.Balance +import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.amount.FeeBalanceExtractor +import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain + +class SwapFeeBalanceExtractor : FeeBalanceExtractor { + + override fun requiredBalanceToPayFee(fee: SwapFee, chainAsset: Chain.Asset): Balance { + return fee.maxAmountDeductionFor(chainAsset) + } +} diff --git a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/common/fee/SwapFeeFormatter.kt b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/common/fee/SwapFeeFormatter.kt new file mode 100644 index 0000000000..86c03f011f --- /dev/null +++ b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/common/fee/SwapFeeFormatter.kt @@ -0,0 +1,30 @@ +package io.novafoundation.nova.feature_swap_impl.presentation.common.fee + +import io.novafoundation.nova.feature_currency_api.presentation.formatters.formatAsCurrency +import io.novafoundation.nova.feature_swap_api.domain.model.SwapFee +import io.novafoundation.nova.feature_swap_impl.domain.interactor.SwapInteractor +import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.formatter.FeeFormatter +import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.model.FeeDisplay +import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.model.FeeStatus + +class SwapFeeFormatter( + private val swapInteractor: SwapInteractor, +) : FeeFormatter { + + override suspend fun formatFee( + fee: SwapFee, + configuration: FeeFormatter.Configuration, + context: FeeFormatter.Context + ): FeeDisplay { + val totalFiatFee = swapInteractor.calculateTotalFiatPrice(fee) + val formattedFiatFee = totalFiatFee.price.formatAsCurrency(totalFiatFee.currency) + return FeeDisplay( + title = formattedFiatFee, + subtitle = null + ) + } + + override suspend fun createLoadingStatus(): FeeStatus.Loading { + return FeeStatus.Loading(visibleDuringProgress = true) + } +} diff --git a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/confirmation/SwapConfirmationFragment.kt b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/confirmation/SwapConfirmationFragment.kt index 976cbc201b..5878ed10cd 100644 --- a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/confirmation/SwapConfirmationFragment.kt +++ b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/confirmation/SwapConfirmationFragment.kt @@ -18,7 +18,7 @@ import io.novafoundation.nova.feature_account_api.view.showAddress import io.novafoundation.nova.feature_swap_api.di.SwapFeatureApi import io.novafoundation.nova.feature_swap_impl.R import io.novafoundation.nova.feature_swap_impl.di.SwapFeatureComponent -import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.setupFeeLoading +import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.v2.setupFeeLoading import kotlinx.android.synthetic.main.fragment_swap_confirmation_settings.swapConfirmationAccount import kotlinx.android.synthetic.main.fragment_swap_confirmation_settings.swapConfirmationAlert import kotlinx.android.synthetic.main.fragment_swap_confirmation_settings.swapConfirmationAssets @@ -66,7 +66,8 @@ class SwapConfirmationFragment : BaseFragment() { observeValidations(viewModel) setupExternalActions(viewModel) observeDescription(viewModel) - setupFeeLoading(viewModel.feeMixin, swapConfirmationNetworkFee) + + viewModel.feeMixin.setupFeeLoading(swapConfirmationNetworkFee) viewModel.swapDetails.observe { swapConfirmationAssets.setModel(it.assets) diff --git a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/confirmation/SwapConfirmationViewModel.kt b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/confirmation/SwapConfirmationViewModel.kt index 7057f50157..72740f2328 100644 --- a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/confirmation/SwapConfirmationViewModel.kt +++ b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/confirmation/SwapConfirmationViewModel.kt @@ -39,6 +39,8 @@ import io.novafoundation.nova.feature_swap_impl.domain.interactor.SwapInteractor import io.novafoundation.nova.feature_swap_impl.presentation.SwapRouter import io.novafoundation.nova.feature_swap_impl.presentation.common.PriceImpactFormatter import io.novafoundation.nova.feature_swap_impl.presentation.common.SlippageAlertMixinFactory +import io.novafoundation.nova.feature_swap_impl.presentation.common.fee.SwapFeeBalanceExtractor +import io.novafoundation.nova.feature_swap_impl.presentation.common.fee.SwapFeeFormatter import io.novafoundation.nova.feature_swap_impl.presentation.common.state.SwapStateStoreProvider import io.novafoundation.nova.feature_swap_impl.presentation.common.state.getStateOrThrow import io.novafoundation.nova.feature_swap_impl.presentation.confirmation.model.SwapConfirmationDetailsModel @@ -49,10 +51,8 @@ import io.novafoundation.nova.feature_wallet_api.domain.interfaces.TokenReposito import io.novafoundation.nova.feature_wallet_api.domain.interfaces.WalletRepository import io.novafoundation.nova.feature_wallet_api.domain.model.Asset import io.novafoundation.nova.feature_wallet_api.presentation.mixin.amountChooser.maxAction.MaxActionProvider -import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.FeeLoaderMixin -import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.FeeStatus -import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.GenericFeeLoaderMixin.Configuration -import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.awaitFee +import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.v2.FeeLoaderMixinV2 +import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.v2.awaitFee import io.novafoundation.nova.feature_wallet_api.presentation.model.AmountModel import io.novafoundation.nova.feature_wallet_api.presentation.model.mapAmountToAmountModel import io.novafoundation.nova.feature_wallet_api.presentation.model.toAssetPayload @@ -103,7 +103,7 @@ class SwapConfirmationViewModel( private val tokenRepository: TokenRepository, private val externalActions: ExternalActions.Presentation, private val swapStateStoreProvider: SwapStateStoreProvider, - private val feeLoaderMixinFactory: FeeLoaderMixin.Factory, + private val feeLoaderMixinFactory: FeeLoaderMixinV2.Factory, private val descriptionBottomSheetLauncher: DescriptionBottomSheetLauncher, private val arbitraryAssetUseCase: ArbitraryAssetUseCase, private val maxActionProviderFactory: MaxActionProviderFactory, @@ -151,13 +151,11 @@ class SwapConfirmationViewModel( .map { it.token } .shareInBackground() - val feeMixin = feeLoaderMixinFactory.createGeneric( - tokenFlow = feeTokenFlow, - configuration = Configuration( - initialState = Configuration.InitialState( - feeStatus = FeeStatus.Loading, - ) - ) + val feeMixin = feeLoaderMixinFactory.create( + scope = viewModelScope, + selectedChainAssetFlow = initialSwapState.map { it.quote.assetIn }, + feeFormatter = SwapFeeFormatter(swapInteractor), + feeBalanceExtractor = SwapFeeBalanceExtractor(), ) private val maxActionProvider = createMaxActionProvider() @@ -372,11 +370,9 @@ class SwapConfirmationViewModel( firstSegmentFees = initialSwapState.first().fee.intermediateSegmentFeesInAssetIn.asset ) - feeMixin.loadFee( - coroutineScope = viewModelScope, - feeConstructor = { swapInteractor.estimateFee(executeArgs) }, - onRetryCancelled = { } - ) + feeMixin.loadFee { + swapInteractor.estimateFee(executeArgs) + } confirmationStateFlow.value = confirmationState.copy(swapQuoteArgs = newSwapQuoteArgs, swapQuote = swapQuote) } diff --git a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/confirmation/di/SwapConfirmationModule.kt b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/confirmation/di/SwapConfirmationModule.kt index 4a4a53ed0d..abbd2a8e9a 100644 --- a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/confirmation/di/SwapConfirmationModule.kt +++ b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/confirmation/di/SwapConfirmationModule.kt @@ -26,7 +26,7 @@ import io.novafoundation.nova.feature_swap_impl.presentation.mixin.maxAction.Max import io.novafoundation.nova.feature_wallet_api.domain.ArbitraryAssetUseCase import io.novafoundation.nova.feature_wallet_api.domain.interfaces.TokenRepository import io.novafoundation.nova.feature_wallet_api.domain.interfaces.WalletRepository -import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.FeeLoaderMixin +import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.v2.FeeLoaderMixinV2 import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry @Module(includes = [ViewModelModule::class]) @@ -50,7 +50,7 @@ class SwapConfirmationModule { validationExecutor: ValidationExecutor, tokenRepository: TokenRepository, externalActions: ExternalActions.Presentation, - feeLoaderMixinFactory: FeeLoaderMixin.Factory, + feeLoaderMixinFactory: FeeLoaderMixinV2.Factory, descriptionBottomSheetLauncher: DescriptionBottomSheetLauncher, assetUseCase: ArbitraryAssetUseCase, maxActionProviderFactory: MaxActionProviderFactory, diff --git a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/main/SwapMainSettingsFragment.kt b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/main/SwapMainSettingsFragment.kt index 609837884d..d11392ea5c 100644 --- a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/main/SwapMainSettingsFragment.kt +++ b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/main/SwapMainSettingsFragment.kt @@ -17,15 +17,13 @@ import io.novafoundation.nova.common.view.setState import io.novafoundation.nova.common.view.showLoadingValue import io.novafoundation.nova.feature_buy_api.presentation.mixin.BuyMixinUi import io.novafoundation.nova.feature_swap_api.di.SwapFeatureApi -import io.novafoundation.nova.feature_swap_core_api.data.primitive.model.SwapDirection import io.novafoundation.nova.feature_swap_api.presentation.model.SwapSettingsPayload +import io.novafoundation.nova.feature_swap_core_api.data.primitive.model.SwapDirection import io.novafoundation.nova.feature_swap_impl.R import io.novafoundation.nova.feature_swap_impl.di.SwapFeatureComponent import io.novafoundation.nova.feature_swap_impl.presentation.main.input.setupSwapAmountInput -import io.novafoundation.nova.feature_account_api.presenatation.fee.select.FeeAssetSelectorBottomSheet import io.novafoundation.nova.feature_swap_impl.presentation.main.view.GetAssetInBottomSheet -import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.setupFeeLoading -import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.setupSelectableFeeToken +import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.v2.setupFeeLoading import kotlinx.android.synthetic.main.fragment_main_swap_settings.swapMainSettingsContinue import kotlinx.android.synthetic.main.fragment_main_swap_settings.swapMainSettingsDetails import kotlinx.android.synthetic.main.fragment_main_swap_settings.swapMainSettingsDetailsNetworkFee @@ -96,10 +94,8 @@ class SwapMainSettingsFragment : BaseFragment() { observeValidations(viewModel) setupSwapAmountInput(viewModel.amountInInput, swapMainSettingsPayInput, swapMainSettingsMaxAmount) setupSwapAmountInput(viewModel.amountOutInput, swapMainSettingsReceiveInput, maxAvailableView = null) - setupFeeLoading(viewModel.feeMixin, swapMainSettingsDetailsNetworkFee) - setupSelectableFeeToken(viewModel.canChangeFeeToken, swapMainSettingsDetailsNetworkFee) { - viewModel.editFeeTokenClicked() - } + + viewModel.feeMixin.setupFeeLoading(swapMainSettingsDetailsNetworkFee) buyMixinUi.setupBuyIntegration(this, viewModel.buyMixin) @@ -119,25 +115,6 @@ class SwapMainSettingsFragment : BaseFragment() { } } - viewModel.canChangeFeeToken.observe { canChangeFeeToken -> - if (canChangeFeeToken) { - swapMainSettingsDetailsNetworkFee.setPrimaryValueStartIcon(R.drawable.ic_pencil_edit, R.color.icon_secondary) - swapMainSettingsDetailsNetworkFee.setOnValueClickListener { viewModel.editFeeTokenClicked() } - } else { - swapMainSettingsDetailsNetworkFee.setPrimaryValueStartIcon(null) - swapMainSettingsDetailsNetworkFee.setOnValueClickListener(null) - } - } - - viewModel.changeFeeTokenEvent.awaitableActionLiveData.observeEvent { - FeeAssetSelectorBottomSheet( - context = requireContext(), - payload = it.payload, - onOptionClicked = it.onSuccess, - onCancel = it.onCancel - ).show() - } - viewModel.validationProgress.observe(swapMainSettingsContinue::setProgressState) viewModel.getAssetInOptionsButtonState.observe(swapMainSettingsGetAssetIn::setState) diff --git a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/main/SwapMainSettingsViewModel.kt b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/main/SwapMainSettingsViewModel.kt index 8dc0f87396..4d82c939eb 100644 --- a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/main/SwapMainSettingsViewModel.kt +++ b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/main/SwapMainSettingsViewModel.kt @@ -12,7 +12,6 @@ import io.novafoundation.nova.common.resources.ResourceManager import io.novafoundation.nova.common.utils.Event import io.novafoundation.nova.common.utils.LOG_TAG import io.novafoundation.nova.common.utils.accumulate -import io.novafoundation.nova.common.utils.combineToPair import io.novafoundation.nova.common.utils.event import io.novafoundation.nova.common.utils.flowOfAll import io.novafoundation.nova.common.utils.formatting.CompoundNumberFormatter @@ -32,10 +31,8 @@ import io.novafoundation.nova.common.validation.FieldValidator import io.novafoundation.nova.common.validation.ValidationExecutor import io.novafoundation.nova.common.view.bottomSheet.description.DescriptionBottomSheetLauncher import io.novafoundation.nova.common.view.bottomSheet.description.launchNetworkFeeDescription -import io.novafoundation.nova.feature_account_api.data.model.decimalAmount import io.novafoundation.nova.feature_account_api.domain.interfaces.SelectedAccountUseCase import io.novafoundation.nova.feature_account_api.domain.model.addressIn -import io.novafoundation.nova.feature_account_api.presenatation.fee.select.FeeAssetSelectorBottomSheet import io.novafoundation.nova.feature_buy_api.presentation.mixin.BuyMixin import io.novafoundation.nova.feature_swap_api.domain.model.SwapFee import io.novafoundation.nova.feature_swap_api.domain.model.SwapQuote @@ -53,6 +50,8 @@ import io.novafoundation.nova.feature_swap_impl.R import io.novafoundation.nova.feature_swap_impl.domain.interactor.SwapInteractor import io.novafoundation.nova.feature_swap_impl.domain.model.GetAssetInOption import io.novafoundation.nova.feature_swap_impl.presentation.SwapRouter +import io.novafoundation.nova.feature_swap_impl.presentation.common.fee.SwapFeeBalanceExtractor +import io.novafoundation.nova.feature_swap_impl.presentation.common.fee.SwapFeeFormatter import io.novafoundation.nova.feature_swap_impl.presentation.common.state.SwapState import io.novafoundation.nova.feature_swap_impl.presentation.common.state.SwapStateStoreProvider import io.novafoundation.nova.feature_swap_impl.presentation.fieldValidation.EnoughAmountToSwapValidatorFactory @@ -76,22 +75,19 @@ import io.novafoundation.nova.feature_wallet_api.presentation.mixin.amountChoose import io.novafoundation.nova.feature_wallet_api.presentation.mixin.amountChooser.invokeMaxClick import io.novafoundation.nova.feature_wallet_api.presentation.mixin.amountChooser.maxAction.MaxActionProvider import io.novafoundation.nova.feature_wallet_api.presentation.mixin.amountChooser.setAmount -import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.FeeLoaderMixin -import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.FeeStatus -import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.GenericFeeLoaderMixin -import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.GenericFeeLoaderMixin.Configuration -import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.awaitFee -import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.loadedFeeModelOrNullFlow +import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.model.FeeDisplay +import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.model.FeeStatus +import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.v2.FeeLoaderMixinV2 +import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.v2.FeeLoaderMixinV2.Configuration +import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.v2.awaitFee import io.novafoundation.nova.feature_wallet_api.presentation.model.AssetPayload import io.novafoundation.nova.feature_wallet_api.presentation.model.fullChainAssetId -import io.novafoundation.nova.runtime.ext.commissionAsset import io.novafoundation.nova.runtime.ext.fullId import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry import io.novafoundation.nova.runtime.multiNetwork.asset import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain import kotlinx.coroutines.CancellationException import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.FlowPreview import kotlinx.coroutines.async import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow @@ -105,7 +101,6 @@ import kotlinx.coroutines.flow.filterIsInstance import kotlinx.coroutines.flow.filterNotNull import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.firstOrNull -import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.mapLatest @@ -138,7 +133,7 @@ class SwapMainSettingsViewModel( private val maxActionProviderFactory: MaxActionProviderFactory, private val swapStateStoreProvider: SwapStateStoreProvider, swapAmountInputMixinFactory: SwapAmountInputMixinFactory, - feeLoaderMixinFactory: FeeLoaderMixin.Factory, + feeLoaderMixinFactory: FeeLoaderMixinV2.Factory, actionAwaitableFactory: ActionAwaitableMixin.Factory, ) : BaseViewModel(), DescriptionBottomSheetLauncher by descriptionBottomSheetLauncher, @@ -160,7 +155,6 @@ class SwapMainSettingsViewModel( private val assetOutFlow = swapSettings.assetFlowOf(SwapSettings::assetOut) private val assetInFlow = swapSettings.assetFlowOf(SwapSettings::assetIn) - private val feeAssetFlow = swapSettings.assetFlowOf(SwapSettings::feeAsset) private val priceImpact = quotingState.map { quoteState -> when (quoteState) { @@ -175,13 +169,12 @@ class SwapMainSettingsViewModel( .map { chainRegistry.getChain(it) } .shareInBackground() - private val nativeAssetFlow = originChainFlow - .flatMapLatest { assetUseCase.assetFlow(it.commissionAsset) } - .shareInBackground() - - val feeMixin = feeLoaderMixinFactory.createGeneric( - tokenFlow = feeAssetFlow.map { it?.token }, - configuration = GenericFeeLoaderMixin.Configuration( + val feeMixin = feeLoaderMixinFactory.create( + scope = viewModelScope, + selectedChainAssetFlow = swapSettings.mapNotNull { it.assetIn }, + feeFormatter = SwapFeeFormatter(swapInteractor), + feeBalanceExtractor = SwapFeeBalanceExtractor(), + configuration = Configuration( initialState = Configuration.InitialState( feeStatus = FeeStatus.NoFee, ) @@ -242,13 +235,6 @@ class SwapMainSettingsViewModel( val swapDirectionFlipped: MutableLiveData> = MutableLiveData() - val canChangeFeeToken = chainAssetIn - .map(::isEditFeeTokenAvailable) - .shareInBackground() - - val changeFeeTokenEvent = actionAwaitableFactory.create() - private val feeTokenOnceChangedManually = MutableStateFlow(false) - private val getAssetInOptions = swapInteractor.availableGetAssetInOptionsFlow(chainAssetIn) .shareInBackground() @@ -304,8 +290,6 @@ class SwapMainSettingsViewModel( feeMixin.setupFees() - setCustomFeeAssetIfNotEnoughNative() - launch { swapInteractor.sync(viewModelScope) } @@ -472,59 +456,18 @@ class SwapMainSettingsViewModel( } } - fun editFeeTokenClicked() = launch { - val swapSettings = swapSettings.first() - val originChain = originChainFlow.first() - - val payload = FeeAssetSelectorBottomSheet.Payload( - options = listOf( - originChain.commissionAsset, - swapSettings.assetIn ?: return@launch, - ), - selectedOption = swapSettings.feeAsset ?: return@launch - ) - val newFeeToken = changeFeeTokenEvent.awaitAction(payload) - feeTokenOnceChangedManually.value = true - - swapSettingState().setFeeAsset(newFeeToken) - } - - private fun setCustomFeeAssetIfNotEnoughNative() { - combineToPair(nativeAssetFlow, feeMixin.loadedFeeModelOrNullFlow()) - .filter { (nativeAsset, feeModel) -> - val canChangeAutomatically = !feeTokenOnceChangedManually.value - val canAcceptAssetInAsPayment = canChangeFeeToken.first() - - if (!canAcceptAssetInAsPayment) return@filter false - if (!canChangeAutomatically) return@filter false - if (nativeAsset.transferable.isZero) return@filter true - if (feeModel == null) return@filter false - - val isFeePaidInNativeAsset = nativeAsset.token.configuration.fullId == feeModel.fee.asset.fullId - val notEnoughNativeToPayFee = nativeAsset.transferable < feeModel.fee.decimalAmount - - isFeePaidInNativeAsset && notEnoughNativeToPayFee - }.onEach { - val assetIn = swapSettings.first().assetIn ?: return@onEach - - swapSettingState().setFeeAsset(assetIn) - } - .launchIn(this) - } - private fun setupUpdateSystem() = launch { swapInteractor.getUpdateSystem(originChainFlow, viewModelScope) .start() .launchIn(viewModelScope) } - @OptIn(FlowPreview::class) - private fun GenericFeeLoaderMixin.Presentation.setupFees() { + private fun FeeLoaderMixinV2.Presentation.setupFees() { quotingState .onEach { when (it) { - is QuotingState.Loading -> invalidateFee() - is QuotingState.NotAvailable -> setFee(null) + is QuotingState.Loading -> setFeeLoading() + is QuotingState.NotAvailable -> setFeeStatus(FeeStatus.NoFee) else -> {} } } @@ -533,21 +476,17 @@ class SwapMainSettingsViewModel( .zipWithPrevious() .mapNotNull { (previous, current) -> current.takeIf { - // allow same value in case user quickly switcher from this value to another and back without waiting for fee loading - previous != current || feeMixin.feeLiveData.value !is FeeStatus.Loaded + // allow same value in case user quickly switched from this value to another and back without waiting for fee loading + previous != current || feeMixin.fee.value !is FeeStatus.Loaded } } - .mapLatest { quoteState -> + .onEach { quoteState -> val swapArgs = quoteState.value.toExecuteArgs( slippage = swapSettings.first().slippage, firstSegmentFees = quoteState.firstSegmentFeeAsset ) - loadFeeSuspending( - feeConstructor = { swapInteractor.estimateFee(swapArgs) }, - retryScope = coroutineScope, - onRetryCancelled = {} - ) + loadFee { swapInteractor.estimateFee(swapArgs) } } .inBackground() .launchIn(viewModelScope) @@ -574,10 +513,6 @@ class SwapMainSettingsViewModel( swapDirectionFlipped.value = newSettings.swapDirection!!.event() } - private suspend fun isEditFeeTokenAvailable(assetIn: Chain.Asset?): Boolean { - return assetIn != null && swapInteractor.canPayFeeInCustomAsset(assetIn) - } - private fun formatRate(swapQuote: SwapQuote): String { return swapRateFormatter.format(swapQuote.swapRate(), swapQuote.assetIn, swapQuote.assetOut) } diff --git a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/main/di/SwapMainSettingsModule.kt b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/main/di/SwapMainSettingsModule.kt index d3c676f724..1da2d31a00 100644 --- a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/main/di/SwapMainSettingsModule.kt +++ b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/main/di/SwapMainSettingsModule.kt @@ -31,7 +31,7 @@ import io.novafoundation.nova.feature_swap_impl.presentation.main.input.SwapInpu import io.novafoundation.nova.feature_swap_impl.presentation.mixin.maxAction.MaxActionProviderFactory import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.AssetSourceRegistry import io.novafoundation.nova.feature_wallet_api.domain.ArbitraryAssetUseCase -import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.FeeLoaderMixin +import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.v2.FeeLoaderMixinV2 import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry @Module(includes = [ViewModelModule::class]) @@ -83,7 +83,7 @@ class SwapMainSettingsModule { swapAmountInputMixinFactory: SwapAmountInputMixinFactory, chainRegistry: ChainRegistry, assetUseCase: ArbitraryAssetUseCase, - feeLoaderMixinFactory: FeeLoaderMixin.Factory, + feeLoaderMixinFactory: FeeLoaderMixinV2.Factory, actionAwaitableMixinFactory: ActionAwaitableMixin.Factory, liquidityFieldValidatorFactory: LiquidityFieldValidatorFactory, enoughAmountToSwapValidatorFactory: EnoughAmountToSwapValidatorFactory, diff --git a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/mixin/maxAction/MaxActionProviderFactory.kt b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/mixin/maxAction/MaxActionProviderFactory.kt index 92a4cedde9..67ad54d290 100644 --- a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/mixin/maxAction/MaxActionProviderFactory.kt +++ b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/mixin/maxAction/MaxActionProviderFactory.kt @@ -1,6 +1,5 @@ package io.novafoundation.nova.feature_swap_impl.presentation.mixin.maxAction -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.types.Balance import io.novafoundation.nova.feature_wallet_api.domain.model.Asset @@ -8,7 +7,7 @@ import io.novafoundation.nova.feature_wallet_api.presentation.mixin.amountChoose import io.novafoundation.nova.feature_wallet_api.presentation.mixin.amountChooser.maxAction.MaxActionProviderDsl.deductFee import io.novafoundation.nova.feature_wallet_api.presentation.mixin.amountChooser.maxAction.MaxActionProviderDsl.providingMaxOf import io.novafoundation.nova.feature_wallet_api.presentation.mixin.amountChooser.maxAction.MaxAvailableDeduction -import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.GenericFeeLoaderMixin +import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.v2.FeeLoaderMixinV2 import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry import kotlinx.coroutines.flow.Flow @@ -17,13 +16,13 @@ class MaxActionProviderFactory( private val chainRegistry: ChainRegistry, ) { - fun create( + fun create( assetInFlow: Flow, assetOutFlow: Flow, field: (Asset) -> Balance, - feeLoaderMixin: GenericFeeLoaderMixin, + feeLoaderMixin: FeeLoaderMixinV2, allowMaxAction: Boolean = true - ): MaxActionProvider where F : Fee, F : MaxAvailableDeduction { + ): MaxActionProvider { return assetInFlow.providingMaxOf(field, allowMaxAction) .deductFee(feeLoaderMixin) .disallowReapingIfHasDependents(assetOutFlow, assetSourceRegistry, chainRegistry) diff --git a/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/data/mappers/Fee.kt b/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/data/mappers/Fee.kt index f9d2c3f5c8..a521d2c65a 100644 --- a/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/data/mappers/Fee.kt +++ b/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/data/mappers/Fee.kt @@ -2,19 +2,23 @@ package io.novafoundation.nova.feature_wallet_api.data.mappers import io.novafoundation.nova.feature_account_api.data.model.FeeBase import io.novafoundation.nova.feature_wallet_api.domain.model.Token -import io.novafoundation.nova.feature_wallet_api.presentation.model.FeeModel +import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.model.FeeDisplay +import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.model.FeeModel +import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.model.toFeeDisplay import io.novafoundation.nova.feature_wallet_api.presentation.model.mapAmountToAmountModel +@Suppress("DeprecatedCallableAddReplaceWith") +@Deprecated("This is a internal logic related to fee mixin. To access or set the fee use corresponding methods from FeeLoaderMixinV2.Presentation") fun mapFeeToFeeModel( fee: F, token: Token, includeZeroFiat: Boolean = true -): FeeModel = FeeModel( +): FeeModel = FeeModel( display = mapAmountToAmountModel( amountInPlanks = fee.amount, token = token, includeZeroFiat = includeZeroFiat - ), + ).toFeeDisplay(), fee = fee ) 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 20b505c182..9fea935b6a 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 @@ -5,6 +5,7 @@ import io.novafoundation.nova.common.validation.ValidationSystem import io.novafoundation.nova.common.validation.ValidationSystemBuilder import io.novafoundation.nova.feature_account_api.data.model.Fee import io.novafoundation.nova.feature_account_api.data.model.FeeBase +import io.novafoundation.nova.feature_account_api.data.model.SubmissionFee import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.types.Balance import io.novafoundation.nova.feature_wallet_api.domain.model.Asset import io.novafoundation.nova.feature_wallet_api.domain.model.OriginFee @@ -71,8 +72,8 @@ 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() } 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 fbc5c73c69..afaa93b119 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 @@ -2,13 +2,10 @@ 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.fee.FeePaymentCurrency -import io.novafoundation.nova.feature_account_api.data.fee.toFeePaymentCurrency 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.data.network.blockhain.types.Balance import io.novafoundation.nova.feature_wallet_api.domain.model.OriginFee -import io.novafoundation.nova.feature_wallet_api.domain.model.Token import io.novafoundation.nova.feature_wallet_api.domain.model.planksFromAmount import io.novafoundation.nova.runtime.ext.accountIdOf import io.novafoundation.nova.runtime.ext.accountIdOrNull @@ -47,15 +44,10 @@ interface AssetTransfer : AssetTransferBase { val sender: MetaAccount - val commissionAssetToken: Token - val amount: BigDecimal override val amountPlanks: Balance get() = originChainAsset.planksFromAmount(amount) - - override val feePaymentCurrency: FeePaymentCurrency - get() = commissionAssetToken.configuration.toFeePaymentCurrency() } fun AssetTransferBase( @@ -85,7 +77,7 @@ class BaseAssetTransfer( override val originChainAsset: Chain.Asset, override val destinationChain: Chain, override val destinationChainAsset: Chain.Asset, - override val commissionAssetToken: Token, + override val feePaymentCurrency: FeePaymentCurrency, override val amount: BigDecimal, ) : AssetTransfer @@ -96,7 +88,7 @@ data class WeightedAssetTransfer( override val originChainAsset: Chain.Asset, override val destinationChain: Chain, override val destinationChainAsset: Chain.Asset, - override val commissionAssetToken: Token, + override val feePaymentCurrency: FeePaymentCurrency, override val amount: BigDecimal, val fee: OriginFee, ) : AssetTransfer { @@ -108,7 +100,7 @@ data class WeightedAssetTransfer( originChainAsset = assetTransfer.originChainAsset, destinationChain = assetTransfer.destinationChain, destinationChainAsset = assetTransfer.destinationChainAsset, - commissionAssetToken = assetTransfer.commissionAssetToken, + feePaymentCurrency = assetTransfer.feePaymentCurrency, amount = assetTransfer.amount, fee = fee ) @@ -121,13 +113,6 @@ fun AssetTransfer.recipientOrNull(): AccountId? { return destinationChain.accountIdOrNull(recipient) } -fun AssetTransfer.senderAccountId(): AccountId { - return sender.requireAccountIdIn(originChain) -} - -val AssetTransfer.commissionAsset - get() = commissionAssetToken.configuration - interface AssetTransfers { fun getValidationSystem(coroutineScope: CoroutineScope): AssetTransfersValidationSystem diff --git a/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/di/WalletFeatureApi.kt b/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/di/WalletFeatureApi.kt index 5652203adb..68438bdc32 100644 --- a/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/di/WalletFeatureApi.kt +++ b/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/di/WalletFeatureApi.kt @@ -24,6 +24,7 @@ import io.novafoundation.nova.feature_wallet_api.domain.validation.PhishingValid import io.novafoundation.nova.feature_wallet_api.domain.validation.ProxyHaveEnoughFeeValidationFactory 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.v2.FeeLoaderMixinV2 import io.novafoundation.nova.runtime.ethereum.contract.erc20.Erc20Standard interface WalletFeatureApi { @@ -75,4 +76,6 @@ interface WalletFeatureApi { val arbitraryTokenUseCase: ArbitraryTokenUseCase val holdsRepository: BalanceHoldsRepository + + val feeLoaderMixinV2Factory: FeeLoaderMixinV2.Factory } diff --git a/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/domain/interfaces/WalletRepository.kt b/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/domain/interfaces/WalletRepository.kt index 08048862e8..67cf3e5104 100644 --- a/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/domain/interfaces/WalletRepository.kt +++ b/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/domain/interfaces/WalletRepository.kt @@ -1,5 +1,6 @@ package io.novafoundation.nova.feature_wallet_api.domain.interfaces +import io.novafoundation.nova.feature_account_api.data.model.SubmissionFee import io.novafoundation.nova.feature_currency_api.domain.model.Currency import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.tranfers.AssetTransfer import io.novafoundation.nova.feature_wallet_api.domain.model.Asset @@ -8,7 +9,6 @@ import io.novafoundation.nova.runtime.multiNetwork.chain.model.ChainId import io.novafoundation.nova.runtime.multiNetwork.chain.model.FullChainAssetId import io.novasama.substrate_sdk_android.runtime.AccountId import kotlinx.coroutines.flow.Flow -import java.math.BigDecimal import java.math.BigInteger interface WalletRepository { @@ -51,7 +51,7 @@ interface WalletRepository { suspend fun insertPendingTransfer( hash: String, assetTransfer: AssetTransfer, - fee: BigDecimal + fee: SubmissionFee ) suspend fun clearAssets(assetIds: List) diff --git a/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/domain/model/FiatAmount.kt b/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/domain/model/FiatAmount.kt new file mode 100644 index 0000000000..708d7cd70a --- /dev/null +++ b/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/domain/model/FiatAmount.kt @@ -0,0 +1,9 @@ +package io.novafoundation.nova.feature_wallet_api.domain.model + +import io.novafoundation.nova.feature_currency_api.domain.model.Currency +import java.math.BigDecimal + +class FiatAmount( + val currency: Currency, + val price: BigDecimal +) 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 index 99ed7f5506..d89ac6aba7 100644 --- 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 @@ -1,22 +1,28 @@ 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.runtime.multiNetwork.chain.model.Chain -import java.math.BigInteger +import io.novafoundation.nova.feature_account_api.data.model.FeeBase +import io.novafoundation.nova.feature_account_api.data.model.SubmissionFee +import io.novafoundation.nova.feature_account_api.data.model.SubstrateFeeBase +import io.novafoundation.nova.feature_account_api.data.model.getAmount data class OriginFee( - val submissionFee: Fee, - val deliveryFee: Fee?, - val chainAsset: Chain.Asset -) : Fee { + val submissionFee: SubmissionFee, + val deliveryFee: SubmissionFee?, +) { - override val amount: BigInteger = submissionFee.amount + deliveryFee?.amount.orZero() + val totalInSubmissionAsset: FeeBase = createTotalFeeInSubmissionAsset() - override val submissionOrigin: SubmissionOrigin = submissionFee.submissionOrigin + fun replaceSubmissionFee(submissionFee: SubmissionFee): OriginFee { + return copy(submissionFee = submissionFee) + } - override val asset = submissionFee.asset + private fun createTotalFeeInSubmissionAsset(): FeeBase { + val submissionAsset = submissionFee.asset + val totalAmount = submissionFee.amount + deliveryFee?.getAmount(submissionAsset).orZero() + return SubstrateFeeBase(totalAmount, submissionAsset) + } } fun OriginFee.intoFeeList(): List { diff --git a/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/domain/model/Token.kt b/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/domain/model/Token.kt index f9d7085e5b..9a53b5661e 100644 --- a/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/domain/model/Token.kt +++ b/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/domain/model/Token.kt @@ -32,6 +32,13 @@ data class Token( fun BigInteger.toAmount() = amountFromPlanks(this) } +fun Token.fiatAmountOf(planks: Balance) : FiatAmount { + return FiatAmount( + currency = currency, + price = planksToFiat(planks) + ) +} + data class HistoricalToken( override val currency: Currency, override val coinRate: HistoricalCoinRate?, diff --git a/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/domain/validation/FeeChangeValidation.kt b/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/domain/validation/FeeChangeValidation.kt index e58cf21a65..85b2d379ce 100644 --- a/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/domain/validation/FeeChangeValidation.kt +++ b/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/domain/validation/FeeChangeValidation.kt @@ -16,7 +16,7 @@ import io.novafoundation.nova.feature_account_api.data.model.SubmissionFee import io.novafoundation.nova.feature_account_api.data.model.decimalAmountByExecutingAccount import io.novafoundation.nova.feature_wallet_api.R import io.novafoundation.nova.feature_wallet_api.presentation.formatters.formatTokenAmount -import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.GenericFeeLoaderMixin +import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.SetFee import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch @@ -82,13 +82,13 @@ fun ValidationSystemBuilder.checkForFeeChanges( fun CoroutineScope.handleFeeSpikeDetected( error: FeeChangeDetectedFailure, resourceManager: ResourceManager, - feeLoaderMixin: GenericFeeLoaderMixin.Presentation, + setFee: SetFee, actions: ValidationFlowActions<*> ): TransformedFailure? = handleFeeSpikeDetected( error = error, resourceManager = resourceManager, actions = actions, - setFee = { feeLoaderMixin.setFee(it.newFee) } + setFee = { setFee.setFee(it.newFee) } ) fun CoroutineScope.handleFeeSpikeDetected( diff --git a/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/presentation/mixin/amountChooser/maxAction/FeeAwareMaxActionProvider.kt b/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/presentation/mixin/amountChooser/maxAction/FeeAwareMaxActionProvider.kt index f9349324f0..e3efba8df9 100644 --- a/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/presentation/mixin/amountChooser/maxAction/FeeAwareMaxActionProvider.kt +++ b/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/presentation/mixin/amountChooser/maxAction/FeeAwareMaxActionProvider.kt @@ -1,30 +1,28 @@ package io.novafoundation.nova.feature_wallet_api.presentation.mixin.amountChooser.maxAction -import androidx.lifecycle.asFlow -import io.novafoundation.nova.feature_account_api.data.model.Fee import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.types.Balance -import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.FeeStatus -import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.GenericFeeLoaderMixin +import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.model.FeeStatus +import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.v2.FeeLoaderMixinV2 import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.combine interface MaxAvailableDeduction { - fun deductionFor(amountAsset: Chain.Asset): Balance + fun maxAmountDeductionFor(amountAsset: Chain.Asset): Balance } -class ComplexFeeAwareMaxActionProvider( - feeInputMixin: GenericFeeLoaderMixin, +class ComplexFeeAwareMaxActionProvider( + feeInputMixin: FeeLoaderMixinV2, inner: MaxActionProvider, -) : MaxActionProvider where F : Fee, F : MaxAvailableDeduction { +) : MaxActionProvider { // Fee is not deducted for display override val maxAvailableForDisplay: Flow = inner.maxAvailableForDisplay override val maxAvailableForAction: Flow = combine( inner.maxAvailableForAction, - feeInputMixin.feeLiveData.asFlow() + feeInputMixin.fee ) { maxAvailable, newFeeStatus -> if (maxAvailable == null) return@combine null @@ -34,12 +32,12 @@ class ComplexFeeAwareMaxActionProvider( is FeeStatus.Loaded -> { val amountAsset = maxAvailable.chainAsset - val deduction = newFeeStatus.feeModel.fee.deductionFor(amountAsset) + val deduction = newFeeStatus.feeModel.fee.maxAmountDeductionFor(amountAsset) maxAvailable - deduction } - FeeStatus.Loading -> null + is FeeStatus.Loading -> null } } } diff --git a/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/presentation/mixin/amountChooser/maxAction/MaxActionProvider.kt b/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/presentation/mixin/amountChooser/maxAction/MaxActionProvider.kt index 948adae7e2..eba02ae4d7 100644 --- a/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/presentation/mixin/amountChooser/maxAction/MaxActionProvider.kt +++ b/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/presentation/mixin/amountChooser/maxAction/MaxActionProvider.kt @@ -1,10 +1,9 @@ package io.novafoundation.nova.feature_wallet_api.presentation.mixin.amountChooser.maxAction import io.novafoundation.nova.common.utils.atLeastZero -import io.novafoundation.nova.feature_account_api.data.model.Fee import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.types.Balance import io.novafoundation.nova.feature_wallet_api.domain.model.Asset -import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.GenericFeeLoaderMixin +import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.v2.FeeLoaderMixinV2 import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain import kotlinx.coroutines.flow.Flow import java.math.BigInteger @@ -24,9 +23,9 @@ object MaxActionProviderDsl { return AssetMaxActionProvider(this, field, allowMaxAction) } - fun MaxActionProvider.deductFee( - feeLoaderMixin: GenericFeeLoaderMixin, - ): MaxActionProvider where F : Fee, F : MaxAvailableDeduction { + fun MaxActionProvider.deductFee( + feeLoaderMixin: FeeLoaderMixinV2, + ): MaxActionProvider { return ComplexFeeAwareMaxActionProvider(feeLoaderMixin, inner = this) } } 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 05284b0bdc..b5298774f9 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 @@ -3,7 +3,6 @@ package io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee import androidx.lifecycle.LiveData import androidx.lifecycle.asFlow import io.novafoundation.nova.common.mixin.api.Retriable -import io.novafoundation.nova.common.utils.castOrNull import io.novafoundation.nova.common.utils.inBackground import io.novafoundation.nova.feature_account_api.data.model.Fee import io.novafoundation.nova.feature_account_api.data.model.FeeBase @@ -11,8 +10,8 @@ import io.novafoundation.nova.feature_wallet_api.domain.TokenUseCase 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.presentation.mixin.fee.GenericFeeLoaderMixin.Configuration -import io.novafoundation.nova.feature_wallet_api.presentation.model.FeeModel -import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain +import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.model.FeeDisplay +import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.model.FeeStatus import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.combine @@ -23,44 +22,21 @@ import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.transform -sealed class FeeStatus { - object Loading : FeeStatus() +interface GenericFeeLoaderMixin : Retriable { - class Loaded(val feeModel: FeeModel) : FeeStatus() - - object NoFee : FeeStatus() - - object Error : FeeStatus() -} - -sealed interface ChangeFeeTokenState { - - class Editable(val selectedCommissionAsset: Chain.Asset, val availableAssets: List) : ChangeFeeTokenState - - object NotSupported : ChangeFeeTokenState -} - - -interface GenericFeeLoaderMixin : Retriable { - - class Configuration( + class Configuration( val showZeroFiat: Boolean = true, val initialState: InitialState = InitialState() ) { - class InitialState( - val supportCustomFee: Boolean = false, - val feeStatus: FeeStatus? = null + class InitialState( + val feeStatus: FeeStatus? = null ) } - val feeLiveData: LiveData> - - val changeFeeTokenState: LiveData - - fun setCommissionAsset(chainAsset: Chain.Asset) + val feeLiveData: LiveData> - interface Presentation : GenericFeeLoaderMixin { + interface Presentation : GenericFeeLoaderMixin, SetFee { suspend fun loadFeeSuspending( retryScope: CoroutineScope, @@ -74,39 +50,19 @@ interface GenericFeeLoaderMixin : Retriable { onRetryCancelled: () -> Unit, ) - suspend fun setFee(fee: F?) - - suspend fun setFeeStatus(feeStatus: FeeStatus) - - suspend fun setSupportCustomFee(supportCustomFee: Boolean) + suspend fun setFeeOrHide(fee: F?) + suspend fun setFeeStatus(feeStatus: FeeStatus) suspend fun invalidateFee() - - fun commissionAssetFlow(): Flow - } - - interface Factory { - - @Deprecated("Use createChangeableFeeGeneric instead") - fun createGeneric( - tokenFlow: Flow, - configuration: Configuration = Configuration() - ): Presentation - - fun createChangeable( - tokenFlow: Flow, - coroutineScope: CoroutineScope, - configuration: Configuration = Configuration() - ): Presentation } } -@Deprecated("Use GenericFeeLoaderMixin instead") +@Deprecated("Use FeeLoaderMixinV2 instead") interface FeeLoaderMixin : GenericFeeLoaderMixin { interface Presentation : GenericFeeLoaderMixin.Presentation, FeeLoaderMixin - interface Factory : GenericFeeLoaderMixin.Factory { + interface Factory { fun create( tokenFlow: Flow, @@ -116,36 +72,19 @@ interface FeeLoaderMixin : GenericFeeLoaderMixin { } suspend fun GenericFeeLoaderMixin.awaitFee(): F = feeLiveData.asFlow() - .filterIsInstance>() + .filterIsInstance>() .first() .feeModel.fee suspend fun GenericFeeLoaderMixin.awaitOptionalFee(): F? = feeLiveData.asFlow() .transform { feeStatus -> when (feeStatus) { - is FeeStatus.Loaded -> emit(feeStatus.feeModel.fee) + is FeeStatus.Loaded -> emit(feeStatus.feeModel.fee) FeeStatus.NoFee -> emit(null) else -> {} // skip } }.first() -fun GenericFeeLoaderMixin.loadedFeeModelOrNullFlow(): Flow?> { - return feeLiveData - .asFlow() - .map { it.castOrNull>()?.feeModel } -} - -@Deprecated("Use createGenericChangeableFee instead") -fun GenericFeeLoaderMixin.Factory.createGeneric(assetFlow: Flow): GenericFeeLoaderMixin.Presentation = createGeneric(assetFlow.map { it.token }) - -fun GenericFeeLoaderMixin.Factory.createSimple(assetFlow: Flow) = createGeneric(assetFlow.map { it.token }) - -fun GenericFeeLoaderMixin.Factory.createChangeable( - assetFlow: Flow, - coroutineScope: CoroutineScope, - configuration: Configuration = Configuration() -) : GenericFeeLoaderMixin.Presentation = createChangeable(assetFlow.map { it.token }, coroutineScope, configuration) - @Deprecated("Use createChangeableFee instead") fun FeeLoaderMixin.Factory.create(assetFlow: Flow) = create(assetFlow.map { it.token }) @@ -189,9 +128,3 @@ fun FeeLoaderMixin.Presentation.connectWith( .inBackground() .launchIn(scope) } - -fun ChangeFeeTokenState.isEditable() = this is ChangeFeeTokenState.Editable - -suspend fun GenericFeeLoaderMixin.Presentation<*>.commissionAsset(): Asset { - return commissionAssetFlow().first() -} diff --git a/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/presentation/mixin/fee/FeeUI.kt b/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/presentation/mixin/fee/FeeUI.kt index 7f1849ba24..eb146cfba6 100644 --- a/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/presentation/mixin/fee/FeeUI.kt +++ b/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/presentation/mixin/fee/FeeUI.kt @@ -1,13 +1,10 @@ package io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee -import android.view.View import io.novafoundation.nova.common.base.BaseFragmentMixin import io.novafoundation.nova.common.base.BaseViewModel import io.novafoundation.nova.common.mixin.impl.observeRetries import io.novafoundation.nova.common.utils.makeGone -import io.novafoundation.nova.feature_account_api.presenatation.fee.select.FeeAssetSelectorBottomSheet import io.novafoundation.nova.feature_wallet_api.presentation.view.FeeView -import kotlinx.coroutines.flow.Flow interface WithFeeLoaderMixin { @@ -18,11 +15,6 @@ fun BaseFragmentMixin.setupFeeLoading(viewModel: V, feeView: FeeView) whe observeRetries(viewModel) viewModel.feeLiveData.observe(feeView::setFeeStatus) - viewModel.changeFeeTokenState.observe { editableStatus -> - feeView.setFeeEditable(editableStatus.isEditable()) { - openEditFeeBottomSheet(viewModel, editableStatus) - } - } } fun BaseFragmentMixin<*>.setupFeeLoading(withFeeLoaderMixin: WithFeeLoaderMixin, feeView: FeeView) { @@ -39,33 +31,4 @@ fun BaseFragmentMixin<*>.setupFeeLoading(mixin: GenericFeeLoaderMixin<*>, feeVie observeRetries(mixin) mixin.feeLiveData.observe(feeView::setFeeStatus) - mixin.changeFeeTokenState.observe { editableStatus -> - feeView.setFeeEditable(editableStatus.isEditable()) { - openEditFeeBottomSheet(mixin, editableStatus) - } - } -} - -fun BaseFragmentMixin<*>.setupSelectableFeeToken( - tokenSelectableFlow: Flow, - feeView: FeeView, - onEditTokenClick: View.OnClickListener -) { - tokenSelectableFlow.observe { canChangeFeeToken -> feeView.setFeeEditable(canChangeFeeToken, onEditTokenClick) } -} - -private fun BaseFragmentMixin<*>.openEditFeeBottomSheet(mixin: GenericFeeLoaderMixin<*>, editableStatus: ChangeFeeTokenState) { - if (editableStatus !is ChangeFeeTokenState.Editable) return - - val payload = FeeAssetSelectorBottomSheet.Payload( - options = editableStatus.availableAssets, - selectedOption = editableStatus.selectedCommissionAsset - ) - - FeeAssetSelectorBottomSheet( - context = providedContext, - payload = payload, - onOptionClicked = { mixin.setCommissionAsset(it) }, - onCancel = { } - ).show() } diff --git a/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/presentation/mixin/fee/SetFee.kt b/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/presentation/mixin/fee/SetFee.kt new file mode 100644 index 0000000000..1989252350 --- /dev/null +++ b/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/presentation/mixin/fee/SetFee.kt @@ -0,0 +1,6 @@ +package io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee + +fun interface SetFee { + + suspend fun setFee(fee: F) +} diff --git a/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/presentation/mixin/fee/amount/DefaultFeeBalanceExtractor.kt b/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/presentation/mixin/fee/amount/DefaultFeeBalanceExtractor.kt new file mode 100644 index 0000000000..ad18782db7 --- /dev/null +++ b/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/presentation/mixin/fee/amount/DefaultFeeBalanceExtractor.kt @@ -0,0 +1,13 @@ +package io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.amount + +import io.novafoundation.nova.feature_account_api.data.model.FeeBase +import io.novafoundation.nova.feature_account_api.data.model.getAmount +import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.types.Balance +import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain + +class DefaultFeeBalanceExtractor : FeeBalanceExtractor { + + override fun requiredBalanceToPayFee(fee: F, chainAsset: Chain.Asset): Balance { + return fee.getAmount(chainAsset) + } +} diff --git a/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/presentation/mixin/fee/amount/FeeBalanceExtractor.kt b/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/presentation/mixin/fee/amount/FeeBalanceExtractor.kt new file mode 100644 index 0000000000..1c46a6198d --- /dev/null +++ b/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/presentation/mixin/fee/amount/FeeBalanceExtractor.kt @@ -0,0 +1,9 @@ +package io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.amount + +import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.types.Balance +import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain + +interface FeeBalanceExtractor { + + fun requiredBalanceToPayFee(fee: F, chainAsset: Chain.Asset): Balance +} diff --git a/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/presentation/mixin/fee/formatter/DefaultFeeFormatter.kt b/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/presentation/mixin/fee/formatter/DefaultFeeFormatter.kt new file mode 100644 index 0000000000..528a8104cd --- /dev/null +++ b/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/presentation/mixin/fee/formatter/DefaultFeeFormatter.kt @@ -0,0 +1,26 @@ +package io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.formatter + +import io.novafoundation.nova.feature_account_api.data.model.FeeBase +import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.model.FeeDisplay +import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.model.FeeStatus +import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.model.toFeeDisplay +import io.novafoundation.nova.feature_wallet_api.presentation.model.mapAmountToAmountModel + +class DefaultFeeFormatter : FeeFormatter { + + override suspend fun formatFee( + fee: F, + configuration: FeeFormatter.Configuration, + context: FeeFormatter.Context + ): FeeDisplay { + return mapAmountToAmountModel( + amountInPlanks = fee.amount, + token = context.feeToken(), + includeZeroFiat = configuration.showZeroFiat + ).toFeeDisplay() + } + + override suspend fun createLoadingStatus(): FeeStatus.Loading { + return FeeStatus.Loading(visibleDuringProgress = true) + } +} diff --git a/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/presentation/mixin/fee/formatter/FeeFormatter.kt b/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/presentation/mixin/fee/formatter/FeeFormatter.kt new file mode 100644 index 0000000000..c5199f2845 --- /dev/null +++ b/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/presentation/mixin/fee/formatter/FeeFormatter.kt @@ -0,0 +1,40 @@ +package io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.formatter + +import io.novafoundation.nova.feature_wallet_api.domain.model.Token +import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.formatter.FeeFormatter.Context +import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.model.FeeModel +import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.model.FeeStatus + +interface FeeFormatter { + + class Configuration( + val showZeroFiat: Boolean + ) + + interface Context { + + suspend fun feeToken(): Token + } + + suspend fun formatFee( + fee: F, + configuration: Configuration, + context: Context, + ): D + + suspend fun createLoadingStatus(): FeeStatus.Loading +} + +context(Context) +suspend fun FeeFormatter.formatFeeStatus( + fee: F?, + configuration: FeeFormatter.Configuration, +): FeeStatus { + return if (fee != null) { + val display = formatFee(fee, configuration, this@Context) + val feeModel = FeeModel(fee, display) + FeeStatus.Loaded(feeModel) + } else { + FeeStatus.NoFee + } +} diff --git a/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/presentation/mixin/fee/model/ChooseFeeCurrencyPayload.kt b/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/presentation/mixin/fee/model/ChooseFeeCurrencyPayload.kt new file mode 100644 index 0000000000..e33157be56 --- /dev/null +++ b/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/presentation/mixin/fee/model/ChooseFeeCurrencyPayload.kt @@ -0,0 +1,5 @@ +package io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.model + +import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain + +class ChooseFeeCurrencyPayload(val selectedCommissionAsset: Chain.Asset, val availableAssets: List) diff --git a/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/presentation/mixin/fee/model/FeeModel.kt b/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/presentation/mixin/fee/model/FeeModel.kt new file mode 100644 index 0000000000..701f93e624 --- /dev/null +++ b/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/presentation/mixin/fee/model/FeeModel.kt @@ -0,0 +1,17 @@ +package io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.model + +import io.novafoundation.nova.feature_wallet_api.presentation.model.AmountModel + +class FeeModel( + val fee: F, + val display: D, +) + +class FeeDisplay( + val title: String, + val subtitle: String? +) + +fun AmountModel.toFeeDisplay(): FeeDisplay { + return FeeDisplay(title = token, subtitle = fiat) +} diff --git a/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/presentation/mixin/fee/model/FeeStatus.kt b/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/presentation/mixin/fee/model/FeeStatus.kt new file mode 100644 index 0000000000..f8b19eb9ac --- /dev/null +++ b/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/presentation/mixin/fee/model/FeeStatus.kt @@ -0,0 +1,43 @@ +package io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.model + +sealed class FeeStatus { + class Loading(val visibleDuringProgress: Boolean) : FeeStatus() + + class Loaded(val feeModel: FeeModel) : FeeStatus() + + object NoFee : FeeStatus() + + object Error : FeeStatus() +} + +fun FeeStatus.mapDisplay(map: (D1) -> D2?): FeeStatus { + return when (this) { + FeeStatus.Error -> FeeStatus.Error + + is FeeStatus.Loaded -> { + val newFeeDisplay = map(feeModel.display) + + if (newFeeDisplay == null) { + FeeStatus.NoFee + } else { + FeeStatus.Loaded(FeeModel(feeModel.fee, newFeeDisplay)) + } + } + + is FeeStatus.Loading -> this + + FeeStatus.NoFee -> FeeStatus.NoFee + } +} + +fun FeeStatus.mapProgress(map: (Boolean) -> Boolean): FeeStatus { + return when (this) { + FeeStatus.Error -> FeeStatus.Error + + is FeeStatus.Loaded -> this + + is FeeStatus.Loading -> FeeStatus.Loading(map(visibleDuringProgress)) + + FeeStatus.NoFee -> FeeStatus.NoFee + } +} diff --git a/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/presentation/mixin/fee/model/PaymentCurrencySelectionMode.kt b/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/presentation/mixin/fee/model/PaymentCurrencySelectionMode.kt new file mode 100644 index 0000000000..1522256446 --- /dev/null +++ b/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/presentation/mixin/fee/model/PaymentCurrencySelectionMode.kt @@ -0,0 +1,34 @@ +package io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.model + +import io.novafoundation.nova.feature_account_api.data.fee.FeePaymentCurrency + +enum class PaymentCurrencySelectionMode { + + /** + * Payment currency cannot be changed and is always equal to [FeePaymentCurrency.Native] + */ + DISABLED, + + /** + * Payment currency can be changed by automatic internal logic, e.g. when there is not enough balance + */ + AUTOMATIC_ONLY, + + /** + * Payment currency can be changed both by automatic internal logic and the user + */ + ENABLED +} + +fun PaymentCurrencySelectionMode.automaticChangeEnabled(): Boolean { + return this != PaymentCurrencySelectionMode.DISABLED +} + + +fun PaymentCurrencySelectionMode.userCanChangeFee(): Boolean { + return this == PaymentCurrencySelectionMode.ENABLED +} + +fun PaymentCurrencySelectionMode.onlyNativeFeeEnabled(): Boolean { + return this == PaymentCurrencySelectionMode.DISABLED +} diff --git a/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/presentation/mixin/fee/provider/ChangeableFeeLoaderProvider.kt b/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/presentation/mixin/fee/provider/ChangeableFeeLoaderProvider.kt deleted file mode 100644 index af26c5fdfb..0000000000 --- a/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/presentation/mixin/fee/provider/ChangeableFeeLoaderProvider.kt +++ /dev/null @@ -1,262 +0,0 @@ -package io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.provider - -import androidx.lifecycle.MutableLiveData -import io.novafoundation.nova.common.mixin.actionAwaitable.ActionAwaitableMixin -import io.novafoundation.nova.common.mixin.api.RetryPayload -import io.novafoundation.nova.common.resources.ResourceManager -import io.novafoundation.nova.common.utils.Event -import io.novafoundation.nova.common.utils.asLiveData -import io.novafoundation.nova.common.utils.singleReplaySharedFlow -import io.novafoundation.nova.feature_account_api.data.model.FeeBase -import io.novafoundation.nova.feature_account_api.presenatation.fee.select.FeeAssetSelectorBottomSheet -import io.novafoundation.nova.feature_wallet_api.R -import io.novafoundation.nova.feature_wallet_api.data.mappers.mapFeeToFeeModel -import io.novafoundation.nova.feature_wallet_api.domain.fee.CustomFeeInteractor -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.presentation.mixin.fee.ChangeFeeTokenState -import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.FeeStatus -import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.GenericFeeLoaderMixin -import io.novafoundation.nova.runtime.ext.commissionAsset -import io.novafoundation.nova.runtime.ext.fullId -import io.novafoundation.nova.runtime.ext.isCommissionAsset -import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry -import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain -import kotlinx.coroutines.CancellationException -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.combine -import kotlinx.coroutines.flow.distinctUntilChangedBy -import kotlinx.coroutines.flow.filterNotNull -import kotlinx.coroutines.flow.first -import kotlinx.coroutines.flow.flatMapLatest -import kotlinx.coroutines.flow.launchIn -import kotlinx.coroutines.flow.map -import kotlinx.coroutines.flow.onEach -import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext - -internal open class ChangeableFeeLoaderProvider( - private val interactor: CustomFeeInteractor, - private val chainRegistry: ChainRegistry, - private val resourceManager: ResourceManager, - private val configuration: GenericFeeLoaderMixin.Configuration, - private val actionAwaitableMixinFactory: ActionAwaitableMixin.Factory, - private val nulableSelectedTokenFlow: Flow, - private val coroutineScope: CoroutineScope -) : GenericFeeLoaderMixin.Presentation { - - private val selectedTokenFlow = nulableSelectedTokenFlow.filterNotNull() - - private val chainFlow = selectedTokenFlow - .distinctUntilChangedBy { it.configuration.chainId } - .map { chainRegistry.getChain(it.configuration.chainId) } - - private val supportCustomFeeFlow = MutableStateFlow(configuration.initialState.supportCustomFee) - - private val canPayFeeInCustomAssetFlow: Flow = combine(supportCustomFeeFlow, selectedTokenFlow) { supportCustomFee, selectedToken -> - if (!supportCustomFee) return@combine false - - interactor.canPayFeeInNonUtilityAsset(selectedToken.configuration, coroutineScope) - } - - private val commissionChainAssetFlow = singleReplaySharedFlow() - - private val commissionAssetFlow: Flow = commissionChainAssetFlow - .distinctUntilChangedBy { it.fullId } - .flatMapLatest { interactor.assetFlow(it) } - - final override val feeLiveData = MutableLiveData>() - - override val retryEvent = MutableLiveData>() - - val changeFeeTokenEvent = actionAwaitableMixinFactory.create() - private val feeMayChangeAutomaticallyFlow = MutableStateFlow(true) - - override val changeFeeTokenState = combine( - supportCustomFeeFlow, - selectedTokenFlow, - commissionChainAssetFlow, - canPayFeeInCustomAssetFlow - ) { supportCustomFee, selectedToken, selectedCommissionAsset, canPayFeeInCustomAsset -> - mapChangeFeeTokenState(supportCustomFee, selectedToken, selectedCommissionAsset, canPayFeeInCustomAsset) - }.asLiveData(coroutineScope) - - init { - configuration.initialState.feeStatus?.let { feeLiveData.postValue(it) } - - setupCustomFee() - } - - override suspend fun loadFeeSuspending( - retryScope: CoroutineScope, - feeConstructor: suspend (Token) -> F?, - onRetryCancelled: () -> Unit, - ): Unit = withContext(Dispatchers.IO) { - postFeeState(FeeStatus.Loading) - - val token = commissionAssetFlow.first().token - - val value = runCatching { - feeConstructor(token) - }.fold( - onSuccess = { genericFee -> onFeeLoaded(token, genericFee) }, - onFailure = { exception -> onError(exception, retryScope, feeConstructor, onRetryCancelled) } - ) - - value?.run { postFeeState(this) } - } - - override fun loadFee( - coroutineScope: CoroutineScope, - feeConstructor: suspend (Token) -> F?, - onRetryCancelled: () -> Unit - ) { - coroutineScope.launch { - loadFeeSuspending( - retryScope = coroutineScope, - feeConstructor = feeConstructor, - onRetryCancelled = onRetryCancelled - ) - } - } - - override suspend fun setFee(fee: F?) { - if (fee != null) { - val token = commissionAssetFlow.first().token - - val feeModel = mapFeeToFeeModel(fee, token, includeZeroFiat = configuration.showZeroFiat) - - postFeeState(FeeStatus.Loaded(feeModel)) - } else { - postFeeState(FeeStatus.NoFee) - } - } - - override suspend fun setFeeStatus(feeStatus: FeeStatus) { - postFeeState(feeStatus) - } - - override suspend fun setSupportCustomFee(supportCustomFee: Boolean) { - supportCustomFeeFlow.value = supportCustomFee - } - - override suspend fun invalidateFee() { - postFeeState(FeeStatus.Loading) - } - - override fun commissionAssetFlow(): Flow { - return commissionAssetFlow - } - - override fun setCommissionAsset(chainAsset: Chain.Asset) { - coroutineScope.launch { - feeMayChangeAutomaticallyFlow.value = false - commissionChainAssetFlow.emit(chainAsset) - } - } - - private fun onFeeLoaded(token: Token, fee: F?): FeeStatus = if (fee != null) { - val feeModel = mapFeeToFeeModel(fee, token, includeZeroFiat = configuration.showZeroFiat) - - FeeStatus.Loaded(feeModel) - } else { - FeeStatus.NoFee - } - - private fun onError( - exception: Throwable, - retryScope: CoroutineScope, - feeConstructor: suspend (Token) -> F?, - onRetryCancelled: () -> Unit, - ) = if (exception !is CancellationException) { - retryEvent.postValue( - Event( - RetryPayload( - title = resourceManager.getString(R.string.choose_amount_network_error), - message = resourceManager.getString(R.string.choose_amount_error_fee), - onRetry = { loadFee(retryScope, feeConstructor, onRetryCancelled) }, - onCancel = onRetryCancelled - ) - ) - ) - - exception.printStackTrace() - - FeeStatus.Error - } else { - null - } - - private fun setupCustomFee() { - // After chain is changed make commission asset default and reset feeMayChangeAutomaticallyFlag - chainFlow.onEach { - feeMayChangeAutomaticallyFlow.value = true - commissionChainAssetFlow.emit(it.commissionAsset) - }.launchIn(coroutineScope) - - // After supportCustomFee is changed make commission asset default - supportCustomFeeFlow.onEach { - commissionChainAssetFlow.emit(chainFlow.first().commissionAsset) - }.launchIn(coroutineScope) - } - - private suspend fun postFeeState(feeStatus: FeeStatus) { - if (feeStatus !is FeeStatus.Loaded) { - feeLiveData.postValue(feeStatus) - return - } - - val feeMayChangeAutomatically = feeMayChangeAutomaticallyFlow.first() - val supportCustomFee = supportCustomFeeFlow.first() - if (!feeMayChangeAutomatically || !supportCustomFee) { - feeLiveData.postValue(feeStatus) - return - } - - val commissionAsset = commissionAssetFlow.first() - val selectedToken = selectedTokenFlow.first() - val selectedAssetIsAvailableToPayFee = canPayFeeInCustomAssetFlow.first() - - if (commissionAsset.isCommissionAsset() && selectedAssetIsAvailableToPayFee) { - val feeAmount = feeStatus.feeModel.fee.amount - if (interactor.hasEnoughBalanceToPayFee(commissionAsset, feeAmount)) { - feeLiveData.postValue(feeStatus) - } else { - // Select custom fee asset - commissionChainAssetFlow.emit(selectedToken.configuration) - } - } else { - feeLiveData.postValue(feeStatus) - } - } - - private suspend fun mapChangeFeeTokenState( - supportCustomFee: Boolean, - selectedToken: Token, - customFeeAsset: Chain.Asset, - canPayFeeInCustomAsset: Boolean - ): ChangeFeeTokenState { - val originChainAsset = selectedToken.configuration - - return when { - !supportCustomFee -> ChangeFeeTokenState.NotSupported - - originChainAsset.isCommissionAsset -> ChangeFeeTokenState.NotSupported - - canPayFeeInCustomAsset -> { - val chain = chainRegistry.getChain(originChainAsset.chainId) - val selectableAssets = listOf(chain.commissionAsset, originChainAsset) - ChangeFeeTokenState.Editable(customFeeAsset, selectableAssets) - } - - else -> ChangeFeeTokenState.NotSupported - } - } - - private fun Asset.isCommissionAsset(): Boolean { - return token.configuration.isCommissionAsset - } -} diff --git a/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/presentation/mixin/fee/provider/FeeLoaderProviderFactory.kt b/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/presentation/mixin/fee/provider/FeeLoaderProviderFactory.kt index ed82bf3a0a..5b04eaf12a 100644 --- a/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/presentation/mixin/fee/provider/FeeLoaderProviderFactory.kt +++ b/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/presentation/mixin/fee/provider/FeeLoaderProviderFactory.kt @@ -1,22 +1,14 @@ package io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.provider -import io.novafoundation.nova.common.mixin.actionAwaitable.ActionAwaitableMixin import io.novafoundation.nova.common.resources.ResourceManager import io.novafoundation.nova.feature_account_api.data.model.Fee -import io.novafoundation.nova.feature_account_api.data.model.FeeBase -import io.novafoundation.nova.feature_wallet_api.domain.fee.CustomFeeInteractor import io.novafoundation.nova.feature_wallet_api.domain.model.Token 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.runtime.multiNetwork.ChainRegistry -import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.Flow class FeeLoaderProviderFactory( - private val customFeeInteractor: CustomFeeInteractor, - private val chainRegistry: ChainRegistry, private val resourceManager: ResourceManager, - private val actionAwaitableMixinFactory: ActionAwaitableMixin.Factory, ) : FeeLoaderMixin.Factory { override fun create( @@ -25,27 +17,4 @@ class FeeLoaderProviderFactory( ): FeeLoaderMixin.Presentation { return GenericFeeLoaderProviderPresentation(resourceManager, configuration, tokenFlow) } - - override fun createGeneric( - tokenFlow: Flow, - configuration: GenericFeeLoaderMixin.Configuration - ): GenericFeeLoaderMixin.Presentation { - return GenericFeeLoaderProvider(resourceManager, configuration, tokenFlow) - } - - override fun createChangeable( - tokenFlow: Flow, - coroutineScope: CoroutineScope, - configuration: GenericFeeLoaderMixin.Configuration - ): GenericFeeLoaderMixin.Presentation { - return ChangeableFeeLoaderProvider( - customFeeInteractor, - chainRegistry, - resourceManager, - configuration, - actionAwaitableMixinFactory, - tokenFlow, - coroutineScope - ) - } } diff --git a/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/presentation/mixin/fee/provider/GenericFeeLoaderProvider.kt b/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/presentation/mixin/fee/provider/GenericFeeLoaderProvider.kt index 6da99419ea..c580086ef3 100644 --- a/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/presentation/mixin/fee/provider/GenericFeeLoaderProvider.kt +++ b/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/presentation/mixin/fee/provider/GenericFeeLoaderProvider.kt @@ -1,23 +1,20 @@ package io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.provider -import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData -import androidx.lifecycle.liveData import io.novafoundation.nova.common.mixin.api.RetryPayload import io.novafoundation.nova.common.resources.ResourceManager import io.novafoundation.nova.common.utils.Event import io.novafoundation.nova.common.utils.firstNotNull import io.novafoundation.nova.feature_account_api.data.model.Fee -import io.novafoundation.nova.feature_account_api.data.model.FeeBase import io.novafoundation.nova.feature_wallet_api.R -import io.novafoundation.nova.feature_wallet_api.data.mappers.mapFeeToFeeModel -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.presentation.mixin.fee.ChangeFeeTokenState import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.FeeLoaderMixin -import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.FeeStatus import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.GenericFeeLoaderMixin -import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain +import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.formatter.DefaultFeeFormatter +import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.formatter.FeeFormatter +import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.formatter.formatFeeStatus +import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.model.FeeDisplay +import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.model.FeeStatus import kotlinx.coroutines.CancellationException import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers @@ -29,21 +26,27 @@ import kotlinx.coroutines.withContext internal class GenericFeeLoaderProviderPresentation( resourceManager: ResourceManager, configuration: GenericFeeLoaderMixin.Configuration, - tokenFlow: Flow, -) : GenericFeeLoaderProvider(resourceManager, configuration, tokenFlow), FeeLoaderMixin.Presentation + tokenFlow: Flow +) : GenericFeeLoaderProvider( + resourceManager = resourceManager, + configuration = configuration, + tokenFlow = tokenFlow, + feeFormatter = DefaultFeeFormatter() +), FeeLoaderMixin.Presentation @Deprecated("Use ChangeableFeeLoaderProvider instead") -internal open class GenericFeeLoaderProvider( - protected val resourceManager: ResourceManager, - protected val configuration: GenericFeeLoaderMixin.Configuration, - protected val tokenFlow: Flow, -) : GenericFeeLoaderMixin.Presentation { +internal open class GenericFeeLoaderProvider( + private val resourceManager: ResourceManager, + private val configuration: GenericFeeLoaderMixin.Configuration, + private val tokenFlow: Flow, + private val feeFormatter: FeeFormatter, +) : GenericFeeLoaderMixin.Presentation, FeeFormatter.Context { - final override val feeLiveData = MutableLiveData>() - override val changeFeeTokenState: LiveData = liveData { emit(ChangeFeeTokenState.NotSupported) } + private val feeFormatterConfiguration = configuration.toFeeFormatterConfiguration() - override val retryEvent = MutableLiveData>() + final override val feeLiveData = MutableLiveData>() + override val retryEvent = MutableLiveData>() init { configuration.initialState.feeStatus?.let(feeLiveData::postValue) } @@ -53,14 +56,14 @@ internal open class GenericFeeLoaderProvider( feeConstructor: suspend (Token) -> F?, onRetryCancelled: () -> Unit, ): Unit = withContext(Dispatchers.IO) { - feeLiveData.postValue(FeeStatus.Loading) + feeLiveData.postValue(FeeStatus.Loading(visibleDuringProgress = true)) val token = tokenFlow.firstNotNull() val value = runCatching { feeConstructor(token) }.fold( - onSuccess = { genericFee -> onFeeLoaded(token, genericFee) }, + onSuccess = { fee -> feeFormatter.formatFeeStatus(fee, feeFormatterConfiguration) }, onFailure = { exception -> onError(exception, retryScope, feeConstructor, onRetryCancelled) } ) @@ -81,43 +84,21 @@ internal open class GenericFeeLoaderProvider( } } - override fun commissionAssetFlow(): Flow { - throw IllegalStateException("commissionAssetFlow not supported") + override suspend fun setFeeOrHide(fee: F?) { + val feeStatus = feeFormatter.formatFeeStatus(fee, feeFormatterConfiguration) + feeLiveData.postValue(feeStatus) } - override fun setCommissionAsset(chainAsset: Chain.Asset) { - // Not supported + override suspend fun setFee(fee: F) { + setFeeOrHide(fee as F?) } - override suspend fun setFee(fee: F?) { - if (fee != null) { - val token = tokenFlow.firstNotNull() - val feeModel = mapFeeToFeeModel(fee, token, includeZeroFiat = configuration.showZeroFiat) - - feeLiveData.postValue(FeeStatus.Loaded(feeModel)) - } else { - feeLiveData.postValue(FeeStatus.NoFee) - } - } - - override suspend fun setFeeStatus(feeStatus: FeeStatus) { + override suspend fun setFeeStatus(feeStatus: FeeStatus) { feeLiveData.postValue(feeStatus) } - override suspend fun setSupportCustomFee(supportCustomFee: Boolean) { - // Not supported - } - override suspend fun invalidateFee() { - feeLiveData.postValue(FeeStatus.Loading) - } - - private fun onFeeLoaded(token: Token, fee: F?): FeeStatus = if (fee != null) { - val feeModel = mapFeeToFeeModel(fee, token, includeZeroFiat = configuration.showZeroFiat) - - FeeStatus.Loaded(feeModel) - } else { - FeeStatus.NoFee + feeLiveData.postValue(FeeStatus.Loading(visibleDuringProgress = true)) } private fun onError( @@ -143,4 +124,12 @@ internal open class GenericFeeLoaderProvider( } else { null } + + private fun GenericFeeLoaderMixin.Configuration<*>.toFeeFormatterConfiguration(): FeeFormatter.Configuration { + return FeeFormatter.Configuration(showZeroFiat) + } + + override suspend fun feeToken(): Token { + return tokenFlow.firstNotNull() + } } diff --git a/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/presentation/mixin/fee/v2/FeeLoaderMixinV2.kt b/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/presentation/mixin/fee/v2/FeeLoaderMixinV2.kt new file mode 100644 index 0000000000..49852a0bd5 --- /dev/null +++ b/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/presentation/mixin/fee/v2/FeeLoaderMixinV2.kt @@ -0,0 +1,89 @@ +package io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.v2 + +import io.novafoundation.nova.common.mixin.actionAwaitable.ActionAwaitableMixin +import io.novafoundation.nova.common.mixin.api.Retriable +import io.novafoundation.nova.feature_account_api.data.fee.FeePaymentCurrency +import io.novafoundation.nova.feature_account_api.data.model.FeeBase +import io.novafoundation.nova.feature_wallet_api.domain.model.Asset +import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.SetFee +import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.amount.DefaultFeeBalanceExtractor +import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.amount.FeeBalanceExtractor +import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.formatter.DefaultFeeFormatter +import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.formatter.FeeFormatter +import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.model.ChooseFeeCurrencyPayload +import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.model.FeeDisplay +import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.model.FeeStatus +import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.model.PaymentCurrencySelectionMode +import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.v2.FeeLoaderMixinV2.Configuration +import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.v2.FeeLoaderMixinV2.Factory +import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.StateFlow + +interface FeeLoaderMixinV2 : Retriable { + + class Configuration( + val showZeroFiat: Boolean = true, + val initialState: InitialState = InitialState(), + val onRetryCancelled: () -> Unit = {} + ) { + + class InitialState( + val paymentCurrencySelectionMode: PaymentCurrencySelectionMode = PaymentCurrencySelectionMode.DISABLED, + val feeStatus: FeeStatus? = null + ) + } + + val fee: StateFlow> + + val userCanChangeFeeAsset: Flow + + val chooseFeeAsset: ActionAwaitableMixin + + fun changePaymentCurrencyClicked() + + interface Presentation : FeeLoaderMixinV2, SetFee { + + suspend fun feeAsset(): Asset + + suspend fun feePaymentCurrency(): FeePaymentCurrency + + fun loadFee(feeConstructor: FeeConstructor) + + suspend fun setPaymentCurrencySelectionMode(mode: PaymentCurrencySelectionMode) + + suspend fun setFeeOrHide(fee: F?) + + suspend fun setFeeLoading() + + suspend fun setFeeStatus(feeStatus: FeeStatus) + } + + interface Factory { + + fun create( + scope: CoroutineScope, + selectedChainAssetFlow: Flow, + feeFormatter: FeeFormatter, + feeBalanceExtractor: FeeBalanceExtractor, + configuration: Configuration = Configuration() + ): Presentation + } +} + +typealias FeeConstructor = suspend (FeePaymentCurrency) -> F? + +fun Factory.createDefault( + scope: CoroutineScope, + selectedChainAssetFlow: Flow, + configuration: Configuration = Configuration() +): FeeLoaderMixinV2.Presentation { + return create( + scope = scope, + selectedChainAssetFlow = selectedChainAssetFlow, + feeFormatter = DefaultFeeFormatter(), + feeBalanceExtractor = DefaultFeeBalanceExtractor(), + configuration = configuration + ) +} diff --git a/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/presentation/mixin/fee/v2/FeeLoaderMixinV2Ext.kt b/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/presentation/mixin/fee/v2/FeeLoaderMixinV2Ext.kt new file mode 100644 index 0000000000..95e54013b5 --- /dev/null +++ b/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/presentation/mixin/fee/v2/FeeLoaderMixinV2Ext.kt @@ -0,0 +1,56 @@ +package io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.v2 + +import io.novafoundation.nova.common.base.BaseViewModel +import io.novafoundation.nova.common.utils.inBackground +import io.novafoundation.nova.feature_account_api.data.fee.FeePaymentCurrency +import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.model.FeeStatus +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.filterIsInstance +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.transform + +suspend fun FeeLoaderMixinV2.awaitFee(): F = fee + .filterIsInstance>() + .first() + .feeModel.fee + +suspend fun FeeLoaderMixinV2.awaitOptionalFee(): F? = fee + .transform { feeStatus -> + when (feeStatus) { + is FeeStatus.Loaded -> emit(feeStatus.feeModel.fee) + FeeStatus.NoFee -> emit(null) + else -> {} // skip + } + }.first() + +suspend fun FeeLoaderMixinV2.Presentation.setFeeOrHide(fee: F?) { + if (fee != null) { + setFee(fee) + } else { + setFeeStatus(FeeStatus.NoFee) + } +} + +context(BaseViewModel) +fun FeeLoaderMixinV2.Presentation.connectWith( + inputSource1: Flow, + inputSource2: Flow, + inputSource3: Flow, + inputSource4: Flow, + feeConstructor: suspend (FeePaymentCurrency, input1: I1, input2: I2, input3: I3, input4: I4) -> F, +) { + combine( + inputSource1, + inputSource2, + inputSource3, + inputSource4 + ) { input1, input2, input3, input4 -> + loadFee( + feeConstructor = { paymentCurrency -> feeConstructor(paymentCurrency, input1, input2, input3, input4) }, + ) + } + .inBackground() + .launchIn(this@BaseViewModel) +} diff --git a/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/presentation/mixin/fee/v2/FeeLoaderV2Factory.kt b/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/presentation/mixin/fee/v2/FeeLoaderV2Factory.kt new file mode 100644 index 0000000000..9ce940527a --- /dev/null +++ b/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/presentation/mixin/fee/v2/FeeLoaderV2Factory.kt @@ -0,0 +1,39 @@ +package io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.v2 + +import io.novafoundation.nova.common.mixin.actionAwaitable.ActionAwaitableMixin +import io.novafoundation.nova.common.resources.ResourceManager +import io.novafoundation.nova.feature_wallet_api.domain.fee.CustomFeeInteractor +import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.amount.FeeBalanceExtractor +import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.formatter.FeeFormatter +import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry +import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.Flow + +class FeeLoaderV2Factory( + private val chainRegistry: ChainRegistry, + private val actionAwaitableMixinFactory: ActionAwaitableMixin.Factory, + private val resourceManager: ResourceManager, + private val interactor: CustomFeeInteractor, +): FeeLoaderMixinV2.Factory { + + override fun create( + scope: CoroutineScope, + selectedChainAssetFlow: Flow, + feeFormatter: FeeFormatter, + feeBalanceExtractor: FeeBalanceExtractor, + configuration: FeeLoaderMixinV2.Configuration + ): FeeLoaderMixinV2.Presentation { + return FeeLoaderV2Provider( + chainRegistry = chainRegistry, + actionAwaitableMixinFactory = actionAwaitableMixinFactory, + resourceManager = resourceManager, + interactor = interactor, + feeFormatter = feeFormatter, + configuration = configuration, + feeBalanceExtractor = feeBalanceExtractor, + selectedChainAssetFlow = selectedChainAssetFlow, + coroutineScope = scope + ) + } +} diff --git a/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/presentation/mixin/fee/v2/FeeLoaderV2Provider.kt b/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/presentation/mixin/fee/v2/FeeLoaderV2Provider.kt new file mode 100644 index 0000000000..be13ebe9f2 --- /dev/null +++ b/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/presentation/mixin/fee/v2/FeeLoaderV2Provider.kt @@ -0,0 +1,296 @@ +package io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.v2 + +import androidx.lifecycle.MutableLiveData +import io.novafoundation.nova.common.mixin.actionAwaitable.ActionAwaitableMixin +import io.novafoundation.nova.common.mixin.api.RetryPayload +import io.novafoundation.nova.common.resources.ResourceManager +import io.novafoundation.nova.common.utils.Event +import io.novafoundation.nova.common.utils.shareInBackground +import io.novafoundation.nova.common.utils.singleReplaySharedFlow +import io.novafoundation.nova.feature_account_api.data.fee.FeePaymentCurrency +import io.novafoundation.nova.feature_account_api.data.fee.toFeePaymentCurrency +import io.novafoundation.nova.feature_wallet_api.R +import io.novafoundation.nova.feature_wallet_api.domain.fee.CustomFeeInteractor +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.presentation.mixin.fee.amount.FeeBalanceExtractor +import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.formatter.FeeFormatter +import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.formatter.formatFeeStatus +import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.model.ChooseFeeCurrencyPayload +import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.model.FeeStatus +import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.model.PaymentCurrencySelectionMode +import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.model.automaticChangeEnabled +import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.model.onlyNativeFeeEnabled +import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.model.userCanChangeFee +import io.novafoundation.nova.runtime.ext.fullId +import io.novafoundation.nova.runtime.ext.isCommissionAsset +import io.novafoundation.nova.runtime.ext.isUtilityAsset +import io.novafoundation.nova.runtime.ext.utilityAsset +import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry +import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.distinctUntilChangedBy +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.launch +import kotlinx.coroutines.suspendCancellableCoroutine +import kotlin.coroutines.cancellation.CancellationException +import kotlin.coroutines.resume + +class FeeLoaderV2Provider( + private val chainRegistry: ChainRegistry, + private val actionAwaitableMixinFactory: ActionAwaitableMixin.Factory, + private val resourceManager: ResourceManager, + private val interactor: CustomFeeInteractor, + + private val feeFormatter: FeeFormatter, + private val configuration: FeeLoaderMixinV2.Configuration, + private val feeBalanceExtractor: FeeBalanceExtractor, + private val selectedChainAssetFlow: Flow, + coroutineScope: CoroutineScope +) : FeeLoaderMixinV2.Presentation, CoroutineScope by coroutineScope, FeeFormatter.Context { + + private val feeFormatterConfiguration = configuration.toFeeFormatterConfiguration() + + private val selectedTokenInfo = selectedChainAssetFlow + .distinctUntilChangedBy { it.fullId } + .map(::constructSelectedTokenInfo) + .shareInBackground() + + private val paymentCurrencySelectionModeFlow = MutableStateFlow(configuration.initialState.paymentCurrencySelectionMode) + + private val feeChainAsset = singleReplaySharedFlow() + + private val feeAsset: Flow = feeChainAsset + .distinctUntilChangedBy { it.fullId } + .flatMapLatest { interactor.assetFlow(it) } + .shareInBackground() + + private val userModifiedFeeInCurrentChain = MutableStateFlow(false) + + private val canSwitchToSelectedToken = combine(selectedTokenInfo, feeChainAsset) { selectedTokenInfo, feeAsset -> + val canSwitchToSelected = feeAsset.fullId != selectedTokenInfo.chainAsset.fullId + val selectedCanBeUsed = selectedTokenInfo.feePaymentSupported + + canSwitchToSelected && selectedCanBeUsed + } + .distinctUntilChanged() + .shareInBackground() + + private val canChangeFeeAutomatically = combine( + userModifiedFeeInCurrentChain, + paymentCurrencySelectionModeFlow, + canSwitchToSelectedToken + ) { userModifiedFee, selectionMode, canSwitchToSelected -> + canSwitchToSelected && selectionMode.automaticChangeEnabled() && !userModifiedFee + }.shareInBackground() + + override val userCanChangeFeeAsset: Flow = combine( + paymentCurrencySelectionModeFlow, + selectedTokenInfo + ) { selectionMode, tokenInfo -> + selectionMode.userCanChangeFee() && tokenInfo.feePaymentSupported + }.shareInBackground() + + override val chooseFeeAsset = actionAwaitableMixinFactory.create() + + override val fee = MutableStateFlow(configuration.initialState.feeStatus ?: FeeStatus.NoFee) + + override val retryEvent = MutableLiveData>() + + private var latestLoadFeeJob: Job? = null + private var latestFeeConstructor: FeeConstructor? = null + + init { + observeChainChanges() + + observeFeeAssetChanges() + } + + override fun loadFee(feeConstructor: FeeConstructor) { + latestLoadFeeJob?.cancel() + latestLoadFeeJob = launch(Dispatchers.IO) { + latestFeeConstructor = feeConstructor + fee.emit(feeFormatter.createLoadingStatus()) + + val feePaymentCurrency = feeChainAsset.first().toFeePaymentCurrency() + + runCatching { feeConstructor(feePaymentCurrency) } + .onSuccess { onFeeLoaded(it, feeConstructor) } + .onFailure { onFeeError(it, feeConstructor) } + } + } + + private suspend fun onFeeError(error: Throwable, feeConstructor: FeeConstructor) { + if (error !is CancellationException) { + error.printStackTrace() + fee.emit(FeeStatus.Error) + + awaitFeeRetry() + + loadFee(feeConstructor) + } + } + + private suspend fun onFeeLoaded(newFee: F?, feeConstructor: FeeConstructor) { + if (newFee != null) { + setFeeWithAutomaticChange(newFee, feeConstructor) + } else { + fee.emit(FeeStatus.NoFee) + } + } + + private suspend fun awaitFeeRetry() { + return suspendCancellableCoroutine { continuation -> + retryEvent.postValue( + Event( + RetryPayload( + title = resourceManager.getString(R.string.choose_amount_network_error), + message = resourceManager.getString(R.string.choose_amount_error_fee), + onRetry = { continuation.resume(Unit) }, + onCancel = { continuation.cancel() } + ) + ) + ) + } + } + + override suspend fun feeAsset(): Asset { + return feeAsset.first() + } + + override suspend fun feeToken(): Token { + return feeAsset.first().token + } + + override suspend fun feePaymentCurrency(): FeePaymentCurrency { + return feeChainAsset.first().toFeePaymentCurrency() + } + + override suspend fun setPaymentCurrencySelectionMode(mode: PaymentCurrencySelectionMode) { + paymentCurrencySelectionModeFlow.value = mode + + val isCustomFee = !feeChainAsset.first().isUtilityAsset + + if (isCustomFee && mode.onlyNativeFeeEnabled()) { + val utilityAsset = selectedTokenInfo.first().chainAsset + feeChainAsset.emit(utilityAsset) + } + } + + override suspend fun setFeeLoading() { + fee.emit(feeFormatter.createLoadingStatus()) + } + + override suspend fun setFeeStatus(feeStatus: FeeStatus) { + fee.emit(feeStatus) + } + + override suspend fun setFee(fee: F) { + setFeeOrHide(fee as F?) + } + + override suspend fun setFeeOrHide(fee: F?) { + val feeStatus = feeFormatter.formatFeeStatus(fee, feeFormatterConfiguration) + setFeeStatus(feeStatus) + } + + private suspend fun setFeeWithAutomaticChange( + newFee: F, + feeConstructor: suspend (FeePaymentCurrency) -> F? + ) { + val feeStatus = feeFormatter.formatFeeStatus(newFee, feeFormatterConfiguration) + + val canChangeFeeAutomatically = canChangeFeeAutomatically.first() + if (!canChangeFeeAutomatically) { + fee.value = feeStatus + return + } + + val feeAsset = feeAsset.first() + if (feeAsset.canPayFee(newFee)) { + fee.value = feeStatus + } else { + val selectedChainAsset = selectedChainAssetFlow.first() + feeChainAsset.emit(selectedChainAsset) + + loadFee(feeConstructor) + } + } + + private fun observeChainChanges() { + selectedTokenInfo.distinctUntilChangedBy { it.chain.id } + .onEach { + fee.value = feeFormatter.createLoadingStatus() + userModifiedFeeInCurrentChain.value = false + feeChainAsset.emit(it.chain.utilityAsset) + } + .launchIn(this) + } + + private fun observeFeeAssetChanges() { + feeChainAsset.distinctUntilChangedBy { it.fullId } + .onEach { + latestFeeConstructor?.let(::loadFee) + }.launchIn(this) + } + + + override fun changePaymentCurrencyClicked() { + launch { + val userCanChangeFee = userCanChangeFeeAsset.first() + if (!userCanChangeFee) return@launch + + val payload = constructChooseFeeCurrencyPayload() + val chosenFeeAsset = chooseFeeAsset.awaitAction(payload) + + feeChainAsset.emit(chosenFeeAsset) + userModifiedFeeInCurrentChain.value = true + } + } + + private suspend fun constructChooseFeeCurrencyPayload(): ChooseFeeCurrencyPayload { + val selectedTokenInfo = selectedTokenInfo.first() + val feeChainAsset = feeChainAsset.first() + + val availableFeeTokens = listOf(selectedTokenInfo.chain.utilityAsset, selectedTokenInfo.chainAsset) + return ChooseFeeCurrencyPayload( + selectedCommissionAsset = feeChainAsset, + availableAssets = availableFeeTokens + ) + } + + private fun Asset.canPayFee(fee: F): Boolean { + return transferableInPlanks >= feeBalanceExtractor.requiredBalanceToPayFee(fee, token.configuration) + } + + private suspend fun constructSelectedTokenInfo(chainAsset: Chain.Asset): SelectedAssetInfo { + val chain = chainRegistry.getChain(chainAsset.chainId) + val canPayFee = canPayFeeIn(chainAsset) + + return SelectedAssetInfo(chainAsset, chain, canPayFee) + } + + private suspend fun canPayFeeIn(chainAsset: Chain.Asset): Boolean { + return chainAsset.isCommissionAsset || interactor.canPayFeeInNonUtilityAsset(chainAsset, this) + } + + private fun FeeLoaderMixinV2.Configuration<*, *>.toFeeFormatterConfiguration(): FeeFormatter.Configuration { + return FeeFormatter.Configuration(showZeroFiat) + } + + private class SelectedAssetInfo( + val chainAsset: Chain.Asset, + val chain: Chain, + val feePaymentSupported: Boolean + ) +} diff --git a/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/presentation/mixin/fee/v2/FeeUI.kt b/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/presentation/mixin/fee/v2/FeeUI.kt new file mode 100644 index 0000000000..70699c55af --- /dev/null +++ b/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/presentation/mixin/fee/v2/FeeUI.kt @@ -0,0 +1,58 @@ +package io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.v2 + +import io.novafoundation.nova.common.base.BaseFragment +import io.novafoundation.nova.common.base.BaseViewModel +import io.novafoundation.nova.common.mixin.actionAwaitable.ActionAwaitableMixin.Action +import io.novafoundation.nova.common.mixin.impl.observeRetries +import io.novafoundation.nova.feature_account_api.presenatation.fee.select.FeeAssetSelectorBottomSheet +import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.model.ChooseFeeCurrencyPayload +import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.model.FeeDisplay +import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.model.FeeStatus +import io.novafoundation.nova.feature_wallet_api.presentation.view.FeeView +import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain + +// TODO We use here since star projections cause 1.7.21 compiler to fail +// https://youtrack.jetbrains.com/issue/KT-51277/NoSuchElementException-Collection-contains-no-element-matching-the-predicate-with-context-receivers-and-star-projection +// We can return to star projections after upgrading Kotlin to at least 1.8.20 +context(BaseFragment) +fun FeeLoaderMixinV2.setupFeeLoading( + setFeeStatus: (FeeStatus) -> Unit, + setUserCanChangeFeeAsset: (Boolean) -> Unit +) { + observeRetries(this) + + fee.observe(setFeeStatus) + + userCanChangeFeeAsset.observe(setUserCanChangeFeeAsset) + + chooseFeeAsset.awaitableActionLiveData.observeEvent { + openEditFeeBottomSheet(it) + } +} + +context(BaseFragment) +fun FeeLoaderMixinV2.setupFeeLoading(feeView: FeeView) { + setupFeeLoading( + setFeeStatus = { feeView.setFeeStatus(it) }, + setUserCanChangeFeeAsset = { + feeView.setFeeEditable(it) { + changePaymentCurrencyClicked() + } + } + ) +} + +context(BaseFragment) +private fun openEditFeeBottomSheet(action: Action) { + val payload = FeeAssetSelectorBottomSheet.Payload( + options = action.payload.availableAssets, + selectedOption = action.payload.selectedCommissionAsset + ) + + FeeAssetSelectorBottomSheet( + context = providedContext, + payload = payload, + onOptionClicked = action.onSuccess, + onCancel = action.onCancel + ).show() +} 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 deleted file mode 100644 index fbf30a1c31..0000000000 --- a/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/presentation/model/FeeModel.kt +++ /dev/null @@ -1,8 +0,0 @@ -package io.novafoundation.nova.feature_wallet_api.presentation.model - -import io.novafoundation.nova.feature_account_api.data.model.FeeBase - -class FeeModel( - val fee: F, - val display: AmountModel, -) diff --git a/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/presentation/view/FeeView.kt b/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/presentation/view/FeeView.kt index 9141904ae6..2c0da2c5cf 100644 --- a/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/presentation/view/FeeView.kt +++ b/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/presentation/view/FeeView.kt @@ -5,7 +5,8 @@ import android.util.AttributeSet import io.novafoundation.nova.common.R import io.novafoundation.nova.common.utils.setVisible import io.novafoundation.nova.common.view.TableCellView -import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.FeeStatus +import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.model.FeeDisplay +import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.model.FeeStatus class FeeView @JvmOverloads constructor( context: Context, @@ -15,16 +16,18 @@ class FeeView @JvmOverloads constructor( init { setTitle(R.string.network_fee) - - setFeeStatus(FeeStatus.Loading) } - fun setFeeStatus(feeStatus: FeeStatus<*>) { + fun setFeeStatus(feeStatus: FeeStatus<*, FeeDisplay>) { setVisible(feeStatus !is FeeStatus.NoFee) when (feeStatus) { is FeeStatus.Loading -> { - showProgress() + if (feeStatus.visibleDuringProgress) { + showProgress() + } else { + setVisible(false) + } } is FeeStatus.Error -> { @@ -32,13 +35,17 @@ class FeeView @JvmOverloads constructor( } is FeeStatus.Loaded -> { - showAmount(feeStatus.feeModel.display) + showFeeDisplay(feeStatus.feeModel.display) } FeeStatus.NoFee -> { } } } + private fun showFeeDisplay(feeDisplay: FeeDisplay) { + showValue(feeDisplay.title, feeDisplay.subtitle) + } + fun setFeeEditable(editable: Boolean, onEditTokenClick: OnClickListener) { if (editable) { setPrimaryValueStartIcon(R.drawable.ic_pencil_edit, R.color.icon_secondary) diff --git a/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/presentation/view/extrinsic/GenericExtrinsicInformationView.kt b/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/presentation/view/extrinsic/GenericExtrinsicInformationView.kt index ff3b83e503..0778a1e17f 100644 --- a/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/presentation/view/extrinsic/GenericExtrinsicInformationView.kt +++ b/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/presentation/view/extrinsic/GenericExtrinsicInformationView.kt @@ -9,7 +9,8 @@ import io.novafoundation.nova.feature_account_api.presenatation.account.wallet.W import io.novafoundation.nova.feature_account_api.presenatation.account.wallet.showWallet import io.novafoundation.nova.feature_account_api.view.showAddress import io.novafoundation.nova.feature_wallet_api.R -import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.FeeStatus +import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.model.FeeDisplay +import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.model.FeeStatus import io.novafoundation.nova.feature_wallet_api.presentation.view.FeeView import kotlinx.android.synthetic.main.view_generic_extrinsic_information.view.viewGenericExtrinsicInformationAccount import kotlinx.android.synthetic.main.view_generic_extrinsic_information.view.viewGenericExtrinsicInformationFee @@ -40,7 +41,7 @@ class GenericExtrinsicInformationView @JvmOverloads constructor( viewGenericExtrinsicInformationAccount.showAddress(addressModel) } - fun setFeeStatus(feeStatus: FeeStatus<*>) { + fun setFeeStatus(feeStatus: FeeStatus<*, FeeDisplay>) { viewGenericExtrinsicInformationFee.setFeeStatus(feeStatus) } } 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 e825fb8811..03e97559a6 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 @@ -6,14 +6,12 @@ import io.novafoundation.nova.feature_account_api.data.ethereum.transaction.into import io.novafoundation.nova.feature_account_api.data.extrinsic.ExtrinsicService import io.novafoundation.nova.feature_account_api.data.extrinsic.ExtrinsicSubmission import io.novafoundation.nova.feature_account_api.data.extrinsic.createDefault -import io.novafoundation.nova.feature_account_api.data.fee.toFeePaymentCurrency 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.AssetTransfers 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.commissionAsset 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.doNotCrossExistentialDepositInUsedAsset @@ -52,8 +50,7 @@ abstract class BaseAssetTransfers( protected abstract suspend fun transferFunctions(chainAsset: Chain.Asset): List> override suspend fun performTransfer(transfer: WeightedAssetTransfer, coroutineScope: CoroutineScope): Result { - val feePaymentCurrency = transfer.commissionAsset.toFeePaymentCurrency() - val submissionOptions = ExtrinsicService.SubmissionOptions(feePaymentCurrency) + val submissionOptions = ExtrinsicService.SubmissionOptions(transfer.feePaymentCurrency) return extrinsicServiceFactory .createDefault(coroutineScope) @@ -63,8 +60,7 @@ abstract class BaseAssetTransfers( } override suspend fun calculateFee(transfer: AssetTransfer, coroutineScope: CoroutineScope): Fee { - val feePaymentCurrency = transfer.commissionAsset.toFeePaymentCurrency() - val submissionOptions = ExtrinsicService.SubmissionOptions(feePaymentCurrency) + val submissionOptions = ExtrinsicService.SubmissionOptions(transfer.feePaymentCurrency) return extrinsicServiceFactory .createDefault(coroutineScope) 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 79cebde270..29617b6c0e 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 @@ -68,10 +68,9 @@ fun AssetTransfersValidationSystemBuilder.checkForFeeChanges( ) = checkForFeeChanges( calculateFee = { payload -> val transfers = assetSourceRegistry.sourceFor(payload.transfer.originChainAsset).transfers - val fee = transfers.calculateFee(payload.transfer, coroutineScope) - payload.originFee.copy(submissionFee = fee) + transfers.calculateFee(payload.transfer, coroutineScope) }, - currentFee = { it.originFee }, + currentFee = { it.originFee.submissionFee }, chainAsset = { it.commissionChainAsset }, error = AssetTransferValidationFailure::FeeChangeDetected ) diff --git a/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/data/repository/TokenRepositoryImpl.kt b/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/data/repository/TokenRepositoryImpl.kt index 2c11f472e6..e4b2c8c008 100644 --- a/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/data/repository/TokenRepositoryImpl.kt +++ b/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/data/repository/TokenRepositoryImpl.kt @@ -44,7 +44,7 @@ class TokenRepositoryImpl( override suspend fun getTokens(chainAssets: List): Map { if (chainAssets.isEmpty()) return emptyMap() - val symbols = chainAssets.mapToSet { it.symbol.value }.distinct() + val symbols = chainAssets.mapToSet { it.symbol.value }.toList() val tokens = tokenDao.getTokensWithCurrency(symbols) diff --git a/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/data/repository/WalletRepositoryImpl.kt b/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/data/repository/WalletRepositoryImpl.kt index 126f8231ad..1574d31dd7 100644 --- a/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/data/repository/WalletRepositoryImpl.kt +++ b/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/data/repository/WalletRepositoryImpl.kt @@ -8,6 +8,7 @@ import io.novafoundation.nova.core_db.model.PhishingAddressLocal import io.novafoundation.nova.core_db.model.TokenLocal import io.novafoundation.nova.core_db.model.operation.OperationBaseLocal import io.novafoundation.nova.core_db.model.operation.OperationLocal +import io.novafoundation.nova.feature_account_api.data.model.SubmissionFee import io.novafoundation.nova.feature_account_api.domain.interfaces.AccountRepository import io.novafoundation.nova.feature_account_api.domain.interfaces.findMetaAccountOrThrow import io.novafoundation.nova.feature_account_api.domain.model.requireAddressIn @@ -19,7 +20,6 @@ import io.novafoundation.nova.feature_wallet_api.data.source.CoinPriceRemoteData import io.novafoundation.nova.feature_wallet_api.domain.interfaces.WalletRepository import io.novafoundation.nova.feature_wallet_api.domain.model.Asset import io.novafoundation.nova.feature_wallet_api.domain.model.CoinRateChange -import io.novafoundation.nova.feature_wallet_api.domain.model.planksFromAmount import io.novafoundation.nova.feature_wallet_impl.data.mappers.mapAssetLocalToAsset import io.novafoundation.nova.feature_wallet_impl.data.network.blockchain.SubstrateRemoteSource import io.novafoundation.nova.feature_wallet_impl.data.network.phishing.PhishingApi @@ -39,7 +39,6 @@ import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.map import kotlinx.coroutines.withContext -import java.math.BigDecimal class WalletRepositoryImpl( private val substrateSource: SubstrateRemoteSource, @@ -171,7 +170,7 @@ class WalletRepositoryImpl( override suspend fun insertPendingTransfer( hash: String, assetTransfer: AssetTransfer, - fee: BigDecimal + fee: SubmissionFee ) { val operation = createAppOperation( hash = hash, @@ -210,7 +209,7 @@ class WalletRepositoryImpl( private fun createAppOperation( hash: String, transfer: AssetTransfer, - fee: BigDecimal, + fee: SubmissionFee, ): OperationLocal { val senderAddress = transfer.sender.requireAddressIn(transfer.originChain) @@ -222,7 +221,7 @@ class WalletRepositoryImpl( amount = transfer.amountInPlanks, senderAddress = senderAddress, receiverAddress = transfer.recipient, - fee = transfer.commissionAssetToken.planksFromAmount(fee), + fee = fee.amount, status = OperationBaseLocal.Status.PENDING, source = OperationBaseLocal.Source.APP ) 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 86ab51d0b9..51667eb49a 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 @@ -59,6 +59,8 @@ import io.novafoundation.nova.feature_wallet_api.presentation.mixin.amountChoose import io.novafoundation.nova.feature_wallet_api.presentation.mixin.amountChooser.AmountChooserProviderFactory import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.FeeLoaderMixin import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.provider.FeeLoaderProviderFactory +import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.v2.FeeLoaderMixinV2 +import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.v2.FeeLoaderV2Factory import io.novafoundation.nova.feature_wallet_impl.data.network.blockchain.SubstrateRemoteSource import io.novafoundation.nova.feature_wallet_impl.data.network.blockchain.WssSubstrateSource import io.novafoundation.nova.feature_wallet_impl.data.network.blockchain.assets.history.realtime.substrate.SubstrateRealtimeOperationFetcherFactory @@ -241,17 +243,20 @@ class WalletFeatureModule { @Provides @FeatureScope fun provideFeeLoaderMixinFactory( - customFeeInteractor: CustomFeeInteractor, - chainRegistry: ChainRegistry, resourceManager: ResourceManager, - actionAwaitableMixinFactory: ActionAwaitableMixin.Factory ): FeeLoaderMixin.Factory { - return FeeLoaderProviderFactory( - customFeeInteractor, - chainRegistry, - resourceManager, - actionAwaitableMixinFactory - ) + return FeeLoaderProviderFactory(resourceManager) + } + + @Provides + @FeatureScope + fun provideFeeLoaderV2MixinFactory( + chainRegistry: ChainRegistry, + actionAwaitableMixinFactory: ActionAwaitableMixin.Factory, + resourceManager: ResourceManager, + interactor: CustomFeeInteractor, + ): FeeLoaderMixinV2.Factory { + return FeeLoaderV2Factory(chainRegistry, actionAwaitableMixinFactory, resourceManager, interactor) } @Provides From 2b1db4f611367c40061e4ce2af7d62099435a010 Mon Sep 17 00:00:00 2001 From: Valentun Date: Thu, 31 Oct 2024 15:53:22 +0300 Subject: [PATCH 37/83] Fix cornercases on swap and send --- .../data/fee/FeePaymentCurrency.kt | 19 +++- .../presentation/send/common/fee/Factory.kt | 4 +- ...ceExtractor.kt => TransferFeeInspector.kt} | 8 +- .../send/confirm/ConfirmSendViewModel.kt | 8 +- .../domain/model/SwapQuoteArgs.kt | 5 +- .../presentation/state/SwapSettings.kt | 1 - .../presentation/state/SwapSettingsState.kt | 2 - .../feature_swap_impl/di/SwapFeatureModule.kt | 3 +- .../domain/swap/RealSwapService.kt | 4 +- ...alanceExtractor.kt => SwapFeeInspector.kt} | 8 +- .../confirmation/SwapConfirmationViewModel.kt | 15 +-- .../presentation/main/QuotingState.kt | 3 +- .../main/SwapMainSettingsViewModel.kt | 22 ++-- .../maxAction/MaxActionProviderFactory.kt | 2 +- .../state/RealSwapSettingsState.kt | 29 +---- .../state/SwapSettingsStateProvider.kt | 4 +- ...nceExtractor.kt => DefaultFeeInspector.kt} | 6 +- ...FeeBalanceExtractor.kt => FeeInspector.kt} | 4 +- .../presentation/mixin/fee/model/FeeStatus.kt | 6 + .../mixin/fee/v2/FeeLoaderMixinV2.kt | 13 ++- .../mixin/fee/v2/FeeLoaderV2Factory.kt | 6 +- .../mixin/fee/v2/FeeLoaderV2Provider.kt | 105 ++++++++++++------ 22 files changed, 165 insertions(+), 112 deletions(-) rename feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/send/common/fee/{TransferFeeBalanceExtractor.kt => TransferFeeInspector.kt} (71%) rename feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/common/fee/{SwapFeeBalanceExtractor.kt => SwapFeeInspector.kt} (74%) rename feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/presentation/mixin/fee/amount/{DefaultFeeBalanceExtractor.kt => DefaultFeeInspector.kt} (76%) rename feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/presentation/mixin/fee/amount/{FeeBalanceExtractor.kt => FeeInspector.kt} (79%) diff --git a/feature-account-api/src/main/java/io/novafoundation/nova/feature_account_api/data/fee/FeePaymentCurrency.kt b/feature-account-api/src/main/java/io/novafoundation/nova/feature_account_api/data/fee/FeePaymentCurrency.kt index cbbd741f86..cc332d6c4b 100644 --- a/feature-account-api/src/main/java/io/novafoundation/nova/feature_account_api/data/fee/FeePaymentCurrency.kt +++ b/feature-account-api/src/main/java/io/novafoundation/nova/feature_account_api/data/fee/FeePaymentCurrency.kt @@ -2,6 +2,7 @@ package io.novafoundation.nova.feature_account_api.data.fee import io.novafoundation.nova.runtime.ext.fullId import io.novafoundation.nova.runtime.ext.isCommissionAsset +import io.novafoundation.nova.runtime.ext.utilityAsset import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain sealed interface FeePaymentCurrency { @@ -9,7 +10,12 @@ sealed interface FeePaymentCurrency { /** * Use native currency of the chain to pay the fee */ - object Native : FeePaymentCurrency + object Native : FeePaymentCurrency { + + override fun toString(): String { + return "Native" + } + } /** * Request to use a specific [asset] for payment fees @@ -28,6 +34,10 @@ sealed interface FeePaymentCurrency { override fun hashCode(): Int { return asset.hashCode() } + + override fun toString(): String { + return "Asset(${asset.symbol})" + } } companion object @@ -39,3 +49,10 @@ fun Chain.Asset.toFeePaymentCurrency(): FeePaymentCurrency { else -> FeePaymentCurrency.Asset(this) } } + +fun FeePaymentCurrency.toChainAsset(chain: Chain): Chain.Asset { + return when (this) { + is FeePaymentCurrency.Asset -> asset + FeePaymentCurrency.Native -> chain.utilityAsset + } +} diff --git a/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/send/common/fee/Factory.kt b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/send/common/fee/Factory.kt index 475327c608..00594a3075 100644 --- a/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/send/common/fee/Factory.kt +++ b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/send/common/fee/Factory.kt @@ -13,11 +13,13 @@ context(BaseViewModel) fun FeeLoaderMixinV2.Factory.createForTransfer( originChainAsset: Flow, formatter: TransferFeeDisplayFormatter, + configuration: FeeLoaderMixinV2.Configuration = FeeLoaderMixinV2.Configuration() ): TransferFeeLoaderMixin { return create( scope = viewModelScope, selectedChainAssetFlow = originChainAsset, feeFormatter = formatter, - feeBalanceExtractor = TransferFeeBalanceExtractor(), + feeInspector = TransferFeeInspector(), + configuration = configuration ) } diff --git a/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/send/common/fee/TransferFeeBalanceExtractor.kt b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/send/common/fee/TransferFeeInspector.kt similarity index 71% rename from feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/send/common/fee/TransferFeeBalanceExtractor.kt rename to feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/send/common/fee/TransferFeeInspector.kt index e425ed780f..9c71fb26c2 100644 --- a/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/send/common/fee/TransferFeeBalanceExtractor.kt +++ b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/send/common/fee/TransferFeeInspector.kt @@ -2,12 +2,16 @@ package io.novafoundation.nova.feature_assets.presentation.send.common.fee import io.novafoundation.nova.feature_assets.domain.send.model.TransferFee import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.types.Balance -import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.amount.FeeBalanceExtractor +import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.amount.FeeInspector import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain -class TransferFeeBalanceExtractor : FeeBalanceExtractor { +class TransferFeeInspector : FeeInspector { override fun requiredBalanceToPayFee(fee: TransferFee, chainAsset: Chain.Asset): Balance { return fee.totalFeeByExecutingAccount(chainAsset) } + + override fun getSubmissionFeeAsset(fee: TransferFee): Chain.Asset { + return fee.originFee.submissionFee.asset + } } 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 97091a26ff..9cb4860631 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 @@ -40,6 +40,7 @@ import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.t import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.tranfers.WeightedAssetTransfer import io.novafoundation.nova.feature_wallet_api.domain.model.OriginFee import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.v2.FeeLoaderMixinV2 +import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.v2.FeeLoaderMixinV2.Configuration import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.v2.awaitFee import io.novafoundation.nova.feature_wallet_api.presentation.model.AmountSign import io.novafoundation.nova.feature_wallet_api.presentation.model.AssetPayload @@ -99,7 +100,12 @@ class ConfirmSendViewModel( private val formatter = TransferFeeDisplayFormatter(crossChainFeeShown = isCrossChain) val feeMixin = feeLoaderMixinFactory.createForTransfer( originChainAsset = flowOf { originAsset() }, - formatter = formatter + formatter = formatter, + configuration = Configuration( + initialState = Configuration.InitialState( + feePaymentCurrency = transferDraft.feePaymentCurrency.toDomain() + ) + ) ) val hintsMixin = hintsFactory.create(this) diff --git a/feature-swap-api/src/main/java/io/novafoundation/nova/feature_swap_api/domain/model/SwapQuoteArgs.kt b/feature-swap-api/src/main/java/io/novafoundation/nova/feature_swap_api/domain/model/SwapQuoteArgs.kt index 63c13587b7..ae90f1488a 100644 --- a/feature-swap-api/src/main/java/io/novafoundation/nova/feature_swap_api/domain/model/SwapQuoteArgs.kt +++ b/feature-swap-api/src/main/java/io/novafoundation/nova/feature_swap_api/domain/model/SwapQuoteArgs.kt @@ -6,6 +6,7 @@ import io.novafoundation.nova.common.utils.atLeastZero import io.novafoundation.nova.common.utils.divideToDecimal import io.novafoundation.nova.common.utils.graph.Path import io.novafoundation.nova.common.utils.isZero +import io.novafoundation.nova.feature_account_api.data.fee.FeePaymentCurrency import io.novafoundation.nova.feature_swap_core_api.data.paths.model.QuotedEdge import io.novafoundation.nova.feature_swap_core_api.data.primitive.model.SwapDirection import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.types.Balance @@ -25,7 +26,7 @@ open class SwapFeeArgs( val slippage: Fraction, val executionPath: Path, val direction: SwapDirection, - val firstSegmentFees: Chain.Asset + val firstSegmentFees: FeePaymentCurrency ) class SegmentExecuteArgs( @@ -111,7 +112,7 @@ private fun SwapLimit.SpecifiedOut.updateInAmount(newAmountInQuote: Balance): Sw ) } -fun SwapQuote.toExecuteArgs(slippage: Fraction, firstSegmentFees: Chain.Asset): SwapFeeArgs { +fun SwapQuote.toExecuteArgs(slippage: Fraction, firstSegmentFees: FeePaymentCurrency): SwapFeeArgs { return SwapFeeArgs( assetIn = amountIn.chainAsset, slippage = slippage, diff --git a/feature-swap-api/src/main/java/io/novafoundation/nova/feature_swap_api/presentation/state/SwapSettings.kt b/feature-swap-api/src/main/java/io/novafoundation/nova/feature_swap_api/presentation/state/SwapSettings.kt index ce0915d3c1..96a2b56032 100644 --- a/feature-swap-api/src/main/java/io/novafoundation/nova/feature_swap_api/presentation/state/SwapSettings.kt +++ b/feature-swap-api/src/main/java/io/novafoundation/nova/feature_swap_api/presentation/state/SwapSettings.kt @@ -11,7 +11,6 @@ val DEFAULT_SLIPPAGE = 0.5.percents data class SwapSettings( val assetIn: Chain.Asset? = null, val assetOut: Chain.Asset? = null, - val feeAsset: Chain.Asset? = null, val amount: Balance? = null, val swapDirection: SwapDirection? = null, val slippage: Fraction diff --git a/feature-swap-api/src/main/java/io/novafoundation/nova/feature_swap_api/presentation/state/SwapSettingsState.kt b/feature-swap-api/src/main/java/io/novafoundation/nova/feature_swap_api/presentation/state/SwapSettingsState.kt index 7244c76c35..0587f53b83 100644 --- a/feature-swap-api/src/main/java/io/novafoundation/nova/feature_swap_api/presentation/state/SwapSettingsState.kt +++ b/feature-swap-api/src/main/java/io/novafoundation/nova/feature_swap_api/presentation/state/SwapSettingsState.kt @@ -12,8 +12,6 @@ interface SwapSettingsState : SelectedOptionSharedState { fun setAssetOut(asset: Chain.Asset) - fun setFeeAsset(asset: Chain.Asset) - fun setAmount(amount: Balance?, swapDirection: SwapDirection) fun setSlippage(slippage: Fraction) diff --git a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/di/SwapFeatureModule.kt b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/di/SwapFeatureModule.kt index 09c7d0dd8c..c01f32816f 100644 --- a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/di/SwapFeatureModule.kt +++ b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/di/SwapFeatureModule.kt @@ -140,9 +140,8 @@ class SwapFeatureModule { @FeatureScope fun provideSwapSettingsStateProvider( computationalCache: ComputationalCache, - chainRegistry: ChainRegistry ): SwapSettingsStateProvider { - return RealSwapSettingsStateProvider(computationalCache, chainRegistry) + return RealSwapSettingsStateProvider(computationalCache) } @Provides diff --git a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/domain/swap/RealSwapService.kt b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/domain/swap/RealSwapService.kt index e53dc99383..6867b5406b 100644 --- a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/domain/swap/RealSwapService.kt +++ b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/domain/swap/RealSwapService.kt @@ -299,10 +299,10 @@ internal class RealSwapService( private suspend fun SwapGraphEdge.identifySegmentCurrency( isFirstSegment: Boolean, - firstSegmentFees: Chain.Asset + firstSegmentFees: FeePaymentCurrency ): FeePaymentCurrency { return if (isFirstSegment) { - firstSegmentFees.toFeePaymentCurrency() + firstSegmentFees } else { // When executing intermediate segments, always pay in sending asset chainRegistry.asset(from).toFeePaymentCurrency() diff --git a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/common/fee/SwapFeeBalanceExtractor.kt b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/common/fee/SwapFeeInspector.kt similarity index 74% rename from feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/common/fee/SwapFeeBalanceExtractor.kt rename to feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/common/fee/SwapFeeInspector.kt index 73be76f8ca..90840aa866 100644 --- a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/common/fee/SwapFeeBalanceExtractor.kt +++ b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/common/fee/SwapFeeInspector.kt @@ -2,12 +2,16 @@ package io.novafoundation.nova.feature_swap_impl.presentation.common.fee import io.novafoundation.nova.feature_swap_api.domain.model.SwapFee import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.types.Balance -import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.amount.FeeBalanceExtractor +import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.amount.FeeInspector import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain -class SwapFeeBalanceExtractor : FeeBalanceExtractor { +class SwapFeeInspector : FeeInspector { override fun requiredBalanceToPayFee(fee: SwapFee, chainAsset: Chain.Asset): Balance { return fee.maxAmountDeductionFor(chainAsset) } + + override fun getSubmissionFeeAsset(fee: SwapFee): Chain.Asset { + return fee.asset + } } diff --git a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/confirmation/SwapConfirmationViewModel.kt b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/confirmation/SwapConfirmationViewModel.kt index 72740f2328..b283d6e20f 100644 --- a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/confirmation/SwapConfirmationViewModel.kt +++ b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/confirmation/SwapConfirmationViewModel.kt @@ -39,8 +39,8 @@ import io.novafoundation.nova.feature_swap_impl.domain.interactor.SwapInteractor import io.novafoundation.nova.feature_swap_impl.presentation.SwapRouter import io.novafoundation.nova.feature_swap_impl.presentation.common.PriceImpactFormatter import io.novafoundation.nova.feature_swap_impl.presentation.common.SlippageAlertMixinFactory -import io.novafoundation.nova.feature_swap_impl.presentation.common.fee.SwapFeeBalanceExtractor import io.novafoundation.nova.feature_swap_impl.presentation.common.fee.SwapFeeFormatter +import io.novafoundation.nova.feature_swap_impl.presentation.common.fee.SwapFeeInspector import io.novafoundation.nova.feature_swap_impl.presentation.common.state.SwapStateStoreProvider import io.novafoundation.nova.feature_swap_impl.presentation.common.state.getStateOrThrow import io.novafoundation.nova.feature_swap_impl.presentation.confirmation.model.SwapConfirmationDetailsModel @@ -155,7 +155,7 @@ class SwapConfirmationViewModel( scope = viewModelScope, selectedChainAssetFlow = initialSwapState.map { it.quote.assetIn }, feeFormatter = SwapFeeFormatter(swapInteractor), - feeBalanceExtractor = SwapFeeBalanceExtractor(), + feeInspector = SwapFeeInspector(), ) private val maxActionProvider = createMaxActionProvider() @@ -365,12 +365,13 @@ class SwapConfirmationViewModel( .onFailure { } .getOrNull() ?: return@launch - val executeArgs = swapQuote.toExecuteArgs( - slippage = slippageFlow.first(), - firstSegmentFees = initialSwapState.first().fee.intermediateSegmentFeesInAssetIn.asset - ) - feeMixin.loadFee { + feeMixin.loadFee { feePaymentCurrency -> + val executeArgs = swapQuote.toExecuteArgs( + slippage = slippageFlow.first(), + firstSegmentFees = feePaymentCurrency + ) + swapInteractor.estimateFee(executeArgs) } diff --git a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/main/QuotingState.kt b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/main/QuotingState.kt index 96b3edca35..ecff0b1953 100644 --- a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/main/QuotingState.kt +++ b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/main/QuotingState.kt @@ -2,7 +2,6 @@ package io.novafoundation.nova.feature_swap_impl.presentation.main import io.novafoundation.nova.feature_swap_api.domain.model.SwapQuote import io.novafoundation.nova.feature_swap_api.domain.model.SwapQuoteArgs -import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain sealed class QuotingState { @@ -12,5 +11,5 @@ sealed class QuotingState { object NotAvailable : QuotingState() - data class Loaded(val value: SwapQuote, val quoteArgs: SwapQuoteArgs, val firstSegmentFeeAsset: Chain.Asset) : QuotingState() + data class Loaded(val value: SwapQuote, val quoteArgs: SwapQuoteArgs) : QuotingState() } diff --git a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/main/SwapMainSettingsViewModel.kt b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/main/SwapMainSettingsViewModel.kt index 4d82c939eb..c74b52d06e 100644 --- a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/main/SwapMainSettingsViewModel.kt +++ b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/main/SwapMainSettingsViewModel.kt @@ -50,8 +50,8 @@ import io.novafoundation.nova.feature_swap_impl.R import io.novafoundation.nova.feature_swap_impl.domain.interactor.SwapInteractor import io.novafoundation.nova.feature_swap_impl.domain.model.GetAssetInOption import io.novafoundation.nova.feature_swap_impl.presentation.SwapRouter -import io.novafoundation.nova.feature_swap_impl.presentation.common.fee.SwapFeeBalanceExtractor import io.novafoundation.nova.feature_swap_impl.presentation.common.fee.SwapFeeFormatter +import io.novafoundation.nova.feature_swap_impl.presentation.common.fee.SwapFeeInspector import io.novafoundation.nova.feature_swap_impl.presentation.common.state.SwapState import io.novafoundation.nova.feature_swap_impl.presentation.common.state.SwapStateStoreProvider import io.novafoundation.nova.feature_swap_impl.presentation.fieldValidation.EnoughAmountToSwapValidatorFactory @@ -77,6 +77,7 @@ import io.novafoundation.nova.feature_wallet_api.presentation.mixin.amountChoose import io.novafoundation.nova.feature_wallet_api.presentation.mixin.amountChooser.setAmount import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.model.FeeDisplay import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.model.FeeStatus +import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.model.PaymentCurrencySelectionMode import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.v2.FeeLoaderMixinV2 import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.v2.FeeLoaderMixinV2.Configuration import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.v2.awaitFee @@ -173,10 +174,10 @@ class SwapMainSettingsViewModel( scope = viewModelScope, selectedChainAssetFlow = swapSettings.mapNotNull { it.assetIn }, feeFormatter = SwapFeeFormatter(swapInteractor), - feeBalanceExtractor = SwapFeeBalanceExtractor(), + feeInspector = SwapFeeInspector(), configuration = Configuration( initialState = Configuration.InitialState( - feeStatus = FeeStatus.NoFee, + paymentCurrencySelectionMode = PaymentCurrencySelectionMode.AUTOMATIC_ONLY ) ) ) @@ -432,7 +433,6 @@ class SwapMainSettingsViewModel( assetOut = assetOut, amount = payload.amount, swapDirection = direction, - feeAsset = chainRegistry.asset(payload.feeAsset.fullChainAssetId), slippage = oldSwapSettings.slippage ) @@ -481,12 +481,14 @@ class SwapMainSettingsViewModel( } } .onEach { quoteState -> - val swapArgs = quoteState.value.toExecuteArgs( - slippage = swapSettings.first().slippage, - firstSegmentFees = quoteState.firstSegmentFeeAsset - ) + loadFee { feePaymentCurrency -> + val swapArgs = quoteState.value.toExecuteArgs( + slippage = swapSettings.first().slippage, + firstSegmentFees = feePaymentCurrency + ) - loadFee { swapInteractor.estimateFee(swapArgs) } + swapInteractor.estimateFee(swapArgs) + } } .inBackground() .launchIn(viewModelScope) @@ -588,7 +590,7 @@ class SwapMainSettingsViewModel( val quote = swapInteractor.quote(swapQuoteArgs, viewModelScope) quotingState.value = quote.fold( - onSuccess = { QuotingState.Loaded(it, swapQuoteArgs, swapSettings.feeAsset!!) }, + onSuccess = { QuotingState.Loaded(it, swapQuoteArgs) }, onFailure = { if (it is CancellationException) { QuotingState.Loading diff --git a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/mixin/maxAction/MaxActionProviderFactory.kt b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/mixin/maxAction/MaxActionProviderFactory.kt index 67ad54d290..c54bfa5462 100644 --- a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/mixin/maxAction/MaxActionProviderFactory.kt +++ b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/mixin/maxAction/MaxActionProviderFactory.kt @@ -25,7 +25,7 @@ class MaxActionProviderFactory( ): MaxActionProvider { return assetInFlow.providingMaxOf(field, allowMaxAction) .deductFee(feeLoaderMixin) - .disallowReapingIfHasDependents(assetOutFlow, assetSourceRegistry, chainRegistry) +// .disallowReapingIfHasDependents(assetOutFlow, assetSourceRegistry, chainRegistry) } } diff --git a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/state/RealSwapSettingsState.kt b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/state/RealSwapSettingsState.kt index 116d0b19e5..5381053a6a 100644 --- a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/state/RealSwapSettingsState.kt +++ b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/state/RealSwapSettingsState.kt @@ -1,22 +1,17 @@ package io.novafoundation.nova.feature_swap_impl.presentation.state import io.novafoundation.nova.common.utils.Fraction -import io.novafoundation.nova.common.utils.Percent -import io.novafoundation.nova.feature_swap_core_api.data.primitive.model.SwapDirection -import io.novafoundation.nova.feature_swap_core_api.data.primitive.model.flip import io.novafoundation.nova.feature_swap_api.presentation.state.SwapSettings import io.novafoundation.nova.feature_swap_api.presentation.state.SwapSettingsState +import io.novafoundation.nova.feature_swap_core_api.data.primitive.model.SwapDirection +import io.novafoundation.nova.feature_swap_core_api.data.primitive.model.flip import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.types.Balance import io.novafoundation.nova.feature_wallet_api.domain.model.amountFromPlanks import io.novafoundation.nova.feature_wallet_api.domain.model.planksFromAmount -import io.novafoundation.nova.runtime.ext.commissionAsset -import io.novafoundation.nova.runtime.ext.isCommissionAsset -import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain import kotlinx.coroutines.flow.MutableStateFlow class RealSwapSettingsState( - private val chainRegistry: ChainRegistry, initialValue: SwapSettings, ) : SwapSettingsState { @@ -25,18 +20,8 @@ class RealSwapSettingsState( override suspend fun setAssetInUpdatingFee(asset: Chain.Asset) { val current = selectedOption.value - val chain = chainRegistry.getChain(asset.chainId) - val newPlanks = current.convertedAmountForNewAssetIn(asset) - - val feeIsNotCommissionAsset = current.feeAsset?.isCommissionAsset == false - val feeIsInAnotherChain = current.feeAsset?.chainId != chain.id - val needToResetFeeToNative = feeIsNotCommissionAsset || feeIsInAnotherChain - val new = if (current.feeAsset == null || needToResetFeeToNative) { - current.copy(assetIn = asset, feeAsset = chain.commissionAsset, amount = newPlanks) - } else { - current.copy(assetIn = asset, amount = newPlanks) - } + val new = current.copy(assetIn = asset, amount = newPlanks) selectedOption.value = new } @@ -49,10 +34,6 @@ class RealSwapSettingsState( selectedOption.value = selectedOption.value.copy(assetOut = asset, amount = newPlanks) } - override fun setFeeAsset(asset: Chain.Asset) { - selectedOption.value = selectedOption.value.copy(feeAsset = asset) - } - override fun setAmount(amount: Balance?, swapDirection: SwapDirection) { selectedOption.value = selectedOption.value.copy(amount = amount, swapDirection = swapDirection) } @@ -64,13 +45,9 @@ class RealSwapSettingsState( override suspend fun flipAssets(): SwapSettings { val currentSettings = selectedOption.value - val newAssetIn = currentSettings.assetOut - val chain = newAssetIn?.chainId?.let { chainRegistry.getChain(it) } - val newSettings = currentSettings.copy( assetIn = currentSettings.assetOut, assetOut = currentSettings.assetIn, - feeAsset = chain?.commissionAsset, // we reset commission asset during flipping to ensure we only allow to pay in commissionAsset or assetIn swapDirection = currentSettings.swapDirection?.flip() ) selectedOption.value = newSettings diff --git a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/state/SwapSettingsStateProvider.kt b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/state/SwapSettingsStateProvider.kt index e59389d6e1..0c8f220776 100644 --- a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/state/SwapSettingsStateProvider.kt +++ b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/state/SwapSettingsStateProvider.kt @@ -5,7 +5,6 @@ import io.novafoundation.nova.common.utils.flowOfAll import io.novafoundation.nova.feature_swap_api.presentation.state.DEFAULT_SLIPPAGE import io.novafoundation.nova.feature_swap_api.presentation.state.SwapSettings import io.novafoundation.nova.feature_swap_api.presentation.state.SwapSettingsStateProvider -import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.Flow @@ -17,12 +16,11 @@ fun SwapSettingsStateProvider.swapSettingsFlow(coroutineScope: CoroutineScope): class RealSwapSettingsStateProvider( private val computationalCache: ComputationalCache, - private val chainRegistry: ChainRegistry ) : SwapSettingsStateProvider { override suspend fun getSwapSettingsState(coroutineScope: CoroutineScope): RealSwapSettingsState { return computationalCache.useCache("SwapSettingsState", coroutineScope) { - RealSwapSettingsState(chainRegistry, SwapSettings(slippage = DEFAULT_SLIPPAGE)) + RealSwapSettingsState(SwapSettings(slippage = DEFAULT_SLIPPAGE)) } } } diff --git a/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/presentation/mixin/fee/amount/DefaultFeeBalanceExtractor.kt b/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/presentation/mixin/fee/amount/DefaultFeeInspector.kt similarity index 76% rename from feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/presentation/mixin/fee/amount/DefaultFeeBalanceExtractor.kt rename to feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/presentation/mixin/fee/amount/DefaultFeeInspector.kt index ad18782db7..cf9c6e11ce 100644 --- a/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/presentation/mixin/fee/amount/DefaultFeeBalanceExtractor.kt +++ b/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/presentation/mixin/fee/amount/DefaultFeeInspector.kt @@ -5,9 +5,13 @@ import io.novafoundation.nova.feature_account_api.data.model.getAmount import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.types.Balance import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain -class DefaultFeeBalanceExtractor : FeeBalanceExtractor { +class DefaultFeeInspector : FeeInspector { override fun requiredBalanceToPayFee(fee: F, chainAsset: Chain.Asset): Balance { return fee.getAmount(chainAsset) } + + override fun getSubmissionFeeAsset(fee: F): Chain.Asset { + return fee.asset + } } diff --git a/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/presentation/mixin/fee/amount/FeeBalanceExtractor.kt b/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/presentation/mixin/fee/amount/FeeInspector.kt similarity index 79% rename from feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/presentation/mixin/fee/amount/FeeBalanceExtractor.kt rename to feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/presentation/mixin/fee/amount/FeeInspector.kt index 1c46a6198d..70d942c412 100644 --- a/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/presentation/mixin/fee/amount/FeeBalanceExtractor.kt +++ b/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/presentation/mixin/fee/amount/FeeInspector.kt @@ -3,7 +3,9 @@ package io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.amount import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.types.Balance import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain -interface FeeBalanceExtractor { +interface FeeInspector { fun requiredBalanceToPayFee(fee: F, chainAsset: Chain.Asset): Balance + + fun getSubmissionFeeAsset(fee: F): Chain.Asset } diff --git a/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/presentation/mixin/fee/model/FeeStatus.kt b/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/presentation/mixin/fee/model/FeeStatus.kt index f8b19eb9ac..8c5aaa6078 100644 --- a/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/presentation/mixin/fee/model/FeeStatus.kt +++ b/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/presentation/mixin/fee/model/FeeStatus.kt @@ -41,3 +41,9 @@ fun FeeStatus.mapProgress(map: (Boolean) -> Boolean): FeeStatus FeeStatus.NoFee } } + +inline fun FeeStatus.onLoaded(action: (FeeModel) -> Unit) { + if (this is FeeStatus.Loaded) { + action(feeModel) + } +} diff --git a/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/presentation/mixin/fee/v2/FeeLoaderMixinV2.kt b/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/presentation/mixin/fee/v2/FeeLoaderMixinV2.kt index 49852a0bd5..de5fa66a39 100644 --- a/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/presentation/mixin/fee/v2/FeeLoaderMixinV2.kt +++ b/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/presentation/mixin/fee/v2/FeeLoaderMixinV2.kt @@ -6,8 +6,8 @@ import io.novafoundation.nova.feature_account_api.data.fee.FeePaymentCurrency import io.novafoundation.nova.feature_account_api.data.model.FeeBase import io.novafoundation.nova.feature_wallet_api.domain.model.Asset import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.SetFee -import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.amount.DefaultFeeBalanceExtractor -import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.amount.FeeBalanceExtractor +import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.amount.DefaultFeeInspector +import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.amount.FeeInspector import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.formatter.DefaultFeeFormatter import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.formatter.FeeFormatter import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.model.ChooseFeeCurrencyPayload @@ -30,8 +30,9 @@ interface FeeLoaderMixinV2 : Retriable { ) { class InitialState( + val feePaymentCurrency: FeePaymentCurrency = FeePaymentCurrency.Native, val paymentCurrencySelectionMode: PaymentCurrencySelectionMode = PaymentCurrencySelectionMode.DISABLED, - val feeStatus: FeeStatus? = null + val feeStatus: FeeStatus = FeeStatus.NoFee ) } @@ -47,6 +48,8 @@ interface FeeLoaderMixinV2 : Retriable { suspend fun feeAsset(): Asset + val feeChainAssetFlow: Flow + suspend fun feePaymentCurrency(): FeePaymentCurrency fun loadFee(feeConstructor: FeeConstructor) @@ -66,7 +69,7 @@ interface FeeLoaderMixinV2 : Retriable { scope: CoroutineScope, selectedChainAssetFlow: Flow, feeFormatter: FeeFormatter, - feeBalanceExtractor: FeeBalanceExtractor, + feeInspector: FeeInspector, configuration: Configuration = Configuration() ): Presentation } @@ -83,7 +86,7 @@ fun Factory.createDefault( scope = scope, selectedChainAssetFlow = selectedChainAssetFlow, feeFormatter = DefaultFeeFormatter(), - feeBalanceExtractor = DefaultFeeBalanceExtractor(), + feeInspector = DefaultFeeInspector(), configuration = configuration ) } diff --git a/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/presentation/mixin/fee/v2/FeeLoaderV2Factory.kt b/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/presentation/mixin/fee/v2/FeeLoaderV2Factory.kt index 9ce940527a..4f8ec13c2a 100644 --- a/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/presentation/mixin/fee/v2/FeeLoaderV2Factory.kt +++ b/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/presentation/mixin/fee/v2/FeeLoaderV2Factory.kt @@ -3,7 +3,7 @@ package io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.v2 import io.novafoundation.nova.common.mixin.actionAwaitable.ActionAwaitableMixin import io.novafoundation.nova.common.resources.ResourceManager import io.novafoundation.nova.feature_wallet_api.domain.fee.CustomFeeInteractor -import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.amount.FeeBalanceExtractor +import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.amount.FeeInspector import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.formatter.FeeFormatter import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain @@ -21,7 +21,7 @@ class FeeLoaderV2Factory( scope: CoroutineScope, selectedChainAssetFlow: Flow, feeFormatter: FeeFormatter, - feeBalanceExtractor: FeeBalanceExtractor, + feeInspector: FeeInspector, configuration: FeeLoaderMixinV2.Configuration ): FeeLoaderMixinV2.Presentation { return FeeLoaderV2Provider( @@ -31,7 +31,7 @@ class FeeLoaderV2Factory( interactor = interactor, feeFormatter = feeFormatter, configuration = configuration, - feeBalanceExtractor = feeBalanceExtractor, + feeInspector = feeInspector, selectedChainAssetFlow = selectedChainAssetFlow, coroutineScope = scope ) diff --git a/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/presentation/mixin/fee/v2/FeeLoaderV2Provider.kt b/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/presentation/mixin/fee/v2/FeeLoaderV2Provider.kt index be13ebe9f2..32a5009ec4 100644 --- a/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/presentation/mixin/fee/v2/FeeLoaderV2Provider.kt +++ b/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/presentation/mixin/fee/v2/FeeLoaderV2Provider.kt @@ -1,25 +1,29 @@ package io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.v2 +import android.util.Log import androidx.lifecycle.MutableLiveData import io.novafoundation.nova.common.mixin.actionAwaitable.ActionAwaitableMixin import io.novafoundation.nova.common.mixin.api.RetryPayload import io.novafoundation.nova.common.resources.ResourceManager import io.novafoundation.nova.common.utils.Event +import io.novafoundation.nova.common.utils.LOG_TAG import io.novafoundation.nova.common.utils.shareInBackground import io.novafoundation.nova.common.utils.singleReplaySharedFlow import io.novafoundation.nova.feature_account_api.data.fee.FeePaymentCurrency +import io.novafoundation.nova.feature_account_api.data.fee.toChainAsset import io.novafoundation.nova.feature_account_api.data.fee.toFeePaymentCurrency import io.novafoundation.nova.feature_wallet_api.R import io.novafoundation.nova.feature_wallet_api.domain.fee.CustomFeeInteractor 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.presentation.mixin.fee.amount.FeeBalanceExtractor +import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.amount.FeeInspector import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.formatter.FeeFormatter import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.formatter.formatFeeStatus import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.model.ChooseFeeCurrencyPayload import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.model.FeeStatus import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.model.PaymentCurrencySelectionMode import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.model.automaticChangeEnabled +import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.model.onLoaded import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.model.onlyNativeFeeEnabled import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.model.userCanChangeFee import io.novafoundation.nova.runtime.ext.fullId @@ -41,12 +45,13 @@ import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.flow.withIndex import kotlinx.coroutines.launch import kotlinx.coroutines.suspendCancellableCoroutine import kotlin.coroutines.cancellation.CancellationException import kotlin.coroutines.resume -class FeeLoaderV2Provider( +internal class FeeLoaderV2Provider( private val chainRegistry: ChainRegistry, private val actionAwaitableMixinFactory: ActionAwaitableMixin.Factory, private val resourceManager: ResourceManager, @@ -54,7 +59,7 @@ class FeeLoaderV2Provider( private val feeFormatter: FeeFormatter, private val configuration: FeeLoaderMixinV2.Configuration, - private val feeBalanceExtractor: FeeBalanceExtractor, + private val feeInspector: FeeInspector, private val selectedChainAssetFlow: Flow, coroutineScope: CoroutineScope ) : FeeLoaderMixinV2.Presentation, CoroutineScope by coroutineScope, FeeFormatter.Context { @@ -68,18 +73,18 @@ class FeeLoaderV2Provider( private val paymentCurrencySelectionModeFlow = MutableStateFlow(configuration.initialState.paymentCurrencySelectionMode) - private val feeChainAsset = singleReplaySharedFlow() + override val feeChainAssetFlow = singleReplaySharedFlow() - private val feeAsset: Flow = feeChainAsset + private val feeAsset: Flow = feeChainAssetFlow .distinctUntilChangedBy { it.fullId } .flatMapLatest { interactor.assetFlow(it) } .shareInBackground() - private val userModifiedFeeInCurrentChain = MutableStateFlow(false) + private val userModifiedFeeInCurrentAsset = MutableStateFlow(false) - private val canSwitchToSelectedToken = combine(selectedTokenInfo, feeChainAsset) { selectedTokenInfo, feeAsset -> + private val canSwitchToSelectedToken = combine(selectedTokenInfo, feeChainAssetFlow) { selectedTokenInfo, feeAsset -> val canSwitchToSelected = feeAsset.fullId != selectedTokenInfo.chainAsset.fullId - val selectedCanBeUsed = selectedTokenInfo.feePaymentSupported + val selectedCanBeUsed = selectedTokenInfo.feePaymentSupported canSwitchToSelected && selectedCanBeUsed } @@ -87,7 +92,7 @@ class FeeLoaderV2Provider( .shareInBackground() private val canChangeFeeAutomatically = combine( - userModifiedFeeInCurrentChain, + userModifiedFeeInCurrentAsset, paymentCurrencySelectionModeFlow, canSwitchToSelectedToken ) { userModifiedFee, selectionMode, canSwitchToSelected -> @@ -103,7 +108,7 @@ class FeeLoaderV2Provider( override val chooseFeeAsset = actionAwaitableMixinFactory.create() - override val fee = MutableStateFlow(configuration.initialState.feeStatus ?: FeeStatus.NoFee) + override val fee = MutableStateFlow(configuration.initialState.feeStatus) override val retryEvent = MutableLiveData>() @@ -111,9 +116,7 @@ class FeeLoaderV2Provider( private var latestFeeConstructor: FeeConstructor? = null init { - observeChainChanges() - - observeFeeAssetChanges() + observeSelectedAssetChanges() } override fun loadFee(feeConstructor: FeeConstructor) { @@ -122,17 +125,18 @@ class FeeLoaderV2Provider( latestFeeConstructor = feeConstructor fee.emit(feeFormatter.createLoadingStatus()) - val feePaymentCurrency = feeChainAsset.first().toFeePaymentCurrency() + val feePaymentCurrency = feeChainAssetFlow.first().toFeePaymentCurrency() runCatching { feeConstructor(feePaymentCurrency) } - .onSuccess { onFeeLoaded(it, feeConstructor) } + .mapCatching { onFeeLoaded(it, feePaymentCurrency, feeConstructor) } .onFailure { onFeeError(it, feeConstructor) } } } private suspend fun onFeeError(error: Throwable, feeConstructor: FeeConstructor) { if (error !is CancellationException) { - error.printStackTrace() + Log.e(LOG_TAG, "Failed to sync fee", error) + fee.emit(FeeStatus.Error) awaitFeeRetry() @@ -141,8 +145,21 @@ class FeeLoaderV2Provider( } } - private suspend fun onFeeLoaded(newFee: F?, feeConstructor: FeeConstructor) { + private suspend fun onFeeLoaded( + newFee: F?, + requestedFeePaymentCurrency: FeePaymentCurrency, + feeConstructor: FeeConstructor + ) { if (newFee != null) { + val actualPaymentCurrency = feeInspector.getSubmissionFeeAsset(newFee).toFeePaymentCurrency() + require(requestedFeePaymentCurrency == actualPaymentCurrency) { + """ + Fee with loaded with different fee payment currency that was requested. + Requested: ${requestedFeePaymentCurrency}. Actual: $actualPaymentCurrency. + Please check you are using the passed FeePaymentCurrency to load the fee. + """.trimIndent() + } + setFeeWithAutomaticChange(newFee, feeConstructor) } else { fee.emit(FeeStatus.NoFee) @@ -173,17 +190,19 @@ class FeeLoaderV2Provider( } override suspend fun feePaymentCurrency(): FeePaymentCurrency { - return feeChainAsset.first().toFeePaymentCurrency() + return feeChainAssetFlow.first().toFeePaymentCurrency() } override suspend fun setPaymentCurrencySelectionMode(mode: PaymentCurrencySelectionMode) { paymentCurrencySelectionModeFlow.value = mode - val isCustomFee = !feeChainAsset.first().isUtilityAsset + val isCustomFee = !feeChainAssetFlow.first().isUtilityAsset if (isCustomFee && mode.onlyNativeFeeEnabled()) { val utilityAsset = selectedTokenInfo.first().chainAsset - feeChainAsset.emit(utilityAsset) + feeChainAssetFlow.emit(utilityAsset) + + reloadFeeWithLatestConstructor() } } @@ -192,6 +211,11 @@ class FeeLoaderV2Provider( } override suspend fun setFeeStatus(feeStatus: FeeStatus) { + feeStatus.onLoaded { feeModel -> + val feeAsset = feeInspector.getSubmissionFeeAsset(feeModel.fee) + feeChainAssetFlow.emit(feeAsset) + } + fee.emit(feeStatus) } @@ -221,30 +245,36 @@ class FeeLoaderV2Provider( fee.value = feeStatus } else { val selectedChainAsset = selectedChainAssetFlow.first() - feeChainAsset.emit(selectedChainAsset) + feeChainAssetFlow.emit(selectedChainAsset) loadFee(feeConstructor) } } - private fun observeChainChanges() { - selectedTokenInfo.distinctUntilChangedBy { it.chain.id } - .onEach { - fee.value = feeFormatter.createLoadingStatus() - userModifiedFeeInCurrentChain.value = false - feeChainAsset.emit(it.chain.utilityAsset) + private fun observeSelectedAssetChanges() { + selectedTokenInfo.distinctUntilChangedBy { it.chainAsset.fullId } + .withIndex() + .onEach { (index, tokenInfo) -> + userModifiedFeeInCurrentAsset.value = false + + if (index == 0) { + // First emission - we have loaded initial chain + val initialFeeAsset = configuration.initialState.feePaymentCurrency.toChainAsset(tokenInfo.chain) + feeChainAssetFlow.emit(initialFeeAsset) + } else { + // Subsequent emissions - chain changed, set the utility asset + feeChainAssetFlow.emit(tokenInfo.chain.utilityAsset) + + reloadFeeWithLatestConstructor() + } } .launchIn(this) } - private fun observeFeeAssetChanges() { - feeChainAsset.distinctUntilChangedBy { it.fullId } - .onEach { - latestFeeConstructor?.let(::loadFee) - }.launchIn(this) + private fun reloadFeeWithLatestConstructor() { + latestFeeConstructor?.let(::loadFee) } - override fun changePaymentCurrencyClicked() { launch { val userCanChangeFee = userCanChangeFeeAsset.first() @@ -253,14 +283,15 @@ class FeeLoaderV2Provider( val payload = constructChooseFeeCurrencyPayload() val chosenFeeAsset = chooseFeeAsset.awaitAction(payload) - feeChainAsset.emit(chosenFeeAsset) - userModifiedFeeInCurrentChain.value = true + feeChainAssetFlow.emit(chosenFeeAsset) + userModifiedFeeInCurrentAsset.value = true + reloadFeeWithLatestConstructor() } } private suspend fun constructChooseFeeCurrencyPayload(): ChooseFeeCurrencyPayload { val selectedTokenInfo = selectedTokenInfo.first() - val feeChainAsset = feeChainAsset.first() + val feeChainAsset = feeChainAssetFlow.first() val availableFeeTokens = listOf(selectedTokenInfo.chain.utilityAsset, selectedTokenInfo.chainAsset) return ChooseFeeCurrencyPayload( @@ -270,7 +301,7 @@ class FeeLoaderV2Provider( } private fun Asset.canPayFee(fee: F): Boolean { - return transferableInPlanks >= feeBalanceExtractor.requiredBalanceToPayFee(fee, token.configuration) + return transferableInPlanks >= feeInspector.requiredBalanceToPayFee(fee, token.configuration) } private suspend fun constructSelectedTokenInfo(chainAsset: Chain.Asset): SelectedAssetInfo { From 8dc2ab324bfd6893a418acc3a8b6fdeb20805e7a Mon Sep 17 00:00:00 2001 From: Valentun Date: Thu, 31 Oct 2024 17:29:27 +0300 Subject: [PATCH 38/83] Warm up fee visitor to improve asset to receive loading --- .../capability/CustomFeeAvailabilityFacade.kt | 2 +- .../fee/chains/AssetHubFeePaymentProvider.kt | 2 +- .../fee/chains/DefaultFeePaymentProvider.kt | 4 +- .../AssetHubFastLookupFeeCapability.kt | 17 ++------- .../feature_assets/di/AssetsFeatureModule.kt | 3 +- .../assets/search/AssetSearchInteractor.kt | 10 ++++- .../swap/AssetSwapFlowViewModel.kt | 6 +++ .../domain/model/SwapGraph.kt | 2 +- .../domain/swap/SwapService.kt | 4 +- .../AssetConversionExchange.kt | 2 +- .../CrossChainTransferAssetExchange.kt | 2 +- .../hydraDx/HydraDxAssetExchange.kt | 38 +++++++++---------- .../feature_swap_impl/di/SwapFeatureModule.kt | 2 - .../domain/interactor/SwapInteractor.kt | 4 +- .../domain/swap/RealSwapService.kt | 29 ++++++++------ .../main/SwapMainSettingsViewModel.kt | 2 + 16 files changed, 67 insertions(+), 62 deletions(-) diff --git a/feature-account-api/src/main/java/io/novafoundation/nova/feature_account_api/data/fee/capability/CustomFeeAvailabilityFacade.kt b/feature-account-api/src/main/java/io/novafoundation/nova/feature_account_api/data/fee/capability/CustomFeeAvailabilityFacade.kt index aa8d694eb2..519755a193 100644 --- a/feature-account-api/src/main/java/io/novafoundation/nova/feature_account_api/data/fee/capability/CustomFeeAvailabilityFacade.kt +++ b/feature-account-api/src/main/java/io/novafoundation/nova/feature_account_api/data/fee/capability/CustomFeeAvailabilityFacade.kt @@ -14,7 +14,7 @@ interface CustomFeeCapability { interface FastLookupCustomFeeCapability { - suspend fun canPayFeeInNonUtilityToken(chainAssetId: ChainAssetId): Boolean + fun canPayFeeInNonUtilityToken(chainAssetId: ChainAssetId): Boolean } interface CustomFeeCapabilityFacade { diff --git a/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/data/fee/chains/AssetHubFeePaymentProvider.kt b/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/data/fee/chains/AssetHubFeePaymentProvider.kt index 4855c18ae1..a7e2d6f6a6 100644 --- a/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/data/fee/chains/AssetHubFeePaymentProvider.kt +++ b/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/data/fee/chains/AssetHubFeePaymentProvider.kt @@ -46,6 +46,6 @@ class AssetHubFeePaymentProvider( override suspend fun fastLookupCustomFeeCapability(): FastLookupCustomFeeCapability { val fetcher = assetHubFeePaymentAssetsFetcher.create(chain) - return AssetHubFastLookupFeeCapability(fetcher) + return AssetHubFastLookupFeeCapability(fetcher.fetchAvailablePaymentAssets()) } } diff --git a/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/data/fee/chains/DefaultFeePaymentProvider.kt b/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/data/fee/chains/DefaultFeePaymentProvider.kt index 711542b140..7aba06b40e 100644 --- a/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/data/fee/chains/DefaultFeePaymentProvider.kt +++ b/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/data/fee/chains/DefaultFeePaymentProvider.kt @@ -19,9 +19,9 @@ class DefaultFeePaymentProvider : FeePaymentProvider { } } -class DefaultFastLookupCustomFeeCapability(): FastLookupCustomFeeCapability { +class DefaultFastLookupCustomFeeCapability: FastLookupCustomFeeCapability { - override suspend fun canPayFeeInNonUtilityToken(chainAssetId: ChainAssetId): Boolean { + override fun canPayFeeInNonUtilityToken(chainAssetId: ChainAssetId): Boolean { return false } } diff --git a/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/data/fee/types/assetHub/AssetHubFastLookupFeeCapability.kt b/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/data/fee/types/assetHub/AssetHubFastLookupFeeCapability.kt index 8ee8c0b67f..c849a93396 100644 --- a/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/data/fee/types/assetHub/AssetHubFastLookupFeeCapability.kt +++ b/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/data/fee/types/assetHub/AssetHubFastLookupFeeCapability.kt @@ -4,21 +4,10 @@ import io.novafoundation.nova.feature_account_api.data.fee.capability.FastLookup import io.novafoundation.nova.runtime.multiNetwork.chain.model.ChainAssetId class AssetHubFastLookupFeeCapability( - private val assetsFetcher: AssetHubFeePaymentAssetsFetcher, + private val allowedPaymentAssets: Set, ): FastLookupCustomFeeCapability { - private var cachedAssets: Set? = null - - override suspend fun canPayFeeInNonUtilityToken(chainAssetId: ChainAssetId): Boolean { - return chainAssetId in getAllowedFeePaymentAssets() - } - - private suspend fun getAllowedFeePaymentAssets(): Set { - // We are not guarding it with mutex to make it more optimized and avoid synchronisation overhead - if (cachedAssets == null) { - cachedAssets = assetsFetcher.fetchAvailablePaymentAssets() - } - - return cachedAssets!! + override fun canPayFeeInNonUtilityToken(chainAssetId: ChainAssetId): Boolean { + return chainAssetId in allowedPaymentAssets } } diff --git a/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/di/AssetsFeatureModule.kt b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/di/AssetsFeatureModule.kt index 3c99128536..3ce133327a 100644 --- a/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/di/AssetsFeatureModule.kt +++ b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/di/AssetsFeatureModule.kt @@ -56,9 +56,8 @@ class AssetsFeatureModule { walletRepository: WalletRepository, accountRepository: AccountRepository, chainRegistry: ChainRegistry, - assetSourceRegistry: AssetSourceRegistry, swapService: SwapService - ) = AssetSearchInteractor(walletRepository, accountRepository, chainRegistry, assetSourceRegistry, swapService) + ) = AssetSearchInteractor(walletRepository, accountRepository, chainRegistry, swapService) @Provides @FeatureScope diff --git a/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/domain/assets/search/AssetSearchInteractor.kt b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/domain/assets/search/AssetSearchInteractor.kt index 1817c6eecd..9cdd67af83 100644 --- a/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/domain/assets/search/AssetSearchInteractor.kt +++ b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/domain/assets/search/AssetSearchInteractor.kt @@ -9,7 +9,6 @@ import io.novafoundation.nova.feature_assets.domain.common.getAssetGroupBaseComp import io.novafoundation.nova.feature_assets.domain.common.groupAndSortAssetsByNetwork import io.novafoundation.nova.feature_assets.domain.common.searchTokens import io.novafoundation.nova.feature_swap_api.domain.swap.SwapService -import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.AssetSourceRegistry import io.novafoundation.nova.feature_wallet_api.domain.interfaces.WalletRepository import io.novafoundation.nova.feature_wallet_api.domain.model.Asset import io.novafoundation.nova.feature_wallet_api.domain.model.ExternalBalance @@ -22,11 +21,13 @@ import io.novafoundation.nova.runtime.multiNetwork.chain.model.FullChainAssetId import io.novafoundation.nova.runtime.multiNetwork.enabledChainById import io.novasama.substrate_sdk_android.hash.isPositive import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.map +import kotlinx.coroutines.withContext private typealias AssetSearchFilter = suspend (Asset) -> Boolean @@ -34,10 +35,15 @@ class AssetSearchInteractor( private val walletRepository: WalletRepository, private val accountRepository: AccountRepository, private val chainRegistry: ChainRegistry, - private val assetSourceRegistry: AssetSourceRegistry, private val swapService: SwapService ) { + suspend fun warmUpSwapCommonlyUsedChains(computationalScope: CoroutineScope) { + withContext(Dispatchers.IO) { + swapService.warmUpCommonChains(computationalScope) + } + } + fun buyAssetSearch( queryFlow: Flow, externalBalancesFlow: Flow>, diff --git a/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/swap/AssetSwapFlowViewModel.kt b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/swap/AssetSwapFlowViewModel.kt index cfa0346e5f..d7ee81bbfd 100644 --- a/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/swap/AssetSwapFlowViewModel.kt +++ b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/swap/AssetSwapFlowViewModel.kt @@ -49,6 +49,12 @@ class AssetSwapFlowViewModel( swapAvailabilityInteractor.sync(viewModelScope) } } + + launch { + if (payload is SwapFlowPayload.InitialSelecting) { + interactor.warmUpSwapCommonlyUsedChains(viewModelScope) + } + } } @StringRes diff --git a/feature-swap-api/src/main/java/io/novafoundation/nova/feature_swap_api/domain/model/SwapGraph.kt b/feature-swap-api/src/main/java/io/novafoundation/nova/feature_swap_api/domain/model/SwapGraph.kt index 8ac1ff1a1a..0ef6690d70 100644 --- a/feature-swap-api/src/main/java/io/novafoundation/nova/feature_swap_api/domain/model/SwapGraph.kt +++ b/feature-swap-api/src/main/java/io/novafoundation/nova/feature_swap_api/domain/model/SwapGraph.kt @@ -39,7 +39,7 @@ interface SwapGraphEdge : QuotableEdge { * Note that returning true here means that [canPayNonNativeFeesInIntermediatePosition] wont be called and checked * */ - suspend fun shouldIgnoreFeeRequirementAfter(predecessor: SwapGraphEdge): Boolean + fun shouldIgnoreFeeRequirementAfter(predecessor: SwapGraphEdge): Boolean /** * Can be used to define additional restrictions on top of default one, "is able to pay submission fee on origin" diff --git a/feature-swap-api/src/main/java/io/novafoundation/nova/feature_swap_api/domain/swap/SwapService.kt b/feature-swap-api/src/main/java/io/novafoundation/nova/feature_swap_api/domain/swap/SwapService.kt index d7c38c421e..269a474afa 100644 --- a/feature-swap-api/src/main/java/io/novafoundation/nova/feature_swap_api/domain/swap/SwapService.kt +++ b/feature-swap-api/src/main/java/io/novafoundation/nova/feature_swap_api/domain/swap/SwapService.kt @@ -16,6 +16,8 @@ import kotlinx.coroutines.flow.Flow interface SwapService { + suspend fun warmUpCommonChains(computationScope: CoroutineScope): Result + suspend fun sync(coroutineScope: CoroutineScope) suspend fun assetsAvailableForSwap(computationScope: CoroutineScope): Flow> @@ -24,8 +26,6 @@ interface SwapService { suspend fun hasAvailableSwapDirections(asset: Chain.Asset, computationScope: CoroutineScope): Flow - suspend fun canPayFeeInNonUtilityAsset(asset: Chain.Asset): Boolean - suspend fun quote( args: SwapQuoteArgs, computationSharingScope: CoroutineScope diff --git a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/assetConversion/AssetConversionExchange.kt b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/assetConversion/AssetConversionExchange.kt index aa240bdb76..c855237a11 100644 --- a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/assetConversion/AssetConversionExchange.kt +++ b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/assetConversion/AssetConversionExchange.kt @@ -193,7 +193,7 @@ private class AssetConversionExchange( return "AssetConversion" } - override suspend fun shouldIgnoreFeeRequirementAfter(predecessor: SwapGraphEdge): Boolean { + override fun shouldIgnoreFeeRequirementAfter(predecessor: SwapGraphEdge): Boolean { return false } diff --git a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/crossChain/CrossChainTransferAssetExchange.kt b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/crossChain/CrossChainTransferAssetExchange.kt index 5df72e08b8..a2bda4c56f 100644 --- a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/crossChain/CrossChainTransferAssetExchange.kt +++ b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/crossChain/CrossChainTransferAssetExchange.kt @@ -117,7 +117,7 @@ class CrossChainTransferAssetExchange( return "To ${chainRegistry.getChain(delegate.to.chainId).name}" } - override suspend fun shouldIgnoreFeeRequirementAfter(predecessor: SwapGraphEdge): Boolean { + override fun shouldIgnoreFeeRequirementAfter(predecessor: SwapGraphEdge): Boolean { return false } diff --git a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/hydraDx/HydraDxAssetExchange.kt b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/hydraDx/HydraDxAssetExchange.kt index 307e2b805f..214981eaa6 100644 --- a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/hydraDx/HydraDxAssetExchange.kt +++ b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/hydraDx/HydraDxAssetExchange.kt @@ -4,6 +4,7 @@ import io.novafoundation.nova.common.data.network.runtime.binding.bindNumber import io.novafoundation.nova.common.utils.Modules import io.novafoundation.nova.common.utils.flatMapAsync import io.novafoundation.nova.common.utils.forEachAsync +import io.novafoundation.nova.common.utils.mapNotNullToSet import io.novafoundation.nova.common.utils.mergeIfMultiple import io.novafoundation.nova.common.utils.singleReplaySharedFlow import io.novafoundation.nova.common.utils.structOf @@ -245,7 +246,7 @@ private class HydraDxAssetExchange( return sourceQuotableEdge.debugLabel() } - override suspend fun shouldIgnoreFeeRequirementAfter(predecessor: SwapGraphEdge): Boolean { + override fun shouldIgnoreFeeRequirementAfter(predecessor: SwapGraphEdge): Boolean { // When chaining multiple hydra edges together, the fee is always paid with the starting edge return predecessor is HydraDxSwapEdge } @@ -507,7 +508,18 @@ private class HydraDxAssetExchange( } override suspend fun fastLookupCustomFeeCapability(): FastLookupCustomFeeCapability { - return HydrationFastLookupFeeCapability() + val acceptedCurrencies = fetchAcceptedCurrencies() + return HydrationFastLookupFeeCapability(acceptedCurrencies) + } + + private suspend fun fetchAcceptedCurrencies(): Set { + val acceptedOnChainIds = remoteStorageSource.query(chain.id) { + metadata.multiTransactionPayment.acceptedCurrencies.keys() + } + + val onChainToLocalIds = hydraDxAssetIdConverter.allOnChainIds(chain) + + return acceptedOnChainIds.mapNotNullToSet { onChainToLocalIds[it]?.id } } } @@ -551,26 +563,14 @@ private class HydraDxAssetExchange( } } - private inner class HydrationFastLookupFeeCapability : FastLookupCustomFeeCapability { + private inner class HydrationFastLookupFeeCapability( + private val acceptedCurrencies: Set + ): FastLookupCustomFeeCapability { private var acceptedCurrenciesCache: Set? = null - override suspend fun canPayFeeInNonUtilityToken(chainAssetId: ChainAssetId): Boolean { - val asset = chain.assetsById[chainAssetId] ?: return false - val onChainId = hydraDxAssetIdConverter.toOnChainIdOrThrow(asset) - - val acceptedCurrencies = getAcceptedCurrencies() - return onChainId in acceptedCurrencies - } - - private suspend fun getAcceptedCurrencies(): Set { - if (acceptedCurrenciesCache != null) return acceptedCurrenciesCache!! - - acceptedCurrenciesCache = remoteStorageSource.query(chain.id) { - metadata.multiTransactionPayment.acceptedCurrencies.keys() - }.toSet() - - return acceptedCurrenciesCache!! + override fun canPayFeeInNonUtilityToken(chainAssetId: ChainAssetId): Boolean { + return chainAssetId in acceptedCurrencies } } diff --git a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/di/SwapFeatureModule.kt b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/di/SwapFeatureModule.kt index c01f32816f..08e2fe9431 100644 --- a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/di/SwapFeatureModule.kt +++ b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/di/SwapFeatureModule.kt @@ -56,7 +56,6 @@ class SwapFeatureModule { computationalCache: ComputationalCache, chainRegistry: ChainRegistry, quoterFactory: PathQuoter.Factory, - customFeeCapabilityFacade: CustomFeeCapabilityFacade, extrinsicServiceFactory: ExtrinsicService.Factory, defaultFeePaymentRegistry: FeePaymentProviderRegistry, tokenRepository: TokenRepository, @@ -69,7 +68,6 @@ class SwapFeatureModule { computationalCache = computationalCache, chainRegistry = chainRegistry, quoterFactory = quoterFactory, - customFeeCapabilityFacade = customFeeCapabilityFacade, extrinsicServiceFactory = extrinsicServiceFactory, defaultFeePaymentProviderRegistry = defaultFeePaymentRegistry, tokenRepository = tokenRepository, diff --git a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/domain/interactor/SwapInteractor.kt b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/domain/interactor/SwapInteractor.kt index bfa81594b7..52ff9cd769 100644 --- a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/domain/interactor/SwapInteractor.kt +++ b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/domain/interactor/SwapInteractor.kt @@ -127,8 +127,8 @@ class SwapInteractor( // } } - suspend fun canPayFeeInCustomAsset(asset: Chain.Asset): Boolean { - return swapService.canPayFeeInNonUtilityAsset(asset) + suspend fun warmUpSwapCommonlyUsedChains(computationalScope: CoroutineScope) { + swapService.warmUpCommonChains(computationalScope) } suspend fun estimateFee(executeArgs: SwapFeeArgs): SwapFee { diff --git a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/domain/swap/RealSwapService.kt b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/domain/swap/RealSwapService.kt index 6867b5406b..ab7bda13fa 100644 --- a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/domain/swap/RealSwapService.kt +++ b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/domain/swap/RealSwapService.kt @@ -18,6 +18,7 @@ import io.novafoundation.nova.common.utils.graph.hasOutcomingDirections import io.novafoundation.nova.common.utils.graph.vertices import io.novafoundation.nova.common.utils.isZero import io.novafoundation.nova.common.utils.mapAsync +import io.novafoundation.nova.common.utils.measureExecution import io.novafoundation.nova.common.utils.mergeIfMultiple import io.novafoundation.nova.common.utils.orZero import io.novafoundation.nova.common.utils.toPercent @@ -26,7 +27,6 @@ import io.novafoundation.nova.feature_account_api.data.extrinsic.ExtrinsicServic import io.novafoundation.nova.feature_account_api.data.fee.FeePaymentCurrency import io.novafoundation.nova.feature_account_api.data.fee.FeePaymentProvider import io.novafoundation.nova.feature_account_api.data.fee.FeePaymentProviderRegistry -import io.novafoundation.nova.feature_account_api.data.fee.capability.CustomFeeCapabilityFacade import io.novafoundation.nova.feature_account_api.data.fee.capability.FastLookupCustomFeeCapability import io.novafoundation.nova.feature_account_api.data.fee.toFeePaymentCurrency import io.novafoundation.nova.feature_account_api.data.model.FeeBase @@ -122,7 +122,6 @@ internal class RealSwapService( private val computationalCache: ComputationalCache, private val chainRegistry: ChainRegistry, private val quoterFactory: PathQuoter.Factory, - private val customFeeCapabilityFacade: CustomFeeCapabilityFacade, private val extrinsicServiceFactory: ExtrinsicService.Factory, private val defaultFeePaymentProviderRegistry: FeePaymentProviderRegistry, private val assetSourceRegistry: AssetSourceRegistry, @@ -130,15 +129,15 @@ internal class RealSwapService( private val debug: Boolean = BuildConfig.DEBUG ) : SwapService { - override suspend fun canPayFeeInNonUtilityAsset(asset: Chain.Asset): Boolean = withContext(Dispatchers.Default) { - val computationScope = CoroutineScope(coroutineContext) - val exchangeRegistry = exchangeRegistry(computationScope) - val paymentRegistry = exchangeRegistry.getFeePaymentRegistry() - - val chain = chainRegistry.getChain(asset.chainId) - val feePayment = paymentRegistry.providerFor(chain.id).feePaymentFor(asset.toFeePaymentCurrency(), computationScope) + override suspend fun warmUpCommonChains(computationScope: CoroutineScope): Result { + return runCatching { + warmUpChain(Chain.Geneses.HYDRA_DX, computationScope) + warmUpChain(Chain.Geneses.POLKADOT_ASSET_HUB, computationScope) + } + } - customFeeCapabilityFacade.canPayFeeInNonUtilityToken(asset, feePayment) + private suspend fun warmUpChain(chainId: ChainId, computationScope: CoroutineScope) { + canPayFeeNodeFilter(computationScope).warmUpChain(chainId) } override suspend fun sync(coroutineScope: CoroutineScope) { @@ -161,7 +160,9 @@ internal class RealSwapService( ): Flow> { return directionsGraph(computationScope).map { val filter = canPayFeeNodeFilter(computationScope) - it.findAllPossibleDestinations(asset.fullId, filter) - asset.fullId + measureExecution("findAllPossibleDestinations") { + it.findAllPossibleDestinations(asset.fullId, filter) - asset.fullId + } } } @@ -410,7 +411,7 @@ internal class RealSwapService( } - private suspend fun canPayFeeNodeFilter(computationScope: CoroutineScope): EdgeVisitFilter { + private suspend fun canPayFeeNodeFilter(computationScope: CoroutineScope): CanPayFeeNodeVisitFilter { return computationalCache.useCache(NODE_VISIT_FILTER, computationScope) { CanPayFeeNodeVisitFilter(this, chainRegistry.chainsById()) } @@ -763,6 +764,10 @@ internal class RealSwapService( private val feePaymentCapabilityCache: MutableMap = mutableMapOf() + suspend fun warmUpChain(chainId: ChainId) { + getFeeCustomFeeCapability(chainId) + } + override suspend fun shouldVisit(edge: SwapGraphEdge, pathPredecessor: SwapGraphEdge?): Boolean { // Utility payments and first path segments are always allowed if (edge.from.isUtility || pathPredecessor == null) return true diff --git a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/main/SwapMainSettingsViewModel.kt b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/main/SwapMainSettingsViewModel.kt index c74b52d06e..2feaf6f6fe 100644 --- a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/main/SwapMainSettingsViewModel.kt +++ b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/main/SwapMainSettingsViewModel.kt @@ -282,6 +282,8 @@ class SwapMainSettingsViewModel( init { initPayload() +// launch { swapInteractor.warmUpSwapCommonlyUsedChains(viewModelScope) } + handleInputChanges(amountInInput, SwapSettings::assetIn, SwapDirection.SPECIFIED_IN) handleInputChanges(amountOutInput, SwapSettings::assetOut, SwapDirection.SPECIFIED_OUT) From 16d35a6c0dc19d6e3bebd26691ffa52ec804c97d Mon Sep 17 00:00:00 2001 From: Valentun Date: Thu, 31 Oct 2024 18:21:11 +0300 Subject: [PATCH 39/83] Skip edges user doesn't have account on destination for --- .../domain/model/MetaAccount.kt | 2 +- .../feature_swap_impl/di/SwapFeatureModule.kt | 5 ++-- .../domain/swap/RealSwapService.kt | 26 ++++++++++++++----- 3 files changed, 23 insertions(+), 10 deletions(-) diff --git a/feature-account-api/src/main/java/io/novafoundation/nova/feature_account_api/domain/model/MetaAccount.kt b/feature-account-api/src/main/java/io/novafoundation/nova/feature_account_api/domain/model/MetaAccount.kt index 8d6666288f..3c4592be40 100644 --- a/feature-account-api/src/main/java/io/novafoundation/nova/feature_account_api/domain/model/MetaAccount.kt +++ b/feature-account-api/src/main/java/io/novafoundation/nova/feature_account_api/domain/model/MetaAccount.kt @@ -115,7 +115,7 @@ fun MetaAccount.addressIn(chain: Chain): String? { fun MetaAccount.mainEthereumAddress() = ethereumAddress?.toEthereumAddress() -fun MetaAccount.requireAddressIn(chain: Chain): String = addressIn(chain) ?: throw NoSuchElementException("No chain account found for $chain in $name") +fun MetaAccount.requireAddressIn(chain: Chain): String = addressIn(chain) ?: throw NoSuchElementException("No chain account found for ${chain.name} in $name") val MetaAccount.defaultSubstrateAddress: String? get() = substrateAccountId?.toDefaultSubstrateAddress() diff --git a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/di/SwapFeatureModule.kt b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/di/SwapFeatureModule.kt index 08e2fe9431..414ed6a39a 100644 --- a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/di/SwapFeatureModule.kt +++ b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/di/SwapFeatureModule.kt @@ -9,7 +9,6 @@ import io.novafoundation.nova.core.storage.StorageCache import io.novafoundation.nova.core_db.dao.OperationDao import io.novafoundation.nova.feature_account_api.data.extrinsic.ExtrinsicService import io.novafoundation.nova.feature_account_api.data.fee.FeePaymentProviderRegistry -import io.novafoundation.nova.feature_account_api.data.fee.capability.CustomFeeCapabilityFacade import io.novafoundation.nova.feature_account_api.domain.interfaces.AccountRepository import io.novafoundation.nova.feature_buy_api.domain.BuyTokenRegistry import io.novafoundation.nova.feature_swap_api.domain.interactor.SwapAvailabilityInteractor @@ -59,6 +58,7 @@ class SwapFeatureModule { extrinsicServiceFactory: ExtrinsicService.Factory, defaultFeePaymentRegistry: FeePaymentProviderRegistry, tokenRepository: TokenRepository, + accountRepository: AccountRepository, assetSourceRegistry: AssetSourceRegistry ): SwapService { return RealSwapService( @@ -71,7 +71,8 @@ class SwapFeatureModule { extrinsicServiceFactory = extrinsicServiceFactory, defaultFeePaymentProviderRegistry = defaultFeePaymentRegistry, tokenRepository = tokenRepository, - assetSourceRegistry = assetSourceRegistry + assetSourceRegistry = assetSourceRegistry, + accountRepository = accountRepository ) } diff --git a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/domain/swap/RealSwapService.kt b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/domain/swap/RealSwapService.kt index ab7bda13fa..4efb13085a 100644 --- a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/domain/swap/RealSwapService.kt +++ b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/domain/swap/RealSwapService.kt @@ -31,6 +31,7 @@ import io.novafoundation.nova.feature_account_api.data.fee.capability.FastLookup import io.novafoundation.nova.feature_account_api.data.fee.toFeePaymentCurrency import io.novafoundation.nova.feature_account_api.data.model.FeeBase import io.novafoundation.nova.feature_account_api.data.model.SubstrateFeeBase +import io.novafoundation.nova.feature_account_api.domain.interfaces.AccountRepository import io.novafoundation.nova.feature_account_api.domain.model.MetaAccount import io.novafoundation.nova.feature_swap_api.domain.model.AtomicSwapOperation import io.novafoundation.nova.feature_swap_api.domain.model.AtomicSwapOperationArgs @@ -84,6 +85,7 @@ import io.novafoundation.nova.runtime.ext.isUtility import io.novafoundation.nova.runtime.ext.utilityAsset import io.novafoundation.nova.runtime.ext.utilityAssetOf import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry +import io.novafoundation.nova.runtime.multiNetwork.ChainWithAsset import io.novafoundation.nova.runtime.multiNetwork.ChainsById import io.novafoundation.nova.runtime.multiNetwork.asset import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain @@ -125,6 +127,7 @@ internal class RealSwapService( private val extrinsicServiceFactory: ExtrinsicService.Factory, private val defaultFeePaymentProviderRegistry: FeePaymentProviderRegistry, private val assetSourceRegistry: AssetSourceRegistry, + private val accountRepository: AccountRepository, private val tokenRepository: TokenRepository, private val debug: Boolean = BuildConfig.DEBUG ) : SwapService { @@ -413,7 +416,11 @@ internal class RealSwapService( private suspend fun canPayFeeNodeFilter(computationScope: CoroutineScope): CanPayFeeNodeVisitFilter { return computationalCache.useCache(NODE_VISIT_FILTER, computationScope) { - CanPayFeeNodeVisitFilter(this, chainRegistry.chainsById()) + CanPayFeeNodeVisitFilter( + computationScope = this, + chainsById = chainRegistry.chainsById(), + selectedAccount = accountRepository.getSelectedMetaAccount() + ) } } @@ -760,6 +767,7 @@ internal class RealSwapService( private inner class CanPayFeeNodeVisitFilter( val computationScope: CoroutineScope, val chainsById: ChainsById, + val selectedAccount: MetaAccount, ) : EdgeVisitFilter { private val feePaymentCapabilityCache: MutableMap = mutableMapOf() @@ -772,8 +780,13 @@ internal class RealSwapService( // Utility payments and first path segments are always allowed if (edge.from.isUtility || pathPredecessor == null) return true + val chainAndAssetOut = chainsById.chainWithAssetOrNull(edge.to) ?: return false + + // User should have account on destination + if (!selectedAccount.hasAccountIn(chainAndAssetOut.chain)) return false + // Destination asset must be sufficient - if (!isSufficient(edge.to)) return false + if (!isSufficient(chainAndAssetOut)) return false // Edge might request us to ignore the default requirement based on its direct predecessor if (edge.shouldIgnoreFeeRequirementAfter(pathPredecessor)) return true @@ -784,12 +797,11 @@ internal class RealSwapService( && edge.canPayNonNativeFeesInIntermediatePosition() } - private fun isSufficient(fullChainAssetId: FullChainAssetId): Boolean { - val (chain, chainAsset) = chainsById.chainWithAssetOrNull(fullChainAssetId) ?: return false - val balance = assetSourceRegistry.sourceFor(chainAsset).balance - return balance.isSelfSufficient(chainAsset).also { isSufficient -> + private fun isSufficient(chainAndAsset: ChainWithAsset): Boolean { + val balance = assetSourceRegistry.sourceFor(chainAndAsset.asset).balance + return balance.isSelfSufficient(chainAndAsset.asset).also { isSufficient -> if (!isSufficient) { - Log.d("Swaps", "${chainAsset.symbol} (${chain.name} is not sufficient)") + Log.d("Swaps", "${chainAndAsset.asset.symbol} (${chainAndAsset.chain.name} is not sufficient)") } } } From 3af944f2fa1b265221d91a1b605ab78f14ffd653 Mon Sep 17 00:00:00 2001 From: valentun Date: Tue, 5 Nov 2024 16:41:05 +0700 Subject: [PATCH 40/83] Fix merge conflicts --- .gitignore | 3 +- .../nova/app/root/navigation/Navigator.kt | 6 +++- .../app/root/navigation/swap/SwapNavigator.kt | 4 +-- .../main/res/navigation/main_nav_graph.xml | 25 +++----------- .../select_swap_token_nav_graph.xml | 33 +++++++++++++++++++ .../res/navigation/start_swap_nav_graph.xml | 19 ++++------- .../hydra_dx_math/HydraDxMathConversions.kt | 11 +++++++ .../hydra_dx_math/xyk/HYKSwapMathBridge.java | 26 +++++++++++++++ ...setViewModeAssetSearchInteractorFactory.kt | 11 ++----- .../presentation/AssetsRouter.kt | 2 ++ .../flow/asset/AssetFlowViewModel.kt | 2 +- .../swap/asset/AssetSwapFlowViewModel.kt | 26 +++++++-------- .../swap/executor/ReselectSwapFlowExecutor.kt | 4 +-- .../interactor/SwapAvailabilityInteractor.kt | 2 ++ .../presentation/state/SwapSettingsState.kt | 2 +- .../di/SwapFeatureDependencies.kt | 3 -- .../RealSwapAvailabilityInteractor.kt | 4 +++ .../domain/swap/RealSwapService.kt | 6 ++-- .../confirmation/SwapConfirmationViewModel.kt | 2 -- .../main/SwapMainSettingsViewModel.kt | 4 +-- .../state/RealSwapSettingsState.kt | 2 +- 21 files changed, 122 insertions(+), 75 deletions(-) create mode 100644 app/src/main/res/navigation/select_swap_token_nav_graph.xml create mode 100644 bindings/hydra-dx-math/src/main/java/io/novafoundation/nova/hydra_dx_math/HydraDxMathConversions.kt create mode 100644 bindings/hydra-dx-math/src/main/java/io/novafoundation/nova/hydra_dx_math/xyk/HYKSwapMathBridge.java diff --git a/.gitignore b/.gitignore index d46fbdb076..44f05d31ee 100644 --- a/.gitignore +++ b/.gitignore @@ -25,5 +25,4 @@ app/*.apk !/core-db/schemas/io.novafoundation.nova.core_db.AppDatabase/8.json !/core-db/schemas/io.novafoundation.nova.core_db.AppDatabase/9.json -google-services.json -/bindings +google-services.json \ No newline at end of file diff --git a/app/src/main/java/io/novafoundation/nova/app/root/navigation/Navigator.kt b/app/src/main/java/io/novafoundation/nova/app/root/navigation/Navigator.kt index 530e524344..4dcf3ef406 100644 --- a/app/src/main/java/io/novafoundation/nova/app/root/navigation/Navigator.kt +++ b/app/src/main/java/io/novafoundation/nova/app/root/navigation/Navigator.kt @@ -396,7 +396,11 @@ class Navigator( } override fun openSwapNetworks(payload: NetworkSwapFlowPayload) { - navController?.navigate(R.id.action_swapFlow_to_swapFlowNetwork, NetworkSwapFlowFragment.createPayload(payload)) + navController?.navigate(R.id.action_selectAssetSwapFlowFragment_to_swapFlowNetworkFragment, NetworkSwapFlowFragment.createPayload(payload)) + } + + override fun returnToMainSwapScreen() { + navController?.navigate(R.id.action_return_to_swap_settings) } override fun openBuyNetworks(payload: NetworkFlowPayload) { diff --git a/app/src/main/java/io/novafoundation/nova/app/root/navigation/swap/SwapNavigator.kt b/app/src/main/java/io/novafoundation/nova/app/root/navigation/swap/SwapNavigator.kt index b638f03062..28791595b9 100644 --- a/app/src/main/java/io/novafoundation/nova/app/root/navigation/swap/SwapNavigator.kt +++ b/app/src/main/java/io/novafoundation/nova/app/root/navigation/swap/SwapNavigator.kt @@ -28,12 +28,12 @@ class SwapNavigator( override fun selectAssetIn(selectedAsset: AssetPayload?) { val payload = SwapFlowPayload.ReselectAssetIn(selectedAsset) - navigationHolder.navController?.navigate(R.id.action_swapMainSettingsFragment_to_swapFlow, AssetSwapFlowFragment.getBundle(payload)) + navigationHolder.navController?.navigate(R.id.action_swapSettingsFragment_to_select_swap_token_graph, AssetSwapFlowFragment.getBundle(payload)) } override fun selectAssetOut(selectedAsset: AssetPayload?) { val payload = SwapFlowPayload.ReselectAssetOut(selectedAsset) - navigationHolder.navController?.navigate(R.id.action_swapMainSettingsFragment_to_swapFlow, AssetSwapFlowFragment.getBundle(payload)) + navigationHolder.navController?.navigate(R.id.action_swapSettingsFragment_to_select_swap_token_graph, AssetSwapFlowFragment.getBundle(payload)) } override fun openSendCrossChain(destination: AssetPayload, recipientAddress: String?) { diff --git a/app/src/main/res/navigation/main_nav_graph.xml b/app/src/main/res/navigation/main_nav_graph.xml index 61b7ba21ca..2811c7fc2f 100644 --- a/app/src/main/res/navigation/main_nav_graph.xml +++ b/app/src/main/res/navigation/main_nav_graph.xml @@ -217,7 +217,7 @@ - - - - - - - + + diff --git a/app/src/main/res/navigation/select_swap_token_nav_graph.xml b/app/src/main/res/navigation/select_swap_token_nav_graph.xml new file mode 100644 index 0000000000..45123fda55 --- /dev/null +++ b/app/src/main/res/navigation/select_swap_token_nav_graph.xml @@ -0,0 +1,33 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/navigation/start_swap_nav_graph.xml b/app/src/main/res/navigation/start_swap_nav_graph.xml index 2ded44e48d..3057353d47 100644 --- a/app/src/main/res/navigation/start_swap_nav_graph.xml +++ b/app/src/main/res/navigation/start_swap_nav_graph.xml @@ -2,7 +2,7 @@ - + app:popExitAnim="@anim/fragment_close_exit" + app:destination="@id/select_swap_token_nav_graph" /> - - + \ No newline at end of file diff --git a/bindings/hydra-dx-math/src/main/java/io/novafoundation/nova/hydra_dx_math/HydraDxMathConversions.kt b/bindings/hydra-dx-math/src/main/java/io/novafoundation/nova/hydra_dx_math/HydraDxMathConversions.kt new file mode 100644 index 0000000000..a2bc6c1bb7 --- /dev/null +++ b/bindings/hydra-dx-math/src/main/java/io/novafoundation/nova/hydra_dx_math/HydraDxMathConversions.kt @@ -0,0 +1,11 @@ +package io.novafoundation.nova.hydra_dx_math + +import io.novafoundation.nova.common.utils.atLeastZero +import java.math.BigInteger + +object HydraDxMathConversions { + + fun String.fromBridgeResultToBalance(): BigInteger? { + return if (this == "-1") null else toBigInteger().atLeastZero() + } +} diff --git a/bindings/hydra-dx-math/src/main/java/io/novafoundation/nova/hydra_dx_math/xyk/HYKSwapMathBridge.java b/bindings/hydra-dx-math/src/main/java/io/novafoundation/nova/hydra_dx_math/xyk/HYKSwapMathBridge.java new file mode 100644 index 0000000000..f9adec639d --- /dev/null +++ b/bindings/hydra-dx-math/src/main/java/io/novafoundation/nova/hydra_dx_math/xyk/HYKSwapMathBridge.java @@ -0,0 +1,26 @@ +package io.novafoundation.nova.hydra_dx_math.xyk; + +public class HYKSwapMathBridge { + + static { + System.loadLibrary("hydra_dx_math_java"); + } + + public static native String calculate_out_given_in( + String balanceIn, + String balanceOut, + String amountIn + ); + + public static native String calculate_in_given_out( + String balanceIn, + String balanceOut, + String amountOut + ); + + public static native String calculate_pool_trade_fee( + String amount, + String feeNumerator, + String feeDenominator + ); +} \ No newline at end of file diff --git a/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/domain/assets/search/AssetViewModeAssetSearchInteractorFactory.kt b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/domain/assets/search/AssetViewModeAssetSearchInteractorFactory.kt index 1617517d26..c1e4565df1 100644 --- a/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/domain/assets/search/AssetViewModeAssetSearchInteractorFactory.kt +++ b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/domain/assets/search/AssetViewModeAssetSearchInteractorFactory.kt @@ -12,15 +12,8 @@ class AssetViewModeAssetSearchInteractorFactory( override fun createByAssetViewMode(): AssetSearchInteractor { return when (assetViewModeRepository.getAssetViewMode()) { - AssetViewMode.TOKENS -> ByTokensAssetSearchInteractor( - assetSearchUseCase, - chainRegistry - ) - - AssetViewMode.NETWORKS -> ByNetworkAssetSearchInteractor( - assetSearchUseCase, - chainRegistry - ) + AssetViewMode.TOKENS -> ByTokensAssetSearchInteractor(assetSearchUseCase, chainRegistry) + AssetViewMode.NETWORKS -> ByNetworkAssetSearchInteractor(assetSearchUseCase, chainRegistry) } } } diff --git a/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/AssetsRouter.kt b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/AssetsRouter.kt index 8c1be6cfeb..28dc1ff223 100644 --- a/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/AssetsRouter.kt +++ b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/AssetsRouter.kt @@ -82,5 +82,7 @@ interface AssetsRouter { fun openSwapNetworks(payload: NetworkSwapFlowPayload) + fun returnToMainSwapScreen() + fun openBuyNetworks(payload: NetworkFlowPayload) } diff --git a/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/flow/asset/AssetFlowViewModel.kt b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/flow/asset/AssetFlowViewModel.kt index 124e1ab81f..2096a32e48 100644 --- a/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/flow/asset/AssetFlowViewModel.kt +++ b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/flow/asset/AssetFlowViewModel.kt @@ -57,7 +57,7 @@ abstract class AssetFlowViewModel( protected val externalBalancesFlow = externalBalancesInteractor.observeExternalBalances() private val searchAssetsFlow = flowOfAll { searchAssetsFlow() } - .shareInBackground() + .shareInBackground(SharingStarted.Lazily) val searchResults = combine( searchAssetsFlow, // lazy use searchAssetsFlow to let subclasses initialize self diff --git a/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/swap/asset/AssetSwapFlowViewModel.kt b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/swap/asset/AssetSwapFlowViewModel.kt index eca8ea7657..54b7cc9876 100644 --- a/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/swap/asset/AssetSwapFlowViewModel.kt +++ b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/swap/asset/AssetSwapFlowViewModel.kt @@ -10,15 +10,15 @@ import io.novafoundation.nova.feature_assets.R import io.novafoundation.nova.feature_assets.domain.assets.ExternalBalancesInteractor import io.novafoundation.nova.feature_assets.domain.assets.models.AssetsByViewModeResult import io.novafoundation.nova.feature_assets.domain.assets.search.AssetSearchInteractorFactory +import io.novafoundation.nova.feature_assets.domain.common.AssetBalance import io.novafoundation.nova.feature_assets.domain.common.AssetWithNetwork -import io.novafoundation.nova.feature_assets.domain.common.NetworkAssetGroup import io.novafoundation.nova.feature_assets.domain.common.AssetWithOffChainBalance +import io.novafoundation.nova.feature_assets.domain.common.NetworkAssetGroup import io.novafoundation.nova.feature_assets.domain.common.TokenAssetGroup -import io.novafoundation.nova.feature_assets.domain.common.AssetBalance import io.novafoundation.nova.feature_assets.presentation.AssetsRouter import io.novafoundation.nova.feature_assets.presentation.balance.common.ControllableAssetCheckMixin -import io.novafoundation.nova.feature_assets.presentation.balance.common.mappers.mapTokenAssetGroupToUi import io.novafoundation.nova.feature_assets.presentation.balance.common.mappers.mapGroupedAssetsToUi +import io.novafoundation.nova.feature_assets.presentation.balance.common.mappers.mapTokenAssetGroupToUi import io.novafoundation.nova.feature_assets.presentation.balance.list.model.items.BalanceListRvItem import io.novafoundation.nova.feature_assets.presentation.balance.list.model.items.TokenGroupUi import io.novafoundation.nova.feature_assets.presentation.flow.asset.AssetFlowViewModel @@ -57,17 +57,7 @@ class AssetSwapFlowViewModel( ) { init { - launch { - if (payload is SwapFlowPayload.InitialSelecting) { - swapAvailabilityInteractor.sync(viewModelScope) - } - } - - launch { - if (payload is SwapFlowPayload.InitialSelecting) { - interactor.warmUpSwapCommonlyUsedChains(viewModelScope) - } - } + launchInitialSwapSync() } @StringRes @@ -111,4 +101,12 @@ class AssetSwapFlowViewModel( mapTokenAssetGroupToUi(assetIconProvider, group, assets = assets) { it.groupBalance.transferable } } } + + private fun launchInitialSwapSync() { + if (swapPayload is SwapFlowPayload.InitialSelecting) { + launch { swapAvailabilityInteractor.warmUpCommonlyUsedChains(viewModelScope) } + + launch { swapAvailabilityInteractor.sync(viewModelScope) } + } + } } diff --git a/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/swap/executor/ReselectSwapFlowExecutor.kt b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/swap/executor/ReselectSwapFlowExecutor.kt index 7d89f5540f..8cefe5b99d 100644 --- a/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/swap/executor/ReselectSwapFlowExecutor.kt +++ b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/swap/executor/ReselectSwapFlowExecutor.kt @@ -18,9 +18,9 @@ class ReselectSwapFlowExecutor( override suspend fun openNextScreen(coroutineScope: CoroutineScope, chainAsset: Chain.Asset) { val state = swapSettingsStateProvider.getSwapSettingsState(coroutineScope) when (selectingDirection) { - SelectingDirection.IN -> state.setAssetInUpdatingFee(chainAsset) + SelectingDirection.IN -> state.setAssetIn(chainAsset) SelectingDirection.OUT -> state.setAssetOut(chainAsset) } - assetsRouter.back() + assetsRouter.returnToMainSwapScreen() } } diff --git a/feature-swap-api/src/main/java/io/novafoundation/nova/feature_swap_api/domain/interactor/SwapAvailabilityInteractor.kt b/feature-swap-api/src/main/java/io/novafoundation/nova/feature_swap_api/domain/interactor/SwapAvailabilityInteractor.kt index 4030c598c1..d5f1063dfc 100644 --- a/feature-swap-api/src/main/java/io/novafoundation/nova/feature_swap_api/domain/interactor/SwapAvailabilityInteractor.kt +++ b/feature-swap-api/src/main/java/io/novafoundation/nova/feature_swap_api/domain/interactor/SwapAvailabilityInteractor.kt @@ -8,6 +8,8 @@ interface SwapAvailabilityInteractor { suspend fun sync(coroutineScope: CoroutineScope) + suspend fun warmUpCommonlyUsedChains(computationScope: CoroutineScope) + fun anySwapAvailableFlow(): Flow suspend fun swapAvailableFlow(asset: Chain.Asset, coroutineScope: CoroutineScope): Flow diff --git a/feature-swap-api/src/main/java/io/novafoundation/nova/feature_swap_api/presentation/state/SwapSettingsState.kt b/feature-swap-api/src/main/java/io/novafoundation/nova/feature_swap_api/presentation/state/SwapSettingsState.kt index 0587f53b83..e89a6a6dc4 100644 --- a/feature-swap-api/src/main/java/io/novafoundation/nova/feature_swap_api/presentation/state/SwapSettingsState.kt +++ b/feature-swap-api/src/main/java/io/novafoundation/nova/feature_swap_api/presentation/state/SwapSettingsState.kt @@ -8,7 +8,7 @@ import io.novafoundation.nova.runtime.state.SelectedOptionSharedState interface SwapSettingsState : SelectedOptionSharedState { - suspend fun setAssetInUpdatingFee(asset: Chain.Asset) + suspend fun setAssetIn(asset: Chain.Asset) fun setAssetOut(asset: Chain.Asset) diff --git a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/di/SwapFeatureDependencies.kt b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/di/SwapFeatureDependencies.kt index 6367cbd0b6..cd981130d7 100644 --- a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/di/SwapFeatureDependencies.kt +++ b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/di/SwapFeatureDependencies.kt @@ -38,7 +38,6 @@ import io.novafoundation.nova.feature_wallet_api.domain.interfaces.CrossChainTra import io.novafoundation.nova.feature_wallet_api.domain.interfaces.TokenRepository import io.novafoundation.nova.feature_wallet_api.domain.interfaces.WalletRepository 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.v2.FeeLoaderMixinV2 import io.novafoundation.nova.runtime.call.MultiChainRuntimeCallsApi import io.novafoundation.nova.runtime.di.REMOTE_STORAGE_SOURCE @@ -51,8 +50,6 @@ import javax.inject.Named interface SwapFeatureDependencies { - val feeLoaderMixinFactory: FeeLoaderMixin.Factory - val validationExecutor: ValidationExecutor val preferences: Preferences diff --git a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/domain/interactor/RealSwapAvailabilityInteractor.kt b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/domain/interactor/RealSwapAvailabilityInteractor.kt index e40b5b8f79..fc967b3f6e 100644 --- a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/domain/interactor/RealSwapAvailabilityInteractor.kt +++ b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/domain/interactor/RealSwapAvailabilityInteractor.kt @@ -19,6 +19,10 @@ class RealSwapAvailabilityInteractor( swapService.sync(coroutineScope) } + override suspend fun warmUpCommonlyUsedChains(computationScope: CoroutineScope) { + swapService.warmUpCommonChains(computationScope) + } + override fun anySwapAvailableFlow(): Flow { return chainRegistry.enabledChainsFlow().map { it.any(Chain::isSwapSupported) } } diff --git a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/domain/swap/RealSwapService.kt b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/domain/swap/RealSwapService.kt index 4efb13085a..1954326c3f 100644 --- a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/domain/swap/RealSwapService.kt +++ b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/domain/swap/RealSwapService.kt @@ -134,8 +134,10 @@ internal class RealSwapService( override suspend fun warmUpCommonChains(computationScope: CoroutineScope): Result { return runCatching { - warmUpChain(Chain.Geneses.HYDRA_DX, computationScope) - warmUpChain(Chain.Geneses.POLKADOT_ASSET_HUB, computationScope) + withContext(Dispatchers.Default) { + warmUpChain(Chain.Geneses.HYDRA_DX, computationScope) + warmUpChain(Chain.Geneses.POLKADOT_ASSET_HUB, computationScope) + } } } diff --git a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/confirmation/SwapConfirmationViewModel.kt b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/confirmation/SwapConfirmationViewModel.kt index f9330206d9..2b39cc66ef 100644 --- a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/confirmation/SwapConfirmationViewModel.kt +++ b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/confirmation/SwapConfirmationViewModel.kt @@ -22,9 +22,7 @@ import io.novafoundation.nova.feature_account_api.presenatation.account.wallet.W import io.novafoundation.nova.feature_account_api.presenatation.account.wallet.WalletUiUseCase import io.novafoundation.nova.feature_account_api.presenatation.actions.ExternalActions import io.novafoundation.nova.feature_account_api.presenatation.actions.showAddressActions -import io.novafoundation.nova.feature_account_api.presenatation.chain.icon import io.novafoundation.nova.feature_account_api.presenatation.chain.getAssetIconOrFallback -import io.novafoundation.nova.feature_swap_core.domain.model.SwapDirection import io.novafoundation.nova.feature_swap_api.domain.model.SwapFee import io.novafoundation.nova.feature_swap_api.domain.model.SwapProgress import io.novafoundation.nova.feature_swap_api.domain.model.SwapQuote diff --git a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/main/SwapMainSettingsViewModel.kt b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/main/SwapMainSettingsViewModel.kt index daa18afd63..f795e73bb7 100644 --- a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/main/SwapMainSettingsViewModel.kt +++ b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/main/SwapMainSettingsViewModel.kt @@ -282,7 +282,7 @@ class SwapMainSettingsViewModel( init { initPayload() -// launch { swapInteractor.warmUpSwapCommonlyUsedChains(viewModelScope) } + launch { swapInteractor.warmUpSwapCommonlyUsedChains(viewModelScope) } handleInputChanges(amountInInput, SwapSettings::assetIn, SwapDirection.SPECIFIED_IN) handleInputChanges(amountOutInput, SwapSettings::assetOut, SwapDirection.SPECIFIED_OUT) @@ -425,7 +425,7 @@ class SwapMainSettingsViewModel( val assetIn = chainRegistry.asset(payload.assetIn.fullChainAssetId) val swapSettingsState = swapSettingState.await() when (payload) { - is SwapSettingsPayload.DefaultFlow -> swapSettingState().setAssetInUpdatingFee(assetIn) + is SwapSettingsPayload.DefaultFlow -> swapSettingState().setAssetIn(assetIn) is SwapSettingsPayload.RepeatOperation -> { val assetOut = chainRegistry.asset(payload.assetOut.fullChainAssetId) diff --git a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/state/RealSwapSettingsState.kt b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/state/RealSwapSettingsState.kt index 5381053a6a..830d0fa907 100644 --- a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/state/RealSwapSettingsState.kt +++ b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/state/RealSwapSettingsState.kt @@ -17,7 +17,7 @@ class RealSwapSettingsState( override val selectedOption = MutableStateFlow(initialValue) - override suspend fun setAssetInUpdatingFee(asset: Chain.Asset) { + override suspend fun setAssetIn(asset: Chain.Asset) { val current = selectedOption.value val newPlanks = current.convertedAmountForNewAssetIn(asset) From 131147f9706575f85e9b26cda79a30e8d196e4ca Mon Sep 17 00:00:00 2001 From: valentun Date: Wed, 6 Nov 2024 14:23:34 +0700 Subject: [PATCH 41/83] Route short view --- .../common/domain/ExtendedLoadingState.kt | 8 ++ .../nova/common/view/GenericTableCellView.kt | 116 ++++++++++++++++++ .../res/layout/view_generic_table_cell.xml | 48 ++++++++ common/src/main/res/values/attrs.xml | 5 + common/src/main/res/values/strings.xml | 1 + .../feature_swap_impl/di/SwapFeatureModule.kt | 8 ++ .../common/route/SwapRouteFormatter.kt | 50 ++++++++ .../common/route/SwapRouteModel.kt | 10 ++ .../common/route/SwapRouteTableCellView.kt | 35 ++++++ .../common/route/SwapRouteView.kt | 67 ++++++++++ .../confirmation/SwapConfirmationFragment.kt | 2 + .../confirmation/SwapConfirmationViewModel.kt | 40 +++--- .../confirmation/di/SwapConfirmationModule.kt | 5 +- .../model/SwapConfirmationDetailsModel.kt | 4 +- .../LiquidityFieldValidator.kt | 2 +- .../presentation/main/QuotingState.kt | 4 +- .../main/SwapMainSettingsFragment.kt | 2 + .../main/SwapMainSettingsViewModel.kt | 34 +++-- .../main/di/SwapMainSettingsModule.kt | 5 +- .../layout/fragment_main_swap_settings.xml | 6 + .../fragment_swap_confirmation_settings.xml | 6 + 21 files changed, 420 insertions(+), 38 deletions(-) create mode 100644 common/src/main/java/io/novafoundation/nova/common/view/GenericTableCellView.kt create mode 100644 common/src/main/res/layout/view_generic_table_cell.xml create mode 100644 feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/common/route/SwapRouteFormatter.kt create mode 100644 feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/common/route/SwapRouteModel.kt create mode 100644 feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/common/route/SwapRouteTableCellView.kt create mode 100644 feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/common/route/SwapRouteView.kt diff --git a/common/src/main/java/io/novafoundation/nova/common/domain/ExtendedLoadingState.kt b/common/src/main/java/io/novafoundation/nova/common/domain/ExtendedLoadingState.kt index b1c23d5159..e63d447658 100644 --- a/common/src/main/java/io/novafoundation/nova/common/domain/ExtendedLoadingState.kt +++ b/common/src/main/java/io/novafoundation/nova/common/domain/ExtendedLoadingState.kt @@ -42,6 +42,10 @@ val ExtendedLoadingState.dataOrNull: T? else -> null } +@get:JvmName("isErrorProp") +val ExtendedLoadingState<*>.isError: Boolean + get() = this is ExtendedLoadingState.Error + fun ExtendedLoadingState.loadedAndEmpty(): Boolean = when (this) { is ExtendedLoadingState.Loaded -> data == null else -> false @@ -55,6 +59,10 @@ fun ExtendedLoadingState<*>.isLoading(): Boolean { return this is ExtendedLoadingState.Loading } +@get:JvmName("isLoadingProp") +val ExtendedLoadingState<*>.isLoading: Boolean + get() = isLoading() + fun ExtendedLoadingState<*>.isError(): Boolean { return this is ExtendedLoadingState.Error } diff --git a/common/src/main/java/io/novafoundation/nova/common/view/GenericTableCellView.kt b/common/src/main/java/io/novafoundation/nova/common/view/GenericTableCellView.kt new file mode 100644 index 0000000000..431e675203 --- /dev/null +++ b/common/src/main/java/io/novafoundation/nova/common/view/GenericTableCellView.kt @@ -0,0 +1,116 @@ +package io.novafoundation.nova.common.view + +import android.content.Context +import android.util.AttributeSet +import android.view.View +import androidx.annotation.DrawableRes +import androidx.annotation.StringRes +import androidx.constraintlayout.widget.ConstraintLayout +import androidx.core.view.children +import io.novafoundation.nova.common.R +import io.novafoundation.nova.common.utils.dp +import io.novafoundation.nova.common.utils.getResourceIdOrNull +import io.novafoundation.nova.common.utils.setDrawableEnd +import io.novafoundation.nova.common.utils.setVisible +import io.novafoundation.nova.common.utils.useAttributes +import kotlinx.android.synthetic.main.view_generic_table_cell.view.genericTableCellTitle +import kotlinx.android.synthetic.main.view_generic_table_cell.view.genericTableCellValueDivider +import kotlinx.android.synthetic.main.view_generic_table_cell.view.genericTableCellValueProgress + +open class GenericTableCellView @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null, + defStyle: Int = 0, + defStyleRes: Int = 0, +) : ConstraintLayout(context, attrs, defStyle, defStyleRes), HasDivider { + + protected lateinit var valueView: V + + companion object { + + private val SELF_IDS = listOf(R.id.genericTableCellValueDivider, R.id.genericTableCellTitle, R.id.genericTableCellValueProgress) + } + + init { + minHeight = 44.dp + + View.inflate(context, R.layout.view_generic_table_cell, this) + + attrs?.let(::applyAttributes) + } + + override fun setDividerVisible(visible: Boolean) { + genericTableCellValueDivider.setVisible(visible) + } + + override fun onFinishInflate() { + super.onFinishInflate() + + findAndPositionValueView() + } + + @Suppress("UNCHECKED_CAST") + private fun findAndPositionValueView() { + children.forEach { + if (it.id !in SELF_IDS) { + valueView = it as V + valueView.layoutParams = createValueViewLayoutParams() + requestLayout() + } + } + } + + override fun addView(child: View) { + if (child.id in SELF_IDS) { + super.addView(child) + } else { + addValueView(child) + } + } + + fun showProgress(showProgress: Boolean) { + genericTableCellValueProgress.setVisible(showProgress) + valueView.setVisible(!showProgress) + } + + fun setTitle(title: String?) { + genericTableCellTitle.text = title + } + + fun setTitle(@StringRes titleRes: Int) { + genericTableCellTitle.setText(titleRes) + } + + fun setTitleIconEnd(@DrawableRes icon: Int?) { + genericTableCellTitle.setDrawableEnd(icon, widthInDp = 16, paddingInDp = 4) + } + + @JvmName("setValueContentView") + protected fun setValueView(view: V) { + addValueView(view) + } + + @Suppress("UNCHECKED_CAST") + private fun addValueView(child: View) { + valueView = child as V + super.addView(child, createValueViewLayoutParams()) + } + + private fun createValueViewLayoutParams() = LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT).apply { + endToEnd = LayoutParams.PARENT_ID + startToEnd = R.id.genericTableCellTitle + marginStart = 16.dp + topToTop = LayoutParams.PARENT_ID + bottomToBottom = LayoutParams.PARENT_ID + horizontalBias = 1.0f + constrainedWidth = true + } + + private fun applyAttributes(attrs: AttributeSet) = context.useAttributes(attrs, R.styleable.GenericTableCellView) { typedArray -> + val titleText = typedArray.getString(R.styleable.GenericTableCellView_title) + setTitle(titleText) + + val titleIconEnd = typedArray.getResourceIdOrNull(R.styleable.GenericTableCellView_titleIcon) + titleIconEnd?.let(::setTitleIconEnd) + } +} diff --git a/common/src/main/res/layout/view_generic_table_cell.xml b/common/src/main/res/layout/view_generic_table_cell.xml new file mode 100644 index 0000000000..9d7488b5ca --- /dev/null +++ b/common/src/main/res/layout/view_generic_table_cell.xml @@ -0,0 +1,48 @@ + + + + + + + + + + \ No newline at end of file diff --git a/common/src/main/res/values/attrs.xml b/common/src/main/res/values/attrs.xml index c8898dffd1..9b1346923a 100644 --- a/common/src/main/res/values/attrs.xml +++ b/common/src/main/res/values/attrs.xml @@ -108,6 +108,11 @@ + + + + + diff --git a/common/src/main/res/values/strings.xml b/common/src/main/res/values/strings.xml index 92b65c89e5..86faa3bc6e 100644 --- a/common/src/main/res/values/strings.xml +++ b/common/src/main/res/values/strings.xml @@ -1,5 +1,6 @@ + Route Send only %1$s token and tokens in %2$s network to this address, or you might lose your funds diff --git a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/di/SwapFeatureModule.kt b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/di/SwapFeatureModule.kt index 414ed6a39a..614d03a379 100644 --- a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/di/SwapFeatureModule.kt +++ b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/di/SwapFeatureModule.kt @@ -32,6 +32,8 @@ import io.novafoundation.nova.feature_swap_impl.presentation.common.PriceImpactF import io.novafoundation.nova.feature_swap_impl.presentation.common.RealPriceImpactFormatter import io.novafoundation.nova.feature_swap_impl.presentation.common.RealSwapRateFormatter import io.novafoundation.nova.feature_swap_impl.presentation.common.SlippageAlertMixinFactory +import io.novafoundation.nova.feature_swap_impl.presentation.common.route.RealSwapRouteFormatter +import io.novafoundation.nova.feature_swap_impl.presentation.common.route.SwapRouteFormatter import io.novafoundation.nova.feature_swap_impl.presentation.common.state.RealSwapStateStoreProvider import io.novafoundation.nova.feature_swap_impl.presentation.common.state.SwapStateStoreProvider import io.novafoundation.nova.feature_swap_impl.presentation.mixin.maxAction.MaxActionProviderFactory @@ -192,4 +194,10 @@ class SwapFeatureModule { fun provideSwapQuoteStoreProvider(computationalCache: ComputationalCache): SwapStateStoreProvider { return RealSwapStateStoreProvider(computationalCache) } + + @Provides + @FeatureScope + fun provideSwapRouteFormatter(chainRegistry: ChainRegistry): SwapRouteFormatter { + return RealSwapRouteFormatter(chainRegistry) + } } diff --git a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/common/route/SwapRouteFormatter.kt b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/common/route/SwapRouteFormatter.kt new file mode 100644 index 0000000000..f6eb7e8b1e --- /dev/null +++ b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/common/route/SwapRouteFormatter.kt @@ -0,0 +1,50 @@ +package io.novafoundation.nova.feature_swap_impl.presentation.common.route + +import io.novafoundation.nova.common.utils.graph.Path +import io.novafoundation.nova.feature_account_api.data.mappers.mapChainToUi +import io.novafoundation.nova.feature_swap_api.domain.model.SwapGraphEdge +import io.novafoundation.nova.feature_swap_api.domain.model.SwapQuote +import io.novafoundation.nova.feature_swap_core_api.data.paths.model.QuotedEdge +import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry +import io.novafoundation.nova.runtime.multiNetwork.chain.model.ChainId +import io.novafoundation.nova.runtime.multiNetwork.chainsById + +interface SwapRouteFormatter { + + suspend fun formatSwapRoute(quote: SwapQuote): SwapRouteModel? +} + +class RealSwapRouteFormatter( + private val chainRegistry: ChainRegistry +) : SwapRouteFormatter { + + override suspend fun formatSwapRoute(quote: SwapQuote): SwapRouteModel? { + val routeChainIds = determinePathChains(quote.quotedPath.path) ?: return null + + val allKnownChains = chainRegistry.chainsById() + val chainModels = routeChainIds.map { mapChainToUi(allKnownChains[it]!!) } + + return SwapRouteModel(chainModels) + } + + private fun determinePathChains(path: Path>): List? { + // Do not display path of less then 2 elements + if (path.size < 2) return null + + val firstEdge = path.first().edge + val firstChain = firstEdge.to.chainId + + var currentChainId = firstChain + val foundChains = mutableListOf(currentChainId) + + path.forEach { + val nextChainId = it.edge.to.chainId + if (nextChainId != currentChainId) { + currentChainId = nextChainId + foundChains.add(nextChainId) + } + } + + return foundChains + } +} diff --git a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/common/route/SwapRouteModel.kt b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/common/route/SwapRouteModel.kt new file mode 100644 index 0000000000..95f57b9480 --- /dev/null +++ b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/common/route/SwapRouteModel.kt @@ -0,0 +1,10 @@ +package io.novafoundation.nova.feature_swap_impl.presentation.common.route + +import io.novafoundation.nova.common.domain.ExtendedLoadingState +import io.novafoundation.nova.feature_account_api.presenatation.chain.ChainUi + +class SwapRouteModel( + val chains: List +) + +typealias SwapRouteState = ExtendedLoadingState diff --git a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/common/route/SwapRouteTableCellView.kt b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/common/route/SwapRouteTableCellView.kt new file mode 100644 index 0000000000..d6dd8e9ac3 --- /dev/null +++ b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/common/route/SwapRouteTableCellView.kt @@ -0,0 +1,35 @@ +package io.novafoundation.nova.feature_swap_impl.presentation.common.route + +import android.content.Context +import android.util.AttributeSet +import io.novafoundation.nova.common.domain.isError +import io.novafoundation.nova.common.domain.isLoading +import io.novafoundation.nova.common.domain.onLoaded +import io.novafoundation.nova.common.utils.setVisible +import io.novafoundation.nova.common.view.GenericTableCellView +import io.novafoundation.nova.feature_swap_impl.R + +class SwapRouteTableCellView @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null, + defStyle: Int = 0, + defStyleRes: Int = 0, +) : GenericTableCellView(context, attrs, defStyle, defStyleRes) { + + init { + setValueView(SwapRouteView(context)) + setTitle(R.string.swap_route) + } + + fun setSwapRouteState(routeState: SwapRouteState) { + setVisible(!routeState.isError) + + showProgress(routeState.isLoading) + + routeState.onLoaded { routeModel -> + setVisible(routeModel != null) + + routeModel?.let(valueView::setModel) + } + } +} diff --git a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/common/route/SwapRouteView.kt b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/common/route/SwapRouteView.kt new file mode 100644 index 0000000000..02408ad96a --- /dev/null +++ b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/common/route/SwapRouteView.kt @@ -0,0 +1,67 @@ +package io.novafoundation.nova.feature_swap_impl.presentation.common.route + +import android.content.Context +import android.util.AttributeSet +import android.view.Gravity +import android.widget.ImageView +import android.widget.LinearLayout +import androidx.core.view.updateMargins +import coil.ImageLoader +import io.novafoundation.nova.common.di.FeatureUtils +import io.novafoundation.nova.common.utils.dp +import io.novafoundation.nova.common.utils.setImageTintRes +import io.novafoundation.nova.feature_account_api.presenatation.chain.ChainUi +import io.novafoundation.nova.feature_account_api.presenatation.chain.loadChainIcon +import io.novafoundation.nova.feature_swap_impl.R + +class SwapRouteView @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null, + defStyleAttr: Int = 0, +) : LinearLayout(context, attrs, defStyleAttr) { + + private val imageLoader: ImageLoader by lazy(LazyThreadSafetyMode.NONE) { + FeatureUtils.getCommonApi(context).imageLoader() + } + + init { + orientation = HORIZONTAL + gravity = Gravity.CENTER_VERTICAL + } + + fun setModel(model: SwapRouteModel) { + removeAllViews() + + addViewsFor(model) + } + + private fun addViewsFor(model: SwapRouteModel) { + model.chains.forEachIndexed { index, chainUi -> + val hasNext = index < model.chains.size - 1 + + addChainView(chainUi) + if (hasNext) { + addArrow() + } + } + } + + private fun addChainView(chainUi: ChainUi) { + ImageView(context).apply { + layoutParams = LayoutParams(16.dp, 16.dp) + + loadChainIcon(chainUi.icon, imageLoader) + }.also(::addView) + } + + private fun addArrow() { + ImageView(context).apply { + layoutParams = LayoutParams(12.dp, 12.dp).apply { + updateMargins(left = 4.dp, right = 4.dp) + } + + setImageResource(R.drawable.ic_arrow_right) + setImageTintRes(R.color.icon_secondary) + }.also(::addView) + } +} diff --git a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/confirmation/SwapConfirmationFragment.kt b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/confirmation/SwapConfirmationFragment.kt index 5878ed10cd..08dc177def 100644 --- a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/confirmation/SwapConfirmationFragment.kt +++ b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/confirmation/SwapConfirmationFragment.kt @@ -26,6 +26,7 @@ import kotlinx.android.synthetic.main.fragment_swap_confirmation_settings.swapCo import kotlinx.android.synthetic.main.fragment_swap_confirmation_settings.swapConfirmationNetworkFee import kotlinx.android.synthetic.main.fragment_swap_confirmation_settings.swapConfirmationPriceDifference import kotlinx.android.synthetic.main.fragment_swap_confirmation_settings.swapConfirmationRate +import kotlinx.android.synthetic.main.fragment_swap_confirmation_settings.swapConfirmationRoute import kotlinx.android.synthetic.main.fragment_swap_confirmation_settings.swapConfirmationSlippage import kotlinx.android.synthetic.main.fragment_swap_confirmation_settings.swapConfirmationToolbar import kotlinx.android.synthetic.main.fragment_swap_confirmation_settings.swapConfirmationWallet @@ -74,6 +75,7 @@ class SwapConfirmationFragment : BaseFragment() { swapConfirmationRate.showValue(it.rate) swapConfirmationPriceDifference.showValueOrHide(it.priceDifference) swapConfirmationSlippage.showValue(it.slippage) + swapConfirmationRoute.setSwapRouteState(it.swapRouteState) } viewModel.wallet.observe { swapConfirmationWallet.showWallet(it) } diff --git a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/confirmation/SwapConfirmationViewModel.kt b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/confirmation/SwapConfirmationViewModel.kt index 2b39cc66ef..403dc9bd10 100644 --- a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/confirmation/SwapConfirmationViewModel.kt +++ b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/confirmation/SwapConfirmationViewModel.kt @@ -4,6 +4,7 @@ import androidx.lifecycle.viewModelScope import io.novafoundation.nova.common.address.AddressIconGenerator import io.novafoundation.nova.common.address.AddressModel import io.novafoundation.nova.common.base.BaseViewModel +import io.novafoundation.nova.common.domain.ExtendedLoadingState import io.novafoundation.nova.common.mixin.api.Validatable import io.novafoundation.nova.common.presentation.AssetIconProvider import io.novafoundation.nova.common.resources.ResourceManager @@ -42,6 +43,7 @@ import io.novafoundation.nova.feature_swap_impl.presentation.common.PriceImpactF import io.novafoundation.nova.feature_swap_impl.presentation.common.SlippageAlertMixinFactory import io.novafoundation.nova.feature_swap_impl.presentation.common.fee.SwapFeeFormatter import io.novafoundation.nova.feature_swap_impl.presentation.common.fee.SwapFeeInspector +import io.novafoundation.nova.feature_swap_impl.presentation.common.route.SwapRouteFormatter import io.novafoundation.nova.feature_swap_impl.presentation.common.state.SwapStateStoreProvider import io.novafoundation.nova.feature_swap_impl.presentation.common.state.getStateOrThrow import io.novafoundation.nova.feature_swap_impl.presentation.confirmation.model.SwapConfirmationDetailsModel @@ -61,11 +63,11 @@ import io.novafoundation.nova.runtime.ext.fullId import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.filter -import kotlinx.coroutines.flow.filterNotNull import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.launchIn @@ -80,7 +82,6 @@ import java.math.BigInteger private data class SwapConfirmationState( val swapQuoteArgs: SwapQuoteArgs, val swapQuote: SwapQuote, - val feeAsset: Chain.Asset ) enum class MaxAction { @@ -108,19 +109,19 @@ class SwapConfirmationViewModel( private val descriptionBottomSheetLauncher: DescriptionBottomSheetLauncher, private val arbitraryAssetUseCase: ArbitraryAssetUseCase, private val maxActionProviderFactory: MaxActionProviderFactory, + private val swapRouteFormatter: SwapRouteFormatter, private val assetIconProvider: AssetIconProvider ) : BaseViewModel(), ExternalActions by externalActions, Validatable by validationExecutor, DescriptionBottomSheetLauncher by descriptionBottomSheetLauncher { - private val confirmationStateFlow = MutableStateFlow(null) + private val confirmationStateFlow = MutableSharedFlow() private val metaAccountFlow = accountRepository.selectedMetaAccountFlow() .shareInBackground() private val slippageConfigFlow = confirmationStateFlow - .filterNotNull() .mapNotNull { swapInteractor.slippageConfig(it.swapQuote.assetIn.chainId) } .shareInBackground() @@ -148,11 +149,6 @@ class SwapConfirmationViewModel( private val maxActionFlow = MutableStateFlow(MaxAction.DISABLED) - // TODO multi chain fees - private val feeTokenFlow = assetInFlow - .map { it.token } - .shareInBackground() - val feeMixin = feeLoaderMixinFactory.create( scope = viewModelScope, selectedChainAssetFlow = initialSwapState.map { it.quote.assetIn }, @@ -166,7 +162,7 @@ class SwapConfirmationViewModel( val validationProgress = _submissionInProgress - val swapDetails = confirmationStateFlow.filterNotNull().map { + val swapDetails = confirmationStateFlow.map { formatToSwapDetailsModel(it) } @@ -281,7 +277,8 @@ class SwapConfirmationViewModel( ), rate = formatRate(confirmationState.swapQuote.swapRate(), assetIn, assetOut), priceDifference = formatPriceDifference(confirmationState.swapQuote.priceImpact), - slippage = slippageFlow.first().formatPercents() + slippage = slippageFlow.first().formatPercents(), + swapRouteState = ExtendedLoadingState.Loaded(swapRouteFormatter.formatSwapRoute(confirmationState.swapQuote)) ) } @@ -346,9 +343,9 @@ class SwapConfirmationViewModel( } } - private fun setMinAmountOut(asset: Chain.Asset, amount: Balance) { + private suspend fun setMinAmountOut(asset: Chain.Asset, amount: Balance) { maxActionFlow.value = MaxAction.DISABLED - val confirmationState = confirmationStateFlow.value ?: return + val confirmationState = confirmationStateFlow.first() runQuoting( confirmationState.swapQuoteArgs.copy( amount = amount, @@ -357,12 +354,12 @@ class SwapConfirmationViewModel( ) } - private fun runQuoting(newSwapQuoteArgs: SwapQuoteArgs) { + private suspend fun runQuoting(newSwapQuoteArgs: SwapQuoteArgs) { // TODO return launch { - val confirmationState = confirmationStateFlow.value ?: return@launch + val confirmationState = confirmationStateFlow.first() val swapQuote = swapInteractor.quote(newSwapQuoteArgs, viewModelScope) .onFailure { } .getOrNull() ?: return@launch @@ -377,7 +374,8 @@ class SwapConfirmationViewModel( swapInteractor.estimateFee(executeArgs) } - confirmationStateFlow.value = confirmationState.copy(swapQuoteArgs = newSwapQuoteArgs, swapQuote = swapQuote) + val newState = confirmationState.copy(swapQuoteArgs = newSwapQuoteArgs, swapQuote = swapQuote) + confirmationStateFlow.emit(newState) } } @@ -405,12 +403,8 @@ class SwapConfirmationViewModel( feeMixin.setFee(swapState.fee) - confirmationStateFlow.value = SwapConfirmationState( - swapQuoteArgs = quoteArgs, - swapQuote = swapQuote, - // TOOD multichain fees - feeAsset = swapState.quote.assetIn - ) + val newState = SwapConfirmationState(quoteArgs, swapQuote) + confirmationStateFlow.emit(newState) } } @@ -420,7 +414,7 @@ class SwapConfirmationViewModel( .mapNotNull { it.second?.balance } .distinctUntilChanged() .onEach { - val confirmationState = confirmationStateFlow.value ?: return@onEach + val confirmationState = confirmationStateFlow.first() runQuoting( confirmationState.swapQuoteArgs.copy( amount = it, diff --git a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/confirmation/di/SwapConfirmationModule.kt b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/confirmation/di/SwapConfirmationModule.kt index 37ed3600af..351da19803 100644 --- a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/confirmation/di/SwapConfirmationModule.kt +++ b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/confirmation/di/SwapConfirmationModule.kt @@ -21,6 +21,7 @@ import io.novafoundation.nova.feature_swap_impl.domain.interactor.SwapInteractor import io.novafoundation.nova.feature_swap_impl.presentation.SwapRouter import io.novafoundation.nova.feature_swap_impl.presentation.common.PriceImpactFormatter import io.novafoundation.nova.feature_swap_impl.presentation.common.SlippageAlertMixinFactory +import io.novafoundation.nova.feature_swap_impl.presentation.common.route.SwapRouteFormatter import io.novafoundation.nova.feature_swap_impl.presentation.common.state.SwapStateStoreProvider import io.novafoundation.nova.feature_swap_impl.presentation.confirmation.SwapConfirmationViewModel import io.novafoundation.nova.feature_swap_impl.presentation.mixin.maxAction.MaxActionProviderFactory @@ -56,6 +57,7 @@ class SwapConfirmationModule { assetUseCase: ArbitraryAssetUseCase, maxActionProviderFactory: MaxActionProviderFactory, swapStateStoreProvider: SwapStateStoreProvider, + swapRouteFormatter: SwapRouteFormatter, assetIconProvider: AssetIconProvider ): ViewModel { return SwapConfirmationViewModel( @@ -78,7 +80,8 @@ class SwapConfirmationModule { descriptionBottomSheetLauncher = descriptionBottomSheetLauncher, arbitraryAssetUseCase = assetUseCase, maxActionProviderFactory = maxActionProviderFactory, - assetIconProvider = assetIconProvider + assetIconProvider = assetIconProvider, + swapRouteFormatter = swapRouteFormatter ) } diff --git a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/confirmation/model/SwapConfirmationDetailsModel.kt b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/confirmation/model/SwapConfirmationDetailsModel.kt index c602591783..b3ff3b4de8 100644 --- a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/confirmation/model/SwapConfirmationDetailsModel.kt +++ b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/confirmation/model/SwapConfirmationDetailsModel.kt @@ -1,10 +1,12 @@ package io.novafoundation.nova.feature_swap_impl.presentation.confirmation.model import io.novafoundation.nova.feature_swap_api.presentation.view.SwapAssetsView +import io.novafoundation.nova.feature_swap_impl.presentation.common.route.SwapRouteState class SwapConfirmationDetailsModel( val assets: SwapAssetsView.Model, val rate: String, val priceDifference: CharSequence?, - val slippage: String + val slippage: String, + val swapRouteState: SwapRouteState, ) diff --git a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/fieldValidation/LiquidityFieldValidator.kt b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/fieldValidation/LiquidityFieldValidator.kt index fdb0429051..5df4b12209 100644 --- a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/fieldValidation/LiquidityFieldValidator.kt +++ b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/fieldValidation/LiquidityFieldValidator.kt @@ -24,7 +24,7 @@ class LiquidityFieldValidator( override fun observe(inputStream: Flow): Flow { return quotingStateFlow.map { quotingState -> - if (quotingState is QuotingState.NotAvailable) { + if (quotingState is QuotingState.Error) { FieldValidationResult.Error( resourceManager.getString(R.string.swap_field_validation_not_enough_liquidity) ) diff --git a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/main/QuotingState.kt b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/main/QuotingState.kt index ecff0b1953..298837bbcf 100644 --- a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/main/QuotingState.kt +++ b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/main/QuotingState.kt @@ -9,7 +9,7 @@ sealed class QuotingState { object Loading : QuotingState() - object NotAvailable : QuotingState() + data class Error(val error: Throwable): QuotingState() - data class Loaded(val value: SwapQuote, val quoteArgs: SwapQuoteArgs) : QuotingState() + data class Loaded(val quote: SwapQuote, val quoteArgs: SwapQuoteArgs) : QuotingState() } diff --git a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/main/SwapMainSettingsFragment.kt b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/main/SwapMainSettingsFragment.kt index d11392ea5c..c240ec22f5 100644 --- a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/main/SwapMainSettingsFragment.kt +++ b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/main/SwapMainSettingsFragment.kt @@ -33,6 +33,7 @@ import kotlinx.android.synthetic.main.fragment_main_swap_settings.swapMainSettin import kotlinx.android.synthetic.main.fragment_main_swap_settings.swapMainSettingsMaxAmount import kotlinx.android.synthetic.main.fragment_main_swap_settings.swapMainSettingsPayInput import kotlinx.android.synthetic.main.fragment_main_swap_settings.swapMainSettingsReceiveInput +import kotlinx.android.synthetic.main.fragment_main_swap_settings.swapMainSettingsRoute import kotlinx.android.synthetic.main.fragment_main_swap_settings.swapMainSettingsToolbar import javax.inject.Inject @@ -100,6 +101,7 @@ class SwapMainSettingsFragment : BaseFragment() { buyMixinUi.setupBuyIntegration(this, viewModel.buyMixin) viewModel.rateDetails.observe { swapMainSettingsDetailsRate.showLoadingValue(it) } + viewModel.swapRouteState.observe(swapMainSettingsRoute::setSwapRouteState) viewModel.showDetails.observe { swapMainSettingsDetails.setVisible(it) } viewModel.buttonState.observe(swapMainSettingsContinue::setState) diff --git a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/main/SwapMainSettingsViewModel.kt b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/main/SwapMainSettingsViewModel.kt index f795e73bb7..d8464d3033 100644 --- a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/main/SwapMainSettingsViewModel.kt +++ b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/main/SwapMainSettingsViewModel.kt @@ -52,6 +52,8 @@ import io.novafoundation.nova.feature_swap_impl.domain.model.GetAssetInOption import io.novafoundation.nova.feature_swap_impl.presentation.SwapRouter import io.novafoundation.nova.feature_swap_impl.presentation.common.fee.SwapFeeFormatter import io.novafoundation.nova.feature_swap_impl.presentation.common.fee.SwapFeeInspector +import io.novafoundation.nova.feature_swap_impl.presentation.common.route.SwapRouteFormatter +import io.novafoundation.nova.feature_swap_impl.presentation.common.route.SwapRouteState import io.novafoundation.nova.feature_swap_impl.presentation.common.state.SwapState import io.novafoundation.nova.feature_swap_impl.presentation.common.state.SwapStateStoreProvider import io.novafoundation.nova.feature_swap_impl.presentation.fieldValidation.EnoughAmountToSwapValidatorFactory @@ -131,6 +133,7 @@ class SwapMainSettingsViewModel( private val buyMixinFactory: BuyMixin.Factory, private val descriptionBottomSheetLauncher: DescriptionBottomSheetLauncher, private val swapRateFormatter: SwapRateFormatter, + private val swapRouteFormatter: SwapRouteFormatter, private val maxActionProviderFactory: MaxActionProviderFactory, private val swapStateStoreProvider: SwapStateStoreProvider, swapAmountInputMixinFactory: SwapAmountInputMixinFactory, @@ -159,11 +162,15 @@ class SwapMainSettingsViewModel( private val priceImpact = quotingState.map { quoteState -> when (quoteState) { - is QuotingState.NotAvailable, QuotingState.Loading, QuotingState.Default -> null - is QuotingState.Loaded -> quoteState.value.priceImpact + is QuotingState.Error, QuotingState.Loading, QuotingState.Default -> null + is QuotingState.Loaded -> quoteState.quote.priceImpact } } + val swapRouteState = quotingState + .map { quoteState -> quoteState.toSwapRouteState() } + .shareInBackground() + private val originChainFlow = swapSettings .mapNotNull { it.assetIn?.chainId } .distinctUntilChanged() @@ -202,7 +209,7 @@ class SwapMainSettingsViewModel( val rateDetails: Flow> = quotingState.map { when (it) { - is QuotingState.Loaded -> ExtendedLoadingState.Loaded(formatRate(it.value)) + is QuotingState.Loaded -> ExtendedLoadingState.Loaded(formatRate(it.quote)) else -> ExtendedLoadingState.Loading } } @@ -212,7 +219,7 @@ class SwapMainSettingsViewModel( when (it) { is QuotingState.Loaded -> true is QuotingState.Default, - is QuotingState.NotAvailable -> false + is QuotingState.Error -> false else -> null // Don't do anything if it's loading state } @@ -326,7 +333,7 @@ class SwapMainSettingsViewModel( if (quotingState !is QuotingState.Loaded) return@launch val swapState = SwapState( - quote = quotingState.value, + quote = quotingState.quote, fee = feeMixin.awaitFee(), slippage = swapSettings.first().slippage ) @@ -471,7 +478,7 @@ class SwapMainSettingsViewModel( .onEach { when (it) { is QuotingState.Loading -> setFeeLoading() - is QuotingState.NotAvailable -> setFeeStatus(FeeStatus.NoFee) + is QuotingState.Error -> setFeeStatus(FeeStatus.NoFee) else -> {} } } @@ -486,7 +493,7 @@ class SwapMainSettingsViewModel( } .onEach { quoteState -> loadFee { feePaymentCurrency -> - val swapArgs = quoteState.value.toExecuteArgs( + val swapArgs = quoteState.quote.toExecuteArgs( slippage = swapSettings.first().slippage, firstSegmentFees = feePaymentCurrency ) @@ -549,7 +556,7 @@ class SwapMainSettingsViewModel( quotingState is QuotingState.Loading -> DescriptiveButtonState.Loading - quotingState is QuotingState.NotAvailable || inputs.any { it.value.isEmpty() } -> { + quotingState is QuotingState.Error || inputs.any { it.value.isEmpty() } -> { DescriptiveButtonState.Disabled(resourceManager.getString(R.string.common_continue)) } @@ -599,7 +606,7 @@ class SwapMainSettingsViewModel( if (it is CancellationException) { QuotingState.Loading } else { - QuotingState.NotAvailable + QuotingState.Error(it) } } ) @@ -607,6 +614,15 @@ class SwapMainSettingsViewModel( handleNewQuote(quote, swapSettings) } + private suspend fun QuotingState.toSwapRouteState(): SwapRouteState { + return when(this) { + QuotingState.Default -> ExtendedLoadingState.Loaded(null) + is QuotingState.Error -> ExtendedLoadingState.Error(error) + is QuotingState.Loaded -> ExtendedLoadingState.Loaded(swapRouteFormatter.formatSwapRoute(quote)) + QuotingState.Loading -> ExtendedLoadingState.Loading + } + } + private fun handleNewQuote(quoteResult: Result, swapSettings: SwapSettings) { quoteResult.onSuccess { quote -> when (swapSettings.swapDirection!!) { diff --git a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/main/di/SwapMainSettingsModule.kt b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/main/di/SwapMainSettingsModule.kt index 8895e89732..b03b1c0095 100644 --- a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/main/di/SwapMainSettingsModule.kt +++ b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/main/di/SwapMainSettingsModule.kt @@ -22,6 +22,7 @@ import io.novafoundation.nova.feature_swap_api.presentation.state.SwapSettingsSt import io.novafoundation.nova.feature_swap_impl.domain.interactor.SwapInteractor import io.novafoundation.nova.feature_swap_impl.presentation.SwapRouter import io.novafoundation.nova.feature_swap_impl.presentation.common.PriceImpactFormatter +import io.novafoundation.nova.feature_swap_impl.presentation.common.route.SwapRouteFormatter import io.novafoundation.nova.feature_swap_impl.presentation.common.state.SwapStateStoreProvider import io.novafoundation.nova.feature_swap_impl.presentation.fieldValidation.EnoughAmountToSwapValidatorFactory import io.novafoundation.nova.feature_swap_impl.presentation.fieldValidation.LiquidityFieldValidatorFactory @@ -99,6 +100,7 @@ class SwapMainSettingsModule { swapRateFormatter: SwapRateFormatter, maxActionProviderFactory: MaxActionProviderFactory, swapStateStoreProvider: SwapStateStoreProvider, + swapRouteFormatter: SwapRouteFormatter ): ViewModel { return SwapMainSettingsViewModel( swapRouter = swapRouter, @@ -121,7 +123,8 @@ class SwapMainSettingsModule { selectedAccountUseCase = accountUseCase, buyMixinFactory = buyMixinFactory, swapStateStoreProvider = swapStateStoreProvider, - maxActionProviderFactory = maxActionProviderFactory + maxActionProviderFactory = maxActionProviderFactory, + swapRouteFormatter = swapRouteFormatter ) } diff --git a/feature-swap-impl/src/main/res/layout/fragment_main_swap_settings.xml b/feature-swap-impl/src/main/res/layout/fragment_main_swap_settings.xml index 4fa5c29362..4d38299cff 100644 --- a/feature-swap-impl/src/main/res/layout/fragment_main_swap_settings.xml +++ b/feature-swap-impl/src/main/res/layout/fragment_main_swap_settings.xml @@ -165,6 +165,12 @@ app:title="@string/swap_rate_title" app:titleIcon="@drawable/ic_info" /> + + + + Date: Wed, 6 Nov 2024 15:07:42 +0700 Subject: [PATCH 42/83] Fix swap filtering issues --- .../networks/AssetNetworksInteractor.kt | 23 ++++--------------- .../flow/network/NetworkFlowViewModel.kt | 6 ++++- .../swap/network/NetworkSwapFlowViewModel.kt | 13 +++++++++-- .../swap/network/di/NetworkSwapFlowModule.kt | 1 + 4 files changed, 21 insertions(+), 22 deletions(-) diff --git a/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/domain/networks/AssetNetworksInteractor.kt b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/domain/networks/AssetNetworksInteractor.kt index db36fa1921..d4fefab7f9 100644 --- a/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/domain/networks/AssetNetworksInteractor.kt +++ b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/domain/networks/AssetNetworksInteractor.kt @@ -2,8 +2,6 @@ package io.novafoundation.nova.feature_assets.domain.networks import io.novafoundation.nova.common.utils.TokenSymbol import io.novafoundation.nova.common.utils.filterList -import io.novafoundation.nova.common.utils.filterSet -import io.novafoundation.nova.common.utils.flowOfAll import io.novafoundation.nova.feature_assets.domain.assets.search.AssetSearchFilter import io.novafoundation.nova.feature_assets.domain.assets.search.AssetSearchUseCase import io.novafoundation.nova.feature_assets.domain.common.AssetWithNetwork @@ -20,7 +18,6 @@ import io.novafoundation.nova.runtime.ext.normalize import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry import io.novafoundation.nova.runtime.multiNetwork.chain.model.FullChainAssetId import io.novafoundation.nova.runtime.multiNetwork.enabledChainById -import io.novafoundation.nova.runtime.multiNetwork.enabledChains import io.novasama.substrate_sdk_android.hash.isPositive import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.Flow @@ -59,11 +56,12 @@ class AssetNetworksInteractor( } fun swapAssetsFlow( + forAssetId: FullChainAssetId?, tokenSymbol: TokenSymbol, externalBalancesFlow: Flow>, coroutineScope: CoroutineScope ): Flow> { - val filterFlow = getSwapAssetsFilter(tokenSymbol, coroutineScope) + val filterFlow = getSwapAssetsFilter(forAssetId, coroutineScope) return searchAssetsByTokenSymbolInternalFlow(tokenSymbol, externalBalancesFlow, filterFlow = filterFlow) } @@ -95,8 +93,8 @@ class AssetNetworksInteractor( } } - private fun getSwapAssetsFilter(tokenSymbol: TokenSymbol, coroutineScope: CoroutineScope): Flow { - return getAvailableSwapAssets(tokenSymbol, coroutineScope) + private fun getSwapAssetsFilter(sourceAsset: FullChainAssetId?, coroutineScope: CoroutineScope): Flow { + return assetSearchUseCase.getAvailableSwapAssets(sourceAsset, coroutineScope) .map { availableAssetsForSwap -> val assetFilter: suspend (Asset) -> Boolean = { asset: Asset -> asset.token.configuration.fullId in availableAssetsForSwap @@ -105,19 +103,6 @@ class AssetNetworksInteractor( assetFilter } } - - private fun getAvailableSwapAssets(tokenSymbol: TokenSymbol, coroutineScope: CoroutineScope): Flow> { - return flowOfAll { - val assetsSupportedTokenSymbol = chainRegistry.enabledChains() - .flatMap { chain -> - chain.assets.filter { it.symbol.normalize() == tokenSymbol } - .map { it.fullId } - } - - swapService.assetsAvailableForSwap(coroutineScope) - .filterSet { fullAssetId -> fullAssetId in assetsSupportedTokenSymbol } - } - } } private fun AssetNetworksInteractor.searchAssetsByTokenSymbolInternalFlow( diff --git a/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/flow/network/NetworkFlowViewModel.kt b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/flow/network/NetworkFlowViewModel.kt index 0a09caeec7..043efea0f4 100644 --- a/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/flow/network/NetworkFlowViewModel.kt +++ b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/flow/network/NetworkFlowViewModel.kt @@ -4,6 +4,7 @@ import io.novafoundation.nova.common.base.BaseViewModel import io.novafoundation.nova.common.resources.ResourceManager import io.novafoundation.nova.common.utils.TokenSymbol import io.novafoundation.nova.common.utils.flowOf +import io.novafoundation.nova.common.utils.flowOfAll import io.novafoundation.nova.feature_account_api.domain.interfaces.SelectedAccountUseCase import io.novafoundation.nova.feature_assets.domain.assets.ExternalBalancesInteractor import io.novafoundation.nova.feature_assets.domain.common.AssetWithNetwork @@ -16,6 +17,7 @@ import io.novafoundation.nova.feature_wallet_api.presentation.model.mapAmountToA import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry import io.novafoundation.nova.runtime.multiNetwork.asset import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.map import kotlinx.coroutines.launch @@ -35,8 +37,10 @@ abstract class NetworkFlowViewModel( protected val externalBalancesFlow = externalBalancesInteractor.observeExternalBalances() val titleFlow: Flow = flowOf { getTitle(networkFlowPayload.asTokenSymbol()) } - val networks: Flow> = assetsFlow(networkFlowPayload.asTokenSymbol()) + + val networks: Flow> = flowOfAll { assetsFlow(networkFlowPayload.asTokenSymbol()) } .map { mapAssets(it) } + .shareInBackground(SharingStarted.Lazily) abstract fun getAssetBalance(asset: AssetWithNetwork): PricedAmount diff --git a/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/swap/network/NetworkSwapFlowViewModel.kt b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/swap/network/NetworkSwapFlowViewModel.kt index 208687d1d3..46d5010d09 100644 --- a/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/swap/network/NetworkSwapFlowViewModel.kt +++ b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/swap/network/NetworkSwapFlowViewModel.kt @@ -6,15 +6,18 @@ import io.novafoundation.nova.common.utils.TokenSymbol import io.novafoundation.nova.feature_account_api.domain.interfaces.SelectedAccountUseCase import io.novafoundation.nova.feature_assets.R import io.novafoundation.nova.feature_assets.domain.assets.ExternalBalancesInteractor -import io.novafoundation.nova.feature_assets.domain.common.PricedAmount import io.novafoundation.nova.feature_assets.domain.common.AssetWithNetwork +import io.novafoundation.nova.feature_assets.domain.common.PricedAmount import io.novafoundation.nova.feature_assets.domain.networks.AssetNetworksInteractor import io.novafoundation.nova.feature_assets.presentation.AssetsRouter import io.novafoundation.nova.feature_assets.presentation.balance.common.ControllableAssetCheckMixin import io.novafoundation.nova.feature_assets.presentation.flow.network.NetworkFlowPayload import io.novafoundation.nova.feature_assets.presentation.flow.network.NetworkFlowViewModel import io.novafoundation.nova.feature_assets.presentation.flow.network.model.NetworkFlowRvItem +import io.novafoundation.nova.feature_assets.presentation.swap.asset.SwapFlowPayload +import io.novafoundation.nova.feature_assets.presentation.swap.asset.constraintDirectionsAsset import io.novafoundation.nova.feature_assets.presentation.swap.executor.SwapFlowExecutor +import io.novafoundation.nova.feature_wallet_api.presentation.model.fullChainAssetId import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry import io.novafoundation.nova.runtime.multiNetwork.asset import kotlinx.coroutines.flow.Flow @@ -29,6 +32,7 @@ class NetworkSwapFlowViewModel( resourceManager: ResourceManager, networkFlowPayload: NetworkFlowPayload, chainRegistry: ChainRegistry, + private val swapFlowPayload: SwapFlowPayload, private val swapFlowExecutor: SwapFlowExecutor ) : NetworkFlowViewModel( interactor, @@ -46,7 +50,12 @@ class NetworkSwapFlowViewModel( } override fun assetsFlow(tokenSymbol: TokenSymbol): Flow> { - return interactor.swapAssetsFlow(tokenSymbol, externalBalancesFlow, viewModelScope) + return interactor.swapAssetsFlow( + forAssetId = swapFlowPayload.constraintDirectionsAsset?.fullChainAssetId, + tokenSymbol = tokenSymbol, + externalBalancesFlow = externalBalancesFlow, + coroutineScope = viewModelScope + ) } override fun networkClicked(network: NetworkFlowRvItem) { diff --git a/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/swap/network/di/NetworkSwapFlowModule.kt b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/swap/network/di/NetworkSwapFlowModule.kt index 23d8dc8c91..d20547d498 100644 --- a/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/swap/network/di/NetworkSwapFlowModule.kt +++ b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/swap/network/di/NetworkSwapFlowModule.kt @@ -49,6 +49,7 @@ class NetworkSwapFlowModule { accountUseCase = accountUseCase, resourceManager = resourceManager, networkFlowPayload = payload.networkFlowPayload, + swapFlowPayload = payload.swapFlowPayload, chainRegistry = chainRegistry, swapFlowExecutor = executorFactory.create(payload.swapFlowPayload) ) From 63ed9eae4f2b2c8407bb6f8687b356e121edb844 Mon Sep 17 00:00:00 2001 From: valentun Date: Thu, 7 Nov 2024 15:11:10 +0700 Subject: [PATCH 43/83] Swap route details --- .../app/root/navigation/swap/SwapNavigator.kt | 2 + .../res/navigation/start_swap_nav_graph.xml | 17 ++- .../common/presentation/AssetIconProvider.kt | 14 +-- .../nova/common/view/GenericTableCellView.kt | 2 + common/src/main/res/values/colors.xml | 3 + common/src/main/res/values/strings.xml | 5 + .../model/AtomicOperationDisplayData.kt | 19 ++++ .../domain/model/AtomicSwapOperation.kt | 2 + .../domain/model/SwapQuoteArgs.kt | 12 ++ .../AssetConversionExchange.kt | 12 ++ .../CrossChainTransferAssetExchange.kt | 16 ++- .../hydraDx/HydraDxAssetExchange.kt | 12 +- .../di/SwapFeatureComponent.kt | 3 + .../feature_swap_impl/di/SwapFeatureModule.kt | 4 +- .../domain/interactor/SwapInteractor.kt | 21 +++- .../presentation/SwapRouter.kt | 2 + .../common/fee/SwapFeeFormatter.kt | 4 +- .../EnoughAmountToSwapFieldValidator.kt | 2 +- .../LiquidityFieldValidator.kt | 2 +- .../fieldValidation/SlippageFieldValidator.kt | 2 +- .../SwapReceiveAmountAboveEDFieldValidator.kt | 2 +- .../maxAction/MaxActionProviderFactory.kt | 2 +- ...xistentialDepositAwareMaxActionProvider.kt | 2 +- .../common/route/SwapRouteFormatter.kt | 7 +- .../state/RealSwapSettingsState.kt | 2 +- .../state/SwapSettingsStateProvider.kt | 2 +- .../common/state/SwapStateStore.kt | 22 +++- .../common/state/SwapStateStoreProvider.kt | 6 + .../{ => common}/views/SwapAmountInputView.kt | 2 +- .../confirmation/SwapConfirmationViewModel.kt | 2 +- .../confirmation/di/SwapConfirmationModule.kt | 2 +- .../main/SwapMainSettingsFragment.kt | 6 + .../main/SwapMainSettingsViewModel.kt | 68 ++++++++---- .../main/di/SwapMainSettingsModule.kt | 8 +- .../main/input/SwapAmountInputUi.kt | 2 +- .../options/SwapOptionsViewModel.kt | 2 +- .../options/di/SwapOptionsModule.kt | 2 +- .../presentation/route/SwapRouteFragment.kt | 80 ++++++++++++++ .../presentation/route/SwapRouteViewModel.kt | 95 ++++++++++++++++ .../route/di/SwapRouteComponent.kt | 26 +++++ .../presentation/route/di/SwapRouteModule.kt | 50 +++++++++ .../route/list/SwapRouteAdapter.kt | 90 +++++++++++++++ .../route/list/SwapRouteHeaderAdapter.kt | 37 +++++++ .../route/list/TimelineItemDecoration.kt | 103 ++++++++++++++++++ .../route/model/SwapRouteItemModel.kt | 24 ++++ .../route/view/TokenAmountView.kt | 56 ++++++++++ .../layout/fragment_main_swap_settings.xml | 4 +- .../src/main/res/layout/fragment_route.xml | 21 ++++ .../src/main/res/layout/item_route_header.xml | 33 ++++++ .../src/main/res/layout/item_route_swap.xml | 77 +++++++++++++ .../main/res/layout/item_route_transfer.xml | 81 ++++++++++++++ .../src/main/res/layout/view_token_amount.xml | 26 +++++ .../domain/model/FiatAmount.kt | 10 +- .../domain/model/Operation.kt | 10 ++ .../presentation/formatters/FiatAmount.kt | 6 + 55 files changed, 1059 insertions(+), 65 deletions(-) create mode 100644 feature-swap-api/src/main/java/io/novafoundation/nova/feature_swap_api/domain/model/AtomicOperationDisplayData.kt rename feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/{ => common}/fieldValidation/EnoughAmountToSwapFieldValidator.kt (94%) rename feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/{ => common}/fieldValidation/LiquidityFieldValidator.kt (93%) rename feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/{ => common}/fieldValidation/SlippageFieldValidator.kt (95%) rename feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/{ => common}/fieldValidation/SwapReceiveAmountAboveEDFieldValidator.kt (97%) rename feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/{ => common}/mixin/maxAction/MaxActionProviderFactory.kt (95%) rename feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/{ => common}/mixin/maxAction/SwapExistentialDepositAwareMaxActionProvider.kt (97%) rename feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/{ => common}/state/RealSwapSettingsState.kt (97%) rename feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/{ => common}/state/SwapSettingsStateProvider.kt (93%) rename feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/{ => common}/views/SwapAmountInputView.kt (98%) create mode 100644 feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/route/SwapRouteFragment.kt create mode 100644 feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/route/SwapRouteViewModel.kt create mode 100644 feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/route/di/SwapRouteComponent.kt create mode 100644 feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/route/di/SwapRouteModule.kt create mode 100644 feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/route/list/SwapRouteAdapter.kt create mode 100644 feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/route/list/SwapRouteHeaderAdapter.kt create mode 100644 feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/route/list/TimelineItemDecoration.kt create mode 100644 feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/route/model/SwapRouteItemModel.kt create mode 100644 feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/route/view/TokenAmountView.kt create mode 100644 feature-swap-impl/src/main/res/layout/fragment_route.xml create mode 100644 feature-swap-impl/src/main/res/layout/item_route_header.xml create mode 100644 feature-swap-impl/src/main/res/layout/item_route_swap.xml create mode 100644 feature-swap-impl/src/main/res/layout/item_route_transfer.xml create mode 100644 feature-swap-impl/src/main/res/layout/view_token_amount.xml create mode 100644 feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/presentation/formatters/FiatAmount.kt diff --git a/app/src/main/java/io/novafoundation/nova/app/root/navigation/swap/SwapNavigator.kt b/app/src/main/java/io/novafoundation/nova/app/root/navigation/swap/SwapNavigator.kt index 28791595b9..1bf6b5eb2c 100644 --- a/app/src/main/java/io/novafoundation/nova/app/root/navigation/swap/SwapNavigator.kt +++ b/app/src/main/java/io/novafoundation/nova/app/root/navigation/swap/SwapNavigator.kt @@ -18,6 +18,8 @@ class SwapNavigator( override fun openSwapConfirmation() = performNavigation(R.id.action_swapMainSettingsFragment_to_swapConfirmationFragment) + override fun openSwapRoute() = performNavigation(R.id.action_swapSettingsFragment_to_swapRouteFragment) + override fun openSwapOptions() { navigationHolder.navController?.navigate(R.id.action_swapMainSettingsFragment_to_swapOptionsFragment) } diff --git a/app/src/main/res/navigation/start_swap_nav_graph.xml b/app/src/main/res/navigation/start_swap_nav_graph.xml index 3057353d47..9fe9d585ec 100644 --- a/app/src/main/res/navigation/start_swap_nav_graph.xml +++ b/app/src/main/res/navigation/start_swap_nav_graph.xml @@ -28,11 +28,19 @@ + app:popExitAnim="@anim/fragment_close_exit" /> + + + + \ No newline at end of file diff --git a/common/src/main/java/io/novafoundation/nova/common/presentation/AssetIconProvider.kt b/common/src/main/java/io/novafoundation/nova/common/presentation/AssetIconProvider.kt index a174975a4f..2b9305644f 100644 --- a/common/src/main/java/io/novafoundation/nova/common/presentation/AssetIconProvider.kt +++ b/common/src/main/java/io/novafoundation/nova/common/presentation/AssetIconProvider.kt @@ -10,9 +10,9 @@ interface AssetIconProvider { companion object; - fun getAssetIconOrFallback(iconName: String): Icon + fun getAssetIcon(iconName: String): Icon - fun getAssetIconOrFallback(iconName: String, iconMode: AssetIconMode): Icon + fun getAssetIcon(iconName: String, iconMode: AssetIconMode): Icon } class RealAssetIconProvider( @@ -21,11 +21,11 @@ class RealAssetIconProvider( private val whiteBaseUrl: String ) : AssetIconProvider { - override fun getAssetIconOrFallback(iconName: String): Icon { - return getAssetIconOrFallback(iconName, assetsIconModeRepository.getIconMode()) + override fun getAssetIcon(iconName: String): Icon { + return getAssetIcon(iconName, assetsIconModeRepository.getIconMode()) } - override fun getAssetIconOrFallback(iconName: String, iconMode: AssetIconMode): Icon { + override fun getAssetIcon(iconName: String, iconMode: AssetIconMode): Icon { val iconUrl = when (assetsIconModeRepository.getIconMode()) { AssetIconMode.COLORED -> "$coloredBaseUrl/$iconName" AssetIconMode.WHITE -> "$whiteBaseUrl/$iconName" @@ -42,7 +42,7 @@ fun AssetIconProvider.getAssetIconOrFallback( iconName: String?, fallback: Icon = AssetIconProvider.fallbackIcon ): Icon { - return iconName?.let { getAssetIconOrFallback(it) } ?: fallback + return iconName?.let { getAssetIcon(it) } ?: fallback } fun AssetIconProvider.getAssetIconOrFallback( @@ -50,5 +50,5 @@ fun AssetIconProvider.getAssetIconOrFallback( iconMode: AssetIconMode, fallback: Icon = AssetIconProvider.fallbackIcon ): Icon { - return iconName?.let { getAssetIconOrFallback(it, iconMode) } ?: fallback + return iconName?.let { getAssetIcon(it, iconMode) } ?: fallback } diff --git a/common/src/main/java/io/novafoundation/nova/common/view/GenericTableCellView.kt b/common/src/main/java/io/novafoundation/nova/common/view/GenericTableCellView.kt index 431e675203..ddb17adae9 100644 --- a/common/src/main/java/io/novafoundation/nova/common/view/GenericTableCellView.kt +++ b/common/src/main/java/io/novafoundation/nova/common/view/GenericTableCellView.kt @@ -36,6 +36,8 @@ open class GenericTableCellView @JvmOverloads constructor( View.inflate(context, R.layout.view_generic_table_cell, this) + setBackgroundResource(R.drawable.bg_primary_list_item) + attrs?.let(::applyAttributes) } diff --git a/common/src/main/res/values/colors.xml b/common/src/main/res/values/colors.xml index 409cd3792e..450bf10f81 100644 --- a/common/src/main/res/values/colors.xml +++ b/common/src/main/res/values/colors.xml @@ -4,6 +4,9 @@ #eeeeee #FFFFFF #101014 + + #373A49 + #3DFFFFFF #05081C diff --git a/common/src/main/res/values/strings.xml b/common/src/main/res/values/strings.xml index 86faa3bc6e..2e0a85970a 100644 --- a/common/src/main/res/values/strings.xml +++ b/common/src/main/res/values/strings.xml @@ -1,5 +1,10 @@ + Fee: %s + + Transfer + Swap + The way that your token will take through different networks to get the desired token. Route Send only %1$s token and tokens in %2$s network to this address, or you might lose your funds diff --git a/feature-swap-api/src/main/java/io/novafoundation/nova/feature_swap_api/domain/model/AtomicOperationDisplayData.kt b/feature-swap-api/src/main/java/io/novafoundation/nova/feature_swap_api/domain/model/AtomicOperationDisplayData.kt new file mode 100644 index 0000000000..2447935a00 --- /dev/null +++ b/feature-swap-api/src/main/java/io/novafoundation/nova/feature_swap_api/domain/model/AtomicOperationDisplayData.kt @@ -0,0 +1,19 @@ +package io.novafoundation.nova.feature_swap_api.domain.model + +import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.types.Balance +import io.novafoundation.nova.feature_wallet_api.domain.model.ChainAssetIdWithAmount +import io.novafoundation.nova.runtime.multiNetwork.chain.model.FullChainAssetId + +sealed class AtomicOperationDisplayData { + + class Transfer( + val from: FullChainAssetId, + val to: FullChainAssetId, + val amount: Balance + ): AtomicOperationDisplayData() + + class Swap( + val from: ChainAssetIdWithAmount, + val to: ChainAssetIdWithAmount, + ): AtomicOperationDisplayData() +} diff --git a/feature-swap-api/src/main/java/io/novafoundation/nova/feature_swap_api/domain/model/AtomicSwapOperation.kt b/feature-swap-api/src/main/java/io/novafoundation/nova/feature_swap_api/domain/model/AtomicSwapOperation.kt index 6ff904cf07..41871a377d 100644 --- a/feature-swap-api/src/main/java/io/novafoundation/nova/feature_swap_api/domain/model/AtomicSwapOperation.kt +++ b/feature-swap-api/src/main/java/io/novafoundation/nova/feature_swap_api/domain/model/AtomicSwapOperation.kt @@ -10,6 +10,8 @@ interface AtomicSwapOperation { val estimatedSwapLimit: SwapLimit + suspend fun constructDisplayData(): AtomicOperationDisplayData + suspend fun estimateFee(): AtomicSwapOperationFee suspend fun requiredAmountInToGetAmountOut(extraOutAmount: Balance): Balance diff --git a/feature-swap-api/src/main/java/io/novafoundation/nova/feature_swap_api/domain/model/SwapQuoteArgs.kt b/feature-swap-api/src/main/java/io/novafoundation/nova/feature_swap_api/domain/model/SwapQuoteArgs.kt index ae90f1488a..a9d2d1761c 100644 --- a/feature-swap-api/src/main/java/io/novafoundation/nova/feature_swap_api/domain/model/SwapQuoteArgs.kt +++ b/feature-swap-api/src/main/java/io/novafoundation/nova/feature_swap_api/domain/model/SwapQuoteArgs.kt @@ -48,6 +48,18 @@ sealed class SwapLimit { ) : SwapLimit() } +val SwapLimit.estimatedAmountIn: Balance + get() = when(this) { + is SwapLimit.SpecifiedIn -> amountIn + is SwapLimit.SpecifiedOut -> amountInQuote + } + +val SwapLimit.estimatedAmountOut: Balance + get() = when(this) { + is SwapLimit.SpecifiedIn -> amountOutQuote + is SwapLimit.SpecifiedOut -> amountOut + } + /** * Adjusts SwapLimit to the [newAmountIn] based on the quoted swap rate * This is only suitable for small changes amount in, as it implicitly assumes the swap rate stays the same diff --git a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/assetConversion/AssetConversionExchange.kt b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/assetConversion/AssetConversionExchange.kt index c855237a11..af469564e1 100644 --- a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/assetConversion/AssetConversionExchange.kt +++ b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/assetConversion/AssetConversionExchange.kt @@ -11,6 +11,7 @@ import io.novafoundation.nova.feature_account_api.data.extrinsic.ExtrinsicServic import io.novafoundation.nova.feature_account_api.data.extrinsic.createDefault import io.novafoundation.nova.feature_account_api.data.extrinsic.execution.requireOk import io.novafoundation.nova.feature_account_api.domain.model.MetaAccount +import io.novafoundation.nova.feature_swap_api.domain.model.AtomicOperationDisplayData import io.novafoundation.nova.feature_swap_api.domain.model.AtomicSwapOperation import io.novafoundation.nova.feature_swap_api.domain.model.AtomicSwapOperationArgs import io.novafoundation.nova.feature_swap_api.domain.model.AtomicSwapOperationFee @@ -22,6 +23,8 @@ import io.novafoundation.nova.feature_swap_api.domain.model.SwapExecutionCorrect import io.novafoundation.nova.feature_swap_api.domain.model.SwapGraphEdge import io.novafoundation.nova.feature_swap_api.domain.model.SwapLimit import io.novafoundation.nova.feature_swap_api.domain.model.UsdConverter +import io.novafoundation.nova.feature_swap_api.domain.model.estimatedAmountIn +import io.novafoundation.nova.feature_swap_api.domain.model.estimatedAmountOut import io.novafoundation.nova.feature_swap_core.data.assetExchange.conversion.types.hydra.sources.Weights import io.novafoundation.nova.feature_swap_core_api.data.primitive.errors.SwapQuoteException import io.novafoundation.nova.feature_swap_core_api.data.primitive.model.SwapDirection @@ -30,9 +33,11 @@ import io.novafoundation.nova.feature_swap_impl.data.assetExchange.FeePaymentPro import io.novafoundation.nova.feature_swap_impl.data.assetExchange.ParentQuoterArgs import io.novafoundation.nova.feature_swap_impl.domain.swap.BaseSwapGraphEdge import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.types.Balance +import io.novafoundation.nova.feature_wallet_api.domain.model.withAmount import io.novafoundation.nova.runtime.call.MultiChainRuntimeCallsApi import io.novafoundation.nova.runtime.call.RuntimeCallsApi import io.novafoundation.nova.runtime.ext.emptyAccountId +import io.novafoundation.nova.runtime.ext.fullId import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain import io.novafoundation.nova.runtime.multiNetwork.chain.model.ChainId import io.novafoundation.nova.runtime.multiNetwork.multiLocation.MultiLocation @@ -235,6 +240,13 @@ private class AssetConversionExchange( override val estimatedSwapLimit: SwapLimit = transactionArgs.estimatedSwapLimit + override suspend fun constructDisplayData(): AtomicOperationDisplayData { + return AtomicOperationDisplayData.Swap( + from = fromAsset.fullId.withAmount(estimatedSwapLimit.estimatedAmountIn), + to = toAsset.fullId.withAmount(estimatedSwapLimit.estimatedAmountOut), + ) + } + override suspend fun estimateFee(): AtomicSwapOperationFee { val submissionFee = extrinsicService.estimateFee( chain = chain, diff --git a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/crossChain/CrossChainTransferAssetExchange.kt b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/crossChain/CrossChainTransferAssetExchange.kt index a2bda4c56f..e93a9d40bb 100644 --- a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/crossChain/CrossChainTransferAssetExchange.kt +++ b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/crossChain/CrossChainTransferAssetExchange.kt @@ -5,6 +5,7 @@ import io.novafoundation.nova.common.utils.graph.Edge import io.novafoundation.nova.feature_account_api.domain.interfaces.AccountRepository import io.novafoundation.nova.feature_account_api.domain.model.MetaAccount import io.novafoundation.nova.feature_account_api.domain.model.requireAddressIn +import io.novafoundation.nova.feature_swap_api.domain.model.AtomicOperationDisplayData import io.novafoundation.nova.feature_swap_api.domain.model.AtomicSwapOperation import io.novafoundation.nova.feature_swap_api.domain.model.AtomicSwapOperationArgs import io.novafoundation.nova.feature_swap_api.domain.model.AtomicSwapOperationFee @@ -17,6 +18,7 @@ import io.novafoundation.nova.feature_swap_api.domain.model.SwapExecutionCorrect import io.novafoundation.nova.feature_swap_api.domain.model.SwapGraphEdge import io.novafoundation.nova.feature_swap_api.domain.model.SwapLimit import io.novafoundation.nova.feature_swap_api.domain.model.UsdConverter +import io.novafoundation.nova.feature_swap_api.domain.model.estimatedAmountIn import io.novafoundation.nova.feature_swap_core.data.assetExchange.conversion.types.hydra.sources.Weights import io.novafoundation.nova.feature_swap_core_api.data.primitive.model.SwapDirection import io.novafoundation.nova.feature_swap_impl.data.assetExchange.AssetExchange @@ -110,7 +112,7 @@ class CrossChainTransferAssetExchange( } override suspend fun appendToOperationPrototype(currentTransaction: AtomicSwapOperationPrototype): AtomicSwapOperationPrototype? { - return null + return null } override suspend fun debugLabel(): String { @@ -136,7 +138,7 @@ class CrossChainTransferAssetExchange( inner class CrossChainTransferOperationPrototype( override val fromChain: ChainId, private val toChain: ChainId, - ): AtomicSwapOperationPrototype { + ) : AtomicSwapOperationPrototype { override suspend fun roughlyEstimateNativeFee(usdConverter: UsdConverter): BigDecimal { var totalAmount = BigDecimal.ZERO @@ -157,7 +159,7 @@ class CrossChainTransferAssetExchange( } private fun isChainWithExpensiveCrossChain(chainId: ChainId): Boolean { - return (chainId == Chain.Geneses.POLKADOT) or (chainId == Chain.Geneses.POLKADOT_ASSET_HUB) + return (chainId == Chain.Geneses.POLKADOT) or (chainId == Chain.Geneses.POLKADOT_ASSET_HUB) } } @@ -168,6 +170,14 @@ class CrossChainTransferAssetExchange( override val estimatedSwapLimit: SwapLimit = transactionArgs.estimatedSwapLimit + override suspend fun constructDisplayData(): AtomicOperationDisplayData { + return AtomicOperationDisplayData.Transfer( + from = edge.from, + to = edge.to, + amount = estimatedSwapLimit.estimatedAmountIn + ) + } + override suspend fun estimateFee(): AtomicSwapOperationFee { val transfer = createTransfer(amount = estimatedSwapLimit.crossChainTransferAmount) diff --git a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/hydraDx/HydraDxAssetExchange.kt b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/hydraDx/HydraDxAssetExchange.kt index 214981eaa6..4e507c2cb5 100644 --- a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/hydraDx/HydraDxAssetExchange.kt +++ b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/hydraDx/HydraDxAssetExchange.kt @@ -25,6 +25,7 @@ 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.domain.model.MetaAccount import io.novafoundation.nova.feature_account_api.domain.model.requireAccountIdIn +import io.novafoundation.nova.feature_swap_api.domain.model.AtomicOperationDisplayData import io.novafoundation.nova.feature_swap_api.domain.model.AtomicSwapOperation import io.novafoundation.nova.feature_swap_api.domain.model.AtomicSwapOperationArgs import io.novafoundation.nova.feature_swap_api.domain.model.AtomicSwapOperationFee @@ -36,6 +37,8 @@ import io.novafoundation.nova.feature_swap_api.domain.model.SwapExecutionCorrect import io.novafoundation.nova.feature_swap_api.domain.model.SwapGraphEdge import io.novafoundation.nova.feature_swap_api.domain.model.SwapLimit import io.novafoundation.nova.feature_swap_api.domain.model.UsdConverter +import io.novafoundation.nova.feature_swap_api.domain.model.estimatedAmountIn +import io.novafoundation.nova.feature_swap_api.domain.model.estimatedAmountOut import io.novafoundation.nova.feature_swap_core.data.assetExchange.conversion.types.hydra.acceptedCurrencies import io.novafoundation.nova.feature_swap_core.data.assetExchange.conversion.types.hydra.accountCurrencyMap import io.novafoundation.nova.feature_swap_core.data.assetExchange.conversion.types.hydra.multiTransactionPayment @@ -53,6 +56,7 @@ import io.novafoundation.nova.feature_swap_impl.data.assetExchange.hydraDx.refer import io.novafoundation.nova.feature_swap_impl.data.assetExchange.hydraDx.referrals.linkedAccounts import io.novafoundation.nova.feature_swap_impl.data.assetExchange.hydraDx.referrals.referralsOrNull import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.types.Balance +import io.novafoundation.nova.feature_wallet_api.domain.model.withAmount import io.novafoundation.nova.runtime.ethereum.StorageSharedRequestsBuilder import io.novafoundation.nova.runtime.ethereum.StorageSharedRequestsBuilderFactory import io.novafoundation.nova.runtime.ext.utilityAsset @@ -270,7 +274,6 @@ private class HydraDxAssetExchange( ) : AtomicSwapOperation { override val estimatedSwapLimit: SwapLimit = aggregatedSwapLimit() - constructor(sourceEdge: HydraDxSourceEdge, args: AtomicSwapOperationArgs) : this(listOf(HydraDxSwapTransactionSegment(sourceEdge, args.estimatedSwapLimit)), args.feePaymentCurrency) @@ -281,6 +284,13 @@ private class HydraDxAssetExchange( return HydraDxOperation(segments + nextSegment, feePaymentCurrency) } + override suspend fun constructDisplayData(): AtomicOperationDisplayData { + return AtomicOperationDisplayData.Swap( + from = segments.first().edge.from.withAmount(estimatedSwapLimit.estimatedAmountIn), + to = segments.last().edge.to.withAmount(estimatedSwapLimit.estimatedAmountOut), + ) + } + override suspend fun estimateFee(): AtomicSwapOperationFee { val submissionFee = swapHost.extrinsicService().estimateFee( chain = chain, diff --git a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/di/SwapFeatureComponent.kt b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/di/SwapFeatureComponent.kt index 2bb8733c5f..46d3c49c47 100644 --- a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/di/SwapFeatureComponent.kt +++ b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/di/SwapFeatureComponent.kt @@ -13,6 +13,7 @@ import io.novafoundation.nova.feature_swap_impl.presentation.SwapRouter import io.novafoundation.nova.feature_swap_impl.presentation.confirmation.di.SwapConfirmationComponent import io.novafoundation.nova.feature_swap_impl.presentation.main.di.SwapMainSettingsComponent import io.novafoundation.nova.feature_swap_impl.presentation.options.di.SwapOptionsComponent +import io.novafoundation.nova.feature_swap_impl.presentation.route.di.SwapRouteComponent import io.novafoundation.nova.feature_wallet_api.di.WalletFeatureApi import io.novafoundation.nova.runtime.di.RuntimeApi @@ -33,6 +34,8 @@ interface SwapFeatureComponent : SwapFeatureApi { fun swapOptions(): SwapOptionsComponent.Factory + fun swapRoute(): SwapRouteComponent.Factory + @Component.Factory interface Factory { diff --git a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/di/SwapFeatureModule.kt b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/di/SwapFeatureModule.kt index 614d03a379..fbf9cc1964 100644 --- a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/di/SwapFeatureModule.kt +++ b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/di/SwapFeatureModule.kt @@ -36,8 +36,8 @@ import io.novafoundation.nova.feature_swap_impl.presentation.common.route.RealSw import io.novafoundation.nova.feature_swap_impl.presentation.common.route.SwapRouteFormatter import io.novafoundation.nova.feature_swap_impl.presentation.common.state.RealSwapStateStoreProvider import io.novafoundation.nova.feature_swap_impl.presentation.common.state.SwapStateStoreProvider -import io.novafoundation.nova.feature_swap_impl.presentation.mixin.maxAction.MaxActionProviderFactory -import io.novafoundation.nova.feature_swap_impl.presentation.state.RealSwapSettingsStateProvider +import io.novafoundation.nova.feature_swap_impl.presentation.common.mixin.maxAction.MaxActionProviderFactory +import io.novafoundation.nova.feature_swap_impl.presentation.common.state.RealSwapSettingsStateProvider import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.AssetSourceRegistry import io.novafoundation.nova.feature_wallet_api.domain.interfaces.CrossChainTransfersUseCase import io.novafoundation.nova.feature_wallet_api.domain.interfaces.TokenRepository diff --git a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/domain/interactor/SwapInteractor.kt b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/domain/interactor/SwapInteractor.kt index 52ff9cd769..2ed49a100e 100644 --- a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/domain/interactor/SwapInteractor.kt +++ b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/domain/interactor/SwapInteractor.kt @@ -14,6 +14,7 @@ import io.novafoundation.nova.feature_swap_api.domain.model.SwapFeeArgs import io.novafoundation.nova.feature_swap_api.domain.model.SwapProgress import io.novafoundation.nova.feature_swap_api.domain.model.SwapQuote import io.novafoundation.nova.feature_swap_api.domain.model.SwapQuoteArgs +import io.novafoundation.nova.feature_swap_api.domain.model.allBasicFees import io.novafoundation.nova.feature_swap_api.domain.swap.SwapService import io.novafoundation.nova.feature_swap_impl.data.network.blockhain.updaters.SwapUpdateSystemFactory import io.novafoundation.nova.feature_swap_impl.data.repository.SwapTransactionHistoryRepository @@ -63,6 +64,25 @@ class SwapInteractor( private val swapTransactionHistoryRepository: SwapTransactionHistoryRepository ) { + suspend fun calculateSegmentFiatPrices(swapFee: SwapFee): List { + return withContext(Dispatchers.Default) { + val basicFeesBySegment = swapFee.segments.map { it.fee.allBasicFees() } + val chainAssets = basicFeesBySegment.flatMap { segmentFees -> segmentFees.map { it.asset } } + + val tokens = tokenRepository.getTokens(chainAssets) + val currency = tokens.values.first().currency + + basicFeesBySegment.map { segmentBasicFees -> + val totalSegmentFees = segmentBasicFees.sumOf { basicFee -> + val token = tokens[basicFee.asset.fullId] + token?.planksToFiat(basicFee.amount) ?: BigDecimal.ZERO + } + + FiatAmount(currency, totalSegmentFees) + } + } + } + suspend fun calculateTotalFiatPrice(swapFee: SwapFee): FiatAmount { return withContext(Dispatchers.Default) { val basicFees = swapFee.allBasicFees() @@ -79,7 +99,6 @@ class SwapInteractor( price = totalFiat ) } - } suspend fun sync(coroutineScope: CoroutineScope) { diff --git a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/SwapRouter.kt b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/SwapRouter.kt index 323caa4b7c..acedf416ce 100644 --- a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/SwapRouter.kt +++ b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/SwapRouter.kt @@ -7,6 +7,8 @@ interface SwapRouter : ReturnableRouter { fun openSwapConfirmation() + fun openSwapRoute() + fun selectAssetIn(selectedAsset: AssetPayload?) fun selectAssetOut(selectedAsset: AssetPayload?) diff --git a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/common/fee/SwapFeeFormatter.kt b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/common/fee/SwapFeeFormatter.kt index 86c03f011f..f778521765 100644 --- a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/common/fee/SwapFeeFormatter.kt +++ b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/common/fee/SwapFeeFormatter.kt @@ -1,8 +1,8 @@ package io.novafoundation.nova.feature_swap_impl.presentation.common.fee -import io.novafoundation.nova.feature_currency_api.presentation.formatters.formatAsCurrency import io.novafoundation.nova.feature_swap_api.domain.model.SwapFee import io.novafoundation.nova.feature_swap_impl.domain.interactor.SwapInteractor +import io.novafoundation.nova.feature_wallet_api.presentation.formatters.formatAsCurrency import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.formatter.FeeFormatter import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.model.FeeDisplay import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.model.FeeStatus @@ -17,7 +17,7 @@ class SwapFeeFormatter( context: FeeFormatter.Context ): FeeDisplay { val totalFiatFee = swapInteractor.calculateTotalFiatPrice(fee) - val formattedFiatFee = totalFiatFee.price.formatAsCurrency(totalFiatFee.currency) + val formattedFiatFee = totalFiatFee.formatAsCurrency() return FeeDisplay( title = formattedFiatFee, subtitle = null diff --git a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/fieldValidation/EnoughAmountToSwapFieldValidator.kt b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/common/fieldValidation/EnoughAmountToSwapFieldValidator.kt similarity index 94% rename from feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/fieldValidation/EnoughAmountToSwapFieldValidator.kt rename to feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/common/fieldValidation/EnoughAmountToSwapFieldValidator.kt index 9c9b3b6ab7..7a763ed832 100644 --- a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/fieldValidation/EnoughAmountToSwapFieldValidator.kt +++ b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/common/fieldValidation/EnoughAmountToSwapFieldValidator.kt @@ -1,4 +1,4 @@ -package io.novafoundation.nova.feature_swap_impl.presentation.fieldValidation +package io.novafoundation.nova.feature_swap_impl.presentation.common.fieldValidation import io.novafoundation.nova.common.resources.ResourceManager import io.novafoundation.nova.common.validation.FieldValidationResult diff --git a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/fieldValidation/LiquidityFieldValidator.kt b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/common/fieldValidation/LiquidityFieldValidator.kt similarity index 93% rename from feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/fieldValidation/LiquidityFieldValidator.kt rename to feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/common/fieldValidation/LiquidityFieldValidator.kt index 5df4b12209..fd0f6a01f5 100644 --- a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/fieldValidation/LiquidityFieldValidator.kt +++ b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/common/fieldValidation/LiquidityFieldValidator.kt @@ -1,4 +1,4 @@ -package io.novafoundation.nova.feature_swap_impl.presentation.fieldValidation +package io.novafoundation.nova.feature_swap_impl.presentation.common.fieldValidation import io.novafoundation.nova.common.resources.ResourceManager import io.novafoundation.nova.common.validation.FieldValidationResult diff --git a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/fieldValidation/SlippageFieldValidator.kt b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/common/fieldValidation/SlippageFieldValidator.kt similarity index 95% rename from feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/fieldValidation/SlippageFieldValidator.kt rename to feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/common/fieldValidation/SlippageFieldValidator.kt index 3f04f97287..e8901a8cfc 100644 --- a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/fieldValidation/SlippageFieldValidator.kt +++ b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/common/fieldValidation/SlippageFieldValidator.kt @@ -1,4 +1,4 @@ -package io.novafoundation.nova.feature_swap_impl.presentation.fieldValidation +package io.novafoundation.nova.feature_swap_impl.presentation.common.fieldValidation import io.novafoundation.nova.common.resources.ResourceManager import io.novafoundation.nova.common.utils.Fraction diff --git a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/fieldValidation/SwapReceiveAmountAboveEDFieldValidator.kt b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/common/fieldValidation/SwapReceiveAmountAboveEDFieldValidator.kt similarity index 97% rename from feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/fieldValidation/SwapReceiveAmountAboveEDFieldValidator.kt rename to feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/common/fieldValidation/SwapReceiveAmountAboveEDFieldValidator.kt index 6b74481868..f214ec6a51 100644 --- a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/fieldValidation/SwapReceiveAmountAboveEDFieldValidator.kt +++ b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/common/fieldValidation/SwapReceiveAmountAboveEDFieldValidator.kt @@ -1,4 +1,4 @@ -package io.novafoundation.nova.feature_swap_impl.presentation.fieldValidation +package io.novafoundation.nova.feature_swap_impl.presentation.common.fieldValidation import io.novafoundation.nova.common.resources.ResourceManager import io.novafoundation.nova.common.validation.FieldValidationResult diff --git a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/mixin/maxAction/MaxActionProviderFactory.kt b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/common/mixin/maxAction/MaxActionProviderFactory.kt similarity index 95% rename from feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/mixin/maxAction/MaxActionProviderFactory.kt rename to feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/common/mixin/maxAction/MaxActionProviderFactory.kt index c54bfa5462..fb0469bcc6 100644 --- a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/mixin/maxAction/MaxActionProviderFactory.kt +++ b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/common/mixin/maxAction/MaxActionProviderFactory.kt @@ -1,4 +1,4 @@ -package io.novafoundation.nova.feature_swap_impl.presentation.mixin.maxAction +package io.novafoundation.nova.feature_swap_impl.presentation.common.mixin.maxAction import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.AssetSourceRegistry import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.types.Balance diff --git a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/mixin/maxAction/SwapExistentialDepositAwareMaxActionProvider.kt b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/common/mixin/maxAction/SwapExistentialDepositAwareMaxActionProvider.kt similarity index 97% rename from feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/mixin/maxAction/SwapExistentialDepositAwareMaxActionProvider.kt rename to feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/common/mixin/maxAction/SwapExistentialDepositAwareMaxActionProvider.kt index 1b6d65a19e..935c55d9f5 100644 --- a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/mixin/maxAction/SwapExistentialDepositAwareMaxActionProvider.kt +++ b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/common/mixin/maxAction/SwapExistentialDepositAwareMaxActionProvider.kt @@ -1,4 +1,4 @@ -package io.novafoundation.nova.feature_swap_impl.presentation.mixin.maxAction +package io.novafoundation.nova.feature_swap_impl.presentation.common.mixin.maxAction import io.novafoundation.nova.common.utils.orFalse import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.AssetSourceRegistry diff --git a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/common/route/SwapRouteFormatter.kt b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/common/route/SwapRouteFormatter.kt index f6eb7e8b1e..618aeafd3a 100644 --- a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/common/route/SwapRouteFormatter.kt +++ b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/common/route/SwapRouteFormatter.kt @@ -28,11 +28,10 @@ class RealSwapRouteFormatter( } private fun determinePathChains(path: Path>): List? { - // Do not display path of less then 2 elements - if (path.size < 2) return null + if (path.isEmpty()) return null val firstEdge = path.first().edge - val firstChain = firstEdge.to.chainId + val firstChain = firstEdge.from.chainId var currentChainId = firstChain val foundChains = mutableListOf(currentChainId) @@ -45,6 +44,6 @@ class RealSwapRouteFormatter( } } - return foundChains + return foundChains.takeIf { foundChains.size >= 2 } } } diff --git a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/state/RealSwapSettingsState.kt b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/common/state/RealSwapSettingsState.kt similarity index 97% rename from feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/state/RealSwapSettingsState.kt rename to feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/common/state/RealSwapSettingsState.kt index 830d0fa907..e7dda60cce 100644 --- a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/state/RealSwapSettingsState.kt +++ b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/common/state/RealSwapSettingsState.kt @@ -1,4 +1,4 @@ -package io.novafoundation.nova.feature_swap_impl.presentation.state +package io.novafoundation.nova.feature_swap_impl.presentation.common.state import io.novafoundation.nova.common.utils.Fraction import io.novafoundation.nova.feature_swap_api.presentation.state.SwapSettings diff --git a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/state/SwapSettingsStateProvider.kt b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/common/state/SwapSettingsStateProvider.kt similarity index 93% rename from feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/state/SwapSettingsStateProvider.kt rename to feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/common/state/SwapSettingsStateProvider.kt index 0c8f220776..e26b6e3cb6 100644 --- a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/state/SwapSettingsStateProvider.kt +++ b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/common/state/SwapSettingsStateProvider.kt @@ -1,4 +1,4 @@ -package io.novafoundation.nova.feature_swap_impl.presentation.state +package io.novafoundation.nova.feature_swap_impl.presentation.common.state import io.novafoundation.nova.common.data.memory.ComputationalCache import io.novafoundation.nova.common.utils.flowOfAll diff --git a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/common/state/SwapStateStore.kt b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/common/state/SwapStateStore.kt index 27f894f668..83f049b6e1 100644 --- a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/common/state/SwapStateStore.kt +++ b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/common/state/SwapStateStore.kt @@ -1,10 +1,18 @@ package io.novafoundation.nova.feature_swap_impl.presentation.common.state +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.filterNotNull + interface SwapStateStore { fun setState(state: SwapState) + fun resetState() + fun getState(): SwapState? + + fun stateFlow(): Flow } fun SwapStateStore.getStateOrThrow(): SwapState { @@ -15,14 +23,22 @@ fun SwapStateStore.getStateOrThrow(): SwapState { class InMemorySwapStateStore() : SwapStateStore { - private var quote: SwapState? = null + private var swapState = MutableStateFlow(null) override fun setState(state: SwapState) { - this.quote = state + this.swapState.value = state + } + + override fun resetState() { + swapState.value = null } override fun getState(): SwapState? { - return quote + return swapState.value + } + + override fun stateFlow(): Flow { + return swapState.filterNotNull() } } diff --git a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/common/state/SwapStateStoreProvider.kt b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/common/state/SwapStateStoreProvider.kt index 9e3d8d6820..69dd2029dc 100644 --- a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/common/state/SwapStateStoreProvider.kt +++ b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/common/state/SwapStateStoreProvider.kt @@ -1,7 +1,9 @@ package io.novafoundation.nova.feature_swap_impl.presentation.common.state import io.novafoundation.nova.common.data.memory.ComputationalCache +import io.novafoundation.nova.common.utils.flowOfAll import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.Flow interface SwapStateStoreProvider { @@ -26,3 +28,7 @@ class RealSwapStateStoreProvider( suspend fun SwapStateStoreProvider.getStateOrThrow(computationScope: CoroutineScope): SwapState { return getStore(computationScope).getStateOrThrow() } + +fun SwapStateStoreProvider.stateFlow(computationScope: CoroutineScope): Flow { + return flowOfAll { getStore(computationScope).stateFlow() } +} diff --git a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/views/SwapAmountInputView.kt b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/common/views/SwapAmountInputView.kt similarity index 98% rename from feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/views/SwapAmountInputView.kt rename to feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/common/views/SwapAmountInputView.kt index 2362cb1a09..177177f5f5 100644 --- a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/views/SwapAmountInputView.kt +++ b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/common/views/SwapAmountInputView.kt @@ -1,4 +1,4 @@ -package io.novafoundation.nova.feature_swap_impl.presentation.views +package io.novafoundation.nova.feature_swap_impl.presentation.common.views import android.content.Context import android.util.AttributeSet diff --git a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/confirmation/SwapConfirmationViewModel.kt b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/confirmation/SwapConfirmationViewModel.kt index 403dc9bd10..d565a1bae6 100644 --- a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/confirmation/SwapConfirmationViewModel.kt +++ b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/confirmation/SwapConfirmationViewModel.kt @@ -47,7 +47,7 @@ import io.novafoundation.nova.feature_swap_impl.presentation.common.route.SwapRo import io.novafoundation.nova.feature_swap_impl.presentation.common.state.SwapStateStoreProvider import io.novafoundation.nova.feature_swap_impl.presentation.common.state.getStateOrThrow import io.novafoundation.nova.feature_swap_impl.presentation.confirmation.model.SwapConfirmationDetailsModel -import io.novafoundation.nova.feature_swap_impl.presentation.mixin.maxAction.MaxActionProviderFactory +import io.novafoundation.nova.feature_swap_impl.presentation.common.mixin.maxAction.MaxActionProviderFactory import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.types.Balance import io.novafoundation.nova.feature_wallet_api.domain.ArbitraryAssetUseCase import io.novafoundation.nova.feature_wallet_api.domain.interfaces.TokenRepository diff --git a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/confirmation/di/SwapConfirmationModule.kt b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/confirmation/di/SwapConfirmationModule.kt index 351da19803..fa75032a94 100644 --- a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/confirmation/di/SwapConfirmationModule.kt +++ b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/confirmation/di/SwapConfirmationModule.kt @@ -24,7 +24,7 @@ import io.novafoundation.nova.feature_swap_impl.presentation.common.SlippageAler import io.novafoundation.nova.feature_swap_impl.presentation.common.route.SwapRouteFormatter import io.novafoundation.nova.feature_swap_impl.presentation.common.state.SwapStateStoreProvider import io.novafoundation.nova.feature_swap_impl.presentation.confirmation.SwapConfirmationViewModel -import io.novafoundation.nova.feature_swap_impl.presentation.mixin.maxAction.MaxActionProviderFactory +import io.novafoundation.nova.feature_swap_impl.presentation.common.mixin.maxAction.MaxActionProviderFactory import io.novafoundation.nova.feature_wallet_api.domain.ArbitraryAssetUseCase import io.novafoundation.nova.feature_wallet_api.domain.interfaces.TokenRepository import io.novafoundation.nova.feature_wallet_api.domain.interfaces.WalletRepository diff --git a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/main/SwapMainSettingsFragment.kt b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/main/SwapMainSettingsFragment.kt index c240ec22f5..8a11e7b187 100644 --- a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/main/SwapMainSettingsFragment.kt +++ b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/main/SwapMainSettingsFragment.kt @@ -8,6 +8,7 @@ import io.novafoundation.nova.common.base.BaseFragment import io.novafoundation.nova.common.di.FeatureUtils import io.novafoundation.nova.common.mixin.impl.observeValidations import io.novafoundation.nova.common.utils.applyStatusBarInsets +import io.novafoundation.nova.common.utils.hideKeyboard import io.novafoundation.nova.common.utils.postToUiThread import io.novafoundation.nova.common.utils.setSelectionEnd import io.novafoundation.nova.common.utils.setVisible @@ -76,6 +77,11 @@ class SwapMainSettingsFragment : BaseFragment() { swapMainSettingsDetailsNetworkFee.setOnClickListener { viewModel.networkFeeClicked() } swapMainSettingsContinue.setOnClickListener { viewModel.continueButtonClicked() } swapMainSettingsContinue.prepareForProgress(this) + swapMainSettingsRoute.setOnClickListener { + viewModel.routeClicked() + + hideKeyboard() + } swapMainSettingsGetAssetIn.setOnClickListener { viewModel.getAssetInClicked() } } diff --git a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/main/SwapMainSettingsViewModel.kt b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/main/SwapMainSettingsViewModel.kt index d8464d3033..6d3b18b88c 100644 --- a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/main/SwapMainSettingsViewModel.kt +++ b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/main/SwapMainSettingsViewModel.kt @@ -52,19 +52,19 @@ import io.novafoundation.nova.feature_swap_impl.domain.model.GetAssetInOption import io.novafoundation.nova.feature_swap_impl.presentation.SwapRouter import io.novafoundation.nova.feature_swap_impl.presentation.common.fee.SwapFeeFormatter import io.novafoundation.nova.feature_swap_impl.presentation.common.fee.SwapFeeInspector +import io.novafoundation.nova.feature_swap_impl.presentation.common.fieldValidation.EnoughAmountToSwapValidatorFactory +import io.novafoundation.nova.feature_swap_impl.presentation.common.fieldValidation.LiquidityFieldValidatorFactory +import io.novafoundation.nova.feature_swap_impl.presentation.common.fieldValidation.SwapReceiveAmountAboveEDFieldValidatorFactory +import io.novafoundation.nova.feature_swap_impl.presentation.common.mixin.maxAction.MaxActionProviderFactory import io.novafoundation.nova.feature_swap_impl.presentation.common.route.SwapRouteFormatter import io.novafoundation.nova.feature_swap_impl.presentation.common.route.SwapRouteState import io.novafoundation.nova.feature_swap_impl.presentation.common.state.SwapState import io.novafoundation.nova.feature_swap_impl.presentation.common.state.SwapStateStoreProvider -import io.novafoundation.nova.feature_swap_impl.presentation.fieldValidation.EnoughAmountToSwapValidatorFactory -import io.novafoundation.nova.feature_swap_impl.presentation.fieldValidation.LiquidityFieldValidatorFactory -import io.novafoundation.nova.feature_swap_impl.presentation.fieldValidation.SwapReceiveAmountAboveEDFieldValidatorFactory +import io.novafoundation.nova.feature_swap_impl.presentation.common.state.swapSettingsFlow import io.novafoundation.nova.feature_swap_impl.presentation.main.input.SwapAmountInputMixin import io.novafoundation.nova.feature_swap_impl.presentation.main.input.SwapAmountInputMixinFactory import io.novafoundation.nova.feature_swap_impl.presentation.main.input.SwapInputMixinPriceImpactFiatFormatterFactory import io.novafoundation.nova.feature_swap_impl.presentation.main.view.GetAssetInBottomSheet -import io.novafoundation.nova.feature_swap_impl.presentation.mixin.maxAction.MaxActionProviderFactory -import io.novafoundation.nova.feature_swap_impl.presentation.state.swapSettingsFlow import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.types.Balance import io.novafoundation.nova.feature_wallet_api.domain.ArbitraryAssetUseCase import io.novafoundation.nova.feature_wallet_api.domain.model.Asset @@ -238,8 +238,10 @@ class SwapMainSettingsViewModel( assetInFlow, assetOutFlow, ::formatButtonStates - ).distinctUntilChanged() + ) + .distinctUntilChanged() .debounce(100) + .shareInBackground() val swapDirectionFlipped: MutableLiveData> = MutableLiveData() @@ -327,18 +329,12 @@ class SwapMainSettingsViewModel( } } - fun continueButtonClicked() { - launch { - val quotingState = quotingState.value - if (quotingState !is QuotingState.Loaded) return@launch + fun routeClicked() = setSwapStateAfter { + swapRouter.openSwapRoute() + } - val swapState = SwapState( - quote = quotingState.quote, - fee = feeMixin.awaitFee(), - slippage = swapSettings.first().slippage - ) - swapStateStoreProvider.getStore(viewModelScope).setState(swapState) - swapRouter.openSwapConfirmation() + fun continueButtonClicked() = setSwapStateAndThen { + swapRouter.openSwapConfirmation() // val validationSystem = swapInteractor.validationSystem() // val payload = getValidationPayload() ?: return@launch @@ -352,7 +348,6 @@ class SwapMainSettingsViewModel( // _validationProgress.value = false // openSwapConfirmation(validPayload) // } - } } fun rateDetailsClicked() { @@ -391,6 +386,41 @@ class SwapMainSettingsViewModel( swapRouter.back() } + private fun setSwapStateAndThen(action: () -> Unit): Unit { + launch { + val quotingState = quotingState.value + if (quotingState !is QuotingState.Loaded) return@launch + + val swapState = SwapState( + quote = quotingState.quote, + fee = feeMixin.awaitFee(), + slippage = swapSettings.first().slippage + ) + swapStateStoreProvider.getStore(viewModelScope).setState(swapState) + action() + } + } + + private fun setSwapStateAfter(action: () -> Unit) { + launch { + val quotingState = quotingState.value + if (quotingState !is QuotingState.Loaded) return@launch + + val store = swapStateStoreProvider.getStore(viewModelScope) + store.resetState() + + action() + + val swapState = SwapState( + quote = quotingState.quote, + fee = feeMixin.awaitFee(), + slippage = swapSettings.first().slippage + ) + + store.setState(swapState) + } + } + private fun createMaxActionProvider(): MaxActionProvider { return maxActionProviderFactory.create( assetInFlow = assetInFlow, @@ -615,7 +645,7 @@ class SwapMainSettingsViewModel( } private suspend fun QuotingState.toSwapRouteState(): SwapRouteState { - return when(this) { + return when (this) { QuotingState.Default -> ExtendedLoadingState.Loaded(null) is QuotingState.Error -> ExtendedLoadingState.Error(error) is QuotingState.Loaded -> ExtendedLoadingState.Loaded(swapRouteFormatter.formatSwapRoute(quote)) diff --git a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/main/di/SwapMainSettingsModule.kt b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/main/di/SwapMainSettingsModule.kt index b03b1c0095..1e88c55e80 100644 --- a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/main/di/SwapMainSettingsModule.kt +++ b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/main/di/SwapMainSettingsModule.kt @@ -24,13 +24,13 @@ import io.novafoundation.nova.feature_swap_impl.presentation.SwapRouter import io.novafoundation.nova.feature_swap_impl.presentation.common.PriceImpactFormatter import io.novafoundation.nova.feature_swap_impl.presentation.common.route.SwapRouteFormatter import io.novafoundation.nova.feature_swap_impl.presentation.common.state.SwapStateStoreProvider -import io.novafoundation.nova.feature_swap_impl.presentation.fieldValidation.EnoughAmountToSwapValidatorFactory -import io.novafoundation.nova.feature_swap_impl.presentation.fieldValidation.LiquidityFieldValidatorFactory -import io.novafoundation.nova.feature_swap_impl.presentation.fieldValidation.SwapReceiveAmountAboveEDFieldValidatorFactory +import io.novafoundation.nova.feature_swap_impl.presentation.common.fieldValidation.EnoughAmountToSwapValidatorFactory +import io.novafoundation.nova.feature_swap_impl.presentation.common.fieldValidation.LiquidityFieldValidatorFactory +import io.novafoundation.nova.feature_swap_impl.presentation.common.fieldValidation.SwapReceiveAmountAboveEDFieldValidatorFactory import io.novafoundation.nova.feature_swap_impl.presentation.main.SwapMainSettingsViewModel import io.novafoundation.nova.feature_swap_impl.presentation.main.input.SwapAmountInputMixinFactory import io.novafoundation.nova.feature_swap_impl.presentation.main.input.SwapInputMixinPriceImpactFiatFormatterFactory -import io.novafoundation.nova.feature_swap_impl.presentation.mixin.maxAction.MaxActionProviderFactory +import io.novafoundation.nova.feature_swap_impl.presentation.common.mixin.maxAction.MaxActionProviderFactory import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.AssetSourceRegistry import io.novafoundation.nova.feature_wallet_api.domain.ArbitraryAssetUseCase import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.v2.FeeLoaderMixinV2 diff --git a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/main/input/SwapAmountInputUi.kt b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/main/input/SwapAmountInputUi.kt index 9d7b5cc20d..9a48733c1c 100644 --- a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/main/input/SwapAmountInputUi.kt +++ b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/main/input/SwapAmountInputUi.kt @@ -1,7 +1,7 @@ package io.novafoundation.nova.feature_swap_impl.presentation.main.input import io.novafoundation.nova.common.base.BaseFragment -import io.novafoundation.nova.feature_swap_impl.presentation.views.SwapAmountInputView +import io.novafoundation.nova.feature_swap_impl.presentation.common.views.SwapAmountInputView import io.novafoundation.nova.feature_wallet_api.presentation.mixin.amountChooser.MaxAvailableView import io.novafoundation.nova.feature_wallet_api.presentation.mixin.amountChooser.setupAmountChooserBase diff --git a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/options/SwapOptionsViewModel.kt b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/options/SwapOptionsViewModel.kt index a07a4fb7dc..832b06ac98 100644 --- a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/options/SwapOptionsViewModel.kt +++ b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/options/SwapOptionsViewModel.kt @@ -20,7 +20,7 @@ import io.novafoundation.nova.feature_swap_impl.R import io.novafoundation.nova.feature_swap_impl.domain.interactor.SwapInteractor import io.novafoundation.nova.feature_swap_impl.presentation.SwapRouter import io.novafoundation.nova.feature_swap_impl.presentation.common.SlippageAlertMixinFactory -import io.novafoundation.nova.feature_swap_impl.presentation.fieldValidation.SlippageFieldValidatorFactory +import io.novafoundation.nova.feature_swap_impl.presentation.common.fieldValidation.SlippageFieldValidatorFactory import kotlinx.coroutines.async import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.combine diff --git a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/options/di/SwapOptionsModule.kt b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/options/di/SwapOptionsModule.kt index ac305e7176..b9eb481626 100644 --- a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/options/di/SwapOptionsModule.kt +++ b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/options/di/SwapOptionsModule.kt @@ -14,7 +14,7 @@ import io.novafoundation.nova.feature_swap_api.presentation.state.SwapSettingsSt import io.novafoundation.nova.feature_swap_impl.domain.interactor.SwapInteractor import io.novafoundation.nova.feature_swap_impl.presentation.SwapRouter import io.novafoundation.nova.feature_swap_impl.presentation.common.SlippageAlertMixinFactory -import io.novafoundation.nova.feature_swap_impl.presentation.fieldValidation.SlippageFieldValidatorFactory +import io.novafoundation.nova.feature_swap_impl.presentation.common.fieldValidation.SlippageFieldValidatorFactory import io.novafoundation.nova.feature_swap_impl.presentation.options.SwapOptionsViewModel @Module(includes = [ViewModelModule::class]) diff --git a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/route/SwapRouteFragment.kt b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/route/SwapRouteFragment.kt new file mode 100644 index 0000000000..574d5615f7 --- /dev/null +++ b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/route/SwapRouteFragment.kt @@ -0,0 +1,80 @@ +package io.novafoundation.nova.feature_swap_impl.presentation.route + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.recyclerview.widget.ConcatAdapter +import io.novafoundation.nova.common.base.BaseFragment +import io.novafoundation.nova.common.di.FeatureUtils +import io.novafoundation.nova.common.domain.onLoaded +import io.novafoundation.nova.common.domain.onNotLoaded +import io.novafoundation.nova.common.utils.applyStatusBarInsets +import io.novafoundation.nova.common.utils.makeGone +import io.novafoundation.nova.common.utils.makeVisible +import io.novafoundation.nova.feature_swap_api.di.SwapFeatureApi +import io.novafoundation.nova.feature_swap_impl.R +import io.novafoundation.nova.feature_swap_impl.di.SwapFeatureComponent +import io.novafoundation.nova.feature_swap_impl.presentation.route.list.SwapRouteAdapter +import io.novafoundation.nova.feature_swap_impl.presentation.route.list.SwapRouteHeaderAdapter +import io.novafoundation.nova.feature_swap_impl.presentation.route.list.SwapRouteViewHolder +import io.novafoundation.nova.feature_swap_impl.presentation.route.list.TimelineItemDecoration +import kotlinx.android.synthetic.main.fragment_route.swapRouteContent +import kotlinx.android.synthetic.main.fragment_route.swapRouteProgress + +class SwapRouteFragment : BaseFragment(), SwapRouteHeaderAdapter.Handler { + + private lateinit var headerAdapter: SwapRouteHeaderAdapter + private lateinit var routeAdapter: SwapRouteAdapter + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle?, + ): View? { + return inflater.inflate(R.layout.fragment_route, container, false) + } + + override fun initViews() { + swapRouteContent.applyStatusBarInsets() + swapRouteContent.setHasFixedSize(true) + swapRouteContent.itemAnimator = null + + val timelineDecoration = TimelineItemDecoration( + context = requireContext(), + shouldDecorate = { it is SwapRouteViewHolder } + ) + swapRouteContent.addItemDecoration(timelineDecoration) + + headerAdapter = SwapRouteHeaderAdapter(this) + routeAdapter = SwapRouteAdapter() + + swapRouteContent.adapter = ConcatAdapter(headerAdapter, routeAdapter) + } + + override fun inject() { + FeatureUtils.getFeature( + requireContext(), + SwapFeatureApi::class.java + ) + .swapRoute() + .create(this) + .inject(this) + } + + override fun subscribe(viewModel: SwapRouteViewModel) { + viewModel.swapRoute.observe { routeState -> + routeState.onLoaded { + swapRouteProgress.makeGone() + routeAdapter.submitList(it) + }.onNotLoaded { + swapRouteProgress.makeVisible() + routeAdapter.submitList(emptyList()) + } + } + } + + override fun backClicked() { + viewModel.backClicked() + } +} diff --git a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/route/SwapRouteViewModel.kt b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/route/SwapRouteViewModel.kt new file mode 100644 index 0000000000..4e8a86c54f --- /dev/null +++ b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/route/SwapRouteViewModel.kt @@ -0,0 +1,95 @@ +package io.novafoundation.nova.feature_swap_impl.presentation.route + +import androidx.lifecycle.viewModelScope +import io.novafoundation.nova.common.base.BaseViewModel +import io.novafoundation.nova.common.presentation.AssetIconProvider +import io.novafoundation.nova.common.resources.ResourceManager +import io.novafoundation.nova.common.utils.withSafeLoading +import io.novafoundation.nova.feature_account_api.presenatation.chain.getAssetIconOrFallback +import io.novafoundation.nova.feature_swap_api.domain.model.AtomicOperationDisplayData +import io.novafoundation.nova.feature_swap_api.domain.model.SwapFee +import io.novafoundation.nova.feature_swap_impl.R +import io.novafoundation.nova.feature_swap_impl.domain.interactor.SwapInteractor +import io.novafoundation.nova.feature_swap_impl.presentation.SwapRouter +import io.novafoundation.nova.feature_swap_impl.presentation.common.state.SwapStateStoreProvider +import io.novafoundation.nova.feature_swap_impl.presentation.common.state.stateFlow +import io.novafoundation.nova.feature_swap_impl.presentation.route.model.SwapRouteItemModel +import io.novafoundation.nova.feature_swap_impl.presentation.route.view.TokenAmountModel +import io.novafoundation.nova.feature_wallet_api.domain.model.FiatAmount +import io.novafoundation.nova.feature_wallet_api.presentation.formatters.formatAsCurrency +import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry +import io.novafoundation.nova.runtime.multiNetwork.asset +import io.novafoundation.nova.runtime.multiNetwork.chainWithAsset +import kotlinx.coroutines.flow.map + +class SwapRouteViewModel( + private val swapInteractor: SwapInteractor, + private val swapStateStoreProvider: SwapStateStoreProvider, + private val chainRegistry: ChainRegistry, + private val assetIconProvider: AssetIconProvider, + private val router: SwapRouter, + private val resourceManager: ResourceManager +) : BaseViewModel() { + + val swapRoute = swapStateStoreProvider.stateFlow(viewModelScope) + .map { it.fee.toSwapRouteUi() } + .withSafeLoading() + .shareInBackground() + + private suspend fun SwapFee.toSwapRouteUi(): List { + val pricedFees = swapInteractor.calculateSegmentFiatPrices(this) + + return pricedFees.zip(segments).mapIndexed { index, (pricedFee, segment) -> + val displayData = segment.operation.constructDisplayData() + displayData.toUi(pricedFee, id = index) + } + } + + private suspend fun AtomicOperationDisplayData.toUi( + fee: FiatAmount, + id: Int + ): SwapRouteItemModel { + val formattedFee = fee.formatAsCurrency() + val feeWithLabel = resourceManager.getString(R.string.common_fee_with_label, formattedFee) + + return when (this) { + is AtomicOperationDisplayData.Swap -> toUi(feeWithLabel, id) + is AtomicOperationDisplayData.Transfer -> toUi(feeWithLabel, id) + } + } + + private suspend fun AtomicOperationDisplayData.Transfer.toUi(fee: String, id: Int): SwapRouteItemModel.Transfer { + val (chainFrom, assetFrom) = chainRegistry.chainWithAsset(from) + val assetFromIcon = assetIconProvider.getAssetIconOrFallback(assetFrom) + + val chainTo = chainRegistry.getChain(to.chainId) + + return SwapRouteItemModel.Transfer( + id = id, + amount = TokenAmountModel.from(assetFrom, assetFromIcon, amount), + fee = fee, + originChainName = chainFrom.name, + destinationChainName = chainTo.name + ) + } + + private suspend fun AtomicOperationDisplayData.Swap.toUi(fee: String, id: Int): SwapRouteItemModel.Swap { + val (chainFrom, assetFrom) = chainRegistry.chainWithAsset(from.chainAssetId) + val assetFromIcon = assetIconProvider.getAssetIconOrFallback(assetFrom) + + val assetTo = chainRegistry.asset(to.chainAssetId) + val assetToIcon = assetIconProvider.getAssetIconOrFallback(assetTo) + + return SwapRouteItemModel.Swap( + id = id, + amountFrom = TokenAmountModel.from(assetFrom, assetFromIcon, from.amount), + amountTo = TokenAmountModel.from(assetTo, assetToIcon, to.amount), + fee = fee, + chain = chainFrom.name + ) + } + + fun backClicked() { + router.back() + } +} diff --git a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/route/di/SwapRouteComponent.kt b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/route/di/SwapRouteComponent.kt new file mode 100644 index 0000000000..e86b868401 --- /dev/null +++ b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/route/di/SwapRouteComponent.kt @@ -0,0 +1,26 @@ +package io.novafoundation.nova.feature_swap_impl.presentation.route.di + +import androidx.fragment.app.Fragment +import dagger.BindsInstance +import dagger.Subcomponent +import io.novafoundation.nova.common.di.scope.ScreenScope +import io.novafoundation.nova.feature_swap_impl.presentation.route.SwapRouteFragment + +@Subcomponent( + modules = [ + SwapRouteModule::class + ] +) +@ScreenScope +interface SwapRouteComponent { + + @Subcomponent.Factory + interface Factory { + + fun create( + @BindsInstance fragment: Fragment, + ): SwapRouteComponent + } + + fun inject(fragment: SwapRouteFragment) +} diff --git a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/route/di/SwapRouteModule.kt b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/route/di/SwapRouteModule.kt new file mode 100644 index 0000000000..4e5850c7fa --- /dev/null +++ b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/route/di/SwapRouteModule.kt @@ -0,0 +1,50 @@ +package io.novafoundation.nova.feature_swap_impl.presentation.route.di + +import androidx.fragment.app.Fragment +import androidx.lifecycle.ViewModel +import androidx.lifecycle.ViewModelProvider +import dagger.Module +import dagger.Provides +import dagger.multibindings.IntoMap +import io.novafoundation.nova.common.di.viewmodel.ViewModelKey +import io.novafoundation.nova.common.di.viewmodel.ViewModelModule +import io.novafoundation.nova.common.presentation.AssetIconProvider +import io.novafoundation.nova.common.resources.ResourceManager +import io.novafoundation.nova.feature_swap_impl.domain.interactor.SwapInteractor +import io.novafoundation.nova.feature_swap_impl.presentation.SwapRouter +import io.novafoundation.nova.feature_swap_impl.presentation.common.state.SwapStateStoreProvider +import io.novafoundation.nova.feature_swap_impl.presentation.route.SwapRouteViewModel +import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry + +@Module(includes = [ViewModelModule::class]) +class SwapRouteModule { + + @Provides + @IntoMap + @ViewModelKey(SwapRouteViewModel::class) + fun provideViewModel( + swapInteractor: SwapInteractor, + swapStateStoreProvider: SwapStateStoreProvider, + chainRegistry: ChainRegistry, + assetIconProvider: AssetIconProvider, + resourceManager: ResourceManager, + router: SwapRouter, + ): ViewModel { + return SwapRouteViewModel( + swapInteractor = swapInteractor, + swapStateStoreProvider = swapStateStoreProvider, + chainRegistry = chainRegistry, + assetIconProvider = assetIconProvider, + router = router, + resourceManager = resourceManager + ) + } + + @Provides + fun provideViewModelCreator( + fragment: Fragment, + viewModelFactory: ViewModelProvider.Factory + ): SwapRouteViewModel { + return ViewModelProvider(fragment, viewModelFactory).get(SwapRouteViewModel::class.java) + } +} diff --git a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/route/list/SwapRouteAdapter.kt b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/route/list/SwapRouteAdapter.kt new file mode 100644 index 0000000000..361780cd64 --- /dev/null +++ b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/route/list/SwapRouteAdapter.kt @@ -0,0 +1,90 @@ +package io.novafoundation.nova.feature_swap_impl.presentation.route.list + +import android.view.View +import android.view.ViewGroup +import androidx.recyclerview.widget.DiffUtil +import io.novafoundation.nova.common.list.BaseListAdapter +import io.novafoundation.nova.common.list.BaseViewHolder +import io.novafoundation.nova.common.utils.inflateChild +import io.novafoundation.nova.common.view.shape.getRoundedCornerDrawable +import io.novafoundation.nova.feature_swap_impl.R +import io.novafoundation.nova.feature_swap_impl.presentation.route.model.SwapRouteItemModel +import kotlinx.android.synthetic.main.item_route_swap.view.itemRouteSwapAmountFrom +import kotlinx.android.synthetic.main.item_route_swap.view.itemRouteSwapAmountTo +import kotlinx.android.synthetic.main.item_route_swap.view.itemRouteSwapChain +import kotlinx.android.synthetic.main.item_route_swap.view.itemRouteSwapFee +import kotlinx.android.synthetic.main.item_route_transfer.view.itemRouteTransferAmount +import kotlinx.android.synthetic.main.item_route_transfer.view.itemRouteTransferFee +import kotlinx.android.synthetic.main.item_route_transfer.view.itemRouteTransferFrom +import kotlinx.android.synthetic.main.item_route_transfer.view.itemRouteTransferTo + +class SwapRouteAdapter : BaseListAdapter(SwapRouteDiffCallback()) { + + override fun onBindViewHolder(holder: SwapRouteViewHolder, position: Int) { + when (val item = getItem(position)) { + is SwapRouteItemModel.Swap -> (holder as SwapRouteSwapViewHolder).bind(item) + is SwapRouteItemModel.Transfer -> (holder as SwapRouteTransferViewHolder).bind(item) + } + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): SwapRouteViewHolder { + return when (viewType) { + R.layout.item_route_swap -> SwapRouteSwapViewHolder(parent) + R.layout.item_route_transfer -> SwapRouteTransferViewHolder(parent) + else -> error("Unknown viewType: $viewType") + } + } + + override fun getItemViewType(position: Int): Int { + return when (getItem(position)) { + is SwapRouteItemModel.Swap -> R.layout.item_route_swap + is SwapRouteItemModel.Transfer -> R.layout.item_route_transfer + } + } +} + +sealed class SwapRouteViewHolder(itemView: View) : BaseViewHolder(itemView) { + + init { + with(itemView) { + background = context.getRoundedCornerDrawable(R.color.input_background) + } + } +} + +class SwapRouteTransferViewHolder(parentView: ViewGroup) : SwapRouteViewHolder(parentView.inflateChild(R.layout.item_route_transfer)) { + + + fun bind(model: SwapRouteItemModel.Transfer) = with(containerView) { + itemRouteTransferAmount.setModel(model.amount) + itemRouteTransferFee.text = model.fee + itemRouteTransferFrom.text = model.originChainName + itemRouteTransferTo.text = model.destinationChainName + } + + override fun unbind() {} +} + +class SwapRouteSwapViewHolder(parentView: ViewGroup) : SwapRouteViewHolder(parentView.inflateChild(R.layout.item_route_swap)) { + + + fun bind(model: SwapRouteItemModel.Swap) = with(containerView) { + itemRouteSwapAmountFrom.setModel(model.amountFrom) + itemRouteSwapAmountTo.setModel(model.amountTo) + itemRouteSwapFee.text = model.fee + itemRouteSwapChain.text = model.chain + } + + override fun unbind() {} +} + +private class SwapRouteDiffCallback : DiffUtil.ItemCallback() { + + override fun areItemsTheSame(oldItem: SwapRouteItemModel, newItem: SwapRouteItemModel): Boolean { + return oldItem.id == newItem.id + } + + override fun areContentsTheSame(oldItem: SwapRouteItemModel, newItem: SwapRouteItemModel): Boolean { + return oldItem == newItem + } +} diff --git a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/route/list/SwapRouteHeaderAdapter.kt b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/route/list/SwapRouteHeaderAdapter.kt new file mode 100644 index 0000000000..7df7c279bb --- /dev/null +++ b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/route/list/SwapRouteHeaderAdapter.kt @@ -0,0 +1,37 @@ +package io.novafoundation.nova.feature_swap_impl.presentation.route.list + +import android.view.ViewGroup +import androidx.recyclerview.widget.RecyclerView +import io.novafoundation.nova.common.utils.inflateChild +import io.novafoundation.nova.feature_swap_impl.R +import kotlinx.android.synthetic.main.item_route_header.view.swapRouteBack + +class SwapRouteHeaderAdapter( + private val handler: Handler +) : RecyclerView.Adapter() { + + interface Handler { + + fun backClicked() + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): SwapRouteHeaderViewHolder { + return SwapRouteHeaderViewHolder(parent, handler) + } + + override fun getItemCount(): Int { + return 1 + } + + override fun onBindViewHolder(holder: SwapRouteHeaderViewHolder, position: Int) {} +} + +class SwapRouteHeaderViewHolder( + parentView: ViewGroup, + handler: SwapRouteHeaderAdapter.Handler +) : RecyclerView.ViewHolder(parentView.inflateChild(R.layout.item_route_header)) { + + init { + itemView.swapRouteBack.setHomeButtonListener { handler.backClicked() } + } +} diff --git a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/route/list/TimelineItemDecoration.kt b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/route/list/TimelineItemDecoration.kt new file mode 100644 index 0000000000..d62d479f4b --- /dev/null +++ b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/route/list/TimelineItemDecoration.kt @@ -0,0 +1,103 @@ +package io.novafoundation.nova.feature_swap_impl.presentation.route.list + +import android.content.Context +import android.graphics.Canvas +import android.graphics.DashPathEffect +import android.graphics.Paint +import android.graphics.Rect +import android.view.View +import android.widget.TextView +import androidx.appcompat.widget.AppCompatTextView +import androidx.core.content.ContextCompat +import androidx.core.widget.TextViewCompat +import androidx.recyclerview.widget.RecyclerView +import io.novafoundation.nova.common.utils.WithContextExtensions +import io.novafoundation.nova.feature_swap_impl.R + + +class TimelineItemDecoration( + context: Context, + private val shouldDecorate: (RecyclerView.ViewHolder) -> Boolean +) : RecyclerView.ItemDecoration(), + WithContextExtensions by WithContextExtensions(context) { + + + private val linePaint = Paint().apply { + color = ContextCompat.getColor(context, R.color.timeline_line_color) + strokeWidth = 1.dpF + style = Paint.Style.STROKE + pathEffect = DashPathEffect(floatArrayOf(4.dpF, 4.dpF), 0f) + } + + private val circlePaint = Paint().apply { + color = ContextCompat.getColor(context, R.color.timeline_circle_color) + isAntiAlias = true + } + + private val textPaint = createTextPaint() + + private val circleRadius = 10.dp + + private val itemTopMargin = 12.dp + + private val lineHorizontalMargin = 18.dp + + private val lineToCircleMargin = 4.dp + + private val itemStartMargin = (lineHorizontalMargin + circleRadius) * 2 + private val itemEndMargin = 16.dp + + private val circleTopMargin = 10.dp + + override fun onDraw(canvas: Canvas, parent: RecyclerView, state: RecyclerView.State) { + val childCount = parent.childCount + for (i in 0 until childCount) { + val child = parent.getChildAt(i) + val viewHolder = parent.getChildViewHolder(child) + if (!shouldDecorate(viewHolder)) continue + + val position = viewHolder.bindingAdapterPosition + val centerX = lineHorizontalMargin + circleRadius.toFloat() + + val circleCenterY = (child.top + circleTopMargin + circleRadius).toFloat() + + if (position < viewHolder.bindingAdapter!!.itemCount - 1) { + val childSpaceEndY = child.bottom + itemTopMargin + val nextCircleTopY = childSpaceEndY + circleTopMargin + + canvas.drawLine( + centerX, + circleCenterY + circleRadius + lineToCircleMargin, + centerX, + nextCircleTopY - lineToCircleMargin.toFloat(), + linePaint + ) + } + + canvas.drawCircle(centerX, circleCenterY, circleRadius.toFloat(), circlePaint) + + val text = (position + 1).toString() + val textY = circleCenterY - (textPaint.descent() + textPaint.ascent()) / 2 + + canvas.drawText(text, centerX, textY, textPaint) + } + } + + override fun getItemOffsets(outRect: Rect, view: View, parent: RecyclerView, state: RecyclerView.State) { + val viewHolder = parent.getChildViewHolder(view) + if (shouldDecorate(viewHolder)) { + outRect.left = itemStartMargin + outRect.top = itemTopMargin + outRect.right = itemEndMargin + } + } + + private fun createTextPaint(): Paint { + val textView: TextView = AppCompatTextView(providedContext) + TextViewCompat.setTextAppearance(textView, R.style.TextAppearance_NovaFoundation_SemiBold_Caps1) + return textView.paint.apply { + color = ContextCompat.getColor(providedContext, R.color.text_secondary) + textAlign = Paint.Align.CENTER + } + } +} diff --git a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/route/model/SwapRouteItemModel.kt b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/route/model/SwapRouteItemModel.kt new file mode 100644 index 0000000000..ef6a986737 --- /dev/null +++ b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/route/model/SwapRouteItemModel.kt @@ -0,0 +1,24 @@ +package io.novafoundation.nova.feature_swap_impl.presentation.route.model + +import io.novafoundation.nova.feature_swap_impl.presentation.route.view.TokenAmountModel + +sealed class SwapRouteItemModel { + + abstract val id: Int + + data class Transfer( + override val id: Int, + val amount: TokenAmountModel, + val fee: String, + val originChainName: String, + val destinationChainName: String, + ): SwapRouteItemModel() + + data class Swap( + override val id: Int, + val amountFrom: TokenAmountModel, + val amountTo: TokenAmountModel, + val fee: String, + val chain: String + ): SwapRouteItemModel() +} diff --git a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/route/view/TokenAmountView.kt b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/route/view/TokenAmountView.kt new file mode 100644 index 0000000000..71dc570be1 --- /dev/null +++ b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/route/view/TokenAmountView.kt @@ -0,0 +1,56 @@ +package io.novafoundation.nova.feature_swap_impl.presentation.route.view + +import android.content.Context +import android.util.AttributeSet +import android.view.Gravity +import android.view.View +import android.widget.LinearLayout +import coil.ImageLoader +import io.novafoundation.nova.common.di.FeatureUtils +import io.novafoundation.nova.common.utils.images.Icon +import io.novafoundation.nova.common.utils.images.setIcon +import io.novafoundation.nova.feature_swap_impl.R +import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.types.Balance +import io.novafoundation.nova.feature_wallet_api.presentation.formatters.formatPlanks +import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain +import kotlinx.android.synthetic.main.view_token_amount.view.viewTokenAmountAmount +import kotlinx.android.synthetic.main.view_token_amount.view.viewTokenAmountIcon + +class TokenAmountModel( + val amount: String, + val tokenIcon: Icon +) { + + companion object { + + fun from(chainAsset: Chain.Asset, assetIcon: Icon, amount: Balance) : TokenAmountModel { + return TokenAmountModel( + amount = amount.formatPlanks(chainAsset), + tokenIcon = assetIcon + ) + } + } +} + +class TokenAmountView @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null, + defStyleAttr: Int = 0, +) : LinearLayout(context, attrs, defStyleAttr) { + + private val imageLoader: ImageLoader by lazy(LazyThreadSafetyMode.NONE) { + FeatureUtils.getCommonApi(context).imageLoader() + } + + init { + orientation = HORIZONTAL + gravity = Gravity.CENTER_VERTICAL + + View.inflate(context, R.layout.view_token_amount, this) + } + + fun setModel(model: TokenAmountModel) { + viewTokenAmountAmount.text = model.amount + viewTokenAmountIcon.setIcon(model.tokenIcon, imageLoader) + } +} diff --git a/feature-swap-impl/src/main/res/layout/fragment_main_swap_settings.xml b/feature-swap-impl/src/main/res/layout/fragment_main_swap_settings.xml index 4d38299cff..836be3fac7 100644 --- a/feature-swap-impl/src/main/res/layout/fragment_main_swap_settings.xml +++ b/feature-swap-impl/src/main/res/layout/fragment_main_swap_settings.xml @@ -50,7 +50,7 @@ app:layout_constraintEnd_toEndOf="parent" app:layout_constraintTop_toTopOf="parent" /> - - + + + + + + \ No newline at end of file diff --git a/feature-swap-impl/src/main/res/layout/item_route_header.xml b/feature-swap-impl/src/main/res/layout/item_route_header.xml new file mode 100644 index 0000000000..eea1189e8c --- /dev/null +++ b/feature-swap-impl/src/main/res/layout/item_route_header.xml @@ -0,0 +1,33 @@ + + + + + + + + + \ No newline at end of file diff --git a/feature-swap-impl/src/main/res/layout/item_route_swap.xml b/feature-swap-impl/src/main/res/layout/item_route_swap.xml new file mode 100644 index 0000000000..48c4a7fdd6 --- /dev/null +++ b/feature-swap-impl/src/main/res/layout/item_route_swap.xml @@ -0,0 +1,77 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/feature-swap-impl/src/main/res/layout/item_route_transfer.xml b/feature-swap-impl/src/main/res/layout/item_route_transfer.xml new file mode 100644 index 0000000000..9765d10789 --- /dev/null +++ b/feature-swap-impl/src/main/res/layout/item_route_transfer.xml @@ -0,0 +1,81 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/feature-swap-impl/src/main/res/layout/view_token_amount.xml b/feature-swap-impl/src/main/res/layout/view_token_amount.xml new file mode 100644 index 0000000000..e1ebe46ac7 --- /dev/null +++ b/feature-swap-impl/src/main/res/layout/view_token_amount.xml @@ -0,0 +1,26 @@ + + + + + + + \ No newline at end of file diff --git a/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/domain/model/FiatAmount.kt b/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/domain/model/FiatAmount.kt index 708d7cd70a..03e5054179 100644 --- a/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/domain/model/FiatAmount.kt +++ b/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/domain/model/FiatAmount.kt @@ -6,4 +6,12 @@ import java.math.BigDecimal class FiatAmount( val currency: Currency, val price: BigDecimal -) +) { + + companion object { + + fun zero(currency: Currency): FiatAmount { + return FiatAmount(currency, BigDecimal.ZERO) + } + } +} diff --git a/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/domain/model/Operation.kt b/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/domain/model/Operation.kt index bb5181c209..38369a8b92 100644 --- a/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/domain/model/Operation.kt +++ b/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/domain/model/Operation.kt @@ -4,6 +4,7 @@ import io.novafoundation.nova.common.utils.isZero import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.types.Balance import io.novafoundation.nova.feature_wallet_api.domain.interfaces.TransactionFilter import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain +import io.novafoundation.nova.runtime.multiNetwork.chain.model.FullChainAssetId import java.math.BigDecimal import java.math.BigInteger @@ -82,6 +83,15 @@ data class ChainAssetWithAmount( val amount: Balance, ) +data class ChainAssetIdWithAmount( + val chainAssetId: FullChainAssetId, + val amount: Balance, +) + +fun FullChainAssetId.withAmount(amount: Balance): ChainAssetIdWithAmount { + return ChainAssetIdWithAmount(this, amount) +} + fun Chain.Asset.withAmount(amount: Balance): ChainAssetWithAmount { return ChainAssetWithAmount(this, amount) } diff --git a/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/presentation/formatters/FiatAmount.kt b/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/presentation/formatters/FiatAmount.kt new file mode 100644 index 0000000000..cfe2b1bfa1 --- /dev/null +++ b/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/presentation/formatters/FiatAmount.kt @@ -0,0 +1,6 @@ +package io.novafoundation.nova.feature_wallet_api.presentation.formatters + +import io.novafoundation.nova.feature_currency_api.presentation.formatters.formatAsCurrency +import io.novafoundation.nova.feature_wallet_api.domain.model.FiatAmount + +fun FiatAmount.formatAsCurrency() = price.formatAsCurrency(currency) From 5f6c381588b58b0858f91e14217cad99461e8302 Mon Sep 17 00:00:00 2001 From: valentun Date: Fri, 8 Nov 2024 14:36:47 +0700 Subject: [PATCH 44/83] Swap fee details --- .../app/root/navigation/swap/SwapNavigator.kt | 4 +- .../res/navigation/start_swap_nav_graph.xml | 25 ++-- .../nova/common/view/GenericTableCellView.kt | 15 +-- .../nova/common/view/HasDivider.kt | 1 + .../nova/common/view/TableCellView.kt | 34 ++++- .../nova/common/view/TableItem.kt | 9 ++ .../nova/common/view/TableView.kt | 21 ++-- .../res/layout/view_generic_table_cell.xml | 10 -- common/src/main/res/values/attrs.xml | 1 + common/src/main/res/values/strings.xml | 2 + .../feature_account_api/data/model/Fee.kt | 4 + .../balance/detail/AssetDetailBalancesView.kt | 4 +- .../balance/detail/LockedTokensBottomSheet.kt | 2 +- .../networkInfo/NetworkInfoAdapter.kt | 2 +- .../model/AtomicOperationFeeDisplayData.kt | 30 +++++ .../domain/model/AtomicSwapOperation.kt | 24 +--- .../feature_swap_api/domain/model/SwapFee.kt | 1 + .../model/fee/AtomicSwapOperationFee.kt | 30 +++++ .../SubmissionOnlyAtomicSwapOperationFee.kt | 25 ++++ .../AssetConversionExchange.kt | 14 +-- .../CrossChainTransferAssetExchange.kt | 61 ++++++--- .../hydraDx/HydraDxAssetExchange.kt | 6 +- .../di/SwapFeatureComponent.kt | 3 + .../domain/interactor/SwapInteractor.kt | 9 ++ .../presentation/SwapRouter.kt | 2 + .../common/route/SwapRouteTableCellView.kt | 9 ++ .../common/route/SwapRouteView.kt | 42 ++++++- .../confirmation/SwapConfirmationFragment.kt | 1 + .../confirmation/SwapConfirmationViewModel.kt | 52 ++++++-- .../presentation/fee/SwapFeeFragment.kt | 116 ++++++++++++++++++ .../presentation/fee/SwapFeeViewModel.kt | 111 +++++++++++++++++ .../presentation/fee/di/SwapFeeComponent.kt | 26 ++++ .../presentation/fee/di/SwapFeeModule.kt | 44 +++++++ .../fee/model/SwapSegmentFeeModel.kt | 14 +++ .../main/SwapMainSettingsViewModel.kt | 11 +- .../src/main/res/layout/fragment_swap_fee.xml | 43 +++++++ .../src/main/res/values/attrs.xml | 10 ++ .../presentation/view/FeeView.kt | 5 + 38 files changed, 714 insertions(+), 109 deletions(-) create mode 100644 common/src/main/java/io/novafoundation/nova/common/view/TableItem.kt create mode 100644 feature-swap-api/src/main/java/io/novafoundation/nova/feature_swap_api/domain/model/AtomicOperationFeeDisplayData.kt create mode 100644 feature-swap-api/src/main/java/io/novafoundation/nova/feature_swap_api/domain/model/fee/AtomicSwapOperationFee.kt create mode 100644 feature-swap-api/src/main/java/io/novafoundation/nova/feature_swap_api/domain/model/fee/SubmissionOnlyAtomicSwapOperationFee.kt create mode 100644 feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/fee/SwapFeeFragment.kt create mode 100644 feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/fee/SwapFeeViewModel.kt create mode 100644 feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/fee/di/SwapFeeComponent.kt create mode 100644 feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/fee/di/SwapFeeModule.kt create mode 100644 feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/fee/model/SwapSegmentFeeModel.kt create mode 100644 feature-swap-impl/src/main/res/layout/fragment_swap_fee.xml create mode 100644 feature-swap-impl/src/main/res/values/attrs.xml diff --git a/app/src/main/java/io/novafoundation/nova/app/root/navigation/swap/SwapNavigator.kt b/app/src/main/java/io/novafoundation/nova/app/root/navigation/swap/SwapNavigator.kt index 1bf6b5eb2c..e9927aa2e7 100644 --- a/app/src/main/java/io/novafoundation/nova/app/root/navigation/swap/SwapNavigator.kt +++ b/app/src/main/java/io/novafoundation/nova/app/root/navigation/swap/SwapNavigator.kt @@ -18,7 +18,9 @@ class SwapNavigator( override fun openSwapConfirmation() = performNavigation(R.id.action_swapMainSettingsFragment_to_swapConfirmationFragment) - override fun openSwapRoute() = performNavigation(R.id.action_swapSettingsFragment_to_swapRouteFragment) + override fun openSwapRoute() = performNavigation(R.id.action_open_swapRouteFragment) + + override fun openSwapFee() = performNavigation(R.id.action_open_swapFeeFragment) override fun openSwapOptions() { navigationHolder.navController?.navigate(R.id.action_swapMainSettingsFragment_to_swapOptionsFragment) diff --git a/app/src/main/res/navigation/start_swap_nav_graph.xml b/app/src/main/res/navigation/start_swap_nav_graph.xml index 9fe9d585ec..c7878d0c3d 100644 --- a/app/src/main/res/navigation/start_swap_nav_graph.xml +++ b/app/src/main/res/navigation/start_swap_nav_graph.xml @@ -33,14 +33,6 @@ app:exitAnim="@anim/fragment_open_exit" app:popEnterAnim="@anim/fragment_close_enter" app:popExitAnim="@anim/fragment_close_exit" /> - - + + + +

+ + \ No newline at end of file diff --git a/common/src/main/java/io/novafoundation/nova/common/view/GenericTableCellView.kt b/common/src/main/java/io/novafoundation/nova/common/view/GenericTableCellView.kt index ddb17adae9..89a4509231 100644 --- a/common/src/main/java/io/novafoundation/nova/common/view/GenericTableCellView.kt +++ b/common/src/main/java/io/novafoundation/nova/common/view/GenericTableCellView.kt @@ -14,7 +14,6 @@ import io.novafoundation.nova.common.utils.setDrawableEnd import io.novafoundation.nova.common.utils.setVisible import io.novafoundation.nova.common.utils.useAttributes import kotlinx.android.synthetic.main.view_generic_table_cell.view.genericTableCellTitle -import kotlinx.android.synthetic.main.view_generic_table_cell.view.genericTableCellValueDivider import kotlinx.android.synthetic.main.view_generic_table_cell.view.genericTableCellValueProgress open class GenericTableCellView @JvmOverloads constructor( @@ -22,13 +21,13 @@ open class GenericTableCellView @JvmOverloads constructor( attrs: AttributeSet? = null, defStyle: Int = 0, defStyleRes: Int = 0, -) : ConstraintLayout(context, attrs, defStyle, defStyleRes), HasDivider { +) : ConstraintLayout(context, attrs, defStyle, defStyleRes), TableItem { protected lateinit var valueView: V companion object { - private val SELF_IDS = listOf(R.id.genericTableCellValueDivider, R.id.genericTableCellTitle, R.id.genericTableCellValueProgress) + private val SELF_IDS = listOf(R.id.genericTableCellTitle, R.id.genericTableCellValueProgress) } init { @@ -41,10 +40,6 @@ open class GenericTableCellView @JvmOverloads constructor( attrs?.let(::applyAttributes) } - override fun setDividerVisible(visible: Boolean) { - genericTableCellValueDivider.setVisible(visible) - } - override fun onFinishInflate() { super.onFinishInflate() @@ -115,4 +110,10 @@ open class GenericTableCellView @JvmOverloads constructor( val titleIconEnd = typedArray.getResourceIdOrNull(R.styleable.GenericTableCellView_titleIcon) titleIconEnd?.let(::setTitleIconEnd) } + + override fun disableOwnDividers() {} + + override fun shouldDrawDivider(): Boolean { + return true + } } diff --git a/common/src/main/java/io/novafoundation/nova/common/view/HasDivider.kt b/common/src/main/java/io/novafoundation/nova/common/view/HasDivider.kt index 2363dec53e..4262c4bca2 100644 --- a/common/src/main/java/io/novafoundation/nova/common/view/HasDivider.kt +++ b/common/src/main/java/io/novafoundation/nova/common/view/HasDivider.kt @@ -1,5 +1,6 @@ package io.novafoundation.nova.common.view interface HasDivider { + fun setDividerVisible(visible: Boolean) } diff --git a/common/src/main/java/io/novafoundation/nova/common/view/TableCellView.kt b/common/src/main/java/io/novafoundation/nova/common/view/TableCellView.kt index 4da2908487..8a18017ed5 100644 --- a/common/src/main/java/io/novafoundation/nova/common/view/TableCellView.kt +++ b/common/src/main/java/io/novafoundation/nova/common/view/TableCellView.kt @@ -45,18 +45,21 @@ import kotlinx.android.synthetic.main.view_table_cell.view.tableCellValuePrimary import kotlinx.android.synthetic.main.view_table_cell.view.tableCellValueProgress import kotlinx.android.synthetic.main.view_table_cell.view.tableCellValueSecondary +private const val DRAW_DIVIDER_DEFAULT = true + open class TableCellView @JvmOverloads constructor( context: Context, attrs: AttributeSet? = null, defStyle: Int = 0, defStyleRes: Int = 0, -) : ConstraintLayout(context, attrs, defStyle, defStyleRes), HasDivider { +) : ConstraintLayout(context, attrs, defStyle, defStyleRes), TableItem { enum class FieldStyle { PRIMARY, SECONDARY, LINK, POSITIVE } companion object { + fun createTableCellView(context: Context): TableCellView { return TableCellView(context).apply { layoutParams = LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT) @@ -85,6 +88,8 @@ open class TableCellView @JvmOverloads constructor( private val contentGroup: Group get() = tableCellContent + private var shouldDrawDivider: Boolean = DRAW_DIVIDER_DEFAULT + val imageLoader: ImageLoader by lazy(LazyThreadSafetyMode.NONE) { FeatureUtils.getCommonApi(context).imageLoader() } @@ -98,6 +103,14 @@ open class TableCellView @JvmOverloads constructor( attrs?.let { applyAttributes(it) } } + override fun disableOwnDividers() { + setOwnDividerVisible(false) + } + + override fun shouldDrawDivider(): Boolean { + return shouldDrawDivider + } + fun setTitle(titleRes: Int) { tableCellTitle.setText(titleRes) } @@ -155,8 +168,14 @@ open class TableCellView @JvmOverloads constructor( valueProgress.makeVisible() } - override fun setDividerVisible(visible: Boolean) { - tableCellValueDivider.setVisible(visible) + @Deprecated( + """ + TableCellView's own divider is deprecated and will be removed in the future. + To show dividers between multiple TableCellViews put them into TableView + """ + ) + fun setOwnDividerVisible(visible: Boolean) { + tableCellValueDivider.setVisible(visible && shouldDrawDivider) } fun setPrimaryValueEndIcon(@DrawableRes icon: Int?, @ColorRes tint: Int? = null) { @@ -223,6 +242,10 @@ open class TableCellView @JvmOverloads constructor( constraintSet.applyTo(this) } + fun setShouldDrawDivider(shouldDrawDivider: Boolean) { + this.shouldDrawDivider = shouldDrawDivider + } + private fun applyAttributes(attrs: AttributeSet) = context.useAttributes(attrs, R.styleable.TableCellView) { typedArray -> val titleText = typedArray.getString(R.styleable.TableCellView_title) setTitle(titleText) @@ -231,7 +254,7 @@ open class TableCellView @JvmOverloads constructor( primaryValueText?.let { showValue(it) } val dividerVisible = typedArray.getBoolean(R.styleable.TableCellView_dividerVisible, true) - setDividerVisible(dividerVisible) + setOwnDividerVisible(dividerVisible) val primaryValueEndIcon = typedArray.getResourceIdOrNull(R.styleable.TableCellView_primaryValueEndIcon) primaryValueEndIcon?.let { @@ -275,6 +298,9 @@ open class TableCellView @JvmOverloads constructor( val titleEllipsisable = typedArray.getBoolean(R.styleable.TableCellView_titleEllipsisable, false) setTitleEllipsisable(titleEllipsisable) + + val shouldDrawDivider = typedArray.getBoolean(R.styleable.TableCellView_shouldDrawDivider, DRAW_DIVIDER_DEFAULT) + setShouldDrawDivider(shouldDrawDivider) } } diff --git a/common/src/main/java/io/novafoundation/nova/common/view/TableItem.kt b/common/src/main/java/io/novafoundation/nova/common/view/TableItem.kt new file mode 100644 index 0000000000..b436e3551c --- /dev/null +++ b/common/src/main/java/io/novafoundation/nova/common/view/TableItem.kt @@ -0,0 +1,9 @@ +package io.novafoundation.nova.common.view + +interface TableItem { + + fun disableOwnDividers() + + // TODO this is only needed until TableView has its own divider + fun shouldDrawDivider(): Boolean +} diff --git a/common/src/main/java/io/novafoundation/nova/common/view/TableView.kt b/common/src/main/java/io/novafoundation/nova/common/view/TableView.kt index 540a2d0770..2fd5b41ef2 100644 --- a/common/src/main/java/io/novafoundation/nova/common/view/TableView.kt +++ b/common/src/main/java/io/novafoundation/nova/common/view/TableView.kt @@ -78,13 +78,16 @@ open class TableView @JvmOverloads constructor( setupTableChildrenAppearance() dividerPath.reset() - children.toList() - .filter { it != titleView && it.isVisible } - .withoutLast() - .forEach { - dividerPath.moveTo(childHorizontalPadding, it.bottom.toFloat()) - dividerPath.lineTo(measuredWidth - childHorizontalPadding, it.bottom.toFloat()) + children.forEachIndexed { idx, child -> + val isVisible = child.isVisible + val allowsToDrawDividers = child is TableItem && child.shouldDrawDivider() + val hasNext = idx < childCount - 1 + + if (isVisible && allowsToDrawDividers && hasNext) { + dividerPath.moveTo(childHorizontalPadding, child.bottom.toFloat()) + dividerPath.lineTo(measuredWidth - childHorizontalPadding, child.bottom.toFloat()) } + } } fun setTitle(title: String?) { @@ -113,7 +116,7 @@ open class TableView @JvmOverloads constructor( } private fun setupTableChildrenAppearance() { - val tableChildren = children.filterNot { it == titleView } + val tableChildren = children.filter { it != titleView } .filter { it.isVisible } .toList() @@ -125,8 +128,8 @@ open class TableView @JvmOverloads constructor( } tableChildren.forEach { - if (it is HasDivider) { - it.setDividerVisible(false) + if (it is TableItem) { + it.disableOwnDividers() } it.updatePadding(start = 16.dp, end = 16.dp) diff --git a/common/src/main/res/layout/view_generic_table_cell.xml b/common/src/main/res/layout/view_generic_table_cell.xml index 9d7488b5ca..4b9554260b 100644 --- a/common/src/main/res/layout/view_generic_table_cell.xml +++ b/common/src/main/res/layout/view_generic_table_cell.xml @@ -35,14 +35,4 @@ app:layout_constraintEnd_toEndOf="parent" app:layout_constraintTop_toTopOf="parent" tools:visibility="visible" /> - - \ No newline at end of file diff --git a/common/src/main/res/values/attrs.xml b/common/src/main/res/values/attrs.xml index 9b1346923a..61e744c6cf 100644 --- a/common/src/main/res/values/attrs.xml +++ b/common/src/main/res/values/attrs.xml @@ -106,6 +106,7 @@ + diff --git a/common/src/main/res/values/strings.xml b/common/src/main/res/values/strings.xml index 2e0a85970a..ce8ce09287 100644 --- a/common/src/main/res/values/strings.xml +++ b/common/src/main/res/values/strings.xml @@ -1,5 +1,7 @@ + Total fee + Fee: %s Transfer 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 68115ce069..d4f25010fb 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 @@ -78,6 +78,10 @@ val Fee.decimalAmountByExecutingAccount: BigDecimal BigDecimal.ZERO } +fun FeeBase.addPlanks(extraPlanks: BigInteger): FeeBase { + return SubstrateFeeBase(amount + extraPlanks, asset) +} + fun List.totalAmount(chainAsset: Chain.Asset): BigInteger { return sumOf { it.getAmount(chainAsset) } } diff --git a/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/balance/detail/AssetDetailBalancesView.kt b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/balance/detail/AssetDetailBalancesView.kt index 47736b795f..4775486b34 100644 --- a/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/balance/detail/AssetDetailBalancesView.kt +++ b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/balance/detail/AssetDetailBalancesView.kt @@ -2,8 +2,8 @@ package io.novafoundation.nova.feature_assets.presentation.balance.detail import android.content.Context import android.util.AttributeSet -import io.novafoundation.nova.feature_assets.R import io.novafoundation.nova.common.utils.setDrawableEnd +import io.novafoundation.nova.feature_assets.R import io.novafoundation.nova.feature_wallet_api.presentation.view.BalancesView class AssetDetailBalancesView @JvmOverloads constructor( @@ -17,7 +17,7 @@ class AssetDetailBalancesView @JvmOverloads constructor( val transferable = item(R.string.wallet_balance_transferable) val locked = item(R.string.wallet_balance_locked).apply { - setDividerVisible(false) + setOwnDividerVisible(false) title.setDrawableEnd(R.drawable.ic_info, paddingInDp = 4) } } diff --git a/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/balance/detail/LockedTokensBottomSheet.kt b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/balance/detail/LockedTokensBottomSheet.kt index 8d20eda11e..8a466d6d11 100644 --- a/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/balance/detail/LockedTokensBottomSheet.kt +++ b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/balance/detail/LockedTokensBottomSheet.kt @@ -30,7 +30,7 @@ class LockedTokensBottomSheet( private fun createViewItem(lock: BalanceLocksModel.Lock): TableCellView { return TableCellView.createTableCellView(context).apply { - setDividerVisible(false) + setOwnDividerVisible(false) setTitle(lock.name) showAmount(lock.amount) updateLayoutParams { diff --git a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/staking/main/components/networkInfo/NetworkInfoAdapter.kt b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/staking/main/components/networkInfo/NetworkInfoAdapter.kt index 9e8e5abebb..b969b5b78f 100644 --- a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/staking/main/components/networkInfo/NetworkInfoAdapter.kt +++ b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/staking/main/components/networkInfo/NetworkInfoAdapter.kt @@ -47,7 +47,7 @@ class NetworkInfoHolder(override val containerView: TableCellView) : BaseViewHol .onLoaded { showValue(it.primary, it.secondary) } .onNotLoaded { showProgress() } - setDividerVisible(!isLast) + setOwnDividerVisible(!isLast) } override fun unbind() {} diff --git a/feature-swap-api/src/main/java/io/novafoundation/nova/feature_swap_api/domain/model/AtomicOperationFeeDisplayData.kt b/feature-swap-api/src/main/java/io/novafoundation/nova/feature_swap_api/domain/model/AtomicOperationFeeDisplayData.kt new file mode 100644 index 0000000000..26d21251d3 --- /dev/null +++ b/feature-swap-api/src/main/java/io/novafoundation/nova/feature_swap_api/domain/model/AtomicOperationFeeDisplayData.kt @@ -0,0 +1,30 @@ +package io.novafoundation.nova.feature_swap_api.domain.model + +import io.novafoundation.nova.feature_account_api.data.model.FeeBase +import io.novafoundation.nova.feature_swap_api.domain.model.AtomicOperationFeeDisplayData.SwapFeeComponentDisplay +import io.novafoundation.nova.feature_swap_api.domain.model.AtomicOperationFeeDisplayData.SwapFeeType + +class AtomicOperationFeeDisplayData( + val components: List +) { + + class SwapFeeComponentDisplay( + val fees: List, + val type: SwapFeeType + ) { + + companion object; + } + + enum class SwapFeeType { + NETWORK, CROSS_CHAIN + } +} + +fun SwapFeeComponentDisplay.Companion.network(vararg fee: FeeBase): SwapFeeComponentDisplay { + return SwapFeeComponentDisplay(fee.toList(), SwapFeeType.NETWORK) +} + +fun SwapFeeComponentDisplay.Companion.crossChain(vararg fee: FeeBase): SwapFeeComponentDisplay { + return SwapFeeComponentDisplay(fee.toList(), SwapFeeType.CROSS_CHAIN) +} diff --git a/feature-swap-api/src/main/java/io/novafoundation/nova/feature_swap_api/domain/model/AtomicSwapOperation.kt b/feature-swap-api/src/main/java/io/novafoundation/nova/feature_swap_api/domain/model/AtomicSwapOperation.kt index 41871a377d..017bef6eda 100644 --- a/feature-swap-api/src/main/java/io/novafoundation/nova/feature_swap_api/domain/model/AtomicSwapOperation.kt +++ b/feature-swap-api/src/main/java/io/novafoundation/nova/feature_swap_api/domain/model/AtomicSwapOperation.kt @@ -4,6 +4,7 @@ import io.novafoundation.nova.feature_account_api.data.fee.FeePaymentCurrency import io.novafoundation.nova.feature_account_api.data.model.FeeBase import io.novafoundation.nova.feature_account_api.data.model.totalAmount import io.novafoundation.nova.feature_account_api.data.model.totalPlanksEnsuringAsset +import io.novafoundation.nova.feature_swap_api.domain.model.fee.AtomicSwapOperationFee import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.types.Balance interface AtomicSwapOperation { @@ -37,28 +38,7 @@ class AtomicSwapOperationArgs( val feePaymentCurrency: FeePaymentCurrency, ) -class AtomicSwapOperationFee( - /** - * Fee that is paid when submitting transaction - */ - val submissionFee: SubmissionFeeWithLabel, - - val postSubmissionFees: PostSubmissionFees = PostSubmissionFees(), -) { - - class PostSubmissionFees( - /** - * Post-submission fees paid by (some) origin account. - * This is typed as `SubmissionFee` as those fee might still use different accounts (e.g. delivery fees are always paid from requested account) - */ - val paidByAccount: List = emptyList(), - - /** - * Post-submission fees paid from swapping amount directly. Its payment is isolated and does not involve any withdrawals from accounts - */ - val paidFromAmount: List = emptyList() - ) -} + fun AtomicSwapOperationFee.amountToLeaveOnOriginToPayTxFees(): Balance { val submissionAsset = submissionFee.asset diff --git a/feature-swap-api/src/main/java/io/novafoundation/nova/feature_swap_api/domain/model/SwapFee.kt b/feature-swap-api/src/main/java/io/novafoundation/nova/feature_swap_api/domain/model/SwapFee.kt index 89f1446560..2c5c2a2dbb 100644 --- a/feature-swap-api/src/main/java/io/novafoundation/nova/feature_swap_api/domain/model/SwapFee.kt +++ b/feature-swap-api/src/main/java/io/novafoundation/nova/feature_swap_api/domain/model/SwapFee.kt @@ -6,6 +6,7 @@ import io.novafoundation.nova.feature_account_api.data.model.FeeBase import io.novafoundation.nova.feature_account_api.data.model.SubstrateFeeBase import io.novafoundation.nova.feature_account_api.data.model.getAmount import io.novafoundation.nova.feature_account_api.data.model.totalAmount +import io.novafoundation.nova.feature_swap_api.domain.model.fee.AtomicSwapOperationFee import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.types.Balance import io.novafoundation.nova.feature_wallet_api.presentation.mixin.amountChooser.maxAction.MaxAvailableDeduction import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain diff --git a/feature-swap-api/src/main/java/io/novafoundation/nova/feature_swap_api/domain/model/fee/AtomicSwapOperationFee.kt b/feature-swap-api/src/main/java/io/novafoundation/nova/feature_swap_api/domain/model/fee/AtomicSwapOperationFee.kt new file mode 100644 index 0000000000..cf1db7a84c --- /dev/null +++ b/feature-swap-api/src/main/java/io/novafoundation/nova/feature_swap_api/domain/model/fee/AtomicSwapOperationFee.kt @@ -0,0 +1,30 @@ +package io.novafoundation.nova.feature_swap_api.domain.model.fee + +import io.novafoundation.nova.feature_swap_api.domain.model.AtomicOperationFeeDisplayData +import io.novafoundation.nova.feature_swap_api.domain.model.FeeWithLabel +import io.novafoundation.nova.feature_swap_api.domain.model.SubmissionFeeWithLabel + +interface AtomicSwapOperationFee { + + /** + * Fee that is paid when submitting transaction + */ + val submissionFee: SubmissionFeeWithLabel + + val postSubmissionFees: PostSubmissionFees + + fun constructDisplayData(): AtomicOperationFeeDisplayData + + class PostSubmissionFees( + /** + * Post-submission fees paid by (some) origin account. + * This is typed as `SubmissionFee` as those fee might still use different accounts (e.g. delivery fees are always paid from requested account) + */ + val paidByAccount: List = emptyList(), + + /** + * Post-submission fees paid from swapping amount directly. Its payment is isolated and does not involve any withdrawals from accounts + */ + val paidFromAmount: List = emptyList() + ) +} diff --git a/feature-swap-api/src/main/java/io/novafoundation/nova/feature_swap_api/domain/model/fee/SubmissionOnlyAtomicSwapOperationFee.kt b/feature-swap-api/src/main/java/io/novafoundation/nova/feature_swap_api/domain/model/fee/SubmissionOnlyAtomicSwapOperationFee.kt new file mode 100644 index 0000000000..6dff81b123 --- /dev/null +++ b/feature-swap-api/src/main/java/io/novafoundation/nova/feature_swap_api/domain/model/fee/SubmissionOnlyAtomicSwapOperationFee.kt @@ -0,0 +1,25 @@ +package io.novafoundation.nova.feature_swap_api.domain.model.fee + +import io.novafoundation.nova.feature_account_api.data.model.SubmissionFee +import io.novafoundation.nova.feature_swap_api.domain.model.AtomicOperationFeeDisplayData +import io.novafoundation.nova.feature_swap_api.domain.model.AtomicOperationFeeDisplayData.SwapFeeComponentDisplay +import io.novafoundation.nova.feature_swap_api.domain.model.SubmissionFeeWithLabel +import io.novafoundation.nova.feature_swap_api.domain.model.fee.AtomicSwapOperationFee.PostSubmissionFees + +class SubmissionOnlyAtomicSwapOperationFee(submissionFee: SubmissionFee) : AtomicSwapOperationFee { + + override val submissionFee: SubmissionFeeWithLabel = SubmissionFeeWithLabel(submissionFee) + + override val postSubmissionFees: PostSubmissionFees = PostSubmissionFees() + + override fun constructDisplayData(): AtomicOperationFeeDisplayData { + return AtomicOperationFeeDisplayData( + components = listOf( + SwapFeeComponentDisplay( + type = AtomicOperationFeeDisplayData.SwapFeeType.NETWORK, + fees = listOf(submissionFee) + ) + ), + ) + } +} diff --git a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/assetConversion/AssetConversionExchange.kt b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/assetConversion/AssetConversionExchange.kt index af469564e1..1ae0699778 100644 --- a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/assetConversion/AssetConversionExchange.kt +++ b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/assetConversion/AssetConversionExchange.kt @@ -14,17 +14,17 @@ import io.novafoundation.nova.feature_account_api.domain.model.MetaAccount import io.novafoundation.nova.feature_swap_api.domain.model.AtomicOperationDisplayData import io.novafoundation.nova.feature_swap_api.domain.model.AtomicSwapOperation import io.novafoundation.nova.feature_swap_api.domain.model.AtomicSwapOperationArgs -import io.novafoundation.nova.feature_swap_api.domain.model.AtomicSwapOperationFee import io.novafoundation.nova.feature_swap_api.domain.model.AtomicSwapOperationPrototype import io.novafoundation.nova.feature_swap_api.domain.model.AtomicSwapOperationSubmissionArgs import io.novafoundation.nova.feature_swap_api.domain.model.ReQuoteTrigger -import io.novafoundation.nova.feature_swap_api.domain.model.SubmissionFeeWithLabel import io.novafoundation.nova.feature_swap_api.domain.model.SwapExecutionCorrection import io.novafoundation.nova.feature_swap_api.domain.model.SwapGraphEdge import io.novafoundation.nova.feature_swap_api.domain.model.SwapLimit import io.novafoundation.nova.feature_swap_api.domain.model.UsdConverter import io.novafoundation.nova.feature_swap_api.domain.model.estimatedAmountIn import io.novafoundation.nova.feature_swap_api.domain.model.estimatedAmountOut +import io.novafoundation.nova.feature_swap_api.domain.model.fee.AtomicSwapOperationFee +import io.novafoundation.nova.feature_swap_api.domain.model.fee.SubmissionOnlyAtomicSwapOperationFee import io.novafoundation.nova.feature_swap_core.data.assetExchange.conversion.types.hydra.sources.Weights import io.novafoundation.nova.feature_swap_core_api.data.primitive.errors.SwapQuoteException import io.novafoundation.nova.feature_swap_core_api.data.primitive.model.SwapDirection @@ -241,10 +241,10 @@ private class AssetConversionExchange( override val estimatedSwapLimit: SwapLimit = transactionArgs.estimatedSwapLimit override suspend fun constructDisplayData(): AtomicOperationDisplayData { - return AtomicOperationDisplayData.Swap( - from = fromAsset.fullId.withAmount(estimatedSwapLimit.estimatedAmountIn), - to = toAsset.fullId.withAmount(estimatedSwapLimit.estimatedAmountOut), - ) + return AtomicOperationDisplayData.Swap( + from = fromAsset.fullId.withAmount(estimatedSwapLimit.estimatedAmountIn), + to = toAsset.fullId.withAmount(estimatedSwapLimit.estimatedAmountOut), + ) } override suspend fun estimateFee(): AtomicSwapOperationFee { @@ -258,7 +258,7 @@ private class AssetConversionExchange( executeSwap(swapLimit = estimatedSwapLimit, sendTo = chain.emptyAccountId()) } - return AtomicSwapOperationFee(SubmissionFeeWithLabel(submissionFee)) + return SubmissionOnlyAtomicSwapOperationFee(submissionFee) } override suspend fun requiredAmountInToGetAmountOut(extraOutAmount: Balance): Balance { diff --git a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/crossChain/CrossChainTransferAssetExchange.kt b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/crossChain/CrossChainTransferAssetExchange.kt index e93a9d40bb..63944168d4 100644 --- a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/crossChain/CrossChainTransferAssetExchange.kt +++ b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/crossChain/CrossChainTransferAssetExchange.kt @@ -2,13 +2,16 @@ package io.novafoundation.nova.feature_swap_impl.data.assetExchange.crossChain import io.novafoundation.nova.common.utils.firstNotNull import io.novafoundation.nova.common.utils.graph.Edge +import io.novafoundation.nova.common.utils.orZero +import io.novafoundation.nova.feature_account_api.data.model.addPlanks import io.novafoundation.nova.feature_account_api.domain.interfaces.AccountRepository import io.novafoundation.nova.feature_account_api.domain.model.MetaAccount import io.novafoundation.nova.feature_account_api.domain.model.requireAddressIn import io.novafoundation.nova.feature_swap_api.domain.model.AtomicOperationDisplayData +import io.novafoundation.nova.feature_swap_api.domain.model.AtomicOperationFeeDisplayData +import io.novafoundation.nova.feature_swap_api.domain.model.AtomicOperationFeeDisplayData.SwapFeeComponentDisplay import io.novafoundation.nova.feature_swap_api.domain.model.AtomicSwapOperation import io.novafoundation.nova.feature_swap_api.domain.model.AtomicSwapOperationArgs -import io.novafoundation.nova.feature_swap_api.domain.model.AtomicSwapOperationFee import io.novafoundation.nova.feature_swap_api.domain.model.AtomicSwapOperationPrototype import io.novafoundation.nova.feature_swap_api.domain.model.AtomicSwapOperationSubmissionArgs import io.novafoundation.nova.feature_swap_api.domain.model.FeeWithLabel @@ -18,7 +21,10 @@ import io.novafoundation.nova.feature_swap_api.domain.model.SwapExecutionCorrect import io.novafoundation.nova.feature_swap_api.domain.model.SwapGraphEdge import io.novafoundation.nova.feature_swap_api.domain.model.SwapLimit import io.novafoundation.nova.feature_swap_api.domain.model.UsdConverter +import io.novafoundation.nova.feature_swap_api.domain.model.crossChain import io.novafoundation.nova.feature_swap_api.domain.model.estimatedAmountIn +import io.novafoundation.nova.feature_swap_api.domain.model.fee.AtomicSwapOperationFee +import io.novafoundation.nova.feature_swap_api.domain.model.network import io.novafoundation.nova.feature_swap_core.data.assetExchange.conversion.types.hydra.sources.Weights import io.novafoundation.nova.feature_swap_core_api.data.primitive.model.SwapDirection import io.novafoundation.nova.feature_swap_impl.data.assetExchange.AssetExchange @@ -27,8 +33,10 @@ import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.t import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.types.Balance import io.novafoundation.nova.feature_wallet_api.domain.implementations.availableInDestinations import io.novafoundation.nova.feature_wallet_api.domain.interfaces.CrossChainTransfersUseCase +import io.novafoundation.nova.feature_wallet_api.domain.model.CrossChainTransferFee import io.novafoundation.nova.feature_wallet_api.domain.model.CrossChainTransfersConfiguration import io.novafoundation.nova.runtime.ext.Geneses +import io.novafoundation.nova.runtime.ext.fullId import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry import io.novafoundation.nova.runtime.multiNetwork.asset import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain @@ -49,8 +57,7 @@ class CrossChainTransferAssetExchangeFactory( ) : AssetExchange.MultiChainFactory { override suspend fun create( - swapHost: AssetExchange.SwapHost, - coroutineScope: CoroutineScope + swapHost: AssetExchange.SwapHost, coroutineScope: CoroutineScope ): AssetExchange { return CrossChainTransferAssetExchange( @@ -172,9 +179,7 @@ class CrossChainTransferAssetExchange( override suspend fun constructDisplayData(): AtomicOperationDisplayData { return AtomicOperationDisplayData.Transfer( - from = edge.from, - to = edge.to, - amount = estimatedSwapLimit.estimatedAmountIn + from = edge.from, to = edge.to, amount = estimatedSwapLimit.estimatedAmountIn ) } @@ -185,17 +190,7 @@ class CrossChainTransferAssetExchange( swapHost.extrinsicService().estimateFee(transfer, computationalScope) } - return AtomicSwapOperationFee( - submissionFee = SubmissionFeeWithLabel(crossChainFee.submissionFee), - postSubmissionFees = AtomicSwapOperationFee.PostSubmissionFees( - paidByAccount = listOfNotNull( - SubmissionFeeWithLabel(crossChainFee.deliveryFee, debugLabel = "Delivery"), - ), - paidFromAmount = listOf( - FeeWithLabel(crossChainFee.executionFee, debugLabel = "Execution") - ) - ), - ) + return CrossChainAtomicOperationFee(crossChainFee) } override suspend fun requiredAmountInToGetAmountOut(extraOutAmount: Balance): Balance { @@ -249,4 +244,36 @@ class CrossChainTransferAssetExchange( is SwapLimit.SpecifiedOut -> amountOut } } + + private class CrossChainAtomicOperationFee( + private val crossChainFee: CrossChainTransferFee + ) : AtomicSwapOperationFee { + + override val submissionFee = SubmissionFeeWithLabel(crossChainFee.submissionFee) + + override val postSubmissionFees = AtomicSwapOperationFee.PostSubmissionFees( + paidByAccount = listOfNotNull( + SubmissionFeeWithLabel(crossChainFee.deliveryFee, debugLabel = "Delivery"), + ), paidFromAmount = listOf( + FeeWithLabel(crossChainFee.executionFee, debugLabel = "Execution") + ) + ) + + override fun constructDisplayData(): AtomicOperationFeeDisplayData { + val deliveryFee = crossChainFee.deliveryFee + val shouldSeparateDeliveryFromExecution = deliveryFee != null && deliveryFee.asset.fullId != crossChainFee.executionFee.asset.fullId + + val crossChainFeeComponentDisplay = if (shouldSeparateDeliveryFromExecution) { + SwapFeeComponentDisplay.crossChain(crossChainFee.executionFee, deliveryFee!!) + } else { + val totalCrossChain = crossChainFee.executionFee.addPlanks(deliveryFee?.amount.orZero()) + SwapFeeComponentDisplay.crossChain(totalCrossChain) + } + + val submissionFeeComponent = SwapFeeComponentDisplay.network(crossChainFee.submissionFee) + + val components = listOf(submissionFeeComponent, crossChainFeeComponentDisplay) + return AtomicOperationFeeDisplayData(components) + } + } } diff --git a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/hydraDx/HydraDxAssetExchange.kt b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/hydraDx/HydraDxAssetExchange.kt index 4e507c2cb5..89725724c0 100644 --- a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/hydraDx/HydraDxAssetExchange.kt +++ b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/hydraDx/HydraDxAssetExchange.kt @@ -28,17 +28,17 @@ import io.novafoundation.nova.feature_account_api.domain.model.requireAccountIdI import io.novafoundation.nova.feature_swap_api.domain.model.AtomicOperationDisplayData import io.novafoundation.nova.feature_swap_api.domain.model.AtomicSwapOperation import io.novafoundation.nova.feature_swap_api.domain.model.AtomicSwapOperationArgs -import io.novafoundation.nova.feature_swap_api.domain.model.AtomicSwapOperationFee import io.novafoundation.nova.feature_swap_api.domain.model.AtomicSwapOperationPrototype import io.novafoundation.nova.feature_swap_api.domain.model.AtomicSwapOperationSubmissionArgs import io.novafoundation.nova.feature_swap_api.domain.model.ReQuoteTrigger -import io.novafoundation.nova.feature_swap_api.domain.model.SubmissionFeeWithLabel import io.novafoundation.nova.feature_swap_api.domain.model.SwapExecutionCorrection import io.novafoundation.nova.feature_swap_api.domain.model.SwapGraphEdge import io.novafoundation.nova.feature_swap_api.domain.model.SwapLimit import io.novafoundation.nova.feature_swap_api.domain.model.UsdConverter import io.novafoundation.nova.feature_swap_api.domain.model.estimatedAmountIn import io.novafoundation.nova.feature_swap_api.domain.model.estimatedAmountOut +import io.novafoundation.nova.feature_swap_api.domain.model.fee.AtomicSwapOperationFee +import io.novafoundation.nova.feature_swap_api.domain.model.fee.SubmissionOnlyAtomicSwapOperationFee import io.novafoundation.nova.feature_swap_core.data.assetExchange.conversion.types.hydra.acceptedCurrencies import io.novafoundation.nova.feature_swap_core.data.assetExchange.conversion.types.hydra.accountCurrencyMap import io.novafoundation.nova.feature_swap_core.data.assetExchange.conversion.types.hydra.multiTransactionPayment @@ -303,7 +303,7 @@ private class HydraDxAssetExchange( executeSwap(estimatedSwapLimit) } - return AtomicSwapOperationFee(SubmissionFeeWithLabel(submissionFee)) + return SubmissionOnlyAtomicSwapOperationFee(submissionFee) } override suspend fun requiredAmountInToGetAmountOut(extraOutAmount: Balance): Balance { diff --git a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/di/SwapFeatureComponent.kt b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/di/SwapFeatureComponent.kt index 46d3c49c47..4881f5a81d 100644 --- a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/di/SwapFeatureComponent.kt +++ b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/di/SwapFeatureComponent.kt @@ -11,6 +11,7 @@ import io.novafoundation.nova.feature_swap_api.di.SwapFeatureApi import io.novafoundation.nova.feature_swap_core_api.di.SwapCoreApi import io.novafoundation.nova.feature_swap_impl.presentation.SwapRouter import io.novafoundation.nova.feature_swap_impl.presentation.confirmation.di.SwapConfirmationComponent +import io.novafoundation.nova.feature_swap_impl.presentation.fee.di.SwapFeeComponent import io.novafoundation.nova.feature_swap_impl.presentation.main.di.SwapMainSettingsComponent import io.novafoundation.nova.feature_swap_impl.presentation.options.di.SwapOptionsComponent import io.novafoundation.nova.feature_swap_impl.presentation.route.di.SwapRouteComponent @@ -36,6 +37,8 @@ interface SwapFeatureComponent : SwapFeatureApi { fun swapRoute(): SwapRouteComponent.Factory + fun swapFee(): SwapFeeComponent.Factory + @Component.Factory interface Factory { diff --git a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/domain/interactor/SwapInteractor.kt b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/domain/interactor/SwapInteractor.kt index 2ed49a100e..3ca2f9250a 100644 --- a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/domain/interactor/SwapInteractor.kt +++ b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/domain/interactor/SwapInteractor.kt @@ -40,10 +40,12 @@ import io.novafoundation.nova.feature_wallet_api.domain.interfaces.CrossChainTra import io.novafoundation.nova.feature_wallet_api.domain.interfaces.TokenRepository import io.novafoundation.nova.feature_wallet_api.domain.interfaces.incomingCrossChainDirectionsAvailable import io.novafoundation.nova.feature_wallet_api.domain.model.FiatAmount +import io.novafoundation.nova.feature_wallet_api.domain.model.Token import io.novafoundation.nova.runtime.ext.fullId 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.chain.model.FullChainAssetId import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.Flow @@ -64,6 +66,13 @@ class SwapInteractor( private val swapTransactionHistoryRepository: SwapTransactionHistoryRepository ) { + suspend fun getAllFeeTokens(swapFee: SwapFee): Map { + val basicFees = swapFee.allBasicFees() + val chainAssets = basicFees.map { it.asset } + + return tokenRepository.getTokens(chainAssets) + } + suspend fun calculateSegmentFiatPrices(swapFee: SwapFee): List { return withContext(Dispatchers.Default) { val basicFeesBySegment = swapFee.segments.map { it.fee.allBasicFees() } diff --git a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/SwapRouter.kt b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/SwapRouter.kt index acedf416ce..9291706b94 100644 --- a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/SwapRouter.kt +++ b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/SwapRouter.kt @@ -9,6 +9,8 @@ interface SwapRouter : ReturnableRouter { fun openSwapRoute() + fun openSwapFee() + fun selectAssetIn(selectedAsset: AssetPayload?) fun selectAssetOut(selectedAsset: AssetPayload?) diff --git a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/common/route/SwapRouteTableCellView.kt b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/common/route/SwapRouteTableCellView.kt index d6dd8e9ac3..f163f46629 100644 --- a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/common/route/SwapRouteTableCellView.kt +++ b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/common/route/SwapRouteTableCellView.kt @@ -32,4 +32,13 @@ class SwapRouteTableCellView @JvmOverloads constructor( routeModel?.let(valueView::setModel) } } + + fun setShowChainNames(showChainNames: Boolean) { + valueView.setShowChainNames(showChainNames) + } + + fun setSwapRouteModel(model: SwapRouteModel) { + setVisible(true) + valueView.setModel(model) + } } diff --git a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/common/route/SwapRouteView.kt b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/common/route/SwapRouteView.kt index 02408ad96a..e22ae38ca9 100644 --- a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/common/route/SwapRouteView.kt +++ b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/common/route/SwapRouteView.kt @@ -1,19 +1,26 @@ package io.novafoundation.nova.feature_swap_impl.presentation.common.route import android.content.Context +import android.text.TextUtils import android.util.AttributeSet import android.view.Gravity import android.widget.ImageView import android.widget.LinearLayout +import android.widget.LinearLayout.LayoutParams.WRAP_CONTENT +import android.widget.TextView import androidx.core.view.updateMargins import coil.ImageLoader import io.novafoundation.nova.common.di.FeatureUtils import io.novafoundation.nova.common.utils.dp import io.novafoundation.nova.common.utils.setImageTintRes +import io.novafoundation.nova.common.utils.setTextColorRes +import io.novafoundation.nova.common.utils.useAttributes import io.novafoundation.nova.feature_account_api.presenatation.chain.ChainUi import io.novafoundation.nova.feature_account_api.presenatation.chain.loadChainIcon import io.novafoundation.nova.feature_swap_impl.R +private const val SHOW_CHAIN_NAMES_DEFAULT = false + class SwapRouteView @JvmOverloads constructor( context: Context, attrs: AttributeSet? = null, @@ -24,9 +31,13 @@ class SwapRouteView @JvmOverloads constructor( FeatureUtils.getCommonApi(context).imageLoader() } + private var shouldShowChainNames: Boolean = SHOW_CHAIN_NAMES_DEFAULT + init { orientation = HORIZONTAL gravity = Gravity.CENTER_VERTICAL + + attrs?.let(::applyAttrs) } fun setModel(model: SwapRouteModel) { @@ -35,18 +46,26 @@ class SwapRouteView @JvmOverloads constructor( addViewsFor(model) } + fun setShowChainNames(showChainNames: Boolean) { + shouldShowChainNames = showChainNames + } + private fun addViewsFor(model: SwapRouteModel) { model.chains.forEachIndexed { index, chainUi -> val hasNext = index < model.chains.size - 1 - addChainView(chainUi) + addChainIcon(chainUi) + if (shouldShowChainNames) { + addChainName(chainUi) + } + if (hasNext) { addArrow() } } } - private fun addChainView(chainUi: ChainUi) { + private fun addChainIcon(chainUi: ChainUi) { ImageView(context).apply { layoutParams = LayoutParams(16.dp, 16.dp) @@ -54,6 +73,20 @@ class SwapRouteView @JvmOverloads constructor( }.also(::addView) } + private fun addChainName(chainUi: ChainUi) { + TextView(context).apply { + layoutParams = LayoutParams(WRAP_CONTENT, WRAP_CONTENT).apply { + updateMargins(left = 8.dp) + } + + ellipsize = TextUtils.TruncateAt.END + setTextAppearance(R.style.TextAppearance_NovaFoundation_Regular_Footnote) + setTextColorRes(R.color.text_primary) + + text = chainUi.name + }.also(::addView) + } + private fun addArrow() { ImageView(context).apply { layoutParams = LayoutParams(12.dp, 12.dp).apply { @@ -64,4 +97,9 @@ class SwapRouteView @JvmOverloads constructor( setImageTintRes(R.color.icon_secondary) }.also(::addView) } + + private fun applyAttrs(attributeSet: AttributeSet) = context.useAttributes(attributeSet, R.styleable.SwapRouteView) { + val shouldShowChainNames = it.getBoolean(R.styleable.SwapRouteView_SwapRouteView_displayChainName, SHOW_CHAIN_NAMES_DEFAULT) + setShowChainNames(shouldShowChainNames) + } } diff --git a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/confirmation/SwapConfirmationFragment.kt b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/confirmation/SwapConfirmationFragment.kt index 08dc177def..3853082555 100644 --- a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/confirmation/SwapConfirmationFragment.kt +++ b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/confirmation/SwapConfirmationFragment.kt @@ -51,6 +51,7 @@ class SwapConfirmationFragment : BaseFragment() { swapConfirmationNetworkFee.setOnClickListener { viewModel.networkFeeClicked() } swapConfirmationAccount.setOnClickListener { viewModel.accountClicked() } swapConfirmationButton.setOnClickListener { viewModel.confirmButtonClicked() } + swapConfirmationRoute.setOnClickListener { viewModel.routeClicked() } } override fun inject() { diff --git a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/confirmation/SwapConfirmationViewModel.kt b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/confirmation/SwapConfirmationViewModel.kt index d565a1bae6..cfa695d6a1 100644 --- a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/confirmation/SwapConfirmationViewModel.kt +++ b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/confirmation/SwapConfirmationViewModel.kt @@ -12,9 +12,9 @@ import io.novafoundation.nova.common.utils.Percent import io.novafoundation.nova.common.utils.combineToPair import io.novafoundation.nova.common.utils.flowOf import io.novafoundation.nova.common.utils.formatting.formatPercents +import io.novafoundation.nova.common.utils.singleReplaySharedFlow import io.novafoundation.nova.common.validation.ValidationExecutor import io.novafoundation.nova.common.view.bottomSheet.description.DescriptionBottomSheetLauncher -import io.novafoundation.nova.common.view.bottomSheet.description.launchNetworkFeeDescription import io.novafoundation.nova.feature_account_api.data.mappers.mapChainToUi import io.novafoundation.nova.feature_account_api.domain.interfaces.AccountRepository import io.novafoundation.nova.feature_account_api.domain.model.MetaAccount @@ -43,11 +43,12 @@ import io.novafoundation.nova.feature_swap_impl.presentation.common.PriceImpactF import io.novafoundation.nova.feature_swap_impl.presentation.common.SlippageAlertMixinFactory import io.novafoundation.nova.feature_swap_impl.presentation.common.fee.SwapFeeFormatter import io.novafoundation.nova.feature_swap_impl.presentation.common.fee.SwapFeeInspector +import io.novafoundation.nova.feature_swap_impl.presentation.common.mixin.maxAction.MaxActionProviderFactory import io.novafoundation.nova.feature_swap_impl.presentation.common.route.SwapRouteFormatter +import io.novafoundation.nova.feature_swap_impl.presentation.common.state.SwapState import io.novafoundation.nova.feature_swap_impl.presentation.common.state.SwapStateStoreProvider import io.novafoundation.nova.feature_swap_impl.presentation.common.state.getStateOrThrow import io.novafoundation.nova.feature_swap_impl.presentation.confirmation.model.SwapConfirmationDetailsModel -import io.novafoundation.nova.feature_swap_impl.presentation.common.mixin.maxAction.MaxActionProviderFactory import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.types.Balance import io.novafoundation.nova.feature_wallet_api.domain.ArbitraryAssetUseCase import io.novafoundation.nova.feature_wallet_api.domain.interfaces.TokenRepository @@ -63,7 +64,6 @@ import io.novafoundation.nova.runtime.ext.fullId import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.distinctUntilChanged @@ -116,7 +116,7 @@ class SwapConfirmationViewModel( Validatable by validationExecutor, DescriptionBottomSheetLauncher by descriptionBottomSheetLauncher { - private val confirmationStateFlow = MutableSharedFlow() + private val confirmationStateFlow = singleReplaySharedFlow() private val metaAccountFlow = accountRepository.selectedMetaAccountFlow() .shareInBackground() @@ -202,8 +202,12 @@ class SwapConfirmationViewModel( ) } - fun networkFeeClicked() { - launchNetworkFeeDescription() + fun networkFeeClicked() = setSwapStateAndThen { + swapRouter.openSwapFee() + } + + fun routeClicked() = setSwapStateAfter { + swapRouter.openSwapRoute() } fun accountClicked() { @@ -232,6 +236,36 @@ class SwapConfirmationViewModel( // } } + private fun setSwapStateAndThen(action: () -> Unit) { + launch { + updateSwapStateInStore() + + action() + } + } + + private fun setSwapStateAfter(action: () -> Unit) { + launch { + val store = swapStateStoreProvider.getStore(viewModelScope) + store.resetState() + + action() + + updateSwapStateInStore() + } + } + + private suspend fun updateSwapStateInStore() { + val quotingState = confirmationStateFlow.first() + + val swapState = SwapState( + quote = quotingState.swapQuote, + fee = feeMixin.awaitFee(), + slippage = slippageFlow.first() + ) + swapStateStoreProvider.getStore(viewModelScope).setState(swapState) + } + private fun createMaxActionProvider(): MaxActionProvider { return maxActionProviderFactory.create( assetInFlow = assetInFlow, @@ -242,11 +276,11 @@ class SwapConfirmationViewModel( } private fun executeSwap() = launch { - val fee = feeMixin.awaitFee() - val quote = confirmationStateFlow.first()?.swapQuote ?: return@launch - _submissionInProgress.value = true + val fee = feeMixin.awaitFee() + val quote = confirmationStateFlow.first().swapQuote + swapInteractor.executeSwap(fee) .onEach { progressResult -> when (progressResult) { diff --git a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/fee/SwapFeeFragment.kt b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/fee/SwapFeeFragment.kt new file mode 100644 index 0000000000..cf72dddaa6 --- /dev/null +++ b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/fee/SwapFeeFragment.kt @@ -0,0 +1,116 @@ +package io.novafoundation.nova.feature_swap_impl.presentation.fee + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.LinearLayout.LayoutParams +import android.widget.LinearLayout.LayoutParams.MATCH_PARENT +import android.widget.LinearLayout.LayoutParams.WRAP_CONTENT +import androidx.core.view.updateMargins +import io.novafoundation.nova.common.base.BaseBottomSheetFragment +import io.novafoundation.nova.common.di.FeatureUtils +import io.novafoundation.nova.common.domain.onLoaded +import io.novafoundation.nova.common.view.TableView +import io.novafoundation.nova.feature_swap_api.di.SwapFeatureApi +import io.novafoundation.nova.feature_swap_impl.R +import io.novafoundation.nova.feature_swap_impl.di.SwapFeatureComponent +import io.novafoundation.nova.feature_swap_impl.presentation.common.route.SwapRouteTableCellView +import io.novafoundation.nova.feature_swap_impl.presentation.fee.model.SwapSegmentFeeModel +import io.novafoundation.nova.feature_swap_impl.presentation.fee.model.SwapSegmentFeeModel.FeeOperationModel +import io.novafoundation.nova.feature_swap_impl.presentation.fee.model.SwapSegmentFeeModel.SwapComponentFeeModel +import io.novafoundation.nova.feature_wallet_api.presentation.view.FeeView +import kotlinx.android.synthetic.main.fragment_swap_fee.swapFeeContent +import kotlinx.android.synthetic.main.fragment_swap_fee.swapFeeTotal + +class SwapFeeFragment : BaseBottomSheetFragment() { + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle?, + ): View? { + return inflater.inflate(R.layout.fragment_swap_fee, container, false) + } + + override fun initViews() {} + + override fun inject() { + FeatureUtils.getFeature( + requireContext(), + SwapFeatureApi::class.java + ) + .swapFee() + .create(this) + .inject(this) + } + + override fun subscribe(viewModel: SwapFeeViewModel) { + viewModel.swapFeeSegments.observe { feeState -> + feeState.onLoaded(::showFeeSegments) + } + + viewModel.totalFee.observe(swapFeeTotal::setText) + } + + private fun showFeeSegments(feeSegments: List) { + swapFeeContent.removeAllViews() + + return feeSegments.forEachIndexed { index, swapSegmentFeeModel -> + showFeeSegment( + feeSegment = swapSegmentFeeModel, + isFirst = index == 0, + isLast = index == feeSegments.size - 1 + ) + } + } + + private fun showFeeSegment( + feeSegment: SwapSegmentFeeModel, + isFirst: Boolean, + isLast: Boolean + ) { + val segmentTable = TableView(requireContext()).apply { + layoutParams = LayoutParams(MATCH_PARENT, WRAP_CONTENT).apply { + updateMargins( + top = if (isFirst) 8.dp else 12.dp, + bottom = if (isLast) 8.dp else 0 + ) + } + } + + with(segmentTable) { + addView(createSegmentOperation(feeSegment.operation)) + + feeSegment.feeComponents.forEach { + val componentViews = createFeeComponentViews(it) + componentViews.forEach(::addView) + } + } + + swapFeeContent.addView(segmentTable) + } + + private fun createFeeComponentViews(model: SwapComponentFeeModel): List { + return model.individualFees.mapIndexed { index, feeDisplay -> + val isFirst = index == 0 + val isLast = index == model.individualFees.size - 1 + + val label = model.label.takeIf { isFirst } + + FeeView(requireContext()).apply { + setShouldDrawDivider(isLast) + setTitle(label) + setFeeDisplay(feeDisplay) + } + } + } + + private fun createSegmentOperation(model: FeeOperationModel): View { + return SwapRouteTableCellView(requireContext()).apply { + setShowChainNames(true) + setTitle(model.label) + setSwapRouteModel(model.swapRoute) + } + } +} diff --git a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/fee/SwapFeeViewModel.kt b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/fee/SwapFeeViewModel.kt new file mode 100644 index 0000000000..20f5321c3a --- /dev/null +++ b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/fee/SwapFeeViewModel.kt @@ -0,0 +1,111 @@ +package io.novafoundation.nova.feature_swap_impl.presentation.fee + +import androidx.lifecycle.viewModelScope +import io.novafoundation.nova.common.base.BaseViewModel +import io.novafoundation.nova.common.resources.ResourceManager +import io.novafoundation.nova.common.utils.withSafeLoading +import io.novafoundation.nova.feature_account_api.data.mappers.mapChainToUi +import io.novafoundation.nova.feature_swap_api.domain.model.AtomicOperationDisplayData +import io.novafoundation.nova.feature_swap_api.domain.model.AtomicOperationFeeDisplayData +import io.novafoundation.nova.feature_swap_api.domain.model.AtomicOperationFeeDisplayData.SwapFeeType +import io.novafoundation.nova.feature_swap_api.domain.model.SwapFee +import io.novafoundation.nova.feature_swap_impl.R +import io.novafoundation.nova.feature_swap_impl.domain.interactor.SwapInteractor +import io.novafoundation.nova.feature_swap_impl.presentation.common.route.SwapRouteModel +import io.novafoundation.nova.feature_swap_impl.presentation.common.state.SwapStateStoreProvider +import io.novafoundation.nova.feature_swap_impl.presentation.common.state.stateFlow +import io.novafoundation.nova.feature_swap_impl.presentation.fee.model.SwapSegmentFeeModel +import io.novafoundation.nova.feature_swap_impl.presentation.fee.model.SwapSegmentFeeModel.FeeOperationModel +import io.novafoundation.nova.feature_swap_impl.presentation.fee.model.SwapSegmentFeeModel.SwapComponentFeeModel +import io.novafoundation.nova.feature_wallet_api.domain.model.Token +import io.novafoundation.nova.feature_wallet_api.presentation.formatters.formatAsCurrency +import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.model.toFeeDisplay +import io.novafoundation.nova.feature_wallet_api.presentation.model.mapAmountToAmountModel +import io.novafoundation.nova.runtime.ext.fullId +import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry +import io.novafoundation.nova.runtime.multiNetwork.chain.model.FullChainAssetId +import kotlinx.coroutines.flow.map + +class SwapFeeViewModel( + private val swapInteractor: SwapInteractor, + private val chainRegistry: ChainRegistry, + private val resourceManager: ResourceManager, + swapStateStoreProvider: SwapStateStoreProvider +) : BaseViewModel() { + + private val swapStateFlow = swapStateStoreProvider.stateFlow(viewModelScope) + + val swapFeeSegments = swapStateFlow + .map { it.fee.toSwapFeeSegments() } + .withSafeLoading() + .shareInBackground() + + val totalFee = swapStateFlow.map { + val fee = swapInteractor.calculateTotalFiatPrice(it.fee) + fee.formatAsCurrency() + }.shareInBackground() + + private suspend fun SwapFee.toSwapFeeSegments(): List { + val allTokens = swapInteractor.getAllFeeTokens(this) + + return segments.map { segment -> + val operationData = segment.operation.constructDisplayData() + val feeDisplayData = segment.fee.constructDisplayData() + + SwapSegmentFeeModel( + operation = operationData.toFeeOperationModel(), + feeComponents = feeDisplayData.toFeeComponentModels(allTokens) + ) + } + } + + private fun AtomicOperationFeeDisplayData.toFeeComponentModels( + tokens: Map + ): List { + return components.map { feeDisplaySegment -> + SwapComponentFeeModel( + label = feeDisplaySegment.type.formatLabel(), + individualFees = feeDisplaySegment.fees.map { individualFee -> + val token = tokens.getValue(individualFee.asset.fullId) + mapAmountToAmountModel(individualFee.amount, token).toFeeDisplay() + } + ) + } + } + + private fun SwapFeeType.formatLabel(): String { + return when (this) { + SwapFeeType.NETWORK -> resourceManager.getString(R.string.network_fee) + SwapFeeType.CROSS_CHAIN -> resourceManager.getString(R.string.wallet_send_cross_chain_fee) + } + } + + private suspend fun AtomicOperationDisplayData.toFeeOperationModel(): FeeOperationModel { + return when (this) { + is AtomicOperationDisplayData.Swap -> toFeeOperationModel() + is AtomicOperationDisplayData.Transfer -> toFeeOperationModel() + } + } + + private suspend fun AtomicOperationDisplayData.Swap.toFeeOperationModel(): FeeOperationModel { + val chain = chainRegistry.getChain(from.chainAssetId.chainId) + val chains = listOf(mapChainToUi(chain)) + + return FeeOperationModel( + label = resourceManager.getString(R.string.swap_route_segment_swap_title), + swapRoute = SwapRouteModel(chains) + ) + } + + private suspend fun AtomicOperationDisplayData.Transfer.toFeeOperationModel(): FeeOperationModel { + val chainFrom = chainRegistry.getChain(from.chainId) + val chainTo = chainRegistry.getChain(to.chainId) + + val chains = listOf(mapChainToUi(chainFrom), mapChainToUi(chainTo)) + + return FeeOperationModel( + label = resourceManager.getString(R.string.swap_route_segment_transfer_title), + swapRoute = SwapRouteModel(chains) + ) + } +} diff --git a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/fee/di/SwapFeeComponent.kt b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/fee/di/SwapFeeComponent.kt new file mode 100644 index 0000000000..dfc6134830 --- /dev/null +++ b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/fee/di/SwapFeeComponent.kt @@ -0,0 +1,26 @@ +package io.novafoundation.nova.feature_swap_impl.presentation.fee.di + +import androidx.fragment.app.Fragment +import dagger.BindsInstance +import dagger.Subcomponent +import io.novafoundation.nova.common.di.scope.ScreenScope +import io.novafoundation.nova.feature_swap_impl.presentation.fee.SwapFeeFragment + +@Subcomponent( + modules = [ + SwapFeeModule::class + ] +) +@ScreenScope +interface SwapFeeComponent { + + @Subcomponent.Factory + interface Factory { + + fun create( + @BindsInstance fragment: Fragment, + ): SwapFeeComponent + } + + fun inject(fragment: SwapFeeFragment) +} diff --git a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/fee/di/SwapFeeModule.kt b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/fee/di/SwapFeeModule.kt new file mode 100644 index 0000000000..e9edf49c04 --- /dev/null +++ b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/fee/di/SwapFeeModule.kt @@ -0,0 +1,44 @@ +package io.novafoundation.nova.feature_swap_impl.presentation.fee.di + +import androidx.fragment.app.Fragment +import androidx.lifecycle.ViewModel +import androidx.lifecycle.ViewModelProvider +import dagger.Module +import dagger.Provides +import dagger.multibindings.IntoMap +import io.novafoundation.nova.common.di.viewmodel.ViewModelKey +import io.novafoundation.nova.common.di.viewmodel.ViewModelModule +import io.novafoundation.nova.common.resources.ResourceManager +import io.novafoundation.nova.feature_swap_impl.domain.interactor.SwapInteractor +import io.novafoundation.nova.feature_swap_impl.presentation.common.state.SwapStateStoreProvider +import io.novafoundation.nova.feature_swap_impl.presentation.fee.SwapFeeViewModel +import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry + +@Module(includes = [ViewModelModule::class]) +class SwapFeeModule { + + @Provides + @IntoMap + @ViewModelKey(SwapFeeViewModel::class) + fun provideViewModel( + swapInteractor: SwapInteractor, + chainRegistry: ChainRegistry, + resourceManager: ResourceManager, + swapStateStoreProvider: SwapStateStoreProvider + ): ViewModel { + return SwapFeeViewModel( + swapInteractor = swapInteractor, + chainRegistry = chainRegistry, + resourceManager = resourceManager, + swapStateStoreProvider = swapStateStoreProvider + ) + } + + @Provides + fun provideViewModelCreator( + fragment: Fragment, + viewModelFactory: ViewModelProvider.Factory + ): SwapFeeViewModel { + return ViewModelProvider(fragment, viewModelFactory).get(SwapFeeViewModel::class.java) + } +} diff --git a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/fee/model/SwapSegmentFeeModel.kt b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/fee/model/SwapSegmentFeeModel.kt new file mode 100644 index 0000000000..06f65a7744 --- /dev/null +++ b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/fee/model/SwapSegmentFeeModel.kt @@ -0,0 +1,14 @@ +package io.novafoundation.nova.feature_swap_impl.presentation.fee.model + +import io.novafoundation.nova.feature_swap_impl.presentation.common.route.SwapRouteModel +import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.model.FeeDisplay + +class SwapSegmentFeeModel( + val operation: FeeOperationModel, + val feeComponents: List +) { + + class SwapComponentFeeModel(val label: String, val individualFees: List) + + class FeeOperationModel(val label: String, val swapRoute: SwapRouteModel) +} diff --git a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/main/SwapMainSettingsViewModel.kt b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/main/SwapMainSettingsViewModel.kt index 6d3b18b88c..0177731719 100644 --- a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/main/SwapMainSettingsViewModel.kt +++ b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/main/SwapMainSettingsViewModel.kt @@ -30,7 +30,6 @@ import io.novafoundation.nova.common.validation.CompoundFieldValidator import io.novafoundation.nova.common.validation.FieldValidator import io.novafoundation.nova.common.validation.ValidationExecutor import io.novafoundation.nova.common.view.bottomSheet.description.DescriptionBottomSheetLauncher -import io.novafoundation.nova.common.view.bottomSheet.description.launchNetworkFeeDescription import io.novafoundation.nova.feature_account_api.domain.interfaces.SelectedAccountUseCase import io.novafoundation.nova.feature_account_api.domain.model.addressIn import io.novafoundation.nova.feature_buy_api.presentation.mixin.BuyMixin @@ -333,6 +332,10 @@ class SwapMainSettingsViewModel( swapRouter.openSwapRoute() } + fun networkFeeClicked() = setSwapStateAndThen { + swapRouter.openSwapFee() + } + fun continueButtonClicked() = setSwapStateAndThen { swapRouter.openSwapConfirmation() @@ -354,10 +357,6 @@ class SwapMainSettingsViewModel( launchSwapRateDescription() } - fun networkFeeClicked() { - launchNetworkFeeDescription() - } - fun flipAssets() = launch { val previousSettings = swapSettings.first() val newSettings = swapSettingState().flipAssets() @@ -386,7 +385,7 @@ class SwapMainSettingsViewModel( swapRouter.back() } - private fun setSwapStateAndThen(action: () -> Unit): Unit { + private fun setSwapStateAndThen(action: () -> Unit) { launch { val quotingState = quotingState.value if (quotingState !is QuotingState.Loaded) return@launch diff --git a/feature-swap-impl/src/main/res/layout/fragment_swap_fee.xml b/feature-swap-impl/src/main/res/layout/fragment_swap_fee.xml new file mode 100644 index 0000000000..088f8cf0f9 --- /dev/null +++ b/feature-swap-impl/src/main/res/layout/fragment_swap_fee.xml @@ -0,0 +1,43 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/feature-swap-impl/src/main/res/values/attrs.xml b/feature-swap-impl/src/main/res/values/attrs.xml new file mode 100644 index 0000000000..77678905c7 --- /dev/null +++ b/feature-swap-impl/src/main/res/values/attrs.xml @@ -0,0 +1,10 @@ + + + + + + + + + + \ No newline at end of file diff --git a/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/presentation/view/FeeView.kt b/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/presentation/view/FeeView.kt index 2c0da2c5cf..1d5c22ed4e 100644 --- a/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/presentation/view/FeeView.kt +++ b/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/presentation/view/FeeView.kt @@ -42,6 +42,11 @@ class FeeView @JvmOverloads constructor( } } + fun setFeeDisplay(feeDisplay: FeeDisplay) { + setVisible(true) + showFeeDisplay(feeDisplay) + } + private fun showFeeDisplay(feeDisplay: FeeDisplay) { showValue(feeDisplay.title, feeDisplay.subtitle) } From 8e2f4fcbf5a5bef1efec4668d9d0c8d209e88d04 Mon Sep 17 00:00:00 2001 From: valentun Date: Fri, 8 Nov 2024 17:46:37 +0700 Subject: [PATCH 45/83] Estimate execution time --- .../nova/common/utils/DurationExt.kt | 3 + .../utils/formatting/NumberFormatters.kt | 3 + .../duration/SecondsDurationFormatter.kt | 19 ++++++ .../nova/common/view/TableCellView.kt | 14 +++- common/src/main/res/values/strings.xml | 7 ++ .../model/AtomicSwapOperationPrototype.kt | 3 + .../domain/model/SwapExecutionEstimate.kt | 10 +++ .../domain/model/SwapQuote.kt | 3 +- .../AssetConversionExchange.kt | 6 ++ .../CrossChainTransferAssetExchange.kt | 24 +++++-- .../hydraDx/HydraDxAssetExchange.kt | 12 +++- .../di/exchanges/HydraDxExchangeModule.kt | 7 +- .../domain/swap/RealSwapService.kt | 67 +++++++++++-------- .../confirmation/SwapConfirmationFragment.kt | 2 + .../confirmation/SwapConfirmationViewModel.kt | 19 ++++-- .../model/SwapConfirmationDetailsModel.kt | 1 + .../presentation/main/QuotingState.kt | 12 +++- .../main/SwapMainSettingsFragment.kt | 2 + .../main/SwapMainSettingsViewModel.kt | 17 +++-- .../layout/fragment_main_swap_settings.xml | 6 ++ .../fragment_swap_confirmation_settings.xml | 6 ++ .../assets/tranfers/AssetTransfers.kt | 14 ++++ .../crosschain/CrossChainTransactor.kt | 3 + .../interfaces/CrossChainTransfersUseCase.kt | 7 ++ .../crosschain/RealCrossChainTransactor.kt | 38 ++++++++++- .../di/WalletFeatureDependencies.kt | 3 + .../di/WalletFeatureModule.kt | 9 ++- .../domain/RealCrossChainTransfersUseCase.kt | 27 ++++++-- .../runtime/multiNetwork/ChainRegistry.kt | 5 ++ .../repository/ChainStateRepository.kt | 19 +++++- 30 files changed, 305 insertions(+), 63 deletions(-) create mode 100644 common/src/main/java/io/novafoundation/nova/common/utils/formatting/duration/SecondsDurationFormatter.kt create mode 100644 feature-swap-api/src/main/java/io/novafoundation/nova/feature_swap_api/domain/model/SwapExecutionEstimate.kt diff --git a/common/src/main/java/io/novafoundation/nova/common/utils/DurationExt.kt b/common/src/main/java/io/novafoundation/nova/common/utils/DurationExt.kt index b5ad2e2658..2257ea7ccd 100644 --- a/common/src/main/java/io/novafoundation/nova/common/utils/DurationExt.kt +++ b/common/src/main/java/io/novafoundation/nova/common/utils/DurationExt.kt @@ -10,3 +10,6 @@ val Duration.lastHours: Int val Duration.lastMinutes: Int get() = this.toComponents { _, _, minutes, _, _ -> minutes } + +val Duration.lastSeconds: Int + get() = this.toComponents { _, _, _, seconds, _ -> seconds } diff --git a/common/src/main/java/io/novafoundation/nova/common/utils/formatting/NumberFormatters.kt b/common/src/main/java/io/novafoundation/nova/common/utils/formatting/NumberFormatters.kt index 54bb1e0e5b..eee71d3269 100644 --- a/common/src/main/java/io/novafoundation/nova/common/utils/formatting/NumberFormatters.kt +++ b/common/src/main/java/io/novafoundation/nova/common/utils/formatting/NumberFormatters.kt @@ -16,6 +16,7 @@ import io.novafoundation.nova.common.utils.formatting.duration.DurationFormatter import io.novafoundation.nova.common.utils.formatting.duration.HoursDurationFormatter import io.novafoundation.nova.common.utils.formatting.duration.MinutesDurationFormatter import io.novafoundation.nova.common.utils.formatting.duration.RoundMinutesDurationFormatter +import io.novafoundation.nova.common.utils.formatting.duration.SecondsDurationFormatter import io.novafoundation.nova.common.utils.formatting.duration.ZeroDurationFormatter import io.novafoundation.nova.common.utils.fractionToPercentage import io.novafoundation.nova.common.utils.isNonNegative @@ -260,12 +261,14 @@ fun baseDurationFormatter( ), hoursDurationFormatter: BoundedDurationFormatter = HoursDurationFormatter(context), minutesDurationFormatter: BoundedDurationFormatter = MinutesDurationFormatter(context), + secondsDurationFormatter: BoundedDurationFormatter = SecondsDurationFormatter(context), zeroDurationFormatter: BoundedDurationFormatter = ZeroDurationFormatter(DayDurationFormatter(context)) ): DurationFormatter { val compoundFormatter = CompoundDurationFormatter( dayDurationFormatter, hoursDurationFormatter, minutesDurationFormatter, + secondsDurationFormatter, zeroDurationFormatter ) diff --git a/common/src/main/java/io/novafoundation/nova/common/utils/formatting/duration/SecondsDurationFormatter.kt b/common/src/main/java/io/novafoundation/nova/common/utils/formatting/duration/SecondsDurationFormatter.kt new file mode 100644 index 0000000000..0a0a2ae346 --- /dev/null +++ b/common/src/main/java/io/novafoundation/nova/common/utils/formatting/duration/SecondsDurationFormatter.kt @@ -0,0 +1,19 @@ +package io.novafoundation.nova.common.utils.formatting.duration + +import android.content.Context +import io.novafoundation.nova.common.R +import io.novafoundation.nova.common.utils.lastSeconds +import kotlin.time.Duration +import kotlin.time.Duration.Companion.seconds + +class SecondsDurationFormatter( + private val context: Context +) : BoundedDurationFormatter { + + override val threshold: Duration = 1.seconds + + override fun format(duration: Duration): String { + val seconds = duration.lastSeconds + return context.resources.getQuantityString(R.plurals.common_seconds_format, seconds, seconds) + } +} diff --git a/common/src/main/java/io/novafoundation/nova/common/view/TableCellView.kt b/common/src/main/java/io/novafoundation/nova/common/view/TableCellView.kt index 8a18017ed5..cc3f8b0451 100644 --- a/common/src/main/java/io/novafoundation/nova/common/view/TableCellView.kt +++ b/common/src/main/java/io/novafoundation/nova/common/view/TableCellView.kt @@ -164,6 +164,8 @@ open class TableCellView @JvmOverloads constructor( } fun showProgress() { + makeVisible() + contentGroup.makeGone() valueProgress.makeVisible() } @@ -323,14 +325,20 @@ fun TableCellView.setExtraInfoAvailable(available: Boolean) { } } -fun TableCellView.showLoadingState(state: ExtendedLoadingState, showData: (T) -> Unit) { +fun TableCellView.showLoadingState(state: ExtendedLoadingState, showData: (T) -> Unit) { when (state) { is ExtendedLoadingState.Error -> showValue(context.getString(R.string.common_error_general_title)) - is ExtendedLoadingState.Loaded -> showData(state.data) + + is ExtendedLoadingState.Loaded -> if (state.data != null) { + showData(state.data) + } else { + makeGone() + } + ExtendedLoadingState.Loading -> showProgress() } } -fun TableCellView.showLoadingValue(state: ExtendedLoadingState) { +fun TableCellView.showLoadingValue(state: ExtendedLoadingState) { showLoadingState(state, ::showValue) } diff --git a/common/src/main/res/values/strings.xml b/common/src/main/res/values/strings.xml index ce8ce09287..b917884d1d 100644 --- a/common/src/main/res/values/strings.xml +++ b/common/src/main/res/values/strings.xml @@ -1,5 +1,12 @@ + + %d second + %d seconds + + + Execution time + Total fee Fee: %s diff --git a/feature-swap-api/src/main/java/io/novafoundation/nova/feature_swap_api/domain/model/AtomicSwapOperationPrototype.kt b/feature-swap-api/src/main/java/io/novafoundation/nova/feature_swap_api/domain/model/AtomicSwapOperationPrototype.kt index f3444f41f5..8594544c62 100644 --- a/feature-swap-api/src/main/java/io/novafoundation/nova/feature_swap_api/domain/model/AtomicSwapOperationPrototype.kt +++ b/feature-swap-api/src/main/java/io/novafoundation/nova/feature_swap_api/domain/model/AtomicSwapOperationPrototype.kt @@ -2,6 +2,7 @@ package io.novafoundation.nova.feature_swap_api.domain.model import io.novafoundation.nova.runtime.multiNetwork.chain.model.ChainId import java.math.BigDecimal +import kotlin.time.Duration interface AtomicSwapOperationPrototype { @@ -12,6 +13,8 @@ interface AtomicSwapOperationPrototype { * Implementations should favour speed instead of precision as this is called for each quoting action */ suspend fun roughlyEstimateNativeFee(usdConverter: UsdConverter): BigDecimal + + suspend fun maximumExecutionTime(): Duration } interface UsdConverter { diff --git a/feature-swap-api/src/main/java/io/novafoundation/nova/feature_swap_api/domain/model/SwapExecutionEstimate.kt b/feature-swap-api/src/main/java/io/novafoundation/nova/feature_swap_api/domain/model/SwapExecutionEstimate.kt new file mode 100644 index 0000000000..8c81a92afa --- /dev/null +++ b/feature-swap-api/src/main/java/io/novafoundation/nova/feature_swap_api/domain/model/SwapExecutionEstimate.kt @@ -0,0 +1,10 @@ +package io.novafoundation.nova.feature_swap_api.domain.model + +import kotlin.time.Duration + +@JvmInline +value class SwapExecutionEstimate(val atomicOperationsEstimates: List) + +fun SwapExecutionEstimate.totalTime(): Duration { + return atomicOperationsEstimates.reduce { acc, next -> acc + next } +} diff --git a/feature-swap-api/src/main/java/io/novafoundation/nova/feature_swap_api/domain/model/SwapQuote.kt b/feature-swap-api/src/main/java/io/novafoundation/nova/feature_swap_api/domain/model/SwapQuote.kt index 8da0885905..6d840dbc14 100644 --- a/feature-swap-api/src/main/java/io/novafoundation/nova/feature_swap_api/domain/model/SwapQuote.kt +++ b/feature-swap-api/src/main/java/io/novafoundation/nova/feature_swap_api/domain/model/SwapQuote.kt @@ -13,7 +13,8 @@ data class SwapQuote( val amountIn: ChainAssetWithAmount, val amountOut: ChainAssetWithAmount, val priceImpact: Percent, - val quotedPath: QuotedPath + val quotedPath: QuotedPath, + val executionEstimate: SwapExecutionEstimate ) { val assetIn: Chain.Asset diff --git a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/assetConversion/AssetConversionExchange.kt b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/assetConversion/AssetConversionExchange.kt index 1ae0699778..4656c2aced 100644 --- a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/assetConversion/AssetConversionExchange.kt +++ b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/assetConversion/AssetConversionExchange.kt @@ -47,6 +47,7 @@ import io.novafoundation.nova.runtime.multiNetwork.multiLocation.converter.toMul import io.novafoundation.nova.runtime.multiNetwork.multiLocation.toEncodableInstance import io.novafoundation.nova.runtime.multiNetwork.runtime.repository.findEventOrThrow import io.novafoundation.nova.runtime.repository.ChainStateRepository +import io.novafoundation.nova.runtime.repository.expectedBlockTime import io.novafoundation.nova.runtime.storage.source.StorageDataSource import io.novafoundation.nova.runtime.storage.source.query.metadata import io.novasama.substrate_sdk_android.runtime.AccountId @@ -60,6 +61,7 @@ import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.drop import kotlinx.coroutines.flow.map import java.math.BigDecimal +import kotlin.time.Duration class AssetConversionExchangeFactory( private val multiLocationConverterFactory: MultiLocationConverterFactory, @@ -230,6 +232,10 @@ private class AssetConversionExchange( // in DOT return 0.0015.toBigDecimal() } + + override suspend fun maximumExecutionTime(): Duration { + return chainStateRepository.expectedBlockTime(chain.id) + } } inner class AssetConversionOperation( diff --git a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/crossChain/CrossChainTransferAssetExchange.kt b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/crossChain/CrossChainTransferAssetExchange.kt index 63944168d4..a88e1bf50a 100644 --- a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/crossChain/CrossChainTransferAssetExchange.kt +++ b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/crossChain/CrossChainTransferAssetExchange.kt @@ -30,6 +30,7 @@ import io.novafoundation.nova.feature_swap_core_api.data.primitive.model.SwapDir import io.novafoundation.nova.feature_swap_impl.data.assetExchange.AssetExchange import io.novafoundation.nova.feature_swap_impl.data.assetExchange.FeePaymentProviderOverride import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.tranfers.AssetTransferBase +import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.tranfers.AssetTransferDirection import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.types.Balance import io.novafoundation.nova.feature_wallet_api.domain.implementations.availableInDestinations import io.novafoundation.nova.feature_wallet_api.domain.interfaces.CrossChainTransfersUseCase @@ -49,6 +50,7 @@ import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.emptyFlow import java.math.BigDecimal import java.math.BigInteger +import kotlin.time.Duration class CrossChainTransferAssetExchangeFactory( private val crossChainTransfersUseCase: CrossChainTransfersUseCase, @@ -115,7 +117,7 @@ class CrossChainTransferAssetExchange( } override suspend fun beginOperationPrototype(): AtomicSwapOperationPrototype { - return CrossChainTransferOperationPrototype(from.chainId, to.chainId) + return CrossChainTransferOperationPrototype(this) } override suspend fun appendToOperationPrototype(currentTransaction: AtomicSwapOperationPrototype): AtomicSwapOperationPrototype? { @@ -143,10 +145,13 @@ class CrossChainTransferAssetExchange( } inner class CrossChainTransferOperationPrototype( - override val fromChain: ChainId, - private val toChain: ChainId, + private val edge: Edge, ) : AtomicSwapOperationPrototype { + override val fromChain: ChainId = edge.from.chainId + + private val toChain: ChainId = edge.to.chainId + override suspend fun roughlyEstimateNativeFee(usdConverter: UsdConverter): BigDecimal { var totalAmount = BigDecimal.ZERO @@ -165,6 +170,15 @@ class CrossChainTransferAssetExchange( return totalAmount } + override suspend fun maximumExecutionTime(): Duration { + val (fromChain, fromAsset) = chainRegistry.chainWithAsset(edge.from) + val (toChain, toAsset) = chainRegistry.chainWithAsset(edge.to) + + val transferDirection = AssetTransferDirection(fromChain, fromAsset, toChain, toAsset) + + return crossChainTransfersUseCase.maximumExecutionTime(transferDirection, computationalScope) + } + private fun isChainWithExpensiveCrossChain(chainId: ChainId): Boolean { return (chainId == Chain.Geneses.POLKADOT) or (chainId == Chain.Geneses.POLKADOT_ASSET_HUB) } @@ -179,7 +193,9 @@ class CrossChainTransferAssetExchange( override suspend fun constructDisplayData(): AtomicOperationDisplayData { return AtomicOperationDisplayData.Transfer( - from = edge.from, to = edge.to, amount = estimatedSwapLimit.estimatedAmountIn + from = edge.from, + to = edge.to, + amount = estimatedSwapLimit.estimatedAmountIn ) } diff --git a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/hydraDx/HydraDxAssetExchange.kt b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/hydraDx/HydraDxAssetExchange.kt index 89725724c0..c63e2777cb 100644 --- a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/hydraDx/HydraDxAssetExchange.kt +++ b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/hydraDx/HydraDxAssetExchange.kt @@ -66,6 +66,8 @@ import io.novafoundation.nova.runtime.multiNetwork.chain.model.ChainId import io.novafoundation.nova.runtime.multiNetwork.chain.model.FullChainAssetId import io.novafoundation.nova.runtime.multiNetwork.runtime.repository.findEvent import io.novafoundation.nova.runtime.multiNetwork.runtime.repository.findEventOrThrow +import io.novafoundation.nova.runtime.repository.ChainStateRepository +import io.novafoundation.nova.runtime.repository.expectedBlockTime import io.novafoundation.nova.runtime.storage.source.StorageDataSource import io.novafoundation.nova.runtime.storage.source.query.metadata import io.novasama.substrate_sdk_android.runtime.AccountId @@ -82,6 +84,7 @@ import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onEach import java.math.BigDecimal import java.math.BigInteger +import kotlin.time.Duration class HydraDxExchangeFactory( @@ -91,7 +94,8 @@ class HydraDxExchangeFactory( private val hydraDxNovaReferral: HydraDxNovaReferral, private val swapSourceFactories: Iterable>, private val quotingFactory: HydraDxQuoting.Factory, - private val hydrationFeeInjector: HydrationFeeInjector + private val hydrationFeeInjector: HydrationFeeInjector, + private val chainStateRepository: ChainStateRepository ) : AssetExchange.SingleChainFactory { override suspend fun create(chain: Chain, swapHost: AssetExchange.SwapHost, coroutineScope: CoroutineScope): AssetExchange { @@ -105,6 +109,7 @@ class HydraDxExchangeFactory( swapHost = swapHost, hydrationFeeInjector = hydrationFeeInjector, delegate = quotingFactory.create(chain), + chainStateRepository = chainStateRepository ) } } @@ -122,6 +127,7 @@ private class HydraDxAssetExchange( private val swapSourceFactories: Iterable>, private val swapHost: AssetExchange.SwapHost, private val hydrationFeeInjector: HydrationFeeInjector, + private val chainStateRepository: ChainStateRepository ) : AssetExchange { private val swapSources: List = createSources() @@ -266,6 +272,10 @@ private class HydraDxAssetExchange( // in HDX return 0.5.toBigDecimal() } + + override suspend fun maximumExecutionTime(): Duration { + return chainStateRepository.expectedBlockTime(chain.id) + } } inner class HydraDxOperation private constructor( diff --git a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/di/exchanges/HydraDxExchangeModule.kt b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/di/exchanges/HydraDxExchangeModule.kt index 8123eb1287..c13a873be4 100644 --- a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/di/exchanges/HydraDxExchangeModule.kt +++ b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/di/exchanges/HydraDxExchangeModule.kt @@ -16,6 +16,7 @@ import io.novafoundation.nova.feature_swap_impl.data.assetExchange.hydraDx.stabl import io.novafoundation.nova.feature_swap_impl.data.assetExchange.hydraDx.xyk.XYKSwapSourceFactory import io.novafoundation.nova.runtime.di.REMOTE_STORAGE_SOURCE import io.novafoundation.nova.runtime.ethereum.StorageSharedRequestsBuilderFactory +import io.novafoundation.nova.runtime.repository.ChainStateRepository import io.novafoundation.nova.runtime.storage.source.StorageDataSource import javax.inject.Named @@ -57,7 +58,8 @@ class HydraDxExchangeModule { hydraDxNovaReferral: HydraDxNovaReferral, swapSourceFactories: Set<@JvmSuppressWildcards HydraDxSwapSource.Factory<*>>, quotingFactory: HydraDxQuoting.Factory, - hydrationFeeInjector: HydrationFeeInjector + hydrationFeeInjector: HydrationFeeInjector, + chainStateRepository: ChainStateRepository ): HydraDxExchangeFactory { return HydraDxExchangeFactory( remoteStorageSource = remoteStorageSource, @@ -66,7 +68,8 @@ class HydraDxExchangeModule { hydraDxNovaReferral = hydraDxNovaReferral, swapSourceFactories = swapSourceFactories, quotingFactory = quotingFactory, - hydrationFeeInjector = hydrationFeeInjector + hydrationFeeInjector = hydrationFeeInjector, + chainStateRepository = chainStateRepository ) } } diff --git a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/domain/swap/RealSwapService.kt b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/domain/swap/RealSwapService.kt index 1954326c3f..3f452c703a 100644 --- a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/domain/swap/RealSwapService.kt +++ b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/domain/swap/RealSwapService.kt @@ -40,6 +40,7 @@ import io.novafoundation.nova.feature_swap_api.domain.model.AtomicSwapOperationS import io.novafoundation.nova.feature_swap_api.domain.model.ReQuoteTrigger import io.novafoundation.nova.feature_swap_api.domain.model.SlippageConfig import io.novafoundation.nova.feature_swap_api.domain.model.SwapExecutionCorrection +import io.novafoundation.nova.feature_swap_api.domain.model.SwapExecutionEstimate import io.novafoundation.nova.feature_swap_api.domain.model.SwapFee import io.novafoundation.nova.feature_swap_api.domain.model.SwapFeeArgs import io.novafoundation.nova.feature_swap_api.domain.model.SwapGraph @@ -108,6 +109,7 @@ import kotlinx.coroutines.withContext import java.math.BigDecimal import java.math.BigInteger import java.math.MathContext +import kotlin.time.Duration import kotlin.time.Duration.Companion.milliseconds private const val ALL_DIRECTIONS_CACHE = "RealSwapService.ALL_DIRECTIONS" @@ -303,6 +305,33 @@ internal class RealSwapService( return finishedSwapTxs } + private suspend fun Path>.constructAtomicOperationPrototypes(): List { + var currentSwapTx: AtomicSwapOperationPrototype? = null + val finishedSwapTxs = mutableListOf() + + forEach { quotedEdge -> + // Initial case - begin first operation + if (currentSwapTx == null) { + currentSwapTx = quotedEdge.edge.beginOperationPrototype() + return@forEach + } + + // Try to append segment to current swap tx + val maybeAppendedCurrentTx = quotedEdge.edge.appendToOperationPrototype(currentSwapTx!!) + + currentSwapTx = if (maybeAppendedCurrentTx == null) { + finishedSwapTxs.add(currentSwapTx!!) + quotedEdge.edge.beginOperationPrototype() + } else { + maybeAppendedCurrentTx + } + } + + finishedSwapTxs.add(currentSwapTx!!) + + return finishedSwapTxs + } + private suspend fun SwapGraphEdge.identifySegmentCurrency( isFirstSegment: Boolean, firstSegmentFees: FeePaymentCurrency @@ -331,14 +360,22 @@ internal class RealSwapService( val amountIn = quotedTrade.amountIn() val amountOut = quotedTrade.amountOut() + val atomicOperationsEstimates = quotedTrade.estimateOperationsMaximumExecutionTime() + return SwapQuote( amountIn = args.tokenIn.configuration.withAmount(amountIn), amountOut = args.tokenOut.configuration.withAmount(amountOut), priceImpact = args.calculatePriceImpact(amountIn, amountOut), - quotedPath = quotedTrade + quotedPath = quotedTrade, + executionEstimate = SwapExecutionEstimate(atomicOperationsEstimates) ) } + private suspend fun QuotedTrade.estimateOperationsMaximumExecutionTime(): List { + return path.constructAtomicOperationPrototypes() + .map { it.maximumExecutionTime() } + } + override suspend fun defaultSlippageConfig(chainId: ChainId): SlippageConfig { return SlippageConfig.default() } @@ -575,33 +612,6 @@ internal class RealSwapService( } } - private suspend fun Path>.constructAtomicOperationPrototypes(): List { - var currentSwapTx: AtomicSwapOperationPrototype? = null - val finishedSwapTxs = mutableListOf() - - forEach { quotedEdge -> - // Initial case - begin first operation - if (currentSwapTx == null) { - currentSwapTx = quotedEdge.edge.beginOperationPrototype() - return@forEach - } - - // Try to append segment to current swap tx - val maybeAppendedCurrentTx = quotedEdge.edge.appendToOperationPrototype(currentSwapTx!!) - - currentSwapTx = if (maybeAppendedCurrentTx == null) { - finishedSwapTxs.add(currentSwapTx!!) - quotedEdge.edge.beginOperationPrototype() - } else { - maybeAppendedCurrentTx - } - } - - finishedSwapTxs.add(currentSwapTx!!) - - return finishedSwapTxs - } - private inner class PriceBasedUsdConverter( private val prices: Map, private val nativeAsset: FullChainAssetId, @@ -626,6 +636,7 @@ internal class RealSwapService( } } + private inner class InnerSwapHost( private val computationScope: CoroutineScope ) : AssetExchange.SwapHost { diff --git a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/confirmation/SwapConfirmationFragment.kt b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/confirmation/SwapConfirmationFragment.kt index 3853082555..8af5bbe5da 100644 --- a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/confirmation/SwapConfirmationFragment.kt +++ b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/confirmation/SwapConfirmationFragment.kt @@ -23,6 +23,7 @@ import kotlinx.android.synthetic.main.fragment_swap_confirmation_settings.swapCo import kotlinx.android.synthetic.main.fragment_swap_confirmation_settings.swapConfirmationAlert import kotlinx.android.synthetic.main.fragment_swap_confirmation_settings.swapConfirmationAssets import kotlinx.android.synthetic.main.fragment_swap_confirmation_settings.swapConfirmationButton +import kotlinx.android.synthetic.main.fragment_swap_confirmation_settings.swapConfirmationExecutionTime import kotlinx.android.synthetic.main.fragment_swap_confirmation_settings.swapConfirmationNetworkFee import kotlinx.android.synthetic.main.fragment_swap_confirmation_settings.swapConfirmationPriceDifference import kotlinx.android.synthetic.main.fragment_swap_confirmation_settings.swapConfirmationRate @@ -77,6 +78,7 @@ class SwapConfirmationFragment : BaseFragment() { swapConfirmationPriceDifference.showValueOrHide(it.priceDifference) swapConfirmationSlippage.showValue(it.slippage) swapConfirmationRoute.setSwapRouteState(it.swapRouteState) + swapConfirmationExecutionTime.showValue(it.estimatedExecutionTime) } viewModel.wallet.observe { swapConfirmationWallet.showWallet(it) } diff --git a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/confirmation/SwapConfirmationViewModel.kt b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/confirmation/SwapConfirmationViewModel.kt index cfa695d6a1..bf784bc3db 100644 --- a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/confirmation/SwapConfirmationViewModel.kt +++ b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/confirmation/SwapConfirmationViewModel.kt @@ -31,6 +31,7 @@ import io.novafoundation.nova.feature_swap_api.domain.model.SwapQuoteArgs import io.novafoundation.nova.feature_swap_api.domain.model.editedBalance import io.novafoundation.nova.feature_swap_api.domain.model.swapRate import io.novafoundation.nova.feature_swap_api.domain.model.toExecuteArgs +import io.novafoundation.nova.feature_swap_api.domain.model.totalTime import io.novafoundation.nova.feature_swap_api.presentation.formatters.SwapRateFormatter import io.novafoundation.nova.feature_swap_api.presentation.view.SwapAssetView import io.novafoundation.nova.feature_swap_api.presentation.view.SwapAssetsView @@ -300,19 +301,23 @@ class SwapConfirmationViewModel( private suspend fun formatToSwapDetailsModel(confirmationState: SwapConfirmationState): SwapConfirmationDetailsModel { val metaAccount = accountRepository.getSelectedMetaAccount() - val assetIn = confirmationState.swapQuote.assetIn - val assetOut = confirmationState.swapQuote.assetOut + val quote = confirmationState.swapQuote + + val assetIn = quote.assetIn + val assetOut = quote.assetOut val chainIn = chainRegistry.getChain(assetIn.chainId) val chainOut = chainRegistry.getChain(assetOut.chainId) + return SwapConfirmationDetailsModel( assets = SwapAssetsView.Model( - assetIn = formatAssetDetails(metaAccount, chainIn, assetIn, confirmationState.swapQuote.planksIn), - assetOut = formatAssetDetails(metaAccount, chainOut, assetOut, confirmationState.swapQuote.planksOut) + assetIn = formatAssetDetails(metaAccount, chainIn, assetIn, quote.planksIn), + assetOut = formatAssetDetails(metaAccount, chainOut, assetOut, quote.planksOut) ), - rate = formatRate(confirmationState.swapQuote.swapRate(), assetIn, assetOut), - priceDifference = formatPriceDifference(confirmationState.swapQuote.priceImpact), + rate = formatRate(quote.swapRate(), assetIn, assetOut), + priceDifference = formatPriceDifference(quote.priceImpact), slippage = slippageFlow.first().formatPercents(), - swapRouteState = ExtendedLoadingState.Loaded(swapRouteFormatter.formatSwapRoute(confirmationState.swapQuote)) + swapRouteState = ExtendedLoadingState.Loaded(swapRouteFormatter.formatSwapRoute(quote)), + estimatedExecutionTime = resourceManager.formatDuration(quote.executionEstimate.totalTime(), estimated = true) ) } diff --git a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/confirmation/model/SwapConfirmationDetailsModel.kt b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/confirmation/model/SwapConfirmationDetailsModel.kt index b3ff3b4de8..d0f1b6b494 100644 --- a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/confirmation/model/SwapConfirmationDetailsModel.kt +++ b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/confirmation/model/SwapConfirmationDetailsModel.kt @@ -9,4 +9,5 @@ class SwapConfirmationDetailsModel( val priceDifference: CharSequence?, val slippage: String, val swapRouteState: SwapRouteState, + val estimatedExecutionTime: String, ) diff --git a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/main/QuotingState.kt b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/main/QuotingState.kt index 298837bbcf..7ecfe02ce9 100644 --- a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/main/QuotingState.kt +++ b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/main/QuotingState.kt @@ -1,5 +1,6 @@ package io.novafoundation.nova.feature_swap_impl.presentation.main +import io.novafoundation.nova.common.domain.ExtendedLoadingState import io.novafoundation.nova.feature_swap_api.domain.model.SwapQuote import io.novafoundation.nova.feature_swap_api.domain.model.SwapQuoteArgs @@ -9,7 +10,16 @@ sealed class QuotingState { object Loading : QuotingState() - data class Error(val error: Throwable): QuotingState() + data class Error(val error: Throwable) : QuotingState() data class Loaded(val quote: SwapQuote, val quoteArgs: SwapQuoteArgs) : QuotingState() } + +inline fun QuotingState.toLoadingState(onLoaded: (SwapQuote) -> T?): ExtendedLoadingState { + return when (this) { + QuotingState.Default -> ExtendedLoadingState.Loaded(null) + is QuotingState.Error -> ExtendedLoadingState.Error(error) + is QuotingState.Loaded -> ExtendedLoadingState.Loaded(onLoaded(quote)) + QuotingState.Loading -> ExtendedLoadingState.Loading + } +} diff --git a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/main/SwapMainSettingsFragment.kt b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/main/SwapMainSettingsFragment.kt index 8a11e7b187..10324d4c92 100644 --- a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/main/SwapMainSettingsFragment.kt +++ b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/main/SwapMainSettingsFragment.kt @@ -29,6 +29,7 @@ import kotlinx.android.synthetic.main.fragment_main_swap_settings.swapMainSettin import kotlinx.android.synthetic.main.fragment_main_swap_settings.swapMainSettingsDetails import kotlinx.android.synthetic.main.fragment_main_swap_settings.swapMainSettingsDetailsNetworkFee import kotlinx.android.synthetic.main.fragment_main_swap_settings.swapMainSettingsDetailsRate +import kotlinx.android.synthetic.main.fragment_main_swap_settings.swapMainSettingsExecutionTime import kotlinx.android.synthetic.main.fragment_main_swap_settings.swapMainSettingsFlip import kotlinx.android.synthetic.main.fragment_main_swap_settings.swapMainSettingsGetAssetIn import kotlinx.android.synthetic.main.fragment_main_swap_settings.swapMainSettingsMaxAmount @@ -108,6 +109,7 @@ class SwapMainSettingsFragment : BaseFragment() { viewModel.rateDetails.observe { swapMainSettingsDetailsRate.showLoadingValue(it) } viewModel.swapRouteState.observe(swapMainSettingsRoute::setSwapRouteState) + viewModel.swapExecutionTime.observe(swapMainSettingsExecutionTime::showLoadingValue) viewModel.showDetails.observe { swapMainSettingsDetails.setVisible(it) } viewModel.buttonState.observe(swapMainSettingsContinue::setState) diff --git a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/main/SwapMainSettingsViewModel.kt b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/main/SwapMainSettingsViewModel.kt index 0177731719..331df513d7 100644 --- a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/main/SwapMainSettingsViewModel.kt +++ b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/main/SwapMainSettingsViewModel.kt @@ -38,6 +38,7 @@ import io.novafoundation.nova.feature_swap_api.domain.model.SwapQuote import io.novafoundation.nova.feature_swap_api.domain.model.SwapQuoteArgs import io.novafoundation.nova.feature_swap_api.domain.model.swapRate import io.novafoundation.nova.feature_swap_api.domain.model.toExecuteArgs +import io.novafoundation.nova.feature_swap_api.domain.model.totalTime import io.novafoundation.nova.feature_swap_api.presentation.formatters.SwapRateFormatter import io.novafoundation.nova.feature_swap_api.presentation.model.SwapSettingsPayload import io.novafoundation.nova.feature_swap_api.presentation.model.mapFromModel @@ -170,6 +171,10 @@ class SwapMainSettingsViewModel( .map { quoteState -> quoteState.toSwapRouteState() } .shareInBackground() + val swapExecutionTime = quotingState + .map { it.toExecutionEstimate() } + .shareInBackground() + private val originChainFlow = swapSettings .mapNotNull { it.assetIn?.chainId } .distinctUntilChanged() @@ -644,11 +649,13 @@ class SwapMainSettingsViewModel( } private suspend fun QuotingState.toSwapRouteState(): SwapRouteState { - return when (this) { - QuotingState.Default -> ExtendedLoadingState.Loaded(null) - is QuotingState.Error -> ExtendedLoadingState.Error(error) - is QuotingState.Loaded -> ExtendedLoadingState.Loaded(swapRouteFormatter.formatSwapRoute(quote)) - QuotingState.Loading -> ExtendedLoadingState.Loading + return toLoadingState { swapRouteFormatter.formatSwapRoute(it) } + } + + private fun QuotingState.toExecutionEstimate(): ExtendedLoadingState { + return toLoadingState { + val estimatedDuration = it.executionEstimate.totalTime() + resourceManager.formatDuration(estimatedDuration, estimated = true) } } diff --git a/feature-swap-impl/src/main/res/layout/fragment_main_swap_settings.xml b/feature-swap-impl/src/main/res/layout/fragment_main_swap_settings.xml index 836be3fac7..eed2f022b0 100644 --- a/feature-swap-impl/src/main/res/layout/fragment_main_swap_settings.xml +++ b/feature-swap-impl/src/main/res/layout/fragment_main_swap_settings.xml @@ -171,6 +171,12 @@ android:layout_height="wrap_content" app:titleIcon="@drawable/ic_info" /> + + + + + + suspend fun estimateMaximumExecutionTime(configuration: CrossChainTransferConfiguration): Duration } diff --git a/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/domain/interfaces/CrossChainTransfersUseCase.kt b/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/domain/interfaces/CrossChainTransfersUseCase.kt index 7e637f2375..7a57d16bee 100644 --- a/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/domain/interfaces/CrossChainTransfersUseCase.kt +++ b/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/domain/interfaces/CrossChainTransfersUseCase.kt @@ -2,6 +2,7 @@ package io.novafoundation.nova.feature_wallet_api.domain.interfaces import io.novafoundation.nova.feature_account_api.data.extrinsic.ExtrinsicService import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.tranfers.AssetTransferBase +import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.tranfers.AssetTransferDirection import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.types.Balance import io.novafoundation.nova.feature_wallet_api.domain.model.Asset import io.novafoundation.nova.feature_wallet_api.domain.model.CrossChainTransferFee @@ -11,6 +12,7 @@ import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.map +import kotlin.time.Duration class IncomingDirection( val asset: Asset, @@ -49,6 +51,11 @@ interface CrossChainTransfersUseCase { transfer: AssetTransferBase, computationalScope: CoroutineScope ): Result + + suspend fun maximumExecutionTime( + assetTransferDirection: AssetTransferDirection, + computationalScope: CoroutineScope + ): Duration } fun CrossChainTransfersUseCase.incomingCrossChainDirectionsAvailable(destination: Flow): Flow { 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 bbe471b7a9..404eb0a123 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 @@ -42,11 +42,16 @@ import io.novafoundation.nova.feature_wallet_impl.data.network.blockchain.assets import io.novafoundation.nova.feature_wallet_impl.data.network.crosschain.validations.canPayCrossChainFee import io.novafoundation.nova.feature_wallet_impl.data.network.crosschain.validations.cannotDropBelowEdBeforePayingDeliveryFee import io.novafoundation.nova.runtime.ext.accountIdOrDefault +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.findRelayChainOrThrow import io.novafoundation.nova.runtime.multiNetwork.multiLocation.MultiLocation import io.novafoundation.nova.runtime.multiNetwork.runtime.repository.EventsRepository import io.novafoundation.nova.runtime.multiNetwork.runtime.repository.getInherentEvents import io.novafoundation.nova.runtime.multiNetwork.runtime.repository.hasEvent +import io.novafoundation.nova.runtime.repository.ChainStateRepository +import io.novafoundation.nova.runtime.repository.expectedBlockTime import io.novasama.substrate_sdk_android.runtime.definitions.types.generics.GenericEvent import io.novasama.substrate_sdk_android.runtime.extrinsic.ExtrinsicBuilder import kotlinx.coroutines.CoroutineScope @@ -57,6 +62,8 @@ import kotlinx.coroutines.flow.shareIn import kotlinx.coroutines.withTimeout import java.math.BigInteger import kotlin.coroutines.coroutineContext +import kotlin.time.Duration +import kotlin.time.Duration.Companion.ZERO import kotlin.time.Duration.Companion.seconds class RealCrossChainTransactor( @@ -65,7 +72,9 @@ class RealCrossChainTransactor( private val phishingValidationFactory: PhishingValidationFactory, private val palletXcmRepository: PalletXcmRepository, private val enoughTotalToStayAboveEDValidationFactory: EnoughTotalToStayAboveEDValidationFactory, - private val eventsRepository: EventsRepository + private val eventsRepository: EventsRepository, + private val chainStateRepository: ChainStateRepository, + private val chainRegistry: ChainRegistry, ) : CrossChainTransactor { override val validationSystem: AssetTransfersValidationSystem = ValidationSystem { @@ -147,6 +156,33 @@ class RealCrossChainTransactor( } } + override suspend fun estimateMaximumExecutionTime(configuration: CrossChainTransferConfiguration): Duration { + val originChainId = configuration.originChainId + val reserveChainId = configuration.reserveFee?.to?.chainId + val destinationChainId = configuration.destinationFee.to.chainId + + val relayId = chainRegistry.findRelayChainOrThrow(originChainId) + + var totalDuration = ZERO + + if (reserveChainId != null) { + totalDuration += maxTimeToTransmitMessage(originChainId, reserveChainId, relayId) + totalDuration += maxTimeToTransmitMessage(reserveChainId, destinationChainId, relayId) + } else { + totalDuration += maxTimeToTransmitMessage(originChainId, destinationChainId, relayId) + } + + return totalDuration + } + + private suspend fun maxTimeToTransmitMessage(from: ChainId, to: ChainId, relay: ChainId): Duration { + val toProduceBlockOnOrigin = chainStateRepository.expectedBlockTime(from) + val toProduceBlockOnDestination = chainStateRepository.expectedBlockTime(to) + val toProduceBlockOnRelay = if (from != relay && to != relay) chainStateRepository.expectedBlockTime(relay) else ZERO + + return toProduceBlockOnOrigin + toProduceBlockOnRelay + toProduceBlockOnDestination + } + private suspend fun Flow>.awaitCrossChainArrival(transfer: AssetTransferBase): Result { return runCatching { withTimeout(60.seconds) { diff --git a/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/di/WalletFeatureDependencies.kt b/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/di/WalletFeatureDependencies.kt index f071f2c288..393c3f3478 100644 --- a/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/di/WalletFeatureDependencies.kt +++ b/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/di/WalletFeatureDependencies.kt @@ -50,6 +50,7 @@ import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry import io.novafoundation.nova.runtime.multiNetwork.multiLocation.converter.MultiLocationConverterFactory import io.novafoundation.nova.runtime.multiNetwork.runtime.repository.EventsRepository import io.novafoundation.nova.runtime.network.rpc.RpcCalls +import io.novafoundation.nova.runtime.repository.ChainStateRepository import io.novafoundation.nova.runtime.repository.ParachainInfoRepository import io.novafoundation.nova.runtime.storage.source.StorageDataSource import io.novasama.substrate_sdk_android.encrypt.Signer @@ -91,6 +92,8 @@ interface WalletFeatureDependencies { val parachainInfoRepository: ParachainInfoRepository + val chainStateRepository: ChainStateRepository + fun preferences(): Preferences fun encryptedPreferences(): EncryptedPreferences 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 b1d1f488a3..08fc1c23c5 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 @@ -91,6 +91,7 @@ import io.novafoundation.nova.runtime.extrinsic.visitor.api.ExtrinsicWalk import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry import io.novafoundation.nova.runtime.multiNetwork.multiLocation.converter.MultiLocationConverterFactory 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 javax.inject.Named @@ -297,14 +298,18 @@ class WalletFeatureModule { phishingValidationFactory: PhishingValidationFactory, palletXcmRepository: PalletXcmRepository, enoughTotalToStayAboveEDValidationFactory: EnoughTotalToStayAboveEDValidationFactory, - eventsRepository: EventsRepository + eventsRepository: EventsRepository, + chainRegistry: ChainRegistry, + chainStateRepository: ChainStateRepository ): CrossChainTransactor = RealCrossChainTransactor( weigher = weigher, assetSourceRegistry = assetSourceRegistry, phishingValidationFactory = phishingValidationFactory, palletXcmRepository = palletXcmRepository, enoughTotalToStayAboveEDValidationFactory = enoughTotalToStayAboveEDValidationFactory, - eventsRepository = eventsRepository + eventsRepository = eventsRepository, + chainStateRepository = chainStateRepository, + chainRegistry = chainRegistry ) @Provides diff --git a/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/domain/RealCrossChainTransfersUseCase.kt b/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/domain/RealCrossChainTransfersUseCase.kt index 3f9de8d140..c8540d01ee 100644 --- a/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/domain/RealCrossChainTransfersUseCase.kt +++ b/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/domain/RealCrossChainTransfersUseCase.kt @@ -10,6 +10,7 @@ import io.novafoundation.nova.feature_account_api.data.model.SubstrateFee import io.novafoundation.nova.feature_account_api.data.model.SubstrateFeeBase import io.novafoundation.nova.feature_account_api.domain.interfaces.AccountRepository import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.tranfers.AssetTransferBase +import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.tranfers.AssetTransferDirection 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.CrossChainTransfersRepository @@ -22,6 +23,7 @@ import io.novafoundation.nova.feature_wallet_api.domain.interfaces.CrossChainTra import io.novafoundation.nova.feature_wallet_api.domain.interfaces.IncomingDirection import io.novafoundation.nova.feature_wallet_api.domain.interfaces.OutcomingDirection import io.novafoundation.nova.feature_wallet_api.domain.interfaces.WalletRepository +import io.novafoundation.nova.feature_wallet_api.domain.model.CrossChainTransferConfiguration import io.novafoundation.nova.feature_wallet_api.domain.model.CrossChainTransferFee import io.novafoundation.nova.feature_wallet_api.domain.model.CrossChainTransfersConfiguration import io.novafoundation.nova.runtime.ext.commissionAsset @@ -39,6 +41,7 @@ import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.map import kotlinx.coroutines.launch +import kotlin.time.Duration private const val INCOMING_DIRECTIONS = "RealCrossChainTransfersUseCase.INCOMING_DIRECTIONS" private const val CONFIGURATION_CACHE = "RealCrossChainTransfersUseCase.CONFIGURATION" @@ -100,11 +103,11 @@ internal class RealCrossChainTransfersUseCase( } override suspend fun getConfiguration(): CrossChainTransfersConfiguration { - return crossChainTransfersRepository.getConfiguration() + return crossChainTransfersRepository.getConfiguration() } override suspend fun requiredRemainingAmountAfterTransfer(sendingAsset: Chain.Asset, originChain: Chain): Balance { - return crossChainTransactor.requiredRemainingAmountAfterTransfer(sendingAsset, originChain) + return crossChainTransactor.requiredRemainingAmountAfterTransfer(sendingAsset, originChain) } @@ -144,15 +147,29 @@ internal class RealCrossChainTransfersUseCase( transfer: AssetTransferBase, computationalScope: CoroutineScope ): Result { + val transferConfiguration = transferConfigurationFor(transfer, computationalScope) + return crossChainTransactor.performAndTrackTransfer(transferConfiguration, transfer) + } + + override suspend fun maximumExecutionTime( + assetTransferDirection: AssetTransferDirection, + computationalScope: CoroutineScope + ): Duration { + val transferConfiguration = transferConfigurationFor(assetTransferDirection, computationalScope) + return crossChainTransactor.estimateMaximumExecutionTime(transferConfiguration) + } + + private suspend fun transferConfigurationFor( + transfer: AssetTransferDirection, + computationalScope: CoroutineScope + ): CrossChainTransferConfiguration { val configuration = cachedConfigurationFlow(computationalScope).first() - val transferConfiguration = configuration.transferConfiguration( + return configuration.transferConfiguration( originChain = transfer.originChain, originAsset = transfer.originChainAsset, destinationChain = transfer.destinationChain, destinationParaId = parachainInfoRepository.paraId(transfer.destinationChain.id) )!! - - return crossChainTransactor.performAndTrackTransfer(transferConfiguration, transfer) } private fun cachedConfigurationFlow(cachingScope: CoroutineScope?): Flow { diff --git a/runtime/src/main/java/io/novafoundation/nova/runtime/multiNetwork/ChainRegistry.kt b/runtime/src/main/java/io/novafoundation/nova/runtime/multiNetwork/ChainRegistry.kt index f8c0879a0a..087176c152 100644 --- a/runtime/src/main/java/io/novafoundation/nova/runtime/multiNetwork/ChainRegistry.kt +++ b/runtime/src/main/java/io/novafoundation/nova/runtime/multiNetwork/ChainRegistry.kt @@ -366,6 +366,11 @@ suspend fun ChainRegistry.findEvmChainFromHexId(evmChainIdHex: String): Chain? { return findEvmChain(addressPrefix) } +suspend fun ChainRegistry.findRelayChainOrThrow(chainId: ChainId): ChainId { + val chain = getChain(chainId) + return chain.parentId ?: chainId +} + fun ChainRegistry.enabledChainsFlow() = currentChains .filterList { it.isEnabled } diff --git a/runtime/src/main/java/io/novafoundation/nova/runtime/repository/ChainStateRepository.kt b/runtime/src/main/java/io/novafoundation/nova/runtime/repository/ChainStateRepository.kt index 7f369b58c3..a5c57d88ee 100644 --- a/runtime/src/main/java/io/novafoundation/nova/runtime/repository/ChainStateRepository.kt +++ b/runtime/src/main/java/io/novafoundation/nova/runtime/repository/ChainStateRepository.kt @@ -2,7 +2,6 @@ package io.novafoundation.nova.runtime.repository import io.novafoundation.nova.common.data.network.runtime.binding.BlockNumber import io.novafoundation.nova.common.data.network.runtime.binding.bindBlockNumber -import io.novafoundation.nova.common.utils.babe import io.novafoundation.nova.common.utils.babeOrNull import io.novafoundation.nova.common.utils.isParachain import io.novafoundation.nova.common.utils.numberConstant @@ -24,6 +23,8 @@ import io.novasama.substrate_sdk_android.runtime.metadata.storageKey import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.map import java.math.BigInteger +import kotlin.time.Duration +import kotlin.time.Duration.Companion.milliseconds private val FALLBACK_BLOCK_TIME_MILLIS_RELAYCHAIN = (6 * 1000).toBigInteger() private val FALLBACK_BLOCK_TIME_MILLIS_PARACHAIN = 2.toBigInteger() * FALLBACK_BLOCK_TIME_MILLIS_RELAYCHAIN @@ -40,8 +41,9 @@ class ChainStateRepository( suspend fun expectedBlockTimeInMillis(chainId: ChainId): BigInteger { val runtime = chainRegistry.getRuntime(chainId) + val chain = chainRegistry.getChain(chainId) - return runtime.metadata.babe().numberConstant("ExpectedBlockTime", runtime) + return blockTimeFromConstants(chain, runtime) } suspend fun predictedBlockTime(chainId: ChainId): BigInteger { @@ -81,10 +83,17 @@ class ChainStateRepository( ?: runtime.metadata.babeOrNull()?.numberConstant("ExpectedBlockTime", runtime) // Some chains incorrectly use these, i.e. it is set to values such as 0 or even 2 // Use a low minimum validity threshold to check these against - ?: runtime.metadata.timestampOrNull()?.numberConstant("MinimumPeriod", runtime)?.takeIf { it > PERIOD_VALIDITY_THRESHOLD } + ?: blockTimeFromTimestampPallet(runtime) ?: fallbackBlockTime(runtime) } + private fun blockTimeFromTimestampPallet(runtime: RuntimeSnapshot): BigInteger? { + val blockTime = runtime.metadata.timestampOrNull()?.numberConstant("MinimumPeriod", runtime)?.takeIf { it > PERIOD_VALIDITY_THRESHOLD } + ?: return null + + return blockTime * 2.toBigInteger() + } + suspend fun blockHashCount(chainId: ChainId): BigInteger? { val runtime = chainRegistry.getRuntime(chainId) @@ -113,3 +122,7 @@ class ChainStateRepository( } } } + +suspend fun ChainStateRepository.expectedBlockTime(chainId: ChainId): Duration { + return expectedBlockTimeInMillis(chainId).toLong().milliseconds +} From 3567705944b6ddcd3853f76558f52d27fd53a16d Mon Sep 17 00:00:00 2001 From: valentun Date: Fri, 8 Nov 2024 18:29:33 +0700 Subject: [PATCH 46/83] Fixes --- .../nova/feature_assets/presentation/AssetsRouter.kt | 2 -- .../presentation/mixin/fee/model/FeeModel.kt | 4 ++-- .../presentation/model/AmountFormatters.kt | 8 +++++--- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/AssetsRouter.kt b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/AssetsRouter.kt index e21ab91229..28dc1ff223 100644 --- a/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/AssetsRouter.kt +++ b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/AssetsRouter.kt @@ -85,6 +85,4 @@ interface AssetsRouter { fun returnToMainSwapScreen() fun openBuyNetworks(payload: NetworkFlowPayload) - - fun returnToMainSwapScreen() } diff --git a/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/presentation/mixin/fee/model/FeeModel.kt b/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/presentation/mixin/fee/model/FeeModel.kt index 701f93e624..7791cb1839 100644 --- a/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/presentation/mixin/fee/model/FeeModel.kt +++ b/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/presentation/mixin/fee/model/FeeModel.kt @@ -8,8 +8,8 @@ class FeeModel( ) class FeeDisplay( - val title: String, - val subtitle: String? + val title: CharSequence, + val subtitle: CharSequence? ) fun AmountModel.toFeeDisplay(): FeeDisplay { diff --git a/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/presentation/model/AmountFormatters.kt b/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/presentation/model/AmountFormatters.kt index 0999e2b8a4..9d59b87c71 100644 --- a/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/presentation/model/AmountFormatters.kt +++ b/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/presentation/model/AmountFormatters.kt @@ -26,12 +26,14 @@ class RealAmountFormatter( val sizeSpan = AbsoluteSizeSpan(resourceManager.getDimensionPixelSize(floatAmountSize)) return with(amountWithFraction) { + val decimalAmount = amountWithFraction.amount + val spannableBuilder = SpannableStringBuilder() - .append(amount) + .append(decimalAmount) if (fraction != null) { spannableBuilder.append(separator + fraction) - val startIndex = amount.length - val endIndex = amount.length + separator.length + fraction!!.length + val startIndex = decimalAmount.length + val endIndex = decimalAmount.length + separator.length + fraction!!.length spannableBuilder.setSpan(colorSpan, startIndex, endIndex, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) spannableBuilder.setSpan(sizeSpan, startIndex, endIndex, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) } From 7e0936029ce19a1e65e1589c11c49529a85616e5 Mon Sep 17 00:00:00 2001 From: valentun Date: Mon, 11 Nov 2024 18:04:44 +0700 Subject: [PATCH 47/83] Swap Execution WIP --- .../app/root/navigation/swap/SwapNavigator.kt | 4 +- .../res/navigation/start_swap_nav_graph.xml | 25 ++- .../nova/common/utils/KotlinExt.kt | 3 + .../nova/common/utils/ViewExt.kt | 4 + .../nova/common/view/Extensions.kt | 16 +- common/src/main/res/values/colors.xml | 1 + common/src/main/res/values/strings.xml | 13 ++ common/src/main/res/values/styles.xml | 5 + .../balance/common/AssetListMixin.kt | 9 +- .../balance/common/ExpandableAssetsMixin.kt | 17 +- .../swap/asset/AssetSwapFlowViewModel.kt | 2 +- .../model/AtomicOperationDisplayData.kt | 4 +- .../domain/model/AtomicSwapOperation.kt | 7 +- .../model/AtomicSwapOperationPrototype.kt | 2 +- .../domain/model/SwapExecutionEstimate.kt | 7 +- .../domain/model/SwapProgress.kt | 11 +- .../AssetConversionExchange.kt | 4 - .../CrossChainTransferAssetExchange.kt | 8 - .../hydraDx/HydraDxAssetExchange.kt | 10 - .../di/SwapFeatureComponent.kt | 3 + .../domain/swap/RealSwapService.kt | 18 +- .../presentation/SwapRouter.kt | 2 + .../confirmation/SwapConfirmationViewModel.kt | 27 +-- .../execution/SwapExecutionFragment.kt | 127 ++++++++++++ .../execution/SwapExecutionViewModel.kt | 186 ++++++++++++++++++ .../execution/di/SwapExecutionComponent.kt | 26 +++ .../execution/di/SwapExecutionModule.kt | 47 +++++ .../execution/model/SwapProgressModel.kt | 16 ++ .../main/res/drawable/bg_swap_progress.xml | 12 ++ .../res/layout/fragment_swap_execution.xml | 139 +++++++++++++ 30 files changed, 665 insertions(+), 90 deletions(-) create mode 100644 feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/execution/SwapExecutionFragment.kt create mode 100644 feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/execution/SwapExecutionViewModel.kt create mode 100644 feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/execution/di/SwapExecutionComponent.kt create mode 100644 feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/execution/di/SwapExecutionModule.kt create mode 100644 feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/execution/model/SwapProgressModel.kt create mode 100644 feature-swap-impl/src/main/res/drawable/bg_swap_progress.xml create mode 100644 feature-swap-impl/src/main/res/layout/fragment_swap_execution.xml diff --git a/app/src/main/java/io/novafoundation/nova/app/root/navigation/swap/SwapNavigator.kt b/app/src/main/java/io/novafoundation/nova/app/root/navigation/swap/SwapNavigator.kt index e9927aa2e7..a691c3945b 100644 --- a/app/src/main/java/io/novafoundation/nova/app/root/navigation/swap/SwapNavigator.kt +++ b/app/src/main/java/io/novafoundation/nova/app/root/navigation/swap/SwapNavigator.kt @@ -22,12 +22,14 @@ class SwapNavigator( override fun openSwapFee() = performNavigation(R.id.action_open_swapFeeFragment) + override fun openSwapExecution() = performNavigation(R.id.action_swapConfirmationFragment_to_swapExecutionFragment) + override fun openSwapOptions() { navigationHolder.navController?.navigate(R.id.action_swapMainSettingsFragment_to_swapOptionsFragment) } override fun openBalanceDetails(assetPayload: AssetPayload) { - navigationHolder.navController?.navigate(R.id.action_swapConfirmationFragment_to_assetDetails, BalanceDetailFragment.getBundle(assetPayload)) + navigationHolder.navController?.navigate(R.id.action_swapExecutionFragment_to_assetDetails, BalanceDetailFragment.getBundle(assetPayload)) } override fun selectAssetIn(selectedAsset: AssetPayload?) { diff --git a/app/src/main/res/navigation/start_swap_nav_graph.xml b/app/src/main/res/navigation/start_swap_nav_graph.xml index c7878d0c3d..4a4498927b 100644 --- a/app/src/main/res/navigation/start_swap_nav_graph.xml +++ b/app/src/main/res/navigation/start_swap_nav_graph.xml @@ -42,13 +42,8 @@ tools:layout="@layout/fragment_swap_confirmation_settings"> + android:id="@+id/action_swapConfirmationFragment_to_swapExecutionFragment" + app:destination="@id/swapExecutionFragment" /> @@ -80,4 +75,20 @@ + + + + + \ No newline at end of file diff --git a/common/src/main/java/io/novafoundation/nova/common/utils/KotlinExt.kt b/common/src/main/java/io/novafoundation/nova/common/utils/KotlinExt.kt index 8b5039735c..91a6fbeaa8 100644 --- a/common/src/main/java/io/novafoundation/nova/common/utils/KotlinExt.kt +++ b/common/src/main/java/io/novafoundation/nova/common/utils/KotlinExt.kt @@ -31,6 +31,7 @@ import kotlin.contracts.contract import kotlin.coroutines.CoroutineContext import kotlin.coroutines.EmptyCoroutineContext import kotlin.math.sqrt +import kotlin.time.Duration import kotlin.time.ExperimentalTime import kotlin.time.measureTimedValue @@ -624,3 +625,5 @@ fun Calendar.resetDay() { inline fun CoroutineScope.launchUnit(crossinline block: suspend CoroutineScope.() -> Unit) { launch { block() } } + +fun Iterable.sum(): Duration = fold(Duration.ZERO) { acc, duration -> acc + duration } diff --git a/common/src/main/java/io/novafoundation/nova/common/utils/ViewExt.kt b/common/src/main/java/io/novafoundation/nova/common/utils/ViewExt.kt index 76402edeac..841ffc101a 100644 --- a/common/src/main/java/io/novafoundation/nova/common/utils/ViewExt.kt +++ b/common/src/main/java/io/novafoundation/nova/common/utils/ViewExt.kt @@ -342,6 +342,10 @@ fun View.applyImeInsetts() = applyInsetter { fun View.setBackgroundColorRes(@ColorRes colorRes: Int) = setBackgroundColor(context.getColor(colorRes)) +fun View.setBackgroundTintRes(@ColorRes colorRes: Int) { + backgroundTintList = ColorStateList.valueOf(context.getColor(colorRes)) +} + fun View.useInputValue(input: Input, onValue: (I) -> Unit) { setVisible(input is Input.Enabled) isEnabled = input is Input.Enabled.Modifiable diff --git a/common/src/main/java/io/novafoundation/nova/common/view/Extensions.kt b/common/src/main/java/io/novafoundation/nova/common/view/Extensions.kt index 00c4df8ee7..cde47a4bb8 100644 --- a/common/src/main/java/io/novafoundation/nova/common/view/Extensions.kt +++ b/common/src/main/java/io/novafoundation/nova/common/view/Extensions.kt @@ -9,6 +9,7 @@ import androidx.lifecycle.Lifecycle import androidx.lifecycle.LifecycleCoroutineScope import io.novafoundation.nova.common.R import io.novafoundation.nova.common.utils.bindTo +import io.novafoundation.nova.common.utils.formatting.TimerValue import io.novafoundation.nova.common.utils.formatting.duration.CompoundDurationFormatter import io.novafoundation.nova.common.utils.formatting.duration.DayAndHourDurationFormatter import io.novafoundation.nova.common.utils.formatting.duration.DayDurationFormatter @@ -16,35 +17,34 @@ import io.novafoundation.nova.common.utils.formatting.duration.DurationFormatter import io.novafoundation.nova.common.utils.formatting.duration.HoursDurationFormatter import io.novafoundation.nova.common.utils.formatting.duration.RoundMinutesDurationFormatter import io.novafoundation.nova.common.utils.formatting.duration.TimeDurationFormatter -import io.novafoundation.nova.common.utils.formatting.TimerValue import io.novafoundation.nova.common.utils.formatting.duration.ZeroDurationFormatter import io.novafoundation.nova.common.utils.makeGone import io.novafoundation.nova.common.utils.onDestroy import kotlinx.coroutines.flow.MutableStateFlow import kotlin.time.Duration.Companion.days import kotlin.time.Duration.Companion.milliseconds -import kotlin.time.ExperimentalTime private val TIMER_TAG = R.string.common_time_left fun TextView.startTimer( value: TimerValue, + durationFormatter: DurationFormatter? = null, @StringRes customMessageFormat: Int? = null, lifecycle: Lifecycle? = null, onTick: ((view: TextView, millisUntilFinished: Long) -> Unit)? = null, onFinish: ((view: TextView) -> Unit)? = null -) = startTimer(value.millis, value.millisCalculatedAt, lifecycle, customMessageFormat, onTick, onFinish) +) = startTimer(value.millis, value.millisCalculatedAt, durationFormatter, lifecycle, customMessageFormat, onTick, onFinish) -@OptIn(ExperimentalTime::class) fun TextView.startTimer( millis: Long, millisCalculatedAt: Long? = null, + durationFormatter: DurationFormatter? = null, lifecycle: Lifecycle? = null, @StringRes customMessageFormat: Int? = null, onTick: ((view: TextView, millisUntilFinished: Long) -> Unit)? = null, onFinish: ((view: TextView) -> Unit)? = null ) { - val durationFormatter = getTimerDurationFormatter(context) + val actualDurationFormatter = durationFormatter ?: getTimerDurationFormatter(context) val timePassedSinceCalculation = if (millisCalculatedAt != null) System.currentTimeMillis() - millisCalculatedAt else 0L @@ -56,7 +56,7 @@ fun TextView.startTimer( val newTimer = object : CountDownTimer(millis - timePassedSinceCalculation, 1000) { override fun onTick(millisUntilFinished: Long) { - setNewValue(durationFormatter, millisUntilFinished, customMessageFormat) + setNewValue(actualDurationFormatter, millisUntilFinished, customMessageFormat) onTick?.invoke(this@startTimer, millisUntilFinished) } @@ -65,7 +65,7 @@ fun TextView.startTimer( if (onFinish != null) { onFinish(this@startTimer) } else { - this@startTimer.text = durationFormatter.format(0L.milliseconds) + this@startTimer.text = actualDurationFormatter.format(0L.milliseconds) } cancel() @@ -78,7 +78,7 @@ fun TextView.startTimer( newTimer.cancel() } - setNewValue(durationFormatter, millis - timePassedSinceCalculation, customMessageFormat) + setNewValue(actualDurationFormatter, millis - timePassedSinceCalculation, customMessageFormat) newTimer.start() setTag(TIMER_TAG, newTimer) diff --git a/common/src/main/res/values/colors.xml b/common/src/main/res/values/colors.xml index 450bf10f81..b980c98c28 100644 --- a/common/src/main/res/values/colors.xml +++ b/common/src/main/res/values/colors.xml @@ -62,6 +62,7 @@ #E53450 #E53450 #2FC864 + #1F2FC864 #08090E diff --git a/common/src/main/res/values/strings.xml b/common/src/main/res/values/strings.xml index 45dd786e2d..7480f3d6e3 100644 --- a/common/src/main/res/values/strings.xml +++ b/common/src/main/res/values/strings.xml @@ -1,5 +1,18 @@ + Do not close the app! + + Failed + + %s of %s operations + %s operations + Swapping %s to %s on %s + Transferring %s to %s + + %s of %s (%s) + %s to %s swap on %s + %s transfer from %s to %s + %d second %d seconds diff --git a/common/src/main/res/values/styles.xml b/common/src/main/res/values/styles.xml index 63d87457d5..612b87a879 100644 --- a/common/src/main/res/values/styles.xml +++ b/common/src/main/res/values/styles.xml @@ -64,6 +64,11 @@ 11sp + +