arch_sdk 0.7.0

A Rust SDK for building applications on the Arch Network blockchain platform. Provides tools and interfaces for developing, testing, and deploying programs with native Bitcoin integration.
Documentation
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_raw_transaction(&txid, None)
                .map_err(|e| format!("Failed to get raw 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 get_account_address_string(&self, pubkey: Pubkey) -> Result<String, String> {
        self.get_account_address(pubkey)
            .await
            .map_err(|e| e.to_string())
    }

    // pub fn rpc_client(&self) -> &Arc<Client> {
    //     &self.rpc_client
    // }

    // pub fn network(&self) -> Network {
    //     self.network
    // }

    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())
}