improve re-export UX; unify USB to done_signing; bugfixes

This commit is contained in:
scgbckbone 2025-04-07 12:14:07 +02:00 committed by doc-hex
parent 8a18e413e9
commit 3239dc6cd5
17 changed files with 425 additions and 459 deletions

View File

@ -1878,9 +1878,9 @@ async def ready2sign(*a):
Put the proposed transaction onto MicroSD card \
in PSBT format (Partially Signed Bitcoin Transaction) \
or upload a transaction to be signed \
from your desktop wallet software or command line tools.\n\n'''
from your desktop wallet software or command line tools.'''
footnotes = ("\n\nYou will always be prompted to confirm the details "
footnotes = ("You will always be prompted to confirm the details "
"before any signature is performed.")
# if we have only one SD card inserted, at this point, we know no PSBTs on them

View File

@ -8,17 +8,15 @@ from ubinascii import b2a_base64, a2b_base64
from ubinascii import hexlify as b2a_hex
from ubinascii import unhexlify as a2b_hex
from uhashlib import sha256
from public_constants import MSG_SIGNING_MAX_LENGTH, SUPPORTED_ADDR_FORMATS
from public_constants import AFC_SCRIPT, AF_CLASSIC, AFC_BECH32, AF_P2WPKH, AF_P2WPKH_P2SH
from public_constants import AFC_SCRIPT, AF_CLASSIC, AFC_BECH32, SUPPORTED_ADDR_FORMATS
from public_constants import STXN_FINALIZE, STXN_VISUALIZE, STXN_SIGNED
from sffile import SFFile
from ux import ux_aborted, ux_show_story, abort_and_goto, ux_dramatic_pause, ux_clear_keys
from ux import show_qr_code, OK, X, ux_input_text, ux_enter_bip32_index, abort_and_push
from ux import ux_show_story, abort_and_goto, ux_dramatic_pause, ux_clear_keys
from ux import show_qr_code, OK, X, abort_and_push, AbortInteraction
from usb import CCBusyError
from utils import HexWriter, xfp2str, problem_file_line, cleanup_deriv_path, chunk_checksum
from utils import B2A, to_ascii_printable, show_single_address
from utils import HexWriter, xfp2str, problem_file_line, cleanup_deriv_path, B2A, show_single_address
from psbt import psbtObject, FatalPSBTIssue, FraudulentChangeOutput
from files import CardSlot, CardMissingError, needs_microsd
from files import CardSlot, CardMissingError
from exceptions import HSMDenied
from version import MAX_TXN_LEN
from charcodes import KEY_QR, KEY_NFC, KEY_ENTER, KEY_CANCEL, KEY_LEFT, KEY_RIGHT
@ -74,7 +72,7 @@ class UserAuthorizedAction:
if allowed_cls and isinstance(cls.active_request, allowed_cls):
return
# check if UX actally was cleared, and we're not really doing that anymore; recover
# check if UX actually was cleared, and we're not really doing that anymore; recover
# - happens if USB caller never comes back for their final results
from ux import the_ux
top_ux = the_ux.top_of_stack()
@ -264,20 +262,25 @@ async def try_push_tx(data, txid, txn_sha=None):
return False
class ApproveTransaction(UserAuthorizedAction):
def __init__(self, psbt_len, flags=0x0, approved_cb=None, psbt_sha=None, cb_kws=None):
from glob import settings
def __init__(self, psbt_len, flags=None, psbt_sha=None, input_method=None,
output_encoder=None, filename=None):
super().__init__()
self.psbt_len = psbt_len
self.do_finalize = bool(flags & STXN_FINALIZE)
self.do_visualize = bool(flags & STXN_VISUALIZE)
# do finalize is None if not USB, None = decide based on is_complete
if flags is None:
self.do_finalize = self.do_visualize = None
else:
self.do_finalize = bool(flags & STXN_FINALIZE)
self.do_visualize = bool(flags & STXN_VISUALIZE)
self.stxn_flags = flags
self.psbt = None
self.psbt_sha = psbt_sha
self.approved_cb = approved_cb
self.input_method = input_method
self.output_encoder = output_encoder
self.filename = filename
self.result = None # will be (len, sha256) of the resulting PSBT
self.cb_kws = cb_kws or {}
self.is_sd = (self.cb_kws.get("input_method", None) is None) \
and (not self.cb_kws.get("force_vdisk", False))
self.chain = chains.current_chain()
def render_output(self, o):
@ -447,7 +450,7 @@ class ApproveTransaction(UserAuthorizedAction):
if needs_txn_explorer:
esc += "2"
msg.write(" Press (2) to explore txn.")
if self.is_sd and CardSlot.both_inserted():
if (self.input_method == "sd") and CardSlot.both_inserted():
esc += "b"
msg.write(" (B) to write to lower SD slot.")
msg.write(" %s to abort." % X)
@ -516,70 +519,17 @@ class ApproveTransaction(UserAuthorizedAction):
except BaseException as exc:
return await self.failure("Signing failed late", exc)
if self.approved_cb:
if self.is_sd and (ch == "b"):
self.cb_kws["slot_b"] = True
await self.approved_cb(self.psbt, **self.cb_kws)
self.done()
return
txid = None
try:
# re-serialize the PSBT back out
with SFFile(TXN_OUTPUT_OFFSET, max_size=MAX_TXN_LEN, message="Saving...") as fd:
if self.do_finalize:
# user is forcing us to finalize over USB
# try it & fail with detailed msg
txid = self.psbt.finalize(fd)
else:
self.psbt.serialize(fd)
self.result = (fd.tell(), fd.checksum.digest())
self.done(redraw=(not txid))
await done_signing(self.psbt, self, self.input_method, self.filename, self.output_encoder,
slot_b=True if ch == "b" else False, finalize=self.do_finalize)
self.done()
except AbortInteraction:
# user might have sent new sign cmd, while we still at export prompt
pass
except BaseException as exc:
# sys.print_exception(exc)
return await self.failure("PSBT output failed", exc)
from glob import NFC
if self.do_finalize and txid and not hsm_active:
# Unsigned PSBT came in via NFC or QR or Teleport
if await try_push_tx(self.result[0], txid, self.result[1]):
return # success, exit
kq, kn = "(1)", "(3)"
if version.has_qwerty:
kq, kn = KEY_QR, KEY_NFC
while 1:
# Show txid when we can; advisory
# - maybe even as QR, hex-encoded in alnum mode
tmsg = txid + '\n\nPress %s for QR Code of TXID. ' % kq
if NFC:
tmsg += 'Press %s to share signed txn via NFC.' % kn
ch = await ux_show_story(tmsg, "Final TXID", escape='13'+KEY_NFC+KEY_QR)
if ch in '1'+KEY_QR:
await show_qr_code(txid, True)
continue
if ch in KEY_NFC+"3" and NFC:
await NFC.share_signed_txn(txid, TXN_OUTPUT_OFFSET,
self.result[0], self.result[1])
continue
break
elif version.has_qwerty and self.psbt.active_multisig:
# Offer to teleport the result, which still needs signatures
from teleport import kt_send_psbt
await kt_send_psbt(self.psbt, self.result[0], post_signing=True)
async def txn_explorer(self):
# Page through unlimited-sized transaction details
@ -761,7 +711,7 @@ def sign_transaction(psbt_len, flags=0x0, psbt_sha=None):
# transaction (binary) loaded into PSRAM already, checksum checked
UserAuthorizedAction.check_busy(ApproveTransaction)
UserAuthorizedAction.active_request = ApproveTransaction(
psbt_len, flags, psbt_sha=psbt_sha, cb_kws={"input_method": "usb"}
psbt_len, flags, psbt_sha=psbt_sha, input_method="usb",
)
# kill any menu stack, and put our thing at the top
@ -789,19 +739,24 @@ def psbt_encoding_taster(taste, psbt_len):
return decoder, output_encoder, psbt_len
async def done_signing(psbt, input_method=None, filename=None, force_vdisk=False,
async def done_signing(psbt, tx_req, input_method=None, filename=None,
output_encoder=None, slot_b=False, finalize=None):
# User authorized PSBT for signing, and we added signatures.
# - allow PushTX if enabled (first thing)
# - can save final TXN out to SD card/VirtDisk, share by NFC, QR.
from glob import dis, PSRAM
from files import CardSlot, CardMissingError
from glob import PSRAM, hsm_active
from sffile import SFFile
from ux import show_qr_code, import_export_prompt, ux_show_story
from ux import show_qr_code, import_export_prompt
first_time = True
msg = None
title = None
txid = None
is_complete = psbt.is_complete()
if finalize is not None:
# USB case - user can choose whether to attempt finalization
is_complete = finalize
with SFFile(TXN_OUTPUT_OFFSET, max_size=MAX_TXN_LEN, message="Saving...") as psram:
if is_complete:
@ -815,9 +770,18 @@ async def done_signing(psbt, input_method=None, filename=None, force_vdisk=False
data_len = psram.tell()
data_sha2 = psram.checksum.digest()
UserAuthorizedAction.cleanup()
if input_method == "usb":
# return result over USB before going to all options
tx_req.result = data_len, data_sha2
if hsm_active:
# it is enough to just return back via USB, other options
# are pointless
return
first_time = False
msg = noun + " shared via USB."
title = "PSBT Signed"
first_time = True
if txid and await try_push_tx(data_len, txid, data_sha2):
# go directly to reexport menu after pushTX
first_time = False
@ -830,32 +794,45 @@ async def done_signing(psbt, input_method=None, filename=None, force_vdisk=False
while True:
ch = None
if first_time:
# first time, assume they want to send out same way it came in -- dont prompt
# first time, assume they want to send out same way it came in -- don't prompt
if input_method == "qr":
ch = KEY_QR
elif input_method == "nfc":
ch = KEY_NFC
elif input_method == "kt":
# not reached .. because we offer KT method before this is called
ch = 't'
else:
# SD/VDisk
ch = {"force_vdisk": force_vdisk, "slot_b": slot_b}
ch = {"force_vdisk": input_method == "vdisk", "slot_b": slot_b}
if not ch:
# show all possible export options (based on hardware enabled, features)
ch = await import_export_prompt(noun, no_qr=not version.has_qr, offer_kt=offer_kt)
intro = []
if msg:
intro.append(msg)
if txid:
intro.append('TXID:\n' + txid)
ch = await import_export_prompt(noun, intro="\n\n".join(intro), offer_kt=offer_kt,
txid=txid, title=title)
if ch == KEY_CANCEL:
UserAuthorizedAction.cleanup()
break
elif txid and (ch == '6'):
await show_qr_code(txid, is_alnum=True, force_msg=True)
continue
elif ch == KEY_QR:
here = PSRAM.read_at(TXN_OUTPUT_OFFSET, data_len)
msg = txid or 'Partly Signed PSBT'
try:
if len(here) > 920:
# too big for simple QR - use BBQr instead
raise ValueError
hex_here = b2a_hex(here).upper().decode()
await show_qr_code(hex_here, is_alnum=True, msg=msg)
except (ValueError, RuntimeError):
except (ValueError, RuntimeError, TypeError):
from ux_q1 import show_bbqr_codes
await show_bbqr_codes('T' if txid else 'P', here, msg)
@ -871,33 +848,26 @@ async def done_signing(psbt, input_method=None, filename=None, force_vdisk=False
msg = noun + " shared via NFC."
elif ch == 't':
# after saving to card, they might want to teleport it
elif (ch == 't') and not is_complete:
# they might want to teleport it, but only if we have PSBT
# there is no need to teleport PSBT if txn is already complete & ready to be broadcast
from teleport import kt_send_psbt
ok = await kt_send_psbt(psbt)
msg = 'Failed to Teleport' if not ok else 'Sent by Teleport'
ok, num_sigs_needed = await kt_send_psbt(psbt, data_len)
title = 'Sent by Teleport' if ok else 'Failed to Teleport'
if num_sigs_needed > 0:
s, aux = ("", "is") if num_sigs_needed == 1 else ("s", "are")
msg = "%d more signature%s %s still required." % (num_sigs_needed, s, aux)
continue
else:
# typical case: save to SD card, show filenames we used
assert isinstance(ch, dict)
msg = await _save_to_disk(psbt, txid, ch, is_complete, data_len, output_encoder, filename)
if not msg:
# it failed so start again with all possible options enabled.
ch = None
input_method = None
first_time = False
continue
msg = await _save_to_disk(psbt, txid, ch, is_complete, data_len,
output_encoder, filename)
msg += '\n\n'
esc = '0'
msg += 'Press (0) to save again by another method.'
ch2 = await ux_show_story(msg, title='PSBT Signed', escape=esc)
if ch2 != '0':
break
else:
input_method = None
first_time = False
input_method = None
first_time = False
title = "PSBT Signed"
async def _save_to_disk(psbt, txid, save_options, is_complete, data_len, output_encoder, filename=None):
# Saving a PSBT from PSRAM to something disk-like.
@ -995,7 +965,7 @@ async def _save_to_disk(psbt, txid, save_options, is_complete, data_len, output_
except OSError as exc:
prob = 'Failed to write!\n\n%s\n\n' % exc
sys.print_exception(exc)
# sys.print_exception(exc)
# fall through to try again
# If this point reached, some problem, we could not write.
@ -1023,8 +993,6 @@ async def _save_to_disk(psbt, txid, save_options, is_complete, data_len, output_
if out2_fn:
msg += 'Finalized transaction (ready for broadcast):\n\n%s' % out2_fn
if txid and not del_after:
msg += '\n\nFinal TXID:\n' + txid
return msg
@ -1085,11 +1053,8 @@ async def sign_psbt_file(filename, force_vdisk=False, slot_b=None, just_read=Fal
UserAuthorizedAction.cleanup()
UserAuthorizedAction.active_request = ApproveTransaction(
psbt_len,
approved_cb=done_signing,
cb_kws={"filename": filename,
"force_vdisk": force_vdisk,
"output_encoder": output_encoder}
psbt_len, input_method="vdisk" if force_vdisk else "sd",
filename=filename, output_encoder=output_encoder,
)
if ux_abort:
# needed for auto vdisk mode

View File

@ -18,9 +18,7 @@ async def export_by_qr(body, label, type_code, force_bbqr=False):
from ux import show_qr_code
try:
# ignore label/title - provides no useful info
# makes qr smaller and harder to read
if force_bbqr:
if force_bbqr or len(body) > 2000:
raise ValueError
await show_qr_code(body)
@ -35,7 +33,7 @@ async def export_by_qr(body, label, type_code, force_bbqr=False):
return
async def export_contents(title, contents, fname_pattern, derive=None, addr_fmt=None, no_qr=None,
async def export_contents(title, contents, fname_pattern, derive=None, addr_fmt=None,
is_json=False, force_bbqr=False, force_prompt=False):
# export text and json files while offering NFC, QR & Vdisk
# produces signed export in case of SD/Vdisk (signed with key at deriv and addr_fmt)
@ -49,8 +47,9 @@ async def export_contents(title, contents, fname_pattern, derive=None, addr_fmt=
dis.fullscreen('Generating...')
contents, derive, addr_fmt = contents()
if no_qr is not None:
no_qr = not version.has_qwerty and (len(contents) >= MAX_V11_CHAR_LIMIT)
# figure out if offering QR code export make sense given HW
# len() is O(1)
no_qr = not version.has_qwerty and (len(contents) >= MAX_V11_CHAR_LIMIT)
sig = not (derive is None and addr_fmt is None)

View File

@ -420,8 +420,7 @@ async def ux_sign_msg(txt, approved_cb=None, kill_menu=True):
the_ux.push(MenuSystem(rv))
async def msg_signing_done(signature, address, text):
ch = await import_export_prompt("Signed Msg", is_import=False,
no_qr=not version.has_qwerty)
ch = await import_export_prompt("Signed Msg")
if ch == KEY_CANCEL:
return

View File

@ -6,7 +6,7 @@ import stash, chains, ustruct, ure, uio, sys, ngu, uos, ujson, version
from utils import xfp2str, str2xfp, swab32, cleanup_deriv_path, keypath_to_str, to_ascii_printable
from utils import str_to_keypath, problem_file_line, parse_extended_key, get_filesize, extract_cosigner
from ux import ux_show_story, ux_confirm, ux_dramatic_pause, ux_clear_keys
from ux import import_export_prompt, ux_enter_bip32_index, ux_enter_number, OK, X
from ux import ux_enter_bip32_index, ux_enter_number, OK, X
from files import CardSlot, CardMissingError, needs_microsd
from descriptor import MultisigDescriptor, multisig_descriptor_template
from public_constants import AF_P2SH, AF_P2WSH_P2SH, AF_P2WSH, AFC_SCRIPT, MAX_SIGNERS, AF_CLASSIC

View File

@ -523,7 +523,6 @@ class NFCHandler:
from auth import psbt_encoding_taster, TXN_INPUT_OFFSET
from auth import UserAuthorizedAction, ApproveTransaction
from ux import the_ux
from auth import done_signing
from sffile import SFFile
data = await self.start_nfc_rx()
@ -567,10 +566,9 @@ class NFCHandler:
# start signing UX
UserAuthorizedAction.cleanup()
UserAuthorizedAction.active_request = ApproveTransaction(
psbt_len, 0x0, psbt_sha=psbt_sha,
approved_cb=done_signing,
cb_kws={"input_method": "nfc",
"output_encoder": output_encoder})
psbt_len, psbt_sha=psbt_sha, input_method="nfc",
output_encoder=output_encoder
)
# kill any menu stack, and put our thing at the top
the_ux.push(UserAuthorizedAction.active_request)

View File

@ -543,9 +543,9 @@ async def start_export(notes):
singular = (len(notes) == 1)
item = notes[0].type_label if singular else 'all notes & passwords'
choice = await import_export_prompt(item, is_import=False, title="Data Export", no_nfc=True,
footnotes="\n\nWARNING: No encryption happens here. "
"Your secrets will be cleartext.")
choice = await import_export_prompt(item, title="Data Export", no_nfc=True,
footnotes="WARNING: No encryption happens here."
" Your secrets will be cleartext.")
if choice == KEY_CANCEL:
return

View File

@ -81,7 +81,16 @@ class QRDisplaySingle(UserInteraction):
# draw display
dis.busy_bar(False)
dis.draw_qr_display(self.qr_data, self.msg or body, self.is_alnum,
if self.msg:
msg = self.msg
else:
msg = None
if isinstance(body, str) and not has_qwerty:
# on Mk4 if no self.msg, we show part of the body
msg = body
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)
@ -103,7 +112,7 @@ class QRDisplaySingle(UserInteraction):
break
else:
# Share any QR over NFC!
await NFC.share_text(self.addrs[self.idx], secret=self.secret)
await NFC.share_text(self.addrs[self.idx], is_secret=self.is_secret)
self.redraw()
continue
elif ch in 'xy'+KEY_ENTER+KEY_CANCEL:

View File

@ -4,11 +4,10 @@
# secure environment of two Q's.
#
import ngu, aes256ctr, bip39, json, ndef, chains
from io import BytesIO
from utils import xfp2str, deserialize_secret
from ubinascii import unhexlify as a2b_hex
from ubinascii import hexlify as b2a_hex
from glob import settings
from glob import settings, dis
from ux import ux_show_story, ux_confirm, the_ux, ux_dramatic_pause
from ux_q1 import show_bbqr_codes, QRScannerInteraction, ux_input_text
from charcodes import KEY_QR, KEY_NFC, KEY_CANCEL
@ -192,8 +191,9 @@ WARNING: Receiver will have full access to all Bitcoin controlled by these keys!
async def kt_do_send(rx_pubkey, dtype, raw=None, obj=None, prefix=b'', rx_label='the receiver', kp=None):
# We are rendering a QR and showing it to them for sending to another Q
from glob import dis
dis.fullscreen("Wait...")
cleartext = dtype.encode() + (raw or json.dumps(obj).encode())
dis.progress_bar_show(0.1)
# Pick and show noid key to sender
noid_key, txt = pick_noid_key()
@ -214,8 +214,8 @@ async def kt_do_send(rx_pubkey, dtype, raw=None, obj=None, prefix=b'', rx_label=
"\n\n %s = %s\n\n" % (rx_label, txt, txt_grouper(txt))
msg += "ENTER to view QR"
await tk_show_payload('S' if not prefix else 'E',
payload, 'Teleport Password', msg, cta='Show to Receiver')
await tk_show_payload('S' if not prefix else 'E', payload,
'Teleport Password', msg, cta='Show to Receiver')
if not prefix:
# not PSBT case ... reset menus, we are deep!
@ -269,7 +269,6 @@ async def kt_decode_rx(is_psbt, payload):
"Sender must start again.", title="Teleport Fail")
return
from glob import dis
while 1:
# ask for noid key
pw = await ux_input_text('', confirm_exit=False, hex_only=False, max_len=8,
@ -312,6 +311,7 @@ async def kt_accept_values(dtype, raw):
'''
from flow import has_se_secrets, goto_top_menu
dis.fullscreen("Wait...")
enc = None
origin = 'Teleported'
label = None
@ -341,7 +341,8 @@ async def kt_accept_values(dtype, raw):
out.write(raw)
# This will take over UX w/ the signing process
sign_transaction(psbt_len, flags=STXN_FINALIZE)
# flags=None --> whether to finalize is decided based on psbt.is_complete
sign_transaction(psbt_len, flags=None)
return
elif dtype == 'b':
@ -579,7 +580,6 @@ class SecretPickerMenu(MenuSystem):
if ch != 'y': return
from backups import render_backup_contents
from glob import dis
dis.fullscreen("Buiding Backup...")
@ -620,7 +620,7 @@ class SecretPickerMenu(MenuSystem):
await kt_do_send(self.rx_pubkey, 's', raw=raw)
async def kt_send_psbt(psbt, psbt_len=None, post_signing=False):
async def kt_send_psbt(psbt, psbt_len):
# We just finished adding our signature to an incomplete PSBT.
# User wants to send to one or more other senders for them to complete signing.
@ -632,44 +632,14 @@ async def kt_send_psbt(psbt, psbt_len=None, post_signing=False):
# maybe it's not really a PSBT where we know the other signers? might be
# a weird coinjoin we don't fully understand
if not need:
if not post_signing:
await ux_show_story("No more signers?")
await ux_show_story("No more signers?")
return
num_to_complete = ms.M - (ms.N - len(need))
# move out of PSRAM
from auth import TXN_OUTPUT_OFFSET
if post_signing:
# They just approved and signed a MS txn perhaps via USB or QR or any source
# - offer to save?
# - offer them to teleport it (we only come this far if possible)
if num_to_complete <= 0:
# Sufficiently signed. We can probably finalize it too.
# - if from USB, we'd be uploading back, SD would be saved, etc
return
ch = await ux_show_story("%d more signatures are still required. Press (T) to pick another co-signer to sign next, using QR codes, or ENTER for other options." % num_to_complete, title="Teleport PSBT?", escape='t')
if ch != 't':
# ENTER/CANCEL both come here because we don't want to lose the PSBT
# - they can also do a "T" and teleport again
from auth import done_signing
await done_signing(psbt)
return
if not psbt_len:
# we need it serialized, might have only saved into Base64 or something
with BytesIO() as fd:
psbt.serialize(fd) # need prog bar?
psbt_len = fd.tell()
bin_psbt = fd.getvalue()
else:
# move out of PSRAM
from auth import TXN_OUTPUT_OFFSET
with SFFile(TXN_OUTPUT_OFFSET, psbt_len) as fd:
bin_psbt = fd.read(psbt_len)
with SFFile(TXN_OUTPUT_OFFSET, psbt_len) as fd:
bin_psbt = fd.read(psbt_len)
my_xfp = settings.get('xfp')
@ -701,7 +671,8 @@ async def kt_send_psbt(psbt, psbt_len=None, post_signing=False):
async def sign_now(*a):
# this will reset the UX stack:
sign_transaction(psbt_len, flags=STXN_FINALIZE)
# flags=None --> whether to finalize is decided based on psbt.is_complete
sign_transaction(psbt_len, flags=None)
f = sign_now
@ -743,7 +714,6 @@ async def kt_send_file_psbt(*a):
from version import MAX_TXN_LEN
from ux import import_export_prompt
from psbt import psbtObject
from glob import dis
# choose any PSBT from SD
picked = await import_export_prompt("PSBT", is_import=True, no_nfc=True, no_qr=True)

View File

@ -393,7 +393,7 @@ def _import_prompt_builder(title, no_qr, no_nfc, slot_b_only=False):
def export_prompt_builder(what_it_is, no_qr=False, no_nfc=False, key0=None, offer_kt=False,
force_prompt=False):
force_prompt=False, txid=None):
# Build the prompt for export
# - key0 can be for special stuff
from glob import NFC, VD
@ -430,6 +430,10 @@ def export_prompt_builder(what_it_is, no_qr=False, no_nfc=False, key0=None, offe
prompt += ", (4) to show QR code"
escape += '4'
if txid:
prompt += ", (6) for QR Code of TXID"
escape += "6"
if offer_kt:
prompt += ", (T) to " + offer_kt
escape += 't'
@ -474,8 +478,9 @@ def import_export_prompt_decode(ch):
return dict(force_vdisk=force_vdisk, slot_b=slot_b)
async def import_export_prompt(what_it_is, is_import=False, no_qr=False,
no_nfc=False, title=None, intro='', footnotes='',
offer_kt=False, slot_b_only=False, force_prompt=False):
no_nfc=False, title=None, intro='', footnotes='',
offer_kt=False, slot_b_only=False, force_prompt=False,
txid=None):
# Show story allowing user to select source for importing/exporting
# - return either str(mode) OR dict(file_args)
@ -483,11 +488,12 @@ async def import_export_prompt(what_it_is, is_import=False, no_qr=False,
# - KEY_CANCEL for abort by user
# - dict() => do file system thing, using file_args to control vdisk vs. SD vs slot_b
# - 't' => key teleport, but only offered with offer_kt is set (contetxt, and Q only)
from glob import NFC
if is_import:
prompt, escape = _import_prompt_builder(what_it_is, no_qr, no_nfc, slot_b_only)
else:
prompt, escape = export_prompt_builder(what_it_is, no_qr, no_nfc,
prompt, escape = export_prompt_builder(what_it_is, no_qr, no_nfc, txid=txid,
force_prompt=force_prompt, offer_kt=offer_kt)
# TODO: detect if we're only asking A or B, when just one card is inserted
@ -498,8 +504,10 @@ async def import_export_prompt(what_it_is, is_import=False, no_qr=False,
# they don't have NFC nor VD enabled, and no second slots... so will be file.
return dict(force_vdisk=False, slot_b=None)
else:
ch = await ux_show_story(intro+prompt+footnotes, escape=escape, title=title,
strict_escape=True)
hints = ("" if no_qr else KEY_QR) + (KEY_NFC if not no_nfc and NFC else "")
msg_lst = [i for i in (intro, prompt, footnotes) if i]
ch = await ux_show_story("\n\n".join(msg_lst), escape=escape, title=title,
strict_escape=True, hint_icons=hints)
return import_export_prompt_decode(ch)

View File

@ -993,7 +993,7 @@ class QRScannerInteraction:
async def qr_psbt_sign(decoder, psbt_len, raw):
# Got a PSBT coming in from QR scanner. Sign it.
# - similar to auth.sign_psbt_file()
from auth import UserAuthorizedAction, ApproveTransaction, done_signing
from auth import UserAuthorizedAction, ApproveTransaction
from ux import the_ux
from sffile import SFFile
from auth import TXN_INPUT_OFFSET, psbt_encoding_taster
@ -1024,9 +1024,8 @@ async def qr_psbt_sign(decoder, psbt_len, raw):
UserAuthorizedAction.cleanup()
UserAuthorizedAction.active_request = ApproveTransaction(
psbt_len, approved_cb=done_signing,
cb_kws={"input_method": "qr",
"output_encoder": output_encoder}
psbt_len, input_method="qr",
output_encoder=output_encoder
)
the_ux.push(UserAuthorizedAction.active_request)
@ -1193,19 +1192,18 @@ async def show_bbqr_codes(type_code, data, msg, already_hex=False):
# BBQr header
hdr = 'B$' + encoding + type_code + int2base36(num_parts) + int2base36(pkt)
# encode the bytes
assert pos < data_len, (pkt, pos, data_len)
if already_hex:
# not encoding, just chars->bytes
# not encoding, just hex string
hp = pos*2
body = data[hp:hp+(part_size*2)].decode()
body = data[hp:hp+(part_size*2)]
else:
# base32 encoding
# encode bytes to base32 encoding
body = b32encode(data[pos:pos+part_size])
pos += part_size
# first first packet, want to discover a working small value for QR version
# first packet, want to discover a working small value for QR version
if pkt == 0:
mnv = 10 if num_parts > 1 else 1
else:

View File

@ -1287,14 +1287,15 @@ def try_sign_microsd(open_microsd, cap_story, pick_menu_item, goto_home,
for r in range(10):
time.sleep(0.1)
title, story = cap_story()
if title == 'PSBT Signed': break
if 'Updated PSBT' in story: break
if 'Finalized transaction' in story: break
else:
assert False, 'timed out'
lines = story.split('\n')
txid = None
if 'Final TXID:' in lines:
txid = lines[lines.index('Final TXID:')+1]
if 'TXID:' in lines:
txid = lines[lines.index('TXID:')+1]
# This is fragile!
# ignore "Press (T) to use Key Teleport to send PSBT to other co-signers" footer
@ -1359,9 +1360,11 @@ def try_sign_microsd(open_microsd, cap_story, pick_menu_item, goto_home,
@pytest.fixture
def try_sign(start_sign, end_sign):
def doit(filename_or_data, accept=True, finalize=False, accept_ms_import=False):
def doit(filename_or_data, accept=True, finalize=False, accept_ms_import=False,
exit_export_loop=True):
ip = start_sign(filename_or_data, finalize=finalize)
return ip, end_sign(accept, finalize=finalize, accept_ms_import=accept_ms_import)
return ip, end_sign(accept, finalize=finalize, accept_ms_import=accept_ms_import,
exit_export_loop=exit_export_loop)
return doit
@ -1387,10 +1390,11 @@ def start_sign(dev):
return doit
@pytest.fixture
def end_sign(dev, need_keypress):
def end_sign(dev, need_keypress, press_cancel):
from ckcc_protocol.protocol import CCUserRefused
def doit(accept=True, in_psbt=None, finalize=False, accept_ms_import=False, expect_txn=True):
def doit(accept=True, finalize=False, accept_ms_import=False, expect_txn=True,
exit_export_loop=True):
if accept_ms_import:
# XXX would be better to do cap_story here, but that would limit test to simulator
@ -1445,6 +1449,9 @@ def end_sign(dev, need_keypress):
for sig in sigs:
assert len(sig) <= 71, "overly long signature observed"
if exit_export_loop:
press_cancel() # landed back to export prompt - exit
return psbt_out
return doit
@ -1871,10 +1878,48 @@ def load_export_and_verify_signature(microsd_path, virtdisk_path, verify_detache
return contents, address, fname
return doit
@pytest.fixture
def file_tx_signing_done(virtdisk_path, microsd_path):
def doit(story, encoding="base64", is_vdisk=False):
path_f = virtdisk_path if is_vdisk else microsd_path
enc = "rb" if encoding == "binary" else "r"
_split = story.split("\n\n")
export = None
if 'Updated PSBT is:' == _split[0]:
fname = _split[1]
path = path_f(fname)
with open(path, enc) as f:
export = f.read().strip()
export_tx = None
if "Finalized transaction (ready for broadcast)" in _split[2]:
fname_tx = _split[3]
path_tx = path_f(fname_tx)
with open(path_tx, enc) as f:
export_tx = f.read().strip()
else:
# just finalized tx
assert "Finalized transaction (ready for broadcast):" == _split[0]
fname_tx = _split[1]
path_tx = path_f(fname_tx)
with open(path_tx, enc) as f:
export_tx = f.read()
txid = None
for l in _split:
if "TXID" in l:
txid = l.split("\n")[-1].strip()
assert len(txid) == 64, "wrong txid"
break
return export, export_tx, txid
return doit
@pytest.fixture
def load_export(need_keypress, cap_story, microsd_path, virtdisk_path, nfc_read_text, nfc_read_json,
load_export_and_verify_signature, is_q1, press_cancel, press_select, readback_bbqr,
cap_screen_qr, nfc_read_txn):
cap_screen_qr, nfc_read_txn, file_tx_signing_done):
def doit(way, label, is_json, sig_check=True, addr_fmt=AF_CLASSIC, ret_sig_addr=False,
tail_check=None, sd_key=None, vdisk_key=None, nfc_key=None, ret_fname=False,
fpattern=None, qr_key=None, is_tx=False, encoding="base64"):
@ -1954,29 +1999,7 @@ def load_export(need_keypress, cap_story, microsd_path, virtdisk_path, nfc_read_
label=label, tail_check=tail_check, fpattern=fpattern
)
elif is_tx:
enc = "rb" if encoding == "binary" else "r"
_split = story.split("\n\n")
export = None
if 'Updated PSBT is:' == _split[0]:
fname = _split[1]
path = path_f(fname)
with open(path, enc) as f:
export = f.read()
export_tx = None
if "Finalized transaction (ready for broadcast)" in _split[2]:
fname_tx = _split[3]
path_tx = path_f(fname_tx)
with open(path_tx, enc) as f:
export_tx = f.read()
else:
# just finalized tx
assert "Finalized transaction (ready for broadcast):" == _split[0]
fname_tx = _split[1]
path_tx = path_f(fname_tx)
with open(path_tx, enc) as f:
export_tx = f.read()
export, export_tx, _ = file_tx_signing_done(story, encoding, is_vdisk=(way == "vdisk"))
return export, export_tx
else:
assert f"{label} file written" in story
@ -2014,7 +2037,6 @@ def signing_artifacts_reexport(cap_story, need_keypress, load_export, press_canc
def _check_story(the_way):
time.sleep(.2)
title, story = cap_story()
assert title == "PSBT Signed"
if the_way in ["qr", "nfc"]:
what = label + " shared via %s." % the_way.upper()
@ -2027,9 +2049,6 @@ def signing_artifacts_reexport(cap_story, need_keypress, load_export, press_canc
if txid:
assert txid in story
assert "Press (0) to save again" in story
need_keypress("0")
to_do = ["sd", "vdisk", "nfc", "qr"]
if not is_usb:
_check_story(way)
@ -2544,6 +2563,7 @@ from test_notes import need_some_notes, need_some_passwords
from test_nfc import try_sign_nfc, ndef_parse_txn_psbt
from test_se2 import goto_trick_menu, clear_all_tricks, new_trick_pin, se2_gate, new_pin_confirmed
from test_seed_xor import restore_seed_xor
from test_sign import txid_from_export_prompt
from test_ux import pass_word_quiz, word_menu_entry, enable_hw_ux
from txn import fake_txn

View File

@ -2706,7 +2706,7 @@ def bitcoind_multisig(bitcoind, bitcoind_d_sim_watch, need_keypress, cap_story,
@pytest.mark.parametrize("script", ["p2wsh", "p2sh-p2wsh", "p2sh"])
@pytest.mark.parametrize('desc', ["multi", "sortedmulti"])
def test_finalization(m_n, script, desc, use_regtest, clear_ms, bitcoind_multisig, bitcoind,
try_sign, cap_story, settings_set):
try_sign, cap_story, settings_set, txid_from_export_prompt, press_cancel):
if desc == "multi":
settings_set("unsort_ms", 1)
@ -2734,15 +2734,16 @@ def test_finalization(m_n, script, desc, use_regtest, clear_ms, bitcoind_multisi
psbt_bytes = base64.b64decode(psbt)
# USB sign with COLDCARD & finalize
_, txn = try_sign(psbt_bytes, finalize=True)
_, txn = try_sign(psbt_bytes, finalize=True, exit_export_loop=False)
tx_hex = txn.hex()
res = wo.testmempoolaccept([tx_hex])
assert res[0]["allowed"]
res = wo.sendrawtransaction(tx_hex)
assert len(res) == 64 # tx id
time.sleep(0.1)
_, story = cap_story()
cc_tx_id = story.split("\n\n")[0]
cc_tx_id = txid_from_export_prompt()
press_cancel() # exit QR display
press_cancel() # exit export loop
assert res == cc_tx_id
wo.generatetoaddress(1, bitcoind.supply_wallet.getnewaddress())
@ -2760,15 +2761,16 @@ def test_finalization(m_n, script, desc, use_regtest, clear_ms, bitcoind_multisi
psbt_bytes = base64.b64decode(psbt)
# USB sign with COLDCARD & finalize
_, txn = try_sign(psbt_bytes, finalize=True)
_, txn = try_sign(psbt_bytes, finalize=True, exit_export_loop=False)
tx_hex = txn.hex()
res = wo.testmempoolaccept([tx_hex])
assert res[0]["allowed"]
res = wo.sendrawtransaction(tx_hex)
assert len(res) == 64 # tx id
time.sleep(0.1)
_, story = cap_story()
cc_tx_id = story.split("\n\n")[0]
cc_tx_id = txid_from_export_prompt()
press_cancel() # exit QR display
press_cancel() # exit export loop
assert res == cc_tx_id
wo.generatetoaddress(1, bitcoind.supply_wallet.getnewaddress())
@ -2779,13 +2781,14 @@ def test_finalization(m_n, script, desc, use_regtest, clear_ms, bitcoind_multisi
@pytest.mark.parametrize("m_n", [(2,3), (3,5), (15,15)])
@pytest.mark.parametrize("script", ["p2wsh", "p2sh-p2wsh", "p2sh"])
@pytest.mark.parametrize("sighash", list(SIGHASH_MAP.keys()))
@pytest.mark.parametrize("psbt_v2", [True, False])
@pytest.mark.parametrize('desc', ["multi", "sortedmulti"])
def test_bitcoind_MofN_tutorial(m_n, script, clear_ms, goto_home, need_keypress, pick_menu_item,
sighash, cap_menu, cap_story, microsd_path, use_regtest, bitcoind,
microsd_wipe, settings_set, psbt_v2, is_q1, try_sign,
finalize_v2_v0_convert, press_select, desc, bitcoind_multisig):
microsd_wipe, settings_set, is_q1, try_sign, press_select,
finalize_v2_v0_convert, desc, bitcoind_multisig, press_cancel,
txid_from_export_prompt, pytestconfig, file_tx_signing_done):
# 2of2 case here is described in docs with tutorial
# TODO This test MUST be run with --psbt2 flag on and off
if desc == "multi":
settings_set("unsort_ms", 1)
@ -2823,7 +2826,7 @@ def test_bitcoind_MofN_tutorial(m_n, script, clear_ms, goto_home, need_keypress,
half_signed_psbt = signer.walletprocesspsbt(psbt, True, sighash, True) # do not finalize
psbt = half_signed_psbt["psbt"]
if psbt_v2:
if pytestconfig.getoption('psbt2'):
# below is noop if psbt is already v2
po = BasicPSBT().parse(base64.b64decode(psbt))
po.to_v2()
@ -2856,20 +2859,11 @@ def test_bitcoind_MofN_tutorial(m_n, script, clear_ms, goto_home, need_keypress,
press_select() # confirm signing
time.sleep(0.1)
title, story = cap_story()
assert "PSBT Signed" == title
assert "Updated PSBT is:" in story
press_select()
os.remove(microsd_path(name))
split_story = story.split("\n\n")
fname = split_story[1]
fname_tx = split_story[3]
cc_tx_id = split_story[-2].split("\n")[-1]
with open(microsd_path(fname), "r") as f:
final_psbt = f.read().strip()
with open(microsd_path(fname_tx), "r") as f:
final_tx = f.read().strip()
final_psbt, final_tx, cc_tx_id = file_tx_signing_done(story)
po = BasicPSBT().parse(base64.b64decode(final_psbt))
res = finalize_v2_v0_convert(po)
@ -2897,7 +2891,7 @@ def test_bitcoind_MofN_tutorial(m_n, script, clear_ms, goto_home, need_keypress,
half_signed_psbt = signer.walletprocesspsbt(psbt, True, sighash, True) # do not finalize
psbt = half_signed_psbt["psbt"]
if psbt_v2:
if pytestconfig.getoption('psbt2'):
# below is noop if psbt is already v2
po = BasicPSBT().parse(base64.b64decode(psbt))
po.to_v2()
@ -2905,15 +2899,15 @@ def test_bitcoind_MofN_tutorial(m_n, script, clear_ms, goto_home, need_keypress,
psbt_bytes = base64.b64decode(psbt)
# USB sign with COLDCARD & finalize
_, txn = try_sign(psbt_bytes, finalize=True)
_, txn = try_sign(psbt_bytes, finalize=True, exit_export_loop=False)
tx_hex = txn.hex()
res = bitcoind_watch_only.testmempoolaccept([tx_hex])
assert res[0]["allowed"]
res = bitcoind_watch_only.sendrawtransaction(tx_hex)
assert len(res) == 64 # tx id
time.sleep(0.1)
_, story = cap_story()
cc_tx_id = story.split("\n\n")[0]
cc_tx_id = txid_from_export_prompt()
press_cancel() # exit QR display
press_cancel() # exit export loop
assert res == cc_tx_id
bitcoind_watch_only.generatetoaddress(1, bitcoind.supply_wallet.getnewaddress()) # need to mine above tx
@ -2931,6 +2925,10 @@ def test_bitcoind_MofN_tutorial(m_n, script, clear_ms, goto_home, need_keypress,
x = BasicPSBT().parse(base64.b64decode(psbt))
for idx, i in enumerate(x.inputs):
i.sighash = SIGHASH_MAP[sighash]
if pytestconfig.getoption('psbt2'):
x.to_v2()
psbt = x.as_b64_str()
name = f"change_{M}of{N}_{script}.psbt"
@ -2957,12 +2955,11 @@ def test_bitcoind_MofN_tutorial(m_n, script, clear_ms, goto_home, need_keypress,
assert "missing" in story
return
assert "PSBT Signed" == title
assert "Updated PSBT is:" in story
press_select()
fname = story.split("\n\n")[-2]
with open(microsd_path(fname), "r") as f:
cc_signed_psbt = f.read().strip()
cc_signed_psbt, _txn, _txid = file_tx_signing_done(story)
assert _txn is None and _txid is None
press_cancel() # exit re-export loop
po = BasicPSBT().parse(base64.b64decode(cc_signed_psbt))
cc_signed_psbt = finalize_v2_v0_convert(po)["psbt"]
@ -3330,7 +3327,6 @@ def test_bare_cc_ms_qr_import(N, make_multisig, scan_a_qr, clear_ms, goto_home,
press_cancel()
@pytest.mark.parametrize("psbtv2", [True, False])
@pytest.mark.parametrize("desc", ["multi", "sortedmulti"])
@pytest.mark.parametrize("data", [
# (out_style, amount, is_change)
@ -3339,8 +3335,9 @@ def test_bare_cc_ms_qr_import(N, make_multisig, scan_a_qr, clear_ms, goto_home,
[("p2wsh-p2sh", 1000000, 1)] * 18 + [("p2wsh", 50000000, 0)] * 12,
[("p2sh", 1000000, 1), ("p2wsh-p2sh", 50000000, 0), ("p2wsh", 800000, 1)] * 14,
])
def test_txout_explorer(psbtv2, data, clear_ms, import_ms_wallet, fake_ms_txn,
start_sign, txout_explorer, desc):
def test_txout_explorer(data, clear_ms, import_ms_wallet, fake_ms_txn,
start_sign, txout_explorer, desc, pytestconfig):
# TODO This test MUST be run with --psbt2 flag on and off
clear_ms()
M, N = 2, 3
descriptor, bip67 = False, True
@ -3362,7 +3359,8 @@ def test_txout_explorer(psbtv2, data, clear_ms, import_ms_wallet, fake_ms_txn,
inp_amount = sum(outvals) + 100000 # 100k sat fee
psbt = fake_ms_txn(1, len(data), M, keys, outstyles=outstyles,
outvals=outvals, change_outputs=change_outputs,
input_amount=inp_amount, psbt_v2=psbtv2, bip67=bip67)
input_amount=inp_amount, psbt_v2=pytestconfig.getoption('psbt2'),
bip67=bip67)
start_sign(psbt)
txout_explorer(data)

View File

@ -244,7 +244,7 @@ def try_sign_nfc(cap_story, pick_menu_item, goto_home, need_keypress,
for r in range(10):
time.sleep(0.1)
title, story = cap_story()
if title == 'PSBT Signed': break
if "shared via NFC" in story: break
else:
assert False, 'timed out'
@ -347,7 +347,7 @@ def test_nfc_after(num_outs, fake_txn, try_sign, nfc_read, need_keypress,
cap_story, is_q1, press_nfc, press_cancel):
# Read signing result (transaction) over NFC, decode it.
psbt = fake_txn(1, num_outs)
orig, result = try_sign(psbt, accept=True, finalize=True)
orig, result = try_sign(psbt, accept=True, finalize=True, exit_export_loop=False)
too_big = len(result) > 8000
@ -356,19 +356,21 @@ def test_nfc_after(num_outs, fake_txn, try_sign, nfc_read, need_keypress,
time.sleep(.1)
title, story = cap_story()
assert 'TXID' in title, story
txid = a2b_hex(story.split()[0])
assert f'Press {KEY_NFC if is_q1 else "(3)"}' in story
assert 'TXID' in story, story
txid = a2b_hex(story.split("\n")[3])
assert f'press {KEY_NFC if is_q1 else "(3)"}' in story
press_nfc()
time.sleep(.2)
if too_big:
title, story = cap_story()
assert 'is too large' in story
press_cancel()
return
contents = nfc_read()
press_cancel()
press_cancel()
#print("contents = " + B2A(contents))
for got in ndef.message_decoder(contents):
@ -482,7 +484,7 @@ def test_nfc_pushtx(num_outs, chain, enable_nfc, settings_set, settings_remove,
psbt = fake_txn(2, num_outs)
if way == "usb":
_, result = try_sign(psbt, finalize=True)
_, result = try_sign(psbt, finalize=True, exit_export_loop=False)
elif way == "sd":
ip, result, txid = try_sign_microsd(psbt, finalize=True, nfc_push_tx=True)
elif way == "nfc":
@ -501,10 +503,9 @@ def test_nfc_pushtx(num_outs, chain, enable_nfc, settings_set, settings_remove,
time.sleep(.1)
title, story = cap_story()
if way == "usb":
assert title == 'Final TXID'
assert 'to share signed txn' in story
assert 'TXID' in story
elif way == "sd":
assert title == "PSBT Signed"
assert ('Updated PSBT' in story) or ('Finalized transaction' in story)
else:
assert False
return

View File

@ -134,7 +134,7 @@ def test_psbt_proxy_parsing(fn, sim_execfile, sim_exec):
@pytest.mark.unfinalized
def test_speed_test(dev, fake_txn, is_mark3, is_mark4, start_sign, end_sign,
press_select):
press_select, press_cancel):
# measure time to sign a larger txn
if is_mark4:
# Mk4: expect
@ -169,6 +169,7 @@ def test_speed_test(dev, fake_txn, is_mark3, is_mark4, start_sign, end_sign,
print(" Tx time: %.1f" % tx_time)
print("Sign time: %.1f" % ready_time)
press_cancel()
if 0:
# TODO: attempt to re-create the mega transaction: 5,569 inputs, one out
@ -1211,10 +1212,26 @@ def hist_count(sim_exec):
'import history; RV.write(str(len(history.OutptValueCache.runtime_cache)));'))
return doit
@pytest.fixture
def txid_from_export_prompt(cap_story, cap_screen_qr, cap_screen, need_keypress):
def doit():
time.sleep(.1)
title, story = cap_story()
assert "(6) for QR Code of TXID" in story
need_keypress("6")
time.sleep(.1)
screen_txid = cap_screen().strip().replace("\n", "").replace("~", "")
qr_txid = cap_screen_qr().decode().strip().lower()
assert qr_txid == screen_txid
return qr_txid
return doit
@pytest.mark.parametrize('num_utxo', [9, 100])
@pytest.mark.parametrize('segwit_in', [False, True])
def test_bip143_attack_data_capture(num_utxo, segwit_in, try_sign, fake_txn, settings_set,
settings_get, cap_story, sim_exec, hist_count):
settings_get, cap_story, sim_exec, hist_count,
txid_from_export_prompt, press_cancel):
# cleanup prev runs, if very first time thru
sim_exec('import history; history.OutptValueCache.clear()')
@ -1224,16 +1241,15 @@ def test_bip143_attack_data_capture(num_utxo, segwit_in, try_sign, fake_txn, set
# make a txn, capture the outputs of that as inputs for another txn
psbt = fake_txn(1, num_utxo+3, segwit_in=segwit_in, change_outputs=range(num_utxo+2),
outstyles=(['p2wpkh']*num_utxo) + ['p2wpkh-p2sh', 'p2pkh'])
_, txn = try_sign(psbt, accept=True, finalize=True)
_, txn = try_sign(psbt, accept=True, finalize=True, exit_export_loop=False)
open('debug/funding.psbt', 'wb').write(psbt)
num_inp_utxo = (1 if segwit_in else 0)
time.sleep(.1)
title, story = cap_story()
assert 'TXID' in title, story
txid = story.strip().split()[0]
txid = txid_from_export_prompt()
press_cancel()
press_cancel()
assert hist_count() in {128, hist_b4+num_utxo+num_inp_utxo}
@ -1273,21 +1289,17 @@ def test_bip143_attack_data_capture(num_utxo, segwit_in, try_sign, fake_txn, set
@pytest.mark.parametrize('segwit', [False, True])
@pytest.mark.parametrize('num_ins', [1, 17])
def test_txid_calc(num_ins, fake_txn, try_sign, dev, segwit, decode_with_bitcoind, cap_story):
def test_txid_calc(num_ins, fake_txn, try_sign, dev, segwit, decode_with_bitcoind, cap_story,
txid_from_export_prompt, press_cancel):
# verify correct txid for transactions is being calculated
xp = dev.master_xpub
psbt = fake_txn(num_ins, 1, xp, segwit_in=segwit)
_, txn = try_sign(psbt, accept=True, finalize=True)
#print('Signed; ' + B2A(txn))
time.sleep(.1)
title, story = cap_story()
assert '0' in story
assert 'TXID' in title, story
txid = story.strip().split()[0]
_, txn = try_sign(psbt, accept=True, finalize=True, exit_export_loop=False)
txid = txid_from_export_prompt()
press_cancel() # exit QR
press_cancel() # exit re-export loop
if 1:
t = CTransaction()
@ -1305,6 +1317,7 @@ def test_txid_calc(num_ins, fake_txn, try_sign, dev, segwit, decode_with_bitcoin
assert decoded['txid'] == txid
@pytest.mark.unfinalized # iff partial=1
@pytest.mark.reexport
@pytest.mark.parametrize('encoding', ['binary', 'hex', 'base64'])
@ -1527,36 +1540,36 @@ def test_value_render(dev, units, fake_txn, start_sign, cap_story, settings_set,
@pytest.mark.qrcode
@pytest.mark.parametrize('num_in', [1,2,3])
@pytest.mark.parametrize('num_out', [1,2,3])
def test_qr_txn(num_in, num_out, request, fake_txn, try_sign, dev, cap_screen_qr, qr_quality_check, cap_story, need_keypress):
segwit=True
@pytest.mark.parametrize('segwit', [True, False])
def test_qr_txn(num_in, num_out, segwit, fake_txn, try_sign, dev, cap_screen_qr,
qr_quality_check, cap_story, need_keypress, is_q1, press_cancel):
psbt = fake_txn(num_in, num_out, dev.master_xpub, segwit_in=False)
psbt = fake_txn(num_in, num_out, dev.master_xpub, segwit_in=segwit)
_, txn = try_sign(psbt, accept=True, finalize=True)
open('debug/last.txn', 'wb').write(txn)
print("txn len = %d bytes" % len(txn))
_, txn = try_sign(psbt, accept=True, finalize=True, exit_export_loop=False)
with open('debug/last.txn', 'wb') as f:
f.write(txn)
title, story = cap_story()
assert 'QR Code' in story
assert '(6) for QR Code of TXID' in story
if 1:
# check TXID qr code
need_keypress('1')
# check TXID qr code
need_keypress('6')
qr = cap_screen_qr().decode()
press_cancel()
qr = cap_screen_qr().decode()
t = CTransaction()
t.deserialize(BytesIO(txn))
assert t.txid().hex() == qr.lower()
t = CTransaction()
t.deserialize(BytesIO(txn))
assert t.txid().hex() == qr.lower()
else:
# TODO: QR for txn itself yet
need_keypress('2')
if is_q1:
need_keypress(KEY_QR)
qr = cap_screen_qr().decode()
assert qr.lower() == txn.hex()
press_cancel()
press_cancel()
def test_missing_keypaths(dev, try_sign, fake_txn):
@ -1755,7 +1768,8 @@ def test_bitcoind_missing_foreign_utxo(bitcoind, bitcoind_d_sim_watch, microsd_p
b"$$$$$$$$$$$$$$ Bitcoin",
b"\xeb\x97\xf7\xb7\xf78\x9a';\x90F_\xfc\xe2b\xa4\x93)\xea\xac\xacR\xff\x9c\xbe\x1c\xf1\xad\xe9!\xee\xd9t1\x1f\x92\x83\x97\xb3\x98/\xff\xc8\xff\xc1\xc0\xdd\x1et\x00L\x13\xe0\xe3\x90\xe4\xd4\xf2x:\xf7Ab\x04\x91\x1e\xa8R\x92\xd3\x96OK\xc6I\x06\x9e\xce=\xb3",
])
def test_op_return_signing(op_return_data, dev, fake_txn, bitcoind_d_sim_watch, bitcoind, start_sign, end_sign, cap_story):
def test_op_return_signing(op_return_data, dev, fake_txn, bitcoind_d_sim_watch, bitcoind,
start_sign, end_sign, cap_story):
cc = bitcoind_d_sim_watch
dest_address = cc.getnewaddress()
bitcoind.supply_wallet.generatetoaddress(101, dest_address)
@ -1864,12 +1878,19 @@ def test_duplicate_unknow_values_in_psbt(dev, start_sign, end_sign, fake_txn):
@pytest.fixture
def _test_single_sig_sighash(cap_story, press_select, start_sign, end_sign, dev,
bitcoind, bitcoind_d_dev_watch, settings_set, finalize_v2_v0_convert):
bitcoind, bitcoind_d_dev_watch, settings_set,
finalize_v2_v0_convert, pytestconfig):
def doit(addr_fmt, sighash, num_inputs=2, num_outputs=2, consolidation=False, sh_checks=False,
psbt_v2=False, tx_check=True):
psbt_v2=None, tx_check=True):
from decimal import Decimal, ROUND_DOWN
if psbt_v2 is None:
# anything passed directly to this function overrides
# pytest flag --psbt2 - only care about pytest flag
# if psbt_v2 is not specified (None)
psbt_v2 = pytestconfig.getoption('psbt2')
if dev.is_simulator:
# if running against real HW you need to set CC to correct sighshchk mode
# Below test need to run with sighshchk disabled:
@ -2044,38 +2065,34 @@ def _test_single_sig_sighash(cap_story, press_select, start_sign, end_sign, dev,
return doit
# TODO Locktime test MUST be run with --psbt2 flag on and off
# pytest test_sign.py -k locktime {--psbt2,}
@pytest.mark.bitcoind
@pytest.mark.parametrize("addr_fmt", ["legacy", "p2sh-segwit", "bech32"])
@pytest.mark.parametrize("sighash", [sh for sh in SIGHASH_MAP if sh != 'ALL'])
@pytest.mark.parametrize("num_outs", [1, 3, 5])
@pytest.mark.parametrize("num_ins", [2, 5])
@pytest.mark.parametrize("psbt_v2", [True, False])
def test_sighash_same(addr_fmt, sighash, num_ins, num_outs, psbt_v2, _test_single_sig_sighash):
def test_sighash_same(addr_fmt, sighash, num_ins, num_outs, _test_single_sig_sighash):
# sighash is the same among all inputs
_test_single_sig_sighash(addr_fmt, [sighash], num_inputs=num_ins, num_outputs=num_outs,
psbt_v2=psbt_v2)
_test_single_sig_sighash(addr_fmt, [sighash], num_inputs=num_ins, num_outputs=num_outs)
@pytest.mark.bitcoind
@pytest.mark.parametrize("addr_fmt", ["legacy", "p2sh-segwit", "bech32"])
@pytest.mark.parametrize("sighash", list(itertools.combinations(SIGHASH_MAP.keys(), 2)))
@pytest.mark.parametrize("num_outs", [2, 3, 5])
@pytest.mark.parametrize("psbt_v2", [True, False])
def test_sighash_different(addr_fmt, sighash, num_outs, psbt_v2, _test_single_sig_sighash):
def test_sighash_different(addr_fmt, sighash, num_outs, _test_single_sig_sighash):
# sighash differ among all inputs
_test_single_sig_sighash(addr_fmt, sighash, num_inputs=2, num_outputs=num_outs,
psbt_v2=psbt_v2)
_test_single_sig_sighash(addr_fmt, sighash, num_inputs=2, num_outputs=num_outs)
@pytest.mark.bitcoind
@pytest.mark.parametrize("addr_fmt", ["legacy", "p2sh-segwit", "bech32"])
@pytest.mark.parametrize("num_outs", [5, 8])
@pytest.mark.parametrize("psbt_v2", [True, False])
def test_sighash_fullmix(addr_fmt, num_outs, psbt_v2, _test_single_sig_sighash):
def test_sighash_fullmix(addr_fmt, num_outs, _test_single_sig_sighash):
# tx with 6 inputs representing all possible sighashes
_test_single_sig_sighash(addr_fmt, tuple(SIGHASH_MAP.keys()), num_inputs=6,
num_outputs=num_outs, psbt_v2=psbt_v2)
_test_single_sig_sighash(addr_fmt, tuple(SIGHASH_MAP.keys()), num_inputs=6, num_outputs=num_outs)
@pytest.mark.bitcoind
@ -2207,7 +2224,7 @@ def test_batch_sign(num_tx, ui_path, action, fake_txn, need_keypress,
time.sleep(.5)
title, story = cap_story()
assert "-signed.psbt" in story
press_select()
press_cancel()
time.sleep(.5)
title, story = cap_story()
@ -2337,7 +2354,7 @@ def test_psbt_v2_global_quantities(way, fake_txn, start_sign, end_sign, cap_stor
])
def test_locktime_ux(use_regtest, bitcoind_d_sim_watch, start_sign, end_sign,
microsd_path, cap_story, goto_home, press_select,
pick_menu_item, bitcoind, locktime):
pick_menu_item, bitcoind, locktime, file_tx_signing_done):
use_regtest()
sim = bitcoind_d_sim_watch
addr = sim.getnewaddress()
@ -2366,12 +2383,15 @@ def test_locktime_ux(use_regtest, bitcoind_d_sim_watch, start_sign, end_sign,
psbt_fname = "locktime.psbt"
with open(microsd_path(psbt_fname), "w") as f:
f.write(psbt)
goto_home()
pick_menu_item('Ready To Sign')
time.sleep(0.1)
pick_menu_item(psbt_fname)
time.sleep(0.1)
title, story = cap_story()
if 'OK TO SEND' not in title:
pick_menu_item(psbt_fname)
time.sleep(0.1)
title, story = cap_story()
assert "WARNING" not in story
if locktime != 0:
@ -2390,18 +2410,10 @@ def test_locktime_ux(use_regtest, bitcoind_d_sim_watch, start_sign, end_sign,
press_select() # confirm signing
time.sleep(0.1)
title, story = cap_story()
assert title == 'PSBT Signed'
assert "Updated PSBT is:" in story
assert "Finalized transaction (ready for broadcast)" in story
assert "TXID" in story
split_story = story.split("\n\n")
story_txid = split_story[-2].split("\n")[-1]
signed_psbt_fname = split_story[1]
with open(microsd_path(signed_psbt_fname), "r") as f:
signed_psbt = f.read().strip()
signed_txn_fname = split_story[3]
with open(microsd_path(signed_txn_fname), "r") as f:
signed_txn = f.read().strip()
signed_psbt, signed_txn, story_txid = file_tx_signing_done(story)
assert signed_psbt != psbt
finalize_res = sim.finalizepsbt(signed_psbt)
bitcoind_signed_txn = finalize_res["hex"]
@ -2426,7 +2438,7 @@ def test_locktime_ux(use_regtest, bitcoind_d_sim_watch, start_sign, end_sign,
def test_nsequence_blockheight_relative_locktime_ux(sequence, use_regtest, bitcoind_d_sim_watch,
start_sign, end_sign, microsd_path, cap_story,
goto_home, press_select, pick_menu_item,
bitcoind, num_ins, differ):
bitcoind, num_ins, differ, file_tx_signing_done):
if differ and (sequence == 0):
# this case makes no sense
return
@ -2475,12 +2487,15 @@ def test_nsequence_blockheight_relative_locktime_ux(sequence, use_regtest, bitco
psbt_fname = "rtl-blockheight.psbt"
with open(microsd_path(psbt_fname), "w") as f:
f.write(psbt)
goto_home()
pick_menu_item('Ready To Sign')
time.sleep(0.1)
pick_menu_item(psbt_fname)
time.sleep(0.1)
title, story = cap_story()
if 'OK TO SEND' not in title:
pick_menu_item(psbt_fname)
time.sleep(0.1)
title, story = cap_story()
assert "WARNING" not in story
if sequence:
@ -2502,18 +2517,11 @@ def test_nsequence_blockheight_relative_locktime_ux(sequence, use_regtest, bitco
press_select() # confirm signing
time.sleep(0.1)
title, story = cap_story()
assert title == 'PSBT Signed'
assert "Updated PSBT is:" in story
assert "Finalized transaction (ready for broadcast)" in story
assert "TXID" in story
split_story = story.split("\n\n")
story_txid = split_story[-2].split("\n")[-1]
signed_psbt_fname = split_story[1]
with open(microsd_path(signed_psbt_fname), "r") as f:
signed_psbt = f.read().strip()
signed_txn_fname = split_story[3]
with open(microsd_path(signed_txn_fname), "r") as f:
signed_txn = f.read().strip()
press_select() # exit saved story
signed_psbt, signed_txn, story_txid = file_tx_signing_done(story)
assert signed_psbt != psbt
finalize_res = sim.finalizepsbt(signed_psbt)
bitcoind_signed_txn = finalize_res["hex"]
@ -2538,13 +2546,13 @@ def test_nsequence_blockheight_relative_locktime_ux(sequence, use_regtest, bitco
@pytest.mark.bitcoind
@pytest.mark.veryslow
@pytest.mark.parametrize("num_ins", [1, 4, 11])
@pytest.mark.parametrize("differ", [True, False])
@pytest.mark.parametrize("seconds", [512, 10000, 1000000, 33554431])
def test_nsequence_timebased_relative_locktime_ux(seconds, use_regtest, bitcoind_d_sim_watch, start_sign,
microsd_path, cap_story, goto_home, press_select,
pick_menu_item, bitcoind, end_sign, num_ins, differ):
pick_menu_item, bitcoind, end_sign, num_ins, differ,
file_tx_signing_done):
sequence = SEQUENCE_LOCKTIME_TYPE_FLAG | (seconds >> 9)
use_regtest()
sim = bitcoind_d_sim_watch
@ -2587,12 +2595,15 @@ def test_nsequence_timebased_relative_locktime_ux(seconds, use_regtest, bitcoind
psbt_fname = "rtl-time.psbt"
with open(microsd_path(psbt_fname), "w") as f:
f.write(psbt)
goto_home()
pick_menu_item('Ready To Sign')
time.sleep(0.1)
pick_menu_item(psbt_fname)
pick_menu_item("Ready To Sign")
time.sleep(0.1)
title, story = cap_story()
if 'OK TO SEND' not in title:
pick_menu_item(psbt_fname)
time.sleep(0.1)
title, story = cap_story()
assert "WARNING" not in story
assert "TX LOCKTIMES" in story
@ -2613,18 +2624,10 @@ def test_nsequence_timebased_relative_locktime_ux(seconds, use_regtest, bitcoind
press_select() # confirm signing
time.sleep(0.1)
title, story = cap_story()
assert title == 'PSBT Signed'
assert "Updated PSBT is:" in story
assert "Finalized transaction (ready for broadcast)" in story
assert "TXID" in story
split_story = story.split("\n\n")
story_txid = split_story[-2].split("\n")[-1]
signed_psbt_fname = split_story[1]
with open(microsd_path(signed_psbt_fname), "r") as f:
signed_psbt = f.read().strip()
signed_txn_fname = split_story[3]
with open(microsd_path(signed_txn_fname), "r") as f:
signed_txn = f.read().strip()
signed_psbt, signed_txn, story_txid = file_tx_signing_done(story)
assert signed_psbt != psbt
finalize_res = sim.finalizepsbt(signed_psbt)
bitcoind_signed_txn = finalize_res["hex"]
@ -2650,12 +2653,11 @@ def test_nsequence_timebased_relative_locktime_ux(seconds, use_regtest, bitcoind
@pytest.mark.bitcoind
@pytest.mark.veryslow
@pytest.mark.parametrize("abs_lock", [True, False])
@pytest.mark.parametrize("num_rtl", [(2,3),(4,7),(8,3),(6,7)])
def test_mixed_locktimes(num_rtl, use_regtest, bitcoind_d_sim_watch, start_sign,
microsd_path, cap_story, goto_home, press_select,
pick_menu_item, bitcoind, end_sign, abs_lock):
def test_mixed_locktimes(num_rtl, use_regtest, bitcoind_d_sim_watch, start_sign, microsd_path,
cap_story, goto_home, press_select, pick_menu_item, bitcoind, end_sign,
abs_lock, file_tx_signing_done):
tb, bb = num_rtl
num_ins = tb + bb
sequence = SEQUENCE_LOCKTIME_TYPE_FLAG | (512 >> 9)
@ -2703,9 +2705,11 @@ def test_mixed_locktimes(num_rtl, use_regtest, bitcoind_d_sim_watch, start_sign,
goto_home()
pick_menu_item('Ready To Sign')
time.sleep(0.1)
pick_menu_item(psbt_fname)
time.sleep(0.1)
title, story = cap_story()
if 'OK TO SEND' not in title:
pick_menu_item(psbt_fname)
time.sleep(0.1)
title, story = cap_story()
assert "WARNING" not in story
assert "TX LOCKTIMES" in story
@ -2726,18 +2730,10 @@ def test_mixed_locktimes(num_rtl, use_regtest, bitcoind_d_sim_watch, start_sign,
press_select() # confirm signing
time.sleep(0.1)
title, story = cap_story()
assert title == 'PSBT Signed'
assert "Updated PSBT is:" in story
assert "Finalized transaction (ready for broadcast)" in story
assert "TXID" in story
split_story = story.split("\n\n")
story_txid = split_story[-2].split("\n")[-1]
signed_psbt_fname = split_story[1]
with open(microsd_path(signed_psbt_fname), "r") as f:
signed_psbt = f.read().strip()
signed_txn_fname = split_story[3]
with open(microsd_path(signed_txn_fname), "r") as f:
signed_txn = f.read().strip()
signed_psbt, signed_txn, story_txid = file_tx_signing_done(story)
assert signed_psbt != psbt
finalize_res = sim.finalizepsbt(signed_psbt)
bitcoind_signed_txn = finalize_res["hex"]
@ -2791,55 +2787,55 @@ def random_nLockTime_test_cases(num=10):
])
def test_timelocks_visualize(start_sign, end_sign, dev, bitcoind, use_regtest,
bitcoind_d_sim_watch, nLockTime):
# - works on simulator and connected USB real-device
nLockTime, expect_ux = nLockTime
num_ins = 10
use_regtest()
bitcoind_d_sim_watch.keypoolrefill(20)
for i in range(num_ins):
addr = bitcoind_d_sim_watch.getnewaddress()
bitcoind.supply_wallet.sendtoaddress(addr, 1)
# - works on simulator and connected USB real-device
nLockTime, expect_ux = nLockTime
num_ins = 10
use_regtest()
bitcoind_d_sim_watch.keypoolrefill(20)
for i in range(num_ins):
addr = bitcoind_d_sim_watch.getnewaddress()
bitcoind.supply_wallet.sendtoaddress(addr, 1)
bitcoind.supply_wallet.generatetoaddress(1, bitcoind.supply_wallet.getnewaddress())
dest_addr = bitcoind_d_sim_watch.getnewaddress() # self-spend
utxos = bitcoind_d_sim_watch.listunspent()
assert len(utxos) == num_ins
bitcoind.supply_wallet.generatetoaddress(1, bitcoind.supply_wallet.getnewaddress())
dest_addr = bitcoind_d_sim_watch.getnewaddress() # self-spend
utxos = bitcoind_d_sim_watch.listunspent()
assert len(utxos) == num_ins
ins = []
for i, utxo in enumerate(utxos):
if i % 2 == 0:
nSeq = (SEQUENCE_LOCKTIME_TYPE_FLAG | i)
else:
confirmations = utxo["confirmations"]
nSeq = confirmations + (20*i)
ins = []
for i, utxo in enumerate(utxos):
if i % 2 == 0:
nSeq = (SEQUENCE_LOCKTIME_TYPE_FLAG | i)
else:
confirmations = utxo["confirmations"]
nSeq = confirmations + (20*i)
inp = {
"txid": utxo["txid"],
"vout": utxo["vout"],
"sequence": nSeq,
}
ins.append(inp)
inp = {
"txid": utxo["txid"],
"vout": utxo["vout"],
"sequence": nSeq,
}
ins.append(inp)
psbt_resp = bitcoind_d_sim_watch.walletcreatefundedpsbt(
ins, [{dest_addr: (num_ins - 0.1)}],
nLockTime, {"fee_rate": 20}
)
psbt = base64.b64decode(psbt_resp.get("psbt"))
psbt_resp = bitcoind_d_sim_watch.walletcreatefundedpsbt(
ins, [{dest_addr: (num_ins - 0.1)}],
nLockTime, {"fee_rate": 20}
)
psbt = base64.b64decode(psbt_resp.get("psbt"))
open('debug/locktimes.psbt', 'wb').write(psbt)
open('debug/locktimes.psbt', 'wb').write(psbt)
# should be able to sign, but get warning
# should be able to sign, but get warning
# use new feature to have Coldcard return the 'visualization' of transaction
start_sign(psbt, False, stxn_flags=STXN_VISUALIZE)
story = end_sign(accept=None, expect_txn=False)
# use new feature to have Coldcard return the 'visualization' of transaction
start_sign(psbt, False, stxn_flags=STXN_VISUALIZE)
story = end_sign(accept=None, expect_txn=False)
story = story.decode('ascii')
assert datetime.datetime.utcfromtimestamp(nLockTime).strftime("%Y-%m-%d %H:%M:%S") == expect_ux
assert f"Abs Locktime: This tx can only be spent after {expect_ux} UTC (MTP)" in story
assert "Block height RTL: 5 inputs have relative block height timelock" in story
# when i=0 in loop time based RTL is zero
assert "Time-based RTL: 4 inputs have relative time-based timelock" in story
story = story.decode('ascii')
assert datetime.datetime.utcfromtimestamp(nLockTime).strftime("%Y-%m-%d %H:%M:%S") == expect_ux
assert f"Abs Locktime: This tx can only be spent after {expect_ux} UTC (MTP)" in story
assert "Block height RTL: 5 inputs have relative block height timelock" in story
# when i=0 in loop time based RTL is zero
assert "Time-based RTL: 4 inputs have relative time-based timelock" in story
@pytest.mark.parametrize('in_out', [(4,1),(2,2),(2,1)])
@ -2947,7 +2943,6 @@ def test_sorting_outputs_by_size(fake_txn, start_sign, cap_story, use_testnet,
press_cancel()
@pytest.mark.parametrize("psbtv2", [True, False])
@pytest.mark.parametrize("chain", ["BTC", "XTN"])
@pytest.mark.parametrize("data", [
# (out_style, amount, is_change)
@ -2956,8 +2951,9 @@ def test_sorting_outputs_by_size(fake_txn, start_sign, cap_story, use_testnet,
[("p2pkh", 1000000, 1)] * 11 + [("p2wpkh", 50000000, 0)] * 16,
[("p2pkh", 1000000, 1), ("p2wpkh", 50000000, 0), ("p2wpkh-p2sh", 800000, 1)] * 11,
])
def test_txout_explorer(psbtv2, chain, data, fake_txn, start_sign,
settings_set, txout_explorer, cap_story):
def test_txout_explorer(chain, data, fake_txn, start_sign, settings_set, txout_explorer,
cap_story, pytestconfig):
# TODO This test MUST be run with --psbt2 flag on and off
settings_set("chain", chain)
outstyles = []
outvals = []
@ -2972,7 +2968,7 @@ def test_txout_explorer(psbtv2, chain, data, fake_txn, start_sign,
inp_amount = sum(outvals) + 100000 # 100k sat fee
psbt = fake_txn(1, len(data), segwit_in=True, outstyles=outstyles,
outvals=outvals, change_outputs=change_outputs,
psbt_v2=psbtv2, input_amount=inp_amount)
psbt_v2=pytestconfig.getoption('psbt2'), input_amount=inp_amount)
start_sign(psbt)
txout_explorer(data, chain)

View File

@ -82,7 +82,7 @@ def grab_payload(press_select, need_keypress, press_cancel, nfc_read_url, cap_s
url = nfc_read_url().replace('%24', '$')
assert url.startswith('https://keyteleport.com#')
assert url.startswith('https://keyteleport.com/#')
nfc_data = url.rsplit('#')[1]
assert nfc_data.startswith(f'B$2{tt_code}0100')
@ -401,6 +401,7 @@ def test_tx_wrong_pub(rx_start, tx_start, cap_menu, enter_complex, pick_menu_ite
# now, send that back
rx_complete(data, pw, expect_fail=True)
time.sleep(.1)
title, body = cap_story()
assert title == 'Teleport Fail'
@ -416,11 +417,11 @@ def test_tx_wrong_pub(rx_start, tx_start, cap_menu, enter_complex, pick_menu_ite
@pytest.mark.parametrize('incl_xpubs', [ False ])
def test_teleport_ms_sign(M, use_regtest, make_myself_wallet, segwit, num_ins, dev, clear_ms,
fake_ms_txn, try_sign, incl_xpubs, bitcoind, cap_story, need_keypress,
cap_menu, pick_menu_item, grab_payload, rx_complete, press_select, ndef_parse_txn_psbt,
press_nfc, nfc_read, settings_get, settings_set):
cap_menu, pick_menu_item, grab_payload, rx_complete, press_select,
ndef_parse_txn_psbt, press_nfc, nfc_read, settings_get, settings_set,
txid_from_export_prompt):
# IMPORTANT: won't work if you start simulator with --ms flag. Use no args
all_out_styles = list(unmap_addr_fmt.keys())
num_outs = len(all_out_styles)
@ -440,13 +441,15 @@ def test_teleport_ms_sign(M, use_regtest, make_myself_wallet, segwit, num_ins, d
cur_wallet = 0
my_xfp = select_wallet(cur_wallet)
_, updated = try_sign(psbt, accept_ms_import=incl_xpubs)
_, updated = try_sign(psbt, accept_ms_import=incl_xpubs, exit_export_loop=False)
open(f'debug/myself-after-1.psbt', 'wb').write(updated)
assert updated != psbt
title, body = cap_story()
assert title == 'Teleport PSBT?'
assert 'Press (T)' in body
assert title == "PSBT Signed"
assert '(T) to use Key Teleport to send PSBT to other co-signers' in body
num_sigs_needed = M - 1 # we have already signed with first at this point
while 1:
# expect: a menu of other signers to pick from
@ -483,6 +486,13 @@ def test_teleport_ms_sign(M, use_regtest, make_myself_wallet, segwit, num_ins, d
nn = xfp2str(next_xfp)
open(f'debug/next_qr_{nn}.txt', 'wt').write(f'{nn}\n\n{pw}\n\n{data}')
time.sleep(.1)
title, story = cap_story()
assert title == 'Sent by Teleport'
s, aux = ("", "is") if num_sigs_needed == 1 else ("s", "are")
msg = "%d more signature%s %s still required." % (num_sigs_needed, s, aux)
assert msg in story
# switch personalities, and try to read that QR
new_xfp = select_wallet(idx)
assert new_xfp == next_xfp
@ -499,14 +509,14 @@ def test_teleport_ms_sign(M, use_regtest, make_myself_wallet, segwit, num_ins, d
time.sleep(.25)
title, body = cap_story()
if title != 'Teleport PSBT?':
if 'Finalized TX' in body:
break
assert title == 'Teleport PSBT?'
assert 'more signatures' in body
assert '(T) to use Key Teleport to send PSBT to other co-signers' in body
num_sigs_needed -= 1
assert title == 'Final TXID'
txid = body.split()[0]
txid = txid_from_export_prompt()
press_select() # exit QR
# share signed txn via low-level NFC
press_nfc()
@ -519,11 +529,11 @@ def test_teleport_ms_sign(M, use_regtest, make_myself_wallet, segwit, num_ins, d
assert got_txn
def test_teleport_big_ms(make_myself_wallet, clear_ms,
fake_ms_txn, try_sign, cap_story, need_keypress,
cap_menu, pick_menu_item, grab_payload, rx_complete, press_select, ndef_parse_txn_psbt,
set_master_key,
goto_home, press_nfc, nfc_read, settings_get, settings_set, open_microsd, import_ms_wallet):
def test_teleport_big_ms(make_myself_wallet, clear_ms, fake_ms_txn, try_sign, cap_story,
need_keypress, cap_menu, pick_menu_item, grab_payload, rx_complete,
press_select, ndef_parse_txn_psbt, set_master_key, goto_home, press_nfc,
nfc_read, settings_get, settings_set, open_microsd, import_ms_wallet,
press_cancel):
# define lots of wallets and do teleport from SD disk
@ -567,8 +577,8 @@ def test_teleport_big_ms(make_myself_wallet, clear_ms,
# have 1 sigs now, need one more via teleport
title, body = cap_story()
assert title == 'Teleport PSBT?'
need_keypress('t')
assert '(T) to use Key Teleport to send PSBT to other co-signers' in body
need_keypress('t')
# pick another one randomly
m = cap_menu()
@ -600,8 +610,8 @@ def test_teleport_big_ms(make_myself_wallet, clear_ms,
time.sleep(.25)
title, body = cap_story()
assert title == 'Final TXID'
assert 'Finalized TX' in body
press_cancel()
@pytest.mark.manual

View File

@ -94,7 +94,6 @@ def try_sign_virtdisk(press_select, virtdisk_path, cap_story, virtdisk_wipe, pre
# wait for it to finish signing
time.sleep(.1)
title, story = cap_story()
assert "PSBT Signed" in title
split_story = story.split("\n\n")
result_fn = split_story[1]
@ -103,11 +102,7 @@ def try_sign_virtdisk(press_select, virtdisk_path, cap_story, virtdisk_wipe, pre
if expect_finalize:
result_txn = split_story[3]
result_txid = split_story[4].split("\n")[-1]
reexport_msg = split_story[5]
else:
reexport_msg = split_story[2]
assert "Press (0) to save again by another method." == reexport_msg
got_psbt = None
got_txn = None
txid, got_txid = None, None