Initial commit: Pezkuwi Wallet Android

Complete rebrand of Nova Wallet for Pezkuwichain ecosystem.

## Features
- Full Pezkuwichain support (HEZ & PEZ tokens)
- Polkadot ecosystem compatibility
- Staking, Governance, DeFi, NFTs
- XCM cross-chain transfers
- Hardware wallet support (Ledger, Polkadot Vault)
- WalletConnect v2
- Push notifications

## Languages
- English, Turkish, Kurmanci (Kurdish), Spanish, French, German, Russian, Japanese, Chinese, Korean, Portuguese, Vietnamese

Based on Nova Wallet by Novasama Technologies GmbH
© Dijital Kurdistan Tech Institute 2026
This commit is contained in:
2026-01-23 01:31:12 +03:00
commit 31c8c5995f
7621 changed files with 425838 additions and 0 deletions
@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
</manifest>
@@ -0,0 +1,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) },
)
@@ -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
}
}
}
@@ -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 }
}
}
@@ -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
}
}
}
@@ -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()
}
}
@@ -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
@@ -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
}
}
@@ -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
}
}
@@ -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
)
@@ -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
}
}
}
@@ -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
)
}
@@ -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
}
}
@@ -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}")
}
}
@@ -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"]),
)
}
}
@@ -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) },
)
@@ -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)
}
@@ -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
}
}
@@ -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,
)
@@ -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
}
}
}
@@ -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 }
}
@@ -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))
}
@@ -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
@@ -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"])
)
}
@@ -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"]))
}
@@ -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
}
}
}
@@ -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)
},
)
@@ -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
}
}
@@ -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) },
)
@@ -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
)
@@ -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"])
)
}
@@ -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)
}
)
}
@@ -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
)
@@ -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) },
)
@@ -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
}
}
@@ -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))
}
@@ -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()
}
@@ -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))
}
@@ -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
}
@@ -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
}
@@ -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)
}
}
@@ -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)
}
}
@@ -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
}
@@ -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,
)
}
}
@@ -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()
}
}