diff options
Diffstat (limited to 'regional-currency')
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 |