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 )