ownership: search particular named wallet via BIP-21 wallet query param
This commit is contained in:
parent
2d5e1234e8
commit
e01822c25e
@ -22,7 +22,10 @@ Spending policies for "Single Signers" adds new spending policy options:
|
||||
- Enhancement: Show QR codes of output addresses in transaction output explorer. Explorer is
|
||||
now offered for transactions of all sizes, not just complex ones.
|
||||
- Enhancement: Added file rename, when listing contents of SD card.
|
||||
- Enhancement: Added ability to restore Coldcard backup via USB (requires updated ckcc, other)
|
||||
- Enhancement: Added ability to restore Coldcard backup via USB (TODO version of updated ckcc)
|
||||
- Enhancement: Address ownership allows to specify particular multisig wallet in which to search.
|
||||
`wallet` query parameter is provided via [BIP-21](https://github.com/bitcoin/bips/blob/master/bip-0021.mediawiki)
|
||||
example: `tb1q4d67p7stxml3kdudrgkg5mgaxsrgzcqzjrrj4gg62nxtvnsnvqjsxjkej0?wallet=my_wal`
|
||||
- Bugfix: If all change outputs have `nValue=0`, they were not shown in UX.
|
||||
- Bugfix: Disallow negative input/output amounts in PSBT.
|
||||
- Bugfix: Fix filesystem initialization after Wife LFS or Destroy Seed.
|
||||
|
||||
@ -713,7 +713,7 @@ class NFCHandler:
|
||||
m = m.decode()
|
||||
what, vals = decode_bip21_text(m)
|
||||
if what == 'addr':
|
||||
return vals[1]
|
||||
return vals
|
||||
|
||||
winner = await self._nfc_reader(f, 'Unable to find address from NFC data.')
|
||||
|
||||
@ -721,10 +721,10 @@ class NFCHandler:
|
||||
|
||||
async def verify_address_nfc(self):
|
||||
# Get an address or complete bip-21 url even and search it... slow.
|
||||
winner = await self.read_address()
|
||||
if winner:
|
||||
_, addr, args = await self.read_address()
|
||||
if addr:
|
||||
from ownership import OWNERSHIP
|
||||
await OWNERSHIP.search_ux(winner)
|
||||
await OWNERSHIP.search_ux(addr, args)
|
||||
|
||||
async def read_extended_private_key(self):
|
||||
f = lambda x: x.decode().strip() if b"prv" in x else None
|
||||
|
||||
@ -211,19 +211,31 @@ class OwnershipCache:
|
||||
return doit
|
||||
|
||||
@classmethod
|
||||
def filter(cls, addr):
|
||||
def filter(cls, addr, args):
|
||||
# Filter possible candidates!
|
||||
# - if you start w/ testnet, we'll follow that
|
||||
from wallet import MiniScriptWallet
|
||||
from glob import dis
|
||||
|
||||
ch = chains.current_chain()
|
||||
args = args or {}
|
||||
|
||||
addr_fmt = ch.possible_address_fmt(addr)
|
||||
if not addr_fmt:
|
||||
# might be valid address over on testnet vs mainnet
|
||||
raise UnknownAddressExplained('That address is not valid on ' + ch.name)
|
||||
|
||||
# user has specified specific (named) wallet
|
||||
named_wal = args.get("wallet", None)
|
||||
if named_wal:
|
||||
# quick search without deserialization
|
||||
res = list(MiniScriptWallet.iter_wallets(name=named_wal))
|
||||
if not res:
|
||||
raise UnknownAddressExplained("Wallet '%s' not defined." % named_wal)
|
||||
|
||||
# only return desired named wallet, no other wallets are searched
|
||||
return res
|
||||
|
||||
possibles = []
|
||||
if addr_fmt == AF_P2TR:
|
||||
possibles.extend([w for w in MiniScriptWallet.iter_wallets() if w.addr_fmt == AF_P2TR])
|
||||
@ -293,10 +305,10 @@ class OwnershipCache:
|
||||
return None, None
|
||||
|
||||
@classmethod
|
||||
def search(cls, addr):
|
||||
def search(cls, addr, args=None):
|
||||
from glob import dis
|
||||
|
||||
matches = OWNERSHIP.filter(addr)
|
||||
matches = OWNERSHIP.filter(addr, args)
|
||||
|
||||
# build cache files for both external & internal chain
|
||||
cachefs = []
|
||||
@ -334,7 +346,7 @@ class OwnershipCache:
|
||||
' without finding a match.' % (c, len(matches)))
|
||||
|
||||
@classmethod
|
||||
async def search_ux(cls, addr):
|
||||
async def search_ux(cls, addr, args):
|
||||
# Provide a simple UX. Called functions do fullscreen, progress bar stuff.
|
||||
from ux import ux_show_story, show_qr_code
|
||||
from charcodes import KEY_QR
|
||||
@ -342,7 +354,7 @@ class OwnershipCache:
|
||||
from public_constants import AFC_BECH32, AFC_BECH32M
|
||||
|
||||
try:
|
||||
_, wallet, subpath = cls.search(addr)
|
||||
_, wallet, subpath = cls.search(addr, args)
|
||||
is_complex = isinstance(wallet, MiniScriptWallet)
|
||||
|
||||
sp = None
|
||||
|
||||
@ -1129,7 +1129,7 @@ async def ux_visualize_bip21(proto, addr, args):
|
||||
|
||||
if ch == '1':
|
||||
from ownership import OWNERSHIP
|
||||
await OWNERSHIP.search_ux(addr)
|
||||
await OWNERSHIP.search_ux(addr, args)
|
||||
|
||||
async def ux_visualize_wif(wif_str, kp, compressed, testnet):
|
||||
# TODO: remove until we support signing w/ WIF keys IMHO
|
||||
|
||||
@ -58,40 +58,46 @@ def test_detector_bin(fname, expect, encoding, try_decode):
|
||||
|
||||
@pytest.mark.parametrize('url', [
|
||||
'mtHSVByP9EYZmB26jASDdPVm19gvpecb5R',
|
||||
'BCRT1QUPYD58NDSH7LUT0ET0VTRQ432JVU9JTDX8FGYV',
|
||||
'mtHSVByP9EYZmB26jASDdPVm19gvpecb5R?label=Luke-Jr',
|
||||
'mtHSVByP9EYZmB26jASDdPVm19gvpecb5R?amount=20.3&label=Luke-Jr',
|
||||
'mtHSVByP9EYZmB26jASDdPVm19gvpecb5R?amount=50&label=Luke-Jr&message=Donation%20for%20project%20xyz',
|
||||
'BCRT1QUPYD58NDSH7LUT0ET0VTRQ432JVU9JTDX8FGYV?amount=50&label=Luke-Jr&message=Donation%20for%20project%20xyz',
|
||||
'mtHSVByP9EYZmB26jASDdPVm19gvpecb5R?req-somethingyoudontunderstand=50&req-somethingelseyoudontget=999',
|
||||
'mtHSVByP9EYZmB26jASDdPVm19gvpecb5R?somethingyoudontunderstand=50&somethingelseyoudontget=999',
|
||||
'tb1q4d67p7stxml3kdudrgkg5mgaxsrgzcqzjrrj4gg62nxtvnsnvqjsxjkej0?wallet=my_wal',
|
||||
'tb1q4d67p7stxml3kdudrgkg5mgaxsrgzcqzjrrj4gg62nxtvnsnvqjsxjkej0?wallet=my wal',
|
||||
'tb1q4d67p7stxml3kdudrgkg5mgaxsrgzcqzjrrj4gg62nxtvnsnvqjsxjkej0?wallet=my%20wal',
|
||||
'tb1q4d67p7stxml3kdudrgkg5mgaxsrgzcqzjrrj4gg62nxtvnsnvqjsxjkej0?wallet=my:wal',
|
||||
'tb1q4d67p7stxml3kdudrgkg5mgaxsrgzcqzjrrj4gg62nxtvnsnvqjsxjkej0?wallet=my-wal',
|
||||
'mtHSVByP9EYZmB26jASDdPVm19gvpecb5R?label=total%20due:%20500',
|
||||
])
|
||||
@pytest.mark.parametrize('bip21', range(2))
|
||||
@pytest.mark.parametrize('addr_fmt', range(2))
|
||||
def test_detector_url(url, bip21, addr_fmt, try_decode):
|
||||
a1, a2 = ('mtHSVByP9EYZmB26jASDdPVm19gvpecb5R',
|
||||
'BCRT1QUPYD58NDSH7LUT0ET0VTRQ432JVU9JTDX8FGYV')
|
||||
def test_detector_url(url, bip21, try_decode):
|
||||
target = url.split('?', 1)[0]
|
||||
if target[:2].lower() in ["tb", "bc"]:
|
||||
target = target.lower()
|
||||
|
||||
if bip21:
|
||||
url = 'bitcoin:' + url
|
||||
|
||||
if addr_fmt:
|
||||
url = url.replace(a1, a2)
|
||||
expect_addr = a2.lower()
|
||||
else:
|
||||
expect_addr = a1
|
||||
url = f"bitcoin:{url}"
|
||||
|
||||
ft, vals = try_decode(url)
|
||||
assert ft == 'addr'
|
||||
proto, addr, args = vals
|
||||
assert addr == expect_addr
|
||||
assert addr == target
|
||||
assert proto == ('bitcoin' if bip21 else None)
|
||||
|
||||
p = urlparse(url)
|
||||
assert (p.path == addr) or (p.path.lower() == addr.lower())
|
||||
|
||||
# below nest values to the list
|
||||
xargs = parse_qs(p.query)
|
||||
if args:
|
||||
assert xargs.keys() == args.keys()
|
||||
assert len(xargs) == len(args)
|
||||
for k, v in args.items():
|
||||
val = xargs[k]
|
||||
assert len(val) == 1
|
||||
# unwrap value
|
||||
assert val[0] == v
|
||||
else:
|
||||
assert not xargs
|
||||
|
||||
|
||||
@ -595,4 +595,118 @@ def test_20_more_build_after_match(sim_exec, import_ms_wallet, clear_ms, wipe_ca
|
||||
assert "1 wallet(s)" in l
|
||||
assert 'without finding a match' in l
|
||||
|
||||
|
||||
def test_named_wallet_search_fail(load_shared_mod, goto_home, pick_menu_item, nfc_write,
|
||||
cap_story):
|
||||
addr = fake_address(AF_P2WSH, True)
|
||||
addr = f"{addr}?wallet=unknown"
|
||||
cc_ndef = load_shared_mod('cc_ndef', '../shared/ndef.py')
|
||||
n = cc_ndef.ndefMaker()
|
||||
n.add_text(addr)
|
||||
ccfile = n.bytes()
|
||||
|
||||
# run simulator w/ --set nfc=1 --eff
|
||||
goto_home()
|
||||
pick_menu_item('Advanced/Tools')
|
||||
pick_menu_item('NFC Tools')
|
||||
pick_menu_item('Verify Address')
|
||||
open('debug/nfc-addr.ndef', 'wb').write(ccfile)
|
||||
nfc_write(ccfile)
|
||||
|
||||
time.sleep(1)
|
||||
title, story = cap_story()
|
||||
assert addr.split("?", 1)[0] == addr_from_display_format(story.split("\n\n")[0])
|
||||
assert "Wallet 'unknown' not defined." in story
|
||||
|
||||
|
||||
@pytest.mark.parametrize('valid', [True, False])
|
||||
@pytest.mark.parametrize('method', ["qr", "nfc"])
|
||||
def test_named_wallet_search(valid, method, clear_ms, import_ms_wallet, is_q1,
|
||||
load_shared_mod, goto_home, pick_menu_item, scan_a_qr,
|
||||
cap_story, need_keypress, nfc_write, use_testnet,
|
||||
wipe_cache, settings_set):
|
||||
|
||||
from test_multisig import make_ms_address, HARD
|
||||
|
||||
if method == "qr" and (not is_q1):
|
||||
raise pytest.skip("QR Mk")
|
||||
|
||||
wipe_cache() # very different codepaths
|
||||
settings_set('accts', [])
|
||||
use_testnet()
|
||||
M, N = 2, 3
|
||||
clear_ms()
|
||||
ms_data = {}
|
||||
# all ms wallets have same address format, different M/N
|
||||
for i in range(3):
|
||||
idx = 5
|
||||
if i == 2:
|
||||
idx = 763
|
||||
name = f'msnw{i}'
|
||||
keys = import_ms_wallet(M+i, N+i, AF_P2WSH, name=name, accept=True)
|
||||
# last address
|
||||
addr, scriptPubKey, script, details = make_ms_address(
|
||||
M+i, keys, is_change=0, idx=idx, addr_fmt=AF_P2WSH,
|
||||
testnet=True, path_mapper=lambda cosigner: [HARD(45), 0, idx]
|
||||
)
|
||||
ms_data[name] = (addr, scriptPubKey, script, keys)
|
||||
|
||||
if valid:
|
||||
# msnw2 -> last added wallet
|
||||
addr, *_ = ms_data["msnw2"]
|
||||
else:
|
||||
# will fail, even tho address is present in different wallet
|
||||
# with wallet=<wal> only specified wallet is searched
|
||||
addr, *_ = ms_data["msnw0"]
|
||||
|
||||
# will only search specified wallet
|
||||
addr = f"{addr}?wallet=msnw2"
|
||||
|
||||
if method == 'qr':
|
||||
goto_home()
|
||||
pick_menu_item('Scan Any QR Code')
|
||||
scan_a_qr(addr)
|
||||
time.sleep(1)
|
||||
|
||||
title, story = cap_story()
|
||||
|
||||
assert addr.split("?", 1)[0] == addr_from_display_format(story.split("\n\n")[0])
|
||||
assert '(1) to verify ownership' in story
|
||||
need_keypress('1')
|
||||
|
||||
elif method == 'nfc':
|
||||
cc_ndef = load_shared_mod('cc_ndef', '../shared/ndef.py')
|
||||
n = cc_ndef.ndefMaker()
|
||||
n.add_text(addr)
|
||||
ccfile = n.bytes()
|
||||
|
||||
# run simulator w/ --set nfc=1 --eff
|
||||
goto_home()
|
||||
pick_menu_item('Advanced/Tools')
|
||||
pick_menu_item('NFC Tools')
|
||||
pick_menu_item('Verify Address')
|
||||
open('debug/nfc-addr.ndef', 'wb').write(ccfile)
|
||||
nfc_write(ccfile)
|
||||
# press_select()
|
||||
|
||||
else:
|
||||
raise ValueError(method)
|
||||
|
||||
time.sleep(1)
|
||||
title, story = cap_story()
|
||||
assert addr.split("?", 1)[0] == addr_from_display_format(story.split("\n\n")[0])
|
||||
|
||||
if valid:
|
||||
assert title == ('Verified Address' if is_q1 else "Verified!")
|
||||
assert 'Found in wallet' in story
|
||||
assert 'Derivation path' in story
|
||||
|
||||
assert "msnw2" in story
|
||||
|
||||
else:
|
||||
assert title == 'Unknown Address'
|
||||
assert 'Searched 1528' in story # max
|
||||
assert "1 wallet(s)" in story
|
||||
assert 'without finding a match' in story
|
||||
|
||||
# EOF
|
||||
|
||||
Loading…
Reference in New Issue
Block a user