summaryrefslogtreecommitdiff
path: root/regional-currency
diff options
context:
space:
mode:
Diffstat (limited to 'regional-currency')
-rw-r--r--regional-currency/.gitignore2
-rw-r--r--regional-currency/.shellcheckrc1
-rw-r--r--regional-currency/ChangeLog7
-rw-r--r--regional-currency/README2
-rwxr-xr-xregional-currency/config.py491
-rwxr-xr-xregional-currency/config_nginx.sh40
-rwxr-xr-xregional-currency/diagnose.sh125
-rwxr-xr-xregional-currency/functions.sh36
-rwxr-xr-xregional-currency/install_packages.sh84
-rwxr-xr-xregional-currency/list-incoming.sh33
-rwxr-xr-xregional-currency/main.sh104
-rw-r--r--regional-currency/nginx-conf/backend.taler-nginx.conf19
-rw-r--r--regional-currency/nginx-conf/bank.taler-nginx.conf23
-rw-r--r--regional-currency/nginx-conf/exchange.taler-nginx.conf16
-rwxr-xr-xregional-currency/setup-exchange.sh242
-rwxr-xr-xregional-currency/setup-libeufin.sh138
-rwxr-xr-xregional-currency/setup-merchant.sh14
-rwxr-xr-xregional-currency/upgrade.sh30
-rw-r--r--regional-currency/vagrant/.gitignore1
-rw-r--r--regional-currency/vagrant/README2
-rw-r--r--regional-currency/vagrant/Vagrantfile77
-rwxr-xr-xregional-currency/withdraw.sh32
22 files changed, 1519 insertions, 0 deletions
diff --git a/regional-currency/.gitignore b/regional-currency/.gitignore
new file mode 100644
index 0000000..26790f8
--- /dev/null
+++ b/regional-currency/.gitignore
@@ -0,0 +1,2 @@
+config/
+setup.log \ No newline at end of file
diff --git a/regional-currency/.shellcheckrc b/regional-currency/.shellcheckrc
new file mode 100644
index 0000000..e170f39
--- /dev/null
+++ b/regional-currency/.shellcheckrc
@@ -0,0 +1 @@
+disable=SC2018,SC2019
diff --git a/regional-currency/ChangeLog b/regional-currency/ChangeLog
new file mode 100644
index 0000000..b2310dd
--- /dev/null
+++ b/regional-currency/ChangeLog
@@ -0,0 +1,7 @@
+Sun Mar 10 12:15:15 PM CET 2024
+ Changed the scripts to enable (!) taler-merchant.target
+ instead of just the taler-merchant-httpd service.
+
+ Added automatically setting the wire-fee for IBAN.
+
+ Added code to automatically run taler-exchange-offline daily (#8623).
diff --git a/regional-currency/README b/regional-currency/README
new file mode 100644
index 0000000..599336a
--- /dev/null
+++ b/regional-currency/README
@@ -0,0 +1,2 @@
+Refer to the following document:
+https://docs.taler.net/libeufin/regional-manual.html#guided-basic-setup
diff --git a/regional-currency/config.py b/regional-currency/config.py
new file mode 100755
index 0000000..e382927
--- /dev/null
+++ b/regional-currency/config.py
@@ -0,0 +1,491 @@
+#!/usr/bin/env python3
+"""Python script to ask questions using an interactive prompt"""
+
+import base64
+import os
+import re
+import subprocess
+import urllib.parse
+import uuid
+import getpass
+from base64 import b64decode, b64encode
+from typing import Callable, Dict, TypeVar
+
+import argon2
+from Crypto.Cipher import ChaCha20_Poly1305
+from Crypto.Hash import SHA512
+from Crypto.Protocol.KDF import PBKDF2
+from Crypto.Random import get_random_bytes
+
+# Early exit if already loaded
+if os.environ.get("CONFIG_LOADED") == "y":
+ exit(0)
+
+log = open("setup.log", "ab", buffering=0)
+CONFIG_FILE = "config/user.conf"
+BIC_PATTERN = re.compile("[A-Z0-9]{4}[A-Z]{2}[A-Z0-9]{2}(?:[A-Z0-9]{3})?")
+IBAN_PATTERN = re.compile("[A-Z]{2}[0-9]{2}[A-Z0-9]{,28}")
+
+
+def load_conf() -> Dict[str, str]:
+ """Load user configuration file"""
+ conf = {}
+ with open(CONFIG_FILE, "r") as f:
+ for kv in f.read().splitlines():
+ if len(kv) != 0:
+ [k, v] = [part.strip() for part in kv.split("=", 1)]
+ if v.startswith('"') and v.endswith('"'):
+ conf[k] = v.strip('"').replace('\\"', '"')
+ elif v.startswith("'") and v.endswith("'"):
+ conf[k] = v.strip("'").replace("'\\''", "'").replace("\\'", "'")
+ else:
+ conf[k] = v
+ return conf
+
+
+conf = load_conf()
+result_conf = {**conf, "CONFIG_LOADED": "y"}
+
+def store_conf():
+ """Update the configuration file"""
+ content = ""
+ for key, value in conf.items():
+ escaped = value.replace("'", "'\\''")
+ content += f"{key}='{escaped}'\n"
+ with open(CONFIG_FILE, "w") as f:
+ f.write(content)
+
+def add_conf(name: str, value: str):
+ """Update a user configuration value and update the configuration file"""
+ conf[name] = value
+ result_conf[name] = value
+ store_conf()
+
+def run_cmd(
+ cmd: list[str], input: str | None = None, env: Dict[str, str] | None = None
+) -> int:
+ """Run a command in a child process and return its exit code"""
+ result = subprocess.run(
+ cmd,
+ stdout=subprocess.PIPE,
+ stderr=subprocess.STDOUT,
+ input=input.encode() if input is not None else None,
+ stdin=subprocess.DEVNULL if input is None else None,
+ env=env,
+ )
+ log.write(result.stdout)
+ if result.returncode != 0:
+ print(result.stdout.decode("utf-8"), end="")
+ return result.returncode
+
+
+def try_cmd(
+ cmd: list[str], input: str | None = None, env: Dict[str, str] | None = None
+) -> bool:
+ """Run a command in a child process and return if successful"""
+ return run_cmd(cmd, input, env) == 0
+
+
+A = TypeVar("A")
+T = TypeVar("T")
+
+
+def conf_value(
+ name: str | None,
+ action: Callable[[], str | None],
+ default: T | None = None,
+ check: Callable[[str], T | None] = lambda it: it,
+ fmt: Callable[[T], str] = lambda it: str(it),
+) -> T:
+ """
+ Logic to configure a value
+
+ :param name: if present will try to fetch the current value and will store the new value
+ :param action: how a value will be obtained
+ :param default: default value to use if no value is given
+ :param check: check and normalize the value
+ :param fmt: format value for storage
+ :return: the configuration value
+ """
+ value = None
+
+ # Fetch current value
+ if name is not None:
+ curr = conf.get(name)
+ if curr is not None:
+ # Check the current value and ask again if invalid
+ value = check(curr)
+
+ # Ask for a new value until we get a valid one
+ while value is None:
+ new = action()
+ # Use default if no value was provided else check the new value
+ value = check(new) if new is not None else default
+
+ # Store the new value
+ if name is not None:
+ add_conf(name, fmt(value))
+
+ return value
+
+
+def ask(
+ name: str | None,
+ msg: str,
+ default: T | None = None,
+ check: Callable[[str], T | None] = lambda it: it,
+ fmt: Callable[[T], str] = lambda it: str(it),
+ secret: bool = False
+) -> T:
+ """
+ Prompt the user to configure a value
+ :param name: if present will try to fetch the current value and will store the new value
+ :param msg: the message to prompt the user with
+ :param default: default value to use if no value is obtained
+ :param check: check and normalize the value
+ :param fmt: format value for storage
+ :param secret: hide the input content
+ :return: the configuration value
+ """
+
+ def do_ask() -> str | None:
+ # Log the prompt
+ log.write(msg.encode() + "\n".encode())
+ # Actual prompt
+ if secret:
+ raw = getpass.getpass(msg).strip()
+ else:
+ raw = input(msg).strip()
+ if raw == "":
+ if default is None:
+ print("You must enter a value")
+ return None
+ return raw
+
+ return conf_value(name, do_ask, default, check, fmt)
+
+
+def ask_str(name: str | None, msg: str, default: str | None = None, secret: bool = False) -> str:
+ "Prompt the user to configure a string"
+ return ask(name, msg, default, secret=secret)
+
+
+def ask_bic(name: str | None, msg: str, default: str | None = None) -> str:
+ "Prompt the user to configure a BIC"
+
+ def check_bic(raw: str) -> str | None:
+ raw = raw.translate({ord(i): None for i in " -"})
+ if not BIC_PATTERN.fullmatch(raw):
+ print("Invalid BIC")
+ return None
+ else:
+ return raw
+
+ return ask(name, msg, default, check_bic)
+
+
+def ask_iban(name: str | None, msg: str, default: str | None = None) -> str:
+ "Prompt the user to configure a IBAN"
+
+ def check_iban(raw: str) -> str | None:
+ raw = raw.translate({ord(i): None for i in " -"})
+ if not IBAN_PATTERN.fullmatch(raw):
+ print("Invalid IBAN") # Checksum check ?
+ return None
+ else:
+ return raw
+
+ return ask(name, msg, default, check_iban)
+
+
+def ask_currency(name: str, msg: str, default: str | None = None) -> str:
+ "Prompt the user to configure a currency name"
+
+ def check_currency(currency: str) -> str | None:
+ currency = currency.upper()
+ if not all([c.isascii() and c.isalpha() for c in currency]):
+ print("The currency name must be an ASCII alphabetic string")
+ elif len(currency) < 3 or 11 < len(currency):
+ print("The currency name had to be between 3 and 11 characters long")
+ else:
+ return currency
+ return None
+
+ return ask(name, msg, default, check_currency)
+
+
+def ask_host(name: str, msg: str, default: str | None = None) -> str:
+ "Prompt the user to configure the installation hostname"
+
+ def check_host(host: str) -> str | None:
+ success = True
+ for subdomain in ["backend", "bank", "exchange"]:
+ success = try_cmd(["ping", "-c", "1", f"{subdomain}.{host}"]) and success
+ if success:
+ return host
+ else:
+ return None
+
+ return ask(name, msg, default, check_host)
+
+
+def ask_terms(name: str, msg: str, kind: str) -> str:
+ "Prompt the user to select a ToS/privacy policy"
+
+ # msg = "9.1. Enter the filename of the ToS. Some available options are:\n"
+ tos_msg = msg
+
+ # Recollect example ToS files
+ tos_path = "/usr/share/taler/terms"
+ for f in os.listdir(tos_path):
+ tos_file = os.path.join(tos_path, f)
+ if os.path.isfile(tos_file) and f.endswith(".rst") and kind in f:
+ tos_msg += f"- {tos_file}\n"
+
+ tos_msg += "=> "
+
+ def check_file(path: str) -> str | None:
+ if not os.path.isfile(path):
+ print("Not a file") # Checksum check ?
+ return None
+ else:
+ return path
+
+ return ask(name, tos_msg, None, check_file)
+
+
+def ask_yes_no(name: str | None, msg: str, default: bool | None = None) -> bool:
+ "Prompt the user to configure a boolean"
+
+ def check_yes_no(raw: str) -> bool | None:
+ raw = raw.lower()
+ if raw == "y" or raw == "yes":
+ return True
+ elif raw == "n" or raw == "no":
+ return False
+ else:
+ print("Expected 'y' or 'n'")
+ return None
+
+ return ask(name, msg, default, check_yes_no, lambda it: "y" if it else "n")
+
+
+# ----- Crypto ----- #
+
+
+def ask_config_password() -> str:
+ "Prompt the user to configure a password stored hashed with argon2id"
+ ph = argon2.PasswordHasher()
+ hash = conf.get("CONFIG_PASSWORD")
+ passwd = None
+ if hash is not None:
+ while True:
+ passwd = ask_str(None, "Enter the config password : ", secret=True)
+ try:
+ ph.verify(hash, passwd)
+ break
+ except argon2.exceptions.VerifyMismatchError:
+ print("invalid password")
+ else:
+ passwd = ask_str(None, "1.1 Choose a config password : ", secret=True)
+
+ if hash is None or ph.check_needs_rehash(hash):
+ add_conf("CONFIG_PASSWORD", ph.hash(passwd))
+
+ return passwd
+
+
+def ask_secret(
+ name: str, msg: str, passwd: str | None, default: str | None = None
+) -> str:
+ "Prompt the user to configure a string stored encryped using pbkdf2_sha512 and chacha20_poly1305"
+ if passwd is None:
+ return ask_str(name, msg, default)
+ else:
+ raw = conf.get(name)
+ plaintext = None
+ if raw is not None:
+ method = "$pbkdf2_sha512_chacha20_poly1305$1000000$"
+ if raw.startswith(method):
+ salt, nonce, tag, ciphertext = [
+ b64decode(it) for it in raw.removeprefix(method).split("$", 3)
+ ]
+ key = PBKDF2(passwd, salt, 32, count=1000000, hmac_hash_module=SHA512)
+ cipher = ChaCha20_Poly1305.new(key=key, nonce=nonce)
+ cipher.update(name.encode())
+ plaintext = cipher.decrypt_and_verify(ciphertext, tag).decode()
+ else:
+ salt = get_random_bytes(16)
+ key = PBKDF2(passwd, salt, 32, count=1000000, hmac_hash_module=SHA512)
+ cipher = ChaCha20_Poly1305.new(key=key)
+ cipher.update(name.encode())
+ ciphertext, tag = cipher.encrypt_and_digest(raw.encode())
+ add_conf(
+ name,
+ f"$pbkdf2_sha512_chacha20_poly1305$1000000${base64.b64encode(salt).decode()}${base64.b64encode(cipher.nonce).decode()}${base64.b64encode(tag).decode()}${base64.b64encode(ciphertext).decode()}",
+ )
+ else:
+ plaintext = ask_str(None, msg, default, True)
+ salt = get_random_bytes(16)
+ key = PBKDF2(passwd, salt, 32, count=1000000, hmac_hash_module=SHA512)
+ cipher = ChaCha20_Poly1305.new(key=key)
+ cipher.update(name.encode())
+ ciphertext, tag = cipher.encrypt_and_digest(plaintext.encode())
+ add_conf(
+ name,
+ f"$pbkdf2_sha512_chacha20_poly1305$1000000${base64.b64encode(salt).decode()}${base64.b64encode(cipher.nonce).decode()}${base64.b64encode(tag).decode()}${base64.b64encode(ciphertext).decode()}",
+ )
+ result_conf[name] = plaintext
+ return plaintext
+
+
+# ----- Prompt ----- #
+
+config_passwd = (
+ ask_config_password()
+ if ask_yes_no(
+ "DO_CONFIG_ENCRYPTION",
+ "1. Do you want to encrypt sensitive config values (Y/n): ",
+ True,
+ )
+ else None
+)
+ask_currency(
+ "CURRENCY",
+ "2. Enter the name of the regional currency (e.g. 'NETZBON'): ",
+ "NETZBON",
+)
+do_conversion = ask_yes_no(
+ "DO_CONVERSION",
+ "3. Do you want setup regional currency conversion to fiat currency (Y/n): ",
+ True,
+)
+if do_conversion:
+ ask_currency(
+ "FIAT_CURRENCY",
+ "3.1. Enter the name of the fiat currency (e.g. 'CHF'): ",
+ "CHF",
+ )
+ ask_str(
+ "FIAT_BANK_NAME",
+ "3.2. Enter the name of your fiat bank (e.g. POSTFINANCE AG): ",
+ )
+ iban = ask_iban(
+ "FIAT_ACCOUNT_IBAN",
+ "3.3. Enter the IBAN of your fiat bank account (e.g. 'CH7789144474425692816'): ",
+ )
+ bic = ask_bic(
+ "FIAT_ACCOUNT_BIC",
+ "3.4. Enter the BIC of your fiat bank account (e.g. 'POFICHBEXXX'): ",
+ )
+ name = ask_str(
+ "FIAT_ACCOUNT_NAME", "3.5. Enter the legal name of your fiat bank account: "
+ )
+ params = urllib.parse.urlencode({"receiver-name": name})
+ add_conf("CONVERSION_PAYTO", f"payto://iban/{bic}/{iban}?{params}")
+bank_name = ask_str(
+ "BANK_NAME",
+ "4. Enter the human-readable name of the bank (e.g. 'Taler Bank'): ",
+ "Taler Bank",
+)
+ask_host("DOMAIN_NAME", "5. Enter the domain name (e.g. 'example.com'): ")
+if ask_yes_no("ENABLE_TLS", "6. Setup TLS using Let's Encrypt? (Y/n): ", True):
+ ask_str("TLS_EMAIL", "6.1. Enter an email address for Let's Encrypt: ")
+
+ def ask_tos():
+ print(
+ "6.2. Please read the Terms of Service at https://letsencrypt.org/documents/LE-SA-v1.3-September-21-2022.pdf."
+ )
+ if not ask_yes_no(
+ None,
+ "6.2. You must agree in order to register with the ACME server. Do you agree? (y/n): ",
+ False,
+ ):
+ print("You must agree in order to register with the ACME server")
+ return None
+ else:
+ return "y"
+
+ conf_value("TLS_TOS", ask_tos)
+ add_conf("PROTO", "https")
+else:
+ add_conf("PROTO", "http")
+
+add_conf(
+ "DO_OFFLINE", "y"
+) # TODO support offline setup again when the documentation is ready
+
+if ask_yes_no(
+ "DO_TELESIGN",
+ "7. Setup SMS two-factor authentication using Telesign https://www.telesign.com? (Y/n): ",
+ True,
+):
+
+ def ask_telesign():
+ customer_id = ask_str(None, "7.1. Enter your Telesign Customer ID: ")
+ api_key = ask_str(None, "7.2. Enter your Telesign API Key: ")
+ phone_number = ask_str(
+ None,
+ "6.3. Enter a phone number to test your API key (e.g. '+447911123456'): ",
+ )
+ auth_token = base64.b64encode(f"{customer_id}:{api_key}".encode()).decode()
+ if not try_cmd(
+ ["libeufin-tan-sms.sh", phone_number],
+ f"T-12345 is your verification code for {bank_name} setup",
+ {**os.environ, "AUTH_TOKEN": auth_token},
+ ):
+ print(
+ "Failed to send an SMS using Telesign API, check your credentials and phone number"
+ )
+ return None
+ code = ask_str(None, f"7.4. Enter the code received by {phone_number} : ")
+ if code != "12345" and code != "T-12345":
+ print(
+ f"Wrong code got '{code}' expected '12345', check your credentials and phone number"
+ )
+ return None
+ return auth_token
+
+ conf_value("TELESIGN_AUTH_TOKEN", ask_telesign)
+generated_password= str(uuid.uuid4())
+admin_password = ask_secret(
+ "BANK_ADMIN_PASSWORD",
+ "8. Enter the admin password for the bank (or press enter to autogenerate password): ",
+ config_passwd,
+ generated_password,
+)
+add_conf("BANK_ADMIN_PASSWORD_GENERATED", "y" if generated_password==admin_password else "n")
+
+if ask_yes_no(
+ "DO_EXCHANGE_TERMS",
+ "9. Do you wish to configure terms of service (ToS) for the exchange? (Y/n): ",
+ True,
+):
+ ask_terms(
+ "EXCHANGE_TERMS_FILE",
+ "9.1. Enter the filename of the ToS. Some available options are:\n",
+ "-tos-",
+ )
+
+if ask_yes_no(
+ "DO_EXCHANGE_PRIVACY",
+ "10. Do you wish to configure a privacy policy for the exchange? (Y/n): ",
+ True,
+):
+ ask_terms(
+ "EXCHANGE_PRIVACY_FILE",
+ "10.1. Enter the filename of the privacy policy. Some available options are:\n",
+ "-pp-",
+ )
+
+# Update on disk format even if nothing have changed
+store_conf()
+
+# ----- Return conf ----- #
+
+content = ""
+for key, value in result_conf.items():
+ escaped = value.replace("'", "'\\''")
+ content += f"export {key}='{escaped}'\n"
+with os.fdopen(3, "w") as f:
+ f.write(content)
diff --git a/regional-currency/config_nginx.sh b/regional-currency/config_nginx.sh
new file mode 100755
index 0000000..84df1e8
--- /dev/null
+++ b/regional-currency/config_nginx.sh
@@ -0,0 +1,40 @@
+#!/bin/bash
+
+source functions.sh
+source config/user.conf
+source config/internal.conf
+
+export PROTO
+export DOMAIN_NAME
+export BANK_PORT
+
+envsubst <nginx-conf/backend.taler-nginx.conf >"/etc/nginx/sites-available/backend.${DOMAIN_NAME}"
+envsubst <nginx-conf/bank.taler-nginx.conf >"/etc/nginx/sites-available/bank.${DOMAIN_NAME}"
+envsubst <nginx-conf/exchange.taler-nginx.conf >"/etc/nginx/sites-available/exchange.${DOMAIN_NAME}"
+
+# Create nginx symlinks
+
+ln -sf /etc/nginx/sites-available/backend."${DOMAIN_NAME}" /etc/nginx/sites-enabled/backend."${DOMAIN_NAME}"
+ln -sf /etc/nginx/sites-available/bank."${DOMAIN_NAME}" /etc/nginx/sites-enabled/bank."${DOMAIN_NAME}"
+ln -sf /etc/nginx/sites-available/exchange."${DOMAIN_NAME}" /etc/nginx/sites-enabled/exchange."${DOMAIN_NAME}"
+
+if test "${ENABLE_TLS}" == "y"; then
+
+ # Replace http with https in the demobank-ui configuration
+
+ sed -i "s/http:\/\/bank./https:\/\/bank./g" /etc/libeufin/settings.json
+
+ # Certbot
+
+ say "Obtaining TLS certificates using Let's Encrypt"
+
+ certbot --nginx -n --agree-tos -m ${TLS_EMAIL} \
+ -d backend."${DOMAIN_NAME}" \
+ -d bank."${DOMAIN_NAME}" \
+ -d exchange."${DOMAIN_NAME}" &>> setup.log
+else
+ sed -i "s/https:\/\/bank./http:\/\/bank./g" /etc/libeufin/settings.json
+fi
+
+say "Restarting Nginx with new configuration"
+systemctl reload nginx &>> setup.log
diff --git a/regional-currency/diagnose.sh b/regional-currency/diagnose.sh
new file mode 100755
index 0000000..a0c513b
--- /dev/null
+++ b/regional-currency/diagnose.sh
@@ -0,0 +1,125 @@
+#!/usr/bin/env bash
+
+# This file is in the public domain.
+
+# Script for basic diagnostics of a Taler regio deployment.
+# @author Florian Dold <dold@taler.net>
+
+if [ "$(id -u)" -ne 0 ]; then
+ echo "FATAL: Please run as root." >&2
+ exit 1
+fi
+
+libeufin_bank_db=$(libeufin-bank config get libeufin-bankdb-postgres config)
+libeufin_nexus_db=$(libeufin-nexus config get libeufin-nexusdb-postgres config)
+exchange_db=$(taler-config -s exchangedb-postgres -o config)
+
+if [[ $libeufin_nexus_db != $libeufin_bank_db ]]; then
+ echo "FATAL: libeufin-bank and libeufin-nexus don't share the same database" >&2
+ exit 1
+fi
+
+libeufin_db=$libeufin_bank_db
+
+# runsql db RESNAME < query
+function runsql() {
+ local sql
+ read -r -d '' sql
+ res=$(cd / && sudo -u postgres psql "$1" -t --csv -c "$sql")
+ printf -v "$2" '%s' "$res"
+}
+
+#
+# Check for conversion trigger
+#
+
+runsql "$libeufin_db" have_conversion_triggers <<EOF
+select count(*) from information_schema.triggers
+ where trigger_schema='libeufin_nexus'
+ and trigger_name='cashin_link';
+EOF
+
+echo "have_conversion_triggers" $have_conversion_triggers
+
+#
+# Check for transactions
+#
+runsql "$libeufin_db" num_nexus_incoming_transactions <<EOF
+select count(*) from libeufin_nexus.incoming_transactions;
+EOF
+echo num_nexus_incoming_transactions: $num_nexus_incoming_transactions
+
+runsql "$libeufin_db" num_nexus_talerable_transactions <<EOF
+select count(*) from libeufin_nexus.talerable_incoming_transactions;
+EOF
+echo "num_nexus_talerable_transactions:" $num_nexus_talerable_transactions
+
+runsql "$libeufin_db" num_nexus_bounced_transactions <<EOF
+select count(*) from libeufin_nexus.bounced_transactions;
+EOF
+echo "num_nexus_bounced_transactions:" $num_nexus_bounced_transactions
+
+runsql "$libeufin_db" num_bank_exchange_incoming <<EOF
+select count(*) from libeufin_bank.taler_exchange_incoming;
+EOF
+echo "num_bank_exchange_incoming:" $num_bank_exchange_incoming
+
+runsql "$exchange_db" num_exchange_reserves_in <<EOF
+select count(*) from exchange.reserves_in;
+EOF
+echo num_exchange_reserves_in: $num_exchange_reserves_in
+
+runsql "$exchange_db" num_exchange_reserves <<EOF
+select count(*) from exchange.reserves;
+EOF
+echo num_exchange_reserves: $num_exchange_reserves
+
+
+function expect_unit_active() {
+ systemctl --quiet is-active "$1"
+ if [[ $? -ne 0 ]]; then
+ echo "WARNING: expected unit $1 to be active, but it is not active"
+ fi
+}
+
+libeufin_units=(
+libeufin-bank.service
+libeufin-nexus-ebics-fetch.service
+libeufin-nexus-ebics-submit.service
+)
+
+exchange_units=(
+taler-exchange-aggregator.service
+taler-exchange-closer.service
+taler-exchange-expire.service
+taler-exchange-httpd.service
+taler-exchange-secmod-cs.service
+taler-exchange-secmod-eddsa.service
+taler-exchange-secmod-rsa.service
+taler-exchange-transfer.service
+taler-exchange-wirewatch.service
+)
+
+
+merchant_units=(
+taler-merchant-httpd.service
+)
+
+all_units=()
+all_units+=( "${libeufin_units[@]}" "${exchange_units[@]}" "${merchant_units[@]}" )
+
+for unit in ${all_units[@]}; do
+ expect_unit_active "$unit"
+done
+
+SINCE="7 days ago"
+echo "analysing logs since $SINCE"
+
+for unit in ${all_units[@]}; do
+ num_warnings=$(journalctl -u "$unit" --since "$SINCE" | grep WARNING | wc -l)
+ num_errors=$(journalctl -u "$unit" --since "$SINCE" | grep ERROR | wc -l)
+ if [[ ( $num_errors -eq 0 ) && ( $num_warnings -eq 0 ) ]]; then
+ continue
+ fi
+ echo "Please check logs for $unit ($num_warnings warnings, $num_errors errors)"
+done
diff --git a/regional-currency/functions.sh b/regional-currency/functions.sh
new file mode 100755
index 0000000..0663fec
--- /dev/null
+++ b/regional-currency/functions.sh
@@ -0,0 +1,36 @@
+#!/bin/bash
+
+notify_err() {
+ say "errexit on line $(caller)"
+ say "Error messages can be found at the end of setup.log"
+ exit 1
+}
+
+trap notify_err ERR
+
+# Message
+function say() {
+ echo "TALER: " "$@" >> setup.log
+ echo "TALER: " "$@"
+}
+
+# Check user if the user is root
+function check_user() {
+ if [ "$(whoami)" != "root" ]; then
+ say "Please run this script as root"
+ exit 1
+ fi
+}
+
+# Set DISTRO to the detected distro or return non-zero
+# status if distro not supported.
+function detect_distro() {
+ unset DISTRO
+ [[ -f /etc/os-release ]] && source /etc/os-release
+ # shellcheck disable=SC2034
+ echo $NAME | grep Ubuntu >/dev/null && DISTRO=ubuntu && return 0
+ # shellcheck disable=SC2034
+ echo $NAME | grep Debian >/dev/null && DISTRO=debian && return 0
+ echo "Unsupported distro, should be either ubuntu or debian" >&2
+ return 1
+}
diff --git a/regional-currency/install_packages.sh b/regional-currency/install_packages.sh
new file mode 100755
index 0000000..3c3f2a5
--- /dev/null
+++ b/regional-currency/install_packages.sh
@@ -0,0 +1,84 @@
+#!/bin/bash
+# This file is in the public domain.
+
+set -eu
+
+source functions.sh
+
+detect_distro
+
+# Program versions
+PG_VERSION=15
+
+say "Installing necessary packages (this may take a while)..."
+
+## Update
+
+apt update &>> setup.log
+
+## General requirements
+
+apt install \
+ uuid-runtime \
+ make \
+ sudo \
+ curl \
+ jq \
+ wget \
+ nginx \
+ gettext-base \
+ postgresql-${PG_VERSION} \
+ postgresql-client-${PG_VERSION} \
+ dbconfig-pgsql \
+ certbot \
+ python3-sphinx \
+ python3-pip \
+ python3-certbot-nginx -y &>> setup.log
+
+pip3 install --break-system-packages \
+ sphinx-markdown-builder \
+ htmlark \
+ argon2-cffi \
+ pycryptodome &>> setup.log
+
+## Add GNU Taler deb.taler.net to /etc/apt/sources.list
+
+say "Adding GNU Taler apt repository"
+say "Detected distro $DISTRO"
+
+case $DISTRO in
+debian)
+ if test ${APT_NIGHTLY:-n} == y; then
+ say "Setup nightly packages"
+ echo "deb [trusted=yes] https://deb.taler.net/apt-nightly bookworm main" >/etc/apt/sources.list.d/taler.list
+ else
+ echo "deb [signed-by=/etc/apt/keyrings/taler-systems.gpg] https://deb.taler.net/apt/debian bookworm main" >/etc/apt/sources.list.d/taler.list
+ fi
+ ;;
+ubuntu)
+ echo "deb [signed-by=/etc/apt/keyrings/taler-systems.gpg] https://deb.taler.net/apt/ubuntu mantic taler-mantic" >/etc/apt/sources.list.d/taler.list
+ ;;
+*)
+ say "Unsupported distro: $DISTRO"
+ exit 1
+ ;;
+esac
+
+wget -P /etc/apt/keyrings https://taler.net/taler-systems.gpg &>> setup.log
+
+## Specific GNU Taler packages
+
+say "Installing GNU Taler packages (this may take a while)..."
+
+apt update &>> setup.log
+apt install \
+ taler-exchange \
+ taler-terms-generator \
+ taler-merchant \
+ taler-harness \
+ taler-wallet-cli \
+ taler-exchange-offline \
+ libeufin-bank \
+ libeufin-nexus \
+ -y \
+ &>> setup.log
diff --git a/regional-currency/list-incoming.sh b/regional-currency/list-incoming.sh
new file mode 100755
index 0000000..bb3a67d
--- /dev/null
+++ b/regional-currency/list-incoming.sh
@@ -0,0 +1,33 @@
+#!/usr/bin/env bash
+
+# This file is in the public domain.
+
+# Script for basic diagnostics of a Taler regio deployment.
+# @author Florian Dold <dold@taler.net>
+
+if [ "$(id -u)" -ne 0 ]; then
+ echo "FATAL: Please run as root." >&2
+ exit 1
+fi
+
+exchange_db=$(taler-config -s exchangedb-postgres -o config)
+
+# runsql db RESNAME < query
+function runsql() {
+ local sql
+ read -r -d '' sql
+ res=$(cd / && sudo -u postgres psql "$1" -t --csv -c "$sql")
+ printf -v "$2" '%s' "$res"
+}
+
+runsql "$exchange_db" reserves_in <<EOF
+select reserve_pub from exchange.reserves_in;
+EOF
+
+mapfile -t lines <<<$reserves_in
+
+for line in "${lines[@]}"; do
+ python3 -c "import binascii; import sys; sys.stdout.buffer.write(binascii.a2b_hex(sys.argv[1][2:]))" "$line" | gnunet-base32
+ echo
+done
+
diff --git a/regional-currency/main.sh b/regional-currency/main.sh
new file mode 100755
index 0000000..a88ac3d
--- /dev/null
+++ b/regional-currency/main.sh
@@ -0,0 +1,104 @@
+#!/bin/bash
+# This file is in the public domain.
+
+# main.sh is the main script that asks the questions and
+# puts the answers into environment variables located at "config/taler-internal.conf or config/taler.conf" files
+# Nginx configuration - Reads values directly from these "config files".
+
+set -eu
+
+# include functions source file
+
+source functions.sh
+
+# Clear logs
+
+> setup.log
+
+# include variables from configuration
+mkdir -p config/
+touch config/user.conf config/internal.conf
+# Values we generated
+source config/internal.conf
+
+# Ask questions to user
+# START USER INTERACTION
+say "Welcome to the GNU Taler regional currency setup!"
+say ""
+say "All configuration values asked during the setup script"
+say "can be changed in config/user.conf."
+say "Logs are written in setup.log."
+say ""
+
+# END USER INTERACTION
+
+# Check if the user is root, otherwise EXIT.
+check_user
+
+# Installation of deb packages required
+say ""
+say "Installing packages (step 1 of 6)"
+. install_packages.sh
+
+say ""
+say "Interactive configuration (step 2 of 6)"
+{ source <(./config.py 3>&1 >&4 4>&-); } 4>&1
+
+# Remove when libeufin currencies.conf is in sync with exchange
+cat >>/usr/share/libeufin/config.d/netzbon.conf <<EOF
+[CURRENCY-NETZBON]
+enabled=yes
+name=NetzBon
+code=NETZBON
+fractional_input_digits=2
+fractional_normal_digits=2
+fractional_trailing_zero_digits=2
+alt_unit_names={"0":"NETZBON"}
+EOF
+
+if test -z "${BANK_EXCHANGE_PASSWORD:-}"; then
+ BANK_EXCHANGE_PASSWORD=$(uuidgen)
+ echo "BANK_EXCHANGE_PASSWORD=\"${BANK_EXCHANGE_PASSWORD}\"" >>config/internal.conf
+fi
+
+if test -z "${BANK_PORT:-}"; then
+ echo "BANK_PORT=8080" >>config/user.conf
+ export BANK_PORT=8080
+fi
+
+say ""
+say "Configuring nginx (step 3 of 6)"
+./config_nginx.sh
+
+say ""
+say "Setting up libeufin (step 4 of 6)"
+./setup-libeufin.sh
+
+say ""
+say "Setting up exchange (step 5 of 6)"
+./setup-exchange.sh
+
+say ""
+say "Setting up merchant (step 6 of 6)"
+./setup-merchant.sh
+
+# Final message to the user
+say ""
+say "Congratulations, you have successfully installed GNU Taler"
+say "Your bank is at ${PROTO}://bank.${DOMAIN_NAME}/"
+if test ${BANK_ADMIN_PASSWORD_GENERATED} == y; then
+ say "You can connect to the bank web UI as 'admin' using '${BANK_ADMIN_PASSWORD}'"
+else
+ say "You can connect to the bank web UI as 'admin' using the password you entered earlier"
+fi
+say "A merchant is at ${PROTO}://backend.${DOMAIN_NAME}/"
+say "You should set credentials for the merchant soon."
+say "The exchange withdraw URI is taler://withdraw-exchange/exchange.${DOMAIN_NAME}/"
+
+if test ${DO_CONVERSION} == y; then
+ say "For currency conversion to work, you must manually complete"
+ say "the EBICS configuration."
+fi
+
+exit 0
+# END INSTALLATION
diff --git a/regional-currency/nginx-conf/backend.taler-nginx.conf b/regional-currency/nginx-conf/backend.taler-nginx.conf
new file mode 100644
index 0000000..ea267df
--- /dev/null
+++ b/regional-currency/nginx-conf/backend.taler-nginx.conf
@@ -0,0 +1,19 @@
+server {
+
+ listen 80;
+ listen [::]:80;
+
+ server_name backend.${DOMAIN_NAME};
+
+ # Bigger than default timeout to support long polling
+ proxy_read_timeout 6500s;
+ keepalive_requests 1000000;
+ keepalive_timeout 6500s;
+
+ location / {
+ proxy_pass http://unix:/var/run/taler/merchant-httpd/merchant-http.sock;
+ proxy_set_header X-Forwarded-Proto "${PROTO}";
+ proxy_set_header X-Forwarded-Host "backend.${DOMAIN_NAME}";
+ proxy_set_header X-Forwarded-Prefix /;
+ }
+}
diff --git a/regional-currency/nginx-conf/bank.taler-nginx.conf b/regional-currency/nginx-conf/bank.taler-nginx.conf
new file mode 100644
index 0000000..1c6a6d3
--- /dev/null
+++ b/regional-currency/nginx-conf/bank.taler-nginx.conf
@@ -0,0 +1,23 @@
+server {
+ listen 80;
+ listen [::]:80;
+
+ server_name bank.${DOMAIN_NAME};
+
+ access_log /var/log/nginx/libeufin-sandbox.log;
+ error_log /var/log/nginx/libeufin-sandbox.err;
+
+ # Bigger than default timeout to support long polling
+ proxy_read_timeout 6500s;
+ keepalive_requests 1000000;
+ keepalive_timeout 6500s;
+
+ # TODO should we proxy SPA with nginx for perf and fallback to bank server on 404 ?
+ location / {
+ proxy_pass http://localhost:${BANK_PORT};
+ #Fixes withdrawal http request
+ proxy_set_header X-Forwarded-Proto "${PROTO}";
+ proxy_set_header X-Forwarded-Host "bank.${DOMAIN_NAME}";
+ proxy_set_header X-Forwarded-Prefix /;
+ }
+}
diff --git a/regional-currency/nginx-conf/exchange.taler-nginx.conf b/regional-currency/nginx-conf/exchange.taler-nginx.conf
new file mode 100644
index 0000000..b1e9d0a
--- /dev/null
+++ b/regional-currency/nginx-conf/exchange.taler-nginx.conf
@@ -0,0 +1,16 @@
+server {
+
+ listen 80;
+ listen [::]:80;
+
+ server_name exchange.${DOMAIN_NAME};
+
+ # Bigger than default timeout to support long polling
+ proxy_read_timeout 6500s;
+ keepalive_requests 1000000;
+ keepalive_timeout 6500s;
+
+ location / {
+ proxy_pass http://unix:/var/run/taler/exchange-httpd/exchange-http.sock;
+ }
+}
diff --git a/regional-currency/setup-exchange.sh b/regional-currency/setup-exchange.sh
new file mode 100755
index 0000000..91f916c
--- /dev/null
+++ b/regional-currency/setup-exchange.sh
@@ -0,0 +1,242 @@
+#!/bin/bash
+# This file is in the public domain.
+#
+# This script configure and launches the Taler exchange.
+#
+# The environment must provide the following variables:
+#
+# - BANK_EXCHANGE_PASSWORD (exchange password for libeufin-bank)
+# - EXCHANGE_WIRE_GATEWAY_URL (where is the exchange wire gateway / libeufin-nexus)
+# - EXCHANGE_PAYTO (exchange account PAYTO)
+# - ENABLE_TLS (http or https?)
+# - DOMAIN_NAME: DNS domain name to use for the setup
+#
+
+set -eu
+
+notify_exit() {
+ [[ $1 == 0 ]] || echo Script "$0" failed, exit code "$1"
+}
+
+notify_err() {
+ echo "errexit on line $(caller)" >&2
+}
+
+trap '(exit 130)' INT
+trap '(exit 143)' TERM
+trap notify_err ERR
+# shellcheck disable=SC2154
+trap 'rc=$?; notify_exit $rc; exit $rc' EXIT
+
+# End of error handling setup
+
+source functions.sh
+source config/user.conf
+source config/internal.conf
+
+EXCHANGE_DB="taler-exchange"
+
+say "Beginning Exchange setup"
+
+if test -z "${BANK_EXCHANGE_PASSWORD:-}"; then
+ say "Failure: BANK_EXCHANGE_PASSWORD not set"
+ exit 1
+fi
+if test -z "${EXCHANGE_PAYTO:-}"; then
+ say "Failure: EXCHANGE_PAYTO not set"
+ exit 1
+fi
+
+function die() {
+ say "$1"
+ exit 1
+}
+
+# Just try if sudo works for diagnostics
+sudo -i -u taler-exchange-offline id >/dev/null || die "Error: Unable to switch to taler-exchange-offline user"
+
+# Create master key as taler-exchange-offline *unless* user already
+# set the MASTER_PUBLIC_KEY to some value we can use.
+export MASTER_PRIV_DIR=.local/share/taler/exchange/offline-keys
+export MASTER_PRIV_FILE=${MASTER_PRIV_DIR}/master.priv
+export SECMOD_TOFU_FILE=${MASTER_PRIV_DIR}/secm_tofus.pub
+if test -z "${MASTER_PUBLIC_KEY:-}"; then
+ if test "${DO_OFFLINE:-y}" == n; then
+ say "Error: No MASTER_PUBLIC_KEY but DO_OFFLINE set to NO"
+ exit 1
+ fi
+ say "Setting up offline key"
+ echo -e "[exchange-offline]\n"\
+ "MASTER_PRIV_FILE=\$HOME/${MASTER_PRIV_FILE}\n"\
+ "SECM_TOFU_FILE=\$HOME/${SECMOD_TOFU_FILE}\n"\
+ >/etc/taler/conf.d/offline-setup.conf
+
+ MASTER_PUBLIC_KEY=$(sudo -i -u taler-exchange-offline taler-exchange-offline -c /etc/taler/taler.conf -LDEBUG setup 2>> setup.log)
+ echo "MASTER_PUBLIC_KEY=\"${MASTER_PUBLIC_KEY}\"" >>config/user.conf
+ if test -z "${DO_OFFLINE:-}"; then
+ # Set 'DO_OFFLINE'
+ DO_OFFLINE=y
+ echo "DO_OFFLINE=y" >>config/user.conf
+ fi
+else
+ say "Master public key is $MASTER_PUBLIC_KEY"
+ if test ${DO_OFFLINE:-y} == y; then
+ MASTER_PUBLIC_KEY2=$(sudo -i -u taler-exchange-offline taler-exchange-offline -c /etc/taler/taler.conf setup 2>> setup.log)
+ if test "${MASTER_PUBLIC_KEY2}" != "${MASTER_PUBLIC_KEY}"; then
+ say "Error: master public key missmatch ${MASTER_PUBLIC_KEY2} does not match ${MASTER_PUBLIC_KEY}"
+ exit 1
+ fi
+ fi
+fi
+
+say "Stopping running exchange before reconfiguration"
+systemctl stop taler-exchange.target &>> setup.log
+
+say "Configuring exchange"
+
+# Generate terms of service (ToS)
+TERMS_ETAG=
+if test ${DO_EXCHANGE_TERMS} == y; then
+ if test -z "${EXCHANGE_TERMS_FILE:-}"; then
+ say "Error: No EXCHANGE_TERMS_FILE set but DO_EXCHANGE_TERMS set to YES"
+ exit 1
+ fi
+
+ TERMS_ETAG="$(basename "$EXCHANGE_TERMS_FILE" .rst)"
+
+ say "Setting up terms of service (ToS)"
+ taler-terms-generator -i "${EXCHANGE_TERMS_FILE}" &>> setup.log
+fi
+
+# Generate privacy policy
+PRIVACY_ETAG=
+if test ${DO_EXCHANGE_PRIVACY} == y; then
+ if test -z "${EXCHANGE_PRIVACY_FILE:-}"; then
+ say "Error: No EXCHANGE_PRIVACY_FILE set but DO_EXCHANGE_PRIVACY set to YES"
+ exit 1
+ fi
+
+ PRIVACY_ETAG="$(basename "$EXCHANGE_PRIVACY_FILE" .rst)"
+
+ say "Setting up the privacy policy"
+ taler-terms-generator -i "${EXCHANGE_PRIVACY_FILE}" &>> setup.log
+fi
+
+export EXCHANGE_BASE_URL="$PROTO://exchange.${DOMAIN_NAME}/"
+
+cat << EOF > /etc/taler/conf.d/setup.conf
+[taler]
+CURRENCY=${CURRENCY}
+CURRENCY_ROUND_UNIT=${CURRENCY}:0.01
+
+[exchange]
+AML_THRESHOLD=${CURRENCY}:1000000
+MASTER_PUBLIC_KEY=${MASTER_PUBLIC_KEY}
+BASE_URL=${EXCHANGE_BASE_URL}
+STEFAN_ABS=${CURRENCY}:0
+STEFAN_LOG=${CURRENCY}:0
+STEFAN_LIN=0
+
+TERMS_ETAG=${TERMS_ETAG}
+PRIVACY_ETAG=${PRIVACY_ETAG}
+
+[merchant-exchange-${DOMAIN_NAME}]
+MASTER_KEY=${MASTER_PUBLIC_KEY}
+CURRENCY=${CURRENCY}
+EXCHANGE_BASE_URL=${EXCHANGE_BASE_URL}
+
+[exchange-account-default]
+PAYTO_URI=${EXCHANGE_PAYTO}
+ENABLE_DEBIT=YES
+ENABLE_CREDIT=YES
+@inline-secret@ exchange-accountcredentials-default ../secrets/exchange-accountcredentials-default.secret.conf
+EOF
+
+cat << EOF > /etc/taler/secrets/exchange-db.secret.conf
+[exchangedb-postgres]
+CONFIG=postgres:///exchange
+EOF
+
+chmod 440 /etc/taler/secrets/exchange-db.secret.conf
+chown root:taler-exchange-db /etc/taler/secrets/exchange-db.secret.conf
+
+cat << EOF > /etc/taler/secrets/exchange-accountcredentials-default.secret.conf
+
+[exchange-accountcredentials-default]
+WIRE_GATEWAY_URL=${PROTO}://bank.$DOMAIN_NAME/accounts/exchange/taler-wire-gateway/
+WIRE_GATEWAY_AUTH_METHOD=basic
+USERNAME=exchange
+PASSWORD=${BANK_EXCHANGE_PASSWORD}
+EOF
+
+chmod 400 /etc/taler/secrets/exchange-accountcredentials-default.secret.conf
+chown taler-exchange-wire:taler-exchange-db /etc/taler/secrets/exchange-accountcredentials-default.secret.conf
+
+taler-harness deployment gen-coin-config \
+ --min-amount "${CURRENCY}":0.01 \
+ --max-amount "${CURRENCY}":100 |
+ sed -e "s/FEE_DEPOSIT = ${CURRENCY}:0.01/FEE_DEPOSIT = ${CURRENCY}:0/" \
+ >/etc/taler/conf.d/"${CURRENCY}"-coins.conf
+
+say "Initializing exchange database"
+taler-exchange-dbconfig -c /etc/taler/taler.conf &>> setup.log
+
+say "Launching exchange"
+systemctl enable taler-exchange.target &>> setup.log
+systemctl restart taler-exchange.target &>> setup.log
+
+say "Waiting for exchange HTTP service (/config)..."
+curl -sS --max-time 2 \
+ --retry-all-errors \
+ --retry-delay 2 \
+ --retry 10 \
+ "${EXCHANGE_BASE_URL}"config &>> setup.log
+
+say "Waiting for exchange management keys (this may take a while)..."
+curl -sS --max-time 30 \
+ --retry-delay 2 \
+ --retry 60 \
+ "${EXCHANGE_BASE_URL}"management/keys &>> setup.log
+
+if test ${DO_OFFLINE} == y; then
+ say "Offline interaction..."
+ sudo -i -u taler-exchange-offline \
+ taler-exchange-offline \
+ -c /etc/taler/taler.conf \
+ download \
+ sign \
+ upload &>> setup.log
+
+ say "Exchange account setup..."
+ sudo -i -u taler-exchange-offline \
+ taler-exchange-offline \
+ enable-account "${EXCHANGE_PAYTO}" \
+ display-hint 0 "${CURRENCY} Exchange" \
+ wire-fee now x-taler-bank "${CURRENCY}":0 "${CURRENCY}":0 \
+ global-fee now "${CURRENCY}":0 "${CURRENCY}":0 "${CURRENCY}":0 1h 6a 0 \
+ upload &>> setup.log
+
+ say "Enabling timer to automate renewals..."
+ systemctl enable taler-exchange-offline.timer &>> setup.log
+ systemctl restart taler-exchange-offline.timer &>> setup.log
+
+ if test ${DO_CONVERSION} == y; then
+ say "Conversion account setup (restricted to CH-only)..."
+ sudo -i -u taler-exchange-offline taler-exchange-offline \
+ enable-account "${CONVERSION_PAYTO}" \
+ display-hint 10 "${FIAT_BANK_NAME}" \
+ conversion-url "${PROTO}://bank.$DOMAIN_NAME/conversion-info/" \
+ debit-restriction deny \
+ wire-fee now iban "${CURRENCY}":0 "${CURRENCY}":0 \
+ upload &>> setup.log
+ fi
+fi
+
+say "Waiting for exchange /keys..."
+curl -sS --max-time 2 \
+ --retry-connrefused \
+ --retry-delay 2 \
+ --retry 10 \
+ "${EXCHANGE_BASE_URL}"keys &>> setup.log
+
+say "Exchange setup finished"
diff --git a/regional-currency/setup-libeufin.sh b/regional-currency/setup-libeufin.sh
new file mode 100755
index 0000000..47d8725
--- /dev/null
+++ b/regional-currency/setup-libeufin.sh
@@ -0,0 +1,138 @@
+#!/bin/bash
+# This file is in the public domain.
+#
+# This script configure libeufin-bank and libeufin-nexus.
+
+
+set -eu
+
+source functions.sh
+{ source <(./config.py 3>&1 >&4 4>&-); } 4>&1
+source config/internal.conf
+
+say "Beginning LibEuFin setup"
+
+if test -z "${BANK_NAME:-}"; then
+ say "Error: config/user.conf does not specify BANK_NAME"
+ exit 1
+fi
+if test -z "${DOMAIN_NAME:-}"; then
+ say "Error: config/user.conf does not specify DOMAIN_NAME"
+ exit 1
+fi
+if test -z "${BANK_ADMIN_PASSWORD:-}"; then
+ say "Error: config/user.conf does not specify BANK_ADMIN_PASSWORD"
+ exit 1
+fi
+if test -z "${BANK_EXCHANGE_PASSWORD:-}"; then
+ say "Error: config/user.conf does not specify BANK_EXCHANGE_PASSWORD"
+ exit 1
+fi
+
+if test ${DO_CONVERSION} == y; then
+ say "Configuring libeufin-nexus with ${FIAT_CURRENCY}..."
+
+ taler-config -s nexus-ebics -o CURRENCY \
+ -V "$FIAT_CURRENCY" -c /etc/libeufin/libeufin-nexus.conf
+ taler-config -s nexus-ebics -o IBAN \
+ -V "$FIAT_ACCOUNT_IBAN" -c /etc/libeufin/libeufin-nexus.conf
+ taler-config -s nexus-ebics -o BIC \
+ -V "$FIAT_ACCOUNT_BIC" -c /etc/libeufin/libeufin-nexus.conf
+ taler-config -s nexus-ebics -o NAME \
+ -V "$FIAT_ACCOUNT_NAME" -c /etc/libeufin/libeufin-nexus.conf
+fi
+
+
+say "Configuring libeufin-bank with ${CURRENCY}..."
+
+cat >/etc/libeufin/libeufin-bank.conf <<EOF
+[libeufin-bank]
+CURRENCY=${CURRENCY}
+NAME="${BANK_NAME}"
+BASE_URL=bank.${DOMAIN_NAME}
+WIRE_TYPE=x-taler-bank
+X_TALER_BANK_PAYTO_HOSTNAME=bank.${DOMAIN_NAME}
+SUGGESTED_WITHDRAWAL_EXCHANGE=${PROTO}://exchange.${DOMAIN_NAME}/
+SERVE=tcp
+PORT=${BANK_PORT}
+EOF
+
+if test ${DO_CONVERSION} == y; then
+ cat >>/etc/libeufin/libeufin-bank.conf <<EOF
+ALLOW_CONVERSION=yes
+FIAT_CURRENCY=${FIAT_CURRENCY}
+ALLOW_EDIT_CASHOUT_PAYTO_URI=yes
+EOF
+fi
+
+if test -n "${TELESIGN_AUTH_TOKEN:-}"; then
+ cat >>/etc/libeufin/libeufin-bank.conf <<EOF
+TAN_SMS=libeufin-tan-sms.sh
+TAN_SMS_ENV={"AUTH_TOKEN":"$TELESIGN_AUTH_TOKEN"}
+EOF
+fi
+
+say "Setting up libeufin database..."
+
+libeufin-dbconfig &>> setup.log
+
+say "Setting up libeufin-bank..."
+
+
+say "Setting up libeufin-bank admin account..."
+sudo -u libeufin-bank \
+ libeufin-bank passwd \
+ -c /etc/libeufin/libeufin-bank.conf \
+ admin "${BANK_ADMIN_PASSWORD}" &>> setup.log
+
+say "Setting up admin's debt limit..."
+sudo -u libeufin-bank \
+ libeufin-bank edit-account \
+ -c /etc/libeufin/libeufin-bank.conf \
+ admin --debit_threshold=${CURRENCY}:200000000 &>> setup.log
+
+say "Setting up SPA configuration..."
+echo "settings = { bankName: \"${BANK_NAME}\" }" >/etc/libeufin/settings.js
+
+say "Create exchange account..."
+if test -z "${EXCHANGE_PAYTO:-}"; then
+ # FIXME create-account should have a way to update the password if the account already exists
+ EXCHANGE_PAYTO_NEW="$(sudo -u libeufin-bank libeufin-bank create-account -c /etc/libeufin/libeufin-bank.conf --username exchange --password "${BANK_EXCHANGE_PASSWORD}" --name Exchange --exchange 2>> setup.log)?receiver-name=Exchange"
+ echo "EXCHANGE_PAYTO=\"${EXCHANGE_PAYTO_NEW}\"" >> config/user.conf
+fi
+
+
+say "Start the bank..."
+systemctl enable libeufin-bank &>> setup.log
+systemctl restart libeufin-bank &>> setup.log
+
+say "Waiting for the bank (/config)..."
+curl -sS --max-time 2 \
+ --retry-all-errors \
+ --retry-delay 2 \
+ --retry 10 \
+ ${PROTO}://bank.${DOMAIN_NAME}/config &>> setup.log
+
+if test ${DO_CONVERSION} == y; then
+say "Setting conversion rates to 1:1 ..."
+# TODO only set conversion rates if known have been set
+curl -sS -u "admin:${BANK_ADMIN_PASSWORD}" \
+ -H 'Content-Type: application/json; charset=utf-8' \
+ ${PROTO}://bank.${DOMAIN_NAME}/conversion-info/conversion-rate \
+--data-binary @- &>> setup.log << EOF
+{
+ "cashin_ratio": "1",
+ "cashin_fee": "${CURRENCY}:0",
+ "cashin_tiny_amount": "${CURRENCY}:0.01",
+ "cashin_rounding_mode": "nearest",
+ "cashin_min_amount": "${FIAT_CURRENCY}:1",
+ "cashout_ratio": "1",
+ "cashout_fee": "${FIAT_CURRENCY}:0",
+ "cashout_tiny_amount": "${FIAT_CURRENCY}:0.01",
+ "cashout_rounding_mode": "nearest",
+ "cashout_min_amount": "${CURRENCY}:1"
+}
+EOF
+fi
+
+say "LibEuFin setup finished"
diff --git a/regional-currency/setup-merchant.sh b/regional-currency/setup-merchant.sh
new file mode 100755
index 0000000..a892b7a
--- /dev/null
+++ b/regional-currency/setup-merchant.sh
@@ -0,0 +1,14 @@
+#!/bin/bash
+
+set -eu
+
+source functions.sh
+source config/user.conf
+source config/internal.conf
+
+say "Setting up merchant database"
+taler-merchant-dbconfig &>> setup.log
+
+say "Launching taler-merchant-httpd"
+systemctl enable taler-merchant.target &>> setup.log
+systemctl restart taler-merchant.target &>> setup.log
diff --git a/regional-currency/upgrade.sh b/regional-currency/upgrade.sh
new file mode 100755
index 0000000..8924a77
--- /dev/null
+++ b/regional-currency/upgrade.sh
@@ -0,0 +1,30 @@
+#!/bin/bash
+# This file is in the public domain.
+set -eu
+
+echo "Fetching package list..."
+apt-get update
+
+echo -n "Stopping Taler services..."
+systemctl disable --now taler-exchange.target &>> upgrade.log
+systemctl disable --now taler-merchant.target &>> upgrade.log
+systemctl disable --now libeufin-bank &>> upgrade.log
+systemctl disable --now libeufin-nexus.target &>> upgrade.log
+echo " OK"
+
+echo "Upgrading packages..."
+apt-get upgrade
+
+echo "Upgrading databases..."
+libeufin-dbconfig &>> upgrade.log
+taler-exchange-dbconfig &>> upgrade.log
+taler-merchant-dbconfig &>> upgrade.log
+
+echo -n "Restarting Taler services..."
+systemctl enable --now taler-exchange.target &>> upgrade.log
+systemctl enable --now taler-merchant.target &>> upgrade.log
+systemctl enable --now libeufin-bank &>> upgrade.log
+systemctl enable --now libeufin-nexus.target &>> upgrade.log
+echo " OK"
+
+exit 0
diff --git a/regional-currency/vagrant/.gitignore b/regional-currency/vagrant/.gitignore
new file mode 100644
index 0000000..8000dd9
--- /dev/null
+++ b/regional-currency/vagrant/.gitignore
@@ -0,0 +1 @@
+.vagrant
diff --git a/regional-currency/vagrant/README b/regional-currency/vagrant/README
new file mode 100644
index 0000000..e9387d3
--- /dev/null
+++ b/regional-currency/vagrant/README
@@ -0,0 +1,2 @@
+This folder contains a vagrant configuration (https://developer.hashicorp.com/vagrant)
+that allows us to easily spin up a virtual machine to test the setup instructions.
diff --git a/regional-currency/vagrant/Vagrantfile b/regional-currency/vagrant/Vagrantfile
new file mode 100644
index 0000000..7cb3574
--- /dev/null
+++ b/regional-currency/vagrant/Vagrantfile
@@ -0,0 +1,77 @@
+# -*- mode: ruby -*-
+# vi: set ft=ruby :
+
+# All Vagrant configuration is done below. The "2" in Vagrant.configure
+# configures the configuration version (we support older styles for
+# backwards compatibility). Please don't change it unless you know what
+# you're doing.
+Vagrant.configure("2") do |config|
+ # The most common configuration options are documented and commented below.
+ # For a complete reference, please see the online documentation at
+ # https://docs.vagrantup.com.
+
+ # Every Vagrant development environment requires a box. You can search for
+ # boxes at https://vagrantcloud.com/search.
+ config.vm.box = "ubuntu/kinetic64"
+
+ config.ssh.forward_agent = true
+ config.ssh.forward_x11 = true
+
+ # Disable automatic box update checking. If you disable this, then
+ # boxes will only be checked for updates when the user runs
+ # `vagrant box outdated`. This is not recommended.
+ # config.vm.box_check_update = false
+
+ # Create a forwarded port mapping which allows access to a specific port
+ # within the machine from a port on the host machine. In the example below,
+ # accessing "localhost:8080" will access port 80 on the guest machine.
+ # NOTE: This will enable public access to the opened port
+ # config.vm.network "forwarded_port", guest: 80, host: 8080
+
+ # Create a forwarded port mapping which allows access to a specific port
+ # within the machine from a port on the host machine and only allow access
+ # via 127.0.0.1 to disable public access
+ # config.vm.network "forwarded_port", guest: 80, host: 8080, host_ip: "127.0.0.1"
+
+ # Create a private network, which allows host-only access to the machine
+ # using a specific IP.
+ # config.vm.network "private_network", ip: "192.168.33.10"
+
+ # Create a public network, which generally matched to bridged network.
+ # Bridged networks make the machine appear as another physical device on
+ # your network.
+ # config.vm.network "public_network"
+
+ # Share an additional folder to the guest VM. The first argument is
+ # the path on the host to the actual folder. The second argument is
+ # the path on the guest to mount the folder. And the optional third
+ # argument is a set of non-required options.
+ # config.vm.synced_folder "../data", "/vagrant_data"
+
+ # Provider-specific configuration so you can fine-tune various
+ # backing providers for Vagrant. These expose provider-specific options.
+ # Example for VirtualBox:
+ #
+ config.vm.provider "virtualbox" do |vb|
+ # Display the VirtualBox GUI when booting the machine
+ vb.gui = true
+
+ # Customize the amount of memory on the VM:
+ vb.memory = "4096"
+
+ # Required, or wayland doesn't seem to work
+ vb.customize ['modifyvm', :id, '--graphicscontroller', 'vmsvga']
+ end
+ #
+ # View the documentation for the provider you are using for more
+ # information on available options.
+
+ # Enable provisioning with a shell script. Additional provisioners such as
+ # Ansible, Chef, Docker, Puppet and Salt are also available. Please see the
+ # documentation for more information about their specific syntax and use.
+ config.vm.provision "shell", reboot: true, inline: <<-SHELL
+ apt-get update
+ apt-get upgrade -y
+ apt-get install -y ubuntu-desktop gnome-shell firefox virtualbox-guest-additions-iso git
+ SHELL
+end
diff --git a/regional-currency/withdraw.sh b/regional-currency/withdraw.sh
new file mode 100755
index 0000000..c0896e5
--- /dev/null
+++ b/regional-currency/withdraw.sh
@@ -0,0 +1,32 @@
+#!/bin/bash
+
+if test "$1" = "--help" || test "$1" = "-h"; then
+ echo "./withdraw [RESERVE_PUB]"
+ echo
+ echo "Injects one incoming CHF payment into nexus database"
+ echo "in order to trigger a Taler withdrawal. The reserve"
+ echo "pub can be passed either as the first parameter, or"
+ echo "it'll be generated by the CLI wallet. In both cases,"
+ echo "the exchange to withdraw from is \$PROTO://exchange.\$DOMAIN"
+
+ exit 0
+fi
+
+RESERVE_PUB="$1" # maybe passed
+set -eu
+
+. config/user.conf # DOMAIN_NAME, CURRENCY & FIAT_CURRENCY
+. config/internal.conf # PROTO
+
+NEXUS_CONFIG_FILE=/etc/libeufin/libeufin-nexus.conf
+if test -z "$RESERVE_PUB"; then
+ RESERVE_PUB=$(taler-wallet-cli \
+ api 'acceptManualWithdrawal' \
+ '{"exchangeBaseUrl":"'${PROTO}'://exchange.'$DOMAIN_NAME'",
+ "amount":"'$CURRENCY':5"
+ }' | jq -r .result.reservePub)
+fi
+DEBTOR_IBAN="CH8389144317421994586"
+sudo -i -u libeufin-nexus libeufin-nexus testing fake-incoming -L DEBUG --subject "$RESERVE_PUB" --amount "$FIAT_CURRENCY:5" "payto://iban/$DEBTOR_IBAN"
+
+taler-wallet-cli run-until-done