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