accumulate_client/generated/
header.rs1#![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 let Some(at_time) = self.at_time {
48 const YEAR_2000_UNIX: u64 = 946684800;
50 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 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 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 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 if !self.principal.is_ascii() {
122 return Err(crate::errors::Error::General("Principal URL must contain only ASCII characters".to_string()));
123 }
124
125 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 if let Some(ref authorities) = self.authorities {
133 self.validate_authorities(authorities)?;
134 }
135
136 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 fn validate_authorities(&self, authorities: &[String]) -> Result<(), crate::errors::Error> {
150 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 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 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 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 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 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 let url_path = &authority[6..]; 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 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}