{ config, lib, pkgs, ... }: let cfg = config.services.dcrd; settingsFormat = pkgs.formats.ini { listsAsDuplicateKeys = true; }; networkPorts = { mainnet = 9108; testnet = 19108; simnet = 18555; regnet = 18655; }; effectivePort = if cfg.port == null then networkPorts.${cfg.network} else cfg.port; # dcrd requires settings to be lowercase lowercaseKeys = attrs: lib.mapAttrs' (k: v: { name = lib.strings.toLower k; value = v; }) attrs; baseAppOptions = {} // (if cfg.network == "testnet" then { testnet = true; } else if cfg.network == "simnet" then { simnet = true; } else if cfg.network == "regnet" then { regnet = true; } else {}) // lib.optionalAttrs (cfg.port != null || cfg.listenAddress != "0.0.0.0") { listen = "${cfg.listenAddress}:${toString effectivePort}"; }; combinedAppOptions = lib.recursiveUpdate baseAppOptions (lowercaseKeys cfg.settings); finalSettings = { "Application Options" = combinedAppOptions; }; generatedConfig = settingsFormat.generate "dcrd.conf" finalSettings; in { options.services.dcrd = with lib; { enable = mkEnableOption "Decred daemon"; package = mkOption { type = types.package; default = pkgs.dcrd; description = "Package providing the dcrd binary"; }; network = mkOption { type = types.enum [ "mainnet" "testnet" "simnet" "regnet" ]; default = "mainnet"; description = "Select which network to run: mainnet, testnet, simnet, or regnet"; }; port = mkOption { type = types.nullOr types.port; default = null; description = "P2P port override. Null uses the default for the selected network"; }; listenAddress = mkOption { type = types.str; default = "0.0.0.0"; description = "IP address to bind to listen for connections"; }; openFirewall = mkOption { type = types.bool; default = true; description = "Open P2P port in the firewall"; }; configFile = mkOption { type = types.nullOr types.path; default = null; description = "Absolute path to a dcrd.conf to use instead of the generated config. When set, the generated settings are ignored."; }; user = mkOption { type = types.str; default = "dcrd"; description = "User to run dcrd as"; }; group = mkOption { type = types.str; default = cfg.user; description = "Group to run dcrd as"; }; settings = mkOption { type = types.submodule { freeformType = types.attrsOf settingsFormat.lib.types.atom; }; default = {}; example = { externalip = [ "203.0.113.42" ]; rpclisten = [ "127.0.0.1:9109" "192.168.1.100:9109" ]; rpcuser = "dcrd"; rpcpass = "hunter2"; txindex = true; proxy = "127.0.0.1:9050"; }; description = '' Options that accept multiple values (like rpclisten, externalip, listen, addpeer, miningaddr, altdnsnames, whitelist) should be specified as lists. Single-value options (like rpcuser, rpcpass, proxy, txindex) should be specified as strings or booleans. ''; }; }; config = lib.mkIf cfg.enable { users.groups.${cfg.group} = {}; users.users.${cfg.user} = { isSystemUser = true; group = cfg.group; description = "Decred daemon user"; }; networking.firewall.allowedTCPPorts = lib.mkIf cfg.openFirewall [ effectivePort ]; systemd.services.dcrd = { description = "Decred full node"; after = [ "network-online.target" ]; wants = [ "network-online.target" ]; wantedBy = [ "multi-user.target" ]; serviceConfig = { User = cfg.user; Group = cfg.group; StateDirectory = "dcrd"; StateDirectoryMode = "0750"; ExecStart = ''${lib.getExe cfg.package} --appdata=/var/lib/dcrd --configfile=${if cfg.configFile != null then cfg.configFile else generatedConfig}''; Restart = "on-failure"; RestartSec = 5; }; restartTriggers = [ generatedConfig ] ++ lib.optional (cfg.configFile != null) cfg.configFile; }; }; }