Skip to content
This repository was archived by the owner on Apr 28, 2025. It is now read-only.

Commit 41708e4

Browse files
committed
generate real UR "Output" descriptor type QR code when possible
1 parent 0ee362b commit 41708e4

File tree

1 file changed

+151
-10
lines changed

1 file changed

+151
-10
lines changed

seedqreader.py

Lines changed: 151 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -30,10 +30,11 @@
3030
from foundation.ur import UR
3131

3232
from urtypes.crypto import PSBT as UR_PSBT
33-
from urtypes.crypto import Account, Output
33+
from urtypes.crypto import Account, Output, HDKey, ECKey, MultiKey, Keypath, PathComponent, SCRIPT_EXPRESSION_TAG_MAP
3434
from urtypes.bytes import Bytes
3535

3636
from embit.psbt import PSBT
37+
from embit.descriptor import Descriptor as EmbitDescriptor
3738

3839
from mss import mss
3940
import numpy as np
@@ -63,6 +64,139 @@ def to_str(bin_):
6364
return bin_.decode('utf-8')
6465

6566

67+
def descriptor_to_output(descriptor_str):
68+
"""Convert a descriptor string to a urtypes Output object."""
69+
from embit.networks import NETWORKS
70+
import binascii
71+
72+
# Parse descriptor using embit
73+
embit_desc = EmbitDescriptor.from_string(descriptor_str)
74+
75+
# Check for advanced miniscript - crypto-output only supports basic descriptors
76+
if hasattr(embit_desc, 'miniscript') and embit_desc.miniscript and not embit_desc.is_basic_multisig:
77+
# Check if it's an advanced miniscript (not just pk/pkh/wpkh)
78+
miniscript_str = str(embit_desc.miniscript)
79+
advanced_operators = ['or_d', 'or_c', 'or_i', 'or_b', 'and_v', 'and_b', 'and_n',
80+
'andor', 'thresh', 'older', 'after', 'sha256', 'hash256',
81+
'ripemd160', 'hash160']
82+
if any(op in miniscript_str for op in advanced_operators):
83+
raise ValueError(f"crypto-output does not support advanced miniscript: {miniscript_str}")
84+
85+
# Build script expressions list based on descriptor type
86+
script_expressions = []
87+
script_type = embit_desc.scriptpubkey_type()
88+
89+
# Map embit script types to urtypes script expressions
90+
# sh = 400, wsh = 401, pk = 402, pkh = 403, wpkh = 404, multi = 406, sortedmulti = 407
91+
if embit_desc.is_wrapped:
92+
script_expressions.append(SCRIPT_EXPRESSION_TAG_MAP[400]) # sh
93+
94+
if script_type == "p2wsh":
95+
script_expressions.append(SCRIPT_EXPRESSION_TAG_MAP[401]) # wsh
96+
if embit_desc.is_basic_multisig:
97+
if embit_desc.is_sorted:
98+
script_expressions.append(SCRIPT_EXPRESSION_TAG_MAP[407]) # sortedmulti
99+
else:
100+
script_expressions.append(SCRIPT_EXPRESSION_TAG_MAP[406]) # multi
101+
elif script_type == "p2wpkh":
102+
script_expressions.append(SCRIPT_EXPRESSION_TAG_MAP[404]) # wpkh
103+
elif script_type == "p2pkh":
104+
script_expressions.append(SCRIPT_EXPRESSION_TAG_MAP[403]) # pkh
105+
elif script_type == "p2pk":
106+
script_expressions.append(SCRIPT_EXPRESSION_TAG_MAP[402]) # pk
107+
108+
# Convert keys
109+
embit_keys = embit_desc.keys
110+
111+
# Handle multisig
112+
if embit_desc.is_basic_multisig:
113+
# Get threshold from the miniscript args
114+
threshold = 1 # default
115+
if hasattr(embit_desc, 'miniscript') and embit_desc.miniscript:
116+
# The first argument is a Number object with the threshold
117+
threshold = embit_desc.miniscript.args[0].num
118+
119+
ec_keys = []
120+
hd_keys = []
121+
122+
for embit_key in embit_keys:
123+
if embit_key.is_extended:
124+
hd_keys.append(_convert_hd_key(embit_key))
125+
else:
126+
ec_keys.append(_convert_ec_key(embit_key))
127+
128+
crypto_key = MultiKey(threshold, ec_keys, hd_keys)
129+
else:
130+
# Single key
131+
embit_key = embit_keys[0]
132+
if embit_key.is_extended:
133+
crypto_key = _convert_hd_key(embit_key)
134+
else:
135+
crypto_key = _convert_ec_key(embit_key)
136+
137+
return Output(script_expressions, crypto_key)
138+
139+
140+
def _convert_ec_key(embit_key):
141+
"""Convert embit Key to urtypes ECKey."""
142+
import binascii
143+
144+
# Get the public key bytes
145+
pubkey_bytes = embit_key.key.sec()
146+
147+
# ECKey(data, origin=None, name=None)
148+
return ECKey(pubkey_bytes, None, None)
149+
150+
151+
def _convert_hd_key(embit_key):
152+
"""Convert embit extended Key to urtypes HDKey."""
153+
import binascii
154+
from urtypes.crypto import CoinInfo
155+
156+
xpub = embit_key.key
157+
158+
# Build HDKey dict
159+
hd_dict = {
160+
"private_key": False, # Explicitly mark as public key (required for proper CBOR encoding)
161+
"key": xpub.key.sec(),
162+
"chain_code": xpub.chain_code,
163+
}
164+
165+
# Handle origin (derivation path)
166+
if embit_key.origin:
167+
origin_components = []
168+
for component in embit_key.origin.derivation:
169+
is_hardened = component >= 0x80000000
170+
index = component - 0x80000000 if is_hardened else component
171+
origin_components.append(PathComponent(index, is_hardened))
172+
173+
origin_fingerprint = embit_key.origin.fingerprint
174+
# Set depth to the number of components in the origin path
175+
origin_depth = len(origin_components)
176+
hd_dict["origin"] = Keypath(
177+
origin_components,
178+
origin_fingerprint,
179+
origin_depth
180+
)
181+
182+
# Add use_info for Bitcoin (type=0, network=0 for mainnet, 1 for testnet)
183+
# Determine network from coin type in origin path (coin_type 0 = mainnet, 1 = testnet)
184+
network = 0 # Default to mainnet
185+
if embit_key.origin and len(embit_key.origin.derivation) >= 2:
186+
coin_type = embit_key.origin.derivation[1]
187+
# Remove hardened bit to get coin type value
188+
coin_type_val = coin_type - 0x80000000 if coin_type >= 0x80000000 else coin_type
189+
network = 1 if coin_type_val == 1 else 0
190+
191+
hd_dict["use_info"] = CoinInfo(0, network)
192+
193+
# Parent fingerprint
194+
if hasattr(xpub, 'fingerprint'):
195+
hd_dict["parent_fingerprint"] = xpub.fingerprint
196+
197+
return HDKey(hd_dict)
198+
199+
66200
@dataclass
67201
class QRCode:
68202
data: str = ''
@@ -282,25 +416,32 @@ def from_string(data, _max=MAX_LEN, type=None, format=None):
282416
out.data = sequence
283417

284418
elif format == FORMAT_UR:
285-
_UR = None
419+
if not _max:
420+
_max = 100000
421+
286422
if type == 'PSBT':
287423
out.data_type = 'crypto-psbt'
288424
data = PSBT.from_string(data).serialize()
289-
_UR = UR_PSBT
425+
ur = UR(out.data_type, UR_PSBT(data).to_cbor())
290426
elif type == 'Descriptor':
291-
out.data_type = 'bytes'
292-
_UR = Bytes
427+
# Try to encode as crypto-output, fall back to bytes for complex descriptors
428+
try:
429+
out.data_type = 'crypto-output'
430+
output_obj = descriptor_to_output(data)
431+
ur = UR(out.data_type, output_obj.to_cbor())
432+
except Exception as e:
433+
print(f"Cannot encode as crypto-output ({e}), encoding as bytes instead")
434+
out.data_type = 'bytes'
435+
ur = UR(out.data_type, Bytes(data).to_cbor())
293436
elif type == 'Key':
294437
out.data_type = 'bytes'
295-
_UR = Bytes
438+
ur = UR(out.data_type, Bytes(data).to_cbor())
296439
elif type == 'Bytes':
297440
out.data_type = 'bytes'
298-
_UR = Bytes
441+
ur = UR(out.data_type, Bytes(data).to_cbor())
299442
else:
300443
return
301-
if not _max:
302-
_max = 100000
303-
ur = UR(out.data_type, _UR(data).to_cbor())
444+
304445
out.encoder = UREncoder(ur, _max)
305446
out.total_sequences = out.encoder.fountain_encoder.seq_len()
306447
else:

0 commit comments

Comments
 (0)