From 59ffd07936acbc4f1946c0eb17becfc0a5ad98a4 Mon Sep 17 00:00:00 2001 From: Illia Kripaka Date: Wed, 10 Dec 2025 16:38:36 +0200 Subject: [PATCH 1/4] BRAKING CHANGE: try to implement contract evaluation in test using LWK --- Cargo.toml | 4 + crates/lwk-utils/Cargo.toml | 31 ++++ crates/lwk-utils/src/lib.rs | 190 +++++++++++++++++++++++ crates/lwk-utils/tests/testing_faucet.rs | 28 ++++ crates/lwk-utils/tests/utils.rs | 12 ++ 5 files changed, 265 insertions(+) create mode 100644 crates/lwk-utils/Cargo.toml create mode 100644 crates/lwk-utils/src/lib.rs create mode 100644 crates/lwk-utils/tests/testing_faucet.rs create mode 100644 crates/lwk-utils/tests/utils.rs diff --git a/Cargo.toml b/Cargo.toml index 32937c6..62d83c6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -42,3 +42,7 @@ tokio = { version = "1.48.0", features = ["macros", "test-util", "rt", "rt-multi tracing = { version = "0.1.41" } tracing-appender = { version = "0.2.3" } tracing-subscriber = { version = "0.3.19", features = ["env-filter"] } + +lwk_wollet = { version = "0.12.0" } +lwk_signer = { version = "0.12.0" } +lwk_common = { version = "0.12.0" } diff --git a/crates/lwk-utils/Cargo.toml b/crates/lwk-utils/Cargo.toml new file mode 100644 index 0000000..76c1f81 --- /dev/null +++ b/crates/lwk-utils/Cargo.toml @@ -0,0 +1,31 @@ +[package] +name = "lwk-utils" +version.workspace = true +edition.workspace = true +rust-version.workspace = true +authors.workspace = true +readme.workspace = true + +[dependencies] +anyhow = { workspace = true } +config = { workspace = true } +contracts = { workspace = true } +contracts-adapter = { workspace = true } +dotenvy = { workspace = true } +elements = { workspace = true } +global-utils = { workspace = true } +hex = { workspace = true } +nostr = { workspace = true } +proptest = { workspace = true } +serde = { workspace = true } +serde_json = { workspace = true } +simplicity-lang = { workspace = true } +simplicityhl = { workspace = true } +simplicityhl-core = { workspace = true } +thiserror = { workspace = true } +tokio = { workspace = true } +tracing = { workspace = true } + +lwk_common = { workspace = true } +lwk_wollet = { workspace = true } +lwk_signer = { workspace = true } \ No newline at end of file diff --git a/crates/lwk-utils/src/lib.rs b/crates/lwk-utils/src/lib.rs new file mode 100644 index 0000000..90959ab --- /dev/null +++ b/crates/lwk-utils/src/lib.rs @@ -0,0 +1,190 @@ +use anyhow::anyhow; +use elements::bitcoin::secp256k1; +use elements::hashes::Hash; +use elements::hex::ToHex; +use elements::schnorr::Keypair; +use elements::secp256k1_zkp::rand::thread_rng; +use elements::secp256k1_zkp::{PublicKey, Secp256k1}; +use lwk_common::Signer; +use lwk_wollet::elements::{Transaction, TxInWitness}; +use lwk_wollet::elements_miniscript::ToPublicKey; +use simplicity::elements::confidential::{AssetBlindingFactor, ValueBlindingFactor}; +use simplicity::elements::pset::{Input, Output, PartiallySignedTransaction}; +use simplicity::elements::{AddressParams, AssetId, OutPoint, TxOut, TxOutSecrets}; +use simplicityhl::simplicity::RedeemNode; +use simplicityhl::simplicity::jet::Elements; +use simplicityhl::simplicity::jet::elements::ElementsEnv; +use simplicityhl::str::WitnessName; +use simplicityhl::value::ValueConstructible; +use simplicityhl::{CompiledProgram, Value}; +use simplicityhl_core::{ + RunnerLogLevel, control_block, fetch_utxo, get_and_verify_env, get_new_asset_entropy, get_p2pk_address, + get_p2pk_program, get_random_seed, obtain_utxo_value, run_program, +}; +use std::collections::HashMap; +use std::sync::Arc; + +#[expect(clippy::too_many_arguments)] +pub async fn issue_asset( + signer: &impl Signer, + blinding_key: PublicKey, + fee_utxo_outpoint: OutPoint, + issue_amount: u64, + fee_amount: u64, + address_params: &'static AddressParams, + lbtc_asset: AssetId, + genesis_block_hash: simplicity::elements::BlockHash, +) -> anyhow::Result { + let fee_utxo_tx_out = fetch_utxo(fee_utxo_outpoint)?; + + let total_input_fee = obtain_utxo_value(&fee_utxo_tx_out)?; + if fee_amount > total_input_fee { + return Err(anyhow!( + "fee exceeds fee input value, fee_input: {fee_amount}, total_input_fee: {total_input_fee}" + )); + } + + let asset_entropy = get_random_seed(); + let asset_entropy_to_return = get_new_asset_entropy(&fee_utxo_outpoint, asset_entropy).to_hex(); + + let mut issuance_tx = Input::from_prevout(fee_utxo_outpoint); + issuance_tx.witness_utxo = Some(fee_utxo_tx_out.clone()); + issuance_tx.issuance_value_amount = Some(issue_amount); + issuance_tx.issuance_inflation_keys = Some(1); + issuance_tx.issuance_asset_entropy = Some(asset_entropy); + + let (asset_id, reissuance_asset_id) = issuance_tx.issuance_ids(); + + let change_recipient = get_p2pk_address( + &get_x_only_pubkey_from_signer(signer)?.x_only_public_key().0, + address_params, + )?; + + let mut inp_txout_sec = std::collections::HashMap::new(); + let mut pst = PartiallySignedTransaction::new_v2(); + + // Issuance token input + { + let issuance_secrets = TxOutSecrets { + asset_bf: AssetBlindingFactor::zero(), + value_bf: ValueBlindingFactor::zero(), + value: fee_utxo_tx_out.value.explicit().unwrap(), + asset: lbtc_asset, + }; + + issuance_tx.blinded_issuance = Some(0x00); + pst.add_input(issuance_tx); + + inp_txout_sec.insert(0, issuance_secrets); + } + + // Passing Reissuance token to new tx_out + { + let mut output = Output::new_explicit( + change_recipient.script_pubkey(), + 1, + reissuance_asset_id, + Some(blinding_key.into()), + ); + output.blinder_index = Some(0); + pst.add_output(output); + } + + // Defining the amount of token issuance + pst.add_output(Output::new_explicit( + change_recipient.script_pubkey(), + issue_amount, + asset_id, + None, + )); + + // Change + pst.add_output(Output::new_explicit( + change_recipient.script_pubkey(), + total_input_fee - fee_amount, + lbtc_asset, + None, + )); + + // Fee + pst.add_output(Output::from_txout(TxOut::new_fee(fee_amount, lbtc_asset))); + + pst.blind_last(&mut thread_rng(), &Secp256k1::new(), &inp_txout_sec)?; + + let tx = finalize_p2pk_transaction( + pst.extract_tx()?, + std::slice::from_ref(&fee_utxo_tx_out), + signer, + 0, + address_params, + genesis_block_hash, + )?; + + tx.verify_tx_amt_proofs(secp256k1::SECP256K1, &[fee_utxo_tx_out])?; + Ok(pst) +} + +fn get_x_only_pubkey_from_signer(signer: &impl Signer) -> anyhow::Result { + Ok(signer + .xpub() + .map_err(|err| anyhow::anyhow!("xpub forming error, err: {err:?}"))? + .public_key) +} + +pub fn finalize_p2pk_transaction( + mut tx: Transaction, + utxos: &[TxOut], + signer: &impl Signer, + input_index: usize, + params: &'static AddressParams, + genesis_hash: lwk_wollet::elements::BlockHash, +) -> anyhow::Result { + let x_only_public_key = get_x_only_pubkey_from_signer(signer)?.x_only_public_key().0; + let p2pk_program = get_p2pk_program(&x_only_public_key)?; + + let env = get_and_verify_env( + &tx, + &p2pk_program, + &x_only_public_key, + utxos, + params, + genesis_hash, + input_index, + )?; + + let pruned = execute_p2pk_program(&p2pk_program, signer, &env, RunnerLogLevel::None)?; + + let (simplicity_program_bytes, simplicity_witness_bytes) = pruned.to_vec_with_witness(); + let cmr = pruned.cmr(); + + tx.input[input_index].witness = TxInWitness { + amount_rangeproof: None, + inflation_keys_rangeproof: None, + script_witness: vec![ + simplicity_witness_bytes, + simplicity_program_bytes, + cmr.as_ref().to_vec(), + control_block(cmr, x_only_public_key).serialize(), + ], + pegin_witness: vec![], + }; + + Ok(tx) +} + +pub fn execute_p2pk_program( + compiled_program: &CompiledProgram, + keypair: &impl Signer, + env: &ElementsEnv>, + runner_log_level: RunnerLogLevel, +) -> anyhow::Result>> { + let sighash_all = secp256k1::Message::from_digest(env.c_tx_env().sighash_all().to_byte_array()); + + let witness_values = simplicityhl::WitnessValues::from(HashMap::from([( + WitnessName::from_str_unchecked("SIGNATURE"), + // TODO: sighash has to be signed + Value::byte_array(keypair.sign_message(sighash_all).serialize()), + )])); + + Ok(run_program(compiled_program, witness_values, env, runner_log_level)?.0) +} diff --git a/crates/lwk-utils/tests/testing_faucet.rs b/crates/lwk-utils/tests/testing_faucet.rs new file mode 100644 index 0000000..7a5c5a3 --- /dev/null +++ b/crates/lwk-utils/tests/testing_faucet.rs @@ -0,0 +1,28 @@ +mod utils; + +mod tests{ + use lwk_signer::SwSigner; + use lwk_wollet::{ElementsNetwork, NoPersist, Wollet}; + use nostr::secp256k1::Secp256k1; + use simplicity::bitcoin::secp256k1::Keypair; + use crate::utils::get_descriptor; + + #[test] + fn test() -> anyhow::Result<()>{ + let mnemonic = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about"; + let network = ElementsNetwork::LiquidTestnet; + + // 1. Create a wallet using SwSigner + let sw_signer = SwSigner::new(mnemonic, false)?; + let sw_wallet = Wollet::new( + network, + NoPersist::new(), + get_descriptor(&sw_signer).unwrap(), + ) + ?; + let secp = Secp256k1::new(); + let keypair = Keypair::from_seckey_str(&secp, sw_signer.xpub().)? + + Ok(()) + } +} \ No newline at end of file diff --git a/crates/lwk-utils/tests/utils.rs b/crates/lwk-utils/tests/utils.rs new file mode 100644 index 0000000..eb2fecf --- /dev/null +++ b/crates/lwk-utils/tests/utils.rs @@ -0,0 +1,12 @@ +use global_utils::logger::{LoggerGuard, init_logger}; +use lwk_common::{Signer, Singlesig, singlesig_desc}; +use lwk_wollet::WolletDescriptor; +use std::sync::LazyLock; + +pub static TEST_LOGGER: LazyLock = LazyLock::new(init_logger); + +pub fn get_descriptor(signer: &S) -> Result { + let descriptor_str = singlesig_desc(signer, Singlesig::Wpkh, lwk_common::DescriptorBlindingKey::Slip77) + .map_err(|e| anyhow::anyhow!("Invalid descriptor: {e}"))?; + Ok(descriptor_str.parse()?) +} From 88887a2dc7cd343d8a942c0d733783948edb3cad Mon Sep 17 00:00:00 2001 From: Illia Kripaka Date: Fri, 12 Dec 2025 18:08:10 +0200 Subject: [PATCH 2/4] add possible tests setup for contract --- Cargo.toml | 9 +- crates/lwk-utils/Cargo.toml | 6 +- crates/lwk-utils/src/lib.rs | 191 +------ crates/lwk-utils/src/types.rs | 27 + crates/lwk-utils/tests/faucet_contract.rs | 234 ++++++++ crates/lwk-utils/tests/testing_faucet.rs | 256 ++++++++- crates/lwk-utils/tests/utils.rs | 636 +++++++++++++++++++++- 7 files changed, 1140 insertions(+), 219 deletions(-) create mode 100644 crates/lwk-utils/src/types.rs create mode 100644 crates/lwk-utils/tests/faucet_contract.rs diff --git a/Cargo.toml b/Cargo.toml index 62d83c6..67715a5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -43,6 +43,9 @@ tracing = { version = "0.1.41" } tracing-appender = { version = "0.2.3" } tracing-subscriber = { version = "0.3.19", features = ["env-filter"] } -lwk_wollet = { version = "0.12.0" } -lwk_signer = { version = "0.12.0" } -lwk_common = { version = "0.12.0" } +lwk_wollet = { version = "0.13.0" } +lwk_signer = { version = "0.13.0" } +lwk_common = { version = "0.13.0" } +lwk_wasm = { version = "0.12.0" } +lwk_test_util = { version = "0.13.0" } +tempfile = { version = "3.23.0" } diff --git a/crates/lwk-utils/Cargo.toml b/crates/lwk-utils/Cargo.toml index 76c1f81..e72a64e 100644 --- a/crates/lwk-utils/Cargo.toml +++ b/crates/lwk-utils/Cargo.toml @@ -28,4 +28,8 @@ tracing = { workspace = true } lwk_common = { workspace = true } lwk_wollet = { workspace = true } -lwk_signer = { workspace = true } \ No newline at end of file +lwk_signer = { workspace = true } + +[dev-dependencies] +lwk_test_util = { workspace = true } +tempfile = { workspace = true } \ No newline at end of file diff --git a/crates/lwk-utils/src/lib.rs b/crates/lwk-utils/src/lib.rs index 90959ab..cd40856 100644 --- a/crates/lwk-utils/src/lib.rs +++ b/crates/lwk-utils/src/lib.rs @@ -1,190 +1 @@ -use anyhow::anyhow; -use elements::bitcoin::secp256k1; -use elements::hashes::Hash; -use elements::hex::ToHex; -use elements::schnorr::Keypair; -use elements::secp256k1_zkp::rand::thread_rng; -use elements::secp256k1_zkp::{PublicKey, Secp256k1}; -use lwk_common::Signer; -use lwk_wollet::elements::{Transaction, TxInWitness}; -use lwk_wollet::elements_miniscript::ToPublicKey; -use simplicity::elements::confidential::{AssetBlindingFactor, ValueBlindingFactor}; -use simplicity::elements::pset::{Input, Output, PartiallySignedTransaction}; -use simplicity::elements::{AddressParams, AssetId, OutPoint, TxOut, TxOutSecrets}; -use simplicityhl::simplicity::RedeemNode; -use simplicityhl::simplicity::jet::Elements; -use simplicityhl::simplicity::jet::elements::ElementsEnv; -use simplicityhl::str::WitnessName; -use simplicityhl::value::ValueConstructible; -use simplicityhl::{CompiledProgram, Value}; -use simplicityhl_core::{ - RunnerLogLevel, control_block, fetch_utxo, get_and_verify_env, get_new_asset_entropy, get_p2pk_address, - get_p2pk_program, get_random_seed, obtain_utxo_value, run_program, -}; -use std::collections::HashMap; -use std::sync::Arc; - -#[expect(clippy::too_many_arguments)] -pub async fn issue_asset( - signer: &impl Signer, - blinding_key: PublicKey, - fee_utxo_outpoint: OutPoint, - issue_amount: u64, - fee_amount: u64, - address_params: &'static AddressParams, - lbtc_asset: AssetId, - genesis_block_hash: simplicity::elements::BlockHash, -) -> anyhow::Result { - let fee_utxo_tx_out = fetch_utxo(fee_utxo_outpoint)?; - - let total_input_fee = obtain_utxo_value(&fee_utxo_tx_out)?; - if fee_amount > total_input_fee { - return Err(anyhow!( - "fee exceeds fee input value, fee_input: {fee_amount}, total_input_fee: {total_input_fee}" - )); - } - - let asset_entropy = get_random_seed(); - let asset_entropy_to_return = get_new_asset_entropy(&fee_utxo_outpoint, asset_entropy).to_hex(); - - let mut issuance_tx = Input::from_prevout(fee_utxo_outpoint); - issuance_tx.witness_utxo = Some(fee_utxo_tx_out.clone()); - issuance_tx.issuance_value_amount = Some(issue_amount); - issuance_tx.issuance_inflation_keys = Some(1); - issuance_tx.issuance_asset_entropy = Some(asset_entropy); - - let (asset_id, reissuance_asset_id) = issuance_tx.issuance_ids(); - - let change_recipient = get_p2pk_address( - &get_x_only_pubkey_from_signer(signer)?.x_only_public_key().0, - address_params, - )?; - - let mut inp_txout_sec = std::collections::HashMap::new(); - let mut pst = PartiallySignedTransaction::new_v2(); - - // Issuance token input - { - let issuance_secrets = TxOutSecrets { - asset_bf: AssetBlindingFactor::zero(), - value_bf: ValueBlindingFactor::zero(), - value: fee_utxo_tx_out.value.explicit().unwrap(), - asset: lbtc_asset, - }; - - issuance_tx.blinded_issuance = Some(0x00); - pst.add_input(issuance_tx); - - inp_txout_sec.insert(0, issuance_secrets); - } - - // Passing Reissuance token to new tx_out - { - let mut output = Output::new_explicit( - change_recipient.script_pubkey(), - 1, - reissuance_asset_id, - Some(blinding_key.into()), - ); - output.blinder_index = Some(0); - pst.add_output(output); - } - - // Defining the amount of token issuance - pst.add_output(Output::new_explicit( - change_recipient.script_pubkey(), - issue_amount, - asset_id, - None, - )); - - // Change - pst.add_output(Output::new_explicit( - change_recipient.script_pubkey(), - total_input_fee - fee_amount, - lbtc_asset, - None, - )); - - // Fee - pst.add_output(Output::from_txout(TxOut::new_fee(fee_amount, lbtc_asset))); - - pst.blind_last(&mut thread_rng(), &Secp256k1::new(), &inp_txout_sec)?; - - let tx = finalize_p2pk_transaction( - pst.extract_tx()?, - std::slice::from_ref(&fee_utxo_tx_out), - signer, - 0, - address_params, - genesis_block_hash, - )?; - - tx.verify_tx_amt_proofs(secp256k1::SECP256K1, &[fee_utxo_tx_out])?; - Ok(pst) -} - -fn get_x_only_pubkey_from_signer(signer: &impl Signer) -> anyhow::Result { - Ok(signer - .xpub() - .map_err(|err| anyhow::anyhow!("xpub forming error, err: {err:?}"))? - .public_key) -} - -pub fn finalize_p2pk_transaction( - mut tx: Transaction, - utxos: &[TxOut], - signer: &impl Signer, - input_index: usize, - params: &'static AddressParams, - genesis_hash: lwk_wollet::elements::BlockHash, -) -> anyhow::Result { - let x_only_public_key = get_x_only_pubkey_from_signer(signer)?.x_only_public_key().0; - let p2pk_program = get_p2pk_program(&x_only_public_key)?; - - let env = get_and_verify_env( - &tx, - &p2pk_program, - &x_only_public_key, - utxos, - params, - genesis_hash, - input_index, - )?; - - let pruned = execute_p2pk_program(&p2pk_program, signer, &env, RunnerLogLevel::None)?; - - let (simplicity_program_bytes, simplicity_witness_bytes) = pruned.to_vec_with_witness(); - let cmr = pruned.cmr(); - - tx.input[input_index].witness = TxInWitness { - amount_rangeproof: None, - inflation_keys_rangeproof: None, - script_witness: vec![ - simplicity_witness_bytes, - simplicity_program_bytes, - cmr.as_ref().to_vec(), - control_block(cmr, x_only_public_key).serialize(), - ], - pegin_witness: vec![], - }; - - Ok(tx) -} - -pub fn execute_p2pk_program( - compiled_program: &CompiledProgram, - keypair: &impl Signer, - env: &ElementsEnv>, - runner_log_level: RunnerLogLevel, -) -> anyhow::Result>> { - let sighash_all = secp256k1::Message::from_digest(env.c_tx_env().sighash_all().to_byte_array()); - - let witness_values = simplicityhl::WitnessValues::from(HashMap::from([( - WitnessName::from_str_unchecked("SIGNATURE"), - // TODO: sighash has to be signed - Value::byte_array(keypair.sign_message(sighash_all).serialize()), - )])); - - Ok(run_program(compiled_program, witness_values, env, runner_log_level)?.0) -} +pub mod types; diff --git a/crates/lwk-utils/src/types.rs b/crates/lwk-utils/src/types.rs new file mode 100644 index 0000000..89d3d62 --- /dev/null +++ b/crates/lwk-utils/src/types.rs @@ -0,0 +1,27 @@ +/// A trait that can be used to sign messages and verify signatures. +/// The sdk user can implement this trait to use their own signer. +pub trait SimplicitySigner: Send + Sync { + /// The master xpub encoded as 78 bytes length as defined in bip32 specification. + /// For reference: + fn xpub(&self) -> anyhow::Result>; + + /// The derived xpub encoded as 78 bytes length as defined in bip32 specification. + /// The derivation path is a string represents the shorter notation of the key tree to derive. For example: + /// m/49'/1'/0'/0/0 + /// m/48'/1'/0'/0/0 + /// For reference: + fn derive_xpub(&self, derivation_path: String) -> anyhow::Result>; + + /// Sign an ECDSA message using the private key derived from the given derivation path + fn sign_ecdsa(&self, msg: Vec, derivation_path: String) -> anyhow::Result>; + + /// Sign an ECDSA message using the private key derived from the master key + fn sign_ecdsa_recoverable(&self, msg: Vec) -> anyhow::Result>; + + /// Return the master blinding key for SLIP77: + fn slip77_master_blinding_key(&self) -> anyhow::Result>; + + /// HMAC-SHA256 using the private key derived from the given derivation path + /// This is used to calculate the linking key of lnurl-auth specification: + fn hmac_sha256(&self, msg: Vec, derivation_path: String) -> anyhow::Result>; +} diff --git a/crates/lwk-utils/tests/faucet_contract.rs b/crates/lwk-utils/tests/faucet_contract.rs new file mode 100644 index 0000000..334ea09 --- /dev/null +++ b/crates/lwk-utils/tests/faucet_contract.rs @@ -0,0 +1,234 @@ +use anyhow::anyhow; +use elements::bitcoin::secp256k1; +use elements::hashes::Hash; +use elements::hex::ToHex; +use elements::schnorr::Keypair; +use elements::secp256k1_zkp::rand::thread_rng; +use elements::secp256k1_zkp::{PublicKey, Secp256k1}; +use lwk_common::Signer; +use lwk_wollet::WalletTx; +use lwk_wollet::elements::{Transaction, TxInWitness}; +use serde::Serialize; +use simplicity::elements::confidential::{AssetBlindingFactor, ValueBlindingFactor}; +use simplicity::elements::pset::{Input, Output, PartiallySignedTransaction}; +use simplicity::elements::{AddressParams, AssetId, OutPoint, TxOut, TxOutSecrets}; +use simplicityhl::simplicity::RedeemNode; +use simplicityhl::simplicity::jet::Elements; +use simplicityhl::simplicity::jet::elements::ElementsEnv; +use simplicityhl::str::WitnessName; +use simplicityhl::value::ValueConstructible; +use simplicityhl::{CompiledProgram, Value}; +use simplicityhl_core::{ + RunnerLogLevel, control_block, get_and_verify_env, get_new_asset_entropy, get_p2pk_address, get_p2pk_program, + get_random_seed, run_program, +}; +use std::collections::HashMap; +use std::sync::Arc; + +#[derive(Clone, Debug)] +pub struct TxInfo { + pub outpoint: OutPoint, + pub wallet_tx: WalletTx, +} + +impl TxInfo { + fn obtain_tx_out(&self) -> TxOut { + self.wallet_tx.tx.output[self.outpoint.vout as usize].clone() + } + + #[inline] + pub fn obtain_token_value(&self, asset: &AssetId) -> anyhow::Result { + println!("{:?}, asset: {asset}", self.wallet_tx.balance.get(asset)); + self.wallet_tx + .balance + .get(asset) + .map(|x| *x as u64) + .ok_or_else(|| anyhow::anyhow!("No value in utxo, check it, signed tx values for asset: {asset:?}")) + } +} + +#[expect(clippy::too_many_arguments)] +pub async fn issue_asset( + signer: &Keypair, + blinding_key: PublicKey, + fee_tx_info: TxInfo, + issue_amount: u64, + fee_amount: u64, + address_params: &'static AddressParams, + lbtc_asset: AssetId, + genesis_block_hash: simplicity::elements::BlockHash, +) -> anyhow::Result { + let fee_utxo_tx_out = fee_tx_info.obtain_tx_out(); + println!("fee_tx_out: {:?}", fee_utxo_tx_out); + let total_input_lbtc_value = fee_tx_info.obtain_token_value(&lbtc_asset)?; + + if fee_amount > total_input_lbtc_value { + return Err(anyhow!( + "fee exceeds fee input value, fee_input: {fee_amount}, total_input_fee: {total_input_lbtc_value}" + )); + } + + let asset_entropy = get_random_seed(); + let asset_entropy_to_return = get_new_asset_entropy(&fee_tx_info.outpoint, asset_entropy).to_hex(); + + let mut issuance_tx = Input::from_prevout(fee_tx_info.outpoint); + issuance_tx.witness_utxo = Some(fee_utxo_tx_out.clone()); + issuance_tx.issuance_value_amount = Some(issue_amount); + issuance_tx.issuance_inflation_keys = Some(1); + issuance_tx.issuance_asset_entropy = Some(asset_entropy); + + let (asset_id, reissuance_asset_id) = issuance_tx.issuance_ids(); + + let change_recipient = get_p2pk_address(&signer.x_only_public_key().0, address_params)?; + + let mut inp_txout_sec = std::collections::HashMap::new(); + let mut pst = PartiallySignedTransaction::new_v2(); + + // Issuance token input + { + let issuance_secrets = TxOutSecrets { + asset_bf: AssetBlindingFactor::zero(), + value_bf: ValueBlindingFactor::zero(), + value: total_input_lbtc_value, + asset: lbtc_asset, + }; + + issuance_tx.blinded_issuance = Some(0x00); + pst.add_input(issuance_tx); + + inp_txout_sec.insert(0, issuance_secrets); + } + + // Passing Reissuance token to new tx_out + { + let mut output = Output::new_explicit( + change_recipient.script_pubkey(), + 1, + reissuance_asset_id, + Some(blinding_key.into()), + ); + output.blinder_index = Some(0); + pst.add_output(output); + } + + // Defining the amount of token issuance + pst.add_output(Output::new_explicit( + change_recipient.script_pubkey(), + issue_amount, + asset_id, + None, + )); + + // Change + pst.add_output(Output::new_explicit( + change_recipient.script_pubkey(), + total_input_lbtc_value - fee_amount, + lbtc_asset, + None, + )); + + // Fee + pst.add_output(Output::from_txout(TxOut::new_fee(fee_amount, lbtc_asset))); + + pst.blind_last(&mut thread_rng(), &Secp256k1::new(), &inp_txout_sec)?; + + let tx = finalize_p2pk_transaction( + pst.extract_tx()?, + std::slice::from_ref(&fee_utxo_tx_out.clone()), + signer, + 0, + address_params, + genesis_block_hash, + )?; + + tx.verify_tx_amt_proofs(secp256k1::SECP256K1, &[fee_utxo_tx_out])?; + Ok(pst) +} + +fn get_x_only_pubkey_from_signer(signer: &impl Signer) -> anyhow::Result { + Ok(signer + .xpub() + .map_err(|err| anyhow::anyhow!("xpub forming error, err: {err:?}"))? + .public_key) +} + +pub fn finalize_p2pk_transaction( + mut tx: Transaction, + utxos: &[TxOut], + signer: &Keypair, + input_index: usize, + params: &'static AddressParams, + genesis_hash: lwk_wollet::elements::BlockHash, +) -> anyhow::Result { + let x_only_public_key = signer.x_only_public_key().0; + let p2pk_program = get_p2pk_program(&x_only_public_key)?; + + let env = get_and_verify_env( + &tx, + &p2pk_program, + &x_only_public_key, + utxos, + params, + genesis_hash, + input_index, + )?; + + let pruned = execute_p2pk_program(&p2pk_program, signer, &env, RunnerLogLevel::None)?; + + let (simplicity_program_bytes, simplicity_witness_bytes) = pruned.to_vec_with_witness(); + let cmr = pruned.cmr(); + + tx.input[input_index].witness = TxInWitness { + amount_rangeproof: None, + inflation_keys_rangeproof: None, + script_witness: vec![ + simplicity_witness_bytes, + simplicity_program_bytes, + cmr.as_ref().to_vec(), + control_block(cmr, x_only_public_key).serialize(), + ], + pegin_witness: vec![], + }; + + Ok(tx) +} + +pub fn execute_p2pk_program( + compiled_program: &CompiledProgram, + keypair: &Keypair, + env: &ElementsEnv>, + runner_log_level: RunnerLogLevel, +) -> anyhow::Result>> { + let sighash_all = secp256k1::Message::from_digest(env.c_tx_env().sighash_all().to_byte_array()); + + let witness_values = simplicityhl::WitnessValues::from(HashMap::from([( + WitnessName::from_str_unchecked("SIGNATURE"), + Value::byte_array(keypair.sign_schnorr(sighash_all).serialize()), + )])); + + Ok(run_program(compiled_program, witness_values, env, runner_log_level)?.0) +} + +// pub fn fetch_utxo(outpoint: OutPoint) -> anyhow::Result { +// // Check file cache first +// let txid_str = outpoint.txid.to_string(); +// let cache_path = cache_path_for_txid(&txid_str)?; +// if cache_path.exists() { +// let cached_hex = fs::read_to_string(&cache_path)?; +// return extract_utxo(&cached_hex, outpoint.vout as usize); +// } +// +// let url = format!( +// "https://blockstream.info/liquidtestnet/api/tx/{}/hex", +// outpoint.txid +// ); +// +// let client = Client::builder().timeout(Duration::from_secs(10)).build()?; +// +// let tx_hex = client.get(&url).send()?.error_for_status()?.text()?; +// // Persist to cache best-effort +// if let Err(_e) = fs::write(&cache_path, &tx_hex) { +// // Ignore cache write errors +// } +// extract_utxo(&tx_hex, outpoint.vout as usize) +// } diff --git a/crates/lwk-utils/tests/testing_faucet.rs b/crates/lwk-utils/tests/testing_faucet.rs index 7a5c5a3..6128d8e 100644 --- a/crates/lwk-utils/tests/testing_faucet.rs +++ b/crates/lwk-utils/tests/testing_faucet.rs @@ -1,28 +1,236 @@ +mod faucet_contract; mod utils; -mod tests{ - use lwk_signer::SwSigner; - use lwk_wollet::{ElementsNetwork, NoPersist, Wollet}; - use nostr::secp256k1::Secp256k1; - use simplicity::bitcoin::secp256k1::Keypair; - use crate::utils::get_descriptor; - - #[test] - fn test() -> anyhow::Result<()>{ - let mnemonic = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about"; - let network = ElementsNetwork::LiquidTestnet; - - // 1. Create a wallet using SwSigner - let sw_signer = SwSigner::new(mnemonic, false)?; - let sw_wallet = Wollet::new( - network, - NoPersist::new(), - get_descriptor(&sw_signer).unwrap(), - ) - ?; - let secp = Secp256k1::new(); - let keypair = Keypair::from_seckey_str(&secp, sw_signer.xpub().)? +use crate::faucet_contract::{TxInfo, issue_asset}; +use crate::utils::{ + TEST_LOGGER, TestWollet, generate_signer, get_descriptor, test_client_electrum, test_client_esplora, + wait_update_with_txs, +}; +use elements::bitcoin::bip32::DerivationPath; +use lwk_signer::SwSigner; +use lwk_test_util::{TestEnvBuilder, generate_view_key, regtest_policy_asset}; +use lwk_wollet::asyncr::EsploraClient; +use lwk_wollet::blocking::BlockchainBackend; +use lwk_wollet::{ElementsNetwork, NoPersist, Wollet, WolletBuilder, WolletDescriptor}; +use nostr::secp256k1::Secp256k1; +use simplicity::bitcoin::secp256k1::Keypair; +use simplicityhl::elements::{AddressParams, TxOut}; +use simplicityhl_core::{LIQUID_TESTNET_BITCOIN_ASSET, LIQUID_TESTNET_GENESIS, derive_public_blinder_key}; +use std::str::FromStr; + +const DEFAULT_MNEMONIC: &str = + "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about"; + +#[tokio::test] +async fn test_issue_custom() -> anyhow::Result<()> { + let _ = dotenvy::dotenv(); + let _guard = &*TEST_LOGGER; + let network = ElementsNetwork::LiquidTestnet; - Ok(()) + let sw_signer = SwSigner::new(DEFAULT_MNEMONIC, false)?; + let mut sw_wallet = Wollet::new(network, NoPersist::new(), get_descriptor(&sw_signer).unwrap())?; + let secp = Secp256k1::new(); + let keypair = Keypair::from_secret_key(&secp, &sw_signer.derive_xprv(&DerivationPath::master())?.private_key); + + let mut esplora_client = { + // let url = match &self.inner { + // lwk_wollet::ElementsNetwork::Liquid => "https://blockstream.info/liquid/api", + // lwk_wollet::ElementsNetwork::LiquidTestnet => { + // "https://blockstream.info/liquidtestnet/api" + // } + // lwk_wollet::ElementsNetwork::ElementsRegtest { policy_asset: _ } => "127.0.0.1:3000", + // }; + EsploraClient::new( + ElementsNetwork::LiquidTestnet, + "https://blockstream.info/liquidtestnet/api/", + ) + // EsploraClient::new(ElementsNetwork::LiquidTestnet, "https://liquid.network/api/") + }; + if let Some(update) = esplora_client.full_scan_to_index(&sw_wallet, 0).await? { + sw_wallet.apply_update(update)?; } -} \ No newline at end of file + println!("address 0: {:?}", sw_wallet.address(Some(0))); + println!("assets owned: {:?}", sw_wallet.assets_owned()); + println!("decriptor: {:?}", sw_wallet.wollet_descriptor()); + println!("transactions: {:?}", sw_wallet.transactions()); + println!("balance: {:?}", sw_wallet.balance()); + // + // let pset = issue_asset( + // &keypair, + // derive_public_blinder_key().public_key(), + // outpoint, + // 123456, + // 500, + // &AddressParams::LIQUID_TESTNET, + // LIQUID_TESTNET_BITCOIN_ASSET, + // *LIQUID_TESTNET_GENESIS, + // ) + // .await?; + // + // println!("pset: {:#?}", pset); + + Ok(()) +} + +#[test] +fn test_issue_custom2() -> anyhow::Result<()> { + let _ = dotenvy::dotenv().ok(); + let _guard = &*TEST_LOGGER; + + let secp = Secp256k1::new(); + let env = TestEnvBuilder::from_env().with_electrum().build(); + let client = test_client_electrum(&env.electrum_url()); + + let signer = generate_signer(); + let view_key = generate_view_key(); + let desc = format!("ct({},elwpkh({}/*))", view_key, signer.xpub()); + let mut wallet = TestWollet::new(client, &desc); + let keypair = Keypair::from_secret_key(&secp, &signer.derive_xprv(&DerivationPath::master())?.private_key); + + let address = wallet.wollet.address(Some(0))?; + wallet.fund_btc(&env); + // wallet.fund( + // &env, + // 10_000_000, + // Some(address.address().clone()), + // Some(LIQUID_TESTNET_BITCOIN_ASSET), + // ); + let utxos = wallet.wollet.utxos()?; + println!("Utxos: {:?}", utxos); + let asset_owned = wallet.wollet.assets_owned()?; + println!("asset_owned: {:?}", asset_owned); + let external_utxos = wallet.wollet.explicit_utxos()?; + println!("external_utxos: {:?}", external_utxos); + + // let mut pset = issue_asset( + // &keypair, + // derive_public_blinder_key().public_key(), + // utxos[0].outpoint, + // 123456, + // 500, + // &AddressParams::LIQUID_TESTNET, + // LIQUID_TESTNET_BITCOIN_ASSET, + // *LIQUID_TESTNET_GENESIS, + // ) + // .await?; + + // let mut pset = tokio::runtime::Runtime::new()?.block_on(async { + // issue_asset( + // &keypair, + // derive_public_blinder_key().public_key(), + // utxos[0].outpoint, + // 123456, + // 500, + // &AddressParams::LIQUID_TESTNET, + // LIQUID_TESTNET_BITCOIN_ASSET, + // *LIQUID_TESTNET_GENESIS, + // ) + // .await + // })?; + + // let tx_to_send = wallet.wollet.finalize(&mut pset)?; + // wallet.client.broadcast(&tx_to_send)?; + + wallet.sync(); + + let utxos = wallet.wollet.utxos()?; + tracing::info!("Utxos after: {:?}", utxos); + + Ok(()) +} + +#[tokio::test] +async fn async_test_issue_custom2() -> anyhow::Result<()> { + let _ = dotenvy::dotenv().ok(); + let _guard = &*TEST_LOGGER; + + let secp = Secp256k1::new(); + let env = TestEnvBuilder::from_env().with_esplora().build(); + let mut client = test_client_esplora(&env.esplora_url()); + + let signer = generate_signer(); + let view_key = generate_view_key(); + let regtest_bitcoin_asset = regtest_policy_asset(); + + let descriptor = format!("ct({},elwpkh({}/*))", view_key, signer.xpub()); + let network = ElementsNetwork::default_regtest(); + let descriptor: WolletDescriptor = descriptor.parse()?; + let mut wollet = WolletBuilder::new(network, descriptor).build()?; + let keypair = Keypair::from_secret_key(&secp, &signer.derive_xprv(&DerivationPath::master())?.private_key); + + let update = client.full_scan(&wollet).await?.unwrap(); + wollet.apply_update(update).unwrap(); + + let address = wollet.address(None)?; + let txid = env.elementsd_sendtoaddress(address.address(), 1_000_011, None); + + let update = wait_update_with_txs(&mut client, &wollet).await; + wollet.apply_update(update)?; + let tx = wollet.transaction(&txid)?.unwrap(); + assert!(tx.height.is_none()); + assert!(wollet.tip().timestamp().is_some()); + + env.elementsd_generate(10); + let update = wait_update_with_txs(&mut client, &wollet).await; + wollet.apply_update(update)?; + let tx = wollet.transaction(&txid)?.unwrap(); + + assert!(tx.height.is_some()); + assert!(wollet.tip().timestamp().is_some()); + + let utxos = wollet.utxos()?; + println!("Utxos: {:#?}", utxos); + let asset_owned = wollet.assets_owned()?; + println!("asset_owned: {:?}", asset_owned); + let external_utxos = wollet.explicit_utxos()?; + println!("external_utxos: {:?}", external_utxos); + + let outpoint = utxos[0].outpoint; + let wallet_tx = wollet.transaction(&outpoint.txid)?.unwrap(); + println!("wallet_tx: {:?}", wallet_tx); + println!("signed balance: {:#?}", wallet_tx.balance); + // println!("wallet_tx outs: {:?}", wallet_tx.outputs[0].unwrap().outpoint); + + let mut pset = issue_asset( + &keypair, + derive_public_blinder_key().public_key(), + TxInfo { outpoint, wallet_tx }, + 123456, + 500, + &AddressParams::LIQUID_TESTNET, + regtest_bitcoin_asset, + *LIQUID_TESTNET_GENESIS, + ) + .await?; + + let tx_to_send = wollet.finalize(&mut pset)?; + client.broadcast(&tx_to_send).await?; + + env.elementsd_generate(10); + let update = wait_update_with_txs(&mut client, &wollet).await; + wollet.apply_update(update)?; + + let utxos = wollet.utxos()?; + println!("[after] Utxos: {:?}", utxos); + let asset_owned = wollet.assets_owned()?; + println!("[after] asset_owned: {:?}", asset_owned); + let external_utxos = wollet.explicit_utxos()?; + println!("[after] external_utxos: {:?}", external_utxos); + let wallet_tx = wollet.transaction(&utxos[0].outpoint.txid)?; + println!("[after] wallet_tx: {:?}", wallet_tx.unwrap()); + + Ok(()) +} + +#[tokio::test] +async fn get_addr() -> anyhow::Result<()> { + let sw_signer = SwSigner::new(DEFAULT_MNEMONIC, false)?; + let secp = Secp256k1::new(); + let keypair = Keypair::from_secret_key(&secp, &sw_signer.derive_xprv(&DerivationPath::master())?.private_key); + + let public_key = keypair.x_only_public_key().0; + let address = simplicityhl_core::get_p2pk_address(&public_key, &AddressParams::LIQUID_TESTNET)?; + println!("X Only Public Key: '{public_key}', P2PK Address: '{address}'"); + + Ok(()) +} diff --git a/crates/lwk-utils/tests/utils.rs b/crates/lwk-utils/tests/utils.rs index eb2fecf..5918ba2 100644 --- a/crates/lwk-utils/tests/utils.rs +++ b/crates/lwk-utils/tests/utils.rs @@ -1,7 +1,21 @@ +use clients::blocking::BlockchainBackend; use global_utils::logger::{LoggerGuard, init_logger}; use lwk_common::{Signer, Singlesig, singlesig_desc}; -use lwk_wollet::WolletDescriptor; +use lwk_signer::SwSigner; +use lwk_signer::*; +use lwk_test_util::generate_mnemonic; +use lwk_test_util::*; +use lwk_wollet::elements_miniscript::{DescriptorPublicKey, ForEachKey}; +use lwk_wollet::*; +use lwk_wollet::{ElectrumClient, ElectrumUrl, WolletDescriptor}; +use simplicityhl::elements::hashes::Hash; +use simplicityhl::elements::pset::PartiallySignedTransaction; +use simplicityhl::elements::{Address, AssetId, ContractHash, OutPoint, Txid}; +use std::str::FromStr; use std::sync::LazyLock; +use std::thread; +use std::time::Duration; +use tempfile::TempDir; pub static TEST_LOGGER: LazyLock = LazyLock::new(init_logger); @@ -10,3 +24,623 @@ pub fn get_descriptor(signer: &S) -> Result SwSigner { + let mnemonic = generate_mnemonic(); + SwSigner::new(&mnemonic, false).unwrap() +} + +pub fn test_client_electrum(url: &str) -> ElectrumClient { + let url = &url.replace("tcp://", ""); + let tls = false; + let validate_domain = false; + let electrum_url = ElectrumUrl::new(url, tls, validate_domain).unwrap(); + ElectrumClient::new(&electrum_url).unwrap() +} + +pub fn test_client_esplora(url: &str) -> lwk_wollet::asyncr::EsploraClient { + let url = &url.replace("tcp://", ""); + lwk_wollet::asyncr::EsploraClient::new(ElementsNetwork::default_regtest(), &url) +} + +fn sync(wollet: &mut Wollet, client: &mut S) { + let update = client.full_scan(wollet).unwrap(); + if let Some(update) = update { + wollet.apply_update(update).unwrap(); + } +} + +/// Used with Esplora +pub async fn wait_update_with_txs(client: &mut clients::asyncr::EsploraClient, wollet: &Wollet) -> Update { + for _ in 0..50 { + let update = client.full_scan(wollet).await.unwrap(); + if let Some(update) = update { + if !update.only_tip() { + return update; + } + } + std::thread::sleep(std::time::Duration::from_millis(200)); + } + panic!("update didn't arrive"); +} + +pub fn wait_for_tx(wollet: &mut Wollet, client: &mut S, txid: &Txid) { + for _ in 0..120 { + sync(wollet, client); + let list = wollet.transactions().unwrap(); + if list.iter().any(|e| &e.txid == txid) { + return; + } + thread::sleep(Duration::from_millis(500)); + } + panic!("Wallet does not have {txid} in its list"); +} + +pub struct TestWollet { + pub wollet: Wollet, + pub client: C, + db_root_dir: TempDir, +} +impl TestWollet { + pub fn new(mut client: C, desc: &str) -> Self { + let db_root_dir = TempDir::new().unwrap(); + + let network = ElementsNetwork::default_regtest(); + let descriptor = add_checksum(desc); + + let desc: WolletDescriptor = descriptor.parse().unwrap(); + let mut wollet = Wollet::with_fs_persist(network, desc, &db_root_dir).unwrap(); + + sync(&mut wollet, &mut client); + + let mut i = 120; + let tip = loop { + assert!(i > 0, "1 minute without updates"); + i -= 1; + let height = client.tip().unwrap().height; + if height >= 101 { + break height; + } else { + thread::sleep(Duration::from_millis(500)); + } + }; + sync(&mut wollet, &mut client); + + assert!(tip >= 101); + + Self { + wollet, + db_root_dir, + client, + } + } + + pub fn tx_builder(&self) -> WolletTxBuilder { + self.wollet.tx_builder() + } + + pub fn db_root_dir(self) -> TempDir { + self.db_root_dir + } + + pub fn policy_asset(&self) -> AssetId { + self.wollet.policy_asset() + } + + pub fn tip(&self) -> Tip { + self.wollet.tip() + } + + pub fn sync(&mut self) { + sync(&mut self.wollet, &mut self.client); + } + + pub fn address(&self) -> Address { + self.wollet.address(None).unwrap().address().clone() + } + + pub fn address_result(&self, last_unused: Option) -> AddressResult { + self.wollet.address(last_unused).unwrap() + } + + /// Wait until tx appears in tx list (max 1 min) + fn wait_for_tx(&mut self, txid: &Txid) { + wait_for_tx(&mut self.wollet, &mut self.client, txid); + } + + /// Wait until the wallet has the transaction, although it might not be in the tx list + /// + /// This might be useful for explicit outputs or blinded outputs that cannot be unblinded. + pub fn wait_for_tx_outside_list(&mut self, txid: &Txid) { + for _ in 0..120 { + sync(&mut self.wollet, &mut self.client); + if self.wollet.transaction(txid).unwrap().is_some() { + return; + } + thread::sleep(Duration::from_millis(500)); + } + panic!("Wallet does not have {txid} in its list"); + } + + /// asset balance in satoshi + pub fn balance(&mut self, asset: &AssetId) -> u64 { + let balance = self.wollet.balance().unwrap(); + *balance.get(asset).unwrap_or(&0u64) + } + + pub fn balance_btc(&mut self) -> u64 { + self.balance(&self.wollet.policy_asset()) + } + + pub fn get_tx(&mut self, txid: &Txid) -> WalletTx { + self.wollet.transaction(txid).unwrap().unwrap() + } + + pub fn fund(&mut self, env: &TestEnv, satoshi: u64, address: Option
, asset: Option) { + let utxos_before = self.wollet.utxos().unwrap().len(); + let balance_before = self.balance(&asset.unwrap_or(self.policy_asset())); + + let address = address.unwrap_or_else(|| self.address()); + let txid = env.elementsd_sendtoaddress(&address, satoshi, asset); + self.wait_for_tx(&txid); + let tx = self.get_tx(&txid); + // We only received, all balances are positive + assert!(tx.balance.values().all(|v| *v > 0)); + assert_eq!(&tx.type_, "incoming"); + let wallet_txid = tx.tx.txid(); + assert_eq!(txid, wallet_txid); + assert_eq!(tx.inputs.iter().filter(|o| o.is_some()).count(), 0); + assert_eq!(tx.outputs.iter().filter(|o| o.is_some()).count(), 1); + + let utxos_after = self.wollet.utxos().unwrap().len(); + let balance_after = self.balance(&asset.unwrap_or(self.policy_asset())); + assert_eq!(utxos_after, utxos_before + 1); + assert_eq!(balance_before + satoshi, balance_after); + } + + pub fn fund_btc(&mut self, env: &TestEnv) { + self.fund(env, 1_000_000, Some(self.address()), None); + } + + pub fn fund_asset(&mut self, env: &TestEnv) -> AssetId { + let satoshi = 10_000; + let asset = env.elementsd_issueasset(satoshi); + self.fund(env, satoshi, Some(self.address()), Some(asset)); + asset + } + + pub fn fund_explicit(&mut self, env: &TestEnv, satoshi: u64, address: Option
, asset: Option) { + let explicit_utxos_before = self.wollet.explicit_utxos().unwrap().len(); + + let address = address.unwrap_or_else(|| self.address()).to_unconfidential(); + let txid = env.elementsd_sendtoaddress(&address, satoshi, asset); + self.wait_for_tx_outside_list(&txid); + + let explicit_utxos_after = self.wollet.explicit_utxos().unwrap().len(); + assert_eq!(explicit_utxos_after, explicit_utxos_before + 1); + } + + /// Send 10_000 satoshi to self with default fee rate. + /// + /// To specify a custom fee rate pass Some in `fee_rate` + /// To specify an external recipient specify the `to` parameter + pub fn send_btc(&mut self, signers: &[&AnySigner], fee_rate: Option, external: Option<(Address, u64)>) { + let balance_before = self.balance_btc(); + + let recipient = external.clone().unwrap_or((self.address(), 10_000)); + + let mut pset = self + .tx_builder() + .add_lbtc_recipient(&recipient.0, recipient.1) + .unwrap() + .fee_rate(fee_rate) + .finish() + .unwrap(); + pset = pset_rt(&pset); + + let details = self.wollet.get_details(&pset).unwrap(); + let fee = details.balance.fee as i64; + assert!(fee > 0); + let balance = match &external { + Some((_a, v)) => -fee - *v as i64, + None => -fee, + }; + assert_eq!(*details.balance.balances.get(&self.policy_asset()).unwrap(), balance); + assert_eq!(n_issuances(&details), 0); + assert_eq!(n_reissuances(&details), 0); + + for signer in signers { + self.sign(signer, &mut pset); + } + assert_fee_rate(compute_fee_rate(&pset), fee_rate); + let txid = self.send(&mut pset); + let balance_after = self.balance_btc(); + assert!(balance_before > balance_after); + let tx = self.get_tx(&txid); + // We only sent, so all balances are negative + assert!(tx.balance.values().all(|v| *v < 0)); + assert_eq!(&tx.type_, "outgoing"); + assert_eq!(tx.fee, fee as u64); + assert!(tx.inputs.iter().filter(|o| o.is_some()).count() > 0); + assert!(tx.outputs.iter().filter(|o| o.is_some()).count() > 0); + + self.wollet.descriptor().descriptor.for_each_key(|k| { + if let DescriptorPublicKey::XPub(x) = k { + if let Some(origin) = &x.origin { + assert_eq!(pset.global.xpub.get(&x.xkey).unwrap(), origin); + } + } + true + }); + } + + /// Send all L-BTC + pub fn send_all_btc(&mut self, signers: &[&AnySigner], fee_rate: Option, address: Address) { + let balance_before = self.balance_btc(); + + let mut pset = self + .tx_builder() + .drain_lbtc_wallet() + .drain_lbtc_to(address) + .fee_rate(fee_rate) + .finish() + .unwrap(); + + let details = self.wollet.get_details(&pset).unwrap(); + let fee = details.balance.fee as i64; + assert!(fee > 0); + assert_eq!( + *details.balance.balances.get(&self.policy_asset()).unwrap(), + -(balance_before as i64) + ); + + for signer in signers { + self.sign(signer, &mut pset); + } + self.send(&mut pset); + let balance_after = self.balance_btc(); + assert_eq!(balance_after, 0); + } + + pub fn send_asset( + &mut self, + signers: &[&AnySigner], + address: &Address, + asset: &AssetId, + fee_rate: Option, + ) -> Txid { + let balance_before = self.balance(asset); + let satoshi: u64 = 10; + let mut pset = self + .tx_builder() + .add_recipient(address, satoshi, *asset) + .unwrap() + .fee_rate(fee_rate) + .finish() + .unwrap(); + + pset = pset_rt(&pset); + + let details = self.wollet.get_details(&pset).unwrap(); + let fee = details.balance.fee as i64; + assert!(fee > 0); + assert_eq!(*details.balance.balances.get(&self.policy_asset()).unwrap(), -fee); + assert_eq!(*details.balance.balances.get(asset).unwrap(), -(satoshi as i64)); + assert_eq!(n_issuances(&details), 0); + assert_eq!(n_reissuances(&details), 0); + + for signer in signers { + self.sign(signer, &mut pset); + } + assert_fee_rate(compute_fee_rate(&pset), fee_rate); + let txid = self.send(&mut pset); + let balance_after = self.balance(asset); + assert!(balance_before > balance_after); + txid + } + + pub fn send_many( + &mut self, + signers: &[&AnySigner], + addr1: &Address, + asset1: &AssetId, + addr2: &Address, + asset2: &AssetId, + fee_rate: Option, + ) { + let balance1_before = self.balance(asset1); + let balance2_before = self.balance(asset2); + let addr1 = addr1.to_string(); + let addr2 = addr2.to_string(); + let ass1 = asset1.to_string(); + let ass2 = asset2.to_string(); + let addressees: Vec = vec![ + UnvalidatedRecipient { + satoshi: 1_000, + address: addr1, + asset: ass1, + }, + UnvalidatedRecipient { + satoshi: 2_000, + address: addr2, + asset: ass2, + }, + ]; + + let mut pset = self + .tx_builder() + .set_unvalidated_recipients(&addressees) + .unwrap() + .fee_rate(fee_rate) + .finish() + .unwrap(); + + pset = pset_rt(&pset); + + let details = self.wollet.get_details(&pset).unwrap(); + let fee = details.balance.fee as i64; + assert!(fee > 0); + // Checking the balance here has a bit too many cases: + // asset1,2 are btc, asset1,2 are equal, addr1,2 belong to the wallet + // Skipping the checks here + assert_eq!(n_issuances(&details), 0); + assert_eq!(n_reissuances(&details), 0); + + for signer in signers { + self.sign(signer, &mut pset); + } + assert_fee_rate(compute_fee_rate(&pset), fee_rate); + self.send(&mut pset); + let balance1_after = self.balance(asset1); + let balance2_after = self.balance(asset2); + assert!(balance1_before > balance1_after); + assert!(balance2_before > balance2_after); + } + + pub fn issueasset( + &mut self, + signers: &[&AnySigner], + satoshi_asset: u64, + satoshi_token: u64, + contract: Option<&str>, + fee_rate: Option, + ) -> (AssetId, AssetId) { + let balance_before = self.balance_btc(); + let contract = contract.map(|c| Contract::from_str(c).unwrap()); + let contract_hash = contract + .as_ref() + .map(|c| c.contract_hash().unwrap()) + .unwrap_or_else(|| ContractHash::from_slice(&[0u8; 32]).expect("static")); + let mut pset = self + .tx_builder() + .issue_asset(satoshi_asset, None, satoshi_token, None, contract) + .unwrap() + .fee_rate(fee_rate) + .finish() + .unwrap(); + pset = pset_rt(&pset); + + let issuance_input = &pset.inputs()[0].clone(); + let (asset, token) = issuance_input.issuance_ids(); + + let details = self.wollet.get_details(&pset).unwrap(); + assert_eq!(n_issuances(&details), 1); + assert_eq!(n_reissuances(&details), 0); + let issuance = &details.issuances[0]; + assert_eq!(asset, issuance.asset().unwrap()); + assert_eq!(token, issuance.token().unwrap()); + assert_eq!(satoshi_asset, issuance.asset_satoshi().unwrap_or(0)); + assert_eq!(satoshi_token, issuance.token_satoshi().unwrap()); + let fee = details.balance.fee as i64; + assert!(fee > 0); + assert_eq!(*details.balance.balances.get(&self.policy_asset()).unwrap(), -fee); + assert_eq!( + *details.balance.balances.get(&asset).unwrap_or(&0), + satoshi_asset as i64 + ); + assert_eq!( + *details.balance.balances.get(&token).unwrap_or(&0), + satoshi_token as i64 + ); + + for signer in signers { + self.sign(signer, &mut pset); + } + assert_fee_rate(compute_fee_rate(&pset), fee_rate); + let txid = self.send(&mut pset); + let tx = self.get_tx(&txid); + assert_eq!(&tx.type_, "issuance"); + + assert_eq!(self.balance(&asset), satoshi_asset); + assert_eq!(self.balance(&token), satoshi_token); + let balance_after = self.balance_btc(); + assert!(balance_before > balance_after); + + let issuance = self.wollet.issuance(&asset).unwrap(); + assert_eq!(issuance.vin, 0); + assert!(!issuance.is_reissuance); + assert_eq!(issuance.asset_amount.unwrap_or(0), satoshi_asset); + assert_eq!(issuance.token_amount.unwrap_or(0), satoshi_token); + + let prevout = OutPoint::new(issuance_input.previous_txid, issuance_input.previous_output_index); + assert_eq!(asset, AssetId::new_issuance(prevout, contract_hash)); + + (asset, token) + } + + pub fn reissueasset(&mut self, signers: &[&AnySigner], satoshi_asset: u64, asset: &AssetId, fee_rate: Option) { + let issuance = self.wollet.issuance(asset).unwrap(); + let balance_btc_before = self.balance_btc(); + let balance_asset_before = self.balance(asset); + let balance_token_before = self.balance(&issuance.token); + let mut pset = self + .tx_builder() + .reissue_asset(*asset, satoshi_asset, None, None) + .unwrap() + .fee_rate(fee_rate) + .finish() + .unwrap(); + pset = pset_rt(&pset); + + let details = self.wollet.get_details(&pset).unwrap(); + assert_eq!(n_issuances(&details), 0); + assert_eq!(n_reissuances(&details), 1); + let reissuance = details.issuances.iter().find(|e| e.is_reissuance()).unwrap(); + assert_eq!(asset, &reissuance.asset().unwrap()); + assert_eq!(issuance.token, reissuance.token().unwrap()); + assert_eq!(satoshi_asset, reissuance.asset_satoshi().unwrap()); + assert!(reissuance.token_satoshi().is_none()); + let fee = details.balance.fee as i64; + assert!(fee > 0); + assert_eq!(*details.balance.balances.get(&self.policy_asset()).unwrap(), -fee); + assert_eq!(*details.balance.balances.get(asset).unwrap(), satoshi_asset as i64); + assert!(!details.balance.balances.contains_key(&issuance.token)); + + for signer in signers { + self.sign(signer, &mut pset); + } + assert_fee_rate(compute_fee_rate(&pset), fee_rate); + let txid = self.send(&mut pset); + let tx = self.get_tx(&txid); + assert_eq!(&tx.type_, "reissuance"); + + assert_eq!(self.balance(asset), balance_asset_before + satoshi_asset); + assert_eq!(self.balance(&issuance.token), balance_token_before); + assert!(self.balance_btc() < balance_btc_before); + + let issuances = self.wollet.issuances().unwrap(); + assert!(issuances.len() > 1); + let reissuance = issuances.iter().find(|e| e.txid == txid).unwrap(); + assert!(reissuance.is_reissuance); + assert_eq!(reissuance.asset_amount, Some(satoshi_asset)); + assert!(reissuance.token_amount.is_none()); + } + + pub fn burnasset(&mut self, signers: &[&AnySigner], satoshi_asset: u64, asset: &AssetId, fee_rate: Option) { + let balance_btc_before = self.balance_btc(); + let balance_asset_before = self.balance(asset); + let mut pset = self + .tx_builder() + .add_burn(satoshi_asset, *asset) + .unwrap() + .fee_rate(fee_rate) + .finish() + .unwrap(); + pset = pset_rt(&pset); + + let details = self.wollet.get_details(&pset).unwrap(); + let fee = details.balance.fee as i64; + assert!(fee > 0); + let btc = self.policy_asset(); + let (expected_asset, expected_btc) = if asset == &btc { + (0, -(fee + satoshi_asset as i64)) + } else { + (-(satoshi_asset as i64), -fee) + }; + assert_eq!(*details.balance.balances.get(&btc).unwrap(), expected_btc); + assert_eq!(*details.balance.balances.get(asset).unwrap_or(&0), expected_asset); + assert_eq!(n_issuances(&details), 0); + assert_eq!(n_reissuances(&details), 0); + + for signer in signers { + self.sign(signer, &mut pset); + } + assert_fee_rate(compute_fee_rate(&pset), fee_rate); + let txid = self.send(&mut pset); + let tx = self.get_tx(&txid); + assert_eq!(&tx.type_, "burn"); + + assert_eq!(self.balance(asset), balance_asset_before - satoshi_asset); + assert!(self.balance_btc() < balance_btc_before); + } + + pub fn sign(&self, signer: &S, pset: &mut PartiallySignedTransaction) { + *pset = pset_rt(pset); + let sigs_added_or_overwritten = signer.sign(pset).unwrap(); + assert!(sigs_added_or_overwritten > 0); + } + + pub fn send(&mut self, pset: &mut PartiallySignedTransaction) -> Txid { + *pset = pset_rt(pset); + // TODO: check we that the tx has some signatures + // check_witnesses_non_empty does not cover the pre-segwit case anymore + // let tx_pre_finalize = pset.extract_tx().unwrap(); + // let err = self.client.broadcast(&tx_pre_finalize).unwrap_err(); + // assert!(matches!(err, lwk_wollet::Error::EmptyWitness)); + let tx = self.wollet.finalize(pset).unwrap(); + let txid = self.client.broadcast(&tx).unwrap(); + self.wait_for_tx(&txid); + txid + } + + pub fn send_outside_list(&mut self, pset: &mut PartiallySignedTransaction) -> Txid { + *pset = pset_rt(pset); + let tx = self.wollet.finalize(pset).unwrap(); + let txid = self.client.broadcast(&tx).unwrap(); + self.wait_for_tx_outside_list(&txid); + txid + } + + pub fn check_persistence(wollet: TestWollet) { + let descriptor = wollet.wollet.descriptor().to_string(); + let expected_updates = wollet.wollet.updates().unwrap(); + let expected = wollet.wollet.balance().unwrap(); + let db_root_dir = wollet.db_root_dir(); + let network = ElementsNetwork::default_regtest(); + + for _ in 0..2 { + let wollet = Wollet::with_fs_persist(network, descriptor.parse().unwrap(), &db_root_dir).unwrap(); + + let balance = wollet.balance().unwrap(); + assert_eq!(expected, balance); + assert_eq!(expected_updates, wollet.updates().unwrap()); + } + } + + pub fn wait_height(&mut self, height: u32) { + for _ in 0..120 { + sync(&mut self.wollet, &mut self.client); + if self.wollet.tip().height() == height { + return; + } + thread::sleep(Duration::from_millis(500)); + } + panic!("Wait for height {height} failed"); + } + + pub fn make_external(&mut self, utxo: &lwk_wollet::WalletTxOut) -> lwk_wollet::ExternalUtxo { + let tx = self.get_tx(&utxo.outpoint.txid).tx; + let txout = tx.output.get(utxo.outpoint.vout as usize).unwrap().clone(); + let tx = if self.wollet.is_segwit() { None } else { Some(tx) }; + lwk_wollet::ExternalUtxo { + outpoint: utxo.outpoint, + txout, + tx, + unblinded: utxo.unblinded, + max_weight_to_satisfy: self.wollet.max_weight_to_satisfy(), + } + } + + #[track_caller] + pub fn assert_spent_unspent(&self, spent: usize, unspent: usize) { + let txos = self.wollet.txos().unwrap(); + let spent_count = txos.iter().filter(|txo| txo.is_spent).count(); + let unspent_count = txos.iter().filter(|txo| !txo.is_spent).count(); + assert_eq!(spent_count, spent, "Wrong number of spent outputs"); + assert_eq!(unspent_count, unspent, "Wrong number of unspent outputs"); + assert_eq!(txos.len(), spent + unspent, "Wrong number of outputs"); + let utxos = self.wollet.utxos().unwrap(); + assert_eq!(utxos.len(), unspent, "Wrong number of unspent outputs"); + assert!(utxos.iter().all(|utxo| !utxo.is_spent)); + let txs = self.wollet.transactions().unwrap(); + let tx_outs_from_tx: Vec<_> = txs + .iter() + .flat_map(|tx| tx.outputs.iter()) + .filter_map(|o| o.as_ref()) + .collect(); + let spent_count_txs = tx_outs_from_tx.iter().filter(|o| o.is_spent).count(); + let unspent_count_txs = tx_outs_from_tx.iter().filter(|o| !o.is_spent).count(); + assert_eq!(spent_count_txs, spent); + assert_eq!(unspent_count_txs, unspent); + } +} From b4d59005b753e9a3b359abd2ecc7cc48ad6b6954 Mon Sep 17 00:00:00 2001 From: Illia Kripaka Date: Mon, 15 Dec 2025 20:13:04 +0200 Subject: [PATCH 3/4] try to transfer assets to p2pk address --- crates/lwk-utils/tests/p2pk_faucet.rs | 64 +++++++++ crates/lwk-utils/tests/testing_faucet.rs | 163 ++++++++++++++++++----- 2 files changed, 195 insertions(+), 32 deletions(-) create mode 100644 crates/lwk-utils/tests/p2pk_faucet.rs diff --git a/crates/lwk-utils/tests/p2pk_faucet.rs b/crates/lwk-utils/tests/p2pk_faucet.rs new file mode 100644 index 0000000..b89f1b5 --- /dev/null +++ b/crates/lwk-utils/tests/p2pk_faucet.rs @@ -0,0 +1,64 @@ +use anyhow::anyhow; +use lwk_common::Signer; +use lwk_wollet::Wollet; +use lwk_wollet::asyncr::EsploraClient; +use simplicity::elements::{Address, AssetId}; + +pub async fn faucet_p2pk_asset( + client: &mut EsploraClient, + signer: &impl Signer, + wollet: &mut Wollet, + recipient_address: &Address, + amount: u64, + asset: AssetId, +) -> anyhow::Result { + let update = client + .full_scan(wollet) + .await + .map_err(|e| anyhow!("Full scan failed: {}", e))?; + + if let Some(update) = update { + wollet + .apply_update(update) + .map_err(|e| anyhow!("Apply update failed: {}", e))?; + } + + let mut builder = wollet.tx_builder(); + + let is_confidential = recipient_address.to_string().starts_with("lq1"); + + if is_confidential { + builder = builder + .add_recipient(recipient_address, amount, asset) + .map_err(|e| anyhow!("Failed to add recipient: {}", e))?; + } else { + builder = builder + .add_explicit_recipient(recipient_address, amount, asset) + .map_err(|e| anyhow!("Failed to add explicit recipient: {}", e))?; + } + + // Build and sign the transaction + let mut unsigned_pset = builder + .finish() + .map_err(|e| anyhow!("Failed to build transaction: {}", e))?; + + let signed_pset = signer + .sign(&mut unsigned_pset) + .map_err(|e| anyhow!("Failed to sign transaction: {e:?}"))?; + + // Finalize and extract transaction + let finalized_pset = wollet + .finalize(&mut unsigned_pset) + .map_err(|e| anyhow!("Failed to finalize transaction: {}", e))?; + + // Broadcast transaction + let txid = client + .broadcast(&finalized_pset) + .await + .map_err(|e| anyhow!("Failed to broadcast transaction: {e:?}"))?; + + Ok(format!( + "Sent {} sats to address {} with transaction {}.", + amount, recipient_address, txid + )) +} diff --git a/crates/lwk-utils/tests/testing_faucet.rs b/crates/lwk-utils/tests/testing_faucet.rs index 6128d8e..d29f07b 100644 --- a/crates/lwk-utils/tests/testing_faucet.rs +++ b/crates/lwk-utils/tests/testing_faucet.rs @@ -1,21 +1,28 @@ mod faucet_contract; +mod p2pk_faucet; mod utils; use crate::faucet_contract::{TxInfo, issue_asset}; +use crate::p2pk_faucet::faucet_p2pk_asset; use crate::utils::{ TEST_LOGGER, TestWollet, generate_signer, get_descriptor, test_client_electrum, test_client_esplora, wait_update_with_txs, }; use elements::bitcoin::bip32::DerivationPath; +use elements::hex::ToHex; use lwk_signer::SwSigner; use lwk_test_util::{TestEnvBuilder, generate_view_key, regtest_policy_asset}; use lwk_wollet::asyncr::EsploraClient; use lwk_wollet::blocking::BlockchainBackend; +use lwk_wollet::elements_miniscript::ToPublicKey; use lwk_wollet::{ElementsNetwork, NoPersist, Wollet, WolletBuilder, WolletDescriptor}; use nostr::secp256k1::Secp256k1; use simplicity::bitcoin::secp256k1::Keypair; +use simplicityhl::elements::secp256k1_zkp::PublicKey; use simplicityhl::elements::{AddressParams, TxOut}; -use simplicityhl_core::{LIQUID_TESTNET_BITCOIN_ASSET, LIQUID_TESTNET_GENESIS, derive_public_blinder_key}; +use simplicityhl_core::{ + LIQUID_TESTNET_BITCOIN_ASSET, LIQUID_TESTNET_GENESIS, derive_public_blinder_key, get_p2pk_address, +}; use std::str::FromStr; const DEFAULT_MNEMONIC: &str = @@ -89,6 +96,13 @@ fn test_issue_custom2() -> anyhow::Result<()> { let address = wallet.wollet.address(Some(0))?; wallet.fund_btc(&env); + + let public_key = keypair.x_only_public_key().0; + let p2pk_address = get_p2pk_address(&public_key, &AddressParams::LIQUID_TESTNET)?; + + let txid = env.elementsd_sendtoaddress(&p2pk_address, 1_000_011, None); + println!("txid on p2pk address: {}", txid); + // wallet.fund( // &env, // 10_000_000, @@ -102,35 +116,6 @@ fn test_issue_custom2() -> anyhow::Result<()> { let external_utxos = wallet.wollet.explicit_utxos()?; println!("external_utxos: {:?}", external_utxos); - // let mut pset = issue_asset( - // &keypair, - // derive_public_blinder_key().public_key(), - // utxos[0].outpoint, - // 123456, - // 500, - // &AddressParams::LIQUID_TESTNET, - // LIQUID_TESTNET_BITCOIN_ASSET, - // *LIQUID_TESTNET_GENESIS, - // ) - // .await?; - - // let mut pset = tokio::runtime::Runtime::new()?.block_on(async { - // issue_asset( - // &keypair, - // derive_public_blinder_key().public_key(), - // utxos[0].outpoint, - // 123456, - // 500, - // &AddressParams::LIQUID_TESTNET, - // LIQUID_TESTNET_BITCOIN_ASSET, - // *LIQUID_TESTNET_GENESIS, - // ) - // .await - // })?; - - // let tx_to_send = wallet.wollet.finalize(&mut pset)?; - // wallet.client.broadcast(&tx_to_send)?; - wallet.sync(); let utxos = wallet.wollet.utxos()?; @@ -139,6 +124,120 @@ fn test_issue_custom2() -> anyhow::Result<()> { Ok(()) } +#[tokio::test] +async fn test_issue_custom2_p2pk() -> anyhow::Result<()> { + let _ = dotenvy::dotenv().ok(); + let _guard = &*TEST_LOGGER; + + let secp = Secp256k1::new(); + let env = TestEnvBuilder::from_env().with_esplora().build(); + let mut client = test_client_esplora(&env.esplora_url()); + + let signer = generate_signer(); + let view_key = generate_view_key(); + let regtest_bitcoin_asset = regtest_policy_asset(); + + let descriptor = format!("ct({},elwpkh({}/*))", view_key, signer.xpub()); + let network = ElementsNetwork::default_regtest(); + let descriptor: WolletDescriptor = descriptor.parse()?; + let mut wollet = WolletBuilder::new(network, descriptor).build()?; + let signer_keypair = Keypair::from_secret_key(&secp, &signer.derive_xprv(&DerivationPath::master())?.private_key); + + let update = client.full_scan(&wollet).await?; + if let Some(update) = update { + wollet.apply_update(update)?; + } + + println!("Utxos1: {:?}", wollet.utxos()?); + + let address = wollet.address(None)?; + let txid = env.elementsd_sendtoaddress(address.address(), 2_000_011, None); + println!("txid sendtoaddress: {}", txid); + + env.elementsd_generate(10); + let update = wait_update_with_txs(&mut client, &wollet).await; + wollet.apply_update(update)?; + println!("Utxos2: {:?}", wollet.utxos()?); + + let public_key = signer_keypair.x_only_public_key().0; + let p2pk_address = get_p2pk_address(&public_key, &AddressParams::ELEMENTS)?; + + env.elementsd_generate(10); + let update = client.full_scan(&wollet).await?; + if let Some(update) = update { + wollet.apply_update(update)?; + } + + println!("Utxos3: {:?}", wollet.utxos()?); + + let msg = faucet_p2pk_asset( + &mut client, + &signer, + &mut wollet, + &p2pk_address, + 1_000_000, + regtest_policy_asset(), + ) + .await?; + println!("txid on p2pk address: '{msg}'"); + println!("Utxos4: {:?}", wollet.utxos()?); + + let utxos = wollet.utxos()?; + println!("Utxos5: {:?}", utxos); + let asset_owned = wollet.assets_owned()?; + println!("asset_owned: {:?}", asset_owned); + let external_utxos = wollet.explicit_utxos()?; + println!("external_utxos: {:?}", external_utxos); + + let update = client.full_scan(&wollet).await?; + if let Some(update) = update { + wollet.apply_update(update)?; + } + + let utxos = wollet.utxos()?; + tracing::info!("Utxos after: {:?}", utxos); + + // retrieve utxos from pt2tr wallet + + let blinding_key = derive_public_blinder_key(); + let view_key = blinding_key.secret_bytes().to_hex(); + // let pk = signer_keypair.public_key().to_hex(); + let pk = "020202020202020202020202020202020202020202020202020202020202020202"; + let pubkey = PublicKey::from_str(pk).unwrap().to_x_only_pubkey(); + let p2pk_address = simplicityhl_core::get_p2pk_address(&pubkey, &AddressParams::ELEMENTS)?; + println!("pk for p2pk_address: {}, p2pk addr: {}", pk, p2pk_address); + + let desc = format!("ct({view_key},elwpkh({pk}))"); + println!("desc: {}", desc); + + let descriptor: WolletDescriptor = desc.parse()?; + let mut p2pk_wollet = WolletBuilder::new(network, descriptor).build()?; + + let update = client.full_scan(&p2pk_wollet).await?; + if let Some(update) = update { + p2pk_wollet.apply_update(update)?; + } + + println!("Utxos p2pk1: {:?}", p2pk_wollet.utxos()?); + + // + // w.fund_btc(&env); + // let balance = w.balance_btc(); + // assert!(balance > 0); + // let utxos = w.wollet.utxos().unwrap(); + // assert_eq!(utxos.len(), 1); + // + // // Receive unconfidential / explicit + // let satoshi = 5_000; + // w.fund_explicit(&env, satoshi, None, None); + // assert_eq!(w.balance_btc(), balance); + // + // let explicit_utxos = w.wollet.explicit_utxos().unwrap(); + // assert_eq!(explicit_utxos.len(), 1); + + Ok(()) +} + #[tokio::test] async fn async_test_issue_custom2() -> anyhow::Result<()> { let _ = dotenvy::dotenv().ok(); @@ -159,7 +258,7 @@ async fn async_test_issue_custom2() -> anyhow::Result<()> { let keypair = Keypair::from_secret_key(&secp, &signer.derive_xprv(&DerivationPath::master())?.private_key); let update = client.full_scan(&wollet).await?.unwrap(); - wollet.apply_update(update).unwrap(); + wollet.apply_update(update)?; let address = wollet.address(None)?; let txid = env.elementsd_sendtoaddress(address.address(), 1_000_011, None); @@ -197,7 +296,7 @@ async fn async_test_issue_custom2() -> anyhow::Result<()> { TxInfo { outpoint, wallet_tx }, 123456, 500, - &AddressParams::LIQUID_TESTNET, + &AddressParams::ELEMENTS, regtest_bitcoin_asset, *LIQUID_TESTNET_GENESIS, ) From 498e792ee308290896e9ec458f0396f09476225f Mon Sep 17 00:00:00 2001 From: Illia Kripaka Date: Fri, 19 Dec 2025 18:42:21 +0200 Subject: [PATCH 4/4] change test to check whether p2pk transaction is available for spending --- crates/lwk-utils/tests/p2pk_faucet.rs | 8 +++--- crates/lwk-utils/tests/testing_faucet.rs | 31 +++++++----------------- 2 files changed, 12 insertions(+), 27 deletions(-) diff --git a/crates/lwk-utils/tests/p2pk_faucet.rs b/crates/lwk-utils/tests/p2pk_faucet.rs index b89f1b5..044d8d0 100644 --- a/crates/lwk-utils/tests/p2pk_faucet.rs +++ b/crates/lwk-utils/tests/p2pk_faucet.rs @@ -3,6 +3,7 @@ use lwk_common::Signer; use lwk_wollet::Wollet; use lwk_wollet::asyncr::EsploraClient; use simplicity::elements::{Address, AssetId}; +use simplicityhl::elements::{Transaction, Txid}; pub async fn faucet_p2pk_asset( client: &mut EsploraClient, @@ -11,7 +12,7 @@ pub async fn faucet_p2pk_asset( recipient_address: &Address, amount: u64, asset: AssetId, -) -> anyhow::Result { +) -> anyhow::Result<(Txid, Transaction)> { let update = client .full_scan(wollet) .await @@ -57,8 +58,5 @@ pub async fn faucet_p2pk_asset( .await .map_err(|e| anyhow!("Failed to broadcast transaction: {e:?}"))?; - Ok(format!( - "Sent {} sats to address {} with transaction {}.", - amount, recipient_address, txid - )) + Ok((txid, finalized_pset)) } diff --git a/crates/lwk-utils/tests/testing_faucet.rs b/crates/lwk-utils/tests/testing_faucet.rs index d29f07b..522cafe 100644 --- a/crates/lwk-utils/tests/testing_faucet.rs +++ b/crates/lwk-utils/tests/testing_faucet.rs @@ -179,7 +179,8 @@ async fn test_issue_custom2_p2pk() -> anyhow::Result<()> { regtest_policy_asset(), ) .await?; - println!("txid on p2pk address: '{msg}'"); + println!("txid on p2pk address: '{}', addr: {p2pk_address}", msg.0); + wollet.apply_transaction(msg.1)?; println!("Utxos4: {:?}", wollet.utxos()?); let utxos = wollet.utxos()?; @@ -199,15 +200,13 @@ async fn test_issue_custom2_p2pk() -> anyhow::Result<()> { // retrieve utxos from pt2tr wallet - let blinding_key = derive_public_blinder_key(); - let view_key = blinding_key.secret_bytes().to_hex(); - // let pk = signer_keypair.public_key().to_hex(); - let pk = "020202020202020202020202020202020202020202020202020202020202020202"; - let pubkey = PublicKey::from_str(pk).unwrap().to_x_only_pubkey(); + let view_key = view_key; + let pk = signer_keypair.public_key().to_hex(); + let pubkey = PublicKey::from_str(&pk)?.to_x_only_pubkey(); let p2pk_address = simplicityhl_core::get_p2pk_address(&pubkey, &AddressParams::ELEMENTS)?; - println!("pk for p2pk_address: {}, p2pk addr: {}", pk, p2pk_address); + println!("pk for p2pk_address: {}, p2pk addr: {}", pubkey, p2pk_address); - let desc = format!("ct({view_key},elwpkh({pk}))"); + let desc = format!("ct({view_key},elwpkh({pubkey}))"); println!("desc: {}", desc); let descriptor: WolletDescriptor = desc.parse()?; @@ -220,20 +219,8 @@ async fn test_issue_custom2_p2pk() -> anyhow::Result<()> { println!("Utxos p2pk1: {:?}", p2pk_wollet.utxos()?); - // - // w.fund_btc(&env); - // let balance = w.balance_btc(); - // assert!(balance > 0); - // let utxos = w.wollet.utxos().unwrap(); - // assert_eq!(utxos.len(), 1); - // - // // Receive unconfidential / explicit - // let satoshi = 5_000; - // w.fund_explicit(&env, satoshi, None, None); - // assert_eq!(w.balance_btc(), balance); - // - // let explicit_utxos = w.wollet.explicit_utxos().unwrap(); - // assert_eq!(explicit_utxos.len(), 1); + assert!(p2pk_wollet.utxos().is_ok()); + assert_eq!(p2pk_wollet.utxos().unwrap().len(), 1); Ok(()) }