breez_sdk_core/
chain.rs

1use std::sync::Arc;
2
3use anyhow::Result;
4use sdk_common::prelude::*;
5use serde::{Deserialize, Serialize};
6
7use crate::bitcoin::hashes::hex::FromHex;
8use crate::bitcoin::{OutPoint, Txid};
9use crate::error::{SdkError, SdkResult};
10
11pub const DEFAULT_MEMPOOL_SPACE_URL: &str = "https://mempool.space/api";
12
13#[tonic::async_trait]
14pub trait ChainService: Send + Sync {
15    async fn recommended_fees(&self) -> SdkResult<RecommendedFees>;
16    /// Gets up to 50 onchain and up to 25 mempool transactions associated with this address.
17    ///
18    /// See <https://mempool.space/docs/api/rest#get-address-transactions>
19    async fn address_transactions(&self, address: String) -> SdkResult<Vec<OnchainTx>>;
20    async fn current_tip(&self) -> SdkResult<u32>;
21    /// Gets the spending status of all tx outputs for this tx.
22    ///
23    /// See <https://mempool.space/docs/api/rest#get-transaction-outspends>
24    async fn transaction_outspends(&self, txid: String) -> SdkResult<Vec<Outspend>>;
25    /// If successful, it returns the transaction ID. Otherwise returns an `Err` describing the error.
26    async fn broadcast_transaction(&self, tx: Vec<u8>) -> SdkResult<String>;
27}
28
29pub trait RedundantChainServiceTrait: ChainService {
30    fn from_base_urls(rest_client: Arc<dyn RestClient>, base_urls: Vec<String>) -> Self;
31}
32
33#[derive(Clone)]
34pub struct RedundantChainService {
35    instances: Vec<MempoolSpace>,
36}
37impl RedundantChainServiceTrait for RedundantChainService {
38    fn from_base_urls(rest_client: Arc<dyn RestClient>, base_urls: Vec<String>) -> Self {
39        Self {
40            instances: base_urls
41                .iter()
42                .map(|url: &String| url.trim_end_matches('/'))
43                .map(|url| MempoolSpace::from_base_url(rest_client.clone(), url))
44                .collect(),
45        }
46    }
47}
48
49#[tonic::async_trait]
50impl ChainService for RedundantChainService {
51    async fn recommended_fees(&self) -> SdkResult<RecommendedFees> {
52        for inst in &self.instances {
53            match inst.recommended_fees().await {
54                Ok(res) => {
55                    return Ok(res);
56                }
57                Err(e) => error!("Call to chain service {} failed: {e}", inst.base_url),
58            }
59        }
60        Err(SdkError::service_connectivity(
61            "All chain service instances failed",
62        ))
63    }
64
65    async fn address_transactions(&self, address: String) -> SdkResult<Vec<OnchainTx>> {
66        for inst in &self.instances {
67            match inst.address_transactions(address.clone()).await {
68                Ok(res) => {
69                    return Ok(res);
70                }
71                Err(e) => error!("Call to chain service {} failed: {e}", inst.base_url),
72            }
73        }
74        Err(SdkError::service_connectivity(
75            "All chain service instances failed",
76        ))
77    }
78
79    async fn current_tip(&self) -> SdkResult<u32> {
80        for inst in &self.instances {
81            match inst.current_tip().await {
82                Ok(res) => {
83                    return Ok(res);
84                }
85                Err(e) => error!("Call to chain service {} failed: {e}", inst.base_url),
86            }
87        }
88        Err(SdkError::service_connectivity(
89            "All chain service instances failed",
90        ))
91    }
92
93    async fn transaction_outspends(&self, txid: String) -> SdkResult<Vec<Outspend>> {
94        for inst in &self.instances {
95            match inst.transaction_outspends(txid.clone()).await {
96                Ok(res) => {
97                    return Ok(res);
98                }
99                Err(e) => error!("Call to chain service {} failed: {e}", inst.base_url),
100            }
101        }
102        Err(SdkError::service_connectivity(
103            "All chain service instances failed",
104        ))
105    }
106
107    async fn broadcast_transaction(&self, tx: Vec<u8>) -> SdkResult<String> {
108        for inst in &self.instances {
109            match inst.broadcast_transaction(tx.clone()).await {
110                Ok(res) => {
111                    return Ok(res);
112                }
113                Err(e) => error!("Call to chain service {} failed: {e}", inst.base_url),
114            }
115        }
116        Err(SdkError::service_connectivity(
117            "All chain service instances failed",
118        ))
119    }
120}
121
122#[derive(Clone)]
123pub struct Utxo {
124    pub out: OutPoint,
125    pub value: u64,
126    pub block_height: Option<u32>,
127}
128
129#[derive(Clone)]
130pub struct AddressUtxos {
131    pub confirmed: Vec<Utxo>,
132}
133
134impl AddressUtxos {
135    /// Get the highest block height of all confirmed transactions that paid to the given onchain address
136    pub(crate) fn _confirmed_block(&self) -> u32 {
137        self.confirmed.iter().fold(0, |b, item| {
138            let confirmed_block = item.block_height.unwrap_or_default();
139            if confirmed_block != 0 || confirmed_block < b {
140                confirmed_block
141            } else {
142                b
143            }
144        })
145    }
146}
147
148/// Gets unspent tx outputs. Specifically filters out inbound utxos that have been spent.
149/// If include_unconfirmed_spends is true, then the result won't include utxos that were spent
150/// in unconfirmed transactions.
151pub(crate) fn get_utxos(
152    address: String,
153    transactions: Vec<OnchainTx>,
154    include_unconfirmed_spends: bool,
155) -> Result<AddressUtxos> {
156    let mut spent_outputs: Vec<OutPoint> = Vec::new();
157    let mut utxos: Vec<Utxo> = Vec::new();
158    for tx in transactions.iter() {
159        for vin in tx.vin.iter() {
160            if vin.prevout.scriptpubkey_address == address.clone()
161                && (include_unconfirmed_spends || tx.status.confirmed)
162            {
163                spent_outputs.push(OutPoint {
164                    txid: Txid::from_hex(vin.txid.as_str())?,
165                    vout: vin.vout,
166                })
167            }
168        }
169    }
170
171    for tx in transactions.iter() {
172        for (index, vout) in tx.vout.iter().enumerate() {
173            if vout.scriptpubkey_address == address {
174                let outpoint = OutPoint {
175                    txid: Txid::from_hex(tx.txid.as_str())?,
176                    vout: index as u32,
177                };
178                if !spent_outputs.contains(&outpoint) {
179                    utxos.push(Utxo {
180                        out: outpoint,
181                        value: vout.value,
182                        block_height: tx.status.block_height,
183                    });
184                }
185            }
186        }
187    }
188    let address_utxos = AddressUtxos {
189        confirmed: utxos
190            .clone()
191            .into_iter()
192            .filter(|u| u.block_height.is_some())
193            .collect(),
194    };
195    Ok(address_utxos)
196}
197
198#[derive(Clone)]
199pub(crate) struct MempoolSpace {
200    rest_client: Arc<dyn RestClient>,
201    pub(crate) base_url: String,
202}
203
204/// Wrapper containing the result of the recommended fees query, in sat/vByte, based on mempool.space data
205#[derive(Deserialize, Serialize, Clone, Debug)]
206pub struct RecommendedFees {
207    #[serde(rename(deserialize = "fastestFee"))]
208    pub fastest_fee: u64,
209
210    #[serde(rename(deserialize = "halfHourFee"))]
211    pub half_hour_fee: u64,
212
213    #[serde(rename(deserialize = "hourFee"))]
214    pub hour_fee: u64,
215
216    #[serde(rename(deserialize = "economyFee"))]
217    pub economy_fee: u64,
218
219    #[serde(rename(deserialize = "minimumFee"))]
220    pub minimum_fee: u64,
221}
222
223#[derive(Default, Deserialize, Serialize, Clone, Debug)]
224pub struct OnchainTx {
225    pub txid: String,
226    pub version: u32,
227    pub locktime: u32,
228    pub vin: Vec<Vin>,
229    pub vout: Vec<Vout>,
230    pub size: u32,
231    pub weight: u32,
232    pub fee: u32,
233    pub status: TxStatus,
234}
235
236#[derive(Default, Deserialize, Serialize, Clone, Debug)]
237pub struct TxStatus {
238    pub confirmed: bool,
239    pub block_height: Option<u32>,
240    pub block_hash: Option<String>,
241    pub block_time: Option<u64>,
242}
243
244#[derive(Default, Deserialize, Serialize, Clone, Debug)]
245pub struct Vout {
246    pub scriptpubkey: String,
247    pub scriptpubkey_asm: String,
248    pub scriptpubkey_type: String,
249    pub scriptpubkey_address: String,
250    pub value: u64,
251}
252
253#[derive(Default, Deserialize, Serialize, Clone, Debug)]
254pub struct Vin {
255    pub txid: String,
256    pub vout: u32,
257    pub prevout: Vout,
258    pub scriptsig: String,
259    pub scriptsig_asm: String,
260    pub witness: Option<Vec<String>>,
261    pub is_coinbase: bool,
262    pub sequence: u32,
263}
264
265/// Spending status of a transaction output.
266///
267/// If this is an outspend of a confirmed tx, `spent` is true and all other fields are set.
268/// If this is an outspend of an unconfirmed tx, `spent` is false and none of the other fields are set.
269#[derive(Serialize, Deserialize, Clone, Debug)]
270pub struct Outspend {
271    pub spent: bool,
272    pub txid: Option<String>,
273    pub vin: Option<u32>,
274    pub status: Option<TxStatus>,
275}
276
277impl MempoolSpace {
278    #[allow(dead_code)]
279    pub fn new(rest_client: Arc<dyn RestClient>) -> MempoolSpace {
280        MempoolSpace {
281            rest_client,
282            base_url: DEFAULT_MEMPOOL_SPACE_URL.into(),
283        }
284    }
285
286    pub fn from_base_url(rest_client: Arc<dyn RestClient>, base_url: &str) -> MempoolSpace {
287        MempoolSpace {
288            rest_client,
289            base_url: base_url.into(),
290        }
291    }
292}
293
294#[tonic::async_trait]
295impl ChainService for MempoolSpace {
296    async fn recommended_fees(&self) -> SdkResult<RecommendedFees> {
297        let (response, _) = get_and_check_success(
298            self.rest_client.as_ref(),
299            &format!("{}/v1/fees/recommended", self.base_url),
300        )
301        .await?;
302        Ok(parse_json(&response)?)
303    }
304
305    async fn address_transactions(&self, address: String) -> SdkResult<Vec<OnchainTx>> {
306        let (response, _) = get_and_check_success(
307            self.rest_client.as_ref(),
308            &format!("{}/address/{address}/txs", self.base_url),
309        )
310        .await?;
311        Ok(parse_json(&response)?)
312    }
313
314    async fn current_tip(&self) -> SdkResult<u32> {
315        let (response, _) = get_and_check_success(
316            self.rest_client.as_ref(),
317            &format!("{}/blocks/tip/height", self.base_url),
318        )
319        .await?;
320        Ok(parse_json(&response)?)
321    }
322
323    async fn transaction_outspends(&self, txid: String) -> SdkResult<Vec<Outspend>> {
324        let (response, _) = get_and_check_success(
325            self.rest_client.as_ref(),
326            &format!("{}/tx/{txid}/outspends", self.base_url),
327        )
328        .await?;
329        Ok(parse_json(&response)?)
330    }
331
332    async fn broadcast_transaction(&self, tx: Vec<u8>) -> SdkResult<String> {
333        let (txid_or_error, _) = self
334            .rest_client
335            .post(
336                &format!("{}/tx", self.base_url),
337                None,
338                Some(hex::encode(tx)),
339            )
340            .await?;
341        match txid_or_error.contains("error") {
342            true => Err(SdkError::Generic {
343                err: format!("Error fetching tx: {txid_or_error}"),
344            }),
345            false => Ok(txid_or_error),
346        }
347    }
348}
349#[cfg(test)]
350mod tests {
351    use std::sync::Arc;
352
353    use crate::{
354        chain::{MempoolSpace, OnchainTx, RedundantChainService, RedundantChainServiceTrait},
355        error::SdkError,
356    };
357    use anyhow::Result;
358    use sdk_common::prelude::{MockResponse, MockRestClient, RestClient};
359    use serde_json::json;
360    use tokio::test;
361
362    use super::ChainService;
363
364    #[test]
365    async fn test_recommended_fees() -> Result<()> {
366        let mock_rest_client = MockRestClient::new();
367
368        let response_body = json!({
369            "economyFee": 2,
370            "fastestFee": 3,
371            "halfHourFee": 2,
372            "hourFee": 2,
373            "minimumFee": 1,
374        })
375        .to_string();
376
377        mock_rest_client.add_response(MockResponse::new(200, response_body));
378        let rest_client: Arc<dyn RestClient> = Arc::new(mock_rest_client);
379
380        let ms = MempoolSpace::new(rest_client);
381        let fees = ms.recommended_fees().await?;
382        assert_eq!(fees.economy_fee, 2);
383        assert_eq!(fees.fastest_fee, 3);
384        assert_eq!(fees.half_hour_fee, 2);
385        assert_eq!(fees.hour_fee, 2);
386        assert_eq!(fees.minimum_fee, 1);
387
388        Ok(())
389    }
390
391    #[test]
392    async fn test_recommended_fees_with_fallback() -> Result<()> {
393        let mock_rest_client = MockRestClient::new();
394
395        let unreachable_response_body = "";
396        let response_body = json!({
397            "economyFee": 2,
398            "fastestFee": 3,
399            "halfHourFee": 2,
400            "hourFee": 2,
401            "minimumFee": 1,
402        });
403
404        mock_rest_client.add_response(MockResponse::new(
405            400,
406            unreachable_response_body.to_string(),
407        ));
408        mock_rest_client.add_response(MockResponse::new(
409            400,
410            unreachable_response_body.to_string(),
411        ));
412        mock_rest_client.add_response(MockResponse::new(200, response_body.to_string()));
413        mock_rest_client.add_response(MockResponse::new(
414            400,
415            unreachable_response_body.to_string(),
416        ));
417        mock_rest_client.add_response(MockResponse::new(
418            400,
419            unreachable_response_body.to_string(),
420        ));
421        mock_rest_client.add_response(MockResponse::new(
422            400,
423            unreachable_response_body.to_string(),
424        ));
425        mock_rest_client.add_response(MockResponse::new(
426            400,
427            unreachable_response_body.to_string(),
428        ));
429        mock_rest_client.add_response(MockResponse::new(200, response_body.to_string()));
430
431        let rest_client: Arc<dyn RestClient> = Arc::new(mock_rest_client);
432
433        let ms = RedundantChainService::from_base_urls(
434            rest_client.clone(),
435            vec!["https://mempool-url-unreachable.space/api/".into()],
436        );
437        assert!(ms.recommended_fees().await.is_err());
438
439        let ms = RedundantChainService::from_base_urls(
440            rest_client.clone(),
441            vec![
442                "https://mempool-url-unreachable.space/api/".into(),
443                "https://mempool.emzy.de/api/".into(),
444            ],
445        );
446        assert!(ms.recommended_fees().await.is_ok());
447
448        let ms = RedundantChainService::from_base_urls(
449            rest_client.clone(),
450            vec![
451                "https://mempool-url-unreachable.space/api/".into(),
452                "https://another-mempool-url-unreachable.space/api/".into(),
453            ],
454        );
455        assert!(ms.recommended_fees().await.is_err());
456
457        let ms = RedundantChainService::from_base_urls(
458            rest_client,
459            vec![
460                "https://mempool-url-unreachable.space/api/".into(),
461                "https://another-mempool-url-unreachable.space/api/".into(),
462                "https://mempool.emzy.de/api/".into(),
463            ],
464        );
465        assert!(ms.recommended_fees().await.is_ok());
466
467        Ok(())
468    }
469
470    #[test]
471    async fn test_address_transactions() -> Result<()> {
472        let mock_rest_client = MockRestClient::new();
473
474        let address_transactions_response_body = r#"[{"txid":"5e0668bf1cd24f2f8656ee82d4886f5303a06b26838e24b7db73afc59e228985","version":2,"locktime":0,"vin":[{"txid":"07c9d3fbffc20f96ea7c93ef3bcdf346c8a8456c25850ea76be62b24a7cf690c","vout":0,"prevout":{"scriptpubkey":"001465c96c830168b8f0b584294d3b9716bb8584c2d8","scriptpubkey_asm":"OP_0 OP_PUSHBYTES_20 65c96c830168b8f0b584294d3b9716bb8584c2d8","scriptpubkey_type":"v0_p2wpkh","scriptpubkey_address":"bc1qvhykeqcpdzu0pdvy99xnh9ckhwzcfskct6h6l2","value":263216},"scriptsig":"","scriptsig_asm":"","witness":["3045022100a2f0ac810ce88625890f7e212d175eb1cd6b7c73ffed95a2bec06b38e0b2de060220036675c6a5c89845988cc27e7acba772e7655f2abb0575449471d8323d5900b301","026b815dddaf1687a05349d75d25911c9b6e2381e55ba72148009cfa0a577c89d9"],"is_coinbase":false,"sequence":0},{"txid":"6d6766c283093e2d043ae877bb915175b3d8672a20f0459300267aaab1b5766a","vout":0,"prevout":{"scriptpubkey":"001485b33c1937058ed08b5b122e30caf18e67ccb282","scriptpubkey_asm":"OP_0 OP_PUSHBYTES_20 85b33c1937058ed08b5b122e30caf18e67ccb282","scriptpubkey_type":"v0_p2wpkh","scriptpubkey_address":"bc1qskencxfhqk8dpz6mzghrpjh33enuev5zh0mrjw","value":33247},"scriptsig":"","scriptsig_asm":"","witness":["304402200272cac1a312aae2a4ee64150e5b26e611a56509a467176e38c905b632d3ce56022005497d0d3ff14911214cb0fbb22a1aa16830ba669f6ff38723684750ceb4b11a01","0397d3b72557bd2044508ee3b22d1216b3f871c0963500f8c8dc6a143ee7a6a206"],"is_coinbase":false,"sequence":0},{"txid":"81af33ae00a9dadeb83b915b05742e986a470fff7456540e3f018deb94abda0e","vout":1,"prevout":{"scriptpubkey":"001431505647092347abb0e4d2a34f6773b74a999d45","scriptpubkey_asm":"OP_0 OP_PUSHBYTES_20 31505647092347abb0e4d2a34f6773b74a999d45","scriptpubkey_type":"v0_p2wpkh","scriptpubkey_address":"bc1qx9g9v3cfydr6hv8y62357emnka9fn8294e73yl","value":172952},"scriptsig":"","scriptsig_asm":"","witness":["30450221008426c1b3d535f10c7cbccec6be3ea9be3514f3a86bf234584722665325283f35022010b6a617a465d1d7eea45562632f0ab80b0894da44b67fab65191a98fd9d3acb01","0221250914423379d3caf662297e8069621ca2c362cf92107388483929f4d9eb67"],"is_coinbase":false,"sequence":0}],"vout":[{"scriptpubkey":"001459c70c09f22b1bb007439af43b6809d6a2bc31b5","scriptpubkey_asm":"OP_0 OP_PUSHBYTES_20 59c70c09f22b1bb007439af43b6809d6a2bc31b5","scriptpubkey_type":"v0_p2wpkh","scriptpubkey_address":"bc1qt8rscz0j9vdmqp6rnt6rk6qf663tcvd44f6gxa","value":2920},{"scriptpubkey":"00202c404e6e9c4d032267a29a6074c5db9333c6ccae0c9d430ced666316233d8c2f","scriptpubkey_asm":"OP_0 OP_PUSHBYTES_32 2c404e6e9c4d032267a29a6074c5db9333c6ccae0c9d430ced666316233d8c2f","scriptpubkey_type":"v0_p2wsh","scriptpubkey_address":"bc1q93qyum5uf5pjyeaznfs8f3wmjveudn9wpjw5xr8dve33vgea3shs9jhvww","value":442557}],"size":532,"weight":1153,"fee":23938,"status":{"confirmed":true,"block_height":674358,"block_hash":"00000000000000000004c6171622f56692cc480d3c76ecae4355e69699a6ae44","block_time":1615595727}},{"txid":"07c9d3fbffc20f96ea7c93ef3bcdf346c8a8456c25850ea76be62b24a7cf690c","version":2,"locktime":0,"vin":[{"txid":"9332d8d11d81c3b674caff75db5543491e7f22e619ecc034bedf4a007518fe3a","vout":0,"prevout":{"scriptpubkey":"001415f0dad74806b03612687038d4f5bab200afcf8e","scriptpubkey_asm":"OP_0 OP_PUSHBYTES_20 15f0dad74806b03612687038d4f5bab200afcf8e","scriptpubkey_type":"v0_p2wpkh","scriptpubkey_address":"bc1qzhcd446gq6crvyngwqudfad6kgq2lnuw9r2a86","value":470675},"scriptsig":"","scriptsig_asm":"","witness":["3045022100f30d84532f96b5e489047174e81394883cd519d427ca8f4facc2366f718cc678022007c083634402f40708c645cd0c1a2757b56de2076ca6ee856e514859381cd93801","02942b44eb4289e3af0aeeb73dfa82b0a5c8a3a06ae85bfd22aa3dcfcd64096462"],"is_coinbase":false,"sequence":0},{"txid":"c62da0c2d1929ab2a2c04d4fbae2a6e4e947f867cba584d1f80c4a1a62f4a75f","vout":1,"prevout":{"scriptpubkey":"0014f0c1d6b471d5e4a483fc146d4220a4e81587bf11","scriptpubkey_asm":"OP_0 OP_PUSHBYTES_20 f0c1d6b471d5e4a483fc146d4220a4e81587bf11","scriptpubkey_type":"v0_p2wpkh","scriptpubkey_address":"bc1q7rqaddr36hj2fqluz3k5yg9yaq2c00c3tw4qy5","value":899778},"scriptsig":"","scriptsig_asm":"","witness":["304402202da0eac25786003181526c4fe1592f982aa8d0f32c642a5103cdebbf4aa8b5a80220750cd6859bfb9a7df8d7c4d79a70e17a6df87f150fe1fdaade4650332ef0f47c01","02ecab80fcfe949633064c25fc33854fd09b8730decdf679db1f429bce201ec685"],"is_coinbase":false,"sequence":0}],"vout":[{"scriptpubkey":"001465c96c830168b8f0b584294d3b9716bb8584c2d8","scriptpubkey_asm":"OP_0 OP_PUSHBYTES_20 65c96c830168b8f0b584294d3b9716bb8584c2d8","scriptpubkey_type":"v0_p2wpkh","scriptpubkey_address":"bc1qvhykeqcpdzu0pdvy99xnh9ckhwzcfskct6h6l2","value":263216},{"scriptpubkey":"00200cea60ae9eea43e64b17ba65a4c17bd3acf9dac307825deda85d5a093181dbc0","scriptpubkey_asm":"OP_0 OP_PUSHBYTES_32 0cea60ae9eea43e64b17ba65a4c17bd3acf9dac307825deda85d5a093181dbc0","scriptpubkey_type":"v0_p2wsh","scriptpubkey_address":"bc1qpn4xpt57afp7vjchhfj6fstm6wk0nkkrq7p9mmdgt4dqjvvpm0qqlxqrns","value":1088924}],"size":383,"weight":881,"fee":18313,"status":{"confirmed":true,"block_height":674357,"block_hash":"00000000000000000008d0d007995a8bc9d60de17bd6b55e28a6e4c6918cb206","block_time":1615594996}}]"#;
475        let transaction_outspends_response_body = r#"[{"spent":true,"txid":"4da22eff957b855c8bde2d8b61bdb9e10add799a04c709dd7142cc796cee0b65","vin":1,"status":{"confirmed":true,"block_height":674365,"block_hash":"000000000000000000038f780364221846a3c11e2a5b33eee69029afe5775a0f","block_time":1615598852}},{"spent":true,"txid":"61585c400d8cfe490d3d3c6e1e3177edb9b6f43e337772530ab32ea4e54db3b4","vin":0,"status":{"confirmed":true,"block_height":797168,"block_hash":"0000000000000000000569b9dca483f10ed6c2bf9245b5a9b45519dd4f3dd40d","block_time":1688489603}}]"#;
476        mock_rest_client.add_response(MockResponse::new(
477            200,
478            address_transactions_response_body.to_string(),
479        ));
480        mock_rest_client.add_response(MockResponse::new(
481            200,
482            transaction_outspends_response_body.to_string(),
483        ));
484        mock_rest_client.add_response(MockResponse::new(404, "".to_string()));
485
486        let rest_client: Arc<dyn RestClient> = Arc::new(mock_rest_client);
487
488        let ms = MempoolSpace::new(rest_client);
489        let txs = ms
490            .address_transactions("bc1qvhykeqcpdzu0pdvy99xnh9ckhwzcfskct6h6l2".to_string())
491            .await?;
492        let serialized_res = serde_json::to_string(&txs)?;
493
494        let expected_txs: Vec<OnchainTx> =
495            serde_json::from_str(address_transactions_response_body)?;
496        let expected_serialized = serde_json::to_string(&expected_txs)?;
497
498        assert_eq!(expected_serialized, serialized_res);
499
500        let outspends = ms
501            .transaction_outspends(
502                "5e0668bf1cd24f2f8656ee82d4886f5303a06b26838e24b7db73afc59e228985".to_string(),
503            )
504            .await?;
505        assert_eq!(outspends.len(), 2);
506
507        let outspends = ms
508            .transaction_outspends(
509                "07c9d3fbffc20f96ea7c93ef3bcdf346c8a8456c25850ea76be62b24a7cf6901".to_string(),
510            )
511            .await;
512        match outspends {
513            Ok(_) => panic!("Expected an error"),
514            Err(e) => match e {
515                SdkError::ServiceConnectivity { err } => {
516                    assert_eq!(err, "GET request https://mempool.space/api/tx/07c9d3fbffc20f96ea7c93ef3bcdf346c8a8456c25850ea76be62b24a7cf6901/outspends failed with status: 404")
517                }
518                _ => panic!("Expected a service connectivity error"),
519            },
520        };
521
522        Ok(())
523    }
524
525    // #[test]
526    // async fn test_address_transactions_mempool() {
527    //     let mock_rest_client = MockRestClient::new();
528    //     let ms = MempoolSpace::new(mock_rest_client);
529    //
530    //     let response_body = r#"[{"txid":"5e0668bf1cd24f2f8656ee82d4886f5303a06b26838e24b7db73afc59e228985","version":2,"locktime":0,"vin":[{"txid":"07c9d3fbffc20f96ea7c93ef3bcdf346c8a8456c25850ea76be62b24a7cf690c","vout":0,"prevout":{"scriptpubkey":"001465c96c830168b8f0b584294d3b9716bb8584c2d8","scriptpubkey_asm":"OP_0 OP_PUSHBYTES_20 65c96c830168b8f0b584294d3b9716bb8584c2d8","scriptpubkey_type":"v0_p2wpkh","scriptpubkey_address":"bc1qvhykeqcpdzu0pdvy99xnh9ckhwzcfskct6h6l2","value":263216},"scriptsig":"","scriptsig_asm":"","witness":["3045022100a2f0ac810ce88625890f7e212d175eb1cd6b7c73ffed95a2bec06b38e0b2de060220036675c6a5c89845988cc27e7acba772e7655f2abb0575449471d8323d5900b301","026b815dddaf1687a05349d75d25911c9b6e2381e55ba72148009cfa0a577c89d9"],"is_coinbase":false,"sequence":0},{"txid":"6d6766c283093e2d043ae877bb915175b3d8672a20f0459300267aaab1b5766a","vout":0,"prevout":{"scriptpubkey":"001485b33c1937058ed08b5b122e30caf18e67ccb282","scriptpubkey_asm":"OP_0 OP_PUSHBYTES_20 85b33c1937058ed08b5b122e30caf18e67ccb282","scriptpubkey_type":"v0_p2wpkh","scriptpubkey_address":"bc1qskencxfhqk8dpz6mzghrpjh33enuev5zh0mrjw","value":33247},"scriptsig":"","scriptsig_asm":"","witness":["304402200272cac1a312aae2a4ee64150e5b26e611a56509a467176e38c905b632d3ce56022005497d0d3ff14911214cb0fbb22a1aa16830ba669f6ff38723684750ceb4b11a01","0397d3b72557bd2044508ee3b22d1216b3f871c0963500f8c8dc6a143ee7a6a206"],"is_coinbase":false,"sequence":0},{"txid":"81af33ae00a9dadeb83b915b05742e986a470fff7456540e3f018deb94abda0e","vout":1,"prevout":{"scriptpubkey":"001431505647092347abb0e4d2a34f6773b74a999d45","scriptpubkey_asm":"OP_0 OP_PUSHBYTES_20 31505647092347abb0e4d2a34f6773b74a999d45","scriptpubkey_type":"v0_p2wpkh","scriptpubkey_address":"bc1qx9g9v3cfydr6hv8y62357emnka9fn8294e73yl","value":172952},"scriptsig":"","scriptsig_asm":"","witness":["30450221008426c1b3d535f10c7cbccec6be3ea9be3514f3a86bf234584722665325283f35022010b6a617a465d1d7eea45562632f0ab80b0894da44b67fab65191a98fd9d3acb01","0221250914423379d3caf662297e8069621ca2c362cf92107388483929f4d9eb67"],"is_coinbase":false,"sequence":0}],"vout":[{"scriptpubkey":"001459c70c09f22b1bb007439af43b6809d6a2bc31b5","scriptpubkey_asm":"OP_0 OP_PUSHBYTES_20 59c70c09f22b1bb007439af43b6809d6a2bc31b5","scriptpubkey_type":"v0_p2wpkh","scriptpubkey_address":"bc1qt8rscz0j9vdmqp6rnt6rk6qf663tcvd44f6gxa","value":2920},{"scriptpubkey":"00202c404e6e9c4d032267a29a6074c5db9333c6ccae0c9d430ced666316233d8c2f","scriptpubkey_asm":"OP_0 OP_PUSHBYTES_32 2c404e6e9c4d032267a29a6074c5db9333c6ccae0c9d430ced666316233d8c2f","scriptpubkey_type":"v0_p2wsh","scriptpubkey_address":"bc1q93qyum5uf5pjyeaznfs8f3wmjveudn9wpjw5xr8dve33vgea3shs9jhvww","value":442557}],"size":532,"weight":1153,"fee":23938,"status":{"confirmed":true,"block_height":674358,"block_hash":"00000000000000000004c6171622f56692cc480d3c76ecae4355e69699a6ae44","block_time":1615595727}},{"txid":"07c9d3fbffc20f96ea7c93ef3bcdf346c8a8456c25850ea76be62b24a7cf690c","version":2,"locktime":0,"vin":[{"txid":"9332d8d11d81c3b674caff75db5543491e7f22e619ecc034bedf4a007518fe3a","vout":0,"prevout":{"scriptpubkey":"001415f0dad74806b03612687038d4f5bab200afcf8e","scriptpubkey_asm":"OP_0 OP_PUSHBYTES_20 15f0dad74806b03612687038d4f5bab200afcf8e","scriptpubkey_type":"v0_p2wpkh","scriptpubkey_address":"bc1qzhcd446gq6crvyngwqudfad6kgq2lnuw9r2a86","value":470675},"scriptsig":"","scriptsig_asm":"","witness":["3045022100f30d84532f96b5e489047174e81394883cd519d427ca8f4facc2366f718cc678022007c083634402f40708c645cd0c1a2757b56de2076ca6ee856e514859381cd93801","02942b44eb4289e3af0aeeb73dfa82b0a5c8a3a06ae85bfd22aa3dcfcd64096462"],"is_coinbase":false,"sequence":0},{"txid":"c62da0c2d1929ab2a2c04d4fbae2a6e4e947f867cba584d1f80c4a1a62f4a75f","vout":1,"prevout":{"scriptpubkey":"0014f0c1d6b471d5e4a483fc146d4220a4e81587bf11","scriptpubkey_asm":"OP_0 OP_PUSHBYTES_20 f0c1d6b471d5e4a483fc146d4220a4e81587bf11","scriptpubkey_type":"v0_p2wpkh","scriptpubkey_address":"bc1q7rqaddr36hj2fqluz3k5yg9yaq2c00c3tw4qy5","value":899778},"scriptsig":"","scriptsig_asm":"","witness":["304402202da0eac25786003181526c4fe1592f982aa8d0f32c642a5103cdebbf4aa8b5a80220750cd6859bfb9a7df8d7c4d79a70e17a6df87f150fe1fdaade4650332ef0f47c01","02ecab80fcfe949633064c25fc33854fd09b8730decdf679db1f429bce201ec685"],"is_coinbase":false,"sequence":0}],"vout":[{"scriptpubkey":"001465c96c830168b8f0b584294d3b9716bb8584c2d8","scriptpubkey_asm":"OP_0 OP_PUSHBYTES_20 65c96c830168b8f0b584294d3b9716bb8584c2d8","scriptpubkey_type":"v0_p2wpkh","scriptpubkey_address":"bc1qvhykeqcpdzu0pdvy99xnh9ckhwzcfskct6h6l2","value":263216},{"scriptpubkey":"00200cea60ae9eea43e64b17ba65a4c17bd3acf9dac307825deda85d5a093181dbc0","scriptpubkey_asm":"OP_0 OP_PUSHBYTES_32 0cea60ae9eea43e64b17ba65a4c17bd3acf9dac307825deda85d5a093181dbc0","scriptpubkey_type":"v0_p2wsh","scriptpubkey_address":"bc1qpn4xpt57afp7vjchhfj6fstm6wk0nkkrq7p9mmdgt4dqjvvpm0qqlxqrns","value":1088924}],"size":383,"weight":881,"fee":18313,"status":{"confirmed":true,"block_height":674357,"block_hash":"00000000000000000008d0d007995a8bc9d60de17bd6b55e28a6e4c6918cb206","block_time":1615594996}}]"#;
531    //     mock_rest_client.add_response(MockResponse::new(200, response_body.to_string()));
532    //
533    //     let txs = ms
534    //         .address_transactions("1N4f3y3LYJZ2Qd9FyPt3AcHp451qt12paR".to_string())
535    //         .await
536    //         .unwrap();
537    //     let serialized_res = serde_json::to_string(&txs).unwrap();
538
539    //     let expected_txs: Vec<OnchainTx> = serde_json::from_str(response_body).unwrap();
540    //     let expected_serialized = serde_json::to_string(&expected_txs).unwrap();
541
542    //     assert_eq!(expected_serialized, serialized_res);
543    // }
544}