From cfc46b565e284c3dc016987b05d17400de74f20f Mon Sep 17 00:00:00 2001 From: scgbckbone Date: Tue, 3 Mar 2026 04:10:11 +0100 Subject: [PATCH] show descriptor & key expression in story; signed key expression export --- shared/actions.py | 20 +++++++++++--------- shared/export.py | 24 ++++++++++++++++-------- testing/test_export.py | 37 ++++++++++++++++++++++++++++++------- 3 files changed, 57 insertions(+), 24 deletions(-) diff --git a/shared/actions.py b/shared/actions.py index 7f4db904..4d97dc8e 100644 --- a/shared/actions.py +++ b/shared/actions.py @@ -1198,8 +1198,8 @@ async def ss_descriptor_skeleton(_0, _1, item): async def key_expression_skeleton_step2(_1, _2, item): # pick a semi-random file name, render and save it. - orig_path = item.arg - await make_key_expression_export(orig_path) + orig_path, addr_fmt = item.arg + await make_key_expression_export(orig_path, addr_fmt) async def key_expression_skeleton(_0, _1, item): # Export key expression -> [xfp/d/e/r]xpub @@ -1212,12 +1212,14 @@ async def key_expression_skeleton(_0, _1, item): elif ch != 'y': return + # element on 2nd index is address format for signed exports + # if multisig key use p2pkh todo = [ - ("Segwit P2WPKH", "m/84h/%dh/%dh"), - ("Classic P2PKH", "m/44h/%dh/%dh"), - ("P2SH-Segwit", "m/49h/%dh/%dh"), - ("Multi P2WSH", "m/48h/%dh/%dh/2h"), - ("Multi P2SH-P2WSH", "m/48h/%dh/%dh/1h"), + ("Segwit P2WPKH", "m/84h/%dh/%dh", AF_P2WPKH), + ("Classic P2PKH", "m/44h/%dh/%dh", AF_CLASSIC), + ("P2SH-Segwit", "m/49h/%dh/%dh", AF_P2WPKH_P2SH), + ("Multi P2WSH", "m/48h/%dh/%dh/2h", AF_CLASSIC), + ("Multi P2SH-P2WSH", "m/48h/%dh/%dh/1h", AF_CLASSIC), ] from address_explorer import KeypathMenu @@ -1228,8 +1230,8 @@ async def key_expression_skeleton(_0, _1, item): ct = chains.current_chain().b44_cointype rv = [ - MenuItem(label, f=key_expression_skeleton_step2, arg=orig_der % (ct, acct_num)) - for label, orig_der in todo + MenuItem(label, f=key_expression_skeleton_step2, arg=(orig_der % (ct, acct_num), af)) + for label, orig_der, af in todo ] rv += [MenuItem("Custom Path", menu=doit)] diff --git a/shared/export.py b/shared/export.py index 6a08b790..a220f2cf 100644 --- a/shared/export.py +++ b/shared/export.py @@ -35,7 +35,8 @@ async def export_by_qr(body, label, type_code, force_bbqr=False): async def export_contents(title, contents, fname_pattern, derive=None, addr_fmt=None, - is_json=False, force_bbqr=False, force_prompt=False, direct_way=None): + is_json=False, force_bbqr=False, force_prompt=False, direct_way=None, + intro="", footer="", ux_title=None): # 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) # checks if suitable to offer QR export on Mk4 @@ -59,8 +60,8 @@ async def export_contents(title, contents, fname_pattern, derive=None, addr_fmt= ch = direct_way # set it to direct way only once, outside the loop while True: if direct_way is None: - ch = await import_export_prompt("%s file" % title, - force_prompt=force_prompt, no_qr=no_qr) + ch = await import_export_prompt("%s file" % title, intro=intro, footnotes=footer, + force_prompt=force_prompt, no_qr=no_qr, title=ux_title) if ch == KEY_CANCEL: break elif ch == KEY_QR: @@ -500,11 +501,15 @@ async def make_descriptor_wallet_export(addr_type, account_num=0, mode=None, int ) dis.progress_bar_show(1) - await export_contents("Descriptor", body, fname_pattern, derive + "/0/0", - addr_type, force_prompt=True, direct_way=direct_way) + + intro, footer = (body, "") if version.has_qwerty else ("", body) + title = "Descriptor" + await export_contents(title, body, fname_pattern, derive + "/0/0", addr_type, + force_prompt=True, direct_way=direct_way, intro=intro, footer=footer, + ux_title=title if version.has_qwerty else None) -async def make_key_expression_export(orig_der, fname_pattern="key_expr.txt"): +async def make_key_expression_export(orig_der, addr_fmt=AF_CLASSIC, fname_pattern="key_expr.txt"): from glob import dis dis.fullscreen('Generating...') @@ -516,8 +521,11 @@ async def make_key_expression_export(orig_der, fname_pattern="key_expr.txt"): body = "[%s/%s]%s" % (xfp, orig_der.replace("m/", ""), ek) - await export_contents("Key Expression", body, fname_pattern, - None, None, force_prompt=True) + intro, footer = (body, "") if version.has_qwerty else ("", body) + title = "Key Expression" + await export_contents(title, body, fname_pattern, orig_der + "/0/0", addr_fmt, + force_prompt=True, intro=intro, footer=footer, + ux_title=title if version.has_qwerty else None) # EOF diff --git a/testing/test_export.py b/testing/test_export.py index ffb8895f..9febe206 100644 --- a/testing/test_export.py +++ b/testing/test_export.py @@ -643,7 +643,7 @@ def test_export_xpub(chain, acct_num, dev, cap_menu, pick_menu_item, goto_home, def test_generic_descriptor_export(chain, addr_fmt, acct_num, goto_home, settings_set, need_keypress, expect_acctnum_captured, OK, pick_menu_item, way, cap_story, cap_menu, int_ext, settings_get, - virtdisk_path, load_export, press_select, skip_if_useless_way): + virtdisk_path, load_export, press_select, skip_if_useless_way, is_q1): skip_if_useless_way(way) @@ -697,9 +697,16 @@ def test_generic_descriptor_export(chain, addr_fmt, acct_num, goto_home, expect_acctnum_captured(acct_num) + time.sleep(.1) + title, story = cap_story() + idx = 0 if is_q1 else 1 + story_desc = story.split("\n\n")[idx] + contents = load_export(way, label="Descriptor", is_json=False, addr_fmt=addr_fmt) descriptor = contents.strip() + assert descriptor == story_desc.strip() + if int_ext is False: descriptor = descriptor.split("\n")[0] # external assert descriptor.startswith(desc_prefix) @@ -771,6 +778,8 @@ def test_zeus_descriptor_export(addr_fmt, acct_num, goto_home, need_keypress, pi time.sleep(.1) title, story = cap_story() + idx = 0 if is_q1 else 1 + story_desc = story.split("\n\n")[idx] expect_acctnum_captured(acct_num) @@ -789,6 +798,8 @@ def test_zeus_descriptor_export(addr_fmt, acct_num, goto_home, need_keypress, pi descriptor = contents.strip() + assert descriptor == story_desc.strip() + assert descriptor.startswith(desc_prefix) desc_obj = Descriptor.parse(descriptor) assert desc_obj.serialize(int_ext=True) == descriptor @@ -895,7 +906,7 @@ def test_samourai_vs_generic(chain, account, settings_set, pick_menu_item, goto_ @pytest.mark.parametrize("acct_num", [None, (2 ** 31) - 1]) def test_key_expression_export(chain, addr_fmt, acct_num, goto_home, settings_set, need_keypress, pick_menu_item, way, cap_story, cap_menu, virtdisk_path, dev, - load_export, press_select, skip_if_useless_way): + load_export, press_select, skip_if_useless_way, is_q1): skip_if_useless_way(way, allow_mk4_qr=True) @@ -934,15 +945,22 @@ def test_key_expression_export(chain, addr_fmt, acct_num, goto_home, settings_se elif addr_fmt == AF_P2WSH: menu_item = "Multi P2WSH" derive = f"m/48h/{chain_num}h/{acct_num}h/2h" + addr_fmt = AF_CLASSIC else: assert addr_fmt == AF_P2WSH_P2SH menu_item = "Multi P2SH-P2WSH" derive = f"m/48h/{chain_num}h/{acct_num}h/1h" + addr_fmt = AF_CLASSIC assert menu_item in menu pick_menu_item(menu_item) - contents = load_export(way, label="Key Expression", is_json=False, sig_check=False) + time.sleep(.1) + title, story = cap_story() + idx = 0 if is_q1 else 1 + story_key_exp = story.split("\n\n")[idx] + + contents = load_export(way, label="Key Expression", is_json=False,addr_fmt=addr_fmt) key_exp = contents.strip() xfp = dev.master_fingerprint @@ -954,7 +972,7 @@ def test_key_expression_export(chain, addr_fmt, acct_num, goto_home, settings_se ).subkey_for_path(derive) target = f"[{xfp}/{derive.replace('m/', '')}]{node.hwif()}" - assert key_exp == target + assert key_exp == target == story_key_exp @pytest.mark.parametrize('path', [ @@ -965,7 +983,7 @@ def test_key_expression_export(chain, addr_fmt, acct_num, goto_home, settings_se "m/45h", ]) def test_custom_key_expression_export(path, goto_home, pick_menu_item, cap_menu, need_keypress, - press_select, load_export, use_testnet, dev): + press_select, load_export, use_testnet, dev, cap_story, is_q1): use_testnet() goto_home() pick_menu_item("Advanced/Tools") @@ -1003,7 +1021,12 @@ def test_custom_key_expression_export(path, goto_home, pick_menu_item, cap_menu, m = cap_menu() pick_menu_item(m[2 if part[-1] == "h" else 3]) - contents = load_export("sd", label="Key Expression", is_json=False, sig_check=False) + time.sleep(.25) + title, story = cap_story() + idx = 0 if is_q1 else 1 + story_key_exp = story.split("\n\n")[idx] + + contents = load_export("sd", label="Key Expression", is_json=False) key_exp = contents.strip() xfp = dev.master_fingerprint @@ -1013,6 +1036,6 @@ def test_custom_key_expression_export(path, goto_home, pick_menu_item, cap_menu, node = BIP32Node.from_master_secret(seed, netcode="XTN").subkey_for_path(path) target = f"[{xfp}/{path.replace('m/', '')}]{node.hwif()}" - assert key_exp == target + assert key_exp == target == story_key_exp # EOF