Skip to content
Open
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
310 changes: 309 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,294 @@ mod tests {

use super::*;

// Minimal valid Transaction JSON base (without input and value, which now have defaults).
// blockHash and hash are Hash = FixedSizeData<32>, requiring 32 bytes (64 hex chars).
fn minimal_transaction_json() -> Value {
json!({
"blockHash": "0x0000000000000000000000000000000000000000000000000000000000000000",
"blockNumber": "0x1",
"gas": "0x5208",
"hash": "0x0000000000000000000000000000000000000000000000000000000000000001",
"nonce": "0x0",
"transactionIndex": "0x0"
})
}

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

#[test]
fn transaction_input_null_defaults_to_empty_data() {
let mut json = minimal_transaction_json();
json["input"] = json!(null);

let tx: Transaction =
serde_json::from_value(json).expect("should deserialize transaction with null input");

assert_eq!(tx.input, Data::default());
}

#[test]
fn transaction_input_missing_defaults_to_empty_data() {
// Field omitted entirely — serde(default) should trigger the deserializer with default
let json = minimal_transaction_json();

let tx: Transaction = serde_json::from_value(json)
.expect("should deserialize transaction with missing input");

assert_eq!(tx.input, Data::default());
}

#[test]
fn transaction_input_valid_hex_is_parsed() {
let mut json = minimal_transaction_json();
json["input"] = json!("0x1234");

let tx: Transaction =
serde_json::from_value(json).expect("should deserialize transaction with hex input");

assert_eq!(tx.input, Data::from([0x12u8, 0x34u8]));
}

#[test]
fn transaction_input_empty_hex_is_empty_data() {
let mut json = minimal_transaction_json();
json["input"] = json!("0x");

let tx: Transaction = serde_json::from_value(json)
.expect("should deserialize transaction with empty hex input");

assert_eq!(tx.input, Data::default());
}

#[test]
fn transaction_input_nonempty_data_roundtrips() {
let mut json = minimal_transaction_json();
json["input"] = json!("0xdeadbeef");

let tx: Transaction =
serde_json::from_value(json).expect("should deserialize transaction with data input");

assert_eq!(tx.input, Data::from([0xdeu8, 0xadu8, 0xbeu8, 0xefu8]));
}

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

#[test]
fn transaction_value_null_defaults_to_zero_quantity() {
let mut json = minimal_transaction_json();
json["value"] = json!(null);

let tx: Transaction =
serde_json::from_value(json).expect("should deserialize transaction with null value");

assert_eq!(tx.value, Quantity::default());
}

#[test]
fn transaction_value_missing_defaults_to_zero_quantity() {
let json = minimal_transaction_json();

let tx: Transaction = serde_json::from_value(json)
.expect("should deserialize transaction with missing value");

assert_eq!(tx.value, Quantity::default());
}

#[test]
fn transaction_value_valid_hex_is_parsed() {
let mut json = minimal_transaction_json();
json["value"] = json!("0x1a");

let tx: Transaction =
serde_json::from_value(json).expect("should deserialize transaction with hex value");

assert_eq!(tx.value, Quantity::from(26u64));
}

#[test]
fn transaction_value_zero_hex_is_zero_quantity() {
let mut json = minimal_transaction_json();
json["value"] = json!("0x0");

let tx: Transaction = serde_json::from_value(json)
.expect("should deserialize transaction with zero hex value");

assert_eq!(tx.value, Quantity::default());
}

// ---- Tempo blockchain scenario (type 0x76, both input and value absent/null) ----

#[test]
fn tempo_transaction_type_0x76_both_input_and_value_null() {
let mut json = minimal_transaction_json();
json["type"] = json!("0x76");
json["input"] = json!(null);
json["value"] = json!(null);

let tx: Transaction = serde_json::from_value(json)
.expect("Tempo 0x76 transaction with null input and value should deserialize");

assert_eq!(tx.input, Data::default());
assert_eq!(tx.value, Quantity::default());
assert_eq!(tx.type_, Some(TransactionType::from(0x76u8)));
}

#[test]
fn tempo_transaction_type_0x76_both_input_and_value_missing() {
let mut json = minimal_transaction_json();
json["type"] = json!("0x76");

let tx: Transaction = serde_json::from_value(json)
.expect("Tempo 0x76 transaction with missing input and value should deserialize");

assert_eq!(tx.input, Data::default());
assert_eq!(tx.value, Quantity::default());
}

// ---- Regression: normal transaction still parses input and value correctly ----

#[test]
fn normal_transaction_with_both_input_and_value_parses_correctly() {
let mut json = minimal_transaction_json();
json["input"] = json!("0xabcd");
json["value"] = json!("0xff");

let tx: Transaction = serde_json::from_value(json)
.expect("transaction with both input and value should deserialize");

assert_eq!(tx.input, Data::from([0xabu8, 0xcdu8]));
assert_eq!(tx.value, Quantity::from(255u64));
}

// ---- Negative / error cases ----

#[test]
fn transaction_input_invalid_hex_no_prefix_is_error() {
// Data requires "0x" prefix; bare hex should fail deserialization
let mut json = minimal_transaction_json();
json["input"] = json!("deadbeef"); // missing 0x prefix

let result: Result<Transaction, _> = serde_json::from_value(json);
assert!(
result.is_err(),
"input without 0x prefix should fail deserialization"
);
}

#[test]
fn transaction_value_empty_quantity_hex_is_error() {
// "0x" alone is not valid for Quantity (only valid for Data)
let mut json = minimal_transaction_json();
json["value"] = json!("0x"); // empty Quantity is invalid

let result: Result<Transaction, _> = serde_json::from_value(json);
assert!(
result.is_err(),
"empty 0x quantity should fail deserialization"
);
}

// ---- Mixed null / valid combinations ----

#[test]
fn transaction_null_input_with_valid_value() {
let mut json = minimal_transaction_json();
json["input"] = json!(null);
json["value"] = json!("0x1");

let tx: Transaction = serde_json::from_value(json)
.expect("null input with valid value should deserialize");

assert_eq!(tx.input, Data::default());
assert_eq!(tx.value, Quantity::from(1u64));
}

#[test]
fn transaction_valid_input_with_null_value() {
let mut json = minimal_transaction_json();
json["input"] = json!("0xab");
json["value"] = json!(null);

let tx: Transaction = serde_json::from_value(json)
.expect("valid input with null value should deserialize");

assert_eq!(tx.input, Data::from([0xabu8]));
assert_eq!(tx.value, Quantity::default());
}

// ---- Boundary value tests ----

#[test]
fn transaction_value_large_boundary() {
// A 256-bit value (common max in EVM for wei amounts)
let mut json = minimal_transaction_json();
json["value"] = json!("0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff");

let tx: Transaction =
serde_json::from_value(json).expect("large boundary value should deserialize");

// Should parse to 32 bytes of 0xff
assert_eq!(tx.value.as_ref(), &[0xffu8; 32]);
}

#[test]
fn transaction_input_large_boundary() {
// A long input data payload (simulate contract calldata)
let mut json = minimal_transaction_json();
// 32-byte input: function selector + 28 bytes of zero-padded argument
json["input"] = json!("0xa9059cbb0000000000000000000000001234567890abcdef1234567890abcdef123456780000000000000000000000000000000000000000000000000de0b6b3a7640000");

let tx: Transaction =
serde_json::from_value(json).expect("large input calldata should deserialize");

assert_eq!(tx.input.as_ref().len(), 68); // 4 + 32 + 32 bytes
}

// ---- Serialization roundtrip ----

#[test]
fn transaction_default_input_value_serde_roundtrip() {
// Build a transaction via JSON where input and value are both present
let mut json = minimal_transaction_json();
json["input"] = json!("0x1234");
json["value"] = json!("0x64");

let tx: Transaction =
serde_json::from_value(json).expect("transaction should deserialize");

// Serialize back to JSON and deserialize again
let serialized = serde_json::to_value(&tx).expect("transaction should serialize");
let tx2: Transaction =
serde_json::from_value(serialized).expect("roundtrip should deserialize");

assert_eq!(tx.input, tx2.input);
assert_eq!(tx.value, tx2.value);
assert_eq!(tx, tx2);
}

#[test]
fn transaction_defaulted_fields_roundtrip_serialization() {
// When input and value are omitted (defaulted), the serialized form should have them
// as "0x" and "0x0" respectively, and roundtrip should preserve equality
let json = minimal_transaction_json();

let tx: Transaction =
serde_json::from_value(json).expect("minimal transaction should deserialize");

let serialized = serde_json::to_value(&tx).expect("should serialize");

// After serialization, input is "0x" and value is "0x0"
assert_eq!(serialized["input"], json!("0x"));
assert_eq!(serialized["value"], json!("0x0"));

// Roundtrip should produce the same struct
let tx2: Transaction =
serde_json::from_value(serialized).expect("serialized form should deserialize");

assert_eq!(tx.input, tx2.input);
assert_eq!(tx.value, tx2.value);
}

#[test]
fn handle_zeta_null_effective_gas_price() {
// real world breaking example on zeta
Expand Down Expand Up @@ -383,4 +691,4 @@ mod tests {
let _: TransactionReceipt =
serde_json::from_value(json).expect("should handle undefined effective gas price");
}
}
}
Loading