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. A failure the node decided —
it evaluated your transaction and said no — surfaces as the narrower
PolicyRejection, which subclasses NetworkError (and CovenantError),
so an existing except NetworkError handler still catches it.
PolicyRejection carries the node’s own reject reason: .code is the
JSON-RPC error code and .reason is the sanitized server message —
first line only, control characters stripped, long tokens redacted,
clipped to 200 characters. That sanitized reason is also embedded in the
exception message. It is the one deliberate carve-out from the module’s
“never put server text in an exception” rule: discarding it previously
made a covenant script failure indistinguishable from a dropped socket,
and hid a real dMint bug for weeks.
from pyrxd.security.errors import NetworkError, PolicyRejection
try:
txid = await client.broadcast(raw_tx)
except PolicyRejection as exc:
# The node evaluated the tx and rejected it. Branch on exc.reason.
print(exc.code, exc.reason) # e.g. -26 bad-txns-inputs-missingorspent
raise
except NetworkError:
# A genuine transport fault: dropped socket, timeout, unreachable server.
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.
All four reach you as a PolicyRejection whose .reason is the string
above, so you can branch on them in code rather than inspecting a block
explorer. Classification is by RPC code (1, -25, -26, -27) or by
a reject-reason marker; the full marker list is _POLICY_MESSAGE_MARKERS
in src/pyrxd/network/electrumx.py,
and bad-txns-, txn-mempool-conflict, min relay fee not met and
mandatory-script-verify-flag-failed are all in it. An RPC error that
matches neither stays a plain NetworkError carrying only the code.
Turning on DEBUG logging for pyrxd.network.electrumx will not add
anything here: the reader loop’s error branch logs nothing before
setting the exception. The exception message is the diagnostic.
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