diff --git a/modules/lightning-pub.nix b/modules/lightning-pub.nix new file mode 100644 index 0000000..dc26cbc --- /dev/null +++ b/modules/lightning-pub.nix @@ -0,0 +1,224 @@ +{ config, lib, pkgs, ... }: + +with lib; + +let + options.services.lightning-pub = { + enable = mkEnableOption "Lightning.Pub, a Lightning Network account system running on top of LND"; + + port = mkOption { + type = types.port; + default = 1776; + description = "Port for the Lightning.Pub REST API."; + }; + + dataDir = mkOption { + type = types.path; + default = "/var/lib/lightning-pub"; + description = "Data directory for Lightning.Pub."; + }; + + user = mkOption { + type = types.str; + default = "lightning-pub"; + description = "User to run Lightning.Pub as."; + }; + + group = mkOption { + type = types.str; + default = cfg.user; + description = "Group to run Lightning.Pub as."; + }; + + source = { + url = mkOption { + type = types.str; + default = "https://git.atitlan.io/aiolabs/lightning-pub"; + description = "Git repository URL for Lightning.Pub source."; + }; + + ref = mkOption { + type = types.str; + default = "master"; + description = "Git ref (branch, tag, or commit) to checkout."; + }; + }; + + logLevel = mkOption { + type = types.enum [ "DEBUG" "INFO" "WARN" "ERROR" ]; + default = "INFO"; + description = "Logging level for Lightning.Pub."; + }; + + nostrRelays = mkOption { + type = types.str; + default = "wss://relay.lightning.pub"; + description = "Space-separated list of Nostr relay WebSocket URLs."; + }; + + serviceFee = { + bps = mkOption { + type = types.int; + default = 60; + description = "Service fee in basis points."; + }; + + floorSats = mkOption { + type = types.int; + default = 10; + description = "Minimum service fee floor in satoshis."; + }; + }; + + watchdogMaxDiffSats = mkOption { + type = types.int; + default = 0; + description = "Maximum allowed balance difference in satoshis for the watchdog (0 = disabled)."; + }; + + extraEnv = mkOption { + type = with types; attrsOf str; + default = {}; + description = "Extra environment variables to pass to Lightning.Pub."; + }; + }; + + cfg = config.services.lightning-pub; + nbLib = config.nix-bitcoin.lib; + lnd = config.services.lnd; + + lightningPubEnv = pkgs.writeShellScript "lightning-pub-env" '' + set -euo pipefail + export PATH=${pkgs.nodejs_22}/bin:$PATH + cd ${cfg.dataDir}/source + exec "$@" + ''; + +in { + inherit options; + + config = mkIf cfg.enable { + services.lnd.enable = true; + + users.users.${cfg.user} = { + isSystemUser = true; + group = cfg.group; + home = cfg.dataDir; + extraGroups = [ lnd.group ]; + }; + users.groups.${cfg.group} = {}; + + nix-bitcoin.operator = { + groups = [ cfg.group ]; + allowRunAsUsers = [ cfg.user ]; + }; + + systemd.tmpfiles.rules = [ + "d '${cfg.dataDir}' 0770 ${cfg.user} ${cfg.group} - -" + ]; + + # Build service (oneshot) — clones repo, installs deps, compiles TypeScript + systemd.services.lightning-pub-build = { + description = "Clone and Build Lightning.Pub"; + wantedBy = [ "multi-user.target" ]; + after = [ "network-online.target" ]; + wants = [ "network-online.target" ]; + + path = with pkgs; [ + nodejs_22 python3 git coreutils bash + stdenv.cc gnumake pkg-config + ]; + + environment = { + HOME = cfg.dataDir; + npm_config_cache = "${cfg.dataDir}/.npm-cache"; + }; + + serviceConfig = { + Type = "oneshot"; + RemainAfterExit = true; + User = cfg.user; + Group = cfg.group; + TimeoutStartSec = "30min"; + ProtectSystem = "strict"; + ProtectHome = true; + NoNewPrivileges = true; + ReadWritePaths = [ cfg.dataDir ]; + PrivateTmp = true; + }; + + script = '' + set -euo pipefail + SOURCE_DIR="${cfg.dataDir}/source" + + # Clone or update + if [ ! -d "$SOURCE_DIR/.git" ]; then + git clone "${cfg.source.url}" "$SOURCE_DIR" + cd "$SOURCE_DIR" + git checkout "${cfg.source.ref}" + NEEDS_BUILD=1 + else + cd "$SOURCE_DIR" + git fetch origin + LOCAL=$(git rev-parse HEAD) + REMOTE=$(git rev-parse "origin/${cfg.source.ref}" 2>/dev/null || git rev-parse "${cfg.source.ref}") + if [ "$LOCAL" != "$REMOTE" ]; then + git checkout "${cfg.source.ref}" + git pull origin "${cfg.source.ref}" 2>/dev/null || true + NEEDS_BUILD=1 + else + NEEDS_BUILD=0 + fi + fi + + # Check build artifacts + [ ! -d "node_modules" ] || [ ! -d "build" ] && NEEDS_BUILD=1 + + [ "$NEEDS_BUILD" = "0" ] && exit 0 + + npm install + npx tsc + ''; + }; + + # Main runtime service + systemd.services.lightning-pub = rec { + description = "Lightning.Pub"; + wantedBy = [ "multi-user.target" ]; + requires = [ "lnd.service" "lightning-pub-build.service" ]; + after = requires ++ [ "nix-bitcoin-secrets.target" ]; + + environment = { + PORT = toString cfg.port; + DATA_DIR = cfg.dataDir; + LOG_LEVEL = cfg.logLevel; + NOSTR_RELAYS = cfg.nostrRelays; + SERVICE_FEE_BPS = toString cfg.serviceFee.bps; + SERVICE_FEE_FLOOR_SATS = toString cfg.serviceFee.floorSats; + WATCHDOG_MAX_DIFF_SATS = toString cfg.watchdogMaxDiffSats; + LND_ADDRESS = "${lnd.rpcAddress}:${toString lnd.rpcPort}"; + LND_CERT_PATH = lnd.certPath; + LND_MACAROON_PATH = "${cfg.dataDir}/admin.macaroon"; + } // cfg.extraEnv; + + serviceConfig = nbLib.defaultHardening // { + # Copy the admin macaroon (only readable by lnd user, not group) + ExecStartPre = [ + (nbLib.rootScript "lightning-pub-copy-macaroon" '' + install --compare -m 640 -o ${cfg.user} -g ${cfg.group} \ + ${lnd.networkDir}/admin.macaroon '${cfg.dataDir}/admin.macaroon' + '') + ]; + ExecStart = "${lightningPubEnv} ${pkgs.nodejs_22}/bin/node build/src/index.js"; + SyslogIdentifier = "lightning-pub"; + User = cfg.user; + Restart = "on-failure"; + RestartSec = "10s"; + ReadWritePaths = [ cfg.dataDir ]; + } // nbLib.allowAllIPAddresses + // nbLib.nodejs; + }; + + networking.firewall.allowedTCPPorts = [ cfg.port ]; + }; +} diff --git a/modules/modules.nix b/modules/modules.nix index 1759a1b..192f4ed 100644 --- a/modules/modules.nix +++ b/modules/modules.nix @@ -29,6 +29,7 @@ ./joinmarket-ob-watcher.nix ./hardware-wallets.nix ./lamassu-lnbits.nix + ./lightning-pub.nix # Support features ./versioning.nix