1pub mod anchor;
7#[cfg(feature = "client")]
8pub mod client;
9pub mod seeds;
10#[cfg(feature = "signing")]
11pub mod signing;
12
13use std::collections::HashMap;
14use std::fmt;
15use std::net::IpAddr;
16use std::str::FromStr;
17
18use serde::{Deserialize, Serialize};
19
20pub extern crate libveritas;
22pub use libveritas::msg::Message;
24use spaces_nums::RootAnchor;
25
26#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
27pub struct TrustId([u8; 32]);
28
29impl TrustId {
30 pub fn to_bytes(self) -> [u8; 32] {
31 self.0
32 }
33}
34
35impl fmt::Display for TrustId {
36 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
37 write!(f, "{}", hex::encode(self.0))
38 }
39}
40
41impl From<[u8; 32]> for TrustId {
42 fn from(bytes: [u8; 32]) -> Self {
43 Self(bytes)
44 }
45}
46
47impl FromStr for TrustId {
48 type Err = hex::FromHexError;
49
50 fn from_str(s: &str) -> Result<Self, Self::Err> {
51 let bytes: [u8; 32] = hex::decode(s)?
52 .try_into()
53 .map_err(|_| hex::FromHexError::InvalidStringLength)?;
54
55 Ok(Self(bytes))
56 }
57}
58
59pub mod capabilities {
64 }
66
67#[derive(Clone, Debug, Serialize, Deserialize)]
69pub struct Query {
70 pub space: String,
72 pub handles: Vec<String>,
74 #[serde(skip_serializing_if = "Option::is_none")]
76 pub epoch_hint: Option<EpochHint>,
77}
78
79impl Query {
80 pub fn new(space: impl Into<String>, handles: Vec<String>) -> Self {
81 Self {
82 space: space.into(),
83 handles,
84 epoch_hint: None,
85 }
86 }
87
88 pub fn with_epoch_hint(mut self, hint: EpochHint) -> Self {
89 self.epoch_hint = Some(hint);
90 self
91 }
92}
93
94#[derive(Clone, Debug, Serialize, Deserialize)]
95pub struct HintsResponse {
96 pub anchor_tip: u32,
97 pub hints: Vec<SpaceHint>,
98}
99
100#[derive(Clone, Debug, Serialize, Deserialize)]
101pub struct AnchorSet {
102 pub entries: Vec<RootAnchor>,
103}
104
105#[derive(Clone, Debug, Serialize, Deserialize)]
106pub struct SpaceHint {
107 pub epoch_tip: u32,
108 pub name: String,
109 pub seq: u64,
110 pub delegate_seq: u64,
111 pub epochs: Vec<EpochResult>,
112}
113
114#[derive(Clone, Debug, Serialize, Deserialize)]
115pub struct EpochResult {
116 pub epoch: u32,
117 pub res: Vec<HandleHint>,
118}
119
120#[derive(Clone, Debug, Serialize, Deserialize)]
121pub struct HandleHint {
122 pub seq: u64,
123 pub name: String,
124}
125
126impl PartialEq for HintsResponse {
127 fn eq(&self, other: &Self) -> bool {
128 self.cmp(other) == std::cmp::Ordering::Equal
129 }
130}
131
132impl Eq for HintsResponse {}
133
134impl PartialOrd for HintsResponse {
135 fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
136 Some(self.cmp(other))
137 }
138}
139
140impl Ord for HintsResponse {
141 fn cmp(&self, other: &Self) -> std::cmp::Ordering {
142 let mut score: i32 = 0;
143
144 for space in &self.hints {
145 let Some(other_space) = other.hints.iter().find(|s| s.name == space.name) else {
146 score += 1;
147 continue;
148 };
149
150 score += cmp_score(space.epoch_tip, other_space.epoch_tip);
151 score += cmp_score(space.seq, other_space.seq);
152 score += cmp_score(space.delegate_seq, other_space.delegate_seq);
153
154 let self_handles = flatten_handles(space);
155 let other_handles = flatten_handles(other_space);
156
157 for (name, self_seq) in &self_handles {
158 match other_handles.get(*name) {
159 Some(other_seq) => score += cmp_score(*self_seq, *other_seq),
160 None => score += 1,
161 }
162 }
163 for name in other_handles.keys() {
164 if !self_handles.contains_key(*name) {
165 score -= 1;
166 }
167 }
168 }
169
170 for other_space in &other.hints {
171 if !self.hints.iter().any(|s| s.name == other_space.name) {
172 score -= 1;
173 }
174 }
175
176 if score != 0 {
177 score.cmp(&0)
178 } else {
179 self.anchor_tip.cmp(&other.anchor_tip)
180 }
181 }
182}
183
184fn cmp_score<T: Ord>(a: T, b: T) -> i32 {
185 match a.cmp(&b) {
186 std::cmp::Ordering::Greater => 1,
187 std::cmp::Ordering::Less => -1,
188 std::cmp::Ordering::Equal => 0,
189 }
190}
191
192fn flatten_handles(space: &SpaceHint) -> HashMap<&str, u64> {
193 let mut map = HashMap::new();
194 for epoch in &space.epochs {
195 for handle in &epoch.res {
196 let existing = map.get(handle.name.as_str()).copied().unwrap_or(0);
197 if handle.seq > existing {
198 map.insert(handle.name.as_str(), handle.seq);
199 }
200 }
201 }
202 map
203}
204
205#[derive(Clone, Debug, Serialize, Deserialize)]
210pub struct EpochHint {
211 pub root: String,
213 pub height: u32,
215}
216
217#[derive(Clone, Debug, Serialize, Deserialize)]
219pub struct QueryRequest {
220 pub queries: Vec<Query>,
222}
223
224impl QueryRequest {
225 pub fn new(queries: Vec<Query>) -> Self {
226 Self { queries }
227 }
228
229 pub fn single(space: impl Into<String>, handles: Vec<String>) -> Self {
230 Self {
231 queries: vec![Query::new(space, handles)],
232 }
233 }
234}
235
236#[derive(Clone, Debug, Serialize, Deserialize)]
240pub struct Announcement {
241 pub url: String,
243 pub capabilities: u32,
245}
246
247impl Announcement {
248 pub fn new(url: impl Into<String>, capabilities: u32) -> Self {
249 Self {
250 url: url.into(),
251 capabilities,
252 }
253 }
254
255 pub fn has_capability(&self, cap: u32) -> bool {
256 self.capabilities & cap != 0
257 }
258}
259
260#[derive(Clone, Debug, Serialize, Deserialize)]
262pub struct PeerInfo {
263 pub source_ip: IpAddr,
265 pub url: String,
267 pub capabilities: u32,
269}
270
271impl PeerInfo {
272 pub fn has_capability(&self, cap: u32) -> bool {
273 self.capabilities & cap != 0
274 }
275}
276
277#[derive(Clone, Debug, Serialize, Deserialize)]
279pub struct ReverseRecord {
280 pub id: String,
281 pub name: String,
282}
283
284#[derive(Clone, Debug, Serialize, Deserialize)]
286pub struct AddrMatch {
287 pub address: String,
288 pub handles: Vec<AddrEntry>,
289}
290
291#[derive(Clone, Debug, Serialize, Deserialize)]
293pub struct AddrEntry {
294 pub handle: String,
296 pub rev: String,
298}
299
300impl AnchorSet {
301 pub fn from_anchors(anchors: Vec<RootAnchor>) -> Self {
302 Self { entries: anchors }
303 }
304}
305
306#[cfg(test)]
307mod tests {
308 use super::*;
309
310 #[test]
311 fn test_query_roundtrip() {
312 let query = Query::new("@bitcoin", vec!["alice".into()]);
313 let req = QueryRequest::new(vec![query]);
314
315 let json = serde_json::to_string(&req).unwrap();
316 let decoded: QueryRequest = serde_json::from_str(&json).unwrap();
317
318 assert_eq!(decoded.queries.len(), 1);
319 assert_eq!(decoded.queries[0].space, "@bitcoin");
320 assert_eq!(decoded.queries[0].handles, vec!["alice"]);
321 }
322
323 #[test]
324 fn test_announcement_roundtrip() {
325 let announcement = Announcement::new("https://relay.example.com", 0);
326 let json = serde_json::to_string(&announcement).unwrap();
327 let decoded: Announcement = serde_json::from_str(&json).unwrap();
328
329 assert_eq!(decoded.url, "https://relay.example.com");
330 assert_eq!(decoded.capabilities, 0);
331 }
332
333 #[test]
334 fn test_peer_info_roundtrip() {
335 let peer = PeerInfo {
336 source_ip: "192.168.1.1".parse().unwrap(),
337 url: "https://peer.example.com".to_string(),
338 capabilities: 0,
339 };
340 let json = serde_json::to_string(&peer).unwrap();
341 let decoded: PeerInfo = serde_json::from_str(&json).unwrap();
342
343 assert_eq!(decoded.url, "https://peer.example.com");
344 assert_eq!(decoded.source_ip.to_string(), "192.168.1.1");
345 }
346
347 #[test]
348 fn test_epoch_hint_skipped_when_none() {
349 let query = Query::new("@bitcoin", vec!["alice".into()]);
350 let json = serde_json::to_string(&query).unwrap();
351 assert!(!json.contains("epoch_hint"));
352 }
353}