btclib_wallet.fetch package¶
Submodules¶
btclib_wallet.fetch.bitcoin_core integration module¶
The btclib fetcher backed by a BitcoinCoreRpcClient.
The client itself is the bitcoin-core-rpc package: zero dependencies of its own, nothing but the standard library behind it, and no part of btclib. What this module adds is the integration – the answers turned into btclib transactions, and the chain the node serves compared with the network those transactions are labelled for.
The names it re-exports are the package’s own, unchanged, so that from btclib_wallet.fetch.bitcoin_core import BitcoinCoreRpcClient keeps resolving. Their behaviour is the package’s too, exceptions included: a client.call reached that way raises bitcoin_core_rpc.RpcError, not btclib.exceptions.RpcError. The Fetcher interface is where btclib’s own exceptions are promised, and _call below is the line that makes it true.
- class btclib_wallet.fetch.bitcoin_core.BitcoinCoreFetcher(client: BitcoinCoreRpcClient, network: str = 'mainnet', *, verify_network: bool = True, signet_challenge: str | bytes | None = None, estimate_mode: str = 'economical')[source]¶
Bases:
NetworkVerifyingFetcherEvery Fetcher question, answered by a node over its RPC.
Also a Broadcaster: broadcast sends sendrawtransaction, which -rest – and so BitcoinCoreRestFetcher – has no equivalent of. Holding a fetcher is not broadcasting; a caller has to call the method, and broadcast’s own docstring is where its non-idempotence is stated, at the point a caller meets it.
Also a FeeEstimator: estimate_fee sends estimatesmartfee, which -rest has no equivalent of either – fee estimation is RPC only.
The client is a constructor argument rather than a set of connection arguments repeated here: one class owns the endpoint and credentials, this one owns the mapping onto btclib types, and a caller who already has a client does not build a second.
network is btclib’s chain label and belongs here, not to the connection: it is what the outputs of a fetched transaction are labelled with. The client knows a URL and no chain, so the label is a claim until the node is asked, and assert_network is the question.
signet_challenge is which signet, for the one label that names more than one chain: Core answers signet for the default signet and for every custom one alike, so a fetcher on a signet of its own passes the challenge and assert_network holds the node to it. Hex or the bytes it spells, as -signetchallenge takes it. It is what a custom signet needs from this class and the whole of it – the addresses of one are signet’s, NETWORKS describing the encoding and not the chain – so it is refused with a network that is no signet, and refused with verify_network off, either being a check that would not be made.
estimate_mode is estimatesmartfee’s own parameter, fixed at construction rather than taken per call: it is not part of FeeEstimator’s contract – Core’s alone among the three backends – so a caller who cares chooses it once, the way signet_challenge is chosen once, rather than a signature every estimate_fee call would otherwise have to widen for one backend’s option. Defaults to Core’s own default, economical.
- assert_network() None[source]¶
Raise unless the node serves the chain this fetcher labels with.
Worth the call, because the failure it catches is silent. A client built for a testnet node – an explicit url, no port default in the way – under a fetcher labelled mainnet renders a mainnet address for every output it fetches, for coins that are not there. getblockchaininfo answers that in one round trip; what it needs is a vocabulary to be compared through, which is chain_from_network, Core naming the chain main where btclib names it mainnet.
Signet is the case a name cannot settle: Core reports signet for the default one and for every custom one alike, so two nodes sharing nothing but the shape of a challenge answer the same string. The challenge is what tells them apart, and this fetcher’s is the constructor’s signet_challenge, or the default signet with none given.
Both comparisons are the client’s assert_chain, this method being the translation into btclib’s vocabulary and btclib’s exceptions: chain_from_network on the way in, client_errors on the way out. The check itself belongs beside the protocol it reads, and lived here only while bitcoin_core_rpc did not have it.
- broadcast(tx: Tx, *, maxfeerate: float | None = None) bytes[source]¶
Announce tx to the node, and return the txid it confirmed.
_verify_once is called first, as every other method here calls it – but a broadcast to a node on the wrong chain is the worst version of the silent failure verify_network exists to catch: a fetch answering with the wrong data is corrected on the next read, where a signed transaction fanned out to the wrong network’s peers cannot be called back.
sendrawtransaction with the wire serialization, witness included – what a peer relays and eventually mines, not the stripped form Tx.id hashes. maxfeerate is forwarded exactly as given, an absent one leaving the parameter out of the call rather than substituting a value of this method’s choosing: Core’s own default (DEFAULT_MAX_RAW_TX_FEE_RATE) refuses a fee a caller may have deliberately chosen, so a default written here would refuse it on the caller’s behalf.
The id is computed from tx before the request, the way tx_from_raw recomputes one from a fetched serialization: a success naming a different txid is a FetchError, the node having confirmed some other transaction. One request and no retry – after a timeout this method cannot tell a transaction that never reached the mempool from one that did and whose acknowledgement was merely lost on the way back, so trying again could announce the same signed spend twice for no information gained. That decision is the caller’s, made with whatever else it can ask the node.
- estimate_fee(target: int) FeeQuote[source]¶
Return a fee rate expected to confirm within target blocks.
estimatesmartfee, with self.estimate_mode as its second parameter. A reply naming no feerate – “only present if no errors were encountered” – is a decline and not a rate, raised as a FetchError carrying the node’s own errors, unreshaped. blocks is the target the node actually estimated for, clamped to at least 2 and at most its own maximum usable target, and is what the returned FeeQuote.target reports – not necessarily target itself.
- get_block_header(height: int) BlockHeader[source]¶
Return the header of the block at this height, checked on arrival.
Two calls, Core answering getblockheader by hash and not by height: getblockhash maps the height first, and getblockheader with verbosity false returns the serialization rather than a rendering of it – the same shape getrawtransaction answers get_tx with.
- get_tx(tx_id: bytes | str | bytearray | memoryview) Tx[source]¶
Return the transaction with this id.
getrawtransaction with no verbosity returns the serialization. Those bytes are what Tx.parse recomputes the id from, so a transaction arriving wrong announces itself. A node answers for a transaction in its mempool, one of its wallet’s, and – only with -txindex – any other. Without the index the error is RPC code -5.
- btclib_wallet.fetch.bitcoin_core.chain_from_network(network: str) Literal['main', 'test', 'testnet4', 'signet', 'regtest'][source]¶
Return Core’s chain name for one of the BIP network names.
Raises rather than passing an unrecognized name through, in both directions: a chain Core adds later is then a failure here, naming what it knows, instead of a string that reaches a node as a port lookup or a directory name.
- btclib_wallet.fetch.bitcoin_core.cookie_auth(cookie_path: Path) str[source]¶
Return the user:password bitcoind wrote in its cookie file.
One line, __cookie__: and 32 random bytes in hex, rewritten at every start of the node. Read at each call rather than once at construction: a client built when the node was up and used an hour later would otherwise answer 401 for the rest of the process, the node having been restarted in between, and the cost is one small local read against an HTTP round trip.
Ascii, one line and a bounded read, because a path that is not a cookie file is the ordinary mistake here and a credential is the one value that must not appear in the error reporting it: what the three checks buy is that a binary file, a log or something enormous arrives as a FetchError naming the file, rather than as a UnicodeDecodeError or as memory nobody agreed to.
A file that is not there is CookieNotFoundError, which is a FetchError too: it says the node wrote no cookie, where the rest say something is at the path and it is not a cookie.
btclib_wallet.fetch.bitcoin_core_rest integration module¶
The btclib fetcher backed by a BitcoinCoreRestClient.
Core’s -rest interface is off by default and authenticates nobody who reaches it – BitcoinCoreRestClient’s own docstring says so, and a node operator who turns it on knows that before btclib is reached for at all. This module is what a caller who has been handed such an endpoint, and no credentials, speaks to a real node with: BitcoinCoreFetcher needs a cookie file or a user and password, and every other backend here speaks to a block explorer rather than to a node.
The client’s own surface is two methods, get_bin and get_json, and no method per resource – path is the caller’s own, built from Core’s doc/REST-interface.md and appended after /rest unread. That is why this is a class of its own rather than EsploraFetcher with a second base_url: EsploraFetcher speaks HTTP itself and takes a base_url with no client in front of it, where this fetcher takes a client, the same shape BitcoinCoreFetcher takes a BitcoinCoreRpcClient in. One class cannot take both a base_url and a client without one of the two arguments being dead in every call.
Past the constructor this checks what BitcoinCoreFetcher checks, and takes the same arguments to do it: rest_chaininfo hands /chaininfo.json whatever getblockchaininfo answers, written out unmodified, so chain and signet_challenge are both members of it and verify_network and signet_challenge mean here exactly what they mean there. What differs is authentication – -rest needs no cookie and refuses nobody, which is what this class is for – and the endpoints the questions are asked at, none of which is shared with EsploraFetcher either: a transaction and a header are read as octets from a .bin endpoint, where Esplora answers text – hex, save for the chain tip’s height, which is a decimal number – and the tip’s height and hash are members of a /chaininfo.json reply rather than endpoints of their own.
- class btclib_wallet.fetch.bitcoin_core_rest.BitcoinCoreRestFetcher(client: BitcoinCoreRestClient, network: str = 'mainnet', *, verify_network: bool = True, signet_challenge: str | bytes | None = None)[source]¶
Bases:
NetworkVerifyingFetcherEvery Fetcher question, answered by a node over -rest.
The client is a constructor argument rather than connection arguments repeated here, the same reason BitcoinCoreFetcher takes a BitcoinCoreRpcClient: one class owns the endpoint, this one owns the mapping onto btclib types.
network labels the outputs get_tx returns, and assert_network holds the node to it.
signet_challenge is which signet, and means what it means on BitcoinCoreFetcher: Core answers signet for the default signet and for every custom one alike, so the name settles nothing and the magic the challenge derives is the identity. Both questions are answered by /chaininfo.json, the one document get_block_count already reads.
get_tx_out is not overridden, and stays the Fetcher base’s derivation from get_tx. BitcoinCoreRestClient’s own docstring is where the reason is argued – /getutxos reads the UTXO set, so a spent output and one that never existed answer it identically – and is not repeated here.
- assert_network() None[source]¶
Raise unless the node serves the chain, and the signet, this labels.
One request, /chaininfo.json. rest_chaininfo writes out what getblockchaininfo answers and adds nothing, so the members read here are the ones BitcoinCoreRpcClient.assert_chain reads over the JSON-RPC server, and the two comparisons are its two: chain against chain_from_network(self.network), Core naming the chain main where btclib names it mainnet; then, on signet alone, the magic signet_challenge derives, since Core answers signet for every signet and the name therefore separates none of them.
A node too old to report its challenge answers a reply this cannot read, so a FetchError: one it cannot answer for, and not a pass.
- get_block_header(height: int) BlockHeader[source]¶
Return the header of the block at this height, checked on arrival.
Two calls, -rest answering /headers by hash and not by height: /blockhashbyheight/<HEIGHT>.bin maps the height first, and /headers/<BLOCK-HASH>.bin?count=1 then answers the serialization of exactly one header – the query parameter and not /headers/<COUNT>/<BLOCK-HASH>, which doc/REST-interface.md marks deprecated, though not removed, since Core 24.0.
/blockhashbyheight’s .bin answers Core’s internal byte order, the reverse of the display order every hash in a -rest url is – uint256::GetHex(), which FromHex inverts, is documented as reversing that internal order for exactly this reason. Reversed here before the hex is built, the same reversal BlockHeader.parse performs on a header’s own hash fields on the way in.
- get_tx(tx_id: bytes | str | bytearray | memoryview) Tx[source]¶
Return the transaction with this id.
/tx/<TX-HASH>.bin returns the serialization, which tx_from_raw recomputes the id from – the same check BitcoinCoreFetcher and EsploraFetcher both perform, and the same reason a node answers for a transaction in its mempool, and for any other only with -txindex.
btclib_wallet.fetch.broadcaster module¶
The interface a chain backend answers to announce a transaction.
Kept apart from Fetcher: broadcasting is a write and not every backend that answers Fetcher’s questions can perform one – BitcoinCoreRestFetcher, over Core’s read-only -rest interface, is exactly that backend, bitcoin-core-rpc’s BitcoinCoreRestClient declaring no method that writes. An ABC method here would force a choice between a NotImplementedError on a class that never promised the capability and a signature every backend has to carry whether or not it can honour it; a Protocol lets BitcoinCoreFetcher and EsploraFetcher satisfy Broadcaster structurally, with nothing to say about BitcoinCoreRestFetcher at all.
Not runtime_checkable. The contract below – the txid check, the single request – is not what isinstance(x, Broadcaster) would verify, only whether something named broadcast exists; marking the protocol checkable would invite exactly that shortcut in place of reading the contract.
- class btclib_wallet.fetch.broadcaster.Broadcaster(*args, **kwargs)[source]¶
Bases:
ProtocolA backend able to announce a signed transaction to the network.
One method and one contract, binding on every implementation of it:
the txid is computed from tx before the request is sent, and a success naming any other txid is refused as a FetchError – the backend answered for a transaction that is not the one it was asked to broadcast;
one request, no retry. After a timeout there is no way to tell a transaction that never reached the backend from one that did and whose acknowledgement was merely lost in transit, so retrying could announce the same signed spend twice for no information gained; whether to try again, and how, is the caller’s decision, made with whatever else it can ask the backend – this is bitcoin-core-rpc’s own one-call contract, carried across;
a refusal keeps its reason. Whatever the backend answered – Core’s RpcError code and message, an explorer’s error body – it reaches the caller through client_errors, text intact and not reshaped into a code common to every backend, because the two refuse for different reasons and a caller acts on the one it got.
btclib_wallet.fetch.decorators module¶
Fetchers that answer from another Fetcher, not from a network.
CachingFetcher composes one backend and remembers what it answered; FallbackFetcher composes several and tries them in order. Both are Fetcher subclasses in their own right – calling code takes a Fetcher and does not learn that either is standing in front of something else.
Nesting order is not free to choose either way. CachingFetcher(FallbackFetcher([a, b])) is the recommended shape: one cache shared across the failover, so a backend going down and the next one taking over does not re-fetch what is already held. FallbackFetcher([CachingFetcher(a), CachingFetcher(b)]) builds two caches that each learn the same transactions separately – twice the memory for the overlap, and a fetch that fails over to b still pays for what a’s cache already knew.
- class btclib_wallet.fetch.decorators.CachingFetcher(fetcher: Fetcher, max_size: int | None = None)[source]¶
Bases:
FetcherA Fetcher that remembers get_tx and get_block_header answers.
Composes a Fetcher rather than subclassing one: the wrapped fetcher is an attribute, fetcher, and any backend satisfies the interface without knowing it is being cached. get_tx_out is not overridden here, and that is what makes the caching pay off rather than being one more place to keep in step – Fetcher.get_tx_out is concrete and calls self.get_tx, so caching get_tx amortizes it for free: twenty outpoints spending one transaction cost one fetch of it. The consequence is that a wrapped backend which overrides get_tx_out with an answer of its own has that override bypassed – this class’s get_tx_out is the ABC’s, inherited, and never reaches the wrapped fetcher’s. No backend here overrides it, so this only matters for a third-party backend.
What is cached, and for how long, differs per method:
get_tx is cached with no expiry. The id is a hash of the bytes, so the answer cannot change without the question changing.
get_block_header is cached with no expiry too, and that has a consequence worth stating rather than leaving implicit: a height is not an identity, and a reorg can replace the block at one. This class holds no chain tip and has no way to notice. Caching it anyway is still the right default – the caller that repeatedly asks for headers by height is building a chain of them, verified by each header’s previous_block_hash link to the one before it, so a stale entry fails that check loudly rather than being trusted silently.
get_block_count and get_best_block_id are never cached – both are the chain tip, and it moves. Every call reaches the wrapped fetcher.
No expiry was rejected as the general policy for the same two: a lifetime is a second parameter with a clock behind it, untestable without freezing time and wrong for somebody at whatever value it defaulted to. Nothing here hammers the tip – get_tx_out never touches it – so a caller who wants it held briefly holds it in a local variable, which costs one line and needs no policy here.
A raised `FetchError` is never cached. get_tx and get_block_header write their cache only after the wrapped fetcher answers successfully, so a transaction that does not exist yet, or a backend that is briefly down, is asked again next time rather than having its failure remembered forever – negative caching is a policy nobody asked for.
max_size bounds each of the two caches separately rather than pooling them under one shared count: they hold answers of different shape and different traffic – a header is a fixed eighty bytes, a transaction is not – and a caller who floods one by asking for many headers should not evict transactions it never touched. None, the default, leaves both unbounded, which is a real leak in a long-running process; a caller that holds a fetcher for a long time is the one to set it. Eviction is least-recently-used, a hit moving an entry to the end of its OrderedDict and an overflow popping from the front.
- get_block_header(height: int) BlockHeader[source]¶
Return the cached header, or fetch and cache it.
- get_tx(tx_id: bytes | str | bytearray | memoryview) Tx[source]¶
Return the cached transaction, or fetch and cache it.
Keyed on tx_id_hex(tx_id) and not on tx_id itself: Octets is bytes, a bytearray, a memoryview or a hex string, so the same transaction asked for two different ways would otherwise be cached under two different keys and fetched twice – the very amortization this class exists for, lost to the argument’s spelling rather than its value. tx_id_hex is the normalizer Fetcher already validates ids with, so this reuses it rather than writing a second one.
- class btclib_wallet.fetch.decorators.FallbackFetcher(fetchers: Sequence[Fetcher])[source]¶
Bases:
FetcherA Fetcher that tries a sequence of backends, in order.
Composes the backends rather than subclassing one, fetchers holding the sequence as given. The first one that does not raise answers; a later one is tried only when an earlier one raises FetchError – which HttpError and RpcError both subclass, so a dead socket, an HTTP 500 and “no such transaction” all fall through the same way. BTClibValueError, which Fetcher.__init__ raises for an unknown network and which client_errors raises for a node serving the wrong chain, propagates instead: a misconfigured backend is a caller error to fix, not a failure to paper over by asking the next one.
When every backend raises, this raises a FetchError naming each of them and its failure, chained (raise … from) off the last one – not the last one’s message alone, which would report a second-string backend’s 404 for what began as the primary’s dead socket.
This never compares two answers. It stops at the first backend that does not raise, so on the disagreement two backends could have about the tip, nothing is thrown away: there is only ever one answer in hand. An object that asked every backend and compared their answers would be a quorum fetcher, a different policy with a different cost – worth its own issue if anyone wants it, and not this class.
Refuses an empty sequence at construction: a FallbackFetcher with nothing to fall back to answers no question at all. Refuses a network mismatch too, comparing every backend’s network rather than trusting the first: a mainnet node and a testnet explorer behind one FallbackFetcher is a configuration error that would otherwise surface as a transaction whose outputs are labelled for the wrong chain, wherever the failover happened to land.
- get_block_header(height: int) BlockHeader[source]¶
Answer from the first backend that does not raise FetchError.
btclib_wallet.fetch.electrum module¶
The btclib fetcher backed by an Electrum server.
btclib.electrum is the codec – the JSON-RPC framing, the shapes of the methods this fetcher asks, and the merkle-branch check – and holds no socket; this module is the Fetcher over it, taking a transport and turning each question into a request line and a line back into a btclib type, the way EsploraFetcher and BitcoinCoreRestFetcher do over their own transports.
`transport` is required, keyword-only, and has no default. LineTransport (btclib_wallet.fetch.transport) carries no host, so a transport that reaches a server is one constructed with that server’s host – TlsLineTransport(host, port) is the one shipped – and a default transport would be a default server, which the next paragraph refuses.
No `base_url` and no default server. Unlike EsploraFetcher, which takes a required url with no default, this fetcher takes none at all: transport is the whole of how it reaches a server, and which server that is is transport’s own business. EsploraFetcher.base_url’s reasoning – BLOCKSTREAM_INFO is offered as a value to pass and never as a default, because which host sees every address a caller looks up is not btclib’s decision to make on anyone’s behalf – applies here with even less room. Electrum’s own shipped server list carries each host’s ports, its pruning and the protocol version it speaks, and nothing about the certificate it presents, so which of its entries a transport verifying against a public CA store reaches is not a question the list answers. Electrum’s own client does not ask it either: where the CA-signed handshake fails it pins the certificate that server presented and connects with check_hostname off (electrum/interface.py, Interface._get_ssl_context), so being listed says what protocol version a server speaks and not that its certificate is one a strict transport accepts. Entries share registrable domains, so the list names fewer operators than hosts, and a caller running their own server is invisible to it entirely. Naming any one entry as a constant would repeat the mistake BLOCKSTREAM_INFO already refuses.
A question the interface does not declare, answered by nothing else in this package. get_tx_merkle and verify_tx are not on Fetcher: adding a return type no other backend can honor is what the interface’s abstract methods, each with a return type of its own, already exist to avoid, and both the issue and issue btclib-org/btclib#1193 hold the interface itself unchanged. What Fetcher’s own class docstring calls “evidence beside the data” is what these two are – a merkle branch checked against a header this fetcher fetched on its own, which is a different kind of answer from the other backends’ word for it, and the reason this backend exists.
- class btclib_wallet.fetch.electrum.ElectrumFetcher(network: str = 'mainnet', *, transport: Callable[[bytes, float], bytes], verify_network: bool = True, timeout: float = 30.0)[source]¶
Bases:
NetworkVerifyingFetcherEvery Fetcher question, and a merkle branch, from an Electrum server.
get_tx is blockchain.transaction.get, the raw hex decoded and handed to tx_from_raw, which recomputes the id the same way every other backend does. get_block_count and get_best_block_id are both blockchain.headers.subscribe, asked once each: the protocol answers the tip’s height together with its header rather than a separate hash field, so the id get_best_block_id returns is not the server’s word – it is BlockHeader.hash of the header block_header_from_raw has already checked is well-formed and cost real work to find, the same check get_block_header runs. get_block_header is blockchain.block.header, cp_height left unsent.
get_tx_merkle is blockchain.transaction.get_merkle, returning the branch and position btclib.electrum.MerkleProof carries. verify_tx fetches the header at the height asked for with get_block_header – the caller’s own height, not the proof’s unchecked block_height – and checks the branch against it with btclib.electrum.verify_merkle_proof, answering False for a malformed branch or a wrong position the way btclib.block.merkle_proof.verify does, and still raising for anything get_block_header itself refuses.
Also a FeeEstimator: estimate_fee is blockchain.estimatefee, answered for the target asked – the protocol carries no field equivalent to Core’s blocks, so unlike the other two backends this one cannot report a clamp the server may have applied underneath it.
get_tx_out is not overridden, and stays the Fetcher base’s derivation from get_tx. The protocol answers a script hash’s history and its unspent outputs, blockchain.scripthash.get_history and .listunspent, but ElectrumFetcher does not ask either question: every Fetcher question is keyed on an identifier the caller already holds – a txid, a height, nothing – and a script is not one.
- assert_network() None[source]¶
Raise unless the server serves the chain this fetcher labels with.
One request and one comparison: blockchain.block.header at height 0 – the same method get_block_header asks for every other height, asked here for the block every chain starts from – against NETWORKS[self.network].genesis_block. What is compared is BlockHeader.hash of the eighty bytes _header_at has checked, so this backend answers the question the way it answers get_best_block_id: by hashing a header rather than by reading a hash the server chose. server.features carries a genesis_hash member and is what Electrum’s own client compares (electrum/interface.py); the header is asked for instead because it makes the server produce eighty bytes that hash to the genesis rather than repeat a string, and because it needs no codec function btclib.electrum does not already have.
Worth the call, because the failure it catches is silent, the same one BitcoinCoreFetcher.assert_network’s docstring names: a fetcher labelled mainnet over a server on another chain renders a mainnet address for every output it fetches, for coins that are not there. It is sharper here than elsewhere, because verify_tx checks a branch against a header fetched from that same server: a caller on the wrong chain is otherwise handed a proof that is valid and about a chain they did not mean.
What a genesis hash cannot separate is two signets, and EsploraFetcher.assert_network’s docstring is where that is written down. This class takes no signet_challenge of its own, for the reason that class takes none: nothing among the methods btclib.electrum speaks is a signet’s challenge to compare against.
- estimate_fee(target: int) FeeQuote[source]¶
Return a fee rate expected to confirm within target blocks.
blockchain.estimatefee. -1, the protocol’s own way of saying no estimate is available, is a decline and raised as a FetchError rather than reaching FeeRate – no sentinel reaches the return type, the way Broadcaster’s third contract bullet already requires of a refusal. Servers commonly proxy this answer from a node’s own estimatesmartfee, so it is read in the same BTC/kvB unit FeeRate.from_btc_per_kvbyte already converts.
- get_best_block_id() bytes[source]¶
Return the id of the chain tip, recomputed from its own header.
headers.subscribe answers the header itself rather than a separate hash field, so this is not the server’s word: it is block_header_from_raw’s check on the bytes it sent, and then the hash of the header that passed it.
- get_block_header(height: int) BlockHeader[source]¶
Return the header at this height, via blockchain.block.header.
- get_tx(tx_id: bytes | str | bytearray | memoryview) Tx[source]¶
Return the transaction with this id, via transaction.get.
- get_tx_merkle(tx_id: bytes | str | bytearray | memoryview, height: int) MerkleProof[source]¶
Return the branch proving tx_id confirmed at height.
blockchain.transaction.get_merkle, the one question no other backend here can answer at all.
- verify_tx(tx_id: bytes | str | bytearray | memoryview, height: int) bool[source]¶
Return whether tx_id is proven confirmed at height.
Fetches the header at height and the branch separately, then checks the second against the first’s merkle root – see this class’s own docstring for what each half already checks on its own.
btclib_wallet.fetch.esplora module¶
The block-explorer fallback, for a caller with no node.
Esplora’s HTTP api, because it is an api and not a product: Blockstream publishes the server as open source, mempool.space serves the same endpoints this module calls, and anyone can run their own – so a client written against it is not written against one company’s endpoint. BLOCKSTREAM_INFO below is the reference deployment and is offered as a value to pass, never as a default: the one decision btclib will not take on a user’s behalf is which stranger gets to see every address they look up. mempool.space is not offered as a second constant for the same reason, and naming it here is the evidence for the sentence above rather than a recommendation.
What the fallback promises is the same answers behind the same interface. What it does not promise is that they are all true. get_tx is the one answer that checks itself completely, in tx_from_raw – the serialization comes back and the id is recomputed from it, so a substituted transaction is caught. get_block_header checks less: block_header_from_raw only asks whether the eighty bytes are well-formed and cost a real hash to mine, which is cheap for a host to fabricate compared with the transaction it would have to forge to pass the id check. The height and the tip hash have nothing here to check them against at all, and are taken on trust.
Which chain the explorer serves is a separate question from the answers above, and verify_network is what asks it: declared once on NetworkVerifyingFetcher rather than agreed among the backends, on by default, comparing /block-height/0 against the genesis NETWORKS carries for the network this fetcher was built for. The failure it catches is the same silent one – a fetcher labelled mainnet over a host serving another chain renders a mainnet address for every output it fetches – and what a genesis hash cannot do is separate two signets; assert_network’s own docstring says why.
Nor does anything here say a transaction is confirmed: the answer to that is a merkle branch checked against a header, which is what the Electrum backend of issue btclib-org/btclib#204 adds and this one cannot. That is the trade the fallback exists to offer, and SECURITY.md states it.
The endpoints reached here answer in plain text rather than json: /tx/<txid>/hex, /blocks/tip/height, /blocks/tip/hash, /block-height/<height> and /block/<hash>/header, read; POST /tx, written. The json renderings beside them carry the same values with more to disagree about – and /hex is what makes the id check above possible at all. That these are what a second deployment has to serve is why they are the thing to check before naming one: a host answering json where this expects text is not compatible in the way that matters.
- class btclib_wallet.fetch.esplora.EsploraFetcher(base_url: str, *, network: str = 'mainnet', verify_network: bool = True, timeout: float = 30.0, transport: ~collections.abc.Callable[[~urllib.request.Request, float], tuple[int, bytes]] = <function urlopen_transport>)[source]¶
Bases:
NetworkVerifyingFetcherEvery Fetcher question, answered by an Esplora instance over HTTP.
base_url is required and has no default, for the reason BLOCKSTREAM_INFO is a constant and not one.
Also a Broadcaster: broadcast is POST /tx, the one endpoint of this class that writes. Holding a fetcher is not broadcasting; a caller has to call the method, and broadcast’s own docstring is where its non-idempotence is stated, at the point a caller meets it.
Also a FeeEstimator: estimate_fee is GET /fee-estimates, the one endpoint here answering json rather than plain text and the one answering a fixed set of confirmation targets rather than the one asked for – estimate_fee’s own docstring is where the resolution between the two is stated.
- assert_network() None[source]¶
Raise unless the explorer serves the chain this fetcher labels with.
One request, /block-height/0 – the same endpoint get_block_header reads for every other height, asked here for the block every chain starts from – compared against NETWORKS[self.network].genesis_block.
Worth the call, because the failure it catches is silent, the same one BitcoinCoreFetcher.assert_network’s docstring names: a fetcher labelled mainnet over an explorer serving another chain renders a mainnet address for every output it fetches, for coins that are not there.
Signet is the case a genesis hash cannot settle: Core builds every signet’s genesis from the same parameters (kernel/chainparams.cpp), the challenge going into the message start and not into the block, so this check separates mainnet, testnet, testnet4, signet and regtest, and separates no two signets – the same limit the node backends have when no signet_challenge is given. This class takes no signet_challenge of its own: nothing Esplora’s api publishes is a signet’s challenge to compare it against.
- broadcast(tx: Tx) bytes[source]¶
Announce tx to the server, and return the txid it confirmed.
_verify_once is called first, as every other method here calls it – but a broadcast to a server on the wrong chain is the worst version of the silent failure verify_network exists to catch: a fetch answering with the wrong data is corrected on the next read, where a signed transaction fanned out to the wrong network’s peers cannot be called back.
POST /tx with the wire serialization’s hex, witness included, as the body – what a peer relays and eventually mines, not the stripped form Tx.id hashes. The server answers the same way its GET endpoints do, plain text and not json, which is why this shares _request with text rather than opening a second seam: the bounded read, the transport argument and the client_errors translation are the same regardless of which verb sent the body.
The id is computed from tx before the request, the way tx_from_raw recomputes one from a fetched serialization: a success naming a different txid is a FetchError, the server having confirmed some other transaction. One request and no retry, for the reason BitcoinCoreFetcher.broadcast’s docstring gives: after a timeout this method cannot tell a transaction that never reached the server from one that did and whose acknowledgement was merely lost on the way back, and that is the caller’s decision to make, not this one’s.
A non-200 status is an HttpError carrying the body, the way text reports one for a GET: an Esplora deployment answers a rejected transaction with a 400 and the reason in plain text.
- estimate_fee(target: int) FeeQuote[source]¶
Return a fee rate expected to confirm within target blocks.
GET /fee-estimates, an object from confirmation target to sat/vB – unlike every other endpoint here, in json. target is resolved to the smallest key the deployment actually quotes at or above it: the smaller target’s price is never below the larger’s, so rounding this way over-pays and never under-pays. A target above _MAX_FEE_TARGET is refused before any request is made.
- get_block_header(height: int) BlockHeader[source]¶
Return the header of the block at this height, checked on arrival.
Two requests: /block-height/<height> maps the height to the hash /block/<hash>/header takes, both plain text like the tip endpoints above.
- get_tx(tx_id: bytes | str | bytearray | memoryview) Tx[source]¶
Return the transaction, parsed and checked against its txid.
- text(path: str, max_body_size: int = 8001024) str[source]¶
Return the body of a GET on path, as stripped text.
max_body_size is what this particular answer may weigh; the default is the widest of the bounds above, so a caller asking for something narrow says so.
A status that is not 200 is an HttpError, which carries it: every answer here is a GET of an immutable value, so a 429 or a 503 from a public deployment is the one failure worth another attempt, and telling it from a 404 without reading a message is what the field is for. btclib retries nothing itself – an explorer’s rate limit is the caller’s budget to spend.
btclib_wallet.fetch.fee_estimator module¶
The interface a chain backend answers to quote a fee rate.
Kept apart from Fetcher, the way Broadcaster is: not every backend that answers Fetcher’s questions can quote a price. BitcoinCoreRestFetcher, over Core’s read-only -rest interface, is exactly that backend again – Core’s -rest interface carries no fee estimation at all, estimatesmartfee being RPC only. A Protocol lets BitcoinCoreFetcher, ElectrumFetcher and EsploraFetcher satisfy FeeEstimator structurally, with nothing to say about BitcoinCoreRestFetcher at all – the same reasoning broadcaster.py’s docstring gives for that class, one capability over.
Not runtime_checkable, for the reason Broadcaster is not: the contract below is not what isinstance(x, FeeEstimator) would verify.
One target asked, one answer – not a mapping. Core and Electrum answer one confirmation target per request, so a mapping would be synthesized for two backends out of three; Esplora’s own targets are a fixed set rather than a general shape, so a mapping is not the answer there either.
The target answered for need not be the target asked for, so the return is not a bare `FeeRate`. Core’s blocks field is documented as the target the estimate was found at, clamped to at least 2 and at most the estimator’s own maximum usable target – asking for 1 and being answered for 2 is the ordinary case, and a bare rate handed back as though it answered the question asked would misreport it. FeeQuote carries both.
This module sits above fee.py: it may import FeeRate from there, and fee.py imports nothing from here, which is also the direction issue btclib-org/btclib#2129 cuts the two into different distributions.
- class btclib_wallet.fetch.fee_estimator.FeeEstimator(*args, **kwargs)[source]¶
Bases:
ProtocolA backend able to quote a fee rate for a confirmation target.
One method and one contract, binding on every implementation of it:
the target answered for need not be the target asked for, and the FeeQuote returned carries the one the rate is actually valid for;
a quote a backend expresses more finely than FeeRate can hold exactly is rounded up, never down – FeeRate.from_sats_per_vbyte and FeeRate.from_btc_per_kvbyte take round_up=True here, rather than the refusal each raises by default for a caller who is stating a price: refusing an ordinary explorer answer would make the backend unusable, and truncating would under-pay. Rounding a conservative estimate up leaves it an estimate;
a backend that cannot quote a rate for the target – Core’s feerate absent from a reply that carries errors instead, Electrum’s -1 – declines rather than answering, and is raised as a FetchError keeping the backend’s own reason, unreshaped, the way Broadcaster’s third contract bullet already requires of a refusal. No sentinel reaches FeeQuote.
- class btclib_wallet.fetch.fee_estimator.FeeQuote(rate: FeeRate, target: int)[source]¶
Bases:
objectA fee rate, and the confirmation target it is valid for.
target is not necessarily the target FeeEstimator.estimate_fee was asked for: a backend may answer for a target it clamped or rounded to, and this is where that answer is carried rather than lost by returning rate alone.
- btclib_wallet.fetch.fee_estimator.valid_confirmation_target(target: int) int[source]¶
Return target, refusing what no backend answers a fee for.
Checked once, ahead of the request, the way fetcher.block_header_height checks a height before the first request that needs one: a target that is not a positive int is refused here rather than left to surface as whichever error the backend happens to answer for it.
btclib_wallet.fetch.fetcher module¶
The interface a chain backend answers, whichever backend it is.
The questions btclib cannot answer from bytes it was handed: what transaction has this id, what output does this outpoint name, where is the chain tip, and what header does a given height carry. Fetcher is those questions and nothing else, so that calling code takes a Fetcher and never learns whether a full node or an explorer is behind it.
What comes back is btclib types – Tx, TxOut, BlockHeader – and not the dicts the backends send. A wrapper handing over response[“vout”][0][“value”] leaves the caller to know which backend it is talking to, in the one place the whole point was not to.
- class btclib_wallet.fetch.fetcher.Fetcher(network: str = 'mainnet')[source]¶
Bases:
ABCWhat a backend must answer, and what btclib does with the answers.
One abstract method per question, each with a return type of its own. That is the shape on purpose rather than one get(kind, id) returning whatever: a backend able to prove what it says – the Electrum protocol serves a merkle branch, which a client checks against a header it already holds (issues btclib-org/btclib#188, btclib-org/btclib#204 and btclib-org/btclib#1132) – returns evidence beside the data, and evidence is a different type. Adding a method for it is additive; widening the return type of get_tx would be a break for everyone already calling it.
get_tx_out is concrete, and is the one operation every backend can derive from another: an output is a field of the transaction that created it. A backend with a cheaper answer overrides it.
- abstractmethod get_block_header(height: int) BlockHeader[source]¶
Return the header of the block at this height.
Height, and not hash: every backend here answers for a hash and maps a height to one with a second call – getblockhash, /block-height/<height>, /blockhashbyheight/<height>.bin – but the Electrum protocol’s blockchain.block.header takes a height and publishes no call that takes a hash at all. A hash-only signature would be one an Electrum backend could never answer, which is the NotImplementedError-in-an-ABC outcome the questions above already avoid.
Checked on arrival: assert_valid and assert_valid_pow, so a well-formed header answers for eighty bytes that took a real hash to produce. assert_valid_time is not among them – it compares against the local clock, and a header that is genuinely on the chain should not start failing this because the machine running it has drifted.
That is the whole of what the check establishes. Not that the header is on the chain the caller means, not that it is truly at the height asked for, and not that it is the tip: a backend serving a real header from the wrong chain, or from the wrong height, passes it just the same. Only a chain of headers answers that, which is not this method’s question.
- abstractmethod get_tx(tx_id: bytes | str | bytearray | memoryview) Tx[source]¶
Return the transaction with this id.
- get_tx_out(out_point: OutPoint) TxOut[source]¶
Return the output an outpoint names, spent or not.
Spent or not, which is what makes this the useful question and not gettxout’s. bitcoind’s gettxout reads the utxo set, so it answers null for an output that has been spent – and every input of a confirmed transaction names an output that has been spent, by that very transaction. A fee is the inputs less the outputs, so an unspent-only answer cannot compute one.
The cost is that the whole previous transaction is fetched to read one output of it, and against bitcoind that means a node with -txindex.
- class btclib_wallet.fetch.fetcher.NetworkVerifyingFetcher(network: str = 'mainnet', *, verify_network: bool = True)[source]¶
Bases:
FetcherA Fetcher that can ask its host which chain it serves.
Every backend reaching a host over a connection is one of these: network is a label the caller chose, a host on another chain answers every question just as readily, and asking is the only way to tell. CachingFetcher and FallbackFetcher are Fetcher`s and not these – each answers from another `Fetcher, which is where the host is and where the question belongs – which is why the check is declared here rather than on Fetcher, where those two would carry a verify_network with nothing to ask.
verify_network is who asks. On by default and before the first fetch rather than in the constructor: the answer costs a round trip that is worth paying where it is checked and wasted where a fetcher is built and never used, and a host that is merely unreachable should not be a failure to construct anything. The answer is then kept – a host does not change chain under a client that goes on pointing at it – and a caller that would rather not ask says verify_network=False.
- abstractmethod assert_network() None[source]¶
Raise unless the host serves the chain this fetcher labels with.
What settles it is the backend’s: a chain name and a signet challenge where the host is a node reporting both, a genesis block where it is a host that only says it validated. Abstract rather than a default, which could only pass: a backend declaring none would check the chain never and compile.
Public, for a caller that wants the question answered at a moment of its own – at startup, or after a client was repointed.
A malformed reply is a FetchError. A disagreement is a BTClibValueError: the host is the authority on which chain it serves, so the fetcher’s label is the thing to fix.
- btclib_wallet.fetch.fetcher.block_header_from_raw(raw: bytes | str | bytearray | memoryview, height: int) BlockHeader[source]¶
Return the header a serialization holds, checked on arrival.
Every backend answers with the raw serialization rather than a rendering of it, the way get_tx does – but a header names no height and no chain of its own, so there is nothing here to recompute it against the way tx_from_raw recomputes a txid. What is checked instead is what Fetcher.get_block_header promises: BlockHeader.assert_valid, for eighty well-formed bytes, and assert_valid_pow, for a hash that took real work to find. Its docstring is where what neither check establishes is written down.
- btclib_wallet.fetch.fetcher.block_header_height(height: int) int[source]¶
Return the height, refusing what no chain has a block at.
Every backend maps a height to a block before it can answer get_block_header at all – getblockhash, /block-height/<height>, /blockhashbyheight/<height>.bin – so each needs the same guard in front of that first request, rather than each discovering its own backend’s answer to a negative or fractional height: RPC code -8 for Core, a 404 or a 400 for an Esplora deployment, neither naming the height as what is wrong with the request. -rest alone does – a 400 whose body is Invalid height: <text> – and the guard still stands in front of it, so that they answer the same exception for the same mistake rather than one of them a better one.
- btclib_wallet.fetch.fetcher.client_errors() Iterator[None][source]¶
Re-raise what the rpc client raises as btclib’s own exception.
bitcoin_core_rpc declares a FetchError, an HttpError and an RpcError of its own – it declares zero dependencies and imports nothing of btclib’s – so those are not the classes btclib.exceptions declares, and an except FetchError written against btclib does not catch them. This is the one place the two meet, and every call that crosses into the package goes through it: the fetches and the broadcasts alike, by way of EsploraFetcher._request – text and broadcast both going through it – BitcoinCoreFetcher._call, which broadcast uses the same as every other method there, and BitcoinCoreRestFetcher._get_bin/_get_json; the constructors that derive a signet challenge before any fetch; and BitcoinCoreFetcher.assert_network, which reaches assert_chain directly where its -rest twin asks the same question through _get_json and is covered by the first group.
ElectrumFetcher._round_trip is the same wrap over a caller-supplied transport rather than a call into this package directly: a LineTransport that cannot answer raises this same vocabulary, its own docstring says so, and this is where that translation happens for every one of that fetcher’s calls.
The fields are what make this a translation rather than a blanket wrap: status and code are the whole reason those two classes exist, and losing them would leave a caller matching on the text of a message again.
args[0] and not str(e): both sides compose their message in __str__, so handing the composed one back in would report “not found (rpc error code -5) (rpc error code -5)” – once more per translation.
- btclib_wallet.fetch.fetcher.fetch_errors(source: str) Iterator[None][source]¶
Report what a conversion refuses as a failure of source.
Every answer a backend gives is a string it chose, so every parse of one is a place the backend can be wrong: a hex field that is not hex, a height that is not a number, a transaction truncated in transit. Those arrive as ValueError and TypeError from int and bytes_from_octets, and as BTClibRuntimeError from the stream readers under Tx.parse – “not enough binary data” is what a transaction truncated in transit looks like from inside var_bytes. All of them name the converter and not the host, and the host is what has to be fixed.
- btclib_wallet.fetch.fetcher.tx_for_network(tx: Tx, network: str) Tx[source]¶
Return the transaction with its outputs labelled for network.
Tx.parse labels every script_pub_key mainnet, and is right to: the serialization carries a script and no network, so a parser handed bytes alone has nothing else to say. A fetcher does – it was told which chain it is talking to – and the label is what ScriptPubKey.address renders from, so an unlabelled testnet output reports a mainnet address for coins that are not there.
Mainnet in, mainnet out: for the default network this returns a transaction equal to its argument, the label being the only thing it touches. The bytes are untouched in every case, ScriptPubKey serializing the script alone.
The name is resolved and not compared as text. Resolving refuses a network no table has, which every check_validity=False below would otherwise write into the transaction handed back, to surface far from here as whatever went on to render an address; and it answers the same for “ MainNet “ as for “mainnet”, where a comparison would relabel every output instead of returning the transaction as it is.
- btclib_wallet.fetch.fetcher.tx_from_raw(raw: bytes | str | bytearray | memoryview, tx_id: str, network: str) Tx[source]¶
Return the transaction a serialization holds, if it is the one asked for.
Every backend answers get_tx with the serialization rather than with a rendering of it, and this is why: the id is a hash of those bytes, so recomputing it says whether what arrived is what was asked for. A height is taken on the backend’s word and a block header answers for itself, which SECURITY.md states per answer rather than here; this one costs a hash of a few hundred bytes.
It is not only the untrusted backend it guards. A node behind a caching proxy, a truncated response and a request that raced another all show up here, as the wrong id rather than as a wrong amount somewhere later.
- btclib_wallet.fetch.fetcher.tx_id_hex(tx_id: bytes | str | bytearray | memoryview) str[source]¶
Return the display hex of a transaction id, checking it is one.
Every backend puts the id in a request as hex, and each accepts whatever Octets accepts, so each needs the same 32-byte check – performed here rather than left to the backend, which would otherwise report a mistyped id as the remote host’s 404.
btclib_wallet.fetch.transport module¶
The transports a fetcher does its I/O with, and the network policy of each.
Two seams, one per wire shape. HttpTransport is re-exported under btclib’s name with its implementations; LineTransport is declared here and TlsLineTransport implements it here.
The HTTP half’s implementation is bitcoin_core_rpc’s: a bounded read, no redirect followed and no proxy taken from the environment, in a package that depends on nothing beyond the standard library. btclib depends on it for the rpc client and reaches the same transport through it, rather than keeping a second copy of that bounded-read and redirect policy in step with the first.
Aliases and not wrappers: EsploraFetcher passes transport= straight through to http_request, and a caller substituting one for a test needs the object those two agree on. HttpTransport is that seam, and this is btclib’s name for it.
The HTTP implementations differ in how they hold the connection. urlopen_transport is the default: one connection per call, opened and handed to the node to close. SessionTransport keeps one connection per (scheme, host, port) open across calls instead, which is worth choosing over many calls against one node – a walker fetching many transactions, a client polling one – where the reused connection, and on https the reused TLS handshake, is what the default pays for on every call. It has a close() and works as a context manager; nothing here calls either on a caller’s behalf.
What does not come through unchanged is the exceptions. http_request raises the package’s FetchError and HttpError, which are not the classes btclib.exceptions declares; btclib_wallet.fetch.fetcher.client_errors is what translates them, and every call into this module from inside btclib is wrapped in it.
`LineTransport` is not an alias of anything. bitcoin_core_rpc’s own transport is HTTP, and the Electrum protocol btclib.electrum speaks is newline-delimited JSON-RPC over a raw TCP or TLS socket, which that package does not carry. ElectrumFetcher (btclib_wallet.fetch.electrum) takes one request line, already terminated by btclib.electrum.encode_request’s own newline, and a timeout in seconds, and returns the answering line with its delimiter consumed rather than included – bytes on both sides, the shape btclib.electrum’s own framing already produces and reads, so the boundary needs no encoding step of its own, the same reason HttpTransport is bytes in and bytes out. A transport that cannot answer raises bitcoin_core_rpc’s own FetchError or a subclass of it, the vocabulary HttpTransport already raises through this same package; btclib_wallet.fetch.fetcher.client_errors is what translates it, at every ElectrumFetcher call the way it is at every other backend’s.
`LineTransport` carries no host, so its implementation is an object that does. TlsLineTransport(host, port) is the callable ElectrumFetcher takes, and that fetcher has no default for it: a default transport would have to be a default server, which ElectrumFetcher’s own docstring refuses.
- class btclib_wallet.fetch.transport.TlsLineTransport(host: str, port: int, *, context: SSLContext | None = None, max_line_size: int = 8001024)[source]¶
Bases:
objectA LineTransport over TLS to one server, verifying its certificate.
Constructed with the host and port it connects to, so the instance is the server: ElectrumFetcher(transport=TlsLineTransport(host, port)) is the whole of naming one. Construction opens nothing.
One connection per call. Each call connects, performs the TLS handshake, sends the one request line, reads the one line answering it, and closes. A connection kept across calls is what blockchain.headers.subscribe rules out: the server then pushes a notification line down that same connection at every new block, and a later call reading the next line would read the notification in place of its own answer. What a kept connection would save – the connect and the handshake – is paid on every call instead. Nothing is kept between calls, so concurrent calls share the ssl.SSLContext and nothing else.
The network policy, the one urlopen_transport keeps for HTTP:
a bounded read: the line is refused, not truncated, once it passes max_line_size octets without a newline. The default is DEFAULT_MAX_BODY_SIZE, sized for a whole block as hex, so blockchain.transaction.get fits for any transaction a block holds;
a timeout over the whole exchange – connect, handshake, send and read – rather than per socket operation: each operation is given what is left of one deadline, so a server dripping a line one octet at a time cannot hold the call open past it;
no proxy from the environment: socket.create_connection reads no proxy variable, so the connection goes to the host named here;
TLS verified by default: context defaults to ssl.create_default_context(), which requires a certificate chaining to the default CA certificates it loads and matching host.
A server whose certificate those do not trust – a server of one’s own with a self-signed certificate, say – is reached by passing a context that trusts that certificate, ssl.create_default_context(cafile=…), which keeps both checks on. No flag here turns verification off; what a context a caller supplies accepts is that caller’s decision, as a transport of their own would be. Plain TCP is not offered: a caller who wants it writes that LineTransport.
Everything that goes wrong below the answer – an unresolvable host, a refused connection, a handshake the certificate fails, a timeout, a connection closed before a whole line, a line over the limit – is bitcoin_core_rpc’s FetchError, the contract LineTransport states, which ElectrumFetcher translates into btclib’s own.
- property context: SSLContext¶
Return the context the TLS handshake is verified against.
- btclib_wallet.fetch.transport.http_request(url: str, *, data: bytes | None = None, headers: ~collections.abc.Mapping[str, str] | None = None, timeout: float = 30.0, max_body_size: int = 8001024, transport: ~collections.abc.Callable[[~urllib.request.Request, float], tuple[int, bytes]] = <function urlopen_transport>) tuple[int, bytes][source]¶
Return the status and body of a GET, or of a POST when data is given.
Everything below the HTTP status is a FetchError: a refused connection, an unresolvable host and an expired timeout are one answer to the caller – the backend did not answer – and none of them is a bitcoin error worth a type of its own.
A non-2xx status is not a failure here. It comes back like any other, because the body of a 500 is where bitcoind’s legacy JSON-RPC 1.1 reply puts its error object, and the body of a 404 is where an explorer says what it could not find. Deciding what a status means is the backend’s job, that being the layer that knows. A 30x is one of those statuses now rather than a second request: urlopen_transport follows no redirect, and _OPENER says why.
max_body_size is what an answer may weigh, and the caller sets it from what it asked for: a tip height is a few octets and a raw transaction is megabytes, so one number for both would be the larger. The body of a failure is bounded by MAX_ERROR_BODY_SIZE instead, and in time by timeout, the same deadline the answer is read against: a drip is a drip whichever status precedes it.
timeout is checked here and not only where BitcoinCoreRpcClient already does, because this function is public on its own: a caller reaching it directly with a transport of their own would otherwise forward a zero, a negative number, True or a NaN straight to that transport unexamined.
- btclib_wallet.fetch.transport.urlopen_transport(request: Request, timeout: float, *, max_body_size: int = 8001024) tuple[int, bytes][source]¶
Perform the request with urllib, reading a bounded response.
The default HttpTransport, and the only function here that opens a socket. It maps nothing and interprets nothing: the status and the bytes go back as they arrived, and http_request is where the failures become the exceptions above.
Bounded, and this is the only place a bound can be incremental: the limit is a keyword with a default, so this function still is an HttpTransport. A transport of someone else’s returns bytes it has already read, so all http_request can do for those is refuse to pass an oversized body on.
No redirect is followed: _OPENER above says why, and what a 30x arrives as is the HTTPError any other non-2xx status does.
timeout bounds the exchange and not each socket operation: the deadline is taken before the connect, so a peer that drips a body one octet at a time cannot hold this call open past it.
The scheme, the timeout and the limit are checked here and not only where http_request already checks them, for the reason that function gives for its own copy: this one is public too, and it takes a Request a caller built. urlopen speaks file: and data: as well, so a request whose url came from configuration would otherwise make this transport read the local disk and report the bytes as a node’s answer – and an invalid control would be refused after the resource was opened rather than instead of opening it.
Module contents¶
Module btclib_wallet.fetch.
Where the chain is. Everything else in btclib works on bytes it was handed. This package is the one place that goes and asks: what transaction has this id, what output does this outpoint name, where is the chain tip. Fetcher is the interface, implemented once per backend – BitcoinCoreFetcher over a full node’s JSON-RPC, BitcoinCoreRestFetcher over the same node’s unauthenticated -rest interface, EsploraFetcher over a block explorer’s HTTP api, ElectrumFetcher over an Electrum server – so that calling code takes a Fetcher and never branches on which one it got.
`Broadcaster` is the one place that announces a transaction rather than asking about one, and is a typing.Protocol rather than another Fetcher method: BitcoinCoreFetcher and EsploraFetcher satisfy it, BitcoinCoreRestFetcher does not – Core’s -rest interface is read-only, and a protocol lets that asymmetry be a fact of the type rather than a NotImplementedError written into a class that never promised the capability.
`FeeEstimator` is a second `Protocol` beside `Broadcaster`, for a backend that quotes a price rather than answers a question about the chain. BitcoinCoreFetcher, ElectrumFetcher and EsploraFetcher satisfy it; BitcoinCoreRestFetcher does not, Core’s -rest interface carrying no fee estimation at all, the capability being RPC-only (estimatesmartfee). FeeQuote is what a call returns: a FeeRate together with the confirmation target it is actually valid for, which need not be the target asked for.
It adds no dependency. The standard library is the whole of every client: urllib.request, json and base64 over HTTP, socket and ssl for the Electrum protocol’s line. The HTTP client’s canonical implementation is the bitcoin-core-rpc package, which btclib depends on and does not contain, and its transport exports here are aliases to that same source, not another implementation; TlsLineTransport is implemented in btclib_wallet.fetch.transport itself. The seam lets the test suite exercise all of this while opening no socket.
A protocol’s code is split in three, and where each part lives is one rule for every protocol. The codec – the framing, the envelope and the shape of each answer – is pure and sits beside btclib.p2p, outside this package, as btclib.electrum does: this __init__ imports every fetcher, so a server importing anything under btclib_wallet.fetch would load urllib, ssl and socket with it. The client transport is this package’s, in btclib_wallet.fetch.transport, which states its network policy once. The socket a server listens on is not btclib’s at all: btclib-node holds its own, over btclib.p2p’s codec. The HTTP transport is bitcoin-core-rpc’s instead, because a transport useful without btclib belongs to that package: it is a standalone client depending on nothing beyond the standard library, and a caller with no bitcoin library has a use for an HTTP transport and none for a line transport without btclib’s codec.
The exceptions a `Fetcher` raises are btclib’s, and that costs one translation. The package declares a FetchError, an HttpError and an RpcError of its own – it declares zero dependencies and imports nothing of btclib’s – so those are not the classes btclib.exceptions declares, and fetcher.client_errors re-raises them as the ones a caller catches, status and code carried across. What that buys back is the import cost: btclib.exceptions no longer reaches a protocol client, so urllib.request, ssl and socket are loaded by the code that fetches and not by every module that catches.
The re-exported client is the exception: btclib_wallet.fetch.BitcoinCoreRpcClient is the package’s class unchanged, so calling it directly raises the package’s exceptions and not btclib’s. It is the client’s API, reached through btclib’s name for it.
Importing the package does not connect to anything, and constructing a fetcher does not either: the first call is what opens a connection, and what raises if there is nothing to connect to.
What is exported, and what is not. The fetchers, the interface they implement, Broadcaster and FeeEstimator beside it with FeeQuote, the clients a Bitcoin Core node is reached through – BitcoinCoreRpcClient for the JSON-RPC server, BitcoinCoreRestClient for -rest – and the transport seam: the timeout, the protocols a substitute has to satisfy and the implementations that open a socket – over HTTP one connection per call and one kept open across calls, and TlsLineTransport for LineTransport, ElectrumFetcher’s own protocol. That last group is here because the seam is the supported way to test calling code without a node, and because a transport is how ElectrumFetcher is told which server to reach, neither being a detail of the fetcher implementations.
bitcoin_core.cookie_auth is deliberately not here: BitcoinCoreRpcClient takes a cookie_path and reads that file at every call – the node rewrites the cookie whenever it restarts – so a caller who wants cookie authentication passes the path and never the credential, and a name for reading it is one way to hold a credential longer than the node does. -rest takes no credential to hold, BitcoinCoreRestClient declaring no equivalent at all. fetcher.NetworkVerifyingFetcher, fetch_errors, tx_from_raw, tx_id_hex and tx_for_network are not here either: they are what an implementation of the interface is built out of, and they are the answer to a caller writing their own rather than to a caller of the ones shipped here – from btclib_wallet.fetch.fetcher import fetch_errors is that answer, and it says which layer it is reaching into.
FetchError, HttpError and RpcError are not here because no exception is: btclib.exceptions holds every one of them together, which is what lets a caller see at a glance what the library raises.
`CachingFetcher` and `FallbackFetcher` answer the same interface from another `Fetcher`, not from a network. Neither is a backend: both compose one or more Fetcher`s and are exported beside the ones that open a connection because a caller reaches for them the same way, `btclib_wallet.fetch.decorators naming the module they actually live in the way btclib_wallet.fetch.fetcher does for the interface itself.
- class btclib_wallet.fetch.BitcoinCoreFetcher(client: BitcoinCoreRpcClient, network: str = 'mainnet', *, verify_network: bool = True, signet_challenge: str | bytes | None = None, estimate_mode: str = 'economical')[source]¶
Bases:
NetworkVerifyingFetcherEvery Fetcher question, answered by a node over its RPC.
Also a Broadcaster: broadcast sends sendrawtransaction, which -rest – and so BitcoinCoreRestFetcher – has no equivalent of. Holding a fetcher is not broadcasting; a caller has to call the method, and broadcast’s own docstring is where its non-idempotence is stated, at the point a caller meets it.
Also a FeeEstimator: estimate_fee sends estimatesmartfee, which -rest has no equivalent of either – fee estimation is RPC only.
The client is a constructor argument rather than a set of connection arguments repeated here: one class owns the endpoint and credentials, this one owns the mapping onto btclib types, and a caller who already has a client does not build a second.
network is btclib’s chain label and belongs here, not to the connection: it is what the outputs of a fetched transaction are labelled with. The client knows a URL and no chain, so the label is a claim until the node is asked, and assert_network is the question.
signet_challenge is which signet, for the one label that names more than one chain: Core answers signet for the default signet and for every custom one alike, so a fetcher on a signet of its own passes the challenge and assert_network holds the node to it. Hex or the bytes it spells, as -signetchallenge takes it. It is what a custom signet needs from this class and the whole of it – the addresses of one are signet’s, NETWORKS describing the encoding and not the chain – so it is refused with a network that is no signet, and refused with verify_network off, either being a check that would not be made.
estimate_mode is estimatesmartfee’s own parameter, fixed at construction rather than taken per call: it is not part of FeeEstimator’s contract – Core’s alone among the three backends – so a caller who cares chooses it once, the way signet_challenge is chosen once, rather than a signature every estimate_fee call would otherwise have to widen for one backend’s option. Defaults to Core’s own default, economical.
- assert_network() None[source]¶
Raise unless the node serves the chain this fetcher labels with.
Worth the call, because the failure it catches is silent. A client built for a testnet node – an explicit url, no port default in the way – under a fetcher labelled mainnet renders a mainnet address for every output it fetches, for coins that are not there. getblockchaininfo answers that in one round trip; what it needs is a vocabulary to be compared through, which is chain_from_network, Core naming the chain main where btclib names it mainnet.
Signet is the case a name cannot settle: Core reports signet for the default one and for every custom one alike, so two nodes sharing nothing but the shape of a challenge answer the same string. The challenge is what tells them apart, and this fetcher’s is the constructor’s signet_challenge, or the default signet with none given.
Both comparisons are the client’s assert_chain, this method being the translation into btclib’s vocabulary and btclib’s exceptions: chain_from_network on the way in, client_errors on the way out. The check itself belongs beside the protocol it reads, and lived here only while bitcoin_core_rpc did not have it.
- broadcast(tx: Tx, *, maxfeerate: float | None = None) bytes[source]¶
Announce tx to the node, and return the txid it confirmed.
_verify_once is called first, as every other method here calls it – but a broadcast to a node on the wrong chain is the worst version of the silent failure verify_network exists to catch: a fetch answering with the wrong data is corrected on the next read, where a signed transaction fanned out to the wrong network’s peers cannot be called back.
sendrawtransaction with the wire serialization, witness included – what a peer relays and eventually mines, not the stripped form Tx.id hashes. maxfeerate is forwarded exactly as given, an absent one leaving the parameter out of the call rather than substituting a value of this method’s choosing: Core’s own default (DEFAULT_MAX_RAW_TX_FEE_RATE) refuses a fee a caller may have deliberately chosen, so a default written here would refuse it on the caller’s behalf.
The id is computed from tx before the request, the way tx_from_raw recomputes one from a fetched serialization: a success naming a different txid is a FetchError, the node having confirmed some other transaction. One request and no retry – after a timeout this method cannot tell a transaction that never reached the mempool from one that did and whose acknowledgement was merely lost on the way back, so trying again could announce the same signed spend twice for no information gained. That decision is the caller’s, made with whatever else it can ask the node.
- estimate_fee(target: int) FeeQuote[source]¶
Return a fee rate expected to confirm within target blocks.
estimatesmartfee, with self.estimate_mode as its second parameter. A reply naming no feerate – “only present if no errors were encountered” – is a decline and not a rate, raised as a FetchError carrying the node’s own errors, unreshaped. blocks is the target the node actually estimated for, clamped to at least 2 and at most its own maximum usable target, and is what the returned FeeQuote.target reports – not necessarily target itself.
- get_block_header(height: int) BlockHeader[source]¶
Return the header of the block at this height, checked on arrival.
Two calls, Core answering getblockheader by hash and not by height: getblockhash maps the height first, and getblockheader with verbosity false returns the serialization rather than a rendering of it – the same shape getrawtransaction answers get_tx with.
- get_tx(tx_id: bytes | str | bytearray | memoryview) Tx[source]¶
Return the transaction with this id.
getrawtransaction with no verbosity returns the serialization. Those bytes are what Tx.parse recomputes the id from, so a transaction arriving wrong announces itself. A node answers for a transaction in its mempool, one of its wallet’s, and – only with -txindex – any other. Without the index the error is RPC code -5.
- class btclib_wallet.fetch.BitcoinCoreRestClient(url: str, *, timeout: float = 30.0, transport: ~collections.abc.Callable[[~urllib.request.Request, float], tuple[int, bytes]] = <function urlopen_transport>)[source]¶
Bases:
objectCore’s -rest interface: one node, no credentials, no envelope.
-rest is off by default (-rest=1) and, unlike the JSON-RPC server beside it, authenticates nobody who reaches it – an operator who turns it on knows that, so this class is what a caller who did speaks with, and not this package recommending it. It answers on the same port BitcoinCoreRpcClient reaches, which is what rpc_port_from_chain and from_chain below are for.
Two methods, because Core’s -rest answers two shapes and this client reads exactly them: get_bin for a .bin path, returning the body unread, and get_json for a .json one, returning what it parses to – the same Decimal-preserving parser call_raw reads its own envelope with, refusing NaN and the two infinities exactly as it does. .hex is the same octets as .bin, one more decode away from a caller who wants them that way, so it is not offered a method of its own.
A path is a path, and this client passes any. There is no get_tx, no get_block, no get_utxos: path is built by the caller from Core’s own documentation of -rest and appended after /rest unread. /getutxos is the reason a per-resource method is refused rather than merely undone here – it reads the UTXO set, so an output that has been spent and one that was never created answer the same way: neither is in the set, and -rest itself cannot tell the two apart. A wrapper turning that answer into None would read as telling a caller more than /getutxos does, when it tells them exactly as little. A caller who wants the output either way fetches the whole transaction through get_json and reads its own outputs, which is the one place the two cases are still told apart.
No credentials, because `-rest` takes none. The constructor takes no user, no password, no cookie_path: passing one would claim an authentication this interface does not perform, and a node behind a proxy that does add one is reached through transport=, the same seam BitcoinCoreRpcClient offers for the case it cannot cover either.
One request, no retry, for the reason BitcoinCoreRpcClient’s module docstring gives its own call: a caller who knows a GET is safe to repeat is free to loop, and this client does not decide that for it.
A non-200 status is HttpError, -rest having no error object of its own to read the way a JSON-RPC reply’s error member is read – a 404 for a transaction or a block Core does not have is the ordinary case, and its body is prose rather than anything to parse. Everything below the status is FetchError, exactly as http_request promises.
- classmethod from_chain(chain: str = 'main', *, timeout: float = 30.0, transport: ~collections.abc.Callable[[~urllib.request.Request, float], tuple[int, bytes]] = <function urlopen_transport>) BitcoinCoreRestClient[source]¶
Return a -rest client for the local node of one of Core’s chains.
The loopback url and the port, rpc_port_from_chain’s – -rest answers on the same port the JSON-RPC server does, there being no separate one to derive. It asks the node nothing, so it is no claim that one is listening on that port, still less that it was started with -rest: the first get_bin or get_json is what finds out, exactly as BitcoinCoreRpcClient.from_chain promises for its own first call.
- get_bin(path: str, *, request_timeout: float | None = None, max_body_size: int = 8001024) bytes[source]¶
Return the raw body of a GET to <url>/rest<path>.
path is appended after /rest unread – /tx/<txid>.bin, /block/<hash>.bin, /headers/<count>/<hash>.bin are Core’s own shapes for it. max_body_size is what DEFAULT_MAX_BODY_SIZE documents for call’s own reply: twice Core’s buffer bound on a serialized block, so a block as raw octets fits by default, and wider still for /headers, which answers several of them at once.
HttpError for a status that is not 200, -rest reporting a transaction or a block Core does not have with a 404 rather than with a reply this client could read a diagnosis out of.
- get_json(path: str, *, request_timeout: float | None = None, max_body_size: int = 8001024) Any[source]¶
Return the parsed json body of a GET to <url>/rest<path>.
path is appended after /rest unread – /chaininfo.json, /tx/<txid>.json, /getutxos/<outpoint>.json are Core’s own shapes for it. The body is read exactly as call_raw reads its own envelope, through the same _parsed_json_body: a Decimal for every number, the three constants json has no number for refused, and nothing about the parsed value’s own shape asked – an object, an array, or a bare scalar all come back as parsed.
HttpError for a status that is not 200, checked before the body is parsed rather than left to _parsed_json_body’s own parse failure: a non-200 reply is not trusted as the node’s answer merely because its body happens to parse as json, -rest’s ordinary failure body being prose rather than an error object, but a proxy in front of the node under no such constraint.
- class btclib_wallet.fetch.BitcoinCoreRestFetcher(client: BitcoinCoreRestClient, network: str = 'mainnet', *, verify_network: bool = True, signet_challenge: str | bytes | None = None)[source]¶
Bases:
NetworkVerifyingFetcherEvery Fetcher question, answered by a node over -rest.
The client is a constructor argument rather than connection arguments repeated here, the same reason BitcoinCoreFetcher takes a BitcoinCoreRpcClient: one class owns the endpoint, this one owns the mapping onto btclib types.
network labels the outputs get_tx returns, and assert_network holds the node to it.
signet_challenge is which signet, and means what it means on BitcoinCoreFetcher: Core answers signet for the default signet and for every custom one alike, so the name settles nothing and the magic the challenge derives is the identity. Both questions are answered by /chaininfo.json, the one document get_block_count already reads.
get_tx_out is not overridden, and stays the Fetcher base’s derivation from get_tx. BitcoinCoreRestClient’s own docstring is where the reason is argued – /getutxos reads the UTXO set, so a spent output and one that never existed answer it identically – and is not repeated here.
- assert_network() None[source]¶
Raise unless the node serves the chain, and the signet, this labels.
One request, /chaininfo.json. rest_chaininfo writes out what getblockchaininfo answers and adds nothing, so the members read here are the ones BitcoinCoreRpcClient.assert_chain reads over the JSON-RPC server, and the two comparisons are its two: chain against chain_from_network(self.network), Core naming the chain main where btclib names it mainnet; then, on signet alone, the magic signet_challenge derives, since Core answers signet for every signet and the name therefore separates none of them.
A node too old to report its challenge answers a reply this cannot read, so a FetchError: one it cannot answer for, and not a pass.
- get_block_header(height: int) BlockHeader[source]¶
Return the header of the block at this height, checked on arrival.
Two calls, -rest answering /headers by hash and not by height: /blockhashbyheight/<HEIGHT>.bin maps the height first, and /headers/<BLOCK-HASH>.bin?count=1 then answers the serialization of exactly one header – the query parameter and not /headers/<COUNT>/<BLOCK-HASH>, which doc/REST-interface.md marks deprecated, though not removed, since Core 24.0.
/blockhashbyheight’s .bin answers Core’s internal byte order, the reverse of the display order every hash in a -rest url is – uint256::GetHex(), which FromHex inverts, is documented as reversing that internal order for exactly this reason. Reversed here before the hex is built, the same reversal BlockHeader.parse performs on a header’s own hash fields on the way in.
- get_tx(tx_id: bytes | str | bytearray | memoryview) Tx[source]¶
Return the transaction with this id.
/tx/<TX-HASH>.bin returns the serialization, which tx_from_raw recomputes the id from – the same check BitcoinCoreFetcher and EsploraFetcher both perform, and the same reason a node answers for a transaction in its mempool, and for any other only with -txindex.
- class btclib_wallet.fetch.BitcoinCoreRpcClient(url: str, *, user: str | None = None, password: str | None = None, cookie_path: str | ~os.PathLike[str] | None = None, timeout: float = 30.0, transport: ~collections.abc.Callable[[~urllib.request.Request, float], tuple[int, bytes]] = <function urlopen_transport>)[source]¶
Bases:
objectOne Bitcoin Core JSON-RPC endpoint, and the credentials to reach it.
Not a dataclass, and that is about the password: a generated __repr__ prints every field, so the credential would appear in any traceback or log line that renders the client.
Credentials or a cookie path, and not both: each of the two says who is calling, so a client given both would have to rank them, and a caller who passed both has a mistaken idea of which one is in use. from_chain is the constructor that fills in a cookie path, along with the port, from Core’s own defaults.
Concurrent calls are supported while the configuration is not mutated. call writes nothing on the client, opens its own connection and takes its request id from no shared counter, so one client serves any number of threads. What is not promised is a client whose url, credentials or transport are reassigned while a call is in flight, or a caller’s transport that is not itself thread-safe – that one is the transport’s own contract.
Basic authentication is cleartext over plain HTTP, that being what Core’s rpc speaks. On loopback, which is what from_chain builds, the cleartext is between one process and the node beside it. For a node anywhere else it is on the wire, and rpc credentials authorise every wallet command that node has: an https url, or a tunnel, is what keeps them off it.
One connection per call by default, urllib holding none open: every call sends Connection: close and opens a socket of its own. Beside the node that is a loopback connect, which costs nothing for one call and is socket churn for a great many – RFC 9112 section 9.6 has the server initiating the close on that option, so it is the node that holds the sockets in TIME_WAIT – and to a node reached over https it is a TLS handshake each time. SessionTransport is this module’s own alternative, one connection kept per (scheme, host, port) and reused across calls; passing it as transport= is what a caller polling one node in a loop wants, ahead of a requests session or an httpx client.
No call asks the node which chain it is on: the url and the cookie path say where to ask, and what the answers mean is the caller’s to hold. getblockchaininfo is the question, its chain member the answer, and network_from_chain the vocabulary to read it in – worth the one round trip, because a client built for a testnet node under code that believes it is on mainnet fails silently. from_chain’s verify_chain makes exactly that call once, at construction.
- assert_chain(chain: str = 'main', *, signet_challenge: str | bytes | bytearray | None = None) None[source]¶
Raise unless the node serves this chain, and this signet of it.
One round trip, getblockchaininfo, and the answer cannot change under a client that goes on pointing at the same node – so this is a question asked at a moment of the caller’s choosing: at startup, after a client was repointed, or by from_chain(verify_chain=True), which is this method.
Worth asking, because the failure it catches is silent. Nothing in an rpc exchange says which chain is behind it: a cookie authenticates the node that wrote it and not what that node is running, so a url, a datadir or an environment variable carried over from another host answers every call and answers about the wrong chain.
Signet is the case a name cannot settle. Core reports signet for the default signet and for every custom one alike, so two nodes sharing nothing but the shape of a challenge answer the same string; the challenge is what tells them apart, and the p2p magic it derives is what this compares – magic_from_signet_challenge of what the node reports, against the caller’s signet_challenge or, with none, magic_from_chain(“signet”). Comparing the derived magic rather than the challenge text is what makes a challenge written in upper case the same challenge.
A challenge off signet is refused before the reply is read: a caller passing one has a signet in mind and this client is on no signet at all, which is the caller’s configuration either way.
BtcRpcValueError for a disagreement, the node being the authority on what it serves and the client’s label therefore the thing to fix. FetchError for a reply with nothing to compare – a result that is not a mapping, a chain that is not a string, a signet answering without a signet_challenge member – this being an interpretation of an untrusted reply like any other.
- auth_header() str[source]¶
Return the Basic credential, from the arguments or the cookie.
RFC 7617 leaves the charset of the credential unspecified and Core compares the decoded bytes, so utf-8 is a choice that only matters for a password with a non-ascii character in it – where it is the choice that matches what a shell and a config file would have written.
- call(method: str, params: Sequence[Any] | Mapping[str, Any] | None = None, *, request_timeout: float | None = None, max_body_size: int = 8001024) Any[source]¶
Invoke one rpc method, returning its result.
params is one value, shaped as json-rpc shapes it: a sequence for the positional form, a mapping for the named one. The client’s own controls are keyword-only for that reason – timeout is a parameter of several Core methods, and a signature mixing the two would have to decide which of them owns the name.
Amounts do not travel as binary floating point in either direction: a number in the reply decodes as a Decimal, and a Decimal parameter is refused rather than rounded through float. NaN and Infinity are refused both ways, being what Python writes for floats json has no numbers for.
request_timeout is this call’s, defaulting to the client’s, and for the default transport it bounds the whole exchange – the node’s thinking and the reply’s arrival together. What it is for is the methods that legitimately run long – rescanblockchain, scantxoutset, dumptxoutset – and the replies large enough to take a while on the wire; the alternative is a second client whose wider timeout applies to everything.
max_body_size is what the reply may weigh: widen it for the answers larger than the default, which DEFAULT_MAX_BODY_SIZE names, and tighten it where the reply is a number, this being the caller’s node and the caller’s memory.
There is no retry: one call is one HTTP request, whatever comes back. HttpError.status is what a caller’s own policy reads, and this module’s docstring says why the policy is theirs.
- call_batch(calls: Sequence[tuple[str, Sequence[Any] | Mapping[str, Any] | None]], *, request_timeout: float | None = None, max_body_size: int = 8001024) list[Any][source]¶
Invoke several rpc methods in one HTTP request.
calls is a sequence of (method, params) pairs, one per member, params shaped exactly as call’s own – a sequence for the positional form, a mapping for the named one, None for no parameters at all. Each member is built the way call builds its one request: the 2.0 marker, an id of its own from the same source call draws from, and the same params validation – a member that fails it is refused before anything is sent, naming its position in calls rather than a position in a request Core never sees.
The answer is a list aligned with calls rather than with whatever order the array came back in: position i holds member i’s result, or its RpcError as a value – a batch partly failing is the ordinary case, and raising the first error would discard every answer beside it. JSON-RPC 2.0 section 6 lets the replies arrive in any order, matched by id, and that is how this aligns them: never by position in the reply array.
Only a failure of the whole exchange raises, exactly as call raises – HttpError or FetchError for the lot: a non-2xx status, a reply that is not an array, a reply that cannot be attributed to any member, or a member with no reply among them. Each member’s own reply, once correlated by id, is read by the same _reply_object-then-version discrimination call reads its own with; there is no second parsing branch for a batch’s shape.
request_timeout and max_body_size are call’s own controls, and what each bounds changes shape here: this is one HTTP exchange that is now N node operations, so request_timeout bounds all of them together rather than one, and max_body_size bounds the sum of every member’s reply rather than any one of them – widen either the way a single large call would ask you to, and for the same reason.
An empty calls is refused with BtcRpcValueError: JSON-RPC 2.0 section 6 has no shape for a batch of zero requests, its own rule being that the server’s answer to an invalid batch is a single reply object rather than the array this method promises.
- call_raw(method: str, params: Sequence[Any] | Mapping[str, Any] | None = None, *, jsonrpc: str | None = '2.0', request_timeout: float | None = None, max_body_size: int = 8001024) tuple[int, Any][source]¶
Send one rpc request and hand back the envelope, unread.
The same authenticated POST call builds – this url, the Authorization header, USER_AGENT, a fresh id, the same params validation – with the protocol marker itself an argument rather than the “2.0” call always sends: a string is sent verbatim as jsonrpc, None sends no jsonrpc member at all, and the default is “2.0”, call’s own.
The answer is the pair as it arrived: the HTTP status, and whatever _parsed_json_body safely parses the body into – Decimal numbers, the three non-number constants refused – but not interpreted: no id check, no version discrimination, no RpcError raised, no result extracted, and no shape assumed either. A conformant node answers with a json object, but this is the seam a caller tests a server’s own conformance through, so an array, a bare string or number, or null comes back exactly as parsed rather than being refused the way call’s own reply has to be – _reply_object’s object-shape gate is a rule about a correlated answer, which is one interpretation this method does not make. What the envelope holds is the caller’s own question, so the envelope is the answer, read exactly as far as call reads before it starts asking what the reply means.
Below the status everything stays a FetchError, exactly as http_request promises: a refused connection, an expired timeout, a body that is not json at all – a non-200 status with an unparsable body is HttpError, precisely as it is for call. This is a raw reply, not raw bytes – a caller wanting the bytes has http_request and auth_header() already, both public.
Deliberately out of scope: a request this client refuses to build – a missing method, a non-string one, params that are neither a sequence nor a mapping. A client constructing an invalid request on purpose is a conformance harness’s job, and http_request is the public seam such a harness builds on.
- for_wallet(wallet_name: str) BitcoinCoreRpcClient[source]¶
Return a client for this node’s /wallet/<name> endpoint.
Which is how a node with several wallets loaded is told which one a wallet command is about. The name is percent-encoded, a wallet being a directory and free to be called anything a filesystem accepts: a space, a # or a / written into the path unencoded addresses a different endpoint, or none.
The credentials, the timeout and the transport are this client’s, the endpoint being the only difference – so a caller working on several wallets builds one client and derives the rest, each from that one client and not from another wallet’s: a client that is already a wallet endpoint is one this refuses to extend, naming the client to call it on.
type(self) and not this class by name, as from_chain builds with cls: a subclass that derives a wallet client keeps whatever it added.
- classmethod from_chain(chain: str = 'main', *, user: str | None = None, password: str | None = None, cookie_path: str | ~os.PathLike[str] | None = None, timeout: float = 30.0, transport: ~collections.abc.Callable[[~urllib.request.Request, float], tuple[int, bytes]] = <function urlopen_transport>, verify_chain: bool = False, signet_challenge: str | bytes | bytearray | None = None) BitcoinCoreRpcClient[source]¶
Return a client for the local node of one of Core’s chains.
The convenience of not writing out a loopback url, a port and a datadir: all three come from Core’s own tables, and everything else is the constructor’s. chain is spelled as Core spells it, so main where BIP32 and BIP173 say mainnet; chain_from_network translates for a caller holding a BIP name, and a chain Core has no default port for is an explicit url with a cookie_path, which is the constructor.
It asks the node nothing, so it is no claim that one is listening on that port, nor that it serves this chain if it is. The first call is what finds out – unless verify_chain says to ask now, which is assert_chain and its docstring for what that settles.
Off by default, because a cookie authenticates only that the node is the one this call was told about – a file only that node could have written – and says nothing about which chain it is running: -chain=test and a main cookie both exist. A caller for whom that gap matters – a cookie path or a datadir carried over from a differently-configured host, an environment variable naming the wrong chain – opts in and gets BtcRpcValueError naming both chains instead of a wrong-network call succeeding silently, at the cost of one round trip here rather than trust in every call after.
signet_challenge is the signet the caller means, and is what assert_chain compares by: without it, signet means the default signet and a node on any other is refused. It is the one argument here that does nothing to the client built – every signet answers on 38332 and keeps its cookie in the same subdirectory – so it is refused rather than ignored when verify_chain is off, that being a caller expecting a check that would not be made.
The datadir comes from default_datadir at this call, which is Core’s own for the platform underneath; where there is no absolute directory to hang it off, deriving a cookie path is what this refuses – naming cookie_path as the answer.
Nothing is derived when the caller said who is calling: a user or a password, either of them, is an answer to that question, and the constructor is where the two are held to going together. A cookie derived before that check would report a missing home directory to a caller who passed a password and forgot the user.
- class btclib_wallet.fetch.Broadcaster(*args, **kwargs)[source]¶
Bases:
ProtocolA backend able to announce a signed transaction to the network.
One method and one contract, binding on every implementation of it:
the txid is computed from tx before the request is sent, and a success naming any other txid is refused as a FetchError – the backend answered for a transaction that is not the one it was asked to broadcast;
one request, no retry. After a timeout there is no way to tell a transaction that never reached the backend from one that did and whose acknowledgement was merely lost in transit, so retrying could announce the same signed spend twice for no information gained; whether to try again, and how, is the caller’s decision, made with whatever else it can ask the backend – this is bitcoin-core-rpc’s own one-call contract, carried across;
a refusal keeps its reason. Whatever the backend answered – Core’s RpcError code and message, an explorer’s error body – it reaches the caller through client_errors, text intact and not reshaped into a code common to every backend, because the two refuse for different reasons and a caller acts on the one it got.
- class btclib_wallet.fetch.CachingFetcher(fetcher: Fetcher, max_size: int | None = None)[source]¶
Bases:
FetcherA Fetcher that remembers get_tx and get_block_header answers.
Composes a Fetcher rather than subclassing one: the wrapped fetcher is an attribute, fetcher, and any backend satisfies the interface without knowing it is being cached. get_tx_out is not overridden here, and that is what makes the caching pay off rather than being one more place to keep in step – Fetcher.get_tx_out is concrete and calls self.get_tx, so caching get_tx amortizes it for free: twenty outpoints spending one transaction cost one fetch of it. The consequence is that a wrapped backend which overrides get_tx_out with an answer of its own has that override bypassed – this class’s get_tx_out is the ABC’s, inherited, and never reaches the wrapped fetcher’s. No backend here overrides it, so this only matters for a third-party backend.
What is cached, and for how long, differs per method:
get_tx is cached with no expiry. The id is a hash of the bytes, so the answer cannot change without the question changing.
get_block_header is cached with no expiry too, and that has a consequence worth stating rather than leaving implicit: a height is not an identity, and a reorg can replace the block at one. This class holds no chain tip and has no way to notice. Caching it anyway is still the right default – the caller that repeatedly asks for headers by height is building a chain of them, verified by each header’s previous_block_hash link to the one before it, so a stale entry fails that check loudly rather than being trusted silently.
get_block_count and get_best_block_id are never cached – both are the chain tip, and it moves. Every call reaches the wrapped fetcher.
No expiry was rejected as the general policy for the same two: a lifetime is a second parameter with a clock behind it, untestable without freezing time and wrong for somebody at whatever value it defaulted to. Nothing here hammers the tip – get_tx_out never touches it – so a caller who wants it held briefly holds it in a local variable, which costs one line and needs no policy here.
A raised `FetchError` is never cached. get_tx and get_block_header write their cache only after the wrapped fetcher answers successfully, so a transaction that does not exist yet, or a backend that is briefly down, is asked again next time rather than having its failure remembered forever – negative caching is a policy nobody asked for.
max_size bounds each of the two caches separately rather than pooling them under one shared count: they hold answers of different shape and different traffic – a header is a fixed eighty bytes, a transaction is not – and a caller who floods one by asking for many headers should not evict transactions it never touched. None, the default, leaves both unbounded, which is a real leak in a long-running process; a caller that holds a fetcher for a long time is the one to set it. Eviction is least-recently-used, a hit moving an entry to the end of its OrderedDict and an overflow popping from the front.
- get_block_header(height: int) BlockHeader[source]¶
Return the cached header, or fetch and cache it.
- get_tx(tx_id: bytes | str | bytearray | memoryview) Tx[source]¶
Return the cached transaction, or fetch and cache it.
Keyed on tx_id_hex(tx_id) and not on tx_id itself: Octets is bytes, a bytearray, a memoryview or a hex string, so the same transaction asked for two different ways would otherwise be cached under two different keys and fetched twice – the very amortization this class exists for, lost to the argument’s spelling rather than its value. tx_id_hex is the normalizer Fetcher already validates ids with, so this reuses it rather than writing a second one.
- class btclib_wallet.fetch.ElectrumFetcher(network: str = 'mainnet', *, transport: Callable[[bytes, float], bytes], verify_network: bool = True, timeout: float = 30.0)[source]¶
Bases:
NetworkVerifyingFetcherEvery Fetcher question, and a merkle branch, from an Electrum server.
get_tx is blockchain.transaction.get, the raw hex decoded and handed to tx_from_raw, which recomputes the id the same way every other backend does. get_block_count and get_best_block_id are both blockchain.headers.subscribe, asked once each: the protocol answers the tip’s height together with its header rather than a separate hash field, so the id get_best_block_id returns is not the server’s word – it is BlockHeader.hash of the header block_header_from_raw has already checked is well-formed and cost real work to find, the same check get_block_header runs. get_block_header is blockchain.block.header, cp_height left unsent.
get_tx_merkle is blockchain.transaction.get_merkle, returning the branch and position btclib.electrum.MerkleProof carries. verify_tx fetches the header at the height asked for with get_block_header – the caller’s own height, not the proof’s unchecked block_height – and checks the branch against it with btclib.electrum.verify_merkle_proof, answering False for a malformed branch or a wrong position the way btclib.block.merkle_proof.verify does, and still raising for anything get_block_header itself refuses.
Also a FeeEstimator: estimate_fee is blockchain.estimatefee, answered for the target asked – the protocol carries no field equivalent to Core’s blocks, so unlike the other two backends this one cannot report a clamp the server may have applied underneath it.
get_tx_out is not overridden, and stays the Fetcher base’s derivation from get_tx. The protocol answers a script hash’s history and its unspent outputs, blockchain.scripthash.get_history and .listunspent, but ElectrumFetcher does not ask either question: every Fetcher question is keyed on an identifier the caller already holds – a txid, a height, nothing – and a script is not one.
- assert_network() None[source]¶
Raise unless the server serves the chain this fetcher labels with.
One request and one comparison: blockchain.block.header at height 0 – the same method get_block_header asks for every other height, asked here for the block every chain starts from – against NETWORKS[self.network].genesis_block. What is compared is BlockHeader.hash of the eighty bytes _header_at has checked, so this backend answers the question the way it answers get_best_block_id: by hashing a header rather than by reading a hash the server chose. server.features carries a genesis_hash member and is what Electrum’s own client compares (electrum/interface.py); the header is asked for instead because it makes the server produce eighty bytes that hash to the genesis rather than repeat a string, and because it needs no codec function btclib.electrum does not already have.
Worth the call, because the failure it catches is silent, the same one BitcoinCoreFetcher.assert_network’s docstring names: a fetcher labelled mainnet over a server on another chain renders a mainnet address for every output it fetches, for coins that are not there. It is sharper here than elsewhere, because verify_tx checks a branch against a header fetched from that same server: a caller on the wrong chain is otherwise handed a proof that is valid and about a chain they did not mean.
What a genesis hash cannot separate is two signets, and EsploraFetcher.assert_network’s docstring is where that is written down. This class takes no signet_challenge of its own, for the reason that class takes none: nothing among the methods btclib.electrum speaks is a signet’s challenge to compare against.
- estimate_fee(target: int) FeeQuote[source]¶
Return a fee rate expected to confirm within target blocks.
blockchain.estimatefee. -1, the protocol’s own way of saying no estimate is available, is a decline and raised as a FetchError rather than reaching FeeRate – no sentinel reaches the return type, the way Broadcaster’s third contract bullet already requires of a refusal. Servers commonly proxy this answer from a node’s own estimatesmartfee, so it is read in the same BTC/kvB unit FeeRate.from_btc_per_kvbyte already converts.
- get_best_block_id() bytes[source]¶
Return the id of the chain tip, recomputed from its own header.
headers.subscribe answers the header itself rather than a separate hash field, so this is not the server’s word: it is block_header_from_raw’s check on the bytes it sent, and then the hash of the header that passed it.
- get_block_header(height: int) BlockHeader[source]¶
Return the header at this height, via blockchain.block.header.
- get_tx(tx_id: bytes | str | bytearray | memoryview) Tx[source]¶
Return the transaction with this id, via transaction.get.
- get_tx_merkle(tx_id: bytes | str | bytearray | memoryview, height: int) MerkleProof[source]¶
Return the branch proving tx_id confirmed at height.
blockchain.transaction.get_merkle, the one question no other backend here can answer at all.
- verify_tx(tx_id: bytes | str | bytearray | memoryview, height: int) bool[source]¶
Return whether tx_id is proven confirmed at height.
Fetches the header at height and the branch separately, then checks the second against the first’s merkle root – see this class’s own docstring for what each half already checks on its own.
- class btclib_wallet.fetch.EsploraFetcher(base_url: str, *, network: str = 'mainnet', verify_network: bool = True, timeout: float = 30.0, transport: ~collections.abc.Callable[[~urllib.request.Request, float], tuple[int, bytes]] = <function urlopen_transport>)[source]¶
Bases:
NetworkVerifyingFetcherEvery Fetcher question, answered by an Esplora instance over HTTP.
base_url is required and has no default, for the reason BLOCKSTREAM_INFO is a constant and not one.
Also a Broadcaster: broadcast is POST /tx, the one endpoint of this class that writes. Holding a fetcher is not broadcasting; a caller has to call the method, and broadcast’s own docstring is where its non-idempotence is stated, at the point a caller meets it.
Also a FeeEstimator: estimate_fee is GET /fee-estimates, the one endpoint here answering json rather than plain text and the one answering a fixed set of confirmation targets rather than the one asked for – estimate_fee’s own docstring is where the resolution between the two is stated.
- assert_network() None[source]¶
Raise unless the explorer serves the chain this fetcher labels with.
One request, /block-height/0 – the same endpoint get_block_header reads for every other height, asked here for the block every chain starts from – compared against NETWORKS[self.network].genesis_block.
Worth the call, because the failure it catches is silent, the same one BitcoinCoreFetcher.assert_network’s docstring names: a fetcher labelled mainnet over an explorer serving another chain renders a mainnet address for every output it fetches, for coins that are not there.
Signet is the case a genesis hash cannot settle: Core builds every signet’s genesis from the same parameters (kernel/chainparams.cpp), the challenge going into the message start and not into the block, so this check separates mainnet, testnet, testnet4, signet and regtest, and separates no two signets – the same limit the node backends have when no signet_challenge is given. This class takes no signet_challenge of its own: nothing Esplora’s api publishes is a signet’s challenge to compare it against.
- broadcast(tx: Tx) bytes[source]¶
Announce tx to the server, and return the txid it confirmed.
_verify_once is called first, as every other method here calls it – but a broadcast to a server on the wrong chain is the worst version of the silent failure verify_network exists to catch: a fetch answering with the wrong data is corrected on the next read, where a signed transaction fanned out to the wrong network’s peers cannot be called back.
POST /tx with the wire serialization’s hex, witness included, as the body – what a peer relays and eventually mines, not the stripped form Tx.id hashes. The server answers the same way its GET endpoints do, plain text and not json, which is why this shares _request with text rather than opening a second seam: the bounded read, the transport argument and the client_errors translation are the same regardless of which verb sent the body.
The id is computed from tx before the request, the way tx_from_raw recomputes one from a fetched serialization: a success naming a different txid is a FetchError, the server having confirmed some other transaction. One request and no retry, for the reason BitcoinCoreFetcher.broadcast’s docstring gives: after a timeout this method cannot tell a transaction that never reached the server from one that did and whose acknowledgement was merely lost on the way back, and that is the caller’s decision to make, not this one’s.
A non-200 status is an HttpError carrying the body, the way text reports one for a GET: an Esplora deployment answers a rejected transaction with a 400 and the reason in plain text.
- estimate_fee(target: int) FeeQuote[source]¶
Return a fee rate expected to confirm within target blocks.
GET /fee-estimates, an object from confirmation target to sat/vB – unlike every other endpoint here, in json. target is resolved to the smallest key the deployment actually quotes at or above it: the smaller target’s price is never below the larger’s, so rounding this way over-pays and never under-pays. A target above _MAX_FEE_TARGET is refused before any request is made.
- get_block_header(height: int) BlockHeader[source]¶
Return the header of the block at this height, checked on arrival.
Two requests: /block-height/<height> maps the height to the hash /block/<hash>/header takes, both plain text like the tip endpoints above.
- get_tx(tx_id: bytes | str | bytearray | memoryview) Tx[source]¶
Return the transaction, parsed and checked against its txid.
- text(path: str, max_body_size: int = 8001024) str[source]¶
Return the body of a GET on path, as stripped text.
max_body_size is what this particular answer may weigh; the default is the widest of the bounds above, so a caller asking for something narrow says so.
A status that is not 200 is an HttpError, which carries it: every answer here is a GET of an immutable value, so a 429 or a 503 from a public deployment is the one failure worth another attempt, and telling it from a 404 without reading a message is what the field is for. btclib retries nothing itself – an explorer’s rate limit is the caller’s budget to spend.
- class btclib_wallet.fetch.FallbackFetcher(fetchers: Sequence[Fetcher])[source]¶
Bases:
FetcherA Fetcher that tries a sequence of backends, in order.
Composes the backends rather than subclassing one, fetchers holding the sequence as given. The first one that does not raise answers; a later one is tried only when an earlier one raises FetchError – which HttpError and RpcError both subclass, so a dead socket, an HTTP 500 and “no such transaction” all fall through the same way. BTClibValueError, which Fetcher.__init__ raises for an unknown network and which client_errors raises for a node serving the wrong chain, propagates instead: a misconfigured backend is a caller error to fix, not a failure to paper over by asking the next one.
When every backend raises, this raises a FetchError naming each of them and its failure, chained (raise … from) off the last one – not the last one’s message alone, which would report a second-string backend’s 404 for what began as the primary’s dead socket.
This never compares two answers. It stops at the first backend that does not raise, so on the disagreement two backends could have about the tip, nothing is thrown away: there is only ever one answer in hand. An object that asked every backend and compared their answers would be a quorum fetcher, a different policy with a different cost – worth its own issue if anyone wants it, and not this class.
Refuses an empty sequence at construction: a FallbackFetcher with nothing to fall back to answers no question at all. Refuses a network mismatch too, comparing every backend’s network rather than trusting the first: a mainnet node and a testnet explorer behind one FallbackFetcher is a configuration error that would otherwise surface as a transaction whose outputs are labelled for the wrong chain, wherever the failover happened to land.
- get_block_header(height: int) BlockHeader[source]¶
Answer from the first backend that does not raise FetchError.
- class btclib_wallet.fetch.FeeEstimator(*args, **kwargs)[source]¶
Bases:
ProtocolA backend able to quote a fee rate for a confirmation target.
One method and one contract, binding on every implementation of it:
the target answered for need not be the target asked for, and the FeeQuote returned carries the one the rate is actually valid for;
a quote a backend expresses more finely than FeeRate can hold exactly is rounded up, never down – FeeRate.from_sats_per_vbyte and FeeRate.from_btc_per_kvbyte take round_up=True here, rather than the refusal each raises by default for a caller who is stating a price: refusing an ordinary explorer answer would make the backend unusable, and truncating would under-pay. Rounding a conservative estimate up leaves it an estimate;
a backend that cannot quote a rate for the target – Core’s feerate absent from a reply that carries errors instead, Electrum’s -1 – declines rather than answering, and is raised as a FetchError keeping the backend’s own reason, unreshaped, the way Broadcaster’s third contract bullet already requires of a refusal. No sentinel reaches FeeQuote.
- class btclib_wallet.fetch.FeeQuote(rate: FeeRate, target: int)[source]¶
Bases:
objectA fee rate, and the confirmation target it is valid for.
target is not necessarily the target FeeEstimator.estimate_fee was asked for: a backend may answer for a target it clamped or rounded to, and this is where that answer is carried rather than lost by returning rate alone.
- class btclib_wallet.fetch.Fetcher(network: str = 'mainnet')[source]¶
Bases:
ABCWhat a backend must answer, and what btclib does with the answers.
One abstract method per question, each with a return type of its own. That is the shape on purpose rather than one get(kind, id) returning whatever: a backend able to prove what it says – the Electrum protocol serves a merkle branch, which a client checks against a header it already holds (issues btclib-org/btclib#188, btclib-org/btclib#204 and btclib-org/btclib#1132) – returns evidence beside the data, and evidence is a different type. Adding a method for it is additive; widening the return type of get_tx would be a break for everyone already calling it.
get_tx_out is concrete, and is the one operation every backend can derive from another: an output is a field of the transaction that created it. A backend with a cheaper answer overrides it.
- abstractmethod get_block_header(height: int) BlockHeader[source]¶
Return the header of the block at this height.
Height, and not hash: every backend here answers for a hash and maps a height to one with a second call – getblockhash, /block-height/<height>, /blockhashbyheight/<height>.bin – but the Electrum protocol’s blockchain.block.header takes a height and publishes no call that takes a hash at all. A hash-only signature would be one an Electrum backend could never answer, which is the NotImplementedError-in-an-ABC outcome the questions above already avoid.
Checked on arrival: assert_valid and assert_valid_pow, so a well-formed header answers for eighty bytes that took a real hash to produce. assert_valid_time is not among them – it compares against the local clock, and a header that is genuinely on the chain should not start failing this because the machine running it has drifted.
That is the whole of what the check establishes. Not that the header is on the chain the caller means, not that it is truly at the height asked for, and not that it is the tip: a backend serving a real header from the wrong chain, or from the wrong height, passes it just the same. Only a chain of headers answers that, which is not this method’s question.
- abstractmethod get_tx(tx_id: bytes | str | bytearray | memoryview) Tx[source]¶
Return the transaction with this id.
- get_tx_out(out_point: OutPoint) TxOut[source]¶
Return the output an outpoint names, spent or not.
Spent or not, which is what makes this the useful question and not gettxout’s. bitcoind’s gettxout reads the utxo set, so it answers null for an output that has been spent – and every input of a confirmed transaction names an output that has been spent, by that very transaction. A fee is the inputs less the outputs, so an unspent-only answer cannot compute one.
The cost is that the whole previous transaction is fetched to read one output of it, and against bitcoind that means a node with -txindex.
- class btclib_wallet.fetch.SessionTransport(*, max_body_size: int = 8001024, connection_factory: ~collections.abc.Callable[[str, str, int, float], ~bitcoin_core_rpc.transport._Connection] = <function _new_connection>)[source]¶
Bases:
objectAn HttpTransport that keeps one connection per (scheme, host, port).
urlopen_transport opens a socket, sends Connection: close – not its own choice but urllib’s AbstractHTTPHandler.do_open, which sets the header unconditionally – and lets the node close it. This one does not: it keeps the connection http.client gives it and hands the same one to the next call addressed to the same scheme, host and port, so a caller making many calls against one node pays the connect cost, and on https the TLS handshake, once rather than every time.
max_body_size and the timeout mean what they mean for urlopen_transport: max_body_size bounds what one answer holds in memory, and timeout is a deadline over the whole exchange – connect or reuse, send, and read – taken as one monotonic() reading before any of the three, and it is what _read_bounded reads the response against, the same bounded, chunked read urlopen_transport uses. No redirect is followed: http.client does not follow one on its own, so a 30x already arrives as the status and body of any other response, with nothing here needing to refuse it.
Thread safety. One instance is safe to share between threads, and the contract is one lock guarding the whole exchange rather than one per connection: a socket can carry one request at a time, so two threads sharing a connection have to be serialized somewhere, and guarding only the dict of connections would still let both drive the same socket’s request() and getresponse() at once, which is corruption on the wire rather than a data race Python’s own GIL prevents. Serializing the whole call is what rules that out, at the cost of one instance never running two calls concurrently even across different hosts; a caller wanting that keeps one instance per host, the same shape BitcoinCoreRpcClient already asks a caller’s own transport for.
A kept connection is probed before it is reused. A connection this transport kept open is one the node may since have closed on its own – an idle timeout on the other end, unrelated to anything this transport does – and whether a send() into that socket fails outright, succeeds and only the read afterwards fails, or fails as a plain ConnectionResetError at either step, is a detail of what the peer did (a graceful close() versus a shutdown()) and of timing that this transport does not control and cannot tell apart from a healthy connection’s own silence by guessing. Before every reuse, select.select asks the kept socket whether it is already readable with no request in flight – _is_reused_connection_dead above – which is unambiguous under HTTP/1.1’s one-request-at-a-time shape: a live connection with nothing asked of it reports not-readable. A readable probe evicts the kept connection and opens a fresh one before anything at all is sent, which is not the reconnect below – nothing has been written yet, so there is nothing to have sent twice – and the fresh connection’s own first failure, if the node itself is also unreachable, is the ordinary fresh-connection case: a node not answering, which no reconnect fixes either.
The one legitimate reconnect. What the probe above does not catch is the same drop landing between the probe and the write, or partway through a response already begun – narrower than “closed since the last call” now that reuse itself is guarded, but not closed by a probe run once before the write and never again. Where the write itself is what notices – a BrokenPipeError, a ConnectionResetError or a ConnectionAbortedError out of request() – nothing reached the wire, unambiguously. Where the read is what notices, only http.client.RemoteDisconnected counts: it is http.client’s own signal for an empty line where a status line belongs, which is the one shape of “nothing came back” a read can report with certainty. A bare ConnectionResetError out of getresponse() is not treated the same way, because it is at least as likely to mean the reset landed after a status line was already read as before one arrived – and that is the line a reconnect must not cross, so it is left to propagate rather than guessed at. Either way the one legitimate reconnect is only offered where the connection was already open before this call, and it is offered once: a fresh connection failing the same way is a node not answering, which no reconnect fixes, and a second failure of the reconnect’s own attempt is not caught again. A response whose status line did arrive and then broke – a truncated body, a malformed header – is not this case either: something came back, so the request reached a node that read it, and it is not re-sent, for the reason the module docstring already gives call’s own lack of a retry: the node may still be executing it.
Nothing failed is left pooled. Any exception request() or getresponse() raises that the paragraph above does not resolve into a successful reconnect closes the connection and drops it from the pool before propagating, whether the connection was fresh or reused: a (scheme, host, port) a first attempt could not reach stays usable for the next attempt once the node answers, rather than failing forever on a dead connection object no later call has any way to replace. Only a connection an exchange actually completed over is kept.
Not a pool of several connections per key, and not eviction under memory pressure: one caller talks to one node, sometimes a second for a second wallet, which is one or two keys for the life of the process – the pool a caller polling many nodes would want is closer to what requests or httpx already build.
No connection is opened by the constructor: one is asked for on the first call addressed to a given (scheme, host, port). connection_factory is the seam a test replaces it with, taking the scheme, the host, the port and the timeout and answering something with _Connection’s interface – a fake never opening a real socket, _new_connection above doing exactly that for everything else.
- class btclib_wallet.fetch.TlsLineTransport(host: str, port: int, *, context: SSLContext | None = None, max_line_size: int = 8001024)[source]¶
Bases:
objectA LineTransport over TLS to one server, verifying its certificate.
Constructed with the host and port it connects to, so the instance is the server: ElectrumFetcher(transport=TlsLineTransport(host, port)) is the whole of naming one. Construction opens nothing.
One connection per call. Each call connects, performs the TLS handshake, sends the one request line, reads the one line answering it, and closes. A connection kept across calls is what blockchain.headers.subscribe rules out: the server then pushes a notification line down that same connection at every new block, and a later call reading the next line would read the notification in place of its own answer. What a kept connection would save – the connect and the handshake – is paid on every call instead. Nothing is kept between calls, so concurrent calls share the ssl.SSLContext and nothing else.
The network policy, the one urlopen_transport keeps for HTTP:
a bounded read: the line is refused, not truncated, once it passes max_line_size octets without a newline. The default is DEFAULT_MAX_BODY_SIZE, sized for a whole block as hex, so blockchain.transaction.get fits for any transaction a block holds;
a timeout over the whole exchange – connect, handshake, send and read – rather than per socket operation: each operation is given what is left of one deadline, so a server dripping a line one octet at a time cannot hold the call open past it;
no proxy from the environment: socket.create_connection reads no proxy variable, so the connection goes to the host named here;
TLS verified by default: context defaults to ssl.create_default_context(), which requires a certificate chaining to the default CA certificates it loads and matching host.
A server whose certificate those do not trust – a server of one’s own with a self-signed certificate, say – is reached by passing a context that trusts that certificate, ssl.create_default_context(cafile=…), which keeps both checks on. No flag here turns verification off; what a context a caller supplies accepts is that caller’s decision, as a transport of their own would be. Plain TCP is not offered: a caller who wants it writes that LineTransport.
Everything that goes wrong below the answer – an unresolvable host, a refused connection, a handshake the certificate fails, a timeout, a connection closed before a whole line, a line over the limit – is bitcoin_core_rpc’s FetchError, the contract LineTransport states, which ElectrumFetcher translates into btclib’s own.
- property context: SSLContext¶
Return the context the TLS handshake is verified against.
- btclib_wallet.fetch.urlopen_transport(request: Request, timeout: float, *, max_body_size: int = 8001024) tuple[int, bytes][source]¶
Perform the request with urllib, reading a bounded response.
The default HttpTransport, and the only function here that opens a socket. It maps nothing and interprets nothing: the status and the bytes go back as they arrived, and http_request is where the failures become the exceptions above.
Bounded, and this is the only place a bound can be incremental: the limit is a keyword with a default, so this function still is an HttpTransport. A transport of someone else’s returns bytes it has already read, so all http_request can do for those is refuse to pass an oversized body on.
No redirect is followed: _OPENER above says why, and what a 30x arrives as is the HTTPError any other non-2xx status does.
timeout bounds the exchange and not each socket operation: the deadline is taken before the connect, so a peer that drips a body one octet at a time cannot hold this call open past it.
The scheme, the timeout and the limit are checked here and not only where http_request already checks them, for the reason that function gives for its own copy: this one is public too, and it takes a Request a caller built. urlopen speaks file: and data: as well, so a request whose url came from configuration would otherwise make this transport read the local disk and report the bytes as a node’s answer – and an invalid control would be refused after the resource was opened rather than instead of opening it.