Updated on 2026-08-14

This commit is contained in:
Tangem 2020-09-02 07:57:50 +00:00
commit 9559ca0564
11 changed files with 270 additions and 43 deletions

View file

@ -3,9 +3,11 @@ package com.tangem.tap.common.redux.global
import com.tangem.blockchain.common.WalletManager
import com.tangem.commands.Card
import org.rekotlin.Action
import java.math.BigDecimal
sealed class GlobalAction : Action {
data class LoadCard(val card: Card) : GlobalAction()
data class LoadWalletManager(val walletManager: WalletManager) : GlobalAction()
data class SetFiatRate(val fiatRates: Pair<String, BigDecimal>) : GlobalAction()
}

View file

@ -11,6 +11,11 @@ fun globalReducer(action: Action, state: AppState): GlobalState {
when (action) {
is GlobalAction.LoadCard -> newState = newState.copy(card = action.card)
is GlobalAction.SetFiatRate -> {
val rates = newState.fiatRates.rates.toMutableMap()
rates[action.fiatRates.first] = action.fiatRates.second
newState = newState.copy(fiatRates = FiatRates(rates))
}
is GlobalAction.LoadWalletManager ->
newState = newState.copy(walletManager = action.walletManager)

View file

@ -2,12 +2,23 @@ package com.tangem.tap.common.redux.global
import com.tangem.blockchain.common.WalletManager
import com.tangem.commands.Card
import com.tangem.tap.domain.TapWalletManager
import org.rekotlin.StateType
import java.math.BigDecimal
data class GlobalState(
val card: Card? = null,
val walletManager: WalletManager? = null,
val tapWalletManager: TapWalletManager = TapWalletManager(),
val fiatRates: FiatRates = FiatRates(emptyMap()),
) : StateType
data class FiatRates(
val rates: Map<String, BigDecimal>
) {
fun getRateForCryptoCurrency(currency: String): BigDecimal? {
return rates[currency]
}
}

View file

@ -0,0 +1,117 @@
package com.tangem.tap.domain
import com.tangem.blockchain.common.Wallet
import com.tangem.blockchain.common.WalletManager
import com.tangem.commands.common.network.Result
import com.tangem.common.extensions.toHexString
import com.tangem.tap.TapConfig
import com.tangem.tap.common.redux.global.GlobalAction
import com.tangem.tap.features.wallet.redux.PayIdState
import com.tangem.tap.features.wallet.redux.WalletAction
import com.tangem.tap.network.coinmarketcap.CoinMarketCapService
import com.tangem.tap.store
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import java.math.BigDecimal
class TapWalletManager {
private val payIdManager = PayIdManager()
private val coinMarketCapService = CoinMarketCapService()
suspend fun loadWalletData() {
val walletManager = store.state.globalState.walletManager
if (walletManager == null) {
store.dispatch(WalletAction.LoadWallet.Failure)
return
}
updateWallet(walletManager)
}
suspend fun loadPayId() {
val result = loadPayIdIfNeeded()
result?.let { handlePayIdResult(it) }
}
suspend fun loadFiatRate() {
val blockchainCurrency = store.state.globalState.walletManager?.wallet?.blockchain?.currency
val tokenCurrency = store.state.globalState.walletManager?.wallet?.token?.symbol
val blockchainRate = blockchainCurrency?.let { coinMarketCapService.getRate(it) }
val tokenRate = tokenCurrency?.let { coinMarketCapService.getRate(it) }
val results = mutableListOf<Pair<String, Result<BigDecimal>?>>()
if (blockchainCurrency != null) results.add(blockchainCurrency to blockchainRate)
if (tokenCurrency != null) results.add(tokenCurrency to tokenRate)
handleFiatRatesResult(results)
}
private suspend fun updateWallet(walletManager: WalletManager) {
val result = try {
walletManager.update()
Result.Success(walletManager.wallet)
} catch (exeption: Exception) {
Result.Failure(exeption)
}
handleUpdateWalletResult(result)
}
private suspend fun handleUpdateWalletResult(result: Result<Wallet>) {
withContext(Dispatchers.Main) {
when (result) {
is Result.Success -> store.dispatch(WalletAction.LoadWallet.Success(result.data))
is Result.Failure -> store.dispatch(WalletAction.LoadWallet.Failure)
}
}
}
private suspend fun loadPayIdIfNeeded(): Result<String?>? {
if (!TapConfig.usePayId ||
store.state.walletState.payIdData.payIdState == PayIdState.Disabled ||
store.state.globalState.walletManager?.wallet?.blockchain?.isPayIdSupported() == false) {
return null
}
val cardId = store.state.globalState.card?.cardId
val publicKey = store.state.globalState.card?.cardPublicKey
if (cardId == null || publicKey == null) {
return null
}
return payIdManager.getPayId(cardId, publicKey.toHexString())
}
private suspend fun handlePayIdResult(result: Result<String?>) {
withContext(Dispatchers.Main) {
when (result) {
is Result.Success -> {
val payId = result.data
if (payId == null) {
store.dispatch(WalletAction.LoadPayId.NotCreated)
} else {
store.dispatch(WalletAction.LoadPayId.Success(payId))
}
}
is Result.Failure -> store.dispatch(WalletAction.LoadPayId.Failure)
}
}
}
private suspend fun handleFiatRatesResult(results: List<Pair<String, Result<BigDecimal>?>>) {
withContext(Dispatchers.Main) {
results.map {
when (it.second) {
is Result.Success -> {
val rate = it.first to (it.second as Result.Success<BigDecimal>).data
store.dispatch(GlobalAction.SetFiatRate(rate))
store.dispatch(WalletAction.LoadFiatRate.Success(rate))
}
is Result.Failure -> store.dispatch(WalletAction.LoadFiatRate.Failure)
null -> {
}
}
}
}
}
}

View file

@ -30,6 +30,7 @@ val homeMiddleware: Middleware<AppState> = { dispatch, state ->
store.dispatch(GlobalAction.LoadCard(result.data.card))
store.dispatch(GlobalAction.LoadWalletManager(result.data.walletManager))
store.dispatch(WalletAction.LoadWallet)
store.dispatch(WalletAction.LoadFiatRate)
store.dispatch(WalletAction.LoadPayId)
store.dispatch(NavigationAction.NavigateTo(AppScreen.Wallet))
}

View file

@ -7,12 +7,17 @@ import com.tangem.tap.common.redux.NotificationAction
import com.tangem.tap.domain.TapError
import com.tangem.wallet.R
import org.rekotlin.Action
import java.math.BigDecimal
sealed class WalletAction : Action {
object LoadWallet : WalletAction() {
data class Success(val wallet: Wallet): WalletAction()
object Failure: WalletAction()
}
object LoadFiatRate : WalletAction() {
data class Success(val fiatRates: Pair<String, BigDecimal>) : WalletAction()
object Failure: WalletAction()
}
object LoadPayId : WalletAction() {
data class Success(val payId: String): WalletAction()
object NotCreated: WalletAction()

View file

@ -6,13 +6,11 @@ import androidx.core.content.ContextCompat
import com.tangem.commands.common.network.Result
import com.tangem.common.CompletionResult
import com.tangem.common.extensions.toHexString
import com.tangem.tap.TapConfig
import com.tangem.tap.common.extensions.copyToClipboard
import com.tangem.tap.common.redux.AppState
import com.tangem.tap.common.redux.global.GlobalAction
import com.tangem.tap.domain.PayIdManager
import com.tangem.tap.domain.TapError
import com.tangem.tap.domain.isPayIdSupported
import com.tangem.tap.scope
import com.tangem.tap.store
import com.tangem.tap.tangemSdkManager
@ -28,50 +26,17 @@ val walletMiddleware: Middleware<AppState> = { dispatch, state ->
when (action) {
is WalletAction.LoadWallet -> {
scope.launch {
val walletManager = store.state.globalState.walletManager
if (walletManager == null) {
store.dispatch(WalletAction.LoadWallet.Failure)
return@launch
}
try {
walletManager.update()
} catch (ex: Exception) {
withContext(Dispatchers.Main) {
store.dispatch(WalletAction.LoadWallet.Failure)
// callback(CompletionResult.Failure(BlockchainInternalErrorConverter.convert(ex)))
next(action)
}
}
withContext(Dispatchers.Main) {
store.dispatch(WalletAction.LoadWallet.Success(walletManager.wallet))
}
store.state.globalState.tapWalletManager.loadWalletData()
}
}
is WalletAction.LoadPayId -> {
if (!TapConfig.usePayId ||
store.state.walletState.payIdData.payIdState == PayIdState.Disabled ||
store.state.globalState.walletManager?.wallet?.blockchain?.isPayIdSupported() == false) {
next(action)
}
scope.launch {
val cardId = store.state.globalState.card?.cardId
val publicKey = store.state.globalState.card?.cardPublicKey
if (cardId != null && publicKey != null) {
val result = PayIdManager().getPayId(cardId, publicKey.toHexString())
withContext(Dispatchers.Main) {
when (result) {
is Result.Success -> {
val payId = result.data
if (payId == null) {
store.dispatch(WalletAction.LoadPayId.NotCreated)
} else {
store.dispatch(WalletAction.LoadPayId.Success(payId))
}
}
is Result.Failure -> store.dispatch(WalletAction.LoadPayId.Failure)
}
}
}
store.state.globalState.tapWalletManager.loadPayId()
}
}
is WalletAction.LoadFiatRate -> {
scope.launch {
store.state.globalState.tapWalletManager.loadFiatRate()
}
}
is WalletAction.CreatePayId.CompleteCreatingPayId -> {
@ -107,6 +72,7 @@ val walletMiddleware: Middleware<AppState> = { dispatch, state ->
store.dispatch(GlobalAction.LoadCard(result.data.card))
store.dispatch(GlobalAction.LoadWalletManager(result.data.walletManager))
store.dispatch(WalletAction.LoadWallet)
store.dispatch(WalletAction.LoadFiatRate)
store.dispatch(WalletAction.LoadPayId)
}
}

View file

@ -1,6 +1,7 @@
package com.tangem.tap.features.wallet.redux
import com.tangem.blockchain.common.AmountType
import com.tangem.tap.common.extensions.toFiatString
import com.tangem.tap.common.extensions.toFormattedString
import com.tangem.tap.common.extensions.toQrCode
import com.tangem.tap.common.redux.AppState
@ -26,22 +27,45 @@ fun walletReducer(action: Action, state: AppState): WalletState {
is WalletAction.LoadWallet.Success -> {
val token = action.wallet.amounts[AmountType.Token]
val tokenData = if (token != null) {
val tokenFiatRate = state.globalState.fiatRates.getRateForCryptoCurrency(token.currencySymbol)
val tokenFiatAmount = tokenFiatRate?.let { token.value?.toFiatString(it) }
TokenData(
token.value?.toFormattedString(action.wallet.blockchain) ?: "",
token.currencySymbol)
token.currencySymbol, tokenFiatAmount)
} else {
null
}
val amount = action.wallet.amounts[AmountType.Coin]?.value
val fiatRate = state.globalState.fiatRates.getRateForCryptoCurrency(action.wallet.blockchain.currency)
val fiatAmount = fiatRate?.let { amount?.toFiatString(it) }
newState = newState.copy(
state = ProgressState.Done, wallet = action.wallet,
currencyData = BalanceWidgetData(
BalanceStatus.VerifiedOnline, action.wallet.blockchain.fullName,
amount?.toFormattedString(action.wallet.blockchain),
token = tokenData,
fiatAmount = fiatAmount
)
)
}
is WalletAction.LoadFiatRate.Success -> {
val rate = action.fiatRates.second
val currency = action.fiatRates.first
val fiatAmount = if (currency == newState.wallet?.blockchain?.currency) {
newState.wallet?.amounts?.get(AmountType.Coin)?.value?.toFiatString(rate)
} else {
null
}
val tokenFiatAmount = if (currency == newState.wallet?.token?.symbol) {
newState.wallet?.amounts?.get(AmountType.Token)?.value?.toFiatString(rate)
} else {
null
}
newState = newState.copy(currencyData = newState.currencyData.copy(
fiatAmount = fiatAmount,
token = newState.currencyData.token?.copy(fiatAmount = tokenFiatAmount)
))
}
is WalletAction.LoadWallet.Failure -> newState = newState.copy(
state = ProgressState.Done,
currencyData = newState.currencyData.copy(status = BalanceStatus.Unreachable)

View file

@ -0,0 +1,38 @@
package com.tangem.tap.network.coinmarketcap
import com.tangem.tap.TapConfig
import com.tangem.tap.network.createRetrofitInstance
import okhttp3.Interceptor
import okhttp3.Response
import retrofit2.http.GET
import retrofit2.http.Query
interface CoinMarketCapApi {
@GET("v1/tools/price-conversion")
suspend fun getRateInfo(
@Query("amount") amount: Int,
@Query("symbol") cryptoId: String
): RateInfoResponse
companion object {
private const val baseUrl = "https://pro-api.coinmarketcap.com/"
fun create(): CoinMarketCapApi {
return createRetrofitInstance(
baseUrl,
listOf(createCoinMarketRequestInterceptor()),
).create(CoinMarketCapApi::class.java)
}
}
}
private fun createCoinMarketRequestInterceptor(): Interceptor {
return object : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {
val requestBuilder = chain.request().newBuilder()
requestBuilder.addHeader("X-CMC_PRO_API_KEY", TapConfig.coinMarketCapKey)
return chain.proceed(requestBuilder.build())
}
}
}

View file

@ -0,0 +1,17 @@
package com.tangem.tap.network.coinmarketcap
import com.tangem.commands.common.network.Result
import com.tangem.commands.common.network.performRequest
import java.math.BigDecimal
class CoinMarketCapService {
private val api: CoinMarketCapApi by lazy { CoinMarketCapApi.create() }
suspend fun getRate(currency: String): Result<BigDecimal> {
val response = performRequest { api.getRateInfo(1, currency) }
return when (response) {
is Result.Success -> Result.Success(response.data.data.quote.usd.price)
is Result.Failure -> response
}
}
}

View file

@ -0,0 +1,41 @@
package com.tangem.tap.network.coinmarketcap
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
import java.math.BigDecimal
@JsonClass(generateAdapter = true)
data class RateInfoResponse(
val status: Status,
val data: RateData
)
@JsonClass(generateAdapter = true)
data class RateData(
val quote: Quote
)
@JsonClass(generateAdapter = true)
data class Quote(
@Json(name = "USD")
val usd: CurrencyRate
)
@JsonClass(generateAdapter = true)
data class CurrencyRate(
val price: BigDecimal
)
@JsonClass(generateAdapter = true)
data class Status(
val timestamp: String,
@Json(name = "error_code")
val errorCode: Int,
@Json(name = "error_message")
val errorMessage: String?,
val elapsed: Int,
@Json(name = "credit_count")
val creditCount: Int,
val notice: String?
)