diff --git a/hypersync-format/src/types/mod.rs b/hypersync-format/src/types/mod.rs index 5edc3c4..f3d3eff 100644 --- a/hypersync-format/src/types/mod.rs +++ b/hypersync-format/src/types/mod.rs @@ -72,6 +72,22 @@ pub struct Block { pub transactions: Vec, } +/// Deserialize a Quantity that may be null or missing, defaulting to zero. +fn deserialize_quantity_or_null<'de, D>(deserializer: D) -> Result +where + D: Deserializer<'de>, +{ + Ok(Option::::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 +where + D: Deserializer<'de>, +{ + Ok(Option::::deserialize(deserializer)?.unwrap_or_default()) +} + /// Evm transaction object /// /// See ethereum rpc spec for the meaning of fields @@ -85,10 +101,14 @@ pub struct Transaction { pub gas: Quantity, pub gas_price: Option, 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
, 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, @@ -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 @@ -383,4 +541,4 @@ mod tests { let _: TransactionReceipt = serde_json::from_value(json).expect("should handle undefined effective gas price"); } -} +} \ No newline at end of file diff --git a/hypersync-format/test-data/tempo_transaction.json b/hypersync-format/test-data/tempo_transaction.json new file mode 100644 index 0000000..b431dc6 --- /dev/null +++ b/hypersync-format/test-data/tempo_transaction.json @@ -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" +} \ No newline at end of file diff --git a/hypersync-format/tests/deserialize_json.rs b/hypersync-format/tests/deserialize_json.rs index ac9c25d..579331b 100644 --- a/hypersync-format/tests/deserialize_json.rs +++ b/hypersync-format/tests/deserialize_json.rs @@ -91,3 +91,27 @@ fn test_tron_block_without_tx_deserialize() { let file = read_json_file("tron_block_without_tx.json"); let _: Block = 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" + ); +} \ No newline at end of file