diff --git a/chronik/chronik-http/src/server.rs b/chronik/chronik-http/src/server.rs index 2165b2759..ce8113368 100644 --- a/chronik/chronik-http/src/server.rs +++ b/chronik/chronik-http/src/server.rs @@ -1,239 +1,249 @@ // Copyright (c) 2022 The Bitcoin developers // Distributed under the MIT software license, see the accompanying // file COPYING or http://www.opensource.org/licenses/mit-license.php. //! Module for [`ChronikServer`]. use std::collections::HashMap; use std::{net::SocketAddr, sync::Arc}; use abc_rust_error::{Result, WrapErr}; use axum::{ extract::{Path, Query, WebSocketUpgrade}, response::IntoResponse, routing, Extension, Router, }; use bitcoinsuite_core::tx::TxId; use chronik_indexer::indexer::ChronikIndexer; use chronik_proto::proto; use hyper::server::conn::AddrIncoming; use thiserror::Error; use tokio::sync::RwLock; use crate::{ error::ReportError, handlers, protobuf::Protobuf, ws::handle_subscribe_socket, }; /// Ref-counted indexer with read or write access pub type ChronikIndexerRef = Arc>; /// Params defining what and where to serve for [`ChronikServer`]. #[derive(Clone, Debug)] pub struct ChronikServerParams { /// Host address (port + IP) where to serve Chronik at. pub hosts: Vec, /// Indexer to read data from pub indexer: ChronikIndexerRef, } /// Chronik HTTP server, holding all the data/handles required to serve an /// instance. #[derive(Debug)] pub struct ChronikServer { server_builders: Vec>, indexer: ChronikIndexerRef, } /// Errors for [`ChronikServer`]. #[derive(Debug, Eq, Error, PartialEq)] pub enum ChronikServerError { /// Binding to host address failed #[error("Chronik failed binding to {0}: {1}")] FailedBindingAddress(SocketAddr, String), /// Serving Chronik failed #[error("Chronik failed serving: {0}")] ServingFailed(String), /// Query is neither a hex hash nor an integer string #[error("400: Not a hash or height: {0}")] NotHashOrHeight(String), /// Query is not a txid #[error("400: Not a txid: {0}")] NotTxId(String), /// Block not found in DB #[error("404: Block not found: {0}")] BlockNotFound(String), } use self::ChronikServerError::*; impl ChronikServer { /// Binds the Chronik server on the given hosts pub fn setup(params: ChronikServerParams) -> Result { let server_builders = params .hosts .into_iter() .map(|host| { axum::Server::try_bind(&host).map_err(|err| { FailedBindingAddress(host, err.to_string()).into() }) }) .collect::>>()?; Ok(ChronikServer { server_builders, indexer: params.indexer, }) } /// Serve a Chronik HTTP endpoint with the given parameters. pub async fn serve(self) -> Result<()> { let app = Self::make_router(self.indexer); let servers = self .server_builders .into_iter() .zip(std::iter::repeat(app)) .map(|(server_builder, app)| { Box::pin(async move { server_builder .serve(app.into_make_service()) .await .map_err(|err| ServingFailed(err.to_string())) }) }); let (result, _, _) = futures::future::select_all(servers).await; result?; Ok(()) } fn make_router(indexer: ChronikIndexerRef) -> Router { Router::new() .route("/blockchain-info", routing::get(handle_blockchain_info)) .route("/block/:hash_or_height", routing::get(handle_block)) .route("/blocks/:start/:end", routing::get(handle_block_range)) .route("/tx/:txid", routing::get(handle_tx)) + .route("/raw-tx/:txid", routing::get(handle_raw_tx)) .route( "/script/:type/:payload/confirmed-txs", routing::get(handle_script_confirmed_txs), ) .route( "/script/:type/:payload/history", routing::get(handle_script_history), ) .route( "/script/:type/:payload/unconfirmed-txs", routing::get(handle_script_unconfirmed_txs), ) .route( "/script/:type/:payload/utxos", routing::get(handle_script_utxos), ) .route("/ws", routing::get(handle_ws)) .fallback(handlers::handle_not_found) .layer(Extension(indexer)) } } async fn handle_blockchain_info( Extension(indexer): Extension, ) -> Result, ReportError> { let indexer = indexer.read().await; let blocks = indexer.blocks(); Ok(Protobuf(blocks.blockchain_info()?)) } async fn handle_block_range( Path((start_height, end_height)): Path<(i32, i32)>, Extension(indexer): Extension, ) -> Result, ReportError> { let indexer = indexer.read().await; let blocks = indexer.blocks(); Ok(Protobuf(blocks.by_range(start_height, end_height)?)) } async fn handle_block( Path(hash_or_height): Path, Extension(indexer): Extension, ) -> Result, ReportError> { let indexer = indexer.read().await; let blocks = indexer.blocks(); Ok(Protobuf(blocks.by_hash_or_height(hash_or_height)?)) } async fn handle_tx( Path(txid): Path, Extension(indexer): Extension, ) -> Result, ReportError> { let indexer = indexer.read().await; let txid = txid.parse::().wrap_err(NotTxId(txid))?; Ok(Protobuf(indexer.txs().tx_by_id(txid)?)) } +async fn handle_raw_tx( + Path(txid): Path, + Extension(indexer): Extension, +) -> Result, ReportError> { + let indexer = indexer.read().await; + let txid = txid.parse::().wrap_err(NotTxId(txid))?; + Ok(Protobuf(indexer.txs().raw_tx_by_id(&txid)?)) +} + async fn handle_script_confirmed_txs( Path((script_type, payload)): Path<(String, String)>, Query(query_params): Query>, Extension(indexer): Extension, ) -> Result, ReportError> { let indexer = indexer.read().await; Ok(Protobuf( handlers::handle_script_confirmed_txs( &script_type, &payload, &query_params, &indexer, ) .await?, )) } async fn handle_script_history( Path((script_type, payload)): Path<(String, String)>, Query(query_params): Query>, Extension(indexer): Extension, ) -> Result, ReportError> { let indexer = indexer.read().await; Ok(Protobuf( handlers::handle_script_history( &script_type, &payload, &query_params, &indexer, ) .await?, )) } async fn handle_script_unconfirmed_txs( Path((script_type, payload)): Path<(String, String)>, Extension(indexer): Extension, ) -> Result, ReportError> { let indexer = indexer.read().await; Ok(Protobuf( handlers::handle_script_unconfirmed_txs( &script_type, &payload, &indexer, ) .await?, )) } async fn handle_script_utxos( Path((script_type, payload)): Path<(String, String)>, Extension(indexer): Extension, ) -> Result, ReportError> { let indexer = indexer.read().await; Ok(Protobuf( handlers::handle_script_utxos(&script_type, &payload, &indexer).await?, )) } async fn handle_ws( ws: WebSocketUpgrade, Extension(indexer): Extension, ) -> impl IntoResponse { ws.on_upgrade(|ws| handle_subscribe_socket(ws, indexer)) } diff --git a/chronik/chronik-indexer/src/query/txs.rs b/chronik/chronik-indexer/src/query/txs.rs index 35650bbe0..78bf14ab3 100644 --- a/chronik/chronik-indexer/src/query/txs.rs +++ b/chronik/chronik-indexer/src/query/txs.rs @@ -1,100 +1,125 @@ // Copyright (c) 2023 The Bitcoin developers // Distributed under the MIT software license, see the accompanying // file COPYING or http://www.opensource.org/licenses/mit-license.php. //! Module for [`QueryTxs`], to query txs from mempool/db. use abc_rust_error::{Result, WrapErr}; -use bitcoinsuite_core::tx::{Tx, TxId}; +use bitcoinsuite_core::{ + ser::BitcoinSer, + tx::{Tx, TxId}, +}; use chronik_bridge::ffi; use chronik_db::{ db::Db, io::{BlockReader, SpentByReader, TxReader}, mem::Mempool, }; use chronik_proto::proto; use thiserror::Error; use crate::{ avalanche::Avalanche, query::{make_tx_proto, OutputsSpent}, }; /// Struct for querying txs from the db/mempool. #[derive(Debug)] pub struct QueryTxs<'a> { /// Database pub db: &'a Db, /// Avalanche pub avalanche: &'a Avalanche, /// Mempool pub mempool: &'a Mempool, } /// Errors indicating something went wrong with reading txs. #[derive(Debug, Error, PartialEq)] pub enum QueryTxError { /// Transaction not in mempool nor DB. #[error("404: Transaction {0} not found in the index")] TxNotFound(TxId), /// Transaction in DB without block #[error("500: Inconsistent DB: {0} has no block")] DbTxHasNoBlock(TxId), /// Reading failed, likely corrupted block data #[error("500: Reading {0} failed")] ReadFailure(TxId), } use self::QueryTxError::*; impl<'a> QueryTxs<'a> { /// Query a tx by txid from the mempool or DB. pub fn tx_by_id(&self, txid: TxId) -> Result { match self.mempool.tx(&txid) { Some(tx) => Ok(make_tx_proto( &tx.tx, &OutputsSpent::new_mempool( self.mempool.spent_by().outputs_spent(&txid), ), tx.time_first_seen, false, None, self.avalanche, )), None => { let tx_reader = TxReader::new(self.db)?; let (tx_num, block_tx) = tx_reader .tx_and_num_by_txid(&txid)? .ok_or(TxNotFound(txid))?; let tx_entry = block_tx.entry; let block_reader = BlockReader::new(self.db)?; let spent_by_reader = SpentByReader::new(self.db)?; let block = block_reader .by_height(block_tx.block_height)? .ok_or(DbTxHasNoBlock(txid))?; let tx = ffi::load_tx( block.file_num, tx_entry.data_pos, tx_entry.undo_pos, ) .wrap_err(ReadFailure(txid))?; let outputs_spent = OutputsSpent::query( &spent_by_reader, &tx_reader, self.mempool.spent_by().outputs_spent(&txid), tx_num, )?; Ok(make_tx_proto( &Tx::from(tx), &outputs_spent, tx_entry.time_first_seen, tx_entry.is_coinbase, Some(&block), self.avalanche, )) } } } + + /// Query the raw serialized tx by txid. + /// + /// Serializes the tx if it's in the mempool, or reads the tx data from the + /// node's storage otherwise. + pub fn raw_tx_by_id(&self, txid: &TxId) -> Result { + let raw_tx = match self.mempool.tx(txid) { + Some(mempool_tx) => mempool_tx.tx.ser().to_vec(), + None => { + let tx_reader = TxReader::new(self.db)?; + let block_reader = BlockReader::new(self.db)?; + let block_tx = + tx_reader.tx_by_txid(txid)?.ok_or(TxNotFound(*txid))?; + let block = block_reader + .by_height(block_tx.block_height)? + .ok_or(DbTxHasNoBlock(*txid))?; + ffi::load_raw_tx(block.file_num, block_tx.entry.data_pos) + .wrap_err(ReadFailure(*txid))? + } + }; + Ok(proto::RawTx { raw_tx }) + } } diff --git a/chronik/chronik-proto/proto/chronik.proto b/chronik/chronik-proto/proto/chronik.proto index 61cf8d98e..6b74da3eb 100644 --- a/chronik/chronik-proto/proto/chronik.proto +++ b/chronik/chronik-proto/proto/chronik.proto @@ -1,236 +1,242 @@ // Copyright (c) 2023 The Bitcoin developers // Distributed under the MIT software license, see the accompanying // file COPYING or http://www.opensource.org/licenses/mit-license.php. syntax = "proto3"; package chronik; // Block on the blockchain message Block { // Info about the block BlockInfo block_info = 1; } // Range of blocks message Blocks { // Queried blocks repeated BlockInfo blocks = 1; } // Info about the state of the blockchain. message BlockchainInfo { // Hash (little-endian) of the current tip bytes tip_hash = 1; // Height of the current tip (genesis has height = 0) int32 tip_height = 2; } // Info about a block message BlockInfo { // Hash (little-endian) bytes hash = 1; // Hash of the previous block (little-endian) bytes prev_hash = 2; // Height in the chain int32 height = 3; // nBits field encoding the target uint32 n_bits = 4; // Timestamp field of the block int64 timestamp = 5; // Whether the block has been finalized by Avalanche bool is_final = 14; } // Details about a transaction message Tx { // TxId (little-endian) of the tx bytes txid = 1; // nVersion int32 version = 2; // Inputs of the tx (aka. `vin`) repeated TxInput inputs = 3; // Outputs of the tx (aka. `vout`) repeated TxOutput outputs = 4; // nLockTime uint32 lock_time = 5; // Which block this tx is in, or None, if in the mempool BlockMetadata block = 8; // Time this tx has first been added to the mempool, or 0 if unknown int64 time_first_seen = 9; // Whether this tx is a coinbase tx bool is_coinbase = 12; } // UTXO of a script. message ScriptUtxo { // txid and out_idx of the unspent output. OutPoint outpoint = 1; // Block height of the UTXO, or -1 if in mempool. int32 block_height = 2; // Whether the UTXO has been created in a coinbase tx. bool is_coinbase = 3; // Value of the output, in satoshis. int64 value = 5; // Whether the UTXO has been finalized by Avalanche. bool is_final = 10; } // COutPoint, points to a coin being spent by an input. message OutPoint { // TxId of the tx of the output being spent. bytes txid = 1; // Index of the output spent within the transaction. uint32 out_idx = 2; } // Points to an input spending a coin. message SpentBy { // TxId of the tx with the input. bytes txid = 1; // Index in the inputs of the tx. uint32 input_idx = 2; } // CTxIn, spends a coin. message TxInput { // Reference to the coin being spent. OutPoint prev_out = 1; // scriptSig, script unlocking the coin. bytes input_script = 2; // scriptPubKey, script of the output locking the coin. bytes output_script = 3; // value of the output being spent, in satoshis. int64 value = 4; // nSequence of the input. uint32 sequence_no = 5; } // CTxOut, creates a new coin. message TxOutput { // Value of the coin, in satoshis. int64 value = 1; // scriptPubKey, script locking the output. bytes output_script = 2; // Which tx and input spent this output, if any. SpentBy spent_by = 4; } // Data about a block which a Tx is in. message BlockMetadata { // Height of the block the tx is in. int32 height = 1; // Hash of the block the tx is in. bytes hash = 2; // nTime of the block the tx is in. int64 timestamp = 3; // Whether the block has been finalized by Avalanche. bool is_final = 4; } // Page with txs message TxHistoryPage { // Txs of the page repeated Tx txs = 1; // How many pages there are total uint32 num_pages = 2; // How many txs there are total uint32 num_txs = 3; } // List of UTXOs of a script message ScriptUtxos { // The serialized script of the UTXOs bytes script = 1; // UTXOs of the script. repeated ScriptUtxo utxos = 2; } +// Raw serialized tx. +message RawTx { + // Bytes of the serialized tx. + bytes raw_tx = 1; +} + // Subscription to WebSocket updates. message WsSub { // Set this to `true` to unsubscribe from the event. bool is_unsub = 1; // What kind of updates to subscribe to. oneof sub_type { // Subscription to block updates WsSubBlocks blocks = 2; // Subscription to a script WsSubScript script = 3; } } // Subscription to blocks. They will be sent any time a block got connected, // disconnected or finalized. message WsSubBlocks {} // Subscription to a script. They will be send every time a tx spending the // given script or sending to the given script has been added to/removed from // the mempool, or confirmed in a block. message WsSubScript { // Script type to subscribe to ("p2pkh", "p2sh", "p2pk", "other"). string script_type = 1; // Payload for the given script type: // - 20-byte hash for "p2pkh" and "p2sh" // - 33-byte or 65-byte pubkey for "p2pk" // - Serialized script for "other" bytes payload = 2; } // Message coming from the WebSocket message WsMsg { // Kind of message oneof msg_type { // Error, e.g. when a bad message has been sent into the WebSocket. Error error = 1; // Block got connected, disconnected, finalized, etc. MsgBlock block = 2; // Tx got added to/removed from the mempool, or confirmed in a block. MsgTx tx = 3; } } // Block got connected, disconnected, finalized, etc. message MsgBlock { // What happened to the block BlockMsgType msg_type = 1; // Hash of the block (little-endian) bytes block_hash = 2; // Height of the block int32 block_height = 3; } // Type of message for the block enum BlockMsgType { // Block connected to the blockchain BLK_CONNECTED = 0; // Block disconnected from the blockchain BLK_DISCONNECTED = 1; // Block has been finalized by Avalanche BLK_FINALIZED = 2; } // Tx got added to/removed from mempool, or confirmed in a block, etc. message MsgTx { // What happened to the tx TxMsgType msg_type = 1; // Txid of the tx (little-endian) bytes txid = 2; } // Type of message for a tx enum TxMsgType { // Tx added to the mempool TX_ADDED_TO_MEMPOOL = 0; // Tx removed from the mempool TX_REMOVED_FROM_MEMPOOL = 1; // Tx confirmed in a block TX_CONFIRMED = 2; // Tx finalized by Avalanche TX_FINALIZED = 3; } // Error message returned from our APIs. message Error { // 2, as legacy chronik uses this for the message so we're still compatible. string msg = 2; } diff --git a/test/functional/chronik_raw_tx.py b/test/functional/chronik_raw_tx.py new file mode 100644 index 000000000..46b187f8b --- /dev/null +++ b/test/functional/chronik_raw_tx.py @@ -0,0 +1,93 @@ +#!/usr/bin/env python3 +# Copyright (c) 2023 The Bitcoin developers +# Distributed under the MIT software license, see the accompanying +# file COPYING or http://www.opensource.org/licenses/mit-license.php. +""" +Test Chronik's /raw-tx/:txid endpoint. +""" + +from test_framework.address import ( + ADDRESS_ECREG_P2SH_OP_TRUE, + ADDRESS_ECREG_UNSPENDABLE, + SCRIPTSIG_OP_TRUE, +) +from test_framework.blocktools import GENESIS_CB_TXID +from test_framework.messages import COutPoint, CTransaction, CTxIn, CTxOut +from test_framework.script import OP_EQUAL, OP_HASH160, CScript, hash160 +from test_framework.test_framework import BitcoinTestFramework +from test_framework.util import assert_equal + + +class ChronikRawTxTest(BitcoinTestFramework): + def set_test_params(self): + self.setup_clean_chain = True + self.num_nodes = 1 + self.extra_args = [['-chronik']] + self.rpc_timeout = 240 + + def skip_test_if_missing_module(self): + self.skip_if_no_chronik() + + def run_test(self): + from test_framework.chronik.client import ChronikClient, pb + + node = self.nodes[0] + chronik = ChronikClient('127.0.0.1', node.chronik_port) + + assert_equal(chronik.tx('0').err(400).msg, '400: Not a txid: 0') + assert_equal(chronik.tx('123').err(400).msg, '400: Not a txid: 123') + assert_equal(chronik.tx('1234f').err(400).msg, '400: Not a txid: 1234f') + assert_equal(chronik.tx('00' * 31).err(400).msg, f'400: Not a txid: {"00"*31}') + assert_equal(chronik.tx('01').err(400).msg, '400: Not a txid: 01') + assert_equal(chronik.tx('12345678901').err(400).msg, + '400: Not a txid: 12345678901') + + assert_equal(chronik.tx('00' * 32).err(404).msg, + f'404: Transaction {"00"*32} not found in the index') + + # Verify queried genesis tx matches + # Note: unlike getrawtransaction, this also works on the Genesis coinbase + assert_equal( + chronik.raw_tx(GENESIS_CB_TXID).ok(), + pb.RawTx(raw_tx=bytes.fromhex( + '0100000001000000000000000000000000000000000000000000000000000000000000' + '0000ffffffff4d04ffff001d0104455468652054696d65732030332f4a616e2f323030' + '39204368616e63656c6c6f72206f6e206272696e6b206f66207365636f6e6420626169' + '6c6f757420666f722062616e6b73ffffffff0100f2052a01000000434104678afdb0fe' + '5548271967f1a67130b7105cd6a828e03909a67962e0ea1f61deb649f6bc3f4cef38c4' + 'f35504e51ec112de5c384df7ba0b8d578a4c702b6bf11d5fac00000000' + )), + ) + + coinblockhash = self.generatetoaddress(node, 1, ADDRESS_ECREG_P2SH_OP_TRUE)[0] + coinblock = node.getblock(coinblockhash) + cointx = coinblock['tx'][0] + + self.generatetoaddress(node, 100, ADDRESS_ECREG_UNSPENDABLE) + + coinvalue = 5000000000 + send_values = [coinvalue - 10000, 1000, 2000, 3000] + send_redeem_scripts = [bytes([i + 0x52]) for i in range(len(send_values))] + send_scripts = [CScript([OP_HASH160, hash160(redeem_script), OP_EQUAL]) + for redeem_script in send_redeem_scripts] + tx = CTransaction() + tx.nVersion = 2 + tx.vin = [CTxIn(outpoint=COutPoint(int(cointx, 16), 0), + scriptSig=SCRIPTSIG_OP_TRUE, + nSequence=0xfffffffe)] + tx.vout = [CTxOut(value, script) + for (value, script) in zip(send_values, send_scripts)] + tx.nLockTime = 1234567890 + + # Submit tx to mempool + raw_tx = tx.serialize() + txid = node.sendrawtransaction(raw_tx.hex()) + assert_equal(chronik.raw_tx(txid).ok(), pb.RawTx(raw_tx=raw_tx)) + + # Mined block still works + self.generatetoaddress(node, 1, ADDRESS_ECREG_UNSPENDABLE) + assert_equal(chronik.raw_tx(txid).ok(), pb.RawTx(raw_tx=raw_tx)) + + +if __name__ == '__main__': + ChronikRawTxTest().main() diff --git a/test/functional/test_framework/chronik/client.py b/test/functional/test_framework/chronik/client.py index aa35f33e2..34b018c45 100644 --- a/test/functional/test_framework/chronik/client.py +++ b/test/functional/test_framework/chronik/client.py @@ -1,160 +1,163 @@ #!/usr/bin/env python3 # Copyright (c) 2023 The Bitcoin developers # Distributed under the MIT software license, see the accompanying # file COPYING or http://www.opensource.org/licenses/mit-license.php. import http.client from typing import Union import chronik_pb2 as pb import websocket # Timespan when HTTP requests to Chronik time out DEFAULT_TIMEOUT = 30 class UnexpectedContentType(Exception): pass class ChronikResponse: def __init__(self, status: int, *, ok_proto=None, error_proto=None) -> None: self.status = status self.ok_proto = ok_proto self.error_proto = error_proto def ok(self): if self.status != 200: raise AssertionError( f'Expected OK response, but got status {self.status}, error: ' f'{self.error_proto}') return self.ok_proto def err(self, status: int): if self.status == 200: raise AssertionError( f'Expected error response status {status}, but got OK: {self.ok_proto}') if self.status != status: raise AssertionError( f'Expected error response status {status}, but got different error ' f'status {self.status}, error: {self.error_proto}') return self.error_proto class ChronikScriptClient: def __init__(self, client: 'ChronikClient', script_type: str, script_payload: str) -> None: self.client = client self.script_type = script_type self.script_payload = script_payload def _query_params(self, page=None, page_size=None) -> str: if page is not None and page_size is not None: return f'?page={page}&page_size={page_size}' elif page is not None: return f'?page={page}' elif page_size is not None: return f'?page_size={page_size}' else: return '' def confirmed_txs(self, page=None, page_size=None): query = self._query_params(page, page_size) return self.client._request_get( f'/script/{self.script_type}/{self.script_payload}/confirmed-txs{query}', pb.TxHistoryPage) def history(self, page=None, page_size=None): query = self._query_params(page, page_size) return self.client._request_get( f'/script/{self.script_type}/{self.script_payload}/history{query}', pb.TxHistoryPage) def unconfirmed_txs(self): return self.client._request_get( f'/script/{self.script_type}/{self.script_payload}/unconfirmed-txs', pb.TxHistoryPage) def utxos(self): return self.client._request_get( f'/script/{self.script_type}/{self.script_payload}/utxos', pb.ScriptUtxos) class ChronikWs: def __init__(self, ws) -> None: self.ws = ws def recv(self): data = self.ws.recv() ws_msg = pb.WsMsg() ws_msg.ParseFromString(data) return ws_msg def send_bytes(self, data: bytes) -> None: self.ws.send(data, websocket.ABNF.OPCODE_BINARY) def sub_to_blocks(self, *, is_unsub=False) -> None: sub = pb.WsSub(is_unsub=is_unsub, blocks=pb.WsSubBlocks()) self.send_bytes(sub.SerializeToString()) def sub_script(self, script_type: str, payload: bytes, *, is_unsub=False) -> None: sub = pb.WsSub( is_unsub=is_unsub, script=pb.WsSubScript(script_type=script_type, payload=payload), ) self.send_bytes(sub.SerializeToString()) class ChronikClient: CONTENT_TYPE = 'application/x-protobuf' def __init__(self, host: str, port: int, timeout=DEFAULT_TIMEOUT) -> None: self.host = host self.port = port self.timeout = timeout def _request_get(self, path: str, pb_type): kwargs = {} if self.timeout is not None: kwargs['timeout'] = self.timeout client = http.client.HTTPConnection(self.host, self.port, **kwargs) client.request('GET', path) response = client.getresponse() content_type = response.getheader('Content-Type') body = response.read() if content_type != self.CONTENT_TYPE: raise UnexpectedContentType( f'Unexpected Content-Type "{content_type}" (expected ' f'"{self.CONTENT_TYPE}"), body: {repr(body)}' ) if response.status != 200: proto_error = pb.Error() proto_error.ParseFromString(body) return ChronikResponse(response.status, error_proto=proto_error) ok_proto = pb_type() ok_proto.ParseFromString(body) return ChronikResponse(response.status, ok_proto=ok_proto) def blockchain_info(self) -> ChronikResponse: return self._request_get('/blockchain-info', pb.BlockchainInfo) def block(self, hash_or_height: Union[str, int]) -> ChronikResponse: return self._request_get(f'/block/{hash_or_height}', pb.Block) def blocks(self, start_height: int, end_height: int) -> ChronikResponse: return self._request_get(f'/blocks/{start_height}/{end_height}', pb.Blocks) def tx(self, txid: str) -> ChronikResponse: return self._request_get(f'/tx/{txid}', pb.Tx) + def raw_tx(self, txid: str) -> bytes: + return self._request_get(f'/raw-tx/{txid}', pb.RawTx) + def script(self, script_type: str, script_payload: str) -> ChronikScriptClient: return ChronikScriptClient(self, script_type, script_payload) def ws(self, *, timeout=None) -> ChronikWs: ws = websocket.WebSocket() ws.connect(f'ws://{self.host}:{self.port}/ws', timeout=timeout) return ChronikWs(ws)