From eda9dbdd8aa1f65156a1da67fd773a11d0adecff Mon Sep 17 00:00:00 2001 From: Edward Houston Date: Wed, 17 Jun 2026 13:55:18 +0200 Subject: [PATCH] feat(electrum): add blockchain.scripthash.get_mempool ~/BLOCKSTREAM/electrs Implement the get_mempool method from the Electrum 1.4 protocol (which electrs already advertises). Returns a script hash's unconfirmed history as {tx_hash, height, fee}, with height 0 when all inputs are confirmed and -1 when there are unconfirmed parents. Mempool-only lookup, reusing the get_history txs_limit / TooPopular guard. Closes #120 --- src/electrum/server.rs | 22 ++++++++++++++++++ tests/electrum.rs | 53 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 75 insertions(+) diff --git a/src/electrum/server.rs b/src/electrum/server.rs index df9ab5103..149694f40 100644 --- a/src/electrum/server.rs +++ b/src/electrum/server.rs @@ -400,6 +400,27 @@ impl Connection { .collect::>())) } + fn blockchain_scripthash_get_mempool(&self, params: &[Value]) -> Result { + let script_hash = hash_from_value(params.get(0))?; + // ask for one extra more than the limit and fail if it exists, to avoid silently truncating + let mempool_txids = self + .query + .mempool() + .history_txids(&script_hash[..], self.txs_limit + 1); + ensure!(mempool_txids.len() <= self.txs_limit, ErrorKind::TooPopular); + + Ok(json!(mempool_txids + .into_iter() + .map(|txid| { + let fee = self.query.get_mempool_tx_fee(&txid); + let has_unconfirmed_parents = self.query.has_unconfirmed_parents(&txid); + // per the Electrum protocol: 0 if all inputs are confirmed, -1 otherwise + let height = if has_unconfirmed_parents { -1 } else { 0 }; + GetHistoryResult { txid, height, fee } + }) + .collect::>())) + } + fn blockchain_scripthash_listunspent(&self, params: &[Value]) -> Result { let script_hash = hash_from_value(params.get(0))?; let utxos = self.query.utxo(&script_hash[..])?; @@ -517,6 +538,7 @@ impl Connection { #[cfg(not(feature = "liquid"))] "blockchain.scripthash.get_balance" => self.blockchain_scripthash_get_balance(¶ms), "blockchain.scripthash.get_history" => self.blockchain_scripthash_get_history(¶ms), + "blockchain.scripthash.get_mempool" => self.blockchain_scripthash_get_mempool(¶ms), "blockchain.scripthash.listunspent" => self.blockchain_scripthash_listunspent(¶ms), "blockchain.scripthash.subscribe" => self.blockchain_scripthash_subscribe(¶ms), "blockchain.scripthash.unsubscribe" => self.blockchain_scripthash_unsubscribe(¶ms), diff --git a/tests/electrum.rs b/tests/electrum.rs index e916e2cac..65b28c9bf 100644 --- a/tests/electrum.rs +++ b/tests/electrum.rs @@ -264,6 +264,59 @@ fn test_electrum_jsonrpc_errors() { assert_eq!(s, expected); } +/// Test blockchain.scripthash.get_mempool returns only the unconfirmed history of a scripthash +#[cfg_attr(not(feature = "liquid"), test)] +#[cfg_attr(feature = "liquid", allow(dead_code))] +fn test_electrum_scripthash_get_mempool() -> Result<()> { + use bitcoin::hashes::{sha256, Hash}; + use bitcoin::hex::DisplayHex; + + let (_electrum_server, electrum_addr, mut tester) = common::init_electrum_tester()?; + + let addr = tester.newaddress()?; + + // Electrum protocol script hash: single SHA256 of the scriptPubKey, in reversed byte order + let mut hash = sha256::Hash::hash(addr.script_pubkey().as_bytes()).to_byte_array(); + hash.reverse(); + let scripthash = hash.to_lower_hex_string(); + + let request = |id: u32| { + format!( + "{{\"jsonrpc\": \"2.0\", \"method\": \"blockchain.scripthash.get_mempool\", \"params\": [\"{}\"], \"id\": {}}}", + scripthash, id + ) + }; + + let mut stream = TcpStream::connect(electrum_addr).unwrap(); + + // no mempool history yet + let s = write_and_read(&mut stream, &request(1)); + assert_eq!(s, "{\"id\":1,\"jsonrpc\":\"2.0\",\"result\":[]}"); + + // an unconfirmed tx funding the address shows up (tester.send syncs the mempool index) + let txid = tester.send(&addr, "0.1 BTC".parse().unwrap())?; + + let s = write_and_read(&mut stream, &request(2)); + let v: electrumd::jsonrpc::serde_json::Value = + electrumd::jsonrpc::serde_json::from_str(&s).unwrap(); + let entries = v["result"].as_array().expect("result array"); + assert_eq!(entries.len(), 1); + assert_eq!( + entries[0]["tx_hash"].as_str(), + Some(txid.to_string().as_str()) + ); + // height 0: all inputs confirmed (no unconfirmed parents) + assert_eq!(entries[0]["height"].as_i64(), Some(0)); + assert!(entries[0]["fee"].as_u64().is_some()); + + // once confirmed, it no longer appears in the mempool result + tester.mine()?; + let s = write_and_read(&mut stream, &request(3)); + assert_eq!(s, "{\"id\":3,\"jsonrpc\":\"2.0\",\"result\":[]}"); + + Ok(()) +} + fn write_and_read(stream: &mut TcpStream, write: &str) -> String { stream.write_all(write.as_bytes()).unwrap(); stream.write(b"\n").unwrap();