Skip to content

Commit 1282471

Browse files
committed
albyhub: add module
1 parent ac1344f commit 1282471

File tree

4 files changed

+376
-0
lines changed

4 files changed

+376
-0
lines changed

modules/albyhub.nix

Lines changed: 372 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,372 @@
1+
{ config, lib, pkgs, ... }:
2+
3+
with lib;
4+
let
5+
cfg = config.services.albyhub;
6+
nbLib = config.nix-bitcoin.lib;
7+
secretsDir = config.nix-bitcoin.secretsDir;
8+
bitcoind = config.services.bitcoind;
9+
lnd = config.services.lnd;
10+
11+
boolToString = b: if b then "true" else "false";
12+
13+
envFileContent =
14+
let
15+
backendOpts =
16+
if cfg.lnBackend == "lnd" then ''
17+
LN_BACKEND_TYPE=LND
18+
LND_ADDRESS=${lnd.rpcAddress}:${toString lnd.rpcPort}
19+
LND_CERT_FILE=${lnd.certPath}
20+
LND_MACAROON_FILE=${cfg.dataDir}/admin.macaroon
21+
'' else if cfg.lnBackend == "ldk" then ''
22+
LN_BACKEND_TYPE=LDK
23+
${optionalString (cfg.ldk.network != null) "LDK_NETWORK=${cfg.ldk.network}"}
24+
${optionalString (cfg.ldk.esploraServer != null) "LDK_ESPLORA_SERVER=${cfg.ldk.esploraServer}"}
25+
${optionalString (cfg.ldk.gossipSource != null) "LDK_GOSSIP_SOURCE=${cfg.ldk.gossipSource}"}
26+
${optionalString (cfg.ldk.logLevel != null) "LDK_LOG_LEVEL=${toString cfg.ldk.logLevel}"}
27+
${optionalString (cfg.ldk.vssUrl != null) "LDK_VSS_URL=${cfg.ldk.vssUrl}"}
28+
${optionalString (cfg.ldk.listeningAddresses != null) "LDK_LISTENING_ADDRESSES=${cfg.ldk.listeningAddresses}"}
29+
${optionalString (cfg.ldk.transientNetworkGraph != null) "LDK_TRANSIENT_NETWORK_GRAPH=${boolToString cfg.ldk.transientNetworkGraph}"}
30+
'' else if cfg.lnBackend == "phoenix" then ''
31+
LN_BACKEND_TYPE=PHOENIX
32+
${optionalString (cfg.phoenix.address != null) "PHOENIXD_ADDRESS=${cfg.phoenix.address}"}
33+
'' else "";
34+
in
35+
''
36+
WORK_DIR=${cfg.dataDir}
37+
DATABASE_URI=${cfg.dataDir}/nwc.db
38+
PORT=${toString cfg.port}
39+
LOG_LEVEL=${toString cfg.logLevel}
40+
AUTO_LINK_ALBY_ACCOUNT=${boolToString cfg.autoLinkAlbyAccount}
41+
${optionalString (cfg.relay != null) "RELAY=${cfg.relay}"}
42+
${optionalString (cfg.logToFile != null) "LOG_TO_FILE=${boolToString cfg.logToFile}"}
43+
${optionalString (cfg.logDBQueries != null) "LOG_DB_QUERIES=${boolToString cfg.logDBQueries}"}
44+
${optionalString (cfg.network != null) "NETWORK=${cfg.network}"}
45+
${optionalString (cfg.mempoolApi != null) "MEMPOOL_API=${cfg.mempoolApi}"}
46+
${optionalString (cfg.albyOAuth.clientId != null) "ALBY_OAUTH_CLIENT_ID=${cfg.albyOAuth.clientId}"}
47+
${optionalString (cfg.baseUrl != null) "BASE_URL=${cfg.baseUrl}"}
48+
${optionalString (cfg.frontendUrl != null) "FRONTEND_URL=${cfg.frontendUrl}"}
49+
${optionalString (cfg.logEvents != null) "LOG_EVENTS=${boolToString cfg.logEvents}"}
50+
${optionalString (cfg.enableAdvancedSetup != null) "ENABLE_ADVANCED_SETUP=${boolToString cfg.enableAdvancedSetup}"}
51+
${optionalString (cfg.boltzApi != null) "BOLTZ_API=${cfg.boltzApi}"}
52+
${backendOpts}
53+
${optionalString (cfg.extraConfig != null) cfg.extraConfig}
54+
'';
55+
56+
# Persisted env file that contains secrets
57+
envFile = "${cfg.dataDir}/.env";
58+
59+
in
60+
{
61+
options.services.albyhub = {
62+
enable = mkOption {
63+
type = types.bool;
64+
default = false;
65+
description = ''
66+
Alby Hub, a service to control lightning wallets over nostr.
67+
See the user guide at https://guides.getalby.com/user-guide/alby-hub.
68+
'';
69+
};
70+
71+
package = mkOption {
72+
type = types.package;
73+
default = config.nix-bitcoin.pkgs.albyhub;
74+
defaultText = "config.nix-bitcoin.pkgs.albyhub";
75+
description = "The albyhub package to use.";
76+
};
77+
78+
user = mkOption {
79+
type = types.str;
80+
default = "albyhub";
81+
description = "The user as which to run Alby Hub.";
82+
};
83+
84+
group = mkOption {
85+
type = types.str;
86+
default = cfg.user;
87+
description = "The group as which to run Alby Hub.";
88+
};
89+
90+
dataDir = mkOption {
91+
type = types.path;
92+
default = "/var/lib/albyhub";
93+
description = "The data directory for Alby Hub.";
94+
};
95+
96+
address = mkOption {
97+
type = types.str;
98+
default = "127.0.0.1";
99+
description = "This option does nothing. It is only present only to satisfy the onion service module requirements.";
100+
};
101+
102+
port = mkOption {
103+
type = types.port;
104+
default = 8082;
105+
description = "The port for Alby Hub to listen on.";
106+
};
107+
108+
relay = mkOption {
109+
type = with types; nullOr str;
110+
default = null;
111+
description = "The default nostr relay.";
112+
example = "wss://relay.getalby.com/v1";
113+
};
114+
115+
logLevel = mkOption {
116+
type = types.int;
117+
default = 4;
118+
description = "Log level for the application. Higher is more verbose.";
119+
};
120+
121+
logToFile = mkOption {
122+
type = with types; nullOr bool;
123+
default = null;
124+
description = "Log to file.";
125+
};
126+
127+
logDBQueries = mkOption {
128+
type = with types; nullOr bool;
129+
default = null;
130+
description = "Log database queries.";
131+
};
132+
133+
network = mkOption {
134+
type = with types; nullOr str;
135+
default = null;
136+
description = "The network to use (e.g. mainnet, testnet, regtest).";
137+
example = "signet";
138+
};
139+
140+
mempoolApi = mkOption {
141+
type = with types; nullOr str;
142+
default = if config.services.mempool.enable then "http://${nbLib.addressWithPort config.services.mempool.address config.services.mempool.port}" else null;
143+
description = "Mempool API endpoint.";
144+
example = "https://mempool.space/api";
145+
};
146+
147+
baseUrl = mkOption {
148+
type = with types; nullOr str;
149+
default = null;
150+
description = "Base URL for the Alby Hub. Required if you want to connect to your Alby account.";
151+
example = "http://localhost:8082";
152+
};
153+
154+
frontendUrl = mkOption {
155+
type = with types; nullOr str;
156+
default = null;
157+
description = "Frontend URL for the Alby Hub";
158+
example = "http://127.0.0.1:8082";
159+
};
160+
161+
logEvents = mkOption {
162+
type = with types; nullOr bool;
163+
default = null;
164+
description = "Log events.";
165+
};
166+
167+
autoLinkAlbyAccount = mkOption {
168+
type = types.bool;
169+
default = false;
170+
description = "Auto link Alby account.";
171+
};
172+
173+
enableAdvancedSetup = mkOption {
174+
type = with types; nullOr bool;
175+
default = null;
176+
description = "Enable advanced setup.";
177+
};
178+
179+
boltzApi = mkOption {
180+
type = with types; nullOr str;
181+
default = null;
182+
description = "Boltz API endpoint.";
183+
};
184+
185+
extraConfig = mkOption {
186+
type = with types; nullOr str;
187+
default = null;
188+
description = "Extra configuration options appended to the environment file.";
189+
};
190+
191+
autoUnlockPasswordFile = mkOption {
192+
type = with types; nullOr path;
193+
default = null;
194+
description = ''
195+
Path to a file containing the password to auto-unlock Alby Hub on startup.
196+
Password will still be required to access the interface.
197+
Setting this option is insecure. The password file will be world-readable
198+
in the Nix store.
199+
If not set, a password will be automatically generated.
200+
'';
201+
};
202+
203+
jwtSecretFile = mkOption {
204+
type = with types; nullOr path;
205+
default = null;
206+
description = ''
207+
Path to a file containing the JWT secret.
208+
Setting this option is insecure. The secret file will be world-readable
209+
in the Nix store.
210+
'';
211+
};
212+
213+
lnBackend = mkOption {
214+
type = with types; nullOr (enum [ "lnd" "ldk" "phoenix" ]);
215+
default = null;
216+
description = ''
217+
The lightning backend to use.
218+
- `lnd`: Use the LND backend.
219+
- `ldk`: Use the built-in LDK backend.
220+
- `phoenix`: Use a phoenixd backend.
221+
'';
222+
};
223+
224+
ldk = {
225+
network = mkOption {
226+
type = with types; nullOr str;
227+
default = null;
228+
description = "The LDK network to use.";
229+
};
230+
esploraServer = mkOption {
231+
type = with types; nullOr str;
232+
default = null;
233+
description = "LDK Esplora Server URL.";
234+
};
235+
gossipSource = mkOption {
236+
type = with types; nullOr str;
237+
default = null;
238+
description = "LDK Gossip Source.";
239+
};
240+
vssUrl = mkOption {
241+
type = with types; nullOr str;
242+
default = null;
243+
description = "LDK VSS URL.";
244+
};
245+
listeningAddresses = mkOption {
246+
type = with types; nullOr str;
247+
default = null;
248+
description = "LDK Listening Addresses.";
249+
};
250+
transientNetworkGraph = mkOption {
251+
type = with types; nullOr bool;
252+
default = null;
253+
description = "LDK Transient Network Graph.";
254+
};
255+
logLevel = mkOption {
256+
type = with types; nullOr int;
257+
default = null;
258+
description = "LDK debug log level to get more info.";
259+
};
260+
};
261+
262+
albyOAuth = {
263+
clientId = mkOption {
264+
type = with types; nullOr str;
265+
default = null;
266+
description = "Alby OAuth Client ID.";
267+
};
268+
clientSecretFile = mkOption {
269+
type = with types; nullOr path;
270+
default = null;
271+
description = ''
272+
Path to a file containing the Alby OAuth client secret.
273+
'';
274+
};
275+
};
276+
277+
phoenix = {
278+
address = mkOption {
279+
type = with types; nullOr str;
280+
default = null;
281+
description = "Phoenixd address.";
282+
};
283+
authorizationFile = mkOption {
284+
type = with types; nullOr path;
285+
default = null;
286+
description = "Path to a file containing the Phoenixd authorization.";
287+
};
288+
};
289+
290+
tor = nbLib.tor;
291+
292+
getPublicAddressCmd = mkOption {
293+
type = types.str;
294+
default = "";
295+
description = ''
296+
Bash expression which outputs the public service address.
297+
If left empty, no address is announced.
298+
'';
299+
};
300+
};
301+
302+
config = mkIf cfg.enable {
303+
services.lnd.enable = mkIf (cfg.lnBackend == "lnd") true;
304+
305+
users.users.${cfg.user} = {
306+
isSystemUser = true;
307+
group = cfg.group;
308+
};
309+
users.groups.${cfg.group} = {};
310+
311+
systemd.tmpfiles.rules = [
312+
"d '${cfg.dataDir}' 0770 ${cfg.user} ${cfg.group} - -"
313+
];
314+
315+
systemd.services.albyhub = {
316+
description = mkDefault "Alby Hub";
317+
wantedBy = [ "multi-user.target" ];
318+
after = [ "network.target" ] ++ optional (cfg.lnBackend == "lnd") "lnd.service" ++ optional config.services.mempool.enable "mempool.service";
319+
320+
serviceConfig = nbLib.defaultHardening // {
321+
ExecStartPre =
322+
let
323+
catSecret = secret: optionalString (secret != null) "cat ${secret}";
324+
appendToFile = key: secret: optionalString (secret != null) ''
325+
echo -n '${key}=' >> ${envFile}
326+
${catSecret secret} >> ${envFile}
327+
echo >> ${envFile}
328+
'';
329+
in
330+
[
331+
(nbLib.rootScript "albyhub-setup" ''
332+
${optionalString (cfg.lnBackend == "lnd") ''
333+
install -m640 -o ${cfg.user} -g ${cfg.group} -D ${lnd.networkDir}/admin.macaroon ${cfg.dataDir}/admin.macaroon
334+
''}
335+
# Create env file without secrets
336+
cat > ${envFile} <<EOF
337+
${envFileContent}
338+
EOF
339+
# Append secrets
340+
${appendToFile "AUTO_UNLOCK_PASSWORD" (if cfg.autoUnlockPasswordFile != null then cfg.autoUnlockPasswordFile else "${secretsDir}/albyhub-auto-unlock-password")}
341+
${optionalString (cfg.jwtSecretFile != null) (appendToFile "JWT_SECRET" cfg.jwtSecretFile)}
342+
${optionalString (cfg.albyOAuth.clientSecretFile != null) (appendToFile "ALBY_OAUTH_CLIENT_SECRET" cfg.albyOAuth.clientSecretFile)}
343+
${optionalString (cfg.phoenix.authorizationFile != null) (appendToFile "PHOENIXD_AUTHORIZATION" cfg.phoenix.authorizationFile)}
344+
345+
chown -R ${cfg.user}:${cfg.group} ${cfg.dataDir}
346+
chmod 600 ${envFile}
347+
'')
348+
];
349+
350+
User = cfg.user;
351+
Group = cfg.group;
352+
ExecStart = "${cfg.package}/bin/albyhub";
353+
EnvironmentFile = "-${envFile}";
354+
WorkingDirectory = cfg.dataDir;
355+
Restart = "on-failure";
356+
RestartSec = "10s";
357+
ReadWritePaths = [ cfg.dataDir ];
358+
} // nbLib.allowedIPAddresses cfg.tor.enforce;
359+
};
360+
361+
nix-bitcoin.secrets = {
362+
albyhub-auto-unlock-password = {
363+
user = cfg.user;
364+
permissions = "400";
365+
};
366+
};
367+
368+
nix-bitcoin.generateSecretsCmds.albyhub = ''
369+
makePasswordSecret albyhub-auto-unlock-password
370+
'';
371+
};
372+
}

modules/nodeinfo.nix

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,7 @@ in {
158158
inherit name cfg;
159159
systemdServiceName = "nginx";
160160
};
161+
albyhub = mkInfo "";
161162
# Only add sshd when it has an onion service
162163
sshd = name: cfg: mkIfOnionPort "sshd" (onionPort: ''
163164
add_service("sshd", """info["onion_address"] = get_onion_address("sshd", ${onionPort})""")

modules/presets/enable-tor.nix

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ in {
2828
# btcpayserver = defaultEnableTorProxy;
2929
lightning-pool = defaultEnableTorProxy;
3030
mempool = defaultEnableTorProxy;
31+
albyhub = defaultEnableTorProxy;
3132

3233
# These services don't make outgoing connections
3334
# (or use Tor by default in case of joinmarket)
@@ -50,5 +51,6 @@ in {
5051
fulcrum.enable = defaultTrue;
5152
joinmarket-ob-watcher.enable = defaultTrue;
5253
rtl.enable = defaultTrue;
54+
albyhub.enable = defaultTrue;
5355
};
5456
}

0 commit comments

Comments
 (0)