summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAntoine A <>2024-03-16 02:23:06 +0100
committerAntoine A <>2024-03-16 02:23:06 +0100
commite292fa357724df8695b6110eec6e4a60c7986363 (patch)
tree417196f539625eeb8b805f8b8d9dc0c5783caf97
parente5e718622da88f8eff5474a3c7092ee51360977e (diff)
downloadlibeufin-e292fa357724df8695b6110eec6e4a60c7986363.tar.gz
libeufin-e292fa357724df8695b6110eec6e4a60c7986363.tar.bz2
libeufin-e292fa357724df8695b6110eec6e4a60c7986363.zip
Simplify error handling for microsecond overflows that never occur in practice
-rw-r--r--bank/src/main/kotlin/tech/libeufin/bank/db/AccountDAO.kt4
-rw-r--r--bank/src/main/kotlin/tech/libeufin/bank/db/CashoutDAO.kt4
-rw-r--r--bank/src/main/kotlin/tech/libeufin/bank/db/Database.kt23
-rw-r--r--bank/src/main/kotlin/tech/libeufin/bank/db/ExchangeDAO.kt4
-rw-r--r--bank/src/main/kotlin/tech/libeufin/bank/db/TanDAO.kt10
-rw-r--r--bank/src/main/kotlin/tech/libeufin/bank/db/TokenDAO.kt12
-rw-r--r--bank/src/main/kotlin/tech/libeufin/bank/db/TransactionDAO.kt2
-rw-r--r--bank/src/main/kotlin/tech/libeufin/bank/db/WithdrawalDAO.kt4
-rw-r--r--bank/src/test/kotlin/StatsTest.kt2
-rw-r--r--common/src/main/kotlin/time.kt66
-rw-r--r--nexus/src/main/kotlin/tech/libeufin/nexus/Log.kt4
-rw-r--r--nexus/src/main/kotlin/tech/libeufin/nexus/db/InitiatedDAO.kt14
-rw-r--r--nexus/src/main/kotlin/tech/libeufin/nexus/db/PaymentDAO.kt12
13 files changed, 52 insertions, 109 deletions
diff --git a/bank/src/main/kotlin/tech/libeufin/bank/db/AccountDAO.kt b/bank/src/main/kotlin/tech/libeufin/bank/db/AccountDAO.kt
index 49ca49c0..7781d807 100644
--- a/bank/src/main/kotlin/tech/libeufin/bank/db/AccountDAO.kt
+++ b/bank/src/main/kotlin/tech/libeufin/bank/db/AccountDAO.kt
@@ -52,7 +52,7 @@ class AccountDAO(private val db: Database) {
checkPaytoIdempotent: Boolean,
ctx: BankPaytoCtx
): AccountCreationResult = db.serializable { it ->
- val now = Instant.now().toDbMicros() ?: throw faultyTimestampByBank()
+ val now = Instant.now().micros()
it.transaction { conn ->
val idempotent = conn.prepareStatement("""
SELECT password_hash, name=?
@@ -194,7 +194,7 @@ class AccountDAO(private val db: Database) {
login: String,
is2fa: Boolean
): AccountDeletionResult = db.serializable { conn ->
- val now = Instant.now().toDbMicros() ?: throw faultyTimestampByBank()
+ val now = Instant.now().micros()
val stmt = conn.prepareStatement("""
SELECT
out_not_found,
diff --git a/bank/src/main/kotlin/tech/libeufin/bank/db/CashoutDAO.kt b/bank/src/main/kotlin/tech/libeufin/bank/db/CashoutDAO.kt
index 0f9d8f86..45da0854 100644
--- a/bank/src/main/kotlin/tech/libeufin/bank/db/CashoutDAO.kt
+++ b/bank/src/main/kotlin/tech/libeufin/bank/db/CashoutDAO.kt
@@ -66,7 +66,7 @@ class CashoutDAO(private val db: Database) {
stmt.setLong(5, amountCredit.value)
stmt.setInt(6, amountCredit.frac)
stmt.setString(7, subject)
- stmt.setLong(8, now.toDbMicros() ?: throw faultyTimestampByBank())
+ stmt.setLong(8, now.micros())
stmt.setBoolean(9, is2fa)
stmt.executeQuery().use {
when {
@@ -117,7 +117,7 @@ class CashoutDAO(private val db: Database) {
creation_time = it.getTalerTimestamp("creation_time"),
confirmation_time = when (val timestamp = it.getLong("confirmation_date")) {
0L -> null
- else -> TalerProtocolTimestamp(timestamp.microsToJavaInstant() ?: throw faultyTimestampByBank())
+ else -> TalerProtocolTimestamp(timestamp.asInstant())
},
tan_channel = it.getString("tan_channel")?.run { TanChannel.valueOf(this) },
tan_info = it.getString("tan_info"),
diff --git a/bank/src/main/kotlin/tech/libeufin/bank/db/Database.kt b/bank/src/main/kotlin/tech/libeufin/bank/db/Database.kt
index 1880b67b..19706a7b 100644
--- a/bank/src/main/kotlin/tech/libeufin/bank/db/Database.kt
+++ b/bank/src/main/kotlin/tech/libeufin/bank/db/Database.kt
@@ -35,25 +35,6 @@ import kotlin.math.abs
private val logger: Logger = LoggerFactory.getLogger("libeufin-bank-db")
-/**
- * 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".
- */
-internal fun faultyTimestampByBank() = internalServerError("Bank took overflowing timestamp")
-internal fun faultyDurationByClient() = badRequest("Overflowing duration, please specify 'forever' instead.")
-
class Database(dbConfig: String, internal val bankCurrency: String, internal val fiatCurrency: String?): DbPool(dbConfig, "libeufin_bank") {
internal val notifWatcher: NotificationWatcher = NotificationWatcher(pgSource)
@@ -206,7 +187,5 @@ enum class AbortResult {
}
fun ResultSet.getTalerTimestamp(name: String): TalerProtocolTimestamp{
- return TalerProtocolTimestamp(
- getLong(name).microsToJavaInstant() ?: throw faultyTimestampByBank()
- )
+ return TalerProtocolTimestamp(getLong(name).asInstant())
} \ No newline at end of file
diff --git a/bank/src/main/kotlin/tech/libeufin/bank/db/ExchangeDAO.kt b/bank/src/main/kotlin/tech/libeufin/bank/db/ExchangeDAO.kt
index 6540172a..69249f1a 100644
--- a/bank/src/main/kotlin/tech/libeufin/bank/db/ExchangeDAO.kt
+++ b/bank/src/main/kotlin/tech/libeufin/bank/db/ExchangeDAO.kt
@@ -130,7 +130,7 @@ class ExchangeDAO(private val db: Database) {
stmt.setString(6, req.exchange_base_url.url)
stmt.setString(7, req.credit_account.canonical)
stmt.setString(8, login)
- stmt.setLong(9, now.toDbMicros() ?: throw faultyTimestampByBank())
+ stmt.setLong(9, now.micros())
stmt.executeQuery().use {
when {
@@ -191,7 +191,7 @@ class ExchangeDAO(private val db: Database) {
stmt.setInt(4, req.amount.frac)
stmt.setString(5, req.debit_account.canonical)
stmt.setString(6, login)
- stmt.setLong(7, now.toDbMicros() ?: throw faultyTimestampByBank())
+ stmt.setLong(7, now.micros())
stmt.executeQuery().use {
when {
diff --git a/bank/src/main/kotlin/tech/libeufin/bank/db/TanDAO.kt b/bank/src/main/kotlin/tech/libeufin/bank/db/TanDAO.kt
index 66771c93..c7328d65 100644
--- a/bank/src/main/kotlin/tech/libeufin/bank/db/TanDAO.kt
+++ b/bank/src/main/kotlin/tech/libeufin/bank/db/TanDAO.kt
@@ -23,7 +23,7 @@ import tech.libeufin.bank.Operation
import tech.libeufin.bank.TanChannel
import tech.libeufin.bank.internalServerError
import tech.libeufin.common.oneOrNull
-import tech.libeufin.common.toDbMicros
+import tech.libeufin.common.micros
import java.time.Duration
import java.time.Instant
import java.util.concurrent.TimeUnit
@@ -46,7 +46,7 @@ class TanDAO(private val db: Database) {
stmt.setString(1, body)
stmt.setString(2, op.name)
stmt.setString(3, code)
- stmt.setLong(4, now.toDbMicros() ?: throw faultyTimestampByBank())
+ stmt.setLong(4, now.micros())
stmt.setLong(5, TimeUnit.MICROSECONDS.convert(validityPeriod))
stmt.setInt(6, retryCounter)
stmt.setString(7, login)
@@ -76,7 +76,7 @@ class TanDAO(private val db: Database) {
stmt.setLong(1, id)
stmt.setString(2, login)
stmt.setString(3, code)
- stmt.setLong(4, now.toDbMicros() ?: throw faultyTimestampByBank())
+ stmt.setLong(4, now.micros())
stmt.setLong(5, TimeUnit.MICROSECONDS.convert(validityPeriod))
stmt.setInt(6, retryCounter)
stmt.executeQuery().use {
@@ -100,7 +100,7 @@ class TanDAO(private val db: Database) {
) = db.serializable { conn ->
val stmt = conn.prepareStatement("SELECT tan_challenge_mark_sent(?,?,?)")
stmt.setLong(1, id)
- stmt.setLong(2, now.toDbMicros() ?: throw faultyTimestampByBank())
+ stmt.setLong(2, now.micros())
stmt.setLong(3, TimeUnit.MICROSECONDS.convert(retransmissionPeriod))
stmt.executeQuery()
}
@@ -129,7 +129,7 @@ class TanDAO(private val db: Database) {
stmt.setLong(1, id)
stmt.setString(2, login)
stmt.setString(3, code)
- stmt.setLong(4, now.toDbMicros() ?: throw faultyTimestampByBank())
+ stmt.setLong(4, now.micros())
stmt.executeQuery().use {
when {
!it.next() -> throw internalServerError("TAN try returned nothing")
diff --git a/bank/src/main/kotlin/tech/libeufin/bank/db/TokenDAO.kt b/bank/src/main/kotlin/tech/libeufin/bank/db/TokenDAO.kt
index 0723ac88..ee1a1669 100644
--- a/bank/src/main/kotlin/tech/libeufin/bank/db/TokenDAO.kt
+++ b/bank/src/main/kotlin/tech/libeufin/bank/db/TokenDAO.kt
@@ -22,9 +22,9 @@ package tech.libeufin.bank.db
import tech.libeufin.bank.BearerToken
import tech.libeufin.bank.TokenScope
import tech.libeufin.common.executeUpdateViolation
-import tech.libeufin.common.microsToJavaInstant
+import tech.libeufin.common.asInstant
import tech.libeufin.common.oneOrNull
-import tech.libeufin.common.toDbMicros
+import tech.libeufin.common.micros
import java.time.Instant
/** Data access logic for auth tokens */
@@ -56,8 +56,8 @@ class TokenDAO(private val db: Database) {
) VALUES (?, ?, ?, ?::token_scope_enum, ?, ?)
""")
stmt.setBytes(1, content)
- stmt.setLong(2, creationTime.toDbMicros() ?: throw faultyTimestampByBank())
- stmt.setLong(3, expirationTime.toDbMicros() ?: throw faultyDurationByClient())
+ stmt.setLong(2, creationTime.micros())
+ stmt.setLong(3, expirationTime.micros())
stmt.setString(4, scope.name)
stmt.setLong(5, bankCustomer)
stmt.setBoolean(6, isRefreshable)
@@ -80,8 +80,8 @@ class TokenDAO(private val db: Database) {
stmt.setBytes(1, token)
stmt.oneOrNull {
BearerToken(
- creationTime = it.getLong("creation_time").microsToJavaInstant() ?: throw faultyDurationByClient(),
- expirationTime = it.getLong("expiration_time").microsToJavaInstant() ?: throw faultyDurationByClient(),
+ creationTime = it.getLong("creation_time").asInstant(),
+ expirationTime = it.getLong("expiration_time").asInstant(),
login = it.getString("login"),
scope = TokenScope.valueOf(it.getString("scope")),
isRefreshable = it.getBoolean("is_refreshable")
diff --git a/bank/src/main/kotlin/tech/libeufin/bank/db/TransactionDAO.kt b/bank/src/main/kotlin/tech/libeufin/bank/db/TransactionDAO.kt
index bbd70bcf..e82f0ba2 100644
--- a/bank/src/main/kotlin/tech/libeufin/bank/db/TransactionDAO.kt
+++ b/bank/src/main/kotlin/tech/libeufin/bank/db/TransactionDAO.kt
@@ -51,7 +51,7 @@ class TransactionDAO(private val db: Database) {
is2fa: Boolean,
requestUid: ShortHashCode?,
): BankTransactionResult = db.serializable { conn ->
- val now = timestamp.toDbMicros() ?: throw faultyTimestampByBank()
+ val now = timestamp.micros()
conn.transaction {
val stmt = conn.prepareStatement("""
SELECT
diff --git a/bank/src/main/kotlin/tech/libeufin/bank/db/WithdrawalDAO.kt b/bank/src/main/kotlin/tech/libeufin/bank/db/WithdrawalDAO.kt
index 27b4002e..3a41c0d8 100644
--- a/bank/src/main/kotlin/tech/libeufin/bank/db/WithdrawalDAO.kt
+++ b/bank/src/main/kotlin/tech/libeufin/bank/db/WithdrawalDAO.kt
@@ -56,7 +56,7 @@ class WithdrawalDAO(private val db: Database) {
stmt.setObject(2, uuid)
stmt.setLong(3, amount.value)
stmt.setInt(4, amount.frac)
- stmt.setLong(5, now.toDbMicros() ?: throw faultyTimestampByBank())
+ stmt.setLong(5, now.micros())
stmt.executeQuery().use {
when {
!it.next() ->
@@ -165,7 +165,7 @@ class WithdrawalDAO(private val db: Database) {
)
stmt.setString(1, login)
stmt.setObject(2, uuid)
- stmt.setLong(3, now.toDbMicros() ?: throw faultyTimestampByBank())
+ stmt.setLong(3, now.micros())
stmt.setBoolean(4, is2fa)
stmt.executeQuery().use {
when {
diff --git a/bank/src/test/kotlin/StatsTest.kt b/bank/src/test/kotlin/StatsTest.kt
index 3f235d4f..c34565fd 100644
--- a/bank/src/test/kotlin/StatsTest.kt
+++ b/bank/src/test/kotlin/StatsTest.kt
@@ -37,7 +37,7 @@ class StatsTest {
suspend fun cashin(amount: String) {
db.conn { conn ->
val stmt = conn.prepareStatement("SELECT 0 FROM cashin(?, ?, (?, ?)::taler_amount, ?)")
- stmt.setLong(1, Instant.now().toDbMicros()!!)
+ stmt.setLong(1, Instant.now().micros())
stmt.setBytes(2, ShortHashCode.rand().raw)
val amount = TalerAmount(amount)
stmt.setLong(3, amount.value)
diff --git a/common/src/main/kotlin/time.kt b/common/src/main/kotlin/time.kt
index 834183ee..255004f5 100644
--- a/common/src/main/kotlin/time.kt
+++ b/common/src/main/kotlin/time.kt
@@ -26,61 +26,35 @@ import java.time.temporal.ChronoUnit
private val logger: Logger = LoggerFactory.getLogger("libeufin-common")
-/**
- * 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)
+/**
+ * Convert Instant to microseconds since the epoch.
+ *
+ * Returns Long.MAX_VALUE if instant is Instant.MAX
+ **/
+fun Instant.micros(): Long {
+ if (this == Instant.MAX)
return Long.MAX_VALUE
- val nanos = this.toNanos() ?: run {
- logger.error("Could not obtain micros to store to database, convenience conversion to nanos overflew.")
- return null
+ try {
+ val micros = ChronoUnit.MICROS.between(Instant.EPOCH, this)
+ if (micros == Long.MAX_VALUE) throw ArithmeticException()
+ return micros
+ } catch (e: ArithmeticException) {
+ throw Exception("${this} is too big to be converted to micros resolution", e)
}
- return nanos / 1000L
}
-/**
- * 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.
+/**
+ * Convert microsecons to Instant.
+ *
+ * Returns Instant.MAX if microseconds is Long.MAX_VALUE
*/
-fun Long.microsToJavaInstant(): Instant? {
+fun Long.asInstant(): 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
+ } catch (e: ArithmeticException ) {
+ throw Exception("${this} is too big to be converted to Instant", e)
}
}
diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/Log.kt b/nexus/src/main/kotlin/tech/libeufin/nexus/Log.kt
index 1367c97c..fa74c791 100644
--- a/nexus/src/main/kotlin/tech/libeufin/nexus/Log.kt
+++ b/nexus/src/main/kotlin/tech/libeufin/nexus/Log.kt
@@ -58,7 +58,7 @@ class FileLogger(path: String?) {
// Subdir based on current day.
val now = Instant.now()
val asUtcDate = LocalDate.ofInstant(now, ZoneId.of("UTC"))
- val nowMs = now.toDbMicros()
+ val nowMs = now.micros()
// Creating the combined dir.
val subDir = dir.resolve("${asUtcDate.year}-${asUtcDate.monthValue}-${asUtcDate.dayOfMonth}").resolve("fetch")
subDir.createDirectories()
@@ -86,7 +86,7 @@ class FileLogger(path: String?) {
// Subdir based on current day.
val now = Instant.now()
val asUtcDate = LocalDate.ofInstant(now, ZoneId.of("UTC"))
- val nowMs = now.toDbMicros()
+ val nowMs = now.micros()
// Creating the combined dir.
val subDir = dir.resolve("${asUtcDate.year}-${asUtcDate.monthValue}-${asUtcDate.dayOfMonth}").resolve("submit")
subDir.createDirectories()
diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/db/InitiatedDAO.kt b/nexus/src/main/kotlin/tech/libeufin/nexus/db/InitiatedDAO.kt
index eba3c78a..162fddee 100644
--- a/nexus/src/main/kotlin/tech/libeufin/nexus/db/InitiatedDAO.kt
+++ b/nexus/src/main/kotlin/tech/libeufin/nexus/db/InitiatedDAO.kt
@@ -48,10 +48,7 @@ class InitiatedDAO(private val db: Database) {
stmt.setInt(2, paymentData.amount.frac)
stmt.setString(3, paymentData.wireTransferSubject)
stmt.setString(4, paymentData.creditPaytoUri.toString())
- val initiationTime = paymentData.initiationTime.toDbMicros() ?: run {
- throw Exception("Initiation time could not be converted to microseconds for the database.")
- }
- stmt.setLong(5, initiationTime)
+ stmt.setLong(5, paymentData.initiationTime.micros())
stmt.setString(6, paymentData.requestUid)
if (stmt.executeUpdateViolation())
return@conn PaymentInitiationResult.SUCCESS
@@ -73,7 +70,7 @@ class InitiatedDAO(private val db: Database) {
,submission_counter = submission_counter + 1
WHERE initiated_outgoing_transaction_id = ?
""")
- stmt.setLong(1, now.toDbMicros()!!)
+ stmt.setLong(1, now.micros())
stmt.setString(2, orderId)
stmt.setLong(3, id)
stmt.execute()
@@ -93,7 +90,7 @@ class InitiatedDAO(private val db: Database) {
,submission_counter = submission_counter + 1
WHERE initiated_outgoing_transaction_id = ?
""")
- stmt.setLong(1, now.toDbMicros()!!)
+ stmt.setLong(1, now.micros())
stmt.setString(2, msg)
stmt.setLong(3, id)
stmt.execute()
@@ -174,10 +171,7 @@ class InitiatedDAO(private val db: Database) {
suspend fun submittable(currency: String): List<InitiatedPayment> = db.conn { conn ->
fun extract(it: ResultSet): InitiatedPayment {
val rowId = it.getLong("initiated_outgoing_transaction_id")
- val initiationTime = it.getLong("initiation_time").microsToJavaInstant()
- if (initiationTime == null) { // nexus fault
- throw Exception("Found invalid timestamp at initiated payment with ID: $rowId")
- }
+ val initiationTime = it.getLong("initiation_time").asInstant()
return InitiatedPayment(
id = it.getLong("initiated_outgoing_transaction_id"),
amount = it.getAmount("amount", currency),
diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/db/PaymentDAO.kt b/nexus/src/main/kotlin/tech/libeufin/nexus/db/PaymentDAO.kt
index 2e315f38..2730a437 100644
--- a/nexus/src/main/kotlin/tech/libeufin/nexus/db/PaymentDAO.kt
+++ b/nexus/src/main/kotlin/tech/libeufin/nexus/db/PaymentDAO.kt
@@ -38,8 +38,7 @@ class PaymentDAO(private val db: Database) {
SELECT out_tx_id, out_initiated, out_found
FROM register_outgoing((?,?)::taler_amount,?,?,?,?)
""")
- val executionTime = paymentData.executionTime.toDbMicros()
- ?: throw Exception("Could not convert outgoing payment execution_time to microseconds")
+ val executionTime = paymentData.executionTime.micros()
stmt.setLong(1, paymentData.amount.value)
stmt.setInt(2, paymentData.amount.frac)
stmt.setString(3, paymentData.wireTransferSubject)
@@ -72,10 +71,8 @@ class PaymentDAO(private val db: Database) {
SELECT out_found, out_tx_id, out_bounce_id
FROM register_incoming_and_bounce((?,?)::taler_amount,?,?,?,?,(?,?)::taler_amount,?)
""")
- val refundTimestamp = now.toDbMicros()
- ?: throw Exception("Could not convert refund execution time from Instant.now() to microsends.")
- val executionTime = paymentData.executionTime.toDbMicros()
- ?: throw Exception("Could not convert payment execution time from Instant to microseconds.")
+ val refundTimestamp = now.micros()
+ val executionTime = paymentData.executionTime.micros()
stmt.setLong(1, paymentData.amount.value)
stmt.setInt(2, paymentData.amount.frac)
stmt.setString(3, paymentData.wireTransferSubject)
@@ -109,8 +106,7 @@ class PaymentDAO(private val db: Database) {
SELECT out_found, out_tx_id
FROM register_incoming_and_talerable((?,?)::taler_amount,?,?,?,?,?)
""")
- val executionTime = paymentData.executionTime.toDbMicros()
- ?: throw Exception("Could not convert payment execution time from Instant to microseconds.")
+ val executionTime = paymentData.executionTime.micros()
stmt.setLong(1, paymentData.amount.value)
stmt.setInt(2, paymentData.amount.frac)
stmt.setString(3, paymentData.wireTransferSubject)