thegraph_core/
deployment_id.rs1use alloy::{hex, primitives::B256};
2
3#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
5pub enum ParseDeploymentIdError {
6 #[error("invalid IPFS / CIDv0 hash length {length}: {value} (length must be 46)")]
8 InvalidIpfsHashLength { value: String, length: usize },
9
10 #[error("invalid IPFS hash \"{value}\": {error}")]
12 InvalidIpfsHash { value: String, error: String },
13
14 #[error("invalid hex string \"{value}\": {error}")]
16 InvalidHexString { value: String, error: String },
17}
18
19impl From<hex::FromHexError> for ParseDeploymentIdError {
20 fn from(err: hex::FromHexError) -> Self {
21 ParseDeploymentIdError::InvalidHexString {
22 value: String::new(),
23 error: format!("{err}"),
24 }
25 }
26}
27
28#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
43#[cfg_attr(
44 feature = "serde",
45 derive(serde_with::SerializeDisplay, serde_with::DeserializeFromStr)
46)]
47#[repr(transparent)]
48pub struct DeploymentId(B256);
49
50impl DeploymentId {
51 pub const ZERO: Self = Self(B256::ZERO);
56
57 pub const fn new(bytes: B256) -> Self {
59 Self(bytes)
60 }
61
62 pub fn as_bytes(&self) -> &[u8; 32] {
64 self.0.as_ref()
65 }
66}
67
68impl AsRef<B256> for DeploymentId {
69 fn as_ref(&self) -> &B256 {
70 &self.0
71 }
72}
73
74impl AsRef<[u8]> for DeploymentId {
75 fn as_ref(&self) -> &[u8] {
76 self.0.as_ref()
77 }
78}
79
80impl AsRef<[u8; 32]> for DeploymentId {
81 fn as_ref(&self) -> &[u8; 32] {
82 self.0.as_ref()
83 }
84}
85
86impl std::borrow::Borrow<[u8]> for DeploymentId {
87 fn borrow(&self) -> &[u8] {
88 self.0.borrow()
89 }
90}
91
92impl std::borrow::Borrow<[u8; 32]> for DeploymentId {
93 fn borrow(&self) -> &[u8; 32] {
94 self.0.borrow()
95 }
96}
97
98impl std::ops::Deref for DeploymentId {
99 type Target = B256;
100
101 fn deref(&self) -> &Self::Target {
102 &self.0
103 }
104}
105
106impl From<B256> for DeploymentId {
107 fn from(bytes: B256) -> Self {
108 Self(bytes)
109 }
110}
111
112impl From<[u8; 32]> for DeploymentId {
113 fn from(value: [u8; 32]) -> Self {
114 Self(value.into())
115 }
116}
117
118impl<'a> From<&'a [u8; 32]> for DeploymentId {
119 fn from(value: &'a [u8; 32]) -> Self {
120 Self(value.into())
121 }
122}
123
124impl<'a> TryFrom<&'a [u8]> for DeploymentId {
125 type Error = <B256 as TryFrom<&'a [u8]>>::Error;
126
127 fn try_from(value: &[u8]) -> Result<Self, Self::Error> {
128 value.try_into().map(Self)
129 }
130}
131
132impl From<DeploymentId> for B256 {
133 fn from(id: DeploymentId) -> Self {
134 id.0
135 }
136}
137
138impl From<&DeploymentId> for B256 {
139 fn from(id: &DeploymentId) -> Self {
140 id.0
141 }
142}
143
144impl std::str::FromStr for DeploymentId {
145 type Err = ParseDeploymentIdError;
146
147 fn from_str(value: &str) -> Result<Self, Self::Err> {
149 if value.starts_with("Qm") {
150 parse_cid_v0_str(value)
152 } else {
153 hex::FromHex::from_hex(value).map_err(Into::into)
155 }
156 }
157}
158
159impl hex::FromHex for DeploymentId {
160 type Error = hex::FromHexError;
161
162 fn from_hex<T: AsRef<[u8]>>(hex: T) -> Result<Self, Self::Error> {
164 B256::from_hex(hex).map(Self)
165 }
166}
167
168impl std::fmt::Display for DeploymentId {
169 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
178 f.write_str(&format_cid_v0(self.0.as_slice()))
179 }
180}
181
182impl std::fmt::Debug for DeploymentId {
183 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
195 write!(f, "DeploymentId({self})")
196 }
197}
198
199impl std::fmt::LowerHex for DeploymentId {
200 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
221 std::fmt::LowerHex::fmt(&self.0, f)
222 }
223}
224
225#[cfg(feature = "fake")]
226impl fake::Dummy<fake::Faker> for DeploymentId {
237 fn dummy_with_rng<R: fake::Rng + ?Sized>(config: &fake::Faker, rng: &mut R) -> Self {
238 <[u8; 32]>::dummy_with_rng(config, rng).into()
239 }
240}
241
242#[cfg(feature = "async-graphql")]
243#[async_graphql::Scalar]
244impl async_graphql::ScalarType for DeploymentId {
245 fn parse(value: async_graphql::Value) -> async_graphql::InputValueResult<Self> {
246 if let async_graphql::Value::String(value) = &value {
247 Ok(value.parse::<DeploymentId>()?)
248 } else {
249 Err(async_graphql::InputValueError::expected_type(value))
250 }
251 }
252
253 fn to_value(&self) -> async_graphql::Value {
254 async_graphql::Value::String(self.to_string())
256 }
257}
258
259fn format_cid_v0(bytes: &[u8]) -> String {
263 let mut buf = [0_u8; 34];
264 buf[0..2].copy_from_slice(&[0x12, 0x20]);
265 buf[2..].copy_from_slice(bytes);
266 bs58::encode(buf).into_string()
267}
268
269fn parse_cid_v0_str(value: &str) -> Result<DeploymentId, ParseDeploymentIdError> {
270 if value.len() != 46 {
272 return Err(ParseDeploymentIdError::InvalidIpfsHashLength {
273 value: value.to_string(),
274 length: value.len(),
275 });
276 }
277
278 let mut buffer = [0_u8; 34];
280 bs58::decode(value)
281 .onto(&mut buffer)
282 .map_err(|e| ParseDeploymentIdError::InvalidIpfsHash {
283 value: value.to_string(),
284 error: e.to_string(),
285 })?;
286
287 let mut bytes = [0_u8; 32];
289 bytes.copy_from_slice(&buffer[2..]);
290
291 Ok(DeploymentId::new(B256::new(bytes)))
292}
293
294#[macro_export]
313#[doc(hidden)]
314macro_rules! __deployment_id {
315 () => {
316 $crate::DeploymentId::ZERO
317 };
318 ($id:tt) => {
319 $crate::DeploymentId::new($crate::__parse_cid_v0_const($id))
320 };
321}
322
323#[doc(hidden)]
325pub const fn __parse_cid_v0_const(value: &str) -> B256 {
326 if value.len() != 46 {
328 panic!("invalid string length (length must be 46)");
329 }
330
331 let data = value.as_bytes();
333 if data[0] != b'Q' || data[1] != b'm' {
334 panic!("provided string does not start with 'Qm'");
335 }
336
337 let decoded = bs58::decode(data).into_array_const_unwrap::<34>();
339
340 let mut bytes = [0_u8; 32];
343 let mut i = 0;
344 while i < 32 {
345 bytes[i] = decoded[i + 2];
346 i += 1;
347 }
348 B256::new(bytes)
349}
350
351#[cfg(test)]
352mod tests {
353 use std::str::FromStr;
354
355 use alloy::primitives::{B256, b256};
356
357 use super::{DeploymentId, ParseDeploymentIdError, format_cid_v0, parse_cid_v0_str};
358 use crate::deployment_id;
359
360 const VALID_CID: &str = "QmWmyoMoctfbAaiEs2G46gpeUmhqFRDW6KWo64y5r581Vz";
361 const VALID_HEX: &str = "0x7d5a99f603f231d53a4f39d1521f98d2e8bb279cf29bebfd0687dc98458e7f89";
362 const EXPECTED_DEPLOYMENT_ID: DeploymentId = deployment_id!(VALID_CID);
363 const EXPECTED_DEPLOYMENT_BYTES: B256 =
364 b256!("7d5a99f603f231d53a4f39d1521f98d2e8bb279cf29bebfd0687dc98458e7f89");
365
366 #[test]
367 fn parse_valid_cid_v0() {
368 let valid_cid = VALID_CID;
370
371 let result = parse_cid_v0_str(valid_cid);
373
374 let id = result.expect("expected a valid ID");
376 assert_eq!(id, EXPECTED_DEPLOYMENT_ID);
377 assert_eq!(id.0, EXPECTED_DEPLOYMENT_BYTES);
378 }
379
380 #[test]
381 fn parse_invalid_length_cid_v0() {
382 let invalid_cid = "QmA";
384
385 let result = parse_cid_v0_str(invalid_cid);
387
388 let err = result.expect_err("expected an error");
390 assert_eq!(
391 err,
392 ParseDeploymentIdError::InvalidIpfsHashLength {
393 value: invalid_cid.to_string(),
394 length: invalid_cid.len(),
395 }
396 );
397 }
398
399 #[test]
400 fn parse_invalid_base58_character_cid_v0() {
401 let invalid_cid = "QmfVqZ9gPyMdU6TznRUh+Y0ui7J5zym+v9BofcmEWOf4k=";
403
404 let result = parse_cid_v0_str(invalid_cid);
406
407 let err = result.expect_err("expected an error");
409 assert_eq!(
410 err,
411 ParseDeploymentIdError::InvalidIpfsHash {
412 value: invalid_cid.to_string(),
413 error: bs58::decode::Error::InvalidCharacter {
414 character: '+',
415 index: 20,
416 }
417 .to_string(),
418 }
419 );
420 }
421
422 #[test]
423 fn format_into_cid_v0() {
424 let expected_str = VALID_CID;
426
427 let bytes = EXPECTED_DEPLOYMENT_BYTES.as_slice();
428
429 let cid = format_cid_v0(bytes);
431
432 assert_eq!(cid, expected_str);
434 }
435
436 #[test]
437 fn format_deployment_id_display() {
438 let expected_str = VALID_CID;
440
441 let valid_id = EXPECTED_DEPLOYMENT_ID;
442
443 let result_str = format!("{}", valid_id);
445
446 assert_eq!(result_str, expected_str);
448 }
449
450 #[test]
451 fn format_deployment_id_lower_hex() {
452 let expected_str = VALID_HEX;
454
455 let valid_id = EXPECTED_DEPLOYMENT_ID;
456
457 let result_str = format!("{:#x}", valid_id);
460
461 assert_eq!(result_str, expected_str);
463 }
464
465 #[test]
466 fn format_deployment_id_debug() {
467 let expected_str = format!("DeploymentId({})", VALID_CID);
469
470 let valid_id = EXPECTED_DEPLOYMENT_ID;
471
472 let result_str = format!("{:?}", valid_id);
474
475 assert_eq!(result_str, expected_str);
477 }
478
479 #[test]
480 fn deployment_id_equality() {
481 let expected_id = deployment_id!(VALID_CID);
483 let expected_repr = VALID_CID;
484
485 let valid_cid = VALID_CID;
486 let valid_hex = VALID_HEX;
487
488 let result_cid = DeploymentId::from_str(valid_cid);
490 let result_hex = DeploymentId::from_str(valid_hex);
491
492 let id_cid = result_cid.expect("expected a valid ID");
494 let id_hex = result_hex.expect("expected a valid ID");
495
496 assert_eq!(id_cid, expected_id);
498 assert_eq!(id_hex, expected_id);
499
500 assert_eq!(id_cid.to_string(), expected_repr);
502 assert_eq!(id_hex.to_string(), expected_repr);
503
504 assert_eq!(id_cid, id_hex);
506 }
507}