File: trezorctl_script_client.py

package info (click to toggle)
python-trezor 0.13.10-3
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid
  • size: 1,596 kB
  • sloc: python: 14,947; xml: 33; sh: 16; makefile: 3
file content (136 lines) | stat: -rw-r--r-- 3,706 bytes parent folder | download | duplicates (3)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
"""
Reference client implementation consuming trezorctl's script interface
(ScriptUI class) available by using `--script` flag in any trezorctl command.

Function `get_address()` is showing the communication with ScriptUI
on a specific example
"""

from __future__ import annotations

import os
import subprocess
import typing as t

import click


def parse_args_from_line(line: str) -> tuple[str, dict[str, t.Any]]:
    # ?PIN code=123
    # ?PASSPHRASE available_on_device
    command, *args = line.split(" ")
    result = {}
    for arg in args:
        if "=" in arg:
            key, value = arg.split("=")
            result[key] = value
        else:
            result[arg] = True
    return command, result


def get_pin_from_user(code: str | None = None) -> str:
    # ?PIN
    # ?PIN code=Current
    while True:
        try:
            pin = click.prompt(
                f"Enter PIN (code: {code})",
                hide_input=True,
                default="",
                show_default=False,
            )
        except click.Abort:
            return "CANCEL"
        if not all(c in "123456789" for c in pin):
            click.echo("PIN must only be numbers 1-9")
            continue
        return ":" + pin


def show_button_request(
    code: str | None = None, pages: str | None = None, name: str | None = None
) -> None:
    # ?BUTTON code=Other
    # ?BUTTON code=SignTx pages=2
    # ?BUTTON code=ProtectCall name=confirm_set_pin
    print(f"Please confirm action on Trezor (code={code} name={name} pages={pages})")


def get_passphrase_from_user(available_on_device: bool = False) -> str:
    # ?PASSPHRASE
    # ?PASSPHRASE available_on_device
    if available_on_device:
        if click.confirm("Enter passphrase on device?", default=True):
            return "ON_DEVICE"

    env_passphrase = os.getenv("PASSPHRASE")
    if env_passphrase:
        if click.confirm("Use env PASSPHRASE?", default=False):
            return ":" + env_passphrase

    while True:
        try:
            passphrase = click.prompt("Enter passphrase", hide_input=True, default="")
        except click.Abort:
            return "CANCEL"

        passphrase2 = click.prompt(
            "Enter passphrase again", hide_input=True, default=""
        )
        if passphrase != passphrase2:
            click.echo("Passphrases do not match")
            continue
        return ":" + passphrase


def get_address() -> str:
    args = """
        trezorctl --script get-address -n "m/49h/0h/0h/0/0"
    """.strip()
    p = subprocess.Popen(
        args,
        stdin=subprocess.PIPE,
        stdout=subprocess.PIPE,
        text=True,
        shell=True,
        bufsize=0,
    )

    assert p.stdout is not None
    assert p.stdin is not None

    text_result = []
    while True:
        line = p.stdout.readline().strip()
        if not line:
            break

        if line.startswith("?"):
            command, args = parse_args_from_line(line)
            if command == "?PIN":
                response = get_pin_from_user(**args)
                p.stdin.write(response + "\n")
            elif command == "?PASSPHRASE":
                response = get_passphrase_from_user(**args)
                p.stdin.write(response + "\n")
            elif command == "?BUTTON":
                show_button_request(**args)
            else:
                print("Unrecognized script command:", line)

        text_result.append(line)
        print(line)

    address = text_result[-1]
    print("Address:", address)
    return address


def clear_session_to_enable_pin():
    os.system("trezorctl clear-session")


if __name__ == "__main__":
    get_address()
    clear_session_to_enable_pin()