1use alloy::primitives::{keccak256, Address, B256, U256};
14use alloy::providers::ProviderBuilder;
15use ed25519_dalek::{Signer, SigningKey, VerifyingKey};
16use reqwest::{Client, Method, RequestBuilder, Response};
17use std::collections::HashMap;
18use std::time::{SystemTime, UNIX_EPOCH};
19
20use crate::agent::{compute_signing_message, AgentInfo};
21use crate::constants::{
22 headers, network_config, IAgentRegistry, NetworkName, DEFAULT_NETWORK,
23};
24
25#[derive(Debug, Clone)]
27pub struct Ed25519AgentConfig {
28 pub private_key: String,
30 pub network: Option<NetworkName>,
32 pub registry_address: Option<Address>,
34 pub rpc_url: Option<String>,
36}
37
38pub struct Ed25519Agent {
67 signing_key: SigningKey,
68 registry_address: Address,
69 rpc_url: String,
70 agent_key: B256,
71 address: Address,
72 http_client: Client,
73}
74
75impl Ed25519Agent {
76 pub fn new(config: Ed25519AgentConfig) -> Result<Self, crate::Error> {
78 let network_name = config.network.unwrap_or(DEFAULT_NETWORK);
79 let net = network_config(network_name);
80
81 let priv_hex = config
82 .private_key
83 .strip_prefix("0x")
84 .unwrap_or(&config.private_key);
85
86 let key_bytes = hex::decode(priv_hex).map_err(|_| crate::Error::InvalidPrivateKey)?;
87 let key_array: [u8; 32] = key_bytes
88 .try_into()
89 .map_err(|_| crate::Error::InvalidPrivateKey)?;
90
91 let signing_key = SigningKey::from_bytes(&key_array);
92 let verifying_key: VerifyingKey = signing_key.verifying_key();
93 let pubkey_bytes = verifying_key.to_bytes();
94
95 let agent_key = B256::from(pubkey_bytes);
97
98 let address = derive_address_from_pubkey(&pubkey_bytes);
100
101 Ok(Self {
102 signing_key,
103 registry_address: config.registry_address.unwrap_or(net.registry_address),
104 rpc_url: config.rpc_url.unwrap_or_else(|| net.rpc_url.to_string()),
105 agent_key,
106 address,
107 http_client: Client::new(),
108 })
109 }
110
111 pub fn address(&self) -> Address {
113 self.address
114 }
115
116 pub fn agent_key(&self) -> B256 {
118 self.agent_key
119 }
120
121 pub fn agent_key_hex(&self) -> String {
123 format!("0x{}", hex::encode(self.agent_key.as_slice()))
124 }
125
126 fn make_provider(
127 &self,
128 ) -> Result<impl alloy::providers::Provider + Clone, crate::Error> {
129 let url: reqwest::Url = self
130 .rpc_url
131 .parse()
132 .map_err(|_| crate::Error::InvalidRpcUrl)?;
133 Ok(ProviderBuilder::new().connect_http(url))
134 }
135
136 pub async fn is_registered(&self) -> Result<bool, crate::Error> {
138 let provider = self.make_provider()?;
139 let registry = IAgentRegistry::new(self.registry_address, provider);
140 let result = registry
141 .isVerifiedAgent(self.agent_key)
142 .call()
143 .await
144 .map_err(|e| crate::Error::RpcError(e.to_string()))?;
145 Ok(result)
146 }
147
148 pub async fn get_info(&self) -> Result<AgentInfo, crate::Error> {
150 let provider = self.make_provider()?;
151 let registry = IAgentRegistry::new(self.registry_address, provider);
152
153 let agent_id = registry
154 .getAgentId(self.agent_key)
155 .call()
156 .await
157 .map_err(|e| crate::Error::RpcError(e.to_string()))?;
158
159 if agent_id == U256::ZERO {
160 return Ok(AgentInfo {
161 address: self.address,
162 agent_key: self.agent_key,
163 agent_id: U256::ZERO,
164 is_verified: false,
165 nullifier: U256::ZERO,
166 agent_count: U256::ZERO,
167 });
168 }
169
170 let is_verified = registry
171 .hasHumanProof(agent_id)
172 .call()
173 .await
174 .map_err(|e| crate::Error::RpcError(e.to_string()))?;
175 let nullifier = registry
176 .getHumanNullifier(agent_id)
177 .call()
178 .await
179 .map_err(|e| crate::Error::RpcError(e.to_string()))?;
180 let agent_count = registry
181 .getAgentCountForHuman(nullifier)
182 .call()
183 .await
184 .map_err(|e| crate::Error::RpcError(e.to_string()))?;
185
186 Ok(AgentInfo {
187 address: self.address,
188 agent_key: self.agent_key,
189 agent_id,
190 is_verified,
191 nullifier,
192 agent_count,
193 })
194 }
195
196 pub fn sign_request(
201 &self,
202 method: &str,
203 url: &str,
204 body: Option<&str>,
205 ) -> HashMap<String, String> {
206 let timestamp = now_millis().to_string();
207 self.sign_request_with_timestamp(method, url, body, ×tamp)
208 }
209
210 pub fn sign_request_with_timestamp(
212 &self,
213 method: &str,
214 url: &str,
215 body: Option<&str>,
216 timestamp: &str,
217 ) -> HashMap<String, String> {
218 let message = compute_signing_message(timestamp, method, url, body);
219
220 let signature = self.signing_key.sign(message.as_ref());
222 let sig_hex = format!("0x{}", hex::encode(signature.to_bytes()));
223
224 let mut headers_map = HashMap::new();
225 headers_map.insert(
226 headers::KEY.to_string(),
227 self.agent_key_hex(),
228 );
229 headers_map.insert(
230 headers::KEYTYPE.to_string(),
231 "ed25519".to_string(),
232 );
233 headers_map.insert(headers::SIGNATURE.to_string(), sig_hex);
234 headers_map.insert(headers::TIMESTAMP.to_string(), timestamp.to_string());
235
236 headers_map
237 }
238
239 pub async fn fetch(
241 &self,
242 url: &str,
243 method: Option<Method>,
244 body: Option<String>,
245 ) -> Result<Response, crate::Error> {
246 let method = method.unwrap_or(Method::GET);
247 let method_str = method.as_str();
248 let body_ref = body.as_deref();
249
250 let auth_headers = self.sign_request(method_str, url, body_ref);
251
252 let mut request: RequestBuilder = self.http_client.request(method, url);
253 for (k, v) in &auth_headers {
254 request = request.header(k, v);
255 }
256 if let Some(b) = body {
257 request = request.header("content-type", "application/json");
258 request = request.body(b);
259 }
260
261 request
262 .send()
263 .await
264 .map_err(|e| crate::Error::HttpError(e.to_string()))
265 }
266}
267
268pub fn derive_address_from_pubkey(pubkey: &[u8; 32]) -> Address {
273 Address::from_slice(&keccak256(pubkey)[12..])
274}
275
276fn now_millis() -> u64 {
278 SystemTime::now()
279 .duration_since(UNIX_EPOCH)
280 .unwrap()
281 .as_millis() as u64
282}
283
284#[cfg(test)]
285mod tests {
286 use super::*;
287
288 #[test]
289 fn ed25519_agent_creation() {
290 let config = Ed25519AgentConfig {
292 private_key: "0x9d61b19deffd5a60ba844af492ec2cc44449c5697b326919703bac031cae7f60"
293 .to_string(),
294 network: Some(NetworkName::Testnet),
295 registry_address: None,
296 rpc_url: None,
297 };
298
299 let agent = Ed25519Agent::new(config).unwrap();
300
301 let key_hex = agent.agent_key_hex();
303 assert!(key_hex.starts_with("0x"));
304 assert_eq!(key_hex.len(), 66); assert_ne!(agent.address(), Address::ZERO);
308 }
309
310 #[test]
311 fn ed25519_sign_request_headers() {
312 let config = Ed25519AgentConfig {
313 private_key: "0x9d61b19deffd5a60ba844af492ec2cc44449c5697b326919703bac031cae7f60"
314 .to_string(),
315 network: Some(NetworkName::Testnet),
316 registry_address: None,
317 rpc_url: None,
318 };
319
320 let agent = Ed25519Agent::new(config).unwrap();
321 let headers = agent.sign_request_with_timestamp("GET", "/api/test", None, "1700000000000");
322
323 assert!(headers.contains_key(headers::KEY));
324 assert_eq!(headers.get(headers::KEYTYPE).unwrap(), "ed25519");
325 assert!(headers.get(headers::SIGNATURE).unwrap().starts_with("0x"));
326 assert_eq!(headers.get(headers::TIMESTAMP).unwrap(), "1700000000000");
327
328 let sig = headers.get(headers::SIGNATURE).unwrap();
330 assert_eq!(sig.len(), 130); }
332
333 #[test]
334 fn ed25519_sign_request_deterministic() {
335 let config = Ed25519AgentConfig {
336 private_key: "0x9d61b19deffd5a60ba844af492ec2cc44449c5697b326919703bac031cae7f60"
337 .to_string(),
338 network: Some(NetworkName::Testnet),
339 registry_address: None,
340 rpc_url: None,
341 };
342
343 let agent = Ed25519Agent::new(config).unwrap();
344
345 let h1 = agent.sign_request_with_timestamp("POST", "/api/data", Some(r#"{"test":true}"#), "1700000000000");
346 let h2 = agent.sign_request_with_timestamp("POST", "/api/data", Some(r#"{"test":true}"#), "1700000000000");
347
348 assert_eq!(
350 h1.get(headers::SIGNATURE),
351 h2.get(headers::SIGNATURE),
352 );
353 }
354
355 #[test]
356 fn ed25519_derive_address() {
357 let config = Ed25519AgentConfig {
358 private_key: "0x9d61b19deffd5a60ba844af492ec2cc44449c5697b326919703bac031cae7f60"
359 .to_string(),
360 network: Some(NetworkName::Testnet),
361 registry_address: None,
362 rpc_url: None,
363 };
364
365 let agent = Ed25519Agent::new(config).unwrap();
366
367 let pubkey_bytes: [u8; 32] = agent.agent_key().0;
369 let derived = derive_address_from_pubkey(&pubkey_bytes);
370 assert_eq!(derived, agent.address());
371 }
372
373 #[test]
374 fn ed25519_invalid_key_length() {
375 let config = Ed25519AgentConfig {
376 private_key: "0xdeadbeef".to_string(), network: None,
378 registry_address: None,
379 rpc_url: None,
380 };
381 assert!(Ed25519Agent::new(config).is_err());
382 }
383
384 #[test]
385 fn ed25519_invalid_key_hex() {
386 let config = Ed25519AgentConfig {
387 private_key: "not-hex-at-all".to_string(),
388 network: None,
389 registry_address: None,
390 rpc_url: None,
391 };
392 assert!(Ed25519Agent::new(config).is_err());
393 }
394
395 #[test]
396 fn ed25519_sign_verify_roundtrip() {
397 use ed25519_dalek::Verifier;
398
399 let config = Ed25519AgentConfig {
400 private_key: "0x9d61b19deffd5a60ba844af492ec2cc44449c5697b326919703bac031cae7f60"
401 .to_string(),
402 network: Some(NetworkName::Testnet),
403 registry_address: None,
404 rpc_url: None,
405 };
406
407 let agent = Ed25519Agent::new(config).unwrap();
408 let headers = agent.sign_request_with_timestamp("GET", "/api/test", None, "1700000000000");
409
410 let message = compute_signing_message("1700000000000", "GET", "/api/test", None);
412
413 let sig_hex = headers.get(headers::SIGNATURE).unwrap();
415 let sig_bytes = hex::decode(sig_hex.strip_prefix("0x").unwrap()).unwrap();
416 let signature = ed25519_dalek::Signature::from_bytes(&sig_bytes.try_into().unwrap());
417
418 let key_hex = headers.get(headers::KEY).unwrap();
420 let key_bytes = hex::decode(key_hex.strip_prefix("0x").unwrap()).unwrap();
421 let pubkey = VerifyingKey::from_bytes(&key_bytes.try_into().unwrap()).unwrap();
422
423 assert!(pubkey.verify(message.as_ref(), &signature).is_ok());
425 }
426}