diff --git a/nixos/modules/module-list.nix b/nixos/modules/module-list.nix
index fd6294f2d7c3..cf25ae3157e6 100644
--- a/nixos/modules/module-list.nix
+++ b/nixos/modules/module-list.nix
@@ -589,6 +589,7 @@
./services/networking/autossh.nix
./services/networking/bird.nix
./services/networking/bitlbee.nix
+ ./services/networking/blockbook-frontend.nix
./services/networking/charybdis.nix
./services/networking/cjdns.nix
./services/networking/cntlm.nix
diff --git a/nixos/modules/services/networking/blockbook-frontend.nix b/nixos/modules/services/networking/blockbook-frontend.nix
new file mode 100644
index 000000000000..61938e51e063
--- /dev/null
+++ b/nixos/modules/services/networking/blockbook-frontend.nix
@@ -0,0 +1,272 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+
+ eachBlockbook = config.services.blockbook-frontend;
+
+ blockbookOpts = { config, lib, name, ...}: {
+
+ options = {
+
+ enable = mkEnableOption "blockbook-frontend application.";
+
+ package = mkOption {
+ type = types.package;
+ default = pkgs.blockbook;
+ description = "Which blockbook package to use.";
+ };
+
+ user = mkOption {
+ type = types.str;
+ default = "blockbook-frontend-${name}";
+ description = "The user as which to run blockbook-frontend-${name}.";
+ };
+
+ group = mkOption {
+ type = types.str;
+ default = "${config.user}";
+ description = "The group as which to run blockbook-frontend-${name}.";
+ };
+
+ certFile = mkOption {
+ type = types.nullOr types.path;
+ default = null;
+ example = "/etc/secrets/blockbook-frontend-${name}/certFile";
+ description = ''
+ To enable SSL, specify path to the name of certificate files without extension.
+ Expecting certFile.crt and certFile.key.
+ '';
+ };
+
+ configFile = mkOption {
+ type = with types; nullOr path;
+ default = null;
+ example = "${config.dataDir}/config.json";
+ description = "Location of the blockbook configuration file.";
+ };
+
+ coinName = mkOption {
+ type = types.str;
+ default = "Bitcoin";
+ example = "Bitcoin";
+ description = ''
+ See
+ for current of coins supported in master (Note: may differ from release).
+ '';
+ };
+
+ cssDir = mkOption {
+ type = types.path;
+ default = "${config.package}/share/css/";
+ example = "${config.dataDir}/static/css/";
+ description = ''
+ Location of the dir with main.css CSS file.
+ By default, the one shipped with the package is used.
+ '';
+ };
+
+ dataDir = mkOption {
+ type = types.path;
+ default = "/var/lib/blockbook-frontend-${name}";
+ description = "Location of blockbook-frontend-${name} data directory.";
+ };
+
+ debug = mkOption {
+ type = types.bool;
+ default = false;
+ description = "Debug mode, return more verbose errors, reload templates on each request.";
+ };
+
+ internal = mkOption {
+ type = types.nullOr types.str;
+ default = ":9030";
+ example = ":9030";
+ description = "Internal http server binding [address]:port.";
+ };
+
+ messageQueueBinding = mkOption {
+ type = types.str;
+ default = "tcp://127.0.0.1:38330";
+ example = "tcp://127.0.0.1:38330";
+ description = "Message Queue Binding address:port.";
+ };
+
+ public = mkOption {
+ type = types.nullOr types.str;
+ default = ":9130";
+ example = ":9130";
+ description = "Public http server binding [address]:port.";
+ };
+
+ rpc = {
+ url = mkOption {
+ type = types.str;
+ default = "http://127.0.0.1";
+ description = "URL for JSON-RPC connections.";
+ };
+
+ port = mkOption {
+ type = types.port;
+ default = 8030;
+ description = "Port for JSON-RPC connections.";
+ };
+
+ user = mkOption {
+ type = types.str;
+ default = "rpc";
+ example = "rpc";
+ description = "Username for JSON-RPC connections.";
+ };
+
+ password = mkOption {
+ type = types.str;
+ default = "rpc";
+ example = "rpc";
+ description = ''
+ RPC password for JSON-RPC connections.
+ Warning: this is stored in cleartext in the Nix store!!!
+ Use configFile or passwordFile if needed.
+ '';
+ };
+
+ passwordFile = mkOption {
+ type = types.nullOr types.path;
+ default = null;
+ description = ''
+ File containing password of the RPC user.
+ Note: This options is ignored when configFile is used.
+ '';
+ };
+ };
+
+ sync = mkOption {
+ type = types.bool;
+ default = true;
+ description = "Synchronizes until tip, if together with zeromq, keeps index synchronized.";
+ };
+
+ templateDir = mkOption {
+ type = types.path;
+ default = "${config.package}/share/templates/";
+ example = "${config.dataDir}/templates/static/";
+ description = "Location of the HTML templates. By default, ones shipped with the package are used.";
+ };
+
+ extraConfig = mkOption {
+ type = types.attrs;
+ default = {};
+ example = literalExample '' {
+ alternative_estimate_fee = "whatthefee-disabled";
+ alternative_estimate_fee_params = "{\"url\": \"https://whatthefee.io/data.json\", \"periodSeconds\": 60}";
+ fiat_rates = "coingecko";
+ fiat_rates_params = "{\"url\": \"https://api.coingecko.com/api/v3\", \"coin\": \"bitcoin\", \"periodSeconds\": 60}";
+ coin_shortcut = "BTC";
+ coin_label = "Bitcoin";
+ xpub_magic = 76067358;
+ xpub_magic_segwit_p2sh = 77429938;
+ xpub_magic_segwit_native = 78792518;
+ }'';
+ description = ''
+ Additional configurations to be appended to coin.conf.
+ Overrides any already defined configuration options.
+ See
+ for current configuration options supported in master (Note: may differ from release).
+ '';
+ };
+
+ extraCmdLineOptions = mkOption {
+ type = types.listOf types.str;
+ default = [];
+ example = [ "-workers=1" "-dbcache=0" "-logtosderr" ];
+ description = ''
+ Extra command line options to pass to Blockbook.
+ Run blockbook --help to list all available options.
+ '';
+ };
+ };
+ };
+in
+{
+ # interface
+
+ options = {
+ services.blockbook-frontend = mkOption {
+ type = types.attrsOf (types.submodule blockbookOpts);
+ default = {};
+ description = "Specification of one or more blockbook-frontend instances.";
+ };
+ };
+
+ # implementation
+
+ config = mkIf (eachBlockbook != {}) {
+
+ systemd.services = mapAttrs' (blockbookName: cfg: (
+ nameValuePair "blockbook-frontend-${blockbookName}" (
+ let
+ configFile = if cfg.configFile != null then cfg.configFile else
+ pkgs.writeText "config.conf" (builtins.toJSON ( {
+ coin_name = "${cfg.coinName}";
+ rpc_user = "${cfg.rpc.user}";
+ rpc_pass = "${cfg.rpc.password}";
+ rpc_url = "${cfg.rpc.url}:${toString cfg.rpc.port}";
+ message_queue_binding = "${cfg.messageQueueBinding}";
+ } // cfg.extraConfig)
+ );
+ in {
+ description = "blockbook-frontend-${blockbookName} daemon";
+ after = [ "network.target" ];
+ wantedBy = [ "multi-user.target" ];
+ preStart = ''
+ ln -sf ${cfg.templateDir} ${cfg.dataDir}/static/
+ ln -sf ${cfg.cssDir} ${cfg.dataDir}/static/
+ ${optionalString (cfg.rpc.passwordFile != null && cfg.configFile == null) ''
+ CONFIGTMP=$(mktemp)
+ ${pkgs.jq}/bin/jq ".rpc_pass = \"$(cat ${cfg.rpc.passwordFile})\"" ${configFile} > $CONFIGTMP
+ mv $CONFIGTMP ${cfg.dataDir}/${blockbookName}-config.json
+ ''}
+ '';
+ serviceConfig = {
+ User = cfg.user;
+ Group = cfg.group;
+ ExecStart = ''
+ ${cfg.package}/bin/blockbook \
+ ${if (cfg.rpc.passwordFile != null && cfg.configFile == null) then
+ "-blockchaincfg=${cfg.dataDir}/${blockbookName}-config.json"
+ else
+ "-blockchaincfg=${configFile}"
+ } \
+ -datadir=${cfg.dataDir} \
+ ${optionalString (cfg.sync != false) "-sync"} \
+ ${optionalString (cfg.certFile != null) "-certfile=${toString cfg.certFile}"} \
+ ${optionalString (cfg.debug != false) "-debug"} \
+ ${optionalString (cfg.internal != null) "-internal=${toString cfg.internal}"} \
+ ${optionalString (cfg.public != null) "-public=${toString cfg.public}"} \
+ ${toString cfg.extraCmdLineOptions}
+ '';
+ Restart = "on-failure";
+ WorkingDirectory = cfg.dataDir;
+ LimitNOFILE = 65536;
+ };
+ }
+ ) )) eachBlockbook;
+
+ systemd.tmpfiles.rules = flatten (mapAttrsToList (blockbookName: cfg: [
+ "d ${cfg.dataDir} 0750 ${cfg.user} ${cfg.group} - -"
+ "d ${cfg.dataDir}/static 0750 ${cfg.user} ${cfg.group} - -"
+ ]) eachBlockbook);
+
+ users.users = mapAttrs' (blockbookName: cfg: (
+ nameValuePair "blockbook-frontend-${blockbookName}" {
+ name = cfg.user;
+ group = cfg.group;
+ home = cfg.dataDir;
+ isSystemUser = true;
+ })) eachBlockbook;
+
+ users.groups = mapAttrs' (instanceName: cfg: (
+ nameValuePair "${cfg.group}" { })) eachBlockbook;
+ };
+}
diff --git a/nixos/tests/all-tests.nix b/nixos/tests/all-tests.nix
index 1e065c804c72..debc60a21d01 100644
--- a/nixos/tests/all-tests.nix
+++ b/nixos/tests/all-tests.nix
@@ -33,6 +33,7 @@ in
bees = handleTest ./bees.nix {};
bind = handleTest ./bind.nix {};
bittorrent = handleTest ./bittorrent.nix {};
+ blockbook-frontend = handleTest ./blockbook-frontend.nix {};
buildkite-agents = handleTest ./buildkite-agents.nix {};
boot = handleTestOn ["x86_64-linux"] ./boot.nix {}; # syslinux is unsupported on aarch64
boot-stage1 = handleTest ./boot-stage1.nix {};
diff --git a/nixos/tests/blockbook-frontend.nix b/nixos/tests/blockbook-frontend.nix
new file mode 100644
index 000000000000..67d0fcab9479
--- /dev/null
+++ b/nixos/tests/blockbook-frontend.nix
@@ -0,0 +1,28 @@
+import ./make-test-python.nix ({ pkgs, ... }: {
+ name = "blockbook-frontend";
+ meta = with pkgs.stdenv.lib; {
+ maintainers = with maintainers; [ maintainers."1000101" ];
+ };
+
+ machine = { ... }: {
+ services.blockbook-frontend."test" = {
+ enable = true;
+ };
+ services.bitcoind = {
+ enable = true;
+ rpc = {
+ port = 8030;
+ users.rpc.passwordHMAC = "acc2374e5f9ba9e62a5204d3686616cf$53abdba5e67a9005be6a27ca03a93ce09e58854bc2b871523a0d239a72968033";
+ };
+ };
+ };
+
+ testScript = ''
+ start_all()
+ machine.wait_for_unit("blockbook-frontend-test.service")
+
+ machine.wait_for_open_port(9030)
+
+ machine.succeed("curl -sSfL http://localhost:9030 | grep 'Blockbook'")
+ '';
+})
diff --git a/pkgs/servers/blockbook/default.nix b/pkgs/servers/blockbook/default.nix
index 4afabb858c38..3dad2f1c6cea 100644
--- a/pkgs/servers/blockbook/default.nix
+++ b/pkgs/servers/blockbook/default.nix
@@ -61,6 +61,12 @@ buildGoModule rec {
subPackages = [ "." ];
+ postInstall = ''
+ mkdir -p $out/share/
+ cp -r $src/static/templates/ $out/share/
+ cp -r $src/static/css/ $out/share/
+ '';
+
meta = with lib; {
description = "Trezor address/account balance backend";
homepage = "https://github.com/trezor/blockbook";