use std::io::Write;
use std::ops::Index;
use crate::chaindef::OutPointHash;
use crate::encode::{decode_vector, encode_vector};
use crate::impl_nexa_consensus_encoding;
use crate::nexa::hash_types::TxIdem;
use crate::utilscript::read_push_from_script;
use anyhow::{Context, Result};
use bitcoin_hashes::hex::ToHex;
use bitcoin_hashes::Hash;
use bitcoincash::blockdata::opcodes;
use bitcoincash::consensus::Decodable;
use bitcoincash::consensus::Encodable;
use bitcoincash::VarInt;
use bitcoincash::{Script, Txid};
use byteorder::WriteBytesExt;
use super::token::{deserialize_amount, TokenID};
#[derive(Copy, Clone, Debug, Eq, Hash, PartialEq, PartialOrd, Ord)]
pub struct OutPoint {
pub hash: OutPointHash,
}
impl OutPoint {
pub fn new(tx_idem: TxIdem, out_index: u32) -> OutPoint {
let mut e = OutPointHash::engine();
tx_idem
.consensus_encode(&mut e)
.expect("failed to encode txidem");
out_index
.consensus_encode(&mut e)
.expect("failed to encode output_index");
OutPoint {
hash: OutPointHash::from_engine(e),
}
}
pub fn is_null(&self) -> bool {
self.hash.eq(&OutPointHash::all_zeros())
}
}
impl_nexa_consensus_encoding!(OutPoint, hash);
#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Debug, Hash)]
pub struct TxIn {
pub txin_type: u8,
pub previous_output: OutPoint,
pub script_sig: bitcoincash::Script,
pub sequence: u32,
pub amount: u64,
}
impl_nexa_consensus_encoding!(
TxIn,
txin_type,
previous_output,
script_sig,
sequence,
amount
);
/**
* Output type
*/
pub enum TxOutType {
// Bitcoin-style output
SATOSHI = 0,
// Nexa template style output. May contain tokens.
TEMPLATE = 1,
}
#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Debug, Hash)]
pub struct TxOut {
pub txout_type: u8,
pub value: u64,
pub script_pubkey: bitcoincash::Script,
}
impl_nexa_consensus_encoding!(TxOut, txout_type, value, script_pubkey);
impl TxOut {
/**
* Removes the token and token amount from script_pubkey. Replaces it with a OP_0
* (as if there was no token in transaction).
*
* Returns modified script or None if no modification is needed (to avoid a copy).
*/
pub fn scriptpubkey_without_token(&self) -> Result<Option<Script>> {
if self.txout_type != (TxOutType::TEMPLATE as u8) {
return Ok(None);
}
if self.script_pubkey.index(0) == &opcodes::all::OP_PUSHBYTES_0.to_u8() {
return Ok(None);
}
let iter = self.script_pubkey.as_bytes().iter();
let (iter, _) = read_push_from_script(iter).context(anyhow!("failed to read tokenhash"))?;
let (iter, _) = read_push_from_script(iter).context("invalid token amount")?;
let normalized_script = Script::from(
std::iter::once(&opcodes::all::OP_PUSHBYTES_0.to_u8())
.chain(iter)
.cloned()
.collect::<Vec<u8>>(),
);
Ok(Some(normalized_script))
}
/**
* If this output has a token transfer/action.
*/
pub fn has_token(&self) -> bool {
if self.script_pubkey.is_empty() || self.txout_type != (TxOutType::TEMPLATE as u8) {
return false;
}
self.script_pubkey.index(0) != &opcodes::all::OP_PUSHBYTES_0.to_u8()
}
}
#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Debug, Hash)]
pub struct Transaction {
pub version: u8,
pub lock_time: u32,
pub input: Vec<TxIn>,
pub output: Vec<TxOut>,
}
impl Transaction {
pub fn txidem(&self) -> TxIdem {
let mut s = TxIdem::engine();
self.version.consensus_encode(&mut s).unwrap();
// We need to encode inputs without scriptsig, so not using `encode_vector`.
VarInt(self.input.len() as u64)
.consensus_encode(&mut s)
.unwrap();
for i in &self.input {
i.txin_type.consensus_encode(&mut s).unwrap();
i.previous_output.consensus_encode(&mut s).unwrap();
i.sequence.consensus_encode(&mut s).unwrap();
i.amount.consensus_encode(&mut s).unwrap();
}
encode_vector(&mut s, &self.output).unwrap();
self.lock_time.consensus_encode(&mut s).unwrap();
TxIdem::from_engine(s)
}
pub fn txid(&self) -> Txid {
use bitcoin_hashes::sha256d;
let mut satisfier_script_hash = sha256d::Hash::engine();
i32::consensus_encode(&(self.input.len() as i32), &mut satisfier_script_hash).unwrap();
for i in &self.input {
satisfier_script_hash
.write_all(i.script_sig.as_bytes())
.unwrap();
satisfier_script_hash.write_u8(0xff).unwrap();
}
let mut txid = Txid::engine();
let txidem = self.txidem();
let satisfier = sha256d::Hash::from_engine(satisfier_script_hash);
txidem.consensus_encode(&mut txid).unwrap();
satisfier.consensus_encode(&mut txid).unwrap();
Txid::from_engine(txid)
}
/// Returns the regular byte-wise consensus-serialized size of this transaction.
pub fn size(&self) -> usize {
let mut input_size = 0;
for input in &self.input {
input_size += 32 + 4 + 4 + /* outpoint (32+4) + nSequence */ VarInt(input.script_sig.len() as u64).len() + input.script_sig.len();
}
let mut output_size = 0;
for output in &self.output {
output_size += 8 + /* value */ VarInt(output.script_pubkey.len() as u64).len() + output.script_pubkey.len();
}
let non_input_size =
// version:
4 +
// count varints:
VarInt(self.input.len() as u64).len() +
VarInt(self.output.len() as u64).len() +
output_size +
// lock_time
4;
non_input_size + input_size
}
pub fn is_coin_base(&self) -> bool {
self.input.is_empty()
}
}
impl Encodable for Transaction {
fn consensus_encode<W: std::io::Write + ?Sized>(
&self,
w: &mut W,
) -> Result<usize, std::io::Error> {
let mut len = 0;
len += self.version.consensus_encode(w)?;
// Can't implement Encodable trait for Vec<TxIn> or TxOut because of Rust orphan rules.
// (We don't own Encodeable and we don't own Vec).
len += encode_vector(w, &self.input)?;
len += encode_vector(w, &self.output)?;
len += self.lock_time.consensus_encode(w)?;
Ok(len)
}
}
impl Decodable for Transaction {
fn consensus_decode<R: std::io::Read + ?Sized>(
r: &mut R,
) -> Result<Self, bitcoincash::consensus::encode::Error> {
let version = u8::consensus_decode(r).unwrap();
let input: Vec<TxIn> = decode_vector(r).unwrap();
let output: Vec<TxOut> = decode_vector(r).unwrap();
Ok(Transaction {
version,
input,
output,
lock_time: Decodable::consensus_decode(r).unwrap(),
})
}
}
pub const WELL_KNOWN_TEMPLATE_P2PKT: [u8; 1] = [1];
#[derive(Debug)]
pub struct ScriptTemplate {
pub token: Option<TokenID>,
pub quantity: Option<i64>,
pub scripthash: Vec<u8>,
pub argumenthash: Vec<u8>,
}
impl ScriptTemplate {
/**
* Human friendly representation of scripthash
*/
pub fn scripthash_human(&self) -> String {
if self.scripthash.as_slice() == WELL_KNOWN_TEMPLATE_P2PKT {
"pay2pubkeytemplate".to_string()
} else {
self.scripthash.to_hex()
}
}
/**
* Get group authority flags
*/
pub fn group_authority(&self) -> Option<u64> {
self.quantity.filter(|x| *x < 0).map(|x| x as u64)
}
}
/**
* Parses a Nexa script template from scriptPubKey.
*
* Does not validate the values (except for that `TokenID` can be constructed).
*/
pub fn parse_script_template(s: &Script) -> Option<ScriptTemplate> {
if s.is_empty() {
return None;
}
let mut iter = s.as_bytes().iter();
let (iter, token) = if s.index(0) == &opcodes::all::OP_PUSHBYTES_0.to_u8() {
// Not a group transaction
iter.next(); // Skip OP_0
(iter, None)
} else {
let (iter, tokenhash) = read_push_from_script(iter).ok()?;
let tokenhash = tokenhash?;
let (iter, amount_serialized) = read_push_from_script(iter).ok()?;
let amount_serialized = amount_serialized?;
let amount = deserialize_amount(&amount_serialized).ok()?;
(
iter,
match TokenID::from_vec(tokenhash) {
Ok(hash) => Some((hash, amount)),
_ => None,
},
)
};
let (iter, scripthash) = read_push_from_script(iter).ok()?;
let (_, argumenthash) = read_push_from_script(iter).ok()?;
Some(ScriptTemplate {
quantity: token.as_ref().map(|(_, q)| *q),
token: token.map(|(t, _)| t),
scripthash: scripthash?,
argumenthash: argumenthash?,
})
}
#[cfg(test)]
mod test {
use crate::{nexa::token::parse_token_from_scriptpubkey, nexa::token::TokenID};
use super::*;
use bitcoincash::{
consensus::encode::deserialize, consensus::encode::deserialize_partial,
consensus::encode::serialize,
};
#[test]
/** Coinbase transaction `8b81d5de14b7b0951a2981c2d58919e9878847af1fad7c4e0fa17957ed8216b6` */
fn deserialize_coinbase_transaction() {
let tx_hex = "0000020100ca9a3b0000000017005114a64ba51750cadde003c7e8cfc400959d7ec7e0420000000000000000000f6a03ba8e000008000000000000000000000000";
let serialized = hex::decode(&tx_hex).unwrap();
let tx: (Transaction, usize) = deserialize_partial(&serialized).unwrap();
assert_eq!(tx.1, serialized.len());
let tx = tx.0;
assert!(tx.is_coin_base());
assert_eq!(tx.output[0].value, 1000000000);
assert_eq!(tx.output[1].value, 0);
}
#[test]
/** Coinbase transaction `8b81d5de14b7b0951a2981c2d58919e9878847af1fad7c4e0fa17957ed8216b6` */
fn serialize_coinbase_transaction() {
let expected_tx_hex = "0000020100ca9a3b0000000017005114a64ba51750cadde003c7e8cfc400959d7ec7e0420000000000000000000f6a03ba8e000008000000000000000000000000";
let tx: Transaction = deserialize(&hex::decode(&expected_tx_hex).unwrap()).unwrap();
assert_eq!(hex::encode(&serialize(&tx)), expected_tx_hex)
}
#[test]
fn txid_coinbase_transaction() {
let tx_hex = "0000020100ca9a3b0000000017005114a64ba51750cadde003c7e8cfc400959d7ec7e0420000000000000000000f6a03ba8e000008000000000000000000000000";
let tx: Transaction = deserialize(&hex::decode(&tx_hex).unwrap()).unwrap();
assert_eq!(
tx.txidem().to_string(),
"79bd936e54853c39bd00e6922a6c050f123ceb9cf3d51cabf806f6a5a13afc9a"
);
assert_eq!(
tx.txid().to_string(),
"8b81d5de14b7b0951a2981c2d58919e9878847af1fad7c4e0fa17957ed8216b6"
);
}
#[test]
fn test_scriptpubkey_without_token() {
let tx_hex = "00020094cac789cea15c56cd65dacb1c0c82c9f2d66347c6c2c049a6e908f5d1ce867d6422210385e6ec5324d707f22dcdf1237601cdbc0b0af30b468b34fc4fafa76ca6a6a2d5403705bea828e293b9c43df45d24eb04e909a2dce4d2d6295626904bee7b451f0907dcdba479095eddd7a49d00c230ba93f9690f0a2675bec3a49732809447367efeffffff220200000000000000ea8e221bb64936acac2a897002cfb6dbaf4f37d564366061b47bea257a6a1a9364222102eb89ce1df77ef589750c922a7e9d4d149355870c740bf29f57a0891f98d5ccd940dae8ba46b1c820d04837b4e03a4f663faa8060c8a49ac7e860ae1aec534ae80601d7692b63610caed05728d701dc8a19f6493443100d2be6fb67917785174bebfeffffffcac59a3b00000000030122020000000000003a201dc245acd1d1023f1c3e4a3143b41f0183ee3efa651581c053d2726ebeeb0000022a005114679fe206162bcfa16f20318a5004b2c67026ba8501220200000000000040201dc245acd1d1023f1c3e4a3143b41f0183ee3efa651581c053d2726ebeeb00000800000000000000fc5114d1cd4eb36aa94dcddba059611ec10833d1a7f8a101a3c19a3b0000000017005114468c14dac6c28b2d4be1f591aefbabceb3a8ad7700000000";
let tx: Transaction = deserialize(&hex::decode(tx_hex).unwrap()).unwrap();
let token_output = tx.output[0].clone();
let (token, amount) = parse_token_from_scriptpubkey(&token_output.script_pubkey).unwrap();
assert_eq!(
token,
TokenID::from_hex("1dc245acd1d1023f1c3e4a3143b41f0183ee3efa651581c053d2726ebeeb0000")
.unwrap()
);
assert_eq!(amount, 42);
let script_pubkey_no_token = token_output.scriptpubkey_without_token().unwrap().unwrap();
assert_eq!(None, parse_token_from_scriptpubkey(&script_pubkey_no_token));
assert_eq!(
script_pubkey_no_token,
Script::from(hex::decode("005114679fe206162bcfa16f20318a5004b2c67026ba85").unwrap())
);
}
#[test]
fn test_parse_script_template() {
let hex = "209d5e7d33920610f39b50d0f8fe13eb67a0419747a2547cf0697d5fc22909000008ed0c0100000000fc5114ea6d19eea0c1dbb1a0be94946dac2c0d5a28287a";
let script = Script::from(hex::decode(hex).unwrap());
let template = parse_script_template(&script).unwrap();
assert_eq!("pay2pubkeytemplate", template.scripthash_human());
assert_eq!(
"ea6d19eea0c1dbb1a0be94946dac2c0d5a28287a",
template.argumenthash.to_hex()
);
assert_eq!(-288230376151642899, template.quantity.unwrap());
assert_eq!(18158513697557908717, template.group_authority().unwrap());
}
}