btclib.fetch package¶
Submodules¶
btclib.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.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.fetch.bitcoin_core.BitcoinCoreFetcher(client: BitcoinCoreRpcClient, network: str = 'mainnet', *, verify_network: bool = True, signet_challenge: str | bytes | None = None)[source]¶
Bases:
FetcherThe three fetcher questions, answered by a node over its RPC.
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.
verify_network is who asks it. On by default and before the first fetch rather than in this 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 node that is merely down should not be a failure to construct anything. The answer is then kept – a node 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.
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.
- assert_network() None[source]¶
Raise unless the node serves the chain this fetcher labels with.
One round trip, asked once and not per fetch: the answer cannot change under a client that goes on pointing at the same node. verify_network is what asks it before the first fetch, and this stays public for a caller that wants the question answered at a moment of its own – at startup, or after a client was repointed.
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.
A malformed reply is a FetchError. A disagreement is a BTClibValueError: the node is the authority on which chain it serves, so the fetcher’s label is the thing to fix.
- 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.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.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.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 three 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 three answers behind the same interface. What it does not promise is that they are true. A node validated the chain it reports; an explorer is a host on the internet that says it did. get_tx is the one answer that checks itself, in tx_from_raw – the serialization comes back and the id is recomputed from it, so a substituted transaction is caught – and the height and the tip hash are taken on trust, there being nothing here to check them against. Nor does anything here say a transaction is confirmed: the answer to that is a merkle branch against a header, which is what the Electrum backend of issue #204 would add and this one cannot. That is the trade the fallback exists to offer, and SECURITY.md states it.
Three endpoints, each answering in plain text rather than json: /tx/<txid>/hex, /blocks/tip/height and /blocks/tip/hash. 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 those three 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.fetch.esplora.EsploraFetcher(base_url: str, *, network: str = 'mainnet', timeout: float = 30.0, transport: ~collections.abc.Callable[[~urllib.request.Request, float], tuple[int, bytes]] = <function urlopen_transport>)[source]¶
Bases:
FetcherThe three questions, 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.
- 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.
Stripped because a deployment behind a proxy may add a newline and none of the three answers can contain whitespace. Decoded with replace rather than strictly: the body of a failure is an error page from whatever is in the way, and it is more use rendered imperfectly than swallowed by a UnicodeDecodeError.
max_body_size is what this particular answer may weigh; the default is the widest of the three, 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.
client_errors because http_request is bitcoin_core_rpc’s and raises that package’s exceptions: a refused connection reaching a caller as something no except FetchError of btclib’s catches is what it is there to prevent.
btclib.fetch.fetcher module¶
The interface a chain backend answers, whichever backend it is.
Three questions btclib cannot answer from bytes it was handed: what transaction has this id, what output does this outpoint name, and where is the chain tip. 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 – 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.fetch.fetcher.Fetcher(network: str = 'mainnet')[source]¶
Bases:
ABCWhat a backend must answer, and what btclib does with the answers.
Three abstract methods, one 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 #188 and #204) – 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_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.
- btclib.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 three 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: EsploraFetcher.text and BitcoinCoreFetcher._call are the two lines that do.
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.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 three name the converter and not the host, and the host is what has to be fixed.
- btclib.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.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.
Both backends answer 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. No other answer here can be checked at all – a height and a tip hash are taken on the backend’s word – and 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.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.
Both backends put the id in a request as hex, and both accept whatever Octets accepts, so both need 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.fetch.transport module¶
The standard-library HTTP transport, re-exported under btclib’s name.
The 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.
Two implementations satisfy it. 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.fetch.fetcher.client_errors is what translates them, and every call into this module from inside btclib is wrapped in it.
- btclib.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.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.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, and it is implemented twice – BitcoinCoreFetcher over a full node’s JSON-RPC, EsploraFetcher over a block explorer’s HTTP api – so that calling code takes a Fetcher and never branches on which one it got.
It adds no dependency. urllib.request, json and base64 from the standard library are the whole of the client. Its canonical implementation is the bitcoin-core-rpc package, which btclib depends on and does not contain; the transport exports here are aliases to that same source, not another implementation. The seam lets the test suite exercise all of this while opening no socket.
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.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 two fetchers, the interface they implement, the rpc client a Bitcoin Core one is reached through, and the transport seam: the timeout, the protocol a substitute has to satisfy and the two implementations of it that open a socket – one connection per call, and one kept open across calls. That last group is here because the seam is the supported way to test calling code without a node, which is not a detail of the two 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. fetcher.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 third implementation rather than to a caller of the two – from btclib.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.
- class btclib.fetch.BitcoinCoreFetcher(client: BitcoinCoreRpcClient, network: str = 'mainnet', *, verify_network: bool = True, signet_challenge: str | bytes | None = None)[source]¶
Bases:
FetcherThe three fetcher questions, answered by a node over its RPC.
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.
verify_network is who asks it. On by default and before the first fetch rather than in this 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 node that is merely down should not be a failure to construct anything. The answer is then kept – a node 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.
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.
- assert_network() None[source]¶
Raise unless the node serves the chain this fetcher labels with.
One round trip, asked once and not per fetch: the answer cannot change under a client that goes on pointing at the same node. verify_network is what asks it before the first fetch, and this stays public for a caller that wants the question answered at a moment of its own – at startup, or after a client was repointed.
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.
A malformed reply is a FetchError. A disagreement is a BTClibValueError: the node is the authority on which chain it serves, so the fetcher’s label is the thing to fix.
- 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.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.fetch.EsploraFetcher(base_url: str, *, network: str = 'mainnet', timeout: float = 30.0, transport: ~collections.abc.Callable[[~urllib.request.Request, float], tuple[int, bytes]] = <function urlopen_transport>)[source]¶
Bases:
FetcherThe three questions, 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.
- 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.
Stripped because a deployment behind a proxy may add a newline and none of the three answers can contain whitespace. Decoded with replace rather than strictly: the body of a failure is an error page from whatever is in the way, and it is more use rendered imperfectly than swallowed by a UnicodeDecodeError.
max_body_size is what this particular answer may weigh; the default is the widest of the three, 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.
client_errors because http_request is bitcoin_core_rpc’s and raises that package’s exceptions: a refused connection reaching a caller as something no except FetchError of btclib’s catches is what it is there to prevent.
- class btclib.fetch.Fetcher(network: str = 'mainnet')[source]¶
Bases:
ABCWhat a backend must answer, and what btclib does with the answers.
Three abstract methods, one 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 #188 and #204) – 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_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.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.
- btclib.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.