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