Accounts.kt
/*
* This file is part of GNU Taler
* (C) 2022 Taler Systems S.A.
*
* GNU Taler is free software; you can redistribute it and/or modify it under the
* terms of the GNU General Public License as published by the Free Software
* Foundation; either version 3, or (at your option) any later version.
*
* GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
* A PARTICULAR PURPOSE. See the GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along with
* GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
package net.taler.wallet.accounts
import android.net.Uri
import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.JsonClassDiscriminator
import net.taler.common.Bech32
import net.taler.wallet.backend.TalerErrorInfo
import androidx.core.net.toUri
@Serializable
data class KnownBankAccountInfo(
val bankAccountId: String,
val paytoUri: String,
/**
* Did we previously complete a KYC process for this bank account?
*/
val kycCompleted: Boolean,
/**
* Currencies supported by the bank, if known.
*/
val currencies: List<String>? = null,
val label: String? = null,
)
@Serializable
sealed class ListBankAccountsResult {
@Serializable
data object None: ListBankAccountsResult()
@Serializable
data class Success(
val accounts: List<KnownBankAccountInfo>,
val currency: String?,
): ListBankAccountsResult()
@Serializable
data class Error(val error: TalerErrorInfo): ListBankAccountsResult()
}
@Serializable
data class ListBankAccountsResponse(
val accounts: List<KnownBankAccountInfo>,
)
@Serializable
data class AddBankAccountResponse(
val bankAccountId: String,
)
@Serializable
@OptIn(ExperimentalSerializationApi::class)
@JsonClassDiscriminator("targetType")
sealed class PaytoUri(
val isKnown: Boolean,
val targetType: String,
) {
abstract val targetPath: String
abstract val params: Map<String, String>
abstract val receiverName: String?
companion object {
fun parse(paytoUri: String): PaytoUri? {
val uri = paytoUri.toUri()
if (uri.scheme != "payto") return null
if (uri.pathSegments.isEmpty()) return null
return when (uri.authority?.lowercase()) {
"iban" -> PaytoUriIban.fromString(uri)
"x-taler-bank" -> PaytoUriTalerBank.fromString(uri)
"cyclos" -> PaytoUriCyclos.fromString(uri)
"bitcoin" -> PaytoUriBitcoin.fromString(uri)
else -> null
}
}
}
}
@Serializable
@SerialName("iban")
data class PaytoUriIban(
val iban: String,
val bic: String? = "SANDBOXX",
override val targetPath: String,
override val params: Map<String, String>,
override val receiverName: String?,
val receiverPostalCode: String?,
val receiverTown: String?,
) : PaytoUri(
isKnown = true,
targetType = "iban",
) {
val paytoUri: String
get() = Uri.Builder()
.scheme("payto")
.authority(targetType)
.apply { if (bic != null) appendPath(bic) }
.appendPath(iban)
.apply {
receiverName?.let { appendQueryParameter("receiver-name", it) }
receiverPostalCode?.let { appendQueryParameter("receiver-postal-code", it) }
receiverTown?.let { appendQueryParameter("receiver-town", it) }
params.forEach { (key, value) ->
if (value.isNotEmpty() && build().getQueryParameter(key) == null) {
appendQueryParameter(key, value)
}
}
}.build().toString()
companion object {
fun fromString(uri: Uri): PaytoUriIban? {
return PaytoUriIban(
iban = uri.lastPathSegment ?: return null,
bic = if (uri.pathSegments.size > 1) {
uri.pathSegments.first() ?: return null
} else null,
params = uri.queryParametersMap,
receiverName = uri.getQueryParameter("receiver-name"),
receiverPostalCode = uri.getQueryParameter("receiver-postal-code"),
receiverTown = uri.getQueryParameter("receiver-town"),
targetPath = "",
)
}
}
}
@Serializable
@SerialName("x-taler-bank")
data class PaytoUriTalerBank(
val host: String,
val account: String,
override val targetPath: String,
override val params: Map<String, String>,
override val receiverName: String?,
) : PaytoUri(
isKnown = true,
targetType = "x-taler-bank",
) {
val paytoUri: String
get() = Uri.Builder()
.scheme("payto")
.authority(targetType)
.appendPath(host)
.appendPath(account)
.apply {
receiverName?.let { appendQueryParameter("receiver-name", it) }
params.forEach { (key, value) ->
if (value.isNotEmpty()) {
appendQueryParameter(key, value)
}
}
}
.build().toString()
companion object {
fun fromString(uri: Uri): PaytoUriTalerBank? {
return PaytoUriTalerBank(
host = uri.pathSegments.getOrNull(0) ?: return null,
account = uri.pathSegments.getOrNull(1) ?: return null,
params = uri.queryParametersMap,
receiverName = uri.getQueryParameter("receiver-name"),
targetPath = "",
)
}
}
}
@Serializable
@SerialName("cyclos")
data class PaytoUriCyclos(
val host: String,
val account: String,
override val targetPath: String,
override val params: Map<String, String>,
override val receiverName: String,
) : PaytoUri(
isKnown = true,
targetType = "cyclos",
) {
val paytoUri: String
get() = Uri.Builder()
.scheme("payto")
.authority(targetType)
.appendPath(host)
.appendPath(account)
.apply {
appendQueryParameter("receiver-name", receiverName)
params.forEach { (key, value) ->
if (value.isNotEmpty()) {
appendQueryParameter(key, value)
}
}
}
.build().toString()
companion object {
fun fromString(uri: Uri): PaytoUriCyclos? {
return PaytoUriCyclos(
account = uri.lastPathSegment ?: return null,
receiverName = uri.getQueryParameter("receiver-name") ?: return null,
host = uri.pathSegments
.subList(0, uri.pathSegments.lastIndex)
.joinToString("/"),
params = uri.queryParametersMap,
targetPath = "",
)
}
}
}
@Serializable
@SerialName("bitcoin")
data class PaytoUriBitcoin(
@SerialName("segwitAddrs")
val segwitAddresses: List<String>,
override val targetPath: String,
override val params: Map<String, String> = emptyMap(),
override val receiverName: String?,
) : PaytoUri(
isKnown = true,
targetType = "bitcoin",
) {
val paytoUri: String
get() = Uri.Builder()
.scheme("payto")
.authority(targetType)
.apply {
segwitAddresses.forEach { address ->
appendPath(address)
}
}
.apply {
receiverName?.let { appendQueryParameter("receiver-name", it) }
params.forEach { (key, value) ->
if (value.isNotEmpty()) {
appendQueryParameter(key, value)
}
}
}
.build().toString()
companion object {
fun fromString(uri: Uri): PaytoUriBitcoin? {
val msg = uri.getQueryParameter("message").orEmpty()
val reg = "\\b([A-Z0-9]{52})\\b".toRegex().find(msg)
val reserve = reg?.value
?: uri.getQueryParameter("subject")
?: return null
val segwitAddresses = Bech32.generateFakeSegwitAddress(
reservePub = reserve,
addr = uri.pathSegments.firstOrNull()
?: return null,
)
return PaytoUriBitcoin(
segwitAddresses = segwitAddresses,
params = uri.queryParametersMap,
receiverName = uri.getQueryParameter("receiver-name"),
targetPath = "",
)
}
}
}
val Uri.queryParametersMap: Map<String, String>
get() = queryParameterNames.mapNotNull { name ->
getQueryParameter(name)?.let { name to it }
}.toMap()