export-nexus-withdrawals.sh (4774B)
1 #!/bin/bash 2 # This file is part of TALER 3 # Copyright (C) 2026 Taler Systems SA 4 # 5 # TALER is free software; you can redistribute it and/or modify it under the 6 # terms of the GNU General Public License as published by the Free Software 7 # Foundation; either version 3, or (at your option) any later version. 8 # 9 # TALER is distributed in the hope that it will be useful, but WITHOUT ANY 10 # WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR 11 # A PARTICULAR PURPOSE. See the GNU General Public License for more details. 12 # 13 # You should have received a copy of the GNU General Public License along with 14 # TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> 15 16 set -euo pipefail 17 18 usage() { 19 cat <<'EOF' 20 Usage: export-nexus-withdrawals.sh YYYY-MM-dd OUTPUT.csv [PSQL-CONNECTION] 21 22 Export Nexus incoming transfers of type 'reserve', from midnight UTC on the 23 given date (inclusive). Requires psql, OpenSSL, and PostgreSQL 12 or later. 24 PSQL-CONNECTION is a database name or libpq connection string; if omitted, 25 psql uses its usual PGDATABASE, PGHOST, PGUSER, etc. environment variables. 26 27 Columns: incoming_transaction_id, execution_time_utc, amount, sender_iban_hash. 28 Amounts use the currency configured for this Nexus instance (not stored in SQL). 29 The hash is SHA-256(random 32-byte salt || uppercase IBAN), truncated to 30 128 bits (32 hexadecimal characters). One fresh salt is used for the entire 31 export, so senders can be grouped within a file but hashes change between runs. 32 The salt is not included in the CSV or saved by the script. 33 Names, payment subjects, and raw payto URIs are not exported. 34 Unsupported/missing IBANs cause an error instead of silently dropping transfers. 35 The output file is replaced only after a successful export, with mode 0600. 36 37 Example: 38 export-nexus-withdrawals.sh 2026-01-01 withdrawals.csv 'dbname=libeufin-nexus' 39 EOF 40 } 41 42 if [[ ${1:-} == --help || ${1:-} == -h ]]; then 43 usage 44 exit 0 45 fi 46 if (( $# < 2 || $# > 3 )); then 47 usage >&2 48 exit 1 49 fi 50 if [[ ! $1 =~ ^[0-9]{4}-[0-9]{2}-[0-9]{2}$ ]]; then 51 echo 'Start date must have the format YYYY-MM-dd.' >&2 52 exit 1 53 fi 54 55 since=$1 56 output=$2 57 connection=() 58 if (( $# == 3 )); then 59 connection=(--dbname="$3") 60 fi 61 62 umask 077 63 salt=$(openssl rand -hex 32) 64 temporary=$(mktemp -- "${output}.tmp.XXXXXXXX") 65 trap 'rm -f -- "$temporary"' EXIT 66 67 # Feed the salt through stdin, keeping it out of the psql process arguments. 68 # A repeatable-read snapshot keeps validation and export consistent. 69 { 70 printf '\\set export_salt %s\n' "$salt" 71 cat <<'SQL' 72 BEGIN TRANSACTION ISOLATION LEVEL REPEATABLE READ READ ONLY; 73 SET LOCAL TIME ZONE 'UTC'; 74 SET LOCAL DateStyle = 'ISO, YMD'; 75 76 -- Timestamps in Nexus are microseconds since the Unix epoch. 77 SELECT (extract(epoch FROM :'since'::date::timestamptz) * 1000000)::bigint AS cutoff 78 \gset 79 80 -- Match both payto://iban/IBAN and payto://iban/BIC/IBAN, ignoring 81 -- query parameters (including the sender's name). Fail closed on other forms. 82 \set iban_pattern '^payto://iban/(?:[A-Z0-9]+/)?([A-Z]{2}[0-9]{2}[A-Z0-9]{11,30})(?:[?].*)?$' 83 SELECT NOT EXISTS ( 84 SELECT 1 85 FROM libeufin_nexus.incoming_transactions AS i 86 JOIN libeufin_nexus.talerable_incoming_transactions AS t 87 USING (incoming_transaction_id) 88 WHERE t.type = 'reserve' AND i.execution_time >= :cutoff 89 AND (i.debit_payto IS NULL OR i.debit_payto !~* :'iban_pattern') 90 ) AS valid_ibans 91 \gset 92 \if :valid_ibans 93 \else 94 \echo 'Export failed: a withdrawal has a missing or unsupported IBAN payto URI.' 95 -- Make ON_ERROR_STOP terminate with a nonzero exit status. 96 DO $$ BEGIN RAISE EXCEPTION 'Cannot export all sender IBANs'; END $$; 97 \endif 98 99 COPY ( 100 SELECT i.incoming_transaction_id, 101 to_char( 102 timestamptz 'epoch' 103 + (i.execution_time / 1000000) * interval '1 second' 104 + (i.execution_time % 1000000) * interval '1 microsecond', 105 'YYYY-MM-DD"T"HH24:MI:SS.US"Z"' 106 ) AS execution_time_utc, 107 (i.amount).val::text || '.' || lpad((i.amount).frac::text, 8, '0') AS amount, 108 left(encode(sha256( 109 decode(:'export_salt', 'hex') 110 || convert_to(upper(substring(i.debit_payto FROM ('(?i)' || :'iban_pattern'))), 'UTF8') 111 ), 'hex'), 32) AS sender_iban_hash 112 FROM libeufin_nexus.incoming_transactions AS i 113 JOIN libeufin_nexus.talerable_incoming_transactions AS t 114 USING (incoming_transaction_id) 115 WHERE t.type = 'reserve' AND i.execution_time >= :cutoff 116 ORDER BY i.execution_time, i.incoming_transaction_id 117 ) TO STDOUT WITH (FORMAT CSV, HEADER); 118 COMMIT; 119 SQL 120 } | psql -X --quiet --set=ON_ERROR_STOP=1 --set=since="$since" "${connection[@]}" > "$temporary" 121 122 mv -f -- "$temporary" "$output"