diff --git a/releases/ChangeLog.md b/releases/ChangeLog.md index 2bcda73a..d00203de 100644 --- a/releases/ChangeLog.md +++ b/releases/ChangeLog.md @@ -5,7 +5,8 @@ - Enhancement: Allow to specify start index for address explorer export and browsing - Enhancement: Allow unlimited index for BIP-85 derivations. Needs to be enabled first in `Danger Zone` - Enhancement: Add `Nunchuk` option to `Export Wallet` -- Enhancement: `View Identity` shows temporary seed active on the top +- Enhancement: Add `Zeus` option to `Export Wallet` +- Enhancement: `View Identity` shows temporary seed active on the top - Change: `Passphrase` menu item is no longer offered if BIP39 passphrase already in use. Use `Restore Master` with ability to keep or purge current passphrase wallet settings. diff --git a/releases/QChangeLog.md b/releases/QChangeLog.md index 336a19b1..1c685f91 100644 --- a/releases/QChangeLog.md +++ b/releases/QChangeLog.md @@ -27,6 +27,7 @@ - Enhancement: Allow to specify start index for address explorer export and browsing - Enhancement: Add `Nunchuk` option to `Export Wallet` +- Enhancement: Add `Zeus` option to `Export Wallet` - Enhancement: `View Identity` shows temporary seed active on the top - Change: `Passphrase` menu item is no longer offered if BIP39 passphrase already in use. Use `Restore Master` with ability to keep or purge current diff --git a/shared/actions.py b/shared/actions.py index 4364dfe9..536d29f9 100644 --- a/shared/actions.py +++ b/shared/actions.py @@ -8,7 +8,7 @@ import ckcc, pyb, version, uasyncio, sys, uos from uhashlib import sha256 from uasyncio import sleep_ms from ubinascii import hexlify as b2a_hex -from utils import imported, pretty_short_delay, problem_file_line, get_filesize +from utils import imported, problem_file_line, get_filesize from utils import xfp2str, B2A, addr_fmt_label from ux import ux_show_story, the_ux, ux_confirm, ux_dramatic_pause, ux_aborted from ux import ux_enter_bip32_index, ux_input_text, import_export_prompt @@ -1079,29 +1079,29 @@ async def electrum_skeleton(*a): elif ch != 'y': return - # pick segwit or classic derivation+such - # - Ordering and terminology from similar screen in Electrum. - rv = [] - - rv.append(MenuItem(addr_fmt_label(AF_CLASSIC), f=electrum_skeleton_step2, - arg=(AF_CLASSIC, account_num))) - rv.append(MenuItem(addr_fmt_label(AF_P2WPKH_P2SH), f=electrum_skeleton_step2, - arg=(AF_P2WPKH_P2SH, account_num))) - rv.append(MenuItem(addr_fmt_label(AF_P2WPKH), f=electrum_skeleton_step2, - arg=(AF_P2WPKH, account_num))) - + rv = [ + MenuItem(addr_fmt_label(af), f=electrum_skeleton_step2, + arg=(af, account_num)) + for af in [AF_P2WPKH, AF_CLASSIC, AF_P2WPKH_P2SH] + ] the_ux.push(MenuSystem(rv)) -def ss_descriptor_export_story(addition="", background=None): +def ss_descriptor_export_story(addition="", background="", acct=True): # saves memory being in a function return ("This saves a ranged xpub descriptor" + addition - + (background or - '. Choose descriptor and address type for the wallet on next screens.'+PICK_ACCOUNT) + + background + + (PICK_ACCOUNT if acct else "") + SENSITIVE_NOT_SECRET) -async def ss_descriptor_skeleton(label, _, item): +async def ss_descriptor_skeleton(_0, _1, item): # Export of descriptor data (wallet) - ch = await ux_show_story(ss_descriptor_export_story(), escape='1') + int_ext, addition = None, "" + allowed_af = [AF_P2WPKH, AF_CLASSIC, AF_P2WPKH_P2SH] + if item.arg: + int_ext, allowed_af, ll = item.arg + addition = " for " + ll + + ch = await ux_show_story(ss_descriptor_export_story(addition), escape='1') account_num = 0 if ch == '1': @@ -1109,27 +1109,24 @@ async def ss_descriptor_skeleton(label, _, item): elif ch != 'y': return - int_ext = True - ch = await ux_show_story( - "To export receiving and change descriptors in one descriptor (<0;1> notation) press OK, " - "press (1) to export receiving and change descriptors separately.", escape='1') - if ch == "1": - int_ext = False - elif ch != "y": - return + if int_ext is None: + ch = await ux_show_story( + "To export receiving and change descriptors in one descriptor " + "(<0;1> notation) press OK, press (1) to export " + "receiving and change descriptors separately.", escape='1') + if ch == "x": return + int_ext = False if ch == "1" else True - # pick segwit or classic derivation+such - # - Ordering and terminology from similar screen in Electrum. - rv = [] - - rv.append(MenuItem(addr_fmt_label(AF_CLASSIC), f=descriptor_skeleton_step2, - arg=(AF_CLASSIC, account_num, int_ext))) - rv.append(MenuItem(addr_fmt_label(AF_P2WPKH_P2SH), f=descriptor_skeleton_step2, - arg=(AF_P2WPKH_P2SH, account_num, int_ext))) - rv.append(MenuItem(addr_fmt_label(AF_P2WPKH), f=descriptor_skeleton_step2, - arg=(AF_P2WPKH, account_num, int_ext))) - - the_ux.push(MenuSystem(rv)) + if len(allowed_af) == 1: + await make_descriptor_wallet_export(allowed_af[0], account_num, + int_ext=int_ext) + else: + rv = [ + MenuItem(addr_fmt_label(af), f=descriptor_skeleton_step2, + arg=(af, account_num, int_ext)) + for af in allowed_af + ] + the_ux.push(MenuSystem(rv)) async def samourai_post_mix_descriptor_export(*a): name = "POST-MIX" @@ -1150,7 +1147,7 @@ async def samourai_account_descriptor(name, account_num): ch = await ux_show_story( ss_descriptor_export_story( addition=" for Samourai %s account." % name, - background=" ") + acct=False) ) if ch != 'y': diff --git a/shared/export.py b/shared/export.py index 87a7236f..799c868c 100644 --- a/shared/export.py +++ b/shared/export.py @@ -139,8 +139,7 @@ async def write_text_file(fname_pattern, body, title, derive, addr_fmt): from ux import import_export_prompt choice = await import_export_prompt("%s file" % title, is_import=False, - no_qr=(not version.has_qwerty)) - + no_qr=(not version.has_qwerty)) if choice == KEY_CANCEL: return elif choice == KEY_QR: diff --git a/shared/flow.py b/shared/flow.py index a836e66f..ed3b0226 100644 --- a/shared/flow.py +++ b/shared/flow.py @@ -171,6 +171,8 @@ WalletExportMenu = [ MenuItem("Bitcoin Core", f=bitcoin_core_skeleton), MenuItem("Sparrow Wallet", f=named_generic_skeleton, arg="Sparrow"), MenuItem("Nunchuk", f=named_generic_skeleton, arg="Nunchuk"), + MenuItem("Zeus", f=ss_descriptor_skeleton, + arg=(True, [AF_P2WPKH, AF_P2WPKH_P2SH], "Zeus Wallet")), MenuItem("Electrum Wallet", f=electrum_skeleton), MenuItem("Wasabi Wallet", f=wasabi_skeleton), MenuItem("Unchained", f=unchained_capital_export), diff --git a/testing/test_export.py b/testing/test_export.py index c7565d1b..f3372f2e 100644 --- a/testing/test_export.py +++ b/testing/test_export.py @@ -16,7 +16,7 @@ from helpers import xfp2str, slip132undo from conftest import simulator_fixed_xfp, simulator_fixed_tprv, simulator_fixed_words from ckcc_protocol.constants import AF_CLASSIC, AF_P2WPKH from pprint import pprint -from charcodes import KEY_NFC +from charcodes import KEY_NFC, KEY_QR @pytest.fixture @@ -571,9 +571,8 @@ def test_export_xpub(use_nfc, acct_num, dev, cap_menu, pick_menu_item, goto_home @pytest.mark.parametrize("acct_num", [None, 0, 1, (2 ** 31) - 1]) @pytest.mark.parametrize("int_ext", [True, False]) def test_generic_descriptor_export(chain, addr_fmt, acct_num, goto_home, settings_set, need_keypress, - pick_menu_item, way, cap_story, cap_menu, nfc_read_text, int_ext, - microsd_path, settings_get, virtdisk_path, load_export, press_select, - mk4_qr_not_allowed): + pick_menu_item, way, cap_story, cap_menu, int_ext, settings_get, + virtdisk_path, load_export, press_select, mk4_qr_not_allowed): mk4_qr_not_allowed(way) settings_set('chain', chain) @@ -585,7 +584,6 @@ def test_generic_descriptor_export(chain, addr_fmt, acct_num, goto_home, setting time.sleep(.1) _, story = cap_story() assert "This saves a ranged xpub descriptor" in story - assert "Choose descriptor and address type for the wallet on next screens" in story assert "Press (1) to enter a non-zero account number" in story assert "sensitive--in terms of privacy" in story assert "not compromise your funds directly" in story @@ -646,6 +644,89 @@ def test_generic_descriptor_export(chain, addr_fmt, acct_num, goto_home, setting assert xpub_target in xpub +@pytest.mark.parametrize("chain", ["BTC", "XTN"]) +@pytest.mark.parametrize("way", ["nfc", "qr"]) +@pytest.mark.parametrize("addr_fmt", [AF_P2WPKH, AF_P2WPKH_P2SH]) +@pytest.mark.parametrize("acct_num", [None, 55]) +def test_zeus_descriptor_export(addr_fmt, acct_num, goto_home, need_keypress, pick_menu_item, + way, cap_story, cap_menu, nfc_read_text, settings_get, chain, + virtdisk_path, load_export, press_select, mk4_qr_not_allowed, + settings_set, is_q1, press_cancel, cap_screen_qr, press_nfc): + + mk4_qr_not_allowed(way) + settings_set('chain', chain) + chain_num = 1 if chain == "XTN" else 0 + + goto_home() + pick_menu_item("Advanced/Tools") + pick_menu_item("Export Wallet") + pick_menu_item("Zeus") + time.sleep(.1) + title, story = cap_story() + + assert "This saves a ranged xpub descriptor" in story + assert "Press (1) to enter a non-zero account number" in story + assert "sensitive--in terms of privacy" in story + assert "not compromise your funds directly" in story + + if isinstance(acct_num, int): + need_keypress("1") # chosse account number + for ch in str(acct_num): + need_keypress(ch) # input num + press_select() # confirm selection + else: + press_select() # confirm story + + time.sleep(.1) + menu = cap_menu() + assert len(menu) == 2 + if addr_fmt == AF_P2WPKH: + menu_item = "Segwit P2WPKH" + desc_prefix = "wpkh(" + bip44_purpose = 84 + else: + assert addr_fmt == AF_P2WPKH_P2SH + menu_item = "P2SH-Segwit" + desc_prefix = "sh(wpkh(" + bip44_purpose = 49 + + assert menu_item in menu + pick_menu_item(menu_item) + + time.sleep(.1) + title, story = cap_story() + + if way == "qr": + assert ("%s to show QR" % (KEY_QR if is_q1 else "(4)")) in story + need_keypress(KEY_QR if is_q1 else "4") + time.sleep(.2) + contents = cap_screen_qr().decode('ascii') + else: + assert ("ress %s to share via NFC" % (KEY_NFC if is_q1 else "(3)")) in story + press_nfc() + time.sleep(.2) + contents = nfc_read_text() + time.sleep(.5) + press_cancel() # exit NFC animation + + descriptor = contents.strip() + + assert descriptor.startswith(desc_prefix) + desc_obj = Descriptor.parse(descriptor) + assert desc_obj.serialize(int_ext=True) == descriptor + assert desc_obj.addr_fmt == addr_fmt + assert len(desc_obj.keys) == 1 + xfp, derive, xpub = desc_obj.keys[0] + assert xfp == settings_get("xfp") + assert derive == f"m/{bip44_purpose}h/{chain_num}h/{acct_num if acct_num is not None else 0}h" + seed = Mnemonic.to_seed(simulator_fixed_words) + node = BIP32Node.from_master_secret( + seed, netcode="BTC" if chain == "BTC" else "XTN" + ).subkey_for_path(derive[2:].replace("h", "H")) + xpub_target = node.hwif() + assert xpub_target in xpub + + @pytest.mark.parametrize("chain", ["BTC", "XTN", "XRT"]) @pytest.mark.parametrize("account", ["Postmix", "Premix"]) def test_samourai_vs_generic(chain, account, settings_set, pick_menu_item, goto_home, @@ -680,7 +761,6 @@ def test_samourai_vs_generic(chain, account, settings_set, pick_menu_item, goto_ _, story = cap_story() assert "This saves a ranged xpub descriptor" in story assert in_story in story - assert "Choose an address type for the wallet on the next screen" not in story # NOT assert "Press 1 to enter a non-zero account number" not in story # NOT assert "sensitive--in terms of privacy" in story assert "not compromise your funds directly" in story