1use std::ops::DerefMut;
6
7use super::{
8 wrapper::{BlockHashDB, TxidDB, XOnlyPublicKeyDB},
9 Database, DatabaseTransaction,
10};
11use crate::execute_query_with_tx;
12use bitcoin::{BlockHash, OutPoint, Txid, XOnlyPublicKey};
13use clementine_errors::BridgeError;
14use eyre::Context;
15use sqlx::QueryBuilder;
16
17impl Database {
18 pub async fn get_last_deposit_idx(
21 &self,
22 tx: Option<DatabaseTransaction<'_>>,
23 ) -> Result<Option<u32>, BridgeError> {
24 let query = sqlx::query_as::<_, (i32,)>("SELECT COALESCE(MAX(idx), -1) FROM withdrawals");
25 let result = execute_query_with_tx!(self.connection, tx, query, fetch_one)?;
26 if result.0 == -1 {
27 Ok(None)
28 } else {
29 Ok(Some(result.0 as u32))
30 }
31 }
32
33 pub async fn get_last_withdrawal_idx(
36 &self,
37 tx: Option<DatabaseTransaction<'_>>,
38 ) -> Result<Option<u32>, BridgeError> {
39 let query = sqlx::query_as::<_, (i32,)>(
40 "SELECT COALESCE(MAX(idx), -1) FROM withdrawals WHERE withdrawal_utxo_txid IS NOT NULL",
41 );
42 let result = execute_query_with_tx!(self.connection, tx, query, fetch_one)?;
43 if result.0 == -1 {
44 Ok(None)
45 } else {
46 Ok(Some(result.0 as u32))
47 }
48 }
49
50 pub async fn upsert_move_to_vault_txid_from_citrea_deposit(
51 &self,
52 tx: Option<DatabaseTransaction<'_>>,
53 citrea_idx: u32,
54 move_to_vault_txid: &Txid,
55 ) -> Result<(), BridgeError> {
56 let query = sqlx::query(
57 "INSERT INTO withdrawals (idx, move_to_vault_txid)
58 VALUES ($1, $2)
59 ON CONFLICT (idx) DO UPDATE
60 SET move_to_vault_txid = $2",
61 )
62 .bind(i32::try_from(citrea_idx).wrap_err("Failed to convert citrea index to i32")?)
63 .bind(TxidDB(*move_to_vault_txid));
64
65 execute_query_with_tx!(self.connection, tx, query, execute)?;
66 Ok(())
67 }
68
69 pub async fn get_move_to_vault_txid_from_citrea_deposit(
70 &self,
71 tx: Option<DatabaseTransaction<'_>>,
72 citrea_idx: u32,
73 ) -> Result<Option<Txid>, BridgeError> {
74 let query = sqlx::query_as::<_, (TxidDB,)>(
75 "SELECT move_to_vault_txid FROM withdrawals WHERE idx = $1",
76 )
77 .bind(i32::try_from(citrea_idx).wrap_err("Failed to convert citrea index to i32")?);
78
79 let result: Option<(TxidDB,)> =
80 execute_query_with_tx!(self.connection, tx, query, fetch_optional)?;
81
82 Ok(result.map(|(move_to_vault_txid,)| move_to_vault_txid.0))
83 }
84
85 pub async fn update_replacement_deposit_move_txid(
86 &self,
87 tx: DatabaseTransaction<'_>,
88 idx: u32,
89 new_move_txid: Txid,
90 ) -> Result<(), BridgeError> {
91 let query = sqlx::query(
92 "UPDATE withdrawals
93 SET move_to_vault_txid = $2
94 WHERE idx = $1
95 RETURNING idx",
96 )
97 .bind(i32::try_from(idx).wrap_err("Failed to convert idx to i32")?)
98 .bind(TxidDB(new_move_txid))
99 .fetch_optional(tx.deref_mut())
100 .await?;
101
102 if query.is_none() {
103 return Err(eyre::eyre!("Replacement move txid not found: {}", idx).into());
104 }
105 Ok(())
106 }
107
108 pub async fn update_withdrawal_utxo_from_citrea_withdrawal(
109 &self,
110 tx: Option<DatabaseTransaction<'_>>,
111 citrea_idx: u32,
112 withdrawal_utxo: OutPoint,
113 withdrawal_batch_proof_bitcoin_block_height: u32,
114 ) -> Result<(), BridgeError> {
115 let query = sqlx::query(
116 "UPDATE withdrawals
117 SET withdrawal_utxo_txid = $2,
118 withdrawal_utxo_vout = $3,
119 withdrawal_batch_proof_bitcoin_block_height = $4
120 WHERE idx = $1",
121 )
122 .bind(i32::try_from(citrea_idx).wrap_err("Failed to convert citrea index to i32")?)
123 .bind(TxidDB(withdrawal_utxo.txid))
124 .bind(
125 i32::try_from(withdrawal_utxo.vout)
126 .wrap_err("Failed to convert withdrawal utxo vout to i32")?,
127 )
128 .bind(
129 i32::try_from(withdrawal_batch_proof_bitcoin_block_height)
130 .wrap_err("Failed to convert withdrawal batch proof bitcoin block height to i32")?,
131 );
132
133 execute_query_with_tx!(self.connection, tx, query, execute)?;
134 Ok(())
135 }
136
137 pub async fn get_withdrawal_utxo_from_citrea_withdrawal(
140 &self,
141 tx: Option<DatabaseTransaction<'_>>,
142 citrea_idx: u32,
143 ) -> Result<OutPoint, BridgeError> {
144 let query = sqlx::query_as::<_, (Option<TxidDB>, Option<i32>)>(
145 "SELECT w.withdrawal_utxo_txid, w.withdrawal_utxo_vout
146 FROM withdrawals w
147 WHERE w.idx = $1",
148 )
149 .bind(i32::try_from(citrea_idx).wrap_err("Failed to convert citrea index to i32")?);
150
151 let results = execute_query_with_tx!(self.connection, tx, query, fetch_optional)?;
152
153 match results {
154 None => Err(eyre::eyre!("Deposit with id {} is not set", citrea_idx).into()),
155 Some((txid, vout)) => match (txid, vout) {
156 (Some(txid), Some(vout)) => Ok(OutPoint {
157 txid: txid.0,
158 vout: u32::try_from(vout)
159 .wrap_err("Failed to convert withdrawal utxo vout to u32")?,
160 }),
161 _ => {
162 Err(eyre::eyre!("Withdrawal utxo is not set for deposit {}", citrea_idx).into())
163 }
164 },
165 }
166 }
167
168 pub async fn get_payout_txs_for_withdrawal_utxos(
171 &self,
172 tx: Option<DatabaseTransaction<'_>>,
173 block_id: u32,
174 ) -> Result<Vec<(u32, Txid)>, BridgeError> {
175 let query = sqlx::query_as::<_, (i32, TxidDB)>(
176 "SELECT w.idx, bsu.spending_txid
177 FROM withdrawals w
178 JOIN bitcoin_syncer_spent_utxos bsu
179 ON bsu.txid = w.withdrawal_utxo_txid
180 AND bsu.vout = w.withdrawal_utxo_vout
181 WHERE bsu.block_id = $1",
182 )
183 .bind(i32::try_from(block_id).wrap_err("Failed to convert block id to i32")?);
184
185 let results = execute_query_with_tx!(self.connection, tx, query, fetch_all)?;
186
187 results
188 .into_iter()
189 .map(|(idx, txid)| {
190 Ok((
191 u32::try_from(idx).wrap_err("Failed to convert withdrawal index to u32")?,
192 txid.0,
193 ))
194 })
195 .collect()
196 }
197
198 pub async fn update_payout_txs_and_payer_operator_xonly_pk(
200 &self,
201 tx: Option<DatabaseTransaction<'_>>,
202 payout_txs_and_payer_operator_xonly_pk: Vec<(
203 u32,
204 Txid,
205 Option<XOnlyPublicKey>,
206 bitcoin::BlockHash,
207 )>,
208 ) -> Result<(), BridgeError> {
209 if payout_txs_and_payer_operator_xonly_pk.is_empty() {
210 return Ok(());
211 }
212 let converted_values: Result<Vec<_>, BridgeError> = payout_txs_and_payer_operator_xonly_pk
214 .iter()
215 .map(|(idx, txid, operator_xonly_pk, block_hash)| {
216 Ok((
217 i32::try_from(*idx).wrap_err("Failed to convert payout index to i32")?,
218 TxidDB(*txid),
219 operator_xonly_pk.map(XOnlyPublicKeyDB),
220 BlockHashDB(*block_hash),
221 ))
222 })
223 .collect();
224 let converted_values = converted_values?;
225
226 let mut query_builder = QueryBuilder::new(
227 "UPDATE withdrawals AS w SET
228 payout_txid = c.payout_txid,
229 payout_payer_operator_xonly_pk = c.payout_payer_operator_xonly_pk,
230 payout_tx_blockhash = c.payout_tx_blockhash
231 FROM (",
232 );
233
234 query_builder.push_values(
235 converted_values.into_iter(),
236 |mut b, (idx, txid, operator_xonly_pk, block_hash)| {
237 b.push_bind(idx)
238 .push_bind(txid)
239 .push_bind(operator_xonly_pk)
240 .push_bind(block_hash);
241 },
242 );
243
244 query_builder
245 .push(") AS c(idx, payout_txid, payout_payer_operator_xonly_pk, payout_tx_blockhash) WHERE w.idx = c.idx");
246
247 let query = query_builder.build();
248 execute_query_with_tx!(self.connection, tx, query, execute)?;
249
250 Ok(())
251 }
252
253 pub async fn get_payout_info_from_move_txid(
254 &self,
255 tx: Option<DatabaseTransaction<'_>>,
256 move_to_vault_txid: Txid,
257 ) -> Result<Option<(Option<XOnlyPublicKey>, BlockHash, Txid, i32)>, BridgeError> {
258 let query = sqlx::query_as::<_, (Option<XOnlyPublicKeyDB>, BlockHashDB, TxidDB, i32)>(
259 "SELECT w.payout_payer_operator_xonly_pk, w.payout_tx_blockhash, w.payout_txid, w.idx
260 FROM withdrawals w
261 WHERE w.move_to_vault_txid = $1
262 AND w.payout_txid IS NOT NULL
263 AND w.payout_tx_blockhash IS NOT NULL",
264 )
265 .bind(TxidDB(move_to_vault_txid));
266
267 let result: Option<(Option<XOnlyPublicKeyDB>, BlockHashDB, TxidDB, i32)> =
268 execute_query_with_tx!(self.connection, tx, query, fetch_optional)?;
269
270 result
271 .map(|(operator_xonly_pk, block_hash, txid, deposit_idx)| {
272 Ok((
273 operator_xonly_pk.map(|pk| pk.0),
274 block_hash.0,
275 txid.0,
276 deposit_idx,
277 ))
278 })
279 .transpose()
280 }
281
282 pub async fn get_first_unhandled_payout_by_operator_xonly_pk(
283 &self,
284 tx: Option<DatabaseTransaction<'_>>,
285 operator_xonly_pk: XOnlyPublicKey,
286 ) -> Result<Option<(u32, Txid, BlockHash)>, BridgeError> {
287 let query = sqlx::query_as::<_, (i32, Option<TxidDB>, Option<BlockHashDB>)>(
288 "SELECT w.idx, w.move_to_vault_txid, w.payout_tx_blockhash
289 FROM withdrawals w
290 WHERE w.payout_txid IS NOT NULL
291 AND w.is_payout_handled = FALSE
292 AND w.payout_payer_operator_xonly_pk = $1
293 ORDER BY w.idx ASC
294 LIMIT 1",
295 )
296 .bind(XOnlyPublicKeyDB(operator_xonly_pk));
297
298 let results = execute_query_with_tx!(self.connection, tx, query, fetch_optional)?;
299
300 results
301 .map(|(citrea_idx, move_to_vault_txid, payout_tx_blockhash)| {
302 Ok((
303 u32::try_from(citrea_idx).wrap_err("Failed to convert citrea index to u32")?,
304 move_to_vault_txid
305 .expect("move_to_vault_txid Must be Some")
306 .0,
307 payout_tx_blockhash
308 .expect("payout_tx_blockhash Must be Some")
309 .0,
310 ))
311 })
312 .transpose()
313 }
314
315 pub async fn get_payer_xonly_pk_blockhash_and_kickoff_txid_from_deposit_id(
316 &self,
317 tx: Option<DatabaseTransaction<'_>>,
318 deposit_id: u32,
319 ) -> Result<(Option<XOnlyPublicKey>, Option<BlockHash>, Option<Txid>), BridgeError> {
320 let query = sqlx::query_as::<
321 _,
322 (
323 Option<XOnlyPublicKeyDB>,
324 Option<BlockHashDB>,
325 Option<TxidDB>,
326 ),
327 >(
328 "SELECT w.payout_payer_operator_xonly_pk, w.payout_tx_blockhash, w.kickoff_txid
329 FROM withdrawals w
330 INNER JOIN deposits d ON d.move_to_vault_txid = w.move_to_vault_txid
331 WHERE d.deposit_id = $1",
332 )
333 .bind(i32::try_from(deposit_id).wrap_err("Failed to convert deposit id to i32")?);
334
335 let result: (
336 Option<XOnlyPublicKeyDB>,
337 Option<BlockHashDB>,
338 Option<TxidDB>,
339 ) = execute_query_with_tx!(self.connection, tx, query, fetch_one)?;
340
341 Ok((
342 result.0.map(|pk| pk.0),
343 result.1.map(|block_hash| block_hash.0),
344 result.2.map(|txid| txid.0),
345 ))
346 }
347
348 pub async fn mark_payout_handled(
349 &self,
350 tx: Option<DatabaseTransaction<'_>>,
351 citrea_idx: u32,
352 kickoff_txid: Txid,
353 ) -> Result<(), BridgeError> {
354 let query = sqlx::query(
355 "UPDATE withdrawals SET is_payout_handled = TRUE, kickoff_txid = $2 WHERE idx = $1",
356 )
357 .bind(i32::try_from(citrea_idx).wrap_err("Failed to convert citrea index to i32")?)
358 .bind(TxidDB(kickoff_txid));
359
360 execute_query_with_tx!(self.connection, tx, query, execute)?;
361 Ok(())
362 }
363
364 pub async fn get_handled_payout_kickoff_txid(
365 &self,
366 tx: Option<DatabaseTransaction<'_>>,
367 payout_txid: Txid,
368 ) -> Result<Option<Txid>, BridgeError> {
369 let query = sqlx::query_as::<_, (Option<TxidDB>,)>(
370 "SELECT kickoff_txid FROM withdrawals WHERE payout_txid = $1 AND is_payout_handled = TRUE",
371 )
372 .bind(TxidDB(payout_txid));
373
374 let result: Option<(Option<TxidDB>,)> =
375 execute_query_with_tx!(self.connection, tx, query, fetch_optional)?;
376
377 Ok(result
378 .map(|(kickoff_txid,)| kickoff_txid.expect("If handled, kickoff_txid must exist").0))
379 }
380}
381
382#[cfg(test)]
383mod tests {
384 use crate::{
385 database::Database,
386 test::common::{create_test_config_with_thread_name, generate_random_xonly_pk},
387 };
388 use bitcoin::{hashes::Hash, BlockHash, Txid};
389
390 #[tokio::test]
391 async fn update_get_payout_txs_from_citrea_withdrawal() {
392 let config = create_test_config_with_thread_name().await;
393 let db = Database::new(&config).await.unwrap();
394
395 let txid = Txid::from_byte_array([0x45; 32]);
396 let index = 0x1F;
397 let operator_xonly_pk = generate_random_xonly_pk();
398 let utxo = bitcoin::OutPoint {
399 txid: bitcoin::Txid::from_byte_array([0x45; 32]),
400 vout: 0,
401 };
402
403 let mut dbtx = db.begin_transaction().await.unwrap();
404
405 let block_id = db
406 .insert_block_info(
407 Some(&mut dbtx),
408 &BlockHash::all_zeros(),
409 &BlockHash::all_zeros(),
410 utxo.vout,
411 )
412 .await
413 .unwrap();
414 db.insert_txid_to_block(&mut dbtx, block_id, &txid)
415 .await
416 .unwrap();
417 db.insert_spent_utxo(&mut dbtx, block_id, &txid, &utxo.txid, utxo.vout.into())
418 .await
419 .unwrap();
420
421 assert!(db
422 .get_withdrawal_utxo_from_citrea_withdrawal(Some(&mut dbtx), index)
423 .await
424 .is_err());
425 db.upsert_move_to_vault_txid_from_citrea_deposit(Some(&mut dbtx), index, &txid)
426 .await
427 .unwrap();
428 db.update_withdrawal_utxo_from_citrea_withdrawal(Some(&mut dbtx), index, utxo, block_id)
429 .await
430 .unwrap();
431
432 let block_hash = BlockHash::all_zeros();
433
434 db.update_payout_txs_and_payer_operator_xonly_pk(
435 Some(&mut dbtx),
436 vec![(index, txid, Some(operator_xonly_pk), block_hash)],
437 )
438 .await
439 .unwrap();
440
441 let txs = db
442 .get_payout_txs_for_withdrawal_utxos(Some(&mut dbtx), block_id)
443 .await
444 .unwrap();
445
446 assert_eq!(txs.len(), 1);
447 assert_eq!(txs[0].0, index);
448 assert_eq!(txs[0].1, txid);
449
450 let withdrawal_utxo = db
451 .get_withdrawal_utxo_from_citrea_withdrawal(Some(&mut dbtx), index)
452 .await
453 .unwrap();
454 assert_eq!(withdrawal_utxo, utxo);
455
456 let move_txid = db
457 .get_move_to_vault_txid_from_citrea_deposit(Some(&mut dbtx), index)
458 .await
459 .unwrap()
460 .unwrap();
461 assert_eq!(move_txid, txid);
462
463 let payout_info = db
465 .get_payout_info_from_move_txid(Some(&mut dbtx), move_txid)
466 .await
467 .unwrap()
468 .unwrap();
469 assert_eq!(payout_info.0, Some(operator_xonly_pk));
470 assert_eq!(payout_info.1, block_hash);
471 assert_eq!(payout_info.2, txid);
472 assert_eq!(payout_info.3, index as i32);
473
474 let index2 = 0x2F;
476 let txid2 = Txid::from_byte_array([0x55; 32]);
477 let utxo2 = bitcoin::OutPoint {
478 txid: bitcoin::Txid::from_byte_array([0x55; 32]),
479 vout: 1,
480 };
481
482 db.insert_txid_to_block(&mut dbtx, block_id, &txid2)
483 .await
484 .unwrap();
485 db.insert_spent_utxo(&mut dbtx, block_id, &txid2, &utxo2.txid, utxo2.vout.into())
486 .await
487 .unwrap();
488
489 db.upsert_move_to_vault_txid_from_citrea_deposit(Some(&mut dbtx), index2, &txid2)
490 .await
491 .unwrap();
492 db.update_withdrawal_utxo_from_citrea_withdrawal(Some(&mut dbtx), index2, utxo2, block_id)
493 .await
494 .unwrap();
495
496 db.update_payout_txs_and_payer_operator_xonly_pk(
498 Some(&mut dbtx),
499 vec![(index2, txid2, None, block_hash)],
500 )
501 .await
502 .unwrap();
503
504 let payout_info2 = db
506 .get_payout_info_from_move_txid(Some(&mut dbtx), txid2)
507 .await
508 .unwrap()
509 .unwrap();
510 assert_eq!(payout_info2.0, None); assert_eq!(payout_info2.1, block_hash);
512 assert_eq!(payout_info2.2, txid2);
513 assert_eq!(payout_info2.3, index2 as i32);
514
515 let all_txs = db
517 .get_payout_txs_for_withdrawal_utxos(Some(&mut dbtx), block_id)
518 .await
519 .unwrap();
520 assert_eq!(all_txs.len(), 2);
521 }
522}