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):
31class HDWalletError(ValueError):
32    """Raised for invalid or unsupported derivation requests."""

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.

ParsedExtendedKey( value: str, version_hex: str = '', format: str = '', is_private: bool = False, depth: int = 0, child_number: int = 0, parent_fingerprint_hex: str = '', chain_code_hex: str = '', public_key_hex: str = '', _node: wallet_hd_derivation_kit.core._SecpNode = <factory>)
value: str
version_hex: str = ''
format: str = ''
is_private: bool = False
depth: int = 0
child_number: int = 0
parent_fingerprint_hex: str = ''
chain_code_hex: str = ''
public_key_hex: str = ''
def to_dict(self) -> dict[str, typing.Any]:
485    def to_dict(self) -> dict[str, Any]:
486        return {key: value for key, value in self.__dict__.items() if key != "_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()}
def parse_extended_key(value: str) -> ParsedExtendedKey:
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]]:
548def supported_chains() -> list[dict[str, Any]]:
549    return [{"id": chain_id, **info} for chain_id, info in CHAINS.items()]