Skip to content
Draft
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
3 changes: 3 additions & 0 deletions src/controller.py
Original file line number Diff line number Diff line change
Expand Up @@ -328,6 +328,9 @@ def run_emulator_command(self) -> "ResponseType":
return {
"response": f"Read and confirm atomic Shamir mnemonic for {shares} shares and threshold {threshold}."
}
elif self.command == "emulator-read-and-confirm-single-shamir-mnemonic":
emulator.read_and_confirm_single_shamir_mnemonic()
return {"response": "Read and confirm Single-share Shamir mnemonic"}
elif self.command == "emulator-allow-unsafe-paths":
emulator.allow_unsafe()
return {"response": "Allowed unsafe path"}
Expand Down
109 changes: 109 additions & 0 deletions src/emulator.py
Original file line number Diff line number Diff line change
Expand Up @@ -700,6 +700,25 @@ def read_and_confirm_mnemonic_t3b1() -> None:
raise NotImplementedError("T3B1 mnemonic confirmation not implemented yet.")


def read_and_confirm_single_shamir_mnemonic() -> None:
"""Dispatcher for Single-share Shamir backup walkthrough.

Detects the running model and calls the appropriate implementation.
"""
if not VERSION_RUNNING:
raise RuntimeError("No emulator running.")

if models.T3W1.internal_name in VERSION_RUNNING:
read_and_confirm_single_shamir_mnemonic_t3w1()
elif models.T3T1.internal_name in VERSION_RUNNING:
# If implemented for T3T1 in the future:
# read_and_confirm_single_shamir_mnemonic_t3t1()
raise RuntimeError("Single-share Shamir not yet implemented for T3T1.")
else:
# Fallback for models that don't support or haven't implemented this yet
raise RuntimeError(f"Model {VERSION_RUNNING} not supported for Single-share Shamir.")


def read_and_confirm_shamir_mnemonic(shares: int, threshold: int) -> None:
if not VERSION_RUNNING:
raise RuntimeError("No emulator running.")
Expand Down Expand Up @@ -1126,6 +1145,96 @@ def read_and_confirm_shamir_mnemonic_t3w1(shares: int, threshold: int) -> None:
time.sleep(SLEEP)


def read_and_confirm_single_shamir_mnemonic_t3w1() -> None:
"""Performs a walkthrough of the Single-share Shamir backup on T3W1.

This handles the SLIP-39 Single-share flow (Backup Type 3) which skips
share/threshold pickers and starts directly with the 20-word backup.
"""

# T3W1 specific coordinates
BOTTOM_BUTTON_COORDS = (200, 450)
RIGHT_BOTTOM_BUTTON_COORDS = (300, 450)
WORD_CHOICE_COORDS = [(200, 200), (200, 300), (200, 400)]

with connect_to_debuglink() as debug:
debug.watch_layout(True)

# 1. Backup Summary Screen
assert_text_on_screen(debug, "20 words")
debug.click(BOTTOM_BUTTON_COORDS)
time.sleep(SLEEP)

# 2. Safety Warning
assert_text_on_screen(debug, "anywhere digital")
debug.click(BOTTOM_BUTTON_COORDS)
time.sleep(SLEEP)

# 3. Instruction Screen
assert_text_on_screen(debug, "following 20 words in order")
debug.click(BOTTOM_BUTTON_COORDS)
time.sleep(SLEEP)

# 4. Read and store all 20 words
mnemonic: list[str] = []
for _ in range(20):
layout = debug.read_layout()
# Extract word from the current screen
words = layout.seed_words()
if words:
mnemonic.extend(words)
# Swipe up to the next word
if len(mnemonic) < 20:
debug.swipe_up()
time.sleep(SLEEP)

# Exit word list
debug.click(RIGHT_BOTTOM_BUTTON_COORDS)
time.sleep(SLEEP)

# 5. Confirmation Screen
# "I wrote down all 20 words"
assert_text_on_screen(debug, "wrote down")
debug.click(RIGHT_BOTTOM_BUTTON_COORDS)
time.sleep(SLEEP)

# 6. Check wallet backup
assert_text_on_screen(debug, "quick check")
debug.click(BOTTOM_BUTTON_COORDS)
time.sleep(SLEEP)

# 7. Handle the 3-word check (Quiz)
for _ in range(3):
layout = debug.read_layout()
assert_text_on_screen(debug, "Select word")

# Find which position is requested (e.g., "Select word #12")
pattern = r"Select word #(\d+)"
screen_text = layout.text_content()
match = re.search(pattern, screen_text)
if match is None:
raise RuntimeError(f"Could not find word position in: {screen_text}")

word_pos = int(match.group(1))
wanted_word = mnemonic[word_pos - 1].lower()

# Find which of the 3 displayed options matches the mnemonic
displayed_words = [w.lower() for w in screen_text.split()[-3:]]
if wanted_word not in displayed_words:
raise RuntimeError(f"Word '{wanted_word}' not found in options {displayed_words}")

# Click the matching coordinate
word_index = displayed_words.index(wanted_word)
debug.click(WORD_CHOICE_COORDS[word_index])
time.sleep(SLEEP)

# 8. Finalization
# "wallet backup completed"
assert_text_on_screen(debug, "completed")
debug.click(BOTTOM_BUTTON_COORDS)
time.sleep(SLEEP)


def read_and_confirm_atomic_shamir_mnemonic(shares: int, threshold: int) -> None:
"""Handles atomic Shamir backup flow where device starts at number picker.

Expand Down
Loading