clementine_core/tx_sender/
nonstandard.rs

1//! TxSender for nonstandard transactions.
2//!
3//! This module contains the logic for sending nonstandard transactions for various bitcoin networks.
4use bitcoin::consensus::serialize;
5use bitcoin::Transaction;
6use hex;
7use std::collections::HashMap;
8
9use super::{log_error_for_tx, SendTxError, TxSender};
10
11impl TxSender {
12    /// Checks if a bridge transaction is nonstandard. Keep in mind that these are not all cases where a transaction is nonstandard.
13    /// We only check non-standard types that clementine generates by default in non-standard mode.
14    /// Currently checks these cases:
15    /// 1. The transaction contains 0 sat non-anchor (only checks our specific anchor address)
16    ///    and non-op return output.
17    /// 2. The transaction weight is bigger than 400k
18    ///
19    /// Arguments:
20    /// * `tx` - The transaction to check.
21    ///
22    /// Returns:
23    /// * `true` if the transaction is nonstandard, `false` otherwise.
24    pub fn is_bridge_tx_nonstandard(&self, tx: &Transaction) -> bool {
25        tx.output.iter().any(|output| {
26            output.value.to_sat() == 0
27                && !self.is_p2a_anchor(output)
28                && !output.script_pubkey.is_op_return()
29        }) || tx.weight().to_wu() > 400_000
30    }
31
32    /// Sends a nonstandard transaction to testnet4 using the mempool.space accelerator.
33    ///
34    /// Arguments:
35    /// * `tx` - The transaction to send.
36    ///
37    /// Returns:
38    /// * `Ok(())` if the transaction is sent successfully to the accelerator.
39    /// * `Err(SendTxError)` if the transaction is not sent successfully to the accelerator.
40    ///
41    /// Note: Mempool.space accelerator doesn't accept transactions if:
42    ///     - At least one of the transaction's inputs is signed with either the SIGHASH_NONE or SIGHASH_ANYONECANPAY flag, which may allow a third party to replace the transaction.
43    ///     - The number of signature operations multiplied by 20 exceeds the transaction's weight.
44    /// [Mempool Space API docs](https://mempool.space/docs/api/rest)
45    /// [Mempool Space Accelerator FAQ](https://mempool.space/accelerator/faq)
46    pub async fn send_testnet4_nonstandard_tx(
47        &self,
48        tx: &Transaction,
49        try_to_send_id: u32,
50    ) -> Result<(), SendTxError> {
51        // Get API key from environment variable
52        let api_key = std::env::var("MEMPOOL_SPACE_API_KEY").map_err(|_| {
53            SendTxError::Other(eyre::eyre!(
54                "MEMPOOL_SPACE_API_KEY environment variable not set, cannot send nonstandard transactions to testnet4"
55            ))
56        })?;
57
58        // first check if the transaction is already submitted to the accelerator
59        let txid = tx.compute_txid();
60        let response = self
61            .http_client
62            .get("https://mempool.space/api/v1/services/accelerator/testnet4/accelerations")
63            .header("X-Mempool-Auth", api_key.clone())
64            .send()
65            .await
66            .map_err(|e| {
67                SendTxError::NetworkError(format!(
68                    "Failed to get transaction history from mempool.space accelerator: {e}"
69                ))
70            })?;
71        if response.status().is_success() {
72            // Try to parse the response, if for some reason response can't be parsed,
73            // don't return errors, so we continue with sending the transaction to the accelerator.
74            let text = response.text().await.unwrap_or_default();
75            let previously_sent_txs: serde_json::Value =
76                serde_json::from_str(&text).unwrap_or_else(|_| serde_json::json!([]));
77
78            // try to parse the response
79            for tx in previously_sent_txs.as_array().unwrap_or(&vec![]) {
80                let Some(response_txid) = tx.get("txid").and_then(|v| v.as_str()) else {
81                    continue;
82                };
83
84                if response_txid == txid.to_string() && tx["status"] != "failed" {
85                    tracing::debug!(
86                        "Found {:?} with status {:?} in accelerator transaction history",
87                        txid,
88                        tx["status"]
89                    );
90                    let _ = self
91                        .db
92                        .update_tx_debug_sending_state(
93                            try_to_send_id,
94                            "nonstandard_testnet4_send_submitted",
95                            false,
96                        )
97                        .await;
98                    return Ok(()); // Already submitted
99                }
100            }
101        } else {
102            let status = response.status();
103            let error_text = response.text().await.unwrap_or_default();
104            return Err(SendTxError::NetworkError(format!(
105                "Accelerator returned HTTP {status}: {error_text}"
106            )));
107        }
108
109        // Serialize transaction to hex
110        let tx_hex = hex::encode(serialize(tx));
111
112        // Prepare form data
113        let mut form_data = HashMap::new();
114        form_data.insert("txInput", tx_hex);
115        form_data.insert("label", format!("clementine-{}", tx.compute_txid()));
116
117        // Make the API request
118        let response = self
119            .http_client
120            .post("https://mempool.space/api/v1/services/accelerator/testnet4/accelerate/hex")
121            .header("X-Mempool-Auth", api_key)
122            .form(&form_data)
123            .send()
124            .await
125            .map_err(|e| {
126                SendTxError::NetworkError(format!(
127                    "Failed to submit transaction to mempool.space accelerator: {e}"
128                ))
129            })?;
130
131        // Check if the request was successful
132        if response.status().is_success() {
133            let response_text = response
134                .text()
135                .await
136                .map_err(|e| SendTxError::NetworkError(format!("Failed to read response: {e}")))?;
137
138            tracing::info!(
139                "Successfully submitted nonstandard transaction {:?} to mempool.space testnet4 accelerator: {}",
140                txid,
141                response_text
142            );
143
144            let _ = self
145                .db
146                .update_tx_debug_sending_state(
147                    try_to_send_id,
148                    "nonstandard_testnet4_send_success",
149                    true,
150                )
151                .await;
152
153            Ok(())
154        } else {
155            let status = response.status();
156            let error_text = response
157                .text()
158                .await
159                .unwrap_or_else(|_| "Unknown error".to_string());
160
161            log_error_for_tx!(
162                self.db,
163                try_to_send_id,
164                format!(
165                    "Failed to submit transaction to mempool.space. Status: {}, Error: {}",
166                    status, error_text
167                )
168            );
169            let _ = self
170                .db
171                .update_tx_debug_sending_state(
172                    try_to_send_id,
173                    "nonstandard_testnet4_send_failed",
174                    true,
175                )
176                .await;
177
178            Err(SendTxError::NetworkError(format!(
179                "Failed to submit transaction to mempool.space. Status: {status}, Error: {error_text}"
180            )))
181        }
182    }
183}