1use serde::{Deserialize, Serialize};
4use serde_json::Value;
5
6#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
35pub struct ActionEnvelope {
36 pub version: String,
38 #[serde(rename = "type")]
40 pub action_type: String,
41 pub identity: String,
43 pub payload: Value,
45 pub timestamp: String,
47 pub signature: String,
49 #[serde(default, skip_serializing_if = "Option::is_none")]
51 pub attestation_chain: Option<Value>,
52 #[serde(default, skip_serializing_if = "Option::is_none")]
54 pub environment: Option<Value>,
55}
56
57#[derive(Debug, Serialize)]
61pub struct ActionSigningData<'a> {
62 pub version: &'a str,
64 #[serde(rename = "type")]
66 pub action_type: &'a str,
67 pub identity: &'a str,
69 pub payload: &'a Value,
71 pub timestamp: &'a str,
73}
74
75impl ActionEnvelope {
76 pub fn signing_data(&self) -> ActionSigningData<'_> {
78 ActionSigningData {
79 version: &self.version,
80 action_type: &self.action_type,
81 identity: &self.identity,
82 payload: &self.payload,
83 timestamp: &self.timestamp,
84 }
85 }
86
87 pub fn canonical_bytes(&self) -> Result<Vec<u8>, String> {
89 let data = self.signing_data();
90 json_canon::to_string(&data)
91 .map(|s| s.into_bytes())
92 .map_err(|e| format!("canonicalization failed: {e}"))
93 }
94}
95
96#[cfg(test)]
97mod tests {
98 use super::*;
99
100 #[test]
101 fn roundtrip_serialization() {
102 let envelope = ActionEnvelope {
103 version: "1.0".into(),
104 action_type: "sign_commit".into(),
105 identity: "did:keri:Eabc123".into(),
106 payload: serde_json::json!({"hash": "abc123"}),
107 timestamp: "2024-01-01T00:00:00Z".into(),
108 signature: "deadbeef".into(),
109 attestation_chain: None,
110 environment: None,
111 };
112
113 let json = serde_json::to_string(&envelope).unwrap();
114 let parsed: ActionEnvelope = serde_json::from_str(&json).unwrap();
115 assert_eq!(envelope, parsed);
116 }
117
118 #[test]
119 fn type_field_renamed_in_json() {
120 let envelope = ActionEnvelope {
121 version: "1.0".into(),
122 action_type: "sign_commit".into(),
123 identity: "did:keri:Eabc123".into(),
124 payload: serde_json::json!({}),
125 timestamp: "2024-01-01T00:00:00Z".into(),
126 signature: "deadbeef".into(),
127 attestation_chain: None,
128 environment: None,
129 };
130
131 let json = serde_json::to_string(&envelope).unwrap();
132 assert!(json.contains("\"type\":"));
133 assert!(!json.contains("\"action_type\":"));
134 }
135
136 #[test]
137 fn optional_fields_omitted_when_none() {
138 let envelope = ActionEnvelope {
139 version: "1.0".into(),
140 action_type: "sign_commit".into(),
141 identity: "did:keri:Eabc123".into(),
142 payload: serde_json::json!({}),
143 timestamp: "2024-01-01T00:00:00Z".into(),
144 signature: "deadbeef".into(),
145 attestation_chain: None,
146 environment: None,
147 };
148
149 let json = serde_json::to_string(&envelope).unwrap();
150 assert!(!json.contains("attestation_chain"));
151 assert!(!json.contains("environment"));
152 }
153
154 #[test]
155 fn wire_compat_with_python_sdk_format() {
156 let python_wire = serde_json::json!({
157 "version": "1.0",
158 "type": "sign_commit",
159 "identity": "did:keri:Eabc123",
160 "payload": {"hash": "abc123"},
161 "timestamp": "2024-01-01T00:00:00Z",
162 "signature": "deadbeef"
163 });
164
165 let envelope: ActionEnvelope = serde_json::from_value(python_wire.clone()).unwrap();
166 assert_eq!(envelope.version, "1.0");
167 assert_eq!(envelope.action_type, "sign_commit");
168
169 let reserialized: serde_json::Value =
170 serde_json::from_str(&serde_json::to_string(&envelope).unwrap()).unwrap();
171 assert_eq!(python_wire, reserialized);
172 }
173
174 #[test]
175 fn canonical_bytes_excludes_signature() {
176 let envelope = ActionEnvelope {
177 version: "1.0".into(),
178 action_type: "sign_commit".into(),
179 identity: "did:keri:Eabc123".into(),
180 payload: serde_json::json!({"hash": "abc123"}),
181 timestamp: "2024-01-01T00:00:00Z".into(),
182 signature: "different_sig".into(),
183 attestation_chain: Some(serde_json::json!([])),
184 environment: Some(serde_json::json!({"region": "us-east-1"})),
185 };
186
187 let canonical = String::from_utf8(envelope.canonical_bytes().unwrap()).unwrap();
188 assert!(!canonical.contains("signature"));
189 assert!(!canonical.contains("attestation_chain"));
190 assert!(!canonical.contains("environment"));
191 assert!(canonical.contains("\"version\""));
192 assert!(canonical.contains("\"type\""));
193 }
194}