Skip to content
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

feat: support other fiat currencies in maya integration #1289

Merged
merged 8 commits into from
Jun 21, 2024
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ package org.dash.wallet.common.ui.recyclerview
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.core.view.isVisible
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ListAdapter
import org.dash.wallet.common.R
import org.dash.wallet.common.databinding.RadiobuttonRowBinding
Expand All @@ -29,8 +30,9 @@ import org.dash.wallet.common.ui.radio_group.RadioGroupAdapter
import org.dash.wallet.common.ui.setRoundedRippleBackground

class IconifiedListAdapter(
diffCallback: DiffUtil.ItemCallback<IconifiedViewItem> = RadioGroupAdapter.DiffCallback(),
private val clickListener: (IconifiedViewItem, Int) -> Unit
): ListAdapter<IconifiedViewItem, IconifiedViewHolder>(RadioGroupAdapter.DiffCallback()) {
): ListAdapter<IconifiedViewItem, IconifiedViewHolder>(diffCallback) {

override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): IconifiedViewHolder {
val inflater = LayoutInflater.from(parent.context)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,11 @@ object GenericUtils {
return currency?.defaultFractionDigits ?: 0
}

fun getCurrencyDigits(code: String): Int {
val currency: Currency? = Currency.getInstance(code)
return currency?.defaultFractionDigits ?: 2
}

private fun stringToBigDecimal(value: String): BigDecimal {
return try {
val format = NumberFormat.getNumberInstance(getDeviceLocale())
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,21 +17,36 @@

package org.dash.wallet.integrations.maya.api

import org.dash.wallet.integrations.maya.model.CurrencyBeaconResponse
import org.dash.wallet.integrations.maya.model.ExchangeRateResponse
import org.dash.wallet.integrations.maya.model.FreeCurrencyResponse
import retrofit2.Response
import retrofit2.http.GET
import retrofit2.http.Header
import retrofit2.http.Query

/**
* https://exchangerate.host/#/docs
* https://v6.exchangerate-api.com/v6/{api-key}}/latest/USD
*/
interface ExchangeRateApi {
@GET("latest")
suspend fun getRates(@Query("base") baseCurrencyCode: String): Response<ExchangeRateResponse>
@GET("latest/USD")
suspend fun getRates(): Response<ExchangeRateResponse>
}

interface CurrencyBeaconApi {
@GET("latest")
suspend fun getRate(
suspend fun getRates(
@Query("base") baseCurrencyCode: String,
@Query("symbols") resultCurrencyCode: String
): Response<ExchangeRateResponse>
@Query("symbols") resultCurrencyCode: String,
@Query("api_key") apiKey: String = "1xsN7q7S2Lo3gzlmtrpdmLufO96OBlRK"
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This key will be temporary for testing. We will need a permanent secret key for production.

): Response<CurrencyBeaconResponse>
}

interface FreeCurrencyApi {
@GET("latest")
suspend fun getRates(
@Query("base_currency") baseCurrencyCode: String = "USD",
@Query("currencies") resultCurrencyCode: String,
@Query("apikey") apiKey: String = "fca_live_SysbOIn5mJg21vRzfUVRYPA6hIxku1umZUaUkNty"
): Response<FreeCurrencyResponse>
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ 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.MayaConfig
import org.dash.wallet.integrations.maya.utils.MayaConstants
import org.slf4j.Logger
import org.slf4j.LoggerFactory
Expand All @@ -31,21 +32,67 @@ import java.util.concurrent.TimeUnit
import javax.inject.Inject

class FiatExchangeRateApiAggregator @Inject constructor(
private val exchangeRateApi: ExchangeRateApi
private val exchangeRateApi: ExchangeRateApi,
private val currencyBeaconApi: CurrencyBeaconApi,
private val freeCurrencyApi: FreeCurrencyApi,
private val mayaConfig: MayaConfig
) {
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())
val lastUpdate = mayaConfig.get(MayaConfig.EXCHANGE_RATE_LAST_UPDATE)
val lastCurrencyCode = mayaConfig.get(MayaConfig.EXCHANGE_RATE_CURRENCY_CODE)
if (lastCurrencyCode != currencyCode || lastUpdate == null || lastUpdate == 0L ||
(System.currentTimeMillis() - lastUpdate) > MayaConfig.expirationDuration) {

if (currencyCode == MayaConstants.DEFAULT_EXCHANGE_CURRENCY) {
return ExchangeRate(MayaConstants.DEFAULT_EXCHANGE_CURRENCY, "1.0")
}
val currencyBeaconResponse =
currencyBeaconApi.getRates(MayaConstants.DEFAULT_EXCHANGE_CURRENCY, currencyCode)
if (currencyBeaconResponse.isSuccessful) {
val response = currencyBeaconResponse.body()
val exchangeRate = response?.rates?.get(currencyCode) ?: 0.0
log.info("exchange rate: {} {}", exchangeRate, currencyCode)
if (exchangeRate != 0.0) {
return saveNewExchangeRate(exchangeRate, currencyCode)
}
}

val freeCurrencyResponse = freeCurrencyApi.getRates(resultCurrencyCode = currencyCode)
if (freeCurrencyResponse.isSuccessful) {
val response = freeCurrencyResponse.body()
val exchangeRate = response?.data?.get(currencyCode) ?: 0.0
log.info("exchange rate: {} {}", exchangeRate, currencyCode)
if (exchangeRate != 0.0) {
return saveNewExchangeRate(exchangeRate, currencyCode)
}
}

val response = exchangeRateApi.getRates().body()
val exchangeRate = response?.rates?.get(currencyCode) ?: 0.0
log.info("exchange rate: {} {}", exchangeRate, currencyCode)
return if (exchangeRate != 0.0) {
saveNewExchangeRate(exchangeRate, currencyCode)
} else {
ExchangeRate(MayaConstants.DEFAULT_EXCHANGE_CURRENCY, "1.0")
}
} else {
null
val lastValue = mayaConfig.get(MayaConfig.EXCHANGE_RATE_VALUE) ?: 0
return ExchangeRate(lastCurrencyCode, lastValue.toString())
}
}

private suspend fun saveNewExchangeRate(
exchangeRate: Double,
currencyCode: String
): ExchangeRate {
mayaConfig.set(MayaConfig.EXCHANGE_RATE_VALUE, exchangeRate)
mayaConfig.set(MayaConfig.EXCHANGE_RATE_CURRENCY_CODE, currencyCode)
mayaConfig.set(MayaConfig.EXCHANGE_RATE_LAST_UPDATE, System.currentTimeMillis())
return ExchangeRate(currencyCode, exchangeRate.toString())
}
}

interface FiatExchangeRateProvider {
Expand All @@ -58,7 +105,6 @@ class FiatExchangeRateAggregatedProvider @Inject constructor(
) : FiatExchangeRateProvider {
companion object {
private val log = LoggerFactory.getLogger(FiatExchangeRateApiAggregator::class.java)
private val UPDATE_FREQ_MS = TimeUnit.SECONDS.toMillis(30)
}

private val responseScope = CoroutineScope(
Expand All @@ -68,31 +114,23 @@ class FiatExchangeRateAggregatedProvider @Inject constructor(
override val fiatExchangeRate = MutableStateFlow(ExchangeRate(MayaConstants.DEFAULT_EXCHANGE_CURRENCY, "1.0"))

override fun observeFiatRate(currencyCode: String): Flow<ExchangeRate?> {
if (shouldRefresh()) {
refreshRates(currencyCode)
}
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
}

private suspend fun updateExchangeRates(currencyCode: String) {
fiatExchangeRateApi.getRate(currencyCode)?.let { rate ->
fiatExchangeRate.value = rate
val newRate = fiatExchangeRateApi.getRate(currencyCode)
if (newRate != null) {
fiatExchangeRate.value = newRate
} else {
fiatExchangeRate.value = ExchangeRate(MayaConstants.DEFAULT_EXCHANGE_CURRENCY, "1.0")
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,11 @@ import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import org.dash.wallet.common.WalletDataProvider
import org.dash.wallet.integrations.maya.api.CurrencyBeaconApi
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.FreeCurrencyApi
import org.dash.wallet.integrations.maya.api.MayaApi
import org.dash.wallet.integrations.maya.api.MayaApiAggregator
import org.dash.wallet.integrations.maya.api.MayaBlockchainApi
Expand Down Expand Up @@ -61,11 +63,26 @@ abstract class MayaModule {
@Provides
fun provideExchangeRateEndpoint(
remoteDataSource: RemoteDataSource,
walletDataProvider: WalletDataProvider
): ExchangeRateApi {
val baseUrl = MayaConstants.EXCHANGERATE_BASE_URL
return remoteDataSource.buildApi(ExchangeRateApi::class.java, baseUrl)
}

@Provides
fun provideCurrencyBeaconEndpoint(
remoteDataSource: RemoteDataSource
): CurrencyBeaconApi {
val baseUrl = MayaConstants.CURRENCYBEACON_BASE_URL
return remoteDataSource.buildApi(CurrencyBeaconApi::class.java, baseUrl)
}

@Provides
fun provideFreeCurrencyApiEndpoint(
remoteDataSource: RemoteDataSource,
): FreeCurrencyApi {
val baseUrl = MayaConstants.FREE_CURRENCY_API_BASE_URL
return remoteDataSource.buildApi(FreeCurrencyApi::class.java, baseUrl)
}
}

@Binds
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
/*
* Copyright 2024 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 <http://www.gnu.org/licenses/>.
*/

package org.dash.wallet.integrations.maya.model

/**
* Response from https://api.currencybeacon.com/v1/latest
*/
class CurrencyBeaconResponse(
val result: String,
val base: String,
val date: String,
val rates: Map<String, Double>
)
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,14 @@

package org.dash.wallet.integrations.maya.model

import com.google.gson.annotations.SerializedName

/**
* Response from api.exchangerates.host
* Response from https://v6.exchangerate-api.com/v6
*/
data class ExchangeRateResponse(
val success: Boolean,
val base: String,
val date: String,
val rates: Map<String, Double>
val result: String,
@SerializedName("base_code") val baseCode: String,
@SerializedName("time_last_update_unix") val lastUpdate: Long,
@SerializedName("conversion_rates") val rates: Map<String, Double>
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
/*
* Copyright 2024 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 <http://www.gnu.org/licenses/>.
*/

package org.dash.wallet.integrations.maya.model

/**
* Response from https://api.freecurrencyapi.com/v1/latest
*/
class FreeCurrencyResponse(
val data: Map<String, Double>
)
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,9 @@ open class MayaKujiraCryptoCurrency : MayaBitcoinCryptoCurrency() {
class MayaKujiraTokenCryptoCurrency(
override val code: String,
override val name: String,
override val asset: String
override val asset: String,
override val codeId: Int,
override val nameId: Int
) : MayaKujiraCryptoCurrency()

class MayaEthereumTokenCryptoCurrency(
Expand Down Expand Up @@ -267,7 +269,14 @@ object MayaCurrencyList {
),

MayaKujiraCryptoCurrency(),
MayaKujiraTokenCryptoCurrency("USK", "USK", "KUJI.USK"),
MayaKujiraTokenCryptoCurrency(
"USK",
"USK",
"KUJI.USK",
R.string.cryptocurrency_usk_code,
R.string.cryptocurrency_usk_network
),

MayaRuneCryptoCurrency()
)
currencyMap = currencyList.associateBy({ it.asset }, { it })
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ import org.dash.wallet.integrations.maya.model.getMayaErrorString
import org.dash.wallet.integrations.maya.model.getMayaErrorType

class MayaAddressInputFragment : AddressInputFragment() {
private val mayaViewModel by viewModels<MayaViewModel>()
private val mayaViewModel by mayaViewModels<MayaViewModel>()
private val mayaAddressInputViewModel by viewModels<MayaAddressInputViewModel>()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ class MayaConvertCryptoFragment : Fragment(R.layout.fragment_maya_convert_crypto
private val binding by viewBinding(FragmentMayaConvertCryptoBinding::bind)
private val viewModel by viewModels<MayaConvertCryptoViewModel>()
private val convertViewModel by mayaViewModels<ConvertViewViewModel>()
private val mayaViewModel by viewModels<MayaViewModel>()
private val mayaViewModel by mayaViewModels<MayaViewModel>()
private val args by navArgs<MayaConvertCryptoFragmentArgs>()

private var loadingDialog: AdaptiveDialog? = null
Expand Down
Loading
Loading