From 40a4a43941dfc59ab025ee799fe362348405fc94 Mon Sep 17 00:00:00 2001 From: "coderabbitai[bot]" <136622811+coderabbitai[bot]@users.noreply.github.com> Date: Thu, 19 Mar 2026 12:16:12 +0000 Subject: [PATCH] CodeRabbit Generated Unit Tests: Add generated unit tests --- hypersync-format/src/types/mod.rs | 181 +++++++++++++++++- .../test-data/tempo_transaction.json | 18 ++ hypersync-format/tests/deserialize_json.rs | 10 + 3 files changed, 208 insertions(+), 1 deletion(-) create mode 100644 hypersync-format/test-data/tempo_transaction.json diff --git a/hypersync-format/src/types/mod.rs b/hypersync-format/src/types/mod.rs index 5edc3c4..a32de64 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,165 @@ mod tests { use super::*; + // Minimal required fields for a Transaction (all non-optional, non-defaulted fields). + // input and value are intentionally omitted here so each test can supply them explicitly. + fn minimal_tx_json() -> Value { + json!({ + "blockHash": "0x0000000000000000000000000000000000000000000000000000000000000001", + "blockNumber": "0x1", + "gas": "0x5208", + "hash": "0x0000000000000000000000000000000000000000000000000000000000000002", + "nonce": "0x1", + "transactionIndex": "0x0" + }) + } + + // ----------------------------------------------------------------------- + // Tests for deserialize_data_or_null (Transaction.input field) + // ----------------------------------------------------------------------- + + #[test] + fn test_transaction_input_null_defaults_to_empty() { + let mut obj = minimal_tx_json(); + obj["input"] = json!(null); + let tx: Transaction = serde_json::from_value(obj).unwrap(); + assert_eq!(tx.input, Data::default()); + } + + #[test] + fn test_transaction_input_missing_defaults_to_empty() { + // The key is absent entirely (serde(default) handles this case). + let obj = minimal_tx_json(); + let tx: Transaction = serde_json::from_value(obj).unwrap(); + assert_eq!(tx.input, Data::default()); + } + + #[test] + fn test_transaction_input_present_is_decoded() { + let mut obj = minimal_tx_json(); + obj["input"] = json!("0xdeadbeef"); + let tx: Transaction = serde_json::from_value(obj).unwrap(); + assert_eq!(tx.input, Data::from([0xde, 0xad, 0xbe, 0xef])); + } + + #[test] + fn test_transaction_input_empty_hex_is_empty_data() { + let mut obj = minimal_tx_json(); + obj["input"] = json!("0x"); + let tx: Transaction = serde_json::from_value(obj).unwrap(); + assert_eq!(tx.input, Data::default()); + } + + // ----------------------------------------------------------------------- + // Tests for deserialize_quantity_or_null (Transaction.value field) + // ----------------------------------------------------------------------- + + #[test] + fn test_transaction_value_null_defaults_to_zero() { + let mut obj = minimal_tx_json(); + obj["value"] = json!(null); + let tx: Transaction = serde_json::from_value(obj).unwrap(); + assert_eq!(tx.value, Quantity::default()); + } + + #[test] + fn test_transaction_value_missing_defaults_to_zero() { + // The key is absent entirely (serde(default) handles this case). + let obj = minimal_tx_json(); + let tx: Transaction = serde_json::from_value(obj).unwrap(); + assert_eq!(tx.value, Quantity::default()); + } + + #[test] + fn test_transaction_value_present_is_decoded() { + let mut obj = minimal_tx_json(); + obj["value"] = json!("0x14"); + let tx: Transaction = serde_json::from_value(obj).unwrap(); + assert_eq!(tx.value, Quantity::from([0x14u8])); + } + + #[test] + fn test_transaction_value_zero_hex_is_zero_quantity() { + let mut obj = minimal_tx_json(); + obj["value"] = json!("0x0"); + let tx: Transaction = serde_json::from_value(obj).unwrap(); + assert_eq!(tx.value, Quantity::default()); + } + + #[test] + fn test_transaction_value_large_amount() { + let mut obj = minimal_tx_json(); + // 1 ETH in wei = 0xDE0B6B3A7640000 + obj["value"] = json!("0xde0b6b3a7640000"); + let tx: Transaction = serde_json::from_value(obj).unwrap(); + assert_eq!( + tx.value.as_ref(), + &[0x0d, 0xe0, 0xb6, 0xb3, 0xa7, 0x64, 0x00, 0x00] + ); + } + + // ----------------------------------------------------------------------- + // Tests for the combined Tempo-style scenario (both null / both missing) + // ----------------------------------------------------------------------- + + #[test] + fn test_transaction_tempo_both_null() { + // Tempo blockchain sends type 0x76 transactions without input or value. + let mut obj = minimal_tx_json(); + obj["input"] = json!(null); + obj["value"] = json!(null); + obj["type"] = json!("0x76"); + let tx: Transaction = serde_json::from_value(obj).unwrap(); + assert_eq!(tx.input, Data::default()); + assert_eq!(tx.value, Quantity::default()); + assert_eq!(tx.type_.unwrap().0, 0x76); + } + + #[test] + fn test_transaction_tempo_both_missing() { + // Fields are entirely absent (no key in JSON object). + let mut obj = minimal_tx_json(); + obj["type"] = json!("0x76"); + let tx: Transaction = serde_json::from_value(obj).unwrap(); + assert_eq!(tx.input, Data::default()); + assert_eq!(tx.value, Quantity::default()); + } + + // ----------------------------------------------------------------------- + // Regression: normal transactions with both input and value still work + // ----------------------------------------------------------------------- + + #[test] + fn test_transaction_normal_input_and_value_preserved() { + let mut obj = minimal_tx_json(); + obj["input"] = json!("0xaabbcc"); + obj["value"] = json!("0x214e8348c4efff9"); + let tx: Transaction = serde_json::from_value(obj).unwrap(); + assert_eq!(tx.input, Data::from([0xaa, 0xbb, 0xcc])); + assert_eq!( + tx.value.as_ref(), + &[0x02, 0x14, 0xe8, 0x34, 0x8c, 0x4e, 0xff, 0xf9] + ); + } + + // ----------------------------------------------------------------------- + // Boundary: null is distinct from the string "0x0" for value + // ----------------------------------------------------------------------- + + #[test] + fn test_transaction_value_null_and_zero_hex_produce_same_result() { + let mut null_obj = minimal_tx_json(); + null_obj["value"] = json!(null); + let null_tx: Transaction = serde_json::from_value(null_obj).unwrap(); + + let mut zero_obj = minimal_tx_json(); + zero_obj["value"] = json!("0x0"); + let zero_tx: Transaction = serde_json::from_value(zero_obj).unwrap(); + + assert_eq!(null_tx.value, zero_tx.value); + assert_eq!(null_tx.value, Quantity::default()); + } + #[test] fn handle_zeta_null_effective_gas_price() { // real world breaking example on zeta @@ -383,4 +562,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..10aee23 --- /dev/null +++ b/hypersync-format/test-data/tempo_transaction.json @@ -0,0 +1,18 @@ +{ + "blockHash": "0xaabbccddeeff00112233445566778899aabbccddeeff00112233445566778899", + "blockNumber": "0x1a2b3c", + "from": "0x735b14bb79463307aacbed86daf3322b1e6226ab", + "gas": "0x5208", + "gasPrice": "0x3b9aca00", + "hash": "0x1122334455667788990011223344556677889900112233445566778899001122", + "input": null, + "nonce": "0x1", + "to": "0x91d18e54daf4f677cb28167158d6dd21f6ab3921", + "transactionIndex": "0x0", + "value": null, + "type": "0x76", + "chainId": "0x1", + "v": "0x1", + "r": "0xd706ba7cebca09321e83d3b3aadf4213392f7a783b92881f4b63fcfe8f79b6ad", + "s": "0x3076c8cea03d3178fb132d3e4c778f240cdd6642f5d36310c72081d61b454f5d" +} \ No newline at end of file diff --git a/hypersync-format/tests/deserialize_json.rs b/hypersync-format/tests/deserialize_json.rs index ac9c25d..8638069 100644 --- a/hypersync-format/tests/deserialize_json.rs +++ b/hypersync-format/tests/deserialize_json.rs @@ -91,3 +91,13 @@ fn test_tron_block_without_tx_deserialize() { let file = read_json_file("tron_block_without_tx.json"); let _: Block = deserialize_with_path(&file).unwrap(); } + +/// Verify that Tempo-style transactions (type 0x76) with null `input` and `value` +/// fields deserialize successfully, with both fields falling back to their defaults. +#[test] +fn test_tempo_transaction_null_input_and_value() { + let file = read_json_file("tempo_transaction.json"); + let tx: Transaction = deserialize_with_path(&file).unwrap(); + assert_eq!(tx.input, Data::default(), "null input should default to empty Data"); + assert_eq!(tx.value, Quantity::default(), "null value should default to zero Quantity"); +} \ No newline at end of file