1use std::{
17 fmt::{Debug, Display},
18 hash::{Hash, Hasher},
19 str::FromStr,
20};
21
22use alloy_primitives::Address;
23use nautilus_core::{correctness::FAILED, hex};
24use serde::{Deserialize, Deserializer, Serialize, Serializer};
25use ustr::Ustr;
26
27#[derive(Clone, Copy, PartialOrd, Ord)]
44pub enum PoolIdentifier {
45 Address(Ustr),
47 PoolId(Ustr),
49}
50
51impl PoolIdentifier {
52 pub fn new_checked<T: AsRef<str>>(value: T) -> anyhow::Result<Self> {
66 let value = value.as_ref();
67
68 if !value.starts_with("0x") {
69 anyhow::bail!("Pool identifier must start with '0x', was: {value}");
70 }
71
72 match value.len() {
73 42 => {
74 validate_hex_string(value)?;
75
76 let addr = value
78 .parse::<Address>()
79 .map_err(|e| anyhow::anyhow!("Invalid address: {e}"))?;
80
81 Ok(Self::Address(Ustr::from(addr.to_checksum(None).as_str())))
83 }
84 66 => {
85 validate_hex_string(value)?;
87
88 Ok(Self::PoolId(Ustr::from(&value.to_lowercase())))
90 }
91 len => {
92 anyhow::bail!(
93 "Pool identifier must be 42 chars (address) or 66 chars (pool ID), was {len} chars: {value}"
94 )
95 }
96 }
97 }
98
99 #[must_use]
105 pub fn new<T: AsRef<str>>(value: T) -> Self {
106 Self::new_checked(value).expect(FAILED)
107 }
108
109 #[must_use]
113 pub fn from_address(address: Address) -> Self {
114 Self::Address(Ustr::from(address.to_checksum(None).as_str()))
115 }
116
117 pub fn from_pool_id_bytes(bytes: &[u8]) -> anyhow::Result<Self> {
123 anyhow::ensure!(
124 bytes.len() == 32,
125 "Pool ID must be 32 bytes, was {}",
126 bytes.len()
127 );
128
129 Ok(Self::PoolId(Ustr::from(&hex::encode_prefixed(bytes))))
130 }
131
132 pub fn from_pool_id_hex<T: AsRef<str>>(hex: T) -> anyhow::Result<Self> {
138 let hex = hex.as_ref();
139 let hex_str = hex.strip_prefix("0x").unwrap_or(hex);
140
141 anyhow::ensure!(
142 hex_str.len() == 64,
143 "Pool ID hex must be 64 characters (32 bytes), was {}",
144 hex_str.len()
145 );
146
147 validate_hex_string(&format!("0x{hex_str}"))?;
148
149 Ok(Self::PoolId(Ustr::from(&format!(
150 "0x{}",
151 hex_str.to_lowercase()
152 ))))
153 }
154
155 #[must_use]
157 pub fn inner(&self) -> Ustr {
158 match self {
159 Self::Address(s) | Self::PoolId(s) => *s,
160 }
161 }
162
163 #[must_use]
165 pub fn as_str(&self) -> &str {
166 match self {
167 Self::Address(s) | Self::PoolId(s) => s.as_str(),
168 }
169 }
170
171 #[must_use]
173 pub fn is_address(&self) -> bool {
174 matches!(self, Self::Address(_))
175 }
176
177 #[must_use]
179 pub fn is_pool_id(&self) -> bool {
180 matches!(self, Self::PoolId(_))
181 }
182
183 pub fn to_address(&self) -> anyhow::Result<Address> {
191 match self {
192 Self::Address(s) => Address::parse_checksummed(s.as_str(), None)
193 .map_err(|e| anyhow::anyhow!("Failed to parse address: {e}")),
194 Self::PoolId(_) => anyhow::bail!("Cannot convert PoolId variant to Address"),
195 }
196 }
197
198 pub fn to_pool_id_bytes(&self) -> anyhow::Result<[u8; 32]> {
206 match self {
207 Self::PoolId(s) => {
208 let hex_str = s.as_str().strip_prefix("0x").unwrap_or(s.as_str());
209 hex::decode_array::<32>(hex_str)
210 .map_err(|e| anyhow::anyhow!("Failed to decode pool ID hex: {e}"))
211 }
212 Self::Address(_) => anyhow::bail!("Cannot convert Address variant to PoolId bytes"),
213 }
214 }
215}
216
217fn validate_hex_string(s: &str) -> anyhow::Result<()> {
219 let hex_part = &s[2..];
220 if !hex_part.chars().all(|c| c.is_ascii_hexdigit()) {
221 anyhow::bail!("Invalid hex characters in: {s}");
222 }
223 Ok(())
224}
225
226impl PartialEq for PoolIdentifier {
227 fn eq(&self, other: &Self) -> bool {
228 match (self, other) {
229 (Self::Address(a), Self::Address(b)) | (Self::PoolId(a), Self::PoolId(b)) => {
230 a.as_str().eq_ignore_ascii_case(b.as_str())
232 }
233 _ => false,
235 }
236 }
237}
238
239impl Eq for PoolIdentifier {}
240
241impl Hash for PoolIdentifier {
242 fn hash<H: Hasher>(&self, state: &mut H) {
243 std::mem::discriminant(self).hash(state);
245
246 match self {
248 Self::Address(s) | Self::PoolId(s) => {
249 for byte in s.as_str().bytes() {
250 state.write_u8(byte.to_ascii_lowercase());
251 }
252 }
253 }
254 }
255}
256
257impl Display for PoolIdentifier {
258 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
259 match self {
260 Self::Address(s) | Self::PoolId(s) => write!(f, "{s}"),
261 }
262 }
263}
264
265impl Debug for PoolIdentifier {
266 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
267 match self {
268 Self::Address(s) => write!(f, "Address({s:?})"),
269 Self::PoolId(s) => write!(f, "PoolId({s:?})"),
270 }
271 }
272}
273
274impl Serialize for PoolIdentifier {
275 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
276 where
277 S: Serializer,
278 {
279 match self {
281 Self::Address(s) | Self::PoolId(s) => s.serialize(serializer),
282 }
283 }
284}
285
286impl<'de> Deserialize<'de> for PoolIdentifier {
287 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
288 where
289 D: Deserializer<'de>,
290 {
291 let value_str: std::borrow::Cow<'de, str> = Deserialize::deserialize(deserializer)?;
292 Self::new_checked(value_str.as_ref()).map_err(serde::de::Error::custom)
293 }
294}
295
296impl FromStr for PoolIdentifier {
297 type Err = anyhow::Error;
298
299 fn from_str(s: &str) -> Result<Self, Self::Err> {
300 Self::new_checked(s)
301 }
302}
303
304impl From<&str> for PoolIdentifier {
305 fn from(value: &str) -> Self {
306 Self::new(value)
307 }
308}
309
310impl From<String> for PoolIdentifier {
311 fn from(value: String) -> Self {
312 Self::new(value)
313 }
314}
315
316impl AsRef<str> for PoolIdentifier {
317 fn as_ref(&self) -> &str {
318 self.as_str()
319 }
320}
321
322#[cfg(test)]
323mod tests {
324 use rstest::rstest;
325
326 use super::*;
327
328 #[rstest]
329 #[case("0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2", true)] #[case("0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2", true)] #[case(
332 "0xc9bc8043294146424a4e4607d8ad837d6a659142822bbaaabc83bb57e7447461",
333 true
334 )] fn test_valid_pool_identifiers(#[case] input: &str, #[case] expected_valid: bool) {
336 let result = PoolIdentifier::new_checked(input);
337 assert_eq!(result.is_ok(), expected_valid, "Input: {input}");
338 }
339
340 #[rstest]
341 #[case("C02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2")] #[case("0xC02aaA39")] #[case("0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2EXTRA")] #[case("0xGGGGGGGGb223FE8D0A0e5C4F27eAD9083C756Cc2")] fn test_invalid_pool_identifiers(#[case] input: &str) {
346 let result = PoolIdentifier::new_checked(input);
347 assert!(result.is_err(), "Input should fail: {input}");
348 }
349
350 #[rstest]
351 fn test_case_insensitive_equality() {
352 let addr1 = PoolIdentifier::new("0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2");
353 let addr2 = PoolIdentifier::new("0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2");
354 let addr3 = PoolIdentifier::new("0xC02AAA39B223FE8D0A0E5C4F27EAD9083C756CC2");
355
356 assert_eq!(addr1, addr2);
357 assert_eq!(addr2, addr3);
358 assert_eq!(addr1, addr3);
359 }
360
361 #[rstest]
362 fn test_case_insensitive_hashing() {
363 use std::collections::HashMap;
364
365 let mut map = HashMap::new();
366 let addr1 = PoolIdentifier::new("0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2");
367 let addr2 = PoolIdentifier::new("0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2");
368
369 map.insert(addr1, "value1");
370
371 assert_eq!(map.get(&addr2), Some(&"value1"));
373 }
374
375 #[rstest]
376 fn test_display_preserves_case() {
377 let checksummed = "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2";
378 let addr = PoolIdentifier::new_checked(checksummed).unwrap();
379
380 assert_eq!(addr.to_string(), checksummed);
382 }
383
384 #[rstest]
385 fn test_variant_detection() {
386 let address = PoolIdentifier::new("0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2");
387 let pool_id = PoolIdentifier::new(
388 "0xc9bc8043294146424a4e4607d8ad837d6a659142822bbaaabc83bb57e7447461",
389 );
390
391 assert!(address.is_address());
392 assert!(!address.is_pool_id());
393
394 assert!(pool_id.is_pool_id());
395 assert!(!pool_id.is_address());
396 }
397
398 #[rstest]
399 fn test_different_variants_not_equal() {
400 let address = PoolIdentifier::new("0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2");
401 let pool_id = PoolIdentifier::new(
402 "0xc9bc8043294146424a4e4607d8ad837d6a659142822bbaaabc83bb57e7447461",
403 );
404
405 assert_ne!(address, pool_id);
406 }
407
408 #[rstest]
409 fn test_serialization_roundtrip() {
410 let original = PoolIdentifier::new("0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2");
411
412 let json = serde_json::to_string(&original).unwrap();
413 let deserialized: PoolIdentifier = serde_json::from_str(&json).unwrap();
414
415 assert_eq!(original, deserialized);
416 }
417
418 #[rstest]
419 fn test_deserialize_from_owned_value() {
420 let value =
421 serde_json::Value::String("0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2".to_string());
422
423 let deserialized: PoolIdentifier = serde_json::from_value(value).unwrap();
424 assert_eq!(
425 deserialized,
426 PoolIdentifier::new("0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2")
427 );
428 }
429
430 #[rstest]
431 fn test_from_address() {
432 let addr = Address::from_str("0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2").unwrap();
433 let pool_id = PoolIdentifier::from_address(addr);
434
435 assert!(pool_id.is_address());
436 assert_eq!(
437 pool_id.to_string(),
438 "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2"
439 );
440 }
441
442 #[rstest]
443 fn test_from_pool_id_bytes() {
444 let bytes: [u8; 32] = [
445 0xc9, 0xbc, 0x80, 0x43, 0x29, 0x41, 0x46, 0x42, 0x4a, 0x4e, 0x46, 0x07, 0xd8, 0xad,
446 0x83, 0x7d, 0x6a, 0x65, 0x91, 0x42, 0x82, 0x2b, 0xba, 0xaa, 0xbc, 0x83, 0xbb, 0x57,
447 0xe7, 0x44, 0x74, 0x61,
448 ];
449
450 let pool_id = PoolIdentifier::from_pool_id_bytes(&bytes).unwrap();
451
452 assert!(pool_id.is_pool_id());
453 assert_eq!(
454 pool_id.to_string(),
455 "0xc9bc8043294146424a4e4607d8ad837d6a659142822bbaaabc83bb57e7447461"
456 );
457 }
458
459 #[rstest]
460 fn test_to_address() {
461 let id = PoolIdentifier::new("0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2");
462 let address = id.to_address().unwrap();
463
464 assert_eq!(
465 address.to_string(),
466 "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2"
467 );
468 }
469
470 #[rstest]
471 fn test_to_address_fails_for_pool_id() {
472 let pool_id = PoolIdentifier::new(
473 "0xc9bc8043294146424a4e4607d8ad837d6a659142822bbaaabc83bb57e7447461",
474 );
475 let result = pool_id.to_address();
476
477 assert!(result.is_err());
478 }
479
480 #[rstest]
481 fn test_to_pool_id_bytes() {
482 let pool_id = PoolIdentifier::new(
483 "0xc9bc8043294146424a4e4607d8ad837d6a659142822bbaaabc83bb57e7447461",
484 );
485 let bytes = pool_id.to_pool_id_bytes().unwrap();
486
487 assert_eq!(bytes.len(), 32);
488 assert_eq!(bytes[0], 0xc9);
489 assert_eq!(bytes[31], 0x61);
490 }
491
492 #[rstest]
493 fn test_to_pool_id_bytes_fails_for_address() {
494 let address = PoolIdentifier::new("0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2");
495 let result = address.to_pool_id_bytes();
496
497 assert!(result.is_err());
498 }
499
500 #[rstest]
501 fn test_conversion_roundtrip_address() {
502 let original_addr =
503 Address::from_str("0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2").unwrap();
504 let pool_id = PoolIdentifier::from_address(original_addr);
505 let converted_addr = pool_id.to_address().unwrap();
506
507 assert_eq!(original_addr, converted_addr);
508 }
509
510 #[rstest]
511 fn test_conversion_roundtrip_pool_id() {
512 let original_bytes: [u8; 32] = [
513 0xc9, 0xbc, 0x80, 0x43, 0x29, 0x41, 0x46, 0x42, 0x4a, 0x4e, 0x46, 0x07, 0xd8, 0xad,
514 0x83, 0x7d, 0x6a, 0x65, 0x91, 0x42, 0x82, 0x2b, 0xba, 0xaa, 0xbc, 0x83, 0xbb, 0x57,
515 0xe7, 0x44, 0x74, 0x61,
516 ];
517
518 let pool_id = PoolIdentifier::from_pool_id_bytes(&original_bytes).unwrap();
519 let converted_bytes = pool_id.to_pool_id_bytes().unwrap();
520
521 assert_eq!(original_bytes, converted_bytes);
522 }
523}