summaryrefslogtreecommitdiff
path: root/bank/src/main/kotlin/tech/libeufin/bank/TalerMessage.kt
blob: b9bfcbec8ca1a731b21f4c0fe36b1257bfef85f2 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
/*
 * 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
 * <http://www.gnu.org/licenses/>
 */

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<out T> {
    data object None : Option<Nothing>()
    data class Some<T>(val value: T) : Option<T>()

    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<T> (
        private val valueSerializer: KSerializer<T>
    ) : KSerializer<Option<T>> {
        override val descriptor: SerialDescriptor = valueSerializer.descriptor

        override fun serialize(encoder: Encoder, value: Option<T>) {
            when (value) {
                None -> encoder.encodeNull()
                is Some -> valueSerializer.serialize(encoder, value.value)
            }
        }

        override fun deserialize(decoder: Decoder): Option<T> {
            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<String?> = Option.None,
    val phone: Option<String?> = 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<IbanPayto?> = Option.None,
    val name: String? = null,
    val is_public: Boolean? = null,
    val debit_threshold: TalerAmount? = null,
    val tan_channel: Option<TanChannel?> = 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<TanChannel>,
    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<AccountMinimalData>
)

/**
 * 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<BankAccountTransactionInfo>
)

// 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<String, String>
)


@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<String>,
    // 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<CashoutInfo>,
)

@Serializable
data class CashoutInfo(
    val cashout_id: Long,
    val status: CashoutStatus,
)


@Serializable
data class GlobalCashouts(
    val cashouts: List<GlobalCashoutInfo>,
)

@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<IncomingReserveTransaction>,
    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<OutgoingTransaction>,
    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<RevenueIncomingBankTransaction>,
    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<PublicAccount>
)

/**
 * 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
)