diff --git a/README.md b/README.md index 438ea406d..49448435d 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`, `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 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/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/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..ab5907e7c 100644 --- a/docker/regtest/docker-compose-common.yml +++ b/docker/regtest/docker-compose-common.yml @@ -1,28 +1,31 @@ - services: - joinmarket_native: build: context: ./dockerfile-deps/joinmarket/latest dockerfile: Dockerfile restart: unless-stopped environment: - ENSURE_WALLET: "true" - REMOVE_LOCK_FILES: "true" + 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_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"] + test: ['CMD', 'supervisorctl', 'status'] interval: 10s timeout: 10s retries: 20 @@ -35,20 +38,105 @@ services: dockerfile: Dockerfile restart: unless-stopped environment: - ENSURE_WALLET: "true" - REMOVE_LOCK_FILES: "true" + 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_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 + - 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: + 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: 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: + - 8000 # obwatch - 28183 # jmwalletd api + - 28283 # jmwalletd websocket + healthcheck: + 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 + start_period: 60s + start_interval: 3s + + joinmarket_ng_peer: + image: ghcr.io/joinmarket-ng/joinmarket-ng/jmwalletd:main + restart: unless-stopped + environment: + 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: 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 + - 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..1857d98ff 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: @@ -12,12 +10,12 @@ services: jm_rpc_user: joinmarket jm_rpc_password: joinmarket ports: - - "62601:62601" - - "28183:28183" - - "28283:28283" + - '62601:62601' + - '28183:28183' + - '28283:28283' volumes: - - "joinmarket_datadir:/root/.joinmarket" - - "initializer_datadir:/root/.regtest-initializer" + - 'joinmarket_datadir:/root/.joinmarket' + - 'initializer_datadir:/root/.regtest-initializer' depends_on: bitcoind: condition: service_healthy @@ -25,6 +23,8 @@ services: condition: service_started irc: condition: service_started + tor: + condition: service_healthy joinmarket2: container_name: jm_regtest_joinmarket2 @@ -37,11 +37,12 @@ services: jm_rpc_wallet_file: jm_secondary jm_rpc_cookie_file: /root/.bitcoin_data/regtest/.cookie ports: - - "29080:80" - - "29183:28183" # exposed for "init setup" routine + - '29080:80' + - '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' + - 'bitcoin_datadir:/root/.bitcoin_data' # mount bitcoind dir to access .cookie file + - 'tor_datadir:/var/lib/tor:ro' depends_on: bitcoind: condition: service_healthy @@ -49,6 +50,8 @@ services: condition: service_started irc: condition: service_started + tor: + condition: service_healthy joinmarket3: container_name: jm_regtest_joinmarket3 @@ -57,16 +60,17 @@ services: service: joinmarket_jam_standalone environment: READY_FILE: /root/.regtest-initializer/btc_fully_synched - WAIT_FOR_BITCOIND: "false" + WAIT_FOR_BITCOIND: 'false' jm_rpc_wallet_file: jm_tertiary jm_rpc_user: joinmarket3 jm_rpc_password: joinmarket3 ports: - - "30080:80" - - "30183:28183" # exposed for "init setup" routine + - '30080:80' + - '30183:28183' # exposed for "init setup" routine volumes: - - "joinmarket3_datadir:/root/.joinmarket" - - "initializer_datadir:/root/.regtest-initializer" + - 'joinmarket3_datadir:/root/.joinmarket' + - 'initializer_datadir:/root/.regtest-initializer' + - 'tor_datadir:/var/lib/tor:ro' depends_on: bitcoind: condition: service_healthy @@ -74,6 +78,58 @@ services: 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: + - '31183:28183' + 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: + - '32183:28183' + 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_ng_orderbook_watcher: + condition: service_healthy + tor: + condition: service_healthy joinmarket_directory_node: container_name: jm_regtest_joinmarket_directory_node @@ -83,19 +139,115 @@ services: 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_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" + - '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_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: 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 + timeout: 10s + retries: 20 + start_period: 60s + start_interval: 3s + depends_on: + joinmarket_ng_directory_node: + condition: service_healthy + 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: testnet + DIRECTORY_SERVER__HOST: 0.0.0.0 + DIRECTORY_SERVER__PORT: 5223 + 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', 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 @@ -104,7 +256,7 @@ services: expose: - 6667 volumes: - - "irc_datadir:/ircd" + - 'irc_datadir:/ircd' bitcoind: container_name: jm_regtest_bitcoind @@ -149,12 +301,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 +323,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 +346,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 +373,7 @@ services: BTCEXP_NO_RATES: 'true' BTCEXP_RPC_ALLOWALL: 'true' ports: - - "3002:3002" + - '3002:3002' depends_on: bitcoind: condition: service_healthy @@ -224,5 +385,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 4803cca7c..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 } @@ -123,6 +127,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..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) @@ -65,7 +69,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" @@ -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 de92e8937..697144a89 100755 --- a/docker/regtest/prepare-setup.sh +++ b/docker/regtest/prepare-setup.sh @@ -29,21 +29,34 @@ 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}" +# 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") -ONION_ADDRESS=`cat ${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 ! [[ "${ONION_ADDRESS}" == *.onion ]]; then - die "Invalid argument: Could not find onion address in ${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 -ONION_ADDRESS_WITH_PORT="${ONION_ADDRESS}:5222" +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=${ONION_ADDRESS_WITH_PORT} +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 c50461630..512950cb6 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 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,16 +98,30 @@ 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. +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`. +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. +All JoinMarket components (reference containers, JoinMarket NG containers, and orderbook watcher) are configured with both directory nodes via `JM_ALL_DIRECTORY_NODES`. -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. -This is useful if you want to perform regression tests. +All directory access is Tor-only in this setup: -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. +- 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 @@ -95,7 +130,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 +149,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 +257,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/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/docs/developing.md b/docs/developing.md index 11b72e17f..48bc65e90 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` (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: + +```bash +npm run dev +``` + +This uses the default `native` backend mode and proxies: + +- `/api` -> `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 + +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=28283 \ +JMOBWATCH_PORT=8080 \ +npm run dev +``` + +`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 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). diff --git a/package.json b/package.json index a2cde317b..7ce9a69e3 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": "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/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/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/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", 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() } 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 } = { diff --git a/vite.config.ts b/vite.config.ts index b77da4c66..08bbabb12 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -3,30 +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', - 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: { @@ -41,18 +75,47 @@ 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:${JMWALLETD_API_PORT}`, + target: `https://127.0.0.1:${config.JMWALLETD_API_PORT}`, changeOrigin: true, secure: false, configure: (proxy, _options) => { @@ -64,13 +127,13 @@ 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, @@ -81,31 +144,73 @@ const serverConfigNative = (): ServerOptions => { } /** - * `standalone` backend has a webserver ("Jam API") running! + * 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:${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/, '/api/v1/ws'), + }, + }, + } +} + +/** + * 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, },