exchange: landing-stats + no-terms stubs (TERMS_ETAG no-terms-v0)
This commit is contained in:
parent
b903fb525b
commit
027fb5bd5d
4 changed files with 594 additions and 0 deletions
15
configs/taler-exchange/terms-src/no-privacy-v0.en.rst
Normal file
15
configs/taler-exchange/terms-src/no-privacy-v0.en.rst
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
No Privacy Policy Required
|
||||||
|
==========================
|
||||||
|
|
||||||
|
This is an **experimental / exploration** GNU Taler exchange for **GOA**.
|
||||||
|
|
||||||
|
**No formal privacy policy is required** for this demo service.
|
||||||
|
|
||||||
|
Note (high level):
|
||||||
|
|
||||||
|
* Wire transfers via the regional bank may identify bank account holders.
|
||||||
|
* The exchange processes withdrawals, deposits, and related operations
|
||||||
|
as required by the GNU Taler protocol.
|
||||||
|
* Logs may be kept for operation and debugging.
|
||||||
|
|
||||||
|
Do not use this service if that is unacceptable.
|
||||||
16
configs/taler-exchange/terms-src/no-terms-v0.en.rst
Normal file
16
configs/taler-exchange/terms-src/no-terms-v0.en.rst
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
No Terms Required
|
||||||
|
=================
|
||||||
|
|
||||||
|
This is an **experimental / exploration** GNU Taler exchange for the
|
||||||
|
currency **GOA** (hacktivism.ch).
|
||||||
|
|
||||||
|
**No formal terms of service are required** to use this service.
|
||||||
|
|
||||||
|
By withdrawing or using GOA coins you acknowledge that:
|
||||||
|
|
||||||
|
* This service is for exploration and testing only.
|
||||||
|
* There is no guaranteed availability, support, or redemption.
|
||||||
|
* Operators may reset balances or change configuration without notice.
|
||||||
|
* Do not use real money you cannot afford to lose.
|
||||||
|
|
||||||
|
If you do not agree, do not use this exchange.
|
||||||
263
scripts/taler-exchange/install_no_terms.sh
Normal file
263
scripts/taler-exchange/install_no_terms.sh
Normal file
|
|
@ -0,0 +1,263 @@
|
||||||
|
#!/bin/bash
|
||||||
|
# Install "No Terms Required" ToS + privacy for the exchange (styled like merchant terms).
|
||||||
|
# Run as root inside the exchange container.
|
||||||
|
set -euo pipefail
|
||||||
|
export PATH="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin${PATH:+:$PATH}"
|
||||||
|
|
||||||
|
CONF="${TALER_EXCHANGE_CONFIG:-/etc/taler-exchange/taler-exchange.conf}"
|
||||||
|
DATA_HOME=$(taler-exchange-config -c "$CONF" -f -s PATHS -o TALER_DATA_HOME 2>/dev/null || true)
|
||||||
|
DATA_HOME="${DATA_HOME:-/var/lib/taler-exchange/}"
|
||||||
|
TERMS_DIR="${DATA_HOME%/}/terms"
|
||||||
|
LANG_DIR="$TERMS_DIR/en"
|
||||||
|
mkdir -p "$LANG_DIR"
|
||||||
|
|
||||||
|
# Bump when HTML/style changes (long Cache-Control on /terms).
|
||||||
|
TOS_ETAG="${TERMS_ETAG:-no-terms-v0}"
|
||||||
|
# Prefer Swiss FADP privacy install (install_swiss_privacy.sh); fallback etag here
|
||||||
|
PP_ETAG="${PRIVACY_ETAG:-exchange-pp-swiss-v0}"
|
||||||
|
|
||||||
|
# Shared palette with merchant-tos-dual / bank terms pages
|
||||||
|
COMMON_CSS='
|
||||||
|
:root { color-scheme: dark light; }
|
||||||
|
body {
|
||||||
|
font-family: system-ui, -apple-system, sans-serif;
|
||||||
|
max-width: 40rem; margin: 2rem auto; padding: 0 1.1rem 3rem;
|
||||||
|
line-height: 1.5; color: #e8e6e3; background: #1a1520;
|
||||||
|
}
|
||||||
|
h1 { font-size: 1.35rem; font-weight: 800; margin: 0 0 1rem; color: #f5f0ea; }
|
||||||
|
h2 { font-size: 1.05rem; margin: 1.4rem 0 0.5rem; color: #e8c878; }
|
||||||
|
p, li { font-size: 0.98rem; }
|
||||||
|
ul { padding-left: 1.2rem; }
|
||||||
|
code, a { color: #5eead4; }
|
||||||
|
a { text-decoration: none; }
|
||||||
|
a:hover { text-decoration: underline; }
|
||||||
|
.badge {
|
||||||
|
display: inline-block; font-size: 0.72rem; font-weight: 700;
|
||||||
|
letter-spacing: 0.06em; text-transform: uppercase;
|
||||||
|
color: #c4b5fd; border: 1px solid rgba(196,181,253,0.35);
|
||||||
|
border-radius: 999px; padding: 0.2rem 0.65rem; margin-bottom: 0.85rem;
|
||||||
|
}
|
||||||
|
.cur {
|
||||||
|
border-radius: 12px; padding: 0.75rem 0.9rem; margin: 0.85rem 0 1rem;
|
||||||
|
border: 1px solid rgba(255,255,255,0.1); background: rgba(0,0,0,0.25);
|
||||||
|
}
|
||||||
|
.cur strong { display: block; font-size: 1.05rem; margin-bottom: 0.25rem; color: #5eead4; }
|
||||||
|
.muted { color: #a39e98; font-size: 0.88rem; }
|
||||||
|
footer { margin-top: 2rem; font-size: 0.85rem; color: #a39e98; }
|
||||||
|
'
|
||||||
|
|
||||||
|
write_tos() {
|
||||||
|
local base="$1"
|
||||||
|
local title="No Terms Required"
|
||||||
|
local body_txt body_md
|
||||||
|
|
||||||
|
body_txt='No Terms Required.
|
||||||
|
|
||||||
|
This is an experimental / exploration GNU Taler exchange for the currency GOA (hacktivism.ch).
|
||||||
|
|
||||||
|
No formal terms of service are required to use this service.
|
||||||
|
|
||||||
|
By withdrawing or using GOA coins you acknowledge that:
|
||||||
|
- This service is for exploration and testing only.
|
||||||
|
- GOA is not legal tender and has no guaranteed real-world value or redemption.
|
||||||
|
- There is no guaranteed availability, support, or uptime.
|
||||||
|
- Operators may reset balances or change configuration without notice.
|
||||||
|
- Do not use real money you cannot afford to lose.
|
||||||
|
|
||||||
|
If you do not agree, do not use this exchange.
|
||||||
|
|
||||||
|
Related:
|
||||||
|
- Bank: https://bank.hacktivism.ch/intro/
|
||||||
|
- Merchant terms: https://taler.hacktivism.ch/terms
|
||||||
|
- Bank terms: https://bank.hacktivism.ch/terms
|
||||||
|
|
||||||
|
Privacy:
|
||||||
|
Processing under Swiss FADP (revDSG). What data is retained is listed at
|
||||||
|
https://exchange.hacktivism.ch/privacy
|
||||||
|
'
|
||||||
|
|
||||||
|
body_md='# No Terms Required
|
||||||
|
|
||||||
|
This is an **experimental / exploration** GNU Taler exchange for the currency **GOA** (hacktivism.ch).
|
||||||
|
|
||||||
|
**No formal terms of service** are required to use this service.
|
||||||
|
|
||||||
|
By withdrawing or using GOA coins you acknowledge that:
|
||||||
|
|
||||||
|
- This service is for exploration and testing only.
|
||||||
|
- GOA is not legal tender and has no guaranteed real-world value or redemption.
|
||||||
|
- There is no guaranteed availability, support, or uptime.
|
||||||
|
- Operators may reset balances or change configuration without notice.
|
||||||
|
- Do not use real money you cannot afford to lose.
|
||||||
|
|
||||||
|
If you do not agree, do not use this exchange.
|
||||||
|
|
||||||
|
## Related
|
||||||
|
|
||||||
|
- [Bank intro](https://bank.hacktivism.ch/intro/)
|
||||||
|
- [Bank terms](https://bank.hacktivism.ch/terms)
|
||||||
|
- [Merchant terms](https://taler.hacktivism.ch/terms)
|
||||||
|
- [Exchange privacy](https://exchange.hacktivism.ch/privacy)
|
||||||
|
|
||||||
|
## Privacy
|
||||||
|
|
||||||
|
Processing under Swiss FADP (revDSG). What data is retained is listed on
|
||||||
|
[exchange.hacktivism.ch/privacy](https://exchange.hacktivism.ch/privacy).
|
||||||
|
'
|
||||||
|
|
||||||
|
printf '%s\n' "$body_txt" >"$LANG_DIR/${base}.txt"
|
||||||
|
printf '%s\n' "$body_md" >"$LANG_DIR/${base}.md"
|
||||||
|
cat >"$LANG_DIR/${base}.html" <<HTML
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8"/>
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1"/>
|
||||||
|
<title>${title}</title>
|
||||||
|
<style>
|
||||||
|
${COMMON_CSS}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="badge">exchange.hacktivism.ch · GOA</div>
|
||||||
|
<h1>${title}</h1>
|
||||||
|
<p>This is an <strong>experimental / exploration</strong> GNU Taler exchange for the
|
||||||
|
currency <strong>GOA</strong> at <code>exchange.hacktivism.ch</code> (hacktivism.ch).</p>
|
||||||
|
<p><strong>No formal terms of service</strong> are required to use this service.</p>
|
||||||
|
|
||||||
|
<div class="cur">
|
||||||
|
<strong>GOA · explorational</strong>
|
||||||
|
Not legal tender. No guaranteed real-world value, redemption, or convertibility.
|
||||||
|
Issued only for exploration and testing on this stack.
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h2>By withdrawing or using GOA coins you acknowledge</h2>
|
||||||
|
<ul>
|
||||||
|
<li>This service is for exploration and testing only.</li>
|
||||||
|
<li>There is no guaranteed availability, support, or uptime.</li>
|
||||||
|
<li>Operators may reset balances or change configuration without notice.</li>
|
||||||
|
<li>Do not use real money you cannot afford to lose.</li>
|
||||||
|
<li>Software is provided as-is, without warranty.</li>
|
||||||
|
</ul>
|
||||||
|
<p>If you do not agree, do not use this exchange.</p>
|
||||||
|
|
||||||
|
<h2>Related</h2>
|
||||||
|
<ul>
|
||||||
|
<li><a href="https://bank.hacktivism.ch/intro/">Bank intro</a></li>
|
||||||
|
<li><a href="https://bank.hacktivism.ch/terms">Bank terms</a></li>
|
||||||
|
<li><a href="https://taler.hacktivism.ch/terms">Merchant terms</a></li>
|
||||||
|
<li><a href="https://exchange.hacktivism.ch/intro/">Exchange intro</a></li>
|
||||||
|
<li><a href="https://exchange.hacktivism.ch/privacy">Exchange privacy</a></li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<h2>Privacy</h2>
|
||||||
|
<p class="muted">Processing under Swiss FADP (revDSG). What data is retained
|
||||||
|
(reserves, wire-in, coins, logs, …) is listed on
|
||||||
|
<a href="https://exchange.hacktivism.ch/privacy">/privacy</a>.</p>
|
||||||
|
<footer class="muted">Version ${base} · hacktivism.ch</footer>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
HTML
|
||||||
|
}
|
||||||
|
|
||||||
|
write_pp() {
|
||||||
|
# Legacy short PP only if not using install_swiss_privacy.sh etag
|
||||||
|
local base="$1"
|
||||||
|
local title="Privacy notice · GOA Exchange"
|
||||||
|
local body_txt body_md
|
||||||
|
|
||||||
|
body_txt='No Privacy Policy Required.
|
||||||
|
|
||||||
|
This is an experimental / exploration GNU Taler exchange for GOA.
|
||||||
|
|
||||||
|
No formal privacy policy is required for this demo service.
|
||||||
|
|
||||||
|
High-level notes:
|
||||||
|
- Wire transfers via the regional bank may identify bank account holders.
|
||||||
|
- The exchange processes withdrawals, deposits, and related protocol operations.
|
||||||
|
- Logs may be kept for operation and debugging.
|
||||||
|
|
||||||
|
Do not use this service if that is unacceptable.
|
||||||
|
|
||||||
|
Related:
|
||||||
|
- Exchange terms: https://exchange.hacktivism.ch/terms
|
||||||
|
'
|
||||||
|
|
||||||
|
body_md='# No Privacy Policy Required
|
||||||
|
|
||||||
|
This is an **experimental / exploration** GNU Taler exchange for **GOA**.
|
||||||
|
|
||||||
|
**No formal privacy policy** is required for this demo service.
|
||||||
|
|
||||||
|
## High-level notes
|
||||||
|
|
||||||
|
- Wire transfers via the regional bank may identify bank account holders.
|
||||||
|
- The exchange processes withdrawals, deposits, and related protocol operations.
|
||||||
|
- Logs may be kept for operation and debugging.
|
||||||
|
|
||||||
|
Do not use this service if that is unacceptable.
|
||||||
|
|
||||||
|
## Related
|
||||||
|
|
||||||
|
- [Exchange terms](https://exchange.hacktivism.ch/terms)
|
||||||
|
'
|
||||||
|
|
||||||
|
printf '%s\n' "$body_txt" >"$LANG_DIR/${base}.txt"
|
||||||
|
printf '%s\n' "$body_md" >"$LANG_DIR/${base}.md"
|
||||||
|
cat >"$LANG_DIR/${base}.html" <<HTML
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8"/>
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1"/>
|
||||||
|
<title>${title}</title>
|
||||||
|
<style>
|
||||||
|
${COMMON_CSS}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="badge">exchange.hacktivism.ch · privacy</div>
|
||||||
|
<h1>${title}</h1>
|
||||||
|
<p>This is an <strong>experimental / exploration</strong> GNU Taler exchange for
|
||||||
|
<strong>GOA</strong> at <code>exchange.hacktivism.ch</code>.</p>
|
||||||
|
<p><strong>No formal privacy policy</strong> is required for this demo service.</p>
|
||||||
|
|
||||||
|
<h2>High-level notes</h2>
|
||||||
|
<ul>
|
||||||
|
<li>Wire transfers via the regional bank may identify bank account holders.</li>
|
||||||
|
<li>The exchange processes withdrawals, deposits, and related protocol operations.</li>
|
||||||
|
<li>Logs may be kept for operation and debugging.</li>
|
||||||
|
</ul>
|
||||||
|
<p>Do not use this service if that is unacceptable.</p>
|
||||||
|
|
||||||
|
<h2>Related</h2>
|
||||||
|
<ul>
|
||||||
|
<li><a href="https://exchange.hacktivism.ch/terms">Exchange terms</a></li>
|
||||||
|
<li><a href="https://bank.hacktivism.ch/terms">Bank terms</a></li>
|
||||||
|
<li><a href="https://taler.hacktivism.ch/terms">Merchant terms</a></li>
|
||||||
|
</ul>
|
||||||
|
<footer class="muted">Version ${base}</footer>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
HTML
|
||||||
|
}
|
||||||
|
|
||||||
|
write_tos "$TOS_ETAG"
|
||||||
|
# Prefer Swiss FADP privacy installer when present (precise retention tables)
|
||||||
|
if [ -x /usr/local/bin/install_swiss_privacy.sh ]; then
|
||||||
|
PRIVACY_ETAG="$PP_ETAG" /usr/local/bin/install_swiss_privacy.sh
|
||||||
|
else
|
||||||
|
write_pp "$PP_ETAG"
|
||||||
|
fi
|
||||||
|
|
||||||
|
chmod -R a+rX "$TERMS_DIR"
|
||||||
|
if id taler-exchange-httpd >/dev/null 2>&1; then
|
||||||
|
chown -R taler-exchange-httpd: "$TERMS_DIR" 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Installed under $LANG_DIR:"
|
||||||
|
ls -la "$LANG_DIR"/${TOS_ETAG}.* "$LANG_DIR"/${PP_ETAG}.* 2>/dev/null || ls -la "$LANG_DIR"
|
||||||
|
echo "Config should set:"
|
||||||
|
echo " TERMS_ETAG = ${TOS_ETAG}"
|
||||||
|
echo " PRIVACY_ETAG = ${PP_ETAG}"
|
||||||
|
echo " TERMS_DIR / PRIVACY_DIR = \${TALER_DATA_HOME}terms/"
|
||||||
300
scripts/taler-exchange/landing-stats-exchange.sh
Normal file
300
scripts/taler-exchange/landing-stats-exchange.sh
Normal file
|
|
@ -0,0 +1,300 @@
|
||||||
|
#!/bin/bash
|
||||||
|
# Run INSIDE taler-hacktivism-exchange-ansible.
|
||||||
|
# Writes /var/www/exchange-landing/stats.json
|
||||||
|
#
|
||||||
|
# Data lives in Postgres DB taler-exchange, schema exchange.*
|
||||||
|
# (reserves, reserves_in, known_coins, withdraw, denominations, …)
|
||||||
|
#
|
||||||
|
# IMPORTANT: never use psql -F$'\t' -v ON_ERROR_STOP=1
|
||||||
|
# If the tab arg is lost, -F eats -v and ON_ERROR_STOP=1 becomes the
|
||||||
|
# *username* → peer auth fails → silent empty counts (all zeros).
|
||||||
|
# Never overwrite stats.json on failure — write stats-run.json instead.
|
||||||
|
set -euo pipefail
|
||||||
|
export TZ="${TZ:-Europe/Zurich}"
|
||||||
|
export PATH="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin${PATH:+:$PATH}"
|
||||||
|
LANDING_DIR="${LANDING_DIR:-/var/www/exchange-landing}"
|
||||||
|
OUT="$LANDING_DIR/stats.json"
|
||||||
|
RUN="$LANDING_DIR/stats-run.json"
|
||||||
|
TMP="${OUT}.tmp.$$"
|
||||||
|
DB="${EXCHANGE_DB:-taler-exchange}"
|
||||||
|
BASE_URL="${EXCHANGE_BASE_URL:-https://exchange.hacktivism.ch}"
|
||||||
|
ERRLOG="${LANDING_STATS_ERRLOG:-/var/log/landing-stats-exchange.err}"
|
||||||
|
mkdir -p "$LANDING_DIR"
|
||||||
|
|
||||||
|
now_iso() { date +%Y-%m-%dT%H:%M%z | sed -E 's/([+-][0-9]{2})([0-9]{2})$/\1:\2/'; }
|
||||||
|
now_human() { date +"%Y-%m-%d %H:%M %Z"; }
|
||||||
|
json_str() {
|
||||||
|
printf '"%s"' "$(printf '%s' "$1" | sed 's/\\/\\\\/g; s/"/\\"/g' | tr '\n\r\t' ' ')"
|
||||||
|
}
|
||||||
|
|
||||||
|
write_run() {
|
||||||
|
local ok_json="$1" msg="${2:-}"
|
||||||
|
cat >"$RUN" <<EOF
|
||||||
|
{
|
||||||
|
"ok": ${ok_json},
|
||||||
|
"at": $(json_str "$(now_iso)"),
|
||||||
|
"at_human": $(json_str "$(now_human)"),
|
||||||
|
"error": $( [ -n "$msg" ] && json_str "$msg" || echo null )
|
||||||
|
}
|
||||||
|
EOF
|
||||||
|
}
|
||||||
|
|
||||||
|
abort() {
|
||||||
|
write_run false "$*"
|
||||||
|
printf '%s\n' "abort exchange-stats: $*" | tee -a "$ERRLOG" >&2
|
||||||
|
rm -f "$TMP" 2>/dev/null || true
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# Prefer runuser; fall back to su. Always absolute-ish via PATH.
|
||||||
|
as_postgres() {
|
||||||
|
if command -v runuser >/dev/null 2>&1; then
|
||||||
|
runuser -u postgres -- "$@"
|
||||||
|
elif command -v su >/dev/null 2>&1; then
|
||||||
|
su -s /bin/bash postgres -c "$*"
|
||||||
|
else
|
||||||
|
return 127
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Single value or one row with '|' separators (no -F, no -v flags).
|
||||||
|
psqlq() {
|
||||||
|
local sql="$1" out ec
|
||||||
|
set +e
|
||||||
|
out=$(as_postgres psql -d "$DB" -At -c "$sql" 2>>"$ERRLOG")
|
||||||
|
ec=$?
|
||||||
|
set -e
|
||||||
|
if [ "$ec" -ne 0 ]; then
|
||||||
|
abort "psql failed (ec=$ec): ${sql:0:120}"
|
||||||
|
fi
|
||||||
|
printf '%s' "$out"
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt_goa() {
|
||||||
|
local v="${1:-0}" f="${2:-0}"
|
||||||
|
v=${v//[^0-9-]/}; f=${f//[^0-9-]/}; v=${v:-0}; f=${f:-0}
|
||||||
|
if [ "$f" = "0" ] || [ -z "$f" ]; then
|
||||||
|
printf 'GOA:%s' "$v"
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
awk -v v="$v" -v f="$f" 'BEGIN{
|
||||||
|
frac = sprintf("%08d", f+0); sub(/0+$/, "", frac)
|
||||||
|
if (frac == "") printf "GOA:%d", v+0
|
||||||
|
else printf "GOA:%d.%s", v+0, frac
|
||||||
|
}'
|
||||||
|
}
|
||||||
|
|
||||||
|
q_count() {
|
||||||
|
local n
|
||||||
|
n=$(psqlq "SELECT count(*)::text FROM $1;")
|
||||||
|
# strip whitespace/newlines
|
||||||
|
n=$(printf '%s' "$n" | tr -d '[:space:]')
|
||||||
|
[[ "$n" =~ ^[0-9]+$ ]] || abort "bad count for $1: '$n'"
|
||||||
|
printf '%s' "$n"
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- preflight ---
|
||||||
|
command -v psql >/dev/null 2>&1 || abort "psql not in PATH ($PATH)"
|
||||||
|
command -v runuser >/dev/null 2>&1 || command -v su >/dev/null 2>&1 || abort "neither runuser nor su in PATH"
|
||||||
|
PROBE=$(psqlq "SELECT 1;")
|
||||||
|
[ "$PROBE" = "1" ] || abort "postgres not reachable (SELECT 1 → '$PROBE')"
|
||||||
|
|
||||||
|
# --- coin lifecycle ---
|
||||||
|
KNOWN_COINS=$(q_count "exchange.known_coins")
|
||||||
|
COINS_LIVE=$(psqlq "SELECT count(*)::text FROM exchange.known_coins WHERE (remaining).val > 0 OR (remaining).frac > 0;")
|
||||||
|
COINS_LIVE=$(printf '%s' "${COINS_LIVE:-0}" | tr -d '[:space:]')
|
||||||
|
COINS_LIVE=${COINS_LIVE:-0}
|
||||||
|
COINS_SPENT=$(( KNOWN_COINS - COINS_LIVE ))
|
||||||
|
|
||||||
|
REM_ROW=$(psqlq "SELECT coalesce(sum((remaining).val),0)::text || '|' || coalesce(sum((remaining).frac),0)::text FROM exchange.known_coins;")
|
||||||
|
IFS='|' read -r REM_VAL REM_FRAC <<<"${REM_ROW:-0|0}"
|
||||||
|
REMAINING_AMT=$(fmt_goa "${REM_VAL:-0}" "${REM_FRAC:-0}")
|
||||||
|
|
||||||
|
DENOMS_TOTAL=$(q_count "exchange.denominations")
|
||||||
|
DENOM_VALUES=$(psqlq "SELECT count(DISTINCT ((coin).val, (coin).frac))::text FROM exchange.denominations;")
|
||||||
|
DENOM_VALUES=$(printf '%s' "${DENOM_VALUES:-0}" | tr -d '[:space:]')
|
||||||
|
|
||||||
|
NOW_US=$(date +%s)000000
|
||||||
|
DENOMS_WITHDRAWABLE=$(psqlq "SELECT count(*)::text FROM exchange.denominations WHERE valid_from <= ${NOW_US} AND expire_withdraw > ${NOW_US};")
|
||||||
|
DENOMS_WITHDRAWABLE=$(printf '%s' "${DENOMS_WITHDRAWABLE:-0}" | tr -d '[:space:]')
|
||||||
|
|
||||||
|
RESERVES=$(q_count "exchange.reserves")
|
||||||
|
RESERVES_IN=$(q_count "exchange.reserves_in")
|
||||||
|
WIN_ROW=$(psqlq "SELECT coalesce(sum((credit).val),0)::text || '|' || coalesce(sum((credit).frac),0)::text FROM exchange.reserves_in;")
|
||||||
|
IFS='|' read -r WIN_VAL WIN_FRAC <<<"${WIN_ROW:-0|0}"
|
||||||
|
WIRE_IN_AMT=$(fmt_goa "${WIN_VAL:-0}" "${WIN_FRAC:-0}")
|
||||||
|
|
||||||
|
WITHDRAW_OPS=$(q_count "exchange.withdraw")
|
||||||
|
WOUT_ROW=$(psqlq "SELECT coalesce(sum((amount_with_fee).val),0)::text || '|' || coalesce(sum((amount_with_fee).frac),0)::text FROM exchange.withdraw;")
|
||||||
|
IFS='|' read -r WO_VAL WO_FRAC <<<"${WOUT_ROW:-0|0}"
|
||||||
|
WITHDRAW_AMT=$(fmt_goa "${WO_VAL:-0}" "${WO_FRAC:-0}")
|
||||||
|
|
||||||
|
REFRESH_OPS=$(q_count "exchange.refresh")
|
||||||
|
RECOUP=$(q_count "exchange.recoup")
|
||||||
|
REFUNDS=$(q_count "exchange.refunds")
|
||||||
|
COIN_DEPOSITS=$(q_count "exchange.coin_deposits")
|
||||||
|
BATCH_DEPOSITS=$(q_count "exchange.batch_deposits")
|
||||||
|
WIRE_OUT=$(q_count "exchange.wire_out")
|
||||||
|
COIN_HISTORY=$(q_count "exchange.coin_history")
|
||||||
|
WIRE_ACCTS=$(q_count "exchange.wire_accounts")
|
||||||
|
|
||||||
|
# known coins by denom value (pipe-separated)
|
||||||
|
BY_DENOM_TSV=$(psqlq "
|
||||||
|
SELECT (d.coin).val::text || '|' || (d.coin).frac::text || '|' || count(*)::text || '|' ||
|
||||||
|
count(*) FILTER (WHERE (k.remaining).val > 0 OR (k.remaining).frac > 0)::text
|
||||||
|
FROM exchange.known_coins k
|
||||||
|
JOIN exchange.denominations d ON d.denominations_serial = k.denominations_serial
|
||||||
|
GROUP BY (d.coin).val, (d.coin).frac
|
||||||
|
ORDER BY (d.coin).val, (d.coin).frac;
|
||||||
|
")
|
||||||
|
|
||||||
|
BY_DENOM_JSON="["
|
||||||
|
bf=1
|
||||||
|
while IFS='|' read -r dv df cnt live; do
|
||||||
|
[ -z "${dv:-}" ] && continue
|
||||||
|
amt=$(fmt_goa "$dv" "$df")
|
||||||
|
if [ "$bf" = 1 ]; then bf=0; else BY_DENOM_JSON="${BY_DENOM_JSON},"; fi
|
||||||
|
BY_DENOM_JSON="${BY_DENOM_JSON}
|
||||||
|
{\"value\": $(json_str "$amt"), \"coins\": ${cnt:-0}, \"live\": ${live:-0}}"
|
||||||
|
done <<<"$BY_DENOM_TSV"
|
||||||
|
BY_DENOM_JSON="${BY_DENOM_JSON}
|
||||||
|
]"
|
||||||
|
|
||||||
|
LADDER_TSV=$(psqlq "
|
||||||
|
SELECT (coin).val::text || '|' || (coin).frac::text || '|' || count(*)::text
|
||||||
|
FROM exchange.denominations
|
||||||
|
GROUP BY (coin).val, (coin).frac
|
||||||
|
ORDER BY (coin).val, (coin).frac;
|
||||||
|
")
|
||||||
|
LADDER_JSON="["
|
||||||
|
lf=1
|
||||||
|
while IFS='|' read -r dv df nkeys; do
|
||||||
|
[ -z "${dv:-}" ] && continue
|
||||||
|
amt=$(fmt_goa "$dv" "$df")
|
||||||
|
if [ "$lf" = 1 ]; then lf=0; else LADDER_JSON="${LADDER_JSON},"; fi
|
||||||
|
LADDER_JSON="${LADDER_JSON}
|
||||||
|
{\"value\": $(json_str "$amt"), \"keys\": ${nkeys:-0}}"
|
||||||
|
done <<<"$LADDER_TSV"
|
||||||
|
LADDER_JSON="${LADDER_JSON}
|
||||||
|
]"
|
||||||
|
|
||||||
|
# recent wire-in / withdraw activity (no personal names — reserve_pub hex truncated)
|
||||||
|
RECENT_JSON="["
|
||||||
|
rf=1
|
||||||
|
while IFS='|' read -r ts val frac; do
|
||||||
|
[ -z "${ts:-}" ] && continue
|
||||||
|
sec=$(awk -v t="$ts" 'BEGIN{printf "%d", int(t/1000000)}')
|
||||||
|
human=$(date -d "@${sec}" +"%Y-%m-%d %H:%M %Z" 2>/dev/null || echo "$sec")
|
||||||
|
amt=$(fmt_goa "${val:-0}" "${frac:-0}")
|
||||||
|
if [ "$rf" = 1 ]; then rf=0; else RECENT_JSON="${RECENT_JSON},"; fi
|
||||||
|
RECENT_JSON="${RECENT_JSON}
|
||||||
|
{\"kind\": \"wire_in\", \"amount\": $(json_str "$amt"), \"ts_human\": $(json_str "$human"), \"ts_us\": ${ts:-0}}"
|
||||||
|
done < <(psqlq "
|
||||||
|
SELECT execution_date::text || '|' || (credit).val::text || '|' || (credit).frac::text
|
||||||
|
FROM exchange.reserves_in
|
||||||
|
ORDER BY execution_date DESC
|
||||||
|
LIMIT 8;
|
||||||
|
")
|
||||||
|
while IFS='|' read -r ts val frac; do
|
||||||
|
[ -z "${ts:-}" ] && continue
|
||||||
|
sec=$(awk -v t="$ts" 'BEGIN{printf "%d", int(t/1000000)}')
|
||||||
|
human=$(date -d "@${sec}" +"%Y-%m-%d %H:%M %Z" 2>/dev/null || echo "$sec")
|
||||||
|
amt=$(fmt_goa "${val:-0}" "${frac:-0}")
|
||||||
|
if [ "$rf" = 1 ]; then rf=0; else RECENT_JSON="${RECENT_JSON},"; fi
|
||||||
|
RECENT_JSON="${RECENT_JSON}
|
||||||
|
{\"kind\": \"withdraw\", \"amount\": $(json_str "$amt"), \"ts_human\": $(json_str "$human"), \"ts_us\": ${ts:-0}}"
|
||||||
|
done < <(psqlq "
|
||||||
|
SELECT execution_date::text || '|' || (amount_with_fee).val::text || '|' || (amount_with_fee).frac::text
|
||||||
|
FROM exchange.withdraw
|
||||||
|
ORDER BY execution_date DESC
|
||||||
|
LIMIT 6;
|
||||||
|
")
|
||||||
|
RECENT_JSON="${RECENT_JSON}
|
||||||
|
]"
|
||||||
|
|
||||||
|
# live performance
|
||||||
|
measure_ms() {
|
||||||
|
local url="$1" t
|
||||||
|
t=$(curl -sS -o /dev/null -m 8 -w '%{time_total}' "$url" 2>/dev/null || echo "")
|
||||||
|
[ -z "$t" ] && { echo "null"; return; }
|
||||||
|
awk -v t="$t" 'BEGIN{printf "%d", (t+0)*1000}'
|
||||||
|
}
|
||||||
|
KEYS_MS=$(measure_ms "${BASE_URL}/keys")
|
||||||
|
CONFIG_MS=$(measure_ms "${BASE_URL}/config")
|
||||||
|
KEYS_HTTP=$(curl -sS -o /dev/null -m 8 -w '%{http_code}' "${BASE_URL}/keys" 2>/dev/null || echo "000")
|
||||||
|
CONFIG_HTTP=$(curl -sS -o /dev/null -m 8 -w '%{http_code}' "${BASE_URL}/config" 2>/dev/null || echo "000")
|
||||||
|
LOADAVG=""
|
||||||
|
[ -r /proc/loadavg ] && LOADAVG=$(awk '{print $1","$2","$3}' /proc/loadavg)
|
||||||
|
|
||||||
|
WW="false"
|
||||||
|
pgrep -f taler-exchange-wirewatch >/dev/null 2>&1 && WW="true"
|
||||||
|
|
||||||
|
STAT_EVENTS=$(q_count "exchange.exchange_statistic_counter_event")
|
||||||
|
|
||||||
|
# Fail closed: zero denoms usually means broken query path (table always has keys)
|
||||||
|
if [ "${DENOMS_TOTAL:-0}" = "0" ]; then
|
||||||
|
abort "denominations count is 0 — refusing to publish (check peer auth / PATH)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
GEN=$(now_iso)
|
||||||
|
HUMAN=$(now_human)
|
||||||
|
num_or_null() { case "${1:-}" in ''|null) echo null ;; *) echo "$1" ;; esac; }
|
||||||
|
|
||||||
|
MEM_JSON='"container_rss_human": "—"'
|
||||||
|
MEM_HELPER="${MEM_HELPER:-/usr/local/lib/landing-mem-snapshot.sh}"
|
||||||
|
if [ -f "$MEM_HELPER" ]; then
|
||||||
|
# shellcheck disable=SC1090
|
||||||
|
. "$MEM_HELPER"
|
||||||
|
mem_snapshot_json || true
|
||||||
|
fi
|
||||||
|
|
||||||
|
cat >"$TMP" <<EOF
|
||||||
|
{
|
||||||
|
"ok": true,
|
||||||
|
"source": "exchange-db",
|
||||||
|
"schema": "exchange.* @ taler-exchange",
|
||||||
|
"focus": "coins",
|
||||||
|
"timezone": $(json_str "$TZ"),
|
||||||
|
"generated_at": $(json_str "$GEN"),
|
||||||
|
"generated_at_human": $(json_str "$HUMAN"),
|
||||||
|
"reserves": ${RESERVES:-0},
|
||||||
|
"wire_in_count": ${RESERVES_IN:-0},
|
||||||
|
"wire_in_amount": $(json_str "$WIRE_IN_AMT"),
|
||||||
|
"wire_out": ${WIRE_OUT:-0},
|
||||||
|
"wire_accounts": ${WIRE_ACCTS:-0},
|
||||||
|
"wirewatch_running": $WW,
|
||||||
|
"known_coins": ${KNOWN_COINS:-0},
|
||||||
|
"coins_live": ${COINS_LIVE:-0},
|
||||||
|
"coins_spent": ${COINS_SPENT:-0},
|
||||||
|
"coins_remaining_amount": $(json_str "$REMAINING_AMT"),
|
||||||
|
"withdraw_ops": ${WITHDRAW_OPS:-0},
|
||||||
|
"withdraw_amount": $(json_str "$WITHDRAW_AMT"),
|
||||||
|
"refresh_ops": ${REFRESH_OPS:-0},
|
||||||
|
"recoup": ${RECOUP:-0},
|
||||||
|
"refunds": ${REFUNDS:-0},
|
||||||
|
"coin_deposits": ${COIN_DEPOSITS:-0},
|
||||||
|
"batch_deposits": ${BATCH_DEPOSITS:-0},
|
||||||
|
"coin_history_events": ${COIN_HISTORY:-0},
|
||||||
|
"denominations": ${DENOMS_TOTAL:-0},
|
||||||
|
"denom_values": ${DENOM_VALUES:-0},
|
||||||
|
"denoms_withdrawable": ${DENOMS_WITHDRAWABLE:-0},
|
||||||
|
"by_denom": $BY_DENOM_JSON,
|
||||||
|
"denom_ladder": $LADDER_JSON,
|
||||||
|
"recent_activity": $RECENT_JSON,
|
||||||
|
"performance": {
|
||||||
|
"keys_http": $(json_str "$KEYS_HTTP"),
|
||||||
|
"keys_ms": $(num_or_null "$KEYS_MS"),
|
||||||
|
"config_http": $(json_str "$CONFIG_HTTP"),
|
||||||
|
"config_ms": $(num_or_null "$CONFIG_MS"),
|
||||||
|
"loadavg": $(json_str "${LOADAVG:-}"),
|
||||||
|
"statistic_counter_events": ${STAT_EVENTS:-0},
|
||||||
|
"memory": {
|
||||||
|
${MEM_JSON}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
EOF
|
||||||
|
grep -q '"ok": true' "$TMP" || abort "tmp json missing ok:true"
|
||||||
|
mv -f "$TMP" "$OUT"
|
||||||
|
write_run true
|
||||||
|
echo "ok exchange reserves=$RESERVES wire_in=$WIRE_IN_AMT coins=$KNOWN_COINS withdraw=$WITHDRAW_AMT denoms=$DENOMS_TOTAL keys_ms=$KEYS_MS -> $OUT"
|
||||||
Loading…
Add table
Add a link
Reference in a new issue