use std::{collections::HashMap, str::FromStr}; use bitcoincore_rpc::bitcoin::secp256k1::PublicKey; use bitcoincore_rpc::json::{self as bitcoin_json}; use sdk_common::silentpayments::sign_transaction; use sdk_common::sp_client::bitcoin::secp256k1::{ rand::thread_rng, Keypair, Message as Secp256k1Message, Secp256k1, ThirtyTwoByteHash, }; use sdk_common::sp_client::bitcoin::{ absolute::LockTime, consensus::serialize, hex::{DisplayHex, FromHex}, key::TapTweak, script::PushBytesBuf, sighash::{Prevouts, SighashCache}, taproot::Signature, transaction::Version, Amount, OutPoint, Psbt, ScriptBuf, TapSighashType, Transaction, TxIn, TxOut, Witness, XOnlyPublicKey, }; use sdk_common::{ network::{FaucetMessage, NewTxMessage}, silentpayments::create_transaction, }; use sdk_common::sp_client::silentpayments::sending::generate_recipient_pubkeys; use sdk_common::sp_client::silentpayments::utils::sending::calculate_partial_secret; use sdk_common::sp_client::{FeeRate, OwnedOutput, Recipient, RecipientAddress}; use anyhow::{Error, Result}; use crate::lock_freezed_utxos; use crate::scan::check_transaction_alone; use crate::{ scan::compute_partial_tweak_to_transaction, MutexExt, SilentPaymentAddress, DAEMON, FAUCET_AMT, WALLET, }; fn spend_from_core(dest: XOnlyPublicKey) -> Result<(Transaction, Amount)> { let core = DAEMON .get() .ok_or(Error::msg("DAEMON not initialized"))? .lock_anyhow()?; let unspent_list: Vec = core.list_unspent_from_to(None)?; if !unspent_list.is_empty() { let network = core.get_network()?; let spk = ScriptBuf::new_p2tr_tweaked(dest.dangerous_assume_tweaked()); let new_psbt = core.create_psbt(&unspent_list, spk, network)?; let processed_psbt = core.process_psbt(new_psbt)?; let finalize_psbt_result = core.finalize_psbt(processed_psbt)?; let final_psbt = Psbt::from_str(&finalize_psbt_result)?; let total_fee = final_psbt.fee()?; let final_tx = final_psbt.extract_tx()?; let fee_rate = total_fee .checked_div(final_tx.weight().to_vbytes_ceil()) .unwrap(); Ok((final_tx, fee_rate)) } else { // we don't have enough available coins to pay for this faucet request Err(Error::msg("No spendable outputs")) } } fn faucet_send( sp_address: SilentPaymentAddress, commitment: &str, ) -> Result<(Transaction, PublicKey)> { let sp_wallet = WALLET .get() .ok_or(Error::msg("Wallet not initialized"))? .lock_anyhow()?; let fee_estimate = DAEMON .get() .ok_or(Error::msg("DAEMON not initialized"))? .lock_anyhow()? .estimate_fee(6) .unwrap_or(Amount::from_sat(1000)) .checked_div(1000) .unwrap(); log::debug!("fee estimate for 6 blocks: {}", fee_estimate); let recipient = Recipient { address: RecipientAddress::SpAddress(sp_address), amount: FAUCET_AMT, }; let freezed_utxos = lock_freezed_utxos()?; // We filter out the freezed utxos from available list let available_outpoints: Vec<(OutPoint, OwnedOutput)> = sp_wallet .get_unspent_outputs() .iter() .filter_map(|(outpoint, output)| { if !freezed_utxos.contains(&outpoint) { Some((*outpoint, output.clone())) } else { None } }) .collect(); // If we had mandatory inputs, we would make sure to put them at the top of the list // We don't care for faucet though // We try to pay the faucet amount if let Ok(unsigned_transaction) = create_transaction( available_outpoints, sp_wallet.get_sp_client(), vec![recipient], Some(Vec::from_hex(commitment).unwrap()), FeeRate::from_sat_per_vb(fee_estimate.to_sat() as f32), ) { let final_tx = sign_transaction(sp_wallet.get_sp_client(), unsigned_transaction)?; let partial_tweak = compute_partial_tweak_to_transaction(&final_tx)?; let daemon = DAEMON .get() .ok_or(Error::msg("DAEMON not initialized"))? .lock_anyhow()?; // First check that mempool accept it daemon.test_mempool_accept(&final_tx)?; let txid = daemon.broadcast(&final_tx)?; log::debug!("Sent tx {}", txid); // We immediately add the new tx to our wallet to prevent accidental double spend check_transaction_alone(sp_wallet, &final_tx, &partial_tweak)?; Ok((final_tx, partial_tweak)) } else { // let's try to spend directly from the mining address let secp = Secp256k1::signing_only(); let keypair = Keypair::new(&secp, &mut thread_rng()); // we first spend from core to the pubkey we just created let (core_tx, fee_rate) = spend_from_core(keypair.x_only_public_key().0)?; // check that the first output of the transaction pays to the key we just created debug_assert!( core_tx.output[0].script_pubkey == ScriptBuf::new_p2tr_tweaked( keypair.x_only_public_key().0.dangerous_assume_tweaked() ) ); // This is ugly and can be streamlined // create a new transaction that spends the newly created UTXO to the sp_address let mut faucet_tx = Transaction { input: vec![TxIn { previous_output: OutPoint::new(core_tx.txid(), 0), ..Default::default() }], output: vec![], version: Version::TWO, lock_time: LockTime::ZERO, }; // now do the silent payment operations with the final recipient address let partial_secret = calculate_partial_secret( &[(keypair.secret_key(), true)], &[(core_tx.txid().to_string(), 0)], )?; let ext_output_key: XOnlyPublicKey = generate_recipient_pubkeys(vec![sp_address.into()], partial_secret)? .into_values() .flatten() .collect::>() .get(0) .expect("Failed to generate keys") .to_owned(); let change_sp_address = sp_wallet.get_sp_client().get_receiving_address(); let change_output_key: XOnlyPublicKey = generate_recipient_pubkeys(vec![change_sp_address], partial_secret)? .into_values() .flatten() .collect::>() .get(0) .expect("Failed to generate keys") .to_owned(); let ext_spk = ScriptBuf::new_p2tr_tweaked(ext_output_key.dangerous_assume_tweaked()); let change_spk = ScriptBuf::new_p2tr_tweaked(change_output_key.dangerous_assume_tweaked()); let mut op_return = PushBytesBuf::new(); op_return.extend_from_slice(&Vec::from_hex(commitment)?)?; let data_spk = ScriptBuf::new_op_return(op_return); // Take some margin to pay for the fees if core_tx.output[0].value < FAUCET_AMT * 4 { return Err(Error::msg("Not enough funds")); } let change_amt = core_tx.output[0].value.checked_sub(FAUCET_AMT).unwrap(); faucet_tx.output.push(TxOut { value: FAUCET_AMT, script_pubkey: ext_spk, }); faucet_tx.output.push(TxOut { value: change_amt, script_pubkey: change_spk, }); faucet_tx.output.push(TxOut { value: Amount::from_sat(0), script_pubkey: data_spk, }); // dummy signature only used for fee estimation faucet_tx.input[0].witness.push([1; 64].to_vec()); let abs_fee = fee_rate .checked_mul(faucet_tx.weight().to_vbytes_ceil()) .ok_or_else(|| Error::msg("Fee rate multiplication overflowed"))?; // reset the witness to empty faucet_tx.input[0].witness = Witness::new(); faucet_tx.output[1].value -= abs_fee; let first_tx_outputs = vec![core_tx.output[0].clone()]; let prevouts = Prevouts::All(&first_tx_outputs); let hash_ty = TapSighashType::Default; let mut cache = SighashCache::new(&faucet_tx); let sighash = cache.taproot_key_spend_signature_hash(0, &prevouts, hash_ty)?; let msg = Secp256k1Message::from_digest(sighash.into_32()); let sig = secp.sign_schnorr_with_rng(&msg, &keypair, &mut thread_rng()); let final_sig = Signature { sig, hash_ty }; faucet_tx.input[0].witness.push(final_sig.to_vec()); { let daemon = DAEMON .get() .ok_or(Error::msg("DAEMON not initialized"))? .lock_anyhow()?; // We don't worry about core_tx being refused by core daemon.broadcast(&core_tx)?; daemon.test_mempool_accept(&faucet_tx)?; let txid = daemon.broadcast(&faucet_tx)?; log::debug!("Sent tx {}", txid); } let partial_tweak = compute_partial_tweak_to_transaction(&faucet_tx)?; check_transaction_alone(sp_wallet, &faucet_tx, &partial_tweak)?; Ok((faucet_tx, partial_tweak)) } } pub fn handle_faucet_request(msg: &FaucetMessage) -> Result { let sp_address = SilentPaymentAddress::try_from(msg.sp_address.as_str())?; log::debug!("Sending bootstrap coins to {}", sp_address); // send bootstrap coins to this sp_address let (tx, partial_tweak) = faucet_send(sp_address, &msg.commitment)?; Ok(NewTxMessage::new( serialize(&tx).to_lower_hex_string(), Some(partial_tweak.to_string()), )) }