Skip to main content

self_agent_sdk/
agent.rs

1// SPDX-FileCopyrightText: 2025-2026 Social Connect Labs, Inc.
2// SPDX-License-Identifier: BUSL-1.1
3// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE.
4
5use 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/// Configuration for creating a [`SelfAgent`].
27#[derive(Debug, Clone)]
28pub struct SelfAgentConfig {
29    /// Agent's private key (hex, with or without 0x prefix).
30    pub private_key: String,
31    /// Network to use: Mainnet (default) or Testnet.
32    pub network: Option<NetworkName>,
33    /// Override: custom registry address (takes precedence over network).
34    pub registry_address: Option<Address>,
35    /// Override: custom RPC URL (takes precedence over network).
36    pub rpc_url: Option<String>,
37}
38
39/// Full agent info from the registry.
40#[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
50/// Agent-side SDK for Self Agent ID.
51///
52/// The agent's on-chain identity is its Ethereum address, zero-padded to bytes32.
53/// For off-chain authentication, the agent signs each request with its private key.
54pub struct SelfAgent {
55    signer: PrivateKeySigner,
56    network_name: NetworkName,
57    registry_address: Address,
58    rpc_url: String,
59    agent_key: B256,
60    http_client: Client,
61}
62
63impl SelfAgent {
64    /// Create a new agent instance.
65    pub fn new(config: SelfAgentConfig) -> Result<Self, crate::Error> {
66        let network_name = config.network.unwrap_or(DEFAULT_NETWORK);
67        let net = network_config(network_name);
68        let signer: PrivateKeySigner = config
69            .private_key
70            .parse()
71            .map_err(|_| crate::Error::InvalidPrivateKey)?;
72        let agent_key = address_to_agent_key(signer.address());
73
74        Ok(Self {
75            signer,
76            network_name,
77            registry_address: config.registry_address.unwrap_or(net.registry_address),
78            rpc_url: config.rpc_url.unwrap_or_else(|| net.rpc_url.to_string()),
79            agent_key,
80            http_client: Client::new(),
81        })
82    }
83
84    /// The agent's Ethereum address.
85    pub fn address(&self) -> Address {
86        self.signer.address()
87    }
88
89    /// The agent's on-chain key (bytes32) — zero-padded address.
90    pub fn agent_key(&self) -> B256 {
91        self.agent_key
92    }
93
94    fn make_provider(
95        &self,
96    ) -> Result<impl alloy::providers::Provider + Clone, crate::Error> {
97        let url: reqwest::Url = self
98            .rpc_url
99            .parse()
100            .map_err(|_| crate::Error::InvalidRpcUrl)?;
101        Ok(ProviderBuilder::new().connect_http(url))
102    }
103
104    /// Create a provider with wallet attached — required for write operations.
105    fn make_signer_provider(
106        &self,
107    ) -> Result<impl alloy::providers::Provider + Clone, crate::Error> {
108        let url: reqwest::Url = self
109            .rpc_url
110            .parse()
111            .map_err(|_| crate::Error::InvalidRpcUrl)?;
112        let wallet = alloy::network::EthereumWallet::from(self.signer.clone());
113        Ok(ProviderBuilder::new().wallet(wallet).connect_http(url))
114    }
115
116    /// Check if this agent is registered and verified on-chain.
117    pub async fn is_registered(&self) -> Result<bool, crate::Error> {
118        let provider = self.make_provider()?;
119        let registry = IAgentRegistry::new(self.registry_address, provider);
120        let result = registry
121            .isVerifiedAgent(self.agent_key)
122            .call()
123            .await
124            .map_err(|e| crate::Error::RpcError(e.to_string()))?;
125        Ok(result)
126    }
127
128    /// Get full agent info from the registry.
129    pub async fn get_info(&self) -> Result<AgentInfo, crate::Error> {
130        let provider = self.make_provider()?;
131        let registry = IAgentRegistry::new(self.registry_address, provider);
132
133        let agent_id = registry
134            .getAgentId(self.agent_key)
135            .call()
136            .await
137            .map_err(|e| crate::Error::RpcError(e.to_string()))?;
138
139        if agent_id == U256::ZERO {
140            return Ok(AgentInfo {
141                address: self.signer.address(),
142                agent_key: self.agent_key,
143                agent_id: U256::ZERO,
144                is_verified: false,
145                nullifier: U256::ZERO,
146                agent_count: U256::ZERO,
147            });
148        }
149
150        let is_verified = registry
151            .hasHumanProof(agent_id)
152            .call()
153            .await
154            .map_err(|e| crate::Error::RpcError(e.to_string()))?;
155        let nullifier = registry
156            .getHumanNullifier(agent_id)
157            .call()
158            .await
159            .map_err(|e| crate::Error::RpcError(e.to_string()))?;
160        let agent_count = registry
161            .getAgentCountForHuman(nullifier)
162            .call()
163            .await
164            .map_err(|e| crate::Error::RpcError(e.to_string()))?;
165
166        Ok(AgentInfo {
167            address: self.signer.address(),
168            agent_key: self.agent_key,
169            agent_id,
170            is_verified,
171            nullifier,
172            agent_count,
173        })
174    }
175
176    /// Generate authentication headers for a request.
177    ///
178    /// Signature covers: `keccak256(timestamp + METHOD + canonicalPathAndQuery + bodyHash)`
179    pub async fn sign_request(
180        &self,
181        method: &str,
182        url: &str,
183        body: Option<&str>,
184    ) -> Result<HashMap<String, String>, crate::Error> {
185        let timestamp = now_millis().to_string();
186        self.sign_request_with_timestamp(method, url, body, &timestamp)
187            .await
188    }
189
190    /// Sign a request with a specific timestamp (useful for testing).
191    pub async fn sign_request_with_timestamp(
192        &self,
193        method: &str,
194        url: &str,
195        body: Option<&str>,
196        timestamp: &str,
197    ) -> Result<HashMap<String, String>, crate::Error> {
198        let message = compute_signing_message(timestamp, method, url, body);
199
200        // EIP-191 personal_sign over the raw 32 bytes
201        let signature = self
202            .signer
203            .sign_message(message.as_ref())
204            .await
205            .map_err(|e| crate::Error::SigningError(e.to_string()))?;
206
207        let sig_hex = format!("0x{}", hex::encode(signature.as_bytes()));
208
209        let mut headers_map = HashMap::new();
210        headers_map.insert(
211            headers::ADDRESS.to_string(),
212            format!("{:#x}", self.signer.address()),
213        );
214        headers_map.insert(headers::SIGNATURE.to_string(), sig_hex);
215        headers_map.insert(headers::TIMESTAMP.to_string(), timestamp.to_string());
216
217        Ok(headers_map)
218    }
219
220    /// Wrapper around reqwest that automatically adds agent signature headers.
221    pub async fn fetch(
222        &self,
223        url: &str,
224        method: Option<Method>,
225        body: Option<String>,
226    ) -> Result<Response, crate::Error> {
227        let method = method.unwrap_or(Method::GET);
228        let method_str = method.as_str();
229        let body_ref = body.as_deref();
230
231        let auth_headers = self.sign_request(method_str, url, body_ref).await?;
232
233        let mut request: RequestBuilder = self.http_client.request(method, url);
234        for (k, v) in &auth_headers {
235            request = request.header(k, v);
236        }
237        if let Some(b) = body {
238            request = request.header("content-type", "application/json");
239            request = request.body(b);
240        }
241
242        request
243            .send()
244            .await
245            .map_err(|e| crate::Error::HttpError(e.to_string()))
246    }
247
248    // ─── A2A Agent Card Methods ────────────────────────────────────────────
249
250    /// Read the agent card from on-chain metadata (if set).
251    /// Supports both the new ERC-8004 format and legacy A2A v0.1 cards.
252    pub async fn get_agent_card(&self) -> Result<Option<Erc8004AgentDocument>, crate::Error> {
253        let provider = self.make_provider()?;
254        let registry = IAgentRegistry::new(self.registry_address, provider);
255
256        let agent_id = registry
257            .getAgentId(self.agent_key)
258            .call()
259            .await
260            .map_err(|e| crate::Error::RpcError(e.to_string()))?;
261        if agent_id == U256::ZERO {
262            return Ok(None);
263        }
264
265        let raw = registry
266            .getAgentMetadata(agent_id)
267            .call()
268            .await
269            .map_err(|e| crate::Error::RpcError(e.to_string()))?;
270        if raw.is_empty() {
271            return Ok(None);
272        }
273
274        match serde_json::from_str::<Erc8004AgentDocument>(&raw) {
275            Ok(card) => Ok(Some(card)),
276            _ => Ok(None),
277        }
278    }
279
280    /// Build and write an A2A Agent Card to on-chain metadata.
281    /// Returns the transaction hash.
282    pub async fn set_agent_card(
283        &self,
284        name: String,
285        description: Option<String>,
286        url: Option<String>,
287        skills: Option<Vec<AgentSkill>>,
288    ) -> Result<B256, crate::Error> {
289        let provider = self.make_signer_provider()?;
290        let registry = IAgentRegistry::new(self.registry_address, &provider);
291
292        let agent_id = registry
293            .getAgentId(self.agent_key)
294            .call()
295            .await
296            .map_err(|e| crate::Error::RpcError(e.to_string()))?;
297        if agent_id == U256::ZERO {
298            return Err(crate::Error::RpcError("Agent not registered".into()));
299        }
300
301        let proof_provider_addr = registry
302            .getProofProvider(agent_id)
303            .call()
304            .await
305            .map_err(|e| crate::Error::RpcError(e.to_string()))?;
306        if proof_provider_addr == Address::ZERO {
307            return Err(crate::Error::RpcError(
308                "Agent has no proof provider — cannot build card".into(),
309            ));
310        }
311
312        let proof_provider =
313            IHumanProofProvider::new(proof_provider_addr, &provider);
314
315        let provider_name = proof_provider
316            .providerName()
317            .call()
318            .await
319            .map_err(|e| crate::Error::RpcError(e.to_string()))?;
320        let strength = proof_provider
321            .verificationStrength()
322            .call()
323            .await
324            .map_err(|e| crate::Error::RpcError(e.to_string()))?;
325
326        let credentials = registry
327            .getAgentCredentials(agent_id)
328            .call()
329            .await
330            .ok();
331
332        let proof_type = get_provider_label(strength).to_string();
333
334        let mut trust_model = TrustModel {
335            proof_type,
336            sybil_resistant: true,
337            ofac_screened: false,
338            minimum_age_verified: 0,
339        };
340
341        let card_credentials = credentials.map(|creds| {
342            let older_than = creds.olderThan.try_into().unwrap_or(0u64);
343            let ofac_screened = creds.ofac.first().copied().unwrap_or(false);
344            trust_model.ofac_screened = ofac_screened;
345            trust_model.minimum_age_verified = older_than;
346
347            CardCredentials {
348                nationality: non_empty(&creds.nationality),
349                issuing_state: non_empty(&creds.issuingState),
350                older_than: if older_than > 0 { Some(older_than) } else { None },
351                ofac_clean: if ofac_screened { Some(true) } else { None },
352                has_name: if !creds.name.is_empty() { Some(true) } else { None },
353                has_date_of_birth: non_empty(&creds.dateOfBirth).map(|_| true),
354                has_gender: non_empty(&creds.gender).map(|_| true),
355                document_expiry: non_empty(&creds.expiryDate),
356            }
357        });
358
359        let chain_id: u64 = alloy::providers::Provider::get_chain_id(&provider)
360            .await
361            .map_err(|e: alloy::transports::RpcError<alloy::transports::TransportErrorKind>| crate::Error::RpcError(e.to_string()))?;
362
363        // Build services and supportedInterfaces if a URL is provided
364        let mut services = Vec::new();
365        let mut supported_interfaces = None;
366        if let Some(ref agent_url) = url {
367            services.push(Erc8004Service {
368                name: "A2A".to_string(),
369                endpoint: agent_url.clone(),
370                version: Some("0.3.0".to_string()),
371            });
372            supported_interfaces = Some(vec![AgentInterface {
373                url: agent_url.clone(),
374                protocol_binding: "JSONRPC".to_string(),
375                protocol_version: "0.3.0".to_string(),
376            }]);
377        }
378
379        let card = Erc8004AgentDocument {
380            doc_type: "https://eips.ethereum.org/EIPS/eip-8004#registration-v1".to_string(),
381            name,
382            description: description.unwrap_or_default(),
383            image: String::new(),
384            services,
385            active: None,
386            registrations: None,
387            supported_trust: None,
388            version: None,
389            url,
390            provider: None,
391            capabilities: None,
392            security_schemes: None,
393            security: None,
394            default_input_modes: None,
395            default_output_modes: None,
396            supported_interfaces,
397            icon_url: None,
398            documentation_url: None,
399            signatures: None,
400            extensions: None,
401            self_protocol: Some(SelfProtocolExtension {
402                agent_id: agent_id.try_into().unwrap_or(0),
403                registry: format!("{:#x}", self.registry_address),
404                chain_id,
405                proof_provider: format!("{:#x}", proof_provider_addr),
406                provider_name,
407                verification_strength: strength,
408                trust_model,
409                credentials: card_credentials,
410            }),
411            skills,
412        };
413
414        let json =
415            serde_json::to_string(&card).map_err(|e| crate::Error::RpcError(e.to_string()))?;
416
417        let tx_hash = registry
418            .updateAgentMetadata(agent_id, json)
419            .send()
420            .await
421            .map_err(|e| crate::Error::RpcError(e.to_string()))?
422            .watch()
423            .await
424            .map_err(|e| crate::Error::RpcError(e.to_string()))?;
425
426        Ok(tx_hash)
427    }
428
429    /// Returns a `data:` URI containing the base64-encoded Agent Card JSON.
430    pub async fn to_agent_card_data_uri(&self) -> Result<String, crate::Error> {
431        let card = self
432            .get_agent_card()
433            .await?
434            .ok_or_else(|| crate::Error::RpcError("No A2A Agent Card set".into()))?;
435        let json =
436            serde_json::to_string(&card).map_err(|e| crate::Error::RpcError(e.to_string()))?;
437        use base64::Engine;
438        let encoded = base64::engine::general_purpose::STANDARD.encode(json.as_bytes());
439        Ok(format!("data:application/json;base64,{}", encoded))
440    }
441
442    /// Read ZK-attested credentials for this agent from on-chain.
443    pub async fn get_credentials(
444        &self,
445    ) -> Result<Option<IAgentRegistry::AgentCredentials>, crate::Error> {
446        let provider = self.make_provider()?;
447        let registry = IAgentRegistry::new(self.registry_address, provider);
448
449        let agent_id = registry
450            .getAgentId(self.agent_key)
451            .call()
452            .await
453            .map_err(|e| crate::Error::RpcError(e.to_string()))?;
454        if agent_id == U256::ZERO {
455            return Ok(None);
456        }
457
458        let creds = registry
459            .getAgentCredentials(agent_id)
460            .call()
461            .await
462            .map_err(|e| crate::Error::RpcError(e.to_string()))?;
463        Ok(Some(creds))
464    }
465
466    /// Read the verification strength score from the provider contract.
467    pub async fn get_verification_strength(&self) -> Result<u8, crate::Error> {
468        let provider = self.make_provider()?;
469        let registry = IAgentRegistry::new(self.registry_address, &provider);
470
471        let agent_id = registry
472            .getAgentId(self.agent_key)
473            .call()
474            .await
475            .map_err(|e| crate::Error::RpcError(e.to_string()))?;
476        if agent_id == U256::ZERO {
477            return Ok(0);
478        }
479
480        let provider_addr = registry
481            .getProofProvider(agent_id)
482            .call()
483            .await
484            .map_err(|e| crate::Error::RpcError(e.to_string()))?;
485        if provider_addr == Address::ZERO {
486            return Ok(0);
487        }
488
489        let proof_provider = IHumanProofProvider::new(provider_addr, &provider);
490        let strength = proof_provider
491            .verificationStrength()
492            .call()
493            .await
494            .map_err(|e| crate::Error::RpcError(e.to_string()))?;
495        Ok(strength)
496    }
497
498    // ─── Registration / Deregistration (REST API) ────────────────────────
499
500    /// Initiate agent registration via the REST API.
501    ///
502    /// Returns a [`RegistrationSession`] with a QR code URL and deep link
503    /// that the human operator must scan with the Self app.
504    pub async fn request_registration(
505        req: RegistrationRequest,
506        api_base: Option<&str>,
507    ) -> Result<RegistrationSession, RegistrationError> {
508        RegistrationSession::request(req, api_base).await
509    }
510
511    /// Query agent info via the REST API (no private key needed).
512    pub async fn get_agent_info_rest(
513        agent_id: u64,
514        network: NetworkName,
515        api_base: Option<&str>,
516    ) -> Result<serde_json::Value, RegistrationError> {
517        let base = api_base.unwrap_or(DEFAULT_API_BASE);
518        let chain_id: u64 = match network {
519            NetworkName::Mainnet => 42220,
520            NetworkName::Testnet => 11142220,
521        };
522        let resp = reqwest::get(format!("{base}/api/agent/info/{chain_id}/{agent_id}"))
523            .await
524            .map_err(|e| RegistrationError::Http(e.to_string()))?;
525        resp.json()
526            .await
527            .map_err(|e| RegistrationError::Http(e.to_string()))
528    }
529
530    /// Query all agents registered to a human address via the REST API.
531    pub async fn get_agents_for_human(
532        address: &str,
533        network: NetworkName,
534        api_base: Option<&str>,
535    ) -> Result<serde_json::Value, RegistrationError> {
536        let base = api_base.unwrap_or(DEFAULT_API_BASE);
537        let chain_id: u64 = match network {
538            NetworkName::Mainnet => 42220,
539            NetworkName::Testnet => 11142220,
540        };
541        let resp = reqwest::get(format!("{base}/api/agent/agents/{chain_id}/{address}"))
542            .await
543            .map_err(|e| RegistrationError::Http(e.to_string()))?;
544        resp.json()
545            .await
546            .map_err(|e| RegistrationError::Http(e.to_string()))
547    }
548
549    /// Initiate deregistration for this agent via the REST API.
550    ///
551    /// Returns a [`DeregistrationSession`] with a QR code URL that the
552    /// human operator must scan with the Self app to confirm removal.
553    pub async fn request_deregistration(
554        &self,
555        api_base: Option<&str>,
556    ) -> Result<DeregistrationSession, RegistrationError> {
557        let network_str = match self.network_name {
558            NetworkName::Mainnet => "mainnet",
559            NetworkName::Testnet => "testnet",
560        };
561        DeregistrationSession::request(
562            DeregistrationRequest {
563                network: network_str.to_string(),
564                agent_address: format!("{:#x}", self.signer.address()),
565            },
566            api_base,
567        )
568        .await
569    }
570}
571
572/// Convert a 20-byte address to a 32-byte agent key (left zero-padded).
573/// Matches TS: `ethers.zeroPadValue(address, 32)`
574pub fn address_to_agent_key(address: Address) -> B256 {
575    let mut bytes = [0u8; 32];
576    bytes[12..32].copy_from_slice(address.as_ref());
577    FixedBytes(bytes)
578}
579
580/// Current time in milliseconds since Unix epoch.
581fn now_millis() -> u64 {
582    SystemTime::now()
583        .duration_since(UNIX_EPOCH)
584        .unwrap()
585        .as_millis() as u64
586}
587
588/// Returns `Some(s)` if non-empty, `None` otherwise.
589fn non_empty(s: &str) -> Option<String> {
590    if s.is_empty() {
591        None
592    } else {
593        Some(s.to_string())
594    }
595}
596
597/// Compute the signing message from request components.
598/// Exposed for use by the verifier.
599pub(crate) fn compute_signing_message(
600    timestamp: &str,
601    method: &str,
602    url: &str,
603    body: Option<&str>,
604) -> B256 {
605    let canonical_url = canonicalize_signing_url(url);
606    let body_text = body.unwrap_or("");
607    let body_hash = keccak256(body_text.as_bytes());
608    let body_hash_hex = format!("{:#x}", body_hash);
609    let concat = format!(
610        "{}{}{}{}",
611        timestamp,
612        method.to_uppercase(),
613        canonical_url,
614        body_hash_hex
615    );
616    keccak256(concat.as_bytes())
617}
618
619/// Canonical URL for signing/verification: path + optional query string.
620pub(crate) fn canonicalize_signing_url(url: &str) -> String {
621    if url.is_empty() {
622        return String::new();
623    }
624
625    if url.starts_with("http://") || url.starts_with("https://") {
626        if let Ok(parsed) = reqwest::Url::parse(url) {
627            let mut out = parsed.path().to_string();
628            if out.is_empty() {
629                out.push('/');
630            }
631            if let Some(query) = parsed.query() {
632                out.push('?');
633                out.push_str(query);
634            }
635            return out;
636        }
637        return url.to_string();
638    }
639
640    if url.starts_with('?') {
641        return format!("/{url}");
642    }
643    if url.starts_with('/') {
644        return url.to_string();
645    }
646
647    // Best effort for inputs like "api/data?x=1"
648    if let Ok(parsed) = reqwest::Url::parse(&format!("http://self.local/{url}")) {
649        let mut out = parsed.path().to_string();
650        if let Some(query) = parsed.query() {
651            out.push('?');
652            out.push_str(query);
653        }
654        return out;
655    }
656
657    url.to_string()
658}