did-btcr2-js

@did-btcr2/cli

Command-line interface for the did:btcr2 DID method.

Part of the did-btcr2-js monorepo.

Summary

This package provides the btcr2 CLI for creating, resolving, updating, and deactivating did:btcr2 decentralized identifiers. It also manages an encrypted keystore of keypairs, reads and writes CLI configuration and profiles, and prints shell completion scripts. It wraps the @did-btcr2/api SDK via dependency injection, using commander.js for argument parsing.

Out of the box, btcr2 resolve works with zero configuration. The Bitcoin network is derived from the DID itself, and public endpoints (mempool.space, ipfs.io) are used as defaults. Override endpoints via CLI flags, environment variables, or a config file.

Signing operations (update, deactivate, and generated create keys) read secret keys from an encrypted on-disk keystore. Choose a key with --signing-key <ref> or set an active key with btcr2 key use <ref>.

Install

npm install -g @did-btcr2/cli

Or with pnpm:

pnpm add -g @did-btcr2/cli

Requires Node.js >= 22.

Without installing globally, run directly via npx:

npx @did-btcr2/cli resolve -i did:btcr2:k1qq...

Commands

Command Alias Description
init - Set up the btcr2 home: create the directory, a default config, and establish the keystore
quickstart - One-command onboarding: init + record the network + (optionally) cache the session and probe endpoints
create - Create an identifier and initial DID document
resolve read Resolve a DID document
update - Update a DID document (signs via the keystore)
deactivate delete Deactivate a DID permanently (signs via the keystore)
key - Manage keypairs in the keystore
keystore - Establish, inspect, and re-key the keystore
config - Read and write CLI configuration
profile - Manage configuration profiles
completion - Print a shell completion script

create

Creates an identifier and initial DID document. Two identifier types, selected by -t/--type:

Flag Description
-t, --type <type> Identifier type: k (deterministic) or x (external). Default: k
-n, --network <network> Bitcoin network: bitcoin, testnet3, testnet4, signet, mutinynet, or regtest. Default: config defaults.network, else the active profile’s network, else regtest
-b, --bytes <bytes> Genesis bytes as a hex string. For type=k, a 33-byte public key (omit to generate a key); for type=x, the 32-byte genesis-document hash

--signing-key <ref> (global) selects a stored key for the existing-key mode; it applies only to -t k.

On a testnet with a public faucet, text-mode create (for a -t k identifier) also prints a funding hint on stderr: the initial P2WPKH beacon address next to its faucet and explorer links, from the per-network preset. It is suppressed under --quiet and -o json, and absent on regtest and mainnet.

resolve (alias: read)

Required flag: -i/--identifier. At most one of -r or -p may be given.

Flag Description
-i, --identifier <identifier> did:btcr2 identifier to resolve (required)
-r, --resolution-options <json> Resolution options as an inline JSON string
-p, --resolution-options-path <path> Path to a JSON file containing resolution options

update

Signs and broadcasts an update to a DID document. The signing key comes from the encrypted keystore (choose one with --signing-key <ref> or set an active key with btcr2 key use).

Required flags: -s/--source-document, --source-version-id, -p/--patches, -m/--verification-method-id, -b/--beacon-id.

Flag Description
-s, --source-document <json> Source DID document as a JSON string
--source-version-id <number> Source version ID as a non-negative integer
-p, --patches <json> JSON Patch operations as a JSON array string
-m, --verification-method-id <id> DID document verification method ID
-b, --beacon-id <json> Beacon ID as a JSON string
--publish-to-cas <mode> Publish update artifacts to a writable CAS before broadcast: auto, always, or never (default: never). See Publishing updates to CAS
--fee-rate <satsPerVByte> Fee rate in sats/vByte for the beacon transaction (default: 5). Raise it under congestion so the transaction confirms (also BTCR2_FEE_RATE, profile btc.feeRate)
--change-address <address> Send transaction change to this address instead of the beacon address, so a DID’s announcements are not linked on-chain (profile btc.changeAddress)

On a network with a block explorer, text-mode update (and deactivate) also prints a Watch: link on stderr for the broadcast txid, suppressed under --quiet and -o json.

deactivate (alias: delete)

Permanently deactivates a DID. This is irreversible. Deactivation applies the { "op": "add", "path": "/deactivated", "value": true } patch and routes through the same signed-update path as update, so it also signs via the keystore.

Required flags: -s/--source-document, --source-version-id, -m/--verification-method-id, -b/--beacon-id. Optional: --publish-to-cas <mode>, --fee-rate <satsPerVByte>, --change-address <address> (same as update).

init

btcr2 init is the one-command setup: it creates the btcr2 home directory, writes a default config if none exists, and establishes the keystore if none exists. It is idempotent (existing files are left untouched unless --force). For a single command that also records the network and (optionally) caches the session and probes the endpoints, see quickstart.

Flag Description
-n, --network <network> Bitcoin network to record as defaults.network so later commands can drop -n. Written idempotently: it never clobbers a network you set earlier
--dev Establish an unencrypted dev keystore (plaintext keys, no passphrase). Testnet/regtest only; mainnet operations are refused
--force Re-create the config even if it exists. Never re-creates the keystore (re-establishing one is the explicit keystore init --force)

By default init establishes an encrypted keystore and prompts (with confirmation) for a passphrase up front, so the first key generate never seals the keystore under an unconfirmed, mistyped passphrase. Supply the passphrase non-interactively with BTCR2_KEYSTORE_PASSPHRASE or --passphrase-file for scripted setup. The output envelope reports the resolved network alongside the paths and protection.

quickstart

btcr2 quickstart collapses onboarding into one step: it runs init’s scaffold, records the network (default mutinynet), and optionally caches the session and probes the endpoints. It reimplements nothing - it composes init, keystore unlock, and config doctor - so the keystore and session guarantees hold unchanged.

Flag Description
-n, --network <network> Bitcoin network to set up. Default: mutinynet
--dev Establish an unencrypted dev keystore (testnet only)
--unlock Cache the passphrase for the session so later commands do not re-prompt. Opt-in; on a fresh encrypted keystore it reuses the establish-time passphrase with no second prompt
--ttl <duration> Session lifetime with --unlock: bare seconds or an s/m/h suffix (default 1h, max 24h; also BTCR2_KEYSTORE_TTL)
--no-doctor Skip the endpoint reachability probe (which is on by default and advisory: a failed probe warns but quickstart still exits 0)
--allow-mainnet Permit -n bitcoin (records mainnet as the default; dev keystores are still refused). Guarded before any writes
--force Re-create the config even if it exists (never the keystore)

The workshop happy path:

btcr2 quickstart -n mutinynet --unlock --ttl 2h   # (or: --dev  for an unencrypted dev keystore)
btcr2 key generate --set-active
btcr2 create                                       # network comes from defaults.network
# ...fund the beacon (create prints the faucet + explorer links), resolve, update, deactivate...

key

Manage keypairs in the keystore. All subcommands operate offline (no Bitcoin connection).

Subcommand Alias Description
key generate - Generate a new keypair and store it. Flags: --name <name>, --set-active
key list ls List stored keys (id, fingerprint, name, active)
key show <ref> - Show a key’s public material and tags (never prints the secret)
key import - Import a secret from a hex file (--secret-file) or a public key as watch-only (--public). Flags: --name, --set-active
key export <ref> - Export public material by default; --secret --out <path> writes the secret to a new 0600 file
key delete <ref> rm Delete a key. --force deletes even the active key
key use <ref> - Set the active key, persisted across invocations

A key reference is a full URN, a unique name tag, or a unique fingerprint prefix.

keystore

Establish, inspect, and re-key the keystore. These operate on the keystore file directly (no Bitcoin connection).

Subcommand Alias Description
keystore init - Establish the keystore (encrypted by default; prompts and confirms the passphrase). --dev creates an unencrypted dev keystore; --force re-establishes an existing one (discards its keys)
keystore status - Show the resolved path, protection mode (encrypted/dev/absent), whether a passphrase is established, the key count, and the session state (whether one is live and its remaining lifetime). Never decrypts or prompts
keystore change-passphrase passwd Re-seal every key under a new passphrase (encrypted keystores only). Prompts for the current passphrase, then a new one (with confirmation). Clears any cached session
keystore unlock - Cache the verified passphrase for the session so later commands do not re-prompt. --ttl <duration> sets the lifetime (default 1h, max 24h; also BTCR2_KEYSTORE_TTL); --allow-mainnet permits unlocking a bitcoin-default context
keystore lock - Revoke the cached session so later commands prompt again. Idempotent; needs no passphrase

Encrypted vs dev keystores. An encrypted keystore seals each secret with argon2id + XChaCha20-Poly1305 under one passphrase, and records a verifier so a mistyped passphrase fails loudly (with Incorrect passphrase) instead of sealing a key under an unknown or divergent passphrase. A dev keystore (--dev) stores secrets in plaintext and never prompts: it is for disposable testnet/regtest keys only, and the CLI hard-refuses to sign or generate a mainnet (bitcoin) key with one.

Session unlock. keystore unlock caches the verified passphrase in <home>/session.json (0600) so a returning operator authenticates once instead of on every signing command. A cached session sits below BTCR2_KEYSTORE_PASSPHRASE / --passphrase-file and above the interactive prompt, so unattended and CI paths still win. Mainnet keeps per-use authentication: a bitcoin operation is withheld from a session that was not unlocked with --allow-mainnet. The cached passphrase is base64url-encoded, not encrypted; its only protection at rest is the 0600 file mode.

config

Read and write CLI configuration.

Subcommand Alias Description
config init - Create a default config file with one profile per network. --force overwrites
config get [path] - Print a value at a dotted path, or the whole config. Secret values are redacted; --show-secrets reveals them
config set <path> <value> - Set a value at a dotted path (value parsed as JSON when valid, else a string). An invalid enum for a known key is rejected; an unknown path warns but writes
config unset <path> - Delete a value at a dotted path
config list ls Print the entire config file. Secret values are redacted; --show-secrets reveals them
config validate - Report unknown keys, invalid enum values, and an unsupported schema version
config effective - Print the resolved connection config with per-value provenance (flag/env/file/default). -n, --network <n> selects the network; --show-secrets reveals the RPC password
config path - Print the resolved home directory, config-file, and keystore paths
config doctor - Probe reachability of the resolved endpoints (read-only; touches the network). -n, --network <n> selects the network

profile

Manage configuration profiles.

Subcommand Alias Description
profile add <name> - Add an empty profile
profile use <name> - Set the active profile (writes defaults.profile)
profile show [name] - Show a profile (defaults to the active profile)
profile remove <name> rm Remove a profile

completion

btcr2 completion [shell] prints a shell completion script (bash, zsh, or fish) to stdout. Defaults to bash. For example: eval "$(btcr2 completion bash)".

Usage

Create a DID

# Generate a fresh key (type=k), store it in the keystore, and print the identifier
btcr2 create -n regtest

# Deterministic (type=k): from an explicit compressed secp256k1 public key (33 bytes hex)
btcr2 create -t k -n regtest -b 02aa...

# Deterministic (type=k): from a stored key's public key
btcr2 create -t k -n regtest --signing-key mykey

# External (type=x): from a SHA-256 hash of a genesis document (32 bytes hex)
btcr2 create -t x -n bitcoin -b bb...

Resolve a DID

# Zero-config: network and endpoints are derived from the DID
btcr2 resolve -i did:btcr2:k1qq...

# Alias: read
btcr2 read -i did:btcr2:k1qq...

# With resolution options as inline JSON
btcr2 resolve -i did:btcr2:k1qq... -r '{"versionId":"1"}'

# With resolution options from a JSON file
btcr2 resolve -i did:btcr2:k1qq... -p resolution-options.json

# JSON output
btcr2 -o json resolve -i did:btcr2:k1qq...

Update a DID

# Signs with the active keystore key (or one chosen via --signing-key)
btcr2 update \
  -s "$(cat did.json)" \
  --source-version-id 1 \
  -p '[{"op":"add","path":"/service/-","value":{"id":"#svc","type":"X","serviceEndpoint":"https://x"}}]' \
  -m 'did:btcr2:k1qq...#key-0' \
  -b '{"id":"#beacon-0","type":"SingletonBeacon","serviceEndpoint":"bitcoin:bc1..."}'

Deactivate a DID

# Irreversible. Applies the deactivation patch and signs via the keystore.
btcr2 deactivate \
  -s "$(cat did.json)" \
  --source-version-id 1 \
  -m 'did:btcr2:k1qq...#key-0' \
  -b '{"id":"#beacon-0","type":"SingletonBeacon","serviceEndpoint":"bitcoin:bc1..."}'

Manage keys

btcr2 key generate --name mykey --set-active
btcr2 key list
btcr2 key use mykey

Configuration

Override precedence, highest wins: CLI flags, then environment variables, then config file, then network defaults.

Global flags

Flag Description
-v, --version Output the current version
-o, --output <format> Output format: json or text (default: config defaults.output, else text)
--verbose Verbose output
--quiet Suppress non-essential output
--home <dir> btcr2 home directory holding config.json + keystore.json (default: ~/.btcr2, %LOCALAPPDATA%\btcr2 on Windows; overrides $BTCR2_HOME)
-c, --config <path> Path to config file (default: <home>/config.json)
--profile <name> Config profile name (default: auto-detected from network)
--btc-rest <url> Override Bitcoin REST endpoint (Esplora API)
--btc-rpc-url <url> Override Bitcoin Core RPC endpoint
--btc-rpc-user <user> Bitcoin Core RPC username
--btc-rpc-pass <pass> Bitcoin Core RPC password (accepts an env:<VAR> or file:<path> secret reference)
--btc-rpc-wallet <name> Bitcoin Core wallet name for wallet-scoped RPCs (/wallet/<name>)
--btc-rpc-header <header> Extra Bitcoin Core RPC header "Key: Value" (repeatable)
--btc-rest-header <header> Extra Bitcoin REST header "Key: Value" (repeatable), e.g. an API key
--btc-timeout <ms> Bitcoin REST/RPC request timeout in milliseconds (default: unbounded)
--cas-gateway <url> IPFS HTTP gateway for CAS reads (read-only)
--cas-rpc-url <url> IPFS HTTP RPC endpoint for a writable CAS (reads + writes; enables --publish-to-cas)
--cas-timeout <ms> CAS request timeout in milliseconds (default: 30000; 0 disables)
--keystore <path> Path to the keystore file (default: <home>/keystore.json)
--passphrase-file <path> Read the keystore passphrase from a file (unattended use)
--signing-key <ref> Key for create/update/deactivate signing: a URN, fingerprint prefix, or name

Environment variables

Variable Equivalent flag
BTCR2_BTC_REST --btc-rest
BTCR2_BTC_RPC_URL --btc-rpc-url
BTCR2_BTC_RPC_USER --btc-rpc-user
BTCR2_BTC_RPC_PASS --btc-rpc-pass
BTCR2_BTC_RPC_PASS_FILE file whose contents are the RPC password
BTCR2_CAS_GATEWAY --cas-gateway
BTCR2_CAS_RPC_URL --cas-rpc-url
BTCR2_BTC_TIMEOUT --btc-timeout
BTCR2_CAS_TIMEOUT --cas-timeout
BTCR2_FEE_RATE --fee-rate
BTCR2_OUTPUT -o, --output
BTCR2_HOME --home
BTCR2_KEYSTORE_PASSPHRASE keystore passphrase (unattended use)
BTCR2_KEYSTORE_TTL session lifetime for keystore unlock / quickstart --unlock (default 1h, max 24h)

Home directory

The CLI keeps its config and keystore side by side in one home directory, resolved as --home <dir>, then $BTCR2_HOME, then the platform default: ~/.btcr2 on Linux/macOS and %LOCALAPPDATA%\btcr2 on Windows. Both files live directly under it (<home>/config.json, <home>/keystore.json); btcr2 config path prints the resolved locations. --config and --keystore still override each file individually, so the historical XDG split can be reproduced explicitly.

Config file

Default location: <home>/config.json. A malformed config file fails loudly (the CLI never silently falls back to public endpoints, and never overwrites an unparseable file).

Profiles are matched by network name when --profile is not specified. For example, resolving a regtest DID automatically selects the "regtest" profile. A profile that is not named after a network can declare its network with a network field.

{
  "schemaVersion": 1,
  "defaults": {
    "profile": "production",
    "network": "bitcoin",
    "output": "text"
  },
  "profiles": {
    "regtest": {
      "btc": {
        "rest": "http://localhost:3000",
        "rpcUrl": "http://localhost:18443",
        "rpcUser": "polaruser",
        "rpcPass": "polarpass",
        "wallet": "primary",
        "feeRate": 5,
        "timeoutMs": 30000
      }
    },
    "production": {
      "network": "bitcoin",
      "btc": {
        "rest": "https://my-mempool/api",
        "headers": { "Authorization": "Bearer <api-key>" },
        "feeRate": 20
      },
      "cas": { "gateway": "https://ipfs.io", "rpcUrl": "http://127.0.0.1:5001", "timeoutMs": 30000 },
      "identity": { "keystore": "/secure/prod-keystore.json", "default": "did:btcr2:...#key-0" }
    }
  }
}

Field notes:

Use config validate to check a file, and config effective to see the resolved values with their provenance.

RPC password and secrets

config get and config list redact secret-looking values (RPC password and any pass/secret/token key) by default; pass --show-secrets to reveal them.

An rpcPass written directly into config.json is stored in cleartext (the file is mode 0600 but not encrypted). For anything sensitive, keep the secret out of the file with a reference or an RPC-URL-embedded credential:

Defaults

When no overrides are configured:

Publishing updates to CAS

CAS publication is optional and never required. Every update and deactivate can be completed and shared entirely via sidecar: the command always prints the artifacts a resolver needs (the signed update, the transaction id, the CAS announcement for CAS beacons, and the SMT proof for SMT beacons) for you to distribute yourself.

Optionally, the signed update (and, for CAS beacons, the announcement) can be published to a content-addressed store before the on-chain broadcast, so any OP_RETURN update hash is fetchable from CAS at resolution time without sidecar data. This is opt-in via --publish-to-cas:

Mode Behavior
never (default) Publish nothing. Distribute the printed artifacts via sidecar.
auto Best-effort. Publish when a writable CAS is configured; otherwise skip publication silently for every beacon type and proceed. Never blocks an update.
always Require a writable CAS; error up-front for every beacon type when none is configured.

A writable CAS is configured with --cas-rpc-url <url> (an IPFS HTTP RPC endpoint, e.g. a local Kubo node at http://127.0.0.1:5001), the BTCR2_CAS_RPC_URL environment variable, or a profile’s cas.rpcUrl. The default IPFS gateway is read-only, so without a configured --cas-rpc-url, --publish-to-cas auto publishes nothing and completes the update sidecar-only, while --publish-to-cas always errors up-front (naming the fix) for every beacon type.

Privacy: under auto/always, canonical signed updates (and announcements) are published to the configured, possibly public, CAS before the on-chain anchor. Keep never (the default) to distribute update data privately via sidecar.

# Opt into CAS publication against a local IPFS (Kubo) node
btcr2 update \
  --cas-rpc-url http://127.0.0.1:5001 \
  --publish-to-cas auto \
  -s "$(cat did.json)" \
  --source-version-id 1 \
  -p '[{"op":"add","path":"/service/-","value":{"id":"#svc","type":"X","serviceEndpoint":"https://x"}}]' \
  -m 'did:btcr2:k1qq...#key-0' \
  -b '{"id":"#beacon-0","type":"SingletonBeacon","serviceEndpoint":"bitcoin:bc1..."}'

License

MPL-2.0