1use crate::batch::BatchOutputType;
2use crate::error::ErrorContext as _;
3use crate::swap_storage::SwapStorage;
4use crate::timeout_op;
5use crate::wallet::BoardingWallet;
6use crate::wallet::OnchainWallet;
7use crate::Blockchain;
8use crate::Client;
9use crate::Error;
10use ark_core::intent;
11use ark_core::send::build_offchain_transactions;
12use ark_core::send::sign_ark_transaction;
13use ark_core::send::sign_checkpoint_transaction;
14use ark_core::send::OffchainTransactions;
15use ark_core::send::VtxoInput;
16use ark_core::server::parse_sequence_number;
17use ark_core::vhtlc::VhtlcOptions;
18use ark_core::vhtlc::VhtlcScript;
19use ark_core::ArkAddress;
20use ark_core::VtxoList;
21use ark_core::VTXO_CONDITION_KEY;
22use bitcoin::absolute;
23use bitcoin::consensus::Encodable;
24use bitcoin::hashes::ripemd160;
25use bitcoin::hashes::sha256;
26use bitcoin::hashes::Hash;
27use bitcoin::io::Write;
28use bitcoin::key::Secp256k1;
29use bitcoin::psbt;
30use bitcoin::secp256k1;
31use bitcoin::secp256k1::schnorr;
32use bitcoin::taproot::LeafVersion;
33use bitcoin::Amount;
34use bitcoin::Psbt;
35use bitcoin::PublicKey;
36use bitcoin::TxOut;
37use bitcoin::Txid;
38use bitcoin::VarInt;
39use bitcoin::XOnlyPublicKey;
40use lightning_invoice::Bolt11Invoice;
41use rand::CryptoRng;
42use rand::Rng;
43use serde::Deserialize;
44use serde::Serialize;
45use serde_with::serde_as;
46use serde_with::DisplayFromStr;
47use std::str::FromStr;
48use std::time::SystemTime;
49use std::time::UNIX_EPOCH;
50
51#[derive(Clone, Debug)]
52pub struct SubmarineSwapResult {
53 pub swap_id: String,
54 pub txid: Txid,
55 pub amount: Amount,
56}
57
58#[derive(Clone, Debug)]
59pub struct ReverseSwapResult {
60 pub swap_id: String,
61 pub amount: Amount,
62 pub invoice: Bolt11Invoice,
63}
64
65#[derive(Clone, Debug)]
66pub struct ClaimVhtlcResult {
67 pub swap_id: String,
68 pub claim_txid: Txid,
69 pub claim_amount: Amount,
70 pub preimage: [u8; 32],
71}
72
73impl<B, W, S, K> Client<B, W, S, K>
74where
75 B: Blockchain,
76 W: BoardingWallet + OnchainWallet,
77 S: SwapStorage + 'static,
78 K: crate::KeyProvider,
79{
80 pub async fn prepare_ln_invoice_payment(
98 &self,
99 invoice: Bolt11Invoice,
100 ) -> Result<SubmarineSwapData, Error> {
101 let refund_public_key = self
102 .next_keypair(crate::key_provider::KeypairIndex::New)?
103 .public_key();
104
105 let preimage_hash = invoice.payment_hash();
106 let preimage_hash = ripemd160::Hash::hash(preimage_hash.as_byte_array());
107
108 let request = CreateSubmarineSwapRequest {
109 from: Asset::Ark,
110 to: Asset::Btc,
111 invoice,
112 refund_public_key: refund_public_key.into(),
113 };
114 let url = format!("{}/v2/swap/submarine", self.inner.boltz_url);
115
116 let client = reqwest::Client::new();
117 let response = client
118 .post(&url)
119 .json(&request)
120 .send()
121 .await
122 .map_err(|e| Error::ad_hoc(e.to_string()))
123 .context("failed to send submarine swap request")?;
124
125 if !response.status().is_success() {
126 let error_text = response
127 .text()
128 .await
129 .map_err(|e| Error::ad_hoc(e.to_string()))
130 .context("failed to read error text")?;
131
132 return Err(Error::ad_hoc(format!(
133 "failed to create submarine swap: {error_text}"
134 )));
135 }
136
137 let swap_response: CreateSubmarineSwapResponse = response
138 .json()
139 .await
140 .map_err(|e| Error::ad_hoc(e.to_string()))
141 .context("failed to deserialize submarine swap response")?;
142
143 let created_at = SystemTime::now()
144 .duration_since(UNIX_EPOCH)
145 .map_err(Error::ad_hoc)
146 .context("failed to compute created_at")?;
147
148 let data = SubmarineSwapData {
149 id: swap_response.id.clone(),
150 status: SwapStatus::Created,
151 preimage_hash,
152 refund_public_key: refund_public_key.into(),
153 claim_public_key: swap_response.claim_public_key,
154 vhtlc_address: swap_response.address,
155 timeout_block_heights: swap_response.timeout_block_heights,
156 amount: swap_response.expected_amount,
157 invoice: request.invoice.clone(),
158 created_at: created_at.as_secs(),
159 };
160
161 self.swap_storage()
162 .insert_submarine(swap_response.id.clone(), data.clone())
163 .await?;
164
165 tracing::info!(
166 swap_id = swap_response.id,
167 vhtlc_address = %data.vhtlc_address,
168 expected_amount = %data.amount,
169 "Prepared Lightning invoice payment"
170 );
171
172 Ok(data)
173 }
174
175 pub async fn pay_ln_invoice(
187 &self,
188 invoice: Bolt11Invoice,
189 ) -> Result<SubmarineSwapResult, Error> {
190 let keypair = self.next_keypair(crate::key_provider::KeypairIndex::New)?;
191 let refund_public_key = keypair.public_key();
192
193 let preimage_hash = invoice.payment_hash();
194 let preimage_hash = ripemd160::Hash::hash(preimage_hash.as_byte_array());
195
196 let request = CreateSubmarineSwapRequest {
197 from: Asset::Ark,
198 to: Asset::Btc,
199 invoice,
200 refund_public_key: refund_public_key.into(),
201 };
202 let url = format!("{}/v2/swap/submarine", self.inner.boltz_url);
203
204 let client = reqwest::Client::new();
205 let response = client
206 .post(&url)
207 .json(&request)
208 .send()
209 .await
210 .map_err(|e| Error::ad_hoc(e.to_string()))
211 .context("failed to send submarine swap request")?;
212
213 if !response.status().is_success() {
214 let error_text = response
215 .text()
216 .await
217 .map_err(|e| Error::ad_hoc(e.to_string()))
218 .context("failed to read error text")?;
219
220 return Err(Error::ad_hoc(format!(
221 "failed to create submarine swap: {error_text}"
222 )));
223 }
224
225 let swap_response: CreateSubmarineSwapResponse = response
226 .json()
227 .await
228 .map_err(|e| Error::ad_hoc(e.to_string()))
229 .context("failed to deserialize submarine swap response")?;
230
231 let created_at = SystemTime::now()
232 .duration_since(UNIX_EPOCH)
233 .map_err(Error::ad_hoc)
234 .context("failed to compute created_at")?;
235
236 self.swap_storage()
237 .insert_submarine(
238 swap_response.id.clone(),
239 SubmarineSwapData {
240 id: swap_response.id.clone(),
241 status: SwapStatus::Created,
242 preimage_hash,
243 refund_public_key: refund_public_key.into(),
244 claim_public_key: swap_response.claim_public_key,
245 vhtlc_address: swap_response.address,
246 timeout_block_heights: swap_response.timeout_block_heights,
247 amount: swap_response.expected_amount,
248 invoice: request.invoice.clone(),
249 created_at: created_at.as_secs(),
250 },
251 )
252 .await?;
253
254 let vhtlc_address = swap_response.address;
255 let amount = swap_response.expected_amount;
256 let txid = self.send_vtxo(vhtlc_address, amount).await?;
257
258 tracing::info!(swap_id = swap_response.id, %amount, "Funded VHTLC");
259
260 Ok(SubmarineSwapResult {
261 swap_id: swap_response.id,
262 txid,
263 amount,
264 })
265 }
266
267 pub async fn wait_for_invoice_paid(&self, swap_id: &str) -> Result<(), Error> {
271 use futures::StreamExt;
272
273 let stream = self.subscribe_to_swap_updates(swap_id.to_string());
274 tokio::pin!(stream);
275
276 while let Some(status_result) = stream.next().await {
277 match status_result {
278 Ok(status) => {
279 tracing::debug!(swap_id, current = ?status, "Swap status");
280 match status {
281 SwapStatus::InvoicePaid => {
282 return Ok(());
283 }
284 SwapStatus::InvoiceExpired => {
285 return Err(Error::ad_hoc(format!(
286 "invoice expired for swap {swap_id}"
287 )));
288 }
289 SwapStatus::Error { error } => {
290 tracing::error!(
291 swap_id,
292 "Got error from swap updates subscription: {error}"
293 );
294 }
295 SwapStatus::InvoiceSet
297 | SwapStatus::InvoicePending
298 | SwapStatus::Created
299 | SwapStatus::TransactionMempool
300 | SwapStatus::TransactionConfirmed
301 | SwapStatus::TransactionRefunded
302 | SwapStatus::TransactionFailed
303 | SwapStatus::TransactionClaimed
304 | SwapStatus::InvoiceFailedToPay
305 | SwapStatus::SwapExpired => {}
306 }
307 }
308 Err(e) => return Err(e),
309 }
310 }
311
312 Err(Error::ad_hoc("Status stream ended unexpectedly"))
313 }
314
315 pub async fn refund_expired_vhtlc(&self, swap_id: &str) -> Result<Txid, Error> {
319 let swap_data = self
320 .swap_storage()
321 .get_submarine(swap_id)
322 .await?
323 .ok_or(Error::ad_hoc("Submarine swap not found"))?;
324
325 let timeout_block_heights = swap_data.timeout_block_heights;
326
327 let vhtlc = VhtlcScript::new(
328 VhtlcOptions {
329 sender: swap_data.refund_public_key.into(),
330 receiver: swap_data.claim_public_key.into(),
331 server: self.server_info.signer_pk.into(),
332 preimage_hash: swap_data.preimage_hash,
333 refund_locktime: timeout_block_heights.refund,
334 unilateral_claim_delay: parse_sequence_number(
335 timeout_block_heights.unilateral_claim as i64,
336 )
337 .map_err(|e| Error::ad_hoc(format!("invalid unilateral claim timeout: {e}")))?,
338 unilateral_refund_delay: parse_sequence_number(
339 timeout_block_heights.unilateral_refund as i64,
340 )
341 .map_err(|e| Error::ad_hoc(format!("invalid unilateral refund timeout: {e}")))?,
342 unilateral_refund_without_receiver_delay: parse_sequence_number(
343 timeout_block_heights.unilateral_refund_without_receiver as i64,
344 )
345 .map_err(|e| {
346 Error::ad_hoc(format!("invalid refund without receiver timeout: {e}"))
347 })?,
348 },
349 self.server_info.network,
350 )
351 .map_err(Error::ad_hoc)?;
352
353 let vhtlc_address = vhtlc.address();
354 if vhtlc_address != swap_data.vhtlc_address {
355 return Err(Error::ad_hoc(format!(
356 "VHTLC address ({vhtlc_address}) does not match swap address ({})",
357 swap_data.vhtlc_address
358 )));
359 }
360
361 let vhtlc_outpoint = {
362 let virtual_tx_outpoints = self
363 .get_virtual_tx_outpoints(std::iter::once(vhtlc_address))
364 .await?;
365
366 let vtxo_list = VtxoList::new(self.server_info.dust, virtual_tx_outpoints);
367
368 let mut unspent = vtxo_list.all_unspent();
370 let vhtlc_outpoint = unspent.next().ok_or_else(|| {
371 Error::ad_hoc(format!("no outpoint found for address {vhtlc_address}"))
372 })?;
373
374 vhtlc_outpoint.clone()
375 };
376
377 let (refund_address, _) = self.get_offchain_address()?;
378 let refund_amount = swap_data.amount;
379
380 let outputs = vec![(&refund_address, refund_amount)];
381
382 let refund_script = vhtlc.refund_without_receiver_script();
383
384 let spend_info = vhtlc.taproot_spend_info();
385 let script_ver = (refund_script, LeafVersion::TapScript);
386 let control_block = spend_info
387 .control_block(&script_ver)
388 .ok_or(Error::ad_hoc("control block not found for refund script"))?;
389
390 let script_pubkey = vhtlc.script_pubkey();
391
392 let refunder_pk = swap_data.refund_public_key.inner.x_only_public_key().0;
393 let vhtlc_input = VtxoInput::new(
394 script_ver.0,
395 Some(absolute::LockTime::from_consensus(
396 swap_data.timeout_block_heights.refund,
397 )),
398 control_block,
399 vhtlc.tapscripts(),
400 script_pubkey,
401 refund_amount,
402 vhtlc_outpoint.outpoint,
403 );
404
405 let OffchainTransactions {
406 mut ark_tx,
407 checkpoint_txs,
408 } = build_offchain_transactions(
409 &outputs,
410 None,
411 std::slice::from_ref(&vhtlc_input),
412 &self.server_info,
413 )?;
414
415 let kp = self.keypair_by_pk(&refunder_pk)?;
416 let sign_fn =
417 |_: &mut psbt::Input,
418 msg: secp256k1::Message|
419 -> Result<Vec<(schnorr::Signature, XOnlyPublicKey)>, ark_core::Error> {
420 let sig = Secp256k1::new().sign_schnorr_no_aux_rand(&msg, &kp);
421 let pk = kp.x_only_public_key().0;
422
423 Ok(vec![(sig, pk)])
424 };
425
426 sign_ark_transaction(sign_fn, &mut ark_tx, 0)?;
427
428 let ark_txid = ark_tx.unsigned_tx.compute_txid();
429
430 let res = self
431 .network_client()
432 .submit_offchain_transaction_request(ark_tx, checkpoint_txs)
433 .await?;
434
435 let mut checkpoint_psbt = res
436 .signed_checkpoint_txs
437 .first()
438 .ok_or_else(|| Error::ad_hoc("no checkpoint PSBTs found"))?
439 .clone();
440
441 let kp = self.keypair_by_pk(&refunder_pk)?;
442 let sign_fn =
443 |_: &mut psbt::Input,
444 msg: secp256k1::Message|
445 -> Result<Vec<(schnorr::Signature, XOnlyPublicKey)>, ark_core::Error> {
446 let sig = Secp256k1::new().sign_schnorr_no_aux_rand(&msg, &kp);
447 let pk = kp.x_only_public_key().0;
448
449 Ok(vec![(sig, pk)])
450 };
451
452 sign_checkpoint_transaction(sign_fn, &mut checkpoint_psbt)?;
453
454 timeout_op(
455 self.inner.timeout,
456 self.network_client()
457 .finalize_offchain_transaction(ark_txid, vec![checkpoint_psbt]),
458 )
459 .await?
460 .map_err(Error::ark_server)
461 .context("failed to finalize offchain transaction")?;
462
463 tracing::info!(txid = %ark_txid, "Refunded VHTLC");
464
465 Ok(ark_txid)
466 }
467
468 pub async fn refund_expired_vhtlc_via_settlement<R>(
472 &self,
473 rng: &mut R,
474 swap_id: &str,
475 ) -> Result<Txid, Error>
476 where
477 R: Rng + CryptoRng,
478 {
479 let swap_data = self
480 .swap_storage()
481 .get_submarine(swap_id)
482 .await?
483 .ok_or(Error::ad_hoc("Submarine swap not found"))?;
484
485 let timeout_block_heights = swap_data.timeout_block_heights;
486
487 let vhtlc = VhtlcScript::new(
488 VhtlcOptions {
489 sender: swap_data.refund_public_key.into(),
490 receiver: swap_data.claim_public_key.into(),
491 server: self.server_info.signer_pk.into(),
492 preimage_hash: swap_data.preimage_hash,
493 refund_locktime: timeout_block_heights.refund,
494 unilateral_claim_delay: parse_sequence_number(
495 timeout_block_heights.unilateral_claim as i64,
496 )
497 .map_err(|e| Error::ad_hoc(format!("invalid unilateral claim timeout: {e}")))?,
498 unilateral_refund_delay: parse_sequence_number(
499 timeout_block_heights.unilateral_refund as i64,
500 )
501 .map_err(|e| Error::ad_hoc(format!("invalid unilateral refund timeout: {e}")))?,
502 unilateral_refund_without_receiver_delay: parse_sequence_number(
503 timeout_block_heights.unilateral_refund_without_receiver as i64,
504 )
505 .map_err(|e| {
506 Error::ad_hoc(format!("invalid refund without receiver timeout: {e}"))
507 })?,
508 },
509 self.server_info.network,
510 )
511 .map_err(Error::ad_hoc)?;
512
513 let vhtlc_address = vhtlc.address();
514 if vhtlc_address != swap_data.vhtlc_address {
515 return Err(Error::ad_hoc(format!(
516 "VHTLC address ({vhtlc_address}) does not match swap address ({})",
517 swap_data.vhtlc_address
518 )));
519 }
520
521 let vhtlc_outpoint = {
522 let virtual_tx_outpoints = self
523 .get_virtual_tx_outpoints(std::iter::once(vhtlc_address))
524 .await?;
525
526 let vtxo_list = VtxoList::new(self.server_info.dust, virtual_tx_outpoints);
527
528 let mut recoverable = vtxo_list.recoverable();
530
531 recoverable
532 .next()
533 .ok_or_else(|| {
534 Error::ad_hoc(format!("no outpoint found for address {vhtlc_address}"))
535 })?
536 .clone()
537 };
538
539 let refund_script = vhtlc.refund_without_receiver_script();
540
541 let spend_info = vhtlc.taproot_spend_info();
542 let script_ver = (refund_script, LeafVersion::TapScript);
543 let control_block = spend_info
544 .control_block(&script_ver)
545 .ok_or(Error::ad_hoc("control block not found for refund script"))?;
546
547 let script_pubkey = vhtlc.script_pubkey();
548
549 let (refund_address, _) = self.get_offchain_address()?;
550 let refund_amount = swap_data.amount;
551
552 let vhtlc_input = intent::Input::new(
553 vhtlc_outpoint.outpoint,
554 parse_sequence_number(timeout_block_heights.unilateral_refund as i64)
555 .map_err(|e| Error::ad_hoc(format!("invalid unilateral refund timeout: {e}")))?,
556 Some(absolute::LockTime::from_consensus(
557 timeout_block_heights.refund,
558 )),
559 TxOut {
560 value: refund_amount,
561 script_pubkey,
562 },
563 vhtlc.tapscripts(),
564 (script_ver.0, control_block),
565 false,
566 true,
567 );
568
569 let commitment_txid = self
570 .join_next_batch(
571 rng,
572 Vec::new(),
573 vec![vhtlc_input],
574 BatchOutputType::Board {
575 to_address: refund_address,
576 to_amount: refund_amount,
577 },
578 )
579 .await
580 .context("failed to join batch")?;
581
582 tracing::info!(txid = %commitment_txid, "Refunded VHTLC via settlement");
583
584 Ok(commitment_txid)
585 }
586
587 pub async fn refund_vhtlc(&self, swap_id: &str) -> Result<Txid, Error> {
593 let swap_data = self
594 .swap_storage()
595 .get_submarine(swap_id)
596 .await?
597 .ok_or(Error::ad_hoc("submarine swap not found"))?;
598
599 let timeout_block_heights = swap_data.timeout_block_heights;
600
601 let vhtlc = VhtlcScript::new(
602 VhtlcOptions {
603 sender: swap_data.refund_public_key.into(),
604 receiver: swap_data.claim_public_key.into(),
605 server: self.server_info.signer_pk.into(),
606 preimage_hash: swap_data.preimage_hash,
607 refund_locktime: timeout_block_heights.refund,
608 unilateral_claim_delay: parse_sequence_number(
609 timeout_block_heights.unilateral_claim as i64,
610 )
611 .map_err(|e| Error::ad_hoc(format!("invalid unilateral claim timeout: {e}")))?,
612 unilateral_refund_delay: parse_sequence_number(
613 timeout_block_heights.unilateral_refund as i64,
614 )
615 .map_err(|e| Error::ad_hoc(format!("invalid unilateral refund timeout: {e}")))?,
616 unilateral_refund_without_receiver_delay: parse_sequence_number(
617 timeout_block_heights.unilateral_refund_without_receiver as i64,
618 )
619 .map_err(|e| {
620 Error::ad_hoc(format!("invalid refund without receiver timeout: {e}"))
621 })?,
622 },
623 self.server_info.network,
624 )
625 .map_err(Error::ad_hoc)?;
626
627 let vhtlc_address = vhtlc.address();
628 if vhtlc_address != swap_data.vhtlc_address {
629 return Err(Error::ad_hoc(format!(
630 "VHTLC address ({vhtlc_address}) does not match swap address ({})",
631 swap_data.vhtlc_address
632 )));
633 }
634
635 let vhtlc_outpoint = {
636 let virtual_tx_outpoints = self
637 .get_virtual_tx_outpoints(std::iter::once(vhtlc_address))
638 .await?;
639
640 let vtxo_list = VtxoList::new(self.server_info.dust, virtual_tx_outpoints);
641
642 let mut unspent = vtxo_list.all_unspent();
644 let vhtlc_outpoint = unspent.next().ok_or_else(|| {
645 Error::ad_hoc(format!("no outpoint found for address {vhtlc_address}"))
646 })?;
647
648 vhtlc_outpoint.clone()
649 };
650
651 let (refund_address, _) = self.get_offchain_address()?;
652 let refund_amount = swap_data.amount;
653
654 let outputs = vec![(&refund_address, refund_amount)];
655
656 let refund_script = vhtlc.refund_script();
658
659 let spend_info = vhtlc.taproot_spend_info();
660 let script_ver = (refund_script, LeafVersion::TapScript);
661 let control_block = spend_info
662 .control_block(&script_ver)
663 .ok_or(Error::ad_hoc("control block not found for refund script"))?;
664
665 let script_pubkey = vhtlc.script_pubkey();
666
667 let refunder_pk = swap_data.refund_public_key.inner.x_only_public_key().0;
668 let vhtlc_input = VtxoInput::new(
669 script_ver.0,
670 None, control_block,
672 vhtlc.tapscripts(),
673 script_pubkey,
674 refund_amount,
675 vhtlc_outpoint.outpoint,
676 );
677
678 let OffchainTransactions {
679 mut ark_tx,
680 checkpoint_txs,
681 } = build_offchain_transactions(
682 &outputs,
683 None,
684 std::slice::from_ref(&vhtlc_input),
685 &self.server_info,
686 )?;
687
688 let kp = self.keypair_by_pk(&refunder_pk)?;
690 let sign_fn =
691 |_: &mut psbt::Input,
692 msg: secp256k1::Message|
693 -> Result<Vec<(schnorr::Signature, XOnlyPublicKey)>, ark_core::Error> {
694 let sig = Secp256k1::new().sign_schnorr_no_aux_rand(&msg, &kp);
695 let pk = kp.x_only_public_key().0;
696
697 Ok(vec![(sig, pk)])
698 };
699
700 sign_ark_transaction(sign_fn, &mut ark_tx, 0)?;
701
702 let checkpoint_psbt = checkpoint_txs
704 .first()
705 .ok_or_else(|| Error::ad_hoc("no checkpoint PSBTs found"))?
706 .clone();
707
708 let url = format!(
711 "{}/v2/swap/submarine/{swap_id}/refund/ark",
712 self.inner.boltz_url
713 );
714 let client = reqwest::Client::new();
715 let response = client
716 .post(&url)
717 .json(&RefundSwapRequest {
718 transaction: ark_tx.to_string(),
719 checkpoint: checkpoint_psbt.to_string(),
720 })
721 .send()
722 .await
723 .map_err(Error::ad_hoc)
724 .context("failed to send refund request to Boltz")?;
725
726 if !response.status().is_success() {
727 let error_text = response
728 .text()
729 .await
730 .map_err(|e| Error::ad_hoc(e.to_string()))
731 .context("failed to read error text")?;
732
733 return Err(Error::ad_hoc(format!(
734 "Boltz refund request failed: {error_text}"
735 )));
736 }
737
738 let refund_response: RefundSwapResponse = response
739 .json()
740 .await
741 .map_err(Error::ad_hoc)
742 .context("failed to deserialize refund response")?;
743
744 if let Some(err) = refund_response.error.as_deref() {
745 return Err(Error::ad_hoc(format!("Boltz refund request failed: {err}")));
746 }
747
748 let boltz_signed_ark_tx = Psbt::from_str(&refund_response.transaction)
750 .map_err(Error::ad_hoc)
751 .context("could not parse refund transaction PSBT")?;
752
753 let boltz_signed_checkpoint = Psbt::from_str(&refund_response.checkpoint)
754 .map_err(Error::ad_hoc)
755 .context("could not parse refund checkpoint PSBT")?;
756
757 let ark_txid = boltz_signed_ark_tx.unsigned_tx.compute_txid();
758
759 let boltz_tap_script_sigs = boltz_signed_checkpoint
761 .inputs
762 .first()
763 .ok_or_else(|| Error::ad_hoc("boltz checkpoint has no inputs"))?
764 .tap_script_sigs
765 .clone();
766
767 let res = self
770 .network_client()
771 .submit_offchain_transaction_request(boltz_signed_ark_tx, vec![boltz_signed_checkpoint])
772 .await?;
773
774 let mut server_signed_checkpoint = res
777 .signed_checkpoint_txs
778 .first()
779 .ok_or_else(|| Error::ad_hoc("no signed checkpoint PSBTs returned"))?
780 .clone();
781
782 let kp = self.keypair_by_pk(&refunder_pk)?;
783 let sign_fn =
784 |_: &mut psbt::Input,
785 msg: secp256k1::Message|
786 -> Result<Vec<(schnorr::Signature, XOnlyPublicKey)>, ark_core::Error> {
787 let sig = Secp256k1::new().sign_schnorr_no_aux_rand(&msg, &kp);
788 let pk = kp.x_only_public_key().0;
789
790 Ok(vec![(sig, pk)])
791 };
792
793 server_signed_checkpoint
794 .inputs
795 .first_mut()
796 .ok_or_else(|| Error::ad_hoc("server checkpoint has no inputs"))?
797 .tap_script_sigs
798 .extend(boltz_tap_script_sigs);
799
800 sign_checkpoint_transaction(sign_fn, &mut server_signed_checkpoint)?;
801
802 timeout_op(
804 self.inner.timeout,
805 self.network_client()
806 .finalize_offchain_transaction(ark_txid, vec![server_signed_checkpoint]),
807 )
808 .await?
809 .map_err(Error::ark_server)
810 .context("failed to finalize offchain transaction")?;
811
812 tracing::info!(swap_id, txid = %ark_txid, "Refunded VHTLC via collaborative refund");
813
814 Ok(ark_txid)
815 }
816
817 pub async fn get_ln_invoice(
831 &self,
832 amount: SwapAmount,
833 expiry_secs: Option<u64>,
834 ) -> Result<ReverseSwapResult, Error> {
835 let preimage: [u8; 32] = rand::random();
836 let preimage_hash_sha256 = sha256::Hash::hash(&preimage);
837 let preimage_hash = ripemd160::Hash::hash(preimage_hash_sha256.as_byte_array());
838
839 let claim_public_key = self
840 .next_keypair(crate::key_provider::KeypairIndex::New)?
841 .public_key();
842
843 let (invoice_amount, onchain_amount) = match amount {
844 SwapAmount::Invoice(amount) => (Some(amount), None),
845 SwapAmount::Vhtlc(amount) => (None, Some(amount)),
846 };
847
848 let request = CreateReverseSwapRequest {
849 from: Asset::Btc,
850 to: Asset::Ark,
851 invoice_amount,
852 onchain_amount,
853 claim_public_key: claim_public_key.into(),
854 preimage_hash: preimage_hash_sha256,
855 invoice_expiry: expiry_secs,
856 };
857
858 let url = format!("{}/v2/swap/reverse", self.inner.boltz_url);
859
860 let client = reqwest::Client::new();
861 let response = client
862 .post(&url)
863 .json(&request)
864 .send()
865 .await
866 .map_err(|e| Error::ad_hoc(e.to_string()))
867 .context("failed to send reverse swap request")?;
868
869 if !response.status().is_success() {
870 let error_text = response
871 .text()
872 .await
873 .map_err(|e| Error::ad_hoc(e.to_string()))
874 .context("failed to read error text")?;
875
876 return Err(Error::ad_hoc(format!(
877 "failed to create reverse swap: {error_text}"
878 )));
879 }
880
881 let response: CreateReverseSwapResponse = response
882 .json()
883 .await
884 .map_err(|e| Error::ad_hoc(e.to_string()))
885 .context("failed to deserialize reverse swap response")?;
886
887 let created_at = SystemTime::now()
888 .duration_since(UNIX_EPOCH)
889 .map_err(Error::ad_hoc)
890 .context("failed to compute created_at")?;
891
892 let swap_amount = response.onchain_amount.or(onchain_amount).ok_or_else(|| {
893 Error::ad_hoc("onchain_amount not provided by Boltz and not specified in request")
894 })?;
895
896 let swap = ReverseSwapData {
897 id: response.id.clone(),
898 status: SwapStatus::Created,
899 preimage: Some(preimage),
900 vhtlc_address: response.lockup_address,
901 preimage_hash,
902 refund_public_key: response.refund_public_key,
903 amount: swap_amount,
904 claim_public_key: claim_public_key.into(),
905 timeout_block_heights: response.timeout_block_heights,
906 created_at: created_at.as_secs(),
907 };
908
909 self.swap_storage()
910 .insert_reverse(response.id.clone(), swap.clone())
911 .await
912 .context("failed to persist swap data")?;
913
914 Ok(ReverseSwapResult {
915 swap_id: swap.id,
916 invoice: response.invoice,
917 amount: swap_amount,
918 })
919 }
920
921 pub async fn get_ln_invoice_from_hash(
941 &self,
942 amount: SwapAmount,
943 expiry_secs: Option<u64>,
944 preimage_hash_sha256: sha256::Hash,
945 ) -> Result<ReverseSwapResult, Error> {
946 let preimage_hash = ripemd160::Hash::hash(preimage_hash_sha256.as_byte_array());
947
948 let keypair = self.next_keypair(crate::key_provider::KeypairIndex::New)?;
949 let claim_public_key = keypair.public_key();
950
951 let (invoice_amount, onchain_amount) = match amount {
952 SwapAmount::Invoice(amount) => (Some(amount), None),
953 SwapAmount::Vhtlc(amount) => (None, Some(amount)),
954 };
955
956 let request = CreateReverseSwapRequest {
957 from: Asset::Btc,
958 to: Asset::Ark,
959 invoice_amount,
960 onchain_amount,
961 claim_public_key: claim_public_key.into(),
962 preimage_hash: preimage_hash_sha256,
963 invoice_expiry: expiry_secs,
964 };
965
966 let url = format!("{}/v2/swap/reverse", self.inner.boltz_url);
967
968 let client = reqwest::Client::new();
969 let response = client
970 .post(&url)
971 .json(&request)
972 .send()
973 .await
974 .map_err(|e| Error::ad_hoc(e.to_string()))
975 .context("failed to send reverse swap request")?;
976
977 if !response.status().is_success() {
978 let error_text = response
979 .text()
980 .await
981 .map_err(|e| Error::ad_hoc(e.to_string()))
982 .context("failed to read error text")?;
983
984 return Err(Error::ad_hoc(format!(
985 "failed to create reverse swap: {error_text}"
986 )));
987 }
988
989 let response: CreateReverseSwapResponse = response
990 .json()
991 .await
992 .map_err(|e| Error::ad_hoc(e.to_string()))
993 .context("failed to deserialize reverse swap response")?;
994
995 let created_at = SystemTime::now()
996 .duration_since(UNIX_EPOCH)
997 .map_err(Error::ad_hoc)
998 .context("failed to compute created_at")?;
999
1000 let swap_amount = response.onchain_amount.or(onchain_amount).ok_or_else(|| {
1001 Error::ad_hoc("onchain_amount not provided by Boltz and not specified in request")
1002 })?;
1003
1004 let swap = ReverseSwapData {
1005 id: response.id.clone(),
1006 status: SwapStatus::Created,
1007 preimage: None, vhtlc_address: response.lockup_address,
1009 preimage_hash,
1010 refund_public_key: response.refund_public_key,
1011 amount: swap_amount,
1012 claim_public_key: claim_public_key.into(),
1013 timeout_block_heights: response.timeout_block_heights,
1014 created_at: created_at.as_secs(),
1015 };
1016
1017 self.swap_storage()
1018 .insert_reverse(response.id.clone(), swap.clone())
1019 .await
1020 .context("failed to persist swap data")?;
1021
1022 Ok(ReverseSwapResult {
1023 swap_id: swap.id,
1024 invoice: response.invoice,
1025 amount: swap_amount,
1026 })
1027 }
1028
1029 pub async fn wait_for_vhtlc_funding(&self, swap_id: &str) -> Result<(), Error> {
1042 use futures::StreamExt;
1043
1044 let stream = self.subscribe_to_swap_updates(swap_id.to_string());
1045 tokio::pin!(stream);
1046
1047 while let Some(status_result) = stream.next().await {
1048 match status_result {
1049 Ok(status) => {
1050 tracing::debug!(swap_id, current = ?status, "Swap status");
1051
1052 match status {
1053 SwapStatus::TransactionMempool | SwapStatus::TransactionConfirmed => {
1054 tracing::debug!(swap_id, "VHTLC funding detected");
1055 return Ok(());
1056 }
1057 SwapStatus::InvoiceExpired => {
1058 return Err(Error::ad_hoc(format!(
1059 "invoice expired for swap {swap_id}"
1060 )));
1061 }
1062 SwapStatus::Error { error } => {
1063 tracing::error!(
1064 swap_id,
1065 "Got error from swap updates subscription: {error}"
1066 );
1067 }
1068 SwapStatus::Created
1070 | SwapStatus::TransactionRefunded
1071 | SwapStatus::TransactionFailed
1072 | SwapStatus::TransactionClaimed
1073 | SwapStatus::InvoiceSet
1074 | SwapStatus::InvoicePending
1075 | SwapStatus::InvoicePaid
1076 | SwapStatus::InvoiceFailedToPay
1077 | SwapStatus::SwapExpired => {}
1078 }
1079 }
1080 Err(e) => return Err(e),
1081 }
1082 }
1083
1084 Err(Error::ad_hoc("Status stream ended unexpectedly"))
1085 }
1086
1087 pub async fn claim_vhtlc(
1101 &self,
1102 swap_id: &str,
1103 preimage: [u8; 32],
1104 ) -> Result<ClaimVhtlcResult, Error> {
1105 let swap = self
1106 .swap_storage()
1107 .get_reverse(swap_id)
1108 .await
1109 .context("failed to get reverse swap data")?
1110 .ok_or_else(|| Error::ad_hoc(format!("reverse swap data not found: {swap_id}")))?;
1111
1112 let preimage_hash_sha256 = sha256::Hash::hash(&preimage);
1114 let preimage_hash = ripemd160::Hash::hash(preimage_hash_sha256.as_byte_array());
1115
1116 if preimage_hash != swap.preimage_hash {
1117 return Err(Error::ad_hoc(format!(
1118 "preimage does not match stored hash for swap {swap_id}"
1119 )));
1120 }
1121
1122 tracing::debug!(swap_id, "Claiming VHTLC with verified preimage");
1123
1124 let timeout_block_heights = swap.timeout_block_heights;
1125
1126 let vhtlc = VhtlcScript::new(
1127 VhtlcOptions {
1128 sender: swap.refund_public_key.into(),
1129 receiver: swap.claim_public_key.into(),
1130 server: self.server_info.signer_pk.into(),
1131 preimage_hash: swap.preimage_hash,
1132 refund_locktime: timeout_block_heights.refund,
1133 unilateral_claim_delay: parse_sequence_number(
1134 timeout_block_heights.unilateral_claim as i64,
1135 )
1136 .map_err(|e| Error::ad_hoc(format!("invalid unilateral claim timeout: {e}")))?,
1137 unilateral_refund_delay: parse_sequence_number(
1138 timeout_block_heights.unilateral_refund as i64,
1139 )
1140 .map_err(|e| Error::ad_hoc(format!("invalid unilateral refund timeout: {e}")))?,
1141 unilateral_refund_without_receiver_delay: parse_sequence_number(
1142 timeout_block_heights.unilateral_refund_without_receiver as i64,
1143 )
1144 .map_err(|e| {
1145 Error::ad_hoc(format!("invalid refund without receiver timeout: {e}"))
1146 })?,
1147 },
1148 self.server_info.network,
1149 )
1150 .map_err(Error::ad_hoc)
1151 .context("failed to build VHTLC script")?;
1152
1153 let vhtlc_address = vhtlc.address();
1154 if vhtlc_address != swap.vhtlc_address {
1155 return Err(Error::ad_hoc(format!(
1156 "VHTLC address ({vhtlc_address}) does not match swap address ({})",
1157 swap.vhtlc_address
1158 )));
1159 }
1160
1161 let vhtlc_outpoint = {
1163 let virtual_tx_outpoints = self
1164 .get_virtual_tx_outpoints(std::iter::once(vhtlc_address))
1165 .await?;
1166
1167 let vtxo_list = VtxoList::new(self.server_info.dust, virtual_tx_outpoints);
1168
1169 let mut unspent = vtxo_list.all_unspent();
1171 let vhtlc_outpoint = unspent.next().ok_or_else(|| {
1172 Error::ad_hoc(format!("no outpoint found for address {vhtlc_address}"))
1173 })?;
1174
1175 vhtlc_outpoint.clone()
1176 };
1177
1178 let (claim_address, _) = self
1179 .get_offchain_address()
1180 .context("failed to get offchain address")?;
1181 let claim_amount = swap.amount;
1182
1183 let outputs = vec![(&claim_address, claim_amount)];
1184
1185 let spend_info = vhtlc.taproot_spend_info();
1186 let script_ver = (vhtlc.claim_script(), LeafVersion::TapScript);
1187 let control_block = spend_info
1188 .control_block(&script_ver)
1189 .ok_or(Error::ad_hoc("control block not found for claim script"))?;
1190
1191 let script_pubkey = vhtlc.script_pubkey();
1192
1193 let claimer_pk = swap.claim_public_key.inner.x_only_public_key().0;
1194 let vhtlc_input = VtxoInput::new(
1195 script_ver.0,
1196 None,
1197 control_block,
1198 vhtlc.tapscripts(),
1199 script_pubkey,
1200 claim_amount,
1201 vhtlc_outpoint.outpoint,
1202 );
1203
1204 let OffchainTransactions {
1205 mut ark_tx,
1206 checkpoint_txs,
1207 } = build_offchain_transactions(
1208 &outputs,
1209 None,
1210 std::slice::from_ref(&vhtlc_input),
1211 &self.server_info,
1212 )
1213 .map_err(Error::from)
1214 .context("failed to build offchain TXs")?;
1215
1216 let kp = self.keypair_by_pk(&claimer_pk)?;
1217 let sign_fn =
1218 |input: &mut psbt::Input,
1219 msg: secp256k1::Message|
1220 -> Result<Vec<(schnorr::Signature, XOnlyPublicKey)>, ark_core::Error> {
1221 {
1223 let mut bytes = vec![1];
1225
1226 let length = VarInt::from(preimage.len() as u64);
1227
1228 length
1229 .consensus_encode(&mut bytes)
1230 .expect("valid length encoding");
1231
1232 bytes.write_all(&preimage).expect("valid preimage encoding");
1233
1234 input.unknown.insert(
1235 psbt::raw::Key {
1236 type_value: 222,
1237 key: VTXO_CONDITION_KEY.to_vec(),
1238 },
1239 bytes,
1240 );
1241 }
1242
1243 let sig = Secp256k1::new().sign_schnorr_no_aux_rand(&msg, &kp);
1244 let pk = kp.x_only_public_key().0;
1245
1246 Ok(vec![(sig, pk)])
1247 };
1248
1249 sign_ark_transaction(sign_fn, &mut ark_tx, 0)
1250 .map_err(Error::from)
1251 .context("failed to sign Ark TX")?;
1252
1253 let ark_txid = ark_tx.unsigned_tx.compute_txid();
1254
1255 let res = self
1256 .network_client()
1257 .submit_offchain_transaction_request(ark_tx, checkpoint_txs)
1258 .await
1259 .map_err(Error::from)
1260 .context("failed to submit offchain TXs")?;
1261
1262 let mut checkpoint_psbt = res
1263 .signed_checkpoint_txs
1264 .first()
1265 .ok_or_else(|| Error::ad_hoc("no checkpoint PSBTs found"))?
1266 .clone();
1267
1268 sign_checkpoint_transaction(sign_fn, &mut checkpoint_psbt)
1269 .map_err(Error::from)
1270 .context("failed to sign checkpoint TX")?;
1271
1272 timeout_op(
1273 self.inner.timeout,
1274 self.network_client()
1275 .finalize_offchain_transaction(ark_txid, vec![checkpoint_psbt]),
1276 )
1277 .await
1278 .context("failed to finalize offchain transaction")?
1279 .map_err(Error::ark_server)
1280 .context("failed to finalize offchain transaction")?;
1281
1282 tracing::info!(swap_id, txid = %ark_txid, "Claimed VHTLC");
1283
1284 let mut updated_swap = swap.clone();
1286 updated_swap.preimage = Some(preimage);
1287 self.swap_storage()
1288 .update_reverse(swap_id, updated_swap)
1289 .await
1290 .context("failed to update swap data with preimage")?;
1291
1292 Ok(ClaimVhtlcResult {
1293 swap_id: swap_id.to_string(),
1294 claim_txid: ark_txid,
1295 claim_amount,
1296 preimage,
1297 })
1298 }
1299
1300 pub async fn wait_for_vhtlc(&self, swap_id: &str) -> Result<ClaimVhtlcResult, Error> {
1308 use futures::StreamExt;
1309
1310 let swap = self
1311 .swap_storage()
1312 .get_reverse(swap_id)
1313 .await
1314 .context("failed to get reverse swap data")?
1315 .ok_or_else(|| Error::ad_hoc(format!("reverse swap data not found: {swap_id}")))?;
1316
1317 let preimage = swap.preimage.ok_or_else(|| {
1319 Error::ad_hoc(format!(
1320 "preimage not found in storage for swap {swap_id}. \
1321 Use wait_for_vhtlc_funding and claim_vhtlc instead."
1322 ))
1323 })?;
1324
1325 let stream = self.subscribe_to_swap_updates(swap_id.to_string());
1326 tokio::pin!(stream);
1327
1328 while let Some(status_result) = stream.next().await {
1329 match status_result {
1330 Ok(status) => {
1331 tracing::debug!(current = ?status, "Swap status");
1332
1333 match status {
1334 SwapStatus::TransactionMempool | SwapStatus::TransactionConfirmed => break,
1335 SwapStatus::InvoiceExpired => {
1336 return Err(Error::ad_hoc(format!(
1337 "invoice expired for swap {swap_id}"
1338 )));
1339 }
1340 SwapStatus::Error { error } => {
1341 tracing::error!(
1342 swap_id,
1343 "Got error from swap updates subscription: {error}"
1344 );
1345 }
1346 SwapStatus::Created
1348 | SwapStatus::TransactionRefunded
1349 | SwapStatus::TransactionFailed
1350 | SwapStatus::TransactionClaimed
1351 | SwapStatus::InvoiceSet
1352 | SwapStatus::InvoicePending
1353 | SwapStatus::InvoicePaid
1354 | SwapStatus::InvoiceFailedToPay
1355 | SwapStatus::SwapExpired => {}
1356 }
1357 }
1358 Err(e) => return Err(e),
1359 }
1360 }
1361
1362 tracing::debug!("Ark transaction for swap found");
1363
1364 let timeout_block_heights = swap.timeout_block_heights;
1365
1366 let vhtlc = VhtlcScript::new(
1367 VhtlcOptions {
1368 sender: swap.refund_public_key.into(),
1369 receiver: swap.claim_public_key.into(),
1370 server: self.server_info.signer_pk.into(),
1371 preimage_hash: swap.preimage_hash,
1372 refund_locktime: timeout_block_heights.refund,
1373 unilateral_claim_delay: parse_sequence_number(
1374 timeout_block_heights.unilateral_claim as i64,
1375 )
1376 .map_err(|e| Error::ad_hoc(format!("invalid unilateral claim timeout: {e}")))?,
1377 unilateral_refund_delay: parse_sequence_number(
1378 timeout_block_heights.unilateral_refund as i64,
1379 )
1380 .map_err(|e| Error::ad_hoc(format!("invalid unilateral refund timeout: {e}")))?,
1381 unilateral_refund_without_receiver_delay: parse_sequence_number(
1382 timeout_block_heights.unilateral_refund_without_receiver as i64,
1383 )
1384 .map_err(|e| {
1385 Error::ad_hoc(format!("invalid refund without receiver timeout: {e}"))
1386 })?,
1387 },
1388 self.server_info.network,
1389 )
1390 .map_err(Error::ad_hoc)
1391 .context("failed to build VHTLC script")?;
1392
1393 let vhtlc_address = vhtlc.address();
1394 if vhtlc_address != swap.vhtlc_address {
1395 return Err(Error::ad_hoc(format!(
1396 "VHTLC address ({vhtlc_address}) does not match swap address ({})",
1397 swap.vhtlc_address
1398 )));
1399 }
1400
1401 let vhtlc_outpoint = {
1403 let virtual_tx_outpoints = self
1404 .get_virtual_tx_outpoints(std::iter::once(vhtlc_address))
1405 .await?;
1406
1407 let vtxo_list = VtxoList::new(self.server_info.dust, virtual_tx_outpoints);
1408
1409 let mut unspent = vtxo_list.all_unspent();
1411 let vhtlc_outpoint = unspent.next().ok_or_else(|| {
1412 Error::ad_hoc(format!("no outpoint found for address {vhtlc_address}"))
1413 })?;
1414
1415 vhtlc_outpoint.clone()
1416 };
1417
1418 let (claim_address, _) = self
1419 .get_offchain_address()
1420 .context("failed to get offchain address")?;
1421 let claim_amount = swap.amount;
1422
1423 let outputs = vec![(&claim_address, claim_amount)];
1424
1425 let spend_info = vhtlc.taproot_spend_info();
1426 let script_ver = (vhtlc.claim_script(), LeafVersion::TapScript);
1427 let control_block = spend_info
1428 .control_block(&script_ver)
1429 .ok_or(Error::ad_hoc("control block not found for claim script"))?;
1430
1431 let script_pubkey = vhtlc.script_pubkey();
1432
1433 let claimer_pk = swap.claim_public_key.inner.x_only_public_key().0;
1434 let vhtlc_input = VtxoInput::new(
1435 script_ver.0,
1436 None,
1437 control_block,
1438 vhtlc.tapscripts(),
1439 script_pubkey,
1440 claim_amount,
1441 vhtlc_outpoint.outpoint,
1442 );
1443
1444 let OffchainTransactions {
1445 mut ark_tx,
1446 checkpoint_txs,
1447 } = build_offchain_transactions(
1448 &outputs,
1449 None,
1450 std::slice::from_ref(&vhtlc_input),
1451 &self.server_info,
1452 )
1453 .map_err(Error::from)
1454 .context("failed to build offchain TXs")?;
1455
1456 let kp = self.keypair_by_pk(&claimer_pk)?;
1457 let sign_fn =
1458 |input: &mut psbt::Input,
1459 msg: secp256k1::Message|
1460 -> Result<Vec<(schnorr::Signature, XOnlyPublicKey)>, ark_core::Error> {
1461 {
1463 let mut bytes = vec![1];
1465
1466 let length = VarInt::from(preimage.len() as u64);
1467
1468 length
1469 .consensus_encode(&mut bytes)
1470 .expect("valid length encoding");
1471
1472 bytes.write_all(&preimage).expect("valid preimage encoding");
1473
1474 input.unknown.insert(
1475 psbt::raw::Key {
1476 type_value: 222,
1477 key: VTXO_CONDITION_KEY.to_vec(),
1478 },
1479 bytes,
1480 );
1481 }
1482
1483 let sig = Secp256k1::new().sign_schnorr_no_aux_rand(&msg, &kp);
1484 let pk = kp.x_only_public_key().0;
1485
1486 Ok(vec![(sig, pk)])
1487 };
1488
1489 sign_ark_transaction(sign_fn, &mut ark_tx, 0)
1490 .map_err(Error::from)
1491 .context("failed to sign Ark TX")?;
1492
1493 let ark_txid = ark_tx.unsigned_tx.compute_txid();
1494
1495 let res = self
1496 .network_client()
1497 .submit_offchain_transaction_request(ark_tx, checkpoint_txs)
1498 .await
1499 .map_err(Error::from)
1500 .context("failed to submit offchain TXs")?;
1501
1502 let mut checkpoint_psbt = res
1503 .signed_checkpoint_txs
1504 .first()
1505 .ok_or_else(|| Error::ad_hoc("no checkpoint PSBTs found"))?
1506 .clone();
1507
1508 sign_checkpoint_transaction(sign_fn, &mut checkpoint_psbt)
1509 .map_err(Error::from)
1510 .context("failed to sign checkpoint TX")?;
1511
1512 timeout_op(
1513 self.inner.timeout,
1514 self.network_client()
1515 .finalize_offchain_transaction(ark_txid, vec![checkpoint_psbt]),
1516 )
1517 .await
1518 .context("failed to finalize offchain transaction")?
1519 .map_err(Error::ark_server)
1520 .context("failed to finalize offchain transaction")?;
1521
1522 tracing::info!(txid = %ark_txid, "Spent VHTLC");
1523
1524 Ok(ClaimVhtlcResult {
1525 swap_id: swap_id.to_string(),
1526 claim_txid: ark_txid,
1527 claim_amount,
1528 preimage,
1529 })
1530 }
1531
1532 pub async fn get_fees(&self) -> Result<BoltzFees, Error> {
1538 let client = reqwest::Client::builder()
1539 .timeout(self.inner.timeout)
1540 .build()
1541 .map_err(|e| Error::ad_hoc(e.to_string()))?;
1542
1543 let submarine_url = format!("{}/v2/swap/submarine", &self.inner.boltz_url);
1545 let submarine_response = client
1546 .get(&submarine_url)
1547 .send()
1548 .await
1549 .map_err(|e| Error::ad_hoc(e.to_string()))
1550 .context("failed to fetch submarine swap fees")?;
1551
1552 if !submarine_response.status().is_success() {
1553 let error_text = submarine_response
1554 .text()
1555 .await
1556 .map_err(|e| Error::ad_hoc(e.to_string()))?;
1557 return Err(Error::ad_hoc(format!(
1558 "failed to fetch submarine swap fees: {error_text}"
1559 )));
1560 }
1561
1562 let submarine_pairs: SubmarinePairsResponse = submarine_response
1563 .json()
1564 .await
1565 .map_err(|e| Error::ad_hoc(e.to_string()))
1566 .context("failed to deserialize submarine swap fees response")?;
1567
1568 let submarine_pair_fees = &submarine_pairs.ark.btc.fees;
1569 let submarine_fees = SubmarineSwapFees {
1570 percentage: submarine_pair_fees.percentage,
1571 miner_fees: submarine_pair_fees.miner_fees,
1572 };
1573
1574 let reverse_url = format!("{}/v2/swap/reverse", self.inner.boltz_url);
1576 let reverse_response = client
1577 .get(&reverse_url)
1578 .send()
1579 .await
1580 .map_err(|e| Error::ad_hoc(e.to_string()))
1581 .context("failed to fetch reverse swap fees")?;
1582
1583 if !reverse_response.status().is_success() {
1584 let error_text = reverse_response
1585 .text()
1586 .await
1587 .map_err(|e| Error::ad_hoc(e.to_string()))?;
1588 return Err(Error::ad_hoc(format!(
1589 "failed to fetch reverse swap fees: {error_text}"
1590 )));
1591 }
1592
1593 let reverse_pairs: ReversePairsResponse = reverse_response
1594 .json()
1595 .await
1596 .map_err(|e| Error::ad_hoc(e.to_string()))
1597 .context("failed to deserialize reverse swap fees response")?;
1598
1599 let reverse_pair_fees = &reverse_pairs.btc.ark.fees;
1600 let reverse_fees = ReverseSwapFees {
1601 percentage: reverse_pair_fees.percentage,
1602 miner_fees: ReverseMinerFees {
1603 lockup: reverse_pair_fees.miner_fees.lockup,
1604 claim: reverse_pair_fees.miner_fees.claim,
1605 },
1606 };
1607
1608 Ok(BoltzFees {
1609 submarine: submarine_fees,
1610 reverse: reverse_fees,
1611 })
1612 }
1613
1614 pub async fn get_limits(&self) -> Result<SwapLimits, Error> {
1620 let client = reqwest::Client::builder()
1621 .timeout(self.inner.timeout)
1622 .build()
1623 .map_err(|e| Error::ad_hoc(e.to_string()))?;
1624
1625 let url = format!("{}/v2/swap/submarine", self.inner.boltz_url);
1626 let response = client
1627 .get(&url)
1628 .send()
1629 .await
1630 .map_err(|e| Error::ad_hoc(e.to_string()))
1631 .context("failed to fetch swap limits")?;
1632
1633 if !response.status().is_success() {
1634 let error_text = response
1635 .text()
1636 .await
1637 .map_err(|e| Error::ad_hoc(e.to_string()))?;
1638 return Err(Error::ad_hoc(format!(
1639 "failed to fetch swap limits: {error_text}"
1640 )));
1641 }
1642
1643 let pairs: SubmarinePairsResponse = response
1644 .json()
1645 .await
1646 .map_err(|e| Error::ad_hoc(e.to_string()))
1647 .context("failed to deserialize swap limits response")?;
1648
1649 Ok(SwapLimits {
1650 min: pairs.ark.btc.limits.minimal,
1651 max: pairs.ark.btc.limits.maximal,
1652 })
1653 }
1654
1655 pub fn subscribe_to_swap_updates(
1658 &self,
1659 swap_id: String,
1660 ) -> impl futures::Stream<Item = Result<SwapStatus, Error>> + '_ {
1661 async_stream::stream! {
1662 let mut last_status: Option<SwapStatus> = None;
1663 let url = format!("{}/v2/swap/{swap_id}", self.inner.boltz_url);
1664
1665 loop {
1666 let client = reqwest::Client::new();
1667 let response = client
1668 .get(&url)
1669 .send()
1670 .await;
1671
1672 match response {
1673 Ok(resp) if resp.status().is_success() => {
1674 let status_response = resp
1675 .json::<GetSwapStatusResponse>()
1676 .await
1677 .map_err(|e| Error::ad_hoc(e.to_string()));
1678
1679 match status_response {
1680 Ok(current_status) => {
1681 let current_status = current_status.status;
1682
1683 if last_status.as_ref() != Some(¤t_status) {
1685 last_status = Some(current_status.clone());
1686 yield Ok(current_status);
1687 }
1688 }
1689 Err(e) => {
1690 yield Err(Error::ad_hoc(format!(
1691 "failed to deserialize swap status response: {e}"
1692 )));
1693 break;
1694 }
1695 }
1696 }
1697 Ok(resp) => {
1698 let error_text = resp
1699 .text()
1700 .await
1701 .unwrap_or_else(|_| "Unknown error".to_string());
1702
1703 yield Err(Error::ad_hoc(format!(
1704 "failed to check swap status: {error_text}"
1705 )));
1706 break;
1707 }
1708 Err(e) => {
1709 yield Err(Error::ad_hoc(e.to_string())
1710 .context("failed to send swap status request"));
1711 break;
1712 }
1713 }
1714
1715 tokio::time::sleep(std::time::Duration::from_secs(1)).await;
1717 }
1718 }
1719 }
1720}
1721
1722pub enum SwapAmount {
1724 Invoice(Amount),
1726 Vhtlc(Amount),
1728}
1729
1730impl SwapAmount {
1731 pub fn invoice(amount: Amount) -> Self {
1732 Self::Invoice(amount)
1733 }
1734
1735 pub fn vhtlc(amount: Amount) -> Self {
1736 Self::Vhtlc(amount)
1737 }
1738}
1739
1740#[serde_as]
1742#[derive(Debug, Clone, Serialize, Deserialize)]
1743pub struct SubmarineSwapData {
1744 pub id: String,
1746 pub preimage_hash: ripemd160::Hash,
1748 pub claim_public_key: PublicKey,
1750 pub refund_public_key: PublicKey,
1752 pub amount: Amount,
1754 pub timeout_block_heights: TimeoutBlockHeights,
1756 #[serde_as(as = "DisplayFromStr")]
1758 pub vhtlc_address: ArkAddress,
1759 pub invoice: Bolt11Invoice,
1761 pub status: SwapStatus,
1763 pub created_at: u64,
1765}
1766
1767#[serde_as]
1769#[derive(Debug, Clone, Serialize, Deserialize)]
1770pub struct ReverseSwapData {
1771 pub id: String,
1773 pub preimage: Option<[u8; 32]>,
1775 pub preimage_hash: ripemd160::Hash,
1777 pub claim_public_key: PublicKey,
1779 pub refund_public_key: PublicKey,
1781 pub amount: Amount,
1783 pub timeout_block_heights: TimeoutBlockHeights,
1785 #[serde_as(as = "DisplayFromStr")]
1787 pub vhtlc_address: ArkAddress,
1788 pub status: SwapStatus,
1790 pub created_at: u64,
1792}
1793
1794#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1798pub enum SwapStatus {
1799 #[serde(rename = "swap.created")]
1801 Created,
1802 #[serde(rename = "transaction.mempool")]
1804 TransactionMempool,
1805 #[serde(rename = "transaction.confirmed")]
1807 TransactionConfirmed,
1808 #[serde(rename = "transaction.refunded")]
1810 TransactionRefunded,
1811 #[serde(rename = "transaction.failed")]
1813 TransactionFailed,
1814 #[serde(rename = "transaction.claimed")]
1816 TransactionClaimed,
1817 #[serde(rename = "invoice.set")]
1819 InvoiceSet,
1820 #[serde(rename = "invoice.pending")]
1822 InvoicePending,
1823 #[serde(rename = "invoice.paid")]
1825 InvoicePaid,
1826 #[serde(rename = "invoice.failedToPay")]
1828 InvoiceFailedToPay,
1829 #[serde(rename = "invoice.expired")]
1831 InvoiceExpired,
1832 #[serde(rename = "swap.expired")]
1834 SwapExpired,
1835 #[serde(rename = "error")]
1837 Error { error: String },
1838}
1839
1840#[derive(Debug, Clone, Serialize, Deserialize, Copy)]
1841#[serde(rename_all = "camelCase")]
1842pub struct TimeoutBlockHeights {
1843 pub refund: u32,
1844 pub unilateral_claim: u32,
1845 pub unilateral_refund: u32,
1846 pub unilateral_refund_without_receiver: u32,
1847}
1848
1849#[derive(Debug, Clone, Serialize, Deserialize)]
1850#[serde(rename_all = "UPPERCASE")]
1851enum Asset {
1852 Btc,
1853 Ark,
1854}
1855
1856#[derive(Debug, Clone, Serialize, Deserialize)]
1857#[serde(rename_all = "camelCase")]
1858struct CreateReverseSwapRequest {
1859 from: Asset,
1860 to: Asset,
1861 #[serde(skip_serializing_if = "Option::is_none")]
1862 invoice_amount: Option<Amount>,
1863 #[serde(skip_serializing_if = "Option::is_none")]
1864 onchain_amount: Option<Amount>,
1865 claim_public_key: PublicKey,
1866 preimage_hash: sha256::Hash,
1867 #[serde(skip_serializing_if = "Option::is_none")]
1871 invoice_expiry: Option<u64>,
1872}
1873
1874#[serde_as]
1875#[derive(Debug, Clone, Serialize, Deserialize)]
1876#[serde(rename_all = "camelCase")]
1877struct CreateReverseSwapResponse {
1878 id: String,
1879 #[serde_as(as = "DisplayFromStr")]
1880 lockup_address: ArkAddress,
1881 refund_public_key: PublicKey,
1882 timeout_block_heights: TimeoutBlockHeights,
1883 invoice: Bolt11Invoice,
1884 onchain_amount: Option<Amount>,
1885}
1886
1887#[derive(Debug, Clone, Serialize, Deserialize)]
1888struct CreateSubmarineSwapRequest {
1889 from: Asset,
1890 to: Asset,
1891 invoice: Bolt11Invoice,
1892 #[serde(rename = "refundPublicKey")]
1893 refund_public_key: PublicKey,
1894}
1895
1896#[serde_as]
1897#[derive(Debug, Clone, Serialize, Deserialize)]
1898#[serde(rename_all = "camelCase")]
1899struct CreateSubmarineSwapResponse {
1900 id: String,
1901 #[serde_as(as = "DisplayFromStr")]
1902 address: ArkAddress,
1903 expected_amount: Amount,
1904 claim_public_key: PublicKey,
1905 timeout_block_heights: TimeoutBlockHeights,
1906}
1907
1908#[derive(Debug, Clone, Serialize, Deserialize)]
1909struct GetSwapStatusResponse {
1910 status: SwapStatus,
1911}
1912
1913#[derive(Debug, Clone, Serialize, Deserialize)]
1914struct RefundSwapRequest {
1915 transaction: String,
1916 checkpoint: String,
1917}
1918
1919#[derive(Debug, Clone, Serialize, Deserialize)]
1920struct RefundSwapResponse {
1921 transaction: String,
1922 checkpoint: String,
1923 #[serde(skip_serializing_if = "Option::is_none")]
1924 error: Option<String>,
1925}
1926
1927#[derive(Debug, Clone, Serialize, Deserialize)]
1929#[serde(rename_all = "camelCase")]
1930pub struct SubmarineSwapFees {
1931 pub percentage: f64,
1933 pub miner_fees: u64,
1935}
1936
1937#[derive(Debug, Clone, Serialize, Deserialize)]
1939pub struct ReverseMinerFees {
1940 pub lockup: u64,
1942 pub claim: u64,
1944}
1945
1946#[derive(Debug, Clone, Serialize, Deserialize)]
1948#[serde(rename_all = "camelCase")]
1949pub struct ReverseSwapFees {
1950 pub percentage: f64,
1952 pub miner_fees: ReverseMinerFees,
1954}
1955
1956#[derive(Debug, Clone, Serialize, Deserialize)]
1958pub struct BoltzFees {
1959 pub submarine: SubmarineSwapFees,
1961 pub reverse: ReverseSwapFees,
1963}
1964
1965#[derive(Debug, Clone, Serialize, Deserialize)]
1967pub struct SwapLimits {
1968 pub min: u64,
1970 pub max: u64,
1972}
1973
1974#[derive(Debug, Clone, Deserialize)]
1977struct PairLimits {
1978 minimal: u64,
1979 maximal: u64,
1980}
1981
1982#[derive(Debug, Clone, Deserialize)]
1984#[serde(rename_all = "camelCase")]
1985struct SubmarinePairFees {
1986 percentage: f64,
1987 miner_fees: u64,
1988}
1989
1990#[derive(Debug, Clone, Deserialize)]
1991struct SubmarinePairInfo {
1992 fees: SubmarinePairFees,
1993 limits: PairLimits,
1994}
1995
1996#[derive(Debug, Clone, Deserialize)]
1997#[serde(rename_all = "UPPERCASE")]
1998struct SubmarineArkPairs {
1999 btc: SubmarinePairInfo,
2000}
2001
2002#[derive(Debug, Clone, Deserialize)]
2003#[serde(rename_all = "UPPERCASE")]
2004struct SubmarinePairsResponse {
2005 ark: SubmarineArkPairs,
2006}
2007
2008#[derive(Debug, Clone, Deserialize)]
2010#[serde(rename_all = "camelCase")]
2011struct ReverseMinerFeesResponse {
2012 claim: u64,
2013 lockup: u64,
2014}
2015
2016#[derive(Debug, Clone, Deserialize)]
2017#[serde(rename_all = "camelCase")]
2018struct ReversePairFees {
2019 percentage: f64,
2020 miner_fees: ReverseMinerFeesResponse,
2021}
2022
2023#[derive(Debug, Clone, Deserialize)]
2024struct ReversePairInfo {
2025 fees: ReversePairFees,
2026}
2027
2028#[derive(Debug, Clone, Deserialize)]
2029#[serde(rename_all = "UPPERCASE")]
2030struct ReverseBtcPairs {
2031 ark: ReversePairInfo,
2032}
2033
2034#[derive(Debug, Clone, Deserialize)]
2035#[serde(rename_all = "UPPERCASE")]
2036struct ReversePairsResponse {
2037 btc: ReverseBtcPairs,
2038}
2039
2040#[cfg(test)]
2041mod tests {
2042 use super::*;
2043
2044 #[test]
2045 fn test_deserialize_create_reverse_swap_response() {
2046 let json = r#"{
2047 "id": "vqhG2fJtNY4H",
2048 "lockupAddress": "tark1qra883hysahlkt0ujcwhv0x2n278849c3m7t3a08l7fdc40f4f2nmw3f7kn37vvq0hqazxtqgtvhwp3z83zfgr7qc82t9mty8vk95ynpx3l43d",
2049 "refundPublicKey": "0206988651c7fbe41747bb21b54ced0a183f4d658e007ee8fdb23fbbfccb8e0c55",
2050 "timeoutBlockHeights": {
2051 "refund": 1760508054,
2052 "unilateralClaim": 9728,
2053 "unilateralRefund": 86528,
2054 "unilateralRefundWithoutReceiver": 86528
2055 },
2056 "invoice": "lntbs10u1p5wmeeepp56ms94rkev7tdrwqyus5a63lny2mqzq9vh2rq3u4ym3v4lxv6xl4qdql2djkuepqw3hjqs2jfvsxzerywfjhxuccqz95xqztfsp5ckaskagag554na8d56tlrfdxasstqrmmpkvswqqqx6y386jcfq9s9qxpqysgqt7z0vkdwkqamydae7ctgkh7l8q75w7q9394ce3lda2mkfxrpfdtj5gmltuctav7jdgatkflhztrjjzutdla5e4xp0uhxxy7sluzll4qpkkh6wv",
2057 "onchainAmount": 996
2058}"#;
2059
2060 let response: CreateReverseSwapResponse =
2061 serde_json::from_str(json).expect("Failed to deserialize CreateReverseSwapResponse");
2062
2063 assert_eq!(response.id, "vqhG2fJtNY4H");
2065 assert_eq!(response.onchain_amount, Some(Amount::from_sat(996)));
2066 assert_eq!(
2067 response.refund_public_key,
2068 PublicKey::from_str(
2069 "0206988651c7fbe41747bb21b54ced0a183f4d658e007ee8fdb23fbbfccb8e0c55"
2070 )
2071 .expect("valid public key")
2072 );
2073 assert_eq!(
2074 response.lockup_address.to_string(),
2075 "tark1qra883hysahlkt0ujcwhv0x2n278849c3m7t3a08l7fdc40f4f2nmw3f7kn37vvq0hqazxtqgtvhwp3z83zfgr7qc82t9mty8vk95ynpx3l43d"
2076 );
2077 assert_eq!(response.timeout_block_heights.refund, 1760508054);
2078 assert_eq!(response.timeout_block_heights.unilateral_claim, 9728);
2079 assert_eq!(response.timeout_block_heights.unilateral_refund, 86528);
2080 assert_eq!(
2081 response
2082 .timeout_block_heights
2083 .unilateral_refund_without_receiver,
2084 86528
2085 );
2086 }
2087}