thegraph_core/
deployment_id.rs

1use alloy::{hex, primitives::B256};
2
3/// Subgraph deployment ID parsing error.
4#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
5pub enum ParseDeploymentIdError {
6    /// Invalid IPFS hash length. The input string must 46 characters long.
7    #[error("invalid IPFS / CIDv0 hash length {length}: {value} (length must be 46)")]
8    InvalidIpfsHashLength { value: String, length: usize },
9
10    /// Invalid IPFS hash format. The input hash string could not be decoded as a CIDv0.
11    #[error("invalid IPFS hash \"{value}\": {error}")]
12    InvalidIpfsHash { value: String, error: String },
13
14    /// Invalid hex string format. The input hex string could not be decoded.
15    #[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/// A Subgraph's Deployment ID represents unique identifier for a deployed subgraph on The Graph.
29///
30/// This is the content ID of the subgraph's manifest.
31///
32/// ## Generating test data
33///
34/// The `DeploymentId` type implements the [`fake`] crate's [`fake::Dummy`] trait, allowing you to
35/// generate random `DeploymentId` values for testing.
36///
37/// Note that the `fake` feature must be enabled to use this functionality.
38///
39/// See the [`Dummy`] trait impl for usage examples.
40///
41/// [`Dummy`]: #impl-Dummy<Faker>-for-DeploymentId
42#[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    /// The "zero" [`DeploymentId`].
52    ///
53    /// This is a constant value that represents the zero ID. It is equivalent to parsing a zeroed
54    /// 32-byte array.
55    pub const ZERO: Self = Self(B256::ZERO);
56
57    /// Create a new [`DeploymentId`].
58    pub const fn new(bytes: B256) -> Self {
59        Self(bytes)
60    }
61
62    /// Get the bytes of the [`DeploymentId`] as a slice.
63    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    /// Parse a deployment ID from a 32-byte hex string or a base58-encoded IPFS hash (CIDv0).
148    fn from_str(value: &str) -> Result<Self, Self::Err> {
149        if value.starts_with("Qm") {
150            // Attempt to decode base58-encoded CIDv0
151            parse_cid_v0_str(value)
152        } else {
153            // Attempt to decode 32-byte hex string
154            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    /// Parse a deployment ID from a 32-byte hex string.
163    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    /// Format the `DeploymentId` as CIDv0 (base58-encoded sha256-hash) string.
170    ///
171    /// ```rust
172    /// # use thegraph_core::{deployment_id, DeploymentId};
173    /// const ID: DeploymentId = deployment_id!("QmSWxvd8SaQK6qZKJ7xtfxCCGoRzGnoi2WNzmJYYJW9BXY");
174    ///
175    /// assert_eq!(format!("{}", ID), "QmSWxvd8SaQK6qZKJ7xtfxCCGoRzGnoi2WNzmJYYJW9BXY");
176    /// ```
177    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    /// Format the `DeploymentId` as a debug string.
184    ///
185    /// ```rust
186    /// # use thegraph_core::{deployment_id, DeploymentId};
187    /// const ID: DeploymentId = deployment_id!("QmSWxvd8SaQK6qZKJ7xtfxCCGoRzGnoi2WNzmJYYJW9BXY");
188    ///
189    /// assert_eq!(
190    ///     format!("{:?}", ID),
191    ///     "DeploymentId(QmSWxvd8SaQK6qZKJ7xtfxCCGoRzGnoi2WNzmJYYJW9BXY)",
192    /// );
193    /// ```
194    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    /// Format the `DeploymentId` as a 32-byte hex string.
201    ///
202    /// Note that the alternate flag, `#`, adds a `0x` in front of the output.
203    ///
204    /// ```rust
205    /// # use thegraph_core::{deployment_id, DeploymentId};
206    /// const ID: DeploymentId = deployment_id!("QmWmyoMoctfbAaiEs2G46gpeUmhqFRDW6KWo64y5r581Vz");
207    ///
208    /// // Lower hex
209    /// assert_eq!(
210    ///     format!("{:x}", ID),
211    ///     "7d5a99f603f231d53a4f39d1521f98d2e8bb279cf29bebfd0687dc98458e7f89"
212    /// );
213    ///
214    /// // Lower hex with alternate flag
215    /// assert_eq!(
216    ///     format!("{:#x}", ID),
217    ///     "0x7d5a99f603f231d53a4f39d1521f98d2e8bb279cf29bebfd0687dc98458e7f89"
218    /// );
219    /// ```
220    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")]
226/// To use the [`fake`] crate to generate random [`DeploymentId`] values, **the `fake` feature must
227/// be enabled.**
228///
229/// ```rust
230/// # use thegraph_core::DeploymentId;
231/// # use fake::Fake;
232/// let deployment_id = fake::Faker.fake::<DeploymentId>();
233///
234/// println!("DeploymentId: {}", deployment_id);
235/// ```
236impl 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        // Convert to CIDv0 (Qm... base58-encoded sha256-hash)
255        async_graphql::Value::String(self.to_string())
256    }
257}
258
259/// Format bytes as a CIDv0 string.
260///
261/// The CIDv0 format is a base58-encoded sha256-hash with a prefix of `Qm`
262fn 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    // Check if the string has a valid length for a CIDv0 (46 characters)
271    if value.len() != 46 {
272        return Err(ParseDeploymentIdError::InvalidIpfsHashLength {
273            value: value.to_string(),
274            length: value.len(),
275        });
276    }
277
278    // Decode the base58-encoded CIDv0
279    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    // Extract the 32-byte hash from the buffer
288    let mut bytes = [0_u8; 32];
289    bytes.copy_from_slice(&buffer[2..]);
290
291    Ok(DeploymentId::new(B256::new(bytes)))
292}
293
294/// Converts a sequence of string literals containing CIDv0 data into a new [`DeploymentId`] at
295/// compile time.
296///
297/// To create an `DeploymentId` from a string literal (Base58) at compile time:
298///
299/// ```rust
300/// # use thegraph_core::{deployment_id, DeploymentId};
301/// const DEPLOYMENT_ID: DeploymentId = deployment_id!("QmSWxvd8SaQK6qZKJ7xtfxCCGoRzGnoi2WNzmJYYJW9BXY");
302/// ```
303///
304/// If no argument is provided, the macro will create an `DeploymentId` with the zero ID:
305///
306/// ```rust
307/// # use thegraph_core::{deployment_id, DeploymentId};
308/// const DEPLOYMENT_ID: DeploymentId = deployment_id!();
309///
310/// assert_eq!(DEPLOYMENT_ID, DeploymentId::ZERO);
311/// ```
312#[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/// Parse a CIDv0 string into a 32-byte hash.
324#[doc(hidden)]
325pub const fn __parse_cid_v0_const(value: &str) -> B256 {
326    // Check if the string has a valid length for a CIDv0 (46 characters)
327    if value.len() != 46 {
328        panic!("invalid string length (length must be 46)");
329    }
330
331    // Check if the string starts with "Qm"
332    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    // Decode the base58-encoded CIDv0 (34 bytes)
338    let decoded = bs58::decode(data).into_array_const_unwrap::<34>();
339
340    // Extract the 32-byte hash from the buffer
341    // Perform bytes.copy_from_slice(&decoded[2..]) in a const fn context
342    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        //* Given
369        let valid_cid = VALID_CID;
370
371        //* When
372        let result = parse_cid_v0_str(valid_cid);
373
374        //* Then
375        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        //* Given
383        let invalid_cid = "QmA";
384
385        //* When
386        let result = parse_cid_v0_str(invalid_cid);
387
388        //* Then
389        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        //* Given
402        let invalid_cid = "QmfVqZ9gPyMdU6TznRUh+Y0ui7J5zym+v9BofcmEWOf4k=";
403
404        //* When
405        let result = parse_cid_v0_str(invalid_cid);
406
407        //* Then
408        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        //* Given
425        let expected_str = VALID_CID;
426
427        let bytes = EXPECTED_DEPLOYMENT_BYTES.as_slice();
428
429        //* When
430        let cid = format_cid_v0(bytes);
431
432        //* Then
433        assert_eq!(cid, expected_str);
434    }
435
436    #[test]
437    fn format_deployment_id_display() {
438        //* Given
439        let expected_str = VALID_CID;
440
441        let valid_id = EXPECTED_DEPLOYMENT_ID;
442
443        //* When
444        let result_str = format!("{}", valid_id);
445
446        //* Then
447        assert_eq!(result_str, expected_str);
448    }
449
450    #[test]
451    fn format_deployment_id_lower_hex() {
452        //* Given
453        let expected_str = VALID_HEX;
454
455        let valid_id = EXPECTED_DEPLOYMENT_ID;
456
457        //* When
458        // The alternate flag, #, adds a 0x in front of the output
459        let result_str = format!("{:#x}", valid_id);
460
461        //* Then
462        assert_eq!(result_str, expected_str);
463    }
464
465    #[test]
466    fn format_deployment_id_debug() {
467        //* Given
468        let expected_str = format!("DeploymentId({})", VALID_CID);
469
470        let valid_id = EXPECTED_DEPLOYMENT_ID;
471
472        //* When
473        let result_str = format!("{:?}", valid_id);
474
475        //* Then
476        assert_eq!(result_str, expected_str);
477    }
478
479    #[test]
480    fn deployment_id_equality() {
481        //* Given
482        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        //* When
489        let result_cid = DeploymentId::from_str(valid_cid);
490        let result_hex = DeploymentId::from_str(valid_hex);
491
492        //* Then
493        let id_cid = result_cid.expect("expected a valid ID");
494        let id_hex = result_hex.expect("expected a valid ID");
495
496        // Assert the two IDs internal representation is correct
497        assert_eq!(id_cid, expected_id);
498        assert_eq!(id_hex, expected_id);
499
500        // Assert the two IDs CIDv0 representation is correct
501        assert_eq!(id_cid.to_string(), expected_repr);
502        assert_eq!(id_hex.to_string(), expected_repr);
503
504        // Assert both IDs are equal
505        assert_eq!(id_cid, id_hex);
506    }
507}