Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
160 changes: 159 additions & 1 deletion hypersync-format/src/types/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,22 @@ pub struct Block<Tx> {
pub transactions: Vec<Tx>,
}

/// Deserialize a Quantity that may be null or missing, defaulting to zero.
fn deserialize_quantity_or_null<'de, D>(deserializer: D) -> Result<Quantity, D::Error>
where
D: Deserializer<'de>,
{
Ok(Option::<Quantity>::deserialize(deserializer)?.unwrap_or_default())
}

/// Deserialize Data that may be null or missing, defaulting to empty.
fn deserialize_data_or_null<'de, D>(deserializer: D) -> Result<Data, D::Error>
where
D: Deserializer<'de>,
{
Ok(Option::<Data>::deserialize(deserializer)?.unwrap_or_default())
}

/// Evm transaction object
///
/// See ethereum rpc spec for the meaning of fields
Expand All @@ -85,10 +101,14 @@ pub struct Transaction {
pub gas: Quantity,
pub gas_price: Option<Quantity>,
pub hash: Hash,
// In the Tempo blockchain, transactions don't need to have an input, and don't if they are of type 0x76
#[serde(default, deserialize_with = "deserialize_data_or_null")]
pub input: Data,
pub nonce: Quantity,
pub to: Option<Address>,
pub transaction_index: TransactionIndex,
// In the Tempo blockchain, transactions don't need to have an input, and don't if they are of type 0x76
#[serde(default, deserialize_with = "deserialize_quantity_or_null")]
pub value: Quantity,
#[serde(rename = "type")]
pub type_: Option<TransactionType>,
Expand Down Expand Up @@ -339,6 +359,144 @@ mod tests {

use super::*;

// Helper that builds a minimal valid Transaction JSON, overriding specific fields.
fn minimal_transaction_json() -> Value {
json!({
"blockHash": "0x0000000000000000000000000000000000000000000000000000000000000000",
"blockNumber": "0x1",
"gas": "0x5208",
"hash": "0x0000000000000000000000000000000000000000000000000000000000000001",
"input": "0x",
"nonce": "0x0",
"transactionIndex": "0x0",
"value": "0x0"
})
}

// --- Tests for deserialize_data_or_null (Transaction.input) ---

#[test]
fn transaction_input_null_deserializes_to_empty_data() {
let mut json = minimal_transaction_json();
json["input"] = json!(null);
let tx: Transaction = serde_json::from_value(json).expect("should handle null input");
assert_eq!(tx.input, Data::default(), "null input should produce empty Data");
}

#[test]
fn transaction_input_missing_deserializes_to_empty_data() {
let mut json = minimal_transaction_json();
json.as_object_mut().unwrap().remove("input");
let tx: Transaction = serde_json::from_value(json).expect("should handle missing input");
assert_eq!(tx.input, Data::default(), "missing input should produce empty Data");
}

#[test]
fn transaction_input_present_deserializes_correctly() {
let mut json = minimal_transaction_json();
json["input"] = json!("0xdeadbeef");
let tx: Transaction = serde_json::from_value(json).expect("should handle present input");
assert_eq!(
tx.input,
Data::from(vec![0xde, 0xad, 0xbe, 0xef]),
"input should parse hex bytes correctly"
);
}

#[test]
fn transaction_input_empty_hex_deserializes_to_empty_data() {
let mut json = minimal_transaction_json();
json["input"] = json!("0x");
let tx: Transaction = serde_json::from_value(json).expect("should handle empty hex input");
assert_eq!(tx.input, Data::default(), "empty hex input should produce empty Data");
}

// --- Tests for deserialize_quantity_or_null (Transaction.value) ---

#[test]
fn transaction_value_null_deserializes_to_zero_quantity() {
let mut json = minimal_transaction_json();
json["value"] = json!(null);
let tx: Transaction = serde_json::from_value(json).expect("should handle null value");
assert_eq!(tx.value, Quantity::default(), "null value should produce zero Quantity");
}

#[test]
fn transaction_value_missing_deserializes_to_zero_quantity() {
let mut json = minimal_transaction_json();
json.as_object_mut().unwrap().remove("value");
let tx: Transaction = serde_json::from_value(json).expect("should handle missing value");
assert_eq!(tx.value, Quantity::default(), "missing value should produce zero Quantity");
}

#[test]
fn transaction_value_present_deserializes_correctly() {
let mut json = minimal_transaction_json();
json["value"] = json!("0xde0b6b3a7640000");
let tx: Transaction = serde_json::from_value(json).expect("should handle present value");
assert_eq!(
tx.value,
Quantity::from(0xde0b6b3a7640000u64),
"value should parse hex quantity correctly"
);
}

#[test]
fn transaction_value_zero_hex_deserializes_to_zero_quantity() {
let mut json = minimal_transaction_json();
json["value"] = json!("0x0");
let tx: Transaction = serde_json::from_value(json).expect("should handle zero hex value");
assert_eq!(tx.value, Quantity::default(), "0x0 value should equal zero Quantity");
}

// --- Tests for the Tempo blockchain type 0x76 case (both fields null/missing) ---

#[test]
fn transaction_tempo_type_both_input_and_value_null() {
// Tempo blockchain: transactions of type 0x76 have no input and no value
let mut json = minimal_transaction_json();
json["input"] = json!(null);
json["value"] = json!(null);
json["type"] = json!("0x76");
let tx: Transaction =
serde_json::from_value(json).expect("should handle Tempo type 0x76 with null fields");
assert_eq!(tx.input, Data::default(), "null input should produce empty Data");
assert_eq!(tx.value, Quantity::default(), "null value should produce zero Quantity");
assert_eq!(
tx.type_,
Some(TransactionType::from(0x76u8)),
"type should be 0x76"
);
}

#[test]
fn transaction_tempo_type_both_input_and_value_missing() {
let mut json = minimal_transaction_json();
{
let obj = json.as_object_mut().unwrap();
obj.remove("input");
obj.remove("value");
obj.insert("type".to_string(), json!("0x76"));
}
let tx: Transaction = serde_json::from_value(json)
.expect("should handle Tempo type 0x76 with missing fields");
assert_eq!(tx.input, Data::default(), "missing input should produce empty Data");
assert_eq!(tx.value, Quantity::default(), "missing value should produce zero Quantity");
}

// --- Regression: standard transaction with all fields present still works ---

#[test]
fn transaction_standard_fields_roundtrip() {
let json = minimal_transaction_json();
let tx: Transaction =
serde_json::from_value(json.clone()).expect("standard transaction should deserialize");
assert_eq!(tx.input, Data::default());
assert_eq!(tx.value, Quantity::default());
assert_eq!(tx.gas, Quantity::from(0x5208u64));
assert_eq!(tx.nonce, Quantity::default());
}

#[test]
fn handle_zeta_null_effective_gas_price() {
// real world breaking example on zeta
Expand Down Expand Up @@ -383,4 +541,4 @@ mod tests {
let _: TransactionReceipt =
serde_json::from_value(json).expect("should handle undefined effective gas price");
}
}
}
14 changes: 14 additions & 0 deletions hypersync-format/test-data/tempo_transaction.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{
"blockHash": "0xa1b2c3d4e5f60000000000000000000000000000000000000000000000000000",
"blockNumber": "0x1",
"from": "0xae40b5aa27e47b2992badd63cb8c7f87e605d6a4",
"gas": "0x5208",
"gasPrice": "0x3b9aca00",
"hash": "0xdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef",
"input": null,
"nonce": "0x0",
"to": "0x735b14bb79463307aacbed86daf3322b1e6226ab",
"transactionIndex": "0x0",
"value": null,
"type": "0x76"
}
24 changes: 24 additions & 0 deletions hypersync-format/tests/deserialize_json.rs
Original file line number Diff line number Diff line change
Expand Up @@ -91,3 +91,27 @@ fn test_tron_block_without_tx_deserialize() {
let file = read_json_file("tron_block_without_tx.json");
let _: Block<Hash> = deserialize_with_path(&file).unwrap();
}

#[test]
fn test_tempo_transaction_null_input_and_value() {
// Tempo blockchain: transactions of type 0x76 have null input and null value fields.
// Verifies that deserialize_data_or_null and deserialize_quantity_or_null handle null correctly.
let file = read_json_file("tempo_transaction.json");
let tx: Transaction = serde_json::from_str(&file)
.expect("should deserialize Tempo transaction with null input and value");
assert_eq!(
tx.input,
Data::default(),
"null input should produce empty Data"
);
assert_eq!(
tx.value,
Quantity::default(),
"null value should produce zero Quantity"
);
assert_eq!(
tx.type_,
Some(TransactionType::from(0x76u8)),
"type should be 0x76"
);
}
Loading