diff --git a/releases/Next-ChangeLog.md b/releases/Next-ChangeLog.md index 5c9d91ba..42f2f435 100644 --- a/releases/Next-ChangeLog.md +++ b/releases/Next-ChangeLog.md @@ -6,6 +6,7 @@ This lists the new changes that have not yet been published in a normal release. - Enhancement: Hide Secure Notes & Passwords in Deltamode. Wipe seed if notes menu accessed. - Enhancement: Hide Seed Vault in Deltamode. Wipe seed if Seed Vault menu accessed. +- Enhancement: Ability to switch between BIP-32 XPUB and SLIP-132 garbage in `Export XPUB` - Bugfix: Sometimes see a struck screen after _Verifying..._ in boot up sequence. On Q, result is blank screen, on Mk4, result is three-dots screen. - Bugfix: Do not allow to enable/disable Seed Vault feature when in temporary seed mode @@ -16,6 +17,7 @@ This lists the new changes that have not yet been published in a normal release. - Change: Testnet3 -> Testnet4 (all parameters are the same) + # Mk4 Specific Changes ## 5.4.1 - 2024-??-?? diff --git a/shared/actions.py b/shared/actions.py index 3fdb3999..27774dcb 100644 --- a/shared/actions.py +++ b/shared/actions.py @@ -1018,6 +1018,7 @@ async def export_xpub(label, _2, item): chain = chains.current_chain() acct = 0 + slip132 = False # non-slip is default from Oct 2024 # decode menu code => standard derivation mode = item.arg @@ -1033,24 +1034,44 @@ async def export_xpub(label, _2, item): else: remap = {44:0, 49:1, 84:2,86:3}[mode] _, path, addr_fmt = chains.CommonDerivations[remap] - path = path.format(account='{acct}', coin_type=chain.b44_cointype, change=0, idx=0)[:-4] - - # always show SLIP-132 style, because defacto - show_slip132 = (addr_fmt != AF_CLASSIC) + path = path.format(account=acct, coin_type=chain.b44_cointype, + change=0, idx=0)[:-4] while 1: - msg = '''Show QR of the XPUB for path:\n\n%s\n\n''' % path + msg = 'Show QR of the XPUB for path:\n\n%s\n\n' % path + esc = "" + if path != "m": + esc += "1" + msg += "Press (1) to select account other than %s. " % (acct or "zero") + if addr_fmt != AF_CLASSIC: + esc += "2" + slp_af = addr_fmt + if slip132: + slp_af = AF_CLASSIC - if '{acct}' in path: - msg += "Press (1) to select account other than zero. " + slp = chain.slip132[slp_af].hint + "pub" + msg += " Press (2) to show %s %s." % ( + slp, "(BIP-32)" if slip132 else "(SLIP-132)" + ) if glob.NFC: - msg += "Press %s to share via NFC. " % (KEY_NFC if version.has_qwerty else "(3)") + if version.has_qwerty: + esc += KEY_NFC + key_hint = KEY_NFC + else: + esc += "3" + key_hint = "(3)" + msg += " Press %s to share via NFC. " % key_hint - ch = await ux_show_story(msg, escape='13') + ch = await ux_show_story(msg, escape=esc) if ch == 'x': return + if ch == "2": + slip132 = not slip132 + continue if ch == '1': acct = await ux_enter_bip32_index('Account Number:') or 0 - path = path.format(acct=acct) + pth_split = path.split("/") + pth_split[-1] = ("%dh" % acct) + path = "/".join(pth_split) continue # assume zero account if not picked @@ -1062,7 +1083,7 @@ async def export_xpub(label, _2, item): # render xpub/ypub/zpub with stash.SensitiveValues() as sv: node = sv.derive_path(path) if path != 'm' else sv.node - xpub = chain.serialize_public(node, addr_fmt) + xpub = chain.serialize_public(node, addr_fmt if slip132 else AF_CLASSIC) from ownership import OWNERSHIP OWNERSHIP.note_wallet_used(addr_fmt, acct) @@ -1072,8 +1093,6 @@ async def export_xpub(label, _2, item): else: await show_qr_code(xpub, False) - break - def electrum_export_story(background=False): # saves memory being in a function diff --git a/shared/flow.py b/shared/flow.py index 9e2dcd6e..f7654fd7 100644 --- a/shared/flow.py +++ b/shared/flow.py @@ -180,7 +180,7 @@ XpubExportMenu = [ MenuItem("Segwit (BIP-84)", f=export_xpub, arg=84), MenuItem("Classic (BIP-44)", f=export_xpub, arg=44), MenuItem("Taproot/P2TR(86)", f=export_xpub, arg=86), - MenuItem("P2WPKH/P2SH (49)", f=export_xpub, arg=49), + MenuItem("P2WPKH/P2SH "+("(BIP-49)"if version.has_qwerty else "(49)"), f=export_xpub, arg=49), MenuItem("Master XPUB", f=export_xpub, arg=0), MenuItem("Current XFP", f=export_xpub, arg=-1), ] diff --git a/testing/test_export.py b/testing/test_export.py index 15d801e0..6e8e37f9 100644 --- a/testing/test_export.py +++ b/testing/test_export.py @@ -547,15 +547,15 @@ def test_export_public_txt(way, dev, pick_menu_item, goto_home, press_select, mi @pytest.mark.qrcode +@pytest.mark.parametrize('chain', ["BTC", "XTN"]) @pytest.mark.parametrize('acct_num', [ None, 0, 99, 8989]) -@pytest.mark.parametrize('use_nfc', [False, True]) -def test_export_xpub(use_nfc, acct_num, dev, cap_menu, pick_menu_item, goto_home, +def test_export_xpub(chain, acct_num, dev, cap_menu, pick_menu_item, goto_home, cap_story, need_keypress, enter_number, cap_screen_qr, - use_mainnet, nfc_read_text, is_q1, press_select, press_cancel, + settings_set, nfc_read_text, is_q1, press_select, press_cancel, press_nfc, expect_acctnum_captured): # XPUB's via QR - use_mainnet() - + settings_set("chain", chain) + chain_num = 0 if chain == "BTC" else 1 goto_home() pick_menu_item('Advanced/Tools') pick_menu_item('Export Wallet') @@ -565,13 +565,13 @@ def test_export_xpub(use_nfc, acct_num, dev, cap_menu, pick_menu_item, goto_home for m in top_items: is_xfp = False if '-84' in m: - expect = "m/84h/0h/{acct}h" - elif '86' in m and 'P2TR' in m: - expect = "m/86h/0h/{acct}h" + expect = f"m/84h/{chain_num}h/{{acct}}h" + elif '-86' in m: + expect = f"m/86h/{chain_num}h/{{acct}}h" elif '-44' in m: - expect = "m/44h/0h/{acct}h" + expect = f"m/44h/{chain_num}h/{{acct}}h" elif '49' in m: - expect = "m/49h/0h/{acct}h" + expect = f"m/49h/{chain_num}h/{{acct}}h" elif 'Master' in m: expect = "m" elif 'XFP' in m: @@ -581,17 +581,21 @@ def test_export_xpub(use_nfc, acct_num, dev, cap_menu, pick_menu_item, goto_home time.sleep(0.3) if is_xfp: got = cap_screen_qr().decode('ascii') - if use_nfc: - press_nfc() - assert got == xfp2str(simulator_fixed_xfp).upper() - press_cancel() + time.sleep(.1) + press_nfc() + time.sleep(.2) + nfc_got = nfc_read_text() + time.sleep(.2) + assert nfc_got == got == xfp2str(simulator_fixed_xfp).upper() + press_cancel() # cancel animation + press_cancel() # cancel QR continue time.sleep(0.3) title, story = cap_story() - assert expect in story + assert expect.format(acct=0) in story - if 'acct' in expect: + if expect != "m": assert "Press (1) to select account" in story if acct_num is not None: need_keypress('1') @@ -601,24 +605,52 @@ def test_export_xpub(use_nfc, acct_num, dev, cap_menu, pick_menu_item, goto_home expect = expect.format(acct=acct_num) title, story = cap_story() assert expect in story - assert "Press (1) to select account" not in story + assert "Press (1) to select account" in story - expect = expect.format(acct=0) - if not use_nfc: - press_select() - got_pub = cap_screen_qr().decode('ascii') - else: - if f'Press {KEY_NFC if is_q1 else "(3)"}' not in story: - raise pytest.skip("NFC disabled") + expect = expect.format(acct=0) + + press_select() + got_pub = cap_screen_qr().decode('ascii') + + if f'Press {KEY_NFC if is_q1 else "(3)"}' in story: assert 'NFC' in story press_nfc() time.sleep(0.2) - got_pub = nfc_read_text() + got_nfc_pub = nfc_read_text() time.sleep(0.1) - #press_select() + press_cancel() # cancel animation + press_cancel() # cancel QR + assert got_nfc_pub == got_pub - if got_pub[0] not in 'xt': - got_pub,*_ = slip132undo(got_pub) + time.sleep(.1) + _, story = cap_story() + assert got_pub[0] in 'xt' + if "Press (2)" in story: + if chain == "BTC": + assert f"{'z' if expect[:5] == 'm/84h' else 'y'}pub (SLIP-132)" in story + else: + assert f"{'v' if expect[:5] == 'm/84h' else 'u'}pub (SLIP-132)" in story + need_keypress("2") + time.sleep(.1) + _, story = cap_story() + assert ("%spub (BIP-32)" % ("x" if chain == "BTC" else "t")) in story + assert "Press (2)" in story + + press_select() + got_slip_pub = cap_screen_qr().decode('ascii') + got_unslip, *_ = slip132undo(got_slip_pub) + assert got_unslip == got_pub + + if f'Press {KEY_NFC if is_q1 else "(3)"}' in story: + assert 'NFC' in story + press_nfc() + time.sleep(0.2) + got_nfc_slip_pub = nfc_read_text() + time.sleep(0.1) + press_cancel() # cancel animation + assert got_slip_pub == got_nfc_slip_pub + + press_cancel() # cancel QR expect_acctnum_captured(acct_num) @@ -628,7 +660,6 @@ def test_export_xpub(use_nfc, acct_num, dev, cap_menu, pick_menu_item, goto_home if expect != 'm': wallet = wallet.subkey_for_path(expect[2:].replace('h', "'")) assert got.sec() == wallet.sec() - press_cancel() @pytest.mark.parametrize("chain", ["BTC", "XTN", "XRT"])