DROP WHEN MERGED: taler modules from #332699

This commit is contained in:
Daniel Lovbrotte Olsen 2024-11-14 22:35:44 +01:00
parent e6baa1c725
commit d88dd0a45c
9 changed files with 872 additions and 0 deletions

View File

@ -0,0 +1,93 @@
{
lib,
config,
options,
...
}:
{
imports = [ (import ./common.nix "bank") ];
options.services.libeufin.bank = {
initialAccounts = lib.mkOption {
type = lib.types.listOf lib.types.attrs;
description = ''
Accounts to enable before the bank service starts.
This is mainly needed for the nexus currency conversion
since the exchange's bank account is expected to be already
registered.
Don't forget to change the account passwords afterwards.
'';
default = [ ];
};
settings = lib.mkOption {
description = ''
Configuration options for the libeufin bank system config file.
For a list of all possible options, please see the man page [`libeufin-bank.conf(5)`](https://docs.taler.net/manpages/libeufin-bank.conf.5.html)
'';
type = lib.types.submodule {
inherit (options.services.libeufin.settings.type.nestedTypes) freeformType;
options = {
libeufin-bank = {
CURRENCY = lib.mkOption {
type = lib.types.str;
description = ''
The currency under which the libeufin-bank should operate.
This defaults to the GNU taler module's currency for convenience
but if you run libeufin-bank separately from taler, you must set
this yourself.
'';
};
PORT = lib.mkOption {
type = lib.types.port;
default = 8082;
description = ''
The port on which libeufin-bank should listen.
'';
};
SUGGESTED_WITHDRAWAL_EXCHANGE = lib.mkOption {
type = lib.types.str;
default = "https://exchange.demo.taler.net/";
description = ''
Exchange that is suggested to wallets when withdrawing.
Note that, in order for withdrawals to work, your libeufin-bank
must be able to communicate with and send money etc. to the bank
at which the exchange used for withdrawals has its bank account.
If you also have your own bank and taler exchange network, you
probably want to set one of your exchange's url here instead of
the demo exchange.
This setting must always be set in order for the Android app to
not crash during the withdrawal process but the exchange to be
used can always be changed in the app.
'';
};
};
libeufin-bankdb-postgres = {
CONFIG = lib.mkOption {
type = lib.types.str;
description = ''
The database connection string for the libeufin-bank database.
'';
};
};
};
};
};
};
config = {
services.libeufin.bank.settings.libeufin-bank.CURRENCY = lib.mkIf (
config.services.taler.enable && (config.services.taler.settings.taler ? CURRENCY)
) config.services.taler.settings.taler.CURRENCY;
services.libeufin.bank.settings.libeufin-bankdb-postgres.CONFIG = lib.mkIf config.services.libeufin.bank.createLocalDatabase "postgresql:///libeufin-bank";
};
}

View File

@ -0,0 +1,156 @@
# TODO: create a common module generator for Taler and Libeufin?
libeufinComponent:
{
lib,
pkgs,
config,
...
}:
{
options.services.libeufin.${libeufinComponent} = {
enable = lib.mkEnableOption "libeufin core banking system and web interface";
package = lib.mkPackageOption pkgs "libeufin" { };
debug = lib.mkEnableOption "debug logging";
createLocalDatabase = lib.mkEnableOption "automatic creation of a local postgres database";
openFirewall = lib.mkOption {
type = lib.types.bool;
default = false;
description = "Whether to open ports in the firewall";
};
};
config =
let
cfgMain = config.services.libeufin;
cfg = cfgMain.${libeufinComponent};
serviceName = "libeufin-${libeufinComponent}";
isNexus = libeufinComponent == "nexus";
# get database name from config
# TODO: should this always be the same db? In which case, should this be an option directly under `services.libeufin`?
dbName =
lib.removePrefix "postgresql:///"
cfg.settings."libeufin-${libeufinComponent}db-postgres".CONFIG;
bankPort = cfg.settings."${if isNexus then "nexus-httpd" else "libeufin-bank"}".PORT;
in
lib.mkIf cfg.enable {
services.libeufin.settings = cfg.settings;
# TODO add system-libeufin.slice?
systemd.services = {
# Main service
"${serviceName}" = {
serviceConfig = {
DynamicUser = true;
ExecStart =
let
args = lib.cli.toGNUCommandLineShell { } {
c = cfgMain.configFile;
L = if cfg.debug then "debug" else null;
};
in
"${lib.getExe' cfg.package "libeufin-${libeufinComponent}"} serve ${args}";
Restart = "on-failure";
RestartSec = "10s";
};
requires = [ "libeufin-dbinit.service" ];
after = [ "libeufin-dbinit.service" ];
wantedBy = [ "multi-user.target" ];
};
# Database Initialisation
libeufin-dbinit =
let
dbScript = pkgs.writers.writeText "libeufin-db-permissions.sql" ''
GRANT SELECT,INSERT,UPDATE,DELETE ON ALL TABLES IN SCHEMA libeufin_bank TO "${serviceName}";
GRANT SELECT,INSERT,UPDATE,DELETE ON ALL TABLES IN SCHEMA libeufin_nexus TO "${serviceName}";
GRANT USAGE ON SCHEMA libeufin_bank TO "${serviceName}";
GRANT USAGE ON SCHEMA libeufin_nexus TO "${serviceName}";
'';
# Accounts to be created after the bank database initialization.
#
# For example, if the bank's currency conversion is enabled, it's
# required that the exchange account is registered before the
# service starts.
initialAccountRegistration = lib.concatMapStringsSep "\n" (
account:
let
args = lib.cli.toGNUCommandLineShell { } {
c = cfgMain.configFile;
inherit (account) username password name;
payto_uri = "payto://x-taler-bank/bank:${toString bankPort}/${account.username}?receiver-name=${account.name}";
exchange = lib.toLower account.username == "exchange";
};
in
"${lib.getExe' cfg.package "libeufin-bank"} create-account ${args}"
) cfg.initialAccounts;
args = lib.cli.toGNUCommandLineShell { } {
c = cfgMain.configFile;
L = if cfg.debug then "debug" else null;
};
in
{
path = [ config.services.postgresql.package ];
serviceConfig = {
Type = "oneshot";
DynamicUser = true;
StateDirectory = "libeufin-dbinit";
StateDirectoryMode = "0750";
User = dbName;
};
script = lib.optionalString cfg.enable ''
${lib.getExe' cfg.package "libeufin-${libeufinComponent}"} dbinit ${args}
'';
# Grant DB permissions after schemas have been created
postStart =
''
psql -U "${dbName}" -f "${dbScript}"
''
+ lib.optionalString ((!isNexus) && (cfg.initialAccounts != [ ])) ''
# only register initial accounts once
if [ ! -e /var/lib/libeufin-dbinit/init ]; then
${initialAccountRegistration}
touch /var/lib/libeufin-dbinit/init
echo "Bank initialisation complete"
fi
'';
requires = lib.optionals cfg.createLocalDatabase [ "postgresql.service" ];
after = [ "network.target" ] ++ lib.optionals cfg.createLocalDatabase [ "postgresql.service" ];
};
};
networking.firewall = lib.mkIf cfg.openFirewall {
allowedTCPPorts = [
bankPort
];
};
environment.systemPackages = [ cfg.package ];
services.postgresql = lib.mkIf cfg.createLocalDatabase {
enable = true;
ensureDatabases = [ dbName ];
ensureUsers = [
{ name = serviceName; }
{
name = dbName;
ensureDBOwnership = true;
}
];
};
assertions = [
{
assertion =
cfg.createLocalDatabase || (cfg.settings."libeufin-${libeufinComponent}db-postgres" ? CONFIG);
message = "Libeufin ${libeufinComponent} database is not configured.";
}
];
};
}

View File

@ -0,0 +1,29 @@
{
lib,
pkgs,
config,
...
}:
let
cfg = config.services.libeufin;
settingsFormat = pkgs.formats.ini { };
in
{
options.services.libeufin = {
configFile = lib.mkOption {
internal = true;
default = settingsFormat.generate "generated-libeufin.conf" cfg.settings;
};
settings = lib.mkOption {
description = "Global configuration options for the libeufin bank system config file.";
type = lib.types.submodule { freeformType = settingsFormat.type; };
default = { };
};
};
config = lib.mkIf (cfg.bank.enable || cfg.nexus.enable) {
environment.etc."libeufin/libeufin.conf".source = cfg.configFile;
};
}

View File

@ -0,0 +1,127 @@
{
lib,
config,
options,
...
}:
{
imports = [ (import ./common.nix "nexus") ];
options.services.libeufin.nexus.settings = lib.mkOption {
description = ''
Configuration options for the libeufin nexus config file.
For a list of all possible options, please see the man page [`libeufin-nexus.conf(5)`](https://docs.taler.net/manpages/libeufin-nexus.conf.5.html)
'';
type = lib.types.submodule {
inherit (options.services.libeufin.settings.type.nestedTypes) freeformType;
options = {
nexus-ebics = {
# Mandatory configuration values
# https://docs.taler.net/libeufin/nexus-manual.html#setting-up-the-ebics-subscriber
# https://docs.taler.net/libeufin/setup-ebics-at-postfinance.html
CURRENCY = lib.mkOption {
description = "Name of the fiat currency.";
type = lib.types.nonEmptyStr;
example = "CHF";
};
HOST_BASE_URL = lib.mkOption {
description = "URL of the EBICS server.";
type = lib.types.nonEmptyStr;
example = "https://ebics.postfinance.ch/ebics/ebics.aspx";
};
BANK_DIALECT = lib.mkOption {
description = ''
Name of the following combination: EBICS version and ISO20022
recommendations that Nexus would honor in the communication with the
bank.
Currently only the "postfinance" or "gls" value is supported.
'';
type = lib.types.enum [
"postfinance"
"gls"
];
example = "postfinance";
};
HOST_ID = lib.mkOption {
description = "Name of the EBICS host.";
type = lib.types.nonEmptyStr;
example = "PFEBICS";
};
USER_ID = lib.mkOption {
description = ''
User ID of the EBICS subscriber.
This value must be assigned by the bank after having activated a new EBICS subscriber.
'';
type = lib.types.nonEmptyStr;
example = "PFC00563";
};
PARTNER_ID = lib.mkOption {
description = ''
Partner ID of the EBICS subscriber.
This value must be assigned by the bank after having activated a new EBICS subscriber.
'';
type = lib.types.nonEmptyStr;
example = "PFC00563";
};
IBAN = lib.mkOption {
description = "IBAN of the bank account that is associated with the EBICS subscriber.";
type = lib.types.nonEmptyStr;
example = "CH7789144474425692816";
};
BIC = lib.mkOption {
description = "BIC of the bank account that is associated with the EBICS subscriber.";
type = lib.types.nonEmptyStr;
example = "POFICHBEXXX";
};
NAME = lib.mkOption {
description = "Legal entity that is associated with the EBICS subscriber.";
type = lib.types.nonEmptyStr;
example = "John Smith S.A.";
};
BANK_PUBLIC_KEYS_FILE = lib.mkOption {
type = lib.types.path;
default = "/var/lib/libeufin-nexus/bank-ebics-keys.json";
description = ''
Filesystem location where Nexus should store the bank public keys.
'';
};
CLIENT_PRIVATE_KEYS_FILE = lib.mkOption {
type = lib.types.path;
default = "/var/lib/libeufin-nexus/client-ebics-keys.json";
description = ''
Filesystem location where Nexus should store the subscriber private keys.
'';
};
};
nexus-httpd = {
PORT = lib.mkOption {
type = lib.types.port;
default = 8084;
description = ''
The port on which libeufin-bank should listen.
'';
};
};
libeufin-nexusdb-postgres = {
CONFIG = lib.mkOption {
type = lib.types.str;
description = ''
The database connection string for the libeufin-nexus database.
'';
};
};
};
};
};
config =
let
cfgMain = config.services.libeufin;
cfg = config.services.libeufin.nexus;
in
lib.mkIf cfg.enable {
services.libeufin.nexus.settings.libeufin-nexusdb-postgres.CONFIG = lib.mkIf (
cfgMain.bank.enable && cfgMain.bank.createLocalDatabase
) "postgresql:///libeufin-bank";
};
}

View File

@ -0,0 +1,10 @@
{
imports = [
./libeufin/bank.nix
./libeufin/module.nix
./libeufin/nexus.nix
./taler/exchange.nix
./taler/merchant.nix
./taler/module.nix
];
}

View File

@ -0,0 +1,117 @@
# TODO: create a common module generator for Taler and Libeufin?
{
talerComponent ? "",
servicesDB ? [ ],
servicesNoDB ? [ ],
...
}:
{
lib,
pkgs,
config,
...
}:
let
cfg = cfgTaler.${talerComponent};
cfgTaler = config.services.taler;
settingsFormat = pkgs.formats.ini { };
services = servicesDB ++ servicesNoDB;
dbName = "taler-${talerComponent}-httpd";
groupName = "taler-${talerComponent}-services";
inherit (cfgTaler) runtimeDir;
in
{
options = {
services.taler.${talerComponent} = {
enable = lib.mkEnableOption "the GNU Taler ${talerComponent}";
package = lib.mkPackageOption pkgs "taler-${talerComponent}" { };
# TODO: make option accept multiple debugging levels?
debug = lib.mkEnableOption "debug logging";
openFirewall = lib.mkOption {
type = lib.types.bool;
default = false;
description = "Whether to open ports in the firewall";
};
};
};
config = lib.mkIf cfg.enable {
services.taler.enable = cfg.enable;
systemd.services = lib.mergeAttrsList [
# Main services
(lib.genAttrs (map (n: "taler-${talerComponent}-${n}") services) (name: {
serviceConfig = {
DynamicUser = true;
User = name;
Group = groupName;
ExecStart = toString [
(lib.getExe' cfg.package name)
"-c /etc/taler/taler.conf"
(lib.optionalString cfg.debug " -L debug")
];
RuntimeDirectory = name;
StateDirectory = name;
CacheDirectory = name;
ReadWritePaths = [ runtimeDir ];
Restart = "always";
RestartSec = "10s";
};
requires = [ "taler-${talerComponent}-dbinit.service" ];
after = [ "taler-${talerComponent}-dbinit.service" ];
wantedBy = [ "multi-user.target" ]; # TODO slice?
}))
# Database Initialisation
{
"taler-${talerComponent}-dbinit" = {
path = [ config.services.postgresql.package ];
serviceConfig = {
Type = "oneshot";
DynamicUser = true;
User = dbName;
Restart = "on-failure";
RestartSec = "5s";
};
requires = [ "postgresql.service" ];
after = [ "postgresql.service" ];
};
}
];
users.groups.${groupName} = { };
systemd.tmpfiles.settings = {
"10-taler-${talerComponent}" = {
"${runtimeDir}" = {
d = {
group = groupName;
user = "nobody";
mode = "070";
};
};
};
};
networking.firewall = lib.mkIf cfg.openFirewall {
allowedTCPPorts = [ cfg.settings."${talerComponent}".PORT ];
};
environment.systemPackages = [ cfg.package ];
services.taler.includes = [ "/etc/taler/conf.d/${talerComponent}.conf" ];
environment.etc."taler/conf.d/${talerComponent}.conf".source = settingsFormat.generate "generated-taler.conf" cfg.settings;
services.postgresql = {
enable = true;
ensureDatabases = [ dbName ];
ensureUsers = map (service: { name = "taler-${talerComponent}-${service}"; }) servicesDB ++ [
{
name = dbName;
ensureDBOwnership = true;
}
];
};
};
}

View File

@ -0,0 +1,143 @@
{
lib,
config,
options,
pkgs,
...
}:
let
cfg = cfgTaler.exchange;
cfgTaler = config.services.taler;
talerComponent = "exchange";
# https://docs.taler.net/taler-exchange-manual.html#services-users-groups-and-file-system-hierarchy
servicesDB = [
"httpd"
"aggregator"
"closer"
"wirewatch"
];
servicesNoDB = [
"secmod-cs"
"secmod-eddsa"
"secmod-rsa"
];
in
{
imports = [
(import ./common.nix { inherit talerComponent servicesDB servicesNoDB; })
];
options.services.taler.exchange = {
settings = lib.mkOption {
description = ''
Configuration options for the taler exchange config file.
For a list of all possible options, please see the man page [`taler.conf(5)`](https://docs.taler.net/manpages/taler.conf.5.html#exchange-options)
'';
type = lib.types.submodule {
inherit (options.services.taler.settings.type.nestedTypes) freeformType;
options = {
# TODO: do we want this to be a sub-attribute or only define the exchange set of options here
exchange = {
AML_THRESHOLD = lib.mkOption {
type = lib.types.str;
default = "${cfgTaler.settings.taler.CURRENCY}:1000000";
defaultText = "1000000 in {option}`CURRENCY`";
description = "Monthly transaction volume until an account is considered suspicious and flagged for AML review.";
};
DB = lib.mkOption {
type = lib.types.str;
internal = true;
default = "postgres";
};
MASTER_PUBLIC_KEY = lib.mkOption {
type = lib.types.str;
default = throw ''
You must provide `MASTER_PUBLIC_KEY` with the public part of your master key.
This will be used by the auditor service to get information about the exchange.
For more information, see https://docs.taler.net/taler-auditor-manual.html#initial-configuration
To generate this key, you must run `taler-exchange-offline setup`. It will print the public key.
'';
defaultText = "None, you must set this yourself.";
description = "Used by the exchange to verify information signed by the offline system.";
};
PORT = lib.mkOption {
type = lib.types.port;
default = 8081;
description = "Port on which the HTTP server listens.";
};
};
exchangedb-postgres = {
CONFIG = lib.mkOption {
type = lib.types.str;
internal = true;
default = "postgres:///taler-exchange-httpd";
description = "Database connection URI.";
};
};
};
};
default = { };
};
denominationConfig = lib.mkOption {
type = lib.types.lines;
defaultText = "None, you must set this yourself.";
example = ''
[COIN-KUDOS-n1-t1718140083]
VALUE = KUDOS:0.1
DURATION_WITHDRAW = 7 days
DURATION_SPEND = 2 years
DURATION_LEGAL = 6 years
FEE_WITHDRAW = KUDOS:0
FEE_DEPOSIT = KUDOS:0.1
FEE_REFRESH = KUDOS:0
FEE_REFUND = KUDOS:0
RSA_KEYSIZE = 2048
CIPHER = RSA
'';
description = ''
This option configures the cash denomination for the coins that the exchange offers.
For more information, consult the [upstream docs](https://docs.taler.net/taler-exchange-manual.html#coins-denomination-keys).
You can either write these manually or you can use the `taler-harness deployment gen-coin-config`
command to generate it.
Warning: Do not modify existing denominations after deployment.
Please see the upstream docs for how to safely do that.
'';
};
};
config = lib.mkIf cfg.enable {
services.taler.includes = [
(pkgs.writers.writeText "exchange-denominations.conf" cfg.denominationConfig)
];
systemd.services.taler-exchange-wirewatch = {
requires = [ "taler-exchange-httpd.service" ];
after = [ "taler-exchange-httpd.service" ];
};
# Taken from https://docs.taler.net/taler-exchange-manual.html#exchange-database-setup
# TODO: Why does aggregator need DELETE?
systemd.services."taler-${talerComponent}-dbinit".script =
let
deletePerm = name: lib.optionalString (name == "aggregator") ",DELETE";
dbScript = pkgs.writers.writeText "taler-exchange-db-permissions.sql" (
lib.pipe servicesDB [
(map (name: ''
GRANT SELECT,INSERT,UPDATE${deletePerm name} ON ALL TABLES IN SCHEMA exchange TO "taler-exchange-${name}";
GRANT USAGE ON SCHEMA exchange TO "taler-exchange-${name}";
''))
lib.concatStrings
]
);
in
''
${lib.getExe' cfg.package "taler-exchange-dbinit"}
psql -U taler-exchange-httpd -f ${dbScript}
'';
};
}

View File

@ -0,0 +1,108 @@
{
lib,
config,
options,
pkgs,
...
}:
let
cfg = cfgTaler.merchant;
cfgTaler = config.services.taler;
talerComponent = "merchant";
# https://docs.taler.net/taler-merchant-manual.html#launching-the-backend
servicesDB = [
"httpd"
"webhook"
"wirewatch"
"depositcheck"
"exchange"
];
in
{
imports = [
(import ./common.nix { inherit talerComponent servicesDB; })
];
options.services.taler.merchant = {
settings = lib.mkOption {
description = ''
Configuration options for the taler merchant config file.
For a list of all possible options, please see the man page [`taler.conf(5)`](https://docs.taler.net/manpages/taler.conf.5.html#merchant-options)
'';
type = lib.types.submodule {
inherit (options.services.taler.settings.type.nestedTypes) freeformType;
options = {
# TODO: do we want this to be a sub-attribute or only define the merchant set of options here
merchant = {
DB = lib.mkOption {
type = lib.types.str;
internal = true;
default = "postgres";
description = "Plugin to use for the database.";
};
PORT = lib.mkOption {
type = lib.types.port;
default = 8083;
description = "Port on which the HTTP server listens.";
};
SERVE = lib.mkOption {
type = lib.types.str;
default = "tcp";
description = ''
Whether the HTTP server should listen on a UNIX domain socket ("unix") or on a TCP socket ("tcp").
'';
};
LEGAL_PRESERVATION = lib.mkOption {
type = lib.types.str;
internal = true;
default = "10 years";
description = "How long to keep data in the database for tax audits after the transaction has completed.";
};
};
merchantdb-postgres = {
CONFIG = lib.mkOption {
type = lib.types.str;
internal = true;
default = "postgres:///taler-${talerComponent}-httpd";
description = "Database connection URI.";
};
SQL_DIR = lib.mkOption {
type = lib.types.str;
internal = true;
default = "${cfg.package}/share/taler/sql/merchant/";
description = "The location for the SQL files to setup the database tables.";
};
};
};
};
default = { };
};
};
config = lib.mkIf cfg.enable {
systemd.services.taler-merchant-depositcheck = {
# taler-merchant-depositcheck needs its executable is in the PATH
# NOTE: couldn't use `lib.getExe` to only get that single executable
path = [ cfg.package ];
};
systemd.services."taler-${talerComponent}-dbinit".script =
let
# NOTE: not documented, but is necessary
dbScript = pkgs.writers.writeText "taler-merchant-db-permissions.sql" (
lib.concatStrings (
map (name: ''
GRANT SELECT,INSERT,UPDATE,DELETE ON ALL TABLES IN SCHEMA merchant TO "taler-merchant-${name}";
GRANT USAGE ON SCHEMA merchant TO "taler-merchant-${name}";
'') servicesDB
)
);
in
''
${lib.getExe' cfg.package "taler-merchant-dbinit"}
psql -U taler-${talerComponent}-httpd -f ${dbScript}
'';
};
}

View File

@ -0,0 +1,89 @@
{
lib,
pkgs,
config,
...
}:
let
cfg = config.services.taler;
settingsFormat = pkgs.formats.ini { };
in
{
# TODO turn this into a generic taler-like service thingy?
options.services.taler = {
enable = lib.mkEnableOption "the GNU Taler system" // lib.mkOption { internal = true; };
includes = lib.mkOption {
type = lib.types.listOf lib.types.path;
default = [ ];
description = ''
Files to include into the config file using Taler's `@inline@` directive.
This allows including arbitrary INI files, including imperatively managed ones.
'';
};
settings = lib.mkOption {
description = ''
Global configuration options for the taler config file.
For a list of all possible options, please see the man page [`taler.conf(5)`](https://docs.taler.net/manpages/taler.conf.5.html)
'';
type = lib.types.submodule {
freeformType = settingsFormat.type;
options = {
taler = {
CURRENCY = lib.mkOption {
type = lib.types.nonEmptyStr;
description = ''
The currency which taler services will operate with. This cannot be changed later.
'';
};
CURRENCY_ROUND_UNIT = lib.mkOption {
type = lib.types.str;
default = "${cfg.settings.taler.CURRENCY}:0.01";
defaultText = lib.literalExpression ''
"''${config.services.taler.settings.taler.CURRENCY}:0.01"
'';
description = ''
Smallest amount in this currency that can be transferred using the underlying RTGS.
You should probably not touch this.
'';
};
};
};
};
default = { };
};
runtimeDir = lib.mkOption {
type = lib.types.str;
default = "/run/taler-system-runtime/";
description = ''
Runtime directory shared between the taler services.
Crypto helpers put their sockets here for instance and the httpd
connects to them.
'';
};
};
config = lib.mkIf cfg.enable {
services.taler.settings.PATHS = {
TALER_DATA_HOME = "\${STATE_DIRECTORY}/";
TALER_CACHE_HOME = "\${CACHE_DIRECTORY}/";
TALER_RUNTIME_DIR = cfg.runtimeDir;
};
environment.etc."taler/taler.conf".source =
let
includes = pkgs.writers.writeText "includes.conf" (
lib.concatStringsSep "\n" (map (include: "@inline@ ${include}") cfg.includes)
);
generatedConfig = settingsFormat.generate "generated-taler.conf" cfg.settings;
in
pkgs.runCommand "taler.conf" { } ''
cat ${includes} > $out
echo >> $out
echo >> $out
cat ${generatedConfig} >> $out
'';
};
}