rostrum 8.0.0

An efficient implementation of Electrum Server with token support
Documentation
use crate::cache::TransactionCache;
use crate::chaindef::BlockHash;
use crate::chaindef::Transaction;
use crate::chaindef::TxOut;
use crate::daemon::Daemon;
use crate::def::COIN;
use crate::mempool::ConfirmationState;
use crate::mempool::Tracker;

use crate::query::header::HeaderQuery;
use crate::rpc::encoding::blockhash_to_hex;
use anyhow::{Context, Result};
use bitcoincash::blockdata::script::Script;
use bitcoincash::consensus::encode::{deserialize, serialize};
use bitcoincash::hash_types::Txid;
use bitcoincash::hashes::hex::ToHex;
use bitcoincash::network::constants::Network;
use bitcoincash::util::address::{Address, AddressType};
use rust_decimal::prelude::*;
use serde_json::Value;
use std::sync::{Arc, RwLock};

///  String returned is intended to be the same as produced by bitcoind
///  GetTxnOutputType
fn get_address_type(out: &TxOut, network: Network) -> Option<&str> {
    #[cfg(feature = "nexa")]
    {
        use crate::nexa::transaction::TxOutType;
        if out.txout_type == (TxOutType::TEMPLATE as u8) {
            return Some("scripttemplate");
        }
    }

    let script = &out.script_pubkey;
    if script.is_op_return() {
        return Some("nulldata");
    }
    let address = Address::from_script(script, network).ok()?;
    let address_type = address.address_type();
    match address_type {
        Some(AddressType::P2pkh) => Some("pubkeyhash"),
        Some(AddressType::P2sh) => Some("scripthash"),
        _ => {
            if !address.is_standard() {
                Some("nonstandard")
            } else {
                None
            }
        }
    }
}

#[cfg(not(feature = "nexa"))]
fn get_addresses(script: &Script, network: Network) -> Vec<String> {
    use crate::bch::cashaddr::encode as bchaddr_encode;
    use crate::bch::cashaddr::version_byte_flags;
    use bitcoincash::util::address::Payload::{PubkeyHash, ScriptHash};
    let address = match Address::from_script(script, network).ok() {
        Some(a) => a,
        None => return vec![],
    };

    match address.payload {
        PubkeyHash(pubhash) => {
            let hash = pubhash.as_hash().to_vec();
            let encoded = bchaddr_encode(&hash, version_byte_flags::TYPE_P2PKH, network);
            let encoded_token =
                bchaddr_encode(&hash, version_byte_flags::TYPE_P2PKH_TOKEN, network);
            match encoded {
                Ok(addr) => {
                    // If it was P2PKH encoded, then it can also be token aware.
                    let addr_token = encoded_token.unwrap();
                    vec![addr, addr_token]
                }
                _ => vec![],
            }
        }
        ScriptHash(scripthash) => {
            let hash = scripthash.as_hash().to_vec();
            let encoded = bchaddr_encode(&hash, version_byte_flags::TYPE_P2SH, network);
            let encoded_token = bchaddr_encode(&hash, version_byte_flags::TYPE_P2SH_TOKEN, network);
            match encoded {
                Ok(addr) => {
                    // If it was P2SH encoded, then it can also be token aware.
                    let addr_token = encoded_token.unwrap();
                    vec![addr, addr_token]
                }
                _ => vec![],
            }
        }
        _ => vec![],
    }
}

#[cfg(feature = "nexa")]
fn get_addresses(_script: &Script, _network: Network) -> Vec<String> {
    // NYI
    vec![]
}

fn value_from_amount(amount: u64) -> Value {
    if amount == 0 {
        return json!(0.0);
    }
    let satoshis = Decimal::new(amount as i64, 0);
    // rust-decimal crate with feature 'serde-float' should make this work
    // without introducing precision errors
    json!(satoshis.checked_div(Decimal::new(COIN as i64, 0)).unwrap())
}

pub struct TxQuery {
    tx_cache: TransactionCache,
    daemon: Daemon,
    mempool: Arc<RwLock<Tracker>>,
    header: Arc<HeaderQuery>,
    duration: Arc<prometheus::HistogramVec>,
    network: Network,
}

impl TxQuery {
    pub fn new(
        tx_cache: TransactionCache,
        daemon: Daemon,
        mempool: Arc<RwLock<Tracker>>,
        header: Arc<HeaderQuery>,
        duration: Arc<prometheus::HistogramVec>,
        network: Network,
    ) -> TxQuery {
        TxQuery {
            tx_cache,
            daemon,
            mempool,
            header,
            duration,
            network,
        }
    }

    /// Get a transaction by Txid.
    pub fn get(
        &self,
        txid: &Txid,
        blockhash: Option<&BlockHash>,
        blockheight: Option<u32>,
    ) -> Result<Transaction> {
        let _timer = self.duration.with_label_values(&["load_txn"]).start_timer();
        if let Some(tx) = self.tx_cache.get(txid) {
            return Ok(tx);
        }
        let hash: Option<BlockHash> = match blockhash {
            Some(hash) => Some(*hash),
            None => match self.header.get_by_txid(*txid, blockheight) {
                Ok(header) => header.map(|h| h.block_hash()),
                Err(_) => None,
            },
        };
        self.load_txn_from_bitcoind(txid, hash.as_ref())
    }

    /// Get an transaction known to be unconfirmed.
    ///
    /// This is slightly faster than `get` as it avoids blockhash lookup. May
    /// or may not return the transaction even if it is confirmed.
    pub fn get_unconfirmed(&self, txid: &Txid) -> Result<Transaction> {
        if let Some(tx) = self.tx_cache.get(txid) {
            Ok(tx)
        } else {
            self.load_txn_from_bitcoind(txid, None)
        }
    }

    #[cfg(not(feature = "nexa"))]
    fn inputs_to_json(&self, tx: &Transaction) -> Vec<Value> {
        tx.input.iter().map(|txin| json!({
            // bitcoind adds scriptSig hex as 'coinbase' when the transaction is a coinbase
            "coinbase": if tx.is_coin_base() { Some(txin.script_sig.to_hex()) } else { None },
            "sequence": txin.sequence,
            "txid": txin.previous_output.txid.to_hex(),
            "vout": txin.previous_output.vout,
            "scriptSig": {
                "asm": txin.script_sig.asm(),
                "hex": txin.script_sig.to_hex(),
            },
        })).collect::<Vec<Value>>()
    }

    #[cfg(feature = "nexa")]
    fn inputs_to_json(&self, tx: &Transaction) -> Vec<Value> {
        tx.input.iter().map(|txin| {
            let mut outpoint = txin.previous_output.hash.to_vec();
            outpoint.reverse();

            json!({
            // bitcoind adds scriptSig hex as 'coinbase' when the transaction is a coinbase
            "coinbase": if tx.is_coin_base() { Some(txin.script_sig.to_hex()) } else { None },
            "sequence": txin.sequence,
            "outpoint": outpoint.to_hex(),
            "scriptSig": {
                "asm": txin.script_sig.asm(),
                "hex": txin.script_sig.to_hex(),
            },
        })}).collect::<Vec<Value>>()
    }

    #[cfg(feature = "nexa")]
    fn outputs_to_json(&self, tx: &Transaction) -> Vec<Value> {
        use crate::nexa::{token::parse_token_from_scriptpubkey, transaction::TxOutType};

        tx.output.iter().enumerate().map(|(n, txout)| {

            let group = if txout.txout_type == (TxOutType::TEMPLATE as u8) {
                parse_token_from_scriptpubkey(&txout.script_pubkey)
            } else {
                None
            };

            json!({
            "type": txout.txout_type,
            "value": value_from_amount(txout.value),
            "value_satoshi": txout.value,
            "value_coin": value_from_amount(txout.value),
            "n": n,
            "scriptPubKey": {
                "asm": txout.script_pubkey.asm(),
                "hex": txout.script_pubkey.to_hex(),
                "type": get_address_type(txout, self.network).unwrap_or_default(),
                "addresses": get_addresses(&txout.script_pubkey, self.network),
                "groupTokenID": if let Some((id, _)) = group.as_ref() { Some(id.to_hex()) } else { None },
                "groupQuantity": if let Some((_, amount)) = group { Some(amount) } else { None },
            }
        })
    }).collect::<Vec<Value>>()
    }

    #[cfg(not(feature = "nexa"))]
    fn outputs_to_json(&self, tx: &Transaction) -> Vec<Value> {
        tx.output
            .iter()
            .enumerate()
            .map(|(n, txout)| {
                json!({
                "value_satoshi": txout.value,
                "value_coin": value_from_amount(txout.value),
                "value": value_from_amount(txout.value),
                "n": n,
                "scriptPubKey": {
                    "asm": txout.script_pubkey.asm(),
                    "hex": txout.script_pubkey.to_hex(),
                    "type": get_address_type(txout, self.network).unwrap_or_default(),
                    "addresses": get_addresses(&txout.script_pubkey, self.network),
                },
                })
            })
            .collect::<Vec<Value>>()
    }

    pub fn get_verbose(&self, txid: &Txid) -> Result<Value> {
        let header = self.header.get_by_txid(*txid, None).unwrap_or_default();
        let blocktime = header.as_ref().map(|header| header.time);
        let height = header
            .as_ref()
            .map(|header| self.header.get_height(header).unwrap());
        let confirmations = match height {
            Some(ref height) => {
                let best = self.header.best();
                let best_height = self.header.get_height(&best).unwrap();

                Some(1 + best_height - height)
            }
            None => None,
        };
        let (blockhash, blockhash_hex) = if let Some(h) = header {
            let hash = h.block_hash();
            (Some(hash), Some(blockhash_to_hex(&hash)))
        } else {
            (None, None)
        };
        let tx = self.get(txid, blockhash.as_ref(), None)?;
        let tx_serialized = serialize(&tx);

        #[allow(unused_mut)]
        let mut tx_details = json!({
            "blockhash": blockhash_hex,
            "blocktime": blocktime,
            "height": height,
            "confirmations": confirmations,
            "hash": tx.txid().to_hex(),
            "txid": tx.txid().to_hex(),
            "size": tx_serialized.len(),
            "hex": hex::encode(tx_serialized),
            "locktime": tx.lock_time,
            "time": blocktime,
            "version": tx.version,
            "vin": self.inputs_to_json(&tx),
            "vout": self.outputs_to_json(&tx),
        });

        #[cfg(feature = "nexa")]
        {
            tx_details
                .as_object_mut()
                .unwrap()
                .insert("txidem".to_string(), json!(tx.txidem().to_hex()));
        }

        Ok(json!(tx_details))
    }

    fn load_txn_from_bitcoind(
        &self,
        txid: &Txid,
        blockhash: Option<&BlockHash>,
    ) -> Result<Transaction> {
        let value: Value = self
            .daemon
            .gettransaction_raw(txid, blockhash, /*verbose*/ false)?;
        let value_hex: &str = value.as_str().context("non-string tx")?;
        let serialized_tx = hex::decode(value_hex).context("non-hex tx")?;
        let tx: Transaction =
            deserialize(&serialized_tx).context("failed to parse serialized tx")?;
        if txid != &tx.txid() {
            // This can happen on nexad, where `getrawtransaction` RPC call supports txidem also.
            bail!(
                "Requested transaction with txid {}, but received one with txid {}",
                txid,
                tx.txid()
            );
        }
        self.tx_cache.put(txid, serialized_tx);
        Ok(tx)
    }

    /// Returns the height the transaction is confirmed at.
    ///
    /// If the transaction is in mempool, it return -1 if it has unconfirmed
    /// parents, or 0 if not.
    ///
    /// Returns None if transaction does not exist.
    pub fn get_confirmation_height(&self, txid: Txid) -> Option<i64> {
        {
            let mempool = self.mempool.read().unwrap();
            match mempool.tx_confirmation_state(&txid, None) {
                ConfirmationState::InMempool => return Some(0),
                ConfirmationState::UnconfirmedParent => return Some(-1),
                _ => (),
            };
        }
        self.header
            .get_confirmed_height_for_tx(txid)
            .map(|height| height as i64)
    }
}