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
181 changes: 180 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,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
Expand Down Expand Up @@ -383,4 +562,4 @@ mod tests {
let _: TransactionReceipt =
serde_json::from_value(json).expect("should handle undefined effective gas price");
}
}
}
18 changes: 18 additions & 0 deletions hypersync-format/test-data/tempo_transaction.json
Original file line number Diff line number Diff line change
@@ -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"
}
10 changes: 10 additions & 0 deletions hypersync-format/tests/deserialize_json.rs
Original file line number Diff line number Diff line change
Expand Up @@ -91,3 +91,13 @@ 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();
}

/// 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");
}
Loading