1use std::str::FromStr;
2use std::sync::Arc;
3
4use bitcoin::hashes::{sha256, Hash};
5use bitcoin::hex::FromHex;
6use bitcoin::key::Secp256k1;
7use bitcoin::secp256k1::Keypair;
8use bitcoin::{consensus, Amount, Transaction as BtcTransaction};
9use elements::{confidential, Transaction as LbtcTransaction};
10use lightning_invoice::Bolt11Invoice;
11use secp256k1_musig::musig;
12use serde_json::Value;
13
14use super::boltz::{
15 BoltzApiClientV2, ChainSwapDetails, Cooperative, CreateReverseResponse,
16 CreateSubmarineResponse, Side, SwapTxKind, SwapType,
17};
18use crate::boltz::TransactionInfo;
19use crate::error::Error;
20use crate::network::{BitcoinClient, Chain, LiquidChain, LiquidClient, Network};
21use crate::swaps::bitcoin::{BtcSwapScript, BtcSwapTx};
22use crate::swaps::fees::estimate_claim_fee;
23use crate::swaps::liquid::{LBtcSwapScript, LBtcSwapTx};
24use crate::util::fees::Fee;
25use crate::util::secrets::Preimage;
26
27#[derive(Clone, Debug)]
28struct ChainClaim {
29 refund_keys: Keypair,
30 lockup_script: SwapScript,
31}
32
33#[derive(Clone, Debug, Default)]
34pub struct DirectTxOptions {
35 blinding_key: Option<bitcoin::secp256k1::SecretKey>,
36}
37
38impl DirectTxOptions {
39 pub fn new() -> Self {
40 Self { blinding_key: None }
41 }
42
43 pub fn with_blinding_key(mut self, blinding_key: bitcoin::secp256k1::SecretKey) -> Self {
44 self.blinding_key = Some(blinding_key);
45 self
46 }
47}
48
49#[derive(Clone, Debug)]
50pub struct TransactionOptions {
51 cooperative: bool,
52 chain_claim: Option<ChainClaim>,
53 lockup_tx: Option<BtcLikeTransaction>,
54}
55
56impl Default for TransactionOptions {
57 fn default() -> Self {
58 Self {
59 cooperative: true,
60 chain_claim: None,
61 lockup_tx: None,
62 }
63 }
64}
65
66impl TransactionOptions {
67 pub fn with_cooperative(mut self, cooperative: bool) -> Self {
69 self.cooperative = cooperative;
70 self
71 }
72
73 pub fn with_chain_claim(mut self, refund_keys: Keypair, lockup_script: SwapScript) -> Self {
76 self.cooperative = true;
77 self.chain_claim = Some(ChainClaim {
78 refund_keys,
79 lockup_script,
80 });
81 self
82 }
83
84 pub fn with_lockup_tx(mut self, lockup_tx: BtcLikeTransaction) -> Self {
85 self.lockup_tx = Some(lockup_tx);
86 self
87 }
88}
89
90#[derive(Clone, Debug)]
92pub enum BtcLikeTransaction {
93 Bitcoin(BtcTransaction),
94 Liquid(LbtcTransaction),
95}
96
97impl BtcLikeTransaction {
98 pub fn from_hex(chain: Chain, hex: &str) -> Result<Self, Error> {
99 match chain {
100 Chain::Bitcoin(_) => Self::from_hex_bitcoin(hex),
101 Chain::Liquid(_) => Self::from_hex_liquid(hex),
102 }
103 }
104
105 pub fn from_hex_bitcoin(hex: &str) -> Result<Self, Error> {
106 let decoded = hex::decode(hex)?;
107 Ok(Self::bitcoin(consensus::deserialize(&decoded)?))
108 }
109
110 pub fn from_hex_liquid(hex: &str) -> Result<Self, Error> {
111 let decoded = hex::decode(hex)?;
112 Ok(Self::liquid(elements::encode::deserialize(&decoded)?))
113 }
114
115 pub fn bitcoin(tx: BtcTransaction) -> Self {
116 Self::Bitcoin(tx)
117 }
118
119 pub fn liquid(tx: LbtcTransaction) -> Self {
120 Self::Liquid(tx)
121 }
122
123 pub fn as_bitcoin(&self) -> Option<&BtcTransaction> {
124 match self {
125 Self::Bitcoin(tx) => Some(tx),
126 Self::Liquid(_) => None,
127 }
128 }
129
130 pub fn as_liquid(&self) -> Option<&LbtcTransaction> {
131 match self {
132 Self::Bitcoin(_) => None,
133 Self::Liquid(tx) => Some(tx),
134 }
135 }
136}
137
138pub struct ChainClient {
140 bitcoin: Option<Box<dyn BitcoinClient>>,
141 liquid: Option<Box<dyn LiquidClient>>,
142}
143
144impl Default for ChainClient {
145 fn default() -> Self {
146 Self::new()
147 }
148}
149
150impl ChainClient {
151 pub fn new() -> Self {
152 Self {
153 bitcoin: None,
154 liquid: None,
155 }
156 }
157
158 pub fn with_bitcoin(mut self, client: impl BitcoinClient + 'static) -> Self {
159 self.bitcoin = Some(Box::new(client));
160 self
161 }
162
163 pub fn with_liquid(mut self, client: impl LiquidClient + 'static) -> Self {
164 self.liquid = Some(Box::new(client));
165 self
166 }
167
168 pub fn bitcoin_client(&self) -> Option<&dyn BitcoinClient> {
169 self.bitcoin.as_deref()
170 }
171
172 pub fn liquid_client(&self) -> Option<&dyn LiquidClient> {
173 self.liquid.as_deref()
174 }
175
176 fn require_bitcoin_client(&self) -> Result<&dyn BitcoinClient, Error> {
177 self.bitcoin_client()
178 .ok_or_else(|| Error::Generic("Expected Bitcoin client".to_string()))
179 }
180
181 fn require_liquid_client(&self) -> Result<&dyn LiquidClient, Error> {
182 self.liquid_client()
183 .ok_or_else(|| Error::Generic("Expected Liquid client".to_string()))
184 }
185
186 pub async fn broadcast_tx(&self, tx: &BtcLikeTransaction) -> Result<String, Error> {
187 match tx {
188 BtcLikeTransaction::Bitcoin(tx) => {
189 let id = self.require_bitcoin_client()?.broadcast_tx(tx).await?;
190 Ok(id.to_string())
191 }
192 BtcLikeTransaction::Liquid(tx) => {
193 let id = self.require_liquid_client()?.broadcast_tx(tx).await?;
194 Ok(id)
195 }
196 }
197 }
198
199 pub async fn try_broadcast_tx(&self, tx: &BtcLikeTransaction) -> Result<(), Error> {
200 match self.broadcast_tx(tx).await {
201 Ok(_) => Ok(()),
202 Err(e) => {
203 if e.message().contains("already in block chain")
204 || e.message().contains("already in utxo set")
205 {
206 Ok(())
207 } else {
208 Err(e)
209 }
210 }
211 }
212 }
213}
214
215pub trait SwapScriptCommon {
217 fn swap_type(&self) -> SwapType;
218
219 fn partial_sign(
220 &self,
221 keys: &Keypair,
222 pub_nonce: &str,
223 transaction_hash: &str,
224 ) -> Result<(musig::PartialSignature, musig::PublicNonce), Error>;
225}
226
227#[derive(Clone, Debug)]
229pub enum SwapScriptImpl {
230 Bitcoin(Arc<BtcSwapScript>),
231 Liquid(Arc<LBtcSwapScript>),
232}
233
234#[derive(Clone, Debug)]
235pub struct SwapScript {
236 script: SwapScriptImpl,
237 boltz_lockup: Option<Amount>,
238 mrh_amount: Option<Amount>,
239}
240
241#[derive(Clone)]
242pub struct SwapTransactionParams<'a> {
243 pub keys: Keypair,
244 pub output_address: String,
245 pub fee: Fee,
246 pub swap_id: String,
247 pub chain_client: &'a ChainClient,
248 pub boltz_client: &'a BoltzApiClientV2,
249 pub options: Option<TransactionOptions>,
250}
251
252impl SwapScriptImpl {
253 pub fn bitcoin(script: BtcSwapScript) -> Self {
254 Self::Bitcoin(Arc::new(script))
255 }
256
257 pub fn liquid(script: LBtcSwapScript) -> Self {
258 Self::Liquid(Arc::new(script))
259 }
260
261 pub fn common(&self) -> &dyn SwapScriptCommon {
262 match self {
263 Self::Bitcoin(script) => script.as_ref(),
264 Self::Liquid(script) => script.as_ref(),
265 }
266 }
267}
268
269impl SwapScript {
270 fn new(
271 script: SwapScriptImpl,
272 boltz_lockup: Option<Amount>,
273 mrh_amount: Option<Amount>,
274 ) -> Self {
275 Self {
276 script,
277 boltz_lockup,
278 mrh_amount,
279 }
280 }
281
282 pub fn submarine_from_swap_resp(
283 chain: Chain,
284 create_swap_response: &CreateSubmarineResponse,
285 our_pubkey: bitcoin::PublicKey,
286 ) -> Result<Self, Error> {
287 let script: Result<SwapScriptImpl, Error> = match chain {
288 Chain::Bitcoin(_) => {
289 let script =
290 BtcSwapScript::submarine_from_swap_resp(create_swap_response, our_pubkey)?;
291 Ok(SwapScriptImpl::bitcoin(script))
292 }
293 Chain::Liquid(_) => {
294 let script =
295 LBtcSwapScript::submarine_from_swap_resp(create_swap_response, our_pubkey)?;
296 Ok(SwapScriptImpl::liquid(script))
297 }
298 };
299 Ok(Self::new(script?, None, None))
301 }
302
303 pub fn reverse_from_swap_resp(
304 chain: Chain,
305 reverse_response: &CreateReverseResponse,
306 our_pubkey: bitcoin::PublicKey,
307 ) -> Result<Self, Error> {
308 let script: Result<SwapScriptImpl, Error> = match chain {
309 Chain::Bitcoin(_) => {
310 let script = BtcSwapScript::reverse_from_swap_resp(reverse_response, our_pubkey)?;
311 Ok(SwapScriptImpl::bitcoin(script))
312 }
313 Chain::Liquid(_) => {
314 let script = LBtcSwapScript::reverse_from_swap_resp(reverse_response, our_pubkey)?;
315 Ok(SwapScriptImpl::liquid(script))
316 }
317 };
318
319 let boltz_lockup = Amount::from_sat(reverse_response.onchain_amount);
320 let mrh_amount = match chain {
321 Chain::Bitcoin(_) => None,
322 Chain::Liquid(_) => Some(boltz_lockup - estimate_claim_fee(chain, 0.1)),
323 };
324 Ok(Self::new(script?, Some(boltz_lockup), mrh_amount))
325 }
326
327 pub fn chain_from_swap_resp(
328 chain: Chain,
329 side: Side,
330 chain_swap_details: ChainSwapDetails,
331 our_pubkey: bitcoin::PublicKey,
332 ) -> Result<Self, Error> {
333 let amount = chain_swap_details.amount;
334 let script: Result<SwapScriptImpl, Error> = match chain {
335 Chain::Bitcoin(_) => {
336 let script =
337 BtcSwapScript::chain_from_swap_resp(side, chain_swap_details, our_pubkey)?;
338 Ok(SwapScriptImpl::bitcoin(script))
339 }
340 Chain::Liquid(_) => {
341 let script =
342 LBtcSwapScript::chain_from_swap_resp(side, chain_swap_details, our_pubkey)?;
343 Ok(SwapScriptImpl::liquid(script))
344 }
345 };
346 Ok(Self::new(
347 script?,
348 if amount > 0 {
349 Some(Amount::from_sat(amount))
350 } else {
351 None
352 },
353 None,
354 ))
355 }
356
357 pub async fn submarine_cooperative_claim(
364 &self,
365 swap_id: &String,
366 keys: &Keypair,
367 invoice: &str,
368 boltz_api: &BoltzApiClientV2,
369 ) -> Result<Value, Error> {
370 if self.script.common().swap_type() != SwapType::Submarine {
371 return Err(Error::Generic(
372 "can only be called for submarine swaps".to_string(),
373 ));
374 }
375 let claim_tx_response = boltz_api.get_submarine_claim_tx_details(swap_id).await?;
377
378 log::debug!("Received claim tx details : {claim_tx_response:?}");
379
380 let preimage = Vec::from_hex(&claim_tx_response.preimage)?;
381
382 let preimage_hash = sha256::Hash::hash(&preimage);
384 let invoice = Bolt11Invoice::from_str(invoice)?;
385 let invoice_payment_hash = invoice.payment_hash();
386 if invoice_payment_hash.to_string() != preimage_hash.to_string() {
387 return Err(Error::Protocol(
388 "Preimage does not match invoice payment hash".to_string(),
389 ));
390 }
391
392 let (partial_sig, pub_nonce) = self.script.common().partial_sign(
394 keys,
395 &claim_tx_response.pub_nonce.to_string(),
396 &claim_tx_response.transaction_hash.to_string(),
397 )?;
398
399 boltz_api
400 .post_submarine_claim_tx_details(swap_id, pub_nonce, partial_sig)
401 .await
402 }
403
404 pub async fn cooperative_chain_claim<'a>(
410 &self,
411 our_refund_keys: &Keypair,
412 swap_id: &String,
413 boltz_api: &'a BoltzApiClientV2,
414 ) -> Result<Cooperative<'a>, Error> {
415 let signature: Option<(musig::PartialSignature, musig::PublicNonce)> = match boltz_api
416 .get_chain_claim_tx_details(swap_id)
417 .await
418 {
419 Ok(claim_tx_response) => {
420 if let Some(claim_tx_response) = claim_tx_response {
421 Some(self.script.common().partial_sign(
422 our_refund_keys,
423 &claim_tx_response.pub_nonce,
424 &claim_tx_response.transaction_hash,
425 )?)
426 } else {
427 None
428 }
429 }
430 Err(Error::JSON(e)) => {
431 log::warn!("Failed to parse chain claim tx details: {e} - continuing without signature as we may have already sent it");
432 None
433 }
434 Err(e) => {
435 return Err(e);
436 }
437 };
438
439 Ok(Cooperative {
440 boltz_api,
441 swap_id: swap_id.clone(),
442 signature,
443 })
444 }
445
446 async fn get_cooperative<'a>(
447 &self,
448 tx_kind: SwapTxKind,
449 options: Option<TransactionOptions>,
450 boltz_client: &'a BoltzApiClientV2,
451 swap_id: String,
452 ) -> Result<Option<Cooperative<'a>>, Error> {
453 let o = options.unwrap_or_default();
454 match o.cooperative {
455 true => match (self.script.common().swap_type(), tx_kind) {
456 (SwapType::Chain, SwapTxKind::Claim) => {
457 let claim = o.chain_claim.ok_or(Error::Generic(
458 "Chain claim options are missing".to_string(),
459 ))?;
460 claim
461 .lockup_script
462 .cooperative_chain_claim(&claim.refund_keys, &swap_id, boltz_client)
463 .await
464 .map(Option::Some)
465 }
466 _ => Ok(Some(Cooperative {
467 boltz_api: boltz_client,
468 swap_id,
469 signature: None,
470 })),
471 },
472 false => Ok(None),
473 }
474 }
475
476 fn check_direct_transaction_inner(
477 mrh_amount: Amount,
478 network: Network,
479 direct_tx: &BtcLikeTransaction,
480 claim_address: &str,
481 options: DirectTxOptions,
482 ) -> Result<(), Error> {
483 let amount = match direct_tx {
484 BtcLikeTransaction::Bitcoin(_) => {
485 Err(Error::Generic("Not implemented for mainchain".to_string()))
486 }
487 BtcLikeTransaction::Liquid(tx) => {
488 let chain = LiquidChain::from(network);
489 let address = elements::Address::parse_with_params(claim_address, chain.into())?;
490 let script_pubkey = address.script_pubkey();
491 let (_, utxo) = super::liquid::find_utxo(tx, &script_pubkey)
492 .ok_or(Error::Generic("No UTXO found for this script".to_string()))?;
493 let secp = Secp256k1::new();
494 let (value, asset) = match (utxo.value, utxo.asset) {
495 (
496 confidential::Value::Explicit(value),
497 confidential::Asset::Explicit(asset),
498 ) => (value, asset),
499 (
500 confidential::Value::Confidential(_),
501 confidential::Asset::Confidential(_),
502 ) => {
503 let secrets = utxo.unblind(
504 &secp,
505 options.blinding_key.ok_or(Error::Generic(
506 "Blinding key is required for confidential UTXO".to_string(),
507 ))?,
508 )?;
509 (secrets.value, secrets.asset)
510 }
511 (_, _) => {
512 return Err(Error::Generic("Inconsistent blinding".to_string()));
513 }
514 };
515 if asset != chain.bitcoin() {
516 return Err(Error::Protocol(format!("Asset is not bitcoin: {asset}")));
517 }
518 Ok(Amount::from_sat(value))
519 }
520 }?;
521
522 if amount < mrh_amount {
523 return Err(Error::Protocol(format!(
524 "Amount received via direct transaction is less than expected: {amount} < {mrh_amount}",
525 )));
526 }
527 Ok(())
528 }
529
530 pub async fn check_direct_transaction(
531 &self,
532 chain_client: &ChainClient,
533 network: Network,
534 direct_tx: &BtcLikeTransaction,
535 claim_address: &str,
536 options: DirectTxOptions,
537 ) -> Result<(), Error> {
538 chain_client.try_broadcast_tx(direct_tx).await?;
539 if let Some(mrh_amount) = self.mrh_amount {
540 return SwapScript::check_direct_transaction_inner(
541 mrh_amount,
542 network,
543 direct_tx,
544 claim_address,
545 options,
546 );
547 }
548 Ok(())
549 }
550
551 pub async fn parse_lockup_transaction(
552 &self,
553 lockup_info: &TransactionInfo,
554 ) -> Result<BtcLikeTransaction, Error> {
555 let hex = lockup_info
556 .hex
557 .as_ref()
558 .ok_or(Error::Generic("Lockup info is missing".to_string()))?;
559 match self.script.clone() {
560 SwapScriptImpl::Bitcoin(_) => BtcLikeTransaction::from_hex_bitcoin(hex),
561 SwapScriptImpl::Liquid(_) => BtcLikeTransaction::from_hex_liquid(hex),
562 }
563 }
564
565 fn validate_lockup_amount(&self, amount: Amount) -> Result<(), Error> {
566 if let Some(boltz_lockup) = self.boltz_lockup {
567 if amount != boltz_lockup {
568 return Err(Error::Protocol(format!(
569 "Lockup amount mismatch: {amount} != {boltz_lockup}",
570 )));
571 }
572 }
573 Ok(())
574 }
575
576 pub async fn construct_claim(
577 &self,
578 preimage: &Preimage,
579 params: SwapTransactionParams<'_>,
580 ) -> Result<BtcLikeTransaction, Error> {
581 let cooperative = self
582 .get_cooperative(
583 SwapTxKind::Claim,
584 params.options.clone(),
585 params.boltz_client,
586 params.swap_id.clone(),
587 )
588 .await?;
589 let lockup_tx = params.options.clone().and_then(|o| o.lockup_tx);
590 if let Some(lockup_tx) = lockup_tx.clone() {
591 params.chain_client.try_broadcast_tx(&lockup_tx).await?;
592 }
593 match self.script.clone() {
594 SwapScriptImpl::Bitcoin(script) => {
595 let chain_client = params.chain_client.require_bitcoin_client()?;
596
597 let utxo = script
598 .fetch_swap_utxo(
599 lockup_tx
600 .as_ref()
601 .map(|tx| {
602 tx.as_bitcoin().ok_or(Error::Generic(
603 "Lockup transaction is not a Bitcoin transaction".to_string(),
604 ))
605 })
606 .transpose()?,
607 chain_client,
608 params.boltz_client,
609 ¶ms.swap_id,
610 SwapTxKind::Claim,
611 )
612 .await?;
613
614 self.validate_lockup_amount(utxo.1.value)?;
615
616 let tx = BtcSwapTx::new_claim_with_utxo(
617 script.as_ref().clone(),
618 params.output_address.clone(),
619 chain_client,
620 utxo,
621 )?;
622
623 tx.sign_claim(¶ms.keys, preimage, params.fee, cooperative)
624 .await
625 .map(BtcLikeTransaction::bitcoin)
626 }
627 SwapScriptImpl::Liquid(script) => {
628 let chain_client = params.chain_client.require_liquid_client()?;
629
630 let utxo = script
631 .fetch_swap_utxo(
632 lockup_tx
633 .as_ref()
634 .map(|tx| {
635 tx.as_liquid().ok_or(Error::Generic(
636 "Lockup transaction is not a Liquid transaction".to_string(),
637 ))
638 })
639 .transpose()?,
640 chain_client,
641 params.boltz_client,
642 ¶ms.swap_id,
643 SwapTxKind::Claim,
644 )
645 .await?;
646
647 if self.boltz_lockup.is_some() {
648 let secrets = super::liquid::unblind_utxo(
649 chain_client.network(),
650 utxo.1.clone(),
651 script.blinding_key.secret_key(),
652 )?;
653 self.validate_lockup_amount(Amount::from_sat(secrets.value))?;
654 }
655
656 let tx = LBtcSwapTx::new_claim_with_utxo(
657 script.as_ref().clone(),
658 params.output_address.clone(),
659 chain_client,
660 utxo,
661 )
662 .await?;
663
664 tx.sign_claim(¶ms.keys, preimage, params.fee, cooperative, true)
665 .await
666 .map(BtcLikeTransaction::liquid)
667 }
668 }
669 }
670
671 pub async fn construct_refund(
672 &self,
673 params: SwapTransactionParams<'_>,
674 ) -> Result<BtcLikeTransaction, Error> {
675 let cooperative = self
676 .get_cooperative(
677 SwapTxKind::Refund,
678 params.options,
679 params.boltz_client,
680 params.swap_id.clone(),
681 )
682 .await?;
683
684 match self.script.clone() {
685 SwapScriptImpl::Bitcoin(script) => {
686 let tx = BtcSwapTx::new_refund(
687 script.as_ref().clone(),
688 ¶ms.output_address,
689 params.chain_client.require_bitcoin_client()?,
690 params.boltz_client,
691 params.swap_id.clone(),
692 )
693 .await?;
694 tx.sign_refund(¶ms.keys, params.fee, cooperative)
695 .await
696 .map(BtcLikeTransaction::bitcoin)
697 }
698 SwapScriptImpl::Liquid(script) => {
699 let tx = LBtcSwapTx::new_refund(
700 script.as_ref().clone(),
701 ¶ms.output_address,
702 params.chain_client.require_liquid_client()?,
703 params.boltz_client,
704 params.swap_id.clone(),
705 )
706 .await?;
707 tx.sign_refund(¶ms.keys, params.fee, cooperative, true)
708 .await
709 .map(BtcLikeTransaction::liquid)
710 }
711 }
712 }
713}
714
715#[cfg(test)]
716mod tests {
717 use super::*;
718 use elements::{
719 confidential::Value, OutPoint, Script, Sequence, Transaction, TxIn, TxInWitness, TxOut,
720 TxOutWitness,
721 };
722
723 fn generate_regtest_address() -> String {
725 use bitcoin::key::rand::thread_rng;
726 let network = Network::Regtest;
727 let chain = LiquidChain::from(network);
728 let secp = bitcoin::secp256k1::Secp256k1::new();
729 let keypair = bitcoin::secp256k1::Keypair::new(&secp, &mut thread_rng());
730
731 let addr = elements::Address::p2wpkh(
732 &elements::bitcoin::PublicKey::new(keypair.public_key()),
733 None,
734 chain.into(),
735 );
736
737 addr.to_string()
738 }
739
740 fn create_liquid_tx_explicit(address: &str, amount: u64) -> BtcLikeTransaction {
742 let network = Network::Regtest;
743 let chain = LiquidChain::from(network);
744 let addr = elements::Address::parse_with_params(address, chain.into()).unwrap();
745 let script_pubkey = addr.script_pubkey();
746
747 let asset = chain.bitcoin();
749
750 let tx = Transaction {
751 version: 2,
752 lock_time: elements::LockTime::ZERO,
753 input: vec![TxIn {
754 previous_output: OutPoint::default(),
755 is_pegin: false,
756 script_sig: Script::new(),
757 sequence: Sequence::MAX,
758 asset_issuance: elements::AssetIssuance::default(),
759 witness: TxInWitness::default(),
760 }],
761 output: vec![
762 TxOut {
763 asset: confidential::Asset::Explicit(asset),
764 value: Value::Explicit(amount),
765 nonce: confidential::Nonce::Null,
766 script_pubkey: script_pubkey.clone(),
767 witness: TxOutWitness::default(),
768 },
769 TxOut {
771 asset: confidential::Asset::Explicit(asset),
772 value: Value::Explicit(1000),
773 nonce: confidential::Nonce::Null,
774 script_pubkey: Script::new(),
775 witness: TxOutWitness::default(),
776 },
777 ],
778 };
779
780 BtcLikeTransaction::Liquid(tx)
781 }
782
783 #[test]
784 fn test_check_direct_transaction_bitcoin_not_implemented() {
785 let btc_tx = BtcLikeTransaction::Bitcoin(bitcoin::Transaction {
786 version: bitcoin::transaction::Version::TWO,
787 lock_time: bitcoin::absolute::LockTime::ZERO,
788 input: vec![],
789 output: vec![],
790 });
791
792 let result = SwapScript::check_direct_transaction_inner(
793 Amount::from_sat(100_000),
794 Network::Regtest,
795 &btc_tx,
796 "bcrt1qw508d6qejxtdg4y5r3zarvary0c5xw7kygt080",
797 DirectTxOptions::new(),
798 );
799
800 assert!(result.is_err());
801 assert!(result
802 .unwrap_err()
803 .to_string()
804 .contains("Not implemented for mainchain"));
805 }
806
807 #[test]
808 fn test_check_direct_transaction_liquid_explicit_success() {
809 let address = generate_regtest_address();
810 let amount = 100_000;
811
812 let liquid_tx = create_liquid_tx_explicit(&address, amount);
813
814 let result = SwapScript::check_direct_transaction_inner(
815 Amount::from_sat(amount),
816 Network::Regtest,
817 &liquid_tx,
818 &address,
819 DirectTxOptions::new(),
820 );
821
822 assert!(result.is_ok());
823 }
824
825 #[test]
826 fn test_check_direct_transaction_liquid_wrong_asset() {
827 let address = generate_regtest_address();
828 let amount = 100_000;
829
830 let network = Network::Regtest;
832 let chain = LiquidChain::from(network);
833 let addr = elements::Address::parse_with_params(&address, chain.into()).unwrap();
834 let script_pubkey = addr.script_pubkey();
835
836 let wrong_asset = elements::AssetId::from_str(
838 "0000000000000000000000000000000000000000000000000000000000000001",
839 )
840 .unwrap();
841
842 let tx = Transaction {
843 version: 2,
844 lock_time: elements::LockTime::ZERO,
845 input: vec![TxIn {
846 previous_output: OutPoint::default(),
847 is_pegin: false,
848 script_sig: Script::new(),
849 sequence: Sequence::MAX,
850 asset_issuance: elements::AssetIssuance::default(),
851 witness: TxInWitness::default(),
852 }],
853 output: vec![
854 TxOut {
855 asset: confidential::Asset::Explicit(wrong_asset),
856 value: Value::Explicit(amount),
857 nonce: confidential::Nonce::Null,
858 script_pubkey: script_pubkey.clone(),
859 witness: TxOutWitness::default(),
860 },
861 TxOut {
863 asset: confidential::Asset::Explicit(wrong_asset),
864 value: Value::Explicit(1000),
865 nonce: confidential::Nonce::Null,
866 script_pubkey: Script::new(),
867 witness: TxOutWitness::default(),
868 },
869 ],
870 };
871
872 let liquid_tx = BtcLikeTransaction::Liquid(tx);
873
874 let result = SwapScript::check_direct_transaction_inner(
875 Amount::from_sat(amount),
876 Network::Regtest,
877 &liquid_tx,
878 &address,
879 DirectTxOptions::new(),
880 );
881
882 assert!(result.is_err());
883 let err_msg = result.unwrap_err().to_string();
884 assert!(err_msg.contains("asset") || err_msg.contains("Asset"));
885 }
886
887 #[test]
888 fn test_check_direct_transaction_liquid_no_utxo_found() {
889 let tx_address = generate_regtest_address();
890 let different_address = generate_regtest_address();
891
892 let liquid_tx = create_liquid_tx_explicit(&tx_address, 100_000);
893
894 let result = SwapScript::check_direct_transaction_inner(
895 Amount::from_sat(100_000),
896 Network::Regtest,
897 &liquid_tx,
898 &different_address,
899 DirectTxOptions::new(),
900 );
901
902 assert!(result.is_err());
903 assert!(result
904 .unwrap_err()
905 .to_string()
906 .contains("No UTXO found for this script"));
907 }
908
909 #[test]
910 fn test_check_direct_transaction_amount_validation_less_than_expected() {
911 let expected_amount = Amount::from_sat(100_000);
912 let address = generate_regtest_address();
913 let actual_amount = 50_000; let liquid_tx = create_liquid_tx_explicit(&address, actual_amount);
916
917 let result = SwapScript::check_direct_transaction_inner(
918 expected_amount,
919 Network::Regtest,
920 &liquid_tx,
921 &address,
922 DirectTxOptions::new(),
923 );
924
925 assert!(result.is_err());
926 let err_msg = result.unwrap_err().to_string();
927 assert!(err_msg.contains("Amount received via direct transaction is less than expected"));
928 }
929
930 #[test]
931 fn test_check_direct_transaction_amount_validation_equal() {
932 let expected_amount = Amount::from_sat(100_000);
933 let address = generate_regtest_address();
934
935 let liquid_tx = create_liquid_tx_explicit(&address, expected_amount.to_sat());
936
937 let result = SwapScript::check_direct_transaction_inner(
938 expected_amount,
939 Network::Regtest,
940 &liquid_tx,
941 &address,
942 DirectTxOptions::new(),
943 );
944
945 assert!(result.is_ok());
946 }
947
948 #[test]
949 fn test_check_direct_transaction_amount_validation_greater() {
950 let expected_amount = Amount::from_sat(100_000);
951 let address = generate_regtest_address();
952 let actual_amount = 150_000; let liquid_tx = create_liquid_tx_explicit(&address, actual_amount);
955
956 let result = SwapScript::check_direct_transaction_inner(
957 expected_amount,
958 Network::Regtest,
959 &liquid_tx,
960 &address,
961 DirectTxOptions::new(),
962 );
963
964 assert!(result.is_ok());
965 }
966}