1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
|
//! Audit client for testing GRASP implementations
use crate::audit::{AuditConfig, AuditEventBuilder, AuditMode};
use anyhow::{anyhow, Result};
use nostr_sdk::prelude::*;
use std::time::Duration;
/// Client for auditing GRASP implementations
pub struct AuditClient {
client: Client,
pub config: AuditConfig,
keys: Keys,
}
impl AuditClient {
/// Create a new audit client
pub async fn new(relay_url: &str, config: AuditConfig) -> Result<Self> {
let keys = Keys::generate();
let client = Client::new(&keys);
client.add_relay(relay_url).await?;
client.connect().await;
// Wait a bit for connection to establish
tokio::time::sleep(Duration::from_millis(500)).await;
Ok(Self {
client,
config,
keys,
})
}
/// Get the public key for this audit client
pub fn public_key(&self) -> PublicKey {
self.keys.public_key()
}
/// Check if connected to relay
pub async fn is_connected(&self) -> bool {
// Check if we have any connected relays
let relays = self.client.relays().await;
relays.values().any(|r| r.is_connected())
}
/// Send an event (with audit tags automatically added)
pub async fn send_event(&self, event: Event) -> Result<EventId> {
if self.config.read_only {
return Err(anyhow!("Client is in read-only mode"));
}
let event_id = self.client.send_event(event).await?;
// Wait a bit for event to propagate
tokio::time::sleep(Duration::from_millis(100)).await;
Ok(event_id)
}
/// Create an event builder with audit tags
pub fn event_builder(&self, kind: Kind, content: impl Into<String>) -> AuditEventBuilder {
AuditEventBuilder::new(kind, content, self.config.clone())
}
/// Query events, optionally filtered to this audit run
pub async fn query(&self, mut filter: Filter) -> Result<Vec<Event>> {
if self.config.mode == AuditMode::CI {
// In CI mode, only see our own audit events
filter = filter
.custom_tag(
SingleLetterTag::lowercase(Alphabet::G),
["true"] // grasp-audit tag
)
.custom_tag(
SingleLetterTag::lowercase(Alphabet::R),
[&self.config.run_id] // audit-run-id tag
);
}
// In Production mode, see all events (no filter modification)
let events = self.client
.get_events_of(vec![filter], Some(Duration::from_secs(5)))
.await?;
Ok(events)
}
/// Subscribe to events with a callback
pub async fn subscribe(
&self,
filters: Vec<Filter>,
timeout: Option<Duration>,
) -> Result<Vec<Event>> {
let events = self.client
.get_events_of(filters, timeout)
.await?;
Ok(events)
}
/// Get the underlying nostr client (for advanced usage)
pub fn client(&self) -> &Client {
&self.client
}
/// Get the keys (for signing custom events)
pub fn keys(&self) -> &Keys {
&self.keys
}
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn test_client_creation() {
let config = AuditConfig::ci();
// This will fail if no relay is running, which is expected in tests
// In real usage, there should be a relay at the URL
let result = AuditClient::new("ws://localhost:7000", config).await;
// We can't test connection without a running relay
// But we can test that the client is created
if let Ok(client) = result {
assert_eq!(client.config.mode, AuditMode::CI);
}
}
#[test]
fn test_event_builder() {
let config = AuditConfig::ci();
let keys = Keys::generate();
let client = AuditClient {
client: Client::new(&keys),
config: config.clone(),
keys: keys.clone(),
};
let builder = client.event_builder(Kind::TextNote, "test content");
// Builder should have the config
assert_eq!(builder.config.run_id, config.run_id);
}
}
|