pyrxd.glyph — Glyph token protocol¶
Glyph protocol — NFT singletons, FT tokens, dMint contracts, mutable refs.
Re-exports the public Glyph API from the submodules. Lazy via PEP 562
__getattr__ so import pyrxd.glyph.X paths that don’t need the
full builder/signing chain (e.g. pyrxd.glyph.inspect from the
browser-hosted inspect tool) avoid pulling in coincurve,
aiohttp, Cryptodome.Cipher, etc. transitively.
See pyrxd for the broader rationale on lazy public re-exports.
- class pyrxd.glyph.ContainerRevealScripts[source]¶
Bases:
objectScripts for a CONTAINER reveal.
- __init__(ref, locking_script, scriptsig_suffix, child_ref)¶
- class pyrxd.glyph.DaaMode[source]¶
Bases:
IntEnum- __new__(value)¶
- FIXED = 0¶
- EPOCH = 1¶
- ASERT = 2¶
- LWMA = 3¶
- SCHEDULE = 4¶
- class pyrxd.glyph.DmintAlgo[source]¶
Bases:
IntEnum- __new__(value)¶
- SHA256D = 0¶
- BLAKE3 = 1¶
- K12 = 2¶
- class pyrxd.glyph.DmintCborPayload[source]¶
Bases:
objectThe
dmintobject embedded in Glyph V2 token metadata CBOR.Indexers read this to discover dMint contracts and display mining parameters in wallets/explorers without parsing the contract script.
Field names mirror Photonic Wallet
DmintPayloadtype in types.ts.- __init__(algo, num_contracts, max_height, reward, premine, diff, daa_mode=DaaMode.FIXED, target_block_time=60, half_life=0, window_size=0)¶
- class pyrxd.glyph.DmintDeployParams[source]¶
Bases:
objectParameters for deploying a V2 dMint contract.
- __init__(contract_ref, token_ref, max_height, reward, difficulty, algo=DmintAlgo.SHA256D, daa_mode=DaaMode.FIXED, target_time=60, half_life=3600, height=0, last_time=0, epoch_length=2016, max_adjustment_log2=2, schedule=())¶
- Parameters:
- Return type:
None
- class pyrxd.glyph.DmintMineResult[source]¶
Bases:
objectThe output of a successful
mine_solution()call.- Parameters:
nonce – The nonce bytes (4B for V1, 8B for V2) that satisfy the target.
attempts – Number of nonce candidates tried before finding the solution.
elapsed_s – Wall-clock seconds spent searching.
- __init__(nonce, attempts, elapsed_s)¶
- class pyrxd.glyph.DmintMintResult[source]¶
Bases:
objectOutput of
build_dmint_mint_tx().- Parameters:
tx – Unsigned transaction (caller must sign).
updated_state – New
DmintStatewritten into the contract output (height incremented, target updated if DAA is active).contract_script – New contract output script (state + separator + code).
reward_script – P2PKH locking script of the miner reward output.
fee – Transaction fee in photons.
Note
The transaction returned here is unsigned — it uses raw script bytes for the contract input’s unlocking script (nonce + preimage halves) built by
build_mint_scriptsig(). The contract script is a covenant, not a P2PKH, so standardTransaction.sign()is not appropriate. The caller must either set the unlocking script directly or use a custom signing path. See docstring ofbuild_dmint_mint_tx()for details.- __init__(tx, updated_state, contract_script, reward_script, fee)¶
- class pyrxd.glyph.DmintState[source]¶
Bases:
objectParsed dMint contract state (from on-chain UTXO script).
Supports both V1 (the current Radiant mainnet format) and V2 (Photonic Wallet’s HEAD spec, not yet seen on mainnet). V1 has 6 state items; V2 has 10.
is_v1is True iff this state was parsed from V1 layout — in which casetarget_timeandlast_timeare not meaningful on-chain values and are set to 0;daa_modeis alwaysFIXEDfor V1 (the V1 contract template has no DAA bytecode).- __init__(height, contract_ref, token_ref, max_height, reward, algo, daa_mode, target_time, last_time, target, is_v1=False)¶
- classmethod from_script(script_bytes)[source]¶
Parse a dMint contract UTXO script into a
DmintState.Tries V2 layout first (10 state items), falls back to V1 (6 items + fingerprinted code epilogue). Raises
ValidationErrorif the script matches neither.- Parameters:
script_bytes (bytes) – Raw script bytes from a dMint contract UTXO output.
- Raises:
ValidationError – Script is malformed or matches neither V1 nor V2 layout.
- Return type:
- class pyrxd.glyph.DmintV1ContractInitialState[source]¶
Bases:
objectJust-deployed state of a V1 dMint contract template.
Carries exactly the parameters needed to reconstruct the initial (height=0) contract codescript for every contract of a given deploy. Used by
find_dmint_contract_utxos()’s fast path, where the caller already knows the deploy params.- Parameters:
num_contracts – Count of parallel contracts the deploy created (1..255 for V1; mainnet GLYPH used 32).
reward_sats – Photons emitted per successful mint (must fit in 3 bytes — V1 protocol constant).
max_height – Maximum mints per contract (3-byte ceiling).
target – 8-byte SHA256d PoW target.
algo – PoW algorithm. Defaults to
DmintAlgo.SHA256D, which is the only algorithm seen on V1 mainnet.
- __init__(num_contracts, reward_sats, max_height, target, algo=DmintAlgo.SHA256D)¶
- class pyrxd.glyph.DmintV1DeployParams[source]¶
Bases:
objectParameters for a V1 dMint deploy (2-tx: commit + reveal).
V1 is the only dMint format on Radiant mainnet today. Unlike V2 (which uses a separate deploy tx with a reward pool), V1 emits
num_contractsparallel singleton contract UTXOs directly in the reveal — each is the full state+epilogue codescript at height=0. Mining works by spending a contract UTXO and re-creating it at height+1 with the same script template; the reward is paid from a miner-supplied funding input.See
docs/dmint-research-photonic-deploy.mdfor the byte-by-byte chain shape this dataclass drives. Live mainnet example: Radiant Glyph Protocol (GLYPH) at commit a443d9df…878b → reveal b965b32d…9dd6.- Parameters:
metadata –
GlyphMetadatafor the token. Must include protocol[GlyphProtocol.FT, GlyphProtocol.DMINT]([1, 4]) and NOT include avversion field (V2 usesv; V1 omits it).owner_pkh – 20-byte PKH of the key that signs commit and all ref-seed P2PKH inputs in the reveal.
num_contracts – Count of parallel V1 dMint contract UTXOs to emit. Total supply =
reward_photons * max_height * num_contracts. Validated to[1, 250]at construction. The 250 ceiling is the standardness limit for tx size at typical V1 contract bytes (≈ 241 bytes/contract output + overhead → 250 contracts fits in a ~64 KB reveal before the embedded media body).max_height – Maximum mints per contract (3-byte ceiling).
reward_photons – Photons paid per successful mint (3-byte ceiling — see V1 contract state layout).
difficulty – Initial PoW difficulty (1 = easiest). Translated to 8-byte target via
difficulty_to_target().premine_amount – Photons to send to
owner_pkhon the reveal tx as an optional premine FT output.None= no premine. Filed as deferred work in M2 (docs/dmint-research-photonic-deploy.md §7.2); accepted in the dataclass but rejected at build time for now.op_return_msg – Optional OP_RETURN data carrier (raw bytes after the 0x6a prefix).
None= no OP_RETURN output.algo – PoW algorithm. Defaults to
DmintAlgo.SHA256D(the only algorithm on V1 mainnet today).
- __init__(metadata, owner_pkh, num_contracts, max_height, reward_photons, difficulty, premine_amount=None, op_return_msg=None, algo=DmintAlgo.SHA256D)¶
- metadata: GlyphMetadata¶
- class pyrxd.glyph.DmintV1DeployResult[source]¶
Bases:
objectOutput of
GlyphBuilder.prepare_dmint_deploy()for V1 deploys.Carries everything the caller needs to broadcast a V1 deploy: the commit-tx script + CBOR body, plus a deferred-builder method that produces the reveal-tx outputs once the commit confirms.
V1 differs from V2 in that there is no separate deploy tx — the reveal directly creates the parallel contract UTXOs. So this result has no
deploy_params_template/initial_pool_photons/placeholder_contract_scriptfields; instead it carriesplaceholder_contract_scripts(one per parallel contract) for fee estimation before the commit txid is known.- Parameters:
commit_result –
CommitResult— commit-tx script + fee. Same shape as the V2 result’s field.cbor_bytes – Encoded CBOR token body.
owner_pkh – 20-byte PKH of the deploy key.
premine_amount – Photons for optional premine output, or
None. Deferred work in M2 — must beNone.num_contracts – Count of parallel V1 contracts.
placeholder_contract_scripts – Tuple of N contract scripts built with the placeholder commit txid (00…00). Each is the same byte length as the final contract script — the only difference is the
contractRef/tokenReftxid component. Use the length for fee estimation.max_height – Echoed from params for
build_reveal_outputsaccess.reward_photons – Echoed from params.
difficulty – Echoed from params.
algo – Echoed from params.
op_return_msg – Echoed from params.
- __init__(commit_result, cbor_bytes, owner_pkh, premine_amount, num_contracts, placeholder_contract_scripts, max_height, reward_photons, difficulty, algo, op_return_msg)¶
- build_reveal_outputs(commit_txid)[source]¶
Build reveal-tx output scripts given the confirmed commit txid.
The V1 reveal: * spends commit vouts 0 + 1..N + (N+1 NFT-commit) + (N+2 change) * emits N parallel dMint contract UTXOs at vouts 0..N-1 * emits the FT NFT singleton + auth NFT singleton + change
The method name is
build_reveal_outputs(notbuild_reveal_scriptsas in V2) because V1’s reveal directly creates the output contract UTXOs — there is no separate deploy tx. The arity also differs from V2’s (no commit_vout / commit_value needed: V1 input values are protocol constants). Distinct names prevent silent polymorphic-call TypeErrors.- Parameters:
commit_txid (str) – txid of the confirmed commit tx.
- Returns:
DmintV1RevealScriptsready to be placed into the reveal tx’s outputs.- Return type:
- commit_result: CommitResult¶
- class pyrxd.glyph.DmintV1RevealScripts[source]¶
Bases:
objectOutput scripts for the V1 dMint deploy reveal tx.
Mirrors the shape of
FtDeployRevealScripts(a flat locking-script + scriptsig-suffix bag), but with V1’s distinctive multi-output structure: N contract scripts + optional premine FT + optional OP_RETURN. The caller composes these into a transaction in declared order, signs each input, and broadcasts.- Parameters:
contract_scripts – Tuple of full V1 dMint contract output scripts (state + epilogue), one per parallel contract. Length equals the deploy’s
num_contracts. Each is the 241-byte layout at height=0 withcontractRef[i] = (commit_txid, i+1)andtokenRef = (commit_txid, 0).contract_value – Photons per contract output. Always 1 (V1 contracts are singletons — the photon value stays at 1 as the contract advances).
cbor_bytes – Encoded CBOR token body. Caller pushes this in the reveal’s vin[0] scriptSig (after sig + pubkey), preceded by the
glymagic bytes push.scriptsig_suffix – The push sequence
<gly> <CBOR>ready to append after<sig> <pubkey>for vin[0]. Mirrors theFtDeployRevealScripts.scriptsig_suffixconvention.premine_script – Locking script for an optional premine FT output (
None= no premine). Deferred work in M2 — the builder currently raises ifpremine_amountis set.premine_amount – Photons for the premine output (
Noneif no premine).op_return_script – Locking script for an optional OP_RETURN data carrier (
Noneif no OP_RETURN).
- __init__(contract_scripts, contract_value, cbor_bytes, scriptsig_suffix, premine_script, premine_amount, op_return_script)¶
- class pyrxd.glyph.DmintV2DeployParams[source]¶
Bases:
objectParameters for a V2 dMint token deploy (2-tx: commit + reveal).
Mirrors
DmintV1DeployParams. V2 emitsnum_contractsparallel 1-photon singleton contract UTXOs directly in the reveal —contractRef[i] = commit:(i+1),tokenRef = commit:0— exactly like V1. The only consensus differences are the V2 contract bytecode (10-item state + the V2 covenant) and the 8-byte mint nonce; the reward + tx fee for each mint come from a miner-supplied funding input, not a pool.All five
DaaModevalues are supported — FIXED, ASERT, LWMA, EPOCH, and SCHEDULE — and the redesigned covenant advancestarget/last_timeon-chain, byte-matched to canonical PhotonicdMintScript(incl. the EPOCH/LWMA int64-overflow fix, Radiant-Core/Photonic-Wallet#2). See #219.V2 is consensus-proven on regtest + mainnet but still pre-external-audit;
prepare_dmint_deploydeploys V2 by default as of 0.9.0 (allow_v2_deploydefaults toTrueand is retained only for backward-compatibility).- Parameters:
metadata –
GlyphMetadata(must includeGlyphProtocol.FTandGlyphProtocol.DMINT; setversion=2so indexers classify it as V2).owner_pkh – 20-byte PKH of the key that signs commit + the ref-seed reveal inputs.
num_contracts – Count of parallel V2 contract UTXOs (
[1, 250]).max_height – Maximum mints per contract.
reward_photons – Photons paid per successful mint.
difficulty – Initial PoW difficulty (1 = easiest).
premine_amount – Deferred (mirrors V1) — must be
None.op_return_msg – Optional OP_RETURN data carrier (raw bytes after 0x6a).
algo – PoW algorithm (default SHA256d; only SHA256D is mined).
daa_mode – Must be
DaaMode.FIXED(the only mintable mode).target_time – Echoed into the state (DAA-only; vestigial for FIXED).
half_life – Echoed into the code (DAA-only; vestigial for FIXED).
- __init__(metadata, owner_pkh, num_contracts, max_height, reward_photons, difficulty, premine_amount=None, op_return_msg=None, algo=DmintAlgo.SHA256D, daa_mode=DaaMode.FIXED, target_time=60, half_life=3600, epoch_length=2016, max_adjustment_log2=2, schedule=())¶
- Parameters:
- Return type:
None
- metadata: GlyphMetadata¶
- class pyrxd.glyph.DmintV2DeployResult[source]¶
Bases:
objectOutput of
GlyphBuilder.prepare_dmint_deploy()for V2 deploys.Mirrors
DmintV1DeployResult: V2 emitsnum_contractsparallel 1-photon singleton contract UTXOs directly in the reveal (no separate deploy tx, no reward pool). Callbuild_reveal_outputs()once the commit confirms to get the reveal-tx output scripts.- Parameters:
commit_result –
CommitResult— commit-tx script + fee.cbor_bytes – Encoded CBOR token body.
owner_pkh – 20-byte PKH of the deploy key.
premine_amount – Deferred — must be
None(mirrors V1).num_contracts – Count of parallel V2 contracts.
placeholder_contract_scripts – Tuple of N V2 contract scripts built with the placeholder commit txid (00…00) — same byte length as the final scripts, for fee estimation before the commit txid is known.
- :param max_height, reward_photons, difficulty, algo, op_return_msg, daa_mode,
target_time, half_life: Echoed from params for
build_reveal_outputs().
- __init__(commit_result, cbor_bytes, owner_pkh, premine_amount, num_contracts, placeholder_contract_scripts, max_height, reward_photons, difficulty, algo, op_return_msg, daa_mode, target_time, half_life, epoch_length=2016, max_adjustment_log2=2, schedule=())¶
- Parameters:
commit_result (CommitResult)
cbor_bytes (bytes)
owner_pkh (Hex20)
premine_amount (int | None)
num_contracts (int)
max_height (int)
reward_photons (int)
difficulty (int)
algo (DmintAlgo)
op_return_msg (bytes | None)
daa_mode (DaaMode)
target_time (int)
half_life (int)
epoch_length (int)
max_adjustment_log2 (int)
- Return type:
None
- build_reveal_outputs(commit_txid)[source]¶
Build reveal-tx output scripts given the confirmed commit txid.
Mirrors
DmintV1DeployResult.build_reveal_outputs(): emitsnum_contractsparallel 1-photon V2 contract UTXOs (contractRef[i] = commit:(i+1),tokenRef = commit:0) + thegly/CBOR reveal scriptSig suffix + optional OP_RETURN. The returnedDmintV1RevealScriptsbag has the same shape for V1 and V2.- Parameters:
commit_txid (str)
- Return type:
- commit_result: CommitResult¶
- class pyrxd.glyph.FtTransferParams[source]¶
Bases:
objectParameters for an FT transfer transaction.
- Parameters:
ref – the
GlyphRefidentifying the tokenutxos – list of
FtUtxoavailable to spendamount – FT units to send to
new_owner_pkhnew_owner_pkh – recipient’s 20-byte PKH
private_key – sender’s
pyrxd.keys.PrivateKeyfee_rate – photons/byte (Radiant post-V2 minimum is 10_000)
change_pkh – FT-change recipient PKH. Defaults to the sender’s PKH when
None.
- __init__(ref, utxos, amount, new_owner_pkh, private_key, fee_rate=10000, change_pkh=None)¶
- class pyrxd.glyph.FtTransferResult[source]¶
Bases:
objectOutput of
FtUtxoSet.build_transfer_tx().- Parameters:
tx – signed
Transaction, ready to broadcastnew_ft_script – locking script of the transfer (recipient) output
change_ft_script – locking script of the change output, or
Noneif the transfer was an exact matchref – the FT’s
GlyphReffee – fee paid in photons
- __init__(tx, new_ft_script, change_ft_script, ref, fee)¶
- class pyrxd.glyph.FtUtxo[source]¶
Bases:
objectA single UTXO holding some quantity of one FT.
- Parameters:
txid – txid of the UTXO
vout – output index within that tx
value – RXD value (photons) on the output
ft_amount – token units held on the output
ft_script – full FT locking script (75 bytes, see
pyrxd.glyph.script.build_ft_locking_script())
- __init__(txid, vout, value, ft_amount, ft_script)¶
- class pyrxd.glyph.FtUtxoSet[source]¶
Bases:
objectManages a set of FT UTXOs for a single token
ref.Responsibilities:
Total the FT amount across the set.
Select a minimum set of UTXOs to cover a requested transfer amount.
Build + sign a transfer tx (two-pass fee calculation) that respects conservation.
- build_transfer_tx(amount, new_owner_pkh, private_key, fee_rate=10000, change_pkh=None, dust_limit=546)[source]¶
Build a signed FT transfer transaction enforcing conservation.
The selected UTXOs are spent with a standard P2PKH scriptSig (same unlock as an NFT transfer — the FT script embeds a full P2PKH prefix before the
OP_PUSHINPUTREF/ conservation epilogue, so<sig> <pubkey>satisfies it). A transfer output locked tonew_owner_pkhis created foramounttoken units; any leftover token units flow to a change output locked tochange_pkh(or the sender’s PKH if omitted).Fee calculation uses the same two-pass pattern as
GlyphBuilder.build_nft_transfer_tx(): build a trial tx → sign → measure bytes → rebuild fresh (so the final signature commits to the final outputs, not the trial ones).- Parameters:
amount (int) – FT units to transfer to
new_owner_pkhnew_owner_pkh (Hex20) – recipient’s 20-byte PKH
private_key (Any) –
pyrxd.keys.PrivateKeyowning the inputsfee_rate (int) – photons/byte (default 10_000, the Radiant minimum)
change_pkh (Hex20 | None) – FT-change recipient PKH. Defaults to the sender’s PKH derived from
private_key.dust_limit (int) – minimum photon value per output (default 546)
- Raises:
ValueError –
amount <= 0; total FT < amount; total RXD from the selected inputs cannot coverdust_limit * n_outputs + fee.- Returns:
FtTransferResult(signed tx, scripts, fee, ref).- Return type:
- select(amount)[source]¶
Greedily select the minimum number of UTXOs covering
amount.Strategy: sort by
ft_amountdescending, take until covered.- Raises:
ValueError –
amountexceedstotal()(including the empty-set case, wheretotal == 0).- Parameters:
amount (int)
- Return type:
- class pyrxd.glyph.GlyphBuilder[source]¶
Bases:
objectBuild unsigned Glyph transactions.
Separate commit and reveal methods — caller is responsible for:
Signing the commit tx and broadcasting it.
Waiting for confirmation.
Passing the confirmed commit txid to the reveal method.
Signing the reveal tx (via
Transaction+PrivateKey).
Method selection guide (N9 — surface grew to 12 methods across 5 protocols)¶
Minting (commit → reveal)
Goal
Protocol tag(s)
Reveal method
Mint a singleton NFT
[NFT]Mint a plain FT
[FT]Mint a dMint FT
[FT, DMINT]prepare_dmint_deploy()(3 txs)Mint a mutable NFT
[NFT, MUT]Mint a collection
``[NFT,CONTAINER]`
Mint a WAVE name
[NFT,MUT,WAVE]For every token type the first step is the same: call
prepare_commit()(which derives the commit script from the metadata protocol list automatically). Only the reveal step differs.Transfers (no commit needed)
NFT transfer:
build_nft_transfer_tx()FT transfer:
build_ft_transfer_tx()(orFtUtxoSetinglyph/ft.py)
Low-level (rarely called directly)
prepare_reveal()— generic reveal;is_nftpicks singleton vs FT reftypebuild_reveal_scripts()— alternate reveal entry that returns scripts, not paramsbuild_transfer_locking_script()— bare FT lock without constructing a txbuild_contract_script()— MUT contract script for mutable NFT reveals
- build_ft_transfer_tx(params)[source]¶
Build a signed FT transfer transaction enforcing conservation.
Thin delegator to
FtUtxoSet.build_transfer_tx()— the real logic (selection, two-pass fee, conservation) lives there so the API surface is available both at the builder level and directly on a UTXO-set instance.- Parameters:
params (FtTransferParams) –
FtTransferParams— see dataclass docstring.- Returns:
FtTransferResult— signed tx + scripts + fee.- Raises:
ValueError – same conditions as
FtUtxoSet.build_transfer_tx()(insufficient FT balance, insufficient RXD for fee + dust).- Return type:
- build_nft_transfer_tx(params)[source]¶
Build a signed NFT transfer transaction.
Spends an existing NFT UTXO (standard P2PKH scriptSig unlock: <sig> <pubkey>) and creates a new NFT output locked to
new_owner_pkh. The 36-byte ref is preserved across the transfer — it’s extracted from the input’s NFT script and written into the new output’s NFT script unchanged.Fee calculation is two-pass: build a trial tx, sign it to measure actual serialised size, then rebuild with the final value = input_value - size*fee_rate. The trial signature is discarded (reset unlocking_script = None before final sign) so the final tx carries a signature over the final outputs, not the trial ones.
- Parameters:
params (TransferParams) – TransferParams — see dataclass docstring
- Returns:
TransferResult — signed tx, new locking script, ref, fee
- Raises:
ValidationError – nft_script is not a valid 63-byte NFT script
ValueError – nft_utxo_value - fee < 546 (dust limit)
- Return type:
TransferResult
- build_transfer_locking_script(ref, new_owner_pkh, is_nft)[source]¶
Build the locking script for a transfer output.
- prepare_commit(params)[source]¶
Prepare the commit transaction parameters.
Returns the commit locking script + CBOR bytes + estimated fee. Caller must build, sign, and broadcast the actual transaction.
The commit script’s
OP_REFTYPE_OUTPUTcheck is derived frommetadata.protocol: NFT (2in protocol) produces anOP_2/SINGLETON-expecting commit; any other protocol mix (FT, dMint FT, data, etc.) produces anOP_1/NORMAL-expecting commit. This means the caller does not hand-pick refType — the metadata drives it. Prior versions forced every commit to NFT shape; seebuild_commit_locking_scriptfor the fix note.- Parameters:
params (CommitParams)
- Return type:
CommitResult
- prepare_container_reveal(commit_txid, commit_vout, cbor_bytes, owner_pkh, child_ref=None)[source]¶
Prepare scripts for a CONTAINER reveal.
A container is an NFT with an additional
OP_PUSHINPUTREF <child_ref>prefix that links it to a child token ref. Whenchild_refisNonethe container is created empty (no child ref in locking script).Protocol field must include
GlyphProtocol.CONTAINER(7).
- prepare_dmint_deploy(params, *, allow_v2_deploy=True)[source]¶
Prepare a dMint token deploy.
Dispatches on the type of
params:DmintV1DeployParams→ returnsDmintV1DeployResult. V1 is the only format on Radiant mainnet today (see GLYPH at a443d9df…878b). Two-tx deploy: commit + reveal (the reveal directly createsparams.num_contractsparallel contract UTXOs).DmintV2DeployParams→ returnsDmintV2DeployResult. V2 is consensus-proven on regtest + mainnet (#219) and now deploys by default (allow_v2_deploy=True). A softUserWarningis emitted if the caller explicitly passesallow_v2_deploy=Falseso the historical opt-out path stays observable without blocking.
- Parameters:
params (DmintV1DeployParams | DmintV2DeployParams) – Either
DmintV1DeployParams(V1 deploy) orDmintV2DeployParams(V2 deploy). The deprecatedDmintFullDeployParamsis accepted (it’s a subclass ofDmintV2DeployParams) but emits aDeprecationWarningat construction time.allow_v2_deploy (bool) – Retained for backward-compatibility; defaults to
True(V2 deploys by default). Ignored for V1.
- Returns:
V1 or V2 result, matching the param type via
@overload.- Raises:
ValidationError – Various per-version invariants — see
_prepare_dmint_v1_deploy()and the V2 implementation below for specifics.- Return type:
- prepare_ft_deploy_reveal(commit_txid, commit_vout, commit_value, cbor_bytes, premine_pkh, premine_amount)[source]¶
Prepare reveal scripts + premine amount for an FT deploy.
Thin convenience wrapper around
prepare_reveal()for the FT-deploy-with-premine flow: the reveal produces one FT output carrying the full issued supply topremine_pkh, and its outpoint becomes the permanent token ref.Caller still constructs the actual transaction. The returned
premine_amountis whatvout[0].valuemust be on the reveal tx — typically the full supply for a premine-only deploy (no covenant UTXO). Radiant FT convention: 1 photon = 1 FT unit, sopremine_amountis the supply in whole units.No dMint-specific logic here. The
cbor_bytesalready encode whatever protocol markers the caller chose — dMint FT ([1,4]), plain FT ([1]), or any other combination — viaGlyphMetadata. pyrxd treats the protocol markers as caller-owned; classification happens at the indexer layer.
- prepare_mutable_reveal(commit_txid, commit_vout, cbor_bytes, owner_pkh)[source]¶
Prepare scripts for a MUT (mutable NFT) reveal.
Returns the two output locking scripts the caller must place in the reveal tx: -
nft_script: 63-byte NFT singleton (token the owner holds) -contract_script: 174-byte mutable contract UTXO (holds state)The reveal scriptSig suffix is also returned; the caller prepends
<sig> <pubkey>to form the full scriptSig.Protocol field in
cbor_bytesmust includeGlyphProtocol.MUT(5). UseGlyphMetadata(protocol=[GlyphProtocol.NFT, GlyphProtocol.MUT]).- Parameters:
- Return type:
- prepare_reveal(params)[source]¶
Prepare the reveal transaction scripts.
Returns locking script + scriptSig suffix. Caller must build, sign, and broadcast the actual transaction.
- Parameters:
params (RevealParams)
- Return type:
RevealScripts
- prepare_wave_reveal(commit_txid, commit_vout, cbor_bytes, owner_pkh, name)[source]¶
Prepare scripts for a WAVE (on-chain naming) reveal.
WAVE extends MUT with a
namefield in the CBOR payload. Protocol field must includeGlyphProtocol.WAVE(11).namemust be non-empty printable ASCII, max 255 characters. The name is validated here but must already be embedded incbor_bytesby the caller via eitherattrs["name"](the Photonic-compatible canonical shape — required for resolution against RXinDexer and other indexers) or top-levelname(legacy pyrxd shape, accepted for backwards compatibility but not indexer-visible).Photonic-compatible CBOR shape (canonical, see Photonic Wallet
packages/lib/src/wave.ts):{ "p": [2, 5, 11], "attrs": { "name": "alice.rxd", "domain": "rxd", "target": "<radiant_address>", "target_type": "address" } }
Use
build_wave_attrs()(orpyrxd.glyph.wave.build_wave_metadata()) to construct the canonical shape; passing a top-levelnamefield still works but emits a token RXinDexer will not index.Protocol requirement:
[NFT(2), MUT(5), WAVE(11)].
- class pyrxd.glyph.GlyphCreator[source]¶
Bases:
objectCreator identity and optional ECDSA signature over the metadata commit hash.
pubkey: 33-byte compressed secp256k1 pubkey, hex-encoded. sig: DER-encoded ECDSA signature, hex-encoded (empty string = unsigned). algo: Signing algorithm identifier string.
- __init__(pubkey, sig='', algo='ecdsa-secp256k1')¶
- class pyrxd.glyph.GlyphFt[source]¶
Bases:
objectA minted or transferable FT Glyph.
- __init__(ref, owner_pkh, amount, metadata)¶
- Parameters:
ref (GlyphRef)
owner_pkh (Hex20)
amount (int)
metadata (GlyphMetadata)
- Return type:
None
- metadata: GlyphMetadata¶
- class pyrxd.glyph.GlyphInspector[source]¶
Bases:
objectParse raw transaction bytes to find Glyph outputs. Pure — no network access.
- extract_reveal_metadata(scriptsig)[source]¶
Parse a reveal TX scriptSig to extract CBOR metadata.
scriptSig format:
<sig> <pubkey> <"gly"> <CBOR>. ReturnsNoneif this is not a reveal scriptSig (or if the CBOR is malformed / unrecognised).Catches
Exceptionbroadly because every call site here crosses a trust boundary: scriptSigs from network-fetched txs are attacker- controlled, and the CBOR decoder + push-data walker may raise anything fromValidationErrortocbor2.CBORDecodeErrortoIndexErroron truncated input. ReturningNoneis the contract callers expect.- Parameters:
scriptsig (bytes)
- Return type:
GlyphMetadata | None
- find_glyphs(tx_outputs)[source]¶
Given list of (satoshis, script_bytes) outputs, return detected Glyphs.
Detects NFT singletons, FT locks, mutable NFTs, and dMint contract outputs. Plain P2PKH and unrecognised scripts are silently skipped. Commit-output classification lives outside
find_glyphsbecause a commit has no meaningfulrefuntil its reveal lands.
- find_reveal_metadata(scriptsigs)[source]¶
Walk every input scriptSig and return the first reveal metadata found.
Returns
(input_index, metadata)for the first input whose scriptSig embeds aglymarker followed by parseable CBOR;Noneif no input does. Distinct fromextract_reveal_metadata()(which checks a single scriptSig) — diagnostic callers want to know which input carried the metadata, and that the inspector looked beyond input 0.- Parameters:
- Return type:
tuple[int, GlyphMetadata] | None
- parse_mint_scriptsig(scriptsig)[source]¶
Decode a dMint mint-claim scriptSig into its 4 canonical pushes.
A V1/V2 dMint mint claim spends the contract UTXO with a scriptSig of the form:
V1 (nonce_width=4): <0x04 nonce(4)> <0x20 inputHash(32)> <0x20 outputHash(32)> <OP_0> → 72 bytes V2 (nonce_width=8): <0x08 nonce(8)> <0x20 inputHash(32)> <0x20 outputHash(32)> <OP_0> → 76 bytes
Where:
nonce— little-endian PoW nonce found by the miner.inputHash—SHA256d(funding_input_locking_script). NOT a preimage half; the on-chain covenant recomputesSHA256(inputHash || outputHash)from these literal pushes.outputHash—SHA256d(OP_RETURN_msg_script at vout[2]).OP_0— the sentinel push the V1/V2 covenant requires.
Verified against mainnet V1 mint
146a4d68…f3cand the V1 mintc9fdcd34…e530.Returns a dict with
nonce_hex,input_hash,output_hash,version_hint("v1"|"v2"|None), andscriptsig_length— orNoneif the scriptSig doesn’t match the canonical 4-push shape.Catches
Exceptionbroadly because every call site crosses a trust boundary: scriptSigs from network-fetched txs are attacker- controlled. Non-mint inputs (P2PKH funding inputs, plain RXD spends, etc.) returnNone.
- class pyrxd.glyph.GlyphMetadata[source]¶
Bases:
objectCBOR payload for a Glyph token.
- __init__(protocol, name='', ticker='', description='', token_type='', main=None, attrs=<factory>, loc='', loc_hash='', decimals=0, image_url='', image_ipfs='', image_sha256='', v=None, dmint_params=None, creator=None, royalty=None, policy=None, rights=None, created='', commit_outpoint='')¶
- Parameters:
name (str)
ticker (str)
description (str)
token_type (str)
main (GlyphMedia | None)
loc (str)
loc_hash (str)
decimals (int)
image_url (str)
image_ipfs (str)
image_sha256 (str)
v (int | None)
dmint_params (DmintCborPayload | None)
creator (GlyphCreator | None)
royalty (GlyphRoyalty | None)
policy (GlyphPolicy | None)
rights (GlyphRights | None)
created (str)
commit_outpoint (str)
- Return type:
None
- creator: GlyphCreator | None = None¶
- dmint_params: DmintCborPayload | None = None¶
- classmethod for_dmint_ft(ticker, name, decimals=0, description='', image_url='', image_ipfs='', image_sha256='', protocol=None, dmint_params=None)[source]¶
Construct GlyphMetadata for a dMint-marked FT deploy.
Pass
dmint_params(aDmintCborPayload) to embed the dMint configuration object in the token metadata. Indexers and wallets use this to display mining parameters without parsing the contract script.Sets
v=2automatically whendmint_paramsis provided.
- policy: GlyphPolicy | None = None¶
- rights: GlyphRights | None = None¶
- royalty: GlyphRoyalty | None = None¶
- class pyrxd.glyph.GlyphNft[source]¶
Bases:
objectA minted or transferable NFT Glyph.
- __init__(ref, owner_pkh, metadata)¶
- Parameters:
ref (GlyphRef)
owner_pkh (Hex20)
metadata (GlyphMetadata)
- Return type:
None
- metadata: GlyphMetadata¶
- class pyrxd.glyph.GlyphPolicy[source]¶
Bases:
objectToken behaviour policy flags.
- __init__(renderable=None, executable=None, nsfw=None, transferable=None)¶
- class pyrxd.glyph.GlyphProtocol[source]¶
Bases:
IntEnum- __new__(value)¶
- FT = 1¶
- NFT = 2¶
- DAT = 3¶
- DMINT = 4¶
- MUT = 5¶
- BURN = 6¶
- CONTAINER = 7¶
- ENCRYPTED = 8¶
- TIMELOCK = 9¶
- AUTHORITY = 10¶
- WAVE = 11¶
- class pyrxd.glyph.GlyphRef[source]¶
Bases:
object36-byte Glyph reference: txid (reversed LE) + vout (4-byte LE).
- classmethod from_contract_hex(contract_hex)[source]¶
Parse a 72-char contract id string as displayed in Radiant explorers.
The Glyph contract id concatenates the display-order txid (64 hex chars) with the big-endian-encoded vout (8 hex chars). Both halves are written in human-readable order so the whole string reads naturally — the trailing
00000004decodes to4:b45dc453befb589a...c380eb31deaf96a2a8 00000004 └────────── txid (display order) ───┘ └─ vout BE ─┘ (= 4)
Equivalent forms:
from_contract_hex("b45dc4...a2a800000004")GlyphRef(txid=Txid("b45dc4...a2a8"), vout=4)
Warning
This is the explorer / UI display form, not the on-chain wire form.
from_bytes()parses the wire form used inside locking scripts, where the txid bytes are reversed and the vout is encoded little-endian. If you have raw bytes pulled out of a script, usefrom_bytes(). Use this method only when you have a contract id in the form a Radiant explorer or wallet UI shows it. Mixing them will silently produce a wrong-vout ref.
- class pyrxd.glyph.GlyphRights[source]¶
Bases:
objectLicensing and attribution information.
- __init__(license='', terms='', attribution='')¶
- class pyrxd.glyph.GlyphRoyalty[source]¶
Bases:
objectOn-chain royalty hint for secondary-market wallets.
bps: Basis points (100 = 1%, 500 = 5%, max 10000 = 100%). address: Radiant address to receive royalty payments. enforced: Whether wallets should enforce this royalty. minimum: Minimum royalty amount in photons (0 = no minimum). splits: Optional list of (address, bps) pairs for royalty splitting.
The sum of split bps should equal the top-level bps.
- __init__(bps, address, enforced=False, minimum=0, splits=<factory>)¶
- class pyrxd.glyph.GlyphScanner[source]¶
Bases:
objectScan a Radiant address or script_hash for Glyph outputs.
- Parameters:
client – An already-connected ElectrumXClient. The scanner does not own the connection lifecycle; callers should use the client as a context manager and pass it in.
- __init__(client)[source]¶
- Parameters:
client (ElectrumXClient)
- Return type:
None
- async scan_script_hash(script_hash)[source]¶
Return all Glyph outputs for script_hash.
Fetches UTXOs, raw transactions, and (where available) reveal transaction metadata, then constructs typed GlyphNft / GlyphFt objects.
Concurrency: UTXO raw-tx fetches and reveal-metadata fetches both run in parallel via
asyncio.gather. Pre-fix (closes ultrareview re-review N17) the reveal-metadata path was inside the per-utxo loop and serialised one round-trip per glyph; for a 100-glyph wallet that meant ~100x the latency of the now- batched version.
- class pyrxd.glyph.MutableRevealScripts[source]¶
Bases:
objectScripts for a MUT reveal — two outputs required.
- __init__(ref, nft_script, contract_script, scriptsig_suffix, payload_hash)¶
- class pyrxd.glyph.PowPreimageResult[source]¶
Bases:
objectThe 64-byte PoW preimage plus the two script hashes a miner must push.
The covenant binds the PoW hash AND the scriptSig pushes together: it recomputes
H2 = SHA256(scriptSig_inputHash || scriptSig_outputHash)and folds that into the same hash the miner solved. Diverging the preimage from the scriptSig pushes is a silent on-chain rejection — seedocs/solutions/runtime-errors/dmint-v1-mint-scriptsig-shape.mdfor the prior incident that motivated returning all three values from a single helper.- Parameters:
preimage – 64-byte SHA256d PoW preimage; feeds
mine_solution.input_hash –
SHA256d(input_script)— push asscriptSig_inputHash.output_hash –
SHA256d(output_script)— push asscriptSig_outputHash.
- __init__(preimage, input_hash, output_hash)¶
- class pyrxd.glyph.RxinDexerClient[source]¶
Bases:
objectThin wrapper over
ElectrumXClientfor RXinDexer extension RPCs.Methods are grouped by RPC namespace (
wave_*,glyph_*,swap_*). Each wraps a single RPC call, parses the response into a typed result, and converts transport / parse failures intoRxinDexerErrorsubclasses.The
pyrxd.glyph.wave.WaveResolveris built on top of this client and is the canonical entry-point for WAVE name resolution in higher-level applications.- __init__(client)[source]¶
- Parameters:
client (ElectrumXClient)
- async glyph_get_balance(address, token_ref=None)[source]¶
glyph.get_balance— fungible-token balance for an address.Pass token_ref to scope the query to a specific token; without it, the indexer returns all FT balances the address holds.
- async wave_resolve(name)[source]¶
Raw
wave.resolvecall. Returns the indexer’s dict response, orNoneif the name is not registered. Higher-level callers should usually usepyrxd.glyph.wave.WaveResolver.
- exception pyrxd.glyph.RxinDexerError[source]¶
Bases:
ExceptionBase class for RXinDexer-specific errors.
- exception pyrxd.glyph.RxinDexerNotFound[source]¶
Bases:
RxinDexerErrorA lookup returned no result (name not registered, token unknown, etc.).
- exception pyrxd.glyph.V2UnvalidatedWarning[source]¶
Bases:
UserWarningRetained warning category for V2 dMint code paths.
HISTORY: V2 dMint was once quarantined behind this warning because it had never been exercised against live consensus. That is no longer true — the canonical-Photonic V2 redesign is byte-matched to upstream and consensus- validated on radiant-core v3.1.1 regtest AND Radiant mainnet (3.1.2): the first V2 dMint deploy + PoW mint confirmed on mainnet (deploy
95335028…bb16fb09, mint1239f64a…e0cd6c67; #219). The per-call warning is therefore no longer emitted.The class is kept (not deleted) so any downstream
warnings.simplefilter(…, V2UnvalidatedWarning)filters remain importable. V2 is still pre-external- audit — that caveat lives in the README / threat-model, the same level as V1, not in a per-call warning.
- class pyrxd.glyph.WaveAttrs[source]¶
Bases:
objectParsed WAVE attrs dict, mirroring the on-chain Photonic shape.
- __init__(name, domain, target, target_type='address')¶
- exception pyrxd.glyph.WaveNameNotFound[source]¶
Bases:
WaveResolverErrorRaised when the requested name does not exist in the indexer.
- class pyrxd.glyph.WaveRecord[source]¶
Bases:
objectA full WAVE registration, as returned by
wave.resolve.The exact response shape from RXinDexer is documented at https://github.com/Radiant-Core/RXinDexer; this class normalizes the minimum fields a swap coordinator needs.
- __init__(name, target, target_type, claim_txid, block_height)¶
- class pyrxd.glyph.WaveResolver[source]¶
Bases:
objectHigh-level WAVE name resolver — composes
RxinDexerClient.Accepts either an
ElectrumXClient(auto-wraps inRxinDexerClient) or an existingRxinDexerClient. The latter is preferred when you have other indexer use cases (Glyph metadata lookups, Swap state, etc.) so the same client is shared.All methods raise
WaveResolverError(a subclass ofRxinDexerError) on transport / parse failures. Name-not-found raisesWaveNameNotFoundso callers can distinguish “does not exist” from “indexer is down”.- __init__(client)[source]¶
- Parameters:
client (ElectrumXClient | RxinDexerClient)
- async resolve(name)[source]¶
Look up a qualified WAVE name (e.g.
"alice.rxd").Raises
WaveNameNotFoundif the name is not registered. RaisesWaveResolverErroron transport / parse failures.- Parameters:
name (str)
- Return type:
- exception pyrxd.glyph.WaveResolverError¶
Bases:
RxinDexerErrorRaised when a WAVE name resolution call fails for any reason. Subclass of RxinDexerError — catch either to handle indexer failures.
- pyrxd.glyph.build_dmint_code_script(params)[source]¶
Build the V2 dMint code bytecode (Part A + powHashOp + Part B + Part C).
- Parameters:
params (DmintDeployParams)
- Return type:
- pyrxd.glyph.build_dmint_contract_script(params)[source]¶
Build the full V2 dMint output script: state + OP_STATESEPARATOR + code.
Byte-identical to the canonical Photonic
dMintScriptfor the same parameters (validated against golden vectors in tests/test_dmint_v2_canonical.py, and consensus-proven on regtest + mainnet).- Parameters:
params (DmintDeployParams)
- Return type:
- pyrxd.glyph.build_dmint_state_script(params)[source]¶
Build the 10-item V2 dMint state script (before OP_STATESEPARATOR).
Layout (canonical redesign §4.2):
height(minimal) | d8:contractRef(36B) | d0:tokenRef(36B) | maxHeight | reward | algoId | daaMode | targetTime | lastTime(4B LE) | target(minimal)
heightandtargetuse minimal pushes (variable width) so the state script is MINIMALDATA-compliant from height 0 / target MAX onward — the old fixed04 [LE4]height push was rejected by radiantd’s MINIMALDATA mempool policy on mainnet.lastTimestays a 4-byte push (Unix timestamps are always 4-byte minimal), which simplifies Part C’s04 || NUM2BIN(4, locktime)reconstruction.- Parameters:
params (DmintDeployParams)
- Return type:
- pyrxd.glyph.build_dmint_v1_ft_output_script(miner_pkh, token_ref)[source]¶
Build the 75-byte P2PKH-wrapped FT output that a V1 mint produces.
Layout (docs/dmint-research-mainnet.md §4 vout[1]):
76 a9 14 <pkh:20> OP_DUP OP_HASH160 PUSH20 pkh 88 ac OP_EQUALVERIFY OP_CHECKSIG (25-byte P2PKH prologue) bd OP_STATESEPARATOR d0 <tokenRef:36> OP_PUSHINPUTREF tokenRef (37 bytes) de c0 e9 aa 76 e3 12-byte covenant fingerprint (`_V1_FT_OUTPUT_EPILOGUE`) 78 e4 a2 69 e6 9d ────────────────────── Total: 75 bytes
This is the FT-bearing reward output — the V1 contract’s
OP_CODESCRIPTHASHVALUESUM_OUTPUTS OP_NUMEQUALVERIFYat epilogue offset 168 sums photons under this codescript and requires the total to equal the contract’srewardfield. Producing a plain P2PKH instead breaks FT conservation and the network rejects the mint.- Raises:
ValidationError –
miner_pkhis not 20 bytes.- Parameters:
- Return type:
- pyrxd.glyph.build_dmint_v1_mint_preimage(contract_utxo, funding_utxo, unsigned_tx)[source]¶
Build the V1 mining preimage AND scriptSig hashes for an unsigned mint tx.
The V1 covenant binds the PoW preimage to:
The contract input’s outpoint txid + the contract ref (so a nonce mined for one contract slot can’t be replayed against another)
The miner’s funding-input locking script (so the miner cannot substitute a different funding source after finding a nonce)
The OP_RETURN msg output script at vout[2] (Photonic’s mainnet-canonical layout; the covenant computes outputHash = SHA256d(this script))
Layout (matches
build_pow_preimage()):preimage = SHA256(txid_LE || contractRef) || SHA256(SHA256d(input_script) || SHA256d(output_script)) input_hash = SHA256d(input_script) ← scriptSig push output_hash = SHA256d(output_script) ← scriptSig pushCallers feed
preimagetomine_solution()and passinput_hash+output_hashtobuild_mint_scriptsig().- Parameters:
contract_utxo (DmintContractUtxo) – The V1 contract UTXO being spent.
funding_utxo (DmintMinerFundingUtxo) – The plain-RXD UTXO providing reward + fee.
unsigned_tx (Any) – The unsigned
Transactionfrombuild_dmint_mint_tx()— vout[2] is required to be the OP_RETURN msg output (mainnet-canonical 4-output shape).
- Returns:
PowPreimageResultcarrying the preimage and the two script hashes that the scriptSig must push for the covenant to accept the mint.- Raises:
ValidationError –
unsigned_txhas fewer than 4 outputs (no OP_RETURN at vout[2]) OR vout[2] is not actually an OP_RETURN script. Build the tx viabuild_dmint_mint_tx()with a non-emptyop_return_msg; skipping that produces a 3-output tx, and hand-building a 4-output tx with a different vout[2] would silently bind the preimage to wrong bytes (the on-chain covenant would then reject after a successful mine — wasting the mining work).- Return type:
- pyrxd.glyph.build_dmint_v2_mint_preimage(contract_utxo, funding_utxo, output_script)[source]¶
Build the V2 mining preimage AND scriptSig hashes.
V2 analog of
build_dmint_v1_mint_preimage(). The preimage shape (and the on-chain covenant’s H1/H2 binding logic) is identical to V1 — V2 inherits the output-validation block via_PART_C=_V1_EPILOGUE_SUFFIX[18:]. The only V1/V2 differences at the mint-tx level are the nonce width (8 bytes for V2 vs 4 for V1, a parameter ofbuild_mint_scriptsig()) and the absence of the Photonic-Walletop_return_msgconvention in V2.Layout (matches
build_pow_preimage()):preimage = SHA256(txid_LE || contractRef) || SHA256(SHA256d(input_script) || SHA256d(output_script)) input_hash = SHA256d(input_script) ← scriptSig push output_hash = SHA256d(output_script) ← scriptSig pushUnlike the V1 helper, this function takes
output_scriptas an explicit argument. V2 has no canonical “OP_RETURN msg at vout[2]” convention (that’s Photonic-Wallet’s V1 layout); the V2 covenant binds outputHash to whatever bytes the caller chooses to push. Callers selectingoutput_scriptshould pick one of the actual transaction outputs and document the binding in their own code.Note
This helper closes the audit’s security-H1 finding (no V2 analog of
build_dmint_v1_mint_preimageleft V2 callers one careless script-mismatch away from reproducing the M1 bug pattern). V2 is consensus-proven on regtest + mainnet (#219).- Parameters:
contract_utxo (DmintContractUtxo) – The V2 contract UTXO being spent. Its
state.is_v1MUST beFalse— passing a V1 UTXO is a bug.funding_utxo (DmintMinerFundingUtxo) – The plain-RXD UTXO providing reward + fee.
output_script (bytes) – The output-script bytes to bind into the preimage. V2 has no canonical convention; pick a transaction output the caller cares about (e.g. an OP_RETURN identifier, or the reward output’s locking script).
- Returns:
PowPreimageResultwith the preimage and the two script hashes the scriptSig must push.- Raises:
ValidationError – V1 contract UTXO passed by mistake, or an empty
output_script.- Return type:
- pyrxd.glyph.build_mint_scriptsig(nonce, input_hash, output_hash, *, nonce_width=8)[source]¶
Build the scriptSig a miner includes in the contract-spend input.
- Format (SHA256d):
V2 (nonce_width=8):
<0x08> <nonce:8B> <0x20> <inputHash:32B> <0x20> <outputHash:32B> <0x00>→ 76 bytes V1 (nonce_width=4):<0x04> <nonce:4B> <0x20> <inputHash:32B> <0x20> <outputHash:32B> <0x00>→ 72 bytes
The V1 layout is documented in docs/dmint-research-mainnet.md §4 (vin[0] of the mainnet mint trace at
146a4d68…f3c). Same shape as V2, differing only in nonce width and corresponding push opcode.The hashes pushed here MUST equal
PowPreimageResult.input_hashandoutput_hashfrom the samebuild_pow_preimage()call that produced the preimage the miner solved. The on-chain covenant recomputesSHA256(input_hash || output_hash)from these pushes and folds that into the PoW hash — diverging them silently produces amandatory-script-verify-flag-failedrejection after a successful mine.- Parameters:
nonce (bytes) – nonce_width-bytes nonce (found during mining).
input_hash (bytes) – 32-byte
SHA256d(input_script)fromPowPreimageResult.output_hash (bytes) – 32-byte
SHA256d(output_script)fromPowPreimageResult.nonce_width (Literal[4, 8]) – 4 for V1 contracts, 8 for V2. Keyword-only and
Literal[4, 8]so a stray positional value is a type error rather than a silent V1/V2 confusion. Default 8 preserves pre-V1-support behavior.
- Return type:
- pyrxd.glyph.build_mutable_nft_script(mutable_ref, payload_hash)[source]¶
Build the 175-byte mutable NFT output script.
- Layout: PUSH32 <payload_hash> OP_DROP OP_STATESEPARATOR
OP_PUSHINPUTREFSINGLETON <mutable_ref:36> <102-byte body>
- pyrxd.glyph.build_mutable_scriptsig(operation, cbor_bytes, contract_output_index, ref_hash_index, ref_index, token_output_index)[source]¶
Build the scriptSig for spending a mutable NFT contract input.
- The mutable NFT script expects the scriptSig stack (bottom→top):
gly_marker | cbor_payload | operation | contract_output_index | ref_hash_index | ref_index | token_output_index
- Parameters:
operation (Literal['mod', 'sl']) –
"mod"(modify — update payload hash) or"sl"(seal — burn the mutable contract).cbor_bytes (bytes) – CBOR-encoded metadata for the new state.
contract_output_index (int) – Output index of the mutable contract in the tx.
ref_hash_index (int) – Index into the refdatasummary for this token.
ref_index (int) – Index of the singleton ref in token output data.
token_output_index (int) – Output index of the token in the tx.
- Return type:
- pyrxd.glyph.build_pow_preimage(txid_le, contract_ref_bytes, input_script, output_script)[source]¶
Build the PoW preimage AND the two script hashes the scriptSig must push.
preimage[0..32] = SHA256(txid_LE || contractRef) preimage[32..64] = SHA256(SHA256d(inputScript) || SHA256d(outputScript))
The covenant pulls
inputHashandoutputHashfrom the scriptSig pushes (not from the preimage halves) and recomputes the second SHA256 on-chain. Returning all three values here forces callers to feed both sites from the same source — splitting the helper into “preimage builder” and “scriptSig builder” with independently-recomputed hashes is what produced the M1 covenant-rejection bug.- Parameters:
- Returns:
PowPreimageResultwithpreimage,input_hash,output_hash.- Return type:
- pyrxd.glyph.build_wave_metadata(*, qualified_name, target, target_type='address', description='')[source]¶
Construct a Photonic-compatible WAVE
GlyphMetadata.- Parameters:
qualified_name (str) – e.g.
"alice.rxd"— split into name + domain.target (str) – the address (or other identifier) the name resolves to.
target_type (str) –
"address"by default; other values are reserved for future schemas (e.g."cross_chain").description (str) – optional human-readable description; stored as top-level
descin CBOR (NOT insideattrs).
- Return type:
The returned metadata has protocol
[NFT, MUT, WAVE]and anattrsdict matching the Photonic on-chain shape — pass it throughencode_payload()and thenGlyphBuilder.prepare_wave_reveal()to construct the actual reveal transaction.The top-level
namefield onGlyphMetadatais intentionally left empty: validation inprepare_wave_revealprefersattrs.name, and emitting both would create ambiguity if they ever disagree.
- pyrxd.glyph.classify_glyph_metadata(metadata)[source]¶
Return the highest-specificity protocol classification for a metadata payload.
Examples
[NFT, MUT, WAVE]→"wave"(when attrs.name present)[NFT, MUT, WAVE]without attrs.name →"mut"(legacy, won’t resolve)[NFT, MUT, CONTAINER]→"container"[NFT, MUT]→"mut"[NFT, AUTHORITY]→"authority"[NFT, ENCRYPTED, TIMELOCK]→"timelock"[NFT, ENCRYPTED]→"encrypted"[NFT]→"nft"[FT, DMINT]→"dmint"[FT]→"ft"[DAT]→"dat"The string mirrors
GlyphOutput.glyph_typevalues where applicable, with extensions for the metadata-only types that scripts alone can’t distinguish (WAVE/CONTAINER/ENCRYPTED/TIMELOCK/AUTHORITY share script templates with MUT/NFT, and DAT is data-only).Ordering is highest-specificity-first: TIMELOCK is checked before ENCRYPTED (TIMELOCK requires ENCRYPTED per the protocol rules in
types, so a timelocked token always carries both).- Parameters:
metadata (GlyphMetadata)
- Return type:
- pyrxd.glyph.compute_next_target_asert(current_target, last_time, current_time, target_time, half_life)[source]¶
Compute next ASERT-lite target (mirrors the redesigned on-chain bytecode).
The redesign replaced OP_LSHIFT/OP_RSHIFT (which Radiant evaluates as a big-endian bit-string shift — wrong on the LE target encoding) with an unrolled 4-step OP_2MUL/OP_2DIV loop with a per-step overflow cap:
drift = trunc((current_time - last_time - target_time) / half_life) # clamp [-4,+4] drift > 0: repeat |drift|x: target = MAX_TARGET if target > MAX/2 else target*2 drift < 0: repeat |drift|x: target = target // 2 minimum target is 1
The per-step cap matches the miner’s
newTarget = min(MAX, oldTarget<<drift)clamp-at-MAX semantics (a naivetarget << driftwould overshoot MAX).Note
V2-only DAA. V1 has no DAA (fixed difficulty).
- pyrxd.glyph.compute_next_target_linear(current_target, last_time, current_time, target_time)[source]¶
Compute next linear/LWMA target (mirrors the redesigned on-chain bytecode).
Divide-first with caps so the on-chain OP_MUL never overflows int64:
timeDelta_capped = max(0, min(current_time - last_time, 4 * target_time)) target_capped = min(current_target, MAX_TARGET // 4) new_target = min(MAX_TARGET, (target_capped // target_time) * timeDelta_capped) minimum target is 1
The MAX/4 target cap means LWMA contracts cannot have a difficulty floor below 4 (
target <= MAX_TARGET/4). The 0-floor ontimeDeltamirrors the on-chainOP_0 OP_MAX(Radiant-Core/Photonic-Wallet#2): a backwards-clock block (locktime earlier than the previous mint) gives a negative delta that would otherwise underflow the on-chain int64 multiply.Note
V2-only DAA.
- pyrxd.glyph.difficulty_to_target(difficulty, algo=DmintAlgo.SHA256D)[source]¶
Convert difficulty to PoW target.
- pyrxd.glyph.extract_wave_attrs(cbor_data)[source]¶
Pull
WaveAttrsout of a decoded CBOR payload, if present.Returns
Nonefor non-WAVE payloads or WAVE payloads using only the legacy top-levelnameshape (those exist on-chain but RXinDexer won’t index them).
- async pyrxd.glyph.find_dmint_contract_utxos(client, *, token_ref, initial_state=None, limit=None, min_confirmations=1)[source]¶
Discover live V1 dMint contract UTXOs for a given
token_ref.Two call shapes:
Fast path — pass
initial_state. The function rebuilds each contract’s expected initial codescript locally (contractRef[i] = (commit_txid, i+1),tokenRef = token_ref), computes its scripthash inline, and asks the server for the UTXO at that scripthash. Oneget_utxoscall per contract. Use this shape immediately after deploy to verify all N contracts went live, or any time the caller has the deploy params handy.Walk-from-reveal fallback — omit
initial_state. The function fetches the deploy commit, derives the FT-commit hashlock’s scripthash, queries history for the reveal txid, then fetches the reveal and extracts every fresh V1 contract output whosetokenRefmatches. Slower (3+ extra round-trips) but works on any live token where you only know thetoken_ref.
Both shapes apply the same security S2 cross-check: for each candidate UTXO returned, the source transaction is fetched and verified to have
txid()matching the server’stx_hash, and its output script byte-equal to the script the server claimed. Defends against a malicious or buggy ElectrumX serving altered bytes (mirrorsfind_dmint_funding_utxo()’s round-4 defense).The fallback path returns fresh contracts only — UTXOs that have been mined from at least once are skipped (their state advanced and their scripthash drifted; following the spend chain forward to locate the current head is filed as deferred work).
- Parameters:
client (Any) – An open
pyrxd.network.electrumx.ElectrumXClient.token_ref (GlyphRef) – The token’s permanent 36-byte ref (the deploy commit’s vout-0 outpoint, LE-reversed). Equivalently:
GlyphRef(txid=commit_txid, vout=0).initial_state (DmintV1ContractInitialState | None) – If supplied, fast-path. If
None, walk from the deploy reveal.limit (int | None) – If supplied, cap the result list at this many contracts.
Nonereturns all available.min_confirmations (int) – Skip UTXOs younger than this many blocks. Default 1 (require at least 1 confirmation).
- Returns:
A list of
DmintContractUtxofor each currently-unspent contract whose script verified S2.- Raises:
ValidationError – Inputs malformed (token_ref must point at
vout=0); or initial_state has out-of-range fields.NetworkError – Propagated from the ElectrumX client.
- Return type:
- pyrxd.glyph.mine_solution(preimage, target, *, algo=DmintAlgo.SHA256D, nonce_width=4, max_attempts=600000000)[source]¶
Search for a nonce satisfying the V1/V2 dMint PoW target.
Sequential nonce sweep starting at 0. The nonce is encoded as a little-endian unsigned integer of the requested width (4 bytes for V1, 8 bytes for V2 — matches glyph-miner’s
nonceBytesForContracts).Calls
verify_sha256d_solution()per candidate; that’s the single source of truth for “does this hash satisfy the target.” Drift between the mining check and the verifier check would let pyrxd produce a nonce that passes locally but fails on-chain (or vice versa).- Parameters:
preimage (bytes) – 64-byte preimage from
build_pow_preimage().target (int) – 8-byte 64-bit target (the V1/V2 contract’s
targetstate field).algo (DmintAlgo) – Hash algorithm. Only SHA256D is implemented; BLAKE3 and K12 raise
NotImplementedError.nonce_width (Literal[4, 8]) – 4 for V1, 8 for V2. Keyword-only and
Literal[4, 8]so a stray positional value is a type error rather than a silent V1/V2 confusion.max_attempts (int) – Upper bound on iterations before raising
MaxAttemptsError. Defaults to ≈minutes single-core at typical CPython hashlib speeds.
- Raises:
ValidationError –
preimageis not 64 bytes,targetis not positive,nonce_widthis not 4 or 8, ormax_attemptsis < 1.NotImplementedError –
algois BLAKE3 or K12.MaxAttemptsError – No solution found within
max_attemptsiterations. The exception’sattemptsandelapsed_sattributes carry telemetry.
- Return type:
Worked example (small target chosen so the loop completes in ms):
>>> from pyrxd.glyph.dmint import ( ... mine_solution, verify_sha256d_solution, MAX_SHA256D_TARGET, ... ) >>> preimage = b"\x00" * 64 >>> target = MAX_SHA256D_TARGET >> 8 # easy: ~1 in 256 expected >>> result = mine_solution(preimage, target, nonce_width=4) >>> verify_sha256d_solution(preimage, result.nonce, target, nonce_width=4) True
- pyrxd.glyph.mine_solution_dispatch(preimage, target, *, nonce_width=4, algo=DmintAlgo.SHA256D, miner_argv=None, max_attempts=600000000, timeout_s=600.0)[source]¶
Mine a nonce — picks the in-process or subprocess miner from one entrypoint.
Most callers want this helper rather than calling
mine_solution()ormine_solution_external()directly. The two paths share semantics — both return aDmintMineResultwith a nonce that satisfies the target — but have disjoint parameter sets (max_attemptsvstimeout_s, no-argv vs argv). Picking between them was a 30-line wrapper that every demo and operator script ended up rewriting; this function is that wrapper, with the branch in one place.Dispatch rule:
miner_argv is None(default): runmine_solution()in this process. Slow but correct. Use for tests, small examples, and contracts where mining takes < a minute.miner_argv is not None: invokemine_solution_external()with the supplied argv. The external miner (e.g.pyrxd.contrib.miner, a custom binary, orglyph-miner) runs as a subprocess and returns a verified nonce via the JSON-over-stdio protocol. The local re-verification inmine_solution_externalis the load-bearing safety check against a buggy or malicious miner.
- Parameters:
preimage (bytes) – 64-byte preimage from
build_pow_preimage().target (int) – The PoW target.
nonce_width (Literal[4, 8]) – 4 for V1 contracts, 8 for V2.
algo (DmintAlgo) – Hash algorithm. Currently only SHA256D is implemented; BLAKE3 and K12 raise from
mine_solution(). Ignored on the external-miner path (the protocol doesn’t carry an algo field; external miners are assumed SHA256D until the protocol is extended).miner_argv (list[str] | None) –
None→ in-process; otherwise an argv list passed tosubprocess.run()for the external miner. Use[sys.executable, "-m", "pyrxd.contrib.miner"]for the bundled parallel miner.max_attempts (int) – Iteration cap on the in-process path. Ignored on the external-miner path (the external miner caps via
timeout_sinstead).timeout_s (float) – Subprocess timeout on the external-miner path. Ignored in-process (use
max_attemptsthere).
- Returns:
DmintMineResultwith the verified nonce.- Raises:
MaxAttemptsError – in-process exhausted
max_attempts, or external miner exceededtimeout_s/ explicitly signalled exhaustion.ValidationError – external miner returned a malformed response or a nonce that fails local verification.
- Return type:
- pyrxd.glyph.mine_solution_external(preimage, target, *, miner_argv, nonce_width=4, timeout_s=600.0)[source]¶
Delegate nonce search to an external miner via JSON-over-subprocess.
Spawns
miner_argvas a subprocess, writes one JSON line to its stdin, reads one JSON line from its stdout, and re-verifies the returned nonce locally. The local re-verification is the load-bearing safety check — a wrong nonce from the external process raises rather than getting silently embedded in a transaction.The miner is expected to:
Read one JSON object from stdin:
{"preimage_hex", "target_hex", "nonce_width"}.Search for a valid nonce.
Write one JSON object to stdout — on a hit (exit 0):
{"nonce_hex", "attempts", "elapsed_s"}; on nonce-space exhaustion (exit 2, added in 0.5.1):{"exhausted": true}(pyrxd then raisesMaxAttemptsErrorimmediately rather than waiting for the parent timeout to fire).
A bundled reference implementation ships at
pyrxd.contrib.miner(added in 0.5.1) — see Parallel mining and the external-miner protocol for the full protocol spec and operational notes. Invoke it via:miner_argv=[sys.executable, "-m", "pyrxd.contrib.miner"]
Warning
Supply-chain risk: pyrxd does NOT pin or verify the miner binary.
miner_argv[0]is resolved by the OS at exec time, so a malicious binary earlier in$PATHcan intercept calls. The local nonce re-verification (below) defends against the miner returning a wrong nonce, but cannot detect side-channel exfiltration: a malicious miner sees the preimage (which encodes the contract ref + miner binding) and can leak it out-of-band over the network.Mitigations the caller should consider:
Invoke with an absolute path (
["/usr/local/bin/glyph-miner", ...]) rather than a bare name to bypass$PATHresolution.Verify the binary’s checksum against the upstream release before first use.
Run pyrxd in an environment where
$PATHis controlled (e.g. a dedicated user account, sandbox, or container).
For testing and trusted environments the bare-name form is fine.
- Parameters:
preimage (bytes) – 64-byte preimage from
build_pow_preimage().target (int) – The PoW target.
miner_argv (list[str]) – argv passed to
subprocess.run()(e.g.["glyph-miner", "--stdin"]). The first element must be a binary or shell-resolvable name; pyrxd does not pin a specific miner. See the supply-chain warning above.nonce_width (Literal[4, 8]) – 4 for V1, 8 for V2.
timeout_s (float) – Hard timeout. The subprocess is killed and
MaxAttemptsErrorraised on expiry.
- Raises:
ValidationError – The miner returned a malformed JSON response, a nonce of wrong width, or a nonce that fails local verification.
MaxAttemptsError – The miner exceeded
timeout_s.FileNotFoundError –
miner_argv[0]is not on PATH.
- Return type:
- pyrxd.glyph.parse_mutable_nft_script(script)[source]¶
Parse a mutable NFT output script, returning (mutable_ref, payload_hash) or None.
- pyrxd.glyph.sign_metadata(metadata, private_key, algo='ecdsa-secp256k1')[source]¶
Return a new GlyphMetadata with creator.sig populated.
The private key’s compressed public key is embedded as creator.pubkey. The signing protocol is:
Build canonical CBOR with sig=”” and the pubkey.
commit_hash = SHA256d(cbor)
message = SHA256(“glyph-v2-creator:” || commit_hash)
sig = ECDSA(private_key, message) [low-s DER, no double-hash]
- Parameters:
metadata (GlyphMetadata) – GlyphMetadata to sign. Any existing creator field is replaced.
private_key (PrivateKey) – pyrxd PrivateKey — the token deployer’s key.
algo (str) – Signing algorithm identifier (default: “ecdsa-secp256k1”).
- Returns:
A frozen copy of metadata with creator.sig set.
- Return type:
- pyrxd.glyph.split_qualified_name(qualified)[source]¶
Split
"alice.rxd"into("alice", "rxd").Names with no domain (e.g.
"alice") default to domain"rxd"— matching Photonic’s behavior. Names with multiple dots use the LAST dot as the domain separator (so"foo.bar.rxd"is("foo.bar", "rxd")).
- pyrxd.glyph.target_to_difficulty(target, algo=DmintAlgo.SHA256D)[source]¶
Convert PoW target to difficulty (approximate).
- pyrxd.glyph.verify_creator_signature(metadata)[source]¶
Verify the creator signature embedded in metadata.
- Returns:
(True, “”) if valid; (False, reason) if invalid or missing.
- Parameters:
metadata (GlyphMetadata)
- Return type:
- pyrxd.glyph.verify_sha256d_solution(preimage, nonce, target, *, nonce_width=8)[source]¶
Verify a SHA256d PoW solution.
Valid if: hash[0..4] == 0x00000000 AND int.from_bytes(hash[4..12], ‘big’) < target
target is clamped to MAX_SHA256D_TARGET before comparison — a caller-supplied target above the maximum would make the check trivially pass for any hash that starts with four zero bytes.
- pyrxd.glyph.wave_attrs_from_metadata(metadata)[source]¶
Convenience wrapper: extract
WaveAttrsfrom a parsedGlyphMetadata(typically fromGlyphInspector.extract_reveal_metadata()).Returns
Nonefor non-WAVE metadata or legacy-shape WAVE withoutattrs.name(which RXinDexer cannot index).- Parameters:
metadata (GlyphMetadata)
- Return type:
WaveAttrs | None