libeufin

Integration and sandbox testing for FinTech APIs and data formats
Log | Files | Refs | Submodules | README | LICENSE

ExchangeDAO.kt (9088B)


      1 /*
      2  * This file is part of LibEuFin.
      3  * Copyright (C) 2024, 2025, 2026 Taler Systems S.A.
      4 
      5  * LibEuFin is free software; you can redistribute it and/or modify
      6  * it under the terms of the GNU Affero General Public License as
      7  * published by the Free Software Foundation; either version 3, or
      8  * (at your option) any later version.
      9 
     10  * LibEuFin is distributed in the hope that it will be useful, but
     11  * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
     12  * or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Affero General
     13  * Public License for more details.
     14 
     15  * You should have received a copy of the GNU Affero General Public
     16  * License along with LibEuFin; see the file COPYING.  If not, see
     17  * <http://www.gnu.org/licenses/>
     18  */
     19 
     20 package tech.libeufin.nexus.db
     21 
     22 import tech.libeufin.common.*
     23 import tech.libeufin.common.db.*
     24 import java.time.Instant
     25 
     26 /** Data access logic for exchange specific logic */
     27 class ExchangeDAO(private val db: Database) {
     28     /** Query history of taler incoming transactions  */
     29     suspend fun incomingHistory(
     30         params: HistoryParams
     31     ): List<IncomingBankTransaction> 
     32         = db.poolHistoryGlobal(params, db::listenIncoming, """
     33             SELECT
     34                 incoming_transaction_id
     35                 ,execution_time
     36                 ,(amount).val AS amount_val
     37                 ,(amount).frac AS amount_frac
     38                 ,(credit_fee).val AS credit_fee_val
     39                 ,(credit_fee).frac AS credit_fee_frac
     40                 ,debit_payto
     41                 ,type
     42                 ,metadata
     43                 ,authorization_pub
     44                 ,authorization_sig
     45             FROM talerable_incoming_transactions
     46                 JOIN incoming_transactions USING(incoming_transaction_id)
     47             WHERE
     48         """, "incoming_transaction_id") {
     49             when (it.getEnum<IncomingType>("type")) {
     50                 IncomingType.reserve -> IncomingReserveTransaction(
     51                     row_id = it.getLong("incoming_transaction_id"),
     52                     date = it.getTalerTimestamp("execution_time"),
     53                     amount = it.getAmount("amount", db.currency),
     54                     credit_fee = it.getAmount("credit_fee", db.currency).notZeroOrNull(),
     55                     debit_account = it.getString("debit_payto"),
     56                     reserve_pub = EddsaPublicKey(it.getBytes("metadata")),
     57                     authorization_pub = it.getOptKey("authorization_pub"),
     58                     authorization_sig = it.getOptSig("authorization_sig")
     59                 )
     60                 IncomingType.kyc -> IncomingKycAuthTransaction(
     61                     row_id = it.getLong("incoming_transaction_id"),
     62                     date = it.getTalerTimestamp("execution_time"),
     63                     amount = it.getAmount("amount", db.currency),
     64                     credit_fee = it.getAmount("credit_fee", db.currency).notZeroOrNull(),
     65                     debit_account = it.getString("debit_payto"),
     66                     account_pub = EddsaPublicKey(it.getBytes("metadata")),
     67                     authorization_pub = it.getOptKey("authorization_pub"),
     68                     authorization_sig = it.getOptSig("authorization_sig")
     69                 )
     70                 IncomingType.map -> throw UnsupportedOperationException()
     71             }
     72         }
     73 
     74     /** Query exchange history of taler outgoing transactions  */
     75     suspend fun outgoingHistory(
     76         params: HistoryParams
     77     ): List<OutgoingTransaction>
     78         = db.poolHistoryGlobal(params, db::listenOutgoing,  """
     79             SELECT
     80                 outgoing_transaction_id
     81                 ,execution_time
     82                 ,(amount).val AS amount_val
     83                 ,(amount).frac AS amount_frac
     84                 ,(debit_fee).val AS debit_fee_val
     85                 ,(debit_fee).frac AS debit_fee_frac
     86                 ,credit_payto
     87                 ,wtid
     88                 ,exchange_base_url
     89                 ,metadata
     90             FROM talerable_outgoing_transactions
     91                 JOIN outgoing_transactions USING(outgoing_transaction_id)
     92             WHERE
     93         """, "outgoing_transaction_id") {
     94             OutgoingTransaction(
     95                 row_id = it.getLong("outgoing_transaction_id"),
     96                 date = it.getTalerTimestamp("execution_time"),
     97                 amount = it.getAmount("amount", db.currency),
     98                 debit_fee = it.getAmount("debit_fee", db.currency).notZeroOrNull(),
     99                 credit_account = it.getString("credit_payto"),
    100                 wtid = ShortHashCode(it.getBytes("wtid")),
    101                 exchange_base_url = it.getString("exchange_base_url"),
    102                 metadata = it.getString("metadata"),
    103             )
    104         }
    105 
    106     /** Result of taler transfer transaction creation */
    107     sealed interface TransferResult {
    108         /** Transaction [id] and wire transfer [timestamp] */
    109         data class Success(val id: Long, val timestamp: TalerTimestamp): TransferResult
    110         data object RequestUidReuse: TransferResult
    111         data object WtidReuse: TransferResult
    112     }
    113 
    114     /** Perform a Taler transfer */
    115     suspend fun transfer(
    116         req: TransferRequest,
    117         endToEndId: String,
    118         timestamp: Instant
    119     ): TransferResult = db.serializable(
    120         """
    121         SELECT
    122             out_request_uid_reuse
    123             ,out_wtid_reuse
    124             ,out_tx_row_id
    125             ,out_timestamp
    126         FROM taler_transfer (
    127             ?, ?, ?,
    128             (?,?)::taler_amount,
    129             ?, ?, ?, ?, ?
    130         )
    131         """
    132     ) {
    133         val subject = fmtOutgoingSubject(req.wtid, req.exchange_base_url, req.metadata)
    134 
    135         bind(req.request_uid)
    136         bind(req.wtid)
    137         bind(subject)
    138         bind(req.amount)
    139         bind(req.exchange_base_url.toString())
    140         bind(req.metadata)
    141         bind(req.credit_account.toString())
    142         bind(endToEndId)
    143         bind(timestamp)
    144         one {
    145             when {
    146                 it.getBoolean("out_request_uid_reuse") -> TransferResult.RequestUidReuse
    147                 it.getBoolean("out_wtid_reuse") -> TransferResult.WtidReuse
    148                 else -> TransferResult.Success(
    149                     id = it.getLong("out_tx_row_id"),
    150                     timestamp = it.getTalerTimestamp("out_timestamp")
    151                 )
    152             }
    153         }
    154     }
    155 
    156     /** Get status of transfer [id] */
    157     suspend fun getTransfer(
    158         id: Long
    159     ): TransferStatus? = db.serializable(
    160         """
    161         SELECT
    162             wtid
    163             ,exchange_base_url
    164             ,metadata
    165             ,(amount).val AS amount_val
    166             ,(amount).frac AS amount_frac
    167             ,credit_payto
    168             ,initiation_time
    169             ,status
    170             ,status_msg
    171         FROM transfer_operations
    172             JOIN initiated_outgoing_transactions USING (initiated_outgoing_transaction_id)
    173         WHERE initiated_outgoing_transaction_id=?
    174         """
    175     ) {
    176         bind(id)
    177         oneOrNull {
    178             TransferStatus(
    179                 status = it.getEnum<SubmissionState>("status").toTransferStatus(),
    180                 status_msg = it.getString("status_msg"),
    181                 amount = it.getAmount("amount", db.currency),
    182                 origin_exchange_url = it.getString("exchange_base_url"),
    183                 metadata = it.getString("metadata"),
    184                 wtid = ShortHashCode(it.getBytes("wtid")),
    185                 credit_account = it.getString("credit_payto"),
    186                 timestamp = it.getTalerTimestamp("initiation_time"),
    187             )
    188         }
    189     }
    190 
    191     /** Get a page of transfers status */
    192     suspend fun pageTransfer(
    193         params: TransferParams
    194     ): List<TransferListStatus> = db.page(
    195         params.page,
    196         "initiated_outgoing_transaction_id",
    197         """
    198         SELECT
    199             initiated_outgoing_transaction_id
    200             ,(amount).val AS amount_val
    201             ,(amount).frac AS amount_frac
    202             ,status
    203             ,credit_payto
    204             ,initiation_time
    205         FROM transfer_operations
    206             JOIN initiated_outgoing_transactions USING (initiated_outgoing_transaction_id)
    207         WHERE ${
    208             when (params.status) {
    209                 null -> ""
    210                 TransferStatusState.pending -> "(status=?::submission_state OR status=?::submission_state) AND"
    211                 else -> "status=?::submission_state AND"
    212             }               
    213         }
    214         """,
    215         {
    216             when (params.status) {
    217                 null -> {}
    218                 TransferStatusState.pending -> {
    219                     bind(SubmissionState.pending)
    220                     bind(SubmissionState.unsubmitted)
    221                 }
    222                 else -> {
    223                     bind(params.status)
    224                 }
    225             }
    226         }
    227     ) {
    228         TransferListStatus(
    229             row_id = it.getLong("initiated_outgoing_transaction_id"),
    230             status = it.getEnum<SubmissionState>("status").toTransferStatus(),
    231             amount = it.getAmount("amount", db.currency),
    232             credit_account = it.getString("credit_payto"),
    233             timestamp = it.getTalerTimestamp("initiation_time"),
    234         )
    235     }
    236 }