taler-android

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

DepositManager.kt (10130B)


      1 /*
      2  * This file is part of GNU Taler
      3  * (C) 2022 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.deposit
     18 
     19 import android.util.Log
     20 import androidx.annotation.UiThread
     21 import kotlinx.coroutines.CoroutineScope
     22 import kotlinx.coroutines.flow.MutableStateFlow
     23 import kotlinx.coroutines.flow.asStateFlow
     24 import kotlinx.coroutines.launch
     25 import kotlinx.coroutines.runBlocking
     26 import kotlinx.serialization.SerialName
     27 import kotlinx.serialization.Serializable
     28 import kotlinx.serialization.json.jsonObject
     29 import kotlinx.serialization.json.jsonPrimitive
     30 import net.taler.common.Amount
     31 import net.taler.wallet.main.TAG
     32 import net.taler.wallet.accounts.KnownBankAccountInfo
     33 import net.taler.wallet.accounts.PaytoUriBitcoin
     34 import net.taler.wallet.accounts.PaytoUriIban
     35 import net.taler.wallet.accounts.PaytoUriTalerBank
     36 import net.taler.wallet.backend.BackendManager
     37 import net.taler.wallet.backend.TalerErrorCode.WALLET_DEPOSIT_GROUP_INSUFFICIENT_BALANCE
     38 import net.taler.wallet.backend.WalletBackendApi
     39 import net.taler.wallet.balances.BalanceManager
     40 import net.taler.wallet.balances.ScopeInfo
     41 import org.json.JSONObject
     42 import androidx.core.net.toUri
     43 
     44 class DepositManager(
     45     private val api: WalletBackendApi,
     46     private val scope: CoroutineScope,
     47     private val balanceManager: BalanceManager,
     48 ) {
     49 
     50     private val mDepositState = MutableStateFlow<DepositState>(DepositState.Start)
     51     internal val depositState = mDepositState.asStateFlow()
     52 
     53     fun isSupportedPayToUri(uriString: String): Boolean {
     54         if (!uriString.startsWith("payto://")) return false
     55         val u = uriString.toUri()
     56         if (!u.authority.equals("iban", ignoreCase = true)) return false
     57         return u.pathSegments.isNotEmpty()
     58     }
     59 
     60     @UiThread
     61     fun selectAccount(account: KnownBankAccountInfo) = scope.launch {
     62         getMaxDepositableForPayto(account.paytoUri).let { response ->
     63             mDepositState.value = DepositState.AccountSelected(account, response)
     64         }
     65     }
     66 
     67     suspend fun checkDepositFees(paytoUri: String, amount: Amount): CheckDepositResult {
     68         var response: CheckDepositResult = CheckDepositResult.None
     69         api.request("checkDeposit", CheckDepositResponse.serializer()) {
     70             put("depositPaytoUri", paytoUri)
     71             put("amount", amount.toJSONString())
     72         }.onSuccess {
     73             runBlocking {
     74                 val max = getMaxDepositAmount(amount.currency, paytoUri)
     75                 response = if (max?.effectiveAmount != null && amount > max.effectiveAmount) {
     76                     CheckDepositResult.ExceedsLimit(
     77                         maxDepositAmountEffective = max.effectiveAmount,
     78                         maxDepositAmountRaw = max.rawAmount,
     79                     )
     80                 } else {
     81                     CheckDepositResult.Success(
     82                         totalDepositCost = it.totalDepositCost,
     83                         effectiveDepositAmount = it.effectiveDepositAmount,
     84                         kycSoftLimit = it.kycSoftLimit,
     85                         kycHardLimit = it.kycHardLimit,
     86                         kycExchanges = it.kycExchanges,
     87                     )
     88                 }
     89             }
     90         }.onError { error ->
     91             Log.e(TAG, "Error checkDeposit $error")
     92             if (error.code == WALLET_DEPOSIT_GROUP_INSUFFICIENT_BALANCE) {
     93                 error.extra["insufficientBalanceDetails"]?.let { details ->
     94                     val maxAmountRaw = details.jsonObject["balanceAvailable"]?.let { amount ->
     95                         Amount.fromJSONString(amount.jsonPrimitive.content)
     96                     }
     97 
     98                     val maxAmountEffective = details.jsonObject["maxEffectiveSpendAmount"]?.let { amount ->
     99                         Amount.fromJSONString(amount.jsonPrimitive.content)
    100                     } ?: maxAmountRaw
    101 
    102                     response = CheckDepositResult.InsufficientBalance(
    103                         maxAmountEffective = maxAmountEffective,
    104                         maxAmountRaw = maxAmountRaw,
    105                     )
    106                 }
    107             }
    108         }
    109 
    110         return response
    111     }
    112 
    113     private suspend fun getMaxDepositAmount(
    114         currency: String,
    115         depositPaytoUri: String?,
    116     ): GetMaxDepositAmountResponse? {
    117         var response: GetMaxDepositAmountResponse? = null
    118         api.request("getMaxDepositAmount", GetMaxDepositAmountResponse.serializer()) {
    119             depositPaytoUri?.let { put("depositPaytoUri", it) }
    120             put("currency", currency)
    121         }.onError { error ->
    122             Log.e(TAG, "Error getMaxDepositAmount $error")
    123         }.onSuccess {
    124             response = it
    125         }
    126 
    127         return response
    128     }
    129 
    130     fun makeDeposit(amount: Amount, paytoUri: String) {
    131         mDepositState.value = DepositState.MakingDeposit
    132 
    133         scope.launch {
    134             api.request("createDepositGroup", CreateDepositGroupResponse.serializer()) {
    135                 put("depositPaytoUri", paytoUri)
    136                 put("amount", amount.toJSONString())
    137             }.onError {
    138                 Log.e(TAG, "Error createDepositGroup $it")
    139                 mDepositState.value = DepositState.Error(it)
    140             }.onSuccess {
    141                 mDepositState.value = DepositState.Success
    142             }
    143         }
    144     }
    145 
    146     @UiThread
    147     fun resetDepositState() {
    148         mDepositState.value = DepositState.Start
    149     }
    150 
    151     suspend fun validateIban(iban: String): Boolean {
    152         var response = false
    153         api.request("validateIban", ValidateIbanResponse.serializer()) {
    154             put("iban", iban)
    155         }.onError {
    156             Log.d(TAG, "Error validateIban $it")
    157             response = false
    158         }.onSuccess {
    159             response = it.valid
    160         }
    161         return response
    162     }
    163 
    164     suspend fun getDepositWireTypes(
    165         currency: String? = null,
    166         scopeInfo: ScopeInfo? = null,
    167     ): GetDepositWireTypesResponse? {
    168         var result: GetDepositWireTypesResponse? = null
    169         api.request("getDepositWireTypes", GetDepositWireTypesResponse.serializer()) {
    170             scopeInfo?.let { put("scopeInfo", JSONObject(BackendManager.json.encodeToString(it))) }
    171             currency?.let { put("currency", it) }
    172             this
    173         }.onError {
    174             Log.e(TAG, "Error getDepositWireTypes $it")
    175         }.onSuccess {
    176             result = it
    177         }
    178         return result
    179     }
    180 
    181     private suspend fun getMaxDepositableForPayto(
    182         paytoUri: String,
    183     ): Map<String, GetMaxDepositAmountResponse?> {
    184         return balanceManager.getCurrencies().associateWith { currency ->
    185             getMaxDepositAmount(currency, paytoUri)
    186         }
    187     }
    188 }
    189 
    190 fun getIbanPayto(
    191     receiverName: String,
    192     receiverPostalCode: String?,
    193     receiverTown: String?,
    194     iban: String,
    195 ) = PaytoUriIban(
    196     iban = iban,
    197     bic = null,
    198     targetPath = "",
    199     params = mapOf(),
    200     receiverName = receiverName,
    201     receiverPostalCode = receiverPostalCode,
    202     receiverTown = receiverTown,
    203 ).paytoUri
    204 
    205 fun getTalerPayto(receiverName: String, host: String, account: String) = PaytoUriTalerBank(
    206     host = host,
    207     account = account,
    208     targetPath = "",
    209     params = mapOf(),
    210     receiverName = receiverName,
    211 ).paytoUri
    212 
    213 fun getBitcoinPayto(bitcoinAddress: String, receiverName: String? = null) = PaytoUriBitcoin(
    214     segwitAddresses = listOf(bitcoinAddress),
    215     targetPath = bitcoinAddress,
    216     receiverName = receiverName,
    217 ).paytoUri
    218 
    219 @Serializable
    220 data class ValidateIbanResponse(
    221     val valid: Boolean,
    222 )
    223 
    224 @Serializable
    225 data class CheckDepositResponse(
    226     val totalDepositCost: Amount,
    227     val effectiveDepositAmount: Amount,
    228     val kycSoftLimit: Amount? = null,
    229     val kycHardLimit: Amount? = null,
    230     val kycExchanges: List<String>? = null,
    231 )
    232 
    233 @Serializable
    234 sealed class CheckDepositResult {
    235     data object None: CheckDepositResult()
    236 
    237     data class InsufficientBalance(
    238         val maxAmountEffective: Amount?,
    239         val maxAmountRaw: Amount?,
    240     ): CheckDepositResult()
    241 
    242     data class ExceedsLimit(
    243         val maxDepositAmountEffective: Amount,
    244         val maxDepositAmountRaw: Amount,
    245     ): CheckDepositResult()
    246 
    247     data class Success(
    248         val totalDepositCost: Amount,
    249         val effectiveDepositAmount: Amount,
    250         val kycSoftLimit: Amount? = null,
    251         val kycHardLimit: Amount? = null,
    252         val kycExchanges: List<String>? = null,
    253     ): CheckDepositResult()
    254 }
    255 
    256 @Serializable
    257 data class GetMaxDepositAmountResponse(
    258     val effectiveAmount: Amount,
    259     val rawAmount: Amount,
    260 )
    261 
    262 @Serializable
    263 data class CreateDepositGroupResponse(
    264     val depositGroupId: String,
    265     val transactionId: String,
    266 )
    267 
    268 @Serializable
    269 data class GetDepositWireTypesResponse(
    270     val wireTypeDetails: List<WireTypeDetails>,
    271 ) {
    272     val wireTypes: List<WireType>
    273         get() = wireTypeDetails.map { it.paymentTargetType }
    274 
    275     val hostNames: List<String>
    276         get() = wireTypeDetails
    277             .flatMap { it.talerBankHostnames }
    278             .distinct()
    279 }
    280 
    281 @Serializable
    282 data class GetScopesForPaytoResponse(
    283     val scopes: List<Scope>,
    284 ) {
    285     @Serializable
    286     data class Scope(
    287         val scopeInfo: ScopeInfo,
    288         val available: Boolean,
    289         // val restrictedAccounts: List<ExchangeWireAccount>,
    290     )
    291 }
    292 
    293 @Serializable
    294 enum class WireType {
    295     Unknown,
    296 
    297     @SerialName("iban")
    298     IBAN,
    299 
    300     @SerialName("x-taler-bank")
    301     TalerBank,
    302 
    303     @SerialName("bitcoin")
    304     Bitcoin,
    305 }
    306 
    307 @Serializable
    308 data class WireTypeDetails(
    309     val paymentTargetType: WireType,
    310     val talerBankHostnames: List<String>,
    311 )