2227 lines
89 KiB
Python
2227 lines
89 KiB
Python
# (c) Copyright 2023 by Coinkite Inc. This file is covered by license found in COPYING-CC.
|
|
#
|
|
# Miniscript-related tests.
|
|
#
|
|
import pytest, json, time, itertools, struct, random, os
|
|
from ckcc.protocol import CCProtocolPacker
|
|
from constants import AF_P2TR
|
|
from psbt import BasicPSBT
|
|
|
|
|
|
H = "50929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0" # BIP-0341
|
|
TREE = {
|
|
1: '%s',
|
|
2: '{%s,%s}',
|
|
3: random.choice(['{{%s,%s},%s}','{%s,{%s,%s}}']),
|
|
4: '{{%s,%s},{%s,%s}}',
|
|
5: random.choice(['{{%s,%s},{%s,{%s,%s}}}', '{{{%s,%s},%s},{%s,%s}}']),
|
|
6: '{{%s,{%s,%s}},{{%s,%s},%s}}',
|
|
7: '{{%s,{%s,%s}},{%s,{%s,{%s,%s}}}}',
|
|
8: '{{{%s,%s},{%s,%s}},{{%s,%s},{%s,%s}}}',
|
|
# more than MAX (4) for test purposes
|
|
9: '{{{%s{%s,%s}},{%s,%s}},{{%s,%s},{%s,%s}}}'
|
|
}
|
|
|
|
|
|
@pytest.fixture
|
|
def offer_minsc_import(cap_story, dev, need_keypress):
|
|
def doit(config):
|
|
# upload the file, trigger import
|
|
file_len, sha = dev.upload_file(config.encode())
|
|
|
|
open('debug/last-config-msc.txt', 'wt').write(config)
|
|
dev.send_recv(CCProtocolPacker.miniscript_enroll(file_len, sha))
|
|
|
|
time.sleep(.2)
|
|
title, story = cap_story()
|
|
return title, story
|
|
|
|
return doit
|
|
|
|
|
|
@pytest.fixture
|
|
def import_miniscript(goto_home, pick_menu_item, cap_story, need_keypress,
|
|
nfc_write_text):
|
|
def doit(fname, way="sd", nfc_data=None):
|
|
goto_home()
|
|
pick_menu_item('Settings')
|
|
pick_menu_item('Miniscript')
|
|
if way == "nfc":
|
|
try:
|
|
pick_menu_item("Import via NFC")
|
|
except KeyError:
|
|
pytest.xfail(way)
|
|
|
|
time.sleep(.1)
|
|
nfc_write_text(nfc_data)
|
|
time.sleep(1)
|
|
return cap_story()
|
|
else:
|
|
pick_menu_item('Import from File')
|
|
time.sleep(.3)
|
|
_, story = cap_story()
|
|
if "Press (1) to import miniscript wallet file from SD Card" in story:
|
|
# in case Vdisk or NFC is enabled
|
|
if way == "sd":
|
|
need_keypress("1")
|
|
|
|
elif way == "vdisk":
|
|
if "ress (2)" not in story:
|
|
pytest.xfail(way)
|
|
|
|
need_keypress("2")
|
|
else:
|
|
if way != "sd":
|
|
pytest.xfail(way)
|
|
|
|
time.sleep(.3)
|
|
need_keypress("y")
|
|
pick_menu_item(fname)
|
|
time.sleep(.1)
|
|
return cap_story()
|
|
|
|
return doit
|
|
|
|
@pytest.fixture
|
|
def import_duplicate(import_miniscript, need_keypress, virtdisk_path, microsd_path):
|
|
def doit(fname, way="sd", nfc_data=None):
|
|
new_fpath = None
|
|
new_fname = None
|
|
path_f = microsd_path
|
|
if way == "vdisk":
|
|
path_f = virtdisk_path
|
|
|
|
title, story = import_miniscript(fname, way, nfc_data=nfc_data)
|
|
if "unique names" in story:
|
|
# trying to import duplicate with same name
|
|
# cannot get over name uniqueness requirement
|
|
# need to duplicate
|
|
if way == "nfc":
|
|
nfc_data["name"] = nfc_data["name"] + "-new"
|
|
else:
|
|
with open(path_f(fname), "r") as f:
|
|
res = f.read()
|
|
|
|
basename, ext = fname.split(".", 1)
|
|
new_fname = basename + "-new" + "." + ext
|
|
new_fpath = path_f(basename+"-new"+"."+ext)
|
|
with open(new_fpath, "w") as f:
|
|
f.write(res)
|
|
|
|
title, story = import_miniscript(new_fname, way, nfc_data=nfc_data)
|
|
|
|
assert "duplicate of already saved wallet" in story
|
|
assert "OK to approve" not in story
|
|
need_keypress("x")
|
|
|
|
if new_fpath:
|
|
os.remove(new_fpath)
|
|
|
|
return doit
|
|
|
|
@pytest.fixture
|
|
def miniscript_descriptors(goto_home, pick_menu_item, need_keypress, cap_story,
|
|
microsd_path):
|
|
def doit(minsc_name):
|
|
goto_home()
|
|
pick_menu_item("Settings")
|
|
pick_menu_item("Miniscript")
|
|
pick_menu_item(minsc_name)
|
|
pick_menu_item("Descriptors")
|
|
pick_menu_item("Export")
|
|
need_keypress("1") # internal and external separately
|
|
time.sleep(.2)
|
|
title, story = cap_story()
|
|
if "Press (1)" in story:
|
|
need_keypress("1")
|
|
time.sleep(.2)
|
|
title, story = cap_story()
|
|
|
|
assert "Miniscript file written" in story
|
|
fname = story.split("\n\n")[-1]
|
|
with open(microsd_path(fname), "r") as f:
|
|
cont = f.read()
|
|
external, internal = cont.split("\n")
|
|
return external, internal
|
|
return doit
|
|
|
|
|
|
@pytest.fixture
|
|
def usb_miniscript_get(dev):
|
|
def doit(name):
|
|
dev.check_mitm()
|
|
resp = dev.send_recv(CCProtocolPacker.miniscript_get(name))
|
|
return json.loads(resp)
|
|
|
|
return doit
|
|
|
|
|
|
@pytest.fixture
|
|
def usb_miniscript_delete(dev):
|
|
def doit(name):
|
|
dev.check_mitm()
|
|
dev.send_recv(CCProtocolPacker.miniscript_delete(name))
|
|
|
|
return doit
|
|
|
|
|
|
@pytest.fixture
|
|
def usb_miniscript_ls(dev):
|
|
def doit():
|
|
dev.check_mitm()
|
|
resp = dev.send_recv(CCProtocolPacker.miniscript_ls())
|
|
return json.loads(resp)
|
|
|
|
return doit
|
|
|
|
|
|
@pytest.fixture
|
|
def usb_miniscript_addr(dev):
|
|
def doit(name, index, change=False):
|
|
dev.check_mitm()
|
|
resp = dev.send_recv(CCProtocolPacker.miniscript_address(name, change, index))
|
|
return resp
|
|
|
|
return doit
|
|
|
|
|
|
@pytest.fixture
|
|
def get_cc_key(dev):
|
|
def doit(path, subderiv=None):
|
|
# cc device key
|
|
master_xfp_str = struct.pack('<I', dev.master_fingerprint).hex()
|
|
cc_key = dev.send_recv(CCProtocolPacker.get_xpub(path), timeout=None)
|
|
return f"[{master_xfp_str}/{path}]{cc_key}{subderiv if subderiv else '/<0;1>/*'}"
|
|
return doit
|
|
|
|
|
|
@pytest.fixture
|
|
def bitcoin_core_signer(bitcoind):
|
|
def doit(name="core_signer"):
|
|
# core signer
|
|
signer = bitcoind.create_wallet(wallet_name=name, disable_private_keys=False,
|
|
blank=False, passphrase=None, avoid_reuse=False,
|
|
descriptors=True)
|
|
target_desc = ""
|
|
bitcoind_descriptors = signer.listdescriptors()["descriptors"]
|
|
for d in bitcoind_descriptors:
|
|
if d["desc"].startswith("pkh(") and d["internal"] is False:
|
|
target_desc = d["desc"]
|
|
break
|
|
core_desc, checksum = target_desc.split("#")
|
|
core_key = core_desc[4:-1]
|
|
return signer, core_key
|
|
return doit
|
|
|
|
|
|
@pytest.fixture
|
|
def address_explorer_check(goto_home, pick_menu_item, need_keypress, cap_menu,
|
|
cap_story, load_export, miniscript_descriptors,
|
|
usb_miniscript_addr):
|
|
def doit(way, addr_fmt, wallet, cc_minsc_name, export_check=True):
|
|
goto_home()
|
|
pick_menu_item("Address Explorer")
|
|
need_keypress('4') # warning
|
|
m = cap_menu()
|
|
wal_name = m[-1]
|
|
pick_menu_item(wal_name)
|
|
|
|
title, story = cap_story()
|
|
if addr_fmt == "bech32m":
|
|
assert "Taproot internal key" in story
|
|
else:
|
|
assert "Taproot internal key" not in story
|
|
|
|
contents = load_export(way, label="Address summary", is_json=False, sig_check=False, vdisk_key="4")
|
|
addr_cont = contents.strip()
|
|
|
|
time.sleep(5)
|
|
title, story = cap_story()
|
|
assert "Press (6)" in story
|
|
assert "change addresses." in story
|
|
need_keypress("6")
|
|
time.sleep(5)
|
|
title, story = cap_story()
|
|
assert "Press (6)" not in story
|
|
assert "change addresses." not in story
|
|
|
|
contents_change = load_export(way, label="Address summary", is_json=False, sig_check=False, vdisk_key="4")
|
|
addr_cont_change = contents_change.strip()
|
|
|
|
if way == "nfc":
|
|
addr_range = [0, 9]
|
|
cc_addrs = addr_cont.split("\n")
|
|
cc_addrs_change = addr_cont_change.split("\n")
|
|
part_addr_index = 0
|
|
else:
|
|
addr_range = [0, 249]
|
|
cc_addrs_split = addr_cont.split("\n")
|
|
cc_addrs_split_change = addr_cont_change.split("\n")
|
|
# header is different for taproot
|
|
if addr_fmt == "bech32m":
|
|
assert "Internal Key" in cc_addrs_split[0]
|
|
assert "Taptree" in cc_addrs_split[0]
|
|
else:
|
|
assert "Internal Key" not in cc_addrs_split[0]
|
|
assert "Taptree" not in cc_addrs_split[0]
|
|
|
|
cc_addrs = cc_addrs_split[1:]
|
|
cc_addrs_change = cc_addrs_split_change[1:]
|
|
part_addr_index = 1
|
|
|
|
time.sleep(2)
|
|
|
|
internal_desc = None
|
|
external_desc = None
|
|
descriptors = wallet.listdescriptors()["descriptors"]
|
|
for desc in descriptors:
|
|
if desc["internal"]:
|
|
internal_desc = desc["desc"]
|
|
else:
|
|
external_desc = desc["desc"]
|
|
|
|
if export_check:
|
|
cc_external, cc_internal = miniscript_descriptors(cc_minsc_name)
|
|
assert cc_external.split("#")[0] == external_desc.split("#")[0].replace("'", "h")
|
|
assert cc_internal.split("#")[0] == internal_desc.split("#")[0].replace("'", "h")
|
|
|
|
bitcoind_addrs = wallet.deriveaddresses(external_desc, addr_range)
|
|
bitcoind_addrs_change = wallet.deriveaddresses(internal_desc, addr_range)
|
|
|
|
for cc, core in [(cc_addrs, bitcoind_addrs), (cc_addrs_change, bitcoind_addrs_change)]:
|
|
for idx, cc_item in enumerate(cc):
|
|
cc_item = cc_item.split(",")
|
|
partial_address = cc_item[part_addr_index]
|
|
_start, _end = partial_address.split("___")
|
|
if way != "nfc":
|
|
_start, _end = _start[1:], _end[:-1]
|
|
assert core[idx].startswith(_start)
|
|
assert core[idx].endswith(_end)
|
|
|
|
# check few USB addresses
|
|
for i in range(5):
|
|
addr = usb_miniscript_addr(cc_minsc_name, i, change=False)
|
|
time.sleep(.1)
|
|
title, story = cap_story()
|
|
assert addr in story
|
|
assert addr == bitcoind_addrs[i]
|
|
|
|
for i in range(5):
|
|
addr = usb_miniscript_addr(cc_minsc_name, i, change=True)
|
|
time.sleep(.1)
|
|
title, story = cap_story()
|
|
assert addr in story
|
|
assert addr == bitcoind_addrs_change[i]
|
|
|
|
return doit
|
|
|
|
|
|
@pytest.mark.bitcoind
|
|
@pytest.mark.parametrize("addr_fmt", ["bech32", "p2sh-segwit"])
|
|
@pytest.mark.parametrize("lt_type", ["older", "after"]) # this is actually not generated by liana (liana is relative only)
|
|
@pytest.mark.parametrize("recovery", [True, False])
|
|
# @pytest.mark.parametrize("lt_val", ["time", "block"]) TODO hard to test timebased
|
|
@pytest.mark.parametrize("minisc", [
|
|
"or_d(pk(@A),and_v(v:pkh(@B),locktime(N)))",
|
|
|
|
"or_d(pk(@A),and_v(v:pk(@B),locktime(N)))", # this is actually not generated by liana
|
|
|
|
"or_d(multi(2,@A,@C),and_v(v:pkh(@B),locktime(N)))",
|
|
|
|
"or_d(pk(@A),and_v(v:multi(2,@B,@C),locktime(N)))",
|
|
])
|
|
def test_liana_miniscripts_simple(addr_fmt, recovery, lt_type, minisc, clear_miniscript, goto_home,
|
|
need_keypress, pick_menu_item, cap_menu, cap_story, microsd_path,
|
|
use_regtest, bitcoind, microsd_wipe, load_export, dev,
|
|
address_explorer_check, get_cc_key, import_miniscript,
|
|
bitcoin_core_signer, import_duplicate):
|
|
normal_cosign_core = False
|
|
recovery_cosign_core = False
|
|
if "multi(" in minisc.split("),", 1)[0]:
|
|
normal_cosign_core = True
|
|
if "multi(" in minisc.split("),", 1)[-1]:
|
|
recovery_cosign_core = True
|
|
|
|
if lt_type == "older":
|
|
sequence = 5
|
|
locktime = 0
|
|
# 101 blocks are mined by default
|
|
to_replace = "older(5)"
|
|
else:
|
|
sequence = None
|
|
locktime = 105
|
|
to_replace = "after(105)"
|
|
|
|
minisc = minisc.replace("locktime(N)", to_replace)
|
|
|
|
if addr_fmt == "bech32":
|
|
desc = f"wsh({minisc})"
|
|
else:
|
|
desc = f"sh(wsh({minisc}))"
|
|
|
|
# core signer
|
|
signer0, core_key0 = bitcoin_core_signer("s0")
|
|
|
|
# cc device key
|
|
cc_key = get_cc_key("84h/0h/0h")
|
|
|
|
if recovery:
|
|
# recevoery path is always B
|
|
desc = desc.replace("@B", cc_key)
|
|
desc = desc.replace("@A", core_key0)
|
|
else:
|
|
desc = desc.replace("@A", cc_key)
|
|
desc = desc.replace("@B", core_key0)
|
|
|
|
if "@C" in desc:
|
|
signer1, core_key1 = bitcoin_core_signer("s1")
|
|
desc = desc.replace("@C", core_key1)
|
|
|
|
use_regtest()
|
|
clear_miniscript()
|
|
name = "core-miniscript"
|
|
fname = f"{name}.txt"
|
|
fpath = microsd_path(fname)
|
|
with open(fpath, "w") as f:
|
|
f.write(desc)
|
|
|
|
wo = bitcoind.create_wallet(wallet_name=name, disable_private_keys=True, blank=True,
|
|
passphrase=None, avoid_reuse=False, descriptors=True)
|
|
|
|
_, story = import_miniscript(fname)
|
|
assert "Create new miniscript wallet?" in story
|
|
# do some checks on policy --> helper function to replace keys with letters
|
|
need_keypress("y")
|
|
import_duplicate(fname)
|
|
menu = cap_menu()
|
|
assert menu[0] == name
|
|
pick_menu_item(menu[0]) # pick imported descriptor multisig wallet
|
|
pick_menu_item("Descriptors")
|
|
pick_menu_item("Bitcoin Core")
|
|
text = load_export("sd", label="Bitcoin Core miniscript", is_json=False, sig_check=False)
|
|
text = text.replace("importdescriptors ", "").strip()
|
|
# remove junk
|
|
r1 = text.find("[")
|
|
r2 = text.find("]", -1, 0)
|
|
text = text[r1: r2]
|
|
core_desc_object = json.loads(text)
|
|
res = wo.importdescriptors(core_desc_object)
|
|
for obj in res:
|
|
assert obj["success"]
|
|
addr = wo.getnewaddress("", addr_fmt)
|
|
addr_dest = wo.getnewaddress("", addr_fmt) # self-spend
|
|
assert bitcoind.supply_wallet.sendtoaddress(addr, 49)
|
|
bitcoind.supply_wallet.generatetoaddress(1, bitcoind.supply_wallet.getnewaddress())
|
|
all_of_it = wo.getbalance()
|
|
unspent = wo.listunspent()
|
|
assert len(unspent) == 1
|
|
inp = {"txid": unspent[0]["txid"], "vout": unspent[0]["vout"]}
|
|
if recovery and sequence:
|
|
inp["sequence"] = sequence
|
|
psbt_resp = wo.walletcreatefundedpsbt(
|
|
[inp],
|
|
[{addr_dest: all_of_it - 1}],
|
|
locktime if recovery else 0,
|
|
{"fee_rate": 20, "change_type": addr_fmt, "subtractFeeFromOutputs": [0]},
|
|
)
|
|
psbt = psbt_resp.get("psbt")
|
|
|
|
if normal_cosign_core or recovery_cosign_core:
|
|
psbt = signer1.walletprocesspsbt(psbt, True, "ALL")["psbt"]
|
|
|
|
name = f"{name}.psbt"
|
|
with open(microsd_path(name), "w") as f:
|
|
f.write(psbt)
|
|
goto_home()
|
|
pick_menu_item("Ready To Sign")
|
|
time.sleep(0.5)
|
|
title, story = cap_story()
|
|
if "Choose PSBT file to be signed" in story:
|
|
need_keypress("y")
|
|
time.sleep(0.1)
|
|
pick_menu_item(name)
|
|
time.sleep(0.1)
|
|
title, story = cap_story()
|
|
assert "OK TO SEND?" in title
|
|
assert "Consolidating" in story
|
|
need_keypress("y") # confirm signing
|
|
time.sleep(0.5)
|
|
title, story = cap_story()
|
|
assert "PSBT Signed" == title
|
|
assert "Updated PSBT is:" in story
|
|
need_keypress("y")
|
|
fname_psbt = story.split("\n\n")[1]
|
|
# fname_txn = story.split("\n\n")[3]
|
|
with open(microsd_path(fname_psbt), "r") as f:
|
|
final_psbt = f.read().strip()
|
|
# with open(microsd_path(fname_txn), "r") as f:
|
|
# final_txn = f.read().strip()
|
|
res = wo.finalizepsbt(final_psbt)
|
|
assert res["complete"]
|
|
tx_hex = res["hex"]
|
|
# assert tx_hex == final_txn
|
|
res = wo.testmempoolaccept([tx_hex])
|
|
if recovery:
|
|
assert not res[0]["allowed"]
|
|
assert res[0]["reject-reason"] == 'non-BIP68-final' if sequence else "non-final"
|
|
bitcoind.supply_wallet.generatetoaddress(6, bitcoind.supply_wallet.getnewaddress())
|
|
res = wo.testmempoolaccept([tx_hex])
|
|
assert res[0]["allowed"]
|
|
else:
|
|
assert res[0]["allowed"]
|
|
|
|
res = wo.sendrawtransaction(tx_hex)
|
|
assert len(res) == 64 # tx id
|
|
|
|
# check addresses
|
|
address_explorer_check("sd", addr_fmt, wo, "core-miniscript")
|
|
|
|
|
|
@pytest.mark.parametrize("addr_fmt", ["bech32", "p2sh-segwit"])
|
|
@pytest.mark.parametrize("minsc", [
|
|
("or_i(and_v(v:pkh($0),older(10)),or_d(multi(3,@A,@B,@C),and_v(v:thresh(2,pkh($1),a:pkh($2),a:pkh($3)),older(5))))", 0),
|
|
("or_i(and_v(v:pkh(@A),older(10)),or_d(multi(3,$0,$1,$2),and_v(v:thresh(2,pkh($3),a:pkh($4),a:pkh($5)),older(5))))", 10),
|
|
("or_i(and_v(v:pkh($0),older(10)),or_d(multi(3,$1,$2,$3),and_v(v:thresh(2,pkh(@A),a:pkh(@B),a:pkh($4)),older(5))))", 5),
|
|
])
|
|
def test_liana_miniscripts_complex(addr_fmt, minsc, bitcoind, use_regtest, clear_miniscript,
|
|
microsd_path, pick_menu_item, need_keypress, cap_story,
|
|
load_export, goto_home, address_explorer_check, cap_menu,
|
|
get_cc_key, import_miniscript, bitcoin_core_signer,
|
|
import_duplicate):
|
|
use_regtest()
|
|
clear_miniscript()
|
|
|
|
minsc, to_gen = minsc
|
|
signer_keys = minsc.count("@")
|
|
bsigners = signer_keys - 1
|
|
random_keys = minsc.count("$")
|
|
bitcoind_signers = []
|
|
for i in range(random_keys + bsigners):
|
|
s, core_key = bitcoin_core_signer(f"co-signer-{i}")
|
|
bitcoind_signers.append((s, core_key))
|
|
|
|
cc_key = get_cc_key("m/84h/1h/0h")
|
|
minsc = minsc.replace("@A", cc_key)
|
|
|
|
use_signers = []
|
|
if bsigners == 2:
|
|
for ph, (s, key) in zip(["@B", "@C"], bitcoind_signers[:2]):
|
|
use_signers.append(s)
|
|
minsc = minsc.replace(ph, key)
|
|
for i, (s, key) in enumerate(bitcoind_signers[2:]):
|
|
ph = f"${i}"
|
|
minsc = minsc.replace(ph, key)
|
|
elif bsigners == 1:
|
|
use_signers.append(bitcoind_signers[0][0])
|
|
minsc = minsc.replace("@B", bitcoind_signers[0][1])
|
|
for i, (s, key) in enumerate(bitcoind_signers[1:]):
|
|
ph = f"${i}"
|
|
minsc = minsc.replace(ph, key)
|
|
elif bsigners == 0:
|
|
for i, (s, key) in enumerate(bitcoind_signers):
|
|
ph = f"${i}"
|
|
minsc = minsc.replace(ph, key)
|
|
else:
|
|
assert False
|
|
|
|
if addr_fmt == "bech32":
|
|
desc = f"wsh({minsc})"
|
|
else:
|
|
desc = f"sh(wsh({minsc}))"
|
|
|
|
name = "cmplx-miniscript"
|
|
fname = f"{name}.txt"
|
|
fpath = microsd_path(fname)
|
|
with open(fpath, "w") as f:
|
|
f.write(desc)
|
|
|
|
wo = bitcoind.create_wallet(wallet_name=name, disable_private_keys=True, blank=True,
|
|
passphrase=None, avoid_reuse=False, descriptors=True)
|
|
|
|
_, story = import_miniscript(fname)
|
|
assert "Create new miniscript wallet?" in story
|
|
# do some checks on policy --> helper function to replace keys with letters
|
|
need_keypress("y")
|
|
import_duplicate(fname)
|
|
menu = cap_menu()
|
|
assert menu[0] == name
|
|
pick_menu_item(menu[0]) # pick imported descriptor multisig wallet
|
|
pick_menu_item("Descriptors")
|
|
pick_menu_item("Bitcoin Core")
|
|
text = load_export("sd", label="Bitcoin Core miniscript", is_json=False, sig_check=False)
|
|
text = text.replace("importdescriptors ", "").strip()
|
|
# remove junk
|
|
r1 = text.find("[")
|
|
r2 = text.find("]", -1, 0)
|
|
text = text[r1: r2]
|
|
core_desc_object = json.loads(text)
|
|
res = wo.importdescriptors(core_desc_object)
|
|
for obj in res:
|
|
assert obj["success"]
|
|
|
|
addr = wo.getnewaddress("", addr_fmt)
|
|
addr_dest = wo.getnewaddress("", addr_fmt) # self-spend
|
|
assert bitcoind.supply_wallet.sendtoaddress(addr, 49)
|
|
bitcoind.supply_wallet.generatetoaddress(1, bitcoind.supply_wallet.getnewaddress())
|
|
unspent = wo.listunspent()
|
|
assert len(unspent) == 1
|
|
inp = {"txid": unspent[0]["txid"], "vout": unspent[0]["vout"]}
|
|
if to_gen:
|
|
inp["sequence"] = to_gen
|
|
|
|
psbt_resp = wo.walletcreatefundedpsbt(
|
|
[inp],
|
|
[{addr_dest: 1}],
|
|
0,
|
|
{"fee_rate": 20, "change_type": addr_fmt, "subtractFeeFromOutputs": [0]},
|
|
)
|
|
psbt = psbt_resp.get("psbt")
|
|
|
|
# cosingers signing first
|
|
for s in use_signers:
|
|
psbt = s.walletprocesspsbt(psbt, True, "ALL")["psbt"]
|
|
|
|
pname = f"{name}.psbt"
|
|
with open(microsd_path(pname), "w") as f:
|
|
f.write(psbt)
|
|
goto_home()
|
|
pick_menu_item("Ready To Sign")
|
|
time.sleep(0.5)
|
|
title, story = cap_story()
|
|
if "Choose PSBT file to be signed" in story:
|
|
need_keypress("y")
|
|
time.sleep(0.1)
|
|
pick_menu_item(pname)
|
|
time.sleep(0.1)
|
|
title, story = cap_story()
|
|
assert "OK TO SEND?" in title
|
|
assert "Consolidating" in story
|
|
need_keypress("y") # confirm signing
|
|
time.sleep(0.5)
|
|
title, story = cap_story()
|
|
assert "PSBT Signed" == title
|
|
assert "Updated PSBT is:" in story
|
|
need_keypress("y")
|
|
fname_psbt = story.split("\n\n")[1]
|
|
# fname_txn = story.split("\n\n")[3]
|
|
with open(microsd_path(fname_psbt), "r") as f:
|
|
final_psbt = f.read().strip()
|
|
# with open(microsd_path(fname_txn), "r") as f:
|
|
# final_txn = f.read().strip()
|
|
res = wo.finalizepsbt(final_psbt)
|
|
assert res["complete"]
|
|
tx_hex = res["hex"]
|
|
# assert tx_hex == final_txn
|
|
res = wo.testmempoolaccept([tx_hex])
|
|
if to_gen:
|
|
assert not res[0]["allowed"]
|
|
assert res[0]["reject-reason"] == 'non-BIP68-final'
|
|
bitcoind.supply_wallet.generatetoaddress(to_gen, bitcoind.supply_wallet.getnewaddress())
|
|
res = wo.testmempoolaccept([tx_hex])
|
|
assert res[0]["allowed"]
|
|
else:
|
|
assert res[0]["allowed"]
|
|
|
|
res = wo.sendrawtransaction(tx_hex)
|
|
assert len(res) == 64 # tx id
|
|
|
|
# check addresses
|
|
address_explorer_check("sd", addr_fmt, wo, name)
|
|
|
|
|
|
@pytest.fixture
|
|
def bitcoind_miniscript(bitcoind, need_keypress, cap_story, load_export,
|
|
pick_menu_item, goto_home, cap_menu, microsd_path,
|
|
use_regtest, get_cc_key, import_miniscript,
|
|
bitcoin_core_signer, import_duplicate):
|
|
def doit(M, N, script_type, internal_key=None, cc_account=0, funded=True, r=None,
|
|
tapscript_threshold=False, add_own_pk=False, same_account=False):
|
|
|
|
use_regtest()
|
|
bitcoind_signers = []
|
|
bitcoind_signers_xpubs = []
|
|
for i in range(N - 1):
|
|
s, core_key = bitcoin_core_signer(f"bitcoind--signer{i}")
|
|
s.keypoolrefill(10)
|
|
bitcoind_signers.append(s)
|
|
bitcoind_signers_xpubs.append(core_key)
|
|
|
|
# watch only wallet where multisig descriptor will be imported
|
|
ms = bitcoind.create_wallet(
|
|
wallet_name=f"watch_only_{script_type}_{M}of{N}", disable_private_keys=True,
|
|
blank=True, passphrase=None, avoid_reuse=False, descriptors=True
|
|
)
|
|
goto_home()
|
|
pick_menu_item('Settings')
|
|
pick_menu_item('Multisig Wallets')
|
|
pick_menu_item('Export XPUB')
|
|
time.sleep(0.5)
|
|
title, story = cap_story()
|
|
assert "extended public keys (XPUB) you would need to join a multisig wallet" in story
|
|
need_keypress("y")
|
|
need_keypress(str(cc_account)) # account
|
|
need_keypress("y")
|
|
xpub_obj = load_export("sd", label="Multisig XPUB", is_json=True, sig_check=False)
|
|
template = xpub_obj[script_type +"_desc"]
|
|
acct_deriv = xpub_obj[script_type + '_deriv']
|
|
|
|
if tapscript_threshold:
|
|
me = f"[{xpub_obj['xfp']}/{acct_deriv.replace('m/','')}]{xpub_obj[script_type]}/<0;1>/*"
|
|
signers_xp = [me] + bitcoind_signers_xpubs
|
|
assert len(signers_xp) == N
|
|
desc = f"tr({H},%s)"
|
|
if internal_key:
|
|
desc = desc.replace(H, internal_key)
|
|
elif r:
|
|
desc = desc.replace(H, f"r={r}")
|
|
|
|
scripts = []
|
|
for c in itertools.combinations(signers_xp, M):
|
|
tmplt = f"sortedmulti_a({M},{','.join(c)})"
|
|
scripts.append(tmplt)
|
|
|
|
if len(scripts) > 8:
|
|
while True:
|
|
# just some of them but at least one has to have my key
|
|
x = random.sample(scripts, 8)
|
|
if any(me in s for s in x):
|
|
scripts = x
|
|
break
|
|
|
|
if add_own_pk:
|
|
if len(scripts) < 8:
|
|
if same_account:
|
|
cc_key = get_cc_key("m/86h/1h/0h", subderiv="/<2;3>/*")
|
|
else:
|
|
cc_key = get_cc_key("m/86h/1h/1000h")
|
|
cc_pk_leaf = f"pk({cc_key})"
|
|
scripts.append(cc_pk_leaf)
|
|
else:
|
|
pytest.skip("Scripts full")
|
|
|
|
temp = TREE[len(scripts)]
|
|
temp = temp % tuple(scripts)
|
|
|
|
desc = desc % temp
|
|
|
|
else:
|
|
if add_own_pk:
|
|
if same_account:
|
|
ss = [get_cc_key("m/86h/1h/0h", subderiv="/<4;5>/*")] + bitcoind_signers_xpubs
|
|
cc_key = get_cc_key("m/86h/1h/0h", subderiv="/<6;7>/*")
|
|
else:
|
|
ss = [get_cc_key("m/86h/1h/0h")] + bitcoind_signers_xpubs
|
|
cc_key = get_cc_key("m/86h/1h/1000h")
|
|
|
|
tmplt = f"sortedmulti_a({M},{','.join(ss)})"
|
|
cc_pk_leaf = f"pk({cc_key})"
|
|
desc = f"tr({H},{{{tmplt},{cc_pk_leaf}}})"
|
|
else:
|
|
desc = template.replace("M", str(M), 1).replace("...", ",".join(bitcoind_signers_xpubs))
|
|
|
|
if internal_key:
|
|
desc = desc.replace(H, internal_key)
|
|
elif r:
|
|
desc = desc.replace(H, f"r={r}")
|
|
|
|
name = "minisc.txt"
|
|
with open(microsd_path(name), "w") as f:
|
|
f.write(desc + "\n")
|
|
_, story = import_miniscript(name)
|
|
assert "Create new miniscript wallet?" in story
|
|
assert name.split(".")[0] in story
|
|
if script_type == "p2tr":
|
|
assert "Taproot internal key" in story
|
|
assert "Taproot tree keys" in story
|
|
assert "Press (1) to see extended public keys" in story
|
|
if script_type == "p2wsh":
|
|
assert "P2WSH" in story
|
|
elif script_type == "p2sh":
|
|
assert "P2SH" in story
|
|
elif script_type == "p2tr":
|
|
assert "P2TR" in story
|
|
else:
|
|
assert "P2SH-P2WSH" in story
|
|
# assert "Derivation:\n Varies (2)" in story
|
|
need_keypress("y") # approve multisig import
|
|
if r == "@":
|
|
# unspendable key is generated randomly
|
|
# descriptors will differ
|
|
with pytest.raises(AssertionError):
|
|
import_duplicate(name)
|
|
else:
|
|
import_duplicate(name)
|
|
goto_home()
|
|
pick_menu_item('Settings')
|
|
pick_menu_item('Miniscript')
|
|
menu = cap_menu()
|
|
pick_menu_item(menu[0]) # pick imported descriptor multisig wallet
|
|
pick_menu_item("Descriptors")
|
|
pick_menu_item("Bitcoin Core")
|
|
text = load_export("sd", label="Bitcoin Core miniscript", is_json=False, sig_check=False)
|
|
text = text.replace("importdescriptors ", "").strip()
|
|
# remove junk
|
|
r1 = text.find("[")
|
|
r2 = text.find("]", -1, 0)
|
|
text = text[r1: r2]
|
|
core_desc_object = json.loads(text)
|
|
# import descriptors to watch only wallet
|
|
res = ms.importdescriptors(core_desc_object)
|
|
assert res[0]["success"]
|
|
assert res[1]["success"]
|
|
|
|
if r and r != "@":
|
|
from pysecp256k1.extrakeys import keypair_create, keypair_xonly_pub, xonly_pubkey_parse
|
|
from pysecp256k1.extrakeys import xonly_pubkey_tweak_add, xonly_pubkey_serialize, xonly_pubkey_from_pubkey
|
|
H_xo = xonly_pubkey_parse(bytes.fromhex(H))
|
|
r_bytes = bytes.fromhex(r)
|
|
kp = keypair_create(r_bytes)
|
|
kp_xo, kp_parity = keypair_xonly_pub(kp)
|
|
pk = xonly_pubkey_tweak_add(H_xo, xonly_pubkey_serialize(kp_xo))
|
|
xo, xo_parity = xonly_pubkey_from_pubkey(pk)
|
|
internal_key_bytes = xonly_pubkey_serialize(xo)
|
|
internal_key_hex = internal_key_bytes.hex()
|
|
assert internal_key_hex in core_desc_object[0]["desc"]
|
|
assert internal_key_hex in core_desc_object[1]["desc"]
|
|
|
|
if funded:
|
|
if script_type == "p2wsh":
|
|
addr_type = "bech32"
|
|
elif script_type == "p2tr":
|
|
addr_type = "bech32m"
|
|
elif script_type == "p2sh":
|
|
addr_type = "legacy"
|
|
else:
|
|
addr_type = "p2sh-segwit"
|
|
|
|
addr = ms.getnewaddress("", addr_type)
|
|
if script_type == "p2wsh":
|
|
sw = "bcrt1q"
|
|
elif script_type == "p2tr":
|
|
sw = "bcrt1p"
|
|
else:
|
|
sw = "2"
|
|
assert addr.startswith(sw)
|
|
# get some coins and fund above multisig address
|
|
bitcoind.supply_wallet.sendtoaddress(addr, 49)
|
|
bitcoind.supply_wallet.generatetoaddress(1, bitcoind.supply_wallet.getnewaddress()) # mine above
|
|
|
|
return ms, bitcoind_signers
|
|
|
|
return doit
|
|
|
|
|
|
@pytest.mark.bitcoind
|
|
@pytest.mark.parametrize("cc_first", [True, False])
|
|
@pytest.mark.parametrize("add_pk", [True, False])
|
|
@pytest.mark.parametrize("same_acct", [True, False])
|
|
@pytest.mark.parametrize("M_N", [(3,4),(4,5),(5,6)])
|
|
def test_tapscript(M_N, cc_first, clear_miniscript, goto_home, need_keypress, pick_menu_item,
|
|
cap_menu, cap_story, microsd_path, use_regtest, bitcoind, microsd_wipe,
|
|
load_export, bitcoind_miniscript, add_pk, same_acct, get_cc_key):
|
|
M, N = M_N
|
|
clear_miniscript()
|
|
microsd_wipe()
|
|
internal_key = None
|
|
if same_acct:
|
|
# provide internal key with same account derivation (change based derivation)
|
|
internal_key = get_cc_key("m/86h/1h/0h", subderiv='/<10;11>/*')
|
|
|
|
wo, signers = bitcoind_miniscript(M, N, "p2tr", tapscript_threshold=True,
|
|
add_own_pk=add_pk, internal_key=internal_key,
|
|
same_account=same_acct)
|
|
addr = wo.getnewaddress("", "bech32m")
|
|
bitcoind.supply_wallet.sendtoaddress(addr, 49)
|
|
bitcoind.supply_wallet.generatetoaddress(1, bitcoind.supply_wallet.getnewaddress())
|
|
conso_addr = wo.getnewaddress("conso", "bech32m")
|
|
psbt = wo.walletcreatefundedpsbt([], [{conso_addr:25}], 0, {"fee_rate": 2})["psbt"]
|
|
if not cc_first:
|
|
for s in signers[0:M-1]:
|
|
psbt = s.walletprocesspsbt(psbt, True, "DEFAULT")["psbt"]
|
|
with open(microsd_path("ts_tree.psbt"), "w") as f:
|
|
f.write(psbt)
|
|
time.sleep(2)
|
|
goto_home()
|
|
pick_menu_item("Ready To Sign")
|
|
time.sleep(0.2)
|
|
title, story = cap_story()
|
|
assert title == "OK TO SEND?"
|
|
need_keypress("y")
|
|
time.sleep(0.1)
|
|
title, story = cap_story()
|
|
assert title == "PSBT Signed"
|
|
fname = [i for i in story.split("\n\n") if ".psbt" in i][0]
|
|
with open(microsd_path(fname), "r") as f:
|
|
psbt = f.read().strip()
|
|
if cc_first:
|
|
# we MUST be able to finalize this without anyone else if add pk
|
|
if not add_pk:
|
|
for s in signers[0:M-1]:
|
|
psbt = s.walletprocesspsbt(psbt, True, "DEFAULT")["psbt"]
|
|
res = wo.finalizepsbt(psbt)
|
|
assert res["complete"] is True
|
|
accept_res = wo.testmempoolaccept([res["hex"]])[0]
|
|
assert accept_res["allowed"] is True
|
|
txid = wo.sendrawtransaction(res["hex"])
|
|
assert len(txid) == 64
|
|
|
|
|
|
@pytest.mark.bitcoind
|
|
@pytest.mark.parametrize("csa", [True, False])
|
|
@pytest.mark.parametrize("add_pk", [True, False])
|
|
@pytest.mark.parametrize('M_N', [(3, 15), (2, 2), (3, 5)])
|
|
@pytest.mark.parametrize('way', ["sd", "vdisk", "nfc"])
|
|
def test_bitcoind_tapscript_address(M_N, clear_miniscript, goto_home, need_keypress,
|
|
pick_menu_item, cap_menu, cap_story, make_multisig,
|
|
import_ms_wallet, microsd_path, bitcoind_miniscript,
|
|
use_regtest, load_export, way, csa, address_explorer_check,
|
|
add_pk):
|
|
use_regtest()
|
|
clear_miniscript()
|
|
M, N = M_N
|
|
ms_wo, _ = bitcoind_miniscript(M, N, "p2tr", funded=False, tapscript_threshold=csa,
|
|
add_own_pk=add_pk)
|
|
address_explorer_check(way, "bech32m", ms_wo, "minisc", export_check=False)
|
|
|
|
|
|
@pytest.mark.bitcoind
|
|
@pytest.mark.parametrize("cc_first", [True, False])
|
|
@pytest.mark.parametrize("m_n", [(2,2), (3, 5), (32, 32)])
|
|
@pytest.mark.parametrize("internal_key_spendable", [True, False, "77ec0c0fdb9733e6a3c753b1374c4a465cba80dff52fc196972640a26dd08b76", "@"])
|
|
def test_tapscript_multisig(cc_first, m_n, internal_key_spendable, use_regtest, bitcoind, goto_home, cap_menu,
|
|
need_keypress, pick_menu_item, cap_story, microsd_path, load_export, microsd_wipe, dev,
|
|
bitcoind_miniscript, clear_miniscript, get_cc_key):
|
|
M, N = m_n
|
|
clear_miniscript()
|
|
microsd_wipe()
|
|
internal_key = None
|
|
r = None
|
|
if internal_key_spendable is True:
|
|
internal_key = get_cc_key("86h/0h/3h")
|
|
elif isinstance(internal_key_spendable, str) and len(internal_key_spendable) == 64:
|
|
r = internal_key_spendable
|
|
elif internal_key_spendable == "@":
|
|
r = "@"
|
|
|
|
tapscript_wo, bitcoind_signers = bitcoind_miniscript(M, N, "p2tr", internal_key=internal_key, r=r)
|
|
|
|
dest_addr = tapscript_wo.getnewaddress("", "bech32m")
|
|
psbt = tapscript_wo.walletcreatefundedpsbt([], [{dest_addr: 1.0}], 0, {"fee_rate": 20})["psbt"]
|
|
fname = "tapscript.psbt"
|
|
if not cc_first:
|
|
# bitcoind cosigner sigs first
|
|
for i in range(M - 1):
|
|
signer = bitcoind_signers[i]
|
|
psbt = signer.walletprocesspsbt(psbt, True, "DEFAULT", True)["psbt"]
|
|
with open(microsd_path(fname), "w") as f:
|
|
f.write(psbt)
|
|
goto_home()
|
|
# bug in goto_home ?
|
|
need_keypress("x")
|
|
time.sleep(0.1)
|
|
# CC signing
|
|
need_keypress("y")
|
|
time.sleep(0.1)
|
|
title, story = cap_story()
|
|
if "Choose" in story:
|
|
need_keypress("y")
|
|
pick_menu_item(fname)
|
|
time.sleep(0.1)
|
|
title, story = cap_story()
|
|
assert title == "OK TO SEND?"
|
|
need_keypress("y")
|
|
time.sleep(0.1)
|
|
title, story = cap_story()
|
|
split_story = story.split("\n\n")
|
|
cc_tx_id = None
|
|
if "(ready for broadcast)" in story:
|
|
signed_fname = split_story[1]
|
|
signed_txn_fname = split_story[-2]
|
|
cc_tx_id = split_story[-1].split("\n")[-1]
|
|
with open(microsd_path(signed_txn_fname), "r") as f:
|
|
signed_txn = f.read().strip()
|
|
else:
|
|
signed_fname = split_story[-1]
|
|
|
|
with open(microsd_path(signed_fname), "r") as f:
|
|
signed_psbt = f.read().strip()
|
|
|
|
if cc_first:
|
|
for signer in bitcoind_signers:
|
|
signed_psbt = signer.walletprocesspsbt(signed_psbt, True, "DEFAULT", True)["psbt"]
|
|
res = tapscript_wo.finalizepsbt(signed_psbt, True)
|
|
assert res['complete']
|
|
tx_hex = res["hex"]
|
|
res = bitcoind.supply_wallet.testmempoolaccept([tx_hex])
|
|
assert res[0]["allowed"]
|
|
txn_id = bitcoind.supply_wallet.sendrawtransaction(tx_hex)
|
|
if cc_tx_id:
|
|
assert tx_hex == signed_txn
|
|
assert txn_id == cc_tx_id
|
|
assert len(txn_id) == 64
|
|
|
|
|
|
@pytest.mark.parametrize("num_leafs", [1, 2, 5, 8])
|
|
@pytest.mark.parametrize("internal_key_spendable", [True, False])
|
|
def test_tapscript_pk(num_leafs, use_regtest, clear_miniscript, microsd_wipe, bitcoind,
|
|
internal_key_spendable, dev, microsd_path, need_keypress, get_cc_key,
|
|
pick_menu_item, cap_story, goto_home, cap_menu, load_export,
|
|
import_miniscript, bitcoin_core_signer, import_duplicate):
|
|
use_regtest()
|
|
clear_miniscript()
|
|
microsd_wipe()
|
|
tmplt = TREE[num_leafs]
|
|
bitcoind_signers_xpubs = []
|
|
bitcoind_signers = []
|
|
for i in range(num_leafs):
|
|
s, core_key = bitcoin_core_signer(f"bitcoind--signer{i}")
|
|
bitcoind_signers.append(s)
|
|
bitcoind_signers_xpubs.append(core_key)
|
|
|
|
bitcoin_signer_leafs = [f"pk({k})" for k in bitcoind_signers_xpubs]
|
|
|
|
cc_key = get_cc_key("86h/0h/100h")
|
|
cc_leaf = f"pk({cc_key})"
|
|
|
|
if internal_key_spendable:
|
|
desc = f"tr({cc_key},{tmplt % (*bitcoin_signer_leafs,)})"
|
|
else:
|
|
internal_key = bitcoind_signers_xpubs[0]
|
|
leafs = bitcoin_signer_leafs[1:] + [cc_leaf]
|
|
random.shuffle(leafs)
|
|
desc = f"tr({internal_key},{tmplt % (*leafs,)})"
|
|
|
|
ts = bitcoind.create_wallet(
|
|
wallet_name=f"watch_only_pk_ts", disable_private_keys=True,
|
|
blank=True, passphrase=None, avoid_reuse=False, descriptors=True
|
|
)
|
|
|
|
fname = "ts_pk.txt"
|
|
with open(microsd_path(fname), "w") as f:
|
|
f.write(desc + "\n")
|
|
_, story = import_miniscript(fname)
|
|
assert "Create new miniscript wallet?" in story
|
|
assert fname.split(".")[0] in story
|
|
assert "Taproot internal key" in story
|
|
assert "Taproot tree keys" in story
|
|
assert "Press (1) to see extended public keys" in story
|
|
assert "P2TR" in story
|
|
|
|
need_keypress("y")
|
|
import_duplicate(fname)
|
|
goto_home()
|
|
pick_menu_item('Settings')
|
|
pick_menu_item('Miniscript')
|
|
menu = cap_menu()
|
|
pick_menu_item(menu[0])
|
|
pick_menu_item("Descriptors")
|
|
pick_menu_item("Bitcoin Core")
|
|
text = load_export("sd", label="Bitcoin Core miniscript", is_json=False, sig_check=False)
|
|
text = text.replace("importdescriptors ", "").strip()
|
|
# remove junk
|
|
r1 = text.find("[")
|
|
r2 = text.find("]", -1, 0)
|
|
text = text[r1: r2]
|
|
core_desc_object = json.loads(text)
|
|
# import descriptors to watch only wallet
|
|
res = ts.importdescriptors(core_desc_object)
|
|
assert res[0]["success"]
|
|
assert res[1]["success"]
|
|
|
|
addr = ts.getnewaddress("", "bech32m")
|
|
assert bitcoind.supply_wallet.sendtoaddress(addr, 49)
|
|
bitcoind.supply_wallet.generatetoaddress(1, bitcoind.supply_wallet.getnewaddress())
|
|
|
|
dest_addr = ts.getnewaddress("", "bech32m") # selfspend
|
|
psbt = ts.walletcreatefundedpsbt([], [{dest_addr: 1.0}], 0, {"fee_rate": 2})["psbt"]
|
|
fname = "ts_pk.psbt"
|
|
with open(microsd_path(fname), "w") as f:
|
|
f.write(psbt)
|
|
|
|
goto_home()
|
|
pick_menu_item("Ready To Sign")
|
|
time.sleep(0.5)
|
|
title, story = cap_story()
|
|
if "Choose PSBT file to be signed" in story:
|
|
need_keypress("y")
|
|
time.sleep(0.1)
|
|
pick_menu_item(fname)
|
|
time.sleep(0.1)
|
|
title, story = cap_story()
|
|
assert "OK TO SEND?" in title
|
|
assert "Consolidating" in story
|
|
need_keypress("y") # confirm signing
|
|
time.sleep(0.5)
|
|
title, story = cap_story()
|
|
assert "PSBT Signed" == title
|
|
assert "Updated PSBT is:" in story
|
|
need_keypress("y")
|
|
fname_psbt = story.split("\n\n")[1]
|
|
# fname_txn = story.split("\n\n")[3]
|
|
with open(microsd_path(fname_psbt), "r") as f:
|
|
final_psbt = f.read().strip()
|
|
# with open(microsd_path(fname_txn), "r") as f:
|
|
# final_txn = f.read().strip()
|
|
res = ts.finalizepsbt(final_psbt)
|
|
assert res["complete"]
|
|
tx_hex = res["hex"]
|
|
# assert tx_hex == final_txn
|
|
res = ts.testmempoolaccept([tx_hex])
|
|
assert res[0]["allowed"]
|
|
txn_id = bitcoind.supply_wallet.sendrawtransaction(tx_hex)
|
|
assert txn_id
|
|
|
|
|
|
@pytest.mark.parametrize("desc", [
|
|
"tr(50929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0,{{sortedmulti_a(2,[0f056943/48h/1h/0h/3h]tpubDF2rnouQaaYrY6CUWTapYkeFEs3h3qrzL4M52ZGoPeU9dkarJMtrw6VF1zJRGuGuAFxYS3kXtavfAwQPTQkU5dyNYpbgxcpftrR8H3U85Ez/<0;1>/*,[b7fe820c/48h/1h/0h/3h]tpubDFdQ1sNV53TbogAMPEd2egY5NXfbdKD1Mnr2iBrJrcwRHJbKC7tuuUMHT8SSHJ2VEKdCf5WYBMfevvWCnyJV53gYUT2wFyxEV8SuUTedBp7/<0;1>/*),sortedmulti_a(2,[0f056943/48h/1h/0h/3h]tpubDF2rnouQaaYrY6CUWTapYkeFEs3h3qrzL4M52ZGoPeU9dkarJMtrw6VF1zJRGuGuAFxYS3kXtavfAwQPTQkU5dyNYpbgxcpftrR8H3U85Ez/<0;1>/*,[30afbe54/48h/1h/0h/3h]tpubDFLVv7cuiLjn3QcsCend5kn3yw5sx6Czazy7hZvdGX61v8pkU95k2Byz9M5jnabzeUg7qWtHYLeKQyCWWAHhUmQQMeZ4Dee2CfGR2TsZqrN/<0;1>/*)},sortedmulti_a(2,[b7fe820c/48h/1h/0h/3h]tpubDFdQ1sNV53TbogAMPEd2egY5NXfbdKD1Mnr2iBrJrcwRHJbKC7tuuUMHT8SSHJ2VEKdCf5WYBMfevvWCnyJV53gYUT2wFyxEV8SuUTedBp7/<0;1>/*,[30afbe54/48h/1h/0h/3h]tpubDFLVv7cuiLjn3QcsCend5kn3yw5sx6Czazy7hZvdGX61v8pkU95k2Byz9M5jnabzeUg7qWtHYLeKQyCWWAHhUmQQMeZ4Dee2CfGR2TsZqrN/<0;1>/*)})#tpm3afjn",
|
|
"tr(50929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0,{sortedmulti_a(2,[b7fe820c/48'/1'/0'/3']tpubDFdQ1sNV53TbogAMPEd2egY5NXfbdKD1Mnr2iBrJrcwRHJbKC7tuuUMHT8SSHJ2VEKdCf5WYBMfevvWCnyJV53gYUT2wFyxEV8SuUTedBp7/0/*,[30afbe54/48'/1'/0'/3']tpubDFLVv7cuiLjn3QcsCend5kn3yw5sx6Czazy7hZvdGX61v8pkU95k2Byz9M5jnabzeUg7qWtHYLeKQyCWWAHhUmQQMeZ4Dee2CfGR2TsZqrN/0/*),{sortedmulti_a(2,[0f056943/48'/1'/0'/3']tpubDF2rnouQaaYrY6CUWTapYkeFEs3h3qrzL4M52ZGoPeU9dkarJMtrw6VF1zJRGuGuAFxYS3kXtavfAwQPTQkU5dyNYpbgxcpftrR8H3U85Ez/0/*,[b7fe820c/48'/1'/0'/3']tpubDFdQ1sNV53TbogAMPEd2egY5NXfbdKD1Mnr2iBrJrcwRHJbKC7tuuUMHT8SSHJ2VEKdCf5WYBMfevvWCnyJV53gYUT2wFyxEV8SuUTedBp7/0/*),sortedmulti_a(2,[0f056943/48'/1'/0'/3']tpubDF2rnouQaaYrY6CUWTapYkeFEs3h3qrzL4M52ZGoPeU9dkarJMtrw6VF1zJRGuGuAFxYS3kXtavfAwQPTQkU5dyNYpbgxcpftrR8H3U85Ez/0/*,[30afbe54/48'/1'/0'/3']tpubDFLVv7cuiLjn3QcsCend5kn3yw5sx6Czazy7hZvdGX61v8pkU95k2Byz9M5jnabzeUg7qWtHYLeKQyCWWAHhUmQQMeZ4Dee2CfGR2TsZqrN/0/*)}})",
|
|
"tr(50929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0,{{sortedmulti_a(2,[0f056943/48'/1'/0'/3']tpubDF2rnouQaaYrY6CUWTapYkeFEs3h3qrzL4M52ZGoPeU9dkarJMtrw6VF1zJRGuGuAFxYS3kXtavfAwQPTQkU5dyNYpbgxcpftrR8H3U85Ez/0/*,[b7fe820c/48'/1'/0'/3']tpubDFdQ1sNV53TbogAMPEd2egY5NXfbdKD1Mnr2iBrJrcwRHJbKC7tuuUMHT8SSHJ2VEKdCf5WYBMfevvWCnyJV53gYUT2wFyxEV8SuUTedBp7/0/*),sortedmulti_a(2,[0f056943/48'/1'/0'/3']tpubDF2rnouQaaYrY6CUWTapYkeFEs3h3qrzL4M52ZGoPeU9dkarJMtrw6VF1zJRGuGuAFxYS3kXtavfAwQPTQkU5dyNYpbgxcpftrR8H3U85Ez/0/*,[30afbe54/48'/1'/0'/3']tpubDFLVv7cuiLjn3QcsCend5kn3yw5sx6Czazy7hZvdGX61v8pkU95k2Byz9M5jnabzeUg7qWtHYLeKQyCWWAHhUmQQMeZ4Dee2CfGR2TsZqrN/0/*)},sortedmulti_a(2,[b7fe820c/48'/1'/0'/3']tpubDFdQ1sNV53TbogAMPEd2egY5NXfbdKD1Mnr2iBrJrcwRHJbKC7tuuUMHT8SSHJ2VEKdCf5WYBMfevvWCnyJV53gYUT2wFyxEV8SuUTedBp7/0/*,[30afbe54/48'/1'/0'/3']tpubDFLVv7cuiLjn3QcsCend5kn3yw5sx6Czazy7hZvdGX61v8pkU95k2Byz9M5jnabzeUg7qWtHYLeKQyCWWAHhUmQQMeZ4Dee2CfGR2TsZqrN/0/*)})",
|
|
"tr(50929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0,{{sortedmulti_a(2,[0f056943/48'/1'/0'/3']tpubDF2rnouQaaYrY6CUWTapYkeFEs3h3qrzL4M52ZGoPeU9dkarJMtrw6VF1zJRGuGuAFxYS3kXtavfAwQPTQkU5dyNYpbgxcpftrR8H3U85Ez/0/*,[b7fe820c/48'/1'/0'/3']tpubDFdQ1sNV53TbogAMPEd2egY5NXfbdKD1Mnr2iBrJrcwRHJbKC7tuuUMHT8SSHJ2VEKdCf5WYBMfevvWCnyJV53gYUT2wFyxEV8SuUTedBp7/0/*),sortedmulti_a(2,[0f056943/48'/1'/0'/3']tpubDF2rnouQaaYrY6CUWTapYkeFEs3h3qrzL4M52ZGoPeU9dkarJMtrw6VF1zJRGuGuAFxYS3kXtavfAwQPTQkU5dyNYpbgxcpftrR8H3U85Ez/0/*,[30afbe54/48'/1'/0'/3']tpubDFLVv7cuiLjn3QcsCend5kn3yw5sx6Czazy7hZvdGX61v8pkU95k2Byz9M5jnabzeUg7qWtHYLeKQyCWWAHhUmQQMeZ4Dee2CfGR2TsZqrN/0/*)},or_d(pk([0f056943/48'/1'/0'/3']tpubDF2rnouQaaYrY6CUWTapYkeFEs3h3qrzL4M52ZGoPeU9dkarJMtrw6VF1zJRGuGuAFxYS3kXtavfAwQPTQkU5dyNYpbgxcpftrR8H3U85Ez/0/*),and_v(v:pkh([30afbe54/48'/1'/0'/3']tpubDFLVv7cuiLjn3QcsCend5kn3yw5sx6Czazy7hZvdGX61v8pkU95k2Byz9M5jnabzeUg7qWtHYLeKQyCWWAHhUmQQMeZ4Dee2CfGR2TsZqrN/0/*),older(500)))})",
|
|
])
|
|
def test_tapscript_import_export(clear_miniscript, pick_menu_item, cap_story, need_keypress,
|
|
import_miniscript, load_export, desc, microsd_path):
|
|
clear_miniscript()
|
|
fname = "imdesc.txt"
|
|
with open(microsd_path(fname), "w") as f:
|
|
f.write(desc)
|
|
_, story = import_miniscript(fname)
|
|
need_keypress("y") # approve miniscript import
|
|
pick_menu_item(fname.split(".")[0])
|
|
pick_menu_item("Descriptors")
|
|
pick_menu_item("Export")
|
|
time.sleep(.1)
|
|
title, story = cap_story()
|
|
assert "(<0;1> notation) press OK" in story
|
|
need_keypress("y")
|
|
contents = load_export("sd", label="Miniscript", is_json=False, addr_fmt=AF_P2TR,
|
|
sig_check=False)
|
|
descriptor = contents.strip()
|
|
assert desc.split("#")[0].replace("<0;1>/*", "0/*").replace("'", "h") == descriptor.split("#")[0].replace("<0;1>/*", "0/*").replace("'", "h")
|
|
|
|
|
|
def test_duplicate_tapscript_leaves(use_regtest, clear_miniscript, microsd_wipe, bitcoind, dev,
|
|
goto_home, pick_menu_item, need_keypress, microsd_path,
|
|
cap_story, load_export, get_cc_key, import_miniscript,
|
|
bitcoin_core_signer, import_duplicate):
|
|
# works in core - but some discussions are ongoing
|
|
# https://github.com/bitcoin/bitcoin/issues/27104
|
|
# CC also allows this for now... (experimental branch)
|
|
use_regtest()
|
|
clear_miniscript()
|
|
microsd_wipe()
|
|
ss, core_key = bitcoin_core_signer(f"dup_leafs")
|
|
|
|
cc_key = get_cc_key("86h/0h/100h")
|
|
cc_leaf = f"pk({cc_key})"
|
|
|
|
tmplt = TREE[2]
|
|
tmplt = tmplt % (cc_leaf, cc_leaf)
|
|
desc = f"tr({core_key},{tmplt})"
|
|
fname = "dup_leafs.txt"
|
|
with open(microsd_path(fname), "w") as f:
|
|
f.write(desc)
|
|
|
|
_, story = import_miniscript(fname)
|
|
assert "Create new miniscript wallet?" in story
|
|
assert fname.split(".")[0] in story
|
|
assert "Taproot internal key" in story
|
|
assert "Taproot tree keys" in story
|
|
assert "Press (1) to see extended public keys" in story
|
|
assert "P2TR" in story
|
|
|
|
need_keypress("y")
|
|
import_duplicate(fname)
|
|
goto_home()
|
|
pick_menu_item('Settings')
|
|
pick_menu_item('Miniscript')
|
|
pick_menu_item(fname.split(".")[0])
|
|
pick_menu_item("Descriptors")
|
|
pick_menu_item("Bitcoin Core")
|
|
text = load_export("sd", label="Bitcoin Core miniscript", is_json=False, sig_check=False)
|
|
text = text.replace("importdescriptors ", "").strip()
|
|
# remove junk
|
|
r1 = text.find("[")
|
|
r2 = text.find("]", -1, 0)
|
|
text = text[r1: r2]
|
|
core_desc_object = json.loads(text)
|
|
# wo wallet
|
|
ts = bitcoind.create_wallet(
|
|
wallet_name=f"dup_leafs_wo", disable_private_keys=True,
|
|
blank=True, passphrase=None, avoid_reuse=False, descriptors=True
|
|
)
|
|
# import descriptors to watch only wallet
|
|
res = ts.importdescriptors(core_desc_object)
|
|
assert res[0]["success"]
|
|
assert res[1]["success"]
|
|
|
|
addr = ts.getnewaddress("", "bech32m")
|
|
assert bitcoind.supply_wallet.sendtoaddress(addr, 49)
|
|
bitcoind.supply_wallet.generatetoaddress(1, bitcoind.supply_wallet.getnewaddress())
|
|
|
|
dest_addr = ts.getnewaddress("", "bech32m") # selfspend
|
|
psbt = ts.walletcreatefundedpsbt([], [{dest_addr: 1.0}], 0, {"fee_rate": 2})["psbt"]
|
|
fname = "ts_pk.psbt"
|
|
with open(microsd_path(fname), "w") as f:
|
|
f.write(psbt)
|
|
|
|
goto_home()
|
|
pick_menu_item("Ready To Sign")
|
|
time.sleep(0.5)
|
|
title, story = cap_story()
|
|
if "Choose PSBT file to be signed" in story:
|
|
need_keypress("y")
|
|
time.sleep(0.1)
|
|
pick_menu_item(fname)
|
|
time.sleep(0.1)
|
|
title, story = cap_story()
|
|
assert "OK TO SEND?" in title
|
|
assert "Consolidating" in story
|
|
need_keypress("y") # confirm signing
|
|
time.sleep(0.5)
|
|
title, story = cap_story()
|
|
assert "PSBT Signed" == title
|
|
assert "Updated PSBT is:" in story
|
|
need_keypress("y")
|
|
fname_psbt = story.split("\n\n")[1]
|
|
# fname_txn = story.split("\n\n")[3]
|
|
with open(microsd_path(fname_psbt), "r") as f:
|
|
final_psbt = f.read().strip()
|
|
# with open(microsd_path(fname_txn), "r") as f:
|
|
# final_txn = f.read().strip()
|
|
res = ts.finalizepsbt(final_psbt)
|
|
assert res["complete"]
|
|
tx_hex = res["hex"]
|
|
# assert tx_hex == final_txn
|
|
res = ts.testmempoolaccept([tx_hex])
|
|
assert res[0]["allowed"]
|
|
txn_id = bitcoind.supply_wallet.sendrawtransaction(tx_hex)
|
|
assert txn_id
|
|
|
|
|
|
def test_same_key_account_based_minisc(goto_home, need_keypress, pick_menu_item, cap_story,
|
|
clear_miniscript, microsd_path, load_export, bitcoind,
|
|
import_miniscript, use_regtest, import_duplicate):
|
|
clear_miniscript()
|
|
use_regtest()
|
|
|
|
desc = ("wsh("
|
|
"or_d(pk([0f056943/84'/1'/0']tpubDC7jGaaSE66Pn4dgtbAAstde4bCyhSUs4r3P8WhMVvPByvcRrzrwqSvpF9Ghx83Z1LfVugGRrSBko5UEKELCz9HoMv5qKmGq3fqnnbS5E9r/<0;1>/*),"
|
|
"and_v("
|
|
"v:pkh([0f056943/84'/1'/9']tpubDC7jGaaSE66QBAcX8TUD3JKWari1zmGH4gNyKZcrfq6NwCofKujNF2kyeVXgKshotxw5Yib8UxLrmmCmWd8NVPVTAL8rGfMdc7TsAKqsy6y/<0;1>/*),"
|
|
"older(5))))#qmwvph5c")
|
|
|
|
name = "mini-accounts"
|
|
fname = f"{name}.txt"
|
|
with open(microsd_path(fname), "w") as f:
|
|
f.write(desc)
|
|
|
|
_, story = import_miniscript(fname)
|
|
assert "Create new miniscript wallet?" in story
|
|
assert fname.split(".")[0] in story
|
|
assert "Press (1) to see extended public keys" in story
|
|
|
|
need_keypress("y")
|
|
import_duplicate(fname)
|
|
goto_home()
|
|
pick_menu_item('Settings')
|
|
pick_menu_item('Miniscript')
|
|
pick_menu_item(fname.split(".")[0])
|
|
pick_menu_item("Descriptors")
|
|
pick_menu_item("Bitcoin Core")
|
|
text = load_export("sd", label="Bitcoin Core miniscript", is_json=False, sig_check=False)
|
|
text = text.replace("importdescriptors ", "").strip()
|
|
# remove junk
|
|
r1 = text.find("[")
|
|
r2 = text.find("]", -1, 0)
|
|
text = text[r1: r2]
|
|
core_desc_object = json.loads(text)
|
|
# wo wallet
|
|
wo = bitcoind.create_wallet(
|
|
wallet_name=f"multi-account", disable_private_keys=True,
|
|
blank=True, passphrase=None, avoid_reuse=False, descriptors=True
|
|
)
|
|
# import descriptors to watch only wallet
|
|
res = wo.importdescriptors(core_desc_object)
|
|
assert res[0]["success"]
|
|
assert res[1]["success"]
|
|
|
|
addr = wo.getnewaddress("", "bech32")
|
|
assert bitcoind.supply_wallet.sendtoaddress(addr, 49)
|
|
bitcoind.supply_wallet.generatetoaddress(1, bitcoind.supply_wallet.getnewaddress())
|
|
|
|
dest_addr = wo.getnewaddress("", "bech32") # selfspend
|
|
psbt = wo.walletcreatefundedpsbt([], [{dest_addr: 1.0}], 0, {"fee_rate": 2})["psbt"]
|
|
fname = "multi-acct.psbt"
|
|
with open(microsd_path(fname), "w") as f:
|
|
f.write(psbt)
|
|
|
|
goto_home()
|
|
pick_menu_item("Ready To Sign")
|
|
time.sleep(0.5)
|
|
title, story = cap_story()
|
|
if "Choose PSBT file to be signed" in story:
|
|
need_keypress("y")
|
|
time.sleep(0.1)
|
|
pick_menu_item(fname)
|
|
time.sleep(0.1)
|
|
title, story = cap_story()
|
|
assert "OK TO SEND?" in title
|
|
assert "Consolidating" in story
|
|
need_keypress("y") # confirm signing
|
|
time.sleep(0.5)
|
|
title, story = cap_story()
|
|
assert "PSBT Signed" == title
|
|
assert "Updated PSBT is:" in story
|
|
need_keypress("y")
|
|
fname_psbt = story.split("\n\n")[1]
|
|
# fname_txn = story.split("\n\n")[3]
|
|
with open(microsd_path(fname_psbt), "r") as f:
|
|
final_psbt = f.read().strip()
|
|
|
|
_psbt = BasicPSBT().parse(final_psbt.encode())
|
|
assert len(_psbt.inputs[0].part_sigs) == 2
|
|
# with open(microsd_path(fname_txn), "r") as f:
|
|
# final_txn = f.read().strip()
|
|
res = wo.finalizepsbt(final_psbt)
|
|
assert res["complete"]
|
|
tx_hex = res["hex"]
|
|
# assert tx_hex == final_txn
|
|
res = wo.testmempoolaccept([tx_hex])
|
|
assert res[0]["allowed"]
|
|
txn_id = bitcoind.supply_wallet.sendrawtransaction(tx_hex)
|
|
assert txn_id
|
|
|
|
|
|
CHANGE_BASED_DESCS = [
|
|
(
|
|
"wsh("
|
|
"or_d("
|
|
"pk([0f056943/84'/1'/0']tpubDC7jGaaSE66Pn4dgtbAAstde4bCyhSUs4r3P8WhMVvPByvcRrzrwqSvpF9Ghx83Z1LfVugGRrSBko5UEKELCz9HoMv5qKmGq3fqnnbS5E9r/<0;1>/*),"
|
|
"and_v("
|
|
"v:pkh([0f056943/84'/1'/0']tpubDC7jGaaSE66Pn4dgtbAAstde4bCyhSUs4r3P8WhMVvPByvcRrzrwqSvpF9Ghx83Z1LfVugGRrSBko5UEKELCz9HoMv5qKmGq3fqnnbS5E9r/<2;3>/*),"
|
|
"older(5)"
|
|
")"
|
|
")"
|
|
")#aq0kpuae"
|
|
),
|
|
(
|
|
"wsh(or_i("
|
|
"and_v("
|
|
"v:pkh([0f056943/84'/1'/0']tpubDC7jGaaSE66Pn4dgtbAAstde4bCyhSUs4r3P8WhMVvPByvcRrzrwqSvpF9Ghx83Z1LfVugGRrSBko5UEKELCz9HoMv5qKmGq3fqnnbS5E9r/<2147483646;2147483647>/*),"
|
|
"older(10)"
|
|
"),"
|
|
"or_d("
|
|
"multi("
|
|
"3,"
|
|
"[0f056943/84'/1'/0']tpubDC7jGaaSE66Pn4dgtbAAstde4bCyhSUs4r3P8WhMVvPByvcRrzrwqSvpF9Ghx83Z1LfVugGRrSBko5UEKELCz9HoMv5qKmGq3fqnnbS5E9r/<100;101>/*,"
|
|
"[0f056943/84'/1'/0']tpubDC7jGaaSE66Pn4dgtbAAstde4bCyhSUs4r3P8WhMVvPByvcRrzrwqSvpF9Ghx83Z1LfVugGRrSBko5UEKELCz9HoMv5qKmGq3fqnnbS5E9r/<26;27>/*,"
|
|
"[0f056943/84'/1'/0']tpubDC7jGaaSE66Pn4dgtbAAstde4bCyhSUs4r3P8WhMVvPByvcRrzrwqSvpF9Ghx83Z1LfVugGRrSBko5UEKELCz9HoMv5qKmGq3fqnnbS5E9r/<4;5>/*"
|
|
"),"
|
|
"and_v("
|
|
"v:thresh("
|
|
"2,"
|
|
"pkh([0f056943/84'/1'/0']tpubDC7jGaaSE66Pn4dgtbAAstde4bCyhSUs4r3P8WhMVvPByvcRrzrwqSvpF9Ghx83Z1LfVugGRrSBko5UEKELCz9HoMv5qKmGq3fqnnbS5E9r/<20;21>/*),"
|
|
"a:pkh([0f056943/84'/1'/0']tpubDC7jGaaSE66Pn4dgtbAAstde4bCyhSUs4r3P8WhMVvPByvcRrzrwqSvpF9Ghx83Z1LfVugGRrSBko5UEKELCz9HoMv5qKmGq3fqnnbS5E9r/<104;105>/*),"
|
|
"a:pkh([0f056943/84'/1'/0']tpubDC7jGaaSE66Pn4dgtbAAstde4bCyhSUs4r3P8WhMVvPByvcRrzrwqSvpF9Ghx83Z1LfVugGRrSBko5UEKELCz9HoMv5qKmGq3fqnnbS5E9r/<22;23>/*)"
|
|
"),"
|
|
"older(5)"
|
|
")"
|
|
")"
|
|
"))#a4nfkskx"
|
|
),
|
|
"tr(50929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0,{or_d(pk([0f056943/84'/1'/0']tpubDC7jGaaSE66Pn4dgtbAAstde4bCyhSUs4r3P8WhMVvPByvcRrzrwqSvpF9Ghx83Z1LfVugGRrSBko5UEKELCz9HoMv5qKmGq3fqnnbS5E9r/<0;1>/*),and_v(v:pkh([0f056943/84'/1'/0']tpubDC7jGaaSE66Pn4dgtbAAstde4bCyhSUs4r3P8WhMVvPByvcRrzrwqSvpF9Ghx83Z1LfVugGRrSBko5UEKELCz9HoMv5qKmGq3fqnnbS5E9r/<2;3>/*),older(5))),or_i(and_v(v:pkh([0f056943/84'/1'/0']tpubDC7jGaaSE66Pn4dgtbAAstde4bCyhSUs4r3P8WhMVvPByvcRrzrwqSvpF9Ghx83Z1LfVugGRrSBko5UEKELCz9HoMv5qKmGq3fqnnbS5E9r/<2147483646;2147483647>/*),older(10)),or_d(multi_a(3,[0f056943/84'/1'/0']tpubDC7jGaaSE66Pn4dgtbAAstde4bCyhSUs4r3P8WhMVvPByvcRrzrwqSvpF9Ghx83Z1LfVugGRrSBko5UEKELCz9HoMv5qKmGq3fqnnbS5E9r/<100;101>/*,[0f056943/84'/1'/0']tpubDC7jGaaSE66Pn4dgtbAAstde4bCyhSUs4r3P8WhMVvPByvcRrzrwqSvpF9Ghx83Z1LfVugGRrSBko5UEKELCz9HoMv5qKmGq3fqnnbS5E9r/<26;27>/*,[0f056943/84'/1'/0']tpubDC7jGaaSE66Pn4dgtbAAstde4bCyhSUs4r3P8WhMVvPByvcRrzrwqSvpF9Ghx83Z1LfVugGRrSBko5UEKELCz9HoMv5qKmGq3fqnnbS5E9r/<4;5>/*),and_v(v:thresh(2,pkh([0f056943/84'/1'/0']tpubDC7jGaaSE66Pn4dgtbAAstde4bCyhSUs4r3P8WhMVvPByvcRrzrwqSvpF9Ghx83Z1LfVugGRrSBko5UEKELCz9HoMv5qKmGq3fqnnbS5E9r/<20;21>/*),a:pkh([0f056943/84'/1'/0']tpubDC7jGaaSE66Pn4dgtbAAstde4bCyhSUs4r3P8WhMVvPByvcRrzrwqSvpF9Ghx83Z1LfVugGRrSBko5UEKELCz9HoMv5qKmGq3fqnnbS5E9r/<104;105>/*),a:pkh([0f056943/84'/1'/0']tpubDC7jGaaSE66Pn4dgtbAAstde4bCyhSUs4r3P8WhMVvPByvcRrzrwqSvpF9Ghx83Z1LfVugGRrSBko5UEKELCz9HoMv5qKmGq3fqnnbS5E9r/<22;23>/*)),older(5))))})#z5x7409w",
|
|
"tr([0f056943/84'/1'/0']tpubDC7jGaaSE66Pn4dgtbAAstde4bCyhSUs4r3P8WhMVvPByvcRrzrwqSvpF9Ghx83Z1LfVugGRrSBko5UEKELCz9HoMv5qKmGq3fqnnbS5E9r/<66;67>/*,{or_d(pk([0f056943/84'/1'/0']tpubDC7jGaaSE66Pn4dgtbAAstde4bCyhSUs4r3P8WhMVvPByvcRrzrwqSvpF9Ghx83Z1LfVugGRrSBko5UEKELCz9HoMv5qKmGq3fqnnbS5E9r/<0;1>/*),and_v(v:pkh([0f056943/84'/1'/0']tpubDC7jGaaSE66Pn4dgtbAAstde4bCyhSUs4r3P8WhMVvPByvcRrzrwqSvpF9Ghx83Z1LfVugGRrSBko5UEKELCz9HoMv5qKmGq3fqnnbS5E9r/<2;3>/*),older(5))),or_i(and_v(v:pkh([0f056943/84'/1'/0']tpubDC7jGaaSE66Pn4dgtbAAstde4bCyhSUs4r3P8WhMVvPByvcRrzrwqSvpF9Ghx83Z1LfVugGRrSBko5UEKELCz9HoMv5qKmGq3fqnnbS5E9r/<2147483646;2147483647>/*),older(10)),or_d(multi_a(3,[0f056943/84'/1'/0']tpubDC7jGaaSE66Pn4dgtbAAstde4bCyhSUs4r3P8WhMVvPByvcRrzrwqSvpF9Ghx83Z1LfVugGRrSBko5UEKELCz9HoMv5qKmGq3fqnnbS5E9r/<100;101>/*,[0f056943/84'/1'/0']tpubDC7jGaaSE66Pn4dgtbAAstde4bCyhSUs4r3P8WhMVvPByvcRrzrwqSvpF9Ghx83Z1LfVugGRrSBko5UEKELCz9HoMv5qKmGq3fqnnbS5E9r/<26;27>/*,[0f056943/84'/1'/0']tpubDC7jGaaSE66Pn4dgtbAAstde4bCyhSUs4r3P8WhMVvPByvcRrzrwqSvpF9Ghx83Z1LfVugGRrSBko5UEKELCz9HoMv5qKmGq3fqnnbS5E9r/<4;5>/*),and_v(v:thresh(2,pkh([0f056943/84'/1'/0']tpubDC7jGaaSE66Pn4dgtbAAstde4bCyhSUs4r3P8WhMVvPByvcRrzrwqSvpF9Ghx83Z1LfVugGRrSBko5UEKELCz9HoMv5qKmGq3fqnnbS5E9r/<20;21>/*),a:pkh([0f056943/84'/1'/0']tpubDC7jGaaSE66Pn4dgtbAAstde4bCyhSUs4r3P8WhMVvPByvcRrzrwqSvpF9Ghx83Z1LfVugGRrSBko5UEKELCz9HoMv5qKmGq3fqnnbS5E9r/<104;105>/*),a:pkh([0f056943/84'/1'/0']tpubDC7jGaaSE66Pn4dgtbAAstde4bCyhSUs4r3P8WhMVvPByvcRrzrwqSvpF9Ghx83Z1LfVugGRrSBko5UEKELCz9HoMv5qKmGq3fqnnbS5E9r/<22;23>/*)),older(5))))})#qqcy9jlr",
|
|
]
|
|
|
|
@pytest.mark.parametrize("desc", CHANGE_BASED_DESCS)
|
|
def test_same_key_change_based_minisc(goto_home, need_keypress, pick_menu_item, cap_story,
|
|
clear_miniscript, microsd_path, load_export, bitcoind,
|
|
import_miniscript, address_explorer_check, use_regtest,
|
|
desc):
|
|
clear_miniscript()
|
|
use_regtest()
|
|
if desc.startswith("tr("):
|
|
af = "bech32m"
|
|
else:
|
|
af = "bech32"
|
|
|
|
name = "mini-change"
|
|
fname = f"{name}.txt"
|
|
with open(microsd_path(fname), "w") as f:
|
|
f.write(desc)
|
|
|
|
_, story = import_miniscript(fname)
|
|
assert "Create new miniscript wallet?" in story
|
|
assert fname.split(".")[0] in story
|
|
assert "Press (1) to see extended public keys" in story
|
|
|
|
need_keypress("y")
|
|
goto_home()
|
|
pick_menu_item('Settings')
|
|
pick_menu_item('Miniscript')
|
|
pick_menu_item(fname.split(".")[0])
|
|
pick_menu_item("Descriptors")
|
|
pick_menu_item("Bitcoin Core")
|
|
text = load_export("sd", label="Bitcoin Core miniscript", is_json=False, sig_check=False)
|
|
text = text.replace("importdescriptors ", "").strip()
|
|
# remove junk
|
|
r1 = text.find("[")
|
|
r2 = text.find("]", -1, 0)
|
|
text = text[r1: r2]
|
|
core_desc_object = json.loads(text)
|
|
# wo wallet
|
|
wo = bitcoind.create_wallet(
|
|
wallet_name=f"minsc-change", disable_private_keys=True,
|
|
blank=True, passphrase=None, avoid_reuse=False, descriptors=True
|
|
)
|
|
# import descriptors to watch only wallet
|
|
res = wo.importdescriptors(core_desc_object)
|
|
assert res[0]["success"]
|
|
assert res[1]["success"]
|
|
|
|
addr = wo.getnewaddress("", af)
|
|
assert bitcoind.supply_wallet.sendtoaddress(addr, 49)
|
|
bitcoind.supply_wallet.generatetoaddress(1, bitcoind.supply_wallet.getnewaddress())
|
|
|
|
dest_addr = wo.getnewaddress("", af) # selfspend
|
|
psbt = wo.walletcreatefundedpsbt([], [{dest_addr: 1.0}], 0, {"fee_rate": 2})["psbt"]
|
|
fname = "msc-change-conso.psbt"
|
|
with open(microsd_path(fname), "w") as f:
|
|
f.write(psbt)
|
|
|
|
goto_home()
|
|
pick_menu_item("Ready To Sign")
|
|
time.sleep(0.5)
|
|
title, story = cap_story()
|
|
if "Choose PSBT file to be signed" in story:
|
|
need_keypress("y")
|
|
time.sleep(0.1)
|
|
pick_menu_item(fname)
|
|
time.sleep(0.1)
|
|
title, story = cap_story()
|
|
assert "OK TO SEND?" in title
|
|
assert "Consolidating" in story
|
|
need_keypress("y") # confirm signing
|
|
time.sleep(0.5)
|
|
title, story = cap_story()
|
|
assert "PSBT Signed" == title
|
|
assert "Updated PSBT is:" in story
|
|
need_keypress("y")
|
|
fname_psbt = story.split("\n\n")[1]
|
|
with open(microsd_path(fname_psbt), "r") as f:
|
|
final_psbt = f.read().strip()
|
|
|
|
res = wo.finalizepsbt(final_psbt)
|
|
assert res["complete"]
|
|
tx_hex = res["hex"]
|
|
res = wo.testmempoolaccept([tx_hex])
|
|
assert res[0]["allowed"]
|
|
txn_id = bitcoind.supply_wallet.sendrawtransaction(tx_hex)
|
|
assert txn_id
|
|
|
|
bitcoind.supply_wallet.generatetoaddress(1, bitcoind.supply_wallet.getnewaddress())
|
|
|
|
dest_addr_0 = bitcoind.supply_wallet.getnewaddress()
|
|
dest_addr_1 = bitcoind.supply_wallet.getnewaddress()
|
|
dest_addr_2 = bitcoind.supply_wallet.getnewaddress()
|
|
psbt = wo.walletcreatefundedpsbt(
|
|
[],
|
|
[{dest_addr_0: 1.0}, {dest_addr_1: 2.56}, {dest_addr_2: 12.99}],
|
|
0, {"fee_rate": 2}
|
|
)["psbt"]
|
|
fname = "msc-change-send.psbt"
|
|
with open(microsd_path(fname), "w") as f:
|
|
f.write(psbt)
|
|
|
|
goto_home()
|
|
pick_menu_item("Ready To Sign")
|
|
time.sleep(0.5)
|
|
title, story = cap_story()
|
|
if "Choose PSBT file to be signed" in story:
|
|
need_keypress("y")
|
|
time.sleep(0.1)
|
|
pick_menu_item(fname)
|
|
time.sleep(0.1)
|
|
title, story = cap_story()
|
|
assert "OK TO SEND?" in title
|
|
assert "Consolidating" not in story
|
|
need_keypress("y") # confirm signing
|
|
time.sleep(0.5)
|
|
title, story = cap_story()
|
|
assert "PSBT Signed" == title
|
|
assert "Updated PSBT is:" in story
|
|
need_keypress("y")
|
|
fname_psbt = story.split("\n\n")[1]
|
|
with open(microsd_path(fname_psbt), "r") as f:
|
|
final_psbt = f.read().strip()
|
|
|
|
res = wo.finalizepsbt(final_psbt)
|
|
assert res["complete"]
|
|
tx_hex = res["hex"]
|
|
res = wo.testmempoolaccept([tx_hex])
|
|
assert res[0]["allowed"]
|
|
txn_id = bitcoind.supply_wallet.sendrawtransaction(tx_hex)
|
|
assert txn_id
|
|
|
|
# check addresses
|
|
address_explorer_check("sd", af, wo, "mini-change")
|
|
|
|
|
|
def test_same_key_account_based_multisig(goto_home, need_keypress, pick_menu_item, cap_story,
|
|
clear_miniscript, microsd_path, load_export, bitcoind,
|
|
import_miniscript):
|
|
clear_miniscript()
|
|
desc = ("wsh(sortedmulti(2,"
|
|
"[0f056943/84'/1'/0']tpubDC7jGaaSE66Pn4dgtbAAstde4bCyhSUs4r3P8WhMVvPByvcRrzrwqSvpF9Ghx83Z1LfVugGRrSBko5UEKELCz9HoMv5qKmGq3fqnnbS5E9r/<0;1>/*,"
|
|
"[0f056943/84'/1'/9']tpubDC7jGaaSE66QBAcX8TUD3JKWari1zmGH4gNyKZcrfq6NwCofKujNF2kyeVXgKshotxw5Yib8UxLrmmCmWd8NVPVTAL8rGfMdc7TsAKqsy6y/<0;1>/*"
|
|
"))")
|
|
name = "multi-accounts"
|
|
fname = f"{name}.txt"
|
|
with open(microsd_path(fname), "w") as f:
|
|
f.write(desc)
|
|
|
|
_, story = import_miniscript(fname)
|
|
assert "Failed to import" in story
|
|
assert "Use Settings -> Multisig Wallets" in story
|
|
|
|
|
|
@pytest.mark.parametrize("desc", [
|
|
"wsh(or_d(pk(@A),and_v(v:pkh(@A),older(5))))",
|
|
"tr(%s,multi_a(2,@A,@A))" % H,
|
|
"tr(%s,{sortedmulti_a(2,@A,@A),pk(@A)})" % H,
|
|
"tr(%s,or_d(pk(@A),and_v(v:pkh(@A),older(5))))" % H,
|
|
])
|
|
def test_insane_miniscript(get_cc_key, pick_menu_item, need_keypress, cap_story,
|
|
microsd_path, desc, import_miniscript):
|
|
|
|
cc_key = get_cc_key("84h/0h/0h")
|
|
desc = desc.replace("@A", cc_key)
|
|
fname = "insane.txt"
|
|
with open(microsd_path(fname), "w") as f:
|
|
f.write(desc)
|
|
|
|
_, story = import_miniscript(fname)
|
|
assert "Failed to import" in story
|
|
assert "Insane" in story
|
|
|
|
def test_tapscript_depth(get_cc_key, pick_menu_item, need_keypress, cap_story,
|
|
microsd_path, import_miniscript):
|
|
leaf_num = 9
|
|
scripts = []
|
|
for i in range(leaf_num):
|
|
k = get_cc_key(f"84h/0h/{i}h")
|
|
scripts.append(f"pk({k})")
|
|
|
|
tree = TREE[leaf_num] % tuple(scripts)
|
|
desc = f"tr({H},{tree})"
|
|
fname = "9leafs.txt"
|
|
with open(microsd_path(fname), "w") as f:
|
|
f.write(desc)
|
|
_, story = import_miniscript(fname)
|
|
assert "Failed to import" in story
|
|
assert "num_leafs > 8" in story
|
|
|
|
@pytest.mark.bitcoind
|
|
@pytest.mark.parametrize("lt_type", ["older", "after"])
|
|
@pytest.mark.parametrize("same_acct", [True, False])
|
|
@pytest.mark.parametrize("recovery", [True, False])
|
|
@pytest.mark.parametrize("leaf2_mine", [True, False])
|
|
@pytest.mark.parametrize("minisc", [
|
|
"or_d(pk(@A),and_v(v:pkh(@B),locktime(N)))",
|
|
|
|
"or_d(pk(@A),and_v(v:pk(@B),locktime(N)))",
|
|
|
|
"or_d(multi_a(2,@A,@C),and_v(v:pkh(@B),locktime(N)))",
|
|
|
|
"or_d(pk(@A),and_v(v:multi_a(2,@B,@C),locktime(N)))",
|
|
])
|
|
def test_minitapscript(leaf2_mine, recovery, lt_type, minisc, clear_miniscript, goto_home,
|
|
need_keypress, pick_menu_item, cap_menu, cap_story, microsd_path,
|
|
use_regtest, bitcoind, microsd_wipe, load_export, dev,
|
|
address_explorer_check, get_cc_key, import_miniscript,
|
|
bitcoin_core_signer, same_acct, import_duplicate):
|
|
|
|
# needs bitcoind 26.0
|
|
normal_cosign_core = False
|
|
recovery_cosign_core = False
|
|
if "multi_a(" in minisc.split("),", 1)[0]:
|
|
normal_cosign_core = True
|
|
if "multi_a(" in minisc.split("),", 1)[-1]:
|
|
recovery_cosign_core = True
|
|
|
|
if lt_type == "older":
|
|
sequence = 5
|
|
locktime = 0
|
|
# 101 blocks are mined by default
|
|
to_replace = "older(5)"
|
|
else:
|
|
sequence = None
|
|
locktime = 105
|
|
to_replace = "after(105)"
|
|
|
|
minisc = minisc.replace("locktime(N)", to_replace)
|
|
|
|
core_keys = []
|
|
signers = []
|
|
for i in range(3):
|
|
# core signers
|
|
signer, core_key = bitcoin_core_signer(f"co-signer{i}")
|
|
core_keys.append(core_key)
|
|
signers.append(signer)
|
|
|
|
# cc device key
|
|
if same_acct:
|
|
cc_key = get_cc_key("86h/1h/0h", subderiv="/<4;5>/*")
|
|
cc_key1 = get_cc_key("86h/1h/0h", subderiv="/<6;7>/*")
|
|
else:
|
|
cc_key = get_cc_key("86h/1h/0h")
|
|
cc_key1 = get_cc_key("86h/1h/1h")
|
|
|
|
if recovery:
|
|
# recevoery path is always B
|
|
minisc = minisc.replace("@B", cc_key)
|
|
minisc = minisc.replace("@A", core_keys[0])
|
|
else:
|
|
minisc = minisc.replace("@A", cc_key)
|
|
minisc = minisc.replace("@B", core_keys[0])
|
|
|
|
if "@C" in minisc:
|
|
minisc = minisc.replace("@C", core_keys[1])
|
|
|
|
if leaf2_mine:
|
|
desc = f"tr({H},{{{minisc},pk({cc_key1})}})"
|
|
else:
|
|
desc = f"tr({H},{{pk({core_keys[2]}),{minisc}}})"
|
|
|
|
use_regtest()
|
|
clear_miniscript()
|
|
name = "minitapscript"
|
|
fname = f"{name}.txt"
|
|
fpath = microsd_path(fname)
|
|
with open(fpath, "w") as f:
|
|
f.write(desc)
|
|
|
|
wo = bitcoind.create_wallet(wallet_name=name, disable_private_keys=True, blank=True,
|
|
passphrase=None, avoid_reuse=False, descriptors=True)
|
|
|
|
_, story = import_miniscript(fname)
|
|
assert "Create new miniscript wallet?" in story
|
|
# do some checks on policy --> helper function to replace keys with letters
|
|
need_keypress("y")
|
|
import_duplicate(fname)
|
|
menu = cap_menu()
|
|
assert menu[0] == name
|
|
pick_menu_item(menu[0]) # pick imported descriptor multisig wallet
|
|
pick_menu_item("Descriptors")
|
|
pick_menu_item("Bitcoin Core")
|
|
text = load_export("sd", label="Bitcoin Core miniscript", is_json=False, sig_check=False)
|
|
text = text.replace("importdescriptors ", "").strip()
|
|
# remove junk
|
|
r1 = text.find("[")
|
|
r2 = text.find("]", -1, 0)
|
|
text = text[r1: r2]
|
|
core_desc_object = json.loads(text)
|
|
res = wo.importdescriptors(core_desc_object)
|
|
for obj in res:
|
|
assert obj["success"]
|
|
addr = wo.getnewaddress("", "bech32m")
|
|
addr_dest = wo.getnewaddress("", "bech32m") # self-spend
|
|
assert bitcoind.supply_wallet.sendtoaddress(addr, 49)
|
|
bitcoind.supply_wallet.generatetoaddress(1, bitcoind.supply_wallet.getnewaddress())
|
|
all_of_it = wo.getbalance()
|
|
unspent = wo.listunspent()
|
|
assert len(unspent) == 1
|
|
inp = {"txid": unspent[0]["txid"], "vout": unspent[0]["vout"]}
|
|
if recovery and sequence and not leaf2_mine:
|
|
inp["sequence"] = sequence
|
|
psbt_resp = wo.walletcreatefundedpsbt(
|
|
[inp],
|
|
[{addr_dest: all_of_it - 1}],
|
|
locktime if (recovery and not leaf2_mine) else 0,
|
|
{"fee_rate": 20, "change_type": "bech32m", "subtractFeeFromOutputs": [0]},
|
|
)
|
|
psbt = psbt_resp.get("psbt")
|
|
|
|
if (normal_cosign_core or recovery_cosign_core) and not leaf2_mine:
|
|
psbt = signers[1].walletprocesspsbt(psbt, True, "ALL")["psbt"]
|
|
|
|
name = f"{name}.psbt"
|
|
with open(microsd_path(name), "w") as f:
|
|
f.write(psbt)
|
|
goto_home()
|
|
pick_menu_item("Ready To Sign")
|
|
time.sleep(0.5)
|
|
title, story = cap_story()
|
|
if "Choose PSBT file to be signed" in story:
|
|
need_keypress("y")
|
|
time.sleep(0.1)
|
|
pick_menu_item(name)
|
|
time.sleep(0.1)
|
|
title, story = cap_story()
|
|
assert "OK TO SEND?" in title
|
|
assert "Consolidating" in story
|
|
need_keypress("y") # confirm signing
|
|
time.sleep(0.5)
|
|
title, story = cap_story()
|
|
assert "PSBT Signed" == title
|
|
assert "Updated PSBT is:" in story
|
|
need_keypress("y")
|
|
fname_psbt = story.split("\n\n")[1]
|
|
# fname_txn = story.split("\n\n")[3]
|
|
with open(microsd_path(fname_psbt), "r") as f:
|
|
final_psbt = f.read().strip()
|
|
# with open(microsd_path(fname_txn), "r") as f:
|
|
# final_txn = f.read().strip()
|
|
res = wo.finalizepsbt(final_psbt)
|
|
assert res["complete"]
|
|
tx_hex = res["hex"]
|
|
# assert tx_hex == final_txn
|
|
res = wo.testmempoolaccept([tx_hex])
|
|
if recovery and not leaf2_mine:
|
|
assert not res[0]["allowed"]
|
|
assert res[0]["reject-reason"] == 'non-BIP68-final' if sequence else "non-final"
|
|
bitcoind.supply_wallet.generatetoaddress(6, bitcoind.supply_wallet.getnewaddress())
|
|
res = wo.testmempoolaccept([tx_hex])
|
|
assert res[0]["allowed"]
|
|
else:
|
|
assert res[0]["allowed"]
|
|
|
|
res = wo.sendrawtransaction(tx_hex)
|
|
assert len(res) == 64 # tx id
|
|
|
|
# check addresses
|
|
address_explorer_check("sd", "bech32m", wo, "minitapscript")
|
|
|
|
@pytest.mark.parametrize("desc", [
|
|
"tr(50929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0,{{sortedmulti(2,[0f056943/48h/1h/0h/3h]tpubDF2rnouQaaYrY6CUWTapYkeFEs3h3qrzL4M52ZGoPeU9dkarJMtrw6VF1zJRGuGuAFxYS3kXtavfAwQPTQkU5dyNYpbgxcpftrR8H3U85Ez/<0;1>/*,[b7fe820c/48h/1h/0h/3h]tpubDFdQ1sNV53TbogAMPEd2egY5NXfbdKD1Mnr2iBrJrcwRHJbKC7tuuUMHT8SSHJ2VEKdCf5WYBMfevvWCnyJV53gYUT2wFyxEV8SuUTedBp7/<0;1>/*),sortedmulti(2,[0f056943/48h/1h/0h/3h]tpubDF2rnouQaaYrY6CUWTapYkeFEs3h3qrzL4M52ZGoPeU9dkarJMtrw6VF1zJRGuGuAFxYS3kXtavfAwQPTQkU5dyNYpbgxcpftrR8H3U85Ez/<0;1>/*,[30afbe54/48h/1h/0h/3h]tpubDFLVv7cuiLjn3QcsCend5kn3yw5sx6Czazy7hZvdGX61v8pkU95k2Byz9M5jnabzeUg7qWtHYLeKQyCWWAHhUmQQMeZ4Dee2CfGR2TsZqrN/<0;1>/*)},sortedmulti(2,[b7fe820c/48h/1h/0h/3h]tpubDFdQ1sNV53TbogAMPEd2egY5NXfbdKD1Mnr2iBrJrcwRHJbKC7tuuUMHT8SSHJ2VEKdCf5WYBMfevvWCnyJV53gYUT2wFyxEV8SuUTedBp7/<0;1>/*,[30afbe54/48h/1h/0h/3h]tpubDFLVv7cuiLjn3QcsCend5kn3yw5sx6Czazy7hZvdGX61v8pkU95k2Byz9M5jnabzeUg7qWtHYLeKQyCWWAHhUmQQMeZ4Dee2CfGR2TsZqrN/<0;1>/*)})",
|
|
"wsh(sortedmulti_a(2,[0f056943/48h/1h/0h/3h]tpubDF2rnouQaaYrY6CUWTapYkeFEs3h3qrzL4M52ZGoPeU9dkarJMtrw6VF1zJRGuGuAFxYS3kXtavfAwQPTQkU5dyNYpbgxcpftrR8H3U85Ez/<0;1>/*,[b7fe820c/48h/1h/0h/3h]tpubDFdQ1sNV53TbogAMPEd2egY5NXfbdKD1Mnr2iBrJrcwRHJbKC7tuuUMHT8SSHJ2VEKdCf5WYBMfevvWCnyJV53gYUT2wFyxEV8SuUTedBp7/<0;1>/*))",
|
|
"sh(wsh(or_d(pk([30afbe54/48h/1h/0h/3h]tpubDFLVv7cuiLjn3QcsCend5kn3yw5sx6Czazy7hZvdGX61v8pkU95k2Byz9M5jnabzeUg7qWtHYLeKQyCWWAHhUmQQMeZ4Dee2CfGR2TsZqrN/<0;1>/*),and_v(v:multi_a(2,[b7fe820c/48h/1h/0h/3h]tpubDFdQ1sNV53TbogAMPEd2egY5NXfbdKD1Mnr2iBrJrcwRHJbKC7tuuUMHT8SSHJ2VEKdCf5WYBMfevvWCnyJV53gYUT2wFyxEV8SuUTedBp7/<0;1>/*,[0f056943/48h/1h/0h/3h]tpubDF2rnouQaaYrY6CUWTapYkeFEs3h3qrzL4M52ZGoPeU9dkarJMtrw6VF1zJRGuGuAFxYS3kXtavfAwQPTQkU5dyNYpbgxcpftrR8H3U85Ez/<0;1>/*),older(500)))))",
|
|
])
|
|
def test_multi_mixin(desc, clear_miniscript, microsd_path, pick_menu_item,
|
|
cap_story, need_keypress, import_miniscript):
|
|
clear_miniscript()
|
|
fname = "imdesc.txt"
|
|
with open(microsd_path(fname), "w") as f:
|
|
f.write(desc)
|
|
|
|
title, story = import_miniscript(fname)
|
|
assert "Failed to import" in story
|
|
assert "multi mixin" in story
|
|
|
|
|
|
def test_timelock_mixin():
|
|
pass
|
|
|
|
|
|
@pytest.mark.parametrize("addr_fmt", ["bech32", "bech32m"])
|
|
@pytest.mark.parametrize("cc_first", [True, False])
|
|
def test_d_wrapper(addr_fmt, bitcoind, get_cc_key, goto_home, pick_menu_item, cap_story, cap_menu,
|
|
need_keypress, load_export, microsd_path, use_regtest, clear_miniscript, cc_first,
|
|
address_explorer_check, import_miniscript, bitcoin_core_signer):
|
|
|
|
# check D wrapper u property for segwit v0 and v1
|
|
# https://github.com/bitcoin/bitcoin/pull/24906/files
|
|
minsc = "thresh(3,c:pk_k(@A),sc:pk_k(@B),sc:pk_k(@C),sdv:older(5))"
|
|
|
|
core_keys = []
|
|
signers = []
|
|
for i in range(2):
|
|
# core signers
|
|
signer, core_key = bitcoin_core_signer(f"co-signer{i}")
|
|
core_keys.append(core_key)
|
|
signers.append(signer)
|
|
|
|
cc_key = get_cc_key(f"{84 if addr_fmt == 'bech32' else 86}h/1h/0h")
|
|
|
|
minsc = minsc.replace("@A", cc_key)
|
|
minsc = minsc.replace("@B", core_keys[0])
|
|
minsc = minsc.replace("@C", core_keys[1])
|
|
|
|
if addr_fmt == "bech32":
|
|
desc = f"wsh({minsc})"
|
|
else:
|
|
desc = f"tr({H},{minsc})"
|
|
|
|
name = "d_wrapper"
|
|
fname = f"{name}.txt"
|
|
|
|
fpath = microsd_path(fname)
|
|
with open(fpath, "w") as f:
|
|
f.write(desc)
|
|
|
|
wo = bitcoind.create_wallet(wallet_name=name, disable_private_keys=True, blank=True,
|
|
passphrase=None, avoid_reuse=False, descriptors=True)
|
|
|
|
clear_miniscript()
|
|
use_regtest()
|
|
_, story = import_miniscript(fname)
|
|
if addr_fmt == "bech32":
|
|
assert "Failed to import" in story
|
|
assert "thresh: X3 should be du" in story
|
|
return
|
|
|
|
assert "Create new miniscript wallet?" in story
|
|
# do some checks on policy --> helper function to replace keys with letters
|
|
need_keypress("y")
|
|
menu = cap_menu()
|
|
assert menu[0] == name
|
|
pick_menu_item(menu[0]) # pick imported descriptor multisig wallet
|
|
pick_menu_item("Descriptors")
|
|
pick_menu_item("Bitcoin Core")
|
|
text = load_export("sd", label="Bitcoin Core miniscript", is_json=False, sig_check=False)
|
|
text = text.replace("importdescriptors ", "").strip()
|
|
# remove junk
|
|
r1 = text.find("[")
|
|
r2 = text.find("]", -1, 0)
|
|
text = text[r1: r2]
|
|
core_desc_object = json.loads(text)
|
|
res = wo.importdescriptors(core_desc_object)
|
|
for obj in res:
|
|
assert obj["success"]
|
|
|
|
addr = wo.getnewaddress("", addr_fmt) # self-spend
|
|
addr_dest = wo.getnewaddress("", addr_fmt) # self-spend
|
|
assert bitcoind.supply_wallet.sendtoaddress(addr, 49)
|
|
bitcoind.supply_wallet.generatetoaddress(1, bitcoind.supply_wallet.getnewaddress())
|
|
all_of_it = wo.getbalance()
|
|
unspent = wo.listunspent()
|
|
assert len(unspent) == 1
|
|
inp = {"txid": unspent[0]["txid"], "vout": unspent[0]["vout"]}
|
|
inp["sequence"] = 5
|
|
psbt_resp = wo.walletcreatefundedpsbt(
|
|
[inp],
|
|
[{addr_dest: all_of_it - 1}],
|
|
0,
|
|
{"fee_rate": 20, "change_type": addr_fmt},
|
|
)
|
|
psbt = psbt_resp.get("psbt")
|
|
|
|
if not cc_first:
|
|
to_sign_psbt_o = signers[0].walletprocesspsbt(psbt, True)
|
|
to_sign_psbt = to_sign_psbt_o["psbt"]
|
|
assert to_sign_psbt != psbt
|
|
else:
|
|
to_sign_psbt = psbt
|
|
|
|
name = f"{name}.psbt"
|
|
with open(microsd_path(name), "w") as f:
|
|
f.write(to_sign_psbt)
|
|
goto_home()
|
|
pick_menu_item("Ready To Sign")
|
|
time.sleep(0.5)
|
|
title, story = cap_story()
|
|
if "Choose PSBT file to be signed" in story:
|
|
need_keypress("y")
|
|
time.sleep(0.1)
|
|
pick_menu_item(name)
|
|
time.sleep(0.1)
|
|
title, story = cap_story()
|
|
assert "OK TO SEND?" in title
|
|
assert "Consolidating" in story
|
|
need_keypress("y") # confirm signing
|
|
time.sleep(0.5)
|
|
title, story = cap_story()
|
|
assert "PSBT Signed" == title
|
|
assert "Updated PSBT is:" in story
|
|
need_keypress("y")
|
|
fname_psbt = story.split("\n\n")[1]
|
|
# fname_txn = story.split("\n\n")[3]
|
|
with open(microsd_path(fname_psbt), "r") as f:
|
|
final_psbt = f.read().strip()
|
|
|
|
assert final_psbt != to_sign_psbt
|
|
# with open(microsd_path(fname_txn), "r") as f:
|
|
# final_txn = f.read().strip()
|
|
|
|
if cc_first:
|
|
done_o = signers[0].walletprocesspsbt(final_psbt, True)
|
|
done = done_o["psbt"]
|
|
else:
|
|
done = final_psbt
|
|
|
|
res = wo.finalizepsbt(done)
|
|
assert res["complete"]
|
|
tx_hex = res["hex"]
|
|
# assert tx_hex == final_txn
|
|
res = wo.testmempoolaccept([tx_hex])
|
|
assert not res[0]["allowed"]
|
|
assert res[0]["reject-reason"] == 'non-BIP68-final'
|
|
bitcoind.supply_wallet.generatetoaddress(6, bitcoind.supply_wallet.getnewaddress())
|
|
res = wo.testmempoolaccept([tx_hex])
|
|
assert res[0]["allowed"]
|
|
|
|
res = wo.sendrawtransaction(tx_hex)
|
|
assert len(res) == 64 # tx id
|
|
|
|
# check addresses
|
|
address_explorer_check("sd", addr_fmt, wo, "d_wrapper")
|
|
|
|
|
|
def test_chain_switching(use_mainnet, use_regtest, settings_get, settings_set,
|
|
clear_miniscript, goto_home, cap_menu, pick_menu_item,
|
|
need_keypress, import_miniscript, microsd_path):
|
|
clear_miniscript()
|
|
use_regtest()
|
|
|
|
x = "wsh(or_d(pk([0f056943/48h/1h/0h/3h]tpubDF2rnouQaaYrY6CUWTapYkeFEs3h3qrzL4M52ZGoPeU9dkarJMtrw6VF1zJRGuGuAFxYS3kXtavfAwQPTQkU5dyNYpbgxcpftrR8H3U85Ez/<0;1>/*),and_v(v:pkh([30afbe54/48h/1h/0h/3h]tpubDFLVv7cuiLjn3QcsCend5kn3yw5sx6Czazy7hZvdGX61v8pkU95k2Byz9M5jnabzeUg7qWtHYLeKQyCWWAHhUmQQMeZ4Dee2CfGR2TsZqrN/<0;1>/*),older(100))))"
|
|
z = "wsh(or_d(pk([0f056943/48'/0'/0'/3']xpub6FQgdFZAHcAeDMVe9KxWoLMxziCjscCExzuKJhRSjM71CA9dUDZEGNgPe4S2SsRumCBXeaTBZ5nKz2cMDiK4UEbGkFXNipHLkm46inpjE9D/0/*),and_v(v:pkh([0f056943/48'/0'/0'/2']xpub6FQgdFZAHcAeAhQX2VvQ42CW2fDdKDhgwzhzXuUhWb4yfArmaZXkLbGS9W1UcgHwNxVESCS1b8BK8tgNYEF8cgmc9zkmsE45QSEvbwdp6Kr/0/*),older(100))))"
|
|
y = f"tr({H},or_d(pk([30afbe54/48h/1h/0h/3h]tpubDFLVv7cuiLjn3QcsCend5kn3yw5sx6Czazy7hZvdGX61v8pkU95k2Byz9M5jnabzeUg7qWtHYLeKQyCWWAHhUmQQMeZ4Dee2CfGR2TsZqrN/<0;1>/*),and_v(v:pk([0f056943/48h/1h/0h/3h]tpubDF2rnouQaaYrY6CUWTapYkeFEs3h3qrzL4M52ZGoPeU9dkarJMtrw6VF1zJRGuGuAFxYS3kXtavfAwQPTQkU5dyNYpbgxcpftrR8H3U85Ez/<0;1>/*),after(800000))))"
|
|
|
|
fname_btc = "BTC.txt"
|
|
fname_xtn = "XTN.txt"
|
|
fname_xtn0 = "XTN0.txt"
|
|
|
|
for desc, fname in [(x, fname_xtn), (z, fname_btc), (y, fname_xtn0)]:
|
|
with open(microsd_path(fname), "w") as f:
|
|
f.write(desc)
|
|
|
|
# cannot import XPUBS when testnet/regtest enabled
|
|
_, story = import_miniscript(fname_btc)
|
|
assert "Failed to import" in story
|
|
assert "wrong chain" in story
|
|
|
|
import_miniscript(fname_xtn)
|
|
need_keypress("y")
|
|
# assert that wallets created at XRT always store XTN anywas (key_chain)
|
|
res = settings_get("miniscript")
|
|
assert len(res) == 1
|
|
assert res[0][1] == "XTN"
|
|
|
|
goto_home()
|
|
pick_menu_item("Settings")
|
|
pick_menu_item("Miniscript")
|
|
time.sleep(0.1)
|
|
m = cap_menu()
|
|
assert "(none setup yet)" not in m
|
|
assert fname_xtn.split(".")[0] in m[0]
|
|
goto_home()
|
|
settings_set("chain", "BTC")
|
|
pick_menu_item("Settings")
|
|
pick_menu_item("Miniscript")
|
|
time.sleep(0.1)
|
|
m = cap_menu()
|
|
# asterisk hints that some wallets are already stored
|
|
# but not on current active chain
|
|
assert "(none setup yet)*" in m
|
|
import_miniscript(fname_btc)
|
|
need_keypress("y")
|
|
goto_home()
|
|
pick_menu_item("Settings")
|
|
pick_menu_item("Miniscript")
|
|
time.sleep(0.1)
|
|
m = cap_menu()
|
|
assert fname_btc.split(".")[0] in m[0]
|
|
for mi in m:
|
|
assert fname_xtn.split(".")[0] not in mi
|
|
|
|
_, story = import_miniscript(fname_xtn)
|
|
assert "Failed to import" in story
|
|
assert "wrong chain" in story
|
|
|
|
settings_set("chain", "XTN")
|
|
import_miniscript(fname_xtn0)
|
|
need_keypress("y")
|
|
goto_home()
|
|
pick_menu_item("Settings")
|
|
pick_menu_item("Miniscript")
|
|
time.sleep(0.1)
|
|
m = cap_menu()
|
|
assert "(none setup yet)" not in m
|
|
assert fname_xtn.split(".")[0] in m[0]
|
|
assert fname_xtn0.split(".")[0] in m[1]
|
|
for mi in m:
|
|
assert fname_btc not in mi
|
|
|
|
|
|
@pytest.mark.parametrize("taproot_ikspendable", [
|
|
(True, False), (True, True), (False, False)
|
|
])
|
|
@pytest.mark.parametrize("minisc", [
|
|
"or_d(pk(@A),and_v(v:pkh(@B),after(100)))",
|
|
"or_d(multi(2,@A,@C),and_v(v:pkh(@B),after(100)))",
|
|
])
|
|
def test_import_same_policy_same_keys_diff_order(taproot_ikspendable, minisc,
|
|
clear_miniscript, use_regtest,
|
|
get_cc_key, bitcoin_core_signer,
|
|
offer_minsc_import, need_keypress,
|
|
cap_menu, bitcoind, pick_menu_item):
|
|
use_regtest()
|
|
clear_miniscript()
|
|
taproot, ik_spendable = taproot_ikspendable
|
|
if taproot:
|
|
minisc = minisc.replace("multi(", "multi_a(")
|
|
if ik_spendable:
|
|
ik = get_cc_key("84h/1h/100h", subderiv="/0/*")
|
|
desc = f"tr({ik},{minisc})"
|
|
else:
|
|
desc = f"tr({H},{minisc})"
|
|
else:
|
|
desc = f"wsh({minisc})"
|
|
|
|
cc_key0 = get_cc_key("84h/1h/0h", subderiv="/0/*")
|
|
signer0, core_key0 = bitcoin_core_signer("s00")
|
|
# recevoery path is always B
|
|
desc0 = desc.replace("@A", cc_key0)
|
|
desc0 = desc0.replace("@B", core_key0)
|
|
|
|
if "@C" in desc:
|
|
signer1, core_key1 = bitcoin_core_signer("s11")
|
|
desc0 = desc0.replace("@C", core_key1)
|
|
|
|
# now just change order of the keys (A,B), but same keys same policy
|
|
desc1 = desc.replace("@B", cc_key0)
|
|
desc1 = desc1.replace("@A", core_key0)
|
|
|
|
if "@C" in desc:
|
|
desc1 = desc1.replace("@C", core_key1)
|
|
|
|
# checksum required if via USB
|
|
desc_info = bitcoind.supply_wallet.getdescriptorinfo(desc0)
|
|
desc0 = desc_info["descriptor"] # with checksum
|
|
desc_info = bitcoind.supply_wallet.getdescriptorinfo(desc1)
|
|
desc1 = desc_info["descriptor"] # with checksum
|
|
|
|
title, story = offer_minsc_import(desc0)
|
|
assert "Create new miniscript wallet?" in story
|
|
need_keypress("y")
|
|
time.sleep(.2)
|
|
title, story = offer_minsc_import(desc1)
|
|
assert "Create new miniscript wallet?" in story
|
|
need_keypress("y")
|
|
pick_menu_item("Settings")
|
|
pick_menu_item("Miniscript")
|
|
m = cap_menu()
|
|
m = [i for i in m if not i.startswith("Import")]
|
|
assert len(m) == 2
|
|
|
|
|
|
@pytest.mark.parametrize("cs", [True, False])
|
|
@pytest.mark.parametrize("way", ["usb", "nfc", "sd", "vdisk"])
|
|
def test_import_miniscript_usb_json(use_regtest, cs, way, cap_menu,
|
|
clear_miniscript, pick_menu_item,
|
|
get_cc_key, bitcoin_core_signer,
|
|
offer_minsc_import, need_keypress,
|
|
bitcoind, microsd_path, virtdisk_path,
|
|
import_miniscript, goto_home):
|
|
name = "my_minisc"
|
|
minsc = f"tr({H},or_d(multi_a(2,@A,@C),and_v(v:pkh(@B),after(100))))"
|
|
use_regtest()
|
|
clear_miniscript()
|
|
|
|
cc_key = get_cc_key("84h/1h/0h", subderiv="/0/*")
|
|
signer0, core_key0 = bitcoin_core_signer("s00")
|
|
# recevoery path is always B
|
|
desc = minsc.replace("@A", cc_key)
|
|
desc = desc.replace("@B", core_key0)
|
|
|
|
signer1, core_key1 = bitcoin_core_signer("s11")
|
|
desc = desc.replace("@C", core_key1)
|
|
|
|
if cs:
|
|
desc_info = bitcoind.supply_wallet.getdescriptorinfo(desc)
|
|
desc = desc_info["descriptor"] # with checksum
|
|
|
|
val = json.dumps({"name": name, "desc": desc})
|
|
|
|
nfc_data = None
|
|
fname = "diff_name.txt" # will be ignored as name in the json has preference
|
|
if way == "usb":
|
|
title, story = offer_minsc_import(val)
|
|
else:
|
|
if way == "nfc":
|
|
nfc_data = val
|
|
else:
|
|
if way == "sd":
|
|
fpath = microsd_path(fname)
|
|
else:
|
|
fpath = virtdisk_path(fname)
|
|
|
|
with open(fpath, "w") as f:
|
|
f.write(val)
|
|
|
|
title, story = import_miniscript(fname, way, nfc_data)
|
|
|
|
assert "Create new miniscript wallet?" in story
|
|
assert name in story
|
|
need_keypress("y")
|
|
time.sleep(.2)
|
|
goto_home()
|
|
pick_menu_item("Settings")
|
|
pick_menu_item("Miniscript")
|
|
m = cap_menu()
|
|
m = [i for i in m if not i.startswith("Import")]
|
|
assert len(m) == 1
|
|
assert m[0] == name
|
|
|
|
|
|
@pytest.mark.parametrize("config", [
|
|
# all dummy data there to satisfy badlen check in usb.py
|
|
# missing 'desc' key
|
|
{"name": "my_miniscript", "random": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"},
|
|
# name longer than 40 chars
|
|
{"name": "a" * 41, "desc": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"},
|
|
# name too short
|
|
{"name": "a", "desc": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"},
|
|
# desc key empty
|
|
{"name": "ab", "desc": "", "random": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"},
|
|
# name type
|
|
{"name": None, "desc": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"},
|
|
# desc type
|
|
{"name": "ab", "desc": None, "random": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"},
|
|
])
|
|
def test_json_import_failures(config, offer_minsc_import):
|
|
with pytest.raises(Exception):
|
|
offer_minsc_import(json.dumps(config))
|
|
|
|
|
|
@pytest.mark.parametrize("way", ["sd", "nfc", "vdisk"])
|
|
@pytest.mark.parametrize("is_json", [True, False])
|
|
def test_unique_name(clear_miniscript, use_regtest, offer_minsc_import,
|
|
pick_menu_item, cap_menu, need_keypress, way,
|
|
microsd_path, virtdisk_path, is_json, goto_home,
|
|
import_miniscript):
|
|
clear_miniscript()
|
|
use_regtest()
|
|
|
|
name = "my_name"
|
|
x = "wsh(or_d(pk([0f056943/48h/1h/0h/3h]tpubDF2rnouQaaYrY6CUWTapYkeFEs3h3qrzL4M52ZGoPeU9dkarJMtrw6VF1zJRGuGuAFxYS3kXtavfAwQPTQkU5dyNYpbgxcpftrR8H3U85Ez/<0;1>/*),and_v(v:pkh([30afbe54/48h/1h/0h/3h]tpubDFLVv7cuiLjn3QcsCend5kn3yw5sx6Czazy7hZvdGX61v8pkU95k2Byz9M5jnabzeUg7qWtHYLeKQyCWWAHhUmQQMeZ4Dee2CfGR2TsZqrN/<0;1>/*),older(100))))"
|
|
y = f"tr({H},or_d(pk([30afbe54/48h/1h/0h/3h]tpubDFLVv7cuiLjn3QcsCend5kn3yw5sx6Czazy7hZvdGX61v8pkU95k2Byz9M5jnabzeUg7qWtHYLeKQyCWWAHhUmQQMeZ4Dee2CfGR2TsZqrN/<0;1>/*),and_v(v:pk([0f056943/48h/1h/0h/3h]tpubDF2rnouQaaYrY6CUWTapYkeFEs3h3qrzL4M52ZGoPeU9dkarJMtrw6VF1zJRGuGuAFxYS3kXtavfAwQPTQkU5dyNYpbgxcpftrR8H3U85Ez/<0;1>/*),after(800000))))"
|
|
|
|
xd = json.dumps({"name": name, "desc": x})
|
|
title, story = offer_minsc_import(xd)
|
|
assert "Create new miniscript wallet?" in story
|
|
assert name in story
|
|
need_keypress("y")
|
|
time.sleep(.2)
|
|
pick_menu_item("Settings")
|
|
pick_menu_item("Miniscript")
|
|
m = cap_menu()
|
|
m = [i for i in m if not i.startswith("Import")]
|
|
assert len(m) == 1
|
|
assert m[0] == name
|
|
|
|
# completely different wallet but with the same name (USB)
|
|
yd = json.dumps({"name": name, "desc": y})
|
|
title, story = offer_minsc_import(yd)
|
|
assert title == "FAILED"
|
|
assert "MUST have unique names" in story
|
|
need_keypress("y")
|
|
# nothing imported
|
|
pick_menu_item("Settings")
|
|
pick_menu_item("Miniscript")
|
|
m = cap_menu()
|
|
m = [i for i in m if not i.startswith("Import")]
|
|
assert len(m) == 1
|
|
assert m[0] == name
|
|
|
|
goto_home()
|
|
fname = f"{name}.txt"
|
|
nfc_data = None
|
|
if way == "nfc":
|
|
if not is_json:
|
|
pytest.xfail("impossible")
|
|
|
|
nfc_data = yd
|
|
else:
|
|
if way == "sd":
|
|
fpath = microsd_path(fname)
|
|
elif way == "vdisk":
|
|
fpath = virtdisk_path(fname)
|
|
else:
|
|
assert False
|
|
|
|
with open(fpath, "w") as f:
|
|
f.write(yd if is_json else y)
|
|
|
|
title, story = import_miniscript(fname=fname, way=way, nfc_data=nfc_data)
|
|
assert "FAILED" == title
|
|
assert "MUST have unique names" in story
|
|
|
|
|
|
@pytest.mark.qrcode
|
|
def test_usb_workflow(usb_miniscript_get, usb_miniscript_ls, clear_miniscript,
|
|
usb_miniscript_addr, usb_miniscript_delete, use_regtest,
|
|
reset_seed_words, offer_minsc_import, need_keypress,
|
|
cap_story, cap_screen_qr):
|
|
use_regtest()
|
|
reset_seed_words()
|
|
clear_miniscript()
|
|
assert [] == usb_miniscript_ls()
|
|
for i, desc in enumerate(CHANGE_BASED_DESCS):
|
|
_, story = offer_minsc_import(json.dumps({"name": f"w{i}", "desc": desc}))
|
|
assert "Create new miniscript wallet?" in story
|
|
need_keypress("y")
|
|
time.sleep(.2)
|
|
|
|
msc_wallets = usb_miniscript_ls()
|
|
assert len(msc_wallets) == 4
|
|
assert sorted(msc_wallets) == ["w0", "w1", "w2", "w3"]
|
|
|
|
# try to get/delete nonexistent wallet
|
|
with pytest.raises(Exception) as err:
|
|
usb_miniscript_get("w4")
|
|
assert err.value.args[0] == "Coldcard Error: Miniscript wallet not found"
|
|
|
|
with pytest.raises(Exception) as err:
|
|
usb_miniscript_delete("w4")
|
|
assert err.value.args[0] == "Coldcard Error: Miniscript wallet not found"
|
|
|
|
for i, w in enumerate(msc_wallets):
|
|
assert usb_miniscript_get(w)["desc"].split("#")[0] == CHANGE_BASED_DESCS[i].split("#")[0].replace("'", 'h')
|
|
|
|
#check random address
|
|
addr = usb_miniscript_addr("w0", 55, False)
|
|
time.sleep(0.1)
|
|
need_keypress('4')
|
|
time.sleep(0.1)
|
|
qr = cap_screen_qr().decode('ascii')
|
|
assert qr == addr.upper()
|
|
|
|
usb_miniscript_delete("w3")
|
|
time.sleep(.2)
|
|
_, story = cap_story()
|
|
assert "Delete miniscript wallet" in story
|
|
assert "'w3'" in story
|
|
need_keypress("y")
|
|
time.sleep(.2)
|
|
assert len(usb_miniscript_ls()) == 3
|
|
with pytest.raises(Exception) as err:
|
|
usb_miniscript_get("w3")
|
|
assert err.value.args[0] == "Coldcard Error: Miniscript wallet not found"
|
|
|
|
usb_miniscript_delete("w2")
|
|
time.sleep(.2)
|
|
_, story = cap_story()
|
|
assert "Delete miniscript wallet" in story
|
|
assert "'w2'" in story
|
|
need_keypress("y")
|
|
time.sleep(.2)
|
|
assert len(usb_miniscript_ls()) == 2
|
|
with pytest.raises(Exception) as err:
|
|
usb_miniscript_get("w2")
|
|
assert err.value.args[0] == "Coldcard Error: Miniscript wallet not found"
|
|
|
|
usb_miniscript_delete("w1")
|
|
time.sleep(.2)
|
|
_, story = cap_story()
|
|
assert "Delete miniscript wallet" in story
|
|
assert "'w1'" in story
|
|
need_keypress("y")
|
|
time.sleep(.2)
|
|
assert len(usb_miniscript_ls()) == 1
|
|
with pytest.raises(Exception) as err:
|
|
usb_miniscript_get("w1")
|
|
assert err.value.args[0] == "Coldcard Error: Miniscript wallet not found"
|
|
|
|
usb_miniscript_delete("w0")
|
|
time.sleep(.2)
|
|
_, story = cap_story()
|
|
assert "Delete miniscript wallet" in story
|
|
assert "'w0'" in story
|
|
need_keypress("y")
|
|
time.sleep(.2)
|
|
assert len(usb_miniscript_ls()) == 0
|
|
with pytest.raises(Exception) as err:
|
|
usb_miniscript_get("w0")
|
|
assert err.value.args[0] == "Coldcard Error: Miniscript wallet not found"
|