pyrxd.btc_wallet — BTC-side wallet

Bitcoin wallet tooling for the Gravity Taker.

Public API

BtcKeypair — keypair with all 4 address formats BtcUtxo — UTXO descriptor BtcPaymentTx — signed transaction result generate_keypair — generate a fresh keypair from CSPRNG keypair_from_wif — load keypair from WIF (testing/recovery) build_payment_tx — build+sign a 1-input segwit-v0 payment tx validate_btc_address — validate a mainnet Bitcoin address string validate_satoshis — validate a satoshi amount

class pyrxd.btc_wallet.BitcoinCoreBroadcaster[source]

Bases: object

BtcBroadcaster backed by a Bitcoin Core sendrawtransaction RPC.

Intended for the regtest milestone (a local node). Reuses the injected rpc(method, params) coroutine so it shares transport/auth with a BitcoinCoreRpcSource rather than opening a second session. Idempotent: an “already known” node response is mapped to the tx’s own txid as success.

__init__(rpc)[source]
Return type:

None

async broadcast(raw_tx)[source]
Parameters:

raw_tx (bytes)

Return type:

str

class pyrxd.btc_wallet.BitcoinTaprootLeg[source]

Bases: object

The concrete BTC HTLC leg (the production btc_leg).

Parameters:
  • network – BTC network prefix (“bcrt” regtest, “tb” testnet/signet, “bc” mainnet).

  • funding_utxo (taker_keypair /) – The taker’s wallet key + the single UTXO that funds the HTLC (one input is the covenant structural constraint of build_payment_tx). funding_utxo must hold btc_sats + fee_sats (plus dust slack for change).

  • broadcaster – A BtcBroadcaster (idempotent).

  • funding_reader – A BtcFundingReader — reads the funded amount from the chain.

  • claim_to_scriptpubkey (refund_to_scriptpubkey /) – Where the refund (taker) and claim (maker) spends pay out.

  • fee_sats – Flat fee for the funding/claim/refund txs (regtest milestone; a fee estimator is a later refinement).

  • min_confirmations – Confirmations required before the on-chain funded amount is trusted.

  • audit_cleared – Explicit opt-in for a value-bearing network (see require_audit_cleared()). Ignored for isolated test chains.

__init__(*, network, taker_keypair, funding_utxo, maker_claim_pubkey_xonly, broadcaster, funding_reader, refund_to_scriptpubkey, claim_to_scriptpubkey, policy=None, maker_claim_privkey=None, audit_cleared=False, fee_sats=<object object>, min_confirmations=<object object>, funding_input_type=<object object>, fund_confirm_poll_s=<object object>, fund_confirm_timeout_s=<object object>)[source]
Parameters:
Return type:

None

async claim(locator, preimage)[source]

Build + idempotently broadcast the maker’s claim tx (reveals p).

Only a MAKER-role leg (constructed with maker_claim_privkey) can do this — the claim spend uses the maker’s claim-leaf key. A taker-role leg without that key fail-closes. build_claim_tx re-verifies sha256(p) opens the leaf hashlock before signing.

Parameters:
Return type:

str

async confirmations_of_claim(claim_tx_bytes)[source]

Confirmation depth of the maker’s BTC claim tx (the reorg gate’s input).

The txid is resolved VIA THE NODE from the exact claim_tx_bytes p was scraped from (never a local segwit parse) — so an attacker can’t reveal p in a shallow tx while pointing the gate at a deep unrelated tx. Fail-closed: any read/derivation error propagates (the coordinator then refuses to claim).

Parameters:

claim_tx_bytes (bytes)

Return type:

int

derive_funding_scriptpubkey(terms)[source]

The funding SPK the taker independently re-derives from the terms.

Return type:

bytes

async fund(terms)[source]

Fund the HTLC P2TR address from the taker’s UTXO; return the locator.

Build → idempotent-broadcast → read the funded amount back from the chain (D4: the amount is the ON-CHAIN value, never a self-report). The funding tx pays output 0 to the HTLC address; change (if any) returns to the taker.

Return type:

BtcHtlcLocator

locked_amount(locator)[source]

The funded amount the coordinator binds to terms.value_amount — sats for BTC (the chain-neutral seam; an ETH leg returns wei).

Return type:

int

promised_funding_scriptpubkey(terms)[source]

The funding SPK the maker promised.

For the HTLC there is no separate maker-side derivation — the SPK is a pure function of the negotiated terms, so the promised SPK equals the re-derived one. (The pre-lock gate’s equality check still runs; a divergence here would signal a terms/derivation bug.)

Return type:

bytes

async refund(locator, timeout)[source]

Build + idempotently broadcast the taker’s CSV refund tx. Returns the txid.

The refund leaf spends via the taker’s refund key (held by this leg) after the relative timelock matures. Idempotent broadcast tolerates a retry.

Parameters:
Return type:

str

scrape_secret(claim_tx_bytes, hashlock)[source]

Scrape p from the maker’s claim tx witness (pure; by sha256==H).

Parameters:
Return type:

bytes

class pyrxd.btc_wallet.BtcBroadcaster[source]

Bases: Protocol

Submit a raw BTC tx to the network. Composed into the leg (not on the ABC).

broadcast MUST be idempotent: if the node already knows the tx, return its txid as success rather than raising — a crash-recovery retry re-broadcasts the same tx and must not be treated as a failure.

__init__(*args, **kwargs)
async broadcast(raw_tx)[source]

Broadcast raw_tx; return the broadcast txid (BE hex).

Parameters:

raw_tx (bytes)

Return type:

str

class pyrxd.btc_wallet.BtcFundingReader[source]

Bases: Protocol

Read BTC chain state the HTLC leg needs: funding amount, confirmation depth, and the canonical txid of a raw tx.

Duck-typed over a BtcDataSource-like object. read_output_amount_sats returns the value of (txid, vout) as committed on-chain (NOT a self-report), enforcing min_confirmations (raise/fail-closed if shallower). confirmations is the symmetric confirmation-depth reader (mirrors RadiantChainIO.confirmations) the reorg gate consumes. txid_of resolves a raw tx’s canonical txid VIA THE NODE — never a local segwit parse (see the reorg gate plan; the gated txid must be that of the exact bytes p was scraped from).

__init__(*args, **kwargs)
async confirmations(txid)[source]

Return the confirmation depth of txid (0 if unconfirmed/unknown).

Parameters:

txid (str)

Return type:

int

async read_output_amount_sats(txid, vout, *, min_confirmations)[source]

Return the on-chain satoshi value of (txid, vout) at >= min_confirmations.

Parameters:
  • txid (str)

  • vout (int)

  • min_confirmations (int)

Return type:

int

async txid_of(raw_tx)[source]

Resolve raw_tx’s canonical txid via the node (NOT a local parse).

Parameters:

raw_tx (bytes)

Return type:

str

class pyrxd.btc_wallet.BtcHtlc[source]

Bases: object

The HTLC funding artifact, before a UTXO funds it.

Carries the script tree, control blocks for each leaf, the NUMS internal key, and the derived funding address/scriptPubKey. with_funding produces a BtcHtlcLocator once the funding outpoint + amount are known.

__init__(script_tree, internal_key, control_block_claim, control_block_refund, network)
Parameters:
Return type:

None

property address: str
property output_key: bytes
property scriptpubkey: bytes
with_funding(outpoint, amount_sats)[source]
Parameters:
Return type:

BtcHtlcLocator

script_tree: ScriptTree
internal_key: bytes
control_block_claim: bytes
control_block_refund: bytes
network: str
class pyrxd.btc_wallet.BtcHtlcLocator[source]

Bases: object

The FULL durable retained state for a funded BTC HTLC.

This is NOT opaque — it is everything required to later claim or refund the output. Persisting a reduced form (e.g. only the privkey) strands the BTC, because the script-path spend needs the whole Tapscript tree + control block.

__init__(funding_outpoint, script_tree, control_block_claim, control_block_refund, internal_key, amount_sats, network='bc')
Parameters:
Return type:

None

property address: str
classmethod from_dict(d)[source]
Parameters:

d (dict)

Return type:

BtcHtlcLocator

network: str = 'bc'
property output_key: bytes
property scriptpubkey: bytes

OP_1 <32-byte output key>.

Type:

The P2TR scriptPubKey

to_dict()[source]

JSON/hex-serialisable form — NEVER contains the preimage p.

Return type:

dict

funding_outpoint: BtcOutpoint
script_tree: ScriptTree
control_block_claim: bytes
control_block_refund: bytes
internal_key: bytes
amount_sats: int
class pyrxd.btc_wallet.BtcKeypair[source]

Bases: object

A Bitcoin keypair with addresses in all 4 Gravity-supported formats.

Private key is stored as PrivateKeyMaterial (never logs/repr leaks).

network

bech32 HRP ("bc" mainnet, "tb" testnet/signet, "bcrt" regtest, or any custom HRP). Defaults to "bc".

Type:

str

__init__(_privkey, pubkey_bytes, p2pkh_address, p2wpkh_address, p2sh_p2wpkh_address, p2tr_address, pkh, p2sh_hash, p2tr_output_key, network='bc')
Parameters:
Return type:

None

network: str = 'bc'
unsafe_wif()[source]

Export WIF. Named ‘unsafe’ to be visible in code review.

Uses the WIF version byte for self.network (0x80 for mainnet, 0xEF for testnet/signet/regtest).

Return type:

str

pubkey_bytes: bytes
p2pkh_address: str
p2wpkh_address: str
p2sh_p2wpkh_address: str
p2tr_address: str
pkh: bytes
p2sh_hash: bytes
p2tr_output_key: bytes
class pyrxd.btc_wallet.BtcOutpoint[source]

Bases: object

A funding outpoint (txid big-endian hex as shown by explorers, + vout).

__init__(txid, vout)
Parameters:
Return type:

None

classmethod from_dict(d)[source]
Parameters:

d (dict)

Return type:

BtcOutpoint

prevout_bytes()[source]

Serialise as the 36-byte wire outpoint (txid LE || vout LE).

Return type:

bytes

to_dict()[source]
Return type:

dict

txid: str
vout: int
class pyrxd.btc_wallet.BtcPaymentTx[source]

Bases: object

Result of build_payment_tx.

__init__(tx_hex, txid, fee_sats, change_sats, input_type, output_type)
Parameters:
  • tx_hex (str)

  • txid (str)

  • fee_sats (int)

  • change_sats (int)

  • input_type (str)

  • output_type (str)

Return type:

None

tx_hex: str
txid: str
fee_sats: int
change_sats: int
input_type: str
output_type: str
class pyrxd.btc_wallet.BtcUtxo[source]

Bases: object

A Bitcoin UTXO to spend.

__init__(txid, vout, value)
Parameters:
Return type:

None

txid: str
vout: int
value: int
class pyrxd.btc_wallet.ScriptTree[source]

Bases: object

A 2-leaf Tapscript tree (claim leaf + refund leaf).

Holds the leaf scripts and their (cached) leaf hashes + merkle root, so the durable swap state never has to re-derive (and risk mis-deriving) the tree.

__init__(claim_script, refund_script, leaf_version=192)
Parameters:
Return type:

None

property claim_leaf_hash: bytes
leaf_version: int = 192
property merkle_root: bytes
property refund_leaf_hash: bytes
script_for(which)[source]
Parameters:

which (str)

Return type:

bytes

sibling_for(which)[source]

Return the merkle-path sibling hash for the named leaf (“claim”|”refund”).

Parameters:

which (str)

Return type:

bytes

claim_script: bytes
refund_script: bytes
class pyrxd.btc_wallet.TimeUnit[source]

Bases: Enum

The unit a Timelock is measured in.

The whole cross-chain safety invariant (t_BTC - t_RXD >= margin) rides on comparing like units; mixing blocks and seconds without conversion is a fail-closed error, not a silent coercion.

BLOCKS = 'blocks'
SECONDS = 'seconds'
class pyrxd.btc_wallet.Timelock[source]

Bases: object

A unit-tagged relative timelock (BIP68/112 CSV).

__init__(value, unit)
Parameters:
Return type:

None

csv_script_operand()[source]

Return the integer that the CSV leaf pushes (matches nSequence encoding).

The value compared by OP_CSV is the nSequence value masked to its relative-locktime bits, so the script operand equals to_nsequence() for the same lock.

Return type:

int

normalize_to(unit, *, block_interval_s)[source]

Return an equivalent Timelock in unit.

block_interval_s is the assumed seconds-per-block used for conversion (caller supplies a measured value for mainnet; estimates are test-only). Conversion is floor-based; the margin check must account for the rounding.

Parameters:
Return type:

Timelock

to_nsequence()[source]

Encode this relative timelock as a BIP68 nSequence value.

Return type:

int

value: int
unit: TimeUnit
pyrxd.btc_wallet.build_claim_tx(*, locator, preimage, claim_privkey, to_scriptpubkey, fee_sats, aux_rand)[source]

Build the maker’s claim tx (spends the claim leaf, reveals p).

Witness: <sig> <preimage> <claim_script> <control_block>.

Parameters:
Return type:

bytes

pyrxd.btc_wallet.build_htlc(*, hashlock, claim_pubkey_xonly, refund_pubkey_xonly, timeout, internal_key_xonly=b'P\x92\x9bt\xc1\xa0IT\xb7\x8bK`5\xe9z^\x07\x8aZ\x0f(\xec\x96\xd5G\xbf\xee\x9a\xce\x80:\xc0', network='bc')[source]

Construct the BTC Taproot HTLC (funding address + control blocks).

The default internal key is the provable NUMS point — every spend is script-path, so a colluding maker cannot key-path-spend without revealing p.

Parameters:
Return type:

BtcHtlc

pyrxd.btc_wallet.build_payment_tx(keypair, utxo, to_hash, to_type, amount_sats, fee_sats, input_type='p2wpkh', change_address=None)[source]

Build and sign a 1-input Bitcoin payment transaction for the Gravity Taker.

Exactly 1 input is required — this is a covenant structural constraint. input_type controls whether the input is native segwit (empty scriptSig) or wrapped segwit (23-byte scriptSig with P2WPKH redeem push).

Parameters:
Return type:

BtcPaymentTx

pyrxd.btc_wallet.build_refund_tx(*, locator, refund_privkey, timeout, to_scriptpubkey, fee_sats, aux_rand)[source]

Build the taker’s pre-signed refund tx (spends the refund leaf via CSV).

v2 tx with nSequence encoding the relative timelock per BIP68; witness is <sig> <refund_script> <control_block> (the refund leaf has no preimage).

Parameters:
Return type:

bytes

pyrxd.btc_wallet.generate_keypair(network='bc')[source]

Generate a fresh Bitcoin keypair using CSPRNG.

Uses secure_scalar_mod_n() for the private key — explicit range check, rejection sampling, never Math.random(). Matches JS btc_wallet.js::generateKeypair() audit-hardened version.

Parameters:

network (str) – bech32 HRP for address serialization. "bc" (default) for mainnet, "tb" for testnet/signet, "bcrt" for regtest, or any custom HRP.

Return type:

BtcKeypair

pyrxd.btc_wallet.keypair_from_wif(wif, network='bc')[source]

Load keypair from WIF string (for testing/recovery).

Parameters:
  • wif (str) – WIF-encoded private key.

  • network (str) – bech32 HRP for address serialization (see generate_keypair). Note: this controls OUTPUT address/WIF encoding only; the input WIF is decoded regardless of its version byte.

Return type:

BtcKeypair

pyrxd.btc_wallet.require_audit_cleared(network, *, audit_cleared)[source]

Retained for backward-compatibility; no longer blocks.

The cross-chain swap stack is unaudited — callers handling real value should verify it themselves. This matches the ecosystem norm (Radiant Core itself ships unaudited and does not hard-block mainnet use): the in-code audit gate is no longer a blocking control as of 0.9.0. The signature and audit_cleared parameter are kept so existing callers continue to work.

Parameters:
  • network (str)

  • audit_cleared (bool)

Return type:

None

pyrxd.btc_wallet.scrape_secret(claim_tx_bytes, hashlock)[source]

Extract the preimage p from a claim tx by matching sha256(p)==H.

Matches over EVERY witness push of EVERY input — never by positional offset (the C-PARSER lesson). Returns the 32-byte preimage. Raises ValidationError if no witness push hashes to H (e.g. this is a refund tx, or the wrong tx).

The hashlock disambiguates which swap this tx belongs to; the caller should pair it with the funding outpoint when multiple swaps share an H.

Parameters:
Return type:

bytes

pyrxd.btc_wallet.validate_btc_address(address)[source]

Validate a mainnet Bitcoin address.

Rejects path traversal, query injection, and anything outside the two recognized mainnet address shapes (Base58Check P2PKH/P2SH and bech32/bech32m).

Raises:

ValidationError – if the address is not a recognized mainnet format.

Parameters:

address (str)

Return type:

None

pyrxd.btc_wallet.validate_satoshis(value, name='value')[source]

Validate a satoshi amount.

Rules:
  • Must be a plain int (not bool, not float).

  • Must be > 0.

  • Must not exceed max BTC supply (21M BTC = 2.1e15 sats).

Raises:

ValidationError – on any violation.

Parameters:
Return type:

None