mirror of
https://github.com/pezkuwichain/pezkuwi-wallet-android.git
synced 2026-04-29 04:17:59 +00:00
Initial commit: Pezkuwi Wallet Android
Complete rebrand of Nova Wallet for Pezkuwichain ecosystem. ## Features - Full Pezkuwichain support (HEZ & PEZ tokens) - Polkadot ecosystem compatibility - Staking, Governance, DeFi, NFTs - XCM cross-chain transfers - Hardware wallet support (Ledger, Polkadot Vault) - WalletConnect v2 - Push notifications ## Languages - English, Turkish, Kurmanci (Kurdish), Spanish, French, German, Russian, Japanese, Chinese, Korean, Portuguese, Vietnamese Based on Nova Wallet by Novasama Technologies GmbH © Dijital Kurdistan Tech Institute 2026
This commit is contained in:
@@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
</manifest>
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
@file:Suppress("RedundantUnitExpression")
|
||||
|
||||
package io.novafoundation.nova.feature_swap_core.data.assetExchange.conversion.types.hydra
|
||||
|
||||
import io.novafoundation.nova.common.data.network.runtime.binding.bindNumber
|
||||
import io.novafoundation.nova.common.utils.multiTransactionPayment
|
||||
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
|
||||
import io.novafoundation.nova.runtime.storage.source.query.api.storage1
|
||||
import io.novasama.substrate_sdk_android.runtime.AccountId
|
||||
import io.novasama.substrate_sdk_android.runtime.metadata.RuntimeMetadata
|
||||
import io.novasama.substrate_sdk_android.runtime.metadata.module.Module
|
||||
import java.math.BigInteger
|
||||
|
||||
@JvmInline
|
||||
value class MultiTransactionPaymentApi(override val module: Module) : QueryableModule
|
||||
|
||||
context(StorageQueryContext)
|
||||
val RuntimeMetadata.multiTransactionPayment: MultiTransactionPaymentApi
|
||||
get() = MultiTransactionPaymentApi(multiTransactionPayment())
|
||||
|
||||
context(StorageQueryContext)
|
||||
val MultiTransactionPaymentApi.acceptedCurrencies: QueryableStorageEntry1<HydraDxAssetId, BigInteger>
|
||||
get() = storage1(
|
||||
name = "AcceptedCurrencies",
|
||||
binding = { decoded, _ -> bindNumber(decoded) },
|
||||
)
|
||||
|
||||
context(StorageQueryContext)
|
||||
val MultiTransactionPaymentApi.accountCurrencyMap: QueryableStorageEntry1<AccountId, HydraDxAssetId>
|
||||
get() = storage1(
|
||||
name = "AccountCurrencyMap",
|
||||
binding = { decoded, _ -> bindNumber(decoded) },
|
||||
)
|
||||
+51
@@ -0,0 +1,51 @@
|
||||
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_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
|
||||
import io.novafoundation.nova.runtime.multiNetwork.getRuntime
|
||||
import io.novasama.substrate_sdk_android.runtime.RuntimeSnapshot
|
||||
import java.math.BigInteger
|
||||
|
||||
private val SYSTEM_ON_CHAIN_ASSET_ID = BigInteger.ZERO
|
||||
|
||||
internal class RealHydraDxAssetIdConverter(
|
||||
private val chainRegistry: ChainRegistry
|
||||
) : HydraDxAssetIdConverter {
|
||||
|
||||
override val systemAssetId: HydraDxAssetId = SYSTEM_ON_CHAIN_ASSET_ID
|
||||
|
||||
override suspend fun toOnChainIdOrNull(chainAsset: Chain.Asset): HydraDxAssetId? {
|
||||
val runtime = chainRegistry.getRuntime(chainAsset.chainId)
|
||||
return chainAsset.omniPoolTokenIdOrNull(runtime)
|
||||
}
|
||||
|
||||
override suspend fun toChainAssetOrNull(chain: Chain, onChainId: HydraDxAssetId): Chain.Asset? {
|
||||
val runtime = chainRegistry.getRuntime(chain.id)
|
||||
|
||||
return chain.assets.find { chainAsset ->
|
||||
val omniPoolId = chainAsset.omniPoolTokenIdOrNull(runtime)
|
||||
|
||||
omniPoolId == onChainId
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun allOnChainIds(chain: Chain): Map<HydraDxAssetId, Chain.Asset> {
|
||||
val runtime = chainRegistry.getRuntime(chain.id)
|
||||
|
||||
return chain.assets.mapNotNull { chainAsset ->
|
||||
chainAsset.omniPoolTokenIdOrNull(runtime)?.let { it to chainAsset }
|
||||
}.toMap()
|
||||
}
|
||||
|
||||
private fun Chain.Asset.omniPoolTokenIdOrNull(runtimeSnapshot: RuntimeSnapshot): HydraDxAssetId? {
|
||||
return when (val type = type) {
|
||||
is Chain.Asset.Type.Orml -> bindNumberOrNull(type.decodeOrNull(runtimeSnapshot))
|
||||
is Chain.Asset.Type.Native -> systemAssetId
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
}
|
||||
+58
@@ -0,0 +1,58 @@
|
||||
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.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.feature_swap_core_api.data.types.hydra.HydraDxQuotingSource
|
||||
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain
|
||||
import io.novasama.substrate_sdk_android.runtime.AccountId
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
class RealHydraDxQuotingFactory(
|
||||
private val conversionSourceFactories: Iterable<HydraDxQuotingSource.Factory<*>>,
|
||||
) : HydraDxQuoting.Factory {
|
||||
|
||||
override fun create(chain: Chain, host: SwapQuoting.QuotingHost): HydraDxQuoting {
|
||||
return RealHydraDxQuoting(
|
||||
chain = chain,
|
||||
quotingSourceFactories = conversionSourceFactories,
|
||||
host = host
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private class RealHydraDxQuoting(
|
||||
private val chain: Chain,
|
||||
private val quotingSourceFactories: Iterable<HydraDxQuotingSource.Factory<*>>,
|
||||
private val host: SwapQuoting.QuotingHost,
|
||||
) : HydraDxQuoting {
|
||||
|
||||
private val quotingSources: Map<String, HydraDxQuotingSource<*>> = createSources()
|
||||
|
||||
override fun getSource(id: String): HydraDxQuotingSource<*> {
|
||||
return quotingSources.getValue(id)
|
||||
}
|
||||
|
||||
override suspend fun sync() {
|
||||
quotingSources.values.forEachAsync { it.sync() }
|
||||
}
|
||||
|
||||
override suspend fun availableSwapDirections(): List<QuotableEdge> {
|
||||
return quotingSources.values.flatMapAsync { source -> source.availableSwapDirections() }
|
||||
}
|
||||
|
||||
override suspend fun runSubscriptions(userAccountId: AccountId, subscriptionBuilder: SharedRequestsBuilder): Flow<Unit> {
|
||||
return quotingSources.values.map {
|
||||
it.runSubscriptions(userAccountId, subscriptionBuilder)
|
||||
}.mergeIfMultiple()
|
||||
}
|
||||
|
||||
private fun createSources(): Map<String, HydraDxQuotingSource<*>> {
|
||||
return quotingSourceFactories.map { it.create(chain, host) }
|
||||
.associateBy { it.identifier }
|
||||
}
|
||||
}
|
||||
+46
@@ -0,0 +1,46 @@
|
||||
package io.novafoundation.nova.feature_swap_core.data.assetExchange.conversion.types.hydra
|
||||
|
||||
import io.novafoundation.nova.common.utils.mapNotNullToSet
|
||||
import io.novafoundation.nova.common.utils.metadata
|
||||
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.types.hydra.HydrationAcceptedFeeCurrenciesFetcher
|
||||
import io.novafoundation.nova.runtime.di.REMOTE_STORAGE_SOURCE
|
||||
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain
|
||||
import io.novafoundation.nova.runtime.multiNetwork.chain.model.ChainAssetId
|
||||
import io.novafoundation.nova.runtime.storage.source.StorageDataSource
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Named
|
||||
|
||||
internal class RealHydrationAcceptedFeeCurrenciesFetcher @Inject constructor(
|
||||
@Named(REMOTE_STORAGE_SOURCE) private val remoteStorage: StorageDataSource,
|
||||
private val hydraDxAssetIdConverter: HydraDxAssetIdConverter
|
||||
) : HydrationAcceptedFeeCurrenciesFetcher {
|
||||
|
||||
override suspend fun fetchAcceptedFeeCurrencies(chain: Chain): Result<Set<ChainAssetId>> {
|
||||
return runCatching {
|
||||
val acceptedOnChainIds = remoteStorage.query(chain.id) {
|
||||
metadata.multiTransactionPayment.acceptedCurrencies.keys()
|
||||
}
|
||||
|
||||
val onChainToLocalIds = hydraDxAssetIdConverter.allOnChainIds(chain)
|
||||
|
||||
acceptedOnChainIds.mapNotNullToSet { onChainToLocalIds[it]?.id }
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun isAcceptedCurrency(chainAsset: Chain.Asset): Result<Boolean> {
|
||||
return runCatching {
|
||||
val onChainId = hydraDxAssetIdConverter.toOnChainIdOrThrow(chainAsset)
|
||||
|
||||
if (hydraDxAssetIdConverter.isSystemAsset(onChainId)) return@runCatching true
|
||||
|
||||
val fallbackPrice = remoteStorage.query(chainAsset.chainId) {
|
||||
metadata.multiTransactionPayment.acceptedCurrencies.query(onChainId)
|
||||
}
|
||||
|
||||
fallbackPrice != null
|
||||
}
|
||||
}
|
||||
}
|
||||
+39
@@ -0,0 +1,39 @@
|
||||
package io.novafoundation.nova.feature_swap_core.data.assetExchange.conversion.types.hydra
|
||||
|
||||
import io.novafoundation.nova.common.data.network.runtime.binding.BalanceOf
|
||||
import io.novafoundation.nova.common.di.scope.FeatureScope
|
||||
import io.novafoundation.nova.common.utils.BigRational
|
||||
import io.novafoundation.nova.common.utils.fixedU128
|
||||
import io.novafoundation.nova.common.utils.metadata
|
||||
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.types.hydra.HydrationPriceConversionFallback
|
||||
import io.novafoundation.nova.runtime.di.REMOTE_STORAGE_SOURCE
|
||||
import io.novafoundation.nova.runtime.ext.isUtilityAsset
|
||||
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain
|
||||
import io.novafoundation.nova.runtime.storage.source.StorageDataSource
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Named
|
||||
|
||||
@FeatureScope
|
||||
internal class RealHydrationPriceConversionFallback @Inject constructor(
|
||||
private val hydraDxAssetIdConverter: HydraDxAssetIdConverter,
|
||||
@Named(REMOTE_STORAGE_SOURCE)
|
||||
private val remoteStorageSource: StorageDataSource,
|
||||
) : HydrationPriceConversionFallback {
|
||||
|
||||
override suspend fun convertNativeAmount(amount: BalanceOf, conversionTarget: Chain.Asset): BalanceOf {
|
||||
if (conversionTarget.isUtilityAsset) return amount
|
||||
|
||||
val targetOnChainId = hydraDxAssetIdConverter.toOnChainIdOrThrow(conversionTarget)
|
||||
|
||||
val fallbackPrice = remoteStorageSource.query(conversionTarget.chainId) {
|
||||
metadata.multiTransactionPayment.acceptedCurrencies.query(targetOnChainId)
|
||||
} ?: error("No fallback price found")
|
||||
|
||||
val fallbackPriceFractional = BigRational.fixedU128(fallbackPrice).quotient
|
||||
val converted = fallbackPriceFractional * amount.toBigDecimal()
|
||||
|
||||
return converted.toBigInteger()
|
||||
}
|
||||
}
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
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
|
||||
|
||||
interface HydraDxQuotableEdge : QuotableEdge
|
||||
+45
@@ -0,0 +1,45 @@
|
||||
package io.novafoundation.nova.feature_swap_core.data.assetExchange.conversion.types.hydra.sources
|
||||
|
||||
import io.novafoundation.nova.common.utils.graph.Path
|
||||
import io.novafoundation.nova.feature_swap_core_api.data.primitive.model.QuotableEdge
|
||||
|
||||
object Weights {
|
||||
|
||||
object Hydra {
|
||||
|
||||
fun weightAppendingToPath(path: Path<*>, baseWeight: Int): Int {
|
||||
// Significantly reduce weight of consequent hydration segments since they are collapsed into single tx
|
||||
return if (path.isNotEmpty() && path.last() is HydraDxQuotableEdge) {
|
||||
// We divide here by 10 to achieve two goals:
|
||||
// 1. Divisor should be significant enough to allow multiple appended segments to be added without influencing total hydration weight much
|
||||
// 2. On the other hand, divisor cannot be extremely large as we will loose precision and it wont be possible
|
||||
// to distinguish different hydration segments weights between each other.
|
||||
// That is also why OMNIPOOL, STABLESWAP and XYK differ by a multiple of ten
|
||||
(baseWeight / 10)
|
||||
} else {
|
||||
baseWeight
|
||||
}
|
||||
}
|
||||
|
||||
const val OMNIPOOL = QuotableEdge.DEFAULT_SEGMENT_WEIGHT
|
||||
|
||||
const val STABLESWAP = QuotableEdge.DEFAULT_SEGMENT_WEIGHT - 10
|
||||
|
||||
const val XYK = QuotableEdge.DEFAULT_SEGMENT_WEIGHT + 10
|
||||
|
||||
const val AAVE = STABLESWAP
|
||||
}
|
||||
|
||||
object AssetConversion {
|
||||
|
||||
// Asset conversion pools liquidity, they are unfavourable
|
||||
// We do x3 to allow heuristics to find routes with 3 cross-chain to be ranked even higher prioritize
|
||||
// Search via Hydration
|
||||
const val SWAP = 3 * CrossChainTransfer.TRANSFER + 10
|
||||
}
|
||||
|
||||
object CrossChainTransfer {
|
||||
|
||||
const val TRANSFER = QuotableEdge.DEFAULT_SEGMENT_WEIGHT
|
||||
}
|
||||
}
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
package io.novafoundation.nova.feature_swap_core.data.assetExchange.conversion.types.hydra.sources.aave
|
||||
|
||||
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
|
||||
|
||||
interface AavePoolQuotingSource : HydraDxQuotingSource<AavePoolQuotingSource.Edge> {
|
||||
|
||||
interface Edge : QuotableEdge {
|
||||
|
||||
val fromAsset: RemoteAndLocalId
|
||||
|
||||
val toAsset: RemoteAndLocalId
|
||||
}
|
||||
}
|
||||
+190
@@ -0,0 +1,190 @@
|
||||
package io.novafoundation.nova.feature_swap_core.data.assetExchange.conversion.types.hydra.sources.aave
|
||||
|
||||
import android.util.Log
|
||||
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.castToList
|
||||
import io.novafoundation.nova.common.data.network.runtime.binding.castToStruct
|
||||
import io.novafoundation.nova.common.di.scope.FeatureScope
|
||||
import io.novafoundation.nova.common.utils.LOG_TAG
|
||||
import io.novafoundation.nova.common.utils.graph.Path
|
||||
import io.novafoundation.nova.common.utils.graph.WeightedEdge
|
||||
import io.novafoundation.nova.common.utils.singleReplaySharedFlow
|
||||
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.Weights.Hydra.weightAppendingToPath
|
||||
import io.novafoundation.nova.feature_swap_core.data.assetExchange.conversion.types.hydra.sources.aave.model.AavePool
|
||||
import io.novafoundation.nova.feature_swap_core.data.assetExchange.conversion.types.hydra.sources.aave.model.AavePools
|
||||
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.matchId
|
||||
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.SwapQuoting
|
||||
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.call.MultiChainRuntimeCallsApi
|
||||
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.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.coroutineScope
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.flow.map
|
||||
import java.math.BigInteger
|
||||
import javax.inject.Inject
|
||||
|
||||
@FeatureScope
|
||||
class AaveSwapQuotingSourceFactory @Inject constructor(
|
||||
private val hydraDxAssetIdConverter: HydraDxAssetIdConverter,
|
||||
private val multiChainRuntimeCallsApi: MultiChainRuntimeCallsApi,
|
||||
) : HydraDxQuotingSource.Factory<AavePoolQuotingSource> {
|
||||
|
||||
companion object {
|
||||
|
||||
const val ID = "Aave"
|
||||
}
|
||||
|
||||
override fun create(chain: Chain, host: SwapQuoting.QuotingHost): AavePoolQuotingSource {
|
||||
return RealAaveSwapQuotingSource(
|
||||
hydraDxAssetIdConverter = hydraDxAssetIdConverter,
|
||||
multiChainRuntimeCallsApi = multiChainRuntimeCallsApi,
|
||||
chain = chain,
|
||||
host = host
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private class RealAaveSwapQuotingSource(
|
||||
private val hydraDxAssetIdConverter: HydraDxAssetIdConverter,
|
||||
private val multiChainRuntimeCallsApi: MultiChainRuntimeCallsApi,
|
||||
private val chain: Chain,
|
||||
private val host: SwapQuoting.QuotingHost,
|
||||
) : AavePoolQuotingSource {
|
||||
|
||||
override val identifier: String = AaveSwapQuotingSourceFactory.ID
|
||||
|
||||
private val initialPoolsInfo: MutableSharedFlow<Collection<AavePoolInitialInfo>> = singleReplaySharedFlow()
|
||||
|
||||
private val aavePools: MutableSharedFlow<AavePools> = singleReplaySharedFlow()
|
||||
|
||||
override suspend fun sync() {
|
||||
val pairs = getPairs()
|
||||
|
||||
val poolInitialInfo = pairs.matchIdsWithLocal()
|
||||
initialPoolsInfo.emit(poolInitialInfo)
|
||||
}
|
||||
|
||||
override suspend fun availableSwapDirections(): Collection<AavePoolQuotingSource.Edge> {
|
||||
val poolInitialInfo = initialPoolsInfo.first()
|
||||
|
||||
return poolInitialInfo.allPossibleDirections()
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
override suspend fun runSubscriptions(
|
||||
userAccountId: AccountId,
|
||||
subscriptionBuilder: SharedRequestsBuilder
|
||||
): Flow<Unit> = coroutineScope {
|
||||
aavePools.resetReplayCache()
|
||||
|
||||
host.sharedSubscriptions.blockNumber(chain.id).map {
|
||||
val pools = getPools()
|
||||
aavePools.emit(pools)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun getPairs(): List<AavePoolPair> {
|
||||
return runCatching {
|
||||
multiChainRuntimeCallsApi.forChain(chain.id).call(
|
||||
section = "AaveTradeExecutor",
|
||||
method = "pairs",
|
||||
arguments = emptyMap(),
|
||||
returnBinding = ::bindPairs
|
||||
)
|
||||
}.onFailure { Log.d(LOG_TAG, "Failed to get aave pairs", it) }
|
||||
.getOrDefault(emptyList())
|
||||
}
|
||||
|
||||
private suspend fun getPools(): AavePools {
|
||||
return multiChainRuntimeCallsApi.forChain(chain.id).call(
|
||||
section = "AaveTradeExecutor",
|
||||
method = "pools",
|
||||
arguments = emptyMap(),
|
||||
returnBinding = ::bindPools
|
||||
)
|
||||
}
|
||||
|
||||
private suspend fun List<AavePoolPair>.matchIdsWithLocal(): List<AavePoolInitialInfo> {
|
||||
val allOnChainIds = hydraDxAssetIdConverter.allOnChainIds(chain)
|
||||
|
||||
return mapNotNull { poolInfo ->
|
||||
AavePoolInitialInfo(
|
||||
firstAsset = allOnChainIds.matchId(poolInfo.firstAsset) ?: return@mapNotNull null,
|
||||
secondAsset = allOnChainIds.matchId(poolInfo.secondAsset) ?: return@mapNotNull null,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun Collection<AavePoolInitialInfo>.allPossibleDirections(): Collection<RealXYKSwapQuotingEdge> {
|
||||
return buildList {
|
||||
this@allPossibleDirections.forEach { poolInfo ->
|
||||
add(RealXYKSwapQuotingEdge(fromAsset = poolInfo.firstAsset, toAsset = poolInfo.secondAsset))
|
||||
add(RealXYKSwapQuotingEdge(fromAsset = poolInfo.secondAsset, toAsset = poolInfo.firstAsset))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
inner class RealXYKSwapQuotingEdge(
|
||||
override val fromAsset: RemoteAndLocalId,
|
||||
override val toAsset: RemoteAndLocalId,
|
||||
) : AavePoolQuotingSource.Edge {
|
||||
|
||||
override val from: FullChainAssetId = fromAsset.second
|
||||
|
||||
override val to: FullChainAssetId = toAsset.second
|
||||
|
||||
override fun weightForAppendingTo(path: Path<WeightedEdge<FullChainAssetId>>): Int {
|
||||
return weightAppendingToPath(path, Weights.Hydra.AAVE)
|
||||
}
|
||||
|
||||
override suspend fun quote(amount: BigInteger, direction: SwapDirection): BigInteger {
|
||||
val allPools = aavePools.first()
|
||||
|
||||
return allPools.quote(fromAsset.first, toAsset.first, amount, direction)
|
||||
?: throw SwapQuoteException.NotEnoughLiquidity
|
||||
}
|
||||
}
|
||||
|
||||
private fun bindPairs(decoded: Any?): List<AavePoolPair> {
|
||||
return bindList(decoded) { item ->
|
||||
val (first, second) = item.castToList()
|
||||
AavePoolPair(bindNumber(first), bindNumber(second))
|
||||
}
|
||||
}
|
||||
|
||||
private fun bindPools(decoded: Any?): AavePools {
|
||||
val pools = bindList(decoded, ::bindPool)
|
||||
return AavePools(pools)
|
||||
}
|
||||
|
||||
private fun bindPool(decoded: Any?): AavePool {
|
||||
val asStruct = decoded.castToStruct()
|
||||
|
||||
return AavePool(
|
||||
reserve = bindNumber(asStruct["reserve"]),
|
||||
atoken = bindNumber(asStruct["atoken"]),
|
||||
liqudityIn = bindNumber(asStruct["liqudityIn"]),
|
||||
liquidityOut = bindNumber(asStruct["liqudityOut"])
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private class AavePoolPair(val firstAsset: HydraDxAssetId, val secondAsset: HydraDxAssetId)
|
||||
|
||||
private class AavePoolInitialInfo(
|
||||
val firstAsset: RemoteAndLocalId,
|
||||
val secondAsset: RemoteAndLocalId
|
||||
)
|
||||
+79
@@ -0,0 +1,79 @@
|
||||
package io.novafoundation.nova.feature_swap_core.data.assetExchange.conversion.types.hydra.sources.aave.model
|
||||
|
||||
import io.novafoundation.nova.common.data.network.runtime.binding.BalanceOf
|
||||
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
|
||||
|
||||
data class AavePools(
|
||||
val pools: List<AavePool>
|
||||
) {
|
||||
|
||||
fun quote(
|
||||
assetIdIn: HydraDxAssetId,
|
||||
assetIdOut: HydraDxAssetId,
|
||||
amount: BigInteger,
|
||||
direction: SwapDirection
|
||||
): BalanceOf? {
|
||||
val pool = findPool(assetIdIn, assetIdOut) ?: return null
|
||||
|
||||
return pool.quote(assetIdOut, amount, direction)
|
||||
}
|
||||
|
||||
private fun findPool(assetIdIn: HydraDxAssetId, assetIdOut: HydraDxAssetId): AavePool? {
|
||||
return pools.find { it.canHandleTrade(assetIdIn, assetIdOut) }
|
||||
}
|
||||
}
|
||||
|
||||
data class AavePool(
|
||||
val reserve: HydraDxAssetId,
|
||||
val atoken: HydraDxAssetId,
|
||||
val liqudityIn: BalanceOf,
|
||||
val liquidityOut: BalanceOf
|
||||
) {
|
||||
|
||||
fun canHandleTrade(assetIdIn: HydraDxAssetId, assetIdOut: HydraDxAssetId): Boolean {
|
||||
return findPoolTokenLiquidity(assetIdIn) != null && findPoolTokenLiquidity(assetIdOut) != null
|
||||
}
|
||||
|
||||
fun quote(
|
||||
assetIdOut: HydraDxAssetId,
|
||||
amount: BigInteger,
|
||||
direction: SwapDirection
|
||||
): BalanceOf? {
|
||||
return when (direction) {
|
||||
SwapDirection.SPECIFIED_IN -> calculateOutGivenIn(assetIdOut, amount)
|
||||
SwapDirection.SPECIFIED_OUT -> calculateInGivenOut(assetIdOut, amount)
|
||||
}
|
||||
}
|
||||
|
||||
// Here and in calculateInGivenOut we always validate amount out (either specified or calculated) against
|
||||
// assetIdOut liquidity since that's the asset that will be removed from the pool
|
||||
private fun calculateOutGivenIn(
|
||||
assetIdOut: HydraDxAssetId,
|
||||
amountIn: BigInteger,
|
||||
): BalanceOf? {
|
||||
val calculatedOut = amountIn
|
||||
val liquidityOut = findPoolTokenLiquidity(assetIdOut) ?: return null
|
||||
|
||||
return calculatedOut.takeIf { calculatedOut <= liquidityOut }
|
||||
}
|
||||
|
||||
private fun calculateInGivenOut(
|
||||
assetIdOut: HydraDxAssetId,
|
||||
amountOut: BigInteger,
|
||||
): BalanceOf? {
|
||||
val calculatedIn = amountOut
|
||||
val liquidityOut = findPoolTokenLiquidity(assetIdOut) ?: return null
|
||||
|
||||
return calculatedIn.takeIf { amountOut <= liquidityOut }
|
||||
}
|
||||
|
||||
private fun findPoolTokenLiquidity(assetId: HydraDxAssetId): BalanceOf? {
|
||||
return when (assetId) {
|
||||
reserve -> liqudityIn
|
||||
atoken -> liquidityOut
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
}
|
||||
+37
@@ -0,0 +1,37 @@
|
||||
package io.novafoundation.nova.feature_swap_core.data.assetExchange.conversion.types.hydra.sources.common
|
||||
|
||||
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_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
|
||||
import io.novafoundation.nova.runtime.storage.source.query.api.storage1
|
||||
import io.novasama.substrate_sdk_android.runtime.definitions.types.composite.DictEnum
|
||||
import io.novasama.substrate_sdk_android.runtime.metadata.RuntimeMetadata
|
||||
import io.novasama.substrate_sdk_android.runtime.metadata.module.Module
|
||||
|
||||
@JvmInline
|
||||
value class AssetRegistryApi(override val module: Module) : QueryableModule
|
||||
|
||||
context(StorageQueryContext)
|
||||
val RuntimeMetadata.assetRegistry: AssetRegistryApi
|
||||
get() = AssetRegistryApi(assetRegistry())
|
||||
|
||||
context(StorageQueryContext)
|
||||
val AssetRegistryApi.assets: QueryableStorageEntry1<HydraDxAssetId, HydrationAssetMetadata>
|
||||
get() = storage1(name = "Assets", binding = ::bindHydrationAssetMetadata)
|
||||
|
||||
private fun bindHydrationAssetMetadata(
|
||||
decoded: Any,
|
||||
assetId: HydraDxAssetId
|
||||
): HydrationAssetMetadata {
|
||||
val asStruct = decoded.castToStruct()
|
||||
|
||||
return HydrationAssetMetadata(
|
||||
assetId = assetId,
|
||||
decimals = bindInt(asStruct["decimals"]),
|
||||
assetType = asStruct.get<DictEnum.Entry<*>>("assetType")!!.name
|
||||
)
|
||||
}
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
package io.novafoundation.nova.feature_swap_core.data.assetExchange.conversion.types.hydra.sources.common
|
||||
|
||||
import io.novafoundation.nova.feature_swap_core_api.data.network.HydraDxAssetId
|
||||
|
||||
class HydrationAssetMetadata(
|
||||
val assetId: HydraDxAssetId,
|
||||
val decimals: Int,
|
||||
val assetType: String
|
||||
) {
|
||||
|
||||
fun determineAssetType(nativeId: HydraDxAssetId): HydrationAssetType {
|
||||
return when {
|
||||
assetId == nativeId -> HydrationAssetType.Native
|
||||
assetType == "Erc20" -> HydrationAssetType.Erc20(assetId)
|
||||
else -> HydrationAssetType.Orml(assetId)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class HydrationAssetMetadataMap(
|
||||
private val nativeId: HydraDxAssetId,
|
||||
private val metadataMap: Map<HydraDxAssetId, HydrationAssetMetadata>
|
||||
) {
|
||||
|
||||
fun getAssetType(assetId: HydraDxAssetId): HydrationAssetType? {
|
||||
val metadata = metadataMap[assetId] ?: return null
|
||||
|
||||
return metadata.determineAssetType(nativeId)
|
||||
}
|
||||
|
||||
fun getDecimals(assetId: HydraDxAssetId): Int? {
|
||||
val metadata = metadataMap[assetId] ?: return null
|
||||
|
||||
return metadata.decimals
|
||||
}
|
||||
}
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
package io.novafoundation.nova.feature_swap_core.data.assetExchange.conversion.types.hydra.sources.common
|
||||
|
||||
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.Chain.Asset.Type.Orml.SubType
|
||||
|
||||
sealed class HydrationAssetType {
|
||||
|
||||
companion object;
|
||||
|
||||
data object Native : HydrationAssetType()
|
||||
|
||||
class Orml(val assetId: HydraDxAssetId) : HydrationAssetType()
|
||||
|
||||
class Erc20(val assetId: HydraDxAssetId) : HydrationAssetType()
|
||||
}
|
||||
|
||||
fun HydrationAssetType.Companion.fromAsset(chainAsset: Chain.Asset, hydrationAssetId: HydraDxAssetId): HydrationAssetType {
|
||||
return when (val type = chainAsset.type) {
|
||||
is Chain.Asset.Type.Native -> HydrationAssetType.Native
|
||||
is Chain.Asset.Type.Orml -> when (type.subType) {
|
||||
SubType.DEFAULT -> HydrationAssetType.Orml(hydrationAssetId)
|
||||
SubType.HYDRATION_EVM -> HydrationAssetType.Erc20(hydrationAssetId)
|
||||
}
|
||||
|
||||
else -> throw IllegalArgumentException("Unsupported asset type: ${chainAsset.type}")
|
||||
}
|
||||
}
|
||||
+157
@@ -0,0 +1,157 @@
|
||||
package io.novafoundation.nova.feature_swap_core.data.assetExchange.conversion.types.hydra.sources.common
|
||||
|
||||
import io.novafoundation.nova.common.data.network.ext.transferableBalance
|
||||
import io.novafoundation.nova.common.data.network.runtime.binding.AccountBalance
|
||||
import io.novafoundation.nova.common.data.network.runtime.binding.AccountInfo
|
||||
import io.novafoundation.nova.common.data.network.runtime.binding.bindNumber
|
||||
import io.novafoundation.nova.common.data.network.runtime.binding.bindOrmlAccountBalanceOrEmpty
|
||||
import io.novafoundation.nova.common.data.network.runtime.binding.castToStruct
|
||||
import io.novafoundation.nova.common.di.scope.FeatureScope
|
||||
import io.novafoundation.nova.common.domain.balance.TransferableMode
|
||||
import io.novafoundation.nova.common.domain.balance.calculateTransferable
|
||||
import io.novafoundation.nova.common.utils.metadata
|
||||
import io.novafoundation.nova.common.utils.tokens
|
||||
import io.novafoundation.nova.core.updater.SharedRequestsBuilder
|
||||
import io.novafoundation.nova.feature_swap_core_api.data.network.HydraDxAssetId
|
||||
import io.novafoundation.nova.feature_swap_core_api.data.primitive.SwapQuoting
|
||||
import io.novafoundation.nova.runtime.call.MultiChainRuntimeCallsApi
|
||||
import io.novafoundation.nova.runtime.call.RuntimeCallsApi
|
||||
import io.novafoundation.nova.runtime.di.REMOTE_STORAGE_SOURCE
|
||||
import io.novafoundation.nova.runtime.multiNetwork.chain.model.ChainId
|
||||
import io.novafoundation.nova.runtime.storage.source.StorageDataSource
|
||||
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 kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.flow
|
||||
import kotlinx.coroutines.flow.map
|
||||
import java.math.BigInteger
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Named
|
||||
|
||||
/**
|
||||
* This is a simplified version of [AssetBalanceSource] which we use here because usage of AssetBalanceSource
|
||||
* would create a circular dependency
|
||||
*
|
||||
* TODO fix this: balances should be extracted to a separate module to allow better reusability
|
||||
*/
|
||||
interface HydrationBalanceFetcher {
|
||||
|
||||
suspend fun subscribeToTransferableBalance(
|
||||
chainId: ChainId,
|
||||
type: HydrationAssetType,
|
||||
accountId: AccountId,
|
||||
subscriptionBuilder: SharedRequestsBuilder
|
||||
): Flow<BigInteger>
|
||||
}
|
||||
|
||||
@FeatureScope
|
||||
class HydrationBalanceFetcherFactory @Inject constructor(
|
||||
@Named(REMOTE_STORAGE_SOURCE)
|
||||
private val remoteStorageDataSource: StorageDataSource,
|
||||
private val multiChainRuntimeCallsApi: MultiChainRuntimeCallsApi
|
||||
) {
|
||||
|
||||
fun create(swapHost: SwapQuoting.QuotingHost): HydrationBalanceFetcher {
|
||||
return RealHydrationBalanceFetcher(remoteStorageDataSource, multiChainRuntimeCallsApi, swapHost)
|
||||
}
|
||||
}
|
||||
|
||||
class RealHydrationBalanceFetcher(
|
||||
private val remoteStorageDataSource: StorageDataSource,
|
||||
private val runtimeCallsApi: MultiChainRuntimeCallsApi,
|
||||
private val swapHost: SwapQuoting.QuotingHost,
|
||||
) : HydrationBalanceFetcher {
|
||||
|
||||
override suspend fun subscribeToTransferableBalance(
|
||||
chainId: ChainId,
|
||||
type: HydrationAssetType,
|
||||
accountId: AccountId,
|
||||
subscriptionBuilder: SharedRequestsBuilder
|
||||
): Flow<BigInteger> {
|
||||
return when (type) {
|
||||
is HydrationAssetType.Native -> subscribeNativeAssetBalance(chainId, accountId, subscriptionBuilder)
|
||||
is HydrationAssetType.Orml -> subscribeOrmlAssetBalance(chainId, type.assetId, accountId, subscriptionBuilder)
|
||||
is HydrationAssetType.Erc20 -> subscribeErc20AssetBalance(chainId, type.assetId, accountId)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun subscribeNativeAssetBalance(
|
||||
chainId: ChainId,
|
||||
poolAccountId: AccountId,
|
||||
subscriptionBuilder: SharedRequestsBuilder
|
||||
): Flow<BigInteger> {
|
||||
return remoteStorageDataSource.subscribe(chainId, subscriptionBuilder) {
|
||||
metadata.system.account.observe(poolAccountId).map {
|
||||
val accountInfo = it ?: AccountInfo.empty()
|
||||
|
||||
accountInfo.transferableBalance()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun subscribeOrmlAssetBalance(
|
||||
chainId: ChainId,
|
||||
hydrationAssetId: HydraDxAssetId,
|
||||
poolAccountId: AccountId,
|
||||
subscriptionBuilder: SharedRequestsBuilder
|
||||
): Flow<BigInteger> {
|
||||
return remoteStorageDataSource.subscribe(chainId, subscriptionBuilder) {
|
||||
metadata.tokens().storage("Accounts").observe(
|
||||
poolAccountId,
|
||||
hydrationAssetId,
|
||||
binding = ::bindOrmlAccountBalanceOrEmpty
|
||||
).map {
|
||||
TransferableMode.REGULAR.calculateTransferable(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun subscribeErc20AssetBalance(
|
||||
chainId: ChainId,
|
||||
hydrationAssetId: HydraDxAssetId,
|
||||
accountId: AccountId,
|
||||
): Flow<BigInteger> {
|
||||
val blockNumberFlow = swapHost.sharedSubscriptions.blockNumber(chainId)
|
||||
|
||||
return flow {
|
||||
val initialBalance = fetchBalance(chainId, hydrationAssetId, accountId)
|
||||
emit(initialBalance)
|
||||
|
||||
blockNumberFlow.collect {
|
||||
val newBalance = fetchBalance(chainId, hydrationAssetId, accountId)
|
||||
emit(newBalance)
|
||||
}
|
||||
}
|
||||
.map { TransferableMode.REGULAR.calculateTransferable(it) }
|
||||
.distinctUntilChanged()
|
||||
}
|
||||
|
||||
private suspend fun fetchBalance(chainId: ChainId, hydrationAssetId: HydraDxAssetId, accountId: AccountId): AccountBalance {
|
||||
return runtimeCallsApi.forChain(chainId).fetchBalance(hydrationAssetId, accountId)
|
||||
}
|
||||
|
||||
private suspend fun RuntimeCallsApi.fetchBalance(hydrationAssetId: HydraDxAssetId, accountId: AccountId): AccountBalance {
|
||||
return call(
|
||||
section = "CurrenciesApi",
|
||||
method = "account",
|
||||
arguments = mapOf(
|
||||
"asset_id" to hydrationAssetId,
|
||||
"who" to accountId
|
||||
),
|
||||
returnBinding = { bindAssetBalance(it) }
|
||||
)
|
||||
}
|
||||
|
||||
private fun bindAssetBalance(decoded: Any?): AccountBalance {
|
||||
val asStruct = decoded.castToStruct()
|
||||
|
||||
return AccountBalance(
|
||||
free = bindNumber(asStruct["free"]),
|
||||
frozen = bindNumber(asStruct["frozen"]),
|
||||
reserved = bindNumber(asStruct["reserved"]),
|
||||
)
|
||||
}
|
||||
}
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
@file:Suppress("RedundantUnitExpression")
|
||||
|
||||
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.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
|
||||
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 DynamicFeesApi(override val module: Module) : QueryableModule
|
||||
|
||||
context(StorageQueryContext)
|
||||
val RuntimeMetadata.dynamicFeesApi: DynamicFeesApi
|
||||
get() = DynamicFeesApi(dynamicFees())
|
||||
|
||||
context(StorageQueryContext)
|
||||
val DynamicFeesApi.assetFee: QueryableStorageEntry1<HydraDxAssetId, DynamicFee>
|
||||
get() = storage1(
|
||||
name = "AssetFee",
|
||||
binding = { decoded, _ -> bindDynamicFee(decoded) },
|
||||
)
|
||||
+8
@@ -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)
|
||||
}
|
||||
+15
@@ -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<OmniPoolQuotingSource.Edge> {
|
||||
|
||||
interface Edge : QuotableEdge {
|
||||
|
||||
val fromAsset: RemoteIdAndLocalAsset
|
||||
|
||||
val toAsset: RemoteIdAndLocalAsset
|
||||
}
|
||||
}
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
@file:Suppress("RedundantUnitExpression")
|
||||
|
||||
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.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
|
||||
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 OmnipoolApi(override val module: Module) : QueryableModule
|
||||
|
||||
context(StorageQueryContext)
|
||||
val RuntimeMetadata.omnipoolOrNull: OmnipoolApi?
|
||||
get() = omnipoolOrNull()?.let(::OmnipoolApi)
|
||||
|
||||
context(StorageQueryContext)
|
||||
val RuntimeMetadata.omnipool: OmnipoolApi
|
||||
get() = OmnipoolApi(omnipool())
|
||||
|
||||
context(StorageQueryContext)
|
||||
val OmnipoolApi.assets: QueryableStorageEntry1<HydraDxAssetId, OmnipoolAssetState>
|
||||
get() = storage1(
|
||||
name = "Assets",
|
||||
binding = ::bindOmnipoolAssetState,
|
||||
)
|
||||
+236
@@ -0,0 +1,236 @@
|
||||
package io.novafoundation.nova.feature_swap_core.data.assetExchange.conversion.types.hydra.sources.omnipool
|
||||
|
||||
import io.novafoundation.nova.common.di.scope.FeatureScope
|
||||
import io.novafoundation.nova.common.utils.dynamicFees
|
||||
import io.novafoundation.nova.common.utils.graph.Path
|
||||
import io.novafoundation.nova.common.utils.graph.WeightedEdge
|
||||
import io.novafoundation.nova.common.utils.metadata
|
||||
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.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.common.HydrationAssetType
|
||||
import io.novafoundation.nova.feature_swap_core.data.assetExchange.conversion.types.hydra.sources.common.HydrationBalanceFetcher
|
||||
import io.novafoundation.nova.feature_swap_core.data.assetExchange.conversion.types.hydra.sources.common.HydrationBalanceFetcherFactory
|
||||
import io.novafoundation.nova.feature_swap_core.data.assetExchange.conversion.types.hydra.sources.common.fromAsset
|
||||
import io.novafoundation.nova.feature_swap_core.data.assetExchange.conversion.types.hydra.sources.Weights.Hydra.weightAppendingToPath
|
||||
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_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.SwapQuoting
|
||||
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.di.REMOTE_STORAGE_SOURCE
|
||||
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.novasama.substrate_sdk_android.runtime.AccountId
|
||||
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
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Named
|
||||
|
||||
@FeatureScope
|
||||
class OmniPoolQuotingSourceFactory @Inject constructor(
|
||||
@Named(REMOTE_STORAGE_SOURCE)
|
||||
private val remoteStorageSource: StorageDataSource,
|
||||
private val chainRegistry: ChainRegistry,
|
||||
private val hydraDxAssetIdConverter: HydraDxAssetIdConverter,
|
||||
private val hydrationBalanceFetcherFactory: HydrationBalanceFetcherFactory
|
||||
) : HydraDxQuotingSource.Factory<OmniPoolQuotingSource> {
|
||||
|
||||
companion object {
|
||||
|
||||
const val SOURCE_ID = "OmniPool"
|
||||
}
|
||||
|
||||
override fun create(chain: Chain, host: SwapQuoting.QuotingHost): OmniPoolQuotingSource {
|
||||
return RealOmniPoolQuotingSource(
|
||||
remoteStorageSource = remoteStorageSource,
|
||||
chainRegistry = chainRegistry,
|
||||
hydraDxAssetIdConverter = hydraDxAssetIdConverter,
|
||||
hydrationBalanceFetcher = hydrationBalanceFetcherFactory.create(host),
|
||||
chain = chain,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private class RealOmniPoolQuotingSource(
|
||||
private val remoteStorageSource: StorageDataSource,
|
||||
private val chainRegistry: ChainRegistry,
|
||||
private val hydraDxAssetIdConverter: HydraDxAssetIdConverter,
|
||||
private val hydrationBalanceFetcher: HydrationBalanceFetcher,
|
||||
private val chain: Chain,
|
||||
) : OmniPoolQuotingSource {
|
||||
|
||||
override val identifier = OmniPoolQuotingSourceFactory.SOURCE_ID
|
||||
|
||||
private val pooledOnChainAssetIdsState: MutableSharedFlow<List<RemoteIdAndLocalAsset>> = singleReplaySharedFlow()
|
||||
|
||||
private val omniPoolFlow: MutableSharedFlow<OmniPool> = singleReplaySharedFlow()
|
||||
|
||||
override suspend fun sync() {
|
||||
val pooledOnChainAssetIds = getPooledOnChainAssetIds()
|
||||
|
||||
val pooledChainAssetsIds = matchKnownChainAssetIds(pooledOnChainAssetIds)
|
||||
pooledOnChainAssetIdsState.emit(pooledChainAssetsIds)
|
||||
}
|
||||
|
||||
override suspend fun availableSwapDirections(): Collection<OmniPoolQuotingSource.Edge> {
|
||||
val pooledOnChainAssetIds = pooledOnChainAssetIdsState.first()
|
||||
|
||||
return pooledOnChainAssetIds.flatMap { remoteAndLocal ->
|
||||
pooledOnChainAssetIds.mapNotNull { otherRemoteAndLocal ->
|
||||
// In OmniPool, each asset is tradable with any other except itself
|
||||
if (remoteAndLocal.second.id != otherRemoteAndLocal.second.id) {
|
||||
RealOmniPoolQuotingEdge(fromAsset = remoteAndLocal, toAsset = otherRemoteAndLocal)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
override suspend fun runSubscriptions(
|
||||
userAccountId: AccountId,
|
||||
subscriptionBuilder: SharedRequestsBuilder
|
||||
): Flow<Unit> {
|
||||
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 hydrationAssetType = HydrationAssetType.fromAsset(chainAsset, omniPoolTokenId)
|
||||
|
||||
hydrationBalanceFetcher.subscribeToTransferableBalance(chainAsset.chainId, hydrationAssetType, 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<BigInteger> {
|
||||
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<HydraDxAssetId>): List<RemoteIdAndLocalAsset> {
|
||||
val hydraDxAssetIds = hydraDxAssetIdConverter.allOnChainIds(chain)
|
||||
|
||||
return onChainIds.mapNotNull { onChainId ->
|
||||
val asset = hydraDxAssetIds[onChainId] ?: return@mapNotNull null
|
||||
|
||||
onChainId to asset
|
||||
}
|
||||
}
|
||||
|
||||
private fun createOmniPool(
|
||||
poolAssetStates: Map<HydraDxAssetId, OmnipoolAssetState>,
|
||||
poolBalances: Map<HydraDxAssetId, BigInteger>,
|
||||
fees: Map<HydraDxAssetId, DynamicFee?>,
|
||||
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 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
|
||||
)
|
||||
}
|
||||
|
||||
private inner class RealOmniPoolQuotingEdge(
|
||||
override val fromAsset: RemoteIdAndLocalAsset,
|
||||
override val toAsset: RemoteIdAndLocalAsset,
|
||||
) : OmniPoolQuotingSource.Edge {
|
||||
|
||||
override val from: FullChainAssetId = fromAsset.second.fullId
|
||||
|
||||
override val to: FullChainAssetId = toAsset.second.fullId
|
||||
|
||||
override fun weightForAppendingTo(path: Path<WeightedEdge<FullChainAssetId>>): Int {
|
||||
return weightAppendingToPath(path, 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
|
||||
}
|
||||
}
|
||||
}
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
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.feature_swap_core_api.data.network.HydraRemoteToLocalMapping
|
||||
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
|
||||
|
||||
typealias RemoteAndLocalId = Pair<HydraDxAssetId, FullChainAssetId>
|
||||
typealias RemoteIdAndLocalAsset = Pair<HydraDxAssetId, Chain.Asset>
|
||||
typealias RemoteAndLocalIdOptional = Pair<HydraDxAssetId, FullChainAssetId?>
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
fun RemoteAndLocalIdOptional.flatten(): RemoteAndLocalId? {
|
||||
return second?.let { this as RemoteAndLocalId }
|
||||
}
|
||||
|
||||
val RemoteAndLocalId.remoteId
|
||||
get() = first
|
||||
|
||||
val RemoteAndLocalId.localId
|
||||
get() = second
|
||||
|
||||
fun HydraRemoteToLocalMapping.matchId(remoteId: HydraDxAssetId): RemoteAndLocalId? {
|
||||
return get(remoteId)?.fullId?.let { remoteId to it }
|
||||
}
|
||||
+39
@@ -0,0 +1,39 @@
|
||||
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
|
||||
import io.novafoundation.nova.common.utils.Perbill
|
||||
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 DynamicFee(
|
||||
val assetFee: Perbill,
|
||||
val protocolFee: Perbill
|
||||
)
|
||||
|
||||
fun bindDynamicFee(decoded: Any): DynamicFee {
|
||||
val asStruct = decoded.castToStruct()
|
||||
|
||||
return DynamicFee(
|
||||
assetFee = bindPermill(asStruct["assetFee"]),
|
||||
protocolFee = bindPermill(asStruct["protocolFee"]),
|
||||
)
|
||||
}
|
||||
|
||||
class FeeParams(
|
||||
val minFee: Perbill,
|
||||
)
|
||||
|
||||
fun bindFeeParams(decoded: Any?): FeeParams {
|
||||
val asStruct = decoded.castToStruct()
|
||||
|
||||
return FeeParams(
|
||||
minFee = bindPermill(asStruct["minFee"]),
|
||||
)
|
||||
}
|
||||
|
||||
fun Module.feeParamsConstant(name: String, runtimeSnapshot: RuntimeSnapshot): FeeParams {
|
||||
return bindFeeParams(constant(name).decoded(runtimeSnapshot))
|
||||
}
|
||||
+105
@@ -0,0 +1,105 @@
|
||||
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_api.data.network.HydraDxAssetId
|
||||
import io.novafoundation.nova.feature_swap_core_api.data.primitive.model.SwapDirection
|
||||
import java.math.BigInteger
|
||||
import kotlin.math.floor
|
||||
|
||||
class OmniPool(
|
||||
val tokens: Map<HydraDxAssetId, OmniPoolToken>,
|
||||
)
|
||||
|
||||
class OmniPoolFees(
|
||||
val protocolFee: Perbill,
|
||||
val assetFee: Perbill
|
||||
)
|
||||
|
||||
class OmniPoolToken(
|
||||
val hubReserve: BigInteger,
|
||||
val shares: BigInteger,
|
||||
val protocolShares: BigInteger,
|
||||
val tradeability: Tradeability,
|
||||
val balance: BigInteger,
|
||||
val fees: OmniPoolFees
|
||||
)
|
||||
|
||||
fun OmniPool.quote(
|
||||
assetIdIn: HydraDxAssetId,
|
||||
assetIdOut: HydraDxAssetId,
|
||||
amount: BigInteger,
|
||||
direction: SwapDirection
|
||||
): BigInteger? {
|
||||
return when (direction) {
|
||||
SwapDirection.SPECIFIED_IN -> calculateOutGivenIn(assetIdIn, assetIdOut, amount)
|
||||
SwapDirection.SPECIFIED_OUT -> calculateInGivenOut(assetIdIn, assetIdOut, amount)
|
||||
}
|
||||
}
|
||||
|
||||
fun OmniPool.calculateOutGivenIn(
|
||||
assetIdIn: HydraDxAssetId,
|
||||
assetIdOut: HydraDxAssetId,
|
||||
amountIn: BigInteger
|
||||
): BigInteger {
|
||||
val tokenInState = tokens.getValue(assetIdIn)
|
||||
val tokenOutState = tokens.getValue(assetIdOut)
|
||||
|
||||
val protocolFee = tokenInState.fees.protocolFee
|
||||
val assetFee = tokenOutState.fees.assetFee
|
||||
|
||||
val inHubReserve = tokenInState.hubReserve.toDouble()
|
||||
val inReserve = tokenInState.balance.toDouble()
|
||||
|
||||
val inAmount = amountIn.toDouble()
|
||||
|
||||
val deltaHubReserveIn = inAmount * inHubReserve / (inReserve + inAmount)
|
||||
|
||||
val protocolFeeAmount = floor(protocolFee.value * deltaHubReserveIn)
|
||||
|
||||
val deltaHubReserveOut = deltaHubReserveIn - protocolFeeAmount
|
||||
|
||||
val outReserveHp = tokenOutState.balance.toDouble()
|
||||
val outHubReserveHp = tokenOutState.hubReserve.toDouble()
|
||||
|
||||
val deltaReserveOut = outReserveHp * deltaHubReserveOut / (outHubReserveHp + deltaHubReserveOut)
|
||||
val amountOut = deltaReserveOut.deductFraction(assetFee)
|
||||
|
||||
return amountOut.toBigDecimal().toBigInteger()
|
||||
}
|
||||
|
||||
fun OmniPool.calculateInGivenOut(
|
||||
assetIdIn: HydraDxAssetId,
|
||||
assetIdOut: HydraDxAssetId,
|
||||
amountOut: BigInteger
|
||||
): BigInteger? {
|
||||
val tokenInState = tokens.getValue(assetIdIn)
|
||||
val tokenOutState = tokens.getValue(assetIdOut)
|
||||
|
||||
val protocolFee = tokenInState.fees.protocolFee
|
||||
val assetFee = tokenOutState.fees.assetFee
|
||||
|
||||
val outHubReserve = tokenOutState.hubReserve.toDouble()
|
||||
val outReserve = tokenOutState.balance.toDouble()
|
||||
|
||||
val outAmount = amountOut.toDouble()
|
||||
|
||||
val outReserveNoFee = outReserve.deductFraction(assetFee)
|
||||
|
||||
val deltaHubReserveOut = outHubReserve * outAmount / (outReserveNoFee - outAmount) + 1
|
||||
|
||||
val deltaHubReserveIn = deltaHubReserveOut / (1.0 - protocolFee.value)
|
||||
|
||||
val inHubReserveHp = tokenInState.hubReserve.toDouble()
|
||||
|
||||
if (deltaHubReserveIn >= inHubReserveHp) {
|
||||
return null
|
||||
}
|
||||
|
||||
val inReserveHp = tokenInState.balance.toDouble()
|
||||
|
||||
val deltaReserveIn = inReserveHp * deltaHubReserveIn / (inHubReserveHp - deltaHubReserveIn) + 1
|
||||
|
||||
return deltaReserveIn.takeIf { it >= 0 }?.toBigDecimal()?.toBigInteger()
|
||||
}
|
||||
|
||||
private fun Double.deductFraction(perbill: Perbill): Double = this - this * perbill.value
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
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_api.data.network.HydraDxAssetId
|
||||
import java.math.BigInteger
|
||||
|
||||
class OmnipoolAssetState(
|
||||
val tokenId: HydraDxAssetId,
|
||||
val hubReserve: BigInteger,
|
||||
val shares: BigInteger,
|
||||
val protocolShares: BigInteger,
|
||||
val tradeability: Tradeability
|
||||
)
|
||||
|
||||
fun bindOmnipoolAssetState(decoded: Any?, tokenId: HydraDxAssetId): OmnipoolAssetState {
|
||||
val struct = decoded.castToStruct()
|
||||
|
||||
return OmnipoolAssetState(
|
||||
tokenId = tokenId,
|
||||
hubReserve = bindNumber(struct["hubReserve"]),
|
||||
shares = bindNumber(struct["shares"]),
|
||||
protocolShares = bindNumber(struct["protocolShares"]),
|
||||
tradeability = bindTradeability(struct["tradable"])
|
||||
)
|
||||
}
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
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 java.math.BigInteger
|
||||
|
||||
@JvmInline
|
||||
value class Tradeability(val value: BigInteger) {
|
||||
|
||||
companion object {
|
||||
// / Asset is allowed to be sold into omnipool
|
||||
val SELL = 0b0000_0001.toBigInteger()
|
||||
|
||||
// / Asset is allowed to be bought into omnipool
|
||||
val BUY = 0b0000_0010.toBigInteger()
|
||||
}
|
||||
|
||||
fun canBuy(): Boolean = flagEnabled(BUY)
|
||||
|
||||
fun canSell(): Boolean = flagEnabled(SELL)
|
||||
|
||||
private fun flagEnabled(flag: BigInteger) = value and flag == flag
|
||||
}
|
||||
|
||||
fun bindTradeability(value: Any?): Tradeability {
|
||||
val asStruct = value.castToStruct()
|
||||
|
||||
return Tradeability(bindNumber(asStruct["bits"]))
|
||||
}
|
||||
+352
@@ -0,0 +1,352 @@
|
||||
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.BalanceOf
|
||||
import io.novafoundation.nova.common.data.network.runtime.binding.BlockNumber
|
||||
import io.novafoundation.nova.common.di.scope.FeatureScope
|
||||
import io.novafoundation.nova.common.utils.filterNotNull
|
||||
import io.novafoundation.nova.common.utils.graph.Path
|
||||
import io.novafoundation.nova.common.utils.graph.WeightedEdge
|
||||
import io.novafoundation.nova.common.utils.metadata
|
||||
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.common.HydrationAssetMetadataMap
|
||||
import io.novafoundation.nova.feature_swap_core.data.assetExchange.conversion.types.hydra.sources.common.HydrationBalanceFetcher
|
||||
import io.novafoundation.nova.feature_swap_core.data.assetExchange.conversion.types.hydra.sources.common.HydrationBalanceFetcherFactory
|
||||
import io.novafoundation.nova.feature_swap_core.data.assetExchange.conversion.types.hydra.sources.common.assetRegistry
|
||||
import io.novafoundation.nova.feature_swap_core.data.assetExchange.conversion.types.hydra.sources.common.assets
|
||||
import io.novafoundation.nova.feature_swap_core.data.assetExchange.conversion.types.hydra.sources.Weights.Hydra.weightAppendingToPath
|
||||
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.Tradeability
|
||||
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.StalbeSwapPoolPegInfo
|
||||
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.SwapQuoting
|
||||
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.di.REMOTE_STORAGE_SOURCE
|
||||
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.encrypt.json.asLittleEndianBytes
|
||||
import io.novasama.substrate_sdk_android.hash.Hasher.blake2b256
|
||||
import io.novasama.substrate_sdk_android.runtime.AccountId
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.coroutineScope
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.flow.flowOf
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import java.math.BigInteger
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Named
|
||||
import io.novafoundation.nova.common.utils.combine as combine6
|
||||
|
||||
@FeatureScope
|
||||
class StableSwapQuotingSourceFactory @Inject constructor(
|
||||
@Named(REMOTE_STORAGE_SOURCE)
|
||||
private val remoteStorageSource: StorageDataSource,
|
||||
private val hydraDxAssetIdConverter: HydraDxAssetIdConverter,
|
||||
private val hydrationBalanceFetcherFactory: HydrationBalanceFetcherFactory,
|
||||
private val gson: Gson,
|
||||
) : HydraDxQuotingSource.Factory<StableSwapQuotingSource> {
|
||||
|
||||
companion object {
|
||||
|
||||
const val ID = "StableSwap"
|
||||
}
|
||||
|
||||
override fun create(chain: Chain, host: SwapQuoting.QuotingHost): StableSwapQuotingSource {
|
||||
return RealStableSwapQuotingSource(
|
||||
remoteStorageSource = remoteStorageSource,
|
||||
hydraDxAssetIdConverter = hydraDxAssetIdConverter,
|
||||
hydrationBalanceFetcher = hydrationBalanceFetcherFactory.create(host),
|
||||
chain = chain,
|
||||
gson = gson,
|
||||
host = host
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private class RealStableSwapQuotingSource(
|
||||
private val remoteStorageSource: StorageDataSource,
|
||||
private val hydraDxAssetIdConverter: HydraDxAssetIdConverter,
|
||||
private val hydrationBalanceFetcher: HydrationBalanceFetcher,
|
||||
override val chain: Chain,
|
||||
private val gson: Gson,
|
||||
private val host: SwapQuoting.QuotingHost,
|
||||
) : StableSwapQuotingSource {
|
||||
|
||||
override val identifier: String = StableSwapQuotingSourceFactory.ID
|
||||
|
||||
private val initialPoolsInfo: MutableSharedFlow<Collection<PoolInitialInfo>> = singleReplaySharedFlow()
|
||||
|
||||
private val stablePools: MutableSharedFlow<List<StablePool>> = singleReplaySharedFlow()
|
||||
|
||||
override suspend fun sync() {
|
||||
val pools = getPools()
|
||||
|
||||
val poolInitialInfo = pools.matchIdsWithLocal()
|
||||
initialPoolsInfo.emit(poolInitialInfo)
|
||||
}
|
||||
|
||||
override suspend fun availableSwapDirections(): Collection<StableSwapQuotingSource.Edge> {
|
||||
val poolInitialInfo = initialPoolsInfo.first()
|
||||
|
||||
return poolInitialInfo.allPossibleDirections()
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
override suspend fun runSubscriptions(
|
||||
userAccountId: AccountId,
|
||||
subscriptionBuilder: SharedRequestsBuilder
|
||||
): Flow<Unit> = 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 allAssetIds = initialPoolsInfo.collectAllAssetIds()
|
||||
val assetsMetadataMap = fetchAssetMetadataMap(allAssetIds)
|
||||
|
||||
val poolSharedAssetBalanceSubscriptions = initialPoolsInfo.map { poolInfo ->
|
||||
val sharedAssetRemoteId = poolInfo.sharedAsset.first
|
||||
|
||||
subscribeTransferableBalance(subscriptionBuilder, omniPoolAccountId, sharedAssetRemoteId, assetsMetadataMap).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, assetsMetadataMap).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 pegsSubscriptions = initialPoolsInfo.map { poolInfo ->
|
||||
remoteStorageSource.subscribe(chain.id, subscriptionBuilder) {
|
||||
val poolId = poolInfo.sharedAsset.first
|
||||
runtime.metadata.stableSwap.poolPegs.observe(poolId).map {
|
||||
poolId to it
|
||||
}
|
||||
}
|
||||
}.toMultiSubscription(initialPoolsInfo.size)
|
||||
|
||||
combine6(
|
||||
poolInfoSubscriptions,
|
||||
poolSharedAssetBalanceSubscriptions,
|
||||
poolParticipatingAssetsBalanceSubscription,
|
||||
totalIssuanceSubscriptions,
|
||||
host.sharedSubscriptions.blockNumber(chain.id),
|
||||
pegsSubscriptions
|
||||
) { poolInfos, poolSharedAssetBalances, poolParticipatingAssetBalances, totalIssuances, currentBlock, pegs ->
|
||||
createStableSwapPool(poolInfos, poolSharedAssetBalances, poolParticipatingAssetBalances, totalIssuances, currentBlock, assetsMetadataMap, pegs)
|
||||
}
|
||||
.onEach(stablePools::emit)
|
||||
.map { }
|
||||
}
|
||||
|
||||
private suspend fun subscribeTransferableBalance(
|
||||
subscriptionBuilder: SharedRequestsBuilder,
|
||||
account: AccountId,
|
||||
assetId: HydraDxAssetId,
|
||||
assetMetadataMap: HydrationAssetMetadataMap,
|
||||
): Flow<BigInteger> {
|
||||
// In case token type was not possible to resolve - just return zero
|
||||
val tokenType = assetMetadataMap.getAssetType(assetId) ?: return flowOf(BalanceOf.ZERO)
|
||||
return hydrationBalanceFetcher.subscribeToTransferableBalance(chain.id, tokenType, account, subscriptionBuilder)
|
||||
}
|
||||
|
||||
private fun createStableSwapPool(
|
||||
poolInfos: Map<HydraDxAssetId, StableSwapPoolInfo?>,
|
||||
poolSharedAssetBalances: Map<HydraDxAssetId, BigInteger>,
|
||||
poolParticipatingAssetBalances: Map<Pair<HydraDxAssetId, HydraDxAssetId>, BigInteger>,
|
||||
totalIssuances: Map<HydraDxAssetId, BigInteger>,
|
||||
currentBlock: BlockNumber,
|
||||
assetMetadataMap: HydrationAssetMetadataMap,
|
||||
pegs: Map<HydraDxAssetId, StalbeSwapPoolPegInfo?>
|
||||
): List<StablePool> {
|
||||
return poolInfos.mapNotNull outer@{ (poolId, poolInfo) ->
|
||||
if (poolInfo == null) return@outer null
|
||||
|
||||
val sharedAssetBalance = poolSharedAssetBalances[poolId].orZero()
|
||||
val sharedChainAssetPrecision = assetMetadataMap.getDecimals(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 = assetMetadataMap.getDecimals(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,
|
||||
pegs = pegs[poolId]?.current ?: StablePool.getDefaultPegs(pooledAssets.size)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun Collection<PoolInitialInfo>.collectAllAssetIds(): List<HydraDxAssetId> {
|
||||
return flatMap { pool ->
|
||||
buildList {
|
||||
add(pool.sharedAsset.first)
|
||||
|
||||
pool.poolAssets.onEach {
|
||||
add(it.first)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun fetchAssetMetadataMap(allAssetIds: List<HydraDxAssetId>): HydrationAssetMetadataMap {
|
||||
return remoteStorageSource.query(chain.id) {
|
||||
val assetMetadatas = metadata.assetRegistry.assets.multi(allAssetIds).filterNotNull()
|
||||
HydrationAssetMetadataMap(
|
||||
nativeId = hydraDxAssetIdConverter.systemAssetId,
|
||||
metadataMap = assetMetadatas
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun stableSwapPoolAccountId(poolId: HydraDxAssetId): AccountId {
|
||||
val prefix = "sts".encodeToByteArray()
|
||||
val suffix = poolId.toInt().asLittleEndianBytes()
|
||||
|
||||
return (prefix + suffix).blake2b256()
|
||||
}
|
||||
|
||||
private suspend fun getPools(): Map<HydraDxAssetId, StableSwapPoolInfo> {
|
||||
return remoteStorageSource.query(chain.id) {
|
||||
val tradabilities = runtime.metadata.stableSwapOrNull?.assetTradability?.entries().orEmpty()
|
||||
runtime.metadata.stableSwapOrNull?.pools?.entries().orEmpty()
|
||||
.filterByTradability(tradabilities)
|
||||
}
|
||||
}
|
||||
|
||||
private fun Map<HydraDxAssetId, StableSwapPoolInfo>.filterByTradability(
|
||||
tradabilities: Map<HydraDxAssetId, Tradeability>
|
||||
): Map<HydraDxAssetId, StableSwapPoolInfo> {
|
||||
return this.filter { (poolId, _) ->
|
||||
val tradability = tradabilities[poolId] ?: return@filter true
|
||||
|
||||
tradability.canBuy() && tradability.canSell()
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun Map<HydraDxAssetId, StableSwapPoolInfo>.matchIdsWithLocal(): List<PoolInitialInfo> {
|
||||
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 Collection<PoolInitialInfo>.allPossibleDirections(): Collection<RealStableSwapQuotingEdge> {
|
||||
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 { RealStableSwapQuotingEdge(assetId, otherAssetId, poolAssetId.first) }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private data class PoolInitialInfo(
|
||||
val sharedAsset: RemoteAndLocalIdOptional,
|
||||
val poolAssets: List<RemoteAndLocalIdOptional>
|
||||
)
|
||||
|
||||
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 fun weightForAppendingTo(path: Path<WeightedEdge<FullChainAssetId>>): Int {
|
||||
return weightAppendingToPath(path, Weights.Hydra.STABLESWAP)
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
+51
@@ -0,0 +1,51 @@
|
||||
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.sources.omnipool.model.Tradeability
|
||||
import io.novafoundation.nova.feature_swap_core.data.assetExchange.conversion.types.hydra.sources.omnipool.model.bindTradeability
|
||||
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.StalbeSwapPoolPegInfo
|
||||
import io.novafoundation.nova.feature_swap_core.data.assetExchange.conversion.types.hydra.sources.stableswap.model.bindPoolPegInfo
|
||||
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
|
||||
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 StableSwapApi(override val module: Module) : QueryableModule
|
||||
|
||||
context(StorageQueryContext)
|
||||
val RuntimeMetadata.stableSwapOrNull: StableSwapApi?
|
||||
get() = stableSwapOrNull()?.let(::StableSwapApi)
|
||||
|
||||
context(StorageQueryContext)
|
||||
val RuntimeMetadata.stableSwap: StableSwapApi
|
||||
get() = StableSwapApi(stableSwap())
|
||||
|
||||
context(StorageQueryContext)
|
||||
val StableSwapApi.pools: QueryableStorageEntry1<HydraDxAssetId, StableSwapPoolInfo>
|
||||
get() = storage1(
|
||||
name = "Pools",
|
||||
binding = ::bindStablePoolInfo,
|
||||
)
|
||||
|
||||
context(StorageQueryContext)
|
||||
val StableSwapApi.poolPegs: QueryableStorageEntry1<HydraDxAssetId, StalbeSwapPoolPegInfo>
|
||||
get() = storage1(
|
||||
name = "PoolPegs",
|
||||
binding = { decoded, _ -> bindPoolPegInfo(decoded) },
|
||||
)
|
||||
|
||||
context(StorageQueryContext)
|
||||
val StableSwapApi.assetTradability: QueryableStorageEntry1<HydraDxAssetId, Tradeability>
|
||||
get() = storage1(
|
||||
name = "AssetTradability",
|
||||
binding = { decoded, _ ->
|
||||
bindTradeability(decoded)
|
||||
},
|
||||
)
|
||||
+21
@@ -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<StableSwapQuotingSource.Edge> {
|
||||
|
||||
val chain: Chain
|
||||
|
||||
interface Edge : QuotableEdge {
|
||||
|
||||
val fromAsset: RemoteAndLocalId
|
||||
|
||||
val toAsset: RemoteAndLocalId
|
||||
|
||||
val poolId: HydraDxAssetId
|
||||
}
|
||||
}
|
||||
+38
@@ -0,0 +1,38 @@
|
||||
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_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
|
||||
import io.novafoundation.nova.runtime.storage.source.query.api.QueryableStorageEntry2
|
||||
import io.novafoundation.nova.runtime.storage.source.query.api.storage1
|
||||
import io.novafoundation.nova.runtime.storage.source.query.api.storage2
|
||||
import io.novasama.substrate_sdk_android.runtime.AccountId
|
||||
import io.novasama.substrate_sdk_android.runtime.metadata.RuntimeMetadata
|
||||
import io.novasama.substrate_sdk_android.runtime.metadata.module.Module
|
||||
import java.math.BigInteger
|
||||
|
||||
@JvmInline
|
||||
value class TokensApi(override val module: Module) : QueryableModule
|
||||
|
||||
context(StorageQueryContext)
|
||||
val RuntimeMetadata.hydraTokens: TokensApi
|
||||
get() = TokensApi(tokens())
|
||||
|
||||
context(StorageQueryContext)
|
||||
val TokensApi.totalIssuance: QueryableStorageEntry1<HydraDxAssetId, BigInteger>
|
||||
get() = storage1(
|
||||
name = "TotalIssuance",
|
||||
binding = { decoded, _ -> bindNumber(decoded) },
|
||||
)
|
||||
|
||||
context(StorageQueryContext)
|
||||
val TokensApi.accounts: QueryableStorageEntry2<AccountId, HydraDxAssetId, AccountBalance>
|
||||
get() = storage2(
|
||||
name = "Accounts",
|
||||
binding = { decoded, _, _ -> bindOrmlAccountData(decoded) },
|
||||
)
|
||||
+206
@@ -0,0 +1,206 @@
|
||||
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_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: BigInteger,
|
||||
val assets: List<StablePoolAsset>,
|
||||
val initialAmplification: BigInteger,
|
||||
val finalAmplification: BigInteger,
|
||||
val initialBlock: BigInteger,
|
||||
val finalBlock: BigInteger,
|
||||
val currentBlock: BlockNumber,
|
||||
fee: Perbill,
|
||||
val gson: Gson,
|
||||
val pegs: List<List<BigInteger>>
|
||||
) {
|
||||
|
||||
companion object {
|
||||
fun getDefaultPegs(size: Int): List<List<BigInteger>> {
|
||||
return (0 until size).map {
|
||||
listOf(BigInteger.ONE, BigInteger.ONE)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val sharedAssetIssuance = sharedAssetIssuance.toString()
|
||||
val fee: String = fee.value.toBigDecimal().toPlainString()
|
||||
|
||||
val reserves: String by lazy(LazyThreadSafetyMode.NONE) {
|
||||
val reservesInput = assets.map { ReservesInput(it.balance.toString(), it.id.toInt(), it.decimals) }
|
||||
gson.toJson(reservesInput)
|
||||
}
|
||||
|
||||
val amplification by lazy(LazyThreadSafetyMode.NONE) {
|
||||
calculateAmplification()
|
||||
}
|
||||
|
||||
val pegsSerialized: String by lazy(LazyThreadSafetyMode.NONE) {
|
||||
val pegsInput = pegs.map { inner -> inner.map { it.toString() } }
|
||||
gson.toJson(pegsInput)
|
||||
}
|
||||
|
||||
private fun calculateAmplification(): String {
|
||||
return StableSwapMathBridge.calculate_amplification(
|
||||
initialAmplification.toString(),
|
||||
finalAmplification.toString(),
|
||||
initialBlock.toString(),
|
||||
finalBlock.toString(),
|
||||
currentBlock.toString()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
class StablePoolAsset(
|
||||
val balance: BigInteger,
|
||||
val id: HydraDxAssetId,
|
||||
val decimals: Int
|
||||
)
|
||||
|
||||
fun StablePool.quote(
|
||||
assetIdIn: HydraDxAssetId,
|
||||
assetIdOut: HydraDxAssetId,
|
||||
amount: BigInteger,
|
||||
direction: SwapDirection
|
||||
): BigInteger? {
|
||||
return when (direction) {
|
||||
SwapDirection.SPECIFIED_IN -> calculateOutGivenIn(assetIdIn, assetIdOut, amount)
|
||||
SwapDirection.SPECIFIED_OUT -> calculateInGivenOut(assetIdIn, assetIdOut, amount)
|
||||
}
|
||||
}
|
||||
|
||||
fun StablePool.calculateOutGivenIn(
|
||||
assetIn: HydraDxAssetId,
|
||||
assetOut: HydraDxAssetId,
|
||||
amountIn: BigInteger,
|
||||
): BigInteger? {
|
||||
return when {
|
||||
assetIn == sharedAsset.id -> calculateWithdrawOneAsset(assetOut, amountIn)
|
||||
assetOut == sharedAsset.id -> calculateShares(assetIn, amountIn)
|
||||
else -> calculateOut(assetIn, assetOut, amountIn)
|
||||
}
|
||||
}
|
||||
|
||||
fun StablePool.calculateInGivenOut(
|
||||
assetIn: HydraDxAssetId,
|
||||
assetOut: HydraDxAssetId,
|
||||
amountOut: BigInteger,
|
||||
): BigInteger? {
|
||||
return when {
|
||||
assetOut == sharedAsset.id -> calculateAddOneAsset(assetIn, amountOut)
|
||||
assetIn == sharedAsset.id -> calculateSharesForAmount(assetOut, amountOut)
|
||||
else -> calculateIn(assetIn, assetOut, amountOut)
|
||||
}
|
||||
}
|
||||
|
||||
private fun StablePool.calculateAddOneAsset(
|
||||
assetIn: HydraDxAssetId,
|
||||
amountOut: BigInteger,
|
||||
): BigInteger? {
|
||||
return StableSwapMathBridge.calculate_add_one_asset(
|
||||
reserves,
|
||||
amountOut.toString(),
|
||||
assetIn.toInt(),
|
||||
amplification,
|
||||
sharedAssetIssuance,
|
||||
fee,
|
||||
pegsSerialized
|
||||
).fromBridgeResultToBalance()
|
||||
}
|
||||
|
||||
private fun StablePool.calculateSharesForAmount(
|
||||
assetOut: HydraDxAssetId,
|
||||
amountOut: BigInteger,
|
||||
): BigInteger? {
|
||||
return StableSwapMathBridge.calculate_shares_for_amount(
|
||||
reserves,
|
||||
assetOut.toInt(),
|
||||
amountOut.toString(),
|
||||
amplification,
|
||||
sharedAssetIssuance,
|
||||
fee,
|
||||
pegsSerialized
|
||||
).fromBridgeResultToBalance()
|
||||
}
|
||||
|
||||
private fun StablePool.calculateIn(
|
||||
assetIn: HydraDxAssetId,
|
||||
assetOut: HydraDxAssetId,
|
||||
amountOut: BigInteger,
|
||||
): BigInteger? {
|
||||
return StableSwapMathBridge.calculate_in_given_out(
|
||||
reserves,
|
||||
assetIn.toInt(),
|
||||
assetOut.toInt(),
|
||||
amountOut.toString(),
|
||||
amplification,
|
||||
fee,
|
||||
pegsSerialized
|
||||
).fromBridgeResultToBalance()
|
||||
}
|
||||
|
||||
private fun StablePool.calculateWithdrawOneAsset(
|
||||
assetOut: HydraDxAssetId,
|
||||
amountIn: BigInteger,
|
||||
): BigInteger? {
|
||||
return StableSwapMathBridge.calculate_liquidity_out_one_asset(
|
||||
reserves,
|
||||
amountIn.toString(),
|
||||
assetOut.toInt(),
|
||||
amplification,
|
||||
sharedAssetIssuance,
|
||||
fee,
|
||||
pegsSerialized
|
||||
).fromBridgeResultToBalance()
|
||||
}
|
||||
|
||||
private fun StablePool.calculateShares(
|
||||
assetIn: HydraDxAssetId,
|
||||
amountIn: BigInteger,
|
||||
): BigInteger? {
|
||||
val assets = listOf(SharesAssetInput(assetIn.toInt(), amountIn.toString()))
|
||||
val assetsJson = gson.toJson(assets)
|
||||
|
||||
return StableSwapMathBridge.calculate_shares(
|
||||
reserves,
|
||||
assetsJson,
|
||||
amplification,
|
||||
sharedAssetIssuance,
|
||||
fee,
|
||||
pegsSerialized
|
||||
).fromBridgeResultToBalance()
|
||||
}
|
||||
|
||||
private fun StablePool.calculateOut(
|
||||
assetIn: HydraDxAssetId,
|
||||
assetOut: HydraDxAssetId,
|
||||
amountIn: BigInteger,
|
||||
): BigInteger? {
|
||||
return StableSwapMathBridge.calculate_out_given_in(
|
||||
this.reserves,
|
||||
assetIn.toInt(),
|
||||
assetOut.toInt(),
|
||||
amountIn.toString(),
|
||||
amplification,
|
||||
fee,
|
||||
pegsSerialized
|
||||
).fromBridgeResultToBalance()
|
||||
}
|
||||
|
||||
private class SharesAssetInput(@SerializedName("asset_id") val assetId: Int, val amount: String)
|
||||
|
||||
private class ReservesInput(
|
||||
val amount: String,
|
||||
@SerializedName("asset_id")
|
||||
val id: Int,
|
||||
val decimals: Int
|
||||
)
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
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_api.data.network.HydraDxAssetId
|
||||
import java.math.BigInteger
|
||||
|
||||
class StableSwapPoolInfo(
|
||||
val poolAssetId: HydraDxAssetId,
|
||||
val assets: List<HydraDxAssetId>,
|
||||
val initialAmplification: BigInteger,
|
||||
val finalAmplification: BigInteger,
|
||||
val initialBlock: BigInteger,
|
||||
val finalBlock: BigInteger,
|
||||
val fee: Perbill,
|
||||
)
|
||||
|
||||
fun bindStablePoolInfo(decoded: Any?, poolTokenId: HydraDxAssetId): StableSwapPoolInfo {
|
||||
val struct = decoded.castToStruct()
|
||||
|
||||
return StableSwapPoolInfo(
|
||||
poolAssetId = poolTokenId,
|
||||
assets = bindList(decoded["assets"], ::bindNumber),
|
||||
initialAmplification = bindNumber(struct["initialAmplification"]),
|
||||
finalAmplification = bindNumber(struct["finalAmplification"]),
|
||||
initialBlock = bindNumber(struct["initialBlock"]),
|
||||
finalBlock = bindNumber(struct["finalBlock"]),
|
||||
fee = bindPermill(struct["fee"])
|
||||
)
|
||||
}
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
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.castToStruct
|
||||
import java.math.BigInteger
|
||||
|
||||
class StalbeSwapPoolPegInfo(
|
||||
val current: List<List<BigInteger>>
|
||||
)
|
||||
|
||||
fun bindPoolPegInfo(decoded: Any?): StalbeSwapPoolPegInfo {
|
||||
val asStruct = decoded.castToStruct()
|
||||
return StalbeSwapPoolPegInfo(
|
||||
current = bindList(asStruct["current"]) { item ->
|
||||
bindList(item, ::bindNumber)
|
||||
}
|
||||
)
|
||||
}
|
||||
+211
@@ -0,0 +1,211 @@
|
||||
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.di.scope.FeatureScope
|
||||
import io.novafoundation.nova.common.utils.combine
|
||||
import io.novafoundation.nova.common.utils.graph.Path
|
||||
import io.novafoundation.nova.common.utils.graph.WeightedEdge
|
||||
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.Weights.Hydra.weightAppendingToPath
|
||||
import io.novafoundation.nova.feature_swap_core.data.assetExchange.conversion.types.hydra.sources.common.HydrationAssetType
|
||||
import io.novafoundation.nova.feature_swap_core.data.assetExchange.conversion.types.hydra.sources.common.HydrationBalanceFetcher
|
||||
import io.novafoundation.nova.feature_swap_core.data.assetExchange.conversion.types.hydra.sources.common.HydrationBalanceFetcherFactory
|
||||
import io.novafoundation.nova.feature_swap_core.data.assetExchange.conversion.types.hydra.sources.common.fromAsset
|
||||
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.omnipool.model.matchId
|
||||
import io.novafoundation.nova.feature_swap_core.data.assetExchange.conversion.types.hydra.sources.omnipool.model.remoteId
|
||||
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.HydraDxAssetIdConverter
|
||||
import io.novafoundation.nova.feature_swap_core_api.data.primitive.SwapQuoting
|
||||
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.di.REMOTE_STORAGE_SOURCE
|
||||
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 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
|
||||
import java.math.BigInteger
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Named
|
||||
|
||||
@FeatureScope
|
||||
class XYKSwapQuotingSourceFactory @Inject constructor(
|
||||
@Named(REMOTE_STORAGE_SOURCE)
|
||||
private val remoteStorageSource: StorageDataSource,
|
||||
private val hydraDxAssetIdConverter: HydraDxAssetIdConverter,
|
||||
private val hydrationBalanceFetcherFactory: HydrationBalanceFetcherFactory,
|
||||
) : HydraDxQuotingSource.Factory<XYKSwapQuotingSource> {
|
||||
|
||||
companion object {
|
||||
|
||||
const val ID = "XYK"
|
||||
}
|
||||
|
||||
override fun create(chain: Chain, host: SwapQuoting.QuotingHost): XYKSwapQuotingSource {
|
||||
return RealXYKSwapQuotingSource(
|
||||
remoteStorageSource = remoteStorageSource,
|
||||
hydraDxAssetIdConverter = hydraDxAssetIdConverter,
|
||||
hydrationBalanceFetcher = hydrationBalanceFetcherFactory.create(host),
|
||||
chain = chain
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private class RealXYKSwapQuotingSource(
|
||||
private val remoteStorageSource: StorageDataSource,
|
||||
private val hydraDxAssetIdConverter: HydraDxAssetIdConverter,
|
||||
private val hydrationBalanceFetcher: HydrationBalanceFetcher,
|
||||
private val chain: Chain
|
||||
) : XYKSwapQuotingSource {
|
||||
|
||||
override val identifier: String = XYKSwapQuotingSourceFactory.ID
|
||||
|
||||
private val initialPoolsInfo: MutableSharedFlow<Collection<PoolInitialInfo>> = singleReplaySharedFlow()
|
||||
|
||||
private val xykPools: MutableSharedFlow<XYKPools> = singleReplaySharedFlow()
|
||||
|
||||
override suspend fun sync() {
|
||||
val pools = getPools()
|
||||
|
||||
val poolInitialInfo = pools.matchIdsWithLocal()
|
||||
initialPoolsInfo.emit(poolInitialInfo)
|
||||
}
|
||||
|
||||
override suspend fun availableSwapDirections(): Collection<XYKSwapQuotingSource.Edge> {
|
||||
val poolInitialInfo = initialPoolsInfo.first()
|
||||
|
||||
return poolInitialInfo.allPossibleDirections()
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
override suspend fun runSubscriptions(
|
||||
userAccountId: AccountId,
|
||||
subscriptionBuilder: SharedRequestsBuilder
|
||||
): Flow<Unit> = 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)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun subscribeToBalance(
|
||||
assetId: RemoteAndLocalId,
|
||||
poolAddress: AccountId,
|
||||
subscriptionBuilder: SharedRequestsBuilder
|
||||
): Flow<BigInteger> {
|
||||
val chainAsset = chain.assetsById.getValue(assetId.localId.assetId)
|
||||
val hydrationAssetType = HydrationAssetType.fromAsset(chainAsset, assetId.remoteId)
|
||||
|
||||
return hydrationBalanceFetcher.subscribeToTransferableBalance(
|
||||
chainId = chainAsset.chainId,
|
||||
type = hydrationAssetType,
|
||||
accountId = poolAddress,
|
||||
subscriptionBuilder = subscriptionBuilder
|
||||
)
|
||||
}
|
||||
|
||||
private suspend fun getPools(): Map<AccountIdKey, XYKPoolInfo> {
|
||||
return remoteStorageSource.query(chain.id) {
|
||||
runtime.metadata.xykOrNull?.poolAssets?.entries().orEmpty()
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun Map<AccountIdKey, XYKPoolInfo>.matchIdsWithLocal(): List<PoolInitialInfo> {
|
||||
val allOnChainIds = hydraDxAssetIdConverter.allOnChainIds(chain)
|
||||
|
||||
return mapNotNull { (poolAddress, poolInfo) ->
|
||||
PoolInitialInfo(
|
||||
poolAddress = poolAddress.value,
|
||||
firstAsset = allOnChainIds.matchId(poolInfo.firstAsset) ?: return@mapNotNull null,
|
||||
secondAsset = allOnChainIds.matchId(poolInfo.secondAsset) ?: return@mapNotNull null,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun Collection<PoolInitialInfo>.allPossibleDirections(): Collection<RealXYKSwapQuotingEdge> {
|
||||
return buildList {
|
||||
this@allPossibleDirections.forEach { poolInfo ->
|
||||
add(
|
||||
RealXYKSwapQuotingEdge(
|
||||
fromAsset = poolInfo.firstAsset,
|
||||
toAsset = poolInfo.secondAsset,
|
||||
poolAddress = poolInfo.poolAddress
|
||||
)
|
||||
)
|
||||
|
||||
add(
|
||||
RealXYKSwapQuotingEdge(
|
||||
fromAsset = poolInfo.secondAsset,
|
||||
toAsset = poolInfo.firstAsset,
|
||||
poolAddress = poolInfo.poolAddress
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
inner class RealXYKSwapQuotingEdge(
|
||||
override val fromAsset: RemoteAndLocalId,
|
||||
override val toAsset: RemoteAndLocalId,
|
||||
override val poolAddress: AccountId
|
||||
) : XYKSwapQuotingSource.Edge {
|
||||
|
||||
override val from: FullChainAssetId = fromAsset.second
|
||||
|
||||
override val to: FullChainAssetId = toAsset.second
|
||||
|
||||
override fun weightForAppendingTo(path: Path<WeightedEdge<FullChainAssetId>>): Int {
|
||||
return weightAppendingToPath(path, Weights.Hydra.XYK)
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class PoolInitialInfo(
|
||||
val poolAddress: AccountId,
|
||||
val firstAsset: RemoteAndLocalId,
|
||||
val secondAsset: RemoteAndLocalId
|
||||
)
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
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.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
|
||||
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<AccountIdKey, XYKPoolInfo>
|
||||
get() = storage1(
|
||||
name = "PoolAssets",
|
||||
keyBinding = { bindAccountId(it).intoKey() },
|
||||
binding = { decoded, _ -> bindXYKPoolInfo(decoded) },
|
||||
)
|
||||
+18
@@ -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<XYKSwapQuotingSource.Edge> {
|
||||
|
||||
interface Edge : QuotableEdge {
|
||||
|
||||
val fromAsset: RemoteAndLocalId
|
||||
|
||||
val toAsset: RemoteAndLocalId
|
||||
|
||||
val poolAddress: AccountId
|
||||
}
|
||||
}
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
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
|
||||
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))
|
||||
}
|
||||
+105
@@ -0,0 +1,105 @@
|
||||
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_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
|
||||
import java.math.BigInteger
|
||||
|
||||
class XYKPools(
|
||||
val fees: XYKFees,
|
||||
val pools: List<XYKPool>
|
||||
) {
|
||||
|
||||
fun quote(
|
||||
poolAddress: AccountId,
|
||||
assetIdIn: HydraDxAssetId,
|
||||
assetIdOut: HydraDxAssetId,
|
||||
amount: BigInteger,
|
||||
direction: SwapDirection
|
||||
): BigInteger? {
|
||||
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: BigInteger,
|
||||
val id: HydraDxAssetId,
|
||||
)
|
||||
|
||||
fun XYKPool.quote(
|
||||
assetIdIn: HydraDxAssetId,
|
||||
assetIdOut: HydraDxAssetId,
|
||||
amount: BigInteger,
|
||||
direction: SwapDirection,
|
||||
fees: XYKFees
|
||||
): BigInteger? {
|
||||
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: BigInteger,
|
||||
feesConfig: XYKFees
|
||||
): BigInteger? {
|
||||
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: BigInteger,
|
||||
feesConfig: XYKFees,
|
||||
): BigInteger? {
|
||||
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): BigInteger? {
|
||||
return HYKSwapMathBridge.calculate_pool_trade_fee(amount.toString(), nominator.toString(), denominator.toString())
|
||||
.fromBridgeResultToBalance()
|
||||
}
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
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_api.data.network.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))
|
||||
}
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
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(
|
||||
dependencies = [
|
||||
SwapCoreDependencies::class,
|
||||
],
|
||||
modules = [
|
||||
SwapCoreModule::class,
|
||||
]
|
||||
)
|
||||
@FeatureScope
|
||||
interface SwapCoreComponent : SwapCoreApi {
|
||||
|
||||
@Component.Factory
|
||||
interface Factory {
|
||||
|
||||
fun create(deps: SwapCoreDependencies): SwapCoreComponent
|
||||
}
|
||||
|
||||
@Component(
|
||||
dependencies = [
|
||||
CommonApi::class,
|
||||
RuntimeApi::class
|
||||
]
|
||||
)
|
||||
interface SwapCoreDependenciesComponent : SwapCoreDependencies
|
||||
}
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
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.call.MultiChainRuntimeCallsApi
|
||||
import io.novafoundation.nova.runtime.di.REMOTE_STORAGE_SOURCE
|
||||
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
|
||||
|
||||
interface SwapCoreDependencies {
|
||||
|
||||
val multiChainRuntimeCallsApi: MultiChainRuntimeCallsApi
|
||||
|
||||
val chainRegistry: ChainRegistry
|
||||
|
||||
val chainStateRepository: ChainStateRepository
|
||||
|
||||
val gson: Gson
|
||||
|
||||
@Named(REMOTE_STORAGE_SOURCE)
|
||||
fun remoteStorageSource(): StorageDataSource
|
||||
|
||||
val computationalCache: ComputationalCache
|
||||
}
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
package io.novafoundation.nova.feature_swap_core.di
|
||||
|
||||
import io.novafoundation.nova.common.di.FeatureApiHolder
|
||||
import io.novafoundation.nova.common.di.FeatureContainer
|
||||
import io.novafoundation.nova.common.di.scope.ApplicationScope
|
||||
import io.novafoundation.nova.runtime.di.RuntimeApi
|
||||
import javax.inject.Inject
|
||||
|
||||
@ApplicationScope
|
||||
class SwapCoreHolder @Inject constructor(
|
||||
featureContainer: FeatureContainer
|
||||
) : FeatureApiHolder(featureContainer) {
|
||||
|
||||
override fun initializeDependencies(): Any {
|
||||
val accountFeatureDependencies = DaggerSwapCoreComponent_SwapCoreDependenciesComponent.builder()
|
||||
.commonApi(commonApi())
|
||||
.runtimeApi(getFeature(RuntimeApi::class.java))
|
||||
.build()
|
||||
|
||||
return DaggerSwapCoreComponent.factory()
|
||||
.create(accountFeatureDependencies)
|
||||
}
|
||||
}
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
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.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])
|
||||
class SwapCoreModule {
|
||||
|
||||
@Provides
|
||||
@FeatureScope
|
||||
fun provideHydraDxAssetIdConverter(
|
||||
chainRegistry: ChainRegistry
|
||||
): HydraDxAssetIdConverter {
|
||||
return RealHydraDxAssetIdConverter(chainRegistry)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@FeatureScope
|
||||
fun providePathsQuoterFactory(
|
||||
computationalCache: ComputationalCache
|
||||
): PathQuoter.Factory {
|
||||
return RealPathQuoterFactory(computationalCache)
|
||||
}
|
||||
}
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
package io.novafoundation.nova.feature_swap_core.di.conversions
|
||||
|
||||
import dagger.Binds
|
||||
import dagger.Module
|
||||
import io.novafoundation.nova.common.di.scope.FeatureScope
|
||||
import io.novafoundation.nova.feature_swap_core.data.assetExchange.conversion.types.hydra.RealHydrationAcceptedFeeCurrenciesFetcher
|
||||
import io.novafoundation.nova.feature_swap_core.data.assetExchange.conversion.types.hydra.RealHydrationPriceConversionFallback
|
||||
import io.novafoundation.nova.feature_swap_core_api.data.types.hydra.HydrationAcceptedFeeCurrenciesFetcher
|
||||
import io.novafoundation.nova.feature_swap_core_api.data.types.hydra.HydrationPriceConversionFallback
|
||||
|
||||
@Module
|
||||
internal interface HydraDxBindsModule {
|
||||
|
||||
@Binds
|
||||
fun bindHydrationPriceConversionFallback(real: RealHydrationPriceConversionFallback): HydrationPriceConversionFallback
|
||||
|
||||
@Binds
|
||||
@FeatureScope
|
||||
fun bindHydrationAcceptedFeeCurrenciesFetcher(real: RealHydrationAcceptedFeeCurrenciesFetcher): HydrationAcceptedFeeCurrenciesFetcher
|
||||
}
|
||||
+51
@@ -0,0 +1,51 @@
|
||||
package io.novafoundation.nova.feature_swap_core.di.conversions
|
||||
|
||||
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.RealHydraDxQuotingFactory
|
||||
import io.novafoundation.nova.feature_swap_core.data.assetExchange.conversion.types.hydra.sources.aave.AaveSwapQuotingSourceFactory
|
||||
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.types.hydra.HydraDxQuoting
|
||||
import io.novafoundation.nova.feature_swap_core_api.data.types.hydra.HydraDxQuotingSource
|
||||
|
||||
@Module(includes = [HydraDxBindsModule::class])
|
||||
class HydraDxConversionModule {
|
||||
|
||||
@Provides
|
||||
@IntoSet
|
||||
fun provideOmniPoolSourceFactory(implementation: OmniPoolQuotingSourceFactory): HydraDxQuotingSource.Factory<*> {
|
||||
return implementation
|
||||
}
|
||||
|
||||
@Provides
|
||||
@IntoSet
|
||||
fun provideStableSwapSourceFactory(implementation: StableSwapQuotingSourceFactory): HydraDxQuotingSource.Factory<*> {
|
||||
return implementation
|
||||
}
|
||||
|
||||
@Provides
|
||||
@IntoSet
|
||||
fun provideXykSwapSourceFactory(implementation: XYKSwapQuotingSourceFactory): HydraDxQuotingSource.Factory<*> {
|
||||
return implementation
|
||||
}
|
||||
|
||||
@Provides
|
||||
@IntoSet
|
||||
fun provideAavePoolQuotingSourceFactory(implementation: AaveSwapQuotingSourceFactory): HydraDxQuotingSource.Factory<*> {
|
||||
return implementation
|
||||
}
|
||||
|
||||
@Provides
|
||||
@FeatureScope
|
||||
fun provideHydraDxAssetConversionFactory(
|
||||
conversionSourceFactories: Set<@JvmSuppressWildcards HydraDxQuotingSource.Factory<*>>,
|
||||
): HydraDxQuoting.Factory {
|
||||
return RealHydraDxQuotingFactory(
|
||||
conversionSourceFactories = conversionSourceFactories,
|
||||
)
|
||||
}
|
||||
}
|
||||
+151
@@ -0,0 +1,151 @@
|
||||
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.Path
|
||||
import io.novafoundation.nova.common.utils.graph.findDijkstraPathsBetween
|
||||
import io.novafoundation.nova.common.utils.graph.numberOfEdges
|
||||
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
|
||||
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 kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.first
|
||||
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 <E : QuotableEdge> create(
|
||||
graphFlow: Flow<Graph<FullChainAssetId, E>>,
|
||||
computationalScope: CoroutineScope,
|
||||
pathFeeEstimation: PathFeeEstimator<E>?,
|
||||
filter: EdgeVisitFilter<E>?
|
||||
): PathQuoter<E> {
|
||||
return RealPathQuoter(computationalCache, graphFlow, computationalScope, pathFeeEstimation, filter)
|
||||
}
|
||||
}
|
||||
|
||||
private class RealPathQuoter<E : QuotableEdge>(
|
||||
private val computationalCache: ComputationalCache,
|
||||
private val graphFlow: Flow<Graph<FullChainAssetId, E>>,
|
||||
private val computationalScope: CoroutineScope,
|
||||
private val pathFeeEstimation: PathFeeEstimator<E>?,
|
||||
private val filter: EdgeVisitFilter<E>?,
|
||||
) : PathQuoter<E> {
|
||||
|
||||
override suspend fun findBestPath(
|
||||
chainAssetIn: Chain.Asset,
|
||||
chainAssetOut: Chain.Asset,
|
||||
amount: BigInteger,
|
||||
swapDirection: SwapDirection,
|
||||
): BestPathQuote<E> {
|
||||
val from = chainAssetIn.fullId
|
||||
val to = chainAssetOut.fullId
|
||||
|
||||
val paths = pathsFromCacheOrCompute(from, to, computationalScope) { graph ->
|
||||
val paths = measureExecution("Finding ${chainAssetIn.symbol} -> ${chainAssetOut.symbol} paths") {
|
||||
graph.findDijkstraPathsBetween(from, to, limit = PATHS_LIMIT, filter)
|
||||
}
|
||||
|
||||
paths
|
||||
}
|
||||
|
||||
val quotedPaths = paths.mapAsync { path -> quotePath(path, amount, swapDirection) }
|
||||
.filterNotNull()
|
||||
|
||||
if (quotedPaths.isEmpty()) {
|
||||
throw SwapQuoteException.NotEnoughLiquidity
|
||||
}
|
||||
|
||||
return BestPathQuote(quotedPaths)
|
||||
}
|
||||
|
||||
private suspend fun pathsFromCacheOrCompute(
|
||||
from: FullChainAssetId,
|
||||
to: FullChainAssetId,
|
||||
scope: CoroutineScope,
|
||||
computation: suspend (graph: Graph<FullChainAssetId, E>) -> List<Path<E>>
|
||||
): List<Path<E>> {
|
||||
val graph = graphFlow.first()
|
||||
|
||||
val cacheKey = "$QUOTES_CACHE:${pathsCacheKey(from, to)}:${graph.numberOfEdges()}"
|
||||
|
||||
return computationalCache.useCache(cacheKey, scope) {
|
||||
computation(graph)
|
||||
}
|
||||
}
|
||||
|
||||
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<E>,
|
||||
amount: BigInteger,
|
||||
swapDirection: SwapDirection
|
||||
): QuotedPath<E>? {
|
||||
val quote = when (swapDirection) {
|
||||
SwapDirection.SPECIFIED_IN -> quotePathSell(path, amount)
|
||||
SwapDirection.SPECIFIED_OUT -> quotePathBuy(path, amount)
|
||||
} ?: return null
|
||||
|
||||
val pathRoughFeeEstimation = pathFeeEstimation.roughlyEstimateFeeOrZero(quote)
|
||||
|
||||
return QuotedPath(swapDirection, quote, pathRoughFeeEstimation)
|
||||
}
|
||||
|
||||
private suspend fun PathFeeEstimator<E>?.roughlyEstimateFeeOrZero(quote: Path<QuotedEdge<E>>): PathRoughFeeEstimation {
|
||||
return this?.roughlyEstimateFee(quote) ?: PathRoughFeeEstimation.zero()
|
||||
}
|
||||
|
||||
private suspend fun quotePathBuy(path: Path<E>, amount: BigInteger): Path<QuotedEdge<E>>? {
|
||||
return runCatching {
|
||||
val initial = mutableListOf<QuotedEdge<E>>() 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
|
||||
}
|
||||
.onFailure { Log.w("Swaps", "Failed to quote path", it) }
|
||||
.getOrNull()
|
||||
}
|
||||
|
||||
private suspend fun quotePathSell(path: Path<E>, amount: BigInteger): Path<QuotedEdge<E>>? {
|
||||
return runCatching {
|
||||
val initial = mutableListOf<QuotedEdge<E>>() 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
|
||||
}
|
||||
.onFailure { Log.w("Swaps", "Failed to quote path", it) }
|
||||
.getOrNull()
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user