# Daemon and CLI

`joinstrd` runs joinstr in the background against a Bitcoin Core wallet and exposes a REST API on localhost. `joinstr-cli` is a command line client for that API. Together they let you run coinjoins on a headless server or build joinstr into other software over HTTP.

| | |
|---|---|
| Requires | Python 3, Bitcoin Core with RPC, Tor |
| Transport | Tor only |
| Source | [gitlab.com/invincible-privacy/joinstr/-/tree/main/cli](https://gitlab.com/invincible-privacy/joinstr/-/tree/main/cli) |

## Install

```sh
git clone https://gitlab.com/invincible-privacy/joinstr.git
cd joinstr
pip install -r cli/requirements.txt
mkdir -p ~/.joinstr
cp cli/config.example.ini ~/.joinstr/config.ini
chmod 600 ~/.joinstr/config.ini
```

## Configure

`~/.joinstr/config.ini` holds the node connection, the relay and the Tor proxy. The daemon warns if other users can read the file.

```ini
[rpc]
url = http://127.0.0.1:38332
user = <rpc user>
password = <rpc password>
wallet = <wallet name, empty for the default wallet>

[nostr]
relay = wss://nos.lol

[tor]
host = 127.0.0.1
port = 9050
```

The example uses the signet RPC port. Use `8332` for mainnet.

Tor is required. At start the daemon checks through the proxy that Tor works and exits if it does not. All relay and mempool.space traffic goes through Tor.

| Environment variable | Default | Meaning |
|---|---|---|
| `JOINSTR_DIR` | `~/.joinstr` | Directory with `config.ini` and state |
| `JOINSTRD_HOST` | `127.0.0.1` | Address the daemon binds |
| `JOINSTRD_PORT` | `8000` | Port the daemon listens on |
| `JOINSTR_API_URL` | `http://127.0.0.1:8000` | Daemon URL used by the CLI |

## Run

```sh
python cli/joinstrd.py
```

The daemon only answers requests whose `Host` header is a loopback address. Interactive API docs are served at `http://127.0.0.1:8000/docs` and `/redoc`, with the schema at `/openapi.json`.

## CLI

```sh
python cli/joinstr-cli.py <command> [options]
```

| Command | What it does |
|---|---|
| `create --denomination <BTC> --peers <n> [--timeout <seconds>]` | Create a pool. Timeout defaults to 3600 seconds. |
| `list [--my \| --others]` | List your pools or open pools on the relay |
| `join [--pool-id <id>]` | Join a pool |
| `output [--address <addr>] [--pool-id <id>]` | Register an output if it was not done automatically |
| `input [--pool-id <id>] [--txid <txid> --vout <n>]` | Pick and sign the coin for the active pool |
| `history` | Show finished coinjoins |
| `stop` | Stop the current operation |

The CLI signs your input with your node (`walletprocesspsbt` with `ALL|ANYONECANPAY`) and hands the signed PSBT to the daemon. It reads the `[rpc]` section of the same `config.ini`.

### A typical round

```sh
# creator
python cli/joinstr-cli.py create --denomination 0.001 --peers 3

# every other peer
python cli/joinstr-cli.py list --others
python cli/joinstr-cli.py join --pool-id <id>

# everyone, once all outputs are registered
python cli/joinstr-cli.py input --txid <txid> --vout <n>
```

## REST API

All endpoints are on `http://127.0.0.1:8000`. The full schema is at `/openapi.json`.

| Method | Path | Purpose |
|---|---|---|
| `GET` | `/status` | Daemon status |
| `GET` | `/pools/my` | Pools you created or joined |
| `GET` | `/pools/others` | Open pools on the relay |
| `POST` | `/pool/create` | Create a pool |
| `POST` | `/pool/join` | Join a pool |
| `GET` | `/pool/info` | Details of the active pool |
| `GET` | `/pool/status` | Phase of the active pool |
| `POST` | `/pool/register-output` | Register an output address |
| `GET` | `/pool/my-output` | Your registered output |
| `GET` | `/pool/outputs` | All registered outputs |
| `POST` | `/pool/register-input` | Register a signed input PSBT |
| `GET` | `/pool/my-input` | Your registered input |
| `GET` | `/pool/inputs` | All registered inputs |
| `GET` | `/history` | Finished coinjoins |
| `POST` | `/stop` | Stop the current operation |

`/pool/register-input` takes a PSBT that is already signed, so the key can live anywhere. A hardware or air gapped signer can sign the registration PSBT with `SIGHASH_ALL|ANYONECANPAY` and the daemon does the rest.

## Fees and broadcast

- The pool fee rate comes from mempool.space (`hourFee`) over Tor, with 5 sat/vB as a fallback.
- A joiner refuses pools with a fee rate above 100 sat/vB and rounds that would cost more than 15000 sats.
- The final transaction is broadcast through your node. If the node rejects it for a reason other than its own policy, the daemon tries mempool.space over Tor on mainnet and signet.
