[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]
[libeufin] branch master updated (973c88cc -> 46e4838f)
From: |
gnunet |
Subject: |
[libeufin] branch master updated (973c88cc -> 46e4838f) |
Date: |
Fri, 29 Sep 2023 09:55:11 +0200 |
This is an automated email from the git hooks/post-receive script.
ms pushed a change to branch master
in repository libeufin.
from 973c88cc support DESTDIR
new a6350237 Time types handling.
new cbdabfad comments
new 46e4838f Stop using longs to manipulate time.
The 3 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails. The revisions
listed as "add" were already present in the repository and have only
been added to this reference.
Summary of changes:
.../main/kotlin/tech/libeufin/bank/BankMessages.kt | 31 +++---
.../tech/libeufin/bank/CorebankApiHandlers.kt | 49 ++++++----
.../src/main/kotlin/tech/libeufin/bank/Database.kt | 95 ++++++++++++++----
bank/src/main/kotlin/tech/libeufin/bank/Main.kt | 107 ++++++++++++++++++---
.../tech/libeufin/bank/WireGatewayApiHandlers.kt | 12 +--
bank/src/main/kotlin/tech/libeufin/bank/helpers.kt | 5 +-
bank/src/test/kotlin/DatabaseTest.kt | 20 ++--
bank/src/test/kotlin/JsonTest.kt | 44 ++++-----
bank/src/test/kotlin/LibeuFinApiTest.kt | 36 ++++++-
util/src/main/kotlin/Config.kt | 2 +
util/src/main/kotlin/DB.kt | 2 -
util/src/main/kotlin/Ebics.kt | 2 -
util/src/main/kotlin/time.kt | 55 ++++++++++-
13 files changed, 340 insertions(+), 120 deletions(-)
diff --git a/bank/src/main/kotlin/tech/libeufin/bank/BankMessages.kt
b/bank/src/main/kotlin/tech/libeufin/bank/BankMessages.kt
index d0807f95..bf400026 100644
--- a/bank/src/main/kotlin/tech/libeufin/bank/BankMessages.kt
+++ b/bank/src/main/kotlin/tech/libeufin/bank/BankMessages.kt
@@ -21,10 +21,11 @@ package tech.libeufin.bank
import io.ktor.http.*
import io.ktor.server.application.*
-import kotlinx.serialization.Contextual
import kotlinx.serialization.Serializable
+import java.time.Duration
+import java.time.Instant
+import java.time.temporal.ChronoUnit
import java.util.*
-import kotlin.reflect.jvm.internal.impl.types.AbstractStubType
/**
* Allowed lengths for fractional digits in amounts.
@@ -38,13 +39,15 @@ enum class FracDigits(howMany: Int) {
/**
* Timestamp containing the number of seconds since epoch.
*/
-@Serializable
+@Serializable(with = TalerProtocolTimestampSerializer::class)
data class TalerProtocolTimestamp(
- val t_s: Long, // FIXME (?): not supporting "never" at the moment.
+ val t_s: Instant,
) {
companion object {
fun fromMicroseconds(uSec: Long): TalerProtocolTimestamp {
- return TalerProtocolTimestamp(uSec / 1000000)
+ return TalerProtocolTimestamp(
+ Instant.EPOCH.plus(uSec, ChronoUnit.MICROS)
+ )
}
}
}
@@ -101,8 +104,9 @@ data class RegisterAccountRequest(
* Internal representation of relative times. The
* "forever" case is represented with Long.MAX_VALUE.
*/
+@Serializable(with = RelativeTimeSerializer::class)
data class RelativeTime(
- val d_us: Long
+ val d_us: Duration
)
/**
@@ -112,7 +116,6 @@ data class RelativeTime(
@Serializable
data class TokenRequest(
val scope: TokenScope,
- @Contextual
val duration: RelativeTime? = null,
val refreshable: Boolean = false
)
@@ -169,6 +172,7 @@ data class Customer(
* maybeCurrency is typically null when the TalerAmount object gets
* defined by the Database class.
*/
+@Serializable(with = TalerAmountSerializer::class)
class TalerAmount(
val value: Long,
val frac: Int,
@@ -241,8 +245,8 @@ data class BearerToken(
val content: ByteArray,
val scope: TokenScope,
val isRefreshable: Boolean = false,
- val creationTime: Long,
- val expirationTime: Long,
+ val creationTime: Instant,
+ val expirationTime: Instant,
/**
* Serial ID of the database row that hosts the bank customer
* that is associated with this token. NOTE: if the token is
@@ -266,7 +270,7 @@ data class BankInternalTransaction(
val debtorAccountId: Long,
val subject: String,
val amount: TalerAmount,
- val transactionDate: Long,
+ val transactionDate: Instant,
val accountServicerReference: String = "not used", // ISO20022
val endToEndId: String = "not used", // ISO20022
val paymentInformationId: String = "not used" // ISO20022
@@ -284,7 +288,7 @@ data class BankAccountTransaction(
val debtorName: String,
val subject: String,
val amount: TalerAmount,
- val transactionDate: Long, // microseconds
+ val transactionDate: Instant,
/**
* Is the transaction debit, or credit for the
* bank account pointed by this object?
@@ -334,8 +338,8 @@ data class Cashout(
val sellAtRatio: Int,
val sellOutFee: TalerAmount,
val subject: String,
- val creationTime: Long,
- val tanConfirmationTime: Long? = null,
+ val creationTime: Instant,
+ val tanConfirmationTime: Instant? = null,
val tanChannel: TanChannel,
val tanCode: String,
val bankAccount: Long,
@@ -592,7 +596,6 @@ data class IncomingReserveTransaction(
@Serializable
data class TransferRequest(
val request_uid: String,
- @Contextual
val amount: TalerAmount,
val exchange_base_url: String,
val wtid: String,
diff --git a/bank/src/main/kotlin/tech/libeufin/bank/CorebankApiHandlers.kt
b/bank/src/main/kotlin/tech/libeufin/bank/CorebankApiHandlers.kt
index a523b291..f4286e58 100644
--- a/bank/src/main/kotlin/tech/libeufin/bank/CorebankApiHandlers.kt
+++ b/bank/src/main/kotlin/tech/libeufin/bank/CorebankApiHandlers.kt
@@ -10,6 +10,9 @@ import net.taler.wallet.crypto.Base32Crockford
import org.slf4j.Logger
import org.slf4j.LoggerFactory
import tech.libeufin.util.*
+import java.time.Duration
+import java.time.Instant
+import java.time.temporal.ChronoUnit
import java.util.*
private val logger: Logger =
LoggerFactory.getLogger("tech.libeufin.bank.accountsMgmtHandlers")
@@ -50,29 +53,36 @@ fun Routing.accountsMgmtHandlers(db: Database, ctx:
BankApplicationContext) {
val tokenBytes = ByteArray(32).apply {
Random().nextBytes(this)
}
- val tokenDurationUs = req.duration?.d_us ?: TOKEN_DEFAULT_DURATION_US
+ val tokenDuration: Duration = req.duration?.d_us ?:
TOKEN_DEFAULT_DURATION
+
+ val creationTime = Instant.now()
+ val expirationTimestamp = if (tokenDuration ==
ChronoUnit.FOREVER.duration) {
+ Instant.MAX
+ } else {
+ try {
+ creationTime.plus(tokenDuration)
+ } catch (e: Exception) {
+ logger.error("Could not add token duration to current time:
${e.message}")
+ throw badRequest("Bad token duration: ${e.message}")
+ }
+ }
val customerDbRow = customer.dbRowId ?: throw internalServerError(
"Could not get customer '${customer.login}' database row ID"
)
- val creationTime = getNowUs()
- val expirationTimestampUs: Long = creationTime + tokenDurationUs
- if (expirationTimestampUs < tokenDurationUs) throw badRequest(
- "Token duration caused arithmetic overflow", // FIXME: need
dedicate EC (?)
- talerErrorCode = TalerErrorCode.TALER_EC_END
- )
val token = BearerToken(
bankCustomer = customerDbRow,
content = tokenBytes,
creationTime = creationTime,
- expirationTime = expirationTimestampUs,
+ expirationTime = expirationTimestamp,
scope = req.scope,
isRefreshable = req.refreshable
)
- if (!db.bearerTokenCreate(token)) throw internalServerError("Failed at
inserting new token in the database")
+ if (!db.bearerTokenCreate(token))
+ throw internalServerError("Failed at inserting new token in the
database")
call.respond(
TokenSuccessResponse(
access_token = Base32Crockford.encode(tokenBytes), expiration
= TalerProtocolTimestamp(
- t_s = expirationTimestampUs / 1000000L
+ t_s = expirationTimestamp
)
)
)
@@ -167,7 +177,7 @@ fun Routing.accountsMgmtHandlers(db: Database, ctx:
BankApplicationContext) {
debtorAccountId = adminBankAccount.expectRowId(),
amount = bonusAmount,
subject = "Registration bonus.",
- transactionDate = getNowUs()
+ transactionDate = Instant.now()
)
when (db.bankTransactionCreate(adminPaysBonus)) {
Database.BankTransactionResult.NO_CREDITOR -> throw
internalServerError("Bonus impossible: creditor not found, despite its recent
creation.")
@@ -294,11 +304,12 @@ fun Routing.accountsMgmtHandlers(db: Database, ctx:
BankApplicationContext) {
// to the selected state _and_ wire the funds to the exchange.
// Note: 'when' helps not to omit more result codes, should more
// be added.
- when (db.talerWithdrawalConfirm(op.withdrawalUuid, getNowUs())) {
- WithdrawalConfirmationResult.BALANCE_INSUFFICIENT -> throw
conflict(
- "Insufficient funds", TalerErrorCode.TALER_EC_END // FIXME:
define EC for this.
- )
-
+ when (db.talerWithdrawalConfirm(op.withdrawalUuid, Instant.now())) {
+ WithdrawalConfirmationResult.BALANCE_INSUFFICIENT ->
+ throw conflict(
+ "Insufficient funds",
+ TalerErrorCode.TALER_EC_END // FIXME: define EC for this.
+ )
WithdrawalConfirmationResult.OP_NOT_FOUND ->
/**
* Despite previous checks, the database _still_ did not
@@ -345,7 +356,7 @@ fun Routing.accountsMgmtHandlers(db: Database, ctx:
BankApplicationContext) {
subject = it.subject,
amount = it.amount.toString(),
direction = it.direction,
- date =
TalerProtocolTimestamp.fromMicroseconds(it.transactionDate),
+ date = TalerProtocolTimestamp(it.transactionDate),
row_id = it.dbRowId ?: throw internalServerError(
"Transaction timestamped with '${it.transactionDate}'
did not have row ID"
)
@@ -380,7 +391,7 @@ fun Routing.accountsMgmtHandlers(db: Database, ctx:
BankApplicationContext) {
creditorAccountId = creditorCustomerData.owningCustomerId,
subject = subject,
amount = amount,
- transactionDate = getNowUs()
+ transactionDate = Instant.now()
)
val res = db.bankTransactionCreate(dbInstructions)
when (res) {
@@ -420,7 +431,7 @@ fun Routing.accountsMgmtHandlers(db: Database, ctx:
BankApplicationContext) {
amount =
"${tx.amount.currency}:${tx.amount.value}.${tx.amount.frac}",
creditor_payto_uri = tx.creditorPaytoUri,
debtor_payto_uri = tx.debtorPaytoUri,
- date =
TalerProtocolTimestamp.fromMicroseconds(tx.transactionDate),
+ date = TalerProtocolTimestamp(tx.transactionDate),
direction = tx.direction,
subject = tx.subject,
row_id = txRowId
diff --git a/bank/src/main/kotlin/tech/libeufin/bank/Database.kt
b/bank/src/main/kotlin/tech/libeufin/bank/Database.kt
index 110f3a1e..c5f06190 100644
--- a/bank/src/main/kotlin/tech/libeufin/bank/Database.kt
+++ b/bank/src/main/kotlin/tech/libeufin/bank/Database.kt
@@ -24,11 +24,14 @@ import org.postgresql.jdbc.PgConnection
import org.slf4j.Logger
import org.slf4j.LoggerFactory
import tech.libeufin.util.getJdbcConnectionFromPg
+import tech.libeufin.util.microsToJavaInstant
+import tech.libeufin.util.toDbMicros
import java.io.File
import java.sql.DriverManager
import java.sql.PreparedStatement
import java.sql.ResultSet
import java.sql.SQLException
+import java.time.Instant
import java.util.*
import kotlin.math.abs
@@ -41,6 +44,25 @@ fun BankAccountTransaction.expectRowId(): Long =
this.dbRowId ?: throw internalS
private val logger: Logger =
LoggerFactory.getLogger("tech.libeufin.bank.Database")
+/**
+ * This error occurs in case the timestamp took by the bank for some
+ * event could not be converted in microseconds. Note: timestamp are
+ * taken via the Instant.now(), then converted to nanos, and then divided
+ * by 1000 to obtain the micros.
+ *
+ * It could be that a legitimate timestamp overflows in the process of
+ * being converted to micros - as described above. In the case of a timestamp,
+ * the fault lies to the bank, because legitimate timestamps must (at the
+ * time of writing!) go through the conversion to micros.
+ *
+ * On the other hand (and for the sake of completeness), in the case of a
+ * timestamp that was calculated after a client-submitted duration, the
overflow
+ * lies to the client, because they must have specified a gigantic amount of
time
+ * that overflew the conversion to micros and should simply have specified
"forever".
+ */
+private fun faultyTimestampByBank() = internalServerError("Bank took
overflowing timestamp")
+private fun faultyDurationByClient() = badRequest("Overflowing duration,
please specify 'forever' instead.")
+
fun initializeDatabaseTables(dbConfig: String, sqlDir: String) {
logger.info("doing DB initialization, sqldir $sqlDir, dbConfig $dbConfig")
val jdbcConnStr = getJdbcConnectionFromPg(dbConfig)
@@ -299,8 +321,8 @@ class Database(private val dbConfig: String, private val
bankCurrency: String) {
(?, ?, ?, ?::token_scope_enum, ?, ?)
""")
stmt.setBytes(1, token.content)
- stmt.setLong(2, token.creationTime)
- stmt.setLong(3, token.expirationTime)
+ stmt.setLong(2, token.creationTime.toDbMicros() ?: throw
faultyTimestampByBank())
+ stmt.setLong(3, token.expirationTime.toDbMicros() ?: throw
faultyDurationByClient())
stmt.setString(4, token.scope.name)
stmt.setLong(5, token.bankCustomer)
stmt.setBoolean(6, token.isRefreshable)
@@ -318,14 +340,13 @@ class Database(private val dbConfig: String, private val
bankCurrency: String) {
FROM bearer_tokens
WHERE content=?;
""")
-
stmt.setBytes(1, token)
stmt.executeQuery().use {
if (!it.next()) return null
return BearerToken(
content = token,
- creationTime = it.getLong("creation_time"),
- expirationTime = it.getLong("expiration_time"),
+ creationTime =
it.getLong("creation_time").microsToJavaInstant() ?: throw
faultyTimestampByBank(),
+ expirationTime =
it.getLong("expiration_time").microsToJavaInstant() ?: throw
faultyDurationByClient(),
bankCustomer = it.getLong("bank_customer"),
scope = it.getString("scope").run {
if (this == TokenScope.readwrite.name) return@run
TokenScope.readwrite
@@ -512,7 +533,7 @@ class Database(private val dbConfig: String, private val
bankCurrency: String) {
stmt.setString(3, tx.subject)
stmt.setLong(4, tx.amount.value)
stmt.setInt(5, tx.amount.frac)
- stmt.setLong(6, tx.transactionDate)
+ stmt.setLong(6, tx.transactionDate.toDbMicros() ?: throw
faultyTimestampByBank())
stmt.setString(7, tx.accountServicerReference)
stmt.setString(8, tx.paymentInformationId)
stmt.setString(9, tx.endToEndId)
@@ -604,7 +625,7 @@ class Database(private val dbConfig: String, private val
bankCurrency: String) {
bankAccountId = it.getLong("bank_account_id"),
paymentInformationId = it.getString("payment_information_id"),
subject = it.getString("subject"),
- transactionDate = it.getLong("transaction_date")
+ transactionDate =
it.getLong("transaction_date").microsToJavaInstant() ?: throw
faultyTimestampByBank()
)
}
}
@@ -700,7 +721,7 @@ class Database(private val dbConfig: String, private val
bankCurrency: String) {
bankAccountId = it.getLong("bank_account_id"),
paymentInformationId =
it.getString("payment_information_id"),
subject = it.getString("subject"),
- transactionDate = it.getLong("transaction_date"),
+ transactionDate =
it.getLong("transaction_date").microsToJavaInstant() ?: throw
faultyTimestampByBank(),
dbRowId = it.getLong("bank_transaction_id")
))
} while (it.next())
@@ -811,11 +832,12 @@ class Database(private val dbConfig: String, private val
bankCurrency: String) {
}
/**
- *
+ * Confirms a Taler withdrawal: flags the operation as
+ * confirmed and performs the related wire transfer.
*/
fun talerWithdrawalConfirm(
opUuid: UUID,
- timestamp: Long,
+ timestamp: Instant,
accountServicerReference: String = "NOT-USED",
endToEndId: String = "NOT-USED",
paymentInfId: String = "NOT-USED"
@@ -831,7 +853,7 @@ class Database(private val dbConfig: String, private val
bankCurrency: String) {
"""
)
stmt.setObject(1, opUuid)
- stmt.setLong(2, timestamp)
+ stmt.setLong(2, timestamp.toDbMicros() ?: throw
faultyTimestampByBank())
stmt.setString(3, accountServicerReference)
stmt.setString(4, endToEndId)
stmt.setString(5, paymentInfId)
@@ -847,6 +869,9 @@ class Database(private val dbConfig: String, private val
bankCurrency: String) {
return WithdrawalConfirmationResult.SUCCESS
}
+ /**
+ * Creates a cashout operation in the database.
+ */
fun cashoutCreate(op: Cashout): Boolean {
reconnect()
val stmt = prepare("""
@@ -895,7 +920,7 @@ class Database(private val dbConfig: String, private val
bankCurrency: String) {
stmt.setLong(10, op.sellOutFee.value)
stmt.setInt(11, op.sellOutFee.frac)
stmt.setString(12, op.subject)
- stmt.setLong(13, op.creationTime)
+ stmt.setLong(13, op.creationTime.toDbMicros() ?: throw
faultyTimestampByBank())
stmt.setString(14, op.tanChannel.name)
stmt.setString(15, op.tanCode)
stmt.setLong(16, op.bankAccount)
@@ -904,6 +929,11 @@ class Database(private val dbConfig: String, private val
bankCurrency: String) {
return myExecute(stmt)
}
+ /**
+ * Flags one cashout operation as confirmed. The backing
+ * payment should already have taken place, before calling
+ * this function.
+ */
fun cashoutConfirm(
opUuid: UUID,
tanConfirmationTimestamp: Long,
@@ -920,11 +950,18 @@ class Database(private val dbConfig: String, private val
bankCurrency: String) {
stmt.setObject(3, opUuid)
return myExecute(stmt)
}
- // used by /abort
+
+ /**
+ * This type is used by the cashout /abort handler.
+ */
enum class CashoutDeleteResult {
SUCCESS,
CONFLICT_ALREADY_CONFIRMED
}
+
+ /**
+ * Deletes a cashout operation from the database.
+ */
fun cashoutDelete(opUuid: UUID): CashoutDeleteResult {
val stmt = prepare("""
SELECT out_already_confirmed
@@ -939,6 +976,11 @@ class Database(private val dbConfig: String, private val
bankCurrency: String) {
return CashoutDeleteResult.SUCCESS
}
}
+
+ /**
+ * Gets a cashout operation from the database, according
+ * to its uuid.
+ */
fun cashoutGetFromUuid(opUuid: UUID): Cashout? {
val stmt = prepare("""
SELECT
@@ -988,7 +1030,7 @@ class Database(private val dbConfig: String, private val
bankCurrency: String) {
credit_payto_uri = it.getString("credit_payto_uri"),
cashoutCurrency = it.getString("cashout_currency"),
cashoutUuid = opUuid,
- creationTime = it.getLong("creation_time"),
+ creationTime =
it.getLong("creation_time").microsToJavaInstant() ?: throw
faultyTimestampByBank(),
sellAtRatio = it.getInt("sell_at_ratio"),
sellOutFee = TalerAmount(
value = it.getLong("sell_out_fee_val"),
@@ -1008,13 +1050,17 @@ class Database(private val dbConfig: String, private
val bankCurrency: String) {
localTransaction = it.getLong("local_transaction"),
tanConfirmationTime = it.getLong("tan_confirmation_time").run {
if (this == 0L) return@run null
- return@run this
+ return@run this.microsToJavaInstant() ?: throw
faultyTimestampByBank()
}
)
}
}
+
+ /**
+ * Represents the database row related to one payment
+ * that was requested by the Taler exchange.
+ */
data class TalerTransferFromDb(
- // Only used when this type if defined from a DB record
val timestamp: Long,
val debitTxRowId: Long,
val requestUid: String,
@@ -1023,7 +1069,9 @@ class Database(private val dbConfig: String, private val
bankCurrency: String) {
val wtid: String,
val creditAccount: String
)
- // Gets a Taler transfer request, given its UID.
+ /**
+ * Gets a Taler transfer request, given its UID.
+ */
fun talerTransferGetFromUid(requestUid: String): TalerTransferFromDb? {
reconnect()
val stmt = prepare("""
@@ -1060,10 +1108,15 @@ class Database(private val dbConfig: String, private
val bankCurrency: String) {
}
}
+ /**
+ * Holds the result of inserting a Taler transfer request
+ * into the database.
+ */
data class TalerTransferCreationResult(
val txResult: BankTransactionResult,
- // Row ID of the debit bank transaction
- // of a successful case. Null upon errors
+ /**
+ * bank transaction that backs this Taler transfer request.
+ */
val txRowId: Long? = null
)
/**
@@ -1080,7 +1133,7 @@ class Database(private val dbConfig: String, private val
bankCurrency: String) {
fun talerTransferCreate(
req: TransferRequest,
exchangeBankAccountId: Long,
- timestamp: Long,
+ timestamp: Instant,
acctSvcrRef: String = "not used",
pmtInfId: String = "not used",
endToEndId: String = "not used",
@@ -1113,7 +1166,7 @@ class Database(private val dbConfig: String, private val
bankCurrency: String) {
stmt.setString(5, req.exchange_base_url)
stmt.setString(6, req.credit_account)
stmt.setLong(7, exchangeBankAccountId)
- stmt.setLong(8, timestamp)
+ stmt.setLong(8, timestamp.toDbMicros() ?: throw
faultyTimestampByBank())
stmt.setString(9, acctSvcrRef)
stmt.setString(10, pmtInfId)
stmt.setString(11, endToEndId)
diff --git a/bank/src/main/kotlin/tech/libeufin/bank/Main.kt
b/bank/src/main/kotlin/tech/libeufin/bank/Main.kt
index c556b0f2..a8ef3e81 100644
--- a/bank/src/main/kotlin/tech/libeufin/bank/Main.kt
+++ b/bank/src/main/kotlin/tech/libeufin/bank/Main.kt
@@ -51,21 +51,23 @@ import kotlinx.coroutines.withContext
import kotlinx.serialization.descriptors.*
import kotlinx.serialization.encoding.Decoder
import kotlinx.serialization.encoding.Encoder
+import kotlinx.serialization.encoding.encodeStructure
import kotlinx.serialization.json.*
-import kotlinx.serialization.modules.SerializersModule
import net.taler.common.errorcodes.TalerErrorCode
import org.slf4j.Logger
import org.slf4j.LoggerFactory
import org.slf4j.event.Level
import tech.libeufin.util.*
import java.time.Duration
+import java.time.Instant
+import java.time.temporal.ChronoUnit
import java.util.zip.InflaterInputStream
import kotlin.system.exitProcess
// GLOBALS
private val logger: Logger = LoggerFactory.getLogger("tech.libeufin.bank.Main")
const val GENERIC_UNDEFINED = -1 // Filler for ECs that don't exist yet.
-val TOKEN_DEFAULT_DURATION_US = Duration.ofDays(1L).seconds * 1000000
+val TOKEN_DEFAULT_DURATION: java.time.Duration = Duration.ofDays(1L)
/**
@@ -115,6 +117,58 @@ data class BankApplicationContext(
val spaCaptchaURL: String?,
)
+
+/**
+ * This custom (de)serializer interprets the Timestamp JSON
+ * type of the Taler common API. In particular, it is responsible
+ * for _serializing_ timestamps, as this datatype is so far
+ * only used to respond to clients.
+ */
+object TalerProtocolTimestampSerializer : KSerializer<TalerProtocolTimestamp> {
+ override fun serialize(encoder: Encoder, value: TalerProtocolTimestamp) {
+ // Thanks:
https://github.com/Kotlin/kotlinx.serialization/blob/master/docs/serializers.md#hand-written-composite-serializer
+ encoder.encodeStructure(descriptor) {
+ if (value.t_s == Instant.MAX) {
+ encodeStringElement(descriptor, 0, "never")
+ return@encodeStructure
+ }
+ encodeLongElement(descriptor, 0, value.t_s.epochSecond)
+ }
+ }
+
+ override fun deserialize(decoder: Decoder): TalerProtocolTimestamp {
+ val jsonInput = decoder as? JsonDecoder ?: throw
internalServerError("TalerProtocolTimestamp had no JsonDecoder")
+ val json = try {
+ jsonInput.decodeJsonElement().jsonObject
+ } catch (e: Exception) {
+ throw badRequest(
+ "Did not find a JSON object for TalerProtocolTimestamp:
${e.message}",
+ TalerErrorCode.TALER_EC_GENERIC_JSON_INVALID
+ )
+ }
+ val maybeTs = json["t_s"]?.jsonPrimitive ?: throw badRequest("Taler
timestamp invalid: t_s field not found")
+ if (maybeTs.isString) {
+ if (maybeTs.content != "never") throw badRequest("Only 'never'
allowed for t_s as string, but '${maybeTs.content}' was found")
+ return TalerProtocolTimestamp(t_s = Instant.MAX)
+ }
+ val ts: Long = maybeTs.longOrNull
+ ?: throw badRequest("Could not convert t_s '${maybeTs.content}' to
a number")
+ val instant = try {
+ Instant.ofEpochSecond(ts)
+ } catch (e: Exception) {
+ logger.error("Could not get Instant from t_s: $ts: ${e.message}")
+ // Bank's fault. API doesn't allow clients to pass this datatype.
+ throw internalServerError("Could not serialize this t_s: ${ts}")
+ }
+ return TalerProtocolTimestamp(instant)
+ }
+
+ override val descriptor: SerialDescriptor =
+ buildClassSerialDescriptor("TalerProtocolTimestamp") {
+ element<JsonElement>("t_s")
+ }
+}
+
/**
* This custom (de)serializer interprets the RelativeTime JSON
* type. In particular, it is responsible for converting the
@@ -122,10 +176,30 @@ data class BankApplicationContext(
* is passed as is.
*/
object RelativeTimeSerializer : KSerializer<RelativeTime> {
+ /**
+ * Internal representation to JSON.
+ */
override fun serialize(encoder: Encoder, value: RelativeTime) {
- throw internalServerError("Encoding of RelativeTime not implemented.")
// API doesn't require this.
+ // Thanks:
https://github.com/Kotlin/kotlinx.serialization/blob/master/docs/serializers.md#hand-written-composite-serializer
+ encoder.encodeStructure(descriptor) {
+ if (value.d_us == ChronoUnit.FOREVER.duration) {
+ encodeStringElement(descriptor, 0, "forever")
+ return@encodeStructure
+ }
+ val dUs = try {
+ value.d_us.toNanos()
+ } catch (e: Exception) {
+ logger.error(e.message)
+ // Bank's fault, as each numeric value should be checked
before entering the system.
+ throw internalServerError("Could not convert
java.time.Duration to JSON")
+ }
+ encodeLongElement(descriptor, 0, dUs / 1000L)
+ }
}
+ /**
+ * JSON to internal representation.
+ */
override fun deserialize(decoder: Decoder): RelativeTime {
val jsonInput = decoder as? JsonDecoder ?: throw
internalServerError("RelativeTime had no JsonDecoder")
val json = try {
@@ -139,15 +213,22 @@ object RelativeTimeSerializer : KSerializer<RelativeTime>
{
val maybeDUs = json["d_us"]?.jsonPrimitive ?: throw
badRequest("Relative time invalid: d_us field not found")
if (maybeDUs.isString) {
if (maybeDUs.content != "forever") throw badRequest("Only
'forever' allowed for d_us as string, but '${maybeDUs.content}' was found")
- return RelativeTime(d_us = Long.MAX_VALUE)
+ return RelativeTime(d_us = ChronoUnit.FOREVER.duration)
+ }
+ val dUs: Long = maybeDUs.longOrNull
+ ?: throw badRequest("Could not convert d_us: '${maybeDUs.content}'
to a number")
+ val duration = try {
+ Duration.ofNanos(dUs * 1000L)
+ } catch (e: Exception) {
+ logger.error("Could not get Duration out of d_us content: ${dUs}.
${e.message}")
+ throw badRequest("Could not get Duration out of d_us content:
${dUs}")
}
- val dUs: Long =
- maybeDUs.longOrNull ?: throw badRequest("Could not convert d_us:
'${maybeDUs.content}' to a number")
- return RelativeTime(d_us = dUs)
+ return RelativeTime(d_us = duration)
}
override val descriptor: SerialDescriptor =
buildClassSerialDescriptor("RelativeTime") {
+ // JsonElement helps to obtain "union" type Long|String
element<JsonElement>("d_us")
}
}
@@ -232,15 +313,6 @@ fun Application.corebankWebApp(db: Database, ctx:
BankApplicationContext) {
encodeDefaults = true
prettyPrint = true
ignoreUnknownKeys = true
- // Registering custom parser for RelativeTime
- serializersModule = SerializersModule {
- contextual(RelativeTime::class) {
- RelativeTimeSerializer
- }
- contextual(TalerAmount::class) {
- TalerAmountSerializer
- }
- }
})
}
install(RequestValidation)
@@ -295,6 +367,9 @@ fun Application.corebankWebApp(db: Database, ctx:
BankApplicationContext) {
*/
exception<LibeufinBankException> { call, cause ->
logger.error(cause.talerError.hint)
+ // Stacktrace if bank's fault
+ if (cause.httpStatus.toString().startsWith('5'))
+ cause.printStackTrace()
call.respond(
status = cause.httpStatus,
message = cause.talerError
diff --git a/bank/src/main/kotlin/tech/libeufin/bank/WireGatewayApiHandlers.kt
b/bank/src/main/kotlin/tech/libeufin/bank/WireGatewayApiHandlers.kt
index 9c5cd9af..b040883d 100644
--- a/bank/src/main/kotlin/tech/libeufin/bank/WireGatewayApiHandlers.kt
+++ b/bank/src/main/kotlin/tech/libeufin/bank/WireGatewayApiHandlers.kt
@@ -27,7 +27,7 @@ import io.ktor.server.request.*
import io.ktor.server.response.*
import io.ktor.server.routing.*
import net.taler.common.errorcodes.TalerErrorCode
-import tech.libeufin.util.getNowUs
+import java.time.Instant
fun Routing.talerWireGatewayHandlers(db: Database, ctx:
BankApplicationContext) {
get("/taler-wire-gateway/config") {
@@ -60,7 +60,7 @@ fun Routing.talerWireGatewayHandlers(db: Database, ctx:
BankApplicationContext)
IncomingReserveTransaction(
row_id = it.expectRowId(),
amount = it.amount.toString(),
- date =
TalerProtocolTimestamp.fromMicroseconds(it.transactionDate),
+ date = TalerProtocolTimestamp(it.transactionDate),
debit_account = it.debtorPaytoUri,
reserve_pub = it.subject
)
@@ -102,7 +102,7 @@ fun Routing.talerWireGatewayHandlers(db: Database, ctx:
BankApplicationContext)
throw badRequest("Currency mismatch: $internalCurrency vs
${req.amount.currency}")
val exchangeBankAccount = db.bankAccountGetFromOwnerId(c.expectRowId())
?: throw internalServerError("Exchange does not have a bank
account")
- val transferTimestamp = getNowUs()
+ val transferTimestamp = Instant.now()
val dbRes = db.talerTransferCreate(
req = req,
exchangeBankAccountId = exchangeBankAccount.expectRowId(),
@@ -122,7 +122,7 @@ fun Routing.talerWireGatewayHandlers(db: Database, ctx:
BankApplicationContext)
?: throw internalServerError("Database did not return the debit tx
row ID")
call.respond(
TransferResponse(
- timestamp =
TalerProtocolTimestamp.fromMicroseconds(transferTimestamp),
+ timestamp = TalerProtocolTimestamp(transferTimestamp),
row_id = debitRowId
)
)
@@ -152,7 +152,7 @@ fun Routing.talerWireGatewayHandlers(db: Database, ctx:
BankApplicationContext)
)
val exchangeAccount = db.bankAccountGetFromOwnerId(c.expectRowId())
?: throw internalServerError("exchange bank account not found,
despite it's a customer")
- val txTimestamp = getNowUs()
+ val txTimestamp = Instant.now()
val op = BankInternalTransaction(
debtorAccountId = walletAccount.expectRowId(),
amount = amount,
@@ -175,7 +175,7 @@ fun Routing.talerWireGatewayHandlers(db: Database, ctx:
BankApplicationContext)
call.respond(
AddIncomingResponse(
row_id = rowId,
- timestamp =
TalerProtocolTimestamp.fromMicroseconds(txTimestamp)
+ timestamp = TalerProtocolTimestamp(txTimestamp)
)
)
return@post
diff --git a/bank/src/main/kotlin/tech/libeufin/bank/helpers.kt
b/bank/src/main/kotlin/tech/libeufin/bank/helpers.kt
index 34a966d8..3007021b 100644
--- a/bank/src/main/kotlin/tech/libeufin/bank/helpers.kt
+++ b/bank/src/main/kotlin/tech/libeufin/bank/helpers.kt
@@ -30,6 +30,7 @@ import org.slf4j.Logger
import org.slf4j.LoggerFactory
import tech.libeufin.util.*
import java.net.URL
+import java.time.Instant
import java.util.*
const val FRACTION_BASE = 100000000
@@ -117,7 +118,7 @@ fun doTokenAuth(
logger.error("Auth token not found")
return null
}
- if (maybeToken.expirationTime - getNowUs() < 0) {
+ if (maybeToken.expirationTime.isBefore(Instant.now())) {
logger.error("Auth token is expired")
return null
}
@@ -437,4 +438,4 @@ fun maybeCreateAdminAccount(db: Database, ctx:
BankApplicationContext): Boolean
}
}
return true
-}
+}
\ No newline at end of file
diff --git a/bank/src/test/kotlin/DatabaseTest.kt
b/bank/src/test/kotlin/DatabaseTest.kt
index 45466947..890ec41a 100644
--- a/bank/src/test/kotlin/DatabaseTest.kt
+++ b/bank/src/test/kotlin/DatabaseTest.kt
@@ -20,7 +20,7 @@
import org.junit.Test
import tech.libeufin.bank.*
import tech.libeufin.util.CryptoUtil
-import tech.libeufin.util.getNowUs
+import java.time.Instant
import java.util.Random
import java.util.UUID
@@ -38,7 +38,7 @@ fun genTx(
accountServicerReference = "acct-svcr-ref",
endToEndId = "end-to-end-id",
paymentInformationId = "pmtinfid",
- transactionDate = 100000L
+ transactionDate = Instant.now()
)
class DatabaseTest {
@@ -123,7 +123,7 @@ class DatabaseTest {
val res = db.talerTransferCreate(
req = exchangeReq,
exchangeBankAccountId = 1L,
- timestamp = getNowUs()
+ timestamp = Instant.now()
)
assert(res.txResult == Database.BankTransactionResult.SUCCESS)
}
@@ -136,8 +136,8 @@ class DatabaseTest {
val token = BearerToken(
bankCustomer = 1L,
content = tokenBytes,
- creationTime = getNowUs(), // make .toMicro()? implicit?
- expirationTime = getNowUs(),
+ creationTime = Instant.now(),
+ expirationTime = Instant.now().plusSeconds(10),
scope = TokenScope.readonly
)
assert(db.bearerTokenGet(token.content) == null)
@@ -197,7 +197,7 @@ class DatabaseTest {
accountServicerReference = "acct-svcr-ref",
endToEndId = "end-to-end-id",
paymentInformationId = "pmtinfid",
- transactionDate = 100000L
+ transactionDate = Instant.now()
)
val barPays = db.bankTransactionCreate(barPaysFoo)
assert(barPays == Database.BankTransactionResult.SUCCESS)
@@ -273,7 +273,7 @@ class DatabaseTest {
))
val opSelected = db.talerWithdrawalGet(uuid)
assert(opSelected?.selectionDone == true &&
!opSelected.confirmationDone)
- assert(db.talerWithdrawalConfirm(uuid, 1L) ==
WithdrawalConfirmationResult.SUCCESS)
+ assert(db.talerWithdrawalConfirm(uuid, Instant.now()) ==
WithdrawalConfirmationResult.SUCCESS)
// Finally confirming the operation (means customer wired funds to the
exchange.)
assert(db.talerWithdrawalGet(uuid)?.confirmationDone == true)
}
@@ -316,10 +316,10 @@ class DatabaseTest {
sellOutFee = TalerAmount(0, 44, currency),
credit_payto_uri = "IBAN",
cashoutCurrency = "KUDOS",
- creationTime = 3L,
+ creationTime = Instant.now(),
subject = "31st",
tanChannel = TanChannel.sms,
- tanCode = "secret",
+ tanCode = "secret"
)
val fooId = db.customerCreate(customerFoo)
assert(fooId != null)
@@ -344,7 +344,7 @@ class DatabaseTest {
accountServicerReference = "acct-svcr-ref",
endToEndId = "end-to-end-id",
paymentInformationId = "pmtinfid",
- transactionDate = 100000L
+ transactionDate = Instant.now()
)
) == Database.BankTransactionResult.SUCCESS)
// Confirming the cash-out
diff --git a/bank/src/test/kotlin/JsonTest.kt b/bank/src/test/kotlin/JsonTest.kt
index 63eff6f9..263ed613 100644
--- a/bank/src/test/kotlin/JsonTest.kt
+++ b/bank/src/test/kotlin/JsonTest.kt
@@ -1,12 +1,11 @@
import kotlinx.serialization.Serializable
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
-import kotlinx.serialization.modules.SerializersModule
import org.junit.Test
-import tech.libeufin.bank.RelativeTime
-import tech.libeufin.bank.RelativeTimeSerializer
-import tech.libeufin.bank.TokenRequest
-import tech.libeufin.bank.TokenScope
+import tech.libeufin.bank.*
+import java.time.Duration
+import java.time.Instant
+import java.time.temporal.ChronoUnit
@Serializable
data class MyJsonType(
@@ -27,23 +26,24 @@ class JsonTest {
""".trimIndent()
Json.decodeFromString<MyJsonType>(serialized)
}
+
+ /**
+ * Testing the custom absolute and relative time serializers.
+ */
@Test
- fun unionTypeTest() {
- val jsonCfg = Json {
- serializersModule = SerializersModule {
- contextual(RelativeTime::class) {
- RelativeTimeSerializer
- }
- }
- }
- assert(jsonCfg.decodeFromString<RelativeTime>("{\"d_us\": 3}").d_us ==
3L)
- assert(jsonCfg.decodeFromString<RelativeTime>("{\"d_us\":
\"forever\"}").d_us == Long.MAX_VALUE)
- val tokenReq = jsonCfg.decodeFromString<TokenRequest>("""
- {
- "scope": "readonly",
- "duration": {"d_us": 30}
- }
- """.trimIndent())
- assert(tokenReq.scope == TokenScope.readonly &&
tokenReq.duration?.d_us == 30L)
+ fun timeSerializers() {
+ // from JSON to time types
+ assert(Json.decodeFromString<RelativeTime>("{\"d_us\":
3}").d_us.toNanos() == 3000L)
+ assert(Json.decodeFromString<RelativeTime>("{\"d_us\":
\"forever\"}").d_us == ChronoUnit.FOREVER.duration)
+ assert(Json.decodeFromString<TalerProtocolTimestamp>("{\"t_s\":
3}").t_s == Instant.ofEpochSecond(3))
+ assert(Json.decodeFromString<TalerProtocolTimestamp>("{\"t_s\":
\"never\"}").t_s == Instant.MAX)
+
+ // from time types to JSON
+ val oneDay = RelativeTime(d_us = Duration.of(1, ChronoUnit.DAYS))
+ val oneDaySerial = Json.encodeToString(oneDay)
+ assert(Json.decodeFromString<RelativeTime>(oneDaySerial).d_us ==
oneDay.d_us)
+ val forever = RelativeTime(d_us = ChronoUnit.FOREVER.duration)
+ val foreverSerial = Json.encodeToString(forever)
+ assert(Json.decodeFromString<RelativeTime>(foreverSerial).d_us ==
forever.d_us)
}
}
\ No newline at end of file
diff --git a/bank/src/test/kotlin/LibeuFinApiTest.kt
b/bank/src/test/kotlin/LibeuFinApiTest.kt
index 4cd2323f..8683fef8 100644
--- a/bank/src/test/kotlin/LibeuFinApiTest.kt
+++ b/bank/src/test/kotlin/LibeuFinApiTest.kt
@@ -8,8 +8,9 @@ import net.taler.wallet.crypto.Base32Crockford
import org.junit.Test
import tech.libeufin.bank.*
import tech.libeufin.util.CryptoUtil
-import tech.libeufin.util.getNowUs
import java.time.Duration
+import java.time.Instant
+import java.time.temporal.ChronoUnit
import kotlin.random.Random
class LibeuFinApiTest {
@@ -131,6 +132,31 @@ class LibeuFinApiTest {
}
}
+ // Creating token with "forever" duration.
+ @Test
+ fun tokenForeverTest() {
+ val db = initDb()
+ val ctx = getTestContext()
+ assert(db.customerCreate(customerFoo) != null)
+ testApplication {
+ application {
+ corebankWebApp(db, ctx)
+ }
+ val newTok = client.post("/accounts/foo/token") {
+ expectSuccess = true
+ contentType(ContentType.Application.Json)
+ basicAuth("foo", "pw")
+ setBody(
+ """
+ {"duration": {"d_us": "forever"}, "scope": "readonly"}
+ """.trimIndent()
+ )
+ }
+ val newTokObj =
Json.decodeFromString<TokenSuccessResponse>(newTok.bodyAsText())
+ assert(newTokObj.expiration.t_s == Instant.MAX)
+ }
+ }
+
// Checking the POST /token handling.
@Test
fun tokenTest() {
@@ -154,8 +180,8 @@ class LibeuFinApiTest {
// Checking that the token lifetime defaulted to 24 hours.
val newTokObj =
Json.decodeFromString<TokenSuccessResponse>(newTok.bodyAsText())
val newTokDb =
db.bearerTokenGet(Base32Crockford.decode(newTokObj.access_token))
- val lifeTime = newTokDb!!.expirationTime - newTokDb.creationTime
- assert(Duration.ofHours(24).seconds * 1000000 == lifeTime)
+ val lifeTime = Duration.between(newTokDb!!.creationTime,
newTokDb.expirationTime)
+ assert(lifeTime == Duration.ofDays(1))
// foo tries on bar endpoint
val r = client.post("/accounts/bar/token") {
expectSuccess = false
@@ -170,9 +196,9 @@ class LibeuFinApiTest {
content = fooTok,
bankCustomer = 1L, // only foo exists.
scope = TokenScope.readonly,
- creationTime = getNowUs(),
+ creationTime = Instant.now(),
isRefreshable = true,
- expirationTime = getNowUs() +
(Duration.ofHours(1).toMillis() * 1000)
+ expirationTime = Instant.now().plus(1, ChronoUnit.DAYS)
)
)
)
diff --git a/util/src/main/kotlin/Config.kt b/util/src/main/kotlin/Config.kt
index 62a302a5..c54dc4ed 100644
--- a/util/src/main/kotlin/Config.kt
+++ b/util/src/main/kotlin/Config.kt
@@ -4,8 +4,10 @@ import ch.qos.logback.classic.Level
import ch.qos.logback.classic.LoggerContext
import ch.qos.logback.core.util.Loader
import io.ktor.util.*
+import org.slf4j.Logger
import org.slf4j.LoggerFactory
+val logger: Logger = LoggerFactory.getLogger("tech.libeufin.util")
/**
* Putting those values into the 'attributes' container because they
* are needed by the util routines that do NOT have Sandbox and Nexus
diff --git a/util/src/main/kotlin/DB.kt b/util/src/main/kotlin/DB.kt
index 7f518ebd..af26d3d3 100644
--- a/util/src/main/kotlin/DB.kt
+++ b/util/src/main/kotlin/DB.kt
@@ -33,8 +33,6 @@ import org.slf4j.Logger
import org.slf4j.LoggerFactory
import java.net.URI
-private val logger: Logger = LoggerFactory.getLogger("tech.libeufin.util.DB")
-
fun getCurrentUser(): String = System.getProperty("user.name")
fun isPostgres(): Boolean {
diff --git a/util/src/main/kotlin/Ebics.kt b/util/src/main/kotlin/Ebics.kt
index 8b78932d..837f49ba 100644
--- a/util/src/main/kotlin/Ebics.kt
+++ b/util/src/main/kotlin/Ebics.kt
@@ -45,8 +45,6 @@ import javax.xml.bind.JAXBElement
import javax.xml.datatype.DatatypeFactory
import javax.xml.datatype.XMLGregorianCalendar
-private val logger: Logger = LoggerFactory.getLogger("tech.libeufin.util")
-
data class EbicsProtocolError(
val httpStatusCode: HttpStatusCode,
val reason: String,
diff --git a/util/src/main/kotlin/time.kt b/util/src/main/kotlin/time.kt
index 687a97f7..6c1b9464 100644
--- a/util/src/main/kotlin/time.kt
+++ b/util/src/main/kotlin/time.kt
@@ -21,6 +21,59 @@ package tech.libeufin.util
import java.time.*
import java.time.temporal.ChronoUnit
+import java.util.concurrent.TimeUnit
+/**
+ * Converts the 'this' Instant to the number of nanoseconds
+ * since the Epoch. It returns the result as Long, or null
+ * if one arithmetic overflow occurred.
+ */
+private fun Instant.toNanos(): Long? {
+ val oneSecNanos = ChronoUnit.SECONDS.duration.toNanos()
+ val nanoBase: Long = this.epochSecond * oneSecNanos
+ if (nanoBase != 0L && nanoBase / this.epochSecond != oneSecNanos) {
+ logger.error("Multiplication overflow: could not convert Instant to
nanos.")
+ return null
+ }
+ val res = nanoBase + this.nano
+ if (res < nanoBase) {
+ logger.error("Addition overflow: could not convert Instant to nanos.")
+ return null
+ }
+ return res
+}
+
+/**
+ * This function converts an Instant input to the
+ * number of microseconds since the Epoch, except that
+ * it yields Long.MAX if the Input is Instant.MAX.
+ *
+ * Takes the name after the way timestamps are designed
+ * in the database: micros since Epoch, or Long.MAX for
+ * "never".
+ *
+ * Returns the Long representation of 'this' or null
+ * if that would overflow.
+ */
+fun Instant.toDbMicros(): Long? {
+ if (this == Instant.MAX)
+ return Long.MAX_VALUE
+ val nanos = this.toNanos() ?: return null
+ return nanos / 1000L
+}
-fun getNowUs(): Long = ChronoUnit.MICROS.between(Instant.EPOCH, Instant.now())
\ No newline at end of file
+/**
+ * This helper is typically used to convert a timestamp expressed
+ * in microseconds from the DB back to the Web application. In case
+ * of _any_ error, it logs it and returns null.
+ */
+fun Long.microsToJavaInstant(): Instant? {
+ if (this == Long.MAX_VALUE)
+ return Instant.MAX
+ return try {
+ Instant.EPOCH.plus(this, ChronoUnit.MICROS)
+ } catch (e: Exception) {
+ logger.error(e.message)
+ return null
+ }
+}
\ No newline at end of file
--
To stop receiving notification emails like this one, please contact
gnunet@gnunet.org.
- [libeufin] branch master updated (973c88cc -> 46e4838f),
gnunet <=