How to troubleshoot common errors¶
Who this page is for: you ran a pyrxd command (or called the library
directly) and got an error, or a symptom that looks like an error but isn’t
one (a balance that’s silently wrong). This page maps concrete error text
and symptoms to a cause and a one-line fix. It is not a full API or
exception reference — for that, read the cited source files directly.
Every entry below quotes a real string found in the pyrxd source at the time of writing, with a file and line citation. Where a plausible-sounding error doesn’t actually exist in the code, this page says so rather than inventing one — grep the source yourself if a string here looks stale; line numbers drift as the codebase changes.
How pyrxd reports errors¶
Every CLI-level failure is a CliError (a click.ClickException subclass)
with up to three parts — a one-line message, a cause (what went wrong,
sensitive values redacted), and a fix (a concrete next step). format_message
renders them as:
error: <message>
cause: <cause>
fix: <fix>
The exit code tells you which family the error is from, before you even read the text:
Code |
Meaning |
|---|---|
|
success |
|
user error (bad input, missing file, insufficient funds) |
|
network error (couldn’t reach ElectrumX, broadcast rejected) |
|
wallet decryption failed |
|
unexpected error (a bug — should not happen; file an issue) |
Pass --debug to any command to also print the underlying Python traceback
(no captured local variables — just function names, line numbers, and source
lines, the same exposure as any uncaught exception).
Library code (used directly, outside the CLI) raises typed exceptions from
pyrxd.security.errors instead — ValidationError, NetworkError,
KeyMaterialError, and so on. The CLI wraps those into the three-line
CliError block above; the entries below give you both forms where they
differ.
1. Zero balance after restoring a wallet (no error at all)¶
Symptom: you restore a mnemonic, pyrxd balance shows 0, but a block
explorer shows the address holding funds — except it’s not the address
your wallet derived, because the funds landed on a different BIP44 path.
Cause: the Radiant ecosystem never agreed on a BIP44 coin_type, so the
same seed produces different addresses depending on which wallet derived it:
coin_type |
Used by |
|---|---|
|
Photonic ≤ v2.x (legacy), Electron-Radiant, Chainbow |
|
SLIP-0044 spec, Tangem, Photonic ≥ v3.0.0 (pyrxd’s default) |
|
pre-#14 pyrxd (BSV’s coin type) |
See the module docstring in
src/pyrxd/hd/discovery.py
for the full explanation and DEFAULT_COIN_TYPES = (0, 512, 236).
Fix: pyrxd wallet recover --scan — scans every coin-type/account
combination and reports which path actually holds funds. Full walkthrough,
including wallet sweep to move the funds once found:
Recover funds across wallet paths.
2. Wallet won’t decrypt after upgrading pyrxd (non-ASCII passphrase)¶
Symptom: a wallet file created before pyrxd 0.12.0 with a non-ASCII BIP39 passphrase (accents, umlauts, CJK — anything beyond plain ASCII) now fails to decrypt, even though you’re certain the mnemonic and passphrase are correct.
What you actually see at the CLI: every wallet-opening command (balance,
utxos, any glyph or wallet subcommand that decrypts the wallet file)
funnels a decrypt failure through the same generic block, exit code 3:
error: Could not decrypt wallet file
cause: wrong mnemonic, wrong passphrase, or wallet file is corrupt
fix: check the mnemonic and passphrase exactly, then try again
— WalletDecryptError’s default message
(src/pyrxd/cli/errors.py:179-184).
Verified: the CLI’s wallet loader deliberately discards the library’s
more specific decrypt-failure message (which, called directly, includes an
NFKD hint pointing at this exact cause) and always re-raises this generic
text —
src/pyrxd/cli/prompts.py:166-174
and
src/pyrxd/cli/wallet_cmds.py:193-201
both do this on purpose (never echo the user’s input back). So at the CLI,
this generic message is the only symptom you get — nothing on-screen
mentions NFKD or normalization.
Cause: BIP39 requires the mnemonic and passphrase to be NFKD-normalized before they enter PBKDF2. pyrxd versions before 0.12.0 skipped that step, so a non-ASCII passphrase could derive a different seed than the spec requires — and the wallet file’s AES-GCM key is derived from that seed, so a file saved under the old seed only decrypts by reproducing it.
Fix: the escape hatch is normalize=False, reachable from
HdWallet.load, load_or_create, from_mnemonic, the bip32_*/bip44_*
derivation helpers, and pyrxd.hd.discover — but not exposed as a CLI
flag, so this recovery is library-only. Full recipe (how to tell if you’re
affected, and how to sweep funds off the legacy seed permanently) is the
“Non-ASCII passphrases” section of
Recover funds across wallet paths.
3. “no single UTXO is large enough to fund the mint / deploy”¶
Symptom, verbatim (one of three, depending on command):
error: no single UTXO is large enough to fund the mint
error: no single UTXO is large enough to fund the deploy
error: no single UTXO is large enough to fund the dMint deploy
From glyph mint-nft (_mint_nft_inner), glyph deploy-ft
(_deploy_ft_inner) and glyph deploy-dmint (_deploy_dmint_inner)
respectively, all in
src/pyrxd/cli/glyph_cmds.py. The cause line names the exact shortfall (need ≥ N photons in one UTXO; largest is M; mint-nft says in one plain-RXD (token-free) UTXO
and largest in the wallet is M, because it never spends a UTXO that carries a
token). When the metadata registers a WAVE name, mint-nft’s cause also names
the registration fee it includes (…, including the 5 RXD WAVE registration fee for alice-name.rxd, which the reveal pays from this UTXO's change; …).
Cause: the commit/deploy builder needs one single UTXO that covers the commit value plus both the commit’s own fee estimate and slack for the reveal — it does not combine multiple smaller UTXOs for this step. For a WAVE name the same UTXO must also hold the registration fee (100, 50, 10 or 5 RXD for a name of up to 3, 4, 5 or 6+ characters). The commit output does not carry it: it stays in the commit’s change, and the reveal spends that change as a second input to pay the WAVE treasury.
Fix (all three, same text): consolidate UTXOs first, or fund the wallet from a single source.
4. “commit value cannot cover the reveal fee” (reveal too big to pay for itself)¶
Symptom, verbatim (from glyph mint-nft and glyph deploy-ft):
error: commit value cannot cover the reveal fee — refusing to broadcast the commit
cause: commit value cannot fund the reveal: <N> photons available, <M> required
(<carrier> carrier + <fee> reveal fee for a <size>-byte reveal carrying
<cbor> bytes of CBOR at <rate> photons/byte) — short by <shortfall>
fix: shrink the metadata (the reveal scriptSig carries the whole CBOR payload) or lower --fee-rate
— _assert_reveal_is_fundable in
src/pyrxd/cli/glyph_cmds.py
wraps the library’s InsufficientFundsError from
check_reveal_funding
(src/pyrxd/glyph/fees.py:620-656). Before that, the whole reveal is measured by
assert_reveal_balances (same file): when the metadata registers a WAVE name its
reveal also spends the wallet input that pays the fee, and a shortfall reads
the reveal does not balance: inputs … ; outputs … + <photons> WAVE registration fee for <name>.rxd to <treasury>; miner fee … — short by <n> (or balances only by dropping its change). The registration fee never comes out of the commit.
Cause: the reveal transaction’s scriptSig carries the entire CBOR
metadata payload, so a large metadata.json (a long image URL, several
attributes) makes the reveal itself big — and therefore its own fee, which
is paid entirely out of the commit output. check_reveal_funding computes
the real fee (via the same SatoshisPerKilobyte.compute_fee the actual
transaction uses, so it can’t drift) and refuses before the commit is
broadcast if the commit can’t cover it. Without this guard, the failure
would happen after the commit was already on-chain, stranding it — see the
module docstring in fees.py for the history.
Fix: shrink the metadata (drop a long image URL, trim attributes), or
raise the effective fee-model headroom. Note: the --fee-rate mentioned
in the fix text is not an actual flag on mint-nft or deploy-ft —
verified: neither command declares a --fee-rate option
(src/pyrxd/cli/glyph_cmds.py).
The real lever is the global fee rate, set via fee_rate in
~/.pyrxd/config.toml or the PYRXD_FEE_RATE environment variable (photons
per byte; default 10000) — see
src/pyrxd/cli/config.py.
In practice, shrinking the metadata is the more useful fix, since fee rate
is a network-relay floor, not something you generally want lower.
Scope: this pre-broadcast guard runs for glyph mint-nft and glyph deploy-ft. glyph deploy-dmint sizes its commit differently (multiple
contract ref-seeds, not a single reveal-fee-vs-carrier trade) and does not
call this same check.
5. Insufficient funds on glyph claim-dmint¶
Two distinct failures, both from glyph claim-dmint:
a) No funding UTXO found at all — verbatim:
error: could not find a plain-RXD funding UTXO for the mint
cause: <InvalidFundingUtxoError text>
fix: fund <miner_address> with >= <needed> photons of plain RXD, or pass --reward-address
— _claim_prepare in
src/pyrxd/cli/glyph_cmds.py.
needed = contract.reward + 10_000_000 + 546 photons — the reward, a fee
buffer, and dust for the change output. This scans for a UTXO that is
plain RXD only (token-bearing UTXOs at the same address are excluded).
b) A funding UTXO exists but is too small — the library’s
PoolTooSmallError, message shape:
funding_utxo (<value> photons) too small to cover reward (<reward>) + fee (<fee>):
change would be <change> photons, below pyrxd's 546-photon uneconomic-change floor
(a pyrxd send policy, NOT a Radiant relay limit — Radiant's floor is 1 photon).
Fund the mint from a larger UTXO.
— raised by build_dmint_mint_tx in
src/pyrxd/glyph/dmint/miner.py
(both the V1 and V2 paths). It can happen even after (a) found a UTXO, because
(a)’s needed allows a flat 10,000,000 photons for the fee while the mint pays
size x fee_rate: a raised fee rate can outgrow that allowance. claim-dmint
reports it as:
error: funding can't cover the mint reward + fee
cause: <PoolTooSmallError text>
fix: fund the reward address with one plain-RXD UTXO of at least the reward + the fee above + 546 photons. ...
claim-dmint has no --fee-rate flag; the rate is the configured one (fee_rate
in the config file, or PYRXD_FEE_RATE). The default rate is the relay floor and
the config refuses anything lower, so lowering it only helps if you raised it.
Fix: fund the reward/miner address (--reward-address, or the wallet
address with the largest UTXO by default) with one plain-RXD UTXO of at least
the reward + the fee in the cause + 546 photons. The funding scan picks the
largest qualifying UTXO, so a new, larger one is the one it uses.
Not a funding problem, and no longer reported as one. Before 2026-09-23 two more errors printed the “funding can’t cover” headline although neither is about the amount; each now has its own:
The grind stopped without a nonce (V2 claims; V1 rerolls its OP_RETURN instead and reports
no nonce found within N preimage rerolls— see below). When the wall clock ran out — the bundled or in-process miner reaching--timeout, or an external--miner-cmdnot answering within it:error: mining timed out after <timeout>s without finding a nonce fix: allow a longer grind with --timeout SECONDS (this run allowed <timeout>); ...
When a count ran out instead (the in-process
--max-attemptscap, or a miner that swept its nonce space):error: mining stopped without finding a nonce. Its fix names a different--op-return, because the proof-of-work preimage binds the contract, the funding script and the OP_RETURN: re-running with the same inputs hands the miner the same preimage. With--miner-cmd in-processit also names--max-attempts, which reaches no other miner. Runpyrxd glyph dmint-estimateagainst the contract to see how long its target is likely to take on your machine.A V1 claim ran out of rerolls (
no nonce found within N preimage rerolls). Reroll i grinds the--op-returnvalue with i appended, so the same--op-returnrepeats every search of the run before; the fix names a different one. It names--timeoutonly when some grind was stopped by the clock (a swept nonce space is not helped by more time), and--max-attemptsonly for--miner-cmd in-process.The funding UTXO carries a token (
InvalidFundingUtxoErrorfrom the mint builder):error: the funding UTXO carries a token and cannot pay for the mint. The funding scan in (a) already skips token-bearing UTXOs, so this is the builder’s second line of defence rather than something (a)’s choice produces.
Contracts claim-dmint refuses before any funding work. Right after it reads
the contract, before it scans the wallet or grinds:
error: pyrxd cannot mine this contract: its proof of work is BLAKE3(orK12). Every minerclaim-dmintcan use grinds and verifies SHA256d, including the external-miner protocol behind--miner-cmd, whose request carries no algorithm. Minting such a contract needs a miner for that hash; pyrxd does not ship one.error: pyrxd will not mint this contract— thecauseline says why:this dMint contract can never be minted: its target ...— a target the covenant cannot read as a number: wider than 8 bytes, or not minimally encoded. pyrxd deployed both before 2026-09-23 — BLAKE3/K12 V2 contracts with 33-byte targets, and V1 contracts at difficulty 256 or more with the target pushed as a fixed 8 bytes. No miner can mint one.this dMint contract can never be minted: its maxHeight ...(orits reward ...) — the other two state numbers the covenant reads on every mint, pushed wider than 8 bytes or not minimally. No miner can mint one.this dMint contract's target is 0— the covenant accepts only a proof of work whose number is exactly 0, and pyrxd does not grind for that.this V1 dMint contract cannot be minted further— a V1 contract at height 2^31−1 whose max height is above 2^31. Its next mint would have to write height 2^31 into a 4-byte field, which the covenant cannot do, so every further spend fails. Every height below that mints normally.
pyrxd glyph dmint-estimaterefuses the same contracts rather than print an ETA.
All of these are mapped in claim_dmint_cmd, _claim_prepare and _grind_stopped_error in
src/pyrxd/cli/glyph_cmds.py.
6. The node rejected your broadcast¶
Broadcast a transaction already documents the
four rejection symptoms you’ll actually hit, in its “Handling broadcast
errors” section — bad-txns-inputs-missingorspent, txn-mempool-conflict,
min relay fee not met, mandatory-script-verify-flag-failed — read that
page for what each one means and how to recover. This section restates
what pyrxd raises for them, which is the same answer that page gives.
Current behavior (verified): ElectrumXClient now classifies a node’s
JSON-RPC error. If the error code is one of {1, -25, -26, -27} (the codes
ElectrumX/bitcoind use for “the node evaluated your tx and said no”), or the
sanitized reason matches a known reject-reason marker (bad-txns-,
txn-mempool-conflict, min relay fee not met,
mandatory-script-verify-flag-failed, dust, non-final, and others — see
_POLICY_MESSAGE_MARKERS), it raises the typed PolicyRejection instead of
a bare NetworkError:
from pyrxd.security.errors import PolicyRejection, NetworkError
try:
txid = await client.broadcast(raw_tx)
except PolicyRejection as exc:
print(exc.code, exc.reason) # e.g. -26, "bad-txns-inputs-missingorspent"
except NetworkError:
... # a genuine transport fault (dropped socket, timeout, etc.)
— src/pyrxd/network/electrumx.py,
_rpc_error and _POLICY_MESSAGE_MARKERS.
PolicyRejection is a subclass of both NetworkError and CovenantError,
so existing except NetworkError handlers still catch it. The node’s
rejection reason is sanitized (first line only, control characters
stripped, long tokens redacted, clipped to 200 chars) but is attached to
the exception via .reason — it is the one deliberate carve-out from
pyrxd’s “never embed server text in an exception” rule, because discarding
it previously hid a real covenant-rejection bug for weeks. All four
rejections named in the broadcast-a-transaction.md table match a marker
above, so all four are distinguishable via exc.reason — you no longer
need to inspect the block explorer to tell them apart. DEBUG logging would
not help either way: the reader loop’s error branch logs nothing before
setting the exception.
Fix: catch PolicyRejection specifically and branch on .reason; fall
back to the broadcast-a-transaction.md symptom table for what each reason
means.
7. Can’t reach ElectrumX, or requests keep timing out¶
Three real strings from ElectrumXClient:
"Failed to connect to any ElectrumX server"—src/pyrxd/network/electrumx.py:1055: every server in your URL list failed to connect. Check the URL(s), your network, and that the server is up."ElectrumX connection lost"—:1143: the WebSocket dropped mid-session; all pending requests fail with this. Reconnect (a freshElectrumXClient/async withblock)."ElectrumX request timed out"(or"... (send)") —:1192, :1197: a single RPC call exceeded its timeout. Retry, or pass a longer timeout if the server is just slow.
A different shape you’ll see during commit→reveal waits: glyph mint-nft, deploy-ft, and deploy-dmint all poll for the commit tx’s
confirmation before broadcasting the reveal, via
pyrxd.network.confirm.wait_for_confirmation
(src/pyrxd/network/confirm.py).
This poller swallows any NetworkError from an individual poll and
keeps retrying — right after a broadcast, the tx is routinely not yet
visible to the server, and that’s expected, not fatal. It remembers the
last such error and names it in the eventual timeout message, so a
persistently broken transport is still diagnosable instead of a bare “gave
up.” At the CLI, a poll that never confirms surfaces as:
error: timed out waiting for confirmation
cause: tx has <N> confirmations, required <M> (timeout (last poll error: <text>))
fix: the transaction <txid> was broadcast and has not confirmed yet. It may still confirm, so do
not simply re-run the command: that would commit, and spend, again. …
— src/pyrxd/cli/glyph_cmds.py,
exit code 2. Fix: check the commit txid on a block explorer, and do not
re-run the mint while the commit may still confirm.
glyph mint-nftorglyph timelock-mint. The command saved a record of the commit before broadcasting it, inpending-mints/beside the wallet file (~/.pyrxd/pending-mintsfor the default wallet). The error names the commit’s txid, its value and that record, and prints the exact command to run once the commit confirms:pyrxd --network <net> --wallet <path> glyph resume-mint <commit txid>(the wallet path home-relative,~/…; with--electrumxif the mint named a server, and--passphraseif the wallet was opened with one — the passphrase itself is never printed), and with the mint’s WAVE fee choice STATED:--wave-registration-feeif it paid the published treasury,--no-wave-registration-feeif it declined, and--wave-treasury <address>with the address the mint was given on its own command line if it named another treasury. That is the only place pyrxd prints a treasury other than the published one: every command printed from a record read off disk — anythingresume-mintprints, and the refusals below — shows--wave-treasury '<ADDRESS>'for you to fill in, because a record can be changed after the mint.--allow-unverified-wave-nameis repeated if the mint went ahead without an indexer’s answer.resume-minttakes the fee choice from its command line, never from the record: a bareresume-mint <txid>of a WAVE claim is refused and prints the commands that state it, and a choice that disagrees with the record (paying what it says was declined, or another treasury) is refused. Declining is always accepted. For a WAVE name it pays for, it asks an indexer again — before and after its confirmation prompt — and pays from a plain wallet input only if the answer is no confirmed registration; if the indexer says the name has been taken, the same command with--no-wave-registration-feereveals the commit without the fee (if the name is taken, a duplicate claim the indexer does not register; if that one server’s answer was wrong, the reveal registers the name unpaid; either way the carrier and the change come back to the wallet). A secondmint-nftfor a name that still has an unrevealed record on the same network is refused unless it is given--ignore-pending-mint. Records carry their network (one directory serves every network a wallet is used on);resume-mintrefuses a record for another network and names the--networkto use. Those two refusals print commands for a record read off disk, so they never print one command carrying the record’s fee choice: unless the record says the mint declined, they print both — one that pays and one that declines — and you choose.The window no check closes. RXinDexer answers
wave.check_availablefrom the blocks it has indexed, not from the mempool (electrumx/server/glyph_api.py:1498-1515callsWaveIndex.check_available,electrumx/server/wave_index.py:1773-1800atca8a6a4e). A rival claim that is broadcast but not yet mined is invisible to every check, and the last check runs just before the reveal is broadcast, not when it is mined. If a rival claim for the name is mined first, this reveal is a duplicate the indexer does not register, and the registration fee it paid buys nothing.What
resume-mintchecks first. The record must be the one for that txid, its commit script must re-derive from its payload and this wallet’s key, the NFT must go to this wallet, the server must list the commit output at the value the record says, and the record’s fee rate must be between Radiant’s relay floor and 10 times it. The reveal’s miner fee comes out of the commit alone; the wallet input pays only the registration fee and its own change. Any mismatch is refused, naming the field, before anything is signed. A value the server lists differently from the record is refused without blaming either: retry against another server (--electrumx) before deciding the record was changed.The record is never deleted. Once a reveal is reported confirmed — by
mint-nftorresume-mint, or byresume-mintfinding a confirmed spend of the commit that pushes the record’s exact payload — the record is moved topending-mints/done/, not deleted. “Confirmed” is the server’s word: a server can echo a reveal it never relayed, or invent a spend around a payload it reconstructed. If an explorer shows the commit output still unspent, runresume-mint <commit txid>against another server (--electrumx): it finds an archived record as well as a live one, moves it back, and reveals the commit. If the server lists the commit output as neither unspent nor spent, it says so and keeps the record where it is. That is why a finishedmint-nft,resume-mintortimelock-mintsays the server reports the reveal confirmed, names where the record is kept, and prints that command with--electrumx '<another-server-url>'(in--json:reveal_confirmed,record,recover_if_not_mined). The SDK’sGlyphMinterdoes the same: it callsPendingStore.archive()once a reveal is reported confirmed, neverdelete(), andJsonFilePendingStoremoves the record todone/.pending-mints/donemust be absent or a real directory. It is checked (withlstat, so a symbolic link is seen as one) before the commit is broadcast and again before the reveal is, and a regular file or a link there is refused before that broadcast: move it aside and run the command again (after a commit, the refusal also names the commit’s recovery).resume-mintrefuses it before it reads any record, live or archived. On POSIX every archive operation — archiving, reading, listing and restoring a record — acts on a descriptor ofdoneopened withO_NOFOLLOW, so a link there is never followed and nothing it points to changes mode. (Apending-mintsdirectory that is itself a link is used as it is; pyrxd tightens a real one to 0700 and leaves a link’s target alone.) If archiving still fails after a reveal is reported confirmed, the command succeeds and says the record was kept where it was (record_archive_errorin--json).Every exit after the commit is broadcast — a declined reveal prompt, a network error, a node rejecting the reveal, this timeout, Ctrl-C — prints the recovery, and in
--jsonmode also writes it to stdout as a JSON document. A commit broadcast that fails or is interrupted (Ctrl-C while it is in flight) says the commit may or may not have reached the network, with what to do in each case.It never confirms. If the commit leaves the mempool without confirming, its inputs were never spent, and
resume-mintwill not find it confirmed.glyph deploy-ftorglyph deploy-dmint, or no record. There is no--resumeflag and noCOMMIT_TXIDenvironment variable for these — earlier versions of this page and of the error hint said otherwise, and were wrong. (COMMIT_TXIDis read by the standaloneexamples/*.pydemo scripts, which carry their own hard-coded metadata and cannot resume a CLI mint.) Recover through the SDK instead:from pyrxd.glyph.builder import GlyphBuilder, RevealParams from pyrxd.glyph.payload import encode_payload cbor_bytes, _payload_hash = encode_payload(metadata) # the SAME metadata file, unmodified scripts = GlyphBuilder().prepare_reveal( RevealParams( commit_txid=commit_txid, # from the explorer commit_vout=0, commit_value=commit_value, # the commit output's value cbor_bytes=cbor_bytes, owner_pkh=owner_pkh, # the minting wallet's PKH is_nft=True, # False for an FT deploy ) )
Then build the reveal transaction spending
commit_txid:0withscripts.scriptsig_suffixappended to a normal P2PKH unlock — seeexamples/glyph_mint_demo.pyfor the shape. If the metadata registers a WAVE name,scripts.registration_fee_outputis the fee: add it, funded from a plain wallet input, only ifWaveResolver.check_availablesays the name is still free; if it is taken, build withpay_registration_fee=Falseinstead.The metadata file must be byte-identical. The commit output’s script is
OP_HASH256 <payload_hash> OP_EQUALVERIFY …before its P2PKH tail, so it can only ever be spent by a reveal that pushes CBOR hashing to that exactpayload_hash. Re-encoding an edited metadata file produces a different hash and the output becomes unspendable — there is no owner-only escape path.For
glyph deploy-dmint, use the code its error prints, not the snippet above. The commit checks the metadata body, the owner key and that the token ref is carried as an FT. It checks none of the dMint contract parameters, so a reveal built with other code or other values can still spend the commit, and the token it creates is permanent. When the commit broadcast’s reply names a different txid,deploy-dmintprints theprepare_dmint_deploycode with every parameter the command used, including thelastTimeit stamped.deploy-ftnamesprepare_ft_deploy_revealwith its treasury and supply.
7b. “no ElectrumX endpoint is configured for network ‘regtest’”¶
error: no ElectrumX endpoint is configured for network 'regtest'
cause: pyrxd ships no default regtest server (shipped defaults exist for: mainnet), and it
will not fall back to another network's endpoint — that is exactly how a regtest
command ends up talking to mainnet
fix: add [networks.regtest] with electrumx = "wss://your-regtest-server:50022/" to
~/.pyrxd/config.toml, or pass --electrumx wss://your-regtest-server:50022/ (or set
PYRXD_ELECTRUMX) for a one-off run
This is deliberate, and it replaced something worse. --network testnet and
--network regtest used to “work” without any per-network configuration — by
using the mainnet ElectrumX server while reporting themselves as testnet or
regtest. A developer testing on regtest could broadcast a real transaction to
mainnet with nothing on screen to suggest it. The top-level electrumx key now
belongs to the top-level network and is never carried across a --network
change; when nothing resolves, pyrxd refuses rather than guessing.
Fix — one of:
# ~/.pyrxd/config.toml
[networks.regtest]
electrumx = "ws://127.0.0.1:50022/"
allow_insecure = true # required for plaintext ws://; regtest only
$ pyrxd --network regtest --electrumx ws://127.0.0.1:50022/ ...
--network mainnet needs no change.
Related: "ElectrumX endpoint is on the wrong chain: genesis <a> != expected <b>"
means the endpoint answered, but block 0 is not the network you selected —
ElectrumXClient.assert_chain checked, rather than trusting the URL. Either the
[networks.<name>] entry points at the wrong server, or --network is wrong.
Both hashes are printed; compare them against
pyrxd.network.registry.GENESIS_BLOCK_HASHES.
7c. --wave-name degrades: two configured servers are “one source”¶
HashMark §7.6 form 2 (verify --wave-name, glyph inspect --wave-name) needs
two configured ElectrumX servers of different operators. pyrxd counts
sources as distinct operators, as declared, or by registered domain:
an operator pyrxd ships knowledge of (
pyrxd.network.registry.KNOWN_OPERATORS— radiant4people, radiantcore and bladenet) is one source however many servers it runs, soelectrumx.radiant4people.comandelectrumx2.radiant4people.comare one;any other hosts under one registered domain are one source (
x.example.comandy.example.com), whilea.co.ukandb.co.ukare two, becauseco.ukis a public suffix; an IP address is its own source;every spelling of this machine (
localhost,127.0.0.1or anything else in127.0.0.0/8,::1) is ONE source.
If your two servers really are run by different parties under one domain, or you want two domains you know to be one operator counted once, say so per server in the config file:
# ~/.pyrxd/config.toml
electrumx_servers = [
{ url = "wss://x.example.com:50022/", operator = "alice" },
{ url = "wss://y.example.com:50022/", operator = "bob" },
]
An operator is 1-64 lower-case letters, digits, . or -. Unknown keys in a
server table, one host counted as two sources (two operators on its URLs, or an
operator on one and not another), and a declaration that contradicts a shipped
operator (e.g. electrumx2.radiant4people.com as anything but radiant4people)
are refused, and so is a declared server next to an UNDECLARED server of the same
registered domain (declare rpc.acme.io and leave backup.acme.io bare, and the
two would count as two sources where without the declaration they are one — a
declaration may merge sources, and may split a domain only when every server of
that domain in the list is declared). All are refused when the list is read: at load for the top-level
electrumx_servers, and when the network is selected for a
[networks.<name>] list — before any server is contacted. pyrxd believes the
declaration: it cannot see who runs a server, so declaring two operators that
are really one makes the check agree with itself. The limit is stated in the
pyrxd.network.source_identity module docstring.
A declaration applies to form 2 and to nothing else. The watchtower
(--rxd-electrumx-url), the BTC Esplora quorum and the ETH RPC quorum take no
operator declaration: they count by registered domain, or by an operator pyrxd
ships knowledge of. If one of them refuses two URLs as one source, use URLs of
two different registered domains.
8. dMint v2 flags and premine¶
--daa-mode requires --v2, verbatim:
error: --daa-mode requires --v2 (V1 dMint is FIXED difficulty only)
— src/pyrxd/cli/glyph_cmds.py,
deploy_dmint_cmd.
V1 dMint contracts are fixed-difficulty only; a difficulty-adjustment mode
(fixed/asert/lwma/epoch/schedule) only makes sense on a V2
contract. Fix: add --v2 to glyph deploy-dmint, or drop --daa-mode.
The historical V2 opt-out is now a non-blocking warning, not an error.
Verified in
src/pyrxd/glyph/builder.py,
GlyphBuilder._prepare_dmint_v2_deploy:
calling prepare_dmint_deploy with allow_v2_deploy=False on
DmintV2DeployParams used to refuse; as of 0.9.0 (V2 consensus-proven on
regtest + mainnet, #219) it only emits a UserWarning:
prepare_dmint_deploy was called with allow_v2_deploy=False for a
DmintV2DeployParams; V2 deploy is no longer blocked (consensus-proven
on regtest + mainnet, #219) and proceeds anyway. Drop the
allow_v2_deploy=False argument to silence this warning.
This only matters if you’re calling GlyphBuilder.prepare_dmint_deploy
directly with that argument — the CLI’s glyph deploy-dmint doesn’t expose
allow_v2_deploy and always deploys V2 when --v2 is passed.
Premine ships, for both V1 and V2. This section used to say premine was
deferred and quoted two ValidationError strings “verbatim”. Neither string
exists in the source, and neither ever fires: _validate_premine
(src/pyrxd/glyph/builder.py,
called from both DmintV1DeployParams.__post_init__ and
DmintV2DeployParams.__post_init__) is a bound check, not a refusal — it
accepts any premine_amount >= 1 photon. The CLI exposes it as
glyph deploy-dmint --premine <photons> [--premine-to <address>].
The refusals that do exist, verbatim:
premine_amount must be >= 1 photon when set (use None for no premine), got 0
premine_pkh was set without premine_amount — the premine output would not be emitted at all. Set premine_amount, or drop premine_pkh.
and, from the CLI’s own pre-check
(src/pyrxd/cli/glyph_cmds.py,
deploy_dmint_cmd):
--premine must be >= 1 photon (omit the flag for no premine)
--premine-to was given without --premine
The floor is 1 photon, not 546 — Radiant-Core has no dust threshold
(GetDustThreshold returns 1 satoshi, IsDust is nValue <= 0), which is why
every mainnet dMint contract sits at 1 photon. The 546-unit guard in
prepare_ft_deploy_reveal is a pyrxd heuristic on a different flow (a whole FT
supply below 546 units is almost certainly a decimals mistake) and does not
apply here.
Fix: if you wanted a premine, pass --premine. Those photons come out of
your own wallet on top of the mineable supply — the total issued supply becomes
reward * max_height * num_contracts + premine.
9. “My FT shows up as plain RXD” (no error — a real gap)¶
Symptom: you hold a Glyph FT (or NFT), but pyrxd balance counts its
value as ordinary RXD, and pyrxd utxos lists its UTXO like any other,
with no indication it’s a token carrier.
There is no error string for this — it’s an honest gap, not a bug that
raises anything. Verified: balance_cmd sums client.get_balance(...)
across every used address’s raw scripthash
(src/pyrxd/cli/query_cmds.py:234-294),
and utxos_cmd lists whatever wallet.collect_spendable(client) returns
(:297-354,
backed by
HdWallet.collect_spendable
at src/pyrxd/hd/wallet.py:1285-1343). Neither path decodes the locking
script to check whether it’s a Glyph FT/NFT envelope versus plain P2PKH —
both just report the UTXO’s raw value field from ElectrumX. A Glyph
carrier’s photon value (the 546-dust NFT carrier, or an FT’s premined
supply amount) gets counted exactly like spendable RXD, which it is
not — spending it as a plain-RXD input burns the token.
Fix: pyrxd glyph list --type ft (or --type nft, or --type all) —
list_cmd in src/pyrxd/cli/glyph_cmds.py —
runs the same gap-limit scan as balance and the spend commands, then decodes
the Glyph envelope at each address it finds used, so token holdings show up as
tokens, separate from the plain-RXD balance. Use this whenever you need to know
what’s actually spendable versus what’s a token carrier.
References¶
src/pyrxd/cli/errors.py— theCliErrorformat and exit-code conventionsrc/pyrxd/security/errors.py— the library’s typed exception hierarchyRecover funds across wallet paths — coin-type scanning and the non-ASCII passphrase / NFKD recovery recipe
Broadcast a transaction — the four common node-rejection symptoms
Glossary — a flat A–Z lookup for the vocabulary used across this page and the rest of the docs (ref, genesis ref vs
contract_id, dust floor, hashlock, coin_type, and more)