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 }