use crate::{Filter, Header, Log, Transaction, TransactionReceipt};
use alloc::{boxed::Box, vec::Vec};
use alloy_primitives::B256;
use alloy_serde::WithOtherFields;
#[derive(Clone, Debug, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize))]
#[cfg_attr(feature = "serde", serde(untagged))]
pub enum SubscriptionResult<T = Transaction, R = TransactionReceipt> {
Header(Box<WithOtherFields<Header>>),
Log(Box<Log>),
TransactionHash(B256),
FullTransaction(Box<T>),
SyncState(PubSubSyncStatus),
TransactionReceipts(Vec<R>),
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "serde", serde(untagged))]
pub enum PubSubSyncStatus {
Simple(bool),
Detailed(SyncStatusMetadata),
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))]
pub struct SyncStatusMetadata {
pub syncing: bool,
#[cfg_attr(feature = "serde", serde(with = "alloy_serde::quantity"))]
pub starting_block: u64,
#[cfg_attr(feature = "serde", serde(with = "alloy_serde::quantity"))]
pub current_block: u64,
#[cfg_attr(
feature = "serde",
serde(
default,
skip_serializing_if = "Option::is_none",
with = "alloy_serde::quantity::opt"
)
)]
pub highest_block: Option<u64>,
}
#[cfg(feature = "serde")]
impl<T, R> serde::Serialize for SubscriptionResult<T, R>
where
T: serde::Serialize,
R: serde::Serialize,
{
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
match *self {
Self::Header(ref header) => header.serialize(serializer),
Self::Log(ref log) => log.serialize(serializer),
Self::TransactionHash(ref hash) => hash.serialize(serializer),
Self::FullTransaction(ref tx) => tx.serialize(serializer),
Self::SyncState(ref sync) => sync.serialize(serializer),
Self::TransactionReceipts(ref receipts) => receipts.serialize(serializer),
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))]
pub enum SubscriptionKind {
NewHeads,
Logs,
NewPendingTransactions,
Syncing,
TransactionReceipts,
}
#[derive(Clone, Debug, Default, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))]
pub struct TransactionReceiptsParams {
#[cfg_attr(feature = "serde", serde(default))]
pub transaction_hashes: Option<Vec<B256>>,
}
impl core::fmt::Display for SubscriptionKind {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match self {
Self::NewHeads => write!(f, "newHeads"),
Self::Logs => write!(f, "logs"),
Self::NewPendingTransactions => write!(f, "newPendingTransactions"),
Self::Syncing => write!(f, "syncing"),
Self::TransactionReceipts => write!(f, "transactionReceipts"),
}
}
}
impl core::str::FromStr for SubscriptionKind {
type Err = &'static str;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"newHeads" => Ok(Self::NewHeads),
"logs" => Ok(Self::Logs),
"newPendingTransactions" => Ok(Self::NewPendingTransactions),
"syncing" => Ok(Self::Syncing),
"transactionReceipts" => Ok(Self::TransactionReceipts),
_ => Err("invalid subscription kind"),
}
}
}
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub enum Params {
#[default]
None,
Logs(Box<Filter>),
Bool(bool),
TransactionReceipts(TransactionReceiptsParams),
}
impl Params {
#[inline]
pub const fn is_bool(&self) -> bool {
matches!(self, Self::Bool(_))
}
#[inline]
pub const fn is_logs(&self) -> bool {
matches!(self, Self::Logs(_))
}
#[cfg(feature = "serde")]
pub fn from_json_value(v: serde_json::Value) -> Result<Self, serde_json::Error> {
if v.is_null() {
return Ok(Self::None);
}
if let Some(val) = v.as_bool() {
return Ok(val.into());
}
let is_transaction_receipts =
v.as_object().is_some_and(|obj| obj.contains_key("transactionHashes"));
if is_transaction_receipts {
return serde_json::from_value::<TransactionReceiptsParams>(v).map(Into::into);
}
serde_json::from_value::<Filter>(v).map(Into::into)
}
}
impl From<Filter> for Params {
fn from(filter: Filter) -> Self {
Self::Logs(Box::new(filter))
}
}
impl From<bool> for Params {
fn from(value: bool) -> Self {
Self::Bool(value)
}
}
impl From<TransactionReceiptsParams> for Params {
fn from(params: TransactionReceiptsParams) -> Self {
Self::TransactionReceipts(params)
}
}
#[cfg(feature = "serde")]
impl serde::Serialize for Params {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
match self {
Self::None => (&[] as &[serde_json::Value]).serialize(serializer),
Self::Logs(logs) => logs.serialize(serializer),
Self::Bool(full) => full.serialize(serializer),
Self::TransactionReceipts(params) => params.serialize(serializer),
}
}
}
#[cfg(feature = "serde")]
impl<'a> serde::Deserialize<'a> for Params {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'a>,
{
let v = serde_json::Value::deserialize(deserializer)?;
Self::from_json_value(v).map_err(serde::de::Error::custom)
}
}
#[cfg(test)]
mod tests {
use super::*;
use alloy_primitives::hex;
use similar_asserts::assert_eq;
#[test]
#[cfg(feature = "serde")]
fn params_serde() {
let s: Params = serde_json::from_str("true").unwrap();
assert_eq!(s, Params::Bool(true));
let s: Params = serde_json::from_str("null").unwrap();
assert_eq!(s, Params::None);
let filter = Filter::default();
let s: Params = serde_json::from_str(&serde_json::to_string(&filter).unwrap()).unwrap();
assert_eq!(s, Params::Logs(Box::new(filter)));
let json = r#"{"transactionHashes":["0x5c504ed432cb51138bcf09aa5e8a410dd4a1e204ef84bfed1be16dfba1b22060"]}"#;
let param: Params = serde_json::from_str(json).unwrap();
match param {
Params::TransactionReceipts(params) => {
assert_eq!(
params.transaction_hashes,
Some(vec![B256::from(hex!(
"0x5c504ed432cb51138bcf09aa5e8a410dd4a1e204ef84bfed1be16dfba1b22060"
))])
);
}
_ => panic!("Expected TransactionReceipts variant"),
}
let json = r#"{"transactionHashes":null}"#;
let param: Params = serde_json::from_str(json).unwrap();
match param {
Params::TransactionReceipts(params) => {
assert_eq!(params.transaction_hashes, None);
}
_ => panic!("Expected TransactionReceipts variant"),
}
let json = r#"{"transactionHashes":[]}"#;
let param: Params = serde_json::from_str(json).unwrap();
match param {
Params::TransactionReceipts(params) => {
assert_eq!(params.transaction_hashes, Some(vec![]));
}
_ => panic!("Expected TransactionReceipts variant"),
}
}
#[test]
fn params_is_bool() {
let param = Params::Bool(true);
assert!(param.is_bool());
let param = Params::None;
assert!(!param.is_bool());
let param = Params::Logs(Box::default());
assert!(!param.is_bool());
}
#[test]
fn params_is_logs() {
let param = Params::Logs(Box::default());
assert!(param.is_logs());
let param = Params::None;
assert!(!param.is_logs());
let param = Params::Bool(true);
assert!(!param.is_logs());
}
#[test]
fn params_from_filter() {
let filter = Filter::default();
let param: Params = filter.clone().into();
assert_eq!(param, Params::Logs(Box::new(filter)));
}
#[test]
fn params_from_bool() {
let param: Params = true.into();
assert_eq!(param, Params::Bool(true));
let param: Params = false.into();
assert_eq!(param, Params::Bool(false));
}
#[test]
fn params_from_transaction_receipts() {
let params = TransactionReceiptsParams { transaction_hashes: Some(vec![B256::random()]) };
let param: Params = params.clone().into();
assert_eq!(param, Params::TransactionReceipts(params));
}
#[test]
#[cfg(feature = "serde")]
fn subscription_kind_str_roundtrip() {
use core::str::FromStr;
for kind in [
SubscriptionKind::NewHeads,
SubscriptionKind::Logs,
SubscriptionKind::NewPendingTransactions,
SubscriptionKind::Syncing,
SubscriptionKind::TransactionReceipts,
] {
let s = kind.to_string();
let parsed: SubscriptionKind = s.parse().unwrap();
assert_eq!(kind, parsed);
let serde_str = serde_json::to_string(&kind).unwrap();
let serde_str = serde_str.trim_matches('"');
assert_eq!(s, serde_str);
assert_eq!(SubscriptionKind::from_str(serde_str).unwrap(), kind);
}
}
#[test]
#[cfg(feature = "serde")]
fn params_serialize_none() {
let param = Params::None;
let serialized = serde_json::to_string(¶m).unwrap();
assert_eq!(serialized, "[]");
}
#[test]
#[cfg(feature = "serde")]
fn params_serialize_bool() {
let param = Params::Bool(true);
let serialized = serde_json::to_string(¶m).unwrap();
assert_eq!(serialized, "true");
let param = Params::Bool(false);
let serialized = serde_json::to_string(¶m).unwrap();
assert_eq!(serialized, "false");
}
#[test]
#[cfg(feature = "serde")]
fn params_serialize_logs() {
let filter = Filter::default();
let param = Params::Logs(Box::new(filter.clone()));
let serialized = serde_json::to_string(¶m).unwrap();
let expected = serde_json::to_string(&filter).unwrap();
assert_eq!(serialized, expected);
}
#[test]
#[cfg(feature = "serde")]
fn params_serialize_transaction_receipts() {
let params = TransactionReceiptsParams {
transaction_hashes: Some(vec![B256::from(hex!(
"0x5c504ed432cb51138bcf09aa5e8a410dd4a1e204ef84bfed1be16dfba1b22060"
))]),
};
let param = Params::TransactionReceipts(params);
let serialized = serde_json::to_string(¶m).unwrap();
let expected = r#"{"transactionHashes":["0x5c504ed432cb51138bcf09aa5e8a410dd4a1e204ef84bfed1be16dfba1b22060"]}"#;
assert_eq!(serialized, expected);
let param = Params::TransactionReceipts(TransactionReceiptsParams::default());
let serialized = serde_json::to_string(¶m).unwrap();
assert_eq!(serialized, r#"{"transactionHashes":null}"#);
let roundtrip: Params = serde_json::from_str(&serialized).unwrap();
assert_eq!(roundtrip, param);
}
#[test]
#[cfg(feature = "serde")]
fn sync_status_metadata_serde() {
let metadata = SyncStatusMetadata {
syncing: true,
starting_block: 900,
current_block: 902,
highest_block: Some(1108),
};
let serialized = serde_json::to_string(&metadata).unwrap();
assert_eq!(
serialized,
r#"{"syncing":true,"startingBlock":"0x384","currentBlock":"0x386","highestBlock":"0x454"}"#
);
let deserialized: SyncStatusMetadata = serde_json::from_str(&serialized).unwrap();
assert_eq!(metadata, deserialized);
let metadata_no_highest = SyncStatusMetadata {
syncing: false,
starting_block: 0,
current_block: 100,
highest_block: None,
};
let serialized = serde_json::to_string(&metadata_no_highest).unwrap();
assert_eq!(serialized, r#"{"syncing":false,"startingBlock":"0x0","currentBlock":"0x64"}"#);
let deserialized: SyncStatusMetadata = serde_json::from_str(&serialized).unwrap();
assert_eq!(metadata_no_highest, deserialized);
}
}