taler-android

Android apps for GNU Taler (wallet, PoS, cashier)
Log | Files | Refs | README | LICENSE

BalanceManager.kt (6157B)


      1 /*
      2  * This file is part of GNU Taler
      3  * (C) 2024 Taler Systems S.A.
      4  *
      5  * GNU Taler is free software; you can redistribute it and/or modify it under the
      6  * terms of the GNU General Public License as published by the Free Software
      7  * Foundation; either version 3, or (at your option) any later version.
      8  *
      9  * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
     10  * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
     11  * A PARTICULAR PURPOSE.  See the GNU General Public License for more details.
     12  *
     13  * You should have received a copy of the GNU General Public License along with
     14  * GNU Taler; see the file COPYING.  If not, see <http://www.gnu.org/licenses/>
     15  */
     16 
     17 package net.taler.wallet.balances
     18 
     19 import android.util.Log
     20 import androidx.annotation.UiThread
     21 import androidx.lifecycle.LiveData
     22 import androidx.lifecycle.MutableLiveData
     23 import androidx.lifecycle.distinctUntilChanged
     24 import kotlinx.coroutines.CoroutineScope
     25 import kotlinx.coroutines.launch
     26 import kotlinx.coroutines.runBlocking
     27 import kotlinx.serialization.Serializable
     28 import net.taler.common.Amount
     29 import net.taler.common.CurrencySpecification
     30 import net.taler.wallet.main.TAG
     31 import net.taler.wallet.backend.TalerErrorInfo
     32 import net.taler.wallet.backend.WalletBackendApi
     33 import net.taler.wallet.donau.DonauSummaryItem
     34 import net.taler.wallet.exchanges.ExchangeItem
     35 import net.taler.wallet.exchanges.ExchangeManager
     36 
     37 @Serializable
     38 data class BalanceResponse(
     39     val balances: List<BalanceItem>,
     40     val donauSummary: List<DonauSummaryItem>? = null,
     41 )
     42 
     43 @Serializable
     44 data class GetCurrencySpecificationResponse(
     45     val currencySpecification: CurrencySpecification,
     46 )
     47 
     48 // TODO: rename to AssetsState
     49 sealed class BalanceState {
     50     data object None: BalanceState()
     51     data object Loading: BalanceState()
     52 
     53     data class Success(
     54         val balances: List<BalanceItem>,
     55         val donauSummary: List<DonauSummaryItem>,
     56     ): BalanceState()
     57 
     58     data class Error(
     59         val error: TalerErrorInfo,
     60     ): BalanceState()
     61 
     62     fun showWelcome() = this is Success
     63             && balances.isEmpty()
     64             && donauSummary.isEmpty()
     65 }
     66 
     67 // TODO: rename to AssetsManager
     68 class BalanceManager(
     69     private val api: WalletBackendApi,
     70     private val scope: CoroutineScope,
     71     private val exchangeManager: ExchangeManager,
     72 ) {
     73     private val mBalances = MutableLiveData<List<BalanceItem>>(emptyList())
     74     val balances: LiveData<List<BalanceItem>> = mBalances
     75 
     76     private val mState = MutableLiveData<BalanceState>(BalanceState.None)
     77     val state: LiveData<BalanceState> = mState.distinctUntilChanged()
     78 
     79     fun loadAssets(loading: Boolean = false) = scope.launch {
     80         if (loading) mState.postValue(BalanceState.Loading)
     81         api.request("getBalances", BalanceResponse.serializer())
     82             .onError {
     83                 Log.e(TAG, "Error retrieving balances: $it")
     84                 mState.postValue(BalanceState.Error(it))
     85             }.onSuccess { res ->
     86                 val balances = res.balances.map { balance ->
     87                     val spec = runBlocking { exchangeManager
     88                         .getCurrencySpecification(balance.scopeInfo) }
     89                     balance.copy(
     90                         available = balance.available.withSpec(spec),
     91                         pendingIncoming = balance.pendingIncoming.withSpec(spec),
     92                         pendingOutgoing = balance.pendingOutgoing.withSpec(spec),
     93                     )
     94                 }
     95 
     96                 val donauSummary = res.donauSummary?.map { item ->
     97                     val spec = runBlocking { exchangeManager
     98                         .getSpecForCurrency(item.amountReceiptsAvailable.currency) }
     99                     item.copy(
    100                         amountReceiptsAvailable = item.amountReceiptsAvailable.withSpec(spec),
    101                         amountReceiptsSubmitted = item.amountReceiptsSubmitted.withSpec(spec),
    102                         amountStatement = item.amountStatement?.withSpec(spec),
    103                     )
    104                 } ?: emptyList()
    105 
    106                 mBalances.postValue(balances)
    107                 mState.postValue(BalanceState.Success(
    108                     balances = balances,
    109                     donauSummary = donauSummary,
    110                 ))
    111             }
    112     }
    113 
    114     @UiThread
    115     fun getCurrencies() = balances.value?.map { balanceItem ->
    116         balanceItem.currency
    117     } ?: emptyList()
    118 
    119     @UiThread
    120     fun getScopes(forPeer: Boolean = false) = balances.value?.filter {
    121         !forPeer || !it.disablePeerPayments
    122     }?.map { it.scopeInfo } ?: emptyList()
    123 
    124     @UiThread
    125     fun hasSufficientBalance(amount: Amount): Boolean {
    126         balances.value?.forEach { balanceItem ->
    127             if (balanceItem.currency == amount.currency) {
    128                 return balanceItem.available >= amount
    129             }
    130         }
    131         return false
    132     }
    133 
    134     fun addGlobalCurrencyExchange(
    135         currency: String,
    136         exchange: ExchangeItem,
    137         onSuccess: () -> Unit,
    138         onError: (error: TalerErrorInfo) -> Unit,
    139     ) = scope.launch {
    140         api.request<Unit>("addGlobalCurrencyExchange") {
    141             put("currency", currency)
    142             put("exchangeBaseUrl", exchange.exchangeBaseUrl)
    143             put("exchangeMasterPub", exchange.masterPub)
    144         }.onError { error ->
    145             Log.e(TAG, "got addGlobalCurrencyExchange error: $error")
    146             onError(error)
    147         }.onSuccess {
    148             onSuccess()
    149         }
    150     }
    151 
    152     fun removeGlobalCurrencyExchange(
    153         currency: String,
    154         exchange: ExchangeItem,
    155         onSuccess: () -> Unit,
    156         onError: (error: TalerErrorInfo) -> Unit,
    157     ) = scope.launch {
    158         api.request<Unit>("removeGlobalCurrencyExchange") {
    159             put("currency", currency)
    160             put("exchangeBaseUrl", exchange.exchangeBaseUrl)
    161             put("exchangeMasterPub", exchange.masterPub)
    162         }.onError { error ->
    163             Log.e(TAG, "got removeGlobalCurrencyExchange error: $error")
    164             onError(error)
    165         }.onSuccess {
    166             onSuccess()
    167         }
    168     }
    169 
    170     fun resetBalances() {
    171         mState.value = BalanceState.None
    172     }
    173 }