QRs in txn output explorer
This commit is contained in:
parent
3075cd07b2
commit
6507856861
@ -7,6 +7,8 @@ This lists the new changes that have not yet been published in a normal release.
|
||||
- Bugfix: If all change outputs have `nValue=0` they're not shown in UX
|
||||
- Bugfix: Disallow negative input/output amounts in PSBT
|
||||
- Enhancement: Add warning for zero value outputs if not OP_RETURNs
|
||||
- Enhancement: Show QR codes of output addresses in Txn output explorer. Output explorer is offered for txns of all sizes.
|
||||
|
||||
|
||||
# Mk4 Specific Changes
|
||||
|
||||
|
||||
@ -296,7 +296,7 @@ class ApproveTransaction(UserAuthorizedAction):
|
||||
try:
|
||||
dest = self.chain.render_address(o.scriptPubKey)
|
||||
|
||||
return '%s\n - to address -\n%s\n' % (val, show_single_address(dest))
|
||||
return '%s\n - to address -\n%s\n' % (val, show_single_address(dest)), dest
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
@ -306,12 +306,13 @@ class ApproveTransaction(UserAuthorizedAction):
|
||||
data_hex, data_ascii = data
|
||||
to_ret = '%s\n - OP_RETURN -\n%s' % (val, data_hex)
|
||||
if data_ascii:
|
||||
return to_ret + " (ascii: %s)\n" % data_ascii
|
||||
return to_ret + "\n"
|
||||
to_ret += " (ascii: %s)" % data_ascii
|
||||
return to_ret + "\n", data_hex
|
||||
|
||||
# Handle future things better: allow them to happen at least.
|
||||
dest = B2A(o.scriptPubKey)
|
||||
return '%s\n - to script -\n%s\n' % (val, dest)
|
||||
|
||||
return '%s\n - to script -\n%s\n' % (val, dest), dest
|
||||
|
||||
async def interact(self):
|
||||
# Prompt user w/ details and get approval
|
||||
@ -420,7 +421,7 @@ class ApproveTransaction(UserAuthorizedAction):
|
||||
))
|
||||
|
||||
# outputs + change story created here
|
||||
needs_txn_explorer = self.output_summary_text(msg)
|
||||
self.output_summary_text(msg)
|
||||
gc.collect()
|
||||
|
||||
if self.psbt.ux_notes:
|
||||
@ -447,11 +448,9 @@ class ApproveTransaction(UserAuthorizedAction):
|
||||
dis.progress_bar_show(1) # finish the Validating...
|
||||
|
||||
if not hsm_active:
|
||||
esc = ""
|
||||
msg.write("Press %s to approve and sign transaction." % OK)
|
||||
if needs_txn_explorer:
|
||||
esc += "2"
|
||||
msg.write(" Press (2) to explore txn.")
|
||||
esc = "2"
|
||||
msg.write("Press %s to approve and sign transaction."
|
||||
" Press (2) to explore txn outputs." % OK)
|
||||
if (self.input_method == "sd") and CardSlot.both_inserted():
|
||||
esc += "b"
|
||||
msg.write(" (B) to write to lower SD slot.")
|
||||
@ -542,11 +541,16 @@ class ApproveTransaction(UserAuthorizedAction):
|
||||
dis.fullscreen('Wait...')
|
||||
rv = ""
|
||||
end = min(offset + count, self.psbt.num_outputs)
|
||||
|
||||
for idx, out in self.psbt.output_iter(offset, end):
|
||||
addrs = []
|
||||
change = []
|
||||
for i, (idx, out) in enumerate(self.psbt.output_iter(offset, end)):
|
||||
outp = self.psbt.outputs[idx]
|
||||
item = "Output %d%s:\n\n" % (idx, " (change)" if outp.is_change else "")
|
||||
item += self.render_output(out)
|
||||
msg, addr_or_script = self.render_output(out)
|
||||
item += msg
|
||||
addrs.append(addr_or_script)
|
||||
if outp.is_change:
|
||||
change.append(i)
|
||||
item += "\n"
|
||||
rv += item
|
||||
dis.progress_sofar(idx-offset+1, count)
|
||||
@ -554,18 +558,28 @@ class ApproveTransaction(UserAuthorizedAction):
|
||||
rv += 'Press RIGHT to see next group'
|
||||
if offset:
|
||||
rv += ', LEFT to go back'
|
||||
|
||||
if not version.has_qwerty:
|
||||
# Q has hint key
|
||||
rv += ", (4) to show QR code"
|
||||
rv += ('. %s to quit.' % X)
|
||||
|
||||
return rv
|
||||
return rv, addrs, change, end
|
||||
|
||||
start = 0
|
||||
n = 10
|
||||
msg = make_msg(start, n)
|
||||
msg, addrs, change, end = make_msg(start, n)
|
||||
while True:
|
||||
ch = await ux_show_story(msg, escape='79'+KEY_RIGHT+KEY_LEFT)
|
||||
ch = await ux_show_story(msg, title="%d-%d" % (start, end-1),
|
||||
escape='479'+KEY_RIGHT+KEY_LEFT+KEY_QR,
|
||||
hint_icons=KEY_QR)
|
||||
if ch == 'x':
|
||||
del msg
|
||||
return
|
||||
elif ch in "4"+KEY_QR:
|
||||
from ux import show_qr_codes
|
||||
await show_qr_codes(addrs, False, start, is_addrs=True, change_idxs=change)
|
||||
continue
|
||||
elif (ch in KEY_LEFT+"7"):
|
||||
if (start - n) < 0:
|
||||
continue
|
||||
@ -582,7 +596,7 @@ class ApproveTransaction(UserAuthorizedAction):
|
||||
# nothing changed - do not recalc msg
|
||||
continue
|
||||
|
||||
msg = make_msg(start, n)
|
||||
msg, addrs, change, end = make_msg(start, n)
|
||||
|
||||
async def save_visualization(self, msg, sign_text=False):
|
||||
# write story text out, maybe signing it as we go
|
||||
@ -624,7 +638,6 @@ class ApproveTransaction(UserAuthorizedAction):
|
||||
MAX_VISIBLE_OUTPUTS = const(10)
|
||||
MAX_VISIBLE_CHANGE = const(20)
|
||||
|
||||
needs_txn_explorer = False
|
||||
largest_outs = []
|
||||
largest_change = []
|
||||
total_change = 0
|
||||
@ -643,7 +656,8 @@ class ApproveTransaction(UserAuthorizedAction):
|
||||
|
||||
else:
|
||||
if len(largest_outs) < MAX_VISIBLE_OUTPUTS:
|
||||
largest_outs.append((tx_out.nValue, self.render_output(tx_out)))
|
||||
rendered, _ = self.render_output(tx_out)
|
||||
largest_outs.append((tx_out.nValue, rendered))
|
||||
if len(largest_outs) == MAX_VISIBLE_OUTPUTS:
|
||||
# descending sort from the biggest value to lowest (sort on out.nValue)
|
||||
largest_outs = sorted(largest_outs, key=lambda x: x[0], reverse=True)
|
||||
@ -663,7 +677,8 @@ class ApproveTransaction(UserAuthorizedAction):
|
||||
if outp.is_change:
|
||||
ret = (here, self.chain.render_address(tx_out.scriptPubKey))
|
||||
else:
|
||||
ret = (here, self.render_output(tx_out))
|
||||
rendered, _ = self.render_output(tx_out)
|
||||
ret = (here, rendered)
|
||||
largest.insert(keep, ret)
|
||||
|
||||
# foreign outputs (soon to be other people's coins)
|
||||
@ -675,7 +690,6 @@ class ApproveTransaction(UserAuthorizedAction):
|
||||
|
||||
left = self.psbt.num_outputs - len(largest_outs) - self.psbt.num_change_outputs
|
||||
if left > 0:
|
||||
needs_txn_explorer = True
|
||||
msg.write('.. plus %d smaller output(s), not shown here, which total: ' % left)
|
||||
|
||||
# calculate left over value
|
||||
@ -700,14 +714,9 @@ class ApproveTransaction(UserAuthorizedAction):
|
||||
|
||||
left_c = self.psbt.num_change_outputs - len(largest_change)
|
||||
if left_c:
|
||||
needs_txn_explorer = True
|
||||
msg.write('.. plus %d smaller change output(s), not shown here, which total: ' % left_c)
|
||||
msg.write('%s %s\n\n' % self.chain.render_value(total_change - visible_change_sum))
|
||||
|
||||
# if we didn't already show all outputs, then give user a chance to
|
||||
# view them individually
|
||||
return needs_txn_explorer
|
||||
|
||||
|
||||
def sign_transaction(psbt_len, flags=0x0, psbt_sha=None):
|
||||
# transaction (binary) loaded into PSRAM already, checksum checked
|
||||
|
||||
@ -335,7 +335,7 @@ class Display:
|
||||
return
|
||||
|
||||
def draw_qr_display(self, qr_data, msg, is_alnum, sidebar, idx_hint, invert,
|
||||
is_addr=False, force_msg=False):
|
||||
is_addr=False, force_msg=False, is_change=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
|
||||
@ -384,11 +384,14 @@ class Display:
|
||||
|
||||
if not sidebar and not msg:
|
||||
pass
|
||||
elif not sidebar and len(msg) > (5*7):
|
||||
elif not sidebar and ((len(msg) > (5*7)) or is_change):
|
||||
# use FontTiny and word wrap (will just split if no spaces)
|
||||
# native segwit addresses and taproot
|
||||
# if is_change=True also p2pkh and p2sh fall into this category as space is needed for "CHANGE"
|
||||
x = bw + lm + 4
|
||||
ww = ((128 - x)//4) - 1 # char width avail
|
||||
y = 1
|
||||
|
||||
parts = list(word_wrap(msg, ww))
|
||||
if len(parts) > 8:
|
||||
parts = parts[:8]
|
||||
@ -399,9 +402,13 @@ class Display:
|
||||
for line in parts:
|
||||
self.text(x, y, line, FontTiny)
|
||||
y += 8
|
||||
|
||||
if is_addr and is_change:
|
||||
self.text(x+4, y+8, "CHANGE", FontTiny)
|
||||
else:
|
||||
# hand-positioned for known cases
|
||||
# - sidebar = (text, #of char per line)
|
||||
# p2pkh and p2sh addresses (if is_change=False)
|
||||
x, y = 73, (0 if is_alnum else 2)
|
||||
dy = 10 if is_alnum else 12
|
||||
sidebar, ll = sidebar if sidebar else (msg, 7)
|
||||
|
||||
@ -657,7 +657,7 @@ class Display:
|
||||
return prev_x
|
||||
|
||||
def draw_qr_display(self, qr_data, msg, is_alnum, sidebar, idx_hint, invert, partial_bar=None,
|
||||
is_addr=False, force_msg=False):
|
||||
is_addr=False, force_msg=False, is_change=False):
|
||||
# Show a QR code on screen w/ some text under it
|
||||
# - invert not supported on Q1
|
||||
# - sidebar not supported here (see users.py)
|
||||
@ -774,6 +774,10 @@ class Display:
|
||||
else:
|
||||
self.text(-1, 0, idx_hint)
|
||||
|
||||
if is_addr and is_change:
|
||||
for i, c in enumerate("CHANGE"):
|
||||
self.text(0, i, c)
|
||||
|
||||
# pass a max brightness flag here, which will be cleared after next show
|
||||
self.show(max_bright=True)
|
||||
else:
|
||||
|
||||
@ -18,7 +18,8 @@ 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,
|
||||
is_addrs=False, force_msg=False, allow_nfc=True, is_secret=False):
|
||||
is_addrs=False, force_msg=False, allow_nfc=True, is_secret=False,
|
||||
change_idxs=None):
|
||||
self.is_alnum = is_alnum
|
||||
self.idx = 0 # start with first address
|
||||
self.invert = False # looks better, but neither mode is ideal
|
||||
@ -32,6 +33,7 @@ class QRDisplaySingle(UserInteraction):
|
||||
self.allow_nfc = allow_nfc
|
||||
# only used for NFC sharing secret material - full chip wipe if is_secret=True
|
||||
self.is_secret = is_secret
|
||||
self.change_idxs = change_idxs or []
|
||||
|
||||
def calc_qr(self, msg):
|
||||
# Version 2 would be nice, but can't hold what we need, even at min error correction,
|
||||
@ -62,6 +64,11 @@ class QRDisplaySingle(UserInteraction):
|
||||
# numbers, letters, etc.
|
||||
return str(self.start_n + self.idx) if len(self.addrs) > 1 else None
|
||||
|
||||
def is_change(self):
|
||||
if self.idx in self.change_idxs:
|
||||
return True
|
||||
return False
|
||||
|
||||
def redraw(self):
|
||||
# Redraw screen.
|
||||
from glob import dis
|
||||
@ -92,7 +99,8 @@ class QRDisplaySingle(UserInteraction):
|
||||
|
||||
dis.draw_qr_display(self.qr_data, msg, self.is_alnum,
|
||||
self.sidebar, self.idx_hint(), self.invert,
|
||||
is_addr=self.is_addrs, force_msg=self.force_msg)
|
||||
is_addr=self.is_addrs, force_msg=self.force_msg,
|
||||
is_change=self.is_change())
|
||||
|
||||
async def interact_bare(self):
|
||||
from glob import NFC, dis
|
||||
|
||||
@ -603,9 +603,15 @@ def verify_qr_address(cap_screen_qr, cap_screen, is_q1):
|
||||
# plus text version of address, if any, is right.
|
||||
from ckcc_protocol.constants import AFC_BECH32
|
||||
|
||||
def doit(addr_fmt, expect_addr=None):
|
||||
def doit(addr_fmt, expect_addr=None, is_change=None):
|
||||
qr = cap_screen_qr().decode('ascii')
|
||||
|
||||
if isinstance(addr_fmt, str):
|
||||
try:
|
||||
addr_fmt = unmap_addr_fmt[addr_fmt]
|
||||
except KeyError:
|
||||
addr_fmt = msg_sign_unmap_addr_fmt[addr_fmt]
|
||||
|
||||
if (addr_fmt & AFC_BECH32) or (addr_fmt & AFC_BECH32M):
|
||||
qr = qr.lower()
|
||||
|
||||
@ -616,9 +622,21 @@ def verify_qr_address(cap_screen_qr, cap_screen, is_q1):
|
||||
# - insists on some spaces
|
||||
full = cap_screen()
|
||||
if is_q1:
|
||||
txt = ''.join(full.split()[2:]).replace('~', '')
|
||||
if is_change:
|
||||
for c, line in zip("CHANGE", full.split('\n')):
|
||||
assert line.startswith(c)
|
||||
elif is_change is False:
|
||||
for c, line in zip("CHANGE", full.split('\n')):
|
||||
assert not line.startswith(c)
|
||||
|
||||
txt = ''.join(l for l in full.split() if len(l)>4).replace('~', '')
|
||||
else:
|
||||
txt = ''.join(full.split())
|
||||
if is_change:
|
||||
assert "CHANGE" in full
|
||||
elif is_change is False:
|
||||
assert "CHANGE" not in full
|
||||
|
||||
txt = ''.join(full.split()).replace('CHANGE', '')
|
||||
|
||||
if txt:
|
||||
assert txt == qr
|
||||
@ -2413,7 +2431,7 @@ def goto_address_explorer(goto_home, pick_menu_item, need_keypress,
|
||||
return doit
|
||||
|
||||
@pytest.fixture
|
||||
def txout_explorer(cap_story, press_cancel, need_keypress, is_q1):
|
||||
def txout_explorer(cap_story, press_cancel, need_keypress, is_q1, verify_qr_address):
|
||||
def doit(data, chain="XTN"):
|
||||
time.sleep(.1)
|
||||
title, story = cap_story()
|
||||
@ -2431,11 +2449,26 @@ def txout_explorer(cap_story, press_cancel, need_keypress, is_q1):
|
||||
assert len(ss) == (len(d) * 2) + 1
|
||||
assert "Press RIGHT to see next group" in ss[-1]
|
||||
if i:
|
||||
assert " LEFT to go back." in ss[-1]
|
||||
assert " LEFT to go back" in ss[-1]
|
||||
else:
|
||||
assert "LEFT" not in ss[-1]
|
||||
|
||||
for i, (sa, sb, (af, amount, change)) in enumerate(zip(ss[:-1:2], ss[1::2], d), start=i):
|
||||
if not is_q1:
|
||||
assert "(4) to show QR code" in ss[-1]
|
||||
|
||||
# collect QR codes first
|
||||
need_keypress(KEY_QR if is_q1 else "4")
|
||||
qr_addr_list = []
|
||||
for af, amount, change in d:
|
||||
qr = verify_qr_address(af, is_change=bool(change))
|
||||
qr_addr_list.append(qr)
|
||||
need_keypress(KEY_RIGHT if is_q1 else "9")
|
||||
time.sleep(.5)
|
||||
|
||||
press_cancel() # QR code on screen - exit
|
||||
|
||||
start = i
|
||||
for i, (sa, sb, (af, amount, change)) in enumerate(zip(ss[:-1:2], ss[1::2], d), start=start):
|
||||
if change:
|
||||
assert f"Output {i} (change):" == sa
|
||||
else:
|
||||
@ -2443,6 +2476,9 @@ def txout_explorer(cap_story, press_cancel, need_keypress, is_q1):
|
||||
|
||||
txt_amount, _, addr = sb.split("\n")
|
||||
addr = addr_from_display_format(addr)
|
||||
# verify QR matches what is on screen
|
||||
assert addr == qr_addr_list[i-start]
|
||||
|
||||
assert txt_amount == f'{amount / 100000000:.8f} {chain}'
|
||||
if af == "p2pkh":
|
||||
if chain == "BTC":
|
||||
|
||||
@ -3062,7 +3062,19 @@ def test_txout_explorer_op_return(finalize, data, fake_txn, start_sign, cap_stor
|
||||
time.sleep(.1)
|
||||
_, story = cap_story()
|
||||
ss = story.split("\n\n")
|
||||
for i, (sa, sb, (amount, d)) in enumerate(zip(ss[:-1:2], ss[1::2], data), start=20):
|
||||
|
||||
# collect QR codes first
|
||||
need_keypress(KEY_QR if is_q1 else "4")
|
||||
qr_list = []
|
||||
for _ in range(len(data)):
|
||||
qr = cap_screen_qr().decode('ascii')
|
||||
qr_list.append(qr)
|
||||
need_keypress(KEY_RIGHT if is_q1 else "9")
|
||||
time.sleep(.5)
|
||||
|
||||
press_cancel() # QR code on screen - exit
|
||||
|
||||
for i, (sa, sb, (amount, data)) in enumerate(zip(ss[:-1:2], ss[1::2], data), start=20):
|
||||
assert f"Output {i}:" == sa
|
||||
try:
|
||||
val, name, dd = sb.split("\n")
|
||||
@ -3073,11 +3085,12 @@ def test_txout_explorer_op_return(finalize, data, fake_txn, start_sign, cap_stor
|
||||
assert f'{amount / 100000000:.8f} XTN' == val
|
||||
if dd:
|
||||
hex_str, ascii_str = dd.split(" ", 1)
|
||||
assert f"(ascii: {d.decode()})" == ascii_str
|
||||
assert d.hex() == hex_str
|
||||
assert hex_str == qr_list[i-20]
|
||||
assert f"(ascii: {data.decode()})" == ascii_str
|
||||
assert data.hex() == hex_str
|
||||
else:
|
||||
assert d.hex()[:200] == dd0
|
||||
assert d.hex()[-200:] == dd1
|
||||
assert data.hex()[:200] == dd0
|
||||
assert data.hex()[-200:] == dd1
|
||||
|
||||
press_cancel() # exit txn out explorer
|
||||
end_sign(finalize=finalize)
|
||||
|
||||
Loading…
Reference in New Issue
Block a user