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::script::extract_checksig_pubkeys;
12use ark_core::send::build_offchain_transactions;
13use ark_core::send::sign_ark_transaction;
14use ark_core::send::sign_checkpoint_transaction;
15use ark_core::send::OffchainTransactions;
16use ark_core::send::SendReceiver;
17use ark_core::send::VtxoInput;
18use ark_core::server::parse_sequence_number;
19use ark_core::server::PendingTx;
20use ark_core::vhtlc::VhtlcOptions;
21use ark_core::vhtlc::VhtlcScript;
22use ark_core::ArkAddress;
23use ark_core::VtxoList;
24use ark_core::VTXO_CONDITION_KEY;
25use bitcoin::absolute;
26use bitcoin::consensus::Encodable;
27use bitcoin::hashes::ripemd160;
28use bitcoin::hashes::sha256;
29use bitcoin::hashes::Hash;
30use bitcoin::io::Write;
31use bitcoin::key::Secp256k1;
32use bitcoin::psbt;
33use bitcoin::secp256k1;
34use bitcoin::secp256k1::schnorr;
35use bitcoin::taproot::LeafVersion;
36use bitcoin::Amount;
37use bitcoin::Psbt;
38use bitcoin::PublicKey;
39use bitcoin::ScriptBuf;
40use bitcoin::TxOut;
41use bitcoin::Txid;
42use bitcoin::VarInt;
43use bitcoin::XOnlyPublicKey;
44use lightning_invoice::Bolt11Invoice;
45use rand::CryptoRng;
46use rand::Rng;
47use serde::Deserialize;
48use serde::Serialize;
49use serde_with::serde_as;
50use serde_with::DisplayFromStr;
51use std::str::FromStr;
52use std::time::SystemTime;
53use std::time::UNIX_EPOCH;
54
55#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
57pub enum SwapType {
58 Submarine,
59 Reverse,
60 Chain,
61 Unknown,
63}
64
65impl std::fmt::Display for SwapType {
66 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
67 match self {
68 Self::Submarine => write!(f, "submarine"),
69 Self::Reverse => write!(f, "reverse"),
70 Self::Chain => write!(f, "chain"),
71 Self::Unknown => write!(f, "unknown"),
72 }
73 }
74}
75
76#[derive(Clone, Debug)]
78pub struct SwapStatusInfo {
79 pub swap_id: String,
80 pub swap_type: SwapType,
81 pub status: SwapStatus,
82}
83
84#[derive(Clone, Debug)]
85pub struct SubmarineSwapResult {
86 pub swap_id: String,
87 pub txid: Txid,
88 pub amount: Amount,
89}
90
91#[derive(Clone, Debug)]
92pub struct ReverseSwapResult {
93 pub swap_id: String,
94 pub amount: Amount,
95 pub invoice: Bolt11Invoice,
96}
97
98#[derive(Clone, Debug)]
99pub struct ClaimVhtlcResult {
100 pub swap_id: String,
101 pub claim_txid: Txid,
102 pub claim_amount: Amount,
103 pub preimage: [u8; 32],
104}
105
106#[derive(Clone, Debug)]
111pub enum PendingVhtlcSpendType {
112 Claim { swap_id: String, preimage: [u8; 32] },
116 CollaborativeRefund { swap_id: String },
120 ExpiredRefund { swap_id: String },
124}
125
126impl PendingVhtlcSpendType {
127 pub fn swap_id(&self) -> &str {
128 match self {
129 Self::Claim { swap_id, .. }
130 | Self::CollaborativeRefund { swap_id }
131 | Self::ExpiredRefund { swap_id } => swap_id,
132 }
133 }
134
135 pub fn name(&self) -> &'static str {
136 match self {
137 Self::Claim { .. } => "Claim",
138 Self::CollaborativeRefund { .. } => "CollaborativeRefund",
139 Self::ExpiredRefund { .. } => "ExpiredRefund",
140 }
141 }
142}
143
144#[derive(Clone, Debug)]
146pub struct PendingVhtlcSpendTx {
147 pub spend_type: PendingVhtlcSpendType,
148 pub pending_tx: PendingTx,
149}
150
151impl<B, W, S, K> Client<B, W, S, K>
152where
153 B: Blockchain,
154 W: BoardingWallet + OnchainWallet,
155 S: SwapStorage + 'static,
156 K: crate::KeyProvider,
157{
158 pub async fn prepare_ln_invoice_payment(
176 &self,
177 invoice: Bolt11Invoice,
178 ) -> Result<SubmarineSwapData, Error> {
179 let refund_keypair = self.next_keypair(crate::key_provider::KeypairIndex::New)?;
180 let refund_public_key = refund_keypair.public_key();
181 let key_derivation_index =
182 self.derivation_index_for_pk(&refund_keypair.x_only_public_key().0);
183
184 let preimage_hash = invoice.payment_hash();
185 let preimage_hash = ripemd160::Hash::hash(preimage_hash.as_byte_array());
186
187 let request = CreateSubmarineSwapRequest {
188 from: Asset::Ark,
189 to: Asset::Btc,
190 invoice,
191 refund_public_key: refund_public_key.into(),
192 };
193 let url = format!("{}/v2/swap/submarine", self.inner.boltz_url);
194
195 let client = reqwest::Client::new();
196 let response = client
197 .post(&url)
198 .json(&request)
199 .send()
200 .await
201 .map_err(|e| Error::ad_hoc(e.to_string()))
202 .context("failed to send submarine swap request")?;
203
204 if !response.status().is_success() {
205 let error_text = response
206 .text()
207 .await
208 .map_err(|e| Error::ad_hoc(e.to_string()))
209 .context("failed to read error text")?;
210
211 return Err(Error::ad_hoc(format!(
212 "failed to create submarine swap: {error_text}"
213 )));
214 }
215
216 let swap_response: CreateSubmarineSwapResponse = response
217 .json()
218 .await
219 .map_err(|e| Error::ad_hoc(e.to_string()))
220 .context("failed to deserialize submarine swap response")?;
221
222 let created_at = SystemTime::now()
223 .duration_since(UNIX_EPOCH)
224 .map_err(Error::ad_hoc)
225 .context("failed to compute created_at")?;
226
227 let data = SubmarineSwapData {
228 id: swap_response.id.clone(),
229 status: SwapStatus::Created,
230 preimage: None,
231 preimage_hash,
232 refund_public_key: refund_public_key.into(),
233 claim_public_key: swap_response.claim_public_key,
234 vhtlc_address: swap_response.address,
235 timeout_block_heights: swap_response.timeout_block_heights,
236 amount: swap_response.expected_amount,
237 invoice: request.invoice.clone(),
238 created_at: created_at.as_secs(),
239 key_derivation_index,
240 };
241
242 self.swap_storage()
243 .insert_submarine(swap_response.id.clone(), data.clone())
244 .await?;
245
246 tracing::info!(
247 swap_id = swap_response.id,
248 vhtlc_address = %data.vhtlc_address,
249 expected_amount = %data.amount,
250 "Prepared Lightning invoice payment"
251 );
252
253 Ok(data)
254 }
255
256 pub async fn pay_ln_invoice(
268 &self,
269 invoice: Bolt11Invoice,
270 ) -> Result<SubmarineSwapResult, Error> {
271 let refund_keypair = self.next_keypair(crate::key_provider::KeypairIndex::New)?;
272 let refund_public_key = refund_keypair.public_key();
273 let key_derivation_index =
274 self.derivation_index_for_pk(&refund_keypair.x_only_public_key().0);
275
276 let preimage_hash = invoice.payment_hash();
277 let preimage_hash = ripemd160::Hash::hash(preimage_hash.as_byte_array());
278
279 let request = CreateSubmarineSwapRequest {
280 from: Asset::Ark,
281 to: Asset::Btc,
282 invoice,
283 refund_public_key: refund_public_key.into(),
284 };
285 let url = format!("{}/v2/swap/submarine", self.inner.boltz_url);
286
287 let client = reqwest::Client::new();
288 let response = client
289 .post(&url)
290 .json(&request)
291 .send()
292 .await
293 .map_err(|e| Error::ad_hoc(e.to_string()))
294 .context("failed to send submarine swap request")?;
295
296 if !response.status().is_success() {
297 let error_text = response
298 .text()
299 .await
300 .map_err(|e| Error::ad_hoc(e.to_string()))
301 .context("failed to read error text")?;
302
303 return Err(Error::ad_hoc(format!(
304 "failed to create submarine swap: {error_text}"
305 )));
306 }
307
308 let swap_response: CreateSubmarineSwapResponse = response
309 .json()
310 .await
311 .map_err(|e| Error::ad_hoc(e.to_string()))
312 .context("failed to deserialize submarine swap response")?;
313
314 let created_at = SystemTime::now()
315 .duration_since(UNIX_EPOCH)
316 .map_err(Error::ad_hoc)
317 .context("failed to compute created_at")?;
318
319 self.swap_storage()
320 .insert_submarine(
321 swap_response.id.clone(),
322 SubmarineSwapData {
323 id: swap_response.id.clone(),
324 status: SwapStatus::Created,
325 preimage: None,
326 preimage_hash,
327 refund_public_key: refund_public_key.into(),
328 claim_public_key: swap_response.claim_public_key,
329 vhtlc_address: swap_response.address,
330 timeout_block_heights: swap_response.timeout_block_heights,
331 amount: swap_response.expected_amount,
332 invoice: request.invoice.clone(),
333 created_at: created_at.as_secs(),
334 key_derivation_index,
335 },
336 )
337 .await?;
338
339 let vhtlc_address = swap_response.address;
340 let amount = swap_response.expected_amount;
341
342 let txid = self
343 .send(vec![SendReceiver::bitcoin(vhtlc_address, amount)])
344 .await?;
345
346 tracing::info!(swap_id = swap_response.id, %amount, "Funded VHTLC");
347
348 Ok(SubmarineSwapResult {
349 swap_id: swap_response.id,
350 txid,
351 amount,
352 })
353 }
354
355 pub async fn wait_for_invoice_paid(&self, swap_id: &str) -> Result<[u8; 32], Error> {
365 use futures::StreamExt;
366
367 let stream = self.subscribe_to_swap_updates(swap_id.to_string());
368 tokio::pin!(stream);
369
370 while let Some(status_result) = stream.next().await {
371 match status_result {
372 Ok(status) => {
373 tracing::debug!(swap_id, current = ?status, "Swap status");
374 match status {
375 SwapStatus::InvoicePaid => {
376 let deadline = tokio::time::Instant::now() + self.inner.timeout;
377
378 loop {
379 match self.extract_submarine_swap_preimage(swap_id).await {
380 Ok(preimage) => return Ok(preimage),
381 Err(e) => {
382 if tokio::time::Instant::now() >= deadline {
383 return Err(e.context(
384 "invoice paid but failed to extract preimage from claim tx",
385 ));
386 }
387
388 tracing::debug!(
389 swap_id,
390 "Preimage not available yet, retrying: {e}"
391 );
392 }
393 }
394
395 tokio::time::sleep(std::time::Duration::from_secs(1)).await;
396 }
397 }
398 SwapStatus::InvoiceExpired => {
399 return Err(Error::ad_hoc(format!(
400 "invoice expired for swap {swap_id}"
401 )));
402 }
403 SwapStatus::Error { error } => {
404 tracing::error!(
405 swap_id,
406 "Got error from swap updates subscription: {error}"
407 );
408 }
409 SwapStatus::InvoiceSet
410 | SwapStatus::InvoicePending
411 | SwapStatus::Created
412 | SwapStatus::TransactionMempool
413 | SwapStatus::TransactionConfirmed
414 | SwapStatus::TransactionServerMempool
415 | SwapStatus::TransactionServerConfirmed
416 | SwapStatus::TransactionRefunded
417 | SwapStatus::TransactionFailed
418 | SwapStatus::TransactionClaimed
419 | SwapStatus::TransactionLockupFailed
420 | SwapStatus::InvoiceFailedToPay
421 | SwapStatus::SwapExpired
422 | SwapStatus::Other(_) => {}
423 }
424 }
425 Err(e) => return Err(e),
426 }
427 }
428
429 Err(Error::ad_hoc("Status stream ended unexpectedly"))
430 }
431
432 pub async fn extract_submarine_swap_preimage(&self, swap_id: &str) -> Result<[u8; 32], Error> {
441 let mut swap_data = self
442 .swap_storage()
443 .get_submarine(swap_id)
444 .await?
445 .ok_or(Error::ad_hoc("submarine swap not found"))?;
446
447 if let Some(preimage) = swap_data.preimage {
449 return Ok(preimage);
450 }
451
452 let vhtlc_address = swap_data.vhtlc_address;
453
454 let virtual_tx_outpoints = self
456 .get_virtual_tx_outpoints(std::iter::once(vhtlc_address))
457 .await
458 .context("failed to get virtual tx outpoints for VHTLC address")?;
459
460 let vhtlc_outpoint = virtual_tx_outpoints
461 .iter()
462 .find(|o| o.is_spent)
463 .ok_or_else(|| Error::ad_hoc("VHTLC outpoint not found or not yet spent (claimed)"))?;
464
465 let claim_txid = vhtlc_outpoint.ark_txid.ok_or_else(|| {
466 Error::ad_hoc("VHTLC is spent but has no ark_txid (claim transaction)")
467 })?;
468
469 let claim_txs = timeout_op(
471 self.inner.timeout,
472 self.network_client()
473 .get_virtual_txs(vec![claim_txid.to_string()], None),
474 )
475 .await?
476 .map_err(|e| Error::ad_hoc(e.to_string()))
477 .context("failed to fetch claim transaction")?;
478
479 let claim_psbt = claim_txs
480 .txs
481 .first()
482 .ok_or_else(|| Error::ad_hoc("claim transaction not found"))?;
483
484 let preimage = extract_preimage_from_psbt(claim_psbt)?;
486
487 let computed_hash = ripemd160::Hash::hash(sha256::Hash::hash(&preimage).as_byte_array());
489 if computed_hash != swap_data.preimage_hash {
490 return Err(Error::ad_hoc(format!(
491 "extracted preimage does not match stored hash: expected {}, got {}",
492 swap_data.preimage_hash, computed_hash
493 )));
494 }
495
496 swap_data.preimage = Some(preimage);
498 self.swap_storage()
499 .update_submarine(swap_id, swap_data)
500 .await
501 .context("failed to persist preimage to swap storage")?;
502
503 tracing::info!(
504 swap_id,
505 "Extracted and persisted preimage from claim transaction"
506 );
507
508 Ok(preimage)
509 }
510
511 pub async fn refund_expired_vhtlc(&self, swap_id: &str) -> Result<Txid, Error> {
515 let swap_data = self
516 .swap_storage()
517 .get_submarine(swap_id)
518 .await?
519 .ok_or(Error::ad_hoc("Submarine swap not found"))?;
520
521 let timeout_block_heights = swap_data.timeout_block_heights;
522
523 let vhtlc = VhtlcScript::new(
524 VhtlcOptions {
525 sender: swap_data.refund_public_key.into(),
526 receiver: swap_data.claim_public_key.into(),
527 server: self.server_info.signer_pk.into(),
528 preimage_hash: swap_data.preimage_hash,
529 refund_locktime: timeout_block_heights.refund,
530 unilateral_claim_delay: parse_sequence_number(
531 timeout_block_heights.unilateral_claim as i64,
532 )
533 .map_err(|e| Error::ad_hoc(format!("invalid unilateral claim timeout: {e}")))?,
534 unilateral_refund_delay: parse_sequence_number(
535 timeout_block_heights.unilateral_refund as i64,
536 )
537 .map_err(|e| Error::ad_hoc(format!("invalid unilateral refund timeout: {e}")))?,
538 unilateral_refund_without_receiver_delay: parse_sequence_number(
539 timeout_block_heights.unilateral_refund_without_receiver as i64,
540 )
541 .map_err(|e| {
542 Error::ad_hoc(format!("invalid refund without receiver timeout: {e}"))
543 })?,
544 },
545 self.server_info.network,
546 )
547 .map_err(Error::ad_hoc)?;
548
549 let vhtlc_address = vhtlc.address();
550 if vhtlc_address != swap_data.vhtlc_address {
551 return Err(Error::ad_hoc(format!(
552 "VHTLC address ({vhtlc_address}) does not match swap address ({})",
553 swap_data.vhtlc_address
554 )));
555 }
556
557 let vhtlc_outpoint = {
558 let virtual_tx_outpoints = self
559 .get_virtual_tx_outpoints(std::iter::once(vhtlc_address))
560 .await?;
561
562 let vtxo_list = VtxoList::new(self.server_info.dust, virtual_tx_outpoints);
563
564 let mut unspent = vtxo_list.all_unspent();
566 let vhtlc_outpoint = unspent.next().ok_or_else(|| {
567 Error::ad_hoc(format!("no outpoint found for address {vhtlc_address}"))
568 })?;
569
570 vhtlc_outpoint.clone()
571 };
572
573 let (refund_address, _) = self.get_offchain_address()?;
574 let refund_amount = swap_data.amount;
575
576 let outputs = vec![SendReceiver {
577 address: refund_address,
578 amount: refund_amount,
579 assets: Vec::new(),
580 }];
581
582 let refund_script = vhtlc.refund_without_receiver_script();
583
584 let spend_info = vhtlc.taproot_spend_info();
585 let script_ver = (refund_script, LeafVersion::TapScript);
586 let control_block = spend_info
587 .control_block(&script_ver)
588 .ok_or(Error::ad_hoc("control block not found for refund script"))?;
589
590 let script_pubkey = vhtlc.script_pubkey();
591
592 let refunder_pk = swap_data.refund_public_key.inner.x_only_public_key().0;
593 let vhtlc_input = VtxoInput::new(
594 script_ver.0,
595 Some(absolute::LockTime::from_consensus(
596 swap_data.timeout_block_heights.refund,
597 )),
598 control_block,
599 vhtlc.tapscripts(),
600 script_pubkey,
601 refund_amount,
602 vhtlc_outpoint.outpoint,
603 vhtlc_outpoint.assets,
604 );
605
606 let change_address = &refund_address;
608
609 let OffchainTransactions {
610 mut ark_tx,
611 checkpoint_txs,
612 } = build_offchain_transactions(
613 &outputs,
614 change_address,
615 std::slice::from_ref(&vhtlc_input),
616 &self.server_info,
617 )?;
618
619 let kp = self.keypair_by_pk(&refunder_pk)?;
620 let sign_fn =
621 |_: &mut psbt::Input,
622 msg: secp256k1::Message|
623 -> Result<Vec<(schnorr::Signature, XOnlyPublicKey)>, ark_core::Error> {
624 let sig = Secp256k1::new().sign_schnorr_no_aux_rand(&msg, &kp);
625 let pk = kp.x_only_public_key().0;
626
627 Ok(vec![(sig, pk)])
628 };
629
630 sign_ark_transaction(sign_fn, &mut ark_tx, 0)?;
631
632 let ark_txid = ark_tx.unsigned_tx.compute_txid();
633
634 let res = self
635 .network_client()
636 .submit_offchain_transaction_request(ark_tx, checkpoint_txs)
637 .await?;
638
639 let mut checkpoint_psbt = res
640 .signed_checkpoint_txs
641 .first()
642 .ok_or_else(|| Error::ad_hoc("no checkpoint PSBTs found"))?
643 .clone();
644
645 let kp = self.keypair_by_pk(&refunder_pk)?;
646 let sign_fn =
647 |_: &mut psbt::Input,
648 msg: secp256k1::Message|
649 -> Result<Vec<(schnorr::Signature, XOnlyPublicKey)>, ark_core::Error> {
650 let sig = Secp256k1::new().sign_schnorr_no_aux_rand(&msg, &kp);
651 let pk = kp.x_only_public_key().0;
652
653 Ok(vec![(sig, pk)])
654 };
655
656 sign_checkpoint_transaction(sign_fn, &mut checkpoint_psbt)?;
657
658 timeout_op(
659 self.inner.timeout,
660 self.network_client()
661 .finalize_offchain_transaction(ark_txid, vec![checkpoint_psbt]),
662 )
663 .await?
664 .map_err(Error::ark_server)
665 .context("failed to finalize offchain transaction")?;
666
667 tracing::info!(txid = %ark_txid, "Refunded VHTLC");
668
669 Ok(ark_txid)
670 }
671
672 pub async fn refund_expired_vhtlc_via_settlement<R>(
676 &self,
677 rng: &mut R,
678 swap_id: &str,
679 ) -> Result<Txid, Error>
680 where
681 R: Rng + CryptoRng,
682 {
683 let swap_data = self
684 .swap_storage()
685 .get_submarine(swap_id)
686 .await?
687 .ok_or(Error::ad_hoc("Submarine swap not found"))?;
688
689 let timeout_block_heights = swap_data.timeout_block_heights;
690
691 let vhtlc = VhtlcScript::new(
692 VhtlcOptions {
693 sender: swap_data.refund_public_key.into(),
694 receiver: swap_data.claim_public_key.into(),
695 server: self.server_info.signer_pk.into(),
696 preimage_hash: swap_data.preimage_hash,
697 refund_locktime: timeout_block_heights.refund,
698 unilateral_claim_delay: parse_sequence_number(
699 timeout_block_heights.unilateral_claim as i64,
700 )
701 .map_err(|e| Error::ad_hoc(format!("invalid unilateral claim timeout: {e}")))?,
702 unilateral_refund_delay: parse_sequence_number(
703 timeout_block_heights.unilateral_refund as i64,
704 )
705 .map_err(|e| Error::ad_hoc(format!("invalid unilateral refund timeout: {e}")))?,
706 unilateral_refund_without_receiver_delay: parse_sequence_number(
707 timeout_block_heights.unilateral_refund_without_receiver as i64,
708 )
709 .map_err(|e| {
710 Error::ad_hoc(format!("invalid refund without receiver timeout: {e}"))
711 })?,
712 },
713 self.server_info.network,
714 )
715 .map_err(Error::ad_hoc)?;
716
717 let vhtlc_address = vhtlc.address();
718 if vhtlc_address != swap_data.vhtlc_address {
719 return Err(Error::ad_hoc(format!(
720 "VHTLC address ({vhtlc_address}) does not match swap address ({})",
721 swap_data.vhtlc_address
722 )));
723 }
724
725 let vhtlc_outpoint = {
726 let virtual_tx_outpoints = self
727 .get_virtual_tx_outpoints(std::iter::once(vhtlc_address))
728 .await?;
729
730 let vtxo_list = VtxoList::new(self.server_info.dust, virtual_tx_outpoints);
731
732 let mut recoverable = vtxo_list.recoverable();
734
735 recoverable
736 .next()
737 .ok_or_else(|| {
738 Error::ad_hoc(format!("no outpoint found for address {vhtlc_address}"))
739 })?
740 .clone()
741 };
742
743 let refund_script = vhtlc.refund_without_receiver_script();
744
745 let spend_info = vhtlc.taproot_spend_info();
746 let script_ver = (refund_script, LeafVersion::TapScript);
747 let control_block = spend_info
748 .control_block(&script_ver)
749 .ok_or(Error::ad_hoc("control block not found for refund script"))?;
750
751 let script_pubkey = vhtlc.script_pubkey();
752
753 let (refund_address, _) = self.get_offchain_address()?;
754 let refund_amount = swap_data.amount;
755
756 let vhtlc_input = intent::Input::new(
757 vhtlc_outpoint.outpoint,
758 parse_sequence_number(timeout_block_heights.unilateral_refund as i64)
759 .map_err(|e| Error::ad_hoc(format!("invalid unilateral refund timeout: {e}")))?,
760 Some(absolute::LockTime::from_consensus(
761 timeout_block_heights.refund,
762 )),
763 TxOut {
764 value: refund_amount,
765 script_pubkey,
766 },
767 vhtlc.tapscripts(),
768 (script_ver.0, control_block),
769 false,
770 true,
771 vhtlc_outpoint.assets,
772 );
773
774 let commitment_txid = self
775 .join_next_batch(
776 rng,
777 Vec::new(),
778 vec![vhtlc_input],
779 BatchOutputType::Board {
780 to_address: refund_address,
781 to_amount: refund_amount,
782 },
783 )
784 .await
785 .context("failed to join batch")?;
786
787 tracing::info!(txid = %commitment_txid, "Refunded VHTLC via settlement");
788
789 Ok(commitment_txid)
790 }
791
792 pub async fn refund_vhtlc(&self, swap_id: &str) -> Result<Txid, Error> {
798 let swap_data = self
799 .swap_storage()
800 .get_submarine(swap_id)
801 .await?
802 .ok_or(Error::ad_hoc("submarine swap not found"))?;
803
804 let timeout_block_heights = swap_data.timeout_block_heights;
805
806 let vhtlc = VhtlcScript::new(
807 VhtlcOptions {
808 sender: swap_data.refund_public_key.into(),
809 receiver: swap_data.claim_public_key.into(),
810 server: self.server_info.signer_pk.into(),
811 preimage_hash: swap_data.preimage_hash,
812 refund_locktime: timeout_block_heights.refund,
813 unilateral_claim_delay: parse_sequence_number(
814 timeout_block_heights.unilateral_claim as i64,
815 )
816 .map_err(|e| Error::ad_hoc(format!("invalid unilateral claim timeout: {e}")))?,
817 unilateral_refund_delay: parse_sequence_number(
818 timeout_block_heights.unilateral_refund as i64,
819 )
820 .map_err(|e| Error::ad_hoc(format!("invalid unilateral refund timeout: {e}")))?,
821 unilateral_refund_without_receiver_delay: parse_sequence_number(
822 timeout_block_heights.unilateral_refund_without_receiver as i64,
823 )
824 .map_err(|e| {
825 Error::ad_hoc(format!("invalid refund without receiver timeout: {e}"))
826 })?,
827 },
828 self.server_info.network,
829 )
830 .map_err(Error::ad_hoc)?;
831
832 let vhtlc_address = vhtlc.address();
833 if vhtlc_address != swap_data.vhtlc_address {
834 return Err(Error::ad_hoc(format!(
835 "VHTLC address ({vhtlc_address}) does not match swap address ({})",
836 swap_data.vhtlc_address
837 )));
838 }
839
840 let vhtlc_outpoint = {
841 let virtual_tx_outpoints = self
842 .get_virtual_tx_outpoints(std::iter::once(vhtlc_address))
843 .await?;
844
845 let vtxo_list = VtxoList::new(self.server_info.dust, virtual_tx_outpoints);
846
847 let mut unspent = vtxo_list.all_unspent();
849 let vhtlc_outpoint = unspent.next().ok_or_else(|| {
850 Error::ad_hoc(format!("no outpoint found for address {vhtlc_address}"))
851 })?;
852
853 vhtlc_outpoint.clone()
854 };
855
856 let (refund_address, _) = self.get_offchain_address()?;
857 let refund_amount = swap_data.amount;
858
859 let outputs = vec![SendReceiver {
860 address: refund_address,
861 amount: refund_amount,
862 assets: Vec::new(),
863 }];
864
865 let refund_script = vhtlc.refund_script();
867
868 let spend_info = vhtlc.taproot_spend_info();
869 let script_ver = (refund_script, LeafVersion::TapScript);
870 let control_block = spend_info
871 .control_block(&script_ver)
872 .ok_or(Error::ad_hoc("control block not found for refund script"))?;
873
874 let script_pubkey = vhtlc.script_pubkey();
875
876 let refunder_pk = swap_data.refund_public_key.inner.x_only_public_key().0;
877 let vhtlc_input = VtxoInput::new(
878 script_ver.0,
879 None, control_block,
881 vhtlc.tapscripts(),
882 script_pubkey,
883 refund_amount,
884 vhtlc_outpoint.outpoint,
885 vhtlc_outpoint.assets,
886 );
887
888 let change_address = &refund_address;
890
891 let OffchainTransactions {
892 mut ark_tx,
893 checkpoint_txs,
894 } = build_offchain_transactions(
895 &outputs,
896 change_address,
897 std::slice::from_ref(&vhtlc_input),
898 &self.server_info,
899 )?;
900
901 let kp = self.keypair_by_pk(&refunder_pk)?;
903 let sign_fn =
904 |_: &mut psbt::Input,
905 msg: secp256k1::Message|
906 -> Result<Vec<(schnorr::Signature, XOnlyPublicKey)>, ark_core::Error> {
907 let sig = Secp256k1::new().sign_schnorr_no_aux_rand(&msg, &kp);
908 let pk = kp.x_only_public_key().0;
909
910 Ok(vec![(sig, pk)])
911 };
912
913 sign_ark_transaction(sign_fn, &mut ark_tx, 0)?;
914
915 let checkpoint_psbt = checkpoint_txs
917 .first()
918 .ok_or_else(|| Error::ad_hoc("no checkpoint PSBTs found"))?
919 .clone();
920
921 let url = format!(
924 "{}/v2/swap/submarine/{swap_id}/refund/ark",
925 self.inner.boltz_url
926 );
927 let client = reqwest::Client::new();
928 let response = client
929 .post(&url)
930 .json(&RefundSwapRequest {
931 transaction: ark_tx.to_string(),
932 checkpoint: checkpoint_psbt.to_string(),
933 })
934 .send()
935 .await
936 .map_err(Error::ad_hoc)
937 .context("failed to send refund request to Boltz")?;
938
939 if !response.status().is_success() {
940 let error_text = response
941 .text()
942 .await
943 .map_err(|e| Error::ad_hoc(e.to_string()))
944 .context("failed to read error text")?;
945
946 return Err(Error::ad_hoc(format!(
947 "Boltz refund request failed: {error_text}"
948 )));
949 }
950
951 let refund_response: RefundSwapResponse = response
952 .json()
953 .await
954 .map_err(Error::ad_hoc)
955 .context("failed to deserialize refund response")?;
956
957 if let Some(err) = refund_response.error.as_deref() {
958 return Err(Error::ad_hoc(format!("Boltz refund request failed: {err}")));
959 }
960
961 let boltz_signed_ark_tx = Psbt::from_str(&refund_response.transaction)
963 .map_err(Error::ad_hoc)
964 .context("could not parse refund transaction PSBT")?;
965
966 let boltz_signed_checkpoint = Psbt::from_str(&refund_response.checkpoint)
967 .map_err(Error::ad_hoc)
968 .context("could not parse refund checkpoint PSBT")?;
969
970 let ark_txid = boltz_signed_ark_tx.unsigned_tx.compute_txid();
971
972 let boltz_tap_script_sigs = boltz_signed_checkpoint
974 .inputs
975 .first()
976 .ok_or_else(|| Error::ad_hoc("boltz checkpoint has no inputs"))?
977 .tap_script_sigs
978 .clone();
979
980 let res = self
983 .network_client()
984 .submit_offchain_transaction_request(boltz_signed_ark_tx, vec![boltz_signed_checkpoint])
985 .await?;
986
987 let mut server_signed_checkpoint = res
990 .signed_checkpoint_txs
991 .first()
992 .ok_or_else(|| Error::ad_hoc("no signed checkpoint PSBTs returned"))?
993 .clone();
994
995 let kp = self.keypair_by_pk(&refunder_pk)?;
996 let sign_fn =
997 |_: &mut psbt::Input,
998 msg: secp256k1::Message|
999 -> Result<Vec<(schnorr::Signature, XOnlyPublicKey)>, ark_core::Error> {
1000 let sig = Secp256k1::new().sign_schnorr_no_aux_rand(&msg, &kp);
1001 let pk = kp.x_only_public_key().0;
1002
1003 Ok(vec![(sig, pk)])
1004 };
1005
1006 server_signed_checkpoint
1007 .inputs
1008 .first_mut()
1009 .ok_or_else(|| Error::ad_hoc("server checkpoint has no inputs"))?
1010 .tap_script_sigs
1011 .extend(boltz_tap_script_sigs);
1012
1013 sign_checkpoint_transaction(sign_fn, &mut server_signed_checkpoint)?;
1014
1015 timeout_op(
1017 self.inner.timeout,
1018 self.network_client()
1019 .finalize_offchain_transaction(ark_txid, vec![server_signed_checkpoint]),
1020 )
1021 .await?
1022 .map_err(Error::ark_server)
1023 .context("failed to finalize offchain transaction")?;
1024
1025 tracing::info!(swap_id, txid = %ark_txid, "Refunded VHTLC via collaborative refund");
1026
1027 Ok(ark_txid)
1028 }
1029
1030 pub async fn get_ln_invoice(
1044 &self,
1045 amount: SwapAmount,
1046 expiry_secs: Option<u64>,
1047 ) -> Result<ReverseSwapResult, Error> {
1048 let preimage: [u8; 32] = rand::random();
1049 let preimage_hash_sha256 = sha256::Hash::hash(&preimage);
1050 let preimage_hash = ripemd160::Hash::hash(preimage_hash_sha256.as_byte_array());
1051
1052 let claim_keypair = self.next_keypair(crate::key_provider::KeypairIndex::New)?;
1053 let claim_public_key = claim_keypair.public_key();
1054 let key_derivation_index =
1055 self.derivation_index_for_pk(&claim_keypair.x_only_public_key().0);
1056
1057 let (invoice_amount, onchain_amount) = match amount {
1058 SwapAmount::Invoice(amount) => (Some(amount), None),
1059 SwapAmount::Vhtlc(amount) => (None, Some(amount)),
1060 };
1061
1062 let request = CreateReverseSwapRequest {
1063 from: Asset::Btc,
1064 to: Asset::Ark,
1065 invoice_amount,
1066 onchain_amount,
1067 claim_public_key: claim_public_key.into(),
1068 preimage_hash: preimage_hash_sha256,
1069 invoice_expiry: expiry_secs,
1070 };
1071
1072 let url = format!("{}/v2/swap/reverse", self.inner.boltz_url);
1073
1074 let client = reqwest::Client::new();
1075 let response = client
1076 .post(&url)
1077 .json(&request)
1078 .send()
1079 .await
1080 .map_err(|e| Error::ad_hoc(e.to_string()))
1081 .context("failed to send reverse swap request")?;
1082
1083 if !response.status().is_success() {
1084 let error_text = response
1085 .text()
1086 .await
1087 .map_err(|e| Error::ad_hoc(e.to_string()))
1088 .context("failed to read error text")?;
1089
1090 return Err(Error::ad_hoc(format!(
1091 "failed to create reverse swap: {error_text}"
1092 )));
1093 }
1094
1095 let response: CreateReverseSwapResponse = response
1096 .json()
1097 .await
1098 .map_err(|e| Error::ad_hoc(e.to_string()))
1099 .context("failed to deserialize reverse swap response")?;
1100
1101 let created_at = SystemTime::now()
1102 .duration_since(UNIX_EPOCH)
1103 .map_err(Error::ad_hoc)
1104 .context("failed to compute created_at")?;
1105
1106 let swap_amount = response.onchain_amount.or(onchain_amount).ok_or_else(|| {
1107 Error::ad_hoc("onchain_amount not provided by Boltz and not specified in request")
1108 })?;
1109
1110 let swap = ReverseSwapData {
1111 id: response.id.clone(),
1112 status: SwapStatus::Created,
1113 preimage: Some(preimage),
1114 vhtlc_address: response.lockup_address,
1115 preimage_hash,
1116 refund_public_key: response.refund_public_key,
1117 amount: swap_amount,
1118 claim_public_key: claim_public_key.into(),
1119 timeout_block_heights: response.timeout_block_heights,
1120 created_at: created_at.as_secs(),
1121 key_derivation_index,
1122 bolt11: response.invoice.to_string(),
1123 invoice_expiry: response.invoice.expiry_time().as_secs(),
1124 };
1125
1126 self.swap_storage()
1127 .insert_reverse(response.id.clone(), swap.clone())
1128 .await
1129 .context("failed to persist swap data")?;
1130
1131 Ok(ReverseSwapResult {
1132 swap_id: swap.id,
1133 invoice: response.invoice,
1134 amount: swap_amount,
1135 })
1136 }
1137
1138 pub async fn get_ln_invoice_from_hash(
1158 &self,
1159 amount: SwapAmount,
1160 expiry_secs: Option<u64>,
1161 preimage_hash_sha256: sha256::Hash,
1162 ) -> Result<ReverseSwapResult, Error> {
1163 let preimage_hash = ripemd160::Hash::hash(preimage_hash_sha256.as_byte_array());
1164
1165 let claim_keypair = self.next_keypair(crate::key_provider::KeypairIndex::New)?;
1166 let claim_public_key = claim_keypair.public_key();
1167 let key_derivation_index =
1168 self.derivation_index_for_pk(&claim_keypair.x_only_public_key().0);
1169
1170 let (invoice_amount, onchain_amount) = match amount {
1171 SwapAmount::Invoice(amount) => (Some(amount), None),
1172 SwapAmount::Vhtlc(amount) => (None, Some(amount)),
1173 };
1174
1175 let request = CreateReverseSwapRequest {
1176 from: Asset::Btc,
1177 to: Asset::Ark,
1178 invoice_amount,
1179 onchain_amount,
1180 claim_public_key: claim_public_key.into(),
1181 preimage_hash: preimage_hash_sha256,
1182 invoice_expiry: expiry_secs,
1183 };
1184
1185 let url = format!("{}/v2/swap/reverse", self.inner.boltz_url);
1186
1187 let client = reqwest::Client::new();
1188 let response = client
1189 .post(&url)
1190 .json(&request)
1191 .send()
1192 .await
1193 .map_err(|e| Error::ad_hoc(e.to_string()))
1194 .context("failed to send reverse swap request")?;
1195
1196 if !response.status().is_success() {
1197 let error_text = response
1198 .text()
1199 .await
1200 .map_err(|e| Error::ad_hoc(e.to_string()))
1201 .context("failed to read error text")?;
1202
1203 return Err(Error::ad_hoc(format!(
1204 "failed to create reverse swap: {error_text}"
1205 )));
1206 }
1207
1208 let response: CreateReverseSwapResponse = response
1209 .json()
1210 .await
1211 .map_err(|e| Error::ad_hoc(e.to_string()))
1212 .context("failed to deserialize reverse swap response")?;
1213
1214 let created_at = SystemTime::now()
1215 .duration_since(UNIX_EPOCH)
1216 .map_err(Error::ad_hoc)
1217 .context("failed to compute created_at")?;
1218
1219 let swap_amount = response.onchain_amount.or(onchain_amount).ok_or_else(|| {
1220 Error::ad_hoc("onchain_amount not provided by Boltz and not specified in request")
1221 })?;
1222
1223 let swap = ReverseSwapData {
1224 id: response.id.clone(),
1225 status: SwapStatus::Created,
1226 preimage: None, vhtlc_address: response.lockup_address,
1228 preimage_hash,
1229 refund_public_key: response.refund_public_key,
1230 amount: swap_amount,
1231 claim_public_key: claim_public_key.into(),
1232 timeout_block_heights: response.timeout_block_heights,
1233 created_at: created_at.as_secs(),
1234 key_derivation_index,
1235 bolt11: response.invoice.to_string(),
1236 invoice_expiry: response.invoice.expiry_time().as_secs(),
1237 };
1238
1239 self.swap_storage()
1240 .insert_reverse(response.id.clone(), swap.clone())
1241 .await
1242 .context("failed to persist swap data")?;
1243
1244 Ok(ReverseSwapResult {
1245 swap_id: swap.id,
1246 invoice: response.invoice,
1247 amount: swap_amount,
1248 })
1249 }
1250
1251 pub async fn wait_for_vhtlc_funding(&self, swap_id: &str) -> Result<(), Error> {
1264 use futures::StreamExt;
1265
1266 let stream = self.subscribe_to_swap_updates(swap_id.to_string());
1267 tokio::pin!(stream);
1268
1269 while let Some(status_result) = stream.next().await {
1270 match status_result {
1271 Ok(status) => {
1272 tracing::debug!(swap_id, current = ?status, "Swap status");
1273
1274 match status {
1275 SwapStatus::TransactionMempool | SwapStatus::TransactionConfirmed => {
1276 tracing::debug!(swap_id, "VHTLC funding detected");
1277 return Ok(());
1278 }
1279 SwapStatus::InvoiceExpired => {
1280 return Err(Error::ad_hoc(format!(
1281 "invoice expired for swap {swap_id}"
1282 )));
1283 }
1284 SwapStatus::Error { error } => {
1285 tracing::error!(
1286 swap_id,
1287 "Got error from swap updates subscription: {error}"
1288 );
1289 }
1290 SwapStatus::Created
1292 | SwapStatus::TransactionRefunded
1293 | SwapStatus::TransactionFailed
1294 | SwapStatus::TransactionClaimed
1295 | SwapStatus::TransactionLockupFailed
1296 | SwapStatus::TransactionServerMempool
1297 | SwapStatus::TransactionServerConfirmed
1298 | SwapStatus::InvoiceSet
1299 | SwapStatus::InvoicePending
1300 | SwapStatus::InvoicePaid
1301 | SwapStatus::InvoiceFailedToPay
1302 | SwapStatus::SwapExpired
1303 | SwapStatus::Other(_) => {}
1304 }
1305 }
1306 Err(e) => return Err(e),
1307 }
1308 }
1309
1310 Err(Error::ad_hoc("Status stream ended unexpectedly"))
1311 }
1312
1313 pub async fn claim_vhtlc(
1327 &self,
1328 swap_id: &str,
1329 preimage: [u8; 32],
1330 ) -> Result<ClaimVhtlcResult, Error> {
1331 let swap = self
1332 .swap_storage()
1333 .get_reverse(swap_id)
1334 .await
1335 .context("failed to get reverse swap data")?
1336 .ok_or_else(|| Error::ad_hoc(format!("reverse swap data not found: {swap_id}")))?;
1337
1338 let preimage_hash_sha256 = sha256::Hash::hash(&preimage);
1340 let preimage_hash = ripemd160::Hash::hash(preimage_hash_sha256.as_byte_array());
1341
1342 if preimage_hash != swap.preimage_hash {
1343 return Err(Error::ad_hoc(format!(
1344 "preimage does not match stored hash for swap {swap_id}"
1345 )));
1346 }
1347
1348 tracing::debug!(swap_id, "Claiming VHTLC with verified preimage");
1349
1350 let timeout_block_heights = swap.timeout_block_heights;
1351
1352 let vhtlc = VhtlcScript::new(
1353 VhtlcOptions {
1354 sender: swap.refund_public_key.into(),
1355 receiver: swap.claim_public_key.into(),
1356 server: self.server_info.signer_pk.into(),
1357 preimage_hash: swap.preimage_hash,
1358 refund_locktime: timeout_block_heights.refund,
1359 unilateral_claim_delay: parse_sequence_number(
1360 timeout_block_heights.unilateral_claim as i64,
1361 )
1362 .map_err(|e| Error::ad_hoc(format!("invalid unilateral claim timeout: {e}")))?,
1363 unilateral_refund_delay: parse_sequence_number(
1364 timeout_block_heights.unilateral_refund as i64,
1365 )
1366 .map_err(|e| Error::ad_hoc(format!("invalid unilateral refund timeout: {e}")))?,
1367 unilateral_refund_without_receiver_delay: parse_sequence_number(
1368 timeout_block_heights.unilateral_refund_without_receiver as i64,
1369 )
1370 .map_err(|e| {
1371 Error::ad_hoc(format!("invalid refund without receiver timeout: {e}"))
1372 })?,
1373 },
1374 self.server_info.network,
1375 )
1376 .map_err(Error::ad_hoc)
1377 .context("failed to build VHTLC script")?;
1378
1379 let vhtlc_address = vhtlc.address();
1380 if vhtlc_address != swap.vhtlc_address {
1381 return Err(Error::ad_hoc(format!(
1382 "VHTLC address ({vhtlc_address}) does not match swap address ({})",
1383 swap.vhtlc_address
1384 )));
1385 }
1386
1387 let vhtlc_outpoint = {
1389 let virtual_tx_outpoints = self
1390 .get_virtual_tx_outpoints(std::iter::once(vhtlc_address))
1391 .await?;
1392
1393 let vtxo_list = VtxoList::new(self.server_info.dust, virtual_tx_outpoints);
1394
1395 let mut unspent = vtxo_list.all_unspent();
1397 let vhtlc_outpoint = unspent.next().ok_or_else(|| {
1398 Error::ad_hoc(format!("no outpoint found for address {vhtlc_address}"))
1399 })?;
1400
1401 vhtlc_outpoint.clone()
1402 };
1403
1404 let (claim_address, _) = self
1405 .get_offchain_address()
1406 .context("failed to get offchain address")?;
1407 let claim_amount = swap.amount;
1408
1409 let outputs = vec![SendReceiver {
1410 address: claim_address,
1411 amount: claim_amount,
1412 assets: Vec::new(),
1413 }];
1414
1415 let spend_info = vhtlc.taproot_spend_info();
1416 let script_ver = (vhtlc.claim_script(), LeafVersion::TapScript);
1417 let control_block = spend_info
1418 .control_block(&script_ver)
1419 .ok_or(Error::ad_hoc("control block not found for claim script"))?;
1420
1421 let script_pubkey = vhtlc.script_pubkey();
1422
1423 let claimer_pk = swap.claim_public_key.inner.x_only_public_key().0;
1424 let vhtlc_input = VtxoInput::new(
1425 script_ver.0,
1426 None,
1427 control_block,
1428 vhtlc.tapscripts(),
1429 script_pubkey,
1430 claim_amount,
1431 vhtlc_outpoint.outpoint,
1432 vhtlc_outpoint.assets,
1433 );
1434
1435 let change_address = &claim_address;
1437
1438 let OffchainTransactions {
1439 mut ark_tx,
1440 checkpoint_txs,
1441 } = build_offchain_transactions(
1442 &outputs,
1443 change_address,
1444 std::slice::from_ref(&vhtlc_input),
1445 &self.server_info,
1446 )
1447 .map_err(Error::from)
1448 .context("failed to build offchain TXs")?;
1449
1450 let kp = self.keypair_by_pk(&claimer_pk)?;
1451 let sign_fn =
1452 |input: &mut psbt::Input,
1453 msg: secp256k1::Message|
1454 -> Result<Vec<(schnorr::Signature, XOnlyPublicKey)>, ark_core::Error> {
1455 {
1457 let mut bytes = vec![1];
1459
1460 let length = VarInt::from(preimage.len() as u64);
1461
1462 length
1463 .consensus_encode(&mut bytes)
1464 .expect("valid length encoding");
1465
1466 bytes.write_all(&preimage).expect("valid preimage encoding");
1467
1468 input.unknown.insert(
1469 psbt::raw::Key {
1470 type_value: 222,
1471 key: VTXO_CONDITION_KEY.to_vec(),
1472 },
1473 bytes,
1474 );
1475 }
1476
1477 let sig = Secp256k1::new().sign_schnorr_no_aux_rand(&msg, &kp);
1478 let pk = kp.x_only_public_key().0;
1479
1480 Ok(vec![(sig, pk)])
1481 };
1482
1483 sign_ark_transaction(sign_fn, &mut ark_tx, 0)
1484 .map_err(Error::from)
1485 .context("failed to sign Ark TX")?;
1486
1487 let ark_txid = ark_tx.unsigned_tx.compute_txid();
1488
1489 let res = self
1490 .network_client()
1491 .submit_offchain_transaction_request(ark_tx, checkpoint_txs)
1492 .await
1493 .map_err(Error::from)
1494 .context("failed to submit offchain TXs")?;
1495
1496 let mut checkpoint_psbt = res
1497 .signed_checkpoint_txs
1498 .first()
1499 .ok_or_else(|| Error::ad_hoc("no checkpoint PSBTs found"))?
1500 .clone();
1501
1502 sign_checkpoint_transaction(sign_fn, &mut checkpoint_psbt)
1503 .map_err(Error::from)
1504 .context("failed to sign checkpoint TX")?;
1505
1506 timeout_op(
1507 self.inner.timeout,
1508 self.network_client()
1509 .finalize_offchain_transaction(ark_txid, vec![checkpoint_psbt]),
1510 )
1511 .await
1512 .context("failed to finalize offchain transaction")?
1513 .map_err(Error::ark_server)
1514 .context("failed to finalize offchain transaction")?;
1515
1516 tracing::info!(swap_id, txid = %ark_txid, "Claimed VHTLC");
1517
1518 let mut updated_swap = swap.clone();
1520 updated_swap.preimage = Some(preimage);
1521 self.swap_storage()
1522 .update_reverse(swap_id, updated_swap)
1523 .await
1524 .context("failed to update swap data with preimage")?;
1525
1526 Ok(ClaimVhtlcResult {
1527 swap_id: swap_id.to_string(),
1528 claim_txid: ark_txid,
1529 claim_amount,
1530 preimage,
1531 })
1532 }
1533
1534 pub async fn wait_for_vhtlc(&self, swap_id: &str) -> Result<ClaimVhtlcResult, Error> {
1542 use futures::StreamExt;
1543
1544 let swap = self
1545 .swap_storage()
1546 .get_reverse(swap_id)
1547 .await
1548 .context("failed to get reverse swap data")?
1549 .ok_or_else(|| Error::ad_hoc(format!("reverse swap data not found: {swap_id}")))?;
1550
1551 let preimage = swap.preimage.ok_or_else(|| {
1553 Error::ad_hoc(format!(
1554 "preimage not found in storage for swap {swap_id}. \
1555 Use wait_for_vhtlc_funding and claim_vhtlc instead."
1556 ))
1557 })?;
1558
1559 let stream = self.subscribe_to_swap_updates(swap_id.to_string());
1560 tokio::pin!(stream);
1561
1562 while let Some(status_result) = stream.next().await {
1563 match status_result {
1564 Ok(status) => {
1565 tracing::debug!(current = ?status, "Swap status");
1566
1567 match status {
1568 SwapStatus::TransactionMempool | SwapStatus::TransactionConfirmed => break,
1569 SwapStatus::InvoiceExpired => {
1570 return Err(Error::ad_hoc(format!(
1571 "invoice expired for swap {swap_id}"
1572 )));
1573 }
1574 SwapStatus::Error { error } => {
1575 tracing::error!(
1576 swap_id,
1577 "Got error from swap updates subscription: {error}"
1578 );
1579 }
1580 SwapStatus::Created
1582 | SwapStatus::TransactionRefunded
1583 | SwapStatus::TransactionFailed
1584 | SwapStatus::TransactionClaimed
1585 | SwapStatus::TransactionLockupFailed
1586 | SwapStatus::TransactionServerMempool
1587 | SwapStatus::TransactionServerConfirmed
1588 | SwapStatus::InvoiceSet
1589 | SwapStatus::InvoicePending
1590 | SwapStatus::InvoicePaid
1591 | SwapStatus::InvoiceFailedToPay
1592 | SwapStatus::SwapExpired
1593 | SwapStatus::Other(_) => {}
1594 }
1595 }
1596 Err(e) => return Err(e),
1597 }
1598 }
1599
1600 tracing::debug!("Ark transaction for swap found");
1601
1602 let timeout_block_heights = swap.timeout_block_heights;
1603
1604 let vhtlc = VhtlcScript::new(
1605 VhtlcOptions {
1606 sender: swap.refund_public_key.into(),
1607 receiver: swap.claim_public_key.into(),
1608 server: self.server_info.signer_pk.into(),
1609 preimage_hash: swap.preimage_hash,
1610 refund_locktime: timeout_block_heights.refund,
1611 unilateral_claim_delay: parse_sequence_number(
1612 timeout_block_heights.unilateral_claim as i64,
1613 )
1614 .map_err(|e| Error::ad_hoc(format!("invalid unilateral claim timeout: {e}")))?,
1615 unilateral_refund_delay: parse_sequence_number(
1616 timeout_block_heights.unilateral_refund as i64,
1617 )
1618 .map_err(|e| Error::ad_hoc(format!("invalid unilateral refund timeout: {e}")))?,
1619 unilateral_refund_without_receiver_delay: parse_sequence_number(
1620 timeout_block_heights.unilateral_refund_without_receiver as i64,
1621 )
1622 .map_err(|e| {
1623 Error::ad_hoc(format!("invalid refund without receiver timeout: {e}"))
1624 })?,
1625 },
1626 self.server_info.network,
1627 )
1628 .map_err(Error::ad_hoc)
1629 .context("failed to build VHTLC script")?;
1630
1631 let vhtlc_address = vhtlc.address();
1632 if vhtlc_address != swap.vhtlc_address {
1633 return Err(Error::ad_hoc(format!(
1634 "VHTLC address ({vhtlc_address}) does not match swap address ({})",
1635 swap.vhtlc_address
1636 )));
1637 }
1638
1639 let vhtlc_outpoint = {
1641 let virtual_tx_outpoints = self
1642 .get_virtual_tx_outpoints(std::iter::once(vhtlc_address))
1643 .await?;
1644
1645 let vtxo_list = VtxoList::new(self.server_info.dust, virtual_tx_outpoints);
1646
1647 let mut unspent = vtxo_list.all_unspent();
1649 let vhtlc_outpoint = unspent.next().ok_or_else(|| {
1650 Error::ad_hoc(format!("no outpoint found for address {vhtlc_address}"))
1651 })?;
1652
1653 vhtlc_outpoint.clone()
1654 };
1655
1656 let (claim_address, _) = self
1657 .get_offchain_address()
1658 .context("failed to get offchain address")?;
1659 let claim_amount = swap.amount;
1660
1661 let outputs = vec![SendReceiver {
1662 address: claim_address,
1663 amount: claim_amount,
1664 assets: Vec::new(),
1665 }];
1666
1667 let spend_info = vhtlc.taproot_spend_info();
1668 let script_ver = (vhtlc.claim_script(), LeafVersion::TapScript);
1669 let control_block = spend_info
1670 .control_block(&script_ver)
1671 .ok_or(Error::ad_hoc("control block not found for claim script"))?;
1672
1673 let script_pubkey = vhtlc.script_pubkey();
1674
1675 let claimer_pk = swap.claim_public_key.inner.x_only_public_key().0;
1676 let vhtlc_input = VtxoInput::new(
1677 script_ver.0,
1678 None,
1679 control_block,
1680 vhtlc.tapscripts(),
1681 script_pubkey,
1682 claim_amount,
1683 vhtlc_outpoint.outpoint,
1684 vhtlc_outpoint.assets,
1685 );
1686
1687 let change_address = &claim_address;
1689
1690 let OffchainTransactions {
1691 mut ark_tx,
1692 checkpoint_txs,
1693 } = build_offchain_transactions(
1694 &outputs,
1695 change_address,
1696 std::slice::from_ref(&vhtlc_input),
1697 &self.server_info,
1698 )
1699 .map_err(Error::from)
1700 .context("failed to build offchain TXs")?;
1701
1702 let kp = self.keypair_by_pk(&claimer_pk)?;
1703 let sign_fn =
1704 |input: &mut psbt::Input,
1705 msg: secp256k1::Message|
1706 -> Result<Vec<(schnorr::Signature, XOnlyPublicKey)>, ark_core::Error> {
1707 {
1709 let mut bytes = vec![1];
1711
1712 let length = VarInt::from(preimage.len() as u64);
1713
1714 length
1715 .consensus_encode(&mut bytes)
1716 .expect("valid length encoding");
1717
1718 bytes.write_all(&preimage).expect("valid preimage encoding");
1719
1720 input.unknown.insert(
1721 psbt::raw::Key {
1722 type_value: 222,
1723 key: VTXO_CONDITION_KEY.to_vec(),
1724 },
1725 bytes,
1726 );
1727 }
1728
1729 let sig = Secp256k1::new().sign_schnorr_no_aux_rand(&msg, &kp);
1730 let pk = kp.x_only_public_key().0;
1731
1732 Ok(vec![(sig, pk)])
1733 };
1734
1735 sign_ark_transaction(sign_fn, &mut ark_tx, 0)
1736 .map_err(Error::from)
1737 .context("failed to sign Ark TX")?;
1738
1739 let ark_txid = ark_tx.unsigned_tx.compute_txid();
1740
1741 let res = self
1742 .network_client()
1743 .submit_offchain_transaction_request(ark_tx, checkpoint_txs)
1744 .await
1745 .map_err(Error::from)
1746 .context("failed to submit offchain TXs")?;
1747
1748 let mut checkpoint_psbt = res
1749 .signed_checkpoint_txs
1750 .first()
1751 .ok_or_else(|| Error::ad_hoc("no checkpoint PSBTs found"))?
1752 .clone();
1753
1754 sign_checkpoint_transaction(sign_fn, &mut checkpoint_psbt)
1755 .map_err(Error::from)
1756 .context("failed to sign checkpoint TX")?;
1757
1758 timeout_op(
1759 self.inner.timeout,
1760 self.network_client()
1761 .finalize_offchain_transaction(ark_txid, vec![checkpoint_psbt]),
1762 )
1763 .await
1764 .context("failed to finalize offchain transaction")?
1765 .map_err(Error::ark_server)
1766 .context("failed to finalize offchain transaction")?;
1767
1768 tracing::info!(txid = %ark_txid, "Spent VHTLC");
1769
1770 Ok(ClaimVhtlcResult {
1771 swap_id: swap_id.to_string(),
1772 claim_txid: ark_txid,
1773 claim_amount,
1774 preimage,
1775 })
1776 }
1777
1778 pub async fn create_chain_swap(
1790 &self,
1791 direction: ChainSwapDirection,
1792 amount: ChainSwapAmount,
1793 ) -> Result<ChainSwapResult, Error> {
1794 let preimage: [u8; 32] = rand::random();
1795 let preimage_hash = sha256::Hash::hash(&preimage);
1796
1797 let claim_keypair = self.next_keypair(crate::key_provider::KeypairIndex::New)?;
1798 let claim_public_key = claim_keypair.public_key();
1799 let claim_key_derivation_index =
1800 self.derivation_index_for_pk(&claim_keypair.x_only_public_key().0);
1801
1802 let refund_keypair = self.next_keypair(crate::key_provider::KeypairIndex::New)?;
1803 let refund_public_key = refund_keypair.public_key();
1804 let refund_key_derivation_index =
1805 self.derivation_index_for_pk(&refund_keypair.x_only_public_key().0);
1806
1807 let (from, to) = match &direction {
1808 ChainSwapDirection::ArkToBtc => (Asset::Ark, Asset::Btc),
1809 ChainSwapDirection::BtcToArk => (Asset::Btc, Asset::Ark),
1810 };
1811
1812 let (user_lock_amount, server_lock_amount) = match &amount {
1813 ChainSwapAmount::UserLock(a) => (Some(*a), None),
1814 ChainSwapAmount::ServerLock(a) => (None, Some(*a)),
1815 };
1816
1817 let request = CreateChainSwapRequest {
1818 from,
1819 to,
1820 user_lock_amount,
1821 server_lock_amount,
1822 claim_public_key: claim_public_key.into(),
1823 refund_public_key: refund_public_key.into(),
1824 preimage_hash,
1825 };
1826
1827 let url = format!("{}/v2/swap/chain", self.inner.boltz_url);
1828
1829 let client = reqwest::Client::new();
1830 let response = client
1831 .post(&url)
1832 .json(&request)
1833 .send()
1834 .await
1835 .map_err(|e| Error::ad_hoc(e.to_string()))
1836 .context("failed to send chain swap request")?;
1837
1838 if !response.status().is_success() {
1839 let error_text = response
1840 .text()
1841 .await
1842 .map_err(|e| Error::ad_hoc(e.to_string()))
1843 .context("failed to read error text")?;
1844
1845 return Err(Error::ad_hoc(format!(
1846 "failed to create chain swap: {error_text}"
1847 )));
1848 }
1849
1850 let swap_response: CreateChainSwapResponse = response
1851 .json()
1852 .await
1853 .map_err(|e| Error::ad_hoc(e.to_string()))
1854 .context("failed to deserialize chain swap response")?;
1855
1856 let created_at = SystemTime::now()
1857 .duration_since(UNIX_EPOCH)
1858 .map_err(Error::ad_hoc)
1859 .context("failed to compute created_at")?;
1860
1861 let bip21 = swap_response
1866 .lockup_details
1867 .bip21
1868 .or(swap_response.claim_details.bip21.clone());
1869
1870 let swap_tree = swap_response
1871 .lockup_details
1872 .swap_tree
1873 .or(swap_response.claim_details.swap_tree.clone());
1874
1875 let data = ChainSwapData {
1876 id: swap_response.id.clone(),
1877 status: SwapStatus::Created,
1878 direction,
1879 preimage: Some(preimage),
1880 preimage_hash,
1881 claim_public_key: claim_public_key.into(),
1882 refund_public_key: refund_public_key.into(),
1883 server_claim_public_key: swap_response.lockup_details.server_public_key,
1884 server_refund_public_key: swap_response.claim_details.server_public_key,
1885 user_lockup_address: swap_response.lockup_details.lockup_address,
1886 server_lockup_address: swap_response.claim_details.lockup_address,
1887 user_lockup_amount: swap_response.lockup_details.amount,
1888 server_lockup_amount: swap_response.claim_details.amount,
1889 user_timeout_block_height: swap_response.lockup_details.timeout_block_height,
1890 server_timeout_block_height: swap_response.claim_details.timeout_block_height,
1891 user_timeout_block_heights: swap_response.lockup_details.timeouts,
1892 server_timeout_block_heights: swap_response.claim_details.timeouts,
1893 bip21,
1894 swap_tree,
1895 created_at: created_at.as_secs(),
1896 claim_key_derivation_index,
1897 refund_key_derivation_index,
1898 };
1899
1900 self.swap_storage()
1901 .insert_chain(swap_response.id.clone(), data.clone())
1902 .await?;
1903
1904 tracing::info!(
1905 swap_id = swap_response.id,
1906 direction = ?data.direction,
1907 user_lockup_address = %data.user_lockup_address,
1908 user_lockup_amount = %data.user_lockup_amount,
1909 server_lockup_amount = %data.server_lockup_amount,
1910 "Created chain swap"
1911 );
1912
1913 Ok(ChainSwapResult {
1914 swap_id: swap_response.id,
1915 user_lockup_address: data.user_lockup_address,
1916 user_lockup_amount: data.user_lockup_amount,
1917 server_lockup_amount: data.server_lockup_amount,
1918 bip21: data.bip21,
1919 })
1920 }
1921
1922 pub async fn wait_for_chain_swap_server_lockup(
1929 &self,
1930 swap_id: &str,
1931 ) -> Result<Option<String>, Error> {
1932 use futures::StreamExt;
1933
1934 let stream = self.subscribe_to_swap_updates(swap_id.to_string());
1935 tokio::pin!(stream);
1936
1937 while let Some(status_result) = stream.next().await {
1938 match status_result {
1939 Ok(status) => {
1940 tracing::debug!(swap_id, current = ?status, "Chain swap status");
1941 match status {
1942 SwapStatus::TransactionServerMempool
1943 | SwapStatus::TransactionServerConfirmed => {
1944 let url = format!("{}/v2/swap/{swap_id}", self.inner.boltz_url);
1946 let txid = async {
1947 reqwest::Client::new()
1948 .get(&url)
1949 .send()
1950 .await
1951 .ok()?
1952 .json::<GetSwapStatusResponse>()
1953 .await
1954 .ok()?
1955 .transaction
1956 .map(|t| t.id)
1957 }
1958 .await;
1959
1960 tracing::info!(
1961 swap_id,
1962 server_lockup_txid = txid.as_deref().unwrap_or("unknown"),
1963 "Server lockup detected"
1964 );
1965 return Ok(txid);
1966 }
1967 SwapStatus::SwapExpired => {
1968 return Err(Error::ad_hoc(format!("chain swap expired: {swap_id}")));
1969 }
1970 SwapStatus::TransactionRefunded | SwapStatus::TransactionFailed => {
1971 return Err(Error::ad_hoc(format!(
1972 "chain swap failed or refunded: {swap_id}"
1973 )));
1974 }
1975 SwapStatus::Error { error } => {
1976 tracing::error!(swap_id, "Got error from chain swap updates: {error}");
1977 }
1978 SwapStatus::Created
1980 | SwapStatus::TransactionMempool
1981 | SwapStatus::TransactionConfirmed
1982 | SwapStatus::TransactionClaimed
1983 | SwapStatus::TransactionLockupFailed
1984 | SwapStatus::InvoiceSet
1985 | SwapStatus::InvoicePending
1986 | SwapStatus::InvoicePaid
1987 | SwapStatus::InvoiceFailedToPay
1988 | SwapStatus::InvoiceExpired
1989 | SwapStatus::Other(_) => {}
1990 }
1991 }
1992 Err(e) => return Err(e),
1993 }
1994 }
1995
1996 Err(Error::ad_hoc("Chain swap status stream ended unexpectedly"))
1997 }
1998
1999 pub async fn claim_chain_swap(&self, swap_id: &str) -> Result<Txid, Error> {
2006 let swap = self
2007 .swap_storage()
2008 .get_chain(swap_id)
2009 .await
2010 .context("failed to get chain swap data")?
2011 .ok_or_else(|| Error::ad_hoc(format!("chain swap data not found: {swap_id}")))?;
2012
2013 let preimage = swap
2014 .preimage
2015 .ok_or_else(|| Error::ad_hoc(format!("preimage not found for chain swap {swap_id}")))?;
2016
2017 let preimage_hash = ripemd160::Hash::hash(swap.preimage_hash.as_byte_array());
2018
2019 let timeout_block_heights = swap.server_timeout_block_heights.ok_or_else(|| {
2020 Error::ad_hoc(format!(
2021 "chain swap {swap_id} has no ARK-side VHTLC timeouts on server lockup \
2022 (this swap's server lockup is on-chain BTC, not an Ark VHTLC)"
2023 ))
2024 })?;
2025
2026 let vhtlc = VhtlcScript::new(
2027 VhtlcOptions {
2028 sender: swap.server_refund_public_key.into(),
2029 receiver: swap.claim_public_key.into(),
2030 server: self.server_info.signer_pk.into(),
2031 preimage_hash,
2032 refund_locktime: timeout_block_heights.refund,
2033 unilateral_claim_delay: parse_sequence_number(
2034 timeout_block_heights.unilateral_claim as i64,
2035 )
2036 .map_err(|e| Error::ad_hoc(format!("invalid unilateral claim timeout: {e}")))?,
2037 unilateral_refund_delay: parse_sequence_number(
2038 timeout_block_heights.unilateral_refund as i64,
2039 )
2040 .map_err(|e| Error::ad_hoc(format!("invalid unilateral refund timeout: {e}")))?,
2041 unilateral_refund_without_receiver_delay: parse_sequence_number(
2042 timeout_block_heights.unilateral_refund_without_receiver as i64,
2043 )
2044 .map_err(|e| {
2045 Error::ad_hoc(format!("invalid refund without receiver timeout: {e}"))
2046 })?,
2047 },
2048 self.server_info.network,
2049 )
2050 .map_err(Error::ad_hoc)
2051 .context("failed to build VHTLC script")?;
2052
2053 let vhtlc_address = vhtlc.address();
2054 let expected_address = ArkAddress::decode(&swap.server_lockup_address)
2055 .map_err(|e| Error::ad_hoc(format!("invalid server lockup address: {e}")))?;
2056
2057 if vhtlc_address != expected_address {
2058 return Err(Error::ad_hoc(format!(
2059 "VHTLC address ({vhtlc_address}) does not match server lockup address ({expected_address})"
2060 )));
2061 }
2062
2063 let vhtlc_outpoint = {
2064 let virtual_tx_outpoints = self
2065 .get_virtual_tx_outpoints(std::iter::once(vhtlc_address))
2066 .await?;
2067
2068 let vtxo_list = VtxoList::new(self.server_info.dust, virtual_tx_outpoints);
2069
2070 let mut unspent = vtxo_list.all_unspent();
2071 let vhtlc_outpoint = unspent.next().ok_or_else(|| {
2072 Error::ad_hoc(format!("no outpoint found for address {vhtlc_address}"))
2073 })?;
2074
2075 vhtlc_outpoint.clone()
2076 };
2077
2078 let (claim_address, _) = self
2079 .get_offchain_address()
2080 .context("failed to get offchain address")?;
2081 let claim_amount = swap.server_lockup_amount;
2082
2083 let outputs = vec![SendReceiver::bitcoin(claim_address, claim_amount)];
2084
2085 let spend_info = vhtlc.taproot_spend_info();
2086 let script_ver = (vhtlc.claim_script(), LeafVersion::TapScript);
2087 let control_block = spend_info
2088 .control_block(&script_ver)
2089 .ok_or(Error::ad_hoc("control block not found for claim script"))?;
2090
2091 let script_pubkey = vhtlc.script_pubkey();
2092
2093 let claimer_pk = swap.claim_public_key.inner.x_only_public_key().0;
2094 let vhtlc_input = VtxoInput::new(
2095 script_ver.0,
2096 None,
2097 control_block,
2098 vhtlc.tapscripts(),
2099 script_pubkey,
2100 claim_amount,
2101 vhtlc_outpoint.outpoint,
2102 vhtlc_outpoint.assets,
2103 );
2104
2105 let change_address = &claim_address;
2107
2108 let OffchainTransactions {
2109 mut ark_tx,
2110 checkpoint_txs,
2111 } = build_offchain_transactions(
2112 &outputs,
2113 change_address,
2114 std::slice::from_ref(&vhtlc_input),
2115 &self.server_info,
2116 )
2117 .map_err(Error::from)
2118 .context("failed to build offchain TXs")?;
2119
2120 let kp = self.keypair_by_pk(&claimer_pk)?;
2121 let sign_fn =
2122 |input: &mut psbt::Input,
2123 msg: secp256k1::Message|
2124 -> Result<Vec<(schnorr::Signature, XOnlyPublicKey)>, ark_core::Error> {
2125 {
2127 let mut bytes = vec![1];
2128
2129 let length = VarInt::from(preimage.len() as u64);
2130
2131 length
2132 .consensus_encode(&mut bytes)
2133 .expect("valid length encoding");
2134
2135 bytes.write_all(&preimage).expect("valid preimage encoding");
2136
2137 input.unknown.insert(
2138 psbt::raw::Key {
2139 type_value: 222,
2140 key: VTXO_CONDITION_KEY.to_vec(),
2141 },
2142 bytes,
2143 );
2144 }
2145
2146 let sig = Secp256k1::new().sign_schnorr_no_aux_rand(&msg, &kp);
2147 let pk = kp.x_only_public_key().0;
2148
2149 Ok(vec![(sig, pk)])
2150 };
2151
2152 sign_ark_transaction(sign_fn, &mut ark_tx, 0)
2153 .map_err(Error::from)
2154 .context("failed to sign Ark TX")?;
2155
2156 let ark_txid = ark_tx.unsigned_tx.compute_txid();
2157
2158 let res = self
2159 .network_client()
2160 .submit_offchain_transaction_request(ark_tx, checkpoint_txs)
2161 .await
2162 .map_err(Error::from)
2163 .context("failed to submit offchain TXs")?;
2164
2165 let mut checkpoint_psbt = res
2166 .signed_checkpoint_txs
2167 .first()
2168 .ok_or_else(|| Error::ad_hoc("no checkpoint PSBTs found"))?
2169 .clone();
2170
2171 sign_checkpoint_transaction(sign_fn, &mut checkpoint_psbt)
2172 .map_err(Error::from)
2173 .context("failed to sign checkpoint TX")?;
2174
2175 timeout_op(
2176 self.inner.timeout,
2177 self.network_client()
2178 .finalize_offchain_transaction(ark_txid, vec![checkpoint_psbt]),
2179 )
2180 .await
2181 .context("failed to finalize offchain transaction")?
2182 .map_err(Error::ark_server)
2183 .context("failed to finalize offchain transaction")?;
2184
2185 tracing::info!(swap_id, txid = %ark_txid, "Claimed chain swap VHTLC");
2186
2187 let mut updated_swap = swap.clone();
2188 updated_swap.status = SwapStatus::TransactionClaimed;
2189 self.swap_storage()
2190 .update_chain(swap_id, updated_swap)
2191 .await
2192 .context("failed to update chain swap data")?;
2193
2194 Ok(ark_txid)
2195 }
2196
2197 pub async fn claim_chain_swap_btc(
2204 &self,
2205 swap_id: &str,
2206 destination_address: bitcoin::Address,
2207 fee_rate_sat_vb: f64,
2208 ) -> Result<Txid, Error> {
2209 let swap = self
2210 .swap_storage()
2211 .get_chain(swap_id)
2212 .await
2213 .context("failed to get chain swap data")?
2214 .ok_or_else(|| Error::ad_hoc(format!("chain swap data not found: {swap_id}")))?;
2215
2216 let preimage = swap
2217 .preimage
2218 .ok_or_else(|| Error::ad_hoc(format!("preimage not found for chain swap {swap_id}")))?;
2219
2220 let swap_tree = swap.swap_tree.clone().ok_or_else(|| {
2221 Error::ad_hoc("no swap tree found (this swap has no on-chain BTC HTLC)")
2222 })?;
2223
2224 let btc_address_str = &swap.server_lockup_address;
2226
2227 let taproot_spend_info = reconstruct_btc_htlc(
2230 swap.server_refund_public_key,
2231 swap.claim_public_key,
2232 &swap_tree,
2233 )?;
2234
2235 let secp = Secp256k1::new();
2236
2237 let expected_spk = ScriptBuf::new_p2tr(
2239 &secp,
2240 taproot_spend_info.internal_key(),
2241 taproot_spend_info.merkle_root(),
2242 );
2243
2244 let parsed_address: bitcoin::Address<bitcoin::address::NetworkUnchecked> = btc_address_str
2245 .parse()
2246 .map_err(|e| Error::ad_hoc(format!("invalid BTC lockup address: {e}")))?;
2247 let parsed_address = parsed_address.assume_checked();
2248 let target_spk = parsed_address.script_pubkey();
2249
2250 if expected_spk != target_spk {
2251 return Err(Error::ad_hoc(format!(
2252 "taproot address mismatch for BTC lockup {btc_address_str}"
2253 )));
2254 }
2255
2256 let claim_script_bytes: Vec<u8> =
2257 bitcoin::hex::FromHex::from_hex(&swap_tree.claim_leaf.output)
2258 .map_err(|e| Error::ad_hoc(format!("invalid claim leaf hex: {e}")))?;
2259 let claim_script = ScriptBuf::from_bytes(claim_script_bytes);
2260 let claim_ver = (claim_script.clone(), LeafVersion::TapScript);
2261
2262 let utxos = self
2264 .inner
2265 .blockchain
2266 .find_outpoints(&parsed_address)
2267 .await
2268 .context("failed to find UTXOs at BTC lockup address")?;
2269
2270 let utxo = utxos.iter().find(|u| !u.is_spent).ok_or_else(|| {
2271 Error::ad_hoc(format!(
2272 "no unspent UTXO found at BTC lockup address {btc_address_str}"
2273 ))
2274 })?;
2275
2276 let control_block = taproot_spend_info
2278 .control_block(&claim_ver)
2279 .ok_or(Error::ad_hoc("control block not found for claim leaf"))?;
2280
2281 let cb_bytes = control_block.serialize();
2282 let witness_weight = 1 + 1 + 64 + 1 + 32 + 1 + claim_script.len() + 1 + cb_bytes.len() + 1;
2284 let weight = 4 * (11 + 41 + 43) + witness_weight;
2285 let vsize = weight.div_ceil(4);
2286 let fee = Amount::from_sat((vsize as f64 * fee_rate_sat_vb).ceil() as u64);
2287
2288 let claim_amount = utxo.amount.checked_sub(fee).ok_or_else(|| {
2289 Error::ad_hoc(format!(
2290 "UTXO amount {} is less than estimated fee {}",
2291 utxo.amount, fee
2292 ))
2293 })?;
2294
2295 let mut tx = bitcoin::Transaction {
2297 version: bitcoin::transaction::Version::TWO,
2298 lock_time: absolute::LockTime::ZERO,
2299 input: vec![bitcoin::TxIn {
2300 previous_output: utxo.outpoint,
2301 script_sig: ScriptBuf::new(),
2302 sequence: bitcoin::Sequence::ENABLE_RBF_NO_LOCKTIME,
2303 witness: bitcoin::Witness::new(),
2304 }],
2305 output: vec![TxOut {
2306 value: claim_amount,
2307 script_pubkey: destination_address.script_pubkey(),
2308 }],
2309 };
2310
2311 let leaf_hash =
2313 bitcoin::taproot::TapLeafHash::from_script(&claim_script, LeafVersion::TapScript);
2314
2315 let prevouts = [TxOut {
2316 value: utxo.amount,
2317 script_pubkey: target_spk.clone(),
2318 }];
2319
2320 let sighash = bitcoin::sighash::SighashCache::new(&tx)
2321 .taproot_script_spend_signature_hash(
2322 0,
2323 &bitcoin::sighash::Prevouts::All(&prevouts),
2324 leaf_hash,
2325 bitcoin::TapSighashType::Default,
2326 )
2327 .map_err(|e| Error::ad_hoc(format!("failed to compute sighash: {e}")))?;
2328
2329 let msg = secp256k1::Message::from_digest(sighash.to_byte_array());
2330 let claim_kp = self.keypair_by_pk(&swap.claim_public_key.inner.x_only_public_key().0)?;
2331 let signature = secp.sign_schnorr_no_aux_rand(&msg, &claim_kp);
2332
2333 let mut witness = bitcoin::Witness::new();
2335 witness.push(signature.serialize());
2336 witness.push(preimage);
2337 witness.push(claim_script.as_bytes());
2338 witness.push(cb_bytes);
2339
2340 tx.input[0].witness = witness;
2341
2342 self.inner
2344 .blockchain
2345 .broadcast(&tx)
2346 .await
2347 .context("failed to broadcast BTC claim transaction")?;
2348
2349 let txid = tx.compute_txid();
2350
2351 tracing::info!(swap_id, %txid, %claim_amount, "Claimed on-chain BTC from chain swap");
2352
2353 let mut updated_swap = swap.clone();
2354 updated_swap.status = SwapStatus::TransactionClaimed;
2355 self.swap_storage()
2356 .update_chain(swap_id, updated_swap)
2357 .await
2358 .context("failed to update chain swap data")?;
2359
2360 Ok(txid)
2361 }
2362
2363 pub async fn refund_chain_swap(&self, swap_id: &str) -> Result<Txid, Error> {
2370 let swap = self
2371 .swap_storage()
2372 .get_chain(swap_id)
2373 .await
2374 .context("failed to get chain swap data")?
2375 .ok_or_else(|| Error::ad_hoc(format!("chain swap data not found: {swap_id}")))?;
2376
2377 let timeout_block_heights = swap.user_timeout_block_heights.ok_or_else(|| {
2378 Error::ad_hoc(
2379 "chain swap has no ARK-side VHTLC timeouts on user lockup \
2380 (user lockup is on-chain BTC, use refund_chain_swap_btc instead)",
2381 )
2382 })?;
2383
2384 let preimage_hash = ripemd160::Hash::hash(swap.preimage_hash.as_byte_array());
2385
2386 let vhtlc = VhtlcScript::new(
2388 VhtlcOptions {
2389 sender: swap.refund_public_key.into(),
2390 receiver: swap.server_claim_public_key.into(),
2391 server: self.server_info.signer_pk.into(),
2392 preimage_hash,
2393 refund_locktime: timeout_block_heights.refund,
2394 unilateral_claim_delay: parse_sequence_number(
2395 timeout_block_heights.unilateral_claim as i64,
2396 )
2397 .map_err(|e| Error::ad_hoc(format!("invalid unilateral claim timeout: {e}")))?,
2398 unilateral_refund_delay: parse_sequence_number(
2399 timeout_block_heights.unilateral_refund as i64,
2400 )
2401 .map_err(|e| Error::ad_hoc(format!("invalid unilateral refund timeout: {e}")))?,
2402 unilateral_refund_without_receiver_delay: parse_sequence_number(
2403 timeout_block_heights.unilateral_refund_without_receiver as i64,
2404 )
2405 .map_err(|e| {
2406 Error::ad_hoc(format!("invalid refund without receiver timeout: {e}"))
2407 })?,
2408 },
2409 self.server_info.network,
2410 )
2411 .map_err(Error::ad_hoc)?;
2412
2413 let vhtlc_address = vhtlc.address();
2414 let expected_address = ArkAddress::decode(&swap.user_lockup_address)
2415 .map_err(|e| Error::ad_hoc(format!("invalid user lockup address: {e}")))?;
2416
2417 if vhtlc_address != expected_address {
2418 return Err(Error::ad_hoc(format!(
2419 "VHTLC address ({vhtlc_address}) does not match user lockup address ({expected_address})"
2420 )));
2421 }
2422
2423 let vhtlc_outpoint = {
2424 let virtual_tx_outpoints = self
2425 .get_virtual_tx_outpoints(std::iter::once(vhtlc_address))
2426 .await?;
2427
2428 let vtxo_list = VtxoList::new(self.server_info.dust, virtual_tx_outpoints);
2429
2430 let mut unspent = vtxo_list.all_unspent();
2431 unspent
2432 .next()
2433 .ok_or_else(|| {
2434 Error::ad_hoc(format!("no outpoint found for address {vhtlc_address}"))
2435 })?
2436 .clone()
2437 };
2438
2439 let (refund_address, _) = self.get_offchain_address()?;
2440 let refund_amount = swap.user_lockup_amount;
2441
2442 let outputs = vec![SendReceiver::bitcoin(refund_address, refund_amount)];
2443
2444 let refund_script = vhtlc.refund_without_receiver_script();
2445 let spend_info = vhtlc.taproot_spend_info();
2446 let script_ver = (refund_script, LeafVersion::TapScript);
2447 let control_block = spend_info
2448 .control_block(&script_ver)
2449 .ok_or(Error::ad_hoc("control block not found for refund script"))?;
2450
2451 let script_pubkey = vhtlc.script_pubkey();
2452 let refunder_pk = swap.refund_public_key.inner.x_only_public_key().0;
2453
2454 let change_address = &refund_address;
2456
2457 let vhtlc_input = VtxoInput::new(
2458 script_ver.0,
2459 Some(absolute::LockTime::from_consensus(
2460 timeout_block_heights.refund,
2461 )),
2462 control_block,
2463 vhtlc.tapscripts(),
2464 script_pubkey,
2465 refund_amount,
2466 vhtlc_outpoint.outpoint,
2467 vhtlc_outpoint.assets,
2468 );
2469
2470 let OffchainTransactions {
2471 mut ark_tx,
2472 checkpoint_txs,
2473 } = build_offchain_transactions(
2474 &outputs,
2475 change_address,
2476 std::slice::from_ref(&vhtlc_input),
2477 &self.server_info,
2478 )?;
2479
2480 let kp = self.keypair_by_pk(&refunder_pk)?;
2481 let sign_fn =
2482 |_: &mut psbt::Input,
2483 msg: secp256k1::Message|
2484 -> Result<Vec<(schnorr::Signature, XOnlyPublicKey)>, ark_core::Error> {
2485 let sig = Secp256k1::new().sign_schnorr_no_aux_rand(&msg, &kp);
2486 let pk = kp.x_only_public_key().0;
2487 Ok(vec![(sig, pk)])
2488 };
2489
2490 sign_ark_transaction(sign_fn, &mut ark_tx, 0)?;
2491
2492 let ark_txid = ark_tx.unsigned_tx.compute_txid();
2493
2494 let res = self
2495 .network_client()
2496 .submit_offchain_transaction_request(ark_tx, checkpoint_txs)
2497 .await?;
2498
2499 let mut checkpoint_psbt = res
2500 .signed_checkpoint_txs
2501 .first()
2502 .ok_or_else(|| Error::ad_hoc("no checkpoint PSBTs found"))?
2503 .clone();
2504
2505 let kp = self.keypair_by_pk(&refunder_pk)?;
2506 let sign_fn =
2507 |_: &mut psbt::Input,
2508 msg: secp256k1::Message|
2509 -> Result<Vec<(schnorr::Signature, XOnlyPublicKey)>, ark_core::Error> {
2510 let sig = Secp256k1::new().sign_schnorr_no_aux_rand(&msg, &kp);
2511 let pk = kp.x_only_public_key().0;
2512 Ok(vec![(sig, pk)])
2513 };
2514
2515 sign_checkpoint_transaction(sign_fn, &mut checkpoint_psbt)?;
2516
2517 timeout_op(
2518 self.inner.timeout,
2519 self.network_client()
2520 .finalize_offchain_transaction(ark_txid, vec![checkpoint_psbt]),
2521 )
2522 .await?
2523 .map_err(Error::ark_server)
2524 .context("failed to finalize offchain transaction")?;
2525
2526 tracing::info!(swap_id, txid = %ark_txid, "Refunded chain swap Ark VHTLC");
2527
2528 let mut updated_swap = swap.clone();
2529 updated_swap.status = SwapStatus::TransactionRefunded;
2530 self.swap_storage()
2531 .update_chain(swap_id, updated_swap)
2532 .await
2533 .context("failed to update chain swap data")?;
2534
2535 Ok(ark_txid)
2536 }
2537
2538 pub async fn refund_chain_swap_btc(
2543 &self,
2544 swap_id: &str,
2545 destination_address: bitcoin::Address,
2546 fee_rate_sat_vb: f64,
2547 ) -> Result<Txid, Error> {
2548 let swap = self
2549 .swap_storage()
2550 .get_chain(swap_id)
2551 .await
2552 .context("failed to get chain swap data")?
2553 .ok_or_else(|| Error::ad_hoc(format!("chain swap data not found: {swap_id}")))?;
2554
2555 let swap_tree = swap.swap_tree.clone().ok_or_else(|| {
2556 Error::ad_hoc("no swap tree found (this swap has no on-chain BTC lockup)")
2557 })?;
2558
2559 let btc_address_str = &swap.user_lockup_address;
2561
2562 let taproot_spend_info = reconstruct_btc_htlc(
2565 swap.server_claim_public_key,
2566 swap.refund_public_key,
2567 &swap_tree,
2568 )?;
2569
2570 let secp = Secp256k1::new();
2571
2572 let refund_script_bytes: Vec<u8> =
2573 bitcoin::hex::FromHex::from_hex(&swap_tree.refund_leaf.output)
2574 .map_err(|e| Error::ad_hoc(format!("invalid refund leaf hex: {e}")))?;
2575 let refund_script = ScriptBuf::from_bytes(refund_script_bytes);
2576 let refund_ver = (refund_script.clone(), LeafVersion::TapScript);
2577
2578 let expected_spk = ScriptBuf::new_p2tr(
2580 &secp,
2581 taproot_spend_info.internal_key(),
2582 taproot_spend_info.merkle_root(),
2583 );
2584
2585 let parsed_address: bitcoin::Address<bitcoin::address::NetworkUnchecked> = btc_address_str
2586 .parse()
2587 .map_err(|e| Error::ad_hoc(format!("invalid BTC lockup address: {e}")))?;
2588 let parsed_address = parsed_address.assume_checked();
2589 let target_spk = parsed_address.script_pubkey();
2590
2591 if expected_spk != target_spk {
2592 return Err(Error::ad_hoc(format!(
2593 "taproot address mismatch for BTC lockup {btc_address_str}"
2594 )));
2595 }
2596
2597 let utxos = self
2599 .inner
2600 .blockchain
2601 .find_outpoints(&parsed_address)
2602 .await
2603 .context("failed to find UTXOs at BTC lockup address")?;
2604
2605 let utxo = utxos.iter().find(|u| !u.is_spent).ok_or_else(|| {
2606 Error::ad_hoc(format!(
2607 "no unspent UTXO found at BTC lockup address {btc_address_str}"
2608 ))
2609 })?;
2610
2611 let control_block = taproot_spend_info
2612 .control_block(&refund_ver)
2613 .ok_or(Error::ad_hoc("control block not found for refund leaf"))?;
2614
2615 let cb_bytes = control_block.serialize();
2616 let witness_weight = 1 + 1 + 64 + 1 + refund_script.len() + 1 + cb_bytes.len() + 1;
2617 let weight = 4 * (11 + 41 + 43) + witness_weight;
2618 let vsize = weight.div_ceil(4);
2619 let fee = Amount::from_sat((vsize as f64 * fee_rate_sat_vb).ceil() as u64);
2620
2621 let refund_amount = utxo.amount.checked_sub(fee).ok_or_else(|| {
2622 Error::ad_hoc(format!(
2623 "UTXO amount {} is less than estimated fee {}",
2624 utxo.amount, fee
2625 ))
2626 })?;
2627
2628 let lock_time = absolute::LockTime::from_consensus(swap.user_timeout_block_height);
2630
2631 let mut tx = bitcoin::Transaction {
2632 version: bitcoin::transaction::Version::TWO,
2633 lock_time,
2634 input: vec![bitcoin::TxIn {
2635 previous_output: utxo.outpoint,
2636 script_sig: ScriptBuf::new(),
2637 sequence: bitcoin::Sequence::ENABLE_LOCKTIME_NO_RBF,
2638 witness: bitcoin::Witness::new(),
2639 }],
2640 output: vec![TxOut {
2641 value: refund_amount,
2642 script_pubkey: destination_address.script_pubkey(),
2643 }],
2644 };
2645
2646 let leaf_hash =
2648 bitcoin::taproot::TapLeafHash::from_script(&refund_script, LeafVersion::TapScript);
2649
2650 let prevouts = [TxOut {
2651 value: utxo.amount,
2652 script_pubkey: target_spk,
2653 }];
2654
2655 let sighash = bitcoin::sighash::SighashCache::new(&tx)
2656 .taproot_script_spend_signature_hash(
2657 0,
2658 &bitcoin::sighash::Prevouts::All(&prevouts),
2659 leaf_hash,
2660 bitcoin::TapSighashType::Default,
2661 )
2662 .map_err(|e| Error::ad_hoc(format!("failed to compute sighash: {e}")))?;
2663
2664 let msg = secp256k1::Message::from_digest(sighash.to_byte_array());
2665 let refund_kp = self.keypair_by_pk(&swap.refund_public_key.inner.x_only_public_key().0)?;
2666 let signature = secp.sign_schnorr_no_aux_rand(&msg, &refund_kp);
2667
2668 let mut witness = bitcoin::Witness::new();
2670 witness.push(signature.serialize());
2671 witness.push(refund_script.as_bytes());
2672 witness.push(cb_bytes);
2673
2674 tx.input[0].witness = witness;
2675
2676 self.inner
2677 .blockchain
2678 .broadcast(&tx)
2679 .await
2680 .context("failed to broadcast BTC refund transaction")?;
2681
2682 let txid = tx.compute_txid();
2683
2684 tracing::info!(swap_id, %txid, %refund_amount, "Refunded on-chain BTC from chain swap");
2685
2686 let mut updated_swap = swap.clone();
2687 updated_swap.status = SwapStatus::TransactionRefunded;
2688 self.swap_storage()
2689 .update_chain(swap_id, updated_swap)
2690 .await
2691 .context("failed to update chain swap data")?;
2692
2693 Ok(txid)
2694 }
2695
2696 pub async fn get_swap_status(&self, swap_id: &str) -> Result<SwapStatusInfo, Error> {
2701 let swap_type = if self.swap_storage().get_submarine(swap_id).await?.is_some() {
2703 SwapType::Submarine
2704 } else if self.swap_storage().get_reverse(swap_id).await?.is_some() {
2705 SwapType::Reverse
2706 } else if self.swap_storage().get_chain(swap_id).await?.is_some() {
2707 SwapType::Chain
2708 } else {
2709 SwapType::Unknown
2710 };
2711
2712 let url = format!("{}/v2/swap/{swap_id}", self.inner.boltz_url);
2714 let client = reqwest::Client::new();
2715 let response = client
2716 .get(&url)
2717 .send()
2718 .await
2719 .map_err(|e| Error::ad_hoc(e.to_string()))
2720 .context("failed to query swap status")?;
2721
2722 if !response.status().is_success() {
2723 let error_text = response
2724 .text()
2725 .await
2726 .map_err(|e| Error::ad_hoc(e.to_string()))?;
2727 return Err(Error::ad_hoc(format!(
2728 "failed to get swap status: {error_text}"
2729 )));
2730 }
2731
2732 let status_response: GetSwapStatusResponse = response
2733 .json()
2734 .await
2735 .map_err(|e| Error::ad_hoc(e.to_string()))
2736 .context("failed to deserialize swap status response")?;
2737
2738 Ok(SwapStatusInfo {
2739 swap_id: swap_id.to_string(),
2740 swap_type,
2741 status: status_response.status,
2742 })
2743 }
2744
2745 pub async fn get_fees(&self) -> Result<BoltzFees, Error> {
2751 let client = reqwest::Client::builder()
2752 .timeout(self.inner.timeout)
2753 .build()
2754 .map_err(|e| Error::ad_hoc(e.to_string()))?;
2755
2756 let submarine_url = format!("{}/v2/swap/submarine", &self.inner.boltz_url);
2758 let submarine_response = client
2759 .get(&submarine_url)
2760 .send()
2761 .await
2762 .map_err(|e| Error::ad_hoc(e.to_string()))
2763 .context("failed to fetch submarine swap fees")?;
2764
2765 if !submarine_response.status().is_success() {
2766 let error_text = submarine_response
2767 .text()
2768 .await
2769 .map_err(|e| Error::ad_hoc(e.to_string()))?;
2770 return Err(Error::ad_hoc(format!(
2771 "failed to fetch submarine swap fees: {error_text}"
2772 )));
2773 }
2774
2775 let submarine_pairs: SubmarinePairsResponse = submarine_response
2776 .json()
2777 .await
2778 .map_err(|e| Error::ad_hoc(e.to_string()))
2779 .context("failed to deserialize submarine swap fees response")?;
2780
2781 let submarine_pair_fees = &submarine_pairs.ark.btc.fees;
2782 let submarine_fees = SubmarineSwapFees {
2783 percentage: submarine_pair_fees.percentage,
2784 miner_fees: submarine_pair_fees.miner_fees,
2785 };
2786
2787 let reverse_url = format!("{}/v2/swap/reverse", self.inner.boltz_url);
2789 let reverse_response = client
2790 .get(&reverse_url)
2791 .send()
2792 .await
2793 .map_err(|e| Error::ad_hoc(e.to_string()))
2794 .context("failed to fetch reverse swap fees")?;
2795
2796 if !reverse_response.status().is_success() {
2797 let error_text = reverse_response
2798 .text()
2799 .await
2800 .map_err(|e| Error::ad_hoc(e.to_string()))?;
2801 return Err(Error::ad_hoc(format!(
2802 "failed to fetch reverse swap fees: {error_text}"
2803 )));
2804 }
2805
2806 let reverse_pairs: ReversePairsResponse = reverse_response
2807 .json()
2808 .await
2809 .map_err(|e| Error::ad_hoc(e.to_string()))
2810 .context("failed to deserialize reverse swap fees response")?;
2811
2812 let reverse_pair_fees = &reverse_pairs.btc.ark.fees;
2813 let reverse_fees = ReverseSwapFees {
2814 percentage: reverse_pair_fees.percentage,
2815 miner_fees: ReverseMinerFees {
2816 lockup: reverse_pair_fees.miner_fees.lockup,
2817 claim: reverse_pair_fees.miner_fees.claim,
2818 },
2819 };
2820
2821 Ok(BoltzFees {
2822 submarine: submarine_fees,
2823 reverse: reverse_fees,
2824 })
2825 }
2826
2827 pub async fn get_limits(&self) -> Result<SwapLimits, Error> {
2833 let client = reqwest::Client::builder()
2834 .timeout(self.inner.timeout)
2835 .build()
2836 .map_err(|e| Error::ad_hoc(e.to_string()))?;
2837
2838 let url = format!("{}/v2/swap/submarine", self.inner.boltz_url);
2839 let response = client
2840 .get(&url)
2841 .send()
2842 .await
2843 .map_err(|e| Error::ad_hoc(e.to_string()))
2844 .context("failed to fetch swap limits")?;
2845
2846 if !response.status().is_success() {
2847 let error_text = response
2848 .text()
2849 .await
2850 .map_err(|e| Error::ad_hoc(e.to_string()))?;
2851 return Err(Error::ad_hoc(format!(
2852 "failed to fetch swap limits: {error_text}"
2853 )));
2854 }
2855
2856 let pairs: SubmarinePairsResponse = response
2857 .json()
2858 .await
2859 .map_err(|e| Error::ad_hoc(e.to_string()))
2860 .context("failed to deserialize swap limits response")?;
2861
2862 Ok(SwapLimits {
2863 min: pairs.ark.btc.limits.minimal,
2864 max: pairs.ark.btc.limits.maximal,
2865 })
2866 }
2867
2868 pub fn subscribe_to_swap_updates(
2871 &self,
2872 swap_id: String,
2873 ) -> impl futures::Stream<Item = Result<SwapStatus, Error>> + '_ {
2874 async_stream::stream! {
2875 let mut last_status: Option<SwapStatus> = None;
2876 let url = format!("{}/v2/swap/{swap_id}", self.inner.boltz_url);
2877
2878 loop {
2879 let client = reqwest::Client::new();
2880 let response = client
2881 .get(&url)
2882 .send()
2883 .await;
2884
2885 match response {
2886 Ok(resp) if resp.status().is_success() => {
2887 let status_response = resp
2888 .json::<GetSwapStatusResponse>()
2889 .await
2890 .map_err(|e| Error::ad_hoc(e.to_string()));
2891
2892 match status_response {
2893 Ok(current_status) => {
2894 let current_status = current_status.status;
2895
2896 if last_status.as_ref() != Some(¤t_status) {
2898 last_status = Some(current_status.clone());
2899 yield Ok(current_status);
2900 }
2901 }
2902 Err(e) => {
2903 yield Err(Error::ad_hoc(format!(
2904 "failed to deserialize swap status response: {e}"
2905 )));
2906 break;
2907 }
2908 }
2909 }
2910 Ok(resp) => {
2911 let error_text = resp
2912 .text()
2913 .await
2914 .unwrap_or_else(|_| "Unknown error".to_string());
2915
2916 yield Err(Error::ad_hoc(format!(
2917 "failed to check swap status: {error_text}"
2918 )));
2919 break;
2920 }
2921 Err(e) => {
2922 yield Err(Error::ad_hoc(e.to_string())
2923 .context("failed to send swap status request"));
2924 break;
2925 }
2926 }
2927
2928 tokio::time::sleep(std::time::Duration::from_secs(1)).await;
2930 }
2931 }
2932 }
2933
2934 pub async fn list_pending_vhtlc_spend_txs(&self) -> Result<Vec<PendingVhtlcSpendTx>, Error> {
2941 let vhtlc_infos = self.collect_active_vhtlc_infos().await?;
2942
2943 if vhtlc_infos.is_empty() {
2944 return Ok(vec![]);
2945 }
2946
2947 let addresses = vhtlc_infos.iter().map(|info| info.address);
2948 let request = ark_core::server::GetVtxosRequest::new_for_addresses(addresses)
2949 .pending_only()
2950 .map_err(Error::from)?;
2951
2952 let vtxos = self
2953 .fetch_all_vtxos(request)
2954 .await
2955 .context("failed to fetch pending VHTLC VTXOs")?;
2956
2957 tracing::debug!(
2958 num_pending_vtxos = vtxos.len(),
2959 "Fetched pending VHTLC VTXOs"
2960 );
2961
2962 if vtxos.is_empty() {
2963 return Ok(vec![]);
2964 }
2965
2966 let info_by_script: std::collections::HashMap<_, _> = vhtlc_infos
2968 .iter()
2969 .map(|info| (info.script_pubkey.clone(), info))
2970 .collect();
2971
2972 let secp = Secp256k1::new();
2973 let mut results = Vec::new();
2974 let mut seen_ark_txids = std::collections::HashSet::new();
2975
2976 for vtxo in &vtxos {
2977 let info = match info_by_script.get(&vtxo.script) {
2978 Some(info) => info,
2979 None => {
2980 tracing::warn!(
2981 outpoint = %vtxo.outpoint,
2982 "Skipping pending VHTLC VTXO with unknown script"
2983 );
2984 continue;
2985 }
2986 };
2987
2988 let intent_input = match info.preimage {
2993 Some(preimage) => intent::Input::new_with_extra_witness(
2994 vtxo.outpoint,
2995 bitcoin::Sequence::ZERO,
2996 None,
2997 TxOut {
2998 value: vtxo.amount,
2999 script_pubkey: info.script_pubkey.clone(),
3000 },
3001 vhtlc_tapscripts(&info.vhtlc),
3002 info.intent_spend_info.clone(),
3003 false,
3004 vtxo.is_swept,
3005 vtxo.assets.clone(),
3006 vec![preimage.to_vec()],
3007 ),
3008 None => intent::Input::new(
3009 vtxo.outpoint,
3010 bitcoin::Sequence::ZERO,
3011 None,
3012 TxOut {
3013 value: vtxo.amount,
3014 script_pubkey: info.script_pubkey.clone(),
3015 },
3016 vhtlc_tapscripts(&info.vhtlc),
3017 info.intent_spend_info.clone(),
3018 false,
3019 vtxo.is_swept,
3020 vtxo.assets.clone(),
3021 ),
3022 };
3023
3024 let sign_for_vtxo_fn = |input: &mut psbt::Input,
3025 msg: secp256k1::Message|
3026 -> Result<
3027 Vec<(schnorr::Signature, XOnlyPublicKey)>,
3028 ark_core::Error,
3029 > {
3030 match &input.witness_script {
3031 None => Err(ark_core::Error::ad_hoc(
3032 "Missing witness script when signing get-pending-tx intent for VHTLC",
3033 )),
3034 Some(script) => {
3035 let pks = extract_checksig_pubkeys(script);
3036 let mut res = vec![];
3037 for pk in &pks {
3038 if let Ok(keypair) = self.keypair_by_pk(pk) {
3039 let sig = secp.sign_schnorr_no_aux_rand(&msg, &keypair);
3040 res.push((sig, keypair.x_only_public_key().0));
3041 }
3042 }
3043 Ok(res)
3044 }
3045 }
3046 };
3047
3048 let sign_for_onchain_fn =
3049 |_: &mut psbt::Input,
3050 _: secp256k1::Message|
3051 -> Result<(schnorr::Signature, XOnlyPublicKey), ark_core::Error> {
3052 Err(ark_core::Error::ad_hoc(
3053 "unexpected onchain input in get-pending-tx intent",
3054 ))
3055 };
3056
3057 let message = intent::IntentMessage::GetPendingTx { expire_at: 0 };
3058 let get_pending_intent = intent::make_intent(
3059 sign_for_vtxo_fn,
3060 sign_for_onchain_fn,
3061 vec![intent_input],
3062 vec![],
3063 message,
3064 )?;
3065
3066 let pending_txs = self
3067 .network_client()
3068 .get_pending_tx(get_pending_intent)
3069 .await
3070 .map_err(Error::ark_server)
3071 .context("failed to get pending VHTLC transactions")?;
3072
3073 for pending_tx in pending_txs {
3074 if !seen_ark_txids.insert(pending_tx.ark_txid) {
3075 continue;
3076 }
3077
3078 let spend_type = Self::identify_vhtlc_spend_type(info, &pending_tx)?;
3079
3080 tracing::info!(
3081 ark_txid = %pending_tx.ark_txid,
3082 swap_id = spend_type.swap_id(),
3083 spend_type = spend_type.name(),
3084 "Found pending VHTLC spend transaction"
3085 );
3086
3087 results.push(PendingVhtlcSpendTx {
3088 spend_type,
3089 pending_tx,
3090 });
3091 }
3092 }
3093
3094 Ok(results)
3095 }
3096
3097 pub async fn continue_pending_vhtlc_spend_tx(
3104 &self,
3105 pending: &PendingVhtlcSpendTx,
3106 ) -> Result<Txid, Error> {
3107 let ark_txid = pending.pending_tx.ark_txid;
3108
3109 match &pending.spend_type {
3110 PendingVhtlcSpendType::Claim { preimage, .. } => {
3111 self.continue_pending_claim(ark_txid, &pending.pending_tx, *preimage)
3112 .await
3113 }
3114 PendingVhtlcSpendType::CollaborativeRefund { swap_id } => {
3115 self.continue_pending_collaborative_refund(ark_txid, &pending.pending_tx, swap_id)
3116 .await
3117 }
3118 PendingVhtlcSpendType::ExpiredRefund { .. } => {
3119 self.continue_pending_expired_refund(ark_txid, &pending.pending_tx)
3120 .await
3121 }
3122 }
3123 }
3124
3125 pub async fn continue_pending_vhtlc_spend_txs(&self) -> Result<Vec<Txid>, Error> {
3127 let pending = self.list_pending_vhtlc_spend_txs().await?;
3128
3129 let mut finalized = Vec::new();
3130 for tx in &pending {
3131 match self.continue_pending_vhtlc_spend_tx(tx).await {
3132 Ok(txid) => finalized.push(txid),
3133 Err(e) => {
3134 tracing::warn!(
3135 ark_txid = %tx.pending_tx.ark_txid,
3136 swap_id = tx.spend_type.swap_id(),
3137 ?e,
3138 "Failed to finalize pending VHTLC spend tx"
3139 );
3140 }
3141 }
3142 }
3143
3144 Ok(finalized)
3145 }
3146
3147 async fn continue_pending_claim(
3149 &self,
3150 ark_txid: Txid,
3151 pending_tx: &PendingTx,
3152 preimage: [u8; 32],
3153 ) -> Result<Txid, Error> {
3154 let mut signed_checkpoint_txs = pending_tx.signed_checkpoint_txs.clone();
3155
3156 for checkpoint_psbt in signed_checkpoint_txs.iter_mut() {
3157 Self::restore_witness_script_if_needed(checkpoint_psbt, &pending_tx.signed_ark_tx)?;
3158
3159 Self::inject_preimage_into_psbt(checkpoint_psbt, preimage);
3161
3162 self.sign_checkpoint_with_own_keys(checkpoint_psbt)?;
3163 }
3164
3165 timeout_op(
3166 self.inner.timeout,
3167 self.network_client()
3168 .finalize_offchain_transaction(ark_txid, signed_checkpoint_txs),
3169 )
3170 .await?
3171 .map_err(Error::ark_server)
3172 .context("failed to finalize pending claim transaction")?;
3173
3174 tracing::info!(txid = %ark_txid, "Finalized pending VHTLC claim");
3175 Ok(ark_txid)
3176 }
3177
3178 async fn continue_pending_collaborative_refund(
3180 &self,
3181 ark_txid: Txid,
3182 pending_tx: &PendingTx,
3183 swap_id: &str,
3184 ) -> Result<Txid, Error> {
3185 let url = format!(
3191 "{}/v2/swap/submarine/{swap_id}/refund/ark",
3192 self.inner.boltz_url
3193 );
3194 let client = reqwest::Client::new();
3195
3196 let mut signed_checkpoint_txs = Vec::new();
3197
3198 for checkpoint_psbt in &pending_tx.signed_checkpoint_txs {
3199 let response = client
3200 .post(&url)
3201 .json(&RefundSwapRequest {
3202 transaction: pending_tx.signed_ark_tx.to_string(),
3203 checkpoint: checkpoint_psbt.to_string(),
3204 })
3205 .send()
3206 .await
3207 .map_err(Error::ad_hoc)
3208 .context("failed to re-request Boltz refund signature")?;
3209
3210 if !response.status().is_success() {
3211 let error_text = response
3212 .text()
3213 .await
3214 .map_err(|e| Error::ad_hoc(e.to_string()))
3215 .context("failed to read Boltz error text")?;
3216
3217 return Err(Error::ad_hoc(format!(
3218 "Boltz refund re-sign request failed: {error_text}"
3219 )));
3220 }
3221
3222 let refund_response: RefundSwapResponse = response
3223 .json()
3224 .await
3225 .map_err(Error::ad_hoc)
3226 .context("failed to deserialize Boltz refund response")?;
3227
3228 if let Some(err) = refund_response.error.as_deref() {
3229 return Err(Error::ad_hoc(format!("Boltz refund re-sign failed: {err}")));
3230 }
3231
3232 let boltz_signed_checkpoint = Psbt::from_str(&refund_response.checkpoint)
3233 .map_err(Error::ad_hoc)
3234 .context("could not parse Boltz-signed checkpoint PSBT")?;
3235
3236 let boltz_tap_script_sigs = boltz_signed_checkpoint
3238 .inputs
3239 .first()
3240 .ok_or_else(|| Error::ad_hoc("Boltz checkpoint has no inputs"))?
3241 .tap_script_sigs
3242 .clone();
3243
3244 let mut final_checkpoint = checkpoint_psbt.clone();
3246 Self::restore_witness_script_if_needed(
3247 &mut final_checkpoint,
3248 &pending_tx.signed_ark_tx,
3249 )?;
3250
3251 final_checkpoint
3253 .inputs
3254 .first_mut()
3255 .ok_or_else(|| Error::ad_hoc("checkpoint has no inputs"))?
3256 .tap_script_sigs
3257 .extend(boltz_tap_script_sigs);
3258
3259 self.sign_checkpoint_with_own_keys(&mut final_checkpoint)?;
3261
3262 signed_checkpoint_txs.push(final_checkpoint);
3263 }
3264
3265 timeout_op(
3266 self.inner.timeout,
3267 self.network_client()
3268 .finalize_offchain_transaction(ark_txid, signed_checkpoint_txs),
3269 )
3270 .await?
3271 .map_err(Error::ark_server)
3272 .context("failed to finalize pending collaborative refund")?;
3273
3274 tracing::info!(txid = %ark_txid, swap_id, "Finalized pending collaborative refund");
3275 Ok(ark_txid)
3276 }
3277
3278 async fn continue_pending_expired_refund(
3280 &self,
3281 ark_txid: Txid,
3282 pending_tx: &PendingTx,
3283 ) -> Result<Txid, Error> {
3284 let mut signed_checkpoint_txs = pending_tx.signed_checkpoint_txs.clone();
3285
3286 for checkpoint_psbt in signed_checkpoint_txs.iter_mut() {
3287 Self::restore_witness_script_if_needed(checkpoint_psbt, &pending_tx.signed_ark_tx)?;
3288 self.sign_checkpoint_with_own_keys(checkpoint_psbt)?;
3289 }
3290
3291 timeout_op(
3292 self.inner.timeout,
3293 self.network_client()
3294 .finalize_offchain_transaction(ark_txid, signed_checkpoint_txs),
3295 )
3296 .await?
3297 .map_err(Error::ark_server)
3298 .context("failed to finalize pending expired refund")?;
3299
3300 tracing::info!(txid = %ark_txid, "Finalized pending expired VHTLC refund");
3301 Ok(ark_txid)
3302 }
3303
3304 fn build_vhtlc_script(
3308 &self,
3309 claim_public_key: PublicKey,
3310 refund_public_key: PublicKey,
3311 preimage_hash: ripemd160::Hash,
3312 timeout_block_heights: &TimeoutBlockHeights,
3313 ) -> Result<VhtlcScript, Error> {
3314 VhtlcScript::new(
3315 VhtlcOptions {
3316 sender: refund_public_key.inner.x_only_public_key().0,
3317 receiver: claim_public_key.inner.x_only_public_key().0,
3318 server: self.server_info.signer_pk.into(),
3319 preimage_hash,
3320 refund_locktime: timeout_block_heights.refund,
3321 unilateral_claim_delay: parse_sequence_number(
3322 timeout_block_heights.unilateral_claim as i64,
3323 )
3324 .map_err(|e| Error::ad_hoc(format!("invalid unilateral claim timeout: {e}")))?,
3325 unilateral_refund_delay: parse_sequence_number(
3326 timeout_block_heights.unilateral_refund as i64,
3327 )
3328 .map_err(|e| Error::ad_hoc(format!("invalid unilateral refund timeout: {e}")))?,
3329 unilateral_refund_without_receiver_delay: parse_sequence_number(
3330 timeout_block_heights.unilateral_refund_without_receiver as i64,
3331 )
3332 .map_err(|e| {
3333 Error::ad_hoc(format!("invalid refund without receiver timeout: {e}"))
3334 })?,
3335 },
3336 self.server_info.network,
3337 )
3338 .map_err(Error::ad_hoc)
3339 }
3340
3341 fn ensure_swap_key_cached(
3348 &self,
3349 pk: &XOnlyPublicKey,
3350 key_derivation_index: Option<u32>,
3351 swap_id: &str,
3352 ) -> bool {
3353 if self.keypair_by_pk(pk).is_ok() {
3355 return true;
3356 }
3357
3358 let Some(index) = key_derivation_index else {
3359 tracing::warn!(
3360 swap_id,
3361 "Legacy swap data without derivation index, skipping recovery"
3362 );
3363 return false;
3364 };
3365
3366 match self.inner.key_provider.derive_at_discovery_index(index) {
3367 Ok(Some(kp)) if kp.x_only_public_key().0 == *pk => {
3368 if let Err(e) = self.inner.key_provider.cache_discovered_keypair(index, kp) {
3369 tracing::warn!(swap_id, %e, "Failed to cache swap key");
3370 return false;
3371 }
3372 true
3373 }
3374 Ok(_) => {
3375 tracing::warn!(
3376 swap_id,
3377 index,
3378 "Key at stored derivation index does not match swap pubkey"
3379 );
3380 false
3381 }
3382 Err(e) => {
3383 tracing::warn!(swap_id, index, %e, "Failed to derive key at stored index");
3384 false
3385 }
3386 }
3387 }
3388
3389 async fn collect_active_vhtlc_infos(&self) -> Result<Vec<VhtlcInfo>, Error> {
3390 let submarine_swaps = self
3391 .swap_storage()
3392 .list_all_submarine()
3393 .await
3394 .context("failed to list submarine swaps")?;
3395
3396 let reverse_swaps = self
3397 .swap_storage()
3398 .list_all_reverse()
3399 .await
3400 .context("failed to list reverse swaps")?;
3401
3402 let mut infos = Vec::new();
3403
3404 for swap in &submarine_swaps {
3405 if swap.status.is_terminal() {
3406 continue;
3407 }
3408
3409 if !self.ensure_swap_key_cached(
3411 &swap.refund_public_key.inner.x_only_public_key().0,
3412 swap.key_derivation_index,
3413 &swap.id,
3414 ) {
3415 continue;
3416 }
3417
3418 let vhtlc = self.build_vhtlc_script(
3419 swap.claim_public_key,
3420 swap.refund_public_key,
3421 swap.preimage_hash,
3422 &swap.timeout_block_heights,
3423 )?;
3424
3425 if vhtlc.address() != swap.vhtlc_address {
3426 tracing::warn!(
3427 swap_id = swap.id,
3428 "VHTLC address mismatch for submarine swap, skipping"
3429 );
3430 continue;
3431 }
3432
3433 let refund_script = vhtlc.refund_without_receiver_script();
3437 let spend_info = vhtlc.taproot_spend_info();
3438 let control_block = spend_info
3439 .control_block(&(refund_script.clone(), LeafVersion::TapScript))
3440 .ok_or_else(|| {
3441 Error::ad_hoc("control block not found for refund_without_receiver script")
3442 })?;
3443
3444 infos.push(VhtlcInfo {
3445 swap_id: swap.id.clone(),
3446 address: swap.vhtlc_address,
3447 script_pubkey: vhtlc.script_pubkey(),
3448 vhtlc,
3449 intent_spend_info: (refund_script, control_block),
3450 preimage: swap.preimage,
3451 });
3452 }
3453
3454 for swap in &reverse_swaps {
3455 if swap.status.is_terminal() {
3456 continue;
3457 }
3458
3459 if !self.ensure_swap_key_cached(
3461 &swap.claim_public_key.inner.x_only_public_key().0,
3462 swap.key_derivation_index,
3463 &swap.id,
3464 ) {
3465 continue;
3466 }
3467
3468 let vhtlc = self.build_vhtlc_script(
3469 swap.claim_public_key,
3470 swap.refund_public_key,
3471 swap.preimage_hash,
3472 &swap.timeout_block_heights,
3473 )?;
3474
3475 if vhtlc.address() != swap.vhtlc_address {
3476 tracing::warn!(
3477 swap_id = swap.id,
3478 "VHTLC address mismatch for reverse swap, skipping"
3479 );
3480 continue;
3481 }
3482
3483 let claim_script = vhtlc.claim_script();
3486 let spend_info = vhtlc.taproot_spend_info();
3487 let control_block = spend_info
3488 .control_block(&(claim_script.clone(), LeafVersion::TapScript))
3489 .ok_or_else(|| Error::ad_hoc("control block not found for claim script"))?;
3490
3491 infos.push(VhtlcInfo {
3492 swap_id: swap.id.clone(),
3493 address: swap.vhtlc_address,
3494 script_pubkey: vhtlc.script_pubkey(),
3495 vhtlc,
3496 intent_spend_info: (claim_script, control_block),
3497 preimage: swap.preimage,
3498 });
3499 }
3500
3501 Ok(infos)
3502 }
3503
3504 fn identify_vhtlc_spend_type(
3506 info: &VhtlcInfo,
3507 pending_tx: &PendingTx,
3508 ) -> Result<PendingVhtlcSpendType, Error> {
3509 let spend_script = pending_tx
3511 .signed_ark_tx
3512 .inputs
3513 .iter()
3514 .find_map(|input| {
3515 input.tap_scripts.values().find_map(|(script, _)| {
3516 let claim = info.vhtlc.claim_script();
3518 let refund = info.vhtlc.refund_script();
3519 let refund_no_recv = info.vhtlc.refund_without_receiver_script();
3520
3521 if *script == claim || *script == refund || *script == refund_no_recv {
3522 Some(script.clone())
3523 } else {
3524 None
3525 }
3526 })
3527 })
3528 .ok_or_else(|| {
3529 Error::ad_hoc(format!(
3530 "could not identify spend script in pending tx {} for swap {}",
3531 pending_tx.ark_txid, info.swap_id
3532 ))
3533 })?;
3534
3535 let claim_script = info.vhtlc.claim_script();
3536 let refund_script = info.vhtlc.refund_script();
3537
3538 if spend_script == claim_script {
3539 let preimage = extract_preimage_from_psbt(&pending_tx.signed_ark_tx)
3543 .ok()
3544 .or(info.preimage)
3545 .ok_or_else(|| {
3546 Error::ad_hoc(format!(
3547 "cannot recover preimage for pending claim of swap {}",
3548 info.swap_id
3549 ))
3550 })?;
3551
3552 Ok(PendingVhtlcSpendType::Claim {
3553 swap_id: info.swap_id.clone(),
3554 preimage,
3555 })
3556 } else if spend_script == refund_script {
3557 Ok(PendingVhtlcSpendType::CollaborativeRefund {
3558 swap_id: info.swap_id.clone(),
3559 })
3560 } else {
3561 Ok(PendingVhtlcSpendType::ExpiredRefund {
3562 swap_id: info.swap_id.clone(),
3563 })
3564 }
3565 }
3566
3567 fn inject_preimage_into_psbt(psbt: &mut Psbt, preimage: [u8; 32]) {
3569 let mut bytes = vec![1];
3570 let length = VarInt::from(preimage.len() as u64);
3571 length
3572 .consensus_encode(&mut bytes)
3573 .expect("valid length encoding");
3574 bytes.write_all(&preimage).expect("valid preimage encoding");
3575
3576 let key = psbt::raw::Key {
3577 type_value: 222,
3578 key: VTXO_CONDITION_KEY.to_vec(),
3579 };
3580
3581 for input in &mut psbt.inputs {
3582 input.unknown.insert(key.clone(), bytes.clone());
3583 }
3584 }
3585
3586 fn sign_checkpoint_with_own_keys(&self, checkpoint_psbt: &mut Psbt) -> Result<(), Error> {
3588 let sign_fn =
3589 |input: &mut psbt::Input,
3590 msg: secp256k1::Message|
3591 -> Result<Vec<(schnorr::Signature, XOnlyPublicKey)>, ark_core::Error> {
3592 let script = input.witness_script.as_ref().ok_or_else(|| {
3593 ark_core::Error::ad_hoc("missing witness script for checkpoint signing")
3594 })?;
3595 let pks = extract_checksig_pubkeys(script);
3596 let mut res = vec![];
3597 for pk in pks {
3598 if let Ok(keypair) = self.keypair_by_pk(&pk) {
3599 let sig = Secp256k1::new().sign_schnorr_no_aux_rand(&msg, &keypair);
3600 res.push((sig, keypair.x_only_public_key().0));
3601 }
3602 }
3603 Ok(res)
3604 };
3605
3606 sign_checkpoint_transaction(sign_fn, checkpoint_psbt)?;
3607 Ok(())
3608 }
3609
3610 fn restore_witness_script_if_needed(
3614 checkpoint_psbt: &mut Psbt,
3615 signed_ark_tx: &Psbt,
3616 ) -> Result<(), Error> {
3617 if checkpoint_psbt
3618 .inputs
3619 .first()
3620 .ok_or_else(|| Error::ad_hoc("checkpoint PSBT has no inputs"))?
3621 .witness_script
3622 .is_some()
3623 {
3624 return Ok(());
3625 }
3626
3627 let checkpoint_txid = checkpoint_psbt.unsigned_tx.compute_txid();
3628
3629 let ark_input_idx = signed_ark_tx
3630 .unsigned_tx
3631 .input
3632 .iter()
3633 .position(|inp| inp.previous_output.txid == checkpoint_txid)
3634 .ok_or_else(|| {
3635 Error::ad_hoc(format!(
3636 "checkpoint txid {checkpoint_txid} not found in ark tx inputs"
3637 ))
3638 })?;
3639
3640 let witness_script = signed_ark_tx
3641 .inputs
3642 .get(ark_input_idx)
3643 .and_then(|input| input.witness_script.clone())
3644 .ok_or_else(|| {
3645 Error::ad_hoc(format!(
3646 "missing witness script on ark tx input {ark_input_idx}"
3647 ))
3648 })?;
3649
3650 checkpoint_psbt
3651 .inputs
3652 .first_mut()
3653 .ok_or_else(|| Error::ad_hoc("checkpoint PSBT has no inputs"))?
3654 .witness_script = Some(witness_script);
3655 Ok(())
3656 }
3657}
3658
3659struct VhtlcInfo {
3661 swap_id: String,
3662 address: ArkAddress,
3663 script_pubkey: ScriptBuf,
3664 vhtlc: VhtlcScript,
3665 intent_spend_info: (ScriptBuf, bitcoin::taproot::ControlBlock),
3667 preimage: Option<[u8; 32]>,
3668}
3669
3670fn reconstruct_btc_htlc(
3675 server_pk: PublicKey,
3676 user_pk: PublicKey,
3677 swap_tree: &SwapTree,
3678) -> Result<bitcoin::taproot::TaprootSpendInfo, Error> {
3679 let claim_script_bytes: Vec<u8> = bitcoin::hex::FromHex::from_hex(&swap_tree.claim_leaf.output)
3680 .map_err(|e| Error::ad_hoc(format!("invalid claim leaf hex: {e}")))?;
3681 let claim_script = ScriptBuf::from_bytes(claim_script_bytes);
3682
3683 let refund_script_bytes: Vec<u8> =
3684 bitcoin::hex::FromHex::from_hex(&swap_tree.refund_leaf.output)
3685 .map_err(|e| Error::ad_hoc(format!("invalid refund leaf hex: {e}")))?;
3686 let refund_script = ScriptBuf::from_bytes(refund_script_bytes);
3687
3688 let musig_server_pk = musig::PublicKey::from_slice(&server_pk.to_bytes())
3689 .map_err(|e| Error::ad_hoc(format!("invalid server key for musig: {e}")))?;
3690 let musig_user_pk = musig::PublicKey::from_slice(&user_pk.to_bytes())
3691 .map_err(|e| Error::ad_hoc(format!("invalid user key for musig: {e}")))?;
3692
3693 let key_agg = musig::musig::KeyAggCache::new(&[&musig_server_pk, &musig_user_pk]);
3694 let internal_key = XOnlyPublicKey::from_slice(&key_agg.agg_pk().serialize())
3695 .map_err(|e| Error::ad_hoc(format!("invalid aggregated key: {e}")))?;
3696
3697 let secp = Secp256k1::new();
3698 bitcoin::taproot::TaprootBuilder::new()
3699 .add_leaf(1, claim_script)
3700 .map_err(|e| Error::ad_hoc(format!("failed to add claim leaf: {e}")))?
3701 .add_leaf(1, refund_script)
3702 .map_err(|e| Error::ad_hoc(format!("failed to add refund leaf: {e}")))?
3703 .finalize(&secp, internal_key)
3704 .map_err(|_| Error::ad_hoc("failed to finalize taproot tree"))
3705}
3706
3707fn vhtlc_tapscripts(vhtlc: &VhtlcScript) -> Vec<ScriptBuf> {
3709 vec![
3710 vhtlc.claim_script(),
3711 vhtlc.refund_script(),
3712 vhtlc.refund_without_receiver_script(),
3713 vhtlc.unilateral_claim_script(),
3714 vhtlc.unilateral_refund_script(),
3715 vhtlc.unilateral_refund_without_receiver_script(),
3716 ]
3717}
3718
3719fn extract_preimage_from_psbt(psbt: &Psbt) -> Result<[u8; 32], Error> {
3724 let condition_key = psbt::raw::Key {
3725 type_value: 222,
3726 key: VTXO_CONDITION_KEY.to_vec(),
3727 };
3728
3729 for input in &psbt.inputs {
3730 if let Some(condition_data) = input.unknown.get(&condition_key) {
3731 if condition_data.is_empty() {
3732 continue;
3733 }
3734
3735 let num_elements = condition_data[0] as usize;
3737 if num_elements == 0 {
3738 continue;
3739 }
3740
3741 let mut cursor = std::io::Cursor::new(&condition_data[1..]);
3743 let length = bitcoin::consensus::Decodable::consensus_decode(&mut cursor)
3744 .map_err(|e| Error::ad_hoc(format!("failed to decode varint length: {e}")))?;
3745 let length: VarInt = length;
3746 let offset = cursor.position() as usize;
3747 let remaining = &condition_data[1 + offset..];
3748
3749 if remaining.len() < length.0 as usize {
3750 return Err(Error::ad_hoc(format!(
3751 "condition data too short: expected {} bytes, got {}",
3752 length.0,
3753 remaining.len()
3754 )));
3755 }
3756
3757 let preimage_bytes = &remaining[..length.0 as usize];
3758
3759 let preimage: [u8; 32] = preimage_bytes.try_into().map_err(|_| {
3760 Error::ad_hoc(format!(
3761 "preimage has unexpected length: {} (expected 32)",
3762 preimage_bytes.len()
3763 ))
3764 })?;
3765
3766 return Ok(preimage);
3767 }
3768 }
3769
3770 Err(Error::ad_hoc(
3771 "no VTXO_CONDITION_KEY found in any PSBT input",
3772 ))
3773}
3774
3775pub enum SwapAmount {
3777 Invoice(Amount),
3779 Vhtlc(Amount),
3781}
3782
3783impl SwapAmount {
3784 pub fn invoice(amount: Amount) -> Self {
3785 Self::Invoice(amount)
3786 }
3787
3788 pub fn vhtlc(amount: Amount) -> Self {
3789 Self::Vhtlc(amount)
3790 }
3791}
3792
3793pub enum ChainSwapAmount {
3795 UserLock(Amount),
3797 ServerLock(Amount),
3799}
3800
3801#[serde_as]
3803#[derive(Debug, Clone, Serialize, Deserialize)]
3804pub struct SubmarineSwapData {
3805 pub id: String,
3807 pub preimage: Option<[u8; 32]>,
3809 pub preimage_hash: ripemd160::Hash,
3811 pub claim_public_key: PublicKey,
3813 pub refund_public_key: PublicKey,
3815 pub amount: Amount,
3817 pub timeout_block_heights: TimeoutBlockHeights,
3819 #[serde_as(as = "DisplayFromStr")]
3821 pub vhtlc_address: ArkAddress,
3822 pub invoice: Bolt11Invoice,
3824 pub status: SwapStatus,
3826 pub created_at: u64,
3828 #[serde(default)]
3832 pub key_derivation_index: Option<u32>,
3833}
3834
3835#[serde_as]
3837#[derive(Debug, Clone, Serialize, Deserialize)]
3838pub struct ReverseSwapData {
3839 pub id: String,
3841 pub preimage: Option<[u8; 32]>,
3843 pub preimage_hash: ripemd160::Hash,
3845 pub claim_public_key: PublicKey,
3847 pub refund_public_key: PublicKey,
3849 pub amount: Amount,
3851 pub timeout_block_heights: TimeoutBlockHeights,
3853 #[serde_as(as = "DisplayFromStr")]
3855 pub vhtlc_address: ArkAddress,
3856 pub status: SwapStatus,
3858 pub created_at: u64,
3860 #[serde(default)]
3864 pub key_derivation_index: Option<u32>,
3865 pub bolt11: String,
3867 pub invoice_expiry: u64,
3869}
3870
3871#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
3875pub enum SwapStatus {
3876 #[serde(rename = "swap.created")]
3878 Created,
3879 #[serde(rename = "transaction.mempool")]
3881 TransactionMempool,
3882 #[serde(rename = "transaction.confirmed")]
3884 TransactionConfirmed,
3885 #[serde(rename = "transaction.refunded")]
3887 TransactionRefunded,
3888 #[serde(rename = "transaction.failed")]
3890 TransactionFailed,
3891 #[serde(rename = "transaction.claimed")]
3893 TransactionClaimed,
3894 #[serde(rename = "transaction.server.mempool")]
3896 TransactionServerMempool,
3897 #[serde(rename = "transaction.server.confirmed")]
3899 TransactionServerConfirmed,
3900 #[serde(rename = "invoice.set")]
3902 InvoiceSet,
3903 #[serde(rename = "invoice.pending")]
3905 InvoicePending,
3906 #[serde(rename = "invoice.paid")]
3908 InvoicePaid,
3909 #[serde(rename = "invoice.failedToPay")]
3911 InvoiceFailedToPay,
3912 #[serde(rename = "invoice.expired")]
3914 InvoiceExpired,
3915 #[serde(rename = "transaction.lockupFailed")]
3917 TransactionLockupFailed,
3918 #[serde(rename = "swap.expired")]
3920 SwapExpired,
3921 #[serde(rename = "error")]
3923 Error { error: String },
3924 #[serde(untagged)]
3926 Other(String),
3927}
3928
3929impl SwapStatus {
3930 pub fn is_terminal(&self) -> bool {
3932 matches!(
3933 self,
3934 Self::TransactionRefunded
3935 | Self::TransactionFailed
3936 | Self::TransactionClaimed
3937 | Self::TransactionLockupFailed
3938 | Self::InvoicePaid
3939 | Self::InvoiceFailedToPay
3940 | Self::InvoiceExpired
3941 | Self::SwapExpired
3942 | Self::Error { .. }
3943 )
3944 }
3945}
3946
3947#[derive(Debug, Clone, Serialize, Deserialize, Copy)]
3948#[serde(rename_all = "camelCase")]
3949pub struct TimeoutBlockHeights {
3950 pub refund: u32,
3951 pub unilateral_claim: u32,
3952 pub unilateral_refund: u32,
3953 pub unilateral_refund_without_receiver: u32,
3954}
3955
3956#[derive(Debug, Clone, Serialize, Deserialize)]
3957#[serde(rename_all = "UPPERCASE")]
3958enum Asset {
3959 Btc,
3960 Ark,
3961}
3962
3963#[derive(Debug, Clone, Serialize, Deserialize)]
3964#[serde(rename_all = "camelCase")]
3965struct CreateReverseSwapRequest {
3966 from: Asset,
3967 to: Asset,
3968 #[serde(skip_serializing_if = "Option::is_none")]
3969 invoice_amount: Option<Amount>,
3970 #[serde(skip_serializing_if = "Option::is_none")]
3971 onchain_amount: Option<Amount>,
3972 claim_public_key: PublicKey,
3973 preimage_hash: sha256::Hash,
3974 #[serde(skip_serializing_if = "Option::is_none")]
3978 invoice_expiry: Option<u64>,
3979}
3980
3981#[serde_as]
3982#[derive(Debug, Clone, Serialize, Deserialize)]
3983#[serde(rename_all = "camelCase")]
3984struct CreateReverseSwapResponse {
3985 id: String,
3986 #[serde_as(as = "DisplayFromStr")]
3987 lockup_address: ArkAddress,
3988 refund_public_key: PublicKey,
3989 timeout_block_heights: TimeoutBlockHeights,
3990 invoice: Bolt11Invoice,
3991 onchain_amount: Option<Amount>,
3992}
3993
3994#[derive(Debug, Clone, Serialize, Deserialize)]
3995struct CreateSubmarineSwapRequest {
3996 from: Asset,
3997 to: Asset,
3998 invoice: Bolt11Invoice,
3999 #[serde(rename = "refundPublicKey")]
4000 refund_public_key: PublicKey,
4001}
4002
4003#[serde_as]
4004#[derive(Debug, Clone, Serialize, Deserialize)]
4005#[serde(rename_all = "camelCase")]
4006struct CreateSubmarineSwapResponse {
4007 id: String,
4008 #[serde_as(as = "DisplayFromStr")]
4009 address: ArkAddress,
4010 expected_amount: Amount,
4011 claim_public_key: PublicKey,
4012 timeout_block_heights: TimeoutBlockHeights,
4013}
4014
4015#[derive(Debug, Clone, Serialize, Deserialize)]
4016struct GetSwapStatusResponse {
4017 status: SwapStatus,
4018 #[serde(default)]
4019 transaction: Option<SwapStatusTransaction>,
4020}
4021
4022#[derive(Debug, Clone, Serialize, Deserialize)]
4023struct SwapStatusTransaction {
4024 id: String,
4025}
4026
4027#[derive(Debug, Clone, Serialize, Deserialize)]
4028struct RefundSwapRequest {
4029 transaction: String,
4030 checkpoint: String,
4031}
4032
4033#[derive(Debug, Clone, Serialize, Deserialize)]
4034struct RefundSwapResponse {
4035 transaction: String,
4036 checkpoint: String,
4037 #[serde(skip_serializing_if = "Option::is_none")]
4038 error: Option<String>,
4039}
4040
4041#[derive(Debug, Clone, Serialize, Deserialize)]
4043#[serde(rename_all = "camelCase")]
4044pub struct SubmarineSwapFees {
4045 pub percentage: f64,
4047 pub miner_fees: u64,
4049}
4050
4051#[derive(Debug, Clone, Serialize, Deserialize)]
4053pub struct ReverseMinerFees {
4054 pub lockup: u64,
4056 pub claim: u64,
4058}
4059
4060#[derive(Debug, Clone, Serialize, Deserialize)]
4062#[serde(rename_all = "camelCase")]
4063pub struct ReverseSwapFees {
4064 pub percentage: f64,
4066 pub miner_fees: ReverseMinerFees,
4068}
4069
4070#[derive(Debug, Clone, Serialize, Deserialize)]
4072pub struct BoltzFees {
4073 pub submarine: SubmarineSwapFees,
4075 pub reverse: ReverseSwapFees,
4077}
4078
4079#[derive(Debug, Clone, Serialize, Deserialize)]
4081pub struct SwapLimits {
4082 pub min: u64,
4084 pub max: u64,
4086}
4087
4088#[derive(Debug, Clone, Deserialize)]
4091struct PairLimits {
4092 minimal: u64,
4093 maximal: u64,
4094}
4095
4096#[derive(Debug, Clone, Deserialize)]
4098#[serde(rename_all = "camelCase")]
4099struct SubmarinePairFees {
4100 percentage: f64,
4101 miner_fees: u64,
4102}
4103
4104#[derive(Debug, Clone, Deserialize)]
4105struct SubmarinePairInfo {
4106 fees: SubmarinePairFees,
4107 limits: PairLimits,
4108}
4109
4110#[derive(Debug, Clone, Deserialize)]
4111#[serde(rename_all = "UPPERCASE")]
4112struct SubmarineArkPairs {
4113 btc: SubmarinePairInfo,
4114}
4115
4116#[derive(Debug, Clone, Deserialize)]
4117#[serde(rename_all = "UPPERCASE")]
4118struct SubmarinePairsResponse {
4119 ark: SubmarineArkPairs,
4120}
4121
4122#[derive(Debug, Clone, Deserialize)]
4124#[serde(rename_all = "camelCase")]
4125struct ReverseMinerFeesResponse {
4126 claim: u64,
4127 lockup: u64,
4128}
4129
4130#[derive(Debug, Clone, Deserialize)]
4131#[serde(rename_all = "camelCase")]
4132struct ReversePairFees {
4133 percentage: f64,
4134 miner_fees: ReverseMinerFeesResponse,
4135}
4136
4137#[derive(Debug, Clone, Deserialize)]
4138struct ReversePairInfo {
4139 fees: ReversePairFees,
4140}
4141
4142#[derive(Debug, Clone, Deserialize)]
4143#[serde(rename_all = "UPPERCASE")]
4144struct ReverseBtcPairs {
4145 ark: ReversePairInfo,
4146}
4147
4148#[derive(Debug, Clone, Deserialize)]
4149#[serde(rename_all = "UPPERCASE")]
4150struct ReversePairsResponse {
4151 btc: ReverseBtcPairs,
4152}
4153
4154#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
4158pub enum ChainSwapDirection {
4159 ArkToBtc,
4161 BtcToArk,
4163}
4164
4165#[serde_as]
4167#[derive(Debug, Clone, Serialize, Deserialize)]
4168pub struct ChainSwapData {
4169 pub id: String,
4171 pub status: SwapStatus,
4173 pub direction: ChainSwapDirection,
4175 pub preimage: Option<[u8; 32]>,
4177 pub preimage_hash: sha256::Hash,
4179 pub claim_public_key: PublicKey,
4181 pub refund_public_key: PublicKey,
4183 pub server_claim_public_key: PublicKey,
4185 pub server_refund_public_key: PublicKey,
4187 pub user_lockup_address: String,
4189 pub server_lockup_address: String,
4191 pub user_lockup_amount: Amount,
4193 pub server_lockup_amount: Amount,
4195 pub user_timeout_block_height: u32,
4197 pub server_timeout_block_height: u32,
4199 #[serde(default)]
4201 pub user_timeout_block_heights: Option<TimeoutBlockHeights>,
4202 #[serde(default)]
4204 pub server_timeout_block_heights: Option<TimeoutBlockHeights>,
4205 #[serde(default)]
4207 pub bip21: Option<String>,
4208 #[serde(default)]
4210 pub swap_tree: Option<SwapTree>,
4211 pub created_at: u64,
4213 #[serde(default)]
4215 pub claim_key_derivation_index: Option<u32>,
4216 #[serde(default)]
4218 pub refund_key_derivation_index: Option<u32>,
4219}
4220
4221#[derive(Clone, Debug)]
4223pub struct ChainSwapResult {
4224 pub swap_id: String,
4226 pub user_lockup_address: String,
4228 pub user_lockup_amount: Amount,
4230 pub server_lockup_amount: Amount,
4232 pub bip21: Option<String>,
4234}
4235
4236#[derive(Debug, Clone, Serialize, Deserialize)]
4240#[serde(rename_all = "camelCase")]
4241pub struct SwapTree {
4242 pub claim_leaf: SwapTreeLeaf,
4244 pub refund_leaf: SwapTreeLeaf,
4246}
4247
4248#[derive(Debug, Clone, Serialize, Deserialize)]
4250pub struct SwapTreeLeaf {
4251 pub version: u8,
4253 pub output: String,
4255}
4256
4257#[derive(Debug, Clone, Serialize, Deserialize)]
4258#[serde(rename_all = "camelCase")]
4259struct CreateChainSwapRequest {
4260 from: Asset,
4261 to: Asset,
4262 #[serde(skip_serializing_if = "Option::is_none")]
4263 user_lock_amount: Option<Amount>,
4264 #[serde(skip_serializing_if = "Option::is_none")]
4265 server_lock_amount: Option<Amount>,
4266 claim_public_key: PublicKey,
4267 refund_public_key: PublicKey,
4268 preimage_hash: sha256::Hash,
4269}
4270
4271#[serde_as]
4272#[derive(Debug, Clone, Serialize, Deserialize)]
4273#[serde(rename_all = "camelCase")]
4274struct CreateChainSwapResponse {
4275 id: String,
4276 claim_details: ChainSwapSideDetails,
4277 lockup_details: ChainSwapSideDetails,
4278}
4279
4280#[serde_as]
4281#[derive(Debug, Clone, Serialize, Deserialize)]
4282#[serde(rename_all = "camelCase")]
4283struct ChainSwapSideDetails {
4284 lockup_address: String,
4285 server_public_key: PublicKey,
4286 timeout_block_height: u32,
4287 #[serde(default)]
4288 timeouts: Option<TimeoutBlockHeights>,
4289 amount: Amount,
4290 #[serde(default)]
4291 swap_tree: Option<SwapTree>,
4292 #[serde(default)]
4293 bip21: Option<String>,
4294}
4295
4296#[cfg(test)]
4297mod tests {
4298 use super::*;
4299
4300 #[test]
4301 fn test_deserialize_create_reverse_swap_response() {
4302 let json = r#"{
4303 "id": "vqhG2fJtNY4H",
4304 "lockupAddress": "tark1qra883hysahlkt0ujcwhv0x2n278849c3m7t3a08l7fdc40f4f2nmw3f7kn37vvq0hqazxtqgtvhwp3z83zfgr7qc82t9mty8vk95ynpx3l43d",
4305 "refundPublicKey": "0206988651c7fbe41747bb21b54ced0a183f4d658e007ee8fdb23fbbfccb8e0c55",
4306 "timeoutBlockHeights": {
4307 "refund": 1760508054,
4308 "unilateralClaim": 9728,
4309 "unilateralRefund": 86528,
4310 "unilateralRefundWithoutReceiver": 86528
4311 },
4312 "invoice": "lntbs10u1p5wmeeepp56ms94rkev7tdrwqyus5a63lny2mqzq9vh2rq3u4ym3v4lxv6xl4qdql2djkuepqw3hjqs2jfvsxzerywfjhxuccqz95xqztfsp5ckaskagag554na8d56tlrfdxasstqrmmpkvswqqqx6y386jcfq9s9qxpqysgqt7z0vkdwkqamydae7ctgkh7l8q75w7q9394ce3lda2mkfxrpfdtj5gmltuctav7jdgatkflhztrjjzutdla5e4xp0uhxxy7sluzll4qpkkh6wv",
4313 "onchainAmount": 996
4314}"#;
4315
4316 let response: CreateReverseSwapResponse =
4317 serde_json::from_str(json).expect("Failed to deserialize CreateReverseSwapResponse");
4318
4319 assert_eq!(response.id, "vqhG2fJtNY4H");
4321 assert_eq!(response.onchain_amount, Some(Amount::from_sat(996)));
4322 assert_eq!(
4323 response.refund_public_key,
4324 PublicKey::from_str(
4325 "0206988651c7fbe41747bb21b54ced0a183f4d658e007ee8fdb23fbbfccb8e0c55"
4326 )
4327 .expect("valid public key")
4328 );
4329 assert_eq!(
4330 response.lockup_address.to_string(),
4331 "tark1qra883hysahlkt0ujcwhv0x2n278849c3m7t3a08l7fdc40f4f2nmw3f7kn37vvq0hqazxtqgtvhwp3z83zfgr7qc82t9mty8vk95ynpx3l43d"
4332 );
4333 assert_eq!(response.timeout_block_heights.refund, 1760508054);
4334 assert_eq!(response.timeout_block_heights.unilateral_claim, 9728);
4335 assert_eq!(response.timeout_block_heights.unilateral_refund, 86528);
4336 assert_eq!(
4337 response
4338 .timeout_block_heights
4339 .unilateral_refund_without_receiver,
4340 86528
4341 );
4342 }
4343
4344 #[test]
4345 fn test_btc_htlc_address_reconstruction_btc_to_ark() {
4346 let server_pk = PublicKey::from_str(
4350 "03ce9f5a57218103d5fe07b9d7ecf4b28ad60a960f0fbfd86dd090013020617389",
4351 )
4352 .unwrap();
4353 let user_pk = PublicKey::from_str(
4354 "02c6047f9441ed7d6d3045406e95c07cd85c778e4b8cef3ca7abac09b95c709ee5",
4355 )
4356 .unwrap();
4357 let swap_tree = SwapTree {
4358 claim_leaf: SwapTreeLeaf {
4359 version: 192,
4360 output: "82012088a914b472a266d0bd89c13706a4132ccfb16f7c3b9fcb8820ce9f5a57218103d5fe07b9d7ecf4b28ad60a960f0fbfd86dd090013020617389ac".into(),
4361 },
4362 refund_leaf: SwapTreeLeaf {
4363 version: 192,
4364 output: "20c6047f9441ed7d6d3045406e95c07cd85c778e4b8cef3ca7abac09b95c709ee5ad03f9832db1".into(),
4365 },
4366 };
4367
4368 let spend_info = reconstruct_btc_htlc(server_pk, user_pk, &swap_tree).unwrap();
4369
4370 let secp = Secp256k1::new();
4371 let spk = ScriptBuf::new_p2tr(&secp, spend_info.internal_key(), spend_info.merkle_root());
4372 let addr = bitcoin::Address::from_script(&spk, bitcoin::Network::Testnet).unwrap();
4373
4374 assert_eq!(
4375 addr.to_string(),
4376 "tb1ptf632fkczflsjn4356ra4x2s6qp6vvk8e7pplprpwnkvcsd8tpwqkw92c7"
4377 );
4378 }
4379
4380 #[test]
4381 fn test_btc_htlc_address_reconstruction_ark_to_btc() {
4382 let server_pk = PublicKey::from_str(
4386 "0207364dc5853e630be83439fde62b531e3c11db34ce8c4f454a56782555c58ed6",
4387 )
4388 .unwrap();
4389 let user_pk = PublicKey::from_str(
4390 "0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798",
4391 )
4392 .unwrap();
4393 let swap_tree = SwapTree {
4394 claim_leaf: SwapTreeLeaf {
4395 version: 192,
4396 output: "82012088a914cf7ff51392e9a37bc72c7284841db669c82e2c14882079be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798ac".into(),
4397 },
4398 refund_leaf: SwapTreeLeaf {
4399 version: 192,
4400 output: "2007364dc5853e630be83439fde62b531e3c11db34ce8c4f454a56782555c58ed6ad036b832db1".into(),
4401 },
4402 };
4403
4404 let spend_info = reconstruct_btc_htlc(server_pk, user_pk, &swap_tree).unwrap();
4405
4406 let secp = Secp256k1::new();
4407 let spk = ScriptBuf::new_p2tr(&secp, spend_info.internal_key(), spend_info.merkle_root());
4408 let addr = bitcoin::Address::from_script(&spk, bitcoin::Network::Testnet).unwrap();
4409
4410 assert_eq!(
4411 addr.to_string(),
4412 "tb1pxa78pf55g0aaurrd8c76fyax4df9e8y38fzps8sw2vkrecf9k3ss36a78m"
4413 );
4414 }
4415}