From 2683d1763cb460c78fce3eb0bfa7ea7988e62627 Mon Sep 17 00:00:00 2001 From: m0wer Date: Wed, 22 Apr 2026 22:44:23 +0200 Subject: [PATCH 01/10] chore(regtest): pin jm-ng backend services for the jam regtest stack Adds jmwalletd, directory servers, makers, and bitcoind to the regtest docker-compose environment so jam can be developed against the jm-ng backend end-to-end. --- docker/regtest/common.sh | 7 +- docker/regtest/docker-compose-common.yml | 75 +++++----- docker/regtest/docker-compose.yml | 167 ++++++++++++----------- docker/regtest/fund-wallet.sh | 14 ++ docker/regtest/init-setup.sh | 2 +- docker/regtest/prepare-setup.sh | 15 +- docker/regtest/readme.md | 24 ++-- vite.config.ts | 4 +- 8 files changed, 165 insertions(+), 143 deletions(-) diff --git a/docker/regtest/common.sh b/docker/regtest/common.sh index 40545855c..7b089640d 100644 --- a/docker/regtest/common.sh +++ b/docker/regtest/common.sh @@ -78,7 +78,7 @@ unlock_wallet() { # param "--insecure": Is needed because a self-signed certificate is used in joinmarket regtest container # param "--silent": Don't show progress meter or error messages (errors are reactivated with "--show-error"). # param "--show-error": When used with -s, --silent, it makes curl show an error message if it fails. - local unlock_result; unlock_result="$(curl "$base_url/api/v1/wallet/$wallet_name/unlock" --silent --show-error --insecure --data "$unlock_request_payload" | jq ".")" + local unlock_result; unlock_result="$(curl "$base_url/api/v1/wallet/$wallet_name/unlock" --silent --show-error --insecure -H "Content-Type: application/json" --data "$unlock_request_payload" | jq ".")" local unlock_result_error_msg; unlock_result_error_msg=$(jq -r '. | select(.message != null) | .message' <<< "$unlock_result") if [ "$unlock_result_error_msg" != "" ]; then @@ -135,7 +135,7 @@ create_wallet() { local wallet_password; wallet_password=${3:-} local create_request_payload; create_request_payload="{\"password\":\"$wallet_password\",\"walletname\":\"$wallet_name\",\"wallettype\":\"sw-fb\"}" - local create_result; create_result="$(curl "$base_url/api/v1/wallet/create" --silent --show-error --insecure --data "$create_request_payload" | jq ".")" + local create_result; create_result="$(curl "$base_url/api/v1/wallet/create" --silent --show-error --insecure -H "Content-Type: application/json" --data "$create_request_payload" | jq ".")" local create_result_error_msg; create_result_error_msg=$(jq -r '. | select(.message != null) | .message' <<< "$create_result") if [ "$create_result_error_msg" != "" ]; then @@ -222,7 +222,8 @@ verify_no_open_session_or_throw() { fi [ "$(jq -r '.session' <<< "$session_result")" != "false" ] && die "Please make sure no session is active." - [ "$(jq -r '.wallet_name' <<< "$session_result")" != "None" ] && die "Please make sure no wallet is active." + local session_wallet_name; session_wallet_name=$(jq -r '.wallet_name' <<< "$session_result") + [ "$session_wallet_name" != "None" ] && [ "$session_wallet_name" != "" ] && die "Please make sure no wallet is active." return 0 } diff --git a/docker/regtest/docker-compose-common.yml b/docker/regtest/docker-compose-common.yml index d7b032649..1fb5ef2dd 100644 --- a/docker/regtest/docker-compose-common.yml +++ b/docker/regtest/docker-compose-common.yml @@ -1,28 +1,33 @@ - services: - joinmarket_native: - build: - context: ./dockerfile-deps/joinmarket/latest - dockerfile: Dockerfile + image: ghcr.io/joinmarket-ng/joinmarket-ng/jmwalletd:main restart: unless-stopped environment: - ENSURE_WALLET: "true" - REMOVE_LOCK_FILES: "true" - jm_blockchain_source: regtest - jm_network: testnet - jm_rpc_host: bitcoind - jm_rpc_port: 43782 - jm_directory_nodes: ${JM_DIRECTORY_NODES:?You must set the onion address generated in prepare step to your .env file} - jm_minimum_makers: 1 # necessary to do coinjoins with this regtest setup; default is 4 - jm_taker_utxo_age: 1 # faster testing of scheduler runs; default is 5 - jm_maker_timeout_sec: 30 # easier testing of maker timeouts on regtest (and "Stall Monitor" retries); default is 60 + JOINMARKET_DATA_DIR: /root/.joinmarket-ng + JMWALLETD_HOST: 0.0.0.0 + JMWALLETD_NO_TLS: ${JMWALLETD_NO_TLS:-false} + BITCOIN__BACKEND_TYPE: descriptor_wallet + BITCOIN__RPC_URL: http://bitcoind:43782 + BITCOIN__RPC_USER: regtest + BITCOIN__RPC_PASSWORD: regtest + NETWORK_CONFIG__NETWORK: regtest + NETWORK_CONFIG__DIRECTORY_SERVERS: ${JM_DIRECTORY_NODES:?You must set the onion address generated in prepare step to your .env file} + DIRECTORY_NODES: ${JM_DIRECTORY_NODES:?You must set the onion address generated in prepare step to your .env file} + OBWATCH_URL: http://joinmarket_orderbook_watcher:8000 + TAKER__MINIMUM_MAKERS: 1 # necessary to do coinjoins with this regtest setup + TAKER__MAKER_TIMEOUT_SEC: 30 # easier testing of maker timeouts on regtest expose: - - 62601 # obwatch + - 8000 # obwatch - 28183 # jmwalletd api - 28283 # jmwalletd websocket healthcheck: - test: ["CMD", "supervisorctl", "status"] + test: + [ + 'CMD', + 'python', + '-c', + "import socket,ssl,sys; s=socket.socket(); s.settimeout(5);\ntry:\n c=ssl.create_default_context(); c.check_hostname=False; c.verify_mode=ssl.CERT_NONE; c.wrap_socket(s, server_hostname='localhost').connect(('127.0.0.1',28183)); sys.exit(0)\nexcept Exception:\n pass\ntry:\n s=socket.socket(); s.settimeout(5); s.connect(('127.0.0.1',28183)); s.close(); sys.exit(0)\nexcept Exception:\n sys.exit(1)", + ] interval: 10s timeout: 10s retries: 20 @@ -30,25 +35,33 @@ services: start_interval: 3s joinmarket_jam_standalone: - build: - context: ./dockerfile-deps/joinmarket/webui-standalone - dockerfile: Dockerfile + image: ghcr.io/joinmarket-ng/joinmarket-ng/jmwalletd:main restart: unless-stopped environment: - ENSURE_WALLET: "true" - REMOVE_LOCK_FILES: "true" - jm_blockchain_source: regtest - jm_network: testnet - jm_rpc_host: bitcoind - jm_rpc_port: 43782 - jm_directory_nodes: ${JM_DIRECTORY_NODES:?You must set the onion address generated in prepare step to your .env file} - jm_minimum_makers: 1 # necessary to do coinjoins with this regtest setup; default is 4 - jm_taker_utxo_age: 1 # faster testing of scheduler runs; default is 5 + JOINMARKET_DATA_DIR: /root/.joinmarket-ng + JMWALLETD_HOST: 0.0.0.0 + JMWALLETD_NO_TLS: ${JMWALLETD_NO_TLS:-false} + BITCOIN__BACKEND_TYPE: descriptor_wallet + BITCOIN__RPC_URL: http://bitcoind:43782 + BITCOIN__RPC_USER: regtest + BITCOIN__RPC_PASSWORD: regtest + NETWORK_CONFIG__NETWORK: regtest + NETWORK_CONFIG__DIRECTORY_SERVERS: ${JM_DIRECTORY_NODES:?You must set the onion address generated in prepare step to your .env file} + DIRECTORY_NODES: ${JM_DIRECTORY_NODES:?You must set the onion address generated in prepare step to your .env file} + OBWATCH_URL: http://joinmarket_orderbook_watcher:8000 + TAKER__MINIMUM_MAKERS: 1 # necessary to do coinjoins with this regtest setup expose: - - 80 # nginx + - 8000 # obwatch - 28183 # jmwalletd api + - 28283 # jmwalletd websocket healthcheck: - test: [ "CMD", "dinitctl", "status", "jmwalletd" ] + test: + [ + 'CMD', + 'python', + '-c', + "import socket,ssl,sys; s=socket.socket(); s.settimeout(5);\ntry:\n c=ssl.create_default_context(); c.check_hostname=False; c.verify_mode=ssl.CERT_NONE; c.wrap_socket(s, server_hostname='localhost').connect(('127.0.0.1',28183)); sys.exit(0)\nexcept Exception:\n pass\ntry:\n s=socket.socket(); s.settimeout(5); s.connect(('127.0.0.1',28183)); s.close(); sys.exit(0)\nexcept Exception:\n sys.exit(1)", + ] interval: 10s timeout: 10s retries: 20 diff --git a/docker/regtest/docker-compose.yml b/docker/regtest/docker-compose.yml index b50dd5f2d..e26dcc6c4 100644 --- a/docker/regtest/docker-compose.yml +++ b/docker/regtest/docker-compose.yml @@ -1,6 +1,4 @@ - services: - joinmarket: container_name: jm_regtest_joinmarket extends: @@ -8,48 +6,44 @@ services: service: joinmarket_native environment: READY_FILE: /root/.regtest-initializer/btc_fully_synched - jm_rpc_wallet_file: jm_primary - jm_rpc_user: joinmarket - jm_rpc_password: joinmarket + BITCOIN__RPC_USER: joinmarket + BITCOIN__RPC_PASSWORD: joinmarket + BITCOIN__DESCRIPTOR_WALLET_NAME: jm_primary ports: - - "62601:62601" - - "28183:28183" - - "28283:28283" + - '62601:8000' + - '28183:28183' + - '28283:28283' volumes: - - "joinmarket_datadir:/root/.joinmarket" - - "initializer_datadir:/root/.regtest-initializer" + - 'joinmarket_datadir:/root/.joinmarket-ng' + - 'initializer_datadir:/root/.regtest-initializer' depends_on: bitcoind: condition: service_healthy bitcoind_regtest_initializer: condition: service_started - irc: - condition: service_started - + joinmarket_orderbook_watcher: + condition: service_healthy joinmarket2: container_name: jm_regtest_joinmarket2 extends: file: docker-compose-common.yml service: joinmarket_jam_standalone environment: - APP_USER: joinmarket - APP_PASSWORD: joinmarket - jm_rpc_wallet_file: jm_secondary - jm_rpc_cookie_file: /root/.bitcoin_data/regtest/.cookie + BITCOIN__RPC_USER: joinmarket2 + BITCOIN__RPC_PASSWORD: joinmarket2 + BITCOIN__DESCRIPTOR_WALLET_NAME: jm_secondary ports: - - "29080:80" - - "29183:28183" # exposed for "init setup" routine + - '29080:28183' + - '29183:28183' # exposed for "init setup" routine volumes: - - "joinmarket2_datadir:/root/.joinmarket" - - "bitcoin_datadir:/root/.bitcoin_data" # mount bitcoind dir to access .cookie file + - 'joinmarket2_datadir:/root/.joinmarket-ng' depends_on: bitcoind: condition: service_healthy bitcoind_regtest_initializer: condition: service_started - irc: - condition: service_started - + joinmarket_orderbook_watcher: + condition: service_healthy joinmarket3: container_name: jm_regtest_joinmarket3 extends: @@ -57,55 +51,61 @@ services: service: joinmarket_jam_standalone environment: READY_FILE: /root/.regtest-initializer/btc_fully_synched - WAIT_FOR_BITCOIND: "false" - jm_rpc_wallet_file: jm_tertiary - jm_rpc_user: joinmarket3 - jm_rpc_password: joinmarket3 + BITCOIN__RPC_USER: joinmarket3 + BITCOIN__RPC_PASSWORD: joinmarket3 + BITCOIN__DESCRIPTOR_WALLET_NAME: jm_tertiary ports: - - "30080:80" - - "30183:28183" # exposed for "init setup" routine + - '30080:28183' + - '30183:28183' # exposed for "init setup" routine volumes: - - "joinmarket3_datadir:/root/.joinmarket" - - "initializer_datadir:/root/.regtest-initializer" + - 'joinmarket3_datadir:/root/.joinmarket-ng' + - 'initializer_datadir:/root/.regtest-initializer' depends_on: bitcoind: condition: service_healthy bitcoind_regtest_initializer: condition: service_started - irc: - condition: service_started + joinmarket_orderbook_watcher: + condition: service_healthy + joinmarket_orderbook_watcher: + container_name: jm_regtest_joinmarket_orderbook_watcher + image: ghcr.io/joinmarket-ng/joinmarket-ng/orderbook-watcher:main + restart: unless-stopped + environment: + NETWORK_CONFIG__NETWORK: regtest + DIRECTORY_NODES: ${JM_DIRECTORY_NODES:?You must set the directory node address in generated env file} + LOGGING__LEVEL: DEBUG + expose: + - 8000 + healthcheck: + test: ['CMD', 'python', '-c', "import socket; s = socket.create_connection(('127.0.0.1', 8000), 5); s.close()"] + interval: 10s + timeout: 10s + retries: 20 + start_period: 60s + start_interval: 3s + depends_on: + joinmarket_directory_node: + condition: service_healthy joinmarket_directory_node: container_name: jm_regtest_joinmarket_directory_node - build: - context: ./dockerfile-deps/joinmarket/directory_node - dockerfile: Dockerfile + image: ghcr.io/joinmarket-ng/joinmarket-ng/directory-server:main restart: unless-stopped environment: - jm_hidden_service_dir: \/root\/.joinmarket\/hidden_service_dir - jm_directory_nodes: ${JM_DIRECTORY_NODES:?You must set the onion address generated in prepare step to your .env file} - jm_blockchain_source: regtest - jm_network: testnet - volumes: - - "joinmarket_directory_node_datadir:/root/.joinmarket" - - "./out/hidden_service_dir:/root/.joinmarket/hidden_service_dir:z" + NETWORK_CONFIG__NETWORK: regtest + DIRECTORY_SERVER__HOST: 0.0.0.0 + DIRECTORY_SERVER__PORT: 5222 + LOGGING__LEVEL: DEBUG + command: ['python', '-m', 'directory_server.main'] healthcheck: - test: ["CMD", "supervisorctl", "status"] + test: ['CMD', 'python', '-c', "import socket; s = socket.socket(); s.connect(('127.0.0.1', 5222)); s.close()"] interval: 10s timeout: 10s retries: 20 start_period: 60s start_interval: 3s - irc: - container_name: jm_regtest_irc - restart: unless-stopped - image: ghcr.io/ergochat/ergo:v2.15.0@sha256:135cd42c6300d957e0045ee53fbe886e43e1c04bb621391ed7b8940c174d68f3 - expose: - - 6667 - volumes: - - "irc_datadir:/ircd" - bitcoind: container_name: jm_regtest_bitcoind restart: unless-stopped @@ -149,12 +149,21 @@ services: - 28333 # ZMQ - 28334 # ZMQ ports: - - "17782:43782" + - '17782:43782' volumes: - - "bitcoin_datadir:/home/bitcoin/data" - - "bitcoin_wallet_datadir:/home/bitcoin/walletdata" + - 'bitcoin_datadir:/home/bitcoin/data' + - 'bitcoin_wallet_datadir:/home/bitcoin/walletdata' healthcheck: - test: [ "CMD", "/entrypoint.sh", "bitcoin-cli", "-chain=regtest", "-rpcport=43782", "-datadir=/home/bitcoin/data", "-getinfo" ] + test: + [ + 'CMD', + '/entrypoint.sh', + 'bitcoin-cli', + '-chain=regtest', + '-rpcport=43782', + '-datadir=/home/bitcoin/data', + '-getinfo', + ] interval: 10s timeout: 10s retries: 20 @@ -162,22 +171,22 @@ services: start_interval: 3s bitcoind_regtest_initializer: - container_name: jm_regtest_bitcoind_initializer - build: - context: ./dockerfile-deps/bitcoin/regtest-initializer - dockerfile: Dockerfile - restart: on-failure - environment: - READY_FILE: /root/.regtest-initializer/btc_fully_synched - RPC_HOST: bitcoind - RPC_PORT: 43782 - RPC_USER: regtest - RPC_PASSWORD: regtest - volumes: - - "initializer_datadir:/root/.regtest-initializer" - depends_on: - bitcoind: - condition: service_healthy + container_name: jm_regtest_bitcoind_initializer + build: + context: ./dockerfile-deps/bitcoin/regtest-initializer + dockerfile: Dockerfile + restart: on-failure + environment: + READY_FILE: /root/.regtest-initializer/btc_fully_synched + RPC_HOST: bitcoind + RPC_PORT: 43782 + RPC_USER: regtest + RPC_PASSWORD: regtest + volumes: + - 'initializer_datadir:/root/.regtest-initializer' + depends_on: + bitcoind: + condition: service_healthy nginx: container_name: jm_regtest_nginx_test_basepath @@ -185,11 +194,11 @@ services: volumes: - ./dockerfile-deps/nginx:/etc/nginx/templates ports: - - "8000:80" + - '8000:80' extra_hosts: - - "host.docker.internal:host-gateway" + - 'host.docker.internal:host-gateway' healthcheck: - test: ["CMD-SHELL", "wget --output-document /dev/null http://127.0.0.1:80/health || exit 1"] + test: ['CMD-SHELL', 'wget --output-document /dev/null http://127.0.0.1:80/health || exit 1'] interval: 10s timeout: 10s retries: 20 @@ -212,7 +221,7 @@ services: BTCEXP_NO_RATES: 'true' BTCEXP_RPC_ALLOWALL: 'true' ports: - - "3002:3002" + - '3002:3002' depends_on: bitcoind: condition: service_healthy @@ -224,5 +233,3 @@ volumes: joinmarket_datadir: joinmarket2_datadir: joinmarket3_datadir: - joinmarket_directory_node_datadir: - irc_datadir: diff --git a/docker/regtest/fund-wallet.sh b/docker/regtest/fund-wallet.sh index 4803cca7c..687f4b503 100755 --- a/docker/regtest/fund-wallet.sh +++ b/docker/regtest/fund-wallet.sh @@ -123,6 +123,20 @@ msg "Trying to fund wallet '$wallet_name'.." [ -z "$(is_docker_container_running "jm_regtest_bitcoind")" ] && die "Please make sure bitcoin container 'jm_regtest_bitcoind' is running." [ -z "$(is_docker_container_running "$container")" ] && die "Please make sure joinmarket container '$container' is running." +session_result=$(fetch_session "$base_url") +session_open=$(jq -r '.session' <<< "$session_result") + +if [ "$session_open" = "true" ]; then + active_wallet_name=$(jq -r '.wallet_name' <<< "$session_result") + [ "$active_wallet_name" != "$wallet_name" ] && die "A different wallet is active: $active_wallet_name" + + msg_warn "Wallet '$wallet_name' is already unlocked - locking it before funding." + unlock_result=$(unlock_wallet "$base_url" "$wallet_name" "$wallet_password") + auth_token=$(jq -r '.token' <<< "$unlock_result") + auth_header="Authorization: Bearer $auth_token" + lock_wallet "$base_url" "$auth_header" "$wallet_name" >/dev/null +fi + verify_no_open_session_or_throw "$base_url" diff --git a/docker/regtest/init-setup.sh b/docker/regtest/init-setup.sh index 57daa804d..1a3435f4a 100755 --- a/docker/regtest/init-setup.sh +++ b/docker/regtest/init-setup.sh @@ -65,7 +65,7 @@ start_maker() { msg "Starting maker service for wallet $wallet_name.." local start_maker_request_payload; start_maker_request_payload="{\"txfee\":\"0\",\"cjfee_a\":\"250\",\"cjfee_r\":\"0.0003\",\"ordertype\":\"sw0absoffer\",\"minsize\":\"1\"}" - local start_maker_result; start_maker_result=$(curl "$base_url/api/v1/wallet/$wallet_name/maker/start" --silent --show-error --insecure -H "$auth_header" --data "$start_maker_request_payload" | jq ".") + local start_maker_result; start_maker_result=$(curl "$base_url/api/v1/wallet/$wallet_name/maker/start" --silent --show-error --insecure -H "$auth_header" -H "Content-Type: application/json" --data "$start_maker_request_payload" | jq ".") if [ "$start_maker_result" != "{}" ]; then msg_warn "There has been a problem starting the maker service: $start_maker_result" diff --git a/docker/regtest/prepare-setup.sh b/docker/regtest/prepare-setup.sh index de92e8937..07b06a231 100755 --- a/docker/regtest/prepare-setup.sh +++ b/docker/regtest/prepare-setup.sh @@ -29,21 +29,10 @@ SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" &>/dev/null && pwd -P) OUTPUT_FILE="$SCRIPT_DIR/.env.generated" -# generate an onion address -HS_SCRIPT_TARGET_DIR="${SCRIPT_DIR}/out/hidden_service_dir" -. "$SCRIPT_DIR/generate-onion-address.sh" "${HS_SCRIPT_TARGET_DIR}" - - -ONION_ADDRESS=`cat ${HS_SCRIPT_TARGET_DIR}/hostname` - -if ! [[ "${ONION_ADDRESS}" == *.onion ]]; then - die "Invalid argument: Could not find onion address in ${HS_SCRIPT_TARGET_DIR}/hostname" -fi - -ONION_ADDRESS_WITH_PORT="${ONION_ADDRESS}:5222" +DIRECTORY_NODE_ADDRESS="joinmarket_directory_node:5222" cat < "${OUTPUT_FILE}" -JM_DIRECTORY_NODES=${ONION_ADDRESS_WITH_PORT} +JM_DIRECTORY_NODES=${DIRECTORY_NODE_ADDRESS} EOF diff --git a/docker/regtest/readme.md b/docker/regtest/readme.md index c50461630..c725ff8b9 100644 --- a/docker/regtest/readme.md +++ b/docker/regtest/readme.md @@ -2,7 +2,7 @@ This setup will help you set up a regtest environment quickly. It starts multiple JoinMarket containers, hence not only API calls but also actual CoinJoin transactions can be tested. -Communication between these containers is done via Tor (if internet connection is available) and IRC (locally running container). +Communication between these containers is done via Tor and a local JoinMarket NG directory server. All containers will have a wallet named `Satoshi.jmdat` with password `test`. The second container has basic auth enabled (username `joinmarket` and password `joinmarket`). @@ -77,16 +77,17 @@ npm run regtest:mine ## Images -The [Docker setup](dockerfile-deps/joinmarket/latest/Dockerfile) is an adaption of [jam-standalone](https://github.com/joinmarket-webui/jam-docker/tree/master/standalone) with as little adaptations as possible. -It will fetch the latest commit from the [`master` branch of the joinmarket-clientserver repo](https://github.com/JoinMarket-Org/joinmarket-clientserver/tree/master). -Keep in mind: Building from `master` is not always reliable. This tradeoff is made to enable testing new features immediately by just rebuilding the images. +The JoinMarket containers use `ghcr.io/joinmarket-ng/joinmarket-ng/jmwalletd:main`. +The directory service uses `ghcr.io/joinmarket-ng/joinmarket-ng/directory-server:main`. +The orderbook watcher uses `ghcr.io/joinmarket-ng/joinmarket-ng/orderbook-watcher:main`. -The second JoinMarket container is based on `joinmarket-webui/jam-dev-standalone:master` which exposes an UI on port `29080` -(username `joinmarket` and pass `joinmarket` for Basic Authentication). -The third container is a copy of the second one exposed on port `30080` without authentication. +Use `:main` for latest unstable/unreleased changes and `:latest` for the latest tagged release. + +The second JoinMarket container is exposed on port `29080`. +The third container is exposed on port `30080`. This is useful if you want to perform regression tests. -One additional JoinMarket container acts as [Directory Node](https://github.com/JoinMarket-Org/joinmarket-clientserver/blob/master/docs/onion-message-channels.md#directory) and exists solely to enable communication between peers. +Additional JoinMarket NG containers act as directory server and orderbook watcher to enable peer discovery and orderbook queries between peers. ### Build @@ -95,7 +96,7 @@ One additional JoinMarket container acts as [Directory Node](https://github.com/ npm run regtest:build ``` -In order to incorporate recent upstream changes (of the `master` branch), simply rebuild the setup from scratch. +In order to incorporate recent upstream image changes, simply rebuild the setup from scratch. ```sh # download and recompile the images from scratch (without using docker cache) @@ -114,7 +115,7 @@ npm run regtest:logs:jmwalletd ### Display running JoinMarket version ```sh -docker exec -t jm_regtest_joinmarket git log --oneline -1 +curl --insecure --silent https://localhost:28183/api/v1/getinfo | jq ``` ## Helper scripts @@ -222,5 +223,4 @@ Successfully generated 5 blocks with rewards to bcrt1qs0aqmzxjq96jk8hhmta5jfn339 ## Resources -- [JoinMarket Server (GitHub)](https://github.com/JoinMarket-Org/joinmarket-clientserver) -- [JoinMarket Server Testing Docs (GitHub)](https://github.com/JoinMarket-Org/joinmarket-clientserver/blob/master/docs/TESTING.md) +- [JoinMarket NG (GitHub)](https://github.com/joinmarket-ng/joinmarket-ng) diff --git a/vite.config.ts b/vite.config.ts index b77da4c66..3410ca627 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -12,7 +12,6 @@ const { JAM_BACKEND = BACKEND_NATIVE, JMWALLETD_API_PORT = '28183', JMWALLETD_WEBSOCKET_PORT = '28283', - JMOBWATCH_PORT = '62601', JAM_API_PORT = undefined, } = process.env @@ -64,10 +63,9 @@ const serverConfigNative = (): ServerOptions => { }, }, '/obwatch': { - target: `http://127.0.0.1:${JMOBWATCH_PORT}`, + target: `https://127.0.0.1:${JMWALLETD_API_PORT}`, changeOrigin: true, secure: false, - rewrite: (p) => p.replace(/^\/obwatch/, ''), }, '/jmws': { target: `https://127.0.0.1:${JMWALLETD_WEBSOCKET_PORT}`, From 38ad3043e55daeb19a87bd0eb337b1ee4eaf6d95 Mon Sep 17 00:00:00 2001 From: m0wer Date: Wed, 22 Apr 2026 22:44:30 +0200 Subject: [PATCH 02/10] chore(regtest): wire docker compose env and vite dev proxy for jm-ng Connects the local regtest containers into a single compose network, adds tor and fund-wallet helpers, and points Vite's dev proxy at the local jmwalletd instance. --- docker/regtest/.env.example | 5 +- docker/regtest/docker-compose-common.yml | 93 +++++++++- docker/regtest/docker-compose.yml | 224 ++++++++++++++++++++--- docker/regtest/fund-wallet.sh | 4 + docker/regtest/init-setup.sh | 12 +- docker/regtest/prepare-setup.sh | 28 ++- docker/regtest/readme.md | 48 ++++- docker/regtest/tor/conf/torrc | 15 ++ package.json | 1 + vite.config.ts | 8 +- 10 files changed, 386 insertions(+), 52 deletions(-) create mode 100644 docker/regtest/tor/conf/torrc diff --git a/docker/regtest/.env.example b/docker/regtest/.env.example index a87c88226..c328c1f4d 100644 --- a/docker/regtest/.env.example +++ b/docker/regtest/.env.example @@ -1,2 +1,3 @@ -JM_DIRECTORY_NODES=replaceme - +JM_REF_DIRECTORY_NODES=replaceme +JM_NG_DIRECTORY_NODES=replaceme +JM_ALL_DIRECTORY_NODES=replaceme,replaceme diff --git a/docker/regtest/docker-compose-common.yml b/docker/regtest/docker-compose-common.yml index 1fb5ef2dd..ab5907e7c 100644 --- a/docker/regtest/docker-compose-common.yml +++ b/docker/regtest/docker-compose-common.yml @@ -1,5 +1,68 @@ services: joinmarket_native: + build: + context: ./dockerfile-deps/joinmarket/latest + dockerfile: Dockerfile + restart: unless-stopped + environment: + ENSURE_WALLET: 'true' + REMOVE_LOCK_FILES: 'true' + jm_blockchain_source: regtest + jm_network: testnet + jm_rpc_host: bitcoind + jm_rpc_port: 43782 + jm_directory_nodes: ${JM_ALL_DIRECTORY_NODES:?You must set the directory node addresses in generated env file} + jm_socks5_host: tor + jm_socks5_port: 9050 + jm_tor_control_host: tor + jm_tor_control_port: 9051 + jm_minimum_makers: 1 # necessary to do coinjoins with this regtest setup; default is 4 + jm_taker_utxo_age: 1 # faster testing of scheduler runs; default is 5 + jm_maker_timeout_sec: 30 # easier testing of maker timeouts on regtest (and "Stall Monitor" retries); default is 60 + expose: + - 62601 # obwatch + - 27183 # joinmarketd daemon + - 28183 # jmwalletd api + - 28283 # jmwalletd websocket + healthcheck: + test: ['CMD', 'supervisorctl', 'status'] + interval: 10s + timeout: 10s + retries: 20 + start_period: 60s + start_interval: 3s + + joinmarket_jam_standalone: + build: + context: ./dockerfile-deps/joinmarket/webui-standalone + dockerfile: Dockerfile + restart: unless-stopped + environment: + ENSURE_WALLET: 'true' + REMOVE_LOCK_FILES: 'true' + jm_blockchain_source: regtest + jm_network: testnet + jm_rpc_host: bitcoind + jm_rpc_port: 43782 + jm_directory_nodes: ${JM_ALL_DIRECTORY_NODES:?You must set the directory node addresses in generated env file} + jm_socks5_host: tor + jm_socks5_port: 9050 + jm_tor_control_host: tor + jm_tor_control_port: 9051 + jm_minimum_makers: 1 # necessary to do coinjoins with this regtest setup; default is 4 + jm_taker_utxo_age: 1 # faster testing of scheduler runs; default is 5 + expose: + - 80 # nginx + - 28183 # jmwalletd api + healthcheck: + test: ['CMD', 'dinitctl', 'status', 'jmwalletd'] + interval: 10s + timeout: 10s + retries: 20 + start_period: 60s + start_interval: 3s + + joinmarket_ng_native: image: ghcr.io/joinmarket-ng/joinmarket-ng/jmwalletd:main restart: unless-stopped environment: @@ -10,10 +73,16 @@ services: BITCOIN__RPC_URL: http://bitcoind:43782 BITCOIN__RPC_USER: regtest BITCOIN__RPC_PASSWORD: regtest - NETWORK_CONFIG__NETWORK: regtest - NETWORK_CONFIG__DIRECTORY_SERVERS: ${JM_DIRECTORY_NODES:?You must set the onion address generated in prepare step to your .env file} - DIRECTORY_NODES: ${JM_DIRECTORY_NODES:?You must set the onion address generated in prepare step to your .env file} - OBWATCH_URL: http://joinmarket_orderbook_watcher:8000 + NETWORK_CONFIG__NETWORK: testnet + NETWORK_CONFIG__BITCOIN_NETWORK: regtest + NETWORK_CONFIG__DIRECTORY_SERVERS: ${JM_ALL_DIRECTORY_NODES:?You must set the directory node addresses in generated env file} + DIRECTORY_NODES: ${JM_ALL_DIRECTORY_NODES:?You must set the directory node addresses in generated env file} + OBWATCH_URL: http://joinmarket_ng_orderbook_watcher:8000 + TOR__SOCKS_HOST: tor + TOR__SOCKS_PORT: 9050 + TOR__CONTROL_HOST: tor + TOR__CONTROL_PORT: 9051 + TOR__COOKIE_PATH: /var/lib/tor/control_auth_cookie TAKER__MINIMUM_MAKERS: 1 # necessary to do coinjoins with this regtest setup TAKER__MAKER_TIMEOUT_SEC: 30 # easier testing of maker timeouts on regtest expose: @@ -34,7 +103,7 @@ services: start_period: 60s start_interval: 3s - joinmarket_jam_standalone: + joinmarket_ng_peer: image: ghcr.io/joinmarket-ng/joinmarket-ng/jmwalletd:main restart: unless-stopped environment: @@ -45,10 +114,16 @@ services: BITCOIN__RPC_URL: http://bitcoind:43782 BITCOIN__RPC_USER: regtest BITCOIN__RPC_PASSWORD: regtest - NETWORK_CONFIG__NETWORK: regtest - NETWORK_CONFIG__DIRECTORY_SERVERS: ${JM_DIRECTORY_NODES:?You must set the onion address generated in prepare step to your .env file} - DIRECTORY_NODES: ${JM_DIRECTORY_NODES:?You must set the onion address generated in prepare step to your .env file} - OBWATCH_URL: http://joinmarket_orderbook_watcher:8000 + NETWORK_CONFIG__NETWORK: testnet + NETWORK_CONFIG__BITCOIN_NETWORK: regtest + NETWORK_CONFIG__DIRECTORY_SERVERS: ${JM_ALL_DIRECTORY_NODES:?You must set the directory node addresses in generated env file} + DIRECTORY_NODES: ${JM_ALL_DIRECTORY_NODES:?You must set the directory node addresses in generated env file} + OBWATCH_URL: http://joinmarket_ng_orderbook_watcher:8000 + TOR__SOCKS_HOST: tor + TOR__SOCKS_PORT: 9050 + TOR__CONTROL_HOST: tor + TOR__CONTROL_PORT: 9051 + TOR__COOKIE_PATH: /var/lib/tor/control_auth_cookie TAKER__MINIMUM_MAKERS: 1 # necessary to do coinjoins with this regtest setup expose: - 8000 # obwatch diff --git a/docker/regtest/docker-compose.yml b/docker/regtest/docker-compose.yml index e26dcc6c4..f772fd862 100644 --- a/docker/regtest/docker-compose.yml +++ b/docker/regtest/docker-compose.yml @@ -6,44 +6,53 @@ services: service: joinmarket_native environment: READY_FILE: /root/.regtest-initializer/btc_fully_synched - BITCOIN__RPC_USER: joinmarket - BITCOIN__RPC_PASSWORD: joinmarket - BITCOIN__DESCRIPTOR_WALLET_NAME: jm_primary + jm_rpc_wallet_file: jm_primary + jm_rpc_user: joinmarket + jm_rpc_password: joinmarket ports: - - '62601:8000' + - '62601:62601' - '28183:28183' - '28283:28283' volumes: - - 'joinmarket_datadir:/root/.joinmarket-ng' + - 'joinmarket_datadir:/root/.joinmarket' - 'initializer_datadir:/root/.regtest-initializer' depends_on: bitcoind: condition: service_healthy bitcoind_regtest_initializer: condition: service_started - joinmarket_orderbook_watcher: + irc: + condition: service_started + tor: condition: service_healthy + joinmarket2: container_name: jm_regtest_joinmarket2 extends: file: docker-compose-common.yml service: joinmarket_jam_standalone environment: - BITCOIN__RPC_USER: joinmarket2 - BITCOIN__RPC_PASSWORD: joinmarket2 - BITCOIN__DESCRIPTOR_WALLET_NAME: jm_secondary + APP_USER: joinmarket + APP_PASSWORD: joinmarket + jm_rpc_wallet_file: jm_secondary + jm_rpc_cookie_file: /root/.bitcoin_data/regtest/.cookie ports: - - '29080:28183' + - '29080:80' - '29183:28183' # exposed for "init setup" routine volumes: - - 'joinmarket2_datadir:/root/.joinmarket-ng' + - 'joinmarket2_datadir:/root/.joinmarket' + - 'bitcoin_datadir:/root/.bitcoin_data' # mount bitcoind dir to access .cookie file + - 'tor_datadir:/var/lib/tor:ro' depends_on: bitcoind: condition: service_healthy bitcoind_regtest_initializer: condition: service_started - joinmarket_orderbook_watcher: + irc: + condition: service_started + tor: condition: service_healthy + joinmarket3: container_name: jm_regtest_joinmarket3 extends: @@ -51,33 +60,129 @@ services: service: joinmarket_jam_standalone environment: READY_FILE: /root/.regtest-initializer/btc_fully_synched - BITCOIN__RPC_USER: joinmarket3 - BITCOIN__RPC_PASSWORD: joinmarket3 - BITCOIN__DESCRIPTOR_WALLET_NAME: jm_tertiary + WAIT_FOR_BITCOIND: 'false' + jm_rpc_wallet_file: jm_tertiary + jm_rpc_user: joinmarket3 + jm_rpc_password: joinmarket3 ports: - - '30080:28183' + - '30080:80' - '30183:28183' # exposed for "init setup" routine volumes: - - 'joinmarket3_datadir:/root/.joinmarket-ng' + - 'joinmarket3_datadir:/root/.joinmarket' + - 'initializer_datadir:/root/.regtest-initializer' + - 'tor_datadir:/var/lib/tor:ro' + depends_on: + bitcoind: + condition: service_healthy + bitcoind_regtest_initializer: + condition: service_started + irc: + condition: service_started + tor: + condition: service_healthy + + joinmarket4: + container_name: jm_regtest_joinmarket4 + extends: + file: docker-compose-common.yml + service: joinmarket_ng_native + environment: + READY_FILE: /root/.regtest-initializer/btc_fully_synched + BITCOIN__RPC_USER: regtest + BITCOIN__RPC_PASSWORD: regtest + BITCOIN__DESCRIPTOR_WALLET_NAME: jm_quaternary + ports: + - '31080:28183' + - '31283:28283' + - '31183:28183' # exposed for "init setup" routine + volumes: + - 'joinmarket4_datadir:/root/.joinmarket-ng' - 'initializer_datadir:/root/.regtest-initializer' + - 'tor_datadir:/var/lib/tor:ro' + depends_on: + bitcoind: + condition: service_healthy + bitcoind_regtest_initializer: + condition: service_started + joinmarket_ng_orderbook_watcher: + condition: service_healthy + tor: + condition: service_healthy + + joinmarket5: + container_name: jm_regtest_joinmarket5 + extends: + file: docker-compose-common.yml + service: joinmarket_ng_peer + environment: + BITCOIN__RPC_USER: regtest + BITCOIN__RPC_PASSWORD: regtest + BITCOIN__DESCRIPTOR_WALLET_NAME: jm_quinary + ports: + - '32080:28183' + - '32283:28283' + - '32183:28183' # exposed for "init setup" routine + volumes: + - 'joinmarket5_datadir:/root/.joinmarket-ng' + - 'tor_datadir:/var/lib/tor:ro' depends_on: bitcoind: condition: service_healthy bitcoind_regtest_initializer: condition: service_started - joinmarket_orderbook_watcher: + joinmarket_ng_orderbook_watcher: + condition: service_healthy + tor: + condition: service_healthy + + joinmarket_directory_node: + container_name: jm_regtest_joinmarket_directory_node + build: + context: ./dockerfile-deps/joinmarket/directory_node + dockerfile: Dockerfile + restart: unless-stopped + environment: + jm_hidden_service_dir: \/root\/.joinmarket\/hidden_service_dir + jm_directory_nodes: ${JM_REF_DIRECTORY_NODES:?You must set the reference directory node address in generated env file} + jm_socks5_host: localhost + jm_socks5_port: 9050 + jm_tor_control_host: localhost + jm_tor_control_port: 9051 + jm_daemon_host: 0.0.0.0 + jm_blockchain_source: regtest + jm_network: testnet + volumes: + - 'joinmarket_directory_node_datadir:/root/.joinmarket' + - './out/hidden_service_dir:/root/.joinmarket/hidden_service_dir:z' + healthcheck: + test: ['CMD', 'supervisorctl', 'status'] + interval: 10s + timeout: 10s + retries: 20 + start_period: 60s + start_interval: 3s + depends_on: + tor: condition: service_healthy + networks: + default: + ipv4_address: 172.30.0.20 - joinmarket_orderbook_watcher: - container_name: jm_regtest_joinmarket_orderbook_watcher + joinmarket_ng_orderbook_watcher: + container_name: jm_regtest_joinmarket_ng_orderbook_watcher image: ghcr.io/joinmarket-ng/joinmarket-ng/orderbook-watcher:main restart: unless-stopped environment: - NETWORK_CONFIG__NETWORK: regtest - DIRECTORY_NODES: ${JM_DIRECTORY_NODES:?You must set the directory node address in generated env file} + NETWORK_CONFIG__NETWORK: testnet + NETWORK_CONFIG__BITCOIN_NETWORK: regtest + DIRECTORY_NODES: ${JM_ALL_DIRECTORY_NODES:?You must set the directory node addresses in generated env file} + TOR__SOCKS_HOST: tor + TOR__SOCKS_PORT: 9050 LOGGING__LEVEL: DEBUG expose: - 8000 + ports: + - '31800:8000' healthcheck: test: ['CMD', 'python', '-c', "import socket; s = socket.create_connection(('127.0.0.1', 8000), 5); s.close()"] interval: 10s @@ -86,25 +191,76 @@ services: start_period: 60s start_interval: 3s depends_on: - joinmarket_directory_node: + joinmarket_ng_directory_node: condition: service_healthy - joinmarket_directory_node: - container_name: jm_regtest_joinmarket_directory_node + tor: + condition: service_healthy + + joinmarket_ng_directory_node: + container_name: jm_regtest_joinmarket_ng_directory_node image: ghcr.io/joinmarket-ng/joinmarket-ng/directory-server:main restart: unless-stopped environment: - NETWORK_CONFIG__NETWORK: regtest + NETWORK_CONFIG__NETWORK: testnet DIRECTORY_SERVER__HOST: 0.0.0.0 - DIRECTORY_SERVER__PORT: 5222 + DIRECTORY_SERVER__PORT: 5223 LOGGING__LEVEL: DEBUG command: ['python', '-m', 'directory_server.main'] healthcheck: - test: ['CMD', 'python', '-c', "import socket; s = socket.socket(); s.connect(('127.0.0.1', 5222)); s.close()"] + test: ['CMD', 'python', '-c', "import socket; s = socket.socket(); s.connect(('127.0.0.1', 5223)); s.close()"] interval: 10s timeout: 10s retries: 20 start_period: 60s start_interval: 3s + networks: + default: + ipv4_address: 172.30.0.21 + + tor: + depends_on: + tor_init: + condition: service_completed_successfully + container_name: jm_regtest_tor + image: ghcr.io/m0wer/docker-tor:latest + restart: unless-stopped + volumes: + - './tor/conf:/etc/tor:ro' + - 'tor_datadir:/var/lib/tor' + healthcheck: + test: + [ + 'CMD-SHELL', + 'test -f /var/lib/tor/ng-directory/hostname && test $(wc -c < /var/lib/tor/control_auth_cookie 2>/dev/null || echo 0) -eq 32', + ] + interval: 10s + timeout: 10s + retries: 60 + start_period: 120s + + tor_init: + container_name: jm_regtest_tor_init + image: busybox:latest + restart: 'no' + command: + [ + 'sh', + '-c', + 'mkdir -p /var/lib/tor/ref-directory /var/lib/tor/ng-directory && cp -f /source/ref/* /var/lib/tor/ref-directory/ && cp -f /source/ng/* /var/lib/tor/ng-directory/ && chown -R 1000:1000 /var/lib/tor && chmod 700 /var/lib/tor/ref-directory /var/lib/tor/ng-directory && chmod 600 /var/lib/tor/ref-directory/* /var/lib/tor/ng-directory/*', + ] + volumes: + - 'tor_datadir:/var/lib/tor' + - './out/hidden_service_dir:/source/ref:ro,z' + - './out/ng_directory_hidden_service:/source/ng:ro,z' + + irc: + container_name: jm_regtest_irc + restart: unless-stopped + image: ghcr.io/ergochat/ergo:v2.15.0@sha256:135cd42c6300d957e0045ee53fbe886e43e1c04bb621391ed7b8940c174d68f3 + expose: + - 6667 + volumes: + - 'irc_datadir:/ircd' bitcoind: container_name: jm_regtest_bitcoind @@ -233,3 +389,15 @@ volumes: joinmarket_datadir: joinmarket2_datadir: joinmarket3_datadir: + joinmarket4_datadir: + joinmarket5_datadir: + joinmarket_directory_node_datadir: + tor_datadir: + irc_datadir: + +networks: + default: + name: regtest_default + ipam: + config: + - subnet: 172.30.0.0/24 diff --git a/docker/regtest/fund-wallet.sh b/docker/regtest/fund-wallet.sh index 687f4b503..b126b46dc 100755 --- a/docker/regtest/fund-wallet.sh +++ b/docker/regtest/fund-wallet.sh @@ -105,11 +105,15 @@ parse_params() { [ "$container" == "jm_regtest_joinmarket" ] || [ "$container" == "jm_regtest_joinmarket2" ] || [ "$container" == "jm_regtest_joinmarket3" ] || + [ "$container" == "jm_regtest_joinmarket4" ] || + [ "$container" == "jm_regtest_joinmarket5" ] || die "Invalid parameter: 'container' must be a known container name" [ "$container" == "jm_regtest_joinmarket" ] && base_url='https://localhost:28183' [ "$container" == "jm_regtest_joinmarket2" ] && base_url='https://localhost:29183' [ "$container" == "jm_regtest_joinmarket3" ] && base_url='https://localhost:30183' + [ "$container" == "jm_regtest_joinmarket4" ] && base_url='https://localhost:31183' + [ "$container" == "jm_regtest_joinmarket5" ] && base_url='https://localhost:32183' return 0 } diff --git a/docker/regtest/init-setup.sh b/docker/regtest/init-setup.sh index 1a3435f4a..c0e4241db 100755 --- a/docker/regtest/init-setup.sh +++ b/docker/regtest/init-setup.sh @@ -7,7 +7,7 @@ # # It has two responsibilities: # - funding wallets in all containers with some coins -# - starting the maker service in the secondary and tertiary container +# - starting the maker service in secondary through quinary containers # ### @@ -24,6 +24,10 @@ script_dir=$(cd "$(dirname "${BASH_SOURCE[0]}")" &>/dev/null && pwd -P) . "$script_dir/fund-wallet.sh" --container jm_regtest_joinmarket2 --unmatured --blocks 50 . "$script_dir/fund-wallet.sh" --container jm_regtest_joinmarket3 --unmatured --blocks 50 +# fund wallet in quaternary and quinary JoinMarket NG containers. +. "$script_dir/fund-wallet.sh" --container jm_regtest_joinmarket4 --unmatured --blocks 50 +. "$script_dir/fund-wallet.sh" --container jm_regtest_joinmarket5 --unmatured --blocks 50 + # fund addresses of seed 'abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about' # this is useful if you "import an existing wallet" and verify rescanning the chain works as expected. dummy_wallet_address1='bcrt1q6rz28mcfaxtmd6v789l9rrlrusdprr9pz3cppk' # 1st address of jar A (m/84'/1'/0'/0/0) @@ -83,3 +87,9 @@ start_maker "https://localhost:29183" "Satoshi.jmdat" "test" msg "Attempt to start maker service for wallet $wallet_name in tertiary container.." start_maker "https://localhost:30183" "Satoshi.jmdat" "test" + +msg "Attempt to start maker service for wallet $wallet_name in quaternary container.." +start_maker "https://localhost:31183" "Satoshi.jmdat" "test" + +msg "Attempt to start maker service for wallet $wallet_name in quinary container.." +start_maker "https://localhost:32183" "Satoshi.jmdat" "test" diff --git a/docker/regtest/prepare-setup.sh b/docker/regtest/prepare-setup.sh index 07b06a231..697144a89 100755 --- a/docker/regtest/prepare-setup.sh +++ b/docker/regtest/prepare-setup.sh @@ -29,10 +29,34 @@ SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" &>/dev/null && pwd -P) OUTPUT_FILE="$SCRIPT_DIR/.env.generated" -DIRECTORY_NODE_ADDRESS="joinmarket_directory_node:5222" +# generate onion address for legacy joinmarket-clientserver directory node +REF_HS_SCRIPT_TARGET_DIR="${SCRIPT_DIR}/out/hidden_service_dir" +. "$SCRIPT_DIR/generate-onion-address.sh" "${REF_HS_SCRIPT_TARGET_DIR}" + +REF_ONION_ADDRESS=$(cat "${REF_HS_SCRIPT_TARGET_DIR}/hostname") + +if ! [[ "${REF_ONION_ADDRESS}" == *.onion ]]; then + die "Invalid argument: Could not find onion address in ${REF_HS_SCRIPT_TARGET_DIR}/hostname" +fi + +# generate onion address for JoinMarket NG directory node (served via external Tor) +NG_HS_SCRIPT_TARGET_DIR="${SCRIPT_DIR}/out/ng_directory_hidden_service" +. "$SCRIPT_DIR/generate-onion-address.sh" "${NG_HS_SCRIPT_TARGET_DIR}" + +NG_ONION_ADDRESS=$(cat "${NG_HS_SCRIPT_TARGET_DIR}/hostname") + +if ! [[ "${NG_ONION_ADDRESS}" == *.onion ]]; then + die "Invalid argument: Could not find onion address in ${NG_HS_SCRIPT_TARGET_DIR}/hostname" +fi + +REFERENCE_DIRECTORY_NODE_ADDRESS="${REF_ONION_ADDRESS}:5222" +NG_DIRECTORY_NODE_ADDRESS="${NG_ONION_ADDRESS}:5222" +ALL_DIRECTORY_NODE_ADDRESSES="${REFERENCE_DIRECTORY_NODE_ADDRESS},${NG_DIRECTORY_NODE_ADDRESS}" cat < "${OUTPUT_FILE}" -JM_DIRECTORY_NODES=${DIRECTORY_NODE_ADDRESS} +JM_REF_DIRECTORY_NODES=${REFERENCE_DIRECTORY_NODE_ADDRESS} +JM_NG_DIRECTORY_NODES=${NG_DIRECTORY_NODE_ADDRESS} +JM_ALL_DIRECTORY_NODES=${ALL_DIRECTORY_NODE_ADDRESSES} EOF diff --git a/docker/regtest/readme.md b/docker/regtest/readme.md index c725ff8b9..b579d2bb9 100644 --- a/docker/regtest/readme.md +++ b/docker/regtest/readme.md @@ -2,7 +2,7 @@ This setup will help you set up a regtest environment quickly. It starts multiple JoinMarket containers, hence not only API calls but also actual CoinJoin transactions can be tested. -Communication between these containers is done via Tor and a local JoinMarket NG directory server. +Communication between these containers is done via Tor and local directory servers. All containers will have a wallet named `Satoshi.jmdat` with password `test`. The second container has basic auth enabled (username `joinmarket` and password `joinmarket`). @@ -22,9 +22,12 @@ npm run regtest:init # mine blocks in regtest periodically npm run regtest:mine -# start jam in development mode +# start jam in development mode against joinmarket-clientserver primary backend npm run dev +# or start jam in development mode against the initialized jm-ng backend +npm run jm-ng:dev + [...] # stop the regtest environment @@ -51,8 +54,26 @@ Once the regtest environment is up and running you can start Jam with: ```sh npm run dev + +# optionally switch to the initialized jm-ng backend +npm run jm-ng:dev ``` +Backend selection can also be controlled manually via environment variables: + +- `JAM_BACKEND=native` with `JMWALLETD_API_PORT`, `JMWALLETD_WEBSOCKET_PORT`, and `JMOBWATCH_PORT` +- `JAM_BACKEND=jam-standalone` with `JAM_API_PORT` + +### Orderbook Access + +There are two easy ways to access orderbook data in this setup: + +- Through Jam itself: start `npm run dev` or `npm run jm-ng:dev` and use the Orderbook page in the UI. +- Through the NG orderbook watcher directly: `http://localhost:31800`. + +For API-level access, Jam proxies `/obwatch` based on `JMOBWATCH_PORT`. +For example, with `npm run jm-ng:dev` it targets port `31800`. + ### Stop ```sh @@ -77,17 +98,30 @@ npm run regtest:mine ## Images -The JoinMarket containers use `ghcr.io/joinmarket-ng/joinmarket-ng/jmwalletd:main`. -The directory service uses `ghcr.io/joinmarket-ng/joinmarket-ng/directory-server:main`. -The orderbook watcher uses `ghcr.io/joinmarket-ng/joinmarket-ng/orderbook-watcher:main`. +This setup runs a mixed environment: + +- `joinmarket`, `joinmarket2`, `joinmarket3`: `joinmarket-clientserver` +- `joinmarket4`, `joinmarket5`: `ghcr.io/joinmarket-ng/joinmarket-ng/jmwalletd:main` +- JoinMarket NG directory service: `ghcr.io/joinmarket-ng/joinmarket-ng/directory-server:main` +- JoinMarket NG orderbook watcher: `ghcr.io/joinmarket-ng/joinmarket-ng/orderbook-watcher:main` Use `:main` for latest unstable/unreleased changes and `:latest` for the latest tagged release. The second JoinMarket container is exposed on port `29080`. The third container is exposed on port `30080`. -This is useful if you want to perform regression tests. +The first JoinMarket NG container is exposed on ports `31080` (API) and `31283` (websocket). +The second JoinMarket NG container is exposed on ports `32080` (API) and `32283` (websocket). +This is useful if you want to perform regression tests across mixed implementations. + +The setup includes both a reference directory node and a JoinMarket NG directory server. They implement the same onion directory protocol and run side-by-side for compatibility testing. +All JoinMarket components (reference containers, JoinMarket NG containers, and orderbook watcher) are configured with both directory nodes via `JM_ALL_DIRECTORY_NODES`. + +All directory access is Tor-only in this setup: -Additional JoinMarket NG containers act as directory server and orderbook watcher to enable peer discovery and orderbook queries between peers. +- An external Tor service (`jm_regtest_tor`) is started as part of the regtest stack. +- Both directory implementations are exposed as `.onion` services by that Tor service. +- All clients use those `.onion` addresses and route directory traffic through Tor SOCKS. +- JoinMarket NG makers also use Tor control + cookie auth via mounted `/var/lib/tor/control_auth_cookie`. ### Build diff --git a/docker/regtest/tor/conf/torrc b/docker/regtest/tor/conf/torrc new file mode 100644 index 000000000..7a7d630cd --- /dev/null +++ b/docker/regtest/tor/conf/torrc @@ -0,0 +1,15 @@ +DataDirectory /var/lib/tor +Log notice stdout + +# SOCKS proxy for JoinMarket components +SocksPort 0.0.0.0:9050 + +# Control port for maker onion service handling +ControlPort 0.0.0.0:9051 +CookieAuthentication 1 +CookieAuthFile /var/lib/tor/control_auth_cookie + +# Hidden service for JoinMarket NG directory node +HiddenServiceDir /var/lib/tor/ng-directory +HiddenServicePort 5222 172.30.0.21:5223 +HiddenServiceVersion 3 diff --git a/package.json b/package.json index a2cde317b..4ac3549b5 100644 --- a/package.json +++ b/package.json @@ -13,6 +13,7 @@ "scripts": { "dev": "VITE_JAM_DEV_MODE=true vite", "dev:secondary": "JAM_BACKEND=jam-standalone JAM_API_PORT=29080 npm run dev", + "jm-ng:dev": "JMWALLETD_API_PORT=32183 JMWALLETD_WEBSOCKET_PORT=32283 JMOBWATCH_PORT=31800 npm run dev", "build": "tsc -b && vite build", "lint": "eslint .", "prettier": "prettier --write \"**/*.{js,jsx,ts,tsx,json,css,md}\"", diff --git a/vite.config.ts b/vite.config.ts index 3410ca627..288d95ddb 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -11,7 +11,9 @@ const { //PUBLIC_URL = '', // TODO: support serving from non-root? JAM_BACKEND = BACKEND_NATIVE, JMWALLETD_API_PORT = '28183', - JMWALLETD_WEBSOCKET_PORT = '28283', + // jm-ng jmwalletd serves WebSocket on the same port as the HTTPS API. + JMWALLETD_WEBSOCKET_PORT = '28183', + JMOBWATCH_PORT = '8080', JAM_API_PORT = undefined, } = process.env @@ -63,16 +65,16 @@ const serverConfigNative = (): ServerOptions => { }, }, '/obwatch': { - target: `https://127.0.0.1:${JMWALLETD_API_PORT}`, + target: `http://127.0.0.1:${JMOBWATCH_PORT}`, changeOrigin: true, secure: false, + rewrite: (p) => p.replace(/^\/obwatch/, ''), }, '/jmws': { target: `https://127.0.0.1:${JMWALLETD_WEBSOCKET_PORT}`, changeOrigin: true, secure: false, ws: true, - rewrite: (p) => p.replace(/^\/jmws/, ''), }, }, } From 767faa1c988b44adb6f595ae904f2291cb3792f9 Mon Sep 17 00:00:00 2001 From: m0wer Date: Wed, 22 Apr 2026 11:52:39 +0200 Subject: [PATCH 03/10] fix(tags): recognize cj-change, used-empty, and flagged UTXO statuses joinmarket-ng's WalletService emits four statuses jam's JmPlainTagValue didn't cover: 'cj-change' (deanonymising change from our CJ), 'used-empty' (previously-used address with zero balance), and 'flagged' (address shared in a CJ that later failed). They fell through to the generic 'default' badge with no distinguishing styling. Add them to JmPlainTagValue and the status->variant map. 'used-empty' maps to the existing 'used' variant, 'flagged' to 'reused' (both signal a used address the user should avoid reusing). Add a dedicated 'cj-change' badge variant (emerald) next to 'cj-out' so the deanonymising change output is visually distinct from an equal-amount CJ output. --- src/components/ui/jam/StatusBadge-variants.ts | 4 ++++ src/lib/tags.ts | 15 ++++++++++++++- 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/src/components/ui/jam/StatusBadge-variants.ts b/src/components/ui/jam/StatusBadge-variants.ts index 65a0943c5..43c480154 100644 --- a/src/components/ui/jam/StatusBadge-variants.ts +++ b/src/components/ui/jam/StatusBadge-variants.ts @@ -18,6 +18,10 @@ export const statusBadgeVariants = cva( variant: 'default', className: 'border-transparent light:bg-green-700 bg-green-700 text-white/90', }), + 'cj-change': badgeVariants({ + variant: 'default', + className: 'border-transparent light:bg-emerald-600 bg-emerald-700/70 text-white/90', + }), 'change-out': badgeVariants({ variant: 'default', className: 'border-transparent light:bg-yellow-300 bg-yellow-400 text-black', diff --git a/src/lib/tags.ts b/src/lib/tags.ts index 24d88ec8e..48ef9df33 100644 --- a/src/lib/tags.ts +++ b/src/lib/tags.ts @@ -5,7 +5,17 @@ import type { AddressSummary } from '@/context/JamWalletInfoContext' import type { Utxo } from '@/hooks/useQueryUtxos' import * as fb from './fidelityBondUtils' -type JmPlainTagValue = 'new' | 'used' | 'reused' | 'cj-out' | 'non-cj-change' | 'change-out' | 'deposit' +type JmPlainTagValue = + | 'new' + | 'used' + | 'reused' + | 'cj-out' + | 'cj-change' + | 'non-cj-change' + | 'change-out' + | 'deposit' + | 'used-empty' + | 'flagged' type AdditionalTagValue = 'locked' | 'pending' | 'frozen' type UtxoTagValue = JmPlainTagValue | AdditionalTagValue | 'bond' | string @@ -16,9 +26,12 @@ const JM_PLAIN_STATUS_TAG_VARIANTS: { [key in JmPlainTagValue]: StatusBadgeVaria used: 'used', reused: 'reused', 'cj-out': 'cj-out', + 'cj-change': 'cj-change', 'change-out': 'change-out', 'non-cj-change': 'non-cj-change', deposit: 'deposit', + 'used-empty': 'used', + flagged: 'reused', } const ADDITIONAL_STATUS_TAG_VARIANTS: { [key in AdditionalTagValue]: StatusBadgeVariant } = { From c2e6fb8fe23657ca5b4a01207dea6e65d1f4abe5 Mon Sep 17 00:00:00 2001 From: m0wer Date: Fri, 24 Apr 2026 10:22:01 +0200 Subject: [PATCH 04/10] fix(send): keep direct send available with maker Only block collaborative sends while the maker service is running. Direct sends stay enabled and the form explains why the collaborative toggle is unavailable. Changelog: Allow direct sends while the maker service is running --- src/components/send/SendForm.tsx | 13 +++++++++++-- src/components/send/SendPage.tsx | 5 ++++- src/i18n/locales/en/translation.json | 1 + 3 files changed, 16 insertions(+), 3 deletions(-) diff --git a/src/components/send/SendForm.tsx b/src/components/send/SendForm.tsx index bea5baaa3..1d6a251d7 100644 --- a/src/components/send/SendForm.tsx +++ b/src/components/send/SendForm.tsx @@ -286,6 +286,12 @@ interface SendFormProps { walletBalanceSummary: BalanceSummary addressSummary: AddressSummary disabled?: boolean + /** + * True while the maker service is running. jm-ng permits non-collaborative + * (direct) sends concurrently but rejects any new coinjoin or tumbler + * activity, so the collaborative toggle is hidden/disabled in that case. + */ + makerRunning?: boolean debug?: boolean } @@ -295,6 +301,7 @@ export function SendForm({ onSourceJarChange, sourceJarLabelButton, disabled, + makerRunning = false, feeConfigValues, forceCoinJoinEnabled = false, walletFileName, @@ -345,7 +352,7 @@ export function SendForm({ const isSweep = useWatch({ control, name: 'amount.isSweep' }) const isCoinJoin = useWatch({ control, name: 'isCoinJoin' }) const collaboratorCount = useWatch({ control, name: 'numCollaborators' }) - const isCoinJoinEnabled = forceCoinJoinEnabled || isCoinJoin === true + const isCoinJoinEnabled = !makerRunning && (forceCoinJoinEnabled || isCoinJoin === true) const destinationAddressInfo = useMemo(() => { try { @@ -719,7 +726,7 @@ export function SendForm({ }, ) }} - disabled={disabled} + disabled={disabled || makerRunning} /> + {makerRunning &&

{t('send.text_maker_running')}

} + {isCoinJoinEnabled && (
diff --git a/src/components/send/SendPage.tsx b/src/components/send/SendPage.tsx index 3e762ab70..0f95e9756 100644 --- a/src/components/send/SendPage.tsx +++ b/src/components/send/SendPage.tsx @@ -600,14 +600,17 @@ export const SendPage = ({ walletFileName }: SendPageProps) => { jars={jars} addressSummary={addressSummary} walletBalanceSummary={walletBalanceSummary} + // jm-ng allows non-collaborative (direct) sends while the maker service is + // running; only collaborative sends (coinjoin) conflict with it. The form + // disables the coinjoin toggle separately when ``makerRunning`` is true. disabled={ - jmSession?.maker_running || collaborativeFlowActive || jmSession?.rescanning || utxoSelectionDialog.isApplying || triggerNonCollaborativeTransaction.isPending || waitForUtxosToBeSpent.length > 0 } + makerRunning={jmSession?.maker_running === true} debug={isDeveloperMode} onSourceJarChange={utxoSelectionDialog.setSourceJarIndex} sourceJarLabelButton={ diff --git a/src/i18n/locales/en/translation.json b/src/i18n/locales/en/translation.json index f78bd922c..eba3e8285 100644 --- a/src/i18n/locales/en/translation.json +++ b/src/i18n/locales/en/translation.json @@ -410,6 +410,7 @@ "sending_options": "Sending options", "toggle_coinjoin": "Send as collaborative transaction", "toggle_coinjoin_subtitle": "Collaborative transactions improve the privacy of yourself and others.", + "text_maker_running": "Collaborative transactions are unavailable while the maker service is running. Stop earning to send collaboratively.", "label_tx_fees": "Mining fee", "button_send": "Send", "button_send_despite_warning": "Ignore warning & try send", From fb19f533d7a3d201dd34a308551be66b8a903b88 Mon Sep 17 00:00:00 2001 From: m0wer Date: Fri, 24 Apr 2026 10:22:01 +0200 Subject: [PATCH 05/10] docs(dev): explain jm-ng backend workflow --- README.md | 2 ++ docs/developing.md | 47 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 49 insertions(+) diff --git a/README.md b/README.md index 438ea406d..0a73ab63a 100644 --- a/README.md +++ b/README.md @@ -84,6 +84,8 @@ There are many ways you can contribute: testing, sharing ideas, writing document Want to get your hands dirty with code? Great! [CONTRIBUTING.md](CONTRIBUTING.md) and [docs/developing.md](docs/developing.md) are where it's at. +For local development against a separately running `joinmarket-ng` backend, start `jmwalletd` and the jm-ng orderbook watcher first, then run Jam with `npm run dev`. Jam's default Vite proxy targets `https://127.0.0.1:28183` for `/api` and `/jmws`, and `http://127.0.0.1:8080` for `/obwatch`. If you are using Jam's regtest jm-ng services instead, run `npm run jm-ng:dev`. + ## 🏛️ History This project builds upon the [jm-web-client](https://github.com/JoinMarket-Org/jm-web-client) which was developed by [Shobhitaa](https://github.com/shobhitaa), [Abhishek](https://github.com/abhishek0405), and [waxwing](https://github.com/AdamISZ) himself. Many people contributed over time, some of which are [listed here](https://github.com/joinmarket-webui/jam/graphs/contributors). diff --git a/docs/developing.md b/docs/developing.md index 11b72e17f..b46d3e082 100644 --- a/docs/developing.md +++ b/docs/developing.md @@ -8,6 +8,53 @@ A place to collect useful information for developers that doesn't really fit els For a complete development environment you need a local JoinMarket instance that the web UI can interact with. We provide a regtest environment that should give you everything needed to get started developing with JoinMarket. You can find details here: [docker/regtest/readme.md](../docker/regtest/readme.md). +## Running Jam Against JoinMarket-NG + +Jam v2 can talk directly to a separately running `jmwalletd` / orderbook watcher from `joinmarket-ng`. You do not need to run the Jam regtest compose or the reference implementation for this workflow. + +### Local dev against a manually started jm-ng backend + +1. Start `jmwalletd` from your `joinmarket-ng` checkout so it is reachable on `https://127.0.0.1:28183`. +2. Start the jm-ng orderbook watcher so it is reachable on `http://127.0.0.1:8080`. +3. In the Jam repo, run: + +```bash +npm run dev +``` + +This uses the default `native` backend mode and proxies: + +- `/api` -> `https://127.0.0.1:28183` +- `/jmws` -> `https://127.0.0.1:28183` +- `/obwatch` -> `http://127.0.0.1:8080` + +### Local dev against the Jam regtest jm-ng services + +If you are using Jam's own regtest environment, the initialized jm-ng services are exposed on different host ports. In that case run: + +```bash +npm run jm-ng:dev +``` + +That switches the Vite proxy to the jm-ng regtest ports exposed by `docker/regtest/docker-compose.yml`: + +- `JMWALLETD_API_PORT=32183` +- `JMWALLETD_WEBSOCKET_PORT=32283` +- `JMOBWATCH_PORT=31800` + +### Custom jm-ng ports + +If your separately running jm-ng services use different ports, you can override them directly: + +```bash +JMWALLETD_API_PORT=28183 \ +JMWALLETD_WEBSOCKET_PORT=28183 \ +JMOBWATCH_PORT=8080 \ +npm run dev +``` + +`jmwalletd` serves the HTTPS API and WebSocket on the same port in jm-ng, so `JMWALLETD_API_PORT` and `JMWALLETD_WEBSOCKET_PORT` are usually identical. + ## Linting We use Create React App's [default ESLint integration](https://create-react-app.dev/docs/setting-up-your-editor/#displaying-lint-output-in-the-editor). From 1a204ec0a75e590254fce4076e74123c15e4d523 Mon Sep 17 00:00:00 2001 From: m0wer Date: Fri, 24 Apr 2026 19:07:00 +0200 Subject: [PATCH 06/10] fix(auth): only clear auth on invalid_token 401s Previously every 401 response cleared the session and logged the user out. jm-ng returns 401 for service-state errors (e.g. POST /tumbler/stop when nothing is running), which should never drop the session. Inspect the WWW-Authenticate header and only clear auth when the server signals ``error="invalid_token"``. Changelog: Non-auth 401 responses no longer log the user out. --- src/lib/config.test.ts | 25 ++++++++++++++++++++++--- src/lib/config.ts | 8 +++++++- 2 files changed, 29 insertions(+), 4 deletions(-) diff --git a/src/lib/config.test.ts b/src/lib/config.test.ts index 98642f9a3..f17338e24 100644 --- a/src/lib/config.test.ts +++ b/src/lib/config.test.ts @@ -20,20 +20,39 @@ describe('unauthorizedResponseInterceptor', () => { }) }) - it('should clear auth on 401', () => { + it('should clear auth on invalid-token 401', () => { expect(authStore.getState().state?.auth?.token).toBe('tok') - unauthorizedResponseInterceptor(new Response(null, { status: 401 })) + unauthorizedResponseInterceptor( + new Response(null, { + status: 401, + headers: { 'WWW-Authenticate': 'Bearer, error="invalid_token", error_description="Invalid token."' }, + }), + ) expect(authStore.getState().state).toBeUndefined() }) it('should return the response after clearing', () => { - const response = new Response(null, { status: 401 }) + const response = new Response(null, { + status: 401, + headers: { 'WWW-Authenticate': 'Bearer, error="invalid_token", error_description="Invalid token."' }, + }) const result = unauthorizedResponseInterceptor(response) expect(result).toBe(response) }) + it('should not clear auth on non-auth 401 responses', () => { + unauthorizedResponseInterceptor(new Response(null, { status: 401 })) + unauthorizedResponseInterceptor( + new Response(null, { + status: 401, + headers: { 'WWW-Authenticate': 'Bearer, error="service_state", error_description="Not running."' }, + }), + ) + expect(authStore.getState().state?.auth?.token).toBe('tok') + }) + it('should not clear auth on non-401 responses', () => { unauthorizedResponseInterceptor(new Response(null, { status: 200 })) unauthorizedResponseInterceptor(new Response(null, { status: 403 })) diff --git a/src/lib/config.ts b/src/lib/config.ts index 00f63226a..a2e79bd83 100644 --- a/src/lib/config.ts +++ b/src/lib/config.ts @@ -42,7 +42,13 @@ const createJamAuthenticationMiddleware = () => { } export function unauthorizedResponseInterceptor(response: Response) { - if (response.status === 401) { + const wwwAuthenticate = response.headers.get('WWW-Authenticate') + const isInvalidToken = + response.status === 401 && + typeof wwwAuthenticate === 'string' && + wwwAuthenticate.toLowerCase().includes('error="invalid_token"') + + if (isInvalidToken) { authStore.getState().clear() queryClient.clear() } From 8dfd83f7c2e48a0607e4430be1636fc0bef3d54b Mon Sep 17 00:00:00 2001 From: m0wer Date: Fri, 24 Apr 2026 19:06:53 +0200 Subject: [PATCH 07/10] fix(dev): proxy /jmws to the jm-ng HTTPS port jm-ng serves WebSocket on the same port as the HTTPS API. The `jm-ng:dev` script used to override JMWALLETD_WEBSOCKET_PORT to 32283, but the regtest stack exposes that port without a matching listener, so vite proxied WebSocket upgrades into a dead port and the handshake returned empty. Default the env var to JMWALLETD_API_PORT and drop the override. Changelog: Fix WebSocket proxying in the local jm-ng dev setup. --- package.json | 2 +- vite.config.ts | 7 +++++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index 4ac3549b5..59ef8880f 100644 --- a/package.json +++ b/package.json @@ -13,7 +13,7 @@ "scripts": { "dev": "VITE_JAM_DEV_MODE=true vite", "dev:secondary": "JAM_BACKEND=jam-standalone JAM_API_PORT=29080 npm run dev", - "jm-ng:dev": "JMWALLETD_API_PORT=32183 JMWALLETD_WEBSOCKET_PORT=32283 JMOBWATCH_PORT=31800 npm run dev", + "jm-ng:dev": "JMWALLETD_API_PORT=32183 JMOBWATCH_PORT=31800 npm run dev", "build": "tsc -b && vite build", "lint": "eslint .", "prettier": "prettier --write \"**/*.{js,jsx,ts,tsx,json,css,md}\"", diff --git a/vite.config.ts b/vite.config.ts index 288d95ddb..f2593b94c 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -11,8 +11,11 @@ const { //PUBLIC_URL = '', // TODO: support serving from non-root? JAM_BACKEND = BACKEND_NATIVE, JMWALLETD_API_PORT = '28183', - // jm-ng jmwalletd serves WebSocket on the same port as the HTTPS API. - JMWALLETD_WEBSOCKET_PORT = '28183', + // jm-ng jmwalletd serves WebSocket on the same port as the HTTPS API, so the + // default mirrors JMWALLETD_API_PORT. The env var remains overridable for + // setups that still run the reference JoinMarket ``wss_port`` on a separate + // TCP port (e.g. 28283). + JMWALLETD_WEBSOCKET_PORT = JMWALLETD_API_PORT, JMOBWATCH_PORT = '8080', JAM_API_PORT = undefined, } = process.env From 91f1a64bfe1049d73db4349cce4a848314e5a3bb Mon Sep 17 00:00:00 2001 From: m0wer Date: Sat, 25 Apr 2026 15:11:37 +0200 Subject: [PATCH 08/10] fix(dev): correct jm-ng websocket port and proxy targets jm-ng `jmwalletd` exposes the HTTPS API and WebSocket on separate TCP ports (28183 and 28283 by default), not the same port as previously assumed. Restore: - the `JMWALLETD_WEBSOCKET_PORT=28283` default in `vite.config.ts` - the `JMWALLETD_WEBSOCKET_PORT=32283` override in `npm run jm-ng:dev`, which matches the regtest container's port mapping Update the README and developer docs to reflect that both ports must be set when overriding for a custom jm-ng setup. --- README.md | 2 +- docs/developing.md | 8 ++++---- package.json | 2 +- vite.config.ts | 6 +----- 4 files changed, 7 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index 0a73ab63a..49448435d 100644 --- a/README.md +++ b/README.md @@ -84,7 +84,7 @@ There are many ways you can contribute: testing, sharing ideas, writing document Want to get your hands dirty with code? Great! [CONTRIBUTING.md](CONTRIBUTING.md) and [docs/developing.md](docs/developing.md) are where it's at. -For local development against a separately running `joinmarket-ng` backend, start `jmwalletd` and the jm-ng orderbook watcher first, then run Jam with `npm run dev`. Jam's default Vite proxy targets `https://127.0.0.1:28183` for `/api` and `/jmws`, and `http://127.0.0.1:8080` for `/obwatch`. If you are using Jam's regtest jm-ng services instead, run `npm run jm-ng:dev`. +For local development against a separately running `joinmarket-ng` backend, start `jmwalletd` and the jm-ng orderbook watcher first, then run Jam with `npm run dev`. Jam's default Vite proxy targets `https://127.0.0.1:28183` for `/api`, `wss://127.0.0.1:28283` for `/jmws`, and `http://127.0.0.1:8080` for `/obwatch`. If you are using Jam's regtest jm-ng services instead, run `npm run jm-ng:dev`. ## 🏛️ History diff --git a/docs/developing.md b/docs/developing.md index b46d3e082..48bc65e90 100644 --- a/docs/developing.md +++ b/docs/developing.md @@ -14,7 +14,7 @@ Jam v2 can talk directly to a separately running `jmwalletd` / orderbook watcher ### Local dev against a manually started jm-ng backend -1. Start `jmwalletd` from your `joinmarket-ng` checkout so it is reachable on `https://127.0.0.1:28183`. +1. Start `jmwalletd` from your `joinmarket-ng` checkout so it is reachable on `https://127.0.0.1:28183` (HTTPS API) and `wss://127.0.0.1:28283` (WebSocket). 2. Start the jm-ng orderbook watcher so it is reachable on `http://127.0.0.1:8080`. 3. In the Jam repo, run: @@ -25,7 +25,7 @@ npm run dev This uses the default `native` backend mode and proxies: - `/api` -> `https://127.0.0.1:28183` -- `/jmws` -> `https://127.0.0.1:28183` +- `/jmws` -> `wss://127.0.0.1:28283` - `/obwatch` -> `http://127.0.0.1:8080` ### Local dev against the Jam regtest jm-ng services @@ -48,12 +48,12 @@ If your separately running jm-ng services use different ports, you can override ```bash JMWALLETD_API_PORT=28183 \ -JMWALLETD_WEBSOCKET_PORT=28183 \ +JMWALLETD_WEBSOCKET_PORT=28283 \ JMOBWATCH_PORT=8080 \ npm run dev ``` -`jmwalletd` serves the HTTPS API and WebSocket on the same port in jm-ng, so `JMWALLETD_API_PORT` and `JMWALLETD_WEBSOCKET_PORT` are usually identical. +`jmwalletd` exposes the HTTPS API and the WebSocket on separate TCP ports in jm-ng (`28183` and `28283` by default), so set both env vars when overriding. ## Linting diff --git a/package.json b/package.json index 59ef8880f..4ac3549b5 100644 --- a/package.json +++ b/package.json @@ -13,7 +13,7 @@ "scripts": { "dev": "VITE_JAM_DEV_MODE=true vite", "dev:secondary": "JAM_BACKEND=jam-standalone JAM_API_PORT=29080 npm run dev", - "jm-ng:dev": "JMWALLETD_API_PORT=32183 JMOBWATCH_PORT=31800 npm run dev", + "jm-ng:dev": "JMWALLETD_API_PORT=32183 JMWALLETD_WEBSOCKET_PORT=32283 JMOBWATCH_PORT=31800 npm run dev", "build": "tsc -b && vite build", "lint": "eslint .", "prettier": "prettier --write \"**/*.{js,jsx,ts,tsx,json,css,md}\"", diff --git a/vite.config.ts b/vite.config.ts index f2593b94c..4057d6d54 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -11,11 +11,7 @@ const { //PUBLIC_URL = '', // TODO: support serving from non-root? JAM_BACKEND = BACKEND_NATIVE, JMWALLETD_API_PORT = '28183', - // jm-ng jmwalletd serves WebSocket on the same port as the HTTPS API, so the - // default mirrors JMWALLETD_API_PORT. The env var remains overridable for - // setups that still run the reference JoinMarket ``wss_port`` on a separate - // TCP port (e.g. 28283). - JMWALLETD_WEBSOCKET_PORT = JMWALLETD_API_PORT, + JMWALLETD_WEBSOCKET_PORT = '28283', JMOBWATCH_PORT = '8080', JAM_API_PORT = undefined, } = process.env From 00f1e0eb53a3a401a7bf98143078f74fe55f47c3 Mon Sep 17 00:00:00 2001 From: Thebora Kompanioni Date: Sun, 3 May 2026 14:23:56 +0200 Subject: [PATCH 09/10] fix(dev): correct jm-ng websocket port (#1237) --- docker/regtest/docker-compose.yml | 8 ++------ docker/regtest/readme.md | 4 ++-- package.json | 2 +- vite.config.ts | 6 +++++- 4 files changed, 10 insertions(+), 10 deletions(-) diff --git a/docker/regtest/docker-compose.yml b/docker/regtest/docker-compose.yml index f772fd862..1857d98ff 100644 --- a/docker/regtest/docker-compose.yml +++ b/docker/regtest/docker-compose.yml @@ -92,9 +92,7 @@ services: BITCOIN__RPC_PASSWORD: regtest BITCOIN__DESCRIPTOR_WALLET_NAME: jm_quaternary ports: - - '31080:28183' - - '31283:28283' - - '31183:28183' # exposed for "init setup" routine + - '31183:28183' volumes: - 'joinmarket4_datadir:/root/.joinmarket-ng' - 'initializer_datadir:/root/.regtest-initializer' @@ -119,9 +117,7 @@ services: BITCOIN__RPC_PASSWORD: regtest BITCOIN__DESCRIPTOR_WALLET_NAME: jm_quinary ports: - - '32080:28183' - - '32283:28283' - - '32183:28183' # exposed for "init setup" routine + - '32183:28183' volumes: - 'joinmarket5_datadir:/root/.joinmarket-ng' - 'tor_datadir:/var/lib/tor:ro' diff --git a/docker/regtest/readme.md b/docker/regtest/readme.md index b579d2bb9..512950cb6 100644 --- a/docker/regtest/readme.md +++ b/docker/regtest/readme.md @@ -109,8 +109,8 @@ Use `:main` for latest unstable/unreleased changes and `:latest` for the latest The second JoinMarket container is exposed on port `29080`. The third container is exposed on port `30080`. -The first JoinMarket NG container is exposed on ports `31080` (API) and `31283` (websocket). -The second JoinMarket NG container is exposed on ports `32080` (API) and `32283` (websocket). +The first JoinMarket NG container is exposed on ports `31183` (API and websocket). +The second JoinMarket NG container is exposed on ports `32183` (API and websocket). This is useful if you want to perform regression tests across mixed implementations. The setup includes both a reference directory node and a JoinMarket NG directory server. They implement the same onion directory protocol and run side-by-side for compatibility testing. diff --git a/package.json b/package.json index 4ac3549b5..d399f84ab 100644 --- a/package.json +++ b/package.json @@ -13,7 +13,7 @@ "scripts": { "dev": "VITE_JAM_DEV_MODE=true vite", "dev:secondary": "JAM_BACKEND=jam-standalone JAM_API_PORT=29080 npm run dev", - "jm-ng:dev": "JMWALLETD_API_PORT=32183 JMWALLETD_WEBSOCKET_PORT=32283 JMOBWATCH_PORT=31800 npm run dev", + "jm-ng:dev": "JMWALLETD_API_PORT=32183 JMWALLETD_WEBSOCKET_PORT=32183 JMWALLETD_WEBSOCKET_API_PATH=/api/v1/ws JMOBWATCH_PORT=31800 npm run dev", "build": "tsc -b && vite build", "lint": "eslint .", "prettier": "prettier --write \"**/*.{js,jsx,ts,tsx,json,css,md}\"", diff --git a/vite.config.ts b/vite.config.ts index 4057d6d54..0c00c170c 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -12,7 +12,10 @@ const { JAM_BACKEND = BACKEND_NATIVE, JMWALLETD_API_PORT = '28183', JMWALLETD_WEBSOCKET_PORT = '28283', - JMOBWATCH_PORT = '8080', + // - "joinmarket-clientserver" listens on `wss://${host}:${JMWALLETD_WEBSOCKET_PORT}` + // - "joinmarket-ng" listens on `wss://${host}:${JMWALLETD_WEBSOCKET_PORT}/api/v1/ws` + JMWALLETD_WEBSOCKET_API_PATH = '', + JMOBWATCH_PORT = '62601', JAM_API_PORT = undefined, } = process.env @@ -74,6 +77,7 @@ const serverConfigNative = (): ServerOptions => { changeOrigin: true, secure: false, ws: true, + rewrite: (p) => p.replace(/^\/jmws/, JMWALLETD_WEBSOCKET_API_PATH), }, }, } From 7581bf3480a47e27c45afd758410ce68615f262b Mon Sep 17 00:00:00 2001 From: Thebora Kompanioni Date: Sun, 3 May 2026 18:27:26 +0200 Subject: [PATCH 10/10] chore(dev): distinct server configs for various backends (#1238) --- package.json | 2 +- vite.config.ts | 156 ++++++++++++++++++++++++++++++++++++++++--------- 2 files changed, 130 insertions(+), 28 deletions(-) diff --git a/package.json b/package.json index d399f84ab..7ce9a69e3 100644 --- a/package.json +++ b/package.json @@ -13,7 +13,7 @@ "scripts": { "dev": "VITE_JAM_DEV_MODE=true vite", "dev:secondary": "JAM_BACKEND=jam-standalone JAM_API_PORT=29080 npm run dev", - "jm-ng:dev": "JMWALLETD_API_PORT=32183 JMWALLETD_WEBSOCKET_PORT=32183 JMWALLETD_WEBSOCKET_API_PATH=/api/v1/ws JMOBWATCH_PORT=31800 npm run dev", + "jm-ng:dev": "JAM_BACKEND=joinmarket-ng JMWALLETD_API_PORT=32183 JMWALLETD_WEBSOCKET_PORT=32183 JMOBWATCH_PORT=31800 npm run dev", "build": "tsc -b && vite build", "lint": "eslint .", "prettier": "prettier --write \"**/*.{js,jsx,ts,tsx,json,css,md}\"", diff --git a/vite.config.ts b/vite.config.ts index 0c00c170c..08bbabb12 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -3,33 +3,64 @@ import react from '@vitejs/plugin-react' import path from 'node:path' import { type ServerOptions, type UserConfig, defineConfig } from 'vite' -const BACKEND_NATIVE = 'native' -const BACKEND_STANDALONE = 'jam-standalone' -const SUPPORTED_BACKENDS = [BACKEND_NATIVE, BACKEND_STANDALONE] +const BACKEND_JOINMARKET_CLIENTSERVER_NATIVE = 'joinmarket-clientserver' +const BACKEND_JOINMARKET_NG_NATIVE = 'joinmarket-ng' +const BACKEND_JAM_STANDALONE = 'jam-standalone' + +type SupportedBackend = + | typeof BACKEND_JOINMARKET_CLIENTSERVER_NATIVE + | typeof BACKEND_JOINMARKET_NG_NATIVE + | typeof BACKEND_JAM_STANDALONE + +const SUPPORTED_BACKENDS: SupportedBackend[] = [ + BACKEND_JOINMARKET_CLIENTSERVER_NATIVE, + BACKEND_JOINMARKET_NG_NATIVE, + BACKEND_JAM_STANDALONE, +] + +function isSupportedBackend(val: unknown): val is SupportedBackend { + return SUPPORTED_BACKENDS.includes(val as SupportedBackend) === true +} + +type BackendConfigEnvironmentVariables = { + JMWALLETD_API_PORT: number + JMWALLETD_WEBSOCKET_PORT: number + JMOBWATCH_PORT: number + JAM_API_PORT?: number // (optioal) Jam specific API endpoint (only present in jam-standalone) +} const { //PUBLIC_URL = '', // TODO: support serving from non-root? - JAM_BACKEND = BACKEND_NATIVE, - JMWALLETD_API_PORT = '28183', - JMWALLETD_WEBSOCKET_PORT = '28283', - // - "joinmarket-clientserver" listens on `wss://${host}:${JMWALLETD_WEBSOCKET_PORT}` - // - "joinmarket-ng" listens on `wss://${host}:${JMWALLETD_WEBSOCKET_PORT}/api/v1/ws` - JMWALLETD_WEBSOCKET_API_PATH = '', - JMOBWATCH_PORT = '62601', + JAM_BACKEND = BACKEND_JOINMARKET_CLIENTSERVER_NATIVE, JAM_API_PORT = undefined, } = process.env +const BACKEND_ENV_JOINMARKET_CLIENTSERVER_DEFAULT: BackendConfigEnvironmentVariables = { + JMWALLETD_API_PORT: 28183, + JMWALLETD_WEBSOCKET_PORT: 28283, + JMOBWATCH_PORT: 62601, +} + +const BACKEND_ENV_JOINMARKET_NG_DEFAULT: BackendConfigEnvironmentVariables = { + JMWALLETD_API_PORT: 28183, + JMWALLETD_WEBSOCKET_PORT: 28183, + JMOBWATCH_PORT: 8000, +} + +const createJamStandloneConfigEnvironmentVariables = (jamApiPort: number): BackendConfigEnvironmentVariables => ({ + JMWALLETD_API_PORT: jamApiPort, + JMWALLETD_WEBSOCKET_PORT: jamApiPort, + JMOBWATCH_PORT: jamApiPort, + JAM_API_PORT: jamApiPort, +}) + // https://vite.dev/config/ export default defineConfig((): UserConfig => { - if (!SUPPORTED_BACKENDS.includes(JAM_BACKEND)) { + if (!isSupportedBackend(JAM_BACKEND)) { throw new Error(`Unsupported backend: Use one of [${SUPPORTED_BACKENDS.join(', ')}]`) } - if (JAM_BACKEND === BACKEND_STANDALONE && JAM_API_PORT === undefined) { - throw new Error('Unsupported port: Please specify a valid JAM_API_PORT') - } - - const server = JAM_BACKEND === BACKEND_NATIVE ? serverConfigNative() : serverConfigStandalone() + const server = createServer(JAM_BACKEND) return { plugins: [react(), tailwindcss()], resolve: { @@ -44,18 +75,84 @@ export default defineConfig((): UserConfig => { } }) +const createServer = (backend: SupportedBackend): ServerOptions => { + switch (backend) { + case BACKEND_JOINMARKET_CLIENTSERVER_NATIVE: { + return createServerConfigJoinmarketClientServerNative({ + ...BACKEND_ENV_JOINMARKET_CLIENTSERVER_DEFAULT, + ...process.env, + }) + } + case BACKEND_JOINMARKET_NG_NATIVE: { + return createServerConfigJoinmarketNgNative({ + ...BACKEND_ENV_JOINMARKET_NG_DEFAULT, + ...process.env, + }) + } + case BACKEND_JAM_STANDALONE: { + if (JAM_API_PORT === undefined) { + throw new Error('Unsupported port: Please specify a valid JAM_API_PORT') + } + return createServerConfigJamStandalone({ + ...createJamStandloneConfigEnvironmentVariables(Number(JAM_API_PORT)), + ...process.env, + }) + } + // No default + } + + throw new Error(`Unsupported backend: Use one of [${SUPPORTED_BACKENDS.join(', ')}]`) +} /** + * Server config for "joinmarket-clientserver". * The "native" installation *does not run* a webserver! * Requests must be adapted: * - proxy API requests to correct target service * - rewrite paths to match target service paths * - translate header "x-jm-authorization" to "Authorization" */ -const serverConfigNative = (): ServerOptions => { +const createServerConfigJoinmarketClientServerNative = (config: BackendConfigEnvironmentVariables): ServerOptions => { + return { + proxy: { + '/api': { + target: `https://127.0.0.1:${config.JMWALLETD_API_PORT}`, + changeOrigin: true, + secure: false, + configure: (proxy, _options) => { + proxy.on('proxyReq', (proxyRequest, request, _response) => { + if (request.headers['x-jm-authorization']) { + proxyRequest.setHeader('Authorization', request.headers['x-jm-authorization']) + } + }) + }, + }, + '/obwatch': { + target: `http://127.0.0.1:${config.JMOBWATCH_PORT}`, + changeOrigin: true, + secure: false, + rewrite: (p) => p.replace(/^\/obwatch/, ''), + }, + '/jmws': { + target: `https://127.0.0.1:${config.JMWALLETD_WEBSOCKET_PORT}`, + changeOrigin: true, + secure: false, + ws: true, + rewrite: (p) => p.replace(/^\/jmws/, ''), + }, + }, + } +} + +/** + * Server config for "joinmarket-ng". + * Similar to `createServerConfigJoinmarketClientServerNative`, but own function to keep concerns seperated: + * Changes are expected as joinmarket-ng keeps maturing. + */ +const createServerConfigJoinmarketNgNative = (config: BackendConfigEnvironmentVariables): ServerOptions => { return { proxy: { '/api': { - target: `https://127.0.0.1:${JMWALLETD_API_PORT}`, + target: `https://127.0.0.1:${config.JMWALLETD_API_PORT}`, changeOrigin: true, secure: false, configure: (proxy, _options) => { @@ -67,48 +164,53 @@ const serverConfigNative = (): ServerOptions => { }, }, '/obwatch': { - target: `http://127.0.0.1:${JMOBWATCH_PORT}`, + target: `http://127.0.0.1:${config.JMOBWATCH_PORT}`, changeOrigin: true, secure: false, rewrite: (p) => p.replace(/^\/obwatch/, ''), }, '/jmws': { - target: `https://127.0.0.1:${JMWALLETD_WEBSOCKET_PORT}`, + target: `https://127.0.0.1:${config.JMWALLETD_WEBSOCKET_PORT}`, changeOrigin: true, secure: false, ws: true, - rewrite: (p) => p.replace(/^\/jmws/, JMWALLETD_WEBSOCKET_API_PATH), + rewrite: (p) => p.replace(/^\/jmws/, '/api/v1/ws'), }, }, } } /** - * `standalone` backend has a webserver ("Jam API") running! + * Server config for "jam-standalone" (Jam Docker image). + * `standalone` backend has a webserver ("Jam API") running and provides a normalized API for the underlying backend. * Requests must be adapted: * - proxy all API requests to "Jam API" */ -const serverConfigStandalone = (): ServerOptions => { +const createServerConfigJamStandalone = (config: BackendConfigEnvironmentVariables): ServerOptions => { + if (config.JAM_API_PORT === undefined) { + throw new Error('Unsupported port: Please specify a valid JAM_API_PORT') + } + return { proxy: { '/api': { - target: `http://127.0.0.1:${JAM_API_PORT}`, + target: `http://127.0.0.1:${config.JMWALLETD_API_PORT}`, changeOrigin: true, secure: false, }, '/obwatch': { - target: `http://127.0.0.1:${JAM_API_PORT}`, + target: `http://127.0.0.1:${config.JMOBWATCH_PORT}`, changeOrigin: true, secure: false, }, '/jmws': { - target: `http://127.0.0.1:${JAM_API_PORT}`, + target: `http://127.0.0.1:${config.JMWALLETD_WEBSOCKET_PORT}`, changeOrigin: true, secure: false, ws: true, }, '/jam': { - target: `http://127.0.0.1:${JAM_API_PORT}`, + target: `http://127.0.0.1:${config.JAM_API_PORT}`, changeOrigin: true, secure: false, },