1use anyhow::{anyhow, Result};
2use boltz_client::swaps::boltz::CreateSubmarineResponse;
3use rusqlite::{named_params, params, Connection, Row};
4use sdk_common::bitcoin::hashes::{hex::ToHex, sha256, Hash};
5use serde::{Deserialize, Serialize};
6
7use crate::error::PaymentError;
8use crate::model::*;
9use crate::persist::{get_where_clause_state_in, Persister};
10use crate::sync::model::data::SendSyncData;
11use crate::sync::model::RecordType;
12use crate::utils::{from_row_to_u64, from_u64_to_row};
13use crate::{ensure_sdk, get_updated_fields};
14
15use super::where_clauses_to_string;
16
17impl Persister {
18 pub(crate) fn insert_or_update_send_swap_inner(
19 con: &Connection,
20 send_swap: &SendSwap,
21 ) -> Result<()> {
22 let id_hash = sha256::Hash::hash(send_swap.id.as_bytes()).to_hex();
23 con.execute(
24 "
25 INSERT INTO send_swaps (
26 id,
27 id_hash,
28 invoice,
29 bolt12_offer,
30 payment_hash,
31 destination_pubkey,
32 timeout_block_height,
33 payer_amount_sat,
34 receiver_amount_sat,
35 create_response_json,
36 refund_private_key,
37 created_at,
38 state,
39 pair_fees_json
40 )
41 VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
42 ON CONFLICT DO NOTHING
43 ",
44 (
45 &send_swap.id,
46 &id_hash,
47 &send_swap.invoice,
48 &send_swap.bolt12_offer,
49 &send_swap.payment_hash,
50 &send_swap.destination_pubkey,
51 from_u64_to_row(send_swap.timeout_block_height)?,
52 from_u64_to_row(send_swap.payer_amount_sat)?,
53 from_u64_to_row(send_swap.receiver_amount_sat)?,
54 &send_swap.create_response_json,
55 &send_swap.refund_private_key,
56 &send_swap.created_at,
57 &send_swap.state,
58 &send_swap.pair_fees_json,
59 ),
60 )?;
61
62 let rows_affected = con.execute(
63 "UPDATE send_swaps
64 SET
65 description = :description,
66 preimage = :preimage,
67 lockup_tx_id = :lockup_tx_id,
68 refund_address = :refund_address,
69 refund_tx_id = :refund_tx_id,
70 state = :state
71 WHERE
72 id = :id AND
73 version = :version",
74 named_params! {
75 ":id": &send_swap.id,
76 ":description": &send_swap.description,
77 ":preimage": &send_swap.preimage,
78 ":lockup_tx_id": &send_swap.lockup_tx_id,
79 ":refund_address": &send_swap.refund_address,
80 ":refund_tx_id": &send_swap.refund_tx_id,
81 ":state": &send_swap.state,
82 ":version": from_u64_to_row(send_swap.metadata.version)?,
83 },
84 )?;
85 ensure_sdk!(
86 rows_affected > 0,
87 anyhow!("Version mismatch for send swap {}", send_swap.id)
88 );
89
90 Ok(())
91 }
92
93 pub(crate) fn insert_or_update_send_swap(&self, send_swap: &SendSwap) -> Result<()> {
94 let maybe_swap = self.fetch_send_swap_by_id(&send_swap.id)?;
95 let updated_fields = SendSyncData::updated_fields(maybe_swap, send_swap);
96
97 let mut con = self.get_connection()?;
98 let tx = con.transaction_with_behavior(rusqlite::TransactionBehavior::Immediate)?;
99
100 Self::insert_or_update_send_swap_inner(&tx, send_swap)?;
101
102 let trigger_sync = updated_fields.as_ref().is_none_or(|u| !u.is_empty());
106 match trigger_sync {
107 true => {
108 self.commit_outgoing(&tx, &send_swap.id, RecordType::Send, updated_fields)?;
109 tx.commit()?;
110 self.trigger_sync();
111 }
112 false => {
113 tx.commit()?;
114 }
115 };
116
117 Ok(())
118 }
119
120 pub(crate) fn update_send_swaps_by_state(
121 &self,
122 from_state: PaymentState,
123 to_state: PaymentState,
124 is_local: Option<bool>,
125 ) -> Result<()> {
126 let con = self.get_connection()?;
127 let mut where_clauses = vec!["state = :from_state".to_string()];
128 if let Some(is_local) = is_local {
129 let mut where_is_local = format!("sync_state.is_local = {}", is_local as u8);
130 if is_local {
131 where_is_local = format!("({where_is_local} OR sync_state.is_local IS NULL)");
132 }
133 where_clauses.push(where_is_local);
134 }
135
136 let where_clause_str = where_clauses_to_string(where_clauses);
137 let query = format!(
138 "
139 UPDATE send_swaps
140 SET state = :to_state
141 WHERE id IN (
142 SELECT id
143 FROM send_swaps AS ss
144 LEFT JOIN sync_state ON ss.id = sync_state.data_id
145 {where_clause_str}
146 ORDER BY created_at
147 )
148 "
149 );
150
151 con.execute(
152 &query,
153 named_params! {
154 ":from_state": from_state,
155 ":to_state": to_state,
156 },
157 )?;
158
159 Ok(())
160 }
161
162 fn list_send_swaps_query(where_clauses: Vec<String>) -> String {
163 let mut where_clause_str = String::new();
164 if !where_clauses.is_empty() {
165 where_clause_str = String::from("WHERE ");
166 where_clause_str.push_str(where_clauses.join(" AND ").as_str());
167 }
168
169 format!(
170 "
171 SELECT
172 id,
173 invoice,
174 bolt12_offer,
175 payment_hash,
176 destination_pubkey,
177 timeout_block_height,
178 description,
179 preimage,
180 payer_amount_sat,
181 receiver_amount_sat,
182 create_response_json,
183 refund_private_key,
184 lockup_tx_id,
185 refund_address,
186 refund_tx_id,
187 created_at,
188 state,
189 pair_fees_json,
190 version,
191 last_updated_at,
192
193 sync_state.is_local
194 FROM send_swaps AS ss
195 LEFT JOIN sync_state ON ss.id = sync_state.data_id
196 {where_clause_str}
197 ORDER BY created_at
198 "
199 )
200 }
201
202 pub(crate) fn fetch_send_swap_by_id(&self, id: &str) -> Result<Option<SendSwap>> {
203 let con: Connection = self.get_connection()?;
204 let query = Self::list_send_swaps_query(vec!["id = ?1 or id_hash = ?1".to_string()]);
205 let res = con.query_row(&query, [id], Self::sql_row_to_send_swap);
206
207 Ok(res.ok())
208 }
209
210 pub(crate) fn fetch_send_swap_by_payment_hash(
211 &self,
212 payment_hash: &str,
213 ) -> Result<Option<SendSwap>> {
214 let con: Connection = self.get_connection()?;
215 let query = Self::list_send_swaps_query(vec!["payment_hash = ?1".to_string()]);
216 let res = con.query_row(&query, [payment_hash], Self::sql_row_to_send_swap);
217 Ok(res.ok())
218 }
219
220 fn sql_row_to_send_swap(row: &Row) -> rusqlite::Result<SendSwap> {
221 Ok(SendSwap {
222 id: row.get(0)?,
223 invoice: row.get(1)?,
224 bolt12_offer: row.get(2)?,
225 payment_hash: row.get(3)?,
226 destination_pubkey: row.get(4)?,
227 timeout_block_height: from_row_to_u64(row, 5)?,
228 description: row.get(6)?,
229 preimage: row.get(7)?,
230 payer_amount_sat: from_row_to_u64(row, 8)?,
231 receiver_amount_sat: from_row_to_u64(row, 9)?,
232 create_response_json: row.get(10)?,
233 refund_private_key: row.get(11)?,
234 lockup_tx_id: row.get(12)?,
235 refund_address: row.get(13)?,
236 refund_tx_id: row.get(14)?,
237 created_at: row.get(15)?,
238 state: row.get(16)?,
239 pair_fees_json: row.get(17)?,
240 metadata: SwapMetadata {
241 version: from_row_to_u64(row, 18)?,
242 last_updated_at: row.get(19)?,
243 is_local: row.get::<usize, Option<bool>>(20)?.unwrap_or(true),
244 },
245 })
246 }
247
248 pub(crate) fn list_send_swaps_where(
249 &self,
250 con: &Connection,
251 where_clauses: Vec<String>,
252 ) -> Result<Vec<SendSwap>> {
253 let query = Self::list_send_swaps_query(where_clauses);
254 let ongoing_send = con
255 .prepare(&query)?
256 .query_map(params![], Self::sql_row_to_send_swap)?
257 .map(|i| i.unwrap())
258 .collect();
259 Ok(ongoing_send)
260 }
261
262 pub(crate) fn list_send_swaps_by_state(
263 &self,
264 states: Vec<PaymentState>,
265 ) -> Result<Vec<SendSwap>> {
266 let con = self.get_connection()?;
267 let where_clause = vec![get_where_clause_state_in(&states)];
268 self.list_send_swaps_where(&con, where_clause)
269 }
270
271 pub(crate) fn list_ongoing_send_swaps(&self) -> Result<Vec<SendSwap>> {
272 self.list_send_swaps_by_state(vec![PaymentState::Created, PaymentState::Pending])
273 }
274
275 pub(crate) fn list_pending_send_swaps(&self) -> Result<Vec<SendSwap>> {
276 self.list_send_swaps_by_state(vec![PaymentState::Pending, PaymentState::RefundPending])
277 }
278
279 pub(crate) fn list_recoverable_send_swaps(&self) -> Result<Vec<SendSwap>> {
280 self.list_send_swaps_by_state(vec![
281 PaymentState::Created,
282 PaymentState::Pending,
283 PaymentState::RefundPending,
284 ])
285 }
286
287 pub(crate) fn try_handle_send_swap_update(
288 &self,
289 swap_id: &str,
290 to_state: PaymentState,
291 preimage: Option<&str>,
292 lockup_tx_id: Option<&str>,
293 refund_tx_id: Option<&str>,
294 ) -> Result<(), PaymentError> {
295 let mut con = self.get_connection()?;
297 let tx = con.transaction_with_behavior(rusqlite::TransactionBehavior::Immediate)?;
298
299 tx.execute(
300 "UPDATE send_swaps
301 SET
302 preimage = COALESCE(preimage, :preimage),
303 lockup_tx_id = COALESCE(lockup_tx_id, :lockup_tx_id),
304 refund_tx_id = COALESCE(refund_tx_id, :refund_tx_id),
305 state = :state
306 WHERE
307 id = :id",
308 named_params! {
309 ":id": swap_id,
310 ":preimage": preimage,
311 ":lockup_tx_id": lockup_tx_id,
312 ":refund_tx_id": refund_tx_id,
313 ":state": to_state,
314 },
315 )?;
316
317 let updated_fields = get_updated_fields!(preimage);
318 self.commit_outgoing(&tx, swap_id, RecordType::Send, updated_fields)?;
319 tx.commit()?;
320
321 self.trigger_sync();
322
323 Ok(())
324 }
325
326 pub(crate) fn set_send_swap_refund_address(
327 &self,
328 swap_id: &str,
329 refund_address: &str,
330 ) -> Result<(), PaymentError> {
331 let con = self.get_connection()?;
332 con.execute(
333 "UPDATE send_swaps
334 SET refund_address = :refund_address
335 WHERE id = :id",
336 named_params! {
337 ":id": swap_id,
338 ":refund_address": refund_address,
339 },
340 )?;
341 Ok(())
342 }
343
344 pub(crate) fn set_send_swap_lockup_tx_id(
345 &self,
346 swap_id: &str,
347 lockup_tx_id: &str,
348 ) -> Result<(), PaymentError> {
349 let con = self.get_connection()?;
350
351 let row_count = con
352 .execute(
353 "UPDATE send_swaps
354 SET lockup_tx_id = :lockup_tx_id
355 WHERE id = :id AND lockup_tx_id IS NULL",
356 named_params! {
357 ":id": swap_id,
358 ":lockup_tx_id": lockup_tx_id,
359 },
360 )
361 .map_err(|e| {
362 log::error!("Failed to set send swap lockup_tx_id: {e:?}");
363 PaymentError::PersistError
364 })?;
365 match row_count {
366 1 => Ok(()),
367 _ => Err(PaymentError::PaymentInProgress),
368 }
369 }
370
371 pub(crate) fn unset_send_swap_lockup_tx_id(
372 &self,
373 swap_id: &str,
374 lockup_tx_id: &str,
375 ) -> Result<(), PaymentError> {
376 let con = self.get_connection()?;
377 con.execute(
378 "UPDATE send_swaps
379 SET lockup_tx_id = NULL
380 WHERE id = :id AND lockup_tx_id = :lockup_tx_id",
381 named_params! {
382 ":id": swap_id,
383 ":lockup_tx_id": lockup_tx_id,
384 },
385 )
386 .map_err(|e| {
387 log::error!("Failed to unset send swap lockup_tx_id: {e:?}");
388 PaymentError::PersistError
389 })?;
390 Ok(())
391 }
392}
393
394#[derive(Clone, Debug, Serialize, Deserialize)]
395pub(crate) struct InternalCreateSubmarineResponse {
396 pub(crate) accept_zero_conf: bool,
397 pub(crate) address: String,
398 pub(crate) bip21: String,
399 pub(crate) claim_public_key: String,
400 pub(crate) expected_amount: u64,
401 pub(crate) referral_id: Option<String>,
402 pub(crate) swap_tree: InternalSwapTree,
403 #[serde(default)]
404 pub(crate) timeout_block_height: u64,
405 pub(crate) blinding_key: Option<String>,
406}
407impl InternalCreateSubmarineResponse {
408 pub(crate) fn try_convert_from_boltz(
409 boltz_create_response: &CreateSubmarineResponse,
410 expected_swap_id: &str,
411 ) -> Result<InternalCreateSubmarineResponse, PaymentError> {
412 ensure_sdk!(
415 boltz_create_response.id == expected_swap_id,
416 PaymentError::PersistError
417 );
418
419 let res = InternalCreateSubmarineResponse {
420 accept_zero_conf: boltz_create_response.accept_zero_conf,
421 address: boltz_create_response.address.clone(),
422 bip21: boltz_create_response.bip21.clone(),
423 claim_public_key: boltz_create_response.claim_public_key.to_string(),
424 expected_amount: boltz_create_response.expected_amount,
425 referral_id: boltz_create_response.referral_id.clone(),
426 swap_tree: boltz_create_response.swap_tree.clone().into(),
427 timeout_block_height: boltz_create_response.timeout_block_height,
428 blinding_key: boltz_create_response.blinding_key.clone(),
429 };
430 Ok(res)
431 }
432}
433
434#[cfg(test)]
435mod tests {
436 use crate::test_utils::persist::{create_persister, new_send_swap};
437 use anyhow::{anyhow, Result};
438
439 use super::PaymentState;
440
441 #[cfg(feature = "browser-tests")]
442 wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser);
443
444 #[sdk_macros::test_all]
445 fn test_fetch_send_swap() -> Result<()> {
446 create_persister!(storage);
447 let send_swap = new_send_swap(None, None);
448
449 storage.insert_or_update_send_swap(&send_swap)?;
450 assert!(storage.fetch_send_swap_by_id(&send_swap.id).is_ok());
452 assert!(storage
454 .fetch_send_swap_by_payment_hash(&send_swap.payment_hash.unwrap())
455 .is_ok());
456
457 Ok(())
458 }
459
460 #[sdk_macros::test_all]
461 fn test_list_send_swap() -> Result<()> {
462 create_persister!(storage);
463
464 let range = 0..3;
466 for _ in range.clone() {
467 storage.insert_or_update_send_swap(&new_send_swap(None, None))?;
468 }
469
470 let con = storage.get_connection()?;
471 let swaps = storage.list_send_swaps_where(&con, vec![])?;
472 assert_eq!(swaps.len(), range.len());
473
474 storage.insert_or_update_send_swap(&new_send_swap(Some(PaymentState::Pending), None))?;
476 let ongoing_swaps = storage.list_ongoing_send_swaps()?;
477 assert_eq!(ongoing_swaps.len(), 4);
478
479 let ongoing_swaps = storage.list_pending_send_swaps()?;
481 assert_eq!(ongoing_swaps.len(), 1);
482
483 Ok(())
484 }
485
486 #[sdk_macros::test_all]
487 fn test_update_send_swap() -> Result<()> {
488 create_persister!(storage);
489
490 let mut send_swap = new_send_swap(None, None);
491 storage.insert_or_update_send_swap(&send_swap)?;
492
493 let new_state = PaymentState::Pending;
495 let preimage = Some("preimage");
496 let lockup_tx_id = Some("lockup_tx_id");
497 let refund_tx_id = Some("refund_tx_id");
498
499 storage.try_handle_send_swap_update(
500 &send_swap.id,
501 new_state,
502 preimage,
503 lockup_tx_id,
504 refund_tx_id,
505 )?;
506
507 let updated_send_swap = storage
508 .fetch_send_swap_by_id(&send_swap.id)?
509 .ok_or(anyhow!("Could not find Send swap in database"))?;
510
511 assert_eq!(new_state, updated_send_swap.state);
512 assert_eq!(preimage, updated_send_swap.preimage.as_deref());
513 assert_eq!(lockup_tx_id, updated_send_swap.lockup_tx_id.as_deref());
514 assert_eq!(refund_tx_id, updated_send_swap.refund_tx_id.as_deref());
515
516 send_swap.state = new_state;
517
518 let new_state = PaymentState::Complete;
520 storage.update_send_swaps_by_state(send_swap.state, PaymentState::Complete, None)?;
521 let updated_send_swap = storage
522 .fetch_send_swap_by_id(&send_swap.id)?
523 .ok_or(anyhow!("Could not find Send swap in database"))?;
524 assert_eq!(new_state, updated_send_swap.state);
525
526 Ok(())
527 }
528
529 #[sdk_macros::async_test_all]
530 async fn test_writing_stale_swap() -> Result<()> {
531 create_persister!(storage);
532
533 let send_swap = new_send_swap(None, None);
534 storage.insert_or_update_send_swap(&send_swap)?;
535
536 let mut send_swap = storage.fetch_send_swap_by_id(&send_swap.id)?.unwrap();
538 send_swap.refund_tx_id = Some("tx_id".to_string());
539 storage.insert_or_update_send_swap(&send_swap)?;
540
541 let send_swap = storage.fetch_send_swap_by_id(&send_swap.id)?.unwrap();
543 storage.insert_or_update_send_swap(&send_swap)?;
544
545 let mut send_swap = storage.fetch_send_swap_by_id(&send_swap.id)?.unwrap();
547 send_swap.refund_tx_id = Some("tx_id_2".to_string());
548 storage.set_send_swap_lockup_tx_id(&send_swap.id, "tx_id")?;
550 assert!(storage.insert_or_update_send_swap(&send_swap).is_err());
551
552 Ok(())
553 }
554}