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