use crate::client::ArchRpcClient;
use crate::ArchError;
use crate::Config;
use crate::{arch_program::pubkey::Pubkey, with_secret_key_file};
use bitcoin::{address::Address, Amount, Network};
use bitcoincore_rpc::{Auth, Client, RpcApi};
use std::str::FromStr;
use std::sync::Arc;
use titan_client::{TitanApi, TitanClient};
#[derive(Clone)]
pub struct BitcoinHelper {
network: Network,
rpc_client: Arc<Client>,
arch_client: ArchRpcClient,
titan_client: TitanClient,
}
impl BitcoinHelper {
pub fn new(config: &Config) -> Result<Self, ArchError> {
let userpass = Auth::UserPass(config.node_username.clone(), config.node_password.clone());
let rpc_client = Arc::new(
Client::new(&config.node_endpoint, userpass)
.map_err(|e| ArchError::BitcoinRpcError(e.to_string()))?,
);
let arch_client = ArchRpcClient::new(config);
Ok(Self {
network: config.network,
rpc_client,
arch_client,
titan_client: TitanClient::new(config.titan_url.as_str()),
})
}
async fn get_account_address(&self, pubkey: Pubkey) -> Result<String, ArchError> {
self.arch_client.get_account_address(&pubkey).await
}
pub async fn send_utxo(&self, pubkey: Pubkey) -> Result<(String, u32), String> {
self.send_utxo_with_amount(pubkey, 3000).await
}
pub async fn send_utxo_with_amount(
&self,
pubkey: Pubkey,
amount_sats: u64,
) -> Result<(String, u32), String> {
let address = self
.get_account_address(pubkey)
.await
.map_err(|e| e.to_string())?;
let account_address = match Address::from_str(&address) {
Ok(addr) => match addr.require_network(self.network) {
Ok(addr) => addr,
Err(e) => return Err(format!("Network mismatch for address: {}", e)),
},
Err(e) => return Err(format!("Failed to parse address: {}", e)),
};
let rpc = self.rpc_client.clone();
let addr_clone = account_address.clone();
let (txid, sent_tx) = tokio::task::spawn_blocking(move || {
let txid = rpc
.send_to_address(
&addr_clone,
Amount::from_sat(amount_sats),
None,
None,
None,
None,
None,
None,
)
.map_err(|e| format!("Failed to send to address: {}", e))?;
let sent_tx = rpc
.get_transaction(&txid, None)
.map_err(|e| format!("Failed to get wallet transaction: {e}"))?
.transaction()
.map_err(|e| format!("Failed to decode wallet transaction: {e}"))?;
Ok::<_, String>((txid, sent_tx))
})
.await
.map_err(|e| format!("spawn_blocking join error: {}", e))??;
let mut vout = 0;
for (index, output) in sent_tx.output.iter().enumerate() {
if output.script_pubkey == account_address.script_pubkey() {
vout = index as u32;
}
}
self.wait_until_titan_indexes_transaction(&txid).await?;
Ok((txid.to_string(), vout))
}
pub async fn send_confirmed_utxo(&self, pubkey: Pubkey) -> Result<(String, u32), String> {
const REGTEST_MINED_DEPTH: u64 = 3;
let (txid, vout) = self.send_utxo(pubkey).await?;
let parsed_txid =
bitcoin::Txid::from_str(&txid).map_err(|e| format!("Invalid transaction ID: {e}"))?;
let min_confirmations = if self.network == Network::Regtest {
let rpc = self.rpc_client.clone();
tokio::task::spawn_blocking(move || {
let address = rpc
.get_new_address(None, None)
.map_err(|e| format!("Failed to get mining address: {e}"))?
.require_network(Network::Regtest)
.map_err(|e| format!("Invalid regtest mining address: {e}"))?;
rpc.generate_to_address(REGTEST_MINED_DEPTH, &address)
.map_err(|e| format!("Failed to mine confirmation: {e}"))?;
Ok::<_, String>(())
})
.await
.map_err(|e| format!("spawn_blocking join error: {e}"))??;
REGTEST_MINED_DEPTH
} else {
1
};
for _ in 0..60 {
let status = self.titan_client.get_transaction_status(&parsed_txid).await;
if let Ok(status) = status {
let deep_enough = match (status.confirmed, status.block_height) {
(true, Some(height)) => self.titan_client.get_tip().await.is_ok_and(|tip| {
tip.height.saturating_sub(height) + 1 >= min_confirmations
}),
(true, None) => min_confirmations <= 1,
(false, _) => false,
};
if deep_enough {
return Ok((txid, vout));
}
}
tokio::time::sleep(std::time::Duration::from_secs(1)).await;
}
Err(format!(
"transaction {txid} did not reach {min_confirmations} Titan-visible \
confirmation(s) within 60s"
))
}
pub async fn get_account_address_string(&self, pubkey: Pubkey) -> Result<String, String> {
self.get_account_address(pubkey)
.await
.map_err(|e| e.to_string())
}
pub async fn wait_until_titan_indexes_transaction(
&self,
txid: &bitcoin::Txid,
) -> Result<(), String> {
for _ in 0..60 {
if self.titan_client.get_transaction(txid).await.is_ok() {
return Ok(());
}
tokio::time::sleep(std::time::Duration::from_secs(1)).await;
}
Err("Failed to wait for transaction to be indexed".to_string())
}
}
pub async fn prepare_fees() -> Result<String, ArchError> {
use bitcoin::{
absolute::LockTime,
address::Address,
key::{TapTweak, TweakedKeypair, XOnlyPublicKey},
secp256k1::{self, Secp256k1},
sighash::{Prevouts, SighashCache},
transaction::Version,
Amount, OutPoint, ScriptBuf, Sequence, TapSighashType, Transaction, TxIn, Witness,
};
use bitcoincore_rpc::{Auth, Client, RawTx, RpcApi};
let test_config = crate::Config::localnet();
let userpass = Auth::UserPass(
test_config.node_username.to_string(),
test_config.node_password.to_string(),
);
let rpc = Arc::new(
Client::new(&test_config.node_endpoint, userpass)
.map_err(|e| ArchError::BitcoinRpcError(e.to_string()))?,
);
let (key_pair, _) = with_secret_key_file(".program")?;
let (public_key, _) = XOnlyPublicKey::from_keypair(&key_pair);
let secp = Secp256k1::new();
let address = Address::p2tr(&secp, public_key, None, test_config.network);
let rpc_clone = rpc.clone();
let addr_clone = address.clone();
let txid = tokio::task::spawn_blocking(move || {
rpc_clone
.send_to_address(
&addr_clone,
Amount::from_sat(100000),
None,
None,
None,
None,
None,
None,
)
.map_err(|e| ArchError::BitcoinRpcError(e.to_string()))
})
.await
.map_err(|e| ArchError::BitcoinRpcError(e.to_string()))??;
let rpc_clone = rpc.clone();
let txid_clone = txid;
let sent_tx = tokio::task::spawn_blocking(move || {
rpc_clone
.get_raw_transaction(&txid_clone, None)
.map_err(|e| ArchError::BitcoinRpcError(e.to_string()))
})
.await
.map_err(|e| ArchError::BitcoinRpcError(e.to_string()))??;
let mut vout: u32 = 0;
for (index, output) in sent_tx.output.iter().enumerate() {
if output.script_pubkey == address.script_pubkey() {
vout = index as u32;
}
}
let mut tx = Transaction {
version: Version::TWO,
input: vec![TxIn {
previous_output: OutPoint { txid, vout },
script_sig: ScriptBuf::new(),
sequence: Sequence::MAX,
witness: Witness::new(),
}],
output: vec![],
lock_time: LockTime::ZERO,
};
let sighash_type = TapSighashType::NonePlusAnyoneCanPay;
let prevouts = vec![sent_tx.output[vout as usize].clone()];
let prevouts = Prevouts::All(&prevouts);
let mut sighasher = SighashCache::new(&mut tx);
let sighash = sighasher
.taproot_key_spend_signature_hash(0, &prevouts, sighash_type)
.map_err(|e| ArchError::BitcoinRpcError(e.to_string()))?;
let secp = Secp256k1::new();
let tweaked: TweakedKeypair = key_pair.tap_tweak(&secp, None);
let msg = secp256k1::Message::from(sighash);
let signature = secp.sign_schnorr(&msg, &tweaked.to_keypair());
let signature = bitcoin::taproot::Signature {
signature,
sighash_type,
};
tx.input[0].witness.push(signature.to_vec());
Ok(tx.raw_hex())
}