-
Notifications
You must be signed in to change notification settings - Fork 3
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add Lifi price provider #1453
Add Lifi price provider #1453
Changes from 1 commit
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,37 @@ | ||
package com.vultisig.wallet.data.api | ||
|
||
import com.vultisig.wallet.data.api.models.LiQuestResponseJson | ||
import com.vultisig.wallet.data.models.Chain | ||
import io.ktor.client.HttpClient | ||
import io.ktor.client.call.body | ||
import io.ktor.client.request.get | ||
import io.ktor.client.request.parameter | ||
import javax.inject.Inject | ||
|
||
internal interface LiQuestApi { | ||
suspend fun getLifiContractPriceUsd( | ||
chain: Chain, | ||
contractAddresses: String, | ||
): LiQuestResponseJson | ||
} | ||
|
||
internal class LiQuestApiImpl @Inject constructor( | ||
private val http: HttpClient, | ||
) : LiQuestApi { | ||
|
||
override suspend fun getLifiContractPriceUsd( | ||
chain: Chain, | ||
contractAddresses: String, | ||
): LiQuestResponseJson = http.get("https://li.quest/v1/token") { | ||
parameter("chain", chain.lifiChainId) | ||
parameter("token", contractAddresses) | ||
}.body() | ||
|
||
|
||
private val Chain.lifiChainId: String | ||
get() = when (this) { | ||
Chain.Ethereum -> "eth" | ||
else -> error("lifi chain id not found for chain $this") | ||
} | ||
} | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
package com.vultisig.wallet.data.api.models | ||
|
||
import kotlinx.serialization.SerialName | ||
import kotlinx.serialization.Serializable | ||
|
||
@Serializable | ||
internal data class LiQuestResponseJson( | ||
@SerialName("priceUSD") | ||
val priceUSD: String, | ||
) |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -2,12 +2,16 @@ package com.vultisig.wallet.data.repositories | |
|
||
import com.vultisig.wallet.data.api.CoinGeckoApi | ||
import com.vultisig.wallet.data.api.CurrencyToPrice | ||
import com.vultisig.wallet.data.api.LiQuestApi | ||
import com.vultisig.wallet.data.db.dao.TokenPriceDao | ||
import com.vultisig.wallet.data.db.models.TokenPriceEntity | ||
import com.vultisig.wallet.data.models.Chain | ||
import com.vultisig.wallet.data.models.Coin | ||
import com.vultisig.wallet.data.models.settings.AppCurrency | ||
import kotlinx.coroutines.ExperimentalCoroutinesApi | ||
import kotlinx.coroutines.async | ||
import kotlinx.coroutines.awaitAll | ||
import kotlinx.coroutines.coroutineScope | ||
import kotlinx.coroutines.flow.Flow | ||
import kotlinx.coroutines.flow.MutableStateFlow | ||
import kotlinx.coroutines.flow.first | ||
|
@@ -43,29 +47,30 @@ interface TokenPriceRepository { | |
): BigDecimal | ||
|
||
suspend fun getPriceByPriceProviderId( | ||
priceProviderId: String | ||
priceProviderId: String, | ||
): BigDecimal | ||
} | ||
|
||
|
||
internal class TokenPriceRepositoryImpl @Inject constructor( | ||
private val appCurrencyRepository: AppCurrencyRepository, | ||
private val coinGeckoApi: CoinGeckoApi, | ||
private val liQuestApi: LiQuestApi, | ||
private val tokenPriceDao: TokenPriceDao, | ||
) : TokenPriceRepository { | ||
|
||
private val tokenIdToPrice = MutableStateFlow(mapOf<String, CurrencyToPrice>()) | ||
|
||
override suspend fun getCachedPrice( | ||
tokenId: String, | ||
appCurrency: AppCurrency | ||
appCurrency: AppCurrency, | ||
): BigDecimal? = tokenPriceDao | ||
.getTokenPrice(tokenId, appCurrency.ticker.lowercase()) | ||
?.let { BigDecimal(it) } | ||
|
||
override suspend fun getCachedPrices( | ||
tokenIds: List<String>, | ||
appCurrency: AppCurrency | ||
appCurrency: AppCurrency, | ||
): List<Pair<String, BigDecimal>> = tokenPriceDao | ||
.getTokenPrices(tokenIds, appCurrency.ticker.lowercase()) | ||
.map { it.tokenId to BigDecimal(it.price) } | ||
|
@@ -116,7 +121,7 @@ internal class TokenPriceRepositoryImpl @Inject constructor( | |
.map { (chain, tokens) -> | ||
val pricesWithContractAddress = fetchPricesWithContractAddress( | ||
chain = chain, | ||
tokens = tokens, | ||
contractAddresses = tokens.map { it.contractAddress }, | ||
currencies = currencies, | ||
).asSequence() | ||
.mapNotNull { (contractAddress, value) -> | ||
|
@@ -178,25 +183,79 @@ internal class TokenPriceRepositoryImpl @Inject constructor( | |
|
||
private suspend fun fetchPricesWithContractAddress( | ||
chain: Chain, | ||
tokens: List<Coin>, | ||
contractAddresses: List<String>, | ||
currencies: List<String>, | ||
): Map<String, CurrencyToPrice> = | ||
if (chain == Chain.Solana) emptyMap() | ||
else coinGeckoApi.getContractsPrice( | ||
chain = chain, | ||
contractAddresses = tokens.map { it.contractAddress }, | ||
currencies = currencies, | ||
): Map<String, CurrencyToPrice> { | ||
return if (chain == Chain.Solana) emptyMap() | ||
else { | ||
coroutineScope { | ||
val coinGeckoContractsPrice = coinGeckoApi.getContractsPrice( | ||
chain = chain, | ||
contractAddresses = contractAddresses, | ||
currencies = currencies, | ||
) | ||
val notInCoinGeckoTokens = contractAddresses.filterNot { address -> | ||
coinGeckoContractsPrice.keys.any { key -> | ||
key.equals( | ||
address, | ||
false | ||
) | ||
} | ||
} | ||
|
||
notInCoinGeckoTokens.takeIf { it.isNotEmpty() } | ||
?: return@coroutineScope coinGeckoContractsPrice | ||
|
||
val tetherPrice = fetchTetherPrice() | ||
val currency = currencies.first() | ||
val lifiContractsPrice = notInCoinGeckoTokens | ||
.map { contractAddress -> | ||
async { | ||
contractAddress to getLifiContractPriceInUsd( | ||
chain, | ||
contractAddress | ||
) | ||
} | ||
}.awaitAll().associate { (contractAddress, priceInUsd) -> | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. is it because that lifi only have USD price , thus we use USDT to convert it to local currency? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Exactly |
||
contractAddress to mapOf(currency to priceInUsd * tetherPrice) | ||
} | ||
coinGeckoContractsPrice + lifiContractsPrice | ||
} | ||
} | ||
} | ||
|
||
private suspend fun getLifiContractPriceInUsd( | ||
chain: Chain, | ||
contract: String, | ||
): BigDecimal = try { | ||
BigDecimal( | ||
liQuestApi.getLifiContractPriceUsd( | ||
chain, contract | ||
).priceUSD | ||
) | ||
} catch (e: Exception) { | ||
BigDecimal.ZERO | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. let's do null here, and decide if we want zero in other places There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fixed |
||
} | ||
|
||
private suspend fun fetchPriceWithContractAddress( | ||
chain: Chain, | ||
contractAddress: String, | ||
currency: String, | ||
): CurrencyToPrice? = | ||
coinGeckoApi.getContractsPrice( | ||
fetchPricesWithContractAddress( | ||
chain = chain, | ||
contractAddresses = listOf(contractAddress), | ||
currencies = listOf(currency), | ||
).values.firstOrNull() | ||
|
||
|
||
private suspend fun fetchTetherPrice() = | ||
getPriceByPriceProviderId(TETHER_PRICE_PROVIDER_ID) | ||
|
||
|
||
|
||
companion object { | ||
private const val TETHER_PRICE_PROVIDER_ID = "tether" | ||
} | ||
|
||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed