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=True)[source]

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

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

Runs the gap-limit scan (refresh()) first, every call. An address is read here only once it is marked used, only the scan marks it, and the mark is not something a caller can be trusted to have made: before this, every CLI spend command but wallet send / wallet sweep collected from a wallet nobody had scanned, so a freshly created, funded wallet reported nothing to spend (#759). Scanning on every call, not once per wallet, is deliberate too: a command that spends twice (a mint’s commit, then its reveal) needs the change address the first spend just used. A scan that cannot read an address raises NetworkError rather than reading it as unused (see refresh()).

strict (the default) raises NetworkError when any address’s UTXO read fails, instead of returning what the other addresses answered. Almost every caller turns a short result into a claim about the whole wallet — “fund this wallet”, “no spendable UTXOs”, “not held by this wallet” — and after a failed read that claim is false: pyrxd mark printed “fund this wallet” when get_utxos failed for the one funded address. strict=False returns the partial view, LOGS the failed reads and names them in Spendable.unread; it is for a caller that only needs enough (send(), pyrxd wallet send, pyrxd swap cancel), which must go through raise_if_reads_failed() before it reports that what it read is not enough. The scan above fails closed either way.

Parameters:
Return type:

Spendable

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 an address this wallet derives.

The path is looked up in self.addresses first. An address not recorded there is searched for locally, with no network: both chains, from index 0 to _GAP_LIMIT past the highest index the wallet knows on that chain. A wallet file records only what something saved, and pyrxd wallet new saves before it derives a single address, so without the search a new wallet could not find the key for its own first receive address. On regtest that stranded a new wallet’s Glyph commit: glyph resume-mint refused to reveal it.

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 derives within that window — the caller has the wrong wallet (or an address past the gap limit), 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.

An address already recorded as used is never marked unused by a scan, even when this server reports no history for it: a server that lags or indexes partially does not get to hide funds the wallet already knew about.

An address counts as used when its P2PKH script hash has history, or, when that is empty, when any of its token script hashes does (_has_token_history()). A Radiant ElectrumX lists a token output under the token script’s hash with its refs zeroed, not under the owner’s P2PKH hash, so an address that has only received a Glyph token has no P2PKH history at all (#787).

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.

Collects with strict=False: an amount send needs enough, not all, so an address whose UTXO read failed is logged and skipped rather than refusing a send the other addresses can fund. The gap-limit scan before it still fails closed. When what was read is short AND a read failed, it raises NetworkError rather than calling that insufficient funds (raise_if_reads_failed(), inside build_send_tx()).

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