cat_gateway/service/common/types/cardano/
transaction_id.rs

1//! Transaction ID.
2
3use std::sync::LazyLock;
4
5use cardano_chain_follower::hashes::{TransactionId, BLAKE_2B256_SIZE};
6use const_format::concatcp;
7use poem_openapi::{
8    registry::{MetaSchema, MetaSchemaRef},
9    types::{Example, ParseError, ParseFromJSON, ParseFromParameter, ParseResult, ToJSON, Type},
10};
11use regex::Regex;
12use serde_json::Value;
13
14use crate::service::{common::types::string_types::impl_string_types, utilities::as_hex_string};
15
16/// Title.
17const TITLE: &str = "Transaction Id/Hash";
18/// Description.
19const DESCRIPTION: &str = "The Blake2b-256 hash of the transaction.";
20/// Example.
21const EXAMPLE: &str = "0x27d0350039fb3d068cccfae902bf2e72583fc553e0aafb960bd9d76d5bff777b";
22/// Length of the hex encoded string;
23const ENCODED_LENGTH: usize = EXAMPLE.len();
24/// Length of the hash itself;
25const HASH_LENGTH: usize = BLAKE_2B256_SIZE;
26/// Validation Regex Pattern
27const PATTERN: &str = concatcp!("^0x", "[A-Fa-f0-9]{", HASH_LENGTH * 2, "}$");
28
29/// Schema.
30#[allow(clippy::cast_lossless)]
31static SCHEMA: LazyLock<MetaSchema> = LazyLock::new(|| {
32    MetaSchema {
33        title: Some(TITLE.to_owned()),
34        description: Some(DESCRIPTION),
35        example: Some(EXAMPLE.into()),
36        max_length: Some(ENCODED_LENGTH),
37        min_length: Some(ENCODED_LENGTH),
38        pattern: Some(PATTERN.to_string()),
39        ..poem_openapi::registry::MetaSchema::ANY
40    }
41});
42
43/// Validate `TxnId` This part is done separately from the `PATTERN`
44fn is_valid(hash: &str) -> bool {
45    /// Regex to validate `TxnId`
46    #[allow(clippy::unwrap_used)] // Safe because the Regex is constant.
47    static RE: LazyLock<Regex> = LazyLock::new(|| Regex::new(PATTERN).unwrap());
48
49    if RE.is_match(hash) {
50        if let Some(h) = hash.strip_prefix("0x") {
51            return hex::decode(h).is_ok();
52        }
53    }
54    false
55}
56
57impl_string_types!(
58    TxnId,
59    "string",
60    "hex:hash(32)",
61    Some(SCHEMA.clone()),
62    is_valid
63);
64
65impl Example for TxnId {
66    fn example() -> Self {
67        Self(EXAMPLE.to_string())
68    }
69}
70
71impl TryFrom<Vec<u8>> for TxnId {
72    type Error = anyhow::Error;
73
74    fn try_from(value: Vec<u8>) -> Result<Self, Self::Error> {
75        if value.len() != HASH_LENGTH {
76            anyhow::bail!("Hash Length Invalid.")
77        }
78        Ok(Self(as_hex_string(&value)))
79    }
80}
81
82impl From<TransactionId> for TxnId {
83    fn from(value: TransactionId) -> Self {
84        Self(value.to_string())
85    }
86}
87
88#[cfg(test)]
89mod tests {
90    use super::*;
91
92    #[test]
93    fn test_txn_id() {
94        assert!(TxnId::parse_from_parameter(EXAMPLE).is_ok());
95
96        let invalid = [
97            "0x27d0350039fb3d068cccfae902bf2e72583fc5",
98            "0x27d0350039fb3d068cccfae902bf2e72583fc553e0aafb960bd9d76d5bff777b0",
99            "0x",
100            "0xqw",
101        ];
102        for v in &invalid {
103            assert!(TxnId::parse_from_parameter(v).is_err());
104        }
105    }
106}