bugfix: Mk4: fix extended keys not fully visible in stories

This commit is contained in:
scgbckbone 2025-04-28 12:56:03 +02:00 committed by doc-hex
parent f40d16b76b
commit 51bbee9eb1
4 changed files with 76 additions and 40 deletions

View File

@ -16,6 +16,7 @@ This lists the new changes that have not yet been published in a normal release.
in export loop and needs reboot to escape.
- Bugfix: PUSHDATA2 in bitcoin script caused yikes.
- Bugfix: Warning for unknown scripts was not shown at the top of the signing story.
- Bugfix: Part of extended keys in stories were not visible
# Q Specific Changes

View File

@ -460,28 +460,38 @@ def call_later_ms(delay, cb, *args, **kws):
uasyncio.create_task(doit())
def txtlen(s):
# width of string in chars, accounting for
# double-wide characters which happen on Q.
rv = len(s)
if DOUBLE_WIDE:
rv += sum(1 for ch in s if ch in DOUBLE_WIDE)
return rv
def word_wrap(ln, w):
# Generate the lines needed to wrap one line into X "width"-long lines.
# - tests in testing/test_unit.py
while True:
# ln_len considers DOUBLE_WIDTH chars
ln_len = 0
idx = 0
sp = None
for idx, ch in enumerate(ln):
if ch == ' ':
# split point on space if possible
sp = idx
if ln_len < w:
ln_len += 1
if ch in DOUBLE_WIDE:
ln_len += 1
else:
if (ln_len == w) and (ch in ".,:;"):
# boundary of allowed width
# if . or , allow one more character
# even if only half visible on Mk4
# on Q it's OK as (CHARS_W-1) is used as w
sp = None
idx += 1
if txtlen(ln) <= w:
yield ln
return
break
else:
yield ln
return
while ln:
# find a space in (width) first part of remainder
sp = ln.rfind(' ', 0, w-1)
if sp == -1:
if sp is None:
if ln[0] == OUT_CTRL_ADDRESS:
# special handling for lines w/ payment address in them
# - add same marker to newly split lines
@ -497,8 +507,7 @@ def word_wrap(ln, w):
return
# bad-break the line
sp = min(txtlen(ln), w)
nsp = sp
sp = nsp = idx
if ln[nsp:nsp+1] == ' ':
nsp += 1
else:
@ -506,14 +515,10 @@ def word_wrap(ln, w):
nsp = sp+1
left = ln[0:sp]
ln = ln[nsp:]
if txtlen(left) + 1 + txtlen(ln) <= w:
# not clear when this would happen? final bit??
left = left + ' ' + ln
ln = ''
yield left
ln = ln[nsp:]
if not ln: return
def parse_extended_key(ln, private=False):
# read an xpub/ypub/etc and return BIP-32 node and what chain it's on.

View File

@ -17,7 +17,8 @@ DEFAULT_IDLE_TIMEOUT = const(4*3600) # (seconds) 4 hours
# See ux_mk or ux_q1 for some display functions now
if version.has_qwerty:
from lcd_display import CHARS_W, CHARS_H
CH_PER_W = CHARS_W
# stories look nicer if we do not use the whole width
CH_PER_W = (CHARS_W - 1)
STORY_H = CHARS_H
from ux_q1 import PressRelease, ux_enter_number, ux_input_text, ux_show_pin
from ux_q1 import ux_login_countdown, ux_dice_rolling, ux_render_words
@ -28,9 +29,9 @@ if version.has_qwerty:
else:
# How many characters can we fit on each line? How many lines?
# (using FontSmall) .. except it's an approximation since variable-width font.
# - even 19 could work sometimes, but not when line is completely full
# - 18 can work but rightmost spot is half-width. We allow . and , in that spot.
# - really should look at rendered-width of text
CH_PER_W = 19
CH_PER_W = 17
STORY_H = 5
from ux_mk4 import PressRelease, ux_enter_number, ux_input_text, ux_show_pin
from ux_mk4 import ux_login_countdown, ux_dice_rolling, ux_render_words

View File

@ -5,6 +5,8 @@
import pytest, os, shutil
from helpers import B2A
from constants import AF_P2WSH, AF_P2SH, AF_P2WSH_P2SH, AF_CLASSIC, AF_P2WPKH, AF_P2WPKH_P2SH
from charcodes import *
def test_remote_exec(sim_exec):
@ -274,28 +276,55 @@ def test_is_dir(microsd_path, sim_exec):
assert rv == "False"
shutil.rmtree(microsd_path("my_dir"))
@pytest.mark.parametrize('txt, x_line2', [
('Disk, press \x0e to share via NFC, \x11 to share', '\x11 to share'),
])
def test_word_wrap(txt, x_line2, sim_exec, only_q1, width=34):
# one tricky double-wide char word-wrapping case .. but add others
assert '\n' not in txt
DOUBLE_W = ['', '', '', '', '', '', '', '', '', '', '', '\x0e', '\x11', '\t', '\x0f', '\x12', '\x13', '\x14', '\x16', '\x17']
@pytest.mark.parametrize('txt, target', [
('Disk, press \x0e to share via NFC, \x11 to share', ['Disk, press \x0e to share via NFC,', '\x11 to share']),
((KEY_NFC * 17)+".", [KEY_NFC * 17, '.']),
((KEY_NFC * 17)+(17*KEY_QR), [KEY_NFC * 17, KEY_QR * 17]),
((KEY_NFC * 17)+" "+(17*KEY_QR), [KEY_NFC * 17, KEY_QR * 17]),
((KEY_NFC * 16)+".", [(KEY_NFC * 16)+'.']),
(f"Use {KEY_NFC}, or {KEY_F1}, {KEY_F2}, {KEY_F3}, or or or {KEY_F4}", [f"Use {KEY_NFC}, or {KEY_F1}, {KEY_F2}, {KEY_F3}, or or or {KEY_F4}"]),
("".join(DOUBLE_W), ["".join(DOUBLE_W[:17]), "".join(DOUBLE_W[17:])]),
("".join(6*DOUBLE_W), ["".join(6*DOUBLE_W)[i:i + 17] for i in range(0, len(6*DOUBLE_W), 17)]),
])
def test_word_wrap_double_wide(only_q1, txt, target, sim_exec):
width = 33 # check shared/ux.py CHAR_PER_W
cmd = f'from utils import word_wrap; RV.write("\\n".join(word_wrap({txt!r}, {width})))'
got = sim_exec(cmd)
assert 'Traceback' not in got
lines = got.split('\n')
assert width*2//3 <= len(lines[0]) <= width
assert lines[1] == x_line2
assert lines == target
want_words = [i.strip() for i in txt.split()]
got_words = [i.strip() for i in got.split()]
@pytest.mark.parametrize('txt, target, width', [
((17*'a')+". ccc", [(17*'a')+".", "ccc"], 17),
((17*'a')+".", [(17*'a')+"."], 17),
((17*'-')+". ccc", [(17*'-')+".", "ccc"], 17),
((34 * 'A'), [33 * "A", "A"], 33),
((33 * 'A')+". ccc", [(33 * "A")+".", "ccc"], 33),
('Coldcard is ready to sign spending transactions!', ['Coldcard is ready to sign', 'spending transactions!'], 33),
('Coldcard is ready to sign spending transactions!', ['Coldcard is ready', 'to sign spending', 'transactions!'], 17),
((16*"B")+ " AAAA", [16*"B", "AAAA"], 17),
((16*"B")+ " AAAA", [(16*"B")+" ", "AAAA"], 17),
((17*"B")+ " AAAA", [17*"B", "AAAA"], 17),
((17*"B")+ " AAAA", [17*"B", " AAAA"], 17),
("(recommended), or by typing numbers.", ["(recommended), or", "by typing numbers."], 17),
("difficult to recover your funds.", ["difficult to", "recover your", "funds."], 17),
("USB Serial Number:", ["USB Serial Number:"], 17),
("USB Serial Number;", ["USB Serial Number;"], 17),
("USB Serial Number/", ["USB Serial", "Number/"], 17),
])
def test_word_wrap(txt, target, width, sim_exec):
cmd = f'from utils import word_wrap; RV.write("\\n".join(word_wrap({txt!r}, {width})))'
got = sim_exec(cmd)
assert 'Traceback' not in got
assert want_words == got_words
lines = got.split('\n')
assert lines == target
from constants import AF_P2WSH, AF_P2SH, AF_P2WSH_P2SH, AF_CLASSIC, AF_P2WPKH, AF_P2WPKH_P2SH
@pytest.mark.parametrize('addr,net,fmt', [
( 'bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4', 'BTC', AF_P2WPKH ),