Skip to main content

accumulate_client/generated/
header.rs

1//! GENERATED FILE - DO NOT EDIT
2//! Source: protocol/transaction.yml
3//! Generated: 2025-10-03 22:05:19
4
5#![allow(missing_docs)]
6
7use serde::{Serialize, Deserialize};
8
9
10mod hex_option_vec {
11    use serde::{Deserialize, Deserializer, Serializer};
12
13    pub fn serialize<S>(value: &Option<Vec<u8>>, serializer: S) -> Result<S::Ok, S::Error>
14    where
15        S: Serializer,
16    {
17        match value {
18            Some(bytes) => serializer.serialize_str(&hex::encode(bytes)),
19            None => serializer.serialize_none(),
20        }
21    }
22
23    pub fn deserialize<'de, D>(deserializer: D) -> Result<Option<Vec<u8>>, D::Error>
24    where
25        D: Deserializer<'de>,
26    {
27        use serde::de::Error;
28        let opt: Option<String> = Option::deserialize(deserializer)?;
29        match opt {
30            Some(hex_str) => {
31                hex::decode(&hex_str).map(Some).map_err(D::Error::custom)
32            }
33            None => Ok(None),
34        }
35    }
36}
37#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
38#[serde(rename_all = "camelCase")]
39pub struct ExpireOptions {
40    pub at_time: Option<u64>,
41}
42
43impl ExpireOptions {
44    pub fn validate(&self) -> Result<(), crate::errors::Error> {
45        // If at_time is provided, it should be a reasonable timestamp
46        // (not in the distant past, and not impossibly far in the future)
47        if let Some(at_time) = self.at_time {
48            // Timestamps before year 2000 are likely errors (Unix timestamp 946684800)
49            const YEAR_2000_UNIX: u64 = 946684800;
50            // Timestamps more than 100 years in the future are likely errors
51            const HUNDRED_YEARS_SECONDS: u64 = 100 * 365 * 24 * 60 * 60;
52
53            if at_time > 0 && at_time < YEAR_2000_UNIX {
54                return Err(crate::errors::ValidationError::OutOfRange {
55                    field: "atTime".to_string(),
56                    min: YEAR_2000_UNIX.to_string(),
57                    max: "far future".to_string(),
58                }.into());
59            }
60
61            // Get current time estimate (we can't use std::time here due to no_std compatibility concerns,
62            // but we can at least check for obviously invalid future values)
63            if at_time > YEAR_2000_UNIX + HUNDRED_YEARS_SECONDS {
64                return Err(crate::errors::ValidationError::OutOfRange {
65                    field: "atTime".to_string(),
66                    min: "now".to_string(),
67                    max: "100 years from epoch".to_string(),
68                }.into());
69            }
70        }
71        Ok(())
72    }
73}
74
75#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
76#[serde(rename_all = "camelCase")]
77pub struct HoldUntilOptions {
78    pub minor_block: Option<u64>,
79}
80
81impl HoldUntilOptions {
82    pub fn validate(&self) -> Result<(), crate::errors::Error> {
83        // If minor_block is provided, it should be positive (block numbers start at 1)
84        if let Some(minor_block) = self.minor_block {
85            if minor_block == 0 {
86                return Err(crate::errors::ValidationError::InvalidFieldValue {
87                    field: "minorBlock".to_string(),
88                    reason: "minor block number must be greater than zero".to_string(),
89                }.into());
90            }
91        }
92        Ok(())
93    }
94}
95
96#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
97#[serde(rename_all = "camelCase")]
98pub struct TransactionHeader {
99    pub principal: String,
100    #[serde(with = "hex::serde")]
101    pub initiator: Vec<u8>,
102    #[serde(skip_serializing_if = "Option::is_none", default)]
103    pub memo: Option<String>,
104    #[serde(skip_serializing_if = "Option::is_none", default)]
105    #[serde(with = "hex_option_vec")]
106    pub metadata: Option<Vec<u8>>,
107    #[serde(skip_serializing_if = "Option::is_none", default)]
108    pub expire: Option<ExpireOptions>,
109    #[serde(skip_serializing_if = "Option::is_none", default)]
110    pub hold_until: Option<HoldUntilOptions>,
111    #[serde(skip_serializing_if = "Option::is_none", default)]
112    pub authorities: Option<Vec<String>>,
113}
114
115impl TransactionHeader {
116    /// Field-level validation aligned with YAML truth
117    pub fn validate(&self) -> Result<(), crate::errors::Error> {
118        if self.principal.is_empty() { return Err(crate::errors::Error::General("Principal URL cannot be empty".to_string())); }
119
120        // Validate principal URL contains only ASCII characters
121        if !self.principal.is_ascii() {
122            return Err(crate::errors::Error::General("Principal URL must contain only ASCII characters".to_string()));
123        }
124
125        // Validate initiator size (reasonable limit: 32KB)
126        const MAX_INITIATOR_SIZE: usize = 32 * 1024;
127        if self.initiator.len() > MAX_INITIATOR_SIZE {
128            return Err(crate::errors::Error::General(format!("Initiator size {} exceeds maximum of {}", self.initiator.len(), MAX_INITIATOR_SIZE)));
129        }
130
131        // Validate authorities (Additional Authorities)
132        if let Some(ref authorities) = self.authorities {
133            self.validate_authorities(authorities)?;
134        }
135
136        // Validate metadata - no null bytes allowed in binary metadata
137        if let Some(ref metadata) = self.metadata {
138            if metadata.contains(&0) {
139                return Err(crate::errors::Error::General("Metadata cannot contain null bytes".to_string()));
140            }
141        }
142
143        if let Some(ref opts) = self.expire { opts.validate()?; }
144        if let Some(ref opts) = self.hold_until { opts.validate()?; }
145        Ok(())
146    }
147
148    /// Validate additional authorities list
149    fn validate_authorities(&self, authorities: &[String]) -> Result<(), crate::errors::Error> {
150        // Maximum number of additional authorities per Go protocol limits
151        const MAX_AUTHORITIES: usize = 20;
152
153        if authorities.len() > MAX_AUTHORITIES {
154            return Err(crate::errors::ValidationError::InvalidFieldValue {
155                field: "authorities".to_string(),
156                reason: format!("too many authorities: {} (max {})", authorities.len(), MAX_AUTHORITIES),
157            }.into());
158        }
159
160        for (index, authority) in authorities.iter().enumerate() {
161            // Authority URL cannot be empty
162            if authority.is_empty() {
163                return Err(crate::errors::ValidationError::InvalidFieldValue {
164                    field: format!("authorities[{}]", index),
165                    reason: "authority URL cannot be empty".to_string(),
166                }.into());
167            }
168
169            // Authority URL must start with acc://
170            if !authority.starts_with("acc://") {
171                return Err(crate::errors::ValidationError::InvalidUrl(
172                    format!("authorities[{}]: must start with 'acc://', got '{}'", index, authority)
173                ).into());
174            }
175
176            // Authority URL must contain only ASCII characters
177            if !authority.is_ascii() {
178                return Err(crate::errors::ValidationError::InvalidUrl(
179                    format!("authorities[{}]: URL must contain only ASCII characters", index)
180                ).into());
181            }
182
183            // Authority URL must not contain whitespace
184            if authority.chars().any(|c| c.is_whitespace()) {
185                return Err(crate::errors::ValidationError::InvalidUrl(
186                    format!("authorities[{}]: URL must not contain whitespace", index)
187                ).into());
188            }
189
190            // Check for reasonable URL length
191            const MAX_URL_LENGTH: usize = 1024;
192            if authority.len() > MAX_URL_LENGTH {
193                return Err(crate::errors::ValidationError::InvalidUrl(
194                    format!("authorities[{}]: URL too long ({} > {})", index, authority.len(), MAX_URL_LENGTH)
195                ).into());
196            }
197
198            // Authority should point to a key book (must contain /book/ or /page/ pattern)
199            // This is advisory - some authorities may be identities themselves
200            // Relaxed validation: just ensure it's a valid Accumulate URL structure
201            let url_path = &authority[6..]; // Skip "acc://"
202            if url_path.is_empty() || url_path == "/" {
203                return Err(crate::errors::ValidationError::InvalidUrl(
204                    format!("authorities[{}]: URL has no identity", index)
205                ).into());
206            }
207        }
208
209        // Check for duplicate authorities
210        let mut seen = std::collections::HashSet::new();
211        for (index, authority) in authorities.iter().enumerate() {
212            let normalized = authority.to_lowercase();
213            if !seen.insert(normalized.clone()) {
214                return Err(crate::errors::ValidationError::InvalidFieldValue {
215                    field: format!("authorities[{}]", index),
216                    reason: format!("duplicate authority URL: {}", authority),
217                }.into());
218            }
219        }
220
221        Ok(())
222    }
223}