Skip to main content

exo_api/
schema.rs

1// Copyright 2026 Exochain Foundation
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at:
6//
7//     https://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14//
15// SPDX-License-Identifier: Apache-2.0
16
17//! API schema types — request/response envelopes.
18use exo_core::{Did, Hash256, Timestamp};
19use serde::{Deserialize, Serialize};
20use uuid::Uuid;
21
22use crate::error::{ApiError, Result};
23
24/// Incoming API request variants for the EXOCHAIN trust fabric.
25#[derive(Debug, Clone, Serialize, Deserialize)]
26pub enum ApiRequest {
27    CreateTransaction {
28        actor: Did,
29        scope: String,
30    },
31    TransitionState {
32        tx_id: Uuid,
33        target_state: String,
34        actor: Did,
35    },
36    QueryTransaction {
37        tx_id: Uuid,
38    },
39    ResolveIdentity {
40        did: Did,
41    },
42    RegisterIdentity {
43        did: Did,
44        public_key_hash: Hash256,
45    },
46    Deliberate {
47        proposal_hash: Hash256,
48        actor: Did,
49    },
50    Vote {
51        proposal_id: Uuid,
52        approve: bool,
53        actor: Did,
54    },
55    Challenge {
56        target_id: Uuid,
57        grounds: String,
58        actor: Did,
59    },
60}
61
62/// Response envelope returned by the API layer.
63#[derive(Debug, Clone, Serialize, Deserialize)]
64pub enum ApiResponse {
65    Success {
66        correlation_id: Uuid,
67        timestamp: Timestamp,
68    },
69    Error {
70        code: u32,
71        message: String,
72    },
73    TransactionState {
74        tx_id: Uuid,
75        state: String,
76    },
77    Identity {
78        did: Did,
79        verified: bool,
80    },
81    Receipt {
82        hash: Hash256,
83        timestamp: Timestamp,
84    },
85}
86
87/// Compute canonical hash for a request (CBOR -> BLAKE3).
88pub fn canonical_request_hash(request: &ApiRequest) -> Result<Hash256> {
89    let mut buf = Vec::new();
90    write_canonical_request(request, &mut buf)?;
91    Ok(Hash256::digest(&buf))
92}
93
94fn write_canonical_request<W: std::io::Write>(request: &ApiRequest, writer: W) -> Result<()> {
95    ciborium::into_writer(request, writer)
96        .map_err(|err| ApiError::SerializationError(err.to_string()))
97}
98
99#[cfg(test)]
100#[allow(clippy::unwrap_used)]
101mod tests {
102    use super::*;
103    fn did(n: &str) -> Did {
104        Did::new(&format!("did:exo:{n}")).unwrap()
105    }
106
107    #[test]
108    fn request_variants_serde() {
109        let reqs: Vec<ApiRequest> = vec![
110            ApiRequest::CreateTransaction {
111                actor: did("a"),
112                scope: "s".into(),
113            },
114            ApiRequest::TransitionState {
115                tx_id: Uuid::nil(),
116                target_state: "t".into(),
117                actor: did("a"),
118            },
119            ApiRequest::QueryTransaction { tx_id: Uuid::nil() },
120            ApiRequest::ResolveIdentity { did: did("a") },
121            ApiRequest::RegisterIdentity {
122                did: did("a"),
123                public_key_hash: Hash256::ZERO,
124            },
125            ApiRequest::Deliberate {
126                proposal_hash: Hash256::ZERO,
127                actor: did("a"),
128            },
129            ApiRequest::Vote {
130                proposal_id: Uuid::nil(),
131                approve: true,
132                actor: did("a"),
133            },
134            ApiRequest::Challenge {
135                target_id: Uuid::nil(),
136                grounds: "g".into(),
137                actor: did("a"),
138            },
139        ];
140        for r in &reqs {
141            let j = serde_json::to_string(r).unwrap();
142            assert!(!j.is_empty());
143        }
144    }
145    #[test]
146    fn response_variants_serde() {
147        let resps: Vec<ApiResponse> = vec![
148            ApiResponse::Success {
149                correlation_id: Uuid::nil(),
150                timestamp: Timestamp::ZERO,
151            },
152            ApiResponse::Error {
153                code: 400,
154                message: "bad".into(),
155            },
156            ApiResponse::TransactionState {
157                tx_id: Uuid::nil(),
158                state: "s".into(),
159            },
160            ApiResponse::Identity {
161                did: did("a"),
162                verified: true,
163            },
164            ApiResponse::Receipt {
165                hash: Hash256::ZERO,
166                timestamp: Timestamp::ZERO,
167            },
168        ];
169        for r in &resps {
170            let j = serde_json::to_string(r).unwrap();
171            assert!(!j.is_empty());
172        }
173    }
174    #[test]
175    fn canonical_hash_deterministic() {
176        let r = ApiRequest::CreateTransaction {
177            actor: did("a"),
178            scope: "s".into(),
179        };
180        assert_eq!(
181            canonical_request_hash(&r).unwrap(),
182            canonical_request_hash(&r).unwrap()
183        );
184    }
185    #[test]
186    fn canonical_hash_differs() {
187        let r1 = ApiRequest::CreateTransaction {
188            actor: did("a"),
189            scope: "s1".into(),
190        };
191        let r2 = ApiRequest::CreateTransaction {
192            actor: did("a"),
193            scope: "s2".into(),
194        };
195        assert_ne!(
196            canonical_request_hash(&r1).unwrap(),
197            canonical_request_hash(&r2).unwrap()
198        );
199    }
200
201    #[test]
202    fn canonical_hash_writer_error_returns_error() {
203        let r = ApiRequest::CreateTransaction {
204            actor: did("a"),
205            scope: "s".into(),
206        };
207
208        let err = write_canonical_request(&r, FailingWriter).unwrap_err();
209        assert!(err.to_string().contains("serialization error"));
210    }
211
212    struct FailingWriter;
213
214    impl std::io::Write for FailingWriter {
215        fn write(&mut self, _buf: &[u8]) -> std::io::Result<usize> {
216            Err(std::io::Error::other("forced writer failure"))
217        }
218
219        fn flush(&mut self) -> std::io::Result<()> {
220            Ok(())
221        }
222    }
223}