iota_graphql_rpc/types/
digest.rs

1// Copyright (c) Mysten Labs, Inc.
2// Modifications Copyright (c) 2024 IOTA Stiftung
3// SPDX-License-Identifier: Apache-2.0
4
5use std::{fmt, str::FromStr};
6
7use async_graphql::*;
8use fastcrypto::encoding::{Base58, Encoding};
9use iota_types::digests::{ObjectDigest, TransactionDigest};
10
11use crate::types::string_input::impl_string_input;
12
13pub(crate) const BASE58_DIGEST_LENGTH: usize = 32;
14
15#[derive(Copy, Clone, Debug, Hash, Eq, PartialEq, Ord, PartialOrd)]
16pub(crate) struct Digest([u8; BASE58_DIGEST_LENGTH]);
17
18#[derive(thiserror::Error, Debug)]
19pub(crate) enum Error {
20    #[error("Invalid Base58: {0}")]
21    InvalidBase58(String),
22
23    #[error("Expected digest to be {expect}B, but got {actual}B")]
24    BadDigestLength { expect: usize, actual: usize },
25}
26
27impl Digest {
28    pub(crate) fn to_vec(self) -> Vec<u8> {
29        self.0.to_vec()
30    }
31
32    pub(crate) fn as_slice(&self) -> &[u8] {
33        &self.0
34    }
35}
36
37impl_string_input!(Digest);
38
39impl FromStr for Digest {
40    type Err = Error;
41
42    fn from_str(s: &str) -> Result<Self, Self::Err> {
43        let buffer = Base58::decode(s).map_err(|_| Error::InvalidBase58(s.to_string()))?;
44        Digest::try_from(&buffer[..])
45    }
46}
47
48impl TryFrom<&[u8]> for Digest {
49    type Error = Error;
50
51    fn try_from(value: &[u8]) -> Result<Self, Error> {
52        let mut result = [0u8; BASE58_DIGEST_LENGTH];
53
54        if value.len() != BASE58_DIGEST_LENGTH {
55            return Err(Error::BadDigestLength {
56                expect: BASE58_DIGEST_LENGTH,
57                actual: value.len(),
58            });
59        }
60
61        result.copy_from_slice(value);
62        Ok(Digest(result))
63    }
64}
65
66impl From<Digest> for ObjectDigest {
67    fn from(digest: Digest) -> Self {
68        ObjectDigest::new(digest.0)
69    }
70}
71
72impl From<TransactionDigest> for Digest {
73    fn from(digest: TransactionDigest) -> Self {
74        Digest(digest.into_inner())
75    }
76}
77
78impl fmt::Display for Digest {
79    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
80        write!(f, "{}", Base58::encode(self.0))
81    }
82}
83
84#[cfg(test)]
85mod tests {
86    use super::{Error, *};
87
88    #[test]
89    fn test_base58_digest() {
90        let digest = [
91            183u8, 119, 223, 39, 204, 68, 220, 4, 126, 234, 232, 146, 106, 249, 98, 12, 170, 209,
92            98, 203, 243, 77, 154, 225, 177, 216, 169, 101, 51, 116, 79, 223,
93        ];
94
95        assert_eq!(
96            Digest::from_str("DMBdBZnpYR4EeTXzXL8A6BtVafqGjAWGsFZhP2zJYmXU").unwrap(),
97            Digest(digest)
98        );
99
100        assert!(matches!(
101            Digest::from_str("ILoveBase58").unwrap_err(),
102            Error::InvalidBase58(_),
103        ));
104
105        let long_digest = {
106            let mut bytes = vec![];
107            bytes.extend(digest);
108            bytes.extend(digest);
109            Base58::encode(bytes)
110        };
111
112        assert!(matches!(
113            Digest::from_str(&long_digest).unwrap_err(),
114            Error::BadDigestLength { .. },
115        ))
116    }
117}