ExchangeManager.kt (11259B)
1 /* 2 * This file is part of GNU Taler 3 * (C) 2020 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.exchanges 18 19 import android.util.Log 20 import androidx.annotation.WorkerThread 21 import androidx.lifecycle.LiveData 22 import androidx.lifecycle.MutableLiveData 23 import kotlinx.coroutines.CoroutineScope 24 import kotlinx.coroutines.delay 25 import kotlinx.coroutines.flow.Flow 26 import kotlinx.coroutines.flow.flow 27 import kotlinx.coroutines.launch 28 import kotlinx.coroutines.runBlocking 29 import kotlinx.serialization.Serializable 30 import net.taler.common.CurrencySpecification 31 import net.taler.common.Event 32 import net.taler.common.toEvent 33 import net.taler.wallet.main.TAG 34 import net.taler.wallet.backend.BackendManager 35 import net.taler.wallet.backend.TalerErrorInfo 36 import net.taler.wallet.backend.WalletBackendApi 37 import net.taler.wallet.balances.GetCurrencySpecificationResponse 38 import net.taler.wallet.balances.ScopeInfo 39 import org.json.JSONObject 40 41 @Serializable 42 data class ExchangeListResponse( 43 val exchanges: List<ExchangeItem>, 44 ) 45 46 class ExchangeManager( 47 private val api: WalletBackendApi, 48 private val scope: CoroutineScope, 49 ) { 50 51 private val mProgress = MutableLiveData<Boolean>() 52 val progress: LiveData<Boolean> = mProgress 53 54 private val mExchanges = MutableLiveData<List<ExchangeItem>>() 55 val exchanges: LiveData<List<ExchangeItem>> get() = list() 56 57 private val mAddError = MutableLiveData<Event<TalerErrorInfo>>() 58 val addError: LiveData<Event<TalerErrorInfo>> = mAddError 59 60 private val mListError = MutableLiveData<Event<TalerErrorInfo>>() 61 val listError: LiveData<Event<TalerErrorInfo>> = mListError 62 63 private val mDeleteError = MutableLiveData<Event<TalerErrorInfo>>() 64 val deleteError: LiveData<Event<TalerErrorInfo>> = mDeleteError 65 66 private val mReloadError = MutableLiveData<Event<TalerErrorInfo>>() 67 val reloadError: LiveData<Event<TalerErrorInfo>> = mReloadError 68 69 var withdrawalExchange: ExchangeItem? = null 70 71 private val currencySpecs: MutableMap<ScopeInfo, CurrencySpecification?> = mutableMapOf() 72 73 private fun list(): LiveData<List<ExchangeItem>> { 74 mProgress.value = true 75 scope.launch { 76 val response = api.request("listExchanges", ExchangeListResponse.serializer()) 77 response.onError { 78 mProgress.value = false 79 mListError.value = it.toEvent() 80 }.onSuccess { 81 Log.d(TAG, "Exchange list: ${it.exchanges}") 82 mProgress.value = false 83 mExchanges.value = it.exchanges 84 scope.launch { 85 // load and cache all currency specs 86 it.exchanges.forEach { exchange -> 87 if (exchange.scopeInfo != null) { 88 getCurrencySpecification(exchange.scopeInfo) 89 } 90 } 91 } 92 } 93 } 94 return mExchanges 95 } 96 97 fun add(exchangeUrl: String, onSuccess: (() -> Unit)? = null) = scope.launch { 98 mProgress.value = true 99 api.request<Unit>("addExchange") { 100 put("allowCompletion", true) 101 put("uri", exchangeUrl) 102 }.onError { 103 Log.e(TAG, "Error adding exchange: $it") 104 mProgress.value = false 105 mAddError.value = it.toEvent() 106 }.onSuccess { 107 mProgress.value = false 108 Log.d(TAG, "Exchange $exchangeUrl added") 109 list() 110 onSuccess?.invoke() 111 } 112 } 113 114 fun reload(exchangeUrl: String, force: Boolean = true) = scope.launch { 115 mProgress.value = true 116 api.request<Unit>("updateExchangeEntry") { 117 put("exchangeBaseUrl", exchangeUrl) 118 put("force", force) 119 }.onError { 120 Log.e(TAG, "Error reloading exchange: $it") 121 mProgress.value = false 122 mReloadError.value = it.toEvent() 123 }.onSuccess { 124 mProgress.value = false 125 Log.d(TAG, "Exchange $exchangeUrl reloaded") 126 list() 127 } 128 } 129 130 fun delete(exchangeUrl: String, purge: Boolean = false) = scope.launch { 131 mProgress.value = true 132 api.request<Unit>("deleteExchange") { 133 put("exchangeBaseUrl", exchangeUrl) 134 put("purge", purge) 135 }.onError { 136 Log.e(TAG, "Error deleting exchange: $it") 137 mProgress.value = false 138 mDeleteError.value = it.toEvent() 139 }.onSuccess { 140 mProgress.value = false 141 Log.d(TAG, "Exchange $exchangeUrl deleted") 142 list() 143 } 144 } 145 146 fun findExchangeForCurrency(currency: String): Flow<ExchangeItem?> = flow { 147 emit(findExchange(currency)) 148 } 149 150 fun findExchangeForBaseUrl(url: String): Flow<ExchangeItem?> = flow { 151 emit(findExchangeByUrl(url)) 152 } 153 154 @WorkerThread 155 suspend fun findExchange(currency: String): ExchangeItem? { 156 var exchange: ExchangeItem? = null 157 api.request( 158 operation = "listExchanges", 159 serializer = ExchangeListResponse.serializer() 160 ).onSuccess { exchangeListResponse -> 161 // just pick the first for now 162 exchange = exchangeListResponse.exchanges.find { it.currency == currency } 163 } 164 return exchange 165 } 166 167 @WorkerThread 168 suspend fun findExchange(scope: ScopeInfo): ExchangeItem? { 169 var exchange: ExchangeItem? = null 170 api.request( 171 operation = "listExchanges", 172 serializer = ExchangeListResponse.serializer() 173 ).onSuccess { exchangeListResponse -> 174 // just pick the first for now 175 exchange = exchangeListResponse.exchanges.find { it.scopeInfo == scope } 176 } 177 return exchange 178 } 179 180 @WorkerThread 181 suspend fun findExchangeByUrl(exchangeUrl: String): ExchangeItem? { 182 var exchange: ExchangeItem? = null 183 api.request("getExchangeEntryByUrl", ExchangeItem.serializer()) { 184 put("exchangeBaseUrl", exchangeUrl) 185 }.onError { 186 Log.e(TAG, "Error getExchangeEntryByUrl: $it") 187 }.onSuccess { 188 exchange = it 189 } 190 return exchange 191 } 192 193 /** 194 * Fetch exchange terms of service. 195 */ 196 suspend fun getExchangeTos( 197 exchangeBaseUrl: String, 198 language: String? = null, 199 ): TosResponse? { 200 var result: TosResponse? = null 201 api.request("getExchangeTos", TosResponse.serializer()) { 202 language?.let { put("acceptLanguage", it) } 203 put("exchangeBaseUrl", exchangeBaseUrl) 204 }.onError { error -> 205 Log.d(TAG, "Error getExchangeTos: $error") 206 }.onSuccess { 207 result = it 208 } 209 return result 210 } 211 212 /** 213 * Accept the currently displayed terms of service. 214 */ 215 suspend fun acceptCurrentTos( 216 exchangeBaseUrl: String, 217 currentEtag: String, 218 ): Boolean { 219 var success = false 220 api.request<Unit>("setExchangeTosAccepted") { 221 put("exchangeBaseUrl", exchangeBaseUrl) 222 put("etag", currentEtag) 223 }.onError { error -> 224 Log.d(TAG, "Error setExchangeTosAccepted: $error") 225 }.onSuccess { 226 success = true 227 // update exchange list 228 list() 229 } 230 return success 231 } 232 233 /** 234 * Un-accept the terms of service of an exchange 235 */ 236 suspend fun forgetCurrentTos( 237 exchangeBaseUrl: String, 238 currentEtag: String, 239 ): Boolean { 240 var success = false 241 api.request<Unit>("setExchangeTosForgotten") { 242 put("exchangeBaseUrl", exchangeBaseUrl) 243 put("etag", currentEtag) 244 }.onError { error -> 245 Log.d(TAG, "Error setExchangeTosForgotten: $error") 246 }.onSuccess { 247 success = true 248 list() 249 } 250 return success 251 } 252 253 fun addDevExchanges() { 254 scope.launch { 255 listOf( 256 "https://exchange.demo.taler.net/", 257 "https://exchange.test.taler.net/", 258 "https://exchange.head.taler.net/", 259 "https://exchange.taler.ar/", 260 "https://exchange.taler.fdold.eu/", 261 "https://exchange.taler.grothoff.org/", 262 ).forEach { exchangeUrl -> 263 add(exchangeUrl) 264 delay(100) 265 } 266 exchanges.value?.let { exs -> 267 exs.find { 268 it.exchangeBaseUrl.startsWith("https://exchange.taler.fdold.eu") 269 }?.let { fDoldExchange -> 270 api.request<Unit>("addGlobalCurrencyExchange") { 271 put("currency", fDoldExchange.currency) 272 put("exchangeBaseUrl", fDoldExchange.exchangeBaseUrl) 273 put("exchangeMasterPub", 274 "7ER30ZWJEXAG026H5KG9M19NGTFC2DKKFPV79GVXA6DK5DCNSWXG") 275 }.onError { 276 Log.e(TAG, "Error addGlobalCurrencyExchange: $it") 277 }.onSuccess { 278 Log.i(TAG, "fdold is global now!") 279 } 280 } 281 } 282 } 283 } 284 285 suspend fun getCurrencySpecification(scopeInfo: ScopeInfo): CurrencySpecification? { 286 if (currencySpecs.containsKey(scopeInfo)) { 287 return currencySpecs[scopeInfo] 288 } 289 290 var spec: CurrencySpecification? = null 291 api.request("getCurrencySpecification", GetCurrencySpecificationResponse.serializer()) { 292 val json = BackendManager.json.encodeToString(scopeInfo) 293 Log.d(TAG, "ExchangeManager: $json") 294 put("scope", JSONObject(json)) 295 }.onSuccess { 296 currencySpecs[scopeInfo] = it.currencySpecification 297 spec = it.currencySpecification 298 }.onError { 299 Log.e(TAG, "Error getting currency spec for scope $scopeInfo: $it") 300 } 301 return spec 302 } 303 304 @Deprecated("Please find spec via scopeInfo instead", ReplaceWith("getSpecForScopeInfo")) 305 fun getSpecForCurrency(currency: String): CurrencySpecification? { 306 return currencySpecs.keys.firstOrNull { 307 it.currency == currency 308 }?.let { currencySpecs[it] } 309 } 310 311 fun getSpecForCurrency(currency: String, scopes: List<ScopeInfo>) = 312 scopes.find { it.currency == currency }?.let { scope -> 313 runBlocking { getCurrencySpecification(scope) } 314 } 315 316 fun getSpecForScopeInfo(scopeInfo: ScopeInfo): CurrencySpecification? { 317 return runBlocking { getCurrencySpecification(scopeInfo) } 318 } 319 }