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