taler-android

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

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 }