pyrxd.network — ElectrumX + BTC sources

pyrxd.network — network layer for Radiant / Bitcoin SPV.

Re-exports the public surface of the sub-modules so callers can do:

from pyrxd.network import ElectrumXClient, ChainTracker, …

class pyrxd.network.BitcoinCoreRpcSource[source]

Bases: BtcDataSource

BtcDataSource backed by a Bitcoin Core JSON-RPC endpoint.

Credentials are stored as SecretBytes and never logged.

Parameters:
  • url – RPC endpoint URL, e.g. http://localhost:8332/.

  • user – RPC username.

  • password – RPC password (stored securely as SecretBytes).

__init__(url, user, password)[source]
Parameters:
Return type:

None

async close()[source]

Close any underlying connections held by this source.

Return type:

None

async get_block_hash(height)[source]

Return the 32-byte block hash at height.

Parameters:

height (BlockHeight)

Return type:

Hex32

async get_block_header_hex(height)[source]

Return the raw 80-byte block header at height.

Parameters:

height (BlockHeight)

Return type:

bytes

async get_header_chain(start_height, count)[source]

Return count consecutive 80-byte headers starting at start_height.

Parameters:
Return type:

list[bytes]

async get_merkle_proof(txid, height)[source]

Return (branch_hashes_hex, leaf_position) for txid at height.

Parameters:
Return type:

tuple[list[str], int]

async get_raw_tx(txid, min_confirmations=6)[source]

Return raw transaction bytes, enforcing min_confirmations.

Parameters:
  • txid (Txid)

  • min_confirmations (int)

Return type:

RawTx

async get_tip_height()[source]

Return the current chain tip block height.

Return type:

BlockHeight

async get_tx_block_height(txid)[source]

Return the block height at which txid was confirmed.

Raises NetworkError if the transaction is unconfirmed or not found.

Parameters:

txid (Txid)

Return type:

BlockHeight

async get_tx_output_script_type(txid, output_index)[source]

Return the output script type: p2pkh, p2wpkh, p2sh, p2tr, or unknown.

Parameters:
Return type:

str

class pyrxd.network.BlockstreamSource[source]

Bases: BtcDataSource

BtcDataSource backed by the blockstream.info HTTP API.

__init__(base_url='https://blockstream.info/api')[source]
Parameters:

base_url (str)

Return type:

None

async close()[source]

Close any underlying connections held by this source.

Return type:

None

async get_block_hash(height)[source]

Return the 32-byte block hash at height.

Parameters:

height (BlockHeight)

Return type:

Hex32

async get_block_header_hex(height)[source]

Return the raw 80-byte block header at height.

Parameters:

height (BlockHeight)

Return type:

bytes

async get_header_chain(start_height, count)[source]

Return count consecutive 80-byte headers starting at start_height.

Parameters:
Return type:

list[bytes]

async get_merkle_proof(txid, height)[source]

Return (branch_hashes_hex, leaf_position) for txid at height.

Parameters:
Return type:

tuple[list[str], int]

async get_raw_tx(txid, min_confirmations=6)[source]

Return raw transaction bytes, enforcing min_confirmations.

Parameters:
  • txid (Txid)

  • min_confirmations (int)

Return type:

RawTx

async get_tip_height()[source]

Return the current chain tip block height.

Return type:

BlockHeight

async get_tx_block_height(txid)[source]

Return the block height at which txid was confirmed.

Raises NetworkError if the transaction is unconfirmed or not found.

Parameters:

txid (Txid)

Return type:

BlockHeight

async get_tx_output_script_type(txid, output_index)[source]

Return the output script type: p2pkh, p2wpkh, p2sh, p2tr, or unknown.

Parameters:
Return type:

str

class pyrxd.network.BtcDataSource[source]

Bases: ABC

Abstract interface for blockchain data providers.

abstractmethod async close()[source]

Close any underlying connections held by this source.

Return type:

None

abstractmethod async get_block_hash(height)[source]

Return the 32-byte block hash at height.

Parameters:

height (BlockHeight)

Return type:

Hex32

abstractmethod async get_block_header_hex(height)[source]

Return the raw 80-byte block header at height.

Parameters:

height (BlockHeight)

Return type:

bytes

abstractmethod async get_header_chain(start_height, count)[source]

Return count consecutive 80-byte headers starting at start_height.

Parameters:
Return type:

list[bytes]

abstractmethod async get_merkle_proof(txid, height)[source]

Return (branch_hashes_hex, leaf_position) for txid at height.

Parameters:
Return type:

tuple[list[str], int]

abstractmethod async get_raw_tx(txid, min_confirmations=6)[source]

Return raw transaction bytes, enforcing min_confirmations.

Parameters:
  • txid (Txid)

  • min_confirmations (int)

Return type:

RawTx

abstractmethod async get_tip_height()[source]

Return the current chain tip block height.

Return type:

BlockHeight

abstractmethod async get_tx_block_height(txid)[source]

Return the block height at which txid was confirmed.

Raises NetworkError if the transaction is unconfirmed or not found.

Parameters:

txid (Txid)

Return type:

BlockHeight

abstractmethod async get_tx_output_script_type(txid, output_index)[source]

Return the output script type: p2pkh, p2wpkh, p2sh, p2tr, or unknown.

Parameters:
Return type:

str

class pyrxd.network.ChainTracker[source]

Bases: object

Verifies Merkle inclusion proofs against confirmed block headers.

Bitcoin block header layout (80 bytes, all fields little-endian):
  • version : 4 bytes [0:4]

  • prev_hash : 32 bytes [4:36]

  • merkle_root : 32 bytes [36:68] ← compared here

  • time : 4 bytes [68:72]

  • bits : 4 bytes [72:76]

  • nonce : 4 bytes [76:80]

The merkle_root in the header is stored in little-endian byte order, matching the convention used by MerklePath.compute_root().

__init__(btc_source)[source]
Parameters:

btc_source (BtcDataSource)

Return type:

None

async is_valid_root(merkle_root, height)[source]

Fetch the block header at height and check its Merkle root.

Parameters:
  • merkle_root (Hex32) – The 32-byte Merkle root to verify (as Hex32).

  • height (BlockHeight) – Block height of the header to check against.

Returns:

True if the header’s Merkle root matches merkle_root.

Return type:

bool

async is_valid_root_for_height(root_hex, height)[source]

Convenience wrapper accepting hex string root and plain int height.

This matches the signature expected by MerklePath.verify().

Parameters:
  • root_hex (str) – 64-char lowercase hex string (big-endian display order, as returned by MerklePath.compute_root()).

  • height (int) – Block height as a plain int.

Return type:

bool

class pyrxd.network.ElectrumXClient[source]

Bases: object

Async ElectrumX JSON-RPC client.

Parameters:
  • urls – One or more ElectrumX server URLs. The client uses the first URL; on disconnect it attempts one reconnect, then raises NetworkError.

  • allow_insecure – If False (default) ws:// URLs raise NetworkError immediately. Set to True only for local testing.

  • timeout – Per-request timeout in seconds (default 30).

__init__(urls, *, allow_insecure=False, timeout=30.0)[source]
Parameters:
Return type:

None

async broadcast(raw_tx)[source]

Broadcast a raw transaction to the network.

Parameters:

raw_tx (bytes) – Serialised transaction bytes.

Returns:

The transaction id returned by the server.

Return type:

Txid

async call_extension(method, params=None)[source]

Call an arbitrary JSON-RPC method on the connected server.

Use this for indexer-extension RPCs that aren’t part of the base ElectrumX surface — e.g. RXinDexer’s wave.resolve, glyph.get_token, swap.get_unconfirmed_orders. The underlying transport (connection, id correlation, error handling) is identical to the built-in methods.

Returns the raw result field from the JSON-RPC response. Server errors raise NetworkError. The caller is responsible for validating the result shape.

Parameters:
  • method (str)

  • params (list | None)

Return type:

Any

async close()[source]

Close the underlying WebSocket connection.

Cancels the reader task, fails any in-flight requests with NetworkError, and closes the socket.

Return type:

None

async get_balance(script_hash)[source]

Return the confirmed and unconfirmed balance for script_hash.

The script_hash is sha256(locking_script) with bytes reversed (ElectrumX little-endian convention). Accepts Hex32, raw bytes (length 32), or a hex str (length 64).

Returns:

(confirmed, unconfirmed)

Return type:

tuple[Satoshis, Satoshis]

Parameters:

script_hash (Hex32 | bytes | str)

async get_block_header(height)[source]

Return the raw 80-byte block header at height.

Parameters:

height (BlockHeight)

Return type:

bytes

async get_history(script_hash)[source]

Return the transaction history for script_hash.

Returns a list of {"tx_hash": str, "height": int} dicts. Unconfirmed transactions have height of 0 or negative.

Parameters:

script_hash (Hex32 | bytes | str)

Return type:

list[dict]

async get_tip_height()[source]

Return the current chain tip block height.

Uses blockchain.headers.subscribe, whose INITIAL response is the current tip header — {"height": N, "hex": "..."} (standard ElectrumX). The call also installs a server-side header-push subscription, but that is harmless here: the reader loop drops every id-less server push (see _reader_loop()), so later header notifications never interfere with request/response matching.

(The prior implementation called blockchain.block.header [0, 0] expecting a {"height", ...} dict, but standard ElectrumX returns the bare genesis-header hex string for that call — so the tip read raised “Unexpected response type” against real servers, e.g. electrumx.radiant4people.com.)

Return type:

BlockHeight

async get_transaction(txid)[source]

Fetch the raw transaction bytes for txid.

Returns:

The serialised transaction (> 64 bytes, Merkle-forgery safe).

Return type:

RawTx

Parameters:

txid (Txid)

async get_transaction_merkle(txid, height)[source]

Fetch the Merkle proof for txid at block height.

Returns:

A parsed Merkle path object.

Return type:

MerklePath

Parameters:
async get_transaction_verbose(txid)[source]

Fetch the verbose JSON-decoded form of a transaction.

Calls blockchain.transaction.get with verbose=True and returns the dict the server provides — including confirmations, blockhash, blocktime. Used by confirmation polling.

Distinct from get_transaction() (which returns raw bytes for cryptographic operations like merkle-proof checks). Callers polling for “is this tx confirmed yet?” want THIS one.

Parameters:

txid (Txid)

Return type:

dict[str, Any]

async get_utxos(script_hash)[source]

Return the list of UTXOs for script_hash.

Accepts Hex32, raw bytes (length 32), or a hex str (length 64). Each UTXO is returned as a typed UtxoRecord.

Parameters:

script_hash (Hex32 | bytes | str)

Return type:

list[UtxoRecord]

class pyrxd.network.MempoolSpaceSource[source]

Bases: BtcDataSource

BtcDataSource backed by the mempool.space HTTP API.

Parameters:

base_url – Base URL of the API (default https://mempool.space/api).

__init__(base_url='https://mempool.space/api')[source]
Parameters:

base_url (str)

Return type:

None

async close()[source]

Close the underlying HTTP session.

Return type:

None

async get_block_hash(height)[source]

Return the 32-byte block hash at height.

Parameters:

height (BlockHeight)

Return type:

Hex32

async get_block_header_hex(height)[source]

Return the raw 80-byte block header at height.

Parameters:

height (BlockHeight)

Return type:

bytes

async get_header_chain(start_height, count)[source]

Return count consecutive 80-byte headers starting at start_height.

Parameters:
Return type:

list[bytes]

async get_merkle_proof(txid, height)[source]

Return (branch_hashes_hex, leaf_position) for txid at height.

Parameters:
Return type:

tuple[list[str], int]

async get_raw_tx(txid, min_confirmations=6)[source]

Return raw transaction bytes, enforcing min_confirmations.

Parameters:
  • txid (Txid)

  • min_confirmations (int)

Return type:

RawTx

async get_tip_height()[source]

Return the current chain tip block height.

Return type:

BlockHeight

async get_tx_block_height(txid)[source]

Return the block height at which txid was confirmed.

Raises NetworkError if the transaction is unconfirmed or not found.

Parameters:

txid (Txid)

Return type:

BlockHeight

async get_tx_output_script_type(txid, output_index)[source]

Return the output script type: p2pkh, p2wpkh, p2sh, p2tr, or unknown.

Parameters:
Return type:

str

class pyrxd.network.MultiSourceBtcDataSource[source]

Bases: BtcDataSource

A quorum-based composite data source.

For read operations, all sources are queried concurrently and the result is returned only if at least quorum sources agree. For broadcast-style operations, sources are tried in order until one succeeds.

Parameters:
  • sources – Two or more BtcDataSource instances.

  • quorum – Minimum number of agreeing sources required (default 2).

__init__(sources, quorum=2)[source]
Parameters:
Return type:

None

async close()[source]

Close all underlying sources.

Return type:

None

async get_block_hash(height)[source]

Return the 32-byte block hash at height.

Parameters:

height (BlockHeight)

Return type:

Hex32

async get_block_header_hex(height)[source]

Return the raw 80-byte block header at height.

Parameters:

height (BlockHeight)

Return type:

bytes

async get_header_chain(start_height, count)[source]

Return count consecutive 80-byte headers starting at start_height.

Parameters:
Return type:

list[bytes]

async get_merkle_proof(txid, height)[source]

Return (branch_hashes_hex, leaf_position) for txid at height.

Parameters:
Return type:

tuple[list[str], int]

async get_raw_tx(txid, min_confirmations=6)[source]

Return raw transaction bytes, enforcing min_confirmations.

Parameters:
  • txid (Txid)

  • min_confirmations (int)

Return type:

RawTx

async get_tip_height()[source]

Return the current chain tip block height.

Return type:

BlockHeight

async get_tx_block_height(txid)[source]

Return the block height at which txid was confirmed.

Raises NetworkError if the transaction is unconfirmed or not found.

Parameters:

txid (Txid)

Return type:

BlockHeight

async get_tx_output_script_type(txid, output_index)[source]

Return the output script type: p2pkh, p2wpkh, p2sh, p2tr, or unknown.

Parameters:
Return type:

str

class pyrxd.network.MultiSourceBtcFundingReader[source]

Bases: object

Quorum BtcFundingReader over N independent Esplora-style providers.

Audit 2026-05-29 F-17: mitigates the single-source confirmation-depth SPOF — a lone compromised/MITM’d source that OVER-reports depth (under-reports block_height) can make an unburied/reorgable tx look final and trigger a premature release.

Operator policy (decided 2026-05-29):
  • quorum = 2 of 3 providers (majority): tolerates one source down or lying.

  • dust_cap_sats = 10_000: at/below the cap a single successful read is accepted (the documented dust posture); ABOVE it the quorum is REQUIRED (fail-closed).

  • confirmations() returns the MINIMUM depth across responding sources — a tx is only as buried as the most-pessimistic source, defeating an over-reporter.

  • read_output_amount_sats() requires >= quorum sources to agree on the EXACT amount (a deterministic value; disagreement fails closed).

Satisfies the same duck-typed reader Protocol as MempoolSpaceFundingReader, so it is a drop-in for the reorg gate / funding read-back on above-dust swaps. A failing source is simply dropped from the quorum (never fails the whole read).

DEFAULT_MAINNET_ENDPOINTS = ('https://mempool.space/api', 'https://blockstream.info/api', 'https://mempool.emzy.de/api')

Default independent mainnet Esplora endpoints (distinct operators).

__init__(readers, *, quorum=2, dust_cap_sats=10000)[source]
Parameters:
Return type:

None

async close()[source]
Return type:

None

async confirmations(txid, *, value_sats=None)[source]

Quorum’d confirmation depth, returning the conservative MINIMUM.

value_sats selects the dust gate: None (the default, used by the reorg gate) or any value above dust_cap_sats REQUIRES the quorum and fails closed otherwise; a value at/below the cap accepts a single source.

Parameters:
  • txid (str)

  • value_sats (int | None)

Return type:

int

classmethod default_mainnet(*, quorum=2, dust_cap_sats=10000)[source]

Wire the three default independent mainnet Esplora endpoints (2-of-3).

Parameters:
  • quorum (int)

  • dust_cap_sats (int)

Return type:

MultiSourceBtcFundingReader

classmethod from_endpoints(urls, *, quorum=2, dust_cap_sats=10000)[source]

Build the reader from Esplora base URLs, clamping the effective quorum to the number of DISTINCT hosts. A quorum of same-host endpoints is false corroboration (one hostile/buggy host satisfies it), so e.g. two mempool.space URLs can never form a 2-of-2 quorum. A clamp is logged loudly so the operator sees the real corroboration level.

Parameters:
Return type:

MultiSourceBtcFundingReader

async list_address_utxos(address)[source]

UTXO discovery (not a value gate): return the first source that responds.

Parameters:

address (str)

Return type:

list[dict]

async read_output_amount_sats(txid, vout, *, min_confirmations)[source]

Quorum’d output-amount read-back. Above the dust cap the exact amount must be corroborated by >= quorum sources; the conf depth is quorum’d separately.

Parameters:
  • txid (str)

  • vout (int)

  • min_confirmations (int)

Return type:

int

async txid_of(raw_tx)[source]
Parameters:

raw_tx (bytes)

Return type:

str

pyrxd.network.choose_funding_reader(value_sats, *, single, multi, dust_cap_sats=10000)[source]

Route a funding-reader choice by swap value (audit 2026-05-29 F-17).

Returns the SINGLE-source reader for a value at/below dust_cap_sats (the documented dust posture — a deliberate single-source SPOF the operator accepts for trivial value) and the MULTI-source quorum reader ABOVE it (fail-closed corroboration; see MultiSourceBtcFundingReader). Inject the result as a BtcLeg’s funding_reader.

single and multi may each be a reader INSTANCE or a zero-argument FACTORY — a factory is invoked only for the chosen reader, so the unused one (e.g. the quorum reader’s three HTTP sessions on a dust swap) is never built.

Use network-appropriate readers: the quorum reader’s default_mainnet() endpoints are mainnet-only, so a signet/testnet above-dust path must supply its own endpoint set.

Parameters:
  • value_sats (int)

  • dust_cap_sats (int)