/* * This file is part of LibEuFin. * Copyright (C) 2024 Taler Systems S.A. * LibEuFin is free software; you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as * published by the Free Software Foundation; either version 3, or * (at your option) any later version. * LibEuFin 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 Affero General * Public License for more details. * You should have received a copy of the GNU Affero General Public * License along with LibEuFin; see the file COPYING. If not, see * */ package tech.libeufin.bank import kotlinx.serialization.ExperimentalSerializationApi import kotlinx.serialization.KSerializer import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable import kotlinx.serialization.descriptors.SerialDescriptor import kotlinx.serialization.encoding.Decoder import kotlinx.serialization.encoding.Encoder import tech.libeufin.common.* import java.time.Instant /** * Allowed lengths for fractional digits in amounts. */ enum class FracDigits { TWO, EIGHT } // Allowed values for bank transactions directions. enum class TransactionDirection { credit, debit } enum class CashoutStatus { pending, aborted, confirmed } enum class WithdrawalStatus { pending, aborted, selected, confirmed } enum class AccountStatus { active, deleted } enum class RoundingMode { zero, up, nearest } enum class Timeframe { hour, day, month, year } enum class Operation { account_reconfig, account_delete, account_auth_reconfig, bank_transaction, cashout, withdrawal } enum class WireMethod { IBAN, X_TALER_BANK } @Serializable(with = Option.Serializer::class) sealed class Option { data object None : Option() data class Some(val value: T) : Option() fun get(): T? { return when (this) { None -> null is Some -> this.value } } inline fun some(lambda: (T) -> Unit) { if (this is Some) { lambda(value) } } fun isSome(): Boolean = this is Some @OptIn(ExperimentalSerializationApi::class) internal class Serializer ( private val valueSerializer: KSerializer ) : KSerializer> { override val descriptor: SerialDescriptor = valueSerializer.descriptor override fun serialize(encoder: Encoder, value: Option) { when (value) { None -> encoder.encodeNull() is Some -> valueSerializer.serialize(encoder, value.value) } } override fun deserialize(decoder: Decoder): Option { return Some(valueSerializer.deserialize(decoder)) } } } @Serializable data class TanChallenge( val challenge_id: Long ) @Serializable data class TanTransmission( val tan_info: String, val tan_channel: TanChannel ) /** * HTTP response type of successful token refresh. * access_token is the Crockford encoding of the 32 byte * access token, whereas 'expiration' is the point in time * when this token expires. */ @Serializable data class TokenSuccessResponse( val access_token: String, val expiration: TalerProtocolTimestamp ) /* Contains contact data to send TAN challges to the * users, to let them complete cashout operations. */ @Serializable data class ChallengeContactData( val email: Option = Option.None, val phone: Option = Option.None ) { init { if (email.get()?.let { !EMAIL_PATTERN.matches(it) } == true) throw badRequest("email contact data '$email' is malformed") if (phone.get()?.let { !PHONE_PATTERN.matches(it) } == true) throw badRequest("phone contact data '$phone' is malformed") } companion object { private val EMAIL_PATTERN = Regex("[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,4}") private val PHONE_PATTERN = Regex("^\\+?[0-9]+$") } } // Type expected at POST /accounts @Serializable data class RegisterAccountRequest( val username: String, val password: String, val name: String, val is_public: Boolean = false, val is_taler_exchange: Boolean = false, val contact_data: ChallengeContactData? = null, val cashout_payto_uri: IbanPayto? = null, val payto_uri: Payto? = null, val debit_threshold: TalerAmount? = null, val tan_channel: TanChannel? = null, ) @Serializable data class RegisterAccountResponse( val internal_payto_uri: String ) /** * Request of PATCH /accounts/{USERNAME} */ @Serializable data class AccountReconfiguration( val contact_data: ChallengeContactData? = null, val cashout_payto_uri: Option = Option.None, val name: String? = null, val is_public: Boolean? = null, val debit_threshold: TalerAmount? = null, val tan_channel: Option = Option.None, val is_taler_exchange: Boolean? = null, ) /** * Type expected at POST /accounts/{USERNAME}/token * It complies with Taler's design document #49 */ @Serializable data class TokenRequest( val scope: TokenScope, val duration: RelativeTime? = null, val refreshable: Boolean = false ) @Serializable sealed interface MonitorResponse { val talerInCount: Long val talerInVolume: TalerAmount val talerOutCount: Long val talerOutVolume: TalerAmount } @Serializable @SerialName("no-conversions") data class MonitorNoConversion( override val talerInCount: Long, override val talerInVolume: TalerAmount, override val talerOutCount: Long, override val talerOutVolume: TalerAmount ) : MonitorResponse @Serializable @SerialName("with-conversions") data class MonitorWithConversion( val cashinCount: Long, val cashinRegionalVolume: TalerAmount, val cashinFiatVolume: TalerAmount, val cashoutCount: Long, val cashoutRegionalVolume: TalerAmount, val cashoutFiatVolume: TalerAmount, override val talerInCount: Long, override val talerInVolume: TalerAmount, override val talerOutCount: Long, override val talerOutVolume: TalerAmount ) : MonitorResponse /** * Convenience type to get bank account information * from/to the database. */ data class BankInfo( val payto: String, val bankAccountId: Long, val isTalerExchange: Boolean, ) // Allowed values for cashout TAN channels. enum class TanChannel { sms, email } // Scopes for authentication tokens. enum class TokenScope { readonly, readwrite, refreshable // Not spec'd as a scope! } data class BearerToken( val scope: TokenScope, val isRefreshable: Boolean, val creationTime: Instant, val expirationTime: Instant, val login: String ) @Serializable data class Config( val currency: String, val currency_specification: CurrencySpecification, val bank_name: String, val allow_conversion: Boolean, val allow_registrations: Boolean, val allow_deletions: Boolean, val allow_edit_name: Boolean, val allow_edit_cashout_payto_uri: Boolean, val default_debit_threshold: TalerAmount, val supported_tan_channels: Set, val wire_type: WireMethod ) { val name: String = "libeufin-bank" val version: String = COREBANK_API_VERSION } @Serializable data class ConversionConfig( val regional_currency: String, val regional_currency_specification: CurrencySpecification, val fiat_currency: String, val fiat_currency_specification: CurrencySpecification, val conversion_rate: ConversionRate ) { val name: String = "taler-conversion-info" val version: String = CONVERSION_API_VERSION } @Serializable data class TalerIntegrationConfigResponse( val currency: String, val currency_specification: CurrencySpecification ) { val name: String = "taler-bank-integration" val version: String = INTEGRATION_API_VERSION } @Serializable data class WireGatewayConfig( val currency: String ) { val name: String = "taler-wire-gateway" val version: String = WIRE_GATEWAY_API_VERSION } @Serializable data class RevenueConfig( val currency: String ) { val name: String = "taler-revenue" val version: String = REVENUE_API_VERSION } enum class CreditDebitInfo { credit, debit } @Serializable data class Balance( val amount: TalerAmount, val credit_debit_indicator: CreditDebitInfo, ) /** * GET /accounts response. */ @Serializable data class AccountMinimalData( val username: String, val name: String, val payto_uri: String, val balance: Balance, val debit_threshold: TalerAmount, val is_public: Boolean, val is_taler_exchange: Boolean, val row_id: Long, val status: AccountStatus ) /** * Response type of GET /accounts. */ @Serializable data class ListBankAccountsResponse( val accounts: List ) /** * GET /accounts/$USERNAME response. */ @Serializable data class AccountData( val name: String, val balance: Balance, val payto_uri: String, val debit_threshold: TalerAmount, val contact_data: ChallengeContactData? = null, val cashout_payto_uri: String? = null, val tan_channel: TanChannel? = null, val is_public: Boolean, val is_taler_exchange: Boolean, val status: AccountStatus ) @Serializable data class TransactionCreateRequest( val payto_uri: Payto, val amount: TalerAmount?, val request_uid: ShortHashCode? ) @Serializable data class TransactionCreateResponse( val row_id: Long ) /* History element, either from GET /transactions/T_ID or from GET /transactions */ @Serializable data class BankAccountTransactionInfo( val creditor_payto_uri: String, val debtor_payto_uri: String, val amount: TalerAmount, val direction: TransactionDirection, val subject: String, val row_id: Long, // is T_ID val date: TalerProtocolTimestamp ) // Response type for histories, namely GET /transactions @Serializable data class BankAccountTransactionsResponse( val transactions: List ) // Taler withdrawal request. @Serializable data class BankAccountCreateWithdrawalRequest( val amount: TalerAmount ) // Taler withdrawal response. @Serializable data class BankAccountCreateWithdrawalResponse( val withdrawal_id: String, val taler_withdraw_uri: String ) @Serializable data class WithdrawalPublicInfo ( val status: WithdrawalStatus, val amount: TalerAmount, val username: String, val selected_reserve_pub: EddsaPublicKey? = null, val selected_exchange_account: String? = null, ) @Serializable data class CurrencySpecification( val name: String, val num_fractional_input_digits: Int, val num_fractional_normal_digits: Int, val num_fractional_trailing_zero_digits: Int, val alt_unit_names: Map ) @Serializable data class BankWithdrawalOperationStatus( val status: WithdrawalStatus, val amount: TalerAmount, val sender_wire: String? = null, val suggested_exchange: String? = null, val confirm_transfer_url: String? = null, val selected_reserve_pub: EddsaPublicKey? = null, val selected_exchange_account: String? = null, val wire_types: List, // TODO remove val aborted: Boolean, val selection_done: Boolean, val transfer_done: Boolean, ) /** * Selection request on a Taler withdrawal. */ @Serializable data class BankWithdrawalOperationPostRequest( val reserve_pub: EddsaPublicKey, val selected_exchange: Payto, ) /** * Response to the wallet after it selects the exchange * and the reserve pub. */ @Serializable data class BankWithdrawalOperationPostResponse( val status: WithdrawalStatus, val confirm_transfer_url: String? = null, // TODO remove val transfer_done: Boolean, ) @Serializable data class CashoutRequest( val request_uid: ShortHashCode, val subject: String?, val amount_debit: TalerAmount, val amount_credit: TalerAmount ) @Serializable data class CashoutResponse( val cashout_id: Long, ) @Serializable data class Cashouts( val cashouts: List, ) @Serializable data class CashoutInfo( val cashout_id: Long, val status: CashoutStatus, ) @Serializable data class GlobalCashouts( val cashouts: List, ) @Serializable data class GlobalCashoutInfo( val cashout_id: Long, val username: String, val status: CashoutStatus, ) @Serializable data class CashoutStatusResponse( val status: CashoutStatus, val amount_debit: TalerAmount, val amount_credit: TalerAmount, val subject: String, val creation_time: TalerProtocolTimestamp, val confirmation_time: TalerProtocolTimestamp? = null, val tan_channel: TanChannel? = null, val tan_info: String? = null ) @Serializable data class ChallengeSolve( val tan: String ) @Serializable data class ConversionResponse( val amount_debit: TalerAmount, val amount_credit: TalerAmount, ) /** * Request to an /admin/add-incoming request from * the Taler Wire Gateway API. */ @Serializable data class AddIncomingRequest( val amount: TalerAmount, val reserve_pub: EddsaPublicKey, val debit_account: Payto ) /** * Response to /admin/add-incoming */ @Serializable data class AddIncomingResponse( val timestamp: TalerProtocolTimestamp, val row_id: Long ) /** * Response of a TWG /history/incoming call. */ @Serializable data class IncomingHistory( val incoming_transactions: List, val credit_account: String ) /** * TWG's incoming payment record. */ @Serializable data class IncomingReserveTransaction( val type: String = "RESERVE", val row_id: Long, // DB row ID of the payment. val date: TalerProtocolTimestamp, val amount: TalerAmount, val debit_account: String, val reserve_pub: EddsaPublicKey ) /** * Response of a TWG /history/outgoing call. */ @Serializable data class OutgoingHistory( val outgoing_transactions: List, val debit_account: String ) /** * TWG's outgoinf payment record. */ @Serializable data class OutgoingTransaction( val row_id: Long, // DB row ID of the payment. val date: TalerProtocolTimestamp, val amount: TalerAmount, val credit_account: String, val wtid: ShortHashCode, val exchange_base_url: String, ) @Serializable data class RevenueIncomingHistory( val incoming_transactions : List, val credit_account: String ) @Serializable data class RevenueIncomingBankTransaction( val row_id: Long, val date: TalerProtocolTimestamp, val amount: TalerAmount, val debit_account: String, val subject: String ) /** * TWG's request to pay a merchant. */ @Serializable data class TransferRequest( val request_uid: HashCode, val amount: TalerAmount, val exchange_base_url: ExchangeUrl, val wtid: ShortHashCode, val credit_account: Payto ) /** * TWG's response to merchant payouts */ @Serializable data class TransferResponse( val timestamp: TalerProtocolTimestamp, val row_id: Long ) /** * Response to GET /public-accounts */ @Serializable data class PublicAccountsResponse( val public_accounts: List ) /** * Single element of GET /public-accounts list. */ @Serializable data class PublicAccount( val username: String, val payto_uri: String, val balance: Balance, val is_taler_exchange: Boolean, val row_id: Long ) /** * Request of PATCH /accounts/{USERNAME}/auth */ @Serializable data class AccountPasswordChange( val new_password: String, val old_password: String? = null )