clementine_core/
tx_sender_queue.rs

1//! Core-specific txsender queue helpers.
2//!
3//! `clementine-tx-sender` intentionally exposes only a low-level, transactional
4//! `insert_try_to_send` API. The higher-level mapping from `TransactionType`
5//! to cancellation/activation semantics is Clementine-core specific and lives here.
6
7use bitcoin::{OutPoint, Transaction};
8use clementine_config::protocol::ProtocolParamset;
9use clementine_errors::BridgeError;
10use clementine_primitives::{TransactionType, UtxoVout};
11use clementine_utils::{FeePayingType, RbfSigningInfo, TxMetadata};
12use eyre::eyre;
13
14use crate::tx_sender::{ActivatedWithOutpoint, TxSenderClient, TxSenderTransaction};
15
16#[tonic::async_trait]
17pub trait TxSenderClientQueueExt {
18    /// Adds a transaction to the txsender sending queue based on core transaction semantics.
19    ///
20    /// This function is a core-level wrapper around [`TxSenderClient::insert_try_to_send`].
21    /// It determines the appropriate [`FeePayingType`] and any cancellation/activation
22    /// dependencies based on the [`TransactionType`].
23    ///
24    /// IMPORTANT: `insert_try_to_send` is transactional. This helper requires an active
25    /// DB transaction and will not partially insert state.
26    #[allow(clippy::too_many_arguments)]
27    async fn add_tx_to_queue(
28        &self,
29        dbtx: &mut TxSenderTransaction,
30        tx_type: TransactionType,
31        signed_tx: &Transaction,
32        related_txs: &[(TransactionType, Transaction)],
33        tx_metadata: Option<TxMetadata>,
34        protocol_paramset: &ProtocolParamset,
35        rbf_info: Option<RbfSigningInfo>,
36    ) -> Result<u32, BridgeError>;
37}
38
39#[tonic::async_trait]
40impl TxSenderClientQueueExt for TxSenderClient {
41    #[allow(clippy::too_many_arguments)]
42    async fn add_tx_to_queue(
43        &self,
44        dbtx: &mut TxSenderTransaction,
45        tx_type: TransactionType,
46        signed_tx: &Transaction,
47        related_txs: &[(TransactionType, Transaction)],
48        tx_metadata: Option<TxMetadata>,
49        protocol_paramset: &ProtocolParamset,
50        rbf_info: Option<RbfSigningInfo>,
51    ) -> Result<u32, BridgeError> {
52        let tx_metadata = tx_metadata.map(|mut data| {
53            data.tx_type = tx_type;
54            data
55        });
56
57        match tx_type {
58            TransactionType::Kickoff
59            | TransactionType::Dummy
60            | TransactionType::ChallengeTimeout
61            | TransactionType::DisproveTimeout
62            | TransactionType::Reimburse
63            | TransactionType::Round
64            | TransactionType::OperatorChallengeNack(_)
65            | TransactionType::UnspentKickoff(_)
66            | TransactionType::MoveToVault
67            | TransactionType::BurnUnusedKickoffConnectors
68            | TransactionType::KickoffNotFinalized
69            | TransactionType::MiniAssert(_)
70            | TransactionType::LatestBlockhashTimeout
71            | TransactionType::LatestBlockhash
72            | TransactionType::EmergencyStop
73            | TransactionType::OptimisticPayout
74            | TransactionType::ReadyToReimburse
75            | TransactionType::ReplacementDeposit
76            | TransactionType::WatchtowerChallenge(_)
77            | TransactionType::AssertTimeout(_) => {
78                // no_dependency and cpfp
79                self.insert_try_to_send(
80                    dbtx,
81                    tx_metadata,
82                    signed_tx,
83                    FeePayingType::CPFP,
84                    rbf_info,
85                    &[],
86                    &[],
87                    &[],
88                    &[],
89                )
90                .await
91            }
92            TransactionType::Challenge | TransactionType::Payout => {
93                self.insert_try_to_send(
94                    dbtx,
95                    tx_metadata,
96                    signed_tx,
97                    FeePayingType::RBF,
98                    rbf_info,
99                    &[],
100                    &[],
101                    &[],
102                    &[],
103                )
104                .await
105            }
106            TransactionType::WatchtowerChallengeTimeout(_) => {
107                // Do not send watchtower timeout if kickoff is already finalized
108                // which is done by adding kickoff finalizer utxo to cancel_outpoints
109                // this is not needed for any timeouts that spend the kickoff finalizer utxo like AssertTimeout.
110                let kickoff_txid = related_txs
111                    .iter()
112                    .find_map(|(t, tx)| {
113                        (matches!(t, TransactionType::Kickoff)).then(|| tx.compute_txid())
114                    })
115                    .ok_or(BridgeError::Eyre(eyre!(
116                        "Couldn't find kickoff tx in related_txs"
117                    )))?;
118
119                self.insert_try_to_send(
120                    dbtx,
121                    tx_metadata,
122                    signed_tx,
123                    FeePayingType::CPFP,
124                    rbf_info,
125                    &[OutPoint {
126                        txid: kickoff_txid,
127                        vout: UtxoVout::KickoffFinalizer.get_vout(),
128                    }],
129                    &[],
130                    &[],
131                    &[],
132                )
133                .await
134            }
135            TransactionType::OperatorChallengeAck(watchtower_idx) => {
136                let kickoff_txid = related_txs
137                    .iter()
138                    .find_map(|(t, tx)| {
139                        (matches!(t, TransactionType::Kickoff)).then(|| tx.compute_txid())
140                    })
141                    .ok_or(BridgeError::Eyre(eyre!(
142                        "Couldn't find kickoff tx in related_txs"
143                    )))?;
144
145                self.insert_try_to_send(
146                    dbtx,
147                    tx_metadata,
148                    signed_tx,
149                    FeePayingType::CPFP,
150                    rbf_info,
151                    &[],
152                    &[],
153                    &[],
154                    &[ActivatedWithOutpoint {
155                        // only send OperatorChallengeAck if corresponding watchtower challenge is sent
156                        outpoint: OutPoint {
157                            txid: kickoff_txid,
158                            vout: UtxoVout::WatchtowerChallenge(watchtower_idx).get_vout(),
159                        },
160                        relative_block_height: protocol_paramset.finality_depth - 1,
161                    }],
162                )
163                .await
164            }
165            TransactionType::Disprove => {
166                self.insert_try_to_send(
167                    dbtx,
168                    tx_metadata,
169                    signed_tx,
170                    FeePayingType::NoFunding,
171                    rbf_info,
172                    &[],
173                    &[],
174                    &[],
175                    &[],
176                )
177                .await
178            }
179            TransactionType::AllNeededForDeposit | TransactionType::YieldKickoffTxid => {
180                unreachable!(
181                    "Higher level transaction types should not be added to the txsender queue"
182                );
183            }
184        }
185    }
186}