use std::{
fmt::{Debug, Display},
hash::{Hash, Hasher},
str::FromStr,
};
use alloy_primitives::Address;
use nautilus_core::{correctness::FAILED, hex};
use serde::{Deserialize, Deserializer, Serialize, Serializer};
use ustr::Ustr;
#[derive(Clone, Copy, PartialOrd, Ord)]
pub enum PoolIdentifier {
Address(Ustr),
PoolId(Ustr),
}
impl PoolIdentifier {
pub fn new_checked<T: AsRef<str>>(value: T) -> anyhow::Result<Self> {
let value = value.as_ref();
if !value.starts_with("0x") {
anyhow::bail!("Pool identifier must start with '0x', was: {value}");
}
match value.len() {
42 => {
validate_hex_string(value)?;
let addr = value
.parse::<Address>()
.map_err(|e| anyhow::anyhow!("Invalid address: {e}"))?;
Ok(Self::Address(Ustr::from(addr.to_checksum(None).as_str())))
}
66 => {
validate_hex_string(value)?;
Ok(Self::PoolId(Ustr::from(&value.to_lowercase())))
}
len => {
anyhow::bail!(
"Pool identifier must be 42 chars (address) or 66 chars (pool ID), was {len} chars: {value}"
)
}
}
}
#[must_use]
pub fn new<T: AsRef<str>>(value: T) -> Self {
Self::new_checked(value).expect(FAILED)
}
#[must_use]
pub fn from_address(address: Address) -> Self {
Self::Address(Ustr::from(address.to_checksum(None).as_str()))
}
pub fn from_pool_id_bytes(bytes: &[u8]) -> anyhow::Result<Self> {
anyhow::ensure!(
bytes.len() == 32,
"Pool ID must be 32 bytes, was {}",
bytes.len()
);
Ok(Self::PoolId(Ustr::from(&hex::encode_prefixed(bytes))))
}
pub fn from_pool_id_hex<T: AsRef<str>>(hex: T) -> anyhow::Result<Self> {
let hex = hex.as_ref();
let hex_str = hex.strip_prefix("0x").unwrap_or(hex);
anyhow::ensure!(
hex_str.len() == 64,
"Pool ID hex must be 64 characters (32 bytes), was {}",
hex_str.len()
);
validate_hex_string(&format!("0x{hex_str}"))?;
Ok(Self::PoolId(Ustr::from(&format!(
"0x{}",
hex_str.to_lowercase()
))))
}
#[must_use]
pub fn inner(&self) -> Ustr {
match self {
Self::Address(s) | Self::PoolId(s) => *s,
}
}
#[must_use]
pub fn as_str(&self) -> &str {
match self {
Self::Address(s) | Self::PoolId(s) => s.as_str(),
}
}
#[must_use]
pub fn is_address(&self) -> bool {
matches!(self, Self::Address(_))
}
#[must_use]
pub fn is_pool_id(&self) -> bool {
matches!(self, Self::PoolId(_))
}
pub fn to_address(&self) -> anyhow::Result<Address> {
match self {
Self::Address(s) => Address::parse_checksummed(s.as_str(), None)
.map_err(|e| anyhow::anyhow!("Failed to parse address: {e}")),
Self::PoolId(_) => anyhow::bail!("Cannot convert PoolId variant to Address"),
}
}
pub fn to_pool_id_bytes(&self) -> anyhow::Result<[u8; 32]> {
match self {
Self::PoolId(s) => {
let hex_str = s.as_str().strip_prefix("0x").unwrap_or(s.as_str());
hex::decode_array::<32>(hex_str)
.map_err(|e| anyhow::anyhow!("Failed to decode pool ID hex: {e}"))
}
Self::Address(_) => anyhow::bail!("Cannot convert Address variant to PoolId bytes"),
}
}
}
fn validate_hex_string(s: &str) -> anyhow::Result<()> {
let hex_part = &s[2..];
if !hex_part.chars().all(|c| c.is_ascii_hexdigit()) {
anyhow::bail!("Invalid hex characters in: {s}");
}
Ok(())
}
impl PartialEq for PoolIdentifier {
fn eq(&self, other: &Self) -> bool {
match (self, other) {
(Self::Address(a), Self::Address(b)) | (Self::PoolId(a), Self::PoolId(b)) => {
a.as_str().eq_ignore_ascii_case(b.as_str())
}
_ => false,
}
}
}
impl Eq for PoolIdentifier {}
impl Hash for PoolIdentifier {
fn hash<H: Hasher>(&self, state: &mut H) {
std::mem::discriminant(self).hash(state);
match self {
Self::Address(s) | Self::PoolId(s) => {
for byte in s.as_str().bytes() {
state.write_u8(byte.to_ascii_lowercase());
}
}
}
}
}
impl Display for PoolIdentifier {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Address(s) | Self::PoolId(s) => write!(f, "{s}"),
}
}
}
impl Debug for PoolIdentifier {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Address(s) => write!(f, "Address({s:?})"),
Self::PoolId(s) => write!(f, "PoolId({s:?})"),
}
}
}
impl Serialize for PoolIdentifier {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
match self {
Self::Address(s) | Self::PoolId(s) => s.serialize(serializer),
}
}
}
impl<'de> Deserialize<'de> for PoolIdentifier {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
let value_str: &str = Deserialize::deserialize(deserializer)?;
Self::new_checked(value_str).map_err(serde::de::Error::custom)
}
}
impl FromStr for PoolIdentifier {
type Err = anyhow::Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Self::new_checked(s)
}
}
impl From<&str> for PoolIdentifier {
fn from(value: &str) -> Self {
Self::new(value)
}
}
impl From<String> for PoolIdentifier {
fn from(value: String) -> Self {
Self::new(value)
}
}
impl AsRef<str> for PoolIdentifier {
fn as_ref(&self) -> &str {
self.as_str()
}
}
#[cfg(test)]
mod tests {
use rstest::rstest;
use super::*;
#[rstest]
#[case("0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2", true)] #[case("0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2", true)] #[case(
"0xc9bc8043294146424a4e4607d8ad837d6a659142822bbaaabc83bb57e7447461",
true
)] fn test_valid_pool_identifiers(#[case] input: &str, #[case] expected_valid: bool) {
let result = PoolIdentifier::new_checked(input);
assert_eq!(result.is_ok(), expected_valid, "Input: {input}");
}
#[rstest]
#[case("C02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2")] #[case("0xC02aaA39")] #[case("0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2EXTRA")] #[case("0xGGGGGGGGb223FE8D0A0e5C4F27eAD9083C756Cc2")] fn test_invalid_pool_identifiers(#[case] input: &str) {
let result = PoolIdentifier::new_checked(input);
assert!(result.is_err(), "Input should fail: {input}");
}
#[rstest]
fn test_case_insensitive_equality() {
let addr1 = PoolIdentifier::new("0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2");
let addr2 = PoolIdentifier::new("0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2");
let addr3 = PoolIdentifier::new("0xC02AAA39B223FE8D0A0E5C4F27EAD9083C756CC2");
assert_eq!(addr1, addr2);
assert_eq!(addr2, addr3);
assert_eq!(addr1, addr3);
}
#[rstest]
fn test_case_insensitive_hashing() {
use std::collections::HashMap;
let mut map = HashMap::new();
let addr1 = PoolIdentifier::new("0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2");
let addr2 = PoolIdentifier::new("0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2");
map.insert(addr1, "value1");
assert_eq!(map.get(&addr2), Some(&"value1"));
}
#[rstest]
fn test_display_preserves_case() {
let checksummed = "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2";
let addr = PoolIdentifier::new_checked(checksummed).unwrap();
assert_eq!(addr.to_string(), checksummed);
}
#[rstest]
fn test_variant_detection() {
let address = PoolIdentifier::new("0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2");
let pool_id = PoolIdentifier::new(
"0xc9bc8043294146424a4e4607d8ad837d6a659142822bbaaabc83bb57e7447461",
);
assert!(address.is_address());
assert!(!address.is_pool_id());
assert!(pool_id.is_pool_id());
assert!(!pool_id.is_address());
}
#[rstest]
fn test_different_variants_not_equal() {
let address = PoolIdentifier::new("0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2");
let pool_id = PoolIdentifier::new(
"0xc9bc8043294146424a4e4607d8ad837d6a659142822bbaaabc83bb57e7447461",
);
assert_ne!(address, pool_id);
}
#[rstest]
fn test_serialization_roundtrip() {
let original = PoolIdentifier::new("0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2");
let json = serde_json::to_string(&original).unwrap();
let deserialized: PoolIdentifier = serde_json::from_str(&json).unwrap();
assert_eq!(original, deserialized);
}
#[rstest]
fn test_from_address() {
let addr = Address::from_str("0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2").unwrap();
let pool_id = PoolIdentifier::from_address(addr);
assert!(pool_id.is_address());
assert_eq!(
pool_id.to_string(),
"0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2"
);
}
#[rstest]
fn test_from_pool_id_bytes() {
let bytes: [u8; 32] = [
0xc9, 0xbc, 0x80, 0x43, 0x29, 0x41, 0x46, 0x42, 0x4a, 0x4e, 0x46, 0x07, 0xd8, 0xad,
0x83, 0x7d, 0x6a, 0x65, 0x91, 0x42, 0x82, 0x2b, 0xba, 0xaa, 0xbc, 0x83, 0xbb, 0x57,
0xe7, 0x44, 0x74, 0x61,
];
let pool_id = PoolIdentifier::from_pool_id_bytes(&bytes).unwrap();
assert!(pool_id.is_pool_id());
assert_eq!(
pool_id.to_string(),
"0xc9bc8043294146424a4e4607d8ad837d6a659142822bbaaabc83bb57e7447461"
);
}
#[rstest]
fn test_to_address() {
let id = PoolIdentifier::new("0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2");
let address = id.to_address().unwrap();
assert_eq!(
address.to_string(),
"0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2"
);
}
#[rstest]
fn test_to_address_fails_for_pool_id() {
let pool_id = PoolIdentifier::new(
"0xc9bc8043294146424a4e4607d8ad837d6a659142822bbaaabc83bb57e7447461",
);
let result = pool_id.to_address();
assert!(result.is_err());
}
#[rstest]
fn test_to_pool_id_bytes() {
let pool_id = PoolIdentifier::new(
"0xc9bc8043294146424a4e4607d8ad837d6a659142822bbaaabc83bb57e7447461",
);
let bytes = pool_id.to_pool_id_bytes().unwrap();
assert_eq!(bytes.len(), 32);
assert_eq!(bytes[0], 0xc9);
assert_eq!(bytes[31], 0x61);
}
#[rstest]
fn test_to_pool_id_bytes_fails_for_address() {
let address = PoolIdentifier::new("0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2");
let result = address.to_pool_id_bytes();
assert!(result.is_err());
}
#[rstest]
fn test_conversion_roundtrip_address() {
let original_addr =
Address::from_str("0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2").unwrap();
let pool_id = PoolIdentifier::from_address(original_addr);
let converted_addr = pool_id.to_address().unwrap();
assert_eq!(original_addr, converted_addr);
}
#[rstest]
fn test_conversion_roundtrip_pool_id() {
let original_bytes: [u8; 32] = [
0xc9, 0xbc, 0x80, 0x43, 0x29, 0x41, 0x46, 0x42, 0x4a, 0x4e, 0x46, 0x07, 0xd8, 0xad,
0x83, 0x7d, 0x6a, 0x65, 0x91, 0x42, 0x82, 0x2b, 0xba, 0xaa, 0xbc, 0x83, 0xbb, 0x57,
0xe7, 0x44, 0x74, 0x61,
];
let pool_id = PoolIdentifier::from_pool_id_bytes(&original_bytes).unwrap();
let converted_bytes = pool_id.to_pool_id_bytes().unwrap();
assert_eq!(original_bytes, converted_bytes);
}
}