diff --git a/crates/contract/src/call.rs b/crates/contract/src/call.rs index 22490ea5751..45bfb11f38f 100644 --- a/crates/contract/src/call.rs +++ b/crates/contract/src/call.rs @@ -3,7 +3,11 @@ use alloy_dyn_abi::{DynSolValue, FunctionExt, JsonAbiExt}; use alloy_json_abi::Function; use alloy_primitives::{Address, Bytes, U256, U64}; use alloy_providers::provider::TempProvider; -use alloy_rpc_types::{state::StateOverride, BlockId, CallInput, CallRequest, TransactionReceipt}; +use alloy_rpc_types::{ + request::{TransactionInput, TransactionRequest}, + state::StateOverride, + BlockId, TransactionReceipt, +}; use alloy_sol_types::SolCall; use std::{ future::{Future, IntoFuture}, @@ -189,7 +193,7 @@ impl CallDecoder for () { pub struct CallBuilder { // TODO: this will not work with `send_transaction` and does not differentiate between EIP-1559 // and legacy tx - request: CallRequest, + request: TransactionRequest, block: Option, state: Option, provider: P, @@ -249,7 +253,8 @@ impl RawCallBuilder

{ impl CallBuilder { fn new_inner(provider: P, input: Bytes, decoder: D) -> Self { - let request = CallRequest { input: CallInput::new(input), ..Default::default() }; + let request = + TransactionRequest { input: TransactionInput::new(input), ..Default::default() }; Self { request, decoder, provider, block: None, state: None } } @@ -272,7 +277,7 @@ impl CallBuilder { /// Sets the `gas` field in the transaction to the provided value pub fn gas(mut self, gas: U256) -> Self { - self.request = self.request.gas(gas); + self.request = self.request.gas_limit(gas); self } @@ -280,7 +285,8 @@ impl CallBuilder { /// If the internal transaction is an EIP-1559 one, then it sets both /// `max_fee_per_gas` and `max_priority_fee_per_gas` to the same value pub fn gas_price(mut self, gas_price: U256) -> Self { - self.request = self.request.gas_price(gas_price); + self.request = self.request.max_fee_per_gas(gas_price); + self.request = self.request.max_priority_fee_per_gas(gas_price); self } diff --git a/crates/providers/src/provider.rs b/crates/providers/src/provider.rs index d179c0a08f2..934c5c99cdc 100644 --- a/crates/providers/src/provider.rs +++ b/crates/providers/src/provider.rs @@ -8,9 +8,9 @@ use alloy_rpc_trace_types::{ parity::LocalizedTransactionTrace, }; use alloy_rpc_types::{ - state::StateOverride, AccessListWithGasUsed, Block, BlockId, BlockNumberOrTag, CallRequest, - EIP1186AccountProofResponse, FeeHistory, Filter, Log, SyncStatus, Transaction, - TransactionReceipt, + request::TransactionRequest, state::StateOverride, AccessListWithGasUsed, Block, BlockId, + BlockNumberOrTag, EIP1186AccountProofResponse, FeeHistory, Filter, Log, SyncStatus, + Transaction, TransactionReceipt, }; use alloy_transport::{BoxTransport, Transport, TransportErrorKind, TransportResult}; use alloy_transport_http::Http; @@ -141,24 +141,28 @@ pub trait TempProvider: Send + Sync { /// Gets syncing info. async fn syncing(&self) -> TransportResult; - /// Execute a smart contract call with [CallRequest] without publishing a transaction. - async fn call(&self, tx: CallRequest, block: Option) -> TransportResult; + /// Execute a smart contract call with [TransactionRequest] without publishing a transaction. + async fn call(&self, tx: TransactionRequest, block: Option) -> TransportResult; - /// Execute a smart contract call with [CallRequest] and state overrides, without publishing a - /// transaction. + /// Execute a smart contract call with [TransactionRequest] and state overrides, without + /// publishing a transaction. /// /// # Note /// /// Not all client implementations support state overrides. async fn call_with_overrides( &self, - tx: CallRequest, + tx: TransactionRequest, block: Option, state: StateOverride, ) -> TransportResult; /// Estimate the gas needed for a transaction. - async fn estimate_gas(&self, tx: CallRequest, block: Option) -> TransportResult; + async fn estimate_gas( + &self, + tx: TransactionRequest, + block: Option, + ) -> TransportResult; /// Sends an already-signed transaction. async fn send_raw_transaction(&self, tx: Bytes) -> TransportResult; @@ -183,7 +187,7 @@ pub trait TempProvider: Send + Sync { async fn create_access_list( &self, - request: CallRequest, + request: TransactionRequest, block: Option, ) -> TransportResult; @@ -379,20 +383,20 @@ impl TempProvider for Provider { self.inner.prepare("eth_syncing", ()).await } - /// Execute a smart contract call with [CallRequest] without publishing a transaction. - async fn call(&self, tx: CallRequest, block: Option) -> TransportResult { + /// Execute a smart contract call with [TransactionRequest] without publishing a transaction. + async fn call(&self, tx: TransactionRequest, block: Option) -> TransportResult { self.inner.prepare("eth_call", (tx, block.unwrap_or_default())).await } - /// Execute a smart contract call with [CallRequest] and state overrides, without publishing a - /// transaction. + /// Execute a smart contract call with [TransactionRequest] and state overrides, without + /// publishing a transaction. /// /// # Note /// /// Not all client implementations support state overrides. async fn call_with_overrides( &self, - tx: CallRequest, + tx: TransactionRequest, block: Option, state: StateOverride, ) -> TransportResult { @@ -400,7 +404,11 @@ impl TempProvider for Provider { } /// Estimate the gas needed for a transaction. - async fn estimate_gas(&self, tx: CallRequest, block: Option) -> TransportResult { + async fn estimate_gas( + &self, + tx: TransactionRequest, + block: Option, + ) -> TransportResult { if let Some(block_id) = block { self.inner.prepare("eth_estimateGas", (tx, block_id)).await } else { @@ -468,7 +476,7 @@ impl TempProvider for Provider { async fn create_access_list( &self, - request: CallRequest, + request: TransactionRequest, block: Option, ) -> TransportResult { self.inner.prepare("eth_createAccessList", (request, block.unwrap_or_default())).await diff --git a/crates/rpc-trace-types/src/tracerequest.rs b/crates/rpc-trace-types/src/tracerequest.rs index 7c1a98501fc..f6c6c23c5a3 100644 --- a/crates/rpc-trace-types/src/tracerequest.rs +++ b/crates/rpc-trace-types/src/tracerequest.rs @@ -1,7 +1,7 @@ //! Builder style functions for `trace_call` use crate::parity::TraceType; -use alloy_rpc_types::{state::StateOverride, BlockId, BlockOverrides, CallRequest}; +use alloy_rpc_types::{request::TransactionRequest, state::StateOverride, BlockId, BlockOverrides}; use serde::{Deserialize, Serialize}; use std::collections::HashSet; @@ -9,7 +9,7 @@ use std::collections::HashSet; #[derive(Debug, Serialize, Deserialize, Default)] pub struct TraceCallRequest { /// call request object - pub call: CallRequest, + pub call: TransactionRequest, /// trace types pub trace_types: HashSet, /// Optional: blockId @@ -21,8 +21,8 @@ pub struct TraceCallRequest { } impl TraceCallRequest { - /// Returns a new [`TraceCallRequest`] given a [`CallRequest`] and [`HashSet`] - pub fn new(call: CallRequest) -> Self { + /// Returns a new [`TraceCallRequest`] given a [`TransactionRequest`] and [`HashSet`] + pub fn new(call: TransactionRequest) -> Self { Self { call, trace_types: HashSet::new(), diff --git a/crates/rpc-types/src/eth/call.rs b/crates/rpc-types/src/eth/call.rs index a954bb76b70..fd766ee22e4 100644 --- a/crates/rpc-types/src/eth/call.rs +++ b/crates/rpc-types/src/eth/call.rs @@ -1,5 +1,5 @@ -use crate::{AccessList, BlockId, BlockOverrides}; -use alloy_primitives::{Address, Bytes, B256, U256, U64, U8}; +use crate::{request::TransactionRequest, BlockId, BlockOverrides}; +use alloy_primitives::Bytes; use serde::{Deserialize, Deserializer, Serialize, Serializer}; /// Bundle of transactions @@ -7,7 +7,7 @@ use serde::{Deserialize, Deserializer, Serialize, Serializer}; #[serde(default, rename_all = "camelCase")] pub struct Bundle { /// All transactions to execute - pub transactions: Vec, + pub transactions: Vec, /// Block overrides to apply pub block_override: Option, } @@ -97,201 +97,6 @@ impl<'de> Deserialize<'de> for TransactionIndex { } } -/// Call request for `eth_call` and adjacent methods. -#[derive(Debug, Clone, Default, Eq, PartialEq, Serialize, Deserialize, Hash)] -#[serde(default, rename_all = "camelCase")] -pub struct CallRequest { - /// From - pub from: Option

, - /// To - pub to: Option
, - /// Gas Price - pub gas_price: Option, - /// EIP-1559 Max base fee the caller is willing to pay - pub max_fee_per_gas: Option, - /// EIP-1559 Priority fee the caller is paying to the block author - pub max_priority_fee_per_gas: Option, - /// Gas - pub gas: Option, - /// Value - pub value: Option, - /// Transaction input data - #[serde(default, flatten)] - pub input: CallInput, - /// Nonce - pub nonce: Option, - /// chain id - pub chain_id: Option, - /// AccessList - pub access_list: Option, - /// Max Fee per Blob gas for EIP-4844 transactions - #[serde(skip_serializing_if = "Option::is_none")] - pub max_fee_per_blob_gas: Option, - /// Blob Versioned Hashes for EIP-4844 transactions - #[serde(skip_serializing_if = "Option::is_none")] - pub blob_versioned_hashes: Option>, - /// EIP-2718 type - #[serde(rename = "type", skip_serializing_if = "Option::is_none")] - pub transaction_type: Option, -} - -impl CallRequest { - /// Returns the configured fee cap, if any. - /// - /// The returns `gas_price` (legacy) if set or `max_fee_per_gas` (EIP1559) - #[inline] - pub fn fee_cap(&self) -> Option { - self.gas_price.or(self.max_fee_per_gas) - } - - /// Returns true if the request has a `blobVersionedHashes` field but it is empty. - #[inline] - pub fn has_empty_blob_hashes(&self) -> bool { - self.blob_versioned_hashes.as_ref().map(|blobs| blobs.is_empty()).unwrap_or(false) - } - - /// Sets the `from` field in the call to the provided address - #[inline] - pub const fn from(mut self, from: Address) -> Self { - self.from = Some(from); - self - } - - /// Sets the `to` field in the call to the provided address - #[inline] - pub const fn to(mut self, to: Option
) -> Self { - self.to = to; - self - } - - /// Sets the `gas` field in the transaction to the provided value - pub const fn gas(mut self, gas: U256) -> Self { - self.gas = Some(gas); - self - } - - /// Sets the `gas_price` field in the transaction to the provided value - /// If the internal transaction is an EIP-1559 one, then it sets both - /// `max_fee_per_gas` and `max_priority_fee_per_gas` to the same value - pub const fn gas_price(mut self, gas_price: U256) -> Self { - // todo: Add legacy support - self.max_fee_per_gas = Some(gas_price); - self.max_fee_per_gas = Some(gas_price); - self - } - - /// Sets the `value` field in the transaction to the provided value - pub const fn value(mut self, value: U256) -> Self { - self.value = Some(value); - self - } - - /// Sets the `nonce` field in the transaction to the provided value - pub const fn nonce(mut self, nonce: U64) -> Self { - self.nonce = Some(nonce); - self - } - - /// Calculates the address that will be created by the transaction, if any. - /// - /// Returns `None` if the transaction is not a contract creation (the `to` field is set), or if - /// the `from` or `nonce` fields are not set. - pub fn calculate_create_address(&self) -> Option
{ - if self.to.is_some() { - return None; - } - let from = self.from.as_ref()?; - let nonce = self.nonce?; - Some(from.create(nonce.to())) - } -} - -/// Helper type that supports both `data` and `input` fields that map to transaction input data. -/// -/// This is done for compatibility reasons where older implementations used `data` instead of the -/// newer, recommended `input` field. -/// -/// If both fields are set, it is expected that they contain the same value, otherwise an error is -/// returned. -#[derive(Debug, Clone, Default, Eq, PartialEq, Serialize, Deserialize, Hash)] -pub struct CallInput { - /// Transaction data - #[serde(skip_serializing_if = "Option::is_none")] - pub input: Option, - /// Transaction data - /// - /// This is the same as `input` but is used for backwards compatibility: - #[serde(skip_serializing_if = "Option::is_none")] - pub data: Option, -} - -impl CallInput { - /// Creates a new instance with the given input data. - pub const fn new(data: Bytes) -> Self { - Self::maybe_input(Some(data)) - } - - /// Creates a new instance with the given input data. - pub const fn maybe_input(input: Option) -> Self { - Self { input, data: None } - } - - /// Consumes the type and returns the optional input data. - #[inline] - pub fn into_input(self) -> Option { - self.input.or(self.data) - } - - /// Consumes the type and returns the optional input data. - /// - /// Returns an error if both `data` and `input` fields are set and not equal. - #[inline] - pub fn try_into_unique_input(self) -> Result, CallInputError> { - self.check_unique_input().map(|()| self.into_input()) - } - - /// Returns the optional input data. - #[inline] - pub fn input(&self) -> Option<&Bytes> { - self.input.as_ref().or(self.data.as_ref()) - } - - /// Returns the optional input data. - /// - /// Returns an error if both `data` and `input` fields are set and not equal. - #[inline] - pub fn unique_input(&self) -> Result, CallInputError> { - self.check_unique_input().map(|()| self.input()) - } - - fn check_unique_input(&self) -> Result<(), CallInputError> { - if let (Some(input), Some(data)) = (&self.input, &self.data) { - if input != data { - return Err(CallInputError::default()); - } - } - Ok(()) - } -} - -impl From for CallInput { - fn from(input: Bytes) -> Self { - Self { input: Some(input), data: None } - } -} - -impl From> for CallInput { - fn from(input: Option) -> Self { - Self { input, data: None } - } -} - -/// Error thrown when both `data` and `input` fields are set and not equal. -#[derive(Debug, Default, thiserror::Error)] -#[error("both \"data\" and \"input\" are set and not equal. Please use \"input\" to pass transaction call data")] -#[non_exhaustive] -pub struct CallInputError; - #[cfg(test)] mod tests { use super::*; @@ -310,29 +115,4 @@ mod tests { let res = serde_json::from_str::(s); assert!(res.is_err()); } - - #[test] - fn serde_call_request() { - let s = r#"{"accessList":[],"data":"0x0902f1ac","to":"0xa478c2975ab1ea89e8196811f51a7b7ade33eb11","type":"0x02"}"#; - let _req = serde_json::from_str::(s).unwrap(); - } - - #[test] - fn serde_unique_call_input() { - let s = r#"{"accessList":[],"data":"0x0902f1ac", "input":"0x0902f1ac","to":"0xa478c2975ab1ea89e8196811f51a7b7ade33eb11","type":"0x02"}"#; - let req = serde_json::from_str::(s).unwrap(); - assert!(req.input.try_into_unique_input().unwrap().is_some()); - - let s = r#"{"accessList":[],"data":"0x0902f1ac","to":"0xa478c2975ab1ea89e8196811f51a7b7ade33eb11","type":"0x02"}"#; - let req = serde_json::from_str::(s).unwrap(); - assert!(req.input.try_into_unique_input().unwrap().is_some()); - - let s = r#"{"accessList":[],"input":"0x0902f1ac","to":"0xa478c2975ab1ea89e8196811f51a7b7ade33eb11","type":"0x02"}"#; - let req = serde_json::from_str::(s).unwrap(); - assert!(req.input.try_into_unique_input().unwrap().is_some()); - - let s = r#"{"accessList":[],"data":"0x0902f1ac", "input":"0x0902f1","to":"0xa478c2975ab1ea89e8196811f51a7b7ade33eb11","type":"0x02"}"#; - let req = serde_json::from_str::(s).unwrap(); - assert!(req.input.try_into_unique_input().is_err()); - } } diff --git a/crates/rpc-types/src/eth/mod.rs b/crates/rpc-types/src/eth/mod.rs index 1486fec7c3b..674688ac012 100644 --- a/crates/rpc-types/src/eth/mod.rs +++ b/crates/rpc-types/src/eth/mod.rs @@ -20,7 +20,7 @@ mod work; pub use account::*; pub use block::*; -pub use call::{Bundle, CallInput, CallInputError, CallRequest, EthCallResponse, StateContext}; +pub use call::{Bundle, EthCallResponse, StateContext}; pub use fee::{FeeHistory, TxGasAndReward}; pub use filter::*; pub use index::Index; diff --git a/crates/rpc-types/src/eth/transaction/blob.rs b/crates/rpc-types/src/eth/transaction/blob.rs index f21cb7224ab..1edc6326063 100644 --- a/crates/rpc-types/src/eth/transaction/blob.rs +++ b/crates/rpc-types/src/eth/transaction/blob.rs @@ -4,7 +4,7 @@ use crate::kzg::{Blob, Bytes48}; use serde::{Deserialize, Serialize}; /// This represents a set of blobs, and its corresponding commitments and proofs. -#[derive(Clone, Debug, PartialEq, Eq, Default, Serialize, Deserialize)] +#[derive(Clone, Debug, PartialEq, Eq, Hash, Default, Serialize, Deserialize)] #[repr(C)] pub struct BlobTransactionSidecar { /// The blob data. diff --git a/crates/rpc-types/src/eth/transaction/request.rs b/crates/rpc-types/src/eth/transaction/request.rs index e4430b47003..42fefe23a5a 100644 --- a/crates/rpc-types/src/eth/transaction/request.rs +++ b/crates/rpc-types/src/eth/transaction/request.rs @@ -1,84 +1,141 @@ //! Alloy basic Transaction Request type. +use std::hash::Hash; + use crate::{eth::transaction::AccessList, other::OtherFields, BlobTransactionSidecar}; -use alloy_primitives::{Address, Bytes, U128, U256, U64, U8}; +use alloy_primitives::{Address, Bytes, B256, U256, U64, U8}; use serde::{Deserialize, Serialize}; -/// Represents _all_ transaction requests received from RPC +/// Represents _all_ transaction requests to/from RPC. #[derive(Clone, Debug, PartialEq, Eq, Default, Serialize, Deserialize)] #[serde(deny_unknown_fields)] #[serde(rename_all = "camelCase")] pub struct TransactionRequest { - /// from address + /// The address of the transaction author. pub from: Option
, - /// to address + /// The destination address of the transaction. pub to: Option
, - /// legacy, gas Price + /// The legacy gas price. #[serde(default)] - pub gas_price: Option, - /// max base fee per gas sender is willing to pay + pub gas_price: Option, + /// The max base fee per gas the sender is willing to pay. #[serde(default)] - pub max_fee_per_gas: Option, - /// miner tip + pub max_fee_per_gas: Option, + /// The max priority fee per gas the sender is willing to pay, also called the miner tip. #[serde(default)] - pub max_priority_fee_per_gas: Option, - /// gas + pub max_priority_fee_per_gas: Option, + /// The max fee per blob gas for EIP-4844 blob transactions. + #[serde(skip_serializing_if = "Option::is_none")] + pub max_fee_per_blob_gas: Option, + /// The gas limit for the transaction. pub gas: Option, - /// value of th tx in wei + /// The value transferred in the transaction, in wei. pub value: Option, - /// Any additional data sent - #[serde(alias = "input")] - pub data: Option, - /// Transaction nonce + /// Transaction data. + #[serde(default, flatten)] + pub input: TransactionInput, + /// The nonce of the transaction. pub nonce: Option, - /// warm storage access pre-payment + /// The chain ID for the transaction. + pub chain_id: Option, + /// An EIP-2930 access list, which lowers cost for accessing accounts and storages in the list. See [EIP-2930](https://eips.ethereum.org/EIPS/eip-2930) for more information. #[serde(default)] pub access_list: Option, - /// EIP-2718 type + /// The EIP-2718 transaction type. See [EIP-2718](https://eips.ethereum.org/EIPS/eip-2718) for more information. #[serde(rename = "type")] pub transaction_type: Option, - /// sidecar for EIP-4844 transactions + /// Blob versioned hashes for EIP-4844 transactions. + #[serde(skip_serializing_if = "Option::is_none")] + pub blob_versioned_hashes: Option>, + /// Blob sidecar for EIP-4844 transactions. + #[serde(skip_serializing_if = "Option::is_none")] pub sidecar: Option, /// Support for arbitrary additional fields. #[serde(flatten)] pub other: OtherFields, } +impl Hash for TransactionRequest { + fn hash(&self, state: &mut H) { + self.from.hash(state); + self.to.hash(state); + self.gas_price.hash(state); + self.max_fee_per_gas.hash(state); + self.max_priority_fee_per_gas.hash(state); + self.max_fee_per_blob_gas.hash(state); + self.gas.hash(state); + self.value.hash(state); + self.input.hash(state); + self.nonce.hash(state); + self.chain_id.hash(state); + self.access_list.hash(state); + self.transaction_type.hash(state); + self.blob_versioned_hashes.hash(state); + self.sidecar.hash(state); + for (k, v) in self.other.iter() { + k.hash(state); + v.to_string().hash(state); + } + } +} + // == impl TransactionRequest == impl TransactionRequest { + /// Returns the configured fee cap, if any. + /// + /// The returns `gas_price` (legacy) if set or `max_fee_per_gas` (EIP1559) + #[inline] + pub fn fee_cap(&self) -> Option { + self.gas_price.or(self.max_fee_per_gas) + } + + /// Returns true if the request has a `blobVersionedHashes` field but it is empty. + #[inline] + pub fn has_empty_blob_hashes(&self) -> bool { + self.blob_versioned_hashes.as_ref().map(|blobs| blobs.is_empty()).unwrap_or(false) + } + + /// Sets the `from` field in the call to the provided address + #[inline] + pub const fn from(mut self, from: Address) -> Self { + self.from = Some(from); + self + } + /// Sets the gas limit for the transaction. - pub fn gas_limit(mut self, gas_limit: u64) -> Self { - self.gas = Some(U256::from(gas_limit)); + pub const fn gas_limit(mut self, gas_limit: U256) -> Self { + self.gas = Some(gas_limit); self } /// Sets the nonce for the transaction. - pub fn nonce(mut self, nonce: u64) -> Self { - self.nonce = Some(U64::from(nonce)); + pub const fn nonce(mut self, nonce: U64) -> Self { + self.nonce = Some(nonce); self } /// Sets the maximum fee per gas for the transaction. - pub fn max_fee_per_gas(mut self, max_fee_per_gas: u128) -> Self { - self.max_fee_per_gas = Some(U128::from(max_fee_per_gas)); + pub const fn max_fee_per_gas(mut self, max_fee_per_gas: U256) -> Self { + self.max_fee_per_gas = Some(max_fee_per_gas); self } /// Sets the maximum priority fee per gas for the transaction. - pub fn max_priority_fee_per_gas(mut self, max_priority_fee_per_gas: u128) -> Self { - self.max_priority_fee_per_gas = Some(U128::from(max_priority_fee_per_gas)); + pub const fn max_priority_fee_per_gas(mut self, max_priority_fee_per_gas: U256) -> Self { + self.max_priority_fee_per_gas = Some(max_priority_fee_per_gas); self } /// Sets the recipient address for the transaction. - pub const fn to(mut self, to: Address) -> Self { - self.to = Some(to); + #[inline] + pub const fn to(mut self, to: Option
) -> Self { + self.to = to; self } - /// Sets the value (amount) for the transaction. - pub fn value(mut self, value: u128) -> Self { - self.value = Some(U256::from(value)); + /// Sets the value (amount) for the transaction. + pub const fn value(mut self, value: U256) -> Self { + self.value = Some(value); self } @@ -89,8 +146,8 @@ impl TransactionRequest { } /// Sets the input data for the transaction. - pub fn input(mut self, input: Bytes) -> Self { - self.data = Some(input); + pub fn input(mut self, input: TransactionInput) -> Self { + self.input = input; self } @@ -99,4 +156,133 @@ impl TransactionRequest { self.transaction_type = Some(U8::from(transaction_type)); self } + + /// Calculates the address that will be created by the transaction, if any. + /// + /// Returns `None` if the transaction is not a contract creation (the `to` field is set), or if + /// the `from` or `nonce` fields are not set. + pub fn calculate_create_address(&self) -> Option
{ + if self.to.is_some() { + return None; + } + let from = self.from.as_ref()?; + let nonce = self.nonce?; + Some(from.create(nonce.to())) + } +} + +/// Helper type that supports both `data` and `input` fields that map to transaction input data. +/// +/// This is done for compatibility reasons where older implementations used `data` instead of the +/// newer, recommended `input` field. +/// +/// If both fields are set, it is expected that they contain the same value, otherwise an error is +/// returned. +#[derive(Debug, Clone, Default, Eq, PartialEq, Serialize, Deserialize, Hash)] +pub struct TransactionInput { + /// Transaction data + #[serde(skip_serializing_if = "Option::is_none")] + pub input: Option, + /// Transaction data + /// + /// This is the same as `input` but is used for backwards compatibility: + #[serde(skip_serializing_if = "Option::is_none")] + pub data: Option, +} + +impl TransactionInput { + /// Creates a new instance with the given input data. + pub const fn new(data: Bytes) -> Self { + Self::maybe_input(Some(data)) + } + + /// Creates a new instance with the given input data. + pub const fn maybe_input(input: Option) -> Self { + Self { input, data: None } + } + + /// Consumes the type and returns the optional input data. + #[inline] + pub fn into_input(self) -> Option { + self.input.or(self.data) + } + + /// Consumes the type and returns the optional input data. + /// + /// Returns an error if both `data` and `input` fields are set and not equal. + #[inline] + pub fn try_into_unique_input(self) -> Result, TransactionInputError> { + self.check_unique_input().map(|()| self.into_input()) + } + + /// Returns the optional input data. + #[inline] + pub fn input(&self) -> Option<&Bytes> { + self.input.as_ref().or(self.data.as_ref()) + } + + /// Returns the optional input data. + /// + /// Returns an error if both `data` and `input` fields are set and not equal. + #[inline] + pub fn unique_input(&self) -> Result, TransactionInputError> { + self.check_unique_input().map(|()| self.input()) + } + + fn check_unique_input(&self) -> Result<(), TransactionInputError> { + if let (Some(input), Some(data)) = (&self.input, &self.data) { + if input != data { + return Err(TransactionInputError::default()); + } + } + Ok(()) + } +} + +impl From for TransactionInput { + fn from(input: Bytes) -> Self { + Self { input: Some(input), data: None } + } +} + +impl From> for TransactionInput { + fn from(input: Option) -> Self { + Self { input, data: None } + } +} + +/// Error thrown when both `data` and `input` fields are set and not equal. +#[derive(Debug, Default, thiserror::Error)] +#[error("both \"data\" and \"input\" are set and not equal. Please use \"input\" to pass transaction call data")] +#[non_exhaustive] +pub struct TransactionInputError; + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn serde_tx_request() { + let s = r#"{"accessList":[],"data":"0x0902f1ac","to":"0xa478c2975ab1ea89e8196811f51a7b7ade33eb11","type":"0x02"}"#; + let _req = serde_json::from_str::(s).unwrap(); + } + + #[test] + fn serde_unique_call_input() { + let s = r#"{"accessList":[],"data":"0x0902f1ac", "input":"0x0902f1ac","to":"0xa478c2975ab1ea89e8196811f51a7b7ade33eb11","type":"0x02"}"#; + let req = serde_json::from_str::(s).unwrap(); + assert!(req.input.try_into_unique_input().unwrap().is_some()); + + let s = r#"{"accessList":[],"data":"0x0902f1ac","to":"0xa478c2975ab1ea89e8196811f51a7b7ade33eb11","type":"0x02"}"#; + let req = serde_json::from_str::(s).unwrap(); + assert!(req.input.try_into_unique_input().unwrap().is_some()); + + let s = r#"{"accessList":[],"input":"0x0902f1ac","to":"0xa478c2975ab1ea89e8196811f51a7b7ade33eb11","type":"0x02"}"#; + let req = serde_json::from_str::(s).unwrap(); + assert!(req.input.try_into_unique_input().unwrap().is_some()); + + let s = r#"{"accessList":[],"data":"0x0902f1ac", "input":"0x0902f1","to":"0xa478c2975ab1ea89e8196811f51a7b7ade33eb11","type":"0x02"}"#; + let req = serde_json::from_str::(s).unwrap(); + assert!(req.input.try_into_unique_input().is_err()); + } }