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 }