use alloc::{
format,
string::{String, ToString},
};
use core::array::TryFromSliceError;
use alloy_primitives::{B256, B64};
use derive_more::derive::{Display, From};
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))]
pub struct SuperchainSignal {
pub recommended: ProtocolVersion,
pub required: ProtocolVersion,
}
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
#[non_exhaustive]
pub enum ProtocolVersion {
V0(ProtocolVersionFormatV0),
}
impl core::fmt::Display for ProtocolVersion {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match self {
Self::V0(value) => write!(f, "{}", value),
}
}
}
#[derive(Copy, Clone, Debug, Display, From)]
pub enum ProtocolVersionError {
#[display("Unsupported version: {_0}")]
UnsupportedVersion(u8),
#[display("Invalid length: got {}, expected {}", got, expected)]
InvalidLength {
got: usize,
expected: usize,
},
#[display("Failed to convert slice to array")]
#[from(TryFromSliceError)]
TryFromSlice,
}
impl ProtocolVersion {
pub fn encode(&self) -> B256 {
let mut bytes = [0u8; 32];
match self {
Self::V0(value) => {
bytes[0] = 0x00; bytes[1..].copy_from_slice(&value.encode());
B256::from_slice(&bytes)
}
}
}
pub fn decode(value: B256) -> Result<Self, ProtocolVersionError> {
let version_type = value[0];
let typed_payload = &value[1..];
match version_type {
0 => Ok(Self::V0(ProtocolVersionFormatV0::decode(typed_payload)?)),
other => Err(ProtocolVersionError::UnsupportedVersion(other)),
}
}
pub const fn inner(&self) -> ProtocolVersionFormatV0 {
match self {
Self::V0(value) => *value,
}
}
pub const fn as_v0(&self) -> Option<ProtocolVersionFormatV0> {
match self {
Self::V0(value) => Some(*value),
}
}
pub const fn build(&self) -> B64 {
match self {
Self::V0(value) => value.build,
}
}
pub const fn major(&self) -> u32 {
match self {
Self::V0(value) => value.major,
}
}
pub const fn minor(&self) -> u32 {
match self {
Self::V0(value) => value.minor,
}
}
pub const fn patch(&self) -> u32 {
match self {
Self::V0(value) => value.patch,
}
}
pub const fn pre_release(&self) -> u32 {
match self {
Self::V0(value) => value.pre_release,
}
}
pub fn display(&self) -> String {
match self {
Self::V0(value) => format!("{}", value),
}
}
}
#[cfg(feature = "serde")]
impl serde::Serialize for ProtocolVersion {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
self.encode().serialize(serializer)
}
}
#[cfg(feature = "serde")]
impl<'de> serde::Deserialize<'de> for ProtocolVersion {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
let value = alloy_primitives::B256::deserialize(deserializer)?;
Self::decode(value).map_err(serde::de::Error::custom)
}
}
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub struct ProtocolVersionFormatV0 {
pub build: B64,
pub major: u32,
pub minor: u32,
pub patch: u32,
pub pre_release: u32,
}
impl core::fmt::Display for ProtocolVersionFormatV0 {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
let build_tag = if self.build.0.iter().any(|&byte| byte != 0) {
if is_human_readable_build_tag(self.build) {
let full = format!("+{}", String::from_utf8_lossy(&self.build.0));
full.trim_end_matches('\0').to_string()
} else {
format!("+{}", self.build)
}
} else {
String::new()
};
let pre_release_tag =
if self.pre_release != 0 { format!("-{}", self.pre_release) } else { String::new() };
write!(f, "v{}.{}.{}{}{}", self.major, self.minor, self.patch, pre_release_tag, build_tag)
}
}
impl ProtocolVersionFormatV0 {
pub fn encode(&self) -> [u8; 31] {
let mut bytes = [0u8; 31];
bytes[0..7].copy_from_slice(&[0u8; 7]);
bytes[7..15].copy_from_slice(&self.build.0);
bytes[15..19].copy_from_slice(&self.major.to_be_bytes());
bytes[19..23].copy_from_slice(&self.minor.to_be_bytes());
bytes[23..27].copy_from_slice(&self.patch.to_be_bytes());
bytes[27..31].copy_from_slice(&self.pre_release.to_be_bytes());
bytes
}
fn decode(value: &[u8]) -> Result<Self, ProtocolVersionError> {
if value.len() != 31 {
return Err(ProtocolVersionError::InvalidLength { got: value.len(), expected: 31 });
}
Ok(Self {
build: B64::from_slice(&value[7..15]),
major: u32::from_be_bytes(value[15..19].try_into()?),
minor: u32::from_be_bytes(value[19..23].try_into()?),
patch: u32::from_be_bytes(value[23..27].try_into()?),
pre_release: u32::from_be_bytes(value[27..31].try_into()?),
})
}
}
fn is_human_readable_build_tag(build: B64) -> bool {
for (i, &c) in build.iter().enumerate() {
if c == 0 {
if build[i..].iter().any(|&d| d != 0) {
return false;
}
return true;
}
if !(c.is_ascii_alphanumeric() || c == b'-' || (c == b'.' && i > 0)) {
return false;
}
}
true
}
#[cfg(test)]
mod tests {
use alloy_primitives::b256;
use super::*;
#[test]
fn test_protocol_version_encode_decode() {
let test_cases = vec![
(
ProtocolVersion::V0(ProtocolVersionFormatV0 {
build: B64::from_slice(&[0x61, 0x62, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00]),
major: 42,
minor: 0,
patch: 2,
pre_release: 0,
}),
"v42.0.2+0x6162010000000000",
b256!("000000000000000061620100000000000000002a000000000000000200000000"),
),
(
ProtocolVersion::V0(ProtocolVersionFormatV0 {
build: B64::from_slice(&[0x61, 0x62, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00]),
major: 42,
minor: 0,
patch: 2,
pre_release: 1,
}),
"v42.0.2-1+0x6162010000000000",
b256!("000000000000000061620100000000000000002a000000000000000200000001"),
),
(
ProtocolVersion::V0(ProtocolVersionFormatV0 {
build: B64::from_slice(&[0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08]),
major: 42,
minor: 0,
patch: 2,
pre_release: 0,
}),
"v42.0.2+0x0102030405060708",
b256!("000000000000000001020304050607080000002a000000000000000200000000"),
),
(
ProtocolVersion::V0(ProtocolVersionFormatV0 {
build: B64::from_slice(&[0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]),
major: 0,
minor: 100,
patch: 2,
pre_release: 0,
}),
"v0.100.2",
b256!("0000000000000000000000000000000000000000000000640000000200000000"),
),
(
ProtocolVersion::V0(ProtocolVersionFormatV0 {
build: B64::from_slice(&[b'O', b'P', b'-', b'm', b'o', b'd', 0x00, 0x00]),
major: 42,
minor: 0,
patch: 2,
pre_release: 1,
}),
"v42.0.2-1+OP-mod",
b256!("00000000000000004f502d6d6f6400000000002a000000000000000200000001"),
),
(
ProtocolVersion::V0(ProtocolVersionFormatV0 {
build: B64::from_slice(&[b'a', b'b', 0x01, 0x00, 0x00, 0x00, 0x00, 0x00]),
major: 42,
minor: 0,
patch: 2,
pre_release: 0,
}),
"v42.0.2+0x6162010000000000", b256!("000000000000000061620100000000000000002a000000000000000200000000"),
),
(
ProtocolVersion::V0(ProtocolVersionFormatV0 {
build: B64::from_slice(b"beta.123"),
major: 1,
minor: 0,
patch: 0,
pre_release: 0,
}),
"v1.0.0+beta.123",
b256!("0000000000000000626574612e31323300000001000000000000000000000000"),
),
];
for (decoded_exp, formatted_exp, encoded_exp) in test_cases {
encode_decode_v0(encoded_exp, formatted_exp, decoded_exp);
}
}
fn encode_decode_v0(encoded_exp: B256, formatted_exp: &str, decoded_exp: ProtocolVersion) {
let decoded = ProtocolVersion::decode(encoded_exp).unwrap();
assert_eq!(decoded, decoded_exp);
let encoded = decoded.encode();
assert_eq!(encoded, encoded_exp);
let formatted = decoded.display();
assert_eq!(formatted, formatted_exp);
}
}