What changed, and why it matters
This commit is a user-interface refactor for importing multisignature wallet setups on the COLDCARD. It replaces three separate menu items ('Import from File', 'Import from QR', 'Import via NFC') with a single 'Import' option that then asks the user to choose the source. The underlying import logic is essentially unchanged, and there is no indication this fixes a security bug or introduces a vulnerability.
No security action required. Treat as a normal UX refactor; review for functional correctness during regular QA.
Security signals we found
No strong security signals were identified.
Evidence from the diff
The change unifies multisig import entry points in shared/multisig.py. The menu now offers one ‘Import’ item calling import_multisig(), which uses a new import_export_prompt() helper to let the user pick SD card, virtual disk, QR, or NFC. QR and NFC remain accessible as shortcut keys via ShortcutItem. The import code still reads a .txt/.json file and passes it to maybe_enroll_xpub(). Test fixtures are updated to match the new menu flow, and a minor test log-parsing fix (‘TXID’ -> ‘TXID:’) is included.
Changed components
shared/multisig.pytesting/conftest.pytesting/test_multisig.pyInspect captured patch +39 / −45
diff --git a/shared/multisig.py b/shared/multisig.py
index 3d56d1b..1934e76 100644
--- a/shared/multisig.py
+++ b/shared/multisig.py
@@ -10,7 +10,7 @@ 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
-from menu import MenuSystem, MenuItem, NonDefaultMenuItem, start_chooser, ToggleMenuItem
+from menu import MenuSystem, MenuItem, NonDefaultMenuItem, start_chooser, ToggleMenuItem, ShortcutItem
from opcodes import OP_CHECKMULTISIG
from exceptions import FatalPSBTIssue
from glob import settings
@@ -1380,7 +1380,7 @@ class MultisigMenu(MenuSystem):
@classmethod
def construct(cls):
# Dynamic menu with user-defined names of wallets shown
- from glob import NFC
+ from flow import nfc_enabled
if not MultisigWallet.exists():
rv = [MenuItem('(none setup yet)', f=no_ms_yet)]
@@ -1390,11 +1390,7 @@ class MultisigMenu(MenuSystem):
rv.append(MenuItem('%d/%d: %s' % (ms.M, ms.N, ms.name),
menu=make_ms_wallet_menu, arg=ms.storage_idx))
- 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), shortcut=KEY_NFC))
+ rv.append(MenuItem('Import', f=import_multisig))
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))
@@ -1408,6 +1404,9 @@ class MultisigMenu(MenuSystem):
rv.append(NonDefaultMenuItem(
'Unsorted Multisig?' if version.has_qwerty else 'Unsorted Multi?',
'unsort_ms', f=unsorted_ms_menu))
+
+ rv.append(ShortcutItem(KEY_NFC, predicate=nfc_enabled, f=import_multisig_nfc))
+ rv.append(ShortcutItem(KEY_QR, predicate=version.has_qwerty, f=import_multisig_qr))
return rv
def update_contents(self):
@@ -1913,26 +1912,19 @@ async def import_multisig_qr(*a):
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
- from glob import VD
-
- force_vdisk = False
- if VD:
- prompt = "Press (1) to import multisig wallet file from SD Card"
- escape = "1"
- if VD is not None:
- prompt += ", press (2) to import from Virtual Disk"
- escape += "2"
- prompt += "."
- ch = await ux_show_story(prompt, escape=escape)
- if ch == "1":
- force_vdisk=False
- elif ch == "2":
- force_vdisk = True
- else:
- return
+ from ux import import_export_prompt
+
+ ch = await import_export_prompt("multisig wallet file", is_import=True)
+ if isinstance(ch, str):
+ if ch == KEY_QR:
+ await import_multisig_qr()
+ elif ch == KEY_NFC:
+ await import_multisig_nfc()
+ return
def possible(filename):
with open(filename, 'rt') as fd:
@@ -1944,11 +1936,11 @@ async def import_multisig(*a):
return True
fn = await file_picker(suffix=['.txt', '.json'], min_size=100, max_size=20*200,
- taster=possible, force_vdisk=force_vdisk)
+ taster=possible, **ch)
if not fn: return
try:
- with CardSlot(force_vdisk=force_vdisk) as card:
+ with CardSlot(**ch) as card:
with open(fn, 'rt') as fp:
data = fp.read()
except CardMissingError:
@@ -1959,8 +1951,8 @@ async def import_multisig(*a):
try:
possible_name = (fn.split('/')[-1].split('.'))[0]
maybe_enroll_xpub(config=data, name=possible_name)
- except Exception as e:
- #import sys; sys.print_exception(e)
- await ux_show_story('Failed to import.\n\n%s\n%s' % (e, problem_file_line(e)))
+ except BaseException as e:
+ # import sys; sys.print_exception(e)
+ await ux_show_story('Failed to import multisig.\n\n%s\n%s' % (e, problem_file_line(e)))
# EOF
diff --git a/testing/conftest.py b/testing/conftest.py
index 651b062..3eae33f 100644
--- a/testing/conftest.py
+++ b/testing/conftest.py
@@ -1958,7 +1958,7 @@ def file_tx_signing_done(virtdisk_path, microsd_path):
txid = None
for l in _split:
- if "TXID" in l:
+ if "TXID:" in l:
txid = l.split("\n")[-1].strip()
assert len(txid) == 64, "wrong txid"
break
diff --git a/testing/test_multisig.py b/testing/test_multisig.py
index 8cafd87..49d87e3 100644
--- a/testing/test_multisig.py
+++ b/testing/test_multisig.py
@@ -161,7 +161,7 @@ def offer_ms_import(cap_story, dev, sim_root_dir):
return doit
@pytest.fixture
-def import_multisig(request, is_q1, need_keypress, offer_ms_import):
+def import_multisig(request, is_q1, need_keypress, offer_ms_import, press_nfc):
def doit(fname=None, way="sd", data=None, name=None):
assert fname or data
if fname:
@@ -192,13 +192,15 @@ def import_multisig(request, is_q1, need_keypress, offer_ms_import):
pick_menu_item("Multisig Wallets")
time.sleep(.1)
- ms_menu = cap_menu()
+ pick_menu_item("Import")
+ time.sleep(.1)
+ title, story = cap_story()
if way == "qr":
- if "Import from QR" not in ms_menu and not is_q1:
+ if "to scan QR code" not in story and not is_q1:
pytest.skip("No QR support")
scan_a_qr = request.getfixturevalue('scan_a_qr')
- pick_menu_item("Import from QR")
+ need_keypress(KEY_QR)
actual_vers, parts = split_qrs(config, 'U', max_version=20)
random.shuffle(parts)
@@ -208,11 +210,11 @@ def import_multisig(request, is_q1, need_keypress, offer_ms_import):
time.sleep(2.0 / len(parts))
elif way == "nfc":
- if "Import via NFC" not in ms_menu:
+ if "import via NFC" not in story:
pytest.skip("NFC disabled")
nfc_write_text = request.getfixturevalue('nfc_write_text')
- pick_menu_item("Import via NFC")
+ press_nfc()
nfc_write_text(config)
time.sleep(0.5)
@@ -228,9 +230,6 @@ def import_multisig(request, is_q1, need_keypress, offer_ms_import):
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")
@@ -862,7 +861,7 @@ def test_import_ux(N, vdisk, goto_home, cap_story, pick_menu_item,
goto_home()
pick_menu_item('Settings')
pick_menu_item('Multisig Wallets')
- pick_menu_item('Import from File')
+ pick_menu_item('Import')
time.sleep(0.5)
_, story = cap_story()
if vdisk:
@@ -1735,7 +1734,7 @@ def test_make_airgapped(addr_fmt, acct_num, M_N, goto_home, cap_story, pick_menu
goto_home()
pick_menu_item('Settings')
pick_menu_item('Multisig Wallets')
- pick_menu_item('Import from File')
+ pick_menu_item('Import')
time.sleep(0.5)
_, story = cap_story()
if "Press (1) to import multisig wallet file from SD Card" in story:
@@ -2573,7 +2572,7 @@ def test_legacy_multisig_witness_utxo_in_psbt(bitcoind, use_regtest, clear_ms, m
goto_home()
pick_menu_item('Settings')
pick_menu_item('Multisig Wallets')
- pick_menu_item('Import from File')
+ pick_menu_item('Import')
time.sleep(0.3)
_, story = cap_story()
if "Press (1) to import multisig wallet file from SD Card" in story:
@@ -3065,7 +3064,7 @@ def test_exotic_descriptors(desc, clear_ms, goto_home, need_keypress, pick_menu_
goto_home()
pick_menu_item('Settings')
pick_menu_item('Multisig Wallets')
- pick_menu_item('Import from File')
+ pick_menu_item('Import')
time.sleep(0.1)
_, story = cap_story()
if "Press (1) to import multisig wallet file from SD Card" in story:
@@ -3336,7 +3335,8 @@ def test_scan_any_qr(fpath, is_q1, scan_a_qr, clear_ms, goto_home,
@pytest.mark.parametrize("N", [3, 15])
def test_bare_cc_ms_qr_import(N, make_multisig, scan_a_qr, clear_ms, goto_home,
- pick_menu_item, cap_story, press_cancel, is_q1):
+ pick_menu_item, cap_story, press_cancel, is_q1,
+ need_keypress):
# bare:
# - no fingerprints
# - no xfps
@@ -3368,7 +3368,9 @@ def test_bare_cc_ms_qr_import(N, make_multisig, scan_a_qr, clear_ms, goto_home,
# multisig import path needs to be used
pick_menu_item("Settings")
pick_menu_item("Multisig Wallets")
- pick_menu_item("Import from QR")
+ pick_menu_item("Import")
+ need_keypress(KEY_QR)
+
for p in parts:
scan_a_qr(p)
time.sleep(2.0 / len(parts))
Why this scored 15/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.