import multisig from QR/BBQr

This commit is contained in:
scgbckbone 2024-04-04 19:07:38 +02:00 committed by doc-hex
parent f07665c8f3
commit 5ab96643b4
3 changed files with 106 additions and 14 deletions

View File

@ -36,6 +36,7 @@
seed without master secret.
- Bugfix: Battery idle timeout also considers last progress bar update
- Enhancement: Allow export of multisig XPUBs via BBQr
- Enhancement: Import multisig via QR/BBQr - both legacy COLDCARD export and descriptors supported
## 1.1.0Q - 2024-04-02

View File

@ -10,7 +10,7 @@ from ux import import_export_prompt, ux_enter_bip32_index, show_qr_code
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
from menu import MenuSystem, MenuItem
from menu import MenuSystem, MenuItem, ShortcutItem
from opcodes import OP_CHECKMULTISIG
from exceptions import FatalPSBTIssue
from glob import settings
@ -875,7 +875,8 @@ class MultisigWallet(WalletABC):
hdr = '%s %s' % (mode, my_xfp)
label = "%s multisig setup" % name
choice = await import_export_prompt("%s file" % label, is_import=False)
choice = await import_export_prompt("%s file" % label, is_import=False,
no_qr=not version.has_qwerty)
if choice == KEY_CANCEL:
return
elif choice in (KEY_NFC, KEY_QR):
@ -1259,8 +1260,10 @@ class MultisigMenu(MenuSystem):
menu=make_ms_wallet_menu, arg=ms.storage_idx))
from glob import NFC
rv.append(MenuItem('Import from File', f=import_multisig))
rv.append(MenuItem('Import from QR', f=import_multisig_qr,
predicate=version.has_qwerty, shortcut=KEY_QR))
rv.append(MenuItem('Import via NFC', f=import_multisig_nfc,
predicate=bool(NFC)))
predicate=bool(NFC), shortcut=KEY_NFC))
rv.append(MenuItem('Export XPUB', f=export_multisig_xpubs))
rv.append(MenuItem('Create Airgapped', f=create_ms_step1))
rv.append(MenuItem('Trust PSBT?', f=trust_psbt_menu))
@ -1309,8 +1312,10 @@ async def make_ms_wallet_descriptor_menu(menu, label, item):
rv = [
MenuItem('View Descriptor', f=ms_wallet_show_descriptor, arg=ms),
MenuItem('Export', f=ms_wallet_ckcc_export, arg=(ms, {"descriptor": True, "desc_pretty": False})),
MenuItem('Bitcoin Core', f=ms_wallet_ckcc_export, arg=(ms, {"descriptor": True, "core": True})),
MenuItem('Export', f=ms_wallet_ckcc_export,
arg=(ms, {"descriptor": True, "desc_pretty": False})),
MenuItem('Bitcoin Core', f=ms_wallet_ckcc_export,
arg=(ms, {"descriptor": True, "core": True})),
]
return rv
@ -1660,6 +1665,19 @@ async def import_multisig_nfc(*a):
except Exception as e:
await ux_show_story(title="ERROR", msg="Failed to import multisig. %s" % str(e))
async def import_multisig_qr(*a):
from auth import maybe_enroll_xpub
from ux_q1 import QRScannerInteraction
data = await QRScannerInteraction().scan_text('Scan Multisig from a QR code')
if not data:
# pressed CANCEL
return
try:
maybe_enroll_xpub(config=data)
except Exception as e:
await ux_show_story('Failed to import.\n\n%s\n%s' % (e, problem_file_line(e)))
async def import_multisig(*a):
# pick text file from SD card, import as multisig setup file
from actions import file_picker

View File

@ -22,6 +22,7 @@ from pycoin.key.BIP32Node import BIP32Node
from pycoin.tx import Tx
from io import BytesIO
from hashlib import sha256
from bbqr import split_qrs
def HARD(n=0):
@ -161,11 +162,12 @@ def offer_ms_import(cap_story, dev):
return doit
@pytest.fixture
def import_ms_wallet(dev, make_multisig, offer_ms_import, press_select, is_q1):
def import_ms_wallet(dev, make_multisig, offer_ms_import, press_select,
is_q1, request, need_keypress):
def doit(M, N, addr_fmt=None, name=None, unique=0, accept=False, common=None,
keys=None, do_import=True, derivs=None, descriptor=False,
int_ext_desc=False, dev_key=False):
int_ext_desc=False, dev_key=False, way=None):
keys = keys or make_multisig(M, N, unique=unique, dev_key=dev_key,
deriv=common or (derivs[0] if derivs else None))
name = name or f'test-{M}-{N}'
@ -211,7 +213,73 @@ def import_ms_wallet(dev, make_multisig, offer_ms_import, press_select, is_q1):
#print(config)
open('debug/last-ms.txt', 'wt').write(config)
title, story = offer_ms_import(config)
if way is None: # USB
title, story = offer_ms_import(config)
else:
# only get those simulator related fixtures here, to be able to
# use this with real HW
cap_menu = request.getfixturevalue('cap_menu')
cap_story = request.getfixturevalue('cap_story')
goto_home = request.getfixturevalue('goto_home')
pick_menu_item = request.getfixturevalue('pick_menu_item')
if "Skip Checks?" not in cap_menu():
# we are not in multisig menu
goto_home()
pick_menu_item("Settings")
pick_menu_item("Multisig Wallets")
time.sleep(.1)
ms_menu = cap_menu()
if way == "qr":
if "Import from QR" not in ms_menu and not is_q1:
pytest.skip("No QR support")
scan_a_qr = request.getfixturevalue('scan_a_qr')
pick_menu_item("Import from QR")
actual_vers, parts = split_qrs(config, 'U', max_version=20)
random.shuffle(parts)
for p in parts:
scan_a_qr(p)
time.sleep(2.0 / len(parts))
elif way == "nfc":
if "Import via NFC" not in ms_menu:
pytest.skip("NFC disabled")
nfc_write_text = request.getfixturevalue('nfc_write_text')
pick_menu_item("Import via NFC")
nfc_write_text(config)
time.sleep(0.5)
else:
assert way in ("sd", "vdisk")
if way == "sd":
path_f = request.getfixturevalue('microsd_path')
else:
path_f = request.getfixturevalue('virtdisk_path')
fname = name + ".txt"
with open(path_f(fname), "w") as f:
f.write(config)
pick_menu_item("Import from File")
time.sleep(.1)
_, story = cap_story()
if way == "vdisk":
if "(2) to import from Virtual Disk" not in story:
pytest.skip("VDisk disabled")
need_keypress("2")
else:
if "Press (1)" in story:
need_keypress("1")
pick_menu_item(fname)
time.sleep(.2)
title, story = cap_story()
assert 'Create new multisig' in story \
or 'Update existing multisig wallet' in story \
@ -614,7 +682,7 @@ def test_import_detail(clear_ms, import_ms_wallet, need_keypress,
press_cancel()
@pytest.mark.parametrize("way", ["sd", "vdisk", "nfc"])
@pytest.mark.parametrize("way", ["qr", "sd", "vdisk", "nfc"])
@pytest.mark.parametrize('acct_num', [0, 99, 123])
@pytest.mark.parametrize('testnet', [True, False])
def test_export_airgap(acct_num, goto_home, cap_story, pick_menu_item, cap_menu,
@ -740,7 +808,7 @@ def test_import_ux(N, vdisk, goto_home, cap_story, pick_menu_item,
try: os.unlink(fname)
except: pass
@pytest.mark.parametrize("way", ["sd", "vdisk", "nfc"])
@pytest.mark.parametrize("way", [None, "sd", "vdisk", "nfc", "qr"])
@pytest.mark.parametrize('addr_fmt', ['p2sh-p2wsh', 'p2sh', 'p2wsh' ])
@pytest.mark.parametrize('comm_prefix', ['m/1/2/3/4/5/6/7/8/9/10/11/12', None, "m/45h"])
def test_export_single_ux(goto_home, comm_prefix, cap_story, pick_menu_item, cap_menu, press_select,
@ -752,8 +820,9 @@ def test_export_single_ux(goto_home, comm_prefix, cap_story, pick_menu_item, cap
clear_ms()
name = 'ex-test-%d' % random.randint(10000,99999)
M,N = 3, 15
keys = import_ms_wallet(M, N, name=name, addr_fmt=addr_fmt, accept=1, common=comm_prefix)
M,N = 3, 5
keys = import_ms_wallet(M, N, name=name, addr_fmt=addr_fmt, accept=1,
common=comm_prefix, way=way)
goto_home()
pick_menu_item('Settings')
@ -764,7 +833,10 @@ def test_export_single_ux(goto_home, comm_prefix, cap_story, pick_menu_item, cap
pick_menu_item(item)
pick_menu_item('Coldcard Export')
contents = load_export(way, label="Coldcard multisig setup", is_json=False, sig_check=False)
contents = load_export(way or "sd", label="Coldcard multisig setup", is_json=False, sig_check=False)
if way == "qr":
# QR code still displayed on screen
press_select()
got = set()
for ln in io.StringIO(contents).readlines():
@ -902,7 +974,8 @@ def test_import_dup_safe(N, clear_ms, make_multisig, offer_ms_import,
menu = cap_menu()
assert f'{M}/{N}: {name}' in menu
assert (len(menu) - num_wallets) in [5, 6] # depending if NFC enabled or not
# depending if NFC enabled or not, and if Q (has QR)
assert (len(menu) - num_wallets) in [5, 6, 7]
title, story = offer_ms_import(make_named('xxx-orig'))
assert 'Create new multisig wallet' in story