pyrxd.hd — BIP-32/39/44 HD wallets

class pyrxd.hd.AccountDescriptors[source]

Bases: object

The pair of ranged descriptors that fully describe one BIP44 account.

Both chains are always present. A watch-only import that takes only the receive descriptor silently under-reports the balance, because change outputs land on the internal chain and would never be scanned.

__init__(receive, change, master_fingerprint, account_path)
Parameters:
  • receive (str)

  • change (str)

  • master_fingerprint (str)

  • account_path (str)

Return type:

None

as_dict()[source]

Flat dict for CLI/JSON emission.

Return type:

dict[str, str]

receive: str

Descriptor for the external chain (/0/*) — addresses you hand out.

change: str

Descriptor for the internal chain (/1/*) — where change lands.

master_fingerprint: str

Hex master-key fingerprint embedded in both descriptors.

account_path: str

The BIP44 account path, in descriptor notation (e.g. 44h/512h/0h).

class pyrxd.hd.AddressRecord[source]

Bases: object

AddressRecord(address: ‘str’, change: ‘int’, index: ‘int’, used: ‘bool’)

__init__(address, change, index, used)
Parameters:
Return type:

None

address: str
change: int
index: int
used: bool
class pyrxd.hd.DiscoveryHit[source]

Bases: object

One derived address that has on-chain history.

__init__(coin_type, account, change, index, address, confirmed, unconfirmed)
Parameters:
Return type:

None

property path: str
property total: int
coin_type: int
account: int
change: int
index: int
address: str
confirmed: int
unconfirmed: int
class pyrxd.hd.DiscoveryReport[source]

Bases: object

Result of a multi-path scan.

__init__(hits, scanned, total_confirmed, total_unconfirmed)
Parameters:
Return type:

None

property found: bool
property total: int
hits: list[DiscoveryHit]
scanned: list[tuple[int, int]]
total_confirmed: int
total_unconfirmed: int
class pyrxd.hd.HdWallet[source]

Bases: object

BIP44 HD wallet for Radiant with gap-limit discovery and encrypted persistence.

account

BIP44 account index (usually 0).

Type:

int

coin_type

BIP44 coin type (read-only property; back-store _coin_type is set at construction and never mutated). 512 is SLIP-0044 spec for Radiant (default, also Tangem); 0 matches Photonic and Electron-Radiant; 236 matches pre-#14 pyrxd. Persisted in the wallet file and validated on load. Read-only because mutating it post-construction would desync from the already-derived _xprv and silently route subsequent addresses to a different path (closes SEV-2 red-team finding).

external_tip

Highest derived index on external chain (change=0).

Type:

int

internal_tip

Highest derived index on internal chain (change=1).

Type:

int

addresses

{path_key: AddressRecord} where path_key is f"{change}/{index}".

Type:

dict[str, pyrxd.hd.wallet.AddressRecord]

__init__(_seed, account=0, _coin_type=<factory>, external_tip=0, internal_tip=0, addresses=<factory>)
Parameters:
Return type:

None

account: int = 0
property account_path: str

This wallet’s BIP44 account path, e.g. m/44'/512'/0'.

Single source of truth for the string several callers used to build inline. The _xprv property derives from exactly this path.

account_xpub()[source]

The account-level xpub (watch-only safe; no private key).

Return type:

Xpub

build_send_max_tx(triples, to_address, *, fee_rate=10000, allow_below_relay_floor=False, allow_overpay=False)[source]

Sweep all triples to to_address minus fee. No change output.

fee_rate is refused below Radiant’s effective relay floor unless allow_below_relay_floor is set, and above the overpay ceiling unless allow_overpay is — see build_send_tx(). A sweep has NO change output, so an unintended overpay leaves entirely with the miner, which is why the ceiling exists; it is also why the override has to be reachable.

Parameters:
  • triples (list[tuple[UtxoRecord, str, PrivateKey]])

  • to_address (str)

  • fee_rate (int)

  • allow_below_relay_floor (bool)

  • allow_overpay (bool)

Return type:

Transaction

build_send_tx(triples, to_address, photons, *, fee_rate=10000, allow_below_relay_floor=False, allow_overpay=False, change_address=None)[source]

Build and sign a P2PKH transfer from HD UTXOs to to_address.

Pure offline operation. Mirrors RxdWallet.build_send_tx() but accepts (utxo, address, privkey) triples so each input is signed by the correct HD-derived key.

change_address defaults to the next unused internal index; callers can override (e.g. to keep change on the external chain for a single-address-style wallet).

fee_rate is refused below Radiant’s effective relay floor unless allow_below_relay_floor is set — the deliberate opt-out for regtest and chains you control. Unlike RxdWallet, the rate arrives per CALL here, so this is where it has to be judged.

allow_overpay is the mirror opt-out for a rate above the overpay ceiling. A ceiling with no reachable override is its own fund-safety bug: a caller who genuinely means a high rate would be refused outright, and Radiant has neither RBF nor CPFP, so a refusal during a timelock race costs the funds the ceiling was protecting.

Parameters:
  • triples (list[tuple[UtxoRecord, str, PrivateKey]])

  • to_address (str)

  • photons (int)

  • fee_rate (int)

  • allow_below_relay_floor (bool)

  • allow_overpay (bool)

  • change_address (str | None)

Return type:

Transaction

property coin_type: int

BIP44 coin type this wallet was constructed with. Read-only.

Read-only because mutating it post-construction would desync from the already-derived _xprv; subsequent address derivations would still happen at the original path while the persisted JSON would advertise the new path. The __setattr__ override blocks wallet._coin_type = X; the property blocks wallet.coin_type = X.

async collect_spendable(client, *, strict=False)[source]

Return (utxo, address, privkey) triples for every UTXO across known addresses.

Address→key mapping is preserved so signing works correctly per UTXO.

A per-address fetch that fails contributes nothing rather than crashing the whole collection — the caller decides whether the resulting balance is enough — but it is now LOGGED rather than dropped in silence, and strict=True refuses the partial result outright. Use strict when the answer is a claim about all the funds; send_max() does.

Parameters:
Return type:

list[tuple[UtxoRecord, str, PrivateKey]]

derive_address(change, index)[source]

Derive the P2PKH address at change/index (public seam).

Parameters:
Return type:

str

descriptors(*, checksum=False)[source]

Output-script descriptors for this account’s receive + change chains.

Watch-only safe: the descriptors embed the account xpub, never the xprv or the seed. Note that an xpub still discloses every address this wallet will ever derive on both chains — a larger privacy surface than handing out a single address.

checksum appends the BIP380 suffix. Off by default because Radiant Core rejects the checksummed form; see pyrxd.hd.descriptor.

Parameters:

checksum (bool)

Return type:

AccountDescriptors

external_tip: int = 0
classmethod from_mnemonic(mnemonic, passphrase='', account=0, coin_type=None, *, normalize=True)[source]

Create a fresh wallet from a BIP39 mnemonic.

coin_type selects the BIP44 derivation path:
  • None (default) uses the module-level configured coin type (env var RXD_PY_SDK_BIP44_DERIVATION_PATH, or SLIP-0044’s 512 if unset).

  • 512 is SLIP-0044 Radiant (also Tangem).

  • 0 matches Photonic and Electron-Radiant — pass this when restoring a mnemonic from those wallets.

  • 236 matches pre-#14 pyrxd wallets.

The chosen coin type is recorded on the wallet and persisted in the wallet file; subsequent load() calls validate it.

normalize controls BIP39 NFKD normalization — see seed_from_mnemonic(). Leave it True unless you are recovering funds from a wallet created before 0.12.0 using a non-ASCII passphrase, which pyrxd then hashed unnormalized. Wrong for every other case: it derives a wallet no other BIP39 implementation can reproduce.

Parameters:
  • mnemonic (str)

  • passphrase (str)

  • account (int)

  • coin_type (int | None)

  • normalize (bool)

Return type:

HdWallet

async get_balance(client, *, strict=False)[source]

Return total confirmed + unconfirmed satoshis across all known addresses.

Uses ElectrumXClient.get_balance per address. Call refresh() first to ensure the address set is current.

A per-address read that fails is logged and contributes zero — so the total is a LOWER BOUND, not a balance. Pass strict=True when the number is being shown to somebody or compared against a threshold.

Parameters:
Return type:

int

async get_utxos(client, *, strict=False)[source]

Return all UTXOs across all known addresses.

A per-address read that fails is logged and contributes nothing; pass strict=True to refuse a partial answer instead (see _read_per_address()).

Parameters:
Return type:

list[UtxoRecord]

internal_tip: int = 0
known_addresses(*, change=None)[source]

Return all known address records, optionally filtered by chain.

Parameters:

change (int | None)

Return type:

list[AddressRecord]

classmethod load(path, mnemonic, passphrase='', coin_type=None, *, normalize=True)[source]

Load a previously saved wallet from path.

The mnemonic is needed to derive the decryption key. Raises FileNotFoundError if path does not exist — a typo’d path will not silently produce an empty wallet that subsequently overwrites a real wallet on save. Callers that explicitly want the create-on-missing behavior should use load_or_create().

coin_type (optional) is validated against the value persisted in the wallet file. A mismatch raises ValidationError — this catches the silent-empty-wallet failure mode where a default change between pyrxd versions would otherwise have the loaded wallet derive at a different path than it was saved at. Pass None (default) to accept whatever was persisted.

normalize controls BIP39 NFKD normalization of the mnemonic and passphrase — see seed_from_mnemonic(). It matters here because the derived seed is also the wallet file’s AES-GCM decryption key: a wallet saved by pyrxd before 0.12.0 with a non-ASCII passphrase was encrypted under the old, unnormalized seed, and can only be decrypted by reproducing that seed with normalize=False. Leave the default True for every other case. Loading never guesses the mode: a GCM failure raises rather than silently retrying with the other seed, so the legacy mode is only ever entered by explicit opt-in.

Parameters:
  • path (Path)

  • mnemonic (str)

  • passphrase (str)

  • coin_type (int | None)

  • normalize (bool)

Return type:

HdWallet

classmethod load_or_create(path, mnemonic, passphrase='', account=0, coin_type=None, *, normalize=True)[source]

Load a wallet from path, or build a fresh one if the file is missing.

Spelled separately from load() so the create-on-missing intent is explicit at the call site. A common safety failure with the old single-load API was that a typo in path would produce an empty wallet that subsequently overwrote the real wallet on save.

coin_type applies to both branches: when loading, it is validated against the persisted value; when creating, it is the coin type the new wallet uses.

normalize also applies to the load branch — see load() for why it matters there (the seed doubles as the wallet file’s decryption key). False is a fund-recovery escape for pre-0.12.0 wallets with non-ASCII passphrases.

Raises:

ValidationError – if path does not exist and normalize=False. The legacy seed mode exists solely to reach funds already held under a pre-0.12.0 wallet; there is nothing to recover at a path that has no wallet on it. Creating one there instead would mint a brand-new, permanently non-conformant wallet whose mnemonic no other BIP39 implementation can restore — and the likeliest way to land in that branch is a typo in path, which is exactly the failure load_or_create was split out to make visible.

Parameters:
Return type:

HdWallet

master_fingerprint()[source]

The BIP32 master key fingerprint: hash160(master pubkey)[:4].

This is what an output-script descriptor’s key-origin field wants, and it is NOT account_xpub().fingerprint — that attribute is the parent fingerprint (payload bytes 5:9), i.e. the fingerprint of m/44'/<coin>', one level up. The two values differ for every account at depth > 1.

The distinction matters because using the parent fingerprint produces a descriptor that still derives the correct addresses, so nothing appears broken — but it misidentifies the key’s origin, and any consumer that later tries to match the descriptor to a signing device (or to another descriptor from the same seed) will fail to.

Public (no private material leaves): the return value is a truncated hash of a public key.

Return type:

bytes

next_receive_address()[source]

Return the first external (change=0) address with no recorded history.

Return type:

str

privkey_for(change, index)[source]

Derive the signing key at change/index (public seam over _privkey_for).

Parameters:
Return type:

PrivateKey

privkey_for_address(address)[source]

Derive the signing key for a known address.

The derivation path is looked up in self.addresses rather than searched for, so this is one ckd chain, not a scan.

Added for pyrxd.glyph.mint.GlyphMinter, which must re-derive the key that spends a Glyph commit output after a crash. It deliberately does not persist the key, only the funding address, so it needs address → key. Keeping that lookup here also keeps the minter’s wallet contract down to two methods (collect_spendable() and this one), which is what makes it practical to drive the minter with a non-HD wallet in a test or a dev script.

Raises:

ValidationError – if the address is not one this wallet derived — the caller has the wrong wallet, and signing with a key that hashes to a different PKH would produce a transaction the network rejects.

Parameters:

address (str)

Return type:

PrivateKey

async refresh(client)[source]

Run BIP44 gap-limit scan on both external and internal chains.

Discovers which derived addresses have on-chain history. Stops after _GAP_LIMIT (20) consecutive unused addresses per chain.

Network errors (a transient ElectrumX outage, a server hangup mid-scan) propagate to the caller as NetworkError — previously they were silently treated as “address unused”, which made a funded wallet look empty after a flaky lookup.

Returns the count of newly discovered used addresses.

Parameters:

client (ElectrumXClient)

Return type:

int

save(path)[source]

Encrypt and atomically save wallet state to path.

Atomicity & permissions

Writes via mkstemp + fchmod(0o600) + fsync + os.replace, so:
  • The file is never visible at a wider mode than 0o600 — the mode is set on the fd before any bytes are written.

  • A crash mid-write cannot leave a half-encrypted blob in place — either the old file remains, or the new fully-fsynced file does.

Encryption

AES-256-GCM under a key derived from the BIP39 seed via scrypt with a per-file random salt. Tampering with the ciphertext breaks the GCM tag — load() raises rather than returning attacker-shaped JSON.

Parameters:

path (Path)

Return type:

None

async send(client, to_address, photons, *, fee_rate=10000, allow_below_relay_floor=False, allow_overpay=False, change_address=None)[source]

Fetch UTXOs, build, sign, broadcast. Returns broadcast txid.

Raises ValidationError on bad inputs or insufficient funds, NetworkError on RPC failure.

Parameters:
Return type:

str

async send_max(client, to_address, *, fee_rate=10000, allow_below_relay_floor=False, allow_overpay=False)[source]

Sweep all UTXOs to to_address minus fee. Returns broadcast txid.

Collection is strict: “sweep everything” is a completeness claim, and a sweep built from a view that silently lost an address moves most of the funds while reporting that it moved all of them. Raises NetworkError if any per-address read failed — nothing is broadcast, and a retry (or a different endpoint) sweeps the whole set. Use send() for an amount, which does not make that claim.

Parameters:
Return type:

str

zeroize()[source]

Scrub the seed and mark the wallet dead; it cannot derive or sign after.

Hardening #8/H1: the account xprv is NO LONGER stored long-lived — the _xprv property re-derives it transiently from the seed per operation — so the ONLY resident long-lived secret is this 64-byte seed, which lives in a SecretBytes and IS memset here. Setting _zeroed (matching SecretBytes._zeroed) makes the _xprv property fail closed (rather than silently re-deriving a garbage key from the now-zeroed seed). Any account-xprv copies that existed only during an in-flight derivation are short-lived locals (GC-eligible immediately, never held across the unlock window); their residency until the pages are reused is bounded by the agent’s best-effort process hygiene (mlock / PR_SET_DUMPABLE 0 / no core dumps), NOT a guaranteed erase — do not over-state it as “erased”.

Return type:

None

addresses: dict[str, AddressRecord]
class pyrxd.hd.WordList[source]

Bases: object

BIP39 word list

LIST_WORDS_COUNT: int = 2048
files: dict[str, str] = {'en': '/home/runner/work/pyrxd/pyrxd/src/pyrxd/hd/wordlist/english.txt', 'zh-cn': '/home/runner/work/pyrxd/pyrxd/src/pyrxd/hd/wordlist/chinese_simplified.txt'}
classmethod get_word(index, lang='en')[source]
Parameters:
Return type:

str

classmethod index_word(word, lang='en')[source]
Parameters:
Return type:

int

classmethod load()[source]
Return type:

None

classmethod load_wordlist(lang='en')[source]
Parameters:

lang (str)

Return type:

list[str]

path = '/home/runner/work/pyrxd/pyrxd/src/pyrxd/hd/wordlist'
wordlist: dict[str, list[str]] = {}
class pyrxd.hd.Xkey[source]

Bases: object

[ : 4] prefix [ 4: 5] depth [ 5: 9] parent public key fingerprint [ 9:13] child index [13:45] chain code [45:78] key (private/public)

__init__(xkey)[source]
Parameters:

xkey (str | bytes)

serialize()[source]

The base58check extended-key string. Explicit, so it can never happen by accident.

Return type:

str

class pyrxd.hd.Xprv[source]

Bases: Xkey

__init__(xprv)[source]
Parameters:

xprv (str | bytes)

address()[source]
Return type:

str

ckd(index)[source]
Parameters:

index (int | str | bytes)

Return type:

Xprv

classmethod from_seed(seed, network=Network.MAINNET)[source]

derive master extended private key from seed

Parameters:
private_key()[source]
Return type:

PrivateKey

public_key()[source]
Return type:

PublicKey

serialize()[source]

Return the base58check-encoded xprv string. Named explicitly to make audit grep easy.

Return type:

str

xpub()[source]
Return type:

Xpub

class pyrxd.hd.Xpub[source]

Bases: Xkey

__init__(xpub)[source]
Parameters:

xpub (str | bytes)

address()[source]
Return type:

str

ckd(index)[source]
Parameters:

index (int | str | bytes)

Return type:

Xpub

classmethod from_xprv(xprv)[source]
Parameters:

xprv (str | bytes | Xprv)

Return type:

Xpub

public_key()[source]
Return type:

PublicKey

pyrxd.hd.account_descriptors(xpub, *, master_fingerprint, account_path, checksum=False)[source]

Build the receive + change descriptor pair for a BIP44 account.

Parameters:
Return type:

AccountDescriptors

pyrxd.hd.append_checksum(descriptor)[source]

Return descriptor with its BIP380 #checksum suffix appended.

Radiant Core rejects the result — see the module docstring. Use this only for Bitcoin-Core-lineage consumers.

Parameters:

descriptor (str)

Return type:

str

pyrxd.hd.bip32_derive_xkeys_from_xkey(xkey, index_start, index_end, path='m/', change=0)[source]

Derive a range of extended keys from Xprv and Xpub keys using BIP32 path structure.

Parameters:
  • xkey (Xprv | Xpub) – Parent extended key (Xprv or Xpub)

  • index_start (str | int) – Starting index for derivation

  • index_end (str | int) – Ending index for derivation (exclusive)

  • path (str) – Base derivation path (default: BIP32_DERIVATION_PATH)

  • change (str | int) – Change level (0 for receiving addresses, 1 for change addresses)

Returns:

List of derived extended keys

Return type:

List[Union[Xprv, Xpub]]

pyrxd.hd.bip32_derive_xprv_from_mnemonic(mnemonic, lang='en', passphrase='', prefix='mnemonic', path='m/', network=Network.MAINNET, *, normalize=True)[source]

Derive the subtree root extended private key from mnemonic and path.

Parameters:
  • normalize (bool) – See seed_from_mnemonic(). Leave True unless recovering a wallet created before 0.12.0 with a non-ASCII passphrase.

  • mnemonic (str)

  • lang (str)

  • passphrase (str)

  • prefix (str)

  • path (str)

  • network (Network)

Return type:

Xprv

pyrxd.hd.bip32_derive_xprvs_from_mnemonic(mnemonic, index_start, index_end, lang='en', passphrase='', prefix='mnemonic', path='m/', change=0, network=Network.MAINNET, *, normalize=True)[source]

Derive a range of extended keys from a nmemonic using BIP32 format

Parameters:
  • normalize (bool) – See seed_from_mnemonic().

  • mnemonic (str)

  • index_start (str | int)

  • index_end (str | int)

  • lang (str)

  • passphrase (str)

  • prefix (str)

  • path (str)

  • change (str | int)

  • network (Network)

Return type:

list[Xprv]

pyrxd.hd.bip44_derive_xprv_from_mnemonic(mnemonic, lang='en', passphrase='', prefix='mnemonic', path="m/44'/512'/0'", network=Network.MAINNET, *, normalize=True)[source]

Derives extended private key using BIP44 format- it is a subset of BIP32. Inherits from BIP32, only changing the default path value.

Parameters:
  • normalize (bool) – See seed_from_mnemonic(). Leave True unless recovering a wallet created before 0.12.0 with a non-ASCII passphrase.

  • mnemonic (str)

  • lang (str)

  • passphrase (str)

  • prefix (str)

  • path (str)

  • network (Network)

Return type:

Xprv

pyrxd.hd.bip44_derive_xprvs_from_mnemonic(mnemonic, index_start, index_end, lang='en', passphrase='', prefix='mnemonic', path="m/44'/512'/0'", change=0, network=Network.MAINNET, *, normalize=True)[source]

Derive a range of extended keys from a nmemonic using BIP44 format

Parameters:
  • normalize (bool) – See seed_from_mnemonic().

  • mnemonic (str)

  • index_start (str | int)

  • index_end (str | int)

  • lang (str)

  • passphrase (str)

  • prefix (str)

  • path (str)

  • change (str | int)

  • network (Network)

Return type:

list[Xprv]

pyrxd.hd.ckd(xkey, path)[source]

ckd = “Child Key Derivation” derive an extended key according to path like “m/44’/512’/1’/0/10” (absolute) or “./0/10” (relative)

512 is Radiant’s SLIP-0044 coin type and is what pyrxd.constants.BIP44_DERIVATION_PATH uses. The examples here used to show coin type 0, which is BITCOIN’s — following them derives a wallet whose addresses are not the ones Photonic >= v3.0.0 or Tangem will show for the same mnemonic. Coin types 0 (Photonic <= v2.x, Electron-Radiant, Chainbow) and 236 (pre-#14 pyrxd) are also in use in the Radiant ecosystem for historical reasons — see pyrxd.hd.discovery, which scans all three — but 512 is the one to derive NEW wallets at.

Parameters:
Return type:

Xprv | Xpub

pyrxd.hd.coin_type_label(coin_type)[source]

Return a human label for coin_type, or a generic note if unknown.

Parameters:

coin_type (int)

Return type:

str

pyrxd.hd.derive_xkeys_from_xkey(xkey, index_start, index_end, change=0)[source]
[DEPRECATED] Use bip32_derive_xkeys_from_xkey instead.

This function name is kept for backward compatibility.

Parameters:
Return type:

list[Xprv | Xpub]

pyrxd.hd.derive_xprv_from_mnemonic(mnemonic, lang='en', passphrase='', prefix='mnemonic', path="m/44'/512'/0'", network=Network.MAINNET, *, normalize=True)[source]
[DEPRECATED] Use bip44_derive_xprv_from_mnemonic instead.

This function name is kept for backward compatibility.

Parameters:
  • mnemonic (str)

  • lang (str)

  • passphrase (str)

  • prefix (str)

  • path (str)

  • network (Network)

  • normalize (bool)

Return type:

Xprv

pyrxd.hd.derive_xprvs_from_mnemonic(mnemonic, index_start, index_end, lang='en', passphrase='', prefix='mnemonic', path="m/44'/512'/0'", change=0, network=Network.MAINNET, *, normalize=True)[source]
[DEPRECATED] Use bip44_derive_xprvs_from_mnemonic instead.

This function name is kept for backward compatibility.

Parameters:
Return type:

list[Xprv]

pyrxd.hd.descriptor_checksum(descriptor)[source]

Return the 8-character BIP380 checksum for descriptor (no #).

Raises:

ValidationError – if descriptor already carries a checksum, or contains a character outside INPUT_CHARSET.

Parameters:

descriptor (str)

Return type:

str

async pyrxd.hd.discover(client, mnemonic, *, passphrase='', coin_types=(0, 512, 236), accounts=(0, 1, 2), normalize=True)[source]

Scan coin_types x accounts for derived addresses with on-chain history.

For each (coin_type, account) pair, builds the account wallet, runs the standard BIP44 gap-limit scan (gap 20, both chains) via HdWallet.refresh(), and records every used address together with its confirmed/unconfirmed balance and full derivation path.

normalize controls BIP39 NFKD normalization of the mnemonic and passphrase — see seed_from_mnemonic(). A scan with the default True derives the spec-conformant seed and therefore cannot see funds sitting on a seed pyrxd derived before 0.12.0 from a non-ASCII passphrase; pass normalize=False to scan that legacy seed’s paths instead. Recovery-only — never for new wallets.

mnemonic is used only to derive keys locally; it is never sent to the server. Only derived addresses (as scripthashes) reach the network.

Raises whatever HdWallet.refresh() / ElectrumXClient.get_balance() raise on network failure — a partial scan is not silently reported as empty. The caller decides how to surface an aborted scan.

Returns a DiscoveryReport; report.found is False when no scanned path had any history (the caller should then suggest widening the ranges or supplying the funded address directly).

Parameters:
Return type:

DiscoveryReport

pyrxd.hd.key_origin(master_fingerprint, path)[source]

Build the descriptor key-origin field, e.g. [1a2b3c4d/44h/512h/0h].

master_fingerprint must be the master key’s fingerprint — hash160(master_pubkey)[:4] for the key at m — NOT the parent fingerprint stored inside the account xpub. Those differ for any account at depth > 1, and getting it wrong yields a descriptor that misidentifies its own origin: it still derives the right addresses, so nothing looks broken, but any consumer trying to match it to a signing device will fail to. pyrxd.hd.wallet.HdWallet.master_fingerprint() returns the right value.

Raises:

ValidationError – if master_fingerprint is not exactly 4 bytes.

Parameters:
Return type:

str

pyrxd.hd.master_xprv_from_seed(seed, network=Network.MAINNET)[source]
Parameters:
Return type:

Xprv

pyrxd.hd.mnemonic_from_entropy(entropy=None, lang='en')[source]
Parameters:
Return type:

str

pyrxd.hd.pkh_descriptor(xpub, *, master_fingerprint, account_path, chain, checksum=False)[source]

Build a ranged pkh() descriptor for one BIP44 chain.

pkh([1a2b3c4d/44h/512h/0h]xpub6.../0/*)

xpub is the account-level extended public key at account_path. chain is EXTERNAL_CHAIN (0, receive) or INTERNAL_CHAIN (1, change). The trailing /* makes it a ranged descriptor, so the consumer walks indices itself.

checksum appends the BIP380 suffix. Defaults to False because Radiant Core rejects the checksummed form — see the module docstring.

Parameters:
Return type:

str

pyrxd.hd.seed_from_mnemonic(mnemonic, lang='en', passphrase='', prefix='mnemonic', *, normalize=True)[source]

Derive the 64-byte BIP39 seed from a mnemonic (+ optional passphrase).

BIP39 requires the mnemonic sentence and the passphrase to be NFKD normalized before they enter PBKDF2. Without it, two spellings that a user cannot tell apart – “café” with a precomposed U+00E9 versus the same word as “e” + combining U+0301 – derive different seeds, and therefore entirely different wallets.

Parameters:
  • normalize (bool) – Leave True (the default) for spec-conformant, cross-wallet-compatible seeds. Pass False only to reproduce the non-conformant seed pyrxd produced before 0.12.0, which is the recovery path for anyone who funded a wallet using a non-ASCII passphrase under the old behavior. It is not interoperable with any other BIP39 implementation – see docs/how-to/recover-funds-across-wallet-paths.md.

  • mnemonic (str)

  • lang (str)

  • passphrase (str)

  • prefix (str)

Return type:

bytes

Note

normalize=False is inert for a passphrase that is already in NFKD form, which includes every pure-ASCII passphrase (the overwhelmingly common case) and both wordlists pyrxd ships. For those inputs the two modes return byte-identical seeds.

pyrxd.hd.step_to_index(step)[source]

convert step (sub path) normal derivation or hardened derivation into child index

Parameters:

step (str | int)

Return type:

int

pyrxd.hd.validate_mnemonic(mnemonic, lang='en')[source]
Parameters:
pyrxd.hd.verify_checksum(descriptor)[source]

Return True if descriptor ends in a valid BIP380 #checksum.

False for an unchecksummed descriptor, a malformed suffix, or a payload that does not match its checksum. Never raises — this is a predicate for validating untrusted input.

Exactly one # is required. # is a member of INPUT_CHARSET, so a doubly checksummed string such as raw(deadbeef)#89f8spxm#4x0avkn4 polymods correctly and used to verify True here — while Bitcoin Core’s descsum_check rejects it, and descriptor_checksum() already refuses to create one. Accepting what we will not emit, and what the consumer will not take, is the wrong half of that pair to be lenient in.

Parameters:

descriptor (str)

Return type:

bool