diff --git a/misc/binfonter/config.py b/misc/binfonter/config.py index e028da50..dbe190b3 100644 --- a/misc/binfonter/config.py +++ b/misc/binfonter/config.py @@ -74,4 +74,9 @@ special_chars = dict(small=[ x x x x x '''), +# thin space +('\u2009', dict(y=0, w=5), '''\ + +'''), + ]) diff --git a/releases/Next-ChangeLog.md b/releases/Next-ChangeLog.md index 62e35d19..03d564c1 100644 --- a/releases/Next-ChangeLog.md +++ b/releases/Next-ChangeLog.md @@ -9,6 +9,7 @@ This lists the new changes that have not yet been published in a normal release. - New Feature: Sign message from note text, or password note - New Feature: Sign message with key resulting from positive ownership check. Press (0) + enter/scan message text - New Feature: Sign message with key selected from Address Explorer Custom Path menu. Press (2) + enter/scan message text +- Enhancement: New address display format improves address verification on screen - Enhancement: Hide Secure Notes & Passwords in Deltamode. Wipe seed if notes menu accessed. - Enhancement: Hide Seed Vault in Deltamode. Wipe seed if Seed Vault menu accessed. - Enhancement: Add ability to switch between BIP-32 xpub, and obsolete diff --git a/shared/address_explorer.py b/shared/address_explorer.py index 1bd6808a..ed95e48f 100644 --- a/shared/address_explorer.py +++ b/shared/address_explorer.py @@ -17,6 +17,7 @@ from glob import settings from auth import write_sig_file from charcodes import KEY_QR, KEY_NFC, KEY_PAGE_UP, KEY_PAGE_DOWN, KEY_HOME, KEY_LEFT, KEY_RIGHT from charcodes import KEY_CANCEL +from utils import show_single_address, problem_file_line def truncate_address(addr): # Truncates address to width of screen, replacing middle chars @@ -315,7 +316,7 @@ Press (3) if you really understand and accept these risks. else: msg += '⋯/%d/%d =>\n' % (change, idx) - msg += addr + '\n\n' + msg += show_single_address(addr) + '\n\n' dis.progress_sofar(idx-start+1, n) else: @@ -328,7 +329,7 @@ Press (3) if you really understand and accept these risks. for idx, addr, deriv in main.yield_addresses(start, n, change if allow_change else None): addrs.append(addr) - msg += "%s =>\n%s\n\n" % (deriv, addr) + msg += "%s =>\n%s\n\n" % (deriv, show_single_address(addr)) dis.progress_sofar(idx-start+1, n or 1) # export options @@ -382,7 +383,7 @@ Press (3) if you really understand and accept these risks. if allow_qr: addr_fmt = addr_fmt or ms_wallet.addr_fmt is_alnum = bool(addr_fmt & (AFC_BECH32 | AFC_BECH32M)) - await show_qr_codes(addrs, is_alnum, start) + await show_qr_codes(addrs, is_alnum, start, is_addrs=True) continue @@ -522,7 +523,6 @@ async def make_address_summary_file(path, addr_fmt, ms_wallet, account_num, await needs_microsd() return except Exception as e: - from utils import problem_file_line await ux_show_story('Failed to write!\n\n\n%s\n%s' % (e, problem_file_line(e))) return diff --git a/shared/auth.py b/shared/auth.py index e6908103..634e6075 100644 --- a/shared/auth.py +++ b/shared/auth.py @@ -16,7 +16,7 @@ from ux import ux_aborted, ux_show_story, abort_and_goto, ux_dramatic_pause, ux_ from ux import show_qr_code, OK, X, ux_input_text, ux_enter_bip32_index from usb import CCBusyError from utils import HexWriter, xfp2str, problem_file_line, cleanup_deriv_path -from utils import B2A, to_ascii_printable +from utils import B2A, to_ascii_printable, show_single_address from psbt import psbtObject, FatalPSBTIssue, FraudulentChangeOutput from files import CardSlot, CardMissingError, needs_microsd from exceptions import HSMDenied @@ -368,14 +368,14 @@ class ApproveMessageSign(UserAuthorizedAction): if hsm_active: ch = await hsm_active.approve_msg_sign(self.text, self.address, self.subpath) else: - story = MSG_SIG_TEMPLATE.format(msg=self.text, addr=self.address, subpath=self.subpath) + story = MSG_SIG_TEMPLATE.format(msg=self.text, addr=show_single_address(self.address), + subpath=self.subpath) ch = await ux_show_story(story) if ch != 'y': # they don't want to! self.refused = True else: - # perform signing (progress bar shown) digest = chains.current_chain().hash_message(self.text.encode()) self.result = sign_message_digest(digest, self.subpath, "Signing...", self.addr_fmt)[0] @@ -661,7 +661,7 @@ async def verify_armored_signed_msg(contents, digest_check=True): title = "CORRECT" warn_msg = "" err_msg = "" - story = "Good signature by address:\n %s" % addr + story = "Good signature by address:\n%s" % show_single_address(addr) if digest_check: digest_prob = verify_signed_file_digest(msg) @@ -744,7 +744,7 @@ class ApproveTransaction(UserAuthorizedAction): try: dest = self.chain.render_address(o.scriptPubKey) - return '%s\n - to address -\n%s\n' % (val, dest) + return '%s\n - to address -\n%s\n' % (val, show_single_address(dest)) except ValueError: pass @@ -1046,8 +1046,10 @@ class ApproveTransaction(UserAuthorizedAction): msg = make_msg(start, n) async def save_visualization(self, msg, sign_text=False): - # write text into spi flash, maybe signing it as we go + # write story text out, maybe signing it as we go # - return length and checksum + from charcodes import OUT_CTRL_ADDRESS + txt_len = msg.seek(0, 2) msg.seek(0) @@ -1055,7 +1057,8 @@ class ApproveTransaction(UserAuthorizedAction): with SFFile(TXN_OUTPUT_OFFSET, max_size=txt_len+300, message="Visualizing...") as fd: while 1: - blk = msg.read(256).encode('ascii') + # replace with empty space, to keep correct txt_len - already hashed + blk = msg.read(256).replace(OUT_CTRL_ADDRESS, ' ').encode('ascii') if not blk: break if chk: chk.update(blk) @@ -1068,7 +1071,7 @@ class ApproveTransaction(UserAuthorizedAction): fd.write(b2a_base64(sig).decode('ascii').strip()) fd.write('\n') - return (fd.tell(), fd.checksum.digest()) + return fd.tell(), fd.checksum.digest() def output_summary_text(self, msg): # Produce text report of where their cash is going. This is what @@ -1146,13 +1149,13 @@ class ApproveTransaction(UserAuthorizedAction): visible_change_sum = 0 if len(largest_change) == 1: visible_change_sum += largest_change[0][0] - msg.write(' - to address -\n%s\n' % largest_change[0][1]) + msg.write(' - to address -\n%s\n' % show_single_address(largest_change[0][1])) else: msg.write(' - to addresses -\n') for val, addr in largest_change: visible_change_sum += val - msg.write(addr) - msg.write('\n') + msg.write(show_single_address(addr)) + msg.write('\n\n') left_c = self.psbt.num_change_outputs - len(largest_change) if left_c: @@ -1539,7 +1542,8 @@ class ShowPKHAddress(ShowAddressBase): self.address = sv.chain.address(node, addr_fmt) def get_msg(self): - return '''{addr}\n\n= {sp}''' .format(addr=self.address, sp=self.subpath) + return '''{addr}\n\n= {sp}''' .format(addr=show_single_address(self.address), + sp=self.subpath) class ShowP2SHAddress(ShowAddressBase): @@ -1566,8 +1570,8 @@ Wallet: Paths: -{sp}'''.format(addr=self.address, name=self.ms.name, - M=self.ms.M, N=self.ms.N, sp='\n\n'.join(self.subpath_help)) +{sp}'''.format(addr=show_single_address(self.address), name=self.ms.name, + M=self.ms.M, N=self.ms.N, sp='\n\n'.join(self.subpath_help)) def start_show_p2sh_address(M, N, addr_format, xfp_paths, witdeem_script): # Show P2SH address to user, also returns it. diff --git a/shared/charcodes.py b/shared/charcodes.py index 06d829c0..a7947475 100644 --- a/shared/charcodes.py +++ b/shared/charcodes.py @@ -107,4 +107,9 @@ if has_qwerty: assert DECODER[KEYNUM_SYMBOL] == KEY_SYMBOL assert DECODER[KEYNUM_LAMP] == KEY_LAMP +# These affect how 'ux stories' are rendered; they are control +# characters on the output side of things, not input. +OUT_CTRL_TITLE = '\x01' # must be first char in line: be a title line +OUT_CTRL_ADDRESS = '\x02' # must be first char in line: it's a payment address + # EOF diff --git a/shared/display.py b/shared/display.py index bf819683..fac05aba 100644 --- a/shared/display.py +++ b/shared/display.py @@ -7,6 +7,7 @@ from ssd1306 import SSD1306_SPI from version import is_devmode import framebuf from graphics_mk4 import Graphics +from charcodes import OUT_CTRL_TITLE, OUT_CTRL_ADDRESS # we support 4 fonts from zevvpeep import FontSmall, FontLarge, FontTiny @@ -304,9 +305,14 @@ class Display: for ln in lines: if ln == 'EOT': self.hline(y+3) - elif ln and ln[0] == '\x01': + elif ln and ln[0] == OUT_CTRL_TITLE: self.text(0, y, ln[1:], FontLarge) y += 21 + elif ln and ln[0] == OUT_CTRL_ADDRESS: + from utils import chunk_address + fmt = '\u2009'.join(chunk_address(ln[1:])) + self.text(14, y, fmt) # fixed indent, to be centered + y += 15 # a bit extra vertical line height else: self.text(0, y, ln) @@ -322,9 +328,10 @@ class Display: # no status bar on Mk4 return - def draw_qr_display(self, qr_data, msg, is_alnum, sidebar, idx_hint, invert): + def draw_qr_display(self, qr_data, msg, is_alnum, sidebar, idx_hint, invert, is_addr=False): # 'sidebar' is a pre-formated obj to show to right of QR -- oled life # - 'msg' will appear to right if very short, else under in tiny + # - ignores "is_addr" because exactly zero space to do anything special from utils import word_wrap self.clear() diff --git a/shared/lcd_display.py b/shared/lcd_display.py index 0e3722a1..01138be8 100644 --- a/shared/lcd_display.py +++ b/shared/lcd_display.py @@ -3,11 +3,11 @@ # lcd_display.py - LCD rendering for Q1's 320x240 pixel *colour* display! # import machine, uzlib, utime, array -from uasyncio import sleep_ms from graphics_q1 import Graphics from st7788 import ST7788 -from utils import xfp2str, word_wrap +from utils import xfp2str, word_wrap, chunk_address from ucollections import namedtuple +from charcodes import OUT_CTRL_TITLE, OUT_CTRL_ADDRESS # the one font: fixed-width (except for a few double-width chars) from font_iosevka import CELL_W, CELL_H, TEXT_PALETTES, COL_TEXT, COL_DARK_TEXT, COL_SCROLL_DARK @@ -612,25 +612,50 @@ class Display: self.clear() y=0 + prev_x = None for ln in lines: if ln == 'EOT': self.text(0, y, '┅'*CHARS_W, dark=True) continue - elif ln and ln[0] == '\x01': + + elif ln and ln[0] == OUT_CTRL_TITLE: # title ... but we have no special font? Inverse! self.text(0, y, ' '+ln[1:]+' ', invert=True) if hint_icons: # maybe show that [QR] can do something self.text(-1, y, hint_icons, dark=True) + + elif ln and ln[0] == OUT_CTRL_ADDRESS: + # we can assume this will be a single line for our display + # thanks to code in utils.word_wrap + prev_x = self._draw_addr(y, ln[1:], prev_x=prev_x) + else: self.text(0, y, ln) + prev_x = None y += 1 self.scroll_bar(top, num_lines, CHARS_H) self.show() - def draw_qr_display(self, qr_data, msg, is_alnum, sidebar, idx_hint, invert, partial_bar=None): + def _draw_addr(self, y, addr, prev_x=None): + # Draw a single-line of an address + # - use prev_x=0 to start centered + if prev_x is None: + # left justify (for stories) + prev_x = x = 1 + elif prev_x == 0: + # center first line, following line will be left-justified to match that + prev_x = x = max(((CHARS_W - (len(addr) * 5) // 4) // 2), 0) + else: + x = prev_x + + self.text(x, y, ' '+' '.join(chunk_address(addr))+' ', invert=1) + + return prev_x + + def draw_qr_display(self, qr_data, msg, is_alnum, sidebar, idx_hint, invert, partial_bar=None, is_addr=False): # Show a QR code on screen w/ some text under it # - invert not supported on Q1 # - sidebar not supported here (see users.py) @@ -643,9 +668,11 @@ class Display: parts = [msg] elif ' ' not in msg and (len(msg) <= CHARS_W*2): # fits in two lines, but has no spaces (ie. payment addr) - # so split nicely, and shift off center + # - so split nicely in middle and/or at mod4 if address hh = len(msg) // 2 - parts = [msg[0:hh] + ' ', ' '+msg[hh:]] + if is_addr: + hh = (hh + 3) & ~0x3 + parts = [msg[0:hh], msg[hh:]] else: # do word wrap parts = list(word_wrap(msg, CHARS_W)) @@ -723,8 +750,12 @@ class Display: if num_lines: # centered text under that y = CHARS_H - num_lines + prev_x = 0 for line in parts: - self.text(None, y, line) + if not is_addr: + self.text(None, y, line) + else: + prev_x = self._draw_addr(y, line, prev_x=prev_x) y += 1 if idx_hint: diff --git a/shared/multisig.py b/shared/multisig.py index c904fa4a..8180167a 100644 --- a/shared/multisig.py +++ b/shared/multisig.py @@ -22,7 +22,6 @@ TRUST_VERIFY = const(0) TRUST_OFFER = const(1) TRUST_PSBT = const(2) - class MultisigOutOfSpace(RuntimeError): pass diff --git a/shared/nvstore.py b/shared/nvstore.py index 8b0dee6c..ae116ae8 100644 --- a/shared/nvstore.py +++ b/shared/nvstore.py @@ -76,6 +76,7 @@ from utils import call_later_ms # cd_pin = [<=mk3] pin code which enables "countdown to brick" mode # kbtn = (1 char str) button will wipe seed during login process (mk4+, Q) # terms_ok = customer has signed-off on the terms of sale +# msas = multisig address show (do not censor multisig addresses) # settings linked to seed # LINKED_SETTINGS = ["multisig", "tp", "ovc", "xfp", "xpub", "words"] diff --git a/shared/ownership.py b/shared/ownership.py index 994a207c..b063f88c 100644 --- a/shared/ownership.py +++ b/shared/ownership.py @@ -7,7 +7,7 @@ from glob import settings from ucollections import namedtuple from ubinascii import hexlify as b2a_hex from exceptions import UnknownAddressExplained -from utils import problem_file_line +from utils import problem_file_line, show_single_address # Track many addresses, but in compressed form # - map from random Bech32/Base58 payment address to (wallet) + keypath @@ -315,7 +315,7 @@ class OwnershipCache: is_ms = isinstance(wallet, MultisigWallet) sp = wallet.render_path(*subpath) - msg = addr + msg = show_single_address(addr) msg += '\n\nFound in wallet:\n ' + wallet.name msg += '\nDerivation path:\n ' + sp if is_ms: @@ -346,7 +346,7 @@ class OwnershipCache: break except UnknownAddressExplained as exc: - await ux_show_story(addr + '\n\n' + str(exc), title="Unknown Address") + await ux_show_story(show_single_address(addr) + '\n\n' + str(exc), title="Unknown Address") except Exception as e: await ux_show_story('Ownership search failed.\n\n%s\n%s' % (e, problem_file_line(e))) diff --git a/shared/qrs.py b/shared/qrs.py index 9f55d720..de7124f0 100644 --- a/shared/qrs.py +++ b/shared/qrs.py @@ -17,13 +17,14 @@ MAX_V11_CHAR_LIMIT = const(321) class QRDisplaySingle(UserInteraction): # Show a single QR code for (typically) a list of addresses, or a single value. - def __init__(self, addrs, is_alnum, start_n=0, sidebar=None, msg=None): + def __init__(self, addrs, is_alnum, start_n=0, sidebar=None, msg=None, is_addrs=False): self.is_alnum = is_alnum self.idx = 0 # start with first address self.invert = False # looks better, but neither mode is ideal self.addrs = addrs self.sidebar = sidebar self.start_n = start_n + self.is_addrs = is_addrs self.msg = msg self.qr_data = None @@ -73,7 +74,7 @@ class QRDisplaySingle(UserInteraction): # draw display dis.busy_bar(False) dis.draw_qr_display(self.qr_data, self.msg or body, self.is_alnum, - self.sidebar, self.idx_hint(), self.invert) + self.sidebar, self.idx_hint(), self.invert, is_addr=self.is_addrs) async def interact_bare(self): from glob import NFC, dis diff --git a/shared/utils.py b/shared/utils.py index 0b1e617d..d5cbce2f 100644 --- a/shared/utils.py +++ b/shared/utils.py @@ -2,10 +2,11 @@ # # utils.py - Misc utils. My favourite kind of source file. # -import gc, sys, ustruct, ngu, chains, ure, time, bip39 +import gc, sys, ustruct, ngu, chains, ure, time, bip39, version from ubinascii import unhexlify as a2b_hex from ubinascii import hexlify as b2a_hex from ubinascii import a2b_base64, b2a_base64 +from charcodes import OUT_CTRL_ADDRESS from uhashlib import sha256 B2A = lambda x: str(b2a_hex(x), 'ascii') @@ -481,11 +482,26 @@ def word_wrap(ln, w): return while ln: - # find a space in (width) first part of remainder sp = ln.rfind(' ', 0, w-1) - if sp == -1: + if ln[0] == OUT_CTRL_ADDRESS: + # special handling for lines w/ payment address in them + # - add same marker to newly split lines + addr = ln[1:] + if version.has_qwerty: + # - do line break in middle, on mod4 boundry + # - will not work if addresses are > 2 lines long (34*2 chars) + aw = ((len(addr) // 2) + 3) & ~3 + else: + # - simply 3 4-char groups on Mk4 + aw = 12 + pos = 0 + while pos < len(addr): + yield OUT_CTRL_ADDRESS + addr[pos:pos+aw] + pos += aw + return + # bad-break the line sp = min(txtlen(ln), w) nsp = sp @@ -502,6 +518,9 @@ def word_wrap(ln, w): # not clear when this would happen? final bit?? left = left + ' ' + ln ln = '' + else: + if ln and ln[0] == ' ' and version.has_qwerty: + ln = " " + ln yield left @@ -669,4 +688,13 @@ def decode_bip21_text(got): def encode_seed_qr(words): return ''.join('%04d' % bip39.get_word_index(w) for w in words) +def show_single_address(addr): + # insert some metadata so display layer can do special rendering + # of addresses (based on hardware capabilities) + return OUT_CTRL_ADDRESS + addr + +def chunk_address(addr): + # useful to show payment addresses specially + return [addr[i:i+4] for i in range(0, len(addr), 4)] + # EOF diff --git a/shared/ux.py b/shared/ux.py index 6259e445..fe1cd4dd 100644 --- a/shared/ux.py +++ b/shared/ux.py @@ -7,7 +7,8 @@ from queues import QueueEmpty import utime, gc, version from utils import word_wrap from charcodes import (KEY_LEFT, KEY_RIGHT, KEY_UP, KEY_DOWN, KEY_HOME, KEY_NFC, KEY_QR, - KEY_END, KEY_PAGE_UP, KEY_PAGE_DOWN, KEY_ENTER, KEY_CANCEL) + KEY_END, KEY_PAGE_UP, KEY_PAGE_DOWN, KEY_ENTER, KEY_CANCEL, OUT_CTRL_TITLE) + from exceptions import AbortInteraction DEFAULT_IDLE_TIMEOUT = const(4*3600) # (seconds) 4 hours @@ -181,8 +182,8 @@ async def ux_show_story(msg, title=None, escape=None, sensitive=False, lines = [] if title: - # kinda weak rendering but it works. - lines.append('\x01' + title) + # render the title line specially, see display/lcd_display.py + lines.append(OUT_CTRL_TITLE + title) if version.has_qwerty: # big screen always needs blank after title @@ -321,14 +322,14 @@ def abort_and_push(m): the_ux.push(m) numpad.abort_ux() -async def show_qr_codes(addrs, is_alnum, start_n): +async def show_qr_codes(addrs, is_alnum, start_n, **kw): from qrs import QRDisplaySingle - o = QRDisplaySingle(addrs, is_alnum, start_n, sidebar=None) + o = QRDisplaySingle(addrs, is_alnum, start_n, **kw) await o.interact_bare() -async def show_qr_code(data, is_alnum=False, msg=None): +async def show_qr_code(data, is_alnum=False, msg=None, **kw): from qrs import QRDisplaySingle - o = QRDisplaySingle([data], is_alnum, msg=msg) + o = QRDisplaySingle([data], is_alnum, msg=msg, **kw) await o.interact_bare() async def ux_enter_bip32_index(prompt, can_cancel=False, unlimited=False): diff --git a/shared/ux_q1.py b/shared/ux_q1.py index 7998580c..a832359e 100644 --- a/shared/ux_q1.py +++ b/shared/ux_q1.py @@ -14,7 +14,7 @@ from ubinascii import hexlify as b2a_hex from ubinascii import unhexlify as a2b_hex from ubinascii import b2a_base64 -from utils import problem_file_line +from utils import problem_file_line, show_single_address from public_constants import MSG_SIGNING_MAX_LENGTH from glob import numpad # may be None depending on import order, careful @@ -1084,7 +1084,7 @@ async def ux_visualize_bip21(proto, addr, args): # - validate address ownership on request from ux import ux_show_story - msg = addr + '\n\n' + msg = show_single_address(addr) + '\n\n' args = args or {} if 'amount' in args: diff --git a/shared/zevvpeep.py b/shared/zevvpeep.py index 624bd5ee..be67f32d 100644 --- a/shared/zevvpeep.py +++ b/shared/zevvpeep.py @@ -36,8 +36,8 @@ class FontSmall(FontBase): _bboxes = [None, (0, -3, 7, 14, 0), (0, -3, 7, 14, 4), (0, -3, 7, 14, 5), (0, -3, 7, 14, 7), (0, -3, 7, 14, 9), (0, -3, 7, 14, 10), (0, -3, 7, 14, 11), (0, -3, 7, 14, 12), (0, -3, 7, 14, 13), (0, -3, - 7, 14, 14), (0, 0, 8, 8, 8), (0, 0, 11, 8, 16), (0, 0, 11, 9, 18), - (0, 0, 14, 10, 20)] + 7, 14, 14), (0, 0, 5, 2, 2), (0, 0, 8, 8, 8), (0, 0, 11, 8, 16), (0, + 0, 11, 9, 18), (0, 0, 14, 10, 20)] _code_points = [ (range(32, 127), [1, 2, 14, 20, 31, 43, 55, 67, 73, 87, 101, 111, 122, @@ -48,11 +48,11 @@ class FontSmall(FontBase): 755, 767, 779, 791, 803, 815, 827, 842, 854, 866, 880, 892, 904, 916, 928, 940, 954, 968, 980, 992, 1004, 1016, 1028, 1040, 1052, 1067, 1079, 1093, 1106, 1120]), -(range(8226, 8227), [1126]), # • -(range(8592, 8593), [1145]), # ← -(range(8594, 8595), [1166]), # → -(range(8627, 8628), [1187]), # ↳ -(range(8943, 8944), [1208]), # ⋯ +(range(8201, 8202), [1126]), # +(range(8226, 8227), [1129]), # • +(range(8592, 8595), [1148, 0, 1169]), # ← → +(range(8627, 8628), [1190]), # ↳ +(range(8943, 8944), [1211]), # ⋯ ] _bitmaps = b"""\ @@ -63,7 +63,7 @@ class FontSmall(FontBase): \x10\x09\x04\x08\x10\x10\x20\x20\x20\x20\x20\x10\x10\x08\x04\x09\x20\x10\ \x08\x08\x04\x04\x04\x04\x04\x08\x08\x10\x20\x05\x00\x00\x00\x00\x24\x18\ \x7e\x18\x24\x06\x00\x00\x00\x08\x08\x08\x3e\x08\x08\x08\x09\x00\x00\x00\ -\x00\x00\x00\x00\x00\x00\x18\x30\x20\x40\x0b\x00\x00\x00\x00\x00\x00\x3e\ +\x00\x00\x00\x00\x00\x00\x18\x30\x20\x40\x0c\x00\x00\x00\x00\x00\x00\x3e\ \x00\x08\x00\x00\x00\x00\x00\x00\x00\x00\x00\x10\x38\x10\x08\x02\x02\x04\ \x04\x08\x08\x10\x10\x20\x20\x40\x40\x07\x00\x18\x24\x42\x42\x4a\x52\x42\ \x42\x24\x18\x07\x00\x08\x18\x28\x48\x08\x08\x08\x08\x08\x08\x07\x00\x3c\ @@ -118,13 +118,13 @@ class FontSmall(FontBase): \x3a\x02\x02\x42\x3c\x07\x00\x00\x00\x00\x7e\x02\x04\x08\x10\x20\x7e\x09\ \x06\x08\x08\x08\x08\x08\x30\x08\x08\x08\x08\x08\x06\x08\x10\x10\x10\x10\ \x10\x10\x10\x10\x10\x10\x10\x10\x09\x60\x10\x10\x10\x10\x10\x0c\x10\x10\ -\x10\x10\x10\x60\x03\x00\x00\x32\x4a\x44\x0d\x00\x00\x00\x00\x00\x00\x00\ -\x00\x00\x00\x03\x80\x03\x80\x03\x80\x00\x00\x0e\x00\x00\x00\x00\x00\x00\ -\x00\x00\x08\x00\x18\x00\x3f\xf8\x18\x00\x08\x00\x00\x00\x0e\x00\x00\x00\ -\x00\x00\x00\x00\x00\x00\x20\x00\x30\x3f\xf8\x00\x30\x00\x20\x00\x00\x0e\ -\x00\x00\x10\x00\x10\x00\x10\x00\x10\x20\x10\x30\x1f\xf8\x00\x30\x00\x20\ -\x00\x00\x0c\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x2a\xa0\x00\ -\x00\ +\x10\x10\x10\x60\x03\x00\x00\x32\x4a\x44\x0b\x00\x00\x0e\x00\x00\x00\x00\ +\x00\x00\x00\x00\x00\x00\x03\x80\x03\x80\x03\x80\x00\x00\x0f\x00\x00\x00\ +\x00\x00\x00\x00\x00\x08\x00\x18\x00\x3f\xf8\x18\x00\x08\x00\x00\x00\x0f\ +\x00\x00\x00\x00\x00\x00\x00\x00\x00\x20\x00\x30\x3f\xf8\x00\x30\x00\x20\ +\x00\x00\x0f\x00\x00\x10\x00\x10\x00\x10\x00\x10\x20\x10\x30\x1f\xf8\x00\ +\x30\x00\x20\x00\x00\x0d\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\ +\x2a\xa0\x00\x00\ """ diff --git a/testing/conftest.py b/testing/conftest.py index 6e518b36..736c3ac3 100644 --- a/testing/conftest.py +++ b/testing/conftest.py @@ -3,7 +3,7 @@ import pytest, time, sys, random, re, ndef, os, glob, hashlib, json, functools, io, math, pdb from subprocess import check_output from ckcc.protocol import CCProtocolPacker -from helpers import B2A, U2SAT, hash160 +from helpers import B2A, U2SAT, hash160, addr_from_display_format from base58 import decode_base58_checksum from bip32 import BIP32Node from msg import verify_message @@ -2145,6 +2145,7 @@ def txout_explorer(cap_story, press_cancel, need_keypress, is_q1): assert f"Output {i}:" == sa txt_amount, _, addr = sb.split("\n") + addr = addr_from_display_format(addr) assert txt_amount == f'{amount / 100000000:.8f} {chain}' if af == "p2pkh": if chain == "BTC": diff --git a/testing/devtest/check_decode.py b/testing/devtest/check_decode.py index 6ef46deb..91b3ece8 100644 --- a/testing/devtest/check_decode.py +++ b/testing/devtest/check_decode.py @@ -35,7 +35,10 @@ if 'destinations' in expect: for (val, addr), (idx, txo) in zip(expect['destinations'], p.output_iter()): assert val == txo.nValue txt = active_request.render_output(txo) - assert addr in txt + # normalize from display format + address = txt.split("\n")[-2] + assert address[0] == "\x02" + assert addr == address[1:] assert '%.8f'%(val/1E8) in txt if 'sw_inputs' in expect: diff --git a/testing/helpers.py b/testing/helpers.py index 2e76e7f2..be781549 100644 --- a/testing/helpers.py +++ b/testing/helpers.py @@ -101,8 +101,11 @@ def xfp2str(xfp): from struct import pack return b2a_hex(pack(' 3 @@ -111,8 +114,12 @@ def parse_change_back(story): assert 'address' in lines[s+2] addrs = [] for y in range(s+3, len(lines)): - if not lines[y]: break - addrs.append(lines[y]) + line = lines[y].strip() + if line: + if line[0] == "\x02": + addrs.append(addr_from_display_format(line)) + if line.startswith(("3","2","1","m","n","tb1","bc1","bcrt")): + addrs.append(line) if len(addrs) >= 2: assert 'to addresses' in lines[s+2] diff --git a/testing/test_addr.py b/testing/test_addr.py index a874bb59..b2f84e42 100644 --- a/testing/test_addr.py +++ b/testing/test_addr.py @@ -10,6 +10,7 @@ from ckcc_protocol.protocol import CCProtocolPacker from ckcc_protocol.constants import * from charcodes import KEY_QR from constants import msg_sign_unmap_addr_fmt +from helpers import addr_from_display_format @pytest.mark.parametrize('path', [ 'm', "m/1/2", "m/1'/100'"]) @pytest.mark.parametrize('addr_fmt', [ AF_CLASSIC, AF_P2WPKH, AF_P2WPKH_P2SH ]) @@ -45,8 +46,7 @@ def test_show_addr_displayed(dev, need_keypress, addr_vs_path, path, addr_fmt, else: assert path in story - assert addr in story - assert addr in story.split('\n') + assert addr in addr_from_display_format(story.split("\n\n")[0]) # check expected addr was used addr_vs_path(addr, path, addr_fmt) @@ -122,7 +122,7 @@ def test_show_addr_nfc(path, str_addr_fmt, nfc_write_text, nfc_read_text, pick_m _, story = cap_story() split_story = story.split("\n\n") - story_addr = split_story[0] + story_addr = addr_from_display_format(split_story[0]) story_path = split_story[1][2:] # remove "= " if not is_q1: assert "Press (3) to share via NFC" in story diff --git a/testing/test_address_explorer.py b/testing/test_address_explorer.py index b601bd8d..a9496c7b 100644 --- a/testing/test_address_explorer.py +++ b/testing/test_address_explorer.py @@ -8,7 +8,7 @@ import pytest, time, io, csv, bech32 from ckcc_protocol.constants import * from bip32 import BIP32Node from base58 import decode_base58_checksum -from helpers import detruncate_address, hash160 +from helpers import detruncate_address, hash160, addr_from_display_format from charcodes import KEY_QR, KEY_LEFT, KEY_RIGHT from constants import MAX_BIP32_IDX @@ -52,7 +52,7 @@ def parse_display_screen(cap_story, is_mark3): d = dict() for path_raw, addr, empty in zip(*[iter(raw_addrs)]*3): path = path_raw.split(" =>")[0] - d[path] = addr + d[path] = addr_from_display_format(addr) assert len(d) == n return d return doit @@ -474,7 +474,7 @@ def test_custom_path(path_sidx, which_fmt, addr_vs_path, pick_menu_item, goto_ad assert 'Showing single addr' in body assert path in body - addr = body.split("\n")[3] + addr = addr_from_display_format(body.split("\n")[3]) addr_vs_path(addr, path, addr_fmt=which_fmt) diff --git a/testing/test_msg.py b/testing/test_msg.py index 1c4b1cb9..7d6c0d78 100644 --- a/testing/test_msg.py +++ b/testing/test_msg.py @@ -10,6 +10,7 @@ from ckcc_protocol.protocol import CCProtocolPacker, CCProtoError, CCUserRefused from ckcc_protocol.constants import * from constants import addr_fmt_names, msg_sign_unmap_addr_fmt from charcodes import KEY_QR, KEY_NFC +from helpers import addr_from_display_format def default_derivation_by_af(addr_fmt, testnet=True): @@ -77,7 +78,7 @@ def verify_msg_sign_story(): assert 'Using the key associated' in story if addr: - assert addr in story + assert addr == addr_from_display_format(story.split("\n\n")[2].split("\n")[-1]) if not subpath: assert 'm =>' not in story @@ -648,7 +649,7 @@ def test_verify_signature_file(way, addr_fmt, path, msg, sign_on_microsd, goto_h title, story = verify_armored_signature(way, fname, should) assert title == "CORRECT" assert "Good signature" in story - assert addr in story + assert addr == addr_from_display_format(story.split("\n")[-1]) if (addr_fmt == "p2pkh") and (chain != "BTC"): res = bitcoind.rpc.verifymessage(addr, sig, msg) assert res is True @@ -999,5 +1000,6 @@ def test_verify_scanned_signed_msg(msg, scan_a_qr, need_keypress, goto_home, cap title, story = cap_story() assert title == "CORRECT" assert "Good signature by address" in story + assert addr == addr_from_display_format(story.split("\n")[-1]) # EOF diff --git a/testing/test_multisig.py b/testing/test_multisig.py index fede3d2c..b04e2464 100644 --- a/testing/test_multisig.py +++ b/testing/test_multisig.py @@ -15,7 +15,7 @@ from ckcc.protocol import CCProtocolPacker, MAX_TXN_LEN from pprint import pprint from base64 import b64encode, b64decode from base58 import encode_base58_checksum -from helpers import B2A, fake_dest_addr, xfp2str +from helpers import B2A, fake_dest_addr, xfp2str, addr_from_display_format from helpers import path_to_str, str_to_path, slip132undo, swab32, hash160 from struct import unpack, pack from constants import * @@ -519,7 +519,7 @@ def test_ms_show_addr(dev, cap_story, press_select, addr_vs_path, bitcoind_p2sh, #print(story) if not has_ms_checks: - assert got_addr in story + assert got_addr == addr_from_display_format(story.split("\n\n")[0]) assert all((xfp2str(xfp) in story) for xfp,_,_ in keys) if bip45: for i in range(len(keys)): @@ -2222,7 +2222,7 @@ def test_ms_addr_explorer(change, M_N, addr_fmt, start_idx, clear_ms, cap_menu, for ln in story.split('\n'): if '=>' not in ln: continue - path,chk,addr = ln.split() + path,chk,addr = ln.split(" ", 2) assert chk == '=>' assert '/' in path @@ -2259,6 +2259,7 @@ def test_ms_addr_explorer(change, M_N, addr_fmt, start_idx, clear_ms, cap_menu, assert int(subpath.split('/')[-1]) == idx #print('../0/%s => \n %s' % (idx, B2A(script))) + addr = addr_from_display_format(addr) if msas: assert addr == expect == qr_addrs[c] else: diff --git a/testing/test_ownership.py b/testing/test_ownership.py index b8ce787a..6fce0dbd 100644 --- a/testing/test_ownership.py +++ b/testing/test_ownership.py @@ -5,7 +5,7 @@ import pytest, time, io, csv from txn import fake_address from base58 import encode_base58_checksum -from helpers import hash160 +from helpers import hash160, addr_from_display_format from bip32 import BIP32Node from constants import AF_P2WSH, AF_P2SH, AF_P2WSH_P2SH, AF_CLASSIC, AF_P2WPKH, AF_P2WPKH_P2SH from constants import simulator_fixed_xprv, simulator_fixed_tprv, addr_fmt_names @@ -193,7 +193,7 @@ def test_ux(valid, testnet, method, title, story = cap_story() - assert addr in story + assert addr == addr_from_display_format(story.split("\n\n")[0]) assert '(1) to verify ownership' in story need_keypress('1') @@ -218,8 +218,7 @@ def test_ux(valid, testnet, method, time.sleep(1) title, story = cap_story() - - assert addr in story + assert addr == addr_from_display_format(story.split("\n\n")[0]) if title == 'Unknown Address' and not testnet: assert 'That address is not valid on Bitcoin Testnet' in story @@ -299,7 +298,7 @@ def test_address_explorer_saver(af, wipe_cache, settings_set, goto_address_explo time.sleep(1) title, story = cap_story() - assert addr in story + assert addr == addr_from_display_format(story.split("\n\n")[0]) assert title == 'Verified Address' assert 'Found in wallet' in story assert 'Derivation path' in story diff --git a/testing/test_sign.py b/testing/test_sign.py index 378c76ae..a5947f7e 100644 --- a/testing/test_sign.py +++ b/testing/test_sign.py @@ -4,22 +4,22 @@ # import time, pytest, os, random, pdb, struct, base64, binascii, itertools, datetime -from ckcc_protocol.protocol import CCProtocolPacker, CCProtoError, MAX_TXN_LEN, CCUserRefused +from ckcc_protocol.protocol import CCProtocolPacker, CCProtoError from binascii import b2a_hex, a2b_hex from psbt import BasicPSBT, BasicPSBTInput, BasicPSBTOutput, PSBT_IN_REDEEM_SCRIPT from io import BytesIO -from pprint import pprint, pformat +from pprint import pprint from decimal import Decimal from base64 import b64encode, b64decode from base58 import encode_base58_checksum -from helpers import B2A, U2SAT, prandom, fake_dest_addr, make_change_addr, parse_change_back +from helpers import B2A, fake_dest_addr, parse_change_back, addr_from_display_format from helpers import xfp2str, seconds2human_readable, hash160 from msg import verify_message from bip32 import BIP32Node -from constants import ADDR_STYLES, ADDR_STYLES_SINGLE, SIGHASH_MAP, simulator_fixed_tpub +from constants import ADDR_STYLES, ADDR_STYLES_SINGLE, SIGHASH_MAP from txn import * from ctransaction import CTransaction, CTxOut, CTxIn, COutPoint -from ckcc_protocol.constants import STXN_FINALIZE, STXN_VISUALIZE, STXN_SIGNED +from ckcc_protocol.constants import STXN_VISUALIZE, STXN_SIGNED from charcodes import KEY_QR, KEY_RIGHT @@ -553,7 +553,9 @@ def test_change_case(start_sign, use_regtest, end_sign, check_against_bitcoind, time.sleep(.1) _, story = cap_story() - assert chg_addr in story + split_sory = story.split("\n\n")[3].split("\n") + assert split_sory[0] == "Change back:" + assert chg_addr == addr_from_display_format(split_sory[-1]) b4 = BasicPSBT().parse(psbt) check_against_bitcoind(B2A(b4.txn), Decimal('0.00000294'), change_outs=[1,]) @@ -623,7 +625,7 @@ def test_change_fraud_path(start_sign, use_regtest, end_sign, case, check_agains time.sleep(.1) _, story = cap_story() - assert chg_addr in story + assert chg_addr == addr_from_display_format(story.split("\n\n")[3].split("\n")[-1]) assert 'Change back:' not in story end_sign(True) @@ -731,7 +733,9 @@ def test_change_p2sh_p2wpkh(start_sign, end_sign, check_against_bitcoind, use_re check_against_bitcoind(B2A(b4.txn), Decimal('0.00000294'), change_outs=[1,], dests=[(1, expect_addr)]) - assert expect_addr in story + split_sory = story.split("\n\n")[3].split("\n") + assert split_sory[0] == "Change back:" + assert expect_addr == addr_from_display_format(split_sory[-1]) assert parse_change_back(story) == (Decimal('1.09997082'), [expect_addr]) end_sign(True)