diff --git a/README.md b/README.md index 894cbdf..5bd8e47 100644 --- a/README.md +++ b/README.md @@ -94,6 +94,7 @@ NixOS modules ([src](modules/modules.nix)) * [liquid](https://github.com/elementsproject/elements): federated sidechain * [JoinMarket](https://github.com/joinmarket-org/joinmarket-clientserver) * [JoinMarket Orderbook Watcher](https://github.com/JoinMarket-Org/joinmarket-clientserver/blob/master/docs/orderbook.md) + * [Jam](https://github.com/joinmarket-webui/jam): simplified user-friendly JoinMarket web interface * [bitcoin-core-hwi](https://github.com/bitcoin-core/HWI) * Helper * [netns-isolation](modules/netns-isolation.nix): isolates applications on the network-level via network namespaces diff --git a/docs/services.md b/docs/services.md index 8037905..44fcf15 100644 --- a/docs/services.md +++ b/docs/services.md @@ -557,6 +557,16 @@ See [here](https://github.com/JoinMarket-Org/joinmarket-clientserver/blob/master 3. Profit +## Run the Jam, the JoinMarket web interface + +The [Jam](https://github.com/joinmarket-webui/jam) is a static web interface +which is served by nginx. It connects to the jmwalled daemon which gets started +in the background. + +TODO +- How to access the web interface using e.g. ssh proxy: `ssh -L 61851:localhost:61851 root@` +- how to access interface over onion? + # clightning ## Plugins diff --git a/examples/configuration.nix b/examples/configuration.nix index 0013bb8..50013f8 100644 --- a/examples/configuration.nix +++ b/examples/configuration.nix @@ -249,6 +249,13 @@ # # Set this to enable the JoinMarket order book watcher. # services.joinmarket-ob-watcher.enable = true; + # + # Set this to enable Jam, the JoinMarket web interface. + # services.joinmarket-jam.enable = true; + # + # Set this to create an onion service to make the Jam web interface + # available via Tor: + # nix-bitcoin.onionServices.joinmarket-jam.enable = true; ### Nodeinfo # Set this to add command `nodeinfo` to the system environment. diff --git a/modules/joinmarket-jam.nix b/modules/joinmarket-jam.nix new file mode 100644 index 0000000..7218465 --- /dev/null +++ b/modules/joinmarket-jam.nix @@ -0,0 +1,180 @@ +{ config, lib, pkgs, ... }: + +with lib; +let + options.services.joinmarket-jam = { + enable = mkEnableOption "Enable the JoinMarket Jam web interface."; + address = mkOption { + type = types.str; + default = "127.0.0.1"; + description = mdDoc "HTTP server address."; + }; + port = mkOption { + type = types.port; + default = 61851; + description = mdDoc "HTTP server port."; + }; + staticContentRoot = mkOption { + type = types.path; + default = nbPkgs.joinmarket-jam; + defaultText = "config.nix-bitcoin.pkgs.joinmarket-jam"; + description = mdDoc "Path of the static content root."; + }; + nginxConfig = mkOption { + readOnly = true; + default = nginxConfig; + defaultText = "(See source)"; + description = mdDoc '' + An attrset of nginx config snippets for assembling a custom + joinmarket's jam nginx config. + ''; + }; + package = mkOption { + type = types.package; + default = nbPkgs.joinmarket-jam; + defaultText = "config.nix-bitcoin.pkgs.joinmarket-jam"; + description = mdDoc "The package providing joinmarket's jam files."; + }; + user = mkOption { + type = types.str; + default = "joinmarket-jam"; + description = mdDoc "The user as which to run Jam."; + }; + group = mkOption { + type = types.str; + default = cfg.user; + description = mdDoc "The group as which to run Jam."; + }; + tor.enforce = nbLib.tor.enforce; + + #settings = mkOption { + #}; + }; + + cfg = config.services.joinmarket-jam; + nbLib = config.nix-bitcoin.lib; + nbPkgs = config.nix-bitcoin.pkgs; + + inherit (config.services) joinmarket-ob-watcher joinmarket-jmwalletd; + + # Nginx configuration is highgly inspired by official jam-docker ui-only container. + # https://github.com/joinmarket-webui/jam-docker/tree/master/ui-only/nginx + nginxConfig = { + staticContent = '' + index index.html; + + add_header Cache-Control "public, no-transform"; + add_header Vary Accept-Language; + add_header Vary Cookie; + ''; + + proxyApi = let + jmwalletd_api_backend = "https://${nbLib.addressWithPort joinmarket-jmwalletd.address joinmarket-jmwalletd.port}"; + jmwalletd_wss_backend = "https://${nbLib.addressWithPort joinmarket-jmwalletd.address joinmarket-jmwalletd.wssPort}/"; + ob_watcher_backend = "http://${nbLib.addressWithPort joinmarket-ob-watcher.address joinmarket-ob-watcher.port}"; + in '' + location / { + #include /etc/nginx/snippets/proxy-params.conf; + + try_files $uri $uri/ /index.html; + add_header Cache-Control no-cache; + } + location /api/ { + proxy_pass ${jmwalletd_api_backend}; + + #include /etc/nginx/snippets/proxy-params.conf; + + proxy_http_version 1.1; + proxy_set_header Connection ""; + proxy_set_header Authorization $http_x_jm_authorization; + proxy_set_header x-jm-authorization ""; + proxy_read_timeout 300s; + proxy_connect_timeout 300s; + } + location = /jmws { + proxy_pass ${jmwalletd_wss_backend}; + + #include /etc/nginx/snippets/proxy-params.conf; + + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "Upgrade"; + proxy_set_header Authorization ""; + + # allow 10m without socket activity (default is 60 sec) + proxy_read_timeout 600s; + proxy_send_timeout 600s; + } + location /obwatch/ { + proxy_pass ${ob_watcher_backend}; + + #include /etc/nginx/snippets/proxy-params.conf; + + proxy_http_version 1.1; + proxy_set_header Connection ""; + proxy_read_timeout 300s; + proxy_connect_timeout 300s; + } + location = /jam/internal/auth { + internal; + proxy_pass http://$server_addr:$server_port/api/v1/session; + + #if ($jm_auth_present != 1) { + # return 401; + #} + + #include /etc/nginx/snippets/proxy-params.conf; + + proxy_http_version 1.1; + proxy_set_header Connection ""; + proxy_pass_request_body off; + proxy_set_header Content-Length ""; + } + location = /jam/api/v0/features { + auth_request /jam/internal/auth; + default_type application/json; + return 200 '{ "features": { "logs": false } }'; + } + location /jam/api/v0/log/ { + auth_request /jam/internal/auth; + return 501; # Not Implemented + } + ''; + }; + +in { + inherit options; + + config = mkIf cfg.enable { + services = { + joinmarket-ob-watcher.enable = true; + joinmarket-jmwalletd.enable = true; + + nginx = { + enable = true; + enableReload = true; + recommendedBrotliSettings = true; + recommendedGzipSettings = true; + recommendedOptimisation = true; + recommendedProxySettings = true; + recommendedTlsSettings = true; + # TODO: Use this to define "map"? See: https://github.com/joinmarket-webui/jam-docker/blob/master/ui-only/nginx/templates/default.conf.template#L20 + #commonHttpConfig = nginxConfig.httpConfig; + virtualHosts."joinmarket-jam" = { + serverName = "_"; + listen = [ { addr = cfg.address; port = cfg.port; } ]; + root = cfg.staticContentRoot; + extraConfig = nginxConfig.staticContent + nginxConfig.proxyApi; + }; + }; + }; + + users = { + users.${cfg.user} = { + isSystemUser = true; + group = cfg.group; + }; + groups.${cfg.group} = {}; + }; + }; +} diff --git a/modules/joinmarket.nix b/modules/joinmarket.nix index 34343fa..39f855a 100644 --- a/modules/joinmarket.nix +++ b/modules/joinmarket.nix @@ -40,6 +40,24 @@ let default = "/var/lib/joinmarket"; description = mdDoc "The data directory for JoinMarket."; }; + max_cj_fee_abs = mkOption { + type = types.ints.unsigned; + default = 30000; # in sats + description = mdDoc '' + Maximum absolute coinjoin fee in satoshi to pay to a single market + maker for a transaction. + ''; + }; + max_cj_fee_rel = mkOption { + type = types.float; + default = 0.0003; # 0.03 % + description = mdDoc '' + Maximum relative coinjoin fee, in fractions of the coinjoin value e.g. + if your coinjoin amount is 2 btc (200 million satoshi) and + max_cj_fee_rel = 0.001 (0.1%), the maximum fee allowed would be 0.002 + btc (200 thousand satoshi). + ''; + }; rpcWalletFile = mkOption { type = types.nullOr types.str; default = "jm_wallet"; @@ -217,6 +235,8 @@ let tx_fees_factor = 0.2 absurd_fee_per_kb = 350000 max_sweep_fee_change = 0.8 + max_cj_fee_abs = ${toString cfg.max_cj_fee_abs} + max_cj_fee_rel = ${toString cfg.max_cj_fee_rel} tx_broadcast = self minimum_makers = 4 max_sats_freeze_reuse = -1 diff --git a/modules/modules.nix b/modules/modules.nix index 6c409e5..95b6cb9 100644 --- a/modules/modules.nix +++ b/modules/modules.nix @@ -28,6 +28,7 @@ ./joinmarket.nix ./joinmarket-ob-watcher.nix ./joinmarket-jmwalletd.nix + ./joinmarket-jam.nix ./hardware-wallets.nix # Support features diff --git a/modules/netns-isolation.nix b/modules/netns-isolation.nix index 6e6bdf6..1b7b41e 100644 --- a/modules/netns-isolation.nix +++ b/modules/netns-isolation.nix @@ -356,6 +356,7 @@ in { }; services.joinmarket-ob-watcher.address = netns.joinmarket-ob-watcher.address; + services.joinmarket-jam.address = netns.nginx.address; services.lightning-pool.rpcAddress = netns.lightning-pool.address; diff --git a/modules/nodeinfo.nix b/modules/nodeinfo.nix index bb4ae8e..ddad845 100644 --- a/modules/nodeinfo.nix +++ b/modules/nodeinfo.nix @@ -149,6 +149,11 @@ in { liquidd = mkInfo ""; joinmarket-ob-watcher = mkInfo ""; joinmarket-jmwalletd = mkInfo ""; + joinmarket-jam = name: cfg: mkInfoLong { + inherit name cfg; + systemdServiceName = "nginx"; + extraCode = ""; + }; rtl = mkInfo ""; mempool = mkInfo ""; mempool-frontend = name: cfg: mkInfoLong { diff --git a/modules/onion-services.nix b/modules/onion-services.nix index 6341869..3bbedfe 100644 --- a/modules/onion-services.nix +++ b/modules/onion-services.nix @@ -110,6 +110,9 @@ in { joinmarket-ob-watcher = { externalPort = 80; }; + joinmarket-jam = { + externalPort = 80; + }; rtl = { externalPort = 80; }; diff --git a/test/tests.nix b/test/tests.nix index 15df96c..fd961fe 100644 --- a/test/tests.nix +++ b/test/tests.nix @@ -133,6 +133,7 @@ let cjfee_r = 0.00003; txfee = 200; }; + tests.joinmarket-jam = cfg.joinmarket-jam.enable; tests.nodeinfo = config.nix-bitcoin.nodeinfo.enable; tests.backups = cfg.backups.enable; @@ -207,6 +208,7 @@ let services.joinmarket.enable = true; services.joinmarket-ob-watcher.enable = true; services.joinmarket-jmwalletd.enable = true; + services.joinmarket-jam.enable = true; services.backups.enable = true; nix-bitcoin.nodeinfo.enable = true; @@ -255,6 +257,7 @@ let services.btcpayserver.enable = true; services.joinmarket.enable = true; services.joinmarket-jmwalletd.enable = true; + services.joinmarket-jam.enable = true; }; # netns and regtest, without secure-node.nix diff --git a/test/tests.py b/test/tests.py index e71f555..d1836fe 100644 --- a/test/tests.py +++ b/test/tests.py @@ -292,6 +292,12 @@ def _(): # Test web server response assert_full_match(f"curl -fsSL --insecure https://{ip('joinmarket')}:28183/api/v1/getinfo | jq -jr keys[0]", "version") +@test("joinmarket-jam") +def _(): + assert_running("nginx") + wait_for_open_port(ip("nginx"), 61851) + assert_matches(f"curl -L {ip('nginx')}:61851", "Jam for JoinMarket") + @test("nodeinfo") def _(): status, _ = machine.execute("systemctl is-enabled --quiet onion-addresses 2> /dev/null")