-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathaddress_types.py
403 lines (347 loc) · 15.4 KB
/
address_types.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
import logging
from bitcoin_usb.i18n import translate
logger = logging.getLogger(__name__)
from typing import Callable, Dict, List, Optional, Sequence, Type
import bdkpython as bdk
from hwilib.common import AddressType as HWIAddressType
from hwilib.descriptor import (
Descriptor,
MultisigDescriptor,
PKHDescriptor,
PubkeyProvider,
SHDescriptor,
TRDescriptor,
WPKHDescriptor,
WSHDescriptor,
parse_descriptor,
)
from hwilib.key import KeyOriginInfo
class ConstDerivationPaths:
receive = "/0/*"
change = "/1/*"
multipath = "/<0;1>/*"
# https://bitcoin.design/guide/glossary/address/
# https://learnmeabitcoin.com/technical/derivation-paths
# https://github.com/bitcoin/bips/blob/master/bip-0380.mediawiki
class AddressType:
def __init__(
self,
short_name: str,
name: str,
is_multisig: bool,
hwi_descriptor_classes: Sequence[Type[Descriptor]],
key_origin: Callable[[bdk.Network], str],
bdk_descriptor_secret: Callable[
[bdk.DescriptorSecretKey, bdk.KeychainKind, bdk.Network], bdk.Descriptor
]
| None = None,
info_url: str | None = None,
description: str | None = None,
bdk_descriptor: Callable[
[bdk.DescriptorPublicKey, str, bdk.KeychainKind, bdk.Network], bdk.Descriptor
]
| None = None,
) -> None:
self.short_name = short_name
self.name = name
self.is_multisig = is_multisig
self.key_origin: Callable[[bdk.Network], str] = key_origin
self.bdk_descriptor_secret = bdk_descriptor_secret
self.info_url = info_url
self.description = description
self.bdk_descriptor = bdk_descriptor
self.hwi_descriptor_classes = hwi_descriptor_classes
def clone(self):
return AddressType(
short_name=self.short_name,
name=self.name,
is_multisig=self.is_multisig,
key_origin=self.key_origin,
bdk_descriptor_secret=self.bdk_descriptor_secret,
info_url=self.info_url,
description=self.description,
bdk_descriptor=self.bdk_descriptor,
hwi_descriptor_classes=self.hwi_descriptor_classes,
)
def __str__(self):
return str(self.name)
def __repr__(self):
return f"AddressType({self.__dict__})"
def get_bip32_path(self, network: bdk.Network, keychain: bdk.KeychainKind, address_index: int) -> str:
return f"m/{0 if keychain == bdk.KeychainKind.EXTERNAL else 1}/{address_index}"
class AddressTypes:
p2pkh = AddressType(
"p2pkh",
"Single Sig (Legacy/p2pkh)",
is_multisig=False,
key_origin=lambda network: f"m/44h/{0 if network==bdk.Network.BITCOIN else 1}h/0h",
bdk_descriptor=bdk.Descriptor.new_bip44_public,
bdk_descriptor_secret=bdk.Descriptor.new_bip44,
info_url="https://learnmeabitcoin.com/technical/derivation-paths",
description="Legacy (single sig) addresses that look like 1addresses",
hwi_descriptor_classes=(PKHDescriptor,),
)
p2sh_p2wpkh = AddressType(
"p2sh-p2wpkh",
"Single Sig (Nested/p2sh-p2wpkh)",
is_multisig=False,
key_origin=lambda network: f"m/49h/{0 if network==bdk.Network.BITCOIN else 1}h/0h",
bdk_descriptor=bdk.Descriptor.new_bip49_public,
bdk_descriptor_secret=bdk.Descriptor.new_bip49,
info_url="https://learnmeabitcoin.com/technical/derivation-paths",
description="Nested (single sig) addresses that look like 3addresses",
hwi_descriptor_classes=(SHDescriptor, WPKHDescriptor),
)
p2wpkh = AddressType(
"p2wpkh",
"Single Sig (SegWit/p2wpkh)",
is_multisig=False,
key_origin=lambda network: f"m/84h/{0 if network==bdk.Network.BITCOIN else 1}h/0h",
bdk_descriptor=bdk.Descriptor.new_bip84_public,
bdk_descriptor_secret=bdk.Descriptor.new_bip84,
info_url="https://learnmeabitcoin.com/technical/derivation-paths",
description="SegWit (single sig) addresses that look like bc1addresses",
hwi_descriptor_classes=(WPKHDescriptor,),
)
p2tr = AddressType(
"p2tr",
"Single Sig (Taproot/p2tr)",
is_multisig=False,
key_origin=lambda network: f"m/86h/{0 if network==bdk.Network.BITCOIN else 1}h/0h",
bdk_descriptor=bdk.Descriptor.new_bip86_public,
bdk_descriptor_secret=bdk.Descriptor.new_bip86,
info_url="https://github.com/bitcoin/bips/blob/master/bip-0386.mediawiki",
description="Taproot (single sig) addresses ",
hwi_descriptor_classes=(TRDescriptor,),
)
p2sh_p2wsh = AddressType(
"p2sh-p2wsh",
"Multi Sig (Nested/p2sh-p2wsh)",
is_multisig=True,
key_origin=lambda network: f"m/48h/{0 if network==bdk.Network.BITCOIN else 1}h/0h/1h",
bdk_descriptor_secret=None,
info_url="https://github.com/bitcoin/bips/blob/master/bip-0048.mediawiki",
description="Nested (multi sig) addresses that look like 3addresses",
hwi_descriptor_classes=(SHDescriptor, WSHDescriptor, MultisigDescriptor),
)
p2wsh = AddressType(
"p2wsh",
"Multi Sig (SegWit/p2wsh)",
is_multisig=True,
key_origin=lambda network: f"m/48h/{0 if network==bdk.Network.BITCOIN else 1}h/0h/2h",
bdk_descriptor_secret=None,
info_url="https://github.com/bitcoin/bips/blob/master/bip-0048.mediawiki",
description="SegWit (multi sig) addresses that look like bc1addresses",
hwi_descriptor_classes=(WSHDescriptor, MultisigDescriptor),
)
def get_address_type_dicts() -> Dict[str, AddressType]:
return {k: v for k, v in AddressTypes.__dict__.items() if (not k.startswith("_"))}
def get_all_address_types() -> List[AddressType]:
return list(get_address_type_dicts().values())
def get_address_types(is_multisig: bool) -> List[AddressType]:
return [a for a in get_all_address_types() if a.is_multisig == is_multisig]
def get_hwi_address_type(address_type: AddressType) -> HWIAddressType:
# see https://hwi.readthedocs.io/en/latest/usage/api-usage.html#hwilib.common.AddressType
if address_type.name in [AddressTypes.p2pkh.name]:
return HWIAddressType.LEGACY
if address_type.name in [AddressTypes.p2wpkh.name, AddressTypes.p2wsh.name]:
return HWIAddressType.WIT
if address_type.name in [
AddressTypes.p2sh_p2wpkh.name,
AddressTypes.p2sh_p2wsh.name,
]:
return HWIAddressType.SH_WIT
if address_type.name in [AddressTypes.p2tr.name]:
return HWIAddressType.TAP
raise ValueError(
translate("bitcoin_usb", "No HWI AddressType could be found for {name}").format(
name=address_type.name
)
)
class SimplePubKeyProvider:
def __init__(
self,
xpub: str,
fingerprint: str,
key_origin: str,
derivation_path: str = ConstDerivationPaths.receive,
) -> None:
self.xpub = xpub.strip()
self.fingerprint = self.format_fingerprint(fingerprint)
# key_origin example: "m/84h/1h/0h"
self.key_origin = self.format_key_origin(key_origin)
# derivation_path example "/0/*"
self.derivation_path = self.format_derivation_path(derivation_path)
@classmethod
def format_derivation_path(cls, value: str) -> str:
value = value.replace(" ", "").strip()
if not value.startswith("/"):
raise ValueError(
translate("bitcoin_usb", "derivation_path {value} must start with a /").format(value=value)
)
return value.replace("'", "h")
@classmethod
def format_key_origin(cls, value: str) -> str:
def filter_characters(s):
allowed_chars = set("m/'h0123456789")
filtered_string = "".join(c for c in s if c in allowed_chars)
return filtered_string
value = filter_characters(value.replace("'", "h").strip())
if value == "m":
# handle the special case that the key is the highest key without derivation
return value
for group in value.split("/"):
if group.count("h") > 1:
raise ValueError(translate("bitcoin_usb", "h cannot appear twice in a index"))
if not value.startswith("m/"):
raise ValueError(translate("bitcoin_usb", "{value} must start with m/").format(value=value))
if "//" in value:
raise ValueError(translate("bitcoin_usb", "{value} cannot contain //").format(value=value))
if "/h" in value:
raise ValueError(translate("bitcoin_usb", "{value} cannot contain /h").format(value=value))
if "hh" in value:
raise ValueError(translate("bitcoin_usb", "{value} cannot contain hh").format(value=value))
if value.endswith("/"):
raise ValueError(translate("bitcoin_usb", "{value} cannot end with /").format(value=value))
return value
@classmethod
def is_fingerprint_valid(cls, fingerprint: str):
try:
int(fingerprint, 16)
return len(fingerprint) == 8
except ValueError:
return False
@classmethod
def format_fingerprint(cls, value: str) -> str:
value = value.replace(" ", "").strip()
if not cls.is_fingerprint_valid(value):
raise ValueError(
translate("bitcoin_usb", "{value} is not a valid fingerprint").format(value=value)
)
return value.upper()
def clone(self) -> "SimplePubKeyProvider":
return SimplePubKeyProvider(self.xpub, self.fingerprint, self.key_origin, self.derivation_path)
def is_testnet(self):
network_str = self.key_origin.split("/")[2]
if not network_str.endswith("h"):
raise ValueError(
translate(
"bitcoin_usb",
"The network part {network_str} of the key origin {key_origin} must be hardened with a h",
).format(network_str=network_str, key_origin=self.key_origin)
)
network_index = int(network_str.replace("h", ""))
if network_index == 0:
return False
elif network_index == 1:
return True
else:
# https://learnmeabitcoin.com/technical/derivation-paths
raise ValueError(
translate("bitcoin_usb", "Unknown network/coin type {network_str} in {key_origin}").format(
network_str=network_str, key_origin=self.key_origin
)
)
@classmethod
def from_hwi(cls, pubkey_provider: PubkeyProvider) -> "SimplePubKeyProvider":
return SimplePubKeyProvider(
xpub=pubkey_provider.pubkey,
fingerprint=pubkey_provider.origin.fingerprint.hex(),
key_origin=pubkey_provider.origin.get_derivation_path(),
derivation_path=pubkey_provider.deriv_path,
)
def to_hwi_pubkey_provider(self) -> PubkeyProvider:
provider = PubkeyProvider(
origin=KeyOriginInfo.from_string(self.key_origin.replace("m", f"{self.fingerprint}")),
pubkey=self.xpub,
deriv_path=self.derivation_path,
)
return provider
def __repr__(self) -> str:
return f"{self.__class__.__name__}({self.__dict__})"
def _get_descriptor_instances(descriptor: Descriptor) -> List[Descriptor]:
"Returns the linear chain of chained descriptors . Multiple subdescriptors return an error"
assert len(descriptor.subdescriptors) <= 1
if descriptor.subdescriptors:
result = [descriptor]
for subdescriptor in descriptor.subdescriptors:
result += _get_descriptor_instances(subdescriptor)
return result
else:
return [descriptor]
def _find_matching_address_type(
descriptor_tuple: List[Descriptor], address_types: List[AddressType]
) -> Optional[AddressType]:
for address_type in address_types:
if len(descriptor_tuple) == len(address_type.hwi_descriptor_classes) and all(
isinstance(i, c) for i, c in zip(descriptor_tuple, address_type.hwi_descriptor_classes)
):
return address_type
return None
class DescriptorInfo:
def __init__(
self,
address_type: AddressType,
spk_providers: List[SimplePubKeyProvider],
threshold=1,
) -> None:
self.address_type: AddressType = address_type
self.spk_providers: List[SimplePubKeyProvider] = spk_providers
self.threshold: int = threshold
if not self.address_type.is_multisig:
assert len(spk_providers) <= 1
def __repr__(self) -> str:
return f"{self.__dict__}"
def get_hwi_descriptor(self, network: bdk.Network):
# check that the key_origins of the spk_providers are matching the desired output address_type
for spk_provider in self.spk_providers:
if spk_provider.key_origin != self.address_type.key_origin(network):
logger.warning(
f"{spk_provider.key_origin} does not match the default key origin {self.address_type.key_origin(network)} for this address type {self.address_type.name}!"
)
if self.address_type.is_multisig:
assert self.address_type.hwi_descriptor_classes[-1] == MultisigDescriptor
hwi_descriptor = MultisigDescriptor(
pubkeys=[provider.to_hwi_pubkey_provider() for provider in self.spk_providers],
thresh=self.threshold,
is_sorted=True,
)
else:
hwi_descriptor = self.address_type.hwi_descriptor_classes[-1](
self.spk_providers[0].to_hwi_pubkey_provider()
)
for hwi_descriptor_class in reversed(self.address_type.hwi_descriptor_classes[:-1]):
hwi_descriptor = hwi_descriptor_class(hwi_descriptor)
return hwi_descriptor
def get_bdk_descriptor(self, network: bdk.Network):
return bdk.Descriptor(self.get_hwi_descriptor(network).to_string(), network=network)
@classmethod
def from_str(cls, descriptor_str: str) -> "DescriptorInfo":
hwi_descriptor = parse_descriptor(descriptor_str)
# first we need to identify the address type
address_type = _find_matching_address_type(
_get_descriptor_instances(hwi_descriptor), get_all_address_types()
)
if not address_type:
supported_types = [address_type.short_name for address_type in get_all_address_types()]
raise ValueError(
f"descriptor {descriptor_str} cannot be matched to a supported template. Supported templates are {supported_types}"
)
# get the pubkey_providers, by "walking to the end of desciptors"
threshold = 1
subdescriptor = hwi_descriptor
for descritptor_class in address_type.hwi_descriptor_classes:
# just double checking that _find_matching_address_type did its job correctly
assert isinstance(subdescriptor, descritptor_class)
subdescriptor = subdescriptor.subdescriptors[0] if subdescriptor.subdescriptors else subdescriptor
pubkey_providers = subdescriptor.pubkeys
if isinstance(subdescriptor, MultisigDescriptor):
# last descriptor is a multisig
threshold = subdescriptor.thresh
return DescriptorInfo(
address_type=address_type,
spk_providers=[
SimplePubKeyProvider.from_hwi(pubkey_provider) for pubkey_provider in pubkey_providers
],
threshold=threshold,
)