docs: new root as prior history lost (orphan + GC); ~215 commits not recoverable

This commit is contained in:
Hernâni Marques 2026-07-13 10:12:00 +02:00
commit 96961f23f5
268 changed files with 24161 additions and 0 deletions

View file

@ -0,0 +1,35 @@
# Exchange archive / bootstrap
## `exchange-bootstrap.sh` (greenfield only)
One-shot bootstrap for a **new** exchange container. **Not** used for daily ops.
Includes current **GOA exploration currency**:
- `exchange-overrides.conf` pattern (no `zz-*`)
- `PORT = 9011`, `SERVE = tcp`
- Unit names: Yotta…Atomic-GOA (scale 8…24)
- ATM `common_amounts` 10…1000
- Full 125 **coin** ladder (Micro-GOA … 10 GOA) in `conf.d/exchange-coins.conf`
After bootstrap:
1. `/root/start_base_services_for_taler_exchange.sh` (root)
2. `./start_exchange.sh` as `taler-exchange-httpd`
3. Offline: wire enable + denom/signkey sign for `/keys`
Live helpers: `../wire-enable-and-upload.sh`, `../offline-sign-upload-keys.sh`, `../start_wire_helpers.sh`
## Removed
| Script | Why |
|--------|-----|
| `exchange-start-all.sh` | Redundant with base + `start_exchange.sh` |
| old zz-hacktivism bootstrap | Wrong ports/currency; superseded |
## Daily layout (merchant model)
| Path | User |
|------|------|
| `/root/start_base_services_for_taler_exchange.sh` | root |
| `/usr/local/bin/start_exchange.sh` | `taler-exchange-httpd` |
| `/usr/local/bin/check_exchange-health.sh` | httpd / any |

View file

@ -0,0 +1,451 @@
#!/bin/bash
# Greenfield bootstrap for exchange.hacktivism.ch (GOA exploration currency).
# Run as root inside container. Not for daily ops — use start_base + start_exchange.
#
# Writes:
# /etc/taler-exchange/exchange-overrides.conf
# /etc/taler-exchange/conf.d/exchange-coins.conf
# Ensures main conf inlines exchange-overrides.conf (merchant pattern).
# Does NOT use conf.d/zz-*.
#
# After this: offline denom sign + wire still required for /keys withdraw.
set -euo pipefail
CONF=/etc/taler-exchange/taler-exchange.conf
OV=/etc/taler-exchange/exchange-overrides.conf
COINS=/etc/taler-exchange/conf.d/exchange-coins.conf
export DEBIAN_FRONTEND=noninteractive
if [ "$(id -u)" -ne 0 ]; then
echo "Run as root" >&2
exit 1
fi
echo "=== 1. postgres ==="
/etc/init.d/postgresql start
sleep 2
pg_isready || true
echo "=== 2. master key (offline user) ==="
mkdir -p /var/lib/taler-exchange/offline
chown -R taler-exchange-offline:taler-exchange-offline /var/lib/taler-exchange/offline
chown taler-exchange-offline:taler-exchange-offline /var/lib/taler-exchange 2>/dev/null || true
OUT=$(runuser -u taler-exchange-offline -- taler-exchange-offline -c "$CONF" setup 2>&1) || true
echo "$OUT"
MASTER_PUB=$(echo "$OUT" | grep -oE '[A-Z0-9]{40,}' | tail -1)
if [ -z "${MASTER_PUB:-}" ]; then
echo "ERROR: no master public key from offline setup" >&2
exit 1
fi
echo "MASTER_PUBLIC_KEY=$MASTER_PUB"
ATTR_KEY=$(openssl rand -hex 32)
SECRETS_DIR=/etc/taler-exchange/secrets
mkdir -p "$SECRETS_DIR"
cat >"$SECRETS_DIR/exchange-attribute-encryption.secret.conf" <<SEOF
# generated by exchange-bootstrap.sh
[exchange]
ATTRIBUTE_ENCRYPTION_KEY = ${ATTR_KEY}
SEOF
chmod 640 "$SECRETS_DIR/exchange-attribute-encryption.secret.conf"
chown root:taler-exchange-httpd "$SECRETS_DIR/exchange-attribute-encryption.secret.conf" 2>/dev/null || true
echo "=== 3. exchange-overrides.conf (site; not zz-*) ==="
# Ensure main conf inlines overrides (merchant pattern)
if ! grep -q 'exchange-overrides.conf' "$CONF" 2>/dev/null; then
printf '\n# Manual site overrides (merchant pattern)\n@inline@ exchange-overrides.conf\n' >>"$CONF"
fi
# Remove legacy zz drop-ins if present
rm -f /etc/taler-exchange/conf.d/zz-hacktivism.conf \
/etc/taler-exchange/conf.d/zz-hacktivism-coins.conf 2>/dev/null || true
cat >"$OV" <<EOF
# Manual site overrides for exchange.hacktivism.ch (GOA exploration currency).
# Same role as /etc/taler-merchant/merchant-overrides.conf on taler-hacktivism.
# Do not edit overrides.conf (tooling). Do not edit package conf.d defaults.
# No conf.d/zz-* drop-ins.
[exchange]
CURRENCY = GOA
CURRENCY_ROUND_UNIT = GOA:0.00000001
TINY_AMOUNT = GOA:0.00000001
DEFAULT_P2P_EXPIRATION = 14 days
BASE_URL = https://exchange.hacktivism.ch/
SERVE = tcp
PORT = 9011
MASTER_PUBLIC_KEY = ${MASTER_PUB}
# ATTRIBUTE_ENCRYPTION_KEY via @inline-secret@ (see secrets/)
@inline-secret@ exchange-attribute-encryption ../secrets/exchange-attribute-encryption.secret.conf
[currency-goa]
ENABLED = YES
name = "GOA exploration currency"
code = GOA
# SI display units. taler-exchange rejects scale keys outside [-8, 24] (no Ronna/Quetta).
fractional_input_digits = 8
fractional_normal_digits = 0
fractional_trailing_zero_digits = 0
alt_unit_names_are_symbols = NO
alt_unit_names = {"24":"Yotta-GOA","21":"Zetta-GOA","18":"Exa-GOA","15":"Peta-GOA","12":"Tera-GOA","9":"Giga-GOA","6":"Mega-GOA","3":"Kilo-GOA","0":"GOA","-1":"Deci-GOA","-2":"Centi-GOA","-3":"Milli-GOA","-6":"Micro-GOA","-7":"Deci-Micro-GOA","-8":"Atomic-GOA"}
common_amounts = "GOA:10 GOA:20 GOA:50 GOA:100 GOA:200 GOA:1000"
### Disable package demonstrator currencies (like merchant-overrides)
[currency-kudos]
ENABLED = NO
[currency-testkudos]
ENABLED = NO
EOF
echo "Wrote $OV"
grep -E '^(name|code|PORT|CURRENCY|alt_unit|fractional_input|common_)' "$OV" || true
echo "=== 4. GOA coin denominations (125 ladder) ==="
if [ -f "$COINS" ] && ! grep -q 'VALUE = GOA' "$COINS" 2>/dev/null; then
mv "$COINS" "${COINS}.package-kudos"
echo "Moved package coins -> ${COINS}.package-kudos"
fi
cat >"$COINS" << 'COINS_EOF'
# GOA denominations for exchange.hacktivism.ch
# Units: 1 GOA = 1000 mGOA = 1_000_000 uGOA
# VALUE uses base currency; coin sections cover 125 ladders at u/m/whole scale.
# --- uGOA (10^-6 GOA) ---
[coin_goa_0_000001]
VALUE = GOA:0.000001
DURATION_WITHDRAW = 7 days
DURATION_SPEND = 2 years
DURATION_LEGAL = 3 years
FEE_WITHDRAW = GOA:0
FEE_DEPOSIT = GOA:0
FEE_REFRESH = GOA:0
FEE_REFUND = GOA:0
RSA_KEYSIZE = 2048
CIPHER = RSA
[coin_goa_0_000002]
VALUE = GOA:0.000002
DURATION_WITHDRAW = 7 days
DURATION_SPEND = 2 years
DURATION_LEGAL = 3 years
FEE_WITHDRAW = GOA:0
FEE_DEPOSIT = GOA:0
FEE_REFRESH = GOA:0
FEE_REFUND = GOA:0
RSA_KEYSIZE = 2048
CIPHER = RSA
[coin_goa_0_000005]
VALUE = GOA:0.000005
DURATION_WITHDRAW = 7 days
DURATION_SPEND = 2 years
DURATION_LEGAL = 3 years
FEE_WITHDRAW = GOA:0
FEE_DEPOSIT = GOA:0
FEE_REFRESH = GOA:0
FEE_REFUND = GOA:0
RSA_KEYSIZE = 2048
CIPHER = RSA
[coin_goa_0_00001]
VALUE = GOA:0.00001
DURATION_WITHDRAW = 7 days
DURATION_SPEND = 2 years
DURATION_LEGAL = 3 years
FEE_WITHDRAW = GOA:0
FEE_DEPOSIT = GOA:0
FEE_REFRESH = GOA:0
FEE_REFUND = GOA:0
RSA_KEYSIZE = 2048
CIPHER = RSA
[coin_goa_0_00002]
VALUE = GOA:0.00002
DURATION_WITHDRAW = 7 days
DURATION_SPEND = 2 years
DURATION_LEGAL = 3 years
FEE_WITHDRAW = GOA:0
FEE_DEPOSIT = GOA:0
FEE_REFRESH = GOA:0
FEE_REFUND = GOA:0
RSA_KEYSIZE = 2048
CIPHER = RSA
[coin_goa_0_00005]
VALUE = GOA:0.00005
DURATION_WITHDRAW = 7 days
DURATION_SPEND = 2 years
DURATION_LEGAL = 3 years
FEE_WITHDRAW = GOA:0
FEE_DEPOSIT = GOA:0
FEE_REFRESH = GOA:0
FEE_REFUND = GOA:0
RSA_KEYSIZE = 2048
CIPHER = RSA
[coin_goa_0_0001]
VALUE = GOA:0.0001
DURATION_WITHDRAW = 7 days
DURATION_SPEND = 2 years
DURATION_LEGAL = 3 years
FEE_WITHDRAW = GOA:0
FEE_DEPOSIT = GOA:0
FEE_REFRESH = GOA:0
FEE_REFUND = GOA:0
RSA_KEYSIZE = 2048
CIPHER = RSA
[coin_goa_0_0002]
VALUE = GOA:0.0002
DURATION_WITHDRAW = 7 days
DURATION_SPEND = 2 years
DURATION_LEGAL = 3 years
FEE_WITHDRAW = GOA:0
FEE_DEPOSIT = GOA:0
FEE_REFRESH = GOA:0
FEE_REFUND = GOA:0
RSA_KEYSIZE = 2048
CIPHER = RSA
[coin_goa_0_0005]
VALUE = GOA:0.0005
DURATION_WITHDRAW = 7 days
DURATION_SPEND = 2 years
DURATION_LEGAL = 3 years
FEE_WITHDRAW = GOA:0
FEE_DEPOSIT = GOA:0
FEE_REFRESH = GOA:0
FEE_REFUND = GOA:0
RSA_KEYSIZE = 2048
CIPHER = RSA
# --- mGOA (10^-3 GOA) ---
[coin_goa_0_001]
VALUE = GOA:0.001
DURATION_WITHDRAW = 7 days
DURATION_SPEND = 2 years
DURATION_LEGAL = 3 years
FEE_WITHDRAW = GOA:0
FEE_DEPOSIT = GOA:0
FEE_REFRESH = GOA:0
FEE_REFUND = GOA:0
RSA_KEYSIZE = 2048
CIPHER = RSA
[coin_goa_0_002]
VALUE = GOA:0.002
DURATION_WITHDRAW = 7 days
DURATION_SPEND = 2 years
DURATION_LEGAL = 3 years
FEE_WITHDRAW = GOA:0
FEE_DEPOSIT = GOA:0
FEE_REFRESH = GOA:0
FEE_REFUND = GOA:0
RSA_KEYSIZE = 2048
CIPHER = RSA
[coin_goa_0_005]
VALUE = GOA:0.005
DURATION_WITHDRAW = 7 days
DURATION_SPEND = 2 years
DURATION_LEGAL = 3 years
FEE_WITHDRAW = GOA:0
FEE_DEPOSIT = GOA:0
FEE_REFRESH = GOA:0
FEE_REFUND = GOA:0
RSA_KEYSIZE = 2048
CIPHER = RSA
[coin_goa_0_01]
VALUE = GOA:0.01
DURATION_WITHDRAW = 7 days
DURATION_SPEND = 2 years
DURATION_LEGAL = 3 years
FEE_WITHDRAW = GOA:0
FEE_DEPOSIT = GOA:0
FEE_REFRESH = GOA:0
FEE_REFUND = GOA:0
RSA_KEYSIZE = 2048
CIPHER = RSA
[coin_goa_0_02]
VALUE = GOA:0.02
DURATION_WITHDRAW = 7 days
DURATION_SPEND = 2 years
DURATION_LEGAL = 3 years
FEE_WITHDRAW = GOA:0
FEE_DEPOSIT = GOA:0
FEE_REFRESH = GOA:0
FEE_REFUND = GOA:0
RSA_KEYSIZE = 2048
CIPHER = RSA
[coin_goa_0_05]
VALUE = GOA:0.05
DURATION_WITHDRAW = 7 days
DURATION_SPEND = 2 years
DURATION_LEGAL = 3 years
FEE_WITHDRAW = GOA:0
FEE_DEPOSIT = GOA:0
FEE_REFRESH = GOA:0
FEE_REFUND = GOA:0
RSA_KEYSIZE = 2048
CIPHER = RSA
[coin_goa_0_1]
VALUE = GOA:0.1
DURATION_WITHDRAW = 7 days
DURATION_SPEND = 2 years
DURATION_LEGAL = 3 years
FEE_WITHDRAW = GOA:0
FEE_DEPOSIT = GOA:0
FEE_REFRESH = GOA:0
FEE_REFUND = GOA:0
RSA_KEYSIZE = 2048
CIPHER = RSA
[coin_goa_0_2]
VALUE = GOA:0.2
DURATION_WITHDRAW = 7 days
DURATION_SPEND = 2 years
DURATION_LEGAL = 3 years
FEE_WITHDRAW = GOA:0
FEE_DEPOSIT = GOA:0
FEE_REFRESH = GOA:0
FEE_REFUND = GOA:0
RSA_KEYSIZE = 2048
CIPHER = RSA
[coin_goa_0_5]
VALUE = GOA:0.5
DURATION_WITHDRAW = 7 days
DURATION_SPEND = 2 years
DURATION_LEGAL = 3 years
FEE_WITHDRAW = GOA:0
FEE_DEPOSIT = GOA:0
FEE_REFRESH = GOA:0
FEE_REFUND = GOA:0
RSA_KEYSIZE = 2048
CIPHER = RSA
# --- whole GOA ---
[coin_goa_1_0]
VALUE = GOA:1
DURATION_WITHDRAW = 7 days
DURATION_SPEND = 2 years
DURATION_LEGAL = 3 years
FEE_WITHDRAW = GOA:0
FEE_DEPOSIT = GOA:0
FEE_REFRESH = GOA:0
FEE_REFUND = GOA:0
RSA_KEYSIZE = 2048
CIPHER = RSA
[coin_goa_2_0]
VALUE = GOA:2
DURATION_WITHDRAW = 7 days
DURATION_SPEND = 2 years
DURATION_LEGAL = 3 years
FEE_WITHDRAW = GOA:0
FEE_DEPOSIT = GOA:0
FEE_REFRESH = GOA:0
FEE_REFUND = GOA:0
RSA_KEYSIZE = 2048
CIPHER = RSA
[coin_goa_5_0]
VALUE = GOA:5
DURATION_WITHDRAW = 7 days
DURATION_SPEND = 2 years
DURATION_LEGAL = 3 years
FEE_WITHDRAW = GOA:0
FEE_DEPOSIT = GOA:0
FEE_REFRESH = GOA:0
FEE_REFUND = GOA:0
RSA_KEYSIZE = 2048
CIPHER = RSA
[coin_goa_10_0]
VALUE = GOA:10
DURATION_WITHDRAW = 7 days
DURATION_SPEND = 2 years
DURATION_LEGAL = 3 years
FEE_WITHDRAW = GOA:0
FEE_DEPOSIT = GOA:0
FEE_REFRESH = GOA:0
FEE_REFUND = GOA:0
RSA_KEYSIZE = 2048
CIPHER = RSA
[coin_goa_20_0]
VALUE = GOA:20
DURATION_WITHDRAW = 7 days
DURATION_SPEND = 2 years
DURATION_LEGAL = 3 years
FEE_WITHDRAW = GOA:0
FEE_DEPOSIT = GOA:0
FEE_REFRESH = GOA:0
FEE_REFUND = GOA:0
RSA_KEYSIZE = 2048
CIPHER = RSA
[coin_goa_50_0]
VALUE = GOA:50
DURATION_WITHDRAW = 7 days
DURATION_SPEND = 2 years
DURATION_LEGAL = 3 years
FEE_WITHDRAW = GOA:0
FEE_DEPOSIT = GOA:0
FEE_REFRESH = GOA:0
FEE_REFUND = GOA:0
RSA_KEYSIZE = 2048
CIPHER = RSA
[coin_goa_100_0]
VALUE = GOA:100
DURATION_WITHDRAW = 7 days
DURATION_SPEND = 2 years
DURATION_LEGAL = 3 years
FEE_WITHDRAW = GOA:0
FEE_DEPOSIT = GOA:0
FEE_REFRESH = GOA:0
FEE_REFUND = GOA:0
RSA_KEYSIZE = 2048
CIPHER = RSA
[coin_goa_200_0]
VALUE = GOA:200
DURATION_WITHDRAW = 7 days
DURATION_SPEND = 2 years
DURATION_LEGAL = 3 years
FEE_WITHDRAW = GOA:0
FEE_DEPOSIT = GOA:0
FEE_REFRESH = GOA:0
FEE_REFUND = GOA:0
RSA_KEYSIZE = 2048
CIPHER = RSA
[coin_goa_1000_0]
VALUE = GOA:1000
DURATION_WITHDRAW = 7 days
DURATION_SPEND = 2 years
DURATION_LEGAL = 3 years
FEE_WITHDRAW = GOA:0
FEE_DEPOSIT = GOA:0
FEE_REFRESH = GOA:0
FEE_REFUND = GOA:0
RSA_KEYSIZE = 2048
CIPHER = RSA
COINS_EOF
echo "Wrote $COINS ($(grep -c '^\[coin_' "$COINS") coin sections)"
echo "=== 5. database ==="
taler-exchange-dbconfig 2>&1 || true
runuser -u taler-exchange-httpd -- taler-exchange-dbinit -c "$CONF" 2>&1 || true
echo "=== 6. next steps (manual) ==="
echo " - /root/start_base_services_for_taler_exchange.sh # root: secmods + shell as httpd"
echo " - ./start_exchange.sh # as taler-exchange-httpd"
echo " - offline: sign denominations + wire accounts for /keys"
echo " - MASTER_PUBLIC_KEY=$MASTER_PUB"
echo "DONE bootstrap"

View file

@ -0,0 +1,88 @@
#!/bin/bash
# Health check for manual taler-exchange (same style as check_merchant-health.sh).
# Runnable as taler-exchange-httpd (or any user that can see processes + curl localhost).
CONF=/etc/taler-exchange/taler-exchange.conf
SOCK=/run/taler-exchange/httpd/exchange-http.sock
green() { echo -e "\e[32m$1\e[0m"; }
red() { echo -e "\e[31m$1\e[0m"; }
yellow() { echo -e "\e[33m$1\e[0m"; }
fail=0
ok() { green "[OK] $1"; }
bad() { red "[FAIL] $1"; fail=1; }
warn() { yellow "[WARN] $1"; }
echo "=== Taler Exchange Health Check ==="
# 1. secmods (started by root base script)
for p in taler-exchange-secmod-rsa taler-exchange-secmod-cs taler-exchange-secmod-eddsa; do
if pgrep -f "$p" >/dev/null 2>&1; then
ok "process $p"
else
bad "process $p not running"
fi
done
# 2. httpd
if pgrep -f taler-exchange-httpd >/dev/null 2>&1; then
ok "process taler-exchange-httpd"
else
bad "process taler-exchange-httpd is NOT running"
fi
# 3. serving mode
SERVE=$(taler-exchange-config -c "$CONF" -s exchange -o SERVE 2>/dev/null || echo unix)
PORT=$(taler-exchange-config -c "$CONF" -s exchange -o PORT 2>/dev/null || echo 9011)
if [ "$SERVE" = "tcp" ]; then
if curl -sf -m 3 "http://127.0.0.1:${PORT}/config" >/dev/null 2>&1; then
ok "HTTP /config on 127.0.0.1:${PORT}"
elif curl -sf -m 3 "http://127.0.0.1:${PORT}/keys" >/dev/null 2>&1; then
ok "HTTP /keys on 127.0.0.1:${PORT}"
else
bad "no HTTP response on 127.0.0.1:${PORT} (/config|/keys)"
fi
else
if [ -S "$SOCK" ]; then
ok "socket $SOCK"
else
bad "socket does NOT exist: $SOCK"
fi
fi
# 4. wire helpers — ensure (no systemd) then require
if [ "${SKIP_ENSURE:-0}" != "1" ]; then
if [ -x /usr/local/bin/ensure_exchange_helpers.sh ]; then
echo "--- ensure_exchange_helpers ---"
/usr/local/bin/ensure_exchange_helpers.sh || warn "ensure_exchange_helpers exited non-zero"
elif [ -x "$(dirname "$0")/ensure_exchange_helpers.sh" ]; then
echo "--- ensure_exchange_helpers ---"
"$(dirname "$0")/ensure_exchange_helpers.sh" || warn "ensure_exchange_helpers exited non-zero"
elif [ -x /root/ensure_exchange_helpers.sh ]; then
echo "--- ensure_exchange_helpers ---"
/root/ensure_exchange_helpers.sh || warn "ensure_exchange_helpers exited non-zero"
fi
fi
live_helper() {
local p="$1"
# COMM is 15 chars; use full cmdline match
pgrep -f "(^|/)(${p})( |$)" >/dev/null 2>&1
}
for p in taler-exchange-aggregator taler-exchange-wirewatch taler-exchange-transfer taler-exchange-closer; do
if live_helper "$p"; then
ok "process $p"
else
bad "process $p not running (settlement needs transfer+aggregator)"
fi
done
if [ "$fail" -eq 0 ]; then
green "=== ALL CRITICAL CHECKS PASSED ==="
exit 0
fi
red "=== SOME CHECKS FAILED ==="
exit 1

View file

@ -0,0 +1,75 @@
#!/bin/bash
# Ensure exchange wire/db helper processes are running (no systemd).
# Root inside taler-exchange container (or host if packages installed there).
#
# Usage:
# ensure_exchange_helpers.sh
# ENSURE_ONLY=1 ensure_exchange_helpers.sh # start missing, no health summary
set -euo pipefail
CONF="${TALER_EXCHANGE_CONFIG:-/etc/taler-exchange/taler-exchange.conf}"
LOG_DIR="${TALER_EXCHANGE_LOG_DIR:-/var/log/taler-exchange}"
if [ "$(id -u)" -ne 0 ]; then
echo "root only" >&2
exit 1
fi
mkdir -p "$LOG_DIR"
chmod 755 "$LOG_DIR" 2>/dev/null || true
# Live process under user. Note: Linux COMM is 15 chars — never use pgrep -x
# for long names like taler-exchange-aggregator / taler-exchange-wirewatch.
is_running() {
local user="$1"
local bin="$2"
local base
base=$(basename "$bin")
# [t] trick avoids matching this grep/pgrep itself
ps -u "$user" -o args= 2>/dev/null | grep -qE "(^|/)[${base:0:1}]${base:1}( |$)" \
|| pgrep -u "$user" -f "(^|/)(${base})( |$)" >/dev/null 2>&1
}
# Start with nohup so SIGHUP from parent shell exit does not kill helpers.
start_one() {
local user="$1"
local name="$2"
local bin="$3"
shift 3
if is_running "$user" "$bin"; then
echo "already: $name ($user)"
return 0
fi
echo "start: $name as $user"
# shellcheck disable=SC2086
nohup runuser -u "$user" -- "$bin" "$@" >>"$LOG_DIR/${name}.log" 2>&1 </dev/null &
disown 2>/dev/null || true
local i
for i in 1 2 3 4 5 6; do
sleep 0.4
if is_running "$user" "$bin"; then
echo " ok: $name"
return 0
fi
done
echo " FAIL: $name did not stay up (see $LOG_DIR/${name}.log)" >&2
tail -15 "$LOG_DIR/${name}.log" 2>/dev/null || true
return 1
}
ec=0
start_one taler-exchange-aggregator taler-exchange-aggregator \
/usr/bin/taler-exchange-aggregator -c "$CONF" -L INFO || ec=1
start_one taler-exchange-closer taler-exchange-closer \
/usr/bin/taler-exchange-closer -c "$CONF" -L INFO || ec=1
start_one taler-exchange-wire taler-exchange-wirewatch \
/usr/bin/taler-exchange-wirewatch -c "$CONF" -L INFO || ec=1
start_one taler-exchange-wire taler-exchange-transfer \
/usr/bin/taler-exchange-transfer -c "$CONF" -L INFO || ec=1
echo "--- live helpers ---"
ps -eo pid,user,stat,etime,args 2>/dev/null \
| grep -E 'taler-exchange-(aggregator|closer|wirewatch|transfer)' \
| grep -vE 'grep| Z |ensure_exchange' || true
exit "$ec"

View 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-v2}"
# 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/"

View file

@ -0,0 +1,168 @@
#!/bin/bash
# Swiss FADP privacy for exchange.hacktivism.ch — run inside exchange container.
set -euo pipefail
export PATH="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin${PATH:+:$PATH}"
ETAG="${PRIVACY_ETAG:-exchange-pp-swiss-v0}"
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/}"
DIR="${PRIVACY_DIR:-${DATA_HOME%/}/terms}/en"
mkdir -p "$DIR"
cat >"$DIR/${ETAG}.txt" <<'EOF'
Privacy notice — exchange.hacktivism.ch (Swiss FADP / revDSG)
Controller: operators of the hacktivism.ch GNU Taler stack.
Service: experimental GOA exchange (not a licensed Swiss bank or e-money institution).
Data retained (precise):
1) Reserves — reserve public keys, current remaining amounts, creation/close times: for service life or wipe.
2) Wire-in (reserves_in) — credit amounts, timestamps, bank-side account reference as provided by the bank/wire gateway: for service life; links reserve funding to bank accounts.
3) Coins — known coin public keys, denomination serials, remaining value, spend/deposit linkage as required by protocol: for service life.
4) Withdraw / refresh / recoup / refund / deposit tables — protocol operation records and timestamps: for service life.
5) Wire-out — transfers to merchant accounts (amounts, payto/target as stored, times): for service life.
6) Keys / denominations — public denomination and signing keys (not personal data by themselves).
7) Technical logs — HTTP and process logs (IP, path, status): daysweeks via rotation.
8) Terms acceptance markers if stored by clients — as required by wallet software.
Not retained by the exchange as plaintext identity of wallet holders: GNU Taler is designed so coin spend is unlinkable to customer bank identity at the exchange when used as intended. Funding reserves via the bank may still identify the bank account holder on the bank side.
Purposes: operate GOA exchange protocol, settle with bank/merchants, security, operations.
Legal basis (FADP): provision of requested exchange service; proportionate operation of experimental stack.
Recipients: bank.hacktivism.ch (wire); merchants depositing GOA; host operators. No sale of data.
Rights: access, rectification, deletion, objection (FADP); complaint to Swiss FDPIC (EDÖB).
Security: TLS; experimental stack.
Related: https://exchange.hacktivism.ch/terms · https://bank.hacktivism.ch/intro/privacy.html · https://taler.hacktivism.ch/privacy
EOF
cat >"$DIR/${ETAG}.md" <<'EOF'
# Privacy notice · GOA Exchange · Swiss FADP
Controller: operators of **hacktivism.ch**. Service: experimental **GOA** exchange at `exchange.hacktivism.ch`.
Swiss Federal Act on Data Protection (**FADP / revDSG**, since 1 Sep 2023).
## Data retained
| Data | Examples | Retention |
|------|----------|-----------|
| Reserves | Reserve pubs, remaining amount, times | Service life or wipe |
| Wire-in | Amounts, timestamps, bank account ref from wire path | Service life |
| Coins | Coin pubs, denoms, remaining, deposit links | Service life |
| Protocol ops | Withdraw, refresh, deposit, refund, recoup | Service life |
| Wire-out | Merchant settlement amounts / targets | Service life |
| Technical logs | IP, path, status | Daysweeks (rotation) |
**Design note:** Coin spend is intended to be unlinkable to the customer at the exchange. Funding a reserve via the bank may identify the **bank** account holder on the bank system.
## Purposes
Run the GOA exchange protocol; wire settlement; security and operations.
## Rights
FADP access, correction, deletion, objection. Complaint: Swiss **FDPIC / EDÖB**.
## Related
- [Exchange terms](https://exchange.hacktivism.ch/terms)
- [Bank privacy](https://bank.hacktivism.ch/intro/privacy.html)
- [Merchant privacy](https://taler.hacktivism.ch/privacy)
EOF
cat >"$DIR/${ETAG}.html" <<'HTML'
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1"/>
<title>Privacy notice · GOA Exchange · Swiss FADP</title>
<style>
:root { color-scheme: dark light; }
body {
font-family: system-ui, -apple-system, sans-serif;
max-width: 42rem; 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.5rem 0 0.5rem; color: #e8c878; }
p, li, td, th { font-size: 0.95rem; }
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;
}
.note {
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);
font-size: 0.9rem; color: #c8c4bf;
}
table { width: 100%; border-collapse: collapse; margin: 0.6rem 0 1rem; font-size: 0.88rem; }
th, td { border: 1px solid rgba(255,255,255,0.12); padding: 0.45rem 0.55rem; text-align: left; vertical-align: top; }
th { background: rgba(0,0,0,0.35); color: #e8c878; font-weight: 700; }
.muted { color: #a39e98; font-size: 0.88rem; }
footer { margin-top: 2rem; font-size: 0.85rem; color: #a39e98; }
</style>
</head>
<body>
<div class="badge">exchange.hacktivism.ch · privacy · CH</div>
<h1>Privacy notice · GOA Exchange</h1>
<p class="note">
Swiss Federal Act on Data Protection (<strong>FADP / revDSG</strong>, since 1&nbsp;Sep&nbsp;2023).
Experimental GOA exchange at <code>exchange.hacktivism.ch</code> — not a licensed bank.
</p>
<h2>1. Controller</h2>
<p>Operators of the hacktivism.ch GNU Taler stack. No separate DPO for this experimental service.</p>
<h2>2. Data retained</h2>
<table>
<thead><tr><th>Data</th><th>Examples</th><th>Typical retention</th></tr></thead>
<tbody>
<tr><td>Reserves</td><td>Reserve public keys, remaining amount, times</td><td>Service life or wipe</td></tr>
<tr><td>Wire-in</td><td>Amounts, timestamps, bank account reference from wire path</td><td>Service life</td></tr>
<tr><td>Coins</td><td>Coin pubs, denominations, remaining value, deposit links</td><td>Service life</td></tr>
<tr><td>Protocol operations</td><td>Withdraw, refresh, deposit, refund, recoup</td><td>Service life</td></tr>
<tr><td>Wire-out</td><td>Merchant settlement amounts / targets</td><td>Service life</td></tr>
<tr><td>Technical logs</td><td>IP, path, status</td><td>Daysweeks (rotation)</td></tr>
</tbody>
</table>
<p><strong>Design note:</strong> Coin spend is intended to be unlinkable to the customer
<em>at the exchange</em>. Funding a reserve via the bank may identify the bank account
holder on the <strong>bank</strong> system.</p>
<h2>3. Purposes</h2>
<ul>
<li>Operate the GOA GNU Taler exchange protocol</li>
<li>Wire settlement with bank and merchants</li>
<li>Security, debugging, capacity monitoring</li>
</ul>
<h2>4. Your rights</h2>
<p>Access, rectification, deletion, objection under the FADP. Complaint:
Swiss <strong>FDPIC / EDÖB</strong>.</p>
<h2>Related</h2>
<ul>
<li><a href="https://exchange.hacktivism.ch/terms">Exchange terms</a></li>
<li><a href="https://bank.hacktivism.ch/intro/privacy.html">Bank privacy</a></li>
<li><a href="https://taler.hacktivism.ch/privacy">Merchant privacy</a></li>
</ul>
<footer class="muted">exchange-pp-swiss-v0 · Swiss FADP (revDSG)</footer>
</body>
</html>
HTML
chmod -R a+rX "${DATA_HOME%/}/terms"
if id taler-exchange-httpd >/dev/null 2>&1; then
chown -R taler-exchange-httpd: "${DATA_HOME%/}/terms" 2>/dev/null || true
fi
echo "ok exchange privacy $ETAG -> $DIR"
ls -la "$DIR"/${ETAG}.*

View 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"

View file

@ -0,0 +1,62 @@
#!/bin/bash
# Offline: download future keys from local exchange, sign, upload. Restore public BASE_URL.
# Run as root inside taler-hacktivism-exchange-ansible.
set -euo pipefail
CONF=/etc/taler-exchange/taler-exchange.conf
OV=/etc/taler-exchange/exchange-overrides.conf
PUBLIC='https://exchange.hacktivism.ch/'
LOCAL='http://127.0.0.1:9011/'
[ "$(id -u)" -eq 0 ] || { echo "root only"; exit 1; }
sed -i "s|^BASE_URL = .*|BASE_URL = ${LOCAL}|" "$OV"
echo "BASE_URL=$(grep '^BASE_URL' "$OV")"
echo "=== download ==="
if ! runuser -u taler-exchange-offline -- \
taler-exchange-offline -c "$CONF" -L INFO download > /tmp/future-keys.json 2>/tmp/dl.err; then
echo "download failed:"; cat /tmp/dl.err
# still show size
fi
echo "dl_err:"; cat /tmp/dl.err | tail -30
echo "dl_size=$(wc -c </tmp/future-keys.json 2>/dev/null || echo 0)"
head -c 400 /tmp/future-keys.json 2>/dev/null; echo
if [ ! -s /tmp/future-keys.json ]; then
echo "empty download — check secmod connectivity / management API"
sed -i "s|^BASE_URL = .*|BASE_URL = ${PUBLIC}|" "$OV"
exit 1
fi
echo "=== sign ==="
runuser -u taler-exchange-offline -- \
taler-exchange-offline -c "$CONF" -L INFO sign < /tmp/future-keys.json > /tmp/signed-keys.json 2>/tmp/sg.err
echo "sg_err:"; cat /tmp/sg.err | tail -20
echo "sg_size=$(wc -c </tmp/signed-keys.json)"
echo "=== upload ==="
runuser -u taler-exchange-offline -- \
taler-exchange-offline -c "$CONF" -L INFO upload < /tmp/signed-keys.json 2>/tmp/up.err
echo "up_err:"; cat /tmp/up.err | tail -20
sed -i "s|^BASE_URL = .*|BASE_URL = ${PUBLIC}|" "$OV"
echo "BASE_URL restored=$(grep '^BASE_URL' "$OV")"
echo "=== /keys probe ==="
sleep 1
# wake suspended handlers with a tiny delay
for i in 1 2 3 4 5 6; do
code=$(curl -sS -m 12 -o /tmp/keys.json -w '%{http_code}' http://127.0.0.1:9011/keys || true)
sz=$(wc -c </tmp/keys.json 2>/dev/null || echo 0)
echo "try $i code=$code size=$sz"
if [ "$code" = "200" ] && [ "$sz" -gt 200 ]; then
echo KEYS_OK
head -c 300 /tmp/keys.json; echo
grep -oE '"master_public_key"[[:space:]]*:[[:space:]]*"[^"]+"' /tmp/keys.json | head -1 || true
exit 0
fi
sleep 2
done
echo KEYS_FAIL
tail -25 /var/log/taler-exchange/taler-exchange-httpd-*.log 2>/dev/null | tail -25
exit 1

View file

@ -0,0 +1,148 @@
#!/bin/bash
# Root: base services for manual exchange (like merchant start_base_services_for_taler.sh).
# Then interactive shell as taler-exchange-httpd → run start_exchange.sh there.
#
# Secmods run as dedicated users (not httpd) — started here as root.
# httpd is started by /usr/local/bin/start_exchange.sh as taler-exchange-httpd.
#
# Usage:
# /root/start_base_services_for_taler_exchange.sh
# /root/start_base_services_for_taler_exchange.sh --no-shell
set -e
CONF=/etc/taler-exchange/taler-exchange.conf
LOG_DIR=/var/log/taler-exchange
PWD_BIN=/usr/local/bin
EXCHANGE_STARTER=start_exchange.sh
if [ "$(id -u)" -ne 0 ]; then
echo "Run as root" >&2
exit 1
fi
NO_SHELL=0
for arg in "$@"; do
case "$arg" in
--no-shell|-n) NO_SHELL=1 ;;
--help|-h)
echo "Usage: $0 [--no-shell]"
exit 0
;;
esac
done
# --- Debian postgresql defaults (pg_createcluster layout) ---
ensure_postgresql() {
echo " Debian perms on /etc/postgresql + data/log/run..."
if [ -d /etc/postgresql ]; then
chown -R root:postgres /etc/postgresql
find /etc/postgresql -type d -exec chmod 755 {} \;
find /etc/postgresql -type f -name '*.conf' -exec chmod 640 {} \;
fi
chown -R postgres:postgres /var/lib/postgresql /var/log/postgresql 2>/dev/null || true
mkdir -p /var/run/postgresql
chown postgres:postgres /var/run/postgresql
chmod 2775 /var/run/postgresql 2>/dev/null || chmod 775 /var/run/postgresql
if pg_isready -q 2>/dev/null; then
echo " already accepting connections"
pg_isready || true
return 0
fi
rm -f /var/run/postgresql/.s.PGSQL.*.lock 2>/dev/null || true
if ! pgrep -u postgres -x postgres >/dev/null 2>&1; then
rm -f /var/lib/postgresql/*/main/postmaster.pid 2>/dev/null || true
fi
if command -v pg_ctlcluster >/dev/null 2>&1 && command -v pg_lsclusters >/dev/null 2>&1; then
while read -r ver name _rest; do
[ -n "$ver" ] || continue
echo " pg_ctlcluster $ver $name start"
pg_ctlcluster "$ver" "$name" start 2>/dev/null || true
done < <(pg_lsclusters --no-header 2>/dev/null || true)
fi
if ! pg_isready -q 2>/dev/null; then
if [ -x /etc/init.d/postgresql ]; then
/etc/init.d/postgresql start || true
else
service postgresql start || true
fi
fi
sleep 1
pg_isready || true
}
echo "Create log + runtime dirs... giving permission to taler-exchange users:"
mkdir -p "$LOG_DIR"
mkdir -p /run/taler-exchange/secmod-rsa /run/taler-exchange/secmod-cs \
/run/taler-exchange/secmod-eddsa /run/taler-exchange/httpd
chown root:root /run/taler-exchange
chmod 755 /run/taler-exchange
chown taler-exchange-secmod-rsa:taler-exchange-secmod /run/taler-exchange/secmod-rsa
chown taler-exchange-secmod-cs:taler-exchange-secmod /run/taler-exchange/secmod-cs
chown taler-exchange-secmod-eddsa:taler-exchange-secmod /run/taler-exchange/secmod-eddsa
chown taler-exchange-httpd:www-data /run/taler-exchange/httpd
chmod 755 /run/taler-exchange/secmod-rsa /run/taler-exchange/secmod-cs /run/taler-exchange/secmod-eddsa
chmod 750 /run/taler-exchange/httpd
chown taler-exchange-httpd: "$LOG_DIR"
chmod 755 "$LOG_DIR"
# Package default: each secmod user owns its tree (keys/ must not be root-owned).
mkdir -p /var/lib/taler-exchange/secmod-rsa /var/lib/taler-exchange/secmod-cs \
/var/lib/taler-exchange/secmod-eddsa
chown -R taler-exchange-secmod-rsa:taler-exchange-secmod /var/lib/taler-exchange/secmod-rsa
chown -R taler-exchange-secmod-cs:taler-exchange-secmod /var/lib/taler-exchange/secmod-cs
chown -R taler-exchange-secmod-eddsa:taler-exchange-secmod /var/lib/taler-exchange/secmod-eddsa
chmod 700 /var/lib/taler-exchange/secmod-rsa /var/lib/taler-exchange/secmod-cs \
/var/lib/taler-exchange/secmod-eddsa
echo "Start base services needed for Taler Exchange."
echo ""
echo "1. postgresql:"
ensure_postgresql
# Linux COMM is 15 chars — never pgrep -x for long names; match full path in args.
start_bg() {
local user="$1"; shift
local name="$1"; shift
local bin="$1"
if ps -eo args= 2>/dev/null | grep -F "$bin" | grep -v grep >/dev/null 2>&1; then
echo " already running: $name"
return 0
fi
echo " start $name as $user"
nohup runuser -u "$user" -- "$@" >>"$LOG_DIR/${name}.log" 2>&1 </dev/null &
disown 2>/dev/null || true
sleep 0.3
}
echo "2. crypto secmods:"
start_bg taler-exchange-secmod-rsa taler-exchange-secmod-rsa \
/usr/bin/taler-exchange-secmod-rsa -c "$CONF" -L INFO
start_bg taler-exchange-secmod-cs taler-exchange-secmod-cs \
/usr/bin/taler-exchange-secmod-cs -c "$CONF" -L INFO
start_bg taler-exchange-secmod-eddsa taler-exchange-secmod-eddsa \
/usr/bin/taler-exchange-secmod-eddsa -c "$CONF" -L INFO
echo "3. wire/db helpers (need wire config to stay up):"
start_bg taler-exchange-aggregator taler-exchange-aggregator \
/usr/bin/taler-exchange-aggregator -c "$CONF" -L INFO || true
start_bg taler-exchange-closer taler-exchange-closer \
/usr/bin/taler-exchange-closer -c "$CONF" -L INFO || true
start_bg taler-exchange-wire taler-exchange-wirewatch \
/usr/bin/taler-exchange-wirewatch -c "$CONF" -L INFO || true
start_bg taler-exchange-wire taler-exchange-transfer \
/usr/bin/taler-exchange-transfer -c "$CONF" -L INFO || true
if [ "$NO_SHELL" -eq 1 ]; then
echo "Base services started (--no-shell). Next: runuser -u taler-exchange-httpd -- $PWD_BIN/$EXCHANGE_STARTER [--restart]"
exit 0
fi
echo "4. Switching now to user taler-exchange-httpd, in $PWD_BIN; find executable $EXCHANGE_STARTER there!"
echo ""
cd "$PWD_BIN"
# same pattern as merchant: -u and -s/--shell are mutually exclusive on util-linux runuser
exec runuser -u taler-exchange-httpd -- bash

View file

@ -0,0 +1,104 @@
#!/bin/bash
# Start / restart taler-exchange-httpd (manual, no systemd).
# Run as: taler-exchange-httpd
# Same role as start_merchant.sh for the merchant.
#
# Usage:
# start_exchange.sh
# start_exchange.sh --restart | -r
# start_exchange.sh --help
set -u
usage() {
cat <<'EOF'
Usage: start_exchange.sh [--restart|-r] [--help|-h]
(default) Start taler-exchange-httpd if not already running.
--restart Stop live taler-exchange-httpd, then start cleanly.
Does not touch postgres/secmods/wire helpers
(those come from /root/start_base_services_for_taler_exchange.sh).
EOF
}
DO_RESTART=0
for arg in "$@"; do
case "$arg" in
--restart|-r) DO_RESTART=1 ;;
--help|-h) usage; exit 0 ;;
*) echo "Unknown option: $arg" >&2; usage >&2; exit 2 ;;
esac
done
if [ "$(id -un)" != "taler-exchange-httpd" ]; then
echo "This script must be run as user taler-exchange-httpd" >&2
exit 1
fi
CONF=/etc/taler-exchange/taler-exchange.conf
LOG_DIR=/var/log/taler-exchange
PORT=$(taler-exchange-config -c "$CONF" -s exchange -o PORT 2>/dev/null || echo 9011)
list_httpd_pids() {
ps -eo pid=,stat=,args= 2>/dev/null | while read -r pid stat args; do
case "$stat" in Z*) continue ;; esac
case "$args" in
*start_exchange.sh*) continue ;;
esac
case "$args" in
*taler-exchange-httpd\ *|taler-exchange-httpd\ *|/usr/bin/taler-exchange-httpd\ *)
echo "$pid"
;;
esac
done | sort -u
}
kill_httpd() {
local pids
pids=$(list_httpd_pids | tr '\n' ' ')
if [ -z "${pids// }" ]; then
echo "No live taler-exchange-httpd processes to stop."
return 0
fi
echo "Stopping PIDs: $pids"
# shellcheck disable=SC2086
kill -TERM $pids 2>/dev/null || true
sleep 2
local left
left=$(list_httpd_pids | tr '\n' ' ')
if [ -n "${left// }" ]; then
echo "SIGKILL remaining: $left"
# shellcheck disable=SC2086
kill -KILL $left 2>/dev/null || true
sleep 1
fi
echo "taler-exchange-httpd stopped."
}
if [ "$DO_RESTART" -eq 1 ]; then
echo "=== restart: kill taler-exchange-httpd ==="
kill_httpd
fi
echo "Start taler-exchange-httpd:"
LOG_FILE="$LOG_DIR/taler-exchange-httpd-$(date +%Y-%m-%d).log"
mkdir -p "$LOG_DIR"
touch "$LOG_FILE" 2>/dev/null || true
if [ "$DO_RESTART" -eq 0 ] && [ -n "$(list_httpd_pids)" ]; then
echo "taler-exchange-httpd already running"
else
nohup taler-exchange-httpd -c "$CONF" -L INFO >>"$LOG_FILE" 2>&1 &
disown 2>/dev/null || true
sleep 2
fi
echo "Live processes:"
ps -eo pid,stat,args 2>/dev/null | grep taler-exchange-httpd | grep -v grep | grep -v ' Z ' || true
if [ -x /usr/local/bin/check_exchange-health.sh ]; then
/usr/local/bin/check_exchange-health.sh || exit 1
elif [ -x ./check_exchange-health.sh ]; then
./check_exchange-health.sh || exit 1
fi
exit 0

View file

@ -0,0 +1,43 @@
#!/bin/bash
# Start wire/db helpers only (root, no interactive shell, no systemd).
# Uses nohup so helpers survive the launching shell.
set -euo pipefail
ROOT=$(cd "$(dirname "$0")" && pwd)
if [ -x "$ROOT/ensure_exchange_helpers.sh" ]; then
exec "$ROOT/ensure_exchange_helpers.sh"
fi
if [ -x /usr/local/bin/ensure_exchange_helpers.sh ]; then
exec /usr/local/bin/ensure_exchange_helpers.sh
fi
# Fallback if ensure not installed yet
CONF=/etc/taler-exchange/taler-exchange.conf
LOG_DIR=/var/log/taler-exchange
[ "$(id -u)" -eq 0 ] || { echo "root only"; exit 1; }
mkdir -p "$LOG_DIR"
start_bg() {
local user="$1"; shift
local name="$1"; shift
local bin="$1"
if pgrep -u "$user" -x "$(basename "$bin")" >/dev/null 2>&1; then
echo "already: $name"
return 0
fi
echo "start: $name as $user"
nohup runuser -u "$user" -- "$@" >>"$LOG_DIR/${name}.log" 2>&1 </dev/null &
disown 2>/dev/null || true
sleep 0.4
}
start_bg taler-exchange-aggregator taler-exchange-aggregator \
/usr/bin/taler-exchange-aggregator -c "$CONF" -L INFO
start_bg taler-exchange-closer taler-exchange-closer \
/usr/bin/taler-exchange-closer -c "$CONF" -L INFO
start_bg taler-exchange-wire taler-exchange-wirewatch \
/usr/bin/taler-exchange-wirewatch -c "$CONF" -L INFO
start_bg taler-exchange-wire taler-exchange-transfer \
/usr/bin/taler-exchange-transfer -c "$CONF" -L INFO
sleep 1
pgrep -af 'taler-exchange-(aggregator|closer|wirewatch|transfer|httpd|secmod)' || true
[ -x /usr/local/bin/check_exchange-health.sh ] && /usr/local/bin/check_exchange-health.sh || true

View file

@ -0,0 +1,91 @@
#!/bin/bash
# One-shot: offline enable wire account + fees, upload to local exchange, verify /keys.
# Run as root inside container taler-hacktivism-exchange-ansible.
set -euo pipefail
CONF=/etc/taler-exchange/taler-exchange.conf
OV=/etc/taler-exchange/exchange-overrides.conf
PAYTO='payto://x-taler-bank/bank.hacktivism.ch/exchange?receiver-name=GOA%20Exchange'
LOCAL_BASE='http://127.0.0.1:9011/'
OPS=/tmp/offline-wire-ops-$$.json
OFFLINE_USER=taler-exchange-offline
if [ "$(id -u)" -ne 0 ]; then
echo "Run as root in exchange container" >&2
exit 1
fi
echo "=== 1. Ensure wire account stanza in exchange-overrides ==="
if ! grep -q '\[exchange-account-1\]' "$OV" 2>/dev/null; then
cat >>"$OV" <<'EOF'
### Regional bank (libeufin, x-taler-bank) — no IBAN
[exchange-account-1]
PAYTO_URI = payto://x-taler-bank/bank.hacktivism.ch/exchange?receiver-name=GOA%20Exchange
ENABLE_CREDIT = YES
ENABLE_DEBIT = YES
@inline-secret@ exchange-accountcredentials-1 ../secrets/exchange-accountcredentials-1.secret.conf
EOF
echo "appended exchange-account-1"
else
echo "exchange-account-1 already present"
fi
# credentials must be readable by wire helpers + httpd
if [ -f /etc/taler-exchange/secrets/exchange-accountcredentials-1.secret.conf ]; then
chown root:taler-exchange-wire /etc/taler-exchange/secrets/exchange-accountcredentials-1.secret.conf 2>/dev/null \
|| chown root:root /etc/taler-exchange/secrets/exchange-accountcredentials-1.secret.conf
chmod 640 /etc/taler-exchange/secrets/exchange-accountcredentials-1.secret.conf
fi
echo "=== 2. Point BASE_URL at local httpd for offline upload ==="
# save public URL
PUBLIC_BASE=$(taler-config -c "$CONF" -s exchange -o BASE_URL 2>/dev/null || echo 'https://exchange.hacktivism.ch/')
# temporary override file (highest priority if inlined last — patch OV)
if grep -q '^BASE_URL' "$OV"; then
sed -i "s|^BASE_URL = .*|BASE_URL = ${LOCAL_BASE}|" "$OV"
else
echo "BASE_URL = ${LOCAL_BASE}" >>"$OV"
fi
echo "BASE_URL now: $(taler-config -c "$CONF" -s exchange -o BASE_URL)"
echo "=== 3. Sign enable-account + wire-fee + global-fee (as offline user) ==="
# global-fee: year, history, account, purse, purse_timeout, history_expiration, max_free_purses
runuser -u "$OFFLINE_USER" -- taler-exchange-offline -c "$CONF" \
enable-account "$PAYTO" \
wire-fee now x-taler-bank GOA:0 GOA:0 \
global-fee now GOA:0 GOA:0 GOA:0 '1 day' '1 year' 5 \
>"$OPS"
echo "ops bytes: $(wc -c <"$OPS")"
head -c 200 "$OPS"; echo
echo "=== 4. Upload ops to exchange ==="
runuser -u "$OFFLINE_USER" -- taler-exchange-offline -c "$CONF" upload <"$OPS"
echo "upload exit: $?"
echo "=== 5. Restore public BASE_URL ==="
sed -i "s|^BASE_URL = .*|BASE_URL = ${PUBLIC_BASE}|" "$OV"
# ensure trailing slash style
if ! grep -q "BASE_URL = https://exchange.hacktivism.ch" "$OV"; then
sed -i "s|^BASE_URL = .*|BASE_URL = https://exchange.hacktivism.ch/|" "$OV"
fi
echo "BASE_URL restored: $(taler-config -c "$CONF" -s exchange -o BASE_URL)"
echo "=== 6. Probe /keys (may need httpd already running) ==="
for i in 1 2 3 4 5; do
code=$(curl -sS -m 8 -o /tmp/keys-out.json -w '%{http_code}' http://127.0.0.1:9011/keys || echo fail)
sz=$(wc -c </tmp/keys-out.json 2>/dev/null || echo 0)
echo "try $i: http=$code size=$sz"
if [ "$code" = "200" ] && [ "${sz:-0}" -gt 100 ]; then
echo "KEYS_OK"
head -c 250 /tmp/keys-out.json; echo
# show accounts if present
grep -oE '"payto_uri"[^,}]+|"master_public_key"[^,}]+' /tmp/keys-out.json | head -10 || true
exit 0
fi
sleep 2
done
echo "KEYS_NOT_YET — check logs"
tail -30 /var/log/taler-exchange/taler-exchange-httpd-*.log 2>/dev/null | tail -30
exit 1