Repository: vinteumorg/Floresta
Pull request #472: [Rpcsaga] add blockhash to gettxoutproof
Author: login: jaoleal
Labels: name: bug
description: Something isn't working
name: RPC
description: Changes something with our JSON-RPC interface
Created: 2025-05-05T17:02:11Z | Merged: 2025-05-22T18:45:42Z

### What is the purpose of this pull request?

- [X] Bug fix
- [ ] Documentation update
- [X] New feature
- [ ] Test
- [ ] Other: <!-- Please describe it -->

### Which crates are being modified?

- [ ] floresta-chain
- [X] floresta-cli
- [ ] floresta-common
- [ ] floresta-compact-filters
- [ ] floresta-electrum
- [ ] floresta-watch-only
- [ ] floresta-wire
- [X] floresta
- [X] florestad
- [ ] Other: <!-- Please describe it -->

### Description

Fix #378

This commit present some changes for gettxoutproof, adding the optional blockhash
input which will make the method interns differ, searching for the transactions
inside the specified block, otherwise, the previous default behavior of getting it
from the wallet cache remains. The method now receives an array of txids instead of
just one.

Also, now named gettxoutproof, was gettxproof.

A unused #[allow(unused)] was also removed from merkle.rs

### Notes to the reviewers

While trying to fork and just complete #379 to accomplish one of [`0.8.0 milestones`](https://github.com/vinteumorg/Floresta/milestone/3) i noticed some opportunities to optimize some decisions and add two new fixes to the #378 issue. (1. and 2.)

Still validating if the changes are effective and are working.
### Author Checklist

<!-- Feel free to remove this section once you've confirmed all items -->

- [ ] I've followed the [contribution guidelines](https://github.com/vinteumorg/Floresta/blob/master/CONTRIBUTING.md)
- [ ] I've verified one of the following:
  - Ran `just pcc` (recommended but slower)
  - Ran `just lint-features '-- -D warnings' && cargo test --release`
  - Confirmed CI passed on my fork
- [X] I've linked any related issue(s) in the sections above

Finally, you are encouraged to sign all your commits (it proves authorship and guards against tampering—see [How (and why) to sign Git commits](https://withblue.ink/2020/05/17/how-and-why-to-sign-git-commits.html) and [GitHub's guide to signing commits](https://docs.github.com/en/authentication/managing-commit-signature-verification/signing-commits)).


Linked issue #378: [Bug] Floresta-CLI `gettxproof` command don't work with `blockhashs` argument passed
**Describe the bug**  
When running `floresta-cli gettxproof` with `[blockhash]` argument passed, i got the error "Transaction not found". When i read the code, the function `get_tx_proof` does have the blockhash parameter.

**Steps to Reproduce**  
- Start florestad
- Wait until it downloads all headers
- In another terminal, run `floresta-cli gettxproof <txid> <blockhash>` 
e.g.:
`floresta-cli gettxproof f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16 00000000d1145790a8694403d4063f323d499e655c83426834d4ce2f8dd4a2ee`

**Expected behavior**  
Returns the proof that transaction were included in a block

**Build environment**
- floresta-cli 0.1.0
- florestad 0.7.0-43-g7083de4
- cargo 1.84.1
- Linux Mint 22.1.0 (xia) [64-bit]



Diff:
diff --git a/crates/floresta-cli/src/lib.rs b/crates/floresta-cli/src/lib.rs
index e60130a02..8e36ceeaa 100644
--- a/crates/floresta-cli/src/lib.rs
+++ b/crates/floresta-cli/src/lib.rs
@@ -11,6 +11,7 @@
 #[cfg(feature = "with-jsonrpc")]
 pub mod jsonrpc_client;
 
+pub mod parsers;
 pub mod rpc;
 pub mod rpc_types;
 
diff --git a/crates/floresta-cli/src/main.rs b/crates/floresta-cli/src/main.rs
index 4f087eca2..9dd339d88 100644
--- a/crates/floresta-cli/src/main.rs
+++ b/crates/floresta-cli/src/main.rs
@@ -56,8 +56,8 @@ fn do_request(cmd: &Cli, client: Client) -> anyhow::Result<String> {
         Methods::GetTxOut { txid, vout } => {
             serde_json::to_string_pretty(&client.get_tx_out(txid, vout)?)?
         }
-        Methods::GetTxProof { txids, .. } => {
-            serde_json::to_string_pretty(&client.get_tx_proof(txids)?)?
+        Methods::GetTxOutProof { txids, blockhash } => {
+            serde_json::to_string_pretty(&client.get_txout_proof(txids, blockhash))?
         }
         Methods::GetTransaction { txid, .. } => {
             serde_json::to_string_pretty(&client.get_transaction(txid, Some(true))?)?
@@ -144,9 +144,14 @@ pub enum Methods {
     #[command(name = "getblockhash")]
     GetBlockHash { height: u32 },
     /// Returns the proof that one or more transactions were included in a block
-    #[command(name = "gettxproof")]
-    GetTxProof {
-        txids: Txid,
+    #[command(name = "gettxoutproof")]
+    GetTxOutProof {
+        /// The transaction IDs to prove
+        #[arg( required = true, value_parser = floresta_cli::parsers::parse_json_array::<Txid>)]
+        txids: std::vec::Vec<Txid>, // you need to specify the path of Vec https://github.com/clap-rs/clap/discussions/4695
+
+        /// The block in which to look for the transactions
+        #[arg(required = false)]
         blockhash: Option<BlockHash>,
     },
     /// Returns the transaction, assuming it is cached by our watch only wallet
diff --git a/crates/floresta-cli/src/parsers.rs b/crates/floresta-cli/src/parsers.rs
new file mode 100644
index 000000000..b8d67c3e7
--- /dev/null
+++ b/crates/floresta-cli/src/parsers.rs
@@ -0,0 +1,62 @@
+use std::any::type_name;
+use std::error::Error;
+use std::fmt::Display;
+use std::str::FromStr;
+
+#[derive(Debug)]
+/// Collection of errors to deal with parsing.
+pub enum ParseError {
+    /// Returned when the user inserts an broken array
+    InvalidArray,
+
+    /// Returned when the consumer of tries to cast into
+    /// a incompatible type.
+    InvalidTarget(String),
+}
+
+impl Display for ParseError {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        match self {
+            ParseError::InvalidArray => write!(
+                f,
+                "Couldnt parse the inserted as an Array, please refer to the docs"
+            ),
+            ParseError::InvalidTarget(target) => {
+                write!(f, "Could parse itens to {target}")
+            }
+        }
+    }
+}
+
+impl Error for ParseError {}
+
+/// Tries to parse a json array, you can insert a type to be casted on each item.
+///
+/// Example: '["4a5e1e4baab89f3a32518a88c31bc87f618f76673e2cc77ab2127b7afdeda33b", "4a5e1e4baab89f3a32518a88c31bc87f618f76673e2cc77ab2127b7afdeda33b"]'
+/// Tries to parse a JSON array of hash strings into a vector of the target type.
+///
+/// The target type must implement Deserialize and can be converted from a hash string.
+/// By default, it will parse into Hash256, but you can specify any other compatible type.
+///
+/// Example:
+/// ```
+/// # use bitcoin::hashes::sha256;
+/// # use floresta_cli::parsers::parse_json_array;
+/// let hashes: Vec<sha256::Hash> =
+///     parse_json_array(r#"["4a5e1e4baab89f3a32518a88c31bc87f618f76673e2cc77ab2127b7afdeda33b"]"#)
+///         .unwrap();
+/// ```
+pub fn parse_json_array<Target>(s: &str) -> Result<Vec<Target>, ParseError>
+where
+    Target: FromStr,
+{
+    let string_vec: Vec<String> = serde_json::from_str(s).map_err(|_| ParseError::InvalidArray)?;
+
+    string_vec
+        .into_iter()
+        .map(|s| {
+            Target::from_str(&s)
+                .map_err(|_| ParseError::InvalidTarget(type_name::<Target>().to_string()))
+        })
+        .collect()
+}
diff --git a/crates/floresta-cli/src/rpc.rs b/crates/floresta-cli/src/rpc.rs
index cd567ed9e..0162c48ae 100644
--- a/crates/floresta-cli/src/rpc.rs
+++ b/crates/floresta-cli/src/rpc.rs
@@ -48,7 +48,7 @@ pub trait FlorestaRPC {
     ///
     /// This method returns the Merkle proof, showing that a transaction was included in a block.
     /// The pooof is returned as a vector hexadecimal string.
-    fn get_tx_proof(&self, tx_id: Txid) -> Result<Vec<String>>;
+    fn get_txout_proof(&self, txids: Vec<Txid>, blockhash: Option<BlockHash>) -> Option<String>;
     /// Loads up a descriptor into the wallet
     ///
     /// This method loads up a descriptor into the wallet. If the rescan option is not None,
@@ -123,7 +123,6 @@ pub trait FlorestaRPC {
     fn get_rpc_info(&self) -> Result<GetRpcInfoRes>;
     /// Returns for how long florestad has been running, in seconds
     fn uptime(&self) -> Result<u32>;
-
     /// Returns a list of all descriptors currently loaded in the wallet
     fn list_descriptors(&self) -> Result<Vec<String>>;
 }
@@ -231,8 +230,20 @@ impl<T: JsonRPCClient> FlorestaRPC for T {
         )
     }
 
-    fn get_tx_proof(&self, tx_id: Txid) -> Result<Vec<String>> {
-        self.call("gettxoutproof", &[Value::String(tx_id.to_string())])
+    fn get_txout_proof(&self, txids: Vec<Txid>, blockhash: Option<BlockHash>) -> Option<String> {
+        let params: Vec<Value> = match blockhash {
+            Some(blockhash) => vec![
+                serde_json::to_value(txids)
+                    .expect("Unreachable, Vec<Txid> can be parsed into a json value"),
+                Value::String(blockhash.to_string()),
+            ],
+            None => {
+                let txids = serde_json::to_value(txids)
+                    .expect("Unreachable, Vec<Txid> can be parsed into a json value");
+                vec![txids]
+            }
+        };
+        self.call("gettxoutproof", &params).ok()
     }
 
     fn get_peer_info(&self) -> Result<Vec<PeerInfo>> {
diff --git a/crates/floresta-cli/src/rpc_types.rs b/crates/floresta-cli/src/rpc_types.rs
index 11fa754a7..a8b113ce2 100644
--- a/crates/floresta-cli/src/rpc_types.rs
+++ b/crates/floresta-cli/src/rpc_types.rs
@@ -3,6 +3,12 @@ use std::fmt::Display;
 use serde::Deserialize;
 use serde::Serialize;
 
+#[derive(Debug, Deserialize, Serialize)]
+/// Return type for the `gettxoutproof` rpc command, the internal is
+/// just the hex representation of the Merkle Block, which was defined
+/// by btc core.
+pub struct GetTxOutProof(pub Vec<u8>);
+
 #[derive(Debug, Deserialize, Serialize)]
 pub struct GetBlockchainInfoRes {
     /// The best block we know about
diff --git a/crates/floresta-electrum/src/electrum_protocol.rs b/crates/floresta-electrum/src/electrum_protocol.rs
index c74d9f39b..88a0b8589 100644
--- a/crates/floresta-electrum/src/electrum_protocol.rs
+++ b/crates/floresta-electrum/src/electrum_protocol.rs
@@ -457,11 +457,11 @@ impl<Blockchain: BlockchainInterface> ElectrumServer<Blockchain> {
                 let tx_id = get_arg!(request, Txid, 0);
                 let proof = self.address_cache.get_merkle_proof(&tx_id);
                 let height = self.address_cache.get_height(&tx_id);
-                if let Some((proof, position)) = proof {
+                if let Some(proof) = proof {
                     let result = json!({
-                        "merkle": proof,
+                        "merkle": proof.hashes,
                         "block_height": height.unwrap_or(0),
-                        "pos": position
+                        "pos": proof.pos
                     });
                     return json_rpc_res!(request, result);
                 }
diff --git a/crates/floresta-watch-only/src/lib.rs b/crates/floresta-watch-only/src/lib.rs
index 7234fb832..76a2d5f1b 100644
--- a/crates/floresta-watch-only/src/lib.rs
+++ b/crates/floresta-watch-only/src/lib.rs
@@ -7,7 +7,6 @@ use bitcoin::ScriptBuf;
 use floresta_chain::BlockConsumer;
 use floresta_common::get_spk_hash;
 use floresta_common::parse_descriptors;
-use floresta_common::prelude::ToString;
 
 pub mod kv_database;
 #[cfg(any(test, feature = "memory-database"))]
@@ -293,17 +292,15 @@ impl<D: AddressCacheDatabase> AddressCacheInner<D> {
         Some(transactions)
     }
 
-    /// Returns the Merkle Proof for a given address
-    fn get_merkle_proof(&self, txid: &Txid) -> Option<(Vec<String>, u32)> {
-        let mut hashes = Vec::new();
-        let tx = self.get_transaction(txid)?;
+    /// Get [Merkle Proof]
+    ///
+    /// Returns none if a given Txid is an unconfirmed transaction or unrelated with your wallet, defined by the xpubs, descriptors and addresses in your `config.toml`.
+    ///
+    /// [Merkle Proof]: https://developer.bitcoin.org/devguide/block_chain.html#merkle-trees
+    fn get_merkle_proof(&self, txid: &Txid) -> Option<MerkleProof> {
         // If a given transaction is cached, but the merkle tree doesn't exist, that means
         // it is an unconfirmed transaction.
-        tx.merkle_block.as_ref()?;
-        for hash in tx.merkle_block?.hashes() {
-            hashes.push(hash.to_string());
-        }
-        Some((hashes, tx.position))
+        self.get_transaction(txid)?.clone().merkle_block
     }
 
     /// Adds a new address to track, should be called at wallet setup and every once in a while
@@ -667,7 +664,10 @@ impl<D: AddressCacheDatabase> AddressCache<D> {
         inner.get_address_history(script_hash)
     }
 
-    pub fn get_merkle_proof(&self, txid: &Txid) -> Option<(Vec<String>, u32)> {
+    /// Returns the Merkle Proof for a given txid.
+    ///
+    /// Fails if a given Txid is an unconfirmed transaction.
+    pub fn get_merkle_proof(&self, txid: &Txid) -> Option<MerkleProof> {
         let inner = self.inner.read().expect("poisoned lock");
         inner.get_merkle_proof(txid)
     }
@@ -864,7 +864,9 @@ mod test {
         let expected_hashes = Vec::from([String::from(
             "e7d6e69230db7dd074cc2610c32be013468f1c224172b347eccdef98f36e0834",
         )]);
-        assert_eq!(cached_merkle_block, (expected_hashes, 1));
+
+        assert_eq!(cached_merkle_block.pos, 1u64);
+        assert_eq!(cached_merkle_block.to_string_array(), expected_hashes);
 
         // TESTS FOR SMALL, HELPER FUNCTIONS
 
@@ -940,7 +942,9 @@ mod test {
         let expected_hashes = Vec::from([String::from(
             "e7d6e69230db7dd074cc2610c32be013468f1c224172b347eccdef98f36e0834",
         )]);
-        assert_eq!(cached_merkle_block, (expected_hashes, 1));
+
+        assert_eq!(cached_merkle_block.pos, 1u64);
+        assert_eq!(cached_merkle_block.to_string_array(), expected_hashes);
 
         // TESTS FOR SMALL HELPER FUNCTIONS
 
diff --git a/crates/floresta-watch-only/src/merkle.rs b/crates/floresta-watch-only/src/merkle.rs
index 9ae9279b0..4dac8b989 100644
--- a/crates/floresta-watch-only/src/merkle.rs
+++ b/crates/floresta-watch-only/src/merkle.rs
@@ -10,9 +10,9 @@ use serde::Deserialize;
 use serde::Serialize;
 #[derive(Clone, Debug, Hash, PartialEq, Eq, Serialize, Deserialize)]
 pub struct MerkleProof {
-    target: Txid,
-    pos: u64,
-    hashes: Vec<sha256d::Hash>,
+    pub target: Txid,
+    pub pos: u64,
+    pub hashes: Vec<sha256d::Hash>,
 }
 impl Default for MerkleProof {
     fn default() -> Self {
@@ -28,10 +28,17 @@ impl MerkleProof {
             pos: 0,
         }
     }
+
+    /// Return the hashes for this proof as a [`Vec<String>`]
+    pub fn to_string_array(&self) -> Vec<String> {
+        self.hashes().iter().map(|hash| hash.to_string()).collect()
+    }
+
     /// Returns the hashes for this proof
     pub fn hashes(&self) -> Vec<sha256d::Hash> {
         self.hashes.clone()
     }
+
     /// Creates a new proof from a list of hashes and a target. Target is a 64 bits
     /// unsigned integer indicating the index of a transaction we with to prove. Note that
     /// this only proves one tx at the time.
@@ -44,6 +51,7 @@ impl MerkleProof {
             hashes: proof,
         }
     }
+
     /// Same as [MerkleProof::from_block_hashes] but you give a block instead of a list of
     /// hashes.
     pub fn from_block(block: &Block, target: u64) -> Self {
@@ -54,7 +62,7 @@ impl MerkleProof {
             .collect();
         Self::from_block_hashes(tx_list, target)
     }
-    #[allow(unused)]
+
     /// Verifies a proof by hashing up all nodes until reach a root, and compare `root` with
     /// computed root.
     pub fn verify(&self, root: sha256d::Hash) -> Result<bool, String> {
@@ -71,15 +79,18 @@ impl MerkleProof {
         }
         Ok(root == computed)
     }
+
     /// Returns the position of a node's parent
     fn get_parent(pos: u64) -> u64 {
         (pos ^ 1) / 2
     }
+
     /// Returns a node's sibling. This is useful because we have to copy a node's sibling
     /// to proof, so we can compute it's parent.
     fn get_sibling(pos: u64) -> u64 {
         pos ^ 1
     }
+
     /// Computes the hash of two node's parent, by taking sha256d(left_child | right_child), where |
     /// means byte-wise concatenation.
     fn parent_hash(left: &[u8], right: &[u8]) -> sha256d::Hash {
@@ -88,6 +99,7 @@ impl MerkleProof {
         engine.input(right);
         sha256d::Hash::from_engine(engine)
     }
+
     /// Iterates over the tree, collecting required nodes for proof, internally we compute
     /// all intermediate nodes, but don't keep them.
     fn transverse(
diff --git a/crates/floresta-wire/src/p2p_wire/node.rs b/crates/floresta-wire/src/p2p_wire/node.rs
index 1f10bf894..f30f0721b 100644
--- a/crates/floresta-wire/src/p2p_wire/node.rs
+++ b/crates/floresta-wire/src/p2p_wire/node.rs
@@ -371,11 +371,12 @@ where
         user_req: UserRequest,
         responder: tokio::sync::oneshot::Sender<NodeResponse>,
     ) {
-        debug!("Performing user request {user_req:?}");
         if self.inflight.len() >= RunningNode::MAX_INFLIGHT_REQUESTS {
             return;
         }
 
+        debug!("Performing user request {user_req:?}");
+
         let req = match user_req {
             UserRequest::Block(block) => NodeRequest::GetBlock((vec![block], false)),
             UserRequest::UtreexoBlock(block) => NodeRequest::GetBlock((vec![block], true)),
diff --git a/florestad/Cargo.toml b/florestad/Cargo.toml
index 90b337257..ba4eec98a 100644
--- a/florestad/Cargo.toml
+++ b/florestad/Cargo.toml
@@ -55,11 +55,7 @@ pretty_assertions = "1"
 experimental-db = ["floresta-chain/experimental-db"]
 compact-filters = ["dep:floresta-compact-filters"]
 zmq-server = ["dep:zmq"]
-json-rpc = [
-    "dep:axum",
-    "dep:tower-http",
-    "compact-filters"
-]
+json-rpc = ["dep:axum", "dep:tower-http", "compact-filters"]
 default = ["json-rpc"]
 metrics = ["dep:metrics", "floresta-wire/metrics"]
 
diff --git a/florestad/src/json_rpc/blockchain.rs b/florestad/src/json_rpc/blockchain.rs
index d6d977ce3..b87e68fc5 100644
--- a/florestad/src/json_rpc/blockchain.rs
+++ b/florestad/src/json_rpc/blockchain.rs
@@ -1,8 +1,10 @@
 use bitcoin::block::Header;
 use bitcoin::consensus::encode::serialize_hex;
+use bitcoin::consensus::Encodable;
 use bitcoin::constants::genesis_block;
 use bitcoin::Block;
 use bitcoin::BlockHash;
+use bitcoin::MerkleBlock;
 use bitcoin::OutPoint;
 use bitcoin::ScriptBuf;
 use bitcoin::Txid;
@@ -13,6 +15,7 @@ use super::res::Error as RpcError;
 use super::res::Error;
 use super::res::GetBlockResVerbose;
 use super::res::GetBlockchainInfoRes;
+use super::res::GetTxOutProof;
 use super::server::RpcChain;
 use super::server::RpcImpl;
 
@@ -30,6 +33,15 @@ impl<Blockchain: RpcChain> RpcImpl<Blockchain> {
             .map_err(|e| RpcError::Node(e.to_string()))
             .and_then(|block| block.ok_or(RpcError::BlockNotFound))
     }
+
+    /// Return the block that contains the given Txid
+    pub fn get_block_by_txid(&self, txid: &Txid) -> Result<Block, RpcError> {
+        let height = self.wallet.get_height(txid).ok_or(RpcError::TxNotFound)?;
+        let blockhash = self.chain.get_block_hash(height).unwrap();
+        self.chain
+            .get_block(&blockhash)
+            .map_err(|_| RpcError::BlockNotFound)
+    }
 }
 
 // blockchain rpcs
@@ -188,13 +200,57 @@ impl<Blockchain: RpcChain> RpcImpl<Blockchain> {
         res.map_err(|_e| RpcError::Encode)
     }
 
-    // gettxoutproof
-    pub(super) fn get_tx_proof(&self, tx_id: Txid) -> Result<Vec<String>, RpcError> {
-        Ok(self
-            .wallet
-            .get_merkle_proof(&tx_id)
-            .ok_or(RpcError::TxNotFound)?
-            .0)
+    /// Computes the necessary information for the RPC `gettxoutproof [txids] blockhash (optional)`
+    ///
+    /// This function has two paths, when blockhash is inserted and when isn't.
+    ///
+    /// Specifying the blockhash will make this function go after the block and search
+    /// for the transactions inside it, building a merkle proof from the block with its
+    /// indexes. Not specifying will redirect it to search for the merkle proof on our
+    /// Watch-only wallet which may not have the transaction cached, returning [`RpcError::UncertainTxNotFound`].
+    ///
+    /// Not founding one of the specified transactions will raise [`RpcError::TxNotFound`].
+    pub(super) async fn get_txout_proof(
+        &self,
+        tx_ids: &[Txid],
+        blockhash: Option<BlockHash>,
+    ) -> Result<GetTxOutProof, RpcError> {
+        let block = match blockhash {
+            Some(blockhash) => self.get_block_inner(blockhash).await?,
+            // Using the first Txid to get the block should be fine since they are expected to all
+            // live in the same block, otherwise, theres no way they have a common proof.
+            None => self.get_block_by_txid(&tx_ids[0])?,
+        };
+
+        // Before building the merkle block we try to remove all txids
+        // that aren't present in the block we found, meaning that
+        // at least one of the txids doesn't belong to the block which
+        // in case needs to make the command fails.
+        //
+        // this makes the use MerkleBlock::from_block_with_predicate useless.
+        let targeted_txids: Vec<Txid> = block
+            .txdata
+            .iter()
+            .filter_map(|tx| {
+                let txid = tx.compute_txid();
+                if tx_ids.contains(&txid) {
+                    Some(txid)
+                } else {
+                    None
+                }
+            })
+            .collect();
+
+        if targeted_txids.len() != tx_ids.len() {
+            return Err(RpcError::TxNotFound);
+        };
+
+        let merkle_block = MerkleBlock::from_block_with_predicate(&block, |tx| tx_ids.contains(tx));
+        let mut bytes: Vec<u8> = Vec::new();
+        merkle_block
+            .consensus_encode(&mut bytes)
+            .expect("This will raise if a writer error happens");
+        Ok(GetTxOutProof(bytes))
     }
 
     // gettxoutsetinfo
diff --git a/florestad/src/json_rpc/res.rs b/florestad/src/json_rpc/res.rs
index bf2ddad51..358caf37d 100644
--- a/florestad/src/json_rpc/res.rs
+++ b/florestad/src/json_rpc/res.rs
@@ -85,6 +85,12 @@ pub struct RpcError {
     pub data: Option<String>,
 }
 
+/// Return type for the `gettxoutproof` rpc command, the internal is
+/// just the hex representation of the Merkle Block, which was defined
+/// by btc core.
+#[derive(Debug, Deserialize, Serialize)]
+pub struct GetTxOutProof(pub Vec<u8>);
+
 /// A full bitcoin block, returned by get_block
 #[derive(Debug, Deserialize, Serialize)]
 pub struct GetBlockResVerbose {
@@ -183,6 +189,7 @@ pub enum Error {
     InvalidVout,
     InvalidHeight,
     InvalidHash,
+    InvalidBlockHash,
     InvalidRequest,
     MethodNotFound,
     Decode(String),
@@ -202,6 +209,7 @@ pub enum Error {
 impl Display for Error {
     fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
         match self {
+            Error::InvalidBlockHash => write!(f, "Provided a invalid BlockHash"),
             Error::InvalidRequest => write!(f, "Invalid request"),
             Error::InvalidHeight => write!(f, "Invalid height"),
             Error::InvalidHash =>  write!(f, "Invalid hash"),
diff --git a/florestad/src/json_rpc/server.rs b/florestad/src/json_rpc/server.rs
index ee5b65ae4..1c470ecac 100644
--- a/florestad/src/json_rpc/server.rs
+++ b/florestad/src/json_rpc/server.rs
@@ -285,11 +285,24 @@ async fn handle_json_rpc_request(
         }
 
         "gettxoutproof" => {
-            let txid = Txid::from_str(params[0].as_str().ok_or(Error::InvalidHash)?)
-                .map_err(|_| Error::InvalidHash)?;
-            state
-                .get_tx_proof(txid)
-                .map(|v| ::serde_json::to_value(v).unwrap())
+            let txids: Vec<Txid> =
+                serde_json::from_value(params[0].clone()).map_err(|_| Error::InvalidHash)?;
+
+            let blockhash = params
+                .get(1)
+                .cloned()
+                .map(serde_json::from_value)
+                .transpose()
+                .map_err(|_| Error::InvalidBlockHash)?;
+
+            Ok(serde_json::to_value(
+                state
+                    .get_txout_proof(&txids, blockhash)
+                    .await?
+                    .0
+                    .to_lower_hex_string(),
+            )
+            .expect("GetTxOutProof implements serde"))
         }
 
         "getrawtransaction" => {
@@ -394,6 +407,7 @@ fn get_http_error_code(err: &Error) -> u16 {
     match err {
         // you messed up
         Error::InvalidHex
+        | Error::InvalidBlockHash
         | Error::InvalidHash
         | Error::InvalidAddress
         | Error::InvalidScript
@@ -430,6 +444,7 @@ fn get_json_rpc_error_code(err: &Error) -> i32 {
 
         // Invalid Request
         Error::InvalidHex
+        | Error::InvalidBlockHash
         | Error::InvalidHash
         | Error::InvalidAddress
         | Error::InvalidScript


Reviews:
user: login: Davidson-Souza
state: CHANGES_REQUESTED
author_association: MEMBER
submitted_at: 2025-05-07T14:32:28Z
user: login: jaoleal
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-07T16:04:47Z
user: login: jaoleal
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-07T16:49:55Z
user: login: Davidson-Souza
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-07T17:01:34Z
user: login: jaoleal
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-07T17:58:04Z
user: login: Davidson-Souza
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-07T20:39:35Z
user: login: jaoleal
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-08T00:58:50Z
user: login: jaoleal
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-08T01:12:49Z
user: login: jaoleal
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-08T19:03:02Z
user: login: jaoleal
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-14T18:48:22Z
user: login: jaoleal
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-14T19:22:42Z
user: login: Davidson-Souza
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-15T14:34:06Z
user: login: Davidson-Souza
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-15T14:35:39Z
user: login: jaoleal
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-15T16:48:51Z
user: login: jaoleal
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-15T17:18:57Z
user: login: jaoleal
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-15T17:19:56Z
user: login: Davidson-Souza
body: I've tested it and it seems to work fine. Cross checked with core and it seems right.

Some comments about the code
state: CHANGES_REQUESTED
author_association: MEMBER
submitted_at: 2025-05-16T23:30:02Z
user: login: jaoleal
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-19T13:22:43Z
user: login: jaoleal
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-19T14:56:34Z
user: login: Davidson-Souza
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-19T15:32:57Z
user: login: Davidson-Souza
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-19T15:33:19Z
user: login: Davidson-Souza
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-19T15:33:45Z
user: login: Davidson-Souza
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-19T15:34:04Z
user: login: Davidson-Souza
state: CHANGES_REQUESTED
author_association: MEMBER
submitted_at: 2025-05-20T16:58:19Z
user: login: jaoleal
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-20T19:10:56Z
user: login: JoseSK999
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-21T14:43:14Z
user: login: jaoleal
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-21T14:44:18Z
user: login: Davidson-Souza
body: I think something got messed up in some rebase, because you're still changing the `florestad` `Cargo.toml`.

Everything else looks fine
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-21T20:31:47Z
user: login: Davidson-Souza
body: ACK [aebb3ac](https://github.com/vinteumorg/Floresta/pull/472/commits/aebb3acf4111e581be7143b200d9e1a781c2bfa1)
state: APPROVED
author_association: MEMBER
submitted_at: 2025-05-22T17:29:58Z

Review comments:
diff_hunk: @@ -666,7 +668,14 @@ impl<D: AddressCacheDatabase> AddressCache<D> {
         let inner = self.inner.read().expect("poisoned lock");
         inner.get_address_history(script_hash)
     }
-
+    // NOTE FOR THE REVIEWER:
+    // wrappers functions like this are all around the floresta project, maybe just decribe thats a wrapper and one click on its documentation
path: crates/floresta-watch-only/src/lib.rs
user: login: Davidson-Souza
body: The inner function isn't exposed. Since this is the function users will see, I think the documentation should be here. And users don't need to care about whether this is a wrapper or not.
created_at: 2025-05-07T14:28:52Z
author_association: MEMBER
diff_hunk: @@ -190,12 +190,35 @@ impl RpcImpl {
     }
 
     // gettxoutproof
-    pub(super) fn get_tx_proof(&self, tx_id: Txid) -> Result<Vec<String>, RpcError> {
-        Ok(self
-            .wallet
-            .get_merkle_proof(&tx_id)
-            .ok_or(RpcError::TxNotFound)?
-            .0)
+    pub(super) async fn get_txout_proof(
+        &self,
+        tx_ids: &[Txid],
+        blockhash: Option<BlockHash>,
+    ) -> Result<Vec<Vec<String>>, RpcError> {
+        let targets = match blockhash {
+            Some(blockhash) => {
+                let ret: Vec<Txid> = self
path: florestad/src/json_rpc/blockchain.rs
user: login: Davidson-Souza
body: If we don't find that `txid` in a block, shouldn't we just return an error?
created_at: 2025-05-07T14:30:08Z
author_association: MEMBER
diff_hunk: @@ -190,12 +190,35 @@ impl RpcImpl {
     }
 
     // gettxoutproof
-    pub(super) fn get_tx_proof(&self, tx_id: Txid) -> Result<Vec<String>, RpcError> {
-        Ok(self
-            .wallet
-            .get_merkle_proof(&tx_id)
-            .ok_or(RpcError::TxNotFound)?
-            .0)
+    pub(super) async fn get_txout_proof(
+        &self,
+        tx_ids: &[Txid],
+        blockhash: Option<BlockHash>,
+    ) -> Result<Vec<Vec<String>>, RpcError> {
+        let targets = match blockhash {
+            Some(blockhash) => {
+                let ret: Vec<Txid> = self
+                    .get_block_inner(blockhash)
+                    .await?
+                    .txdata
+                    .iter()
+                    .map(|tx| tx.compute_txid())
+                    .filter(|tx| tx_ids.contains(&tx))
+                    .collect();
+                ret
+            }
+            None => tx_ids.into(),
+        };
+        Ok(targets
path: florestad/src/json_rpc/blockchain.rs
user: login: Davidson-Souza
body: The block hash param is a hint about what block this tx might be, so we can build a proof, even though we don't have a txindex to assist. If the block hash is present, you should try to get the merkle path from the block itself. 
created_at: 2025-05-07T14:31:35Z
author_association: MEMBER
diff_hunk: @@ -190,12 +190,35 @@ impl RpcImpl {
     }
 
     // gettxoutproof
-    pub(super) fn get_tx_proof(&self, tx_id: Txid) -> Result<Vec<String>, RpcError> {
-        Ok(self
-            .wallet
-            .get_merkle_proof(&tx_id)
-            .ok_or(RpcError::TxNotFound)?
-            .0)
+    pub(super) async fn get_txout_proof(
+        &self,
+        tx_ids: &[Txid],
+        blockhash: Option<BlockHash>,
+    ) -> Result<Vec<Vec<String>>, RpcError> {
+        let targets = match blockhash {
+            Some(blockhash) => {
+                let ret: Vec<Txid> = self
+                    .get_block_inner(blockhash)
+                    .await?
+                    .txdata
+                    .iter()
+                    .map(|tx| tx.compute_txid())
+                    .filter(|tx| tx_ids.contains(&tx))
+                    .collect();
+                ret
+            }
+            None => tx_ids.into(),
+        };
+        Ok(targets
+            .into_iter()
+            .map(|target| {
+                self.wallet
+                    .get_merkle_proof(&target)
+                    .ok_or(RpcError::TxNotFound)
+                    .expect("") // m not certain of what to write here ""
path: florestad/src/json_rpc/blockchain.rs
user: login: Davidson-Souza
body: We may not have this tx cached. This shouldn't be an `expect`
created_at: 2025-05-07T14:32:09Z
author_association: MEMBER
diff_hunk: @@ -190,12 +190,35 @@ impl RpcImpl {
     }
 
     // gettxoutproof
-    pub(super) fn get_tx_proof(&self, tx_id: Txid) -> Result<Vec<String>, RpcError> {
-        Ok(self
-            .wallet
-            .get_merkle_proof(&tx_id)
-            .ok_or(RpcError::TxNotFound)?
-            .0)
+    pub(super) async fn get_txout_proof(
+        &self,
+        tx_ids: &[Txid],
+        blockhash: Option<BlockHash>,
+    ) -> Result<Vec<Vec<String>>, RpcError> {
+        let targets = match blockhash {
+            Some(blockhash) => {
+                let ret: Vec<Txid> = self
path: florestad/src/json_rpc/blockchain.rs
user: login: jaoleal
body: Yes
created_at: 2025-05-07T16:04:47Z
author_association: MEMBER
diff_hunk: @@ -190,12 +190,35 @@ impl RpcImpl {
     }
 
     // gettxoutproof
-    pub(super) fn get_tx_proof(&self, tx_id: Txid) -> Result<Vec<String>, RpcError> {
-        Ok(self
-            .wallet
-            .get_merkle_proof(&tx_id)
-            .ok_or(RpcError::TxNotFound)?
-            .0)
+    pub(super) async fn get_txout_proof(
+        &self,
+        tx_ids: &[Txid],
+        blockhash: Option<BlockHash>,
+    ) -> Result<Vec<Vec<String>>, RpcError> {
+        let targets = match blockhash {
+            Some(blockhash) => {
+                let ret: Vec<Txid> = self
+                    .get_block_inner(blockhash)
+                    .await?
+                    .txdata
+                    .iter()
+                    .map(|tx| tx.compute_txid())
+                    .filter(|tx| tx_ids.contains(&tx))
+                    .collect();
+                ret
+            }
+            None => tx_ids.into(),
+        };
+        Ok(targets
+            .into_iter()
+            .map(|target| {
+                self.wallet
+                    .get_merkle_proof(&target)
+                    .ok_or(RpcError::TxNotFound)
+                    .expect("") // m not certain of what to write here ""
path: florestad/src/json_rpc/blockchain.rs
user: login: jaoleal
body: Hmmmm, the function would yield an error Independently if its an `expect` or a `unwrap` right ? Considering the case where the transaction is not cached. I made an error only for that. wdyt ?
created_at: 2025-05-07T16:49:55Z
author_association: MEMBER
diff_hunk: @@ -190,12 +190,35 @@ impl RpcImpl {
     }
 
     // gettxoutproof
-    pub(super) fn get_tx_proof(&self, tx_id: Txid) -> Result<Vec<String>, RpcError> {
-        Ok(self
-            .wallet
-            .get_merkle_proof(&tx_id)
-            .ok_or(RpcError::TxNotFound)?
-            .0)
+    pub(super) async fn get_txout_proof(
+        &self,
+        tx_ids: &[Txid],
+        blockhash: Option<BlockHash>,
+    ) -> Result<Vec<Vec<String>>, RpcError> {
+        let targets = match blockhash {
+            Some(blockhash) => {
+                let ret: Vec<Txid> = self
+                    .get_block_inner(blockhash)
+                    .await?
+                    .txdata
+                    .iter()
+                    .map(|tx| tx.compute_txid())
+                    .filter(|tx| tx_ids.contains(&tx))
+                    .collect();
+                ret
+            }
+            None => tx_ids.into(),
+        };
+        Ok(targets
+            .into_iter()
+            .map(|target| {
+                self.wallet
+                    .get_merkle_proof(&target)
+                    .ok_or(RpcError::TxNotFound)
+                    .expect("") // m not certain of what to write here ""
path: florestad/src/json_rpc/blockchain.rs
user: login: Davidson-Souza
body: You should return this error. `get_merkle_proof` return an `Option`, `ok_or` will turn it into a `Result`. If the `Option` value was `None`, you would panic. You need to return this `Result`.
created_at: 2025-05-07T17:01:34Z
author_association: MEMBER
diff_hunk: @@ -190,12 +190,35 @@ impl RpcImpl {
     }
 
     // gettxoutproof
-    pub(super) fn get_tx_proof(&self, tx_id: Txid) -> Result<Vec<String>, RpcError> {
-        Ok(self
-            .wallet
-            .get_merkle_proof(&tx_id)
-            .ok_or(RpcError::TxNotFound)?
-            .0)
+    pub(super) async fn get_txout_proof(
+        &self,
+        tx_ids: &[Txid],
+        blockhash: Option<BlockHash>,
+    ) -> Result<Vec<Vec<String>>, RpcError> {
+        let targets = match blockhash {
+            Some(blockhash) => {
+                let ret: Vec<Txid> = self
+                    .get_block_inner(blockhash)
+                    .await?
+                    .txdata
+                    .iter()
+                    .map(|tx| tx.compute_txid())
+                    .filter(|tx| tx_ids.contains(&tx))
+                    .collect();
+                ret
+            }
+            None => tx_ids.into(),
+        };
+        Ok(targets
+            .into_iter()
+            .map(|target| {
+                self.wallet
+                    .get_merkle_proof(&target)
+                    .ok_or(RpcError::TxNotFound)
+                    .expect("") // m not certain of what to write here ""
path: florestad/src/json_rpc/blockchain.rs
user: login: jaoleal
body: Ooof, forgot that `unwrap` and `expect` will panic instead of returning the error.
created_at: 2025-05-07T17:58:04Z
author_association: MEMBER
diff_hunk: @@ -190,12 +191,67 @@ impl RpcImpl {
     }
 
     // gettxoutproof
-    pub(super) fn get_tx_proof(&self, tx_id: Txid) -> Result<Vec<String>, RpcError> {
-        Ok(self
-            .wallet
-            .get_merkle_proof(&tx_id)
-            .ok_or(RpcError::TxNotFound)?
-            .0)
+    /// Computes the necessary information for the RPC `gettxoutproof [txids] blockhash (optional)`
+    ///
+    /// This function has two paths, when blockhash is inserted and when isnt.
+    ///
+    /// Specifying the blockhash will make this function go after the block and search
+    /// for the transcations inside it, building a merkle proof from the block with its
+    /// indexes. Not specifying will redirect it to search for the merkle proof on our
+    /// read-only wallet which may not have the transaction cached, returning [`RpcError::UncertainTxNotFound`].
path: florestad/src/json_rpc/blockchain.rs
user: login: Davidson-Souza
body: nit: Watch-only. We do write to it.
created_at: 2025-05-07T20:31:40Z
author_association: MEMBER
diff_hunk: @@ -190,12 +191,67 @@ impl RpcImpl {
     }
 
     // gettxoutproof
-    pub(super) fn get_tx_proof(&self, tx_id: Txid) -> Result<Vec<String>, RpcError> {
-        Ok(self
-            .wallet
-            .get_merkle_proof(&tx_id)
-            .ok_or(RpcError::TxNotFound)?
-            .0)
+    /// Computes the necessary information for the RPC `gettxoutproof [txids] blockhash (optional)`
+    ///
+    /// This function has two paths, when blockhash is inserted and when isnt.
path: florestad/src/json_rpc/blockchain.rs
user: login: Davidson-Souza
body: Super nit (I wouldn't even comment if this was the only thing :joy:): `isn't`
created_at: 2025-05-07T20:32:40Z
author_association: MEMBER
diff_hunk: @@ -190,12 +191,67 @@ impl RpcImpl {
     }
 
     // gettxoutproof
-    pub(super) fn get_tx_proof(&self, tx_id: Txid) -> Result<Vec<String>, RpcError> {
-        Ok(self
-            .wallet
-            .get_merkle_proof(&tx_id)
-            .ok_or(RpcError::TxNotFound)?
-            .0)
+    /// Computes the necessary information for the RPC `gettxoutproof [txids] blockhash (optional)`
+    ///
+    /// This function has two paths, when blockhash is inserted and when isnt.
+    ///
+    /// Specifying the blockhash will make this function go after the block and search
+    /// for the transcations inside it, building a merkle proof from the block with its
+    /// indexes. Not specifying will redirect it to search for the merkle proof on our
+    /// read-only wallet which may not have the transaction cached, returning [`RpcError::UncertainTxNotFound`].
+    ///
+    /// Not founding one of the specified transactions will raise [`RpcError::TxNotFound`], if none is
+    /// found, [`RpcError::TxsNotFound`] is raised then.
+    pub(super) async fn get_txout_proof(
+        &self,
+        tx_ids: &[Txid],
+        blockhash: Option<BlockHash>,
+    ) -> Result<Vec<Vec<String>>, RpcError> {
+        match blockhash {
+            Some(blockhash) => {
+                let block = self.get_block_inner(blockhash).await?;
+                let targets: Vec<usize> = block
+                    .txdata
+                    .iter()
+                    .enumerate()
+                    .filter_map(|(index, tx)| {
+                        if tx_ids.contains(&tx.compute_txid()) {
+                            Some(index)
+                        } else {
+                            None
+                        }
+                    })
+                    .collect();
+
+                if !targets.is_empty() {
+                    return Err(RpcError::TxsNotFound);
+                } else if targets.len() != tx_ids.len() {
+                    return Err(RpcError::TxNotFound); //
path: florestad/src/json_rpc/blockchain.rs
user: login: Davidson-Souza
body: There's a `//` at the end of this line
created_at: 2025-05-07T20:34:36Z
author_association: MEMBER
diff_hunk: @@ -190,12 +191,67 @@ impl RpcImpl {
     }
 
     // gettxoutproof
-    pub(super) fn get_tx_proof(&self, tx_id: Txid) -> Result<Vec<String>, RpcError> {
-        Ok(self
-            .wallet
-            .get_merkle_proof(&tx_id)
-            .ok_or(RpcError::TxNotFound)?
-            .0)
+    /// Computes the necessary information for the RPC `gettxoutproof [txids] blockhash (optional)`
+    ///
+    /// This function has two paths, when blockhash is inserted and when isnt.
+    ///
+    /// Specifying the blockhash will make this function go after the block and search
+    /// for the transcations inside it, building a merkle proof from the block with its
+    /// indexes. Not specifying will redirect it to search for the merkle proof on our
+    /// read-only wallet which may not have the transaction cached, returning [`RpcError::UncertainTxNotFound`].
+    ///
+    /// Not founding one of the specified transactions will raise [`RpcError::TxNotFound`], if none is
+    /// found, [`RpcError::TxsNotFound`] is raised then.
+    pub(super) async fn get_txout_proof(
+        &self,
+        tx_ids: &[Txid],
+        blockhash: Option<BlockHash>,
+    ) -> Result<Vec<Vec<String>>, RpcError> {
path: florestad/src/json_rpc/blockchain.rs
user: login: Davidson-Souza
body: From [Core's documentaion](https://bitcoincore.org/en/doc/28.0.0/rpc/blockchain/gettxoutproof/) I think this function should return one string. It is probably better if you use `rust-bitcoin` for this. For the block case you can use `MerkleBlock::from_header_txids_with_predicate` and then `serialize_hex`. For the cached ones it's harder, I don't think there's an easy solution.
created_at: 2025-05-07T20:39:01Z
author_association: MEMBER
diff_hunk: @@ -190,12 +191,67 @@ impl RpcImpl {
     }
 
     // gettxoutproof
-    pub(super) fn get_tx_proof(&self, tx_id: Txid) -> Result<Vec<String>, RpcError> {
-        Ok(self
-            .wallet
-            .get_merkle_proof(&tx_id)
-            .ok_or(RpcError::TxNotFound)?
-            .0)
+    /// Computes the necessary information for the RPC `gettxoutproof [txids] blockhash (optional)`
+    ///
+    /// This function has two paths, when blockhash is inserted and when isnt.
+    ///
+    /// Specifying the blockhash will make this function go after the block and search
+    /// for the transcations inside it, building a merkle proof from the block with its
+    /// indexes. Not specifying will redirect it to search for the merkle proof on our
+    /// read-only wallet which may not have the transaction cached, returning [`RpcError::UncertainTxNotFound`].
path: florestad/src/json_rpc/blockchain.rs
user: login: jaoleal
body: Oops
created_at: 2025-05-08T00:58:50Z
author_association: MEMBER
diff_hunk: @@ -190,12 +191,67 @@ impl RpcImpl {
     }
 
     // gettxoutproof
-    pub(super) fn get_tx_proof(&self, tx_id: Txid) -> Result<Vec<String>, RpcError> {
-        Ok(self
-            .wallet
-            .get_merkle_proof(&tx_id)
-            .ok_or(RpcError::TxNotFound)?
-            .0)
+    /// Computes the necessary information for the RPC `gettxoutproof [txids] blockhash (optional)`
+    ///
+    /// This function has two paths, when blockhash is inserted and when isnt.
+    ///
+    /// Specifying the blockhash will make this function go after the block and search
+    /// for the transcations inside it, building a merkle proof from the block with its
+    /// indexes. Not specifying will redirect it to search for the merkle proof on our
+    /// read-only wallet which may not have the transaction cached, returning [`RpcError::UncertainTxNotFound`].
+    ///
+    /// Not founding one of the specified transactions will raise [`RpcError::TxNotFound`], if none is
+    /// found, [`RpcError::TxsNotFound`] is raised then.
+    pub(super) async fn get_txout_proof(
+        &self,
+        tx_ids: &[Txid],
+        blockhash: Option<BlockHash>,
+    ) -> Result<Vec<Vec<String>>, RpcError> {
+        match blockhash {
+            Some(blockhash) => {
+                let block = self.get_block_inner(blockhash).await?;
+                let targets: Vec<usize> = block
+                    .txdata
+                    .iter()
+                    .enumerate()
+                    .filter_map(|(index, tx)| {
+                        if tx_ids.contains(&tx.compute_txid()) {
+                            Some(index)
+                        } else {
+                            None
+                        }
+                    })
+                    .collect();
+
+                if !targets.is_empty() {
+                    return Err(RpcError::TxsNotFound);
+                } else if targets.len() != tx_ids.len() {
+                    return Err(RpcError::TxNotFound); //
path: florestad/src/json_rpc/blockchain.rs
user: login: jaoleal
body: interesting that neither `fmt` and `clippy` got that. Theoretically is a blank space no ?
created_at: 2025-05-08T01:12:49Z
author_association: MEMBER
diff_hunk: @@ -190,12 +191,67 @@ impl RpcImpl {
     }
 
     // gettxoutproof
-    pub(super) fn get_tx_proof(&self, tx_id: Txid) -> Result<Vec<String>, RpcError> {
-        Ok(self
-            .wallet
-            .get_merkle_proof(&tx_id)
-            .ok_or(RpcError::TxNotFound)?
-            .0)
+    /// Computes the necessary information for the RPC `gettxoutproof [txids] blockhash (optional)`
+    ///
+    /// This function has two paths, when blockhash is inserted and when isnt.
+    ///
+    /// Specifying the blockhash will make this function go after the block and search
+    /// for the transcations inside it, building a merkle proof from the block with its
+    /// indexes. Not specifying will redirect it to search for the merkle proof on our
+    /// read-only wallet which may not have the transaction cached, returning [`RpcError::UncertainTxNotFound`].
+    ///
+    /// Not founding one of the specified transactions will raise [`RpcError::TxNotFound`], if none is
+    /// found, [`RpcError::TxsNotFound`] is raised then.
+    pub(super) async fn get_txout_proof(
+        &self,
+        tx_ids: &[Txid],
+        blockhash: Option<BlockHash>,
+    ) -> Result<Vec<Vec<String>>, RpcError> {
path: florestad/src/json_rpc/blockchain.rs
user: login: jaoleal
body: Okay, this change a lot of things to really make this core-like. Turning to draft again.
created_at: 2025-05-08T19:03:02Z
author_association: MEMBER
diff_hunk: @@ -190,12 +191,67 @@ impl RpcImpl {
     }
 
     // gettxoutproof
-    pub(super) fn get_tx_proof(&self, tx_id: Txid) -> Result<Vec<String>, RpcError> {
-        Ok(self
-            .wallet
-            .get_merkle_proof(&tx_id)
-            .ok_or(RpcError::TxNotFound)?
-            .0)
+    /// Computes the necessary information for the RPC `gettxoutproof [txids] blockhash (optional)`
+    ///
+    /// This function has two paths, when blockhash is inserted and when isnt.
+    ///
+    /// Specifying the blockhash will make this function go after the block and search
+    /// for the transcations inside it, building a merkle proof from the block with its
+    /// indexes. Not specifying will redirect it to search for the merkle proof on our
+    /// read-only wallet which may not have the transaction cached, returning [`RpcError::UncertainTxNotFound`].
+    ///
+    /// Not founding one of the specified transactions will raise [`RpcError::TxNotFound`], if none is
+    /// found, [`RpcError::TxsNotFound`] is raised then.
+    pub(super) async fn get_txout_proof(
+        &self,
+        tx_ids: &[Txid],
+        blockhash: Option<BlockHash>,
+    ) -> Result<Vec<Vec<String>>, RpcError> {
path: florestad/src/json_rpc/blockchain.rs
user: login: jaoleal
body: `MerkleBlock` doesnt have any `serialize_hex`, im trying to use `consensus_encode()` since its what  might be the right way to do it... The only thing that might be wrong here is if rust-bitcoin enconding doesnt match cores. But im not being able to reproduce the command perfectly, the setup needed is quite boring to workout and floresta is giving me some strange bugs that i may post issues about.
created_at: 2025-05-14T18:48:22Z
author_association: MEMBER
diff_hunk: @@ -144,9 +147,14 @@ pub enum Methods {
     #[command(name = "getblockhash")]
     GetBlockHash { height: u32 },
     /// Returns the proof that one or more transactions were included in a block
-    #[command(name = "gettxproof")]
-    GetTxProof {
-        txids: Txid,
+    #[command(name = "gettxoutproof")]
+    GetTxOutProof {
+        /// The transaction IDs to prove
+        #[arg( required = true, value_parser = parsers::jsonlike_array_of_txids)]
+        txids: std::vec::Vec<Txid>, // you need to specify the path of Vec https://github.com/clap-rs/clap/discussions/4695
+
+        /// The block in which to look for the transactions
+        #[arg(short = 'b', long = "blockhash", last = true, required = false)]
         blockhash: Option<BlockHash>,
     },
path: crates/floresta-cli/src/main.rs
user: login: jaoleal
body: Im struggling with clap config aiming to match cores api but it seems impossible since the only way that i can pass `blockhash` is by either specifying a flag or passing it after a `--`
The way it is, you can call it like: 
``` Bash
floresta-cli gettxoutproof '["77bc13bb64c290fc6a1be9f2b9141d84417329a416411ada2f035ee2f754bb86"]' -- 00000000000000000002266ea7f71eb350a9a55e5672136a51ce6534d42247eb
```
in the end, youll need atleast to adaptate something to integrate floresta RPC where cores was
created_at: 2025-05-14T19:22:35Z
author_association: MEMBER
diff_hunk: @@ -144,9 +147,14 @@ pub enum Methods {
     #[command(name = "getblockhash")]
     GetBlockHash { height: u32 },
     /// Returns the proof that one or more transactions were included in a block
-    #[command(name = "gettxproof")]
-    GetTxProof {
-        txids: Txid,
+    #[command(name = "gettxoutproof")]
+    GetTxOutProof {
+        /// The transaction IDs to prove
+        #[arg( required = true, value_parser = parsers::jsonlike_array_of_txids)]
+        txids: std::vec::Vec<Txid>, // you need to specify the path of Vec https://github.com/clap-rs/clap/discussions/4695
+
+        /// The block in which to look for the transactions
+        #[arg(short = 'b', long = "blockhash", last = true, required = false)]
         blockhash: Option<BlockHash>,
     },
path: crates/floresta-cli/src/main.rs
user: login: Davidson-Souza
body: This is because you made it into an option, rather than an argument. Remove all the annotation for the second field but `#[arg(required = false)]`
created_at: 2025-05-15T14:34:06Z
author_association: MEMBER
diff_hunk: @@ -190,12 +191,67 @@ impl RpcImpl {
     }
 
     // gettxoutproof
-    pub(super) fn get_tx_proof(&self, tx_id: Txid) -> Result<Vec<String>, RpcError> {
-        Ok(self
-            .wallet
-            .get_merkle_proof(&tx_id)
-            .ok_or(RpcError::TxNotFound)?
-            .0)
+    /// Computes the necessary information for the RPC `gettxoutproof [txids] blockhash (optional)`
+    ///
+    /// This function has two paths, when blockhash is inserted and when isnt.
+    ///
+    /// Specifying the blockhash will make this function go after the block and search
+    /// for the transcations inside it, building a merkle proof from the block with its
+    /// indexes. Not specifying will redirect it to search for the merkle proof on our
+    /// read-only wallet which may not have the transaction cached, returning [`RpcError::UncertainTxNotFound`].
+    ///
+    /// Not founding one of the specified transactions will raise [`RpcError::TxNotFound`], if none is
+    /// found, [`RpcError::TxsNotFound`] is raised then.
+    pub(super) async fn get_txout_proof(
+        &self,
+        tx_ids: &[Txid],
+        blockhash: Option<BlockHash>,
+    ) -> Result<Vec<Vec<String>>, RpcError> {
path: florestad/src/json_rpc/blockchain.rs
user: login: Davidson-Souza
body: > MerkleBlock doesnt have any serialize_hex

`serialize_hex` is a standalone function from `bitcoin::consensus`
created_at: 2025-05-15T14:35:39Z
author_association: MEMBER
diff_hunk: @@ -190,12 +191,67 @@ impl RpcImpl {
     }
 
     // gettxoutproof
-    pub(super) fn get_tx_proof(&self, tx_id: Txid) -> Result<Vec<String>, RpcError> {
-        Ok(self
-            .wallet
-            .get_merkle_proof(&tx_id)
-            .ok_or(RpcError::TxNotFound)?
-            .0)
+    /// Computes the necessary information for the RPC `gettxoutproof [txids] blockhash (optional)`
+    ///
+    /// This function has two paths, when blockhash is inserted and when isnt.
+    ///
+    /// Specifying the blockhash will make this function go after the block and search
+    /// for the transcations inside it, building a merkle proof from the block with its
+    /// indexes. Not specifying will redirect it to search for the merkle proof on our
+    /// read-only wallet which may not have the transaction cached, returning [`RpcError::UncertainTxNotFound`].
+    ///
+    /// Not founding one of the specified transactions will raise [`RpcError::TxNotFound`], if none is
+    /// found, [`RpcError::TxsNotFound`] is raised then.
+    pub(super) async fn get_txout_proof(
+        &self,
+        tx_ids: &[Txid],
+        blockhash: Option<BlockHash>,
+    ) -> Result<Vec<Vec<String>>, RpcError> {
path: florestad/src/json_rpc/blockchain.rs
user: login: jaoleal
body: Ok, saw it... but it just does what im already doing... I`ll change the code for sanity but the result will be the same
created_at: 2025-05-15T16:48:51Z
author_association: MEMBER
diff_hunk: @@ -144,9 +147,14 @@ pub enum Methods {
     #[command(name = "getblockhash")]
     GetBlockHash { height: u32 },
     /// Returns the proof that one or more transactions were included in a block
-    #[command(name = "gettxproof")]
-    GetTxProof {
-        txids: Txid,
+    #[command(name = "gettxoutproof")]
+    GetTxOutProof {
+        /// The transaction IDs to prove
+        #[arg( required = true, value_parser = parsers::jsonlike_array_of_txids)]
+        txids: std::vec::Vec<Txid>, // you need to specify the path of Vec https://github.com/clap-rs/clap/discussions/4695
+
+        /// The block in which to look for the transactions
+        #[arg(short = 'b', long = "blockhash", last = true, required = false)]
         blockhash: Option<BlockHash>,
     },
path: crates/floresta-cli/src/main.rs
user: login: jaoleal
body: nice, made it work!!
created_at: 2025-05-15T17:18:57Z
author_association: MEMBER
diff_hunk: @@ -144,9 +147,14 @@ pub enum Methods {
     #[command(name = "getblockhash")]
     GetBlockHash { height: u32 },
     /// Returns the proof that one or more transactions were included in a block
-    #[command(name = "gettxproof")]
-    GetTxProof {
-        txids: Txid,
+    #[command(name = "gettxoutproof")]
+    GetTxOutProof {
+        /// The transaction IDs to prove
+        #[arg( required = true, value_parser = parsers::jsonlike_array_of_txids)]
+        txids: std::vec::Vec<Txid>, // you need to specify the path of Vec https://github.com/clap-rs/clap/discussions/4695
+
+        /// The block in which to look for the transactions
+        #[arg(short = 'b', long = "blockhash", last = true, required = false)]
         blockhash: Option<BlockHash>,
     },
path: crates/floresta-cli/src/main.rs
user: login: jaoleal
body: ```Bash
floresta-cli gettxoutproof '["77bc13bb64c290fc6a1be9f2b9141d84417329a416411ada2f035ee2f754bb86"  , "77bc13bb64c290fc6a1be9f2b9141d84417329a416411ada2f035ee2f754bb86"]' 00000000000000000002266ea7f71eb350a9a55e5672136a51ce6534d42247eb
```
When i print with
```Rust
println!("{:?}", txids);
println!("{:?}", blockhash);
```

[77bc13bb64c290fc6a1be9f2b9141d84417329a416411ada2f035ee2f754bb86, 77bc13bb64c290fc6a1be9f2b9141d84417329a416411ada2f035ee2f754bb86]
Some(00000000000000000002266ea7f71eb350a9a55e5672136a51ce6534d42247eb)
created_at: 2025-05-15T17:19:56Z
author_association: MEMBER
diff_hunk: @@ -21,6 +21,7 @@ serde = { version = "1.0", features = ["derive"] }
 serde_json = "1.0"
 anyhow = "1.0"
 jsonrpc = { version = "0.18.0", optional = true, features = ["minreq_http"] }
+thiserror = "2.0.12"
path: crates/floresta-cli/Cargo.toml
user: login: Davidson-Souza
body: I think in #86 we've discussed we wouldn't use `thiserror` forward. Using the macros and manual derivation instead. So I think we shouldn't add it here.
created_at: 2025-05-15T19:04:27Z
author_association: MEMBER
diff_hunk: @@ -208,3 +213,28 @@ pub enum Methods {
     #[command(name = "listdescriptors")]
     ListDescriptors,
 }
+
+/// These are parser methods that help to maintain a core-like API.
+///
+/// A good example for these parsers is the ['parsers::jsonlike_array_of_txids']
+/// that permit to `gettxoutproof` to receive the txids input as '["txid1","txid2"]'.
+pub mod parsers {
path: crates/floresta-cli/src/main.rs
user: login: Davidson-Souza
body: Do you intended to add more of these? Might be better to leave them in their own file.
created_at: 2025-05-15T19:06:10Z
author_association: MEMBER
diff_hunk: @@ -231,8 +230,15 @@ impl<T: JsonRPCClient> FlorestaRPC for T {
         )
     }
 
-    fn get_tx_proof(&self, tx_id: Txid) -> Result<Vec<String>> {
-        self.call("gettxoutproof", &[Value::String(tx_id.to_string())])
+    fn get_txout_proof(&self, txids: Vec<Txid>, blockhash: Option<BlockHash>) -> Result<String> {
+        let params: Vec<Value> = match blockhash {
+            Some(blockhash) => vec![
+                serde_json::to_value(txids).unwrap(),
path: crates/floresta-cli/src/rpc.rs
user: login: Davidson-Souza
body: Remove the `unwrap`s
created_at: 2025-05-15T19:07:00Z
author_association: MEMBER
diff_hunk: @@ -293,17 +292,13 @@ impl<D: AddressCacheDatabase> AddressCacheInner<D> {
         Some(transactions)
     }
 
-    /// Returns the Merkle Proof for a given address
-    fn get_merkle_proof(&self, txid: &Txid) -> Option<(Vec<String>, u32)> {
-        let mut hashes = Vec::new();
-        let tx = self.get_transaction(txid)?;
+    /// Returns the Merkle Proof for a given txid
+    ///
+    /// Fails if a given Txid is an unconfirmed transaction.
path: crates/floresta-watch-only/src/lib.rs
user: login: Davidson-Souza
body: Returns `None`. Failing more like a function that returns `Result`
created_at: 2025-05-16T23:13:13Z
author_association: MEMBER
diff_hunk: @@ -28,6 +28,10 @@ impl MerkleProof {
             pos: 0,
         }
     }
+    /// Return the hashes fot this proof as a [`Vec<String>`]
path: crates/floresta-watch-only/src/merkle.rs
user: login: Davidson-Souza
body: nit: typo on `for`
created_at: 2025-05-16T23:14:26Z
author_association: MEMBER
diff_hunk: @@ -25,6 +25,7 @@ floresta-common = { path = "../crates/floresta-common" }
 floresta-electrum = { path = "../crates/floresta-electrum" }
 floresta-watch-only = { path = "../crates/floresta-watch-only" }
 floresta-wire = { path = "../crates/floresta-wire" }
+floresta-cli = { path = "../crates/floresta-cli" }
path: florestad/Cargo.toml
user: login: Davidson-Souza
body: I think this should be done elsewhere, with more discussion. Not here.
created_at: 2025-05-16T23:15:01Z
author_association: MEMBER
diff_hunk: @@ -1,13 +1,17 @@
+use anyhow::Result;
path: florestad/src/json_rpc/blockchain.rs
user: login: Davidson-Souza
body: Please don't use anyhow inside `florestad`. I'm going to remove it soon.
created_at: 2025-05-16T23:15:48Z
author_association: MEMBER
diff_hunk: @@ -189,13 +193,64 @@ impl RpcImpl {
         res.map_err(|_e| RpcError::Encode)
     }
 
-    // gettxoutproof
-    pub(super) fn get_tx_proof(&self, tx_id: Txid) -> Result<Vec<String>, RpcError> {
-        Ok(self
-            .wallet
-            .get_merkle_proof(&tx_id)
-            .ok_or(RpcError::TxNotFound)?
-            .0)
+    /// Return the block that contains the given Txid
+    pub fn get_block_by_txid(&self, txid: &Txid) -> Result<Block, RpcError> {
+        let height = self.wallet.get_height(txid).ok_or(RpcError::TxNotFound)?;
+        let blockhash = self.chain.get_block_hash(height).unwrap();
+        Ok(self.chain.get_block(&blockhash).unwrap())
+    }
+
+    /// Computes the necessary information for the RPC `gettxoutproof [txids] blockhash (optional)`
+    ///
+    /// This function has two paths, when blockhash is inserted and when isn't.
+    ///
+    /// Specifying the blockhash will make this function go after the block and search
+    /// for the transcations inside it, building a merkle proof from the block with its
path: florestad/src/json_rpc/blockchain.rs
user: login: Davidson-Souza
body: typo: transcations
created_at: 2025-05-16T23:18:03Z
author_association: MEMBER
diff_hunk: @@ -189,13 +193,64 @@ impl RpcImpl {
         res.map_err(|_e| RpcError::Encode)
     }
 
-    // gettxoutproof
-    pub(super) fn get_tx_proof(&self, tx_id: Txid) -> Result<Vec<String>, RpcError> {
-        Ok(self
-            .wallet
-            .get_merkle_proof(&tx_id)
-            .ok_or(RpcError::TxNotFound)?
-            .0)
+    /// Return the block that contains the given Txid
+    pub fn get_block_by_txid(&self, txid: &Txid) -> Result<Block, RpcError> {
+        let height = self.wallet.get_height(txid).ok_or(RpcError::TxNotFound)?;
+        let blockhash = self.chain.get_block_hash(height).unwrap();
+        Ok(self.chain.get_block(&blockhash).unwrap())
+    }
+
+    /// Computes the necessary information for the RPC `gettxoutproof [txids] blockhash (optional)`
+    ///
+    /// This function has two paths, when blockhash is inserted and when isn't.
+    ///
+    /// Specifying the blockhash will make this function go after the block and search
+    /// for the transcations inside it, building a merkle proof from the block with its
+    /// indexes. Not specifying will redirect it to search for the merkle proof on our
+    /// Watch-only wallet which may not have the transaction cached, returning [`RpcError::UncertainTxNotFound`].
+    ///
+    /// Not founding one of the specified transactions will raise [`RpcError::TxNotFound`].
+    pub(super) async fn get_txout_proof(
+        &self,
+        tx_ids: &[Txid],
+        blockhash: Option<BlockHash>,
+    ) -> Result<GetTxOutProof, RpcError> {
+        let block = match blockhash {
+            Some(blockhash) => self.get_block_inner(blockhash).await?,
+            // Using the first Txid to get the block should be fine since they are expected to all
+            // live in the same block, otherwise, theres no way they have a common proof.
+            None => self.get_block_by_txid(&tx_ids[0])?,
+        };
+
+        // Before building the merkle block we try to remove all txids
+        // that arent present in the block we found, meaning that
+        // at least one of the txids doesnt belong to the block which
+        // in case needs to make the command fails.
+        //
+        // this makes the use MerkleBlock::from_block_with_predicate useless.
path: florestad/src/json_rpc/blockchain.rs
user: login: Davidson-Souza
body: ```suggestion
        // Before building the merkle block we try to remove all txids
        // that aren't present in the block we found, meaning that
        // at least one of the txids doesn't belong to the block which
        // in case needs to make the command fails.
        //
        // this makes the use MerkleBlock::from_block_with_predicate useless.
```
created_at: 2025-05-16T23:19:50Z
author_association: MEMBER
diff_hunk: @@ -202,6 +203,7 @@ pub enum Error {
 impl Display for Error {
     fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
         match self {
+            Error::InvalidBlockHash => write!(f, "Inserted a invalid BlockHash"),
path: florestad/src/json_rpc/res.rs
user: login: Davidson-Souza
body: ```suggestion
            Error::InvalidBlockHash => write!(f, "Provided a invalid BlockHash"),
```
created_at: 2025-05-16T23:21:57Z
author_association: MEMBER
diff_hunk: @@ -270,13 +270,26 @@ async fn handle_json_rpc_request(req: Value, state: Arc<RpcImpl>) -> Result<serd
         }
 
         "gettxoutproof" => {
-            let txid = Txid::from_str(params[0].as_str().ok_or(Error::InvalidHash)?)
-                .map_err(|_| Error::InvalidHash)?;
-            state
-                .get_tx_proof(txid)
-                .map(|v| ::serde_json::to_value(v).unwrap())
+            let txids: Vec<Txid> =
+                serde_json::from_value(params[0].clone()).map_err(|_| Error::InvalidHash)?;
+
+            let blockhash = if params.len() > 1 {
+                Some(
+                    serde_json::from_value(params[1].clone())
+                        .map_err(|_| Error::InvalidBlockHash)?,
+                )
+            } else {
+                None
+            };
+            Ok(serde_json::to_value(
+                state
+                    .get_txout_proof(&txids, blockhash)
+                    .await?
+                    .0
+                    .to_lower_hex_string(),
+            )
+            .expect("GetTxOutProof implements serde"))
         }
path: florestad/src/json_rpc/server.rs
user: login: Davidson-Souza
body: Nit empty line after the end of `gettxoutproof`
created_at: 2025-05-16T23:26:09Z
author_association: MEMBER
diff_hunk: @@ -270,13 +270,26 @@ async fn handle_json_rpc_request(req: Value, state: Arc<RpcImpl>) -> Result<serd
         }
 
         "gettxoutproof" => {
-            let txid = Txid::from_str(params[0].as_str().ok_or(Error::InvalidHash)?)
-                .map_err(|_| Error::InvalidHash)?;
-            state
-                .get_tx_proof(txid)
-                .map(|v| ::serde_json::to_value(v).unwrap())
+            let txids: Vec<Txid> =
+                serde_json::from_value(params[0].clone()).map_err(|_| Error::InvalidHash)?;
+
+            let blockhash = if params.len() > 1 {
+                Some(
+                    serde_json::from_value(params[1].clone())
+                        .map_err(|_| Error::InvalidBlockHash)?,
+                )
+            } else {
+                None
+            };
path: florestad/src/json_rpc/server.rs
user: login: Davidson-Souza
body: I think you can make this more clear with combinators

```suggestion
            let blockhash = params
                .get(1)
                .cloned()
                .map(serde_json::from_value)
                .transpose()
                .map_err(|_| Error::InvalidBlockHash)?;
```
created_at: 2025-05-16T23:29:12Z
author_association: MEMBER
diff_hunk: @@ -21,6 +21,7 @@ serde = { version = "1.0", features = ["derive"] }
 serde_json = "1.0"
 anyhow = "1.0"
 jsonrpc = { version = "0.18.0", optional = true, features = ["minreq_http"] }
+thiserror = "2.0.12"
path: crates/floresta-cli/Cargo.toml
user: login: jaoleal
body: i didnt saw any discussion about this on #86 , maybe was on another PR ? anyway, ill refactor the errors
created_at: 2025-05-19T13:22:43Z
author_association: MEMBER
diff_hunk: @@ -293,17 +292,13 @@ impl<D: AddressCacheDatabase> AddressCacheInner<D> {
         Some(transactions)
     }
 
-    /// Returns the Merkle Proof for a given address
-    fn get_merkle_proof(&self, txid: &Txid) -> Option<(Vec<String>, u32)> {
-        let mut hashes = Vec::new();
-        let tx = self.get_transaction(txid)?;
+    /// Returns the Merkle Proof for a given txid
+    ///
+    /// Fails if a given Txid is an unconfirmed transaction.
path: crates/floresta-watch-only/src/lib.rs
user: login: jaoleal
body: Ok, i feel that i didnt understand how to get a transaction cached by this electrum server... and this would be a good part of the docs, im changing it to the following;

    /// Get [Merkle Proof](https://developer.bitcoin.org/devguide/block_chain.html#merkle-trees) for a given txid.
    ///
    /// Returns none if a given Txid is an unconfirmed transaction or unrelated with your wallet, defined by the xpubs, descriptors and addresses in your `config.toml`.
 
created_at: 2025-05-19T14:56:34Z
author_association: MEMBER
diff_hunk: @@ -25,6 +25,7 @@ floresta-common = { path = "../crates/floresta-common" }
 floresta-electrum = { path = "../crates/floresta-electrum" }
 floresta-watch-only = { path = "../crates/floresta-watch-only" }
 floresta-wire = { path = "../crates/floresta-wire" }
+floresta-cli = { path = "../crates/floresta-cli" }
path: florestad/Cargo.toml
user: login: Davidson-Souza
body: It's still there :sweat_smile: 
created_at: 2025-05-19T15:32:57Z
author_association: MEMBER
diff_hunk: @@ -28,6 +28,10 @@ impl MerkleProof {
             pos: 0,
         }
     }
+    /// Return the hashes for this proof as a [`Vec<String>`]
path: crates/floresta-watch-only/src/merkle.rs
user: login: Davidson-Souza
body: nit: empty line before and after this function
created_at: 2025-05-19T15:33:19Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,75 @@
+/// These are parser methods that help to maintain a core-like API.
+///
+/// A good example for these parsers is the ['parsers::jsonlike_array_of_txids']
+/// that permit to `gettxoutproof` to receive the txids input as '["txid1","txid2"]'.
+use std::any::type_name;
+/// These are parser methods that help to maintain a core-like API.
+///
+/// A good example for these parsers is the ['parsers::jsonlike_array_of_txids']
+/// that permit to `gettxoutproof` to receive the txids input as '["txid1","txid2"]'.
+use std::error::Error;
+/// These are parser methods that help to maintain a core-like API.
+///
+/// A good example for these parsers is the ['parsers::jsonlike_array_of_txids']
+/// that permit to `gettxoutproof` to receive the txids input as '["txid1","txid2"]'.
+use std::fmt::Display;
+/// These are parser methods that help to maintain a core-like API.
+///
+/// A good example for these parsers is the ['parsers::jsonlike_array_of_txids']
+/// that permit to `gettxoutproof` to receive the txids input as '["txid1","txid2"]'.
+use std::str::FromStr;
+
+#[derive(Debug)]
+/// Collection of errors to deal with parsing.
+pub enum ParseError {
+    /// Returned when the user inserts an broken array
+    InvalidHashArray,
+    /// Returned when the consumer of jsonlike_arrayofhashes tries to
+    /// cast into a incompatible type.
+    InvalidHashTarget(String),
+}
+impl Display for ParseError {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        match self {
+            ParseError::InvalidHashArray => write!(
+                f,
+                "Couldnt parse the inserted as a Hash Array, please refer to the docs"
+            ),
+            ParseError::InvalidHashTarget(target) => {
+                write!(f, "Could parse the hash itens to {target}")
+            }
+        }
+    }
+}
+impl Error for ParseError {}
path: crates/floresta-cli/src/parsers.rs
user: login: Davidson-Souza
body: nit empty space here too
created_at: 2025-05-19T15:33:45Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,75 @@
+/// These are parser methods that help to maintain a core-like API.
+///
+/// A good example for these parsers is the ['parsers::jsonlike_array_of_txids']
+/// that permit to `gettxoutproof` to receive the txids input as '["txid1","txid2"]'.
+use std::any::type_name;
+/// These are parser methods that help to maintain a core-like API.
+///
+/// A good example for these parsers is the ['parsers::jsonlike_array_of_txids']
+/// that permit to `gettxoutproof` to receive the txids input as '["txid1","txid2"]'.
+use std::error::Error;
+/// These are parser methods that help to maintain a core-like API.
+///
+/// A good example for these parsers is the ['parsers::jsonlike_array_of_txids']
+/// that permit to `gettxoutproof` to receive the txids input as '["txid1","txid2"]'.
+use std::fmt::Display;
+/// These are parser methods that help to maintain a core-like API.
+///
+/// A good example for these parsers is the ['parsers::jsonlike_array_of_txids']
+/// that permit to `gettxoutproof` to receive the txids input as '["txid1","txid2"]'.
+use std::str::FromStr;
path: crates/floresta-cli/src/parsers.rs
user: login: Davidson-Souza
body: here too
created_at: 2025-05-19T15:34:03Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,60 @@
+use std::any::type_name;
+use std::error::Error;
+use std::fmt::Display;
+use std::str::FromStr;
+
+#[derive(Debug)]
+/// Collection of errors to deal with parsing.
+pub enum ParseError {
+    /// Returned when the user inserts an broken array
+    InvalidHashArray,
+    /// Returned when the consumer of jsonlike_arrayofhashes tries to
+    /// cast into a incompatible type.
+    InvalidHashTarget(String),
+}
+impl Display for ParseError {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        match self {
+            ParseError::InvalidHashArray => write!(
+                f,
+                "Couldnt parse the inserted as a Hash Array, please refer to the docs"
+            ),
+            ParseError::InvalidHashTarget(target) => {
+                write!(f, "Could parse the hash itens to {target}")
+            }
+        }
+    }
+}
+
+impl Error for ParseError {}
+
+pub type Result<T> = std::result::Result<T, ParseError>;
+/// Tries to parse a json array of hashes256, you can insert a type to be casted on each hash256.
+///
+/// Example: '["4a5e1e4baab89f3a32518a88c31bc87f618f76673e2cc77ab2127b7afdeda33b", "4a5e1e4baab89f3a32518a88c31bc87f618f76673e2cc77ab2127b7afdeda33b"]'
+/// Tries to parse a JSON array of hash strings into a vector of the target type.
+///
+/// The target type must implement Deserialize and can be converted from a hash string.
+/// By default, it will parse into Hash256, but you can specify any other compatible type.
+///
+/// Example:
+/// ```
+/// let hashes: Vec<Hash256> = jsonlike_arrayofhashes(
+///     r#"["4a5e1e4baab89f3a32518a88c31bc87f618f76673e2cc77ab2127b7afdeda33b"]"#,
+/// )?;
+/// ```
+pub fn jsonlike_arrayofhashes<Target>(s: &str) -> Result<Vec<Target>>
path: crates/floresta-cli/src/parsers.rs
user: login: Davidson-Souza
body: It seems like this method parses more than hashes. How about "parse_json_array"?
created_at: 2025-05-20T16:39:17Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,60 @@
+use std::any::type_name;
+use std::error::Error;
+use std::fmt::Display;
+use std::str::FromStr;
+
+#[derive(Debug)]
+/// Collection of errors to deal with parsing.
+pub enum ParseError {
+    /// Returned when the user inserts an broken array
+    InvalidHashArray,
+    /// Returned when the consumer of jsonlike_arrayofhashes tries to
+    /// cast into a incompatible type.
+    InvalidHashTarget(String),
+}
path: crates/floresta-cli/src/parsers.rs
user: login: Davidson-Souza
body: And then update the errors to be something more generic
created_at: 2025-05-20T16:40:16Z
author_association: MEMBER
diff_hunk: @@ -231,8 +230,16 @@ impl<T: JsonRPCClient> FlorestaRPC for T {
         )
     }
 
-    fn get_tx_proof(&self, tx_id: Txid) -> Result<Vec<String>> {
-        self.call("gettxoutproof", &[Value::String(tx_id.to_string())])
+    fn get_txout_proof(&self, txids: Vec<Txid>, blockhash: Option<BlockHash>) -> Option<String> {
+        let params: Vec<Value> = match blockhash {
+            Some(blockhash) => vec![
+                serde_json::to_value(txids)
+                    .expect("Unreachable, Vec<Txid> can be parsed into a json value"),
+                Value::String(blockhash.to_string()),
+            ],
+            None => vec![serde_json::to_value(txids).unwrap()],
path: crates/floresta-cli/src/rpc.rs
user: login: Davidson-Souza
body: `expect` here too.

Also, maybe create a variable for the txids.

```rust
let txids = serde_json::to_value(txids)
                    .expect("Unreachable, Vec<Txid> can be parsed into a json value");
```
created_at: 2025-05-20T16:43:11Z
author_association: MEMBER
diff_hunk: @@ -188,13 +191,64 @@ impl<Blockchain: RpcChain> RpcImpl<Blockchain> {
         res.map_err(|_e| RpcError::Encode)
     }
 
-    // gettxoutproof
-    pub(super) fn get_tx_proof(&self, tx_id: Txid) -> Result<Vec<String>, RpcError> {
-        Ok(self
-            .wallet
-            .get_merkle_proof(&tx_id)
-            .ok_or(RpcError::TxNotFound)?
-            .0)
+    /// Return the block that contains the given Txid
+    pub fn get_block_by_txid(&self, txid: &Txid) -> Result<Block, RpcError> {
+        let height = self.wallet.get_height(txid).ok_or(RpcError::TxNotFound)?;
+        let blockhash = self.chain.get_block_hash(height).unwrap();
+        Ok(self.chain.get_block(&blockhash).unwrap())
+    }
path: florestad/src/json_rpc/blockchain.rs
user: login: Davidson-Souza
body: The way I was organizing this, all helper methods should be in the first `impl` block. In the second block we should only have the RPC methods.
created_at: 2025-05-20T16:48:23Z
author_association: MEMBER
diff_hunk: @@ -188,13 +191,64 @@ impl<Blockchain: RpcChain> RpcImpl<Blockchain> {
         res.map_err(|_e| RpcError::Encode)
     }
 
-    // gettxoutproof
-    pub(super) fn get_tx_proof(&self, tx_id: Txid) -> Result<Vec<String>, RpcError> {
-        Ok(self
-            .wallet
-            .get_merkle_proof(&tx_id)
-            .ok_or(RpcError::TxNotFound)?
-            .0)
+    /// Return the block that contains the given Txid
+    pub fn get_block_by_txid(&self, txid: &Txid) -> Result<Block, RpcError> {
+        let height = self.wallet.get_height(txid).ok_or(RpcError::TxNotFound)?;
+        let blockhash = self.chain.get_block_hash(height).unwrap();
+        Ok(self.chain.get_block(&blockhash).unwrap())
path: florestad/src/json_rpc/blockchain.rs
user: login: Davidson-Souza
body: Don't unwrap, we may not get it.
created_at: 2025-05-20T16:49:10Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,60 @@
+use std::any::type_name;
+use std::error::Error;
+use std::fmt::Display;
+use std::str::FromStr;
+
+#[derive(Debug)]
+/// Collection of errors to deal with parsing.
+pub enum ParseError {
+    /// Returned when the user inserts an broken array
+    InvalidHashArray,
+    /// Returned when the consumer of jsonlike_arrayofhashes tries to
path: crates/floresta-cli/src/parsers.rs
user: login: Davidson-Souza
body: Empty spacing between enum variants.
created_at: 2025-05-20T16:52:13Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,60 @@
+use std::any::type_name;
+use std::error::Error;
+use std::fmt::Display;
+use std::str::FromStr;
+
+#[derive(Debug)]
+/// Collection of errors to deal with parsing.
+pub enum ParseError {
+    /// Returned when the user inserts an broken array
+    InvalidHashArray,
+    /// Returned when the consumer of jsonlike_arrayofhashes tries to
+    /// cast into a incompatible type.
+    InvalidHashTarget(String),
+}
+impl Display for ParseError {
path: crates/floresta-cli/src/parsers.rs
user: login: Davidson-Souza
body: between these two items.
created_at: 2025-05-20T16:52:38Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,60 @@
+use std::any::type_name;
+use std::error::Error;
+use std::fmt::Display;
+use std::str::FromStr;
+
+#[derive(Debug)]
+/// Collection of errors to deal with parsing.
+pub enum ParseError {
+    /// Returned when the user inserts an broken array
+    InvalidHashArray,
+    /// Returned when the consumer of jsonlike_arrayofhashes tries to
+    /// cast into a incompatible type.
+    InvalidHashTarget(String),
+}
+impl Display for ParseError {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        match self {
+            ParseError::InvalidHashArray => write!(
+                f,
+                "Couldnt parse the inserted as a Hash Array, please refer to the docs"
+            ),
+            ParseError::InvalidHashTarget(target) => {
+                write!(f, "Could parse the hash itens to {target}")
+            }
+        }
+    }
+}
+
+impl Error for ParseError {}
+
+pub type Result<T> = std::result::Result<T, ParseError>;
path: crates/floresta-cli/src/parsers.rs
user: login: Davidson-Souza
body: between these two (also, is this Result needed at all? Seems like simple type)
created_at: 2025-05-20T16:53:18Z
author_association: MEMBER
diff_hunk: @@ -3,6 +3,12 @@ use std::fmt::Display;
 use serde::Deserialize;
 use serde::Serialize;
 
+/// Return type for the `gettxoutproof` rpc command, the internal is
+/// just the hex representation of the Merkle Block, which was defined
+/// by btc core.
+#[derive(Debug, Deserialize, Serialize)]
path: crates/floresta-cli/src/rpc_types.rs
user: login: Davidson-Souza
body: Swap docstrings and attribute
created_at: 2025-05-20T16:55:09Z
author_association: MEMBER
diff_hunk: @@ -293,17 +292,13 @@ impl<D: AddressCacheDatabase> AddressCacheInner<D> {
         Some(transactions)
     }
 
-    /// Returns the Merkle Proof for a given address
-    fn get_merkle_proof(&self, txid: &Txid) -> Option<(Vec<String>, u32)> {
-        let mut hashes = Vec::new();
-        let tx = self.get_transaction(txid)?;
+    /// Get [Merkle Proof](https://developer.bitcoin.org/devguide/block_chain.html#merkle-trees) for a given txid.
path: crates/floresta-watch-only/src/lib.rs
user: login: Davidson-Souza
body: Use the non-inline links

E.g. 

```
Get a [Merkle Proof]....

[Merkle Proof]: https://developer.bitcoin.org/devguide/block_chain.html#merkle-trees
```
created_at: 2025-05-20T16:57:46Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,60 @@
+use std::any::type_name;
+use std::error::Error;
+use std::fmt::Display;
+use std::str::FromStr;
+
+#[derive(Debug)]
+/// Collection of errors to deal with parsing.
+pub enum ParseError {
+    /// Returned when the user inserts an broken array
+    InvalidHashArray,
+    /// Returned when the consumer of jsonlike_arrayofhashes tries to
+    /// cast into a incompatible type.
+    InvalidHashTarget(String),
+}
+impl Display for ParseError {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        match self {
+            ParseError::InvalidHashArray => write!(
+                f,
+                "Couldnt parse the inserted as a Hash Array, please refer to the docs"
+            ),
+            ParseError::InvalidHashTarget(target) => {
+                write!(f, "Could parse the hash itens to {target}")
+            }
+        }
+    }
+}
+
+impl Error for ParseError {}
+
+pub type Result<T> = std::result::Result<T, ParseError>;
path: crates/floresta-cli/src/parsers.rs
user: login: jaoleal
body: Reduces some code i think
created_at: 2025-05-20T19:10:55Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,60 @@
+use std::any::type_name;
+use std::error::Error;
+use std::fmt::Display;
+use std::str::FromStr;
+
+#[derive(Debug)]
+/// Collection of errors to deal with parsing.
+pub enum ParseError {
+    /// Returned when the user inserts an broken array
+    InvalidArray,
+
+    /// Returned when the consumer of tries to cast into
+    /// a incompatible type.
+    InvalidTarget(String),
+}
+
+impl Display for ParseError {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        match self {
+            ParseError::InvalidArray => write!(
+                f,
+                "Couldnt parse the inserted as an Array, please refer to the docs"
+            ),
+            ParseError::InvalidTarget(target) => {
+                write!(f, "Could parse itens to {target}")
+            }
+        }
+    }
+}
+
+impl Error for ParseError {}
+
+/// Tries to parse a json array, you can insert a type to be casted on each item.
+///
+/// Example: '["4a5e1e4baab89f3a32518a88c31bc87f618f76673e2cc77ab2127b7afdeda33b", "4a5e1e4baab89f3a32518a88c31bc87f618f76673e2cc77ab2127b7afdeda33b"]'
+/// Tries to parse a JSON array of hash strings into a vector of the target type.
+///
+/// The target type must implement Deserialize and can be converted from a hash string.
+/// By default, it will parse into Hash256, but you can specify any other compatible type.
+///
+/// Example:
+/// ```
+/// let hashes: Vec<Hash256> = parse_json_array(
+///     r#"["4a5e1e4baab89f3a32518a88c31bc87f618f76673e2cc77ab2127b7afdeda33b"]"#,
+/// )?;
+/// ```
path: crates/floresta-cli/src/parsers.rs
user: login: JoseSK999
body: Suggestion for fixing the doctest:
```suggestion
/// ```
/// # use bitcoin::hashes::sha256;
/// # use floresta_cli::parsers::parse_json_array;
/// let hashes: Vec<sha256::Hash> =
///     parse_json_array(r#"["4a5e1e4baab89f3a32518a88c31bc87f618f76673e2cc77ab2127b7afdeda33b"]"#)
///         .unwrap();
/// ```
```
created_at: 2025-05-21T14:43:14Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,60 @@
+use std::any::type_name;
+use std::error::Error;
+use std::fmt::Display;
+use std::str::FromStr;
+
+#[derive(Debug)]
+/// Collection of errors to deal with parsing.
+pub enum ParseError {
+    /// Returned when the user inserts an broken array
+    InvalidArray,
+
+    /// Returned when the consumer of tries to cast into
+    /// a incompatible type.
+    InvalidTarget(String),
+}
+
+impl Display for ParseError {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        match self {
+            ParseError::InvalidArray => write!(
+                f,
+                "Couldnt parse the inserted as an Array, please refer to the docs"
+            ),
+            ParseError::InvalidTarget(target) => {
+                write!(f, "Could parse itens to {target}")
+            }
+        }
+    }
+}
+
+impl Error for ParseError {}
+
+/// Tries to parse a json array, you can insert a type to be casted on each item.
+///
+/// Example: '["4a5e1e4baab89f3a32518a88c31bc87f618f76673e2cc77ab2127b7afdeda33b", "4a5e1e4baab89f3a32518a88c31bc87f618f76673e2cc77ab2127b7afdeda33b"]'
+/// Tries to parse a JSON array of hash strings into a vector of the target type.
+///
+/// The target type must implement Deserialize and can be converted from a hash string.
+/// By default, it will parse into Hash256, but you can specify any other compatible type.
+///
+/// Example:
+/// ```
+/// let hashes: Vec<Hash256> = parse_json_array(
+///     r#"["4a5e1e4baab89f3a32518a88c31bc87f618f76673e2cc77ab2127b7afdeda33b"]"#,
+/// )?;
+/// ```
path: crates/floresta-cli/src/parsers.rs
user: login: jaoleal
body: Thanks!
created_at: 2025-05-21T14:44:18Z
author_association: MEMBER
diff_hunk: @@ -30,6 +33,14 @@ impl<Blockchain: RpcChain> RpcImpl<Blockchain> {
             .map_err(|e| RpcError::Node(e.to_string()))
             .and_then(|block| block.ok_or(RpcError::BlockNotFound))
     }
+    /// Return the block that contains the given Txid
path: florestad/src/json_rpc/blockchain.rs
user: login: Davidson-Souza
body: Empty line
created_at: 2025-05-21T20:30:13Z
author_association: MEMBER

PR comments:
user: login: jaoleal
created_at: 2025-05-07T16:46:23Z
body: Applied @Davidson-Souza requests and rewrote the commit message, now appears to better explain whats being done here.

Refer to the PR description to read the last commit message
author_association: MEMBER
user: login: jaoleal
created_at: 2025-05-07T18:13:45Z
body: Applied @Davidson-Souza s suggestions. (https://github.com/vinteumorg/Floresta/pull/472#discussion_r2078086332)
author_association: MEMBER
user: login: jaoleal
created_at: 2025-05-15T18:09:22Z
body: 332e69d, made the command succeed exactly with cores api, you can check this by yourself.
```Bash
floresta-cli gettxoutproof '["5a4ebf66822b0b2d56bd9dc64ece0bc38ee7844a23ff1d7320a88c5fdb2ad3e2"]' 000000000043a8c0fd1d6f726790caa2a406010d19efd2780db27bdbbd93baf6

"01000000ba8b9cda965dd8e536670f9ddec10e53aab14b20bacad27b9137190000000000190760b278fe7b8565fda3b968b918d5fd997f993b23674c0af3b6fde300b38f33a5914ce6ed5b1b01e32f570200000002252bf9d75c4f481ebb6278d708257d1f12beb6dd30301d26c623f789b2ba6fc0e2d32adb5f8ca820731dff234a84e78ec30bce4ec69dbd562d0b2b8266bf4e5a0105"
```
and this is from a bitcoin core node (Im using a gambiarra with jq and a public node)
```Bash
[nix-shell:~]$ btc-rpc gettxoutproof '["5a4ebf66822b0b2d56bd9dc64ece0bc38ee7844a23ff1d7320a88c5fdb2ad3e2"]' 000000000043a8c0fd1d6f726790caa2a406010d19efd2780db27bdbbd93baf6

{
  "jsonrpc": "2.0",
  "id": "curl",
  "result": "01000000ba8b9cda965dd8e536670f9ddec10e53aab14b20bacad27b9137190000000000190760b278fe7b8565fda3b968b918d5fd997f993b23674c0af3b6fde300b38f33a5914ce6ed5b1b01e32f570200000002252bf9d75c4f481ebb6278d708257d1f12beb6dd30301d26c623f789b2ba6fc0e2d32adb5f8ca820731dff234a84e78ec30bce4ec69dbd562d0b2b8266bf4e5a0105"
}
```

cc @Davidson-Souza 

author_association: MEMBER
user: login: jaoleal
created_at: 2025-05-19T15:25:50Z
body: Added @Davidson-Souza Suggestions on f50525d
author_association: MEMBER
user: login: jaoleal
created_at: 2025-05-19T15:39:52Z
body: Added @Davidson-Souza Suggestions on 64b0536
author_association: MEMBER
user: login: jaoleal
created_at: 2025-05-19T19:34:37Z
body: signed and retriggered the buggy CI
author_association: MEMBER
user: login: jaoleal
created_at: 2025-05-20T00:52:18Z
body: Noooow its done, forgot to exclude floresta-cli from `cargo.toml`. thanks @Davidson-Souza 
author_association: MEMBER
user: login: jaoleal
created_at: 2025-05-22T15:02:30Z
body: > I think something got messed up in some rebase, because you're still changing the `florestad` `Cargo.toml`.
> 
> Everything else looks fine

Yes, strange... The change now is being triggered by lint, not by me.

Applied @Davidson-Souza suggestions in e60d583
author_association: MEMBER