How to broadcast a transaction¶
You have a signed Radiant transaction (as raw bytes or a hex string) and
want to push it to the network. pyrxd does not ship a generic
pyrxd broadcast subcommand — the CLI surface is task-shaped (pyrxd glyph deploy-ft, pyrxd glyph transfer-ft, etc.), and each task
broadcasts its own tx as the final step. For a tx you built yourself,
broadcast via the async ElectrumXClient.broadcast(...) API.
Python: broadcast a signed tx via ElectrumXClient¶
ElectrumXClient is async-only. The client is a context manager —
opening it inside async with guarantees the WebSocket is closed on
exit and any in-flight RPC is cancelled.
import asyncio
from pyrxd.network.electrumx import ElectrumXClient
from pyrxd.security.errors import NetworkError
ELECTRUMX_URL = "wss://electrumx.radiant4people.com:50022/"
async def main(signed_tx_hex: str) -> None:
raw_tx = bytes.fromhex(signed_tx_hex)
async with ElectrumXClient([ELECTRUMX_URL]) as client:
txid = await client.broadcast(raw_tx)
print(f"Broadcast: {txid}")
asyncio.run(main("0100000001..."))
broadcast() accepts bytes/bytearray and validates them as
RawTx (must be > 64 bytes, the Merkle-forgery defense). It returns a
Txid on success. If you already have a Transaction object, pass
tx.serialize() rather than tx.hex() — same bytes, no round-trip
through hex. For the end-to-end “build + sign + broadcast” pattern see
examples/ft_transfer_demo.py.
The URL must be wss:// (TLS). Bare ws:// is rejected at construction
time unless you pass allow_insecure=True; do that only for a local
regtest node.
Handling broadcast errors¶
Every broadcast failure pyrxd can detect surfaces as
pyrxd.security.errors.NetworkError. The client deliberately does not
embed the server’s raw error string in the exception message (that
string can include attacker-controlled bytes from the rejected tx).
Diagnose by the symptom on the chain, not the exception text.
from pyrxd.security.errors import NetworkError
try:
txid = await client.broadcast(raw_tx)
except NetworkError:
# Inspect inputs against the chain to decide what went wrong.
raise
The four rejections you will actually hit:
bad-txns-inputs-missingorspent— one of your inputs is already spent (or never existed). Re-fetch UTXOs withclient.get_utxos(script_hash)and rebuild the tx from the current set.txn-mempool-conflict— a different tx that spends the same input is already in the mempool. Either wait for it to confirm and rebuild from the resulting UTXO set, or RBF-replace it (Radiant inherits BCH’s first-seen policy — RBF is not guaranteed; in practice you wait).min relay fee not met— the fee per byte is below the node’s relay floor. Increasefee_rate(the examples use10000photons/ byte for transfers) and rebuild. The fee field is the total fee, derived fromfee_rate * tx_byte_length.mandatory-script-verify-flag-failed— a script in your tx failed verification. For a V1 dMint mint specifically, this is the symptom of the scriptSig divergence bug fixed in 0.5.0; see V1 dMint mint scriptSig divergence for the root cause and the migration steps. For non-dMint txs it means a signature or hashlock is wrong; re-check the sighash type (Radiant uses BIP143 variants, seepyrxd.security.types.SighashFlag), the signed digest, and any ref-aware preimage pieces.
The client surfaces all four as a generic NetworkError. To see the
underlying ElectrumX response code, log at DEBUG level on the
pyrxd.network.electrumx logger — the reader loop logs RPC errors
before wrapping them.
Verify the tx landed¶
ElectrumXClient.get_transaction_verbose(txid) returns the server’s
JSON-decoded view of the tx, including a confirmations field. Poll
that until it’s >= 1 (or however many confirmations your use case
demands):
import asyncio
from pyrxd.network.electrumx import ElectrumXClient
from pyrxd.security.errors import NetworkError
from pyrxd.security.types import Txid
async def wait_for_confirm(
client: ElectrumXClient,
txid: str,
*,
min_confirmations: int = 1,
timeout_s: float = 1800.0,
interval_s: float = 10.0,
) -> None:
deadline = asyncio.get_event_loop().time() + timeout_s
while True:
try:
info = await client.get_transaction_verbose(Txid(txid))
if int(info.get("confirmations", 0)) >= min_confirmations:
return
except NetworkError:
# Tx may not be visible to this server yet — keep polling.
pass
if asyncio.get_event_loop().time() > deadline:
raise TimeoutError(f"{txid} did not confirm within {timeout_s:.0f}s")
await asyncio.sleep(interval_s)
A transient NetworkError while polling usually means the server
hasn’t seen the tx yet (mempool replication lag) — keep going. A
persistent failure across the full timeout means the tx never landed;
diagnose with the error table above. As a sanity check, the tx is also
visible to any Radiant block explorer once it confirms.
References¶
pyrxd.network.electrumx.ElectrumXClient— the broadcast and polling APIexamples/ft_transfer_demo.py— full build + sign + broadcast patternV1 dMint mint scriptSig divergence — the
mandatory-script-verify-flag-failedsymptom for V1 mints