pyrxd.eth_wallet — Ethereum counter-leg¶
The ETH side of a cross-chain swap. The chain-neutral coordinator and the EthLeg
orchestrator live in pyrxd.gravity — Cross-chain atomic swaps; the durable EthHtlcLocator and recover_secret are
re-exported on the package and documented under pyrxd (top-level).
EVM chain registry¶
EVM counter-chain registry — the per-chain safety knobs for the finalized-checkpoint leg.
The swap coordinator treats every non-BTC counter leg as a finalized-checkpoint (EVM)
chain: the proven EthLeg + EthHtlc.sol machinery is chain-id-agnostic (the same
contract bytecode and finalized-tag reads work on any EVM-equivalent chain), so adding
an EVM chain does NOT touch the coordinator. What IS chain-specific — and safety-critical
for an atomic swap — is how long the ``finalized`` tag lags the tip, which sizes
MarginPolicy.eth_finalization_window_s (the reorg gate’s finalization reserve).
This module is that knowledge, written down once with provenance, instead of a magic number per harness.
Window discipline (matches the existing codebase split):
finalization_window_sis the steady-state lag of thefinalizedtag. Stalls (L1 inactivity leak, batcher outage) are budgeted SEPARATELY viaCrossClockMargin.eth_finality_stall_tolerance_s— do not inflate the window to cover them.Ethereum L1: finality = 2 epochs = 768 s steady-state (Casper FFG). The 768 s floor is enforced by
MarginPolicy.Base (OP-stack L2): an L2 block is
finalizedonce the batch containing it sits in a FINALIZED L1 block — i.e. batch-posting cadence (~1 min on Base) + L1 inclusion + the same 2-epoch L1 finality. Steady-state ≈ 15 min; the entry below uses 900 s (CHOSEN/ ESTIMATED). HONEST WORST CASE: the OP-stack sequencing window permits a batch to land up to 12 hours late (and a chain that misses it can reorg that far) — a swap operator must sizeeth_finality_stall_tolerance_s(and therefore the RXD timelock) for the stall they are willing to survive, exactly as for an L1 finality stall. Sources: Base “Transaction Finality” docs; OP-stack batcher/configurability specs (https://docs.base.org/base-chain/network-information/transaction-finality, https://specs.optimism.io/protocol/configurability.html).Optimism (OP-stack L2): the SAME stack as Base, identical finalized-tag semantics; 900 s steady-state (observed ~15-20 min), 12 h sequencing-window worst case budgeted separately. Source: https://docs.optimism.io/app-developers/transactions/statuses
Arbitrum One (Nitro optimistic rollup): finalized = L2 block whose Sequencer batch sits in a FINALIZED L1 block (Ethereum-anchored hard finality). ~10-20 min steady-state -> 1200 s. WORST CASE: the sequencer force-inclusion delay (
maxTimeVariation) is ~24 h (vs OP-stack 12 h) — a liveness stall of the finalized tag, budgeted viaeth_finality_stall_tolerance_s; NOT the ~6.4 d withdrawal dispute window (irrelevant to reorg safety). Source: https://docs.arbitrum.io/how-arbitrum-works/transaction-lifecycleLinea (zk / validity rollup): finalized = L2 block whose validity PROOF is verified in a finalized L1 block (Ethereum-anchored). Proof-cadence-dominated: official MEDIAN hard finality ~1 h 40 (6000 s), documented to “never exceed 16 h” — that tail goes to the stall budget, not the steady window. Source: https://docs.linea.build (finality).
Deliberately NOT in the registry — Polygon PoS (chain_id 137): it does not fit this model.
Its finalized tag is Polygon’s OWN validator-set “milestone” finality (Heimdall / CometBFT,
~5 s), NOT Ethereum-anchored — Polygon PoS is a commit-chain/sidechain that checkpoints to
Ethereum for withdrawal proofs but does not inherit Ethereum finality block-by-block. So the
768 s floor misrepresents it in BOTH directions: it finalizes far faster than 768 s (via its own
~5 s consensus), and that finality is secured by Polygon’s stake, not Ethereum’s. Treating it as
“just another EVM chain” would silently swap the trust model an atomic-swap operator relies on; a
Polygon-PoS swap needs an explicit, separately-justified finality model (a reorg depth in
Polygon’s own security terms), not this Ethereum-anchored window. evm_chain_by_id(137) fails
closed. (A 2025-09-10 faulty-milestone incident delayed Polygon finality ~15 min-1 h, resolved
only by an emergency hard fork — a validator-set liveness risk with no Ethereum analogue.)
Deliberately NOT in the registry — BNB Smart Chain (chain_id 56), for the same reason as
Polygon PoS and it is worth stating separately because the token side looks so inviting. BSC’s
finalized tag is its OWN validator set’s (Parlia + BEP-126 fast finality), not
Ethereum-anchored. MEASURED 2026-08-25 against three independent endpoints
(bsc-dataseed.bnbchain.org, bsc-mainnet.public.blastapi.io, bsc-dataseed1.defibit.io):
the finalized tag sits 0-2 seconds / 1-2 blocks behind the tip. An Ethereum-anchored
chain cannot finalize in under the 768 s L1 checkpoint, so that measurement alone settles what
kind of finality this is.
The floor would force a 768 s window onto it, and — exactly as for Polygon — that number
misrepresents BSC in both directions: it finalizes far faster, and the finality it does have is
secured by BSC’s stake, not Ethereum’s. Note that inflating the window does NOT recover the
difference. A longer wait buys more of BSC’s own security, never Ethereum’s, so the swap
operator’s trust model is silently substituted whatever number is chosen. BSC needs a reorg depth
argued in its own validator-set terms before it can carry value here; evm_chain_by_id(56)
fails closed until someone writes that down.
(The token side is genuinely clean — see the BSC note in tokens.py — which is precisely why
this exclusion is recorded here rather than left to be rediscovered.)
The network tag feeds the existing fail-closed gates unchanged: any tag not in
AUDIT_CLEARED_NETWORKS (only isolated test chains are) is value-bearing and refuses to
run without the explicit post-audit audit_cleared=True opt-in — so every chain here,
including the testnets, stays behind the audit gate by construction.
- pyrxd.eth_wallet.chains.ETH_FINALIZATION_WINDOW_FLOOR_S: int = 768¶
Hard floor (SECONDS) on any chain’s
finalization_window_s. THE ONE DEFINITION —pyrxd.gravity.swap_coordinator.MarginPolicyimports this name rather than holding its own copy.Post-Merge Ethereum finality is two epochs:
2 x 32 slots x 12 s. Every EVM chain here settles to Ethereum L1, so none of them can finalize faster than the L1 checkpoint they anchor to, which makes this a floor for L2s as much as for L1.It was two independent literals — this one and
MarginPolicy’s — joined by a “Keep in sync with” comment. That comment is the whole failure mode written down: it names the obligation and provides nothing that can discharge it. The two guard the SAME rule at two different boundaries (registry construction here,MarginPolicyconstruction there), so raising one to reflect a real consensus change and missing the other leaves a cross-chain swap claiming against a finalization reserve that the chain no longer honours — the reorg gate’s entire purpose.
- class pyrxd.eth_wallet.chains.EvmChain[source]¶
Bases:
objectOne EVM-equivalent counter chain the ETH leg machinery can run against.
chain_idpins the chain everywhere it matters:EthRpc(expected_chain_id=...)refuses a node on the wrong chain,EthHtlcContractLeg(chain_id=...)signs with EIP-155 replay protection, and the durableEthHtlcLocatorrecords it.networkis the tagEthLeg(network=...)reads for the value-bearing/audit gates.finalization_window_sseedsMarginPolicy.eth_finalization_window_s.- is_testnet: bool = False¶
Whether this chain’s coins are FAUCET money. Stated per entry, never inferred.
network cannot answer this. It feeds the audit gate, whose cleared set holds Bitcoin-family tags only — so every EVM chain here, testnets included, reads as “not audit-cleared”. That is correct for what that gate does (nothing here is audit-cleared) and useless for deciding whether real value is at stake. Reading it as the latter forced measured margins and a multi-endpoint quorum onto a Base Sepolia rehearsal: a guard refusing honest work, caught by the runner’s own wiring tests.
Not derived from the name either. “ends in -sepolia” is true of every testnet in this registry today and is a naming convention, not a property; the next testnet that breaks it would be silently promoted to real-value.
JSON-RPC client¶
Minimal async Ethereum JSON-RPC client (web3-backed), mirroring the repo’s BTC client.
Follows the network/bitcoin.py / network/electrumx.py house style: a
client-owned session, close() lifecycle, NetworkError on transport failure, and a
bounded response size. web3 is imported LAZILY so eth_wallet loads with no Ethereum
dependency installed — only constructing/using EthRpc requires web3 (a
Phase-3 network dependency), which is exactly when a live RPC endpoint is also needed.
This is the I/O layer; the security-critical preimage parsing is the pure
pyrxd.eth_wallet.secret.recover_secret() (offline-fuzzable, no web3).
- class pyrxd.eth_wallet.rpc.EthRpc[source]¶
Bases:
objectThin async wrapper over
AsyncWeb3for the handful of calls the leg needs.Construction requires web3 + an RPC URL; signing keys are NOT held here (the leg feeds raw bytes from
PrivateKeyMaterialto the signer at the call site).- property write_w3: Any¶
The web3 used to BUILD transactions. Identical to
w3here.It exists so the legs can name the difference between building a transaction (which has to happen against one endpoint) and reading state (which should not).
MultiSourceEthRpcmakesw3fatal precisely so an unconverted READ cannot quietly run single-source; the write sites saywrite_w3and mean it.
- async latest_block_timestamp()[source]¶
Head timestamp, for the claim-deadline guard (wants the LATEST any source admits to).
- Return type:
- async latest_block_timestamp_quorum()[source]¶
Same read; the multi-source class returns the quorum-th head instead. One endpoint has one answer, so all three accessors coincide here.
- Return type:
- async latest_block_timestamp_min()[source]¶
Same read; the name exists so the multi-source class can aggregate it the OTHER way for the staleness and refund-maturity guards. Identical here — one endpoint has one answer.
- Return type:
- async assert_chain()[source]¶
Fail-closed if the endpoint is not the chain this swap was negotiated for.
- Return type:
None
- async get_transaction_count(address, block='pending')[source]¶
Nonce for the sender. Defaults to
pendingso a freshly built tx does not collide with one still in the mempool.The
blockparameter exists so a caller can compare the two:pending != latestmeans this sender has transactions in flight. That is the difference between “the HTLC holds nothing” and “the HTLC holds nothing YET”, which a balance read alone cannot tell apart — see the resume guard inErc20HtlcLeg._push_and_bind().
- async fee_fields()[source]¶
EIP-1559 fee fields (maxFeePerGas / maxPriorityFeePerGas) from the node.
- Return type:
- async preflight(tx)[source]¶
eth_call the tx to detect a guaranteed revert BEFORE broadcasting.
Fails fast (raises
ValidationError) instead of burning gas on a tx the node will mine-and-revert (e.g. a premature refund, a bad preimage, an already-settled HTLC). A transport failure is aNetworkError. Strips gas/fee fields the node would reject in an eth_call.CONSERVATIVE CLASSIFICATION (red-team): a definite revert is recognised ONLY from web3’s TYPED contract-exception classes — an honest node raises ContractLogicError / ContractCustomError / ContractPanicError for a real revert (custom errors arrive as a 4-byte selector, e.g. NotYetExpired() -> 0x59912c06). We deliberately do NOT substring- match the error text: that string is RPC-controlled, so a lying node could stuff “revert” into a transport error to make us classify the HONEST taker refund (the only exit path) as a permanent ValidationError and abort it. An untyped failure is therefore treated as a retryable NetworkError — preflight is a gas-saving optimisation, not a safety gate, so under uncertainty we retry rather than permanently block the exit. A genuinely premature refund still reverts typed (NotYetExpired) on any honest node.
- Parameters:
tx (dict)
- Return type:
None
- async get_transaction_receipt(tx_hash)[source]¶
A single NON-BLOCKING receipt fetch (eth_getTransactionReceipt). Returns
Nonewhen the tx is not currently mined — pending, or reorg-orphaned back to the mempool — instead of blocking likewait_receipt()(a poller must never sleep inside one read). A transport failure is still aNetworkError(fail-closed).
- async finalized_block_number()[source]¶
Block number of the finalized consensus checkpoint (the reorg-safe tip).
SANITY-BOUNDED (red-team HIGH: single-source finality): a finalized value that exceeds the latest head from the SAME provider is incoherent (finalized is always <= head) and is rejected fail-closed — this catches a naive lying RPC that over-reports finalized to make a non-final claim look FINAL. It does NOT defend a fully-consistent malicious provider that lies about BOTH finalized and the canonical chain: for a real-value path a multi-source finality quorum is required (deferred; documented in claim_finality_verdict).
- Return type:
- async block_number()[source]¶
The current
latesthead block number (eth_blockNumber). Used alongsidefinalized_block_number()to feed the across-time PoS finality-stall tracker the(head, finalized)pair (a frozenfinalizedwhile the head climbs = a stall).- Return type:
- async canonical_block_hash(block_number)[source]¶
The canonical block hash at
block_number(eth_getBlockByNumber). Used to bind a receipt’s claimed blockNumber to the canonical chain (red-team HIGH: receipt blockNumber on faith) — a fabricated receipt height is caught when its blockHash != the canonical hash.
- async get_logs(*, address, topics=None, from_block='earliest', to_block='latest')[source]¶
eth_getLogs for ONE contract address, optionally filtered by
topics. READ-ONLY.Scoped to a single address (the per-swap-unique HTLC), so the result is that contract’s own event history — a handful of entries. Pass an int
from_block(e.g. the deploy block) to bound the scan;to_block="latest"catches a JUST-mined claim. Detection deliberately reads tolatest, notfinalized: a watchtower must not MISS a fresh claim it has to race, and reorg-safety is asserted SEPARATELY by the finalized-checkpoint verdict (a non-final log can only ever cause a false PAGE, never a broadcast). Transport failure →NetworkError; the entry count is bounded (a pathological return must not OOM the tower).
HTLC contract leg¶
EthHtlcContractLeg — the web3-backed ETH counter-chain leg.
Implements the counter-chain leg surface (deploy/claim/refund/recover-secret/is-final)
for native-ETH HTLC swaps. This is the I/O-bearing layer; the security-critical preimage
recovery is the pure pyrxd.eth_wallet.secret.recover_secret(), and the durable
state is EthHtlcLocator.
DESIGNED-AND-UNPROVEN until the Sepolia end-to-end proof (Phase 4). web3 is imported
lazily, so this module loads without the Ethereum stack; only the network-touching
methods require it. The Phase-6 CounterChainLeg ABC will reconcile method names with
the BTC leg; until then this exposes ETH-native names and is driven by the Phase-4
Sepolia harness (mirroring how the BTC leg was first proven by its own spike driver).
Key handling (HARD): the signing key is PrivateKeyMaterial; its raw bytes are
fed to the signer at the call site and never persisted as an eth_account object.
- Security gates enforced here (off-chain, per the security review):
pre-fund:
eth_getCoderuntime-bytecode == the committed artifact’s, the contract immutables (hashlock/claimant/refundee/timeout) == negotiated, and the funded balance >= the negotiated amount. Run inside the funder’s ownfund(), and again on the maker’s side before the maker reveals p (the maker’s RXD lock precedes both). The balance is a LOWER BOUND on purpose — anyone can force-send wei to a contract (selfdestruct/coinbase), so an== expectedcheck is griefable into a permanent verify failure; over-funding is safe because claim/refund sweep the whole balance to the winner. This line said “== negotiated amount” until 2026-09-03; the check has beenbal < expected_amount_weisince the red-team LOW that introduced it, andEthHtlcContractLeg.verify_funded()documents it correctly at the check itself.EOA-only claimant/refundee (a recipient contract that reverts on receive would lock funds via the contract’s
require(ok)).
- class pyrxd.eth_wallet.htlc_leg.EthHtlcContractLeg[source]¶
Bases:
objectNative-ETH HTLC counter-chain leg (Sepolia-first).
- Parameters:
rpc – An
pyrxd.eth_wallet.rpc.EthRpc(web3-backed).signing_key –
PrivateKeyMaterialfor the EOA that sends txs (taker for fund/refund, maker for claim — separate leg instances per role).chain_id – EIP-155 chain id; must match
rpc’s endpoint (asserted at use).artifact – The EthHtlc contract artifact dict (
abi+bytecode+runtime_bytecode), owned and INJECTED by the deploying application (its audited Foundry build output). Useload_artifact()to read it from disk. pyrxd ships no contract bytecode of its own.
- __init__(*, rpc, signing_key, chain_id, artifact, private_submitter=None)[source]¶
- Parameters:
rpc (Any)
signing_key (PrivateKeyMaterial)
chain_id (int)
artifact (dict)
private_submitter (Any)
- Return type:
None
- recover_secret(artifacts, hashlock)[source]¶
Recover
pfrom claim calldata + event-log data (pure; see secret.py).
- async verify_funded(locator, *, expected_amount_wei, block_identifier=None, allow_delegated_eoa_recipients=False)[source]¶
Pre-RXD-lock gate: the on-chain contract matches the negotiated terms.
Fail-closed checks (any mismatch raises; the taker does NOT tell the maker to lock RXD):
chain id matches;
deployed runtime logic == the committed artifact’s (immutable slots masked — no attacker contract / no modified logic);
the contract IMMUTABLES (hashlock/claimant/refundee/timeout) read back via the getters == the negotiated terms in the locator (the meaningful binding check — proves the contract releases on the right secret to the right party at the right time);
claimant and refundee are EOAs (empty code) — a contract recipient that reverts on
receivewould brick claim/refund via the contract’srequire(ok);funded balance >= expected amount (a LOWER bound — see the comment at the check) (no underfunded contract).
block_identifier(red-team HIGH TOCTOU): pin EVERY read to one block. The taker’s fund-time self-verify reads ‘latest’ (None). The MAKER’s pre-lock re-verify passes'finalized'so a reorg cannot re-deploy a DIFFERENT contract at the same CREATE address (EVM addresses are (deployer,nonce)-derived) between verify and the RXD lock — a finalized deploy is non-reorgable. All getters + get_code + get_balance honour it.
- async fund(*, hashlock, claimant, refundee, timeout, amount_wei, on_deploy=None, resume_from=None, push_nonce=None, on_push_nonce=None)[source]¶
Deploy + fund the HTLC (payable constructor). Returns the locator ONLY after the deploy tx confirms with status==1 (a reverted/dropped deploy never yields a ‘funded’ locator). The TAKER calls this; claimant=maker, refundee=taker.
- async claim(locator, preimage)[source]¶
Maker: call claim(preimage); returns the tx hash. On MAINNET the maker SHOULD use private inclusion (Flashbots) — the public mempool exposes p before mining, letting the taker claim RXD while this ETH claim is still reorg-able.
Routed through the injected
private_submitterwhen one is supplied (private=True); otherwise it goes to the public mempool (the privacy property is then NOT provided — the operator opted out by not injecting a submitter).PREIMAGE-LEAK FIX (red-team MEDIUM): the off-chain
eth_callpreflight sends the claim calldata — which CONTAINS p — to the (public) RPC, defeating private inclusion before the private submit even runs. So when a private submitter IS injected we SKIP the preflight (the whole point is that p must not touch the public RPC); the on-chain revert protection the preflight gave is traded for p-privacy, which is the correct trade for the reveal tx. On the public-fallback path (no submitter) p goes public anyway, so the preflight stays.
- async refund(locator)[source]¶
Taker: call refund() after timeout; returns the tx hash. Taker-unilateral (no maker signature; the contract pays the immutable refundee).
P3 maturity pre-check: the contract’s
refund()reverts untilblock.timestamp >= timeout. We read the latest block timestamp and refuse a premature refund with a clear “matures at unix T, now S” message rather than burning gas on a guaranteed revert. Unlike the BTC/RXD CSV legs, a premature ETH refund is a deterministic on-chain revert (never a silent mempool strand), so this is a gas/clarity guard — the contract stays the source of truth for the deadline.- Parameters:
locator (EthHtlcLocator)
- Return type:
- async fetch_claim_artifacts(tx_hash)[source]¶
Fetch the candidate byte blobs for recover_secret: the tx INPUT calldata + the DATA of every log in the receipt. Works on a reverted-but-mined tx too (calldata is still present). Pure recover_secret(…) then matches by sha256==H.
SIZE-BOUNDED (red-team LOW): recover_secret does an O(n) sliding-window sha256 scan, so a malicious RPC returning attacker-sized calldata/log data is a CPU/memory DoS. A legitimate
claim(bytes32)calldata +Claimed(bytes32)log are ~tens of bytes; we cap each blob and the aggregate well above that and fail closed past the cap rather than scan unbounded.
- async is_final(tx_hash)[source]¶
True once the tx’s block is at/under the finalized checkpoint. The taker must NOT mark the swap COMPLETED (RXD claim irreversible) until the ETH claim is FINAL, since a pre-finality reorg could un-mine it.
- async claim_finality_verdict(tx_hash)[source]¶
The POINT-IN-TIME counter-leg finality verdict for the maker’s ETH claim, from the post-Merge
finalizedcheckpoint (NOT a confirmation depth — seeCounterClaimFinality):the claim’s block is at/under
finalized→FINAL;otherwise (not yet finalized, OR reverted/dropped
status != 1) →NOT_YET_FINAL_LIVE.
This is a stateless single observation: it never emits
COUNTER_CHAIN_NOT_FINALIZING. That verdict means the chain is not advancing finalization, which can only be judged across time (post-Mergefinalizedadvances at epoch boundaries, ~6.4 min, so a single non-advance is normal, not a stall). Detecting a genuine non-finality stall — finalized stuck for ≥ a patience window of epochs — is the coordinator’s polling-loop responsibility (Phase-3 wiring), not this point-in-time producer, which would otherwise false-positive on any fast poll. ETH finality is not a depth, so the verdict carries noconfirmations/required_depth.MALICIOUS-RPC HARDENING (red-team HIGH, single-source finality):
finalized_block_numberrejects a finalized > head over-report, and we bind the receipt’sblockHashto the CANONICAL block atblockNumber(a fabricated receipt height is caught when its hash != the canonical hash) — so a naive lying RPC cannot make a non-final claim read FINAL. This does NOT defend a fully-consistent malicious provider (one that lies coherently about the whole chain): a real-value path MUST use a multi-source finality quorum (≥2 independent providers must agree the claim is final). That quorum is DEFERRED to the audit-gated real-value track; the dust/pre-audit path accepts a single trusted provider.- Parameters:
tx_hash (str)
- Return type:
CounterClaimFinality
- async assert_claim_provenance(tx_hash, *, contract_address, preimage)[source]¶
Provenance gate (R6): the maker’s claim tx MUST target THIS swap’s HTLC contract instance AND emit the revealed secret
pfrom it — the ETH analogue of the BTC “claim tx spends our funding outpoint” check (_assert_claim_tx_spends_our_htlc).Each swap deploys a FRESH HTLC contract at a unique CREATE address (recorded in the locator after
verify_funded()), so the contract address is per-swap-unique exactly like a BTC funding outpoint. The contract’s claim path emitsClaimed(bytes32 preimage)with the SECRETpin the (non-indexed) log data. This leg targets the PER-SWAP-deployEthHtlc.solmodel (one fresh contract per swap,claim(bytes32 preimage)+ immutable hashlock/claimant/refundee/timeout getters thatverify_funded()reads back). NB the sibling repo’s canonicalHashedTimelock.solis a DIFFERENT shared-multi-swap model (claim(bytes32 swapId, bytes32 preimage), no per-swap immutables) NOT compatible with this leg — Phase 4 must inject anEthHtlc.sol-shaped artifact and reconcile/pin the exact event selector (keccak('Claimed(bytes32)')) rather than the current ABI-free p-in-log match.recover_secretmatchessha256(p)==Hover the supplied tx but TRUSTS that the tx belongs to this swap; we verify that here, fail-closed (NOT viatx.to— that rejected legitimate claims routed through a smart-contract wallet / multicall, red-team MEDIUM — but via the strictly-stronger log-emitter binding):receipt.status == 1— the claim actually succeeded (the ETH moved; a reverted tx never paid the maker even ifpsits in its calldata);a log emitted BY
contract_addresswhose data carries the SECRETp— the on-chainClaimed(p)event. We bind top, NOT the public hashlockH:His negotiated openly and reused on both legs (so anH-match adds no authenticity), and the deployed contract NEVER re-emitsH(it is a constructor immutable) — anH-in-log gate would reject every legitimate claim.pis secret until the maker reveals it, sopappearing in a log from our unique contract is a genuine, swap-specific proof of a real claim on it.
preimageis the value the coordinator already recovered viascrape_secretand re-verifiedsha256(p)==H, so passing it here adds no trust assumption. Any RPC error propagates and aborts the claim — also fail-closed. The redundant receipt read vsfetch_claim_artifacts()is deliberate (correctness over a saved round-trip).
- pyrxd.eth_wallet.htlc_leg.create_address(sender, nonce)[source]¶
The CREATE address for
senderatnonce—keccak(rlp([sender, nonce]))[12:].Both inputs belong to the deployer, so a deploy receipt’s
contractAddresswas never something to be told. Minimal RLP: a 20-byte address is a0x94-prefixed string, and the nonce is either the empty string (0), a single byte below0x80, or a length-prefixed big-endian integer.DUPLICATED, deliberately and temporarily, with
erc20_leg._create_address: the token leg imports FROM this module, so importing back would be circular, and collapsing the two means editing that file. They are pinned to agree by a differential test rather than by a comment.
- pyrxd.eth_wallet.htlc_leg.load_artifact(path)[source]¶
Load an EthHtlc artifact (ABI + bytecode + runtime_bytecode) from
path.The contract artifact is owned by the DEPLOYING application (its audited Foundry build output), NOT shipped inside the pyrxd wheel — it is INJECTED into
EthHtlcContractLegvia its constructor so the wheel carries no contract bytecode and the audited artifact stays beside its contract source. This helper is a convenience for callers that have the artifact on disk; pass the resulting dict in.