Align Wallet Connections with proven Alby Hub API contract

This commit is contained in:
copilot-swe-agent[bot]
2026-07-27 03:41:41 +00:00
committed by GitHub
parent f24be16b98
commit 82ee21f13f
6 changed files with 495 additions and 317 deletions
+97 -164
View File
@@ -1,223 +1,156 @@
{ config, pkgs, lib, ... }:
# ── Alby Hub version pin ───────────────────────────────────────────────────────
# Pinned to getalby/hub release v1.14.2 (2024-11-15).
# Update `rev` and `sha256` together when upgrading. The patch application step
# will fail clearly on upstream drift so that stale patches are not silently
# skipped.
let
albyhubVersion = "1.14.2";
albyhubSrc = pkgs.fetchFromGitHub {
owner = "getAlby";
repo = "hub";
rev = "v${albyhubVersion}";
sha256 = "sha256-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=";
};
patchedAlbyHub = pkgs.albyhub.overrideAttrs (old: {
patches = (old.patches or []) ++ [
../packages/albyhub/0001-private-route-hints.patch
../packages/albyhub/0002-isolated-invoice-app-id.patch
];
});
# Patch 1 — private route hints for regular invoices.
# Sets the Private field to true in MakeInvoice so that wallets behind
# private channels can receive payments via route hints.
# Context lines must match getalby/hub v1.14.2 exactly; patch fails on drift.
patchPrivateRouteHints = pkgs.writeText "0001-lnd-private-route-hints.patch" ''
--- a/lnclient/lnd/lnd.go
+++ b/lnclient/lnd/lnd.go
@@ -1,5 +1,6 @@
invoice := &lnrpc.Invoice{
Memo: description,
Value: amountSat,
+ Private: true,
Expiry: expiry,
}
lndRpcAddress = lib.attrByPath [ "services" "lnd" "rpcAddress" ] "127.0.0.1" config;
lndRpcPort = toString (lib.attrByPath [ "services" "lnd" "rpcPort" ] 10009 config);
lndCertPath = lib.attrByPath [ "services" "lnd" "certPath" ] "/var/lib/lnd/tls.cert" config;
albyhubWrapper = pkgs.writeShellScript "albyhub-wrapper" ''
set -euo pipefail
password_file="/var/lib/albyhub/unlock-password"
if [ ! -s "$password_file" ]; then
umask 077
${pkgs.openssl}/bin/openssl rand -hex 32 > "$password_file"
fi
export AUTO_UNLOCK_PASSWORD="$(cat "$password_file")"
exec ${patchedAlbyHub}/bin/hub
'';
# Patch 2 — optional isolated app attribution for invoice creation.
# Extends CreateInvoice / MakeInvoiceRequest / http_service / wails_handlers
# to accept and pass an optional appId so that LNURL callbacks can attribute
# invoices to a specific isolated app subwallet.
# Context lines must match getalby/hub v1.14.2 exactly; patch fails on drift.
patchAppIdAttribution = pkgs.writeText "0002-invoice-app-attribution.patch" ''
--- a/api/models.go
+++ b/api/models.go
@@ -1,5 +1,6 @@
type MakeInvoiceRequest struct {
Amount int64 `json:"amount"`
Description string `json:"description"`
DescriptionHash string `json:"descriptionHash"`
Expiry *int64 `json:"expiry"`
+ AppId *uint `json:"appId"`
}
'';
albyhub = pkgs.buildGoModule {
pname = "albyhub";
version = albyhubVersion;
src = albyhubSrc;
# go.sum-derived vendor hash — regenerate after any Go dependency change
vendorHash = "sha256-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=";
patches = [ patchPrivateRouteHints patchAppIdAttribution ];
# Run gofmt on modified Go sources after patching
postPatch = ''
gofmt -w lnclient/lnd/lnd.go api/models.go api/transactions.go \
http/http_service.go wails/wails_handlers.go
'';
meta = {
description = "Alby Hub self-hosted NWC wallet server (Sovran_SystemsOS build)";
license = lib.licenses.gpl3;
mainProgram = "hub";
};
};
in
lib.mkIf config.sovran_systemsOS.features."nwc-wallets" {
assertions = [
{
assertion = config.services.lnd.enable;
message = "Wallet Connections requires services.lnd.enable = true.";
message = "Wallet Connections requires services.lnd.enable = true.";
}
{
assertion = !(lib.attrByPath [ "nix-bitcoin" "netns-isolation" "enable" ] false config);
message = "Wallet Connections requires nix-bitcoin.netns-isolation.enable = false.";
}
];
# ── Users and groups ─────────────────────────────────────────────
users.groups.albyhub = {};
users.groups.albyhub = { };
users.users.albyhub = {
isSystemUser = true;
group = "albyhub";
home = "/var/lib/albyhub";
createHome = false;
extraGroups = [];
group = "albyhub";
home = "/var/lib/albyhub";
createHome = false;
extraGroups = [ ];
};
users.groups.nwc-lnurl = {};
users.groups.nwc-lnurl = { };
users.users.nwc-lnurl = {
isSystemUser = true;
group = "nwc-lnurl";
home = "/var/lib/nwc-lnurl";
createHome = false;
extraGroups = [ "albyhub" ]; # needs to read /var/lib/albyhub/unlock-password
group = "nwc-lnurl";
home = "/var/lib/nwc-lnurl";
createHome = false;
extraGroups = [ "albyhub" ];
};
# ── State directories ────────────────────────────────────────────
systemd.tmpfiles.rules = [
"d /var/lib/albyhub 0700 albyhub albyhub -"
"d /var/lib/nwc-lnurl 0750 nwc-lnurl nwc-lnurl -"
];
# ── Restricted LND macaroon for Alby Hub ────────────────────────
services.lnd.macaroons.albyhub = {
user = "albyhub";
permissions = ''
{"entity":"info","action":"read"},
{"entity":"offchain","action":"read"},
{"entity":"offchain","action":"write"},
{"entity":"invoices","action":"read"},
{"entity":"invoices","action":"write"},
{"entity":"onchain","action":"read"},
{"entity":"address","action":"read"},
{"entity":"message","action":"read"},
{"entity":"message","action":"write"}
'';
permissions = lib.concatStringsSep "," [
''{"entity":"info","action":"read"}''
''{"entity":"offchain","action":"read"}''
''{"entity":"offchain","action":"write"}''
''{"entity":"invoices","action":"read"}''
''{"entity":"invoices","action":"write"}''
''{"entity":"onchain","action":"read"}''
''{"entity":"address","action":"read"}''
''{"entity":"message","action":"read"}''
''{"entity":"message","action":"write"}''
];
};
# ── Alby Hub unlock-password (generated once) ────────────────────
systemd.services.albyhub-init = {
description = "Initialise Alby Hub state directory and unlock password";
wantedBy = [ "multi-user.target" ];
before = [ "albyhub.service" ];
serviceConfig = {
Type = "oneshot";
RemainAfterExit = true;
User = "root";
UMask = "0077";
};
script = ''
install -d -m 0700 -o albyhub -g albyhub /var/lib/albyhub
if [ ! -f /var/lib/albyhub/unlock-password ]; then
${pkgs.openssl}/bin/openssl rand -hex 32 > /var/lib/albyhub/unlock-password
chown albyhub:albyhub /var/lib/albyhub/unlock-password
chmod 0600 /var/lib/albyhub/unlock-password
fi
'';
};
# ── Alby Hub service ─────────────────────────────────────────────
systemd.services.albyhub = {
description = "Alby Hub NWC wallet server";
wantedBy = [ "multi-user.target" ];
after = [
"network.target"
"lnd.service"
"albyhub-init.service"
];
requires = [ "lnd.service" "albyhub-init.service" ];
wantedBy = [ "multi-user.target" ];
after = [ "network.target" "lnd.service" ];
requires = [ "lnd.service" ];
environment = {
WORK_DIR = "/var/lib/albyhub";
PORT = "8080";
LDK_NETWORK = "bitcoin";
LOG_TO_FILE = "false";
AUTO_UNLOCK_PASSWORD_FILE = "/var/lib/albyhub/unlock-password";
ALBY_ACCOUNT_AUTOLINK = "false";
ALBY_DISABLE_EVENTS = "true";
ENABLE_SECURE_COOKIE = "false";
ALBY_HUB_HIDE_VERSION_BANNER = "true";
HOME = "/var/lib/albyhub";
HOST = "127.0.0.1";
LN_BACKEND_TYPE = "LND";
ENABLE_ADVANCED_SETUP = "false";
LND_ADDRESS = "${lndRpcAddress}:${lndRpcPort}";
LND_CERT_FILE = lndCertPath;
LND_MACAROON_FILE = "/run/lnd/albyhub.macaroon";
WORK_DIR = "/var/lib/albyhub";
DATABASE_URI = "/var/lib/albyhub/nwc.db";
PORT = "8080";
RELAY = "wss://relay.getalby.com,wss://relay2.getalby.com";
AUTO_LINK_ALBY_ACCOUNT = "false";
SEND_EVENTS_TO_ALBY = "false";
LOG_TO_FILE = "false";
HIDE_UPDATE_BANNER = "true";
};
serviceConfig = {
Type = "simple";
User = "albyhub";
Group = "albyhub";
Type = "simple";
User = "albyhub";
Group = "albyhub";
WorkingDirectory = "/var/lib/albyhub";
ExecStart = "${albyhub}/bin/hub";
Restart = "on-failure";
RestartSec = "10s";
UMask = "0027";
ExecStart = albyhubWrapper;
Restart = "on-failure";
RestartSec = "10s";
UMask = "0077";
NoNewPrivileges = true;
PrivateTmp = true;
ProtectHome = true;
ProtectSystem = "strict";
ReadWritePaths = [ "/var/lib/albyhub" ];
ReadOnlyPaths = [
config.services.lnd.certFile or "/var/lib/lnd/tls.cert"
"/run/lnd"
];
PrivateTmp = true;
ProtectHome = true;
ProtectSystem = "strict";
ReadWritePaths = [ "/var/lib/albyhub" ];
ReadOnlyPaths = [ lndCertPath "/run/lnd" ];
};
};
# ── Dedicated LNURL service ──────────────────────────────────────
systemd.services.nwc-lnurl = {
description = "Wallet Connections public LNURL service";
wantedBy = [ "multi-user.target" ];
after = [ "albyhub.service" "sovran-hub-web.service" ];
wants = [ "albyhub.service" ];
wantedBy = [ "multi-user.target" ];
after = [ "albyhub.service" "sovran-hub-web.service" ];
wants = [ "albyhub.service" ];
serviceConfig = {
Type = "simple";
User = "nwc-lnurl";
Group = "nwc-lnurl";
Type = "simple";
User = "nwc-lnurl";
Group = "nwc-lnurl";
ExecStart = "${config.services.sovranHub.webPackage}/bin/nwc-lnurl";
Restart = "on-failure";
RestartSec = "10s";
UMask = "0027";
Restart = "on-failure";
RestartSec = "10s";
UMask = "0027";
NoNewPrivileges = true;
PrivateTmp = true;
ProtectHome = true;
ProtectSystem = "strict";
ReadOnlyPaths = [
PrivateTmp = true;
ProtectHome = true;
ProtectSystem = "strict";
ReadOnlyPaths = [
"/var/lib/domains/lightning"
"/var/lib/albyhub/unlock-password"
];
};
};
# ── Domain requirement ───────────────────────────────────────────
systemd.services.sovran-hub-web.environment = {
NWC_LND_ADDRESS = "${lndRpcAddress}:${lndRpcPort}";
NWC_LND_CERT_FILE = lndCertPath;
NWC_LND_MACAROON_FILE = "/run/lnd/albyhub.macaroon";
};
sovran_systemsOS.domainRequirements = [
{
name = "lightning";
label = "Lightning Address Domain";
example = "pay.yourdomain.com";
name = "lightning";
label = "Lightning Address Domain";
example = "pay.yourdomain.com";
needsDDNS = true;
}
];