clementine_tx_sender/lib.rs
1//! # Clementine Transaction Sender
2//!
3//! This crate handles the creation, signing, and broadcasting of Bitcoin transactions,
4//! supporting various fee-bumping strategies like CPFP and RBF.
5
6pub mod client;
7pub mod cpfp;
8pub mod nonstandard;
9pub mod rbf;
10pub mod task;
11
12// Define a macro for logging errors and saving them to the database
13#[macro_export]
14macro_rules! log_error_for_tx {
15 ($db:expr, $try_to_send_id:expr, $err:expr) => {{
16 let db = $db.clone();
17 let try_to_send_id = $try_to_send_id;
18 let err = $err.to_string();
19 tracing::warn!(try_to_send_id, "{}", err);
20 tokio::spawn(async move {
21 let _ = db
22 .save_tx_debug_submission_error(None, try_to_send_id, &err)
23 .await;
24 });
25 }};
26}
27
28pub use clementine_errors::SendTxError;
29use clementine_primitives::BitcoinSyncerEvent;
30pub use client::TxSenderClient;
31
32use async_trait::async_trait;
33use bitcoin::secp256k1::schnorr;
34use bitcoin::taproot::TaprootSpendInfo;
35use bitcoin::{
36 Address, Amount, FeeRate, OutPoint, Sequence, Transaction, Txid, Weight, XOnlyPublicKey,
37};
38use bitcoincore_rpc::RpcApi;
39use clementine_config::protocol::ProtocolParamset;
40use clementine_config::tx_sender::TxSenderLimits;
41use clementine_errors::{BridgeError, ResultExt};
42
43pub type Result<T, E = SendTxError> = std::result::Result<T, E>;
44
45use clementine_utils::sign::TapTweakData;
46use clementine_utils::{FeePayingType, RbfSigningInfo, TxMetadata};
47use eyre::OptionExt;
48use serde::{Deserialize, Serialize};
49
50/// Activation condition based on a transaction ID.
51#[derive(Copy, Clone, Debug, Eq, Hash, PartialEq, PartialOrd, Ord, Serialize, Deserialize)]
52pub struct ActivatedWithTxid {
53 /// The transaction ID that must be seen.
54 pub txid: Txid,
55 /// Number of blocks that must pass after seeing the transaction.
56 pub relative_block_height: u32,
57}
58
59/// Activation condition based on an outpoint.
60#[derive(Copy, Clone, Debug, Eq, Hash, PartialEq, PartialOrd, Ord, Serialize, Deserialize)]
61pub struct ActivatedWithOutpoint {
62 /// The outpoint that must be spent.
63 pub outpoint: OutPoint,
64 /// Number of blocks that must pass after seeing the outpoint spent.
65 pub relative_block_height: u32,
66}
67
68/// Default sequence for transactions.
69pub const DEFAULT_SEQUENCE: Sequence = Sequence(0xFFFFFFFD);
70
71/// Represents a spendable UTXO.
72#[derive(Debug, Clone)]
73pub struct SpendableUtxo {
74 pub outpoint: OutPoint,
75 pub txout: bitcoin::TxOut,
76 pub spend_info: Option<TaprootSpendInfo>,
77}
78
79impl SpendableInputInfo for SpendableUtxo {
80 fn get_prevout(&self) -> &bitcoin::TxOut {
81 &self.txout
82 }
83
84 fn get_outpoint(&self) -> OutPoint {
85 self.outpoint
86 }
87}
88
89/// Trait for extracting information from a spendable input.
90/// This allows different input types (SpendableUtxo, SpendableTxIn) to be used interchangeably.
91pub trait SpendableInputInfo: Send + Sync + Clone {
92 /// Returns a reference to the previous output (TxOut) for this input.
93 fn get_prevout(&self) -> &bitcoin::TxOut;
94
95 /// Returns the outpoint for this input.
96 fn get_outpoint(&self) -> OutPoint;
97}
98
99/// Trait for building child transactions in the transaction sender.
100///
101/// This abstraction allows the core crate to provide `SpendableTxIn`-based transaction building
102/// using `TxHandler`, while keeping the tx-sender crate independent of the builder module.
103///
104/// All methods are static - no instance of this trait is stored.
105pub trait TxSenderTxBuilder: Send + Sync + 'static {
106 /// The type representing a spendable transaction input.
107 /// In core, this would be `SpendableTxIn`.
108 type SpendableInput: SpendableInputInfo;
109
110 /// Builds a child transaction for CPFP.
111 ///
112 /// This method constructs a child transaction that spends the P2A anchor output
113 /// and fee payer UTXOs, paying the required fees for the CPFP package.
114 ///
115 /// # Arguments
116 /// * `p2a_anchor` - The P2A anchor output to spend
117 /// * `anchor_sat` - Amount in the anchor output
118 /// * `fee_payer_utxos` - UTXOs to fund the child transaction
119 /// * `change_address` - Address for the change output
120 /// * `required_fee` - The calculated required fee for the package
121 /// * `signer_address` - The signer's address (for script pubkey)
122 /// * `signer` - The signer to sign the transaction inputs
123 ///
124 /// # Returns
125 /// A signed child transaction ready for package submission.
126 fn build_child_tx<S: TxSenderSigner>(
127 p2a_anchor: OutPoint,
128 anchor_sat: Amount,
129 fee_payer_utxos: Vec<Self::SpendableInput>,
130 change_address: Address,
131 required_fee: Amount,
132 signer: &S,
133 ) -> Result<Transaction, BridgeError>;
134
135 /// Converts database UTXOs into the builder's SpendableInput type.
136 ///
137 /// # Arguments
138 /// * `utxos` - Vector of (txid, vout, amount) tuples from the database
139 /// * `signer_address` - The signer's address (for script pubkey generation)
140 ///
141 /// # Returns
142 /// Vector of SpendableInput instances ready for use in transaction building.
143 fn utxos_to_spendable_inputs(
144 utxos: Vec<(Txid, u32, Amount)>,
145 signer_address: &Address,
146 ) -> Vec<Self::SpendableInput>;
147}
148
149/// Trait for signing transactions in the transaction sender.
150#[async_trait]
151pub trait TxSenderSigner: Send + Sync {
152 /// Returns the signer's Bitcoin address.
153 fn address(&self) -> &Address;
154
155 /// Returns the signer's X-only public key.
156 fn xonly_public_key(&self) -> XOnlyPublicKey;
157
158 /// Signs a message with a tweak.
159 fn sign_with_tweak_data(
160 &self,
161 sighash: bitcoin::TapSighash,
162 tweak_data: TapTweakData,
163 ) -> Result<schnorr::Signature, BridgeError>;
164}
165
166/// Trait for database operations required by the transaction sender.
167#[async_trait]
168pub trait TxSenderDatabase: Send + Sync + Clone {
169 /// Type for database transactions.
170 type Transaction: Send;
171
172 /// Begin a new database transaction.
173 async fn begin_transaction(&self) -> Result<Self::Transaction, BridgeError>;
174
175 /// Commit a database transaction.
176 async fn commit_transaction(&self, dbtx: Self::Transaction) -> Result<(), BridgeError>;
177
178 /// Save a debug message for a transaction submission error.
179 async fn save_tx_debug_submission_error(
180 &self,
181 dbtx: Option<&mut Self::Transaction>,
182 id: u32,
183 error: &str,
184 ) -> Result<(), BridgeError>;
185
186 /// Get transactions that are ready to be sent.
187 async fn get_sendable_txs(
188 &self,
189 fee_rate: FeeRate,
190 current_tip_height: u32,
191 ) -> Result<Vec<u32>, BridgeError>;
192
193 /// Get details of a transaction to be sent.
194 async fn get_try_to_send_tx(
195 &self,
196 dbtx: Option<&mut Self::Transaction>,
197 id: u32,
198 ) -> Result<
199 (
200 Option<TxMetadata>,
201 Transaction,
202 FeePayingType,
203 Option<u32>,
204 Option<RbfSigningInfo>,
205 ),
206 BridgeError,
207 >;
208
209 /// Update the debug sending state of a transaction.
210 async fn update_tx_debug_sending_state(
211 &self,
212 id: u32,
213 state: &str,
214 is_error: bool,
215 ) -> Result<(), BridgeError>;
216
217 /// Get all unconfirmed fee payer transactions.
218 async fn get_all_unconfirmed_fee_payer_txs(
219 &self,
220 dbtx: Option<&mut Self::Transaction>,
221 ) -> Result<Vec<(u32, u32, Txid, u32, Amount, Option<u32>)>, BridgeError>;
222
223 /// Get unconfirmed fee payer transactions for a specific parent transaction.
224 async fn get_unconfirmed_fee_payer_txs(
225 &self,
226 dbtx: Option<&mut Self::Transaction>,
227 bumped_id: u32,
228 ) -> Result<Vec<(u32, Txid, u32, Amount)>, BridgeError>;
229
230 /// Mark a fee payer UTXO as evicted.
231 async fn mark_fee_payer_utxo_as_evicted(
232 &self,
233 dbtx: Option<&mut Self::Transaction>,
234 id: u32,
235 ) -> Result<(), BridgeError>;
236
237 /// Get confirmed fee payer UTXOs for a specific parent transaction.
238 async fn get_confirmed_fee_payer_utxos(
239 &self,
240 dbtx: Option<&mut Self::Transaction>,
241 id: u32,
242 ) -> Result<Vec<(Txid, u32, Amount)>, BridgeError>;
243
244 /// Save a fee payer transaction.
245 #[allow(clippy::too_many_arguments)]
246 async fn save_fee_payer_tx(
247 &self,
248 dbtx: Option<&mut Self::Transaction>,
249 bumped_id: u32,
250 fee_payer_txid: Txid,
251 vout: u32,
252 amount: Amount,
253 replacement_of_id: Option<u32>,
254 ) -> Result<(), BridgeError>;
255
256 /// Get the last RBF transaction ID for a specific send attempt.
257 async fn get_last_rbf_txid(
258 &self,
259 dbtx: Option<&mut Self::Transaction>,
260 id: u32,
261 ) -> Result<Option<Txid>, BridgeError>;
262
263 /// Save a new RBF transaction ID.
264 async fn save_rbf_txid(
265 &self,
266 dbtx: Option<&mut Self::Transaction>,
267 id: u32,
268 txid: Txid,
269 ) -> Result<(), BridgeError>;
270
271 /// Save a cancelled outpoint activation condition.
272 async fn save_cancelled_outpoint(
273 &self,
274 dbtx: Option<&mut Self::Transaction>,
275 cancelled_id: u32,
276 outpoint: OutPoint,
277 ) -> Result<(), BridgeError>;
278
279 /// Save a cancelled transaction ID activation condition.
280 async fn save_cancelled_txid(
281 &self,
282 dbtx: Option<&mut Self::Transaction>,
283 cancelled_id: u32,
284 txid: Txid,
285 ) -> Result<(), BridgeError>;
286
287 /// Save an activated transaction ID condition.
288 async fn save_activated_txid(
289 &self,
290 dbtx: Option<&mut Self::Transaction>,
291 activated_id: u32,
292 prerequisite_tx: &ActivatedWithTxid,
293 ) -> Result<(), BridgeError>;
294
295 /// Save an activated outpoint condition.
296 async fn save_activated_outpoint(
297 &self,
298 dbtx: Option<&mut Self::Transaction>,
299 activated_id: u32,
300 activated_outpoint: &ActivatedWithOutpoint,
301 ) -> Result<(), BridgeError>;
302
303 /// Update the effective fee rate of a transaction.
304 async fn update_effective_fee_rate(
305 &self,
306 dbtx: Option<&mut Self::Transaction>,
307 id: u32,
308 effective_fee_rate: FeeRate,
309 current_tip_height: u32,
310 ) -> Result<(), BridgeError>;
311
312 /// Check if a transaction already exists in the transaction sender queue.
313 async fn check_if_tx_exists_on_txsender(
314 &self,
315 dbtx: Option<&mut Self::Transaction>,
316 txid: Txid,
317 ) -> Result<Option<u32>, BridgeError>;
318
319 /// Save a transaction to the sending queue.
320 async fn save_tx(
321 &self,
322 dbtx: Option<&mut Self::Transaction>,
323 tx_metadata: Option<TxMetadata>,
324 tx: &Transaction,
325 fee_paying_type: FeePayingType,
326 txid: Txid,
327 rbf_signing_info: Option<RbfSigningInfo>,
328 ) -> Result<u32, BridgeError>;
329
330 /// Returns debug information for a transaction.
331 async fn get_tx_debug_info(
332 &self,
333 dbtx: Option<&mut Self::Transaction>,
334 id: u32,
335 ) -> Result<Option<String>, BridgeError>;
336
337 /// Returns submission errors for a transaction.
338 async fn get_tx_debug_submission_errors(
339 &self,
340 dbtx: Option<&mut Self::Transaction>,
341 id: u32,
342 ) -> Result<Vec<(String, String)>, BridgeError>;
343
344 /// Returns fee payer UTXOs for an attempt.
345 async fn get_tx_debug_fee_payer_utxos(
346 &self,
347 dbtx: Option<&mut Self::Transaction>,
348 id: u32,
349 ) -> Result<Vec<(Txid, u32, Amount, bool)>, BridgeError>;
350
351 /// Debug method to log information about inactive transactions.
352 async fn debug_inactive_txs(&self, _fee_rate: FeeRate, _current_tip_height: u32) {}
353
354 /// Fetch the next event from the Bitcoin syncer.
355 async fn fetch_next_bitcoin_syncer_evt(
356 &self,
357 dbtx: &mut Self::Transaction,
358 consumer_id: &str,
359 ) -> Result<Option<BitcoinSyncerEvent>, BridgeError>;
360
361 /// Synchronize transaction confirmations based on canonical block status.
362 /// Confirms transactions in canonical blocks and unconfirms transactions in
363 /// non-canonical blocks.
364 async fn sync_transaction_confirmations(
365 &self,
366 dbtx: Option<&mut Self::Transaction>,
367 ) -> Result<(), BridgeError>;
368
369 /// Get the maximum height of canonical blocks in the database.
370 async fn get_max_height(
371 &self,
372 dbtx: Option<&mut Self::Transaction>,
373 ) -> Result<Option<u32>, BridgeError>;
374
375 /// Get the effective fee rate of a transaction and the block height when it was set.
376 /// Returns (None, None) if no effective fee rate has been set yet.
377 async fn get_effective_fee_rate(
378 &self,
379 dbtx: Option<&mut Self::Transaction>,
380 id: u32,
381 ) -> Result<(Option<FeeRate>, Option<u32>), BridgeError>;
382}
383
384#[derive(Clone, Debug)]
385pub struct MempoolConfig {
386 pub host: Option<String>,
387 pub endpoint: Option<String>,
388}
389
390/// Manages the process of sending Bitcoin transactions, including handling fee bumping
391/// strategies like Replace-By-Fee (RBF) and Child-Pays-For-Parent (CPFP).
392///
393/// It interacts with a Bitcoin Core RPC endpoint (`ExtendedBitcoinRpc`) to query network state
394/// (like fee rates) and submit transactions. It uses a `Database` to persist transaction
395/// state, track confirmation status, and manage associated data like fee payer UTXOs.
396/// The `Actor` provides signing capabilities for transactions controlled by this service.
397///
398/// The `TxSenderTxBuilder` type parameter provides static methods for transaction building
399/// capabilities for CPFP child transactions, using `SpendableTxIn` and `TxHandler`.
400#[derive(Clone)]
401pub struct TxSender<S, D, B>
402where
403 S: TxSenderSigner + 'static,
404 D: TxSenderDatabase + 'static,
405 B: TxSenderTxBuilder + 'static,
406{
407 pub signer: S,
408 pub rpc: clementine_extended_rpc::ExtendedBitcoinRpc,
409 pub db: D,
410 pub protocol_paramset: &'static ProtocolParamset,
411 pub tx_sender_limits: TxSenderLimits,
412 pub http_client: reqwest::Client,
413 mempool_config: MempoolConfig,
414 /// Phantom data to track the TxBuilder type.
415 /// B provides static methods for transaction building.
416 _tx_builder: std::marker::PhantomData<B>,
417}
418
419impl<S, D, B> std::fmt::Debug for TxSender<S, D, B>
420where
421 S: TxSenderSigner + std::fmt::Debug + 'static,
422 D: TxSenderDatabase + std::fmt::Debug + 'static,
423 B: TxSenderTxBuilder + 'static,
424{
425 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
426 f.debug_struct("TxSender")
427 .field("signer", &self.signer)
428 .field("db", &self.db)
429 .field("protocol_paramset", &self.protocol_paramset)
430 .field("tx_sender_limits", &self.tx_sender_limits)
431 .finish()
432 }
433}
434
435impl<S, D, B> TxSender<S, D, B>
436where
437 S: TxSenderSigner + 'static,
438 D: TxSenderDatabase + 'static,
439 B: TxSenderTxBuilder + 'static,
440{
441 /// Creates a new TxSender.
442 ///
443 /// The type parameter `B` provides static methods for CPFP child transaction creation
444 /// using SpendableTxIn and TxHandler from the core builder module.
445 pub fn new(
446 signer: S,
447 rpc: clementine_extended_rpc::ExtendedBitcoinRpc,
448 db: D,
449 protocol_paramset: &'static ProtocolParamset,
450 tx_sender_limits: TxSenderLimits,
451 mempool_config: MempoolConfig,
452 ) -> Self {
453 Self {
454 signer,
455 rpc,
456 db,
457 protocol_paramset,
458 tx_sender_limits,
459 http_client: reqwest::Client::new(),
460 mempool_config,
461 _tx_builder: std::marker::PhantomData,
462 }
463 }
464
465 pub async fn get_fee_rate(&self) -> Result<FeeRate, BridgeError> {
466 self.rpc
467 .get_fee_rate(
468 self.protocol_paramset.network,
469 &self.mempool_config.host,
470 &self.mempool_config.endpoint,
471 self.tx_sender_limits.mempool_fee_rate_multiplier,
472 self.tx_sender_limits.mempool_fee_rate_offset_sat_kvb,
473 self.tx_sender_limits.fee_rate_hard_cap,
474 )
475 .await
476 .map_err(|e| BridgeError::Eyre(e.into()))
477 }
478
479 /// Calculates the total fee required for a transaction package based on the fee bumping strategy.
480 ///
481 /// # Arguments
482 /// * `parent_tx_weight` - The weight of the main transaction being bumped.
483 /// * `num_fee_payer_utxos` - The number of fee payer UTXOs used (relevant for child tx size in CPFP).
484 /// * `fee_rate` - The target fee rate (sat/kwu or similar).
485 /// * `fee_paying_type` - The strategy being used (CPFP or RBF).
486 ///
487 /// # Calculation Logic
488 /// * **CPFP:** Calculates the weight of the hypothetical child transaction based on the
489 /// number of fee payer inputs and standard P2TR output sizes. It then calculates the
490 /// fee based on the *combined virtual size* (vbytes) of the parent and child transactions,
491 /// as miners evaluate the package deal.
492 /// * **RBF:** Calculates the weight of the replacement transaction itself (assuming inputs
493 /// and potentially outputs change slightly). The fee is calculated based on the weight
494 /// of this single replacement transaction.
495 ///
496 /// Reference for weight estimates: <https://bitcoin.stackexchange.com/a/116959>
497 fn calculate_required_fee(
498 parent_tx_weight: Weight,
499 num_fee_payer_utxos: usize,
500 fee_rate: FeeRate,
501 fee_paying_type: FeePayingType,
502 ) -> Result<Amount> {
503 tracing::info!(
504 "Calculating required fee for {} fee payer utxos",
505 num_fee_payer_utxos
506 );
507 // Estimate the weight of the child transaction (for CPFP) or the RBF replacement.
508 // P2TR input witness adds ~57.5vbytes (230 WU). P2TR output adds 43 vbytes (172 WU).
509 // Base transaction overhead (version, locktime, input/output counts) ~ 10.5 vBytes (42 WU)
510 // Anchor input marker (OP_FALSE OP_RETURN ..) adds overhead. Exact WU TBD.
511 // For CPFP child: (N fee payer inputs) + (1 anchor input) + (1 change output)
512 // For RBF replacement: (N fee payer inputs) + (1 change output) - assuming it replaces a tx with an anchor.
513 let child_tx_weight = match fee_paying_type {
514 // CPFP Child: N fee payer inputs + 1 anchor input + 1 change output + base overhead.
515 // Approx WU: (230 * num_fee_payer_utxos) + 230 + 172 + base_overhead_wu
516 // Simplified calculation used here needs verification.
517 FeePayingType::CPFP => Weight::from_wu_usize(230 * num_fee_payer_utxos + 207 + 172),
518 // RBF Replacement: N fee payer inputs + 1 change output + base overhead.
519 // Assumes it replaces a tx of similar structure but potentially different inputs/fees.
520 // Simplified calculation used here needs verification.
521 FeePayingType::RBF => Weight::from_wu_usize(230 * num_fee_payer_utxos + 172),
522 FeePayingType::NoFunding => Weight::from_wu_usize(0),
523 };
524
525 // Calculate total weight for fee calculation.
526 // For CPFP, miners consider the effective fee rate over the combined *vbytes* of parent + child.
527 // For RBF, miners consider the fee rate of the single replacement transaction's weight.
528 let total_weight = match fee_paying_type {
529 FeePayingType::CPFP => Weight::from_vb_unchecked(
530 child_tx_weight.to_vbytes_ceil() + parent_tx_weight.to_vbytes_ceil(),
531 ),
532 FeePayingType::RBF => child_tx_weight + parent_tx_weight, // Should likely just be the RBF tx weight? Check RBF rules.
533 FeePayingType::NoFunding => parent_tx_weight,
534 };
535
536 fee_rate
537 .checked_mul_by_weight(total_weight)
538 .ok_or_eyre("Fee calculation overflow")
539 .map_err(Into::into)
540 }
541
542 pub fn is_p2a_anchor(&self, output: &bitcoin::TxOut) -> bool {
543 clementine_utils::address::is_p2a_anchor(output)
544 }
545
546 pub fn find_p2a_vout(&self, tx: &Transaction) -> Result<usize, BridgeError> {
547 tx.output
548 .iter()
549 .position(|output| self.is_p2a_anchor(output))
550 .ok_or_eyre("P2A anchor output not found in transaction")
551 .map_err(BridgeError::Eyre)
552 }
553
554 /// Fetches transactions that are eligible to be sent or bumped from
555 /// database based on the given fee rate and tip height. Then, places a send
556 /// transaction request to the Bitcoin based on the fee strategy.
557 ///
558 /// For each eligible transaction (`id`):
559 ///
560 /// 1. **Send/Bump Main Tx:** Calls `send_tx` to either perform RBF or CPFP on the main
561 /// transaction (`id`) using the `new_fee_rate`.
562 /// 2. **Handle Errors:**
563 /// - [`SendTxError::UnconfirmedFeePayerUTXOsLeft`]: Skips the current tx, waiting for fee
564 /// payers to confirm.
565 /// - [`SendTxError::InsufficientFeePayerAmount`]: Calls `create_fee_payer_utxo` to
566 /// provision more funds for a future CPFP attempt.
567 /// - Other errors are logged.
568 ///
569 /// # Arguments
570 /// * `new_fee_rate` - The current target fee rate based on network conditions.
571 /// * `current_tip_height` - The current blockchain height, used for time-lock checks.
572 /// * `is_tip_height_increased` - True if the tip height has increased since the last time we sent unconfirmed transactions.
573 #[tracing::instrument(skip_all, fields(new_fee_rate, current_tip_height))]
574 async fn try_to_send_unconfirmed_txs(
575 &self,
576 new_fee_rate: FeeRate,
577 current_tip_height: u32,
578 is_tip_height_increased: bool,
579 ) -> Result<()> {
580 // get_sendable_txs doesn't return txs that we already sent in the past with >= fee rate to the current fee rate
581 // but if we have a new block height, but the tx is still not confirmed, we want to send it again anyway in case
582 // some error occurred on our bitcoin rpc/our tx got evicted from mempool somehow (for ex: if a fee payer of cpfp tx was reorged,
583 // cpfp tx will get evicted as v3 cpfp cannot have unconfirmed ancestors)
584 // if block height is increased, we use a dummy high fee rate to get all sendable txs
585 let get_sendable_txs_fee_rate = if is_tip_height_increased {
586 FeeRate::from_sat_per_kwu(u32::MAX as u64)
587 } else {
588 new_fee_rate
589 };
590 let txs = self
591 .db
592 .get_sendable_txs(get_sendable_txs_fee_rate, current_tip_height)
593 .await
594 .map_to_eyre()?;
595
596 // bump fees of fee payer transactions that are unconfirmed
597 self.bump_fees_of_unconfirmed_fee_payer_txs(new_fee_rate)
598 .await?;
599
600 if !txs.is_empty() {
601 tracing::debug!("Trying to send {} sendable txs ", txs.len());
602 }
603
604 if std::env::var("TXSENDER_DBG_INACTIVE_TXS").is_ok() {
605 self.db
606 .debug_inactive_txs(get_sendable_txs_fee_rate, current_tip_height)
607 .await;
608 }
609
610 for id in txs {
611 // Update debug state
612 tracing::debug!(
613 try_to_send_id = id,
614 "Processing TX in try_to_send_unconfirmed_txs with fee rate {new_fee_rate}",
615 );
616
617 let (tx_metadata, tx, fee_paying_type, seen_block_id, rbf_signing_info) =
618 match self.db.get_try_to_send_tx(None, id).await {
619 Ok(res) => res,
620 Err(e) => {
621 log_error_for_tx!(self.db, id, format!("Failed to get tx details: {}", e));
622 continue;
623 }
624 };
625
626 // Check if the transaction is already confirmed (only happens if it was confirmed after this loop started)
627 if let Some(block_id) = seen_block_id {
628 tracing::debug!(
629 try_to_send_id = id,
630 "Transaction already confirmed in block with block id of {}",
631 block_id
632 );
633
634 // Update sending state
635 let _ = self
636 .db
637 .update_tx_debug_sending_state(id, "confirmed", true)
638 .await;
639
640 continue;
641 }
642
643 // Get effective fee rate and block height to calculate adjusted fee rate
644 let (previous_effective_fee_rate, last_bump_block_height) =
645 match self.db.get_effective_fee_rate(None, id).await {
646 Ok(res) => res,
647 Err(e) => {
648 log_error_for_tx!(
649 self.db,
650 id,
651 format!("Failed to get effective fee rate: {}", e)
652 );
653 continue;
654 }
655 };
656
657 // Calculate adjusted fee rate considering:
658 // 1. If new_fee_rate > previous_effective_fee_rate, use max(new_fee_rate, previous_effective_fee_rate + incremental_fee_rate)
659 // 2. If tx has been stuck for 10+ blocks, bump with incremental fee
660 let adjusted_fee_rate = match self
661 .calculate_target_fee_rate(
662 previous_effective_fee_rate,
663 new_fee_rate,
664 last_bump_block_height,
665 current_tip_height,
666 )
667 .await
668 {
669 Ok(rate) => rate,
670 Err(e) => {
671 log_error_for_tx!(
672 self.db,
673 id,
674 format!("Failed to calculate adjusted fee rate: {}", e)
675 );
676 continue;
677 }
678 };
679
680 let result = match fee_paying_type {
681 // Send nonstandard transactions to testnet4 using the mempool.space accelerator.
682 // As mempool uses out of band payment, we don't need to do cpfp or rbf.
683 _ if self.protocol_paramset.network == bitcoin::Network::Testnet4
684 && self.is_bridge_tx_nonstandard(&tx) =>
685 {
686 self.send_testnet4_nonstandard_tx(&tx, id).await
687 }
688 FeePayingType::CPFP => {
689 self.send_cpfp_tx(id, tx, tx_metadata, adjusted_fee_rate, current_tip_height)
690 .await
691 }
692 FeePayingType::RBF => {
693 self.send_rbf_tx(
694 id,
695 tx,
696 tx_metadata,
697 adjusted_fee_rate,
698 rbf_signing_info,
699 current_tip_height,
700 )
701 .await
702 }
703 FeePayingType::NoFunding => self.send_no_funding_tx(id, tx, tx_metadata).await,
704 };
705
706 if let Err(e) = result {
707 log_error_for_tx!(self.db, id, format!("Failed to send tx: {:?}", e));
708 }
709 }
710
711 Ok(())
712 }
713 pub fn client(&self) -> TxSenderClient<D> {
714 TxSenderClient::new(self.db.clone())
715 }
716
717 /// Calculates the effective fee rate for a transaction, considering previous effective fee rate
718 /// and minimum incremental fee requirements.
719 ///
720 /// This function implements the logic for fee bumping that ensures:
721 /// 1. If no previous effective fee rate exists, use the new fee rate
722 /// 2. If previous effective fee rate exists, use the maximum of:
723 /// - The new fee rate
724 /// - Previous effective fee rate + minimum incremental fee rate
725 ///
726 /// # Arguments
727 /// * `previous_effective_fee_rate` - The previous effective fee rate (if any)
728 /// * `new_fee_rate` - The target fee rate for the new attempt
729 /// * `last_bump_block_height` - The block height when the last fee bump was done (if any)
730 /// * `current_tip_height` - The current blockchain tip height
731 ///
732 /// # Returns
733 /// The effective fee rate to use (in sat/kwu), capped by the hard cap from config
734 pub async fn calculate_target_fee_rate(
735 &self,
736 previous_effective_fee_rate: Option<FeeRate>,
737 new_fee_rate: FeeRate,
738 last_bump_block_height: Option<u32>,
739 current_tip_height: u32,
740 ) -> Result<FeeRate> {
741 // Hard cap from config (in sat/vB), convert to sat/kwu
742 let hard_cap = FeeRate::from_sat_per_vb(self.tx_sender_limits.fee_rate_hard_cap)
743 .expect("fee_rate_hard_cap should be valid");
744
745 let Some(previous_rate) = previous_effective_fee_rate else {
746 // No previous effective fee rate, use the new fee rate (capped)
747 return Ok(std::cmp::min(new_fee_rate, hard_cap));
748 };
749
750 // Check if the tx has been stuck for 10+ blocks
751 let is_stuck = match last_bump_block_height {
752 Some(block_height) => {
753 current_tip_height.saturating_sub(block_height)
754 >= self.tx_sender_limits.fee_bump_after_blocks
755 }
756 None => false,
757 };
758
759 // Get minimum fee increment rate from node for BIP125 compliance. Returned value is in BTC/kvB
760 let incremental_fee_rate = self
761 .rpc
762 .get_network_info()
763 .await
764 .map_err(|e| eyre::eyre!(e))?
765 .incremental_fee;
766 let incremental_fee_rate_sat_per_kvb = incremental_fee_rate.to_sat();
767 let incremental_fee_rate =
768 FeeRate::from_sat_per_kwu(incremental_fee_rate_sat_per_kvb.div_ceil(4));
769
770 // Minimum bump fee rate required by BIP125
771 let min_bump_feerate =
772 previous_rate.to_sat_per_kwu() + incremental_fee_rate.to_sat_per_kwu();
773
774 // If new fee rate is higher than previous, use max of new_fee_rate and min_bump_feerate
775 if new_fee_rate.to_sat_per_kwu() > previous_rate.to_sat_per_kwu() {
776 let effective_feerate = std::cmp::max(new_fee_rate.to_sat_per_kwu(), min_bump_feerate);
777 let result = FeeRate::from_sat_per_kwu(effective_feerate);
778 return Ok(std::cmp::min(result, hard_cap));
779 }
780
781 // If the tx is stuck for 10+ blocks, force a fee bump (previous + incremental)
782 if is_stuck {
783 let result = FeeRate::from_sat_per_kwu(min_bump_feerate);
784 let capped_result = std::cmp::min(result, hard_cap);
785
786 tracing::debug!(
787 "TX stuck for at least {} blocks, forcing fee bump from {} to {} sat/kwu (hard cap: {} sat/kwu)",
788 self.tx_sender_limits.fee_bump_after_blocks,
789 previous_rate.to_sat_per_kwu(),
790 capped_result.to_sat_per_kwu(),
791 hard_cap.to_sat_per_kwu()
792 );
793
794 return Ok(capped_result);
795 }
796
797 // Neither higher fee rate nor stuck, use previous rate (no change needed)
798 Ok(previous_rate)
799 }
800
801 /// Sends a transaction that is already fully funded and signed.
802 ///
803 /// This function is used for transactions that do not require fee bumping strategies
804 /// like RBF or CPFP. The transaction is submitted directly to the Bitcoin network
805 /// without any modifications.
806 ///
807 /// # Arguments
808 /// * `try_to_send_id` - The database ID tracking this send attempt.
809 /// * `tx` - The fully funded and signed transaction ready for broadcast.
810 /// * `tx_metadata` - Optional metadata associated with the transaction for debugging.
811 ///
812 /// # Behavior
813 /// 1. Attempts to broadcast the transaction using `send_raw_transaction` RPC.
814 /// 2. Updates the database with success/failure state for debugging purposes.
815 /// 3. Logs appropriate messages for monitoring and troubleshooting.
816 ///
817 /// # Returns
818 /// * `Ok(())` - If the transaction was successfully broadcast.
819 /// * `Err(SendTxError)` - If the broadcast failed.
820 #[tracing::instrument(skip_all, fields(try_to_send_id, tx_meta=?tx_metadata))]
821 pub async fn send_no_funding_tx(
822 &self,
823 try_to_send_id: u32,
824 tx: Transaction,
825 tx_metadata: Option<TxMetadata>,
826 ) -> Result<()> {
827 match self.rpc.send_raw_transaction(&tx).await {
828 Ok(sent_txid) => {
829 tracing::debug!(
830 try_to_send_id,
831 "Successfully sent no funding tx with txid {}",
832 sent_txid
833 );
834 let _ = self
835 .db
836 .update_tx_debug_sending_state(try_to_send_id, "no_funding_send_success", true)
837 .await;
838 }
839 Err(e) => {
840 tracing::error!(
841 "Failed to send no funding tx with try_to_send_id: {try_to_send_id:?} and metadata: {tx_metadata:?}"
842 );
843 let err_msg = format!("send_raw_transaction error for no funding tx: {e}");
844 log_error_for_tx!(self.db, try_to_send_id, err_msg);
845 let _ = self
846 .db
847 .update_tx_debug_sending_state(try_to_send_id, "no_funding_send_failed", true)
848 .await;
849 return Err(SendTxError::Other(eyre::eyre!(e)));
850 }
851 };
852
853 Ok(())
854 }
855}