Skip to main content

smplx_sdk/signer/
core.rs

1use std::collections::{HashMap, HashSet};
2use std::str::FromStr;
3use std::sync::Arc;
4
5use simplicityhl::Value;
6use simplicityhl::WitnessValues;
7use simplicityhl::elements::pset::PartiallySignedTransaction;
8use simplicityhl::elements::secp256k1_zkp::{All, Keypair, Message, Secp256k1, ecdsa, schnorr};
9use simplicityhl::elements::{Address, AssetId, OutPoint, Script, Transaction, Txid};
10use simplicityhl::simplicity::bitcoin::XOnlyPublicKey;
11use simplicityhl::simplicity::hashes::Hash;
12use simplicityhl::str::WitnessName;
13use simplicityhl::value::ValueConstructible;
14
15use bip39::Mnemonic;
16use bip39::rand::thread_rng;
17
18use elements_miniscript::{
19    ConfidentialDescriptor, Descriptor, DescriptorPublicKey,
20    bitcoin::{NetworkKind, PrivateKey, PublicKey, bip32::DerivationPath},
21    elements::{
22        EcdsaSighashType,
23        bitcoin::bip32::{Fingerprint, Xpriv, Xpub},
24        sighash::SighashCache,
25    },
26    elementssig_to_rawsig,
27    psbt::PsbtExt,
28    slip77::MasterBlindingKey,
29};
30
31use crate::constants::MIN_FEE;
32use crate::program::ProgramTrait;
33use crate::provider::ProviderTrait;
34use crate::provider::SimplicityNetwork;
35use crate::signer::wtns_injector::WtnsInjector;
36use crate::transaction::{FinalTransaction, PartialInput, PartialOutput, RequiredSignature, TxReceipt, UTXO};
37
38use super::error::SignerError;
39
40/// A placeholder dummy fee amount used during transaction estimation.
41pub const PLACEHOLDER_FEE: u64 = 1;
42
43/// Common signing interface spanning over standard explicit inputs and Simplicity programs.
44pub trait SignerTrait {
45    /// Generates a Schnorr signature to satisfy a target Simplicity program input.
46    ///
47    /// # Errors
48    /// Returns a `SignerError` if the elements environment fails to build or if the message digest fails to construct.
49    fn sign_program(
50        &self,
51        pst: &PartiallySignedTransaction,
52        program: &dyn ProgramTrait,
53        input_index: usize,
54        network: &SimplicityNetwork,
55    ) -> Result<schnorr::Signature, SignerError>;
56
57    /// Generates an ECDSA signature to spend a standard transaction input.
58    ///
59    /// # Errors
60    /// Returns a `SignerError` if the transaction formatting or sighash msg extraction fails.
61    fn sign_input(
62        &self,
63        pst: &PartiallySignedTransaction,
64        input_index: usize,
65    ) -> Result<(PublicKey, ecdsa::Signature), SignerError>;
66}
67
68/// Core interface responsible for managing keys, interfacing with the blockchain provider,
69/// assembling descriptors, estimating fees, and finalizing/signing transactions.
70pub struct Signer {
71    mnemonic: Mnemonic,
72    xprv: Xpriv,
73    provider: Box<dyn ProviderTrait>,
74    network: SimplicityNetwork,
75    secp: Secp256k1<All>,
76}
77
78impl SignerTrait for Signer {
79    fn sign_program(
80        &self,
81        pst: &PartiallySignedTransaction,
82        program: &dyn ProgramTrait,
83        input_index: usize,
84        network: &SimplicityNetwork,
85    ) -> Result<schnorr::Signature, SignerError> {
86        let env = program.get_env(pst, input_index, network)?;
87        let msg = Message::from_digest(env.c_tx_env().sighash_all().to_byte_array());
88
89        let private_key = self.get_private_key();
90        let keypair = Keypair::from_secret_key(&self.secp, &private_key.inner);
91
92        Ok(self.secp.sign_schnorr(&msg, &keypair))
93    }
94
95    fn sign_input(
96        &self,
97        pst: &PartiallySignedTransaction,
98        input_index: usize,
99    ) -> Result<(PublicKey, ecdsa::Signature), SignerError> {
100        let tx = pst.extract_tx()?;
101
102        let mut sighash_cache = SighashCache::new(&tx);
103        let genesis_hash = elements_miniscript::elements::BlockHash::all_zeros();
104
105        let message = pst
106            .sighash_msg(input_index, &mut sighash_cache, None, genesis_hash)?
107            .to_secp_msg();
108
109        let private_key = self.get_private_key();
110        let public_key = private_key.public_key(&self.secp);
111
112        let signature = self.secp.sign_ecdsa_low_r(&message, &private_key.inner);
113
114        Ok((public_key, signature))
115    }
116}
117
118enum Estimate {
119    Success(Transaction, u64),
120    Failure(u64),
121}
122
123impl Signer {
124    /// Creates a new `Signer` instance seeded from the provided mnemonic and paired with the specified provider.
125    ///
126    /// # Panics
127    /// Panics if the mnemonic fails to parse, or if deriving the master private key fails.
128    #[must_use]
129    pub fn new(mnemonic: &str, provider: Box<dyn ProviderTrait>) -> Self {
130        let secp = Secp256k1::new();
131        let mnemonic: Mnemonic = mnemonic
132            .parse()
133            .map_err(|e: bip39::Error| SignerError::Mnemonic(e.to_string()))
134            .unwrap();
135        let seed = mnemonic.to_seed("");
136        let xprv = Xpriv::new_master(NetworkKind::Test, &seed).unwrap();
137
138        let network = *provider.get_network();
139
140        Self {
141            mnemonic,
142            xprv,
143            provider,
144            network,
145            secp,
146        }
147    }
148
149    /// Composes, funds, and broadcasts a standard network transaction sending the specified value of the primary policy asset.
150    ///
151    /// # Errors
152    /// Returns a `SignerError` if compiling the inputs fails, there are insufficient funds/fees, or broadcast is rejected.
153    // TODO: add an ability to send arbitrary assets
154    pub fn send(&self, to: Script, amount: u64) -> Result<TxReceipt<'_>, SignerError> {
155        let mut ft = FinalTransaction::new();
156
157        ft.add_output(PartialOutput::new(to, amount, self.network.policy_asset()));
158
159        let (tx, _fee) = self.finalize(&ft)?;
160
161        Ok(self.provider.broadcast_transaction(&tx)?)
162    }
163
164    /// Evaluates, funds, and broadcasts an already assembled `FinalTransaction`.
165    ///
166    /// # Errors
167    /// Returns a `SignerError` if finalizing the payload fails or if the network rejects the broadcast.
168    pub fn broadcast(&self, tx: &FinalTransaction) -> Result<TxReceipt<'_>, SignerError> {
169        let (tx, _fee) = self.finalize(tx)?;
170
171        Ok(self.provider.broadcast_transaction(&tx)?)
172    }
173
174    /// Evaluates the input components of a `FinalTransaction`, iteratively selecting available wallet UTXOs to cover outputs and estimated fees.
175    ///
176    /// # Errors
177    /// Returns a `SignerError` if the wallet contains insufficient funds to satisfy output values and target fee rates.
178    pub fn finalize(&self, tx: &FinalTransaction) -> Result<(Transaction, u64), SignerError> {
179        let mut signer_utxos = self.get_utxos_asset(self.network.policy_asset())?;
180        let mut set = HashSet::new();
181
182        for input in tx.inputs() {
183            set.insert(OutPoint {
184                txid: input.partial_input.witness_txid,
185                vout: input.partial_input.witness_output_index,
186            });
187        }
188
189        signer_utxos.retain(|utxo| !set.contains(&utxo.outpoint));
190
191        // descending sort of both confidential and explicit utxos
192        signer_utxos.sort_by_key(|utxo| std::cmp::Reverse(utxo.amount()));
193
194        let mut fee_tx = tx.clone();
195        let mut curr_fee = MIN_FEE;
196        let fee_rate = self.provider.fetch_fee_rate(1)?;
197
198        for utxo in signer_utxos {
199            let policy_amount_delta = fee_tx.calculate_fee_delta(&self.network);
200
201            if policy_amount_delta >= curr_fee.cast_signed() {
202                match self.estimate_tx(fee_tx.clone(), fee_rate, policy_amount_delta.cast_unsigned())? {
203                    Estimate::Success(tx, fee) => return Ok((tx, fee)),
204                    Estimate::Failure(required_fee) => curr_fee = required_fee,
205                }
206            }
207
208            fee_tx.add_input(PartialInput::new(utxo), RequiredSignature::NativeEcdsa);
209        }
210
211        // need to try one more time after the loop
212        let policy_amount_delta = fee_tx.calculate_fee_delta(&self.network);
213
214        if policy_amount_delta >= curr_fee.cast_signed() {
215            match self.estimate_tx(fee_tx.clone(), fee_rate, policy_amount_delta.cast_unsigned())? {
216                Estimate::Success(tx, fee) => return Ok((tx, fee)),
217                Estimate::Failure(required_fee) => curr_fee = required_fee,
218            }
219        }
220
221        Err(SignerError::NotEnoughFunds(curr_fee))
222    }
223
224    /// Verifies and finalizes a transaction against a strict target confirmation window (in blocks).
225    /// This function also assumes that the transaction already includes the coin selection.
226    ///
227    /// # Errors
228    /// Returns a `SignerError` if the assembled inputs do not meet dust limits or fail to cover the
229    ///  dynamically estimated required fee.
230    pub fn finalize_strict(
231        &self,
232        tx: &FinalTransaction,
233        target_blocks: u32,
234    ) -> Result<(Transaction, u64), SignerError> {
235        let policy_amount_delta = tx.calculate_fee_delta(&self.network);
236
237        if policy_amount_delta < MIN_FEE.cast_signed() {
238            return Err(SignerError::DustAmount(policy_amount_delta));
239        }
240
241        let fee_rate = self.provider.fetch_fee_rate(target_blocks)?;
242
243        // policy_amount_delta will be > 0
244        match self.estimate_tx(tx.clone(), fee_rate, policy_amount_delta.cast_unsigned())? {
245            Estimate::Success(tx, fee) => Ok((tx, fee)),
246            Estimate::Failure(required_fee) => Err(SignerError::NotEnoughFeeAmount(policy_amount_delta, required_fee)),
247        }
248    }
249
250    /// Returns a reference to the active configured network provider.
251    #[must_use]
252    pub fn get_provider(&self) -> &dyn ProviderTrait {
253        self.provider.as_ref()
254    }
255
256    /// Returns the confidential elements address matching the local wallet logic.
257    ///
258    /// # Panics
259    /// Panics if the SLIP77 descriptor cannot be generated or parsed, or if address derivation fails.
260    #[must_use]
261    pub fn get_confidential_address(&self) -> Address {
262        let mut descriptor =
263            ConfidentialDescriptor::<DescriptorPublicKey>::from_str(&self.get_slip77_descriptor().unwrap())
264                .map_err(|e| SignerError::Slip77Descriptor(e.to_string()))
265                .unwrap();
266
267        // confidential descriptor doesn't support multipath
268        descriptor.descriptor = descriptor.descriptor.into_single_descriptors().unwrap()[0].clone();
269
270        descriptor
271            .at_derivation_index(1)
272            .unwrap()
273            .address(&self.secp, self.network.address_params())
274            .unwrap()
275    }
276
277    /// Returns the standard unblinded address matching the local wallet logic.
278    ///
279    /// # Panics
280    /// Panics if the WPKH descriptor cannot be generated or parsed, or if address derivation fails.
281    #[must_use]
282    pub fn get_address(&self) -> Address {
283        let descriptor = Descriptor::<DescriptorPublicKey>::from_str(&self.get_wpkh_descriptor().unwrap())
284            .map_err(|e| SignerError::WpkhDescriptor(e.to_string()))
285            .unwrap();
286
287        descriptor.into_single_descriptors().unwrap()[0]
288            .at_derivation_index(1)
289            .unwrap()
290            .address(self.network.address_params())
291            .unwrap()
292    }
293
294    /// Iterates against the network provider to select and unblind all known UTXOs.
295    ///
296    /// # Errors
297    /// Returns a `SignerError` if querying the network or unblinding operations fail.
298    pub fn get_utxos(&self) -> Result<Vec<UTXO>, SignerError> {
299        self.get_utxos_filter(&|_| true, &|_| true)
300    }
301
302    /// Finds all known UTXOs belonging to the specific `AssetId`.
303    ///
304    /// # Errors
305    /// Returns a `SignerError` if network interaction or confidential output decryption fails.
306    pub fn get_utxos_asset(&self, asset: AssetId) -> Result<Vec<UTXO>, SignerError> {
307        self.get_utxos_filter(&|utxo| utxo.asset() == asset, &|utxo| utxo.asset() == asset)
308    }
309
310    /// Finds all known UTXOs deriving from a targeted `Txid`.
311    ///
312    /// # Errors
313    /// Returns a `SignerError` if querying the network fails.
314    // TODO: can this be optimized to not populate TxOuts that are filtered out?
315    pub fn get_utxos_txid(&self, txid: Txid) -> Result<Vec<UTXO>, SignerError> {
316        self.get_utxos_filter(&|utxo| utxo.outpoint.txid == txid, &|utxo| utxo.outpoint.txid == txid)
317    }
318
319    /// Maps UTXOs retrieved from the provider through arbitrary functional filters.
320    /// Separate filtering criteria apply explicitly vs confidentially.
321    ///
322    /// # Errors
323    /// Returns a `SignerError` if retrieving remote outputs or executing confidential node unblinding throws an error.
324    pub fn get_utxos_filter(
325        &self,
326        explicit_filter: &dyn Fn(&UTXO) -> bool,
327        confidential_filter: &dyn Fn(&UTXO) -> bool,
328    ) -> Result<Vec<UTXO>, SignerError> {
329        // fetch explicit and confidential utxos
330        let mut all_utxos = self.provider.fetch_address_utxos(&self.get_confidential_address())?;
331
332        // filter out only confidential utxos and unblind them
333        let mut confidential_utxos = self.unblind(
334            all_utxos
335                .iter()
336                .filter(|utxo| utxo.txout.value.is_confidential())
337                .cloned()
338                .collect(),
339        )?;
340        // leave only explicit utxos
341        all_utxos.retain(|utxo| !utxo.txout.value.is_confidential());
342
343        all_utxos.retain(explicit_filter);
344        confidential_utxos.retain(confidential_filter);
345
346        // push unblinded utxos to explicit ones
347        all_utxos.extend(confidential_utxos);
348
349        Ok(all_utxos)
350    }
351
352    /// Derives the X-Only public key specifically used for Schnorr and Taproot structures.
353    #[must_use]
354    pub fn get_schnorr_public_key(&self) -> XOnlyPublicKey {
355        let private_key = self.get_private_key();
356        let keypair = Keypair::from_secret_key(&self.secp, &private_key.inner);
357
358        keypair.x_only_public_key().0
359    }
360
361    /// Resolves the standard format ECDSA public key.
362    #[must_use]
363    pub fn get_ecdsa_public_key(&self) -> PublicKey {
364        self.get_private_key().public_key(&self.secp)
365    }
366
367    /// Resolves the corresponding blinding public key.
368    #[must_use]
369    pub fn get_blinding_public_key(&self) -> PublicKey {
370        self.get_blinding_private_key().public_key(&self.secp)
371    }
372
373    /// Internally derives and exposes the wallet's signing active private key.
374    ///
375    /// # Panics
376    /// Panics if the master private key or derivation path cannot be derived.
377    #[must_use]
378    pub fn get_private_key(&self) -> PrivateKey {
379        let master_xprv = self.master_xpriv().unwrap();
380        let full_path = self.get_derivation_path().unwrap();
381
382        let derived = full_path.extend(
383            DerivationPath::from_str("0/1")
384                .map_err(|e| SignerError::DerivationPath(e.to_string()))
385                .unwrap(),
386        );
387
388        let ext_derived = master_xprv.derive_priv(&self.secp, &derived).unwrap();
389
390        PrivateKey::new(ext_derived.private_key, NetworkKind::Test)
391    }
392
393    /// Generates the private key linked to confidential payload blinding.
394    ///
395    /// The generated `PrivateKey` is associated with the `Test` (non-Bitcoin-mainnet) network kind.
396    /// Retrieves the blinding private key derived from the master SLIP77 key and the script public key of the address.
397    ///
398    /// # Panics
399    /// Panics if the master SLIP77 key cannot be derived.
400    #[must_use]
401    pub fn get_blinding_private_key(&self) -> PrivateKey {
402        let blinding_key = self
403            .master_slip77()
404            .unwrap()
405            .blinding_private_key(&self.get_address().script_pubkey());
406
407        PrivateKey::new(blinding_key, NetworkKind::Test)
408    }
409
410    fn unblind(&self, utxos: Vec<UTXO>) -> Result<Vec<UTXO>, SignerError> {
411        let mut unblinded: Vec<UTXO> = Vec::new();
412
413        for mut utxo in utxos {
414            let blinding_key = self.get_blinding_private_key();
415            let secrets = utxo.txout.unblind(&self.secp, blinding_key.inner)?;
416
417            utxo.secrets = Some(secrets);
418
419            unblinded.push(utxo);
420        }
421
422        Ok(unblinded)
423    }
424
425    fn estimate_tx(
426        &self,
427        mut fee_tx: FinalTransaction,
428        fee_rate: f32,
429        available_delta: u64,
430    ) -> Result<Estimate, SignerError> {
431        // estimate the tx fee with the change
432        // use this wpkh address as a change script
433        // TODO: this should be confidential
434        fee_tx.add_output(PartialOutput::new(
435            self.get_address().script_pubkey(),
436            PLACEHOLDER_FEE,
437            self.network.policy_asset(),
438        ));
439
440        fee_tx.add_output(PartialOutput::new(
441            Script::new(),
442            PLACEHOLDER_FEE,
443            self.network.policy_asset(),
444        ));
445
446        let final_tx = self.sign_tx(&fee_tx)?;
447        let fee = fee_tx.calculate_fee(final_tx.discount_weight(), fee_rate);
448
449        if available_delta > fee && available_delta - fee >= MIN_FEE {
450            // we have enough funds to cover the change UTXO
451            let outputs = fee_tx.outputs_mut();
452
453            outputs[outputs.len() - 2].amount = available_delta - fee;
454            outputs[outputs.len() - 1].amount = fee;
455
456            let final_tx = self.sign_tx(&fee_tx)?;
457
458            return Ok(Estimate::Success(final_tx, fee));
459        }
460
461        // not enough funds, so we need to estimate without the change
462        fee_tx.remove_output(fee_tx.n_outputs() - 2);
463
464        let final_tx = self.sign_tx(&fee_tx)?;
465        let fee = fee_tx.calculate_fee(final_tx.discount_weight(), fee_rate);
466
467        if available_delta < fee {
468            return Ok(Estimate::Failure(fee));
469        }
470
471        let outputs = fee_tx.outputs_mut();
472
473        // change the fee output amount
474        outputs[outputs.len() - 1].amount = available_delta;
475
476        // finalize the tx with fee and without the change
477        let final_tx = self.sign_tx(&fee_tx)?;
478
479        Ok(Estimate::Success(final_tx, fee))
480    }
481
482    fn sign_tx(&self, tx: &FinalTransaction) -> Result<Transaction, SignerError> {
483        let (mut pst, secrets) = tx.extract_pst();
484        let inputs = tx.inputs();
485
486        if tx.needs_blinding() {
487            pst.blind_last(&mut thread_rng(), &self.secp, &secrets)?;
488        }
489
490        for (index, input_i) in inputs.iter().enumerate() {
491            // we need to prune the program
492            if let Some(program_input) = &input_i.program_input {
493                let signing_info: Option<(&String, &[String])> = match &input_i.required_sig {
494                    RequiredSignature::Witness(wtns_name) => Some((wtns_name, &[])),
495                    RequiredSignature::WitnessWithPath(wtns_name, sig_path) => Some((wtns_name, sig_path)),
496                    _ => None,
497                };
498
499                let signed_witness: Result<WitnessValues, SignerError> = match signing_info {
500                    // sign the program and inject the signature into the witness
501                    Some((witness_name, sig_path)) => Ok(self.get_signed_program_witness(
502                        &pst,
503                        program_input.program.as_ref(),
504                        &program_input.witness.build_witness(),
505                        witness_name,
506                        sig_path,
507                        index,
508                    )?),
509                    // just build the witness
510                    None => Ok(program_input.witness.build_witness()),
511                };
512
513                let pruned_witness =
514                    program_input
515                        .program
516                        .finalize(&pst, &signed_witness.unwrap(), index, &self.network)?;
517
518                pst.inputs_mut()[index].final_script_witness = Some(pruned_witness);
519            } else {
520                // we need to sign the UTXO as is
521                // TODO: do we always sign?
522                let signed_witness = self.sign_input(&pst, index)?;
523                let raw_sig = elementssig_to_rawsig(&(signed_witness.1, EcdsaSighashType::All));
524
525                pst.inputs_mut()[index].final_script_witness = Some(vec![raw_sig, signed_witness.0.to_bytes()]);
526            }
527        }
528
529        Ok(pst.extract_tx()?)
530    }
531
532    fn get_signed_program_witness(
533        &self,
534        pst: &PartiallySignedTransaction,
535        program: &dyn ProgramTrait,
536        witness: &WitnessValues,
537        witness_name: &str,
538        sig_path: &[String],
539        index: usize,
540    ) -> Result<WitnessValues, SignerError> {
541        let signature = self.sign_program(pst, program, index, &self.network)?;
542
543        // inject the signature into the wtns name directly if the path is not provided
544        let sig_val = if sig_path.is_empty() {
545            Value::byte_array(signature.serialize())
546        } else {
547            let witness_types = program.get_witness_types()?;
548            let witness_type = witness_types
549                .get(&WitnessName::from_str_unchecked(witness_name))
550                .ok_or(SignerError::WtnsFieldNotFound(witness_name.to_string()))?;
551
552            let local_wtns = Arc::new(
553                witness
554                    .get(&WitnessName::from_str_unchecked(witness_name))
555                    .expect("checked above")
556                    .clone(),
557            );
558
559            WtnsInjector::inject_value(
560                &local_wtns,
561                witness_type,
562                sig_path,
563                Value::byte_array(signature.serialize()),
564            )?
565        };
566
567        let mut hm = HashMap::new();
568
569        witness.iter().for_each(|el| {
570            hm.insert(el.0.clone(), el.1.clone());
571        });
572
573        hm.insert(WitnessName::from_str_unchecked(witness_name), sig_val);
574
575        Ok(WitnessValues::from(hm))
576    }
577
578    #[allow(clippy::unnecessary_wraps)]
579    fn master_slip77(&self) -> Result<MasterBlindingKey, SignerError> {
580        let seed = self.mnemonic.to_seed("");
581
582        Ok(MasterBlindingKey::from_seed(&seed[..]))
583    }
584
585    fn derive_xpriv(&self, path: &DerivationPath) -> Result<Xpriv, SignerError> {
586        Ok(self.xprv.derive_priv(&self.secp, &path)?)
587    }
588
589    fn master_xpriv(&self) -> Result<Xpriv, SignerError> {
590        self.derive_xpriv(&DerivationPath::master())
591    }
592
593    fn derive_xpub(&self, path: &DerivationPath) -> Result<Xpub, SignerError> {
594        let derived = self.derive_xpriv(path)?;
595
596        Ok(Xpub::from_priv(&self.secp, &derived))
597    }
598
599    fn master_xpub(&self) -> Result<Xpub, SignerError> {
600        self.derive_xpub(&DerivationPath::master())
601    }
602
603    fn fingerprint(&self) -> Result<Fingerprint, SignerError> {
604        Ok(self.master_xpub()?.fingerprint())
605    }
606
607    fn get_slip77_descriptor(&self) -> Result<String, SignerError> {
608        let wpkh_descriptor = self.get_wpkh_descriptor()?;
609        let blinding_key = self.master_slip77()?;
610
611        Ok(format!("ct(slip77({blinding_key}),{wpkh_descriptor})"))
612    }
613
614    fn get_wpkh_descriptor(&self) -> Result<String, SignerError> {
615        let fingerprint = self.fingerprint()?;
616        let path = self.get_derivation_path()?;
617        let xpub = self.derive_xpub(&path)?;
618
619        Ok(format!("elwpkh([{fingerprint}/{path}]{xpub}/<0;1>/*)"))
620    }
621
622    fn get_derivation_path(&self) -> Result<DerivationPath, SignerError> {
623        let coin_type = if self.network.is_mainnet() { 1776 } else { 1 };
624        let path = format!("84h/{coin_type}h/0h");
625
626        DerivationPath::from_str(&format!("m/{path}")).map_err(|e| SignerError::DerivationPath(e.to_string()))
627    }
628}
629
630#[cfg(test)]
631mod tests {
632    use crate::provider::EsploraProvider;
633    use crate::utils::random_mnemonic;
634
635    use super::*;
636
637    fn create_signer() -> Signer {
638        let url = "https://blockstream.info/liquidtestnet/api".to_string();
639        let network = SimplicityNetwork::LiquidTestnet;
640
641        Signer::new(random_mnemonic().as_str(), Box::new(EsploraProvider::new(url, network)))
642    }
643
644    #[test]
645    fn keys_correspond_to_address() {
646        let signer = create_signer();
647
648        let address = signer.get_address();
649        let pubkey = signer.get_ecdsa_public_key();
650
651        let derived_addr = Address::p2wpkh(&pubkey, None, signer.get_provider().get_network().address_params());
652
653        assert_eq!(derived_addr.to_string(), address.to_string());
654    }
655
656    #[test]
657    fn descriptors() {
658        let signer = create_signer();
659
660        println!("{}", signer.get_address());
661        println!("{}", signer.get_confidential_address());
662    }
663}