1use crate::TxSenderDatabase;
7use crate::{ActivatedWithOutpoint, ActivatedWithTxid};
8use bitcoin::{OutPoint, Transaction, Txid};
9use clementine_config::protocol::ProtocolParamset;
10use clementine_errors::BridgeError;
11use clementine_primitives::{TransactionType, UtxoVout};
12use clementine_utils::{FeePayingType, RbfSigningInfo, TxMetadata};
13use eyre::eyre;
14use std::collections::BTreeMap;
15
16#[derive(Debug, Clone)]
17pub struct TxSenderClient<D>
18where
19 D: TxSenderDatabase,
20{
21 pub db: D,
22}
23
24impl<D> TxSenderClient<D>
25where
26 D: TxSenderDatabase,
27{
28 pub fn new(db: D) -> Self {
29 Self { db }
30 }
31
32 #[tracing::instrument(err(level = tracing::Level::ERROR), ret(level = tracing::Level::TRACE), skip_all, fields(?tx_metadata))]
62 #[allow(clippy::too_many_arguments)]
63 pub async fn insert_try_to_send(
64 &self,
65 mut dbtx: Option<&mut D::Transaction>,
66 tx_metadata: Option<TxMetadata>,
67 signed_tx: &Transaction,
68 fee_paying_type: FeePayingType,
69 rbf_signing_info: Option<RbfSigningInfo>,
70 cancel_outpoints: &[OutPoint],
71 cancel_txids: &[Txid],
72 activate_txids: &[ActivatedWithTxid],
73 activate_outpoints: &[ActivatedWithOutpoint],
74 ) -> Result<u32, BridgeError> {
75 let txid = signed_tx.compute_txid();
76
77 let tx_exists = self
79 .db
80 .check_if_tx_exists_on_txsender(dbtx.as_deref_mut(), txid)
81 .await?;
82 if let Some(try_to_send_id) = tx_exists {
83 return Ok(try_to_send_id);
84 }
85
86 tracing::info!(
87 "Added tx {} with txid {} to the queue",
88 tx_metadata
89 .as_ref()
90 .map(|data| format!("{:?}", data.tx_type))
91 .unwrap_or("N/A".to_string()),
92 txid
93 );
94
95 let try_to_send_id = self
96 .db
97 .save_tx(
98 dbtx.as_deref_mut(),
99 tx_metadata,
100 signed_tx,
101 fee_paying_type,
102 txid,
103 rbf_signing_info,
104 )
105 .await?;
106
107 #[cfg(test)]
109 tracing::debug!(target: "ci", "Saved tx to database with try_to_send_id: {try_to_send_id}, metadata: {tx_metadata:?}, raw tx: {}", hex::encode(bitcoin::consensus::serialize(signed_tx)));
110
111 for input_outpoint in signed_tx.input.iter().map(|input| input.previous_output) {
112 self.db
113 .save_cancelled_outpoint(dbtx.as_deref_mut(), try_to_send_id, input_outpoint)
114 .await?;
115 }
116
117 for outpoint in cancel_outpoints {
118 self.db
119 .save_cancelled_outpoint(dbtx.as_deref_mut(), try_to_send_id, *outpoint)
120 .await?;
121 }
122
123 for txid in cancel_txids {
124 self.db
125 .save_cancelled_txid(dbtx.as_deref_mut(), try_to_send_id, *txid)
126 .await?;
127 }
128
129 let mut max_timelock_of_activated_txids = BTreeMap::new();
130
131 for activated_txid in activate_txids {
132 let timelock = max_timelock_of_activated_txids
133 .entry(activated_txid.txid)
134 .or_insert(activated_txid.relative_block_height);
135 if *timelock < activated_txid.relative_block_height {
136 *timelock = activated_txid.relative_block_height;
137 }
138 }
139
140 for input in signed_tx.input.iter() {
141 let relative_block_height = if input.sequence.is_relative_lock_time() {
142 let relative_locktime = input
143 .sequence
144 .to_relative_lock_time()
145 .expect("Invalid relative locktime");
146 match relative_locktime {
147 bitcoin::relative::LockTime::Blocks(height) => height.value() as u32,
148 _ => {
149 return Err(BridgeError::Eyre(eyre!("Invalid relative locktime")));
150 }
151 }
152 } else {
153 0
154 };
155 let timelock = max_timelock_of_activated_txids
156 .entry(input.previous_output.txid)
157 .or_insert(relative_block_height);
158 if *timelock < relative_block_height {
159 *timelock = relative_block_height;
160 }
161 }
162
163 for (txid, timelock) in max_timelock_of_activated_txids {
164 self.db
165 .save_activated_txid(
166 dbtx.as_deref_mut(),
167 try_to_send_id,
168 &ActivatedWithTxid {
169 txid,
170 relative_block_height: timelock,
171 },
172 )
173 .await?;
174 }
175
176 for activated_outpoint in activate_outpoints {
177 self.db
178 .save_activated_outpoint(dbtx.as_deref_mut(), try_to_send_id, activated_outpoint)
179 .await?;
180 }
181
182 Ok(try_to_send_id)
183 }
184
185 #[allow(clippy::too_many_arguments)]
209 pub async fn add_tx_to_queue(
210 &self,
211 dbtx: Option<&mut D::Transaction>,
212 tx_type: TransactionType,
213 signed_tx: &Transaction,
214 related_txs: &[(TransactionType, Transaction)],
215 tx_metadata: Option<TxMetadata>,
216 protocol_paramset: &ProtocolParamset,
217 rbf_info: Option<RbfSigningInfo>,
218 ) -> Result<u32, BridgeError> {
219 let tx_metadata = tx_metadata.map(|mut data| {
220 data.tx_type = tx_type;
221 data
222 });
223 match tx_type {
224 TransactionType::Kickoff
225 | TransactionType::Dummy
226 | TransactionType::ChallengeTimeout
227 | TransactionType::DisproveTimeout
228 | TransactionType::Reimburse
229 | TransactionType::Round
230 | TransactionType::OperatorChallengeNack(_)
231 | TransactionType::UnspentKickoff(_)
232 | TransactionType::MoveToVault
233 | TransactionType::BurnUnusedKickoffConnectors
234 | TransactionType::KickoffNotFinalized
235 | TransactionType::MiniAssert(_)
236 | TransactionType::LatestBlockhashTimeout
237 | TransactionType::LatestBlockhash
238 | TransactionType::EmergencyStop
239 | TransactionType::OptimisticPayout
240 | TransactionType::ReadyToReimburse
241 | TransactionType::ReplacementDeposit
242 | TransactionType::WatchtowerChallenge(_)
243 | TransactionType::AssertTimeout(_) => {
244 self.insert_try_to_send(
246 dbtx,
247 tx_metadata,
248 signed_tx,
249 FeePayingType::CPFP,
250 rbf_info,
251 &[],
252 &[],
253 &[],
254 &[],
255 )
256 .await
257 }
258 TransactionType::Challenge | TransactionType::Payout => {
259 self.insert_try_to_send(
260 dbtx,
261 tx_metadata,
262 signed_tx,
263 FeePayingType::RBF,
264 rbf_info,
265 &[],
266 &[],
267 &[],
268 &[],
269 )
270 .await
271 }
272 TransactionType::WatchtowerChallengeTimeout(_) => {
273 let kickoff_txid = related_txs
277 .iter()
278 .find_map(|(tx_type, tx)| {
279 if let TransactionType::Kickoff = tx_type {
280 Some(tx.compute_txid())
281 } else {
282 None
283 }
284 })
285 .ok_or(BridgeError::Eyre(eyre!(
286 "Couldn't find kickoff tx in related_txs"
287 )))?;
288 self.insert_try_to_send(
289 dbtx,
290 tx_metadata,
291 signed_tx,
292 FeePayingType::CPFP,
293 rbf_info,
294 &[OutPoint {
295 txid: kickoff_txid,
296 vout: UtxoVout::KickoffFinalizer.get_vout(),
297 }],
298 &[],
299 &[],
300 &[],
301 )
302 .await
303 }
304 TransactionType::OperatorChallengeAck(watchtower_idx) => {
305 let kickoff_txid = related_txs
306 .iter()
307 .find_map(|(tx_type, tx)| {
308 if let TransactionType::Kickoff = tx_type {
309 Some(tx.compute_txid())
310 } else {
311 None
312 }
313 })
314 .ok_or(BridgeError::Eyre(eyre!(
315 "Couldn't find kickoff tx in related_txs"
316 )))?;
317 self.insert_try_to_send(
318 dbtx,
319 tx_metadata,
320 signed_tx,
321 FeePayingType::CPFP,
322 rbf_info,
323 &[],
324 &[],
325 &[],
326 &[ActivatedWithOutpoint {
327 outpoint: OutPoint {
329 txid: kickoff_txid,
330 vout: UtxoVout::WatchtowerChallenge(watchtower_idx).get_vout(),
331 },
332 relative_block_height: protocol_paramset.finality_depth - 1,
333 }],
334 )
335 .await
336 }
337 TransactionType::Disprove => {
338 self.insert_try_to_send(
339 dbtx,
340 tx_metadata,
341 signed_tx,
342 FeePayingType::NoFunding,
343 rbf_info,
344 &[],
345 &[],
346 &[],
347 &[],
348 )
349 .await
350 }
351 TransactionType::AllNeededForDeposit | TransactionType::YieldKickoffTxid => {
352 unreachable!("Higher level transaction types used for yielding kickoff txid from sighash stream or denoting all txs should not be added to the queue");
353 }
354 }
355 }
356}