{ config, lib, pkgs, ... }: with lib; let options.services.rtl = { enable = mkEnableOption "Ride The Lightning, a web interface for lnd and clightning "; address = mkOption { type = types.str; default = "127.0.0.1"; description = "HTTP server address."; }; port = mkOption { type = types.port; default = 3000; description = "HTTP server port."; }; dataDir = mkOption { type = types.path; default = "/var/lib/rtl"; description = "The data directory for RTL."; }; nodes = { clightning = mkOption { type = types.bool; default = false; description = "Enable the clightning node interface."; }; lnd = mkOption { type = types.bool; default = false; description = "Enable the lnd node interface."; }; reverseOrder = mkOption { type = types.bool; default = false; description = '' Reverse the order of nodes shown in the UI. By default, clightning is shown before lnd. ''; }; }; loop = mkOption { type = types.bool; default = false; description = "Whether to enable swaps with lightning-loop."; }; nightTheme = mkOption { type = types.bool; default = false; description = "Enable the Night UI Theme."; }; extraCurrency = mkOption { type = with types; nullOr str; default = null; example = "USD"; description = '' Currency code (ISO 4217) of the extra currency used for displaying balances. When set, this option enables online currency rate fetching. Warning: Rate fetching requires outgoing clearnet connections, so option `tor.enforce` is automatically disabled. ''; }; user = mkOption { type = types.str; default = "rtl"; description = "The user as which to run RTL."; }; group = mkOption { type = types.str; default = cfg.user; description = "The group as which to run RTL."; }; cl-rest = { enable = mkOption { readOnly = true; type = types.bool; default = cfg.nodes.clightning; description = '' Enable c-lightning-REST server. This service is required for clightning support and is automatically enabled. ''; }; address = mkOption { readOnly = true; default = "0.0.0.0"; description = '' Rest server address. Not configurable. The server always listens on all interfaces: https://github.com/Ride-The-Lightning/c-lightning-REST/issues/84 ''; }; port = mkOption { type = types.port; default = 3001; description = "REST server port."; }; docPort = mkOption { type = types.port; default = 4001; description = "Swagger API documentation server port."; }; }; tor.enforce = nbLib.tor.enforce; }; cfg = config.services.rtl; nbLib = config.nix-bitcoin.lib; nbPkgs = config.nix-bitcoin.pkgs; secretsDir = config.nix-bitcoin.secretsDir; node = { isLnd, index }: '' { "index": ${toString index}, "lnNode": "Node", "lnImplementation": "${if isLnd then "LND" else "CLT"}", "Authentication": { ${optionalString (isLnd && cfg.loop) ''"swapMacaroonPath": "${lightning-loop.dataDir}/${bitcoind.network}",'' } "macaroonPath": "${if isLnd then "${cfg.dataDir}/macaroons" else "${cl-rest.dataDir}/certs" }" }, "Settings": { "userPersona": "OPERATOR", "themeMode": "${if cfg.nightTheme then "NIGHT" else "DAY"}", "themeColor": "PURPLE", ${optionalString isLnd ''"channelBackupPath": "${cfg.dataDir}/backup/lnd",'' } "logLevel": "INFO", "fiatConversion": ${if cfg.extraCurrency == null then "false" else "true"}, ${optionalString (cfg.extraCurrency != null) ''"currencyUnit": "${cfg.extraCurrency}",'' } ${optionalString (isLnd && cfg.loop) ''"swapServerUrl": "https://${nbLib.addressWithPort lightning-loop.restAddress lightning-loop.restPort}",'' } "lnServerUrl": "https://${ if isLnd then nbLib.addressWithPort lnd.restAddress lnd.restPort else nbLib.addressWithPort cfg.cl-rest.address cfg.cl-rest.port }" } } ''; nodes' = optional cfg.nodes.clightning (node { isLnd = false; index = 1; }) ++ optional cfg.nodes.lnd (node { isLnd = true; index = 2; }); nodes = if cfg.nodes.reverseOrder then reverseList nodes' else nodes'; configFile = builtins.toFile "config" '' { "multiPass": "@multiPass@", "host": "${cfg.address}", "port": "${toString cfg.port}", "SSO": { "rtlSSO": 0 }, "nodes": [ ${builtins.concatStringsSep ",\n" nodes} ] } ''; cl-rest = { configFile = builtins.toFile "config" '' { "PORT": ${toString cfg.cl-rest.port}, "DOCPORT": ${toString cfg.cl-rest.docPort}, "LNRPCPATH": "${clightning.dataDir}/${bitcoind.makeNetworkName "bitcoin" "regtest"}/lightning-rpc", "PROTOCOL": "https", "EXECMODE": "production", "RPCCOMMANDS": ["*"] } ''; # serviceConfig.StateDirectory dataDir = "/var/lib/cl-rest"; }; inherit (config.services) bitcoind lnd clightning lightning-loop; in { inherit options; config = mkIf cfg.enable { assertions = [ { assertion = cfg.nodes.clightning || cfg.nodes.lnd; message = '' RTL: At least one of `nodes.lnd` or `nodes.clightning` must be `true`. ''; } ]; services.lnd.enable = mkIf cfg.nodes.lnd true; services.lightning-loop.enable = mkIf cfg.loop true; services.clightning.enable = mkIf cfg.nodes.clightning true; systemd.tmpfiles.rules = [ "d '${cfg.dataDir}' 0770 ${cfg.user} ${cfg.group} - -" ]; services.rtl.tor.enforce = mkIf (cfg.extraCurrency != null) false; systemd.services.rtl = rec { wantedBy = [ "multi-user.target" ]; requires = optional cfg.nodes.clightning "cl-rest.service" ++ optional cfg.nodes.lnd "lnd.service"; after = requires; environment.RTL_CONFIG_PATH = cfg.dataDir; serviceConfig = nbLib.defaultHardening // { ExecStartPre = [ (nbLib.script "rtl-setup-config" '' <${configFile} sed "s|@multiPass@|$(cat ${secretsDir}/rtl-password)|" \ > '${cfg.dataDir}/RTL-Config.json' '') ] ++ optional cfg.nodes.lnd (nbLib.rootScript "rtl-copy-macaroon" '' install -D -o ${cfg.user} -g ${cfg.group} ${lnd.networkDir}/admin.macaroon \ '${cfg.dataDir}/macaroons/admin.macaroon' ''); ExecStart = "${nbPkgs.rtl}/bin/rtl"; # Show "rtl" instead of "node" in the journal SyslogIdentifier = "rtl"; User = cfg.user; Restart = "on-failure"; RestartSec = "10s"; ReadWritePaths = cfg.dataDir; } // nbLib.allowedIPAddresses cfg.tor.enforce // nbLib.nodejs; }; systemd.services.cl-rest = mkIf cfg.cl-rest.enable { wantedBy = [ "multi-user.target" ]; requires = [ "clightning.service" ]; after = [ "clightning.service" ]; path = [ pkgs.openssl ]; preStart = '' ln -sfn ${cl-rest.configFile} cl-rest-config.json ''; environment.CL_REST_STATE_DIR = cl-rest.dataDir; serviceConfig = nbLib.defaultHardening // { StateDirectory = "cl-rest"; # cl-rest reads the config file from the working directory WorkingDirectory = cl-rest.dataDir; ExecStart = "${nbPkgs.cl-rest}/bin/cl-rest"; # Show "cl-rest" instead of "node" in the journal SyslogIdentifier = "cl-rest"; User = cfg.user; Restart = "on-failure"; RestartSec = "10s"; } // nbLib.allowLocalIPAddresses // nbLib.nodejs; }; users.users.${cfg.user} = { isSystemUser = true; group = cfg.group; extraGroups = # Enable clightning RPC access for cl-rest optional cfg.cl-rest.enable clightning.group ++ optional cfg.loop lnd.group; }; users.groups.${cfg.group} = {}; nix-bitcoin.secrets.rtl-password.user = cfg.user; nix-bitcoin.generateSecretsCmds.rtl = '' makePasswordSecret rtl-password ''; }; }