use aelf_proto::aelf::{ResourceTokenCharged, TransactionFeeCharged};
use base64::Engine;
use prost::Message;
use serde::{Deserialize, Deserializer, Serialize};
use serde_json::{Map, Value};
use std::collections::{BTreeMap, HashMap};
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
#[serde(rename_all = "PascalCase")]
pub struct ChainStatusDto {
pub chain_id: String,
#[serde(default, deserialize_with = "block_height_map_as_default")]
pub branches: HashMap<String, i64>,
#[serde(default, deserialize_with = "block_height_map_as_default")]
pub not_linked_blocks: HashMap<String, i64>,
pub longest_chain_height: i64,
pub longest_chain_hash: String,
pub genesis_block_hash: String,
pub genesis_contract_address: String,
pub last_irreversible_block_hash: String,
pub last_irreversible_block_height: i64,
pub best_chain_hash: String,
pub best_chain_height: i64,
}
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
#[serde(rename_all = "PascalCase")]
pub struct BlockDto {
pub block_hash: String,
pub header: BlockHeaderDto,
pub body: BlockBodyDto,
}
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
#[serde(rename_all = "PascalCase")]
pub struct BlockHeaderDto {
pub height: i64,
#[serde(default)]
pub previous_block_hash: String,
}
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
#[serde(rename_all = "PascalCase")]
pub struct BlockBodyDto {
#[serde(default)]
pub transactions: Vec<String>,
}
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
#[serde(rename_all = "PascalCase")]
pub struct TransactionPoolStatusOutput {
#[serde(default)]
pub queued: i64,
#[serde(default)]
pub validated: i64,
}
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
#[serde(rename_all = "PascalCase")]
pub struct CreateRawTransactionInput {
pub from: String,
pub to: String,
pub ref_block_number: i64,
pub ref_block_hash: String,
pub method_name: String,
pub params: String,
}
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
#[serde(rename_all = "PascalCase")]
pub struct CreateRawTransactionOutput {
pub raw_transaction: String,
}
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
#[serde(rename_all = "PascalCase")]
pub struct ExecuteRawTransactionDto {
pub raw_transaction: String,
pub signature: String,
}
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
#[serde(rename_all = "PascalCase")]
pub struct SendRawTransactionInput {
pub transaction: String,
pub signature: String,
pub return_transaction: bool,
}
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
#[serde(rename_all = "PascalCase")]
pub struct SendRawTransactionOutput {
#[serde(alias = "TransactionID")]
pub transaction_id: String,
#[serde(default)]
pub transaction: TransactionDto,
}
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
#[serde(rename_all = "PascalCase")]
pub struct SendTransactionInput {
pub raw_transaction: String,
}
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
#[serde(rename_all = "PascalCase")]
pub struct SendTransactionOutput {
#[serde(alias = "TransactionID")]
pub transaction_id: String,
}
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
#[serde(rename_all = "PascalCase")]
pub struct SendTransactionsInput {
pub raw_transactions: String,
}
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
#[serde(rename_all = "PascalCase")]
pub struct TransactionDto {
#[serde(default)]
pub from: String,
#[serde(default)]
pub to: String,
#[serde(default)]
pub ref_block_number: i64,
#[serde(default)]
pub ref_block_prefix: String,
#[serde(default)]
pub method_name: String,
#[serde(default)]
pub params: String,
#[serde(default)]
pub signature: String,
}
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
#[serde(rename_all = "PascalCase")]
pub struct LogEventDto {
pub address: String,
pub name: String,
#[serde(default, deserialize_with = "null_vec_as_default")]
pub indexed: Vec<String>,
#[serde(default, deserialize_with = "null_string_as_default")]
pub non_indexed: String,
}
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
#[serde(rename_all = "PascalCase")]
pub struct TransactionResultDto {
#[serde(alias = "TransactionID")]
pub transaction_id: String,
pub status: String,
#[serde(default, deserialize_with = "null_vec_as_default")]
pub logs: Vec<LogEventDto>,
#[serde(default, deserialize_with = "null_string_as_default")]
pub bloom: String,
#[serde(default, deserialize_with = "null_json_as_default")]
pub transaction: serde_json::Value,
#[serde(default, deserialize_with = "null_string_as_default")]
pub return_value: String,
#[serde(default, deserialize_with = "null_i64_as_default")]
pub block_number: i64,
#[serde(default, deserialize_with = "null_string_as_default")]
pub block_hash: String,
#[serde(default, deserialize_with = "null_string_as_default")]
pub error: String,
}
impl TransactionResultDto {
pub fn get_transaction_fees(&self) -> HashMap<String, i64> {
let engine = base64::engine::general_purpose::STANDARD;
let mut result = HashMap::new();
for log in &self.logs {
match log.name.as_str() {
"TransactionFeeCharged" => {
let Ok(bytes) = engine.decode(&log.non_indexed) else {
continue;
};
let Ok(event) = TransactionFeeCharged::decode(bytes.as_slice()) else {
continue;
};
result.insert(event.symbol, event.amount);
}
"ResourceTokenCharged" => {
let Ok(bytes) = engine.decode(&log.non_indexed) else {
continue;
};
let Ok(event) = ResourceTokenCharged::decode(bytes.as_slice()) else {
continue;
};
result.insert(event.symbol, event.amount);
}
_ => {}
}
}
result
}
}
fn null_string_as_default<'de, D>(deserializer: D) -> Result<String, D::Error>
where
D: Deserializer<'de>,
{
let value = Option::<String>::deserialize(deserializer)?;
Ok(value.unwrap_or_default())
}
fn null_vec_as_default<'de, D, T>(deserializer: D) -> Result<Vec<T>, D::Error>
where
D: Deserializer<'de>,
T: Deserialize<'de>,
{
let value = Option::<Vec<T>>::deserialize(deserializer)?;
Ok(value.unwrap_or_default())
}
fn null_json_as_default<'de, D>(deserializer: D) -> Result<serde_json::Value, D::Error>
where
D: Deserializer<'de>,
{
let value = Option::<serde_json::Value>::deserialize(deserializer)?;
Ok(value.unwrap_or(serde_json::Value::Null))
}
fn null_i64_as_default<'de, D>(deserializer: D) -> Result<i64, D::Error>
where
D: Deserializer<'de>,
{
let value = Option::<i64>::deserialize(deserializer)?;
Ok(value.unwrap_or_default())
}
fn block_height_map_as_default<'de, D>(deserializer: D) -> Result<HashMap<String, i64>, D::Error>
where
D: Deserializer<'de>,
{
let value = Option::<Map<String, Value>>::deserialize(deserializer)?;
let mut result = HashMap::new();
for (key, value) in value.unwrap_or_default() {
match value {
Value::Number(number) => {
let height = number.as_i64().ok_or_else(|| {
serde::de::Error::custom(format!(
"block height value for '{key}' is not a signed integer"
))
})?;
result.insert(key, height);
}
Value::String(text) => {
if let Ok(height) = text.parse::<i64>() {
result.insert(key, height);
continue;
}
let inverted_height = key.parse::<i64>().map_err(|_| {
serde::de::Error::custom(format!(
"invalid block height map entry '{key}': '{text}'"
))
})?;
result.insert(text, inverted_height);
}
other => {
return Err(serde::de::Error::custom(format!(
"invalid block height map value for '{key}': {other}"
)));
}
}
}
Ok(result)
}
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
#[serde(rename_all = "PascalCase")]
pub struct MerklePathNodeDto {
pub hash: String,
pub is_left_child_node: bool,
}
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
#[serde(rename_all = "PascalCase")]
pub struct MerklePathDto {
#[serde(default)]
pub merkle_path_nodes: Vec<MerklePathNodeDto>,
}
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
#[serde(rename_all = "PascalCase")]
pub struct PeerDto {
#[serde(default)]
pub ip_address: String,
#[serde(flatten)]
pub extra: BTreeMap<String, serde_json::Value>,
}
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
#[serde(rename_all = "PascalCase")]
pub struct NetworkInfoOutput {
#[serde(default)]
pub version: String,
#[serde(flatten)]
pub extra: BTreeMap<String, serde_json::Value>,
}
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
#[serde(rename_all = "PascalCase")]
pub struct TaskQueueInfoDto {
#[serde(flatten)]
pub extra: BTreeMap<String, serde_json::Value>,
}
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
#[serde(rename_all = "PascalCase")]
pub struct CalculateTransactionFeeInput {
pub raw_transaction: String,
}
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
#[serde(rename_all = "PascalCase")]
pub struct CalculateTransactionFeeOutput {
pub success: bool,
#[serde(default)]
pub transaction_fee: HashMap<String, f64>,
}
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
pub struct WebAppErrorResponse {
pub error: WebAppError,
}
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
pub struct WebAppError {
pub code: String,
pub message: String,
#[serde(default)]
pub details: Option<String>,
}
#[cfg(test)]
mod tests {
use super::{ChainStatusDto, LogEventDto, TransactionResultDto};
use aelf_proto::aelf::{Address, ResourceTokenCharged, TransactionFeeCharged};
use base64::Engine;
use prost::Message;
use serde_json::json;
#[test]
fn parses_transaction_fee_logs() {
let engine = base64::engine::general_purpose::STANDARD;
let tx_fee = TransactionFeeCharged {
symbol: "ELF".to_owned(),
amount: 12_345,
};
let resource_fee = ResourceTokenCharged {
symbol: "CPU".to_owned(),
amount: 999,
contract_address: Some(Address {
value: vec![1_u8; 32],
}),
};
let result = TransactionResultDto {
transaction_id: "0x01".to_owned(),
status: "MINED".to_owned(),
logs: vec![
LogEventDto {
address: "addr".to_owned(),
name: "TransactionFeeCharged".to_owned(),
indexed: Vec::new(),
non_indexed: engine.encode(tx_fee.encode_to_vec()),
},
LogEventDto {
address: "addr".to_owned(),
name: "ResourceTokenCharged".to_owned(),
indexed: Vec::new(),
non_indexed: engine.encode(resource_fee.encode_to_vec()),
},
],
bloom: String::new(),
transaction: serde_json::Value::Null,
return_value: String::new(),
block_number: 1,
block_hash: "0x02".to_owned(),
error: String::new(),
};
let fees = result.get_transaction_fees();
assert_eq!(fees.get("ELF"), Some(&12_345));
assert_eq!(fees.get("CPU"), Some(&999));
}
#[test]
fn parses_chain_status_hash_to_height_maps() {
let status: ChainStatusDto = serde_json::from_value(json!({
"ChainId": "AELF",
"Branches": {
"abc": 42
},
"NotLinkedBlocks": null,
"LongestChainHeight": 42,
"LongestChainHash": "abc",
"GenesisBlockHash": "genesis",
"GenesisContractAddress": "contract",
"LastIrreversibleBlockHash": "lib",
"LastIrreversibleBlockHeight": 40,
"BestChainHash": "abc",
"BestChainHeight": 42
}))
.expect("chain status");
assert_eq!(status.branches.get("abc"), Some(&42));
assert!(status.not_linked_blocks.is_empty());
}
#[test]
fn parses_chain_status_height_to_hash_maps() {
let status: ChainStatusDto = serde_json::from_value(json!({
"ChainId": "AELF",
"Branches": {
"42": "abc"
},
"NotLinkedBlocks": {
"7": "def"
},
"LongestChainHeight": 42,
"LongestChainHash": "abc",
"GenesisBlockHash": "genesis",
"GenesisContractAddress": "contract",
"LastIrreversibleBlockHash": "lib",
"LastIrreversibleBlockHeight": 40,
"BestChainHash": "abc",
"BestChainHeight": 42
}))
.expect("chain status");
assert_eq!(status.branches.get("abc"), Some(&42));
assert_eq!(status.not_linked_blocks.get("def"), Some(&7));
}
}