commit 90be1bee51c819afb9ce8928837f229eb343411a
parent 0bcadb6e7637a92744029f909f14c5df8dfe078a
Author: Antoine A <>
Date: Wed, 8 Nov 2023 17:28:09 +0000
Improve /monitor
Diffstat:
5 files changed, 213 insertions(+), 73 deletions(-)
diff --git a/bank/src/main/kotlin/tech/libeufin/bank/TalerMessage.kt b/bank/src/main/kotlin/tech/libeufin/bank/TalerMessage.kt
@@ -127,31 +127,33 @@ data class TokenRequest(
@Serializable
sealed class MonitorResponse {
abstract val talerInCount: Long
- abstract val talerInInternalVolume: TalerAmount
+ abstract val talerInVolume: TalerAmount
abstract val talerOutCount: Long
- abstract val talerOutInternalVolume: TalerAmount
+ abstract val talerOutVolume: TalerAmount
}
@Serializable
@SerialName("just-payouts")
data class MonitorJustPayouts(
override val talerInCount: Long,
- override val talerInInternalVolume: TalerAmount,
+ override val talerInVolume: TalerAmount,
override val talerOutCount: Long,
- override val talerOutInternalVolume: TalerAmount
+ override val talerOutVolume: TalerAmount
) : MonitorResponse()
@Serializable
@SerialName("with-cashout")
data class MonitorWithCashout(
- val cashinCount: Long?,
+ val cashinCount: Long,
+ val cashinInternalVolume: TalerAmount,
val cashinExternalVolume: TalerAmount,
val cashoutCount: Long,
+ val cashoutInternalVolume: TalerAmount,
val cashoutExternalVolume: TalerAmount,
override val talerInCount: Long,
- override val talerInInternalVolume: TalerAmount,
+ override val talerInVolume: TalerAmount,
override val talerOutCount: Long,
- override val talerOutInternalVolume: TalerAmount
+ override val talerOutVolume: TalerAmount
) : MonitorResponse()
/**
diff --git a/bank/src/main/kotlin/tech/libeufin/bank/db/Database.kt b/bank/src/main/kotlin/tech/libeufin/bank/db/Database.kt
@@ -847,11 +847,15 @@ class Database(dbConfig: String, internal val bankCurrency: String, internal val
val stmt = conn.prepareStatement("""
SELECT
cashin_count
- ,(cashin_volume).val as cashin_volume_val
- ,(cashin_volume).frac as cashin_volume_frac
+ ,(cashin_internal_volume).val as cashin_internal_volume_val
+ ,(cashin_internal_volume).frac as cashin_internal_volume_frac
+ ,(cashin_external_volume).val as cashin_external_volume_val
+ ,(cashin_external_volume).frac as cashin_external_volume_frac
,cashout_count
- ,(cashout_volume).val as cashout_volume_val
- ,(cashout_volume).frac as cashout_volume_frac
+ ,(cashout_internal_volume).val as cashout_internal_volume_val
+ ,(cashout_internal_volume).frac as cashout_internal_volume_frac
+ ,(cashout_external_volume).val as cashout_external_volume_val
+ ,(cashout_external_volume).frac as cashout_external_volume_frac
,taler_in_count
,(taler_in_volume).val as taler_in_volume_val
,(taler_in_volume).frac as taler_in_volume_frac
@@ -870,25 +874,35 @@ class Database(dbConfig: String, internal val bankCurrency: String, internal val
fiatCurrency?.run {
MonitorWithCashout(
cashinCount = it.getLong("cashin_count"),
+ cashinInternalVolume = TalerAmount(
+ value = it.getLong("cashin_internal_volume_val"),
+ frac = it.getInt("cashin_internal_volume_frac"),
+ currency = bankCurrency
+ ),
cashinExternalVolume = TalerAmount(
- value = it.getLong("cashin_volume_val"),
- frac = it.getInt("cashin_volume_frac"),
+ value = it.getLong("cashin_external_volume_val"),
+ frac = it.getInt("cashin_external_volume_frac"),
currency = this
),
cashoutCount = it.getLong("cashout_count"),
+ cashoutInternalVolume = TalerAmount(
+ value = it.getLong("cashout_internal_volume_val"),
+ frac = it.getInt("cashout_internal_volume_frac"),
+ currency = bankCurrency
+ ),
cashoutExternalVolume = TalerAmount(
- value = it.getLong("cashout_volume_val"),
- frac = it.getInt("cashout_volume_frac"),
+ value = it.getLong("cashout_external_volume_val"),
+ frac = it.getInt("cashout_external_volume_frac"),
currency = this
),
talerInCount = it.getLong("taler_in_count"),
- talerInInternalVolume = TalerAmount(
+ talerInVolume = TalerAmount(
value = it.getLong("taler_in_volume_val"),
frac = it.getInt("taler_in_volume_frac"),
currency = bankCurrency
),
talerOutCount = it.getLong("taler_out_count"),
- talerOutInternalVolume = TalerAmount(
+ talerOutVolume = TalerAmount(
value = it.getLong("taler_out_volume_val"),
frac = it.getInt("taler_out_volume_frac"),
currency = bankCurrency
@@ -896,13 +910,13 @@ class Database(dbConfig: String, internal val bankCurrency: String, internal val
)
} ?: MonitorJustPayouts(
talerInCount = it.getLong("taler_in_count"),
- talerInInternalVolume = TalerAmount(
+ talerInVolume = TalerAmount(
value = it.getLong("taler_in_volume_val"),
frac = it.getInt("taler_in_volume_frac"),
currency = bankCurrency
),
talerOutCount = it.getLong("taler_out_count"),
- talerOutInternalVolume = TalerAmount(
+ talerOutVolume = TalerAmount(
value = it.getLong("taler_out_volume_val"),
frac = it.getInt("taler_out_volume_frac"),
currency = bankCurrency
diff --git a/bank/src/test/kotlin/StatsTest.kt b/bank/src/test/kotlin/StatsTest.kt
@@ -23,9 +23,11 @@ import io.ktor.http.*
import io.ktor.http.content.*
import io.ktor.server.testing.*
import java.time.*
+import java.time.Instant
import java.util.*
import kotlin.test.*
import kotlin.reflect.full.declaredMemberProperties
+import kotlin.suspend
import kotlinx.serialization.json.Json
import org.junit.Test
import tech.libeufin.bank.*
@@ -33,7 +35,8 @@ import tech.libeufin.util.*
class StatsTest {
@Test
- fun transfer() = bankSetup { _ ->
+ fun register() = bankSetup { db ->
+ setMaxDebt("merchant", TalerAmount("KUDOS:1000"))
setMaxDebt("exchange", TalerAmount("KUDOS:1000"))
setMaxDebt("customer", TalerAmount("KUDOS:1000"))
client.patch("/accounts/customer") {
@@ -46,25 +49,58 @@ class StatsTest {
})
}.assertNoContent()
- suspend fun monitor(countName: String, volumeName: String, count: Long, amount: String) {
+ 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.setString(2, IbanPayTo("payto://iban/CUSTOMER-IBAN-XYZ").canonical)
+ val amount = TalerAmount(amount)
+ stmt.setLong(3, amount.value)
+ stmt.setInt(4, amount.frac)
+ stmt.setString(5, "")
+ stmt.executeQueryCheck();
+ }
+ }
+
+ suspend fun monitor(
+ dbCount: (MonitorWithCashout) -> Long,
+ count: Long,
+ internalVolume: (MonitorWithCashout) -> TalerAmount,
+ internalAmount: String,
+ externalVolume: ((MonitorWithCashout) -> TalerAmount)? = null,
+ externalAmount: String? = null
+ ) {
Timeframe.entries.forEach { timestamp ->
client.get("/monitor?timestamp=${timestamp.name}") { basicAuth("admin", "admin-password") }.assertOk().run {
- val resp = json<MonitorResponse>()
- assertEquals(count, resp.javaClass.kotlin.declaredMemberProperties.first { it.name == countName }.get(resp))
- assertEquals(TalerAmount(amount), resp.javaClass.kotlin.declaredMemberProperties.first { it.name == volumeName }.get(resp))
+ println(bodyAsText())
+ val resp = json<MonitorResponse>() as MonitorWithCashout
+ assertEquals(count, dbCount(resp))
+ assertEquals(TalerAmount(internalAmount), internalVolume(resp))
+ externalVolume?.run { assertEquals(TalerAmount(externalAmount!!), this(resp)) }
}
}
}
- suspend fun monitorTalerOut(count: Long, amount: String) = monitor("talerOutCount" , "talerOutInternalVolume", count, amount)
- suspend fun monitorTalerIn(count: Long, amount: String) = monitor("talerInCount" , "talerInInternalVolume", count, amount)
- suspend fun monitorCashin(count: Long, amount: String) = monitor("cashinCount" , "cashinExternalVolume", count, amount)
- suspend fun monitorCashout(count: Long, amount: String) = monitor("cashoutCount" , "cashoutExternalVolume", count, amount)
+ suspend fun monitorTalerIn(count: Long, amount: String) =
+ monitor({it.talerInCount}, count, {it.talerInVolume}, amount)
+ suspend fun monitorTalerOut(count: Long, amount: String) =
+ monitor({it.talerOutCount}, count, {it.talerOutVolume}, amount)
+ suspend fun monitorCashin(count: Long, internalAmount: String, externalAmount: String) =
+ monitor({it.cashinCount}, count, {it.cashinInternalVolume}, internalAmount, {it.cashinExternalVolume}, externalAmount)
+ suspend fun monitorCashout(count: Long, internalAmount: String, externalAmount: String) =
+ monitor({it.cashoutCount}, count, {it.cashoutInternalVolume}, internalAmount, {it.cashoutExternalVolume}, externalAmount)
- monitorTalerOut(0, "KUDOS:0")
monitorTalerIn(0, "KUDOS:0")
- monitorCashin(0, "FIAT:0")
- monitorCashout(0, "FIAT:0")
+ monitorTalerOut(0, "KUDOS:0")
+ monitorCashin(0, "KUDOS:0", "FIAT:0")
+ monitorCashout(0, "KUDOS:0", "FIAT:0")
+
+ addIncoming("KUDOS:3")
+ monitorTalerIn(1, "KUDOS:3")
+ addIncoming("KUDOS:7.6")
+ monitorTalerIn(2, "KUDOS:10.6")
+ addIncoming("KUDOS:12.3")
+ monitorTalerIn(3, "KUDOS:22.9")
transfer("KUDOS:10.0")
monitorTalerOut(1, "KUDOS:10.0")
@@ -73,24 +109,24 @@ class StatsTest {
transfer("KUDOS:42")
monitorTalerOut(3, "KUDOS:82.5")
- addIncoming("KUDOS:3")
- monitorTalerIn(1, "KUDOS:3")
- addIncoming("KUDOS:7.6")
- monitorTalerIn(2, "KUDOS:10.6")
- addIncoming("KUDOS:12.3")
- monitorTalerIn(3, "KUDOS:22.9")
+ cashin("FIAT:10")
+ monitorCashin(1, "KUDOS:7.98", "FIAT:10")
+ cashin("FIAT:20")
+ monitorCashin(2, "KUDOS:23.96", "FIAT:30")
+ cashin("FIAT:40")
+ monitorCashin(3, "KUDOS:55.94", "FIAT:70")
cashout("KUDOS:3")
- monitorCashout(1, "FIAT:3.747")
+ monitorCashout(1, "KUDOS:3", "FIAT:3.747")
cashout("KUDOS:7.6")
- monitorCashout(2, "FIAT:13.244")
+ monitorCashout(2, "KUDOS:10.6", "FIAT:13.244")
cashout("KUDOS:12.3")
- monitorCashout(3, "FIAT:28.616")
+ monitorCashout(3, "KUDOS:22.9", "FIAT:28.616")
- monitorTalerOut(3, "KUDOS:82.5")
monitorTalerIn(3, "KUDOS:22.9")
- monitorCashin(0, "FIAT:0")
- monitorCashout(3, "FIAT:28.616")
+ monitorTalerOut(3, "KUDOS:82.5")
+ monitorCashin(3, "KUDOS:55.94", "FIAT:70")
+ monitorCashout(3, "KUDOS:22.9", "FIAT:28.616")
}
@Test
@@ -98,7 +134,7 @@ class StatsTest {
db.conn { conn ->
suspend fun register(now: OffsetDateTime, amount: TalerAmount) {
val stmt = conn.prepareStatement(
- "CALL stats_register_payment('taler_out', ?::timestamp, (?, ?)::taler_amount)"
+ "CALL stats_register_payment('taler_out', ?::timestamp, (?, ?)::taler_amount, null)"
)
stmt.setObject(1, now)
stmt.setLong(2, amount.value)
diff --git a/database-versioning/libeufin-bank-0001.sql b/database-versioning/libeufin-bank-0001.sql
@@ -254,9 +254,11 @@ CREATE TABLE IF NOT EXISTS bank_stats (
timeframe stat_timeframe_enum NOT NULL
,start_time timestamp NOT NULL
,cashin_count BIGINT NOT NULL DEFAULT 0
- ,cashin_volume taler_amount NOT NULL DEFAULT (0, 0)
+ ,cashin_internal_volume taler_amount NOT NULL DEFAULT (0, 0)
+ ,cashin_external_volume taler_amount NOT NULL DEFAULT (0, 0)
,cashout_count BIGINT NOT NULL DEFAULT 0
- ,cashout_volume taler_amount NOT NULL DEFAULT (0, 0)
+ ,cashout_internal_volume taler_amount NOT NULL DEFAULT (0, 0)
+ ,cashout_external_volume taler_amount NOT NULL DEFAULT (0, 0)
,taler_in_count BIGINT NOT NULL DEFAULT 0
,taler_in_volume taler_amount NOT NULL DEFAULT (0, 0)
,taler_out_count BIGINT NOT NULL DEFAULT 0
@@ -268,9 +270,11 @@ COMMENT ON TABLE bank_stats IS 'Stores statistics about the bank usage.';
COMMENT ON COLUMN bank_stats.timeframe IS 'particular timeframe that this row accounts for';
COMMENT ON COLUMN bank_stats.start_time IS 'timestamp of the start of the timeframe that this row accounts for, truncated according to the precision of the timeframe';
COMMENT ON COLUMN bank_stats.cashin_count IS 'how many cashin operations took place in the timeframe';
-COMMENT ON COLUMN bank_stats.cashin_volume IS 'how much fiat currency was cashed in in the timeframe';
+COMMENT ON COLUMN bank_stats.cashin_internal_volume IS 'how much internal currency was cashed in in the timeframe';
+COMMENT ON COLUMN bank_stats.cashin_external_volume IS 'how much external currency was cashed in in the timeframe';
COMMENT ON COLUMN bank_stats.cashout_count IS 'how many cashout operations took place in the timeframe';
-COMMENT ON COLUMN bank_stats.cashout_volume IS 'how much fiat currency was payed by the bank to customers in the timeframe';
+COMMENT ON COLUMN bank_stats.cashout_internal_volume IS 'how much internal currency was payed by the bank to customers in the timeframe';
+COMMENT ON COLUMN bank_stats.cashout_external_volume IS 'how much external currency was payed by the bank to customers in the timeframe';
COMMENT ON COLUMN bank_stats.taler_out_count IS 'how many internal payments were made by a Taler exchange';
COMMENT ON COLUMN bank_stats.taler_out_volume IS 'how much internal currency was paid by a Taler exchange';
COMMENT ON COLUMN bank_stats.taler_in_count IS 'how many internal payments were made to a Taler exchange';
diff --git a/database-versioning/libeufin-bank-procedures.sql b/database-versioning/libeufin-bank-procedures.sql
@@ -283,7 +283,7 @@ INSERT
SELECT (amount).val, (amount).frac, bank_account_id
INTO local_amount.val, local_amount.frac, local_bank_account_id
FROM bank_account_transactions WHERE bank_transaction_id=in_tx_row_id;
-CALL stats_register_payment('taler_out', now()::TIMESTAMP, local_amount);
+CALL stats_register_payment('taler_out', now()::TIMESTAMP, local_amount, null);
-- notify new transaction
PERFORM pg_notify('outgoing_tx', local_bank_account_id || ' ' || in_tx_row_id);
END $$;
@@ -312,7 +312,7 @@ INSERT
SELECT (amount).val, (amount).frac, bank_account_id
INTO local_amount.val, local_amount.frac, local_bank_account_id
FROM bank_account_transactions WHERE bank_transaction_id=in_tx_row_id;
-CALL stats_register_payment('taler_in', now()::TIMESTAMP, local_amount);
+CALL stats_register_payment('taler_in', now()::TIMESTAMP, local_amount, null);
-- notify new transaction
PERFORM pg_notify('incoming_tx', local_bank_account_id || ' ' || in_tx_row_id);
END $$;
@@ -1000,6 +1000,66 @@ WHERE bank_account_id=in_creditor_account_id;
PERFORM pg_notify('bank_tx', in_debtor_account_id || ' ' || in_creditor_account_id || ' ' || out_debit_row_id || ' ' || out_credit_row_id);
END $$;
+CREATE OR REPLACE FUNCTION cashin(
+ IN in_now_date BIGINT,
+ IN in_payto_uri TEXT,
+ IN in_amount taler_amount,
+ IN in_subject TEXT,
+ -- Error status
+ OUT out_no_account BOOLEAN,
+ OUT out_too_small BOOLEAN,
+ OUT out_balance_insufficient BOOLEAN
+)
+LANGUAGE plpgsql AS $$
+DECLARE
+ converted_amount taler_amount;
+ admin_account_id BIGINT;
+ wallet_account_id BIGINT;
+BEGIN
+-- Recover account info
+SELECT bank_account_id
+ INTO wallet_account_id
+ FROM bank_accounts
+ WHERE internal_payto_uri = in_payto_uri;
+
+-- Retrieve admin account id
+SELECT bank_account_id
+ INTO admin_account_id
+ FROM bank_accounts
+ JOIN customers
+ ON customer_id=owning_customer_id
+ WHERE login = 'admin';
+
+-- Perform conversion
+SELECT (to_amount).val, (to_amount).frac, too_small
+ INTO converted_amount.val, converted_amount.frac, out_too_small
+ FROM conversion_to(in_amount, 'buy'::text);
+IF out_too_small THEN
+ RETURN;
+END IF;
+
+-- Perform bank wire transfer
+SELECT transfer.out_balance_insufficient
+INTO out_balance_insufficient
+FROM bank_wire_transfer(
+ wallet_account_id,
+ admin_account_id,
+ in_subject,
+ converted_amount,
+ in_now_date,
+ 'not-used',
+ 'not-used',
+ 'not-used'
+) as transfer;
+IF out_balance_insufficient THEN
+ RETURN;
+END IF;
+
+-- update stats
+CALL stats_register_payment('cashin', now()::TIMESTAMP, converted_amount, in_amount);
+END $$;
+
+
CREATE OR REPLACE FUNCTION cashout_create(
IN in_account_username TEXT,
IN in_request_uid BYTEA,
@@ -1194,7 +1254,7 @@ UPDATE cashout_operations
WHERE cashout_id=in_cashout_id;
-- update stats
-CALL stats_register_payment('cashout', now()::TIMESTAMP, amount_credit_local);
+CALL stats_register_payment('cashout', now()::TIMESTAMP, amount_debit_local, amount_credit_local);
END $$;
CREATE OR REPLACE FUNCTION challenge_create (
@@ -1289,9 +1349,11 @@ CREATE OR REPLACE FUNCTION stats_get_frame(
IN in_timeframe stat_timeframe_enum,
IN which INTEGER,
OUT cashin_count BIGINT,
- OUT cashin_volume taler_amount,
+ OUT cashin_internal_volume taler_amount,
+ OUT cashin_external_volume taler_amount,
OUT cashout_count BIGINT,
- OUT cashout_volume taler_amount,
+ OUT cashout_internal_volume taler_amount,
+ OUT cashout_external_volume taler_amount,
OUT taler_in_count BIGINT,
OUT taler_in_volume taler_amount,
OUT taler_out_count BIGINT,
@@ -1310,11 +1372,15 @@ BEGIN
END;
SELECT
s.cashin_count
- ,(s.cashin_volume).val
- ,(s.cashin_volume).frac
+ ,(s.cashin_internal_volume).val
+ ,(s.cashin_internal_volume).frac
+ ,(s.cashin_external_volume).val
+ ,(s.cashin_external_volume).frac
,s.cashout_count
- ,(s.cashout_volume).val
- ,(s.cashout_volume).frac
+ ,(s.cashout_internal_volume).val
+ ,(s.cashout_internal_volume).frac
+ ,(s.cashout_external_volume).val
+ ,(s.cashout_external_volume).frac
,s.taler_in_count
,(s.taler_in_volume).val
,(s.taler_in_volume).frac
@@ -1323,11 +1389,15 @@ BEGIN
,(s.taler_out_volume).frac
INTO
cashin_count
- ,cashin_volume.val
- ,cashin_volume.frac
+ ,cashin_internal_volume.val
+ ,cashin_internal_volume.frac
+ ,cashin_external_volume.val
+ ,cashin_external_volume.frac
,cashout_count
- ,cashout_volume.val
- ,cashout_volume.frac
+ ,cashout_internal_volume.val
+ ,cashout_internal_volume.frac
+ ,cashout_external_volume.val
+ ,cashout_external_volume.frac
,taler_in_count
,taler_in_volume.val
,taler_in_volume.frac
@@ -1342,24 +1412,38 @@ END $$;
CREATE OR REPLACE PROCEDURE stats_register_payment(
IN name TEXT,
IN now TIMESTAMP,
- IN amount taler_amount
+ IN internal_amount taler_amount,
+ IN external_amount taler_amount
)
LANGUAGE plpgsql AS $$
DECLARE
frame stat_timeframe_enum;
query TEXT;
BEGIN
- query = format('INSERT INTO bank_stats AS s '
- '(timeframe, start_time, %1$I_count, %1$I_volume) '
- 'VALUES ($1, $2, 1, $3) '
- 'ON CONFLICT (timeframe, start_time) DO UPDATE '
- 'SET %1$I_count=s.%1$I_count+1, '
- ' %1$I_volume=(SELECT amount_add(s.%1$I_volume, $3))',
- name);
-
- FOREACH frame IN ARRAY enum_range(null::stat_timeframe_enum) LOOP
- EXECUTE query USING frame, date_trunc(frame::text, now), amount;
- END LOOP;
+ IF external_amount IS NULL THEN
+ query = format('INSERT INTO bank_stats AS s '
+ '(timeframe, start_time, %1$I_count, %1$I_volume) '
+ 'VALUES ($1, $2, 1, $3) '
+ 'ON CONFLICT (timeframe, start_time) DO UPDATE '
+ 'SET %1$I_count=s.%1$I_count+1 '
+ ', %1$I_volume=(SELECT amount_add(s.%1$I_volume, $3))',
+ name);
+ FOREACH frame IN ARRAY enum_range(null::stat_timeframe_enum) LOOP
+ EXECUTE query USING frame, date_trunc(frame::text, now), internal_amount;
+ END LOOP;
+ ELSE
+ query = format('INSERT INTO bank_stats AS s '
+ '(timeframe, start_time, %1$I_count, %1$I_internal_volume, %1$I_external_volume) '
+ 'VALUES ($1, $2, 1, $3, $4)'
+ 'ON CONFLICT (timeframe, start_time) DO UPDATE '
+ 'SET %1$I_count=s.%1$I_count+1 '
+ ', %1$I_internal_volume=(SELECT amount_add(s.%1$I_internal_volume, $3))'
+ ', %1$I_external_volume=(SELECT amount_add(s.%1$I_external_volume, $4))',
+ name);
+ FOREACH frame IN ARRAY enum_range(null::stat_timeframe_enum) LOOP
+ EXECUTE query USING frame, date_trunc(frame::text, now), internal_amount, external_amount;
+ END LOOP;
+ END IF;
END $$;
CREATE OR REPLACE PROCEDURE config_set_amount(