use std::sync::Arc;
use bytemuck::cast_ref;
use chrono::ParseError as ChronoParseError;
use hex::FromHexError;
use serde::{
Deserialize, Serialize,
ser::{Serializer, SerializeMap}
};
use sha2::{Sha256, Digest};
use snafu::{OptionExt, ResultExt, Snafu, ensure};
use crate::{
ABISerializable, APIClient, Action, ActionError, BlockId, Bytes, ChainId,
Checksum256, Extensions, JsonValue, PrivateKey, Signature, TimePointSec, TransactionId,
VarUint32,
api::HttpError,
bitops::endian_reverse_u32,
convert::{ConversionError, variant_to_object, variant_to_str, variant_to_uint},
impl_auto_error_conversion, json, with_location
};
extern crate self as kudu;
#[with_location]
#[derive(Debug, Snafu)]
pub enum TransactionError {
#[snafu(display("unknown field: '{field}'"))]
UnknownField { field: String },
#[snafu(display("cannot convert field to required target type"))]
Conversion { source: ConversionError },
#[snafu(display("cannot parse date/time"))]
DateTimeParse { source: ChronoParseError },
#[snafu(display("invalid action"))]
InvalidAction { source: ActionError },
#[snafu(display("unlinked transaction: {message}"))]
UnlinkedTransaction { message: String },
#[snafu(display("network error: {message}\ndetails: {source}"))]
NetworkError { message: String, source: HttpError },
#[snafu(display("invalid chain id: {chain_id}"))]
InvalidChainId { chain_id: String, source: FromHexError },
#[snafu(display("could not match JSON object to transaction"))]
FromJson { source: serde_json::Error },
#[snafu(display("Nodeos error: {message}"))]
NodeosError { message: String },
}
impl_auto_error_conversion!(ChronoParseError, TransactionError, DateTimeParseSnafu);
impl_auto_error_conversion!(ConversionError, TransactionError, ConversionSnafu);
impl_auto_error_conversion!(ActionError, TransactionError, InvalidActionSnafu);
impl_auto_error_conversion!(serde_json::Error, TransactionError, FromJsonSnafu);
#[derive(Eq, Hash, PartialEq, Debug, Clone, Default, Serialize, Deserialize)]
pub struct Transaction {
pub expiration: TimePointSec,
pub ref_block_num: u16,
pub ref_block_prefix: u32,
pub max_net_usage_words: VarUint32,
pub max_cpu_usage_ms: u8,
pub delay_sec: VarUint32,
pub context_free_actions: Vec<Action>,
pub actions: Vec<Action>,
pub transaction_extensions: Extensions,
#[serde(skip)]
pub chain_id: Option<ChainId>,
#[serde(skip)]
pub client: Option<Arc<APIClient>>,
}
type DigestType = Checksum256;
impl Transaction {
pub fn new(actions: Vec<Action>) -> Transaction {
Transaction {
expiration: 0.into(),
ref_block_num: 0,
ref_block_prefix: 0,
max_net_usage_words: VarUint32(0),
max_cpu_usage_ms: 0,
delay_sec: VarUint32(0),
context_free_actions: vec![],
actions,
transaction_extensions: vec![],
chain_id: None,
client: None
}
}
pub fn id(&self) -> TransactionId {
let mut data = Bytes::new();
self.to_bin(&mut data);
let hash = sha2::Sha256::digest(&data);
let r: [u8; 32] = hash.into();
r.into()
}
pub fn from_json(tx: &JsonValue) -> Result<Transaction, TransactionError> {
let mut result = Transaction::default();
for (field, value) in variant_to_object(tx)?.iter() {
match field.as_str() {
"expiration" => result.expiration = variant_to_str(value)?.parse()?,
"ref_block_num" => result.ref_block_num = variant_to_uint(value)?,
"ref_block_prefix" => result.ref_block_prefix = variant_to_uint(value)?,
"max_cpu_usage_ms" => result.max_cpu_usage_ms = variant_to_uint(value)?,
"max_net_usage_words" => result.max_net_usage_words = variant_to_uint::<u32>(value)?.into(),
"delay_sec" => result.delay_sec = variant_to_uint::<u32>(value)?.into(),
"context_free_actions" => result.context_free_actions = Action::from_json_array(value)?,
"actions" => result.actions = Action::from_json_array(value)?,
"transaction_extensions" => result.transaction_extensions = serde_json::from_value(value.clone())?,
other => UnknownFieldSnafu { field: other }.fail()?,
}
}
Ok(result)
}
pub fn sig_digest(&self, context_free_data: &[u8]) -> Result<DigestType, TransactionError> {
let mut hasher = Sha256::new();
match &self.chain_id {
Some(chain_id) => hasher.update(chain_id),
None => UnlinkedTransactionSnafu { message: "you need a chain ID to be set to compute the tx digest".to_string() }.fail()?,
}
let mut ds = Bytes::new();
self.to_bin(&mut ds);
hasher.update(&ds);
if !context_free_data.is_empty() {
hasher.update(Sha256::digest(context_free_data));
}
else {
hasher.update([0u8; 32]); }
let r: [u8; 32] = hasher.finalize().into();
Ok(r.into())
}
fn get_tapos_info(block: &BlockId) -> (u16, u32) {
let hash = cast_ref::<[u8; 32], [u64; 4]>(&block.0);
let ref_block_num = endian_reverse_u32((hash[0] & 0xFFFFFFFF) as u32) as u16;
let ref_block_prefix = hash[1] as u32;
(ref_block_num, ref_block_prefix)
}
pub fn link(&mut self, client: Arc<APIClient>) -> Result<&mut Self, TransactionError> {
let info = client.get("/v1/chain/get_info").context(NetworkSnafu { message: "cannot get chain info".to_string() })?;
let block_id = info["last_irreversible_block_id"].as_str()
.context(NodeosSnafu { message: "chain info 'last_irreversible_block_id' is not a string" })?;
let block_id = BlockId::from_hex(block_id)
.map_err(|_| NodeosSnafu { message: "chain info 'last_irreversible_block_id' is not an hex value" }.build())?;
let (ref_block_num, ref_block_prefix) = Self::get_tapos_info(&block_id);
self.ref_block_num = ref_block_num;
self.ref_block_prefix = ref_block_prefix;
let chain_id = info["chain_id"].as_str()
.context(NodeosSnafu { message: "chain info 'chain_id' is not a string" })?;
self.chain_id = Some(ChainId::from_hex(chain_id).context(InvalidChainIdSnafu { chain_id })?);
let expiration_delay_seconds = 120;
let head_block_time: TimePointSec = info["head_block_time"].as_str()
.context(NodeosSnafu { message: "chain info 'head_block_time' is not a string" })?
.parse()?;
self.expiration = head_block_time + expiration_delay_seconds;
self.client = Some(client);
Ok(self)
}
fn get_signature(&self, signing_key: &PrivateKey) -> Result<Signature, TransactionError> {
let context_free_data = b""; Ok(signing_key.sign_digest(self.sig_digest(context_free_data)?))
}
pub fn sign(&self, signing_key: &PrivateKey) -> Result<SignedTransaction, TransactionError> {
ensure!(self.chain_id.is_some(),
UnlinkedTransactionSnafu { message: "cannot sign transaction" });
let sig = self.get_signature(signing_key)?;
Ok(SignedTransaction {
tx: self.clone(),
signatures: vec![sig],
compression: false,
packed_content_free_data: Bytes::new(),
})
}
}
impl kudu::ABISerializable for Transaction {
fn to_bin(&self, s: &mut kudu::Bytes) {
self.expiration.to_bin(s);
self.ref_block_num.to_bin(s);
self.ref_block_prefix.to_bin(s);
self.max_net_usage_words.to_bin(s);
self.max_cpu_usage_ms.to_bin(s);
self.delay_sec.to_bin(s);
self.context_free_actions.to_bin(s);
self.actions.to_bin(s);
self.transaction_extensions.to_bin(s);
}
fn from_bin(s: &mut kudu::ByteStream) -> ::core::result::Result<Self, kudu::SerializeError> {
Ok(Self {
expiration: TimePointSec::from_bin(s)?,
ref_block_num: u16::from_bin(s)?,
ref_block_prefix: u32::from_bin(s)?,
max_net_usage_words: VarUint32::from_bin(s)?,
max_cpu_usage_ms: u8::from_bin(s)?,
delay_sec: VarUint32::from_bin(s)?,
context_free_actions: Vec::<Action>::from_bin(s)?,
actions: Vec::<Action>::from_bin(s)?,
transaction_extensions: Extensions::from_bin(s)?,
chain_id: None,
client: None,
})
}
}
#[derive(Eq, Hash, PartialEq, Debug, Clone)]
pub struct SignedTransaction {
pub tx: Transaction,
pub signatures: Vec<Signature>,
pub compression: bool,
pub packed_content_free_data: Bytes,
}
impl SignedTransaction {
pub fn send(&self) -> Result<JsonValue, TransactionError> {
let signed_tx = json!(self);
let result = self.tx.client.as_ref().unwrap() .call("/v1/chain/push_transaction", &signed_tx)
.context(NetworkSnafu { message: format!("Could not push transaction: {}", &signed_tx) })?;
Ok(result)
}
pub fn send_unchecked(&self) -> Result<JsonValue, TransactionError> {
let signed_tx = json!(self);
let result = self.tx.client.as_ref().unwrap() .call_unchecked("/v1/chain/push_transaction", &signed_tx)
.context(NetworkSnafu { message: format!("Could not push transaction: {}", &signed_tx) })?;
Ok(result)
}
}
impl Serialize for SignedTransaction {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer
{
let mut map = serializer.serialize_map(Some(9 + 4))?;
map.serialize_entry("signatures", &self.signatures)?;
map.serialize_entry("compression", &self.compression)?;
map.serialize_entry("packed_content_free_data", &self.packed_content_free_data)?;
let mut s = Bytes::new();
self.tx.to_bin(&mut s);
map.serialize_entry("packed_trx", &s.to_hex())?;
map.end()
}
}
#[cfg(test)]
mod tests {
use color_eyre::eyre::Result;
use crate::{chain::Transfer, Name};
use super::*;
#[test]
fn test_tapos() -> Result<()> {
let block_id = Checksum256::from_hex("0eeb31a70905138203051bf848fc7176336a0eb41d078338460af949d8cf2abd")?;
let (ref_block_num, ref_block_prefix) = Transaction::get_tapos_info(&block_id);
assert_eq!(ref_block_num, 12711);
assert_eq!(ref_block_prefix, 4162520323);
Ok(())
}
#[test]
fn test_sign_transaction() -> Result<()> {
let transfer = Transfer {
from: Name::new("useraaaaaaaa")?,
to: Name::new("useraaaaaaab")?,
quantity: "0.0001 SYS".try_into()?,
memo: "".into(),
};
let mut tx = Transaction {
expiration: "2009-02-13T23:31:31.000".parse()?,
ref_block_num: 1234,
ref_block_prefix: 5678,
actions: vec![
Action::new(("useraaaaaaaa", "active"), &transfer),
],
..Default::default()
};
assert_eq!(json!(tx), json!({
"ref_block_num": 1234,
"ref_block_prefix": 5678,
"expiration": "2009-02-13T23:31:31.000",
"max_net_usage_words": 0,
"max_cpu_usage_ms": 0,
"delay_sec": 0,
"context_free_actions": [],
"actions": [{
"account": "eosio.token",
"name": "transfer",
"authorization": [{"actor":"useraaaaaaaa","permission":"active"}],
"data": "608c31c6187315d6708c31c6187315d60100000000000000045359530000000000"
}],
"transaction_extensions": []
}));
let signing_key = PrivateKey::eosio_dev();
tx.chain_id = Some(Checksum256::from_hex(crate::config::JUNGLE_CHAIN_ID)?);
let sig = tx.get_signature(&signing_key)?;
let signed_tx = SignedTransaction {
tx: tx.clone(),
signatures: vec![sig],
compression: false,
packed_content_free_data: Bytes::new(),
};
assert_eq!(json!(signed_tx), json!({
"signatures": ["SIG_K1_K18qEA2qTqVj153ZKriMnnRwHpLuENX7bp9UYs5AJsRWhgD6diPgMeoebwRRFQuvyicDsgwVYTt3g4GsG5FxCXM3WNZVN7"],
"compression": false,
"packed_content_free_data": "",
"packed_trx": "d3029649d2042e160000000000000100a6823403ea3055000000572d3ccdcd01608c31c6187315d600000000a8ed323221608c31c6187315d6708c31c6187315d6010000000000000004535953000000000000",
}));
Ok(())
}
}