Skip to content

Commit

Permalink
PWN-961 - add Jupiter API as price source
Browse files Browse the repository at this point in the history
  • Loading branch information
gslevinkov committed Feb 13, 2024
1 parent 1235116 commit dca651b
Show file tree
Hide file tree
Showing 14 changed files with 164 additions and 49 deletions.
17 changes: 4 additions & 13 deletions app/src/main/java/org/p2p/wallet/home/events/SolanaTokensLoader.kt
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
import org.p2p.core.common.di.AppScope
import org.p2p.core.token.Token
import org.p2p.token.service.api.events.manager.TokenServiceEventManager
Expand All @@ -23,10 +22,9 @@ class SolanaTokensLoader(
private val userTokensInteractor: UserTokensInteractor,
private val tokenKeyProvider: TokenKeyProvider,
private val tokenServiceEventManager: TokenServiceEventManager,
private val appScope: AppScope
appScope: AppScope
) {

private val state: MutableStateFlow<SolanaTokenLoadState> = MutableStateFlow(SolanaTokenLoadState.Idle)
private val state = MutableStateFlow<SolanaTokenLoadState>(SolanaTokenLoadState.Idle)

init {
userTokensInteractor.observeUserTokens()
Expand All @@ -40,10 +38,9 @@ class SolanaTokensLoader(
try {
updateState(SolanaTokenLoadState.Loading)

tokenServiceEventManager.subscribe(SolanaTokensRatesEventSubscriber(::saveTokensRates))
val tokens = userTokensInteractor.loadUserTokens(tokenKeyProvider.publicKey.toPublicKey())
userTokensInteractor.saveUserTokens(tokens)
userTokensInteractor.loadUserRates(tokens)
userTokensInteractor.loadAndSaveUserRates(tokens)
} catch (e: CancellationException) {
Timber.d("Loading sol tokens job cancelled")
} catch (e: Throwable) {
Expand All @@ -58,7 +55,7 @@ class SolanaTokensLoader(

val tokens = userTokensInteractor.loadUserTokens(tokenKeyProvider.publicKey.toPublicKey())
userTokensInteractor.saveUserTokens(tokens)
userTokensInteractor.loadUserRates(tokens)
userTokensInteractor.loadAndSaveUserRates(tokens)
} catch (e: CancellationException) {
Timber.d("Refreshing sol tokens job cancelled")
} catch (e: Throwable) {
Expand All @@ -71,12 +68,6 @@ class SolanaTokensLoader(
return userTokensInteractor.getUserTokens()
}

private fun saveTokensRates(list: List<TokenServicePrice>) {
appScope.launch {
userTokensInteractor.saveUserTokensRates(list)
}
}

private fun updateState(newState: SolanaTokenLoadState) {
state.value = newState
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ interface SwapTokensDao {
suspend fun findTokensByMints(mints: Set<String>): List<SwapTokenEntity>

// todo: should be done with pagination
@Query("SELECT * from swap_tokens LIMIT 150")
@Query("SELECT * from swap_tokens LIMIT 100")
suspend fun getAllSwapTokens(): List<SwapTokenEntity>

@Query("SELECT COUNT(*) from swap_tokens")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,9 @@ class SwapInfoLiquidityFeeMapper(
fun mapLiquidityFees(
route: JupiterSwapRouteV6,
): Flow<List<AnyCellItem>> = flow {
// emit banner while we load
emit(listOf(createLiquidityFeeBanner()))

val keyAppFee = route.fees.platformFeeTokenB
val tokenBMint = route.outTokenMint

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@ import org.p2p.token.service.api.events.manager.TokenServiceEventPublisher
import org.p2p.token.service.model.TokenServiceNetwork
import org.p2p.token.service.model.TokenServicePrice
import org.p2p.token.service.repository.TokenServiceRepository
import org.p2p.wallet.home.model.TokenComparator
import org.p2p.wallet.home.model.TokenConverter
import org.p2p.wallet.user.repository.UserLocalRepository
import org.p2p.wallet.user.repository.UserTokensLocalRepository
Expand All @@ -32,12 +31,10 @@ class UserTokensInteractor(
private val dispatchers: CoroutineDispatchers
) {

suspend fun loadUserRates(userTokens: List<Token.Active>) {
suspend fun loadAndSaveUserRates(userTokens: List<Token.Active>) {
val tokenAddresses = userTokens.map { it.tokenServiceAddress }
tokenServiceInteractor.loadTokensPrice(
networkChain = TokenServiceNetwork.SOLANA,
addresses = tokenAddresses
)
val prices = tokenServiceRepository.getTokenPricesByAddress(tokenAddresses, TokenServiceNetwork.SOLANA)
saveUserTokensRates(prices)
}

suspend fun loadUserTokens(publicKey: PublicKey): List<Token.Active> {
Expand Down Expand Up @@ -93,7 +90,6 @@ class UserTokensInteractor(
val cachedTokens = userTokensLocalRepository.getUserTokens()
tokens
.updateVisibilityState(cachedTokens)
.sortedWith(TokenComparator())
.let { userTokensLocalRepository.updateTokens(it) }
}

Expand All @@ -110,7 +106,8 @@ class UserTokensInteractor(
}

fun observeUserTokens(): Flow<List<Token.Active>> {
return userTokensLocalRepository.observeUserTokens().map { it.filterTokensByAvailability() }
return userTokensLocalRepository.observeUserTokens()
.map { it.filterTokensByAvailability() }
}

fun observeUserToken(mintAddress: Base58String): Flow<Token.Active> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import org.p2p.core.token.Token
import org.p2p.core.utils.scaleShort
import org.p2p.token.service.model.TokenServicePrice
import org.p2p.wallet.home.db.TokenDao
import org.p2p.wallet.home.model.TokenComparator
import org.p2p.wallet.home.model.TokenConverter

class UserTokensDatabaseRepository(
Expand All @@ -33,13 +34,15 @@ class UserTokensDatabaseRepository(

override fun observeUserTokens(): Flow<List<Token.Active>> {
return tokensDao.getTokensFlow()
.map { tokenEntities ->
tokenEntities.map { TokenConverter.fromDatabase(it) }
.map {
it.map(TokenConverter::fromDatabase)
.sortedWith(TokenComparator())
}
}

override fun observeUserToken(mintAddress: Base58String): Flow<Token.Active> =
tokensDao.getSingleTokenFlow(mintAddress.base58Value).map { TokenConverter.fromDatabase(it) }
tokensDao.getSingleTokenFlow(mintAddress.base58Value)
.map(TokenConverter::fromDatabase)

override suspend fun updateTokenBalance(publicKey: Base58String, newTotal: BigDecimal, newTotalInUsd: BigDecimal?) {
tokensDao.updateTokenTotal(
Expand All @@ -58,7 +61,8 @@ class UserTokensDatabaseRepository(

override suspend fun getUserTokens(): List<Token.Active> {
return tokensDao.getTokens()
.map { TokenConverter.fromDatabase(it) }
.map(TokenConverter::fromDatabase)
.sortedWith(TokenComparator())
}

override suspend fun clear() {
Expand Down
5 changes: 2 additions & 3 deletions core/src/main/java/org/p2p/core/network/NetworkCoreModule.kt
Original file line number Diff line number Diff line change
Expand Up @@ -95,9 +95,8 @@ object NetworkCoreModule : InjectionModule {
}

fun Scope.httpLoggingInterceptor(logTag: String): HttpLoggingInterceptor {
return HttpLoggingInterceptor(DebugHttpLoggingLogger(gson = get(), logTag = logTag)).apply {
level = HttpLoggingInterceptor.Level.BODY
}
return HttpLoggingInterceptor(DebugHttpLoggingLogger(gson = get(), logTag = logTag))
.apply { level = HttpLoggingInterceptor.Level.BODY }
}

fun Scope.getClient(
Expand Down
10 changes: 6 additions & 4 deletions core/src/main/java/org/p2p/core/utils/AmountExtensions.kt
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,10 @@ package org.p2p.core.utils
import java.math.BigDecimal
import java.math.BigInteger
import java.math.RoundingMode
import kotlin.math.pow
import org.p2p.core.token.Token
import org.p2p.core.utils.Constants.FIAT_FRACTION_LENGTH

private val POWER_VALUE = BigDecimal("10")
const val SOL_DECIMALS = 9
const val MOONPAY_DECIMAL = 2
const val STRIGA_FIAT_DECIMALS = 2
Expand All @@ -26,7 +26,7 @@ fun String?.toBigIntegerOrZero(): BigInteger {
return this?.toBigIntegerOrNull() ?: BigInteger.ZERO
}

fun Int.toPowerValue(): BigDecimal = POWER_VALUE.pow(this)
fun Int.toPowerValue(): BigDecimal = BigDecimal.TEN.pow(this)

fun BigDecimal.scaleShortOrFirstNotZero(): BigDecimal {
return if (isZero()) {
Expand Down Expand Up @@ -56,10 +56,12 @@ fun BigDecimal.scaleLong(decimals: Int = SCALE_VALUE_LONG): BigDecimal =

// do not use BigDecimal(double) sometimes it makes the amount less
// example: pass 0.030, get 0.029
fun BigInteger.fromLamports(decimals: Int): BigDecimal =
(this.toBigDecimal() / (POWER_VALUE.pow(decimals)))
fun BigInteger.fromLamports(decimals: Int): BigDecimal {
return (this.toDouble() / (10.0.pow(decimals)))
.toBigDecimal()
.stripTrailingZeros() // removing zeros, case: 0.02000 -> 0.02
.scaleLong(decimals)
}

fun BigDecimal.toLamports(decimals: Int): BigInteger =
this.multiply(decimals.toPowerValue()).toBigInteger()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ import org.p2p.core.common.di.InjectionModule
import org.p2p.core.network.NetworkCoreModule.getRetrofit
import org.p2p.core.network.environment.NetworkServicesUrlProvider
import org.p2p.core.rpc.RPC_JSON_QUALIFIER
import org.p2p.token.service.api.JupiterPricesDataSource
import org.p2p.token.service.api.TokenServiceDataSource
import org.p2p.token.service.api.TokenServiceRemoteDataSource
import org.p2p.token.service.api.events.manager.TokenServiceEventManager
import org.p2p.token.service.api.events.manager.TokenServiceEventPublisher
Expand All @@ -25,6 +27,7 @@ import org.p2p.token.service.repository.metadata.TokenMetadataInMemoryRepository
import org.p2p.token.service.repository.metadata.TokenMetadataLocalRepository
import org.p2p.token.service.repository.metadata.TokenMetadataRemoteRepository
import org.p2p.token.service.repository.metadata.TokenMetadataRepository
import org.p2p.token.service.repository.price.JupiterTokenPriceRepository
import org.p2p.token.service.repository.price.TokenPriceDatabaseRepository
import org.p2p.token.service.repository.price.TokenPriceLocalRepository
import org.p2p.token.service.repository.price.TokenPriceRemoteRepository
Expand All @@ -35,14 +38,18 @@ object TokenServiceModule : InjectionModule {

override fun create() = module {
includes(TokenServiceDatabaseModule.create())
single<org.p2p.token.service.api.TokenServiceDataSource> {
single<TokenServiceDataSource> {
TokenServiceRemoteDataSource(
api = get<Retrofit>(named(TOKEN_SERVICE_RETROFIT_QUALIFIER)).create(),
gson = get(named(RPC_JSON_QUALIFIER)),
urlProvider = get()
)
}

single<JupiterPricesDataSource> {
getRetrofit(baseUrl = "https://price.jup.ag/", tag = null, interceptor = null).create()
}

single(named(TOKEN_SERVICE_RETROFIT_QUALIFIER)) {
val url = get<NetworkServicesUrlProvider>()
getRetrofit(
Expand All @@ -52,9 +59,10 @@ object TokenServiceModule : InjectionModule {
)
}

single<TokenMetadataLocalRepository> { TokenMetadataInMemoryRepository() }
single<TokenPriceLocalRepository> { TokenPriceDatabaseRepository(get(), get()) }
factory<TokenPriceRepository> { TokenPriceRemoteRepository(get(), get()) }
singleOf(::TokenMetadataInMemoryRepository) bind TokenMetadataLocalRepository::class
singleOf(::TokenPriceDatabaseRepository) bind TokenPriceLocalRepository::class
factoryOf(::TokenPriceRemoteRepository) bind TokenPriceRepository::class
factoryOf(::JupiterTokenPriceRepository)
factoryOf(::TokenServiceAmountsRemoteConverter) bind TokenServiceAmountsConverter::class

factoryOf(::TokenServiceMapper)
Expand All @@ -64,7 +72,8 @@ object TokenServiceModule : InjectionModule {
priceRemoteRepository = get(),
priceLocalRepository = get(),
metadataLocalRepository = get(),
metadataRemoteRepository = get()
metadataRemoteRepository = get(),
jupiterPriceRepository = get()
)
}
singleOf(::TokenServiceEventPublisher)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package org.p2p.token.service.api

import retrofit2.http.GET
import retrofit2.http.Query

internal interface JupiterPricesDataSource {
@GET("v4/price")
suspend fun getPrices(@Query("ids", encoded = true) tokenMints: String): JupiterPricesRootResponse
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package org.p2p.token.service.api

import com.google.gson.annotations.SerializedName

internal class JupiterPricesRootResponse(
@SerializedName("data")
val tokenMintsToPrices: Map<String, JupiterPricesResponse>
)

internal class JupiterPricesResponse(
@SerializedName("id")
val mintAddress: String,
@SerializedName("mintSymbol")
val tokenSymbol: String,
// 1 unit of the token worth in USDC
@SerializedName("price")
val usdPrice: String
)
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import org.p2p.core.dispatchers.CoroutineDispatchers
import org.p2p.token.service.model.TokenServiceNetwork
import org.p2p.token.service.repository.TokenServiceRepository

@Deprecated("Maybe deprecated due to fetching prices from remote only without cache")
class TokenServiceEventPublisher(
private val tokenServiceInteractor: TokenServiceRepository,
private val eventManager: TokenServiceEventManager,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,13 @@ interface TokenServiceRepository {
networkChain: TokenServiceNetwork,
forceRemote: Boolean = false
): TokenServicePrice?

suspend fun getTokenPricesByAddress(
tokenAddress: List<String>,
networkChain: TokenServiceNetwork,
forceRemote: Boolean = false
): List<TokenServicePrice>

fun findTokenMetadataByAddress(
networkChain: TokenServiceNetwork,
tokenAddress: String
Expand Down
Original file line number Diff line number Diff line change
@@ -1,28 +1,32 @@
package org.p2p.token.service.repository

import timber.log.Timber
import kotlinx.coroutines.flow.Flow
import org.p2p.token.service.model.TokenServiceMetadata
import org.p2p.token.service.model.TokenServiceNetwork
import org.p2p.token.service.model.TokenServicePrice
import org.p2p.token.service.repository.metadata.TokenMetadataLocalRepository
import org.p2p.token.service.repository.metadata.TokenMetadataRepository
import org.p2p.token.service.repository.price.JupiterTokenPriceRepository
import org.p2p.token.service.repository.price.TokenPriceLocalRepository
import org.p2p.token.service.repository.price.TokenPriceRepository

internal class TokenServiceRepositoryImpl(
private val priceRemoteRepository: TokenPriceRepository,
private val jupiterPriceRepository: JupiterTokenPriceRepository,
private val priceLocalRepository: TokenPriceLocalRepository,
private val metadataLocalRepository: TokenMetadataLocalRepository,
private val metadataRemoteRepository: TokenMetadataRepository
) : TokenServiceRepository {

@Deprecated("Use getTokenPriceByAddress")
override suspend fun fetchTokenPricesForTokens(chain: TokenServiceNetwork, tokenAddresses: List<String>) {
val result = priceRemoteRepository.loadTokensPrice(
chain = chain,
addresses = tokenAddresses
)
val tokensPrices = result.flatMap { it.items }
priceLocalRepository.saveTokensPrice(tokensPrices)
// val result = priceRemoteRepository.loadTokensPrice(
// chain = chain,
// addresses = tokenAddresses
// )
// val tokensPrices = result.flatMap { it.items }
// priceLocalRepository.saveTokensPrice(tokensPrices)
}

override suspend fun fetchMetadataForTokens(
Expand All @@ -46,12 +50,25 @@ internal class TokenServiceRepositoryImpl(
networkChain: TokenServiceNetwork,
forceRemote: Boolean
): TokenServicePrice? {
val localPrice = priceLocalRepository.findTokenPriceByAddress(tokenAddress, networkChain)
if (forceRemote || localPrice == null) {
fetchTokenPricesForTokens(networkChain, listOf(tokenAddress))
}
return getTokenPricesByAddress(listOf(tokenAddress), networkChain).firstOrNull()
}

return priceLocalRepository.findTokenPriceByAddress(tokenAddress, networkChain)
override suspend fun getTokenPricesByAddress(
tokenAddress: List<String>,
networkChain: TokenServiceNetwork,
forceRemote: Boolean
): List<TokenServicePrice> = try {
when (networkChain) {
TokenServiceNetwork.SOLANA -> {
jupiterPriceRepository.loadTokensPrice(networkChain, tokenAddress).items
}
TokenServiceNetwork.ETHEREUM -> {
priceRemoteRepository.loadTokensPrice(networkChain, tokenAddress).flatMap { it.items }
}
}
} catch (error: Exception) {
Timber.i(error)
emptyList()
}

override fun findTokenMetadataByAddress(
Expand Down
Loading

0 comments on commit dca651b

Please sign in to comment.