1{
2 config,
3 lib,
4 pkgs,
5 ...
6}:
7let
8
9 eachBlockbook = config.services.blockbook-frontend;
10
11 blockbookOpts =
12 {
13 config,
14 lib,
15 name,
16 ...
17 }:
18 {
19
20 options = {
21
22 enable = lib.mkEnableOption "blockbook-frontend application";
23
24 package = lib.mkPackageOption pkgs "blockbook" { };
25
26 user = lib.mkOption {
27 type = lib.types.str;
28 default = "blockbook-frontend-${name}";
29 description = "The user as which to run blockbook-frontend-${name}.";
30 };
31
32 group = lib.mkOption {
33 type = lib.types.str;
34 default = "${config.user}";
35 description = "The group as which to run blockbook-frontend-${name}.";
36 };
37
38 certFile = lib.mkOption {
39 type = lib.types.nullOr lib.types.path;
40 default = null;
41 example = "/etc/secrets/blockbook-frontend-${name}/certFile";
42 description = ''
43 To enable SSL, specify path to the name of certificate files without extension.
44 Expecting {file}`certFile.crt` and {file}`certFile.key`.
45 '';
46 };
47
48 configFile = lib.mkOption {
49 type = with lib.types; nullOr path;
50 default = null;
51 example = "${config.dataDir}/config.json";
52 description = "Location of the blockbook configuration file.";
53 };
54
55 coinName = lib.mkOption {
56 type = lib.types.str;
57 default = "Bitcoin";
58 description = ''
59 See <https://github.com/trezor/blockbook/blob/master/bchain/coins/blockchain.go#L61>
60 for current of coins supported in master (Note: may differ from release).
61 '';
62 };
63
64 cssDir = lib.mkOption {
65 type = lib.types.path;
66 default = "${config.package}/share/css/";
67 defaultText = lib.literalExpression ''"''${package}/share/css/"'';
68 example = lib.literalExpression ''"''${dataDir}/static/css/"'';
69 description = ''
70 Location of the dir with {file}`main.css` CSS file.
71 By default, the one shipped with the package is used.
72 '';
73 };
74
75 dataDir = lib.mkOption {
76 type = lib.types.path;
77 default = "/var/lib/blockbook-frontend-${name}";
78 description = "Location of blockbook-frontend-${name} data directory.";
79 };
80
81 debug = lib.mkOption {
82 type = lib.types.bool;
83 default = false;
84 description = "Debug mode, return more verbose errors, reload templates on each request.";
85 };
86
87 internal = lib.mkOption {
88 type = lib.types.nullOr lib.types.str;
89 default = ":9030";
90 description = "Internal http server binding `[address]:port`.";
91 };
92
93 messageQueueBinding = lib.mkOption {
94 type = lib.types.str;
95 default = "tcp://127.0.0.1:38330";
96 description = "Message Queue Binding `address:port`.";
97 };
98
99 public = lib.mkOption {
100 type = lib.types.nullOr lib.types.str;
101 default = ":9130";
102 description = "Public http server binding `[address]:port`.";
103 };
104
105 rpc = {
106 url = lib.mkOption {
107 type = lib.types.str;
108 default = "http://127.0.0.1";
109 description = "URL for JSON-RPC connections.";
110 };
111
112 port = lib.mkOption {
113 type = lib.types.port;
114 default = 8030;
115 description = "Port for JSON-RPC connections.";
116 };
117
118 user = lib.mkOption {
119 type = lib.types.str;
120 default = "rpc";
121 description = "Username for JSON-RPC connections.";
122 };
123
124 password = lib.mkOption {
125 type = lib.types.str;
126 default = "rpc";
127 description = ''
128 RPC password for JSON-RPC connections.
129 Warning: this is stored in cleartext in the Nix store!!!
130 Use `configFile` or `passwordFile` if needed.
131 '';
132 };
133
134 passwordFile = lib.mkOption {
135 type = lib.types.nullOr lib.types.path;
136 default = null;
137 description = ''
138 File containing password of the RPC user.
139 Note: This options is ignored when `configFile` is used.
140 '';
141 };
142 };
143
144 sync = lib.mkOption {
145 type = lib.types.bool;
146 default = true;
147 description = "Synchronizes until tip, if together with zeromq, keeps index synchronized.";
148 };
149
150 templateDir = lib.mkOption {
151 type = lib.types.path;
152 default = "${config.package}/share/templates/";
153 defaultText = lib.literalExpression ''"''${package}/share/templates/"'';
154 example = lib.literalExpression ''"''${dataDir}/templates/static/"'';
155 description = "Location of the HTML templates. By default, ones shipped with the package are used.";
156 };
157
158 extraConfig = lib.mkOption {
159 type = lib.types.attrs;
160 default = { };
161 example = lib.literalExpression ''
162 {
163 "alternative_estimate_fee" = "whatthefee-disabled";
164 "alternative_estimate_fee_params" = "{\"url\": \"https://whatthefee.io/data.json\", \"periodSeconds\": 60}";
165 "fiat_rates" = "coingecko";
166 "fiat_rates_params" = "{\"url\": \"https://api.coingecko.com/api/v3\", \"coin\": \"bitcoin\", \"periodSeconds\": 60}";
167 "coin_shortcut" = "BTC";
168 "coin_label" = "Bitcoin";
169 "parse" = true;
170 "subversion" = "";
171 "address_format" = "";
172 "xpub_magic" = 76067358;
173 "xpub_magic_segwit_p2sh" = 77429938;
174 "xpub_magic_segwit_native" = 78792518;
175 "mempool_workers" = 8;
176 "mempool_sub_workers" = 2;
177 "block_addresses_to_keep" = 300;
178 }'';
179 description = ''
180 Additional configurations to be appended to {file}`coin.conf`.
181 Overrides any already defined configuration options.
182 See <https://github.com/trezor/blockbook/tree/master/configs/coins>
183 for current configuration options supported in master (Note: may differ from release).
184 '';
185 };
186
187 extraCmdLineOptions = lib.mkOption {
188 type = lib.types.listOf lib.types.str;
189 default = [ ];
190 example = [
191 "-workers=1"
192 "-dbcache=0"
193 "-logtosderr"
194 ];
195 description = ''
196 Extra command line options to pass to Blockbook.
197 Run blockbook --help to list all available options.
198 '';
199 };
200 };
201 };
202in
203{
204 # interface
205
206 options = {
207 services.blockbook-frontend = lib.mkOption {
208 type = lib.types.attrsOf (lib.types.submodule blockbookOpts);
209 default = { };
210 description = "Specification of one or more blockbook-frontend instances.";
211 };
212 };
213
214 # implementation
215
216 config = lib.mkIf (eachBlockbook != { }) {
217
218 systemd.services = lib.mapAttrs' (
219 blockbookName: cfg:
220 (lib.nameValuePair "blockbook-frontend-${blockbookName}" (
221 let
222 configFile =
223 if cfg.configFile != null then
224 cfg.configFile
225 else
226 pkgs.writeText "config.conf" (
227 builtins.toJSON (
228 {
229 coin_name = "${cfg.coinName}";
230 rpc_user = "${cfg.rpc.user}";
231 rpc_pass = "${cfg.rpc.password}";
232 rpc_url = "${cfg.rpc.url}:${toString cfg.rpc.port}";
233 message_queue_binding = "${cfg.messageQueueBinding}";
234 }
235 // cfg.extraConfig
236 )
237 );
238 in
239 {
240 description = "blockbook-frontend-${blockbookName} daemon";
241 after = [ "network.target" ];
242 wantedBy = [ "multi-user.target" ];
243 preStart = ''
244 ln -sf ${cfg.templateDir} ${cfg.dataDir}/static/
245 ln -sf ${cfg.cssDir} ${cfg.dataDir}/static/
246 ${lib.optionalString (cfg.rpc.passwordFile != null && cfg.configFile == null) ''
247 CONFIGTMP=$(mktemp)
248 ${pkgs.jq}/bin/jq ".rpc_pass = \"$(cat ${cfg.rpc.passwordFile})\"" ${configFile} > $CONFIGTMP
249 mv $CONFIGTMP ${cfg.dataDir}/${blockbookName}-config.json
250 ''}
251 '';
252 serviceConfig = {
253 User = cfg.user;
254 Group = cfg.group;
255 ExecStart = ''
256 ${cfg.package}/bin/blockbook \
257 ${
258 if (cfg.rpc.passwordFile != null && cfg.configFile == null) then
259 "-blockchaincfg=${cfg.dataDir}/${blockbookName}-config.json"
260 else
261 "-blockchaincfg=${configFile}"
262 } \
263 -datadir=${cfg.dataDir} \
264 ${lib.optionalString (cfg.sync != false) "-sync"} \
265 ${lib.optionalString (cfg.certFile != null) "-certfile=${toString cfg.certFile}"} \
266 ${lib.optionalString (cfg.debug != false) "-debug"} \
267 ${lib.optionalString (cfg.internal != null) "-internal=${toString cfg.internal}"} \
268 ${lib.optionalString (cfg.public != null) "-public=${toString cfg.public}"} \
269 ${toString cfg.extraCmdLineOptions}
270 '';
271 Restart = "on-failure";
272 WorkingDirectory = cfg.dataDir;
273 LimitNOFILE = 65536;
274 };
275 }
276 ))
277 ) eachBlockbook;
278
279 systemd.tmpfiles.rules = lib.flatten (
280 lib.mapAttrsToList (blockbookName: cfg: [
281 "d ${cfg.dataDir} 0750 ${cfg.user} ${cfg.group} - -"
282 "d ${cfg.dataDir}/static 0750 ${cfg.user} ${cfg.group} - -"
283 ]) eachBlockbook
284 );
285
286 users.users = lib.mapAttrs' (
287 blockbookName: cfg:
288 (lib.nameValuePair "blockbook-frontend-${blockbookName}" {
289 name = cfg.user;
290 group = cfg.group;
291 home = cfg.dataDir;
292 isSystemUser = true;
293 })
294 ) eachBlockbook;
295
296 users.groups = lib.mapAttrs' (
297 instanceName: cfg: (lib.nameValuePair "${cfg.group}" { })
298 ) eachBlockbook;
299 };
300
301 meta.maintainers = with lib.maintainers; [ _1000101 ];
302
303}