Skip to content

Commit 9305b37

Browse files
committed
feat: add trezor emulator
1 parent 703e38b commit 9305b37

8 files changed

Lines changed: 623 additions & 0 deletions

File tree

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,3 +6,5 @@ target/
66
.DS_Store
77
*.local.*
88
.vscode/
9+
.trezor-user-env/
10+
__pycache__/

AGENTS.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
# Agent Notes
2+
3+
## Bitkit App Testing Docs
4+
5+
- Treat `README.md` as the entry point for testing Bitkit app PRs and merged features.
6+
- When adding or changing test workflows, keep the actionable setup and check steps in `README.md`, not only in supporting docs.
7+
- Supporting docs such as `docs/trezor-emulator.md` may contain deeper internals and troubleshooting, but README must remain enough to run the test flow.

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1616
- Preserve LNURL-pay invoice millisatoshi precision by creating invoices with LND `value_msat` instead of truncating callback amounts to sats
1717

1818
### Added
19+
- Trezor User Env Docker service and `scripts/trezor-emulator` helper for quickly smoke-testing Bitkit app Trezor PRs
1920
- Support `amount_msat` query param in `/generate/bolt11` endpoint for sub-sat precision invoices
2021
- `bolt11` command in `bitcoin-cli` for creating regular Lightning invoices (supports `--msat` and `-m` memo)
2122
- LND hold invoice commands in `bitcoin-cli`: `holdinvoice`, `settleinvoice`, `cancelinvoice`

README.md

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -165,6 +165,62 @@ docker compose logs -f bitcoind
165165

166166
### Bitkit Testing
167167

168+
#### Trezor Hardware PRs
169+
170+
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:
171+
172+
```bash
173+
./scripts/trezor-emulator start
174+
```
175+
176+
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`.
177+
178+
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.
179+
180+
##### Bitkit Android
181+
182+
For a physical phone, reverse the Bridge port and install the dev build with Bridge enabled:
183+
184+
```bash
185+
./scripts/trezor-emulator adb
186+
TREZOR_BRIDGE=true TREZOR_BRIDGE_URL=http://127.0.0.1:21325 ./gradlew installDevDebug
187+
```
188+
189+
For an Android emulator, install with the emulator host Bridge URL:
190+
191+
```bash
192+
TREZOR_BRIDGE=true TREZOR_BRIDGE_URL=http://10.0.2.2:21325 ./gradlew installDevDebug
193+
```
194+
195+
Open the dashboard at `Settings -> Advanced -> Dev Settings -> Trezor`, then check:
196+
197+
- Scan shows the Bridge emulator device
198+
- Connect succeeds and device features are shown
199+
- Get address succeeds
200+
- Get public key succeeds
201+
- Sign and verify message succeed
202+
- Send or compose reaches the expected funded or no-funds state
203+
- Disconnect, reconnect, and forget-device cleanup behave correctly
204+
205+
##### Bitkit iOS
206+
207+
Run the relevant Trezor branch from Xcode. The User Env dashboard and Bridge are available on the host at:
208+
209+
- User Env dashboard: `http://localhost:9002`
210+
- Trezor Bridge: `http://localhost:21325`
211+
212+
Open the dashboard at `Settings -> Advanced -> Trezor Hardware Wallet`, then check:
213+
214+
- Scan shows the Bridge emulator device
215+
- Connect succeeds and device features are shown
216+
- Get address succeeds
217+
- Get public key succeeds
218+
- Sign and verify message succeed
219+
- Send or compose reaches the expected funded or no-funds state
220+
- Disconnect, reconnect, and forget-device cleanup behave correctly
221+
222+
See [docs/trezor-emulator.md](docs/trezor-emulator.md) for helper internals, environment overrides, and troubleshooting commands.
223+
168224
#### Bech32 LNURL Pay
169225

170226
- in `Env.{kt,swift}`, use for REGTEST electrum server: `"tcp://localhost:60001"`

docker-compose.yml

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -239,6 +239,39 @@ services:
239239
ports:
240240
- "5050:5050"
241241

242+
trezor-user-env-mac:
243+
image: ghcr.io/trezor/trezor-user-env
244+
ports:
245+
- "9001:9001" # User Env controller websocket
246+
- "9002:9002" # User Env dashboard
247+
- "21325:21325" # Trezor Bridge legacy port used by Bitkit
248+
- "21328:21328" # Trezor Bridge current port
249+
- "15900:5900" # VNC port, offset to avoid local VNC conflicts
250+
- "6080:6080" # noVNC web viewer
251+
- "9003:9003" # MCP server SSE transport
252+
environment:
253+
- MACOS=1
254+
- REGTEST_RPC_URL=http://host.docker.internal:43782
255+
volumes:
256+
- ./.trezor-user-env/trezor-suite:/trezor-user-env/trezor-suite
257+
- ./.trezor-user-env/logs/screens:/trezor-user-env/logs/screens
258+
- ./.trezor-user-env/logs/mcp-screenshots:/trezor-user-env/logs/mcp-screenshots
259+
- ./.trezor-user-env/firmware/user_downloaded:/trezor-user-env/src/binaries/firmware/bin/user_downloaded
260+
261+
trezor-user-env-linux:
262+
container_name: trezor-user-env.unix
263+
image: ghcr.io/trezor/trezor-user-env
264+
profiles:
265+
- trezor-linux
266+
network_mode: "host"
267+
environment:
268+
- PHYSICAL_TREZOR=${PHYSICAL_TREZOR:-}
269+
volumes:
270+
- ./.trezor-user-env/trezor-suite:/trezor-user-env/trezor-suite
271+
- ./.trezor-user-env/logs/screens:/trezor-user-env/logs/screens
272+
- ./.trezor-user-env/logs/mcp-screenshots:/trezor-user-env/logs/mcp-screenshots
273+
- ./.trezor-user-env/firmware/user_downloaded:/trezor-user-env/src/binaries/firmware/bin/user_downloaded
274+
242275
volumes:
243276
bitcoin_home:
244277
postgres_data:

docs/trezor-emulator.md

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
# Trezor Emulator Checks
2+
3+
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.
4+
5+
## Start the Emulator
6+
7+
```bash
8+
./scripts/trezor-emulator start
9+
```
10+
11+
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.
12+
13+
Linux needs host networking like upstream User Env, so its service stays behind the `trezor-linux` profile:
14+
15+
```bash
16+
docker compose --profile trezor-linux up -d trezor-user-env-linux
17+
```
18+
19+
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.
20+
21+
The default emulator configuration is:
22+
23+
- model: `T2T1`
24+
- firmware: `2-main`
25+
- bridge: `node-bridge`
26+
- mnemonic: `all all all all all all all all all all all all`
27+
- pin: empty
28+
- passphrase protection: off
29+
- label: `Bitkit Test Trezor`
30+
31+
You can override these with environment variables, for example:
32+
33+
```bash
34+
TREZOR_MODEL=T3T1 TREZOR_FIRMWARE=3-main ./scripts/trezor-emulator start
35+
```
36+
37+
## App Setup
38+
39+
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.
40+
41+
### Bitkit Android
42+
43+
For a physical phone:
44+
45+
```bash
46+
./scripts/trezor-emulator adb
47+
TREZOR_BRIDGE=true TREZOR_BRIDGE_URL=http://127.0.0.1:21325 ./gradlew installDevDebug
48+
```
49+
50+
For an Android emulator:
51+
52+
```bash
53+
TREZOR_BRIDGE=true TREZOR_BRIDGE_URL=http://10.0.2.2:21325 ./gradlew installDevDebug
54+
```
55+
56+
The Trezor dashboard is under `Settings -> Advanced -> Dev Settings -> Trezor`.
57+
58+
### Bitkit iOS
59+
60+
Run Bitkit from Xcode on the relevant Trezor branch, then open `Settings -> Advanced -> Trezor Hardware Wallet`.
61+
62+
The User Env dashboard and Bridge remain available at the same localhost endpoints:
63+
64+
- User Env dashboard: <http://localhost:9002>
65+
- Trezor Bridge: <http://localhost:21325>
66+
67+
## Smoke Checklist
68+
69+
Use this checklist when reviewing any Bitkit app PR that needs the Trezor emulator:
70+
71+
- Scan shows the Bridge emulator device.
72+
- Connect succeeds and device features are shown.
73+
- Get address succeeds.
74+
- Get public key succeeds.
75+
- Sign and verify message succeed.
76+
- Send or compose reaches the expected funded or no-funds state.
77+
- Disconnect, reconnect, and forget-device cleanup behave correctly.
78+
79+
## Helpful Commands
80+
81+
```bash
82+
./scripts/trezor-emulator status
83+
./scripts/trezor-emulator logs
84+
./scripts/trezor-emulator stop
85+
```
86+
87+
Open the User Env dashboard at <http://localhost:9002>. Trezor Bridge listens at <http://localhost:21325>.
88+
89+
## How It Works
90+
91+
`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`.
92+
93+
The Python script talks to the User Env websocket controller at `ws://127.0.0.1:9001` and sends the setup commands:
94+
95+
- `bridge-start`
96+
- `emulator-start`
97+
- `emulator-setup`
98+
- `background-check`
99+
100+
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.
101+
102+
Use `send-json` for one-off controller commands:
103+
104+
```bash
105+
./scripts/trezor-emulator send-json '{"type":"emulator-get-features"}'
106+
```
107+
108+
On Apple Silicon, the helper installs `libsdl3-0` and `libsdl3-image0` inside the User Env container when they are missing.

scripts/trezor-controller.py

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
#!/usr/bin/env python3
2+
"""Small client for the Trezor User Env websocket controller.
3+
4+
The shell helper runs this file inside the User Env container so the host does
5+
not need a Python websocket package installed.
6+
"""
7+
8+
from __future__ import annotations
9+
10+
import asyncio
11+
import json
12+
import os
13+
import sys
14+
from typing import Any
15+
16+
import websockets
17+
18+
19+
CONTROLLER_WS = os.environ.get("TREZOR_CONTROLLER_WS", "ws://127.0.0.1:9001")
20+
DEFAULT_MNEMONIC = "all all all all all all all all all all all all"
21+
22+
23+
def next_id() -> int:
24+
next_id.value += 1
25+
return next_id.value
26+
27+
28+
next_id.value = 0
29+
30+
31+
async def send(payload: dict[str, Any], *, allow_failure: bool = False) -> dict[str, Any]:
32+
async with websockets.connect(CONTROLLER_WS) as websocket:
33+
await websocket.recv()
34+
await websocket.send(json.dumps(payload))
35+
raw_response = await websocket.recv()
36+
37+
response = json.loads(raw_response)
38+
print(json.dumps(response, indent=2, sort_keys=True))
39+
40+
if not allow_failure and not response.get("success", False):
41+
raise RuntimeError(response.get("error", response))
42+
43+
return response
44+
45+
46+
async def setup() -> None:
47+
await send(
48+
{
49+
"type": "bridge-start",
50+
"version": os.environ.get("TREZOR_BRIDGE_VERSION", "node-bridge"),
51+
"id": next_id(),
52+
}
53+
)
54+
await send(
55+
{
56+
"type": "emulator-start",
57+
"model": os.environ.get("TREZOR_MODEL", "T2T1"),
58+
"version": os.environ.get("TREZOR_FIRMWARE", "2-main"),
59+
"wipe": os.environ.get("TREZOR_WIPE", "true").lower() != "false",
60+
"id": next_id(),
61+
}
62+
)
63+
await send(
64+
{
65+
"type": "emulator-setup",
66+
"mnemonic": os.environ.get("TREZOR_MNEMONIC", DEFAULT_MNEMONIC),
67+
"pin": os.environ.get("TREZOR_PIN", ""),
68+
"passphrase_protection": os.environ.get(
69+
"TREZOR_PASSPHRASE_PROTECTION", "false"
70+
).lower()
71+
== "true",
72+
"label": os.environ.get("TREZOR_LABEL", "Bitkit Test Trezor"),
73+
"needs_backup": os.environ.get("TREZOR_NEEDS_BACKUP", "false").lower()
74+
== "true",
75+
"id": next_id(),
76+
}
77+
)
78+
await status()
79+
80+
81+
async def status() -> None:
82+
await send({"type": "background-check", "id": next_id()})
83+
84+
85+
async def stop() -> None:
86+
await send({"type": "emulator-stop", "id": next_id()}, allow_failure=True)
87+
await send({"type": "bridge-stop", "id": next_id()}, allow_failure=True)
88+
await status()
89+
90+
91+
async def raw(payload: str) -> None:
92+
parsed = json.loads(payload)
93+
parsed.setdefault("id", next_id())
94+
await send(parsed)
95+
96+
97+
async def main() -> None:
98+
command = sys.argv[1] if len(sys.argv) > 1 else "setup"
99+
100+
if command == "ping":
101+
await send({"type": "ping", "id": next_id()})
102+
elif command == "setup":
103+
await setup()
104+
elif command == "status":
105+
await status()
106+
elif command == "stop":
107+
await stop()
108+
elif command == "send-json":
109+
if len(sys.argv) != 3:
110+
raise SystemExit("send-json expects one JSON payload argument")
111+
await raw(sys.argv[2])
112+
else:
113+
raise SystemExit(f"Unknown controller command: {command}")
114+
115+
116+
if __name__ == "__main__":
117+
asyncio.run(main())

0 commit comments

Comments
 (0)