use crate::cache::TransactionCache;
use crate::chaindef::BlockHash;
use crate::chaindef::OutPointHash;
use crate::chaindef::ScriptHash;
use crate::chaindef::Transaction;
use crate::chaindef::TxOut;
use crate::daemon::Daemon;
use crate::def::COIN;
use crate::encode::compute_outpoint_hash_from_tx;
use crate::encode::outpoint_hash;
use crate::indexes::outputindex::OutputIndexRow;
use crate::indexes::DBRow;
use crate::mempool::ConfirmationState;
use crate::mempool::MempoolEntry;
use crate::mempool::Tracker;
use crate::query::header::HeaderQuery;
use crate::query::queryutil;
use crate::rpc::encoding::blockhash_to_hex;
use crate::store::DBStore;
use crate::store::Row;
use crate::writebatch::PrefilledSpentData;
use anyhow::{Context, Result};
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 futures::stream::TryStreamExt;
use futures_util::stream;
use futures_util::StreamExt;
use rust_decimal::prelude::*;
use serde_json::Value;
use std::collections::HashMap;
use std::collections::HashSet;
use std::sync::Arc;
use tokio::select;
/// String returned is intended to be the same as produced by bitcoind
/// GetTxnOutputType
fn get_address_type(out: &TxOut, network: Network) -> Option<&str> {
#[cfg(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(bch)]
pub fn get_addresses(out: &TxOut, 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 script = &out.script_pubkey;
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![],
}
}
/**
* Derive address(s) for a output
*/
#[cfg(nexa)]
pub fn get_addresses(output: &TxOut, network: Network) -> Vec<String> {
use crate::nexa::cashaddr::encode as nexaaddr_encode;
use crate::nexa::cashaddr::version_byte_flags;
use crate::nexa::transaction::TxOutType;
use bitcoincash::util::address::Payload::{PubkeyHash, ScriptHash};
if output.txout_type == TxOutType::TEMPLATE as u8 {
let output_normalized = match output.scriptpubkey_without_token() {
Ok(Some(s)) => s.to_bytes(),
Ok(None) => output.script_pubkey.to_bytes(),
Err(_) => {
// invalid encoding, don't try to make it into an address.
return vec![];
}
};
if let Ok(addr) = nexaaddr_encode(
&output_normalized,
version_byte_flags::TYPE_SCRIPT_TEMPLATE,
network,
) {
return vec![addr];
}
return vec![];
}
if output.txout_type != TxOutType::SATOSHI as u8 {
// unsupported output
return vec![];
}
let address = match Address::from_script(&output.script_pubkey, network).ok() {
Some(a) => a,
None => return vec![],
};
match address.payload {
PubkeyHash(pubhash) => {
let hash = pubhash.as_hash().to_vec();
if let Ok(addr) = nexaaddr_encode(&hash, version_byte_flags::TYPE_P2PKH, network) {
return vec![addr];
}
vec![]
}
ScriptHash(scripthash) => {
let hash = scripthash.as_hash().to_vec();
if let Ok(addr) = nexaaddr_encode(&hash, version_byte_flags::TYPE_P2SH, network) {
return vec![addr];
}
vec![]
}
_ => 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())
}
/**
* Given an output, lookup how many satoshis were/are in the utxo.
*/
pub(crate) async fn sats_from_outpoint(
store: &DBStore,
outpointhash: &OutPointHash,
) -> Option<u64> {
let key = OutputIndexRow::filter_by_outpointhash(outpointhash);
let (key, value) = store.get(OutputIndexRow::CF, key).await;
let row = OutputIndexRow::from_row(&Row {
key: key.into_boxed_slice(),
value: value?.into_boxed_slice(),
});
Some(row.value())
}
pub struct TxQuery {
tx_cache: TransactionCache,
daemon: Daemon,
mempool: Arc<Tracker>,
header: Arc<HeaderQuery>,
duration: Arc<prometheus::HistogramVec>,
network: Network,
confirmed_index: Arc<DBStore>,
}
impl TxQuery {
pub fn new(
tx_cache: TransactionCache,
daemon: Daemon,
mempool: Arc<Tracker>,
header: Arc<HeaderQuery>,
duration: Arc<prometheus::HistogramVec>,
network: Network,
confirmed_index: Arc<DBStore>,
) -> TxQuery {
TxQuery {
tx_cache,
daemon,
mempool,
header,
duration,
network,
confirmed_index,
}
}
pub async fn get_tx_funding_prevout(
&self,
prevout: &OutPointHash,
) -> Result<Option<(Transaction, u32, u32)>> {
if let Some(found) =
queryutil::get_tx_funding_prevout(self.mempool.index(), self, prevout).await?
{
return Ok(Some(found));
}
queryutil::get_tx_funding_prevout(&self.confirmed_index, self, prevout).await
}
/// Get a transaction by Txid.
pub async fn get(
&self,
txid: &Txid,
blockhash: Option<&BlockHash>,
blockheight: Option<u32>,
allow_by_txidem: bool,
) -> Result<Transaction> {
let mempool_check = async {
if blockhash.is_none() && blockheight.is_none() {
self.mempool.get_txn(txid).await
} else {
None
}
};
let cache_check = async { self.tx_cache.get(txid).await };
// Try fetching from both local caches in parallel.
select! {
Some(cache_tx) = cache_check => {
return Ok(cache_tx)
}
Some(mempool_tx) = mempool_check => {
return Ok(mempool_tx)
}
else => {
// Continue to fetch from node
}
};
let _timer = self.duration.with_label_values(&["load_txn"]).start_timer();
let hash: Option<BlockHash> = match blockhash {
Some(hash) => Some(*hash),
None => match self.header.get_by_txid(txid, blockheight).await {
Ok(header) => header.map(|h| h.block_hash()),
Err(_) => None,
},
};
self.load_txn_from_bitcoind(txid, hash.as_ref(), allow_by_txidem)
.await
}
/// 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 async fn get_unconfirmed(&self, txid: &Txid) -> Result<Transaction> {
if let Some(tx) = self.tx_cache.get(txid).await {
Ok(tx)
} else {
self.load_txn_from_bitcoind(txid, None, false).await
}
}
#[cfg(bch)]
async fn inputs_to_json(&self, tx: &Transaction) -> Result<(Vec<Value>, u64)> {
use bitcoincash::blockdata::token::Capability;
use crate::errors::rpc_invalid_params;
use super::queryutil::get_tx_funding_prevout;
let inputs = stream::iter(tx.input.iter()).then(|txin| async move {
let outpoint = outpoint_hash(&txin.previous_output);
let value = match sats_from_outpoint(self.mempool.index(), &outpoint).await {
Some(v) => Some(v),
None => sats_from_outpoint(&self.confirmed_index, &outpoint).await
};
let utxo = if tx.is_coin_base() {
None
} else {
let (utxo_creation_tx, output_index, _) = match get_tx_funding_prevout(self.mempool.index(), self, &outpoint).await? {
Some(found) => found,
None => match get_tx_funding_prevout(&self.confirmed_index, self, &outpoint).await? {
Some(found) => found,
None => return Err(rpc_invalid_params("could not find funding tx for outpoint".to_string()))
}
};
let utxo = utxo_creation_tx.output.get(output_index as usize).cloned();
if utxo.is_none() {
bail!(rpc_invalid_params(format!(
"out_n {} does not exist on tx {}, the transaction has {} outputs",
output_index,
utxo_creation_tx.txid(),
utxo_creation_tx.output.len()
)));
}
utxo
};
let token = utxo.and_then(|u| u.token);
let tokendata = if let Some(tokendata) = &token {
let nft = if tokendata.has_nft() {
let capability = if tokendata.is_minting_nft() {
"minting".to_string()
} else if tokendata.capability() & Capability::Mutable as u8 != 0 {
"mutable".to_string()
} else {
"none".to_string()
};
Some(json!({
"commitment": tokendata.commitment.to_hex(),
"capability": capability
}))
} else {
None
};
Some(json!({
"category": tokendata.id.to_hex(),
"amount": tokendata.amount.to_string(),
"nft": nft,
}))
} else {
None
};
Ok((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,
"value_satoshi": value,
"value_coin": value.map(value_from_amount),
"scriptSig": {
"asm": txin.script_sig.asm(),
"hex": txin.script_sig.to_hex(),
},
"tokenData": tokendata,
}), value.unwrap_or(0)))
}).try_collect::<Vec<(Value, u64)>>().await?;
let total_value_in = inputs.iter().map(|(_, v)| v).sum();
let vin = inputs.into_iter().map(|(i, _)| i).collect();
Ok((vin, total_value_in))
}
#[cfg(nexa)]
async fn inputs_to_json(&self, tx: &Transaction) -> Result<(Vec<Value>, u64)> {
use super::queryutil::get_tx_funding_prevout;
use crate::errors::rpc_invalid_params;
use crate::nexa::transaction::{parse_script_template, TxOutType};
let inputs: Vec<(Value, u64)> = stream::iter(tx.input.iter()).then(|txin| async move {
let outpoint = txin.previous_output.hash;
let value = match sats_from_outpoint(self.mempool.index(), &outpoint).await {
Some(v) => Some(v),
None => sats_from_outpoint(&self.confirmed_index, &outpoint).await
};
let (utxo_creation_tx, output_index, _) = match get_tx_funding_prevout(self.mempool.index(), self, &outpoint).await? {
Some(found) => found,
None => match get_tx_funding_prevout(&self.confirmed_index, self, &outpoint).await? {
Some(found) => found,
None => return Err(rpc_invalid_params("could not find funding tx for outpoint".to_string()))
}
};
let utxo = match utxo_creation_tx.output.get(output_index as usize) {
Some(utxo) => utxo,
None => {
bail!(rpc_invalid_params(format!(
"out_n {} does not exist on tx {}, the transaction has {} outputs",
output_index,
utxo_creation_tx.txid(),
utxo_creation_tx.output.len()
)));
}
};
let template = if utxo.txout_type == TxOutType::TEMPLATE as u8 {
parse_script_template(&utxo.script_pubkey)
} else {
None
};
let mut jval = 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(),
"value_satoshi": value,
"value_coin": value.map(value_from_amount),
"value": value.map(value_from_amount),
"scriptSig": {
"asm": txin.script_sig.asm(),
"hex": txin.script_sig.to_hex(),
},
"addresses": get_addresses(utxo, self.network)
});
let v = jval.as_object_mut().unwrap();
v.insert(
"group".to_string(),
json!(template.as_ref().map(|t| t
.token
.as_ref()
.map(|token| token.as_cashaddr(self.network)))),
);
v.insert(
"token_id_hex".to_string(),
json!(template
.as_ref()
.map(|t| t.token.as_ref().map(|token| token.to_hex()))),
);
v.insert(
"groupQuantity".to_string(),
json!(template.as_ref().map(|t| t.quantity)),
);
v.insert(
"groupAuthority".to_string(),
json!(template.as_ref().map(|t| t.group_authority().unwrap_or(0))),
);
Ok((jval, value.unwrap_or(0)))
}).try_collect().await?;
let total_value = inputs.iter().map(|(_, value)| value).sum();
let vin: Vec<Value> = inputs.into_iter().map(|(input, _)| input).collect();
Ok((vin, total_value))
}
#[cfg(nexa)]
fn outputs_to_json(&self, tx: &Transaction) -> (Vec<Value>, u64) {
use crate::{
encode::compute_outpoint_hash,
nexa::transaction::{parse_script_template, TxOutType},
};
let total_output = tx.output.iter().map(|o| o.value).sum();
let txidem = tx.txidem();
let vout = tx.output
.iter()
.enumerate()
.map(|(n, txout)| {
let template = if txout.txout_type == (TxOutType::TEMPLATE as u8) {
parse_script_template(&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, self.network),
"groupQuantity": template.as_ref().map(|t| t.quantity),
"groupAuthority": template.as_ref().map(|t| t.group_authority().unwrap_or(0)),
"scriptHash": template.as_ref().map(|t| t.scripthash_human()),
"argsHash": template.as_ref().map(|t| t.argumenthash.to_hex()),
"group": template.as_ref().map(|t| t.token.as_ref().map(|id| id.as_cashaddr(self.network))),
"token_id_hex": template.map(|t| t.token.map(|id| id.to_hex())),
},
"outpoint_hash": compute_outpoint_hash(&txidem, n as u32),
})
})
.collect::<Vec<Value>>();
(vout, total_output)
}
#[cfg(bch)]
fn outputs_to_json(&self, tx: &Transaction) -> (Vec<Value>, u64) {
use bitcoincash::blockdata::token::Capability;
let capability_str = |capability: u8| {
if (capability & Capability::Minting as u8) != 0 {
"minting"
} else if (capability & Capability::Mutable as u8) != 0 {
"mutable"
} else {
"none"
}
};
let total_value_out: u64 = tx.output.iter().map(|o| o.value).sum();
let vout = tx
.output
.iter()
.enumerate()
.map(|(n, txout)| {
let token_data = txout.token.as_ref().map(|token| {
json!({
"category": token.id,
"amount": token.amount.to_string(),
"nft": if token.has_nft() {
Some(json!({
"commitment": token.commitment.to_hex(),
"capability": capability_str(token.capability()),
}))
} else {
None
}
})
});
json!({
"value_satoshi": txout.value,
"value_coin": value_from_amount(txout.value),
"value": value_from_amount(txout.value),
"n": n,
"tokenData": token_data,
"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, self.network),
},
})
})
.collect::<Vec<Value>>();
(vout, total_value_out)
}
pub async fn get_verbose(&self, txid_or_txidem: &Txid, allow_by_txidem: bool) -> Result<Value> {
let tx = self
.get(txid_or_txidem, None, None, allow_by_txidem)
.await?;
let txid = tx.txid();
let header = self
.header
.get_by_txid(&txid, None)
.await
.unwrap_or_default();
let blocktime = header.as_ref().map(|header| header.time);
let height = if let Some(ref header) = header {
Some(
self.header
.get_height(header)
.await
.context("header missing")?,
)
} else {
None
};
let confirmations = match height {
Some(ref height) => {
let best = self.header.best().await;
let best_height = self
.header
.get_height(&best)
.await
.context("header missing")?;
Some(1 + best_height - height)
}
None => None,
};
let tx_serialized = serialize(&tx);
let (vin, total_value_in) = self.inputs_to_json(&tx).await?;
let (vout, total_value_out) = self.outputs_to_json(&tx);
#[allow(unused_mut)]
let mut tx_details = json!({
"blockhash": header.map(|h| blockhash_to_hex(&h.block_hash())),
"blocktime": blocktime,
"height": height,
"confirmations": confirmations,
"hash": txid.to_hex(),
"txid": txid.to_hex(),
"size": tx_serialized.len(),
"hex": hex::encode(tx_serialized),
"locktime": tx.lock_time,
"time": blocktime,
"version": tx.version,
"vin": vin,
"vout": vout,
"fee": value_from_amount(total_value_in.saturating_sub(total_value_out)),
"fee_satoshi": total_value_in.saturating_sub(total_value_out),
});
#[cfg(nexa)]
{
tx_details
.as_object_mut()
.unwrap()
.insert("txidem".to_string(), json!(tx.txidem().to_hex()));
}
Ok(json!(tx_details))
}
async fn load_txn_from_bitcoind(
&self,
txid: &Txid,
blockhash: Option<&BlockHash>,
allow_by_txidem: bool,
) -> Result<Transaction> {
let value: Value = self
.daemon
.gettransaction_raw(txid, blockhash, /*verbose*/ false)
.await?;
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")?;
let actual_txid = tx.txid();
if txid != &actual_txid {
match allow_by_txidem {
#[cfg(nexa)]
true => {
use bitcoin_hashes::Hash;
// If txid was actually a txidem, allow it on nexa. See issue #194
if tx.txidem().as_inner() != txid.as_inner() {
bail!(
"Requested transaction with ID {}, but received one with txid {} and txidem {}",
txid,
tx.txid(),
tx.txidem(),
);
}
}
_ => {
bail!(
"Requested transaction with txid {}, but received one with txid {}",
txid,
tx.txid()
);
}
}
}
self.tx_cache.put(actual_txid, serialized_tx).await;
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 async fn get_confirmation_height(&self, txid: &Txid) -> Option<i64> {
{
match self.mempool.tx_confirmation_state(txid, None).await {
ConfirmationState::InMempool => return Some(0),
ConfirmationState::UnconfirmedParent => return Some(-1),
_ => (),
};
}
self.header
.get_confirmed_height_for_tx(txid)
.await
.map(|height| height as i64)
}
/// Create input amount cache out of outsputs from given tx set, where possible.
pub async fn create_input_cache(&self, txids: &HashSet<Txid>) -> HashMap<OutPointHash, u64> {
stream::iter(txids)
.filter_map(|txid| async move {
match self.get_unconfirmed(txid).await {
Ok(tx) => {
let outputs = tx
.output
.iter()
.enumerate()
.map(|(n, o)| {
let outpoint = compute_outpoint_hash_from_tx(&tx, n as u32);
(outpoint, o.value)
})
.collect::<Vec<(OutPointHash, u64)>>();
Some(stream::iter(outputs))
}
Err(_) => None,
}
})
.flatten()
.collect::<HashMap<OutPointHash, u64>>()
.await
}
// Requires that we have all the transactions in inputs (and enfoces this by failing).
// Note: Mempool.add assumes we enforce no orphans!
pub async fn create_prefilled_spent_data(
&self,
spending_tx: &Transaction,
) -> Result<HashMap<OutPointHash, PrefilledSpentData>> {
let mut prefilled_spent_data = HashMap::with_capacity(spending_tx.input.len());
for input in spending_tx.input.iter() {
let outpoint = outpoint_hash(&input.previous_output);
let (prevtx, vout) = match self.get_tx_funding_prevout(&outpoint).await {
Ok(Some((tx, idx, _))) => (tx, idx as usize),
_ => {
bail!("failed to get previous tx for input {}", outpoint.to_hex());
}
};
let scripthash = ScriptHash::normalized_from_txout(&prevtx.output[vout]);
let has_token = prevtx.output[vout].has_token();
prefilled_spent_data.insert(
outpoint,
PrefilledSpentData {
scripthash,
has_token,
},
);
}
Ok(prefilled_spent_data)
}
/**
* Create a mempool entry for given transaction.
*
* Note: We need to pass mempool, rather than use self.mempool to avoid dead lock,
* since we call this method from the mempool itself.
*/
pub async fn mempool_entry_from(
&self,
mempool: &Tracker,
tx: Transaction,
input_cache: &HashMap<OutPointHash, u64>,
) -> Result<MempoolEntry> {
let total_stats_in: u64 = stream::iter(tx.input.iter())
.then(|i| async move {
let outpoint = outpoint_hash(&i.previous_output);
match input_cache.get(&outpoint) {
Some(sats) => Ok(*sats),
None => match sats_from_outpoint(mempool.index(), &outpoint).await {
Some(v) => Ok(v),
None => sats_from_outpoint(&self.confirmed_index, &outpoint)
.await
.context(format!("Failed to get input {}", outpoint.to_hex())),
},
}
})
.try_collect::<Vec<u64>>()
.await?
.into_iter()
.sum();
let total_sats_out: u64 = tx.output.iter().map(|o| o.value).sum();
let fee: u64 = if total_sats_out > total_stats_in {
if tx.is_coin_base() {
0
} else {
// Invalid tx
return Err(anyhow!("Inputs are greater than outputs"));
}
} else {
total_stats_in - total_sats_out
};
Ok(MempoolEntry::new(fee, tx))
}
pub async fn get_mempool_entry(
&self,
mempool: &Tracker,
txid: &Txid,
input_cache: &HashMap<OutPointHash, u64>,
) -> Result<MempoolEntry> {
let tx = self.get_unconfirmed(txid).await?;
self.mempool_entry_from(mempool, tx, input_cache).await
}
}
#[cfg(test)]
mod test {
// Test case for bug where output of tx on the nexa chain was encoded with an extra prefixed byte.
// Issue #188
#[cfg(nexa)]
#[test]
fn test_encode_address_bug() {
use super::get_addresses;
use crate::nexa;
use bitcoincash::{consensus::deserialize, Network};
let txhex = "00010090c4c1de7a046ed38738c852d11c3520912bde69139f5774faa394bcf244b0b664222103b9521b653db81b3e5dde95092fa8483e22deb7b9646bd3c68e90ff1364a66120403ffee795329706d8e1fe88393869088b7c421604fa1dce99b478d28d6b2e6f89162b2921b3c82f4ac0eb6ff2b49a083c679ef20e572ba2b78954297efdfc17d6feffffff85ad0a00000000000201e09304000000000017005114247a304a71f7c08a66df3d41eb25c1609b4d75f001ca1806000000000017005114264164083c381bb50ea386d0913d1b6b8597c3cc861d0300";
let tx: nexa::transaction::Transaction =
deserialize(&hex::decode(&txhex).unwrap()).unwrap();
let addresses = get_addresses(&tx.output[1], Network::Bitcoin);
assert_eq!(1, addresses.len());
let (payload, _, _) = nexa::cashaddr::decode(&addresses[0]).unwrap();
assert_eq!(payload, tx.output[1].script_pubkey.to_bytes());
assert_eq!(
"nexa:nqtsq5g5yeqkgzpu8qdm2r4rsmgfz0gmdwze0s7vuwn0psk9",
addresses[0]
);
}
// Test case for bug where token and token amount was not removed from payload
// Issue #195
#[cfg(nexa)]
#[test]
fn test_encode_address_bug2() {
let txhex = "0001002b21b6fbc90bf9d71c1a10ce0f7ce00486a6436e749ef50180fc494758d4239c64222103b586b8ff5362136ebbbf937dba7853e766436bff297025c5fdf2d202fe4190bc4065fb1adf72ecbec07e24e81a032e6628f8eb6068213083c9515634f3cb7c678673fba27a4dfcc29d7cd9614b43e7b82876fca372c25bc06a6c2df21e16a2598cfeffffffc22e023b00000000030000000000000000005a6a0438564c05054e49465459084e696674794172742368747470733a2f2f6e696674796172742e636173682f74642f6e696674792e6a736f6e200b4abe023ae013fd9cc30c9e8b3fbf40a06beceb5048419bd01cc8480a91fab00122020000000000004020cacf3d958161a925c28a970d3c40deec1a3fe06796fe1b4a7b68f377cdb900000833c10000000000fc511461977da240fe17110fa1e4df813957b058f3e2cf017b2a023b00000000170051146ff69e4cfcfeba70dd606cd35bb4010c791bf96a00000000";
use super::get_addresses;
use crate::nexa;
use bitcoincash::{consensus::deserialize, Network};
let tx: nexa::transaction::Transaction =
deserialize(&hex::decode(&txhex).unwrap()).unwrap();
let addresses = get_addresses(&tx.output[1], Network::Bitcoin);
assert_eq!(1, addresses.len());
let (payload, _, _) = nexa::cashaddr::decode(&addresses[0]).unwrap();
assert_eq!(
payload,
tx.output[1]
.scriptpubkey_without_token()
.unwrap()
.unwrap()
.to_bytes()
);
assert_eq!(
"nexa:nqtsq5g5vxthmgjqlct3zrapun0czw2hkpv08ck0frlkvpws",
addresses[0]
);
}
}