Accounts.kt (9157B)
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.accounts 18 19 import android.net.Uri 20 import kotlinx.serialization.ExperimentalSerializationApi 21 import kotlinx.serialization.SerialName 22 import kotlinx.serialization.Serializable 23 import kotlinx.serialization.json.JsonClassDiscriminator 24 import net.taler.common.Bech32 25 import net.taler.wallet.backend.TalerErrorInfo 26 import androidx.core.net.toUri 27 28 @Serializable 29 data class KnownBankAccountInfo( 30 val bankAccountId: String, 31 val paytoUri: String, 32 33 /** 34 * Did we previously complete a KYC process for this bank account? 35 */ 36 val kycCompleted: Boolean, 37 38 /** 39 * Currencies supported by the bank, if known. 40 */ 41 val currencies: List<String>? = null, 42 43 val label: String? = null, 44 ) 45 46 @Serializable 47 sealed class ListBankAccountsResult { 48 @Serializable 49 data object None: ListBankAccountsResult() 50 51 @Serializable 52 data class Success( 53 val accounts: List<KnownBankAccountInfo>, 54 val currency: String?, 55 ): ListBankAccountsResult() 56 57 @Serializable 58 data class Error(val error: TalerErrorInfo): ListBankAccountsResult() 59 } 60 61 @Serializable 62 data class ListBankAccountsResponse( 63 val accounts: List<KnownBankAccountInfo>, 64 ) 65 66 @Serializable 67 data class AddBankAccountResponse( 68 val bankAccountId: String, 69 ) 70 71 @Serializable 72 @OptIn(ExperimentalSerializationApi::class) 73 @JsonClassDiscriminator("targetType") 74 sealed class PaytoUri( 75 val isKnown: Boolean, 76 val targetType: String, 77 ) { 78 abstract val targetPath: String 79 abstract val params: Map<String, String> 80 abstract val receiverName: String? 81 82 companion object { 83 fun parse(paytoUri: String): PaytoUri? { 84 val uri = paytoUri.toUri() 85 if (uri.scheme != "payto") return null 86 if (uri.pathSegments.isEmpty()) return null 87 return when (uri.authority?.lowercase()) { 88 "iban" -> PaytoUriIban.fromString(uri) 89 "x-taler-bank" -> PaytoUriTalerBank.fromString(uri) 90 "cyclos" -> PaytoUriCyclos.fromString(uri) 91 "bitcoin" -> PaytoUriBitcoin.fromString(uri) 92 else -> null 93 } 94 } 95 } 96 } 97 98 @Serializable 99 @SerialName("iban") 100 data class PaytoUriIban( 101 val iban: String, 102 val bic: String? = "SANDBOXX", 103 override val targetPath: String, 104 override val params: Map<String, String>, 105 override val receiverName: String?, 106 val receiverPostalCode: String?, 107 val receiverTown: String?, 108 ) : PaytoUri( 109 isKnown = true, 110 targetType = "iban", 111 ) { 112 val paytoUri: String 113 get() = Uri.Builder() 114 .scheme("payto") 115 .authority(targetType) 116 .apply { if (bic != null) appendPath(bic) } 117 .appendPath(iban) 118 .apply { 119 receiverName?.let { appendQueryParameter("receiver-name", it) } 120 receiverPostalCode?.let { appendQueryParameter("receiver-postal-code", it) } 121 receiverTown?.let { appendQueryParameter("receiver-town", it) } 122 params.forEach { (key, value) -> 123 if (value.isNotEmpty() && build().getQueryParameter(key) == null) { 124 appendQueryParameter(key, value) 125 } 126 } 127 }.build().toString() 128 129 companion object { 130 fun fromString(uri: Uri): PaytoUriIban? { 131 return PaytoUriIban( 132 iban = uri.lastPathSegment ?: return null, 133 bic = if (uri.pathSegments.size > 1) { 134 uri.pathSegments.first() ?: return null 135 } else null, 136 params = uri.queryParametersMap, 137 receiverName = uri.getQueryParameter("receiver-name"), 138 receiverPostalCode = uri.getQueryParameter("receiver-postal-code"), 139 receiverTown = uri.getQueryParameter("receiver-town"), 140 targetPath = "", 141 ) 142 } 143 } 144 } 145 146 @Serializable 147 @SerialName("x-taler-bank") 148 data class PaytoUriTalerBank( 149 val host: String, 150 val account: String, 151 override val targetPath: String, 152 override val params: Map<String, String>, 153 override val receiverName: String?, 154 ) : PaytoUri( 155 isKnown = true, 156 targetType = "x-taler-bank", 157 ) { 158 val paytoUri: String 159 get() = Uri.Builder() 160 .scheme("payto") 161 .authority(targetType) 162 .appendPath(host) 163 .appendPath(account) 164 .apply { 165 receiverName?.let { appendQueryParameter("receiver-name", it) } 166 params.forEach { (key, value) -> 167 if (value.isNotEmpty()) { 168 appendQueryParameter(key, value) 169 } 170 } 171 } 172 .build().toString() 173 174 companion object { 175 fun fromString(uri: Uri): PaytoUriTalerBank? { 176 return PaytoUriTalerBank( 177 host = uri.pathSegments.getOrNull(0) ?: return null, 178 account = uri.pathSegments.getOrNull(1) ?: return null, 179 params = uri.queryParametersMap, 180 receiverName = uri.getQueryParameter("receiver-name"), 181 targetPath = "", 182 ) 183 } 184 } 185 } 186 187 @Serializable 188 @SerialName("cyclos") 189 data class PaytoUriCyclos( 190 val host: String, 191 val account: String, 192 override val targetPath: String, 193 override val params: Map<String, String>, 194 override val receiverName: String, 195 ) : PaytoUri( 196 isKnown = true, 197 targetType = "cyclos", 198 ) { 199 val paytoUri: String 200 get() = Uri.Builder() 201 .scheme("payto") 202 .authority(targetType) 203 .appendPath(host) 204 .appendPath(account) 205 .apply { 206 appendQueryParameter("receiver-name", receiverName) 207 params.forEach { (key, value) -> 208 if (value.isNotEmpty()) { 209 appendQueryParameter(key, value) 210 } 211 } 212 } 213 .build().toString() 214 215 companion object { 216 fun fromString(uri: Uri): PaytoUriCyclos? { 217 return PaytoUriCyclos( 218 account = uri.lastPathSegment ?: return null, 219 receiverName = uri.getQueryParameter("receiver-name") ?: return null, 220 host = uri.pathSegments 221 .subList(0, uri.pathSegments.lastIndex) 222 .joinToString("/"), 223 params = uri.queryParametersMap, 224 targetPath = "", 225 ) 226 } 227 } 228 } 229 230 @Serializable 231 @SerialName("bitcoin") 232 data class PaytoUriBitcoin( 233 @SerialName("segwitAddrs") 234 val segwitAddresses: List<String>, 235 override val targetPath: String, 236 override val params: Map<String, String> = emptyMap(), 237 override val receiverName: String?, 238 ) : PaytoUri( 239 isKnown = true, 240 targetType = "bitcoin", 241 ) { 242 val paytoUri: String 243 get() = Uri.Builder() 244 .scheme("payto") 245 .authority(targetType) 246 .apply { 247 segwitAddresses.forEach { address -> 248 appendPath(address) 249 } 250 } 251 .apply { 252 receiverName?.let { appendQueryParameter("receiver-name", it) } 253 params.forEach { (key, value) -> 254 if (value.isNotEmpty()) { 255 appendQueryParameter(key, value) 256 } 257 } 258 } 259 .build().toString() 260 261 companion object { 262 fun fromString(uri: Uri): PaytoUriBitcoin? { 263 val msg = uri.getQueryParameter("message").orEmpty() 264 val reg = "\\b([A-Z0-9]{52})\\b".toRegex().find(msg) 265 val reserve = reg?.value 266 ?: uri.getQueryParameter("subject") 267 ?: return null 268 val segwitAddresses = Bech32.generateFakeSegwitAddress( 269 reservePub = reserve, 270 addr = uri.pathSegments.firstOrNull() 271 ?: return null, 272 ) 273 274 return PaytoUriBitcoin( 275 segwitAddresses = segwitAddresses, 276 params = uri.queryParametersMap, 277 receiverName = uri.getQueryParameter("receiver-name"), 278 targetPath = "", 279 ) 280 } 281 } 282 } 283 284 val Uri.queryParametersMap: Map<String, String> 285 get() = queryParameterNames.mapNotNull { name -> 286 getQueryParameter(name)?.let { name to it } 287 }.toMap()