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

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

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

Parameters:
Return type:

Transaction

build_send_tx(triples, to_address, photons, *, fee_rate=10000, 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).

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

  • to_address (str)

  • photons (int)

  • fee_rate (int)

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

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

Address→key mapping is preserved so signing works correctly per UTXO. Falls back gracefully if any per-address fetch fails (the failed address contributes nothing rather than crashing the whole collection — the caller decides whether the resulting balance is enough).

Parameters:

client (ElectrumXClient)

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

external_tip: int = 0
classmethod from_mnemonic(mnemonic, passphrase='', account=0, coin_type=None)[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.

Parameters:
  • mnemonic (str)

  • passphrase (str)

  • account (int)

  • coin_type (int | None)

Return type:

HdWallet

async get_balance(client)[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.

Parameters:

client (ElectrumXClient)

Return type:

int

async get_utxos(client)[source]

Return all UTXOs across all known addresses.

Parameters:

client (ElectrumXClient)

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)[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.

Parameters:
  • path (Path)

  • mnemonic (str)

  • passphrase (str)

  • coin_type (int | None)

Return type:

HdWallet

classmethod load_or_create(path, mnemonic, passphrase='', account=0, coin_type=None)[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.

Parameters:
  • path (Path)

  • mnemonic (str)

  • passphrase (str)

  • account (int)

  • coin_type (int | None)

Return type:

HdWallet

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

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

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

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)

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

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

Parameters:
  • 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)[source]

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

Parameters:
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)[source]

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

Parameters:
  • 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)[source]

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

Parameters:
Return type:

list[Xprv]

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

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

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)[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)

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)[source]
[DEPRECATED] Use bip44_derive_xprvs_from_mnemonic instead.

This function name is kept for backward compatibility.

Parameters:
Return type:

list[Xprv]

async pyrxd.hd.discover(client, mnemonic, *, passphrase='', coin_types=(0, 512, 236), accounts=(0, 1, 2))[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.

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.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.seed_from_mnemonic(mnemonic, lang='en', passphrase='', prefix='mnemonic')[source]
Parameters:
Return type:

bytes

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: