wallet_hd_derivation_kit
Offline, native HD-wallet key and address derivation.
1"""Offline, native HD-wallet key and address derivation.""" 2 3from .core import ( 4 API_SCHEMA_VERSION, 5 HDWalletError, 6 ParsedExtendedKey, 7 derive_account_private_key, 8 derive_account_public_key, 9 derive_address, 10 derive_address_from_extended_public_key, 11 derive_addresses, 12 derive_node, 13 parse_extended_key, 14 serialize_extended_key, 15 supported_chains, 16) 17 18__all__ = [ 19 "API_SCHEMA_VERSION", 20 "HDWalletError", 21 "ParsedExtendedKey", 22 "derive_node", 23 "derive_account_public_key", 24 "derive_account_private_key", 25 "derive_address", 26 "derive_addresses", 27 "derive_address_from_extended_public_key", 28 "parse_extended_key", 29 "serialize_extended_key", 30 "supported_chains", 31]
API_SCHEMA_VERSION =
1
class
HDWalletError(builtins.ValueError):
Raised for invalid or unsupported derivation requests.
@dataclass(frozen=True)
class
ParsedExtendedKey:
470@dataclass(frozen=True) 471class ParsedExtendedKey: 472 """Parsed extended-key metadata plus an opaque native node.""" 473 474 value: str = field(repr=False) 475 version_hex: str = "" 476 format: str = "" 477 is_private: bool = False 478 depth: int = 0 479 child_number: int = 0 480 parent_fingerprint_hex: str = "" 481 chain_code_hex: str = field(default="", repr=False) 482 public_key_hex: str = "" 483 _node: _SecpNode = field(default_factory=_SecpNode, repr=False) 484 485 def to_dict(self) -> dict[str, Any]: 486 return {key: value for key, value in self.__dict__.items() if key != "_node"}
Parsed extended-key metadata plus an opaque native node.
def
derive_node( source: Union[Mapping[str, Any], bytes], curve: str = 'secp256k1', path: str = 'm') -> dict[str, typing.Any]:
412def derive_node(source: Mapping[str, Any] | bytes, curve: str = "secp256k1", path: str = "m") -> dict[str, Any]: 413 seed = _source_seed(source) 414 if curve == "ed25519": 415 derived_node: _EdNode | _SecpNode = _ed25519_from_seed(seed, path) 416 elif curve == "secp256k1": 417 derived_node = _SecpNode.from_seed(seed).derive_path(path) 418 else: 419 raise HDWalletError(f"unsupported curve: {curve}") 420 return {"schemaVersion": API_SCHEMA_VERSION, "curve": curve, "path": path, "publicKeyHex": derived_node.public_key.hex(), "chainCodeHex": derived_node.chain_code.hex(), "depth": derived_node.depth, "childNumber": derived_node.child_number}
def
derive_account_public_key( source: Union[Mapping[str, Any], bytes], chain: str = 'bitcoin', script_type: str | None = None, account: int = 0, format: str | None = None, path: str | None = None) -> dict[str, typing.Any]:
423def derive_account_public_key(source: Mapping[str, Any] | bytes, chain: str = "bitcoin", script_type: str | None = None, account: int = 0, format: str | None = None, path: str | None = None) -> dict[str, Any]: 424 info = _chain(chain) 425 if info["curve"] == "ed25519": 426 raise HDWalletError("Solana SLIP-0010 does not define extended public keys") 427 selected, fmt = _format(chain, format, script_type) or ("xpub", FORMATS["xpub"]) 428 resolved_path = path or _account_path(chain, account, format, script_type) 429 node = _SecpNode.from_seed(_source_seed(source)).derive_path(resolved_path) 430 return {"schemaVersion": API_SCHEMA_VERSION, "chain": chain, "curve": "secp256k1", "path": resolved_path, "format": selected, "extendedPublicKey": node.serialize(bytes.fromhex(fmt["public"]), private=False), "publicKeyHex": node.public_key.hex()}
def
derive_account_private_key( source: Union[Mapping[str, Any], bytes], chain: str = 'bitcoin', script_type: str | None = None, account: int = 0, format: str | None = None, path: str | None = None) -> dict[str, typing.Any]:
433def derive_account_private_key(source: Mapping[str, Any] | bytes, chain: str = "bitcoin", script_type: str | None = None, account: int = 0, format: str | None = None, path: str | None = None) -> dict[str, Any]: 434 """Explicitly export account private material.""" 435 info = _chain(chain) 436 resolved_path = path or _account_path(chain, account, format, script_type) 437 if info["curve"] == "ed25519": 438 ed_node = _ed25519_from_seed(_source_seed(source), resolved_path) 439 return {"schemaVersion": 1, "chain": chain, "curve": "ed25519", "path": resolved_path, "extendedPrivateKey": None, "privateKeyHex": ed_node.private_key.hex(), "publicKeyHex": ed_node.public_key.hex()} 440 selected, fmt = _format(chain, format, script_type) or ("xpub", FORMATS["xpub"]) 441 secp_node = _SecpNode.from_seed(_source_seed(source)).derive_path(resolved_path) 442 assert secp_node.private_key is not None 443 return {"schemaVersion": 1, "chain": chain, "curve": "secp256k1", "path": resolved_path, "format": selected, "extendedPrivateKey": secp_node.serialize(bytes.fromhex(fmt["private"]), private=True), "privateKeyHex": secp_node.private_key.hex(), "publicKeyHex": secp_node.public_key.hex()}
Explicitly export account private material.
def
derive_address( source: Union[Mapping[str, Any], bytes], chain: str = 'bitcoin', account: int = 0, change: int = 0, index: int = 0, script_type: str | None = None, format: str | None = None, path: str | None = None) -> dict[str, typing.Any]:
446def derive_address(source: Mapping[str, Any] | bytes, chain: str = "bitcoin", account: int = 0, change: int = 0, index: int = 0, script_type: str | None = None, format: str | None = None, path: str | None = None) -> dict[str, Any]: 447 info = _chain(chain) 448 account, change, index = _index(account, "account"), _index(change, "change"), _index(index, "index") 449 script = script_type or info.get("defaultScriptType") or (_format(chain, format, None) or (None, {"script": "p2pkh"}))[1]["script"] 450 if info["curve"] == "ed25519": 451 resolved_path = path or f"m/44'/{info['coinType']}'/{account}'/{index}'" 452 ed_node = _ed25519_from_seed(_source_seed(source), resolved_path) 453 public_key = ed_node.public_key 454 address = _base58_encode(public_key) 455 else: 456 resolved_path = path or f"{_account_path(chain, account, format, script_type)}/{change}/{index}" 457 secp_node = _SecpNode.from_seed(_source_seed(source)).derive_path(resolved_path) 458 public_key = secp_node.public_key 459 address = _address(public_key, chain, script) 460 return {"schemaVersion": 1, "chain": chain, "curve": info["curve"], "path": resolved_path, "account": account, "change": change, "index": index, "scriptType": script, "address": address, "publicKeyHex": public_key.hex()}
def
derive_addresses( source: Union[Mapping[str, Any], bytes], *, start: int = 0, count: int = 20, **kwargs: Any) -> list[dict[str, typing.Any]]:
463def derive_addresses(source: Mapping[str, Any] | bytes, *, start: int = 0, count: int = 20, **kwargs: Any) -> list[dict[str, Any]]: 464 start = _index(start, "start") 465 if isinstance(count, bool) or not isinstance(count, int) or not 1 <= count <= 10_000 or start + count > HARDENED: 466 raise HDWalletError("count must be between 1 and 10000 and stay within the index range") 467 return [derive_address(source, index=start + offset, **kwargs) for offset in range(count)]
def
derive_address_from_extended_public_key( extended_public_key: str, *, chain: str, change: int = 0, index: int = 0, script_type: str | None = None) -> dict[str, typing.Any]:
534def derive_address_from_extended_public_key(extended_public_key: str, *, chain: str, change: int = 0, index: int = 0, script_type: str | None = None) -> dict[str, Any]: 535 info = _chain(chain) 536 if info["curve"] != "secp256k1": 537 raise HDWalletError("extended public derivation is available only for secp256k1 chains") 538 parsed = parse_extended_key(extended_public_key) 539 if parsed.is_private: 540 raise HDWalletError("use an extended public key, not an extended private key") 541 change, index = _index(change, "change"), _index(index, "index") 542 node = parsed._node.derive(change).derive(index) 543 inferred = {"ypub": "p2sh-p2wpkh", "upub": "p2sh-p2wpkh", "Mtub": "p2sh-p2wpkh", "zpub": "p2wpkh", "vpub": "p2wpkh"}.get(parsed.format) 544 script = script_type or inferred or info.get("defaultScriptType") or "p2pkh" 545 return {"schemaVersion": 1, "chain": chain, "curve": "secp256k1", "path": f"{change}/{index}", "account": 0, "change": change, "index": index, "scriptType": script, "address": _address(node.public_key, chain, script), "publicKeyHex": node.public_key.hex()}
489def parse_extended_key(value: str) -> ParsedExtendedKey: 490 try: 491 payload = _base58check_decode(value) 492 except (HDWalletError, TypeError, ValueError) as exc: 493 raise HDWalletError("invalid extended key checksum or encoding") from exc 494 if len(payload) != 78: 495 raise HDWalletError("extended key payload must be 78 bytes") 496 version = payload[:4].hex() 497 found = next(((name, fmt, private) for name, fmt in FORMATS.items() for private in (False, True) if version == fmt["private" if private else "public"]), None) 498 if found is None: 499 raise HDWalletError(f"unknown extended-key version: {version}") 500 name, _, is_private = found 501 depth = payload[4] 502 parent_fingerprint = payload[5:9] 503 child_number = int.from_bytes(payload[9:13], "big") 504 chain_code = payload[13:45] 505 key_data = payload[45:] 506 if depth == 0 and (parent_fingerprint != b"\0\0\0\0" or child_number != 0): 507 raise HDWalletError("root extended key must have zero parent fingerprint and child number") 508 try: 509 if is_private: 510 if key_data[0] != 0 or not 0 < int.from_bytes(key_data[1:], "big") < SECP256K1_ORDER: 511 raise HDWalletError("invalid extended private key material") 512 private_key = key_data[1:] 513 public_key = PrivateKey(private_key).public_key.format(compressed=True) 514 else: 515 if key_data[0] not in (2, 3): 516 raise HDWalletError("invalid compressed extended public key") 517 public_key = PublicKey(key_data).format(compressed=True) 518 private_key = None 519 except (IndexError, ValueError) as exc: 520 raise HDWalletError("invalid extended key material") from exc 521 node = _SecpNode(private_key, public_key, chain_code, depth, parent_fingerprint, child_number) 522 return ParsedExtendedKey(value, version, name, is_private, depth, child_number, parent_fingerprint.hex(), chain_code.hex(), public_key.hex(), node)
def
serialize_extended_key( parsed: ParsedExtendedKey, *, private: bool = False, format: str | None = None) -> str:
525def serialize_extended_key(parsed: ParsedExtendedKey, *, private: bool = False, format: str | None = None) -> str: 526 """Serialize a parsed key; private export requires ``private=True``.""" 527 fmt_name = format or parsed.format 528 if fmt_name not in FORMATS: 529 raise HDWalletError(f"unsupported extended-key format: {fmt_name}") 530 version = bytes.fromhex(FORMATS[fmt_name]["private" if private else "public"]) 531 return parsed._node.serialize(version, private=private)
Serialize a parsed key; private export requires private=True.
def
supported_chains() -> list[dict[str, typing.Any]]: