boltz_client/swaps/
wrappers.rs

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    /// Whether a cooperative claim with boltz should be attempted
68    pub fn with_cooperative(mut self, cooperative: bool) -> Self {
69        self.cooperative = cooperative;
70        self
71    }
72
73    /// For a cooperative claim of a chain swap, the refund keys and lockup script of the swap have to be provided
74    /// Calling this function will implicitly set cooperative to true
75    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/// A wrapper for transactions that can be either Bitcoin or Liquid
91#[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
138/// A wrapper for blockchain clients that can be either Bitcoin or Liquid
139pub 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
215/// Trait for common functionality between Bitcoin and Liquid swap transactions
216pub 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/// A wrapper for swap scripts that can be either Bitcoin or Liquid
228#[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        // we dont have to validate our own lockup amounts
300        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    /// Cooperatively claim a submarine swap with Boltz.
358    ///
359    /// This function should be called when the swap status is `transaction.claim.pending`, indicating
360    /// that Boltz has detected the on-chain funding transaction and has paid the invoice.
361    /// The function will verify that boltz indeed has paid the given `invoice` by checking the returned preimage
362    /// before sending the partial signature for the claim transaction of boltz.
363    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        // Get claim tx details from Boltz
376        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        // Verify preimage matches invoice payment hash
383        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        // Generate partial signature
393        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    // Initiates a cooperative claim for a chain swap with Boltz.
405    //
406    // This function should be called when the swap status is `transaction.server.confirmed`,
407    // It creates a partial signature for boltz's side of the transaction, and returns a Cooperative struct which
408    // can be passed to `sign_claim` where it is used in exchange for the signature for our own claim transaction.
409    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                        &params.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(&params.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                        &params.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(&params.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                    &params.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(&params.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                    &params.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(&params.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    // Helper function to generate a regtest address
724    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    // Helper function to create a Liquid transaction with explicit value
741    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        // Use the regtest LBTC asset
748        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                // Add a fee output
770                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        // Create a transaction with the wrong asset
831        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        // Use a wrong asset ID (just some random asset, not the regtest LBTC)
837        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                // Add a fee output
862                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; // Less than expected
914
915        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; // Greater than expected
953
954        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}