From 964abcc82eddc173f5132a2b8a1fecea7eeb7f5d Mon Sep 17 00:00:00 2001 From: Piotr Stachyra Date: Thu, 18 Jun 2026 15:44:06 +0200 Subject: [PATCH 01/12] test: treazor emulator foundations --- .gitignore | 1 + README.md | 99 +++++++++++++ ci_run_android.sh | 2 + docker/docker-compose.yml | 37 +++++ scripts/adb-reverse.sh | 2 + scripts/build-android-apk.sh | 12 +- scripts/build-ios-sim.sh | 13 ++ scripts/trezor-controller.py | 117 +++++++++++++++ scripts/trezor-emulator | 279 +++++++++++++++++++++++++++++++++++ 9 files changed, 560 insertions(+), 2 deletions(-) create mode 100755 scripts/trezor-controller.py create mode 100755 scripts/trezor-emulator diff --git a/.gitignore b/.gitignore index beeb1b2c..3f273c29 100644 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,7 @@ node_modules docker/lnd docker/lnurl-server-data docker/lnurl-server/data +docker/.trezor-user-env artifacts WARP.md .ai diff --git a/README.md b/README.md index 6b3b0e54..69efe1b8 100644 --- a/README.md +++ b/README.md @@ -83,8 +83,107 @@ BACKEND=local ./scripts/build-ios-sim.sh BACKEND=regtest ./scripts/build-ios-sim.sh ``` +Optional Trezor Bridge support is disabled by default and can be enabled per build: + +```bash +# Android emulator, local backend +TREZOR_BRIDGE=true ./scripts/build-android-apk.sh + +# Android physical device, local backend +TREZOR_BRIDGE=true TREZOR_BRIDGE_URL=http://127.0.0.1:21325 ./scripts/build-android-apk.sh + +# Android emulator, staging regtest backend +BACKEND=regtest TREZOR_BRIDGE=true ./scripts/build-android-apk.sh + +# iOS simulator, local backend +TREZOR_BRIDGE=true TREZOR_ELECTRUM_URL=tcp://127.0.0.1:60001 ./scripts/build-ios-sim.sh + +# iOS simulator, staging regtest backend +BACKEND=regtest TREZOR_BRIDGE=true ./scripts/build-ios-sim.sh +``` + --- +### ๐Ÿ” Manual Trezor Emulator Setup + +The local docker setup includes an opt-in Trezor User Env fixture for manual hardware-wallet checks. It starts the official Trezor emulator and Bridge, but it is not part of the default `docker compose up -d` stack. + +Default emulator state: + +- 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` + +Start or reset the emulator: + +```bash +./scripts/trezor-emulator start +./scripts/trezor-emulator status +``` + +Useful URLs: + +- User Env dashboard: `http://localhost:9002` +- Trezor Bridge: `http://localhost:21325` + +The Trezor User Env image is pinned in `docker/docker-compose.yml` so the emulator Bridge keeps the raw message format expected by current Bitkit builds. + +#### Android Emulator + +```bash +# Local backend +cd docker +docker compose up -d +cd .. +./scripts/trezor-emulator start +TREZOR_BRIDGE=true ./scripts/build-android-apk.sh +npm run e2e:android + +# Staging regtest backend +./scripts/trezor-emulator start +BACKEND=regtest TREZOR_BRIDGE=true ./scripts/build-android-apk.sh +BACKEND=regtest npm run e2e:android +``` + +For manual checks, open Bitkit and use the app's developer Trezor screen to scan and connect to `Bitkit Test Trezor`. + +#### Android Physical Device + +```bash +./scripts/trezor-emulator start +./scripts/trezor-emulator adb +TREZOR_BRIDGE=true TREZOR_BRIDGE_URL=http://127.0.0.1:21325 ./scripts/build-android-apk.sh +``` + +#### iOS Simulator + +```bash +# Local backend +cd docker +docker compose up -d +cd .. +./scripts/trezor-emulator start +TREZOR_BRIDGE=true TREZOR_ELECTRUM_URL=tcp://127.0.0.1:60001 ./scripts/build-ios-sim.sh +npm run e2e:ios + +# Staging regtest backend +./scripts/trezor-emulator start +BACKEND=regtest TREZOR_BRIDGE=true ./scripts/build-ios-sim.sh +BACKEND=regtest npm run e2e:ios +``` + +Stop the emulator when finished: + +```bash +./scripts/trezor-emulator stop +``` + +Backend and Trezor are independent. `BACKEND=local` uses local Bitcoin/Electrum, while `BACKEND=regtest` uses remote staging regtest services. The Trezor emulator always provides only the device and Bridge. Fund or mine against the same backend the app was built for. + ### ๐Ÿงช Running tests **Important:** The `BACKEND` environment variable controls which infrastructure the tests use for blockchain operations (deposits, mining blocks): diff --git a/ci_run_android.sh b/ci_run_android.sh index 1e4fc180..a44d7fc7 100755 --- a/ci_run_android.sh +++ b/ci_run_android.sh @@ -56,6 +56,8 @@ if [[ "${BACKEND:-local}" != "mainnet" ]]; then adb reverse tcp:30001 tcp:30001 # homegate port adb reverse tcp:6288 tcp:6288 + # trezor bridge port + adb reverse tcp:21325 tcp:21325 fi # show touches adb shell settings put system show_touches 1 diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml index 8c07dddc..e8796ee6 100644 --- a/docker/docker-compose.yml +++ b/docker/docker-compose.yml @@ -1,3 +1,5 @@ +x-trezor-user-env-image: &trezor_user_env_image ghcr.io/trezor/trezor-user-env@sha256:15871ebb234bf7c0197cfcd31ddc2edbe90173cd6386fc0283fafe05bf888c06 + services: bitcoind: container_name: bitcoin @@ -199,6 +201,41 @@ services: ports: - '6288:8080' + trezor-user-env-mac: + image: *trezor_user_env_image + profiles: + - trezor + 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: *trezor_user_env_image + 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: diff --git a/scripts/adb-reverse.sh b/scripts/adb-reverse.sh index 00198813..7c4140b6 100755 --- a/scripts/adb-reverse.sh +++ b/scripts/adb-reverse.sh @@ -7,6 +7,8 @@ for d in $(adb devices | awk 'NR>1 && $2=="device" {print $1}'); do adb -s "$d" reverse tcp:9735 tcp:9735 adb -s "$d" reverse tcp:30001 tcp:30001 adb -s "$d" reverse tcp:6288 tcp:6288 + adb -s "$d" reverse tcp:21325 tcp:21325 + done echo "Done." \ No newline at end of file diff --git a/scripts/build-android-apk.sh b/scripts/build-android-apk.sh index 7d6dfa10..a41727fa 100755 --- a/scripts/build-android-apk.sh +++ b/scripts/build-android-apk.sh @@ -16,12 +16,16 @@ # ./scripts/build-android-apk.sh # BACKEND=regtest ./scripts/build-android-apk.sh # BACKEND=mainnet ./scripts/build-android-apk.sh +# TREZOR_BRIDGE=true ./scripts/build-android-apk.sh +# TREZOR_BRIDGE=true TREZOR_BRIDGE_URL=http://127.0.0.1:21325 ./scripts/build-android-apk.sh set -euo pipefail E2E_ROOT="$(cd "$(dirname "$0")/.." && pwd)" ANDROID_ROOT="$(cd "$E2E_ROOT/../bitkit-android" && pwd)" BACKEND="${BACKEND:-local}" +TREZOR_BRIDGE="${TREZOR_BRIDGE:-false}" +TREZOR_BRIDGE_URL="${TREZOR_BRIDGE_URL:-http://10.0.2.2:21325}" E2E_BACKEND="local" GRADLE_TASK="assembleDevDebug" APK_FLAVOR_DIR="dev/debug" @@ -40,10 +44,14 @@ else echo "ERROR: Unsupported BACKEND value: $BACKEND" >&2 exit 1 fi -echo "Building Android APK (BACKEND=$BACKEND, E2E_BACKEND=$E2E_BACKEND, GRADLE_TASK=$GRADLE_TASK)..." +echo "Building Android APK (BACKEND=$BACKEND, E2E_BACKEND=$E2E_BACKEND, TREZOR_BRIDGE=$TREZOR_BRIDGE, TREZOR_BRIDGE_URL=$TREZOR_BRIDGE_URL, GRADLE_TASK=$GRADLE_TASK)..." pushd "$ANDROID_ROOT" >/dev/null -E2E=true E2E_BACKEND="$E2E_BACKEND" ./gradlew "$GRADLE_TASK" --no-daemon --stacktrace +E2E=true \ + E2E_BACKEND="$E2E_BACKEND" \ + TREZOR_BRIDGE="$TREZOR_BRIDGE" \ + TREZOR_BRIDGE_URL="$TREZOR_BRIDGE_URL" \ + ./gradlew "$GRADLE_TASK" --no-daemon --stacktrace popd >/dev/null # Find the universal APK diff --git a/scripts/build-ios-sim.sh b/scripts/build-ios-sim.sh index 3dadbbc6..8a68e0c3 100755 --- a/scripts/build-ios-sim.sh +++ b/scripts/build-ios-sim.sh @@ -14,11 +14,16 @@ # Usage: # ./scripts/build-ios-sim.sh # BACKEND=regtest ./scripts/build-ios-sim.sh +# TREZOR_BRIDGE=true ./scripts/build-ios-sim.sh +# TREZOR_BRIDGE=true BACKEND=regtest ./scripts/build-ios-sim.sh set -euo pipefail E2E_ROOT="$(cd "$(dirname "$0")/.." && pwd)" IOS_ROOT="$(cd "$E2E_ROOT/../bitkit-ios" && pwd)" BACKEND="${BACKEND:-local}" +TREZOR_BRIDGE="${TREZOR_BRIDGE:-false}" +TREZOR_BRIDGE_URL="${TREZOR_BRIDGE_URL:-http://127.0.0.1:21325}" +TREZOR_ELECTRUM_URL="${TREZOR_ELECTRUM_URL:-}" E2E_BACKEND="local" E2E_NETWORK="regtest" XCODE_EXTRA_ARGS=() @@ -35,9 +40,17 @@ fi XCODE_EXTRA_ARGS+=( "E2E_BACKEND=$E2E_BACKEND" "E2E_NETWORK=$E2E_NETWORK" + "TREZOR_BRIDGE=$TREZOR_BRIDGE" + "TREZOR_BRIDGE_URL=$TREZOR_BRIDGE_URL" "SWIFT_ACTIVE_COMPILATION_CONDITIONS=\$(inherited) E2E_BUILD" ) +if [[ -n "$TREZOR_ELECTRUM_URL" ]]; then + XCODE_EXTRA_ARGS+=("TREZOR_ELECTRUM_URL=$TREZOR_ELECTRUM_URL") +fi + +echo "Building iOS simulator app (BACKEND=$BACKEND, E2E_BACKEND=$E2E_BACKEND, TREZOR_BRIDGE=$TREZOR_BRIDGE, TREZOR_BRIDGE_URL=$TREZOR_BRIDGE_URL)..." + xcodebuild \ -project "$IOS_ROOT/Bitkit.xcodeproj" \ -scheme Bitkit \ diff --git a/scripts/trezor-controller.py b/scripts/trezor-controller.py new file mode 100755 index 00000000..942aea02 --- /dev/null +++ b/scripts/trezor-controller.py @@ -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()) diff --git a/scripts/trezor-emulator b/scripts/trezor-emulator new file mode 100755 index 00000000..d48367fe --- /dev/null +++ b/scripts/trezor-emulator @@ -0,0 +1,279 @@ +#!/usr/bin/env bash +set -euo pipefail + +ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +COMPOSE_FILE="$ROOT_DIR/docker/docker-compose.yml" +CONTROLLER_SCRIPT="$ROOT_DIR/scripts/trezor-controller.py" +TREZOR_DATA_DIR="$ROOT_DIR/docker/.trezor-user-env" + +DASHBOARD_URL="${TREZOR_DASHBOARD_URL:-http://localhost:9002}" +BRIDGE_URL="${TREZOR_BRIDGE_URL:-http://localhost:21325}" +BRIDGE_PORT="${TREZOR_BRIDGE_PORT:-21325}" + +usage() { + cat <<'EOF' +Usage: ./scripts/trezor-emulator + +Commands: + start Start Trezor User Env, Bridge, and a deterministic T2T1 emulator + status Show User Env bridge/emulator status and Bridge enumerate output + adb Reverse the Bridge port for a physical Android device + stop Stop the emulator and Bridge; stop repo-managed User Env when owned here + logs Tail the User Env container logs + send-json Send one raw JSON command to the User Env controller + help Show this help + +Useful environment overrides: + TREZOR_MODEL=T2T1 + TREZOR_FIRMWARE=2-main + TREZOR_BRIDGE_VERSION=node-bridge + TREZOR_LABEL="Bitkit Test Trezor" + TREZOR_WIPE=false +EOF +} + +detect_service() { + case "$(uname -s)" in + Darwin) + TREZOR_PROFILE="${TREZOR_PROFILE:-trezor}" + TREZOR_SERVICE="${TREZOR_SERVICE:-trezor-user-env-mac}" + ;; + Linux) + TREZOR_PROFILE="${TREZOR_PROFILE:-trezor-linux}" + TREZOR_SERVICE="${TREZOR_SERVICE:-trezor-user-env-linux}" + ;; + *) + echo "Unsupported OS for the bundled Trezor User Env helper: $(uname -s)" >&2 + exit 1 + ;; + esac +} + +compose() { + docker compose -f "$COMPOSE_FILE" "$@" +} + +compose_with_profile() { + if [[ -n "${TREZOR_PROFILE:-}" ]]; then + compose --profile "$TREZOR_PROFILE" "$@" + else + compose "$@" + fi +} + +resolve_container() { + local compose_id + + if [[ -n "${TREZOR_CONTAINER:-}" ]] && docker inspect "$TREZOR_CONTAINER" >/dev/null 2>&1; then + return 0 + fi + + compose_id="$(compose_with_profile ps -q "$TREZOR_SERVICE" 2>/dev/null | head -n 1 || true)" + if [[ -n "$compose_id" ]]; then + TREZOR_CONTAINER="$compose_id" + return 0 + fi + + return 1 +} + +container_exists() { + resolve_container +} + +container_running() { + resolve_container || return 1 + [[ "$(docker inspect -f '{{.State.Running}}' "$TREZOR_CONTAINER" 2>/dev/null)" == "true" ]] +} + +container_image() { + resolve_container || return 0 + docker inspect -f '{{.Config.Image}}' "$TREZOR_CONTAINER" 2>/dev/null || true +} + +container_project() { + resolve_container || return 0 + docker inspect -f '{{ index .Config.Labels "com.docker.compose.project" }}' "$TREZOR_CONTAINER" 2>/dev/null || true +} + +container_logs() { + if container_exists; then + docker logs "$@" "$TREZOR_CONTAINER" + else + compose_with_profile logs "$@" "$TREZOR_SERVICE" + fi +} + +prepare_dirs() { + mkdir -p \ + "$TREZOR_DATA_DIR/trezor-suite" \ + "$TREZOR_DATA_DIR/logs/screens" \ + "$TREZOR_DATA_DIR/logs/mcp-screenshots" \ + "$TREZOR_DATA_DIR/firmware/user_downloaded" +} + +controller() { + local python_bin="${TREZOR_CONTAINER_PYTHON:-/trezor-user-env/.venv/bin/python3}" + + resolve_container || { + echo "Trezor User Env container is not running yet." >&2 + return 1 + } + + docker exec -i \ + -e TREZOR_CONTROLLER_WS="${TREZOR_CONTROLLER_WS:-ws://127.0.0.1:9001}" \ + -e TREZOR_MODEL="${TREZOR_MODEL:-T2T1}" \ + -e TREZOR_FIRMWARE="${TREZOR_FIRMWARE:-2-main}" \ + -e TREZOR_BRIDGE_VERSION="${TREZOR_BRIDGE_VERSION:-node-bridge}" \ + -e TREZOR_MNEMONIC="${TREZOR_MNEMONIC:-all all all all all all all all all all all all}" \ + -e TREZOR_PIN="${TREZOR_PIN:-}" \ + -e TREZOR_PASSPHRASE_PROTECTION="${TREZOR_PASSPHRASE_PROTECTION:-false}" \ + -e TREZOR_LABEL="${TREZOR_LABEL:-Bitkit Test Trezor}" \ + -e TREZOR_NEEDS_BACKUP="${TREZOR_NEEDS_BACKUP:-false}" \ + -e TREZOR_WIPE="${TREZOR_WIPE:-true}" \ + "$TREZOR_CONTAINER" "$python_bin" - "$@" < "$CONTROLLER_SCRIPT" +} + +wait_for_controller() { + local attempt + + if ! container_running; then + echo "Trezor User Env container is not running. Use ./scripts/trezor-emulator start." >&2 + return 1 + fi + + for attempt in $(seq 1 60); do + if controller ping >/dev/null 2>&1; then + return 0 + fi + sleep 2 + done + + echo "Trezor User Env controller did not become ready." >&2 + container_logs --tail=80 >&2 || true + exit 1 +} + +install_apple_silicon_sdl_packages() { + if [[ "$(uname -s)" != "Darwin" || "$(uname -m)" != "arm64" ]]; then + return 0 + fi + + docker exec "$TREZOR_CONTAINER" sh -lc ' + if dpkg -s libsdl3-0 libsdl3-image0 >/dev/null 2>&1; then + exit 0 + fi + + echo "Installing SDL3 runtime packages required by the Trezor emulator..." + apt-get update + apt-get install -y libsdl3-0 libsdl3-image0 + ' +} + +start_env() { + prepare_dirs + + compose_with_profile up -d "$TREZOR_SERVICE" + TREZOR_CONTAINER="$(compose_with_profile ps -q "$TREZOR_SERVICE" | head -n 1)" + + wait_for_controller + install_apple_silicon_sdl_packages +} + +bridge_enumerate() { + curl --fail --silent --show-error -X POST "$BRIDGE_URL/enumerate" +} + +print_ready_notes() { + cat </dev/null 2>&1; then + echo "adb is not available on PATH." >&2 + exit 1 + fi + + adb reverse "tcp:$BRIDGE_PORT" "tcp:$BRIDGE_PORT" +} + +stop() { + if resolve_container && container_running; then + controller stop || true + fi + + compose_with_profile stop "$TREZOR_SERVICE" +} + +logs() { + container_logs -f +} + +send_json() { + if [[ $# -ne 1 ]]; then + echo "send-json expects one JSON payload argument." >&2 + exit 1 + fi + + wait_for_controller + controller send-json "$1" +} + +main() { + detect_service + + local command="${1:-help}" + shift || true + + case "$command" in + start) start ;; + status) status ;; + adb) adb_reverse ;; + stop) stop ;; + logs) logs ;; + send-json) send_json "$@" ;; + help|--help|-h) usage ;; + *) + usage >&2 + exit 1 + ;; + esac +} + +main "$@" From 0960fa0fea95c0521eb2540c5ae02b68a34fa840 Mon Sep 17 00:00:00 2001 From: Piotr Stachyra Date: Thu, 18 Jun 2026 15:54:02 +0200 Subject: [PATCH 02/12] feat: random mnemonic --- README.md | 14 +++++++++++++- scripts/trezor-emulator | 32 +++++++++++++++++++++++++++++++- 2 files changed, 44 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 69efe1b8..bf83c244 100644 --- a/README.md +++ b/README.md @@ -113,7 +113,7 @@ Default emulator state: - Model: `T2T1` - Firmware: `2-main` - Bridge: `node-bridge` -- Mnemonic: `all all all all all all all all all all all all` +- Mnemonic: random 12-word BIP39 phrase generated on each `start` - PIN: empty - Passphrase protection: off - Label: `Bitkit Test Trezor` @@ -125,6 +125,18 @@ Start or reset the emulator: ./scripts/trezor-emulator status ``` +Use the deterministic seed when you want to reuse known history/funds: + +```bash +TREZOR_RANDOM_MNEMONIC=false ./scripts/trezor-emulator start +``` + +Or provide an explicit seed: + +```bash +TREZOR_MNEMONIC="all all all all all all all all all all all all" ./scripts/trezor-emulator start +``` + Useful URLs: - User Env dashboard: `http://localhost:9002` diff --git a/scripts/trezor-emulator b/scripts/trezor-emulator index d48367fe..73cd4758 100755 --- a/scripts/trezor-emulator +++ b/scripts/trezor-emulator @@ -27,6 +27,8 @@ Useful environment overrides: TREZOR_MODEL=T2T1 TREZOR_FIRMWARE=2-main TREZOR_BRIDGE_VERSION=node-bridge + TREZOR_RANDOM_MNEMONIC=true + TREZOR_MNEMONIC="all all all all all all all all all all all all" TREZOR_LABEL="Bitkit Test Trezor" TREZOR_WIPE=false EOF @@ -112,20 +114,48 @@ prepare_dirs() { "$TREZOR_DATA_DIR/firmware/user_downloaded" } +random_mnemonic() { + docker exec -i "$TREZOR_CONTAINER" /trezor-user-env/.venv/bin/python3 - <<'PY' +from mnemonic import Mnemonic + +print(Mnemonic("english").generate(strength=128)) +PY +} + +resolve_mnemonic() { + if [[ -n "${TREZOR_MNEMONIC:-}" ]]; then + echo "$TREZOR_MNEMONIC" + return + fi + + if [[ "${TREZOR_RANDOM_MNEMONIC:-true}" == "true" ]]; then + random_mnemonic + return + fi + + echo "all all all all all all all all all all all all" +} + controller() { local python_bin="${TREZOR_CONTAINER_PYTHON:-/trezor-user-env/.venv/bin/python3}" + local mnemonic="all all all all all all all all all all all all" resolve_container || { echo "Trezor User Env container is not running yet." >&2 return 1 } + if [[ "${1:-}" == "setup" ]]; then + mnemonic="$(resolve_mnemonic)" + echo "Using Trezor mnemonic: $mnemonic" >&2 + fi + docker exec -i \ -e TREZOR_CONTROLLER_WS="${TREZOR_CONTROLLER_WS:-ws://127.0.0.1:9001}" \ -e TREZOR_MODEL="${TREZOR_MODEL:-T2T1}" \ -e TREZOR_FIRMWARE="${TREZOR_FIRMWARE:-2-main}" \ -e TREZOR_BRIDGE_VERSION="${TREZOR_BRIDGE_VERSION:-node-bridge}" \ - -e TREZOR_MNEMONIC="${TREZOR_MNEMONIC:-all all all all all all all all all all all all}" \ + -e TREZOR_MNEMONIC="$mnemonic" \ -e TREZOR_PIN="${TREZOR_PIN:-}" \ -e TREZOR_PASSPHRASE_PROTECTION="${TREZOR_PASSPHRASE_PROTECTION:-false}" \ -e TREZOR_LABEL="${TREZOR_LABEL:-Bitkit Test Trezor}" \ From 1934368e0a9a9d0573e28a56725d46c10e635d48 Mon Sep 17 00:00:00 2001 From: Piotr Stachyra Date: Fri, 19 Jun 2026 10:19:00 +0200 Subject: [PATCH 03/12] feat: trezor address output json --- README.md | 14 +++++ scripts/trezor-emulator | 125 ++++++++++++++++++++++++++++++++++++++-- 2 files changed, 135 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index bf83c244..11fafef2 100644 --- a/README.md +++ b/README.md @@ -125,6 +125,14 @@ Start or reset the emulator: ./scripts/trezor-emulator status ``` +`start` prints the generated mnemonic and the first native regtest receive address (`m/84h/1h/0h/0/0`) so it can be funded during manual checks. + +For CI or scripts, use JSON output: + +```bash +./scripts/trezor-emulator start --json > artifacts/trezor-emulator.json +``` + Use the deterministic seed when you want to reuse known history/funds: ```bash @@ -137,6 +145,12 @@ Or provide an explicit seed: TREZOR_MNEMONIC="all all all all all all all all all all all all" ./scripts/trezor-emulator start ``` +Override the printed address coin/path when needed: + +```bash +TREZOR_ADDRESS_COIN=Testnet TREZOR_ADDRESS_PATH="m/84h/1h/0h/0/0" ./scripts/trezor-emulator start +``` + Useful URLs: - User Env dashboard: `http://localhost:9002` diff --git a/scripts/trezor-emulator b/scripts/trezor-emulator index 73cd4758..2f900eb0 100755 --- a/scripts/trezor-emulator +++ b/scripts/trezor-emulator @@ -15,7 +15,9 @@ usage() { Usage: ./scripts/trezor-emulator Commands: - start Start Trezor User Env, Bridge, and a deterministic T2T1 emulator + start Start Trezor User Env, Bridge, and a T2T1 emulator + start --json + Start and print a machine-readable summary to stdout status Show User Env bridge/emulator status and Bridge enumerate output adb Reverse the Bridge port for a physical Android device stop Stop the emulator and Bridge; stop repo-managed User Env when owned here @@ -29,6 +31,8 @@ Useful environment overrides: TREZOR_BRIDGE_VERSION=node-bridge TREZOR_RANDOM_MNEMONIC=true TREZOR_MNEMONIC="all all all all all all all all all all all all" + TREZOR_ADDRESS_COIN=Regtest + TREZOR_ADDRESS_PATH="m/84'/1'/0'/0/0" TREZOR_LABEL="Bitkit Test Trezor" TREZOR_WIPE=false EOF @@ -146,7 +150,7 @@ controller() { } if [[ "${1:-}" == "setup" ]]; then - mnemonic="$(resolve_mnemonic)" + mnemonic="${TREZOR_MNEMONIC:-$(resolve_mnemonic)}" echo "Using Trezor mnemonic: $mnemonic" >&2 fi @@ -164,6 +168,58 @@ controller() { "$TREZOR_CONTAINER" "$python_bin" - "$@" < "$CONTROLLER_SCRIPT" } +get_address() { + local address_path="${TREZOR_ADDRESS_PATH:-m/84h/1h/0h/0/0}" + + docker exec -i \ + -e TREZOR_ADDRESS_COIN="${TREZOR_ADDRESS_COIN:-Regtest}" \ + -e TREZOR_ADDRESS_PATH="$address_path" \ + "$TREZOR_CONTAINER" /trezor-user-env/.venv/bin/python3 - <<'PY' +import os +import time + +from trezorlib import btc, messages +from trezorlib.client import get_default_client +from trezorlib.tools import parse_path +from trezorlib.transport.bridge import BridgeTransport + +transport = None +for _ in range(30): + transport = next(iter(BridgeTransport.enumerate()), None) + if transport is not None: + break + time.sleep(1) + +if transport is None: + raise SystemExit("No Trezor Bridge device found.") + +coin = os.environ["TREZOR_ADDRESS_COIN"] +path = os.environ["TREZOR_ADDRESS_PATH"].replace("h", "'") + +client = get_default_client("bitkit-e2e", transport) +with client: + session = client.get_session() + address = btc.get_address( + session, + coin, + parse_path(path), + show_display=False, + script_type=messages.InputScriptType.SPENDWITNESS, + ) + +print(address) +PY +} + +print_address() { + local address_path="${TREZOR_ADDRESS_PATH:-m/84h/1h/0h/0/0}" + local display_path="${address_path//h/\'}" + local address + + address="$(get_address)" + echo "Trezor address (${TREZOR_ADDRESS_COIN:-Regtest}, $display_path): $address" +} + wait_for_controller() { local attempt @@ -235,9 +291,56 @@ Bitkit app quick checks: EOF } +print_json_summary() { + local mnemonic="$1" + local address="$2" + local address_path="${TREZOR_ADDRESS_PATH:-m/84h/1h/0h/0/0}" + + MNEMONIC="$mnemonic" \ + ADDRESS="$address" \ + ADDRESS_COIN="${TREZOR_ADDRESS_COIN:-Regtest}" \ + ADDRESS_PATH="$address_path" \ + DASHBOARD_URL="$DASHBOARD_URL" \ + BRIDGE_URL="$BRIDGE_URL" \ + python3 - <<'PY' +import json +import os + +print(json.dumps({ + "dashboardUrl": os.environ["DASHBOARD_URL"], + "bridgeUrl": os.environ["BRIDGE_URL"], + "mnemonic": os.environ["MNEMONIC"], + "address": { + "coin": os.environ["ADDRESS_COIN"], + "path": os.environ["ADDRESS_PATH"], + "value": os.environ["ADDRESS"], + }, +}, sort_keys=True)) +PY +} + start() { + local output_json="${1:-false}" + local mnemonic + local address + + if [[ "$output_json" == "true" ]]; then + start_env >&2 + mnemonic="$(resolve_mnemonic)" + TREZOR_MNEMONIC="$mnemonic" controller setup >&2 + address="$(get_address)" + print_json_summary "$mnemonic" "$address" + return + fi + start_env - controller setup + mnemonic="$(resolve_mnemonic)" + TREZOR_MNEMONIC="$mnemonic" controller setup + address="$(get_address)" + local address_path="${TREZOR_ADDRESS_PATH:-m/84h/1h/0h/0/0}" + local display_path="${address_path//h/\'}" + + echo "Trezor address (${TREZOR_ADDRESS_COIN:-Regtest}, $display_path): $address" echo echo "Bridge enumerate:" bridge_enumerate @@ -292,7 +395,21 @@ main() { shift || true case "$command" in - start) start ;; + start) + case "${1:-}" in + --json) + shift + start true "$@" + ;; + "") + start false + ;; + *) + usage >&2 + exit 1 + ;; + esac + ;; status) status ;; adb) adb_reverse ;; stop) stop ;; From fdfb10c63b60d502df65c9ad2601831582d95946 Mon Sep 17 00:00:00 2001 From: Piotr Stachyra Date: Fri, 19 Jun 2026 10:41:31 +0200 Subject: [PATCH 04/12] feat: emulator start/restart --- README.md | 6 ++++ scripts/trezor-emulator | 67 +++++++++++++++++++++++++++++++++-------- 2 files changed, 60 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index 11fafef2..689e86cc 100644 --- a/README.md +++ b/README.md @@ -125,6 +125,12 @@ Start or reset the emulator: ./scripts/trezor-emulator status ``` +`start` refuses to wipe/reseed an already-running emulator. Use `--reset` when you explicitly want a fresh device: + +```bash +./scripts/trezor-emulator start --reset +``` + `start` prints the generated mnemonic and the first native regtest receive address (`m/84h/1h/0h/0/0`) so it can be funded during manual checks. For CI or scripts, use JSON output: diff --git a/scripts/trezor-emulator b/scripts/trezor-emulator index 2f900eb0..1326c8f5 100755 --- a/scripts/trezor-emulator +++ b/scripts/trezor-emulator @@ -18,6 +18,8 @@ Commands: start Start Trezor User Env, Bridge, and a T2T1 emulator start --json Start and print a machine-readable summary to stdout + start --reset + Wipe and restart the emulator even if one is already running status Show User Env bridge/emulator status and Bridge enumerate output adb Reverse the Bridge port for a physical Android device stop Stop the emulator and Bridge; stop repo-managed User Env when owned here @@ -270,6 +272,25 @@ bridge_enumerate() { curl --fail --silent --show-error -X POST "$BRIDGE_URL/enumerate" } +emulator_running() { + local status + + status="$(controller status 2>/dev/null || true)" + STATUS="$status" python3 - <<'PY' +import json +import os +import sys + +try: + status = json.loads(os.environ.get("STATUS", "")) +except json.JSONDecodeError: + print("false") + raise SystemExit(0) + +print("true" if status.get("emulator_status", {}).get("is_running") else "false") +PY +} + print_ready_notes() { cat <&2 + if [[ "$reset" != "true" && "$(emulator_running)" == "true" ]]; then + echo "Trezor emulator is already running. Use './scripts/trezor-emulator status', './scripts/trezor-emulator stop', or './scripts/trezor-emulator start --reset'." >&2 + exit 1 + fi mnemonic="$(resolve_mnemonic)" TREZOR_MNEMONIC="$mnemonic" controller setup >&2 address="$(get_address)" @@ -334,6 +360,14 @@ start() { fi start_env + if [[ "$reset" != "true" && "$(emulator_running)" == "true" ]]; then + echo "Trezor emulator is already running." + echo + echo "Use './scripts/trezor-emulator status' to inspect it." + echo "Use './scripts/trezor-emulator stop' before starting a new one." + echo "Use './scripts/trezor-emulator start --reset' to wipe and reseed it." + exit 1 + fi mnemonic="$(resolve_mnemonic)" TREZOR_MNEMONIC="$mnemonic" controller setup address="$(get_address)" @@ -396,19 +430,26 @@ main() { case "$command" in start) - case "${1:-}" in - --json) - shift - start true "$@" - ;; - "") - start false - ;; - *) - usage >&2 - exit 1 - ;; - esac + local output_json="false" + local reset="false" + + while [[ $# -gt 0 ]]; do + case "$1" in + --json) + output_json="true" + ;; + --reset) + reset="true" + ;; + *) + usage >&2 + exit 1 + ;; + esac + shift + done + + start "$output_json" "$reset" ;; status) status ;; adb) adb_reverse ;; From dbd531632d9b21b02cb50a018e74ed9e627e3466 Mon Sep 17 00:00:00 2001 From: Piotr Stachyra Date: Fri, 19 Jun 2026 10:48:44 +0200 Subject: [PATCH 05/12] feat: trezor status json --- README.md | 1 + scripts/trezor-emulator | 51 +++++++++++++++++++++++++++++++++++++++-- 2 files changed, 50 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 689e86cc..0220a2d2 100644 --- a/README.md +++ b/README.md @@ -137,6 +137,7 @@ For CI or scripts, use JSON output: ```bash ./scripts/trezor-emulator start --json > artifacts/trezor-emulator.json +./scripts/trezor-emulator status --json ``` Use the deterministic seed when you want to reuse known history/funds: diff --git a/scripts/trezor-emulator b/scripts/trezor-emulator index 1326c8f5..2722367b 100755 --- a/scripts/trezor-emulator +++ b/scripts/trezor-emulator @@ -5,6 +5,7 @@ ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" COMPOSE_FILE="$ROOT_DIR/docker/docker-compose.yml" CONTROLLER_SCRIPT="$ROOT_DIR/scripts/trezor-controller.py" TREZOR_DATA_DIR="$ROOT_DIR/docker/.trezor-user-env" +TREZOR_METADATA_FILE="$TREZOR_DATA_DIR/trezor-emulator.json" DASHBOARD_URL="${TREZOR_DASHBOARD_URL:-http://localhost:9002}" BRIDGE_URL="${TREZOR_BRIDGE_URL:-http://localhost:21325}" @@ -21,6 +22,8 @@ Commands: start --reset Wipe and restart the emulator even if one is already running status Show User Env bridge/emulator status and Bridge enumerate output + status --json + Print the last start metadata, including the receive address adb Reverse the Bridge port for a physical Android device stop Stop the emulator and Bridge; stop repo-managed User Env when owned here logs Tail the User Env container logs @@ -330,7 +333,7 @@ import os print(json.dumps({ "dashboardUrl": os.environ["DASHBOARD_URL"], "bridgeUrl": os.environ["BRIDGE_URL"], - "mnemonic": os.environ["MNEMONIC"], + "mnemonic": os.environ["MNEMONIC"] or None, "address": { "coin": os.environ["ADDRESS_COIN"], "path": os.environ["ADDRESS_PATH"], @@ -340,6 +343,14 @@ print(json.dumps({ PY } +write_json_summary() { + local mnemonic="$1" + local address="$2" + + mkdir -p "$TREZOR_DATA_DIR" + print_json_summary "$mnemonic" "$address" > "$TREZOR_METADATA_FILE" +} + start() { local output_json="${1:-false}" local reset="${2:-false}" @@ -355,6 +366,7 @@ start() { mnemonic="$(resolve_mnemonic)" TREZOR_MNEMONIC="$mnemonic" controller setup >&2 address="$(get_address)" + write_json_summary "$mnemonic" "$address" print_json_summary "$mnemonic" "$address" return fi @@ -371,6 +383,7 @@ start() { mnemonic="$(resolve_mnemonic)" TREZOR_MNEMONIC="$mnemonic" controller setup address="$(get_address)" + write_json_summary "$mnemonic" "$address" local address_path="${TREZOR_ADDRESS_PATH:-m/84h/1h/0h/0/0}" local display_path="${address_path//h/\'}" @@ -391,6 +404,22 @@ status() { echo } +status_json() { + local address + + wait_for_controller + + if [[ -f "$TREZOR_METADATA_FILE" ]]; then + python3 -m json.tool "$TREZOR_METADATA_FILE" >/dev/null + cat "$TREZOR_METADATA_FILE" + echo + return + fi + + address="$(get_address)" + print_json_summary "" "$address" +} + adb_reverse() { if ! command -v adb >/dev/null 2>&1; then echo "adb is not available on PATH." >&2 @@ -451,7 +480,25 @@ main() { start "$output_json" "$reset" ;; - status) status ;; + status) + case "${1:-}" in + --json) + shift + if [[ $# -ne 0 ]]; then + usage >&2 + exit 1 + fi + status_json + ;; + "") + status + ;; + *) + usage >&2 + exit 1 + ;; + esac + ;; adb) adb_reverse ;; stop) stop ;; logs) logs ;; From e805ce850242b6090522e4bd97686ff74565f1a5 Mon Sep 17 00:00:00 2001 From: Piotr Stachyra Date: Fri, 19 Jun 2026 10:58:42 +0200 Subject: [PATCH 06/12] chore: refactor get-address --- scripts/trezor-controller.py | 36 +++++++++++++++++++++++++++++++++++ scripts/trezor-emulator | 37 ++---------------------------------- 2 files changed, 38 insertions(+), 35 deletions(-) diff --git a/scripts/trezor-controller.py b/scripts/trezor-controller.py index 942aea02..fca4e45e 100755 --- a/scripts/trezor-controller.py +++ b/scripts/trezor-controller.py @@ -11,6 +11,7 @@ import json import os import sys +import time from typing import Any import websockets @@ -94,6 +95,39 @@ async def raw(payload: str) -> None: await send(parsed) +def get_address() -> None: + from trezorlib import btc, messages + from trezorlib.client import get_default_client + from trezorlib.tools import parse_path + from trezorlib.transport.bridge import BridgeTransport + + transport = None + for _ in range(30): + transport = next(iter(BridgeTransport.enumerate()), None) + if transport is not None: + break + time.sleep(1) + + if transport is None: + raise SystemExit("No Trezor Bridge device found.") + + coin = os.environ.get("TREZOR_ADDRESS_COIN", "Regtest") + path = os.environ.get("TREZOR_ADDRESS_PATH", "m/84h/1h/0h/0/0").replace("h", "'") + + client = get_default_client("bitkit-e2e", transport) + with client: + session = client.get_session() + address = btc.get_address( + session, + coin, + parse_path(path), + show_display=False, + script_type=messages.InputScriptType.SPENDWITNESS, + ) + + print(address) + + async def main() -> None: command = sys.argv[1] if len(sys.argv) > 1 else "setup" @@ -109,6 +143,8 @@ async def main() -> None: if len(sys.argv) != 3: raise SystemExit("send-json expects one JSON payload argument") await raw(sys.argv[2]) + elif command == "get-address": + get_address() else: raise SystemExit(f"Unknown controller command: {command}") diff --git a/scripts/trezor-emulator b/scripts/trezor-emulator index 2722367b..e5f63740 100755 --- a/scripts/trezor-emulator +++ b/scripts/trezor-emulator @@ -177,43 +177,10 @@ get_address() { local address_path="${TREZOR_ADDRESS_PATH:-m/84h/1h/0h/0/0}" docker exec -i \ + -e TREZOR_CONTROLLER_WS="${TREZOR_CONTROLLER_WS:-ws://127.0.0.1:9001}" \ -e TREZOR_ADDRESS_COIN="${TREZOR_ADDRESS_COIN:-Regtest}" \ -e TREZOR_ADDRESS_PATH="$address_path" \ - "$TREZOR_CONTAINER" /trezor-user-env/.venv/bin/python3 - <<'PY' -import os -import time - -from trezorlib import btc, messages -from trezorlib.client import get_default_client -from trezorlib.tools import parse_path -from trezorlib.transport.bridge import BridgeTransport - -transport = None -for _ in range(30): - transport = next(iter(BridgeTransport.enumerate()), None) - if transport is not None: - break - time.sleep(1) - -if transport is None: - raise SystemExit("No Trezor Bridge device found.") - -coin = os.environ["TREZOR_ADDRESS_COIN"] -path = os.environ["TREZOR_ADDRESS_PATH"].replace("h", "'") - -client = get_default_client("bitkit-e2e", transport) -with client: - session = client.get_session() - address = btc.get_address( - session, - coin, - parse_path(path), - show_display=False, - script_type=messages.InputScriptType.SPENDWITNESS, - ) - -print(address) -PY + "$TREZOR_CONTAINER" "${TREZOR_CONTAINER_PYTHON:-/trezor-user-env/.venv/bin/python3}" - get-address < "$CONTROLLER_SCRIPT" } print_address() { From e18b22fb4b9c3352eae1ae2c5ce0a2842d6f617b Mon Sep 17 00:00:00 2001 From: Piotr Stachyra Date: Fri, 19 Jun 2026 11:05:14 +0200 Subject: [PATCH 07/12] feat: simplify status, remove reset --- README.md | 11 ++++------- scripts/trezor-emulator | 36 +++++------------------------------- 2 files changed, 9 insertions(+), 38 deletions(-) diff --git a/README.md b/README.md index 0220a2d2..2b335261 100644 --- a/README.md +++ b/README.md @@ -118,20 +118,15 @@ Default emulator state: - Passphrase protection: off - Label: `Bitkit Test Trezor` -Start or reset the emulator: +Start the emulator: ```bash ./scripts/trezor-emulator start ./scripts/trezor-emulator status ``` -`start` refuses to wipe/reseed an already-running emulator. Use `--reset` when you explicitly want a fresh device: - -```bash -./scripts/trezor-emulator start --reset -``` - `start` prints the generated mnemonic and the first native regtest receive address (`m/84h/1h/0h/0/0`) so it can be funded during manual checks. +`start` refuses to wipe/reseed an already-running emulator. Use `stop` before starting a new one. For CI or scripts, use JSON output: @@ -140,6 +135,8 @@ For CI or scripts, use JSON output: ./scripts/trezor-emulator status --json ``` +`status --json` returns the current receive address metadata, but not the mnemonic. + Use the deterministic seed when you want to reuse known history/funds: ```bash diff --git a/scripts/trezor-emulator b/scripts/trezor-emulator index e5f63740..e205d3de 100755 --- a/scripts/trezor-emulator +++ b/scripts/trezor-emulator @@ -5,7 +5,6 @@ ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" COMPOSE_FILE="$ROOT_DIR/docker/docker-compose.yml" CONTROLLER_SCRIPT="$ROOT_DIR/scripts/trezor-controller.py" TREZOR_DATA_DIR="$ROOT_DIR/docker/.trezor-user-env" -TREZOR_METADATA_FILE="$TREZOR_DATA_DIR/trezor-emulator.json" DASHBOARD_URL="${TREZOR_DASHBOARD_URL:-http://localhost:9002}" BRIDGE_URL="${TREZOR_BRIDGE_URL:-http://localhost:21325}" @@ -19,11 +18,9 @@ Commands: start Start Trezor User Env, Bridge, and a T2T1 emulator start --json Start and print a machine-readable summary to stdout - start --reset - Wipe and restart the emulator even if one is already running status Show User Env bridge/emulator status and Bridge enumerate output status --json - Print the last start metadata, including the receive address + Print the current receive address metadata adb Reverse the Bridge port for a physical Android device stop Stop the emulator and Bridge; stop repo-managed User Env when owned here logs Tail the User Env container logs @@ -310,47 +307,35 @@ print(json.dumps({ PY } -write_json_summary() { - local mnemonic="$1" - local address="$2" - - mkdir -p "$TREZOR_DATA_DIR" - print_json_summary "$mnemonic" "$address" > "$TREZOR_METADATA_FILE" -} - start() { local output_json="${1:-false}" - local reset="${2:-false}" local mnemonic local address if [[ "$output_json" == "true" ]]; then start_env >&2 - if [[ "$reset" != "true" && "$(emulator_running)" == "true" ]]; then - echo "Trezor emulator is already running. Use './scripts/trezor-emulator status', './scripts/trezor-emulator stop', or './scripts/trezor-emulator start --reset'." >&2 + if [[ "$(emulator_running)" == "true" ]]; then + echo "Trezor emulator is already running. Use './scripts/trezor-emulator status' or './scripts/trezor-emulator stop' before starting a new one." >&2 exit 1 fi mnemonic="$(resolve_mnemonic)" TREZOR_MNEMONIC="$mnemonic" controller setup >&2 address="$(get_address)" - write_json_summary "$mnemonic" "$address" print_json_summary "$mnemonic" "$address" return fi start_env - if [[ "$reset" != "true" && "$(emulator_running)" == "true" ]]; then + if [[ "$(emulator_running)" == "true" ]]; then echo "Trezor emulator is already running." echo echo "Use './scripts/trezor-emulator status' to inspect it." echo "Use './scripts/trezor-emulator stop' before starting a new one." - echo "Use './scripts/trezor-emulator start --reset' to wipe and reseed it." exit 1 fi mnemonic="$(resolve_mnemonic)" TREZOR_MNEMONIC="$mnemonic" controller setup address="$(get_address)" - write_json_summary "$mnemonic" "$address" local address_path="${TREZOR_ADDRESS_PATH:-m/84h/1h/0h/0/0}" local display_path="${address_path//h/\'}" @@ -376,13 +361,6 @@ status_json() { wait_for_controller - if [[ -f "$TREZOR_METADATA_FILE" ]]; then - python3 -m json.tool "$TREZOR_METADATA_FILE" >/dev/null - cat "$TREZOR_METADATA_FILE" - echo - return - fi - address="$(get_address)" print_json_summary "" "$address" } @@ -427,16 +405,12 @@ main() { case "$command" in start) local output_json="false" - local reset="false" while [[ $# -gt 0 ]]; do case "$1" in --json) output_json="true" ;; - --reset) - reset="true" - ;; *) usage >&2 exit 1 @@ -445,7 +419,7 @@ main() { shift done - start "$output_json" "$reset" + start "$output_json" ;; status) case "${1:-}" in From b0986280ccfd17c5dd3a272c7ae6ab7fba3ad342 Mon Sep 17 00:00:00 2001 From: Piotr Stachyra Date: Fri, 19 Jun 2026 15:05:17 +0200 Subject: [PATCH 08/12] test: initial hardware wallet e2e --- test/helpers/hardware-wallet.ts | 129 ++++++++++++++++++++++++++++++ test/helpers/navigation.ts | 23 ++++++ test/specs/hardware-wallet.e2e.ts | 42 ++++++++++ 3 files changed, 194 insertions(+) create mode 100644 test/helpers/hardware-wallet.ts create mode 100644 test/specs/hardware-wallet.e2e.ts diff --git a/test/helpers/hardware-wallet.ts b/test/helpers/hardware-wallet.ts new file mode 100644 index 00000000..0d41db52 --- /dev/null +++ b/test/helpers/hardware-wallet.ts @@ -0,0 +1,129 @@ +import { execFileSync } from 'node:child_process'; +import fs from 'node:fs'; +import path from 'node:path'; + +import { + doNavigationClose, + elementById, + expectText, + sleep, + tap, + typeText, +} from './actions'; +import { openHomeWidgets, openSettings } from './navigation'; + +const E2E_ROOT = path.resolve(__dirname, '..', '..'); +const ARTIFACTS_DIR = path.join(E2E_ROOT, 'artifacts'); +const TREZOR_FIXTURE_PATH = path.join(ARTIFACTS_DIR, 'trezor-emulator.json'); + +export type TrezorEmulatorFixture = { + dashboardUrl: string; + bridgeUrl: string; + mnemonic: string | null; + address: { + coin: string; + path: string; + value: string; + }; +}; + +function runTrezorEmulatorJson(args: string[]): TrezorEmulatorFixture { + const output = execFileSync('./scripts/trezor-emulator', args, { + cwd: E2E_ROOT, + encoding: 'utf8', + stdio: ['ignore', 'pipe', 'inherit'], + }); + return JSON.parse(output) as TrezorEmulatorFixture; +} + +function writeFixture(fixture: TrezorEmulatorFixture): TrezorEmulatorFixture { + fs.mkdirSync(ARTIFACTS_DIR, { recursive: true }); + fs.writeFileSync(TREZOR_FIXTURE_PATH, `${JSON.stringify(fixture, null, 2)}\n`); + return fixture; +} + +export function ensureTrezorEmulator(): TrezorEmulatorFixture { + try { + return writeFixture(runTrezorEmulatorJson(['status', '--json'])); + } catch { + return writeFixture(runTrezorEmulatorJson(['start', '--json'])); + } +} + +export async function openHardwareWalletSettings() { + await openSettings('general'); + await tap('HardwareWalletsSettings'); + await elementById('HardwareWalletsScreen').waitForDisplayed(); +} + +export async function startHardwareWalletFlowFromSuggestion() { + await openHomeWidgets(); + await elementById('Suggestion-hardware').waitForDisplayed({ timeout: 30_000 }); + await tap('Suggestion-hardware'); +} + +export async function connectHardwareWalletFromSettings(label: string) { + await openHardwareWalletSettings(); + await tap('AddHardwareWallet'); + await completeHardwareWalletFlow(label); + await elementById('HardwareWalletsScreen').waitForDisplayed(); +} + +export async function completeHardwareWalletFlow(label: string) { + await elementById('HardwareWalletIntroScreen').waitForDisplayed(); + await sleep(1000); + await tap('HardwareWalletIntroContinue'); + + await elementById('HardwareWalletFoundScreen').waitForDisplayed({ timeout: 60_000 }); + await elementById('HardwareWalletFoundDeviceName').waitForDisplayed(); + await sleep(1000); + await tap('HardwareWalletFoundConnect'); + + await elementById('HardwareWalletPairedScreen').waitForDisplayed({ timeout: 60_000 }); + await typeText('HardwareWalletLabelInput', label); + await tap('HardwareWalletPairedFinish'); + await sleep(1000); +} + +export async function expectHardwareWalletInSettings( + label: string, + { visible }: { visible: boolean } +) { + await expectText(label, { visible, strategy: 'contains' }); +} + +export async function expectHardwareSuggestion({ visible }: { visible: boolean }) { + if (visible) { + await openHomeWidgets(); + } + await elementById('Suggestion-hardware').waitForDisplayed({ + reverse: !visible, + timeout: 30_000, + }); +} + +export async function expectHardwareWalletOnHome(label: string, { visible }: { visible: boolean }) { + await doNavigationClose(); + await elementById('ActivityHardware').waitForDisplayed({ + reverse: !visible, + timeout: 30_000, + }); + if (visible) { + await expectText(label, { strategy: 'contains' }); + } +} + +export async function removeHardwareWalletFromSettings(label: string) { + await openHardwareWalletSettings(); + await expectHardwareWalletInSettings(label, { visible: true }); + await tapFirstHardwareWalletDelete(); + await tap('DialogConfirm'); + await sleep(500); + await expectHardwareWalletInSettings(label, { visible: false }); +} + +async function tapFirstHardwareWalletDelete() { + const deleteButton = await $('android=new UiSelector().resourceIdMatches(".*HardwareWalletRowDelete_.*")'); + await deleteButton.waitForDisplayed({ timeout: 30_000 }); + await deleteButton.click(); +} diff --git a/test/helpers/navigation.ts b/test/helpers/navigation.ts index 585a61d5..8b566e8b 100644 --- a/test/helpers/navigation.ts +++ b/test/helpers/navigation.ts @@ -53,6 +53,18 @@ export async function openProfile() { await sleep(500); } +/** + * Opens the Home widgets page from the drawer. + * On first use, the app shows the widgets intro screen; choose the + * View & Organize action to land on the widgets page. + */ +export async function openHomeWidgets() { + await tap('HeaderMenu'); + await tap('DrawerWidgets'); + await tapWidgetsIntroViewOrganizeIfShown(); + await elementById('SuggestionsWidget').waitForDisplayed({ timeout: 30_000 }); +} + /** * Closes the drawer and navigates back to the Wallet home screen. */ @@ -75,3 +87,14 @@ export async function doTriggerTimedSheet() { await sleep(500); await doNavigationClose(); } + +async function tapWidgetsIntroViewOrganizeIfShown() { + const viewOrganize = elementById('WidgetsOnboardingViewOrganize'); + try { + await viewOrganize.waitForDisplayed({ timeout: 3_000 }); + await viewOrganize.click(); + await sleep(500); + } catch { + // Widgets intro is shown only once. + } +} diff --git a/test/specs/hardware-wallet.e2e.ts b/test/specs/hardware-wallet.e2e.ts new file mode 100644 index 00000000..0a688ebe --- /dev/null +++ b/test/specs/hardware-wallet.e2e.ts @@ -0,0 +1,42 @@ +import { completeOnboarding } from '../helpers/actions'; +import { + completeHardwareWalletFlow, + ensureTrezorEmulator, + expectHardwareSuggestion, + expectHardwareWalletInSettings, + expectHardwareWalletOnHome, + openHardwareWalletSettings, + removeHardwareWalletFromSettings, + startHardwareWalletFlowFromSuggestion, +} from '../helpers/hardware-wallet'; +import { reinstallApp } from '../helpers/setup'; +import { ciIt } from '../helpers/suite'; + +describe('@hardware_wallet - Hardware Wallet', () => { + const walletLabel = 'E2E Trezor'; + + before(function () { + if (!driver.isAndroid) { + this.skip(); + } + ensureTrezorEmulator(); + }); + + beforeEach(async () => { + await reinstallApp(); + await completeOnboarding(); + }); + + ciIt('@hardware_wallet_1 - Can connect, show, and remove a Trezor emulator wallet', async () => { + await expectHardwareSuggestion({ visible: true }); + await startHardwareWalletFlowFromSuggestion(); + await completeHardwareWalletFlow(walletLabel); + await expectHardwareSuggestion({ visible: false }); + await openHardwareWalletSettings(); + await expectHardwareWalletInSettings(walletLabel, { visible: true }); + await expectHardwareWalletOnHome(walletLabel, { visible: true }); + await removeHardwareWalletFromSettings(walletLabel); + await expectHardwareWalletInSettings(walletLabel, { visible: false }); + await expectHardwareWalletOnHome(walletLabel, { visible: false }); + }); +}); From b64bb43984bc1c2cd6c764ac170a38e3f1051d0d Mon Sep 17 00:00:00 2001 From: Piotr Stachyra Date: Fri, 19 Jun 2026 15:43:09 +0200 Subject: [PATCH 09/12] test: add hardware wallet receive e2e --- test/helpers/actions.ts | 12 ++++++---- test/helpers/hardware-wallet.ts | 37 +++++++++++++++++++++++++++++++ test/specs/hardware-wallet.e2e.ts | 29 ++++++++++++++++++++++-- 3 files changed, 72 insertions(+), 6 deletions(-) diff --git a/test/helpers/actions.ts b/test/helpers/actions.ts index 7cc6f24b..ff488be4 100644 --- a/test/helpers/actions.ts +++ b/test/helpers/actions.ts @@ -327,7 +327,7 @@ function checkBalanceCondition( } } -async function expectBalanceWithWait( +export async function expectBalanceWithWait( getter: () => Promise, name: string, expected: number, @@ -1151,10 +1151,12 @@ export async function receiveOnchainFunds({ sats = 100_000, blocksToMine = 1, expectHighBalanceWarning = false, + verifyBalances = true, }: { sats?: number; blocksToMine?: number; expectHighBalanceWarning?: boolean; + verifyBalances?: boolean; } = {}) { // receive some first const address = await getReceiveAddress(); @@ -1168,9 +1170,11 @@ export async function receiveOnchainFunds({ await acknowledgeReceivedPaymentIfPresent(); } - await expectTotalBalance(sats); - await expectSavingsBalance(sats); - await expectSpendingBalance(0); + if (verifyBalances) { + await expectTotalBalance(sats); + await expectSavingsBalance(sats); + await expectSpendingBalance(0); + } await dismissBackupTimedSheet({ triggerTimedSheet: true }); if (expectHighBalanceWarning) { diff --git a/test/helpers/hardware-wallet.ts b/test/helpers/hardware-wallet.ts index 0d41db52..6c606c8e 100644 --- a/test/helpers/hardware-wallet.ts +++ b/test/helpers/hardware-wallet.ts @@ -3,14 +3,20 @@ import fs from 'node:fs'; import path from 'node:path'; import { + acknowledgeReceivedPaymentIfPresent, doNavigationClose, elementById, + expectBalanceWithWait, + expectTextWithin, expectText, + formatSats, + getAmountUnder, sleep, tap, typeText, } from './actions'; import { openHomeWidgets, openSettings } from './navigation'; +import { deposit, mineBlocks } from './regtest'; const E2E_ROOT = path.resolve(__dirname, '..', '..'); const ARTIFACTS_DIR = path.join(E2E_ROOT, 'artifacts'); @@ -113,6 +119,36 @@ export async function expectHardwareWalletOnHome(label: string, { visible }: { v } } +export async function expectHardwareWalletBalance(expected: number): Promise { + return expectBalanceWithWait( + () => getAmountUnder('ActivityHardware'), + 'hardware wallet', + expected, + ); +} + +export async function fundHardwareWalletAndAcknowledge( + fixture: TrezorEmulatorFixture, + { sats = 15_000, blocksToMine = 1 }: { sats?: number; blocksToMine?: number } = {} +) { + await deposit(fixture.address.value, sats); + if (blocksToMine > 0) { + await mineBlocks(blocksToMine); + } + await acknowledgeReceivedPaymentIfPresent(); +} + +export async function expectHardwareWalletReceivedActivity(sats: number) { + await doNavigationClose(); + await elementById('ActivityHardware').waitForDisplayed(); + await expectTextWithin('ActivityHardware', formatSats(sats)); + await tap('ActivityHardware'); + await elementById('HardwareWalletScreen').waitForDisplayed(); + await elementById('Activity-1').waitForDisplayed(); + await expectTextWithin('Activity-1', 'Received'); + await expectTextWithin('Activity-1', formatSats(sats)); +} + export async function removeHardwareWalletFromSettings(label: string) { await openHardwareWalletSettings(); await expectHardwareWalletInSettings(label, { visible: true }); @@ -127,3 +163,4 @@ async function tapFirstHardwareWalletDelete() { await deleteButton.waitForDisplayed({ timeout: 30_000 }); await deleteButton.click(); } + diff --git a/test/specs/hardware-wallet.e2e.ts b/test/specs/hardware-wallet.e2e.ts index 0a688ebe..11eb5514 100644 --- a/test/specs/hardware-wallet.e2e.ts +++ b/test/specs/hardware-wallet.e2e.ts @@ -1,25 +1,31 @@ -import { completeOnboarding } from '../helpers/actions'; +import { completeOnboarding, doNavigationClose, expectSavingsBalance, expectSpendingBalance, expectTotalBalance, receiveOnchainFunds } from '../helpers/actions'; import { completeHardwareWalletFlow, + connectHardwareWalletFromSettings, ensureTrezorEmulator, expectHardwareSuggestion, + expectHardwareWalletBalance, expectHardwareWalletInSettings, expectHardwareWalletOnHome, + expectHardwareWalletReceivedActivity, + fundHardwareWalletAndAcknowledge, openHardwareWalletSettings, removeHardwareWalletFromSettings, startHardwareWalletFlowFromSuggestion, + type TrezorEmulatorFixture, } from '../helpers/hardware-wallet'; import { reinstallApp } from '../helpers/setup'; import { ciIt } from '../helpers/suite'; describe('@hardware_wallet - Hardware Wallet', () => { const walletLabel = 'E2E Trezor'; + let trezorFixture: TrezorEmulatorFixture; before(function () { if (!driver.isAndroid) { this.skip(); } - ensureTrezorEmulator(); + trezorFixture = ensureTrezorEmulator(); }); beforeEach(async () => { @@ -39,4 +45,23 @@ describe('@hardware_wallet - Hardware Wallet', () => { await expectHardwareWalletInSettings(walletLabel, { visible: false }); await expectHardwareWalletOnHome(walletLabel, { visible: false }); }); + + ciIt('@hardware_wallet_2 - Can receive onchain funds to hardware wallet', async () => { + const sats = 15_000; + + await connectHardwareWalletFromSettings(walletLabel); + await fundHardwareWalletAndAcknowledge(trezorFixture, { sats }); + await expectHardwareWalletReceivedActivity(sats); + await doNavigationClose(); + await expectTotalBalance(sats); + await expectHardwareWalletBalance(sats); + await expectSavingsBalance(0); + await expectSpendingBalance(0); + + await receiveOnchainFunds({ sats, verifyBalances: false }); + await expectTotalBalance(2 * sats); + await expectHardwareWalletBalance(sats); + await expectSavingsBalance(sats); + await expectSpendingBalance(0); + }); }); From 9dbb12220a248c33bdde92302ef1f6f8d3ab681d Mon Sep 17 00:00:00 2001 From: Piotr Stachyra Date: Fri, 19 Jun 2026 15:53:33 +0200 Subject: [PATCH 10/12] fix: make it work on local backend --- test/specs/hardware-wallet.e2e.ts | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/test/specs/hardware-wallet.e2e.ts b/test/specs/hardware-wallet.e2e.ts index 11eb5514..8d5d1a71 100644 --- a/test/specs/hardware-wallet.e2e.ts +++ b/test/specs/hardware-wallet.e2e.ts @@ -1,4 +1,5 @@ import { completeOnboarding, doNavigationClose, expectSavingsBalance, expectSpendingBalance, expectTotalBalance, receiveOnchainFunds } from '../helpers/actions'; +import initElectrum from '../helpers/electrum'; import { completeHardwareWalletFlow, connectHardwareWalletFromSettings, @@ -14,23 +15,32 @@ import { startHardwareWalletFlowFromSuggestion, type TrezorEmulatorFixture, } from '../helpers/hardware-wallet'; +import { ensureLocalFunds } from '../helpers/regtest'; import { reinstallApp } from '../helpers/setup'; import { ciIt } from '../helpers/suite'; describe('@hardware_wallet - Hardware Wallet', () => { const walletLabel = 'E2E Trezor'; let trezorFixture: TrezorEmulatorFixture; + let electrum: Awaited> | undefined; - before(function () { + before(async function () { if (!driver.isAndroid) { this.skip(); } - trezorFixture = ensureTrezorEmulator(); + await ensureLocalFunds(); + electrum = await initElectrum(); }); beforeEach(async () => { + trezorFixture = ensureTrezorEmulator(); await reinstallApp(); await completeOnboarding(); + await electrum?.waitForSync(); + }); + + after(async () => { + await electrum?.stop(); }); ciIt('@hardware_wallet_1 - Can connect, show, and remove a Trezor emulator wallet', async () => { From 87a054c201d2d63af7eae39873cfd4ac8fd8f371 Mon Sep 17 00:00:00 2001 From: Piotr Stachyra Date: Fri, 19 Jun 2026 16:03:40 +0200 Subject: [PATCH 11/12] teat: fresh trezor emulator for each test --- test/helpers/hardware-wallet.ts | 14 +++++++++++++- test/specs/hardware-wallet.e2e.ts | 2 +- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/test/helpers/hardware-wallet.ts b/test/helpers/hardware-wallet.ts index 6c606c8e..7df9604c 100644 --- a/test/helpers/hardware-wallet.ts +++ b/test/helpers/hardware-wallet.ts @@ -42,13 +42,25 @@ function runTrezorEmulatorJson(args: string[]): TrezorEmulatorFixture { return JSON.parse(output) as TrezorEmulatorFixture; } +function runTrezorEmulator(args: string[]) { + execFileSync('./scripts/trezor-emulator', args, { + cwd: E2E_ROOT, + stdio: 'inherit', + }); +} + function writeFixture(fixture: TrezorEmulatorFixture): TrezorEmulatorFixture { fs.mkdirSync(ARTIFACTS_DIR, { recursive: true }); fs.writeFileSync(TREZOR_FIXTURE_PATH, `${JSON.stringify(fixture, null, 2)}\n`); return fixture; } -export function ensureTrezorEmulator(): TrezorEmulatorFixture { +export function ensureTrezorEmulator({ fresh = false }: { fresh?: boolean } = {}): TrezorEmulatorFixture { + if (fresh) { + runTrezorEmulator(['stop']); + return writeFixture(runTrezorEmulatorJson(['start', '--json'])); + } + try { return writeFixture(runTrezorEmulatorJson(['status', '--json'])); } catch { diff --git a/test/specs/hardware-wallet.e2e.ts b/test/specs/hardware-wallet.e2e.ts index 8d5d1a71..baad7bc6 100644 --- a/test/specs/hardware-wallet.e2e.ts +++ b/test/specs/hardware-wallet.e2e.ts @@ -33,7 +33,7 @@ describe('@hardware_wallet - Hardware Wallet', () => { }); beforeEach(async () => { - trezorFixture = ensureTrezorEmulator(); + trezorFixture = ensureTrezorEmulator({ fresh: true }); await reinstallApp(); await completeOnboarding(); await electrum?.waitForSync(); From fc17b00047f163ba179dce35d7d7a1f0e626d04e Mon Sep 17 00:00:00 2001 From: Piotr Stachyra Date: Sat, 20 Jun 2026 11:48:39 +0200 Subject: [PATCH 12/12] test: adjustments --- test/helpers/hardware-wallet.ts | 8 ++++---- test/helpers/navigation.ts | 1 + 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/test/helpers/hardware-wallet.ts b/test/helpers/hardware-wallet.ts index 7df9604c..485a7b7f 100644 --- a/test/helpers/hardware-wallet.ts +++ b/test/helpers/hardware-wallet.ts @@ -14,6 +14,7 @@ import { sleep, tap, typeText, + handleAndroidAlert, } from './actions'; import { openHomeWidgets, openSettings } from './navigation'; import { deposit, mineBlocks } from './regtest'; @@ -91,13 +92,12 @@ export async function completeHardwareWalletFlow(label: string) { await elementById('HardwareWalletIntroScreen').waitForDisplayed(); await sleep(1000); await tap('HardwareWalletIntroContinue'); + await handleAndroidAlert(); - await elementById('HardwareWalletFoundScreen').waitForDisplayed({ timeout: 60_000 }); - await elementById('HardwareWalletFoundDeviceName').waitForDisplayed(); + await elementById('HardwareWalletFoundScreen').waitForDisplayed(); await sleep(1000); await tap('HardwareWalletFoundConnect'); - - await elementById('HardwareWalletPairedScreen').waitForDisplayed({ timeout: 60_000 }); + await elementById('HardwareWalletPairedScreen').waitForDisplayed(); await typeText('HardwareWalletLabelInput', label); await tap('HardwareWalletPairedFinish'); await sleep(1000); diff --git a/test/helpers/navigation.ts b/test/helpers/navigation.ts index 8b566e8b..fab97107 100644 --- a/test/helpers/navigation.ts +++ b/test/helpers/navigation.ts @@ -92,6 +92,7 @@ async function tapWidgetsIntroViewOrganizeIfShown() { const viewOrganize = elementById('WidgetsOnboardingViewOrganize'); try { await viewOrganize.waitForDisplayed({ timeout: 3_000 }); + await sleep(500); await viewOrganize.click(); await sleep(500); } catch {