diff --git a/Cargo.lock b/Cargo.lock --- a/Cargo.lock +++ b/Cargo.lock @@ -656,6 +656,7 @@ "hex-literal", "ripemd", "serde", + "serde_json", "sha2", "thiserror 2.0.4", ] diff --git a/chronik/bitcoinsuite-core/Cargo.toml b/chronik/bitcoinsuite-core/Cargo.toml --- a/chronik/bitcoinsuite-core/Cargo.toml +++ b/chronik/bitcoinsuite-core/Cargo.toml @@ -25,6 +25,9 @@ # Serialize structs serde = { version = "1.0", features = ["derive"] } +# JSON en-/decoding +serde_json = "1.0.133" + # Implementation of SHA-256 etc. cryptographic hash functions sha2 = "0.10" diff --git a/chronik/bitcoinsuite-core/src/tx/txid.rs b/chronik/bitcoinsuite-core/src/tx/txid.rs --- a/chronik/bitcoinsuite-core/src/tx/txid.rs +++ b/chronik/bitcoinsuite-core/src/tx/txid.rs @@ -2,6 +2,8 @@ // Distributed under the MIT software license, see the accompanying // file COPYING or http://www.opensource.org/licenses/mit-license.php. +use std::str::FromStr; + use serde::{Deserialize, Serialize}; use crate::{ @@ -136,6 +138,19 @@ } } +impl TryFrom<&'_ serde_json::Value> for TxId { + type Error = &'static str; + + fn try_from(value: &'_ serde_json::Value) -> Result<Self, Self::Error> { + let txid_hex = value.as_str(); + if txid_hex.is_none() { + return Err("TxId must be a hexadecimal Value::String"); + } + TxId::from_str(txid_hex.unwrap()) + .or(Err("Cannot parse TxId from hex string")) + } +} + impl From<[u8; 32]> for TxId { fn from(array: [u8; 32]) -> Self { TxId(Sha256d(array)) diff --git a/chronik/chronik-http/src/electrum.rs b/chronik/chronik-http/src/electrum.rs --- a/chronik/chronik-http/src/electrum.rs +++ b/chronik/chronik-http/src/electrum.rs @@ -4,7 +4,6 @@ //! Module for [`ChronikElectrumServer`]. -use std::str::FromStr; use std::{net::SocketAddr, sync::Arc}; use abc_rust_error::Result; @@ -275,6 +274,9 @@ "transaction.get" => Some(Box::new(move |params: Value| { Box::pin(self.transaction_get(params)) })), + "transaction.get_height" => Some(Box::new(move |params: Value| { + Box::pin(self.transaction_get_height(params)) + })), _ => None, } } @@ -332,14 +334,9 @@ let txid_hex = get_param!(params, 0, "txid")?; let verbose = get_optional_param!(params, 1, "verbose", Value::Bool(false))?; - 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 txid = TxId::try_from(&txid_hex).or(Err( + RPCError::InvalidParams("'txid' must be a hexadecimal string"), + ))?; let verbose = match verbose { Value::Bool(v) => Ok(v), @@ -371,7 +368,7 @@ // mempool transaction return Ok(json!({ "confirmations": 0, - "hash": txid_hex, + "hash": txid_hex.as_str(), "hex": raw_tx, "time": tx.time_first_seen, })); @@ -386,9 +383,32 @@ "blockhash": blockhash.hex_be(), "blocktime": block.timestamp, "confirmations": confirmations, - "hash": txid_hex, + "hash": txid_hex.as_str(), "hex": raw_tx, "time": tx.time_first_seen, })) } + + async fn transaction_get_height( + &self, + params: Value, + ) -> Result<Value, RPCError> { + let txid_hex = get_param!(params, 0, "txid")?; + let txid = TxId::try_from(&txid_hex).or(Err( + RPCError::InvalidParams("'txid' must be a hexadecimal string"), + ))?; + + let indexer = self.indexer.read().await; + let query_tx = indexer.txs(&self.node); + let tx = query_tx + .tx_by_id(txid) + .or(Err(RPCError::InvalidRequest("Unknown txid")))?; + + if tx.block.is_none() { + // mempool transaction + return Ok(json!(0)); + } + let block = tx.block.unwrap(); + Ok(json!(block.height)) + } } diff --git a/test/functional/chronik_electrum_blockchain.py b/test/functional/chronik_electrum_blockchain.py --- a/test/functional/chronik_electrum_blockchain.py +++ b/test/functional/chronik_electrum_blockchain.py @@ -13,6 +13,7 @@ ) from test_framework.test_framework import BitcoinTestFramework from test_framework.util import assert_equal +from test_framework.wallet import MiniWallet COINBASE_TX_HEX = ( "01000000010000000000000000000000000000000000000000000000000000000000000000ffffffff4d" @@ -25,7 +26,6 @@ class ChronikElectrumBlockchain(BitcoinTestFramework): def set_test_params(self): - self.setup_clean_chain = True self.num_nodes = 1 self.extra_args = [["-chronik"]] @@ -34,6 +34,9 @@ def run_test(self): self.client = self.nodes[0].get_chronik_electrum_client() + self.node = self.nodes[0] + self.wallet = MiniWallet(self.node) + self.test_invalid_params() self.test_transaction_get() @@ -79,7 +82,7 @@ ): assert_equal( response.error, - {"code": -32602, "message": "Failed to parse txid"}, + {"code": -32602, "message": "'txid' must be a hexadecimal string"}, ) # Valid txid, but no such transaction was found @@ -106,20 +109,40 @@ { "blockhash": GENESIS_BLOCK_HASH, "blocktime": TIME_GENESIS_BLOCK, - "confirmations": 1, + "confirmations": 201, "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, + 203, ) + def test_transaction_get_height(self): + response = self.client.blockchain.transaction.get_height(GENESIS_CB_TXID) + assert_equal(response.result, 0) + + self.wallet.rescan_utxos() + tx = self.wallet.send_self_transfer(from_node=self.node) + + response = self.client.blockchain.transaction.get(tx["txid"]) + assert_equal(response.result, tx["hex"]) + + # A mempool transaction has height 0 + response = self.client.blockchain.transaction.get_height(tx["txid"]) + assert_equal(response.result, 0) + + # Mine the tx + self.generate(self.node, 1) + response = self.client.blockchain.transaction.get_height(tx["txid"]) + assert_equal(response.result, 203) + if __name__ == "__main__": ChronikElectrumBlockchain().main()