#!/usr/bin/env python3
# This file is part of GNU Taler.
#
# GNU Taler is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# GNU Taler is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with GNU Taler. If not, see .
import signal
import socket
import shutil
import atexit
import click
import types
import os
import sys
import os.path
import subprocess
import time
import random
import logging
import json
from os import listdir
from os.path import isdir, join
from pathlib import Path
from dataclasses import dataclass
from typing import List, Callable
from shutil import copy
from multiprocessing import Process
from string import ascii_letters, ascii_uppercase
from sys import exit
from urllib.parse import urljoin, quote
from os import remove
import requests
from collections import OrderedDict
import errno
from pathlib import Path
from subprocess import Popen, DEVNULL, PIPE
from datetime import datetime
from requests_unixsocket import Session
from flask import Flask, request, Response
from werkzeug.datastructures import Headers
from werkzeug.exceptions import HTTPException
TALER_ROOT_DIR = Path.home() / ".taler"
TALER_PREFIX = Path.home() / ".local"
# Print No Newline.
def print_nn(msg):
print(msg, end="")
sys.stdout.flush()
@dataclass
class Repo:
name: str
url: str
deps: List[str]
builder: Callable[["Repo", Path], None]
@click.group()
def cli():
pass
def split_repos_list(repos):
return [repo for repo in repos.split(",") if repo != ""]
def update_checkout(r: Repo, p: Path):
"""Clean the repository's working directory and
update it to the match the latest version of the upstream branch
that we are tracking."""
subprocess.run(["git", "-C", str(p), "clean", "-fdx"], check=True)
subprocess.run(["git", "-C", str(p), "fetch"], check=True)
subprocess.run(["git", "-C", str(p), "reset"], check=True)
res = subprocess.run(
[
"git",
"-C",
str(p),
"rev-parse",
"--abbrev-ref",
"--symbolic-full-name",
"@{u}",
],
stderr=subprocess.DEVNULL,
stdout=subprocess.PIPE,
encoding="utf-8",
)
if res.returncode != 0:
ref = "HEAD"
else:
ref = res.stdout.strip("\n ")
print(f"resetting {r.name} to ref {ref}")
subprocess.run(["git", "-C", str(p), "reset", "--hard", ref], check=True)
def default_configure(*extra):
extra_list = list(extra)
subprocess.run(["./configure", f"--prefix={TALER_PREFIX}"] + extra_list, check=True)
def pyconfigure(*extra):
"""For python programs, --prefix doesn't work."""
subprocess.run(["./configure"] + list(extra), check=True)
def build_libeufin(r: Repo, p: Path):
update_checkout(r, p)
subprocess.run(["./bootstrap"], check=True)
default_configure()
subprocess.run(["make", "install"], check=True)
(p / "taler-buildstamp").touch()
def build_libmicrohttpd(r: Repo, p: Path):
update_checkout(r, p)
subprocess.run(["./bootstrap"], check=True)
# Debian gnutls packages are too old ...
default_configure("--with-gnutls=/usr/local")
subprocess.run(["make"], check=True)
subprocess.run(["make", "install"], check=True)
(p / "taler-buildstamp").touch()
def build_gnunet(r: Repo, p: Path):
update_checkout(r, p)
subprocess.run(["./bootstrap"], check=True)
pfx = Path.home() / ".local"
default_configure(
"--enable-logging=verbose",
f"--with-microhttpd={pfx}",
"--disable-documentation",
)
subprocess.run(["make", "install"], check=True)
(p / "taler-buildstamp").touch()
def build_exchange(r: Repo, p: Path):
update_checkout(r, p)
subprocess.run(["./bootstrap"], check=True)
pfx = Path.home() / ".local"
default_configure(
"CFLAGS=-ggdb -O0",
"--enable-logging=verbose",
f"--with-microhttpd={pfx}",
f"--with-gnunet={pfx}",
)
subprocess.run(["make", "install"], check=True)
(p / "taler-buildstamp").touch()
def build_wallet(r, p):
update_checkout(r, p)
subprocess.run(["./bootstrap"], check=True)
default_configure()
subprocess.run(["make", "install"], check=True)
(p / "taler-buildstamp").touch()
def build_twister(r, p):
update_checkout(r, p)
subprocess.run(["./bootstrap"], check=True)
pfx = Path.home() / ".local"
default_configure(
"CFLAGS=-ggdb -O0",
"--enable-logging=verbose",
f"--with-exchange={pfx}",
f"--with-gnunet={pfx}",
)
subprocess.run(["make", "install"], check=True)
(p / "taler-buildstamp").touch()
def build_merchant(r, p):
update_checkout(r, p)
subprocess.run(["./bootstrap"], check=True)
pfx = Path.home() / ".local"
default_configure(
"CFLAGS=-ggdb -O0",
"--enable-logging=verbose",
f"--with-microhttpd={pfx}",
f"--with-exchange={pfx}",
f"--with-gnunet={pfx}",
"--disable-doc",
)
subprocess.run(["make", "install"], check=True)
(p / "taler-buildstamp").touch()
def build_sync(r, p):
update_checkout(r, p)
subprocess.run(["./bootstrap"], check=True)
pfx = Path.home() / ".local"
default_configure(
"CFLAGS=-ggdb -O0",
"--enable-logging=verbose",
f"--with-microhttpd={pfx}",
f"--with-exchange={pfx}",
f"--with-merchant={pfx}",
f"--with-gnunet={pfx}",
"--disable-doc",
)
subprocess.run(["make", "install"], check=True)
(p / "taler-buildstamp").touch()
def build_anastasis(r, p):
update_checkout(r, p)
subprocess.run(["./bootstrap"], check=True)
pfx = Path.home() / ".local"
default_configure(
"CFLAGS=-ggdb -O0",
"--enable-logging=verbose",
f"--with-microhttpd={pfx}",
f"--with-exchange={pfx}",
f"--with-merchant={pfx}",
f"--with-gnunet={pfx}",
"--disable-doc",
)
subprocess.run(["make", "install"], check=True)
(p / "taler-buildstamp").touch()
def build_demos(r, p):
update_checkout(r, p)
pfx = Path.home() / ".local"
pyconfigure()
subprocess.run(["make", "install"], check=True)
(p / "taler-buildstamp").touch()
def build_backoffice(r, p):
update_checkout(r, p)
subprocess.run(["./bootstrap"])
subprocess.run(["./configure"])
subprocess.run(["make", "build-single"])
(p / "taler-buildstamp").touch()
repos = {
"libmicrohttpd": Repo(
"libmicrohttpd",
"git://git.gnunet.org/libmicrohttpd.git",
[],
build_libmicrohttpd,
),
"gnunet": Repo(
"gnunet",
"git://git.gnunet.org/gnunet.git",
["libmicrohttpd"],
build_gnunet
),
"exchange": Repo(
"exchange",
"git://git.taler.net/exchange",
["gnunet", "libmicrohttpd"],
build_exchange,
),
"merchant": Repo(
"merchant",
"git://git.taler.net/merchant",
["exchange","libmicrohttpd","gnunet"],
build_merchant,
),
"sync": Repo(
"sync",
"git://git.taler.net/sync",
["exchange",
"merchant",
"gnunet",
"libmicrohttpd"],
build_sync,
),
"anastasis": Repo(
"anastasis",
"git://git.taler.net/anastasis",
["exchange",
"merchant",
"libmicrohttpd",
"gnunet"],
build_anastasis,
),
"wallet-core": Repo(
"wallet-core",
"git://git.taler.net/wallet-core",
[],
build_wallet,
),
"libeufin": Repo(
"libeufin",
"git://git.taler.net/libeufin.git",
[],
build_libeufin,
),
"taler-merchant-demos": Repo(
"taler-merchant-demos",
"git://git.taler.net/taler-merchant-demos",
[],
build_demos,
),
"twister": Repo(
"twister",
"git://git.taler.net/twister",
["gnunet", "libmicrohttpd"],
build_twister,
),
}
def get_repos_names() -> List[str]:
r_dir = TALER_ROOT_DIR / "sources"
return [el for el in listdir(r_dir) if isdir(join(r_dir, el)) and repos.get(el)]
# Get the installed repositories from the sources directory.
def load_repos(reposNames) -> List[Repo]:
return [repos.get(r) for r in reposNames if repos.get(r)]
def update_repos(repos: List[Repo]) -> None:
for r in repos:
r_dir = TALER_ROOT_DIR / "sources" / r.name
subprocess.run(["git", "-C", str(r_dir), "fetch"], check=True)
res = subprocess.run(
["git", "-C", str(r_dir), "status", "-sb"],
check=True,
stdout=subprocess.PIPE,
encoding="utf-8",
)
if "behind" in res.stdout:
print(f"new commits in {r}")
s = r_dir / "taler-buildstamp"
if s.exists():
s.unlink()
def get_stale_repos(repos: List[Repo]) -> List[Repo]:
timestamps = {}
stale = []
for r in repos:
r_dir = TALER_ROOT_DIR / "sources" / r.name
s = r_dir / "taler-buildstamp"
if not s.exists():
timestamps[r.name] = time.time()
stale.append(r)
continue
ts = timestamps[r.name] = s.stat().st_mtime
for dep in r.deps:
# When 'dep' in not found, it has been
# excluded from the compilation.
if timestamps.get("dep", 0) > ts:
stale.append(r)
break
return stale
@cli.command()
@click.option(
"--without-repos", metavar="WITHOUT REPOS",
help="WITHOUT REPOS is a unspaced and comma-separated list \
of the repositories to _exclude_ from compilation",
default="")
@click.option(
"--only-repos", metavar="ONLY REPOS",
help="ONLY REPOS is a unspaced and comma-separated exclusive list \
of the repositories to include in the compilation",
default="")
def build(without_repos, only_repos) -> None:
"""Build the deployment from source."""
if only_repos != "" and without_repos != "":
print("Either use --only-repos or --without-repos")
exit(1)
repos_names = get_repos_names()
if only_repos != "":
repos_names = list(filter(
lambda x: x in split_repos_list(only_repos),
repos_names
))
if without_repos != "":
repos_names = list(filter(
lambda x: x not in split_repos_list(without_repos),
repos_names
))
# Reorder the list of repositories so that the
# most fundamental dependecies appear left-most.
repos_keys = repos.keys()
sorted_repos = sorted(
set(repos_keys).intersection(repos_names),
key=lambda x: list(repos_keys).index(x)
)
target_repos = load_repos(sorted_repos) # Get Repo objects
update_repos(target_repos)
stale = get_stale_repos(target_repos)
print(f"found stale repos: {[r.name for r in stale]}")
for r in stale:
# Warn, if a dependency is not being built:
diff = set(r.deps) - set(repos_names)
if len(diff) > 0:
print(f"WARNING: those dependencies are not being built: {diff}")
p = TALER_ROOT_DIR / "sources" / r.name
os.chdir(str(p))
r.builder(r, p)
@cli.command()
@click.option(
"--repos", "-r",
metavar="REPOS",
help="REPOS is a unspaced and comma-separated list of the repositories to clone.",
default="libmicrohttpd,gnunet,exchange,merchant,wallet-core,taler-merchant-demos,sync,anastasis,libeufin",
show_default=True,
)
@click.option(
"--list-repos/--no-list-repos", default=False,
help="Lists the repositories that were bootstrapped.",
)
def bootstrap(list_repos, repos) -> None:
"""Clone all the specified repositories."""
if list_repos:
for repo in get_repos_names():
print(repo)
return
# Download the repository.
def checkout_repos(repos: List[Repo]):
if len(repos) == 0:
print("No repositories can be checked out. Spelled correctly?")
return
sources = TALER_ROOT_DIR / "sources"
for r in repos:
r_dir = sources / r.name
if not r_dir.exists():
r_dir.mkdir(parents=True, exist_ok=True)
subprocess.run(["git", "-C", str(sources), "clone", r.url], check=True)
reposList = split_repos_list(repos)
checkout_repos(load_repos(reposList))
# Globals sharead accross multiple sub-commands:
# needed to configure and launch the reverse proxy.
REV_PROXY_HOSTNAME = "localhost"
REV_PROXY_PORT = "8080"
REV_PROXY_NETLOC = REV_PROXY_HOSTNAME + ":" + REV_PROXY_PORT
REV_PROXY_PROTO = "http"
REV_PROXY_URL = f"{REV_PROXY_PROTO}://{REV_PROXY_NETLOC}"
UNIX_SOCKETS_DIR = TALER_ROOT_DIR / "sockets"
LOG_DIR = TALER_ROOT_DIR / "logs"
# needed to create the customer's bank account and
# to let them subsequently withdraw via the Access API.
CUSTOMER_BANK_ACCOUNT = "sandbox-account-customer"
CUSTOMER_BANK_PASSWORD = "secret"
# needed along preparation and later to withdraw via
# the Access API.
CURRENCY = "EUR"
@cli.command()
def prepare():
"""Generate configuration, run-time blobs, instances, euFin accounts."""
def is_serving(check_url, tries=10):
for i in range(tries):
try:
print_nn(".")
# Raises if the service is not reachable.
response = requests.get(
check_url,
timeout=1
)
# The reverse proxy may return 500 if the
# end service is not ready, therefore this
# case should be tolerated.
response.raise_for_status()
except:
time.sleep(0.5)
if i == tries - 1:
return False
continue
break
return True
def fail(reason=None):
if reason:
print("ERROR:", reason)
print(f"Logs in {LOG_DIR}")
exit(1)
def kill(proc):
proc.terminate()
proc.wait()
def get_nexus_cli_env(
username,
password,
nexus_url
):
env = os.environ.copy()
env["LIBEUFIN_NEXUS_USERNAME"] = username
env["LIBEUFIN_NEXUS_PASSWORD"] = password
env["LIBEUFIN_NEXUS_URL"] = nexus_url
return env
def get_sandbox_cli_env(
username, password
):
env = os.environ.copy()
env["LIBEUFIN_SANDBOX_USERNAME"] = username
env["LIBEUFIN_SANDBOX_PASSWORD"] = password
return env
# Will be extended to include a SANDBOX_ADMIN_TOKEN
# that will obsolete the 'superuser' flag of ordinary
# user accounts. Likewise, the client side will be
# modified to use such token.
def get_sandbox_server_env(db_file, base_url, admin_password):
env = os.environ.copy()
env["LIBEUFIN_SANDBOX_DB_CONNECTION"] = f"jdbc:sqlite:{db_file}"
env["LIBEUFIN_SANDBOX_BASE_URL"] = base_url
env["LIBEUFIN_SANDBOX_ADMIN_PASSWORD"] = admin_password
return env
def get_nexus_server_env(db_file, base_url):
env = os.environ.copy()
env["LIBEUFIN_NEXUS_DB_CONNECTION"] = f"jdbc:sqlite:{db_file}"
env["LIBEUFIN_NEXUS_BASE_URL"] = base_url
return env
def urljoin_nodrop(a, b):
a = a + "/" # urljoin will drop extra trailing slashes.
b = "/".join([x for x in b.split("/") if x != ""]) # remove leading slashes.
return urljoin(a, b)
def prepare_nexus_account(
ebics_url,
ebics_host_id,
ebics_partner_id,
ebics_user_id,
bank_connection_name,
bank_account_name_sandbox,
bank_account_name_nexus,
env
):
# make connection
Command(
[
"libeufin-cli", "connections",
"new-ebics-connection",
"--ebics-url", ebics_url,
"--host-id", ebics_host_id,
"--partner-id", ebics_partner_id,
"--ebics-user-id", ebics_user_id,
bank_connection_name
],
env
).run()
# connect
Command(
[
"libeufin-cli", "connections",
"connect", bank_connection_name
],
env
).run()
# Import bank account
Command(
[
"libeufin-cli", "connections",
"download-bank-accounts",
bank_connection_name
],
env
).run()
Command(
[
"libeufin-cli", "connections",
"import-bank-account",
"--offered-account-id",
bank_account_name_sandbox,
"--nexus-bank-account-id",
bank_account_name_nexus,
bank_connection_name
],
env
).run()
# Set background tasks.
Command(
[
"libeufin-cli", "accounts",
"task-schedule", bank_account_name_nexus,
"--task-type", "submit",
"--task-name", "submit-payments-each-second",
"--task-cronspec", "* * *"
],
env
).run()
Command(
[
"libeufin-cli", "accounts",
"task-schedule", bank_account_name_nexus,
"--task-type", "fetch",
"--task-name", "fetch-reports-each-second",
"--task-cronspec", "* * *",
"--task-param-level", "report",
"--task-param-range-type", "latest"
],
env
).run()
def get_sandbox_account_info(
sandbox_url,
bank_account_label,
password,
):
customer_env = os.environ.copy()
customer_env["LIBEUFIN_SANDBOX_USERNAME"] = bank_account_label
customer_env["LIBEUFIN_SANDBOX_PASSWORD"] = password
demobank_url = urljoin_nodrop(sandbox_url, "/demobanks/default")
r = Command([
"libeufin-cli", "sandbox",
"--sandbox-url", demobank_url,
"demobank", "info",
"--bank-account", bank_account_label],
env = customer_env,
capture_stdout=True
).run()
return json.loads(r)
def prepare_sandbox_account(
sandbox_url,
ebics_host_id,
ebics_partner_id,
ebics_user_id,
person_name,
# This value is BOTH a username
# and a bank account label.
bank_account_name,
password
):
demobank_url = urljoin_nodrop(sandbox_url, "/demobanks/default")
user_env = os.environ.copy()
user_env["LIBEUFIN_SANDBOX_USERNAME"] = bank_account_name
user_env["LIBEUFIN_SANDBOX_PASSWORD"] = password
Command(
[
"libeufin-cli", "sandbox",
"--sandbox-url", demobank_url,
"demobank", "register"
],
env = user_env
).run()
admin_env = os.environ.copy()
admin_env["LIBEUFIN_SANDBOX_USERNAME"] = SANDBOX_ADMIN_USERNAME
admin_env["LIBEUFIN_SANDBOX_PASSWORD"] = SANDBOX_ADMIN_PASSWORD
Command([
"libeufin-cli", "sandbox",
"--sandbox-url", demobank_url,
"demobank", "new-ebicssubscriber",
"--host-id", ebics_host_id,
"--partner-id", ebics_partner_id,
"--user-id", ebics_user_id,
"--bank-account", bank_account_name
],
env = admin_env
).run()
WIRE_METHOD = "iban"
# euFin URLs
SANDBOX_URL = REV_PROXY_URL + "/sandbox"
NEXUS_URL = REV_PROXY_URL + "/nexus"
# Filesystem's paths
CFG_OUTDIR = TALER_ROOT_DIR / "config"
TALER_RUNTIME_DIR = TALER_ROOT_DIR / "runtime"
TALER_DATA_DIR = TALER_ROOT_DIR / "data"
TALER_UNIT_FILES_DIR = systemd_user_dir = Path.home() / ".config" / "systemd" / "user"
def get_random_iban():
cc_no_check = 131400 # is "DE00"
bban = "".join(random.choices("0123456789", k=4))
check_digits = 98 - (int(f"{bban}{cc_no_check}") % 97)
return "DE" + (f"0{check_digits}"[-2:]) + bban
# IBANs
IBAN_EXCHANGE = get_random_iban()
IBAN_CUSTOMER = get_random_iban()
IBAN_MERCHANT_DEFAULT = get_random_iban()
IBAN_MERCHANT_DEMOSHOP = get_random_iban()
# Instances
INSTANCES = {
"GNUnet": IBAN_MERCHANT_DEMOSHOP,
"Taler": IBAN_MERCHANT_DEMOSHOP,
"Tor": IBAN_MERCHANT_DEMOSHOP,
"survey": IBAN_MERCHANT_DEMOSHOP,
"blog": IBAN_MERCHANT_DEMOSHOP
}
# Credentials / API keys
EXCHANGE_NEXUS_USERNAME = "exchange-nexus-user"
EXCHANGE_NEXUS_PASSWORD = "exchange-nexus-password"
FRONTENDS_API_TOKEN = "secret-token:secret"
TALER_MERCHANT_TOKEN = "secret-token:secret"
ALL_INSTANCES_BANK_PASSWORD = "secret"
EXCHANGE_BANK_ACCOUNT_SANDBOX = "sandbox-account-exchange"
EXCHANGE_BANK_ACCOUNT_PASSWORD = "secret"
# EBICS
EBICS_HOST_ID = "ebicsDeployedHost"
EXCHANGE_EBICS_USER_ID = "exchangeEbicsUserId"
EXCHANGE_EBICS_PARTNER_ID = "exchangeEbicsPartnerId"
EBICS_URL = REV_PROXY_URL + "/sandbox/ebicsweb"
# euFin
EXCHANGE_BANK_ACCOUNT_NEXUS = "exchange-imported-account-nexus"
EXCHANGE_BANK_CONNECTION = "exchange-ebics-connection"
NEXUS_DB_FILE = "/tmp/nexus.sqlite"
SANDBOX_DB_FILE = "/tmp/sandbox.sqlite"
EXCHANGE_FACADE_NAME = "exchange-taler-facade"
SANDBOX_ADMIN_USERNAME = "admin"
SANDBOX_ADMIN_PASSWORD = "secret"
class Command:
def __init__(
self, cmd, env=os.environ, log_dir=LOG_DIR,
custom_name=None, capture_stdout=False
):
if len(cmd) == 0:
fail("Command to execute was given empty.")
self.name = custom_name if custom_name else cmd[0]
self.cmd = cmd
self.capture_stdout = capture_stdout
self.log_dir = log_dir
self.env = env
def run(self):
self.do()
return_code = self.handle.wait()
if return_code != 0:
fail(f"Command {self.name} failed. Logs in {self.get_log_filename()}")
self.cleanup()
if self.capture_stdout:
return self.handle.communicate()[0].decode("utf-8").rstrip()
def get_log_filename(self):
return self.log_file.name
def cleanup(self):
if not self.log_file.closed:
self.log_file.flush()
self.log_file.close()
def do(self):
if not self.log_dir.is_dir():
os.makedirs(self.log_dir)
try:
log_filename = self.log_dir / f"{self.name}.log"
self.log_file = open(log_filename, "a+")
except Exception as error:
fail(f"Could not open log file: {log_filename}: {error}")
try:
self.handle = Popen(
self.cmd, # list
stdin=DEVNULL,
stdout=self.log_file if not self.capture_stdout else PIPE,
stderr=self.log_file,
env=self.env
)
except Exception as error:
fail(f"Could not execute: {' '.join(self.cmd)}: {error}")
class ConfigFile:
def __init__(self, filename):
self.sections = OrderedDict()
self.filename = filename
def destroy(self):
del self.sections
self.sections = OrderedDict()
def cfg_put(self, section_name, key, value):
s = self.sections[section_name] = self.sections.get(section_name, OrderedDict())
s[key] = value
def cfg_write(self, outdir):
if outdir:
if not os.path.isdir(outdir):
os.makedirs(outdir)
fstream = open(os.path.join(outdir, self.filename), "w")
else:
fstream = open(sys.stdout)
for section_name, section in self.sections.items():
fstream.write("[" + section_name + "]" + "\n")
for key, value in section.items():
fstream.write(key + " = " + value + "\n")
fstream.write("\n")
fstream.close()
def config_specify_master_pub(
filename,
currency,
exchange_master_pub
):
Command([
"taler-config", "-c", filename,
"-s", "exchange", "-o", "master_public_key",
"-V", exchange_master_pub
]).run()
Command([
"taler-config", "-c", filename,
"-s", f"merchant-exchange-{currency}",
"-o", "master_key",
"-V", exchange_master_pub
]).run()
# When called, there is no exchange master pub yet.
# taler-exchange-offline will prouce the key _after_
# taler.conf is generated. Only after that, we'll
# specify the master key where it is missing; namely
# in the merchant backend and exchange HTTP daemon sections.
def config_main(
filename,
outdir,
unix_sockets_dir,
currency,
rev_proxy_url,
wire_method,
merchant_wire_address,
exchange_wire_gateway_username,
exchange_wire_gateway_password,
frontend_api_key,
taler_runtime_dir
):
def coin(
obj,
currency,
name,
value,
d_withdraw="3 years",
d_spend="5 years",
d_legal="10 years",
f_withdraw="0.01",
f_deposit="0.01",
f_refresh="0.01",
f_refund="0.01",
rsa_keysize="2048",
):
sec = "coin_" + currency + "_" + name
obj.cfg_put(sec, "value", currency + ":" + value)
obj.cfg_put(sec, "duration_withdraw", d_withdraw)
obj.cfg_put(sec, "duration_spend", d_spend)
obj.cfg_put(sec, "duration_legal", d_legal)
obj.cfg_put(sec, "fee_withdraw", currency + ":" + f_withdraw)
obj.cfg_put(sec, "fee_refresh", currency + ":" + f_refresh)
obj.cfg_put(sec, "fee_refund", currency + ":" + f_refund)
obj.cfg_put(sec, "fee_deposit", currency + ":" + f_deposit)
obj.cfg_put(sec, "rsa_keysize", rsa_keysize)
obj = ConfigFile("taler.conf")
obj.cfg_put("paths", "TALER_DATA_HOME", str(TALER_DATA_DIR))
if not taler_runtime_dir.is_dir():
os.makedirs(taler_runtime_dir)
obj.cfg_put("paths", "TALER_RUNTIME_DIR", str(taler_runtime_dir))
obj.cfg_put("taler", "CURRENCY", currency)
obj.cfg_put("taler", "CURRENCY_ROUND_UNIT", f"{currency}:0.01")
obj.cfg_put("bank", "serve", "uwsgi")
obj.cfg_put("bank", "uwsgi_serve", "unix")
obj.cfg_put("bank", "uwsgi_unixpath", str(unix_sockets_dir / "bank.sock"))
obj.cfg_put("bank", "uwsgi_unixpath_mode", "660")
obj.cfg_put("bank", "database", "taler")
obj.cfg_put("bank", "max_debt", "%s:500.0" % currency)
obj.cfg_put("bank", "max_debt_bank", "%s:1000000000.0" % currency)
obj.cfg_put("bank", "allow_registrations", "YES")
obj.cfg_put("bank", "base_url", rev_proxy_url + "/bank/")
obj.cfg_put("bank", "database", "postgres:///taler")
obj.cfg_put("bank", "suggested_exchange", rev_proxy_url + "/exchange/")
obj.cfg_put("donations", "serve", "http")
obj.cfg_put("donations", "http_serve", "unix")
obj.cfg_put("donations", "http_unixpath", str(unix_sockets_dir / "donations.sock"))
obj.cfg_put("donations", "http_unixpath_mode", "660")
obj.cfg_put("landing", "serve", "http")
obj.cfg_put("landing", "http_serve", "unix")
obj.cfg_put("landing", "http_unixpath", str(unix_sockets_dir / "landing.sock"))
obj.cfg_put("landing", "http_unixpath_mode", "660")
obj.cfg_put("blog", "serve", "http")
obj.cfg_put("blog", "http_serve", "unix")
obj.cfg_put("blog", "http_unixpath", str(unix_sockets_dir / "blog.sock"))
obj.cfg_put("blog", "http_unixpath_mode", "660")
obj.cfg_put("survey", "serve", "http")
obj.cfg_put("survey", "http_serve", "unix")
obj.cfg_put("survey", "http_unixpath", str(unix_sockets_dir / "survey.sock"))
obj.cfg_put("survey", "http_unixpath_mode", "660")
obj.cfg_put("survey", "bank_password", "x")
obj.cfg_put("merchant", "serve", "unix")
obj.cfg_put("merchant", "unixpath", str(unix_sockets_dir / "merchant-backend.sock"))
obj.cfg_put("merchant", "wire_transfer_delay", "0 s")
obj.cfg_put("merchant", "default_max_wire_fee", currency + ":" + "0.01")
obj.cfg_put("merchant", "default_max_deposit_fee", currency + ":" + "0.05")
obj.cfg_put("merchantdb-postgres", "config", "postgres:///taler")
obj.cfg_put("frontends", "backend", rev_proxy_url + "/merchant-backend/")
obj.cfg_put(
"merchant-exchange-{}".format(currency),
"exchange_base_url", rev_proxy_url + "/exchange/",
)
obj.cfg_put(
"merchant-exchange-{}".format(currency),
"currency", currency
)
obj.cfg_put("auditor", "serve", "unix")
# FIXME: both below used?
obj.cfg_put("auditor", "base_url", rev_proxy_url + "/auditor")
obj.cfg_put("auditor", "auditor_url", rev_proxy_url + "/auditor")
obj.cfg_put("auditor", "unixpath", str(unix_sockets_dir / "auditor.sock"))
obj.cfg_put("auditor", "tiny_amount", currency + ":0.01")
obj.cfg_put(
"taler-exchange-secmod-eddsa",
"unixpath",
str(unix_sockets_dir / "exchange-secmod-eddsa.sock")
)
obj.cfg_put(
"taler-exchange-secmod-rsa",
"unixpath",
str(unix_sockets_dir / "exchange-secmod-rsa.sock")
)
obj.cfg_put("taler-exchange-secmod-rsa", "sm_priv_key",
"${TALER_DATA_HOME}/taler-exchange-secmod-rsa/secmod-private-key"
)
obj.cfg_put("exchange", "base_url", rev_proxy_url + "/exchange/")
obj.cfg_put("exchange", "serve", "unix")
obj.cfg_put("exchange", "unixpath", str(unix_sockets_dir / "exchange.sock"))
obj.cfg_put("exchange", "terms_etag", "0")
obj.cfg_put("exchange", "terms_dir", "$HOME/.local/share/taler-exchange/tos")
obj.cfg_put("exchange", "privacy_etag", "0")
obj.cfg_put("exchange", "privacy_dir", "$HOME/.local/share/taler-exchange/pp")
obj.cfg_put("exchangedb-postgres", "db_conn_str", "postgres:///taler")
obj.cfg_put("exchangedb-postgres", "config", "postgres:///taler")
obj.cfg_put("auditordb-postgres", "db_conn_str", "postgres:///taler")
obj.cfg_put("auditordb-postgres", "config", "postgres:///taler")
obj.cfg_put("exchange-account-1", "enable_debit", "yes")
obj.cfg_put("exchange-account-1", "enable_credit", "yes")
obj.cfg_put("merchant-account-merchant", "payto_uri",
f"payto://{wire_method}/{rev_proxy_url + '/sandbox'}/{merchant_wire_address}"
)
obj.cfg_put("merchant-account-merchant",
"wire_response",
"${TALER_DATA_HOME}/merchant/wire/merchant.json",
)
obj.cfg_put("merchant-account-merchant", "wire_file_mode", "770")
obj.cfg_put("frontends", "backend_apikey", f"{frontend_api_key}")
coin(obj, currency, "ct_10", "0.10")
coin(obj, currency, "1", "1")
coin(obj, currency, "2", "2")
coin(obj, currency, "5", "5")
coin(obj, currency, "10", "10")
coin(obj, currency, "1000", "1000")
obj.cfg_write(outdir)
return obj
def config_sync(filename, outdir, unix_sockets_dir, currency, api_key, rev_proxy_url):
obj = ConfigFile(filename)
obj.cfg_put("taler", "currency", currency)
obj.cfg_put("sync", "serve", "unix")
obj.cfg_put("sync", "unixpath", str(unix_sockets_dir / "sync.sock"))
obj.cfg_put("sync", "apikey", f"Bearer secret-token:{api_key}")
obj.cfg_put("sync", "annual_fee", f"{currency}:0.1")
obj.cfg_put("sync", "fulfillment_url", "taler://fulfillment-success/")
obj.cfg_put("sync", "payment_backend_url", rev_proxy_url + "merchant-backend/instances/Taler/")
obj.cfg_put("syncdb-postgres", "config", f"postgres:///taler")
obj.cfg_write(outdir)
def config_anastasis(filename, outdir, unix_sockets_dir, currency, rev_proxy_url, api_key):
obj = ConfigFile(filename)
obj.cfg_put("taler", "currency", currency)
obj.cfg_put("anastasis", "serve", "unix")
obj.cfg_put("anastasis", "business_name", f"GNU Taler Demo Anastasis Provider")
obj.cfg_put("anastasis", "unixpath", str(unix_sockets_dir / "anastasis.sock"))
obj.cfg_put("anastasis", "annual_fee", f"{currency}:0")
obj.cfg_put("anastasis", "question_cost", f"{currency}:0")
obj.cfg_put("anastasis", "insurance", f"{currency}:0")
obj.cfg_put("anastasis", "truth_upload_fee", f"{currency}:0")
obj.cfg_put("anastasis", "fulfillment_url", "taler://fulfillment-success/")
obj.cfg_put("anastasis", "server_salt", "kreb3ia9dmj43gfa")
obj.cfg_put("stasis-postgres", "config", f"postgres:///taler")
obj.cfg_put("anastasis-merchant-backend",
"payment_backend_url",
rev_proxy_url + "merchant-backend/instances/anastasis/"
)
obj.cfg_put("anastasis-merchant-backend", "api_key", f"Bearer secret-token:{api_key}")
obj.cfg_put("authorization-question", "cost", f"{currency}:0")
obj.cfg_put("authorization-question", "enabled", "yes")
obj.cfg_write(outdir)
def unit_file_content(description, cmd, env=None):
executable_name = cmd.split(" ")[0].split("/")[-1]
content = (
"[Unit]\n"
f"Description={description}\n"
"[Service]\n"
f"ExecStart={cmd}\n"
f"StandardOutput=append:{LOG_DIR / executable_name}.log\n"
f"StandardError=append:{LOG_DIR / executable_name}.log"
)
if env:
content += f"\nEnvironmentFile={env}"
return content
print_nn("Ensure no service is running...")
if is_serving(REV_PROXY_URL + "/", tries=3):
fail("Reverse proxy is unexpectedly running!")
if UNIX_SOCKETS_DIR.is_dir():
for left_socket in os.listdir(UNIX_SOCKETS_DIR):
s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
socket_file = str(UNIX_SOCKETS_DIR / left_socket)
if s.connect_ex(socket_file.encode("utf-8")) == 0:
fail(f"A service is unexpectedly running and bound to {socket_file}!")
print(" OK")
print_nn("Remove stale data and config...")
if TALER_DATA_DIR.exists():
shutil.rmtree(TALER_DATA_DIR)
if TALER_RUNTIME_DIR.exists():
shutil.rmtree(TALER_RUNTIME_DIR)
if CFG_OUTDIR.exists():
shutil.rmtree(CFG_OUTDIR)
print(" OK")
print_nn("Generate preliminary taler.conf...")
mc = config_main(
"taler.conf",
outdir=CFG_OUTDIR,
unix_sockets_dir=UNIX_SOCKETS_DIR,
currency=CURRENCY,
rev_proxy_url=REV_PROXY_URL,
wire_method=WIRE_METHOD,
merchant_wire_address=IBAN_MERCHANT_DEFAULT,
exchange_wire_gateway_username=EXCHANGE_NEXUS_USERNAME,
exchange_wire_gateway_password=EXCHANGE_NEXUS_PASSWORD,
frontend_api_key=FRONTENDS_API_TOKEN,
taler_runtime_dir=TALER_RUNTIME_DIR
)
print(" OK")
print_nn("Installing SystemD unit files...")
if not systemd_user_dir.exists():
systemd_user_dir.mkdir(parents=True, exist_ok=True)
if not TALER_UNIT_FILES_DIR.exists():
TALER_UNIT_FILES_DIR.mkdir(parents=True, exist_ok=True)
# Exchange HTTPD unit file.
with open(TALER_UNIT_FILES_DIR / "taler-local-exchange-httpd.service", "w") as exchange_unit:
exchange_unit.write(unit_file_content(
description = "Taler Exchange HTTP daemon",
cmd = f"{TALER_PREFIX}/bin/taler-exchange-httpd -L DEBUG -c {CFG_OUTDIR / 'taler.conf'}",
env = TALER_UNIT_FILES_DIR / "taler-local-postgres.env" if os.environ.get("PGPORT") else None
))
with open(TALER_UNIT_FILES_DIR / "taler-local-exchange-wirewatch.service", "w") as exchange_wirewatch_unit:
exchange_wirewatch_unit.write(unit_file_content(
description = "Taler Exchange Wirewatch",
cmd = f"{TALER_PREFIX}/bin/taler-exchange-wirewatch -L DEBUG -c {CFG_OUTDIR / 'taler.conf'}",
env = TALER_UNIT_FILES_DIR / "taler-local-postgres.env" if os.environ.get("PGPORT") else None
))
with open(TALER_UNIT_FILES_DIR / "taler-local-exchange-aggregator.service", "w") as exchange_aggregator_unit:
exchange_aggregator_unit.write(unit_file_content(
description = "Taler Exchange Aggregator",
cmd = f"{TALER_PREFIX}/bin/taler-exchange-aggregator -L DEBUG -c {CFG_OUTDIR / 'taler.conf'}",
env = TALER_UNIT_FILES_DIR / "taler-local-postgres.env" if os.environ.get("PGPORT") else None
))
with open(TALER_UNIT_FILES_DIR / "taler-local-exchange-secmod-rsa.service", "w") as exchange_rsa_unit:
exchange_rsa_unit.write(unit_file_content(
description = "Taler Exchange RSA security module",
cmd = f"{TALER_PREFIX}/bin/taler-exchange-secmod-rsa -L DEBUG -c {CFG_OUTDIR / 'taler.conf'}"
))
with open(TALER_UNIT_FILES_DIR / "taler-local-exchange-secmod-eddsa.service", "w") as exchange_eddsa_unit:
exchange_eddsa_unit.write(unit_file_content(
description = "Taler Exchange EDDSA security module",
cmd = f"{TALER_PREFIX}/bin/taler-exchange-secmod-eddsa -L DEBUG -c {CFG_OUTDIR / 'taler.conf'}"
))
with open(TALER_UNIT_FILES_DIR / "taler-local-merchant-backend.service", "w") as merchant_unit:
merchant_unit.write(unit_file_content(
description = "Taler Merchant backend",
cmd = f"{TALER_PREFIX}/bin/taler-merchant-httpd -L DEBUG -c {CFG_OUTDIR / 'taler.conf'}",
env = TALER_UNIT_FILES_DIR / "taler-local-postgres.env" if os.environ.get("PGPORT") else None
))
with open(TALER_UNIT_FILES_DIR / "taler-local-merchant-backend-token.service", "w") as merchant_token_unit:
merchant_token_unit.write(unit_file_content(
description = "Taler Merchant backend with auth token to allow default instance creation.",
cmd = f"{TALER_PREFIX}/bin/taler-merchant-httpd -a {TALER_MERCHANT_TOKEN} -L DEBUG -c {CFG_OUTDIR / 'taler.conf'}",
env = TALER_UNIT_FILES_DIR / "taler-local-postgres.env" if os.environ.get("PGPORT") else None
))
# Custom Postgres connection.
if os.environ.get("PGPORT"):
with open(TALER_UNIT_FILES_DIR / "taler-local-postgres.env", "w") as postgres_env:
postgres_env.write(f"PGPORT={os.environ.get('PGPORT')}")
# euFin unit files.
with open(TALER_UNIT_FILES_DIR / "taler-local-sandbox.service", "w") as sandbox_unit:
sandbox_unit.write(unit_file_content(
description = "euFin Sandbox",
cmd = f"{TALER_PREFIX}/bin/libeufin-sandbox serve --with-unix-socket {UNIX_SOCKETS_DIR / 'sandbox.sock'}",
env = TALER_UNIT_FILES_DIR / "taler-local-sandbox.env"
))
with open(TALER_UNIT_FILES_DIR / "taler-local-nexus.service", "w") as nexus_unit:
nexus_unit.write(unit_file_content(
description = "euFin Nexus",
cmd = f"{TALER_PREFIX}/bin/libeufin-nexus serve --with-unix-socket {UNIX_SOCKETS_DIR / 'nexus.sock'}",
env = TALER_UNIT_FILES_DIR / "taler-local-nexus.env"
))
# euFin env files.
with open(TALER_UNIT_FILES_DIR / "taler-local-sandbox.env", "w") as sandbox_env:
sandbox_env.write(f"LIBEUFIN_SANDBOX_DB_CONNECTION=jdbc:sqlite:{SANDBOX_DB_FILE}\n")
sandbox_env.write(f"LIBEUFIN_SANDBOX_ADMIN_PASSWORD={SANDBOX_ADMIN_PASSWORD}")
with open(TALER_UNIT_FILES_DIR / "taler-local-nexus.env", "w") as nexus_env:
nexus_env.write(f"LIBEUFIN_NEXUS_DB_CONNECTION=jdbc:sqlite:{NEXUS_DB_FILE}\n")
with open(TALER_UNIT_FILES_DIR / "taler-local-donations.service", "w") as donations_unit:
donations_unit.write(unit_file_content(
description = "Donation Website that accepts Taler payments.",
cmd = f"{TALER_PREFIX}/bin/taler-merchant-demos donations -c {CFG_OUTDIR / 'taler.conf'}",
env = TALER_UNIT_FILES_DIR / "taler-frontends-local.env"
))
with open(TALER_UNIT_FILES_DIR / "taler-local-blog.service", "w") as blog_unit:
blog_unit.write(unit_file_content(
description = "Blog that accepts Taler payments.",
cmd = f"{TALER_PREFIX}/bin/taler-merchant-demos blog -c {CFG_OUTDIR / 'taler.conf'}",
env = TALER_UNIT_FILES_DIR / "taler-frontends-local.env"
))
with open(TALER_UNIT_FILES_DIR / "taler-local-survey.service", "w") as survey_unit:
survey_unit.write(unit_file_content(
description = "Survey Website awarding tips via Taler.",
cmd = f"{TALER_PREFIX}/bin/taler-merchant-demos survey -c {CFG_OUTDIR / 'taler.conf'}",
env = TALER_UNIT_FILES_DIR / "taler-frontends-local.env"
))
with open(TALER_UNIT_FILES_DIR / "taler-local-landing.service", "w") as landing_unit:
landing_unit.write(unit_file_content(
description = "Landing Website of Taler demo.",
cmd = f"{TALER_PREFIX}/bin/taler-merchant-demos landing -c {CFG_OUTDIR / 'taler.conf'}",
env = TALER_UNIT_FILES_DIR / "taler-frontends-local.env"
))
with open(TALER_UNIT_FILES_DIR / "taler-local-frontends.env", "w") as frontends_env:
frontends_env.write((
f"PATH={os.environ.get('PATH')}\n"
f"TALER_CONFIG_FILE={CFG_OUTDIR / 'taler.conf'}\n"
f"TALER_ENV_URL_INTRO={REV_PROXY_URL + '/landing/'}\n"
f"TALER_ENV_URL_BANK={SANDBOX_URL + '/'}\n"
f"TALER_ENV_URL_MERCHANT_BLOG={REV_PROXY_URL + '/blog/'}\n"
f"TALER_ENV_URL_MERCHANT_DONATIONS={REV_PROXY_URL + '/donations/'}\n"
f"TALER_ENV_URL_MERCHANT_SURVEY={REV_PROXY_URL + '/survey/'}\n"
))
with open(TALER_UNIT_FILES_DIR / "taler-local-nginx.service", "w") as nginx_unit:
nginx_unit.write(unit_file_content(
description = "Nginx: reverse proxy for taler-local.",
cmd = f"nginx -c {CFG_OUTDIR / 'nginx.conf'}",
))
print(" OK")
print_nn("Reload SystemD...")
Command(["systemctl", "--user", "daemon-reload"]).run()
atexit.register(lambda: subprocess.run(
["systemctl", "--user", "stop", "taler-local-*.service"],
check=True
)
)
print(" OK")
print_nn("Generate exchange's master key...")
EXCHANGE_MASTER_PUB = Command(
[
"taler-exchange-offline",
"-c", CFG_OUTDIR / "taler.conf",
"setup"
],
capture_stdout=True
).run()
print(" OK")
print_nn("Specify exchange master pub in taler.conf...")
config_specify_master_pub(
CFG_OUTDIR / "taler.conf",
CURRENCY,
EXCHANGE_MASTER_PUB
)
print(" OK")
print_nn("Generating sync.conf...")
config_sync(
"sync.conf",
outdir=CFG_OUTDIR,
unix_sockets_dir=UNIX_SOCKETS_DIR,
currency=CURRENCY,
api_key=FRONTENDS_API_TOKEN,
rev_proxy_url=REV_PROXY_URL
)
print(" OK")
print_nn("Generating anastasis.conf...")
config_anastasis(
"anastasis.conf",
outdir=CFG_OUTDIR,
unix_sockets_dir=UNIX_SOCKETS_DIR,
currency=CURRENCY,
rev_proxy_url=REV_PROXY_URL,
api_key=FRONTENDS_API_TOKEN
)
print(" OK")
print_nn("Reset and init exchange DB..")
Command([
"taler-exchange-dbinit",
"-c", CFG_OUTDIR / "taler.conf",
"--reset"]
).run()
print(" OK")
print_nn("Launching the reverse proxy...")
with open(CFG_OUTDIR / "nginx.conf", "w") as nginx_conf:
nginx_conf.write((
f"error_log {LOG_DIR / 'nginx.log'};\n"
f"pid {TALER_ROOT_DIR / 'nginx.pid'};\n"
"daemon off;\n"
"events {}\n"
"http {\n"
f"access_log {LOG_DIR / 'nginx.log'};\n"
"server {\n"
f"listen {REV_PROXY_PORT};\n"
"location / {\n"
"return 200 'Hello, I am Nginx - proxying taler-local';\n"
"}\n"
"location ~* ^/(?[a-z\-]+)(/(?.*))? {\n"
"proxy_pass http://unix:/home/job/.taler/sockets/$component.sock:/$taler_uri?$args;\n"
"proxy_redirect off;\n"
"proxy_set_header X-Forwarded-Prefix /$component;\n"
f"proxy_set_header X-Forwarded-Host {REV_PROXY_NETLOC};\n"
f"proxy_set_header X-Forwarded-Proto {REV_PROXY_PROTO};\n"
"}\n"
"}\n"
"}\n"
))
subprocess.run(["systemctl", "--user", "start", "taler-local-nginx.service"], check=True)
if not is_serving(REV_PROXY_URL + "/"):
fail(f"Reverse proxy did not start correctly")
# Do check.
print(" OK")
print_nn("Launching the exchange RSA helper...")
subprocess.run(["systemctl", "--user", "start", "taler-local-exchange-secmod-rsa.service"])
print(" OK")
print_nn("Launching the exchange EDDSA helper...")
subprocess.run(["systemctl", "--user", "start", "taler-local-exchange-secmod-eddsa.service"])
print(" OK")
print_nn("Launching the exchange...")
subprocess.run(["systemctl", "--user", "start", "taler-local-exchange-httpd.service"])
if not is_serving(REV_PROXY_URL + "/exchange/"):
fail(f"Exchange did not start correctly.")
print(" OK")
print_nn("exchange-offline: signing key material...")
Command([
"taler-exchange-offline",
"-c", CFG_OUTDIR / "taler.conf",
"download", "sign", "upload"
]).run()
print(" OK")
# Set up wire fees for next 5 years
NOW = datetime.now()
YEAR = NOW.year
print_nn("Setting wire fees for the next 5 years...")
for year in range(YEAR, YEAR+5):
Command(
[
"taler-exchange-offline",
"-c", CFG_OUTDIR / "taler.conf",
"wire-fee",
str(year),
WIRE_METHOD,
CURRENCY + ":0.01",
CURRENCY + ":0.01",
"upload"
],
custom_name="set-wire-fee"
).run()
print(" OK")
print_nn("Reset and init auditor DB..")
Command([
"taler-auditor-dbinit",
"-c", CFG_OUTDIR / "taler.conf",
"--reset"]
).run()
print(" OK")
print_nn("Add this exchange to the auditor...")
Command(
[
"taler-auditor-exchange",
"-c", CFG_OUTDIR / "taler.conf",
"-m", EXCHANGE_MASTER_PUB,
"-u", REV_PROXY_URL + "/exchange/"
],
).run()
print(" OK")
## Step 4: Set up euFin
print_nn("Resetting euFin databases...")
try:
remove(SANDBOX_DB_FILE)
remove(NEXUS_DB_FILE)
except OSError as error:
if error.errno != errno.ENOENT:
raise error
print(" OK")
# This step transparantly creates a default demobank.
print_nn("Launching Sandbox...")
subprocess.run(["systemctl", "--user", "start", "taler-local-sandbox.service"])
if not is_serving(SANDBOX_URL):
fail(f"Sandbox did not start correctly.")
print(" OK")
print_nn("Make Sandbox EBICS host...")
Command(
[
"libeufin-cli", "sandbox",
"--sandbox-url", SANDBOX_URL,
"ebicshost", "create",
"--host-id", EBICS_HOST_ID,
],
env=get_sandbox_cli_env(
SANDBOX_ADMIN_USERNAME,
SANDBOX_ADMIN_PASSWORD,
),
custom_name="sandbox-create-ebicshost",
).run()
print(" OK")
print_nn("Create Exchange account at Sandbox...")
prepare_sandbox_account(
sandbox_url=SANDBOX_URL,
ebics_host_id=EBICS_HOST_ID,
ebics_partner_id=EXCHANGE_EBICS_PARTNER_ID,
ebics_user_id=EXCHANGE_EBICS_USER_ID,
person_name="Exchange Owner",
bank_account_name=EXCHANGE_BANK_ACCOUNT_SANDBOX,
password=EXCHANGE_BANK_ACCOUNT_PASSWORD
)
print(" OK")
print_nn("Getting exchange payto-URI from the bank.")
exchange_bank_account_info = get_sandbox_account_info(
SANDBOX_URL,
EXCHANGE_BANK_ACCOUNT_SANDBOX,
EXCHANGE_BANK_ACCOUNT_PASSWORD
)
EXCHANGE_PAYTO = exchange_bank_account_info["paytoUri"]
print(" OK")
print_nn("Specify own payto-URI to exchange's configuration..")
Command([
"taler-config", "-c", CFG_OUTDIR / 'taler.conf',
"-s", "exchange-account-1", "-o", "payto_uri", "-V",
EXCHANGE_PAYTO
]).run()
print(" OK")
print_nn(f"exchange-offline: enabling {EXCHANGE_PAYTO}...")
Command([
"taler-exchange-offline",
"-c", CFG_OUTDIR / "taler.conf",
"enable-account", EXCHANGE_PAYTO, "upload"
]).run()
print(" OK")
# Give each instance a Sandbox account (note: 'default')
# won't have one, as it should typically only manage other
# instances.
for instance_id, iban in INSTANCES.items():
print_nn(f"Create account of {instance_id} at Sandbox...")
prepare_sandbox_account(
sandbox_url=SANDBOX_URL,
ebics_host_id=EBICS_HOST_ID,
ebics_partner_id="unusedMerchantEbicsPartnerId",
ebics_user_id=f"unused{instance_id}EbicsUserId",
person_name=f"Shop Owner of {instance_id}",
bank_account_name=f"sandbox-account-{instance_id.lower()}",
password=ALL_INSTANCES_BANK_PASSWORD
)
print(" OK")
print_nn("Create Customer account at Sandbox...")
prepare_sandbox_account(
sandbox_url=SANDBOX_URL,
ebics_host_id=EBICS_HOST_ID,
ebics_partner_id="unusedCustomerEbicsPartnerId",
ebics_user_id="unusedCustomerEbicsUserId",
person_name="Customer Person",
bank_account_name=CUSTOMER_BANK_ACCOUNT,
password=CUSTOMER_BANK_PASSWORD
)
print(" OK")
print_nn("Make Nexus superuser ...")
Command(
[
"libeufin-nexus", "superuser",
EXCHANGE_NEXUS_USERNAME,
"--password", EXCHANGE_NEXUS_PASSWORD
],
env=get_nexus_server_env(
NEXUS_DB_FILE,
NEXUS_URL
),
custom_name="nexus-superuser",
).run()
print(" OK")
print_nn("Launching Nexus...")
subprocess.run(["systemctl", "--user", "start", "taler-local-nexus.service"])
if not is_serving(NEXUS_URL):
fail(f"Nexus did not start correctly")
print(" OK")
print_nn("Create Exchange account at Nexus...")
prepare_nexus_account(
ebics_url=EBICS_URL,
ebics_host_id=EBICS_HOST_ID,
ebics_partner_id=EXCHANGE_EBICS_PARTNER_ID,
ebics_user_id=EXCHANGE_EBICS_USER_ID,
bank_connection_name=EXCHANGE_BANK_CONNECTION,
bank_account_name_sandbox=EXCHANGE_BANK_ACCOUNT_SANDBOX,
bank_account_name_nexus=EXCHANGE_BANK_ACCOUNT_NEXUS,
env=get_nexus_cli_env(
EXCHANGE_NEXUS_USERNAME,
EXCHANGE_NEXUS_PASSWORD,
NEXUS_URL
)
)
print(" OK")
print_nn("Create Taler facade ...")
Command(
[
"libeufin-cli", "facades",
"new-taler-wire-gateway-facade",
"--currency", CURRENCY,
"--facade-name", EXCHANGE_FACADE_NAME,
EXCHANGE_BANK_CONNECTION,
EXCHANGE_BANK_ACCOUNT_NEXUS
],
env=get_nexus_cli_env(
EXCHANGE_NEXUS_USERNAME,
EXCHANGE_NEXUS_PASSWORD,
NEXUS_URL
),
custom_name="create-taler-facade",
).run()
print(" OK")
try:
response = requests.get(
NEXUS_URL + "/facades",
auth=requests.auth.HTTPBasicAuth(
EXCHANGE_NEXUS_USERNAME,
EXCHANGE_NEXUS_PASSWORD
)
)
response.raise_for_status()
except Exception as error:
fail(error)
FACADE_URL=response.json().get("facades")[0].get("baseUrl")
print_nn("Set suggested exchange at Sandbox...")
Command([
"libeufin-sandbox",
"default-exchange",
REV_PROXY_URL + "/exchange/",
EXCHANGE_PAYTO],
env={
"PATH": os.environ["PATH"],
"LIBEUFIN_SANDBOX_DB_CONNECTION": f"jdbc:sqlite:{SANDBOX_DB_FILE}"
}).run()
print(" OK")
# Point the exchange to the facade.
Command(
[
"taler-config",
"-c", CFG_OUTDIR / "taler.conf",
"-s", "exchange-accountcredentials-1",
"-o" "wire_gateway_auth_method",
"-V", "basic"
],
custom_name="specify-wire-gateway-auth-method",
).run()
Command(
[
"taler-config",
"-c", CFG_OUTDIR / "taler.conf",
"-s", "exchange-accountcredentials-1",
"-o" "wire_gateway_url",
"-V", FACADE_URL
],
custom_name="specify-facade-url",
).run()
Command(
[
"taler-config",
"-c", CFG_OUTDIR / "taler.conf",
"-s", "exchange-accountcredentials-1",
"-o" "username",
"-V", EXCHANGE_NEXUS_USERNAME
],
custom_name="specify-username-for-facade",
).run()
Command(
[
"taler-config",
"-c", CFG_OUTDIR / "taler.conf",
"-s", "exchange-accountcredentials-1",
"-o" "password",
"-V", EXCHANGE_NEXUS_PASSWORD
],
custom_name="specify-password-for-facade",
).run()
## Step 6: Set up merchant
print_nn("Reset and init merchant database...")
Command([
"taler-merchant-dbinit",
"-c", CFG_OUTDIR / "taler.conf",
"--reset"
]).run()
print(" OK")
def ensure_instance(
currency, instance_id,
backend_url, bank_hostname,
wire_method, merchant_wire_address,
auth_token
):
auth_header = {"Authorization": f"Bearer {auth_token}"}
resp = requests.get(
urljoin_nodrop(backend_url, f"management/instances/{instance_id}"),
headers = auth_header
)
bankaccount_info = get_sandbox_account_info(
SANDBOX_URL,
f"sandbox-account-{instance_id.lower()}",
ALL_INSTANCES_BANK_PASSWORD
)
req = dict(
id=instance_id,
name=f"Name of '{instance_id}'",
payto_uris=[bankaccount_info["paytoUri"]],
address=dict(),
jurisdiction=dict(),
default_max_wire_fee=f"{currency}:1",
default_wire_fee_amortization=3,
default_max_deposit_fee=f"{currency}:1",
default_wire_transfer_delay=dict(d_ms="forever"),
default_pay_delay=dict(d_ms="forever"),
auth=dict(method="token", token=auth_token),
)
http_method = requests.post
endpoint = "management/instances"
# Instance exists, patching it.
if resp.status_code == 200:
print(f"Patching instance '{instance_id}'")
http_method = requests.patch
endpoint = f"management/instances/{instance_id}"
resp = http_method(
urljoin_nodrop(backend_url, endpoint),
json=req,
headers = auth_header
)
if resp.status_code < 200 or resp.status_code >= 300:
print(f"Backend responds: {resp.status_code}/{resp.text}")
fail(f"Could not create (or patch) instance '{instance_id}'")
print_nn(f"Start merchant (with TALER_MERCHANT_TOKEN into the env)...")
subprocess.run(["systemctl", "--user", "start", "taler-local-merchant-backend-token.service"], check=True)
if not is_serving(REV_PROXY_URL + "/merchant-backend/config"):
fail(
f"Merchant backend did not start correctly.",
)
print(" OK")
print_nn("Give default instance a bank account...")
prepare_sandbox_account(
sandbox_url=SANDBOX_URL,
ebics_host_id=EBICS_HOST_ID,
ebics_partner_id="unusedMerchantEbicsPartnerId",
ebics_user_id=f"unusedDefaultInstanceEbicsUserId",
person_name=f"Shop Owner of default instance",
bank_account_name="sandbox-account-default",
password=ALL_INSTANCES_BANK_PASSWORD
)
print(" OK")
ensure_instance(
currency=CURRENCY,
instance_id="default",
backend_url = REV_PROXY_URL + "/merchant-backend",
bank_hostname = REV_PROXY_NETLOC + "/sandbox",
wire_method = "iban",
merchant_wire_address = IBAN_MERCHANT_DEFAULT,
auth_token=FRONTENDS_API_TOKEN
)
print_nn("Restarting the merchant WITHOUT the auth-token in the env...")
subprocess.run(["systemctl", "--user", "start", "taler-local-merchant-backend.service"], check=True)
if not is_serving(REV_PROXY_URL + "/merchant-backend/config"):
# check_running logs errors already.
fail(f"Merchant backend did not re start correctly.")
print(" OK")
for instance_id, iban in INSTANCES.items():
print_nn(f"Creating the {instance_id} instance...")
ensure_instance(
currency=CURRENCY,
instance_id=instance_id,
backend_url = REV_PROXY_URL + "/merchant-backend",
bank_hostname = REV_PROXY_NETLOC + "/sandbox",
wire_method = "iban",
merchant_wire_address = iban,
auth_token=FRONTENDS_API_TOKEN
)
print(" OK")
@cli.command()
def launch():
subprocess.run(["systemctl", "--user", "start", "taler-local-nginx.service"], check=True)
subprocess.run(["systemctl", "--user", "start", "taler-local-exchange-secmod-rsa.service"], check=True)
subprocess.run(["systemctl", "--user", "start", "taler-local-exchange-secmod-eddsa.service"], check=True)
subprocess.run(["systemctl", "--user", "start", "taler-local-exchange-httpd.service"], check=True)
subprocess.run(["systemctl", "--user", "start", "taler-local-exchange-wirewatch.service"], check=True)
subprocess.run(["systemctl", "--user", "start", "taler-local-exchange-aggregator.service"], check=True)
subprocess.run(["systemctl", "--user", "start", "taler-local-merchant-backend.service"], check=True)
subprocess.run(["systemctl", "--user", "start", "taler-local-sandbox.service"], check=True)
subprocess.run(["systemctl", "--user", "start", "taler-local-nexus.service"], check=True)
subprocess.run(["systemctl", "--user", "start", "taler-local-donations.service"], check=True)
subprocess.run(["systemctl", "--user", "start", "taler-local-blog.service"], check=True)
subprocess.run(["systemctl", "--user", "start", "taler-local-survey.service"], check=True)
@cli.command()
def stop():
subprocess.run(["systemctl", "--user", "stop", "taler-local-*.service"], check=True)
@cli.command()
def withdraw():
print_nn("Create withdrawal operation...")
resp = requests.post(REV_PROXY_URL +
f"/sandbox/demobanks/default/access-api/accounts/{CUSTOMER_BANK_ACCOUNT}/withdrawals",
json = dict(amount=CURRENCY + ":5"),
auth = requests.auth.HTTPBasicAuth(CUSTOMER_BANK_ACCOUNT, CUSTOMER_BANK_PASSWORD)
)
try:
resp.raise_for_status()
except Exception as error:
print("Could not create withdrawal")
print(error)
exit(1)
withdrawal_id = resp.json()["withdrawal_id"]
withdraw_uri = resp.json()["taler_withdraw_uri"]
print(" OK")
print("Let wallet specify the reserve public key at the bank...")
# Let wallet give the reserve public key to the bank.
subprocess.run(["taler-wallet-cli", "handle-uri", withdraw_uri], check=True)
# Let the user confirm the withdrawal operation and
# get the bank wire the funds.
print_nn("Confirm withdrawal operation at the bank...")
resp = requests.post(REV_PROXY_URL +
f"/sandbox/demobanks/default/access-api/accounts/{CUSTOMER_BANK_ACCOUNT}/withdrawals/{withdrawal_id}/confirm",
auth = requests.auth.HTTPBasicAuth(CUSTOMER_BANK_ACCOUNT, CUSTOMER_BANK_PASSWORD)
)
try:
resp.raise_for_status()
except Exception as error:
print("Could not create withdrawal")
print(error)
exit(1)
print(" OK")
print("Let wallet complete all pending operations")
subprocess.run(["taler-wallet-cli", "handle-uri", withdraw_uri], check=True)
subprocess.run(["taler-wallet-cli", "run-until-done"], check=True)
if __name__ == "__main__":
cli()