1use crate::batch::BatchOutputType;
6use crate::error::ErrorContext as _;
7use crate::swap_storage::SwapStorage;
8use crate::timeout_op;
9use crate::wallet::OnchainWallet;
10use crate::Blockchain;
11use crate::Client;
12use crate::Error;
13use ark_core::intent;
14use ark_core::script::extract_checksig_pubkeys;
15use ark_core::send::build_offchain_transactions;
16use ark_core::send::sign_ark_transaction;
17use ark_core::send::sign_checkpoint_transaction;
18use ark_core::send::OffchainTransactions;
19use ark_core::send::SendReceiver;
20use ark_core::send::VtxoInput;
21use ark_core::server::parse_sequence_number;
22use ark_core::server::Info;
23use ark_core::server::PendingTx;
24use ark_core::vhtlc::VhtlcOptions;
25use ark_core::vhtlc::VhtlcScript;
26use ark_core::ArkAddress;
27use ark_core::VtxoList;
28use ark_core::VTXO_CONDITION_KEY;
29use bitcoin::absolute;
30use bitcoin::consensus::Encodable;
31use bitcoin::hashes::ripemd160;
32use bitcoin::hashes::sha256;
33use bitcoin::hashes::Hash;
34use bitcoin::io::Write;
35use bitcoin::key::Secp256k1;
36use bitcoin::psbt;
37use bitcoin::secp256k1;
38use bitcoin::secp256k1::schnorr;
39use bitcoin::taproot::LeafVersion;
40use bitcoin::Amount;
41use bitcoin::Psbt;
42use bitcoin::PublicKey;
43use bitcoin::ScriptBuf;
44use bitcoin::TxOut;
45use bitcoin::Txid;
46use bitcoin::VarInt;
47use bitcoin::XOnlyPublicKey;
48use lightning_invoice::Bolt11Invoice;
49use rand::CryptoRng;
50use rand::Rng;
51use serde::Deserialize;
52use serde::Serialize;
53use serde_with::serde_as;
54use serde_with::DisplayFromStr;
55use std::str::FromStr;
56use std::time::SystemTime;
57use std::time::UNIX_EPOCH;
58
59const MAX_BOLT11_DESCRIPTION_BYTES: usize = 639;
64
65fn validate_invoice_description(description: Option<&str>) -> Result<(), Error> {
66 if let Some(d) = description {
67 if d.len() > MAX_BOLT11_DESCRIPTION_BYTES {
68 return Err(Error::consumer(format!(
69 "invoice description is {} bytes (> {} bytes).",
70 d.len(),
71 MAX_BOLT11_DESCRIPTION_BYTES,
72 )));
73 }
74 }
75 Ok(())
76}
77
78#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
80pub enum SwapType {
81 Submarine,
82 Reverse,
83 Chain,
84 Unknown,
86}
87
88impl std::fmt::Display for SwapType {
89 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
90 match self {
91 Self::Submarine => write!(f, "submarine"),
92 Self::Reverse => write!(f, "reverse"),
93 Self::Chain => write!(f, "chain"),
94 Self::Unknown => write!(f, "unknown"),
95 }
96 }
97}
98
99#[derive(Clone, Debug)]
101pub struct SwapStatusInfo {
102 pub swap_id: String,
103 pub swap_type: SwapType,
104 pub status: SwapStatus,
105}
106
107#[derive(Clone, Debug)]
108pub struct SubmarineSwapResult {
109 pub swap_id: String,
110 pub txid: Txid,
111 pub amount: Amount,
112}
113
114#[derive(Clone, Debug)]
115pub struct ReverseSwapResult {
116 pub swap_id: String,
117 pub amount: Amount,
118 pub invoice: Bolt11Invoice,
119}
120
121#[derive(Clone, Debug)]
122pub struct ClaimVhtlcResult {
123 pub swap_id: String,
124 pub claim_txid: Txid,
125 pub claim_amount: Amount,
126 pub preimage: [u8; 32],
127}
128
129#[derive(Clone, Debug)]
134pub enum PendingVhtlcSpendType {
135 Claim { swap_id: String, preimage: [u8; 32] },
139 CollaborativeRefund { swap_id: String },
143 ExpiredRefund { swap_id: String },
147}
148
149impl PendingVhtlcSpendType {
150 pub fn swap_id(&self) -> &str {
151 match self {
152 Self::Claim { swap_id, .. }
153 | Self::CollaborativeRefund { swap_id }
154 | Self::ExpiredRefund { swap_id } => swap_id,
155 }
156 }
157
158 pub fn name(&self) -> &'static str {
159 match self {
160 Self::Claim { .. } => "Claim",
161 Self::CollaborativeRefund { .. } => "CollaborativeRefund",
162 Self::ExpiredRefund { .. } => "ExpiredRefund",
163 }
164 }
165}
166
167#[derive(Clone, Debug)]
169pub struct PendingVhtlcSpendTx {
170 pub spend_type: PendingVhtlcSpendType,
171 pub pending_tx: PendingTx,
172}
173
174impl<B, W, S> Client<B, W, S>
175where
176 B: Blockchain,
177 W: OnchainWallet,
178 S: SwapStorage + 'static,
179{
180 pub async fn prepare_ln_invoice_payment(
198 &self,
199 invoice: Bolt11Invoice,
200 ) -> Result<SubmarineSwapData, Error> {
201 let refund_keypair = self.next_keypair(crate::key_provider::KeypairIndex::New)?;
202 let refund_public_key = refund_keypair.public_key();
203 let key_derivation_index =
204 self.derivation_index_for_pk(&refund_keypair.x_only_public_key().0);
205
206 let preimage_hash = invoice.payment_hash();
207 let preimage_hash = ripemd160::Hash::hash(preimage_hash.as_byte_array());
208
209 let request = CreateSubmarineSwapRequest {
210 from: Asset::Ark,
211 to: Asset::Btc,
212 invoice,
213 refund_public_key: refund_public_key.into(),
214 referral_id: self.inner.boltz_referral_id.clone(),
215 };
216 let url = format!("{}/v2/swap/submarine", self.inner.boltz_url);
217
218 let client = reqwest::Client::new();
219 let response = client
220 .post(&url)
221 .json(&request)
222 .send()
223 .await
224 .map_err(|e| Error::ad_hoc(e.to_string()))
225 .context("failed to send submarine swap request")?;
226
227 if !response.status().is_success() {
228 let error_text = response
229 .text()
230 .await
231 .map_err(|e| Error::ad_hoc(e.to_string()))
232 .context("failed to read error text")?;
233
234 return Err(Error::ad_hoc(format!(
235 "failed to create submarine swap: {error_text}"
236 )));
237 }
238
239 let swap_response: CreateSubmarineSwapResponse = response
240 .json()
241 .await
242 .map_err(|e| Error::ad_hoc(e.to_string()))
243 .context("failed to deserialize submarine swap response")?;
244
245 let created_at = SystemTime::now()
246 .duration_since(UNIX_EPOCH)
247 .map_err(Error::ad_hoc)
248 .context("failed to compute created_at")?;
249
250 let data = SubmarineSwapData {
251 id: swap_response.id.clone(),
252 status: SwapStatus::Created,
253 preimage: None,
254 preimage_hash,
255 refund_public_key: refund_public_key.into(),
256 claim_public_key: swap_response.claim_public_key,
257 vhtlc_address: swap_response.address,
258 timeout_block_heights: swap_response.timeout_block_heights,
259 amount: swap_response.expected_amount,
260 invoice: request.invoice.clone(),
261 created_at: created_at.as_secs(),
262 key_derivation_index,
263 };
264
265 self.swap_storage()
266 .insert_submarine(swap_response.id.clone(), data.clone())
267 .await?;
268
269 tracing::info!(
270 swap_id = swap_response.id,
271 vhtlc_address = %data.vhtlc_address,
272 expected_amount = %data.amount,
273 "Prepared Lightning invoice payment"
274 );
275
276 Ok(data)
277 }
278
279 pub async fn pay_ln_invoice(
291 &self,
292 invoice: Bolt11Invoice,
293 ) -> Result<SubmarineSwapResult, Error> {
294 let refund_keypair = self.next_keypair(crate::key_provider::KeypairIndex::New)?;
295 let refund_public_key = refund_keypair.public_key();
296 let key_derivation_index =
297 self.derivation_index_for_pk(&refund_keypair.x_only_public_key().0);
298
299 let preimage_hash = invoice.payment_hash();
300 let preimage_hash = ripemd160::Hash::hash(preimage_hash.as_byte_array());
301
302 let request = CreateSubmarineSwapRequest {
303 from: Asset::Ark,
304 to: Asset::Btc,
305 invoice,
306 refund_public_key: refund_public_key.into(),
307 referral_id: self.inner.boltz_referral_id.clone(),
308 };
309 let url = format!("{}/v2/swap/submarine", self.inner.boltz_url);
310
311 let client = reqwest::Client::new();
312 let response = client
313 .post(&url)
314 .json(&request)
315 .send()
316 .await
317 .map_err(|e| Error::ad_hoc(e.to_string()))
318 .context("failed to send submarine swap request")?;
319
320 if !response.status().is_success() {
321 let error_text = response
322 .text()
323 .await
324 .map_err(|e| Error::ad_hoc(e.to_string()))
325 .context("failed to read error text")?;
326
327 return Err(Error::ad_hoc(format!(
328 "failed to create submarine swap: {error_text}"
329 )));
330 }
331
332 let swap_response: CreateSubmarineSwapResponse = response
333 .json()
334 .await
335 .map_err(|e| Error::ad_hoc(e.to_string()))
336 .context("failed to deserialize submarine swap response")?;
337
338 let created_at = SystemTime::now()
339 .duration_since(UNIX_EPOCH)
340 .map_err(Error::ad_hoc)
341 .context("failed to compute created_at")?;
342
343 self.swap_storage()
344 .insert_submarine(
345 swap_response.id.clone(),
346 SubmarineSwapData {
347 id: swap_response.id.clone(),
348 status: SwapStatus::Created,
349 preimage: None,
350 preimage_hash,
351 refund_public_key: refund_public_key.into(),
352 claim_public_key: swap_response.claim_public_key,
353 vhtlc_address: swap_response.address,
354 timeout_block_heights: swap_response.timeout_block_heights,
355 amount: swap_response.expected_amount,
356 invoice: request.invoice.clone(),
357 created_at: created_at.as_secs(),
358 key_derivation_index,
359 },
360 )
361 .await?;
362
363 let vhtlc_address = swap_response.address;
364 let amount = swap_response.expected_amount;
365
366 let txid = self
367 .send(vec![SendReceiver::bitcoin(vhtlc_address, amount)])
368 .await?;
369
370 tracing::info!(swap_id = swap_response.id, %amount, "Funded VHTLC");
371
372 Ok(SubmarineSwapResult {
373 swap_id: swap_response.id,
374 txid,
375 amount,
376 })
377 }
378
379 pub async fn wait_for_invoice_paid(&self, swap_id: &str) -> Result<[u8; 32], Error> {
389 use futures::StreamExt;
390
391 let stream = self.subscribe_to_swap_updates(swap_id.to_string());
392 tokio::pin!(stream);
393
394 while let Some(status_result) = stream.next().await {
395 match status_result {
396 Ok(status) => {
397 tracing::debug!(swap_id, current = ?status, "Swap status");
398 match status {
399 SwapStatus::InvoicePaid => {
400 let deadline = tokio::time::Instant::now() + self.inner.timeout;
401
402 loop {
403 match self.extract_submarine_swap_preimage(swap_id).await {
404 Ok(preimage) => return Ok(preimage),
405 Err(e) => {
406 if tokio::time::Instant::now() >= deadline {
407 return Err(e.context(
408 "invoice paid but failed to extract preimage from claim tx",
409 ));
410 }
411
412 tracing::debug!(
413 swap_id,
414 "Preimage not available yet, retrying: {e}"
415 );
416 }
417 }
418
419 tokio::time::sleep(std::time::Duration::from_secs(1)).await;
420 }
421 }
422 SwapStatus::InvoiceExpired => {
423 return Err(Error::ad_hoc(format!(
424 "invoice expired for swap {swap_id}"
425 )));
426 }
427 SwapStatus::Error { error } => {
428 tracing::error!(
429 swap_id,
430 "Got error from swap updates subscription: {error}"
431 );
432 }
433 SwapStatus::InvoiceSet
434 | SwapStatus::InvoicePending
435 | SwapStatus::Created
436 | SwapStatus::TransactionMempool
437 | SwapStatus::TransactionConfirmed
438 | SwapStatus::TransactionServerMempool
439 | SwapStatus::TransactionServerConfirmed
440 | SwapStatus::TransactionRefunded
441 | SwapStatus::TransactionFailed
442 | SwapStatus::TransactionClaimed
443 | SwapStatus::TransactionLockupFailed
444 | SwapStatus::InvoiceFailedToPay
445 | SwapStatus::SwapExpired
446 | SwapStatus::Other(_) => {}
447 }
448 }
449 Err(e) => return Err(e),
450 }
451 }
452
453 Err(Error::ad_hoc("Status stream ended unexpectedly"))
454 }
455
456 pub async fn extract_submarine_swap_preimage(&self, swap_id: &str) -> Result<[u8; 32], Error> {
465 let mut swap_data = self
466 .swap_storage()
467 .get_submarine(swap_id)
468 .await?
469 .ok_or(Error::ad_hoc("submarine swap not found"))?;
470
471 if let Some(preimage) = swap_data.preimage {
473 return Ok(preimage);
474 }
475
476 let vhtlc_address = swap_data.vhtlc_address;
477
478 let virtual_tx_outpoints = self
480 .get_virtual_tx_outpoints(std::iter::once(vhtlc_address))
481 .await
482 .context("failed to get virtual tx outpoints for VHTLC address")?;
483
484 let vhtlc_outpoint = virtual_tx_outpoints
485 .iter()
486 .find(|o| o.is_spent)
487 .ok_or_else(|| Error::ad_hoc("VHTLC outpoint not found or not yet spent (claimed)"))?;
488
489 let claim_txid = vhtlc_outpoint.ark_txid.ok_or_else(|| {
490 Error::ad_hoc("VHTLC is spent but has no ark_txid (claim transaction)")
491 })?;
492
493 let claim_txs = timeout_op(
495 self.inner.timeout,
496 self.network_client()
497 .get_virtual_txs(vec![claim_txid.to_string()], None),
498 )
499 .await?
500 .map_err(|e| Error::ad_hoc(e.to_string()))
501 .context("failed to fetch claim transaction")?;
502
503 let claim_psbt = claim_txs
504 .txs
505 .first()
506 .ok_or_else(|| Error::ad_hoc("claim transaction not found"))?;
507
508 let preimage = extract_preimage_from_psbt(claim_psbt)?;
510
511 let computed_hash = ripemd160::Hash::hash(sha256::Hash::hash(&preimage).as_byte_array());
513 if computed_hash != swap_data.preimage_hash {
514 return Err(Error::ad_hoc(format!(
515 "extracted preimage does not match stored hash: expected {}, got {}",
516 swap_data.preimage_hash, computed_hash
517 )));
518 }
519
520 swap_data.preimage = Some(preimage);
522 self.swap_storage()
523 .update_submarine(swap_id, swap_data)
524 .await
525 .context("failed to persist preimage to swap storage")?;
526
527 tracing::info!(
528 swap_id,
529 "Extracted and persisted preimage from claim transaction"
530 );
531
532 Ok(preimage)
533 }
534
535 pub async fn refund_expired_vhtlc(&self, swap_id: &str) -> Result<Txid, Error> {
539 let swap_data = self
540 .swap_storage()
541 .get_submarine(swap_id)
542 .await?
543 .ok_or(Error::ad_hoc("Submarine swap not found"))?;
544
545 let timeout_block_heights = swap_data.timeout_block_heights;
546 let server_info = self.server_info().await?;
547
548 let vhtlc = self.reconstruct_vhtlc_for_address(
549 &server_info,
550 |server| {
551 Ok(VhtlcOptions {
552 sender: swap_data.refund_public_key.into(),
553 receiver: swap_data.claim_public_key.into(),
554 server,
555 preimage_hash: swap_data.preimage_hash,
556 refund_locktime: timeout_block_heights.refund,
557 unilateral_claim_delay: parse_sequence_number(
558 timeout_block_heights.unilateral_claim as i64,
559 )
560 .map_err(|e| Error::ad_hoc(format!("invalid unilateral claim timeout: {e}")))?,
561 unilateral_refund_delay: parse_sequence_number(
562 timeout_block_heights.unilateral_refund as i64,
563 )
564 .map_err(|e| {
565 Error::ad_hoc(format!("invalid unilateral refund timeout: {e}"))
566 })?,
567 unilateral_refund_without_receiver_delay: parse_sequence_number(
568 timeout_block_heights.unilateral_refund_without_receiver as i64,
569 )
570 .map_err(|e| {
571 Error::ad_hoc(format!("invalid refund without receiver timeout: {e}"))
572 })?,
573 })
574 },
575 &swap_data.vhtlc_address,
576 )?;
577 let vhtlc_address = vhtlc.address();
578
579 let vhtlc_outpoint = {
580 let virtual_tx_outpoints = self
581 .get_virtual_tx_outpoints(std::iter::once(vhtlc_address))
582 .await?;
583
584 let vtxo_list = VtxoList::new(server_info.dust, virtual_tx_outpoints);
585
586 let mut unspent = vtxo_list.all_unspent();
588 let vhtlc_outpoint = unspent.next().ok_or_else(|| {
589 Error::ad_hoc(format!("no outpoint found for address {vhtlc_address}"))
590 })?;
591
592 vhtlc_outpoint.clone()
593 };
594
595 let (refund_address, _) = self.get_offchain_address().await?;
596 let refund_amount = swap_data.amount;
597
598 let outputs = vec![SendReceiver {
599 address: refund_address,
600 amount: refund_amount,
601 assets: Vec::new(),
602 }];
603
604 let refund_script = vhtlc.refund_without_receiver_script();
605
606 let spend_info = vhtlc.taproot_spend_info();
607 let script_ver = (refund_script, LeafVersion::TapScript);
608 let control_block = spend_info
609 .control_block(&script_ver)
610 .ok_or(Error::ad_hoc("control block not found for refund script"))?;
611
612 let script_pubkey = vhtlc.script_pubkey();
613
614 let refunder_pk = swap_data.refund_public_key.inner.x_only_public_key().0;
615 let vhtlc_input = VtxoInput::new(
616 script_ver.0,
617 Some(absolute::LockTime::from_consensus(
618 swap_data.timeout_block_heights.refund,
619 )),
620 control_block,
621 vhtlc.tapscripts(),
622 script_pubkey,
623 refund_amount,
624 vhtlc_outpoint.outpoint,
625 vhtlc_outpoint.assets,
626 );
627
628 let change_address = &refund_address;
630
631 let OffchainTransactions {
632 mut ark_tx,
633 checkpoint_txs,
634 } = build_offchain_transactions(
635 &outputs,
636 change_address,
637 std::slice::from_ref(&vhtlc_input),
638 &server_info,
639 )?;
640
641 let kp = self.keypair_by_pk(&refunder_pk)?;
642 let sign_fn =
643 |_: &mut psbt::Input,
644 msg: secp256k1::Message|
645 -> Result<Vec<(schnorr::Signature, XOnlyPublicKey)>, ark_core::Error> {
646 let sig = Secp256k1::new().sign_schnorr_no_aux_rand(&msg, &kp);
647 let pk = kp.x_only_public_key().0;
648
649 Ok(vec![(sig, pk)])
650 };
651
652 sign_ark_transaction(sign_fn, &mut ark_tx, 0)?;
653
654 let ark_txid = ark_tx.unsigned_tx.compute_txid();
655
656 let res = self
657 .network_client()
658 .submit_offchain_transaction_request(ark_tx, checkpoint_txs)
659 .await?;
660
661 let mut checkpoint_psbt = res
662 .signed_checkpoint_txs
663 .first()
664 .ok_or_else(|| Error::ad_hoc("no checkpoint PSBTs found"))?
665 .clone();
666
667 let kp = self.keypair_by_pk(&refunder_pk)?;
668 let sign_fn =
669 |_: &mut psbt::Input,
670 msg: secp256k1::Message|
671 -> Result<Vec<(schnorr::Signature, XOnlyPublicKey)>, ark_core::Error> {
672 let sig = Secp256k1::new().sign_schnorr_no_aux_rand(&msg, &kp);
673 let pk = kp.x_only_public_key().0;
674
675 Ok(vec![(sig, pk)])
676 };
677
678 sign_checkpoint_transaction(sign_fn, &mut checkpoint_psbt)?;
679
680 timeout_op(
681 self.inner.timeout,
682 self.network_client()
683 .finalize_offchain_transaction(ark_txid, vec![checkpoint_psbt]),
684 )
685 .await?
686 .map_err(Error::ark_server)
687 .context("failed to finalize offchain transaction")?;
688
689 tracing::info!(txid = %ark_txid, "Refunded VHTLC");
690
691 Ok(ark_txid)
692 }
693
694 pub async fn refund_expired_vhtlc_via_settlement<R>(
698 &self,
699 rng: &mut R,
700 swap_id: &str,
701 ) -> Result<Txid, Error>
702 where
703 R: Rng + CryptoRng,
704 {
705 let swap_data = self
706 .swap_storage()
707 .get_submarine(swap_id)
708 .await?
709 .ok_or(Error::ad_hoc("Submarine swap not found"))?;
710
711 let timeout_block_heights = swap_data.timeout_block_heights;
712 let server_info = self.server_info().await?;
713
714 let vhtlc = self.reconstruct_vhtlc_for_address(
715 &server_info,
716 |server| {
717 Ok(VhtlcOptions {
718 sender: swap_data.refund_public_key.into(),
719 receiver: swap_data.claim_public_key.into(),
720 server,
721 preimage_hash: swap_data.preimage_hash,
722 refund_locktime: timeout_block_heights.refund,
723 unilateral_claim_delay: parse_sequence_number(
724 timeout_block_heights.unilateral_claim as i64,
725 )
726 .map_err(|e| Error::ad_hoc(format!("invalid unilateral claim timeout: {e}")))?,
727 unilateral_refund_delay: parse_sequence_number(
728 timeout_block_heights.unilateral_refund as i64,
729 )
730 .map_err(|e| {
731 Error::ad_hoc(format!("invalid unilateral refund timeout: {e}"))
732 })?,
733 unilateral_refund_without_receiver_delay: parse_sequence_number(
734 timeout_block_heights.unilateral_refund_without_receiver as i64,
735 )
736 .map_err(|e| {
737 Error::ad_hoc(format!("invalid refund without receiver timeout: {e}"))
738 })?,
739 })
740 },
741 &swap_data.vhtlc_address,
742 )?;
743 let vhtlc_address = vhtlc.address();
744
745 let vhtlc_outpoint = {
746 let virtual_tx_outpoints = self
747 .get_virtual_tx_outpoints(std::iter::once(vhtlc_address))
748 .await?;
749
750 let vtxo_list = VtxoList::new(server_info.dust, virtual_tx_outpoints);
751
752 let mut recoverable = vtxo_list.recoverable();
754
755 recoverable
756 .next()
757 .ok_or_else(|| {
758 Error::ad_hoc(format!("no outpoint found for address {vhtlc_address}"))
759 })?
760 .clone()
761 };
762
763 let refund_script = vhtlc.refund_without_receiver_script();
764
765 let spend_info = vhtlc.taproot_spend_info();
766 let script_ver = (refund_script, LeafVersion::TapScript);
767 let control_block = spend_info
768 .control_block(&script_ver)
769 .ok_or(Error::ad_hoc("control block not found for refund script"))?;
770
771 let script_pubkey = vhtlc.script_pubkey();
772
773 let (refund_address, _) = self.get_offchain_address_with_server_info(&server_info)?;
774 let refund_amount = swap_data.amount;
775
776 let vhtlc_input = intent::Input::new(
777 vhtlc_outpoint.outpoint,
778 parse_sequence_number(timeout_block_heights.unilateral_refund as i64)
779 .map_err(|e| Error::ad_hoc(format!("invalid unilateral refund timeout: {e}")))?,
780 Some(absolute::LockTime::from_consensus(
781 timeout_block_heights.refund,
782 )),
783 TxOut {
784 value: refund_amount,
785 script_pubkey,
786 },
787 vhtlc.tapscripts(),
788 (script_ver.0, control_block),
789 false,
790 true,
791 vhtlc_outpoint.assets,
792 );
793
794 let commitment_txid = self
795 .join_next_batch(
796 rng,
797 &server_info,
798 Vec::new(),
799 vec![vhtlc_input],
800 BatchOutputType::Board {
801 to_address: refund_address,
802 to_amount: refund_amount,
803 },
804 )
805 .await
806 .context("failed to join batch")?;
807
808 tracing::info!(txid = %commitment_txid, "Refunded VHTLC via settlement");
809
810 Ok(commitment_txid)
811 }
812
813 pub async fn refund_vhtlc(&self, swap_id: &str) -> Result<Txid, Error> {
819 let swap_data = self
820 .swap_storage()
821 .get_submarine(swap_id)
822 .await?
823 .ok_or(Error::ad_hoc("submarine swap not found"))?;
824
825 let timeout_block_heights = swap_data.timeout_block_heights;
826 let server_info = self.server_info().await?;
827
828 let vhtlc = self.reconstruct_vhtlc_for_address(
829 &server_info,
830 |server| {
831 Ok(VhtlcOptions {
832 sender: swap_data.refund_public_key.into(),
833 receiver: swap_data.claim_public_key.into(),
834 server,
835 preimage_hash: swap_data.preimage_hash,
836 refund_locktime: timeout_block_heights.refund,
837 unilateral_claim_delay: parse_sequence_number(
838 timeout_block_heights.unilateral_claim as i64,
839 )
840 .map_err(|e| Error::ad_hoc(format!("invalid unilateral claim timeout: {e}")))?,
841 unilateral_refund_delay: parse_sequence_number(
842 timeout_block_heights.unilateral_refund as i64,
843 )
844 .map_err(|e| {
845 Error::ad_hoc(format!("invalid unilateral refund timeout: {e}"))
846 })?,
847 unilateral_refund_without_receiver_delay: parse_sequence_number(
848 timeout_block_heights.unilateral_refund_without_receiver as i64,
849 )
850 .map_err(|e| {
851 Error::ad_hoc(format!("invalid refund without receiver timeout: {e}"))
852 })?,
853 })
854 },
855 &swap_data.vhtlc_address,
856 )?;
857 let vhtlc_address = vhtlc.address();
858
859 let vhtlc_outpoint = {
860 let virtual_tx_outpoints = self
861 .get_virtual_tx_outpoints(std::iter::once(vhtlc_address))
862 .await?;
863
864 let vtxo_list = VtxoList::new(server_info.dust, virtual_tx_outpoints);
865
866 let mut unspent = vtxo_list.all_unspent();
868 let vhtlc_outpoint = unspent.next().ok_or_else(|| {
869 Error::ad_hoc(format!("no outpoint found for address {vhtlc_address}"))
870 })?;
871
872 vhtlc_outpoint.clone()
873 };
874
875 let (refund_address, _) = self.get_offchain_address().await?;
876 let refund_amount = swap_data.amount;
877
878 let outputs = vec![SendReceiver {
879 address: refund_address,
880 amount: refund_amount,
881 assets: Vec::new(),
882 }];
883
884 let refund_script = vhtlc.refund_script();
886
887 let spend_info = vhtlc.taproot_spend_info();
888 let script_ver = (refund_script, LeafVersion::TapScript);
889 let control_block = spend_info
890 .control_block(&script_ver)
891 .ok_or(Error::ad_hoc("control block not found for refund script"))?;
892
893 let script_pubkey = vhtlc.script_pubkey();
894
895 let refunder_pk = swap_data.refund_public_key.inner.x_only_public_key().0;
896 let vhtlc_input = VtxoInput::new(
897 script_ver.0,
898 None, control_block,
900 vhtlc.tapscripts(),
901 script_pubkey,
902 refund_amount,
903 vhtlc_outpoint.outpoint,
904 vhtlc_outpoint.assets,
905 );
906
907 let change_address = &refund_address;
909
910 let OffchainTransactions {
911 mut ark_tx,
912 checkpoint_txs,
913 } = build_offchain_transactions(
914 &outputs,
915 change_address,
916 std::slice::from_ref(&vhtlc_input),
917 &server_info,
918 )?;
919
920 let kp = self.keypair_by_pk(&refunder_pk)?;
922 let sign_fn =
923 |_: &mut psbt::Input,
924 msg: secp256k1::Message|
925 -> Result<Vec<(schnorr::Signature, XOnlyPublicKey)>, ark_core::Error> {
926 let sig = Secp256k1::new().sign_schnorr_no_aux_rand(&msg, &kp);
927 let pk = kp.x_only_public_key().0;
928
929 Ok(vec![(sig, pk)])
930 };
931
932 sign_ark_transaction(sign_fn, &mut ark_tx, 0)?;
933
934 let checkpoint_psbt = checkpoint_txs
936 .first()
937 .ok_or_else(|| Error::ad_hoc("no checkpoint PSBTs found"))?
938 .clone();
939
940 let url = format!(
943 "{}/v2/swap/submarine/{swap_id}/refund/ark",
944 self.inner.boltz_url
945 );
946 let client = reqwest::Client::new();
947 let response = client
948 .post(&url)
949 .json(&RefundSwapRequest {
950 transaction: ark_tx.to_string(),
951 checkpoint: checkpoint_psbt.to_string(),
952 })
953 .send()
954 .await
955 .map_err(Error::ad_hoc)
956 .context("failed to send refund request to Boltz")?;
957
958 if !response.status().is_success() {
959 let error_text = response
960 .text()
961 .await
962 .map_err(|e| Error::ad_hoc(e.to_string()))
963 .context("failed to read error text")?;
964
965 return Err(Error::ad_hoc(format!(
966 "Boltz refund request failed: {error_text}"
967 )));
968 }
969
970 let refund_response: RefundSwapResponse = response
971 .json()
972 .await
973 .map_err(Error::ad_hoc)
974 .context("failed to deserialize refund response")?;
975
976 if let Some(err) = refund_response.error.as_deref() {
977 return Err(Error::ad_hoc(format!("Boltz refund request failed: {err}")));
978 }
979
980 let boltz_signed_ark_tx = Psbt::from_str(&refund_response.transaction)
982 .map_err(Error::ad_hoc)
983 .context("could not parse refund transaction PSBT")?;
984
985 let boltz_signed_checkpoint = Psbt::from_str(&refund_response.checkpoint)
986 .map_err(Error::ad_hoc)
987 .context("could not parse refund checkpoint PSBT")?;
988
989 let ark_txid = boltz_signed_ark_tx.unsigned_tx.compute_txid();
990
991 let boltz_tap_script_sigs = boltz_signed_checkpoint
993 .inputs
994 .first()
995 .ok_or_else(|| Error::ad_hoc("boltz checkpoint has no inputs"))?
996 .tap_script_sigs
997 .clone();
998
999 let res = self
1002 .network_client()
1003 .submit_offchain_transaction_request(boltz_signed_ark_tx, vec![boltz_signed_checkpoint])
1004 .await?;
1005
1006 let mut server_signed_checkpoint = res
1009 .signed_checkpoint_txs
1010 .first()
1011 .ok_or_else(|| Error::ad_hoc("no signed checkpoint PSBTs returned"))?
1012 .clone();
1013
1014 let kp = self.keypair_by_pk(&refunder_pk)?;
1015 let sign_fn =
1016 |_: &mut psbt::Input,
1017 msg: secp256k1::Message|
1018 -> Result<Vec<(schnorr::Signature, XOnlyPublicKey)>, ark_core::Error> {
1019 let sig = Secp256k1::new().sign_schnorr_no_aux_rand(&msg, &kp);
1020 let pk = kp.x_only_public_key().0;
1021
1022 Ok(vec![(sig, pk)])
1023 };
1024
1025 server_signed_checkpoint
1026 .inputs
1027 .first_mut()
1028 .ok_or_else(|| Error::ad_hoc("server checkpoint has no inputs"))?
1029 .tap_script_sigs
1030 .extend(boltz_tap_script_sigs);
1031
1032 sign_checkpoint_transaction(sign_fn, &mut server_signed_checkpoint)?;
1033
1034 timeout_op(
1036 self.inner.timeout,
1037 self.network_client()
1038 .finalize_offchain_transaction(ark_txid, vec![server_signed_checkpoint]),
1039 )
1040 .await?
1041 .map_err(Error::ark_server)
1042 .context("failed to finalize offchain transaction")?;
1043
1044 tracing::info!(swap_id, txid = %ark_txid, "Refunded VHTLC via collaborative refund");
1045
1046 Ok(ark_txid)
1047 }
1048
1049 async fn validate_reverse_recipient_address(
1052 &self,
1053 recipient_address: Option<&ArkAddress>,
1054 ) -> Result<(), Error> {
1055 let Some(recipient_address) = recipient_address else {
1056 return Ok(());
1057 };
1058
1059 let server_info = self.server_info().await?;
1060 let server_signer: XOnlyPublicKey = server_info.signer_pk.into();
1061 if recipient_address.server() != server_signer {
1062 return Err(Error::consumer(format!(
1063 "recipient Arkade address belongs to a different server: expected {server_signer}, got {}",
1064 recipient_address.server()
1065 )));
1066 }
1067
1068 Ok(())
1069 }
1070
1071 async fn reverse_claim_address(&self, swap: &ReverseSwapData) -> Result<ArkAddress, Error> {
1072 if let Some(address) = swap.claim_address {
1073 self.validate_reverse_recipient_address(Some(&address))
1074 .await?;
1075 return Ok(address);
1076 }
1077
1078 let (address, _) = self
1079 .get_offchain_address()
1080 .await
1081 .context("failed to get offchain address")?;
1082
1083 Ok(address)
1084 }
1085
1086 pub async fn get_ln_invoice(
1102 &self,
1103 amount: SwapAmount,
1104 expiry_secs: Option<u64>,
1105 description: Option<String>,
1106 ) -> Result<ReverseSwapResult, Error> {
1107 self.create_reverse_swap_invoice_with_new_preimage(amount, expiry_secs, None, description)
1108 .await
1109 }
1110
1111 pub async fn get_ln_invoice_for_address(
1130 &self,
1131 amount: SwapAmount,
1132 recipient_address: ArkAddress,
1133 expiry_secs: Option<u64>,
1134 description: Option<String>,
1135 ) -> Result<ReverseSwapResult, Error> {
1136 self.create_reverse_swap_invoice_with_new_preimage(
1137 amount,
1138 expiry_secs,
1139 Some(recipient_address),
1140 description,
1141 )
1142 .await
1143 }
1144
1145 async fn create_reverse_swap_invoice_with_new_preimage(
1146 &self,
1147 amount: SwapAmount,
1148 expiry_secs: Option<u64>,
1149 recipient_address: Option<ArkAddress>,
1150 description: Option<String>,
1151 ) -> Result<ReverseSwapResult, Error> {
1152 let preimage: [u8; 32] = rand::random();
1153 let preimage_hash_sha256 = sha256::Hash::hash(&preimage);
1154
1155 self.create_reverse_swap_invoice(
1156 amount,
1157 expiry_secs,
1158 preimage_hash_sha256,
1159 Some(preimage),
1160 recipient_address,
1161 description,
1162 )
1163 .await
1164 }
1165
1166 pub async fn get_ln_invoice_from_hash(
1190 &self,
1191 amount: SwapAmount,
1192 expiry_secs: Option<u64>,
1193 preimage_hash_sha256: sha256::Hash,
1194 description: Option<String>,
1195 ) -> Result<ReverseSwapResult, Error> {
1196 self.create_reverse_swap_invoice(
1197 amount,
1198 expiry_secs,
1199 preimage_hash_sha256,
1200 None,
1201 None,
1202 description,
1203 )
1204 .await
1205 }
1206
1207 pub async fn get_ln_invoice_from_hash_for_address(
1213 &self,
1214 amount: SwapAmount,
1215 recipient_address: ArkAddress,
1216 expiry_secs: Option<u64>,
1217 preimage_hash_sha256: sha256::Hash,
1218 description: Option<String>,
1219 ) -> Result<ReverseSwapResult, Error> {
1220 self.create_reverse_swap_invoice(
1221 amount,
1222 expiry_secs,
1223 preimage_hash_sha256,
1224 None,
1225 Some(recipient_address),
1226 description,
1227 )
1228 .await
1229 }
1230
1231 async fn create_reverse_swap_invoice(
1232 &self,
1233 amount: SwapAmount,
1234 expiry_secs: Option<u64>,
1235 preimage_hash_sha256: sha256::Hash,
1236 preimage: Option<[u8; 32]>,
1237 recipient_address: Option<ArkAddress>,
1238 description: Option<String>,
1239 ) -> Result<ReverseSwapResult, Error> {
1240 validate_invoice_description(description.as_deref())?;
1241 self.validate_reverse_recipient_address(recipient_address.as_ref())
1242 .await?;
1243
1244 let preimage_hash = ripemd160::Hash::hash(preimage_hash_sha256.as_byte_array());
1245
1246 let claim_keypair = self.next_keypair(crate::key_provider::KeypairIndex::New)?;
1247 let claim_public_key = claim_keypair.public_key();
1248 let key_derivation_index =
1249 self.derivation_index_for_pk(&claim_keypair.x_only_public_key().0);
1250
1251 let (invoice_amount, onchain_amount) = match amount {
1252 SwapAmount::Invoice(amount) => (Some(amount), None),
1253 SwapAmount::Vhtlc(amount) => (None, Some(amount)),
1254 };
1255
1256 let request = CreateReverseSwapRequest {
1257 from: Asset::Btc,
1258 to: Asset::Ark,
1259 invoice_amount,
1260 onchain_amount,
1261 claim_public_key: claim_public_key.into(),
1262 preimage_hash: preimage_hash_sha256,
1263 invoice_expiry: expiry_secs,
1264 referral_id: self.inner.boltz_referral_id.clone(),
1265 description,
1266 };
1267
1268 let url = format!("{}/v2/swap/reverse", self.inner.boltz_url);
1269
1270 let client = reqwest::Client::new();
1271 let response = client
1272 .post(&url)
1273 .json(&request)
1274 .send()
1275 .await
1276 .map_err(|e| Error::ad_hoc(e.to_string()))
1277 .context("failed to send reverse swap request")?;
1278
1279 if !response.status().is_success() {
1280 let error_text = response
1281 .text()
1282 .await
1283 .map_err(|e| Error::ad_hoc(e.to_string()))
1284 .context("failed to read error text")?;
1285
1286 return Err(Error::ad_hoc(format!(
1287 "failed to create reverse swap: {error_text}"
1288 )));
1289 }
1290
1291 let response: CreateReverseSwapResponse = response
1292 .json()
1293 .await
1294 .map_err(|e| Error::ad_hoc(e.to_string()))
1295 .context("failed to deserialize reverse swap response")?;
1296
1297 let created_at = SystemTime::now()
1298 .duration_since(UNIX_EPOCH)
1299 .map_err(Error::ad_hoc)
1300 .context("failed to compute created_at")?;
1301
1302 let swap_amount = response.onchain_amount.or(onchain_amount).ok_or_else(|| {
1303 Error::ad_hoc("onchain_amount not provided by Boltz and not specified in request")
1304 })?;
1305
1306 let swap = ReverseSwapData {
1307 id: response.id.clone(),
1308 status: SwapStatus::Created,
1309 preimage,
1310 vhtlc_address: response.lockup_address,
1311 preimage_hash,
1312 refund_public_key: response.refund_public_key,
1313 amount: swap_amount,
1314 claim_public_key: claim_public_key.into(),
1315 timeout_block_heights: response.timeout_block_heights,
1316 created_at: created_at.as_secs(),
1317 key_derivation_index,
1318 bolt11: response.invoice.to_string(),
1319 invoice_expiry: response.invoice.expiry_time().as_secs(),
1320 claim_address: recipient_address,
1321 };
1322
1323 self.swap_storage()
1324 .insert_reverse(response.id.clone(), swap.clone())
1325 .await
1326 .context("failed to persist swap data")?;
1327
1328 Ok(ReverseSwapResult {
1329 swap_id: swap.id,
1330 invoice: response.invoice,
1331 amount: swap_amount,
1332 })
1333 }
1334
1335 pub async fn wait_for_vhtlc_funding(&self, swap_id: &str) -> Result<(), Error> {
1348 use futures::StreamExt;
1349
1350 let stream = self.subscribe_to_swap_updates(swap_id.to_string());
1351 tokio::pin!(stream);
1352
1353 while let Some(status_result) = stream.next().await {
1354 match status_result {
1355 Ok(status) => {
1356 tracing::debug!(swap_id, current = ?status, "Swap status");
1357
1358 match status {
1359 SwapStatus::TransactionMempool | SwapStatus::TransactionConfirmed => {
1360 tracing::debug!(swap_id, "VHTLC funding detected");
1361 return Ok(());
1362 }
1363 SwapStatus::InvoiceExpired => {
1364 return Err(Error::ad_hoc(format!(
1365 "invoice expired for swap {swap_id}"
1366 )));
1367 }
1368 SwapStatus::Error { error } => {
1369 tracing::error!(
1370 swap_id,
1371 "Got error from swap updates subscription: {error}"
1372 );
1373 }
1374 SwapStatus::Created
1376 | SwapStatus::TransactionRefunded
1377 | SwapStatus::TransactionFailed
1378 | SwapStatus::TransactionClaimed
1379 | SwapStatus::TransactionLockupFailed
1380 | SwapStatus::TransactionServerMempool
1381 | SwapStatus::TransactionServerConfirmed
1382 | SwapStatus::InvoiceSet
1383 | SwapStatus::InvoicePending
1384 | SwapStatus::InvoicePaid
1385 | SwapStatus::InvoiceFailedToPay
1386 | SwapStatus::SwapExpired
1387 | SwapStatus::Other(_) => {}
1388 }
1389 }
1390 Err(e) => return Err(e),
1391 }
1392 }
1393
1394 Err(Error::ad_hoc("Status stream ended unexpectedly"))
1395 }
1396
1397 pub async fn claim_vhtlc(
1411 &self,
1412 swap_id: &str,
1413 preimage: [u8; 32],
1414 ) -> Result<ClaimVhtlcResult, Error> {
1415 let swap = self
1416 .swap_storage()
1417 .get_reverse(swap_id)
1418 .await
1419 .context("failed to get reverse swap data")?
1420 .ok_or_else(|| Error::ad_hoc(format!("reverse swap data not found: {swap_id}")))?;
1421
1422 let preimage_hash_sha256 = sha256::Hash::hash(&preimage);
1424 let preimage_hash = ripemd160::Hash::hash(preimage_hash_sha256.as_byte_array());
1425
1426 if preimage_hash != swap.preimage_hash {
1427 return Err(Error::ad_hoc(format!(
1428 "preimage does not match stored hash for swap {swap_id}"
1429 )));
1430 }
1431
1432 tracing::debug!(swap_id, "Claiming VHTLC with verified preimage");
1433
1434 let timeout_block_heights = swap.timeout_block_heights;
1435 let server_info = self.server_info().await?;
1436
1437 let vhtlc = self.reconstruct_vhtlc_for_address(
1438 &server_info,
1439 |server| {
1440 Ok(VhtlcOptions {
1441 sender: swap.refund_public_key.into(),
1442 receiver: swap.claim_public_key.into(),
1443 server,
1444 preimage_hash: swap.preimage_hash,
1445 refund_locktime: timeout_block_heights.refund,
1446 unilateral_claim_delay: parse_sequence_number(
1447 timeout_block_heights.unilateral_claim as i64,
1448 )
1449 .map_err(|e| Error::ad_hoc(format!("invalid unilateral claim timeout: {e}")))?,
1450 unilateral_refund_delay: parse_sequence_number(
1451 timeout_block_heights.unilateral_refund as i64,
1452 )
1453 .map_err(|e| {
1454 Error::ad_hoc(format!("invalid unilateral refund timeout: {e}"))
1455 })?,
1456 unilateral_refund_without_receiver_delay: parse_sequence_number(
1457 timeout_block_heights.unilateral_refund_without_receiver as i64,
1458 )
1459 .map_err(|e| {
1460 Error::ad_hoc(format!("invalid refund without receiver timeout: {e}"))
1461 })?,
1462 })
1463 },
1464 &swap.vhtlc_address,
1465 )?;
1466 let vhtlc_address = vhtlc.address();
1467
1468 let vhtlc_outpoint = {
1470 let virtual_tx_outpoints = self
1471 .get_virtual_tx_outpoints(std::iter::once(vhtlc_address))
1472 .await?;
1473
1474 let vtxo_list = VtxoList::new(server_info.dust, virtual_tx_outpoints);
1475
1476 let mut unspent = vtxo_list.all_unspent();
1478 let vhtlc_outpoint = unspent.next().ok_or_else(|| {
1479 Error::ad_hoc(format!("no outpoint found for address {vhtlc_address}"))
1480 })?;
1481
1482 vhtlc_outpoint.clone()
1483 };
1484
1485 let claim_address = self.reverse_claim_address(&swap).await?;
1486 let claim_amount = swap.amount;
1487
1488 let outputs = vec![SendReceiver {
1489 address: claim_address,
1490 amount: claim_amount,
1491 assets: Vec::new(),
1492 }];
1493
1494 let spend_info = vhtlc.taproot_spend_info();
1495 let script_ver = (vhtlc.claim_script(), LeafVersion::TapScript);
1496 let control_block = spend_info
1497 .control_block(&script_ver)
1498 .ok_or(Error::ad_hoc("control block not found for claim script"))?;
1499
1500 let script_pubkey = vhtlc.script_pubkey();
1501
1502 let claimer_pk = swap.claim_public_key.inner.x_only_public_key().0;
1503 let vhtlc_input = VtxoInput::new(
1504 script_ver.0,
1505 None,
1506 control_block,
1507 vhtlc.tapscripts(),
1508 script_pubkey,
1509 claim_amount,
1510 vhtlc_outpoint.outpoint,
1511 vhtlc_outpoint.assets,
1512 );
1513
1514 let change_address = &claim_address;
1516
1517 let OffchainTransactions {
1518 mut ark_tx,
1519 checkpoint_txs,
1520 } = build_offchain_transactions(
1521 &outputs,
1522 change_address,
1523 std::slice::from_ref(&vhtlc_input),
1524 &server_info,
1525 )
1526 .map_err(Error::from)
1527 .context("failed to build offchain TXs")?;
1528
1529 let kp = self.keypair_by_pk(&claimer_pk)?;
1530 let sign_fn =
1531 |input: &mut psbt::Input,
1532 msg: secp256k1::Message|
1533 -> Result<Vec<(schnorr::Signature, XOnlyPublicKey)>, ark_core::Error> {
1534 {
1536 let mut bytes = vec![1];
1538
1539 let length = VarInt::from(preimage.len() as u64);
1540
1541 length
1542 .consensus_encode(&mut bytes)
1543 .expect("valid length encoding");
1544
1545 bytes.write_all(&preimage).expect("valid preimage encoding");
1546
1547 input.unknown.insert(
1548 psbt::raw::Key {
1549 type_value: 222,
1550 key: VTXO_CONDITION_KEY.to_vec(),
1551 },
1552 bytes,
1553 );
1554 }
1555
1556 let sig = Secp256k1::new().sign_schnorr_no_aux_rand(&msg, &kp);
1557 let pk = kp.x_only_public_key().0;
1558
1559 Ok(vec![(sig, pk)])
1560 };
1561
1562 sign_ark_transaction(sign_fn, &mut ark_tx, 0)
1563 .map_err(Error::from)
1564 .context("failed to sign Ark TX")?;
1565
1566 let ark_txid = ark_tx.unsigned_tx.compute_txid();
1567
1568 let res = self
1569 .network_client()
1570 .submit_offchain_transaction_request(ark_tx, checkpoint_txs)
1571 .await
1572 .map_err(Error::from)
1573 .context("failed to submit offchain TXs")?;
1574
1575 let mut checkpoint_psbt = res
1576 .signed_checkpoint_txs
1577 .first()
1578 .ok_or_else(|| Error::ad_hoc("no checkpoint PSBTs found"))?
1579 .clone();
1580
1581 sign_checkpoint_transaction(sign_fn, &mut checkpoint_psbt)
1582 .map_err(Error::from)
1583 .context("failed to sign checkpoint TX")?;
1584
1585 timeout_op(
1586 self.inner.timeout,
1587 self.network_client()
1588 .finalize_offchain_transaction(ark_txid, vec![checkpoint_psbt]),
1589 )
1590 .await
1591 .context("failed to finalize offchain transaction")?
1592 .map_err(Error::ark_server)
1593 .context("failed to finalize offchain transaction")?;
1594
1595 tracing::info!(swap_id, txid = %ark_txid, "Claimed VHTLC");
1596
1597 let mut updated_swap = swap.clone();
1599 updated_swap.preimage = Some(preimage);
1600 self.swap_storage()
1601 .update_reverse(swap_id, updated_swap)
1602 .await
1603 .context("failed to update swap data with preimage")?;
1604
1605 Ok(ClaimVhtlcResult {
1606 swap_id: swap_id.to_string(),
1607 claim_txid: ark_txid,
1608 claim_amount,
1609 preimage,
1610 })
1611 }
1612
1613 pub async fn wait_for_vhtlc(&self, swap_id: &str) -> Result<ClaimVhtlcResult, Error> {
1621 use futures::StreamExt;
1622
1623 let swap = self
1624 .swap_storage()
1625 .get_reverse(swap_id)
1626 .await
1627 .context("failed to get reverse swap data")?
1628 .ok_or_else(|| Error::ad_hoc(format!("reverse swap data not found: {swap_id}")))?;
1629
1630 let preimage = swap.preimage.ok_or_else(|| {
1632 Error::ad_hoc(format!(
1633 "preimage not found in storage for swap {swap_id}. \
1634 Use wait_for_vhtlc_funding and claim_vhtlc instead."
1635 ))
1636 })?;
1637
1638 let stream = self.subscribe_to_swap_updates(swap_id.to_string());
1639 tokio::pin!(stream);
1640
1641 while let Some(status_result) = stream.next().await {
1642 match status_result {
1643 Ok(status) => {
1644 tracing::debug!(current = ?status, "Swap status");
1645
1646 match status {
1647 SwapStatus::TransactionMempool | SwapStatus::TransactionConfirmed => break,
1648 SwapStatus::InvoiceExpired => {
1649 return Err(Error::ad_hoc(format!(
1650 "invoice expired for swap {swap_id}"
1651 )));
1652 }
1653 SwapStatus::Error { error } => {
1654 tracing::error!(
1655 swap_id,
1656 "Got error from swap updates subscription: {error}"
1657 );
1658 }
1659 SwapStatus::Created
1661 | SwapStatus::TransactionRefunded
1662 | SwapStatus::TransactionFailed
1663 | SwapStatus::TransactionClaimed
1664 | SwapStatus::TransactionLockupFailed
1665 | SwapStatus::TransactionServerMempool
1666 | SwapStatus::TransactionServerConfirmed
1667 | SwapStatus::InvoiceSet
1668 | SwapStatus::InvoicePending
1669 | SwapStatus::InvoicePaid
1670 | SwapStatus::InvoiceFailedToPay
1671 | SwapStatus::SwapExpired
1672 | SwapStatus::Other(_) => {}
1673 }
1674 }
1675 Err(e) => return Err(e),
1676 }
1677 }
1678
1679 tracing::debug!("Ark transaction for swap found");
1680
1681 let timeout_block_heights = swap.timeout_block_heights;
1682 let server_info = self.server_info().await?;
1683
1684 let vhtlc = self.reconstruct_vhtlc_for_address(
1685 &server_info,
1686 |server| {
1687 Ok(VhtlcOptions {
1688 sender: swap.refund_public_key.into(),
1689 receiver: swap.claim_public_key.into(),
1690 server,
1691 preimage_hash: swap.preimage_hash,
1692 refund_locktime: timeout_block_heights.refund,
1693 unilateral_claim_delay: parse_sequence_number(
1694 timeout_block_heights.unilateral_claim as i64,
1695 )
1696 .map_err(|e| Error::ad_hoc(format!("invalid unilateral claim timeout: {e}")))?,
1697 unilateral_refund_delay: parse_sequence_number(
1698 timeout_block_heights.unilateral_refund as i64,
1699 )
1700 .map_err(|e| {
1701 Error::ad_hoc(format!("invalid unilateral refund timeout: {e}"))
1702 })?,
1703 unilateral_refund_without_receiver_delay: parse_sequence_number(
1704 timeout_block_heights.unilateral_refund_without_receiver as i64,
1705 )
1706 .map_err(|e| {
1707 Error::ad_hoc(format!("invalid refund without receiver timeout: {e}"))
1708 })?,
1709 })
1710 },
1711 &swap.vhtlc_address,
1712 )?;
1713 let vhtlc_address = vhtlc.address();
1714
1715 let vhtlc_outpoint = {
1717 let virtual_tx_outpoints = self
1718 .get_virtual_tx_outpoints(std::iter::once(vhtlc_address))
1719 .await?;
1720
1721 let vtxo_list = VtxoList::new(server_info.dust, virtual_tx_outpoints);
1722
1723 let mut unspent = vtxo_list.all_unspent();
1725 let vhtlc_outpoint = unspent.next().ok_or_else(|| {
1726 Error::ad_hoc(format!("no outpoint found for address {vhtlc_address}"))
1727 })?;
1728
1729 vhtlc_outpoint.clone()
1730 };
1731
1732 let claim_address = self.reverse_claim_address(&swap).await?;
1733 let claim_amount = swap.amount;
1734
1735 let outputs = vec![SendReceiver {
1736 address: claim_address,
1737 amount: claim_amount,
1738 assets: Vec::new(),
1739 }];
1740
1741 let spend_info = vhtlc.taproot_spend_info();
1742 let script_ver = (vhtlc.claim_script(), LeafVersion::TapScript);
1743 let control_block = spend_info
1744 .control_block(&script_ver)
1745 .ok_or(Error::ad_hoc("control block not found for claim script"))?;
1746
1747 let script_pubkey = vhtlc.script_pubkey();
1748
1749 let claimer_pk = swap.claim_public_key.inner.x_only_public_key().0;
1750 let vhtlc_input = VtxoInput::new(
1751 script_ver.0,
1752 None,
1753 control_block,
1754 vhtlc.tapscripts(),
1755 script_pubkey,
1756 claim_amount,
1757 vhtlc_outpoint.outpoint,
1758 vhtlc_outpoint.assets,
1759 );
1760
1761 let change_address = &claim_address;
1763
1764 let OffchainTransactions {
1765 mut ark_tx,
1766 checkpoint_txs,
1767 } = build_offchain_transactions(
1768 &outputs,
1769 change_address,
1770 std::slice::from_ref(&vhtlc_input),
1771 &server_info,
1772 )
1773 .map_err(Error::from)
1774 .context("failed to build offchain TXs")?;
1775
1776 let kp = self.keypair_by_pk(&claimer_pk)?;
1777 let sign_fn =
1778 |input: &mut psbt::Input,
1779 msg: secp256k1::Message|
1780 -> Result<Vec<(schnorr::Signature, XOnlyPublicKey)>, ark_core::Error> {
1781 {
1783 let mut bytes = vec![1];
1785
1786 let length = VarInt::from(preimage.len() as u64);
1787
1788 length
1789 .consensus_encode(&mut bytes)
1790 .expect("valid length encoding");
1791
1792 bytes.write_all(&preimage).expect("valid preimage encoding");
1793
1794 input.unknown.insert(
1795 psbt::raw::Key {
1796 type_value: 222,
1797 key: VTXO_CONDITION_KEY.to_vec(),
1798 },
1799 bytes,
1800 );
1801 }
1802
1803 let sig = Secp256k1::new().sign_schnorr_no_aux_rand(&msg, &kp);
1804 let pk = kp.x_only_public_key().0;
1805
1806 Ok(vec![(sig, pk)])
1807 };
1808
1809 sign_ark_transaction(sign_fn, &mut ark_tx, 0)
1810 .map_err(Error::from)
1811 .context("failed to sign Ark TX")?;
1812
1813 let ark_txid = ark_tx.unsigned_tx.compute_txid();
1814
1815 let res = self
1816 .network_client()
1817 .submit_offchain_transaction_request(ark_tx, checkpoint_txs)
1818 .await
1819 .map_err(Error::from)
1820 .context("failed to submit offchain TXs")?;
1821
1822 let mut checkpoint_psbt = res
1823 .signed_checkpoint_txs
1824 .first()
1825 .ok_or_else(|| Error::ad_hoc("no checkpoint PSBTs found"))?
1826 .clone();
1827
1828 sign_checkpoint_transaction(sign_fn, &mut checkpoint_psbt)
1829 .map_err(Error::from)
1830 .context("failed to sign checkpoint TX")?;
1831
1832 timeout_op(
1833 self.inner.timeout,
1834 self.network_client()
1835 .finalize_offchain_transaction(ark_txid, vec![checkpoint_psbt]),
1836 )
1837 .await
1838 .context("failed to finalize offchain transaction")?
1839 .map_err(Error::ark_server)
1840 .context("failed to finalize offchain transaction")?;
1841
1842 tracing::info!(txid = %ark_txid, "Spent VHTLC");
1843
1844 Ok(ClaimVhtlcResult {
1845 swap_id: swap_id.to_string(),
1846 claim_txid: ark_txid,
1847 claim_amount,
1848 preimage,
1849 })
1850 }
1851
1852 pub async fn create_chain_swap(
1864 &self,
1865 direction: ChainSwapDirection,
1866 amount: ChainSwapAmount,
1867 ) -> Result<ChainSwapResult, Error> {
1868 let preimage: [u8; 32] = rand::random();
1869 let preimage_hash = sha256::Hash::hash(&preimage);
1870
1871 let claim_keypair = self.next_keypair(crate::key_provider::KeypairIndex::New)?;
1872 let claim_public_key = claim_keypair.public_key();
1873 let claim_key_derivation_index =
1874 self.derivation_index_for_pk(&claim_keypair.x_only_public_key().0);
1875
1876 let refund_keypair = self.next_keypair(crate::key_provider::KeypairIndex::New)?;
1877 let refund_public_key = refund_keypair.public_key();
1878 let refund_key_derivation_index =
1879 self.derivation_index_for_pk(&refund_keypair.x_only_public_key().0);
1880
1881 let (from, to) = match &direction {
1882 ChainSwapDirection::ArkToBtc => (Asset::Ark, Asset::Btc),
1883 ChainSwapDirection::BtcToArk => (Asset::Btc, Asset::Ark),
1884 };
1885
1886 let (user_lock_amount, server_lock_amount) = match &amount {
1887 ChainSwapAmount::UserLock(a) => (Some(*a), None),
1888 ChainSwapAmount::ServerLock(a) => (None, Some(*a)),
1889 };
1890
1891 let request = CreateChainSwapRequest {
1892 from,
1893 to,
1894 user_lock_amount,
1895 server_lock_amount,
1896 claim_public_key: claim_public_key.into(),
1897 refund_public_key: refund_public_key.into(),
1898 preimage_hash,
1899 referral_id: self.inner.boltz_referral_id.clone(),
1900 };
1901
1902 let url = format!("{}/v2/swap/chain", self.inner.boltz_url);
1903
1904 let client = reqwest::Client::new();
1905 let response = client
1906 .post(&url)
1907 .json(&request)
1908 .send()
1909 .await
1910 .map_err(|e| Error::ad_hoc(e.to_string()))
1911 .context("failed to send chain swap request")?;
1912
1913 if !response.status().is_success() {
1914 let error_text = response
1915 .text()
1916 .await
1917 .map_err(|e| Error::ad_hoc(e.to_string()))
1918 .context("failed to read error text")?;
1919
1920 return Err(Error::ad_hoc(format!(
1921 "failed to create chain swap: {error_text}"
1922 )));
1923 }
1924
1925 let swap_response: CreateChainSwapResponse = response
1926 .json()
1927 .await
1928 .map_err(|e| Error::ad_hoc(e.to_string()))
1929 .context("failed to deserialize chain swap response")?;
1930
1931 let created_at = SystemTime::now()
1932 .duration_since(UNIX_EPOCH)
1933 .map_err(Error::ad_hoc)
1934 .context("failed to compute created_at")?;
1935
1936 let bip21 = swap_response
1941 .lockup_details
1942 .bip21
1943 .or(swap_response.claim_details.bip21.clone());
1944
1945 let swap_tree = swap_response
1946 .lockup_details
1947 .swap_tree
1948 .or(swap_response.claim_details.swap_tree.clone());
1949
1950 let data = ChainSwapData {
1951 id: swap_response.id.clone(),
1952 status: SwapStatus::Created,
1953 direction,
1954 preimage: Some(preimage),
1955 preimage_hash,
1956 claim_public_key: claim_public_key.into(),
1957 refund_public_key: refund_public_key.into(),
1958 server_claim_public_key: swap_response.lockup_details.server_public_key,
1959 server_refund_public_key: swap_response.claim_details.server_public_key,
1960 user_lockup_address: swap_response.lockup_details.lockup_address,
1961 server_lockup_address: swap_response.claim_details.lockup_address,
1962 user_lockup_amount: swap_response.lockup_details.amount,
1963 server_lockup_amount: swap_response.claim_details.amount,
1964 user_timeout_block_height: swap_response.lockup_details.timeout_block_height,
1965 server_timeout_block_height: swap_response.claim_details.timeout_block_height,
1966 user_timeout_block_heights: swap_response.lockup_details.timeouts,
1967 server_timeout_block_heights: swap_response.claim_details.timeouts,
1968 bip21,
1969 swap_tree,
1970 created_at: created_at.as_secs(),
1971 claim_key_derivation_index,
1972 refund_key_derivation_index,
1973 };
1974
1975 self.swap_storage()
1976 .insert_chain(swap_response.id.clone(), data.clone())
1977 .await?;
1978
1979 tracing::info!(
1980 swap_id = swap_response.id,
1981 direction = ?data.direction,
1982 user_lockup_address = %data.user_lockup_address,
1983 user_lockup_amount = %data.user_lockup_amount,
1984 server_lockup_amount = %data.server_lockup_amount,
1985 "Created chain swap"
1986 );
1987
1988 Ok(ChainSwapResult {
1989 swap_id: swap_response.id,
1990 user_lockup_address: data.user_lockup_address,
1991 user_lockup_amount: data.user_lockup_amount,
1992 server_lockup_amount: data.server_lockup_amount,
1993 bip21: data.bip21,
1994 })
1995 }
1996
1997 pub async fn wait_for_chain_swap_server_lockup(
2004 &self,
2005 swap_id: &str,
2006 ) -> Result<Option<String>, Error> {
2007 use futures::StreamExt;
2008
2009 let stream = self.subscribe_to_swap_updates(swap_id.to_string());
2010 tokio::pin!(stream);
2011
2012 while let Some(status_result) = stream.next().await {
2013 match status_result {
2014 Ok(status) => {
2015 tracing::debug!(swap_id, current = ?status, "Chain swap status");
2016 match status {
2017 SwapStatus::TransactionServerMempool
2018 | SwapStatus::TransactionServerConfirmed => {
2019 let url = format!("{}/v2/swap/{swap_id}", self.inner.boltz_url);
2021 let txid = async {
2022 reqwest::Client::new()
2023 .get(&url)
2024 .send()
2025 .await
2026 .ok()?
2027 .json::<GetSwapStatusResponse>()
2028 .await
2029 .ok()?
2030 .transaction
2031 .map(|t| t.id)
2032 }
2033 .await;
2034
2035 tracing::info!(
2036 swap_id,
2037 server_lockup_txid = txid.as_deref().unwrap_or("unknown"),
2038 "Server lockup detected"
2039 );
2040 return Ok(txid);
2041 }
2042 SwapStatus::SwapExpired => {
2043 return Err(Error::ad_hoc(format!("chain swap expired: {swap_id}")));
2044 }
2045 SwapStatus::TransactionRefunded | SwapStatus::TransactionFailed => {
2046 return Err(Error::ad_hoc(format!(
2047 "chain swap failed or refunded: {swap_id}"
2048 )));
2049 }
2050 SwapStatus::Error { error } => {
2051 tracing::error!(swap_id, "Got error from chain swap updates: {error}");
2052 }
2053 SwapStatus::Created
2055 | SwapStatus::TransactionMempool
2056 | SwapStatus::TransactionConfirmed
2057 | SwapStatus::TransactionClaimed
2058 | SwapStatus::TransactionLockupFailed
2059 | SwapStatus::InvoiceSet
2060 | SwapStatus::InvoicePending
2061 | SwapStatus::InvoicePaid
2062 | SwapStatus::InvoiceFailedToPay
2063 | SwapStatus::InvoiceExpired
2064 | SwapStatus::Other(_) => {}
2065 }
2066 }
2067 Err(e) => return Err(e),
2068 }
2069 }
2070
2071 Err(Error::ad_hoc("Chain swap status stream ended unexpectedly"))
2072 }
2073
2074 pub async fn claim_chain_swap(&self, swap_id: &str) -> Result<Txid, Error> {
2081 let swap = self
2082 .swap_storage()
2083 .get_chain(swap_id)
2084 .await
2085 .context("failed to get chain swap data")?
2086 .ok_or_else(|| Error::ad_hoc(format!("chain swap data not found: {swap_id}")))?;
2087
2088 let preimage = swap
2089 .preimage
2090 .ok_or_else(|| Error::ad_hoc(format!("preimage not found for chain swap {swap_id}")))?;
2091
2092 let preimage_hash = ripemd160::Hash::hash(swap.preimage_hash.as_byte_array());
2093
2094 let timeout_block_heights = swap.server_timeout_block_heights.ok_or_else(|| {
2095 Error::ad_hoc(format!(
2096 "chain swap {swap_id} has no ARK-side VHTLC timeouts on server lockup \
2097 (this swap's server lockup is on-chain BTC, not an Ark VHTLC)"
2098 ))
2099 })?;
2100 let server_info = self.server_info().await?;
2101
2102 let expected_address = ArkAddress::decode(&swap.server_lockup_address)
2103 .map_err(|e| Error::ad_hoc(format!("invalid server lockup address: {e}")))?;
2104
2105 let vhtlc = self.reconstruct_vhtlc_for_address(
2106 &server_info,
2107 |server| {
2108 Ok(VhtlcOptions {
2109 sender: swap.server_refund_public_key.into(),
2110 receiver: swap.claim_public_key.into(),
2111 server,
2112 preimage_hash,
2113 refund_locktime: timeout_block_heights.refund,
2114 unilateral_claim_delay: parse_sequence_number(
2115 timeout_block_heights.unilateral_claim as i64,
2116 )
2117 .map_err(|e| Error::ad_hoc(format!("invalid unilateral claim timeout: {e}")))?,
2118 unilateral_refund_delay: parse_sequence_number(
2119 timeout_block_heights.unilateral_refund as i64,
2120 )
2121 .map_err(|e| {
2122 Error::ad_hoc(format!("invalid unilateral refund timeout: {e}"))
2123 })?,
2124 unilateral_refund_without_receiver_delay: parse_sequence_number(
2125 timeout_block_heights.unilateral_refund_without_receiver as i64,
2126 )
2127 .map_err(|e| {
2128 Error::ad_hoc(format!("invalid refund without receiver timeout: {e}"))
2129 })?,
2130 })
2131 },
2132 &expected_address,
2133 )?;
2134 let vhtlc_address = vhtlc.address();
2135
2136 let vhtlc_outpoint = {
2137 let virtual_tx_outpoints = self
2138 .get_virtual_tx_outpoints(std::iter::once(vhtlc_address))
2139 .await?;
2140
2141 let vtxo_list = VtxoList::new(server_info.dust, virtual_tx_outpoints);
2142
2143 let mut unspent = vtxo_list.all_unspent();
2144 let vhtlc_outpoint = unspent.next().ok_or_else(|| {
2145 Error::ad_hoc(format!("no outpoint found for address {vhtlc_address}"))
2146 })?;
2147
2148 vhtlc_outpoint.clone()
2149 };
2150
2151 let (claim_address, _) = self
2152 .get_offchain_address()
2153 .await
2154 .context("failed to get offchain address")?;
2155 let claim_amount = swap.server_lockup_amount;
2156
2157 let outputs = vec![SendReceiver::bitcoin(claim_address, claim_amount)];
2158
2159 let spend_info = vhtlc.taproot_spend_info();
2160 let script_ver = (vhtlc.claim_script(), LeafVersion::TapScript);
2161 let control_block = spend_info
2162 .control_block(&script_ver)
2163 .ok_or(Error::ad_hoc("control block not found for claim script"))?;
2164
2165 let script_pubkey = vhtlc.script_pubkey();
2166
2167 let claimer_pk = swap.claim_public_key.inner.x_only_public_key().0;
2168 let vhtlc_input = VtxoInput::new(
2169 script_ver.0,
2170 None,
2171 control_block,
2172 vhtlc.tapscripts(),
2173 script_pubkey,
2174 claim_amount,
2175 vhtlc_outpoint.outpoint,
2176 vhtlc_outpoint.assets,
2177 );
2178
2179 let change_address = &claim_address;
2181
2182 let OffchainTransactions {
2183 mut ark_tx,
2184 checkpoint_txs,
2185 } = build_offchain_transactions(
2186 &outputs,
2187 change_address,
2188 std::slice::from_ref(&vhtlc_input),
2189 &server_info,
2190 )
2191 .map_err(Error::from)
2192 .context("failed to build offchain TXs")?;
2193
2194 let kp = self.keypair_by_pk(&claimer_pk)?;
2195 let sign_fn =
2196 |input: &mut psbt::Input,
2197 msg: secp256k1::Message|
2198 -> Result<Vec<(schnorr::Signature, XOnlyPublicKey)>, ark_core::Error> {
2199 {
2201 let mut bytes = vec![1];
2202
2203 let length = VarInt::from(preimage.len() as u64);
2204
2205 length
2206 .consensus_encode(&mut bytes)
2207 .expect("valid length encoding");
2208
2209 bytes.write_all(&preimage).expect("valid preimage encoding");
2210
2211 input.unknown.insert(
2212 psbt::raw::Key {
2213 type_value: 222,
2214 key: VTXO_CONDITION_KEY.to_vec(),
2215 },
2216 bytes,
2217 );
2218 }
2219
2220 let sig = Secp256k1::new().sign_schnorr_no_aux_rand(&msg, &kp);
2221 let pk = kp.x_only_public_key().0;
2222
2223 Ok(vec![(sig, pk)])
2224 };
2225
2226 sign_ark_transaction(sign_fn, &mut ark_tx, 0)
2227 .map_err(Error::from)
2228 .context("failed to sign Ark TX")?;
2229
2230 let ark_txid = ark_tx.unsigned_tx.compute_txid();
2231
2232 let res = self
2233 .network_client()
2234 .submit_offchain_transaction_request(ark_tx, checkpoint_txs)
2235 .await
2236 .map_err(Error::from)
2237 .context("failed to submit offchain TXs")?;
2238
2239 let mut checkpoint_psbt = res
2240 .signed_checkpoint_txs
2241 .first()
2242 .ok_or_else(|| Error::ad_hoc("no checkpoint PSBTs found"))?
2243 .clone();
2244
2245 sign_checkpoint_transaction(sign_fn, &mut checkpoint_psbt)
2246 .map_err(Error::from)
2247 .context("failed to sign checkpoint TX")?;
2248
2249 timeout_op(
2250 self.inner.timeout,
2251 self.network_client()
2252 .finalize_offchain_transaction(ark_txid, vec![checkpoint_psbt]),
2253 )
2254 .await
2255 .context("failed to finalize offchain transaction")?
2256 .map_err(Error::ark_server)
2257 .context("failed to finalize offchain transaction")?;
2258
2259 tracing::info!(swap_id, txid = %ark_txid, "Claimed chain swap VHTLC");
2260
2261 let mut updated_swap = swap.clone();
2262 updated_swap.status = SwapStatus::TransactionClaimed;
2263 self.swap_storage()
2264 .update_chain(swap_id, updated_swap)
2265 .await
2266 .context("failed to update chain swap data")?;
2267
2268 Ok(ark_txid)
2269 }
2270
2271 pub async fn claim_chain_swap_btc(
2278 &self,
2279 swap_id: &str,
2280 destination_address: bitcoin::Address,
2281 fee_rate_sat_vb: f64,
2282 ) -> Result<Txid, Error> {
2283 let swap = self
2284 .swap_storage()
2285 .get_chain(swap_id)
2286 .await
2287 .context("failed to get chain swap data")?
2288 .ok_or_else(|| Error::ad_hoc(format!("chain swap data not found: {swap_id}")))?;
2289
2290 let preimage = swap
2291 .preimage
2292 .ok_or_else(|| Error::ad_hoc(format!("preimage not found for chain swap {swap_id}")))?;
2293
2294 let swap_tree = swap.swap_tree.clone().ok_or_else(|| {
2295 Error::ad_hoc("no swap tree found (this swap has no on-chain BTC HTLC)")
2296 })?;
2297
2298 let btc_address_str = &swap.server_lockup_address;
2300
2301 let taproot_spend_info = reconstruct_btc_htlc(
2304 swap.server_refund_public_key,
2305 swap.claim_public_key,
2306 &swap_tree,
2307 )?;
2308
2309 let secp = Secp256k1::new();
2310
2311 let expected_spk = ScriptBuf::new_p2tr(
2313 &secp,
2314 taproot_spend_info.internal_key(),
2315 taproot_spend_info.merkle_root(),
2316 );
2317
2318 let parsed_address: bitcoin::Address<bitcoin::address::NetworkUnchecked> = btc_address_str
2319 .parse()
2320 .map_err(|e| Error::ad_hoc(format!("invalid BTC lockup address: {e}")))?;
2321 let parsed_address = parsed_address.assume_checked();
2322 let target_spk = parsed_address.script_pubkey();
2323
2324 if expected_spk != target_spk {
2325 return Err(Error::ad_hoc(format!(
2326 "taproot address mismatch for BTC lockup {btc_address_str}"
2327 )));
2328 }
2329
2330 let claim_script_bytes: Vec<u8> =
2331 bitcoin::hex::FromHex::from_hex(&swap_tree.claim_leaf.output)
2332 .map_err(|e| Error::ad_hoc(format!("invalid claim leaf hex: {e}")))?;
2333 let claim_script = ScriptBuf::from_bytes(claim_script_bytes);
2334 let claim_ver = (claim_script.clone(), LeafVersion::TapScript);
2335
2336 let utxos = self
2338 .inner
2339 .blockchain
2340 .find_outpoints(&parsed_address)
2341 .await
2342 .context("failed to find UTXOs at BTC lockup address")?;
2343
2344 let utxo = utxos.iter().find(|u| !u.is_spent).ok_or_else(|| {
2345 Error::ad_hoc(format!(
2346 "no unspent UTXO found at BTC lockup address {btc_address_str}"
2347 ))
2348 })?;
2349
2350 let control_block = taproot_spend_info
2352 .control_block(&claim_ver)
2353 .ok_or(Error::ad_hoc("control block not found for claim leaf"))?;
2354
2355 let cb_bytes = control_block.serialize();
2356 let witness_weight = 1 + 1 + 64 + 1 + 32 + 1 + claim_script.len() + 1 + cb_bytes.len() + 1;
2358 let weight = 4 * (11 + 41 + 43) + witness_weight;
2359 let vsize = weight.div_ceil(4);
2360 let fee = Amount::from_sat((vsize as f64 * fee_rate_sat_vb).ceil() as u64);
2361
2362 let claim_amount = utxo.amount.checked_sub(fee).ok_or_else(|| {
2363 Error::ad_hoc(format!(
2364 "UTXO amount {} is less than estimated fee {}",
2365 utxo.amount, fee
2366 ))
2367 })?;
2368
2369 let mut tx = bitcoin::Transaction {
2371 version: bitcoin::transaction::Version::TWO,
2372 lock_time: absolute::LockTime::ZERO,
2373 input: vec![bitcoin::TxIn {
2374 previous_output: utxo.outpoint,
2375 script_sig: ScriptBuf::new(),
2376 sequence: bitcoin::Sequence::ENABLE_RBF_NO_LOCKTIME,
2377 witness: bitcoin::Witness::new(),
2378 }],
2379 output: vec![TxOut {
2380 value: claim_amount,
2381 script_pubkey: destination_address.script_pubkey(),
2382 }],
2383 };
2384
2385 let leaf_hash =
2387 bitcoin::taproot::TapLeafHash::from_script(&claim_script, LeafVersion::TapScript);
2388
2389 let prevouts = [TxOut {
2390 value: utxo.amount,
2391 script_pubkey: target_spk.clone(),
2392 }];
2393
2394 let sighash = bitcoin::sighash::SighashCache::new(&tx)
2395 .taproot_script_spend_signature_hash(
2396 0,
2397 &bitcoin::sighash::Prevouts::All(&prevouts),
2398 leaf_hash,
2399 bitcoin::TapSighashType::Default,
2400 )
2401 .map_err(|e| Error::ad_hoc(format!("failed to compute sighash: {e}")))?;
2402
2403 let msg = secp256k1::Message::from_digest(sighash.to_byte_array());
2404 let claim_kp = self.keypair_by_pk(&swap.claim_public_key.inner.x_only_public_key().0)?;
2405 let signature = secp.sign_schnorr_no_aux_rand(&msg, &claim_kp);
2406
2407 let mut witness = bitcoin::Witness::new();
2409 witness.push(signature.serialize());
2410 witness.push(preimage);
2411 witness.push(claim_script.as_bytes());
2412 witness.push(cb_bytes);
2413
2414 tx.input[0].witness = witness;
2415
2416 self.inner
2418 .blockchain
2419 .broadcast(&tx)
2420 .await
2421 .context("failed to broadcast BTC claim transaction")?;
2422
2423 let txid = tx.compute_txid();
2424
2425 tracing::info!(swap_id, %txid, %claim_amount, "Claimed on-chain BTC from chain swap");
2426
2427 let mut updated_swap = swap.clone();
2428 updated_swap.status = SwapStatus::TransactionClaimed;
2429 self.swap_storage()
2430 .update_chain(swap_id, updated_swap)
2431 .await
2432 .context("failed to update chain swap data")?;
2433
2434 Ok(txid)
2435 }
2436
2437 pub async fn refund_chain_swap(&self, swap_id: &str) -> Result<Txid, Error> {
2444 let swap = self
2445 .swap_storage()
2446 .get_chain(swap_id)
2447 .await
2448 .context("failed to get chain swap data")?
2449 .ok_or_else(|| Error::ad_hoc(format!("chain swap data not found: {swap_id}")))?;
2450
2451 let timeout_block_heights = swap.user_timeout_block_heights.ok_or_else(|| {
2452 Error::ad_hoc(
2453 "chain swap has no ARK-side VHTLC timeouts on user lockup \
2454 (user lockup is on-chain BTC, use refund_chain_swap_btc instead)",
2455 )
2456 })?;
2457
2458 let preimage_hash = ripemd160::Hash::hash(swap.preimage_hash.as_byte_array());
2459 let server_info = self.server_info().await?;
2460
2461 let expected_address = ArkAddress::decode(&swap.user_lockup_address)
2463 .map_err(|e| Error::ad_hoc(format!("invalid user lockup address: {e}")))?;
2464
2465 let vhtlc = self.reconstruct_vhtlc_for_address(
2466 &server_info,
2467 |server| {
2468 Ok(VhtlcOptions {
2469 sender: swap.refund_public_key.into(),
2470 receiver: swap.server_claim_public_key.into(),
2471 server,
2472 preimage_hash,
2473 refund_locktime: timeout_block_heights.refund,
2474 unilateral_claim_delay: parse_sequence_number(
2475 timeout_block_heights.unilateral_claim as i64,
2476 )
2477 .map_err(|e| Error::ad_hoc(format!("invalid unilateral claim timeout: {e}")))?,
2478 unilateral_refund_delay: parse_sequence_number(
2479 timeout_block_heights.unilateral_refund as i64,
2480 )
2481 .map_err(|e| {
2482 Error::ad_hoc(format!("invalid unilateral refund timeout: {e}"))
2483 })?,
2484 unilateral_refund_without_receiver_delay: parse_sequence_number(
2485 timeout_block_heights.unilateral_refund_without_receiver as i64,
2486 )
2487 .map_err(|e| {
2488 Error::ad_hoc(format!("invalid refund without receiver timeout: {e}"))
2489 })?,
2490 })
2491 },
2492 &expected_address,
2493 )?;
2494 let vhtlc_address = vhtlc.address();
2495
2496 let vhtlc_outpoint = {
2497 let virtual_tx_outpoints = self
2498 .get_virtual_tx_outpoints(std::iter::once(vhtlc_address))
2499 .await?;
2500
2501 let vtxo_list = VtxoList::new(server_info.dust, virtual_tx_outpoints);
2502
2503 let mut unspent = vtxo_list.all_unspent();
2504 unspent
2505 .next()
2506 .ok_or_else(|| {
2507 Error::ad_hoc(format!("no outpoint found for address {vhtlc_address}"))
2508 })?
2509 .clone()
2510 };
2511
2512 let (refund_address, _) = self.get_offchain_address().await?;
2513 let refund_amount = swap.user_lockup_amount;
2514
2515 let outputs = vec![SendReceiver::bitcoin(refund_address, refund_amount)];
2516
2517 let refund_script = vhtlc.refund_without_receiver_script();
2518 let spend_info = vhtlc.taproot_spend_info();
2519 let script_ver = (refund_script, LeafVersion::TapScript);
2520 let control_block = spend_info
2521 .control_block(&script_ver)
2522 .ok_or(Error::ad_hoc("control block not found for refund script"))?;
2523
2524 let script_pubkey = vhtlc.script_pubkey();
2525 let refunder_pk = swap.refund_public_key.inner.x_only_public_key().0;
2526
2527 let change_address = &refund_address;
2529
2530 let vhtlc_input = VtxoInput::new(
2531 script_ver.0,
2532 Some(absolute::LockTime::from_consensus(
2533 timeout_block_heights.refund,
2534 )),
2535 control_block,
2536 vhtlc.tapscripts(),
2537 script_pubkey,
2538 refund_amount,
2539 vhtlc_outpoint.outpoint,
2540 vhtlc_outpoint.assets,
2541 );
2542
2543 let OffchainTransactions {
2544 mut ark_tx,
2545 checkpoint_txs,
2546 } = build_offchain_transactions(
2547 &outputs,
2548 change_address,
2549 std::slice::from_ref(&vhtlc_input),
2550 &server_info,
2551 )?;
2552
2553 let kp = self.keypair_by_pk(&refunder_pk)?;
2554 let sign_fn =
2555 |_: &mut psbt::Input,
2556 msg: secp256k1::Message|
2557 -> Result<Vec<(schnorr::Signature, XOnlyPublicKey)>, ark_core::Error> {
2558 let sig = Secp256k1::new().sign_schnorr_no_aux_rand(&msg, &kp);
2559 let pk = kp.x_only_public_key().0;
2560 Ok(vec![(sig, pk)])
2561 };
2562
2563 sign_ark_transaction(sign_fn, &mut ark_tx, 0)?;
2564
2565 let ark_txid = ark_tx.unsigned_tx.compute_txid();
2566
2567 let res = self
2568 .network_client()
2569 .submit_offchain_transaction_request(ark_tx, checkpoint_txs)
2570 .await?;
2571
2572 let mut checkpoint_psbt = res
2573 .signed_checkpoint_txs
2574 .first()
2575 .ok_or_else(|| Error::ad_hoc("no checkpoint PSBTs found"))?
2576 .clone();
2577
2578 let kp = self.keypair_by_pk(&refunder_pk)?;
2579 let sign_fn =
2580 |_: &mut psbt::Input,
2581 msg: secp256k1::Message|
2582 -> Result<Vec<(schnorr::Signature, XOnlyPublicKey)>, ark_core::Error> {
2583 let sig = Secp256k1::new().sign_schnorr_no_aux_rand(&msg, &kp);
2584 let pk = kp.x_only_public_key().0;
2585 Ok(vec![(sig, pk)])
2586 };
2587
2588 sign_checkpoint_transaction(sign_fn, &mut checkpoint_psbt)?;
2589
2590 timeout_op(
2591 self.inner.timeout,
2592 self.network_client()
2593 .finalize_offchain_transaction(ark_txid, vec![checkpoint_psbt]),
2594 )
2595 .await?
2596 .map_err(Error::ark_server)
2597 .context("failed to finalize offchain transaction")?;
2598
2599 tracing::info!(swap_id, txid = %ark_txid, "Refunded chain swap Ark VHTLC");
2600
2601 let mut updated_swap = swap.clone();
2602 updated_swap.status = SwapStatus::TransactionRefunded;
2603 self.swap_storage()
2604 .update_chain(swap_id, updated_swap)
2605 .await
2606 .context("failed to update chain swap data")?;
2607
2608 Ok(ark_txid)
2609 }
2610
2611 pub async fn refund_chain_swap_btc(
2616 &self,
2617 swap_id: &str,
2618 destination_address: bitcoin::Address,
2619 fee_rate_sat_vb: f64,
2620 ) -> Result<Txid, Error> {
2621 let swap = self
2622 .swap_storage()
2623 .get_chain(swap_id)
2624 .await
2625 .context("failed to get chain swap data")?
2626 .ok_or_else(|| Error::ad_hoc(format!("chain swap data not found: {swap_id}")))?;
2627
2628 let swap_tree = swap.swap_tree.clone().ok_or_else(|| {
2629 Error::ad_hoc("no swap tree found (this swap has no on-chain BTC lockup)")
2630 })?;
2631
2632 let btc_address_str = &swap.user_lockup_address;
2634
2635 let taproot_spend_info = reconstruct_btc_htlc(
2638 swap.server_claim_public_key,
2639 swap.refund_public_key,
2640 &swap_tree,
2641 )?;
2642
2643 let secp = Secp256k1::new();
2644
2645 let refund_script_bytes: Vec<u8> =
2646 bitcoin::hex::FromHex::from_hex(&swap_tree.refund_leaf.output)
2647 .map_err(|e| Error::ad_hoc(format!("invalid refund leaf hex: {e}")))?;
2648 let refund_script = ScriptBuf::from_bytes(refund_script_bytes);
2649 let refund_ver = (refund_script.clone(), LeafVersion::TapScript);
2650
2651 let expected_spk = ScriptBuf::new_p2tr(
2653 &secp,
2654 taproot_spend_info.internal_key(),
2655 taproot_spend_info.merkle_root(),
2656 );
2657
2658 let parsed_address: bitcoin::Address<bitcoin::address::NetworkUnchecked> = btc_address_str
2659 .parse()
2660 .map_err(|e| Error::ad_hoc(format!("invalid BTC lockup address: {e}")))?;
2661 let parsed_address = parsed_address.assume_checked();
2662 let target_spk = parsed_address.script_pubkey();
2663
2664 if expected_spk != target_spk {
2665 return Err(Error::ad_hoc(format!(
2666 "taproot address mismatch for BTC lockup {btc_address_str}"
2667 )));
2668 }
2669
2670 let utxos = self
2672 .inner
2673 .blockchain
2674 .find_outpoints(&parsed_address)
2675 .await
2676 .context("failed to find UTXOs at BTC lockup address")?;
2677
2678 let utxo = utxos.iter().find(|u| !u.is_spent).ok_or_else(|| {
2679 Error::ad_hoc(format!(
2680 "no unspent UTXO found at BTC lockup address {btc_address_str}"
2681 ))
2682 })?;
2683
2684 let control_block = taproot_spend_info
2685 .control_block(&refund_ver)
2686 .ok_or(Error::ad_hoc("control block not found for refund leaf"))?;
2687
2688 let cb_bytes = control_block.serialize();
2689 let witness_weight = 1 + 1 + 64 + 1 + refund_script.len() + 1 + cb_bytes.len() + 1;
2690 let weight = 4 * (11 + 41 + 43) + witness_weight;
2691 let vsize = weight.div_ceil(4);
2692 let fee = Amount::from_sat((vsize as f64 * fee_rate_sat_vb).ceil() as u64);
2693
2694 let refund_amount = utxo.amount.checked_sub(fee).ok_or_else(|| {
2695 Error::ad_hoc(format!(
2696 "UTXO amount {} is less than estimated fee {}",
2697 utxo.amount, fee
2698 ))
2699 })?;
2700
2701 let lock_time = absolute::LockTime::from_consensus(swap.user_timeout_block_height);
2703
2704 let mut tx = bitcoin::Transaction {
2705 version: bitcoin::transaction::Version::TWO,
2706 lock_time,
2707 input: vec![bitcoin::TxIn {
2708 previous_output: utxo.outpoint,
2709 script_sig: ScriptBuf::new(),
2710 sequence: bitcoin::Sequence::ENABLE_LOCKTIME_NO_RBF,
2711 witness: bitcoin::Witness::new(),
2712 }],
2713 output: vec![TxOut {
2714 value: refund_amount,
2715 script_pubkey: destination_address.script_pubkey(),
2716 }],
2717 };
2718
2719 let leaf_hash =
2721 bitcoin::taproot::TapLeafHash::from_script(&refund_script, LeafVersion::TapScript);
2722
2723 let prevouts = [TxOut {
2724 value: utxo.amount,
2725 script_pubkey: target_spk,
2726 }];
2727
2728 let sighash = bitcoin::sighash::SighashCache::new(&tx)
2729 .taproot_script_spend_signature_hash(
2730 0,
2731 &bitcoin::sighash::Prevouts::All(&prevouts),
2732 leaf_hash,
2733 bitcoin::TapSighashType::Default,
2734 )
2735 .map_err(|e| Error::ad_hoc(format!("failed to compute sighash: {e}")))?;
2736
2737 let msg = secp256k1::Message::from_digest(sighash.to_byte_array());
2738 let refund_kp = self.keypair_by_pk(&swap.refund_public_key.inner.x_only_public_key().0)?;
2739 let signature = secp.sign_schnorr_no_aux_rand(&msg, &refund_kp);
2740
2741 let mut witness = bitcoin::Witness::new();
2743 witness.push(signature.serialize());
2744 witness.push(refund_script.as_bytes());
2745 witness.push(cb_bytes);
2746
2747 tx.input[0].witness = witness;
2748
2749 self.inner
2750 .blockchain
2751 .broadcast(&tx)
2752 .await
2753 .context("failed to broadcast BTC refund transaction")?;
2754
2755 let txid = tx.compute_txid();
2756
2757 tracing::info!(swap_id, %txid, %refund_amount, "Refunded on-chain BTC from chain swap");
2758
2759 let mut updated_swap = swap.clone();
2760 updated_swap.status = SwapStatus::TransactionRefunded;
2761 self.swap_storage()
2762 .update_chain(swap_id, updated_swap)
2763 .await
2764 .context("failed to update chain swap data")?;
2765
2766 Ok(txid)
2767 }
2768
2769 pub async fn get_swap_status(&self, swap_id: &str) -> Result<SwapStatusInfo, Error> {
2774 let swap_type = if self.swap_storage().get_submarine(swap_id).await?.is_some() {
2776 SwapType::Submarine
2777 } else if self.swap_storage().get_reverse(swap_id).await?.is_some() {
2778 SwapType::Reverse
2779 } else if self.swap_storage().get_chain(swap_id).await?.is_some() {
2780 SwapType::Chain
2781 } else {
2782 SwapType::Unknown
2783 };
2784
2785 let url = format!("{}/v2/swap/{swap_id}", self.inner.boltz_url);
2787 let client = reqwest::Client::new();
2788 let response = client
2789 .get(&url)
2790 .send()
2791 .await
2792 .map_err(|e| Error::ad_hoc(e.to_string()))
2793 .context("failed to query swap status")?;
2794
2795 if !response.status().is_success() {
2796 let error_text = response
2797 .text()
2798 .await
2799 .map_err(|e| Error::ad_hoc(e.to_string()))?;
2800 return Err(Error::ad_hoc(format!(
2801 "failed to get swap status: {error_text}"
2802 )));
2803 }
2804
2805 let status_response: GetSwapStatusResponse = response
2806 .json()
2807 .await
2808 .map_err(|e| Error::ad_hoc(e.to_string()))
2809 .context("failed to deserialize swap status response")?;
2810
2811 Ok(SwapStatusInfo {
2812 swap_id: swap_id.to_string(),
2813 swap_type,
2814 status: status_response.status,
2815 })
2816 }
2817
2818 pub async fn get_fees(&self) -> Result<BoltzFees, Error> {
2824 let client = reqwest::Client::builder()
2825 .timeout(self.inner.timeout)
2826 .build()
2827 .map_err(|e| Error::ad_hoc(e.to_string()))?;
2828
2829 let submarine_url = format!("{}/v2/swap/submarine", &self.inner.boltz_url);
2831 let submarine_response = client
2832 .get(&submarine_url)
2833 .send()
2834 .await
2835 .map_err(|e| Error::ad_hoc(e.to_string()))
2836 .context("failed to fetch submarine swap fees")?;
2837
2838 if !submarine_response.status().is_success() {
2839 let error_text = submarine_response
2840 .text()
2841 .await
2842 .map_err(|e| Error::ad_hoc(e.to_string()))?;
2843 return Err(Error::ad_hoc(format!(
2844 "failed to fetch submarine swap fees: {error_text}"
2845 )));
2846 }
2847
2848 let submarine_pairs: SubmarinePairsResponse = submarine_response
2849 .json()
2850 .await
2851 .map_err(|e| Error::ad_hoc(e.to_string()))
2852 .context("failed to deserialize submarine swap fees response")?;
2853
2854 let submarine_pair_fees = &submarine_pairs.ark.btc.fees;
2855 let submarine_fees = SubmarineSwapFees {
2856 percentage: submarine_pair_fees.percentage,
2857 miner_fees: submarine_pair_fees.miner_fees,
2858 };
2859
2860 let reverse_url = format!("{}/v2/swap/reverse", self.inner.boltz_url);
2862 let reverse_response = client
2863 .get(&reverse_url)
2864 .send()
2865 .await
2866 .map_err(|e| Error::ad_hoc(e.to_string()))
2867 .context("failed to fetch reverse swap fees")?;
2868
2869 if !reverse_response.status().is_success() {
2870 let error_text = reverse_response
2871 .text()
2872 .await
2873 .map_err(|e| Error::ad_hoc(e.to_string()))?;
2874 return Err(Error::ad_hoc(format!(
2875 "failed to fetch reverse swap fees: {error_text}"
2876 )));
2877 }
2878
2879 let reverse_pairs: ReversePairsResponse = reverse_response
2880 .json()
2881 .await
2882 .map_err(|e| Error::ad_hoc(e.to_string()))
2883 .context("failed to deserialize reverse swap fees response")?;
2884
2885 let reverse_pair_fees = &reverse_pairs.btc.ark.fees;
2886 let reverse_fees = ReverseSwapFees {
2887 percentage: reverse_pair_fees.percentage,
2888 miner_fees: ReverseMinerFees {
2889 lockup: reverse_pair_fees.miner_fees.lockup,
2890 claim: reverse_pair_fees.miner_fees.claim,
2891 },
2892 };
2893
2894 Ok(BoltzFees {
2895 submarine: submarine_fees,
2896 reverse: reverse_fees,
2897 })
2898 }
2899
2900 pub async fn get_limits(&self) -> Result<SwapLimits, Error> {
2906 let client = reqwest::Client::builder()
2907 .timeout(self.inner.timeout)
2908 .build()
2909 .map_err(|e| Error::ad_hoc(e.to_string()))?;
2910
2911 let url = format!("{}/v2/swap/submarine", self.inner.boltz_url);
2912 let response = client
2913 .get(&url)
2914 .send()
2915 .await
2916 .map_err(|e| Error::ad_hoc(e.to_string()))
2917 .context("failed to fetch swap limits")?;
2918
2919 if !response.status().is_success() {
2920 let error_text = response
2921 .text()
2922 .await
2923 .map_err(|e| Error::ad_hoc(e.to_string()))?;
2924 return Err(Error::ad_hoc(format!(
2925 "failed to fetch swap limits: {error_text}"
2926 )));
2927 }
2928
2929 let pairs: SubmarinePairsResponse = response
2930 .json()
2931 .await
2932 .map_err(|e| Error::ad_hoc(e.to_string()))
2933 .context("failed to deserialize swap limits response")?;
2934
2935 Ok(SwapLimits {
2936 min: pairs.ark.btc.limits.minimal,
2937 max: pairs.ark.btc.limits.maximal,
2938 })
2939 }
2940
2941 pub fn subscribe_to_swap_updates(
2944 &self,
2945 swap_id: String,
2946 ) -> impl futures::Stream<Item = Result<SwapStatus, Error>> + '_ {
2947 async_stream::stream! {
2948 let mut last_status: Option<SwapStatus> = None;
2949 let url = format!("{}/v2/swap/{swap_id}", self.inner.boltz_url);
2950
2951 loop {
2952 let client = reqwest::Client::new();
2953 let response = client
2954 .get(&url)
2955 .send()
2956 .await;
2957
2958 match response {
2959 Ok(resp) if resp.status().is_success() => {
2960 let status_response = resp
2961 .json::<GetSwapStatusResponse>()
2962 .await
2963 .map_err(|e| Error::ad_hoc(e.to_string()));
2964
2965 match status_response {
2966 Ok(current_status) => {
2967 let current_status = current_status.status;
2968
2969 if last_status.as_ref() != Some(¤t_status) {
2971 last_status = Some(current_status.clone());
2972 yield Ok(current_status);
2973 }
2974 }
2975 Err(e) => {
2976 yield Err(Error::ad_hoc(format!(
2977 "failed to deserialize swap status response: {e}"
2978 )));
2979 break;
2980 }
2981 }
2982 }
2983 Ok(resp) => {
2984 let error_text = resp
2985 .text()
2986 .await
2987 .unwrap_or_else(|_| "Unknown error".to_string());
2988
2989 yield Err(Error::ad_hoc(format!(
2990 "failed to check swap status: {error_text}"
2991 )));
2992 break;
2993 }
2994 Err(e) => {
2995 yield Err(Error::ad_hoc(e.to_string())
2996 .context("failed to send swap status request"));
2997 break;
2998 }
2999 }
3000
3001 tokio::time::sleep(std::time::Duration::from_secs(1)).await;
3003 }
3004 }
3005 }
3006
3007 pub async fn list_pending_vhtlc_spend_txs(&self) -> Result<Vec<PendingVhtlcSpendTx>, Error> {
3014 let vhtlc_infos = self.collect_active_vhtlc_infos().await?;
3015
3016 if vhtlc_infos.is_empty() {
3017 return Ok(vec![]);
3018 }
3019
3020 let addresses = vhtlc_infos.iter().map(|info| info.address);
3021 let request = ark_core::server::GetVtxosRequest::new_for_addresses(addresses)
3022 .pending_only()
3023 .map_err(Error::from)?;
3024
3025 let vtxos = self
3026 .fetch_all_vtxos(request)
3027 .await
3028 .context("failed to fetch pending VHTLC VTXOs")?;
3029
3030 tracing::debug!(
3031 num_pending_vtxos = vtxos.len(),
3032 "Fetched pending VHTLC VTXOs"
3033 );
3034
3035 if vtxos.is_empty() {
3036 return Ok(vec![]);
3037 }
3038
3039 let info_by_script: std::collections::HashMap<_, _> = vhtlc_infos
3041 .iter()
3042 .map(|info| (info.script_pubkey.clone(), info))
3043 .collect();
3044
3045 let secp = Secp256k1::new();
3046 let mut results = Vec::new();
3047 let mut seen_ark_txids = std::collections::HashSet::new();
3048
3049 for vtxo in &vtxos {
3050 let info = match info_by_script.get(&vtxo.script) {
3051 Some(info) => info,
3052 None => {
3053 tracing::warn!(
3054 outpoint = %vtxo.outpoint,
3055 "Skipping pending VHTLC VTXO with unknown script"
3056 );
3057 continue;
3058 }
3059 };
3060
3061 let intent_input = match info.preimage {
3066 Some(preimage) => intent::Input::new_with_extra_witness(
3067 vtxo.outpoint,
3068 bitcoin::Sequence::ZERO,
3069 None,
3070 TxOut {
3071 value: vtxo.amount,
3072 script_pubkey: info.script_pubkey.clone(),
3073 },
3074 vhtlc_tapscripts(&info.vhtlc),
3075 info.intent_spend_info.clone(),
3076 false,
3077 vtxo.is_swept,
3078 vtxo.assets.clone(),
3079 vec![preimage.to_vec()],
3080 ),
3081 None => intent::Input::new(
3082 vtxo.outpoint,
3083 bitcoin::Sequence::ZERO,
3084 None,
3085 TxOut {
3086 value: vtxo.amount,
3087 script_pubkey: info.script_pubkey.clone(),
3088 },
3089 vhtlc_tapscripts(&info.vhtlc),
3090 info.intent_spend_info.clone(),
3091 false,
3092 vtxo.is_swept,
3093 vtxo.assets.clone(),
3094 ),
3095 };
3096
3097 let sign_for_vtxo_fn = |input: &mut psbt::Input,
3098 msg: secp256k1::Message|
3099 -> Result<
3100 Vec<(schnorr::Signature, XOnlyPublicKey)>,
3101 ark_core::Error,
3102 > {
3103 match &input.witness_script {
3104 None => Err(ark_core::Error::ad_hoc(
3105 "Missing witness script when signing get-pending-tx intent for VHTLC",
3106 )),
3107 Some(script) => {
3108 let pks = extract_checksig_pubkeys(script);
3109 let mut res = vec![];
3110 for pk in &pks {
3111 if let Ok(keypair) = self.keypair_by_pk(pk) {
3112 let sig = secp.sign_schnorr_no_aux_rand(&msg, &keypair);
3113 res.push((sig, keypair.x_only_public_key().0));
3114 }
3115 }
3116 Ok(res)
3117 }
3118 }
3119 };
3120
3121 let sign_for_onchain_fn =
3122 |_: &mut psbt::Input,
3123 _: secp256k1::Message|
3124 -> Result<(schnorr::Signature, XOnlyPublicKey), ark_core::Error> {
3125 Err(ark_core::Error::ad_hoc(
3126 "unexpected onchain input in get-pending-tx intent",
3127 ))
3128 };
3129
3130 let message = intent::IntentMessage::GetPendingTx { expire_at: 0 };
3131 let get_pending_intent = intent::make_intent(
3132 sign_for_vtxo_fn,
3133 sign_for_onchain_fn,
3134 vec![intent_input],
3135 vec![],
3136 message,
3137 )?;
3138
3139 let pending_txs = self
3140 .network_client()
3141 .get_pending_tx(get_pending_intent)
3142 .await
3143 .map_err(Error::ark_server)
3144 .context("failed to get pending VHTLC transactions")?;
3145
3146 for pending_tx in pending_txs {
3147 if !seen_ark_txids.insert(pending_tx.ark_txid) {
3148 continue;
3149 }
3150
3151 let spend_type = Self::identify_vhtlc_spend_type(info, &pending_tx)?;
3152
3153 tracing::info!(
3154 ark_txid = %pending_tx.ark_txid,
3155 swap_id = spend_type.swap_id(),
3156 spend_type = spend_type.name(),
3157 "Found pending VHTLC spend transaction"
3158 );
3159
3160 results.push(PendingVhtlcSpendTx {
3161 spend_type,
3162 pending_tx,
3163 });
3164 }
3165 }
3166
3167 Ok(results)
3168 }
3169
3170 pub async fn continue_pending_vhtlc_spend_tx(
3177 &self,
3178 pending: &PendingVhtlcSpendTx,
3179 ) -> Result<Txid, Error> {
3180 let ark_txid = pending.pending_tx.ark_txid;
3181
3182 match &pending.spend_type {
3183 PendingVhtlcSpendType::Claim { preimage, .. } => {
3184 self.continue_pending_claim(ark_txid, &pending.pending_tx, *preimage)
3185 .await
3186 }
3187 PendingVhtlcSpendType::CollaborativeRefund { swap_id } => {
3188 self.continue_pending_collaborative_refund(ark_txid, &pending.pending_tx, swap_id)
3189 .await
3190 }
3191 PendingVhtlcSpendType::ExpiredRefund { .. } => {
3192 self.continue_pending_expired_refund(ark_txid, &pending.pending_tx)
3193 .await
3194 }
3195 }
3196 }
3197
3198 pub async fn continue_pending_vhtlc_spend_txs(&self) -> Result<Vec<Txid>, Error> {
3200 let pending = self.list_pending_vhtlc_spend_txs().await?;
3201
3202 let mut finalized = Vec::new();
3203 for tx in &pending {
3204 match self.continue_pending_vhtlc_spend_tx(tx).await {
3205 Ok(txid) => finalized.push(txid),
3206 Err(e) => {
3207 tracing::warn!(
3208 ark_txid = %tx.pending_tx.ark_txid,
3209 swap_id = tx.spend_type.swap_id(),
3210 ?e,
3211 "Failed to finalize pending VHTLC spend tx"
3212 );
3213 }
3214 }
3215 }
3216
3217 Ok(finalized)
3218 }
3219
3220 async fn continue_pending_claim(
3222 &self,
3223 ark_txid: Txid,
3224 pending_tx: &PendingTx,
3225 preimage: [u8; 32],
3226 ) -> Result<Txid, Error> {
3227 let mut signed_checkpoint_txs = pending_tx.signed_checkpoint_txs.clone();
3228
3229 for checkpoint_psbt in signed_checkpoint_txs.iter_mut() {
3230 Self::restore_witness_script_if_needed(checkpoint_psbt, &pending_tx.signed_ark_tx)?;
3231
3232 Self::inject_preimage_into_psbt(checkpoint_psbt, preimage);
3234
3235 self.sign_checkpoint_with_own_keys(checkpoint_psbt)?;
3236 }
3237
3238 timeout_op(
3239 self.inner.timeout,
3240 self.network_client()
3241 .finalize_offchain_transaction(ark_txid, signed_checkpoint_txs),
3242 )
3243 .await?
3244 .map_err(Error::ark_server)
3245 .context("failed to finalize pending claim transaction")?;
3246
3247 tracing::info!(txid = %ark_txid, "Finalized pending VHTLC claim");
3248 Ok(ark_txid)
3249 }
3250
3251 async fn continue_pending_collaborative_refund(
3253 &self,
3254 ark_txid: Txid,
3255 pending_tx: &PendingTx,
3256 swap_id: &str,
3257 ) -> Result<Txid, Error> {
3258 let url = format!(
3264 "{}/v2/swap/submarine/{swap_id}/refund/ark",
3265 self.inner.boltz_url
3266 );
3267 let client = reqwest::Client::new();
3268
3269 let mut signed_checkpoint_txs = Vec::new();
3270
3271 for checkpoint_psbt in &pending_tx.signed_checkpoint_txs {
3272 let response = client
3273 .post(&url)
3274 .json(&RefundSwapRequest {
3275 transaction: pending_tx.signed_ark_tx.to_string(),
3276 checkpoint: checkpoint_psbt.to_string(),
3277 })
3278 .send()
3279 .await
3280 .map_err(Error::ad_hoc)
3281 .context("failed to re-request Boltz refund signature")?;
3282
3283 if !response.status().is_success() {
3284 let error_text = response
3285 .text()
3286 .await
3287 .map_err(|e| Error::ad_hoc(e.to_string()))
3288 .context("failed to read Boltz error text")?;
3289
3290 return Err(Error::ad_hoc(format!(
3291 "Boltz refund re-sign request failed: {error_text}"
3292 )));
3293 }
3294
3295 let refund_response: RefundSwapResponse = response
3296 .json()
3297 .await
3298 .map_err(Error::ad_hoc)
3299 .context("failed to deserialize Boltz refund response")?;
3300
3301 if let Some(err) = refund_response.error.as_deref() {
3302 return Err(Error::ad_hoc(format!("Boltz refund re-sign failed: {err}")));
3303 }
3304
3305 let boltz_signed_checkpoint = Psbt::from_str(&refund_response.checkpoint)
3306 .map_err(Error::ad_hoc)
3307 .context("could not parse Boltz-signed checkpoint PSBT")?;
3308
3309 let boltz_tap_script_sigs = boltz_signed_checkpoint
3311 .inputs
3312 .first()
3313 .ok_or_else(|| Error::ad_hoc("Boltz checkpoint has no inputs"))?
3314 .tap_script_sigs
3315 .clone();
3316
3317 let mut final_checkpoint = checkpoint_psbt.clone();
3319 Self::restore_witness_script_if_needed(
3320 &mut final_checkpoint,
3321 &pending_tx.signed_ark_tx,
3322 )?;
3323
3324 final_checkpoint
3326 .inputs
3327 .first_mut()
3328 .ok_or_else(|| Error::ad_hoc("checkpoint has no inputs"))?
3329 .tap_script_sigs
3330 .extend(boltz_tap_script_sigs);
3331
3332 self.sign_checkpoint_with_own_keys(&mut final_checkpoint)?;
3334
3335 signed_checkpoint_txs.push(final_checkpoint);
3336 }
3337
3338 timeout_op(
3339 self.inner.timeout,
3340 self.network_client()
3341 .finalize_offchain_transaction(ark_txid, signed_checkpoint_txs),
3342 )
3343 .await?
3344 .map_err(Error::ark_server)
3345 .context("failed to finalize pending collaborative refund")?;
3346
3347 tracing::info!(txid = %ark_txid, swap_id, "Finalized pending collaborative refund");
3348 Ok(ark_txid)
3349 }
3350
3351 async fn continue_pending_expired_refund(
3353 &self,
3354 ark_txid: Txid,
3355 pending_tx: &PendingTx,
3356 ) -> Result<Txid, Error> {
3357 let mut signed_checkpoint_txs = pending_tx.signed_checkpoint_txs.clone();
3358
3359 for checkpoint_psbt in signed_checkpoint_txs.iter_mut() {
3360 Self::restore_witness_script_if_needed(checkpoint_psbt, &pending_tx.signed_ark_tx)?;
3361 self.sign_checkpoint_with_own_keys(checkpoint_psbt)?;
3362 }
3363
3364 timeout_op(
3365 self.inner.timeout,
3366 self.network_client()
3367 .finalize_offchain_transaction(ark_txid, signed_checkpoint_txs),
3368 )
3369 .await?
3370 .map_err(Error::ark_server)
3371 .context("failed to finalize pending expired refund")?;
3372
3373 tracing::info!(txid = %ark_txid, "Finalized pending expired VHTLC refund");
3374 Ok(ark_txid)
3375 }
3376
3377 fn reconstruct_vhtlc_for_address(
3385 &self,
3386 server_info: &Info,
3387 mk_opts: impl Fn(XOnlyPublicKey) -> Result<VhtlcOptions, Error>,
3388 expected_address: &ArkAddress,
3389 ) -> Result<VhtlcScript, Error> {
3390 reconstruct_vhtlc_from_keys(
3391 server_info.all_server_keys(),
3392 server_info.network,
3393 mk_opts,
3394 expected_address,
3395 )
3396 }
3397
3398 fn build_vhtlc_script(
3400 &self,
3401 server_info: &Info,
3402 claim_public_key: PublicKey,
3403 refund_public_key: PublicKey,
3404 preimage_hash: ripemd160::Hash,
3405 timeout_block_heights: &TimeoutBlockHeights,
3406 expected_address: &ArkAddress,
3407 ) -> Result<VhtlcScript, Error> {
3408 let unilateral_claim_delay =
3409 parse_sequence_number(timeout_block_heights.unilateral_claim as i64)
3410 .map_err(|e| Error::ad_hoc(format!("invalid unilateral claim timeout: {e}")))?;
3411 let unilateral_refund_delay =
3412 parse_sequence_number(timeout_block_heights.unilateral_refund as i64)
3413 .map_err(|e| Error::ad_hoc(format!("invalid unilateral refund timeout: {e}")))?;
3414 let unilateral_refund_without_receiver_delay =
3415 parse_sequence_number(timeout_block_heights.unilateral_refund_without_receiver as i64)
3416 .map_err(|e| {
3417 Error::ad_hoc(format!("invalid refund without receiver timeout: {e}"))
3418 })?;
3419
3420 self.reconstruct_vhtlc_for_address(
3421 server_info,
3422 |server| {
3423 Ok(VhtlcOptions {
3424 sender: refund_public_key.inner.x_only_public_key().0,
3425 receiver: claim_public_key.inner.x_only_public_key().0,
3426 server,
3427 preimage_hash,
3428 refund_locktime: timeout_block_heights.refund,
3429 unilateral_claim_delay,
3430 unilateral_refund_delay,
3431 unilateral_refund_without_receiver_delay,
3432 })
3433 },
3434 expected_address,
3435 )
3436 }
3437
3438 fn ensure_swap_key_cached(
3445 &self,
3446 pk: &XOnlyPublicKey,
3447 key_derivation_index: Option<u32>,
3448 swap_id: &str,
3449 ) -> bool {
3450 if self.keypair_by_pk(pk).is_ok() {
3452 return true;
3453 }
3454
3455 let Some(index) = key_derivation_index else {
3456 tracing::warn!(
3457 swap_id,
3458 "Legacy swap data without derivation index, skipping recovery"
3459 );
3460 return false;
3461 };
3462
3463 let Some(key_provider) = self.inner.discoverable_key_provider.as_ref() else {
3464 return false;
3465 };
3466
3467 match key_provider.derive_at_discovery_index(index) {
3468 Ok(Some(kp)) if kp.x_only_public_key().0 == *pk => {
3469 if let Err(e) = key_provider.cache_discovered_keypair(index, kp) {
3470 tracing::warn!(swap_id, %e, "Failed to cache swap key");
3471 return false;
3472 }
3473 true
3474 }
3475 Ok(_) => {
3476 tracing::warn!(
3477 swap_id,
3478 index,
3479 "Key at stored derivation index does not match swap pubkey"
3480 );
3481 false
3482 }
3483 Err(e) => {
3484 tracing::warn!(swap_id, index, %e, "Failed to derive key at stored index");
3485 false
3486 }
3487 }
3488 }
3489
3490 async fn collect_active_vhtlc_infos(&self) -> Result<Vec<VhtlcInfo>, Error> {
3491 let submarine_swaps = self
3492 .swap_storage()
3493 .list_all_submarine()
3494 .await
3495 .context("failed to list submarine swaps")?;
3496
3497 let reverse_swaps = self
3498 .swap_storage()
3499 .list_all_reverse()
3500 .await
3501 .context("failed to list reverse swaps")?;
3502
3503 let server_info = self.server_info().await?;
3504 let mut infos = Vec::new();
3505
3506 for swap in &submarine_swaps {
3507 if swap.status.is_terminal() {
3508 continue;
3509 }
3510
3511 if !self.ensure_swap_key_cached(
3513 &swap.refund_public_key.inner.x_only_public_key().0,
3514 swap.key_derivation_index,
3515 &swap.id,
3516 ) {
3517 continue;
3518 }
3519
3520 let vhtlc = self.build_vhtlc_script(
3521 &server_info,
3522 swap.claim_public_key,
3523 swap.refund_public_key,
3524 swap.preimage_hash,
3525 &swap.timeout_block_heights,
3526 &swap.vhtlc_address,
3527 )?;
3528
3529 let refund_script = vhtlc.refund_without_receiver_script();
3533 let spend_info = vhtlc.taproot_spend_info();
3534 let control_block = spend_info
3535 .control_block(&(refund_script.clone(), LeafVersion::TapScript))
3536 .ok_or_else(|| {
3537 Error::ad_hoc("control block not found for refund_without_receiver script")
3538 })?;
3539
3540 infos.push(VhtlcInfo {
3541 swap_id: swap.id.clone(),
3542 address: swap.vhtlc_address,
3543 script_pubkey: vhtlc.script_pubkey(),
3544 vhtlc,
3545 intent_spend_info: (refund_script, control_block),
3546 preimage: swap.preimage,
3547 });
3548 }
3549
3550 for swap in &reverse_swaps {
3551 if swap.status.is_terminal() {
3552 continue;
3553 }
3554
3555 if !self.ensure_swap_key_cached(
3557 &swap.claim_public_key.inner.x_only_public_key().0,
3558 swap.key_derivation_index,
3559 &swap.id,
3560 ) {
3561 continue;
3562 }
3563
3564 let vhtlc = self.build_vhtlc_script(
3565 &server_info,
3566 swap.claim_public_key,
3567 swap.refund_public_key,
3568 swap.preimage_hash,
3569 &swap.timeout_block_heights,
3570 &swap.vhtlc_address,
3571 )?;
3572
3573 let claim_script = vhtlc.claim_script();
3576 let spend_info = vhtlc.taproot_spend_info();
3577 let control_block = spend_info
3578 .control_block(&(claim_script.clone(), LeafVersion::TapScript))
3579 .ok_or_else(|| Error::ad_hoc("control block not found for claim script"))?;
3580
3581 infos.push(VhtlcInfo {
3582 swap_id: swap.id.clone(),
3583 address: swap.vhtlc_address,
3584 script_pubkey: vhtlc.script_pubkey(),
3585 vhtlc,
3586 intent_spend_info: (claim_script, control_block),
3587 preimage: swap.preimage,
3588 });
3589 }
3590
3591 Ok(infos)
3592 }
3593
3594 fn identify_vhtlc_spend_type(
3596 info: &VhtlcInfo,
3597 pending_tx: &PendingTx,
3598 ) -> Result<PendingVhtlcSpendType, Error> {
3599 let spend_script = pending_tx
3601 .signed_ark_tx
3602 .inputs
3603 .iter()
3604 .find_map(|input| {
3605 input.tap_scripts.values().find_map(|(script, _)| {
3606 let claim = info.vhtlc.claim_script();
3608 let refund = info.vhtlc.refund_script();
3609 let refund_no_recv = info.vhtlc.refund_without_receiver_script();
3610
3611 if *script == claim || *script == refund || *script == refund_no_recv {
3612 Some(script.clone())
3613 } else {
3614 None
3615 }
3616 })
3617 })
3618 .ok_or_else(|| {
3619 Error::ad_hoc(format!(
3620 "could not identify spend script in pending tx {} for swap {}",
3621 pending_tx.ark_txid, info.swap_id
3622 ))
3623 })?;
3624
3625 let claim_script = info.vhtlc.claim_script();
3626 let refund_script = info.vhtlc.refund_script();
3627
3628 if spend_script == claim_script {
3629 let preimage = extract_preimage_from_psbt(&pending_tx.signed_ark_tx)
3633 .ok()
3634 .or(info.preimage)
3635 .ok_or_else(|| {
3636 Error::ad_hoc(format!(
3637 "cannot recover preimage for pending claim of swap {}",
3638 info.swap_id
3639 ))
3640 })?;
3641
3642 Ok(PendingVhtlcSpendType::Claim {
3643 swap_id: info.swap_id.clone(),
3644 preimage,
3645 })
3646 } else if spend_script == refund_script {
3647 Ok(PendingVhtlcSpendType::CollaborativeRefund {
3648 swap_id: info.swap_id.clone(),
3649 })
3650 } else {
3651 Ok(PendingVhtlcSpendType::ExpiredRefund {
3652 swap_id: info.swap_id.clone(),
3653 })
3654 }
3655 }
3656
3657 fn inject_preimage_into_psbt(psbt: &mut Psbt, preimage: [u8; 32]) {
3659 let mut bytes = vec![1];
3660 let length = VarInt::from(preimage.len() as u64);
3661 length
3662 .consensus_encode(&mut bytes)
3663 .expect("valid length encoding");
3664 bytes.write_all(&preimage).expect("valid preimage encoding");
3665
3666 let key = psbt::raw::Key {
3667 type_value: 222,
3668 key: VTXO_CONDITION_KEY.to_vec(),
3669 };
3670
3671 for input in &mut psbt.inputs {
3672 input.unknown.insert(key.clone(), bytes.clone());
3673 }
3674 }
3675
3676 fn sign_checkpoint_with_own_keys(&self, checkpoint_psbt: &mut Psbt) -> Result<(), Error> {
3678 let sign_fn =
3679 |input: &mut psbt::Input,
3680 msg: secp256k1::Message|
3681 -> Result<Vec<(schnorr::Signature, XOnlyPublicKey)>, ark_core::Error> {
3682 let script = input.witness_script.as_ref().ok_or_else(|| {
3683 ark_core::Error::ad_hoc("missing witness script for checkpoint signing")
3684 })?;
3685 let pks = extract_checksig_pubkeys(script);
3686 let mut res = vec![];
3687 for pk in pks {
3688 if let Ok(keypair) = self.keypair_by_pk(&pk) {
3689 let sig = Secp256k1::new().sign_schnorr_no_aux_rand(&msg, &keypair);
3690 res.push((sig, keypair.x_only_public_key().0));
3691 }
3692 }
3693 Ok(res)
3694 };
3695
3696 sign_checkpoint_transaction(sign_fn, checkpoint_psbt)?;
3697 Ok(())
3698 }
3699
3700 fn restore_witness_script_if_needed(
3704 checkpoint_psbt: &mut Psbt,
3705 signed_ark_tx: &Psbt,
3706 ) -> Result<(), Error> {
3707 if checkpoint_psbt
3708 .inputs
3709 .first()
3710 .ok_or_else(|| Error::ad_hoc("checkpoint PSBT has no inputs"))?
3711 .witness_script
3712 .is_some()
3713 {
3714 return Ok(());
3715 }
3716
3717 let checkpoint_txid = checkpoint_psbt.unsigned_tx.compute_txid();
3718
3719 let ark_input_idx = signed_ark_tx
3720 .unsigned_tx
3721 .input
3722 .iter()
3723 .position(|inp| inp.previous_output.txid == checkpoint_txid)
3724 .ok_or_else(|| {
3725 Error::ad_hoc(format!(
3726 "checkpoint txid {checkpoint_txid} not found in ark tx inputs"
3727 ))
3728 })?;
3729
3730 let witness_script = signed_ark_tx
3731 .inputs
3732 .get(ark_input_idx)
3733 .and_then(|input| input.witness_script.clone())
3734 .ok_or_else(|| {
3735 Error::ad_hoc(format!(
3736 "missing witness script on ark tx input {ark_input_idx}"
3737 ))
3738 })?;
3739
3740 checkpoint_psbt
3741 .inputs
3742 .first_mut()
3743 .ok_or_else(|| Error::ad_hoc("checkpoint PSBT has no inputs"))?
3744 .witness_script = Some(witness_script);
3745 Ok(())
3746 }
3747}
3748
3749struct VhtlcInfo {
3751 swap_id: String,
3752 address: ArkAddress,
3753 script_pubkey: ScriptBuf,
3754 vhtlc: VhtlcScript,
3755 intent_spend_info: (ScriptBuf, bitcoin::taproot::ControlBlock),
3757 preimage: Option<[u8; 32]>,
3758}
3759
3760fn reconstruct_btc_htlc(
3765 server_pk: PublicKey,
3766 user_pk: PublicKey,
3767 swap_tree: &SwapTree,
3768) -> Result<bitcoin::taproot::TaprootSpendInfo, Error> {
3769 let claim_script_bytes: Vec<u8> = bitcoin::hex::FromHex::from_hex(&swap_tree.claim_leaf.output)
3770 .map_err(|e| Error::ad_hoc(format!("invalid claim leaf hex: {e}")))?;
3771 let claim_script = ScriptBuf::from_bytes(claim_script_bytes);
3772
3773 let refund_script_bytes: Vec<u8> =
3774 bitcoin::hex::FromHex::from_hex(&swap_tree.refund_leaf.output)
3775 .map_err(|e| Error::ad_hoc(format!("invalid refund leaf hex: {e}")))?;
3776 let refund_script = ScriptBuf::from_bytes(refund_script_bytes);
3777
3778 let musig_server_pk = musig::PublicKey::from_slice(&server_pk.to_bytes())
3779 .map_err(|e| Error::ad_hoc(format!("invalid server key for musig: {e}")))?;
3780 let musig_user_pk = musig::PublicKey::from_slice(&user_pk.to_bytes())
3781 .map_err(|e| Error::ad_hoc(format!("invalid user key for musig: {e}")))?;
3782
3783 let key_agg = musig::musig::KeyAggCache::new(&[&musig_server_pk, &musig_user_pk]);
3784 let internal_key = XOnlyPublicKey::from_slice(&key_agg.agg_pk().serialize())
3785 .map_err(|e| Error::ad_hoc(format!("invalid aggregated key: {e}")))?;
3786
3787 let secp = Secp256k1::new();
3788 bitcoin::taproot::TaprootBuilder::new()
3789 .add_leaf(1, claim_script)
3790 .map_err(|e| Error::ad_hoc(format!("failed to add claim leaf: {e}")))?
3791 .add_leaf(1, refund_script)
3792 .map_err(|e| Error::ad_hoc(format!("failed to add refund leaf: {e}")))?
3793 .finalize(&secp, internal_key)
3794 .map_err(|_| Error::ad_hoc("failed to finalize taproot tree"))
3795}
3796
3797fn vhtlc_tapscripts(vhtlc: &VhtlcScript) -> Vec<ScriptBuf> {
3799 vec![
3800 vhtlc.claim_script(),
3801 vhtlc.refund_script(),
3802 vhtlc.refund_without_receiver_script(),
3803 vhtlc.unilateral_claim_script(),
3804 vhtlc.unilateral_refund_script(),
3805 vhtlc.unilateral_refund_without_receiver_script(),
3806 ]
3807}
3808
3809fn extract_preimage_from_psbt(psbt: &Psbt) -> Result<[u8; 32], Error> {
3814 let condition_key = psbt::raw::Key {
3815 type_value: 222,
3816 key: VTXO_CONDITION_KEY.to_vec(),
3817 };
3818
3819 for input in &psbt.inputs {
3820 if let Some(condition_data) = input.unknown.get(&condition_key) {
3821 if condition_data.is_empty() {
3822 continue;
3823 }
3824
3825 let num_elements = condition_data[0] as usize;
3827 if num_elements == 0 {
3828 continue;
3829 }
3830
3831 let mut cursor = std::io::Cursor::new(&condition_data[1..]);
3833 let length = bitcoin::consensus::Decodable::consensus_decode(&mut cursor)
3834 .map_err(|e| Error::ad_hoc(format!("failed to decode varint length: {e}")))?;
3835 let length: VarInt = length;
3836 let offset = cursor.position() as usize;
3837 let remaining = &condition_data[1 + offset..];
3838
3839 if remaining.len() < length.0 as usize {
3840 return Err(Error::ad_hoc(format!(
3841 "condition data too short: expected {} bytes, got {}",
3842 length.0,
3843 remaining.len()
3844 )));
3845 }
3846
3847 let preimage_bytes = &remaining[..length.0 as usize];
3848
3849 let preimage: [u8; 32] = preimage_bytes.try_into().map_err(|_| {
3850 Error::ad_hoc(format!(
3851 "preimage has unexpected length: {} (expected 32)",
3852 preimage_bytes.len()
3853 ))
3854 })?;
3855
3856 return Ok(preimage);
3857 }
3858 }
3859
3860 Err(Error::ad_hoc(
3861 "no VTXO_CONDITION_KEY found in any PSBT input",
3862 ))
3863}
3864
3865pub enum SwapAmount {
3867 Invoice(Amount),
3869 Vhtlc(Amount),
3871}
3872
3873impl SwapAmount {
3874 pub fn invoice(amount: Amount) -> Self {
3875 Self::Invoice(amount)
3876 }
3877
3878 pub fn vhtlc(amount: Amount) -> Self {
3879 Self::Vhtlc(amount)
3880 }
3881}
3882
3883pub enum ChainSwapAmount {
3885 UserLock(Amount),
3887 ServerLock(Amount),
3889}
3890
3891#[serde_as]
3893#[derive(Debug, Clone, Serialize, Deserialize)]
3894pub struct SubmarineSwapData {
3895 pub id: String,
3897 pub preimage: Option<[u8; 32]>,
3899 pub preimage_hash: ripemd160::Hash,
3901 pub claim_public_key: PublicKey,
3903 pub refund_public_key: PublicKey,
3905 pub amount: Amount,
3907 pub timeout_block_heights: TimeoutBlockHeights,
3909 #[serde_as(as = "DisplayFromStr")]
3911 pub vhtlc_address: ArkAddress,
3912 pub invoice: Bolt11Invoice,
3914 pub status: SwapStatus,
3916 pub created_at: u64,
3918 #[serde(default)]
3922 pub key_derivation_index: Option<u32>,
3923}
3924
3925#[serde_as]
3927#[derive(Debug, Clone, Serialize, Deserialize)]
3928pub struct ReverseSwapData {
3929 pub id: String,
3931 pub preimage: Option<[u8; 32]>,
3933 pub preimage_hash: ripemd160::Hash,
3935 pub claim_public_key: PublicKey,
3937 pub refund_public_key: PublicKey,
3939 pub amount: Amount,
3941 pub timeout_block_heights: TimeoutBlockHeights,
3943 #[serde_as(as = "DisplayFromStr")]
3945 pub vhtlc_address: ArkAddress,
3946 pub status: SwapStatus,
3948 pub created_at: u64,
3950 #[serde(default)]
3954 pub key_derivation_index: Option<u32>,
3955 pub bolt11: String,
3957 pub invoice_expiry: u64,
3959 #[serde_as(as = "Option<DisplayFromStr>")]
3964 #[serde(default)]
3965 pub claim_address: Option<ArkAddress>,
3966}
3967
3968#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
3972pub enum SwapStatus {
3973 #[serde(rename = "swap.created")]
3975 Created,
3976 #[serde(rename = "transaction.mempool")]
3978 TransactionMempool,
3979 #[serde(rename = "transaction.confirmed")]
3981 TransactionConfirmed,
3982 #[serde(rename = "transaction.refunded")]
3984 TransactionRefunded,
3985 #[serde(rename = "transaction.failed")]
3987 TransactionFailed,
3988 #[serde(rename = "transaction.claimed")]
3990 TransactionClaimed,
3991 #[serde(rename = "transaction.server.mempool")]
3993 TransactionServerMempool,
3994 #[serde(rename = "transaction.server.confirmed")]
3996 TransactionServerConfirmed,
3997 #[serde(rename = "invoice.set")]
3999 InvoiceSet,
4000 #[serde(rename = "invoice.pending")]
4002 InvoicePending,
4003 #[serde(rename = "invoice.paid")]
4005 InvoicePaid,
4006 #[serde(rename = "invoice.failedToPay")]
4008 InvoiceFailedToPay,
4009 #[serde(rename = "invoice.expired")]
4011 InvoiceExpired,
4012 #[serde(rename = "transaction.lockupFailed")]
4014 TransactionLockupFailed,
4015 #[serde(rename = "swap.expired")]
4017 SwapExpired,
4018 #[serde(rename = "error")]
4020 Error { error: String },
4021 #[serde(untagged)]
4023 Other(String),
4024}
4025
4026impl SwapStatus {
4027 pub fn is_terminal(&self) -> bool {
4029 matches!(
4030 self,
4031 Self::TransactionRefunded
4032 | Self::TransactionFailed
4033 | Self::TransactionClaimed
4034 | Self::TransactionLockupFailed
4035 | Self::InvoicePaid
4036 | Self::InvoiceFailedToPay
4037 | Self::InvoiceExpired
4038 | Self::SwapExpired
4039 | Self::Error { .. }
4040 )
4041 }
4042}
4043
4044#[derive(Debug, Clone, Serialize, Deserialize, Copy)]
4045#[serde(rename_all = "camelCase")]
4046pub struct TimeoutBlockHeights {
4047 pub refund: u32,
4048 pub unilateral_claim: u32,
4049 pub unilateral_refund: u32,
4050 pub unilateral_refund_without_receiver: u32,
4051}
4052
4053#[derive(Debug, Clone, Serialize, Deserialize)]
4054#[serde(rename_all = "UPPERCASE")]
4055enum Asset {
4056 Btc,
4057 Ark,
4058}
4059
4060#[derive(Debug, Clone, Serialize, Deserialize)]
4061#[serde(rename_all = "camelCase")]
4062struct CreateReverseSwapRequest {
4063 from: Asset,
4064 to: Asset,
4065 #[serde(skip_serializing_if = "Option::is_none")]
4066 invoice_amount: Option<Amount>,
4067 #[serde(skip_serializing_if = "Option::is_none")]
4068 onchain_amount: Option<Amount>,
4069 claim_public_key: PublicKey,
4070 preimage_hash: sha256::Hash,
4071 #[serde(skip_serializing_if = "Option::is_none")]
4075 invoice_expiry: Option<u64>,
4076 #[serde(skip_serializing_if = "Option::is_none")]
4077 referral_id: Option<String>,
4078 #[serde(skip_serializing_if = "Option::is_none")]
4079 description: Option<String>,
4080}
4081
4082#[serde_as]
4083#[derive(Debug, Clone, Serialize, Deserialize)]
4084#[serde(rename_all = "camelCase")]
4085struct CreateReverseSwapResponse {
4086 id: String,
4087 #[serde_as(as = "DisplayFromStr")]
4088 lockup_address: ArkAddress,
4089 refund_public_key: PublicKey,
4090 timeout_block_heights: TimeoutBlockHeights,
4091 invoice: Bolt11Invoice,
4092 onchain_amount: Option<Amount>,
4093}
4094
4095#[derive(Debug, Clone, Serialize, Deserialize)]
4096struct CreateSubmarineSwapRequest {
4097 from: Asset,
4098 to: Asset,
4099 invoice: Bolt11Invoice,
4100 #[serde(rename = "refundPublicKey")]
4101 refund_public_key: PublicKey,
4102 #[serde(rename = "referralId", skip_serializing_if = "Option::is_none")]
4103 referral_id: Option<String>,
4104}
4105
4106#[serde_as]
4107#[derive(Debug, Clone, Serialize, Deserialize)]
4108#[serde(rename_all = "camelCase")]
4109struct CreateSubmarineSwapResponse {
4110 id: String,
4111 #[serde_as(as = "DisplayFromStr")]
4112 address: ArkAddress,
4113 expected_amount: Amount,
4114 claim_public_key: PublicKey,
4115 timeout_block_heights: TimeoutBlockHeights,
4116}
4117
4118#[derive(Debug, Clone, Serialize, Deserialize)]
4119struct GetSwapStatusResponse {
4120 status: SwapStatus,
4121 #[serde(default)]
4122 transaction: Option<SwapStatusTransaction>,
4123}
4124
4125#[derive(Debug, Clone, Serialize, Deserialize)]
4126struct SwapStatusTransaction {
4127 id: String,
4128}
4129
4130#[derive(Debug, Clone, Serialize, Deserialize)]
4131struct RefundSwapRequest {
4132 transaction: String,
4133 checkpoint: String,
4134}
4135
4136#[derive(Debug, Clone, Serialize, Deserialize)]
4137struct RefundSwapResponse {
4138 transaction: String,
4139 checkpoint: String,
4140 #[serde(skip_serializing_if = "Option::is_none")]
4141 error: Option<String>,
4142}
4143
4144#[derive(Debug, Clone, Serialize, Deserialize)]
4146#[serde(rename_all = "camelCase")]
4147pub struct SubmarineSwapFees {
4148 pub percentage: f64,
4150 pub miner_fees: u64,
4152}
4153
4154#[derive(Debug, Clone, Serialize, Deserialize)]
4156pub struct ReverseMinerFees {
4157 pub lockup: u64,
4159 pub claim: u64,
4161}
4162
4163#[derive(Debug, Clone, Serialize, Deserialize)]
4165#[serde(rename_all = "camelCase")]
4166pub struct ReverseSwapFees {
4167 pub percentage: f64,
4169 pub miner_fees: ReverseMinerFees,
4171}
4172
4173#[derive(Debug, Clone, Serialize, Deserialize)]
4175pub struct BoltzFees {
4176 pub submarine: SubmarineSwapFees,
4178 pub reverse: ReverseSwapFees,
4180}
4181
4182#[derive(Debug, Clone, Serialize, Deserialize)]
4184pub struct SwapLimits {
4185 pub min: u64,
4187 pub max: u64,
4189}
4190
4191#[derive(Debug, Clone, Deserialize)]
4194struct PairLimits {
4195 minimal: u64,
4196 maximal: u64,
4197}
4198
4199#[derive(Debug, Clone, Deserialize)]
4201#[serde(rename_all = "camelCase")]
4202struct SubmarinePairFees {
4203 percentage: f64,
4204 miner_fees: u64,
4205}
4206
4207#[derive(Debug, Clone, Deserialize)]
4208struct SubmarinePairInfo {
4209 fees: SubmarinePairFees,
4210 limits: PairLimits,
4211}
4212
4213#[derive(Debug, Clone, Deserialize)]
4214#[serde(rename_all = "UPPERCASE")]
4215struct SubmarineArkPairs {
4216 btc: SubmarinePairInfo,
4217}
4218
4219#[derive(Debug, Clone, Deserialize)]
4220#[serde(rename_all = "UPPERCASE")]
4221struct SubmarinePairsResponse {
4222 ark: SubmarineArkPairs,
4223}
4224
4225#[derive(Debug, Clone, Deserialize)]
4227#[serde(rename_all = "camelCase")]
4228struct ReverseMinerFeesResponse {
4229 claim: u64,
4230 lockup: u64,
4231}
4232
4233#[derive(Debug, Clone, Deserialize)]
4234#[serde(rename_all = "camelCase")]
4235struct ReversePairFees {
4236 percentage: f64,
4237 miner_fees: ReverseMinerFeesResponse,
4238}
4239
4240#[derive(Debug, Clone, Deserialize)]
4241struct ReversePairInfo {
4242 fees: ReversePairFees,
4243}
4244
4245#[derive(Debug, Clone, Deserialize)]
4246#[serde(rename_all = "UPPERCASE")]
4247struct ReverseBtcPairs {
4248 ark: ReversePairInfo,
4249}
4250
4251#[derive(Debug, Clone, Deserialize)]
4252#[serde(rename_all = "UPPERCASE")]
4253struct ReversePairsResponse {
4254 btc: ReverseBtcPairs,
4255}
4256
4257#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
4261pub enum ChainSwapDirection {
4262 ArkToBtc,
4264 BtcToArk,
4266}
4267
4268#[serde_as]
4270#[derive(Debug, Clone, Serialize, Deserialize)]
4271pub struct ChainSwapData {
4272 pub id: String,
4274 pub status: SwapStatus,
4276 pub direction: ChainSwapDirection,
4278 pub preimage: Option<[u8; 32]>,
4280 pub preimage_hash: sha256::Hash,
4282 pub claim_public_key: PublicKey,
4284 pub refund_public_key: PublicKey,
4286 pub server_claim_public_key: PublicKey,
4288 pub server_refund_public_key: PublicKey,
4290 pub user_lockup_address: String,
4292 pub server_lockup_address: String,
4294 pub user_lockup_amount: Amount,
4296 pub server_lockup_amount: Amount,
4298 pub user_timeout_block_height: u32,
4300 pub server_timeout_block_height: u32,
4302 #[serde(default)]
4304 pub user_timeout_block_heights: Option<TimeoutBlockHeights>,
4305 #[serde(default)]
4307 pub server_timeout_block_heights: Option<TimeoutBlockHeights>,
4308 #[serde(default)]
4310 pub bip21: Option<String>,
4311 #[serde(default)]
4313 pub swap_tree: Option<SwapTree>,
4314 pub created_at: u64,
4316 #[serde(default)]
4318 pub claim_key_derivation_index: Option<u32>,
4319 #[serde(default)]
4321 pub refund_key_derivation_index: Option<u32>,
4322}
4323
4324#[derive(Clone, Debug)]
4326pub struct ChainSwapResult {
4327 pub swap_id: String,
4329 pub user_lockup_address: String,
4331 pub user_lockup_amount: Amount,
4333 pub server_lockup_amount: Amount,
4335 pub bip21: Option<String>,
4337}
4338
4339#[derive(Debug, Clone, Serialize, Deserialize)]
4343#[serde(rename_all = "camelCase")]
4344pub struct SwapTree {
4345 pub claim_leaf: SwapTreeLeaf,
4347 pub refund_leaf: SwapTreeLeaf,
4349}
4350
4351#[derive(Debug, Clone, Serialize, Deserialize)]
4353pub struct SwapTreeLeaf {
4354 pub version: u8,
4356 pub output: String,
4358}
4359
4360#[derive(Debug, Clone, Serialize, Deserialize)]
4361#[serde(rename_all = "camelCase")]
4362struct CreateChainSwapRequest {
4363 from: Asset,
4364 to: Asset,
4365 #[serde(skip_serializing_if = "Option::is_none")]
4366 user_lock_amount: Option<Amount>,
4367 #[serde(skip_serializing_if = "Option::is_none")]
4368 server_lock_amount: Option<Amount>,
4369 claim_public_key: PublicKey,
4370 refund_public_key: PublicKey,
4371 preimage_hash: sha256::Hash,
4372 #[serde(skip_serializing_if = "Option::is_none")]
4373 referral_id: Option<String>,
4374}
4375
4376#[serde_as]
4377#[derive(Debug, Clone, Serialize, Deserialize)]
4378#[serde(rename_all = "camelCase")]
4379struct CreateChainSwapResponse {
4380 id: String,
4381 claim_details: ChainSwapSideDetails,
4382 lockup_details: ChainSwapSideDetails,
4383}
4384
4385#[serde_as]
4386#[derive(Debug, Clone, Serialize, Deserialize)]
4387#[serde(rename_all = "camelCase")]
4388struct ChainSwapSideDetails {
4389 lockup_address: String,
4390 server_public_key: PublicKey,
4391 timeout_block_height: u32,
4392 #[serde(default)]
4393 timeouts: Option<TimeoutBlockHeights>,
4394 amount: Amount,
4395 #[serde(default)]
4396 swap_tree: Option<SwapTree>,
4397 #[serde(default)]
4398 bip21: Option<String>,
4399}
4400
4401pub(crate) fn reconstruct_vhtlc_from_keys(
4411 server_keys: impl Iterator<Item = XOnlyPublicKey>,
4412 network: bitcoin::Network,
4413 mk_opts: impl Fn(XOnlyPublicKey) -> Result<VhtlcOptions, Error>,
4414 expected_address: &ArkAddress,
4415) -> Result<VhtlcScript, Error> {
4416 for server_key in server_keys {
4417 let opts = mk_opts(server_key)?;
4418 let vhtlc = VhtlcScript::new(opts, network).map_err(Error::ad_hoc)?;
4419 if &vhtlc.address() == expected_address {
4420 return Ok(vhtlc);
4421 }
4422 }
4423 Err(Error::ad_hoc(format!(
4424 "VHTLC script could not be reconstructed for address {expected_address}: \
4425 does not match current or any deprecated server key"
4426 )))
4427}
4428
4429#[cfg(test)]
4430mod tests {
4431 use super::*;
4432
4433 #[test]
4434 fn test_deserialize_create_reverse_swap_response() {
4435 let json = r#"{
4436 "id": "vqhG2fJtNY4H",
4437 "lockupAddress": "tark1qra883hysahlkt0ujcwhv0x2n278849c3m7t3a08l7fdc40f4f2nmw3f7kn37vvq0hqazxtqgtvhwp3z83zfgr7qc82t9mty8vk95ynpx3l43d",
4438 "refundPublicKey": "0206988651c7fbe41747bb21b54ced0a183f4d658e007ee8fdb23fbbfccb8e0c55",
4439 "timeoutBlockHeights": {
4440 "refund": 1760508054,
4441 "unilateralClaim": 9728,
4442 "unilateralRefund": 86528,
4443 "unilateralRefundWithoutReceiver": 86528
4444 },
4445 "invoice": "lntbs10u1p5wmeeepp56ms94rkev7tdrwqyus5a63lny2mqzq9vh2rq3u4ym3v4lxv6xl4qdql2djkuepqw3hjqs2jfvsxzerywfjhxuccqz95xqztfsp5ckaskagag554na8d56tlrfdxasstqrmmpkvswqqqx6y386jcfq9s9qxpqysgqt7z0vkdwkqamydae7ctgkh7l8q75w7q9394ce3lda2mkfxrpfdtj5gmltuctav7jdgatkflhztrjjzutdla5e4xp0uhxxy7sluzll4qpkkh6wv",
4446 "onchainAmount": 996
4447}"#;
4448
4449 let response: CreateReverseSwapResponse =
4450 serde_json::from_str(json).expect("Failed to deserialize CreateReverseSwapResponse");
4451
4452 assert_eq!(response.id, "vqhG2fJtNY4H");
4454 assert_eq!(response.onchain_amount, Some(Amount::from_sat(996)));
4455 assert_eq!(
4456 response.refund_public_key,
4457 PublicKey::from_str(
4458 "0206988651c7fbe41747bb21b54ced0a183f4d658e007ee8fdb23fbbfccb8e0c55"
4459 )
4460 .expect("valid public key")
4461 );
4462 assert_eq!(
4463 response.lockup_address.to_string(),
4464 "tark1qra883hysahlkt0ujcwhv0x2n278849c3m7t3a08l7fdc40f4f2nmw3f7kn37vvq0hqazxtqgtvhwp3z83zfgr7qc82t9mty8vk95ynpx3l43d"
4465 );
4466 assert_eq!(response.timeout_block_heights.refund, 1760508054);
4467 assert_eq!(response.timeout_block_heights.unilateral_claim, 9728);
4468 assert_eq!(response.timeout_block_heights.unilateral_refund, 86528);
4469 assert_eq!(
4470 response
4471 .timeout_block_heights
4472 .unilateral_refund_without_receiver,
4473 86528
4474 );
4475 }
4476
4477 #[test]
4478 fn test_btc_htlc_address_reconstruction_btc_to_ark() {
4479 let server_pk = PublicKey::from_str(
4483 "03ce9f5a57218103d5fe07b9d7ecf4b28ad60a960f0fbfd86dd090013020617389",
4484 )
4485 .unwrap();
4486 let user_pk = PublicKey::from_str(
4487 "02c6047f9441ed7d6d3045406e95c07cd85c778e4b8cef3ca7abac09b95c709ee5",
4488 )
4489 .unwrap();
4490 let swap_tree = SwapTree {
4491 claim_leaf: SwapTreeLeaf {
4492 version: 192,
4493 output: "82012088a914b472a266d0bd89c13706a4132ccfb16f7c3b9fcb8820ce9f5a57218103d5fe07b9d7ecf4b28ad60a960f0fbfd86dd090013020617389ac".into(),
4494 },
4495 refund_leaf: SwapTreeLeaf {
4496 version: 192,
4497 output: "20c6047f9441ed7d6d3045406e95c07cd85c778e4b8cef3ca7abac09b95c709ee5ad03f9832db1".into(),
4498 },
4499 };
4500
4501 let spend_info = reconstruct_btc_htlc(server_pk, user_pk, &swap_tree).unwrap();
4502
4503 let secp = Secp256k1::new();
4504 let spk = ScriptBuf::new_p2tr(&secp, spend_info.internal_key(), spend_info.merkle_root());
4505 let addr = bitcoin::Address::from_script(&spk, bitcoin::Network::Testnet).unwrap();
4506
4507 assert_eq!(
4508 addr.to_string(),
4509 "tb1ptf632fkczflsjn4356ra4x2s6qp6vvk8e7pplprpwnkvcsd8tpwqkw92c7"
4510 );
4511 }
4512
4513 #[test]
4514 fn submarine_swap_request_serializes_referral_id_when_set() {
4515 let request = CreateSubmarineSwapRequest {
4516 from: Asset::Ark,
4517 to: Asset::Btc,
4518 invoice: Bolt11Invoice::from_str(
4519 "lntbs10u1p5wmeeepp56ms94rkev7tdrwqyus5a63lny2mqzq9vh2rq3u4ym3v4lxv6xl4qdql2djkuepqw3hjqs2jfvsxzerywfjhxuccqz95xqztfsp5ckaskagag554na8d56tlrfdxasstqrmmpkvswqqqx6y386jcfq9s9qxpqysgqt7z0vkdwkqamydae7ctgkh7l8q75w7q9394ce3lda2mkfxrpfdtj5gmltuctav7jdgatkflhztrjjzutdla5e4xp0uhxxy7sluzll4qpkkh6wv",
4520 )
4521 .unwrap(),
4522 refund_public_key: PublicKey::from_str(
4523 "02c6047f9441ed7d6d3045406e95c07cd85c778e4b8cef3ca7abac09b95c709ee5",
4524 )
4525 .unwrap(),
4526 referral_id: Some("partner-xyz".to_string()),
4527 };
4528
4529 let json: serde_json::Value = serde_json::to_value(&request).unwrap();
4530 assert_eq!(json["referralId"], "partner-xyz");
4531 }
4532
4533 #[test]
4534 fn submarine_swap_request_omits_referral_id_when_none() {
4535 let request = CreateSubmarineSwapRequest {
4536 from: Asset::Ark,
4537 to: Asset::Btc,
4538 invoice: Bolt11Invoice::from_str(
4539 "lntbs10u1p5wmeeepp56ms94rkev7tdrwqyus5a63lny2mqzq9vh2rq3u4ym3v4lxv6xl4qdql2djkuepqw3hjqs2jfvsxzerywfjhxuccqz95xqztfsp5ckaskagag554na8d56tlrfdxasstqrmmpkvswqqqx6y386jcfq9s9qxpqysgqt7z0vkdwkqamydae7ctgkh7l8q75w7q9394ce3lda2mkfxrpfdtj5gmltuctav7jdgatkflhztrjjzutdla5e4xp0uhxxy7sluzll4qpkkh6wv",
4540 )
4541 .unwrap(),
4542 refund_public_key: PublicKey::from_str(
4543 "02c6047f9441ed7d6d3045406e95c07cd85c778e4b8cef3ca7abac09b95c709ee5",
4544 )
4545 .unwrap(),
4546 referral_id: None,
4547 };
4548
4549 let json: serde_json::Value = serde_json::to_value(&request).unwrap();
4550 assert!(json.get("referralId").is_none());
4551 assert!(json.get("referral_id").is_none());
4552 }
4553
4554 #[test]
4555 fn reverse_swap_request_serializes_referral_id_when_set() {
4556 let request = CreateReverseSwapRequest {
4557 from: Asset::Btc,
4558 to: Asset::Ark,
4559 invoice_amount: Some(Amount::from_sat(1000)),
4560 onchain_amount: None,
4561 claim_public_key: PublicKey::from_str(
4562 "02c6047f9441ed7d6d3045406e95c07cd85c778e4b8cef3ca7abac09b95c709ee5",
4563 )
4564 .unwrap(),
4565 preimage_hash: sha256::Hash::from_byte_array([1u8; 32]),
4566 invoice_expiry: Some(3600),
4567 referral_id: Some("partner-xyz".to_string()),
4568 description: None,
4569 };
4570
4571 let json: serde_json::Value = serde_json::to_value(&request).unwrap();
4572 assert_eq!(json["referralId"], "partner-xyz");
4573 }
4574
4575 #[test]
4576 fn reverse_swap_request_omits_referral_id_when_none() {
4577 let request = CreateReverseSwapRequest {
4578 from: Asset::Btc,
4579 to: Asset::Ark,
4580 invoice_amount: Some(Amount::from_sat(1000)),
4581 onchain_amount: None,
4582 claim_public_key: PublicKey::from_str(
4583 "02c6047f9441ed7d6d3045406e95c07cd85c778e4b8cef3ca7abac09b95c709ee5",
4584 )
4585 .unwrap(),
4586 preimage_hash: sha256::Hash::from_byte_array([1u8; 32]),
4587 invoice_expiry: Some(3600),
4588 referral_id: None,
4589 description: None,
4590 };
4591
4592 let json: serde_json::Value = serde_json::to_value(&request).unwrap();
4593 assert!(json.get("referralId").is_none());
4594 assert!(json.get("referral_id").is_none());
4595 }
4596
4597 #[test]
4598 fn chain_swap_request_serializes_referral_id_when_set() {
4599 let request = CreateChainSwapRequest {
4600 from: Asset::Ark,
4601 to: Asset::Btc,
4602 user_lock_amount: Some(Amount::from_sat(1000)),
4603 server_lock_amount: None,
4604 claim_public_key: PublicKey::from_str(
4605 "02c6047f9441ed7d6d3045406e95c07cd85c778e4b8cef3ca7abac09b95c709ee5",
4606 )
4607 .unwrap(),
4608 refund_public_key: PublicKey::from_str(
4609 "0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798",
4610 )
4611 .unwrap(),
4612 preimage_hash: sha256::Hash::from_byte_array([1u8; 32]),
4613 referral_id: Some("partner-xyz".to_string()),
4614 };
4615
4616 let json: serde_json::Value = serde_json::to_value(&request).unwrap();
4617 assert_eq!(json["referralId"], "partner-xyz");
4618 }
4619
4620 #[test]
4621 fn chain_swap_request_omits_referral_id_when_none() {
4622 let request = CreateChainSwapRequest {
4623 from: Asset::Ark,
4624 to: Asset::Btc,
4625 user_lock_amount: Some(Amount::from_sat(1000)),
4626 server_lock_amount: None,
4627 claim_public_key: PublicKey::from_str(
4628 "02c6047f9441ed7d6d3045406e95c07cd85c778e4b8cef3ca7abac09b95c709ee5",
4629 )
4630 .unwrap(),
4631 refund_public_key: PublicKey::from_str(
4632 "0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798",
4633 )
4634 .unwrap(),
4635 preimage_hash: sha256::Hash::from_byte_array([1u8; 32]),
4636 referral_id: None,
4637 };
4638
4639 let json: serde_json::Value = serde_json::to_value(&request).unwrap();
4640 assert!(json.get("referralId").is_none());
4641 assert!(json.get("referral_id").is_none());
4642 }
4643
4644 #[test]
4645 fn test_btc_htlc_address_reconstruction_ark_to_btc() {
4646 let server_pk = PublicKey::from_str(
4650 "0207364dc5853e630be83439fde62b531e3c11db34ce8c4f454a56782555c58ed6",
4651 )
4652 .unwrap();
4653 let user_pk = PublicKey::from_str(
4654 "0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798",
4655 )
4656 .unwrap();
4657 let swap_tree = SwapTree {
4658 claim_leaf: SwapTreeLeaf {
4659 version: 192,
4660 output: "82012088a914cf7ff51392e9a37bc72c7284841db669c82e2c14882079be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798ac".into(),
4661 },
4662 refund_leaf: SwapTreeLeaf {
4663 version: 192,
4664 output: "2007364dc5853e630be83439fde62b531e3c11db34ce8c4f454a56782555c58ed6ad036b832db1".into(),
4665 },
4666 };
4667
4668 let spend_info = reconstruct_btc_htlc(server_pk, user_pk, &swap_tree).unwrap();
4669
4670 let secp = Secp256k1::new();
4671 let spk = ScriptBuf::new_p2tr(&secp, spend_info.internal_key(), spend_info.merkle_root());
4672 let addr = bitcoin::Address::from_script(&spk, bitcoin::Network::Testnet).unwrap();
4673
4674 assert_eq!(
4675 addr.to_string(),
4676 "tb1pxa78pf55g0aaurrd8c76fyax4df9e8y38fzps8sw2vkrecf9k3ss36a78m"
4677 );
4678 }
4679
4680 #[test]
4681 fn validate_invoice_description_accepts_none_empty_and_max_length() {
4682 assert!(validate_invoice_description(None).is_ok());
4683 assert!(validate_invoice_description(Some("")).is_ok());
4684 let at_limit = "a".repeat(MAX_BOLT11_DESCRIPTION_BYTES);
4685 assert!(validate_invoice_description(Some(&at_limit)).is_ok());
4686 }
4687
4688 #[test]
4689 fn validate_invoice_description_rejects_over_limit() {
4690 let too_long = "a".repeat(MAX_BOLT11_DESCRIPTION_BYTES + 1);
4691 let err = validate_invoice_description(Some(&too_long)).unwrap_err();
4692 let msg = err.to_string();
4693 assert!(msg.contains("640"), "unexpected error message: {msg}");
4694 assert!(msg.contains("639"), "unexpected error message: {msg}");
4695 }
4696
4697 fn fixture_opts(server: XOnlyPublicKey) -> VhtlcOptions {
4703 let sender = XOnlyPublicKey::from(
4704 PublicKey::from_str(
4705 "030192e796452d6df9697c280542e1560557bcf79a347d925895043136225c7cb4",
4706 )
4707 .unwrap()
4708 .inner,
4709 );
4710 let receiver = XOnlyPublicKey::from(
4711 PublicKey::from_str(
4712 "021e1bb85455fe3f5aed60d101aa4dbdb9e7714f6226769a97a17a5331dadcd53b",
4713 )
4714 .unwrap()
4715 .inner,
4716 );
4717 VhtlcOptions {
4718 sender,
4719 receiver,
4720 server,
4721 preimage_hash: ripemd160::Hash::from_str("4d487dd3753a89bc9fe98401d1196523058251fc")
4722 .unwrap(),
4723 refund_locktime: 265,
4724 unilateral_claim_delay: bitcoin::Sequence::from_height(17),
4725 unilateral_refund_delay: bitcoin::Sequence::from_height(144),
4726 unilateral_refund_without_receiver_delay: bitcoin::Sequence::from_height(144),
4727 }
4728 }
4729
4730 fn fixture_server_xonly() -> XOnlyPublicKey {
4731 XOnlyPublicKey::from(
4732 PublicKey::from_str(
4733 "03aad52d58162e9eefeafc7ad8a1cdca8060b5f01df1e7583362d052e266208f88",
4734 )
4735 .unwrap()
4736 .inner,
4737 )
4738 }
4739
4740 const FIXTURE_ADDRESS: &str = "tark1qz4d2t2czchfaml2l3ad3gwde2qxpd0srhc7wkpnvtg99cnxyz8c3pnvvhnhumhwhqthmlxmdryakwx99s6508y8dunj9sty2p5mr7unh5re63";
4742
4743 fn wrong_server_xonly() -> XOnlyPublicKey {
4745 XOnlyPublicKey::from(
4746 PublicKey::from_str(
4747 "0206988651c7fbe41747bb21b54ced0a183f4d658e007ee8fdb23fbbfccb8e0c55",
4748 )
4749 .unwrap()
4750 .inner,
4751 )
4752 }
4753
4754 #[test]
4755 fn reconstruct_matches_with_single_current_key() {
4756 let server = fixture_server_xonly();
4757 let expected = ArkAddress::decode(FIXTURE_ADDRESS).unwrap();
4758
4759 let vhtlc = reconstruct_vhtlc_from_keys(
4760 std::iter::once(server),
4761 bitcoin::Network::Testnet,
4762 |sk| Ok(fixture_opts(sk)),
4763 &expected,
4764 )
4765 .unwrap();
4766
4767 assert_eq!(vhtlc.address(), expected);
4768 }
4769
4770 #[test]
4771 fn reconstruct_skips_wrong_key_and_finds_deprecated() {
4772 let wrong = wrong_server_xonly();
4773 let correct = fixture_server_xonly();
4774 let expected = ArkAddress::decode(FIXTURE_ADDRESS).unwrap();
4775
4776 let keys = [wrong, correct].into_iter();
4778 let vhtlc = reconstruct_vhtlc_from_keys(
4779 keys,
4780 bitcoin::Network::Testnet,
4781 |sk| Ok(fixture_opts(sk)),
4782 &expected,
4783 )
4784 .unwrap();
4785
4786 assert_eq!(vhtlc.address(), expected);
4787 }
4788
4789 #[test]
4790 fn reconstruct_errors_when_no_key_matches() {
4791 let wrong = wrong_server_xonly();
4792 let expected = ArkAddress::decode(FIXTURE_ADDRESS).unwrap();
4793
4794 let err = reconstruct_vhtlc_from_keys(
4795 std::iter::once(wrong),
4796 bitcoin::Network::Testnet,
4797 |sk| Ok(fixture_opts(sk)),
4798 &expected,
4799 )
4800 .err()
4801 .expect("should have failed");
4802
4803 assert!(
4804 err.to_string()
4805 .contains("does not match current or any deprecated server key"),
4806 "unexpected error: {err}"
4807 );
4808 }
4809
4810 #[test]
4811 fn reconstruct_propagates_mk_opts_error() {
4812 let server = fixture_server_xonly();
4813 let expected = ArkAddress::decode(FIXTURE_ADDRESS).unwrap();
4814
4815 let err = reconstruct_vhtlc_from_keys(
4816 std::iter::once(server),
4817 bitcoin::Network::Testnet,
4818 |_| Err(Error::ad_hoc("options error")),
4819 &expected,
4820 )
4821 .err()
4822 .expect("should have failed");
4823
4824 assert!(
4825 err.to_string().contains("options error"),
4826 "unexpected: {err}"
4827 );
4828 }
4829
4830 #[test]
4831 fn build_vhtlc_script_sender_is_refund_receiver_is_claim() {
4832 let claim_pk = PublicKey::from_str(
4835 "021e1bb85455fe3f5aed60d101aa4dbdb9e7714f6226769a97a17a5331dadcd53b",
4836 )
4837 .unwrap();
4838 let refund_pk = PublicKey::from_str(
4839 "030192e796452d6df9697c280542e1560557bcf79a347d925895043136225c7cb4",
4840 )
4841 .unwrap();
4842 let server = fixture_server_xonly();
4843 let expected = ArkAddress::decode(FIXTURE_ADDRESS).unwrap();
4844
4845 let opts = VhtlcOptions {
4846 sender: refund_pk.inner.x_only_public_key().0,
4847 receiver: claim_pk.inner.x_only_public_key().0,
4848 server,
4849 preimage_hash: ripemd160::Hash::from_str("4d487dd3753a89bc9fe98401d1196523058251fc")
4850 .unwrap(),
4851 refund_locktime: 265,
4852 unilateral_claim_delay: bitcoin::Sequence::from_height(17),
4853 unilateral_refund_delay: bitcoin::Sequence::from_height(144),
4854 unilateral_refund_without_receiver_delay: bitcoin::Sequence::from_height(144),
4855 };
4856 let manual_vhtlc =
4857 VhtlcScript::new(opts, bitcoin::Network::Testnet).expect("valid options");
4858
4859 assert_eq!(manual_vhtlc.address(), expected);
4861 }
4862}