diff --git a/app/src/main/java/org/p2p/wallet/home/events/SolanaTokensLoader.kt b/app/src/main/java/org/p2p/wallet/home/events/SolanaTokensLoader.kt index f4b1cd2a8a..7c28e5a172 100644 --- a/app/src/main/java/org/p2p/wallet/home/events/SolanaTokensLoader.kt +++ b/app/src/main/java/org/p2p/wallet/home/events/SolanaTokensLoader.kt @@ -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 @@ -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 = MutableStateFlow(SolanaTokenLoadState.Idle) + private val state = MutableStateFlow(SolanaTokenLoadState.Idle) init { userTokensInteractor.observeUserTokens() @@ -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) { @@ -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) { @@ -71,12 +68,6 @@ class SolanaTokensLoader( return userTokensInteractor.getUserTokens() } - private fun saveTokensRates(list: List) { - appScope.launch { - userTokensInteractor.saveUserTokensRates(list) - } - } - private fun updateState(newState: SolanaTokenLoadState) { state.value = newState } diff --git a/app/src/main/java/org/p2p/wallet/jupiter/repository/tokens/db/SwapTokensDao.kt b/app/src/main/java/org/p2p/wallet/jupiter/repository/tokens/db/SwapTokensDao.kt index a2c86c579b..fd88560e3c 100644 --- a/app/src/main/java/org/p2p/wallet/jupiter/repository/tokens/db/SwapTokensDao.kt +++ b/app/src/main/java/org/p2p/wallet/jupiter/repository/tokens/db/SwapTokensDao.kt @@ -35,7 +35,7 @@ interface SwapTokensDao { suspend fun findTokensByMints(mints: Set): List // todo: should be done with pagination - @Query("SELECT * from swap_tokens LIMIT 150") + @Query("SELECT * from swap_tokens LIMIT 100") suspend fun getAllSwapTokens(): List @Query("SELECT COUNT(*) from swap_tokens") diff --git a/app/src/main/java/org/p2p/wallet/jupiter/ui/info/SwapInfoLiquidityFeeMapper.kt b/app/src/main/java/org/p2p/wallet/jupiter/ui/info/SwapInfoLiquidityFeeMapper.kt index 0e6c538dec..221ad3ac38 100644 --- a/app/src/main/java/org/p2p/wallet/jupiter/ui/info/SwapInfoLiquidityFeeMapper.kt +++ b/app/src/main/java/org/p2p/wallet/jupiter/ui/info/SwapInfoLiquidityFeeMapper.kt @@ -43,6 +43,9 @@ class SwapInfoLiquidityFeeMapper( fun mapLiquidityFees( route: JupiterSwapRouteV6, ): Flow> = flow { + // emit banner while we load + emit(listOf(createLiquidityFeeBanner())) + val keyAppFee = route.fees.platformFeeTokenB val tokenBMint = route.outTokenMint diff --git a/app/src/main/java/org/p2p/wallet/user/interactor/UserTokensInteractor.kt b/app/src/main/java/org/p2p/wallet/user/interactor/UserTokensInteractor.kt index f682288cf8..cc175737dc 100644 --- a/app/src/main/java/org/p2p/wallet/user/interactor/UserTokensInteractor.kt +++ b/app/src/main/java/org/p2p/wallet/user/interactor/UserTokensInteractor.kt @@ -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 @@ -32,12 +31,10 @@ class UserTokensInteractor( private val dispatchers: CoroutineDispatchers ) { - suspend fun loadUserRates(userTokens: List) { + suspend fun loadAndSaveUserRates(userTokens: List) { 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 { @@ -93,7 +90,6 @@ class UserTokensInteractor( val cachedTokens = userTokensLocalRepository.getUserTokens() tokens .updateVisibilityState(cachedTokens) - .sortedWith(TokenComparator()) .let { userTokensLocalRepository.updateTokens(it) } } @@ -110,7 +106,8 @@ class UserTokensInteractor( } fun observeUserTokens(): Flow> { - return userTokensLocalRepository.observeUserTokens().map { it.filterTokensByAvailability() } + return userTokensLocalRepository.observeUserTokens() + .map { it.filterTokensByAvailability() } } fun observeUserToken(mintAddress: Base58String): Flow { diff --git a/app/src/main/java/org/p2p/wallet/user/repository/UserTokensDatabaseRepository.kt b/app/src/main/java/org/p2p/wallet/user/repository/UserTokensDatabaseRepository.kt index 98f77cfafc..f89dd55ce9 100644 --- a/app/src/main/java/org/p2p/wallet/user/repository/UserTokensDatabaseRepository.kt +++ b/app/src/main/java/org/p2p/wallet/user/repository/UserTokensDatabaseRepository.kt @@ -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( @@ -33,13 +34,15 @@ class UserTokensDatabaseRepository( override fun observeUserTokens(): Flow> { return tokensDao.getTokensFlow() - .map { tokenEntities -> - tokenEntities.map { TokenConverter.fromDatabase(it) } + .map { + it.map(TokenConverter::fromDatabase) + .sortedWith(TokenComparator()) } } override fun observeUserToken(mintAddress: Base58String): Flow = - 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( @@ -58,7 +61,8 @@ class UserTokensDatabaseRepository( override suspend fun getUserTokens(): List { return tokensDao.getTokens() - .map { TokenConverter.fromDatabase(it) } + .map(TokenConverter::fromDatabase) + .sortedWith(TokenComparator()) } override suspend fun clear() { diff --git a/core/src/main/java/org/p2p/core/network/NetworkCoreModule.kt b/core/src/main/java/org/p2p/core/network/NetworkCoreModule.kt index 31d609bd31..1127c2ef63 100644 --- a/core/src/main/java/org/p2p/core/network/NetworkCoreModule.kt +++ b/core/src/main/java/org/p2p/core/network/NetworkCoreModule.kt @@ -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( diff --git a/core/src/main/java/org/p2p/core/utils/AmountExtensions.kt b/core/src/main/java/org/p2p/core/utils/AmountExtensions.kt index 691ef0c3c5..9d948b15e2 100644 --- a/core/src/main/java/org/p2p/core/utils/AmountExtensions.kt +++ b/core/src/main/java/org/p2p/core/utils/AmountExtensions.kt @@ -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 @@ -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()) { @@ -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() diff --git a/token-service/src/main/java/org/p2p/token/service/TokenServiceModule.kt b/token-service/src/main/java/org/p2p/token/service/TokenServiceModule.kt index 932acd4522..aca43e81dd 100644 --- a/token-service/src/main/java/org/p2p/token/service/TokenServiceModule.kt +++ b/token-service/src/main/java/org/p2p/token/service/TokenServiceModule.kt @@ -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 @@ -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 @@ -35,7 +38,7 @@ object TokenServiceModule : InjectionModule { override fun create() = module { includes(TokenServiceDatabaseModule.create()) - single { + single { TokenServiceRemoteDataSource( api = get(named(TOKEN_SERVICE_RETROFIT_QUALIFIER)).create(), gson = get(named(RPC_JSON_QUALIFIER)), @@ -43,6 +46,10 @@ object TokenServiceModule : InjectionModule { ) } + single { + getRetrofit(baseUrl = "https://price.jup.ag/", tag = null, interceptor = null).create() + } + single(named(TOKEN_SERVICE_RETROFIT_QUALIFIER)) { val url = get() getRetrofit( @@ -52,9 +59,10 @@ object TokenServiceModule : InjectionModule { ) } - single { TokenMetadataInMemoryRepository() } - single { TokenPriceDatabaseRepository(get(), get()) } - factory { 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) @@ -64,7 +72,8 @@ object TokenServiceModule : InjectionModule { priceRemoteRepository = get(), priceLocalRepository = get(), metadataLocalRepository = get(), - metadataRemoteRepository = get() + metadataRemoteRepository = get(), + jupiterPriceRepository = get() ) } singleOf(::TokenServiceEventPublisher) diff --git a/token-service/src/main/java/org/p2p/token/service/api/JupiterPricesDataSource.kt b/token-service/src/main/java/org/p2p/token/service/api/JupiterPricesDataSource.kt new file mode 100644 index 0000000000..a042f0e2cd --- /dev/null +++ b/token-service/src/main/java/org/p2p/token/service/api/JupiterPricesDataSource.kt @@ -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 +} diff --git a/token-service/src/main/java/org/p2p/token/service/api/JupiterPricesRootResponse.kt b/token-service/src/main/java/org/p2p/token/service/api/JupiterPricesRootResponse.kt new file mode 100644 index 0000000000..8cb118656a --- /dev/null +++ b/token-service/src/main/java/org/p2p/token/service/api/JupiterPricesRootResponse.kt @@ -0,0 +1,18 @@ +package org.p2p.token.service.api + +import com.google.gson.annotations.SerializedName + +internal class JupiterPricesRootResponse( + @SerializedName("data") + val tokenMintsToPrices: Map +) + +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 +) diff --git a/token-service/src/main/java/org/p2p/token/service/api/events/manager/TokenServiceEventPublisher.kt b/token-service/src/main/java/org/p2p/token/service/api/events/manager/TokenServiceEventPublisher.kt index 31e47730f7..d4bc1e9b44 100644 --- a/token-service/src/main/java/org/p2p/token/service/api/events/manager/TokenServiceEventPublisher.kt +++ b/token-service/src/main/java/org/p2p/token/service/api/events/manager/TokenServiceEventPublisher.kt @@ -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, diff --git a/token-service/src/main/java/org/p2p/token/service/repository/TokenServiceRepository.kt b/token-service/src/main/java/org/p2p/token/service/repository/TokenServiceRepository.kt index 636eda2728..e3cfe2ccd9 100644 --- a/token-service/src/main/java/org/p2p/token/service/repository/TokenServiceRepository.kt +++ b/token-service/src/main/java/org/p2p/token/service/repository/TokenServiceRepository.kt @@ -21,6 +21,13 @@ interface TokenServiceRepository { networkChain: TokenServiceNetwork, forceRemote: Boolean = false ): TokenServicePrice? + + suspend fun getTokenPricesByAddress( + tokenAddress: List, + networkChain: TokenServiceNetwork, + forceRemote: Boolean = false + ): List + fun findTokenMetadataByAddress( networkChain: TokenServiceNetwork, tokenAddress: String diff --git a/token-service/src/main/java/org/p2p/token/service/repository/TokenServiceRepositoryImpl.kt b/token-service/src/main/java/org/p2p/token/service/repository/TokenServiceRepositoryImpl.kt index 6da967b328..44b2bda58a 100644 --- a/token-service/src/main/java/org/p2p/token/service/repository/TokenServiceRepositoryImpl.kt +++ b/token-service/src/main/java/org/p2p/token/service/repository/TokenServiceRepositoryImpl.kt @@ -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) { - 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( @@ -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, + networkChain: TokenServiceNetwork, + forceRemote: Boolean + ): List = 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( diff --git a/token-service/src/main/java/org/p2p/token/service/repository/price/JupiterTokenPriceRepository.kt b/token-service/src/main/java/org/p2p/token/service/repository/price/JupiterTokenPriceRepository.kt new file mode 100644 index 0000000000..dcd819933d --- /dev/null +++ b/token-service/src/main/java/org/p2p/token/service/repository/price/JupiterTokenPriceRepository.kt @@ -0,0 +1,58 @@ +package org.p2p.token.service.repository.price + +import timber.log.Timber +import kotlinx.coroutines.withContext +import org.p2p.core.dispatchers.CoroutineDispatchers +import org.p2p.core.utils.Constants +import org.p2p.token.service.api.JupiterPricesDataSource +import org.p2p.token.service.model.TokenRate +import org.p2p.token.service.model.TokenServiceNetwork +import org.p2p.token.service.model.TokenServicePrice +import org.p2p.token.service.model.TokenServiceQueryResult + +internal class JupiterTokenPriceRepository( + private val api: JupiterPricesDataSource, + private val dispatchers: CoroutineDispatchers +) { + suspend fun loadTokensPrice( + chain: TokenServiceNetwork, + addresses: List + ): TokenServiceQueryResult = withContext(dispatchers.io) { + try { + require(chain == TokenServiceNetwork.SOLANA) { "Only Solana tokens prices can be loaded" } + + val isNativeRequested = Constants.TOKEN_SERVICE_NATIVE_SOL_TOKEN in addresses + + val mintsAsQuery = addresses.joinToString(separator = ",") { + if (it == Constants.TOKEN_SERVICE_NATIVE_SOL_TOKEN) Constants.WRAPPED_SOL_MINT else it + } + val tokenMintsToRates = api.getPrices(mintsAsQuery).tokenMintsToPrices + .mapValues { it.value.usdPrice } + .run { + if (isNativeRequested) { + val nativeSolRate = Constants.TOKEN_SERVICE_NATIVE_SOL_TOKEN to this[Constants.WRAPPED_SOL_MINT] + this + nativeSolRate + } else { + this + } + } + + val prices = addresses.map { mintAddress -> + val rate = tokenMintsToRates[mintAddress]?.toBigDecimal() + if (rate == null) { + Timber.e("USD rate for $mintAddress not found!") + } + + TokenServicePrice( + tokenAddress = mintAddress, + rate = TokenRate(rate), + network = chain + ) + } + TokenServiceQueryResult(networkChain = chain, items = prices) + } catch (error: Exception) { + Timber.i(error, "Failed fetching prices") + throw error + } + } +}