Key expression export
This commit is contained in:
parent
f235a83cf3
commit
192f2d2dda
@ -4,7 +4,8 @@ This lists the new changes that have not yet been published in a normal release.
|
||||
|
||||
# Shared Improvements - Both Mk4 and Q
|
||||
|
||||
- tbd
|
||||
- New Feature: Export [BIP-380](https://github.com/bitcoin/bips/blob/master/bip-0380.mediawiki) extended key expression.
|
||||
Navigate to `Advanced/Tools -> Export Wallet -> Key Expression`
|
||||
|
||||
|
||||
# Mk4 Specific Changes
|
||||
|
||||
@ -14,7 +14,7 @@ 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, OK, X, ux_render_words
|
||||
from export import export_contents, make_summary_file, make_descriptor_wallet_export
|
||||
from export import make_bitcoin_core_wallet, generate_wasabi_wallet, generate_generic_export
|
||||
from export import generate_unchained_export, generate_electrum_wallet
|
||||
from export import generate_unchained_export, generate_electrum_wallet, make_key_expression_export
|
||||
from files import CardSlot, CardMissingError, needs_microsd
|
||||
from public_constants import AF_CLASSIC, AF_P2WPKH, AF_P2WPKH_P2SH
|
||||
from glob import settings
|
||||
@ -1193,6 +1193,46 @@ async def ss_descriptor_skeleton(_0, _1, item):
|
||||
]
|
||||
the_ux.push(MenuSystem(rv))
|
||||
|
||||
|
||||
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)
|
||||
|
||||
async def key_expression_skeleton(_0, _1, item):
|
||||
# Export key expression -> [xfp/d/e/r]xpub
|
||||
|
||||
acct_num = 0
|
||||
ch = await ux_show_story("This saves a extended key expression."
|
||||
+ PICK_ACCOUNT + SENSITIVE_NOT_SECRET, escape='1')
|
||||
if ch == '1':
|
||||
acct_num = await ux_enter_bip32_index('Account Number:', unlimited=True) or 0
|
||||
elif ch != 'y':
|
||||
return
|
||||
|
||||
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"),
|
||||
]
|
||||
|
||||
from address_explorer import KeypathMenu
|
||||
|
||||
async def doit(*a):
|
||||
return KeypathMenu(ranged=False, done_fn=make_key_expression_export)
|
||||
|
||||
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
|
||||
]
|
||||
rv += [MenuItem("Custom Path", menu=doit)]
|
||||
|
||||
the_ux.push(MenuSystem(rv))
|
||||
|
||||
async def samourai_post_mix_descriptor_export(*a):
|
||||
name = "POST-MIX"
|
||||
post_mix_acct_num = 2147483646
|
||||
|
||||
@ -30,8 +30,10 @@ def censor_address(addr):
|
||||
return addr[0:12] + '___' + addr[12+3:]
|
||||
|
||||
class KeypathMenu(MenuSystem):
|
||||
def __init__(self, path=None, nl=0):
|
||||
def __init__(self, path=None, nl=0, ranged=True, done_fn=None):
|
||||
self.prefix = None
|
||||
self.done_fn = done_fn
|
||||
self.ranged = ranged
|
||||
|
||||
if path is None:
|
||||
# Top level menu; useful shortcuts, and special case just "m"
|
||||
@ -40,10 +42,13 @@ class KeypathMenu(MenuSystem):
|
||||
MenuItem("m/44h/⋯", f=self.deeper),
|
||||
MenuItem("m/49h/⋯", f=self.deeper),
|
||||
MenuItem("m/84h/⋯", f=self.deeper),
|
||||
MenuItem("m/0/{idx}", menu=self.done),
|
||||
MenuItem("m/{idx}", menu=self.done),
|
||||
MenuItem("m", f=self.done),
|
||||
]
|
||||
if self.ranged:
|
||||
items += [
|
||||
MenuItem("m/0/{idx}", menu=self.done),
|
||||
MenuItem("m/{idx}", menu=self.done),
|
||||
]
|
||||
else:
|
||||
# drill down one layer: (nl) is the current leaf
|
||||
# - hardened choice first
|
||||
@ -53,11 +58,14 @@ class KeypathMenu(MenuSystem):
|
||||
MenuItem(p+"/⋯", menu=self.deeper),
|
||||
MenuItem(p+"h", menu=self.done),
|
||||
MenuItem(p, menu=self.done),
|
||||
MenuItem(p+"h/0/{idx}", menu=self.done),
|
||||
MenuItem(p+"/0/{idx}", menu=self.done), #useful shortcut?
|
||||
MenuItem(p+"h/{idx}", menu=self.done),
|
||||
MenuItem(p+"/{idx}", menu=self.done),
|
||||
]
|
||||
if self.ranged:
|
||||
items += [
|
||||
MenuItem(p + "h/0/{idx}", menu=self.done),
|
||||
MenuItem(p + "/0/{idx}", menu=self.done), # useful shortcut?
|
||||
MenuItem(p + "h/{idx}", menu=self.done),
|
||||
MenuItem(p + "/{idx}", menu=self.done),
|
||||
]
|
||||
|
||||
# simple consistent truncation when needed
|
||||
max_wide = max(len(mi.label) for mi in items)
|
||||
@ -95,9 +103,12 @@ class KeypathMenu(MenuSystem):
|
||||
if isinstance(top, KeypathMenu):
|
||||
the_ux.pop()
|
||||
continue
|
||||
assert isinstance(top, AddressListMenu)
|
||||
# assert isinstance(top, AddressListMenu), type(top)
|
||||
break
|
||||
|
||||
if self.done_fn:
|
||||
return await self.done_fn(final_path)
|
||||
|
||||
return PickAddrFmtMenu(final_path, top)
|
||||
|
||||
async def deeper(self, _1, _2, item):
|
||||
@ -105,7 +116,7 @@ class KeypathMenu(MenuSystem):
|
||||
assert val.endswith('/⋯')
|
||||
cpath = val[:-2]
|
||||
nl = await ux_enter_bip32_index('%s/' % cpath, unlimited=True)
|
||||
return KeypathMenu(cpath, nl)
|
||||
return KeypathMenu(cpath, nl, ranged=self.ranged, done_fn=self.done_fn)
|
||||
|
||||
class PickAddrFmtMenu(MenuSystem):
|
||||
def __init__(self, path, parent):
|
||||
|
||||
@ -471,7 +471,7 @@ async def make_descriptor_wallet_export(addr_type, account_num=0, mode=None, int
|
||||
dis.fullscreen('Generating...')
|
||||
chain = chains.current_chain()
|
||||
|
||||
xfp = settings.get('xfp')
|
||||
xfp = settings.get('xfp', 0)
|
||||
dis.progress_bar_show(0.1)
|
||||
if mode is None:
|
||||
mode = chains.af_to_bip44_purpose(addr_type)
|
||||
@ -503,5 +503,18 @@ async def make_descriptor_wallet_export(addr_type, account_num=0, mode=None, int
|
||||
await export_contents("Descriptor", body, fname_pattern, derive + "/0/0",
|
||||
addr_type, force_prompt=True, direct_way=direct_way)
|
||||
|
||||
|
||||
async def make_key_expression_export(orig_der, fname_pattern="key_expr.txt"):
|
||||
|
||||
xfp = xfp2str(settings.get('xfp', 0)).lower()
|
||||
|
||||
with stash.SensitiveValues() as sv:
|
||||
ek = chains.current_chain().serialize_public(sv.derive_path(orig_der))
|
||||
|
||||
body = "[%s/%s]%s" % (xfp, orig_der.replace("m/", ""), ek)
|
||||
|
||||
await export_contents("Key Expression", body, fname_pattern,
|
||||
None, None, force_prompt=True)
|
||||
|
||||
# EOF
|
||||
|
||||
|
||||
@ -235,6 +235,7 @@ WalletExportMenu = [
|
||||
MenuItem("Descriptor", f=ss_descriptor_skeleton),
|
||||
MenuItem("Generic JSON", f=generic_skeleton),
|
||||
MenuItem("Export XPUB", menu=XpubExportMenu),
|
||||
MenuItem("Key Expression", f=key_expression_skeleton),
|
||||
MenuItem("Dump Summary", predicate=has_secrets, f=dump_summary),
|
||||
]
|
||||
|
||||
|
||||
@ -2568,8 +2568,8 @@ def skip_if_useless_way(is_q1, nfc_disabled, vdisk_disabled):
|
||||
# when NFC is disabled, no point trying to do a PSBT via NFC
|
||||
# - important: run_sim_tests.py will enable NFC for complete testing
|
||||
# - similarly: the Mk4 and earlier had no QR scanner, so cannot use that as input
|
||||
def doit(way):
|
||||
if way == "qr" and not is_q1:
|
||||
def doit(way, allow_mk4_qr=False):
|
||||
if way == "qr" and (not is_q1 and not allow_mk4_qr):
|
||||
raise pytest.skip("mk4 QR not supported")
|
||||
elif way == 'nfc' and nfc_disabled():
|
||||
# runner will test these cases, but fail faster otherwise
|
||||
|
||||
@ -13,7 +13,6 @@ from bip32 import BIP32Node
|
||||
from ckcc_protocol.constants import *
|
||||
from helpers import xfp2str, slip132undo
|
||||
from conftest import simulator_fixed_xfp, simulator_fixed_tprv, simulator_fixed_words, simulator_fixed_xprv
|
||||
from ckcc_protocol.constants import AF_CLASSIC, AF_P2WPKH
|
||||
from pprint import pprint
|
||||
from charcodes import KEY_NFC, KEY_QR
|
||||
|
||||
@ -888,4 +887,131 @@ def test_samourai_vs_generic(chain, account, settings_set, pick_menu_item, goto_
|
||||
file_desc = load_export("sd", label="Descriptor", is_json=False, addr_fmt=AF_P2WPKH)
|
||||
assert file_desc.strip() == file_desc_generic.strip()
|
||||
|
||||
|
||||
@pytest.mark.parametrize("chain", ["BTC", "XTN"])
|
||||
@pytest.mark.parametrize("way", ["sd", "vdisk", "nfc", "qr"])
|
||||
@pytest.mark.parametrize("addr_fmt", [AF_P2WPKH, AF_P2WPKH_P2SH, AF_CLASSIC, AF_P2WSH, AF_P2WSH_P2SH])
|
||||
@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):
|
||||
|
||||
skip_if_useless_way(way, allow_mk4_qr=True)
|
||||
|
||||
settings_set('chain', chain)
|
||||
chain_num = 1 if chain in ["XTN", "XRT"] else 0
|
||||
goto_home()
|
||||
pick_menu_item("Advanced/Tools")
|
||||
pick_menu_item("Export Wallet")
|
||||
pick_menu_item("Key Expression")
|
||||
time.sleep(.1)
|
||||
_, story = cap_story()
|
||||
assert "This saves a extended key expression" 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
|
||||
acct_num = 0
|
||||
|
||||
menu = cap_menu()
|
||||
if addr_fmt == AF_P2WPKH:
|
||||
menu_item = "Segwit P2WPKH"
|
||||
derive = f"m/84h/{chain_num}h/{acct_num}h"
|
||||
elif addr_fmt == AF_P2WPKH_P2SH:
|
||||
menu_item = "P2SH-Segwit"
|
||||
derive = f"m/49h/{chain_num}h/{acct_num}h"
|
||||
elif addr_fmt == AF_CLASSIC:
|
||||
menu_item = "Classic P2PKH"
|
||||
derive = f"m/44h/{chain_num}h/{acct_num}h"
|
||||
elif addr_fmt == AF_P2WSH:
|
||||
menu_item = "Multi P2WSH"
|
||||
derive = f"m/48h/{chain_num}h/{acct_num}h/2h"
|
||||
else:
|
||||
assert addr_fmt == AF_P2WSH_P2SH
|
||||
menu_item = "Multi P2SH-P2WSH"
|
||||
derive = f"m/48h/{chain_num}h/{acct_num}h/1h"
|
||||
|
||||
assert menu_item in menu
|
||||
pick_menu_item(menu_item)
|
||||
|
||||
contents = load_export(way, label="Key Expression", is_json=False, sig_check=False)
|
||||
key_exp = contents.strip()
|
||||
|
||||
xfp = dev.master_fingerprint
|
||||
xfp = xfp2str(xfp).lower()
|
||||
|
||||
seed = Mnemonic.to_seed(simulator_fixed_words)
|
||||
node = BIP32Node.from_master_secret(
|
||||
seed, netcode="BTC" if chain == "BTC" else "XTN"
|
||||
).subkey_for_path(derive)
|
||||
|
||||
target = f"[{xfp}/{derive.replace('m/', '')}]{node.hwif()}"
|
||||
assert key_exp == target
|
||||
|
||||
|
||||
@pytest.mark.parametrize('path', [
|
||||
# NOTE: (2**31)-1 = 0x7fff_ffff = 2147483647
|
||||
"m/2147483647/2147483647/2147483647/2147483647/2147483647/2147483647/2147483647/2147483647",
|
||||
"m/1/2/3/4/5",
|
||||
"m/1h/2h/3h/4h/5h",
|
||||
"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):
|
||||
use_testnet()
|
||||
goto_home()
|
||||
pick_menu_item("Advanced/Tools")
|
||||
pick_menu_item("Export Wallet")
|
||||
pick_menu_item("Key Expression")
|
||||
press_select() # story
|
||||
pick_menu_item("Custom Path")
|
||||
|
||||
# blind entry, using only first 2 menu items
|
||||
deeper = path.split("/")[1:]
|
||||
for depth, part in enumerate(deeper):
|
||||
time.sleep(.01)
|
||||
m = cap_menu()
|
||||
for mi in m:
|
||||
assert "{idx}" not in mi # ranged values not allowed here
|
||||
if depth == 0:
|
||||
assert m[0] == 'm/⋯'
|
||||
pick_menu_item(m[0])
|
||||
else:
|
||||
assert m[0].endswith("h/⋯")
|
||||
assert m[1].endswith("/⋯")
|
||||
assert m[0] != m[1]
|
||||
|
||||
pick_menu_item(m[0 if last_part[-1] == "h" else 1])
|
||||
|
||||
# enter path component
|
||||
for d in part:
|
||||
if d == "h": break
|
||||
need_keypress(d)
|
||||
press_select()
|
||||
|
||||
last_part = part
|
||||
|
||||
time.sleep(.01)
|
||||
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)
|
||||
key_exp = contents.strip()
|
||||
|
||||
xfp = dev.master_fingerprint
|
||||
xfp = xfp2str(xfp).lower()
|
||||
|
||||
seed = Mnemonic.to_seed(simulator_fixed_words)
|
||||
node = BIP32Node.from_master_secret(seed, netcode="XTN").subkey_for_path(path)
|
||||
|
||||
target = f"[{xfp}/{path.replace('m/', '')}]{node.hwif()}"
|
||||
assert key_exp == target
|
||||
|
||||
# EOF
|
||||
|
||||
Loading…
Reference in New Issue
Block a user