From 6aaa93b7c2e483bcfa55b47ad48737a88c722383 Mon Sep 17 00:00:00 2001 From: HashEngineering Date: Wed, 20 Sep 2023 14:58:00 -0700 Subject: [PATCH] feat(maya): add currency picker with local currency prices --- .../ui/radio_group/RadioGroupAdapter.kt | 2 +- .../ui/recyclerview/IconifiedListAdapter.kt | 61 ++++++ .../src/main/res/layout/iconifieditem_row.xml | 103 ++++++++++ .../integrations/maya/api/ApiStatuses.kt | 7 + .../integrations/maya/api/ExchangeRateApi.kt | 11 + .../maya/api/FiatExchangeRateApi.kt | 85 ++++++++ .../wallet/integrations/maya/api/MayaApi.kt | 188 ++++++++++++++++++ .../maya/api/MayaBlockchainApi.kt | 8 + .../integrations/maya/api/MayaWebApi.kt | 45 +++++ .../integrations/maya/api/RemoteDataSource.kt | 33 +++ .../maya/data/CryptoCurrencyItem.kt | 3 + .../wallet/integrations/maya/di/MayaModule.kt | 54 +++++ .../maya/model/ExchangeRateResponse.kt | 14 ++ .../integrations/maya/model/PoolInfo.kt | 42 ++++ .../ui/MayaCryptoCurrencyPickerFragment.kt | 119 +++++++++++ .../maya/ui/MayaPortalFragment.kt | 6 +- .../integrations/maya/ui/MayaViewModel.kt | 80 +++++++- .../integrations/maya/utils/MayaConfig.kt | 3 + .../integrations/maya/utils/MayaConstants.kt | 12 ++ .../res/layout/fragment_currency_picker.xml | 122 ++++++++++++ .../maya/src/main/res/navigation/nav_maya.xml | 26 +++ .../maya/src/main/res/values/strings-maya.xml | 13 ++ wallet/res/navigation/nav_home.xml | 9 +- 23 files changed, 1029 insertions(+), 17 deletions(-) create mode 100644 common/src/main/java/org/dash/wallet/common/ui/recyclerview/IconifiedListAdapter.kt create mode 100644 common/src/main/res/layout/iconifieditem_row.xml create mode 100644 integrations/maya/src/main/java/org/dash/wallet/integrations/maya/api/ApiStatuses.kt create mode 100644 integrations/maya/src/main/java/org/dash/wallet/integrations/maya/api/ExchangeRateApi.kt create mode 100644 integrations/maya/src/main/java/org/dash/wallet/integrations/maya/api/FiatExchangeRateApi.kt create mode 100644 integrations/maya/src/main/java/org/dash/wallet/integrations/maya/api/MayaApi.kt create mode 100644 integrations/maya/src/main/java/org/dash/wallet/integrations/maya/api/MayaBlockchainApi.kt create mode 100644 integrations/maya/src/main/java/org/dash/wallet/integrations/maya/api/MayaWebApi.kt create mode 100644 integrations/maya/src/main/java/org/dash/wallet/integrations/maya/api/RemoteDataSource.kt create mode 100644 integrations/maya/src/main/java/org/dash/wallet/integrations/maya/data/CryptoCurrencyItem.kt create mode 100644 integrations/maya/src/main/java/org/dash/wallet/integrations/maya/di/MayaModule.kt create mode 100644 integrations/maya/src/main/java/org/dash/wallet/integrations/maya/model/ExchangeRateResponse.kt create mode 100644 integrations/maya/src/main/java/org/dash/wallet/integrations/maya/model/PoolInfo.kt create mode 100644 integrations/maya/src/main/java/org/dash/wallet/integrations/maya/ui/MayaCryptoCurrencyPickerFragment.kt create mode 100644 integrations/maya/src/main/res/layout/fragment_currency_picker.xml create mode 100644 integrations/maya/src/main/res/navigation/nav_maya.xml diff --git a/common/src/main/java/org/dash/wallet/common/ui/radio_group/RadioGroupAdapter.kt b/common/src/main/java/org/dash/wallet/common/ui/radio_group/RadioGroupAdapter.kt index 83dc3fbb84..c7687b9861 100644 --- a/common/src/main/java/org/dash/wallet/common/ui/radio_group/RadioGroupAdapter.kt +++ b/common/src/main/java/org/dash/wallet/common/ui/radio_group/RadioGroupAdapter.kt @@ -83,7 +83,7 @@ class RadioGroupAdapter( } } -class RadioButtonViewHolder( +open class RadioButtonViewHolder( val binding: RadiobuttonRowBinding, private val isCheckMark: Boolean ) : RecyclerView.ViewHolder(binding.root) { diff --git a/common/src/main/java/org/dash/wallet/common/ui/recyclerview/IconifiedListAdapter.kt b/common/src/main/java/org/dash/wallet/common/ui/recyclerview/IconifiedListAdapter.kt new file mode 100644 index 0000000000..5871997ee6 --- /dev/null +++ b/common/src/main/java/org/dash/wallet/common/ui/recyclerview/IconifiedListAdapter.kt @@ -0,0 +1,61 @@ +/* + * Copyright 2021 Dash Core Group. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.dash.wallet.common.ui.recyclerview + +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.core.view.isVisible +import androidx.recyclerview.widget.ListAdapter +import org.dash.wallet.common.R +import org.dash.wallet.common.databinding.RadiobuttonRowBinding +import org.dash.wallet.common.ui.radio_group.IconifiedViewItem +import org.dash.wallet.common.ui.radio_group.RadioButtonViewHolder +import org.dash.wallet.common.ui.radio_group.RadioGroupAdapter +import org.dash.wallet.common.ui.setRoundedRippleBackground + +class IconifiedListAdapter( + private val clickListener: (IconifiedViewItem, Int) -> Unit +): ListAdapter(RadioGroupAdapter.DiffCallback()) { + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): IconifiedViewHolder { + val inflater = LayoutInflater.from(parent.context) + val binding = RadiobuttonRowBinding.inflate(inflater, parent, false) + + return IconifiedViewHolder(binding) + } + + override fun onBindViewHolder(holder: IconifiedViewHolder, position: Int) { + holder.itemView.isSelected = false + val item = getItem(position) + holder.bind(item) + + holder.binding.root.setOnClickListener { + clickListener.invoke(item, position) + } + } +} + +class IconifiedViewHolder( + binding: RadiobuttonRowBinding +) : RadioButtonViewHolder(binding, false) { + fun bind(option: IconifiedViewItem) { + super.bind(option, false) + binding.checkmarkFrame.isVisible = false + itemView.setRoundedRippleBackground(R.style.ListViewButtonBackground) + } +} diff --git a/common/src/main/res/layout/iconifieditem_row.xml b/common/src/main/res/layout/iconifieditem_row.xml new file mode 100644 index 0000000000..1078095399 --- /dev/null +++ b/common/src/main/res/layout/iconifieditem_row.xml @@ -0,0 +1,103 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/integrations/maya/src/main/java/org/dash/wallet/integrations/maya/api/ApiStatuses.kt b/integrations/maya/src/main/java/org/dash/wallet/integrations/maya/api/ApiStatuses.kt new file mode 100644 index 0000000000..faa35386f5 --- /dev/null +++ b/integrations/maya/src/main/java/org/dash/wallet/integrations/maya/api/ApiStatuses.kt @@ -0,0 +1,7 @@ +package org.dash.wallet.integrations.maya.api + +open class MayaException(message: String): Exception(message) { + companion object { + const val SWAP_ERROR = "deposit_error" + } +} \ No newline at end of file diff --git a/integrations/maya/src/main/java/org/dash/wallet/integrations/maya/api/ExchangeRateApi.kt b/integrations/maya/src/main/java/org/dash/wallet/integrations/maya/api/ExchangeRateApi.kt new file mode 100644 index 0000000000..efaec5dcac --- /dev/null +++ b/integrations/maya/src/main/java/org/dash/wallet/integrations/maya/api/ExchangeRateApi.kt @@ -0,0 +1,11 @@ +package org.dash.wallet.integrations.maya.api + +import org.dash.wallet.integrations.maya.model.ExchangeRateResponse +import retrofit2.Response +import retrofit2.http.GET +import retrofit2.http.Query + +interface ExchangeRateApi { + @GET("latest") + suspend fun getRates(@Query("base") baseCurrencyCode: String): Response +} diff --git a/integrations/maya/src/main/java/org/dash/wallet/integrations/maya/api/FiatExchangeRateApi.kt b/integrations/maya/src/main/java/org/dash/wallet/integrations/maya/api/FiatExchangeRateApi.kt new file mode 100644 index 0000000000..721d16cf09 --- /dev/null +++ b/integrations/maya/src/main/java/org/dash/wallet/integrations/maya/api/FiatExchangeRateApi.kt @@ -0,0 +1,85 @@ +package org.dash.wallet.integrations.maya.api + +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.asCoroutineDispatcher +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.launch +import org.dash.wallet.common.data.entity.ExchangeRate +import org.dash.wallet.integrations.maya.utils.MayaConstants +import org.slf4j.Logger +import org.slf4j.LoggerFactory +import java.util.concurrent.Executors +import java.util.concurrent.TimeUnit +import javax.inject.Inject + +class FiatExchangeRateApiAggregator @Inject constructor( + val exchangeRateApi: ExchangeRateApi +) { + companion object { + val log: Logger = LoggerFactory.getLogger(FiatExchangeRateApiAggregator::class.java) + } + suspend fun getRate(currencyCode: String): ExchangeRate? { + val response = exchangeRateApi.getRates(MayaConstants.DEFAULT_EXCHANGE_CURRENCY).body() + val exchangeRate = response?.rates?.get(currencyCode) ?: 0.0 + log.info("exchange rate: {} {}", exchangeRate, currencyCode) + return if (exchangeRate != 0.0) { + ExchangeRate(currencyCode, exchangeRate.toString()) + } else { + null + } + } +} + +interface FiatExchangeRateProvider { + val fiatExchangeRate: Flow + fun observeFiatRates(): Flow> + fun observeFiatRate(currencyCode: String): Flow +} + +class FiatExchangeRateAggregatedProvider @Inject constructor( + val fiatExchangeRateApi: FiatExchangeRateApiAggregator +) : FiatExchangeRateProvider { + companion object { + private val log = LoggerFactory.getLogger(FiatExchangeRateApiAggregator::class.java) + private val UPDATE_FREQ_MS = TimeUnit.SECONDS.toMillis(30) + } + + private val responseScope = CoroutineScope( + Executors.newSingleThreadExecutor().asCoroutineDispatcher() + ) + private var poolListLastUpdated: Long = 0 + override val fiatExchangeRate = MutableStateFlow(ExchangeRate(MayaConstants.DEFAULT_EXCHANGE_CURRENCY, "1.0")) + override fun observeFiatRates(): Flow> { + TODO("Not yet implemented") + } + + override fun observeFiatRate(currencyCode: String): Flow { + if (shouldRefresh()) { + refreshRates(currencyCode) + } + return fiatExchangeRate + } + + private fun refreshRates(currencyCode: String) { + if (!shouldRefresh()) { + return + } + + responseScope.launch { + updateExchangeRates(currencyCode) + poolListLastUpdated = System.currentTimeMillis() + } + } + + private fun shouldRefresh(): Boolean { + val now = System.currentTimeMillis() + return poolListLastUpdated == 0L || now - poolListLastUpdated > UPDATE_FREQ_MS + } + + suspend fun updateExchangeRates(currencyCode: String) { + fiatExchangeRateApi.getRate(currencyCode)?.let { rate -> + fiatExchangeRate.value = rate + } + } +} diff --git a/integrations/maya/src/main/java/org/dash/wallet/integrations/maya/api/MayaApi.kt b/integrations/maya/src/main/java/org/dash/wallet/integrations/maya/api/MayaApi.kt new file mode 100644 index 0000000000..ae65436934 --- /dev/null +++ b/integrations/maya/src/main/java/org/dash/wallet/integrations/maya/api/MayaApi.kt @@ -0,0 +1,188 @@ +package org.dash.wallet.integrations.maya.api + +import android.content.Context +import android.content.Intent +import dagger.hilt.android.qualifiers.ApplicationContext +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.asCoroutineDispatcher +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.filterNot +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.launch +import kotlinx.coroutines.runBlocking +import org.bitcoinj.utils.Fiat +import org.dash.wallet.common.Configuration +import org.dash.wallet.common.WalletDataProvider +import org.dash.wallet.common.services.AuthenticationManager +import org.dash.wallet.common.services.NotificationService +import org.dash.wallet.common.services.TransactionMetadataProvider +import org.dash.wallet.common.services.analytics.AnalyticsService +import org.dash.wallet.integrations.maya.MayaWebApi +import org.dash.wallet.integrations.maya.model.PoolInfo +import org.dash.wallet.integrations.maya.utils.MayaConfig +import org.dash.wallet.integrations.maya.utils.MayaConstants +import org.slf4j.LoggerFactory +import java.util.concurrent.Executors +import java.util.concurrent.TimeUnit +import javax.inject.Inject + +interface MayaApi { + val poolInfoList: MutableStateFlow> + val apiError: MutableStateFlow + var notificationIntent: Intent? + var showNotificationOnResult: Boolean + + suspend fun swap() + suspend fun reset() + //fun observePoolList(): Flow> + fun observePoolList(fiatExchangeRate: Fiat): Flow> +} + +class MayaApiAggregator @Inject constructor( + private val webApi: MayaWebApi, + private val blockchainApi: MayaBlockchainApi, + private val walletDataProvider: WalletDataProvider, + private val notificationService: NotificationService, + private val analyticsService: AnalyticsService, + private val config: MayaConfig, + private val globalConfig: Configuration, + private val securityFunctions: AuthenticationManager, + private val transactionMetadataProvider: TransactionMetadataProvider, + @ApplicationContext private val appContext: Context +): MayaApi { + companion object { + private val log = LoggerFactory.getLogger(MayaApiAggregator::class.java) + private val UPDATE_FREQ_MS = TimeUnit.SECONDS.toMillis(30) + private const val CONFIRMED_STATUS = "confirmed" + private const val VALID_STATUS = "valid" + private const val MESSAGE_RECEIVED_STATUS = "received" + private const val MESSAGE_FAILED_STATUS = "failed" + } + + private val params = walletDataProvider.networkParameters + private var tickerJob: Job? = null + private val configScope = CoroutineScope(Dispatchers.IO) + private val responseScope = CoroutineScope( + Executors.newSingleThreadExecutor().asCoroutineDispatcher() + ) + private val statusScope = CoroutineScope( + Executors.newSingleThreadExecutor().asCoroutineDispatcher() + ) + private var poolListLastUpdated: Long = 0 + override val poolInfoList = MutableStateFlow>(listOf()) + + override val apiError = MutableStateFlow(null) + override var notificationIntent: Intent? = null + override var showNotificationOnResult = false + + override suspend fun swap() { + TODO("Not yet implemented") + } + + init { + walletDataProvider.attachOnWalletWipedListener { + configScope.launch { reset() } + } + + config.observe(MayaConfig.BACKGROUND_ERROR) + .filterNot { it.isNullOrEmpty() } + .onEach { + if (apiError.value == null) { + apiError.value = MayaException(it ?: "") + config.set(MayaConfig.BACKGROUND_ERROR, "") + } + } + .launchIn(configScope) + } + + private suspend fun updatePoolList(fiatExchangeRate: Fiat) { + val resultWithUSDRates = webApi.getPoolInfo() + val resultWithFiatRates = resultWithUSDRates.map { pool -> + log.info("adjusting fiat value {}: {}", pool.asset, pool) + log.info(" {}", fiatExchangeRate.toFriendlyString()) + pool.setAssetPrice(fiatExchangeRate) + log.info(" {}", pool.assetPriceFiat.toFriendlyString()) + pool + } + log.info("USD: {}", resultWithUSDRates.map { it.assetPriceFiat }) + log.info("Fiat: {}", resultWithFiatRates.map { it.assetPriceFiat }) + poolInfoList.value = resultWithFiatRates + } + + override suspend fun reset() { + log.info("reset is triggered") + poolInfoList.value = listOf() + apiError.value = null + } + + private fun isError(): Boolean { + val savedError = runBlocking { config.get(MayaConfig.BACKGROUND_ERROR) ?: "" } + + if (savedError.isNotEmpty()) { + apiError.value = MayaException(savedError) + configScope.launch { config.set(MayaConfig.BACKGROUND_ERROR, "") } + log.info("found an error: $savedError") + return true + } + + return false + } + + private fun cancelTrackingJob() { + tickerJob?.cancel() + tickerJob = null + } + + private fun handleError(ex: Exception, error: String) { + apiError.value = ex + notifyIfNeeded(error, "maya_error") + log.error("$error: $ex") + analyticsService.logError(ex) + } + + private fun notifyIfNeeded(message: String, tag: String) { + if (showNotificationOnResult) { + notificationService.showNotification( + tag, + message, + intent = notificationIntent + ) + } + } + +// override fun observePoolList(): Flow> { +// if (shouldRefresh()) { +// refreshRates(Fiat.valueOf(MayaConstants.DEFAULT_EXCHANGE_CURRENCY, 100000000)) +// } +// return poolInfoList +// } + + override fun observePoolList(fiatExchangeRate: Fiat): Flow> { + log.info("observePoolList(${fiatExchangeRate.toFriendlyString()})") + if (shouldRefresh()) { + refreshRates(fiatExchangeRate) + } + return poolInfoList + } + + private fun refreshRates(fiatExchangeRate: Fiat) { + log.info("refreshRates(${fiatExchangeRate.toFriendlyString()})") + if (!shouldRefresh()) { + return + } + + responseScope.launch { + updatePoolList(fiatExchangeRate) + poolListLastUpdated = System.currentTimeMillis() + } + } + + private fun shouldRefresh(): Boolean { + val now = System.currentTimeMillis() + return poolListLastUpdated == 0L || now - poolListLastUpdated > UPDATE_FREQ_MS + } +} diff --git a/integrations/maya/src/main/java/org/dash/wallet/integrations/maya/api/MayaBlockchainApi.kt b/integrations/maya/src/main/java/org/dash/wallet/integrations/maya/api/MayaBlockchainApi.kt new file mode 100644 index 0000000000..dc77daec90 --- /dev/null +++ b/integrations/maya/src/main/java/org/dash/wallet/integrations/maya/api/MayaBlockchainApi.kt @@ -0,0 +1,8 @@ +package org.dash.wallet.integrations.maya.api + +import javax.inject.Inject + +class MayaBlockchainApi @Inject constructor() { + suspend fun swap() { + } +} diff --git a/integrations/maya/src/main/java/org/dash/wallet/integrations/maya/api/MayaWebApi.kt b/integrations/maya/src/main/java/org/dash/wallet/integrations/maya/api/MayaWebApi.kt new file mode 100644 index 0000000000..7b0bc30590 --- /dev/null +++ b/integrations/maya/src/main/java/org/dash/wallet/integrations/maya/api/MayaWebApi.kt @@ -0,0 +1,45 @@ +package org.dash.wallet.integrations.maya + +import org.dash.wallet.common.services.analytics.AnalyticsService +import org.dash.wallet.integrations.maya.model.PoolInfo +import org.slf4j.LoggerFactory +import retrofit2.Response +import retrofit2.http.GET +import java.io.IOException +import javax.inject.Inject + +interface MayaEndpoint { + @GET("pools") + suspend fun getPoolInfo(): Response> +} + +open class MayaWebApi @Inject constructor( + private val endpoint: MayaEndpoint, + private val analyticsService: AnalyticsService +) { + companion object { + private val log = LoggerFactory.getLogger(MayaWebApi::class.java) + } + + suspend fun getPoolInfo(): List { + return try { + val response = endpoint.getPoolInfo() + log.info("maya: response: {}", response) + + return if (response.isSuccessful && response.body()?.isNotEmpty() == true) { + response.body()!!.toList() + } else { + log.error("getWithdrawalLimits not successful; ${response.code()} : ${response.message()}") + listOf() + } + } catch (ex: Exception) { + log.error("Error in getPoolInfo: $ex") + + if (ex !is IOException) { + analyticsService.logError(ex) + } + + listOf() + } + } +} diff --git a/integrations/maya/src/main/java/org/dash/wallet/integrations/maya/api/RemoteDataSource.kt b/integrations/maya/src/main/java/org/dash/wallet/integrations/maya/api/RemoteDataSource.kt new file mode 100644 index 0000000000..eb8501b851 --- /dev/null +++ b/integrations/maya/src/main/java/org/dash/wallet/integrations/maya/api/RemoteDataSource.kt @@ -0,0 +1,33 @@ +package org.dash.wallet.integrations.maya.api + +import okhttp3.OkHttpClient +import okhttp3.logging.HttpLoggingInterceptor +import org.dash.wallet.common.BuildConfig +import retrofit2.Retrofit +import retrofit2.converter.gson.GsonConverterFactory +import javax.inject.Inject + +class RemoteDataSource @Inject constructor() { + fun buildApi( + api: Class, + baseUrl: String + ): Api { + return Retrofit.Builder() + .baseUrl(baseUrl) + .client(getRetrofitClient()) + .addConverterFactory(GsonConverterFactory.create()) + .build() + .create(api) + } + + private fun getRetrofitClient(): OkHttpClient { + return OkHttpClient.Builder() + .also { client -> + if (BuildConfig.DEBUG) { + val logging = HttpLoggingInterceptor() + logging.level = HttpLoggingInterceptor.Level.BODY + client.addInterceptor(logging) + } + }.build() + } +} \ No newline at end of file diff --git a/integrations/maya/src/main/java/org/dash/wallet/integrations/maya/data/CryptoCurrencyItem.kt b/integrations/maya/src/main/java/org/dash/wallet/integrations/maya/data/CryptoCurrencyItem.kt new file mode 100644 index 0000000000..cb346bd4b1 --- /dev/null +++ b/integrations/maya/src/main/java/org/dash/wallet/integrations/maya/data/CryptoCurrencyItem.kt @@ -0,0 +1,3 @@ +package org.dash.wallet.integrations.maya.data + +data class CryptoCurrencyItem(val currency: String) diff --git a/integrations/maya/src/main/java/org/dash/wallet/integrations/maya/di/MayaModule.kt b/integrations/maya/src/main/java/org/dash/wallet/integrations/maya/di/MayaModule.kt new file mode 100644 index 0000000000..ab50c316dd --- /dev/null +++ b/integrations/maya/src/main/java/org/dash/wallet/integrations/maya/di/MayaModule.kt @@ -0,0 +1,54 @@ +package org.dash.wallet.integrations.maya.di + +import dagger.Binds +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.FlowPreview +import org.dash.wallet.common.WalletDataProvider +import org.dash.wallet.integrations.maya.MayaEndpoint +import org.dash.wallet.integrations.maya.api.ExchangeRateApi +import org.dash.wallet.integrations.maya.api.FiatExchangeRateAggregatedProvider +import org.dash.wallet.integrations.maya.api.FiatExchangeRateProvider +import org.dash.wallet.integrations.maya.api.MayaApi +import org.dash.wallet.integrations.maya.api.MayaApiAggregator +import org.dash.wallet.integrations.maya.api.RemoteDataSource +import org.dash.wallet.integrations.maya.utils.MayaConstants +import javax.inject.Singleton +import kotlin.time.ExperimentalTime + +@Module +@InstallIn(SingletonComponent::class) +@ExperimentalCoroutinesApi +@ExperimentalTime +@FlowPreview +abstract class MayaModule { + companion object { + @Provides + fun provideMayaEndpoint( + remoteDataSource: RemoteDataSource, + walletDataProvider: WalletDataProvider + ): MayaEndpoint { + val baseUrl = MayaConstants.getBaseUrl(walletDataProvider.networkParameters) + return remoteDataSource.buildApi(MayaEndpoint::class.java, baseUrl) + } + @Provides + fun provideExchangeRateEndpoint( + remoteDataSource: RemoteDataSource, + walletDataProvider: WalletDataProvider + ): ExchangeRateApi { + val baseUrl = MayaConstants.EXCHANGERATE_BASE_URL + return remoteDataSource.buildApi(ExchangeRateApi::class.java, baseUrl) + } + } + + @Binds + @Singleton + abstract fun bindMayaApi(mayaApi: MayaApiAggregator): MayaApi + + @Binds + @Singleton + abstract fun bindFiatExchangeRateApi(fiatApi: FiatExchangeRateAggregatedProvider): FiatExchangeRateProvider +} \ No newline at end of file diff --git a/integrations/maya/src/main/java/org/dash/wallet/integrations/maya/model/ExchangeRateResponse.kt b/integrations/maya/src/main/java/org/dash/wallet/integrations/maya/model/ExchangeRateResponse.kt new file mode 100644 index 0000000000..494a474fb7 --- /dev/null +++ b/integrations/maya/src/main/java/org/dash/wallet/integrations/maya/model/ExchangeRateResponse.kt @@ -0,0 +1,14 @@ +package org.dash.wallet.integrations.maya.model + +data class ExchangeRateResponse( + val motd: Motd, + val success: Boolean, + val base: String, + val date: String, + val rates: Map +) + +data class Motd( + val msg: String, + val url: String +) diff --git a/integrations/maya/src/main/java/org/dash/wallet/integrations/maya/model/PoolInfo.kt b/integrations/maya/src/main/java/org/dash/wallet/integrations/maya/model/PoolInfo.kt new file mode 100644 index 0000000000..0ab820f966 --- /dev/null +++ b/integrations/maya/src/main/java/org/dash/wallet/integrations/maya/model/PoolInfo.kt @@ -0,0 +1,42 @@ +package org.dash.wallet.integrations.maya.model + +import org.bitcoinj.utils.Fiat +import org.dash.wallet.common.util.toBigDecimal +import org.dash.wallet.common.util.toFiat +import org.dash.wallet.integrations.maya.utils.MayaConstants +import java.math.BigDecimal + +data class PoolInfo( + val annualPercentageRate: String, + val asset: String, + val assetDepth: String, + val assetPrice: String, + val assetPriceUSD: String, + val liquidityUnits: String, + val poolAPY: String, + val runeDepth: String, + val status: String, + val synthSupply: String, + val synthUnits: String, + val units: String, + val volume24h: String +) { + var assetPriceFiat: Fiat = Fiat.valueOf(MayaConstants.DEFAULT_EXCHANGE_CURRENCY, 0) + + fun getAssetPriceUSD() : Fiat { + return Fiat.parseFiatInexact("USD", assetPriceUSD) + } + + fun setAssetPrice(fiatExchangeRate: Fiat) { + assetPriceFiat = BigDecimal(assetPriceUSD) + .multiply(fiatExchangeRate.toBigDecimal()) + .toFiat(fiatExchangeRate.currencyCode) + } + + val currencyCode: String + get() { + val codeIndex = asset.indexOf('.') + val smartContract = asset.indexOf('-') + return asset.substring(codeIndex + 1, if (smartContract != -1) smartContract else asset.length) + } +} diff --git a/integrations/maya/src/main/java/org/dash/wallet/integrations/maya/ui/MayaCryptoCurrencyPickerFragment.kt b/integrations/maya/src/main/java/org/dash/wallet/integrations/maya/ui/MayaCryptoCurrencyPickerFragment.kt new file mode 100644 index 0000000000..1138fe40de --- /dev/null +++ b/integrations/maya/src/main/java/org/dash/wallet/integrations/maya/ui/MayaCryptoCurrencyPickerFragment.kt @@ -0,0 +1,119 @@ +package org.dash.wallet.integrations.maya.ui + +import android.os.Bundle +import android.view.View +import androidx.core.content.ContextCompat +import androidx.fragment.app.Fragment +import androidx.fragment.app.viewModels +import androidx.lifecycle.lifecycleScope +import androidx.navigation.fragment.findNavController +import dagger.hilt.android.AndroidEntryPoint +import kotlinx.coroutines.launch +import org.dash.wallet.common.ui.decorators.ListDividerDecorator +import org.dash.wallet.common.ui.dialogs.AdaptiveDialog +import org.dash.wallet.common.ui.radio_group.IconSelectMode +import org.dash.wallet.common.ui.radio_group.IconifiedViewItem +import org.dash.wallet.common.ui.recyclerview.IconifiedListAdapter +import org.dash.wallet.common.ui.viewBinding +import org.dash.wallet.common.util.GenericUtils +import org.dash.wallet.common.util.observe +import org.dash.wallet.integrations.maya.R +import org.dash.wallet.integrations.maya.databinding.FragmentCurrencyPickerBinding +import org.dash.wallet.integrations.maya.model.PoolInfo + +@AndroidEntryPoint +class MayaCryptoCurrencyPickerFragment : Fragment(R.layout.fragment_currency_picker) { + private val binding by viewBinding(FragmentCurrencyPickerBinding::bind) + private val viewModel by viewModels() + private var itemList = listOf() + private lateinit var defaultItemMap: Map + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + binding.toolbar.setNavigationOnClickListener { + findNavController().popBackStack() + } + + val adapter = IconifiedListAdapter() { item, index -> + viewModel.poolList.value.firstOrNull { + it.currencyCode == item.title + }?.let { + clickListener(it) + } + } + + // val adapter1 = ListAdapter(RadioGroupAdapter.DiffCallback()) + + val divider = ContextCompat.getDrawable(requireContext(), org.dash.wallet.common.R.drawable.list_divider)!! + val decorator = ListDividerDecorator( + divider, + showAfterLast = false, + marginStart = resources.getDimensionPixelOffset(org.dash.wallet.common.R.dimen.divider_margin_horizontal), + marginEnd = resources.getDimensionPixelOffset(org.dash.wallet.common.R.dimen.divider_margin_horizontal) + ) + binding.contentList.addItemDecoration(decorator) + binding.contentList.adapter = adapter + + defaultItemMap = mapOf( + "BTC.BTC" to IconifiedViewItem( + requireContext().getString(R.string.cryptocurrency_bitcoin_code), + requireContext().getString(R.string.cryptocurrency_bitcoin_network) + // R.drawable.ic_btc_logo + ), + "ETH.ETH" to IconifiedViewItem( + requireContext().getString(R.string.cryptocurrency_ethereum_code), + requireContext().getString(R.string.cryptocurrency_ethereum_network) + // R.drawable.ic_eth_logo + ), + "DASH.DASH" to IconifiedViewItem( + requireContext().getString(R.string.cryptocurrency_dash_code), + requireContext().getString(R.string.cryptocurrency_dash_network) + // R.drawable.ic_dash_d_circle + ), + "ETH.USDC-0XA0B86991C6218B36C1D19D4A2E9EB0CE3606EB48" to IconifiedViewItem( + requireContext().getString(R.string.cryptocurrency_usdcoin_code), + requireContext().getString(R.string.cryptocurrency_usdcoin_network) + // R.drawable.blue_circle + ), + "ETH.USDT-0XDAC17F958D2EE523A2206206994597C13D831EC7" to IconifiedViewItem( + requireContext().getString(R.string.cryptocurrency_tether_code), + requireContext().getString(R.string.cryptocurrency_tether_network) + // R.drawable.blue_circle + ), + "THOR.RUNE" to IconifiedViewItem( + requireContext().getString(R.string.cryptocurrency_rune_code), + requireContext().getString(R.string.cryptocurrency_rune_network) + // R.drawable.ic_dash_d_circle + ) + ) + + viewModel.poolList.observe(viewLifecycleOwner) { + lifecycleScope.launch { + itemList = it.filter { pool -> pool.asset != "DASH.DASH" } + .map { pool -> + if (defaultItemMap.containsKey(pool.asset)) { + val item = defaultItemMap[pool.asset]!!.copy( + iconUrl = "https://raw.githubusercontent.com/jsupa/crypto-icons/main/icons/" + + "${pool.currencyCode.lowercase()}.png", + iconSelectMode = IconSelectMode.None, + additionalInfo = GenericUtils.formatFiatWithoutComma( + viewModel.formatFiat(pool.assetPriceFiat) + ) + ) + println(item.iconUrl + " " + item.iconUrl) + item + } else { + IconifiedViewItem(pool.currencyCode, pool.asset) + } + }.sortedBy { it.title } + adapter.submitList(itemList) + } + } + } + + fun clickListener(pool: PoolInfo) { + AdaptiveDialog.simple("${pool.currencyCode} was chosen", "Close").show(requireActivity()) { + } + } +} diff --git a/integrations/maya/src/main/java/org/dash/wallet/integrations/maya/ui/MayaPortalFragment.kt b/integrations/maya/src/main/java/org/dash/wallet/integrations/maya/ui/MayaPortalFragment.kt index 30be0eaefa..e945f601c9 100644 --- a/integrations/maya/src/main/java/org/dash/wallet/integrations/maya/ui/MayaPortalFragment.kt +++ b/integrations/maya/src/main/java/org/dash/wallet/integrations/maya/ui/MayaPortalFragment.kt @@ -23,12 +23,11 @@ import android.view.View import androidx.core.view.isVisible import androidx.fragment.app.Fragment import androidx.fragment.app.viewModels -import androidx.lifecycle.lifecycleScope import androidx.navigation.fragment.findNavController import dagger.hilt.android.AndroidEntryPoint -import kotlinx.coroutines.launch import org.dash.wallet.common.databinding.FragmentIntegrationPortalBinding import org.dash.wallet.common.ui.viewBinding +import org.dash.wallet.common.util.safeNavigate import org.dash.wallet.integrations.maya.R @AndroidEntryPoint @@ -42,7 +41,7 @@ class MayaPortalFragment : Fragment(R.layout.fragment_integration_portal) { super.onCreate(savedInstanceState) binding.balanceDash.isVisible = false - binding.balanceDash.setFormat(viewModel.balanceFormat) + binding.balanceDash.setFormat(viewModel.fiatFormat) binding.balanceDash.setApplyMarkup(false) binding.toolbarTitle.text = getString(R.string.maya_service_name) @@ -72,6 +71,7 @@ class MayaPortalFragment : Fragment(R.layout.fragment_integration_portal) { binding.convertBtn.setOnClickListener { // TODO: add handler code here + safeNavigate(MayaPortalFragmentDirections.mayaPortalToCurrencyPicker()) } } } diff --git a/integrations/maya/src/main/java/org/dash/wallet/integrations/maya/ui/MayaViewModel.kt b/integrations/maya/src/main/java/org/dash/wallet/integrations/maya/ui/MayaViewModel.kt index a121fad76d..99caf8fbc1 100644 --- a/integrations/maya/src/main/java/org/dash/wallet/integrations/maya/ui/MayaViewModel.kt +++ b/integrations/maya/src/main/java/org/dash/wallet/integrations/maya/ui/MayaViewModel.kt @@ -21,44 +21,112 @@ import androidx.lifecycle.* import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.* +import org.bitcoinj.core.Coin +import org.bitcoinj.utils.Fiat import org.bitcoinj.utils.MonetaryFormat import org.dash.wallet.common.Configuration -import org.dash.wallet.common.WalletDataProvider import org.dash.wallet.common.data.SingleLiveEvent import org.dash.wallet.common.data.WalletUIConfig import org.dash.wallet.common.data.entity.ExchangeRate import org.dash.wallet.common.services.ExchangeRatesProvider import org.dash.wallet.common.services.analytics.AnalyticsService +import org.dash.wallet.common.util.GenericUtils +import org.dash.wallet.common.util.isCurrencyFirst +import org.dash.wallet.integrations.maya.api.FiatExchangeRateProvider +import org.dash.wallet.integrations.maya.api.MayaApi +import org.dash.wallet.integrations.maya.data.CryptoCurrencyItem +import org.dash.wallet.integrations.maya.model.PoolInfo import org.dash.wallet.integrations.maya.utils.MayaConfig +import org.slf4j.LoggerFactory +import java.util.Locale import javax.inject.Inject +data class MayaPortalUIState( + val errorCode: Int? = null +) + @OptIn(ExperimentalCoroutinesApi::class) @HiltViewModel class MayaViewModel @Inject constructor( private val globalConfig: Configuration, private val config: MayaConfig, + private val mayaApi: MayaApi, + private val fiatExchangeRateProvider: FiatExchangeRateProvider, exchangeRatesProvider: ExchangeRatesProvider, val analytics: AnalyticsService, walletUIConfig: WalletUIConfig ) : ViewModel() { + companion object { + val log = LoggerFactory.getLogger(MayaViewModel::class.java) + } - val balanceFormat: MonetaryFormat - get() = globalConfig.format.noCode() + val fiatFormat = MonetaryFormat().minDecimals(2).withLocale(Locale.getDefault()).noCode() val networkError = SingleLiveEvent() private val _exchangeRate: MutableLiveData = MutableLiveData() - val exchangeRate: LiveData - get() = _exchangeRate + + private var dashExchangeRate: org.bitcoinj.utils.ExchangeRate? = null + private var fiatExchangeRate: Fiat? = null + + // val currencyList = MutableStateFlow>(listOf()) + + private val _uiState = MutableStateFlow(MayaPortalUIState()) + val uiState: StateFlow = _uiState.asStateFlow() val dashFormat: MonetaryFormat get() = globalConfig.format.noCode() + val poolList = MutableStateFlow>(listOf()) + init { walletUIConfig.observe(WalletUIConfig.SELECTED_CURRENCY) .filterNotNull() .flatMapLatest(exchangeRatesProvider::observeExchangeRate) - .onEach(_exchangeRate::postValue) + .onEach { rate -> + dashExchangeRate = rate?.let { org.bitcoinj.utils.ExchangeRate(Coin.COIN, rate.fiat) } + // val fiatBalance = exchangeRate?.coinToFiat(_uiState.value.balance) + _uiState.update { it.copy() } + } + .launchIn(viewModelScope) + +// mayaApi.observePoolList() +// .filterNotNull() +// .onEach { +// log.info("Pool List: {}", it) +// poolList.value = it +// } +// .launchIn(viewModelScope) + + walletUIConfig.observe(WalletUIConfig.SELECTED_CURRENCY) + .filterNotNull() + .flatMapLatest(fiatExchangeRateProvider::observeFiatRate) + .onEach { + it?.let { fiatRate -> fiatExchangeRate = fiatRate.fiat } + log.info("exchange rate: $it") + } + .flatMapLatest { mayaApi.observePoolList(it!!.fiat) } + .onEach { + log.info("exchange rate in view model: {}", fiatExchangeRate?.toFriendlyString()) + log.info("Pool List: {}", it) + log.info("Pool List: {}", it.map { pool -> pool.assetPriceFiat }) + it.forEach { pool -> + pool.setAssetPrice(fiatExchangeRate!!) + } + poolList.value = it + } .launchIn(viewModelScope) } + + fun formatFiat(fiatAmount: Fiat): String { + val localCurrencySymbol = GenericUtils.getLocalCurrencySymbol(fiatAmount.currencyCode) + + val fiatBalance = fiatFormat.format(fiatAmount).toString() + + return if (fiatAmount!!.isCurrencyFirst()) { + "$localCurrencySymbol $fiatBalance" + } else { + "$fiatBalance $localCurrencySymbol" + } + } } diff --git a/integrations/maya/src/main/java/org/dash/wallet/integrations/maya/utils/MayaConfig.kt b/integrations/maya/src/main/java/org/dash/wallet/integrations/maya/utils/MayaConfig.kt index 6e2d0adf9b..560c545812 100644 --- a/integrations/maya/src/main/java/org/dash/wallet/integrations/maya/utils/MayaConfig.kt +++ b/integrations/maya/src/main/java/org/dash/wallet/integrations/maya/utils/MayaConfig.kt @@ -18,6 +18,7 @@ package org.dash.wallet.integrations.maya.utils import android.content.Context +import androidx.datastore.preferences.core.stringPreferencesKey import org.dash.wallet.common.WalletDataProvider import org.dash.wallet.common.data.BaseConfig import javax.inject.Inject @@ -30,5 +31,7 @@ open class MayaConfig @Inject constructor( ) : BaseConfig(context, PREFERENCES_NAME, walletDataProvider) { companion object { const val PREFERENCES_NAME = "maya" + + val BACKGROUND_ERROR = stringPreferencesKey("error") } } diff --git a/integrations/maya/src/main/java/org/dash/wallet/integrations/maya/utils/MayaConstants.kt b/integrations/maya/src/main/java/org/dash/wallet/integrations/maya/utils/MayaConstants.kt index 3fe50d1fba..4dabf589b6 100644 --- a/integrations/maya/src/main/java/org/dash/wallet/integrations/maya/utils/MayaConstants.kt +++ b/integrations/maya/src/main/java/org/dash/wallet/integrations/maya/utils/MayaConstants.kt @@ -17,7 +17,19 @@ package org.dash.wallet.integrations.maya.utils +import org.bitcoinj.core.NetworkParameters + object MayaConstants { + const val DEFAULT_EXCHANGE_CURRENCY = "USD" private const val MAINNET_BASE_URL = "https://midgard.mayachain.info/v2/" + + /** + * https://exchangerate.host/#/docs + */ + const val EXCHANGERATE_BASE_URL = "https://api.exchangerate.host/" + + fun getBaseUrl(params: NetworkParameters): String { + return MAINNET_BASE_URL + } } diff --git a/integrations/maya/src/main/res/layout/fragment_currency_picker.xml b/integrations/maya/src/main/res/layout/fragment_currency_picker.xml new file mode 100644 index 0000000000..a13cfb07cc --- /dev/null +++ b/integrations/maya/src/main/res/layout/fragment_currency_picker.xml @@ -0,0 +1,122 @@ + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/integrations/maya/src/main/res/navigation/nav_maya.xml b/integrations/maya/src/main/res/navigation/nav_maya.xml new file mode 100644 index 0000000000..37fc60a06d --- /dev/null +++ b/integrations/maya/src/main/res/navigation/nav_maya.xml @@ -0,0 +1,26 @@ + + + + + + + + + \ No newline at end of file diff --git a/integrations/maya/src/main/res/values/strings-maya.xml b/integrations/maya/src/main/res/values/strings-maya.xml index 57e2f16539..e6eed6fb9b 100644 --- a/integrations/maya/src/main/res/values/strings-maya.xml +++ b/integrations/maya/src/main/res/values/strings-maya.xml @@ -17,6 +17,19 @@ Maya Convert Dash + Convert Dash to From Dash Wallet to any crypto + BTC + Bitcoin + DASH + Dash + RUNE + Rune + ETH + USDC + USDT + Ethereum + USD Coin + USDT \ No newline at end of file diff --git a/wallet/res/navigation/nav_home.xml b/wallet/res/navigation/nav_home.xml index 1334f27c98..6c82a6085b 100644 --- a/wallet/res/navigation/nav_home.xml +++ b/wallet/res/navigation/nav_home.xml @@ -215,7 +215,7 @@ - - + \ No newline at end of file