pyrxd.gravity — Cross-chain atomic swaps¶
pyrxd.gravity — Gravity protocol covenant transaction builders and orchestrator.
Phase 3a implements the Radiant-side covenant transaction builders that
correspond to the JS prototype’s claim_tx.js, finalize_tx.js, and
forfeit_tx.js.
Phase 3b adds the high-level GravityTrade orchestrator that wraps the
full 4-step BTC↔RXD swap into a single async class.
Public surface¶
GravityOffer— all Maker-committed parameters for a covenantClaimResult— output ofbuild_claim_tx()FinalizeResult— output ofbuild_finalize_tx()ForfeitResult— output ofbuild_forfeit_tx()build_claim_tx— spend MakerOffer → create MakerClaimed UTXObuild_finalize_tx— spend MakerClaimed → release photons to Takerbuild_forfeit_tx— Maker reclaims after claimDeadlinecompute_p2sh_code_hash— derive the expectedClaimedCodeHash a covenant checksGravityTrade— high-level async swap orchestrator (Phase 3b)TradeConfig— tunable parameters for GravityTradeConfirmationStatus— BTC confirmation poll result
- class pyrxd.gravity.ActiveOffer[source]¶
Bases:
objectState of a live Gravity MakerOffer on Radiant.
Returned by
GravityMakerSession.create_offer()and required by all subsequent lifecycle methods.- offer¶
The original
GravityOffercovenant parameters.
- maker_offer_result¶
Raw tx details from
build_maker_offer_tx.
- __init__(offer, maker_offer_result, offer_txid, offer_vout, offer_photons)¶
- Parameters:
offer (GravityOffer)
maker_offer_result (MakerOfferResult)
offer_txid (str)
offer_vout (int)
offer_photons (int)
- Return type:
None
- offer: GravityOffer¶
- maker_offer_result: MakerOfferResult¶
- class pyrxd.gravity.CappedFeeWalletSource[source]¶
Bases:
objectA capped
FeeUtxoSourceover a fixed pre-funded pool.- Parameters:
pool – The pre-funded inventory: small plain-RXD
FeeInputUTXOs the capped-pool wallet owns. Each must be a bare P2PKH UTXO whose pkh matches its own WIF (validated). Must be non-empty and free of duplicate outpoints (a duplicate would double-spend).total_cap_photons – Hard cumulative ceiling on dispensed value. Dispensing stops once the next input would push the running total over this — before handing it out.
max_per_input_photons – Optional per-input ceiling. If given, construction fails when any pool UTXO exceeds it, keeping the “a fee input is small” invariant structural rather than assumed.
- property funded_photons: int¶
Total value of the pre-funded pool. This is the ceiling only if the pool key is isolated from the operator’s main wallet (a deployment property this class cannot verify — see the module docstring and the design note’s residuals).
- next_fee_input()[source]¶
Dispense (commit) the next pool UTXO.
Raises
FeePoolExhaustedError— fail-closed — when the pool is empty or the next input would exceedtotal_cap_photons. Dispense-once: the returned UTXO is never returned again.- Return type:
- release_unspent(fee_input)[source]¶
Credit back the cap charge for a dispensed input that was never broadcast.
The cap authorises spend, but
next_fee_input()had to charge it at dispense — and the caller dispenses before it can know whether the spend will build. A build that refuses (the fee is below the node’s deadline-aware relay floor) puts nothing on-chain and pays no fee, yet left the charge standing: repeated refusals ate the budget the covering input needed, and the pool exhausted with a funded, unspendable UTXO still in it while the asset ran out its deadline (audit B3).Returns
Truewhen the charge was credited,Falsewhen this input was already credited (idempotent — a second credit would fabricate budget). RaisesValidationErrorfor anything this source has not dispensed.The cursor is deliberately not rewound. Dispense-once is the property that stops one UTXO ever backing two transactions, and it must survive the un-charge: the released input is retired, and the next dispense moves on to the next one — which is also what stops a small head-of-line input from re-refusing forever while a covering input sits behind it.
Only the caller can know whether it broadcast, so this is a report, not an inference. Call it exactly on the paths where the dispensed input provably never reached a node.
- property remaining_inputs: int¶
Count of pool UTXOs not yet dispensed (physical inventory; some may be blocked by the cap — see
remaining_photonsfor the actually-spendable budget).
- property remaining_photons: int¶
Photons that
next_fee_input()will actually dispense from here — the in-order prefix of remaining inputs that fits under the cap. Dispensing is in-order and stops at the first input that would exceed the cap (head-of-line), so this is 0 once the next input no longer fits, giving a tower an honest “page now” signal that matches dispense behaviour.
- class pyrxd.gravity.ClaimResult[source]¶
Bases:
objectClaimResult(tx_hex: ‘str’, txid: ‘str’, tx_size: ‘int’, offer_p2sh: ‘str’, claimed_p2sh: ‘str’, fee_sats: ‘int’, output_photons: ‘int’)
- __init__(tx_hex, txid, tx_size, offer_p2sh, claimed_p2sh, fee_sats, output_photons)¶
- class pyrxd.gravity.ConfirmationStatus[source]¶
Bases:
objectStatus returned by
GravityTrade.wait_confirmations().- __init__(txid, confirmations, confirmed, block_height)¶
- class pyrxd.gravity.CovenantArtifact[source]¶
Bases:
objectA loaded, pre-compiled covenant artifact with parameter substitution.
- __init__(contract, hex_template, abi)¶
- classmethod from_json(json_text, *, allow_legacy=False)[source]¶
Load from raw artifact JSON (e.g. from a custom compiled artifact).
- Parameters:
- Return type:
- classmethod load(name, *, allow_legacy=False)[source]¶
Load a bundled artifact by stem name (without
.artifact.json).Available artifacts: -
maker_offer-maker_covenant_6x12_p2wpkh-maker_covenant_flat_6x12_p2wpkh-maker_covenant_trade- Parameters:
- Return type:
- substitute(params)[source]¶
Substitute constructor params into the hex template.
Returns the full redeem script bytes. Raises
ValidationErrorif any required param is missing, any placeholder remains unfilled, or any fixed-width typed param (Ripemd160/Sha256/PubKey) has the wrong byte length — these would silently encode as the wrong push and produce an on-chain-rejected covenant.Values: -
intparams: pass Pythonint-bytes/Ripemd160/Sha256/PubKeyparams: pass hex string
- class pyrxd.gravity.FeeInput[source]¶
Bases:
objectA plain-RXD P2PKH UTXO that pays the miner fee for an HTLC spend.
The single covenant output carries the asset and cannot also pay the fee, so every HTLC spend joins a fee input the spender owns. The whole surplus (
value - out0_value) is consumed as the miner fee — there is no change output (the covenant forbids a second output), sovalueIS the miner fee. Size it upstream to clear the per-kB min-relay fee without being wastefully large. Both ends are bounded, differently on purpose: the builders REFUSE an under-fee’d transaction (_assert_fee_clears_relay_floor()), because Radiant has neither RBF nor CPFP and an under-fee’d time-critical spend cannot be repaired after broadcast; they only WARN on a wildly over-large one (_warn_if_fee_is_an_overpay()), because refusing a spend the node would have accepted can forfeit the asset to the counterparty’s refund.- __init__(txid, vout, value, scriptpubkey, wif)¶
- class pyrxd.gravity.FinalizeResult[source]¶
Bases:
objectFinalizeResult(tx_hex: ‘str’, txid: ‘str’, tx_size: ‘int’, fee_sats: ‘int’, output_photons: ‘int’)
- __init__(tx_hex, txid, tx_size, fee_sats, output_photons)¶
- class pyrxd.gravity.ForfeitResult[source]¶
Bases:
objectForfeitResult(tx_hex: ‘str’, txid: ‘str’, tx_size: ‘int’, fee_sats: ‘int’, output_photons: ‘int’)
- __init__(tx_hex, txid, tx_size, fee_sats, output_photons)¶
- class pyrxd.gravity.GravityMakerSession[source]¶
Bases:
objectManage the full lifecycle of a Gravity BTC↔RXD atomic swap offer.
This class handles the Maker’s side of the swap:
Build and broadcast the MakerOffer tx (
create_offer).Poll for the Taker’s claim (
wait_for_claim).Broadcast a cancel tx if the Taker never claims (
cancel_offer).Query current state (
check_status).
- Parameters:
rxd_client – Connected
ElectrumXClientfor Radiant chain operations (broadcast, query UTXOs).btc_source – A
BtcDataSource— used only by subclasses / extensions that need BTC confirmation data. May beNonefor pure Radiant operations.maker_priv – Maker’s secp256k1 private key wrapped in
PrivateKeyMaterial.poll_interval_seconds – Seconds between UTXO polls in
wait_for_claim. Default 30.fee_policy – Min-relay rate every transaction this session builds is sized and checked against. Defaults to
DEFAULT_RADIANT_DEADLINE_FEE_POLICY(mainnet). Set this on regtest, whose node advertises a tenth of the mainnet floor — without it the high-level API has no way to reach the escape hatch the builders already accept.
Examples
Typical Maker flow:
async with ElectrumXClient(["wss://electrumx.example.com"]) as rxd: session = GravityMakerSession(rxd_client=rxd, maker_priv=priv) params = GravityOfferParams( offer=offer, funding_txid="...", funding_vout=0, funding_photons=12_000_000, fee_sats=2_500_000, # ~250-byte funding tx at the 10_000 photons/byte floor ) active = await session.create_offer(params) claim_txid = await session.wait_for_claim(active, timeout_seconds=3600) if claim_txid is None: # fee omitted: sized from the cancel tx's own measured bytes. cancel_txid = await session.cancel_offer(active, maker_address=maker_addr)
- __init__(rxd_client, maker_priv, btc_source=None, poll_interval_seconds=30, fee_policy=None)[source]¶
- Parameters:
rxd_client (ElectrumXClient)
maker_priv (PrivateKeyMaterial)
btc_source (BtcDataSource | None)
poll_interval_seconds (int)
fee_policy (DeadlineFeePolicy | None)
- Return type:
None
- async cancel_offer(offer, fee_sats=None, maker_address='', fee_policy=None)[source]¶
Broadcast the cancel (MakerOffer.cancel()) transaction.
Reclaims the MakerOffer UTXO before the claim deadline using
build_cancel_tx. This is only valid if the Taker has NOT yet claimed the UTXO.- Parameters:
offer (ActiveOffer) – The
ActiveOfferto cancel.fee_sats (int | None) – Miner fee in photons for the cancel tx.
None(the default) sizes it from the assembled transaction’s own bytes at the relay floor — the only correct default, because the cancel scriptSig carries the whole MakerOffer redeem script and its size therefore varies per offer. This parameter previously defaulted to1000, ~2,840x under the floor for a 285-byte cancel, which made the documentedcancel_offer(active)flow raise on first use and left the Maker with no revocation path.maker_address (str) – Maker’s Radiant P2PKH address to receive the reclaimed photons. Required — must be a valid Radiant address.
fee_policy (DeadlineFeePolicy | None) – Per-call override of the session’s policy. Set on regtest, which advertises a tenth of the mainnet relay floor.
- Returns:
The cancel tx’s txid.
- Return type:
- Raises:
ValidationError – If
maker_addressis empty or the offer redeem is invalid.NetworkError – On broadcast failure.
- async check_status(offer)[source]¶
Return the current status of the offer UTXO.
Queries the Radiant ElectrumX server for the MakerOffer P2SH UTXO.
Returns one of:
"open"— UTXO is still unspent (offer not yet claimed)."claimed"— UTXO no longer in unspent set (Taker has claimed)."expired"— claim_deadline has passed and UTXO is unspent(Maker can now forfeit).
"unknown"— UTXO not found and not yet past deadline(may be unconfirmed or already finalized/cancelled).
- Parameters:
offer (ActiveOffer) – The
ActiveOfferto check.- Returns:
One of
"open","claimed","expired","unknown".- Return type:
- Raises:
NetworkError – On ElectrumX query failure.
- async create_offer(offer_params)[source]¶
Build and broadcast the MakerOffer funding tx.
The offer UTXO is a P2SH output locked to
offer_params.offer’s MakerOffer covenant. Once broadcast, the Taker can claim it by spending it withbuild_claim_tx.- Parameters:
offer_params (GravityOfferParams) – Funding-UTXO details and the
GravityOffercovenant.- Returns:
Populated with the resulting txid and UTXO details.
- Return type:
- Raises:
ValidationError – On any parameter format or covenant validation error.
NetworkError – On broadcast failure.
- async wait_for_claim(offer, timeout_seconds=3600, *, clock=<built-in function monotonic>)[source]¶
Poll for the Taker’s claim transaction.
Polls
get_utxos()on the MakerOffer P2SH script hash. When the UTXO disappears from the unspent set the Taker has claimed it.This method cannot directly return the claim txid — ElectrumX’s
listunspentAPI only reports which UTXOs are currently unspent. Once the offer UTXO is spent (claimed), we return the offer’s txid as a sentinel so the caller knows which offer was claimed. Callers that need the actual claim txid should fetch the spending tx separately (e.g. viaget_transactionon the address history).- Parameters:
offer (ActiveOffer) – The
ActiveOfferreturned bycreate_offer.timeout_seconds (int) – Maximum seconds to wait, as WALL-CLOCK seconds. Returns
Noneon timeout.clock (Callable[[], float]) – Monotonic-ish time source. Injectable so the timeout branch is reachable in a test without sleeping — the same shape
pyrxd.network.confirmuses, and for the same reason: a fakesleepdoes not advance a real clock.
- Returns:
The offer txid (as a claimed-sentinel) on success, or
Noneon timeout.- Return type:
str or None
- class pyrxd.gravity.GravityOffer[source]¶
Bases:
objectAll parameters a Maker commits into a MakerOffer covenant.
Mirrors
CovenantParamsinpyrxd.spv.proofbut adds Radiant-side fields and the two precomputed redeem scripts.- __init__(btc_receive_hash, btc_receive_type, btc_satoshis, chain_anchor, anchor_height, merkle_depth, taker_radiant_pkh, claim_deadline, photons_offered, offer_redeem_hex, claimed_redeem_hex, expected_code_hash_hex, expected_nbits=None, expected_nbits_next=None)¶
- Parameters:
btc_receive_hash (bytes)
btc_receive_type (str)
btc_satoshis (int)
chain_anchor (bytes)
anchor_height (int)
merkle_depth (int)
taker_radiant_pkh (bytes)
claim_deadline (int)
photons_offered (int)
offer_redeem_hex (str)
claimed_redeem_hex (str)
expected_code_hash_hex (str)
expected_nbits (bytes | None)
expected_nbits_next (bytes | None)
- Return type:
None
- validate_deadline_from_now(accept_short_deadline=False)[source]¶
Check that
claim_deadlineis at leastMIN_DEADLINE_FROM_NOW_HOURSfrom now.Raises
ValidationErrorunless accept_short_deadline isTrue(audit 04-S1 guard: Taker needs time to confirm BTC + build SPV proof + finalize on Radiant).- Parameters:
accept_short_deadline (bool)
- Return type:
None
- class pyrxd.gravity.GravityOfferParams[source]¶
Bases:
objectParameters required to create a new Gravity MakerOffer.
These are the funding-UTXO details for the Maker’s side. The
GravityOfferitself (covenant bytecode, BTC-side params, etc.) is built externally (e.g. viabuild_gravity_offer) and passed asoffer.- offer¶
Fully populated
GravityOfferwithoffer_redeem_hexset.
- change_address¶
Optional Radiant P2PKH address for change output. See
build_maker_offer_txfor semantics.- Type:
str | None
- __init__(offer, funding_txid, funding_vout, funding_photons, fee_sats, change_address=None)¶
- offer: GravityOffer¶
- class pyrxd.gravity.GravityTrade[source]¶
Bases:
objectOrchestrate a complete Gravity BTC↔RXD atomic swap.
- Parameters:
radiant_network – Connected
ElectrumXClientfor Radiant chain operations (broadcast, fetch tx/block).bitcoin_source – A
BtcDataSourcefor Bitcoin chain data (tx fetch, Merkle proof, block headers).config – Optional
TradeConfig. Uses defaults if not provided.
Examples
Typical Taker flow:
async with ElectrumXClient(["wss://electrumx.example.com"]) as rxd: trade = GravityTrade(radiant_network=rxd, bitcoin_source=btc_src) claim = await trade.claim( offer=offer, offer_txid="...", offer_vout=0, offer_photons=10_000_000, # Photons, at the 10,000/byte mainnet floor: size it from the tx you # actually build. These are worked examples, not constants to copy. fee_sats=3_000_000, # ~300-byte claim taker_privkey=privkey, ) btc_txid = "..." # broadcast BTC payment externally status = await trade.wait_confirmations(btc_txid) result = await trade.finalize( btc_txid=btc_txid, offer=offer, claimed_txid=claim.txid, claimed_vout=0, claimed_photons=claim.output_photons, taker_address="...", fee_sats=30_000_000, # finalize carries the SPV proof: ~10x the claim )
- __init__(*, radiant_network, bitcoin_source, config=None)[source]¶
- Parameters:
radiant_network (ElectrumXClient)
bitcoin_source (BtcDataSource)
config (TradeConfig | None)
- Return type:
None
- async claim(offer, offer_txid, offer_vout, offer_photons, fee_sats, taker_privkey, fee_policy=None)[source]¶
Spend the MakerOffer UTXO, creating a MakerClaimed UTXO.
Broadcasts the claim transaction to the Radiant network and returns a
ClaimResult.The claim transaction requires Taker’s signature (audit 04-S3).
build_claim_txindependently verifies the code hash before signing (audit 05-F-13).- Parameters:
offer (GravityOffer) – The
GravityOfferposted by the Maker.offer_txid (str) – Radiant txid of the MakerOffer funding output.
offer_vout (int) – Output index of the MakerOffer UTXO.
offer_photons (int) – Value of the MakerOffer UTXO in photons.
fee_sats (int) – Radiant miner fee in photons. Must clear the relay floor for the assembled transaction’s real size — at the mainnet floor of 10,000 photons/byte a ~300-byte claim needs ~3,000,000 photons, not the
1000this docstring used to show.taker_privkey (PrivateKeyMaterial) – Taker’s secp256k1 private key.
fee_policy (DeadlineFeePolicy | None) – Per-call override of
TradeConfig.fee_policy.
- Return type:
- async finalize(btc_txid, offer, claimed_txid, claimed_vout, claimed_photons, taker_address, fee_sats, btc_tx_height=None, fee_policy=None)[source]¶
Fetch the BTC SPV proof, verify it, and broadcast the finalize tx.
This method always runs the full
SpvProofBuilderverifier chain — there is no way to bypass verification at this level.- Parameters:
btc_txid (str) – Bitcoin transaction ID of the Taker’s BTC payment.
offer (GravityOffer) – The
GravityOfferoriginally posted by the Maker. Used to constructCovenantParamsfor SPV proof verification.claimed_txid (str) – Radiant txid of the MakerClaimed UTXO (output of
claim()).claimed_vout (int) – Output index of the MakerClaimed UTXO.
claimed_photons (int) – Value of the MakerClaimed UTXO in photons.
taker_address (str) – Taker’s Radiant P2PKH address to receive the photons.
fee_sats (int) – Radiant miner fee in photons. The finalize tx is by far the largest in this module — it pushes the whole BTC transaction, N block headers and the Merkle branch into one scriptSig — so its relay floor is an order of magnitude above the claim’s. A fee that was ample for a claim is nowhere near enough here.
btc_tx_height (int | None) – Optional: Bitcoin block height where btc_txid was confirmed. If not provided, the orchestrator will determine it automatically.
fee_policy (DeadlineFeePolicy | None) – Per-call override of
TradeConfig.fee_policy.
- Raises:
SpvVerificationError – If any SPV verifier rejects the proof.
NetworkError – On any network failure fetching BTC data.
ValidationError – On any parameter format error.
- Return type:
- async wait_confirmations(btc_txid, min_confirmations=None)[source]¶
Poll Bitcoin until btc_txid reaches the required confirmations.
- Parameters:
btc_txid (str) – Bitcoin transaction ID (64 hex chars, big-endian).
min_confirmations (int | None) – Override
config.min_btc_confirmationsfor this call. Bound the same way the config field is (>= 1), which it was not:TradeConfighas refusedmin_btc_confirmations < 1since it was written, but the per-call override went straight to the source. Measured against a source holding the tx in the mempool only,min_confirmations=0returnedconfirmed=True, confirmations=0on the FIRST poll and-5returnedconfirmations=-5— a “confirmed” verdict for a transaction with no depth at all, handed to the caller who is about to release the other leg. Zero is not a weaker policy here, it is no policy: the depth this waits for MUST equal the covenant’s header-depthN.
- Returns:
Always has
confirmed=Trueon return (raises on timeout).- Return type:
- Raises:
NetworkError – If polling exceeds
config.max_poll_attempts.ValidationError – If btc_txid is not a valid 64-char hex string, or min_confirmations is given and is below 1.
- class pyrxd.gravity.HtlcCovenant[source]¶
Bases:
objectA built HTLC covenant: the funded SPK + the bindings a spend must satisfy.
- prologue_len¶
Length of the compiled body (==
len(funded_spk)for NFT/RXD; the offset of the FT epilogue weld for FT). The bare-0xbd guard pins to this.- Type:
- taker_holder_script / maker_holder_script
The holder scripts
output[0]of a claim (taker) / refund (maker) must equal; the covenant bindshash256of each.
- expected_taker_hash / expected_maker_hash
hash256(taker_holder_script)/hash256(maker_holder_script)— the values baked into the covenant.
- __init__(variant, funded_spk, prologue_len, taker_holder_script, maker_holder_script, expected_taker_hash, expected_maker_hash, genesis_ref, hashlock, refund_csv)¶
- class pyrxd.gravity.MakerOfferResult[source]¶
Bases:
objectOutput of
build_maker_offer_tx()— the MakerOffer funding tx.- __init__(tx_hex, txid, tx_size, offer_p2sh, fee_sats, output_photons)¶
- class pyrxd.gravity.OfferReceive[source]¶
Bases:
objectA per-offer BTC receive destination derived from a maker account xpub.
Persist
offer_indexwith the offer: the maker needs it to (a) spend the received BTC via the matching child key and (b) never reuse it for another live offer.- __init__(btc_receive_hash, btc_receive_type, offer_index)¶
- class pyrxd.gravity.RadiantChainIO[source]¶
Bases:
objectThin chain helper over an
ElectrumXClient-like object.Provides exactly what the leg needs: broadcast, confirmation depth, and the on-chain value of a covenant output. NOT unified with
GravityTrade(that drives the SPV-oracle finalize swap, a different protocol).The injected
clientmust exposebroadcast(raw)->txid,get_transaction_verbose(txid)->dict(withconfirmations), andget_utxos(script_hash)->list(records withtx_hash/tx_pos/value).- async covenant_unspent_incl_mempool(outpoint)[source]¶
Mempool-AWARE liveness of a covenant outpoint — the complement to
find_covenant_utxo’s mempool-BLIND scantxoutset scan.True= unspent considering the mempool;False= spent (confirmed OR by a PENDING mempool tx);None= the client cannot answer (the caller keeps its own idempotency guard). Lets the autonomous claim executor treat a covenant already spent IN THE MEMPOOL as claimed — killing the per-tick re-carve drain WITHOUT a durable cross-restart store and WITHOUT the SeenStore’s eviction blind spot (a truly-unspent covenant, e.g. after a claim is evicted by a reorg, correctly re-fires).
- async find_covenant_utxo(spk, *, expected_value=None, pin_outpoint=None)[source]¶
Locate the funded covenant UTXO for
spk->(outpoint, value, height).Scans the UTXO set of the covenant scriptPubKey (ElectrumX script-hash =
sha256(spk)reversed). The HONEST funding is one output, but the SPK is a pure function of PUBLIC negotiated terms, so anyone can pay it and the scan can return several — see thelen(utxos) > 1branch below, which SELECTS the earliest-confirmed match rather than refusing (refusing on ambiguity is a denial anyone can trigger). Ifexpected_valueis given, a match must equal it (a wrong value is a mis-funded covenant -> fail-closed);pin_outpoint, once known, selects instead of re-discovering. The returned value is the ON-CHAIN value, never a self-report.THIS SAID “the covenant funds exactly one output, so there is one matching UTXO”, one screen above the address-poisoning branch that exists because that is not true.
UNITS.
expected_valueis aPhotonValuebecause it is matched against the UTXO’s NATIVE carrier value. It is NOT a Glyph FT token count: a token covenant enforcesrefValueSum(ref) == amountand its carrier can be dust of any size, so an FT amount passed here is a units error (#505). The third element is aChainHeight— the height the covenant was MINED at, never a confirmation depth; the two sort in opposite directions and the earliest-confirmed rule below inverts under the wrong one.
- class pyrxd.gravity.RadiantCovenantLeg[source]¶
Bases:
objectThe concrete Radiant
radiant_leg(HTLC covenant claim/refund).- Parameters:
network – Radiant network tag (regtest test chains bypass the audit gate).
maker_pkh (taker_pkh /) – The taker (claim) and maker (refund) Radiant holder pubkey-hashes. The covenant binds
hash256(holder(pkh)); these must reproduce the terms’taker_dest_hash/maker_dest_hash(asserted inexpected_covenant_scriptpubkey()).chain_io – A
RadiantChainIO(broadcast + confirmations + UTXO value).fee_source – A
FeeUtxoSourcesupplying the fee input for each spend.min_confirmations – Confirmations required before the funded covenant value is trusted.
audit_cleared – Explicit opt-in for a value-bearing
network(seepyrxd.btc_wallet.htlc_leg.require_audit_cleared()).fee_policy – The
DeadlineFeePolicythe pre-broadcast affordability gate enforces. Defaults to the reference node’s advertised 0.10 RXD/kB effective relay rate; pass an explicit policy when the node this leg broadcasts to advertises a differenteffective_minrelaytxfee.
- __init__(*, network, taker_pkh, maker_pkh, chain_io, fee_source, min_confirmations=1, audit_cleared=False, fee_policy=None)[source]¶
- async claim_asset(record, preimage)[source]¶
Build + broadcast the TAKER’s claim spend (reveals
p). Returns the txid.Fee-sized against the DEADLINE: the maker’s CSV refund branch opens once the covenant is
t_rxdconfirmations deep, sot_rxd - confirmationsis the number of Radiant blocks in which this claim must be mined, not merely broadcast. The pre-broadcast gate refuses (and pages) if the dispensed fee input cannot meet that requirement — there is no post-broadcast remedy on Radiant.
- async covenant_outpoint(terms)[source]¶
Locate the funded covenant UTXO
txid:voutby scanning its SPK’s UTXO set.The maker locks the asset into the covenant SPK (a pure function of the terms); the leg finds that single funded UTXO on-chain via ElectrumX. The carrier value is bound to
terms.radiant_amountso a mis-funded covenant fails closed.- Parameters:
terms (NegotiatedTerms)
- Return type:
- async expected_covenant_scriptpubkey(terms)[source]¶
The covenant SPK the on-chain lock must equal (built from the terms).
- Parameters:
terms (NegotiatedTerms)
- Return type:
- async rebroadcast_claim_if_evicted(record, preimage)[source]¶
Re-broadcast the taker’s claim if it has fallen out of the mempool. Returns the new txid, or None when nothing needed doing.
WHY THIS EXISTS. A non-BIP68-final refund is rejected from the mempool (Radiant Core
validation.cpp:724-728), so the maker CANNOT pre-broadcast and a claim already sitting in the mempool at CSV maturity wins the race. The whole safety of the claim window therefore rests on the claim STAYING there — and Radiant has no RBF and no CPFP, so a claim that is evicted cannot be bumped back in. Mempool expiry is about eight hours.The coordinator broadcast the claim and advanced straight to a completed state, so an eviction was invisible: the maker’s refund became valid at maturity, confirmed, and took both legs while the swap’s own record said it had finished.
Single-shot on purpose — no loop, no clock. The caller drives it on whatever tick it already has, which keeps this testable and keeps clock ownership where the rest of the module puts it.
Returns None when the covenant is already spent (our claim is in the mempool or mined — nothing to do) and when the source ABSTAINS, because an unknown answer must not be treated as “evicted” and turned into a duplicate broadcast.
- async refund_asset(record)[source]¶
Build + broadcast the MAKER’s CSV refund spend. Returns the txid.
P3 maturity self-check: the covenant’s CSV refund leaf is only spendable once the covenant UTXO is buried
t_rxddeep (the BIP68 relative-block timelock the covenant was built with:refund_csv=t_rxd.value, mature atconfirmations >= t_rxd.value). Refuse a non-final refund HERE rather than emit a tx a node rejects — under a deadline-pinning mempool “rely on node rejection” is fragile — with an exact “needs N confirmations, has M” message a block-based poller retries on. This guards EVERYrefund_assetcaller (mutual_refund,maybe_refund_asset_on_maker_stall) at the leg, complementing the coordinator-side height check inmaybe_refund_asset_on_maker_stall. (The CLAIM branch has no CSV, soclaim_assetis intentionally NOT gated this way.)- Parameters:
record (SwapRecord)
- Return type:
- async verify_maker_asset_funded(terms, *, min_confirmations=None)[source]¶
TAKER-side fail-closed gate: is the MAKER’s asset really locked, at the agreed value, buried deep enough, before the taker funds the counter leg? Returns
(outpoint, value_photons, confirmations); RAISES on anything else — the taker MUST NOT lock BTC/ETH if this raises. The Radiant twin ofpyrxd.btc_wallet.htlc_leg.BitcoinTaprootLeg.verify_counterparty_funded().WHY:
docs/htlc-handshake-wire-format.mdHZ-1 states it normatively — “a taker MUST NOT fund the counter leg until it has confirmed the maker’s asset lock on chain, at the agreed scriptPubKey, for the agreed value, at a depth the taker chose.” Nothing else in the handshake gives the taker that. The BTC claim leaf is<H> … <makerClaimPk> OP_CHECKSIGwith no precondition that the asset was ever locked, and the maker holds bothpand the claim key from the moment it publishes the envelope. So a maker that locks NOTHING and simply waits can sweep the taker’s HTLC the instant it appears: the taker’s loss is the fullbtc_sats, and the FSM’s nominal “taker locks first” ordering is bookkeeping, not a safety guarantee.What is checked, all fail-closed:
the covenant scriptPubKey is re-derived here from the taker’s own ``terms`` (
_build_covenant()— amount, H,t_rxdCSV, both dest hashes, the asset REF), never taken from anything the maker advertises;that exact SPK holds a funded UTXO, and its ON-CHAIN value equals
terms.radiant_amount— an unfunded SPK, a mis-valued one, and an ambiguous UTXO set all raise (RadiantChainIO.find_covenant_utxo());the funding is buried
min_confirmationsdeep. “Funded” alone is NOT enough: ElectrumXlistunspentincludes MEMPOOL outputs, so a maker can fund with a replaceable transaction, wait for the taker’s lock, then double-spend the funding away — it still claims the counter leg withpwhile the vanished covenant leaves the taker nothing to claim.Noneuses this leg’s configuredmin_confirmations; the coordinator passes the policy’s RXD burial depth for a real-value swap.
- class pyrxd.gravity.RefAuthenticityIndexer[source]¶
Bases:
ProtocolThe minimal indexer surface needed to verify a genesis REF is real.
Implementations resolve a genesis-outpoint ref to its on-chain reveal as a
ResolvedRef.resolve_refis async (the underlyingglyph.get_tokenRPC is async) and MUST raise (not returnNoneoptimistically) when the indexer cannot reach a definitive answer — the caller treatsNone, any missing/invalid field, or any exception as fail-closed. ReturningNonemeans “no such token” (also fail-closed).- __init__(*args, **kwargs)¶
- async resolve_ref(genesis_ref)[source]¶
Resolve
genesis_refto its reveal, orNoneif unknown.- Parameters:
genesis_ref (bytes)
- Return type:
ResolvedRef | None
- class pyrxd.gravity.ResolvedRef[source]¶
Bases:
objectA trusted indexer’s resolution of a genesis ref to its on-chain reveal.
This is the inspectable surface the gate binds against — the gate does NOT trust a bare boolean from the indexer; it re-checks each field against what the taker agreed to. A real adapter populates this from the indexer’s
glyph.get_tokenresponse (ref_outpoint/payload_hash/confirmations + a decodedglymarker); a test fake constructs it directly.- genesis_outpoint¶
The 36-byte genesis outpoint (txid||vout) the indexer says this token was minted at. Binding (a)/(d): MUST equal the advertised
genesis_ref.- Type:
- has_gly_marker¶
True iff the reveal carries a
glyenvelope (binding (b)). A bare singleton on a plain wallet UTXO has none — the exact R1 forgery.- Type:
- payload_hash¶
The reveal’s payload-commitment hash (binding (c)), or
b""if the indexer did not supply one.- Type:
- confirmations¶
Confirmations on the genesis tx (binding (e)). A negative/None value is treated as fail-closed by the gate.
- Type:
- __init__(genesis_outpoint, has_gly_marker, payload_hash, confirmations)¶
- class pyrxd.gravity.RxinDexerRefAdapter[source]¶
Bases:
objectResolve a genesis ref to a
ResolvedRefvia RXinDexerglyph.get_token.Implements the
RefAuthenticityIndexerprotocol the pre-lock gate awaits. Maps the indexer’s token dict to the inspectable fields the gate binds:genesis_outpoint — from the token’s
ref_outpoint(txid:vout), re-encoded to the 36-byte wire ref so it compares equal to the advertisedgenesis_ref. (glyph.get_tokenonly returns genuinely-minted Glyph tokens, so a resolvable token IS aglyreveal — seehas_gly_marker.)has_gly_marker —
Truewhenever the indexer returned a token dict for the ref (the indexer only indexes realglyenvelopes). A bare wallet-UTXO singleton (the R1 forgery) resolves toNoneand the gate fails closed.payload_hash — from
payload_hash(bytes), orb""if absent.confirmations — read separately from the genesis tx via
chain_io(glyph.get_tokendoes not carry confs).
NOTE (T7 plan D3): a single indexer is a SPOF, and decoding a token dict is NOT SPV authenticity (no Merkle/header binding). For the regtest milestone the local node is ground truth; SPV-bound / multi-source cross-checking is the audit-gated track. This adapter is the single-indexer regtest backend.
- __init__(indexer, chain_io)[source]¶
- Parameters:
indexer (Any)
chain_io (RadiantChainIO)
- Return type:
None
- async resolve_ref(genesis_ref)[source]¶
- Parameters:
genesis_ref (bytes)
- Return type:
ResolvedRef | None
- class pyrxd.gravity.SeenStore[source]¶
Bases:
objectIn-memory H-freshness store (the coordinator’s
reserve/has_seen).Records every hashlock H the coordinator has committed to funding, so a reused H is rejected for BOTH reasons: economic (free-option replay) and cross-swap preimage replay.
reserve(H)is the authoritative atomic test-and-set the coordinator calls PRE-broadcast;has_seenis a read-only advisory probe (the pre-lock gate’s cheap early-reject), never the binding decision.NON-DURABLE (
durable = False): a plainset, so freshness does NOT survive a restart or a second process. That is acceptable only for a single-process, single-shot run that mints a fresh H per swap (the dust runbook); the coordinator’s construct-time guard refuses this store on a value-bearing network unless the operator passesCoordinatorConfig(accept_nondurable_seen=True). A durable replacement (SQLiteINSERT OR IGNOREkeyed on H, declaringdurable = True) is deferred to the external-audit track; it MUST stay non-blocking (asyncio.to_threadbehind an asyncreserve) and fsync the reservation BEFORE the BTC broadcast. The method shape is duck-compatible so that durable store drops in unchanged.- durable = False¶
- class pyrxd.gravity.TradeConfig[source]¶
Bases:
objectTunable parameters for GravityTrade.
- min_btc_confirmations¶
Minimum on-chain BTC confirmations before finalizing. MUST equal the covenant’s header-depth N (the finalize path verifies exactly N consecutive headers from the anchor; a proof with fewer is rejected). Default 6 — matches the default N=6 covenant and Bitcoin’s standard finality convention (~1h). N is a per-offer MAKER knob: raise it (e.g. 12) for high-value/irreversible assets to roughly double the reorg cost, at the price of a longer wait. When using a covenant built with a different N, set this to that N (audit 2026-05-24: the two must match).
- Type:
- max_poll_attempts¶
Maximum number of polls before
wait_confirmationsraisesNetworkError. Default 120 (= 2 hours at 60s).- Type:
- accept_short_deadline¶
If
True, suppress the 24h deadline guard (audit 04-S1). Only for testing — do NOT set in production.- Type:
- deadline_warning_seconds¶
Emit a WARNING log in
finalize()when the Maker’s claim deadline is less than this many seconds away. Default 7200 (2 hours). Set to 0 to disable. Takers should finalize immediately if this fires (audit 04-S1 forfeit race).- Type:
- fee_policy¶
Min-relay rate
claimandfinalizesize their transactions against.NoneusesDEFAULT_RADIANT_DEADLINE_FEE_POLICY(mainnet). The builders have taken this since #407/#411; without it on the config the high-level orchestrator could not reach the escape hatch at all, so a regtest node — which advertises a tenth of the mainnet floor — had no way to opt out.- Type:
pyrxd.gravity.fee_policy.DeadlineFeePolicy | None
- __init__(min_btc_confirmations=6, poll_interval_seconds=60, max_poll_attempts=120, accept_short_deadline=False, deadline_warning_seconds=7200, fee_policy=None)¶
- pyrxd.gravity.build_claim_tx(offer, funding_txid, funding_vout, funding_photons, fee_sats, taker_privkey, accept_short_deadline=False, fee_policy=None)[source]¶
Build the Radiant
claim()spending tx: MakerOffer → MakerClaimed.Requires Taker’s private key to produce a Radiant signature satisfying
MakerOffer.claim(takerSig)— prevents third-party state-advance grief (audit 04-S3).Audit 05-F-13: verifies
claimedRedeemHexmatchesexpectedClaimedCodeHashbefore building, so the tx won’t be rejected on-chain.scriptSig layout:
<takerSig+hashtype> OP_1 <offer redeem script>
- Parameters:
offer (GravityOffer) – Fully populated
GravityOffer(validated in__post_init__).funding_txid (str) – Hex txid of the MakerOffer UTXO being spent.
funding_vout (int) – Output index of the MakerOffer UTXO.
funding_photons (int) – Value of the MakerOffer UTXO in photons.
fee_sats (int) – Miner fee in photons (== satoshis on Radiant).
taker_privkey (PrivateKeyMaterial) – Taker’s secp256k1 private key (wrapped in
PrivateKeyMaterial).accept_short_deadline (bool) – If
True, suppress the 24-hour deadline guard (audit 04-S1).fee_policy (DeadlineFeePolicy | None) – The min-relay rate the assembled transaction is checked against. Defaults to
DEFAULT_RADIANT_DEADLINE_FEE_POLICY. Pass a policy built from the target node’s owngetmempoolinfo/effective_minrelaytxfeewhen it advertises something else, which regtest does.
- Raises:
InsufficientFundsError – If
fee_satsis below the node’s min-relay floor for the transaction’s real, signed size. See_assert_fee_clears_relay_floor().- Return type:
- pyrxd.gravity.build_finalize_tx(spv_proof, claimed_redeem_hex, funding_txid, funding_vout, funding_photons, to_address, fee_sats, minimum_output_photons=0, header_slots=None, branch_slots=None, fee_policy=None)[source]¶
Build the Radiant
finalize()tx: MakerClaimed → Taker’s address.The
spv_proofmust be a fully-verifiedSpvProofproduced bySpvProofBuilder.build()— this is the only way to construct one.No Radiant signature is required — the covenant accepts the scriptSig based on the SPV proof data alone. Output routing is enforced by the covenant’s committed
takerRadiantPkhstate.scriptSig layout (pushed bottom-to-top; last push is TOP at exec):
<h1> <h2> ... <hN> <branch> <rawTx> OP_0 <claimed redeem script>
OP_0(empty push = selector 0) selects thefinalize()function.- Parameters:
spv_proof (SpvProof) – Fully-verified SPV proof (only obtainable from
SpvProofBuilder).claimed_redeem_hex (str) – Hex of MakerClaimed locking bytecode.
funding_txid (str) – Txid of the MakerClaimed UTXO being spent.
funding_vout (int) – Output index of the MakerClaimed UTXO.
funding_photons (int) – Value of the MakerClaimed UTXO in photons.
to_address (str) – Taker’s Radiant P2PKH address.
fee_sats (int) – Miner fee in photons.
minimum_output_photons (int) – The covenant’s
totalPhotonsInOutputfloor — baked in at offer creation time. The finalize tx is rejected on-chain ifoutput[0].value < totalPhotonsInOutput, so we validate here before burning relay fees. Passoffer.photons_offeredwhen calling fromGravityTrade. Defaults to 0 (no floor check) for callers that have already verified externally.fee_policy (DeadlineFeePolicy | None) – The min-relay rate the assembled transaction is checked against. Defaults to
DEFAULT_RADIANT_DEADLINE_FEE_POLICY. Pass a policy built from the target node’s owngetmempoolinfo/effective_minrelaytxfeewhen it advertises something else, which regtest does.header_slots (int | None)
branch_slots (int | None)
- Raises:
InsufficientFundsError – If
fee_satsis below the node’s min-relay floor for the transaction’s real size. This is the largest transaction in the module — the whole BTC payment tx,header_slots× 80-byte headers and abranch_slots-level Merkle branch all go into one scriptSig — so its floor is an order of magnitude above a claim’s and a fee that was ample there is not ample here. See_assert_fee_clears_relay_floor().- Return type:
- pyrxd.gravity.build_forfeit_tx(offer, funding_txid, funding_vout, funding_photons, maker_address, fee_sats, fee_policy=None)[source]¶
Build the Radiant
forfeit()tx: Maker reclaims afterclaimDeadline.Can only be built once
offer.claim_deadlinehas passed (i.e. the current wall-clock time is >=claim_deadline).Sets
nLockTime = claim_deadlineforOP_CHECKLOCKTIMEVERIFY. Sets input sequence to0xFFFFFFFE(<0xFFFFFFFF— required for CLTV to be evaluated).scriptSig layout:
OP_1 <claimed redeem script>
OP_1(selector 1) selects theforfeit()function.- Parameters:
offer (GravityOffer) –
GravityOfferwhoseclaim_deadlinehas already passed.funding_txid (str) – Txid of the MakerClaimed UTXO being forfeited.
funding_vout (int) – Output index of the MakerClaimed UTXO.
funding_photons (int) – Value of the MakerClaimed UTXO in photons.
maker_address (str) – Maker’s Radiant P2PKH address to receive the reclaimed photons.
fee_sats (int) – Miner fee in photons.
fee_policy (DeadlineFeePolicy | None) – The min-relay rate the assembled transaction is checked against. Defaults to
DEFAULT_RADIANT_DEADLINE_FEE_POLICY. Pass a policy built from the target node’s owngetmempoolinfo/effective_minrelaytxfeewhen it advertises something else, which regtest does.
- Raises:
InsufficientFundsError – If
fee_satsis below the node’s min-relay floor for the transaction’s real size — forfeit is the last exit from a MakerClaimed UTXO, so one that cannot relay leaves the Maker with nothing else to try. See_assert_fee_clears_relay_floor().- Return type:
- pyrxd.gravity.build_gravity_offer(maker_pkh, maker_pk, taker_pk, taker_radiant_pkh, btc_receive_hash, btc_receive_type, btc_satoshis, btc_chain_anchor, expected_nbits, anchor_height, merkle_depth, claim_deadline, photons_offered, expected_nbits_next=None, accept_short_deadline=False, covenant_artifact_name='maker_covenant_flat_12x20_sentinel_all', offer_artifact_name='maker_offer', used_btc_receive_hashes=None, reject_low_difficulty=True, min_difficulty_nbits=None)[source]¶
Build a
GravityOfferwith real covenant redeem scripts generated from the bundled artifacts.This is the top-level entry point for Maker-side offer construction. Internally it:
Validates the claim deadline (S1 guard).
Loads the MakerClaimed covenant artifact and substitutes code-section params.
Computes
expectedClaimedCodeHash = hash256(P2SH_scriptPubKey)from the substituted redeem script.Loads the MakerOffer artifact and substitutes its params (including the code hash from step 3).
Returns a
GravityOfferwith both redeem scripts populated.
- Parameters:
accept_short_deadline (bool) – Override the 24h deadline guard. Set True only for test harnesses you control — never because a counterparty asks.
covenant_artifact_name (str) – Override the MakerClaimed artifact stem.
offer_artifact_name (str) – Override the MakerOffer artifact stem.
used_btc_receive_hashes (set[bytes] | None) – Optional set of
btc_receive_hashvalues already committed to other LIVE offers by this Maker. If the newbtc_receive_hashis in this set, the call is rejected.reject_low_difficulty (bool) – Enforce a difficulty FLOOR on the committed nBits (audit 2026-05-29 F-02). The covenant only pins
nBits == committed, so a min-difficulty commit (e.g. theffff001dfootgun) lets an attacker mine a fake SPV header chain off the real anchor for ~$0. With this True the committed target must be strictly harder than the floor. Covenant-less retained uses (bridge-in / oracle / gate) MUST set this True — and SHOULD also passmin_difficulty_nbits, because the default floor (difficulty-1) only blocks the difficulty-1 class, not a target merely easier than mainnet. Defaults True (secure-by-default, audit 2026-05-29 F-02 follow-up): a real mainnet nBits is far harder than difficulty-1 so it passes the default floor; only a difficulty-1-class commit (ffff001d) is rejected. Passreject_low_difficulty=Falsefor regtest/test offers that useffff001d.min_difficulty_nbits (bytes | None) – Optional 4-byte wire nBits defining the difficulty floor used when
reject_low_difficultyis True. Source this from the live block header atanchor_heightfor a real network-difficulty floor; if omitted, the floor defaults to difficulty-1 (a coarse footgun guard only).maker_pkh (bytes)
maker_pk (bytes)
taker_pk (bytes)
taker_radiant_pkh (bytes)
btc_receive_hash (bytes)
btc_receive_type (str)
btc_satoshis (int)
btc_chain_anchor (bytes)
expected_nbits (bytes)
anchor_height (int)
merkle_depth (int)
claim_deadline (int)
photons_offered (int)
expected_nbits_next (bytes | None)
- Return type:
Warning
CROSS-OFFER REPLAY (audit 2026-05-24 C-ECON-1). A Bitcoin payment cannot reference a Radiant offer, so the covenant binds the payment only by
btc_receive_hash+btc_satoshis+btc_chain_anchor. If the samebtc_receive_hash(BTC receive address) + amount is reused across two offers with overlapping anchor windows, one BTC payment + one SPV proof can finalize BOTH offers — a taker pays once and takes two assets. There is NO on-chain or automatic defense; the earlier “per-offer-derived btcReceiveHash (H1)” control described in the design notes was never implemented. The Maker MUST use a fresh, unique BTC receive address per offer. Passused_btc_receive_hashesto have this function reject reuse it can see; offers built by separate processes are the caller’s responsibility.PREFER
build_gravity_offer_derived(), which derives a distinct receive address per offer from the maker’s account xpub — that is the structural fix (distinct address ⇒ distinct code hash ⇒ replay impossible) and needs no caller-side live-set tracking. This raw-hash entry point remains for callers that manage receive addresses themselves; theused_btc_receive_hashesguard is only best-effort.
- pyrxd.gravity.build_gravity_offer_derived(account_xpub, offer_index, *, maker_pkh, maker_pk, taker_pk, taker_radiant_pkh, btc_satoshis, btc_chain_anchor, expected_nbits, anchor_height, merkle_depth, claim_deadline, photons_offered, expected_nbits_next=None, accept_short_deadline=False, covenant_artifact_name='maker_covenant_flat_12x20_sentinel_all', offer_artifact_name='maker_offer', reject_low_difficulty=True, min_difficulty_nbits=None)[source]¶
Build an offer whose BTC receive address is DERIVED per-offer (replay-safe).
This is the structural fix for the cross-offer replay (C-ECON-1 / “H1”): the receive hash is derived from
account_xpubatoffer_indexviapyrxd.gravity.receive.derive_offer_btc_receive(), so every offer commits to a DISTINCT BTC address. A payment to one offer’s address cannot satisfy another offer’s covenant (differentbtcReceiveHash⇒ different code hash), so one BTC payment can finalize at most one offer — no caller-supplied live-set bookkeeping required.Prefer this over passing a raw
btc_receive_hashtobuild_gravity_offer(). The caller MUST allocate a fresh, never-reusedoffer_indexper offer (a persistent monotonic counter per account) and hold the matching xprv to spend received BTC.- Returns:
(GravityOffer, OfferReceive)— persistOfferReceive.offer_indexwith the offer so the maker can later spend the received BTC and never reuse it.- Parameters:
account_xpub (Any)
offer_index (int)
maker_pkh (bytes)
maker_pk (bytes)
taker_pk (bytes)
taker_radiant_pkh (bytes)
btc_satoshis (int)
btc_chain_anchor (bytes)
expected_nbits (bytes)
anchor_height (int)
merkle_depth (int)
claim_deadline (int)
photons_offered (int)
expected_nbits_next (bytes | None)
accept_short_deadline (bool)
covenant_artifact_name (str)
offer_artifact_name (str)
reject_low_difficulty (bool)
min_difficulty_nbits (bytes | None)
- Return type:
- pyrxd.gravity.build_htlc_claim_tx(*, covenant, covenant_outpoint, carrier_value, preimage, fee, fee_policy=None)[source]¶
Build the TAKER’s claim spend: reveal
p, pay the single output to the taker.Covenant scriptSig =
<preimage push> <OP_0>(preimage first/under, selector last/on top). The single output pays the covenant’s pinned TAKER holder script at the carrier value; the fee input’s full surplus is the miner fee (no change).fee_policy(default: the reference node’s 0.10 RXD/kB effective relay rate) supplies the min-relay rate the assembled transaction is checked against. Pass an explicit policy when the target node advertises a different rate — it is node policy, not a protocol constant.- Parameters:
covenant (HtlcCovenant)
covenant_outpoint (str)
carrier_value (int)
preimage (bytes)
fee (FeeInput)
fee_policy (DeadlineFeePolicy | None)
- Return type:
- pyrxd.gravity.build_htlc_covenant_ft(*, genesis_txid, genesis_vout, amount, taker_pkh, maker_pkh, hashlock, refund_csv)[source]¶
Build the FT-variant HTLC covenant (genesis ref bound via the FT epilogue weld).
- pyrxd.gravity.build_htlc_covenant_nft(*, genesis_txid, genesis_vout, nft_carrier_value, taker_pkh, maker_pkh, hashlock, refund_csv)[source]¶
Build the NFT-variant HTLC covenant (singleton
d8<ref>inside the body).
- pyrxd.gravity.build_htlc_covenant_rxd(*, amount, taker_pkh, maker_pkh, hashlock, refund_csv)[source]¶
Build the RXD-variant HTLC covenant (native RXD: NO genesis ref, NO ref ops).
- pyrxd.gravity.build_htlc_refund_tx(*, covenant, covenant_outpoint, carrier_value, fee, fee_policy=None)[source]¶
Build the MAKER’s CSV refund spend (function selector OP_1, after the timelock).
Covenant scriptSig =
<OP_1>ONLY (no preimage, no sig — gated by the relative timelock). The covenant input’snSequence=covenant.refund_csvandtx.version = 2so BIP68 engages. The single output pays the covenant’s pinned MAKER holder script; the fee input’s full surplus is the miner fee.- Parameters:
covenant (HtlcCovenant)
covenant_outpoint (str)
carrier_value (int)
fee (FeeInput)
fee_policy (DeadlineFeePolicy | None)
- Return type:
- pyrxd.gravity.build_maker_offer_tx(offer, funding_txid, funding_vout, funding_photons, fee_sats, maker_privkey, change_address=None, fee_policy=None)[source]¶
Build the Radiant funding tx that deploys a MakerOffer P2SH UTXO.
Spends a plain P2PKH UTXO owned by the Maker and creates a P2SH output locked to the MakerOffer redeem script. Once confirmed, the Taker can spend it with
build_claim_tx().The P2SH scriptPubKey is:
OP_HASH160 <hash160(offer_redeem)> OP_EQUAL
Signing uses standard BIP143 P2PKH sighash (the input is a plain P2PKH UTXO, not a covenant) with Radiant’s
hashOutputHashesextension. The scriptCode for signing is the P2PKH scriptPubKey of the funding input, derived from the Maker’s compressed public key.- Parameters:
offer (GravityOffer) – Fully populated
GravityOfferwithoffer_redeem_hexset.funding_txid (str) – Hex txid of the Maker’s P2PKH UTXO being spent.
funding_vout (int) – Output index of the Maker’s P2PKH UTXO.
funding_photons (int) – Value of the Maker’s P2PKH UTXO in photons.
fee_sats (int) – Miner fee in photons. The offer output receives
funding_photons - fee_satsphotons.maker_privkey (PrivateKeyMaterial) – Maker’s secp256k1 private key (
PrivateKeyMaterial). Used to sign the P2PKH input and derive the P2PKH scriptCode for hashing.change_address (str | None) – Default
None(single-output): the fullfunding_photons - fee_satsis locked in the P2SH, so surplus aboveoffer.photons_offeredstays with the covenant to fund the later claim/finalize tx fees. When set (two-output): the P2SH receives exactlyoffer.photons_offeredand the remainder goes to a P2PKH output atchange_address. Use the two-output form only whenoffer.photons_offeredalready includes a buffer for downstream claim/finalize fees — otherwise the covenant will reject those txs.fee_policy (DeadlineFeePolicy | None) – The min-relay rate the assembled transaction is checked against. Defaults to
DEFAULT_RADIANT_DEADLINE_FEE_POLICY(the reference mainnet node’s 0.10 RXD/kB effective rate). The rate is node policy, not a protocol constant — pass a policy built from the target node’s owngetmempoolinfo/effective_minrelaytxfeewhen it advertises something else, which regtest does.
- Raises:
InsufficientFundsError – If
fee_satsis below the node’s min-relay floor for the transaction’s real, signed size. See_assert_fee_clears_relay_floor().- Return type:
- pyrxd.gravity.compute_p2sh_code_hash(redeem_script)[source]¶
Compute expectedClaimedCodeHash:
hash256of the P2SH scriptPubKey.This is what MakerOffer checks on-chain:
hash256(tx.outputs[0].codeScript) == expectedClaimedCodeHash
For P2SH outputs the
codeScriptis the 23-byteOP_HASH160 <hash> OP_EQUALscriptPubKey.Audit 05-F-13 fix: caller passes the claimed redeem script; we derive the hash independently rather than trusting a caller-supplied value.
- pyrxd.gravity.derive_offer_btc_receive(account_xpub, offer_index)[source]¶
Derive a unique P2WPKH receive hash for one offer.
- Parameters:
account_xpub (str | bytes | Xpub) – The maker’s BIP32 account-level xpub (e.g. the public form of
m/84'/0'/0'). Child derivation is non-hardened so it is reproducible from the xpub; the maker holds the matching xprv to spend received BTC.offer_index (int) – A per-offer, never-reused non-hardened index. The caller owns allocation (a monotonic counter) — this function is pure and does NOT track which indices have been issued.
- Returns:
OfferReceivewith the derived 20-byte receive hash and the index.- Raises:
ValidationError – on an out-of-range index or an unusable xpub.
- Return type:
- pyrxd.gravity.validate_claim_deadline(claim_deadline, *, min_future_seconds=86400, bypass=False)[source]¶
Raise
ValidationErrorifclaim_deadlineis not at leastmin_future_secondsfrom now (default: 24h).This is the Python port of the S1 check in
extract_p2sh_code_hash.js(audit 04 finding S1: a short deadline lets Maker race-snipe Taker’s claim).
- async pyrxd.gravity.verify_ref_authenticity(indexer, genesis_ref, *, asset_variant, min_confirmations, expected_payload_hash=None)[source]¶
Hard pre-payment gate: confirm the covenant’s REF is a real minted asset.
awaitthis BEFORE the taker pays any BTC for an FT/NFT swap. Plain-RXD swaps carry no ref and are skipped. Enforces the five bindings (a)-(e) documented at module level and fails closed on EVERY uncertain outcome: indexer unreachable/error,None(unknown token), a missing/invalid field, genesis-outpoint ≠ ref, absentglymarker, payload mismatch, or a genesis shallower thanmin_confirmations.- Parameters:
indexer (RefAuthenticityIndexer) – a trusted
RefAuthenticityIndexer. A lying or attacker-controlled indexer defeats this gate — the taker must use an indexer they trust (the audit-gated track adds SPV/multi-source cross-checking; a single indexer is a SPOF, see T7 plan D3).genesis_ref (bytes) – the 36-byte genesis outpoint ref baked into the covenant. This IS the advertised asset’s identity (binding d).
asset_variant (str) – “rxd” | “ft” | “nft”. Only ft/nft carry a ref to verify.
min_confirmations (int) – required confirmations on the genesis tx (binding e). Must be a non-negative int.
expected_payload_hash (bytes | None) – if the taker agreed to a specific payload, the reveal’s payload hash MUST match it (binding c).
Noneskips this single binding (the others still apply).
- Raises:
ValidationError – if the ref is not provably the advertised authentic asset. The caller MUST NOT pay the counter-leg (BTC or ETH) when this raises.
- Return type:
None