pyrxd.hd — BIP-32/39/44 HD wallets¶
- class pyrxd.hd.AddressRecord[source]¶
Bases:
objectAddressRecord(address: ‘str’, change: ‘int’, index: ‘int’, used: ‘bool’)
- __init__(address, change, index, used)¶
- class pyrxd.hd.DiscoveryHit[source]¶
Bases:
objectOne derived address that has on-chain history.
- __init__(coin_type, account, change, index, address, confirmed, unconfirmed)¶
- class pyrxd.hd.DiscoveryReport[source]¶
Bases:
objectResult of a multi-path scan.
- __init__(hits, scanned, total_confirmed, total_unconfirmed)¶
- hits: list[DiscoveryHit]¶
- class pyrxd.hd.HdWallet[source]¶
Bases:
objectBIP44 HD wallet for Radiant with gap-limit discovery and encrypted persistence.
- coin_type¶
BIP44 coin type (read-only property; back-store
_coin_typeis 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_xprvand silently route subsequent addresses to a different path (closes SEV-2 red-team finding).
- addresses¶
{path_key: AddressRecord}where path_key isf"{change}/{index}".- Type:
- __init__(_seed, account=0, _coin_type=<factory>, external_tip=0, internal_tip=0, addresses=<factory>)¶
- Parameters:
_seed (SecretBytes)
account (int)
_coin_type (int)
external_tip (int)
internal_tip (int)
addresses (dict[str, AddressRecord])
- Return type:
None
- build_send_max_tx(triples, to_address, *, fee_rate=10000)[source]¶
Sweep all triples to to_address minus fee. No change output.
- 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_addressdefaults to the next unused internal index; callers can override (e.g. to keep change on the external chain for a single-address-style wallet).
- 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 blockswallet._coin_type = X; the property blockswallet.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:
- 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 varRXD_PY_SDK_BIP44_DERIVATION_PATH, or SLIP-0044’s 512 if unset).512is SLIP-0044 Radiant (also Tangem).0matches Photonic and Electron-Radiant — pass this when restoring a mnemonic from those wallets.236matches pre-#14 pyrxd wallets.
The chosen coin type is recorded on the wallet and persisted in the wallet file; subsequent
load()calls validate it.
- async get_balance(client)[source]¶
Return total confirmed + unconfirmed satoshis across all known addresses.
Uses
ElectrumXClient.get_balanceper address. Callrefresh()first to ensure the address set is current.- Parameters:
client (ElectrumXClient)
- Return type:
- async get_utxos(client)[source]¶
Return all UTXOs across all known addresses.
- Parameters:
client (ElectrumXClient)
- Return type:
list[UtxoRecord]
- known_addresses(*, change=None)[source]¶
Return all known address records, optionally filtered by chain.
- Parameters:
change (int | None)
- Return type:
- 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
FileNotFoundErrorif 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 useload_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. PassNone(default) to accept whatever was persisted.
- 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.
- next_receive_address()[source]¶
Return the first external (change=0) address with no recorded history.
- Return type:
- privkey_for(change, index)[source]¶
Derive the signing key at
change/index(public seam over_privkey_for).
- 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:
- 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
ValidationErroron bad inputs or insufficient funds,NetworkErroron RPC failure.
- async send_max(client, to_address, *, fee_rate=10000)[source]¶
Sweep all UTXOs to to_address minus fee. Returns broadcast txid.
- Parameters:
client (ElectrumXClient)
to_address (str)
fee_rate (int)
- Return type:
- 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
_xprvproperty re-derives it transiently from the seed per operation — so the ONLY resident long-lived secret is this 64-byte seed, which lives in aSecretBytesand IS memset here. Setting_zeroed(matchingSecretBytes._zeroed) makes the_xprvproperty 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:
objectBIP39 word list
- 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'}¶
- path = '/home/runner/work/pyrxd/pyrxd/src/pyrxd/hd/wordlist'¶
- 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)
- class pyrxd.hd.Xprv[source]¶
Bases:
Xkey- classmethod from_seed(seed, network=Network.MAINNET)[source]¶
derive master extended private key from seed
- 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.
- 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.
- 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
- 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.
- 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
- 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)
- pyrxd.hd.coin_type_label(coin_type)[source]¶
Return a human label for coin_type, or a generic note if unknown.
- 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.
- 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.
- 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.
- async pyrxd.hd.discover(client, mnemonic, *, passphrase='', coin_types=(0, 512, 236), accounts=(0, 1, 2))[source]¶
Scan
coin_types x accountsfor 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) viaHdWallet.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.foundisFalsewhen no scanned path had any history (the caller should then suggest widening the ranges or supplying the funded address directly).- Parameters:
- Return type: