pyrxd.hd — BIP-32/39/44 HD wallets¶
- class pyrxd.hd.AccountDescriptors[source]¶
Bases:
objectThe pair of ranged descriptors that fully describe one BIP44 account.
Both chains are always present. A watch-only import that takes only the receive descriptor silently under-reports the balance, because change outputs land on the internal chain and would never be scanned.
- __init__(receive, change, master_fingerprint, account_path)¶
- class pyrxd.hd.AddressRecord[source]¶
Bases:
objectAddressRecord(address: ‘str’, change: ‘int’, index: ‘int’, used: ‘bool’)
- __init__(address, change, index, used)¶
- class pyrxd.hd.DiscoveryHit[source]¶
Bases:
objectOne derived address that has on-chain history.
- __init__(coin_type, account, change, index, address, confirmed, unconfirmed)¶
- class pyrxd.hd.DiscoveryReport[source]¶
Bases:
objectResult of a multi-path scan.
- __init__(hits, scanned, total_confirmed, total_unconfirmed)¶
- hits: list[DiscoveryHit]¶
- class pyrxd.hd.HdWallet[source]¶
Bases:
objectBIP44 HD wallet for Radiant with gap-limit discovery and encrypted persistence.
- coin_type¶
BIP44 coin type (read-only property; back-store
_coin_typeis set at construction and never mutated). 512 is SLIP-0044 spec for Radiant (default, also Tangem); 0 matches Photonic and Electron-Radiant; 236 matches pre-#14 pyrxd. Persisted in the wallet file and validated on load. Read-only because mutating it post-construction would desync from the already-derived_xprvand silently route subsequent addresses to a different path (closes SEV-2 red-team finding).
- addresses¶
{path_key: AddressRecord}where path_key isf"{change}/{index}".- Type:
- __init__(_seed, account=0, _coin_type=<factory>, external_tip=0, internal_tip=0, addresses=<factory>)¶
- Parameters:
_seed (SecretBytes)
account (int)
_coin_type (int)
external_tip (int)
internal_tip (int)
addresses (dict[str, AddressRecord])
- Return type:
None
- property account_path: str¶
This wallet’s BIP44 account path, e.g.
m/44'/512'/0'.Single source of truth for the string several callers used to build inline. The
_xprvproperty derives from exactly this path.
- build_send_max_tx(triples, to_address, *, fee_rate=10000, allow_below_relay_floor=False, allow_overpay=False)[source]¶
Sweep all triples to to_address minus fee. No change output.
fee_rateis refused below Radiant’s effective relay floor unlessallow_below_relay_flooris set, and above the overpay ceiling unlessallow_overpayis — seebuild_send_tx(). A sweep has NO change output, so an unintended overpay leaves entirely with the miner, which is why the ceiling exists; it is also why the override has to be reachable.
- build_send_tx(triples, to_address, photons, *, fee_rate=10000, allow_below_relay_floor=False, allow_overpay=False, change_address=None)[source]¶
Build and sign a P2PKH transfer from HD UTXOs to to_address.
Pure offline operation. Mirrors
RxdWallet.build_send_tx()but accepts (utxo, address, privkey) triples so each input is signed by the correct HD-derived key.change_addressdefaults to the next unused internal index; callers can override (e.g. to keep change on the external chain for a single-address-style wallet).fee_rateis refused below Radiant’s effective relay floor unlessallow_below_relay_flooris set — the deliberate opt-out for regtest and chains you control. UnlikeRxdWallet, the rate arrives per CALL here, so this is where it has to be judged.allow_overpayis the mirror opt-out for a rate above the overpay ceiling. A ceiling with no reachable override is its own fund-safety bug: a caller who genuinely means a high rate would be refused outright, and Radiant has neither RBF nor CPFP, so a refusal during a timelock race costs the funds the ceiling was protecting.
- property coin_type: int¶
BIP44 coin type this wallet was constructed with. Read-only.
Read-only because mutating it post-construction would desync from the already-derived
_xprv; subsequent address derivations would still happen at the original path while the persisted JSON would advertise the new path. The__setattr__override blockswallet._coin_type = X; the property blockswallet.coin_type = X.
- async collect_spendable(client, *, strict=True)[source]¶
Return
(utxo, address, privkey)triples for every UTXO across known addresses.Address→key mapping is preserved so signing works correctly per UTXO.
Runs the gap-limit scan (
refresh()) first, every call. An address is read here only once it is markedused, only the scan marks it, and the mark is not something a caller can be trusted to have made: before this, every CLI spend command butwallet send/wallet sweepcollected from a wallet nobody had scanned, so a freshly created, funded wallet reported nothing to spend (#759). Scanning on every call, not once per wallet, is deliberate too: a command that spends twice (a mint’s commit, then its reveal) needs the change address the first spend just used. A scan that cannot read an address raisesNetworkErrorrather than reading it as unused (seerefresh()).strict(the default) raisesNetworkErrorwhen any address’s UTXO read fails, instead of returning what the other addresses answered. Almost every caller turns a short result into a claim about the whole wallet — “fund this wallet”, “no spendable UTXOs”, “not held by this wallet” — and after a failed read that claim is false:pyrxd markprinted “fund this wallet” whenget_utxosfailed for the one funded address.strict=Falsereturns the partial view, LOGS the failed reads and names them inSpendable.unread; it is for a caller that only needs enough (send(),pyrxd wallet send,pyrxd swap cancel), which must go throughraise_if_reads_failed()before it reports that what it read is not enough. The scan above fails closed either way.- Parameters:
client (ElectrumXClient)
strict (bool)
- Return type:
Spendable
- descriptors(*, checksum=False)[source]¶
Output-script descriptors for this account’s receive + change chains.
Watch-only safe: the descriptors embed the account xpub, never the xprv or the seed. Note that an xpub still discloses every address this wallet will ever derive on both chains — a larger privacy surface than handing out a single address.
checksum appends the BIP380 suffix. Off by default because Radiant Core rejects the checksummed form; see
pyrxd.hd.descriptor.- Parameters:
checksum (bool)
- Return type:
- classmethod from_mnemonic(mnemonic, passphrase='', account=0, coin_type=None, *, normalize=True)[source]¶
Create a fresh wallet from a BIP39 mnemonic.
- coin_type selects the BIP44 derivation path:
None(default) uses the module-level configured coin type (env varRXD_PY_SDK_BIP44_DERIVATION_PATH, or SLIP-0044’s 512 if unset).512is SLIP-0044 Radiant (also Tangem).0matches Photonic and Electron-Radiant — pass this when restoring a mnemonic from those wallets.236matches pre-#14 pyrxd wallets.
The chosen coin type is recorded on the wallet and persisted in the wallet file; subsequent
load()calls validate it.normalize controls BIP39 NFKD normalization — see
seed_from_mnemonic(). Leave itTrueunless you are recovering funds from a wallet created before 0.12.0 using a non-ASCII passphrase, which pyrxd then hashed unnormalized. Wrong for every other case: it derives a wallet no other BIP39 implementation can reproduce.
- async get_balance(client, *, strict=False)[source]¶
Return total confirmed + unconfirmed satoshis across all known addresses.
Uses
ElectrumXClient.get_balanceper address. Callrefresh()first to ensure the address set is current.A per-address read that fails is logged and contributes zero — so the total is a LOWER BOUND, not a balance. Pass
strict=Truewhen the number is being shown to somebody or compared against a threshold.- Parameters:
client (ElectrumXClient)
strict (bool)
- Return type:
- async get_utxos(client, *, strict=False)[source]¶
Return all UTXOs across all known addresses.
A per-address read that fails is logged and contributes nothing; pass
strict=Trueto refuse a partial answer instead (see_read_per_address()).- Parameters:
client (ElectrumXClient)
strict (bool)
- Return type:
list[UtxoRecord]
- known_addresses(*, change=None)[source]¶
Return all known address records, optionally filtered by chain.
- Parameters:
change (int | None)
- Return type:
- classmethod load(path, mnemonic, passphrase='', coin_type=None, *, normalize=True)[source]¶
Load a previously saved wallet from path.
The mnemonic is needed to derive the decryption key. Raises
FileNotFoundErrorif path does not exist — a typo’d path will not silently produce an empty wallet that subsequently overwrites a real wallet on save. Callers that explicitly want the create-on-missing behavior should useload_or_create().coin_type (optional) is validated against the value persisted in the wallet file. A mismatch raises
ValidationError— this catches the silent-empty-wallet failure mode where a default change between pyrxd versions would otherwise have the loaded wallet derive at a different path than it was saved at. PassNone(default) to accept whatever was persisted.normalize controls BIP39 NFKD normalization of the mnemonic and passphrase — see
seed_from_mnemonic(). It matters here because the derived seed is also the wallet file’s AES-GCM decryption key: a wallet saved by pyrxd before 0.12.0 with a non-ASCII passphrase was encrypted under the old, unnormalized seed, and can only be decrypted by reproducing that seed withnormalize=False. Leave the defaultTruefor every other case. Loading never guesses the mode: a GCM failure raises rather than silently retrying with the other seed, so the legacy mode is only ever entered by explicit opt-in.
- classmethod load_or_create(path, mnemonic, passphrase='', account=0, coin_type=None, *, normalize=True)[source]¶
Load a wallet from path, or build a fresh one if the file is missing.
Spelled separately from
load()so the create-on-missing intent is explicit at the call site. A common safety failure with the old single-load API was that a typo in path would produce an empty wallet that subsequently overwrote the real wallet on save.coin_type applies to both branches: when loading, it is validated against the persisted value; when creating, it is the coin type the new wallet uses.
normalize also applies to the load branch — see
load()for why it matters there (the seed doubles as the wallet file’s decryption key).Falseis a fund-recovery escape for pre-0.12.0 wallets with non-ASCII passphrases.- Raises:
ValidationError – if path does not exist and
normalize=False. The legacy seed mode exists solely to reach funds already held under a pre-0.12.0 wallet; there is nothing to recover at a path that has no wallet on it. Creating one there instead would mint a brand-new, permanently non-conformant wallet whose mnemonic no other BIP39 implementation can restore — and the likeliest way to land in that branch is a typo in path, which is exactly the failureload_or_createwas split out to make visible.- Parameters:
- Return type:
- master_fingerprint()[source]¶
The BIP32 master key fingerprint:
hash160(master pubkey)[:4].This is what an output-script descriptor’s key-origin field wants, and it is NOT
account_xpub().fingerprint— that attribute is the parent fingerprint (payload bytes 5:9), i.e. the fingerprint ofm/44'/<coin>', one level up. The two values differ for every account at depth > 1.The distinction matters because using the parent fingerprint produces a descriptor that still derives the correct addresses, so nothing appears broken — but it misidentifies the key’s origin, and any consumer that later tries to match the descriptor to a signing device (or to another descriptor from the same seed) will fail to.
Public (no private material leaves): the return value is a truncated hash of a public key.
- Return type:
- next_receive_address()[source]¶
Return the first external (change=0) address with no recorded history.
- Return type:
- privkey_for(change, index)[source]¶
Derive the signing key at
change/index(public seam over_privkey_for).
- privkey_for_address(address)[source]¶
Derive the signing key for an address this wallet derives.
The path is looked up in
self.addressesfirst. An address not recorded there is searched for locally, with no network: both chains, from index 0 to_GAP_LIMITpast the highest index the wallet knows on that chain. A wallet file records only what something saved, andpyrxd wallet newsaves before it derives a single address, so without the search a new wallet could not find the key for its own first receive address. On regtest that stranded a new wallet’s Glyph commit:glyph resume-mintrefused to reveal it.Added for
pyrxd.glyph.mint.GlyphMinter, which must re-derive the key that spends a Glyph commit output after a crash. It deliberately does not persist the key, only the funding address, so it needs address → key. Keeping that lookup here also keeps the minter’s wallet contract down to two methods (collect_spendable()and this one), which is what makes it practical to drive the minter with a non-HD wallet in a test or a dev script.- Raises:
ValidationError – if the address is not one this wallet derives within that window — the caller has the wrong wallet (or an address past the gap limit), and signing with a key that hashes to a different PKH would produce a transaction the network rejects.
- Parameters:
address (str)
- Return type:
PrivateKey
- async refresh(client)[source]¶
Run BIP44 gap-limit scan on both external and internal chains.
Discovers which derived addresses have on-chain history. Stops after
_GAP_LIMIT(20) consecutive unused addresses per chain.Network errors (a transient ElectrumX outage, a server hangup mid-scan) propagate to the caller as
NetworkError— previously they were silently treated as “address unused”, which made a funded wallet look empty after a flaky lookup.An address already recorded as used is never marked unused by a scan, even when this server reports no history for it: a server that lags or indexes partially does not get to hide funds the wallet already knew about.
An address counts as used when its P2PKH script hash has history, or, when that is empty, when any of its token script hashes does (
_has_token_history()). A Radiant ElectrumX lists a token output under the token script’s hash with its refs zeroed, not under the owner’s P2PKH hash, so an address that has only received a Glyph token has no P2PKH history at all (#787).Returns the count of newly discovered used addresses.
- Parameters:
client (ElectrumXClient)
- Return type:
- save(path)[source]¶
Encrypt and atomically save wallet state to path.
Atomicity & permissions¶
- Writes via mkstemp + fchmod(0o600) + fsync + os.replace, so:
The file is never visible at a wider mode than 0o600 — the mode is set on the fd before any bytes are written.
A crash mid-write cannot leave a half-encrypted blob in place — either the old file remains, or the new fully-fsynced file does.
Encryption¶
AES-256-GCM under a key derived from the BIP39 seed via scrypt with a per-file random salt. Tampering with the ciphertext breaks the GCM tag —
load()raises rather than returning attacker-shaped JSON.- Parameters:
path (Path)
- Return type:
None
- async send(client, to_address, photons, *, fee_rate=10000, allow_below_relay_floor=False, allow_overpay=False, change_address=None)[source]¶
Fetch UTXOs, build, sign, broadcast. Returns broadcast txid.
Raises
ValidationErroron bad inputs or insufficient funds,NetworkErroron RPC failure.Collects with
strict=False: an amount send needs enough, not all, so an address whose UTXO read failed is logged and skipped rather than refusing a send the other addresses can fund. The gap-limit scan before it still fails closed. When what was read is short AND a read failed, it raisesNetworkErrorrather than calling that insufficient funds (raise_if_reads_failed(), insidebuild_send_tx()).
- async send_max(client, to_address, *, fee_rate=10000, allow_below_relay_floor=False, allow_overpay=False)[source]¶
Sweep all UTXOs to to_address minus fee. Returns broadcast txid.
Collection is
strict: “sweep everything” is a completeness claim, and a sweep built from a view that silently lost an address moves most of the funds while reporting that it moved all of them. RaisesNetworkErrorif any per-address read failed — nothing is broadcast, and a retry (or a different endpoint) sweeps the whole set. Usesend()for an amount, which does not make that claim.
- zeroize()[source]¶
Scrub the seed and mark the wallet dead; it cannot derive or sign after.
Hardening #8/H1: the account xprv is NO LONGER stored long-lived — the
_xprvproperty re-derives it transiently from the seed per operation — so the ONLY resident long-lived secret is this 64-byte seed, which lives in aSecretBytesand IS memset here. Setting_zeroed(matchingSecretBytes._zeroed) makes the_xprvproperty fail closed (rather than silently re-deriving a garbage key from the now-zeroed seed). Any account-xprv copies that existed only during an in-flight derivation are short-lived locals (GC-eligible immediately, never held across the unlock window); their residency until the pages are reused is bounded by the agent’s best-effort process hygiene (mlock/PR_SET_DUMPABLE 0/ no core dumps), NOT a guaranteed erase — do not over-state it as “erased”.- Return type:
None
- addresses: dict[str, AddressRecord]¶
- class pyrxd.hd.WordList[source]¶
Bases:
objectBIP39 word list
- files: dict[str, str] = {'en': '/home/runner/work/pyrxd/pyrxd/src/pyrxd/hd/wordlist/english.txt', 'zh-cn': '/home/runner/work/pyrxd/pyrxd/src/pyrxd/hd/wordlist/chinese_simplified.txt'}¶
- path = '/home/runner/work/pyrxd/pyrxd/src/pyrxd/hd/wordlist'¶
- class pyrxd.hd.Xkey[source]¶
Bases:
object[ : 4] prefix [ 4: 5] depth [ 5: 9] parent public key fingerprint [ 9:13] child index [13:45] chain code [45:78] key (private/public)
- class pyrxd.hd.Xprv[source]¶
Bases:
Xkey- classmethod from_seed(seed, network=Network.MAINNET)[source]¶
derive master extended private key from seed
- pyrxd.hd.account_descriptors(xpub, *, master_fingerprint, account_path, checksum=False)[source]¶
Build the receive + change descriptor pair for a BIP44 account.
- pyrxd.hd.append_checksum(descriptor)[source]¶
Return descriptor with its BIP380
#checksumsuffix appended.Radiant Core rejects the result — see the module docstring. Use this only for Bitcoin-Core-lineage consumers.
- pyrxd.hd.bip32_derive_xkeys_from_xkey(xkey, index_start, index_end, path='m/', change=0)[source]¶
Derive a range of extended keys from Xprv and Xpub keys using BIP32 path structure.
- pyrxd.hd.bip32_derive_xprv_from_mnemonic(mnemonic, lang='en', passphrase='', prefix='mnemonic', path='m/', network=Network.MAINNET, *, normalize=True)[source]¶
Derive the subtree root extended private key from mnemonic and path.
- pyrxd.hd.bip32_derive_xprvs_from_mnemonic(mnemonic, index_start, index_end, lang='en', passphrase='', prefix='mnemonic', path='m/', change=0, network=Network.MAINNET, *, normalize=True)[source]¶
Derive a range of extended keys from a nmemonic using BIP32 format
- pyrxd.hd.bip44_derive_xprv_from_mnemonic(mnemonic, lang='en', passphrase='', prefix='mnemonic', path="m/44'/512'/0'", network=Network.MAINNET, *, normalize=True)[source]¶
Derives extended private key using BIP44 format- it is a subset of BIP32. Inherits from BIP32, only changing the default path value.
- pyrxd.hd.bip44_derive_xprvs_from_mnemonic(mnemonic, index_start, index_end, lang='en', passphrase='', prefix='mnemonic', path="m/44'/512'/0'", change=0, network=Network.MAINNET, *, normalize=True)[source]¶
Derive a range of extended keys from a nmemonic using BIP44 format
- pyrxd.hd.ckd(xkey, path)[source]¶
ckd = “Child Key Derivation” derive an extended key according to path like “m/44’/512’/1’/0/10” (absolute) or “./0/10” (relative)
512 is Radiant’s SLIP-0044 coin type and is what
pyrxd.constants.BIP44_DERIVATION_PATHuses. The examples here used to show coin type 0, which is BITCOIN’s — following them derives a wallet whose addresses are not the ones Photonic >= v3.0.0 or Tangem will show for the same mnemonic. Coin types 0 (Photonic <= v2.x, Electron-Radiant, Chainbow) and 236 (pre-#14 pyrxd) are also in use in the Radiant ecosystem for historical reasons — seepyrxd.hd.discovery, which scans all three — but 512 is the one to derive NEW wallets at.
- pyrxd.hd.coin_type_label(coin_type)[source]¶
Return a human label for coin_type, or a generic note if unknown.
- pyrxd.hd.derive_xkeys_from_xkey(xkey, index_start, index_end, change=0)[source]¶
- [DEPRECATED] Use bip32_derive_xkeys_from_xkey instead.
This function name is kept for backward compatibility.
- pyrxd.hd.derive_xprv_from_mnemonic(mnemonic, lang='en', passphrase='', prefix='mnemonic', path="m/44'/512'/0'", network=Network.MAINNET, *, normalize=True)[source]¶
- [DEPRECATED] Use bip44_derive_xprv_from_mnemonic instead.
This function name is kept for backward compatibility.
- pyrxd.hd.derive_xprvs_from_mnemonic(mnemonic, index_start, index_end, lang='en', passphrase='', prefix='mnemonic', path="m/44'/512'/0'", change=0, network=Network.MAINNET, *, normalize=True)[source]¶
- [DEPRECATED] Use bip44_derive_xprvs_from_mnemonic instead.
This function name is kept for backward compatibility.
- pyrxd.hd.descriptor_checksum(descriptor)[source]¶
Return the 8-character BIP380 checksum for descriptor (no
#).- Raises:
ValidationError – if descriptor already carries a checksum, or contains a character outside
INPUT_CHARSET.- Parameters:
descriptor (str)
- Return type:
- async pyrxd.hd.discover(client, mnemonic, *, passphrase='', coin_types=(0, 512, 236), accounts=(0, 1, 2), normalize=True)[source]¶
Scan
coin_types x accountsfor derived addresses with on-chain history.For each
(coin_type, account)pair, builds the account wallet, runs the standard BIP44 gap-limit scan (gap 20, both chains) viaHdWallet.refresh(), and records every used address together with its confirmed/unconfirmed balance and full derivation path.normalize controls BIP39 NFKD normalization of the mnemonic and passphrase — see
seed_from_mnemonic(). A scan with the defaultTruederives the spec-conformant seed and therefore cannot see funds sitting on a seed pyrxd derived before 0.12.0 from a non-ASCII passphrase; passnormalize=Falseto scan that legacy seed’s paths instead. Recovery-only — never for new wallets.mnemonic is used only to derive keys locally; it is never sent to the server. Only derived addresses (as scripthashes) reach the network.
Raises whatever
HdWallet.refresh()/ElectrumXClient.get_balance()raise on network failure — a partial scan is not silently reported as empty. The caller decides how to surface an aborted scan.Returns a
DiscoveryReport;report.foundisFalsewhen no scanned path had any history (the caller should then suggest widening the ranges or supplying the funded address directly).- Parameters:
- Return type:
- pyrxd.hd.key_origin(master_fingerprint, path)[source]¶
Build the descriptor key-origin field, e.g.
[1a2b3c4d/44h/512h/0h].master_fingerprint must be the master key’s fingerprint —
hash160(master_pubkey)[:4]for the key atm— NOT the parent fingerprint stored inside the account xpub. Those differ for any account at depth > 1, and getting it wrong yields a descriptor that misidentifies its own origin: it still derives the right addresses, so nothing looks broken, but any consumer trying to match it to a signing device will fail to.pyrxd.hd.wallet.HdWallet.master_fingerprint()returns the right value.- Raises:
ValidationError – if master_fingerprint is not exactly 4 bytes.
- Parameters:
- Return type:
- pyrxd.hd.pkh_descriptor(xpub, *, master_fingerprint, account_path, chain, checksum=False)[source]¶
Build a ranged
pkh()descriptor for one BIP44 chain.pkh([1a2b3c4d/44h/512h/0h]xpub6.../0/*)xpub is the account-level extended public key at account_path. chain is
EXTERNAL_CHAIN(0, receive) orINTERNAL_CHAIN(1, change). The trailing/*makes it a ranged descriptor, so the consumer walks indices itself.checksum appends the BIP380 suffix. Defaults to False because Radiant Core rejects the checksummed form — see the module docstring.
- pyrxd.hd.seed_from_mnemonic(mnemonic, lang='en', passphrase='', prefix='mnemonic', *, normalize=True)[source]¶
Derive the 64-byte BIP39 seed from a mnemonic (+ optional passphrase).
BIP39 requires the mnemonic sentence and the passphrase to be NFKD normalized before they enter PBKDF2. Without it, two spellings that a user cannot tell apart – “café” with a precomposed U+00E9 versus the same word as “e” + combining U+0301 – derive different seeds, and therefore entirely different wallets.
- Parameters:
normalize (bool) – Leave
True(the default) for spec-conformant, cross-wallet-compatible seeds. PassFalseonly to reproduce the non-conformant seed pyrxd produced before 0.12.0, which is the recovery path for anyone who funded a wallet using a non-ASCII passphrase under the old behavior. It is not interoperable with any other BIP39 implementation – seedocs/how-to/recover-funds-across-wallet-paths.md.mnemonic (str)
lang (str)
passphrase (str)
prefix (str)
- Return type:
Note
normalize=Falseis inert for a passphrase that is already in NFKD form, which includes every pure-ASCII passphrase (the overwhelmingly common case) and both wordlists pyrxd ships. For those inputs the two modes return byte-identical seeds.
- pyrxd.hd.step_to_index(step)[source]¶
convert step (sub path) normal derivation or hardened derivation into child index
- pyrxd.hd.verify_checksum(descriptor)[source]¶
Return True if descriptor ends in a valid BIP380
#checksum.False for an unchecksummed descriptor, a malformed suffix, or a payload that does not match its checksum. Never raises — this is a predicate for validating untrusted input.
Exactly one
#is required.#is a member ofINPUT_CHARSET, so a doubly checksummed string such asraw(deadbeef)#89f8spxm#4x0avkn4polymods correctly and used to verify True here — while Bitcoin Core’sdescsum_checkrejects it, anddescriptor_checksum()already refuses to create one. Accepting what we will not emit, and what the consumer will not take, is the wrong half of that pair to be lenient in.