1use alloy::primitives::{keccak256, Address, B256, FixedBytes, U256};
6use alloy::providers::ProviderBuilder;
7use alloy::signers::local::PrivateKeySigner;
8use alloy::signers::Signer;
9use reqwest::{Client, Method, RequestBuilder, Response};
10use std::collections::HashMap;
11use std::time::{SystemTime, UNIX_EPOCH};
12
13use crate::agent_card::{
14 Erc8004AgentDocument, AgentSkill, CardCredentials, SelfProtocolExtension, TrustModel,
15 Erc8004Service, AgentInterface,
16 get_provider_label,
17};
18use crate::constants::{
19 headers, network_config, IAgentRegistry, IHumanProofProvider, NetworkName, DEFAULT_NETWORK,
20};
21use crate::registration_flow::{
22 DeregistrationRequest, DeregistrationSession, RegistrationError, RegistrationRequest,
23 RegistrationSession, DEFAULT_API_BASE,
24};
25
26#[derive(Debug, Clone)]
28pub struct SelfAgentConfig {
29 pub private_key: String,
31 pub network: Option<NetworkName>,
33 pub registry_address: Option<Address>,
35 pub rpc_url: Option<String>,
37}
38
39#[derive(Debug, Clone)]
41pub struct AgentInfo {
42 pub address: Address,
43 pub agent_key: B256,
44 pub agent_id: U256,
45 pub is_verified: bool,
46 pub nullifier: U256,
47 pub agent_count: U256,
48}
49
50pub struct SelfAgent {
55 signer: PrivateKeySigner,
56 private_key: String,
57 network_name: NetworkName,
58 registry_address: Address,
59 rpc_url: String,
60 agent_key: B256,
61 http_client: Client,
62}
63
64impl SelfAgent {
65 pub fn new(config: SelfAgentConfig) -> Result<Self, crate::Error> {
67 let network_name = config.network.unwrap_or(DEFAULT_NETWORK);
68 let net = network_config(network_name);
69 let signer: PrivateKeySigner = config
70 .private_key
71 .parse()
72 .map_err(|_| crate::Error::InvalidPrivateKey)?;
73 let agent_key = address_to_agent_key(signer.address());
74
75 Ok(Self {
76 signer,
77 private_key: config.private_key,
78 network_name,
79 registry_address: config.registry_address.unwrap_or(net.registry_address),
80 rpc_url: config.rpc_url.unwrap_or_else(|| net.rpc_url.to_string()),
81 agent_key,
82 http_client: Client::new(),
83 })
84 }
85
86 pub fn address(&self) -> Address {
88 self.signer.address()
89 }
90
91 pub fn agent_key(&self) -> B256 {
93 self.agent_key
94 }
95
96 fn make_provider(
97 &self,
98 ) -> Result<impl alloy::providers::Provider + Clone, crate::Error> {
99 let url: reqwest::Url = self
100 .rpc_url
101 .parse()
102 .map_err(|_| crate::Error::InvalidRpcUrl)?;
103 Ok(ProviderBuilder::new().connect_http(url))
104 }
105
106 fn make_signer_provider(
108 &self,
109 ) -> Result<impl alloy::providers::Provider + Clone, crate::Error> {
110 let url: reqwest::Url = self
111 .rpc_url
112 .parse()
113 .map_err(|_| crate::Error::InvalidRpcUrl)?;
114 let wallet = alloy::network::EthereumWallet::from(self.signer.clone());
115 Ok(ProviderBuilder::new().wallet(wallet).connect_http(url))
116 }
117
118 pub async fn is_registered(&self) -> Result<bool, crate::Error> {
120 let provider = self.make_provider()?;
121 let registry = IAgentRegistry::new(self.registry_address, provider);
122 let result = registry
123 .isVerifiedAgent(self.agent_key)
124 .call()
125 .await
126 .map_err(|e| crate::Error::RpcError(e.to_string()))?;
127 Ok(result)
128 }
129
130 pub async fn get_info(&self) -> Result<AgentInfo, crate::Error> {
132 let provider = self.make_provider()?;
133 let registry = IAgentRegistry::new(self.registry_address, provider);
134
135 let agent_id = registry
136 .getAgentId(self.agent_key)
137 .call()
138 .await
139 .map_err(|e| crate::Error::RpcError(e.to_string()))?;
140
141 if agent_id == U256::ZERO {
142 return Ok(AgentInfo {
143 address: self.signer.address(),
144 agent_key: self.agent_key,
145 agent_id: U256::ZERO,
146 is_verified: false,
147 nullifier: U256::ZERO,
148 agent_count: U256::ZERO,
149 });
150 }
151
152 let is_verified = registry
153 .hasHumanProof(agent_id)
154 .call()
155 .await
156 .map_err(|e| crate::Error::RpcError(e.to_string()))?;
157 let nullifier = registry
158 .getHumanNullifier(agent_id)
159 .call()
160 .await
161 .map_err(|e| crate::Error::RpcError(e.to_string()))?;
162 let agent_count = registry
163 .getAgentCountForHuman(nullifier)
164 .call()
165 .await
166 .map_err(|e| crate::Error::RpcError(e.to_string()))?;
167
168 Ok(AgentInfo {
169 address: self.signer.address(),
170 agent_key: self.agent_key,
171 agent_id,
172 is_verified,
173 nullifier,
174 agent_count,
175 })
176 }
177
178 pub async fn sign_request(
182 &self,
183 method: &str,
184 url: &str,
185 body: Option<&str>,
186 ) -> Result<HashMap<String, String>, crate::Error> {
187 let timestamp = now_millis().to_string();
188 self.sign_request_with_timestamp(method, url, body, ×tamp)
189 .await
190 }
191
192 pub async fn sign_request_with_timestamp(
194 &self,
195 method: &str,
196 url: &str,
197 body: Option<&str>,
198 timestamp: &str,
199 ) -> Result<HashMap<String, String>, crate::Error> {
200 let message = compute_signing_message(timestamp, method, url, body);
201
202 let signature = self
204 .signer
205 .sign_message(message.as_ref())
206 .await
207 .map_err(|e| crate::Error::SigningError(e.to_string()))?;
208
209 let sig_hex = format!("0x{}", hex::encode(signature.as_bytes()));
210
211 let mut headers_map = HashMap::new();
212 headers_map.insert(
213 headers::ADDRESS.to_string(),
214 format!("{:#x}", self.signer.address()),
215 );
216 headers_map.insert(headers::SIGNATURE.to_string(), sig_hex);
217 headers_map.insert(headers::TIMESTAMP.to_string(), timestamp.to_string());
218
219 Ok(headers_map)
220 }
221
222 pub async fn fetch(
224 &self,
225 url: &str,
226 method: Option<Method>,
227 body: Option<String>,
228 ) -> Result<Response, crate::Error> {
229 let method = method.unwrap_or(Method::GET);
230 let method_str = method.as_str();
231 let body_ref = body.as_deref();
232
233 let auth_headers = self.sign_request(method_str, url, body_ref).await?;
234
235 let mut request: RequestBuilder = self.http_client.request(method, url);
236 for (k, v) in &auth_headers {
237 request = request.header(k, v);
238 }
239 if let Some(b) = body {
240 request = request.header("content-type", "application/json");
241 request = request.body(b);
242 }
243
244 request
245 .send()
246 .await
247 .map_err(|e| crate::Error::HttpError(e.to_string()))
248 }
249
250 pub async fn get_agent_card(&self) -> Result<Option<Erc8004AgentDocument>, crate::Error> {
255 let provider = self.make_provider()?;
256 let registry = IAgentRegistry::new(self.registry_address, provider);
257
258 let agent_id = registry
259 .getAgentId(self.agent_key)
260 .call()
261 .await
262 .map_err(|e| crate::Error::RpcError(e.to_string()))?;
263 if agent_id == U256::ZERO {
264 return Ok(None);
265 }
266
267 let raw = registry
268 .getAgentMetadata(agent_id)
269 .call()
270 .await
271 .map_err(|e| crate::Error::RpcError(e.to_string()))?;
272 if raw.is_empty() {
273 return Ok(None);
274 }
275
276 match serde_json::from_str::<Erc8004AgentDocument>(&raw) {
277 Ok(card) => Ok(Some(card)),
278 _ => Ok(None),
279 }
280 }
281
282 pub async fn set_agent_card(
285 &self,
286 name: String,
287 description: Option<String>,
288 url: Option<String>,
289 skills: Option<Vec<AgentSkill>>,
290 ) -> Result<B256, crate::Error> {
291 let provider = self.make_signer_provider()?;
292 let registry = IAgentRegistry::new(self.registry_address, &provider);
293
294 let agent_id = registry
295 .getAgentId(self.agent_key)
296 .call()
297 .await
298 .map_err(|e| crate::Error::RpcError(e.to_string()))?;
299 if agent_id == U256::ZERO {
300 return Err(crate::Error::RpcError("Agent not registered".into()));
301 }
302
303 let proof_provider_addr = registry
304 .getProofProvider(agent_id)
305 .call()
306 .await
307 .map_err(|e| crate::Error::RpcError(e.to_string()))?;
308 if proof_provider_addr == Address::ZERO {
309 return Err(crate::Error::RpcError(
310 "Agent has no proof provider — cannot build card".into(),
311 ));
312 }
313
314 let proof_provider =
315 IHumanProofProvider::new(proof_provider_addr, &provider);
316
317 let provider_name = proof_provider
318 .providerName()
319 .call()
320 .await
321 .map_err(|e| crate::Error::RpcError(e.to_string()))?;
322 let strength = proof_provider
323 .verificationStrength()
324 .call()
325 .await
326 .map_err(|e| crate::Error::RpcError(e.to_string()))?;
327
328 let credentials = registry
329 .getAgentCredentials(agent_id)
330 .call()
331 .await
332 .ok();
333
334 let proof_type = get_provider_label(strength).to_string();
335
336 let mut trust_model = TrustModel {
337 proof_type,
338 sybil_resistant: true,
339 ofac_screened: false,
340 minimum_age_verified: 0,
341 };
342
343 let card_credentials = credentials.map(|creds| {
344 let older_than = creds.olderThan.try_into().unwrap_or(0u64);
345 let ofac_screened = creds.ofac.first().copied().unwrap_or(false);
346 trust_model.ofac_screened = ofac_screened;
347 trust_model.minimum_age_verified = older_than;
348
349 CardCredentials {
350 nationality: non_empty(&creds.nationality),
351 issuing_state: non_empty(&creds.issuingState),
352 older_than: if older_than > 0 { Some(older_than) } else { None },
353 ofac_clean: if ofac_screened { Some(true) } else { None },
354 has_name: if !creds.name.is_empty() { Some(true) } else { None },
355 has_date_of_birth: non_empty(&creds.dateOfBirth).map(|_| true),
356 has_gender: non_empty(&creds.gender).map(|_| true),
357 document_expiry: non_empty(&creds.expiryDate),
358 }
359 });
360
361 let chain_id: u64 = alloy::providers::Provider::get_chain_id(&provider)
362 .await
363 .map_err(|e: alloy::transports::RpcError<alloy::transports::TransportErrorKind>| crate::Error::RpcError(e.to_string()))?;
364
365 let mut services = Vec::new();
367 let mut supported_interfaces = None;
368 if let Some(ref agent_url) = url {
369 services.push(Erc8004Service {
370 name: "A2A".to_string(),
371 endpoint: agent_url.clone(),
372 version: Some("0.3.0".to_string()),
373 });
374 supported_interfaces = Some(vec![AgentInterface {
375 url: agent_url.clone(),
376 protocol_binding: "JSONRPC".to_string(),
377 protocol_version: "0.3.0".to_string(),
378 }]);
379 }
380
381 let card = Erc8004AgentDocument {
382 doc_type: "https://eips.ethereum.org/EIPS/eip-8004#registration-v1".to_string(),
383 name,
384 description: description.unwrap_or_default(),
385 image: String::new(),
386 services,
387 active: None,
388 registrations: None,
389 supported_trust: None,
390 version: None,
391 url,
392 provider: None,
393 capabilities: None,
394 security_schemes: None,
395 security: None,
396 default_input_modes: None,
397 default_output_modes: None,
398 supported_interfaces,
399 icon_url: None,
400 documentation_url: None,
401 signatures: None,
402 extensions: None,
403 self_protocol: Some(SelfProtocolExtension {
404 agent_id: agent_id.try_into().unwrap_or(0),
405 registry: format!("{:#x}", self.registry_address),
406 chain_id,
407 proof_provider: format!("{:#x}", proof_provider_addr),
408 provider_name,
409 verification_strength: strength,
410 trust_model,
411 credentials: card_credentials,
412 }),
413 skills,
414 };
415
416 let json =
417 serde_json::to_string(&card).map_err(|e| crate::Error::RpcError(e.to_string()))?;
418
419 let tx_hash = registry
420 .updateAgentMetadata(agent_id, json)
421 .send()
422 .await
423 .map_err(|e| crate::Error::RpcError(e.to_string()))?
424 .watch()
425 .await
426 .map_err(|e| crate::Error::RpcError(e.to_string()))?;
427
428 Ok(tx_hash)
429 }
430
431 pub async fn to_agent_card_data_uri(&self) -> Result<String, crate::Error> {
433 let card = self
434 .get_agent_card()
435 .await?
436 .ok_or_else(|| crate::Error::RpcError("No A2A Agent Card set".into()))?;
437 let json =
438 serde_json::to_string(&card).map_err(|e| crate::Error::RpcError(e.to_string()))?;
439 use base64::Engine;
440 let encoded = base64::engine::general_purpose::STANDARD.encode(json.as_bytes());
441 Ok(format!("data:application/json;base64,{}", encoded))
442 }
443
444 pub async fn get_credentials(
446 &self,
447 ) -> Result<Option<IAgentRegistry::AgentCredentials>, crate::Error> {
448 let provider = self.make_provider()?;
449 let registry = IAgentRegistry::new(self.registry_address, provider);
450
451 let agent_id = registry
452 .getAgentId(self.agent_key)
453 .call()
454 .await
455 .map_err(|e| crate::Error::RpcError(e.to_string()))?;
456 if agent_id == U256::ZERO {
457 return Ok(None);
458 }
459
460 let creds = registry
461 .getAgentCredentials(agent_id)
462 .call()
463 .await
464 .map_err(|e| crate::Error::RpcError(e.to_string()))?;
465 Ok(Some(creds))
466 }
467
468 pub async fn get_verification_strength(&self) -> Result<u8, crate::Error> {
470 let provider = self.make_provider()?;
471 let registry = IAgentRegistry::new(self.registry_address, &provider);
472
473 let agent_id = registry
474 .getAgentId(self.agent_key)
475 .call()
476 .await
477 .map_err(|e| crate::Error::RpcError(e.to_string()))?;
478 if agent_id == U256::ZERO {
479 return Ok(0);
480 }
481
482 let provider_addr = registry
483 .getProofProvider(agent_id)
484 .call()
485 .await
486 .map_err(|e| crate::Error::RpcError(e.to_string()))?;
487 if provider_addr == Address::ZERO {
488 return Ok(0);
489 }
490
491 let proof_provider = IHumanProofProvider::new(provider_addr, &provider);
492 let strength = proof_provider
493 .verificationStrength()
494 .call()
495 .await
496 .map_err(|e| crate::Error::RpcError(e.to_string()))?;
497 Ok(strength)
498 }
499
500 pub async fn request_registration(
507 req: RegistrationRequest,
508 api_base: Option<&str>,
509 ) -> Result<RegistrationSession, RegistrationError> {
510 RegistrationSession::request(req, api_base).await
511 }
512
513 pub async fn get_agent_info_rest(
515 agent_id: u64,
516 network: NetworkName,
517 api_base: Option<&str>,
518 ) -> Result<serde_json::Value, RegistrationError> {
519 let base = api_base.unwrap_or(DEFAULT_API_BASE);
520 let chain_id: u64 = match network {
521 NetworkName::Mainnet => 42220,
522 NetworkName::Testnet => 11142220,
523 };
524 let resp = reqwest::get(format!("{base}/api/agent/info/{chain_id}/{agent_id}"))
525 .await
526 .map_err(|e| RegistrationError::Http(e.to_string()))?;
527 resp.json()
528 .await
529 .map_err(|e| RegistrationError::Http(e.to_string()))
530 }
531
532 pub async fn get_agents_for_human(
534 address: &str,
535 network: NetworkName,
536 api_base: Option<&str>,
537 ) -> Result<serde_json::Value, RegistrationError> {
538 let base = api_base.unwrap_or(DEFAULT_API_BASE);
539 let chain_id: u64 = match network {
540 NetworkName::Mainnet => 42220,
541 NetworkName::Testnet => 11142220,
542 };
543 let resp = reqwest::get(format!("{base}/api/agent/agents/{chain_id}/{address}"))
544 .await
545 .map_err(|e| RegistrationError::Http(e.to_string()))?;
546 resp.json()
547 .await
548 .map_err(|e| RegistrationError::Http(e.to_string()))
549 }
550
551 pub async fn request_deregistration(
556 &self,
557 api_base: Option<&str>,
558 ) -> Result<DeregistrationSession, RegistrationError> {
559 let network_str = match self.network_name {
560 NetworkName::Mainnet => "mainnet",
561 NetworkName::Testnet => "testnet",
562 };
563 DeregistrationSession::request(
564 DeregistrationRequest {
565 network: network_str.to_string(),
566 agent_address: format!("{:#x}", self.signer.address()),
567 },
568 api_base,
569 )
570 .await
571 }
572}
573
574pub fn address_to_agent_key(address: Address) -> B256 {
577 let mut bytes = [0u8; 32];
578 bytes[12..32].copy_from_slice(address.as_ref());
579 FixedBytes(bytes)
580}
581
582fn now_millis() -> u64 {
584 SystemTime::now()
585 .duration_since(UNIX_EPOCH)
586 .unwrap()
587 .as_millis() as u64
588}
589
590fn non_empty(s: &str) -> Option<String> {
592 if s.is_empty() {
593 None
594 } else {
595 Some(s.to_string())
596 }
597}
598
599pub(crate) fn compute_signing_message(
602 timestamp: &str,
603 method: &str,
604 url: &str,
605 body: Option<&str>,
606) -> B256 {
607 let canonical_url = canonicalize_signing_url(url);
608 let body_text = body.unwrap_or("");
609 let body_hash = keccak256(body_text.as_bytes());
610 let body_hash_hex = format!("{:#x}", body_hash);
611 let concat = format!(
612 "{}{}{}{}",
613 timestamp,
614 method.to_uppercase(),
615 canonical_url,
616 body_hash_hex
617 );
618 keccak256(concat.as_bytes())
619}
620
621pub(crate) fn canonicalize_signing_url(url: &str) -> String {
623 if url.is_empty() {
624 return String::new();
625 }
626
627 if url.starts_with("http://") || url.starts_with("https://") {
628 if let Ok(parsed) = reqwest::Url::parse(url) {
629 let mut out = parsed.path().to_string();
630 if out.is_empty() {
631 out.push('/');
632 }
633 if let Some(query) = parsed.query() {
634 out.push('?');
635 out.push_str(query);
636 }
637 return out;
638 }
639 return url.to_string();
640 }
641
642 if url.starts_with('?') {
643 return format!("/{url}");
644 }
645 if url.starts_with('/') {
646 return url.to_string();
647 }
648
649 if let Ok(parsed) = reqwest::Url::parse(&format!("http://self.local/{url}")) {
651 let mut out = parsed.path().to_string();
652 if let Some(query) = parsed.query() {
653 out.push('?');
654 out.push_str(query);
655 }
656 return out;
657 }
658
659 url.to_string()
660}