diff --git a/chronik/chronik-http/src/electrum.rs b/chronik/chronik-http/src/electrum.rs
index 4f6bb195a..8026aa5be 100644
--- a/chronik/chronik-http/src/electrum.rs
+++ b/chronik/chronik-http/src/electrum.rs
@@ -1,249 +1,377 @@
 // Copyright (c) 2024 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 [`ChronikElectrumServer`].
 
+use std::str::FromStr;
 use std::{net::SocketAddr, sync::Arc};
 
 use abc_rust_error::Result;
+use bitcoinsuite_core::{
+    hash::{Hashed, Sha256},
+    tx::TxId,
+};
 use futures::future;
 use itertools::izip;
 use karyon_jsonrpc::{RPCError, RPCMethod, RPCService, Server};
 use karyon_net::{Addr, Endpoint};
 use rustls::pki_types::{
     pem::PemObject,
     {CertificateDer, PrivateKeyDer},
 };
-use serde_json::Value;
+use serde_json::{json, Value};
 use thiserror::Error;
 
 use crate::{
     server::{ChronikIndexerRef, NodeRef},
     {electrum::ChronikElectrumServerError::*, electrum_codec::ElectrumCodec},
 };
 
 /// Chronik Electrum protocol
 #[derive(Clone, Copy, Debug)]
 pub enum ChronikElectrumProtocol {
     /// TCP
     Tcp,
     /// TLS
     Tls,
 }
 
 /// Params defining what and where to serve for [`ChronikElectrumServer`].
 #[derive(Clone, Debug)]
 pub struct ChronikElectrumServerParams {
     /// Host address (port + IP) where to serve the electrum server at.
     pub hosts: Vec<(SocketAddr, ChronikElectrumProtocol)>,
     /// Indexer to read data from
     pub indexer: ChronikIndexerRef,
     /// Access to the bitcoind node
     pub node: NodeRef,
     /// The TLS certificate chain file
     pub tls_cert_path: String,
     /// The TLS private key file
     pub tls_privkey_path: String,
 }
 
 /// Chronik Electrum server, holding all the data/handles required to serve an
 /// instance.
 #[derive(Debug)]
 pub struct ChronikElectrumServer {
     hosts: Vec<(SocketAddr, ChronikElectrumProtocol)>,
-    _indexer: ChronikIndexerRef,
-    _node: NodeRef,
+    indexer: ChronikIndexerRef,
+    node: NodeRef,
     tls_cert_path: String,
     tls_privkey_path: String,
 }
 
 /// Errors for [`ChronikElectrumServer`].
 #[derive(Debug, Eq, Error, PartialEq)]
 pub enum ChronikElectrumServerError {
     /// Binding to host address failed
     #[error("Chronik Electrum failed binding to {0}: {1}")]
     FailedBindingAddress(SocketAddr, String),
 
     /// Serving Electrum failed
     #[error("Chronik Electrum failed serving: {0}")]
     ServingFailed(String),
 
     /// Chronik Electrum TLS invalid configuration
     #[error("Chronik Electrum TLS configuration is invalid: {0}")]
     InvalidTlsConfiguration(String),
 
     /// Chronik Electrum TLS configuration failed
     #[error("Chronik Electrum TLS configuration failed: {0}")]
     TlsConfigurationFailed(String),
 
     /// Missing certificate chain file
     #[error(
         "Chronik Electrum TLS configuration requires a certificate chain file \
          (see -chronikelectrumcert)"
     )]
     MissingCertificateFile,
 
     /// Certificate chain file not found
     #[error(
         "Chronik Electrum TLS configuration failed to open the certificate \
          chain file {0}: {1}"
     )]
     CertificateFileNotFound(String, String),
 
     /// Missing private key file
     #[error(
         "Chronik Electrum TLS configuration requires a private key file (see \
          -chronikelectrumprivkey)"
     )]
     MissingPrivateKeyFile,
 
     /// Private key file not found
     #[error(
         "Chronik Electrum TLS configuration failed to open the private key \
          file {0}, {1}"
     )]
     PrivateKeyFileNotFound(String, String),
 }
 
 struct ChronikElectrumRPCServerEndpoint {}
 
+struct ChronikElectrumRPCBlockchainEndpoint {
+    indexer: ChronikIndexerRef,
+    node: NodeRef,
+}
+
 impl ChronikElectrumServer {
     /// Binds the Chronik server on the given hosts
     pub fn setup(params: ChronikElectrumServerParams) -> Result<Self> {
         Ok(ChronikElectrumServer {
             hosts: params.hosts,
-            // FIXME below params are unused but will be used in the future
-            _indexer: params.indexer,
-            _node: params.node,
+            indexer: params.indexer,
+            node: params.node,
             tls_cert_path: params.tls_cert_path,
             tls_privkey_path: params.tls_privkey_path,
         })
     }
 
     /// Start the Chronik electrum server
     pub async fn serve(self) -> Result<()> {
         // The behavior is to bind the endpoint name to its method name like so:
         // endpoint.method as the name of the RPC
         let server_endpoint = Arc::new(ChronikElectrumRPCServerEndpoint {});
+        let blockchain_endpoint =
+            Arc::new(ChronikElectrumRPCBlockchainEndpoint {
+                indexer: self.indexer,
+                node: self.node,
+            });
 
         let tls_cert_path = self.tls_cert_path.clone();
         let tls_privkey_path = self.tls_privkey_path.clone();
 
         let servers = izip!(
             self.hosts,
             std::iter::repeat(server_endpoint),
+            std::iter::repeat(blockchain_endpoint),
             std::iter::repeat(tls_cert_path),
             std::iter::repeat(tls_privkey_path)
         )
         .map(
             |(
                 (host, protocol),
                 server_endpoint,
+                blockchain_endpoint,
                 tls_cert_path,
                 tls_privkey_path,
             )| {
                 Box::pin(async move {
                     let mut require_tls_config = false;
                     // Don't use the karyon Endpoint parsing as it doesn't
                     // appear to support IPv6.
                     let endpoint = match protocol {
                         ChronikElectrumProtocol::Tcp => {
                             Endpoint::Tcp(Addr::Ip(host.ip()), host.port())
                         }
                         ChronikElectrumProtocol::Tls => {
                             require_tls_config = true;
                             Endpoint::Tls(Addr::Ip(host.ip()), host.port())
                         }
                     };
 
                     let mut builder = Server::builder_with_json_codec(
                         endpoint,
                         ElectrumCodec {},
                     )
                     .map_err(|err| {
                         FailedBindingAddress(host, err.to_string())
                     })?;
 
                     if require_tls_config {
                         if tls_cert_path.is_empty() {
                             return Err(MissingCertificateFile);
                         }
                         if tls_privkey_path.is_empty() {
                             return Err(MissingPrivateKeyFile);
                         }
 
                         let certs: Vec<_> = CertificateDer::pem_file_iter(
                             tls_cert_path.as_str(),
                         )
                         .map_err(|err| {
                             CertificateFileNotFound(
                                 tls_cert_path,
                                 err.to_string(),
                             )
                         })?
                         .map(|cert| cert.unwrap())
                         .collect();
                         let private_key = PrivateKeyDer::from_pem_file(
                             tls_privkey_path.as_str(),
                         )
                         .map_err(|err| {
                             PrivateKeyFileNotFound(
                                 tls_privkey_path,
                                 err.to_string(),
                             )
                         })?;
 
                         let tls_config = rustls::ServerConfig::builder()
                             .with_no_client_auth()
                             .with_single_cert(certs, private_key)
                             .map_err(|err| {
                                 InvalidTlsConfiguration(err.to_string())
                             })?;
                         builder =
                             builder.tls_config(tls_config).map_err(|err| {
                                 TlsConfigurationFailed(err.to_string())
                             })?;
                     }
 
                     let server = builder
                         .service(server_endpoint)
+                        .service(blockchain_endpoint)
                         .build()
                         .await
                         .map_err(|err| ServingFailed(err.to_string()))?;
                     server.start();
 
                     let () = future::pending().await;
 
                     Ok::<(), ChronikElectrumServerError>(())
                 })
             },
         );
 
         let (result, _, _) = futures::future::select_all(servers).await;
         result?;
         Ok(())
     }
 }
 
 impl RPCService for ChronikElectrumRPCServerEndpoint {
     fn name(&self) -> String {
         "server".to_string()
     }
 
     fn get_method(&self, name: &str) -> Option<RPCMethod<'_>> {
         match name {
             // TODO Create a macro to generate this or avoid duplicated code.
             "ping" => {
                 Some(Box::new(move |params: Value| Box::pin(self.ping(params))))
             }
             _ => None,
         }
     }
 }
 
 impl ChronikElectrumRPCServerEndpoint {
     async fn ping(&self, _params: Value) -> Result<Value, RPCError> {
         Ok(Value::Null)
     }
 }
+
+impl RPCService for ChronikElectrumRPCBlockchainEndpoint {
+    fn name(&self) -> String {
+        "blockchain".to_string()
+    }
+
+    fn get_method(&self, name: &str) -> Option<RPCMethod<'_>> {
+        match name {
+            "transaction.get" => Some(Box::new(move |params: Value| {
+                Box::pin(self.transaction_get(params))
+            })),
+            _ => None,
+        }
+    }
+}
+
+impl ChronikElectrumRPCBlockchainEndpoint {
+    async fn transaction_get(&self, params: Value) -> Result<Value, RPCError> {
+        let txid_hex: Value;
+        let verbose: Value;
+        match params {
+            Value::Array(arr) => {
+                txid_hex = arr
+                    .first()
+                    .ok_or(RPCError::InvalidParams(
+                        "Missing mandatory 'txid' parameter",
+                    ))?
+                    .clone();
+                verbose = match arr.get(1) {
+                    Some(val) => val.clone(),
+                    None => Value::Bool(false),
+                };
+            }
+            Value::Object(obj) => {
+                txid_hex = match obj.get("txid") {
+                    Some(txid) => Ok(txid.clone()),
+                    None => Err(RPCError::InvalidParams(
+                        "Missing mandatory 'txid' parameter",
+                    )),
+                }?;
+                verbose = match obj.get("verbose") {
+                    Some(verbose) => verbose.clone(),
+                    None => Value::Bool(false),
+                };
+            }
+            _ => {
+                return Err(RPCError::InvalidParams(
+                    "'params' must be an array or an object",
+                ))
+            }
+        };
+        let txid_hex = match txid_hex {
+            Value::String(s) => Ok(s),
+            _ => Err(RPCError::InvalidParams(
+                "'txid' must be a hexadecimal string",
+            )),
+        }?;
+        let txid = TxId::from_str(&txid_hex)
+            .or(Err(RPCError::InvalidParams("Failed to parse txid")))?;
+
+        let verbose = match verbose {
+            Value::Bool(v) => Ok(v),
+            _ => Err(RPCError::InvalidParams("'verbose' must be a boolean")),
+        }?;
+
+        let indexer = self.indexer.read().await;
+        let query_tx = indexer.txs(&self.node);
+        let raw_tx = hex::encode(
+            query_tx
+                .raw_tx_by_id(&txid)
+                .or(Err(RPCError::InvalidRequest("Unknown transaction id")))?
+                .raw_tx,
+        );
+        if !verbose {
+            return Ok(Value::String(raw_tx));
+        }
+
+        let tx = query_tx
+            .tx_by_id(txid)
+            // The following error should be unreachable, unless raw_tx_by_id
+            // and tx_by_id are inconsistent
+            .or(Err(RPCError::InvalidRequest("Unknown transaction id")))?;
+        let blockchaininfo = indexer.blocks(&self.node).blockchain_info();
+        if blockchaininfo.is_err() {
+            return Err(RPCError::InternalError);
+        }
+        if tx.block.is_none() {
+            // mempool transaction
+            return Ok(json!({
+                "confirmations": 0,
+                "hash": txid_hex,
+                "hex": raw_tx,
+                "time": tx.time_first_seen,
+            }));
+        }
+        let block = tx.block.unwrap();
+        let blockhash = Sha256::from_le_slice(block.hash.as_ref()).unwrap();
+        let confirmations =
+            blockchaininfo.ok().unwrap().tip_height - block.height + 1;
+        // TODO: more verbose fields, inputs, outputs
+        //      (but for now only "confirmations" is used in Electrum ABC)
+        Ok(json!({
+            "blockhash": blockhash.hex_be(),
+            "blocktime": block.timestamp,
+            "confirmations": confirmations,
+            "hash": txid_hex,
+            "hex": raw_tx,
+            "time": tx.time_first_seen,
+        }))
+    }
+}
diff --git a/test/functional/chronik_electrum_blockchain.py b/test/functional/chronik_electrum_blockchain.py
new file mode 100644
index 000000000..d7ada20ff
--- /dev/null
+++ b/test/functional/chronik_electrum_blockchain.py
@@ -0,0 +1,71 @@
+# Copyright (c) 2024-present 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 electrum interface: blockchain.* methods
+"""
+from test_framework.blocktools import (
+    GENESIS_BLOCK_HASH,
+    GENESIS_CB_SCRIPT_PUBKEY,
+    GENESIS_CB_SCRIPT_SIG,
+    GENESIS_CB_TXID,
+    TIME_GENESIS_BLOCK,
+)
+from test_framework.test_framework import BitcoinTestFramework
+from test_framework.util import assert_equal
+
+COINBASE_TX_HEX = (
+    "01000000010000000000000000000000000000000000000000000000000000000000000000ffffffff4d"
+    + GENESIS_CB_SCRIPT_SIG.hex()
+    + "ffffffff0100f2052a0100000043"
+    + GENESIS_CB_SCRIPT_PUBKEY.hex()
+    + "00000000"
+)
+
+
+class ChronikElectrumBlockchain(BitcoinTestFramework):
+    def set_test_params(self):
+        self.setup_clean_chain = True
+        self.num_nodes = 1
+        self.extra_args = [["-chronik"]]
+
+    def skip_test_if_missing_module(self):
+        self.skip_if_no_chronik()
+
+    def run_test(self):
+        self.client = self.nodes[0].get_chronik_electrum_client()
+
+        for response in (
+            self.client.blockchain.transaction.get(GENESIS_CB_TXID),
+            self.client.blockchain.transaction.get(GENESIS_CB_TXID, False),
+            self.client.blockchain.transaction.get(txid=GENESIS_CB_TXID),
+            self.client.blockchain.transaction.get(txid=GENESIS_CB_TXID, verbose=False),
+        ):
+            assert_equal(response.result, COINBASE_TX_HEX)
+
+        for response in (
+            self.client.blockchain.transaction.get(GENESIS_CB_TXID, True),
+            self.client.blockchain.transaction.get(txid=GENESIS_CB_TXID, verbose=True),
+        ):
+            assert_equal(
+                response.result,
+                {
+                    "blockhash": GENESIS_BLOCK_HASH,
+                    "blocktime": TIME_GENESIS_BLOCK,
+                    "confirmations": 1,
+                    "hash": GENESIS_CB_TXID,
+                    "hex": COINBASE_TX_HEX,
+                    "time": 0,
+                },
+            )
+        self.generate(self.nodes[0], 2)
+        assert_equal(
+            self.client.blockchain.transaction.get(
+                txid=GENESIS_CB_TXID, verbose=True
+            ).result["confirmations"],
+            3,
+        )
+
+
+if __name__ == "__main__":
+    ChronikElectrumBlockchain().main()