Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,5 @@ target/
.DS_Store
*.local.*
.vscode/
.trezor-user-env/
__pycache__/
7 changes: 7 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# Agent Notes

## Bitkit App Testing Docs

- Treat `README.md` as the entry point for testing Bitkit app PRs and merged features.
- When adding or changing test workflows, keep the actionable setup and check steps in `README.md`, not only in supporting docs.
- Supporting docs such as `docs/trezor-emulator.md` may contain deeper internals and troubleshooting, but README must remain enough to run the test flow.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Preserve LNURL-pay invoice millisatoshi precision by creating invoices with LND `value_msat` instead of truncating callback amounts to sats

### Added
- Trezor User Env Docker service and `scripts/trezor-emulator` helper for quickly smoke-testing Bitkit app Trezor PRs
- Support `amount_msat` query param in `/generate/bolt11` endpoint for sub-sat precision invoices
- `bolt11` command in `bitcoin-cli` for creating regular Lightning invoices (supports `--msat` and `-m` memo)
- LND hold invoice commands in `bitcoin-cli`: `holdinvoice`, `settleinvoice`, `cancelinvoice`
Expand Down
56 changes: 56 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,62 @@ docker compose logs -f bitcoind

### Bitkit Testing

#### Trezor Hardware PRs

Use this section as the entry point when checking Bitkit app PRs or merged features that need the official Trezor emulator. Start by preparing the deterministic Trezor User Env:

```bash
./scripts/trezor-emulator start
```

The macOS Trezor User Env service is included in the default `docker compose up -d` stack. The helper starts or reuses that service, then resets Bridge and the emulator into the deterministic review state. Linux users can start the host-network service with `docker compose --profile trezor-linux up -d trezor-user-env-linux`.

The helper starts the official Trezor User Env without its regtest stack, launches Bridge, wipes a deterministic T2T1 emulator, and sets it up with the `all all ...` seed and `Bitkit Test Trezor` label. It uses `scripts/trezor-controller.py` inside the container to talk to the User Env websocket controller.

##### Bitkit Android

For a physical phone, reverse the Bridge port and install the dev build with Bridge enabled:

```bash
./scripts/trezor-emulator adb
TREZOR_BRIDGE=true TREZOR_BRIDGE_URL=http://127.0.0.1:21325 ./gradlew installDevDebug
```

For an Android emulator, install with the emulator host Bridge URL:

```bash
TREZOR_BRIDGE=true TREZOR_BRIDGE_URL=http://10.0.2.2:21325 ./gradlew installDevDebug
```

Open the dashboard at `Settings -> Advanced -> Dev Settings -> Trezor`, then check:

- Scan shows the Bridge emulator device
- Connect succeeds and device features are shown
- Get address succeeds
- Get public key succeeds
- Sign and verify message succeed
- Send or compose reaches the expected funded or no-funds state
- Disconnect, reconnect, and forget-device cleanup behave correctly

##### Bitkit iOS

Run the relevant Trezor branch from Xcode. The User Env dashboard and Bridge are available on the host at:

- User Env dashboard: `http://localhost:9002`
- Trezor Bridge: `http://localhost:21325`

Open the dashboard at `Settings -> Advanced -> Trezor Hardware Wallet`, then check:

- Scan shows the Bridge emulator device
- Connect succeeds and device features are shown
- Get address succeeds
- Get public key succeeds
- Sign and verify message succeed
- Send or compose reaches the expected funded or no-funds state
- Disconnect, reconnect, and forget-device cleanup behave correctly

See [docs/trezor-emulator.md](docs/trezor-emulator.md) for helper internals, environment overrides, and troubleshooting commands.

#### Bech32 LNURL Pay

- in `Env.{kt,swift}`, use for REGTEST electrum server: `"tcp://localhost:60001"`
Expand Down
33 changes: 33 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -239,6 +239,39 @@ services:
ports:
- "5050:5050"

trezor-user-env-mac:
image: ghcr.io/trezor/trezor-user-env
ports:
- "9001:9001" # User Env controller websocket
- "9002:9002" # User Env dashboard
- "21325:21325" # Trezor Bridge legacy port used by Bitkit
- "21328:21328" # Trezor Bridge current port
- "15900:5900" # VNC port, offset to avoid local VNC conflicts
- "6080:6080" # noVNC web viewer
- "9003:9003" # MCP server SSE transport
environment:
- MACOS=1
- REGTEST_RPC_URL=http://host.docker.internal:43782
volumes:
- ./.trezor-user-env/trezor-suite:/trezor-user-env/trezor-suite
- ./.trezor-user-env/logs/screens:/trezor-user-env/logs/screens
- ./.trezor-user-env/logs/mcp-screenshots:/trezor-user-env/logs/mcp-screenshots
- ./.trezor-user-env/firmware/user_downloaded:/trezor-user-env/src/binaries/firmware/bin/user_downloaded

trezor-user-env-linux:
container_name: trezor-user-env.unix
image: ghcr.io/trezor/trezor-user-env
profiles:
- trezor-linux
network_mode: "host"
environment:
- PHYSICAL_TREZOR=${PHYSICAL_TREZOR:-}
volumes:
- ./.trezor-user-env/trezor-suite:/trezor-user-env/trezor-suite
- ./.trezor-user-env/logs/screens:/trezor-user-env/logs/screens
- ./.trezor-user-env/logs/mcp-screenshots:/trezor-user-env/logs/mcp-screenshots
- ./.trezor-user-env/firmware/user_downloaded:/trezor-user-env/src/binaries/firmware/bin/user_downloaded

volumes:
bitcoin_home:
postgres_data:
Expand Down
108 changes: 108 additions & 0 deletions docs/trezor-emulator.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
# Trezor Emulator Checks

Bitkit app PRs that need Trezor hardware behavior can use the official Trezor User Env through this repo. The helper starts the User Env without its extra regtest stack, starts Trezor Bridge, wipes a deterministic T2T1 emulator, and configures it with a stable seed and label.

## Start the Emulator

```bash
./scripts/trezor-emulator start
```

On macOS, the Trezor User Env service is part of the default compose stack, so `docker compose up -d` starts it with the rest of the Bitkit services. The helper is still useful because it resets Bridge and the emulator into the deterministic review state.

Linux needs host networking like upstream User Env, so its service stays behind the `trezor-linux` profile:

```bash
docker compose --profile trezor-linux up -d trezor-user-env-linux
```

If an upstream `trezor-user-env.mac` or `trezor-user-env.unix` container is already running, the helper reuses it instead of creating a duplicate container with the same fixed name.

The default emulator configuration is:

- model: `T2T1`
- firmware: `2-main`
- bridge: `node-bridge`
- mnemonic: `all all all all all all all all all all all all`
- pin: empty
- passphrase protection: off
- label: `Bitkit Test Trezor`

You can override these with environment variables, for example:

```bash
TREZOR_MODEL=T3T1 TREZOR_FIRMWARE=3-main ./scripts/trezor-emulator start
```

## App Setup

Use the same emulator stack for Bitkit Android and Bitkit iOS work. The commands below are app-specific launch notes; the emulator and Bridge setup stays the same.

### Bitkit Android

For a physical phone:

```bash
./scripts/trezor-emulator adb
TREZOR_BRIDGE=true TREZOR_BRIDGE_URL=http://127.0.0.1:21325 ./gradlew installDevDebug
```

For an Android emulator:

```bash
TREZOR_BRIDGE=true TREZOR_BRIDGE_URL=http://10.0.2.2:21325 ./gradlew installDevDebug
```

The Trezor dashboard is under `Settings -> Advanced -> Dev Settings -> Trezor`.

### Bitkit iOS

Run Bitkit from Xcode on the relevant Trezor branch, then open `Settings -> Advanced -> Trezor Hardware Wallet`.

The User Env dashboard and Bridge remain available at the same localhost endpoints:

- User Env dashboard: <http://localhost:9002>
- Trezor Bridge: <http://localhost:21325>

## Smoke Checklist

Use this checklist when reviewing any Bitkit app PR that needs the Trezor emulator:

- Scan shows the Bridge emulator device.
- Connect succeeds and device features are shown.
- Get address succeeds.
- Get public key succeeds.
- Sign and verify message succeed.
- Send or compose reaches the expected funded or no-funds state.
- Disconnect, reconnect, and forget-device cleanup behave correctly.

## Helpful Commands

```bash
./scripts/trezor-emulator status
./scripts/trezor-emulator logs
./scripts/trezor-emulator stop
```

Open the User Env dashboard at <http://localhost:9002>. Trezor Bridge listens at <http://localhost:21325>.

## How It Works

`scripts/trezor-emulator` is the entrypoint. It starts or reuses the User Env container, then runs `scripts/trezor-controller.py` inside that container with `/trezor-user-env/.venv/bin/python3`.

The Python script talks to the User Env websocket controller at `ws://127.0.0.1:9001` and sends the setup commands:

- `bridge-start`
- `emulator-start`
- `emulator-setup`
- `background-check`

Running the Python script inside the container keeps the host machine free of extra Python package requirements. The container already has the `websockets` dependency that the controller client needs.

Use `send-json` for one-off controller commands:

```bash
./scripts/trezor-emulator send-json '{"type":"emulator-get-features"}'
```

On Apple Silicon, the helper installs `libsdl3-0` and `libsdl3-image0` inside the User Env container when they are missing.
117 changes: 117 additions & 0 deletions scripts/trezor-controller.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
#!/usr/bin/env python3
"""Small client for the Trezor User Env websocket controller.

The shell helper runs this file inside the User Env container so the host does
not need a Python websocket package installed.
"""

from __future__ import annotations

import asyncio
import json
import os
import sys
from typing import Any

import websockets


CONTROLLER_WS = os.environ.get("TREZOR_CONTROLLER_WS", "ws://127.0.0.1:9001")
DEFAULT_MNEMONIC = "all all all all all all all all all all all all"


def next_id() -> int:
next_id.value += 1
return next_id.value


next_id.value = 0


async def send(payload: dict[str, Any], *, allow_failure: bool = False) -> dict[str, Any]:
async with websockets.connect(CONTROLLER_WS) as websocket:
await websocket.recv()
await websocket.send(json.dumps(payload))
raw_response = await websocket.recv()

response = json.loads(raw_response)
print(json.dumps(response, indent=2, sort_keys=True))

if not allow_failure and not response.get("success", False):
raise RuntimeError(response.get("error", response))

return response


async def setup() -> None:
await send(
{
"type": "bridge-start",
"version": os.environ.get("TREZOR_BRIDGE_VERSION", "node-bridge"),
"id": next_id(),
}
)
await send(
{
"type": "emulator-start",
"model": os.environ.get("TREZOR_MODEL", "T2T1"),
"version": os.environ.get("TREZOR_FIRMWARE", "2-main"),
"wipe": os.environ.get("TREZOR_WIPE", "true").lower() != "false",
"id": next_id(),
}
)
await send(
{
"type": "emulator-setup",
"mnemonic": os.environ.get("TREZOR_MNEMONIC", DEFAULT_MNEMONIC),
"pin": os.environ.get("TREZOR_PIN", ""),
"passphrase_protection": os.environ.get(
"TREZOR_PASSPHRASE_PROTECTION", "false"
).lower()
== "true",
"label": os.environ.get("TREZOR_LABEL", "Bitkit Test Trezor"),
"needs_backup": os.environ.get("TREZOR_NEEDS_BACKUP", "false").lower()
== "true",
"id": next_id(),
}
)
await status()


async def status() -> None:
await send({"type": "background-check", "id": next_id()})


async def stop() -> None:
await send({"type": "emulator-stop", "id": next_id()}, allow_failure=True)
await send({"type": "bridge-stop", "id": next_id()}, allow_failure=True)
await status()


async def raw(payload: str) -> None:
parsed = json.loads(payload)
parsed.setdefault("id", next_id())
await send(parsed)


async def main() -> None:
command = sys.argv[1] if len(sys.argv) > 1 else "setup"

if command == "ping":
await send({"type": "ping", "id": next_id()})
elif command == "setup":
await setup()
elif command == "status":
await status()
elif command == "stop":
await stop()
elif command == "send-json":
if len(sys.argv) != 3:
raise SystemExit("send-json expects one JSON payload argument")
await raw(sys.argv[2])
else:
raise SystemExit(f"Unknown controller command: {command}")


if __name__ == "__main__":
asyncio.run(main())
Loading